Skip to content

Commit 208b038

Browse files
authored
Add output diff rendering (#18)
1 parent 26899b9 commit 208b038

File tree

8 files changed

+310
-265
lines changed

8 files changed

+310
-265
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,6 @@ build/
2727
dist/
2828
/result.md
2929
/results.md
30+
31+
.DS_Store
32+
.vscode/

Pipfile

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@ schema = "*"
99
cpplint = "*"
1010
datetime = "*"
1111
black = "*"
12+
typing-extensions = "*"
1213

1314
[dev-packages]
14-
15-
[requires]
16-
python_version = "3.8"

Pipfile.lock

Lines changed: 158 additions & 204 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

homework_checker/core/md_writer.py

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,21 @@
1010
TABLE_TEMPLATE = "| {hw_name} | {task_name} | {test_name} | {result_sign} |\n"
1111
TABLE_SEPARATOR = "|---|---|---|:---:|\n"
1212

13+
ENTRY_TEMPLATE = """
14+
**`{name}`**
15+
```{syntax}
16+
{content}
17+
```
18+
"""
19+
20+
STATUS_CODE_TEMPLATE = """
21+
**`Status code`** {code}
22+
"""
23+
1324
ERROR_TEMPLATE = """
1425
<details><summary><b>{hw_name} | {task_name} | {test_name}</b></summary>
1526
16-
**`stderr`**
17-
```apiblueprint
18-
{stderr}
19-
```
20-
21-
**`stdout`**
22-
```
23-
{stdout}
24-
```
27+
{entries}
2528
2629
--------
2730
@@ -120,10 +123,30 @@ def _add_error(
120123
if expired:
121124
self._errors += EXPIRED_TEMPLATE.format(hw_name=hw_name)
122125
return
126+
entries = STATUS_CODE_TEMPLATE.format(code=test_result.status)
127+
if test_result.output_mismatch:
128+
if test_result.output_mismatch.input:
129+
entries += ENTRY_TEMPLATE.format(
130+
name="Input",
131+
syntax="",
132+
content=test_result.output_mismatch.input,
133+
)
134+
entries += ENTRY_TEMPLATE.format(
135+
name="Output mismatch",
136+
syntax="diff",
137+
content=test_result.output_mismatch.diff(),
138+
)
139+
if test_result.stderr:
140+
entries += ENTRY_TEMPLATE.format(
141+
name="stderr", syntax="css", content=test_result.stderr
142+
)
143+
if test_result.stdout:
144+
entries += ENTRY_TEMPLATE.format(
145+
name="stdout", syntax="", content=test_result.stdout
146+
)
123147
self._errors += ERROR_TEMPLATE.format(
124148
hw_name=hw_name,
125149
task_name=task_name,
126150
test_name=test_name,
127-
stderr=test_result.stderr,
128-
stdout=test_result.stdout,
151+
entries=entries,
129152
)

homework_checker/core/tasks.py

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,6 @@
1313

1414
log = logging.getLogger("GHC")
1515

16-
17-
OUTPUT_MISMATCH_MESSAGE = """Given input: '{input}'
18-
Your output '{actual}'
19-
Expected output: '{expected}'"""
20-
2116
BUILD_SUCCESS_TAG = "Build succeeded"
2217
STYLE_ERROR_TAG = "Style errors"
2318

@@ -289,14 +284,24 @@ def _run_test(self: CppTask, test_node: dict, executable_folder: Path):
289284
our_output, error = tools.convert_to(self._output_type, run_result.stdout)
290285
if not our_output:
291286
# Conversion has failed.
292-
run_result.stderr = error
293-
return run_result
287+
return tools.CmdResult(
288+
status=tools.CmdResult.FAILURE,
289+
stdout=run_result.stdout,
290+
stderr=error,
291+
)
294292
expected_output, error = tools.convert_to(
295293
self._output_type, test_node[Tags.EXPECTED_OUTPUT_TAG]
296294
)
297295
if our_output != expected_output:
298-
run_result.stderr = OUTPUT_MISMATCH_MESSAGE.format(
299-
actual=our_output, input=input_str, expected=expected_output
296+
return tools.CmdResult(
297+
status=tools.CmdResult.FAILURE,
298+
stdout=run_result.stdout,
299+
stderr=run_result.stderr,
300+
output_mismatch=tools.OutputMismatch(
301+
input=input_str,
302+
expected_output=expected_output,
303+
actual_output=our_output,
304+
),
300305
)
301306
return run_result
302307

@@ -342,13 +347,23 @@ def _run_test(
342347
our_output, error = tools.convert_to(self._output_type, run_result.stdout)
343348
if not our_output:
344349
# Conversion has failed.
345-
run_result.stderr = error
346-
return run_result
350+
return tools.CmdResult(
351+
status=tools.CmdResult.FAILURE,
352+
stdout=run_result.stdout,
353+
stderr=error,
354+
)
347355
expected_output, error = tools.convert_to(
348356
self._output_type, test_node[Tags.EXPECTED_OUTPUT_TAG]
349357
)
350358
if our_output != expected_output:
351-
run_result.stderr = OUTPUT_MISMATCH_MESSAGE.format(
352-
actual=our_output, input=input_str, expected=expected_output
359+
return tools.CmdResult(
360+
status=tools.CmdResult.FAILURE,
361+
stdout=run_result.stdout,
362+
stderr=run_result.stderr,
363+
output_mismatch=tools.OutputMismatch(
364+
input=input_str,
365+
expected_output=expected_output,
366+
actual_output=our_output,
367+
),
353368
)
354369
return run_result

homework_checker/core/tests/data/homework/example_job.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,11 @@ homeworks:
5252
Another line
5353
test_me.sh
5454
- name: Test wrong output
55-
expected_output: Different output that doesn't match generated one
55+
expected_output: |
56+
Hello World!
57+
Expected non-matching line
58+
59+
test_me.sh
5660
- name: Test input piping
5761
language: cpp
5862
folder: task_5

homework_checker/core/tools.py

Lines changed: 80 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import datetime
1414
import signal
1515
import shutil
16+
import difflib
1617
import hashlib
1718

1819
from .schema_tags import OutputTags
@@ -138,32 +139,77 @@ def parse_git_url(git_url: str) -> Tuple[Optional[str], Optional[str], Optional[
138139
return domain, user, project
139140

140141

142+
class OutputMismatch:
143+
def __init__(self, input: str, expected_output: str, actual_output: str) -> None:
144+
"""Initialize the output mismatch class."""
145+
self._input = input
146+
self._expected_output = expected_output
147+
self._actual_output = actual_output
148+
149+
@property
150+
def input(self: OutputMismatch) -> str:
151+
"""Get input."""
152+
return self._input
153+
154+
@property
155+
def expected_output(self: OutputMismatch) -> str:
156+
"""Get expected output."""
157+
return self._expected_output
158+
159+
@property
160+
def actual_output(self: OutputMismatch) -> str:
161+
"""Get actual output."""
162+
return self._actual_output
163+
164+
def diff(self: OutputMismatch) -> str:
165+
actual = str(self._actual_output)
166+
expected = str(self._expected_output)
167+
diff = difflib.unified_diff(
168+
actual.split("\n"),
169+
expected.split("\n"),
170+
fromfile="Actual output",
171+
tofile="Expected output",
172+
)
173+
diff_str = ""
174+
for line in diff:
175+
diff_str += line + "\n"
176+
return diff_str
177+
178+
def __repr__(self: OutputMismatch) -> str:
179+
"""Representation of the output mismatch object."""
180+
return "input: {}, expected: {}, actual: {}".format(
181+
self._input, self._expected_output, self._actual_output
182+
)
183+
184+
141185
class CmdResult:
142186
"""A small container for command result."""
143187

144188
SUCCESS = 0
145189
FAILURE = 13
190+
TIMEOUT = 42
146191

147192
def __init__(
148-
self: CmdResult, returncode: int = None, stdout: str = None, stderr: str = None
193+
self: CmdResult,
194+
status: int,
195+
stdout: str = None,
196+
stderr: str = None,
197+
output_mismatch: OutputMismatch = None,
149198
):
150199
"""Initialize either stdout of stderr."""
151-
self._returncode = returncode
200+
self._status = status
152201
self._stdout = stdout
153202
self._stderr = stderr
203+
self._output_mismatch = output_mismatch
154204

155205
def succeeded(self: CmdResult) -> bool:
156206
"""Check if the command succeeded."""
157-
if self.returncode is not None:
158-
return self.returncode == CmdResult.SUCCESS
159-
if self.stderr:
160-
return False
161-
return True
207+
return self._status == CmdResult.SUCCESS
162208

163209
@property
164-
def returncode(self: CmdResult) -> Optional[int]:
165-
"""Get returncode."""
166-
return self._returncode
210+
def status(self: CmdResult) -> int:
211+
"""Get status."""
212+
return self._status
167213

168214
@property
169215
def stdout(self: CmdResult) -> Optional[str]:
@@ -175,24 +221,26 @@ def stderr(self: CmdResult) -> Optional[str]:
175221
"""Get stderr."""
176222
return self._stderr
177223

178-
@stderr.setter
179-
def stderr(self, value: str):
180-
self._returncode = None # We can't rely on returncode anymore
181-
self._stderr = value
224+
@property
225+
def output_mismatch(self: CmdResult) -> Optional[OutputMismatch]:
226+
"""Get output_mismatch."""
227+
return self._output_mismatch
182228

183229
@staticmethod
184230
def success() -> CmdResult:
185231
"""Return a cmd result that is a success."""
186-
return CmdResult(stdout="Success!")
232+
return CmdResult(status=CmdResult.SUCCESS)
187233

188234
def __repr__(self: CmdResult) -> str:
189-
"""Representatin of command result."""
190-
stdout = self.stdout
191-
if not stdout:
192-
stdout = ""
193-
if self.stderr:
194-
return "stdout: {}, stderr: {}".format(stdout.strip(), self.stderr.strip())
195-
return stdout.strip()
235+
"""Representation of command result."""
236+
repr = "status: {} ".format(self._status)
237+
if self._stdout:
238+
repr += "stdout: {} ".format(self._stdout)
239+
if self._stderr:
240+
repr += "stderr: {} ".format(self._stderr)
241+
if self._output_mismatch:
242+
repr += "output_mismatch: {}".format(self._output_mismatch)
243+
return repr.strip()
196244

197245

198246
def run_command(
@@ -228,21 +276,21 @@ def run_command(
228276
timeout=timeout,
229277
)
230278
return CmdResult(
231-
returncode=process.returncode,
279+
status=process.returncode,
232280
stdout=process.stdout.decode("utf-8"),
233281
stderr=process.stderr.decode("utf-8"),
234282
)
235283
except subprocess.CalledProcessError as error:
236284
output_text = error.output.decode("utf-8")
237-
log.error("command '%s' finished with code: %s", error.cmd, error.returncode)
285+
log.error("command '%s' finished with code: %s", error.cmd, error.status)
238286
log.debug("command output: \n%s", output_text)
239-
return CmdResult(returncode=error.returncode, stderr=output_text)
287+
return CmdResult(status=error.status, stderr=output_text)
240288
except subprocess.TimeoutExpired as error:
241289
output_text = "Timeout: command '{}' ran longer than {} seconds".format(
242290
error.cmd.strip(), error.timeout
243291
)
244292
log.error(output_text)
245-
return CmdResult(returncode=1, stderr=output_text)
293+
return CmdResult(status=CmdResult.TIMEOUT, stderr=output_text)
246294

247295

248296
def __run_subprocess(
@@ -281,11 +329,11 @@ def __run_subprocess(
281329
raise TimeoutExpired(
282330
process.args, timeout, output=stdout, stderr=stderr
283331
) from timeout_error
284-
retcode = process.poll()
285-
if retcode is None:
286-
retcode = 1
287-
if check and retcode:
332+
return_code = process.poll()
333+
if return_code is None:
334+
return_code = 1
335+
if check and return_code:
288336
raise CalledProcessError(
289-
retcode, process.args, output=stdout, stderr=stderr
337+
return_code, process.args, output=stdout, stderr=stderr
290338
)
291-
return CompletedProcess(process.args, retcode, stdout, stderr)
339+
return CompletedProcess(process.args, return_code, stdout, stderr)

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from setuptools import find_packages
77
from setuptools.command.install import install
88

9-
VERSION_STRING = "1.1.0"
9+
VERSION_STRING = "1.2.0"
1010

1111
PACKAGE_NAME = "homework_checker"
1212

0 commit comments

Comments
 (0)