Repository navigation
Expand file tree
/
Copy pathcache-key.test.js
More file actions
150 lines (122 loc) · 5.8 KB
/
Copy pathcache-key.test.js
File metadata and controls
150 lines (122 loc) · 5.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
import { execFileSync } from 'child_process';
import fs from 'fs';
import os from 'os';
import path from 'path';
import { afterEach, describe, test, expect } from 'vitest';
import { getPythonIdentity, parsePythonIdentity, virtualenvBaseKey, virtualenvMatchesInterpreter } from './cache-key.js';
const hasPython3 = (() => {
try {
execFileSync('python3', ['--version']);
return true;
} catch {
return false;
}
})();
const identity = {
executable: '/opt/hostedtoolcache/Python/3.13.7/x64/bin/python3.13',
baseExecutable: '/opt/hostedtoolcache/Python/3.13.7/x64/bin/python3',
version: '3.13.7',
extSuffix: '.cpython-313-x86_64-linux-gnu.so',
osRelease: 'ubuntu-24.04',
};
const virtualenvPath = '/home/runner/work/site/site/trellis/.trellis/virtualenv';
function withFakePython3(script) {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'fake-python3-'));
fs.writeFileSync(path.join(dir, 'python3'), `#!/bin/sh\n${script}\n`, { mode: 0o755 });
process.env.PATH = `${dir}${path.delimiter}${process.env.PATH}`;
return dir;
}
describe('getPythonIdentity', () => {
const originalPath = process.env.PATH;
afterEach(() => {
process.env.PATH = originalPath;
});
test.skipIf(!hasPython3)('reads the identity of the real python3', async () => {
const parsed = await getPythonIdentity();
expect(parsed.executable).toMatch(/^\//);
expect(parsed.baseExecutable).toMatch(/^\//);
expect(parsed.extSuffix).toMatch(/^\..*\.so$/);
});
test('runs the identity script isolated and without site hooks', async () => {
const dir = withFakePython3(`echo "$1 $2 $3" > "$(dirname "$0")/args"\necho '${JSON.stringify(identity)}'`);
await expect(getPythonIdentity()).resolves.toEqual(identity);
expect(fs.readFileSync(path.join(dir, 'args'), 'utf8').trim()).toEqual('-I -S -c');
});
test('rejects with the stderr of a failing python3', async () => {
withFakePython3('echo "pyenv: version 3.11.9 is not installed" >&2\nexit 1');
await expect(getPythonIdentity()).rejects.toThrow('python3 exited with code 1: pyenv: version 3.11.9 is not installed');
});
});
describe('parsePythonIdentity', () => {
test('uses the last line of output', () => {
const stdout = `notice: something, printed first\n${JSON.stringify(identity)}\n`;
expect(parsePythonIdentity(stdout)).toEqual(identity);
});
test.each([
['empty output', ''],
['non-JSON output', 'Python 3.13.7'],
['a JSON scalar', '3.13'],
['an invalid version', JSON.stringify({ ...identity, version: '3.13' })],
...['executable', 'baseExecutable', 'version', 'extSuffix', 'osRelease'].map((field) => [
`a missing ${field}`,
JSON.stringify({ ...identity, [field]: undefined }),
]),
])('throws on %s', (_, stdout) => {
expect(() => parsePythonIdentity(stdout)).toThrow('Unexpected Python identity output');
});
});
describe('virtualenvBaseKey', () => {
test('matches the expected key', () => {
expect(virtualenvBaseKey(identity, virtualenvPath)).toBe('trellis-virtualenv-python3.13.7-a3f9877607a58023');
});
test('is stable for the same interpreter and path', () => {
expect(virtualenvBaseKey({ ...identity }, virtualenvPath)).toEqual(virtualenvBaseKey(identity, virtualenvPath));
});
test.each([
['Python patch release', { version: '3.13.8', executable: '/opt/hostedtoolcache/Python/3.13.8/x64/bin/python3.13' }],
['resolved interpreter path', { executable: '/usr/bin/python3.13' }],
['unresolved interpreter path', { baseExecutable: '/usr/local/bin/python3' }],
['ABI', { extSuffix: '.cpython-313t-x86_64-linux-gnu.so' }],
['architecture', { extSuffix: '.cpython-313-aarch64-linux-gnu.so' }],
['OS release', { osRelease: 'ubuntu-22.04' }],
])('changes with the %s', (_, change) => {
expect(virtualenvBaseKey({ ...identity, ...change }, virtualenvPath)).not.toEqual(virtualenvBaseKey(identity, virtualenvPath));
});
test('changes with the virtualenv path', () => {
expect(virtualenvBaseKey(identity, '/home/runner/_work/site/site/trellis/.trellis/virtualenv')).not.toEqual(virtualenvBaseKey(identity, virtualenvPath));
});
});
describe('virtualenvMatchesInterpreter', () => {
const pyvenvCfg = [
'home = /opt/hostedtoolcache/Python/3.13.7/x64/bin',
'include-system-site-packages = false',
'version = 3.13.7',
'executable = /opt/hostedtoolcache/Python/3.13.7/x64/bin/python3.13',
'command = /opt/hostedtoolcache/Python/3.13.7/x64/bin/python3 -m venv /home/runner/work/site/site/trellis/.trellis/virtualenv',
].join('\n');
test('accepts a virtualenv built by the interpreter', () => {
expect(virtualenvMatchesInterpreter(pyvenvCfg, identity)).toBe(true);
});
test('accepts a pyvenv.cfg without an executable line', () => {
expect(virtualenvMatchesInterpreter('home = /opt/hostedtoolcache/Python/3.13.7/x64/bin\n', identity)).toBe(true);
});
test('rejects a virtualenv built by another interpreter', () => {
expect(virtualenvMatchesInterpreter(pyvenvCfg.replaceAll('3.13.7', '3.13.6'), identity)).toBe(false);
});
test('rejects a virtualenv linked through another path to the same binary', () => {
expect(virtualenvMatchesInterpreter(pyvenvCfg, { ...identity, baseExecutable: '/usr/local/bin/python3' })).toBe(false);
});
test('rejects a missing pyvenv.cfg', () => {
expect(virtualenvMatchesInterpreter('', identity)).toBe(false);
});
test.skipIf(!hasPython3)('accepts a virtualenv built by the real python3', async (context) => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'venv-'));
try {
execFileSync('python3', ['-m', 'venv', '--without-pip', path.join(dir, 'venv')]);
} catch {
context.skip();
}
const realIdentity = await getPythonIdentity();
expect(virtualenvMatchesInterpreter(fs.readFileSync(path.join(dir, 'venv', 'pyvenv.cfg'), 'utf8'), realIdentity)).toBe(true);
});
});