cltester: as/act/trace

The test_chain class supports this syntax for pushing single-action transactions:

chain.as("alice"_n).act<token::actions::transfer>(
       "alice"_n, "example"_n, s2a("300.0000 EOS"), "memo");

as

as(account) returns an object that represents an account's active authority. as also supports other authorities:

chain.as("alice"_n, "owner"_n).act<token::actions::transfer>(
       "alice"_n, "example"_n, s2a("300.0000 EOS"), "memo");

act

act<action wrapper>(action args) creates, signs, and executes a single-action transaction. It also verifies the transaction succeeded. If it fails, it aborts the test with an error message.

The contract headers use EOSIO_ACTIONS(...) to define the action wrappers, e.g. token::actions::transfer or example::actions::buydog. The wrappers record the default contract name (e.g. eosio.token), the name of the action (e.g. transfer), and the argument types. This allows strong type safety. It also bypasses any need for ABIs.

act signs with default_priv_key - a well-known key used for testing (5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3). This key pairs with default_pub_key (EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV). Both the create_account and create_code_account methods create accounts with default_pub_key.

trace

Like act, trace<action wrapper>(action args) creates, signs, and executes a single-action transaction. Unlike act, trace does not verify success. Instead, it returns the transaction's trace.

We could display the trace:

auto result = chain.as("alice"_n).trace<example::actions::buydog>(
    "alice"_n, "fido"_n, s2a("100.0000 OTHER"));
std::cout << format_json(result) << "\n";

This produces output like the following:

{
    "id": "F4EE6CACEF935889E35355568C492409C6F4535565B0B801EC31352DEFAA40F3",
    "status": "hard_fail",
    "cpu_usage_us": 0,
    "net_usage_words": 0,
    "elapsed": "62",
    "net_usage": "124",
    "scheduled": false,
    "action_traces": [
        {
            "action_ordinal": 1,
            "creator_action_ordinal": 0,
            "receipt": null,
            "receiver": "example",
            "act": {
                "account": "example",
                "name": "buydog",
                "authorization": [
                    {
                        "actor": "alice",
                        "permission": "active"
                    }
                ],
                "data": [...]
            },
            "context_free": false,
            "elapsed": "34",
            "console": "",
            "account_ram_deltas": [],
            "account_disk_deltas": [],
            "except": "eosio_assert_message assertion failure (3050003)\nassertion failure with message: This contract does not deal with this token\npending console output: \n",
            "error_code": "10000000000000000000",
            "return_value": []
        }
    ],
    "account_ram_delta": null,
    "except": "eosio_assert_message assertion failure (3050003)\nassertion failure with message: This contract does not deal with this token\npending console output: \n",
    "error_code": "10000000000000000000",
    "failed_dtrx_trace": []
}

expect

expect verifies that a transaction trace's except field contains within it an expected error message. If the the transaction succeeded, or the transaction failed but with a different message, then expect aborts the test with an error message. expect does a substring match.

expect(chain.as("alice"_n).trace<example::actions::buydog>(
            "alice"_n, "fido"_n, s2a("100.0000 OTHER")),
         "This contract does not deal with this token");

with_code

The action wrappers provide a default account name that the contract is normally installed on. e.g. the token wrappers assume eosio.token. with_code overrides this default.

This example sets up a fake EOS token to try to fool our example code.

// The hacker.token account runs the token contract
chain.create_code_account("hacker.token"_n);
chain.set_code("hacker.token"_n, CLSDK_CONTRACTS_DIR "token.wasm");
chain.as("hacker.token"_n)
      .with_code("hacker.token"_n)
      .act<token::actions::create>("hacker.token"_n, s2a("1000000.0000 EOS"));
chain.as("hacker.token"_n)
      .with_code("hacker.token"_n)
      .act<token::actions::issue>("hacker.token"_n, s2a("1000000.0000 EOS"), "");

// Give fake EOS to Alice
chain.as("hacker.token"_n)
      .with_code("hacker.token"_n)
      .act<token::actions::transfer>("hacker.token"_n, "alice"_n, s2a("10000.0000 EOS"), "");

// Alice transfers fake EOS to the example contract
chain.as("alice"_n)
      .with_code("hacker.token"_n)
      .act<token::actions::transfer>(
         "alice"_n, "example"_n, s2a("300.0000 EOS"), "");

// The contract didn't credit her account with the fake EOS
expect(chain.as("alice"_n).trace<example::actions::buydog>(
            "alice"_n, "fido"_n, s2a("100.0000 EOS")),
         "user does not have a balance");

as() variables

as() returns an object which can be stored in a variable to reduce repetition.

auto alice = chain.as("alice"_n);
alice.act<token::actions::transfer>(
    "alice"_n, "example"_n, s2a("300.0000 EOS"), "");
alice.act<example::actions::buydog>(
    "alice"_n, "fido"_n, s2a("100.0000 EOS"));
alice.act<example::actions::buydog>(
    "alice"_n, "barf"_n, s2a("110.0000 EOS"));