Techelson
Techelson is a Test Execution Environment (TEE) for Michelson smart contracts. Michelson is the stack-based language used by the tezos blockchain. Techelson is open source and hosted on github, where you can find the build instructions. If you just want to retrieve a binary, head over to releases.
If you have problems or suggestions, help us make Techelson better by opening an issue.
Techelson emulates just enough of the tezos blockchain protocol to be able to create smart contracts and make transfers between contracts. Currently, techelson only aims at testing functional properties of smart contracts. In particular, it does not provide any information about the gas or burn of transfers/contracts. This is because computing the burn, and especially the gas of a transfer is rather complex and would require techelson to drop some of the abstractions it makes over the tezos protocol to run tests faster.
Techelson can be used either as a command-line tool or as an OCaml library. This book focuses on the former use case.
Also, this book assumes the reader is fairly familiar with the michelson language. We will discuss what a contract is and how it behaves, but the reader should know what michelson types and instructions look like, and their semantics.
Michelson is a fairly low-level language which makes it difficult to discuss complex contracts. This book will sometimes give contracts as Liquidity contracts. Liquidity is a higher-level, OCaml-like language for tezos smart contracts which compiles to Michelson.
NB: if you are a Liquidity user, you should probably take a look at this blog post on how to write tests directly in Liquidity, and run these tests using Techelson.
The chapters of this book are
- Michelson, provides a very brief introduction to michelson smart contracts.
- Running Tests, describes techelson's workflow for running tests through examples.
- Test Generation, discusses techelson's test generation features.
- Quick Reference, quick reminders of techelson's features, such as extensions.
All examples in this book are available in the rsc
directory of the github repository.
Michelson
Michelson is the stack-based, strongly typed, low-level language supported by the tezos blockchain for smart contracts. We only provide a brief description of michelson here, and refer the reader to the official documentation for more details.
A michelson contract is similar to transition system. The storage of a contract is its current state; the entry point (code) of a contract is a function which takes
- some tezos tokens (of type
mutez
), - the current storage of the contract, and
- a parameter of a certain type provided by the client of the contract.
It returns
- a list of operations (contract/account creation, transfers, etc.), and
- the new storage of the contract.
In practice, a contract looks as follows:
storage <type> ;
parameter <type> ;
code <instruction> ;
Note that tokens are passed implicitely: they are credited to the contract before it starts running
(although the amount of the transfer can be accessed with the AMOUNT
instruction). Hence the code
of the contract takes two parameters, which are aggregated in a pair (parameter, storage)
. The
same goes with the operations and the new storage returned by the contract, which are returned as a
pair (operations, new_storage)
.
In the stack-based context of michelson, "takes <something>
as argument" means "starts with a
stack containing <something>
". Likewise, "returns <something>
" here means "leaves <something>
on the stack at the end (and nothing else than <something>
)".
A Simple Example
Let us build a contract which counts how many time it was called. We will allow clients to specify
that they do not want to be counted by passing a boolean ghost
: if it is true, the contract will
not count the transfer.
Based on this description, we already have the storage and parameter types:
storage nat;
parameter bool;
The code of this contract should perform the following steps:
- destroy the parameter/storage pair
- branch on the ghost parameter: this consumes the parameter, meaning the storage is now on the
top of the stack
- do nothing if ghost is true: the storage is unchanged
- add
1
to the storage otherwise
- pair an empty list of operations with the new storage
The complete description of the contract, simpleExample.tz, is thus
storage nat;
parameter bool;
code {
UNPAIR; # Unpair parameter and storage.
IF { # Asked not to count: storage is unchanged, nothing to do.
} {
PUSH nat 1;
ADD
};
NIL operation; # We don't want to perform any operations.
PAIR # Aggregate the operation list and the new storage.
};
Running Tests
Interaction with techelson is currently file-based. There are two kinds of files techelson works
with. Contract files are just plain michelson smart contract files, and testcase files are
files containing a Michelson instruction, usually a sequence of instructions { <instructions> }
.
Omitting options, running techelson looks like (the --
separator is optional):
$ techelson \
--contract <contract_1> .. --contract <contract_n> \
-- <testcase_1> ... <testcase_m>
Techelson will then run the testcases in sequence. All testcases will have access to all the
contracts provided with --contract
. For more information about command-line refer to the Usage
section.
This section builds on the small example from the Michelson section, and a slightly more involved example later on, to introduce techelson's workflow and its extended instruction set.
-
First Steps shows how to run a simple testcase with no contract.
Introduces
PRINT_STACK
andSTEP
. -
Creating and Calling Contracts discusses the contract environment and contract creation and transfers.
Introduces
APPLY_OPERATIONS
. -
Live Contract Inspection deals with recovering the balance and the storage of live (deployed) contracts.
Introduces
GET_BALANCE
andGET_STORAGE
. -
Anonymous Contracts details how to create anonymous contracts.
-
Transfers introduces the creation of transfers to a contract.
-
Testing for Failures shows how to test that an operation fails and how. This section is the first to use the slightly more complex example admins.tz, and it's liquidity version admins.liq.
Introduces
MUST_FAIL
. -
Usurpation of Identity illustrates how to have your testcases pretend they are a specific contract/account, and to create operations in their name.
Introduces
SET_SOURCE
.
First Steps
Let's give ourselves a testcase file test1.techel
{
PUSH string "starting the test" ;
}
This testcase does not use any contract. To run it, simply run
$ techelson rsc/no_contract/okay/test1.techel
Running test `Test1`
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
Done running test `Test1`
Introspection
This is not very informative, which is why techelson provides extensions such as PRINT_STACK
.
This instruction prints the state of the stack in a readable way. For example, if we change the
example above to test2.techel to
{
PUSH string "starting the test" ;
PRINT_STACK
}
the output becomes
$ techelson rsc/no_contract/okay/test2.techel
Running test `Test2`
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
stack:
|==================================================================================================|
| "starting the test" |
| string |
|==================================================================================================|
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
Done running test `Test2`
Steps
When you run a complex testcase or contract, it can be useful to have break point that stop the
execution. This gives you time to read a PRINT_STACK
before the next step is actually performed,
make one step, read the state of the stack, etc.
The STEP
techelson extension allows to do just that. You can also provide a string that will be displayed when the STEP
instruction is reached.
The following example (test3.techel) showcases the STEP
instruction:
{
PUSH string "starting the test" ;
PRINT_STACK ;
STEP "just started the test" ; # The description string is optional, see below.
PUSH @not_important string "a string with a variable name" ;
PRINT_STACK ;
STEP ; # No description string.
DROP ;
PRINT_STACK ;
STEP "The string should be gone. Also, we're done."
}
Techelson will stop on all STEP
instructions and ask you to press enter to keep going:
$ techelson rsc/no_contract/okay/test3.techel
Running test `Test3`
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
stack:
|==================================================================================================|
| "starting the test" |
| string |
|==================================================================================================|
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
stopping [just started the test] press `return` to continue
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
stack:
|==================================================================================================|
| "starting the test" |
| string |
|--------------------------------------------------------------------------------------------------|
| @not_important |
| "a string with a variable name" |
| string |
|==================================================================================================|
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
stopping [no information] press `return` to continue
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
stack:
|==================================================================================================|
| "starting the test" |
| string |
|==================================================================================================|
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
stopping [The string should be gone. Also, we're done.] press `return` to continue
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
Done running test `Test3`
Pro tip 1: you can use
PRINT_STACK
andSTEP
in contracts too (see the extensions section for details). Also, techelson treats#>
as a whitespace. Hence, your can have#> STEP ;
and/or#> PRINT_STACK ;
in your michelson contract, which lets you inspect it during tests. Your contract remains legal michelson thanks to the leading#
which comments the command.
Pro tip 2: passing
--skip on
totechelson
will skip (but still display) all the steps. The output of the commands reported in this book are all obtained by running techelson with--skip on
.
Creating and Calling Contracts
When you pass a contract to techelson using techelson --contract <file> ...
, the contract becomes
a named contract in the techelson environment. The name of the contract is the name of the file
- up to its first
.
character, - with the first letter capitalized.
So
techelson \
--contract my_contract.tz \
--contract myContract.tz \
--contract my.contract.tz \
...
defines three named contracts: My_contract
, MyContract
and My
.
Note that the naming convention is the same for testcases, based on the testcase file. The name of a testcase might be used in techelson's output to provide information, but it has no practical use currently.
Named Contract Creation
Techelson extends the CREATE_CONTRACT
michelson instruction with the following rule
instruction | parameter | stack |
---|---|---|
CREATE_CONTRACT | <string> | :: key_hash : option key_hash : bool : bool : mutez : 'g : 'S |
-> operation : address : 'S |
where <string>
is the name of a contract with storage type 'g
in the environment. The semantics
of the stack parameters is the same as in michelson: manager, optional delegate, the two spendable
and delegatable flags, and the balance and storage of the contract created.
NB: techelson also provides the
SPAWN_CONTRACT
extension which takes the name of the contract on the stack. See techelson's Extensions for more details.
Say we have the following contract in file simpleExample.tz.
storage nat;
parameter bool;
code {
UNPAIR; # Unpair parameter and storage.
IF { # Asked not to count: storage is unchanged, nothing to do.
} {
PUSH nat 1;
ADD
};
NIL operation; # We don't want to perform any operations.
PAIR # Aggregate the operation list and the new storage.
};
We can craft a creation operation in file create1.techel as follows
{
PUSH @storage nat 0 ;
PUSH @amount mutez 3 ;
PUSH @delegatable bool True ;
PUSH @spendable bool True ;
NONE @delegate key_hash ;
PUSH key "manager address" ;
SHA512 @manager ;
PRINT_STACK ;
STEP "before creation" ;
CREATE_CONTRACT "SimpleExample" ;
PRINT_STACK ;
STEP "after creation" ;
}
This produces the following output
$ techelson --contract rsc/simpleExample/contracts/simpleExample.tz -- rsc/simpleExample/okay/create1.techel
Running test `Create1`
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
stack:
|==================================================================================================|
| @storage |
| 0p |
| nat |
|--------------------------------------------------------------------------------------------------|
| @amount |
| 3utz |
| mutez |
|--------------------------------------------------------------------------------------------------|
| @delegatable |
| True |
| bool |
|--------------------------------------------------------------------------------------------------|
| @spendable |
| True |
| bool |
|--------------------------------------------------------------------------------------------------|
| @delegate |
| None |
| (option key_hash) |
|--------------------------------------------------------------------------------------------------|
| @manager |
| "sha512:manager address" |
| key_hash |
|==================================================================================================|
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
stopping [before creation] press `return` to continue
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
stack:
|==================================================================================================|
| address[1] |
| address |
|--------------------------------------------------------------------------------------------------|
| CREATE[uid:0] (@address[1], "sha512:manager address", None, true, true, 3utz) "SimpleExample" |
| operation |
|==================================================================================================|
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
stopping [after creation] press `return` to continue
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
Done running test `Create1`
Applying Operations
Michelson operations (contract/account creation, transfers) cannot be applied directly in a michelson contract. Instead, a contract produces a list of operation which the tezos runtime applies after the contract is done running.
A techelson test case needs to be able to apply operations however, which is why the
APPLY_OPERATIONS
extension exists. This instruction suspends the execution of the testcase to
apply the list of operations on the top of the stack. When all these operations are done running,
techelson resumes the execution of the testcase.
Warning: this instruction is only legal in testcases, not in contracts.
Consider testcase create2.techel:
{
PUSH @storage nat 0 ;
PUSH @amount mutez 3 ;
PUSH @delegatable bool True ;
PUSH @spendable bool True ;
NONE @delegate key_hash ;
PUSH key "manager address" ;
SHA512 @manager ;
CREATE_CONTRACT @main @main_op "SimpleExample" ;
DIP { NIL operation } ;
CONS ;
PRINT_STACK ;
STEP "operation is now in a list" ;
APPLY_OPERATIONS ;
PRINT_STACK ;
STEP "testing that contract exists" ;
# Takes the address on the top of the stack, retrieves a contract of parameter `bool`.
CONTRACT bool ;
IF_NONE { # There is no hope, failing.
PUSH @err_msg string "failed to retrieve contract" ;
FAILWITH
} {
PUSH string "success" ;
PRINT_STACK
}
}
Running it yields
$ techelson --contract rsc/simpleExample/contracts/simpleExample.tz -- rsc/simpleExample/okay/create2.techel
Running test `Create2`
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
stack:
|==================================================================================================|
| @main |
| address[1]@main |
| address |
|--------------------------------------------------------------------------------------------------|
| [ CREATE[uid:0] (@address[1]@main, "sha512:manager address", None, true, true, 3utz) "SimpleExample" ] |
| (list operation) |
|==================================================================================================|
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
stopping [operation is now in a list] press `return` to continue
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
applying operation CREATE[uid:0] (@address[1]@main, "sha512:manager address", None, true, true, 3utz) "SimpleExample"
timestamp: 1970-01-01 00:00:00 +00:00
live contracts: none
=> live contracts: SimpleExample (3utz) address[1]@main
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
stack:
|==================================================================================================|
| @main |
| address[1]@main |
| address |
|==================================================================================================|
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
stopping [testing that contract exists] press `return` to continue
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
stack:
|==================================================================================================|
| address[1]@main |
| (contract bool) |
|--------------------------------------------------------------------------------------------------|
| "success" |
| string |
|==================================================================================================|
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
Done running test `Create2`
Notice the line Applying operations...
. We could increase techelson's verbosity to obtain more
information as to which contracts are deployed, but let's see how to inspect the state of a live
(deployed) contract instead.
Live Contract Inspection
Michelson smart contracts cannot access each other's storage. They can only interact through transfers, during which the client of the contract provides a parameter that the contract runs its code on.
As a test framework, techelson provides inspection instructions which give access to the balance
and the storage of a live (deployed) contract. Both consume a contract
from the top of the
stack.
instruction | parameter | stack |
---|---|---|
GET_STORAGE | 'storage | :: contract _ : 'S or :: address : 'S |
-> (option 'storage) : 'S | ||
GET_BALANCE | none | :: contract _ : 'S or :: address : 'S |
-> mutez : 'S |
Let's extend the previous example to inspection.techel which checks that the balance and storage of the contract deployed are correct.
{
PUSH @storage nat 0 ;
PUSH @amount mutez 3 ;
PUSH @delegatable bool True ;
PUSH @spendable bool True ;
NONE @delegate key_hash ;
PUSH key "manager address" ;
SHA512 @manager ;
CREATE_CONTRACT @main @main_op "SimpleExample" ;
DIP { NIL operation } ;
CONS ;
APPLY_OPERATIONS ;
# Takes the address on the top of the stack, retrieves a contract of parameter `bool`.
CONTRACT bool ;
IF_NONE { # There is no hope, failing.
PUSH @err_msg string "failed to retrieve contract" ;
FAILWITH
} {} ;
DUP ;
GET_BALANCE ;
PRINT_STACK ;
STEP "retrieved the balance of the contract" ;
PUSH mutez 3 ;
IFCMPNEQ {
PUSH string "balance should be 3utz" ;
FAILWITH
} {} ;
GET_STORAGE nat ;
IF_NONE {
PUSH string "unable to retrieve storage of contract" ;
FAILWITH
} {
PRINT_STACK ;
STEP "retrieved the storage of the contract" ;
PUSH nat 0 ;
IFCMPNEQ {
PUSH string "storage should be 0 (nat)" ;
FAILWITH
} {} ;
}
}
The testcase does not fail and produces the output
$ techelson --contract rsc/simpleExample/contracts/simpleExample.tz -- rsc/simpleExample/okay/inspection.techel
Running test `Inspection`
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
applying operation CREATE[uid:0] (@address[1]@main, "sha512:manager address", None, true, true, 3utz) "SimpleExample"
timestamp: 1970-01-01 00:00:00 +00:00
live contracts: none
=> live contracts: SimpleExample (3utz) address[1]@main
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
stack:
|==================================================================================================|
| address[1]@main |
| (contract bool) |
|--------------------------------------------------------------------------------------------------|
| 3utz |
| mutez |
|==================================================================================================|
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
stopping [retrieved the balance of the contract] press `return` to continue
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
stack:
|==================================================================================================|
| 0p |
| nat |
|==================================================================================================|
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
stopping [retrieved the storage of the contract] press `return` to continue
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
Done running test `Inspection`
Anonymous Contracts
Techelson accepts contracts through its --contract
option. These contracts are named as discussed
in Creating and Calling Contracts. Contracts defined at michelson level in testcases and
contracts however are considered anonymous. Anonymous contracts can also be deployed and
inspected. In fact, they are not really different from named contracts apart from their lack of
name, which (currently) prevent techelson from mentioning where they really come from in its debug
output.
The following anonymous.techel testcase is similar to the one from the Live Contract Inspection except that the contract deployed is not given to the environment, it is inlined in the testcase.
{
PUSH @storage nat 0 ;
PUSH @amount mutez 3 ;
PUSH @delegatable bool True ;
PUSH @spendable bool True ;
NONE @delegate key_hash ;
PUSH key "manager address" ;
SHA512 @manager ;
CREATE_CONTRACT @main @main_op {
storage nat;
parameter bool;
code {
UNPAIR ;
IF {
} {
PUSH nat 1;
ADD
} ;
NIL operation ;
PAIR
};
} ;
DIP { NIL operation } ;
CONS ;
APPLY_OPERATIONS ;
# Takes the address on the top of the stack, retrieves a contract of parameter `bool`.
CONTRACT bool ;
IF_NONE { # There is no hope, failing.
PUSH @err_msg string "failed to retrieve contract" ;
FAILWITH
} {} ;
DUP ;
GET_BALANCE ;
PRINT_STACK ;
STEP "retrieved the balance of the contract" ;
PUSH mutez 3 ;
IFCMPNEQ {
PUSH string "balance should be 3utz" ;
FAILWITH
} {} ;
GET_STORAGE nat ;
IF_NONE {
PUSH string "unable to retrieve storage of contract" ;
FAILWITH
} {
PRINT_STACK ;
STEP "retrieved the storage of the contract" ;
PUSH nat 0 ;
IFCMPNEQ {
PUSH string "storage should be 0 (nat)" ;
FAILWITH
} {} ;
}
}
This produces the exact same output (modulo the testcase's name, and as long as we do not increase verbosity) as for inspection.techel:
$ techelson rsc/no_contract/okay/anonymous.techel
Running test `Anonymous`
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
applying operation CREATE[uid:0] (@address[1]@main, "sha512:manager address", None, true, true, 3utz)
{
storage nat ;
parameter bool ;
code ...;
}
timestamp: 1970-01-01 00:00:00 +00:00
live contracts: none
=> live contracts: <anonymous> (3utz) address[1]@main
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
stack:
|==================================================================================================|
| address[1]@main |
| (contract bool) |
|--------------------------------------------------------------------------------------------------|
| 3utz |
| mutez |
|==================================================================================================|
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
stopping [retrieved the balance of the contract] press `return` to continue
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
stack:
|==================================================================================================|
| 0p |
| nat |
|==================================================================================================|
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
stopping [retrieved the storage of the contract] press `return` to continue
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
Done running test `Anonymous`
Transfers
At this point creating and applying a transfer should be relatively straightforward. Simply create
the operation using michelson's TRANSFER_TOKENS
, and apply it with APPLY_OPERATIONS
. For
instance, transfer.techel builds on inspection.techel. It creates an instance of
simpleExample.tz, and creates and applies two operations: the first transfers 7
tokens with a
parameter equal to False
, and the second transfers 13
tokens with True
. (Remember that
simpleExample.tz will count transfers for which the parameter is False
.)
{
PUSH @storage nat 0 ;
PUSH @amount mutez 3 ;
PUSH @delegatable bool True ;
PUSH @spendable bool True ;
NONE @delegate key_hash ;
PUSH key "manager address" ;
SHA512 @manager ;
CREATE_CONTRACT @main @main_op "SimpleExample" ;
... # Omitting code creating the contract.
{ # Making a non-ghost transfer.
DUP ;
PUSH @amount mutez 7 ;
PUSH @ghost bool False ;
TRANSFER_TOKENS ;
} ;
DIP { # Making a ghost transfer.
DUP ;
PUSH @amount mutez 13 ;
PUSH @ghost bool True ;
TRANSFER_TOKENS ;
} ;
{ # Creating the list of all operations.
DIP { DIP {NIL operation } ; CONS } ;
CONS ;
} ;
APPLY_OPERATIONS ;
Finally, it checks that the balance and storage are the ones expected:
GET_BALANCE ;
PRINT_STACK ;
STEP "retrieved the balance of the contract" ;
PUSH mutez 23 ;
IFCMPNEQ {
PUSH string "balance should be 23utz" ;
FAILWITH
} {} ;
GET_STORAGE nat ;
IF_NONE {
PUSH string "unable to retrieve storage of contract" ;
FAILWITH
} {
PRINT_STACK ;
STEP "retrieved the storage of the contract" ;
PUSH nat 1 ;
IFCMPNEQ {
PUSH string "storage should be 1 (nat)" ;
FAILWITH
} {} ;
}
}
The test passes and its output is
$ techelson --contract rsc/simpleExample/contracts/simpleExample.tz -- rsc/simpleExample/okay/transfer.techel
Running test `Transfer`
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
applying operation CREATE[uid:0] (@address[1]@main, "sha512:manager address", None, true, true, 3utz) "SimpleExample"
timestamp: 1970-01-01 00:00:00 +00:00
live contracts: none
=> live contracts: SimpleExample (3utz) address[1]@main
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
applying operation TRANSFER[uid:1] address[0]@Transfer -> address[1]@main 7utz False
timestamp: 1970-01-01 00:00:00 +00:00
live contracts: SimpleExample (3utz) address[1]@main
running TRANSFER[uid:1] address[0]@Transfer -> address[1]@main 7utz False
timestamp: 1970-01-01 00:00:00 +00:00
=> live contracts: SimpleExample (10utz) address[1]@main
applying operation TRANSFER[uid:2] address[0]@Transfer -> address[1]@main 13utz True
timestamp: 1970-01-01 00:00:00 +00:00
live contracts: SimpleExample (10utz) address[1]@main
running TRANSFER[uid:2] address[0]@Transfer -> address[1]@main 13utz True
timestamp: 1970-01-01 00:00:00 +00:00
=> live contracts: SimpleExample (23utz) address[1]@main
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
stack:
|==================================================================================================|
| address[1]@main |
| (contract bool) |
|--------------------------------------------------------------------------------------------------|
| 23utz |
| mutez |
|==================================================================================================|
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
stopping [retrieved the balance of the contract] press `return` to continue
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
stack:
|==================================================================================================|
| 1p |
| nat |
|==================================================================================================|
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
stopping [retrieved the storage of the contract] press `return` to continue
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
Done running test `Transfer`
Testing for Failures
This section is going to use a slightly more complex contract in order to showcase failures and how to test them. Even if you are not familiar with liquidity, the contract's code will most likely be more readable in liquidity than in michelson. Here is the liquidity version, admins.liq:
[%%version 0.405]
type storage = {
admins : (string, address) map ;
(* Unused in this example. *)
nus : (string, (address * tez * UnitContract.instance)) map ;
}
let admin_check (storage : storage) (name : string) (a : address) : unit =
match Map.find name storage.admins with
| None -> failwith "only admins can perform administrative tasks"
| Some address ->
if address <> a then
failwith "illegal access to admin account"
let%entry add_admin
((admin_name, nu_name, nu_address) : string * string * address)
(storage : storage)
: operation list * storage
=
admin_check storage admin_name (Current.sender ());
let storage =
storage.admins <- Map.update nu_name (Some nu_address) storage.admins
in
[], storage
Note that the clients
field of the storage is unused in this example. The admins
map maps
administrator names to addresses. The only entry point (in this example) is add_admin
which
allows administrators to add new administrators by registering their name and their address. More
precisely, calling this contract is only legal if the SENDER
(Current.sender ()
) of the call is
an administrator (c.f. admin_check
). If the call to the contract is not legal, the transfer
fails:
let admin_check (storage : storage) (name : string) (a : address) : unit =
match Map.find name storage.admins with
| None -> failwith "only admins can perform administrative tasks"
| Some address ->
if address <> a then
failwith "illegal access to admin account"
The parameters of the entry point are
admin_name
: name associated with theSENDER
administrator,nu_name
: name of the new administrator to add,nu_address
: the address of the new administrator.
let%entry add_admin
((admin_name, nu_name, nu_address) : string * string * address)
Using liquidity to compile the contract to michelson (for instance using liquidity's online
editor), we obtain admins.tz. Here are the storage
and parameter
types:
parameter (pair string (pair string address));
storage
(pair :storage
(map %admins string address)
(map %clients string (pair address (pair mutez (contract :UnitContract unit)))));
We omit the contract's code (admins.tz) as i) it is not very readable and ii)
we do not need to know what the code precisely is to create the contract and call it, as long as we
know the storage
and parameter
types.
Creation
Creating a contract has been covered in previous sections, so let's give ourselves some code to
create the contract with one administrator called root
. In fact, let's make an account for root
and register it as an administrator. The new administrator new_admin
is also deployed as an
account. Testcase create.techel does exactly that:
{
NIL operation ;
{ # Create an account for `root`.
PUSH @balance mutez 0 ;
PUSH @delegatable bool True ;
PUSH @delegate (option key_hash) None ;
PUSH @manager key "@root_manager" ;
SHA512 ;
CREATE_ACCOUNT @root ;
} ;
SWAP ;
DIP { CONS } ;
{ # Create an account for `new_admin`.
PUSH @balance mutez 0 ;
PUSH @delegatable bool True ;
PUSH @delegate (option key_hash) None ;
PUSH @manager key "@new_admin_manager" ;
SHA512 ;
CREATE_ACCOUNT @new_admin ;
} ;
SWAP ;
DIP { SWAP ; DIP CONS } ;
{ # Create an `admins` contract.
# Create the storage's (empty) `clients` field.
EMPTY_MAP @clients string (pair address (pair mutez (contract unit))) ;
# Create the storage's `admins` field and register `root`.
EMPTY_MAP @admins string address ;
DUUUUP ; # Retrieve root's address.
SOME @address ;
PUSH @name string "root" ;
PRINT_STACK ;
UPDATE ;
PAIR @storage ;
PUSH @balance mutez 0 ;
PUSH @delegatable bool True ;
PUSH @spendable bool False ;
PUSH @delegate (option key_hash) None ;
PUSH @manager key "@contract_manager" ;
SHA512 ;
CREATE_CONTRACT @admins "Admins"
} ;
SWAP ;
DIP { SWAP ; DIP { SWAP ; DIP CONS } } ;
DIIIP { APPLY_OPERATIONS } ;
PRINT_STACK ;
STEP "after applying creation operations."
}
Running this test produces the following output
$ techelson --contract rsc/admins/contracts/admins.tz -- rsc/admins/okay/create.techel
Running test `Create`
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
stack:
|==================================================================================================|
| [ CREATE[uid:1] (@address[2]@new_admin, "sha512:@new_admin_manager", None, true, true, 0utz)
{
storage unit ;
parameter unit ;
code ...;
}, CREATE[uid:0] (@address[1]@root, "sha512:@root_manager", None, true, true, 0utz)
{
storage unit ;
parameter unit ;
code ...;
} ] |
| (list operation) |
|--------------------------------------------------------------------------------------------------|
| @root |
| address[1]@root |
| address |
|--------------------------------------------------------------------------------------------------|
| @new_admin |
| address[2]@new_admin |
| address |
|--------------------------------------------------------------------------------------------------|
| @clients |
| Map { } |
| (map string (pair address (pair mutez (contract unit)))) |
|--------------------------------------------------------------------------------------------------|
| @admins |
| Map { } |
| (map string address) |
|--------------------------------------------------------------------------------------------------|
| @address |
| (Some address[1]@root) |
| (option address) |
|--------------------------------------------------------------------------------------------------|
| @name |
| "root" |
| string |
|==================================================================================================|
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
applying operation CREATE[uid:2] (@address[3]@admins, "sha512:@contract_manager", None, false, true, 0utz) "Admins"
timestamp: 1970-01-01 00:00:00 +00:00
live contracts: none
=> live contracts: <anonymous> (0utz) address[2]@new_admin
Admins (0utz) address[3]@admins
<anonymous> (0utz) address[1]@root
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
stack:
|==================================================================================================|
| @root |
| address[1]@root |
| address |
|--------------------------------------------------------------------------------------------------|
| @new_admin |
| address[2]@new_admin |
| address |
|--------------------------------------------------------------------------------------------------|
| @admins |
| address[3]@admins |
| address |
|==================================================================================================|
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
stopping [after applying creation operations.] press `return` to continue
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
Done running test `Create`
Transfer Failures
Let's now add new_admin
as a new administrator. Testcase addAdmin.techel only adds the
following few instructions at the end of create.techel:
# Retrieve the actual contract.
CONTRACT (pair string (pair string address)) ;
IF_NONE {
PUSH string "failed to retrieve `admins` contract" ;
STEP
} {} ;
# Saving the contract for later.
DUP ;
PUSH @amount mutez 0 ;
# New admin's address.
DUUUUP ;
# New admin's name.
PUSH @new_name string "new_admin" ;
PAIR ;
# Root's name.
PUSH @name string "root" ;
PAIR @storage ;
TRANSFER_TOKENS ;
DIP { NIL operation } ;
CONS ;
APPLY_OPERATIONS ;
What should the result of applying this transfer be? Remember than before adding an administrator, the contract checks that the sender is an admin.
let admin_check (storage : storage) (name : string) (a : address) : unit =
match Map.find name storage.admins with
| None -> failwith "only admins can perform administrative tasks"
| Some address ->
if address <> a then
failwith "illegal access to admin account"
let%entry add_admin
((admin_name, nu_name, nu_address) : string * string * address)
...
=
admin_check storage admin_name (Current.sender ());
...
So, if everything goes well, the transfer should fail: the sender here is not root
, but the
testcase. In techelson, the testcase currently running has its own address. It is in particular
not the address of root
. Hence, the transfer fails as it should and so does the whole testcase.
The (relevant part of the) output is
Test `AddAdmin` failed:
Error
operation TRANSFER[uid:3] address[0]@AddAdmin -> address[3]@admins 0utz ("root", ("new_admin", address[2]@new_admin)) was expected to succeed
but failed on operation TRANSFER[uid:3] address[0]@AddAdmin -> address[3]@admins 0utz ("root", ("new_admin", address[2]@new_admin))
operation failed on "illegal access to admin account" : string
You can see in the transfer the sender and the target of the transfer:
TRANSFER[uid:3] address[0]@AddAdmin -> address[3]@admins
AddAdmin
is the name of our testcase, and address[0]@AddAdmin
is its address. Name "root"
does not map to this address in the contract and the transfer fails.
Handling Failures
Before getting into making this transfer work (next section), note that this (failing) testcase is actually useful. Or at least it should be: the transfer we are trying to make is illegal indeed. We do want the transfer to fail, but the testcase should
- succeed if the transfer does fail,
- fail if the transfer succeeds: anyone can add admins, which is bad.
This is what the MUST_FAIL
techelson extension does. It takes an operation wraps it in a
construct telling techelson that this operation must fail: either the operation itself or, if it is
a transfer, the operations created by this transfer. Here is its signature:
instruction | parameter | stack |
---|---|---|
MUST_FAIL | <type> | :: option <type> : operation : 'S |
-> operation : 'S |
Let's ignore the <type>
parameter and the first stack argument for now and just use this
instruction right away. Testcase mustFail.techel is the same as addAdmin.techel except for a
few lines after the transfer:
TRANSFER_TOKENS ;
PUSH (option string) None ;
MUST_FAIL @this_must_fail string ;
PRINT_STACK ;
The test now passes successfully:
$ techelson --contract rsc/admins/contracts/admins.tz -- rsc/admins/okay/mustFail.techel
Running test `MustFail`
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
applying operation CREATE[uid:2] (@address[3]@admins, "sha512:@contract_manager", None, false, true, 0utz) "Admins"
timestamp: 1970-01-01 00:00:00 +00:00
live contracts: none
=> live contracts: <anonymous> (0utz) address[2]@new_admin
Admins (0utz) address[3]@admins
<anonymous> (0utz) address[1]@root
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
stack:
|==================================================================================================|
| @root |
| address[1]@root |
| address |
|--------------------------------------------------------------------------------------------------|
| @new_admin |
| address[2]@new_admin |
| address |
|--------------------------------------------------------------------------------------------------|
| address[3]@admins |
| (contract (pair string (pair string address))) |
|--------------------------------------------------------------------------------------------------|
| @this_must_fail |
| MUST_FAIL[uid:4] _ (TRANSFER[uid:3] address[0]@MustFail -> address[3]@admins 0utz ("root", ("new_admin", address[2]@new_admin))) |
| operation |
|==================================================================================================|
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
applying operation MUST_FAIL[uid:4] _ (TRANSFER[uid:3] address[0]@MustFail -> address[3]@admins 0utz ("root", ("new_admin", address[2]@new_admin)))
timestamp: 1970-01-01 00:00:00 +00:00
live contracts: <anonymous> (0utz) address[2]@new_admin
Admins (0utz) address[3]@admins
<anonymous> (0utz) address[1]@root
running TRANSFER[uid:3] address[0]@MustFail -> address[3]@admins 0utz ("root", ("new_admin", address[2]@new_admin))
timestamp: 1970-01-01 00:00:00 +00:00
=> live contracts: <anonymous> (0utz) address[2]@new_admin
Admins (0utz) address[3]@admins
<anonymous> (0utz) address[1]@root
failure confirmed on test operation
MUST_FAIL[uid:4] _ (TRANSFER[uid:3] address[0]@MustFail -> address[3]@admins 0utz ("root", ("new_admin", address[2]@new_admin)))
while running operation TRANSFER[uid:3] address[0]@MustFail -> address[3]@admins 0utz ("root", ("new_admin", address[2]@new_admin))
failed with value "illegal access to admin account" : string
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
Done running test `MustFail`
Notice that Techelson lets you know the failure is confirmed:
failure confirmed on test operation
MUST_FAIL[uid:4] _ (TRANSFER[uid:3] address[0]@MustFail -> address[3]@admins 0utz ("root", ("new_admin", address[2]@new_admin)))
while running operation TRANSFER[uid:3] address[0]@MustFail -> address[3]@admins 0utz ("root", ("new_admin", address[2]@new_admin))
failed with value "illegal access to admin account" : string
(More) Precise Failure Testing
Now, MUST_FAIL
(as it is used here) succeeds if the transfer ends in a tezos protocol failure.
This include explicit failures in the code, illegal transfers due to insufficient funds, duplicate
operations, etc. It does not include type-checking errors and internal techelson errors.
This means in particular that if the transfer above fails for a reason different from "illegal access to admin account"
then MUST_FAIL
will consider the test a success. To make sure the cause
for failure is actually the one we want, we must use MUST_FAIL
's optional stack parameter. A
failure in michelson code always has a value of some type associated to it. In this case, the type
of this value is string
and its value is "illegal access to admin account"
. Testcase
preciseMustFail.techel only changes mustFail.techel to pass the failure value expected to MUST_FAIL
:
TRANSFER_TOKENS ;
PUSH (option string) (Some "illegal access to admin account") ;
MUST_FAIL @this_must_fail string ;
PRINT_STACK ;
As a consequence, if the transfer fails with anything else than an explicit failure with a value of
type string
equal to "illegal access to admin account"
, then the whole testcase will fail.
Everything works fine here, and the output is
$ techelson --contract rsc/admins/contracts/admins.tz -- rsc/admins/okay/preciseMustFail.techel
Running test `PreciseMustFail`
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
applying operation CREATE[uid:2] (@address[3]@admins, "sha512:@contract_manager", None, false, true, 0utz) "Admins"
timestamp: 1970-01-01 00:00:00 +00:00
live contracts: none
=> live contracts: <anonymous> (0utz) address[2]@new_admin
Admins (0utz) address[3]@admins
<anonymous> (0utz) address[1]@root
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
stack:
|==================================================================================================|
| @root |
| address[1]@root |
| address |
|--------------------------------------------------------------------------------------------------|
| @new_admin |
| address[2]@new_admin |
| address |
|--------------------------------------------------------------------------------------------------|
| address[3]@admins |
| (contract (pair string (pair string address))) |
|--------------------------------------------------------------------------------------------------|
| @this_must_fail |
| MUST_FAIL[uid:4] "illegal access to admin account" : string (TRANSFER[uid:3] address[0]@PreciseMustFail -> address[3]@admins 0utz ("root", ("new_admin", address[2]@new_admin))) |
| operation |
|==================================================================================================|
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
applying operation MUST_FAIL[uid:4] "illegal access to admin account" :
string (TRANSFER[uid:3] address[0]@PreciseMustFail -> address[3]@admins 0utz ("root", ("new_admin", address[2]@new_admin)))
timestamp: 1970-01-01 00:00:00 +00:00
live contracts: <anonymous> (0utz) address[2]@new_admin
Admins (0utz) address[3]@admins
<anonymous> (0utz) address[1]@root
running TRANSFER[uid:3] address[0]@PreciseMustFail -> address[3]@admins 0utz ("root", ("new_admin", address[2]@new_admin))
timestamp: 1970-01-01 00:00:00 +00:00
=> live contracts: <anonymous> (0utz) address[2]@new_admin
Admins (0utz) address[3]@admins
<anonymous> (0utz) address[1]@root
failure confirmed on test operation
MUST_FAIL[uid:4] "illegal access to admin account" : string (TRANSFER[uid:3] address[0]@PreciseMustFail -> address[3]@admins 0utz ("root", ("new_admin", address[2]@new_admin)))
while running operation TRANSFER[uid:3] address[0]@PreciseMustFail -> address[3]@admins 0utz ("root", ("new_admin", address[2]@new_admin))
failed with value "illegal access to admin account" : string
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
Done running test `PreciseMustFail`
Notice that the MUST_FAIL
operation now mentions the value expected:
MUST_FAIL[uid:4] "illegal access to admin account" : string (TRANSFER[uid:3] ...)
as opposed to the _
wildcard from testcase mustFail.techel, which means no value was given:
MUST_FAIL[uid:4] _ (TRANSFER[uid:3] ...)
Usurpation of Identity
The previous section used the liquidity contract admins.liq and its techelson version admins.tz. It showcased how to handle expected transfer failures and turn them into test objectives. The failure used to demonstrate the example was that we tried to add a new administrator by calling the contract from the testcase, which failed because only administrators can add other administrators, and the (address of the) testcase was not registered as such.
PUSH @amount mutez 0 ;
# New admin's address.
DUUUUP ;
# New admin's name.
PUSH @new_name string "new_admin" ;
PAIR ;
# Root's name.
PUSH @name string "root" ;
PAIR @storage ;
TRANSFER_TOKENS ;
PUSH (option string) (Some "illegal access to admin account") ;
MUST_FAIL @this_must_fail string ;
PRINT_STACK ;
DIP {NIL operation } ;
CONS ;
APPLY_OPERATIONS ;
One solution would be to register the testcase directly, but it would be more natural and more
generic to be able to apply a transfer as someone else. Hence the SET_SOURCE
extension:
instruction | parameter | stack |
---|---|---|
SET_SOURCE | code | :: address : 'A |
-> 'B | ||
iff code :: [ A -> B ] |
Warning: the
SET_SOURCE
extension is only legal in testcases.
This extension allows to pretend the testcase is a live contract or account from the environment.
More precisely, all operations created in the code
under the SET_SOURCE
will have their source
and sender be the address from the stack. Testcase setSource.techel uses this instruction to
pretend that root
is the one adding a new administrator.
# New admin's address.
DUUUUP ;
# New admin's name.
PUSH @new_name string "new_admin" ;
PAIR ;
# Root's name.
PUSH @name string "root" ;
PAIR @storage ;
{ # Pretending to be `root`.
DUUUUUUP ;
PRINT_STACK ;
STEP ;
SET_SOURCE {
TRANSFER_TOKENS
}
} ;
PRINT_STACK ;
DIP { NIL operation } ;
CONS ;
APPLY_OPERATIONS
The testcase now succeeds, and its output is
$ techelson --contract rsc/admins/contracts/admins.tz -- rsc/admins/okay/setSource.techel
Running test `SetSource`
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
applying operation CREATE[uid:2] (@address[3]@admins, "sha512:@contract_manager", None, false, true, 0utz) "Admins"
timestamp: 1970-01-01 00:00:00 +00:00
live contracts: none
=> live contracts: <anonymous> (0utz) address[2]@new_admin
Admins (0utz) address[3]@admins
<anonymous> (0utz) address[1]@root
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
stack:
|==================================================================================================|
| @root |
| address[1]@root |
| address |
|--------------------------------------------------------------------------------------------------|
| @new_admin |
| address[2]@new_admin |
| address |
|--------------------------------------------------------------------------------------------------|
| address[3]@admins |
| (contract (pair string (pair string address))) |
|--------------------------------------------------------------------------------------------------|
| address[3]@admins |
| (contract (pair string (pair string address))) |
|--------------------------------------------------------------------------------------------------|
| @amount |
| 0utz |
| mutez |
|--------------------------------------------------------------------------------------------------|
| @storage |
| ("root", ("new_admin", address[2]@new_admin)) |
| (pair string (pair string address)) |
|--------------------------------------------------------------------------------------------------|
| address[1]@root |
| address |
|==================================================================================================|
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
stopping [no information] press `return` to continue
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
stack:
|==================================================================================================|
| @root |
| address[1]@root |
| address |
|--------------------------------------------------------------------------------------------------|
| @new_admin |
| address[2]@new_admin |
| address |
|--------------------------------------------------------------------------------------------------|
| address[3]@admins |
| (contract (pair string (pair string address))) |
|--------------------------------------------------------------------------------------------------|
| TRANSFER[uid:3] address[1]@root -> address[3]@admins 0utz ("root", ("new_admin", address[2]@new_admin)) |
| operation |
|==================================================================================================|
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
applying operation TRANSFER[uid:3] address[1]@root -> address[3]@admins 0utz ("root", ("new_admin", address[2]@new_admin))
timestamp: 1970-01-01 00:00:00 +00:00
live contracts: <anonymous> (0utz) address[2]@new_admin
Admins (0utz) address[3]@admins
<anonymous> (0utz) address[1]@root
running TRANSFER[uid:3] address[1]@root -> address[3]@admins 0utz ("root", ("new_admin", address[2]@new_admin))
timestamp: 1970-01-01 00:00:00 +00:00
=> live contracts: <anonymous> (0utz) address[2]@new_admin
Admins (0utz) address[3]@admins
<anonymous> (0utz) address[1]@root
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
Done running test `SetSource`
Notice how, in the last PRINT_STACK
, the sender of the transfer is now root
:
TRANSFER[uid:3] address[1]@root -> address[3]@admins ...
Timestamp Control
Some contracts need to reason about time, based on the timestamp of the block the computation takes
place in. By default, the timestamp of all blocks in Techelson is 1970-01-01T00:00:00Z
. Testcases
can set this timestamp to anything, the only constraint is that the new timestamp is older than the
previous one. The relevant instruction is
instruction | parameter | stack |
---|---|---|
SET_TIMESTAMP | none | :: timestamp : 'S |
-> 'S |
As an example, consider the following contract Timestamp which takes (or unit (contract unit))
:
it
- receives money once
(Left Unit)
, and - unlocks it if one week has passed since it receives money.
To unlock the money, someone must call the contract and give it a unit contract to collect the
money (Right <contract>)
. Unlocking the money is only legal after one week (604800
seconds) has
passed since the money was received.
The code follows. Its storage is (option timestamp)
which stores the last time it receives money.
The contract fails if asked to
- receive money but it's already storing money (its storage is not
None
), - unlock the money but it hasn't received anything (its storage is
None
), and - unlock the money but one week hasn't passed since it was received.
storage (option timestamp) ;
parameter (or unit (contract unit)) ;
code {
UNPAIR @storage @param ;
IF_LEFT {
DROP ;
IF_NONE {
NOW ;
SOME ;
NIL operation ;
PAIR
} {
PUSH string "cannot receive money twice" ;
FAILWITH
}
} {
SWAP ;
IF_NONE {
PUSH string "cannot send money, no money received" ;
FAILWITH
} {
NOW ;
SUB ;
PUSH int 604800 ; # One week in seconds.
IFCMPGT {
PUSH string "cannot send money, it has not been one week yet" ;
FAILWITH
} {
BALANCE ;
UNIT ;
TRANSFER_TOKENS ;
DIP {
NONE timestamp ;
NIL operation
} ;
CONS ;
PAIR
}
}
} ;
} ;
Let's go through TestTimestamp, the testcase for Timestamp. The first step should be unsurprising by now: deploy the contract and an account (so that we can unlock the money later).
{
{ # Deploy contract.
NONE timestamp ;
PUSH mutez 0 ;
PUSH bool False ;
PUSH bool True ;
NONE key_hash ;
PUSH key "manager" ;
HASH_KEY ;
CREATE_CONTRACT @contract "Timestamp" ;
DIP NIL operation ;
CONS ;
} ;
{ # Deploy account to retrieve the money later on.
PUSH mutez 0 ;
PUSH bool True ;
NONE key_hash ;
PUSH key "manager" ;
HASH_KEY ;
CREATE_ACCOUNT @account ;
DIP SWAP
} ;
CONS ;
APPLY_OPERATIONS ;
SWAP ;
{ # Retrieve contract.
CONTRACT (or unit (contract unit)) ;
IF_NONE {
PUSH string "failed to retrieve contract" ;
FAILWITH
} {}
} ;
DIP { # Retrieve account.
CONTRACT unit ;
IF_NONE {
PUSH string "failed to retrieve account" ;
FAILWITH
} {}
} ;
Next, let's set the timestamp to January 1 2019, 11am, and send some money to the contract.
{ # Set timestamp.
PUSH timestamp "2019-01-01T11:00:00Z" ;
SET_TIMESTAMP
} ;
{ # Send money to the account.
DUP ;
PUSH mutez 10 ;
UNIT ;
LEFT (contract unit) ;
TRANSFER_TOKENS ;
DIP NIL operation ;
CONS ;
APPLY_OPERATIONS
} ;
We now check the storage is what it should be:
{ # Check the storage is correct.
DUP ;
GET_STORAGE (option timestamp) ;
IF_NONE {
PUSH string "failed to retrieve storage" ;
FAILWITH
} {
IF_NONE {
PUSH string "storage should not be `None`" ;
FAILWITH
} {
PUSH timestamp "2019-01-01T11:00:00Z" ;
IFCMPNEQ {
PUSH string "storage should be `2019-01-01T11:00:00Z`" ;
FAILWITH
} {}
}
}
} ;
Let's make sure unlocking the money before one week has passed fails. First, the testcase sets the timestamp to January 8 2019, 9am, which is not one week later than the date at which we sent money to the contract. So this should fail.
{ # Set timestamp to almost one week later.
PUSH timestamp "2019-01-08T09:00:00Z" ;
SET_TIMESTAMP
} ;
{ # Try to retrieve the money.
DUP ;
PUSH mutez 0 ;
DUUUUP ;
RIGHT unit ;
TRANSFER_TOKENS ;
PUSH (option string) (Some "cannot send money, it has not been one week yet") ;
MUST_FAIL string ;
DIP NIL operation ;
CONS ;
APPLY_OPERATIONS
} ;
Last, let's set the date to January 8 2019, 11am, at which point unlocking the money should work.
{ # Set timestamp to exactly one week later.
PUSH timestamp "2019-01-08T11:00:00Z" ;
SET_TIMESTAMP
} ;
{ # Try to retrieve the money.
DUP ;
PUSH mutez 0 ;
DUUUUP ;
RIGHT unit ;
TRANSFER_TOKENS ;
DIP NIL operation ;
CONS ;
APPLY_OPERATIONS
} ;
{ # Check the account received it.
DROP ;
GET_BALANCE ;
PUSH mutez 10 ;
IFCMPNEQ {
PUSH string "account's balance should be 10utz" ;
FAILWITH
} {}
}
}
All set. Running techelson yields the following output. It is split in two parts here, first up to the request to unlock the money on January 8 2019 at 9am, which should fail:
$ techelson --contract rsc/timestamp/contracts/timestamp.tz -- rsc/timestamp/okay/testTimestamp.techel
Running test `TestTimestamp`
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
applying operation CREATE[uid:1] (@address[2]@account, "b58check:manager", None, true, true, 0utz)
{
storage unit ;
parameter unit ;
code ...;
}
timestamp: 1970-01-01 00:00:00 +00:00
live contracts: none
=> live contracts: <anonymous> (0utz) address[2]@account
Timestamp (0utz) address[1]@contract
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
applying operation TRANSFER[uid:2] address[0]@TestTimestamp -> address[1]@contract 10utz (Left Unit)
timestamp: 2019-01-01 11:00:00 +00:00
live contracts: <anonymous> (0utz) address[2]@account
Timestamp (0utz) address[1]@contract
running TRANSFER[uid:2] address[0]@TestTimestamp -> address[1]@contract 10utz (Left Unit)
timestamp: 2019-01-01 11:00:00 +00:00
=> live contracts: <anonymous> (0utz) address[2]@account
Timestamp (10utz) address[1]@contract
running test script...
timestamp: 2019-01-01 11:00:00 +00:00
applying operation MUST_FAIL[uid:4] "cannot send money, it has not been one week yet" :
string (TRANSFER[uid:3] address[0]@TestTimestamp -> address[1]@contract 0utz (Right address[2]@account))
timestamp: 2019-01-08 09:00:00 +00:00
live contracts: <anonymous> (0utz) address[2]@account
Timestamp (10utz) address[1]@contract
running TRANSFER[uid:3] address[0]@TestTimestamp -> address[1]@contract 0utz (Right address[2]@account)
timestamp: 2019-01-08 09:00:00 +00:00
=> live contracts: <anonymous> (0utz) address[2]@account
Timestamp (10utz) address[1]@contract
failure confirmed on test operation
MUST_FAIL[uid:4] "cannot send money, it has not been one week yet" :
string (TRANSFER[uid:3] address[0]@TestTimestamp -> address[1]@contract 0utz (Right address[2]@account))
while running operation TRANSFER[uid:3] address[0]@TestTimestamp -> address[1]@contract 0utz (Right address[2]@account)
failed with value "cannot send money, it has not been one week yet" :
string
So far so good. Finally, the rest of the output should go smoothly and succeed:
running test script...
timestamp: 2019-01-08 09:00:00 +00:00
applying operation TRANSFER[uid:5] address[0]@TestTimestamp -> address[1]@contract 0utz (Right address[2]@account)
timestamp: 2019-01-08 11:00:00 +00:00
live contracts: <anonymous> (0utz) address[2]@account
Timestamp (10utz) address[1]@contract
running TRANSFER[uid:5] address[0]@TestTimestamp -> address[1]@contract 0utz (Right address[2]@account)
timestamp: 2019-01-08 11:00:00 +00:00
=> live contracts: <anonymous> (0utz) address[2]@account
Timestamp (10utz) address[1]@contract
applying operation TRANSFER[uid:6] address[1]@contract -> address[2]@account 10utz Unit
timestamp: 2019-01-08 11:00:00 +00:00
live contracts: <anonymous> (0utz) address[2]@account
Timestamp (10utz) address[1]@contract
running TRANSFER[uid:6] address[1]@contract -> address[2]@account 10utz Unit
timestamp: 2019-01-08 11:00:00 +00:00
=> live contracts: <anonymous> (10utz) address[2]@account
Timestamp (0utz) address[1]@contract
running test script...
timestamp: 2019-01-08 11:00:00 +00:00
Done running test `TestTimestamp`
Test Generation
Techelson has a test generation feature. It is relatively naive: the testcases it generates do not really take the semantics of your contract into account. Given a contract, it will generate a random storage for that contract and deploy it. Then, it will create a random number of transfers to that contract with random parameters.
Note that while test generation is random, it is expected to be deterministic: the same test generation command on a contract should always generate the same test cases.
It is naive in the sense that it can (and statistically will) generate testcases which are not successful. Still, this feature is useful to generate a testcase skeleton with random contract creation and transfers that you can edit to test the behavior of your contract.
Example
Let's showcase testgeneration on simpleExample.tz:
storage nat;
parameter bool;
code {
UNPAIR; # Unpair parameter and storage.
IF { # Asked not to count: storage is unchanged, nothing to do.
} {
PUSH nat 1;
ADD
};
NIL operation; # We don't want to perform any operations.
PAIR # Aggregate the operation list and the new storage.
};
Test generation is activated by passing to techelson a testgen
argument triggering the test
generation mode. You can read more about modes in the Usage section. First, let's generate a
single testcase (-n 1
, or --count 1
) and let techelson run it:
$ techelson --contract rsc/simpleExample/contracts/simpleExample.tz testgen -n 1
done generating test for contract SimpleExample
Running test `SimpleExampleTest1`
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
applying operation CREATE[uid:0] (@address[1], "blake2b:11a6ffbc9fb85", None, false, false, 638557938255190utz) "SimpleExample"
timestamp: 1970-01-01 00:00:00 +00:00
live contracts: none
=> live contracts: SimpleExample (638557938255190utz) address[1]
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
applying operation TRANSFER[uid:1] address[0]@SimpleExampleTest1 -> address[1] 120584798270008utz True
timestamp: 1970-01-01 00:00:00 +00:00
live contracts: SimpleExample (638557938255190utz) address[1]
running TRANSFER[uid:1] address[0]@SimpleExampleTest1 -> address[1] 120584798270008utz True
timestamp: 1970-01-01 00:00:00 +00:00
=> live contracts: SimpleExample (759142736525198utz) address[1]
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
Done running test `SimpleExampleTest1`
While this can be useful for simple contracts, usually you want to retrieve the testcase directly
so that you can modify it to suit your needs. So let's still generate one testcase but this time we
will dump it in the current directory (trailing .
in the techelson
command).
$ techelson --contract rsc/simpleExample/contracts/simpleExample.tz testgen -n 1 . ; echo ; echo "testcase:" ; echo ; cat SimpleExampleTest1.techel ; rm SimpleExampleTest1.techel
done generating test for contract SimpleExample
dumping testcases to `.`
testcase:
{
# deploying contract `SimpleExample`
{
# creating contract creation parameters
{
# creating storage for contract `SimpleExample`
PUSH nat 807338681362247 ;
# money transferred
PUSH mutez 638557938255190 ;
# delegatable
PUSH bool False ;
# spendable
PUSH bool False ;
# delegate
NONE key_hash ;
# manager
{
PUSH key "11a6ffbc9fb85" ;
BLAKE2B
}
} ;
CREATE_CONTRACT "SimpleExample"
} ;
# create a list of operations and apply
{
NIL operation ;
SWAP ;
CONS ;
APPLY_OPERATIONS
} ;
# create transfer operation
{
DUP ;
# retrieve contract from address, fail if none
{
CONTRACT bool ;
IF_NONE {
PUSH string "unable to spawn contract `SimpleExample`" ;
FAILWITH
}
{}
} ;
PUSH mutez 120584798270008 ;
PUSH bool True ;
TRANSFER_TOKENS
} ;
# create a list of operations and apply
{
NIL operation ;
SWAP ;
CONS ;
APPLY_OPERATIONS
}
}
Quick Reference
This section provides a quick overview of some of techelson's features:
- list of all the extensions
- (brief) discussion on techelson's command-line usage
These sections try to provide relatively raw, brief and crucial information about the features they discuss. They will usually point to the relevant sections of this book if you need more details.
Extensions
Warning: the following instructions can only be used in contracts, not testcases:
SENDER
SOURCE
Techelson testcases have access to an extended instruction set to ease the process of writing
tests. See rsc/tests/
for more examples. Note that techelson treats #>
as whitespace, so you
can use extensions in contracts (when legal) while keeping them pure
michelson, like in the example below
DIP {
...
CONS ;
#> PRINT_STACK ;
#> STEP "after list cons" ;
PUSH int 3 ;
...
}
Unrestricted Extensions
The following instructions are legal in testcases and contracts:
-
STEP
andSTEP <string>
:'S
->
'S
Since
0.7.0
, relevant section of this book: First Steps.suspends the evaluator and prints a string, if any.
-
PRINT_STACK
:'S
->
'S
Since
0.7.0
, relevant section of this book: First Steps.prints the current state of the stack
Note that Techelson treats
#>
as whitespaces. You can have#> STEP "important step" ;
in your contracts which keeps them pure Michelson, but Techelson will stop when it reaches theSTEP
.
Testcase-only Extensions
The following instructions are only legal in testcases:
-
CREATE_CONTRACT <string>
::: key_hash : option key_hash : bool : bool : mutez : 'g : 'S
->
operation : address : 'S
Since
0.7.0
, relevant section of this book: Creating and Calling Contracts.Creates an instance of the contract named after the string parameter. The type of the storage of the contract must be
'g
. -
SPAWN_CONTRACT 'g
::: string : key_hash : option key_hash : bool : bool : mutez : 'g : 'S
->
operation : address : 'S
Since
0.7.0
, relevant section of this book: Creating and Calling Contracts.Exactly the same as the
CREATE_CONTRACT
extension above but the name of the contract is passed as a stack parameter, and the type of the storage is an explicit parameter of the instruction. -
APPLY_OPERATIONS
:(list operation) : 'S
->
'S
Since
0.7.0
, relevant section of this book: Creating and Calling Contracts.- consumes a list of operations
- suspends the execution of the testcase
- applies all the operations in the list (these operations can create operations which will be applied too)
- resumes the execution of the testcase
-
GET_STORAGE 'storage
:contract _ : 'S
->
(option 'storage) : 'S
address : 'S
->
(option 'storage) : 'S
Since
0.7.0
, relevant section of this book: Live Contract Inspection.- consumes a contract
- pushes
Some
of the current value of the storage of the contract if its storage has type'storage
,None
otherwise
-
GET_BALANCE
:contract _ : 'S
->
mutez : 'S
address : 'S
->
mutez : 'S
Since
0.7.0
, relevant section of this book: Live Contract Inspection.Same as
GET_STORAGE
, but pushes the balance of the contract instead of its storage -
MUST_FAIL 'a
:(option 'a) : operation : 'S
->
'S
Since
0.7.0
, relevant section of this book: Testing for Failures.Specifies that an operation (or the operation it creates recursively) must fail, and optionaly that it
FAIL
edWITH
a certain value. More precisely, the whole testcase will fail if theoperation
or the operations it creates recursively, when applied, either- succeeds, or
- the
(option 'a)
parameter is(Some value)
and the operation's failure was not caused by aFAILWITH
on preciselyvalue
Note that if the optional value is
NONE
, thenMUST_FAIL
accepts any kind of protocol failure, not justFAILWITH
. For instance, it will also accept creation/transfer operations that fail because of insufficient balance, because this precise operation already ran (it wasDUP
-ed), etc. -
SET_SOURCE code
, withcode :: [ 'A -> 'B ]
address :: 'A
->
'B
Since
0.7.0
, relevant section of this book: Usurpation of Identity.Sets the source of the testcase. Without this extension, the source of all transfers can only be the testcase. This allows to run
code
while pretending the testcase is a different live contract. -
SET_TIMESTAMP
timestamp :: 'S
->
'S
Since
0.7.0
, relevant section of this book: Timestamp Control.Sets the timestamp of the next block to some value. The default timestamp is
1970-01-01T00:00:00Z
.
Command-Line Options
Warning: so-called contract initializers are mentioned in the
--help
but are not currently supported.
The full list of options (for the nominal mode) is obtained with --help
:
$ techelson --help
techelson v0.7.0
USAGE:
../bin/techelson [OPTIONS] -- [FILES]*
../bin/techelson [OPTIONS] testgen [TESTGEN_OPTIONS] [-- DIR]?
OPTIONS:
-h, --help
prints this help message
-v, --verb <int>?
increases or sets verbosity [default: 1]
-q
decreases verbosity
-s, --step (on|true|True|no|off|false|False)?
(de)activates step-by-step evaluation [default: false]
--skip (on|true|True|no|off|false|False)?
if true, all steps will automatically advance (and `--step` will be set to
false) [default: false]
--contract <string> ',' <string>?
adds a contract to the test environment. The second optional argument is the
contract's initializer.
MODES:
testgen
activates and controls test generation
run `../bin/techelson <MODE> --help` to obtain help on a specific mode. For example: `../bin/techelson testgen --help`
Modes
Modes available:
testgen
: Test Generation
Techelson's modes are triggered by simply passing the name of the mode as an argument. For instance,
$ techelson --contract rsc/simpleExample/contracts/simpleExample.tz testgen -n 2
done generating tests for contract SimpleExample
Running test `SimpleExampleTest1`
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
applying operation CREATE[uid:0] (@address[1], "blake2b:11a6ffbc9fb85", None, false, false, 638557938255190utz) "SimpleExample"
timestamp: 1970-01-01 00:00:00 +00:00
live contracts: none
=> live contracts: SimpleExample (638557938255190utz) address[1]
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
applying operation TRANSFER[uid:1] address[0]@SimpleExampleTest1 -> address[1] 120584798270008utz True
timestamp: 1970-01-01 00:00:00 +00:00
live contracts: SimpleExample (638557938255190utz) address[1]
running TRANSFER[uid:1] address[0]@SimpleExampleTest1 -> address[1] 120584798270008utz True
timestamp: 1970-01-01 00:00:00 +00:00
=> live contracts: SimpleExample (759142736525198utz) address[1]
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
Done running test `SimpleExampleTest1`
Running test `SimpleExampleTest2`
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
applying operation CREATE[uid:0] (@address[3], "blake2b:", (Some "blake2b:"), true, true, 290035844265409utz) "SimpleExample"
timestamp: 1970-01-01 00:00:00 +00:00
live contracts: none
=> live contracts: SimpleExample (290035844265409utz) address[3]
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
applying operation TRANSFER[uid:1] address[2]@SimpleExampleTest2 -> address[3] 520090578679187utz True
timestamp: 1970-01-01 00:00:00 +00:00
live contracts: SimpleExample (290035844265409utz) address[3]
running TRANSFER[uid:1] address[2]@SimpleExampleTest2 -> address[3] 520090578679187utz True
timestamp: 1970-01-01 00:00:00 +00:00
=> live contracts: SimpleExample (810126422944596utz) address[3]
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
applying operation TRANSFER[uid:2] address[2]@SimpleExampleTest2 -> address[3] 845034275504874utz False
timestamp: 1970-01-01 00:00:00 +00:00
live contracts: SimpleExample (810126422944596utz) address[3]
running TRANSFER[uid:2] address[2]@SimpleExampleTest2 -> address[3] 845034275504874utz False
timestamp: 1970-01-01 00:00:00 +00:00
=> live contracts: SimpleExample (1655160698449470utz) address[3]
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
Done running test `SimpleExampleTest2`
You can have techelson print the options for mode <mode>
with techelson <mode> --help
.
Test Generation
Given a contract, this mode is in charge of generating a testcase automatically. The relevant chapter of this book is Test Generation.
$ techelson testgen --help
Generates testcases for some contract(s). If a directory is provided, the testcases will
be dumped there. Otherwise techelson will just run the testcases it generated.
USAGE:
../bin/techelson [OPTIONS] testgen [TESTGEN_OPTIONS] [-- DIR]?
TESTGEN_OPTIONS:
-h, --help
prints this help message
-n, --count <int>
sets the number of testcases to generate [default: 1]