mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 01:32:44 +08:00
226 lines
6.8 KiB
JavaScript
226 lines
6.8 KiB
JavaScript
require('ts-node/register/transpile-only');
|
|
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs/promises');
|
|
const os = require('node:os');
|
|
const path = require('node:path');
|
|
const { test } = require('node:test');
|
|
const {
|
|
LocalFileExecutionArtifactAllocator,
|
|
} = require('../../back/runtime/adapters/fs/localFileExecutionArtifactAllocator');
|
|
const {
|
|
durableLocalProcessOutput,
|
|
} = require('../../back/runtime/adapters/local-process/durableLocalProcessOutput');
|
|
const {
|
|
localExecutionArtifactId,
|
|
} = require('../../back/runtime/domain/localExecutionArtifact');
|
|
|
|
const CAPACITY_POLICY = Object.freeze({
|
|
maximumAttemptBytes: 64 * 1024,
|
|
minimumFreeBytes: 0,
|
|
});
|
|
|
|
function candidate(overrides = {}) {
|
|
return {
|
|
runId: 'run-artifact',
|
|
attemptId: 'attempt-artifact',
|
|
projectId: 'default',
|
|
taskId: 'task-artifact',
|
|
taskRevision: 'revision-1',
|
|
executorType: 'local_process',
|
|
priority: 0,
|
|
queuedAtMs: 1_760_000_000_000,
|
|
attemptCreatedAtMs: 1_760_000_000_000,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
async function temporaryRoots(t) {
|
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'ql3-artifact-'));
|
|
t.after(() => fs.rm(root, { recursive: true, force: true }));
|
|
return {
|
|
root,
|
|
artifacts: path.join(root, 'artifacts'),
|
|
receipts: path.join(root, 'receipts'),
|
|
};
|
|
}
|
|
|
|
function allocator(roots, policy = CAPACITY_POLICY, capacity) {
|
|
return new LocalFileExecutionArtifactAllocator(
|
|
roots.artifacts,
|
|
roots.receipts,
|
|
policy,
|
|
capacity,
|
|
);
|
|
}
|
|
|
|
test('allocates a private deterministic Artifact and serializes accepted writes', async (t) => {
|
|
const roots = await temporaryRoots(t);
|
|
const artifacts = allocator(roots);
|
|
const prepared = await artifacts.prepare(candidate());
|
|
assert.equal(prepared.logArtifactId, localExecutionArtifactId(candidate()));
|
|
assert.equal(prepared.logArtifactId.length, 36);
|
|
const capability = durableLocalProcessOutput(prepared.output);
|
|
assert.ok(capability);
|
|
assert.equal(capability.maximumBytes, CAPACITY_POLICY.maximumAttemptBytes);
|
|
assert.equal(capability.logArtifactId, prepared.logArtifactId);
|
|
assert.equal(JSON.stringify(prepared.output).includes(roots.root), false);
|
|
|
|
await Promise.all([
|
|
prepared.output.write({
|
|
stream: 'stdout',
|
|
chunk: Buffer.from('first\n'),
|
|
observedAtMs: 1,
|
|
}),
|
|
prepared.output.write({
|
|
stream: 'stderr',
|
|
chunk: Buffer.from('second\n'),
|
|
observedAtMs: 2,
|
|
}),
|
|
]);
|
|
await prepared.dispose();
|
|
assert.equal(
|
|
await fs.readFile(capability.outputFilePath, 'utf8'),
|
|
'first\nsecond\n',
|
|
);
|
|
assert.equal((await fs.stat(capability.outputFilePath)).mode & 0o777, 0o600);
|
|
assert.equal(
|
|
(await fs.stat(path.dirname(capability.outputFilePath))).mode & 0o777,
|
|
0o700,
|
|
);
|
|
await assert.rejects(
|
|
prepared.output.write({
|
|
stream: 'stdout',
|
|
chunk: Buffer.from('late'),
|
|
observedAtMs: 3,
|
|
}),
|
|
/closed/,
|
|
);
|
|
|
|
const replay = await artifacts.prepare(candidate());
|
|
assert.equal(replay.logArtifactId, prepared.logArtifactId);
|
|
await replay.output.write({
|
|
stream: 'stdout',
|
|
chunk: Buffer.from('replay\n'),
|
|
observedAtMs: 4,
|
|
});
|
|
await replay.dispose();
|
|
assert.equal(
|
|
await fs.readFile(capability.outputFilePath, 'utf8'),
|
|
'first\nsecond\nreplay\n',
|
|
);
|
|
});
|
|
|
|
test('uses a different opaque Artifact for each Attempt', async (t) => {
|
|
const roots = await temporaryRoots(t);
|
|
const artifacts = allocator(roots);
|
|
const first = await artifacts.prepare(candidate());
|
|
const second = await artifacts.prepare(candidate({ attemptId: 'attempt-2' }));
|
|
assert.notEqual(first.logArtifactId, second.logArtifactId);
|
|
await Promise.all([first.dispose(), second.dispose()]);
|
|
});
|
|
|
|
test('drains writes accepted before asynchronous disposal closes the file', async (t) => {
|
|
const roots = await temporaryRoots(t);
|
|
const artifacts = allocator(roots);
|
|
const prepared = await artifacts.prepare(candidate());
|
|
const capability = durableLocalProcessOutput(prepared.output);
|
|
const accepted = prepared.output.write({
|
|
stream: 'stdout',
|
|
chunk: Buffer.from('accepted-before-close'),
|
|
observedAtMs: 1,
|
|
});
|
|
await prepared.dispose();
|
|
await accepted;
|
|
assert.equal(
|
|
await fs.readFile(capability.outputFilePath, 'utf8'),
|
|
'accepted-before-close',
|
|
);
|
|
});
|
|
|
|
test('rejects relative roots and refuses a symlink Artifact target', async (t) => {
|
|
assert.throws(
|
|
() =>
|
|
new LocalFileExecutionArtifactAllocator(
|
|
'relative',
|
|
'/tmp/receipts',
|
|
CAPACITY_POLICY,
|
|
),
|
|
/must be absolute/,
|
|
);
|
|
const roots = await temporaryRoots(t);
|
|
const reference = candidate();
|
|
const artifactId = localExecutionArtifactId(reference);
|
|
const shard = artifactId.slice(6, 8);
|
|
const directory = path.join(roots.artifacts, shard);
|
|
await fs.mkdir(directory, { recursive: true, mode: 0o700 });
|
|
const outside = path.join(roots.root, 'outside.log');
|
|
await fs.writeFile(outside, 'outside');
|
|
await fs.symlink(outside, path.join(directory, `${artifactId}.log`));
|
|
const artifacts = allocator(roots);
|
|
await assert.rejects(artifacts.prepare(reference));
|
|
assert.equal(await fs.readFile(outside, 'utf8'), 'outside');
|
|
});
|
|
|
|
test('hard-caps ordinary output and never writes a partial byte past quota', async (t) => {
|
|
const roots = await temporaryRoots(t);
|
|
const artifacts = allocator(roots);
|
|
const prepared = await artifacts.prepare(candidate());
|
|
const capability = durableLocalProcessOutput(prepared.output);
|
|
const oversized = Buffer.alloc(
|
|
CAPACITY_POLICY.maximumAttemptBytes + 37,
|
|
0x61,
|
|
);
|
|
await assert.rejects(
|
|
prepared.output.write({
|
|
stream: 'stdout',
|
|
chunk: oversized,
|
|
observedAtMs: 1,
|
|
}),
|
|
/byte quota/,
|
|
);
|
|
await assert.rejects(
|
|
prepared.output.write({
|
|
stream: 'stderr',
|
|
chunk: Buffer.from('must-not-append'),
|
|
observedAtMs: 2,
|
|
}),
|
|
/byte quota/,
|
|
);
|
|
await prepared.dispose();
|
|
const stored = await fs.readFile(capability.outputFilePath);
|
|
assert.equal(stored.length, CAPACITY_POLICY.maximumAttemptBytes);
|
|
assert.equal(stored.equals(oversized.subarray(0, stored.length)), true);
|
|
});
|
|
|
|
test('reserves free space before opening an Attempt Artifact', async (t) => {
|
|
const roots = await temporaryRoots(t);
|
|
let inspected = 0;
|
|
const artifacts = allocator(
|
|
roots,
|
|
{ maximumAttemptBytes: 64 * 1024, minimumFreeBytes: 128 * 1024 },
|
|
{
|
|
async inspect(root) {
|
|
inspected += 1;
|
|
assert.equal(root, roots.artifacts);
|
|
return {
|
|
availableBytes: BigInt(128 * 1024),
|
|
totalBytes: BigInt(1024 * 1024),
|
|
};
|
|
},
|
|
},
|
|
);
|
|
await assert.rejects(
|
|
artifacts.prepare(candidate()),
|
|
/capacity is unavailable/,
|
|
);
|
|
assert.equal(inspected, 1);
|
|
const artifactId = localExecutionArtifactId(candidate());
|
|
const target = path.join(
|
|
roots.artifacts,
|
|
artifactId.slice(6, 8),
|
|
`${artifactId}.log`,
|
|
);
|
|
await assert.rejects(fs.lstat(target), /ENOENT/);
|
|
});
|