From 6244c7ca95edd37dd145a98c24a0f26ed83f363e Mon Sep 17 00:00:00 2001 From: konard Date: Tue, 9 Sep 2025 21:20:08 +0300 Subject: [PATCH 1/4] Initial commit with task details for issue #34 Adding CLAUDE.md with task information for AI processing. This file will be removed when the task is complete. Issue: https://github.com/link-foundation/command-stream/issues/34 --- CLAUDE.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..2681fba5 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,5 @@ +Issue to solve: https://github.com/link-foundation/command-stream/issues/34 +Your prepared branch: issue-34-389ba67e +Your prepared working directory: /tmp/gh-issue-solver-1757442003757 + +Proceed. \ No newline at end of file From a50d9e858e60fc193c422edfdbf1948fa9bf4826 Mon Sep 17 00:00:00 2001 From: konard Date: Tue, 9 Sep 2025 21:20:27 +0300 Subject: [PATCH 2/4] Remove CLAUDE.md - PR created successfully --- CLAUDE.md | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 2681fba5..00000000 --- a/CLAUDE.md +++ /dev/null @@ -1,5 +0,0 @@ -Issue to solve: https://github.com/link-foundation/command-stream/issues/34 -Your prepared branch: issue-34-389ba67e -Your prepared working directory: /tmp/gh-issue-solver-1757442003757 - -Proceed. \ No newline at end of file From b107cfb0f0de40c39765771b1282e7e216a90937 Mon Sep 17 00:00:00 2001 From: konard Date: Tue, 9 Sep 2025 21:29:00 +0300 Subject: [PATCH 3/4] Implement command builder API for safe command construction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit πŸš€ Add $.command() builder API as requested in issue #34: - CommandBuilder class with fluent API - Safe argument escaping to prevent shell injection - Pipe configuration support: $.command.stdout(), $.command.exitCode() - Full feature set: stdin, env, cwd, capture, mirror options - Method chaining: .arg(), .env(), .cwd(), .run() - Direct access via CommandBuilder class and command() function βœ… Features: - Exactly matches issue example: $.command("cat", "./file").pipe($.command.stdout("inherit"), $.command.exitCode).run() - Automatic string conversion and shell escaping for safety - Integration with existing ProcessRunner infrastructure - Comprehensive test suite with 21 tests covering safety and functionality - Full documentation with examples and comparison table πŸ”§ Breaking: Minor version bump to 0.8.0 for new API Fixes #34 πŸ€– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- README.md | 63 ++++++++ examples/command-builder-demo.mjs | 51 +++++++ examples/test-command-builder-result.mjs | 13 ++ package.json | 2 +- src/$.mjs | 135 ++++++++++++++++- tests/command-builder.test.mjs | 175 +++++++++++++++++++++++ 6 files changed, 437 insertions(+), 2 deletions(-) create mode 100644 examples/command-builder-demo.mjs create mode 100644 examples/test-command-builder-result.mjs create mode 100644 tests/command-builder.test.mjs diff --git a/README.md b/README.md index fc45e260..b3949b00 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ A modern $ shell utility library with streaming, async iteration, and EventEmitt | **πŸ“ˆ Total Downloads** | **Growing** | **6B+** | **5.4B** | N/A (Built-in) | **596M** | **37M** | | **Runtime Support** | βœ… Bun + Node.js | βœ… Node.js | βœ… Node.js | 🟑 Bun only | βœ… Node.js | βœ… Node.js | | **Template Literals** | βœ… `` $`cmd` `` | βœ… `` $`cmd` `` | ❌ Function calls | βœ… `` $`cmd` `` | ❌ Function calls | βœ… `` $`cmd` `` | +| **Command Builder API** | βœ… **`$.command()`** injection-safe | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No | | **Real-time Streaming** | βœ… Live output | 🟑 Limited | ❌ Buffer only | ❌ Buffer only | ❌ Buffer only | ❌ Buffer only | | **Synchronous Execution** | βœ… `.sync()` with events | βœ… `execaSync` | βœ… `spawnSync` | ❌ No | βœ… Sync by default | ❌ No | | **Async Iteration** | βœ… `for await (chunk of $.stream())` | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No | @@ -252,6 +253,68 @@ await $prod`npm start`; await $prod`npm test`; ``` +### Command Builder API (πŸš€ NEW!) + +**Safe, injection-free command construction with fluent API:** + +```javascript +import { $ } from 'command-stream'; + +// Basic usage - exactly as requested in the issue +const result = await $.command("cat", "./some-file.txt").pipe( + $.command.stdout("inherit"), + $.command.exitCode +).run(); + +// Method chaining for configuration +const result2 = await $.command('echo', 'hello world') + .arg('extra', 'arguments') + .stdout('inherit') + .capture(true) + .env({ DEBUG: '1' }) + .cwd('/tmp') + .run(); + +// Safe argument handling - prevents shell injection +const userInput = "dangerous; rm -rf /"; +const safeResult = await $.command('echo', userInput).run(); +console.log(safeResult.stdout); // Outputs literal string, not executed + +// Environment and working directory +const envResult = await $.command('env') + .env({ MY_VAR: 'value', ANOTHER: 'test' }) + .run({ capture: true, mirror: false }); + +// stdin input +const stdinResult = await $.command('cat') + .stdin('Hello from stdin!') + .run({ capture: true, mirror: false }); + +// Complex argument escaping handled automatically +const complexArgs = await $.command('echo', 'file with spaces.txt', "quotes'and\"stuff") + .run({ capture: true, mirror: false }); + +// Direct access to CommandBuilder and command function +import { CommandBuilder, command } from 'command-stream'; + +const cmd = new CommandBuilder('ls', ['-la']); +const cmd2 = command('pwd'); // Factory function +``` + +**Key Features:** +- **πŸ›‘οΈ Injection-Safe**: All arguments are properly escaped automatically +- **πŸ”— Fluent API**: Method chaining for readable configuration +- **βš™οΈ Full Configuration**: stdin, stdout, stderr, env, cwd, capture, mirror +- **πŸ”§ Pipe Support**: Works with pipe configuration functions +- **βœ… Type Safety**: No shell parsing - direct argument passing +- **πŸ—οΈ Extensible**: Use CommandBuilder class directly for advanced cases + +**Why Use Command Builder?** +- **Security**: Eliminates shell injection vulnerabilities +- **Clarity**: Explicit arguments vs. string interpolation +- **Compatibility**: Similar to Rust's `std::process::Command` and Effect's `Command` +- **Reliability**: Arguments are passed exactly as intended + ### Execution Control (NEW!) ```javascript diff --git a/examples/command-builder-demo.mjs b/examples/command-builder-demo.mjs new file mode 100644 index 00000000..3b28fad2 --- /dev/null +++ b/examples/command-builder-demo.mjs @@ -0,0 +1,51 @@ +import { $ } from '../src/$.mjs'; + +console.log('=== Command Builder Demo ===\n'); + +// Example from the GitHub issue +console.log('1. Issue example:'); +const result1 = await $.command("cat", "./README.md").pipe( + $.command.stdout("inherit"), + $.command.exitCode +).run(); + +console.log('\n2. Basic usage:'); +const result2 = await $.command('echo', 'hello world').run(); +console.log('Output:', result2.stdout.trim()); +console.log('Exit code:', result2.code); + +console.log('\n3. With arguments that need escaping:'); +const result3 = await $.command('echo', 'hello "world"', 'file with spaces.txt').run(); +console.log('Output:', result3.stdout.trim()); + +console.log('\n4. Environment variables:'); +const result4 = await $.command('env') + .env({ DEMO_VAR: 'demo_value', ANOTHER: 'variable' }) + .run({ capture: true, mirror: false }); +console.log('Environment includes DEMO_VAR:', result4.stdout.includes('DEMO_VAR=demo_value')); + +console.log('\n5. Working directory:'); +const result5 = await $.command('pwd') + .cwd('/tmp') + .run({ capture: true, mirror: false }); +console.log('Working directory:', result5.stdout.trim()); + +console.log('\n6. stdin input:'); +const result6 = await $.command('cat') + .stdin('Hello from stdin!') + .run({ capture: true, mirror: false }); +console.log('Cat output:', result6.stdout.trim()); + +console.log('\n7. Safety - preventing shell injection:'); +const malicious = 'hello; rm -rf /' +const result7 = await $.command('echo', malicious).run({ capture: true, mirror: false }); +console.log('Safe output (not executed):', result7.stdout.trim()); + +console.log('\n8. Method chaining:'); +const result8 = await $.command('echo', 'test') + .arg('additional', 'arguments') + .stdout('inherit') + .capture(true) + .run(); + +console.log('\n=== All examples completed successfully! ==='); \ No newline at end of file diff --git a/examples/test-command-builder-result.mjs b/examples/test-command-builder-result.mjs new file mode 100644 index 00000000..1a22c978 --- /dev/null +++ b/examples/test-command-builder-result.mjs @@ -0,0 +1,13 @@ +import { $ } from '../src/$.mjs'; + +async function testResult() { + const result = await $.command('echo', 'hello').run(); + console.log('Result keys:', Object.keys(result)); + console.log('Result:', JSON.stringify(result, null, 2)); + console.log('exitCode:', result.exitCode); + console.log('exit_code:', result.exit_code); + console.log('code:', result.code); + console.log('result instanceof:', result.constructor.name); +} + +testResult().catch(console.error); \ No newline at end of file diff --git a/package.json b/package.json index 6723c5b9..9ecf987a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "command-stream", - "version": "0.7.1", + "version": "0.8.0", "description": "Modern $ shell utility library with streaming, async iteration, and EventEmitter support, optimized for Bun runtime", "type": "module", "main": "src/$.mjs", diff --git a/src/$.mjs b/src/$.mjs index 46c72588..84e4f695 100755 --- a/src/$.mjs +++ b/src/$.mjs @@ -4379,6 +4379,137 @@ function $tagged(strings, ...values) { return runner; } +// Command Builder Class - for safe, injection-free command construction +class CommandBuilder { + constructor(cmd, args = []) { + this.cmd = cmd; + this.args = [...args.map(String)]; // Convert all args to strings + this.options = {}; + } + + // Add arguments to the command + arg(...args) { + this.args.push(...args.map(String)); + return this; + } + + // Set stdout handling + stdout(mode) { + this.options.stdout = mode; + return this; + } + + // Set stderr handling + stderr(mode) { + this.options.stderr = mode; + return this; + } + + // Set stdin handling + stdin(input) { + this.options.stdin = input; + return this; + } + + // Set working directory + cwd(dir) { + this.options.cwd = dir; + return this; + } + + // Set environment variables + env(envVars) { + this.options.env = { ...this.options.env, ...envVars }; + return this; + } + + // Configure capture behavior + capture(shouldCapture = true) { + this.options.capture = shouldCapture; + return this; + } + + // Configure mirroring behavior + mirror(shouldMirror = true) { + this.options.mirror = shouldMirror; + return this; + } + + // Build the command string for execution + buildCommand() { + // Use proper shell escaping for arguments + const escapedArgs = this.args.map(arg => { + // Simple escaping for safety - wrap in single quotes and escape single quotes + if (typeof arg !== 'string') { + arg = String(arg); + } + + // If arg contains single quotes, we need special handling + if (arg.includes("'")) { + // Replace ' with '\'' (close quote, escaped quote, open quote) + return `'${arg.replace(/'/g, "'\\''")}'`; + } + + // Simple case - wrap in single quotes + return `'${arg}'`; + }); + + return [this.cmd, ...escapedArgs].join(' '); + } + + // Create a new ProcessRunner with the built command + run(additionalOptions = {}) { + trace('CommandBuilder', () => `run() called | cmd: ${this.cmd}, args: ${JSON.stringify(this.args)}`); + + const command = this.buildCommand(); + const finalOptions = { + mirror: true, + capture: true, + ...this.options, + ...additionalOptions + }; + + trace('CommandBuilder', () => `Creating ProcessRunner | command: ${command}, options: ${JSON.stringify(finalOptions)}`); + + return new ProcessRunner({ mode: 'shell', command }, finalOptions); + } + + // Support piping configuration + pipe(...configs) { + // Apply each configuration function to this builder + for (const config of configs) { + if (typeof config === 'function') { + config(this); + } + } + return this; + } +} + +// Command builder factory function +function command(cmd, ...args) { + trace('API', () => `command() called | cmd: ${cmd}, args: ${JSON.stringify(args)}`); + return new CommandBuilder(cmd, args); +} + +// Configuration helper functions for piping +command.stdout = (mode) => (builder) => builder.stdout(mode); +command.stderr = (mode) => (builder) => builder.stderr(mode); +command.stdin = (input) => (builder) => builder.stdin(input); +command.capture = (shouldCapture = true) => (builder) => builder.capture(shouldCapture); +command.mirror = (shouldMirror = true) => (builder) => builder.mirror(shouldMirror); +command.cwd = (dir) => (builder) => builder.cwd(dir); +command.env = (envVars) => (builder) => builder.env(envVars); + +// For compatibility with the issue example ($.command.exitCode) +command.exitCode = (builder) => { + // This is a no-op configuration - exitCode is always available on ProcessRunner + return builder; +}; + +// Attach the command builder to $tagged +$tagged.command = command; + function create(defaultOptions = {}) { trace('API', () => `create ENTER | ${JSON.stringify({ defaultOptions }, null, 2)}`); @@ -4642,6 +4773,8 @@ export { configureAnsi, getAnsiConfig, processOutput, - forceCleanupAll + forceCleanupAll, + CommandBuilder, + command }; export default $tagged; \ No newline at end of file diff --git a/tests/command-builder.test.mjs b/tests/command-builder.test.mjs new file mode 100644 index 00000000..e9cd4de5 --- /dev/null +++ b/tests/command-builder.test.mjs @@ -0,0 +1,175 @@ +import { test, expect, describe, beforeEach, afterEach } from 'bun:test'; +import './test-helper.mjs'; // Automatically sets up beforeEach/afterEach cleanup +import { $, CommandBuilder, command } from '../src/$.mjs'; + +describe('Command Builder API', () => { + test('basic command construction', () => { + const cmd = $.command('echo', 'hello'); + expect(cmd).toBeInstanceOf(CommandBuilder); + expect(cmd.cmd).toBe('echo'); + expect(cmd.args).toEqual(['hello']); + }); + + test('command with multiple arguments', () => { + const cmd = $.command('cat', './some-file.txt', './another-file.txt'); + expect(cmd.args).toEqual(['./some-file.txt', './another-file.txt']); + }); + + test('argument escaping for shell safety', () => { + const cmd = $.command('echo', "hello world", "file with spaces.txt"); + const builtCommand = cmd.buildCommand(); + expect(builtCommand).toBe("echo 'hello world' 'file with spaces.txt'"); + }); + + test('argument escaping with single quotes', () => { + const cmd = $.command('echo', "don't escape", "it's working"); + const builtCommand = cmd.buildCommand(); + expect(builtCommand).toBe("echo 'don'\\''t escape' 'it'\\''s working'"); + }); + + test('fluent API with method chaining', () => { + const cmd = $.command('echo', 'hello') + .arg('world') + .stdout('inherit') + .capture(false); + + expect(cmd.args).toEqual(['hello', 'world']); + expect(cmd.options.stdout).toBe('inherit'); + expect(cmd.options.capture).toBe(false); + }); + + test('pipe configuration as shown in issue example', () => { + const cmd = $.command('cat', './some-file.txt').pipe( + $.command.stdout('inherit'), + $.command.exitCode + ); + + expect(cmd.options.stdout).toBe('inherit'); + }); + + test('environment variable configuration', () => { + const cmd = $.command('env') + .env({ FOO: 'bar', BAZ: 'qux' }) + .env({ ANOTHER: 'var' }); + + expect(cmd.options.env).toEqual({ + FOO: 'bar', + BAZ: 'qux', + ANOTHER: 'var' + }); + }); + + test('working directory configuration', () => { + const cmd = $.command('pwd').cwd('/tmp'); + expect(cmd.options.cwd).toBe('/tmp'); + }); + + test('stdin configuration', () => { + const cmd = $.command('cat').stdin('hello world'); + expect(cmd.options.stdin).toBe('hello world'); + }); + + test('stderr configuration', () => { + const cmd = $.command('cat', 'nonexistent.txt').stderr('ignore'); + expect(cmd.options.stderr).toBe('ignore'); + }); + + test('run method returns ProcessRunner', () => { + const cmd = $.command('echo', 'hello'); + const runner = cmd.run(); + + // Should return a ProcessRunner-like object + expect(runner).toBeDefined(); + expect(typeof runner.then).toBe('function'); // Should be thenable + }); + + test('direct command function usage', () => { + const cmd = command('echo', 'hello'); + expect(cmd).toBeInstanceOf(CommandBuilder); + expect(cmd.cmd).toBe('echo'); + expect(cmd.args).toEqual(['hello']); + }); + + test('numeric arguments converted to strings', () => { + const cmd = $.command('echo', 42, 3.14); + expect(cmd.args).toEqual(['42', '3.14']); + + const builtCommand = cmd.buildCommand(); + expect(builtCommand).toBe("echo '42' '3.14'"); + }); +}); + +describe('Command Builder Integration', () => { + test('can execute simple echo command', async () => { + const result = await $.command('echo', 'hello world').run(); + expect(result.stdout.trim()).toBe('hello world'); + expect(result.code).toBe(0); + }); + + test('can execute cat command with file', async () => { + // Create a temp file first using the built-in echo command + const tempContent = 'test content\nsecond line'; + const writeResult = await $.command('sh', '-c', `echo "${tempContent}" > /tmp/test-command-builder.txt`).run(); + expect(writeResult.code).toBe(0); + + const result = await $.command('cat', '/tmp/test-command-builder.txt').run(); + expect(result.stdout.trim()).toBe(tempContent); + expect(result.code).toBe(0); + + // Cleanup + await $.command('rm', '-f', '/tmp/test-command-builder.txt').run(); + }); + + test('pipe configuration works with execution', async () => { + const result = await $.command('echo', 'hello') + .pipe( + $.command.stdout('inherit'), + $.command.capture(true) + ) + .run(); + + expect(result.stdout.trim()).toBe('hello'); + }); + + test('environment variables work in execution', async () => { + const result = await $.command('env') + .env({ TEST_VAR: 'test_value' }) + .run({ capture: true, mirror: false }); + + expect(result.stdout).toContain('TEST_VAR=test_value'); + }); + + test('stdin input works', async () => { + const result = await $.command('cat') + .stdin('hello from stdin') + .run({ capture: true, mirror: false }); + + expect(result.stdout.trim()).toBe('hello from stdin'); + }); +}); + +describe('Command Builder Safety', () => { + test('prevents shell injection in arguments', async () => { + // This should NOT execute the embedded command + const maliciousArg = 'hello; echo "injected"'; + const result = await $.command('echo', maliciousArg).run(); + + // Should output the literal string, not execute the embedded command + expect(result.stdout.trim()).toBe(maliciousArg); + expect(result.stdout).not.toContain('injected\n'); + }); + + test('handles special characters safely', async () => { + const specialChars = '$HOME && rm -rf / || echo "danger"'; + const result = await $.command('echo', specialChars).run(); + + // Should output the literal string + expect(result.stdout.trim()).toBe(specialChars); + }); + + test('handles empty arguments', () => { + const cmd = $.command('echo', '', 'hello', ''); + const builtCommand = cmd.buildCommand(); + expect(builtCommand).toBe("echo '' 'hello' ''"); + }); +}); \ No newline at end of file From 0acfa3d52ae7b8b4b24504f5d11385a264735c89 Mon Sep 17 00:00:00 2001 From: konard Date: Sat, 24 Oct 2026 12:37:19 +0000 Subject: [PATCH 4/4] Fix command builder configuration parity and tighten comparisons --- .../34/pulls/113/analysis/requirements.md | 18 +- js/.changeset/complete-command-builder.md | 5 +- js/docs/COMMAND-BUILDER.md | 19 +- js/docs/COMPARISON.md | 4 +- js/docs/COMPETITOR_TEST_AUDIT.md | 136 +++++----- js/src/$.process-runner-execution.mjs | 147 +---------- js/src/$.process-runner-sync.mjs | 187 ++++++++++++++ js/src/command-builder.mjs | 19 +- js/tests/command-builder.node.test.mjs | 53 ++-- js/tests/command-builder.test.mjs | 53 ++++ js/tests/competitor-compatibility.test.mjs | 26 ++ js/tests/competitor-corpus.mjs | 12 +- js/tests/competitor-decisions.jsonl | 6 +- js/tests/competitor-dispositions.jsonl | 6 +- js/tests/sync.test.mjs | 5 +- js/tests/types/command-builder.ts | 52 ++++ js/types/advanced.d.cts | 233 +++++++++++++++++- rust/README.md | 2 + .../20261024_103000_command_builder.md | 1 + rust/docs/COMPARISON.md | 80 ++++++ rust/docs/COMPETITOR_TEST_AUDIT.md | 79 +++--- rust/src/command_builder.rs | 11 +- rust/tests/competitor_compatibility.rs | 4 +- .../competitor_compatibility/behavior.rs | 42 +++- rust/tests/competitor_compatibility/corpus.rs | 32 +-- .../tests/competitor_compatibility/fixture.rs | 8 + rust/tests/competitor_decisions.jsonl | 20 +- rust/tests/competitor_dispositions.jsonl | 20 +- 28 files changed, 950 insertions(+), 330 deletions(-) create mode 100644 js/src/$.process-runner-sync.mjs create mode 100644 rust/docs/COMPARISON.md diff --git a/dev/log/issues/34/pulls/113/analysis/requirements.md b/dev/log/issues/34/pulls/113/analysis/requirements.md index a4bc7ba0..8e3937db 100644 --- a/dev/log/issues/34/pulls/113/analysis/requirements.md +++ b/dev/log/issues/34/pulls/113/analysis/requirements.md @@ -1,6 +1,6 @@ # Issue 34 / PR 113 acceptance evidence -Sources reviewed: the complete [issue](https://github.com/link-foundation/command-stream/issues/34), its empty comment collection, the PR description, all conversation comments, inline comments and reviews, and merged PRs 225 and 236. There are no required sub-issues. The implementation is reconciled with main commit `2f8cea139a1b00f3067b11785616f3f2ea00ecc4`. +Sources reviewed: the complete [issue](https://github.com/link-foundation/command-stream/issues/34), its empty comment collection, the PR description, all conversation comments (including the [latest quality/comparison request](https://github.com/link-foundation/command-stream/pull/113#issuecomment-6383907892)), inline comments and reviews, and merged PRs 225 and 236. There are no required sub-issues. The implementation is reconciled with main commit `2f8cea139a1b00f3067b11785616f3f2ea00ecc4`. | Requirement | Implementation and verification | | --- | --- | @@ -10,7 +10,7 @@ Sources reviewed: the complete [issue](https://github.com/link-foundation/comman | Support API styles and configurations from the PR feedback | Variadic and argv-array factories, constructor, fluent methods, functional transforms, option objects, nested factory defaults, configured tags and `create`; all ProcessOptions pass through the existing runner. Tests cover environment precedence/removal, cwd, buffered/live stdin, stdio, capture/mirror, overrides, deadlines, abort, events, pipelines and streams. Node tests cover full/core ESM/CommonJS; strict TypeScript tests cover the public exports and signatures. | | Rust counterpart and the referenced Rust Command patterns | Owned fluent/functional composition, env/envs/clear/removal, stdio, cwd, async and sync execution using one native command configuration. A 160 KiB stdin/stdout test verifies concurrent draining instead of blocking on a full pipe. | | Resolve conflicts without removing current features | Merge main into the prepared branch, retain the monorepo layout and current core/compatibility implementations; only the obsolete draft builder tests/examples are replaced. Compare the final diff against main and run the existing suites. | -| Clear docs and comparison across all features | `js/docs/COMMAND-BUILDER.md` documents every builder operation and runner option, precedence, execution contracts and safety boundaries. `js/docs/COMPARISON.md` covers process, shell, streams, lifecycle, compatibility, terminal, translation and development features with primary-source links, covering all twelve projects in the pinned JavaScript audit, both issue references, and the fourteen-project Rust audit. Generated feature examples provide further per-feature alternatives. | +| Clear docs and comparison across all features | `js/docs/COMMAND-BUILDER.md` documents every builder operation and runner option, precedence, execution contracts and safety boundaries. `js/docs/COMPARISON.md` covers process, shell, streams, lifecycle, compatibility, terminal, translation and development features with primary-source links, covering all twelve projects in the pinned JavaScript audit, both issue references, and the fourteen-project Rust audit. `rust/docs/COMPARISON.md` now gives a per-project construction, I/O, configuration, lifecycle and tooling comparison for all fourteen Rust projects. Generated feature examples provide further per-feature alternatives. Both audits identify implemented features accurately and preserve exact upstream conformance limits. | | Competitive claims supported by evidence | Credit competitors' argv safety, fluent options, streams, transforms, plugins and native process controls. Link import-budget checks, compatibility corpora, feature examples and benchmarks. No unmeasured claim that one library universally outperforms every competitor; the comparison records specialized contracts and runtime limits explicitly. | | Prepare release and review | JavaScript minor changeset and Rust minor changelog fragment; update PR title/body, preserve `Fixes #34`, verify current-SHA checks and mark ready without merging. | @@ -23,3 +23,17 @@ Review found another env-preset failure: `command.with({env: {A: 'a'}})('echo', The Rust reproduction initially failed to compile with E0599 at `.pipe` and `.env_clear`. Adding synchronous execution and sharing native command configuration across sync/async resolves those missing contracts while retaining literal OsString arguments and signal exit status normalization. All regression cases remain in the normal automated suites. Investigation logs are kept under `.git/issue-34/` locally; fresh remote failures, if any, are downloaded under `ci-logs/` and checked against commit timestamps and SHA. + +## Follow-up quality audit + +- New failing runtime tests demonstrated that synchronous native execution always piped stdio, ignored `capture:false`, and failed to forward inherited stdin. The extracted shared sync module now applies native stdio modes, emits only piped data, and discards disabled capture. Node subprocess tests verify output appears exactly once and parent stdin reaches the child; Bun tests cover ignored output and events with capture disabled. +- A failing mutation regression showed that `preferLocal` directory arrays remained caller-owned. Options snapshots now also copy/freeze those arrays and ANSI configuration, with regressions for presets and direct construction. Buffers and AbortSignals remain explicitly shared resources. +- Failing strict TypeScript assertions exposed the old variadic `any` fallback: long pipelines could erase exit-code selection and join incompatible result types. Typed overloads now validate up to twenty transforms, including generic configuration after selection; homogeneous transforms can be composed without that limit or pipelines can be chained. A separate failing test at the twentieth-transform boundary caught a shadowed type parameter in the draft overload; its input and output types now remain independent. +- Rust's environment changes now replay in configuration order through native Command rather than a case-sensitive unordered map. A cross-platform fixture verifies replacement/removal, re-addition and Windows case-insensitive versus Unix case-sensitive names in sync and async executions. The Windows branch is verified by the hosted Windows matrix, not inferred from a Linux run. +- Environment-clear/removal accounting was stale in both audits. Three Deno units and ten Rust units now map to executable public invariants in the paired decisions/dispositions ledgers. Other entries state which core/adapter implementations exist and which exact upstream contracts are still unported; adding an option is not treated as proof of full upstream conformance. + +The complete comparison uses the published twelve-project JavaScript and fourteen-project Rust discovery sets, plus Effect as the issue reference. Universal superiority over every open-source project is not established: specialized native handles, expect sessions, persistent result caching and domain return adapters remain documented differences. No unmeasured performance ranking or complete upstream API conformance is claimed. + +Validation logs from this continuation live in `.git/issue-34/`. The first full Rust attempt failed at `rust/tests/cancelled_output.rs:105` (exit -1 versus 0), while its immediate focused rerun passed; the same intermittent cancellation-test behavior is recorded in `dev/log/issues/219/pulls/222/README.md`. The full serial rerun passed 962 tests with 7 ignored, without increasing that test's deadlines. Rust formatting, all-target/all-feature Clippy with warnings denied, and rustdoc with warnings denied also pass. JavaScript lint, formatting, duplication and strict type checks pass, including the twentieth-transform regression. All 37 feature examples pass parity checks in JavaScript and Rust, generated documentation is current (39 files), and documentation validation passes all 251 checks including the new Rust comparison. + +The final full Bun run passed 2,899 tests with 10 skipped and no failures (66,897 assertions across 183 files). The standalone Node builder suite passed all 7 tests. Hosted checks must be verified against the new commit SHA before the PR is marked ready; the PR description records their final status without treating the earlier commit's passing checks as current evidence. diff --git a/js/.changeset/complete-command-builder.md b/js/.changeset/complete-command-builder.md index 12e9e7f6..82f79880 100644 --- a/js/.changeset/complete-command-builder.md +++ b/js/.changeset/complete-command-builder.md @@ -4,5 +4,6 @@ Complete literal command builders with immutable fluent methods, argv arrays, factory and shell presets, merged environment layers, per-run overrides and -synchronous execution. Document every API style and correct the library-wide -competitor comparison. +synchronous execution. Honor inherited/ignored stdio and disabled capture in +sync calls, snapshot nested configuration, and preserve long pipeline types. +Document every API style and correct the library-wide competitor comparison. diff --git a/js/docs/COMMAND-BUILDER.md b/js/docs/COMMAND-BUILDER.md index 4be8b5c4..827e6c2f 100644 --- a/js/docs/COMMAND-BUILDER.md +++ b/js/docs/COMMAND-BUILDER.md @@ -77,8 +77,9 @@ configuration, then per-execution overrides. Later values win. Explicit `env` layers merge with each other; setting a key to `undefined` removes it. When `env` is supplied, it replaces the inherited process environment: include `...process.env` to extend the parent. An initial `env: {}` starts with an empty environment. -Options and their env maps are copied when bound; later caller edits cannot -change them. Buffers and AbortSignals remain shared resources. +Options, their env and ANSI maps, and `preferLocal` directory arrays are copied +and frozen when bound; later caller edits cannot change them. Buffers and +AbortSignals remain shared resources. Builders default to `capture:true` and `mirror:false`. ProcessRunner's other settings apply, including inherited stdin. Inherit/ignore stdout or stderr is @@ -94,7 +95,10 @@ literal builder into a shell command. For shell grammar use the tagged template API or explicitly execute an interpreter. Synchronous execution follows ProcessRunner's sync limitations, including unsupported async virtual handlers. AbortSignal cancellation applies to asynchronous execution; use `timeout` for -synchronous deadlines. +synchronous deadlines. Sync calls honor the same inherit/ignore output and +capture policies. Sync events fire after the child exits, and native sync spawn +buffers piped output even with `capture:false`; use asynchronous streaming for +bounded memory. Inherited output bypasses the runner and is never mirrored twice. ```js const runner = command('node', ['-e', 'process.stdin.pipe(process.stdout)']) @@ -139,7 +143,9 @@ may still interpret its own options or code arguments. In particular, calling `arg`, `args`, `cwd`, `env`, `envs`, `env_clear`, `env_remove`, `stdin`, `stdout`, `stderr`, and `.pipe(|builder| ...)` transforms. `.run().await` and `.run_sync()` return CommandResult; `.exit_code().await` selects status. `.shell_string()` -renders POSIX argv. Inputs use OsString; captured results are UTF-8 strings. +renders POSIX argv. Arguments and environment use OsString; configured stdin and +captured results are UTF-8 strings. Native non-UTF-8 values execute losslessly; +the shell-string renderer uses lossy text conversion for them. Use the Rust ProcessRunner, stream, and compatibility APIs for events, cancellation, pipelines and additional execution options. @@ -152,5 +158,6 @@ The tests execute hostile argv and executable paths, the issue example, immutabl presets and env layers, overrides, stdio policies, sync/async selection, events, pipelines, streaming, timeouts and cancellation. -See the [complete comparison](COMPARISON.md) for competitor APIs across the -library's features and the pinned compatibility corpus for coverage and limits. +See the [JavaScript comparison](COMPARISON.md) and [Rust comparison](../../rust/docs/COMPARISON.md) +for competitor APIs across the library's features, and the pinned compatibility +corpora for coverage and limits. diff --git a/js/docs/COMPARISON.md b/js/docs/COMPARISON.md index bbd1e1b8..13843d13 100644 --- a/js/docs/COMPARISON.md +++ b/js/docs/COMPARISON.md @@ -99,7 +99,9 @@ counterparts in Rust. The following links lead to runnable examples and output: | Cold import/module budget | [Core entry](CORE.md), [automated module-budget tests](../tests/core-entry.test.mjs) | | Process latency, memory, package size, features and real workloads | [JavaScript benchmarks](../benchmarks/README.md), [Rust benchmarks](../../rust/benchmarks/README.md) | -Rust's [competitor audit](../../rust/docs/COMPETITOR_TEST_AUDIT.md) also covers +The [Rust comparison](../../rust/docs/COMPARISON.md) compares construction, +execution, output, configuration, lifecycle and tooling for all fourteen Rust +projects. Rust's [competitor audit](../../rust/docs/COMPETITOR_TEST_AUDIT.md) covers Tokio, async-process, duct, subprocess, xshell, rust_cmd_lib, rust-shell, run_script, shellfn, bkt, assert_cmd, rexpect and expectrl. The audit records contracts such as binary collection, arbitrary handles, expect sessions, diff --git a/js/docs/COMPETITOR_TEST_AUDIT.md b/js/docs/COMPETITOR_TEST_AUDIT.md index 032bf46d..d3cb150b 100644 --- a/js/docs/COMPETITOR_TEST_AUDIT.md +++ b/js/docs/COMPETITOR_TEST_AUDIT.md @@ -66,8 +66,8 @@ frameworks. Copying their files byte-for-byte would mostly test that Execa, Bun, or zx exists. Instead, every scoped test belongs to exactly one disposition: 1. A portable public process invariant is adapted into the executable suite. -2. A public invariant needs a feature command-stream does not have and is listed - in the missing-feature ledger below. +2. A public upstream contract is not fully ported and is recorded in the + remaining-contract ledger below, with its current implementation status. 3. A test is inapplicable because it verifies competitor API shape, private internals, unrelated utilities, runtime behavior, or harness mechanics. @@ -93,32 +93,33 @@ summary-level assertion. ## Executable behavior ports -| Case ID | Public invariant | -| ----------------------------- | ------------------------------------------------------------------------------------------------ | -| `direct-exact-argv` | Direct execution bypasses shell tokenization. | -| `argument-edge-cases` | Empty, whitespace, multiline, Unicode, quote, and shell-metacharacter arguments survive exactly. | -| `safe-template-interpolation` | An untrusted interpolated value remains one literal argument. | -| `array-interpolation` | An interpolated array expands to separate, safely quoted arguments. | -| `cwd-string` | An explicit string working directory reaches the child. | -| `environment` | An explicit environment reaches the child without value corruption. | -| `stdout-stderr-separation` | Standard output and error remain separate. | -| `newline-preservation` | Captured output preserves final and repeated newlines. | -| `unicode-output` | UTF-8 output is decoded without loss. | -| `large-output` | One MiB of output is captured without truncation or deadlock. | -| `nonzero-exit` | Non-zero status reads through `code` and `exitCode`, on results and on errors. | -| `result-text` | `text()` returns captured stdout. | -| `stdin-string` | String input is written completely and stdin is closed. | -| `stdin-buffer` | Buffer input is written without textual coercion. | -| `lazy-execution` | A runner starts only when consumed. | -| `concurrent-execution` | Concurrent children keep their results isolated. | -| `streamed-before-exit` | Output events arrive before a delayed child exits. | -| `events-and-await` | Events and the awaited result observe identical bytes. | -| `bound-options` | Options bound to a tagged command apply when the command executes. | -| `sync-execution` | Synchronous direct execution preserves exact argv. | -| `programmatic-pipeline` | Source stdout becomes destination stdin. | -| `spawn-error-result` | Sync and async unavailable executables produce results instead of uncaught errors. | -| `abort-signal` | An external `AbortSignal` terminates a running child. | -| `zx-compatible-api` | Every zx unit runs one-to-one against the zx-compatible layer, `command-stream/zx`. | +| Case ID | Public invariant | +| ------------------------------ | ------------------------------------------------------------------------------------------------ | +| `direct-exact-argv` | Direct execution bypasses shell tokenization. | +| `argument-edge-cases` | Empty, whitespace, multiline, Unicode, quote, and shell-metacharacter arguments survive exactly. | +| `safe-template-interpolation` | An untrusted interpolated value remains one literal argument. | +| `array-interpolation` | An interpolated array expands to separate, safely quoted arguments. | +| `cwd-string` | An explicit string working directory reaches the child. | +| `environment` | An explicit environment reaches the child without value corruption. | +| `environment-clear-and-remove` | Sync and async execution replace inherited env and omit undefined values. | +| `stdout-stderr-separation` | Standard output and error remain separate. | +| `newline-preservation` | Captured output preserves final and repeated newlines. | +| `unicode-output` | UTF-8 output is decoded without loss. | +| `large-output` | One MiB of output is captured without truncation or deadlock. | +| `nonzero-exit` | Non-zero status reads through `code` and `exitCode`, on results and on errors. | +| `result-text` | `text()` returns captured stdout. | +| `stdin-string` | String input is written completely and stdin is closed. | +| `stdin-buffer` | Buffer input is written without textual coercion. | +| `lazy-execution` | A runner starts only when consumed. | +| `concurrent-execution` | Concurrent children keep their results isolated. | +| `streamed-before-exit` | Output events arrive before a delayed child exits. | +| `events-and-await` | Events and the awaited result observe identical bytes. | +| `bound-options` | Options bound to a tagged command apply when the command executes. | +| `sync-execution` | Synchronous direct execution preserves exact argv. | +| `programmatic-pipeline` | Source stdout becomes destination stdin. | +| `spawn-error-result` | Sync and async unavailable executables produce results instead of uncaught errors. | +| `abort-signal` | An external `AbortSignal` terminates a running child. | +| `zx-compatible-api` | Every zx unit runs one-to-one against the zx-compatible layer, `command-stream/zx`. | ### zx-compatible layer @@ -139,44 +140,61 @@ the invariant appears upstream. Existing focused command-stream suites continue to exercise shell operators, virtual commands, built-ins, terminal capture, signals, cleanup, CommonJS, and runtime-specific paths in greater depth. -## Missing-feature ledger +## Remaining upstream contracts -These are comparison inputs for later work, not promises that every competitor -choice fits command-stream. Stability of the existing API takes priority. A -future implementation should add a failing focused test and update the manifest -in the same change. +The stable `missing` disposition IDs below track upstream contracts that have +not been ported completely into this corpus. They are not a claim that every +named feature is absent from the current package: later core implementations +and compatibility entries are identified explicitly. A supported option alone +does not establish all upstream error, timing or platform semantics. The +[current comparison](COMPARISON.md) identifies the public surface to use. +Environment replacement/removal is now executable in both sync and async ports; +its three Deno units have moved from missing to ported in both exact ledgers. ### timeout-option Execa, zx, Dax, and nano-spawn provide a per-command deadline option. -command-stream accepts external abort signals but has no public timeout option. +Core ProcessOptions and literal builders support `timeout`, `signal`, +`killSignal` and `killGrace`; zx and Execa entries expose their own deadlines. +This corpus has not ported every upstream deadline/error contract, including +specialized shell/script behavior. ### ipc-and-fork Node, Bun, Deno's Node compatibility layer, and Execa expose IPC channels or -fork helpers. command-stream currently exposes only stdin/stdout/stderr process -communication. +fork helpers. The core builder has three stdio channels; use the native +cross-spawn or Execa entry for IPC. These corpus units have not been ported +against those entries. ### custom-stdio-descriptors Node-derived APIs accept arbitrary stdio arrays, extra file descriptors, files, -and inherited streams. command-stream has fixed stdin/stdout/stderr policies. +and inherited streams. The core builder has fixed stdin/stdout/stderr policies; +the native spawn and Execa entries support richer descriptors. Their complete +upstream descriptor conformance is outside these portable core ports. ### configurable-encoding Several competitors support caller-selected encodings or raw binary output. command-stream results currently expose strings, with Buffer access available -through streaming interfaces rather than a result encoding option. +through streaming interfaces rather than a result encoding option. The Execa +entry supports encoding/byte results under its own result contract. ### max-buffer-policy Node `exec`, Execa, and ShellJS allow a maximum captured-output policy. -command-stream streams and captures without a configurable upper bound. +The core builder has no configurable maximum capture size (Node synchronous +spawn also has its native buffer limit). Use asynchronous `capture:false` with +bounded stream queues for bounded collection, or Execa's capture policy when +its semantics are required. ### output-transforms-and-line-iteration Execa, zx, Dax, and nano-spawn provide line iteration, transforms, generators, -or verbose output hooks beyond command-stream's typed chunk stream. +or verbose output hooks. Core streams now provide line framing, map/filter, +batching/windows, statistics and bounded queues; zx and Execa have additional +adapter-specific transforms. These units still require per-contract checks +rather than a blanket equivalence with those interfaces. ### local-binary-resolution @@ -190,7 +208,8 @@ audit. cross-spawn and nano-spawn normalize Windows shebangs, `PATHEXT`, and `cmd.exe` escaping. command-stream relies on the runtime and platform shell for those -rules and does not provide a dedicated resolver. +rules in the core runner. The cross-spawn export uses the actual cross-spawn +resolver; selecting that export does not change core-builder resolution. ### cross-platform-inline-environment-syntax @@ -199,21 +218,18 @@ variable references for Windows command processors. command-stream accepts an explicit environment object, but it does not provide this command-line rewrite layer. -### environment-clear-and-remove - -Deno can clear inherited environment values before spawning. command-stream -can add or replace values but has no environment clear/remove policy. - ### process-credentials Node and Deno can select child user and group IDs on supported platforms. -command-stream has no public process-credential options. +The core builder has no credential options; native spawn/Execa entries accept +their platform-specific process options. The corpus does not claim those +privileged platform contracts have been ported. ### windows-process-window-options Node and Deno expose Windows-only process creation controls such as hidden -windows and raw command-line arguments. command-stream delegates those choices -to the JavaScript runtime. +windows and raw command-line arguments. Use the native spawn entry for those +Node options; the core ProcessOptions surface does not expose them. ### child-process-handle-lifecycle @@ -234,19 +250,23 @@ command-stream's current virtual-command set. ### rich-error-and-timing-metadata Competitors expose fields such as command text, escaped command, failed stage, -signal description, duration, and timing. command-stream's result is limited to -status and captured streams. +signal description, duration, and timing. Core results include status, signal, +PID, timeout state and streams; richer error/timing fields belong to the +compatibility entries. This corpus does not assert every upstream metadata +field or failed-stage contract. ### graceful-termination Execa, zx, and Dax provide configurable graceful cancellation and forced-kill -fallbacks. command-stream supports a kill signal but no public grace-period -policy. +fallbacks. Core async execution now supports `killSignal` and `killGrace`, with +process-tree cleanup; sync native spawn has its own termination behavior. The +complete upstream cancellation protocols remain distinct from this core policy. ### combined-all-output Execa and nano-spawn can preserve interleaved stdout and stderr in a combined -result. command-stream deliberately keeps the two captured streams separate. +result. The core builder keeps captured streams separate; Execa's `all` and +zx's combined ProcessOutput use their respective compatibility entries. ### iterable-and-stream-input-options @@ -256,9 +276,11 @@ ignore, or an interactively accessed stdin pipe. ### layered-bound-option-merging -Execa can bind several option layers and merge selected nested fields while -letting invocation-level values override defaults. command-stream supports one -bound tagged-template option object but not layered or per-invocation merging. +Literal builders support nested `command.with` presets, fluent/functional +configuration, explicit env merging and per-run overrides. Execa's own binding +contract also treats undefined/default and selected nested options according to +its API. Use the Execa entry for that exact contract; these upstream units have +not been reclassified merely because builder presets now exist. ### url-working-directory diff --git a/js/src/$.process-runner-execution.mjs b/js/src/$.process-runner-execution.mjs index 791df8f1..634cc21c 100644 --- a/js/src/$.process-runner-execution.mjs +++ b/js/src/$.process-runner-execution.mjs @@ -3,7 +3,6 @@ import { pumpInheritedStdin } from './$.process-runner-stdin.mjs'; import cp from 'child_process'; import { toStreamResult } from './$.result-streams.mjs'; import { trace } from './$.trace.mjs'; -import { normalizeBunExitSignal } from './$.process-runner-signal.mjs'; import { normalizeTimeout, setupCommandTimeout, @@ -21,12 +20,9 @@ import { attachExitCodeAlias, createCancelledResult, createCommandError, - createExecutionErrorResult, createResult, determineFinalExitCode, finishExecutionError, - getStdinString, - getSyncStdinInput, isProcessLaunchError, prepareSpawnErrorResult, } from './$.result.mjs'; @@ -41,6 +37,10 @@ import { drainPumpsAfterExit, } from './$.process-runner-exit.mjs'; import { effectiveCwd, effectiveEnv } from './$.process-context.mjs'; +import { + executeSyncProcess, + processSyncResult, +} from './$.process-runner-sync.mjs'; import { startDetached, startDetachedSync, @@ -498,143 +498,6 @@ function throwErrexitIfNeeded(runner, globalShellSettings) { ); } -/** - * Execute sync process using Bun - * @param {Array} argv - Command arguments - * @param {object} options - Spawn options - * @returns {object} Result object - */ -function executeSyncBun(argv, options) { - const { cwd, env, stdin, timeout, killSignal } = options; - let proc; - try { - proc = Bun.spawnSync(argv, { - cwd, - env, - stdin: getSyncStdinInput(stdin), - stdout: 'pipe', - stderr: 'pipe', - timeout, - killSignal, - }); - } catch (error) { - return createExecutionErrorResult(error); - } - - const signal = normalizeBunExitSignal(proc.exitCode, proc.signalCode); - const result = createResult({ - code: determineFinalExitCode(proc.exitCode, signal), - signal, - stdout: proc.stdout?.toString('utf8') || '', - stderr: proc.stderr?.toString('utf8') || '', - stdin: getStdinString(stdin), - }); - result.timedOut = Boolean(proc.exitedDueToTimeout); - result.child = proc; - return result; -} - -/** - * Execute sync process using Node - * @param {Array} argv - Command arguments - * @param {object} options - Spawn options - * @returns {object} Result object - */ -function executeSyncNode(argv, options) { - const { cwd, env, stdin, shell, timeout, killSignal } = options; - const proc = cp.spawnSync(argv[0], argv.slice(1), { - cwd, - env, - shell, - input: getSyncStdinInput(stdin), - encoding: 'utf8', - stdio: ['pipe', 'pipe', 'pipe'], - timeout, - killSignal, - }); - // A `timeout` kill sets `error` (ETIMEDOUT) next to the kill signal; report - // it as a signal exit like the async path does, not as a spawn failure. - const timedOut = proc.error?.code === 'ETIMEDOUT'; - - if (proc.error && !timedOut) { - const result = createExecutionErrorResult(proc.error); - result.child = proc; - return result; - } - - const signal = isBun - ? normalizeBunExitSignal(proc.status, proc.signal) - : (proc.signal ?? null); - const result = createResult({ - code: determineFinalExitCode(proc.status, signal), - signal, - stdout: proc.stdout || '', - stderr: proc.stderr || '', - stdin: getStdinString(stdin), - }); - result.timedOut = timedOut; - result.child = proc; - return result; -} - -/** - * Execute sync process with appropriate runtime - * @param {Array} argv - Command arguments - * @param {object} options - Spawn options - * @returns {object} Result object - */ -function executeSyncProcess(argv, options) { - // Guard against a deleted/inaccessible working directory (issue #44). - options = { ...options, cwd: resolveSpawnCwd(options.cwd) }; - return isBun && !options.shell - ? executeSyncBun(argv, options) - : executeSyncNode(argv, options); -} - -/** - * Handle sync result processing - * @param {object} runner - ProcessRunner instance - * @param {object} result - Result object - * @param {object} globalShellSettings - Shell settings - * @returns {object} Result - */ -function processSyncResult(runner, result, globalShellSettings) { - // The sync spawn has already exited, but reports the pid it ran under. - runner._pid = result.child?.pid ?? runner._pid; - - if (runner.options.mirror) { - if (result.stdout) { - safeWrite(process.stdout, result.stdout); - } - if (result.stderr) { - safeWrite(process.stderr, result.stderr); - } - } - - runner.outChunks = result.stdout ? [Buffer.from(result.stdout)] : []; - runner.errChunks = result.stderr ? [Buffer.from(result.stderr)] : []; - - if (result.stdout) { - runner._emitProcessedData('stdout', Buffer.from(result.stdout)); - } - if (result.stderr) { - runner._emitProcessedData('stderr', Buffer.from(result.stderr)); - } - - runner.finish(result); - - if (globalShellSettings.errexit && result.code !== 0) { - throw createCommandError(`Command failed with exit code ${result.code}`, { - code: result.code, - stdout: result.stdout, - stderr: result.stderr, - result, - }); - } - - return result; -} - /** * Stop a runner for an aborted `signal` or an expired `timeout`. Honors the * configured killSignal (defaults to SIGTERM) so both use the same stop signal @@ -1439,6 +1302,8 @@ export function attachExecutionMethods(ProcessRunner, deps) { shell: shellArgv, timeout: normalizeTimeout(this.options.timeout) || undefined, killSignal: this.options.killSignal, + stdout: this.options.stdout, + stderr: this.options.stderr, }); return processSyncResult(this, result, shellSettings); }; diff --git a/js/src/$.process-runner-sync.mjs b/js/src/$.process-runner-sync.mjs new file mode 100644 index 00000000..aaabd03e --- /dev/null +++ b/js/src/$.process-runner-sync.mjs @@ -0,0 +1,187 @@ +// Native synchronous execution shared by shell runners and literal builders. +import cp from 'node:child_process'; +import { trace } from './$.trace.mjs'; +import { normalizeBunExitSignal } from './$.process-runner-signal.mjs'; +import { resolveSpawnCwd } from './$.shell.mjs'; +import { safeWrite } from './$.stream-utils.mjs'; +import { + createCommandError, + createExecutionErrorResult, + createResult, + determineFinalExitCode, + getStdinString, + getSyncStdinInput, +} from './$.result.mjs'; + +const isBun = typeof globalThis.Bun !== 'undefined'; + +/** + * Execute sync process using Bun + * @param {Array} argv - Command arguments + * @param {object} options - Spawn options + * @returns {object} Result object + */ +function executeSyncBun(argv, options) { + const { cwd, env, stdin, timeout, killSignal } = options; + let proc; + try { + proc = Bun.spawnSync(argv, { + cwd, + env, + stdin: + getSyncStdinInput(stdin) ?? + (stdin === 'inherit' ? 'inherit' : 'ignore'), + stdout: options.stdout ?? 'pipe', + stderr: options.stderr ?? 'pipe', + timeout, + killSignal, + }); + } catch (error) { + return createExecutionErrorResult(error); + } + + const signal = normalizeBunExitSignal(proc.exitCode, proc.signalCode); + const result = createResult({ + code: determineFinalExitCode(proc.exitCode, signal), + signal, + stdout: proc.stdout?.toString('utf8') || '', + stderr: proc.stderr?.toString('utf8') || '', + stdin: getStdinString(stdin), + }); + result.timedOut = Boolean(proc.exitedDueToTimeout); + result.child = proc; + return result; +} + +/** + * Execute sync process using Node + * @param {Array} argv - Command arguments + * @param {object} options - Spawn options + * @returns {object} Result object + */ +function executeSyncNode(argv, options) { + const { cwd, env, stdin, shell, timeout, killSignal } = options; + const proc = cp.spawnSync(argv[0], argv.slice(1), { + cwd, + env, + shell, + input: getSyncStdinInput(stdin), + encoding: 'utf8', + stdio: [ + stdin === 'inherit' || stdin === 'ignore' ? stdin : 'pipe', + options.stdout ?? 'pipe', + options.stderr ?? 'pipe', + ], + timeout, + killSignal, + }); + // A `timeout` kill sets `error` (ETIMEDOUT) next to the kill signal; report + // it as a signal exit like the async path does, not as a spawn failure. + const timedOut = proc.error?.code === 'ETIMEDOUT'; + + if (proc.error && !timedOut) { + const result = createExecutionErrorResult(proc.error); + result.child = proc; + return result; + } + + const signal = isBun + ? normalizeBunExitSignal(proc.status, proc.signal) + : (proc.signal ?? null); + const result = createResult({ + code: determineFinalExitCode(proc.status, signal), + signal, + stdout: proc.stdout || '', + stderr: proc.stderr || '', + stdin: getStdinString(stdin), + }); + result.timedOut = timedOut; + result.child = proc; + return result; +} + +/** + * Execute sync process with appropriate runtime + * @param {Array} argv - Command arguments + * @param {object} options - Spawn options + * @returns {object} Result object + */ +export function executeSyncProcess(argv, options) { + // Guard against a deleted/inaccessible working directory (issue #44). + options = { ...options, cwd: resolveSpawnCwd(options.cwd) }; + trace( + 'ProcessRunner', + () => + `sync stdio | ${JSON.stringify({ + stdin: + getSyncStdinInput(options.stdin) === undefined + ? options.stdin + : 'buffer', + stdout: options.stdout ?? 'pipe', + stderr: options.stderr ?? 'pipe', + })}`, + { options } + ); + return isBun && !options.shell + ? executeSyncBun(argv, options) + : executeSyncNode(argv, options); +} + +/** + * Handle sync result processing + * @param {object} runner - ProcessRunner instance + * @param {object} result - Result object + * @param {object} globalShellSettings - Shell settings + * @returns {object} Result + */ +export function processSyncResult(runner, result, globalShellSettings) { + // The sync spawn has already exited, but reports the pid it ran under. + runner._pid = result.child?.pid ?? runner._pid; + + if (runner.options.mirror) { + if (result.stdout) { + safeWrite(process.stdout, result.stdout); + } + if (result.stderr) { + safeWrite(process.stderr, result.stderr); + } + } + + runner.outChunks = runner.options.capture + ? result.stdout + ? [Buffer.from(result.stdout)] + : [] + : null; + runner.errChunks = runner.options.capture + ? result.stderr + ? [Buffer.from(result.stderr)] + : [] + : null; + + if (result.stdout) { + runner._emitProcessedData('stdout', Buffer.from(result.stdout)); + } + if (result.stderr) { + runner._emitProcessedData('stderr', Buffer.from(result.stderr)); + } + + if (!runner.options.capture) { + result.stdout = undefined; + result.stderr = undefined; + result.stdin = undefined; + result.text = () => Promise.resolve(''); + } + + runner.finish(result); + + if (globalShellSettings.errexit && result.code !== 0) { + throw createCommandError(`Command failed with exit code ${result.code}`, { + code: result.code, + stdout: result.stdout, + stderr: result.stderr, + result, + }); + } + + return result; +} diff --git a/js/src/command-builder.mjs b/js/src/command-builder.mjs index 758c7a84..6000ab89 100644 --- a/js/src/command-builder.mjs +++ b/js/src/command-builder.mjs @@ -32,6 +32,17 @@ function mergeOptions(base, overrides = {}) { }; } +function snapshotOptions(options) { + return Object.freeze({ + ...options, + ...(options.env && { env: Object.freeze({ ...options.env }) }), + ...(options.ansi && { ansi: Object.freeze({ ...options.ansi }) }), + ...(Array.isArray(options.preferLocal) && { + preferLocal: Object.freeze([...options.preferLocal]), + }), + }); +} + /** An immutable, lazy exact-argv command specification. */ export class CommandBuilder { constructor(file, args = [], options = {}, select = identity) { @@ -48,11 +59,10 @@ export class CommandBuilder { } this.file = file; this.args = Object.freeze(args.map(argument)); - this.options = Object.freeze({ + this.options = snapshotOptions({ mirror: false, capture: true, ...options, - ...(options.env && { env: Object.freeze({ ...options.env }) }), }); this.select = select; Object.freeze(this); @@ -165,10 +175,7 @@ export class CommandBuilder { } function factory(options = {}) { - const defaults = { - ...options, - ...(options.env && { env: { ...options.env } }), - }; + const defaults = snapshotOptions(options); const make = (file, ...args) => { if (Array.isArray(args[0])) { if (args.length > 2) { diff --git a/js/tests/command-builder.node.test.mjs b/js/tests/command-builder.node.test.mjs index 4240a3ca..463f6006 100644 --- a/js/tests/command-builder.node.test.mjs +++ b/js/tests/command-builder.node.test.mjs @@ -40,24 +40,45 @@ for (const [format, api] of [ ); } -test( - 'inherit writes exactly once while ignore discards output', - { timeout: 10000 }, - () => { - const source = ` +for (const execution of ['run', 'sync']) { + test( + `${execution}: inherit writes exactly once while ignore discards output`, + { timeout: 10000 }, + () => { + const source = ` const { command } = await import('./js/src/command-builder.mjs'); const builder = command(process.execPath, '-e', "console.log('visible');console.error('hidden')") .stdout('inherit').stderr('ignore').mirror(true); - const result = await builder.run(); + const result = await builder.${execution}(); if (String(result.stdout) !== '' || String(result.stderr) !== '') process.exit(9); `; - const result = spawnSync( - process.execPath, - ['--input-type=module', '-e', source], - { encoding: 'utf8' } - ); - assert.equal(result.status, 0, result.stderr); - assert.equal(result.stdout, 'visible\n'); - assert.equal(result.stderr, ''); - } -); + const result = spawnSync( + process.execPath, + ['--input-type=module', '-e', source], + { encoding: 'utf8' } + ); + assert.equal(result.status, 0, result.stderr); + assert.equal(result.stdout, 'visible\n'); + assert.equal(result.stderr, ''); + } + ); +} + +test('sync inherits piped stdin from the parent', { timeout: 10000 }, () => { + const source = ` + const { command } = await import('./js/src/command-builder.mjs'); + const builder = command(process.execPath, '-e', 'process.stdin.pipe(process.stdout)'); + const result = builder.stdin('inherit').sync(); + process.stdout.write(String(result.stdout)); + `; + const result = spawnSync( + process.execPath, + ['--input-type=module', '-e', source], + { + input: 'inherited input', + encoding: 'utf8', + } + ); + assert.equal(result.status, 0, result.stderr); + assert.equal(result.stdout, 'inherited input'); +}); diff --git a/js/tests/command-builder.test.mjs b/js/tests/command-builder.test.mjs index ddc04078..f7806609 100644 --- a/js/tests/command-builder.test.mjs +++ b/js/tests/command-builder.test.mjs @@ -188,6 +188,59 @@ describe('command builder API styles', () => { controller.abort(); expect((await builder.run({ signal: controller.signal })).code).not.toBe(0); }, 5000); + + test('sync honors ignored stdio without capture, events or mirroring', async () => { + const builder = node("console.log('hidden'); console.error('hidden')") + .stdout('ignore') + .stderr('ignore') + .mirror(true); + const runner = builder.runner(); + const events = []; + runner.on('data', (chunk) => events.push(chunk)); + const result = runner.sync(); + expect(result.code).toBe(0); + expect(await result.text()).toBe(''); + expect(String(result.stderr)).toBe(''); + expect(events).toEqual([]); + }); + + test('sync capture:false keeps events but discards retained output', async () => { + const runner = node("console.log('visible'); console.error('error')") + .capture(false) + .runner(); + let stdout = ''; + let stderr = ''; + runner.on('stdout', (chunk) => { + stdout += chunk; + }); + runner.on('stderr', (chunk) => { + stderr += chunk; + }); + const result = runner.sync(); + expect(stdout).toBe('visible\n'); + expect(stderr).toBe('error\n'); + expect(result.stdout).toBeUndefined(); + expect(result.stderr).toBeUndefined(); + expect(await result.text()).toBe(''); + expect(runner.outChunks).toBeNull(); + expect(runner.errChunks).toBeNull(); + }); + + test('presets and builders snapshot nested configuration', () => { + const directories = ['/initial']; + const ansi = { preserveAnsi: false }; + const options = { preferLocal: directories, ansi }; + const preset = command.with(options); + const builder = command('echo', [], options); + directories.push('/mutated'); + ansi.preserveAnsi = true; + for (const configured of [preset('echo'), builder]) { + expect(configured.options.preferLocal).toEqual(['/initial']); + expect(configured.options.ansi.preserveAnsi).toBe(false); + expect(Object.isFrozen(configured.options.preferLocal)).toBe(true); + expect(Object.isFrozen(configured.options.ansi)).toBe(true); + } + }); }); describe('command builder safety and rendering', () => { diff --git a/js/tests/competitor-compatibility.test.mjs b/js/tests/competitor-compatibility.test.mjs index 52844309..21ca3722 100644 --- a/js/tests/competitor-compatibility.test.mjs +++ b/js/tests/competitor-compatibility.test.mjs @@ -451,6 +451,32 @@ describe('ported public process behavior', () => { } ); + port( + 'environment-clear-and-remove', + 'replaces inherited environment and removes explicitly undefined keys', + async () => { + const [parentKey] = Object.keys(process.env); + expect(parentKey).toBeDefined(); + const env = { + COMMAND_STREAM_CORPUS_ALPHA: 'one', + [parentKey]: undefined, + }; + for (const sync of [false, true]) { + const runner = fixtureRunner( + 'env', + [parentKey, 'COMMAND_STREAM_CORPUS_ALPHA'], + { env } + ); + const result = sync ? runner.sync() : await runner; + expect(result.code).toBe(0); + expect(JSON.parse(result.stdout)).toEqual({ + [parentKey]: null, + COMMAND_STREAM_CORPUS_ALPHA: 'one', + }); + } + } + ); + port( 'stdout-stderr-separation', 'captures stdout-only, stderr-only, and mixed output independently', diff --git a/js/tests/competitor-corpus.mjs b/js/tests/competitor-corpus.mjs index 9d697ba6..799459ec 100644 --- a/js/tests/competitor-corpus.mjs +++ b/js/tests/competitor-corpus.mjs @@ -488,6 +488,14 @@ export const portedCases = [ 'mod.test.ts', ], }, + { + id: 'environment-clear-and-remove', + competitors: ['deno-command'], + upstream: [ + 'tests/unit/command_test.ts', + 'tests/unit_node/child_process_test.ts', + ], + }, { // Every zx unit is ported one-to-one under js/tests/zx against the // zx-compatible layer (`command-stream/zx`, issue #26). @@ -528,10 +536,6 @@ export const missingFeatures = [ id: 'cross-platform-inline-environment-syntax', competitors: ['cross-env'], }, - { - id: 'environment-clear-and-remove', - competitors: ['deno-command'], - }, { id: 'process-credentials', competitors: ['node-child-process', 'deno-command'], diff --git a/js/tests/competitor-decisions.jsonl b/js/tests/competitor-decisions.jsonl index b090b31e..e14d4f67 100644 --- a/js/tests/competitor-decisions.jsonl +++ b/js/tests/competitor-decisions.jsonl @@ -571,7 +571,7 @@ {"id":"deno-command:tests/unit_node/child_process_test.ts:880:1:registration","disposition":{"kind":"ported","id":"abort-signal"}} {"id":"deno-command:tests/unit_node/child_process_test.ts:901:1:registration","disposition":{"kind":"ported","id":"abort-signal"}} {"id":"deno-command:tests/unit_node/child_process_test.ts:917:1:registration","disposition":{"kind":"ported","id":"environment"}} -{"id":"deno-command:tests/unit_node/child_process_test.ts:950:1:registration","disposition":{"kind":"missing","id":"environment-clear-and-remove"}} +{"id":"deno-command:tests/unit_node/child_process_test.ts:950:1:registration","disposition":{"kind":"ported","id":"environment-clear-and-remove"}} {"id":"deno-command:tests/unit_node/child_process_test.ts:985:1:registration","disposition":{"kind":"ported","id":"environment"}} {"id":"deno-command:tests/unit_node/child_process_test.ts:1020:1:registration","disposition":{"kind":"ported","id":"environment"}} {"id":"deno-command:tests/unit_node/child_process_test.ts:1039:1:registration","disposition":{"kind":"ported","id":"sync-execution"}} @@ -639,8 +639,8 @@ {"id":"deno-command:tests/unit/command_test.ts:751:1:registration","disposition":{"kind":"ported","id":"environment"}} {"id":"deno-command:tests/unit/command_test.ts:769:1:registration","disposition":{"kind":"ported","id":"environment"}} {"id":"deno-command:tests/unit/command_test.ts:793:1:registration","disposition":{"kind":"ported","id":"environment"}} -{"id":"deno-command:tests/unit/command_test.ts:811:1:registration","disposition":{"kind":"missing","id":"environment-clear-and-remove"}} -{"id":"deno-command:tests/unit/command_test.ts:838:1:registration","disposition":{"kind":"missing","id":"environment-clear-and-remove"}} +{"id":"deno-command:tests/unit/command_test.ts:811:1:registration","disposition":{"kind":"ported","id":"environment-clear-and-remove"}} +{"id":"deno-command:tests/unit/command_test.ts:838:1:registration","disposition":{"kind":"ported","id":"environment-clear-and-remove"}} {"id":"deno-command:tests/unit/command_test.ts:865:1:registration","disposition":{"kind":"missing","id":"process-credentials"}} {"id":"deno-command:tests/unit/command_test.ts:893:1:registration","disposition":{"kind":"missing","id":"windows-shebang-and-pathext-resolution"}} {"id":"deno-command:tests/unit/command_test.ts:924:1:registration","disposition":{"kind":"missing","id":"process-credentials"}} diff --git a/js/tests/competitor-dispositions.jsonl b/js/tests/competitor-dispositions.jsonl index 774397a8..fea43965 100644 --- a/js/tests/competitor-dispositions.jsonl +++ b/js/tests/competitor-dispositions.jsonl @@ -583,7 +583,7 @@ {"record":"unit","id":"deno-command:tests/unit_node/child_process_test.ts:880:1:registration","source":"deno-command","unit":"registration","path":"tests/unit_node/child_process_test.ts","line":880,"column":1,"label":"[node/child_process spawn] AbortSignal aborts the child and emits AbortError","disposition":{"kind":"ported","id":"abort-signal"},"url":"https://github.com/denoland/deno/blob/336da420f4343cbb1dcbd5eed9d075ff555ed6ee/tests/unit_node/child_process_test.ts#L880"} {"record":"unit","id":"deno-command:tests/unit_node/child_process_test.ts:901:1:registration","source":"deno-command","unit":"registration","path":"tests/unit_node/child_process_test.ts","line":901,"column":1,"label":"[node/child_process] kill() on an exited process returns false and does not throw","disposition":{"kind":"ported","id":"abort-signal"},"url":"https://github.com/denoland/deno/blob/336da420f4343cbb1dcbd5eed9d075ff555ed6ee/tests/unit_node/child_process_test.ts#L901"} {"record":"unit","id":"deno-command:tests/unit_node/child_process_test.ts:917:1:registration","source":"deno-command","unit":"registration","path":"tests/unit_node/child_process_test.ts","line":917,"column":1,"label":"[node/child_process spawn] child inherits Deno.env when options.env is not provided","disposition":{"kind":"ported","id":"environment"},"url":"https://github.com/denoland/deno/blob/336da420f4343cbb1dcbd5eed9d075ff555ed6ee/tests/unit_node/child_process_test.ts#L917"} -{"record":"unit","id":"deno-command:tests/unit_node/child_process_test.ts:950:1:registration","source":"deno-command","unit":"registration","path":"tests/unit_node/child_process_test.ts","line":950,"column":1,"label":"[node/child_process spawn] child doesn","disposition":{"kind":"missing","id":"environment-clear-and-remove"},"url":"https://github.com/denoland/deno/blob/336da420f4343cbb1dcbd5eed9d075ff555ed6ee/tests/unit_node/child_process_test.ts#L950"} +{"record":"unit","id":"deno-command:tests/unit_node/child_process_test.ts:950:1:registration","source":"deno-command","unit":"registration","path":"tests/unit_node/child_process_test.ts","line":950,"column":1,"label":"[node/child_process spawn] child doesn","disposition":{"kind":"ported","id":"environment-clear-and-remove"},"url":"https://github.com/denoland/deno/blob/336da420f4343cbb1dcbd5eed9d075ff555ed6ee/tests/unit_node/child_process_test.ts#L950"} {"record":"unit","id":"deno-command:tests/unit_node/child_process_test.ts:985:1:registration","source":"deno-command","unit":"registration","path":"tests/unit_node/child_process_test.ts","line":985,"column":1,"label":"undefinedValueInEnvVar","disposition":{"kind":"ported","id":"environment"},"url":"https://github.com/denoland/deno/blob/336da420f4343cbb1dcbd5eed9d075ff555ed6ee/tests/unit_node/child_process_test.ts#L985"} {"record":"unit","id":"deno-command:tests/unit_node/child_process_test.ts:1020:1:registration","source":"deno-command","unit":"registration","path":"tests/unit_node/child_process_test.ts","line":1020,"column":1,"label":"spawnSyncUndefinedValueInEnvVar","disposition":{"kind":"ported","id":"environment"},"url":"https://github.com/denoland/deno/blob/336da420f4343cbb1dcbd5eed9d075ff555ed6ee/tests/unit_node/child_process_test.ts#L1020"} {"record":"unit","id":"deno-command:tests/unit_node/child_process_test.ts:1039:1:registration","source":"deno-command","unit":"registration","path":"tests/unit_node/child_process_test.ts","line":1039,"column":1,"label":"spawnSyncStdioUndefined","disposition":{"kind":"ported","id":"sync-execution"},"url":"https://github.com/denoland/deno/blob/336da420f4343cbb1dcbd5eed9d075ff555ed6ee/tests/unit_node/child_process_test.ts#L1039"} @@ -651,8 +651,8 @@ {"record":"unit","id":"deno-command:tests/unit/command_test.ts:751:1:registration","source":"deno-command","unit":"registration","path":"tests/unit/command_test.ts","line":751,"column":1,"label":"commandEnv","disposition":{"kind":"ported","id":"environment"},"url":"https://github.com/denoland/deno/blob/336da420f4343cbb1dcbd5eed9d075ff555ed6ee/tests/unit/command_test.ts#L751"} {"record":"unit","id":"deno-command:tests/unit/command_test.ts:769:1:registration","source":"deno-command","unit":"registration","path":"tests/unit/command_test.ts","line":769,"column":1,"label":"commandLoaderEnvValueValidation","disposition":{"kind":"ported","id":"environment"},"url":"https://github.com/denoland/deno/blob/336da420f4343cbb1dcbd5eed9d075ff555ed6ee/tests/unit/command_test.ts#L769"} {"record":"unit","id":"deno-command:tests/unit/command_test.ts:793:1:registration","source":"deno-command","unit":"registration","path":"tests/unit/command_test.ts","line":793,"column":1,"label":"commandSyncEnv","disposition":{"kind":"ported","id":"environment"},"url":"https://github.com/denoland/deno/blob/336da420f4343cbb1dcbd5eed9d075ff555ed6ee/tests/unit/command_test.ts#L793"} -{"record":"unit","id":"deno-command:tests/unit/command_test.ts:811:1:registration","source":"deno-command","unit":"registration","path":"tests/unit/command_test.ts","line":811,"column":1,"label":"commandClearEnv","disposition":{"kind":"missing","id":"environment-clear-and-remove"},"url":"https://github.com/denoland/deno/blob/336da420f4343cbb1dcbd5eed9d075ff555ed6ee/tests/unit/command_test.ts#L811"} -{"record":"unit","id":"deno-command:tests/unit/command_test.ts:838:1:registration","source":"deno-command","unit":"registration","path":"tests/unit/command_test.ts","line":838,"column":1,"label":"commandSyncClearEnv","disposition":{"kind":"missing","id":"environment-clear-and-remove"},"url":"https://github.com/denoland/deno/blob/336da420f4343cbb1dcbd5eed9d075ff555ed6ee/tests/unit/command_test.ts#L838"} +{"record":"unit","id":"deno-command:tests/unit/command_test.ts:811:1:registration","source":"deno-command","unit":"registration","path":"tests/unit/command_test.ts","line":811,"column":1,"label":"commandClearEnv","disposition":{"kind":"ported","id":"environment-clear-and-remove"},"url":"https://github.com/denoland/deno/blob/336da420f4343cbb1dcbd5eed9d075ff555ed6ee/tests/unit/command_test.ts#L811"} +{"record":"unit","id":"deno-command:tests/unit/command_test.ts:838:1:registration","source":"deno-command","unit":"registration","path":"tests/unit/command_test.ts","line":838,"column":1,"label":"commandSyncClearEnv","disposition":{"kind":"ported","id":"environment-clear-and-remove"},"url":"https://github.com/denoland/deno/blob/336da420f4343cbb1dcbd5eed9d075ff555ed6ee/tests/unit/command_test.ts#L838"} {"record":"unit","id":"deno-command:tests/unit/command_test.ts:865:1:registration","source":"deno-command","unit":"registration","path":"tests/unit/command_test.ts","line":865,"column":1,"label":"commandUid","disposition":{"kind":"missing","id":"process-credentials"},"url":"https://github.com/denoland/deno/blob/336da420f4343cbb1dcbd5eed9d075ff555ed6ee/tests/unit/command_test.ts#L865"} {"record":"unit","id":"deno-command:tests/unit/command_test.ts:893:1:registration","source":"deno-command","unit":"registration","path":"tests/unit/command_test.ts","line":893,"column":1,"label":"rejectBatAndCmdFiles","disposition":{"kind":"missing","id":"windows-shebang-and-pathext-resolution"},"url":"https://github.com/denoland/deno/blob/336da420f4343cbb1dcbd5eed9d075ff555ed6ee/tests/unit/command_test.ts#L893"} {"record":"unit","id":"deno-command:tests/unit/command_test.ts:924:1:registration","source":"deno-command","unit":"registration","path":"tests/unit/command_test.ts","line":924,"column":1,"label":"commandSyncUid","disposition":{"kind":"missing","id":"process-credentials"},"url":"https://github.com/denoland/deno/blob/336da420f4343cbb1dcbd5eed9d075ff555ed6ee/tests/unit/command_test.ts#L924"} diff --git a/js/tests/sync.test.mjs b/js/tests/sync.test.mjs index e16a8abc..99a916fe 100644 --- a/js/tests/sync.test.mjs +++ b/js/tests/sync.test.mjs @@ -110,9 +110,8 @@ describe('Synchronous Execution (.sync())', () => { cmd.options.capture = false; const result = cmd.sync(); - // capture option doesn't affect sync mode - always captures - // This is different from async mode - expect(result.stdout.trim()).toBe('test'); + expect(result.stdout).toBeUndefined(); + expect(result.stderr).toBeUndefined(); }); test('should handle stdin options', () => { diff --git a/js/tests/types/command-builder.ts b/js/tests/types/command-builder.ts index 25f1c89a..1f79e30e 100644 --- a/js/tests/types/command-builder.ts +++ b/js/tests/types/command-builder.ts @@ -46,3 +46,55 @@ literal.stdout('bad'); // @ts-expect-error Exit-code builders return a number. const invalid: StreamResult = fluent.sync(); void invalid; + +const manyTransforms = literal.pipe( + command.capture(), + command.mirror(false), + command.arg('six'), + command.stdout('ignore'), + command.stderr('ignore'), + command.exitCode, + command.options({ timeout: 100 }), + command.capture(false) +); +const manyResult: number = manyTransforms.sync(); +void manyResult; +// @ts-expect-error Incompatible result types cannot be joined in a pipeline. +literal.pipe(command.exitCode, (builder: CommandBuilder) => builder); +literal.pipe( + command.capture(), + command.mirror(), + command.arg('a'), + command.cwd('.'), + // @ts-expect-error Long pipelines must also check intermediate result types. + command.exitCode, + (builder: CommandBuilder) => builder +); +// @ts-expect-error Long pipelines preserve selection after generic transforms. +const erasedSelection: string = manyTransforms.sync(); +void erasedSelection; + +const twentyTransforms = literal.pipe( + (builder: CommandBuilder) => builder, + command.capture(), + command.capture(), + command.capture(), + command.capture(), + command.capture(), + command.capture(), + command.capture(), + command.capture(), + command.capture(), + command.capture(), + command.capture(), + command.capture(), + command.capture(), + command.capture(), + command.capture(), + command.capture(), + command.capture(), + command.capture(), + command.exitCode +); +const twentiethResult: number = twentyTransforms.sync(); +void twentiethResult; diff --git a/js/types/advanced.d.cts b/js/types/advanced.d.cts index 0bb73a10..878bda9c 100644 --- a/js/types/advanced.d.cts +++ b/js/types/advanced.d.cts @@ -18,6 +18,7 @@ import type { ChildProcess, SpawnOptions } from 'node:child_process'; export type CommandArgument = string | number | bigint | boolean; export type CommandStdio = 'pipe' | 'inherit' | 'ignore'; + export class CommandBuilder implements PromiseLike { constructor( file: string, @@ -65,12 +66,234 @@ export class CommandBuilder implements PromiseLike { d: (builder: CommandBuilder) => CommandBuilder, e: (builder: CommandBuilder) => CommandBuilder ): CommandBuilder; - pipe( - ...transforms: [ - ...Array<(builder: CommandBuilder) => CommandBuilder>, - (builder: CommandBuilder) => CommandBuilder, - ] + pipe( + a: (builder: CommandBuilder) => CommandBuilder, + b: (builder: CommandBuilder) => CommandBuilder, + c: (builder: CommandBuilder) => CommandBuilder, + d: (builder: CommandBuilder) => CommandBuilder, + e: (builder: CommandBuilder) => CommandBuilder, + f: (builder: CommandBuilder) => CommandBuilder + ): CommandBuilder; + pipe( + a: (builder: CommandBuilder) => CommandBuilder, + b: (builder: CommandBuilder) => CommandBuilder, + c: (builder: CommandBuilder) => CommandBuilder, + d: (builder: CommandBuilder) => CommandBuilder, + e: (builder: CommandBuilder) => CommandBuilder, + f: (builder: CommandBuilder) => CommandBuilder, + g: (builder: CommandBuilder) => CommandBuilder + ): CommandBuilder; + pipe( + a: (builder: CommandBuilder) => CommandBuilder, + b: (builder: CommandBuilder) => CommandBuilder, + c: (builder: CommandBuilder) => CommandBuilder, + d: (builder: CommandBuilder) => CommandBuilder, + e: (builder: CommandBuilder) => CommandBuilder, + f: (builder: CommandBuilder) => CommandBuilder, + g: (builder: CommandBuilder) => CommandBuilder, + h: (builder: CommandBuilder) => CommandBuilder + ): CommandBuilder; + pipe( + a: (builder: CommandBuilder) => CommandBuilder, + b: (builder: CommandBuilder) => CommandBuilder, + c: (builder: CommandBuilder) => CommandBuilder, + d: (builder: CommandBuilder) => CommandBuilder, + e: (builder: CommandBuilder) => CommandBuilder, + f: (builder: CommandBuilder) => CommandBuilder, + g: (builder: CommandBuilder) => CommandBuilder, + h: (builder: CommandBuilder) => CommandBuilder, + i: (builder: CommandBuilder) => CommandBuilder + ): CommandBuilder; + pipe( + a: (builder: CommandBuilder) => CommandBuilder, + b: (builder: CommandBuilder) => CommandBuilder, + c: (builder: CommandBuilder) => CommandBuilder, + d: (builder: CommandBuilder) => CommandBuilder, + e: (builder: CommandBuilder) => CommandBuilder, + f: (builder: CommandBuilder) => CommandBuilder, + g: (builder: CommandBuilder) => CommandBuilder, + h: (builder: CommandBuilder) => CommandBuilder, + i: (builder: CommandBuilder) => CommandBuilder, + j: (builder: CommandBuilder) => CommandBuilder + ): CommandBuilder; + pipe( + a: (builder: CommandBuilder) => CommandBuilder, + b: (builder: CommandBuilder) => CommandBuilder, + c: (builder: CommandBuilder) => CommandBuilder, + d: (builder: CommandBuilder) => CommandBuilder, + e: (builder: CommandBuilder) => CommandBuilder, + f: (builder: CommandBuilder) => CommandBuilder, + g: (builder: CommandBuilder) => CommandBuilder, + h: (builder: CommandBuilder) => CommandBuilder, + i: (builder: CommandBuilder) => CommandBuilder, + j: (builder: CommandBuilder) => CommandBuilder, + k: (builder: CommandBuilder) => CommandBuilder + ): CommandBuilder; + pipe( + a: (builder: CommandBuilder) => CommandBuilder, + b: (builder: CommandBuilder) => CommandBuilder, + c: (builder: CommandBuilder) => CommandBuilder, + d: (builder: CommandBuilder) => CommandBuilder, + e: (builder: CommandBuilder) => CommandBuilder, + f: (builder: CommandBuilder) => CommandBuilder, + g: (builder: CommandBuilder) => CommandBuilder, + h: (builder: CommandBuilder) => CommandBuilder, + i: (builder: CommandBuilder) => CommandBuilder, + j: (builder: CommandBuilder) => CommandBuilder, + k: (builder: CommandBuilder) => CommandBuilder, + l: (builder: CommandBuilder) => CommandBuilder + ): CommandBuilder; + pipe( + a: (builder: CommandBuilder) => CommandBuilder, + b: (builder: CommandBuilder) => CommandBuilder, + c: (builder: CommandBuilder) => CommandBuilder, + d: (builder: CommandBuilder) => CommandBuilder, + e: (builder: CommandBuilder) => CommandBuilder, + f: (builder: CommandBuilder) => CommandBuilder, + g: (builder: CommandBuilder) => CommandBuilder, + h: (builder: CommandBuilder) => CommandBuilder, + i: (builder: CommandBuilder) => CommandBuilder, + j: (builder: CommandBuilder) => CommandBuilder, + k: (builder: CommandBuilder) => CommandBuilder, + l: (builder: CommandBuilder) => CommandBuilder, + m: (builder: CommandBuilder) => CommandBuilder + ): CommandBuilder; + pipe( + a: (builder: CommandBuilder) => CommandBuilder, + b: (builder: CommandBuilder) => CommandBuilder, + c: (builder: CommandBuilder) => CommandBuilder, + d: (builder: CommandBuilder) => CommandBuilder, + e: (builder: CommandBuilder) => CommandBuilder, + f: (builder: CommandBuilder) => CommandBuilder, + g: (builder: CommandBuilder) => CommandBuilder, + h: (builder: CommandBuilder) => CommandBuilder, + i: (builder: CommandBuilder) => CommandBuilder, + j: (builder: CommandBuilder) => CommandBuilder, + k: (builder: CommandBuilder) => CommandBuilder, + l: (builder: CommandBuilder) => CommandBuilder, + m: (builder: CommandBuilder) => CommandBuilder, + n: (builder: CommandBuilder) => CommandBuilder + ): CommandBuilder; + pipe( + a: (builder: CommandBuilder) => CommandBuilder, + b: (builder: CommandBuilder) => CommandBuilder, + c: (builder: CommandBuilder) => CommandBuilder, + d: (builder: CommandBuilder) => CommandBuilder, + e: (builder: CommandBuilder) => CommandBuilder, + f: (builder: CommandBuilder) => CommandBuilder, + g: (builder: CommandBuilder) => CommandBuilder, + h: (builder: CommandBuilder) => CommandBuilder, + i: (builder: CommandBuilder) => CommandBuilder, + j: (builder: CommandBuilder) => CommandBuilder, + k: (builder: CommandBuilder) => CommandBuilder, + l: (builder: CommandBuilder) => CommandBuilder, + m: (builder: CommandBuilder) => CommandBuilder, + n: (builder: CommandBuilder) => CommandBuilder, + o: (builder: CommandBuilder) => CommandBuilder + ): CommandBuilder; + pipe( + a: (builder: CommandBuilder) => CommandBuilder, + b: (builder: CommandBuilder) => CommandBuilder, + c: (builder: CommandBuilder) => CommandBuilder, + d: (builder: CommandBuilder) => CommandBuilder, + e: (builder: CommandBuilder) => CommandBuilder, + f: (builder: CommandBuilder) => CommandBuilder, + g: (builder: CommandBuilder) => CommandBuilder, + h: (builder: CommandBuilder) => CommandBuilder, + i: (builder: CommandBuilder) => CommandBuilder, + j: (builder: CommandBuilder) => CommandBuilder, + k: (builder: CommandBuilder) => CommandBuilder, + l: (builder: CommandBuilder) => CommandBuilder, + m: (builder: CommandBuilder) => CommandBuilder, + n: (builder: CommandBuilder) => CommandBuilder, + o: (builder: CommandBuilder) => CommandBuilder, + p: (builder: CommandBuilder) => CommandBuilder

+ ): CommandBuilder

, + q: (builder: CommandBuilder

) => CommandBuilder + ): CommandBuilder; + pipe( + a: (builder: CommandBuilder) => CommandBuilder, + b: (builder: CommandBuilder) => CommandBuilder, + c: (builder: CommandBuilder) => CommandBuilder, + d: (builder: CommandBuilder) => CommandBuilder, + e: (builder: CommandBuilder) => CommandBuilder, + f: (builder: CommandBuilder) => CommandBuilder, + g: (builder: CommandBuilder) => CommandBuilder, + h: (builder: CommandBuilder) => CommandBuilder, + i: (builder: CommandBuilder) => CommandBuilder, + j: (builder: CommandBuilder) => CommandBuilder, + k: (builder: CommandBuilder) => CommandBuilder, + l: (builder: CommandBuilder) => CommandBuilder, + m: (builder: CommandBuilder) => CommandBuilder, + n: (builder: CommandBuilder) => CommandBuilder, + o: (builder: CommandBuilder) => CommandBuilder, + p: (builder: CommandBuilder) => CommandBuilder

, + q: (builder: CommandBuilder

) => CommandBuilder, + r: (builder: CommandBuilder) => CommandBuilder ): CommandBuilder; + pipe( + a: (builder: CommandBuilder) => CommandBuilder, + b: (builder: CommandBuilder) => CommandBuilder, + c: (builder: CommandBuilder) => CommandBuilder, + d: (builder: CommandBuilder) => CommandBuilder, + e: (builder: CommandBuilder) => CommandBuilder, + f: (builder: CommandBuilder) => CommandBuilder, + g: (builder: CommandBuilder) => CommandBuilder, + h: (builder: CommandBuilder) => CommandBuilder, + i: (builder: CommandBuilder) => CommandBuilder, + j: (builder: CommandBuilder) => CommandBuilder, + k: (builder: CommandBuilder) => CommandBuilder, + l: (builder: CommandBuilder) => CommandBuilder, + m: (builder: CommandBuilder) => CommandBuilder, + n: (builder: CommandBuilder) => CommandBuilder, + o: (builder: CommandBuilder) => CommandBuilder, + p: (builder: CommandBuilder) => CommandBuilder

, + q: (builder: CommandBuilder

) => CommandBuilder, + r: (builder: CommandBuilder) => CommandBuilder, + s: (builder: CommandBuilder) => CommandBuilder + ): CommandBuilder; + pipe( + a: (builder: CommandBuilder) => CommandBuilder, + b: (builder: CommandBuilder) => CommandBuilder, + c: (builder: CommandBuilder) => CommandBuilder, + d: (builder: CommandBuilder) => CommandBuilder, + e: (builder: CommandBuilder) => CommandBuilder, + f: (builder: CommandBuilder) => CommandBuilder, + g: (builder: CommandBuilder) => CommandBuilder, + h: (builder: CommandBuilder) => CommandBuilder, + i: (builder: CommandBuilder) => CommandBuilder, + j: (builder: CommandBuilder) => CommandBuilder, + k: (builder: CommandBuilder) => CommandBuilder, + l: (builder: CommandBuilder) => CommandBuilder, + m: (builder: CommandBuilder) => CommandBuilder, + n: (builder: CommandBuilder) => CommandBuilder, + o: (builder: CommandBuilder) => CommandBuilder, + p: (builder: CommandBuilder) => CommandBuilder

, + q: (builder: CommandBuilder

) => CommandBuilder, + r: (builder: CommandBuilder) => CommandBuilder, + s: (builder: CommandBuilder) => CommandBuilder, + t: (builder: CommandBuilder) => CommandBuilder + ): CommandBuilder; + pipe( + ...transforms: Array<(builder: CommandBuilder) => CommandBuilder> + ): CommandBuilder; runner(options?: ProcessOptions): ProcessRunner; run(options?: ProcessOptions): Promise; sync(options?: ProcessOptions): T; diff --git a/rust/README.md b/rust/README.md index 0f61061b..9b833c0f 100644 --- a/rust/README.md +++ b/rust/README.md @@ -17,6 +17,8 @@ The shared advanced APIs include `CommandBuilder`, `DataStream`, `terminal::TerminalBuffer`, and `CommandEnvironment`. See the [advanced workflows guide](../js/docs/ADVANCED-WORKFLOWS.md#rust-equivalents) and `examples/language_features.rs` for executable examples. +[Compare the complete Rust APIs](docs/COMPARISON.md) with all fourteen projects +in the pinned competitor audit, including their specialized capabilities. [Source translation and competitor migration](../js/docs/FY.md) use the same options in both implementations. diff --git a/rust/changelog.d/20261024_103000_command_builder.md b/rust/changelog.d/20261024_103000_command_builder.md index 0470a620..834a8d36 100644 --- a/rust/changelog.d/20261024_103000_command_builder.md +++ b/rust/changelog.d/20261024_103000_command_builder.md @@ -6,3 +6,4 @@ bump: minor - Complete reusable command builders with environment layers and removal, functional configuration and synchronous execution alongside async execution. + Preserve native environment key rules by applying changes in configuration order. diff --git a/rust/docs/COMPARISON.md b/rust/docs/COMPARISON.md new file mode 100644 index 00000000..c1a832ab --- /dev/null +++ b/rust/docs/COMPARISON.md @@ -0,0 +1,80 @@ +# Rust command execution comparison + +This comparison covers all fourteen projects in the [pinned Rust +audit](COMPETITOR_TEST_AUDIT.md), using their linked public documentation and +the audited source revisions. Reviewed 2026-10-24. It covers execution, output, +configuration, process control, shell syntax, testing and development tools. +The [JavaScript comparison](../../js/docs/COMPARISON.md) covers the separate +JavaScript APIs; a JavaScript compatibility import is not a Rust implementation. + +## Construction, execution and output + +| Project / primary reference | Construction and execution | I/O and composition | Configuration and lifecycle | +| ------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------- | +| command-stream | Reusable owned `CommandBuilder`, `.arg/.args`, functional `.pipe`, async `.run` and `.run_sync`; separate macros and runners | Builder collects text; `StreamingRunner` yields bytes; `Pipeline` connects processes; Execa module provides byte results | Builder cwd, native environment, clear/remove and three stdio modes; runner exposes cancellation, deadlines and process cleanup | +| [std::process](https://doc.rust-lang.org/std/process/struct.Command.html) | Reusable mutable native argv builder; `output`, `status`, `spawn` | Byte results, native handles; construct pipelines using handles | Native cwd/env/clear/remove, platform extension traits, child wait/try_wait/kill and native ExitStatus | +| [Tokio process](https://docs.rs/tokio/latest/tokio/process/index.html) | Native-style command builder, async status/output/wait | Async native stdin/stdout/stderr and byte output; compose with Tokio I/O | Native configuration and platform extensions, kill_on_drop; deadlines via Tokio time primitives | +| [async-process](https://github.com/smol-rs/async-process) | Async native-style command builder, output/status/spawn | Async I/O traits, byte results and line reading through buffered readers | Native configuration; background reaper; child lifecycle controls | +| [duct](https://github.com/oconnor663/duct.rs) | Reusable command expressions; run/read/start; nonzero exits checked by default | Expression pipelines, redirects, stderr merging, incremental readers | cwd/env/clear/remove, handles and unchecked status policy | +| [subprocess](https://github.com/hniksic/rust-subprocess) | Exec and pipeline builders; join/capture/start | OS pipelines, arbitrary reader/byte input, merged streams, concurrent communicate with size/time limits | cwd/env, redirects, process groups, nonblocking polling and shared process handles | +| [xshell](https://github.com/matklad/xshell) | Shell context and `cmd!` macro with scalar/collection interpolation; run/read/output | Captured output and pipe input; scripting and filesystem helpers | Context cwd/env, per-command settings, checked execution and deadline support | +| [rust_cmd_lib](https://github.com/rust-shell-script/rust_cmd_lib) | Compile-time command macros, vector arguments, command groups and custom functions | Pipelines, redirections, captured function results and background process groups | Scoped cwd/env, logging, checked pipeline execution; no external shell required for macro grammar | +| [run_script](https://github.com/sagiegurari/run_script) | Script text plus args/options, run/spawn functions and macros | Script output or native spawned child streams; grammar supplied by an interpreter | Interpreter and script options; native child lifecycle via spawn | +| [rust-shell](https://github.com/google/rust-shell) | ShellCommand macros, checked run, native child and thread helpers | UTF-8 convenience capture and native process output | Environment expansion and signals; upstream documents Unix-only support | +| [shellfn](https://github.com/synek317/shellfn) | Attribute macros turn scripts into Rust functions | Arguments mapped through environment; output converted into declared Rust return types | Interpreter/script attributes and domain-specific parsing/error conversion | +| [bkt](https://github.com/dimo414/bkt) | Command invocation with persistent caching | Cached stdout, stderr and status | TTL, stale refresh and cache keys incorporating selected command state; a different execution policy from always running a description | +| [assert_cmd](https://github.com/assert-rs/assert_cmd) | Native command wrapper for CLI integration tests and Cargo binary discovery | Buffered stdin/output, predicates and readable assertion failures | Native command settings, deadlines and test-oriented status checks | +| [rexpect](https://github.com/rust-cli/rexpect) | Interactive sessions and shell helpers | PTY send/read, string/regex/EOF expectations and prompt synchronization | Session/read deadlines and interactive signal handling | +| [expectrl](https://github.com/zhiburt/expectrl) | Interactive session API with optional async support | Pattern expectations, logging, control codes and interactive forwarding | Session controls and timeouts; upstream documents Windows support | + +The table names each library's useful surface, not interchangeable signatures. +For example, duct's `.pipe` connects expressions; CommandBuilder's `.pipe` +applies a configuration transform. The latter's process pipelines use a runner +or `Pipeline`. Nonzero exit policies also differ: CommandBuilder returns status; +call `error_for_status()` when failure should become an error. + +## Builder guarantees and boundaries + +`CommandBuilder` executes native `OsString` executable names, arguments and +environment keys/values. Configuration is owned and cloning preserves a saved +description. Environment changes are replayed in order through native Command, +including Windows case-insensitive names. Async and sync executions share the +same native configuration. Buffered text stdin is written while stdout/stderr +are drained, including input larger than a pipe buffer. + +| Requirement | Public command-stream surface and verification | +| ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ | +| Literal argv, configuration transforms, repeated sync/async runs | [Builder tests](../tests/command_builder.rs), [builder source](../src/command_builder.rs) | +| Empty/hostile arguments, cwd, environment replacement/removal and platform key rules | [Portable compatibility tests](../tests/competitor_compatibility/behavior.rs) | +| Byte input and lossless collected output, richer process options | [Rust Execa guide](../../js/docs/EXECA_MIGRATION.md#rust); core builder stdin/results remain text | +| Streaming, cancellation, deadlines, signals and descendants | [Rust guide](../README.md#streaming), `StreamingRunner`, ProcessRunner and RunOptions; these are separate from builder configuration | +| Arbitrary caller-supplied stdio handles or native child inspection | Use native std/Tokio process APIs; the builder has fixed pipe/inherit/ignore output policies and returns CommandResult | +| Array/splat macros and typed expression pipelines | Builder `.args` accepts iterators; core macros and pipelines have their own grammar and do not implement xshell/cmd_lib/duct macros | +| Pattern-based interactive PTY sessions | Terminal capture records scripted sessions; it does not implement rexpect/expectrl's expect protocol | +| Persistent subprocess cache or script-to-domain return conversion | Application code or specialized bkt/shellfn APIs; running a builder always executes the command | + +`.shell_string()` renders POSIX argv, omitting execution configuration. It uses +lossy text conversion for non-UTF-8 native values; execute the builder to retain +native bytes. It is not a Windows command-line serializer. + +## Features beyond the process builder + +The combined command, stream and tooling APIs are the main breadth advantage. +Native process crates remain useful for direct OS control; specialized crates +offer contracts beyond this builder. + +| Area | command-stream evidence | Corresponding competitor approach | +| ------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | +| Shell parsing, redirects and portable built-ins | [Rust guide](../README.md), [generated feature catalog](../../docs/README.md) | cmd_lib macro grammar; xshell scripting helpers; run_script/shellfn host interpreters; native runtimes require application composition | +| Custom commands, middleware and command environments | [Advanced workflows](../../js/docs/ADVANCED-WORKFLOWS.md#rust-equivalents) | cmd_lib custom command functions; ordinary Rust functions around the other APIs | +| Async transforms, batching, windows, bounded queues and statistics | [Stream processing guide](../../docs/stream-processing-guide.md) | Tokio/async-process I/O plus stream combinators; duct/subprocess readers plus application transforms | +| ANSI processing, terminal snapshots and recording | [Feature catalog](../../docs/README.md) | rexpect/expectrl specialize in interactive expectations; native APIs supply the underlying I/O | +| Shell translation, examples and development workflows | [Translation guide](../../js/docs/FY.md), [native Cargo watching and Evcxr REPL](../../js/docs/ADVANCED-WORKFLOWS.md#rust-equivalents) | Script macros and integration with external development tools; browser/VS Code tools belong to the JavaScript package | +| CLI assertions and mocks | [Portable integration tests](../tests/competitor_compatibility/behavior.rs) use ordinary Rust assertions; the JS mock/snapshot helper API is separate | assert_cmd provides Cargo-aware command discovery and predicate assertions; expect libraries test interactive protocols | +| Performance and memory | [Rust benchmark guide](../benchmarks/README.md) | Compare the same output, workload, platform and build configuration before making a performance claim | + +The audit accounts for every test unit in its published discovery set. It does +not establish superiority over every open-source project or full conformance to +every upstream API. The missing-contract entries identify specialized behavior +and verification limits; passing a portable invariant does not prove all of a +competitor's implementation or platform contracts. diff --git a/rust/docs/COMPETITOR_TEST_AUDIT.md b/rust/docs/COMPETITOR_TEST_AUDIT.md index 9487218c..eb320015 100644 --- a/rust/docs/COMPETITOR_TEST_AUDIT.md +++ b/rust/docs/COMPETITOR_TEST_AUDIT.md @@ -71,8 +71,8 @@ assumptions. Copying their files byte-for-byte would mostly test those crates. Instead, every scoped test belongs to one disposition: 1. A portable public process invariant is adapted into the executable suite. -2. A public invariant needs a feature command-stream does not have and is in - the missing-feature ledger below. +2. A public upstream contract is not fully ported and is recorded in the + remaining-contract ledger below, with its current implementation status. 3. A test verifies competitor API shape, private internals, unrelated helpers, upstream runtime behavior, or harness mechanics and is inapplicable. @@ -95,36 +95,41 @@ inventory total, and ensure all fourteen selected projects are represented. ## Executable behavior ports -| Case ID | Public invariant | -| ----------------------------- | ------------------------------------------------------------------------------------------ | -| `direct-exact-argv` | Direct execution bypasses shell tokenization and preserves argument boundaries. | -| `argument-edge-cases` | Empty, whitespace, multiline, Unicode, quote, and metacharacter arguments survive exactly. | -| `safe-template-interpolation` | On POSIX shells, an untrusted interpolation remains one literal argument. | -| `cwd-string` | An explicit working directory reaches the child. | -| `environment` | Explicit environment values reach the child without corruption. | -| `stdout-stderr-separation` | Standard output and error remain separate. | -| `newline-preservation` | Captured output preserves final and repeated newlines. | -| `unicode-output` | UTF-8 output is captured without loss. | -| `large-output` | One MiB is captured without truncation or deadlock. | -| `nonzero-exit` | A non-zero child status is returned, and reads through `code` and `exit_code`. | -| `stdin-string` | String input is written completely and stdin closes. | -| `lazy-execution` | Constructing a runner does not spawn it. | -| `concurrent-execution` | Concurrent children keep results isolated. | -| `streamed-before-exit` | Output arrives before a delayed child exits. | -| `sync-execution` | Synchronous execution returns captured output and status. | -| `programmatic-pipeline` | One process's stdout becomes the next process's stdin. | -| `spawn-error-propagation` | An unavailable exact executable returns an error, never a false success. | -| `stream-kill` | Killing an active stream terminates the child and reports termination. | +| Case ID | Public invariant | +| ------------------------------ | ------------------------------------------------------------------------------------------- | +| `direct-exact-argv` | Direct execution bypasses shell tokenization and preserves argument boundaries. | +| `argument-edge-cases` | Empty, whitespace, multiline, Unicode, quote, and metacharacter arguments survive exactly. | +| `safe-template-interpolation` | On POSIX shells, an untrusted interpolation remains one literal argument. | +| `cwd-string` | An explicit working directory reaches the child. | +| `environment` | Explicit environment values reach the child without corruption. | +| `environment-clear-and-remove` | Builder clear/removal, ordering and native platform key rules hold for sync and async runs. | +| `stdout-stderr-separation` | Standard output and error remain separate. | +| `newline-preservation` | Captured output preserves final and repeated newlines. | +| `unicode-output` | UTF-8 output is captured without loss. | +| `large-output` | One MiB is captured without truncation or deadlock. | +| `nonzero-exit` | A non-zero child status is returned, and reads through `code` and `exit_code`. | +| `stdin-string` | String input is written completely and stdin closes. | +| `lazy-execution` | Constructing a runner does not spawn it. | +| `concurrent-execution` | Concurrent children keep results isolated. | +| `streamed-before-exit` | Output arrives before a delayed child exits. | +| `sync-execution` | Synchronous execution returns captured output and status. | +| `programmatic-pipeline` | One process's stdout becomes the next process's stdin. | +| `spawn-error-propagation` | An unavailable exact executable returns an error, never a false success. | +| `stream-kill` | Killing an active stream terminates the child and reports termination. | Existing focused Rust tests continue to cover shell parsing, virtual commands, signals, process groups, terminal capture, redirection, state, and built-ins in greater depth. -## Missing-feature ledger +## Remaining upstream contracts -These are comparison inputs for later work, not promises that every competitor -choice fits command-stream. A future implementation should start with a failing -focused test and update the corpus in the same change. +Stable `missing` disposition IDs below identify contracts not completely ported +by this corpus. Current implementations in a separate runner or compatibility +module are described where applicable; they do not automatically prove every +upstream contract. See the [complete Rust comparison](COMPARISON.md) for API +selection and the [JavaScript comparison](../../js/docs/COMPARISON.md) for the +separate JavaScript surfaces. The ten environment-clear/removal units now have +an executable builder invariant and are ported in both exact ledgers. ### array-and-splat-interpolation @@ -136,20 +141,19 @@ display values only. The native, Tokio, async-process, assert_cmd, and subprocess APIs accept platform `OsStr` values throughout. `StreamingRunner::from_argv` accepts native -arguments, but command-stream's environment and captured result APIs require -UTF-8 strings, so it cannot preserve this contract end to end. +arguments, and CommandBuilder now accepts native executable, argument and +environment values. Core RunOptions environment and collected text results +still require UTF-8; use the builder for native env and the Execa byte result +API for lossless collection. The complete lower-level upstream contracts are +not inferred from those separate surfaces. ### binary-input-and-lossless-output Several competitors accept byte input and return raw output bytes. command-stream streaming chunks are bytes, but collected output and configured -stdin are strings and use lossy UTF-8 decoding. - -### environment-clear-and-remove - -Native process builders and several wrappers can clear the inherited -environment or remove one variable. command-stream can add or replace values -but has no clear/remove policy. +stdin are strings and use lossy UTF-8 decoding in the core builder/runner. +The separate Rust Execa module accepts and collects bytes; it is documented in +the [Execa guide](../../js/docs/EXECA_MIGRATION.md#rust). ### custom-stdio-and-file-handles @@ -166,7 +170,10 @@ command-stream currently captures without a configurable maximum-buffer policy. assert_cmd, xshell, run_script, rexpect, and expectrl expose deadlines in their assertion, script, or session layers. command-stream can be cancelled through -its stream handle but has no per-command timeout option. +its stream handle, and ProcessRunner's `RunOptions::timeout` supplies a +per-command deadline with configured termination signal/grace. CommandBuilder +does not expose that runner option, and a process deadline does not implement +an expect session's per-read deadline. Those contracts remain separate. ### try-wait-and-shared-child-handle diff --git a/rust/src/command_builder.rs b/rust/src/command_builder.rs index b62cf206..a3a04475 100644 --- a/rust/src/command_builder.rs +++ b/rust/src/command_builder.rs @@ -1,6 +1,5 @@ //! Lazy exact-argv command descriptions, parallel to JavaScript `$.command`. use crate::{CommandResult, Result, StdinOption}; -use std::collections::HashMap; use std::ffi::OsString; use std::path::PathBuf; use std::process::Stdio; @@ -30,7 +29,9 @@ pub struct CommandBuilder { pub file: OsString, pub arguments: Vec, cwd: Option, - env: HashMap>, + // Replay changes in order so native Command applies platform-specific + // key equality (notably case-insensitive environment names on Windows). + env: Vec<(OsString, Option)>, clear_env: bool, stdout: CommandStdio, stderr: CommandStdio, @@ -43,7 +44,7 @@ impl CommandBuilder { file: file.into(), arguments: Vec::new(), cwd: None, - env: HashMap::new(), + env: Vec::new(), clear_env: false, stdout: CommandStdio::Pipe, stderr: CommandStdio::Pipe, @@ -70,7 +71,7 @@ impl CommandBuilder { self } pub fn env(mut self, key: impl Into, value: impl Into) -> Self { - self.env.insert(key.into(), Some(value.into())); + self.env.push((key.into(), Some(value.into()))); self } pub fn envs(mut self, variables: I) -> Self @@ -85,7 +86,7 @@ impl CommandBuilder { self } pub fn env_remove(mut self, key: impl Into) -> Self { - self.env.insert(key.into(), None); + self.env.push((key.into(), None)); self } pub fn env_clear(mut self) -> Self { diff --git a/rust/tests/competitor_compatibility.rs b/rust/tests/competitor_compatibility.rs index 86a871bb..4823d607 100644 --- a/rust/tests/competitor_compatibility.rs +++ b/rust/tests/competitor_compatibility.rs @@ -264,7 +264,7 @@ fn every_selected_project_and_case_is_accounted_for() { } assert_eq!(referenced, known); - assert_eq!(PORTED_CASES.len(), 18); + assert_eq!(PORTED_CASES.len(), 19); assert_eq!( PORTED_CASES .iter() @@ -272,7 +272,7 @@ fn every_selected_project_and_case_is_accounted_for() { .collect::>(), BEHAVIOR_CASE_IDS.iter().copied().collect::>() ); - assert_eq!(MISSING_FEATURES.len(), 13); + assert_eq!(MISSING_FEATURES.len(), 12); assert_eq!(EXCLUDED_TEST_CLASSES.len(), 5); for class in EXCLUDED_TEST_CLASSES { assert!(!class.id.is_empty()); diff --git a/rust/tests/competitor_compatibility/behavior.rs b/rust/tests/competitor_compatibility/behavior.rs index 389ab2b5..74350cc5 100644 --- a/rust/tests/competitor_compatibility/behavior.rs +++ b/rust/tests/competitor_compatibility/behavior.rs @@ -1,9 +1,8 @@ -#[cfg(not(windows))] use super::support::fixture_path; use super::support::{decode_hex_lines, fixture_runner, hex, run_fixture, shell_fixture_command}; #[cfg(not(windows))] use command_stream::cmd; -use command_stream::{run_sync, OutputChunk, Pipeline, StreamingRunner}; +use command_stream::{run_sync, CommandBuilder, OutputChunk, Pipeline, StreamingRunner}; use std::collections::HashMap; use std::ffi::OsString; use std::time::Duration; @@ -30,6 +29,7 @@ pub const BEHAVIOR_CASE_IDS: &[&str] = &[ "safe-template-interpolation", "cwd-string", "environment", + "environment-clear-and-remove", "stdout-stderr-separation", "newline-preservation", "unicode-output", @@ -131,6 +131,44 @@ async fn environment_passes_explicit_values_to_the_child() { assert_eq!(decode_hex_lines(&result.stdout), ["one", "hΓ©llΓΈβ˜ƒ"]); } +#[tokio::test] +async fn builder_environment_clear_remove_and_native_key_rules() { + let parent_key = std::env::vars_os() + .map(|(key, _)| key) + .find(|key| key != "COMMAND_STREAM_ENV_TEST" && key != "command_stream_env_test") + .expect("the test runner has an inherited environment"); + let builder = || { + CommandBuilder::new(fixture_path()) + .arg("env-state") + .arg(&parent_key) + .args(["COMMAND_STREAM_ENV_TEST", "command_stream_env_test"]) + .env("DISCARDED_BEFORE_CLEAR", "discarded") + .env_clear() + .env("COMMAND_STREAM_ENV_TEST", "first") + .env("command_stream_env_test", "second") + .env_remove("COMMAND_STREAM_ENV_TEST") + }; + // On Windows environment names are case-insensitive; on Unix they are + // distinct. Preserve native ordering and key comparison on both platforms. + let expected = if cfg!(windows) { + "absent\nabsent\nabsent\n" + } else { + "absent\nabsent\npresent:7365636f6e64\n" + }; + for _ in 0..16 { + let command = builder(); + assert_eq!(command.run().await.unwrap().stdout, expected); + assert_eq!(command.run_sync().unwrap().stdout, expected); + } + let restored = builder().env("COMMAND_STREAM_ENV_TEST", "restored"); + assert!(restored + .run() + .await + .unwrap() + .stdout + .contains("726573746f726564")); +} + #[tokio::test] async fn stdout_and_stderr_are_captured_separately() { for (stdout, stderr) in [ diff --git a/rust/tests/competitor_compatibility/corpus.rs b/rust/tests/competitor_compatibility/corpus.rs index c1ff39c3..d5cf6f5a 100644 --- a/rust/tests/competitor_compatibility/corpus.rs +++ b/rust/tests/competitor_compatibility/corpus.rs @@ -190,6 +190,22 @@ pub const COMPETITORS: &[Competitor] = &[ ]; pub const PORTED_CASES: &[CorpusEntry] = &[ + CorpusEntry { + id: "environment-clear-and-remove", + competitors: &[ + "rust-std-process", + "tokio-process", + "async-process", + "assert-cmd", + "duct", + "xshell", + "subprocess", + ], + upstream: &[ + "tests/ui/process/process-remove-from-env.rs", + "tests/std.rs", + ], + }, CorpusEntry { id: "direct-exact-argv", competitors: &[ @@ -516,22 +532,6 @@ pub const MISSING_FEATURES: &[CorpusEntry] = &[ ], upstream: &["tests/std.rs", "src/test.rs", "src/tests/communicate.rs"], }, - CorpusEntry { - id: "environment-clear-and-remove", - competitors: &[ - "rust-std-process", - "tokio-process", - "async-process", - "assert-cmd", - "duct", - "xshell", - "subprocess", - ], - upstream: &[ - "tests/ui/process/process-remove-from-env.rs", - "tests/std.rs", - ], - }, CorpusEntry { id: "custom-stdio-and-file-handles", competitors: &[ diff --git a/rust/tests/competitor_compatibility/fixture.rs b/rust/tests/competitor_compatibility/fixture.rs index fbfa4e12..6ef96700 100644 --- a/rust/tests/competitor_compatibility/fixture.rs +++ b/rust/tests/competitor_compatibility/fixture.rs @@ -24,6 +24,14 @@ fn main() { println!("{}", hex(env::var(&key).unwrap_or_default().as_bytes())); } } + "env-state" => { + for key in args { + match env::var_os(key) { + Some(value) => println!("present:{}", hex(value.to_string_lossy().as_bytes())), + None => println!("absent"), + } + } + } "output" => { let stdout = args.next().unwrap_or_default(); let stderr = args.next().unwrap_or_default(); diff --git a/rust/tests/competitor_decisions.jsonl b/rust/tests/competitor_decisions.jsonl index a8ae7200..8d8644a9 100644 --- a/rust/tests/competitor_decisions.jsonl +++ b/rust/tests/competitor_decisions.jsonl @@ -24,7 +24,7 @@ {"id":"rust-std-process:library/std/src/process/tests.rs:501:1:registration","disposition":{"kind":"ported","id":"environment"}} {"id":"rust-std-process:library/std/src/process/tests.rs:512:1:registration","disposition":{"kind":"ported","id":"environment"}} {"id":"rust-std-process:library/std/src/process/tests.rs:524:1:registration","disposition":{"kind":"inapplicable","id":"competitor-api-shape"}} -{"id":"rust-std-process:library/std/src/process/tests.rs:533:1:registration","disposition":{"kind":"missing","id":"environment-clear-and-remove"}} +{"id":"rust-std-process:library/std/src/process/tests.rs:533:1:registration","disposition":{"kind":"ported","id":"environment-clear-and-remove"}} {"id":"rust-std-process:library/std/src/process/tests.rs:539:1:registration","disposition":{"kind":"inapplicable","id":"competitor-api-shape"}} {"id":"rust-std-process:library/std/src/process/tests.rs:680:1:registration","disposition":{"kind":"missing","id":"shell-expression-composition-and-redirection"}} {"id":"rust-std-process:library/std/src/process/tests.rs:699:1:registration","disposition":{"kind":"missing","id":"shell-expression-composition-and-redirection"}} @@ -50,7 +50,7 @@ {"id":"rust-std-process:tests/ui/process/process-envs.rs:1:1:file","disposition":{"kind":"inapplicable","id":"upstream-runtime-regressions"}} {"id":"rust-std-process:tests/ui/process/process-exit.rs:1:1:file","disposition":{"kind":"inapplicable","id":"upstream-runtime-regressions"}} {"id":"rust-std-process:tests/ui/process/process-panic-after-fork.rs:1:1:file","disposition":{"kind":"inapplicable","id":"upstream-runtime-regressions"}} -{"id":"rust-std-process:tests/ui/process/process-remove-from-env.rs:1:1:file","disposition":{"kind":"missing","id":"environment-clear-and-remove"}} +{"id":"rust-std-process:tests/ui/process/process-remove-from-env.rs:1:1:file","disposition":{"kind":"ported","id":"environment-clear-and-remove"}} {"id":"rust-std-process:tests/ui/process/process-sigpipe.rs:1:1:file","disposition":{"kind":"missing","id":"custom-stdio-and-file-handles"}} {"id":"rust-std-process:tests/ui/process/process-spawn-failure.rs:1:1:file","disposition":{"kind":"ported","id":"spawn-error-propagation"}} {"id":"rust-std-process:tests/ui/process/process-spawn-nonexistent.rs:1:1:file","disposition":{"kind":"ported","id":"spawn-error-propagation"}} @@ -94,7 +94,7 @@ {"id":"async-process:tests/std.rs:245:1:registration","disposition":{"kind":"missing","id":"try-wait-and-shared-child-handle"}} {"id":"async-process:tests/std.rs:257:1:registration","disposition":{"kind":"missing","id":"try-wait-and-shared-child-handle"}} {"id":"async-process:tests/std.rs:270:1:registration","disposition":{"kind":"missing","id":"try-wait-and-shared-child-handle"}} -{"id":"async-process:tests/std.rs:319:1:registration","disposition":{"kind":"missing","id":"environment-clear-and-remove"}} +{"id":"async-process:tests/std.rs:319:1:registration","disposition":{"kind":"ported","id":"environment-clear-and-remove"}} {"id":"async-process:tests/std.rs:341:1:registration","disposition":{"kind":"ported","id":"environment"}} {"id":"async-process:tests/std.rs:358:1:registration","disposition":{"kind":"ported","id":"environment"}} {"id":"async-process:tests/std.rs:383:1:registration","disposition":{"kind":"ported","id":"stream-kill"}} @@ -112,7 +112,7 @@ {"id":"assert-cmd:tests/assert.rs:81:1:registration","disposition":{"kind":"ported","id":"stdout-stderr-separation"}} {"id":"assert-cmd:tests/assert.rs:119:1:registration","disposition":{"kind":"ported","id":"stdout-stderr-separation"}} {"id":"assert-cmd:tests/cargo.rs:6:1:registration","disposition":{"kind":"missing","id":"binary-input-and-lossless-output"}} -{"id":"assert-cmd:tests/cargo.rs:13:1:registration","disposition":{"kind":"missing","id":"environment-clear-and-remove"}} +{"id":"assert-cmd:tests/cargo.rs:13:1:registration","disposition":{"kind":"ported","id":"environment-clear-and-remove"}} {"id":"assert-cmd:tests/cargo.rs:20:1:registration","disposition":{"kind":"inapplicable","id":"competitor-api-shape"}} {"id":"assert-cmd:tests/cargo.rs:41:1:registration","disposition":{"kind":"inapplicable","id":"competitor-api-shape"}} {"id":"assert-cmd:tests/cargo.rs:50:1:registration","disposition":{"kind":"inapplicable","id":"competitor-api-shape"}} @@ -141,9 +141,9 @@ {"id":"duct:src/test.rs:320:1:registration","disposition":{"kind":"ported","id":"stdout-stderr-separation"}} {"id":"duct:src/test.rs:333:1:registration","disposition":{"kind":"ported","id":"cwd-string"}} {"id":"duct:src/test.rs:361:1:registration","disposition":{"kind":"ported","id":"environment"}} -{"id":"duct:src/test.rs:370:1:registration","disposition":{"kind":"missing","id":"environment-clear-and-remove"}} -{"id":"duct:src/test.rs:397:1:registration","disposition":{"kind":"missing","id":"environment-clear-and-remove"}} -{"id":"duct:src/test.rs:417:1:registration","disposition":{"kind":"missing","id":"environment-clear-and-remove"}} +{"id":"duct:src/test.rs:370:1:registration","disposition":{"kind":"ported","id":"environment-clear-and-remove"}} +{"id":"duct:src/test.rs:397:1:registration","disposition":{"kind":"ported","id":"environment-clear-and-remove"}} +{"id":"duct:src/test.rs:417:1:registration","disposition":{"kind":"ported","id":"environment-clear-and-remove"}} {"id":"duct:src/test.rs:455:1:registration","disposition":{"kind":"ported","id":"environment"}} {"id":"duct:src/test.rs:579:1:registration","disposition":{"kind":"missing","id":"custom-stdio-and-file-handles"}} {"id":"duct:src/test.rs:588:1:registration","disposition":{"kind":"inapplicable","id":"competitor-internals"}} @@ -173,7 +173,7 @@ {"id":"xshell:tests/it/compile_failures.rs:109:1:registration","disposition":{"kind":"ported","id":"safe-template-interpolation"}} {"id":"xshell:tests/it/compile_failures.rs:119:1:registration","disposition":{"kind":"missing","id":"array-and-splat-interpolation"}} {"id":"xshell:tests/it/env.rs:6:1:registration","disposition":{"kind":"ported","id":"environment"}} -{"id":"xshell:tests/it/env.rs:58:1:registration","disposition":{"kind":"missing","id":"environment-clear-and-remove"}} +{"id":"xshell:tests/it/env.rs:58:1:registration","disposition":{"kind":"ported","id":"environment-clear-and-remove"}} {"id":"xshell:tests/it/main.rs:30:1:registration","disposition":{"kind":"ported","id":"direct-exact-argv"}} {"id":"xshell:tests/it/main.rs:39:1:registration","disposition":{"kind":"inapplicable","id":"competitor-api-shape"}} {"id":"xshell:tests/it/main.rs:47:1:registration","disposition":{"kind":"ported","id":"direct-exact-argv"}} @@ -391,8 +391,8 @@ {"id":"subprocess:src/tests/posix.rs:74:1:registration","disposition":{"kind":"ported","id":"stream-kill"}} {"id":"subprocess:src/tests/posix.rs:83:1:registration","disposition":{"kind":"missing","id":"try-wait-and-shared-child-handle"}} {"id":"subprocess:src/tests/posix.rs:98:1:registration","disposition":{"kind":"missing","id":"native-exit-status-and-signal-metadata"}} -{"id":"subprocess:src/tests/posix.rs:106:1:registration","disposition":{"kind":"missing","id":"environment-clear-and-remove"}} -{"id":"subprocess:src/tests/posix.rs:118:1:registration","disposition":{"kind":"missing","id":"environment-clear-and-remove"}} +{"id":"subprocess:src/tests/posix.rs:106:1:registration","disposition":{"kind":"ported","id":"environment-clear-and-remove"}} +{"id":"subprocess:src/tests/posix.rs:118:1:registration","disposition":{"kind":"ported","id":"environment-clear-and-remove"}} {"id":"subprocess:src/tests/posix.rs:132:1:registration","disposition":{"kind":"missing","id":"custom-stdio-and-file-handles"}} {"id":"subprocess:src/tests/posix.rs:147:1:registration","disposition":{"kind":"missing","id":"native-exit-status-and-signal-metadata"}} {"id":"subprocess:src/tests/posix.rs:162:1:registration","disposition":{"kind":"missing","id":"native-exit-status-and-signal-metadata"}} diff --git a/rust/tests/competitor_dispositions.jsonl b/rust/tests/competitor_dispositions.jsonl index 236e694a..6e7b2bf3 100644 --- a/rust/tests/competitor_dispositions.jsonl +++ b/rust/tests/competitor_dispositions.jsonl @@ -38,7 +38,7 @@ {"record":"unit","id":"rust-std-process:library/std/src/process/tests.rs:501:1:registration","source":"rust-std-process","unit":"registration","path":"library/std/src/process/tests.rs","line":501,"column":1,"label":"test_interior_nul_in_env_key_is_error","disposition":{"kind":"ported","id":"environment"},"url":"https://github.com/rust-lang/rust/blob/24d472027454741e74f8e913755fbc7e03f02af5/library/std/src/process/tests.rs#L501"} {"record":"unit","id":"rust-std-process:library/std/src/process/tests.rs:512:1:registration","source":"rust-std-process","unit":"registration","path":"library/std/src/process/tests.rs","line":512,"column":1,"label":"test_interior_nul_in_env_value_is_error","disposition":{"kind":"ported","id":"environment"},"url":"https://github.com/rust-lang/rust/blob/24d472027454741e74f8e913755fbc7e03f02af5/library/std/src/process/tests.rs#L512"} {"record":"unit","id":"rust-std-process:library/std/src/process/tests.rs:524:1:registration","source":"rust-std-process","unit":"registration","path":"library/std/src/process/tests.rs","line":524,"column":1,"label":"test_command_implements_send_sync","disposition":{"kind":"inapplicable","id":"competitor-api-shape"},"url":"https://github.com/rust-lang/rust/blob/24d472027454741e74f8e913755fbc7e03f02af5/library/std/src/process/tests.rs#L524"} -{"record":"unit","id":"rust-std-process:library/std/src/process/tests.rs:533:1:registration","source":"rust-std-process","unit":"registration","path":"library/std/src/process/tests.rs","line":533,"column":1,"label":"env_empty","disposition":{"kind":"missing","id":"environment-clear-and-remove"},"url":"https://github.com/rust-lang/rust/blob/24d472027454741e74f8e913755fbc7e03f02af5/library/std/src/process/tests.rs#L533"} +{"record":"unit","id":"rust-std-process:library/std/src/process/tests.rs:533:1:registration","source":"rust-std-process","unit":"registration","path":"library/std/src/process/tests.rs","line":533,"column":1,"label":"env_empty","disposition":{"kind":"ported","id":"environment-clear-and-remove"},"url":"https://github.com/rust-lang/rust/blob/24d472027454741e74f8e913755fbc7e03f02af5/library/std/src/process/tests.rs#L533"} {"record":"unit","id":"rust-std-process:library/std/src/process/tests.rs:539:1:registration","source":"rust-std-process","unit":"registration","path":"library/std/src/process/tests.rs","line":539,"column":1,"label":"debug_print","disposition":{"kind":"inapplicable","id":"competitor-api-shape"},"url":"https://github.com/rust-lang/rust/blob/24d472027454741e74f8e913755fbc7e03f02af5/library/std/src/process/tests.rs#L539"} {"record":"unit","id":"rust-std-process:library/std/src/process/tests.rs:680:1:registration","source":"rust-std-process","unit":"registration","path":"library/std/src/process/tests.rs","line":680,"column":1,"label":"run_bat_script","disposition":{"kind":"missing","id":"shell-expression-composition-and-redirection"},"url":"https://github.com/rust-lang/rust/blob/24d472027454741e74f8e913755fbc7e03f02af5/library/std/src/process/tests.rs#L680"} {"record":"unit","id":"rust-std-process:library/std/src/process/tests.rs:699:1:registration","source":"rust-std-process","unit":"registration","path":"library/std/src/process/tests.rs","line":699,"column":1,"label":"run_canonical_bat_script","disposition":{"kind":"missing","id":"shell-expression-composition-and-redirection"},"url":"https://github.com/rust-lang/rust/blob/24d472027454741e74f8e913755fbc7e03f02af5/library/std/src/process/tests.rs#L699"} @@ -64,7 +64,7 @@ {"record":"unit","id":"rust-std-process:tests/ui/process/process-envs.rs:1:1:file","source":"rust-std-process","unit":"file","path":"tests/ui/process/process-envs.rs","line":1,"column":1,"label":"","disposition":{"kind":"inapplicable","id":"upstream-runtime-regressions"},"url":"https://github.com/rust-lang/rust/blob/24d472027454741e74f8e913755fbc7e03f02af5/tests/ui/process/process-envs.rs#L1"} {"record":"unit","id":"rust-std-process:tests/ui/process/process-exit.rs:1:1:file","source":"rust-std-process","unit":"file","path":"tests/ui/process/process-exit.rs","line":1,"column":1,"label":"","disposition":{"kind":"inapplicable","id":"upstream-runtime-regressions"},"url":"https://github.com/rust-lang/rust/blob/24d472027454741e74f8e913755fbc7e03f02af5/tests/ui/process/process-exit.rs#L1"} {"record":"unit","id":"rust-std-process:tests/ui/process/process-panic-after-fork.rs:1:1:file","source":"rust-std-process","unit":"file","path":"tests/ui/process/process-panic-after-fork.rs","line":1,"column":1,"label":"","disposition":{"kind":"inapplicable","id":"upstream-runtime-regressions"},"url":"https://github.com/rust-lang/rust/blob/24d472027454741e74f8e913755fbc7e03f02af5/tests/ui/process/process-panic-after-fork.rs#L1"} -{"record":"unit","id":"rust-std-process:tests/ui/process/process-remove-from-env.rs:1:1:file","source":"rust-std-process","unit":"file","path":"tests/ui/process/process-remove-from-env.rs","line":1,"column":1,"label":"","disposition":{"kind":"missing","id":"environment-clear-and-remove"},"url":"https://github.com/rust-lang/rust/blob/24d472027454741e74f8e913755fbc7e03f02af5/tests/ui/process/process-remove-from-env.rs#L1"} +{"record":"unit","id":"rust-std-process:tests/ui/process/process-remove-from-env.rs:1:1:file","source":"rust-std-process","unit":"file","path":"tests/ui/process/process-remove-from-env.rs","line":1,"column":1,"label":"","disposition":{"kind":"ported","id":"environment-clear-and-remove"},"url":"https://github.com/rust-lang/rust/blob/24d472027454741e74f8e913755fbc7e03f02af5/tests/ui/process/process-remove-from-env.rs#L1"} {"record":"unit","id":"rust-std-process:tests/ui/process/process-sigpipe.rs:1:1:file","source":"rust-std-process","unit":"file","path":"tests/ui/process/process-sigpipe.rs","line":1,"column":1,"label":"","disposition":{"kind":"missing","id":"custom-stdio-and-file-handles"},"url":"https://github.com/rust-lang/rust/blob/24d472027454741e74f8e913755fbc7e03f02af5/tests/ui/process/process-sigpipe.rs#L1"} {"record":"unit","id":"rust-std-process:tests/ui/process/process-spawn-failure.rs:1:1:file","source":"rust-std-process","unit":"file","path":"tests/ui/process/process-spawn-failure.rs","line":1,"column":1,"label":"","disposition":{"kind":"ported","id":"spawn-error-propagation"},"url":"https://github.com/rust-lang/rust/blob/24d472027454741e74f8e913755fbc7e03f02af5/tests/ui/process/process-spawn-failure.rs#L1"} {"record":"unit","id":"rust-std-process:tests/ui/process/process-spawn-nonexistent.rs:1:1:file","source":"rust-std-process","unit":"file","path":"tests/ui/process/process-spawn-nonexistent.rs","line":1,"column":1,"label":"","disposition":{"kind":"ported","id":"spawn-error-propagation"},"url":"https://github.com/rust-lang/rust/blob/24d472027454741e74f8e913755fbc7e03f02af5/tests/ui/process/process-spawn-nonexistent.rs#L1"} @@ -108,7 +108,7 @@ {"record":"unit","id":"async-process:tests/std.rs:245:1:registration","source":"async-process","unit":"registration","path":"tests/std.rs","line":245,"column":1,"label":"test_finish_once","disposition":{"kind":"missing","id":"try-wait-and-shared-child-handle"},"url":"https://github.com/smol-rs/async-process/blob/f4485f156f9294b86a5be37f7236bcf0cf93c76b/tests/std.rs#L245"} {"record":"unit","id":"async-process:tests/std.rs:257:1:registration","source":"async-process","unit":"registration","path":"tests/std.rs","line":257,"column":1,"label":"test_finish_twice","disposition":{"kind":"missing","id":"try-wait-and-shared-child-handle"},"url":"https://github.com/smol-rs/async-process/blob/f4485f156f9294b86a5be37f7236bcf0cf93c76b/tests/std.rs#L257"} {"record":"unit","id":"async-process:tests/std.rs:270:1:registration","source":"async-process","unit":"registration","path":"tests/std.rs","line":270,"column":1,"label":"test_wait_with_output_once","disposition":{"kind":"missing","id":"try-wait-and-shared-child-handle"},"url":"https://github.com/smol-rs/async-process/blob/f4485f156f9294b86a5be37f7236bcf0cf93c76b/tests/std.rs#L270"} -{"record":"unit","id":"async-process:tests/std.rs:319:1:registration","source":"async-process","unit":"registration","path":"tests/std.rs","line":319,"column":1,"label":"test_override_env","disposition":{"kind":"missing","id":"environment-clear-and-remove"},"url":"https://github.com/smol-rs/async-process/blob/f4485f156f9294b86a5be37f7236bcf0cf93c76b/tests/std.rs#L319"} +{"record":"unit","id":"async-process:tests/std.rs:319:1:registration","source":"async-process","unit":"registration","path":"tests/std.rs","line":319,"column":1,"label":"test_override_env","disposition":{"kind":"ported","id":"environment-clear-and-remove"},"url":"https://github.com/smol-rs/async-process/blob/f4485f156f9294b86a5be37f7236bcf0cf93c76b/tests/std.rs#L319"} {"record":"unit","id":"async-process:tests/std.rs:341:1:registration","source":"async-process","unit":"registration","path":"tests/std.rs","line":341,"column":1,"label":"test_add_to_env","disposition":{"kind":"ported","id":"environment"},"url":"https://github.com/smol-rs/async-process/blob/f4485f156f9294b86a5be37f7236bcf0cf93c76b/tests/std.rs#L341"} {"record":"unit","id":"async-process:tests/std.rs:358:1:registration","source":"async-process","unit":"registration","path":"tests/std.rs","line":358,"column":1,"label":"test_capture_env_at_spawn","disposition":{"kind":"ported","id":"environment"},"url":"https://github.com/smol-rs/async-process/blob/f4485f156f9294b86a5be37f7236bcf0cf93c76b/tests/std.rs#L358"} {"record":"unit","id":"async-process:tests/std.rs:383:1:registration","source":"async-process","unit":"registration","path":"tests/std.rs","line":383,"column":1,"label":"child_status_preserved_with_kill_on_drop","disposition":{"kind":"ported","id":"stream-kill"},"url":"https://github.com/smol-rs/async-process/blob/f4485f156f9294b86a5be37f7236bcf0cf93c76b/tests/std.rs#L383"} @@ -126,7 +126,7 @@ {"record":"unit","id":"assert-cmd:tests/assert.rs:81:1:registration","source":"assert-cmd","unit":"registration","path":"tests/assert.rs","line":81,"column":1,"label":"stdout_example","disposition":{"kind":"ported","id":"stdout-stderr-separation"},"url":"https://github.com/assert-rs/assert_cmd/blob/a57ef45a33986390be3057c192c6bdbe61b8912d/tests/assert.rs#L81"} {"record":"unit","id":"assert-cmd:tests/assert.rs:119:1:registration","source":"assert-cmd","unit":"registration","path":"tests/assert.rs","line":119,"column":1,"label":"stderr_example","disposition":{"kind":"ported","id":"stdout-stderr-separation"},"url":"https://github.com/assert-rs/assert_cmd/blob/a57ef45a33986390be3057c192c6bdbe61b8912d/tests/assert.rs#L119"} {"record":"unit","id":"assert-cmd:tests/cargo.rs:6:1:registration","source":"assert-cmd","unit":"registration","path":"tests/cargo.rs","line":6,"column":1,"label":"cargo_binary","disposition":{"kind":"missing","id":"binary-input-and-lossless-output"},"url":"https://github.com/assert-rs/assert_cmd/blob/a57ef45a33986390be3057c192c6bdbe61b8912d/tests/cargo.rs#L6"} -{"record":"unit","id":"assert-cmd:tests/cargo.rs:13:1:registration","source":"assert-cmd","unit":"registration","path":"tests/cargo.rs","line":13,"column":1,"label":"cargo_binary_with_empty_env","disposition":{"kind":"missing","id":"environment-clear-and-remove"},"url":"https://github.com/assert-rs/assert_cmd/blob/a57ef45a33986390be3057c192c6bdbe61b8912d/tests/cargo.rs#L13"} +{"record":"unit","id":"assert-cmd:tests/cargo.rs:13:1:registration","source":"assert-cmd","unit":"registration","path":"tests/cargo.rs","line":13,"column":1,"label":"cargo_binary_with_empty_env","disposition":{"kind":"ported","id":"environment-clear-and-remove"},"url":"https://github.com/assert-rs/assert_cmd/blob/a57ef45a33986390be3057c192c6bdbe61b8912d/tests/cargo.rs#L13"} {"record":"unit","id":"assert-cmd:tests/cargo.rs:20:1:registration","source":"assert-cmd","unit":"registration","path":"tests/cargo.rs","line":20,"column":1,"label":"mod_example","disposition":{"kind":"inapplicable","id":"competitor-api-shape"},"url":"https://github.com/assert-rs/assert_cmd/blob/a57ef45a33986390be3057c192c6bdbe61b8912d/tests/cargo.rs#L20"} {"record":"unit","id":"assert-cmd:tests/cargo.rs:41:1:registration","source":"assert-cmd","unit":"registration","path":"tests/cargo.rs","line":41,"column":1,"label":"trait_example","disposition":{"kind":"inapplicable","id":"competitor-api-shape"},"url":"https://github.com/assert-rs/assert_cmd/blob/a57ef45a33986390be3057c192c6bdbe61b8912d/tests/cargo.rs#L41"} {"record":"unit","id":"assert-cmd:tests/cargo.rs:50:1:registration","source":"assert-cmd","unit":"registration","path":"tests/cargo.rs","line":50,"column":1,"label":"cargo_bin_example_1","disposition":{"kind":"inapplicable","id":"competitor-api-shape"},"url":"https://github.com/assert-rs/assert_cmd/blob/a57ef45a33986390be3057c192c6bdbe61b8912d/tests/cargo.rs#L50"} @@ -155,9 +155,9 @@ {"record":"unit","id":"duct:src/test.rs:320:1:registration","source":"duct","unit":"registration","path":"src/test.rs","line":320,"column":1,"label":"test_capture_both","disposition":{"kind":"ported","id":"stdout-stderr-separation"},"url":"https://github.com/oconnor663/duct.rs/blob/0195544c9d963d94348e8bc94fc60b8519e5516b/src/test.rs#L320"} {"record":"unit","id":"duct:src/test.rs:333:1:registration","source":"duct","unit":"registration","path":"src/test.rs","line":333,"column":1,"label":"test_dir","disposition":{"kind":"ported","id":"cwd-string"},"url":"https://github.com/oconnor663/duct.rs/blob/0195544c9d963d94348e8bc94fc60b8519e5516b/src/test.rs#L333"} {"record":"unit","id":"duct:src/test.rs:361:1:registration","source":"duct","unit":"registration","path":"src/test.rs","line":361,"column":1,"label":"test_env","disposition":{"kind":"ported","id":"environment"},"url":"https://github.com/oconnor663/duct.rs/blob/0195544c9d963d94348e8bc94fc60b8519e5516b/src/test.rs#L361"} -{"record":"unit","id":"duct:src/test.rs:370:1:registration","source":"duct","unit":"registration","path":"src/test.rs","line":370,"column":1,"label":"test_full_env","disposition":{"kind":"missing","id":"environment-clear-and-remove"},"url":"https://github.com/oconnor663/duct.rs/blob/0195544c9d963d94348e8bc94fc60b8519e5516b/src/test.rs#L370"} -{"record":"unit","id":"duct:src/test.rs:397:1:registration","source":"duct","unit":"registration","path":"src/test.rs","line":397,"column":1,"label":"test_env_remove","disposition":{"kind":"missing","id":"environment-clear-and-remove"},"url":"https://github.com/oconnor663/duct.rs/blob/0195544c9d963d94348e8bc94fc60b8519e5516b/src/test.rs#L397"} -{"record":"unit","id":"duct:src/test.rs:417:1:registration","source":"duct","unit":"registration","path":"src/test.rs","line":417,"column":1,"label":"test_env_remove_case_sensitivity","disposition":{"kind":"missing","id":"environment-clear-and-remove"},"url":"https://github.com/oconnor663/duct.rs/blob/0195544c9d963d94348e8bc94fc60b8519e5516b/src/test.rs#L417"} +{"record":"unit","id":"duct:src/test.rs:370:1:registration","source":"duct","unit":"registration","path":"src/test.rs","line":370,"column":1,"label":"test_full_env","disposition":{"kind":"ported","id":"environment-clear-and-remove"},"url":"https://github.com/oconnor663/duct.rs/blob/0195544c9d963d94348e8bc94fc60b8519e5516b/src/test.rs#L370"} +{"record":"unit","id":"duct:src/test.rs:397:1:registration","source":"duct","unit":"registration","path":"src/test.rs","line":397,"column":1,"label":"test_env_remove","disposition":{"kind":"ported","id":"environment-clear-and-remove"},"url":"https://github.com/oconnor663/duct.rs/blob/0195544c9d963d94348e8bc94fc60b8519e5516b/src/test.rs#L397"} +{"record":"unit","id":"duct:src/test.rs:417:1:registration","source":"duct","unit":"registration","path":"src/test.rs","line":417,"column":1,"label":"test_env_remove_case_sensitivity","disposition":{"kind":"ported","id":"environment-clear-and-remove"},"url":"https://github.com/oconnor663/duct.rs/blob/0195544c9d963d94348e8bc94fc60b8519e5516b/src/test.rs#L417"} {"record":"unit","id":"duct:src/test.rs:455:1:registration","source":"duct","unit":"registration","path":"src/test.rs","line":455,"column":1,"label":"test_env_case_preserving","disposition":{"kind":"ported","id":"environment"},"url":"https://github.com/oconnor663/duct.rs/blob/0195544c9d963d94348e8bc94fc60b8519e5516b/src/test.rs#L455"} {"record":"unit","id":"duct:src/test.rs:579:1:registration","source":"duct","unit":"registration","path":"src/test.rs","line":579,"column":1,"label":"test_broken_pipe","disposition":{"kind":"missing","id":"custom-stdio-and-file-handles"},"url":"https://github.com/oconnor663/duct.rs/blob/0195544c9d963d94348e8bc94fc60b8519e5516b/src/test.rs#L579"} {"record":"unit","id":"duct:src/test.rs:588:1:registration","source":"duct","unit":"registration","path":"src/test.rs","line":588,"column":1,"label":"test_silly","disposition":{"kind":"inapplicable","id":"competitor-internals"},"url":"https://github.com/oconnor663/duct.rs/blob/0195544c9d963d94348e8bc94fc60b8519e5516b/src/test.rs#L588"} @@ -187,7 +187,7 @@ {"record":"unit","id":"xshell:tests/it/compile_failures.rs:109:1:registration","source":"xshell","unit":"registration","path":"tests/it/compile_failures.rs","line":109,"column":1,"label":"interpolate_integer","disposition":{"kind":"ported","id":"safe-template-interpolation"},"url":"https://github.com/matklad/xshell/blob/52f71bac326aaac291d07146ea790ad886dd8131/tests/it/compile_failures.rs#L109"} {"record":"unit","id":"xshell:tests/it/compile_failures.rs:119:1:registration","source":"xshell","unit":"registration","path":"tests/it/compile_failures.rs","line":119,"column":1,"label":"splat_fn_pointer","disposition":{"kind":"missing","id":"array-and-splat-interpolation"},"url":"https://github.com/matklad/xshell/blob/52f71bac326aaac291d07146ea790ad886dd8131/tests/it/compile_failures.rs#L119"} {"record":"unit","id":"xshell:tests/it/env.rs:6:1:registration","source":"xshell","unit":"registration","path":"tests/it/env.rs","line":6,"column":1,"label":"test_env","disposition":{"kind":"ported","id":"environment"},"url":"https://github.com/matklad/xshell/blob/52f71bac326aaac291d07146ea790ad886dd8131/tests/it/env.rs#L6"} -{"record":"unit","id":"xshell:tests/it/env.rs:58:1:registration","source":"xshell","unit":"registration","path":"tests/it/env.rs","line":58,"column":1,"label":"test_env_clear","disposition":{"kind":"missing","id":"environment-clear-and-remove"},"url":"https://github.com/matklad/xshell/blob/52f71bac326aaac291d07146ea790ad886dd8131/tests/it/env.rs#L58"} +{"record":"unit","id":"xshell:tests/it/env.rs:58:1:registration","source":"xshell","unit":"registration","path":"tests/it/env.rs","line":58,"column":1,"label":"test_env_clear","disposition":{"kind":"ported","id":"environment-clear-and-remove"},"url":"https://github.com/matklad/xshell/blob/52f71bac326aaac291d07146ea790ad886dd8131/tests/it/env.rs#L58"} {"record":"unit","id":"xshell:tests/it/main.rs:30:1:registration","source":"xshell","unit":"registration","path":"tests/it/main.rs","line":30,"column":1,"label":"smoke","disposition":{"kind":"ported","id":"direct-exact-argv"},"url":"https://github.com/matklad/xshell/blob/52f71bac326aaac291d07146ea790ad886dd8131/tests/it/main.rs#L30"} {"record":"unit","id":"xshell:tests/it/main.rs:39:1:registration","source":"xshell","unit":"registration","path":"tests/it/main.rs","line":39,"column":1,"label":"into_command","disposition":{"kind":"inapplicable","id":"competitor-api-shape"},"url":"https://github.com/matklad/xshell/blob/52f71bac326aaac291d07146ea790ad886dd8131/tests/it/main.rs#L39"} {"record":"unit","id":"xshell:tests/it/main.rs:47:1:registration","source":"xshell","unit":"registration","path":"tests/it/main.rs","line":47,"column":1,"label":"multiline","disposition":{"kind":"ported","id":"direct-exact-argv"},"url":"https://github.com/matklad/xshell/blob/52f71bac326aaac291d07146ea790ad886dd8131/tests/it/main.rs#L47"} @@ -405,8 +405,8 @@ {"record":"unit","id":"subprocess:src/tests/posix.rs:74:1:registration","source":"subprocess","unit":"registration","path":"src/tests/posix.rs","line":74,"column":1,"label":"err_terminate","disposition":{"kind":"ported","id":"stream-kill"},"url":"https://github.com/hniksic/rust-subprocess/blob/e8cd8d0c930a790ce29373a8c3ee080dd746b9eb/src/tests/posix.rs#L74"} {"record":"unit","id":"subprocess:src/tests/posix.rs:83:1:registration","source":"subprocess","unit":"registration","path":"src/tests/posix.rs","line":83,"column":1,"label":"waitpid_echild","disposition":{"kind":"missing","id":"try-wait-and-shared-child-handle"},"url":"https://github.com/hniksic/rust-subprocess/blob/e8cd8d0c930a790ce29373a8c3ee080dd746b9eb/src/tests/posix.rs#L83"} {"record":"unit","id":"subprocess:src/tests/posix.rs:98:1:registration","source":"subprocess","unit":"registration","path":"src/tests/posix.rs","line":98,"column":1,"label":"send_signal","disposition":{"kind":"missing","id":"native-exit-status-and-signal-metadata"},"url":"https://github.com/hniksic/rust-subprocess/blob/e8cd8d0c930a790ce29373a8c3ee080dd746b9eb/src/tests/posix.rs#L98"} -{"record":"unit","id":"subprocess:src/tests/posix.rs:106:1:registration","source":"subprocess","unit":"registration","path":"src/tests/posix.rs","line":106,"column":1,"label":"env_set_all_1","disposition":{"kind":"missing","id":"environment-clear-and-remove"},"url":"https://github.com/hniksic/rust-subprocess/blob/e8cd8d0c930a790ce29373a8c3ee080dd746b9eb/src/tests/posix.rs#L106"} -{"record":"unit","id":"subprocess:src/tests/posix.rs:118:1:registration","source":"subprocess","unit":"registration","path":"src/tests/posix.rs","line":118,"column":1,"label":"env_set_all_2","disposition":{"kind":"missing","id":"environment-clear-and-remove"},"url":"https://github.com/hniksic/rust-subprocess/blob/e8cd8d0c930a790ce29373a8c3ee080dd746b9eb/src/tests/posix.rs#L118"} +{"record":"unit","id":"subprocess:src/tests/posix.rs:106:1:registration","source":"subprocess","unit":"registration","path":"src/tests/posix.rs","line":106,"column":1,"label":"env_set_all_1","disposition":{"kind":"ported","id":"environment-clear-and-remove"},"url":"https://github.com/hniksic/rust-subprocess/blob/e8cd8d0c930a790ce29373a8c3ee080dd746b9eb/src/tests/posix.rs#L106"} +{"record":"unit","id":"subprocess:src/tests/posix.rs:118:1:registration","source":"subprocess","unit":"registration","path":"src/tests/posix.rs","line":118,"column":1,"label":"env_set_all_2","disposition":{"kind":"ported","id":"environment-clear-and-remove"},"url":"https://github.com/hniksic/rust-subprocess/blob/e8cd8d0c930a790ce29373a8c3ee080dd746b9eb/src/tests/posix.rs#L118"} {"record":"unit","id":"subprocess:src/tests/posix.rs:132:1:registration","source":"subprocess","unit":"registration","path":"src/tests/posix.rs","line":132,"column":1,"label":"exec_setpgid","disposition":{"kind":"missing","id":"custom-stdio-and-file-handles"},"url":"https://github.com/hniksic/rust-subprocess/blob/e8cd8d0c930a790ce29373a8c3ee080dd746b9eb/src/tests/posix.rs#L132"} {"record":"unit","id":"subprocess:src/tests/posix.rs:147:1:registration","source":"subprocess","unit":"registration","path":"src/tests/posix.rs","line":147,"column":1,"label":"send_signal_group","disposition":{"kind":"missing","id":"native-exit-status-and-signal-metadata"},"url":"https://github.com/hniksic/rust-subprocess/blob/e8cd8d0c930a790ce29373a8c3ee080dd746b9eb/src/tests/posix.rs#L147"} {"record":"unit","id":"subprocess:src/tests/posix.rs:162:1:registration","source":"subprocess","unit":"registration","path":"src/tests/posix.rs","line":162,"column":1,"label":"send_signal_group_after_finish","disposition":{"kind":"missing","id":"native-exit-status-and-signal-metadata"},"url":"https://github.com/hniksic/rust-subprocess/blob/e8cd8d0c930a790ce29373a8c3ee080dd746b9eb/src/tests/posix.rs#L162"}

; + pipe( + a: (builder: CommandBuilder) => CommandBuilder, + b: (builder: CommandBuilder) => CommandBuilder, + c: (builder: CommandBuilder) => CommandBuilder, + d: (builder: CommandBuilder) => CommandBuilder, + e: (builder: CommandBuilder) => CommandBuilder, + f: (builder: CommandBuilder) => CommandBuilder, + g: (builder: CommandBuilder) => CommandBuilder, + h: (builder: CommandBuilder) => CommandBuilder, + i: (builder: CommandBuilder) => CommandBuilder, + j: (builder: CommandBuilder) => CommandBuilder, + k: (builder: CommandBuilder) => CommandBuilder, + l: (builder: CommandBuilder) => CommandBuilder, + m: (builder: CommandBuilder) => CommandBuilder, + n: (builder: CommandBuilder) => CommandBuilder, + o: (builder: CommandBuilder) => CommandBuilder, + p: (builder: CommandBuilder) => CommandBuilder