or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

configuration.mdindex.mdplugin-development.mdtest-discovery.mdtest-tools.md

plugin-development.mddocs/

0

# Plugin Development

1

2

Framework for creating custom plugins that extend nose2's functionality through a comprehensive hook system and event-driven architecture. Plugins can modify behavior at every stage of test execution.

3

4

## Capabilities

5

6

### Plugin Base Class

7

8

All nose2 plugins must inherit from the Plugin base class and implement hook methods to extend functionality.

9

10

```python { .api }

11

class Plugin:

12

"""

13

Base class for nose2 plugins.

14

15

All nose2 plugins must subclass this class and implement hook methods

16

to extend test execution functionality.

17

"""

18

19

# Class attributes for plugin configuration

20

commandLineSwitch: tuple # (short_opt, long_opt, help_text)

21

configSection: str # Config file section name

22

alwaysOn: bool # Auto-register plugin flag

23

24

# Instance attributes set during initialization

25

session: Session # Test run session

26

config: Config # Plugin configuration section

27

28

def __init__(self, **kwargs):

29

"""

30

Initialize plugin.

31

32

Config values should be extracted from self.config in __init__

33

for sphinx documentation generation to work properly.

34

"""

35

36

def register(self):

37

"""

38

Register plugin with session hooks.

39

40

Called automatically if alwaysOn=True or command line switch is used.

41

"""

42

43

def addOption(self, callback, short_opt, long_opt, help_text=None, nargs=0):

44

"""

45

Add command line option.

46

47

Parameters:

48

- callback: Function to call when option is used, or list to append values to

49

- short_opt: Single character short option (must be uppercase, no dashes)

50

- long_opt: Long option name (without dashes)

51

- help_text: Help text for option

52

- nargs: Number of arguments (default: 0)

53

"""

54

55

def addArgument(self, callback, short_opt, long_opt, help_text=None):

56

"""

57

Add command line option that takes one argument.

58

59

Parameters:

60

- callback: Function to call when option is used (receives one argument)

61

- short_opt: Single character short option (must be uppercase, no dashes)

62

- long_opt: Long option name (without dashes)

63

- help_text: Help text for option

64

"""

65

```

66

67

### Session Management

68

69

The Session class coordinates plugin loading, configuration, and execution.

70

71

```python { .api }

72

class Session:

73

"""

74

Configuration session that encapsulates all configuration for a test run.

75

"""

76

77

# Core attributes

78

argparse: argparse.ArgumentParser # Command line parser

79

pluginargs: argparse.ArgumentGroup # Plugin argument group

80

hooks: PluginInterface # Plugin hook interface

81

plugins: list # List of loaded plugins

82

config: ConfigParser # Configuration parser

83

84

# Test run configuration

85

verbosity: int # Verbosity level

86

startDir: str # Test discovery start directory

87

topLevelDir: str # Top-level project directory

88

testResult: PluggableTestResult # Test result instance

89

testLoader: PluggableTestLoader # Test loader instance

90

logLevel: int # Logging level

91

92

def __init__(self):

93

"""Initialize session with default configuration."""

94

95

def get(self, section):

96

"""

97

Get a config section.

98

99

Parameters:

100

- section: The section name to retrieve

101

102

Returns:

103

Config instance for the section

104

"""

105

106

def loadConfigFiles(self, *filenames):

107

"""

108

Load configuration from files.

109

110

Parameters:

111

- filenames: Configuration file paths to load

112

"""

113

114

def loadPlugins(self, plugins, exclude):

115

"""

116

Load plugins into the session.

117

118

Parameters:

119

- plugins: List of plugin module names to load

120

- exclude: List of plugin module names to exclude

121

"""

122

123

def setVerbosity(self, verbosity, verbose, quiet):

124

"""

125

Set verbosity level from configuration and command line.

126

127

Parameters:

128

- verbosity: Base verbosity level

129

- verbose: Number of -v flags

130

- quiet: Number of -q flags

131

"""

132

133

def isPluginLoaded(self, plugin_name):

134

"""

135

Check if a plugin is loaded.

136

137

Parameters:

138

- plugin_name: Full plugin module name

139

140

Returns:

141

True if plugin is loaded, False otherwise

142

"""

143

```

144

145

### Plugin Interface and Hooks

146

147

The PluginInterface provides the hook system for plugin method calls.

148

149

```python { .api }

150

class PluginInterface:

151

"""Interface for plugin method hooks."""

152

153

def register(self, method_name, plugin):

154

"""

155

Register a plugin method for a hook.

156

157

Parameters:

158

- method_name: Name of hook method

159

- plugin: Plugin instance to register

160

"""

161

162

# Hook methods (examples - many more available)

163

def loadTestsFromModule(self, event):

164

"""Called when loading tests from a module."""

165

166

def loadTestsFromName(self, event):

167

"""Called when loading tests from a name."""

168

169

def startTest(self, event):

170

"""Called when a test starts."""

171

172

def stopTest(self, event):

173

"""Called when a test stops."""

174

175

def testOutcome(self, event):

176

"""Called when a test completes with an outcome."""

177

178

def createTests(self, event):

179

"""Called to create the top-level test suite."""

180

181

def runnerCreated(self, event):

182

"""Called when test runner is created."""

183

```

184

185

### Event Classes

186

187

Events carry information between the test framework and plugins.

188

189

```python { .api }

190

class Event:

191

"""Base class for all plugin events."""

192

193

handled: bool # Set to True if plugin handles the event

194

195

class LoadFromModuleEvent(Event):

196

"""Event fired when loading tests from a module."""

197

198

def __init__(self, loader, module):

199

self.loader = loader

200

self.module = module

201

self.extraTests = []

202

203

class StartTestEvent(Event):

204

"""Event fired when a test starts."""

205

206

def __init__(self, test, result, startTime):

207

self.test = test

208

self.result = result

209

self.startTime = startTime

210

211

class TestOutcomeEvent(Event):

212

"""Event fired when a test completes."""

213

214

def __init__(self, test, result, outcome, err=None, reason=None, expected=None):

215

self.test = test

216

self.result = result

217

self.outcome = outcome # 'error', 'failed', 'skipped', 'passed', 'subtest'

218

self.err = err

219

self.reason = reason

220

self.expected = expected

221

```

222

223

## Usage Examples

224

225

### Simple Plugin

226

227

```python

228

from nose2.events import Plugin

229

230

class TimingPlugin(Plugin):

231

"""Plugin that times test execution."""

232

233

configSection = 'timing'

234

commandLineSwitch = ('T', 'timing', 'Time test execution')

235

236

def __init__(self):

237

# Extract config values in __init__

238

self.enabled = self.config.as_bool('enabled', default=True)

239

self.threshold = self.config.as_float('threshold', default=1.0)

240

self.times = {}

241

242

def startTest(self, event):

243

"""Record test start time."""

244

import time

245

self.times[event.test] = time.time()

246

247

def stopTest(self, event):

248

"""Calculate and report test time."""

249

import time

250

if event.test in self.times:

251

elapsed = time.time() - self.times[event.test]

252

if elapsed > self.threshold:

253

print(f"SLOW: {event.test} took {elapsed:.2f}s")

254

del self.times[event.test]

255

```

256

257

### Configuration-Based Plugin

258

259

```python

260

from nose2.events import Plugin

261

262

class DatabasePlugin(Plugin):

263

"""Plugin for database test setup."""

264

265

configSection = 'database'

266

alwaysOn = True # Always load this plugin

267

268

def __init__(self):

269

# Read configuration

270

self.db_url = self.config.as_str('url', default='sqlite:///:memory:')

271

self.reset_db = self.config.as_bool('reset', default=True)

272

self.fixtures = self.config.as_list('fixtures', default=[])

273

274

# Add command line options

275

self.addArgument('--db-url', help='Database URL')

276

self.addArgument('--no-db-reset', action='store_true',

277

help='Skip database reset')

278

279

def handleArgs(self, event):

280

"""Handle command line arguments."""

281

args = event.args

282

if hasattr(args, 'db_url') and args.db_url:

283

self.db_url = args.db_url

284

if hasattr(args, 'no_db_reset') and args.no_db_reset:

285

self.reset_db = False

286

287

def createTests(self, event):

288

"""Set up database before creating tests."""

289

if self.reset_db:

290

self.setup_database()

291

292

def setup_database(self):

293

"""Initialize database with fixtures."""

294

# Database setup code

295

pass

296

```

297

298

### Test Loader Plugin

299

300

```python

301

from nose2.events import Plugin

302

303

class CustomLoaderPlugin(Plugin):

304

"""Custom test loader plugin."""

305

306

configSection = 'custom-loader'

307

commandLineSwitch = ('C', 'custom-loader', 'Use custom test loader')

308

309

def __init__(self):

310

self.pattern = self.config.as_str('pattern', default='spec_*.py')

311

self.base_class = self.config.as_str('base_class', default='Specification')

312

313

def loadTestsFromModule(self, event):

314

"""Load tests using custom pattern."""

315

module = event.module

316

tests = []

317

318

# Custom loading logic

319

for name in dir(module):

320

obj = getattr(module, name)

321

if (isinstance(obj, type) and

322

issubclass(obj, unittest.TestCase) and

323

obj.__name__.startswith(self.base_class)):

324

325

suite = self.session.testLoader.loadTestsFromTestCase(obj)

326

tests.append(suite)

327

328

if tests:

329

event.extraTests.extend(tests)

330

```

331

332

### Result Plugin

333

334

```python

335

from nose2.events import Plugin

336

337

class CustomResultPlugin(Plugin):

338

"""Custom test result reporter."""

339

340

configSection = 'custom-result'

341

342

def __init__(self):

343

self.output_file = self.config.as_str('output', default='results.txt')

344

self.include_passed = self.config.as_bool('include_passed', default=False)

345

self.results = []

346

347

def testOutcome(self, event):

348

"""Record test outcomes."""

349

test_info = {

350

'test': str(event.test),

351

'outcome': event.outcome,

352

'error': str(event.err) if event.err else None,

353

'reason': event.reason

354

}

355

356

if event.outcome != 'passed' or self.include_passed:

357

self.results.append(test_info)

358

359

def afterTestRun(self, event):

360

"""Write results to file after test run."""

361

with open(self.output_file, 'w') as f:

362

for result in self.results:

363

f.write(f"{result['test']}: {result['outcome']}\n")

364

if result['error']:

365

f.write(f" Error: {result['error']}\n")

366

if result['reason']:

367

f.write(f" Reason: {result['reason']}\n")

368

```

369

370

### Plugin Registration

371

372

```python

373

# In your plugin module (e.g., my_plugins.py)

374

from nose2.events import Plugin

375

376

class MyPlugin(Plugin):

377

configSection = 'my-plugin'

378

379

def __init__(self):

380

pass

381

382

def startTest(self, event):

383

print(f"Starting test: {event.test}")

384

385

# To use the plugin:

386

# 1. Via command line: nose2 --plugin my_plugins.MyPlugin

387

# 2. Via config file:

388

# [unittest]

389

# plugins = my_plugins.MyPlugin

390

# 3. Via programmatic loading:

391

# from nose2.main import PluggableTestProgram

392

# PluggableTestProgram(plugins=['my_plugins.MyPlugin'])

393

```

394

395

### Plugin Configuration

396

397

```ini

398

# unittest.cfg or nose2.cfg

399

[unittest]

400

plugins = my_plugins.TimingPlugin

401

my_plugins.DatabasePlugin

402

403

[timing]

404

enabled = true

405

threshold = 0.5

406

407

[database]

408

url = postgresql://localhost/test_db

409

reset = true

410

fixtures = users.json

411

products.json

412

413

[my-plugin]

414

some_option = value

415

flag = true

416

```

417

418

### Advanced Plugin Patterns

419

420

```python

421

from nose2.events import Plugin

422

423

class LayeredPlugin(Plugin):

424

"""Plugin that works with test layers."""

425

426

def startTestRun(self, event):

427

"""Called at the start of the test run."""

428

self.setup_resources()

429

430

def stopTestRun(self, event):

431

"""Called at the end of the test run."""

432

self.cleanup_resources()

433

434

def startTestClass(self, event):

435

"""Called when starting tests in a test class."""

436

if hasattr(event.testClass, 'layer'):

437

self.setup_layer(event.testClass.layer)

438

439

def stopTestClass(self, event):

440

"""Called when finishing tests in a test class."""

441

if hasattr(event.testClass, 'layer'):

442

self.teardown_layer(event.testClass.layer)

443

444

def setup_resources(self):

445

"""Set up resources for entire test run."""

446

pass

447

448

def cleanup_resources(self):

449

"""Clean up resources after test run."""

450

pass

451

452

def setup_layer(self, layer):

453

"""Set up resources for a test layer."""

454

pass

455

456

def teardown_layer(self, layer):

457

"""Tear down resources for a test layer."""

458

pass

459

```