@yamf/test 0.1.3 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +49 -5
- package/package.json +5 -4
- package/src/assertion-errors.js +1 -1
- package/src/cli.js +7 -49
- package/src/example-helpers.js +29 -0
- package/src/helpers.js +95 -26
- package/src/index.js +4 -1
- package/src/runner.js +55 -18
- package/src/tests/terminate-after-tests.js +64 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2017 mcbrumagin
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
A minimal, zero-dependency testing library with polymorphic async/await and simultaneous assertion reports.
|
|
4
4
|
|
|
5
|
-
[]()
|
|
6
6
|
[]()
|
|
7
7
|
|
|
8
8
|
## Features
|
|
@@ -46,6 +46,10 @@ runTests({ testAddition, testAsyncFetch })
|
|
|
46
46
|
|
|
47
47
|
Assert that a value or function result passes all assertion functions.
|
|
48
48
|
|
|
49
|
+
**Failure output:** the runner prints the distilled **first argument** (`for target … value = …`) and each failing predicate (`failed -> …`). Put the value under test first and express expectations in the callbacks — avoid `assert(expr, x => x === true)` where `expr` is already a boolean, or failures only show `true`/`false` with no useful predicate text.
|
|
50
|
+
|
|
51
|
+
**Errors:** for `Error` values (e.g. a caught `execSync` failure), use **`assertErr(error, …)`** instead of `assert` — see below.
|
|
52
|
+
|
|
49
53
|
```javascript
|
|
50
54
|
// Direct value
|
|
51
55
|
assert(5, n => n > 0, n => n < 10)
|
|
@@ -248,16 +252,52 @@ import { registryServer, createService } from '@yamf/core'
|
|
|
248
252
|
|
|
249
253
|
async function testServices() {
|
|
250
254
|
await terminateAfter(
|
|
251
|
-
registryServer(),
|
|
252
|
-
createService(
|
|
255
|
+
() => registryServer(),
|
|
256
|
+
() => createService('my-service', (p) => ({ ok: true })),
|
|
253
257
|
async (registry, service) => {
|
|
254
|
-
const result = await callService('
|
|
258
|
+
const result = await callService('my-service', {})
|
|
255
259
|
assert(result.ok, v => v === true)
|
|
256
260
|
}
|
|
257
261
|
)
|
|
258
262
|
}
|
|
259
263
|
```
|
|
260
264
|
|
|
265
|
+
**Single-callback form** — when the test only needs a registry (and services register against it), you can pass one async function; teardown uses `terminateActiveRegistryServers()` from `@yamf/core` (no need to return the registry instance):
|
|
266
|
+
|
|
267
|
+
```javascript
|
|
268
|
+
export async function myTestCaseFunction() {
|
|
269
|
+
await terminateAfter(async () => {
|
|
270
|
+
await registryServer()
|
|
271
|
+
await createService('my-service', (p) => ({ ok: true }))
|
|
272
|
+
const result = await callService('my-service', {})
|
|
273
|
+
assert(result.ok, (v) => v === true)
|
|
274
|
+
})
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Prefer **named `async function …`** for that body (and for multi-arg factory thunks) so stack traces read clearly; avoid `() =>` wrappers for routine registry/service setup.
|
|
279
|
+
|
|
280
|
+
### `withInlineRegistry(...factories, testFn)`
|
|
281
|
+
|
|
282
|
+
Shorthand for `terminateAfter(() => registryServer(), ...)`.
|
|
283
|
+
|
|
284
|
+
### `pickListenPort()`
|
|
285
|
+
|
|
286
|
+
Returns a free TCP port on `127.0.0.1`. Use with `withEnv({ YAMF_REGISTRY_URL: \`http://127.0.0.1:${port}\` }, …)` when fixed ports from `.env.test` conflict (e.g. parallel runs or drain races).
|
|
287
|
+
|
|
288
|
+
```javascript
|
|
289
|
+
import { pickListenPort, withEnv, terminateAfter } from '@yamf/test'
|
|
290
|
+
import { registryServer } from '@yamf/core'
|
|
291
|
+
|
|
292
|
+
const port = await pickListenPort()
|
|
293
|
+
await withEnv({ YAMF_REGISTRY_URL: `http://127.0.0.1:${port}` }, async () => {
|
|
294
|
+
await terminateAfter(
|
|
295
|
+
() => registryServer(),
|
|
296
|
+
async () => { /* … */ }
|
|
297
|
+
)
|
|
298
|
+
})
|
|
299
|
+
```
|
|
300
|
+
|
|
261
301
|
### `withEnv(envVars, testFn)`
|
|
262
302
|
|
|
263
303
|
Run a test with temporary environment variables that are restored after.
|
|
@@ -277,6 +317,8 @@ async function testWithEnv() {
|
|
|
277
317
|
}
|
|
278
318
|
```
|
|
279
319
|
|
|
320
|
+
**YAMF integration tests:** Prefer defaults from the suite’s `.env.test` and `terminateAfter` for cleanup. Use `withEnv` only when a case must *change* the environment (missing vars, feature flags, per-test secrets, etc.). See [../../docs/TESTING.md](../../docs/TESTING.md) in the YAMF repo.
|
|
321
|
+
|
|
280
322
|
## Solo and Mute Flags
|
|
281
323
|
|
|
282
324
|
Focus on specific tests or skip others during development.
|
|
@@ -361,7 +403,7 @@ function TODOtestNewFeature() {
|
|
|
361
403
|
✔ testDirectValue (1ms)
|
|
362
404
|
✔ testFunctionResult (0ms)
|
|
363
405
|
✔ testAsyncFunction (15ms)
|
|
364
|
-
✘ testFailingAssertion
|
|
406
|
+
✘ testFailingAssertion (12ms)
|
|
365
407
|
|
|
366
408
|
----- Testing Complete -----
|
|
367
409
|
✔ ✔ ✔ Success Report ✔ ✔ ✔
|
|
@@ -385,6 +427,8 @@ testFailingAssertion failed with error: AssertionFailure: ...
|
|
|
385
427
|
ℹ duration_ms 42
|
|
386
428
|
```
|
|
387
429
|
|
|
430
|
+
Each `✔` / `✘` line includes wall time for that test. For a **slowest-first table** (e.g. profiling), run `yamf test --timings` or set `YAMF_TEST_TIMINGS=true`, then use `-f` / `-n` to focus one file or test: `yamf test -d packages/cli -f cli-rolling --timings -n testRestart`.
|
|
431
|
+
|
|
388
432
|
## API Reference
|
|
389
433
|
|
|
390
434
|
### Assertions
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yamf/test",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Functional testing module - with polymorphic async/await and simultaneous assertion reports",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"./utils": "./src/utils.js"
|
|
12
12
|
},
|
|
13
13
|
"engines": {
|
|
14
|
-
"node": ">=
|
|
14
|
+
"node": ">=22.0.0"
|
|
15
15
|
},
|
|
16
16
|
"keywords": [
|
|
17
17
|
"testing",
|
|
@@ -31,9 +31,10 @@
|
|
|
31
31
|
"url": "https://github.com/mcbrumagin/yamf"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
|
-
"@yamf/core": "
|
|
34
|
+
"@yamf/core": "0.9.0"
|
|
35
35
|
},
|
|
36
36
|
"scripts": {
|
|
37
|
-
"test": "
|
|
37
|
+
"test": "yamf test -d .",
|
|
38
|
+
"test:all": "yamf test -d . --include-e2e"
|
|
38
39
|
}
|
|
39
40
|
}
|
package/src/assertion-errors.js
CHANGED
|
@@ -38,7 +38,7 @@ export class AssertionFailureDetail {
|
|
|
38
38
|
childMessages = childMessages || ''
|
|
39
39
|
return `${pad}for target (${type}) value = ${val}`
|
|
40
40
|
+ this.assertFns.map(fn =>
|
|
41
|
-
`\n${pad}failed -> ${fn?.name || fn.toString().replace(/^\s
|
|
41
|
+
`\n${pad}failed -> ${fn?.name || fn.toString().replace(/^\s?.+?\=\>\s?/, '')}`
|
|
42
42
|
).join('')
|
|
43
43
|
+ childMessages
|
|
44
44
|
}
|
package/src/cli.js
CHANGED
|
@@ -1,49 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
---TODO determine features and flags from the following suggested list---
|
|
9
|
-
|
|
10
|
-
Essential test CLI options and flags vary significantly depending on the specific testing framework or tool being used. However, common categories of options and flags found across many testing CLIs include:
|
|
11
|
-
Execution Control:
|
|
12
|
-
|
|
13
|
-
Running specific tests:
|
|
14
|
-
--test-name <name> or similar: Executes a single test or a group of tests matching a pattern.
|
|
15
|
-
--grep <pattern>: Filters tests based on a regular expression pattern in their names.
|
|
16
|
-
--file <path>: Runs tests only from specified files.
|
|
17
|
-
Controlling execution flow:
|
|
18
|
-
--fail-fast or --first: Stops test execution immediately upon the first failure.
|
|
19
|
-
--bail: Similar to fail-fast, often with more granular control over how many failures are tolerated before stopping.
|
|
20
|
-
--retries <n>: Reruns failed tests a specified number of times.
|
|
21
|
-
|
|
22
|
-
Output and Reporting:
|
|
23
|
-
|
|
24
|
-
Verbosity levels:
|
|
25
|
-
--verbose or -v: Provides more detailed output during test execution.
|
|
26
|
-
--quiet or -q: Suppresses most output, showing only essential information like summaries.
|
|
27
|
-
Reporting formats:
|
|
28
|
-
--reporter <format>: Specifies the output format for test results (e.g., json, junit, html).
|
|
29
|
-
--output <file>: Redirects test output to a specified file.
|
|
30
|
-
Code coverage:
|
|
31
|
-
--coverage: Enables code coverage analysis and reporting.
|
|
32
|
-
--coverage-report <format>: Specifies the format for coverage reports.
|
|
33
|
-
|
|
34
|
-
Configuration and Setup:
|
|
35
|
-
|
|
36
|
-
Configuration files:
|
|
37
|
-
--config <file>: Specifies an alternative configuration file for the test runner.
|
|
38
|
-
Environment variables:
|
|
39
|
-
--env <key=value>: Sets environment variables for the test process.
|
|
40
|
-
Setup/Teardown scripts:
|
|
41
|
-
--pre-flight <script>: Runs a script or command before any tests execute.
|
|
42
|
-
--post-flight <script>: Runs a script or command after all tests have completed.
|
|
43
|
-
|
|
44
|
-
Debugging and Development:
|
|
45
|
-
|
|
46
|
-
Debugging tools:
|
|
47
|
-
--inspect or --debug: Enables debugging features, often allowing attachment of a debugger.
|
|
48
|
-
--watch: Reruns tests automatically when relevant files change.
|
|
49
|
-
*/
|
|
1
|
+
/**
|
|
2
|
+
* The yamf CLI has moved to @yamf/cli.
|
|
3
|
+
*
|
|
4
|
+
* Run tests with: yamf test
|
|
5
|
+
*
|
|
6
|
+
* Install: pnpm add -D @yamf/cli
|
|
7
|
+
*/
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { createServer } from 'node:net'
|
|
2
|
+
import { registryServer } from '@yamf/core'
|
|
3
|
+
import { terminateAfter } from './helpers.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Returns an ephemeral TCP port on 127.0.0.1 for examples/tests that must avoid
|
|
7
|
+
* fixed ports (EADDRINUSE / drain races).
|
|
8
|
+
* @returns {Promise<number>}
|
|
9
|
+
*/
|
|
10
|
+
export async function pickListenPort () {
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
const s = createServer()
|
|
13
|
+
s.once('error', reject)
|
|
14
|
+
s.listen(0, '127.0.0.1', () => {
|
|
15
|
+
const addr = s.address()
|
|
16
|
+
const port = typeof addr === 'object' && addr ? addr.port : null
|
|
17
|
+
s.close((err) => (err ? reject(err) : resolve(port)))
|
|
18
|
+
})
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Run a test body with a local registry and optional service factories (thunks).
|
|
24
|
+
* Same argument order as {@link terminateAfter}: `() => registryServer()`, `() => createX()`, …, `async (reg, x) => {}`.
|
|
25
|
+
* @param {...(function|function[])} serviceFactoriesAndTestFn
|
|
26
|
+
*/
|
|
27
|
+
export async function withInlineRegistry (...args) {
|
|
28
|
+
return terminateAfter(() => registryServer(), ...args)
|
|
29
|
+
}
|
package/src/helpers.js
CHANGED
|
@@ -1,39 +1,107 @@
|
|
|
1
|
-
import { Logger, envConfig } from '@yamf/core'
|
|
1
|
+
import { Logger, envConfig, envTruthy, terminateActiveRegistryServers } from '@yamf/core'
|
|
2
2
|
|
|
3
3
|
const logger = new Logger()
|
|
4
4
|
|
|
5
5
|
export const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
let
|
|
7
|
+
function isThenable (x) {
|
|
8
|
+
return x != null && typeof x.then === 'function'
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async function terminateAfterSingleFn (fn) {
|
|
12
|
+
let bodyError
|
|
13
13
|
try {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
await fn()
|
|
15
|
+
} catch (e) {
|
|
16
|
+
bodyError = e
|
|
17
|
+
} finally {
|
|
18
|
+
try {
|
|
19
|
+
await terminateActiveRegistryServers()
|
|
20
|
+
} catch (e) {
|
|
21
|
+
if (process.env.YAMF_TEST_VERBOSE_TEARDOWN != null && envTruthy(process.env.YAMF_TEST_VERBOSE_TEARDOWN)) {
|
|
22
|
+
console.warn(`[terminateAfter] registry shutdown failed: ${e.message}`)
|
|
20
23
|
}
|
|
21
24
|
}
|
|
25
|
+
}
|
|
26
|
+
if (bodyError) throw bodyError
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Start servers, run the test, then terminate in a safe order (non-registry first, registry last).
|
|
31
|
+
*
|
|
32
|
+
* **Evaluation order (JavaScript):** `terminateAfter(() => registry(), () => create(), testFn)` *starts* all
|
|
33
|
+
* async tasks while building the argument list, before `terminateAfter` runs—so a later `createRoute()`
|
|
34
|
+
* can hit the registry before it is listening. **Pass thunks (zero-arg functions) so work starts
|
|
35
|
+
* in sequence** inside this helper, e.g. `terminateAfter(() => registryServer(), () => createService('x', fn), testFn)`.
|
|
36
|
+
* You can still pass an already-started Promise (a thenable) when you need parallel start
|
|
37
|
+
* outside this helper's sequential `for` loop.
|
|
38
|
+
*
|
|
39
|
+
* - Each server item is either: a **thenable** (awaited as-is), or a **function** (called with no
|
|
40
|
+
* args; the return is awaited, then optional array flattening applies as below).
|
|
41
|
+
* - A single `Promise.all([...])` can be one argument: `() => Promise.all([...])` as a thunk, or
|
|
42
|
+
* pass the Promise if you need parallel start outside the sequential `for` loop.
|
|
43
|
+
*
|
|
44
|
+
* @param {...(Promise<unknown> | (() => unknown) | unknown | unknown[])} serverFns
|
|
45
|
+
* Each item may be a thenable, a no-arg factory, a value, or (after await) a non-empty `Array` of
|
|
46
|
+
* server-like objects.
|
|
47
|
+
* @param {Function} testFn
|
|
48
|
+
*/
|
|
49
|
+
export async function terminateAfter (...args) {
|
|
50
|
+
if (args.length === 1 && typeof args[0] === 'function') {
|
|
51
|
+
return await terminateAfterSingleFn(args[0])
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const testFn = args[args.length - 1]
|
|
55
|
+
const serverInputs = args.slice(0, -1)
|
|
56
|
+
if (typeof testFn !== 'function') {
|
|
57
|
+
throw new Error('terminateAfter last argument must be a function')
|
|
58
|
+
}
|
|
22
59
|
|
|
23
|
-
|
|
24
|
-
|
|
60
|
+
const flatServers = []
|
|
61
|
+
try {
|
|
62
|
+
for (const item of serverInputs) {
|
|
63
|
+
let toAwait
|
|
64
|
+
if (isThenable(item)) {
|
|
65
|
+
toAwait = item
|
|
66
|
+
} else if (typeof item === 'function') {
|
|
67
|
+
const out = item()
|
|
68
|
+
toAwait = isThenable(out) ? out : Promise.resolve(out)
|
|
69
|
+
} else {
|
|
70
|
+
toAwait = Promise.resolve(item)
|
|
71
|
+
}
|
|
72
|
+
const resolved = await toAwait
|
|
73
|
+
if (Array.isArray(resolved) && resolved.length > 0) {
|
|
74
|
+
for (const s of resolved) {
|
|
75
|
+
flatServers.push(s)
|
|
76
|
+
}
|
|
77
|
+
} else {
|
|
78
|
+
flatServers.push(resolved)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return await testFn(...flatServers)
|
|
25
82
|
} finally {
|
|
26
|
-
|
|
83
|
+
const registryIndex = flatServers.findIndex(s => s && s.isRegistry)
|
|
27
84
|
if (registryIndex > -1) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
for (
|
|
31
|
-
|
|
32
|
-
|
|
85
|
+
const registryServer_ = flatServers[registryIndex]
|
|
86
|
+
const otherServers = flatServers.filter((_, i) => i !== registryIndex)
|
|
87
|
+
for (const server of otherServers) {
|
|
88
|
+
if (!server) {
|
|
89
|
+
continue
|
|
90
|
+
}
|
|
91
|
+
await server.terminate()
|
|
92
|
+
logger.info(`terminated server ${server.name} at port ${server.port}`)
|
|
33
93
|
}
|
|
34
|
-
await
|
|
35
|
-
logger.info(`terminated registry server at port ${
|
|
36
|
-
} else
|
|
94
|
+
await registryServer_?.terminate()
|
|
95
|
+
logger.info(`terminated registry server at port ${registryServer_?.port}`)
|
|
96
|
+
} else {
|
|
97
|
+
for (const server of flatServers) {
|
|
98
|
+
if (!server) {
|
|
99
|
+
continue
|
|
100
|
+
}
|
|
101
|
+
await server.terminate()
|
|
102
|
+
logger.info(`terminated server ${server.name} at port ${server.port}`)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
37
105
|
}
|
|
38
106
|
}
|
|
39
107
|
|
|
@@ -42,8 +110,9 @@ function setEnv(key, value) {
|
|
|
42
110
|
delete process.env[key]
|
|
43
111
|
envConfig.config.delete(key)
|
|
44
112
|
} else {
|
|
45
|
-
|
|
46
|
-
|
|
113
|
+
const str = String(value)
|
|
114
|
+
process.env[key] = str
|
|
115
|
+
envConfig.set(key, envConfig.parseValue(str))
|
|
47
116
|
}
|
|
48
117
|
}
|
|
49
118
|
|
package/src/index.js
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
terminateAfter,
|
|
21
21
|
withEnv
|
|
22
22
|
} from './helpers.js'
|
|
23
|
+
import { withInlineRegistry, pickListenPort } from './example-helpers.js'
|
|
23
24
|
|
|
24
25
|
import runTests, {
|
|
25
26
|
mergeAllTestsSafely,
|
|
@@ -45,5 +46,7 @@ export {
|
|
|
45
46
|
runTests,
|
|
46
47
|
runTestFnsSequentially,
|
|
47
48
|
TestRunner,
|
|
48
|
-
withEnv
|
|
49
|
+
withEnv,
|
|
50
|
+
withInlineRegistry,
|
|
51
|
+
pickListenPort
|
|
49
52
|
}
|
package/src/runner.js
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
26
|
import crypto from 'crypto'
|
|
27
|
-
import { Logger } from '@yamf/core'
|
|
27
|
+
import { Logger, envConfig, envTruthy } from '@yamf/core'
|
|
28
28
|
|
|
29
29
|
const logger = new Logger({ includeLogLineNumbers: false })
|
|
30
30
|
|
|
@@ -91,6 +91,8 @@ export async function runTestFnsSequentially(testFns) {
|
|
|
91
91
|
let failedCases = []
|
|
92
92
|
let todoCases = []
|
|
93
93
|
let testSuites = {}
|
|
94
|
+
/** @type {{ name: string, durationMs: number, ok: boolean }[]} */
|
|
95
|
+
let testTimings = []
|
|
94
96
|
|
|
95
97
|
// Support arrays or objects
|
|
96
98
|
testFns = Array.isArray(testFns) ? testFns : Object.values(testFns)
|
|
@@ -122,10 +124,24 @@ export async function runTestFnsSequentially(testFns) {
|
|
|
122
124
|
if (fn.suite && !testSuites[fn.suite]) {
|
|
123
125
|
testSuites[fn.suite] = true
|
|
124
126
|
}
|
|
127
|
+
const startTime = Date.now()
|
|
125
128
|
try {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
+
const timeoutRaw = process.env.YAMF_TEST_CASE_TIMEOUT_MS
|
|
130
|
+
const timeoutMs = timeoutRaw != null && timeoutRaw !== '' ? Number(timeoutRaw) : NaN
|
|
131
|
+
const useTimeout = Number.isFinite(timeoutMs) && timeoutMs > 0
|
|
132
|
+
if (useTimeout) {
|
|
133
|
+
await new Promise((resolve, reject) => {
|
|
134
|
+
const t = setTimeout(() => reject(new Error(`Test timed out after ${timeoutMs}ms`)), timeoutMs)
|
|
135
|
+
Promise.resolve(fn()).then(
|
|
136
|
+
(v) => { clearTimeout(t); resolve(v) },
|
|
137
|
+
(e) => { clearTimeout(t); reject(e) }
|
|
138
|
+
)
|
|
139
|
+
})
|
|
140
|
+
} else {
|
|
141
|
+
await fn()
|
|
142
|
+
}
|
|
143
|
+
const durationMs = Date.now() - startTime
|
|
144
|
+
testTimings.push({ name: fn.name, durationMs, ok: true })
|
|
129
145
|
logger.info(logger.writeColor('green', `✔ ${fn.name}`) + logger.writeColor('gray', ` (${durationMs}ms)`))
|
|
130
146
|
testSuccess++
|
|
131
147
|
successCases.push(fn.name)
|
|
@@ -134,7 +150,9 @@ export async function runTestFnsSequentially(testFns) {
|
|
|
134
150
|
todoCases.push(fn.name)
|
|
135
151
|
return
|
|
136
152
|
}
|
|
137
|
-
|
|
153
|
+
const durationMs = Date.now() - startTime
|
|
154
|
+
testTimings.push({ name: fn.name, durationMs, ok: false })
|
|
155
|
+
logger.error(logger.writeColor('red', `✘ ${fn.name}`) + logger.writeColor('gray', ` (${durationMs}ms)`))
|
|
138
156
|
if (err.message.includes('terminateAfter')) {
|
|
139
157
|
logger.error(logger.writeColor('magenta', 'Exiting early due to failure in terminateAfter: ', err.stack))
|
|
140
158
|
process.exit(1)
|
|
@@ -165,11 +183,23 @@ export async function runTestFnsSequentially(testFns) {
|
|
|
165
183
|
durationMs,
|
|
166
184
|
isSoloRun,
|
|
167
185
|
isMuteRun,
|
|
168
|
-
testSuitesCount: Object.keys(testSuites).length
|
|
186
|
+
testSuitesCount: Object.keys(testSuites).length,
|
|
187
|
+
testTimings
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function printTimingTable (testTimings) {
|
|
192
|
+
if (!testTimings || testTimings.length === 0) return
|
|
193
|
+
const sorted = [...testTimings].sort((a, b) => b.durationMs - a.durationMs)
|
|
194
|
+
logger.info('----- Per-test timing (slowest first) -----')
|
|
195
|
+
for (const t of sorted) {
|
|
196
|
+
const tag = t.ok ? '✔' : '✘'
|
|
197
|
+
logger.info(` ${tag} ${t.name} ${t.durationMs}ms`)
|
|
169
198
|
}
|
|
199
|
+
logger.info('')
|
|
170
200
|
}
|
|
171
201
|
|
|
172
|
-
function reportTestResults({
|
|
202
|
+
function reportTestResults ({
|
|
173
203
|
testSuccess, successCases,
|
|
174
204
|
testFail, failedCases,
|
|
175
205
|
testSuitesCount = 0,
|
|
@@ -177,13 +207,16 @@ function reportTestResults({
|
|
|
177
207
|
todoCases = [],
|
|
178
208
|
isSoloRun = false,
|
|
179
209
|
isMuteRun = false,
|
|
180
|
-
durationMs
|
|
210
|
+
durationMs,
|
|
211
|
+
testTimings
|
|
181
212
|
}) {
|
|
182
|
-
|
|
183
213
|
logger.info('\n')
|
|
184
214
|
logger.info(`----- Testing Complete -----`)
|
|
185
215
|
|
|
186
|
-
if (
|
|
216
|
+
if (
|
|
217
|
+
testSuccess > 0 &&
|
|
218
|
+
!envTruthy(envConfig.get('YAMF_TEST_QUIET_PASSES', false))
|
|
219
|
+
) {
|
|
187
220
|
logger.info(logger.writeColor('green', '✔ ✔ ✔ Success Report ✔ ✔ ✔'))
|
|
188
221
|
logger.info(logger.writeColor('green', '\n ' + successCases.join('\n ')))
|
|
189
222
|
logger.info('')
|
|
@@ -196,17 +229,21 @@ function reportTestResults({
|
|
|
196
229
|
} else logger.info(logger.writeColor('green', '✔ ✔ ✔ All Tests Passed! ✔ ✔ ✔'))
|
|
197
230
|
|
|
198
231
|
logger.info('\n----- Test Overview -----')
|
|
199
|
-
logger.info(`ℹ tests ${testSuccess + testFail}`)
|
|
200
|
-
if (testSuitesCount > 0) logger.info(`ℹ suites ${testSuitesCount}`)
|
|
201
|
-
logger.info(`ℹ pass ${testSuccess}`)
|
|
202
|
-
logger.info(`ℹ fail ${testFail}`)
|
|
203
|
-
logger.info(`ℹ skipped ${skippedCases.length}`)
|
|
204
|
-
logger.info(`ℹ todo ${todoCases.length}`)
|
|
205
|
-
logger.info(`ℹ duration_ms ${durationMs}`)
|
|
232
|
+
logger.info(`ℹ tests ${testSuccess + testFail} `)
|
|
233
|
+
if (testSuitesCount > 0) logger.info(`ℹ suites ${testSuitesCount} `)
|
|
234
|
+
logger.info(`ℹ pass ${testFail === 0 ? logger.writeColor('green', testSuccess) : testSuccess} `)
|
|
235
|
+
logger.info(`ℹ fail ${testFail > 0 ? logger.writeColor('red', testFail) : testFail} `)
|
|
236
|
+
logger.info(`ℹ skipped ${skippedCases.length} `)
|
|
237
|
+
logger.info(`ℹ todo ${todoCases.length} `)
|
|
238
|
+
logger.info(`ℹ duration_ms ${durationMs} `)
|
|
206
239
|
logger.info('')
|
|
207
240
|
|
|
208
241
|
if (isSoloRun) logger.warn(logger.writeColor('magenta', 'This was a solo test run, remove "solo" flags for a full test run'))
|
|
209
242
|
if (isMuteRun) logger.warn(logger.writeColor('magenta', 'This was a partially muted test run, remove "mute" flags for a full test run'))
|
|
243
|
+
|
|
244
|
+
if (envTruthy(envConfig.get('YAMF_TEST_TIMINGS', false)) && testTimings && testTimings.length > 0) {
|
|
245
|
+
printTimingTable(testTimings)
|
|
246
|
+
}
|
|
210
247
|
}
|
|
211
248
|
|
|
212
249
|
// ============================================================================
|
|
@@ -261,7 +298,7 @@ export default async function runTests(testSuitesOrFns) {
|
|
|
261
298
|
}
|
|
262
299
|
}
|
|
263
300
|
|
|
264
|
-
//
|
|
301
|
+
// Named export mirrors the default export (`runTests`).
|
|
265
302
|
export { runTests }
|
|
266
303
|
|
|
267
304
|
// ============================================================================
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* terminateAfter single-fn overload (in-process registry tracking from @yamf/core).
|
|
3
|
+
*/
|
|
4
|
+
import { assert, terminateAfter, withEnv, pickListenPort } from '@yamf/test'
|
|
5
|
+
import { registryServer, createService } from '@yamf/core'
|
|
6
|
+
|
|
7
|
+
export async function testTerminateAfterSingleFnStopsRegistryCascade () {
|
|
8
|
+
const port = await pickListenPort()
|
|
9
|
+
await withEnv({ YAMF_REGISTRY_URL: `http://127.0.0.1:${port}` }, async () => {
|
|
10
|
+
let ran = false
|
|
11
|
+
await terminateAfter(async () => {
|
|
12
|
+
await registryServer()
|
|
13
|
+
await createService('noop', function noop () {
|
|
14
|
+
return {}
|
|
15
|
+
})
|
|
16
|
+
ran = true
|
|
17
|
+
})
|
|
18
|
+
await assert(ran, r => r === true)
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function testTerminateAfterSingleFnNoRegistryNoOp () {
|
|
23
|
+
await terminateAfter(async () => {
|
|
24
|
+
await assert(1 + 1, x => x === 2)
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function testTerminateAfterSingleFnPropagatesBodyError () {
|
|
29
|
+
let saw = false
|
|
30
|
+
try {
|
|
31
|
+
await terminateAfter(async () => {
|
|
32
|
+
throw new Error('body-fail')
|
|
33
|
+
})
|
|
34
|
+
} catch (e) {
|
|
35
|
+
saw = e instanceof Error && e.message === 'body-fail'
|
|
36
|
+
}
|
|
37
|
+
await assert(saw, s => s === true)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function testTerminateAfterVerboseTeardownEnvSafe () {
|
|
41
|
+
const prev = process.env.YAMF_TEST_VERBOSE_TEARDOWN
|
|
42
|
+
process.env.YAMF_TEST_VERBOSE_TEARDOWN = 'true'
|
|
43
|
+
try {
|
|
44
|
+
await terminateAfter(async () => {})
|
|
45
|
+
} finally {
|
|
46
|
+
if (prev === undefined) delete process.env.YAMF_TEST_VERBOSE_TEARDOWN
|
|
47
|
+
else process.env.YAMF_TEST_VERBOSE_TEARDOWN = prev
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function testTerminateAfterMultiArgUnchanged () {
|
|
52
|
+
const port = await pickListenPort()
|
|
53
|
+
await withEnv({ YAMF_REGISTRY_URL: `http://127.0.0.1:${port}` }, async () => {
|
|
54
|
+
await terminateAfter(
|
|
55
|
+
() => registryServer(),
|
|
56
|
+
() => createService('ex', function ex () {
|
|
57
|
+
return { ok: true }
|
|
58
|
+
}),
|
|
59
|
+
async () => {
|
|
60
|
+
await assert(1, n => n === 1)
|
|
61
|
+
}
|
|
62
|
+
)
|
|
63
|
+
})
|
|
64
|
+
}
|