From a6c1f15a7beb96287187ac6228ff4d72f1dfb07a Mon Sep 17 00:00:00 2001 From: Craig Oates Date: Fri, 2 Jul 2021 17:46:32 +0100 Subject: [PATCH] Squashed commit of the following: commit d6f93b022d84d3247bbed15b94f167ffa6b537bc Author: Craig Oates Date: Fri Jul 2 17:36:52 2021 +0100 add tests which validate the /status/latest call. The URL which returns the latest status for all the devices at once is validated. The tests are not extensive. They just check to make sure all the intended device are present and the last device on the list is 'device 6'. If any of the device names change or 'device 6' is not the last device, it means the API has changed. commit 75d47481503a0fba6bd6450f62e9f5eb27e9dc00 Author: Craig Oates Date: Fri Jul 2 16:51:59 2021 +0100 rename light meter status tests (factory devices). Rename the tests to make it easier to read and make the naming convention align with the status tests for the gallery devices. commit 4d8a1e6f9b851faec684fd499281ef3bbcc4d325 Author: Craig Oates Date: Fri Jul 2 16:43:47 2021 +0100 correct 'reading' tests to 'status' tests and add new tests. I managed to not notice I wrote tests which check the status (on/off) of the devices but labelled the test as 'reading data' checks. I've relabelled the tests to avoid the confusion. I've added additional test which validate the data produced by the devices in the gallery (part of the Return to Ritherdon project). And, I've written tests which actually validate the light reading data produced by the devices in the welding booths in Ritherdon (again, part of the Return to Ritherdon project). The new tests include code from the Ratify package. I've only used the datetime-p function for now. It checks if the timestamps produced are valid datetime strings -- and can be parsed as such. commit c9559f0a670c94bef9ff464e619997769d9c954c Author: Craig Oates Date: Fri Jul 2 16:41:18 2021 +0100 add Ratify to the testing system. At the moment, this is test the timestamps returned from the API. If Ratify can parse the timestamps, it returns them. If it can't parse them, it returns NIL. commit 2ebd6d2a0f1cfddee5e7869dcfd6077b1864c692 Author: Craig Oates Date: Fri Jul 2 00:45:36 2021 +0100 writing tests for validating light reading data. These tests check the shape of the data produced by the light meters in the welding in booths in Ritherdon. I've place a note with the factory3 tests explaining the reason why there is a factory3 in the system but there is not actual light meter in the third welding booth in Ritherdon. commit 77952ad2945ee6d4a8a49765d8eff80a153e051f Author: Craig Oates Date: Fri Jul 2 00:40:35 2021 +0100 ASDF package definition changes. The changes here are done out of frustration and desperation. SLIME was in a right mess (caching issue I assume) so I started changing things in here to try and fix it. This was before I stumbled on the 'restart Emacs/SLIIME' solution. Becuase it's now working, I've just kept the changes in place. The relief of getting 'asdf:test-system' working after about two hours of faffing was enough for me to not touch this file again. I might come back to this at a later date -- see if/how it needs cleaning up. I might just skip it and start a new project -- this is a learning exercise after all. commit d11119bfdc6425b141f1e6d7fc3f44246b4bd14d Author: Craig Oates Date: Fri Jul 2 00:37:31 2021 +0100 export additional test function (at package level). This is so you can run the tests with ASDF, I think. It was something I did whilst SLIME was a bit messes-up and I was a bit desperate with trying to fix it. So, I don't know if this additional exported function is needed. Everything seems to work with it still there -- after I restarted Emacs and SLIME -- so I'm keeping it like this for now. commit b055b97e2a5d82e8723362f37b21feebfcd9c544 Author: Craig Oates Date: Fri Jul 2 00:32:52 2021 +0100 rename tests file to main.lisp. This was not needed in the end. I was having trouble with SLIME and I was getting desperate at one point and starting changing everything. It turns out I just needed to restart SLIME. At the time of writing, I didn't know this or how to do that so I only noticed when I restarted Emacs. It looks like stuff got messed-up in the cache and SLIME couldn't find files/packages because it was looking for old one which were either renames or deleted (due to me frantically changing things). commit 8c87cf5b37aa88405c6e12e4c543b9475c76145b Author: Craig Oates Date: Wed Jun 30 01:44:45 2021 +0100 fix typo in .gitignore (.directory) Git is picking up my systems .directory files. They are not part of this project's code base. I accidently put /.directory in a previous commit instead of .directory. commit 09dd5f9b6e37872f9afb51e6574cf9be7fcba03f Author: Craig Oates Date: Wed Jun 30 01:40:49 2021 +0100 add functions and comments for making HTTP-Requests. These are comments and functions to get me into the swing of writting Common Lisp. The code here is the main part of the system but this commit is mostly about how this file and its code fits into the bigger picture which is the system/code base. commit e912fdc73d56b2e9ee9ad3b45cb7c0e94ab69477 Author: Craig Oates Date: Wed Jun 30 01:38:36 2021 +0100 add drakma and cl-json packages to project. These packages are for making an HTTP-Request (to Ritherdon REST API) and parse the JSON returned. commit 05b876bea755d3097d52695576e171b27b778f72 Author: Craig Oates Date: Tue Jun 29 21:38:48 2021 +0100 add 'hello' function (for quick test after loading package). This is so I can make sure I know ritherdon-rest.lisp can/is loaded correctly -- via Quicklisp or ASDF -- and is ready to start adding the actual code. commit 8f8cd147ce179886c9b56b3f1c9025d5cbc1a511 Author: Craig Oates Date: Tue Jun 29 21:34:14 2021 +0100 integrate FiveAM into test package def. and main testing file. This builds on the initial set-up in the .asd file. With the .asd file knowing the tests package needs FiveAM, the code here integrates the testing framework in to the .lisp files responsible for housing the tests. The code here is placeholder tests and should be deleted the more I get into the project. They exist just to make sure everything is set-up properly between the various definition/set-up files. commit 943a912d9658c66e86f5ca53ae9936a182ade0d3 Author: Craig Oates Date: Tue Jun 29 21:32:11 2021 +0100 connect fiveAM testing package to main project in .asd file. This is so you can use asdf:test-system by just calling the ritherdon-rest project -- making it easier to work with. commit 26b025361d0838f1df50737af1e1f7ea0feec0f0 Author: Craig Oates Date: Tue Jun 29 21:31:11 2021 +0100 remove doc folder. This is beyond where I'm at right now so going to leave it for a future project. commit aa1abec7fe0aec0fdc371c28c669da8ae0efe65c Author: Craig Oates Date: Tue Jun 29 00:22:44 2021 +0100 add fiveAM package and create initial test. I've changed how the tests and doc systems are defined in the .asd file. The changes are based on what SLIME outputted when compiling the project. The initial test are irrelevant to the project. I wrote it to make sure fiveAM (and the test project as a whole) was connected together properly. This test will (should) not remain once the main code is up and running. commit 00ba38f3b069ef2d9104fa33de61264b33fbf851 Author: Craig Oates Date: Mon Jun 28 23:11:46 2021 +0100 create project files. --- .gitignore | 1 + LICENSE | 2 +- README.md | 4 +- ritherdon-rest.asd | 34 ++++++ src/package.lisp | 7 ++ src/ritherdon-rest.lisp | 37 ++++++ tests/main.lisp | 255 ++++++++++++++++++++++++++++++++++++++++ tests/package.lisp | 9 ++ 8 files changed, 346 insertions(+), 3 deletions(-) create mode 100644 ritherdon-rest.asd create mode 100644 src/package.lisp create mode 100644 src/ritherdon-rest.lisp create mode 100644 tests/main.lisp create mode 100644 tests/package.lisp diff --git a/.gitignore b/.gitignore index bf4db1e..c4b839e 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ *.wx64fsl *.wx32fsl +*.directory diff --git a/LICENSE b/LICENSE index 204b93d..1723fbb 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -MIT License Copyright (c) +MIT License Copyright (c) 2021 Craig Oates Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 0fb9a43..445c55a 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ -# ritherdon-rest +# Ritherdon REST -A CLI program which grabs data from the Return to Ritherdon project's REST API server. \ No newline at end of file +A CLI program which grabs data from the Return to Ritherdon project's REST API server. diff --git a/ritherdon-rest.asd b/ritherdon-rest.asd new file mode 100644 index 0000000..d789c89 --- /dev/null +++ b/ritherdon-rest.asd @@ -0,0 +1,34 @@ +;;;; ritherdon-rest.asd + +(asdf:defsystem #:ritherdon-rest + :description "Grabs data from the Return to Ritherdon project REST + API and prints out the results." + :author "craig@craigoates.net" + :license "MIT" + :version "0.0.1" + :serial t + :depends-on (:drakma + :cl-json) + :pathname "src/" + :components ((:file "package") + (:file "ritherdon-rest")) + :in-order-to ((test-op (test-op :ritherdon-rest/tests)))) + +;; The use of '...rest/tests' was because of a warning when trying to +;; run the code in SLIME. +(asdf:defsystem #:ritherdon-rest/tests + :description "The test suite for the ritherdon-rest project." + :author "craig@craigoates.net" + :license "MIT" + :version "0.0.1" + :serial t + :depends-on (:ritherdon-rest + :fiveam + :ratify) + :pathname "tests/" + :components ((:file "package") + (:file "main")) + :perform (test-op (o s) + (uiop:symbol-call :ritherdon-rest-tests :test-quasi))) + + diff --git a/src/package.lisp b/src/package.lisp new file mode 100644 index 0000000..1c4cfb1 --- /dev/null +++ b/src/package.lisp @@ -0,0 +1,7 @@ +;;;; package.lisp + +(defpackage #:ritherdon-rest + (:use #:cl #:drakma :cl-json) + (:export :parse-request + :decode-json)) ; Part of cl-json, getting errors without + ; it in SLIME. diff --git a/src/ritherdon-rest.lisp b/src/ritherdon-rest.lisp new file mode 100644 index 0000000..7c5a512 --- /dev/null +++ b/src/ritherdon-rest.lisp @@ -0,0 +1,37 @@ +;;;; ritherdon-rest.lisp + +;;; A collection of functions to aid in the make +;;; http-requests to the Ritherdon REST API. This project is help me +;;; learn Common Lisp. So, there is not much emphasis on being too +;;; strict on best practices and defensive coding. + +;;; NOTE: The Ritherdon REST API is part of an art exhibition at time +;;; of writing (29/06/2021). You will need to check if the server is +;;; still running if you want to run this code. + +(in-package #:ritherdon-rest) + +;; All the data provided by the Ritherdon REST API is in JSON. Becuase +;; of this I can set the `TEXT-CONTENT-TYPE' to JSON and keep it like +;; that until further notice. +(setq drakma:*text-content-types* (cons '("application" . "json") + drakma:*text-content-types*)) + +;; The Base URL which you build all your HTTP Requests on. You will +;; need to refer back to http://ritherdon.abbether.net/api/ui/ for the +;; full set of URL's. +(defvar *base-url* "http://ritherdon.abbether.net/api") + +;; The http-request is actually 'drakma:http-request'. I've done it +;; this way to highlight the feature. It is the same as what you've +;; used in the past with C# and 'using static' statements. So, not +;; much to go into here apart from highlighting it's a thing in Common +;; Lisp, too. +(defun make-request (query) + (http-request (concatenate 'string *base-url* query))) + +;; Same as the drakma example above. It can read as +;; 'cl-json:decode-json-from-string' but omitted the 'cl-json' +;; bit. You will find all package declarations in /src/package.lisp'. +(defun parse-request (query) + (decode-json-from-string(make-request query))) diff --git a/tests/main.lisp b/tests/main.lisp new file mode 100644 index 0000000..f89874c --- /dev/null +++ b/tests/main.lisp @@ -0,0 +1,255 @@ +;;;; tests/main.lisp + +(in-package #:ritherdon-rest-tests) + +(def-suite all-tests + :description "The master suite of all ritherdon-rest tests.") + +(in-suite all-tests) + +;;; HAD TO REVERT BACK TO FULL PACKAGE NAMES +;;; ======================================== + +;;; BELOW EXPLAINS HOW YOU CAN OMIT PACKAGE NAMES FROM FUNCTION +;;; CALLS. BUT, I'VE ADDED THE 'RATIFY' PACKAGE SINCE THEN AND I WAS +;;; GETTING NAMING CONFLICTS (WITH 'TEST'). SO, I'VE HAD TO USE THE +;;; FULL 'FIVEAM:TEST' FUNCTION CALLS. I'VE KEPT THE NOT FOR FUTURE +;;; REFERENCE AND ADDED THIS AS EXTRA CONTEXT FOR WHEN/HOW TO OMIT +;;; PACKAGE NAMES. + +;;; THIS BIT KINDA OUT-OF-DATE... + +;;; These two examples show the 'full' call to the fiveAm test +;;; functions. This is just for reference. The 'namespace' is already +;;; 'imported' in 'package.lisp'. + +;;; (fiveam:test sum-1 +;;; (fiveam:is (= 3 (+ 1 2)))) + +;;; (fiveam:run!) + +;;; How you would normally create the tests -- with fiveAM already +;;; set-up in 'package.lisp' and not needing to be explicit about it +;;; here. This is similar to 'using static' in C#. + +(defun test-quasi() + (run! 'all-tests)) + +;;; REST API Used Here is Temporary +;;; =============================== +;;; This REST API is a temporary one -- it's part of an artwork for +;;; the Return to Ritherdon project. The intention is to retire the +;;; API when the exhibition ends. So, these tests might fail because +;;; the API is no longer active. + + +;;; DEVICE STATUS TESTS +;;; =================== +;;; These tests are checking the 'shape' of the data returned by the +;;; HTTP (REST) request matches what is expected. These tests are +;;; expecting JSON data which looks similar to: + +;;; ((:ID . 79) (:STATUS . "off") (:TIME . "2021-07-01T16:00:001")). + +;;; ID: The row Id. in the database. +;;; STATUS: The current power state of the device (I.E. on or off). +;;; TIME: The timestamp when the status was recorded. + +;;; Usually, these updates are logged when each device is either just +;;; finished powering up or as they are about to power down. + +(fiveam:test factory-1-status-data + :description "Validates the (status) data produced by `FACTORY1'." + (let ((data (ritherdon-rest:parse-request "/status/latest/1"))) + ;; Data should look something like: + ;; ((:ID . 79) (:STATUS . "off") (:TIME . "2021-07-01T16:00:001")). + (is (= 3 (length data))) + (is (equal t (consp data))) + (is (equal ':id (first (nth 0 data)))) + (is (equal ':status (first (nth 1 data)))) + (is (equal ':time (first (nth 2 data)))) + (is (equal t (typep (cdr (car data)) 'integer))) ; row :id number + (is (> (cdr (car data)) 0)) + (is (equal t (typep (cdr (car (cdr data))) 'string))) ; :status value + (is (equal t (or (string-equal (cdr (car (cdr data))) "off") + (string-equal (cdr (car (cdr data))) "on")))))) + +(fiveam:test factory-2-status-data + :description "Validates the (status) data produced by `FACTORY2'." + (let ((data (ritherdon-rest:parse-request "/status/latest/2"))) + ;; Data should look something like: + ;; ((:ID . 79) (:STATUS . "off") (:TIME . "2021-07-01T16:00:001")). + (is (= 3 (length data))) + (is (equal t (consp data))) + (is (equal ':id (first (nth 0 data)))) + (is (equal ':status (first (nth 1 data)))) + (is (equal ':time (first (nth 2 data)))) + (is (equal t (typep (cdr (car data)) 'integer))) ; row :id number + (is (> (cdr (car data)) 0)) + (is (equal t (typep (cdr (car (cdr data))) 'string))) ; :status value + (is (equal t (or (string-equal (cdr (car (cdr data))) "off") + (string-equal (cdr (car (cdr data))) "on")))))) + +(fiveam:test factory-3-status-data + :description "Validates the (status) data produced by `FACTORY3'." + (let ((data (ritherdon-rest:parse-request "/status/latest/3"))) + ;; Ritherdon has a third welding booth but it was not included in + ;; the art project so no light sensor was installed. Therefore, + ;; this should always return the 'default/initial/seed' data + ;; readings. + ;; Data should still take the same shape as factory1 and factory 2: + ;; ((:ID . 1) (:STATUS . "off") (:TIME . "2021-04-26T20:42:19.400868")). + (is (= 3 (length data))) + (is (equal t (consp data))) + (is (equal ':id (first (nth 0 data)))) + (is (equal ':status (first (nth 1 data)))) + (is (equal ':time (first (nth 2 data)))) + (is (equal t (typep (cdr (car data)) 'integer))) ; row :id number + (is (> (cdr (car data)) 0)) + (is (equal t (typep (cdr (car (cdr data))) 'string))) ; :status value + (is (equal t (or (string-equal (cdr (car (cdr data))) "off") + (string-equal (cdr (car (cdr data))) "on")))))) + +(fiveam:test gallery-1-status-data + :description "Validates the (status) data produced by `GALLERY1'." + ;; Data should still take the same shape as others: + ;; ((:ID . 1) (:STATUS . "off") (:TIME . "2021-04-26T20:42:19.400868")) + (let ((data (ritherdon-rest:parse-request "/status/latest/4"))) + (is (= 3 (length data))) + (is (equal t (consp data))) + (is (equal ':id (first (nth 0 data)))) + (is (equal ':status (first (nth 1 data)))) + (is (equal ':time (first (nth 2 data)))) + (is (equal t (typep (cdr (car data)) 'integer))) ; row :id number + (is (> (cdr (car data)) 0)) + (is (equal t (typep (cdr (car (cdr data))) 'string))) ; :status value + (is (equal t (or (string-equal (cdr (car (cdr data))) "off") + (string-equal (cdr (car (cdr data))) "on")))))) + +(fiveam:test gallery-2-status-data + :description "Validates the (status) data produced by `GALLERY2'." + ;; Data should still take the same shape as others: + ;; ((:ID . 1) (:STATUS . "off") (:TIME . "2021-04-26T20:42:19.400868")) + (let ((data (ritherdon-rest:parse-request "/status/latest/5"))) + (is (= 3 (length data))) + (is (equal t (consp data))) + (is (equal ':id (first (nth 0 data)))) + (is (equal ':status (first (nth 1 data)))) + (is (equal ':time (first (nth 2 data)))) + (is (equal t (typep (cdr (car data)) 'integer))) ; row :id number + (is (> (cdr (car data)) 0)) + (is (equal t (typep (cdr (car (cdr data))) 'string))) ; :status value + (is (equal t (or (string-equal (cdr (car (cdr data))) "off") + (string-equal (cdr (car (cdr data))) "on")))))) + +(fiveam:test gallery-3-status-data + :description "Validates the (status) data produced by `GALLERY3'." + ;; This corresponds to 'factory3' -- no light sensor in welding + ;; booth 3 in Ritherdon. Therefore, no data to send. Because of + ;; this, the data returned here should be the 'seed data' unless + ;; I've manually updated it as part of another test. + ;; Data should still take the same shape as others: + ;; ((:ID . 1) (:STATUS . "off") (:TIME . "2021-04-26T20:42:19.400868")). + (let ((data (ritherdon-rest:parse-request "/status/latest/6"))) + (is (= 3 (length data))) + (is (equal t (consp data))) + (is (equal ':id (first (nth 0 data)))) + (is (equal ':status (first (nth 1 data)))) + (is (equal ':time (first (nth 2 data)))) + (is (equal t (typep (cdr (car data)) 'integer))) ; row :id number + (is (> (cdr (car data)) 0)) + (is (equal t (typep (cdr (car (cdr data))) 'string))) ; :status value + (is (equal t (or (string-equal (cdr (car (cdr data))) "off") + (string-equal (cdr (car (cdr data))) "on")))))) + + +;;; LIGHT READINGS TESTS +;;; ==================== + +;;; Theses tests check the 'shape' of the light meter readings +;;; returned by the HTTP (REST) requests. They make sure the data +;;; matches what's expected. These tests are expecting JSON data along +;;; the lines of: + +;;; ((:ID . 1855109) (:READING . 16) (:TIME . "2021-07-02T13:42:16")) + +;;; ID: The row Id. in the database. +;;; READING: The amount of light recorded. +;;; TIME: The timestamp when the reading was taken. + +;;; Because of how the light meters work, they do not always take +;;; light meter readings at consistent intervals. But, both devices +;;; aim to take a reading for as long as they are on. Also, Ritherdon +;;; has three welding booths and the system has the capacity to record +;;; all three booths. The Return to Ritherdon project decided on only +;;; recording two of the booths so 'factory3' should only return it +;;; seed data -- unless I've manually updated it (for another test +;;; most likely). + +(fiveam:test factory-1-reading-data ; Had to add fiveam here because + ; of naming conflict with ratify + ; package. Comment at top of file + ; explaining further. + :description "Validates the (light reading) data produced by `FACTORY1'." + ;; ((:ID . 1855109) (:READING . 16) (:TIME . "2021-07-02T13:42:16")) + (let ((data (ritherdon-rest:parse-request "/readings/latest/1"))) + (is-true (= 3 (length data))) + (is-true (consp data)) + (is (equal ':id (caar data))) + (is (equal ':reading (caadr data))) + (is (equal ':time (caaddr data))) + (is-true (> (cdar data) 0)) ; Db Id numbers start at 0. + (is-true (typep (cdadr data) 'integer)) ; reading can be <=> 0. + ;; Returns the date-time if data could be parsed. Returns `NIL' if + ;; it could not. Don't need to check if string. If system can't + ;; parse it doesn't matter what form the date-time is in. + (is-false (equal (ratify:datetime-p (cdaddr data)) nil)))) ; time value. + +(fiveam:test factory-2-reading-data + :description "Validates the (light reading) data produced by `FACTORY2'." + ;; ((:ID . 1855109) (:READING . 16) (:TIME . "2021-07-02T13:42:16")) + (let ((data (ritherdon-rest:parse-request "/readings/latest/2"))) + (is-true (= 3 (length data))) + (is-true (consp data)) + (is (equal ':id (caar data))) + (is (equal ':reading (caadr data))) + (is (equal ':time (caaddr data))) + (is-true (> (cdar data) 0)) ; Db Id numbers start at 0. + (is-true (typep (cdadr data) 'integer)) ; reading can be <=> 0. + ;; Returns the date-time if data could be parsed. Returns `NIL' if + ;; it could not. Don't need to check if string. If system can't + ;; parse it doesn't matter what form the date-time is in. + (is-false (equal (ratify:datetime-p (cdaddr data)) nil)))) ; time value. + +(fiveam:test factory-3-reading-data + :description "Validates the (light reading) data produced by `FACTORY3'." + ;; ((:ID . 1855109) (:READING . 16) (:TIME . "2021-07-02T13:42:16")) + (let ((data (ritherdon-rest:parse-request "/readings/latest/3"))) + (is-true (= 3 (length data))) + (is-true (consp data)) + (is (equal ':id (caar data))) + (is (equal ':reading (caadr data))) + (is (equal ':time (caaddr data))) + (is-true (> (cdar data) 0)) ; Db Id numbers start at 0. + (is-true (typep (cdadr data) 'integer)) ; reading can be <=> 0. + ;; This device is not active so the seed data should be + ;; returned. This data should not be produce `NIL' when Ratify + ;; tries to parse it (not a valid timestamp). If the timestamp can + ;; be parsed it most likely means I've updated the latest reading + ;; data (manually most likely) which should result in this failing + ;; because Ratify will return the parsed timestamp and not `NIL'. + (is (equal (ratify:datetime-p (cdaddr data)) nil)))) ; time value. + +(fiveam:test all-device-status-data + :description "Validates the status data produced when all status + data for all devices is requested in the same API call." + (let ((data (ritherdon-rest:parse-request "/status/latest"))) + (is (= 6 (length data))) + (is-true (consp data)) + (is (equal ':|DEVICE 1| (car (first data)))) + (is (equal ':|DEVICE 2| (car (second data)))) + (is (equal ':|DEVICE 3| (car (third data)))) + (is (equal ':|DEVICE 4| (car (fourth data)))) + (is (equal ':|DEVICE 5| (car (fifth data)))) + ;; if 6 not last a big change has occurred. + (is (equal ':|DEVICE 6| (caar (last data)))))) diff --git a/tests/package.lisp b/tests/package.lisp new file mode 100644 index 0000000..08b4fcc --- /dev/null +++ b/tests/package.lisp @@ -0,0 +1,9 @@ +;;;; tests/package.lisp + +(defpackage #:ritherdon-rest-tests + (:use #:cl + #:fiveam + #:ratify) + (:export #:run! + #:all-tests + #:test-quasi))