@travetto/test 7.0.0-rc.2 → 7.0.0-rc.3

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.
@@ -3,7 +3,7 @@ import { AssertionError } from 'node:assert';
3
3
  import { Env, TimeUtil, Runtime, castTo, classConstruct } from '@travetto/runtime';
4
4
  import { Registry } from '@travetto/registry';
5
5
 
6
- import { TestConfig, TestResult, TestRun } from '../model/test.ts';
6
+ import { TestConfig, TestResult, type TestRun } from '../model/test.ts';
7
7
  import { SuiteConfig, SuiteFailure, SuiteResult } from '../model/suite.ts';
8
8
  import { TestConsumerShape } from '../consumer/types.ts';
9
9
  import { AssertCheck } from '../assert/check.ts';
@@ -14,6 +14,7 @@ import { AssertUtil } from '../assert/util.ts';
14
14
  import { Barrier } from './barrier.ts';
15
15
  import { ExecutionError } from './error.ts';
16
16
  import { SuiteRegistryIndex } from '../registry/registry-index.ts';
17
+ import { TestModelUtil } from '../model/util.ts';
17
18
 
18
19
  const TEST_TIMEOUT = TimeUtil.fromValue(Env.TRV_TEST_TIMEOUT.value) ?? 5000;
19
20
 
@@ -94,13 +95,16 @@ export class TestExecutor {
94
95
  passed: 0,
95
96
  failed: 0,
96
97
  skipped: 0,
98
+ unknown: 0,
97
99
  total: 0,
100
+ status: 'unknown',
98
101
  lineStart: suite.lineStart,
99
102
  lineEnd: suite.lineEnd,
100
103
  import: suite.import,
101
104
  classId: suite.classId,
105
+ sourceHash: suite.sourceHash,
102
106
  duration: 0,
103
- tests: []
107
+ tests: {}
104
108
  };
105
109
  }
106
110
 
@@ -118,12 +122,14 @@ export class TestExecutor {
118
122
  methodName: test.methodName,
119
123
  description: test.description,
120
124
  classId: test.classId,
125
+ tags: test.tags,
121
126
  lineStart: test.lineStart,
122
127
  lineEnd: test.lineEnd,
123
128
  lineBodyStart: test.lineBodyStart,
124
129
  import: test.import,
125
130
  sourceImport: test.sourceImport,
126
- status: 'skipped',
131
+ sourceHash: test.sourceHash,
132
+ status: 'unknown',
127
133
  assertions: [],
128
134
  duration: 0,
129
135
  durationTotal: 0,
@@ -184,19 +190,23 @@ export class TestExecutor {
184
190
  }
185
191
 
186
192
  const result: SuiteResult = this.createSuiteResult(suite);
193
+ const validTestMethodNames = new Set(tests.map(t => t.methodName));
194
+ const testConfigs = Object.fromEntries(
195
+ Object.entries(suite.tests).filter(([key]) => validTestMethodNames.has(key))
196
+ );
187
197
 
188
198
  const startTime = Date.now();
189
199
 
190
200
  // Mark suite start
191
- this.#consumer.onEvent({ phase: 'before', type: 'suite', suite });
201
+ this.#consumer.onEvent({ phase: 'before', type: 'suite', suite: { ...suite, tests: testConfigs } });
192
202
 
193
- const mgr = new TestPhaseManager(suite, result, event => this.#onSuiteFailure(event));
203
+ const manager = new TestPhaseManager(suite, result, event => this.#onSuiteFailure(event));
194
204
 
195
205
  const originalEnv = { ...process.env };
196
206
 
197
207
  try {
198
208
  // Handle the BeforeAll calls
199
- await mgr.startPhase('all');
209
+ await manager.startPhase('all');
200
210
 
201
211
  const suiteEnv = { ...process.env };
202
212
 
@@ -212,29 +222,30 @@ export class TestExecutor {
212
222
  const testStart = Date.now();
213
223
 
214
224
  // Handle BeforeEach
215
- await mgr.startPhase('each');
225
+ await manager.startPhase('each');
216
226
 
217
227
  // Run test
218
228
  const testResult = await this.executeTest(test);
219
229
  result[testResult.status]++;
220
- result.tests.push(testResult);
230
+ result.tests[testResult.methodName] = testResult;
221
231
 
222
232
  // Handle after each
223
- await mgr.endPhase('each');
233
+ await manager.endPhase('each');
224
234
  testResult.durationTotal = Date.now() - testStart;
225
235
  }
226
236
 
227
237
  // Handle after all
228
- await mgr.endPhase('all');
238
+ await manager.endPhase('all');
229
239
  } catch (error) {
230
- await mgr.onError(error);
240
+ await manager.onError(error);
231
241
  }
232
242
 
233
243
  // Restore env
234
244
  process.env = { ...originalEnv };
235
245
 
236
246
  result.duration = Date.now() - startTime;
237
- result.total = result.passed + result.failed;
247
+ result.total = result.passed + result.failed + result.skipped;
248
+ result.status = TestModelUtil.countsToTestStatus(result);
238
249
 
239
250
  // Mark suite complete
240
251
  this.#consumer.onEvent({ phase: 'after', type: 'suite', suite: result });
@@ -87,7 +87,7 @@ export class TestPhaseManager {
87
87
  );
88
88
 
89
89
  this.#onSuiteFailure(failure);
90
- this.#result.tests.push(failure.testResult);
90
+ this.#result.tests[failure.testResult.methodName] = failure.testResult;
91
91
  this.#result.failed++;
92
92
  }
93
93
  }
@@ -0,0 +1,247 @@
1
+ import { createReadStream } from 'node:fs';
2
+ import fs from 'node:fs/promises';
3
+ import readline from 'node:readline/promises';
4
+ import path from 'node:path';
5
+
6
+ import { Env, ExecUtil, ShutdownManager, Util, RuntimeIndex, Runtime, TimeUtil, JSONUtil } from '@travetto/runtime';
7
+ import { WorkPool } from '@travetto/worker';
8
+ import { Registry } from '@travetto/registry';
9
+
10
+ import type { TestConfig, TestRunInput, TestRun, TestGlobInput, TestDiffInput } from '../model/test.ts';
11
+ import type { TestRemoveEvent } from '../model/event.ts';
12
+ import type { TestConsumerShape } from '../consumer/types.ts';
13
+ import { RunnableTestConsumer } from '../consumer/types/runnable.ts';
14
+ import type { TestConsumerConfig } from './types.ts';
15
+ import { TestConsumerRegistryIndex } from '../consumer/registry-index.ts';
16
+ import { TestExecutor } from './executor.ts';
17
+ import { buildStandardTestManager } from '../worker/standard.ts';
18
+ import { SuiteRegistryIndex } from '../registry/registry-index.ts';
19
+
20
+ type RunState = {
21
+ runs: TestRun[];
22
+ removes?: TestRemoveEvent[];
23
+ };
24
+
25
+ /**
26
+ * Test Utilities for Running
27
+ */
28
+ export class RunUtil {
29
+ /**
30
+ * Add 50 ms to the shutdown to allow for buffers to output properly
31
+ */
32
+ static registerCleanup(scope: string): void {
33
+ ShutdownManager.onGracefulShutdown(() => Util.blockingTimeout(50), `test.${scope}.bufferOutput`);
34
+ }
35
+
36
+ /**
37
+ * Determine if a given file path is a valid test file
38
+ */
39
+ static async isTestFile(file: string): Promise<boolean> {
40
+ const reader = readline.createInterface({ input: createReadStream(file) });
41
+ const state = { imp: false, suite: false };
42
+ for await (const line of reader) {
43
+ state.imp ||= line.includes('@travetto/test');
44
+ state.suite ||= line.includes('Suite'); // Decorator or name
45
+ if (state.imp && state.suite) {
46
+ reader.close();
47
+ return true;
48
+ }
49
+ }
50
+ return false;
51
+ }
52
+
53
+ /**
54
+ * Find all valid test files given the globs
55
+ */
56
+ static async* getTestImports(globs?: string[]): AsyncIterable<string> {
57
+ const all = RuntimeIndex.find({
58
+ module: mod => mod.roles.includes('test') || mod.roles.includes('std'),
59
+ folder: folder => folder === 'test',
60
+ file: file => file.role === 'test'
61
+ });
62
+
63
+ // Collect globs
64
+ if (globs?.length) {
65
+ const allFiles = new Map(all.map(file => [file.sourceFile, file]));
66
+ for await (const item of fs.glob(globs)) {
67
+ const source = Runtime.workspaceRelative(path.resolve(item));
68
+ const match = allFiles.get(source);
69
+ if (match && await this.isTestFile(match.sourceFile)) {
70
+ yield match.import;
71
+ }
72
+ }
73
+ } else {
74
+ for await (const match of all) {
75
+ if (await this.isTestFile(match.sourceFile)) {
76
+ yield match.import;
77
+ }
78
+ }
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Get count of tests for a given set of globs
84
+ * @param input
85
+ */
86
+ static async resolveGlobInput({ globs, tags, metadata }: TestGlobInput): Promise<TestRun[]> {
87
+ const digestProcess = await ExecUtil.getResult(
88
+ ExecUtil.spawnTrv('test:digest', ['-o', 'json', ...globs], {
89
+ env: { ...process.env, ...Env.FORCE_COLOR.export(0), ...Env.NO_COLOR.export(true) },
90
+ }),
91
+ { catch: true }
92
+ );
93
+
94
+ if (!digestProcess.valid) {
95
+ throw new Error(digestProcess.stderr);
96
+ }
97
+
98
+ const testFilter = tags?.length ?
99
+ Util.allowDeny<string, [TestConfig]>(
100
+ tags,
101
+ rule => rule,
102
+ (rule, core) => core.tags?.includes(rule) ?? false
103
+ ) :
104
+ ((): boolean => true);
105
+
106
+ const parsed: TestConfig[] = JSONUtil.parseSafe(digestProcess.stdout);
107
+
108
+ const events = parsed.filter(testFilter).reduce((runs, test) => {
109
+ if (!runs.has(test.classId)) {
110
+ runs.set(test.classId, { import: test.import, classId: test.classId, methodNames: [], runId: Util.uuid(), metadata });
111
+ }
112
+ runs.get(test.classId)!.methodNames!.push(test.methodName);
113
+ return runs;
114
+ }, new Map<string, TestRun>());
115
+
116
+ return [...events.values()].sort((a, b) => a.runId!.localeCompare(b.runId!));
117
+ }
118
+
119
+ /**
120
+ * Resolve a test diff source to ensure we are only running changed tests
121
+ */
122
+ static async resolveDiffInput({ import: importPath, diffSource: diff, metadata }: TestDiffInput): Promise<RunState> {
123
+ // Runs, defaults to new classes
124
+ const runs: TestRun[] = [];
125
+ const addRun = (clsId: string | undefined, methods?: string[]): void => {
126
+ runs.push({ import: importPath, classId: clsId, methodNames: methods?.length ? methods : undefined, metadata });
127
+ };
128
+ const removes: TestRemoveEvent[] = [];
129
+ const removeTest = (clsId: string, methodName?: string): void => {
130
+ removes.push({ type: 'removeTest', import: importPath, classId: clsId, methodName });
131
+ };
132
+
133
+ const imported = await Registry.manualInit([importPath]);
134
+ const classes = Object.fromEntries(
135
+ imported
136
+ .filter(cls => SuiteRegistryIndex.hasConfig(cls))
137
+ .map(cls => [cls.Ⲑid, SuiteRegistryIndex.getConfig(cls)])
138
+ );
139
+
140
+ // New classes
141
+ for (const clsId of Object.keys(classes)) {
142
+ if (!diff[clsId]) {
143
+ addRun(clsId);
144
+ }
145
+ }
146
+
147
+ // Looking at Diff
148
+ for (const [clsId, config] of Object.entries(diff)) {
149
+ const local = classes[clsId];
150
+ if (!local) { // Removed classes
151
+ removeTest(clsId);
152
+ } else if (local.sourceHash !== config.sourceHash) { // Class changed or added
153
+ // Methods to run, defaults to newly added
154
+ const methods: string[] = Object.keys(local.tests ?? {}).filter(key => !config.methods[key]);
155
+ let didRemove = false;
156
+ for (const key of Object.keys(config.methods)) {
157
+ const localMethod = local.tests?.[key];
158
+ if (!localMethod) { // Test is removed
159
+ removeTest(clsId, key);
160
+ didRemove = true;
161
+ } else if (localMethod.sourceHash !== config.methods[key]) { // Method changed or added
162
+ methods.push(key);
163
+ }
164
+ }
165
+ if (!didRemove || methods.length > 0) {
166
+ addRun(clsId, methods);
167
+ }
168
+ }
169
+ }
170
+
171
+ if (runs.length === 0 && removes.length === 0) { // Re-run entire file, classes unchanged
172
+ addRun(undefined);
173
+ }
174
+
175
+ return { runs, removes };
176
+ }
177
+
178
+ /**
179
+ * Reinitialize the manifest if needed, mainly for single test runs
180
+ */
181
+ static async reinitManifestIfNeeded(runs: TestRun[]): Promise<void> {
182
+ if (runs.length === 1) {
183
+ const [run] = runs;
184
+ const entry = RuntimeIndex.getFromImport(run.import)!;
185
+
186
+ if (entry.module !== Runtime.main.name) {
187
+ RuntimeIndex.reinitForModule(entry.module);
188
+ }
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Build test consumer that wraps a given targeted consumer, and the tests to be run
194
+ */
195
+ static async getRunnableConsumer(target: TestConsumerShape, testRuns: TestRun[]): Promise<RunnableTestConsumer> {
196
+ const consumer = new RunnableTestConsumer(target);
197
+ const testCount = testRuns.reduce((acc, cur) => acc + (cur.methodNames ? cur.methodNames.length : 0), 0);
198
+
199
+ await consumer.onStart({ testCount });
200
+ return consumer;
201
+ }
202
+
203
+ /**
204
+ * Resolve input into run state
205
+ */
206
+ static async resolveInput(input: TestRunInput): Promise<RunState> {
207
+ if ('diffSource' in input) {
208
+ return await this.resolveDiffInput(input);
209
+ } else if ('globs' in input) {
210
+ return { runs: await this.resolveGlobInput(input) };
211
+ } else {
212
+ return { runs: [input], removes: [] };
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Run tests
218
+ */
219
+ static async runTests(consumerConfig: TestConsumerConfig, input: TestRunInput): Promise<boolean | undefined> {
220
+ const { runs, removes } = await this.resolveInput(input);
221
+
222
+ await this.reinitManifestIfNeeded(runs);
223
+
224
+ const targetConsumer = await TestConsumerRegistryIndex.getInstance(consumerConfig);
225
+ const consumer = await this.getRunnableConsumer(targetConsumer, runs);
226
+
227
+ for (const item of removes ?? []) {
228
+ consumer.onRemoveEvent(item);
229
+ }
230
+
231
+ if (runs.length === 1) {
232
+ await new TestExecutor(consumer).execute(runs[0]);
233
+ } else {
234
+ await WorkPool.run(
235
+ run => buildStandardTestManager(consumer, run),
236
+ runs,
237
+ {
238
+ idleTimeoutMillis: TimeUtil.asMillis(10, 's'),
239
+ min: 1,
240
+ max: consumerConfig.concurrency
241
+ }
242
+ );
243
+ }
244
+
245
+ return consumer.summarizeAsBoolean();
246
+ }
247
+ }
@@ -1,9 +1,7 @@
1
- import { TestRun } from '../model/test.ts';
2
-
3
1
  /**
4
- * Run state
2
+ * Test Consumer Configuration
5
3
  */
6
- export interface RunState {
4
+ export interface TestConsumerConfig {
7
5
  /**
8
6
  * Test result consumer
9
7
  */
@@ -16,17 +14,4 @@ export interface RunState {
16
14
  * Number of test suites to run concurrently, when mode is not single
17
15
  */
18
16
  concurrency?: number;
19
- /**
20
- * The tags to include or exclude from testing
21
- */
22
- tags?: string[];
23
- /**
24
- * target
25
- */
26
- target: TestRun | {
27
- /**
28
- * Globs to run
29
- */
30
- globs?: string[];
31
- };
32
17
  }
@@ -1,14 +1,14 @@
1
+ import { ManifestModuleUtil } from '@travetto/manifest';
1
2
  import { Registry } from '@travetto/registry';
2
3
  import { WorkPool } from '@travetto/worker';
3
- import { AsyncQueue, Runtime, RuntimeIndex, castTo, describeFunction } from '@travetto/runtime';
4
+ import { AsyncQueue, RuntimeIndex, TimeUtil, watchCompiler } from '@travetto/runtime';
4
5
 
5
6
  import { buildStandardTestManager } from '../worker/standard.ts';
6
7
  import { TestConsumerRegistryIndex } from '../consumer/registry-index.ts';
7
8
  import { CumulativeSummaryConsumer } from '../consumer/types/cumulative.ts';
8
- import { TestRun } from '../model/test.ts';
9
- import { RunnerUtil } from './util.ts';
10
- import { TestReadyEvent, TestRemovedEvent } from '../worker/types.ts';
11
- import { SuiteRegistryIndex } from '../registry/registry-index.ts';
9
+ import type { TestDiffInput, TestRun } from '../model/test.ts';
10
+ import { RunUtil } from './run.ts';
11
+ import { isTestRunEvent, type TestReadyEvent } from '../worker/types.ts';
12
12
 
13
13
  /**
14
14
  * Test Watcher.
@@ -25,77 +25,51 @@ export class TestWatcher {
25
25
 
26
26
  await Registry.init();
27
27
 
28
- const events: TestRun[] = [];
28
+ const events: (TestRun | TestDiffInput)[] = [];
29
29
 
30
30
  if (runAllOnStart) {
31
- const tests = await RunnerUtil.getTestDigest();
32
- events.push(...RunnerUtil.getTestRuns(tests));
31
+ events.push(...await RunUtil.resolveGlobInput({ globs: [] }));
33
32
  }
34
33
 
35
34
  const queue = new AsyncQueue(events);
36
35
  const consumer = new CumulativeSummaryConsumer(
37
36
  await TestConsumerRegistryIndex.getInstance({ consumer: format })
38
- )
39
- .withFilter(event => event.metadata?.partial !== true || event.type !== 'suite');
40
-
41
- Registry.onMethodChange((event) => {
42
- const [cls, method] = 'previous' in event ? event.previous : event.current;
43
-
44
- if (!cls || describeFunction(cls).abstract) {
45
- return;
46
- }
47
-
48
- const classId = cls.Ⲑid;
49
- if (!method) {
50
- consumer.removeClass(classId);
51
- return;
52
- }
53
-
54
- const config = SuiteRegistryIndex.getTestConfig(cls, method)!;
55
- if (event.type !== 'removing') {
56
- if (config) {
57
- const run: TestRun = {
58
- import: config.import, classId: config.classId, methodNames: [config.methodName], metadata: { partial: true }
59
- };
60
- console.log('Triggering', run);
61
- queue.add(run, true); // Shift to front
62
- }
63
- } else {
64
- process.send?.({
65
- type: 'removeTest',
66
- methodNames: method?.name ? [method.name!] : undefined!,
67
- method: method?.name,
68
- classId,
69
- import: Runtime.getImport(cls)
70
- } satisfies TestRemovedEvent);
71
- }
72
- }, SuiteRegistryIndex);
73
-
74
- // If a file is changed, but doesn't emit classes, re-run whole file
75
- Registry.onNonClassChanges(imp => queue.add({ import: imp }));
37
+ );
76
38
 
77
39
  process.on('message', event => {
78
- if (typeof event === 'object' && event && 'type' in event && event.type === 'run-test') {
79
- console.log('Received message', event);
80
- // Legacy
81
- if ('file' in event && typeof event.file === 'string') {
82
- event = { import: RuntimeIndex.getFromSource(event.file)?.import! };
83
- }
84
- console.debug('Manually triggered', event);
85
- queue.add(castTo(event), true);
40
+ if (isTestRunEvent(event)) {
41
+ queue.add(event, true);
86
42
  }
87
43
  });
88
44
 
89
45
  process.send?.({ type: 'ready' } satisfies TestReadyEvent);
90
46
 
91
- await WorkPool.run(
47
+ const queueProcessor = WorkPool.run(
92
48
  buildStandardTestManager.bind(null, consumer),
93
49
  queue,
94
50
  {
95
- idleTimeoutMillis: 120000,
51
+ idleTimeoutMillis: TimeUtil.asMillis('2m'),
96
52
  min: 2,
97
53
  max: WorkPool.DEFAULT_SIZE
98
54
  }
99
55
  );
56
+
57
+ for await (const event of watchCompiler()) {
58
+ const fileType = ManifestModuleUtil.getFileType(event.file);
59
+ if (
60
+ (fileType === 'ts' || fileType === 'js') &&
61
+ RuntimeIndex.findModuleForArbitraryFile(event.file) !== undefined
62
+ ) {
63
+ if (event.action === 'delete') {
64
+ consumer.removeTest(event.import);
65
+ } else {
66
+ const diffSource = consumer.produceDiffSource(event.import);
67
+ queue.add({ import: event.import, diffSource }, true);
68
+ }
69
+ }
70
+ }
71
+
72
+ // Cleanup
73
+ await queueProcessor;
100
74
  }
101
75
  }
@@ -29,6 +29,10 @@ export interface SuiteCore {
29
29
  * Description
30
30
  */
31
31
  description?: string;
32
+ /**
33
+ * Hash of the suite/test source code
34
+ */
35
+ sourceHash?: number;
32
36
  }
33
37
 
34
38
  /**
@@ -14,6 +14,8 @@ export type EventPhase = 'before' | 'after';
14
14
  type EventTpl<T extends EventEntity, P extends EventPhase, V extends {}> =
15
15
  { type: T, phase: P, metadata?: Record<string, unknown> } & V;
16
16
 
17
+ export type TestRemoveEvent = { type: 'removeTest', import: string, classId?: string, methodName?: string, metadata?: Record<string, unknown> };
18
+
17
19
  /**
18
20
  * Different test event shapes
19
21
  */
@@ -22,4 +24,4 @@ export type TestEvent =
22
24
  EventTpl<'test', 'before', { test: TestConfig }> |
23
25
  EventTpl<'test', 'after', { test: TestResult }> |
24
26
  EventTpl<'suite', 'before', { suite: SuiteConfig }> |
25
- EventTpl<'suite', 'after', { suite: SuiteResult }>;
27
+ EventTpl<'suite', 'after', { suite: SuiteResult }>;
@@ -1,6 +1,6 @@
1
1
  import type { Class } from '@travetto/runtime';
2
2
 
3
- import { Assertion, TestConfig, TestResult } from './test.ts';
3
+ import { Assertion, TestConfig, TestResult, type TestStatus } from './test.ts';
4
4
  import { Skip, SuiteCore } from './common.ts';
5
5
 
6
6
  /**
@@ -48,37 +48,26 @@ export interface Counts {
48
48
  passed: number;
49
49
  skipped: number;
50
50
  failed: number;
51
+ unknown: number;
51
52
  total: number;
52
53
  }
53
54
 
54
55
  /**
55
56
  * Results of a suite run
56
57
  */
57
- export interface SuiteResult extends Counts {
58
+ export interface SuiteResult extends Counts, SuiteCore {
58
59
  /**
59
- * Class identifier
60
+ * All test results
60
61
  */
61
- classId: string;
62
- /**
63
- * Import for the suite
64
- */
65
- import: string;
66
- /**
67
- * Start of the suite
68
- */
69
- lineStart: number;
70
- /**
71
- * End of the suite
72
- */
73
- lineEnd: number;
74
- /**
75
- * ALl test results
76
- */
77
- tests: TestResult[];
62
+ tests: Record<string, TestResult>;
78
63
  /**
79
64
  * Suite duration
80
65
  */
81
66
  duration: number;
67
+ /**
68
+ * Overall status
69
+ */
70
+ status: TestStatus;
82
71
  }
83
72
 
84
73
  /**
package/src/model/test.ts CHANGED
@@ -5,6 +5,13 @@ import { Skip, TestCore } from './common.ts';
5
5
  export type ThrowableError = string | RegExp | Class<Error> | ((error: Error | string) => boolean | void | undefined);
6
6
  export type TestLog = Omit<ConsoleEvent, 'args' | 'scope'> & { message: string };
7
7
 
8
+ export type TestDiffSource = Record<string, {
9
+ sourceHash: number;
10
+ methods: Record<string, number>;
11
+ }>;
12
+
13
+ export type TestStatus = 'passed' | 'skipped' | 'failed' | 'unknown';
14
+
8
15
  /**
9
16
  * Specific configuration for a test
10
17
  */
@@ -88,7 +95,7 @@ export interface TestResult extends TestCore {
88
95
  /**
89
96
  * status
90
97
  */
91
- status: 'passed' | 'skipped' | 'failed';
98
+ status: TestStatus;
92
99
  /**
93
100
  * Error if failed
94
101
  */
@@ -136,3 +143,42 @@ export type TestRun = {
136
143
  */
137
144
  runId?: string;
138
145
  };
146
+
147
+ /**
148
+ * Test Diff Input
149
+ */
150
+ export type TestDiffInput = {
151
+ /**
152
+ * Import for run
153
+ */
154
+ import: string;
155
+ /**
156
+ * Diff Source
157
+ */
158
+ diffSource: TestDiffSource;
159
+ /**
160
+ * Test run metadata
161
+ */
162
+ metadata?: Record<string, unknown>;
163
+ };
164
+
165
+ /**
166
+ * Test Glob Input
167
+ */
168
+ export type TestGlobInput = {
169
+ /**
170
+ * Globs to run
171
+ */
172
+ globs: string[];
173
+ /**
174
+ * Tags to filter by
175
+ */
176
+ tags?: string[];
177
+ /**
178
+ * Test run metadata
179
+ */
180
+ metadata?: Record<string, unknown>;
181
+ };
182
+
183
+
184
+ export type TestRunInput = TestRun | TestDiffInput | TestGlobInput;
@@ -0,0 +1,8 @@
1
+ import type { Counts } from './suite';
2
+ import type { TestStatus } from './test';
3
+
4
+ export class TestModelUtil {
5
+ static countsToTestStatus(counts: Counts): TestStatus {
6
+ return counts.failed ? 'failed' : (counts.passed ? 'passed' : counts.skipped ? 'skipped' : 'unknown');
7
+ }
8
+ }