-
Notifications
You must be signed in to change notification settings - Fork 406
Expand file tree
/
Copy pathcli.ts
More file actions
139 lines (133 loc) · 5.39 KB
/
Copy pathcli.ts
File metadata and controls
139 lines (133 loc) · 5.39 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { readFile } from 'node:fs/promises';
import { resolve } from 'node:path';
import { createExternalSubjectAdapter } from './external-subject.js';
import {
createHarborExecutor,
createPierExecutor,
type HarnessExecutor,
} from './harness-executor.js';
import { openExperimentDirectory } from './experiment-directory.js';
import type { ExperimentSpec } from './experiment.js';
import { createMakaSubjectAdapter } from './maka-subject.js';
import { runExperiment, type ExperimentExecutor, type SubjectAdapter } from './runner.js';
import { parseExperimentSpec } from './spec.js';
const USAGE = 'usage: maka eval run <spec.json> --out <directory> [--cell <cell-id>]';
export interface EvalCliDependencies {
readonly loadExecutor: (spec: ExperimentSpec, specPath: string) => ExperimentExecutor;
readonly subjects: readonly SubjectAdapter[];
readonly signal?: AbortSignal;
readonly stdout: (text: string) => void;
readonly stderr: (text: string) => void;
}
export async function runMakaEvalCli(
argv: readonly string[],
overrides: Partial<EvalCliDependencies> = {},
): Promise<number> {
const controller = overrides.signal ? undefined : new AbortController();
const signal = overrides.signal ?? controller?.signal;
let interrupted: number | undefined;
const interrupt = () => {
interrupted = 130;
controller?.abort();
};
const terminate = () => {
interrupted = 143;
controller?.abort();
};
if (controller) {
process.once('SIGINT', interrupt);
process.once('SIGTERM', terminate);
}
try {
const command = parseArgs(argv);
if (command.kind === 'help') {
(overrides.stdout ?? process.stdout.write.bind(process.stdout))(`${USAGE}\n`);
return 0;
}
const specPath = resolve(command.specPath);
const spec = parseExperimentSpec(JSON.parse(await readFile(specPath, 'utf8')) as unknown);
const directory = await openExperimentDirectory(resolve(command.outDir), spec);
let executor: ExperimentExecutor;
if (overrides.loadExecutor) {
executor = overrides.loadExecutor(spec, specPath);
} else {
const builtin = builtinExecutor(spec, specPath);
await builtin.preflight({
subjectCredentialNames: [
...new Set(spec.subjects.flatMap((subject) => subject.credentials)),
],
...(signal ? { signal } : {}),
});
executor = builtin;
}
const results = await runExperiment({
spec,
store: directory.attempts,
executor,
subjects: overrides.subjects ?? [createMakaSubjectAdapter(), createExternalSubjectAdapter()],
...(command.cellIds.length > 0 ? { cellIds: command.cellIds } : {}),
...(signal ? { signal } : {}),
});
const incomplete = spec.tasks.length * spec.repetitions * spec.subjects.length - results.size;
(overrides.stdout ?? process.stdout.write.bind(process.stdout))(
`${JSON.stringify({ experimentId: spec.id, cells: results.size, incomplete })}\n`,
);
return interrupted ?? (incomplete === 0 ? 0 : 1);
} catch (error) {
(overrides.stderr ?? process.stderr.write.bind(process.stderr))(
`maka eval: ${error instanceof Error ? error.message : String(error)}\n${USAGE}\n`,
);
return interrupted ?? 2;
} finally {
if (controller) {
process.removeListener('SIGINT', interrupt);
process.removeListener('SIGTERM', terminate);
}
}
}
function builtinExecutor(spec: ExperimentSpec, specPath: string): HarnessExecutor {
if (spec.executor.kind === 'harbor') return createHarborExecutor(spec.executor.config, specPath);
if (spec.executor.kind === 'pier') return createPierExecutor(spec.executor.config, specPath);
throw new Error(`unsupported executor: ${spec.executor.kind}`);
}
function parseArgs(argv: readonly string[]):
| { readonly kind: 'help' }
| {
readonly kind: 'run';
readonly specPath: string;
readonly outDir: string;
readonly cellIds: readonly string[];
} {
if (argv.length === 1 && (argv[0] === '--help' || argv[0] === '-h')) return { kind: 'help' };
if (argv[0] !== 'run' || !argv[1] || argv[1].startsWith('-')) throw new Error(USAGE);
let outDir: string | undefined;
const cellIds: string[] = [];
for (let index = 2; index < argv.length; index += 2) {
const flag = argv[index];
const value = argv[index + 1];
if (!value || value.startsWith('-')) throw new Error(`${flag} requires a value`);
if (flag === '--out') outDir = value;
else if (flag === '--cell') cellIds.push(value);
else throw new Error(`unexpected argument: ${flag}`);
}
if (!outDir) throw new Error('--out is required');
return { kind: 'run', specPath: argv[1], outDir, cellIds };
}