RLisp unit testing framework is based on pattern matching. Its core is assert
macro which defines a domain specific language for testing:
(require "rlunit.rl")
(test-suite RLUnitExample
(test example
(assert 9 == (+ 2 7))
(assert 2 == (- 6 4))
(assert 2 == (- 6 4) msg: "Subtraction should work")
(assert nil? nil)
(assert 1.99 == 2.02 delta: 0.05 msg: "Deviation is too high")
(assert '() instance_of? Array)
(assert '(foo bar) instance_of? Array)
(assert '(foo bar) kind_of? Enumerable)
(assert () != nil)
(assert #f same: false)
(assert #t same: true)
)
)
The framework had just one tiny problem. In Fabien's words:What you'd need now is a way to dynamically extend your matches with additional patterns, so that you can plug new kinds of tests without hacking the framework's sources.And that's pretty much what I did. Patterns are kept in hash table
pattern-macros-db
. pattern-macro-create
creates a new macro, pattern-macro-extend
adds new patterns (prepended to the list), and pattern-macro-recompile
recompiles the macro.(let pattern-macros-db (hash))
(defmacro pattern-macro-recompile (name)
`(defmacro ,name args
(match args ,@[pattern-macros-db get name])
)
)
(defmacro pattern-macro-create (name . patterns)
`[pattern-macros-db set ',name ',patterns]
)
(defmacro pattern-macro-extend (name . patterns)
`[pattern-macros-db set ',name
[',patterns + [pattern-macros-db get ',name]]
]
)
Now the definition of assert
macro looks like:(pattern-macro-create assert
('nil? a) `[self assert_nil ,a]
('nil? a 'msg: msg) `[self assert_nil ,a ,msg]
(a '=~ b) `[self assert_match ,b ,a]
(a '=~ b 'msg: msg) `[self assert_match ,b ,a ,msg]
(a '!~ b) `[self assert_no_match ,b ,a]
(a '!~ b 'msg: msg) `[self assert_no_match ,b ,a ,msg]
(a '!= b) `[self assert_not_equal ,b ,a]
(a '!= b 'msg: msg) `[self assert_not_equal ,b ,a ,msg]
(a 'same: b) `[self assert_same ,b ,a]
(a 'same: b 'msg: msg) `[self assert_same ,b ,a ,msg]
(a 'not_same: b) `[self assert_not_same ,b ,a]
(a 'not_same: b 'msg: msg) `[self assert_not_same ,b ,a ,msg]
(a 'kind_of? b) `[self assert_kind_of ,b ,a]
(a 'kind_of? b 'msg: msg) `[self assert_kind_of ,b ,a ,msg]
(a 'instance_of? b) `[self assert_instance_of ,b ,a]
(a 'instance_of? b msg: msg) `[self assert_instance_of ,b ,a ,msg]
('block: blk) `[self assert_block & (fn () ,@blk)]
('block: blk 'msg: msg) `[self assert_block ,msg & (fn () ,@blk)]
('msg: msg 'block: blk) `[self assert_block ,msg & (fn () ,@blk)]
(a '== b 'delta: c) `[self assert_in_delta ,b ,a ,c]
(a '== b 'delta: c
'msg: msg) `[self assert_in_delta ,b ,a ,c ,msg]
(a '== b 'msg: msg
'delta: c) `[self assert_in_delta ,b ,a ,c ,msg]
(a '== b) `[self assert_equal ,b ,a]
(a '== b 'msg: msg) `[self assert_equal ,b ,a ,msg]
(a 'macroexpands: b) `(assert (macroexpand-rec ',a) == ',b)
(a 'macroexpands: b
'msg: msg) `(assert (macroexpand-rec ',a) == ',b msg: ,msg)
(a b) `[self assert_equal ,b ,a]
(a b 'msg: msg) `[self assert_equal ,b ,a ,msg]
(a) `[self assert ,a]
(a 'msg: msg) `[self assert ,a ,msg]
(raise SyntaxError [(cons 'assert args) inspect_lisp])
)
(pattern-macro-recompile assert)
And it can be extended trivially, also allowing recursive definitions:
(pattern-macro-extend assert
(rlisp 'syntax: ruby) `(assert ,rlisp == (ruby-eval ,ruby))
(rlisp 'not_syntax: ruby) `(assert ,rlisp != (ruby-eval ,ruby))
)
(pattern-macro-recompile assert)
(test-suite Syntax
(test true_class
(assert true syntax: "true")
(assert #t syntax: "true")
(assert 't not_syntax: "true")
)
(test false_class
(assert false syntax: "false")
(assert #f syntax: "false")
(assert nil not_syntax: "false")
(assert () not_syntax: "false")
)
(test nil_class
(assert nil syntax: "nil")
(assert () not_syntax: "nil")
)
; ...
)
Explicit calls to (pattern-macro-recompile ...)
are required.RLisp processes files one global statement at time. So
(a) (b)
is processed:- Macroexpand
(a)
- Execute
(a)
- Macroexpand
(b)
- Execute
(b)
(do (a) (b))
is processed:- Macroexpand
(a)
- Macroexpand
(b)
- Execute
(a)
- Execute
(b)
(a)
affects macroexpansion of (b)
. What would be the case if one macro tried to expand to (do (update pattern-macros-db ...) (pattern-macros-recompile ...))
- the recompilation would be macroexpanded before pattern-macros-db
would get updated. eval
would of course work, as would some other tricks, but I think explicit recompilation is not that bad.
1 comment:
Nice; have you seen this paper on Active Patterns in F#:
http://blogs.msdn.com/dsyme/attachment/2044281.ashx
They also allow to extend their pattern matching capabilities.
Post a Comment