Ctrl K
ComunidadGuías…

tests/standalonePython.test.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// The standalone Python the Install button fetches when a machine has neither
// uv nor a Python 3.10–3.13 (server/standalonePython.ts) — the packaged
// desktop app's usual case on Windows and Linux.
//
// The Windows half cannot be RUN here, so its path logic is proved as logic:
// the lookup that replaced `where` (PATHEXT, the `Path` spelling, quoted
// entries), the venv's Scripts\python.exe, the standalone's python.exe, and
// the Windows asset in the pinned table. The unpacker is driven with archives
// the system tar writes in both of its formats, plus a hand-made hostile one.

import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { createHash } from "node:crypto";
import { existsSync, lstatSync, mkdtempSync, readFileSync, readlinkSync, rmSync, statSync, writeFileSync, mkdirSync } from "node:fs";
import os from "node:os";
import path from "node:path";
import { gzipSync } from "node:zlib";
import { after, describe, it } from "node:test";
import {
  extractTarGz,
  fetchStandalonePython,
  findOnPath,
  PYTHON_BUILDS,
  pythonBuildFor,
  standalonePythonExe,
  venvPythonIn,
  type PythonBuild,
} from "../server/standalonePython.ts";

const tmp = mkdtempSync(path.join(os.tmpdir(), "astrolabe-pbs-"));
after(() => rmSync(tmp, { recursive: true, force: true }));

describe("finding a program without which or where", () => {
  it("walks a POSIX PATH in order", () => {
    const files = new Set(["/opt/uv/bin/uv", "/usr/bin/uv"]);
    assert.equal(findOnPath("uv", { PATH: "/nope:/opt/uv/bin:/usr/bin" }, "linux", (p) => files.has(p)), "/opt/uv/bin/uv");
    assert.equal(findOnPath("python3", { PATH: "/usr/bin:/bin" }, "linux", (p) => files.has(p)), null);
    assert.equal(findOnPath("uv", {}, "linux", () => true), null, "no PATH, nothing found");
  });

  it("on Windows: PATHEXT, the `Path` spelling, quoted entries, case-blind extensions", () => {
    const files = new Set(["C:\\Users\\r\\.local\\bin\\uv.exe", "C:\\Program Files\\Python312\\python.exe"]);
    const env = { Path: 'C:\\Windows\\System32;"C:\\Program Files\\Python312";C:\\Users\\r\\.local\\bin', PATHEXT: ".COM;.EXE;.BAT" };
    assert.equal(findOnPath("uv", env, "win32", (p) => files.has(p)), "C:\\Users\\r\\.local\\bin\\uv.exe");
    assert.equal(findOnPath("python", env, "win32", (p) => files.has(p)), "C:\\Program Files\\Python312\\python.exe");
    assert.equal(findOnPath("python.exe", env, "win32", (p) => files.has(p)), "C:\\Program Files\\Python312\\python.exe");
    assert.equal(findOnPath("py", env, "win32", (p) => files.has(p)), null);
  });
});

describe("where the interpreters are", () => {
  it("the venv: Scripts\\python.exe on Windows, bin/python elsewhere", () => {
    assert.equal(venvPythonIn("C:\\data\\tts\\venv", "win32"), "C:\\data\\tts\\venv\\Scripts\\python.exe");
    assert.equal(venvPythonIn("/data/tts/venv", "linux"), "/data/tts/venv/bin/python");
  });

  it("the standalone build: python.exe at its root on Windows, bin/python3 elsewhere", () => {
    assert.equal(standalonePythonExe("C:\\data\\tts\\python", "win32"), "C:\\data\\tts\\python\\python.exe");
    assert.equal(standalonePythonExe("/data/tts/python", "darwin"), "/data/tts/python/bin/python3");
  });
});

describe("the pinned builds", () => {
  it("covers the desktop app's platforms, each an install-only stripped CPython 3.12 with a size and a digest", () => {
    for (const key of ["linux-x64", "linux-arm64", "win32-x64", "win32-arm64", "darwin-x64", "darwin-arm64"]) {
      const b = PYTHON_BUILDS[key];
      assert.ok(b, key);
      assert.match(b.file, /^cpython-3\.12\.\d+\+\d{8}-.+-install_only_stripped\.tar\.gz$/, key);
      assert.match(b.sha256, /^[0-9a-f]{64}$/, key);
      assert.ok(b.bytes > 15_000_000 && b.bytes < 40_000_000, `${key}: ${b.bytes}`);
      assert.ok(b.url.startsWith("https://github.com/astral-sh/python-build-standalone/releases/download/"), key);
      assert.ok(b.url.includes("%2B"), `${key}: the + in the name is escaped`);
    }
    assert.match(pythonBuildFor("win32", "x64")!.file, /x86_64-pc-windows-msvc/);
    assert.match(pythonBuildFor("linux", "x64")!.file, /x86_64-unknown-linux-gnu/);
    assert.equal(pythonBuildFor("linux", "ia32"), null);
    assert.equal(pythonBuildFor("freebsd", "x64"), null);
  });
});

/** A python-build-standalone-shaped tree: python/bin/python3 (a script
 *  standing in), a symlink beside it, a long path, a Windows python.exe. */
function makeTree(root: string): void {
  const long = path.join(root, "python", "lib", "a".repeat(60), "b".repeat(60));
  mkdirSync(path.join(root, "python", "bin"), { recursive: true });
  mkdirSync(long, { recursive: true });
  writeFileSync(path.join(root, "python", "bin", "python3.12"), "#!/bin/sh\necho 3 12\n", { mode: 0o755 });
  spawnSync("ln", ["-s", "python3.12", path.join(root, "python", "bin", "python3")]);
  writeFileSync(path.join(long, "module.py"), "x = 1\n");
  writeFileSync(path.join(root, "python", "python.exe"), "MZ");
}

function tarOf(format: "gnu" | "pax"): string {
  const src = path.join(tmp, `src-${format}`);
  makeTree(src);
  const out = path.join(tmp, `py-${format}.tar.gz`);
  const r = spawnSync("tar", [`--format=${format}`, "-czf", out, "-C", src, "python"], { encoding: "utf8" });
  assert.equal(r.status, 0, r.stderr);
  return out;
}

describe("unpacking", () => {
  const hasTar = spawnSync("tar", ["--version"]).status === 0;

  for (const format of ["gnu", "pax"] as const) {
    it(`unpacks a ${format} archive: files keep their mode, symlinks stay links, long names arrive`, { skip: !hasTar }, async () => {
      const dest = path.join(tmp, `out-${format}`);
      await extractTarGz(tarOf(format), dest, "linux");
      const exe = path.join(dest, "python", "bin", "python3.12");
      assert.equal(readFileSync(exe, "utf8"), "#!/bin/sh\necho 3 12\n");
      assert.ok((statSync(exe).mode & 0o111) !== 0, "executable");
      const link = path.join(dest, "python", "bin", "python3");
      assert.ok(lstatSync(link).isSymbolicLink());
      assert.equal(readlinkSync(link), "python3.12");
      assert.ok(existsSync(path.join(dest, "python", "lib", "a".repeat(60), "b".repeat(60), "module.py")));
    });
  }

  it("on Windows a symlink is unpacked as a copy of its target", { skip: !hasTar }, async () => {
    const dest = path.join(tmp, "out-win");
    await extractTarGz(tarOf("gnu"), dest, "win32");
    const link = path.join(dest, "python", "bin", "python3");
    assert.ok(!lstatSync(link).isSymbolicLink());
    assert.equal(readFileSync(link, "utf8"), "#!/bin/sh\necho 3 12\n");
  });

  it("refuses an archive that names a path outside its folder", async () => {
    const header = Buffer.alloc(512);
    header.write("../escaped.txt", 0, "utf8");
    header.write("0000644\0", 100, "latin1");
    header.write("00000000005\0", 124, "latin1");
    header.write("0", 156, "latin1");
    header.write("ustar\0", 257, "latin1");
    header.write("00", 263, "latin1");
    header.fill(" ", 148, 156);
    let sum = 0;
    for (const b of header) sum += b;
    header.write(sum.toString(8).padStart(6, "0") + "\0 ", 148, "latin1");
    const body = Buffer.alloc(512);
    body.write("evil\n");
    const archive = path.join(tmp, "evil.tar.gz");
    writeFileSync(archive, gzipSync(Buffer.concat([header, body, Buffer.alloc(1024)])));
    const dest = path.join(tmp, "out-evil");
    await assert.rejects(extractTarGz(archive, dest, "linux"), /outside its folder/);
    assert.ok(!existsSync(path.join(tmp, "escaped.txt")));
  });
});

describe("fetching", () => {
  const hasTar = spawnSync("tar", ["--version"]).status === 0;
  const serve = (bytes: Buffer): typeof fetch =>
    (async () => new Response(new Uint8Array(bytes), { status: 200 })) as unknown as typeof fetch;

  it("checks the size and the digest, unpacks into the root, and answers the interpreter", { skip: !hasTar }, async () => {
    const bytes = readFileSync(tarOf("gnu"));
    const spec: PythonBuild = { file: "py.tar.gz", url: "https://example.invalid/py.tar.gz", bytes: bytes.length, sha256: createHash("sha256").update(bytes).digest("hex") };
    const root = path.join(tmp, "data", "tts", "python");
    const heard: number[] = [];
    const exe = await fetchStandalonePython(root, (p) => heard.push(p), { platform: "linux", arch: "x64", fetcher: serve(bytes), spec });
    assert.equal(exe, path.join(root, "bin", "python3"));
    assert.ok(existsSync(exe));
    assert.equal(heard[heard.length - 1], 100, "the progress reaches 100");
    assert.ok(!existsSync(`${root}.part`), "no staging left behind");
    assert.deepEqual(
      (await import("node:fs")).readdirSync(path.dirname(root)).filter((n) => n.endsWith(".part")),
      [],
      "no tarball left behind",
    );
    // A second call finds it and fetches nothing.
    const again = await fetchStandalonePython(root, () => {}, { platform: "linux", arch: "x64", fetcher: (() => { throw new Error("fetched twice"); }) as unknown as typeof fetch, spec });
    assert.equal(again, exe);
  });

  it("refuses a download of the wrong size or the wrong digest, and keeps nothing", async () => {
    const bytes = Buffer.from("not a python");
    const root = path.join(tmp, "data2", "tts", "python");
    const wrongSize: PythonBuild = { file: "py.tar.gz", url: "https://example.invalid/py", bytes: 999, sha256: "0".repeat(64) };
    await assert.rejects(fetchStandalonePython(root, () => {}, { platform: "linux", arch: "x64", fetcher: serve(bytes), spec: wrongSize }), /arrived at 12 bytes, not 999/);
    const wrongSum: PythonBuild = { ...wrongSize, bytes: bytes.length };
    await assert.rejects(fetchStandalonePython(root, () => {}, { platform: "linux", arch: "x64", fetcher: serve(bytes), spec: wrongSum }), /checksum/);
    assert.ok(!existsSync(root));
  });

  it("says so when the platform has no build", async () => {
    await assert.rejects(fetchStandalonePython(path.join(tmp, "x"), () => {}, { platform: "linux", arch: "ia32" }), /No standalone Python is offered for linux-ia32/);
  });
});