mirror of
https://github.com/astral-sh/setup-uv.git
synced 2026-09-04 06:39:22 +00:00
Expose Python runtime identity for downstream caching
This commit is contained in:
@@ -0,0 +1,210 @@
|
||||
import * as path from "node:path";
|
||||
import type * as exec from "@actions/exec";
|
||||
import {
|
||||
afterEach,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
jest,
|
||||
} from "@jest/globals";
|
||||
import { createSetupInputs } from "../helpers/setup-inputs";
|
||||
|
||||
const mockGetExecOutput = jest.fn<typeof exec.getExecOutput>();
|
||||
const mockDebug = jest.fn();
|
||||
const originalPlatform = process.platform;
|
||||
|
||||
jest.unstable_mockModule("@actions/core", () => ({
|
||||
debug: mockDebug,
|
||||
isDebug: jest.fn(() => false),
|
||||
}));
|
||||
|
||||
jest.unstable_mockModule("@actions/exec", () => ({
|
||||
getExecOutput: mockGetExecOutput,
|
||||
}));
|
||||
|
||||
const { getPythonRuntimeId } = await import("../../src/utils/python-runtime");
|
||||
|
||||
function mockRuntime(overrides: Record<string, unknown> = {}) {
|
||||
mockGetExecOutput.mockResolvedValue({
|
||||
exitCode: 0,
|
||||
stderr: "",
|
||||
stdout: `${JSON.stringify({
|
||||
freethreaded: false,
|
||||
implementation: "cpython",
|
||||
implementationVersion: [3, 13, 1, "final", 0],
|
||||
pythonVersion: "3.13.1",
|
||||
...overrides,
|
||||
})}\r\n`,
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mockGetExecOutput.mockReset();
|
||||
mockRuntime();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Object.defineProperty(process, "platform", { value: originalPlatform });
|
||||
});
|
||||
|
||||
describe("getPythonRuntimeId", () => {
|
||||
it("does not query Python when environment activation is disabled", async () => {
|
||||
const inputs = createSetupInputs({ pythonVersion: "3.15t" });
|
||||
|
||||
expect(await getPythonRuntimeId(inputs)).toBe("");
|
||||
expect(mockGetExecOutput).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("identifies the resolved CPython version rather than the request", async () => {
|
||||
const inputs = createSetupInputs({
|
||||
activateEnvironment: true,
|
||||
pythonVersion: "3.13",
|
||||
});
|
||||
|
||||
expect(await getPythonRuntimeId(inputs)).toBe("cpython-3.13.1");
|
||||
expect(inputs.pythonVersion).toBe("3.13");
|
||||
});
|
||||
|
||||
it.each([
|
||||
["3.15.0a1", false, "cpython-3.15.0a1"],
|
||||
["3.15.0b2", false, "cpython-3.15.0b2"],
|
||||
["3.15.0rc1", false, "cpython-3.15.0rc1"],
|
||||
["3.15.0rc1", true, "cpython-3.15.0rc1-freethreaded"],
|
||||
["3.15.0rc2", true, "cpython-3.15.0rc2-freethreaded"],
|
||||
["3.15.0", true, "cpython-3.15.0-freethreaded"],
|
||||
])(
|
||||
"preserves version %s and free-threading=%s as %s",
|
||||
async (pythonVersion, freethreaded, expected) => {
|
||||
mockRuntime({ freethreaded, pythonVersion });
|
||||
|
||||
expect(
|
||||
await getPythonRuntimeId(
|
||||
createSetupInputs({ activateEnvironment: true }),
|
||||
),
|
||||
).toBe(expected);
|
||||
},
|
||||
);
|
||||
|
||||
it("derives free-threading from the interpreter rather than the input", async () => {
|
||||
const inputs = createSetupInputs({
|
||||
activateEnvironment: true,
|
||||
pythonVersion: "3.13.1t",
|
||||
});
|
||||
|
||||
expect(await getPythonRuntimeId(inputs)).toBe("cpython-3.13.1");
|
||||
});
|
||||
|
||||
it.each([
|
||||
[[7, 3, 23, "final", 0], "pypy-7.3.23-python-3.11.15"],
|
||||
[[7, 3, 24, "final", 0], "pypy-7.3.24-python-3.11.15"],
|
||||
[[7, 3, 24, "alpha", 1], "pypy-7.3.24a1-python-3.11.15"],
|
||||
[[7, 3, 24, "beta", 2], "pypy-7.3.24b2-python-3.11.15"],
|
||||
[[7, 3, 24, "candidate", 3], "pypy-7.3.24rc3-python-3.11.15"],
|
||||
])("includes PyPy implementation version %j", async (version, expected) => {
|
||||
mockRuntime({
|
||||
implementation: "pypy",
|
||||
implementationVersion: version,
|
||||
pythonVersion: "3.11.15",
|
||||
});
|
||||
|
||||
expect(
|
||||
await getPythonRuntimeId(
|
||||
createSetupInputs({ activateEnvironment: true }),
|
||||
),
|
||||
).toBe(expected);
|
||||
});
|
||||
|
||||
it("includes the name and version of other Python implementations", async () => {
|
||||
mockRuntime({
|
||||
implementation: "graalpy",
|
||||
implementationVersion: [25, 0, 0, "final", 0],
|
||||
pythonVersion: "3.12.8",
|
||||
});
|
||||
|
||||
expect(
|
||||
await getPythonRuntimeId(
|
||||
createSetupInputs({ activateEnvironment: true }),
|
||||
),
|
||||
).toBe("graalpy-25.0.0-python-3.12.8");
|
||||
});
|
||||
|
||||
it.each(["linux", "darwin", "win32"])(
|
||||
"queries the custom venv directly on %s, including paths with spaces",
|
||||
async (platform) => {
|
||||
Object.defineProperty(process, "platform", { value: platform });
|
||||
const inputs = createSetupInputs({
|
||||
activateEnvironment: true,
|
||||
venvPath: "/runner temp/custom venv",
|
||||
workingDirectory: "/different/project",
|
||||
});
|
||||
|
||||
await getPythonRuntimeId(inputs);
|
||||
|
||||
const pythonPath =
|
||||
platform === "win32"
|
||||
? path.join(inputs.venvPath, "Scripts", "python.exe")
|
||||
: path.join(inputs.venvPath, "bin", "python");
|
||||
expect(mockGetExecOutput).toHaveBeenCalledWith(
|
||||
`"${pythonPath}"`,
|
||||
["-I", "-c", expect.any(String)],
|
||||
{ silent: true },
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it("escapes quotes in the venv path for @actions/exec", async () => {
|
||||
Object.defineProperty(process, "platform", { value: "linux" });
|
||||
const inputs = createSetupInputs({
|
||||
activateEnvironment: true,
|
||||
venvPath: '/workspace/a "quoted" venv',
|
||||
});
|
||||
|
||||
await getPythonRuntimeId(inputs);
|
||||
|
||||
expect(mockGetExecOutput.mock.calls[0][0]).toContain('a \\"quoted\\" venv');
|
||||
});
|
||||
|
||||
it.each(["", "not JSON", "null", "{}"])(
|
||||
"returns an empty output for invalid interpreter output: %j",
|
||||
async (stdout) => {
|
||||
mockGetExecOutput.mockResolvedValue({ exitCode: 0, stderr: "", stdout });
|
||||
|
||||
expect(
|
||||
await getPythonRuntimeId(
|
||||
createSetupInputs({ activateEnvironment: true }),
|
||||
),
|
||||
).toBe("");
|
||||
expect(mockDebug).toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
|
||||
it("returns an empty output for an invalid implementation version", async () => {
|
||||
mockRuntime({
|
||||
implementation: "pypy",
|
||||
implementationVersion: [7, 3, 24, "unknown", 0],
|
||||
});
|
||||
|
||||
expect(
|
||||
await getPythonRuntimeId(
|
||||
createSetupInputs({ activateEnvironment: true }),
|
||||
),
|
||||
).toBe("");
|
||||
});
|
||||
|
||||
it.each([new Error("interpreter missing"), "interpreter failed"])(
|
||||
"returns an empty output if the interpreter cannot be queried: %s",
|
||||
async (error) => {
|
||||
mockGetExecOutput.mockRejectedValue(error);
|
||||
|
||||
expect(
|
||||
await getPythonRuntimeId(
|
||||
createSetupInputs({ activateEnvironment: true }),
|
||||
),
|
||||
).toBe("");
|
||||
expect(mockDebug).toHaveBeenCalledWith(
|
||||
expect.stringContaining(error instanceof Error ? error.message : error),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -1,131 +0,0 @@
|
||||
import * as path from "node:path";
|
||||
import type * as exec from "@actions/exec";
|
||||
import {
|
||||
afterEach,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
jest,
|
||||
} from "@jest/globals";
|
||||
import { createSetupInputs } from "../helpers/setup-inputs";
|
||||
|
||||
const mockGetExecOutput = jest.fn<typeof exec.getExecOutput>();
|
||||
const mockDebug = jest.fn();
|
||||
const originalPlatform = process.platform;
|
||||
|
||||
jest.unstable_mockModule("@actions/core", () => ({
|
||||
debug: mockDebug,
|
||||
isDebug: jest.fn(() => false),
|
||||
}));
|
||||
|
||||
jest.unstable_mockModule("@actions/exec", () => ({
|
||||
getExecOutput: mockGetExecOutput,
|
||||
}));
|
||||
|
||||
const { getResolvedPythonVersion } = await import(
|
||||
"../../src/utils/python-version"
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
mockGetExecOutput.mockReset();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Object.defineProperty(process, "platform", { value: originalPlatform });
|
||||
});
|
||||
|
||||
describe("getResolvedPythonVersion", () => {
|
||||
it("does not query Python when environment activation is disabled", async () => {
|
||||
const inputs = createSetupInputs({ pythonVersion: "3.15t" });
|
||||
|
||||
expect(await getResolvedPythonVersion(inputs)).toBe("");
|
||||
expect(mockGetExecOutput).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each([
|
||||
["3.13", "3.13.1"],
|
||||
["3.13.1t", "3.13.1"],
|
||||
["3.15t", "3.15.0rc1"],
|
||||
["pypy3.11", "3.11.11"],
|
||||
["", "3.12.9"],
|
||||
])(
|
||||
"resolves %j to the interpreter's version %s",
|
||||
async (request, version) => {
|
||||
mockGetExecOutput.mockResolvedValue({
|
||||
exitCode: 0,
|
||||
stderr: "",
|
||||
stdout: `${version}\r\n`,
|
||||
});
|
||||
const inputs = createSetupInputs({
|
||||
activateEnvironment: true,
|
||||
pythonVersion: request,
|
||||
});
|
||||
|
||||
expect(await getResolvedPythonVersion(inputs)).toBe(version);
|
||||
expect(inputs.pythonVersion).toBe(request);
|
||||
},
|
||||
);
|
||||
|
||||
it.each(["linux", "darwin", "win32"])(
|
||||
"queries the custom venv directly on %s, including paths with spaces",
|
||||
async (platform) => {
|
||||
Object.defineProperty(process, "platform", { value: platform });
|
||||
mockGetExecOutput.mockResolvedValue({
|
||||
exitCode: 0,
|
||||
stderr: "",
|
||||
stdout: "3.13.1\n",
|
||||
});
|
||||
const inputs = createSetupInputs({
|
||||
activateEnvironment: true,
|
||||
venvPath: "/runner temp/custom venv",
|
||||
workingDirectory: "/different/project",
|
||||
});
|
||||
|
||||
await getResolvedPythonVersion(inputs);
|
||||
|
||||
const pythonPath =
|
||||
platform === "win32"
|
||||
? path.join(inputs.venvPath, "Scripts", "python.exe")
|
||||
: path.join(inputs.venvPath, "bin", "python");
|
||||
expect(mockGetExecOutput).toHaveBeenCalledWith(
|
||||
`"${pythonPath}"`,
|
||||
["-I", "-c", "import platform; print(platform.python_version())"],
|
||||
{ silent: true },
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it("escapes quotes in the venv path for @actions/exec", async () => {
|
||||
Object.defineProperty(process, "platform", { value: "linux" });
|
||||
mockGetExecOutput.mockResolvedValue({
|
||||
exitCode: 0,
|
||||
stderr: "",
|
||||
stdout: "3.13.1\n",
|
||||
});
|
||||
const inputs = createSetupInputs({
|
||||
activateEnvironment: true,
|
||||
venvPath: '/workspace/a "quoted" venv',
|
||||
});
|
||||
|
||||
await getResolvedPythonVersion(inputs);
|
||||
|
||||
expect(mockGetExecOutput.mock.calls[0][0]).toContain('a \\"quoted\\" venv');
|
||||
});
|
||||
|
||||
it.each([new Error("interpreter missing"), "interpreter failed"])(
|
||||
"returns an empty output if the interpreter cannot be queried: %s",
|
||||
async (error) => {
|
||||
mockGetExecOutput.mockRejectedValue(error);
|
||||
|
||||
expect(
|
||||
await getResolvedPythonVersion(
|
||||
createSetupInputs({ activateEnvironment: true }),
|
||||
),
|
||||
).toBe("");
|
||||
expect(mockDebug).toHaveBeenCalledWith(
|
||||
expect.stringContaining(error instanceof Error ? error.message : error),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user