|
1 | 1 | import sys |
2 | | -import constants |
3 | | -sys.path.insert(0, constants.CODE_DIR) |
4 | | - |
5 | 2 | import doctest |
6 | | -import os |
7 | | -import argparse |
8 | | -from dataclasses import dataclass |
9 | 3 | 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() |
63 | 4 |
|
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 |
72 | 6 |
|
73 | | -totalFailures = 0 |
74 | | -totalTests = 0 |
75 | 7 |
|
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>' |
77 | 58 |
|
78 | | -if opts.diffOutput: |
79 | | - doctestOptions = doctestOptions | doctest.REPORT_NDIFF |
80 | 59 |
|
81 | | -# We use our own DocTestParser to replace exception names in stacktraces |
82 | 60 | class MyDocTestParser(doctest.DocTestParser): |
83 | 61 | def get_examples(self, string, name='<string>'): |
| 62 | + """ |
| 63 | + The string is the docstring from the file which we want to test. |
| 64 | + """ |
84 | 65 | prefs = {'WyppTypeError: ': 'errors.WyppTypeError: ', |
85 | 66 | 'WyppNameError: ': 'errors.WyppNameError: ', |
86 | 67 | 'WyppAttributeError: ': 'errors.WyppAttributeError: '} |
87 | 68 | lines = [] |
88 | 69 | for l in string.split('\n'): |
89 | 70 | for pref,repl in prefs.items(): |
90 | 71 | if l.startswith(pref): |
91 | | - l = repl + l[len(pref):] |
| 72 | + l = repl + l |
92 | 73 | lines.append(l) |
| 74 | + rewriteLines(lines) |
93 | 75 | string = '\n'.join(lines) |
94 | 76 | x = super().get_examples(string, name) |
95 | 77 | return x |
96 | 78 |
|
97 | | -for repl in opts.repls: |
| 79 | +def testRepl(repl: str, defs: dict) -> tuple[int, int]: |
| 80 | + doctestOptions = doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS |
98 | 81 | (failures, tests) = doctest.testfile(repl, globs=defs, module_relative=False, |
99 | 82 | optionflags=doctestOptions, parser=MyDocTestParser()) |
100 | | - |
101 | | - totalFailures += failures |
102 | | - totalTests += tests |
103 | 83 | if failures == 0: |
104 | 84 | if tests == 0: |
105 | 85 | print(f'No tests in {repl}') |
106 | 86 | else: |
107 | 87 | print(f'All {tests} tests in {repl} succeeded') |
108 | 88 | else: |
109 | 89 | 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!') |
115 | 106 | 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) |
0 commit comments