we can now self-host...almost
[jackhill/mal.git] / runtest.py
index bd1d24b..c5b1f42 100755 (executable)
@@ -28,13 +28,7 @@ def log(data, end='\n'):
     print(data, end=end)
     sys.stdout.flush()
 
-# TODO: do we need to support '\n' too
-import platform
-if platform.system().find("CYGWIN_NT") >= 0:
-    # TODO: this is weird, is this really right on Cygwin?
-    sep = "\n\r\n"
-else:
-    sep = "\r\n"
+sep = "\n"
 rundir = None
 
 parser = argparse.ArgumentParser(
@@ -54,7 +48,7 @@ parser.add_argument('--log-file', type=str,
 parser.add_argument('--debug-file', type=str,
         help="Write all test interaction the named file")
 parser.add_argument('--hard', action='store_true',
-        help="Turn soft tests following a ';>>> soft=True' into hard failures")
+        help="Turn soft tests (soft, deferrable, optional) into hard failures")
 
 # Control whether deferrable and optional tests are executed
 parser.add_argument('--deferrable', dest='deferrable', action='store_true',
@@ -68,14 +62,16 @@ parser.add_argument('--no-optional', dest='optional', action='store_false',
         help="Disable optional tests that follow a ';>>> optional=True'")
 parser.set_defaults(optional=True)
 
-parser.add_argument('test_file', type=argparse.FileType('r'),
+parser.add_argument('test_file', type=str,
         help="a test file formatted as with mal test data")
 parser.add_argument('mal_cmd', nargs="*",
         help="Mal implementation command line. Use '--' to "
              "specify a Mal command line with dashed options.")
+parser.add_argument('--crlf', dest='crlf', action='store_true',
+        help="Write \\r\\n instead of \\n to the input")
 
 class Runner():
-    def __init__(self, args, no_pty=False):
+    def __init__(self, args, no_pty=False, line_break="\n"):
         #print "args: %s" % repr(args)
         self.no_pty = no_pty
 
@@ -118,6 +114,8 @@ class Runner():
         self.buf = ""
         self.last_prompt = ""
 
+        self.line_break = line_break
+
     def read_to_prompt(self, prompts, timeout):
         end_time = time.time() + timeout
         while time.time() < end_time:
@@ -128,18 +126,7 @@ class Runner():
                 #print("new_data: '%s'" % new_data)
                 debug(new_data)
                 # Perform newline cleanup
-                if self.no_pty:
-                    self.buf += new_data.replace("\n", "\r\n")
-                else:
-                    self.buf += new_data
-                self.buf = self.buf.replace("\r\r", "\r")
-                # Remove ANSI codes generally
-                #ansi_escape = re.compile(r'\x1B\[[0-?]*[ -/]*[@-~]')
-                # Remove rustyline ANSI CSI codes:
-                #  - [6C - CR + cursor forward
-                #  - [6K - CR + erase in line
-                ansi_escape = re.compile(r'\r\x1B\[[0-9]*[CK]')
-                self.buf = ansi_escape.sub('', self.buf)
+                self.buf += new_data.replace("\r", "")
                 for prompt in prompts:
                     regexp = re.compile(prompt)
                     match = regexp.search(self.buf)
@@ -155,7 +142,7 @@ class Runner():
         def _to_bytes(s):
             return bytes(s, "utf-8") if IS_PY_3 else s
 
-        self.stdin.write(_to_bytes(str + "\n"))
+        self.stdin.write(_to_bytes(str.replace('\r', '\x16\r') + self.line_break))
 
     def cleanup(self):
         #print "cleaning up"
@@ -169,7 +156,8 @@ class Runner():
 class TestReader:
     def __init__(self, test_file):
         self.line_num = 0
-        self.data = test_file.read().split('\n')
+        f = open(test_file, newline='') if IS_PY_3 else open(test_file)
+        self.data = f.read().split('\n')
         self.soft = False
         self.deferrable = False
         self.optional = False
@@ -203,8 +191,7 @@ class TestReader:
                     return True
                 continue
             elif line[0:1] == ";":         # unexpected comment
-                log("Test data error at line %d:\n%s" % (self.line_num, line))
-                return None
+                raise Exception("Test data error at line %d:\n%s" % (self.line_num, line))
             self.form = line   # the line is a form to send
 
             # Now find the output and return value
@@ -215,15 +202,19 @@ class TestReader:
                     self.line_num += 1
                     self.data.pop(0)
                     break
-                elif line[0:2] == "; ":
+                elif line[0:2] == ";/":
                     self.out = self.out + line[2:] + sep
                     self.line_num += 1
                     self.data.pop(0)
                 else:
-                    self.ret = "*"
+                    self.ret = ""
                     break
-            if self.ret: break
+            if self.ret != None: break
 
+        if self.out[-1:] == sep and not self.ret:
+            # If there is no return value, output should not end in
+            # separator
+            self.out = self.out[0:-1]
         return self.form
 
 args = parser.parse_args(sys.argv[1:])
@@ -236,7 +227,7 @@ if args.rundir: os.chdir(args.rundir)
 if args.log_file:   log_file   = open(args.log_file, "a")
 if args.debug_file: debug_file = open(args.debug_file, "a")
 
-r = Runner(args.mal_cmd, no_pty=args.no_pty)
+r = Runner(args.mal_cmd, no_pty=args.no_pty, line_break="\r\n" if args.crlf else "\n")
 t = TestReader(args.test_file)
 
 
@@ -247,7 +238,7 @@ def assert_prompt(runner, prompts, timeout):
         if header:
             log("Started with:\n%s" % header)
     else:
-        log("Did not one of following prompt(s): %s" % repr(prompts))
+        log("Did not receive one of following prompt(s): %s" % repr(prompts))
         log("    Got      : %s" % repr(r.buf))
         sys.exit(1)
 
@@ -264,8 +255,8 @@ except:
 # Send the pre-eval code if any
 if args.pre_eval:
     sys.stdout.write("RUNNING pre-eval: %s" % args.pre_eval)
-    p.write(args.pre_eval)
-    assert_prompt(args.test_timeout)
+    r.writeline(args.pre_eval)
+    assert_prompt(r, ['[^\s()<>]+> '], args.test_timeout)
 
 test_cnt = 0
 pass_cnt = 0
@@ -288,13 +279,13 @@ while t.next():
 
     if t.form == None: continue
 
-    log("TEST: %s -> [%s,%s]" % (t.form, repr(t.out), t.ret), end='')
+    log("TEST: %s -> [%s,%s]" % (repr(t.form), repr(t.out), t.ret), end='')
 
     # The repeated form is to get around an occasional OS X issue
     # where the form is repeated.
     # https://github.com/kanaka/mal/issues/30
-    expected = ["%s%s%s%s" % (t.form, sep, t.out, t.ret),
-                "%s%s%s%s%s%s" % (t.form, sep, t.form, sep, t.out, t.ret)]
+    expects = [".*%s%s%s" % (sep, t.out, re.escape(t.ret)),
+               ".*%s.*%s%s%s" % (sep, sep, t.out, re.escape(t.ret))]
 
     r.writeline(t.form)
     try:
@@ -302,7 +293,11 @@ while t.next():
         res = r.read_to_prompt(['\r\n[^\s()<>]+> ', '\n[^\s()<>]+> '],
                                 timeout=args.test_timeout)
         #print "%s,%s,%s" % (idx, repr(p.before), repr(p.after))
-        if t.ret == "*" or res in expected:
+        if (t.ret == "" and t.out == ""):
+            log(" -> SUCCESS (result ignored)")
+            pass_cnt += 1
+        elif (re.search(expects[0], res, re.S) or
+                re.search(expects[1], res, re.S)):
             log(" -> SUCCESS")
             pass_cnt += 1
         else:
@@ -314,11 +309,12 @@ while t.next():
                 log(" -> FAIL (line %d):" % t.line_num)
                 fail_cnt += 1
                 fail_type = ""
-            log("    Expected : %s" % repr(expected[0]))
+            log("    Expected : %s" % repr(expects[0]))
             log("    Got      : %s" % repr(res))
             failed_test = """%sFAILED TEST (line %d): %s -> [%s,%s]:
     Expected : %s
-    Got      : %s""" % (fail_type, t.line_num, t.form, repr(t.out), t.ret, repr(expected[0]), repr(res))
+    Got      : %s""" % (fail_type, t.line_num, t.form, repr(t.out),
+                        t.ret, repr(expects[0]), repr(res))
             failures.append(failed_test)
     except:
         _, exc, _ = sys.exc_info()
@@ -337,7 +333,7 @@ TEST RESULTS (for %s):
   %3d: failing tests
   %3d: passing tests
   %3d: total tests
-""" % (args.test_file.name, soft_fail_cnt, fail_cnt,
+""" % (args.test_file, soft_fail_cnt, fail_cnt,
         pass_cnt, test_cnt)
 log(results)