Skip to content

Commit a348b3f

Browse files
committed
features resolve-dependencies and template apply test
1 parent bcb3652 commit a348b3f

File tree

2 files changed

+167
-0
lines changed

2 files changed

+167
-0
lines changed

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

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,119 @@ describe('CLI features subcommands', async function () {
425425
});
426426
});
427427

428+
describe('features resolve-dependencies', function () {
429+
430+
it('should resolve dependencies when workspace-folder defaults to current directory', async function () {
431+
// Create a test config with features that have dependencies
432+
const testConfigPath = path.resolve(__dirname, 'configs/feature-dependencies/dependsOn/oci-ab');
433+
const originalCwd = process.cwd();
434+
435+
try {
436+
// Change to test config directory to test default workspace folder behavior
437+
process.chdir(testConfigPath);
438+
439+
// Use absolute path to CLI to prevent npm ENOENT errors
440+
const absoluteTmpPath = path.resolve(originalCwd, tmp);
441+
const absoluteCliPath = `npx --prefix ${absoluteTmpPath} devcontainer`;
442+
443+
// First check if the config file exists
444+
const configExists = require('fs').existsSync('.devcontainer/devcontainer.json') ||
445+
require('fs').existsSync('.devcontainer.json');
446+
assert.isTrue(configExists, 'Test config file should exist');
447+
448+
let result;
449+
try {
450+
result = await shellExec(`${absoluteCliPath} features resolve-dependencies --log-level trace`);
451+
} catch (error: any) {
452+
// If command fails, log details for debugging
453+
console.error('Command failed:', error);
454+
if (error.stderr) {
455+
console.error('STDERR:', error.stderr);
456+
}
457+
if (error.stdout) {
458+
console.error('STDOUT:', error.stdout);
459+
}
460+
throw error;
461+
}
462+
463+
// Verify the command succeeded
464+
assert.isDefined(result);
465+
assert.isString(result.stdout);
466+
assert.isNotEmpty(result.stdout.trim(), 'Command should produce output');
467+
468+
// Parse the JSON output to verify it contains expected structure
469+
let jsonOutput;
470+
try {
471+
// Try parsing stdout directly first
472+
jsonOutput = JSON.parse(result.stdout.trim());
473+
} catch (parseError) {
474+
// If direct parsing fails, try extracting JSON from mixed output
475+
const lines = result.stdout.split('\n');
476+
477+
// Find the last occurrence of '{' that starts a complete JSON object
478+
let jsonStartIndex = -1;
479+
let jsonEndIndex = -1;
480+
let braceCount = 0;
481+
482+
// Work backwards from the end to find the complete JSON
483+
for (let i = lines.length - 1; i >= 0; i--) {
484+
const line = lines[i].trim();
485+
if (line === '}' && jsonEndIndex === -1) {
486+
jsonEndIndex = i;
487+
braceCount = 1;
488+
} else if (jsonEndIndex !== -1) {
489+
// Count braces to find matching opening
490+
for (const char of line) {
491+
if (char === '}') {
492+
braceCount++;
493+
} else if (char === '{') {
494+
braceCount--;
495+
}
496+
}
497+
if (braceCount === 0 && line === '{') {
498+
jsonStartIndex = i;
499+
break;
500+
}
501+
}
502+
}
503+
504+
if (jsonStartIndex >= 0 && jsonEndIndex >= 0) {
505+
// Extract just the JSON lines
506+
const jsonLines = lines.slice(jsonStartIndex, jsonEndIndex + 1);
507+
const jsonString = jsonLines.join('\n');
508+
try {
509+
jsonOutput = JSON.parse(jsonString);
510+
} catch (innerError) {
511+
console.error('Failed to parse extracted JSON:', jsonString.substring(0, 500) + '...');
512+
throw new Error(`Failed to parse extracted JSON: ${innerError}`);
513+
}
514+
} else {
515+
console.error('Could not find complete JSON in output');
516+
console.error('Last 10 lines:', lines.slice(-10));
517+
throw new Error(`Failed to find complete JSON in output: ${parseError}`);
518+
}
519+
}
520+
521+
assert.isDefined(jsonOutput, 'Should have valid JSON output');
522+
assert.property(jsonOutput, 'installOrder');
523+
assert.isArray(jsonOutput.installOrder);
524+
525+
// Verify the install order contains the expected features
526+
const installOrder = jsonOutput.installOrder;
527+
assert.isAbove(installOrder.length, 0, 'Install order should contain at least one feature');
528+
529+
// Each item should have id and options
530+
installOrder.forEach((item: any) => {
531+
assert.property(item, 'id');
532+
assert.property(item, 'options');
533+
});
534+
535+
} finally {
536+
process.chdir(originalCwd);
537+
}
538+
});
539+
});
540+
428541
describe('features package', function () {
429542

430543
it('features package subcommand by collection', async function () {

src/test/container-templates/templatesCLICommands.test.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,60 @@ describe('tests apply command', async function () {
6262
// Assert that the Feature included in the command was added.
6363
assert.match(file, /"ghcr.io\/devcontainers\/features\/azure-cli:1": {\n/);
6464
});
65+
66+
it('templates apply subcommand with default workspace folder', async function () {
67+
const testOutputPath = path.resolve(__dirname, 'tmp-default-workspace');
68+
const originalCwd = process.cwd();
69+
70+
try {
71+
// Create and change to test output directory to test default workspace folder behavior
72+
await shellExec(`rm -rf ${testOutputPath}`);
73+
await shellExec(`mkdir -p ${testOutputPath}`);
74+
process.chdir(testOutputPath);
75+
76+
// Use absolute path to CLI to prevent npm ENOENT errors
77+
const absoluteTmpPath = path.resolve(originalCwd, tmp);
78+
const absoluteCliPath = `npx --prefix ${absoluteTmpPath} devcontainer`;
79+
80+
let success = false;
81+
let result: ExecResult | undefined = undefined;
82+
83+
try {
84+
// Run without --workspace-folder to test default behavior
85+
result = await shellExec(`${absoluteCliPath} templates apply \
86+
--template-id ghcr.io/devcontainers/templates/docker-from-docker:latest \
87+
--template-args '{ "installZsh": "false", "upgradePackages": "true", "dockerVersion": "20.10", "moby": "true", "enableNonRootDocker": "true" }' \
88+
--log-level trace`);
89+
success = true;
90+
91+
} catch (error) {
92+
assert.fail('templates apply sub-command should not throw when using default workspace folder');
93+
}
94+
95+
assert.isTrue(success);
96+
assert.isDefined(result);
97+
assert.strictEqual(result.stdout.trim(), '{"files":["./.devcontainer/devcontainer.json"]}');
98+
99+
// Verify the file was created in the current working directory (default workspace folder)
100+
const file = (await readLocalFile(path.join(testOutputPath, '.devcontainer', 'devcontainer.json'))).toString();
101+
102+
assert.match(file, /"name": "Docker from Docker"/);
103+
assert.match(file, /"installZsh": "false"/);
104+
assert.match(file, /"upgradePackages": "true"/);
105+
assert.match(file, /"version": "20.10"/);
106+
assert.match(file, /"moby": "true"/);
107+
assert.match(file, /"enableNonRootDocker": "true"/);
108+
109+
// Assert that the Features included in the template were not removed.
110+
assert.match(file, /"ghcr.io\/devcontainers\/features\/common-utils:1": {\n/);
111+
assert.match(file, /"ghcr.io\/devcontainers\/features\/docker-from-docker:1": {\n/);
112+
113+
} finally {
114+
process.chdir(originalCwd);
115+
// Clean up test directory
116+
await shellExec(`rm -rf ${testOutputPath}`);
117+
}
118+
});
65119
});
66120

67121
describe('tests packageTemplates()', async function () {

0 commit comments

Comments
 (0)