mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-11-05 07:37:30 +00:00
* feat(ci): rebalance test shards from recorded timings * fix(ci): preserve artifact retries for shard timings Find shard-plan generation by its stable step ID and clarify the native runner cohort count (#3748). Note: pre-existing reference-oracle parser type failures in two test_oracle_assets cases remain outside this change. * fix(ci): keep retry timing snapshots immutable Require the existing snapshot on reruns so a failed download cannot refresh history and change shard assignments. Cover the workflow contract and document recovery when the artifact is unavailable. * docs(ci): document shard timing profile selection * fix(ci): balance cold shard estimates consistently --------- Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
113 lines
4.4 KiB
TypeScript
113 lines
4.4 KiB
TypeScript
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import path from 'node:path';
|
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
import type { TestSpecification, Vitest } from 'vitest/node';
|
|
import PerfSequencer from '../helpers/perf-sequencer.js';
|
|
|
|
const roots: string[] = [];
|
|
afterEach(() => {
|
|
vi.unstubAllEnvs();
|
|
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
|
|
});
|
|
|
|
function fixture() {
|
|
const root = mkdtempSync(path.join(tmpdir(), 'gitnexus-shards-'));
|
|
roots.push(root);
|
|
mkdirSync(path.join(root, '.vitest/timings'), { recursive: true });
|
|
const specs = ['a', 'b', 'c', 'd'].map((name) => {
|
|
const moduleId = path.join(root, `${name}.test.ts`);
|
|
writeFileSync(moduleId, '// same size\n');
|
|
return {
|
|
moduleId,
|
|
project: { name: 'native', config: { fileParallelism: false } },
|
|
} as unknown as TestSpecification;
|
|
});
|
|
const history = (profile: string, weights: number[]) =>
|
|
writeFileSync(
|
|
path.join(root, '.vitest/timings', `${profile}.json`),
|
|
JSON.stringify(
|
|
Object.fromEntries(
|
|
specs.map((spec, i) => [`native:${path.basename(spec.moduleId)}`, weights[i]]),
|
|
),
|
|
),
|
|
);
|
|
const split = async () =>
|
|
Promise.all(
|
|
[1, 2].map(async (index) => {
|
|
const ctx = { config: { root, shard: { index, count: 2 } } } as Vitest;
|
|
return new PerfSequencer(ctx).shard(specs);
|
|
}),
|
|
);
|
|
return { root, specs, history, split };
|
|
}
|
|
|
|
describe('timing-based CI sharding', () => {
|
|
it('learns from measured durations instead of putting both slow files on one shard', async () => {
|
|
const { specs, history, split } = fixture();
|
|
history('coverage', [10000, 10, 9000, 10]);
|
|
const bins = await split();
|
|
expect(bins.some((bin) => bin.includes(specs[0]) && bin.includes(specs[2]))).toBe(false);
|
|
expect(
|
|
bins
|
|
.flat()
|
|
.map((spec) => spec.moduleId)
|
|
.sort(),
|
|
).toEqual(specs.map((spec) => spec.moduleId).sort());
|
|
});
|
|
|
|
it('keeps platform histories separate and gives independent runners identical partitions', async () => {
|
|
const { specs, history, split } = fixture();
|
|
history('windows-latest', [10000, 10, 9000, 10]);
|
|
history('macos-latest', [9000, 10000, 10, 10]);
|
|
vi.stubEnv('GITNEXUS_SHARD_PROFILE', 'windows-latest');
|
|
const windows = await split();
|
|
expect(await split()).toEqual(windows);
|
|
expect(windows.some((bin) => bin.includes(specs[0]) && bin.includes(specs[2]))).toBe(false);
|
|
vi.stubEnv('GITNEXUS_SHARD_PROFILE', 'macos-latest');
|
|
const mac = await split();
|
|
expect(mac.some((bin) => bin.includes(specs[0]) && bin.includes(specs[1]))).toBe(false);
|
|
expect(mac).not.toEqual(windows);
|
|
});
|
|
|
|
it('still schedules new files and cold starts as complete disjoint partitions', async () => {
|
|
const { specs, history, split } = fixture();
|
|
const cold = await split();
|
|
expect(cold.map((bin) => bin.length)).toEqual([2, 2]);
|
|
history('coverage', [10000]);
|
|
const warm = await split();
|
|
expect(new Set(warm.flat()).size).toBe(specs.length);
|
|
expect(warm.flat().length).toBe(specs.length);
|
|
});
|
|
|
|
it('spreads the parallel cohort across all cold shards when sequential counts differ', async () => {
|
|
const { root } = fixture();
|
|
const specs = Array.from({ length: 1414 }, (_, i) => {
|
|
const moduleId = path.join(root, `${i}.test.ts`);
|
|
writeFileSync(moduleId, ' '.repeat(10 * 1024));
|
|
const sequential = i < 94;
|
|
return {
|
|
moduleId,
|
|
project: {
|
|
name: sequential ? 'native' : 'default',
|
|
config: { fileParallelism: !sequential },
|
|
},
|
|
} as unknown as TestSpecification;
|
|
});
|
|
const bins = await Promise.all(
|
|
Array.from({ length: 8 }, (_, i) => {
|
|
const ctx = { config: { root, shard: { index: i + 1, count: 8 } } } as Vitest;
|
|
return new PerfSequencer(ctx).shard(specs);
|
|
}),
|
|
);
|
|
const parallelCounts = bins.map(
|
|
(bin) => bin.filter((spec) => spec.project.name === 'default').length,
|
|
);
|
|
// A one-file difference in sequential work must not concentrate the entire
|
|
// parallel cohort on two runners (660 files each in the failing CI run).
|
|
expect(Math.min(...parallelCounts)).toBeGreaterThan(100);
|
|
expect(Math.max(...parallelCounts)).toBeLessThan(250);
|
|
expect(new Set(bins.flat()).size).toBe(specs.length);
|
|
expect(bins.flat()).toHaveLength(specs.length);
|
|
});
|
|
});
|