diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index fd873e8e..f3896cb5 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -299,42 +299,6 @@ jobs: run: npm clean-install - name: Run integration tests run: npm run coverage:integration - test-mutation-unit: - name: Mutation (Unit) - runs-on: ubuntu-24.04 - needs: - - test-unit - steps: - - name: Checkout repository - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - with: - persist-credentials: false - - name: Verify action checksums - uses: ./.github/actions/ghasum - - name: Install Node.js - uses: actions/setup-node@v6.3.0 - with: - cache: npm - node-version-file: .nvmrc - - name: Cache Stryker incremental report - uses: actions/cache@v5.0.3 - with: - path: .cache/stryker-incremental-unit.json - key: mutation-unit-${{ github.run_number }} - restore-keys: | - mutation-unit- - - name: Install dependencies - run: npm clean-install - - name: Run mutation tests - run: npm run mutation:unit - - name: Upload mutation report - uses: actions/upload-artifact@v7.0.0 - if: ${{ failure() || success() }} - with: - name: mutation-unit-report - path: | - _reports/mutation/unit.html - .cache/stryker-incremental-unit.json test-mutation-integration: name: Mutation (integration) runs-on: ubuntu-24.04 @@ -392,6 +356,15 @@ jobs: node-version-file: .nvmrc - name: Install dependencies run: npm clean-install + - name: Run unit tests + run: | + cd node_modules/shescape-previous + git init . + git add . + git config --global user.name "John Doe" + git config --global user.email johndoe@example.com + git commit -m 'n/a' + git apply ../../2410.patch - name: Run unit tests run: npm run coverage:unit transpile: diff --git a/2410.patch b/2410.patch new file mode 100644 index 00000000..8eb5dedf --- /dev/null +++ b/2410.patch @@ -0,0 +1,26 @@ +diff --git a/src/internal/unix/bash.js b/src/internal/unix/bash.js +index 19fba6d..c1c57ce 100644 +--- a/src/internal/unix/bash.js ++++ b/src/internal/unix/bash.js +@@ -15,7 +15,7 @@ function escapeArg(arg) { + .replace(/\n/gu, " ") + .replace(/\\/gu, "\\\\") + .replace(/(?<=^|\s)([#~])/gu, "\\$1") +- .replace(/(["$&'()*;<>?`{|])/gu, "\\$1") ++ .replace(/(["$&'()*;<>?`{|[\]])/gu, "\\$1") + .replace(/(?<=[:=])(~)(?=[\s+\-/0:=]|$)/gu, "\\$1") + .replace(/([\t ])/gu, "\\$1"); + } +diff --git a/src/internal/unix/busybox.js b/src/internal/unix/busybox.js +index e61262f..0c91dc4 100644 +--- a/src/internal/unix/busybox.js ++++ b/src/internal/unix/busybox.js +@@ -15,7 +15,7 @@ function escapeArg(arg) { + .replace(/\n/gu, " ") + .replace(/\\/gu, "\\\\") + .replace(/(?<=^|\s)([#~])/gu, "\\$1") +- .replace(/(["$&'()*;<>?`|])/gu, "\\$1") ++ .replace(/(["$&'()*;<>?`|[\]])/gu, "\\$1") + .replace(/([\t ])/gu, "\\$1"); + } + diff --git a/package-lock.json b/package-lock.json index 23f3c349..50df7268 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,7 +46,7 @@ "prettier": "3.7.3", "publint": "0.3.18", "rollup": "4.60.0", - "shescape-previous": "npm:shescape@2.1.10", + "shescape-previous": "npm:shescape@2.1.7", "sinon": "21.0.3" }, "engines": { @@ -13515,17 +13515,16 @@ }, "node_modules/shescape-previous": { "name": "shescape", - "version": "2.1.10", - "resolved": "https://registry.npmjs.org/shescape/-/shescape-2.1.10.tgz", - "integrity": "sha512-CJvUj3QcmIUqxfSci5x6SjATMbcVPYWnHDPoS6dZeLrB/o0qwPGYyshtylAEKH7eH61WfUYineGz5RJaWXuTbw==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/shescape/-/shescape-2.1.7.tgz", + "integrity": "sha512-Y1syY0ggm3ow7mE1zrcK9YrOhAqv/IGbm3+J9S+MXLukwXf/M8yzL3hZp7ubVeSy250TT7M5SVKikTZkKyib6w==", "dev": true, "license": "MPL-2.0", "dependencies": { - "@ericcornelissen/lregexp": "^1.0.3", "which": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" }, "engines": { - "node": "^14.18.0 || ^16.13.0 || ^18 || ^19 || ^20 || ^22 || ^24 || ^25" + "node": "^14.18.0 || ^16.13.0 || ^18 || ^19 || ^20 || ^22 || ^24" } }, "node_modules/side-channel": { diff --git a/package.json b/package.json index f1a9698a..544bdcde 100644 --- a/package.json +++ b/package.json @@ -96,7 +96,7 @@ "prettier": "3.7.3", "publint": "0.3.18", "rollup": "4.60.0", - "shescape-previous": "npm:shescape@2.1.10", + "shescape-previous": "npm:shescape@2.1.7", "sinon": "21.0.3" }, "scripts": { @@ -139,7 +139,7 @@ "mutation:unit": "stryker run config/stryker/unit.js", "mutation:integration": "npm run transpile && stryker run config/stryker/integration.js", "test": "npm-run-all test:*", - "test:unit": "ava test/unit/**/*.test.js", + "test:unit": "ava --timeout 59m test/unit/**/*.test.js", "test:integration": "npm run transpile && ava test/integration/**/*.test.js --timeout 2m", "test:e2e": "node script/busybox-sh.js && node script/double-link-sh.js && ava test/e2e/**/*.test.js --timeout 1m", "test:compat": "npm-run-all test:compat:*", diff --git a/test/unit/unix/bash.test.js b/test/unit/unix/bash.test.js new file mode 100644 index 00000000..3452cb0e --- /dev/null +++ b/test/unit/unix/bash.test.js @@ -0,0 +1,58 @@ +/** + * @overview Contains (additional) unit tests for the escaping functionality for + * the Bourne-again shell (Bash). + * @license MIT + */ + +import { testProp } from "@fast-check/ava"; +import * as fc from "fast-check"; + +import * as old from "../../../node_modules/shescape-previous/src/internal/unix/bash.js"; +import * as upd from "../../../src/internal/unix/bash.js"; + +const numRuns = 5_000_000; + +testProp( + "escape functionality is unchanged", + [fc.string()], + (t, arg) => { + const updFn = upd.getEscapeFunction(); + const oldFn = old.getEscapeFunction(); + + const got = updFn(arg).replace(/(?<=[:=])(\\~)(?!\\?[\s+\-/0:=]|$)/gu, "~"); + const want = oldFn(arg); + t.is(got, want); + }, + { numRuns }, +); + +testProp( + "quote functionality is unchanged", + [fc.string()], + (t, arg) => { + const updFn = upd.getQuoteFunction(); + const oldFn = old.getQuoteFunction(); + + const [updEscape, updQuote] = updFn; + const [oldEscape, oldQuote] = oldFn; + + const got = updQuote(updEscape(arg)); + const want = oldQuote(oldEscape(arg)); + t.is(got, want); + }, + { numRuns }, +); + +testProp( + "flag protection functionality is unchanged", + [fc.string()], + (t, arg) => { + const updFn = upd.getFlagProtectionFunction(); + const oldFn = old.getFlagProtectionFunction(); + + const got = updFn(arg); + const want = oldFn(arg); + t.is(got, want); + }, + { numRuns }, +); diff --git a/test/unit/unix/busybox.test.js b/test/unit/unix/busybox.test.js new file mode 100644 index 00000000..666b1892 --- /dev/null +++ b/test/unit/unix/busybox.test.js @@ -0,0 +1,58 @@ +/** + * @overview Contains (additional) unit tests for the escaping functionality for + * the BusyBox shell. + * @license MIT + */ + +import { testProp } from "@fast-check/ava"; +import * as fc from "fast-check"; + +import * as old from "../../../node_modules/shescape-previous/src/internal/unix/busybox.js"; +import * as upd from "../../../src/internal/unix/busybox.js"; + +const numRuns = 5_000_000; + +testProp( + "escape functionality is unchanged", + [fc.string()], + (t, arg) => { + const updFn = upd.getEscapeFunction(); + const oldFn = old.getEscapeFunction(); + + const got = updFn(arg); + const want = oldFn(arg); + t.is(got, want); + }, + { numRuns }, +); + +testProp( + "quote functionality is unchanged", + [fc.string()], + (t, arg) => { + const updFn = upd.getQuoteFunction(); + const oldFn = old.getQuoteFunction(); + + const [updEscape, updQuote] = updFn; + const [oldEscape, oldQuote] = oldFn; + + const got = updQuote(updEscape(arg)); + const want = oldQuote(oldEscape(arg)); + t.is(got, want); + }, + { numRuns }, +); + +testProp( + "flag protection functionality is unchanged", + [fc.string()], + (t, arg) => { + const updFn = upd.getFlagProtectionFunction(); + const oldFn = old.getFlagProtectionFunction(); + + const got = updFn(arg); + const want = oldFn(arg); + t.is(got, want); + }, + { numRuns }, +); diff --git a/test/unit/unix/csh.test.js b/test/unit/unix/csh.test.js index e85c50ca..57405e22 100644 --- a/test/unit/unix/csh.test.js +++ b/test/unit/unix/csh.test.js @@ -9,10 +9,57 @@ import { TextDecoder } from "node:util"; import { testProp } from "@fast-check/ava"; import * as fc from "fast-check"; +import * as old from "../../../node_modules/shescape-previous/src/internal/unix/csh.js"; import * as csh from "../../../src/internal/unix/csh.js"; +const numRuns = 5_000_000; const textDecoder = new TextDecoder("utf-8", { fatal: true }); +testProp( + "escape functionality is unchanged", + [fc.string()], + (t, arg) => { + const updFn = csh.getEscapeFunction(); + const oldFn = old.getEscapeFunction(); + + const got = updFn(arg).replace(/\\!$/gu, "!"); + const want = oldFn(arg); + t.is(got, want); + }, + { numRuns }, +); + +testProp( + "quote functionality is unchanged", + [fc.string()], + (t, arg) => { + const updFn = csh.getQuoteFunction(); + const oldFn = old.getQuoteFunction(); + + const [updEscape, updQuote] = updFn; + const [oldEscape, oldQuote] = oldFn; + + const got = updQuote(updEscape(arg).replace(/(? { + const updFn = csh.getFlagProtectionFunction(); + const oldFn = old.getFlagProtectionFunction(); + + const got = updFn(arg); + const want = oldFn(arg); + t.is(got, want); + }, + { numRuns }, +); + testProp( "characters with 0xA0 when utf-8 encoded", [ diff --git a/test/unit/unix/no-shell.test.js b/test/unit/unix/no-shell.test.js index b9dcff1c..32fe45dc 100644 --- a/test/unit/unix/no-shell.test.js +++ b/test/unit/unix/no-shell.test.js @@ -7,8 +7,39 @@ import { testProp } from "@fast-check/ava"; import * as fc from "fast-check"; +import * as old from "../../../node_modules/shescape-previous/src/internal/unix/no-shell.js"; import * as nosh from "../../../src/internal/unix/no-shell.js"; +const numRuns = 5_000_000; + +testProp( + "escape functionality is unchanged", + [fc.string()], + (t, arg) => { + const updFn = nosh.getEscapeFunction(); + const oldFn = old.getEscapeFunction(); + + const got = updFn(arg); + const want = oldFn(arg); + t.is(got, want); + }, + { numRuns }, +); + +testProp( + "flag protection functionality is unchanged", + [fc.string()], + (t, arg) => { + const updFn = nosh.getFlagProtectionFunction(); + const oldFn = old.getFlagProtectionFunction(); + + const got = updFn(arg); + const want = oldFn(arg); + t.is(got, want); + }, + { numRuns }, +); + testProp("quote function", [fc.string()], (t, arg) => { const expected = { instanceOf: Error, diff --git a/test/unit/win/cmd.test.js b/test/unit/win/cmd.test.js new file mode 100644 index 00000000..91461855 --- /dev/null +++ b/test/unit/win/cmd.test.js @@ -0,0 +1,58 @@ +/** + * @overview Contains (additional) unit tests for the escaping functionality for + * the the Windows Command Prompt. + * @license MIT + */ + +import { testProp } from "@fast-check/ava"; +import * as fc from "fast-check"; + +import * as old from "../../../node_modules/shescape-previous/src/internal/win/cmd.js"; +import * as upd from "../../../src/internal/win/cmd.js"; + +const numRuns = 5_000_000; + +testProp( + "escape functionality is unchanged", + [fc.string()], + (t, arg) => { + const updFn = upd.getEscapeFunction(); + const oldFn = old.getEscapeFunction(); + + const got = updFn(arg); + const want = oldFn(arg); + t.is(got, want); + }, + { numRuns }, +); + +testProp( + "quote functionality is unchanged", + [fc.string()], + (t, arg) => { + const updFn = upd.getQuoteFunction(); + const oldFn = old.getQuoteFunction(); + + const [updEscape, updQuote] = updFn; + const [oldEscape, oldQuote] = oldFn; + + const got = updQuote(updEscape(arg)); + const want = oldQuote(oldEscape(arg)); + t.is(got, want); + }, + { numRuns }, +); + +testProp( + "flag protection functionality is unchanged", + [fc.string()], + (t, arg) => { + const updFn = upd.getFlagProtectionFunction(); + const oldFn = old.getFlagProtectionFunction(); + + const got = updFn(arg); + const want = oldFn(arg); + t.is(got, want); + }, + { numRuns }, +); diff --git a/test/unit/win/no-shell.test.js b/test/unit/win/no-shell.test.js index 9f7d3a93..fe904f0f 100644 --- a/test/unit/win/no-shell.test.js +++ b/test/unit/win/no-shell.test.js @@ -7,8 +7,39 @@ import { testProp } from "@fast-check/ava"; import * as fc from "fast-check"; +import * as old from "../../../node_modules/shescape-previous/src/internal/win/no-shell.js"; import * as nosh from "../../../src/internal/win/no-shell.js"; +const numRuns = 5_000_000; + +testProp( + "escape functionality is unchanged", + [fc.string()], + (t, arg) => { + const updFn = nosh.getEscapeFunction(); + const oldFn = old.getEscapeFunction(); + + const got = updFn(arg); + const want = oldFn(arg); + t.is(got, want); + }, + { numRuns }, +); + +testProp( + "flag protection functionality is unchanged", + [fc.string()], + (t, arg) => { + const updFn = nosh.getFlagProtectionFunction(); + const oldFn = old.getFlagProtectionFunction(); + + const got = updFn(arg); + const want = oldFn(arg); + t.is(got, want); + }, + { numRuns }, +); + testProp("quote function", [fc.string()], (t, arg) => { const expected = { instanceOf: Error, diff --git a/test/unit/win/powershell.test.js b/test/unit/win/powershell.test.js new file mode 100644 index 00000000..df45f313 --- /dev/null +++ b/test/unit/win/powershell.test.js @@ -0,0 +1,58 @@ +/** + * @overview Contains (additional) unit tests for the escaping functionality for + * PowerShell. + * @license MIT + */ + +import { testProp } from "@fast-check/ava"; +import * as fc from "fast-check"; + +import * as old from "../../../node_modules/shescape-previous/src/internal/win/powershell.js"; +import * as upd from "../../../src/internal/win/powershell.js"; + +const numRuns = 5_000_000; + +testProp( + "escape functionality is unchanged", + [fc.string()], + (t, arg) => { + const updFn = upd.getEscapeFunction(); + const oldFn = old.getEscapeFunction(); + + const got = updFn(arg); + const want = oldFn(arg); + t.is(got, want); + }, + { numRuns }, +); + +testProp( + "quote functionality is unchanged", + [fc.string()], + (t, arg) => { + const updFn = upd.getQuoteFunction(); + const oldFn = old.getQuoteFunction(); + + const [updEscape, updQuote] = updFn; + const [oldEscape, oldQuote] = oldFn; + + const got = updQuote(updEscape(arg)); + const want = oldQuote(oldEscape(arg)); + t.is(got, want); + }, + { numRuns }, +); + +testProp( + "flag protection functionality is unchanged", + [fc.string()], + (t, arg) => { + const updFn = upd.getFlagProtectionFunction(); + const oldFn = old.getFlagProtectionFunction(); + + const got = updFn(arg); + const want = oldFn(arg); + t.is(got, want); + }, + { numRuns }, +);