doc-test.lisp 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. "This code block parses and executes all code blocks found within
  2. documentation strings for a given module.
  3. This script should be run as follows:
  4. ```sh
  5. $ bin/urn.lua plugins/doc-test.lisp --run -- list
  6. ```
  7. This will import the `list` library, and use the symbols declared in
  8. that library to declare tests. One can also generate tests from
  9. re-exported symbols declared in other libraries by passing the
  10. `--all` (or `-a`) flag:
  11. ```sh
  12. $ bin/urn.lua plugins/doc-test.lisp --run -- list --all
  13. ```
  14. You can exclude particular code blocks from being tested by appending
  15. `:no-test` after the language part of the code block.
  16. The current implementation has several limitations which will be
  17. rectified in the future:
  18. - Does not test the module-level documentation string (namely this
  19. thing).
  20. - Does not handle standard output ([[print!]] and the like), as it only
  21. expects a single line of output.
  22. - Does not handle results which span multiple lines, such as that found
  23. in `io/do`.
  24. - Cannot test for expressions which error."
  25. (import compiler/resolve _)
  26. (import compiler _)
  27. (import urn/documentation _)
  28. (import urn/parser _/parser)
  29. (import test _/test)
  30. (defun _/var-warning! (var msg)
  31. :hidden
  32. (_/logger/put-node-warning! msg
  33. (_/var-definition var) nil (_/range/get-source (_/var-definition var)) ""))
  34. (defun _/var-error! (var msg)
  35. :hidden
  36. (_/logger/do-node-error! msg
  37. (_/var-definition var) nil (_/range/get-source (_/var-definition var)) ""))
  38. (defun _/subst (tree vars)
  39. "Substitute the variables in TREE with VARS."
  40. :hidden
  41. (case (type tree)
  42. ["symbol" (or (.> vars (symbol->string tree)) tree)]
  43. ["list"
  44. (for i 1 (n tree) 1
  45. (.<! tree i (_/subst (nth tree i) vars)))
  46. tree]
  47. [_ tree]))
  48. (defun _/build-vars (libs)
  49. :hidden
  50. (let* [(top-level '())
  51. (tests `(_/test/describe "The stdlib"))
  52. (vars (_/scope-vars))]
  53. (for-each name (sort! (keys vars))
  54. (when (and (not (string/starts-with? name "_/")) (or (empty? libs)
  55. (any (lambda (x)
  56. (string/starts-with? (.> (.> vars name) :full-name) (.. x "/")))
  57. libs)))
  58. (let* [(var (.> vars name))
  59. (docs (-> (or (_/var-docstring var) "")
  60. _/parse-docstring
  61. (filter (lambda (x) (and (= (.> x :kind) "mono")
  62. (string/starts-with? (.> x :whole) "```")
  63. (not (string/find (.> x :whole) "^```[^\n]*:no%-test[^\n]*\n")))) <>)
  64. (map (cut .> <> :contents) <>)))]
  65. (for-each entry docs
  66. (let [(lines (string/split entry "\n"))
  67. (asserts `(_/test/it ,(.. "has tests for " (.> var :full-name))))]
  68. (push! tests asserts)
  69. (cond
  70. ;; Just do a couple of sanity checks on the code
  71. [(empty? lines)
  72. (_/var-warning! var "This example is empty.")
  73. (.<! asserts 1 `_/test/pending)]
  74. ;; Everything is OK so let's build a list
  75. [true
  76. (let [(subst {})
  77. (i 1)]
  78. (loop []
  79. [(> i (n lines))]
  80. (if (/= (string/char-at (nth lines i) 1) ">")
  81. (progn
  82. (_/var-warning! var (.. "Expected line beginning with '>', got " (string/quoted (nth lines i))))
  83. (.<! asserts 1 `_/test/pending))
  84. (with (buffer (list (string/sub (nth lines i) 2)))
  85. (inc! i)
  86. ;; Gobble lines starting with "."
  87. (loop [] [(> i (n lines))]
  88. (with (line (nth lines i))
  89. (when (= (string/char-at line 1) ".")
  90. (push! buffer (string/sub line 2))
  91. (inc! i)
  92. (recur))))
  93. ;; Parse the expression
  94. (with ((ok res) (pcall _/parser/read (concat buffer "\n")))
  95. (cond
  96. ;; Check we didn't fail.
  97. [(not ok)
  98. (_/var-error! var (format true "Parsing failed for {#name}: {#res}"))
  99. (.<! asserts 1 `_/test/pending)]
  100. ;; Each line must have exactly one entry
  101. [(/= (n res) 1)
  102. (_/var-warning! var (.. "Expected exactly one node, got " (n res)))
  103. (.<! asserts 1 `_/test/pending)
  104. (set! ok false)]
  105. ;; Do a primitive check for top level definitions, ensuring they are pushed to the head.
  106. [(and (list? (car res)) (elem? (caar res) '(define define-macro defun defmacro defgeneric)))
  107. (with (renamed (string->symbol (.. name "/" (symbol->string (cadar res)))))
  108. (.<! subst (symbol->string (cadar res)) renamed)
  109. (push! top-level (_/subst (car res) subst))
  110. (set! res renamed))]
  111. [true (set! res (_/subst (car res) subst))])
  112. (when ok
  113. (with (stdout '())
  114. ;; Gobble stdout lines
  115. (loop [] [(> i (n lines))]
  116. (with (line (nth lines i))
  117. (unless (or (string/starts-with? line "out = ") (string/starts-with? line ">"))
  118. (push! stdout line)
  119. (inc! i)
  120. (recur))))
  121. (with (line (nth lines i))
  122. (cond
  123. ;; If we're the last line, then we expect some sort of result
  124. [(not line)
  125. (_/var-warning! var "Expected result, got nothing")
  126. (.<! asserts 1 `_/test/pending)]
  127. ;; If we've got no result and we're not the last entry then just push the expression
  128. ;; unless there was a stdout, then warn.
  129. [(not (string/starts-with? line "out ="))
  130. (if (empty? stdout)
  131. (progn
  132. (push! asserts res)
  133. (recur))
  134. (progn
  135. (_/var-warning! var (.. "Expected result to start with \"out = \", got " (pretty line)))
  136. (.<! asserts 1 `_/test/pending)))]
  137. ;; Otherwise, let's push our affirmation and continue
  138. [true
  139. (with (res-lines (list (string/trim (string/sub line 6))))
  140. (inc! i)
  141. (loop [] [(> i (n lines))]
  142. (with (line (nth lines i))
  143. (when (string/starts-with? line " ")
  144. (push! res-lines (string/trim line))
  145. (inc! i)
  146. (recur))))
  147. (if (empty? stdout)
  148. (push! asserts `(_/test/affirm (= (pretty ,res) ,(concat res-lines " "))))
  149. (with (stdout-sym (gensym 'stdout))
  150. (push! asserts `(let* [(,stdout-sym '())
  151. (print! (lambda (,'&args) (push! ,stdout-sym (concat (map tostring ,'args) " ")) nil))]
  152. (_/test/affirm
  153. (= (pretty ,res) ,(concat res-lines " "))
  154. (eq? ',stdout ,stdout-sym)))))))
  155. ;; Discard lines starting with ";"
  156. (loop [] [(> i (n lines))]
  157. (when (= (string/char-at (nth lines i) 1) ";")
  158. (inc! i)
  159. (recur)))
  160. (recur)])))))))))]))))))
  161. (push! top-level tests)
  162. top-level))
  163. ,@(with (args
  164. (loop
  165. [(args *arguments*)]
  166. [(empty? args) '()]
  167. (if (= (car args) "--")
  168. (cdr args)
  169. (recur (cdr args)))))
  170. (when (empty? args) (fail! "No arguments given to doc-test"))
  171. (with (libs (filter (lambda (x) (/= (string/char-at x 1) "-")) args))
  172. (with (gen (map (lambda (x) `(import ,(string->symbol x) ())) libs))
  173. (push! gen (list `unquote-splice
  174. `(_/build-vars ',(if (or (elem? "--all" args) (elem? "-a" args))
  175. '()
  176. libs))))
  177. gen)))