Skip to content

Commit f154afa

Browse files
Merge branch 'main' into test-timeouts
2 parents a2006d0 + 539c04c commit f154afa

11 files changed

Lines changed: 138 additions & 12 deletions

File tree

.vscode/extensions.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"recommendations": [
33
"dbaeumer.vscode-eslint",
4+
"hbenl.vscode-mocha-test-adapter"
45
]
56
}

.vscode/settings.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,21 @@
33
"search.exclude": {
44
"dist": true
55
},
6-
"typescript.tsc.autoDetect": "off",
6+
"js/ts.tsc.autoDetect": "off",
77
"eslint.options": {
88
"rulePaths": [
99
"./build/eslint"
1010
]
1111
},
12-
"mochaExplorer.files": "test/**/*.test.ts",
12+
"mochaExplorer.files": "src/test/**/*.test.ts",
1313
"mochaExplorer.require": "ts-node/register",
1414
"mochaExplorer.env": {
1515
"TS_NODE_PROJECT": "src/test/tsconfig.json"
1616
},
1717
"files.associations": {
1818
"devcontainer-features.json": "jsonc"
1919
},
20-
"typescript.tsdk": "node_modules/typescript/lib",
20+
"js/ts.tsdk.path": "node_modules/typescript/lib",
2121
"git.branchProtection": [
2222
"main",
2323
"release/*"

src/spec-configuration/lockfile.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,24 @@ export async function writeLockfile(params: ContainerFeatureInternalParams, conf
5353
return;
5454
}
5555

56-
const newLockfileContentString = JSON.stringify(lockfile, null, 2);
56+
// Trailing newline per POSIX convention
57+
const newLockfileContentString = JSON.stringify(lockfile, null, 2) + '\n';
5758
const newLockfileContent = Buffer.from(newLockfileContentString);
5859
if (params.experimentalFrozenLockfile && !oldLockfileContent) {
5960
throw new Error('Lockfile does not exist.');
6061
}
61-
if (!oldLockfileContent || !newLockfileContent.equals(oldLockfileContent)) {
62+
// Normalize the existing lockfile through JSON.parse -> JSON.stringify to produce
63+
// the same canonical format as newLockfileContentString, so that the string comparison
64+
// below ignores cosmetic differences (indentation, trailing whitespace, etc.).
65+
let oldLockfileNormalized: string | undefined;
66+
if (oldLockfileContent) {
67+
try {
68+
oldLockfileNormalized = JSON.stringify(JSON.parse(oldLockfileContent.toString()), null, 2) + '\n';
69+
} catch {
70+
// Empty or invalid JSON; treat as needing rewrite.
71+
}
72+
}
73+
if (!oldLockfileNormalized || oldLockfileNormalized !== newLockfileContentString) {
6274
if (params.experimentalFrozenLockfile) {
6375
throw new Error('Lockfile does not match.');
6476
}

src/test/container-features/configs/lockfile-dependson/expected.devcontainer-lock.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,4 @@
2727
]
2828
}
2929
}
30-
}
30+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"image": "mcr.microsoft.com/devcontainers/base:ubuntu",
3+
"features": {
4+
"ghcr.io/codspace/features/flower:1": {},
5+
"ghcr.io/codspace/features/color:1": {}
6+
}
7+
}

src/test/container-features/configs/lockfile-frozen/.devcontainer-lock.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,4 @@
1111
"integrity": "sha256:c9cc1ac636b9ef595512b5ca7ecb3a35b7d3499cb6f86372edec76ae0cd71d43"
1212
}
1313
}
14-
}
14+
}

src/test/container-features/configs/lockfile-outdated/expected.devcontainer-lock.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,4 @@
1111
"integrity": "sha256:c9cc1ac636b9ef595512b5ca7ecb3a35b7d3499cb6f86372edec76ae0cd71d43"
1212
}
1313
}
14-
}
14+
}

src/test/container-features/configs/lockfile-upgrade-command/upgraded.devcontainer-lock.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,4 @@
2121
"integrity": "sha256:9024deeca80347dea7603a3bb5b4951988f0bf5894ba036a6ee3f29c025692c6"
2222
}
2323
}
24-
}
24+
}

src/test/container-features/configs/lockfile/expected.devcontainer-lock.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,4 @@
1616
"integrity": "sha256:41607bd6aba3975adcd0641cc479e67b04abd21763ba8a41ea053bcc04a6a818"
1717
}
1818
}
19-
}
19+
}

src/test/container-features/lockfile.test.ts

Lines changed: 103 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import * as assert from 'assert';
77
import * as path from 'path';
88
import * as semver from 'semver';
99
import { shellExec } from '../testUtils';
10-
import { cpLocal, readLocalFile, rmLocal } from '../../spec-utils/pfs';
10+
import { cpLocal, readLocalFile, rmLocal, writeLocalFile } from '../../spec-utils/pfs';
1111

1212
const pkg = require('../../../package.json');
1313

@@ -279,6 +279,44 @@ describe('Lockfile', function () {
279279
}
280280
});
281281

282+
it('lockfile ends with trailing newline', async () => {
283+
const workspaceFolder = path.join(__dirname, 'configs/lockfile');
284+
285+
const lockfilePath = path.join(workspaceFolder, '.devcontainer-lock.json');
286+
await rmLocal(lockfilePath, { force: true });
287+
288+
const res = await shellExec(`${cli} build --workspace-folder ${workspaceFolder} --experimental-lockfile`);
289+
const response = JSON.parse(res.stdout);
290+
assert.equal(response.outcome, 'success');
291+
const actual = (await readLocalFile(lockfilePath)).toString();
292+
assert.ok(actual.endsWith('\n'), 'Lockfile should end with a trailing newline');
293+
});
294+
295+
it('frozen lockfile matches despite formatting differences', async () => {
296+
const workspaceFolder = path.join(__dirname, 'configs/lockfile-frozen');
297+
const lockfilePath = path.join(workspaceFolder, '.devcontainer-lock.json');
298+
299+
// Read the existing lockfile, strip trailing newline to create a byte-different but semantically identical file
300+
const original = (await readLocalFile(lockfilePath)).toString();
301+
const stripped = original.replace(/\n$/, '');
302+
assert.notEqual(original, stripped, 'Test setup: should have removed trailing newline');
303+
assert.deepEqual(JSON.parse(original), JSON.parse(stripped), 'Test setup: JSON content should be identical');
304+
305+
try {
306+
await writeLocalFile(lockfilePath, Buffer.from(stripped));
307+
308+
// Frozen lockfile should succeed because JSON content is the same
309+
const res = await shellExec(`${cli} build --workspace-folder ${workspaceFolder} --experimental-lockfile --experimental-frozen-lockfile`);
310+
const response = JSON.parse(res.stdout);
311+
assert.equal(response.outcome, 'success', 'Frozen lockfile should not fail when only formatting differs');
312+
const actual = (await readLocalFile(lockfilePath)).toString();
313+
assert.strictEqual(actual, stripped, 'Frozen lockfile should remain unchanged when only formatting differs');
314+
} finally {
315+
// Restore original lockfile
316+
await writeLocalFile(lockfilePath, Buffer.from(original));
317+
}
318+
});
319+
282320
it('upgrade command should work with default workspace folder', async () => {
283321
const workspaceFolder = path.join(__dirname, 'configs/lockfile-upgrade-command');
284322
const absoluteTmpPath = path.resolve(__dirname, 'tmp');
@@ -298,4 +336,68 @@ describe('Lockfile', function () {
298336
process.chdir(originalCwd);
299337
}
300338
});
339+
340+
it('frozen lockfile fails when lockfile does not exist', async () => {
341+
const workspaceFolder = path.join(__dirname, 'configs/lockfile-frozen-no-lockfile');
342+
const lockfilePath = path.join(workspaceFolder, '.devcontainer-lock.json');
343+
await rmLocal(lockfilePath, { force: true });
344+
345+
try {
346+
throw await shellExec(`${cli} build --workspace-folder ${workspaceFolder} --experimental-lockfile --experimental-frozen-lockfile`);
347+
} catch (res) {
348+
const response = JSON.parse(res.stdout);
349+
assert.equal(response.outcome, 'error');
350+
assert.equal(response.message, 'Lockfile does not exist.');
351+
}
352+
});
353+
354+
it('corrupt lockfile causes build error', async () => {
355+
const workspaceFolder = path.join(__dirname, 'configs/lockfile');
356+
const lockfilePath = path.join(workspaceFolder, '.devcontainer-lock.json');
357+
const expectedPath = path.join(workspaceFolder, 'expected.devcontainer-lock.json');
358+
359+
try {
360+
// Write invalid JSON to the lockfile
361+
await writeLocalFile(lockfilePath, Buffer.from('this is not valid json{{{'));
362+
363+
try {
364+
throw await shellExec(`${cli} build --workspace-folder ${workspaceFolder} --experimental-lockfile`);
365+
} catch (res) {
366+
const response = JSON.parse(res.stdout);
367+
assert.equal(response.outcome, 'error');
368+
}
369+
} finally {
370+
// Restore from the known-good expected lockfile
371+
await cpLocal(expectedPath, lockfilePath);
372+
}
373+
});
374+
375+
it('no lockfile flags and no existing lockfile is a no-op', async () => {
376+
const workspaceFolder = path.join(__dirname, 'configs/lockfile');
377+
const lockfilePath = path.join(workspaceFolder, '.devcontainer-lock.json');
378+
const expectedPath = path.join(workspaceFolder, 'expected.devcontainer-lock.json');
379+
380+
try {
381+
await rmLocal(lockfilePath, { force: true });
382+
383+
// Build without any lockfile flags
384+
const res = await shellExec(`${cli} build --workspace-folder ${workspaceFolder}`);
385+
const response = JSON.parse(res.stdout);
386+
assert.equal(response.outcome, 'success');
387+
388+
// Lockfile should not have been created
389+
let exists = true;
390+
await readLocalFile(lockfilePath).catch(err => {
391+
if (err?.code === 'ENOENT') {
392+
exists = false;
393+
} else {
394+
throw err;
395+
}
396+
});
397+
assert.equal(exists, false, 'Lockfile should not be created when no lockfile flags are set');
398+
} finally {
399+
// Restore from the known-good expected lockfile
400+
await cpLocal(expectedPath, lockfilePath);
401+
}
402+
});
301403
});

0 commit comments

Comments
 (0)