@quenty/nevermore-cli 4.22.0 → 4.23.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 +11 -0
- package/dist/commands/batch-command/batch-deploy-command.js +1 -2
- package/dist/commands/batch-command/batch-deploy-command.js.map +1 -1
- package/dist/commands/batch-command/batch-test-command.d.ts.map +1 -1
- package/dist/commands/batch-command/batch-test-command.js +35 -7
- package/dist/commands/batch-command/batch-test-command.js.map +1 -1
- package/dist/commands/deploy-command/index.js +1 -1
- package/dist/commands/deploy-command/index.js.map +1 -1
- package/dist/commands/test-command/test-command.d.ts.map +1 -1
- package/dist/commands/test-command/test-command.js +6 -3
- package/dist/commands/test-command/test-command.js.map +1 -1
- package/dist/utils/batch/batch-runner.d.ts +2 -1
- package/dist/utils/batch/batch-runner.d.ts.map +1 -1
- package/dist/utils/batch/batch-runner.js +8 -3
- package/dist/utils/batch/batch-runner.js.map +1 -1
- package/dist/utils/build/upload.d.ts.map +1 -1
- package/dist/utils/build/upload.js +10 -1
- package/dist/utils/build/upload.js.map +1 -1
- package/dist/utils/job-context/base-job-context.d.ts +4 -3
- package/dist/utils/job-context/base-job-context.d.ts.map +1 -1
- package/dist/utils/job-context/base-job-context.js +6 -4
- package/dist/utils/job-context/base-job-context.js.map +1 -1
- package/dist/utils/job-context/batch-script-job-context.d.ts +4 -2
- package/dist/utils/job-context/batch-script-job-context.d.ts.map +1 -1
- package/dist/utils/job-context/batch-script-job-context.js +39 -19
- package/dist/utils/job-context/batch-script-job-context.js.map +1 -1
- package/dist/utils/job-context/cloud-job-context.d.ts +3 -3
- package/dist/utils/job-context/cloud-job-context.d.ts.map +1 -1
- package/dist/utils/job-context/cloud-job-context.js +23 -8
- package/dist/utils/job-context/cloud-job-context.js.map +1 -1
- package/dist/utils/job-context/job-context.d.ts +2 -3
- package/dist/utils/job-context/job-context.d.ts.map +1 -1
- package/dist/utils/job-context/local-job-context.d.ts +3 -3
- package/dist/utils/job-context/local-job-context.d.ts.map +1 -1
- package/dist/utils/job-context/local-job-context.js +6 -6
- package/dist/utils/job-context/local-job-context.js.map +1 -1
- package/dist/utils/open-cloud/open-cloud-client.d.ts +1 -1
- package/dist/utils/open-cloud/open-cloud-client.d.ts.map +1 -1
- package/dist/utils/open-cloud/open-cloud-client.js +40 -11
- package/dist/utils/open-cloud/open-cloud-client.js.map +1 -1
- package/dist/utils/testing/parsers/batch-log-parser.d.ts +2 -0
- package/dist/utils/testing/parsers/batch-log-parser.d.ts.map +1 -1
- package/dist/utils/testing/parsers/batch-log-parser.js +3 -1
- package/dist/utils/testing/parsers/batch-log-parser.js.map +1 -1
- package/dist/utils/testing/runner/combined-project-generator.d.ts +9 -0
- package/dist/utils/testing/runner/combined-project-generator.d.ts.map +1 -1
- package/dist/utils/testing/runner/combined-project-generator.js +17 -1
- package/dist/utils/testing/runner/combined-project-generator.js.map +1 -1
- package/dist/utils/testing/runner/test-runner.d.ts +3 -2
- package/dist/utils/testing/runner/test-runner.d.ts.map +1 -1
- package/dist/utils/testing/runner/test-runner.js +5 -5
- package/dist/utils/testing/runner/test-runner.js.map +1 -1
- package/dist/utils/testing/test-log-parser.d.ts +11 -0
- package/dist/utils/testing/test-log-parser.d.ts.map +1 -1
- package/dist/utils/testing/test-log-parser.js +22 -0
- package/dist/utils/testing/test-log-parser.js.map +1 -1
- package/package.json +6 -6
- package/src/commands/batch-command/batch-deploy-command.ts +1 -2
- package/src/commands/batch-command/batch-test-command.ts +42 -10
- package/src/commands/deploy-command/index.ts +1 -1
- package/src/commands/test-command/test-command.ts +6 -2
- package/src/utils/batch/batch-runner.ts +12 -2
- package/src/utils/build/upload.ts +13 -1
- package/src/utils/job-context/base-job-context.ts +8 -6
- package/src/utils/job-context/batch-script-job-context.ts +40 -21
- package/src/utils/job-context/cloud-job-context.ts +24 -8
- package/src/utils/job-context/job-context.ts +2 -3
- package/src/utils/job-context/local-job-context.ts +4 -6
- package/src/utils/open-cloud/open-cloud-client.ts +43 -11
- package/src/utils/testing/parsers/batch-log-parser.ts +4 -1
- package/src/utils/testing/runner/combined-project-generator.ts +28 -1
- package/src/utils/testing/runner/test-runner.ts +5 -6
- package/src/utils/testing/test-log-parser.ts +33 -0
- package/templates/batch-test-runner.luau +9 -9
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -30,22 +30,30 @@ class CloudDeployment implements Deployment {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
export class CloudJobContext extends BaseJobContext {
|
|
33
|
-
constructor(openCloudClient: OpenCloudClient) {
|
|
34
|
-
super(openCloudClient);
|
|
33
|
+
constructor(reporter: Reporter, openCloudClient: OpenCloudClient) {
|
|
34
|
+
super(reporter, openCloudClient);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
async deployBuiltPlaceAsync(
|
|
38
|
-
reporter: Reporter,
|
|
39
38
|
options: DeployPlaceOptions
|
|
40
39
|
): Promise<Deployment> {
|
|
41
40
|
const { builtPlace, packageName, packagePath } = options;
|
|
42
41
|
const { rbxlPath, target } = builtPlace;
|
|
43
42
|
|
|
44
|
-
|
|
43
|
+
this._reporter.onPackagePhaseChange(packageName, 'uploading');
|
|
44
|
+
|
|
45
45
|
const version = await this._openCloudClient!.uploadPlaceAsync(
|
|
46
46
|
target.universeId,
|
|
47
47
|
target.placeId,
|
|
48
|
-
rbxlPath
|
|
48
|
+
rbxlPath,
|
|
49
|
+
undefined,
|
|
50
|
+
(transferred, total) => {
|
|
51
|
+
this._reporter.onPackageProgressUpdate(packageName, {
|
|
52
|
+
kind: 'bytes',
|
|
53
|
+
transferredBytes: transferred,
|
|
54
|
+
totalBytes: total,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
49
57
|
);
|
|
50
58
|
|
|
51
59
|
// Eagerly release build artifacts after upload (disposeAsync is safety net)
|
|
@@ -62,13 +70,12 @@ export class CloudJobContext extends BaseJobContext {
|
|
|
62
70
|
|
|
63
71
|
async runScriptAsync(
|
|
64
72
|
deployment: Deployment,
|
|
65
|
-
reporter: Reporter,
|
|
66
73
|
options: RunScriptOptions
|
|
67
74
|
): Promise<ScriptRunResult> {
|
|
68
75
|
const cloudDeployment = deployment as CloudDeployment;
|
|
69
76
|
const { scriptContent, packageName, timeoutMs = 120_000 } = options;
|
|
70
77
|
|
|
71
|
-
|
|
78
|
+
this._reporter.onPackagePhaseChange(packageName, 'scheduling');
|
|
72
79
|
const task = await this._openCloudClient!.createExecutionTaskAsync(
|
|
73
80
|
cloudDeployment.universeId,
|
|
74
81
|
cloudDeployment.placeId,
|
|
@@ -76,11 +83,20 @@ export class CloudJobContext extends BaseJobContext {
|
|
|
76
83
|
scriptContent
|
|
77
84
|
);
|
|
78
85
|
|
|
86
|
+
let pollCount = 0;
|
|
79
87
|
const completedTask = await Promise.race([
|
|
80
88
|
this._openCloudClient!.pollTaskCompletionAsync(task.path, (state) => {
|
|
89
|
+
pollCount++;
|
|
81
90
|
if (state === 'PROCESSING') {
|
|
82
|
-
|
|
91
|
+
this._reporter.onPackagePhaseChange(packageName, 'executing');
|
|
83
92
|
}
|
|
93
|
+
const label = state === 'QUEUED' ? 'queued' : `poll #${pollCount}`;
|
|
94
|
+
this._reporter.onPackageProgressUpdate(packageName, {
|
|
95
|
+
kind: 'steps',
|
|
96
|
+
completed: pollCount,
|
|
97
|
+
total: 0,
|
|
98
|
+
label,
|
|
99
|
+
});
|
|
84
100
|
}),
|
|
85
101
|
timeoutAsync(timeoutMs, `Test timed out after ${timeoutMs / 1000}s`),
|
|
86
102
|
]);
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { type Reporter } from '@quenty/cli-output-helpers/reporting';
|
|
2
1
|
import { type BuildPlaceOptions, type BuiltPlace } from '../build/build.js';
|
|
3
2
|
|
|
4
3
|
export type { BuiltPlace } from '../build/build.js';
|
|
@@ -32,10 +31,10 @@ export interface JobContext {
|
|
|
32
31
|
buildPlaceAsync(options: BuildPlaceOptions): Promise<BuiltPlace>;
|
|
33
32
|
|
|
34
33
|
/** Deploy a built place to the execution environment. Returns a handle for subsequent operations. */
|
|
35
|
-
deployBuiltPlaceAsync(
|
|
34
|
+
deployBuiltPlaceAsync(options: DeployPlaceOptions): Promise<Deployment>;
|
|
36
35
|
|
|
37
36
|
/** Execute a Luau script in a deployed place. */
|
|
38
|
-
runScriptAsync(deployment: Deployment,
|
|
37
|
+
runScriptAsync(deployment: Deployment, options: RunScriptOptions): Promise<ScriptRunResult>;
|
|
39
38
|
|
|
40
39
|
/** Retrieve raw logs from the most recent script execution on this deployment. */
|
|
41
40
|
getLogsAsync(deployment: Deployment): Promise<string>;
|
|
@@ -24,12 +24,11 @@ class LocalDeployment implements Deployment {
|
|
|
24
24
|
export class LocalJobContext extends BaseJobContext {
|
|
25
25
|
private _deployments = new Set<LocalDeployment>();
|
|
26
26
|
|
|
27
|
-
constructor(openCloudClient?: OpenCloudClient) {
|
|
28
|
-
super(openCloudClient);
|
|
27
|
+
constructor(reporter: Reporter, openCloudClient?: OpenCloudClient) {
|
|
28
|
+
super(reporter, openCloudClient);
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
async deployBuiltPlaceAsync(
|
|
32
|
-
reporter: Reporter,
|
|
33
32
|
options: DeployPlaceOptions
|
|
34
33
|
): Promise<Deployment> {
|
|
35
34
|
const { builtPlace, packageName } = options;
|
|
@@ -38,7 +37,7 @@ export class LocalJobContext extends BaseJobContext {
|
|
|
38
37
|
placePath: builtPlace.rbxlPath,
|
|
39
38
|
onPhase: (phase) => {
|
|
40
39
|
if (phase === 'launching' || phase === 'connecting') {
|
|
41
|
-
|
|
40
|
+
this._reporter.onPackagePhaseChange(packageName, phase);
|
|
42
41
|
}
|
|
43
42
|
},
|
|
44
43
|
});
|
|
@@ -52,13 +51,12 @@ export class LocalJobContext extends BaseJobContext {
|
|
|
52
51
|
|
|
53
52
|
async runScriptAsync(
|
|
54
53
|
deployment: Deployment,
|
|
55
|
-
reporter: Reporter,
|
|
56
54
|
options: RunScriptOptions
|
|
57
55
|
): Promise<ScriptRunResult> {
|
|
58
56
|
const localDeployment = deployment as LocalDeployment;
|
|
59
57
|
const { scriptContent, packageName, timeoutMs } = options;
|
|
60
58
|
|
|
61
|
-
|
|
59
|
+
this._reporter.onPackagePhaseChange(packageName, 'executing');
|
|
62
60
|
|
|
63
61
|
try {
|
|
64
62
|
const result = await localDeployment.bridge.executeAsync({
|
|
@@ -106,7 +106,8 @@ export class OpenCloudClient {
|
|
|
106
106
|
universeId: number,
|
|
107
107
|
placeId: number,
|
|
108
108
|
rbxlPath: string,
|
|
109
|
-
publish?: boolean
|
|
109
|
+
publish?: boolean,
|
|
110
|
+
onProgress?: (transferredBytes: number, totalBytes: number) => void
|
|
110
111
|
): Promise<number> {
|
|
111
112
|
OutputHelper.verbose(
|
|
112
113
|
`Uploading to https://www.roblox.com/games/${placeId}/place ...`
|
|
@@ -114,19 +115,50 @@ export class OpenCloudClient {
|
|
|
114
115
|
|
|
115
116
|
const apiKey = await this._resolveApiKeyAsync();
|
|
116
117
|
const fileBuffer = await fs.readFile(rbxlPath);
|
|
117
|
-
const
|
|
118
|
+
const totalBytes = fileBuffer.length;
|
|
118
119
|
const versionType = publish ? 'Published' : 'Saved';
|
|
119
120
|
const url = `https://apis.roblox.com/universes/v1/${universeId}/places/${placeId}/versions?versionType=${versionType}`;
|
|
120
121
|
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
122
|
+
const headers: Record<string, string> = {
|
|
123
|
+
'Content-Type': 'application/octet-stream',
|
|
124
|
+
Accept: 'application/json',
|
|
125
|
+
'X-API-Key': apiKey,
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
let response: Response;
|
|
129
|
+
|
|
130
|
+
if (onProgress) {
|
|
131
|
+
// Wrap the buffer in a ReadableStream that reports bytes as each chunk
|
|
132
|
+
// is consumed by the transport, giving real-time upload progress.
|
|
133
|
+
const CHUNK_SIZE = 64 * 1024;
|
|
134
|
+
let offset = 0;
|
|
135
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
136
|
+
pull(controller) {
|
|
137
|
+
if (offset >= totalBytes) {
|
|
138
|
+
controller.close();
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const end = Math.min(offset + CHUNK_SIZE, totalBytes);
|
|
142
|
+
controller.enqueue(fileBuffer.subarray(offset, end));
|
|
143
|
+
offset = end;
|
|
144
|
+
onProgress(offset, totalBytes);
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// fetch with ReadableStream body requires duplex: 'half' in Node
|
|
149
|
+
response = await this._rateLimiter.fetchAsync(url, {
|
|
150
|
+
method: 'POST',
|
|
151
|
+
headers,
|
|
152
|
+
body: stream,
|
|
153
|
+
duplex: 'half',
|
|
154
|
+
} as RequestInit);
|
|
155
|
+
} else {
|
|
156
|
+
response = await this._rateLimiter.fetchAsync(url, {
|
|
157
|
+
method: 'POST',
|
|
158
|
+
headers,
|
|
159
|
+
body: new Uint8Array(fileBuffer),
|
|
160
|
+
});
|
|
161
|
+
}
|
|
130
162
|
|
|
131
163
|
if (!response.ok) {
|
|
132
164
|
const text = await response.text();
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { OutputHelper } from '@quenty/cli-output-helpers';
|
|
2
|
+
import { type ParsedTestCounts, parseTestCounts } from '../test-log-parser.js';
|
|
2
3
|
|
|
3
4
|
export interface BatchPackageResult {
|
|
4
5
|
slug: string;
|
|
5
6
|
success: boolean;
|
|
6
7
|
logs: string;
|
|
8
|
+
testCounts?: ParsedTestCounts;
|
|
7
9
|
}
|
|
8
10
|
|
|
9
11
|
const BEGIN_MARKER = '===BATCH_TEST_BEGIN ';
|
|
@@ -136,7 +138,8 @@ export function parseBatchTestLogs(
|
|
|
136
138
|
);
|
|
137
139
|
}
|
|
138
140
|
|
|
139
|
-
|
|
141
|
+
const testCounts = sectionLogs ? parseTestCounts(sectionLogs) : undefined;
|
|
142
|
+
results.set(packageName, { slug, success, logs: sectionLogs, testCounts });
|
|
140
143
|
}
|
|
141
144
|
|
|
142
145
|
return results;
|
|
@@ -28,6 +28,16 @@ interface RojoNode {
|
|
|
28
28
|
[key: string]: unknown;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
import { type StepProgress } from '@quenty/cli-output-helpers/reporting';
|
|
32
|
+
|
|
33
|
+
/** Progress callbacks for granular phase reporting during combined builds. */
|
|
34
|
+
export interface CombinedBuildProgress {
|
|
35
|
+
onPackageBuildStart?(packageName: string): void;
|
|
36
|
+
onPackageBuildComplete?(packageName: string): void;
|
|
37
|
+
onCombineStart?(): void;
|
|
38
|
+
onStepProgress?(progress: StepProgress): void;
|
|
39
|
+
}
|
|
40
|
+
|
|
31
41
|
/** Per-package build info collected during phase 1. */
|
|
32
42
|
interface PackageBuildInfo {
|
|
33
43
|
slug: string;
|
|
@@ -47,8 +57,9 @@ export async function generateCombinedProjectAsync(options: {
|
|
|
47
57
|
repoRoot: string;
|
|
48
58
|
batchPlaceId?: number;
|
|
49
59
|
batchUniverseId?: number;
|
|
60
|
+
progress?: CombinedBuildProgress;
|
|
50
61
|
}): Promise<CombinedProjectResult> {
|
|
51
|
-
const { packages, batchPlaceId, batchUniverseId } = options;
|
|
62
|
+
const { packages, batchPlaceId, batchUniverseId, progress } = options;
|
|
52
63
|
|
|
53
64
|
if (packages.length === 0) {
|
|
54
65
|
throw new Error('No packages provided for combined project generation');
|
|
@@ -64,6 +75,7 @@ export async function generateCombinedProjectAsync(options: {
|
|
|
64
75
|
|
|
65
76
|
// ── Phase 1: Build each package individually ──
|
|
66
77
|
|
|
78
|
+
let buildIndex = 0;
|
|
67
79
|
for (const pkg of packages) {
|
|
68
80
|
const configPath = resolveDeployConfigPath(pkg.path);
|
|
69
81
|
const config = await loadDeployConfigAsync(configPath);
|
|
@@ -92,7 +104,16 @@ export async function generateCombinedProjectAsync(options: {
|
|
|
92
104
|
// Build this package's .rbxl
|
|
93
105
|
const rbxlPath = buildContext.resolvePath(`${slug}.rbxl`);
|
|
94
106
|
OutputHelper.verbose(`Building ${pkg.name} (${slug})...`);
|
|
107
|
+
progress?.onPackageBuildStart?.(pkg.name);
|
|
95
108
|
await buildContext.rojoBuildAsync({ projectPath, output: rbxlPath });
|
|
109
|
+
buildIndex++;
|
|
110
|
+
progress?.onPackageBuildComplete?.(pkg.name);
|
|
111
|
+
progress?.onStepProgress?.({
|
|
112
|
+
kind: 'steps',
|
|
113
|
+
completed: buildIndex,
|
|
114
|
+
total: packages.length,
|
|
115
|
+
label: pkg.name,
|
|
116
|
+
});
|
|
96
117
|
|
|
97
118
|
// Resolve scriptTemplate path
|
|
98
119
|
if (!target.scriptTemplate) {
|
|
@@ -123,6 +144,12 @@ export async function generateCombinedProjectAsync(options: {
|
|
|
123
144
|
OutputHelper.verbose(
|
|
124
145
|
`Merging ${builds.length} packages into combined .rbxl...`
|
|
125
146
|
);
|
|
147
|
+
progress?.onCombineStart?.();
|
|
148
|
+
progress?.onStepProgress?.({
|
|
149
|
+
kind: 'steps',
|
|
150
|
+
completed: 0,
|
|
151
|
+
total: builds.length,
|
|
152
|
+
});
|
|
126
153
|
await buildContext.executeLuneTransformScriptAsync(luneScriptPath, ...luneArgs);
|
|
127
154
|
|
|
128
155
|
OutputHelper.verbose(
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import * as fs from 'fs/promises';
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import { randomUUID } from 'crypto';
|
|
4
|
-
import { type Reporter } from '@quenty/cli-output-helpers/reporting';
|
|
5
4
|
import { type JobContext } from '../../job-context/job-context.js';
|
|
6
|
-
import { parseTestLogs } from '../test-log-parser.js';
|
|
5
|
+
import { type ParsedTestCounts, parseTestLogs, parseTestCounts } from '../test-log-parser.js';
|
|
7
6
|
|
|
8
7
|
export interface SingleTestResult {
|
|
9
8
|
success: boolean;
|
|
10
9
|
logs: string;
|
|
10
|
+
testCounts?: ParsedTestCounts;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
export interface SingleTestOptions {
|
|
@@ -26,7 +26,6 @@ export interface SingleTestOptions {
|
|
|
26
26
|
*/
|
|
27
27
|
export async function runSingleTestAsync(
|
|
28
28
|
context: JobContext,
|
|
29
|
-
reporter: Reporter,
|
|
30
29
|
options: SingleTestOptions
|
|
31
30
|
): Promise<SingleTestResult> {
|
|
32
31
|
const {
|
|
@@ -41,21 +40,20 @@ export async function runSingleTestAsync(
|
|
|
41
40
|
targetName: 'test',
|
|
42
41
|
outputFileName: `test-${sessionId}.rbxl`,
|
|
43
42
|
packagePath,
|
|
44
|
-
reporter,
|
|
45
43
|
packageName,
|
|
46
44
|
});
|
|
47
45
|
|
|
48
46
|
const scriptContent =
|
|
49
47
|
scriptText ?? (await readTestScriptAsync(packagePath, builtPlace.target.scriptTemplate));
|
|
50
48
|
|
|
51
|
-
const deployment = await context.deployBuiltPlaceAsync(
|
|
49
|
+
const deployment = await context.deployBuiltPlaceAsync({
|
|
52
50
|
builtPlace,
|
|
53
51
|
packageName,
|
|
54
52
|
packagePath,
|
|
55
53
|
});
|
|
56
54
|
|
|
57
55
|
try {
|
|
58
|
-
const result = await context.runScriptAsync(deployment,
|
|
56
|
+
const result = await context.runScriptAsync(deployment, {
|
|
59
57
|
scriptContent,
|
|
60
58
|
packageName,
|
|
61
59
|
timeoutMs,
|
|
@@ -67,6 +65,7 @@ export async function runSingleTestAsync(
|
|
|
67
65
|
return {
|
|
68
66
|
success: result.success && parsed.success,
|
|
69
67
|
logs: parsed.logs,
|
|
68
|
+
testCounts: parseTestCounts(parsed.logs),
|
|
70
69
|
};
|
|
71
70
|
} finally {
|
|
72
71
|
await context.releaseAsync(deployment);
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import { OutputHelper } from '@quenty/cli-output-helpers';
|
|
2
2
|
|
|
3
|
+
export interface ParsedTestCounts {
|
|
4
|
+
passed: number;
|
|
5
|
+
failed: number;
|
|
6
|
+
total: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
3
9
|
export interface ParsedTestLogs {
|
|
4
10
|
success: boolean;
|
|
5
11
|
logs: string;
|
|
12
|
+
testCounts?: ParsedTestCounts;
|
|
6
13
|
}
|
|
7
14
|
|
|
8
15
|
/**
|
|
@@ -25,5 +32,31 @@ export function parseTestLogs(rawOutput: string): ParsedTestLogs {
|
|
|
25
32
|
return {
|
|
26
33
|
success: !hasJestFailures && !hasRuntimeError,
|
|
27
34
|
logs: rawOutput,
|
|
35
|
+
testCounts: parseTestCounts(rawOutput),
|
|
28
36
|
};
|
|
29
37
|
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Parse Jest "Tests: N failed, N passed, N total" line into structured counts.
|
|
41
|
+
* Returns undefined if no test summary line is found.
|
|
42
|
+
*/
|
|
43
|
+
export function parseTestCounts(rawOutput: string): ParsedTestCounts | undefined {
|
|
44
|
+
const clean = OutputHelper.stripAnsi(rawOutput);
|
|
45
|
+
|
|
46
|
+
// Match "Tests: 2 failed, 23 passed, 25 total" or "Tests: 25 passed, 25 total"
|
|
47
|
+
const match = clean.match(/Tests:\s+(.+?)\s+total/);
|
|
48
|
+
if (!match) return undefined;
|
|
49
|
+
|
|
50
|
+
const prefix = match[1];
|
|
51
|
+
const totalMatch = clean.match(/Tests:\s+.+?(\d+)\s+total/);
|
|
52
|
+
if (!totalMatch) return undefined;
|
|
53
|
+
|
|
54
|
+
const total = parseInt(totalMatch[1], 10);
|
|
55
|
+
const passedMatch = prefix.match(/(\d+)\s+passed/);
|
|
56
|
+
const failedMatch = prefix.match(/(\d+)\s+failed/);
|
|
57
|
+
|
|
58
|
+
const passed = passedMatch ? parseInt(passedMatch[1], 10) : 0;
|
|
59
|
+
const failed = failedMatch ? parseInt(failedMatch[1], 10) : 0;
|
|
60
|
+
|
|
61
|
+
return { passed, failed, total };
|
|
62
|
+
}
|
|
@@ -84,15 +84,6 @@ for _, slug in packageSlugs do
|
|
|
84
84
|
-- the marker in the log stream.
|
|
85
85
|
RunService.Heartbeat:Wait()
|
|
86
86
|
|
|
87
|
-
if testOk then
|
|
88
|
-
print("===BATCH_TEST_END " .. slug .. " PASS===")
|
|
89
|
-
table.insert(results, { slug = slug, success = true })
|
|
90
|
-
else
|
|
91
|
-
warn("[BatchTest] " .. slug .. ": " .. tostring(testErr))
|
|
92
|
-
print("===BATCH_TEST_END " .. slug .. " FAIL===")
|
|
93
|
-
table.insert(results, { slug = slug, success = false, error = tostring(testErr) })
|
|
94
|
-
end
|
|
95
|
-
|
|
96
87
|
-- CLEANUP: restore service state
|
|
97
88
|
allPackages[slug].Parent = nil
|
|
98
89
|
for _, c in workspace:GetChildren() do
|
|
@@ -119,6 +110,15 @@ for _, slug in packageSlugs do
|
|
|
119
110
|
for _ = 1, 3 do
|
|
120
111
|
RunService.Heartbeat:Wait()
|
|
121
112
|
end
|
|
113
|
+
|
|
114
|
+
if testOk then
|
|
115
|
+
print("===BATCH_TEST_END " .. slug .. " PASS===")
|
|
116
|
+
table.insert(results, { slug = slug, success = true })
|
|
117
|
+
else
|
|
118
|
+
warn("[BatchTest] " .. slug .. ": " .. tostring(testErr))
|
|
119
|
+
print("===BATCH_TEST_END " .. slug .. " FAIL===")
|
|
120
|
+
table.insert(results, { slug = slug, success = false, error = tostring(testErr) })
|
|
121
|
+
end
|
|
122
122
|
end
|
|
123
123
|
|
|
124
124
|
-- Restore all packages
|