ta0kira / zeolite

Zeolite is a statically-typed, general-purpose programming language.
Apache License 2.0
18 stars 0 forks source link

Make it easier to write unit tests. #74

Closed ta0kira closed 3 years ago

ta0kira commented 4 years ago

For example:

ta0kira commented 4 years ago

It might also be useful to add a pragma to the compiler to collect a bunch of procedures to execute.

testcase "all tests" {
  success $RunAllTests$
}

concrete Tests {
  @type test1 () -> ()
  @type test2 () -> ()
}

define Tests {
  test1 () { $SingleTest$
    // ...
  }

  test2 () { $SingleTest$
    // ...
  }
}

The semantics are unclear, however. The main point would be to compile them all into a single binary to cut down on compilation time, but dealing with reporting and allowing multiple failures would require some design.

Collection of test procedures would also be a little messy; especially since the ProcedureContext would need to know if the procedure is actually a part of a testcase.

ta0kira commented 3 years ago

An alternative macro/pragma approach:

testcase "all tests" {
  success Tests.run()
}

concrete Tests {
  @type run () -> ()
}

define Tests {
  run () {
    $RunAllTests$
  }

  $UnitTest[test1]$
  test1 () {
    // ...
  }

  $UnitTest[test2]$
  test2 () {
    // ...
  }
}

This way, the $UnitTest$ label won't be visible outside of Tests.

For this to allow multiple failures, however, there will need to be a way to accumulate them, i.e., allow a single test to fail without using fail. This means that there will need to be an additional "failure" macro that logs the error and returns from the test.

Additionally, for that to be feasible within a library (e.g., a regex matcher), there would need to be some sort of exception semantics to short-circuit the rest of the test, so that the caller doesn't need if (!Helpers.test()) { return _ } every time a test helper is called.

Maybe it doesn't need to be full exception semantics; maybe just a special return type could be used.

concrete Helpers {
  @type regexMatch (String,String) -> $TestResult$
}

define Helpers {
  regexMatch (string,regex) {
    if (!Regex.matches(string,regex)) {
      $TestFail[string + " does not match regex " + regex]$
    }
  }
}

Then only allow Helpers.regexMatch to be called in $UnitTest$ or other $TestResult$ functions. Calls to such functions can generate C++ that short-circuits if the call results in a failure. There doesn't necessarily need to even be a return value; it could be done with a ThreadCapture at the top of the $UnitTest$, to avoid boxing and unboxing a bool.

This seems like a hack to sneak in exceptions for unit tests without actually supporting exceptions more generally. The main things that are lacking here are arbitrary return types, user-defined exception types, and the ability to handle exceptions.

ta0kira commented 3 years ago

The overall objective is performance; it takes extremely long to compile the generated C++ for each individual test case, and combined tests (e.g., those in lib/math) currently cannot have multiple failures.

One possible solution for this is to have runSingleTest execute a single test binary multiple times; once per unit test. This is slower than executing every test in one call to the binary, but it will allow tests and helpers to still use fail.

To further avoid errors and ambiguities when unittest is used with success Test.run(), maybe success and crash shouldn't have an expression argument. Maybe it should also be an error for a testcase to have no unittest.

ta0kira commented 3 years ago

Closing the loop on the original plan: