Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
587b516
feat: add test stub generation
skywardboundd Jun 2, 2025
aafb8dc
fmt
skywardboundd Jun 2, 2025
621821a
may be fix
skywardboundd Jun 2, 2025
a9dc455
fmt
skywardboundd Jun 2, 2025
95fde95
x2 fix
skywardboundd Jun 2, 2025
2f0e22c
fix getter names
skywardboundd Jun 2, 2025
cdb4077
fix :(
skywardboundd Jun 2, 2025
55f5585
use cleanText in text recievers
skywardboundd Jun 2, 2025
9cd771c
back eslintrc
skywardboundd Jun 2, 2025
7aea274
use mustache
skywardboundd Jun 4, 2025
c02a836
fmt
skywardboundd Jun 4, 2025
d2e85d9
review
skywardboundd Jun 5, 2025
15b9eef
Merge remote-tracking branch 'origin/main' into 3287-we-should-genera…
skywardboundd Jun 5, 2025
918d13c
fix
skywardboundd Jun 5, 2025
ab166e8
upd
skywardboundd Jun 5, 2025
df7ea9e
fix
skywardboundd Jun 5, 2025
743c6bd
fix
skywardboundd Jun 6, 2025
eec818c
fmt
skywardboundd Jun 6, 2025
7d7c54c
Merge branch 'main' into 3287-we-should-generate-test-template-as-wra…
skywardboundd Jun 6, 2025
7e67feb
fix windows shvindows bobush vobush
skywardboundd Jun 6, 2025
3da6ee7
Merge branch '3287-we-should-generate-test-template-as-wrappers' of h…
skywardboundd Jun 6, 2025
8c5945e
fmt aggaaain
skywardboundd Jun 6, 2025
916dec5
update docs
skywardboundd Jun 6, 2025
e885c60
Update docs/src/content/docs/book/debug.mdx
skywardboundd Jun 6, 2025
ada9062
Update docs/src/content/docs/book/debug.mdx
skywardboundd Jun 6, 2025
39d2d0f
Update docs/src/content/docs/book/debug.mdx
skywardboundd Jun 6, 2025
ce0bb6a
Update docs/src/content/docs/book/debug.mdx
skywardboundd Jun 6, 2025
4c33fb7
Update docs/src/content/docs/book/compile.mdx
skywardboundd Jun 6, 2025
40cd7a7
Update docs/src/content/docs/book/compile.mdx
skywardboundd Jun 6, 2025
c94d3f6
Update docs/src/content/docs/book/debug.mdx
skywardboundd Jun 6, 2025
e946949
Update docs/src/content/docs/book/compile.mdx
skywardboundd Jun 6, 2025
5ecc307
Update docs/src/content/docs/book/compile.mdx
skywardboundd Jun 6, 2025
e12ec2c
Update docs/src/content/docs/book/config.mdx
skywardboundd Jun 6, 2025
e6ad976
Update docs/src/content/docs/book/compile.mdx
skywardboundd Jun 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
185 changes: 185 additions & 0 deletions src/bindings/writeTests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
import { Writer } from "@/utils/Writer";
import type { ABIArgument, ContractABI, ABIReceiver } from "@ton/core";
import type { WrappersConstantDescription } from "@/bindings/writeTypescript";
import type { CompilerContext } from "@/context/context";
import type { TypeDescription } from "@/types/types";

function getReceiverFunctionName(receiver: ABIReceiver): string {
const receiverType = receiver.receiver; // 'internal' or 'external'
const messageKind = receiver.message.kind; // 'empty', 'typed', 'text', 'any'

let name = receiverType.charAt(0).toUpperCase() + receiverType.slice(1); // Internal or External

switch (messageKind) {
case "empty":
name += "Empty";
break;
case "typed":
name += "Message";
name += receiver.message.type ?? "Typed";
break;
case "text":
name += "Text";
if (receiver.message.text) {
const cleanText = receiver.message.text.replace(
/[^a-zA-Z0-9]/g,
"",
);
name += cleanText.charAt(0).toUpperCase() + cleanText.slice(1);
}
break;
case "any":
name += "Any";
break;
default:
name += "Unknown";
}

return name;
}

export function writeTests(
abi: ContractABI,
_ctx: CompilerContext,
_constants: readonly WrappersConstantDescription[],
_contract: undefined | TypeDescription,
generatedContractPath: string,
_init?: {
code: string;
system: string | null;
args: ABIArgument[];
prefix?:
| {
value: number;
bits: number;
}
| undefined;
},
) {
const w = new Writer();
const contractName = abi.name ?? "Contract";

w.write(
`// !!THIS FILE IS GENERATED BY TACT. THIS FILE IS REGENERATED EVERY TIME, COPY IT TO YOUR PROJECT MANUALLY!!`,
);
w.write(`// https://docs.tact-lang.org/book/debug/`);
w.append();

w.write(`import { ${contractName} } from './${generatedContractPath}';`);

w.write(`import { Blockchain, createShardAccount } from "@ton/sandbox";`);
w.append();

const FromInit = `FromInit${contractName}`;
w.write(`export type ${FromInit} = typeof ${contractName}.fromInit;`);
w.append();
w.write(`export type TestCase = (fromInit: ${FromInit}) => void;`);
w.append();

w.write(`export const test${contractName} = (fromInit: ${FromInit}) => {`);
w.inIndent(() => {
w.write(`describe("${contractName} Contract", () => {`);
w.inIndent(() => {
w.write("// Test receivers");
if (abi.receivers && abi.receivers.length > 0) {
for (const receiver of abi.receivers) {
const receiverName = getReceiverFunctionName(receiver);
w.write(`test${receiverName}(fromInit);`);
}
}
w.write("// Test getters");
if (abi.getters && abi.getters.length > 0) {
for (const getter of abi.getters) {
w.write(`getterTest${getter.name}(fromInit);`);
}
}
});
w.write(`});`);
});
w.write(`};`);

w.append();

w.write(`const globalSetup = async (fromInit: ${FromInit}) => {`);
w.inIndent(() => {
w.append("const blockchain = await Blockchain.create();");
w.append("// @ts-ignore");
w.append(`const contract = await blockchain.openContract(await fromInit(
// TODO: implement default values
));
`);

w.append(
"// Universal method for deploy contract without sending message",
);
w.append(`await blockchain.setShardAccount(contract.address, createShardAccount({
address: contract.address,
code: contract.init!.code,
data: contract.init!.data,
balance: 0n,
workchain: 0
}));
`);

w.append(`const owner = await blockchain.treasury("owner");`);
w.append(`const notOwner = await blockchain.treasury("notOwner");`);
w.append();

w.append(`return { blockchain, contract, owner, notOwner };`);
});
w.write("};");
w.append();

const generateBody = (functionName: string, name: string) => {
w.write(`const ${functionName}: TestCase = (fromInit) => {`);
w.inIndent(() => {
w.write(`describe("${name}", () => {`);
w.inIndent(() => {
w.write(`const setup = async () => {`);
w.inIndent(() => {
w.write(`return await globalSetup(fromInit);`);
});
w.write(`};`);
w.append();
w.write(
`// !!THIS FILE IS GENERATED BY TACT. THIS FILE IS REGENERATED EVERY TIME, COPY IT TO YOUR PROJECT MANUALLY!!`,
);
w.write(`// TODO: You can write tests for ${name} here`);
w.append();
w.write(`it("should perform correctly", async () => {`);
w.inIndent(() => {
w.write(
`const { blockchain, contract, owner, notOwner } = await setup();`,
);
});
w.write(`});`);
});
w.write(`});`);
});
w.write(`};`);
w.append();
};

// Generate tests for receivers
if (abi.receivers && abi.receivers.length > 0) {
for (const receiver of abi.receivers) {
const receiverName = getReceiverFunctionName(receiver);
generateBody("test" + receiverName, receiverName);
}
}

// Generate tests for getters
if (abi.getters && abi.getters.length > 0) {
for (const getter of abi.getters) {
generateBody("getterTest" + getter.name, getter.name);
}
}

w.write("// entry point");
w.write(
`test${contractName}(${contractName}.fromInit.bind(${contractName}));`,
);
w.append();

return w.end();
}
6 changes: 6 additions & 0 deletions src/pipeline/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type { TypeDescription } from "@/types/types";
import { doPackaging } from "@/pipeline/packaging";
import { doBindings } from "@/pipeline/bindings";
import { doReports } from "@/pipeline/reports";
import { doTests } from "@/pipeline/tests";
import { createVirtualFileSystem } from "@/vfs/createVirtualFileSystem";
import * as Stdlib from "@/stdlib/stdlib";

Expand Down Expand Up @@ -144,6 +145,11 @@ export async function build(args: {
return BuildFail(bCtx.errorMessages);
}

const testsRes = doTests(bCtx, packages);
if (!testsRes) {
return BuildFail(bCtx.errorMessages);
}

const reportsRes = doReports(bCtx, packages);
if (!reportsRes) {
return BuildFail(bCtx.errorMessages);
Expand Down
49 changes: 49 additions & 0 deletions src/pipeline/tests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { writeTests } from "@/bindings/writeTests";
import type { BuildContext } from "@/pipeline/build";
import type { Packages } from "@/pipeline/packaging";

export function doTests(bCtx: BuildContext, packages: Packages): boolean {
const { project, config, logger } = bCtx;

logger.info(" > Tests");

for (const pkg of packages) {
logger.info(` > ${pkg.name}`);

if (pkg.init.deployment.kind !== "system-cell") {
const message = ` > ${pkg.name}: unsupported deployment kind ${pkg.init.deployment.kind}`;
logger.error(message);
bCtx.errorMessages.push(new Error(message));
return false;
}

try {
const testsCode = writeTests(
JSON.parse(pkg.abi),
bCtx.ctx,
bCtx.built[pkg.name]?.constants ?? [],
bCtx.built[pkg.name]?.contract,
config.name + "_" + pkg.name,
{
code: pkg.code,
prefix: pkg.init.prefix,
system: pkg.init.deployment.system,
args: pkg.init.args,
},
);
const testsPath = project.resolve(
config.output,
config.name + "_" + pkg.name + ".stub.tests.ts",
);
project.writeFile(testsPath, testsCode);
} catch (e) {
const error = e as Error;
error.message = `Tests generator crashed: ${error.message}`;
logger.error(error);
bCtx.errorMessages.push(error);
return false;
}
}

return true;
}
Loading