diff --git a/tests/config/run-test.js b/tests/config/run-test.js index 8896019ed..4186f1403 100644 --- a/tests/config/run-test.js +++ b/tests/config/run-test.js @@ -1,53 +1,16 @@ -import path from "node:path"; -import createEsmUtils from "esm-utils"; -import { BOM, FULL_TEST } from "./constants.js"; -import * as failedTests from "./failed-format-tests.js"; -import { format, parse } from "./run-prettier.js"; +import { FULL_TEST } from "./constants.js"; +import { format } from "./run-prettier.js"; import consistentEndOfLine from "./utils/consistent-end-of-line.js"; import createSnapshot from "./utils/create-snapshot.js"; import visualizeEndOfLine from "./utils/visualize-end-of-line.js"; +import * as testAstCompare from "./test-ast-compare.js"; +import * as testBom from "./test-bom.js"; +import * as testEndOfLine from "./test-end-of-line.js"; +import * as testSecondFormat from "./test-second-format.js"; +import * as testBytecodeCompare from "./test-bytecode-compare.js"; +import * as testAntlrFormat from "./test-antlr-format.js"; +import * as testVariantCoverage from "./test-variant-coverage.js"; import { shouldThrowOnFormat } from "./utilities.js"; -import getPrettier from "./get-prettier.js"; -import getCreateParser from "./get-create-parser.js"; -import getVariantCoverage from "./get-variant-coverage.js"; -import getPlugins from "./get-plugins.js"; -import compileContract from "./utils/compile-contract.js"; - -const { __dirname } = createEsmUtils(import.meta); - -const testsWithAstChanges = new Map( - [ - "Parentheses/AddNoParentheses.sol", - "Parentheses/SubNoParentheses.sol", - "Parentheses/MulNoParentheses.sol", - "Parentheses/DivNoParentheses.sol", - "Parentheses/ModNoParentheses.sol", - "Parentheses/ExpNoParentheses.sol", - "Parentheses/ShiftLNoParentheses.sol", - "Parentheses/ShiftRNoParentheses.sol", - "Parentheses/BitAndNoParentheses.sol", - "Parentheses/BitOrNoParentheses.sol", - "Parentheses/BitXorNoParentheses.sol", - "Parentheses/LogicNoParentheses.sol", - "HexLiteral/HexLiteral.sol", - "ModifierInvocations/ModifierInvocations.sol", - ].map((fixture) => { - const [file, compareBytecode = () => true] = Array.isArray(fixture) - ? fixture - : [fixture]; - return [path.join(__dirname, "../format/", file), compareBytecode]; - }), -); - -const shouldCompareBytecode = (filename, options) => { - const testFunction = testsWithAstChanges.get(filename); - - if (!testFunction) { - return false; - } - - return testFunction(options); -}; async function runTest({ parsers, @@ -101,114 +64,22 @@ async function runTest({ if (!FULL_TEST) { return; } - - if (formatOptions.parser === "slang") { - const createParser = await getCreateParser(); - const variantCoverage = await getVariantCoverage(); - const { parser, parseOutput } = createParser(code, formatOptions); - - // Check coverage - variantCoverage(parseOutput.tree.asNonterminalNode()); - - if (!failedTests.isAntlrMismatch(filename, formatOptions)) { - // Compare with ANTLR's format - const prettier = await getPrettier(); - const { formatted: antlrOutput } = await prettier.formatWithCursor(code, { - ...formatOptions, - // Since Slang forces us to decide on a compiler version, we need to do the - // same for ANTLR unless it was already given as an option. - compiler: formatOptions.compiler || parser.languageVersion, - parser: "antlr", - plugins: await getPlugins(), - }); - expect(antlrOutput).toEqual(formatResult.output); - } - } - - const isUnstableTest = failedTests.isUnstable(filename, formatOptions); - if ( - (formatResult.changed || isUnstableTest) && - // No range and cursor - formatResult.input === code - ) { - const { eolVisualizedOutput: firstOutput, output } = formatResult; - const { eolVisualizedOutput: secondOutput } = await format( - output, - formatOptions, - ); - if (isUnstableTest) { - // To keep eye on failed tests, this assert never supposed to pass, - // if it fails, just remove the file from `unstableTests` - expect(secondOutput).not.toEqual(firstOutput); - } else { - expect(secondOutput).toEqual(firstOutput); - } - } - - const isAstUnstableTest = failedTests.isAstUnstable(filename, formatOptions); - // Some parsers skip parsing empty files - if (formatResult.changed && code.trim()) { - const { input, output } = formatResult; - const originalAst = await parse(input, formatOptions); - const formattedAst = await parse(output, formatOptions); - if (isAstUnstableTest) { - expect(formattedAst).not.toEqual(originalAst); - } else { - expect(formattedAst).toEqual(originalAst); - } - } - - if (!shouldSkipEolTest(code, formatResult.options)) { - for (const eol of ["\r\n", "\r"]) { - const { eolVisualizedOutput: output } = await format( - code.replace(/\n/gu, eol), - formatOptions, - ); - // Only if `endOfLine: "auto"` the result will be different - const expected = - formatOptions.endOfLine === "auto" - ? visualizeEndOfLine( - // All `code` use `LF`, so the `eol` of result is always `LF` - formatResult.outputWithCursor.replace(/\n/gu, eol), - ) - : formatResult.eolVisualizedOutput; - expect(output).toEqual(expected); - } - } - - if (code.charAt(0) !== BOM) { - const { eolVisualizedOutput: output } = await format( - BOM + code, - formatOptions, - ); - const expected = BOM + formatResult.eolVisualizedOutput; - expect(output).toEqual(expected); - } - - if (shouldCompareBytecode(filename, formatOptions)) { - const output = compileContract(filename, formatResult.output); - const expected = compileContract(filename, formatResult.input); - expect(output).toEqual(expected); - } -} - -function shouldSkipEolTest(code, options) { - if (code.includes("\r")) { - return true; - } - const { requirePragma, rangeStart, rangeEnd } = options; - if (requirePragma) { - return true; - } - - if ( - typeof rangeStart === "number" && - typeof rangeEnd === "number" && - rangeStart >= rangeEnd - ) { - return true; - } - return false; + await Promise.all( + [ + testAntlrFormat.run, + testVariantCoverage.run, + testSecondFormat.run, + testAstCompare.run, + testBom.run, + testBytecodeCompare.run, + ] + .map((run) => run(code, formatResult, filename, formatOptions)) + .join( + ["\r\n", "\r"].map((eol) => + testEndOfLine.run(code, formatResult, filename, formatOptions, eol), + ), + ), + ); } export { runTest }; diff --git a/tests/config/test-antlr-format.js b/tests/config/test-antlr-format.js new file mode 100644 index 000000000..2d76066d2 --- /dev/null +++ b/tests/config/test-antlr-format.js @@ -0,0 +1,27 @@ +import * as failedTests from "./failed-format-tests.js"; +import getPrettier from "./get-prettier.js"; +import getCreateParser from "./get-create-parser.js"; +import getPlugins from "./get-plugins.js"; + +async function testAntlrFormat(source, formatResult, filename, formatOptions) { + if ( + formatOptions.parser === "slang" && + !failedTests.isAntlrMismatch(filename, formatOptions) + ) { + // Compare with ANTLR's format + const createParser = await getCreateParser(); + const { parser } = createParser(source, formatOptions); + const prettier = await getPrettier(); + const { formatted: antlrOutput } = await prettier.formatWithCursor(source, { + ...formatOptions, + // Since Slang forces us to decide on a compiler version, we need to do the + // same for ANTLR unless it was already given as an option. + compiler: formatOptions.compiler || parser.languageVersion, + parser: "antlr", + plugins: await getPlugins(), + }); + expect(antlrOutput).toEqual(formatResult.output); + } +} + +export { testAntlrFormat as run }; diff --git a/tests/config/test-ast-compare.js b/tests/config/test-ast-compare.js new file mode 100644 index 000000000..c82d27b13 --- /dev/null +++ b/tests/config/test-ast-compare.js @@ -0,0 +1,21 @@ +import * as failedTests from "./failed-format-tests.js"; +import { parse } from "./run-prettier.js"; + +async function testAstCompare(source, formatResult, filename, formatOptions) { + const isAstUnstableTest = failedTests.isAstUnstable(filename, formatOptions); + // Some parsers skip parsing empty files + if (formatResult.changed && source.trim()) { + const [originalAst, formattedAst] = await Promise.all( + [formatResult.input, formatResult.output].map((code) => + parse(code, formatResult.options), + ), + ); + if (isAstUnstableTest) { + expect(formattedAst).not.toEqual(originalAst); + } else { + expect(formattedAst).toEqual(originalAst); + } + } +} + +export { testAstCompare as run }; diff --git a/tests/config/test-bom.js b/tests/config/test-bom.js new file mode 100644 index 000000000..7c8f1ec2a --- /dev/null +++ b/tests/config/test-bom.js @@ -0,0 +1,15 @@ +import { BOM } from "./constants.js"; +import { format } from "./run-prettier.js"; + +async function testBom(source, formatResult, _filename, formatOptions) { + if (source.charAt(0) !== BOM) { + const { eolVisualizedOutput: output } = await format( + BOM + source, + formatOptions, + ); + const expected = BOM + formatResult.eolVisualizedOutput; + expect(output).toEqual(expected); + } +} + +export { testBom as run }; diff --git a/tests/config/test-bytecode-compare.js b/tests/config/test-bytecode-compare.js new file mode 100644 index 000000000..cdf0b0c65 --- /dev/null +++ b/tests/config/test-bytecode-compare.js @@ -0,0 +1,54 @@ +import path from "node:path"; +import createEsmUtils from "esm-utils"; +import compileContract from "./utils/compile-contract.js"; + +async function testBytecodeCompare( + _source, + formatResult, + filename, + formatOptions, +) { + if (shouldCompareBytecode(filename, formatOptions)) { + const output = compileContract(filename, formatResult.output); + const expected = compileContract(filename, formatResult.input); + expect(output).toEqual(expected); + } +} + +const { __dirname } = createEsmUtils(import.meta); + +const testsWithAstChanges = new Map( + [ + "Parentheses/AddNoParentheses.sol", + "Parentheses/SubNoParentheses.sol", + "Parentheses/MulNoParentheses.sol", + "Parentheses/DivNoParentheses.sol", + "Parentheses/ModNoParentheses.sol", + "Parentheses/ExpNoParentheses.sol", + "Parentheses/ShiftLNoParentheses.sol", + "Parentheses/ShiftRNoParentheses.sol", + "Parentheses/BitAndNoParentheses.sol", + "Parentheses/BitOrNoParentheses.sol", + "Parentheses/BitXorNoParentheses.sol", + "Parentheses/LogicNoParentheses.sol", + "HexLiteral/HexLiteral.sol", + "ModifierInvocations/ModifierInvocations.sol", + ].map((fixture) => { + const [file, compareBytecode = () => true] = Array.isArray(fixture) + ? fixture + : [fixture]; + return [path.join(__dirname, "../format/", file), compareBytecode]; + }), +); + +const shouldCompareBytecode = (filename, options) => { + const testFunction = testsWithAstChanges.get(filename); + + if (!testFunction) { + return false; + } + + return testFunction(options); +}; + +export { testBytecodeCompare as run }; diff --git a/tests/config/test-end-of-line.js b/tests/config/test-end-of-line.js new file mode 100644 index 000000000..ae3cb165a --- /dev/null +++ b/tests/config/test-end-of-line.js @@ -0,0 +1,47 @@ +import { format } from "./run-prettier.js"; +import visualizeEndOfLine from "./utils/visualize-end-of-line.js"; + +async function testEndOfLine( + source, + formatResult, + _filename, + formatOptions, + eol, +) { + if (!shouldSkipEolTest(source, formatResult.options)) { + const { eolVisualizedOutput: output } = await format( + source.replace(/\n/gu, eol), + formatOptions, + ); + // Only if `endOfLine: "auto"` the result will be different + const expected = + formatOptions.endOfLine === "auto" + ? visualizeEndOfLine( + // All `code` use `LF`, so the `eol` of result is always `LF` + formatResult.outputWithCursor.replace(/\n/gu, eol), + ) + : formatResult.eolVisualizedOutput; + expect(output).toEqual(expected); + } +} + +function shouldSkipEolTest(source, options) { + if (source.includes("\r")) { + return true; + } + const { requirePragma, rangeStart, rangeEnd } = options; + if (requirePragma) { + return true; + } + + if ( + typeof rangeStart === "number" && + typeof rangeEnd === "number" && + rangeStart >= rangeEnd + ) { + return true; + } + return false; +} + +export { testEndOfLine as run }; diff --git a/tests/config/test-second-format.js b/tests/config/test-second-format.js new file mode 100644 index 000000000..fd6e0df9b --- /dev/null +++ b/tests/config/test-second-format.js @@ -0,0 +1,27 @@ +import * as failedTests from "./failed-format-tests.js"; +import { format } from "./run-prettier.js"; + +async function testSecondFormat(source, formatResult, filename, formatOptions) { + const isUnstableTest = failedTests.isUnstable(filename, formatOptions); + if ( + (formatResult.changed || isUnstableTest) && + // No range and cursor + formatResult.input === source + ) { + const { eolVisualizedOutput: firstOutput, output } = formatResult; + const { eolVisualizedOutput: secondOutput } = await format( + output, + formatOptions, + ); + // To keep eye on failed tests, this assert never supposed to pass, + // if it fails, just remove the file from `unstableTests` + if (isUnstableTest) { + expect(secondOutput).not.toEqual(firstOutput); + return; + } + + expect(secondOutput).toEqual(firstOutput); + } +} + +export { testSecondFormat as run }; diff --git a/tests/config/test-variant-coverage.js b/tests/config/test-variant-coverage.js new file mode 100644 index 000000000..477626421 --- /dev/null +++ b/tests/config/test-variant-coverage.js @@ -0,0 +1,20 @@ +import getCreateParser from "./get-create-parser.js"; +import getVariantCoverage from "./get-variant-coverage.js"; + +async function testVariantCoverage( + source, + _formatResult, + _filename, + formatOptions, +) { + if (formatOptions.parser === "slang") { + const createParser = await getCreateParser(); + const variantCoverage = await getVariantCoverage(); + const { parseOutput } = createParser(source, formatOptions); + + // Check coverage + variantCoverage(parseOutput.tree.asNonterminalNode()); + } +} + +export { testVariantCoverage as run };