Skip to content

Commit 5ee0908

Browse files
committed
test: cover the SEA VFS compile cache error paths
A module that does not compile, including TypeScript that type stripping does not support, is left out of the cache and the executable still runs; a vfsArchive that cannot be parsed, a main script that does not compile and a build environment without a usable temporary directory fail the build without writing the blob. Signed-off-by: Matteo Collina <hello@matteocollina.com>
1 parent 8edf13e commit 5ee0908

1 file changed

Lines changed: 227 additions & 0 deletions

File tree

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
'use strict';
2+
3+
// This tests the error conditions of generating the compile cache of the
4+
// virtual file system when "useCodeCache" is combined with "useVfs": a module
5+
// that does not compile is left out of the cache, while a broken archive, a
6+
// broken main script or a build environment without a usable temporary
7+
// directory fail the build.
8+
9+
const common = require('../common');
10+
11+
const {
12+
buildSEA,
13+
skipIfBuildSEAIsNotSupported,
14+
} = require('../common/sea');
15+
16+
skipIfBuildSEAIsNotSupported();
17+
18+
const assert = require('assert');
19+
const fs = require('fs');
20+
const tmpdir = require('../common/tmpdir');
21+
const fixtures = require('../common/fixtures');
22+
const { spawnSyncAndAssert } = require('../common/child_process');
23+
24+
const kCompileCachePrefix = 'node:sea:compile-cache/';
25+
const kBlob = 'sea-prep.blob';
26+
27+
const hasTypeScript = process.features.typescript === 'strip';
28+
29+
function countCompileCacheEntries(blobPath) {
30+
const blob = fs.readFileSync(blobPath).toString('latin1');
31+
return blob.split(kCompileCachePrefix).length - 1;
32+
}
33+
34+
function writeConfig(name, config) {
35+
fs.writeFileSync(tmpdir.resolve(name), JSON.stringify(config));
36+
}
37+
38+
// Generates the preparation blob of the configuration in the working
39+
// directory and returns the child process result.
40+
function generateBlob(configName, env, expectations) {
41+
return spawnSyncAndAssert(
42+
process.execPath,
43+
['--experimental-sea-config', configName],
44+
{
45+
cwd: tmpdir.path,
46+
env: {
47+
...process.env,
48+
NODE_DEBUG: 'sea',
49+
NODE_DEBUG_NATIVE: 'SEA',
50+
...env,
51+
},
52+
},
53+
expectations);
54+
}
55+
56+
// Test: a module that does not compile is skipped, the rest is cached, and
57+
// the executable runs with the broken module failing when it is loaded.
58+
{
59+
tmpdir.refresh();
60+
fs.writeFileSync(tmpdir.resolve('main.js'), `
61+
const path = require('path');
62+
try {
63+
require(path.join(__dirname, 'modules', 'broken.js'));
64+
} catch (err) {
65+
console.log('broken:', err.name);
66+
}
67+
console.log('good:', require(path.join(__dirname, 'modules', 'good.js')));
68+
if (process.features.typescript === 'strip') {
69+
console.log('ts:', require(path.join(__dirname, 'modules', 'good.ts')));
70+
}
71+
`);
72+
fs.writeFileSync(tmpdir.resolve('good.js'), 'module.exports = 42;\n');
73+
fs.writeFileSync(tmpdir.resolve('broken.js'), 'module.exports = {\n');
74+
const assets = {
75+
'modules/good.js': 'good.js',
76+
'modules/broken.js': 'broken.js',
77+
};
78+
// main.js, good.js and, with type stripping, good.ts, which has two
79+
// entries: the stripped source and the code cache compiled from it.
80+
let modules = 2;
81+
let entries = 2;
82+
if (hasTypeScript) {
83+
fs.writeFileSync(tmpdir.resolve('good.ts'),
84+
'const n: number = 42;\nmodule.exports = n;\n');
85+
// Enums are not supported by type stripping.
86+
fs.writeFileSync(tmpdir.resolve('enum.ts'), 'enum E { A }\n');
87+
assets['modules/good.ts'] = 'good.ts';
88+
assets['modules/enum.ts'] = 'enum.ts';
89+
modules++;
90+
entries += 2;
91+
}
92+
const config = {
93+
main: 'main.js',
94+
output: kBlob,
95+
useVfs: true,
96+
useCodeCache: true,
97+
assets,
98+
};
99+
writeConfig('sea-config-blob.json', config);
100+
writeConfig('sea-config.json', { ...config, output: 'sea' });
101+
102+
generateBlob('sea-config-blob.json', {}, {
103+
stderr: common.mustCall((stderr) => {
104+
assert.match(stderr, /not caching \S*broken\.js: /);
105+
if (hasTypeScript) {
106+
assert.match(stderr, /not caching \S*enum\.ts: /);
107+
}
108+
assert.match(stderr, new RegExp(
109+
`Generated ${entries} compile cache entries for ${modules} modules`));
110+
}),
111+
});
112+
assert.strictEqual(countCompileCacheEntries(tmpdir.resolve(kBlob)), entries);
113+
114+
const outputFile = buildSEA(tmpdir.path);
115+
spawnSyncAndAssert(
116+
outputFile,
117+
{
118+
env: {
119+
...process.env,
120+
NODE_DEBUG: undefined,
121+
NODE_DEBUG_NATIVE: undefined,
122+
},
123+
},
124+
{
125+
stdout: common.mustCall((stdout) => {
126+
assert.match(stdout, /^broken: SyntaxError$/m);
127+
assert.match(stdout, /^good: 42$/m);
128+
if (hasTypeScript) {
129+
assert.match(stdout, /^ts: 42$/m);
130+
}
131+
}),
132+
stderr: common.mustCall((stderr) => {
133+
assert.doesNotMatch(stderr, /Code cache data rejected/);
134+
}),
135+
});
136+
}
137+
138+
// Test: an ES module that does not compile is skipped too.
139+
{
140+
tmpdir.refresh();
141+
fs.cpSync(fixtures.path('sea', 'vfs-esm'), tmpdir.path, { recursive: true });
142+
fs.writeFileSync(tmpdir.resolve('broken.mjs'), 'export {\n');
143+
const config = JSON.parse(
144+
fs.readFileSync(tmpdir.resolve('sea-config-code-cache.json'), 'utf8'));
145+
config.assets['modules/broken.mjs'] = 'broken.mjs';
146+
writeConfig('sea-config-code-cache.json', config);
147+
148+
generateBlob('sea-config-code-cache.json', {}, {
149+
stderr: common.mustCall((stderr) => {
150+
assert.match(stderr, /not caching \S*broken\.mjs: /);
151+
assert.match(stderr, /Generated 4 compile cache entries for 4 modules/);
152+
}),
153+
});
154+
assert.strictEqual(countCompileCacheEntries(tmpdir.resolve(kBlob)), 4);
155+
}
156+
157+
// Test: a "vfsArchive" that passes the signature check but cannot be parsed
158+
// fails the build when the compile cache is generated from it.
159+
{
160+
tmpdir.refresh();
161+
fs.writeFileSync(tmpdir.resolve('main.js'), 'console.log("hello");\n');
162+
fs.writeFileSync(tmpdir.resolve('assets.zip'),
163+
Buffer.concat([Buffer.from('PK\x03\x04'), Buffer.alloc(64)]));
164+
writeConfig('sea-config.json', {
165+
main: 'main.js',
166+
output: kBlob,
167+
useVfs: true,
168+
useCodeCache: true,
169+
vfsArchive: 'assets.zip',
170+
});
171+
172+
generateBlob('sea-config.json', {}, {
173+
status: 1,
174+
stderr: common.mustCall((stderr) => {
175+
assert.match(stderr, /ERR_ZIP_INVALID_ARCHIVE/);
176+
assert.match(
177+
stderr, /Cannot generate the compile cache of the virtual file system/);
178+
}),
179+
});
180+
assert.strictEqual(fs.existsSync(tmpdir.resolve(kBlob)), false);
181+
}
182+
183+
// Test: a main script that does not compile fails the build.
184+
{
185+
tmpdir.refresh();
186+
fs.writeFileSync(tmpdir.resolve('main.js'), 'console.log(\n');
187+
writeConfig('sea-config.json', {
188+
main: 'main.js',
189+
output: kBlob,
190+
useVfs: true,
191+
useCodeCache: true,
192+
});
193+
194+
generateBlob('sea-config.json', {}, {
195+
status: 1,
196+
stderr: /Cannot generate V8 code cache/,
197+
});
198+
assert.strictEqual(fs.existsSync(tmpdir.resolve(kBlob)), false);
199+
}
200+
201+
// Test: the compile cache is collected through a temporary directory, so the
202+
// build fails without a usable one.
203+
{
204+
tmpdir.refresh();
205+
fs.writeFileSync(tmpdir.resolve('main.js'), 'console.log("hello");\n');
206+
writeConfig('sea-config.json', {
207+
main: 'main.js',
208+
output: kBlob,
209+
useVfs: true,
210+
useCodeCache: true,
211+
});
212+
213+
const missing = tmpdir.resolve('missing');
214+
generateBlob('sea-config.json', {
215+
TMPDIR: missing,
216+
TMP: missing,
217+
TEMP: missing,
218+
}, {
219+
status: 1,
220+
stderr: common.mustCall((stderr) => {
221+
assert.match(stderr, /ENOENT/);
222+
assert.match(
223+
stderr, /Cannot generate the compile cache of the virtual file system/);
224+
}),
225+
});
226+
assert.strictEqual(fs.existsSync(tmpdir.resolve(kBlob)), false);
227+
}

0 commit comments

Comments
 (0)