I have moved!

I've moved my blog
CLICK HERE

Friday 13 February 2009

Displaying a nested evaluation tree from Expression<Func<bool>>

Updated: The downloadable source (see link below) is now tidied up a bit, and also displays any embedded string comparisons in a similar way to NUnit.

This might be useful as a way to write Assert statements in tests. Instead of requiring many different forms of Assert to capture values and intents, we could just have one Assert that accepts a lambda:

public static void Assert(Expression<Func<bool>> expr)
{
    if (!(bool)Trace(expr.Body, 0))
    {
        // throw an exception...
    }
}

Now all we need is a Trace function. The challenge is making it include all the relevant information about the test, including the values being used. But this isn't actually that hard, because you just recursively search through the tree of nested expressions, evaluating them all:

public static object Trace(Expression part, int indent)
{
    string indentString = new String(' ', indent*2);

    Console.Write(indentString);
    Console.Write(GetExpressionText(part));
    Console.Write(" == ");
    LambdaExpression lambda = Expression.Lambda(part);
    Delegate callable = lambda.Compile();
    object result = callable.DynamicInvoke(null);

    if (result is string)
        result = "\"" + result + "\"";

    Console.Write(result);

    var nestedExpressions = part
        .GetType()
        .GetProperties()
        .Where(p => typeof(Expression).IsAssignableFrom(p.PropertyType))
        .Select(p => (Expression)p.GetValue(part, null))
        .Where(x => (x != null) && !(x is ConstantExpression));

    if (nestedExpressions.Any())
    {
        Console.WriteLine(" where");
        Console.Write(indentString);
        Console.WriteLine("{");

        foreach (Expression nested in nestedExpressions)
            Trace(nested, indent + 1);

        Console.Write(indentString);
        Console.WriteLine("}");
    }
    else
        Console.WriteLine();

    return result;
}

I just use reflection and a bit of LINQ to hunt for nested expressions, so I don't need to laboriously handle the various kinds of expression.

I don't bother presenting the value of constants, because it would just explain to the user that 5 == 5, which isn't very enlightening.

The GetExpressionText function is a helper to tidy up the output. Variables captured in the lambda have some nasty looking prefix to identify which compiler-generated class they belong to, but that's unlikely to be useful here, so I strip it out:

public static string GetExpressionText(Expression expr)
{
    string text = expr.ToString();

    const string prefix = "value(";
    const string suffix = ").";

    StringBuilder builder = new StringBuilder();

    int start = 0;
    int pos = text.IndexOf(prefix);
    while (pos != -1)
    {
        builder.Append(text.Substring(start, pos - start));
        start = text.IndexOf(suffix, pos) + suffix.Length;
        pos = text.IndexOf(prefix, start);
    }

    if (start < text.Length)
        builder.Append(text.Substring(start, text.Length - start));

    return builder.ToString();
}

And now for a little demo:

int x = 3;
string t = "hi";
Assert(() => 5*x + (2 / t.Length) < 99);

Which produces the remarkably readable output:

(((5 * x) + (2 / t.Length)) < 99) == True where
{
  ((5 * x) + (2 / t.Length)) == 16 where
  {
    (5 * x) == 15 where
    {
      x == 3
    }
    (2 / t.Length) == 1 where
    {
      t.Length == 2 where
      {
        t == "hi"
      }
    }
  }
}

Obviously not a hugely strenuous test, but I suspect any other edge cases could easily be dealt with.

The way I've written Assert is just for demo purposes, because I wanted to see the trace regardless of whether the test passes or fails. In a real framework you'd only print out the trace if the test failed. Apart from making the successful output as short as possible (silence is golden) it would also help your tests run faster.

Download full source here: http://www.earwicker.com/blogfiles/ExpressionTracer.zip

1 comment:

IDisposable said...

Very nice idea!


You can StringBuilder.Append a substring directly like:
builder.Append(text, start, pos - start);

Instead of calling:
builder.Append(text.Substring(start, pos - start));