Merge commit '728a186263' as 'third_party/lisp/fiveam'
				
					
				
			This commit is contained in:
		
						commit
						7db9b2aa71
					
				
					 20 changed files with 2596 additions and 0 deletions
				
			
		
							
								
								
									
										265
									
								
								third_party/lisp/fiveam/src/random.lisp
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										265
									
								
								third_party/lisp/fiveam/src/random.lisp
									
										
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -0,0 +1,265 @@ | |||
| ;;;; -*- Mode: Lisp; indent-tabs-mode: nil -*- | ||||
| 
 | ||||
| (in-package :it.bese.fiveam) | ||||
| 
 | ||||
| ;;;; ** Random (QuickCheck-ish) testing | ||||
| 
 | ||||
| ;;;; FiveAM provides the ability to automatically generate a | ||||
| ;;;; collection of random input data for a specific test and run a | ||||
| ;;;; test multiple times. | ||||
| 
 | ||||
| ;;;; Specification testing is done through the FOR-ALL macro. This | ||||
| ;;;; macro will bind variables to random data and run a test body a | ||||
| ;;;; certain number of times. Should the test body ever signal a | ||||
| ;;;; failure we stop running and report what values of the variables | ||||
| ;;;; caused the code to fail. | ||||
| 
 | ||||
| ;;;; The generation of the random data is done using "generator | ||||
| ;;;; functions" (see below for details). A generator function is a | ||||
| ;;;; function which creates, based on user supplied parameters, a | ||||
| ;;;; function which returns random data. In order to facilitate | ||||
| ;;;; generating good random data the FOR-ALL macro also supports guard | ||||
| ;;;; conditions and creating one random input based on the values of | ||||
| ;;;; another (see the FOR-ALL macro for details). | ||||
| 
 | ||||
| ;;;; *** Public Interface to the Random Tester | ||||
| 
 | ||||
| (defparameter *num-trials* 100 | ||||
|   "Number of times we attempt to run the body of the FOR-ALL test.") | ||||
| 
 | ||||
| (defparameter *max-trials* 10000 | ||||
|   "Number of total times we attempt to run the body of the | ||||
|   FOR-ALL test including when the body is skipped due to failed | ||||
|   guard conditions. | ||||
| 
 | ||||
| Since we have guard conditions we may get into infinite loops | ||||
| where the test code is never run due to the guards never | ||||
| returning true. This second run limit prevents that.") | ||||
| 
 | ||||
| (defmacro for-all (bindings &body body) | ||||
|   "Bind BINDINGS to random variables and test BODY *num-trials* times. | ||||
| 
 | ||||
| BINDINGS is a list of binding forms, each element is a list | ||||
| of (BINDING VALUE &optional GUARD). Value, which is evaluated | ||||
| once when the for-all is evaluated, must return a generator which | ||||
| be called each time BODY is evaluated. BINDING is either a symbol | ||||
| or a list which will be passed to destructuring-bind. GUARD is a | ||||
| form which, if present, stops BODY from executing when IT returns | ||||
| NIL. The GUARDS are evaluated after all the random data has been | ||||
| generated and they can refer to the current value of any | ||||
| binding. NB: Generator forms, unlike guard forms, can not contain | ||||
| references to the bound variables. | ||||
| 
 | ||||
| Examples: | ||||
| 
 | ||||
|   (for-all ((a (gen-integer))) | ||||
|     (is (integerp a))) | ||||
| 
 | ||||
|   (for-all ((a (gen-integer) (plusp a))) | ||||
|     (is (integerp a)) | ||||
|     (is (plusp a))) | ||||
| 
 | ||||
|   (for-all ((less (gen-integer)) | ||||
|             (more (gen-integer) (< less more))) | ||||
|     (is (<= less more))) | ||||
| 
 | ||||
|   (for-all (((a b) (gen-two-integers))) | ||||
|     (is (integerp a)) | ||||
|     (is (integerp b)))" | ||||
|   (with-gensyms (test-lambda-args) | ||||
|     `(perform-random-testing | ||||
|       (list ,@(mapcar #'second bindings)) | ||||
|       (lambda (,test-lambda-args) | ||||
|         (destructuring-bind ,(mapcar #'first bindings) | ||||
|             ,test-lambda-args | ||||
|           (if (and ,@(delete-if #'null (mapcar #'third bindings))) | ||||
|               (progn ,@body) | ||||
|               (throw 'run-once | ||||
|                 (list :guard-conditions-failed)))))))) | ||||
| 
 | ||||
| ;;;; *** Implementation | ||||
| 
 | ||||
| ;;;; We could just make FOR-ALL a monster macro, but having FOR-ALL be | ||||
| ;;;; a preproccessor for the perform-random-testing function is | ||||
| ;;;; actually much easier. | ||||
| 
 | ||||
| (defun perform-random-testing (generators body) | ||||
|   (loop | ||||
|      with random-state = *random-state* | ||||
|      with total-counter = *max-trials* | ||||
|      with counter = *num-trials* | ||||
|      with run-at-least-once = nil | ||||
|      until (or (zerop total-counter) | ||||
|                (zerop counter)) | ||||
|      do (let ((result (perform-random-testing/run-once generators body))) | ||||
|           (ecase (first result) | ||||
|             (:pass | ||||
|              (decf counter) | ||||
|              (decf total-counter) | ||||
|              (setf run-at-least-once t)) | ||||
|             (:no-tests | ||||
|              (add-result 'for-all-test-no-tests | ||||
|                          :reason "No tests" | ||||
|                          :random-state random-state) | ||||
|              (return-from perform-random-testing nil)) | ||||
|             (:guard-conditions-failed | ||||
|              (decf total-counter)) | ||||
|             (:fail | ||||
|              (add-result 'for-all-test-failed | ||||
|                          :reason "Found failing test data" | ||||
|                          :random-state random-state | ||||
|                          :failure-values (second result) | ||||
|                          :result-list (third result)) | ||||
|              (return-from perform-random-testing nil)))) | ||||
|      finally (if run-at-least-once | ||||
|                  (add-result 'for-all-test-passed) | ||||
|                  (add-result 'for-all-test-never-run | ||||
|                              :reason "Guard conditions never passed")))) | ||||
| 
 | ||||
| (defun perform-random-testing/run-once (generators body) | ||||
|   (catch 'run-once | ||||
|     (bind-run-state ((result-list '())) | ||||
|       (let ((values (mapcar #'funcall generators))) | ||||
|         (funcall body values) | ||||
|         (cond | ||||
|           ((null result-list) | ||||
|            (throw 'run-once (list :no-tests))) | ||||
|           ((every #'test-passed-p result-list) | ||||
|            (throw 'run-once (list :pass))) | ||||
|           ((notevery #'test-passed-p result-list) | ||||
|            (throw 'run-once (list :fail values result-list)))))))) | ||||
| 
 | ||||
| (defclass for-all-test-result () | ||||
|   ((random-state :initarg :random-state))) | ||||
| 
 | ||||
| (defclass for-all-test-passed (test-passed for-all-test-result) | ||||
|   ()) | ||||
| 
 | ||||
| (defclass for-all-test-failed (test-failure for-all-test-result) | ||||
|   ((failure-values :initarg :failure-values) | ||||
|    (result-list :initarg :result-list))) | ||||
| 
 | ||||
| (defgeneric for-all-test-failed-p (object) | ||||
|   (:method ((object for-all-test-failed)) t) | ||||
|   (:method ((object t)) nil)) | ||||
| 
 | ||||
| (defmethod reason ((result for-all-test-failed)) | ||||
|   (format nil "Falsifiable with ~S" (slot-value result 'failure-values))) | ||||
| 
 | ||||
| (defclass for-all-test-no-tests (test-failure for-all-test-result) | ||||
|   ()) | ||||
| 
 | ||||
| (defclass for-all-test-never-run (test-failure for-all-test-result) | ||||
|   ()) | ||||
| 
 | ||||
| ;;;; *** Generators | ||||
| 
 | ||||
| ;;;; Since this is random testing we need some way of creating random | ||||
| ;;;; data to feed to our code. Generators are regular functions which | ||||
| ;;;; create this random data. | ||||
| 
 | ||||
| ;;;; We provide a set of built-in generators. | ||||
| 
 | ||||
| (defun gen-integer (&key (max (1+ most-positive-fixnum)) | ||||
|                          (min (1- most-negative-fixnum))) | ||||
|   "Returns a generator which produces random integers greater | ||||
| than or equal to MIN and less than or equal to MAX." | ||||
|   (lambda () | ||||
|     (+ min (random (1+ (- max min)))))) | ||||
| 
 | ||||
| (defun gen-float (&key bound (type 'short-float)) | ||||
|   "Returns a generator which produces floats of type TYPE. BOUND, | ||||
| if specified, constrains the results to be in the range (-BOUND, | ||||
| BOUND)." | ||||
|   (lambda () | ||||
|     (let* ((most-negative (ecase type | ||||
|                             (short-float most-negative-short-float) | ||||
|                             (single-float most-negative-single-float) | ||||
|                             (double-float most-negative-double-float) | ||||
|                             (long-float most-negative-long-float))) | ||||
|            (most-positive (ecase type | ||||
|                             (short-float most-positive-short-float) | ||||
|                             (single-float most-positive-single-float) | ||||
|                             (double-float most-positive-double-float) | ||||
|                             (long-float most-positive-long-float))) | ||||
|            (bound (or bound (max most-positive (- most-negative))))) | ||||
|       (coerce | ||||
|        (ecase (random 2) | ||||
|          (0 ;; generate a positive number | ||||
|           (random (min most-positive bound))) | ||||
|          (1 ;; generate a negative number | ||||
|           (- (random (min (- most-negative) bound))))) | ||||
|        type)))) | ||||
| 
 | ||||
| (defun gen-character (&key (code-limit char-code-limit) | ||||
|                            (code (gen-integer :min 0 :max (1- code-limit))) | ||||
|                            (alphanumericp nil)) | ||||
|   "Returns a generator of characters. | ||||
| 
 | ||||
| CODE must be a generator of random integers. ALPHANUMERICP, if | ||||
| non-NIL, limits the returned chars to those which pass | ||||
| alphanumericp." | ||||
|   (lambda () | ||||
|     (loop | ||||
|        for count upfrom 0 | ||||
|        for char = (code-char (funcall code)) | ||||
|        until (and char | ||||
|                   (or (not alphanumericp) | ||||
|                       (alphanumericp char))) | ||||
|        when (= 1000 count) | ||||
|        do (error "After 1000 iterations ~S has still not generated ~:[a valid~;an alphanumeric~] character :(." | ||||
|                  code alphanumericp) | ||||
|        finally (return char)))) | ||||
| 
 | ||||
| (defun gen-string (&key (length (gen-integer :min 0 :max 80)) | ||||
|                         (elements (gen-character)) | ||||
|                         (element-type 'character)) | ||||
|   "Returns a generator which produces random strings. LENGTH must | ||||
| be a generator which produces integers, ELEMENTS must be a | ||||
| generator which produces characters of type ELEMENT-TYPE." | ||||
|   (lambda () | ||||
|     (loop | ||||
|        with length = (funcall length) | ||||
|        with string = (make-string length :element-type element-type) | ||||
|        for index below length | ||||
|        do (setf (aref string index) (funcall elements)) | ||||
|        finally (return string)))) | ||||
| 
 | ||||
| (defun gen-list (&key (length (gen-integer :min 0 :max 10)) | ||||
|                       (elements (gen-integer :min -10 :max 10))) | ||||
|   "Returns a generator which produces random lists. LENGTH must be | ||||
| an integer generator and ELEMENTS must be a generator which | ||||
| produces objects." | ||||
|   (lambda () | ||||
|     (loop | ||||
|        repeat (funcall length) | ||||
|        collect (funcall elements)))) | ||||
| 
 | ||||
| (defun gen-tree (&key (size 20) | ||||
|                       (elements (gen-integer :min -10 :max 10))) | ||||
|   "Returns a generator which produces random trees. SIZE controls | ||||
| the approximate size of the tree, but don't try anything above | ||||
|  30, you have been warned. ELEMENTS must be a generator which | ||||
| will produce the elements." | ||||
|   (labels ((rec (&optional (current-depth 0)) | ||||
|              (let ((key (random (+ 3 (- size current-depth))))) | ||||
|                (cond ((> key 2) | ||||
|                       (list (rec (+ current-depth 1)) | ||||
|                             (rec (+ current-depth 1)))) | ||||
|                      (t (funcall elements)))))) | ||||
|     (lambda () | ||||
|       (rec)))) | ||||
| 
 | ||||
| (defun gen-buffer (&key (length (gen-integer :min 0 :max 50)) | ||||
|                         (element-type '(unsigned-byte 8)) | ||||
|                         (elements (gen-integer :min 0 :max (1- (expt 2 8))))) | ||||
|   (lambda () | ||||
|     (let ((buffer (make-array (funcall length) :element-type element-type))) | ||||
|       (map-into buffer elements)))) | ||||
| 
 | ||||
| (defun gen-one-element (&rest elements) | ||||
|   (lambda () | ||||
|     (nth (random (length elements)) elements))) | ||||
| 
 | ||||
| ;;;; The trivial always-produce-the-same-thing generator is done using | ||||
| ;;;; cl:constantly. | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue