summaryrefslogtreecommitdiffstats
path: root/remote/test/puppeteer/tools/internal/job.ts
diff options
context:
space:
mode:
Diffstat (limited to 'remote/test/puppeteer/tools/internal/job.ts')
-rw-r--r--remote/test/puppeteer/tools/internal/job.ts153
1 files changed, 153 insertions, 0 deletions
diff --git a/remote/test/puppeteer/tools/internal/job.ts b/remote/test/puppeteer/tools/internal/job.ts
new file mode 100644
index 0000000000..ef6ea10237
--- /dev/null
+++ b/remote/test/puppeteer/tools/internal/job.ts
@@ -0,0 +1,153 @@
+import {createHash} from 'crypto';
+import {existsSync, Stats} from 'fs';
+import {mkdir, readFile, stat, writeFile} from 'fs/promises';
+import {tmpdir} from 'os';
+import {dirname, join} from 'path';
+
+import glob from 'glob';
+
+interface JobContext {
+ name: string;
+ inputs: string[];
+ outputs: string[];
+}
+
+class JobBuilder {
+ #inputs: string[] = [];
+ #outputs: string[] = [];
+ #callback: (ctx: JobContext) => Promise<void>;
+ #name: string;
+ #value = '';
+ #force = false;
+
+ constructor(name: string, callback: (ctx: JobContext) => Promise<void>) {
+ this.#name = name;
+ this.#callback = callback;
+ }
+
+ get jobHash(): string {
+ return createHash('sha256').update(this.#name).digest('hex');
+ }
+
+ force() {
+ this.#force = true;
+ return this;
+ }
+
+ value(value: string) {
+ this.#value = value;
+ return this;
+ }
+
+ inputs(inputs: string[]): JobBuilder {
+ this.#inputs = inputs.flatMap(value => {
+ if (glob.hasMagic(value)) {
+ return glob.sync(value);
+ }
+ return value;
+ });
+ return this;
+ }
+
+ outputs(outputs: string[]): JobBuilder {
+ if (!this.#name) {
+ this.#name = outputs.join(' and ');
+ }
+
+ this.#outputs = outputs;
+ return this;
+ }
+
+ async build(): Promise<void> {
+ console.log(`Running job ${this.#name}...`);
+ // For debugging.
+ if (this.#force) {
+ return this.#run();
+ }
+ // In case we deleted an output file on purpose.
+ if (!this.getOutputStats()) {
+ return this.#run();
+ }
+ // Run if the job has a value, but it changes.
+ if (this.#value) {
+ if (!(await this.isValueDifferent())) {
+ return;
+ }
+ return this.#run();
+ }
+ // Always run when there is no output.
+ if (!this.#outputs.length) {
+ return this.#run();
+ }
+ // Make-like comparator.
+ if (!(await this.areInputsNewer())) {
+ return;
+ }
+ return this.#run();
+ }
+
+ async isValueDifferent(): Promise<boolean> {
+ const file = join(tmpdir(), `puppeteer/${this.jobHash}.txt`);
+ await mkdir(dirname(file), {recursive: true});
+ if (!existsSync(file)) {
+ await writeFile(file, this.#value);
+ return true;
+ }
+ return this.#value !== (await readFile(file, 'utf8'));
+ }
+
+ #outputStats?: Stats[];
+ async getOutputStats(): Promise<Stats[] | undefined> {
+ if (this.#outputStats) {
+ return this.#outputStats;
+ }
+ try {
+ this.#outputStats = await Promise.all(
+ this.#outputs.map(output => {
+ return stat(output);
+ })
+ );
+ } catch {}
+ return this.#outputStats;
+ }
+
+ async areInputsNewer(): Promise<boolean> {
+ const inputStats = await Promise.all(
+ this.#inputs.map(input => {
+ return stat(input);
+ })
+ );
+ const outputStats = await this.getOutputStats();
+ if (
+ outputStats &&
+ outputStats.reduce(reduceMinTime, Infinity) >
+ inputStats.reduce(reduceMaxTime, 0)
+ ) {
+ return false;
+ }
+ return true;
+ }
+
+ #run(): Promise<void> {
+ return this.#callback({
+ name: this.#name,
+ inputs: this.#inputs,
+ outputs: this.#outputs,
+ });
+ }
+}
+
+export const job = (
+ name: string,
+ callback: (ctx: JobContext) => Promise<void>
+): JobBuilder => {
+ return new JobBuilder(name, callback);
+};
+
+const reduceMaxTime = (time: number, stat: Stats) => {
+ return time < stat.mtimeMs ? stat.mtimeMs : time;
+};
+
+const reduceMinTime = (time: number, stat: Stats) => {
+ return time > stat.mtimeMs ? stat.mtimeMs : time;
+};