Quantcast
Channel: Cadence Blogs
Viewing all articles
Browse latest Browse all 6681

SKILL for the Skilled: Simple Testing Macros

$
0
0
In this post I want to look at an easy way to write simple self-testing code. This includes using the SKILL built-in assert macro and a few other macros which you can derive from it.

The assert macro

This new macro, assert, was added to SKILL in SKILL version 32. You can find out which version of SKILL you are using with the SKILL functiongetSkillVersion, which returns a string such as"SKILL32.00" or "SKILL33.00".

Using this macro is simple. In its simplest form, you wrap any single expression within (assert ...). At evaluation time this will trigger an error if the expression evaluates to nil.

CIW> (assert 1 + 1 == 2)nil

CIW> (assert 2 + 2 == 5)*Error* ASSERT FAILED: ((2 + 2) == 5)<<< Stack Trace >>>
error("ASSERT FAILED: %L\n" '((2 + 2) == 5))
unless(((2 + 2) == 5) error("ASSERT FAILED: %L\n" '(& == 5)))

You can also specify the error message using printf-style arguments.

CIW> (defun testit (x "n")
       (assert x > 3 "expecting x > 3, not %L" x)
       x-3)

CIW> (testit 12)9

CIW> (testit 2)*Error* expecting x > 3, not 2<<< Stack Trace >>>
error("expecting x > 3, not %L" x)
unless((x > 3) error("expecting x > 3, not %L" x))
testit(2)

What is a macro?

Macros are a feature in many lisps including emacs lisp, common lisp, and SKILL. Consequently, you can find tons of information on the Internet explaining lisp macros. A quick Google search for "lisp macro" returns pages of useful results.

In particular, a SKILL macro is a special type of function which computes and returns another piece of SKILL code in the form of an s-expression. This capability allows SKILL programs to write SKILL programs as a function of their raw, unevaluated, operands at the call-site. Although macros can certainly be abused, when used correctly SKILL macros can greatly enhance readability by abstracting away certain details, or by automating repetitive patterns.

You can find out more about SKILL macros by consulting the Cadence documentation. There is also an Advanced SKILL Programming class which Cadence Educational Services offers.

In-line tests

You can use assert inside your functions for run-time checks. You can also use assert at the top level of your SKILL files for load-time checks. This has an added advantage of helping the person who reads your code to understand how the function you are defining is used. In the following example anyone who reads your function definition can immediately see some examples of how to use the function.
;; Sort a list of strings case independently into alphabetical order.
(defun sort_case_independent (words "l")
  (sort words (lambda (w1 w2)
                (alphalessp (lowerCase w1)
                            (lowerCase w2)))))

(assert nil 
        ; works for empty list?
        == (sort_case_independent nil)) 

(assert '("a"); works for singleton list?
         ==  (sort_case_independent '("a")))) 

(assert '("a" "B")
         == (sort_case_independent '("B" "a")))

(assert '("a" "B")
         == (sort_case_independent '("a" "B")))

(assert '("A" "b")
        == (sort_case_independent '("b" "A")))

(assert '("A" "b")
        == (sort_case_independent '("A" "b")))

(assert '("A" "b" "c" "D")
        == (sort_case_independent '("c" "D" "A" "b")))
Writing your SKILL files to include these top-level assertions has yet another advantage: if someone later modifies your function, sort_case_independent, the tests will run the next time anyone loads the file. This means if an error has been introduced in the function, some sanity testing happens at load time. Furthermore, if someone enhances the function in a way that breaks backward compatibility, the assertion will fail at load time.

Defining assert if it is missing

If you are using a version of Virtuoso, Allegro, etc, which does not contain a definition of assert, you can define it yourself.
(unless (isCallable 'assert)
  (defmacro assert (expression @rest printf_style_args) 
    (if printf_style_args 
        `(unless ,expression
           (error ,@printf_style_args))
        `(unless ,expression
           (error "ASSERTION FAILED: %L\n" ',expression)))))

The assert_test macro

Some unit testing frameworks supply assertion functions such as assert_less, assert_greater, assert_equal, assert_not_equal. It is possible in SKILL to define a single assertion macro called, assert_test, which provides all these capabilities in one. You don't really need assert_equal, assert_not_equal, asset_lessp etc...

This macro is useful for building test cases. This macro attempts to output a helpful message if the assertion fails. The message includes the parameters to the testing expression, and the values they evaluate to. For example:

CIW> A = 33

CIW> B = 22

CIW> (assert_test A+B == B+2) *Error* (A + B)
  --> 3
(B + 2)
  --> 4
FAILED ASSERTION: ((A + B) == (B + 2))<<< Stack Trace >>>
...
CIW> (assert_test A+B > B+2)
*Error* (A + B)
  --> 3
(B + 2)
  --> 4
FAILED ASSERTION: ((A + B) > (B + 2))<<< Stack Trace >>>
...

The intelligent thing about assert_test as can be seen from the above example, is that it constructs an error message which tells you the text of the assertion that failed: ((A + B) == (B + 2)). It also tells you the arguments to the testing function in both raw and evaluated form: (A + B) --> 3 and (B + 2) --> 4

The macro definition is not trivial, but the code is given here. You don't really need to understand how it works in order to use it.

;; ARGUMENTS:
;;   expr - an expression to evaluate, asserting that it does not return nil
;;   ?ident ident - specifies an optional identifier which will be printed with [%L] in
;;                     the output if the assertion fails.  This will help you identify the
;;                     exact assertion that failed when scanning a testing log file.
;;   printf_style_args - additional printed information which will be output if the
;;                     assertion fails.
(defmacro assert_test (expr @key ident @rest printf_style_args)
  (if (atom expr)
      `(assert ,expr)
      (let ((extra (if printf_style_args
                       `(strcat "\n" (sprintf nil ,@printf_style_args))"")))
        (destructuringBind (operator @rest operands) expr
          (letseq ((vars (foreach mapcar _operand operands
                           (gensym)))
                   (bindings (foreach mapcar (var operand) vars operands
                               (list var operand)))
                   (assertion `(,operator ,@vars))
                   (errors (foreach mapcar (var operand) vars operands
                             `(sprintf nil "%L\n  --> %L" ',operand ,var))))
            `(let ,bindings
               (unless ,assertion
                 (error "%s%s%s"
                        (if ',ident
                            (sprintf nil "[%L] " ,ident)"")
                        (buildString (list ,@errors
                                           (sprintf nil "FAILED ASSERTION: %L" ',expr))"\n")
                        ,extra))))))))

The assert_fails macro

With the assertion macros presented above you can pretty robustly make assertions about the return values of functions. A limitation, however, is you cannot easily make assertions about the corner cases where your function triggers an error.

The following macro,assert_fails, provides the ability to assert that an expression triggers an error. For example, thesort_case_independent function defined above will fail, triggering an error, if given a list containing a non-string.

CIW> (sort_case_independent '("a" "b" 42 "c" "d"))*Error* lowerCase: argument #1 should be either a string or a symbol (type template = "S") at line 112 of file "*ciwInPort*" - 42<<< Stack Trace >>>
lowerCase(w2)
alphalessp(lowerCase(w1) lowerCase(w2))
funobj@0x2cac49a8("a" 42)
sort(words lambda((w1 w2) alphalessp(lowerCase(w1) lowerCase(w2))))
sort_case_independent('("a" "b"))

You could fix this by enhancing the function to do something reasonable in such a situation. Or you could simply document the limitation, in which case you might want to extend the in-line test cases as well.

(assert_fails (sort_case_independent '("a" "b" 42 "c" "d")))

Here is an implementation of such a assert_fails macro.

(defmacro assert_fails (expression)
  `(assert (not (errset ,expression))"EXPECTING FAILURE: %L\n"',expression))

Summary

In this article we looked at the assert macro, which is probably in the version of Virtuoso or Allegro you are using, and if not you can easily define it yourself. We also looked at assert_test and assert_fails which you can define yourself. You can use these three macros to easily improve the robustness of your SKILL programs.

See Also


Viewing all articles
Browse latest Browse all 6681