Skip to content

Commit 4c076a7

Browse files
change structure of artifacts zip
1 parent 8ddc992 commit 4c076a7

2 files changed

Lines changed: 139 additions & 88 deletions

File tree

src/providers/maestro.ts

Lines changed: 98 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -2479,21 +2479,30 @@ export default class Maestro extends BaseProvider<MaestroOptions> {
24792479
return s;
24802480
}
24812481

2482-
private buildFlowDirNames(flows: MaestroFlowInfo[]): Map<number, string> {
2483-
const baseNames = new Map<number, string>();
2482+
private buildFlowDirNames(
2483+
entries: Array<{ runId: number; flow: MaestroFlowInfo }>,
2484+
reserved: Set<string> = new Set(),
2485+
): Map<string, string> {
2486+
const baseNames = new Map<string, string>();
24842487
const counts = new Map<string, number>();
24852488

2486-
for (const flow of flows) {
2489+
for (const r of reserved) {
2490+
counts.set(r, 1);
2491+
}
2492+
2493+
for (const { runId, flow } of entries) {
24872494
const sanitized = this.sanitizeFlowDirName(flow.name);
2488-
const base = sanitized ? `flow_${sanitized}` : `flow_${flow.id}`;
2489-
baseNames.set(flow.id, base);
2495+
const base = sanitized || `flow_${flow.id}`;
2496+
baseNames.set(`${runId}:${flow.id}`, base);
24902497
counts.set(base, (counts.get(base) || 0) + 1);
24912498
}
24922499

2493-
const result = new Map<number, string>();
2494-
for (const flow of flows) {
2495-
const base = baseNames.get(flow.id)!;
2496-
result.set(flow.id, (counts.get(base) || 0) > 1 ? `${base}_${flow.id}` : base);
2500+
const result = new Map<string, string>();
2501+
for (const { runId, flow } of entries) {
2502+
const key = `${runId}:${flow.id}`;
2503+
const base = baseNames.get(key)!;
2504+
const collides = (counts.get(base) || 0) > 1;
2505+
result.set(key, collides ? `${base}_${runId}_${flow.id}` : base);
24972506
}
24982507
return result;
24992508
}
@@ -2601,84 +2610,110 @@ export default class Maestro extends BaseProvider<MaestroOptions> {
26012610
);
26022611

26032612
try {
2613+
const runDetailsList: Array<{
2614+
run: MaestroRunInfo;
2615+
details: MaestroRunDetails;
2616+
}> = [];
2617+
26042618
for (const run of runsToDownload) {
2619+
if (!this.options.quiet) {
2620+
logger.info(` Waiting for artifacts sync for run ${run.id}...`);
2621+
}
26052622
try {
2606-
if (!this.options.quiet) {
2607-
logger.info(` Waiting for artifacts sync for run ${run.id}...`);
2608-
}
2623+
const details = await this.waitForArtifactsSync(run.id);
2624+
runDetailsList.push({ run, details });
2625+
} catch (error) {
2626+
logger.error(
2627+
`Failed to download artifacts for run ${run.id}: ${error instanceof Error ? error.message : error}`,
2628+
);
2629+
}
2630+
}
26092631

2610-
const runDetails = await this.waitForArtifactsSync(run.id);
2632+
const multiRun = runDetailsList.length > 1;
2633+
const runReportName = (runId: number): string =>
2634+
multiRun ? `report_${runId}.xml` : 'report.xml';
2635+
const runAssetsDirName = (runId: number): string =>
2636+
multiRun ? `run_${runId}` : '';
26112637

2612-
const flowsWithAssets = (runDetails.flows || []).filter(
2613-
(flow) => flow.assets,
2614-
);
2638+
const reservedNames = new Set<string>();
2639+
for (const { run, details } of runDetailsList) {
2640+
if (details.report) reservedNames.add(runReportName(run.id));
2641+
if (details.assets) {
2642+
const dir = runAssetsDirName(run.id);
2643+
if (dir) reservedNames.add(dir);
2644+
}
2645+
}
26152646

2616-
if (!runDetails.assets && flowsWithAssets.length === 0) {
2617-
if (!this.options.quiet) {
2618-
logger.info(` No artifacts available for run ${run.id}`);
2619-
}
2620-
continue;
2621-
}
2647+
const flowEntries = runDetailsList.flatMap(({ run, details }) =>
2648+
(details.flows || [])
2649+
.filter((flow) => flow.assets)
2650+
.map((flow) => ({ runId: run.id, flow })),
2651+
);
2652+
const flowDirNames = this.buildFlowDirNames(flowEntries, reservedNames);
26222653

2623-
const runDir = path.join(tempDir, `run_${run.id}`);
2624-
await fs.promises.mkdir(runDir, { recursive: true });
2654+
for (const { run, details } of runDetailsList) {
2655+
const flowsWithAssets = (details.flows || []).filter(
2656+
(flow) => flow.assets,
2657+
);
26252658

2626-
if (runDetails.assets) {
2627-
await this.downloadAssetBundle(runDetails.assets, runDir);
2659+
if (!details.assets && flowsWithAssets.length === 0) {
2660+
if (!this.options.quiet) {
2661+
logger.info(` No artifacts available for run ${run.id}`);
26282662
}
2663+
continue;
2664+
}
26292665

2630-
const flowDirNames = this.buildFlowDirNames(flowsWithAssets);
2631-
2632-
for (const flow of flowsWithAssets) {
2633-
const flowDirName = flowDirNames.get(flow.id)!;
2634-
const flowDir = path.join(runDir, flowDirName);
2635-
await fs.promises.mkdir(flowDir, { recursive: true });
2636-
await this.downloadAssetBundle(flow.assets!, flowDir);
2637-
2638-
if (flow.report) {
2639-
const flowReportPath = path.join(flowDir, 'report.xml');
2640-
try {
2641-
await fs.promises.writeFile(
2642-
flowReportPath,
2643-
flow.report,
2644-
'utf-8',
2645-
);
2646-
if (!this.options.quiet) {
2647-
logger.info(` Saved ${flowDirName} report.xml`);
2648-
}
2649-
} catch (error) {
2650-
logger.error(
2651-
` Failed to save report.xml for ${flowDirName}: ${error instanceof Error ? error.message : error}`,
2652-
);
2653-
}
2654-
}
2666+
if (details.assets) {
2667+
const dirName = runAssetsDirName(run.id);
2668+
const targetDir = dirName ? path.join(tempDir, dirName) : tempDir;
2669+
if (dirName) {
2670+
await fs.promises.mkdir(targetDir, { recursive: true });
26552671
}
2672+
await this.downloadAssetBundle(details.assets, targetDir);
2673+
}
2674+
2675+
for (const flow of flowsWithAssets) {
2676+
const flowDirName = flowDirNames.get(`${run.id}:${flow.id}`)!;
2677+
const flowDir = path.join(tempDir, flowDirName);
2678+
await fs.promises.mkdir(flowDir, { recursive: true });
2679+
await this.downloadAssetBundle(flow.assets!, flowDir);
26562680

2657-
if (runDetails.report) {
2658-
const reportPath = path.join(runDir, 'report.xml');
2681+
if (flow.report) {
2682+
const flowReportPath = path.join(flowDir, 'report.xml');
26592683
try {
26602684
await fs.promises.writeFile(
2661-
reportPath,
2662-
runDetails.report,
2685+
flowReportPath,
2686+
flow.report,
26632687
'utf-8',
26642688
);
26652689
if (!this.options.quiet) {
2666-
logger.info(` Saved report.xml`);
2690+
logger.info(` Saved ${flowDirName}/report.xml`);
26672691
}
26682692
} catch (error) {
26692693
logger.error(
2670-
` Failed to save report.xml: ${error instanceof Error ? error.message : error}`,
2694+
` Failed to save report.xml for ${flowDirName}: ${error instanceof Error ? error.message : error}`,
26712695
);
26722696
}
26732697
}
2698+
}
26742699

2675-
if (!this.options.quiet) {
2676-
logger.info(` Artifacts for run ${run.id} downloaded`);
2700+
if (details.report) {
2701+
const reportName = runReportName(run.id);
2702+
const reportPath = path.join(tempDir, reportName);
2703+
try {
2704+
await fs.promises.writeFile(reportPath, details.report, 'utf-8');
2705+
if (!this.options.quiet) {
2706+
logger.info(` Saved ${reportName}`);
2707+
}
2708+
} catch (error) {
2709+
logger.error(
2710+
` Failed to save ${reportName}: ${error instanceof Error ? error.message : error}`,
2711+
);
26772712
}
2678-
} catch (error) {
2679-
logger.error(
2680-
`Failed to download artifacts for run ${run.id}: ${error instanceof Error ? error.message : error}`,
2681-
);
2713+
}
2714+
2715+
if (!this.options.quiet) {
2716+
logger.info(` Artifacts for run ${run.id} downloaded`);
26822717
}
26832718
}
26842719

tests/providers/maestro.test.ts

Lines changed: 41 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2397,13 +2397,9 @@ describe('Maestro', () => {
23972397
},
23982398
]);
23992399

2400-
const runDir = path.join(tempDir, 'run_5678');
2401-
const flow11Dir = path.join(runDir, 'flow_login_flow.yaml');
2402-
const flow12Dir = path.join(runDir, 'flow_checkout.yaml');
2400+
const flow11Dir = path.join(tempDir, 'login_flow.yaml');
2401+
const flow12Dir = path.join(tempDir, 'checkout.yaml');
24032402

2404-
expect(fs.promises.mkdir).toHaveBeenCalledWith(runDir, {
2405-
recursive: true,
2406-
});
24072403
expect(fs.promises.mkdir).toHaveBeenCalledWith(flow11Dir, {
24082404
recursive: true,
24092405
});
@@ -2434,11 +2430,17 @@ describe('Maestro', () => {
24342430
'utf-8',
24352431
);
24362432
expect(fs.promises.writeFile).toHaveBeenCalledWith(
2437-
path.join(runDir, 'report.xml'),
2433+
path.join(tempDir, 'report.xml'),
24382434
'<run-report/>',
24392435
'utf-8',
24402436
);
24412437

2438+
// Single run: no run-level wrapper directory should be created.
2439+
const mkdirPaths = (fs.promises.mkdir as jest.Mock).mock.calls.map(
2440+
(call) => call[0],
2441+
);
2442+
expect(mkdirPaths).not.toContain(path.join(tempDir, 'run_5678'));
2443+
24422444
const noArtifactsLog = consoleSpy.mock.calls.find(
24432445
(call) =>
24442446
typeof call[0] === 'string' &&
@@ -2451,38 +2453,52 @@ describe('Maestro', () => {
24512453

24522454
it('should sanitize, cap, disambiguate, and fall back when building flow dir names', () => {
24532455
const longName = 'a'.repeat(100);
2454-
const flows: MaestroFlowInfo[] = [
2455-
{ id: 1, name: 'Login Flow', status: 'DONE' },
2456-
{ id: 2, name: 'login flow', status: 'DONE' },
2457-
{ id: 3, name: 'Login Flow', status: 'DONE' },
2458-
{ id: 4, name: '', status: 'DONE' },
2459-
{ id: 5, name: '!!! @@@ ###', status: 'DONE' },
2460-
{ id: 6, name: longName, status: 'DONE' },
2456+
const entries = [
2457+
{ runId: 100, flow: { id: 1, name: 'Login Flow', status: 'DONE' as MaestroFlowStatus } },
2458+
{ runId: 100, flow: { id: 2, name: 'login flow', status: 'DONE' as MaestroFlowStatus } },
2459+
{ runId: 100, flow: { id: 3, name: 'Login Flow', status: 'DONE' as MaestroFlowStatus } },
2460+
{ runId: 100, flow: { id: 4, name: '', status: 'DONE' as MaestroFlowStatus } },
2461+
{ runId: 100, flow: { id: 5, name: '!!! @@@ ###', status: 'DONE' as MaestroFlowStatus } },
2462+
{ runId: 100, flow: { id: 6, name: longName, status: 'DONE' as MaestroFlowStatus } },
24612463
];
24622464

2463-
const names = maestro['buildFlowDirNames'](flows);
2465+
const names = maestro['buildFlowDirNames'](entries);
24642466

24652467
// No collisions
24662468
const values = Array.from(names.values());
24672469
expect(new Set(values).size).toBe(values.length);
24682470

2469-
// Distinct sanitized name kept as-is
2470-
expect(names.get(2)).toBe('flow_login_flow');
2471+
// Distinct sanitized name kept as-is (no flow_ prefix)
2472+
expect(names.get('100:2')).toBe('login_flow');
24712473

2472-
// Collisions get _<id> suffix
2473-
expect(names.get(1)).toBe('flow_Login_Flow_1');
2474-
expect(names.get(3)).toBe('flow_Login_Flow_3');
2474+
// Collisions get _<runId>_<flowId> suffix
2475+
expect(names.get('100:1')).toBe('Login_Flow_100_1');
2476+
expect(names.get('100:3')).toBe('Login_Flow_100_3');
24752477

24762478
// Empty / unsanitizable names fall back to flow_<id>
2477-
expect(names.get(4)).toBe('flow_4');
2478-
expect(names.get(5)).toBe('flow_5');
2479+
expect(names.get('100:4')).toBe('flow_4');
2480+
expect(names.get('100:5')).toBe('flow_5');
24792481

2480-
// Long names are capped (sanitized portion <= 64 chars)
2481-
const dirName = names.get(6)!;
2482-
const sanitized = dirName.replace(/^flow_/, '').replace(/_\d+$/, '');
2482+
// Long names are capped at 64 chars (or 64 + suffix when colliding)
2483+
const dirName = names.get('100:6')!;
2484+
const sanitized = dirName.replace(/_\d+_\d+$/, '');
24832485
expect(sanitized.length).toBeLessThanOrEqual(64);
24842486
});
24852487

2488+
it('should disambiguate flows that would collide with reserved names', () => {
2489+
const entries = [
2490+
{ runId: 100, flow: { id: 1, name: 'report.xml', status: 'DONE' as MaestroFlowStatus } },
2491+
];
2492+
const reserved = new Set(['report.xml']);
2493+
2494+
const names = maestro['buildFlowDirNames'](entries, reserved);
2495+
2496+
// The flow's base would be "report.xml" but that name is taken
2497+
// by the run-level report file → must be disambiguated.
2498+
expect(names.get('100:1')).not.toBe('report.xml');
2499+
expect(names.get('100:1')).toBe('report.xml_100_1');
2500+
});
2501+
24862502
it('should log "No artifacts available" when neither run nor flows have assets', async () => {
24872503
const optionsWithArtifacts = new MaestroOptions(
24882504
'path/to/app.apk',

0 commit comments

Comments
 (0)