Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions python/code/wypp/cmdlineArgs.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ def parseCmdlineArgs(argList):
parser.add_argument('--no-typechecking', dest='checkTypes', action='store_const',
const=False, default=True,
help='Do not check type annotations')
parser.add_argument('--repl', action='extend', type=str, nargs='+', default=[], dest='repls',
help='Run repl tests in the file given')
parser.add_argument('file', metavar='FILE',
help='The file to run', nargs='?')
if argList is None:
Expand Down
163 changes: 76 additions & 87 deletions python/code/wypp/replTester.py
Original file line number Diff line number Diff line change
@@ -1,119 +1,108 @@
import sys
import constants
sys.path.insert(0, constants.CODE_DIR)

import doctest
import os
import argparse
from dataclasses import dataclass
from myLogging import *
import runCode

usage = """python3 replTester.py [ ARGUMENTS ] LIB_1 ... LIB_n --repl SAMPLE_1 ... SAMPLE_m

If no library files should be used to test the REPL samples, omit LIB_1 ... LIB_n
and the --repl flag.
The definitions of LIB_1 ... LIB_n are made available when testing
SAMPLE_1 ... SAMPLE_m, where identifer in LIB_i takes precedence over identifier in
LIB_j if i > j.
"""

@dataclass
class Options:
verbose: bool
diffOutput: bool
libs: list[str]
repls: list[str]

def parseCmdlineArgs():
parser = argparse.ArgumentParser(usage=usage,
formatter_class=argparse.RawTextHelpFormatter)
parser.add_argument('--verbose', dest='verbose', action='store_const',
const=True, default=False,
help='Be verbose')
parser.add_argument('--diffOutput', dest='diffOutput',
action='store_const', const=True, default=False,
help='print diff of expected/given output')
args, restArgs = parser.parse_known_args()
libs = []
repls = []
replFlag = '--repl'
if replFlag in restArgs:
cur = libs
for x in restArgs:
if x == replFlag:
cur = repls
else:
cur.append(x)
else:
repls = restArgs
if len(repls) == 0:
print('No SAMPLE arguments given')
sys.exit(1)
return Options(verbose=args.verbose, diffOutput=args.diffOutput, libs=libs, repls=repls)

opts = parseCmdlineArgs()

if opts.verbose:
enableVerbose()

libDir = os.path.dirname(__file__)
libFile = os.path.join(libDir, 'writeYourProgram.py')
defs = globals()

for lib in opts.libs:
d = os.path.dirname(lib)
if d not in sys.path:
sys.path.insert(0, d)

for lib in opts.libs:
verbose(f"Loading lib {lib}")
defs = runCode.runCode(lib, defs)
# We use our own DocTestParser to replace exception names in stacktraces

totalFailures = 0
totalTests = 0

doctestOptions = doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS
def rewriteLines(lines: list[str]):
"""
Each line has exactly one of the following four kinds:
- COMMENT: if it starts with '#' (leading whitespace stripped)
- PROMPT: if it starts with '>>>' (leading whitespace stripped)
- EMPTY: if it contains only whitespace
- OUTPUT: otherwise

rewriteLines replaces every EMPTY lines with '<BLANKLINE>', provided
the first non-EMPTY line before the line has kind PROMPT OR OUTPUT
and the next non-EMPTY line after the line has kind OUTPUT.
"""

def get_line_kind(line: str) -> str:
stripped = line.lstrip()
if not stripped:
return 'EMPTY'
elif stripped.startswith('#'):
return 'COMMENT'
elif stripped.startswith('>>>'):
return 'PROMPT'
else:
return 'OUTPUT'

def find_prev_non_empty(idx: int) -> tuple[int, str]:
"""Find the first non-EMPTY line before idx. Returns (index, kind)"""
for i in range(idx - 1, -1, -1):
kind = get_line_kind(lines[i])
if kind != 'EMPTY':
return i, kind
return -1, 'NONE'

def find_next_non_empty(idx: int) -> tuple[int, str]:
"""Find the first non-EMPTY line after idx. Returns (index, kind)"""
for i in range(idx + 1, len(lines)):
kind = get_line_kind(lines[i])
if kind != 'EMPTY':
return i, kind
return -1, 'NONE'

# Process each line
for i in range(len(lines)):
if get_line_kind(lines[i]) == 'EMPTY':
# Check conditions for replacement
prev_idx, prev_kind = find_prev_non_empty(i)
next_idx, next_kind = find_next_non_empty(i)

# Replace if previous is PROMPT or OUTPUT and next is OUTPUT
if prev_kind in ['PROMPT', 'OUTPUT'] and next_kind == 'OUTPUT':
lines[i] = '<BLANKLINE>'

if opts.diffOutput:
doctestOptions = doctestOptions | doctest.REPORT_NDIFF

# We use our own DocTestParser to replace exception names in stacktraces
class MyDocTestParser(doctest.DocTestParser):
def get_examples(self, string, name='<string>'):
"""
The string is the docstring from the file which we want to test.
"""
prefs = {'WyppTypeError: ': 'errors.WyppTypeError: ',
'WyppNameError: ': 'errors.WyppNameError: ',
'WyppAttributeError: ': 'errors.WyppAttributeError: '}
lines = []
for l in string.split('\n'):
for pref,repl in prefs.items():
if l.startswith(pref):
l = repl + l[len(pref):]
l = repl + l
lines.append(l)
rewriteLines(lines)
string = '\n'.join(lines)
x = super().get_examples(string, name)
return x

for repl in opts.repls:
def testRepl(repl: str, defs: dict) -> tuple[int, int]:
doctestOptions = doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS
(failures, tests) = doctest.testfile(repl, globs=defs, module_relative=False,
optionflags=doctestOptions, parser=MyDocTestParser())

totalFailures += failures
totalTests += tests
if failures == 0:
if tests == 0:
print(f'No tests in {repl}')
else:
print(f'All {tests} tests in {repl} succeeded')
else:
print(f'ERROR: {failures} out of {tests} in {repl} failed')

if totalFailures == 0:
if totalTests == 0:
print('ERROR: No tests found at all!')
sys.exit(1)
return (failures, tests)

def testRepls(repls: list[str], defs: dict):
totalFailures = 0
totalTests = 0
for r in repls:
(failures, tests) = testRepl(r, defs)
totalFailures += failures
totalTests += tests

if totalFailures == 0:
if totalTests == 0:
print('ERROR: No tests found at all!')
sys.exit(1)
else:
print(f'All {totalTests} tests succeeded. Great!')
else:
print(f'All {totalTests} tests succeeded. Great!')
else:
print(f'ERROR: {failures} out of {tests} failed')
sys.exit(1)
print(f'ERROR: {failures} out of {tests} failed')
sys.exit(1)
4 changes: 4 additions & 0 deletions python/code/wypp/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ def main(globals, argList=None):
runCode.performChecks(args.check, args.testFile, globals, libDefs, doTypecheck=args.checkTypes,
extraDirs=args.extraDirs, loadingFailed=loadingFailed)

if args.repls:
import replTester
replTester.testRepls(args.repls, globals)

if isInteractive:
interactive.enterInteractive(globals, args.checkTypes, loadingFailed)

Expand Down
2 changes: 1 addition & 1 deletion python/integration-tests/testIntegration.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,6 @@ class ReplTesterTests(unittest.TestCase):

def test_replTester(self):
d = shell.pwd()
cmd = f'python3 {d}/code/wypp/replTester.py {d}/integration-test-data/repl-test-lib.py --repl {d}/integration-test-data/repl-test-checks.py'
cmd = f'{d}/run {d}/integration-test-data/repl-test-lib.py --repl {d}/integration-test-data/repl-test-checks.py'
res = shell.run(cmd, captureStdout=True, onError='die', cwd='/tmp')
self.assertIn('All 1 tests succeeded. Great!', res.stdout)
2 changes: 1 addition & 1 deletion python/run
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/bin/bash

if [ -z "$PY" ]; then
PY=python3.13
PY=python3
fi
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"

Expand Down
5 changes: 0 additions & 5 deletions python/run-repl-tester

This file was deleted.