From a977bbb768a072f176c8d1e611bab2c86b8e1946 Mon Sep 17 00:00:00 2001 From: Michael Cooper Date: Fri, 13 Sep 2024 10:50:07 -0700 Subject: [PATCH] install and test poetry (#9) --- Dockerfile | 5 +- bin/test.ts | 9 ++- tests/dataloader-languages.test.ts | 75 ++++++++++++++++++----- tests/fixtures/poetry-test/pyproject.toml | 6 ++ tests/index.ts | 38 +++++++++--- 5 files changed, 107 insertions(+), 26 deletions(-) create mode 100644 tests/fixtures/poetry-test/pyproject.toml diff --git a/Dockerfile b/Dockerfile index c04b9f7..65ebe04 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,12 +3,11 @@ FROM buildpack-deps:bookworm AS base ENV CACHEBUST=2024-09-06 RUN apt update -# Rust envvars ENV RUSTUP_HOME=/usr/local/rustup \ CARGO_HOME=/usr/local/cargo \ RUST_VERSION=1.81.0 \ VIRTUAL_ENV=/var/local/python-venv -ENV PATH=/usr/local/cargo/bin:$VIRTUAL_ENV/bin:$PATH +ENV PATH=/usr/local/cargo/bin:$VIRTUAL_ENV/bin:/root/.local/bin:$PATH # == node ====================== FROM base AS node @@ -33,6 +32,8 @@ RUN --mount=type=cache,target=/var/cache/apt,id=framework-runtime-python \ python3-wheel \ python3-dev \ python3-venv \ + pipx \ + && pipx install poetry \ && python3 -m venv $VIRTUAL_ENV # == R =========================== diff --git a/bin/test.ts b/bin/test.ts index 27c985b..46aea81 100755 --- a/bin/test.ts +++ b/bin/test.ts @@ -5,6 +5,13 @@ import { dirname } from "node:path"; import { run as runTests } from "node:test"; import { spec } from "node:test/reporters"; +import { parseArgs } from "node:util"; +const { values: { "only": argOnly } } = parseArgs({ + options: { + "only": { type: "boolean" }, + } +}); + export async function buildTestImage() { console.log("building image..."); let stdio = new StringStream(); @@ -34,7 +41,7 @@ const files = await glob(["tests/**/*.test.ts"], { }); await buildTestImage(); -runTests({ files, concurrency: true }) +runTests({ files, concurrency: true, only: argOnly }) .on("test:fail", () => { process.exitCode = 1; }) diff --git a/tests/dataloader-languages.test.ts b/tests/dataloader-languages.test.ts index e1e8a08..9cfd7d6 100644 --- a/tests/dataloader-languages.test.ts +++ b/tests/dataloader-languages.test.ts @@ -1,4 +1,5 @@ import { test, describe } from "node:test"; +import os from "node:os"; import assert from "node:assert/strict"; import { assertSemver, @@ -6,23 +7,10 @@ import { binaryVersionTest, runCommandInContainer, } from "./index.ts"; +import { cp, mkdtemp, rm } from "node:fs/promises"; +import { join } from "node:path"; describe("Dataloader languages", () => { - describe("Python", () => { - binaryVersionTest({ - binary: "python3", - semver: "^3.11", - prefix: "Python", - }); - - binaryOnPathTest({ binary: "pip" }); - - test(`A Python virtual environment is activated`, async () => { - // should not throw - await runCommandInContainer(["pip", "install", "requests"]); - }); - }); - describe("JavaScript", () => { binaryVersionTest({ binary: "node", semver: "^20.17" }); binaryVersionTest({ binary: "npm", semver: "^10.5" }); @@ -57,6 +45,63 @@ describe("Dataloader languages", () => { }); }); + describe("Python", () => { + binaryVersionTest({ + binary: "python3", + semver: "^3.11", + prefix: "Python", + }); + + binaryVersionTest({ + binary: "pip", + semver: "^23.0.1", + extract: /^pip ([^ ]+) /, + }); + binaryVersionTest({ binary: "pipx", semver: "^1.1.0" }); + binaryVersionTest({ + binary: "poetry", + semver: "^1.8.3", + prefix: "Poetry (version ", + suffix: ")", + }); + + test(`A Python virtual environment is activated`, async () => { + // should not throw + await runCommandInContainer(["pip", "install", "pip-install-test"]); + }); + + test(`Poetry can install dependencies in the virtualenv`, async () => { + let testDir = await mkdtemp(join(os.tmpdir(), "poetry-test-")); + try { + // This will install dependencies using Poetry, and then try to run `ls` + // in the installed dependency's package. If the package is not + // installed here, the `ls` command will exit non-zero and + // `runCommandInContainer` will throw. + await cp( + "./tests/fixtures/poetry-test/pyproject.toml", + `${testDir}/pyproject.toml`, + ); + let res = await runCommandInContainer( + [ + "sh", + "-c", + "poetry install; ls $(poetry env info --path)/lib/python3.11/site-packages/pip_install_test/__init__.py", + ], + { + workingDir: "/poetry-test", + mounts: [{ host: testDir, container: "/poetry-test" }], + }, + ); + } finally { + try { + await rm(testDir, { recursive: true }); + } catch { + /* ok */ + } + } + }); + }); + binaryVersionTest({ binary: "Rscript", semver: "^4.4.1", diff --git a/tests/fixtures/poetry-test/pyproject.toml b/tests/fixtures/poetry-test/pyproject.toml new file mode 100644 index 0000000..a1efe59 --- /dev/null +++ b/tests/fixtures/poetry-test/pyproject.toml @@ -0,0 +1,6 @@ +[tool.poetry] +package-mode = false + +[tool.poetry.dependencies] +python = "^3.11" +pip-install-test = "^0.5" diff --git a/tests/index.ts b/tests/index.ts index 8631845..4d0360b 100644 --- a/tests/index.ts +++ b/tests/index.ts @@ -1,4 +1,5 @@ import { test, before } from "node:test"; +import { resolve } from "node:path"; import assert from "node:assert"; import Dockerode from "dockerode"; import { Stream } from "node:stream"; @@ -13,6 +14,7 @@ export interface AssertBinaryVersionOptions { semver: string; extract?: RegExp; prefix?: string; + suffix?: string; expectStderr?: RegExp; } @@ -22,12 +24,16 @@ export async function binaryVersionTest({ semver, extract, prefix, + suffix, expectStderr = /^$/, }: AssertBinaryVersionOptions) { await test(`${name} ${semver} is available`, async () => { const res = await runCommandInContainer([binary, "--version"]); - assert.ok(res.stderr.match(expectStderr), `Expected stderr to match, got: ${res.stderr}`); - assertSemver(res.stdout, semver, { extract, prefix }); + assert.ok( + res.stderr.match(expectStderr), + `Expected stderr to match, got: ${res.stderr}`, + ); + assertSemver(res.stdout, semver, { extract, prefix, suffix }); }); } @@ -54,7 +60,7 @@ export function assertSemver( prefix, suffix, extract, - }: { prefix?: string; suffix?: string; extract?: RegExp } = {} + }: { prefix?: string; suffix?: string; extract?: RegExp } = {}, ) { actual = actual.trim(); if (prefix && actual.startsWith(prefix)) actual = actual.slice(prefix.length); @@ -70,7 +76,7 @@ export function assertSemver( actual = actual.trim(); assert.ok( semverSatisfies(actual, expected), - `Expected semver match for ${expected}, got ${actual}` + `Expected semver match for ${expected}, got ${JSON.stringify(actual)}`, ); } @@ -82,12 +88,26 @@ function ensureDocker() { before(ensureDocker); export async function runCommandInContainer( - command: string[] + command: string[], + { + mounts = [], + workingDir = "/", + }: { + mounts?: { host: string; container: string}[]; + workingDir?: string; + } = {}, ): Promise<{ stdout: string; stderr: string }> { const docker = ensureDocker(); const container = await docker.createContainer({ + WorkingDir: workingDir, Image: IMAGE_TAG, Cmd: command, + HostConfig: { + Binds: mounts.map( + ({ host, container }) => + `${resolve(host)}:${container}`, + ), + }, }); const stdout = new StringStream(); const stderr = new StringStream(); @@ -100,9 +120,11 @@ export async function runCommandInContainer( await container.start(); const wait = (await container.wait()) as { StatusCode: number }; if (wait.StatusCode !== 0) { - throw new Error(`Command failed with status code ${wait.StatusCode}\n` + - `stdout:\n${stdout.string}\n\n` + - `stderr:\n${stderr.string}`); + throw new Error( + `Command failed with status code ${wait.StatusCode}\n` + + `stdout:\n${stdout.string}\n\n` + + `stderr:\n${stderr.string}`, + ); } return { stdout: stdout.string, stderr: stderr.string }; }