aboutsummaryrefslogblamecommitdiff
path: root/skel/helper.sml
blob: d20e96f51f58ebc277ef87b0be4c24f775320515 (plain) (tree)












































































































































































                                                                                                                    
(* 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