Assignment A – Testing Properties
Due Fri 4/25 11:59pm
Unlike the assignments with numbers in their names, this assignment is worth more points. See the course overview for more information about how your grade is calculated.
The subject of this assignment is the problem of finding shortest paths in a directed graph where edges have nonnegative, rational weights. We will consider two implementations; first one that I wrote that’s buggy and then one that you write that you’ll strive to debugify. The overall goal is to experiment with random testing and properties, using this problem to make the issues concrete. Keep in mind that the goal is get experience with random testing, not to learn graph algorithms (although a dose of graph algorithms from time to time is not a bad thing).
1.
The function below is a buggy implementation of the Floyd-Warshall algorithm. It uses graph.rkt (which, as far as I know, is not buggy); see the the top of the file for an explanation of the library.
Note that this algorithm works when the edge weights are negative but we are not going to use it in that case (you can imagine a credible reason why some application might never have negative weights on the edges I suppose; the pedagogic reason is that we want this part of the assignment to mesh with later parts of the assignment where another algorithm cannot handle negative edge weights).
Run the file and observe that the test cases in the file (which are correct) all pass. Formulate a property that should hold for the shortest path function and then find an input that fails to satisfy that property using random testing. Simplify the test case to find the smallest test case you can. (You can use a shrinker like we did in class or you can do this manually.) Once you have that input, turn it into a small test case and include the test case in your code. Fix the bug.
#lang racket |
(require "graph.rkt") |
(provide |
(contract-out |
[shortest-paths |
(-> (and/c graph? no-negative-edges?) |
(-> string? string? |
(listof string?)))] |
;; given a graph, returns a function |
;; that finds shortest paths in the graph. |
;; mutations to the graph after the outer |
;; function returns are effectively ignored |
)) |
(define (shortest-paths g) |
(define dist (make-hash)) |
(define prev (make-hash)) |
(FloydWarshallWithPathReconstruction g dist prev) |
(λ (u v) |
(Path prev u v))) |
(define (FloydWarshallWithPathReconstruction g dist prev) |
(define ns (nodes g)) |
(for* ([u (in-list ns)] |
[v (in-list ns)]) |
(define w (edge-weight g u v)) |
(cond |
[w |
(hash-set! dist (cons u v) w) |
(hash-set! prev (cons u v) u)] |
[else |
(hash-set! dist (cons u v) +inf.0) |
(hash-set! prev (cons u v) '())])) |
(for ([v (in-list ns)]) |
(hash-set! dist (cons v v) 0) |
(hash-set! prev (cons v v) v)) |
(for ([k (in-list ns)]) |
(for ([i (in-list ns)]) |
(for ([j (in-list ns)]) |
(when (> (hash-ref dist (cons i j)) |
(+ (hash-ref dist (cons i k)) |
(hash-ref dist (cons k j)))) |
(hash-set! dist (cons i j) |
(+ (hash-ref dist (cons i k)) |
(hash-ref dist (cons k j)))) |
(hash-set! prev (cons i j) (hash-ref prev (cons i k)))))))) |
(define (Path prev u v) |
(cond |
[(equal? (hash-ref prev (cons u v)) '()) |
'()] |
[else |
(define path (list v)) |
(let loop () |
(unless (equal? u v) |
(set! v (hash-ref prev (cons u v))) |
(set! path (cons v path)) |
(loop))) |
path])) |
;; this is a debugging aid; it prints out the |
;; dist matrix (and it works best when the node |
;; names are at most two characters). |
(define (show-dist g dist) |
(printf "~a " (pad "")) |
(for ([j (in-list (nodes g))]) |
(printf "~a " (pad j))) |
(printf "\n") |
(for ([i (in-list (nodes g))]) |
(printf "~a " (pad i)) |
(for ([j (in-list (nodes g))]) |
(define d (hash-ref dist (cons i j))) |
(printf "~a " (pad (if (equal? +inf.0 d) "∞" (~a d))))) |
(printf "\n")) |
(printf "\n")) |
(define (pad s) |
(define size 2) |
(cond |
[(< (string-length s) size) |
(string-append (make-string (- size (string-length s)) #\space) |
s)] |
[else s])) |
(module+ test |
(require rackunit) |
(define example-graph (new-graph '(("1") ("2") ("3") ("4") ("5")) #:directed? #t)) |
(add-edge! example-graph "1" "3" 0) |
(add-edge! example-graph "1" "2" 4) |
(add-edge! example-graph "2" "3" 3) |
(add-edge! example-graph "3" "4" 200) |
(add-edge! example-graph "4" "2" 10) |
(define sp (shortest-paths example-graph)) |
(check-equal? (sp "1" "1") (list "1")) |
(check-equal? (sp "1" "2") (list "1" "2")) |
(check-equal? (sp "1" "3") (list "1" "3")) |
(check-equal? (sp "1" "5") '())) |
2.
Implement Dijkstra’s algorithm for finding shortest paths and then use that to implement a function that finds all the shortest paths in the graphs, just like Floyd-Warshall from problem. Use the exact same contract for the shortest-paths function that is used in the code above.
Depending on how you use structure your code, there may be a problem with mutating the graph after calling shortest-path (but before calling the result function). If your code has this problem, a simple way to avoid this problem is to copy the graph and use only the copy (yay for GC).
Use the same random testing from part 1 to try to find bugs in your solution. Debug any problems you find.
3
One of the best sources of properties is when you can afford two different implementations of the same functionality (perhaps one is a reference implementation meant for people to understand the system with well-known limitations or perhaps there are good reasons that multiple independent implementations exist, e.g. C compilers).
It is tempting to, in this case, imagine that these two algorithms offer such an opportunity with the property saying that the shortest path found by Dijkstra is the same as the one found by Floyd-Warshall.
Code up that property and run it on random graphs and find a failure. Explain why this failure is not because of a bug in the implementations (or, if it is, then fix the bug and try again!).
Submit a single racket file with your solution via Canvas.