Sunday, April 22, 2012

Movimentum - Unit testing the parser

The parser (and model builder) is not that complicated - but there is a minimal number of tests that we should provide. First of all, each path in the actions should be tested once - a copy paste error could e.g. produce a wrong operator, or drop a part of an expression. Moreover, a few features warrant special testing. They are:
  1. The hard-wired anchor definition computations.
  2. Image loading.
  3. The time computation for the steps (the two variants of the @ feature).
  4. Producing different variables when using the underscore _.
  5. Correct disambiguation in the rule with backtrack=true.
This takes some work. However, when the grammar is extended (and it will be extended - there are already a few features I'd like to have), these unit tests are necessary as a safety net.

A first test for numbers might look like this:

    [Test]
    public void Test_number() {
        Assert.AreEqual(1.0, GetParser("1").number(), 1e-10);
        Assert.AreEqual(1.0, GetParser("1.").number(), 1e-10);
        Assert.AreEqual(10.0, GetParser("1.E1").number(), 1e-10);
        Assert.AreEqual(0.01, GetParser("1.E-2").number(), 1e-10);
        Assert.AreEqual(10.1, GetParser("1.01E1").number(), 1e-10);
        Assert.AreEqual(0.010001, GetParser("1.0001E-2").number(), 1e-10);
    }

Then, a test might just parse all sorts of scalar expressions:

        [Test]
        public void TestJustParse_scalarexpr() {
            GetParser("a+b").scalarexpr();
            GetParser("a+b*c").scalarexpr();
            ...
        }

However, this test will not work - it throws an exception about an "unexpected EOF". Why? Well, the grammar knows that scalar expressions can never be at the very end of an input file. So, it treats an EOF right after a scalar expression as an error. We can repair this by providing enough following symbols for the LL parser so that it remains happy. In our case, a semicolon suffices, as each constraint - which might have a scalar expressions at the end - is terminated by that symbol. So, the following actually tests parsing of scalar expressions:

        [Test]
        public void TestJustParse_scalarexpr() {
            GetParser("a+b ;").scalarexpr();
            GetParser("a+b*c ;").scalarexpr();
            GetParser("(a-b)*c ;").scalarexpr();
            GetParser("a+b*-c*d ;").scalarexpr();
            GetParser("(a-b*c)/-(-d) ;").scalarexpr();
            GetParser(".i(a+2*b) ;").scalarexpr();
            GetParser("(3+.d(-a-b*2))-d ;").scalarexpr();
            GetParser("-.a([0,1],[1,1]) ;").scalarexpr();
            GetParser("_+_ ;").scalarexpr();
            GetParser("3*.t ;").scalarexpr();
            GetParser("(.iv-.t)/2 ;").scalarexpr();
        }

Next, we have to check that the correct model is returned - and here we get a problem: How do we compare two models? There are two possibilities: We can either compare each property, or we implement an Equals method in each class. As we might want to compare model parts also later (e.g. to find out that a constraint is superfluous; or for constant folding), I opt for the Equals methods. So each expression (at least) now gets three methods - here is an example for class BinaryScalarExpr:

        public bool Equals(BinaryScalarExpr obj) {
            return Equals((object)obj);
        }
        public override bool Equals(object obj) {
            BinaryScalarExpr o = obj as BinaryScalarExpr;
            return o != null
                && o._lhs.Equals(_lhs)
                && o._operator.Equals(_operator)
                && o._rhs.Equals(_rhs);
        }
        public override int GetHashCode() {
            return _lhs.GetHashCode() + (int)_operator + _rhs.GetHashCode();
        }

The tests I write look like this. They are a sort of mixture between single-purpose tests (where you would put only exactly one operator or the like into an expression) and integrating tests - and are quite boring to write ...

    ScalarExpr x = CreateParserForSyntacticTests("a+b*-c/d ;").scalarexpr();
    Assert.AreEqual(
        new BinaryScalarExpr(new ScalarVariable("a"),
            BinaryScalarOperator.PLUS,
            new BinaryScalarExpr(
                new BinaryScalarExpr(new ScalarVariable("b"),
                    BinaryScalarOperator.TIMES,
                    new UnaryScalarExpr(UnaryScalarOperator.MINUS,
                        new ScalarVariable("c")
                    )
                ),
                BinaryScalarOperator.DIVIDE,
                new ScalarVariable("d")
            )
        ), x);

I wrote about 20 of them up to now - that should suffice for my enterprise. If I find errors, I'll add more. Tests for the five specific items mentioned at the beginning are still missing, I confess.

Remark: During testing I found -  I think - some quite serious restrictions in my grammar. For example, I cannot write 2*[0,1] for a multiplication of a scalar and a vector, much less a*[b,c]. I'll leave it at that right now:
  • Either one can work around the restrictions - this is an animation program, not a generic mathematical equation solver;
  • or I introduce the {...} braces for vector expressions (if that helps);
  • or I find some other clever way ...
UPDATE: I have now written some more test cases, especially for 4 of the 5 special issues mentioned at the beginning. Still missing are tests for correct (and failing) image loading - I'll push them to later times.

No comments:

Post a Comment