diff options
Diffstat (limited to 'skel/helper.sml')
-rw-r--r-- | skel/helper.sml | 173 |
1 files changed, 173 insertions, 0 deletions
diff --git a/skel/helper.sml b/skel/helper.sml new file mode 100644 index 0000000..d20e96f --- /dev/null +++ b/skel/helper.sml @@ -0,0 +1,173 @@ +(* Copyright (C) 2017 Ryan Kavanagh <rkavanagh@cs.cmu.edu> *) +(* Distributed under the ISC license, see COPYING for details. *) + +functor Helper (structure C : sig val handinPath : string end) + :> HELPER = +struct + +exception MissingFile of string + +datatype checks = Check of string * (unit -> unit) + | Problem of string * (unit -> real) + +val handinPath = C.handinPath + +fun printLn s = print (s ^ "\n") + +(* Puts strings in boxes... *) +fun stringsInBox strs = + let fun repeatChar c n = String.implode (List.tabulate (n, fn _ => c)) + val maxLength = List.foldl (fn (s,themax) => Int.max (String.size s, themax)) 0 strs + val edge = repeatChar #"#" (maxLength + 4) + in + edge :: (List.foldr (fn (s,thelist) => ("# " + ^ s + ^ (repeatChar #" " (maxLength - (String.size s))) + ^ " #") :: thelist) + [edge] + strs) + end + +(* Prints strings in boxes... *) +fun printInBox strs = List.app printLn (stringsInBox strs) + +(* Generates an AutoLab json score string *) +fun scoresToString (scores, scoreboard) = + let val scoreStrings = map (fn (problem, score) => "\"" ^ problem ^ "\": " ^ (Real.toString score)) scores + val scores = String.concatWith ", " scoreStrings + val scoreboard = case scoreboard + of SOME l => ", \"scoreboard\": [" ^ (String.concatWith ", " (map Int.toString l)) ^ "]" + | NONE => "" + in + "{\"scores\": {" ^ scores ^ "}" ^ scoreboard ^ "}" + end + +(* Aborts the program by printing strs, and gives an empty score. *) +fun abortWithMessage strs = + let val _ = List.app printLn strs + val _ = printLn (scoresToString ([],NONE)) + in + OS.Process.exit OS.Process.success + end + + +(* Reads the lines of a file into a list *) +(* Each string in the file will always be contain a newline (#"\n") at the end. *) +fun readLines filename = + let val inFile = TextIO.openIn filename + fun readlines ins = + case TextIO.inputLine ins + of SOME ln => ln :: readlines ins + | NONE => [] + val lines = readlines inFile + val _ = TextIO.closeIn inFile + in + lines + end + +(* Check if the file exists *) +fun checkFileExists (name : string) : unit = + if OS.FileSys.access (name, [OS.FileSys.A_READ]) + then () + else raise MissingFile name + +fun joinHandinPath file = + OS.Path.concat (handinPath, file) + +fun stripHandinPath path = + if String.isPrefix handinPath path then + String.extract (path, String.size handinPath + 1, NONE) + else + path + + +(* Takes in a list of filenames, and checks if those files *) +(* exist in the handinPath directory. *) +(* Exits catastrophically if a file is missing. *) +fun checkFilesExist filenames = + List.app (checkFileExists o joinHandinPath) filenames + handle MissingFile name => (abortWithMessage o stringsInBox) + [ "File " ^ (stripHandinPath name) ^ " missing." + , "Please make sure you included all required files and resubmit."] + +fun runCmd cmd = (printLn cmd; OS.Process.system cmd) + +(* Reads from fd in n byte chunks and treats it all as strings. *) +fun readAllFDAsString (fd, n) = + let val v = Posix.IO.readVec (fd, n) + in if Word8Vector.length v = 0 then + "" + else + (Byte.bytesToString v) ^ (readAllFDAsString (fd, n)) + end + +(* Runs a command c (command and argument list) using Posix.Process.execp. *) +(* Return the program's output as a string, along with its exit status. *) +fun execpOutput (c : string * string list) : string * Posix.Process.exit_status = + let val { infd = infd, outfd = outfd } = Posix.IO.pipe () + in case Posix.Process.fork () + of NONE => (* Child *) + (( Posix.IO.close infd + ; Posix.IO.dup2 { old = outfd, new = Posix.FileSys.stdout } + ; Posix.IO.dup2 { old = outfd, new = Posix.FileSys.stderr } + ; Posix.Process.execp c) + handle OS.SysErr (err, _) => + ( print ("Fatal error in child: " ^ err ^ "\n") + ; OS.Process.exit OS.Process.failure )) + | SOME pid => (* Parent *) + let val _ = Posix.IO.close outfd + val (_, status) = Posix.Process.waitpid (Posix.Process.W_CHILD pid, []) + val output = readAllFDAsString (infd, 100) + val _ = Posix.IO.close infd + in (output, status) end + end + + +(* Check if a submitted file is a valid PDF using ghostscript. *) +fun checkPDF pdf = + let val spdf = joinHandinPath pdf in + case ( runCmd ("gs -o/dev/null -sDEVICE=nullpage " ^ spdf) + , Posix.FileSys.ST.size (Posix.FileSys.stat spdf) ) + of (_,0) => let val _ = printInBox [ "Warning: The empty file " ^ pdf ^ " is not a valid PDF document." + , "Please make sure to resubmit with a valid PDF document in its place." ] + in () end + | (0,_) => () + | _ => (abortWithMessage o stringsInBox) + [ "The file " ^ pdf ^ " is not a valid PDF document." + , "Please resubmit with a valid PDF document (or an empty file) in its place." + , "If you are convinced you submitted a valid PDF, please contact the course staff." ] + end + +(* Runs all of the checks and grades all of the problems in "checks". *) +fun runChecks (checks : checks list) = + List.foldl (fn (cs,results) => + case cs + of (Check (n, c)) => let val _ = printLn ("\n\nRunning check " ^ n ^ "...") + val _ = c () + val _ = printLn " Success.\n" + in results end + | (Problem (n, c)) => let val _ = printLn ("\n\nChecking problem " ^ n ^ "...") + val res = c () + val _ = printLn (" Score: " ^ (Real.toString res) ^ ".\n") + in (n, res) :: results end) + [] + checks + + +(* Returns a score of zero for all problems. *) +(* Useful when you need to abort but still provide a score. *) +fun failAll (checks : checks list) = + List.foldr (fn (cs,results) => + case cs + of Problem (n, c) => (n, 0) :: results + | _ => results) + [] + checks + +structure RE = RegExpFn(structure P = AwkSyntax + structure E = ThompsonEngine) + +fun matchesAwkRegex (r, s) = + let val r = RE.find (RE.compileString r) + in Option.isSome (StringCvt.scanString r s) end +end |