Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/rust-port-convergence.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ jobs:
- name: Command matrix drift check
run: node build/generate-command-matrix.js --check

- name: Generated parity artifact checks
run: make parity-artifact-tests check-parity-inventory check-upstream-test-coverage

- name: Schema drift check
run: node build/check-spec-drift.js

Expand Down
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
tap-check \
homebrew-distribution-check \
npm-publish-workflow-check \
parity-artifact-tests \
check-parity-inventory \
check-cli-metadata \
check-compatibility-dashboard \
Expand All @@ -44,7 +45,7 @@ ACTIONLINT := uv tool run --from actionlint-py actionlint
SHELLCHECK := uv tool run --from shellcheck-py shellcheck
SHELLCHECK_FILES := $(shell git ls-files -- '*.sh' '.githooks/pre-commit' ':(exclude)upstream/**' ':(exclude)spec/**' ':(exclude)target/**' ':(exclude)node_modules/**')

tests: rust-fmt rust-tests rust-clippy rust-check rust-doc rust-coverage cargo-deny-check actionlint-check shellcheck build-release standalone-artifact-smoke pypi-wheel-smoke native-only-startup-contract acceptance-fixtures-check check-upstream-submodule command-matrix-drift-check check-cli-reference schema-drift-check parity-harness no-node-runtime npm-wrapper-check npm-publish-script-check npm-package-smoke artifact-smoke-workflow-check tap-check homebrew-distribution-check npm-publish-workflow-check check-parity-inventory check-cli-metadata check-compatibility-dashboard check-upstream-test-coverage check-devcontainer-config devcontainer-provision-smoke upstream-compatibility
tests: rust-fmt rust-tests rust-clippy rust-check rust-doc rust-coverage cargo-deny-check actionlint-check shellcheck build-release standalone-artifact-smoke pypi-wheel-smoke native-only-startup-contract acceptance-fixtures-check check-upstream-submodule command-matrix-drift-check check-cli-reference schema-drift-check parity-harness no-node-runtime npm-wrapper-check npm-publish-script-check npm-package-smoke artifact-smoke-workflow-check tap-check homebrew-distribution-check npm-publish-workflow-check parity-artifact-tests check-parity-inventory check-cli-metadata check-compatibility-dashboard check-upstream-test-coverage check-devcontainer-config devcontainer-provision-smoke upstream-compatibility

rust-fmt:
cargo fmt --manifest-path $(RUST_MANIFEST) --all -- --check
Expand Down Expand Up @@ -136,6 +137,9 @@ homebrew-distribution-check:
npm-publish-workflow-check:
node build/check-npm-publish-workflow.js

parity-artifact-tests:
node --test build/test-parity-artifacts.js

check-parity-inventory:
node build/generate-parity-inventory.js --check

Expand Down
15 changes: 14 additions & 1 deletion build/check-upstream-test-coverage.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,16 @@ function validateCoverageMap(report) {
fail(`Coverage map entry ${suite.upstreamTest} must list native tests for status ${suite.status}.`);
}

if (suite.unportedScenarios !== undefined) {
if (suite.status !== 'partial') {
fail(`Coverage map entry ${suite.upstreamTest} may only enumerate unported scenarios when marked partial.`);
}
if (!Array.isArray(suite.unportedScenarios) || suite.unportedScenarios.length === 0
|| suite.unportedScenarios.some(scenario => typeof scenario !== 'string' || !scenario.trim())) {
fail(`Coverage map entry ${suite.upstreamTest} must contain a non-empty unportedScenarios string array.`);
}
}

for (const nativeTest of suite.nativeTests) {
const absoluteNativePath = path.join(repositoryRoot, nativeTest);
if (!fs.existsSync(absoluteNativePath)) {
Expand Down Expand Up @@ -132,8 +142,11 @@ function renderMarkdown(report) {
];

for (const suite of [...report.suites].sort((left, right) => left.upstreamTest.localeCompare(right.upstreamTest))) {
const unportedScenarios = suite.unportedScenarios?.length
? ` Still unported: ${suite.unportedScenarios.join('; ')}.`
: '';
lines.push(
`| \`${suite.upstreamTest}\` | ${suite.status} | ${suite.nativeTests.length ? suite.nativeTests.map(test => `\`${test}\``).join('<br>') : 'none'} | ${suite.notes || ''} |`
`| \`${suite.upstreamTest}\` | ${suite.status} | ${suite.nativeTests.length ? suite.nativeTests.map(test => `\`${test}\``).join('<br>') : 'none'} | ${suite.notes || ''}${unportedScenarios} |`
);
}

Expand Down
4 changes: 4 additions & 0 deletions build/generate-cli-reference.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ function renderReference(matrix) {
`- Upstream commit: \`${matrix.upstreamCommit}\``,
`- Source: \`${matrix.sourcePath}\``,
'',
'## Global Options',
'',
renderOptions(matrix.globalOptions || []),
'',
'## Top-Level Commands',
'',
'| Command | Description |',
Expand Down
17 changes: 17 additions & 0 deletions build/generate-command-matrix.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,22 @@ function parseCommandBlock(source) {
return source.slice(start, end);
}

function parseGlobalOptionNames(source) {
const start = source.indexOf('const y = yargs(');
const end = source.indexOf("y.command('up'");
if (start === -1 || end === -1 || end <= start) {
throw new Error(`Unable to locate CLI global options in ${path.relative(repositoryRoot, upstreamCliPath)}.`);
}
const names = new Set();
const optionRegex = /\.option\('([^']+)'/g;
let match;
const block = source.slice(start, end);
while ((match = optionRegex.exec(block)) !== null) {
names.add(match[1]);
}
return Array.from(names).sort();
}

function parseCommands(commandBlock) {
const lines = commandBlock.split('\n');
const commands = [];
Expand Down Expand Up @@ -166,6 +182,7 @@ function generateCommandMatrix() {
return {
upstreamCommit: runGit(['rev-parse', 'HEAD:upstream']),
sourcePath: path.relative(repositoryRoot, upstreamCliPath),
globalOptions: parseGlobalOptionNames(source),
topLevel,
commands,
allCommandPaths: commands.map(command => command.path),
Expand Down
35 changes: 32 additions & 3 deletions build/generate-parity-inventory.js
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,10 @@ function isTestOnlyPath(relativePath) {
return path.basename(relativePath) === 'tests.rs' || pathSegments.includes('tests');
}

function isNativeImplementationPath(relativePath) {
return relativePath.endsWith('.rs') && !isTestOnlyPath(relativePath);
}

function stripCfgTestBlocks(source) {
let stripped = '';
let cursor = 0;
Expand Down Expand Up @@ -381,7 +385,7 @@ function commandSourceFiles(commandPath) {
}
return configuredPaths
.flatMap(relativePath => walkFiles(relativePath))
.filter(relativePath => !isTestOnlyPath(relativePath));
.filter(isNativeImplementationPath);
}

function optionEvidence(commandPath, optionName) {
Expand All @@ -394,6 +398,14 @@ function optionEvidence(commandPath, optionName) {
.sort();
}

function globalOptionEvidence(optionName) {
const needle = `--${optionName}`;
return walkFiles('cmd/devcontainer/src')
.filter(isNativeImplementationPath)
.filter(relativePath => readSourceForEvidence(relativePath).includes(needle))
.sort();
}

function declaredTopLevelCommand(command) {
const cliSource = fs.readFileSync(cliSourcePath, 'utf8');
const commandsMod = fs.readFileSync(commandsModPath, 'utf8');
Expand All @@ -419,6 +431,14 @@ function commandDeclared(pathValue) {

function buildInventory() {
const matrix = JSON.parse(fs.readFileSync(commandMatrixPath, 'utf8'));
const globalOptions = (matrix.globalOptions || []).map(optionName => {
const evidence = globalOptionEvidence(optionName);
return {
name: optionName,
sourceReferenced: evidence.length > 0,
evidence,
};
});
const inventory = matrix.commands.map(command => {
const declared = commandDeclared(command.path);
const options = command.options.map(optionName => {
Expand All @@ -445,8 +465,10 @@ function buildInventory() {
};
});

const totalOptions = inventory.reduce((sum, command) => sum + command.optionSummary.total, 0);
const referencedOptions = inventory.reduce((sum, command) => sum + command.optionSummary.referenced, 0);
const totalOptions = globalOptions.length
+ inventory.reduce((sum, command) => sum + command.optionSummary.total, 0);
const referencedOptions = globalOptions.filter(option => option.sourceReferenced).length
+ inventory.reduce((sum, command) => sum + command.optionSummary.referenced, 0);
return {
upstreamCommit: matrix.upstreamCommit,
sourcePath: matrix.sourcePath,
Expand All @@ -457,6 +479,7 @@ function buildInventory() {
optionsReferenced: referencedOptions,
optionsMissing: totalOptions - referencedOptions,
},
globalOptions,
commands: inventory,
};
}
Expand All @@ -474,6 +497,12 @@ function renderMarkdown(report) {
'',
'This report is a static inventory, not a semantic parity proof. A referenced option can still be only partially implemented, and command-level known gaps are called out explicitly below.',
'',
'## Global options',
'',
...(report.globalOptions || []).map(option =>
`- \`--${option.name}\`: ${option.sourceReferenced ? 'referenced' : 'missing'}${option.evidence.length ? ` (${option.evidence.map(value => `\`${value}\``).join(', ')})` : ''}`
),
'',
'## Summary',
'',
'| Command | Declared | Option refs | Missing refs | Known gaps |',
Expand Down
41 changes: 41 additions & 0 deletions build/test-parity-artifacts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) devcontainer-rs contributors.
* Licensed under the MIT License.
*--------------------------------------------------------------------------------------------*/

'use strict';

const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const test = require('node:test');

const { buildInventory } = require('./generate-parity-inventory');

const repositoryRoot = path.join(__dirname, '..');
const coverageMapPath = path.join(repositoryRoot, 'docs', 'upstream', 'test-coverage-map.json');

test('generated CLI metadata is not accepted as native option evidence', () => {
const inventory = buildInventory();
const evidence = [
...inventory.globalOptions.flatMap(option => option.evidence),
...inventory.commands.flatMap(command => command.options.flatMap(option => option.evidence)),
];

assert.ok(!evidence.includes('cmd/devcontainer/src/cli_metadata.json'));
});

test('partial OCI and Feature configuration mappings enumerate unported scenarios', () => {
const report = JSON.parse(fs.readFileSync(coverageMapPath, 'utf8'));
for (const upstreamTest of [
'upstream/src/test/container-features/containerFeaturesOCI.test.ts',
'upstream/src/test/container-features/generateFeaturesConfig.test.ts',
]) {
const suite = report.suites.find(candidate => candidate.upstreamTest === upstreamTest);
assert.ok(suite, `missing coverage entry for ${upstreamTest}`);
assert.equal(suite.status, 'partial');
assert.ok(Array.isArray(suite.unportedScenarios));
assert.ok(suite.unportedScenarios.length > 0);
assert.ok(suite.unportedScenarios.every(scenario => typeof scenario === 'string' && scenario.length > 0));
}
});
Loading
Loading