Using syntax/loc and unit test macros

:: Racket, racket-cookbook, macros

In my previous post, I wrote about a nuance with syntax/loc, using the example of a macro that both defines and provides a function. But why don’t I back up, and look at a simpler example of why you’d want to use syntax/loc. The example is a simple macro you might often find yourself wanting, to reduce the tedium of writing unit test cases.

Let’s say we have a function my-function-that-increments, with some unit tests.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#lang racket

(require rackunit)

(define (my-function-that-increments x)
  (add1 x))

(check-equal? (my-function-that-increments 0)
              1)
(check-equal? (my-function-that-increments 1)
              1)

The last test correctly fails1, and the rackunit error message points to the check-equal? on line 10:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
--------------------
FAILURE
actual:     2
expected:   1
name:       check-equal?
location:   (#<path:/tmp/bp.rkt> 10 0 151 62)
expression: (check-equal? (my-function-that-increments 1) 1)

; Check failure
--------------------

Great. But let’s say we have dozens of tests, and typing all the check-equal? and my-function-that-increments is wearisome. We think, “Aha, I can write a macro!” So we write:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#lang racket

(require rackunit)

(define (my-function-that-increments x)
  (add1 x))

(define-syntax (chk stx)
  (syntax-case stx ()
    [(_ input expected)
     #'(check-equal? (my-function-that-increments input)
                     expected)]))

(chk 1 0)
(chk 2 1)

Much less tedious. Nice.

But…hey wait. The rackunit error message points to line 11:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
--------------------
FAILURE
actual:     3
expected:   1
name:       check-equal?
location:   (#<path:/tmp/bp.rkt> 11 7 166 80)
expression: (check-equal? (my-function-that-increments 2) 1)

; Check failure
--------------------

Line 11 isn’t where the failing (chk 2 1) is. It’s inside our macro. Gah. We wanted to write this macro because we have dozens of unit tests…but we can’t see which one of them failed? This whole idea of using a macro seems to have backfired.

Fortunately, this is where syntax/loc helps. It lets us specify the source location for the syntax returned from our macro.

The macro above uses #', which is shorthand for syntax. First let’s rewrite the macro using syntax:

1
2
3
4
5
(define-syntax (chk stx)
  (syntax-case stx ()
    [(_ input expected)
     (syntax (check-equal? (my-function-that-increments input)
                           expected))]))

Of course this still has the source location problem. But, we change syntax to syntax/loc, supplying it the stx given to our macro:

1
2
3
4
5
6
(define-syntax (chk stx)
  (syntax-case stx ()
    [(_ input expected)
     (syntax/loc stx
       (check-equal? (my-function-that-increments input)
                     expected))]))

Here’s the full new sample, so that the line numbers work for this blog post:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#lang racket

(require rackunit)

(define (my-function-that-increments x)
  (add1 x))

(define-syntax (chk stx)
  (syntax-case stx ()
    [(_ input expected)
     (syntax/loc stx
       (check-equal? (my-function-that-increments input)
                     expected))]))

(chk 1 0)
(chk 2 1)

And now rackunit has the correct source location to report, line 16:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
--------------------
FAILURE
actual:     3
expected:   1
name:       check-equal?
location:   (#<path:/tmp/bp.rkt> 16 0 281 9)
expression: (check-equal? (my-function-that-increments 2) 1)

; Check failure
--------------------

Hopefully this shows how syntax/loc can help with the sort of “casual” macro you might frequently want to write.

  1. For some definition of “correctly”.