Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
244 changes: 244 additions & 0 deletions .github/scripts/__tests__/pr-check-secondary-examples.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
jest.mock("child_process", () => ({
execSync: jest.fn(),
spawnSync: jest.fn(),
}));

const { execSync, spawnSync } = require("child_process");

const {
toModule,
getAllExamples,
getChangedExamples,
runExample,
runAll,
computeExecutionPlan,
} = require("../pr-check-secondary-examples");

// ---------------------------------------------------------------------------
// toModule
// ---------------------------------------------------------------------------
describe("toModule", () => {
test("converts nested path to module", () => {
expect(toModule("examples/a/b.py")).toBe("examples.a.b");
});

test("converts root-level example path to module", () => {
expect(toModule("examples/foo.py")).toBe("examples.foo");
});
});

// ---------------------------------------------------------------------------
// getAllExamples
// ---------------------------------------------------------------------------
describe("getAllExamples", () => {
beforeEach(() => jest.clearAllMocks());

test("returns list of example files", () => {
execSync.mockReturnValueOnce("examples/a.py\nexamples/b/c.py\n");

expect(getAllExamples()).toEqual(["examples/a.py", "examples/b/c.py"]);
});

test("excludes __init__.py files", () => {
execSync.mockReturnValueOnce("examples/a.py\nexamples/__init__.py\nexamples/b.py\n");

expect(getAllExamples()).toEqual(["examples/a.py", "examples/b.py"]);
});

test("returns empty array when git ls-files produces no output", () => {
execSync.mockReturnValueOnce("");

expect(getAllExamples()).toEqual([]);
});
});

// ---------------------------------------------------------------------------
// getChangedExamples
// ---------------------------------------------------------------------------
describe("getChangedExamples", () => {
const ORIG_ENV = process.env;

beforeEach(() => {
jest.clearAllMocks();
process.env = { ...ORIG_ENV };
});

afterEach(() => {
process.env = ORIG_ENV;
});

test("returns empty array when GITHUB_BASE_REF is not set", () => {
delete process.env.GITHUB_BASE_REF;

expect(getChangedExamples()).toEqual([]);
expect(spawnSync).not.toHaveBeenCalled();
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

test("returns only changed example .py files", () => {
process.env.GITHUB_BASE_REF = "main";
spawnSync
.mockReturnValueOnce({ status: 0, stdout: "" }) // git fetch
.mockReturnValueOnce({ status: 0, stdout: "" }) // git rev-parse --verify
.mockReturnValueOnce({ status: 0, stdout: "examples/a.py\nsrc/foo.py\nexamples/b.py\nREADME.md\n" }); // git diff

expect(getChangedExamples()).toEqual(["examples/a.py", "examples/b.py"]);
});

test("filters out non-.py files under examples/", () => {
process.env.GITHUB_BASE_REF = "main";
spawnSync
.mockReturnValueOnce({ status: 0, stdout: "" })
.mockReturnValueOnce({ status: 0, stdout: "" })
.mockReturnValueOnce({ status: 0, stdout: "examples/a.py\nexamples/data.json\nexamples/b.py\n" });

expect(getChangedExamples()).toEqual(["examples/a.py", "examples/b.py"]);
});

test("excludes __init__.py from changed examples", () => {
process.env.GITHUB_BASE_REF = "main";
spawnSync
.mockReturnValueOnce({ status: 0, stdout: "" })
.mockReturnValueOnce({ status: 0, stdout: "" })
.mockReturnValueOnce({ status: 0, stdout: "examples/a.py\nexamples/__init__.py\nexamples/b.py\n" });

expect(getChangedExamples()).toEqual(["examples/a.py", "examples/b.py"]);
});

test("returns empty array when diff is empty", () => {
process.env.GITHUB_BASE_REF = "main";
spawnSync
.mockReturnValueOnce({ status: 0, stdout: "" })
.mockReturnValueOnce({ status: 0, stdout: "" })
.mockReturnValueOnce({ status: 0, stdout: "" });

expect(getChangedExamples()).toEqual([]);
});

test("returns empty array when fetch fails", () => {
process.env.GITHUB_BASE_REF = "main";
spawnSync.mockReturnValueOnce({ status: 1, stdout: "" });

expect(getChangedExamples()).toEqual([]);
});

test("returns empty array when spawnSync throws", () => {
process.env.GITHUB_BASE_REF = "main";
spawnSync.mockImplementationOnce(() => { throw new Error("git not found"); });

expect(getChangedExamples()).toEqual([]);
});
});

// ---------------------------------------------------------------------------
// computeExecutionPlan
// ---------------------------------------------------------------------------
describe("computeExecutionPlan", () => {
test("removes changed files from remaining", () => {
const all = ["examples/a.py", "examples/b.py"];
const changed = ["examples/b.py"];

const { remaining } = computeExecutionPlan(all, changed);

expect(remaining).toEqual(["examples/a.py"]);
});

test("remaining is all files when nothing changed", () => {
const all = ["examples/a.py", "examples/b.py"];

const { remaining } = computeExecutionPlan(all, []);

expect(remaining).toEqual(all);
});

test("remaining is empty when all files changed", () => {
const all = ["examples/a.py", "examples/b.py"];

const { remaining } = computeExecutionPlan(all, all);

expect(remaining).toEqual([]);
});

test("excludes deleted/renamed files from phase-1 when not present in all", () => {
const all = ["examples/a.py", "examples/b.py"];
const changed = ["examples/b.py", "examples/deleted.py"];

const { changed: validChanged, remaining } = computeExecutionPlan(all, changed);

expect(validChanged).toEqual(["examples/b.py"]);
expect(remaining).toEqual(["examples/a.py"]);
});
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// ---------------------------------------------------------------------------
// runExample
// ---------------------------------------------------------------------------
describe("runExample", () => {
beforeEach(() => jest.clearAllMocks());

test("runs successfully", () => {
spawnSync.mockReturnValue({ status: 0 });

expect(() => runExample("examples/a.py")).not.toThrow();

expect(spawnSync).toHaveBeenCalledWith(
"uv",
["run", "-m", "examples.a"],
expect.objectContaining({ stdio: "inherit" })
);
});

test("fails and exits with code 1", () => {
spawnSync.mockReturnValue({ status: 1 });

const exitSpy = jest
.spyOn(process, "exit")
.mockImplementation(() => { throw new Error("exit"); });

expect(() => runExample("examples/a.py")).toThrow("exit");
expect(exitSpy).toHaveBeenCalledWith(1);

exitSpy.mockRestore();
});
});

// ---------------------------------------------------------------------------
// runAll
// ---------------------------------------------------------------------------
describe("runAll", () => {
beforeEach(() => jest.clearAllMocks());

test("runs all files in order", () => {
spawnSync.mockReturnValue({ status: 0 });

runAll(["examples/a.py", "examples/b.py"]);

expect(spawnSync).toHaveBeenCalledTimes(2);
expect(spawnSync).toHaveBeenNthCalledWith(1, "uv", ["run", "-m", "examples.a"], expect.anything());
expect(spawnSync).toHaveBeenNthCalledWith(2, "uv", ["run", "-m", "examples.b"], expect.anything());
});

test("stops on first failure and does not run subsequent files", () => {
spawnSync
.mockReturnValueOnce({ status: 0 })
.mockReturnValueOnce({ status: 1 });

const exitSpy = jest
.spyOn(process, "exit")
.mockImplementation(() => { throw new Error("exit"); });

expect(() =>
runAll(["examples/a.py", "examples/b.py", "examples/c.py"])
).toThrow("exit");

expect(exitSpy).toHaveBeenCalledWith(1);
expect(spawnSync).toHaveBeenCalledTimes(2);

exitSpy.mockRestore();
});

test("does nothing for empty list", () => {
runAll([]);

expect(spawnSync).not.toHaveBeenCalled();
});
});
4 changes: 4 additions & 0 deletions .github/scripts/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/** @type {import('jest').Config} */
module.exports = {
testMatch: ["**/__tests__/**/*.test.js"],
};
Loading
Loading