scheme_test.py (plain text)


"""Unit testing framework for the Logo interpreter.

Usage: python3 scheme_test.py FILE

Interprets FILE as interactive Scheme source code, and compares each line
of printed output from the read-eval-print loop and from any output functions
to an expected output described in a comment.  For example,

(display (+ 2 3))
; expect 5

Differences between printed and expected outputs are printed with line numbers.
"""

import io
import sys
from buffer import Buffer
from ucb import main
from scheme import scheme_repl, create_global_frame

def summarize(output, expected_output):
    """Summarize results of running tests."""
    num_failed, num_expected = 0, len(expected_output)
    for (actual, (expected, line_number)) in zip(output, expected_output):
        if expected.startswith("Error"):
            if not actual.startswith("Error"):
                num_failed += 1
                print('test failed at line {0}'.format(line_number))
                print('  expected an error indication')
                print('   printed: {0}'.format(actual))
        elif actual != expected:
            num_failed += 1
            print('test failed at line {0}'.format(line_number))
            print('  expected: {0}'.format(expected))
            print('   printed: {0}'.format(actual))
    print('{0} tested; {1} failed.'.format(num_expected, num_failed))

EXPECT_STRING = '; expect'

@main
def run_tests(src_file = 'tests.scm'):
    """Run a read-eval loop that reads from src_file and collects outputs."""
    expected_output = []
    line_number = 0

    def read_lines(src):
        """Creates a generator that returns the lines of src, filtering out
        '; expect' strings and collecting them into expected_output with their
        line numbers.  The variable line_number gives the number of the last
        line returned for diagnostic purposes."""
        nonlocal line_number
        while True:
            line_number += 1
            line = src.readline()
            if line.lstrip().startswith(EXPECT_STRING):
                expected = line.split(EXPECT_STRING, 1)[1][1:-1]
                expected_output.append((expected, line_number))
                continue
            if not line:
                return
            yield line

    sys.stderr = sys.stdout = io.StringIO() # Collect output to stdout and stderr
    try:
        source = read_lines(open(src_file))
        scheme_repl(source, "", create_global_frame(), False)
    except BaseException as exc:
        sys.stderr = sys.__stderr__
        print("Tests terminated due to unhandled exception "
              "after line {0}:\n>>>".format(line_number),
              file=sys.stderr)
        raise
    output = sys.stdout.getvalue().split('\n')
    sys.stdout = sys.__stdout__  # Revert stdout
    summarize(output, expected_output)