Skip to content

Commit

Permalink
add features test ... command
Browse files Browse the repository at this point in the history
  • Loading branch information
joshspicer authored and chrmarti committed Jul 8, 2022
1 parent 6d18acf commit a367183
Show file tree
Hide file tree
Showing 8 changed files with 651 additions and 8 deletions.
10 changes: 10 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,16 @@
"dbaeumer.vscode-eslint",
"GitHub.vscode-pull-request-github"
]
},
"codespaces": {
"repositories": {
"devcontainers/features": {
"permissions": {
"contents": "write",
"workflows": "write"
}
}
}
}
}
}
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ This CLI is in active development. Current status:
- [x] `devcontainer exec` - Executes a command in a container with `userEnvProbe`, `remoteUser`, `remoteEnv`, and other properties applied
- [ ] `devcontainer stop` - Stops containers
- [ ] `devcontainer down` - Stops and deletes containers
- [ ] `devcontainer features <...>` - Tools to assist in authoring and testing [dev container 'features'](https://github.com/devcontainers/spec/blob/main/proposals/devcontainer-features.md)

## Try it out

Expand All @@ -40,6 +41,7 @@ Commands:
devcontainer build [path] Build a dev container image
devcontainer run-user-commands Run user commands
devcontainer read-configuration Read configuration
devcontainer features Features commands
devcontainer exec <cmd> [args..] Execute a command on a running dev container

Options:
Expand Down
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
},
"devDependencies": {
"@types/chai": "^4.3.0",
"@types/chalk": "^2.2.0",
"@types/follow-redirects": "^1.13.1",
"@types/js-yaml": "^4.0.5",
"@types/mocha": "^9.1.0",
Expand Down Expand Up @@ -66,10 +67,11 @@
"vinyl-fs": "^3.0.3"
},
"dependencies": {
"ncp": "^2.0.0",
"chalk": "^4",
"follow-redirects": "^1.14.8",
"js-yaml": "^4.1.0",
"jsonc-parser": "^3.0.0",
"ncp": "^2.0.0",
"node-pty": "^0.10.1",
"pull-stream": "^3.6.14",
"semver": "^7.3.5",
Expand All @@ -80,4 +82,4 @@
"vscode-uri": "^3.0.3",
"yargs": "~17.0.1"
}
}
}
8 changes: 5 additions & 3 deletions src/spec-node/devContainers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export interface ProvisionOptions {
remoteEnv: Record<string, string>;
additionalCacheFroms: string[];
useBuildKit: 'auto' | 'never';
omitLoggerHeader?: boolean | undefined;
buildxPlatform: string | undefined;
buildxPush: boolean;
}
Expand All @@ -55,6 +56,7 @@ export async function launch(options: ProvisionOptions, disposables: (() => Prom
const output = params.common.output;
const text = 'Resolving Remote';
const start = output.start(text);

const result = await resolve(params, options.configFile, options.overrideConfigFile, options.idLabels);
output.stop(text, start);
const { dockerContainerId, composeProjectName } = result;
Expand Down Expand Up @@ -82,7 +84,7 @@ export async function createDockerParams(options: ProvisionOptions, disposables:
const extensionPath = path.join(__dirname, '..', '..');
const sessionStart = new Date();
const pkg = await getPackageConfig(extensionPath);
const output = createLog(options, pkg, sessionStart, disposables);
const output = createLog(options, pkg, sessionStart, disposables, options.omitLoggerHeader);

const appRoot = undefined;
const cwd = options.workspaceFolder || process.cwd();
Expand Down Expand Up @@ -164,8 +166,8 @@ export interface LogOptions {
terminalDimensions: LogDimensions | undefined;
}

export function createLog(options: LogOptions, pkg: PackageConfiguration, sessionStart: Date, disposables: (() => Promise<unknown> | undefined)[]) {
const header = `${pkg.name} ${pkg.version}.`;
export function createLog(options: LogOptions, pkg: PackageConfiguration, sessionStart: Date, disposables: (() => Promise<unknown> | undefined)[], omitHeader?: boolean) {
const header = omitHeader ? undefined : `${pkg.name} ${pkg.version}.`;
const output = createLogFrom(options, sessionStart, header);
output.dimensions = options.terminalDimensions;
disposables.push(() => output.join());
Expand Down
90 changes: 87 additions & 3 deletions src/spec-node/devContainersSpecCLI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,11 @@ import { getDockerComposeFilePaths } from '../spec-configuration/configuration';
import { workspaceFromPath } from '../spec-utils/workspaces';
import { readDevContainerConfigFile } from './configContainer';
import { getDefaultDevContainerConfigPath, getDevContainerConfigPathIn, uriToFsPath } from '../spec-configuration/configurationCommonUtils';
import { getCLIHost } from '../spec-common/cliHost';
import { CLIHost, getCLIHost } from '../spec-common/cliHost';
import { loadNativeModule } from '../spec-common/commonUtils';
import { generateFeaturesConfig, getContainerFeaturesFolder } from '../spec-configuration/containerFeaturesConfiguration';
import { doFeaturesTestCommand } from './featuresCLI/testContainerFeatures';
import { PackageConfiguration } from '../spec-utils/product';

const defaultDefaultUserEnvProbe: UserEnvProbe = 'loginInteractiveShell';

Expand All @@ -47,6 +49,8 @@ const defaultDefaultUserEnvProbe: UserEnvProbe = 'loginInteractiveShell';
y.command('build [path]', 'Build a dev container image', buildOptions, buildHandler);
y.command('run-user-commands', 'Run user commands', runUserCommandsOptions, runUserCommandsHandler);
y.command('read-configuration', 'Read configuration', readConfigurationOptions, readConfigurationHandler);
y.command('features', 'Features commands', (y: Argv) =>
y.command('test', 'Test features', featuresTestOptions, featuresTestHandler));
y.command(restArgs ? ['exec', '*'] : ['exec <cmd> [args..]'], 'Execute a command on a running dev container', execOptions, execHandler);
y.parse(restArgs ? argv.slice(1) : argv);

Expand Down Expand Up @@ -705,7 +709,7 @@ function execOptions(y: Argv) {
});
}

type ExecArgs = UnpackArgv<ReturnType<typeof execOptions>>;
export type ExecArgs = UnpackArgv<ReturnType<typeof execOptions>>;

function execHandler(args: ExecArgs) {
(async () => exec(args))().catch(console.error);
Expand All @@ -719,7 +723,7 @@ async function exec(args: ExecArgs) {
process.exit(exitCode);
}

async function doExec({
export async function doExec({
'user-data-folder': persistedFolder,
'docker-path': dockerPath,
'docker-compose-path': dockerComposePath,
Expand Down Expand Up @@ -776,6 +780,7 @@ async function doExec({
remoteEnv: keyValuesToRecord(addRemoteEnvs),
additionalCacheFroms: [],
useBuildKit: 'auto',
omitLoggerHeader: true,
buildxPlatform: undefined,
buildxPush: false,
}, disposables);
Expand Down Expand Up @@ -827,6 +832,85 @@ async function doExec({
}
}

// -- 'features test' command
function featuresTestOptions(y: Argv) {
return y
.options({
'base-image': { type: 'string', alias: 'i', default: 'ubuntu:focal', description: 'Base Image' },
'features': { type: 'array', alias: 'f', describe: 'Feature(s) to test as space-separated parameters. Omit to auto-detect all features in collection directory. Cannot be combined with \'-s\'.', },
'scenarios': { type: 'string', alias: 's', description: 'Path to scenario test directory (containing scenarios.json). Cannot be combined with \'-f\'.' },
'remote-user': { type: 'string', alias: 'u', default: 'root', describe: 'Remote user', },
'collection-folder': { type: 'string', alias: 'c', default: '.', description: 'Path to folder containing \'src\' and \'test\' sub-folders.' },
'log-level': { choices: ['info' as 'info', 'debug' as 'debug', 'trace' as 'trace'], default: 'info' as 'info', description: 'Log level.' },
'quiet': { type: 'boolean', alias: 'q', default: false, description: 'Quiets output' },
})
.check(argv => {
if (argv['scenarios'] && argv['features']) {
throw new Error('Cannot combine --scenarios and --features');
}
return true;
});
}

export type FeaturesTestArgs = UnpackArgv<ReturnType<typeof featuresTestOptions>>;
export interface FeaturesTestCommandInput {
cliHost: CLIHost;
pkg: PackageConfiguration;
baseImage: string;
collectionFolder: string;
features?: string[];
scenariosFolder: string | undefined;
remoteUser: string;
quiet: boolean;
logLevel: LogLevel;
disposables: (() => Promise<unknown> | undefined)[];
}

function featuresTestHandler(args: FeaturesTestArgs) {
(async () => await featuresTest(args))().catch(console.error);
}

async function featuresTest({
'base-image': baseImage,
'collection-folder': collectionFolder,
features,
scenarios: scenariosFolder,
'remote-user': remoteUser,
quiet,
'log-level': inputLogLevel,
}: FeaturesTestArgs) {
const disposables: (() => Promise<unknown> | undefined)[] = [];
const dispose = async () => {
await Promise.all(disposables.map(d => d()));
};

const cwd = process.cwd();
const cliHost = await getCLIHost(cwd, loadNativeModule);
const extensionPath = path.join(__dirname, '..', '..');
const pkg = await getPackageConfig(extensionPath);

const logLevel = mapLogLevel(inputLogLevel);

const args: FeaturesTestCommandInput = {
baseImage,
cliHost,
logLevel,
quiet,
pkg,
collectionFolder: cliHost.path.resolve(collectionFolder),
features: features ? (Array.isArray(features) ? features as string[] : [features]) : undefined,
scenariosFolder: scenariosFolder ? cliHost.path.resolve(scenariosFolder) : undefined,
remoteUser,
disposables
};

const exitCode = await doFeaturesTestCommand(args);

await dispose();
process.exit(exitCode);
}
// -- End: 'features test' command

function keyValuesToRecord(keyValues: string[]): Record<string, string> {
return keyValues.reduce((envs, env) => {
const i = env.indexOf('=');
Expand Down
Loading

0 comments on commit a367183

Please sign in to comment.