util.ss
;;;
;;; Time-stamp: <06/01/03 14:28:46 nhw>
;;;
;;; Copyright (C) 2005 by Noel Welsh.
;;;

;;; This library is free software; you can redistribute it
;;; and/or modify it under the terms of the GNU Lesser
;;; General Public License as published by the Free Software
;;; Foundation; either version 2.1 of the License, or (at
;;; your option) any later version.

;;; This library is distributed in the hope that it will be
;;; useful, but WITHOUT ANY WARRANTY; without even the
;;; implied warranty of MERCHANTABILITY or FITNESS FOR A
;;; PARTICULAR PURPOSE.  See the GNU Lesser General Public
;;; License for more details.

;;; You should have received a copy of the GNU Lesser
;;; General Public License along with this library; if not,
;;; write to the Free Software Foundation, Inc., 59 Temple
;;; Place, Suite 330, Boston, MA 02111-1307 USA

;;; Author: Noel Welsh <[email protected]>
;;
;;
;; Commentary:

(module util mzscheme

  (require (planet "test.ss" ("schematics" "schemeunit.plt" 2))
           (only (lib "base64.ss" "net") base64-encode-stream))
  
  ;; For dealing with serialised hash tables.
  (require
   ;; (planet "ssax.ss" ("lizorkin" "ssax.plt" 1))
   (only (planet "sxml.ss" ("lizorkin" "sxml.plt" 1)) sxpath))

  (provide (all-defined))

  ;; RPC timeout
  ;; When working on the library without an internet connection (or, more
  ;; importantly, when your proxies are set up incorrectly), some of the RPC
  ;; tests should timeout sooner rather than later. And, they should
  ;; "fail", although techincally it isn't exactly a failure.
  (define RPC-TIMEOUT 10)
  
  (define-syntax (with-timeout stx)
    (syntax-case stx ()
      [(_ timeout-msg time body)
       #`(let ()
           (define complete? #f)
           (define test-thread 
             (thread (lambda () (set! complete? body))))
           (define timeout-thread
             (thread (lambda () 
                       (sleep time)
                       (when (thread-running? test-thread)
                         (printf "Test timed out.~n")
                         (printf "Test skipped: ~a~n" timeout-msg)
                         (kill-thread test-thread)))))
           (define test-done (thread-dead-evt test-thread))
           (define kill-done (thread-dead-evt timeout-thread))
           
           ;; Wait for the test or kill to be done.
           (sync test-done kill-done)
           
           ;; If the test completed, that's good... but we
           ;; should cleanup the timeout thread.
           (if (thread-running? timeout-thread)
               (kill-thread timeout-thread))

           ;; Regardless of who killed whom, we return the state variable.
           complete?
           )]))
  
  ;; Failure check
  ;; I want to write some tests that are supposed to fail. These are,
  ;; I suppose, failure tests. I'll just wrap these up in a macro
  ;; to make life a little easier.
  (define-syntax (check-fail stx)
    (syntax-case stx ()
      [(_ check)
       #`(check-exn
          exn:test:check?
          (lambda ()
            check))]))
  
  (define-check (check-hash-table-equal? hash1 hash2)
    (check-hash-table-contains hash1 hash2)
    (check-hash-table-contains hash2 hash1))
  
  (define (set-equal? ls1 ls2)
    (define (check-set s1 s2)
      (andmap (lambda (o)
                (member o s2)) s1))
    (and (check-set ls1 ls2)
         (check-set ls2 ls1)))
  
  ;; check-serialised-hash-table-equal? : sxml sxml -> void
  ;; Unfortunately, the serialisation checks for hash tables don't
  ;; reliably give us ordering... so we have to do the same thing for
  ;; the serialised form as we do with hashes that are deserialised.
  ;;
  ;; They come in looking like:
  ;;
  ;; (value
  ;;  (struct
  ;;   (member (name "c") (value (double "3.0")))
  ;;   (member (name "b") (value (string "2")))
  ;;   (member (name "a") (value (int "1")))))
  ;;
  ;; Therefore, I'll pull them apart like the lists that they are, and
  ;; run some checks over the names and values.
  (define-check (check-serialised-hash-table-equal? sxml1 sxml2)
    (define (extract-names sxml) ((sxpath '(// name)) sxml))
    (define (extract-values sxml) ((sxpath '(// value)) sxml))
    (let ([names1 (extract-names sxml1)]
          [names2 (extract-names sxml2)]
          [values1 (extract-values sxml1)]
          [values2 (extract-values sxml2)])

      ;; Make sure both hash tables have the same number of elements
      ;; before proceeding.
      (when (not (equal? (length names1) (length names2)))
        (with-check-info
         (('message "Hash tables have different numbers of elements."))
         (fail-check)))
      
      ;; Check that both sets of keys are equal.
      (unless (set-equal? names1 names2)
        (with-check-info
         (('message 
           (format "First hash has different keys than second hash:~n\tH1: ~s~n\tH2 ~s~n"
                   names1 names2)))
         (fail-check)))
      
      ;; Do the same (unordered) check for values.
      (unless (set-equal? values1 values2)
        (with-check-info
         (('message 
           (format "First hash has different values than second hash:~n\tH1: ~s~n\tH2 ~s~n"
                   values1 values2)))
         (fail-check)))
      
      ;; And lastly, do a hash-table check on them... because I don't want to implement
      ;; it using association lists. So, I'll load each of these lists of names and values
      ;; into a hash table, and then use 'check-hash-table-equal?' on them. It's a bit
      ;; of a cheat, and feels round-about... but it's a step.
      (let ([h1 (make-hash-table 'equal)]
            [h2 (make-hash-table 'equal)])
        (define (load-hash h lon lov)
          (for-each (lambda (n v)
                      (hash-table-put! h (string->symbol (cadr n)) v))
                    lon lov))
        (load-hash h1 names1 values1)
        (load-hash h2 names2 values2)
        
        (unless (check-hash-table-equal? h1 h2)
          (with-check-info 
           (('message "Serialised hash tables are not equal."))
           (fail-check))))
            
        ))
  
  ;; check-hash-table-contains : hash-table hash-table -> void
  ;;
  ;; Assert that hash1 contains all the values in hash2.
  ;; I.e. that hash1 is equal to or a superset of hash2.
  (define-check (check-hash-table-contains hash1 hash2)
    (hash-table-for-each
     hash2
     (lambda (key v2)
       (let ((v1
              (hash-table-get
               hash1 key
               (lambda ()
                 (with-check-info
                  (('message
                    (format "No value found with key ~e" key)))
                  (fail-check))))))
         (if (and (hash-table? v1) (hash-table? v2))
             (check-hash-table-equal?* v1 v2)
             (check-equal? v1 v2))))))

  ;; base64-encode : bytes -> string
  (define (base64-encode byte)
    (let ((output (open-output-string)))
      (base64-encode-stream
       (open-input-bytes byte)
       output
       #"")
      (get-output-string output)))
  
  )