Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/test/testExecutor.ts
13383 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
import * as path from 'path';
6
import { IPromptWorkspaceLabels, PromptWorkspaceLabels } from '../src/extension/context/node/resolvers/promptWorkspaceLabels';
7
import { INewWorkspacePreviewContentManager, NewWorkspacePreviewContentManagerImpl } from '../src/extension/intents/node/newIntent';
8
import { IntentError } from '../src/extension/prompt/node/intents';
9
import { ISimulationModelConfig } from '../src/extension/test/node/services';
10
import { IToolsService } from '../src/extension/tools/common/toolsService';
11
import { TestToolsService } from '../src/extension/tools/node/test/testToolsService';
12
import { IEndpointProvider } from '../src/platform/endpoint/common/endpointProvider';
13
import { TestEndpointProvider } from '../src/platform/endpoint/test/node/testEndpointProvider';
14
import { ConsoleLog, ILogService, LogServiceImpl } from '../src/platform/log/common/logService';
15
import { APIUsage } from '../src/platform/networking/common/openai';
16
import { ISimulationTestContext } from '../src/platform/simulationTestContext/common/simulationTestContext';
17
import { ITasksService } from '../src/platform/tasks/common/tasksService';
18
import { TestTasksService } from '../src/platform/tasks/common/testTasksService';
19
import { TestingServiceCollection } from '../src/platform/test/node/services';
20
import { ITokenizerProvider } from '../src/platform/tokenizer/node/tokenizer';
21
import { count } from '../src/util/common/arrays';
22
import { WellKnownLanguageId } from '../src/util/common/languages';
23
import { groupBy } from '../src/util/vs/base/common/collections';
24
import { BugIndicatingError } from '../src/util/vs/base/common/errors';
25
import { Lazy } from '../src/util/vs/base/common/lazy';
26
import { safeStringify } from '../src/util/vs/base/common/objects';
27
import { SyncDescriptor } from '../src/util/vs/platform/instantiation/common/descriptors';
28
import { SimulationExtHostToolsService } from './base/extHostContext/simulationExtHostToolsService';
29
import { SimulationBaseline, TestBaselineComparison } from './base/simulationBaseline';
30
import { CacheMode, createSimulationAccessor, CurrentTestRunInfo, SimulationServicesOptions } from './base/simulationContext';
31
import { ISimulationEndpointHealth } from './base/simulationEndpointHealth';
32
import { SimulationOptions } from './base/simulationOptions';
33
import { ISimulationOutcome } from './base/simulationOutcome';
34
import { FetchRequestCollector } from './base/spyingChatMLFetcher';
35
import { ISimulationTestRuntime, SimulationTest, SimulationTestRuntime, toDirname } from './base/stest';
36
import { IJSONOutputPrinter } from './jsonOutputPrinter';
37
import { green, red, violet, yellow } from './outputColorer';
38
import { ExternalSimulationTestRuntime } from './simulation/externalScenarios';
39
import * as shared from './simulation/shared/sharedTypes';
40
import { ITestSnapshots, TestSnapshotsImpl } from './simulation/testSnapshot';
41
import { TaskRunner } from './taskRunner';
42
import { TestExecutionInExtension } from './testExecutionInExtension';
43
import { createScoreRenderer, printTime } from './util';
44
45
/**
46
* Represents outcome of N runs of a scenario.
47
*/
48
export interface ITestResult {
49
test: string;
50
outcomeDirectory: string;
51
conversationPath?: string;
52
score: number;
53
usage: APIUsage;
54
// FIXME@ulugbekna: specify when the outcome is undefined
55
outcomes: (shared.SimulationTestOutcome | undefined)[];
56
duration: number;
57
cacheInfo: TestRunCacheInfo[];
58
originalResults: ITestRunResult[];
59
}
60
61
interface ITestRunResultCommon {
62
contentFilterCount: number;
63
usage: APIUsage;
64
cacheInfo: TestRunCacheInfo;
65
hasCacheMiss: boolean;
66
}
67
68
interface ITestRunResultPass extends ITestRunResultCommon {
69
kind: 'pass';
70
explicitScore: number | undefined;
71
duration: number;
72
outcome: shared.SimulationTestOutcome | undefined;
73
}
74
75
interface ITestRunResultFail extends ITestRunResultCommon {
76
kind: 'fail';
77
message: string;
78
duration: number;
79
outcome: shared.SimulationTestOutcome;
80
}
81
82
/**
83
* Represents outcome of a single run of a scenario.
84
*/
85
export type ITestRunResult = ITestRunResultPass | ITestRunResultFail;
86
87
export type CacheInfo = { type: 'request'; key: string }; // TODO: add other caches here
88
89
export type TestRunCacheInfo = CacheInfo[];
90
91
export interface SimulationTestContext {
92
opts: SimulationOptions;
93
baseline: SimulationBaseline;
94
canUseBaseline: boolean;
95
jsonOutputPrinter: IJSONOutputPrinter;
96
outputPath: string;
97
externalScenariosPath?: string;
98
modelConfig: ISimulationModelConfig;
99
simulationEndpointHealth: ISimulationEndpointHealth;
100
simulationServicesOptions: SimulationServicesOptions;
101
simulationOutcome: ISimulationOutcome;
102
tokenizerProvider: ITokenizerProvider;
103
}
104
105
export type GroupedScores = Map<string, Map<WellKnownLanguageId | undefined, Map<string | undefined, number[]>>>;
106
107
function mergeGroupedScopes(into: GroupedScores, from: GroupedScores) {
108
for (const [key, value] of from) {
109
const intoValue = into.get(key);
110
if (!intoValue) {
111
into.set(key, value);
112
continue;
113
}
114
115
for (const [language, scores] of value) {
116
const intoScores = intoValue.get(language);
117
if (intoScores) {
118
for (const [model, score] of scores) {
119
if (intoScores.has(model)) {
120
intoScores.set(model, [...intoScores.get(model)!, ...score]);
121
} else {
122
intoScores.set(model, score);
123
}
124
}
125
} else {
126
intoValue.set(language, scores);
127
}
128
}
129
}
130
}
131
132
export type ExecuteTestResult = {
133
testResultsPromises: Promise<ITestResult>[];
134
getGroupedScores(): Promise<GroupedScores>;
135
};
136
137
export async function executeTests(ctx: SimulationTestContext, testsToRun: readonly SimulationTest[]): Promise<ExecuteTestResult> {
138
const location = groupBy(testsToRun as SimulationTest[], test => (test.suite.extHost ?? ctx.opts.inExtensionHost) ? 'extHost' : 'local');
139
140
const extensionRunner = new Lazy(() => TestExecutionInExtension.create(ctx));
141
const [extHost, local] = await Promise.all([
142
executeTestsUsing(ctx, location['extHost'] ?? [], (...args) => extensionRunner.value.then(e => e.executeTest(...args))),
143
executeTestsUsing(ctx, location['local'] ?? [], executeTestOnce),
144
]);
145
146
return {
147
testResultsPromises: [...extHost.testResultsPromises, ...local.testResultsPromises],
148
getGroupedScores: async () => {
149
const [fromExtHost, fromLocal] = await Promise.all([extHost.getGroupedScores(), local.getGroupedScores()]);
150
await extensionRunner.rawValue?.then(r => r.dispose());
151
mergeGroupedScopes(fromLocal, fromExtHost);
152
return fromLocal;
153
},
154
};
155
}
156
157
async function executeTestsUsing(ctx: SimulationTestContext, testsToRun: readonly SimulationTest[], executeTestFn: ExecuteTestOnceFn): Promise<ExecuteTestResult> {
158
const { opts, jsonOutputPrinter } = ctx;
159
const groupedScores: Map<string, Map<WellKnownLanguageId | undefined, Map<string | undefined, number[]>>> = new Map();
160
161
const taskRunner = new TaskRunner(opts.parallelism);
162
163
const testResultsPromises: Promise<ITestResult>[] = [];
164
for (const test of testsToRun) {
165
166
if (test.options.optional && (test.options.skip(ctx.opts) || opts.ci)) { // CI never runs optional stests
167
// Avoid spamming the console, we now have very many skipped stests
168
// console.log(` Skipping ${test.fullName}`);
169
ctx.baseline.setSkippedTest(test.fullName);
170
jsonOutputPrinter.print({ type: shared.OutputType.skippedTest, name: test.fullName });
171
continue;
172
}
173
174
const testRun = executeTestNTimes(ctx, taskRunner, test, groupedScores, executeTestFn);
175
176
testResultsPromises.push(testRun);
177
178
if (opts.parallelism === 1) {
179
await testRun;
180
}
181
}
182
183
return {
184
testResultsPromises,
185
getGroupedScores: async () => {
186
await Promise.all(testResultsPromises);
187
return groupedScores;
188
},
189
};
190
}
191
192
/** Runs a single scenario `nRuns` times. */
193
async function executeTestNTimes(
194
ctx: SimulationTestContext,
195
taskRunner: TaskRunner,
196
test: SimulationTest,
197
groupedScores: Map<string, Map<WellKnownLanguageId | undefined, Map<string | undefined, number[]>>>,
198
executeTestFn: ExecuteTestOnceFn
199
): Promise<ITestResult> {
200
201
const { opts } = ctx;
202
203
const outcomeDirectory = path.join(ctx.outputPath, toDirname(test.fullName));
204
205
const testStartTime = Date.now();
206
207
const scheduledTestRuns: Promise<ITestRunResult>[] = [];
208
for (let kthRun = 0; kthRun < opts.nRuns; kthRun++) {
209
scheduledTestRuns.push(taskRunner.run(() => executeTestFn(ctx, taskRunner.parallelism, outcomeDirectory, test, kthRun)));
210
}
211
212
const runResults: ITestRunResult[] = await Promise.all(scheduledTestRuns);
213
214
const testElapsedTime = Date.now() - testStartTime;
215
216
const testSummary = {
217
results: runResults,
218
hasCacheMisses: runResults.some(x => x.hasCacheMiss),
219
contentFilterCount: runResults.filter(x => x.contentFilterCount > 0).length,
220
};
221
222
if (!opts.externalScenarios) {
223
await ctx.simulationOutcome.set(test, testSummary.results);
224
}
225
226
const testResultToScore = (result: ITestRunResult) => result.kind === 'pass' ? (result.explicitScore ?? 1) : 0;
227
228
const scoreTotal = Math.round(testSummary.results.reduce((total, result) => total + testResultToScore(result), 0) * 1000) / 1000;
229
230
const currentScore = scoreTotal / testSummary.results.length;
231
232
const currentPassCount = count(testSummary.results, s => s.kind === 'pass');
233
234
const baselineComparison = ctx.baseline.setCurrentResult({
235
name: test.fullName,
236
optional: test.options.optional ? true : undefined,
237
contentFilterCount: testSummary.contentFilterCount,
238
passCount: currentPassCount,
239
failCount: testSummary.results.length - currentPassCount,
240
score: currentScore,
241
attributes: test.attributes
242
});
243
244
printTestRunResultsToCli({ testSummary, ctx, test, currentScore, testElapsedTime, baselineComparison, });
245
246
if (opts.verbose !== undefined) {
247
printVerbose(opts, testSummary);
248
}
249
250
updateGroupedScores({ test, currentScore, groupedScores });
251
252
const duration = testSummary.results.reduce((acc, c) => acc + c.duration, 0);
253
254
const initial: APIUsage = { completion_tokens: 0, prompt_tokens: 0, total_tokens: 0, prompt_tokens_details: { cached_tokens: 0 } };
255
const usage: APIUsage = testSummary.results.reduce((acc, c): APIUsage => {
256
if (c.usage === undefined) { return acc; }
257
const { completion_tokens, prompt_tokens, total_tokens, prompt_tokens_details } = c.usage;
258
return {
259
completion_tokens: acc.completion_tokens + completion_tokens,
260
prompt_tokens: acc.prompt_tokens + prompt_tokens,
261
total_tokens: acc.total_tokens + total_tokens,
262
prompt_tokens_details: {
263
cached_tokens: (acc.prompt_tokens_details?.cached_tokens ?? 0) + (prompt_tokens_details?.cached_tokens ?? 0),
264
}
265
} satisfies APIUsage;
266
}, initial);
267
268
return {
269
test: test.fullName,
270
outcomeDirectory: path.relative(ctx.outputPath, outcomeDirectory),
271
conversationPath: test.options.conversationPath,
272
score: currentScore,
273
duration,
274
usage,
275
outcomes: testSummary.results.map(r => r.outcome),
276
cacheInfo: testSummary.results.map(r => r.cacheInfo),
277
originalResults: testSummary.results,
278
};
279
}
280
281
function printTestRunResultsToCli({ testSummary, ctx, test, currentScore, testElapsedTime, baselineComparison }: {
282
testSummary: {
283
contentFilterCount: number;
284
results: ITestRunResult[];
285
hasCacheMisses: boolean;
286
};
287
ctx: SimulationTestContext;
288
test: SimulationTest;
289
currentScore: number;
290
testElapsedTime: number;
291
baselineComparison: TestBaselineComparison;
292
}) {
293
294
const scoreToString = createScoreRenderer(ctx.opts, ctx.canUseBaseline);
295
const didScoreChange = !baselineComparison.isNew && baselineComparison.prevScore !== baselineComparison.currScore;
296
const prettyScoreValue = didScoreChange
297
? `${scoreToString(baselineComparison.prevScore)} -> ${scoreToString(baselineComparison.currScore)}`
298
: `${scoreToString(currentScore)}`;
299
300
let icon = '=';
301
let color = (x: string | number) => x;
302
if (baselineComparison.isNew) {
303
icon = '◆';
304
color = violet;
305
} else if (baselineComparison.isImproved) {
306
icon = '▲';
307
color = green;
308
} else if (baselineComparison.isWorsened) {
309
icon = '▼';
310
color = red;
311
}
312
313
const prettyTestTime = ctx.opts.parallelism === 1 ? ` (${(testElapsedTime > 10 ? yellow(printTime(testElapsedTime)) : printTime(testElapsedTime))})` : '';
314
315
const prettyContentFilter = (testSummary.contentFilterCount ? yellow(` (⚠️ content filter affected ${testSummary.contentFilterCount} runs)`) : '');
316
317
const hadCacheMisses = testSummary.hasCacheMisses ? yellow(' (️️️💸 cache miss)') : '';
318
319
console.log(` ${color(icon)} [${color(prettyScoreValue)}] ${color(test.fullName)}${prettyTestTime}${hadCacheMisses}${prettyContentFilter}`);
320
}
321
322
function printVerbose(
323
opts: SimulationOptions,
324
testSummary: {
325
contentFilterCount: number;
326
results: ITestRunResult[];
327
}
328
) {
329
for (let i = 0; i < testSummary.results.length; i++) {
330
const result = testSummary.results[i];
331
332
console.log(` ${i + 1} - ${result.kind === 'pass' ? green(result.kind) : red(result.kind)}`);
333
if (result.kind === 'fail' && result.message && opts.verbose !== 0) {
334
// indent the message and print
335
console.error(result.message.split(/\r\n|\r|\n/g).map(line => ` ${line}`).join('\n'));
336
}
337
}
338
}
339
340
function updateGroupedScores({ test, currentScore, groupedScores }: {
341
test: SimulationTest;
342
currentScore: number;
343
groupedScores: Map<string, Map<string | undefined, Map<string | undefined, number[]>>>;
344
}) {
345
const suiteName = test.suite.fullName;
346
const model = test.model;
347
if (groupedScores.has(suiteName)) {
348
const scoresPerSuite = groupedScores.get(suiteName);
349
if (scoresPerSuite!.has(test.language)) {
350
const scoresPerLanguage = scoresPerSuite!.get(test.language);
351
if (scoresPerLanguage!.has(model)) {
352
scoresPerLanguage!.set(model, [...scoresPerLanguage!.get(model)!, currentScore]);
353
} else {
354
scoresPerLanguage?.set(model, [currentScore]);
355
}
356
} else {
357
scoresPerSuite!.set(test.language, new Map([[model, [currentScore]]]));
358
}
359
} else {
360
groupedScores.set(suiteName, new Map());
361
groupedScores.get(suiteName)!.set(test.language, new Map([[model, [currentScore]]]));
362
}
363
}
364
365
type ExecuteTestOnceFn = (
366
ctx: SimulationTestContext,
367
parallelism: number,
368
outcomeDirectory: string,
369
test: SimulationTest,
370
runNumber: number,
371
) => Promise<ITestRunResult>;
372
373
export const executeTestOnce = async (
374
ctx: SimulationTestContext,
375
parallelism: number,
376
outcomeDirectory: string,
377
test: SimulationTest,
378
runNumber: number,
379
isInRealExtensionHost = false,
380
) => {
381
const { opts, jsonOutputPrinter } = ctx;
382
const fetchRequestCollector = new FetchRequestCollector();
383
384
const currentTestRunInfo: CurrentTestRunInfo = {
385
test,
386
testRunNumber: runNumber,
387
fetchRequestCollector: fetchRequestCollector,
388
isInRealExtensionHost,
389
};
390
391
let testingServiceCollection: TestingServiceCollection;
392
try {
393
testingServiceCollection = await createSimulationAccessor(
394
ctx.modelConfig,
395
ctx.simulationServicesOptions,
396
currentTestRunInfo
397
);
398
} catch (e) {
399
const msg = e instanceof Error ? (e.stack ?? e.message) : String(e);
400
console.error(`Error in createSimulationAccessor`, e);
401
jsonOutputPrinter.print({ type: shared.OutputType.testRunStart, name: test.fullName, runNumber } satisfies shared.ITestRunStartOutput);
402
jsonOutputPrinter.print({
403
type: shared.OutputType.testRunEnd,
404
name: test.fullName,
405
runNumber,
406
duration: 0,
407
writtenFiles: [],
408
error: msg,
409
pass: false,
410
explicitScore: undefined,
411
annotations: undefined,
412
averageRequestDuration: undefined,
413
requestCount: 0,
414
hasCacheMiss: false,
415
} satisfies shared.ITestRunEndOutput);
416
return {
417
kind: 'fail',
418
message: msg,
419
contentFilterCount: 0,
420
duration: 0,
421
usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
422
outcome: { kind: 'failed', error: msg, hitContentFilter: false, critical: true },
423
cacheInfo: [],
424
hasCacheMiss: false,
425
} satisfies ITestRunResultFail;
426
}
427
428
testingServiceCollection.define(ISimulationOutcome, ctx.simulationOutcome);
429
testingServiceCollection.define(ITokenizerProvider, ctx.tokenizerProvider);
430
testingServiceCollection.define(ISimulationEndpointHealth, ctx.simulationEndpointHealth);
431
testingServiceCollection.define(IJSONOutputPrinter, ctx.jsonOutputPrinter);
432
testingServiceCollection.define(ITasksService, new TestTasksService());
433
434
if (test.model || test.embeddingType) {
435
// We prefer opts that come from the CLI over test specific args since Opts are global and must apply to the entire simulation
436
const smartChatModel = (opts.smartChatModel ?? opts.chatModel) ?? test.model;
437
const fastChatModel = (opts.fastChatModel ?? opts.chatModel) ?? test.model;
438
const fastRewriteModel = (opts.fastRewriteModel ?? opts.chatModel) ?? test.model;
439
testingServiceCollection.define(IEndpointProvider, new SyncDescriptor(TestEndpointProvider, [smartChatModel, fastChatModel, fastRewriteModel, currentTestRunInfo, opts.modelCacheMode === CacheMode.Disable, undefined]));
440
}
441
442
const simulationTestRuntime = (ctx.externalScenariosPath !== undefined)
443
? new ExternalSimulationTestRuntime(ctx.outputPath, outcomeDirectory, runNumber)
444
: new SimulationTestRuntime(ctx.outputPath, outcomeDirectory, runNumber);
445
testingServiceCollection.define(ISimulationTestRuntime, simulationTestRuntime);
446
testingServiceCollection.define(ISimulationTestContext, simulationTestRuntime);
447
testingServiceCollection.define(ILogService, new SyncDescriptor(LogServiceImpl, [[new ConsoleLog(`🪵 ${currentTestRunInfo.test.fullName} (Run #${currentTestRunInfo.testRunNumber + 1}):\n`), simulationTestRuntime]]));
448
449
testingServiceCollection.define(INewWorkspacePreviewContentManager, new SyncDescriptor(NewWorkspacePreviewContentManagerImpl));
450
451
let snapshots: TestSnapshotsImpl | undefined;
452
if (test.options.location) {
453
snapshots = new TestSnapshotsImpl(test.options.location.path, test.fullName, runNumber);
454
testingServiceCollection.define(ITestSnapshots, snapshots);
455
}
456
457
testingServiceCollection.define(IPromptWorkspaceLabels, new SyncDescriptor(PromptWorkspaceLabels));
458
if (isInRealExtensionHost) {
459
testingServiceCollection.define(IToolsService, new SyncDescriptor(SimulationExtHostToolsService, [ctx.simulationServicesOptions.disabledTools]));
460
} else {
461
testingServiceCollection.define(IToolsService, new SyncDescriptor(TestToolsService, [ctx.simulationServicesOptions.disabledTools]));
462
}
463
464
jsonOutputPrinter.print({ type: shared.OutputType.testRunStart, name: test.fullName, runNumber } satisfies shared.ITestRunStartOutput);
465
if (process.stdout.isTTY && parallelism === 1) {
466
process.stdout.write(` Running scenario: ${test.fullName} - ${runNumber + 1}/${opts.nRuns}`.substring(0, process.stdout.columns - 1));
467
}
468
469
const testStartTime = Date.now();
470
let pass = true;
471
let err: unknown | undefined;
472
try {
473
await test.run(testingServiceCollection);
474
await snapshots?.dispose();
475
476
await fetchRequestCollector.complete();
477
478
const result: ITestRunResultPass = {
479
kind: 'pass',
480
explicitScore: simulationTestRuntime.getExplicitScore(),
481
usage: fetchRequestCollector.usage,
482
contentFilterCount: fetchRequestCollector.contentFilterCount,
483
duration: Date.now() - testStartTime,
484
outcome: simulationTestRuntime.getOutcome(),
485
cacheInfo: fetchRequestCollector.cacheInfo,
486
hasCacheMiss: fetchRequestCollector.hasCacheMiss,
487
};
488
489
return result;
490
} catch (e) {
491
pass = false;
492
err = e;
493
let msg = err instanceof Error ? (err.stack ? err.stack : err.message) : safeStringify(err);
494
await fetchRequestCollector.complete();
495
496
let critical = false;
497
if (e instanceof BugIndicatingError || e instanceof TypeError) {
498
critical = true;
499
}
500
if (e instanceof CriticalError) {
501
critical = true;
502
msg = e.message;
503
}
504
505
const result: ITestRunResultFail = {
506
kind: 'fail',
507
message: msg,
508
contentFilterCount: fetchRequestCollector.contentFilterCount,
509
duration: Date.now() - testStartTime,
510
usage: fetchRequestCollector.usage,
511
outcome: {
512
kind: 'failed',
513
error: msg,
514
hitContentFilter: fetchRequestCollector.contentFilterCount > 0,
515
critical,
516
},
517
cacheInfo: fetchRequestCollector.cacheInfo,
518
hasCacheMiss: fetchRequestCollector.hasCacheMiss,
519
};
520
521
return result;
522
} finally {
523
// (context.safeGet(ILanguageFeaturesService) as { dispose?: () => Promise<void> })?.dispose?.();
524
525
await simulationTestRuntime.writeFile(shared.SIMULATION_REQUESTS_FILENAME, JSON.stringify(fetchRequestCollector.interceptedRequests.map(r => r.toJSON()), undefined, 2), shared.REQUESTS_TAG);
526
527
if (err) {
528
simulationTestRuntime.log(`Scenario failed due to an error:`, err);
529
if ((<any>err).code !== 'ERR_ASSERTION' && !(err instanceof IntentError)) {
530
// Make visible to the console unexpected errors
531
console.log(`Scenario ${test.fullName} failed due to an error:`);
532
console.log(err);
533
}
534
}
535
536
await simulationTestRuntime.flushLogs();
537
538
jsonOutputPrinter.print({
539
type: shared.OutputType.testRunEnd,
540
name: test.fullName,
541
runNumber,
542
duration: Date.now() - testStartTime,
543
writtenFiles: simulationTestRuntime.getWrittenFiles(),
544
error: err instanceof Error ? `${err.message}\n${err.stack}` : JSON.stringify(err),
545
pass,
546
explicitScore: simulationTestRuntime.getExplicitScore(),
547
annotations: simulationTestRuntime.getOutcome()?.annotations,
548
averageRequestDuration: fetchRequestCollector.averageRequestDuration,
549
requestCount: fetchRequestCollector.interceptedRequests.length,
550
hasCacheMiss: fetchRequestCollector.hasCacheMiss,
551
} satisfies shared.ITestRunEndOutput);
552
if (process.stdout.isTTY && parallelism === 1) {
553
process.stdout.write('\r\x1b[K');
554
}
555
556
testingServiceCollection.dispose();
557
}
558
};
559
560
/**
561
* When thrown, fails stest CI.
562
*/
563
export class CriticalError extends Error {
564
constructor(message: string) {
565
super(message);
566
this.name = 'CriticalError';
567
}
568
}
569
570