Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: property based testing of methods of map of all basic types (messages and structs are not supported for now) #1839

Open
wants to merge 11 commits into
base: main
Choose a base branch
from

Conversation

Mell0r
Copy link

@Mell0r Mell0r commented Feb 14, 2025

Checklist

  • I have updated CHANGELOG.md
  • I have run all the tests locally and no test failure was reported
  • I have run the linter, formatter and spellchecker
  • I did not do unrelated and/or undiscussed refactorings

@Mell0r Mell0r requested a review from a team as a code owner February 14, 2025 10:40
@Mell0r Mell0r changed the title feat: property based test of 'set' method on map<Int, Int> feat: property based testing of methods of map<Int, Int> Feb 18, 2025
@anton-trunov anton-trunov requested a review from jeshecdom March 4, 2025 06:16
Copy link
Contributor

@jeshecdom jeshecdom left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did some quick check of the code, but I will later have a deeper look to fully understand everything.

Quite interesting. The code gave me some ideas on how to approach my unit test generation. Thank you!

@Mell0r Mell0r changed the title feat: property based testing of methods of map<Int, Int> feat: property based testing of methods of map of all basic types (messages and structs are not supported for now) Mar 4, 2025
@jeshecdom
Copy link
Contributor

Sorry to keep you waiting. I think your solution is very clever. However, I talked with @anton-trunov and we arrived to the conclusion that we need to change the approach on this PR so that instead of generating tact and spec files separately, the script should carry out the tests directly. This means that it would need to generate the ASTs, compile them and then run the fast-check properties on the contracts once loaded in Sandbox (all those steps carried out in the same script).

We require this because it would be faster to work with the ASTs and the compiled contracts directly, than parsing tact files and then parsing and compiling spec files and running them separately. Also, doing everything in the same script would allow tweaking the build process so that only what is absolutely necessary is included in the build logic (for example, compiling only a subset of stdlib).

For doing the suggested changes, we would need to do the following steps:

  • For each key/value type:
    • Create the AstModule representing your tact file.

      • For this, you would need to create it programmatically, so that it imitates your tact template file src/test/e2e-emulated/map-tests/property-based/map-property-based.tact.template. You would need to use "makeNode" functions (as an example, see "makeX" functions at the start of file src/test/autogenerated/gen-initof-reachability.ts in PR tests: check initOf and codeOf reachability #2083).
    • Once you have an AstModule representing your tact file, you will need to compile it using your own build function.

      • For this, just copy the buildModule function in file src/test/autogenerated/util.ts of PR tests: check initOf and codeOf reachability #2083.
        The buildModule function returns a map with the name of the contract as key and its compiled binary code as value. So, you would obtain your contract code as:
        const compiledContractMap = buildModule(astF, myAst);    <---- "astF" is the ast factory you used in the "makeX" functions, 
                                                                        and "myAst" is the AST you built in the previous step.
        const compiledContract = compiledContractMap.get("MapTestContract")!;
        
    • Once you have your compiled contract, you must load it in Sandbox. You will need to write your own contract wrapper and manually load it in Sandbox. For this, just do the following:

      • Define a contract wrapper class. You could copy the class ProxyContract in file src/test/autogenerated/util.ts in PR tests: check initOf and codeOf reachability #2083, but you will need to add generic getters to that class, because we want the proxy to work for arbitrary types:
      export class ProxyContract implements Contract {
          address: Address;
          init: StateInit;
      
          constructor(stateInit: StateInit) {
      	this.address = contractAddress(0, stateInit);
      	this.init = stateInit;
          }
      
          async send(
      	provider: ContractProvider,
      	via: Sender,
      	args: { value: bigint; bounce?: boolean | null | undefined },
      	body: Cell,
          ) {
      	await provider.internal(via, { ...args, body: body });
          }
      
          async getWholeMap(provider: ContractProvider) {
      	const builder = new TupleBuilder();
      	return (await provider.get('wholeMap', builder.build())).stack;
          }
          
          async getGetValue(provider: ContractProvider, key: TupleItem[]) {
      	return (await provider.get('getValue', key)).stack;
          }
          
          async getExists(provider: ContractProvider, key: TupleItem[]) {
      	const source = (await provider.get('exists', builder.build())).stack;
      	const result = source.readBoolean();
      	return result;
          }
      }
      
      • Write a loader function for your contract. In your case, the function should look like as follows, since your init does not have arguments:
      function getContractStateInit(contractCode: Buffer): StateInit {
          const data = beginCell().storeUint(0, 1).endCell();
          const code = Cell.fromBoc(contractCode);
          if (typeof code === "undefined") {
      	throw new Error("Code cell expected");
          }
          return { code, data };
      }
      

      Then, you can load your contract in Sandbox, as follows:

      const blockchain = await Blockchain.create();
      const contractToTestStateInit = getContractStateInit(
          compiledContract   <------------- This is the compiled contract obtained by the buildModule function
      );
      const contractToTest = blockchain.openContract(
          new ProxyContract(contractToTestStateInit),
      );
          <------ Up to this point, it is as if we executed:
                  const contractToTest = blockchain.openContract(await MapTestContract.fromInit())
                  but without the need to import the wrapper files, since the contract was loaded dinamically
      
      • From here onwards, you can just carry out the tests you had in your spec.ts.template file. However, when calling a contract getter, you will need to build the appropriate TupleItem[] and read from the TupleReader.
        For example, if you are in the Int/Int type pair and we want to call the getGetValue getter, you would need to build your TupleItem[] as follows:

        const builder = new TupleBuilder();
        builder.writeNumber(number);
        const tuples = builder.build();
        

        And then call the getter:

        const result = await contractToTest.getGetValue(tuples);  <---- "result" has type "TupleReader"
        

        And reading from the TupleReader as:

        result.readBigNumberOpt();
        

        This could be achieved by having a matrix indexed by the key/value types. At each cell in the matrix, you would have two functions: one that builds TupleItem[] for that pair of types and one that reads from TupleReader for that pair of types. You could even pregenerate that matrix if it is too bothersome to write it manually.


In case you find some step confusing or need help, I can explain or jump in to help you with the coding. I will be in charge of reviewing this PR, together with the other PR you opened for the fuzzer #2340.

Probably other issues will arise that I did not think of, but feel free to comment in this PR if you get stuck in something.
Thank you!

@jeshecdom
Copy link
Contributor

Oh, I was forgetting. I suggest to use fixed opcodes for the message structs. For example,

message(100) SetKeyValue {
    key: Int;
    value: Int;
}

message(101) DeleteKey {
    key: Int;
}

message(102) ClearRequest {}

In this way, we can build a cell and pass it to the send function of the ProxyContract:

await contractToTest.send(
            treasure.getSender(),
            { value: toNano("10") },
            beginCell()
             .storeUint(100, 32)    <---- The opcode for the SetKeyValue message struct
             .storeInt(10, 257)    <---- The key field
             .storeInt(2, 257)    <---- The value field
             .endCell()
        );

Also, if you decide to copy the buildModule function in file src/test/autogenerated/util.ts of PR #2083, you will need to change the openContext function as shown in file src/context/store.ts of the same PR, for the buildModule function to work as expected.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants