External Contract Example
Let's give ourselves an external contract that we can write a test for. We will use contracts/multi.liq. It's whole code is available in the listing, but for writing a test we only need a high-level understanding of what it does.
Multi
, in contracts/multi.liq, offers its clients to store tokens for them. It has some
administrators, which are the only one able to add new clients. First, the contract's storage is
type storage = {
admins : (string, address) map ;
users : (string, (address * tez * UnitContract.instance)) map ;
}
Multi
stores some named administrators in a map admins
from names to administrator addresses.
It also stores some named clients in a map clients
. It maps the name of a client to
- its address
- the amount of money it has stored on this contract
- an account which will receive all the money the client has if the client asks to drains it.
Entry Points
The entry points of Multi
we are interested in are
let%entry add_client (
(admin_name, user_name, user, c) :
string * string * address * UnitContract.instance
) (storage : storage) =
...
Adds a new user. Only administrators can do this.
let%entry drain (name : string) (storage : storage) =
...
Transfers all the money of a user to the account provided on creation. Only the client can call this entry point (with the right name).
Testing Multi (and failing to do so)
Let's first write a test which creates an instance of Multi
with an admin named root
. It then adds a new administrator:
let nothing : operation list * unit = [], ()
(* Creates a storage for Multi with one administrator. *)
let one_admin (root : string) (address : address) : Multi.storage = {
Multi.admins =
Map.add root address (Map : (string, address) map) ;
Multi.users =
(Map : (string, (address * tez * UnitContract.instance)) map) ;
}
(* Deploys an account with an arbitrary manager. *)
let deploy_account_op (amount: tez) : operation * address =
let delegate : key_hash option = None in
Account.create
~manager:tz1YLtLqD1fWHthSVHPD116oYvsd4PTAHUoc
~delegate
~delegatable:true
~amount
(* Deploys an instance of multi with an arbitrary manager. *)
let deploy_contract_op (storage : Multi.storage) : operation * address =
let delegate : key_hash option = None in
Contract.create
~manager:tz1YLtLqD1fWHthSVHPD116oYvsd4PTAHUoc
~delegate
~delegatable:true
~spendable:false
~amount:0tz
~storage
~code:(contract Multi)
(* Storage of the test is irrelevant. *)
type storage = unit
(* Actual test. *)
let%entry test (_param : unit) (_storage : unit) =
let root_op, root = deploy_account_op 0tz in
let storage = one_admin "root" root in
let main_op, main = deploy_contract_op storage in
(* ask techelson to apply these operations. *)
Techel.apply_operations [ root_op ; main_op ];
(* root and main are live now *)
(* let's check root is an admin, and that the address is correct *)
let storage =
match Techel.get_storage [%type: Multi.storage] main with
| Some storage -> storage
| None -> failwith "can't retrieve contract's storage"
in
(
match Map.find "root" storage.Multi.admins with
| None -> failwith "no root in storage"
| Some address -> (
if address <> root then (
failwith "wrong address for root"
)
)
);
let client_op, client = deploy_account_op 15tz in
(* deploy the client *)
Techel.apply_operations [ client_op ];
(* client is live now *)
(* retrieve client instance for registration *)
let client_instance =
match (Contract.at client : UnitContract.instance option) with
| None -> failwith "could not retrieve main contract"
| Some instance -> instance
in
(* retrieve multi's instance to call it *)
let main_instance =
match Multi.at main with
| None -> failwith "could not retrieve main contract"
| Some instance -> instance
in
(* let's add a client *)
let add_client =
Contract.call ~dest:main_instance ~amount:0tz ~entry:add_client ~parameter:(
"root", "lucy", client, client_instance
)
in
Techel.apply_operations [ add_client ];
nothing
Let's run this test. It will fail though, as it should. So we will call it tests/test1_err.liq. The relevant part of the output is
$ ./../test.sh ../tests/test1_err.liq
[....]
Test `Test1_err` failed:
Error
operation TRANSFER[uid:3] address[0]@Test1_err -> address[2] 0utz (Right (Right (Left ("root", ("lucy", (address[3], address[3])))))) was expected to succeed
but failed on operation TRANSFER[uid:3] address[0]@Test1_err -> address[2] 0utz (Right (Right (Left ("root", ("lucy", (address[3], address[3]))))))
operation failed on "illegal access to admin account" : string
The reason this test failed is because only administrator can add clients. So here, only root
can
add a new client. Hence, we need to pretend to be root
. More precisely, we need to pretend to be
root when we create the operation.
But this test is not worthless. It shows that an outsider cannot add new clients even by using the
name of an existing outsider. What we should do is say that this transfer must fail. This is what
tests/test1.liq does. Only the apply_operations
changes:
let must_fail = Techel.must_fail None bad_add_client in
Techel.apply_operations [ must_fail ];
Extension must_fail None op
produces an operation that succeeds iff op
fails. Techelson notifies you that the failure was confirmed in its output:
failure confirmed on test operation
MUST_FAIL[uid:4] _ (TRANSFER[uid:3] address[0]@Test1 -> address[2] 0utz (Right (Right (Left ("root", ("lucy", (address[3], address[3])))))))
while running operation TRANSFER[uid:3] address[0]@Test1 -> address[2] 0utz (Right (Right (Left ("root", ("lucy", (address[3], address[3]))))))
failed with value "illegal access to admin account" : string
We can do better than this: we can ask Techelson to verify the error message is the one we expect.
This is exactly what the first argument of must_fail
does. must_fail (Some msg) op
succeeds iff
op
fails with exactly the string msg
.
Test tests/test1_better.liq only changes in the error message:
let error_message = Some "illegal access to admin account" in
let must_fail = Techel.must_fail error_message bad_add_client in
Techel.apply_operations [ must_fail ];
Techelson confirms the failure and its error message (see the output):
failure confirmed on test operation
MUST_FAIL[uid:4] "illegal access to admin account" : string (TRANSFER[uid:3] address[0]@Test1_better -> address[2] 0utz (Right (Right (Left ("root", ("lucy", (address[3], address[3])))))))
while running operation TRANSFER[uid:3] address[0]@Test1_better -> address[2] 0utz (Right (Right (Left ("root", ("lucy", (address[3], address[3]))))))
failed with value "illegal access to admin account" : string
Testing Multi
Let's now make the transfer work by pretending to be root
. The current solution for this is not
very satisfactory, but it will do the job until techelson is more tightly integrated in Liquidity.
The result is tests/test2.liq, where we add:
Techel.start_set_source root ;
(* all operations created in here will appear to have been created by `root` *)
let add_client =
Contract.call ~dest:main_instance ~amount:0tz ~entry:add_client ~parameter:(
"root", "lucy", client, client_instance
)
in
Techel.end_set_source () ;
The test is now successful:
$ ./../test.sh ../tests/test1.liq
Compiling ../tests/test1.liq...
Module Techel
Contract Multi
Main contract Test1
File "../tests/test1.liq.techel" generated
If tezos is compiled, you may want to typecheck with:
tezos-client typecheck script ../tests/test1.liq.techel
Running test ../tests/test1.liq.techel
Running test `Test1`
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
applying operation CREATE[uid:0] (@address[1], "tz1YLtLqD1fWHthSVHPD116oYvsd4PTAHUoc", 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]
<anonymous> (0utz) address[1]
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
applying operation CREATE[uid:2] (@address[3], "tz1YLtLqD1fWHthSVHPD116oYvsd4PTAHUoc", None, true, true, 15000000utz)
{
storage unit ;
parameter unit ;
code ...;
}
timestamp: 1970-01-01 00:00:00 +00:00
live contracts: <anonymous> (0utz) address[2]
<anonymous> (0utz) address[1]
=> live contracts: <anonymous> (0utz) address[2]
<anonymous> (15000000utz) address[3]
<anonymous> (0utz) address[1]
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
applying operation MUST_FAIL[uid:4] _ (TRANSFER[uid:3] address[0]@Test1 -> address[2] 0utz (Right (Right (Left ("root", ("lucy", (address[3], address[3])))))))
timestamp: 1970-01-01 00:00:00 +00:00
live contracts: <anonymous> (0utz) address[2]
<anonymous> (15000000utz) address[3]
<anonymous> (0utz) address[1]
running TRANSFER[uid:3] address[0]@Test1 -> address[2] 0utz (Right (Right (Left ("root", ("lucy", (address[3], address[3]))))))
timestamp: 1970-01-01 00:00:00 +00:00
=> live contracts: <anonymous> (0utz) address[2]
<anonymous> (15000000utz) address[3]
<anonymous> (0utz) address[1]
failure confirmed on test operation
MUST_FAIL[uid:4] _ (TRANSFER[uid:3] address[0]@Test1 -> address[2] 0utz (Right (Right (Left ("root", ("lucy", (address[3], address[3])))))))
while running operation TRANSFER[uid:3] address[0]@Test1 -> address[2] 0utz (Right (Right (Left ("root", ("lucy", (address[3], address[3]))))))
failed with value "illegal access to admin account" : string
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
Done running test `Test1`
Finally, let's add more tests. We will have the client ("lucy"
) deposit 10tz
on her account,
which should leave her with 5tz
(remember she was created with 15tz
). She will then drain her
account, which will trigger Multi to send her all of her money, at which point she should have
15tz
.
The additional code, in tests/test3.liq, is
Techel.start_set_source client ;
let deposit_money =
Contract.call ~dest:main_instance ~amount:10tz ~entry:deposit ~parameter:"lucy"
in
Techel.end_set_source () ;
Techel.apply_operations [ deposit_money ];
let balance_lucy = Techel.get_balance client in
if balance_lucy <> 5tz then (
failwith "lucy should have 5tz now"
);
(* lucy walks out of the whole thing *)
Techel.start_set_source client ;
let drain =
Contract.call ~dest:main_instance ~amount:0tz ~entry:drain ~parameter:"lucy"
in
Techel.end_set_source () ;
Techel.apply_operations [ drain ] ;
(* lucy should have her money back *)
let balance_lucy = Techel.get_balance client in
if balance_lucy <> 15tz then (
failwith "lucy should have 15tz now"
);
which is successful:
$ ./../test.sh ./../tests/test3.liq
Compiling ./../tests/test3.liq...
Module Techel
Contract Multi
Main contract Test3
File "./../tests/test3.liq.techel" generated
If tezos is compiled, you may want to typecheck with:
tezos-client typecheck script ./../tests/test3.liq.techel
Running test ./../tests/test3.liq.techel
Running test `Test3`
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
applying operation CREATE[uid:0] (@address[1], "tz1YLtLqD1fWHthSVHPD116oYvsd4PTAHUoc", 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]
<anonymous> (0utz) address[1]
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
applying operation CREATE[uid:2] (@address[3], "tz1YLtLqD1fWHthSVHPD116oYvsd4PTAHUoc", None, true, true, 15000000utz)
{
storage unit ;
parameter unit ;
code ...;
}
timestamp: 1970-01-01 00:00:00 +00:00
live contracts: <anonymous> (0utz) address[2]
<anonymous> (0utz) address[1]
=> live contracts: <anonymous> (0utz) address[2]
<anonymous> (15000000utz) address[3]
<anonymous> (0utz) address[1]
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]@Test3 -> address[2] 0utz (Right (Right (Left ("root", ("lucy", (address[3], address[3])))))))
timestamp: 1970-01-01 00:00:00 +00:00
live contracts: <anonymous> (0utz) address[2]
<anonymous> (15000000utz) address[3]
<anonymous> (0utz) address[1]
running TRANSFER[uid:3] address[0]@Test3 -> address[2] 0utz (Right (Right (Left ("root", ("lucy", (address[3], address[3]))))))
timestamp: 1970-01-01 00:00:00 +00:00
=> live contracts: <anonymous> (0utz) address[2]
<anonymous> (15000000utz) address[3]
<anonymous> (0utz) address[1]
applying operation TRANSFER[uid:5] address[1] -> address[2] 0utz (Right (Right (Left ("root", ("lucy", (address[3], address[3]))))))
timestamp: 1970-01-01 00:00:00 +00:00
live contracts: <anonymous> (0utz) address[2]
<anonymous> (15000000utz) address[3]
<anonymous> (0utz) address[1]
failure confirmed on test operation
MUST_FAIL[uid:4] "illegal access to admin account" : string (TRANSFER[uid:3] address[0]@Test3 -> address[2] 0utz (Right (Right (Left ("root", ("lucy", (address[3], address[3])))))))
while running operation TRANSFER[uid:3] address[0]@Test3 -> address[2] 0utz (Right (Right (Left ("root", ("lucy", (address[3], address[3]))))))
failed with value "illegal access to admin account" : string
running TRANSFER[uid:5] address[1] -> address[2] 0utz (Right (Right (Left ("root", ("lucy", (address[3], address[3]))))))
timestamp: 1970-01-01 00:00:00 +00:00
=> live contracts: <anonymous> (0utz) address[2]
<anonymous> (15000000utz) address[3]
<anonymous> (0utz) address[1]
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
applying operation TRANSFER[uid:6] address[3] -> address[2] 10000000utz (Right (Right (Right (Left "lucy"))))
timestamp: 1970-01-01 00:00:00 +00:00
live contracts: <anonymous> (0utz) address[2]
<anonymous> (15000000utz) address[3]
<anonymous> (0utz) address[1]
running TRANSFER[uid:6] address[3] -> address[2] 10000000utz (Right (Right (Right (Left "lucy"))))
timestamp: 1970-01-01 00:00:00 +00:00
=> live contracts: <anonymous> (10000000utz) address[2]
<anonymous> (5000000utz) address[3]
<anonymous> (0utz) address[1]
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
applying operation TRANSFER[uid:7] address[3] -> address[2] 0utz (Right (Right (Right (Right (Right "lucy")))))
timestamp: 1970-01-01 00:00:00 +00:00
live contracts: <anonymous> (10000000utz) address[2]
<anonymous> (5000000utz) address[3]
<anonymous> (0utz) address[1]
running TRANSFER[uid:7] address[3] -> address[2] 0utz (Right (Right (Right (Right (Right "lucy")))))
timestamp: 1970-01-01 00:00:00 +00:00
=> live contracts: <anonymous> (10000000utz) address[2]
<anonymous> (5000000utz) address[3]
<anonymous> (0utz) address[1]
applying operation TRANSFER[uid:8] address[2] -> address[3] 10000000utz Unit
timestamp: 1970-01-01 00:00:00 +00:00
live contracts: <anonymous> (10000000utz) address[2]
<anonymous> (5000000utz) address[3]
<anonymous> (0utz) address[1]
running TRANSFER[uid:8] address[2] -> address[3] 10000000utz Unit
timestamp: 1970-01-01 00:00:00 +00:00
=> live contracts: <anonymous> (0utz) address[2]
<anonymous> (15000000utz) address[3]
<anonymous> (0utz) address[1]
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
Done running test `Test3`
Last, let's again make sure we're actually testing something by checking that, at the end, lucy's
balance is 2tz
(it's not) in tests/test3_err.liq:
(* lucy should have her money back *)
let balance_lucy = Techel.get_balance client in
if balance_lucy <> 2tz then (
failwith "lucy should have 2tz now"
);
This fails:
$ ./../test.sh ../tests/test3_err.liq
Compiling ../tests/test3_err.liq...
Module Techel
Contract Multi
Main contract Test3_err
File "../tests/test3_err.liq.techel" generated
If tezos is compiled, you may want to typecheck with:
tezos-client typecheck script ../tests/test3_err.liq.techel
Running test ../tests/test3_err.liq.techel
Running test `Test3_err`
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
applying operation CREATE[uid:0] (@address[1], "tz1YLtLqD1fWHthSVHPD116oYvsd4PTAHUoc", 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]
<anonymous> (0utz) address[1]
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
applying operation CREATE[uid:2] (@address[3], "tz1YLtLqD1fWHthSVHPD116oYvsd4PTAHUoc", None, true, true, 15000000utz)
{
storage unit ;
parameter unit ;
code ...;
}
timestamp: 1970-01-01 00:00:00 +00:00
live contracts: <anonymous> (0utz) address[2]
<anonymous> (0utz) address[1]
=> live contracts: <anonymous> (0utz) address[2]
<anonymous> (15000000utz) address[3]
<anonymous> (0utz) address[1]
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]@Test3_err -> address[2] 0utz (Right (Right (Left ("root", ("lucy", (address[3], address[3])))))))
timestamp: 1970-01-01 00:00:00 +00:00
live contracts: <anonymous> (0utz) address[2]
<anonymous> (15000000utz) address[3]
<anonymous> (0utz) address[1]
running TRANSFER[uid:3] address[0]@Test3_err -> address[2] 0utz (Right (Right (Left ("root", ("lucy", (address[3], address[3]))))))
timestamp: 1970-01-01 00:00:00 +00:00
=> live contracts: <anonymous> (0utz) address[2]
<anonymous> (15000000utz) address[3]
<anonymous> (0utz) address[1]
applying operation TRANSFER[uid:5] address[1] -> address[2] 0utz (Right (Right (Left ("root", ("lucy", (address[3], address[3]))))))
timestamp: 1970-01-01 00:00:00 +00:00
live contracts: <anonymous> (0utz) address[2]
<anonymous> (15000000utz) address[3]
<anonymous> (0utz) address[1]
failure confirmed on test operation
MUST_FAIL[uid:4] "illegal access to admin account" : string (TRANSFER[uid:3] address[0]@Test3_err -> address[2] 0utz (Right (Right (Left ("root", ("lucy", (address[3], address[3])))))))
while running operation TRANSFER[uid:3] address[0]@Test3_err -> address[2] 0utz (Right (Right (Left ("root", ("lucy", (address[3], address[3]))))))
failed with value "illegal access to admin account" : string
running TRANSFER[uid:5] address[1] -> address[2] 0utz (Right (Right (Left ("root", ("lucy", (address[3], address[3]))))))
timestamp: 1970-01-01 00:00:00 +00:00
=> live contracts: <anonymous> (0utz) address[2]
<anonymous> (15000000utz) address[3]
<anonymous> (0utz) address[1]
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
applying operation TRANSFER[uid:6] address[3] -> address[2] 10000000utz (Right (Right (Right (Left "lucy"))))
timestamp: 1970-01-01 00:00:00 +00:00
live contracts: <anonymous> (0utz) address[2]
<anonymous> (15000000utz) address[3]
<anonymous> (0utz) address[1]
running TRANSFER[uid:6] address[3] -> address[2] 10000000utz (Right (Right (Right (Left "lucy"))))
timestamp: 1970-01-01 00:00:00 +00:00
=> live contracts: <anonymous> (10000000utz) address[2]
<anonymous> (5000000utz) address[3]
<anonymous> (0utz) address[1]
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
applying operation TRANSFER[uid:7] address[3] -> address[2] 0utz (Right (Right (Right (Right (Right "lucy")))))
timestamp: 1970-01-01 00:00:00 +00:00
live contracts: <anonymous> (10000000utz) address[2]
<anonymous> (5000000utz) address[3]
<anonymous> (0utz) address[1]
running TRANSFER[uid:7] address[3] -> address[2] 0utz (Right (Right (Right (Right (Right "lucy")))))
timestamp: 1970-01-01 00:00:00 +00:00
=> live contracts: <anonymous> (10000000utz) address[2]
<anonymous> (5000000utz) address[3]
<anonymous> (0utz) address[1]
applying operation TRANSFER[uid:8] address[2] -> address[3] 10000000utz Unit
timestamp: 1970-01-01 00:00:00 +00:00
live contracts: <anonymous> (10000000utz) address[2]
<anonymous> (5000000utz) address[3]
<anonymous> (0utz) address[1]
running TRANSFER[uid:8] address[2] -> address[3] 10000000utz Unit
timestamp: 1970-01-01 00:00:00 +00:00
=> live contracts: <anonymous> (0utz) address[2]
<anonymous> (15000000utz) address[3]
<anonymous> (0utz) address[1]
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
Test `Test3_err` failed:
Tezos protocol error
Failure on value "lucy should have 2tz now" : string
Error
1 of the 1 testcase failed