From 571b126d2547745c2e9fdd8720e90a77f281c007 Mon Sep 17 00:00:00 2001 From: Byakuren Hijiri Date: Thu, 18 Jul 2024 05:13:21 +0000 Subject: [PATCH] feat(tests): Differential tests based on jest' diff utility --- .gitignore | 1 + src/codegen/codegen.spec.ts | 178 ++++++++++++++++++++++++++++++ src/codegen/contracts/Simple.tact | 5 + src/pipeline/compile.ts | 11 +- 4 files changed, 190 insertions(+), 5 deletions(-) create mode 100644 src/codegen/codegen.spec.ts create mode 100644 src/codegen/contracts/Simple.tact diff --git a/.gitignore b/.gitignore index 7ffdeb777..505434aed 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,5 @@ output/ src/grammar/grammar.ohm-bundle.js src/grammar/grammar.ohm-bundle.d.ts src/func/funcfiftlib.wasm.js +src/codegen/contracts/*.config.json diff --git a/src/codegen/codegen.spec.ts b/src/codegen/codegen.spec.ts new file mode 100644 index 000000000..d16d395d9 --- /dev/null +++ b/src/codegen/codegen.spec.ts @@ -0,0 +1,178 @@ +import * as fs from "fs"; +import * as path from "path"; + +import { __DANGER_resetNodeId } from "../grammar/ast"; +import { compile } from "../pipeline/compile"; +import { precompile } from "../pipeline/precompile"; +import { getContracts } from "../types/resolveDescriptors"; +import { CompilationOutput, CompilationResults } from "../pipeline/compile"; +import { createNodeFileSystem } from "../vfs/createNodeFileSystem"; +import { CompilerContext } from "../context"; + +const CONTRACTS_DIR = path.join(__dirname, "./contracts/"); + +function capitalize(str: string): string { + if (str.length === 0) return str; + return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase(); +} + +/** + * Generates a Tact configuration file for the given contract (imported from Misti). + */ +export function generateConfig(contractName: string): string { + const config = { + projects: [ + { + name: `${contractName}`, + path: `./${contractName}.tact`, + output: `./output`, + options: {}, + }, + ], + }; + const configPath = path.join(CONTRACTS_DIR, `${contractName}.config.json`); + fs.writeFileSync(configPath, JSON.stringify(config), { + encoding: "utf8", + flag: "w", + }); + return configPath; +} + +/** + * Compiles the contract on the given filepath to CompilationResults replicating the Tact compiler pipeline. + */ +async function compileContract( + backend: "new" | "old", + contractName: string, +): Promise { + const _ = generateConfig(contractName); + + // see: pipeline/build.ts + const project = createNodeFileSystem(CONTRACTS_DIR, false); + const stdlib = createNodeFileSystem( + path.resolve(__dirname, "..", "..", "stdlib"), + false, + ); + let ctx: CompilerContext = new CompilerContext({ shared: {} }); + ctx = precompile(ctx, project, stdlib, contractName); + + return await Promise.all( + getContracts(ctx).map(async (contract) => { + const res = await compile( + ctx, + contract, + `${contractName}_${contract}`, + backend, + ); + return res; + }), + ); +} + +function compareCompilationOutputs( + newOut: CompilationOutput, + oldOut: CompilationOutput, +): void { + const errors: string[] = []; + + if (newOut === undefined || oldOut === undefined) { + errors.push("One of the outputs is undefined."); + } else { + try { + expect(newOut.entrypoint).toBe(oldOut.entrypoint); + } catch (error) { + if (error instanceof Error) { + errors.push(`Entrypoint mismatch: ${error.message}`); + } else { + errors.push(`Entrypoint mismatch: ${String(error)}`); + } + } + + try { + expect(newOut.abi).toBe(oldOut.abi); + } catch (error) { + if (error instanceof Error) { + errors.push(`ABI mismatch: ${error.message}`); + } else { + errors.push(`ABI mismatch: ${String(error)}`); + } + } + + const unmatchedFiles = new Set(oldOut.files.map((file) => file.name)); + + for (const newFile of newOut.files) { + const oldFile = oldOut.files.find( + (file) => file.name === newFile.name, + ); + if (oldFile) { + unmatchedFiles.delete(oldFile.name); + try { + expect(newFile.code).toBe(oldFile.code); + } catch (error) { + if (error instanceof Error) { + errors.push( + `Code mismatch in file ${newFile.name}: ${error.message}`, + ); + } else { + errors.push( + `Code mismatch in file ${newFile.name}: ${String(error)}`, + ); + } + } + } else { + errors.push( + `File ${newFile.name} is missing in the old output.`, + ); + } + } + + for (const missingFile of unmatchedFiles) { + errors.push(`File ${missingFile} is missing in the new output.`); + } + } + + if (errors.length > 0) { + throw new Error(errors.join("\n")); + } +} + +describe("codegen", () => { + beforeEach(async () => { + __DANGER_resetNodeId(); + }); + + fs.readdirSync(CONTRACTS_DIR).forEach((file) => { + if (!file.endsWith(".tact")) { + return; + } + const contractName = capitalize(file); + // Differential tests with the old backend + it(`Should compile the ${file} contract`, async () => { + Promise.all([ + compileContract("new", contractName), + compileContract("old", contractName), + ]) + .then(([resultsNew, resultsOld]) => { + if (resultsNew.length !== resultsOld.length) { + throw new Error("Not all contracts have been compiled"); + } + const zipped = resultsNew.map((value, idx) => [ + value, + resultsOld[idx], + ]); + zipped.forEach(([newRes, oldRes]) => { + compareCompilationOutputs( + newRes!.output, + oldRes!.output, + ); + }); + }) + .catch((error) => { + console.error( + "An error occurred during compilation:", + error, + ); + }); + }); + }); +}); diff --git a/src/codegen/contracts/Simple.tact b/src/codegen/contracts/Simple.tact new file mode 100644 index 000000000..4e06edb30 --- /dev/null +++ b/src/codegen/contracts/Simple.tact @@ -0,0 +1,5 @@ +contract A { + get fun foo(): Int { + return 1; + } +} diff --git a/src/pipeline/compile.ts b/src/pipeline/compile.ts index 0b98e854a..04c6668a3 100644 --- a/src/pipeline/compile.ts +++ b/src/pipeline/compile.ts @@ -24,10 +24,11 @@ export async function compile( ctx: CompilerContext, contractName: string, abiName: string, + backend: "new" | "old" = "old", ): Promise { const abi = createABI(ctx, contractName); let output: CompilationOutput; - if (process.env.NEW_CODEGEN === "1") { + if (backend === "new" || process.env.NEW_CODEGEN === "1") { output = await FuncGenerator.fromTactProject( ctx, abi, @@ -36,9 +37,9 @@ export async function compile( } else { output = await writeProgram(ctx, abi, abiName); } - console.log(`${contractName} output:`); - output.files.forEach((o) => - console.log(`---------------\nname=${o.name}; code:\n${o.code}\n`), - ); + // console.log(`${contractName} output:`); + // output.files.forEach((o) => + // console.log(`---------------\nname=${o.name}; code:\n${o.code}\n`), + // ); return { output, ctx }; }