Skip to content

Commit 314fccb

Browse files
authored
Merge pull request #179 from skogsbaer/fix-repl
fix repl tester
2 parents 1be048a + 8524091 commit 314fccb

File tree

6 files changed

+84
-94
lines changed

6 files changed

+84
-94
lines changed

python/code/wypp/cmdlineArgs.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ def parseCmdlineArgs(argList):
3939
parser.add_argument('--no-typechecking', dest='checkTypes', action='store_const',
4040
const=False, default=True,
4141
help='Do not check type annotations')
42+
parser.add_argument('--repl', action='extend', type=str, nargs='+', default=[], dest='repls',
43+
help='Run repl tests in the file given')
4244
parser.add_argument('file', metavar='FILE',
4345
help='The file to run', nargs='?')
4446
if argList is None:

python/code/wypp/replTester.py

Lines changed: 76 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,119 +1,108 @@
11
import sys
2-
import constants
3-
sys.path.insert(0, constants.CODE_DIR)
4-
52
import doctest
6-
import os
7-
import argparse
8-
from dataclasses import dataclass
93
from myLogging import *
10-
import runCode
11-
12-
usage = """python3 replTester.py [ ARGUMENTS ] LIB_1 ... LIB_n --repl SAMPLE_1 ... SAMPLE_m
13-
14-
If no library files should be used to test the REPL samples, omit LIB_1 ... LIB_n
15-
and the --repl flag.
16-
The definitions of LIB_1 ... LIB_n are made available when testing
17-
SAMPLE_1 ... SAMPLE_m, where identifer in LIB_i takes precedence over identifier in
18-
LIB_j if i > j.
19-
"""
20-
21-
@dataclass
22-
class Options:
23-
verbose: bool
24-
diffOutput: bool
25-
libs: list[str]
26-
repls: list[str]
27-
28-
def parseCmdlineArgs():
29-
parser = argparse.ArgumentParser(usage=usage,
30-
formatter_class=argparse.RawTextHelpFormatter)
31-
parser.add_argument('--verbose', dest='verbose', action='store_const',
32-
const=True, default=False,
33-
help='Be verbose')
34-
parser.add_argument('--diffOutput', dest='diffOutput',
35-
action='store_const', const=True, default=False,
36-
help='print diff of expected/given output')
37-
args, restArgs = parser.parse_known_args()
38-
libs = []
39-
repls = []
40-
replFlag = '--repl'
41-
if replFlag in restArgs:
42-
cur = libs
43-
for x in restArgs:
44-
if x == replFlag:
45-
cur = repls
46-
else:
47-
cur.append(x)
48-
else:
49-
repls = restArgs
50-
if len(repls) == 0:
51-
print('No SAMPLE arguments given')
52-
sys.exit(1)
53-
return Options(verbose=args.verbose, diffOutput=args.diffOutput, libs=libs, repls=repls)
54-
55-
opts = parseCmdlineArgs()
56-
57-
if opts.verbose:
58-
enableVerbose()
59-
60-
libDir = os.path.dirname(__file__)
61-
libFile = os.path.join(libDir, 'writeYourProgram.py')
62-
defs = globals()
634

64-
for lib in opts.libs:
65-
d = os.path.dirname(lib)
66-
if d not in sys.path:
67-
sys.path.insert(0, d)
68-
69-
for lib in opts.libs:
70-
verbose(f"Loading lib {lib}")
71-
defs = runCode.runCode(lib, defs)
5+
# We use our own DocTestParser to replace exception names in stacktraces
726

73-
totalFailures = 0
74-
totalTests = 0
757

76-
doctestOptions = doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS
8+
def rewriteLines(lines: list[str]):
9+
"""
10+
Each line has exactly one of the following four kinds:
11+
- COMMENT: if it starts with '#' (leading whitespace stripped)
12+
- PROMPT: if it starts with '>>>' (leading whitespace stripped)
13+
- EMPTY: if it contains only whitespace
14+
- OUTPUT: otherwise
15+
16+
rewriteLines replaces every EMPTY lines with '<BLANKLINE>', provided
17+
the first non-EMPTY line before the line has kind PROMPT OR OUTPUT
18+
and the next non-EMPTY line after the line has kind OUTPUT.
19+
"""
20+
21+
def get_line_kind(line: str) -> str:
22+
stripped = line.lstrip()
23+
if not stripped:
24+
return 'EMPTY'
25+
elif stripped.startswith('#'):
26+
return 'COMMENT'
27+
elif stripped.startswith('>>>'):
28+
return 'PROMPT'
29+
else:
30+
return 'OUTPUT'
31+
32+
def find_prev_non_empty(idx: int) -> tuple[int, str]:
33+
"""Find the first non-EMPTY line before idx. Returns (index, kind)"""
34+
for i in range(idx - 1, -1, -1):
35+
kind = get_line_kind(lines[i])
36+
if kind != 'EMPTY':
37+
return i, kind
38+
return -1, 'NONE'
39+
40+
def find_next_non_empty(idx: int) -> tuple[int, str]:
41+
"""Find the first non-EMPTY line after idx. Returns (index, kind)"""
42+
for i in range(idx + 1, len(lines)):
43+
kind = get_line_kind(lines[i])
44+
if kind != 'EMPTY':
45+
return i, kind
46+
return -1, 'NONE'
47+
48+
# Process each line
49+
for i in range(len(lines)):
50+
if get_line_kind(lines[i]) == 'EMPTY':
51+
# Check conditions for replacement
52+
prev_idx, prev_kind = find_prev_non_empty(i)
53+
next_idx, next_kind = find_next_non_empty(i)
54+
55+
# Replace if previous is PROMPT or OUTPUT and next is OUTPUT
56+
if prev_kind in ['PROMPT', 'OUTPUT'] and next_kind == 'OUTPUT':
57+
lines[i] = '<BLANKLINE>'
7758

78-
if opts.diffOutput:
79-
doctestOptions = doctestOptions | doctest.REPORT_NDIFF
8059

81-
# We use our own DocTestParser to replace exception names in stacktraces
8260
class MyDocTestParser(doctest.DocTestParser):
8361
def get_examples(self, string, name='<string>'):
62+
"""
63+
The string is the docstring from the file which we want to test.
64+
"""
8465
prefs = {'WyppTypeError: ': 'errors.WyppTypeError: ',
8566
'WyppNameError: ': 'errors.WyppNameError: ',
8667
'WyppAttributeError: ': 'errors.WyppAttributeError: '}
8768
lines = []
8869
for l in string.split('\n'):
8970
for pref,repl in prefs.items():
9071
if l.startswith(pref):
91-
l = repl + l[len(pref):]
72+
l = repl + l
9273
lines.append(l)
74+
rewriteLines(lines)
9375
string = '\n'.join(lines)
9476
x = super().get_examples(string, name)
9577
return x
9678

97-
for repl in opts.repls:
79+
def testRepl(repl: str, defs: dict) -> tuple[int, int]:
80+
doctestOptions = doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS
9881
(failures, tests) = doctest.testfile(repl, globs=defs, module_relative=False,
9982
optionflags=doctestOptions, parser=MyDocTestParser())
100-
101-
totalFailures += failures
102-
totalTests += tests
10383
if failures == 0:
10484
if tests == 0:
10585
print(f'No tests in {repl}')
10686
else:
10787
print(f'All {tests} tests in {repl} succeeded')
10888
else:
10989
print(f'ERROR: {failures} out of {tests} in {repl} failed')
110-
111-
if totalFailures == 0:
112-
if totalTests == 0:
113-
print('ERROR: No tests found at all!')
114-
sys.exit(1)
90+
return (failures, tests)
91+
92+
def testRepls(repls: list[str], defs: dict):
93+
totalFailures = 0
94+
totalTests = 0
95+
for r in repls:
96+
(failures, tests) = testRepl(r, defs)
97+
totalFailures += failures
98+
totalTests += tests
99+
100+
if totalFailures == 0:
101+
if totalTests == 0:
102+
print('ERROR: No tests found at all!')
103+
sys.exit(1)
104+
else:
105+
print(f'All {totalTests} tests succeeded. Great!')
115106
else:
116-
print(f'All {totalTests} tests succeeded. Great!')
117-
else:
118-
print(f'ERROR: {failures} out of {tests} failed')
119-
sys.exit(1)
107+
print(f'ERROR: {failures} out of {tests} failed')
108+
sys.exit(1)

python/code/wypp/runner.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,10 @@ def main(globals, argList=None):
9999
runCode.performChecks(args.check, args.testFile, globals, libDefs, doTypecheck=args.checkTypes,
100100
extraDirs=args.extraDirs, loadingFailed=loadingFailed)
101101

102+
if args.repls:
103+
import replTester
104+
replTester.testRepls(args.repls, globals)
105+
102106
if isInteractive:
103107
interactive.enterInteractive(globals, args.checkTypes, loadingFailed)
104108

python/integration-tests/testIntegration.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,6 @@ class ReplTesterTests(unittest.TestCase):
8989

9090
def test_replTester(self):
9191
d = shell.pwd()
92-
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'
92+
cmd = f'{d}/run {d}/integration-test-data/repl-test-lib.py --repl {d}/integration-test-data/repl-test-checks.py'
9393
res = shell.run(cmd, captureStdout=True, onError='die', cwd='/tmp')
9494
self.assertIn('All 1 tests succeeded. Great!', res.stdout)

python/run

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#!/bin/bash
22

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

python/run-repl-tester

Lines changed: 0 additions & 5 deletions
This file was deleted.

0 commit comments

Comments
 (0)