@feasibleone/blong-chain 1.9.0 โ 1.11.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/CHANGELOG.md +19 -0
- package/README.md +67 -47
- package/index.ts +101 -18
- package/package.json +4 -4
- package/test-types.ts +104 -9
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.11.0](https://github.com/feasibleone/blong/compare/blong-chain-v1.10.0...blong-chain-v1.11.0) (2026-09-26)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* progress points in test reports ([fb73036](https://github.com/feasibleone/blong/commit/fb730360877eb371ab3d1603ae7bf35976c170dc))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* misc CI failures ([60689af](https://github.com/feasibleone/blong/commit/60689af66dc246ef8f88fef8edbd2b8a16a6c4d9))
|
|
14
|
+
|
|
15
|
+
## [1.10.0](https://github.com/feasibleone/blong/compare/blong-chain-v1.9.0...blong-chain-v1.10.0) (2026-09-24)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Features
|
|
19
|
+
|
|
20
|
+
* sequence diagram progress points ([dc5b153](https://github.com/feasibleone/blong/commit/dc5b15327f31698a26a75ec73bdf9a0956a43dd9))
|
|
21
|
+
|
|
3
22
|
## [1.9.0](https://github.com/feasibleone/blong/compare/blong-chain-v1.8.2...blong-chain-v1.9.0) (2026-09-15)
|
|
4
23
|
|
|
5
24
|
|
package/README.md
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
**Parallel test execution with automatic dependency detection through thenable proxies.**
|
|
4
4
|
|
|
5
|
-
`blong-chain` is a TypeScript test framework that automatically detects
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
`blong-chain` is a TypeScript test framework that automatically detects dependencies between test
|
|
6
|
+
steps and executes them in parallel when possible, maximizing performance while maintaining
|
|
7
|
+
correctness.
|
|
8
8
|
|
|
9
9
|
## Key Features
|
|
10
10
|
|
|
@@ -14,10 +14,8 @@ maximizing performance while maintaining correctness.
|
|
|
14
14
|
- ๐ฏ **Thenable Proxies** - Natural async/await syntax for step dependencies
|
|
15
15
|
- ๐ **Performance Metrics** - Queue time, execution time, and critical path analysis
|
|
16
16
|
- ๐ณ **Nested Groups** - Hierarchical test organization with proper indentation
|
|
17
|
-
- ๐ง **Error Handling** - Graceful failure with continued execution of
|
|
18
|
-
|
|
19
|
-
- ๐งช **Test Framework Integration** - Works seamlessly with node:test, tap,
|
|
20
|
-
and others
|
|
17
|
+
- ๐ง **Error Handling** - Graceful failure with continued execution of independent steps
|
|
18
|
+
- ๐งช **Test Framework Integration** - Works seamlessly with node:test, tap, and others
|
|
21
19
|
|
|
22
20
|
## Installation
|
|
23
21
|
|
|
@@ -32,7 +30,7 @@ import {TestExecutor} from '@feasibleone/blong-chain';
|
|
|
32
30
|
import assert from 'node:assert/strict';
|
|
33
31
|
import {test} from 'node:test';
|
|
34
32
|
|
|
35
|
-
test('parallel execution example', async
|
|
33
|
+
test('parallel execution example', async t => {
|
|
36
34
|
const executor = new TestExecutor({concurrency: 10});
|
|
37
35
|
|
|
38
36
|
const steps = [
|
|
@@ -69,8 +67,7 @@ test('parallel execution example', async (t) => {
|
|
|
69
67
|
|
|
70
68
|
### Thenable Proxies
|
|
71
69
|
|
|
72
|
-
Steps access previous results through thenable proxies that automatically track
|
|
73
|
-
dependencies:
|
|
70
|
+
Steps access previous results through thenable proxies that automatically track dependencies:
|
|
74
71
|
|
|
75
72
|
```typescript
|
|
76
73
|
// Pattern 1: Direct await
|
|
@@ -89,7 +86,14 @@ async function step3(assert, {previousStep}) {
|
|
|
89
86
|
}
|
|
90
87
|
|
|
91
88
|
// Pattern 4: Deep destructuring
|
|
92
|
-
async function step4(
|
|
89
|
+
async function step4(
|
|
90
|
+
assert,
|
|
91
|
+
{
|
|
92
|
+
previousStep: {
|
|
93
|
+
user: {name},
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
) {
|
|
93
97
|
const userName = await name;
|
|
94
98
|
}
|
|
95
99
|
```
|
|
@@ -155,23 +159,37 @@ Organize steps into hierarchical groups for better structure and output:
|
|
|
155
159
|
|
|
156
160
|
```typescript
|
|
157
161
|
const databaseSetup = [
|
|
158
|
-
async function connect() {
|
|
159
|
-
|
|
160
|
-
|
|
162
|
+
async function connect() {
|
|
163
|
+
return {connected: true};
|
|
164
|
+
},
|
|
165
|
+
async function createSchema() {
|
|
166
|
+
return {created: true};
|
|
167
|
+
},
|
|
168
|
+
async function seedData() {
|
|
169
|
+
return {users: []};
|
|
170
|
+
},
|
|
161
171
|
] as any;
|
|
162
172
|
databaseSetup.name = 'Database Setup';
|
|
163
173
|
|
|
164
174
|
const apiTests = [
|
|
165
|
-
async function testEndpoint1() {
|
|
166
|
-
|
|
175
|
+
async function testEndpoint1() {
|
|
176
|
+
return {status: 200};
|
|
177
|
+
},
|
|
178
|
+
async function testEndpoint2() {
|
|
179
|
+
return {status: 200};
|
|
180
|
+
},
|
|
167
181
|
] as any;
|
|
168
182
|
apiTests.name = 'API Tests';
|
|
169
183
|
|
|
170
184
|
const steps = [
|
|
171
|
-
async function initialize() {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
185
|
+
async function initialize() {
|
|
186
|
+
return {ready: true};
|
|
187
|
+
},
|
|
188
|
+
databaseSetup, // Nested group
|
|
189
|
+
apiTests, // Nested group
|
|
190
|
+
async function cleanup() {
|
|
191
|
+
return {done: true};
|
|
192
|
+
},
|
|
175
193
|
];
|
|
176
194
|
|
|
177
195
|
await executor.execute(steps, {}, t);
|
|
@@ -194,9 +212,13 @@ await executor.execute(steps, {}, t);
|
|
|
194
212
|
โ cleanup
|
|
195
213
|
```
|
|
196
214
|
|
|
197
|
-
###
|
|
215
|
+
### Sync Barriers
|
|
216
|
+
|
|
217
|
+
Use empty arrays `[]` as sync barriers to synchronize parallel execution. All steps before a barrier
|
|
218
|
+
must complete before any steps after it begin:
|
|
198
219
|
|
|
199
|
-
|
|
220
|
+
(A _snapshot marker_ โ `snapshot('name', ...steps)` โ is a different thing: it waits for steps
|
|
221
|
+
**and** snapshots their results. A sync barrier snapshots nothing.)
|
|
200
222
|
|
|
201
223
|
```typescript
|
|
202
224
|
const steps = [
|
|
@@ -216,7 +238,7 @@ const steps = [
|
|
|
216
238
|
return {loggerReady: true};
|
|
217
239
|
},
|
|
218
240
|
|
|
219
|
-
//
|
|
241
|
+
// Sync barrier: Wait for all Phase 1 steps to complete
|
|
220
242
|
[],
|
|
221
243
|
|
|
222
244
|
// Phase 2: These run in parallel, but only after Phase 1 completes
|
|
@@ -230,7 +252,7 @@ const steps = [
|
|
|
230
252
|
return await fetchProducts(config.apiUrl);
|
|
231
253
|
},
|
|
232
254
|
|
|
233
|
-
// Another
|
|
255
|
+
// Another barrier
|
|
234
256
|
[],
|
|
235
257
|
|
|
236
258
|
// Phase 3: Runs only after Phase 2 completes
|
|
@@ -243,6 +265,7 @@ const steps = [
|
|
|
243
265
|
```
|
|
244
266
|
|
|
245
267
|
**Use Cases:**
|
|
268
|
+
|
|
246
269
|
- **Phased execution**: Separate initialization, data loading, processing, and cleanup phases
|
|
247
270
|
- **Resource management**: Ensure all resources are ready before proceeding
|
|
248
271
|
- **Testing stages**: Complete all setup before running tests, then cleanup
|
|
@@ -255,7 +278,7 @@ Monitor test execution in real-time:
|
|
|
255
278
|
```typescript
|
|
256
279
|
const executor = new TestExecutor({concurrency: 10});
|
|
257
280
|
|
|
258
|
-
executor.on('test:start',
|
|
281
|
+
executor.on('test:start', progress => {
|
|
259
282
|
console.log('Test started:', progress.testName);
|
|
260
283
|
});
|
|
261
284
|
|
|
@@ -314,9 +337,7 @@ console.log(`Critical path: ${latency.criticalPath.join(' โ ')}`);
|
|
|
314
337
|
|
|
315
338
|
// View bottlenecks
|
|
316
339
|
for (const bottleneck of latency.bottlenecks) {
|
|
317
|
-
console.log(
|
|
318
|
-
`${bottleneck.stepName} blocked ${bottleneck.blockedSteps.length} steps`
|
|
319
|
-
);
|
|
340
|
+
console.log(`${bottleneck.stepName} blocked ${bottleneck.blockedSteps.length} steps`);
|
|
320
341
|
}
|
|
321
342
|
|
|
322
343
|
// View individual step metrics
|
|
@@ -391,7 +412,7 @@ const executor = new TestExecutor({
|
|
|
391
412
|
## Real-World Example
|
|
392
413
|
|
|
393
414
|
```typescript
|
|
394
|
-
test('e-commerce checkout flow', async
|
|
415
|
+
test('e-commerce checkout flow', async t => {
|
|
395
416
|
const executor = new TestExecutor({concurrency: 5});
|
|
396
417
|
|
|
397
418
|
const steps = [
|
|
@@ -421,9 +442,7 @@ test('e-commerce checkout flow', async (t) => {
|
|
|
421
442
|
return {amount: product.price * 0.08};
|
|
422
443
|
},
|
|
423
444
|
|
|
424
|
-
async function calculateTotal(assert, {
|
|
425
|
-
loadProduct, calculateShipping, calculateTax
|
|
426
|
-
}) {
|
|
445
|
+
async function calculateTotal(assert, {loadProduct, calculateShipping, calculateTax}) {
|
|
427
446
|
const product = await loadProduct;
|
|
428
447
|
const shipping = await calculateShipping;
|
|
429
448
|
const tax = await calculateTax;
|
|
@@ -438,9 +457,7 @@ test('e-commerce checkout flow', async (t) => {
|
|
|
438
457
|
};
|
|
439
458
|
},
|
|
440
459
|
|
|
441
|
-
async function processPayment(assert, {
|
|
442
|
-
calculateTotal, validateInventory
|
|
443
|
-
}) {
|
|
460
|
+
async function processPayment(assert, {calculateTotal, validateInventory}) {
|
|
444
461
|
const total = await calculateTotal;
|
|
445
462
|
const inventory = await validateInventory;
|
|
446
463
|
|
|
@@ -465,10 +482,14 @@ test('e-commerce checkout flow', async (t) => {
|
|
|
465
482
|
},
|
|
466
483
|
];
|
|
467
484
|
|
|
468
|
-
await executor.execute(
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
485
|
+
await executor.execute(
|
|
486
|
+
steps,
|
|
487
|
+
{
|
|
488
|
+
testId: 'checkout-001',
|
|
489
|
+
environment: 'test',
|
|
490
|
+
},
|
|
491
|
+
t,
|
|
492
|
+
);
|
|
472
493
|
|
|
473
494
|
// Verify results
|
|
474
495
|
const progress = executor.getProgress();
|
|
@@ -494,8 +515,7 @@ In this example:
|
|
|
494
515
|
|
|
495
516
|
- **[SHOWCASE.md](./SHOWCASE.md)** - Comprehensive feature showcase with all patterns
|
|
496
517
|
- **[TESTING.md](./TESTING.md)** - Testing guide and CI integration
|
|
497
|
-
- **[NESTED_TEST_CONTEXT.md](./NESTED_TEST_CONTEXT.md)** - Test framework
|
|
498
|
-
integration details
|
|
518
|
+
- **[NESTED_TEST_CONTEXT.md](./NESTED_TEST_CONTEXT.md)** - Test framework integration details
|
|
499
519
|
- **[test-types.ts](./test-types.ts)** - Complete TypeScript API documentation
|
|
500
520
|
|
|
501
521
|
## Running Tests
|
|
@@ -514,17 +534,17 @@ npm run test:all
|
|
|
514
534
|
|
|
515
535
|
## How It Works
|
|
516
536
|
|
|
517
|
-
1. **Detection Phase**: When you destructure or access `context.stepName`, a
|
|
518
|
-
|
|
537
|
+
1. **Detection Phase**: When you destructure or access `context.stepName`, a thenable proxy is
|
|
538
|
+
returned
|
|
519
539
|
2. **Dependency Tracking**: The access is recorded as a dependency
|
|
520
540
|
3. **Scheduling Phase**: Steps are added to a priority queue based on dependencies
|
|
521
541
|
4. **Execution Phase**: Steps run as soon as all their dependencies complete
|
|
522
542
|
5. **Resolution Phase**: Step results are stored and proxies are resolved
|
|
523
|
-
6. **
|
|
524
|
-
|
|
543
|
+
6. **Sync Barriers**: Empty arrays `[]` create synchronization barriers, waiting for all previous
|
|
544
|
+
steps to complete before continuing
|
|
525
545
|
|
|
526
|
-
This creates a dynamic dependency graph that enables maximum parallelization
|
|
527
|
-
|
|
546
|
+
This creates a dynamic dependency graph that enables maximum parallelization while ensuring
|
|
547
|
+
correctness, with optional synchronization points for phased execution.
|
|
528
548
|
|
|
529
549
|
## Use Cases
|
|
530
550
|
|
package/index.ts
CHANGED
|
@@ -13,10 +13,12 @@
|
|
|
13
13
|
import assert from 'node:assert';
|
|
14
14
|
import {EventEmitter} from 'node:events';
|
|
15
15
|
import PQueue from 'p-queue';
|
|
16
|
+
import {progressTree, reportProgress} from './progress.ts';
|
|
16
17
|
import type {
|
|
17
18
|
IDependencyEdge,
|
|
18
19
|
IDependencyGraph,
|
|
19
20
|
IMeta,
|
|
21
|
+
IProgressEntry,
|
|
20
22
|
IPromiseEntry,
|
|
21
23
|
ISourceLocation,
|
|
22
24
|
IStepError,
|
|
@@ -371,7 +373,7 @@ function captureSourceLocation(): ISourceLocation {
|
|
|
371
373
|
const DEFAULT_MAX_RETRIES = 1;
|
|
372
374
|
|
|
373
375
|
// ============================================================================
|
|
374
|
-
// Masking helpers (also used by assert.snapshot and
|
|
376
|
+
// Masking helpers (also used by assert.snapshot and snapshot markers)
|
|
375
377
|
// ============================================================================
|
|
376
378
|
|
|
377
379
|
/**
|
|
@@ -542,13 +544,13 @@ export class TestExecutor extends EventEmitter {
|
|
|
542
544
|
): Promise<void> {
|
|
543
545
|
const stepPromises: Promise<void>[] = [];
|
|
544
546
|
const namedPromises = new Map<string, Promise<void>>();
|
|
545
|
-
let
|
|
547
|
+
let snapshotIndex = 0;
|
|
546
548
|
|
|
547
549
|
for (const step of steps) {
|
|
548
550
|
if (Array.isArray(step)) {
|
|
549
551
|
// Distinguish by element type:
|
|
550
552
|
// [] empty array โ sync barrier (existing behaviour)
|
|
551
|
-
// ['*'] / ['s1','s2'] โ snapshot
|
|
553
|
+
// ['*'] / ['s1','s2'] โ snapshot marker
|
|
552
554
|
// [fn, ...] nested โ nested step group (existing behaviour)
|
|
553
555
|
if (step.length === 0) {
|
|
554
556
|
// Sync barrier โ wait for all parallel steps in this batch
|
|
@@ -558,37 +560,35 @@ export class TestExecutor extends EventEmitter {
|
|
|
558
560
|
}
|
|
559
561
|
|
|
560
562
|
if (step.every(s => typeof s === 'string')) {
|
|
561
|
-
// Snapshot
|
|
562
|
-
const
|
|
563
|
+
// Snapshot marker: await the relevant steps, then snapshot
|
|
564
|
+
const marker = step as unknown as string[];
|
|
563
565
|
|
|
564
|
-
if (
|
|
566
|
+
if (marker.length === 1 && marker[0] === '*') {
|
|
565
567
|
// ['*'] โ wait for entire current batch
|
|
566
568
|
await Promise.all(stepPromises);
|
|
567
569
|
stepPromises.length = 0;
|
|
568
570
|
} else {
|
|
569
571
|
// ['step1','step2'] โ wait only for the named steps
|
|
570
|
-
const namedToWait =
|
|
572
|
+
const namedToWait = marker
|
|
571
573
|
.map(name => namedPromises.get(name))
|
|
572
574
|
.filter((p): p is Promise<void> => p !== undefined);
|
|
573
575
|
await Promise.all(namedToWait);
|
|
574
576
|
// stepPromises is NOT cleared โ other steps keep running
|
|
575
577
|
}
|
|
576
578
|
const cpName =
|
|
577
|
-
(
|
|
578
|
-
(
|
|
579
|
-
? `context`
|
|
580
|
-
: checkpoint.join('-'));
|
|
579
|
+
(marker as {name?: string}).name ??
|
|
580
|
+
(marker.length === 1 && marker[0] === '*' ? `context` : marker.join('-'));
|
|
581
581
|
// Disambiguate when the same name is used more than once
|
|
582
582
|
const snapshotName =
|
|
583
|
-
|
|
584
|
-
|
|
583
|
+
snapshotIndex === 0 ? cpName : `${cpName}-${snapshotIndex}`;
|
|
584
|
+
snapshotIndex++;
|
|
585
585
|
|
|
586
586
|
const stepsToSnapshot =
|
|
587
|
-
|
|
587
|
+
marker.length === 1 && marker[0] === '*'
|
|
588
588
|
? [...this.progress.steps.entries()]
|
|
589
589
|
.filter(([, s]) => s.status === 'completed')
|
|
590
590
|
.map(([name]) => name)
|
|
591
|
-
:
|
|
591
|
+
: marker.filter(name =>
|
|
592
592
|
Object.prototype.hasOwnProperty.call(this.realContext, name),
|
|
593
593
|
);
|
|
594
594
|
|
|
@@ -653,6 +653,43 @@ export class TestExecutor extends EventEmitter {
|
|
|
653
653
|
await Promise.all(stepPromises);
|
|
654
654
|
}
|
|
655
655
|
|
|
656
|
+
/**
|
|
657
|
+
* The invocation's progress list as it stands, for a read at a step boundary (PRD R26/R27).
|
|
658
|
+
*
|
|
659
|
+
* The `$meta` object is the one every step of this run shares โ the same object the steps
|
|
660
|
+
* destructure, and the one the handlers they call are handed.
|
|
661
|
+
*/
|
|
662
|
+
private _progressList(): IProgressEntry[] | undefined {
|
|
663
|
+
const progress = (this.realContext.$meta as IMeta | undefined)?.progress;
|
|
664
|
+
return Array.isArray(progress) ? progress : undefined;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* What was announced since `seen`, or `undefined` when nothing was (PRD R26/R27).
|
|
669
|
+
*
|
|
670
|
+
* Two shapes, and both are ordinary. A list that still holds what it held is *appended* to,
|
|
671
|
+
* and the difference between the two boundary reads is the step's own progress. A list that
|
|
672
|
+
* no longer begins with those entries was **replaced**: a step that resets it to scope its
|
|
673
|
+
* own assertions (`$meta.progress = []`, which is how a scenario keeps one step's
|
|
674
|
+
* checkpoints out of the next one's) then owns everything in the new list, including when it
|
|
675
|
+
* happens to end up as long as it started.
|
|
676
|
+
*
|
|
677
|
+
* Best-effort attribution, and deliberately so: steps that run in parallel share the one
|
|
678
|
+
* list, so a point announced while two of them were running is reported by whichever
|
|
679
|
+
* boundary read it. It is never lost, and a scenario whose steps are ordered by their
|
|
680
|
+
* dependencies โ which is what a test that asserts on progress is โ has one at a time.
|
|
681
|
+
*/
|
|
682
|
+
private _announcedSince(seen: IProgressEntry[] | undefined): IProgressEntry[] | undefined {
|
|
683
|
+
const progress = this._progressList();
|
|
684
|
+
if (progress === undefined || progress.length === 0) {
|
|
685
|
+
return undefined;
|
|
686
|
+
}
|
|
687
|
+
if (!keeps(progress, seen)) {
|
|
688
|
+
return progress;
|
|
689
|
+
}
|
|
690
|
+
return progress.length > seen.length ? progress.slice(seen.length) : undefined;
|
|
691
|
+
}
|
|
692
|
+
|
|
656
693
|
/**
|
|
657
694
|
* Executes a single step function
|
|
658
695
|
*/
|
|
@@ -703,8 +740,6 @@ export class TestExecutor extends EventEmitter {
|
|
|
703
740
|
// Wrap execution function for potential test context wrapping
|
|
704
741
|
// When a TAP sub-test context is supplied, assert is augmented with:
|
|
705
742
|
// assert.snapshot(value, 'name', opts?) โ explicit snapshot
|
|
706
|
-
// assert.snapshot({mask?: []}) โ deferred: snapshot the
|
|
707
|
-
// step's return value
|
|
708
743
|
// assert.snapshot() โ deferred, no extra mask
|
|
709
744
|
// Deferred snapshots are taken after fn() returns, under the step name.
|
|
710
745
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
@@ -762,6 +797,7 @@ export class TestExecutor extends EventEmitter {
|
|
|
762
797
|
this.graph.nodes.get(stepName)!.status = 'running';
|
|
763
798
|
this.graph.nodes.get(stepName)!.startTime = latency.startedAt;
|
|
764
799
|
|
|
800
|
+
const progressBefore = this._progressList();
|
|
765
801
|
this.emit('step:start', stepName, stepProgress);
|
|
766
802
|
|
|
767
803
|
try {
|
|
@@ -823,6 +859,11 @@ export class TestExecutor extends EventEmitter {
|
|
|
823
859
|
// Store result in real context
|
|
824
860
|
this.realContext[stepName] = result;
|
|
825
861
|
|
|
862
|
+
// What this step announced, read at the boundary before the event so a
|
|
863
|
+
// listener that writes the step's result file sees it (see `step:end`).
|
|
864
|
+
const announced = this._announcedSince(progressBefore);
|
|
865
|
+
if (announced !== undefined) stepProgress.progress = announced;
|
|
866
|
+
|
|
826
867
|
// Resolve all promises for this step
|
|
827
868
|
this.promiseManager.resolveStep(stepName, result);
|
|
828
869
|
|
|
@@ -847,6 +888,24 @@ export class TestExecutor extends EventEmitter {
|
|
|
847
888
|
|
|
848
889
|
this.progress.completedSteps++;
|
|
849
890
|
this.emit('step:end', stepName, stepProgress);
|
|
891
|
+
|
|
892
|
+
// A point becomes a sub-test of the step that announced it, and a branch the
|
|
893
|
+
// sub-test holding the points taken inside it โ a checkpoint as a *step* in
|
|
894
|
+
// the report, which is what "checkpoints drive test reporting" claims
|
|
895
|
+
// (PRD R26/R27). Rendered from the same entries `blong-allure` maps into
|
|
896
|
+
// Allure steps, so the two reports cannot describe different shapes.
|
|
897
|
+
//
|
|
898
|
+
// The context is checked the way the snapshot handling checks it, because
|
|
899
|
+
// the queue hands the task its own options where there is no test context:
|
|
900
|
+
// only a context that can nest a test gets progress nested in it.
|
|
901
|
+
if (
|
|
902
|
+
typeof stepTestContext?.test === 'function' &&
|
|
903
|
+
stepProgress.progress !== undefined
|
|
904
|
+
) {
|
|
905
|
+
for (const node of progressTree(stepProgress.progress)) {
|
|
906
|
+
await reportProgress(stepTestContext, node);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
850
909
|
} catch (error) {
|
|
851
910
|
// Handle error
|
|
852
911
|
latency.completedAt = Date.now();
|
|
@@ -871,6 +930,12 @@ export class TestExecutor extends EventEmitter {
|
|
|
871
930
|
this.graph.nodes.get(stepName)!.endTime = latency.completedAt;
|
|
872
931
|
this.graph.nodes.get(stepName)!.error = error as Error;
|
|
873
932
|
|
|
933
|
+
// Kept even though the step failed, and especially then: what a failing step
|
|
934
|
+
// announced is the evidence a reader wants, and Allure renders it from here.
|
|
935
|
+
// The tap side leaves it out โ sub-tests are not added to a step that threw.
|
|
936
|
+
const announcedOnFailure = this._announcedSince(progressBefore);
|
|
937
|
+
if (announcedOnFailure !== undefined) stepProgress.progress = announcedOnFailure;
|
|
938
|
+
|
|
874
939
|
this.progress.failedSteps++;
|
|
875
940
|
this.emit('step:error', stepName, error as Error, stepProgress);
|
|
876
941
|
this.log?.error?.({err: error}, `step ${stepName} failed`);
|
|
@@ -940,7 +1005,7 @@ export class TestExecutor extends EventEmitter {
|
|
|
940
1005
|
|
|
941
1006
|
for (const step of steps) {
|
|
942
1007
|
if (Array.isArray(step)) {
|
|
943
|
-
// Skip
|
|
1008
|
+
// Skip snapshot markers (string-only arrays) โ they are not steps
|
|
944
1009
|
if (step.every(s => typeof s === 'string')) continue;
|
|
945
1010
|
// Recursively collect from nested step groups
|
|
946
1011
|
const nested = this._collectStepNames(step as StepArray);
|
|
@@ -1119,3 +1184,21 @@ export class TestExecutor extends EventEmitter {
|
|
|
1119
1184
|
|
|
1120
1185
|
// Export all types
|
|
1121
1186
|
export type * from './test-types.js';
|
|
1187
|
+
export {progressTree, reportProgress} from './progress.ts';
|
|
1188
|
+
|
|
1189
|
+
/**
|
|
1190
|
+
* Whether a progress list still holds what it held, entry for entry and by identity.
|
|
1191
|
+
*
|
|
1192
|
+
* Identity rather than equality, because the entries are objects the recorder pushed: a list
|
|
1193
|
+
* that kept them is one that was spread back with additions, and a list whose first entry is a
|
|
1194
|
+
* different object is one that was reset and announced afresh โ which is the difference between
|
|
1195
|
+
* a step *adding* to the invocation's progress and *replacing* it.
|
|
1196
|
+
*/
|
|
1197
|
+
const keeps = (
|
|
1198
|
+
progress: IProgressEntry[],
|
|
1199
|
+
seen: IProgressEntry[] | undefined,
|
|
1200
|
+
): seen is IProgressEntry[] =>
|
|
1201
|
+
progress === seen ||
|
|
1202
|
+
(seen !== undefined &&
|
|
1203
|
+
progress.length >= seen.length &&
|
|
1204
|
+
seen.every((entry, at) => progress[at] === entry));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@feasibleone/blong-chain",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.0",
|
|
4
4
|
"description": "Parallel testing with automatic dependencies",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"blong",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"@types/node": "^24",
|
|
25
25
|
"tap": "^21.6.3",
|
|
26
26
|
"typescript": "^6.0.3",
|
|
27
|
-
"@feasibleone/blong-dev": "1.
|
|
27
|
+
"@feasibleone/blong-dev": "1.7.0"
|
|
28
28
|
},
|
|
29
29
|
"scripts": {
|
|
30
30
|
"build": "heft build --clean",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"ci-lint": "blong-dev lint",
|
|
34
34
|
"ci-publish": "node ../../common/scripts/install-run-rush-pnpm.js publish --access public --provenance",
|
|
35
35
|
"ci-test": "blong-dev test '*.test.ts';./test-examples-ci.sh",
|
|
36
|
-
"test": "
|
|
37
|
-
"test:examples": "node --test
|
|
36
|
+
"test": "blong-dev test '*.test.ts';./test-examples-ci.sh",
|
|
37
|
+
"test:examples": "node --test examples/*.test.ts"
|
|
38
38
|
}
|
|
39
39
|
}
|
package/test-types.ts
CHANGED
|
@@ -49,26 +49,28 @@ export type StepFunction = (
|
|
|
49
49
|
) => unknown | Promise<unknown>;
|
|
50
50
|
|
|
51
51
|
/**
|
|
52
|
-
* A snapshot
|
|
53
|
-
* steps array. At runtime, when the executor encounters it the
|
|
54
|
-
*
|
|
52
|
+
* A snapshot marker โ an array of step-name strings placed inside the
|
|
53
|
+
* steps array. At runtime, when the executor encounters it the relevant steps
|
|
54
|
+
* are awaited and their results snapshotted into the TAP context.
|
|
55
55
|
*
|
|
56
56
|
* - `['*']` โ snapshot ALL completed steps' results into one context object
|
|
57
57
|
* - `['step1', 'step2']` โ snapshot only those specific steps
|
|
58
|
-
* - `[]` โ sync barrier only, no snapshot (
|
|
58
|
+
* - `[]` โ sync barrier only, no snapshot (the empty array is the spelling;
|
|
59
|
+
* there is no symbol for it, because inventing one would touch every
|
|
60
|
+
* call site for no gain)
|
|
59
61
|
*
|
|
60
62
|
* The array may carry an optional `.name` to give the snapshot a stable name:
|
|
61
63
|
* ```
|
|
62
|
-
* const
|
|
64
|
+
* const marker = Object.assign(['*'], {name: 'provisioning-complete'});
|
|
63
65
|
* ```
|
|
64
66
|
*/
|
|
65
|
-
export type
|
|
67
|
+
export type SnapshotMarker = string[] & {name?: string};
|
|
66
68
|
|
|
67
69
|
/**
|
|
68
70
|
* Array of test steps. May contain step functions, nested step groups, or
|
|
69
|
-
*
|
|
71
|
+
* snapshot markers (string arrays; `[]` being a sync barrier).
|
|
70
72
|
*/
|
|
71
|
-
export type StepArray = (StepFunction | StepArray |
|
|
73
|
+
export type StepArray = (StepFunction | StepArray | SnapshotMarker)[] & {name?: string};
|
|
72
74
|
|
|
73
75
|
/**
|
|
74
76
|
* Meta information passed through test execution
|
|
@@ -76,6 +78,15 @@ export type StepArray = (StepFunction | StepArray | CheckpointMarker)[] & {name?
|
|
|
76
78
|
export interface IMeta {
|
|
77
79
|
/** Optional concurrency limit for parallel step execution */
|
|
78
80
|
concurrency?: number;
|
|
81
|
+
/**
|
|
82
|
+
* What the invocation has announced so far, in the order it announced it (PRD R26/R27).
|
|
83
|
+
*
|
|
84
|
+
* The same array a framework dispatch keeps on its own `$meta`: the handlers a step calls
|
|
85
|
+
* are handed this object, so whatever they announce while it runs appears here โ which is
|
|
86
|
+
* the only place the executor can read a step's progress from, and why it reads it at the
|
|
87
|
+
* step's boundaries.
|
|
88
|
+
*/
|
|
89
|
+
progress?: IProgressEntry[];
|
|
79
90
|
/** Additional metadata properties */
|
|
80
91
|
[key: string]: unknown;
|
|
81
92
|
}
|
|
@@ -87,6 +98,14 @@ export interface IMeta {
|
|
|
87
98
|
export interface ITestFrameworkContext {
|
|
88
99
|
/** Creates a nested test scope for proper indentation */
|
|
89
100
|
test: (name: string, fn: (t: unknown) => void | Promise<void>) => unknown;
|
|
101
|
+
/**
|
|
102
|
+
* Writes a comment into the run's output (`# โฆ` in TAP).
|
|
103
|
+
*
|
|
104
|
+
* What a point is reported as: a moment a step passed through is not a test,
|
|
105
|
+
* so it does not deserve a sub-test of its own โ but it is what a reader
|
|
106
|
+
* wants to see beside the step, which is what a comment is.
|
|
107
|
+
*/
|
|
108
|
+
comment?: (text: string) => void;
|
|
90
109
|
/** Captures a snapshot of a value under the given name */
|
|
91
110
|
matchSnapshot?: (value: unknown, name: string) => void;
|
|
92
111
|
}
|
|
@@ -199,6 +218,71 @@ export interface IDependencyEdge {
|
|
|
199
218
|
// Progress Tracking Types
|
|
200
219
|
// ============================================================================
|
|
201
220
|
|
|
221
|
+
/**
|
|
222
|
+
* A branch as a progress entry names it (PRD R11/R26).
|
|
223
|
+
*
|
|
224
|
+
* Declared structurally here, matching the framework's own declaration in
|
|
225
|
+
* `core/blong/types.ts` the way {@link IMeta} does, because this package stands at the bottom
|
|
226
|
+
* of the dependency graph with `p-queue` as its only dependency. The names are what a report
|
|
227
|
+
* groups by; the position a log mints for itself is deliberately not here.
|
|
228
|
+
*/
|
|
229
|
+
export interface IRegionMark {
|
|
230
|
+
/** What the branch was about, stable across runs. */
|
|
231
|
+
discriminator: string;
|
|
232
|
+
/** Every candidate considered, in evaluation order. */
|
|
233
|
+
candidates: string[];
|
|
234
|
+
/** The branch taken, or `none` when none of them matched. */
|
|
235
|
+
chosen: string;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** A milestone an invocation announced (PRD R26). */
|
|
239
|
+
export interface IProgressPoint {
|
|
240
|
+
/** Which of the two shapes this entry is; one array holds both. */
|
|
241
|
+
kind: 'point';
|
|
242
|
+
/** Stable name of the moment, e.g. `total-calculated`. */
|
|
243
|
+
name: string;
|
|
244
|
+
/** What was true there; kept locally, exactly as a record keeps it. */
|
|
245
|
+
data?: unknown;
|
|
246
|
+
/** When it was announced, in epoch milliseconds. */
|
|
247
|
+
timestamp: number;
|
|
248
|
+
/** The branches it was announced *inside*, outermost first. */
|
|
249
|
+
regions?: IRegionMark[];
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** A branch the invocation took, in the same array as the points (PRD R11/R26). */
|
|
253
|
+
export interface IProgressRegion extends IRegionMark {
|
|
254
|
+
/** Which of the two shapes this entry is; one array holds both. */
|
|
255
|
+
kind: 'region';
|
|
256
|
+
/** The values the decision was made from. */
|
|
257
|
+
values: Record<string, unknown>;
|
|
258
|
+
/** The branches this one was itself taken inside, outermost first. */
|
|
259
|
+
regions?: IRegionMark[];
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** One entry of an invocation's `progress` list. */
|
|
263
|
+
export type IProgressEntry = IProgressPoint | IProgressRegion;
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* One node of the progress tree a report is drawn from (PRD R26/R27).
|
|
267
|
+
*
|
|
268
|
+
* `name` is the display name rather than the raw entry, so both renderers label a branch the
|
|
269
|
+
* same way: `<discriminator> = <chosen>` for a region, the point's own name for a point.
|
|
270
|
+
*/
|
|
271
|
+
export interface IProgressNode {
|
|
272
|
+
/** What a report calls it: the point's name, or `<discriminator> = <chosen>`. */
|
|
273
|
+
name: string;
|
|
274
|
+
/** Which of the two shapes it is. */
|
|
275
|
+
kind: 'point' | 'region';
|
|
276
|
+
/**
|
|
277
|
+
* What the point announced, printed beside it when it is small enough to read.
|
|
278
|
+
*
|
|
279
|
+
* A branch carries none: what it has to say is its name.
|
|
280
|
+
*/
|
|
281
|
+
data?: Record<string, unknown>;
|
|
282
|
+
/** The points and branches announced inside it, in the order they were announced. */
|
|
283
|
+
children: IProgressNode[];
|
|
284
|
+
}
|
|
285
|
+
|
|
202
286
|
/**
|
|
203
287
|
* Overall test execution progress
|
|
204
288
|
*/
|
|
@@ -257,6 +341,17 @@ export interface IStepProgress {
|
|
|
257
341
|
result?: unknown;
|
|
258
342
|
/** Error information if step failed */
|
|
259
343
|
error?: IStepError;
|
|
344
|
+
/**
|
|
345
|
+
* What this step announced while it ran (PRD R26/R27), or absent when it announced
|
|
346
|
+
* nothing.
|
|
347
|
+
*
|
|
348
|
+
* Read from the invocation's own list at the step's boundaries rather than reported by the
|
|
349
|
+
* step: the handlers it called announced into the `$meta` every step shares, so the window
|
|
350
|
+
* is the only thing that attributes a point to the step that made it. A report draws these
|
|
351
|
+
* as the step's nested steps โ a point as a step, a branch as the group of the points
|
|
352
|
+
* taken inside it.
|
|
353
|
+
*/
|
|
354
|
+
progress?: IProgressEntry[];
|
|
260
355
|
}
|
|
261
356
|
|
|
262
357
|
/**
|
|
@@ -370,7 +465,7 @@ export interface ITestExecutorConfig {
|
|
|
370
465
|
log?: ITestLogger;
|
|
371
466
|
/**
|
|
372
467
|
* Chain-level mask paths. Applied to ALL snapshot operations in this chain:
|
|
373
|
-
* `autoSnapshot`, `assert.snapshot()`, and
|
|
468
|
+
* `autoSnapshot`, `assert.snapshot()`, and snapshot markers.
|
|
374
469
|
*
|
|
375
470
|
* Supports:
|
|
376
471
|
* - Simple name: `'id'` โ masks the `id` field in the snapshotted value
|