4

When writing tests in F# I am trying to generate useful messages about the state that caused errors. In python I would include all the locals(), so they are easily accessible in the test trace.

Is there a similar construct in F#?

I have been searching the web and the excellent fsharpforfunandprofit site, as well as gone through the list of reserved keywords.

Here is some code I would like to be able to do.

[<Property>]
let some_test x =
    let example_inner_helper y =
        let z = y + x
        // Example of the meta-construction i am looking for
        let scope = {| 
            local = {| y = y; z = z |}; 
            capture = {| 
                            local = {| x = x |};
                            capture = {| (* ... *) |};
            |};
        |}
        x = z |@ sprintf "require %A=%A (%A)" x y scope
    example_inner_helper (x+x)

Which would produce the very useful output

 Tests.some_test
   Source: Tests.fs line 105
   Duration: 51 ms

  Message: 
    FsCheck.Xunit.PropertyFailedException : 
    Falsifiable, after 1 test (1 shrink) (StdGen (387696160,296644521)):
    Label of failing property: require 1=2 ({ capture = { capture = {}
                  local = { x = 1 } }
      local = { y = 2
                z = 3 } })
    Original:
    -1
    Shrunk:
    1

However, I am having to explicitly capture the information that a construct like "scope" could automatically provide. Resulting in ugly, error prone stuff like

let ``firstAscendingLastDescendingPartE Prop`` (a: Extent<int>) b =
    let validateT ea eb =
        let checkXbeforeY ex ey headx restx =
            match restx with 
                | ValueNone -> // No rest
                    (ex = headx) |@ sprintf "no rest: ex = headx (%A = %A)" ex headx 
                    .&. ((intersectE ex ey) = ValueNone) |@ sprintf "no rest: ex ∩ ey = ∅ (%A ∩ %A)" ex ey
                | ValueSome rex -> // Some rest -> 
                    // headx and restx combines to ex
                    ((expandE headx rex) = ex) |@ sprintf "rest: headx+rex = ex (%A...%A)" headx rex 
                    .&. (exactlyTouchesE headx rex) |@ sprintf "rest: headx ends where rex starts (ex=%A ey=%A headx=%A restx=%A)" ex ey headx restx
                    .&. (exactlyTouchesE headx ey) |@ sprintf "rest: headx ends where ey starts (ex=%A ey=%A headx=%A restx=%A)" ex ey headx restx
....

(Of course, I don't care about the actual type of the "scope" construct)

Regarding unquote

I already looked at unquote which is quite cute and looks about right. But it limits the code quite a bit:

  • Error FS3155 A quotation may not involve an assignment to or taking the address of a captured local variable
  • Error FS1230 Inner generic functions are not permitted in quoted expressions. Consider adding some type constraints until this function is no longer generic.

I have code that looks a bit like:

[<Struct>]
type X<'T when 'T: comparison and 'T: equality> =
    val public first: 'T
    val public last: 'T
    new (first: 'T, last: 'T) = {
        first = 
            (if last <= first then 
                invalidOp (sprintf "first < last required, (%A) is not < (%A)" first last) 
             else first)
        last = last;
    }
    // ..

So I have issues with tests using the two constructs below (causing the above errors).

let quote_limits () = 
    let x first last = X(first, last)
    let validate (x: X<'a>) = x.first < x.last
    let a = X(0, 1)
    <@ a.first = 0 @> |> test
    <@ validate a @> |> test 

The first one i can workaround with functions to access the struct parts, but the limitation on generics is PITA.

Helge Jensen
  • 133
  • 10

1 Answers1

4

I don't think there is a way to do this, even in principle. F# code gets compiled to .NET bytecode which is stack based and so local variables do not really (in general) exist in the compiled code. You might be able to get some globals (they are static members) or perhaps use debug information somehow, but I don't think there is a standard way of doing that.

That said, in your specific case, F# actually has a nice option using the unquote testing framework. This lets you put test code in F# quotations and it then shows how this quotation evaluates. For example:

let some_test x =
    let example_inner_helper y =
        <@ 
          let z = y + x
          x = z 
        @> |> test
    example_inner_helper (x+x)

some_test 10

Here, I defined the local variable z inside the quotation. When you run the test, unquote will print the individual evaluation steps and you can see the value of z there:

Test failed:

let z = y + x in x = z
let z = 20 + 10 in x = z
let z = 30 in x = z
false
Tomas Petricek
  • 240,744
  • 19
  • 378
  • 553
  • Thanks for that. I actually started out with unquote, but had issues with the restrictions on the expressions that could be quoted (se updates to question). – Helge Jensen Sep 15 '19 at 21:08