aboutsummaryrefslogtreecommitdiff
path: root/skel/helper.sml
blob: d20e96f51f58ebc277ef87b0be4c24f775320515 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
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