Skip to content

Commit

Permalink
Merge pull request #555 from dodona-edu/feat/add-comments
Browse files Browse the repository at this point in the history
Feat/add comments
  • Loading branch information
jorg-vr authored Dec 4, 2024
2 parents c018623 + 2147cf2 commit 32f961a
Show file tree
Hide file tree
Showing 18 changed files with 150 additions and 2 deletions.
22 changes: 22 additions & 0 deletions tested/dsl/ast_translator.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@
"""

import ast
import collections
import io
import tokenize
from decimal import Decimal
from typing import Literal, cast, overload

Expand Down Expand Up @@ -333,6 +336,25 @@ def _translate_to_ast(node: ast.Interactive, is_return: bool) -> Statement:
return _convert_statement(statement_or_expression)


def extract_comment(code: str) -> str:
"""
Extract the comment from the code.
:param code: The code to extract the comment from.
:return: The comment if it exists, otherwise an empty string.
"""
comment = ""
tokens = tokenize.generate_tokens(io.StringIO(code).readline)
# The "maxlen" is 3 because, the tokenizer will always generate a NEWLINE
# and ENDMARKER token at the end. So a comment comes just before those 2 tokens.
candidates = collections.deque(tokens, maxlen=3)
if len(candidates):
candidate = candidates.popleft()
if candidate.type == tokenize.COMMENT:
comment = candidate.string.lstrip("#").strip()
return comment


@overload
def parse_string(code: str, is_return: Literal[True]) -> Value: ...

Expand Down
6 changes: 5 additions & 1 deletion tested/dsl/translate_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
resolve_to_basic,
)
from tested.dodona import ExtendedMessage
from tested.dsl.ast_translator import InvalidDslError, parse_string
from tested.dsl.ast_translator import InvalidDslError, extract_comment, parse_string
from tested.parsing import get_converter, suite_to_json
from tested.serialisation import (
BooleanType,
Expand Down Expand Up @@ -567,6 +567,7 @@ def _convert_testcase(testcase: YamlDict, context: DslContext) -> Testcase:
if "statement" in testcase and "return" in testcase:
testcase["expression"] = testcase.pop("statement")

line_comment = ""
_validate_testcase_combinations(testcase)
if (expr_stmt := testcase.get("statement", testcase.get("expression"))) is not None:
if isinstance(expr_stmt, dict) or context.language != "tested":
Expand All @@ -583,6 +584,8 @@ def _convert_testcase(testcase: YamlDict, context: DslContext) -> Testcase:
the_input = LanguageLiterals(literals=the_dict, type=the_type)
else:
assert isinstance(expr_stmt, str)
if testcase.get("description") is None:
line_comment = extract_comment(expr_stmt)
the_input = parse_string(expr_stmt)
return_channel = IgnoredChannel.IGNORED if "statement" in testcase else None
else:
Expand Down Expand Up @@ -653,6 +656,7 @@ def _convert_testcase(testcase: YamlDict, context: DslContext) -> Testcase:
input=the_input,
output=output,
link_files=context.files,
line_comment=line_comment,
)


Expand Down
3 changes: 3 additions & 0 deletions tested/languages/bash/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ def datatype_support(self) -> dict[AllTypes, TypeSupport]:
def file_extension(self) -> str:
return "sh"

def comment(self, text: str) -> str:
return f"# {text}"

def initial_dependencies(self) -> list[str]:
return []

Expand Down
3 changes: 3 additions & 0 deletions tested/languages/c/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ def needs_selector(self):
def file_extension(self) -> str:
return "c"

def comment(self, text: str) -> str:
return f"// {text}"

def naming_conventions(self) -> dict[Conventionable, NamingConventions]:
return {"global_identifier": "macro_case"}

Expand Down
3 changes: 3 additions & 0 deletions tested/languages/csharp/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ def needs_selector(self):
def file_extension(self) -> str:
return "cs"

def comment(self, text: str) -> str:
return f"// {text}"

def naming_conventions(self) -> dict[Conventionable, NamingConventions]:
return {
"namespace": "pascal_case",
Expand Down
6 changes: 6 additions & 0 deletions tested/languages/generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,11 +146,17 @@ def get_readable_input(
format_ = bundle.config.programming_language
text = generate_statement(bundle, case.input)
text = bundle.language.cleanup_description(text)

if case.line_comment:
text = f"{text} {bundle.language.comment(case.line_comment)}"
else:
assert isinstance(case.input, LanguageLiterals)
text = case.input.get_for(bundle.config.programming_language)
format_ = bundle.config.programming_language

if case.line_comment:
text = f"{text} {bundle.language.comment(case.line_comment)}"

# If there are no files, return now. This means we don't need to do ugly stuff.
if not case.link_files:
return ExtendedMessage(description=text, format=format_), set()
Expand Down
3 changes: 3 additions & 0 deletions tested/languages/haskell/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ def needs_selector(self):
def file_extension(self) -> str:
return "hs"

def comment(self, text: str) -> str:
return f"-- {text}"

def naming_conventions(self) -> dict[Conventionable, NamingConventions]:
return {
"namespace": "pascal_case",
Expand Down
3 changes: 3 additions & 0 deletions tested/languages/java/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ def needs_selector(self):
def file_extension(self) -> str:
return "java"

def comment(self, text: str) -> str:
return f"// {text}"

def naming_conventions(self) -> dict[Conventionable, NamingConventions]:
return {
"namespace": "pascal_case",
Expand Down
3 changes: 3 additions & 0 deletions tested/languages/javascript/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ def needs_selector(self):
def file_extension(self) -> str:
return "js"

def comment(self, text: str) -> str:
return f"// {text}"

def naming_conventions(self) -> dict[Conventionable, NamingConventions]:
return {
"namespace": "camel_case",
Expand Down
3 changes: 3 additions & 0 deletions tested/languages/kotlin/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ def needs_selector(self):
def file_extension(self) -> str:
return "kt"

def comment(self, text: str) -> str:
return f"// {text}"

def naming_conventions(self) -> dict[Conventionable, NamingConventions]:
return {
"namespace": "pascal_case",
Expand Down
10 changes: 10 additions & 0 deletions tested/languages/language.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,16 @@ def file_extension(self) -> str:
"""
raise NotImplementedError

@abstractmethod
def comment(self, text: str) -> str:
"""
Generate a comment for the given text.
:param text: The text to comment.
:return: The comment.
"""
raise NotImplementedError

def is_source_file(self, file: Path) -> bool:
"""
Check if the given file could be a source file for this programming language.
Expand Down
3 changes: 3 additions & 0 deletions tested/languages/python/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ def supports_debug_information(self) -> bool:
def file_extension(self) -> str:
return "py"

def comment(self, text: str) -> str:
return f"# {text}"

def naming_conventions(self) -> dict[Conventionable, NamingConventions]:
return {"class": "pascal_case", "global_identifier": "macro_case"}

Expand Down
3 changes: 3 additions & 0 deletions tested/languages/typescript/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ def needs_selector(self) -> bool:
def file_extension(self) -> str:
return "ts"

def comment(self, text: str) -> str:
return f"// {text}"

def naming_conventions(self) -> dict[Conventionable, NamingConventions]:
return {
"namespace": "camel_case",
Expand Down
1 change: 1 addition & 0 deletions tested/testsuite.py
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,7 @@ class Testcase(WithFeatures, WithFunctions):
description: Message | None = None
output: Output = field(factory=Output)
link_files: list[FileUrl] = field(factory=list)
line_comment: str = ""

def get_used_features(self) -> FeatureSet:
return combine_features(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
- tab: "Global variable"
testcases:
- expression: "GLOBAL_VAR # The name of the global variable"
return: "GLOBAL"
description:
description: "Hallo # This is a greeting"
format: "code"
2 changes: 1 addition & 1 deletion tests/exercises/global/evaluation/plan.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
- tab: "Global variable"
testcases:
- expression: "GLOBAL_VAR"
- expression: "GLOBAL_VAR # The name of the global variable"
return: "GLOBAL"
description:
description: "Hallo"
Expand Down
4 changes: 4 additions & 0 deletions tests/exercises/global/evaluation/plan_no_description.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
- tab: "Global variable"
testcases:
- expression: "GLOBAL_VAR # The name of the global variable"
return: "GLOBAL"
67 changes: 67 additions & 0 deletions tests/test_functionality.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,73 @@ def test_global_variable_yaml(
assert updates.find_status_enum() == ["correct"]


@pytest.mark.parametrize(
"language, comment_start",
[
("bash", "#"),
("python", "#"),
("kotlin", "//"),
("csharp", "//"),
("java", "//"),
("c", "//"),
("javascript", "//"),
("haskell", "--"),
],
)
def test_global_comment(
language: str, comment_start: str, tmp_path: Path, pytestconfig: pytest.Config
):
conf = configuration(
pytestconfig,
"global",
language,
tmp_path,
"plan_no_description.yaml",
"correct",
)
result = execute_config(conf)
updates = assert_valid_output(result, pytestconfig)
description = updates.find_next("start-testcase")

assert "description" in description and "description" in description["description"]
assert description["description"]["description"].endswith(
f"{comment_start} The name of the global variable"
)


@pytest.mark.parametrize("language", ALL_LANGUAGES)
def test_global_no_comment(language: str, tmp_path: Path, pytestconfig: pytest.Config):
conf = configuration(
pytestconfig, "global", language, tmp_path, "plan.yaml", "correct"
)
result = execute_config(conf)
updates = assert_valid_output(result, pytestconfig)
description = updates.find_next("start-testcase")

assert "description" in description and "description" in description["description"]
assert description["description"]["description"] == "Hallo"


@pytest.mark.parametrize("language", ALL_LANGUAGES)
def test_global_comment_description(
language: str, tmp_path: Path, pytestconfig: pytest.Config
):
conf = configuration(
pytestconfig,
"global",
language,
tmp_path,
"comment_description_plan.yaml",
"correct",
)
result = execute_config(conf)
updates = assert_valid_output(result, pytestconfig)
description = updates.find_next("start-testcase")

assert "description" in description and "description" in description["description"]
assert description["description"]["description"] == "Hallo # This is a greeting"


@pytest.mark.parametrize("lang", EXCEPTION_LANGUAGES)
def test_generic_exception_wrong(
lang: str, tmp_path: Path, pytestconfig: pytest.Config
Expand Down

0 comments on commit 32f961a

Please sign in to comment.