Makelore 2.0 initial clean snapshot
This commit is contained in:
178
tests/unit/bundled-python-packaging.test.ts
Normal file
178
tests/unit/bundled-python-packaging.test.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
import { createHash } from 'node:crypto';
|
||||
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import { parse as parseYaml } from 'yaml';
|
||||
|
||||
const temporaryDirectories: string[] = [];
|
||||
|
||||
async function loadManifest() {
|
||||
return await import(pathToFileURL(join(process.cwd(), 'scripts', 'bundled-python-manifest.mjs')).href);
|
||||
}
|
||||
|
||||
async function loadDownloader() {
|
||||
return await import(pathToFileURL(join(process.cwd(), 'scripts', 'download-bundled-python.mjs')).href);
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(temporaryDirectories.splice(0).map((directory) => rm(directory, { recursive: true, force: true })));
|
||||
});
|
||||
|
||||
describe('bundled Python packaging', () => {
|
||||
it('pins Python 3.12.13 archives and checksums for every desktop target', async () => {
|
||||
const { PYTHON_RELEASE, PYTHON_TARGETS, PYTHON_VERSION } = await loadManifest();
|
||||
|
||||
expect(PYTHON_VERSION).toBe('3.12.13');
|
||||
expect(PYTHON_RELEASE).toBe('20260510');
|
||||
expect(Object.keys(PYTHON_TARGETS).sort()).toEqual([
|
||||
'darwin-arm64',
|
||||
'darwin-x64',
|
||||
'linux-arm64',
|
||||
'linux-x64',
|
||||
'win32-x64',
|
||||
]);
|
||||
expect(PYTHON_TARGETS['win32-x64']).toMatchObject({
|
||||
archiveName: 'cpython-3.12.13+20260510-x86_64-pc-windows-msvc-install_only_stripped.tar.gz',
|
||||
sha256: '24168aff2e7d93784c6a436124c4ebb79b076a4e289bde4902c08333507b71d0',
|
||||
executable: 'python/python.exe',
|
||||
});
|
||||
expect(PYTHON_TARGETS['darwin-arm64'].sha256).toBe('55bc1a5edbc8ac4da0081f4f5731ed2d1ed10c57cb37a820b2a0dbc7cad742e9');
|
||||
expect(PYTHON_TARGETS['darwin-x64'].sha256).toBe('6bab7fa97d4f2ddba86da0e05acff66c53b5edaca1df8edcf00ddca785a9c59b');
|
||||
expect(PYTHON_TARGETS['linux-arm64'].sha256).toBe('8e2907baca8b08ae43f366e65f09fda6337d3543f322c399d8800cf689c79973');
|
||||
expect(PYTHON_TARGETS['linux-x64'].sha256).toBe('d480f5d5878910ecbae212bf23bd7c25d7b209eb8cf5e98823c977384d272e88');
|
||||
for (const target of Object.values(PYTHON_TARGETS) as Array<{ url: string }>) {
|
||||
expect(target.url).toContain('/releases/download/20260510/');
|
||||
expect(target.url).toContain('%2B20260510-');
|
||||
}
|
||||
expect(PYTHON_TARGETS['win32-x64'].mirrorUrl).toContain('/python-build-standalone/20260510/');
|
||||
});
|
||||
|
||||
it('rejects unsupported targets before downloading', async () => {
|
||||
const { stagePythonTarget } = await loadDownloader();
|
||||
await expect(stagePythonTarget('freebsd-x64')).rejects.toThrow('Unsupported bundled Python target: freebsd-x64');
|
||||
});
|
||||
|
||||
it('rejects a digest mismatch and removes temporary files', async () => {
|
||||
const resourcesRoot = await mkdtemp(join(tmpdir(), 'niancode-python-bad-digest-'));
|
||||
temporaryDirectories.push(resourcesRoot);
|
||||
const { stagePythonTarget } = await loadDownloader();
|
||||
|
||||
await expect(stagePythonTarget('win32-x64', {
|
||||
resourcesRoot,
|
||||
fetchImpl: async () => new Response(Buffer.from('not-the-pinned-archive')),
|
||||
extractArchive: async () => undefined,
|
||||
})).rejects.toThrow('SHA-256 mismatch for win32-x64');
|
||||
|
||||
await expect(readFile(join(resourcesRoot, 'python', 'win32-x64', '.niancode-python.json'))).rejects.toMatchObject({ code: 'ENOENT' });
|
||||
});
|
||||
|
||||
it('atomically stages a verified runtime and writes its marker', async () => {
|
||||
const resourcesRoot = await mkdtemp(join(tmpdir(), 'niancode-python-valid-'));
|
||||
temporaryDirectories.push(resourcesRoot);
|
||||
const archive = Buffer.from('verified-test-archive');
|
||||
const digest = createHash('sha256').update(archive).digest('hex');
|
||||
const { stagePythonTarget } = await loadDownloader();
|
||||
|
||||
const staged = await stagePythonTarget('win32-x64', {
|
||||
resourcesRoot,
|
||||
targetOverride: {
|
||||
archiveName: 'fixture.tar.gz',
|
||||
url: 'https://example.invalid/fixture.tar.gz',
|
||||
sha256: digest,
|
||||
executable: 'python/python.exe',
|
||||
},
|
||||
fetchImpl: async () => new Response(archive),
|
||||
extractArchive: async (_archivePath: string, destination: string) => {
|
||||
const executable = join(destination, 'python', 'python.exe');
|
||||
await mkdir(dirname(executable), { recursive: true });
|
||||
await writeFile(executable, 'fixture');
|
||||
},
|
||||
});
|
||||
|
||||
expect(staged).toBe(join(resourcesRoot, 'python', 'win32-x64'));
|
||||
expect(JSON.parse(await readFile(join(staged, '.niancode-python.json'), 'utf8'))).toEqual({
|
||||
release: '20260510',
|
||||
sha256: digest,
|
||||
target: 'win32-x64',
|
||||
version: '3.12.13+20260510',
|
||||
});
|
||||
expect(await readFile(join(staged, 'python', 'python.exe'), 'utf8')).toBe('fixture');
|
||||
});
|
||||
|
||||
it('falls back to the configured mirror when GitHub is unreachable', async () => {
|
||||
const resourcesRoot = await mkdtemp(join(tmpdir(), 'niancode-python-mirror-'));
|
||||
temporaryDirectories.push(resourcesRoot);
|
||||
const archive = Buffer.from('mirror-archive');
|
||||
const digest = createHash('sha256').update(archive).digest('hex');
|
||||
const requestedUrls: string[] = [];
|
||||
const { stagePythonTarget } = await loadDownloader();
|
||||
|
||||
await stagePythonTarget('win32-x64', {
|
||||
resourcesRoot,
|
||||
targetOverride: {
|
||||
archiveName: 'fixture.tar.gz',
|
||||
url: 'https://github.example/fixture.tar.gz',
|
||||
mirrorUrl: 'https://mirror.example/fixture.tar.gz',
|
||||
sha256: digest,
|
||||
executable: 'python/python.exe',
|
||||
},
|
||||
fetchImpl: async (url: string) => {
|
||||
requestedUrls.push(url);
|
||||
if (url.includes('github.example')) throw Object.assign(new Error('read ECONNRESET'), { code: 'ECONNRESET' });
|
||||
return new Response(archive);
|
||||
},
|
||||
extractArchive: async (_archivePath: string, destination: string) => {
|
||||
const executable = join(destination, 'python', 'python.exe');
|
||||
await mkdir(dirname(executable), { recursive: true });
|
||||
await writeFile(executable, 'fixture');
|
||||
},
|
||||
});
|
||||
|
||||
expect(requestedUrls).toEqual([
|
||||
'https://github.example/fixture.tar.gz',
|
||||
'https://mirror.example/fixture.tar.gz',
|
||||
]);
|
||||
});
|
||||
|
||||
it('prepares Python before every package target', async () => {
|
||||
const packageJson = JSON.parse(await readFile(join(process.cwd(), 'package.json'), 'utf8'));
|
||||
|
||||
expect(packageJson.scripts['python:download:win']).toContain('--platform=win');
|
||||
expect(packageJson.scripts['python:download:mac']).toContain('--platform=mac');
|
||||
expect(packageJson.scripts['python:download:linux']).toContain('--platform=linux');
|
||||
expect(packageJson.scripts['package:win']).toMatch(/^pnpm run python:download:win && /);
|
||||
expect(packageJson.scripts['package:mac']).toMatch(/^pnpm run python:download:mac && /);
|
||||
expect(packageJson.scripts['package:mac:local']).toMatch(/^pnpm run python:download:mac && /);
|
||||
expect(packageJson.scripts['package:linux']).toMatch(/^pnpm run python:download:linux && /);
|
||||
expect(packageJson.scripts.release).toMatch(/^pnpm run python:download && /);
|
||||
});
|
||||
|
||||
it('copies only the target architecture into packaged resources', async () => {
|
||||
const builder = parseYaml(await readFile(join(process.cwd(), 'electron-builder.yml'), 'utf8'));
|
||||
|
||||
expect(builder.win.extraResources).toContainEqual({
|
||||
from: 'resources/python/win32-${arch}/python',
|
||||
to: 'python',
|
||||
});
|
||||
expect(builder.mac.extraResources).toContainEqual({
|
||||
from: 'resources/python/darwin-${arch}/python',
|
||||
to: 'python',
|
||||
});
|
||||
expect(builder.linux.extraResources).toContainEqual({
|
||||
from: 'resources/python/linux-${arch}/python',
|
||||
to: 'python',
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps downloaded Python runtimes out of source control', async () => {
|
||||
const rootIgnore = await readFile(join(process.cwd(), '.gitignore'), 'utf8');
|
||||
expect(rootIgnore.split(/\r?\n/)).toContain('resources/python/');
|
||||
});
|
||||
|
||||
it('keeps executable module scripts on LF for Windows test runners', async () => {
|
||||
const attributes = await readFile(join(process.cwd(), '.gitattributes'), 'utf8');
|
||||
expect(attributes.split(/\r?\n/)).toContain('scripts/*.mjs text eol=lf');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user