Work on test framework

Merged E. Rivas requested to merge er433/test/run into dev
  • has a changelog entry

This MR tries to improve on the testing framework for easier usability. Currently, it modifies the previous framework as follows.

  1. There's a new type for representing contracts (or typed addresses) with parameter and storage: (parameter, storage) typed_address.
  2. The new type for Test.originate is Test.originate : ('parameter * 'storage -> operation list * 'storage) -> 'storage -> tez -> ('parameter, 'storage) typed_address, previous originate is still accessible as Test.originate_from_file, but with an extra argument in tez for initial balance.
  3. There's a new function for transforming a typed_address into a contract: Test.to_contract : ('p, 's) typed_address -> 'p contract. There's also some initial support for entrypoints, using Test.to_entrypoint : string -> ('p, 's) typed_address -> 'q contract that requires an additional string representing the entrypoint (the term needs to be annotated). Actually, the type of Test.to_contract is ('p, 's) typed_address -> 'e contract where 'e is the type of the default entrypoint in 'p in case there's one, or 'p if not.
  4. The new type for Test.get_storage is Test.get_storage : ('p, 's) typed_address -> 's, previous get_storage is still accessible as Test.get_storage_of_address.
  5. There's a new function Test.eval for getting the michelson_program of a value: Test.eval : 'a -> michelson_program.
  6. There's a new function Test.run that takes a function and an argument, it will: a) compile the function to Michelson f_mich; b) it will take the value to which the argument evaluates to, and compile it to Michelson v_mich; c) run the Michelson interpreter on code f_mich with starting stack [ v_mich ]. It returns something of type michelson_program.
  7. Test.get_balance now has type Test.get_balance : address -> tez.
  8. Test.transfer and Test.transfer_exn now take tez instead of nat.
  9. There are new functions for transferring directly to a contract: transfer_to_contract : 'p contract -> 'p -> tez -> test_exec_result and the corresponding transfer_to_contract_exn.
  10. Tests are top-level, names prefixed with test are printed in the output together with the value they evaluate to.

Let's see a self-contained example (test.mligo):

(* This is the example from LIGO's frontpage: *)
type storage = int
type parameter =
  Increment of int
| Decrement of int
| Reset

type return = operation list * storage

// Two entrypoints
let add (store, delta : storage * int) : storage = store + delta
let sub (store, delta : storage * int) : storage = store - delta

(* Main access point that dispatches to the entrypoints according to
   the smart contract parameter. *)
let main (action, store : parameter * storage) : return =
 ([] : operation list),    // No operations
 (match action with
   Increment (n) -> add (store, n)
 | Decrement (n) -> sub (store, n)
 | Reset         -> 0)

(* Here comes the test: *)
let test =
  (* Test.run f x : compiles x, compiles f, and runs f on the stack with x *)
  let x : michelson_program = Test.run (fun (x : int) -> Some x) 42 in
  (* This will fail, as argument uses Test primitive:
     let x = Test.run (fun (g : unit -> unit) -> g ()) (fun () -> Test.log "hello test!") in *)
  (* Originate main contract with 42 as initial storage *)
  let (typed_addr, _, _) = Test.originate main 42 0tez in
  (* We recover the contract from the typed_address *)
  let contr = Test.to_contract typed_addr in
  (* We execute an external call to the contract with parameter Increment 5 *)
  match Test.transfer_to_contract contr (Increment 5) 0tez with
    Success ->
      let st = Test.get_storage typed_addr in
      (47 = st)
  | Fail (_) ->
      false

We run it as before:

$ dune exec -- ligo test test.mligo test
Everything at the top-level was executed.
- test exited with value true.

Slightly more complex variant, that uses records in the storage, mainly for internal testing:

type flag = Reseted of int | Not_reseted
type storage = { value : int ; reseted : flag }
type parameter =
  Increment of int
| Decrement of int
| Reset

type return = operation list * storage

// Two entrypoints
let add (store, delta : int * int) : int = store + delta
let sub (store, delta : int * int) : int = store - delta

(* Main access point that dispatches to the entrypoints according to
   the smart contract parameter. *)
let main (action, store : parameter * storage) : return =
 ([] : operation list),    // No operations
 (match action with
   Increment (n) -> { store with value = add (store.value, n) }
 | Decrement (n) -> { store with value = sub (store.value, n) }
 | Reset         -> { value = 0; reseted = Reseted store.value })

let test =
  let init_st = { value = 12 ; reseted = Not_reseted } in
  let (typed_addr, _, _) = Test.originate main init_st 0tez in
  let contr = Test.to_contract typed_addr in
  let () = Test.transfer_to_contract_exn contr Reset 1mutez in
  match Test.transfer_to_contract contr (Increment 5) 1mutez with
    Success ->
      let st = Test.get_storage typed_addr in
      (5 = st.value) && st.reseted <> Not_reseted
  | Fail (_) ->
      false

As more interesting example, here's FA1.2.mligo file from LIGO distribution tested using the framework (scroll to the end to see the tests).

Edited by E. Rivas