From 6b9fd25f564899d75cab326a501c9dcd5f7bbed6 Mon Sep 17 00:00:00 2001 From: Novus Nota <68142933+novusnota@users.noreply.github.com> Date: Sat, 25 May 2024 20:49:19 +0200 Subject: [PATCH 01/13] feat: new debugging page WIP: 3 more things to do --- pages/book/debug.mdx | 283 ++++++++++++++++++++++++------------------- 1 file changed, 157 insertions(+), 126 deletions(-) diff --git a/pages/book/debug.mdx b/pages/book/debug.mdx index 0a4f1ff7..aeed9d9f 100644 --- a/pages/book/debug.mdx +++ b/pages/book/debug.mdx @@ -1,166 +1,197 @@ -# Debugging Tact Contracts +# Debugging Tact contracts -Tact has first-class support for Jest and `@tact-lang/emulator` for contract tests and debugging. The preferred type of test is the one with inline snapshots. They are easy to write and easy to debug. +import { Callout, Steps, Tabs } from 'nextra/components' -For a quick start, it is better to use `tact-template` that has everything built in. +Without fail, the code we write as smart contract developers doesn’t always do what we expected it to do. Sometimes it does something completely different! When the unexpected happens, the next task is to figure out why. To do so, there are various ways to reveal problems or "bugs" in the code. Let's get to *debugging*! -## Printing out debug data +## General approach [#approach] -Tact has a built-in `dump` function that is similar to the one in FunC that allows you to print out data in your contract. It is very useful for debugging. + -To make `dump` work you need to enable the feature `debug` in `tact.conf.json`. +### Clarify the problem by asking yourself the right questions [#approach-1] -Example: -```tact -dump("Hello World"); -dump(123 + 444); -dump(a != null); -``` +It helps to clarify the problem that you ran into before you try to fix it. Furthermore, clearly stated problems are much more easier to understand for someone else — this is very handy if you want to get meaningful help when asking in Tact's [Telegram chat][tg] or filling an issue on Tact's [GitHub](https://github.com/tact-lang). + +So, before you start debugging, make sure you've identified the problem you're trying to solve: + +1. What did you expect your code to do? + +2. What happened instead? + + If you've run into a syntax error (breaking the rules of the language), reference error (using the wrong names), type error (confusing one type for another) or some other exception during compilation, that's great! This means that compiler has already found the issue for you, and now all that's left is to fix it. + + If something else happened, it's most likely a logic error in your code, when your expectations didn't match the actual state the contract got in. To resolve that, try stepping through your code and checking state of variables in it via [`dump(){:tact}`][dump] function or alike. + +### Examine your assumptions [#approach-2] + +Before you investigate a bug or an error, think of the assumptions that made you expect a certain result. Unknown or unclear expectations can get in the way of identifying a problem, even when you're looking right at the cause of the problem. + +Here are a few questions to ask yourself to challenge your assumptions: + +* Are you using the right API (that is, the right [global static function](/book/functions#global-static-functions) or [extension function](/book/functions#extension-function))? An API that you're using might not do what you think it does, so make sure to consult with the [Reference](/ref) section or ask in the Tact's [Telegram chat][tg]. + +* Are you using an API correctly? Maybe, you used the right API but didn't use it in the right way. + +* Did you make a change to your code and assume it's unrelated to the issue you're facing? A common pitfall here is to modify the code and try to run the tests right away, without compiling the changes first. -## Using `@tact-lang/emulator` +* Did you expect variable or a [`Cell{:tact}`](/book/types#primitive-types) to contain a certain value (or a certain type of value) that is different from what was really there? Pay attention to your types and data layouts, especially their representation in [TL-B schemas](https://docs.ton.org/develop/data-formats/tl-b-language). -Ton Emulator allows you to have a small virtual blockchain in your Node.js code. This library is built specifically for testing smart contracts in unit tests. +* Do you know the intent of the code? It's often more difficult to debug someone else's code. If it's not your code, it's possible you might need to spend time learning exactly what the code does before you can debug it effectively. -```typescript -import { ContractSystem } from '@tact-lang/emulator'; -import { sample_Contract } from './output/sample_Contract'; + -// -// Init System -// + When writing contracts, start small and start with code that works! Sometimes, it is easier to fix a large or complicated set of code by starting with a small piece of code that demonstrates the core task you are trying to achieve. Then, you can modify or add code incrementally, testing at each point for errors. -// Contract System is a virtual environment that emulates the TON blockchain -const system = await ContractSystem.create(); + Here, it may be helpful to test your assumptions in a small [experimental playground](#lab) before rolling out a complete implementation. -// Treasure is a contract that has 1m of TONs and is a handy entry point for your smart contracts -let treasure = await system.treasure('name of treasure'); + -// -// Open contract -// +### Go over your code and observe the values [#step-3] -// Contract itself -let contract = system.open(sample_Contract.fromInit(treasure.address)); +At the moment, Tact doesn't have a step-through debugger. Despite that, it's still possible to use the [_printf debugging_](https://en.wikipedia.org/wiki/Debugging#printf_debugging) approach. -// This object would track all transactions in this contract -let tracker = system.track(contract.address); +It involves actively placing [`dump(){:tact}`][dump] and [`dumpStack(){:tact}`](/ref/core-debug#dumpstack) functions throughout your code and observing states of variables at a given point of time. Note, that those functions work only in a [debug mode](#debug-mode) and won't be executed otherwise. -// This object would track all logs -let logger = system.log(contract.address); +Once you found that some value isn't equal to what you've expected it to be, don't rush to fixing the issue on the spot. That's because what you're seeing may not be the root cause of it and merely a symptom, effect. Be very careful with cause-and-effect relationships and figure out which's which to resolve the cause and not introduce new mess for your future self. -// -// Sending a message -// +In addition to dumping values, it's often helpful to use assertive functions like [`require(){:tact}`](/ref/core-debug#require), [`nativeThrowWhen(){:tact}`](/ref/core-debug#nativethrowwhen) and [`nativeThrowUnless(){:tact}`](/ref/core-debug#nativethrowunless). They help stating your assumptions clear, and are handy for setting up "trip wires" for catching issues in the future. -// First we enqueue messages. NOTE: You can enqueue multiple messages in a row -await contract.send(treasure, { value: toNano(1) }, { $$type: "Deploy", queryId: 0n }); -await contract.send(treasure, { value: toNano(1) }, { $$type: "Increment" }); +And if you didn't find or cannot resolve the cause of your issues, try asking the community in Tact's [Telegram chat][tg] or, if your issue or question is generally related to TON more than it's related to Tact, hop into [TON Dev Telegram chat](https://t.me/tondev_eng). -// Run the system until there are no more messages -await system.run(); + -// -// Collecting results -// +## Enabling debug mode in compilation options [#debug-mode] -console.log(track.collect()); // Prints out all transactions in contract -console.log(logger.collect()); // Prints out all logs for each transaction +In order to make certain functions like [`dump(){:tact}`][dump] or [`dumpStack(){:tact}`](/ref/core-debug#dumpstack) work, one needs to enable debug mode. -// -// Invoking get methods -// +The simplest and recommended approach is to modify a [`tact.config.json`](/book/config) file in the root of your project (or create it if it didn't exist yet), and [set the `debug` option to `true{:tact}`](/book/config#options-debug). -let counter = await contract.getCounter(); -console.log(counter); // Prints out counter value +If you're working on a [Blueprint][bp]-based project, you can enable debug mode in the compilation configs of your contracts, which are located in a directory named `wrappers/`: +```typescript filename="wrappers/YourContractName.compile.ts" {7} +import { CompilerConfig } from '@ton/blueprint'; + +export const compile: CompilerConfig = { + lang: 'tact', + target: 'contracts/your_contract_name.tact', + options: { + debug: true, // ← that's the stuff! + } +}; ``` -## Snapshot testing with `jest` +Note, that `tact.config.json` may still be used in [Blueprint][bp] projects. In such cases values specified in `tact.config.json` act as default unless modified in the `wrappers/`. + + + + Read more about configuration and `tact.config.json` file: [Configuration](/book/config). + + + +## Writing tests with `jest` and Blueprint [#tests] + +{/* TODO: Refine local text */} + +## Logging via `emit` [#logging] + +{/* TODO: Refine local text */} + +## Handling bounced messages [#bounced] + +{/* TODO: Refine local text */} + +## Experimental lab setup [#lab] -One of the most powerful features of Jest is the ability to write snapshot tests. Snapshot tests are a great way to test your contracts. +If you're overwhelmed by the testing setup of [Blueprint][bp] or just want to test some things quickly, worry not — there is a way to set up a simple playground as an experimental lab to test your ideas and hypotheses. -### Initial setup + -Example of a minimal test file: +### Generate a new contract [#lab-1] -```typescript -import { ContractSystem } from '@tact-lang/emulator'; -import { sample_Contract } from './output/sample_Contract'; +It can be named anything, but I'll name it `Playground` to convey the right intent. To create it, run the following in the root of your [Blueprint][bp] project and remember to make an empty Tact contract by selecting the proper version from the list: -describe("contract", () => { - it("should deploy correctly", async () => { +```shell +npx blueprint create Playground +``` + +Versions of [Blueprint][bp] starting with $0.20.0$ automatically enable debug mode in `wrappers/` for new contracts, so we only have to adjust the testing suite. + +### Update the test suite [#lab-2] - // Init test - const system = await ContractSystem.create(); - const treasure = await system.treasure('my treasure'); - const contract = system.open(sample_Contract.fromInit(treasure.address)); - const tracker = system.track(contract.address); +In the `tests/Playground.spec.ts`, change the `"should deploy"` test closure to the following: - // Send a message - await contract.send(treasure, { value: toNano(1) }, { $$type: "Deploy", queryId: 0n }); - await system.run(); +```typescript filename="tests/Playground.spec.ts" +it('_playground', async () => { + const res = await playground.send( + deployer.getSender(), + { value: toNano('0.5') }, + '_playground', + ); - // Testing output - expect(tracker.collect()).toMatchInlineSnapshot(); - }); + console.log("Address of our contract: " + playground.address); + console.log(res.externals); // ← here one would see results of emit() calls }); ``` -### Generating snapshots - -After running the `yarn jest` command, the line with `toMatchInlineSnapshot` of the test will be automatically updated with a snapshot of the output. - -```typescript -// ... - expect(tracker.collect()).toMatchInlineSnapshot(` - [ - { - "$seq": 0, - "events": [ - { - "$type": "deploy", - }, - { - "$type": "received", - "message": { - "body": { - "cell": "x{946A98B60000000000000000}", - "type": "cell", - }, - "bounce": true, - "from": "kQAI-3FJVc_ywSuY4vq0bYrzR7S4Och4y7bTU_i5yLOB3A6P", - "to": "kQBrSAP2y7QIUw4_1q0qciHfqdFmOYR9CC1oinn7kyWWRuoV", - "type": "internal", - "value": 1000000000n, - }, - }, - { - "$type": "processed", - "gasUsed": 6077n, - }, - { - "$type": "sent", - "messages": [ - { - "body": { - "cell": "x{AFF90F570000000000000000}", - "type": "cell", - }, - "bounce": true, - "from": "kQBrSAP2y7QIUw4_1q0qciHfqdFmOYR9CC1oinn7kyWWRuoV", - "to": "kQAI-3FJVc_ywSuY4vq0bYrzR7S4Och4y7bTU_i5yLOB3A6P", - "type": "internal", - "value": 992727000n, - }, - ], - }, - ], - }, - ] - `); -// ... +### Modify the contract [#lab-3] + +Replace the code in `contracts/playground.tact` with the following: + +```tact filename="contracts/playground.tact" {4-6} +import "@stdlib/deploy"; + +contract Playground with Deployable { + receive("_playground") { + // NOTE: write your test logic here! + } +} +``` + +The basic idea of this setup is to place the code you want to test into the [receiver function](/book/contracts#receiver-functions) responding to the [string](/book/types#primitive-types) message `"_playground"{:tact}`. + +Note, that you can still write any valid Tact code outside of that [receiver](/book/contracts#receiver-functions). But in order to test it you'll need to write related test logic inside of it. + +### Let's test! [#lab-4] + +With that, our experimental lab setup is complete. To execute that single test we've prepared for our `Playground` contract, run the following: + +```shell +yarn test -t _playground +``` + +From now on, to test something you only need to modify the contents of the tested [receiver function](/book/contracts#receiver-functions) of your Tact contract file and re-run the command above. Rinse and repeat that process until you've tested what you wanted to test. + +For simplicity and cleaner output's sake, you may add a new field to `scripts` in your `package.json`, such that you'll only need to run `yarn lab{:shell}` to build and test in one. + +On Linux or macOS, it would look like: + +```json filename="package.json" {3} +{ + "scripts": { + "lab": "blueprint build 1>/dev/null && yarn test -t _playground" + } +} +``` + +And here's how it may look on Windows: + +```json filename="package.json" {3-4} +{ + "scripts": { + "build": "blueprint build | out-null" + "lab": "yarn build && yarn test -t _playground" + } +} +``` + +To run: + +```shell +yarn lab ``` -### Updating snapshots + -When you change your contract, your snapshots will be outdated. For example, gas usage or addresses were changed. To update them, you need to run the `yarn jest -u` command. +[dump]: /ref/core-debug#dump +[tg]: https://t.me/tactlang +[bp]: https://github.com/ton-org/blueprint From 23d7f03284e3874f13d97d7e519abb7c01c42fda Mon Sep 17 00:00:00 2001 From: Novus Nota <68142933+novusnota@users.noreply.github.com> Date: Sun, 26 May 2024 13:16:08 +0200 Subject: [PATCH 02/13] Apply suggestions from code review Co-authored-by: Anton Trunov --- pages/book/debug.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pages/book/debug.mdx b/pages/book/debug.mdx index aeed9d9f..2fc1ecfa 100644 --- a/pages/book/debug.mdx +++ b/pages/book/debug.mdx @@ -28,7 +28,7 @@ Before you investigate a bug or an error, think of the assumptions that made you Here are a few questions to ask yourself to challenge your assumptions: -* Are you using the right API (that is, the right [global static function](/book/functions#global-static-functions) or [extension function](/book/functions#extension-function))? An API that you're using might not do what you think it does, so make sure to consult with the [Reference](/ref) section or ask in the Tact's [Telegram chat][tg]. +* Are you using the right API (that is, the right [global static function](/book/functions#global-static-functions) or [extension function](/book/functions#extension-function))? An API that you're using might not do what you think it does, so make sure to consult with the [Reference](/ref) section or ask in the Tact [Telegram chat][tg]. * Are you using an API correctly? Maybe, you used the right API but didn't use it in the right way. @@ -114,7 +114,7 @@ It can be named anything, but I'll name it `Playground` to convey the right inte npx blueprint create Playground ``` -Versions of [Blueprint][bp] starting with $0.20.0$ automatically enable debug mode in `wrappers/` for new contracts, so we only have to adjust the testing suite. +Versions of [Blueprint][bp] starting with 0.20.0 automatically enable debug mode in `wrappers/` for new contracts, so we only have to adjust the testing suite. ### Update the test suite [#lab-2] From 6c8298fc4f620ccbffcc8a04aad0aa4cf5106374 Mon Sep 17 00:00:00 2001 From: Novus Nota <68142933+novusnota@users.noreply.github.com> Date: Sat, 8 Jun 2024 18:37:56 +0200 Subject: [PATCH 03/13] change experimental lab setup to use external project --- pages/book/debug.mdx | 55 +++++++++++++++++++++++++++++--------------- 1 file changed, 37 insertions(+), 18 deletions(-) diff --git a/pages/book/debug.mdx b/pages/book/debug.mdx index 2fc1ecfa..89db3296 100644 --- a/pages/book/debug.mdx +++ b/pages/book/debug.mdx @@ -106,26 +106,45 @@ If you're overwhelmed by the testing setup of [Blueprint][bp] or just want to te -### Generate a new contract [#lab-1] - -It can be named anything, but I'll name it `Playground` to convey the right intent. To create it, run the following in the root of your [Blueprint][bp] project and remember to make an empty Tact contract by selecting the proper version from the list: - -```shell -npx blueprint create Playground -``` - -Versions of [Blueprint][bp] starting with 0.20.0 automatically enable debug mode in `wrappers/` for new contracts, so we only have to adjust the testing suite. +### Create a new Blueprint project [#lab-1] + +That will prevent pollution of your existing one with arbitrary code and tests. + +The new project can be named anything, but I'll name it `Playground` to convey the right intent. + +To create it, run the following command: + + + + ```shell + npm create ton -- tact-playground --type tact-empty --contractName Playground + ``` + + + ```shell + # recommended + yarn create ton tact-playground --type tact-empty --contractName Playground + ``` + + + ```shell + pnpm create ton tact-playground --type tact-empty --contractName Playground + ``` + + + +Versions of [Blueprint][bp] starting with 0.20.0 automatically enable debug mode in `wrappers/` for new contracts, so we only have to adjust the testing suite and prepare our `Playground` contract for testing. ### Update the test suite [#lab-2] -In the `tests/Playground.spec.ts`, change the `"should deploy"` test closure to the following: +Move into the newly created `tact-playground/` project and in the `tests/Playground.spec.ts`, change the `"should deploy"` test closure to the following: ```typescript filename="tests/Playground.spec.ts" -it('_playground', async () => { +it('plays', async () => { const res = await playground.send( deployer.getSender(), - { value: toNano('0.5') }, - '_playground', + { value: toNano('0.5') }, // ← here you may increase the value in nanoToncoins sent + 'plays', ); console.log("Address of our contract: " + playground.address); @@ -141,13 +160,13 @@ Replace the code in `contracts/playground.tact` with the following: import "@stdlib/deploy"; contract Playground with Deployable { - receive("_playground") { + receive("plays") { // NOTE: write your test logic here! } } ``` -The basic idea of this setup is to place the code you want to test into the [receiver function](/book/contracts#receiver-functions) responding to the [string](/book/types#primitive-types) message `"_playground"{:tact}`. +The basic idea of this setup is to place the code you want to test into the [receiver function](/book/contracts#receiver-functions) responding to the [string](/book/types#primitive-types) message `"plays"{:tact}`. Note, that you can still write any valid Tact code outside of that [receiver](/book/contracts#receiver-functions). But in order to test it you'll need to write related test logic inside of it. @@ -156,7 +175,7 @@ Note, that you can still write any valid Tact code outside of that [receiver](/b With that, our experimental lab setup is complete. To execute that single test we've prepared for our `Playground` contract, run the following: ```shell -yarn test -t _playground +yarn test -t plays ``` From now on, to test something you only need to modify the contents of the tested [receiver function](/book/contracts#receiver-functions) of your Tact contract file and re-run the command above. Rinse and repeat that process until you've tested what you wanted to test. @@ -168,7 +187,7 @@ On Linux or macOS, it would look like: ```json filename="package.json" {3} { "scripts": { - "lab": "blueprint build 1>/dev/null && yarn test -t _playground" + "lab": "blueprint build 1>/dev/null && yarn test -t plays" } } ``` @@ -179,7 +198,7 @@ And here's how it may look on Windows: { "scripts": { "build": "blueprint build | out-null" - "lab": "yarn build && yarn test -t _playground" + "lab": "yarn build && yarn test -t plays" } } ``` From 1572c80ccee09cab688b031980352d8b17e8a7ea Mon Sep 17 00:00:00 2001 From: Novus Nota <68142933+novusnota@users.noreply.github.com> Date: Sat, 8 Jun 2024 19:06:06 +0200 Subject: [PATCH 04/13] bouncin' --- pages/book/debug.mdx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/pages/book/debug.mdx b/pages/book/debug.mdx index 89db3296..600f71da 100644 --- a/pages/book/debug.mdx +++ b/pages/book/debug.mdx @@ -98,7 +98,17 @@ Note, that `tact.config.json` may still be used in [Blueprint][bp] projects. In ## Handling bounced messages [#bounced] -{/* TODO: Refine local text */} +When [sent](/book/send) with `bounce: true{:tact}`, messages can bounce back in case of errors. Make sure to write relevant [`bounced(){:tact}`](/book/bounced) message receivers and handle bounced messages gracefully: + +```tact +bounced(msg: YourMessage) { + // ...alright squad, let's bounce!... +} +``` + +Keep in mind that bounced messages in TON have only $224$ usable data bits in their message body and don't have any references, so one cannot recover much data from it. However, you still get to see whether the message has bounced or not, allowing you to create more robust contracts. + +Read more about bounced messages and receivers: [Bounced messages](/book/bounced). ## Experimental lab setup [#lab] From 1aab97ffc079ef4cb0084bedfcf6b6fda6e8f0f8 Mon Sep 17 00:00:00 2001 From: Novus Nota <68142933+novusnota@users.noreply.github.com> Date: Sun, 9 Jun 2024 15:51:33 +0200 Subject: [PATCH 05/13] little whoopsie fixed --- pages/book/operators.mdx | 2 +- pages/ref/core-common.mdx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pages/book/operators.mdx b/pages/book/operators.mdx index 6303c0e7..9d44bb14 100644 --- a/pages/book/operators.mdx +++ b/pages/book/operators.mdx @@ -471,7 +471,7 @@ two ^ 3; // 1 Binary bar (_bitwise OR_) operator `|{:tact}` applies a [bitwise OR](https://en.wikipedia.org/wiki/Bitwise_operation#OR), which performs the [logical OR](#binary-logical-or) operation on each pair of the corresponding bits of operands. This is useful when we want to apply a specific [bitmask](https://en.wikipedia.org/wiki/Mask_(computing)). -For example, _bitwise OR_ is commonly used in Tact to [combine base modes with optional flags](http://localhost:3000/book/message-mode#combining-modes-with-flags) by masking specific bits to $1$ in order to construct a target [message `mode`](/book/message-mode). +For example, _bitwise OR_ is commonly used in Tact to [combine base modes with optional flags](/book/message-mode#combining-modes-with-flags) by masking specific bits to $1$ in order to construct a target [message `mode`](/book/message-mode). Can only be applied to values of type [`Int{:tact}`][int]: diff --git a/pages/ref/core-common.mdx b/pages/ref/core-common.mdx index cf1da95b..7ec3137b 100644 --- a/pages/ref/core-common.mdx +++ b/pages/ref/core-common.mdx @@ -235,14 +235,14 @@ emit("Catch me if you can, Mr. Holmes".asComment()); // asComment() converts a S - To analyze `emit(){:tact}` calls, one has to look at [external messages](http://localhost:3000/book/external) produced by the contract. For example, when deploying in [Sandbox](https://github.com/ton-org/sandbox), you may call `emit(){:tact}` from a [receiver function](/book/contracts#receiver-functions) and then observe the list of sent external messages: + To analyze `emit(){:tact}` calls, one has to look at [external messages](/book/external) produced by the contract. For example, when deploying in [Sandbox](https://github.com/ton-org/sandbox), you may call `emit(){:tact}` from a [receiver function](/book/contracts#receiver-functions) and then observe the list of sent external messages: ```typescript {9-10} it('emits', async () => { const res = await simpleCounter.send( deployer.getSender(), { value: toNano('0.05') }, - 'emit_receiver', + 'emit_receiver', // ← change to the message your receiver handles ); console.log("Address of our contract: " + simpleCounter.address); From b54dc1d32029e057300b257fdb1e67977bb682e7 Mon Sep 17 00:00:00 2001 From: Novus Nota <68142933+novusnota@users.noreply.github.com> Date: Sun, 9 Jun 2024 16:54:56 +0200 Subject: [PATCH 06/13] emit() and how to work with it --- pages/book/debug.mdx | 34 +++++++++++++++++++++++++++++++--- pages/ref/core-common.mdx | 18 +++--------------- 2 files changed, 34 insertions(+), 18 deletions(-) diff --git a/pages/book/debug.mdx b/pages/book/debug.mdx index 600f71da..0bdd27fd 100644 --- a/pages/book/debug.mdx +++ b/pages/book/debug.mdx @@ -94,7 +94,35 @@ Note, that `tact.config.json` may still be used in [Blueprint][bp] projects. In ## Logging via `emit` [#logging] -{/* TODO: Refine local text */} +A [global static function](/book/functions#global-static-functions) [`emit(){:tact}`](/ref/core-common#emit) sends a message to the outer world — it doesn't have a specific recipient. + +This function is very handy for logging and analyzing data off-chain — one just has to look at [external messages](/book/external) produced by the contract. + +### Logs in local Sandbox tests [#logging-local] + +When deploying in the [Sandbox](https://github.com/ton-org/sandbox), you may call [`emit(){:tact}`](/ref/core-common#emit) from a [receiver function](/book/contracts#receiver-functions) and then observe the list of sent [external messages](/book/external): + +```typescript {9-10} +it('emits', async () => { + const res = await simpleCounter.send( + deployer.getSender(), + { value: toNano('0.05') }, + 'emit_receiver', // ← change to the message your receiver handles + ); + + console.log("Address of our contract: " + simpleCounter.address); + console.log(res.externals); // ← here one would see results of emit() calls, + // and all external messages in general +}); +``` + +### Logs of a deployed contract [#logging-deployed] + +Every transaction on TON Blockchain [contains `out_msgs`](https://docs.ton.org/develop/data-formats/transaction-layout#transaction) — a dictionary that holds the list of outgoing messages that were created while executing the transaction. + +To see logs from [`emit(){:tact}`](/ref/core-common#emit) in that dictionary, look for external messages without a recipient. In various TON Blockchain explorers, such transactions will be marked as `external-out` with destination specified as `-` or `empty`. + +Note, that some explorers deserialize the message body sent for you, while others don't. ## Handling bounced messages [#bounced] @@ -147,7 +175,7 @@ Versions of [Blueprint][bp] starting with 0.20.0 automatically enable debug mode ### Update the test suite [#lab-2] -Move into the newly created `tact-playground/` project and in the `tests/Playground.spec.ts`, change the `"should deploy"` test closure to the following: +Move into the newly created `tact-playground/` project and in the `tests/Playground.spec.ts`, change the `"should deploy"{:tact}` test closure to the following: ```typescript filename="tests/Playground.spec.ts" it('plays', async () => { @@ -207,7 +235,7 @@ And here's how it may look on Windows: ```json filename="package.json" {3-4} { "scripts": { - "build": "blueprint build | out-null" + "build": "blueprint build | out-null", "lab": "yarn build && yarn test -t plays" } } diff --git a/pages/ref/core-common.mdx b/pages/ref/core-common.mdx index 7ec3137b..b28a7999 100644 --- a/pages/ref/core-common.mdx +++ b/pages/ref/core-common.mdx @@ -235,22 +235,10 @@ emit("Catch me if you can, Mr. Holmes".asComment()); // asComment() converts a S - To analyze `emit(){:tact}` calls, one has to look at [external messages](/book/external) produced by the contract. For example, when deploying in [Sandbox](https://github.com/ton-org/sandbox), you may call `emit(){:tact}` from a [receiver function](/book/contracts#receiver-functions) and then observe the list of sent external messages: - - ```typescript {9-10} - it('emits', async () => { - const res = await simpleCounter.send( - deployer.getSender(), - { value: toNano('0.05') }, - 'emit_receiver', // ← change to the message your receiver handles - ); - - console.log("Address of our contract: " + simpleCounter.address); - console.log(res.externals); // ← here one would see results of emit() calls, - // and all external messages in general - }); - ``` + To analyze `emit(){:tact}` calls, one has to look at [external messages](/book/external) produced by the contract. + Read more: [Logging via `emit(){:tact}`](/book/debug#logging). + [p]: /book/types#primitive-types From d449442f7fb3f63bbcde33addb14ade63aa8a1a7 Mon Sep 17 00:00:00 2001 From: Novus Nota <68142933+novusnota@users.noreply.github.com> Date: Sun, 9 Jun 2024 18:00:10 +0200 Subject: [PATCH 07/13] negative tests --- pages/book/debug.mdx | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/pages/book/debug.mdx b/pages/book/debug.mdx index 0bdd27fd..4d8c814b 100644 --- a/pages/book/debug.mdx +++ b/pages/book/debug.mdx @@ -89,8 +89,38 @@ Note, that `tact.config.json` may still be used in [Blueprint][bp] projects. In ## Writing tests with `jest` and Blueprint [#tests] +## Writing tests with Jest and Blueprint [#tests] {/* TODO: Refine local text */} +### Test transactions with errors [#tests-errors] + +Sometimes it's useful to make negative tests, featuring intentional errors and throwing specific [exit codes](/book/exit-codes). + +Example of such test closure: + +```typescript filename="tests/YourTestFileHere.spec.ts" {9,15} +it('throws specific exit code', async () => { + // Send a specific message to our contract and store the results + const res = await your_contract_name.send( + deployer.getSender(), + { + value: toNano('0.5'), // ← here you may modify the value in nanoToncoins sent + bounce: true, // ← here you may make it non-bounceable + }, + 'the message your receiver expects', // ← change it to yours + ); + + // Expect the transaction to our contract fail with a certain exit code + expect(res.transactions).toHaveTransaction({ + to: your_contract_name.address, + exitCode: 5, // ← change it to yours + }); +}); +``` + +Note, that to track down transactions with a certain exit code, you only need to specify `exitCode` field in `toHaveTransaction(){:typescript}` method of `expect(){:typescript}`. + +However, it's useful to narrow the scope by specifying the recipient address `to`, such that Jest would look only at the transaction caused by our message to the contract. ## Logging via `emit` [#logging] From 2c7e4a9c795430b06b51d442fd406d8d79a75ee4 Mon Sep 17 00:00:00 2001 From: Novus Nota <68142933+novusnota@users.noreply.github.com> Date: Mon, 10 Jun 2024 01:34:06 +0200 Subject: [PATCH 08/13] cspell --- cspell.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cspell.json b/cspell.json index e1823628..9e06221d 100644 --- a/cspell.json +++ b/cspell.json @@ -59,9 +59,10 @@ ], "flagWords": [], "ignorePaths": [ + ".github/temp-archive", "components/icons", - "grammars/grammar-ohm.json", "dist", + "grammars/grammar-ohm.json", "grammars/grammar-tact.json", "next.config.js", "node_modules", From 49a53f03c02cf48b5bf01b1b1266a4112372eecc Mon Sep 17 00:00:00 2001 From: Novus Nota <68142933+novusnota@users.noreply.github.com> Date: Mon, 10 Jun 2024 01:36:28 +0200 Subject: [PATCH 09/13] almost there --- pages/book/debug.mdx | 166 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 157 insertions(+), 9 deletions(-) diff --git a/pages/book/debug.mdx b/pages/book/debug.mdx index 4d8c814b..1d83fb41 100644 --- a/pages/book/debug.mdx +++ b/pages/book/debug.mdx @@ -60,6 +60,10 @@ And if you didn't find or cannot resolve the cause of your issues, try asking th +## Common debugging functions [#debug-functions] + +Tact provides a handful amount of various functions useful for debugging: [Core library → Debug](/ref/core-debug). + ## Enabling debug mode in compilation options [#debug-mode] In order to make certain functions like [`dump(){:tact}`][dump] or [`dumpStack(){:tact}`](/ref/core-debug#dumpstack) work, one needs to enable debug mode. @@ -88,15 +92,155 @@ Note, that `tact.config.json` may still be used in [Blueprint][bp] projects. In -## Writing tests with `jest` and Blueprint [#tests] -## Writing tests with Jest and Blueprint [#tests] +## Writing tests in Blueprint, with Sandbox and Jest [#tests] + +The [Blueprint][bp] is a popular development framework for writing, testing and deploying smart contracts on TON Blockchain. + +For testing smart contracts it uses the [Sandbox][sb], a local TON Blockchain emulator and [Jest][jest], a JavaScript testing framework. + +Whenever you create a new [Blueprint][bp] project or use `blueprint create` command inside the existing project, it creates a new contract alongside with a test suite file for it. + +Those files are placed in `tests/` folder and executed with [Jest][jest]. By default, all tests run, unless you specify specific group or test closure. For other options, refer to the brief documentation in the Jest CLI: `jest --help`. + +### Test file structure [#tests-file] + +Let's say that we have a contract named `Playground`, written in `contracts/playground.tact` file. If we've created that contract through [Blueprint][bp], then it also created a `tests/Playground.spec.ts` test suite file for us. + +The test file contains a single `describe(){:typescript}` [Jest][jest] function call, which denotes a test group. + +Inside that group, you'll have three variables, available in all tests within: + +* `blockchain` — local blockchain instance provided by [Sandbox][sb] +* `deployer` — a TypeScript wrapper used for deploying our `Playground` contract or any other we'd like to be deployed +* `playground` — a TypeScript wrapper for our `Playground` contract + + + + It's a common mistake to update `.tact` code and run tests without making a build first. That's because tests in [Blueprint][bp] rely on TypeScript wrappers generated by a Tact compiler and work with the latest build made. + + That's why every time you make a change to your Tact code, make sure to also build it with `npx blueprint build` before you execute the test suite. For your convenience, you may unite builds and tests into a single command, as shown in the [experimental lab setup](#lab-4). + + + +Then, a `beforeEach(){:tact}` [Jest][jest] function is called — it specifies all the code to be executed before each of the subsequent test closures. + + + + It is strongly advised not to modify the contents of `beforeEach(){:tact}`, unless you really need some specific behavior for each test closure or parameters of your [`init(){:tact}`](/book/contracts#init-function) function have changed. + + + +Finally, each test closure is described with a call to `it(){:tact}` [Jest][jest] function — that's where tests are actually written. + +A simplest example of the test closure can look like that: + +```typescript +it('should deploy', async () => { + // The check is done inside beforeEach, so this can be empty +}); +``` + +The integral parts of writing tests is ensuring that your expectations match the observed reality. For that, [Jest][jest] provides a function `expect(){:tact}`, which is used as follows: + +1. First, an observed variable is provided +2. Then, a specific method is called to check a certain property of that variable. + +Here's a more involved example, which uses `expect(){:tact}` function to check that counter contract actually properly increases the counter: + +```typescript +it('should increase counter', async () => { + const increaseTimes = 3; + for (let i = 0; i < increaseTimes; i++) { + console.log(`increase ${i + 1}/${increaseTimes}`); + + const increaser = await blockchain.treasury('increaser' + i); + + const counterBefore = await simpleCounter.getCounter(); + console.log('counter before increasing', counterBefore); + + const increaseBy = BigInt(Math.floor(Math.random() * 100)); + console.log('increasing by', increaseBy); + + const increaseResult = await simpleCounter.send( + increaser.getSender(), + { value: toNano('0.05') }, + { $$type: 'Add', queryId: 0n, amount: increaseBy } + ); + + expect(increaseResult.transactions).toHaveTransaction({ + from: increaser.address, + to: simpleCounter.address, + success: true, + }); + + const counterAfter = await simpleCounter.getCounter(); + console.log('counter after increasing', counterAfter); + + expect(counterAfter).toBe(counterBefore + increaseBy); + } +}); +``` + + + + See more test examples in the [Sandbox][sb] documentation:\ + [Testing flow (FunC)](https://github.com/ton-org/sandbox/blob/main/docs/testing-key-points.md)\ + [Writing tests for Tact](https://github.com/ton-org/sandbox/blob/main/docs/tact-testing-examples.md) + + + +### Utility methods [#tests-jest-utils] + +Test files generated by [Blueprint][bp] import `@ton/test-utils` library, which provides access to a number of additional helper methods for the result type of `expect(){:typescript}` [Jest][jest] function. Note, that regular methods like `toEqual(){:typescript}` are still there and ready to be used. -{/* TODO: Refine local text */} -### Test transactions with errors [#tests-errors] +#### toHaveTransaction + +The method `expect(…).toHaveTransaction(){:typescript}` checks that the list of transactions has a transaction matching certaing properties you specify: + +```typescript {2} +const res = await yourContractName.send(…); +expect(res.transactions).toHaveTransaction({ + // For example, let's check that a transaction to your contract was successful: + to: yourContractName.address, + success: true, +}); +``` + +To know the full list of such properties, look at auto-completion options provided by your editor or IDE. + +#### toEqualCell + +The method `expect(…).toEqualCell(){:typescript}` checks equality of two [cells](/book/types#primitive-types): + +```typescript {3} +expect(oneCell).toEqualCell(anotherCell); +``` + +#### toEqualSlice + +The method `expect(…).toEqualSlice(){:typescript}` checks equality of two [slices](/book/types#primitive-types): + +```typescript {3} +expect(oneSlice).toEqualSlice(anotherSlice); +``` + +#### toEqualAddress + +The method `expect(…).toEqualAddress(){:typescript}` checks equality of two [addresses](/book/types#primitive-types): + +```typescript {3} +expect(oneAddress).toEqualAddress(anotherAddress); +``` + +### Send messages to receivers [#tests-receive] + +### Observe the fees [#tests-fees] + +### Test transactions with intentional errors [#tests-errors] Sometimes it's useful to make negative tests, featuring intentional errors and throwing specific [exit codes](/book/exit-codes). -Example of such test closure: +Example of such [Jest][jest] test closure in [Blueprint][bp]: ```typescript filename="tests/YourTestFileHere.spec.ts" {9,15} it('throws specific exit code', async () => { @@ -104,8 +248,8 @@ it('throws specific exit code', async () => { const res = await your_contract_name.send( deployer.getSender(), { - value: toNano('0.5'), // ← here you may modify the value in nanoToncoins sent - bounce: true, // ← here you may make it non-bounceable + value: toNano('0.5'), // value in nanoToncoins sent + bounce: true, // (default) bounceable message }, 'the message your receiver expects', // ← change it to yours ); @@ -122,6 +266,8 @@ Note, that to track down transactions with a certain exit code, you only need to However, it's useful to narrow the scope by specifying the recipient address `to`, such that Jest would look only at the transaction caused by our message to the contract. +### Simulate passage of time in tests [#tests-time] + ## Logging via `emit` [#logging] A [global static function](/book/functions#global-static-functions) [`emit(){:tact}`](/ref/core-common#emit) sends a message to the outer world — it doesn't have a specific recipient. @@ -130,7 +276,7 @@ This function is very handy for logging and analyzing data off-chain — one jus ### Logs in local Sandbox tests [#logging-local] -When deploying in the [Sandbox](https://github.com/ton-org/sandbox), you may call [`emit(){:tact}`](/ref/core-common#emit) from a [receiver function](/book/contracts#receiver-functions) and then observe the list of sent [external messages](/book/external): +When deploying in the [Sandbox][sb], you may call [`emit(){:tact}`](/ref/core-common#emit) from a [receiver function](/book/contracts#receiver-functions) and then observe the list of sent [external messages](/book/external): ```typescript {9-10} it('emits', async () => { @@ -156,7 +302,7 @@ Note, that some explorers deserialize the message body sent for you, while other ## Handling bounced messages [#bounced] -When [sent](/book/send) with `bounce: true{:tact}`, messages can bounce back in case of errors. Make sure to write relevant [`bounced(){:tact}`](/book/bounced) message receivers and handle bounced messages gracefully: +When [sent](/book/send) with `bounce: true{:tact}`, messages can bounce back in case of errors. Make sure to write relevant [`bounced(){:tact}`](/book/bounced) message [receivers](/book/contracts#receiver-functions) and handle bounced messages gracefully: ```tact bounced(msg: YourMessage) { @@ -282,3 +428,5 @@ yarn lab [dump]: /ref/core-debug#dump [tg]: https://t.me/tactlang [bp]: https://github.com/ton-org/blueprint +[sb]: https://github.com/ton-org/sandbox +[jest]: https://jestjs.io From e2dd796b297eb67b5144e853e8799040373eaffd Mon Sep 17 00:00:00 2001 From: Novus Nota <68142933+novusnota@users.noreply.github.com> Date: Mon, 10 Jun 2024 07:17:43 +0200 Subject: [PATCH 10/13] done! --- pages/book/debug.mdx | 134 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 131 insertions(+), 3 deletions(-) diff --git a/pages/book/debug.mdx b/pages/book/debug.mdx index 1d83fb41..96161953 100644 --- a/pages/book/debug.mdx +++ b/pages/book/debug.mdx @@ -195,7 +195,7 @@ Test files generated by [Blueprint][bp] import `@ton/test-utils` library, which #### toHaveTransaction -The method `expect(…).toHaveTransaction(){:typescript}` checks that the list of transactions has a transaction matching certaing properties you specify: +The method `expect(…).toHaveTransaction(){:typescript}` checks that the list of transactions has a transaction matching certain properties you specify: ```typescript {2} const res = await yourContractName.send(…); @@ -232,9 +232,107 @@ The method `expect(…).toEqualAddress(){:typescript}` checks equality of two [a expect(oneAddress).toEqualAddress(anotherAddress); ``` -### Send messages to receivers [#tests-receive] +### Send messages to contracts [#tests-send] -### Observe the fees [#tests-fees] +To send messages to contracts, use `.send(){:typescript}` method on their TypeScript wrappers like so: + +```typescript +// It accepts 3 arguments: +await yourContractName.send( + // 1. sender of the message + deployer.getSender(), // this is a default treasury, can be replaced + + // 2. value and (optional) bounce, which is true by default + { value: toNano('0.5'), bounce: false }, + + // 3. a message body, if any + 'Look at me!', +); +``` + +Message body can be a simple string, or an object specifying fields of the [Message](/book/structs-and-messages#messages) type: + +```typescript {4-8} +await yourContractName.send( + deployer.getSender(), + { value: toNano('0.5') }, + { + $$type: 'NameOfYourMessageType', + field1: 0n, // bigint zero + field2: 'yay', + }, +); +``` + +More often than not, it's important to store results of such sends, because they contain events occurred, transactions made and external messages sent: + +```typescript +const res = await yourContractName.send(…); +// res.events — array of events occurred +// res.externals — array of external-out messages +// res.transactions — array of transactions made +``` + +With that, we can easily filter or check certain transactions: + +```typescript +expect(res.transactions).toHaveTransaction(…); +``` + +### Observe the fees and values [#tests-fees] + +[Sandbox][sb] provides a helper function `printTransactionFees(){:typescript}`, which pretty-prints all the values and fees that went into transactions provided. It is quite handy for observing the flow of nanoToncoins. + +To use it, modify imports from `@ton/sandbox` on top of the test file: + +```typescript +import { Blockchain, SandboxContract, TreasuryContract, printTransactionFees } from '@ton/sandbox'; +// ^^^^^^^^^^^^^^^^^^^^ +``` + +Then, provide an array of transactions as an argument, like so: + +```typescript +printTransactionFees(res.transactions); +``` + +To work with individual values of total fees or fees from compute and action [phases](https://docs.ton.org/learn/tvm-instructions/tvm-overview#transactions-and-phases), inspect each transaction individually: + +```typescript {11,17,21} +// Storing the transaction handled by the receiver in a separate constant +const receiverHandledTx = res.transactions[1]; +expect(receiverHandledTx.description.type).toEqual('generic'); + +// Needed to please TypeScript +if (receiverHandledTx.description.type !== 'generic') { + throw new Error('Generic transaction expected'); +} + +// Total fees +console.log('Total fees: ', receiverHandledTx.totalFees); + +// Compute fee +const computeFee = receiverHandledTx.description.computePhase.type === 'vm' + ? receiverHandledTx.description.computePhase.gasFees + : undefined; +console.log('Compute fee: ', computeFee); + +// Action fee +const actionFee = receiverHandledTx.description.actionPhase?.totalActionFees; +console.log('Action fee: ', actionFee); + +// Now we can do some involved checks, like limiting the fees to 1 Toncoin +expect( + (computeFee ?? 0n) + + (actionFee ?? 0n) +).toBeLessThanOrEqual(toNano('1')); +``` + + + + [Sandbox][sb] has many more utility functions, which are often handy. For example, it provides `prettyLogTransaction(){:typescript}` and `prettyLogTransactions(){:typescript}`, which operate on a single or multiple transactions respectively and pretty-print flow of values between the addresses. + + ### Test transactions with intentional errors [#tests-errors] @@ -268,6 +366,36 @@ However, it's useful to narrow the scope by specifying the recipient address `to ### Simulate passage of time in tests [#tests-time] +The Unix time in local blockchain instances provided by [Sandbox][bp] starts at the moment of the creation of those in `beforeEach(){:typescript}` block. + +```typescript {2} +beforeEach(async () => { + blockchain = await Blockchain.create(); // ← here + // ... +}); +``` + +Previously, we've been warned not to modify the `beforeEach(){:typescript}` block unless we really need to. And now, to override the time and time travel a little, we do. + +Let's add the following line by the end of it, setting `blockchain.now` explicitly to the time when deployment message was handled: +```typescript {3} +beforeEach(async () => { + // ... + blockchain.now = deployResult.transactions[1].now; +}); +``` + +Now, we can manipulate time in out test clauses. For example, let's make a transaction one minute after the deployment and another one after two: + +```typescript {2,4} +it('your test clause title', async () => { + blockchain.now += 60; // 60 seconds late + const res1 = await yourContractName.send(…); + blockchain.now += 60; // another 60 seconds late + const res2 = await yourContractName.send(…); +}); +``` + ## Logging via `emit` [#logging] A [global static function](/book/functions#global-static-functions) [`emit(){:tact}`](/ref/core-common#emit) sends a message to the outer world — it doesn't have a specific recipient. From b94f6c4c37eaed36163960466ae4d29b26cf8333 Mon Sep 17 00:00:00 2001 From: Novus Nota <68142933+novusnota@users.noreply.github.com> Date: Mon, 10 Jun 2024 08:39:17 +0200 Subject: [PATCH 11/13] showcase of dump results plus a couple of nits --- pages/book/debug.mdx | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/pages/book/debug.mdx b/pages/book/debug.mdx index 96161953..1462a0cc 100644 --- a/pages/book/debug.mdx +++ b/pages/book/debug.mdx @@ -50,7 +50,7 @@ Here are a few questions to ask yourself to challenge your assumptions: At the moment, Tact doesn't have a step-through debugger. Despite that, it's still possible to use the [_printf debugging_](https://en.wikipedia.org/wiki/Debugging#printf_debugging) approach. -It involves actively placing [`dump(){:tact}`][dump] and [`dumpStack(){:tact}`](/ref/core-debug#dumpstack) functions throughout your code and observing states of variables at a given point of time. Note, that those functions work only in a [debug mode](#debug-mode) and won't be executed otherwise. +It involves actively placing [`dump(){:tact}`][dump] and [`dumpStack(){:tact}`](/ref/core-debug#dumpstack) function calls throughout your code and observing states of variables at a given point of time. Note, that those functions work only in a [debug mode](#debug-mode) and won't be executed otherwise. Once you found that some value isn't equal to what you've expected it to be, don't rush to fixing the issue on the spot. That's because what you're seeing may not be the root cause of it and merely a symptom, effect. Be very careful with cause-and-effect relationships and figure out which's which to resolve the cause and not introduce new mess for your future self. @@ -64,6 +64,22 @@ And if you didn't find or cannot resolve the cause of your issues, try asking th Tact provides a handful amount of various functions useful for debugging: [Core library → Debug](/ref/core-debug). +The most commonly used debugging function is [`dump(){:tact}`][dump], which prints the passed argument to the contract's debug console. It only works in a [debug mode](#debug-mode) and does nothing otherwise. + +In practice, when you write: + +```tact {2} +// ... somewhere appropriate in your contract ... +dump("To infinity and beyond!"); +``` + +You'll see the following in the logs whenever execution gets to that [`dump(){:tact}`][dump] call: + +```shell +#DEBUG#: [DEBUG] File contracts/your_contract_name.tact:42:69 +#DEBUG#: To infinity and beyond! +``` + ## Enabling debug mode in compilation options [#debug-mode] In order to make certain functions like [`dump(){:tact}`][dump] or [`dumpStack(){:tact}`](/ref/core-debug#dumpstack) work, one needs to enable debug mode. @@ -140,6 +156,8 @@ it('should deploy', async () => { }); ``` +### State expectations with `expect()` [#tests-expect] + The integral parts of writing tests is ensuring that your expectations match the observed reality. For that, [Jest][jest] provides a function `expect(){:tact}`, which is used as follows: 1. First, an observed variable is provided @@ -334,7 +352,7 @@ expect( -### Test transactions with intentional errors [#tests-errors] +### Transactions with intentional errors [#tests-errors] Sometimes it's useful to make negative tests, featuring intentional errors and throwing specific [exit codes](/book/exit-codes). @@ -364,7 +382,7 @@ Note, that to track down transactions with a certain exit code, you only need to However, it's useful to narrow the scope by specifying the recipient address `to`, such that Jest would look only at the transaction caused by our message to the contract. -### Simulate passage of time in tests [#tests-time] +### Simulate passage of time [#tests-time] The Unix time in local blockchain instances provided by [Sandbox][bp] starts at the moment of the creation of those in `beforeEach(){:typescript}` block. From 5358d3787ed5c9572994cda9ba4fc89ac6ad7432 Mon Sep 17 00:00:00 2001 From: Novus Nota <68142933+novusnota@users.noreply.github.com> Date: Mon, 10 Jun 2024 08:41:19 +0200 Subject: [PATCH 12/13] link --- pages/book/debug.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/book/debug.mdx b/pages/book/debug.mdx index 1462a0cc..0074a741 100644 --- a/pages/book/debug.mdx +++ b/pages/book/debug.mdx @@ -73,7 +73,7 @@ In practice, when you write: dump("To infinity and beyond!"); ``` -You'll see the following in the logs whenever execution gets to that [`dump(){:tact}`][dump] call: +You'll see the following in the [test logs](#tests) whenever execution gets to that [`dump(){:tact}`][dump] call: ```shell #DEBUG#: [DEBUG] File contracts/your_contract_name.tact:42:69 From c9747006f5dd098a51673ce3a51501cedf541b93 Mon Sep 17 00:00:00 2001 From: Novus Nota <68142933+novusnota@users.noreply.github.com> Date: Wed, 12 Jun 2024 13:59:05 +0200 Subject: [PATCH 13/13] dump 1. dump 2. dump 3. dump 4. ??? 5. ~~Profit!~~ dump --- pages/book/debug.mdx | 89 +++++++++++++++++++++++++++++++++----------- pages/index.mdx | 3 +- 2 files changed, 69 insertions(+), 23 deletions(-) diff --git a/pages/book/debug.mdx b/pages/book/debug.mdx index 0074a741..0cf44102 100644 --- a/pages/book/debug.mdx +++ b/pages/book/debug.mdx @@ -46,14 +46,20 @@ Here are a few questions to ask yourself to challenge your assumptions: -### Go over your code and observe the values [#step-3] +### Go over your code and observe the values [#approach-3] -At the moment, Tact doesn't have a step-through debugger. Despite that, it's still possible to use the [_printf debugging_](https://en.wikipedia.org/wiki/Debugging#printf_debugging) approach. +At the moment, Tact doesn't have a step-through debugger. Despite that, it's still possible to use the ["printf debugging"](https://en.wikipedia.org/wiki/Debugging#printf_debugging) approach. It involves actively placing [`dump(){:tact}`][dump] and [`dumpStack(){:tact}`](/ref/core-debug#dumpstack) function calls throughout your code and observing states of variables at a given point of time. Note, that those functions work only in a [debug mode](#debug-mode) and won't be executed otherwise. Once you found that some value isn't equal to what you've expected it to be, don't rush to fixing the issue on the spot. That's because what you're seeing may not be the root cause of it and merely a symptom, effect. Be very careful with cause-and-effect relationships and figure out which's which to resolve the cause and not introduce new mess for your future self. + + + See how to use [`dump(){:tact}`][dump] for debugging: [Debug with `dump(){:tact}`](#tests-dump). + + + In addition to dumping values, it's often helpful to use assertive functions like [`require(){:tact}`](/ref/core-debug#require), [`nativeThrowWhen(){:tact}`](/ref/core-debug#nativethrowwhen) and [`nativeThrowUnless(){:tact}`](/ref/core-debug#nativethrowunless). They help stating your assumptions clear, and are handy for setting up "trip wires" for catching issues in the future. And if you didn't find or cannot resolve the cause of your issues, try asking the community in Tact's [Telegram chat][tg] or, if your issue or question is generally related to TON more than it's related to Tact, hop into [TON Dev Telegram chat](https://t.me/tondev_eng). @@ -64,22 +70,6 @@ And if you didn't find or cannot resolve the cause of your issues, try asking th Tact provides a handful amount of various functions useful for debugging: [Core library → Debug](/ref/core-debug). -The most commonly used debugging function is [`dump(){:tact}`][dump], which prints the passed argument to the contract's debug console. It only works in a [debug mode](#debug-mode) and does nothing otherwise. - -In practice, when you write: - -```tact {2} -// ... somewhere appropriate in your contract ... -dump("To infinity and beyond!"); -``` - -You'll see the following in the [test logs](#tests) whenever execution gets to that [`dump(){:tact}`][dump] call: - -```shell -#DEBUG#: [DEBUG] File contracts/your_contract_name.tact:42:69 -#DEBUG#: To infinity and beyond! -``` - ## Enabling debug mode in compilation options [#debug-mode] In order to make certain functions like [`dump(){:tact}`][dump] or [`dumpStack(){:tact}`](/ref/core-debug#dumpstack) work, one needs to enable debug mode. @@ -100,11 +90,14 @@ export const compile: CompilerConfig = { }; ``` -Note, that `tact.config.json` may still be used in [Blueprint][bp] projects. In such cases values specified in `tact.config.json` act as default unless modified in the `wrappers/`. +Note, that versions of [Blueprint][bp] starting with 0.20.0 automatically enable debug mode in `wrappers/` for new contracts. + +In addition to that, [`tact.config.json`](/book/config) may still be used in [Blueprint][bp] projects. In such cases values specified in [`tact.config.json`](/book/config) act as default unless modified in the `wrappers/`. - Read more about configuration and `tact.config.json` file: [Configuration](/book/config). + Read more about configuration and [`tact.config.json`](/book/config) file: [Configuration](/book/config).\ + See how to use [`dump(){:tact}`][dump] for debugging: [Debug with `dump(){:tact}`](#tests-dump). @@ -118,7 +111,7 @@ Whenever you create a new [Blueprint][bp] project or use `blueprint create` comm Those files are placed in `tests/` folder and executed with [Jest][jest]. By default, all tests run, unless you specify specific group or test closure. For other options, refer to the brief documentation in the Jest CLI: `jest --help`. -### Test file structure [#tests-file] +### Structure of test files [#tests-structure] Let's say that we have a contract named `Playground`, written in `contracts/playground.tact` file. If we've created that contract through [Blueprint][bp], then it also created a `tests/Playground.spec.ts` test suite file for us. @@ -156,11 +149,63 @@ it('should deploy', async () => { }); ``` +### Debug with `dump()` [#tests-dump] + +To see results of [`dump(){:tact}`][dump] function calls and use ["printf debugging"](#approach-3) approach, one has to: + +1. Put calls to [`dump(){:tact}`][dump] and other [common debugging functions](#debug-functions) in relevant places of the code. +2. Run [Jest][jest] tests, which would call target functions and send messages to target receivers. + +Assuming you've created a [new counter contract project](/#start), let's see how it works in practice. + +First, let's place a call to [`dump(){:tact}`][dump] in `contracts/simple_counter.tact`, which would output the `amount` passed in `msg{:tact}` [Struct](/book/structs-and-messages#structs) to contract's debug console: + +```tact filename="contracts/simple_counter.tact" {3} +// ... +receive(msg: Add) { + dump(msg.amount); + // ... +} +// ... +``` + +Next, let's comment out all existing `it(){:typescript}` test closures in `tests/SimpleCounter.spec.ts` file. And then add the following one: + +```typescript filename="tests/SimpleCounter.spec.ts" +it('should dump', async () => { + await playground.send( + deployer.getSender(), + { value: toNano('0.5') }, + { $$type: 'Add', queryId: 1n, amount: 1n }, + ); +}); +``` + +It sends a message to our contract's `receive(msg: Add){:tact}` [receiver](/book/receive) without storing the [results of such send](#tests-send). + +Now, if we build our contract with `yarn build{:shell}` and run our test suite with `yarn test{:shell}`, we'll see the following in the test logs: + +```txt +console.log + #DEBUG#: [DEBUG] File contracts/simple_counter.tact:17:9 + #DEBUG#: 1 + + at SmartContract.runCommon (node_modules/@ton/sandbox/dist/blockchain/SmartContract.js:221:21) +``` + +Which is produced by of our [`dump(){:tact}`][dump] call above. + + + + Read more about sending messages to contracts in tests: [Send messages to contracts](#tests-send). + + + ### State expectations with `expect()` [#tests-expect] The integral parts of writing tests is ensuring that your expectations match the observed reality. For that, [Jest][jest] provides a function `expect(){:tact}`, which is used as follows: -1. First, an observed variable is provided +1. First, an observed variable is provided. 2. Then, a specific method is called to check a certain property of that variable. Here's a more involved example, which uses `expect(){:tact}` function to check that counter contract actually properly increases the counter: diff --git a/pages/index.mdx b/pages/index.mdx index 3ed30d78..746f4f0f 100644 --- a/pages/index.mdx +++ b/pages/index.mdx @@ -18,7 +18,7 @@ To check it, run `node --version{:shell}` — it should show you the version 18. It will create a new project with the simple counter contract: - + ```shell npm create ton -- simple-counter --type tact-counter --contractName SimpleCounter @@ -26,6 +26,7 @@ It will create a new project with the simple counter contract: ```shell + # recommended yarn create ton simple-counter --type tact-counter --contractName SimpleCounter ```