Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tweak exception handling in problem statements #493

Merged
merged 1 commit into from
Jan 29, 2024
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
47 changes: 12 additions & 35 deletions tested/descriptions/renderer.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"""
A Marko renderer that only renders TESTed code; all other things are left alone.
"""
from doctest import DocTestParser

from marko import block
from marko.md_renderer import MarkdownRenderer
Expand All @@ -10,9 +9,7 @@
from tested.dsl import parse_dsl, parse_string
from tested.judge.evaluation import Channel, guess_expected_value, should_show
from tested.languages.generation import generate_statement, get_readable_input
from tested.testsuite import OutputChannel, Testcase

TESTED_EXAMPLE_FORMAT = "console?lang=tested"
from tested.testsuite import ExceptionOutputChannel, OutputChannel, Testcase


def render_one_statement(bundle: Bundle, statement: str) -> str:
Expand All @@ -31,6 +28,17 @@ def _add_output(
):
if should_show(output, channel):
expected = guess_expected_value(bundle, output)
# Special handling of exceptions
if channel == Channel.EXCEPTION:
assert isinstance(output, ExceptionOutputChannel)
if output.exception and not output.exception.get_type(
bundle.config.programming_language
):
name = bundle.language.get_declaration_metadata().get(
"exception", "Exception"
)
expected = f"{name}: {expected}"

results.append(expected)


Expand All @@ -48,35 +56,6 @@ def get_expected_output(bundle: Bundle, tc: Testcase) -> list[str]:

class TestedRenderer(MarkdownRenderer):
bundle: Bundle
_doctest_parser: DocTestParser

def __init__(self):
super().__init__()
self._doctest_parser = DocTestParser()

def _render_doctest(self, element: block.FencedCode) -> str:
"""
Render a "doctest" code block.
"""
assert element.lang == TESTED_EXAMPLE_FORMAT
raw_code = self.render_children(element)
doctests = self._doctest_parser.get_examples(raw_code)

resulting_lines = []
prompt = self.bundle.language.get_declaration_metadata().get("prompt", ">")

# Both the doctests and the results are parsed as values in the DSL.
for examples in doctests:
generated_statement = render_one_statement(self.bundle, examples.source)
resulting_lines.append(f"{prompt} {generated_statement}")
resulting_lines.append(examples.want.lstrip())

language = (
f"console?lang={self.bundle.config.programming_language}&prompt={prompt}"
)
body = "\n".join(resulting_lines)

return f"```{language}\n{body}```\n"

def _render_normal_statements(self, element: block.FencedCode) -> str:
"""
Expand Down Expand Up @@ -133,7 +112,5 @@ def render_fenced_code(self, element: block.FencedCode) -> str:
return self._render_normal_statements(element)
elif element.lang == "dsl":
return self._render_dsl_statements(element)
elif element.lang == TESTED_EXAMPLE_FORMAT:
return self._render_doctest(element)
else:
return super().render_fenced_code(element)
1 change: 1 addition & 0 deletions tested/languages/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ class TypeDeclarationMetadata(TypedDict):
nested_overrides: NotRequired[dict[AllTypes, tuple[str, str]]]
prompt: NotRequired[str]
natural_overrides: NotRequired[dict[str, dict[AllTypes, tuple[str, str]]]]
exception: NotRequired[str]


class Language(ABC):
Expand Down
3 changes: 2 additions & 1 deletion tested/languages/csharp/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,8 @@ def get_declaration_metadata(self) -> TypeDeclarationMetadata:
"list": "List",
"tuple": "Tuple",
"any": "Object",
}
},
"exception": "Exception",
}

def is_void_method(self, name: str) -> bool:
Expand Down
1 change: 1 addition & 0 deletions tested/languages/haskell/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,4 +209,5 @@ def get_declaration_metadata(self) -> TypeDeclarationMetadata:
"sequence": True,
},
"nested_overrides": {"tuple": ("(", ")")}, # type: ignore
"exception": "Exception",
}
1 change: 1 addition & 0 deletions tested/languages/java/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ def get_declaration_metadata(self) -> TypeDeclarationMetadata:
},
"nested": ("<", ">"),
"nested_overrides": {"array": ("[", "]")}, # type: ignore
"exception": "Exception",
}

def is_void_method(self, name: str) -> bool:
Expand Down
1 change: 1 addition & 0 deletions tested/languages/javascript/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,4 +235,5 @@ def get_declaration_metadata(self) -> TypeDeclarationMetadata:
"any": "object",
},
"nested": ("<", ">"),
"exception": "Error",
}
1 change: 1 addition & 0 deletions tested/languages/kotlin/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,4 +262,5 @@ def get_declaration_metadata(self) -> TypeDeclarationMetadata:
"any": "Any",
},
"nested": ("<", ">"),
"exception": "Exception",
}
1 change: 1 addition & 0 deletions tested/languages/python/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,4 +262,5 @@ def get_declaration_metadata(self) -> TypeDeclarationMetadata:
"any": "Any",
},
"prompt": ">>>",
"exception": "Exception",
}
4 changes: 0 additions & 4 deletions tests/descriptions/example.haskell.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,3 @@ aFunctionCall "yes"
```

This is haskell.

```console?lang=haskell&prompt=>
> aFunctionCall "yes"
```
4 changes: 0 additions & 4 deletions tests/descriptions/example.java.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,3 @@ Submission.aFunctionCall("yes")
```

This is java.

```console?lang=java&prompt=>
> Submission.aFunctionCall("yes")
```
4 changes: 0 additions & 4 deletions tests/descriptions/example.kotlin.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,3 @@ aFunctionCall("yes")
```

This is kotlin.

```console?lang=kotlin&prompt=>
> aFunctionCall("yes")
```
3 changes: 0 additions & 3 deletions tests/descriptions/example.md.jinja2
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,3 @@ This is java.
This is haskell.
{% endif %}

```console?lang=tested
>>> a_function_call("yes")
```
4 changes: 0 additions & 4 deletions tests/descriptions/example.python.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,3 @@ a_function_call('yes')
```

This is python.

```console?lang=python&prompt=>>>
>>> a_function_call('yes')
```
18 changes: 4 additions & 14 deletions tests/descriptions/recoupling.md.j2
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
Write a function `{{ function('divide') }}` that takes two arguments:
_i_) a word (`{{ datatype("text")}}`) and
_ii_) the number of (non-overlapping) groups $$n \\in \\mathbb{N}\_0$$ (`{{datatype("integer")}}`) into which the word must be divided.
If the word passed to the function `{{function('divide')}}` cannot be divided into $$n$$ groups that have the same length, an `AssertionError` must be raised with the message `invalid division`.
If the word passed to the function `{{function('divide')}}` cannot be divided into $$n$$ groups that have the same length, an exception must be raised with the message `invalid division`.
Otherwise, the function must return a {{datatype("list").singular}} (`{{datatype("list")}}`) containing the $$n$$ groups (`{{datatype("text")}}`) into which the given word can be divided.
All groups need to have the same length (same number of letters).

Write another function `{{function('recouple')}}` that takes two arguments:
_i_) a {{datatype("sequence").singular}} (`{{datatype("sequence")}}`) of $$m \\in \\mathbb{N}\_0$$ words (`{{datatype("text")}}`) and
_ii_) the number of (non-overlapping) groups $$n \\in \\mathbb{N}\_0$$ (`{{datatype("integer")}}`) into which the words must be divided.
If at least one of the words passed to the function `{{function('recouple')}}` cannot be divided into $$n$$ groups that have the same length, an `AssertionError` must be raised with the message `invalid division`.
If at least one of the words passed to the function `{{function('recouple')}}` cannot be divided into $$n$$ groups that have the same length, an exception must be raised with the message `invalid division`.
Otherwise, the function must return a {{datatype("sequence").singular}} containing the $$n$$ new words (`{{datatype("text")}}`) obtained when each of the $$m$$ given words is divided into $$n$$ groups that have the same length, and if each of the $$m$$ corresponding groups is merged into a new word.
The type of the returned {{datatype("sequence").singular}} (`{{datatype("sequence")}}`) must correspond to the type of the {{datatype("sequence").singular}} passed as a first argument to the function.

Expand All @@ -23,23 +23,13 @@ units:
- expression: "divide('COMMUNED', 4)"
return: ["CO", "MM", "UN", "ED"]
- expression: "divide('programming', 5)"
exception:
message: "invalid division"
types:
python: AssertionError
javascript: AssertionError
java: IllegalArgumentException
exception: "invalid division"
- unit: "Recouple"
scripts:
- expression: "recouple(['ACcoST', 'COmmIT', 'LAunCH', 'DEedED'], 3)"
return: !list ["ACCOLADE", "communed", "STITCHED"]
- expression: "recouple(('ACCOLADE', 'communed', 'STITCHED'), 4)"
return: !tuple ["ACcoST", "COmmIT", "LAunCH", "DEedED"]
- expression: "recouple(['programming', 'computer', 'games'], 5)"
exception:
message: "invalid division"
types:
python: AssertionError
javascript: AssertionError
java: IllegalArgumentException
exception: "invalid division"
```
8 changes: 4 additions & 4 deletions tests/descriptions/recoupling.python.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
Write a function `divide` that takes two arguments:
*i*) a word (`str`) and
*ii*) the number of (non-overlapping) groups $$n \\in \\mathbb{N}\_0$$ (`int`) into which the word must be divided.
If the word passed to the function `divide` cannot be divided into $$n$$ groups that have the same length, an `AssertionError` must be raised with the message `invalid division`.
If the word passed to the function `divide` cannot be divided into $$n$$ groups that have the same length, an exception must be raised with the message `invalid division`.
Otherwise, the function must return a list (`list`) containing the $$n$$ groups (`str`) into which the given word can be divided.
All groups need to have the same length (same number of letters).

Write another function `recouple` that takes two arguments:
*i*) a sequence (`list` or `tuple`) of $$m \\in \\mathbb{N}\_0$$ words (`str`) and
*ii*) the number of (non-overlapping) groups $$n \\in \\mathbb{N}\_0$$ (`int`) into which the words must be divided.
If at least one of the words passed to the function `recouple` cannot be divided into $$n$$ groups that have the same length, an `AssertionError` must be raised with the message `invalid division`.
If at least one of the words passed to the function `recouple` cannot be divided into $$n$$ groups that have the same length, an exception must be raised with the message `invalid division`.
Otherwise, the function must return a sequence containing the $$n$$ new words (`str`) obtained when each of the $$m$$ given words is divided into $$n$$ groups that have the same length, and if each of the $$m$$ corresponding groups is merged into a new word.
The type of the returned sequence (`list` or `tuple`) must correspond to the type of the sequence passed as a first argument to the function.

Expand All @@ -20,12 +20,12 @@ The type of the returned sequence (`list` or `tuple`) must correspond to the typ
>>> divide('COMMUNED', 4)
['CO', 'MM', 'UN', 'ED']
>>> divide('programming', 5)
AssertionError: invalid division
Exception: invalid division

>>> recouple(['ACcoST', 'COmmIT', 'LAunCH', 'DEedED'], 3)
['ACCOLADE', 'communed', 'STITCHED']
>>> recouple(('ACCOLADE', 'communed', 'STITCHED'), 4)
('ACcoST', 'COmmIT', 'LAunCH', 'DEedED')
>>> recouple(['programming', 'computer', 'games'], 5)
AssertionError: invalid division
Exception: invalid division
```
92 changes: 2 additions & 90 deletions tests/test_problem_statements.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@


@pytest.mark.parametrize("language", ["python", "kotlin", "java", "haskell"])
def test_python_description(language: str):
def test_small_descriptions(language: str):
test_dir = Path(__file__).parent
description_template = test_dir / "descriptions" / "example.md.jinja2"
description_python = test_dir / "descriptions" / f"example.{language}.md"
Expand All @@ -18,7 +18,7 @@ def test_python_description(language: str):
expected = dp.read()
actual = process_problem_statement(template, language)

assert actual == expected
assert actual.strip() == expected.strip()
assert f"This is {language}." in actual


Expand Down Expand Up @@ -130,38 +130,6 @@ def test_template_natural_type_name_nl(lang: str, tested_type: Any, expected: st
assert instance == f"{expected}"


@pytest.mark.parametrize(
("lang", "prompt"),
[
("python", ">>>"),
("java", ">"),
("c", ">"),
("kotlin", ">"),
("javascript", ">"),
("haskell", ">"),
],
)
def test_template_code_block_markdown(lang: str, prompt: str):
template = """```console?lang=tested
>>> random()
5
```"""
expected_stmt = (
"random "
if lang == "haskell"
else "Submission.random()"
if lang == "java"
else "random()"
)
expected_expr = "5"
instance = process_problem_statement(template, lang)
expected = f"""```console?lang={lang}&prompt={prompt}
{prompt} {expected_stmt}
{expected_expr}
```"""
assert instance == expected


@pytest.mark.parametrize(
("lang", "expected"),
[
Expand Down Expand Up @@ -227,62 +195,6 @@ def test_template_failed_string():
process_problem_statement(template, "java")


def test_multiline_results():
template = """
```console?lang=tested
>>> dots("paper.txt")
###..###...##..#..#.####.###..#....###.
#..#.#..#.#..#.#.#..#....#..#.#....#..#
#..#.#..#.#....##...###..###..#....#..#
###..###..#....#.#..#....#..#.#....###.
#.#..#....#..#.#.#..#....#..#.#....#.#.
#..#.#.....##..#..#.#....###..####.#..#
```
"""
actual = process_problem_statement(template, "javascript")
expected = """
```console?lang=javascript&prompt=>
> dots("paper.txt")
###..###...##..#..#.####.###..#....###.
#..#.#..#.#..#.#.#..#....#..#.#....#..#
#..#.#..#.#....##...###..###..#....#..#
###..###..#....#.#..#....#..#.#....###.
#.#..#....#..#.#.#..#....#..#.#....#.#.
#..#.#.....##..#..#.#....###..####.#..#
```
"""
assert actual == expected


def test_multiline_statement():
template = """
```console?lang=tested
>>> dots(
... "paper.txt"
... )
###..###...##..#..#.####.###..#....###.
#..#.#..#.#..#.#.#..#....#..#.#....#..#
#..#.#..#.#....##...###..###..#....#..#
###..###..#....#.#..#....#..#.#....###.
#.#..#....#..#.#.#..#....#..#.#....#.#.
#..#.#.....##..#..#.#....###..####.#..#
```
"""
actual = process_problem_statement(template, "javascript")
expected = """
```console?lang=javascript&prompt=>
> dots("paper.txt")
###..###...##..#..#.####.###..#....###.
#..#.#..#.#..#.#.#..#....#..#.#....#..#
#..#.#..#.#....##...###..###..#....#..#
###..###..#....#.#..#....#..#.#....###.
#.#..#....#..#.#.#..#....#..#.#....#.#.
#..#.#.....##..#..#.#....###..####.#..#
```
"""
assert actual == expected


def test_long_description():
test_dir = Path(__file__).parent
description_template = test_dir / "descriptions" / "recoupling.md.j2"
Expand Down
Loading