So I've long wanted my unit test library (on the off chance someone hasn't seen it yet: https://bitbucket.org/zck/unit-test.arc) to delay macroexpansion until the unit test is actually ran. This is useful for several reasons: mainly that you can change the definition of a macro and be able to simply re-run the test, but also so that you can write tests before you define the macros they use. Fallintothis was good enough to spend a ton of time teaching me about some intricacies of macros (https://bitbucket.org/zck/unit-test.arc/issue/10/if-a-macro-is-redefined-any-existing-tests), and I came away thinking it would be basically impossible to delay macroexpansion time. Then I came across lisp-unit: http://www.cliki.net/lisp-unit . Dang, it does exactly what I want. How? It stores the body of a test as a list of forms, then passes that list to run-code: (defun run-code (code)
"Run the code to test the assertions."
(funcall (coerce `(lambda () ,@code) 'function)))
Code from https://github.com/OdonataResearchLLC/lisp-unit/blob/master/lisp-unit.lisp#L646And it works! [1]> (defun run-code (code)
"Run the code to test the assertions."
(funcall (coerce `(lambda () ,@code) 'function)))
RUN-CODE
[2]> (run-code '((test-macro)))
*** - EVAL: undefined function TEST-MACRO
The following restarts are available:
USE-VALUE :R1 Input a value to be used instead of (FDEFINITION 'TEST-MACRO).
RETRY :R2 Retry
STORE-VALUE :R3 Input a new value for (FDEFINITION 'TEST-MACRO).
ABORT :R4 Abort main loop
Break 1 [3]> abort
[4]> (defmacro test-macro () ''value)
TEST-MACRO
[5]> (run-code '((test-macro)))
VALUE
Wow, pretty cool. Can we do this in arc? We run into a problem coercing a list to a function; it errors. Check out the definition of ar-coerce in ac.scm for why. arc> (coerce '(lambda () 3) 'fn)
Error: "Can't coerce (lambda nil 3 . nil) fn"
Ugh, so that seems to be out. But...what about eval? arc> (def run-code (code)
(eval `(do ,@code)))
#<procedure: run-code>
arc> (run-code '((test-macro)))
Error: "_test-macro: undefined;\n cannot reference undefined identifier"
arc> (mac test-macro () (prn "macroexpansion time!") ''value)
#(tagged mac #<procedure: test-macro>)
arc> (run-code '((test-macro)))
macroexpansion time!
value
Well, I'll be damned. This seems to work, and it delays macroexpansion time. It was especially easy to add this to unit-test.arc: https://bitbucket.org/zck/unit-test.arc/commits/8a0d5eb9a3a30600c3739f6d8dbc48e85c91506aAnd now macroexpansion time is when we run the tests! arc> (suite macroexpansion-test only-test (assert-same 'return (my-test-macro)))
Successfully created suite macroexpansion-test with 1 test and 0 nested suites.
#hash()
arc> (run-suite macroexpansion-test)
Running suite macroexpansion-test
macroexpansion-test.only-test failed: _my-test-macro: undefined;
cannot reference undefined identifier
In suite macroexpansion-test, 0 of 1 tests passed.
Oh dear, 1 of 1 failed.
nil
arc> (mac my-test-macro () (prn "macroexpansion time!") ''return)
#(tagged mac #<procedure: my-test-macro>)
arc> (run-suite macroexpansion-test)
Running suite macroexpansion-test
macroexpansion time!
In suite macroexpansion-test, all 1 tests passed!
Yay! The single test passed!
nil
Are there any downsides to this? Because of the way setup is done, setup also works fine (code elided; this is gonna be long enough without it). I'm somewhat wary to put in an eval, but I don't quite see the harm here. I know it's somewhat of a code smell, but none of the reasons that eval is bad seem to apply here^1 . I'd love any feedback you have to offer, or to point out any problems I'll run into.[1] To present an honest attempt to understand the reasons eval is bad, and why I think they don't apply: 1. Eval is slow. To the small amounts of code that will be eval'd, this seems like premature optimization. This is especially true since I don't see another way of getting this feature. 2. Eval'd code can't refer to variables lexically bound. Tests should not be written this way. If you need to refer to variables outside the body of the test you write, suite-w/setup should be used. And that works fine. 3. It makes code confusing. Again, there's no other way of doing this, to my knowledge. And it's not much more confusing than the code already was. |