Files
openwork/scripts/ci/verify-clean-revert.test.mjs
T

94 lines
3.7 KiB
JavaScript

import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { resolve } from "node:path";
import { afterEach, test } from "node:test";
const script = resolve(import.meta.dirname, "verify-clean-revert.mjs");
const tempDirs = [];
afterEach(() => {
for (const directory of tempDirs.splice(0)) rmSync(directory, { recursive: true, force: true });
});
function git(repo, ...args) {
const result = spawnSync("git", ["-c", "user.name=t", "-c", "user.email=t@t", ...args], {
cwd: repo,
encoding: "utf8",
});
assert.equal(result.status, 0, result.stderr);
return result.stdout.trim();
}
function fixture() {
const repo = mkdtempSync(resolve(tmpdir(), "clean-revert-test-"));
tempDirs.push(repo);
git(repo, "init", "-q");
writeFileSync(resolve(repo, "file.txt"), "A\n");
git(repo, "add", "file.txt");
git(repo, "commit", "-q", "-m", "A");
const a = git(repo, "rev-parse", "HEAD");
writeFileSync(resolve(repo, "file.txt"), "B\n");
git(repo, "commit", "-q", "-am", "B");
const b = git(repo, "rev-parse", "HEAD");
git(repo, "revert", "--no-edit", b);
const c = git(repo, "rev-parse", "HEAD");
return { repo, a, b, c };
}
function verify(repo, ...args) {
return spawnSync(process.execPath, [script, ...args], { cwd: repo, encoding: "utf8" });
}
test("passes an exact revert parsed from the head message", () => {
const { repo, b, c } = fixture();
const result = verify(repo, "--base", b, "--head", c);
assert.equal(result.status, 0, result.stdout + result.stderr);
assert.match(result.stdout, new RegExp(`PASS clean-revert of ${b}`));
assert.match(result.stdout, /verdict=pass/);
});
test("fails when the revert tree contains an extra edit", () => {
const { repo, b, c } = fixture();
writeFileSync(resolve(repo, "extra.txt"), "tampered\n");
git(repo, "add", "extra.txt");
git(repo, "commit", "-q", "-m", "extra edit");
const tampered = git(repo, "rev-parse", "HEAD");
const result = verify(repo, "--base", b, "--head", tampered, "--reverted", b);
assert.equal(result.status, 1);
assert.match(result.stdout, /FAIL PR tree is not the exact inverse/);
assert.notEqual(c, tampered);
});
test("fails when source drift makes the revert conflict", () => {
const { repo, b } = fixture();
git(repo, "switch", "-q", "--detach", b);
writeFileSync(resolve(repo, "file.txt"), "unrelated edit to the same line\n");
git(repo, "commit", "-q", "-am", "source drift");
const driftedBase = git(repo, "rev-parse", "HEAD");
const result = verify(repo, "--base", driftedBase, "--head", driftedBase, "--reverted", b);
assert.equal(result.status, 1);
assert.match(result.stdout, /FAIL revert does not apply cleanly to PR base/);
});
test("fails when the reverted commit is not an ancestor of base", () => {
const { repo, a, b, c } = fixture();
git(repo, "switch", "-q", "--detach", a);
writeFileSync(resolve(repo, "other.txt"), "other\n");
git(repo, "add", "other.txt");
git(repo, "commit", "-q", "-m", "divergent");
const divergent = git(repo, "rev-parse", "HEAD");
const result = verify(repo, "--base", b, "--head", c, "--reverted", divergent);
assert.equal(result.status, 1);
assert.match(result.stdout, /FAIL reverted commit is not an ancestor of the PR base/);
});
test("fails when the head message does not identify a reverted commit", () => {
const { repo, a, b } = fixture();
const result = verify(repo, "--base", a, "--head", b);
assert.equal(result.status, 1);
assert.match(result.stdout, /FAIL head commit message has no 'This reverts commit <40-hex>' line/);
assert.match(result.stdout, /verdict=fail/);
});