|
| 1 | +import os, re |
| 2 | + |
| 3 | +# Expected output has been supplied as an environment variable |
| 4 | +# and the program has been stoped because the output was incorrect |
| 5 | + |
| 6 | +def explain_output_difference(loc, output_stream, color): |
| 7 | + # autotest may try to print all errors in red, so disable this |
| 8 | + print(color('', 'black'), end='', file=output_stream) |
| 9 | + explain_output_difference1(loc, output_stream, color) |
| 10 | + print(file=output_stream) |
| 11 | + |
| 12 | +def explain_output_difference1(loc, output_stream, color): |
| 13 | + |
| 14 | + # values supplied for expected output for this execution |
| 15 | + expected_stdout = os.environb.get(b'DCC_EXPECTED_STDOUT', '') |
| 16 | +# ignore_case = getenv_boolean('DCC_IGNORE_CASE') |
| 17 | +# ignore_empty_lines = getenv_boolean('DCC_IGNORE_EMPTY_LINES') |
| 18 | + ignore_trailing_white_space = getenv_boolean('DCC_IGNORE_TRAILING_WHITE_SPACE', default=True) |
| 19 | +# compare_only_characters = os.environ.get('DCC_COMPARE_ONLY_CHARACTERS') |
| 20 | + |
| 21 | + # value describing discrepency between actual and expected output |
| 22 | + reason = os.environ.get('DCC_OUTPUT_ERROR', '') |
| 23 | + line_number = getenv_int('DCC_ACTUAL_LINE_NUMBER') |
| 24 | + |
| 25 | + # these values do not include current line |
| 26 | + n_expected_bytes_seen = getenv_int('DCC_N_EXPECTED_BYTES_SEEN') |
| 27 | + n_actual_bytes_seen = getenv_int('DCC_N_ACTUAL_BYTES_SEEN') |
| 28 | + |
| 29 | + expected_line = os.environb.get(b'DCC_EXPECTED_LINE', '') |
| 30 | + actual_line = os.environb.get(b'DCC_ACTUAL_LINE', '') |
| 31 | + expected_column = getenv_int('DCC_EXPECTED_COLUMN') |
| 32 | + actual_column = getenv_int('DCC_ACTUAL_COLUMN') |
| 33 | + |
| 34 | + if 0 <= actual_column < len(actual_line): |
| 35 | + actual_char = actual_line[actual_column] |
| 36 | + else: |
| 37 | + actual_char = None |
| 38 | + |
| 39 | + if ignore_trailing_white_space and expected_stdout[-1:] == b'\n': |
| 40 | + expected_stdout = expected_stdout.rstrip() + b'\n' |
| 41 | + |
| 42 | + n_expected_lines = len(expected_stdout.splitlines()) |
| 43 | + is_last_expected_line = n_expected_bytes_seen + len(expected_line) >= len(expected_stdout) |
| 44 | + |
| 45 | + if 0 <= expected_column < len(expected_line): |
| 46 | + expected_byte = expected_line[expected_column:expected_column+1] |
| 47 | + else: |
| 48 | + expected_byte = None |
| 49 | + |
| 50 | +# primary = lambda text, **kwargs: color(text, style='bold', **kwargs) |
| 51 | + danger = lambda text, **kwargs: color(text, fg='red', style='bold', **kwargs) |
| 52 | + success = lambda text, **kwargs: color(text, fg='green', style='bold', **kwargs) |
| 53 | +# info = lambda text, **kwargs: color(text, fg='cyan', style='bold', **kwargs) |
| 54 | + |
| 55 | + if not actual_line.endswith(b"\n"): |
| 56 | + print("Execution failed because ", end='', file=output_stream) |
| 57 | + else: |
| 58 | + print("Execution stopped because ", end='', file=output_stream) |
| 59 | + |
| 60 | + if reason == "expected line too long": |
| 61 | + print("internal error: expected line too long", file=output_stream) |
| 62 | + return |
| 63 | + |
| 64 | + if reason == "line too long": |
| 65 | + print("program wrote", danger("a line containing over " + str(actual_column) + " bytes."), file=output_stream) |
| 66 | + print("Do you have an infinite loop?", file=output_stream) |
| 67 | + print_line(actual_line, "start of the", danger, output_stream) |
| 68 | + return |
| 69 | + |
| 70 | + if reason == "too much output": |
| 71 | + print("program produced", danger("too much output."), file=output_stream) |
| 72 | + print("Your program printed", actual_column, "bytes.") |
| 73 | + print("Do you have an infinite loop?", file=output_stream) |
| 74 | + print_line(actual_line, "last", danger, output_stream) |
| 75 | + return |
| 76 | + |
| 77 | + if reason == "zero byte": |
| 78 | + print("a", danger("zero byte ('\\0')"), "was printed.", file=output_stream) |
| 79 | + print("Byte", actual_column, "of line", line_number, "of program's output was a zero byte ('\\0')", file=output_stream) |
| 80 | + if len(expected_line): |
| 81 | + print("Here are the characters on the line before the zero byte:", file=output_stream) |
| 82 | + print(sanitize(actual_line), file=output_stream) |
| 83 | + print("\nFor more information go to:", explanation_url("zero_byte"), file=output_stream) |
| 84 | + return |
| 85 | + |
| 86 | + if n_actual_bytes_seen == 0 and len(actual_line) == 0: |
| 87 | + print("program produced", danger("no output."), file=output_stream) |
| 88 | + print(n_expected_lines, "lines of output were expected", file=output_stream) |
| 89 | + print_line(expected_line, "first expected", success, output_stream) |
| 90 | + return |
| 91 | + |
| 92 | + if len(actual_line) == 0: |
| 93 | + print("of", danger("missing output lines."), file=output_stream) |
| 94 | + print("Your program printed", line_number, "lines of correct output but stopped before printing all the expected output.", file=output_stream) |
| 95 | + print_line(expected_line, "next expected", success, output_stream) |
| 96 | + return |
| 97 | + |
| 98 | + if n_expected_bytes_seen == 0 and len(expected_line) == 0: |
| 99 | + print("program produced", danger("output when no output was expected."), file=output_stream) |
| 100 | + print_line(actual_line, "unexpected", danger, output_stream) |
| 101 | + return |
| 102 | + |
| 103 | + if len(expected_line) == 0: |
| 104 | + print("of", danger("unexpected extra output."), file=output_stream) |
| 105 | + print("The program produced all the expected output and then produced extra output.", file=output_stream) |
| 106 | + print_line(actual_line, "extra", danger, output_stream) |
| 107 | + return |
| 108 | + |
| 109 | + if is_last_expected_line and expected_byte == b'\n' and actual_char is None: |
| 110 | + print('the last', danger("newline was missing."), file=output_stream) |
| 111 | + print("Your program produced all the expected output, except the last newline ('\\n') was missing.", file=output_stream) |
| 112 | + print("\nFor more information go to", explanation_url("missing_newline"), file=output_stream) |
| 113 | + return |
| 114 | + |
| 115 | + show_line_length = max(len(expected_line) + 8, 80) |
| 116 | + |
| 117 | + |
| 118 | + bad_characters_explanation = check_bad_characters(actual_line, line_number, danger, expected_line) |
| 119 | + if bad_characters_explanation: |
| 120 | + print(bad_characters_explanation, end='', file=output_stream) |
| 121 | + return |
| 122 | + |
| 123 | + print("of an", danger('incorrect output line.'), file=output_stream) |
| 124 | + print('Byte', actual_column + 1, 'of line', line_number, "of program output was incorrect.", file=output_stream) |
| 125 | + |
| 126 | + if not actual_line[actual_column+1:]: |
| 127 | + if actual_line.rstrip(b'\n') + expected_byte == expected_line.rstrip(b'\n'): |
| 128 | + print("A", "'" + danger(sanitize(expected_byte)) + "'", "was missing from the end of the output line.", file=output_stream) |
| 129 | + else: |
| 130 | + print("The characters you printed were correct, but more characters were expected.", file=output_stream) |
| 131 | + |
| 132 | + print("The correct output line was:", file=output_stream) |
| 133 | + print(success(sanitize(expected_line, max_line_length_shown=show_line_length)), file=output_stream) |
| 134 | + |
| 135 | + print("Your program printed this line:", file=output_stream) |
| 136 | + |
| 137 | + correct_prefix = success(sanitize(actual_line[0:actual_column])) |
| 138 | + print(correct_prefix, end='', file=output_stream) |
| 139 | + |
| 140 | + incorrect_byte = actual_line[actual_column:actual_column + 1] |
| 141 | + if incorrect_byte == ' ': |
| 142 | + print(danger(sanitize(incorrect_byte), bg='red'), end='', file=output_stream) |
| 143 | + else: |
| 144 | + print(danger(sanitize(incorrect_byte)), end='', file=output_stream) |
| 145 | + |
| 146 | + print(sanitize(actual_line[actual_column+1:], max_line_length_shown=show_line_length - actual_column), file=output_stream) |
| 147 | + |
| 148 | +def print_line(line, description, line_color, output_stream): |
| 149 | + if line == b'\n': |
| 150 | + print("The", description, "line was an empty line (a '\\n').", file=output_stream) |
| 151 | + else: |
| 152 | + print("The", description, "line was:", file=output_stream) |
| 153 | + print(line_color(sanitize(line)), file=output_stream) |
| 154 | + |
| 155 | + |
| 156 | +def sanitize(line, max_line_length_shown=256): |
| 157 | + if len(line) > max_line_length_shown: |
| 158 | + line = line[0:max_line_length_shown] + b' ...' |
| 159 | + return repr(line.rstrip(b'\n'))[2:-1] |
| 160 | + |
| 161 | + |
| 162 | +def check_bad_characters(line, line_number, danger, expected): |
| 163 | + if re.search(rb'[\x00-\x08\x14-\x1f\x7f-\xff]', expected): |
| 164 | + return None |
| 165 | + m = re.search(rb'^(.*?)([\x00-\x08\x14-\x1f\x7f-\xff])', line) |
| 166 | + if not m: |
| 167 | + return None |
| 168 | + (prefix, offending_char) = m.groups() |
| 169 | + offending_value = ord(offending_char) |
| 170 | + if offending_value == 0: |
| 171 | + description = "zero byte ('" + danger('\\0') + "')" |
| 172 | + elif offending_value > 127: |
| 173 | + description = "non-ascii byte " + danger("\\x%02x" % (offending_value)) |
| 174 | + else: |
| 175 | + description = "non-printable character " + danger("\\x%02x" % (offending_value)) |
| 176 | + column = len(prefix) |
| 177 | + explanation = "a " + danger("non-ASCII byte") + " was printed.\n" |
| 178 | + explanation += "Byte %d of line %d of program output was a %s\n" % (column + 1, line_number, description) |
| 179 | + |
| 180 | + explanation += "Here is line %d with non-printable characters replaced with backslash-escaped equivalents:\n" % (line_number) |
| 181 | + line = repr(line)[2:-1] + '\n' |
| 182 | + line = re.sub(r'(\\x[0-9a-f][0-9a-f])', danger(r'\1'), line) |
| 183 | + explanation += line |
| 184 | + if offending_value == 255: |
| 185 | + explanation += "\nHave you accidentally printed the special EOF value getchar returns?\n" |
| 186 | + explanation += "For more information go to: " + explanation_url("eof_byte") + "\n" |
| 187 | + return explanation |
| 188 | + |
| 189 | +def getenv_boolean(name, default=False): |
| 190 | + if name in os.environ: |
| 191 | + value = os.environ[name] |
| 192 | + return value and value[0] not in "0fFnN" |
| 193 | + else: |
| 194 | + return default |
| 195 | + |
| 196 | + |
| 197 | +def getenv_int(name): |
| 198 | + try: |
| 199 | + return int(os.environ.get(name, 0)) |
| 200 | + except ValueError: |
| 201 | + return 0 |
| 202 | + |
| 203 | +EXPLANATION_BASE_URL = "https://comp1511unsw.github.io/dcc/" |
| 204 | + |
| 205 | +def explanation_url(page): |
| 206 | + return EXPLANATION_BASE_URL + page + ".html" |
0 commit comments