Skip to content
This repository was archived by the owner on Dec 12, 2024. It is now read-only.

feat: new debugging page #233

Merged
merged 16 commits into from
Jun 12, 2024
Merged
283 changes: 157 additions & 126 deletions pages/book/debug.mdx
Original file line number Diff line number Diff line change
@@ -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.
<Steps>

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';
<Callout>

//
// 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');
</Callout>

//
// 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();
</Steps>

//
// 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/`.

<Callout>

Read more about configuration and `tact.config.json` file: [Configuration](/book/config).

</Callout>

## 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
<Steps>

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
</Steps>

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
Loading