Wrapping libfswatch for a test-running prototype

First off, I'd like to get some acknowledgments out of the way. Jérémy Korwin greatly inspire this post with his post: Code Kata with srfi-64 and @flat on the Scheme Discord who helped me overcome some obstacles using the FFI when it came to extracting data from an array of structs.


Jérémy's article, and more specifically, his code uses the watch utility the commonly ships on Linux to run a guile command in a folder on an interval. The goal of my project was to use libfswatch to:

  • Locate and pair files to the their tests
  • Run tests on a file change

The library makes the following assumptions (though I intend to provide customizability here in future iterations): your tests live in a ./tests/ folder in the project root and your tests end with .test.ss. If the library finds the matching test for your file or your file is a test itself, a save triggers a re-execution of the test.

An gif of the test-runner in action

FFI Learnings

In my opinion, this is a great bit of follow-up code to the previous post on Chez's FFI.

It expands on concepts not previously covered by providing a practical example of using a callback to run scheme code off a C event as well as diving deeper into extracting data out of more complex data structures.

The callback scaffolding should be familiar to those who have read the FFI documentation:

(define callback
    (let ([code (foreign-callable __collect_safe p (FSW_HANDLE int) FSW_STATUS)])
      (lock-object code)
      (foreign-callable-entry-point code))))

As you can see what it returns is the entry point itself; the FSW_HANDLE and int reflecting the signature of C callback.

What's handed to the callback is the handle to the memory, which is an array of Events, as well as the number of events.

The following ftype-struct defines the shape of each event:

(define-ftype fsw_cevent (struct
                          [path (* char)]
                          [evt_time long]
                          [flags (* int)]
                          [flags_num unsigned-int]))

The on-update function does a bulk of the heavy-lifting:

(define on-update
   (lambda (events-pointer event-num)
     (let ([events (make-ftype-pointer fsw_cevent events-pointer)])
       (let process-event ([i 0])
         (when (< i event-num)
           (let* ([event-time (ftype-ref fsw_cevent (evt_time) events i)]
                  [path (let buildname ([chars '()]
                                        [offset 0])
                          (let ([char (ftype-ref fsw_cevent (path offset) events i)])
                            (if (eq? char #\nul)
                                (list->string (reverse! chars))
                                (buildname (cons char chars) (+ 1 offset)))))]
                  [flags-num (ftype-ref fsw_cevent (flags_num) events i)]
                  [flags-list (let build-flags-list ([fl '()]
                                                     [j 0])
                                (if (< j flags-num)
                                     (cons (ftype-ref fsw_cevent (flags j) events i)
                                     (+ 1 j))
             (if (or (memq FSW_UPDATE_FLAG flags-list)
                     (memq FSW_CREATE_FLAG flags-list))
                 (handle-event path))
             (process-event (+ 1 i))))))

Here, at the top lambda, you can see the events-pointer and the event-num being made available.

Next, (make-ftype-pointer fsw_cevent events-pointer) takes the chunk of data and makes it a structure Chez can understand.

The first real big hangup for me occurred when iterating over the events themselves, I had originally stored each event as its own binding in a named let. Iterating through each one, offsetting from each as if it was the root. This meant the first piece of data in the array of structs worked, the path field, but the rest were garbage. The ftype-ref offset for each field had to be passed the offset from the root and the events object from the root for Chez to keep the right reference.

With all event and flag data properly mapped, it simply became a matter of passing the path of the matching file to handle-event for further processing and execution of the tests themselves.

The code:

You can check out the repo here: https://github.com/vidjuheffex/job/

Left to do:

  • I want to extend this to accept a config scheme script for options like:
  • tests stored next to their files
  • enabled/disabling recursive dir searching
  • setting extension type for tests