#lang racket/base
(require rackunit
rackunit/text-ui
"suffixtree.rkt"
(for-syntax racket/base))
(define (make-counter)
(let ((n -1))
(lambda ()
(set! n (add1 n))
n)))
(define (trace-tree-walk tree label)
(let* ((events '())
(add-event! (lambda args (set! events (cons args events)))))
(tree-walk tree label
(lambda (node up-label-offset)
(add-event! 'success node up-label-offset))
(lambda (node up-label-offset input-label-offset)
(add-event! 'fail node up-label-offset input-label-offset)))
(reverse events)))
(define (trace-equal? trace-1 trace-2)
(if (or (null? trace-1) (null? trace-2))
(and (null? trace-1) (null? trace-2))
(let-values (((head-1 head-2) (values (car trace-1) (car trace-2))))
(and (eq? (car head-1) (car head-2))
(equal? (cdr head-1) (cdr head-2))
(trace-equal? (cdr trace-1) (cdr trace-2))))))
(define-simple-check (check-trace-equal? a b)
(trace-equal? a b))
(define suffixtree-tests
(let ((tree #f)
(root #f)
(count-f #f))
(let-syntax
((make-test-case
(lambda (stx)
(syntax-case stx ()
((_ name e1 e2 ...)
(syntax/loc stx
(test-case
name
(set! tree (make-tree))
(set! root (tree-root tree))
(set! count-f (make-counter))
e1 e2 ...)))))))
(test-suite
"suffixtree.rkt"
(make-test-case
"tree? #t"
(check-true (tree? tree)))
(make-test-case
"tree? #f"
(check-false (tree? "arbor")))
(make-test-case
"tree-root"
(check-true (node? root)))
(make-test-case
"node? #f"
(check-false (node? tree))
(check-false (node? "bulge")))
(make-test-case
"tree-add! \"\" should fail"
(check-exn exn:fail?
(lambda () (tree-add! tree (string->label "")))))
(make-test-case
"tree-add! \"x\" should not fail"
(check-not-exn (lambda () (tree-add! tree (string->label "x")))))
(make-test-case
"tree-add! on duplicate strings should raise implicit tree exception"
(check-exn exn:fail? (lambda ()
(tree-add! tree (string->label "a"))
(tree-add! tree (string->label "a")))))
(make-test-case
"node-up-label on root"
(check label-equal? (string->label "") (node-up-label root)))
(make-test-case
"node-up-label on leaves"
(tree-add! tree (string->label "abc"))
(let* ((nodes (node-children root))
(strings (map (lambda (node)
(label->string (node-up-label node)))
nodes)))
(check-not-false (member "abc" strings))
(check-not-false (member "bc" strings))
(check-not-false (member "c" strings))
(check-equal? 3 (length strings))))
(make-test-case
"node-parent of root is #f"
(check-eq? #f (node-parent root)))
(make-test-case
"node-parent of single-edged tree should be root"
(tree-add! tree (string->label "a"))
(check-equal? 1 (length (node-children root)))
(let ((leaf (car (node-children root))))
(check-equal? root (node-parent leaf))))
(make-test-case
"node-parent with joint in the middle"
(tree-add! tree (string->label "aah"))
(let ((joint (node-find-child root #\a)))
(check-eq? root (node-parent joint))
(check label-equal? (string->label "a") (node-up-label joint))
(let ((leaf (node-find-child joint #\h)))
(check-eq? joint (node-parent leaf))
(check label-equal? (string->label "h") (node-up-label leaf)))))
(make-test-case
"node-find-child on empty returns #f"
(check-false (node-find-child root (string->label "z"))))
(make-test-case
"node-find-child on single leafed tree"
(tree-add! tree (string->label "a"))
(check-false (node-find-child root #\z))
(check-not-false (node-find-child root #\a))
(check-eq? (car (node-children root))
(node-find-child root #\a)))
(make-test-case
"node-find-child with choices"
(tree-add! tree (string->label "a"))
(tree-add! tree (string->label "b"))
(tree-add! tree (string->label "c"))
(check-equal? 3 (length (node-children root)))
(let ((a (node-find-child root #\a))
(b (node-find-child root #\b))
(c (node-find-child root #\c)))
(check-not-false a)
(check-not-false b)
(check-not-false c)
(check-not-eq? a b)
(check-not-eq? a c)
(check-not-eq? b c)
(check-eq? root (node-parent a))
(check-eq? root (node-parent b))
(check-eq? root (node-parent c))
(check-equal? "a" (label->string (node-up-label a)))
(check-equal? "b" (label->string (node-up-label b)))
(check-equal? "c" (label->string (node-up-label c)))))
(make-test-case
"structure tests + suffix-link test on 1011$
The tree should have the following structure:
(root) +-- $ (a)
|
+-- 011$ (b)
|
+-- 1 (c) -- $ (d)
|
+------- 011$ (e)
|
+------- 1$ (f)
with the only suffix link directed from (c) to the (root).
"
(tree-add! tree (string->label "1011$"))
(let* ((a (node-find-child root #\$))
(b (node-find-child root #\0))
(c (node-find-child root #\1))
(d (node-find-child c #\$))
(e (node-find-child c #\0))
(f (node-find-child c #\1)))
(for-each (lambda (n) (check-not-false n)) (list a b c d e f))
(check-eq? root (node-suffix-link c))
(for-each (lambda (n) (check-eq? root (node-parent n)))
(list a b c))
(for-each (lambda (n) (check-eq? c (node-parent n)))
(list d e f))
(for-each (lambda (node s)
(check string=? s
(label->string (node-up-label node))))
(list a b c d e f)
(list "$" "011$" "1" "$" "011$" "1$"))))
(make-test-case
"string->label/with-sentinel"
(check-equal? 2 (label-length (string->label/with-sentinel "1"))))
(make-test-case
"string->label/with-sentinel: sentinels are unique"
(let ((l1 (string->label/with-sentinel "1"))
(l2 (string->label/with-sentinel "1")))
(check-not-eq? (label-ref l1 1) (label-ref l2 1))
(check char=? (label-ref l1 0) (label-ref l2 0))))
(make-test-case
"label->string on empty is empty"
(check-equal? "" (label->string (string->label ""))))
(make-test-case
"label->string on whole label"
(check-equal? "chocolate" (label->string (string->label "chocolate"))))
(make-test-case
"label->string on sublabel"
(check-equal? "oh" (label->string (sublabel (string->label "pooh bear") 2 4))))
(make-test-case
"vector->label"
(let ((label (vector->label '#(1 two 3.0))))
(check-equal? 3 (label-length label))
(check-equal? 1 (label-ref label 0))
(check-equal? 'two (label-ref label 1))
(check-equal? 3.0 (label-ref label 2))))
(make-test-case
"vector->label on empty case"
(let ((label (vector->label #())))
(check-equal? 0 (label-length label))))
(make-test-case
"vector->label/with-sentinel"
(check-equal? 1 (label-length (vector->label/with-sentinel #())))
(let ((label-1 (vector->label/with-sentinel #()))
(label-2 (vector->label/with-sentinel #())))
(check-not-eq? (label-ref label-1 0)
(label-ref label-2 0))))
(make-test-case
"label->vector"
(let ((label (string->label "hello world")))
(check-equal? (list->vector (string->list "hello world"))
(label->vector label))
(check-true (immutable? (label->vector label)))))
(make-test-case
"label->vector with slicing"
(let* ((label-1 (string->label "tom and jerry"))
(label-2 (sublabel label-1 8)))
(check-equal? (list->vector (string->list "jerry"))
(label->vector label-2))
(check-true (immutable? (label->vector label-1)))
(check-true (immutable? (label->vector label-2)))))
(make-test-case
"label-equal? on same label"
(let ((label (string->label "foo")))
(check-true (label-equal? label label))))
(make-test-case
"label-equal? on different labels obviously false"
(let ((label-1 (string->label "foo"))
(label-2 (string->label "bar")))
(check-false (label-equal? label-1 label-2))))
(make-test-case
"label-equal? on different labels true"
(let ((label-1 (string->label "blocks"))
(label-2 (string->label "locks")))
(check-true (label-equal? (sublabel label-1 1) label-2))))
(make-test-case
"label-source-id"
(let* ((label-1 (string->label "hello"))
(label-2 (string->label "world")))
(check-false (equal? (label-source-id label-1) (label-source-id label-2)))))
(make-test-case
"label-source-id on empty string"
(let* ((label (string->label ""))
(subl (sublabel label 0)))
(check-equal? (label-source-id label) (label-source-id subl))))
(make-test-case
"label-source-id on another string"
(let* ((label (string->label "terminator"))
(subl (sublabel label 3 5)))
(check-equal? (label-source-id label) (label-source-id subl))))
(make-test-case
"label-source-eq?"
(let* ((label-1 (string->label "jump"))
(label-2 (string->label "jump")))
(check-true (label-source-eq? (sublabel label-1 0 2)
(sublabel label-1 2))
(check-false (label-source-eq? label-1 label-2)))))
(make-test-case
"null tree-walk"
(check-trace-equal? `((success ,(tree-root tree) 0))
(trace-tree-walk tree (string->label ""))))
(make-test-case
"unsuccessful tree-walk from empty tree"
(check-trace-equal? `((fail ,(tree-root tree) 0 0))
(trace-tree-walk tree (string->label "the truth is out there"))))
(make-test-case
"tree walks from aa$, whose tree looks like:
v
+---- $ (1)
|
+---- a (2) ---- $ (3)
|
+ ------- a$ (4)
I'll try to do this as exhaustively as I can.
"
(tree-add! tree (string->label "aa$"))
(let* ((n1 (node-find-child root #\$))
(n2 (node-find-child root #\a))
(n3 (node-find-child n2 #\$))
(n4 (node-find-child n2 #\a)))
(check-trace-equal? `((success ,n1 1))
(trace-tree-walk tree (string->label "$")))
(check-trace-equal? `((fail ,root 0 0))
(trace-tree-walk tree (string->label "z")))
(check-trace-equal? `((fail ,n1 1 1))
(trace-tree-walk tree (string->label "$z")))
(check-trace-equal? `((success ,n2 1))
(trace-tree-walk tree (string->label "a")))
(check-trace-equal? `((fail ,n2 1 1))
(trace-tree-walk tree (string->label "az")))
(check-trace-equal? `((success ,n3 1))
(trace-tree-walk tree (string->label "a$")))
(check-trace-equal? `((fail ,n3 1 2))
(trace-tree-walk tree (string->label "a$z")))
(check-trace-equal? `((success ,n4 1))
(trace-tree-walk tree (string->label "aa")))
(check-trace-equal? `((fail ,n4 1 2))
(trace-tree-walk tree (string->label "aaz")))
(check-trace-equal? `((success ,n4 2))
(trace-tree-walk tree (string->label "aa$")))
(check-trace-equal? `((fail ,n4 2 3))
(trace-tree-walk tree (string->label "aa$z")))))
(make-test-case
"single-character successful tree-walk"
(let* ((label-x (string->label "x"))
(_ (tree-add! tree label-x))
(leaf (node-find-child root #\x)))
(check trace-equal? `((success ,leaf 1))
(trace-tree-walk tree label-x))))
(make-test-case
"missisippi$ with tree-contains?"
(tree-add! tree (string->label "mississippi$"))
(check-false (tree-contains? tree (string->label "california$")))
(for-each (lambda (s)
(check-true (tree-contains? tree (string->label s))))
'("mississippi$"
"ississippi$"
"ssissippi$"
"sissippi$"
"issippi$"
"ssippi$"
"sippi$"
"ippi$"
"ppi$"
"pi$"
"i$"
"$"
"")))))))
(error-print-width 800)
(printf "suffixtree.rkt tests~%")
(void (run-tests suffixtree-tests))