diff --git a/test/unit/_macros.js b/test/unit/_macros.js index f5bb636da..9baa2b86c 100644 --- a/test/unit/_macros.js +++ b/test/unit/_macros.js @@ -3,7 +3,10 @@ * @license MIT */ +import { performance } from "node:perf_hooks"; + import test from "ava"; +import fc from "fast-check"; /** * Transforms a string by replacing control characters with unicode point codes @@ -94,6 +97,36 @@ export const flag = test.macro({ }, }); +/** + * The flag macro tests the behaviour of the function returned by the provided + * `getFlagProtectionFunction`. + * + * @param {object} t The AVA test object. + * @param {object} args The arguments for this function. + * @param {any} args.arbitraries The arbitraries to test with. + * @param {number} args.maxMillis The maximum duration in milliseconds. + * @param {Function} args.setup A function to setup the function to test. + */ +export const duration = test.macro({ + exec(t, { arbitraries, maxMillis, setup }) { + fc.assert( + fc.property(...arbitraries, (...args) => { + const fn = setup(); + + const startTime = performance.now(); + try { + fn(...args); + } catch (_) { + // not concerned about functional correctness + } + const endTime = performance.now(); + + t.true(endTime - startTime < maxMillis); + }), + ); + }, +}); + /** * The quote macro tests the behaviour of the function returned by the provided * `getQuoteFunction`. diff --git a/test/unit/unix/shells.test.js b/test/unit/unix/shells.test.js index 3487b7423..d87fc1313 100644 --- a/test/unit/unix/shells.test.js +++ b/test/unit/unix/shells.test.js @@ -44,6 +44,20 @@ for (const [shellName, shellExports] of Object.entries(shells)) { t.is(typeof result, "string"); }); + test(`escape performance for ${shellName}`, macros.duration, { + arbitraries: [fc.string({ size: "xlarge" })], + maxMillis: 50, + setup: shellExports.getEscapeFunction, + }); + + redosFixtures.forEach((input, id) => { + test(`${shellName}, ReDoS #${id}`, (t) => { + const escape = shellExports.getEscapeFunction(); + escape(input); + t.pass(); + }); + }); + flagFixtures.forEach(({ input, expected }) => { test(macros.flag, { expected: expected.unquoted, @@ -63,6 +77,12 @@ for (const [shellName, shellExports] of Object.entries(shells)) { }, ); + test(`flag protection performance for ${shellName}`, macros.duration, { + arbitraries: [fc.string({ size: "xlarge" })], + maxMillis: 50, + setup: shellExports.getFlagProtectionFunction, + }); + if (shellExports !== nosh) { quoteFixtures.forEach(({ input, expected }) => { test(macros.quote, { @@ -80,13 +100,14 @@ for (const [shellName, shellExports] of Object.entries(shells)) { const result = quoteFn(intermediate); t.is(typeof result, "string"); }); - } - redosFixtures.forEach((input, id) => { - test(`${shellName}, ReDoS #${id}`, (t) => { - const escape = shellExports.getEscapeFunction(); - escape(input); - t.pass(); + test(`quote performance for ${shellName}`, macros.duration, { + arbitraries: [fc.string({ size: "xlarge" })], + maxMillis: 50, + setup: () => { + const [escapeFn, quoteFn] = shellExports.getQuoteFunction(); + return (arg) => quoteFn(escapeFn(arg)); + }, }); - }); + } } diff --git a/test/unit/win/shells.test.js b/test/unit/win/shells.test.js index 101db9e87..e004b2370 100644 --- a/test/unit/win/shells.test.js +++ b/test/unit/win/shells.test.js @@ -39,6 +39,12 @@ for (const [shellName, shellExports] of Object.entries(shells)) { t.is(typeof result, "string"); }); + test(`escape performance for ${shellName}`, macros.duration, { + arbitraries: [fc.string({ size: "xlarge" })], + maxMillis: 50, + setup: shellExports.getEscapeFunction, + }); + flagFixtures.forEach(({ input, expected }) => { test(macros.flag, { expected: expected.unquoted, @@ -58,6 +64,12 @@ for (const [shellName, shellExports] of Object.entries(shells)) { }, ); + test(`flag protection performance for ${shellName}`, macros.duration, { + arbitraries: [fc.string({ size: "xlarge" })], + maxMillis: 50, + setup: shellExports.getFlagProtectionFunction, + }); + if (shellExports !== nosh) { quoteFixtures.forEach(({ input, expected }) => { test(macros.quote, { @@ -75,5 +87,14 @@ for (const [shellName, shellExports] of Object.entries(shells)) { const result = quoteFn(intermediate); t.is(typeof result, "string"); }); + + test(`quote performance for ${shellName}`, macros.duration, { + arbitraries: [fc.string({ size: "xlarge" })], + maxMillis: 50, + setup: () => { + const [escapeFn, quoteFn] = shellExports.getQuoteFunction(); + return (arg) => quoteFn(escapeFn(arg)); + }, + }); } }