or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

discovery.mdevents.mdexecution.mdframework.mdindex.mdlogging.md

discovery.mddocs/

0

# Test Discovery

1

2

Test discovery system using fingerprints to identify test classes through annotations or inheritance patterns, enabling automatic detection of test suites during SBT's discovery phase.

3

4

## Capabilities

5

6

### Fingerprint Base Interface

7

8

Base interface for test class identification patterns used during discovery.

9

10

```scala { .api }

11

/**

12

* A way to identify test classes and/or modules that should be discovered.

13

* Implementations may not rely on identity of Fingerprints since they are serialized between JS/JVM.

14

*/

15

trait Fingerprint

16

```

17

18

### AnnotatedFingerprint

19

20

Identifies test classes or modules based on the presence of specific annotations.

21

22

```scala { .api }

23

/**

24

* Indicates classes or modules with a specific annotation should be discovered as test classes.

25

* The annotation can be on top-level methods or on the class/module itself.

26

*/

27

trait AnnotatedFingerprint extends Fingerprint {

28

/**

29

* Indicates whether modules with the annotation should be considered during discovery.

30

* If a framework allows both classes and modules, return two different fingerprints:

31

* one with isModule() = false and another with isModule() = true.

32

* @return true for modules (singleton objects), false for classes

33

*/

34

def isModule(): Boolean

35

36

/**

37

* The fully qualified name of the annotation that identifies test classes/modules.

38

* @return annotation name for test identification

39

*/

40

def annotationName(): String

41

}

42

```

43

44

**Usage Examples:**

45

46

```scala

47

// Discover classes with @Test annotation

48

val classAnnotationFingerprint = new AnnotatedFingerprint {

49

def isModule() = false

50

def annotationName() = "org.junit.Test"

51

}

52

53

// Discover objects with @TestSuite annotation

54

val objectAnnotationFingerprint = new AnnotatedFingerprint {

55

def isModule() = true

56

def annotationName() = "com.example.TestSuite"

57

}

58

59

// Framework returning both fingerprints

60

class MyFramework extends Framework {

61

def fingerprints(): Array[Fingerprint] = Array(

62

classAnnotationFingerprint,

63

objectAnnotationFingerprint

64

)

65

}

66

```

67

68

### SubclassFingerprint

69

70

Identifies test classes or modules based on inheritance from specific superclasses or traits.

71

72

```scala { .api }

73

/**

74

* Indicates classes (and possibly modules) that extend a particular superclass

75

* or mix in a particular supertrait should be discovered as test classes.

76

*/

77

trait SubclassFingerprint extends Fingerprint {

78

/**

79

* Indicates whether modules (singleton objects) that extend the superclass/supertrait

80

* should be considered during discovery.

81

* Returning false speeds up discovery by quickly bypassing module classes.

82

* @return true to discover modules, false for classes only

83

*/

84

def isModule(): Boolean

85

86

/**

87

* The name of the superclass or supertrait that identifies test classes.

88

* @return superclass/supertrait name for test identification

89

*/

90

def superclassName(): String

91

92

/**

93

* Indicates whether discovered classes must have a no-arg constructor.

94

* If true, client should not discover subclasses without no-arg constructors.

95

* @return true if no-arg constructor required

96

*/

97

def requireNoArgConstructor(): Boolean

98

}

99

```

100

101

**Usage Examples:**

102

103

```scala

104

// Discover classes extending TestSuite (with no-arg constructor)

105

val suiteFingerprint = new SubclassFingerprint {

106

def isModule() = false

107

def superclassName() = "com.example.TestSuite"

108

def requireNoArgConstructor() = true

109

}

110

111

// Discover objects extending SpecBase (no constructor requirement for objects)

112

val specFingerprint = new SubclassFingerprint {

113

def isModule() = true

114

def superclassName() = "com.example.SpecBase"

115

def requireNoArgConstructor() = false

116

}

117

118

// Discover both classes and objects extending BaseTest

119

val flexibleFingerprint = new SubclassFingerprint {

120

def isModule() = false // or true for separate fingerprint

121

def superclassName() = "com.example.BaseTest"

122

def requireNoArgConstructor() = false

123

}

124

```

125

126

## Discovery Process

127

128

### Framework Fingerprint Registration

129

130

Test frameworks register their discovery patterns by returning fingerprints from the `fingerprints()` method:

131

132

```scala

133

class MyTestFramework extends Framework {

134

def fingerprints(): Array[Fingerprint] = Array(

135

// JUnit-style annotation discovery

136

new AnnotatedFingerprint {

137

def isModule() = false

138

def annotationName() = "org.junit.Test"

139

},

140

141

// ScalaTest-style inheritance discovery

142

new SubclassFingerprint {

143

def isModule() = false

144

def superclassName() = "org.scalatest.Suite"

145

def requireNoArgConstructor() = true

146

},

147

148

// Specs2-style object discovery

149

new SubclassFingerprint {

150

def isModule() = true

151

def superclassName() = "org.specs2.Specification"

152

def requireNoArgConstructor() = false

153

}

154

)

155

}

156

```

157

158

### Discovery Algorithm

159

160

SBT uses fingerprints to scan the classpath and identify test classes:

161

162

1. **Classpath Scanning**: SBT scans compiled classes in test directories

163

2. **Fingerprint Matching**: Each class/object checked against all registered fingerprints

164

3. **Annotation Checking**: For `AnnotatedFingerprint`, check for specified annotations

165

4. **Inheritance Checking**: For `SubclassFingerprint`, check superclass hierarchy

166

5. **Constructor Validation**: If `requireNoArgConstructor()` is true, verify no-arg constructor exists

167

6. **TaskDef Creation**: Create `TaskDef` instances for discovered test classes

168

7. **Task Creation**: Pass `TaskDef` array to `Runner.tasks()` for task creation

169

170

### Multiple Fingerprints

171

172

Frameworks can register multiple fingerprints to support different discovery patterns:

173

174

```scala

175

def fingerprints(): Array[Fingerprint] = Array(

176

// Method-level annotations

177

new AnnotatedFingerprint {

178

def isModule() = false

179

def annotationName() = "com.example.Test"

180

},

181

182

// Class-level annotations

183

new AnnotatedFingerprint {

184

def isModule() = false

185

def annotationName() = "com.example.TestClass"

186

},

187

188

// Inheritance-based discovery

189

new SubclassFingerprint {

190

def isModule() = false

191

def superclassName() = "com.example.BaseTest"

192

def requireNoArgConstructor() = true

193

},

194

195

// Object-based specs

196

new SubclassFingerprint {

197

def isModule() = true

198

def superclassName() = "com.example.Spec"

199

def requireNoArgConstructor() = false // Objects don't have constructors

200

}

201

)

202

```

203

204

### Discovery Best Practices

205

206

**Performance Optimization:**

207

- Set `isModule() = false` for class-only frameworks to skip object scanning

208

- Use `requireNoArgConstructor() = true` to filter classes early

209

- Keep `superclassName()` and `annotationName()` as specific as possible

210

211

**Flexibility:**

212

- Support both classes and objects by providing separate fingerprints

213

- Consider annotation-based and inheritance-based patterns

214

- Allow for different constructor requirements

215

216

**Framework Compatibility:**

217

- Use standard annotation names when possible (e.g., JUnit annotations)

218

- Follow common inheritance patterns (e.g., ScalaTest Suite)

219

- Support both new and legacy test patterns

220

221

```scala

222

// Comprehensive discovery for flexible framework

223

class FlexibleFramework extends Framework {

224

def fingerprints(): Array[Fingerprint] = Array(

225

// JUnit compatibility

226

new AnnotatedFingerprint {

227

def isModule() = false

228

def annotationName() = "org.junit.Test"

229

},

230

231

// Custom annotation support

232

new AnnotatedFingerprint {

233

def isModule() = false

234

def annotationName() = "com.myframework.Test"

235

},

236

237

// Suite-based classes

238

new SubclassFingerprint {

239

def isModule() = false

240

def superclassName() = "com.myframework.TestSuite"

241

def requireNoArgConstructor() = true

242

},

243

244

// Spec-based objects

245

new SubclassFingerprint {

246

def isModule() = true

247

def superclassName() = "com.myframework.Spec"

248

def requireNoArgConstructor() = false

249

}

250

)

251

}

252

```

253

254

## Error Handling

255

256

### Discovery Failures

257

258

Discovery typically handles errors gracefully by skipping problematic classes:

259

260

```scala

261

// Framework should handle discovery errors internally

262

def fingerprints(): Array[Fingerprint] = {

263

try {

264

Array(

265

createAnnotationFingerprint(),

266

createSubclassFingerprint()

267

)

268

} catch {

269

case _: ClassNotFoundException =>

270

// Fallback to basic discovery

271

Array(createBasicFingerprint())

272

}

273

}

274

```

275

276

### Validation

277

278

Fingerprints should validate their configuration:

279

280

```scala

281

class MyAnnotatedFingerprint(annotation: String) extends AnnotatedFingerprint {

282

require(annotation != null && annotation.nonEmpty, "Annotation name cannot be empty")

283

284

def annotationName() = annotation

285

def isModule() = false

286

}

287

```