From ea74d48d6eddd85b73147c6fd064fe6c35f6592e Mon Sep 17 00:00:00 2001 From: Niko Strijbol Date: Tue, 8 Aug 2023 23:36:07 +0200 Subject: [PATCH] Rework problem statements --- Pipfile | 7 +- flake.nix | 28 +- tested/description_instance.py | 320 ------------- tested/descriptions/__init__.py | 44 ++ tested/descriptions/__main__.py | 93 ++++ tested/descriptions/converters.py | 83 ++++ tested/descriptions/renderer.py | 60 +++ tested/dsl/ast_translator.py | 1 - tested/dsl/translate_parser.py | 1 - tested/instantiate_exercise.py | 558 ---------------------- tested/internationalization/en.yaml | 55 +++ tested/internationalization/nl.yaml | 55 +++ tested/languages/bash/config.py | 16 +- tested/languages/bash/types.json | 35 -- tested/languages/c/config.py | 32 +- tested/languages/c/types.json | 117 ----- tested/languages/config.py | 51 +- tested/languages/csharp/config.py | 36 +- tested/languages/csharp/types.json | 159 ------ tested/languages/description_generator.py | 222 --------- tested/languages/generation.py | 82 +++- tested/languages/haskell/config.py | 36 +- tested/languages/haskell/types.json | 153 ------ tested/languages/java/config.py | 56 ++- tested/languages/java/types.json | 173 ------- tested/languages/javascript/config.py | 40 +- tested/languages/javascript/types.json | 156 ------ tested/languages/kotlin/config.py | 40 +- tested/languages/kotlin/types.json | 156 ------ tested/languages/python/config.py | 41 +- tested/languages/python/types.json | 33 -- tested/languages/runhaskell/config.py | 8 - tests/descriptions/example.haskell.md | 12 + tests/descriptions/example.java.md | 12 + tests/descriptions/example.kotlin.md | 12 + tests/descriptions/example.md.mako | 20 + tests/descriptions/example.python.md | 12 + tests/test_problem_statements.py | 243 ++++++++++ tests/test_serialisation.py | 9 + tests/test_utils.py | 361 +------------- 40 files changed, 1144 insertions(+), 2484 deletions(-) delete mode 100644 tested/description_instance.py create mode 100644 tested/descriptions/__init__.py create mode 100644 tested/descriptions/__main__.py create mode 100644 tested/descriptions/converters.py create mode 100644 tested/descriptions/renderer.py delete mode 100644 tested/instantiate_exercise.py delete mode 100644 tested/languages/bash/types.json delete mode 100644 tested/languages/c/types.json delete mode 100644 tested/languages/csharp/types.json delete mode 100644 tested/languages/description_generator.py delete mode 100644 tested/languages/haskell/types.json delete mode 100644 tested/languages/java/types.json delete mode 100644 tested/languages/javascript/types.json delete mode 100644 tested/languages/kotlin/types.json create mode 100644 tests/descriptions/example.haskell.md create mode 100644 tests/descriptions/example.java.md create mode 100644 tests/descriptions/example.kotlin.md create mode 100644 tests/descriptions/example.md.mako create mode 100644 tests/descriptions/example.python.md create mode 100644 tests/test_problem_statements.py diff --git a/Pipfile b/Pipfile index d2a92435..a10cc867 100644 --- a/Pipfile +++ b/Pipfile @@ -18,9 +18,12 @@ pylint = "==2.17.1" pytest = "" pytest-mock = "" pytest-cov = "" -# Still needed for instantiating exercise descriptions. + +# Needed for exercise instantiation +# You'll need to install these if you want to use it. # TODO: should we get rid of this? -mako = "==1.1.6" +mako = "" +marko = "" [requires] python_version = "3.11" diff --git a/flake.nix b/flake.nix index bc051b36..07807204 100644 --- a/flake.nix +++ b/flake.nix @@ -61,15 +61,39 @@ maintainers = [ ]; }; }; + marko = python.pkgs.buildPythonPackage rec { + pname = "marko"; + version = "2.0.0"; + format = "pyproject"; + + src = pkgs.fetchPypi { + inherit pname version; + hash = "sha256-78JkYIkyUME3UQJa6SAuuxOJiHA2/A35AJxquHVGcDA="; + }; + + nativeBuildInputs = [ + python.pkgs.pdm-pep517 python.pkgs.pdm-backend + ]; + + doCheck = false; + + meta = with pkgs.lib; { + homepage = "https://github.com/frostming/marko"; + license = licenses.mit; + maintainers = [ ]; + }; + }; core-packages = ps: with ps; [ psutil - mako my-pydantic jsonschema typing-inspect pyyaml pygments - python-i18n + python-i18n + # For scripts, not judge itself. + mako + marko ]; python-env = python.withPackages(ps: (core-packages ps) ++ [ ps.pylint diff --git a/tested/description_instance.py b/tested/description_instance.py deleted file mode 100644 index 85d3b079..00000000 --- a/tested/description_instance.py +++ /dev/null @@ -1,320 +0,0 @@ -import html -import os -import re -import sys -from argparse import ArgumentParser, FileType -from functools import partial -from typing import List - -from mako.template import Template - -from tested.configs import Bundle, DodonaConfig, GlobalConfig -from tested.languages import get_language, language_exists -from tested.languages.conventionalize import conventionalize_namespace -from tested.languages.description_generator import TYPE_ARG, TYPE_CONFIG_NAME -from tested.testsuite import Suite -from tested.utils import smart_close - -open_brackets = ("(", "[", "{") -close_brackets = {")": "(", "]": "[", "}": "{"} - -natural_languages = {"en": "English", "nl": "Nederlands"} - - -def _mako_uncomment(m: re.Match) -> str: - result = f"${{{repr(m.group(1))}}}" - return result - - -def _analyse_body(body: str) -> str: - expressions, same, html_tag, stack = [], False, False, [] - for line in body.splitlines(keepends=False): - line = line.lstrip() - new_same = line and line[-1] == "\\" - line = line[:-1] if new_same else line - if same or stack: - expressions[-1] += " " + line - else: - expressions.append(line) - stack = _analyse_line(line, stack) - same = new_same - - if stack: - raise ValueError("Statement or expression brackets are not balanced") - - for index, expr in enumerate(expressions): - if not expr: - continue - elif expr[0] == ">": - expressions[index] = f"${{statement({repr(expr[1:].strip())})}}" - else: - expressions[index] = f"${{expression({repr(expr.strip())})}}" - - return "\n".join(expressions) + ("" if same else "\n") - - -def _analyse_line(line: str, stack: List[str]) -> List[str]: - is_string, escape = False, False - for c in line: - if is_string: - if c == "\\": - escape = True - elif c == '"': - if not escape: - is_string = False - else: - escape = False - elif c == '"': - is_string = True - elif c in open_brackets: - stack.append(c) - elif c in close_brackets: - try: - if close_brackets[c] != stack[-1]: - raise ValueError( - "Statement or expression brackets are not balanced" - ) - else: - stack.pop() - except IndexError: - raise ValueError("Statement or expression brackets are not balanced") - else: - continue - if is_string: - raise ValueError("String is not closed on this line") - return stack - - -def prepare_template(template: str, is_html: bool = True) -> Template: - if is_html: - re_tag = "(\"[^\"]*\"|'[^']*'|[^'\">])*" - - regex_code, body_index = ( - re.compile( - rf"(()(\s*\\?))((?!).*?)()", - re.MULTILINE | re.DOTALL, - ), - -3, - ) - else: - regex_code, body_index = ( - re.compile(r"^(```tested\r?\n)(((?!```).*\r?\n)*)(```)$", re.MULTILINE), - 1, - ) - - regex_comment_mako = re.compile(r"(?m)^(\s*#.*)$") - - last_end, mako_template = 0, [] - - for match in regex_code.finditer(template): - groups = match.groups() - span = match.span(0) - mako_template.extend( - regex_comment_mako.sub(_mako_uncomment, template[last_end : span[0]]) - ) - last_end = span[1] - if is_html: - mako_template.extend(groups[0]) - else: - mako_template.extend( - "```console?lang=${programming_language_raw}&prompt=${prompt}\n" - ) - mako_template.extend(_analyse_body(groups[body_index])) - mako_template.extend(groups[-1]) - - mako_template.extend(regex_comment_mako.sub(_mako_uncomment, template[last_end:])) - mako_template = "".join(mako_template) - return Template(mako_template, cache_enabled=False) - - -def create_description_instance_from_template( - template: Template, - programming_language: str = "python", - natural_language: str = "en", - namespace: str = "submission", - is_html: bool = True, -) -> str: - from pathlib import Path - - judge_directory = Path(__file__).parent.parent - global_config = GlobalConfig( - dodona=DodonaConfig( - resources="", # type: ignore - source="", # type: ignore - time_limit=0, - memory_limit=0, - natural_language=natural_language, - programming_language=programming_language, - workdir="", # type: ignore - judge=judge_directory, - test_suite="suite.yaml", - ), - context_separator_secret="", - testcase_separator_secret="", - suite=Suite(namespace=namespace), - ) - language = get_language(global_config, programming_language) - - bundle = Bundle( - global_config=global_config, - lang_config=language, - out=open(os.devnull, "w"), - ) - - description_generator = language.get_description_generator() - - # Partial function doesn't work because of bundle must be given, - # but custom_type_map not - def get_type_name(args: TYPE_ARG, custom_type_map: TYPE_CONFIG_NAME = None) -> str: - return description_generator.get_type_name( - args, bundle, custom_type_map, is_html=is_html - ) - - def get_natural_type_name(type_name: str, plural: bool = False): - return description_generator.get_natural_type_name( - type_name, bundle, plural, is_html - ) - - def get_variable(var_name: str, is_global: bool = True): - if is_global: - return description_generator.get_global_variable_name(var_name, is_html) - return description_generator.get_variable_name(var_name, is_html) - - namespace = conventionalize_namespace(language, namespace) - if is_html: - namespace = html.escape(namespace) - - return template.render( # type: ignore - function=partial(description_generator.get_function_name, is_html=is_html), - property=partial(description_generator.get_property_name, is_html=is_html), - variable=get_variable, - datatype_common=get_natural_type_name, - datatype=get_type_name, - statement=partial( - description_generator.get_code, - bundle=bundle, - is_html=is_html, - statement=True, - ), - expression=partial( - description_generator.get_code, - bundle=bundle, - is_html=is_html, - statement=False, - ), - prompt=description_generator.get_prompt(is_html=is_html), - programming_language=description_generator.get_prompt_language(is_html=is_html), - programming_language_raw=description_generator.get_prompt_language( - is_html=False - ), - namespace=conventionalize_namespace(language, namespace), - natural_language=natural_languages.get(natural_language, natural_language), - natural_language_iso639=natural_language, - ) - - -def create_description_instance( - template: str, - programming_language: str = "python", - natural_language: str = "en", - namespace: str = "submission", - is_html: bool = True, -) -> str: - if not language_exists(programming_language): - raise ValueError(f"Language {programming_language} doesn't exists") - - template = prepare_template(template, is_html) # type: ignore - return create_description_instance_from_template( - template, programming_language, natural_language, namespace, is_html # type: ignore - ) - - -if __name__ == "__main__": - parser = ArgumentParser( - description="Translate description for language", - usage="%(prog)s [-h] [-d TEMPLATE] [-o INSTANCE] [-l PROGRAMMING_LANGUAGE] " - "[-i NATURAL_LANGUAGE] [-n NAMESPACE] [(-M | -H)] " - "[template [instance]]", - ) - parser.add_argument( - "-d", - "--description", - type=FileType("r"), - help="Description template", - default="-", - ) - parser.add_argument( - "-l", "--language", type=str, help="Programming language", default="python" - ) - parser.add_argument( - "-o", - "--output", - type=FileType("w"), - help="Output description instance", - default="-", - ) - parser.add_argument( - "-i", - "--i18n", - type=str, - help="Natural language (en, nl), default: en", - default="en", - ) - parser.add_argument( - "-n", - "--namespace", - type=str, - help="Namespace of the submission", - default="submission", - ) - - type_group = parser.add_mutually_exclusive_group() - type_group.add_argument( - "-M", "--markdown", action="store_true", help="Generate markdown" - ) - type_group.add_argument( - "-H", "--html", action="store_true", help="Generate html (default)" - ) - - parser.add_argument( - "template", - metavar="template", - nargs="?", - type=FileType("r"), - help="Where the template should be read from, override " "option when given.", - ) - parser.add_argument( - "instance", - metavar="instance", - nargs="?", - type=FileType("w"), - help="Where the translate instance should be written to, " - "override option when given.", - ) - - parser = parser.parse_args() - if parser.template is not None: - smart_close(parser.description) - parser.description = parser.template - if parser.instance is not None: - smart_close(parser.output) - parser.output = parser.instance - - with smart_close(parser.description) as template_fd: - template_str = template_fd.read() - - try: - rendered = create_description_instance( - template_str, - parser.language, - parser.i18n, - parser.namespace, - not bool(parser.markdown), - ) - except ValueError as e: - print(e, file=sys.stderr) - sys.exit(-1) - - with smart_close(parser.output) as output_fd: - print(rendered, file=output_fd) diff --git a/tested/descriptions/__init__.py b/tested/descriptions/__init__.py new file mode 100644 index 00000000..5d5adcc9 --- /dev/null +++ b/tested/descriptions/__init__.py @@ -0,0 +1,44 @@ +import os +from pathlib import Path + +from tested.configs import Bundle, DodonaConfig, GlobalConfig +from tested.descriptions.converters import convert_mako_problem, convert_tested_markdown +from tested.languages import get_language +from tested.testsuite import Suite + + +def process_problem_statement( + problem_statement: str, programming_language: str, natural_language: str = "en" +) -> str: + judge_directory = Path(__file__).parent.parent + global_config = GlobalConfig( + dodona=DodonaConfig( + resources=Path(), + source=Path(), + time_limit=0, + memory_limit=0, + natural_language=natural_language, + programming_language=programming_language, + workdir=Path(), + judge=judge_directory, + ), + context_separator_secret="", + testcase_separator_secret="", + suite=Suite(tabs=[]), + ) + + language = get_language(global_config, global_config.dodona.programming_language) + + bundle = Bundle( + global_config=global_config, + lang_config=language, + out=open(os.devnull, "w"), + ) + + tested_markdown = convert_mako_problem(bundle, problem_statement) + normal_markdown = convert_tested_markdown(bundle, tested_markdown) + + if not tested_markdown.endswith("\n"): + normal_markdown = normal_markdown.rstrip("\n") + + return normal_markdown diff --git a/tested/descriptions/__main__.py b/tested/descriptions/__main__.py new file mode 100644 index 00000000..01595c27 --- /dev/null +++ b/tested/descriptions/__main__.py @@ -0,0 +1,93 @@ +""" +Script to convert a "generic" Markdown problem statement into a language-specific +problem statement. + +A generic problem statement has two special aspects: + +1. The problem statement is a Mako template, so conditionals are possible. + See the section below for information on some available variables. +2. Code blocks in Markdown that are annotated with the language `tested` will + be converted into the programming language. + + +== Mako templates == + +The following variables are available: + +- `programming_language`: name of the programming language (e.g. `java`) + +To convert a name of something (e.g. a class) to the conventions of the specific +programming language, the following functions are available: `namespace`, `function`, +`identifier`, `property`, `clazz`, `global_identifier`. + +Finally, there are two functions that convert "TESTed" data types to either the type +in the programming language (e.g. "sequence" becomes "list" in Python"). You can use +the function `datatype` for this. + +== Code blocks == + +All code blocks that are marked as `tested` will be treated as DSL expressions and +statements. They can thus use the Python-like syntax. See the docs on the DSL for +more information. + +== Other == + +Note that the file is first converted by Mako to a normal Markdown file. +Afterwards, the code blocks are replaced. +""" +import sys +from argparse import ArgumentParser, FileType + +from tested.descriptions import process_problem_statement +from tested.languages import LANGUAGES +from tested.utils import smart_close + +parser = ArgumentParser( + description="Convert generic problem statements to language-specific ones" +) + +parser.add_argument( + "-p", + "--problem", + type=FileType("r"), + help="Generic problem statement", + default="-", +) +parser.add_argument( + "-o", + "--output", + type=FileType("w"), + help="Language-specific problem statement", + default="-", +) +parser.add_argument( + "-l", + "--language", + type=str, + help="Programming language to convert the generic problem statement into.", + choices=LANGUAGES.keys(), +) +parser.add_argument( + "-n", + "--natural_language", + type=str, + help="The natural language for the names of data types.", + default="en", +) + +parser = parser.parse_args() + +try: + with smart_close(parser.problem) as problem_fd: + problem_statement = problem_fd.read() + + result = process_problem_statement( + problem_statement, parser.language, parser.natural_language + ) + + with smart_close(parser.output) as output_fd: + print(result, file=output_fd) + +except ValueError as e: + print(e, file=sys.stderr) + sys.exit(-1) diff --git a/tested/descriptions/converters.py b/tested/descriptions/converters.py new file mode 100644 index 00000000..919f0ef5 --- /dev/null +++ b/tested/descriptions/converters.py @@ -0,0 +1,83 @@ +from functools import partial +from typing import cast + +from mako.template import Template +from marko import Markdown + +from tested.configs import Bundle +from tested.datatypes import AllTypes +from tested.descriptions.renderer import TestedRenderer, render_one_statement +from tested.internationalization import get_i18n_string, set_locale +from tested.languages import Language +from tested.languages.conventionalize import ( + conventionalize_class, + conventionalize_function, + conventionalize_global_identifier, + conventionalize_identifier, + conventionalize_namespace, + conventionalize_property, +) +from tested.languages.generation import NestedTypeDeclaration, generate_type_declaration + + +def type_declaration( + language: Language, type_: AllTypes, *others: NestedTypeDeclaration +) -> str: + if len(others): + result = generate_type_declaration(language, (type_, others)) + else: + result = generate_type_declaration(language, type_) + assert isinstance(result, str) + return result + + +def common_type_name(type_: AllTypes, plural: bool = False): + key = "plural" if plural else "singular" + return get_i18n_string(f"types.{key}.{type_}") + + +def convert_mako_problem(bundle: Bundle, raw_description: str) -> str: + """ + Render a Mako problem into a normal problem. + + :param bundle: The bundle of the programming language to convert to. + :param raw_description: The raw, Mako description. + :return: The processed (Markdown) description. + """ + description_template = Template(raw_description, output_encoding="utf8") + language = bundle.lang_config + set_locale(bundle.config.natural_language) + return cast( + str, + description_template.render( + # Conventionalize functions + namespace=partial(conventionalize_namespace, language), + function=partial(conventionalize_function, language), + identifier=partial(conventionalize_identifier, language), + property=partial(conventionalize_property, language), + clazz=partial(conventionalize_class, language), + global_identifier=partial(conventionalize_global_identifier, language), + # Access to the current programming language + programming_language=bundle.config.programming_language, + # Data type conversion + datatype=partial(type_declaration, language), + datatype_common=common_type_name, + t=partial(render_one_statement, bundle), + ), + ) + + +def convert_tested_markdown(bundle: Bundle, markdown_description: str) -> str: + """ + Convert code blocks in the Markdown text. + + :param bundle: The configuration bundle. + :param markdown_description: The markdown scription. + :return: A converted description. + """ + marko = Markdown(renderer=TestedRenderer) + # Ugly, but needed. + marko.parse("") + cast(TestedRenderer, marko.renderer).bundle = bundle + + return marko.convert(markdown_description) diff --git a/tested/descriptions/renderer.py b/tested/descriptions/renderer.py new file mode 100644 index 00000000..f0251c12 --- /dev/null +++ b/tested/descriptions/renderer.py @@ -0,0 +1,60 @@ +""" +A Marko renderer that only renders TESTed code; all other things are left alone. +""" +from marko import block +from marko.md_renderer import MarkdownRenderer + +from tested.configs import Bundle +from tested.dsl import parse_string +from tested.languages.generation import generate_statement + + +def render_one_statement(bundle: Bundle, statement: str) -> str: + """ + Render a single statement. + """ + parsed_string = parse_string(statement) + generated_statement = generate_statement(bundle, parsed_string) + # Allow the language to modify the template a bit. + return bundle.lang_config.cleanup_description(generated_statement) + + +class TestedRenderer(MarkdownRenderer): + bundle: Bundle + + def render_fenced_code(self, element: block.FencedCode) -> str: + if element.lang not in ("tested", "console?lang=tested"): + return super().render_fenced_code(element) + + rendered_lines = self.render_children(element).splitlines() + resulting_lines = [] + one_console = False + for rendered_line in rendered_lines: + console = False + # We have a console line, so strip the prefix before processing it. + if rendered_line.startswith(">>>"): + one_console = True + rendered_line = rendered_line[3:] + rendered_line = rendered_line.lstrip() + console = True + + generated_statement = render_one_statement(self.bundle, rendered_line) + # Re-add the correct console prefix if this was a console line. + if console: + prefix = ( + self.bundle.lang_config.get_declaration_metadata().get( + "prompt", ">" + ) + + " " + ) + else: + prefix = "" + resulting_lines.append(prefix + generated_statement) + if one_console: + prompt = self.bundle.lang_config.get_declaration_metadata().get( + "prompt", ">" + ) + language = f"console?lang={self.bundle.config.programming_language}&prompt={prompt}" + else: + language = self.bundle.config.programming_language + return "```" + language + "\n" + "\n".join(resulting_lines) + "\n```\n" diff --git a/tested/dsl/ast_translator.py b/tested/dsl/ast_translator.py index e2b46db6..7033da45 100644 --- a/tested/dsl/ast_translator.py +++ b/tested/dsl/ast_translator.py @@ -52,7 +52,6 @@ ObjectKeyValuePair, ObjectType, SequenceType, - SpecialNumbers, Statement, Value, VariableType, diff --git a/tested/dsl/translate_parser.py b/tested/dsl/translate_parser.py index 6cd8d073..1c217c9b 100644 --- a/tested/dsl/translate_parser.py +++ b/tested/dsl/translate_parser.py @@ -131,7 +131,6 @@ def _convert_value(value: YamlObject) -> Value: ) else: data = [] - # noinspection PyTypeChecker for key, val in value.items(): data.append( ObjectKeyValuePair( diff --git a/tested/instantiate_exercise.py b/tested/instantiate_exercise.py deleted file mode 100644 index 4fd765cb..00000000 --- a/tested/instantiate_exercise.py +++ /dev/null @@ -1,558 +0,0 @@ -import json -import shutil -import sys -from argparse import ArgumentParser -from copy import deepcopy -from dataclasses import dataclass -from itertools import groupby -from pathlib import Path -from typing import Any, Dict, List, Optional, Tuple - -from mako.template import Template -from pydantic.json import pydantic_encoder - -from tested.datatypes import BasicObjectTypes, BasicSequenceTypes -from tested.description_instance import ( - create_description_instance_from_template, - prepare_template, -) -from tested.dsl import parse_dsl -from tested.features import fallback_type_support_map -from tested.languages import LANGUAGES, get_language, language_exists -from tested.testsuite import Suite, parse_test_suite - - -class InstantiateError(Exception): - pass - - -@dataclass -class DescriptionFile: - location: Path - type: str = "md" - natural_language: str = "en" - is_natural_language_explicit: bool = False - is_template: bool = False - template: Optional[Template] = None - - -def _analyse_description_dir( - description_dir: Path, default_i18n: str -) -> Tuple[List[DescriptionFile], List[Path]]: - """ - Read the description directory - - :param description_dir: Description directory to analyse - :return: tuple of a list of description files and a list of the other files - """ - descriptions, other = [], [] - for path in description_dir.iterdir(): - if ( - path.is_file() - and path.name.startswith("description") - and path.suffix.lower() in (".md", ".html", ".mako") - ): - file = DescriptionFile(location=path, natural_language=default_i18n) - file.is_template = path.suffix.lower() == ".mako" - # Natural language is given - if len(path.suffixes) > (1 + int(file.is_template)): - file.is_natural_language_explicit = True - i18n_suffix = path.suffixes[-(2 + int(file.is_template))] - # Slice to remove dot - file.natural_language = i18n_suffix[1:].lower() - # Slice to remove dot - file.type = path.suffixes[-(1 + int(file.is_template))][1:].lower() - - descriptions.append(file) - else: - other.append(path) - return descriptions, other - - -def _check_if_all_languages_exists(languages: List[str]): - """ - Check if all languages to check exists in TESTed - - :param languages: list of the languages to test - :return: - """ - for language in languages: - if not language_exists(language): - raise InstantiateError( - f"Programming language '{language}' isn't supported by TESTed" - ) - - -def _check_if_directory_exists(name: str, path: Path): - """ - Check if the given directory exist - - :param name: Name of the directory - :param path: Location of the directory - :return: - """ - if not path.exists(): - raise InstantiateError(f"{name} directory '{path}' doesn't exists") - elif not path.is_dir(): - raise InstantiateError(f"{name} path '{path}' isn't a directory") - - -def _check_if_file_exists(name: str, path: Path): - """ - Check if the given file exist - - :param name: Name of the file - :param path: Location of the file - :return: - """ - if not path.exists(): - raise InstantiateError(f"{name} file '{path}' doesn't exists") - elif not path.is_file(): - raise InstantiateError(f"{name} path '{path}' isn't a file") - - -def _copy_all(template_dir: Path, instance_dir: Path): - """ - Copy all files and directories except from the description directory and the - config.template.json file - - :param template_dir: The template directory as source - :param instance_dir: The instance directory as destination - :return: - """ - - for path in template_dir.iterdir(): - if path.name in ("config.template.json", "description"): - continue - elif path.is_dir(): - shutil.copytree(path, instance_dir / path.name) - else: - shutil.copy2(path, instance_dir) - - -def _filter_valid_languages(languages: List[str], test_suite: Suite) -> List[str]: - """ - Filter out all languages for which the test suite isn't supported - - :param languages: languages to check - :param test_suite: test suite to support - :return: all given languages which support the test suite - """ - - def is_supported(language_str: str) -> bool: - language = get_language(None, language_str) - - from tested.features import TypeSupport - - required = test_suite.get_used_features() - - # Check constructs - available_constructs = language.supported_constructs() - if not (required.constructs <= available_constructs): - return False - - mapping = fallback_type_support_map(language) - for t in required.types: - if mapping[t] == TypeSupport.UNSUPPORTED: - return False - - # Check language-specific evaluators - for testcase in ( - testcase - for tab in test_suite.tabs # type: ignore - for context in tab.contexts # type: ignore - for testcase in context.testcases - ): - eval_langs = testcase.output.get_specific_eval_languages() - if eval_langs is not None and language not in eval_langs: - return False - - nested_types = filter( - lambda x: x[0] in (BasicSequenceTypes.SET, BasicObjectTypes.MAP), - required.nested_types, - ) - restricted = { - BasicSequenceTypes.SET: language.set_type_restrictions(), - BasicObjectTypes.MAP: language.map_type_restrictions(), - } - for key, value_types in nested_types: - if not (value_types <= restricted[key]): # type: ignore - return False - - return True - - return list(filter(is_supported, languages)) - - -def _get_config(config_json_path: Path) -> Dict[str, Any]: - """ - Load exercise configuration - - :param config_json_path: Configuration file location - :return: The loaded configuration dictionary - """ - # noinspection PyTypeChecker - with open(config_json_path, "r") as json_fd: - return json.load(json_fd) - - -def _instantiate( - template_dir: Path, - instance_dir: Path, - test_suite: Suite, - descriptions: List[DescriptionFile], - other_files_descriptions: List[Path], - config_json_dict: Dict[str, Any], - language: str, - human_readable: bool = False, - backup_descriptions: bool = False, -): - """ - Instantiate template for a specific programming language - - :param template_dir: The template directory - :param instance_dir: The instance directory - :param test_suite: The test suite to use - :param descriptions: The description file list - :param other_files_descriptions: The other files from the description folders - :param config_json_dict: Configuration dictionary - :param language: The programming language - :param human_readable: If the converted test suite must be human-readable - :param backup_descriptions: Keep the old description folder - :return: - """ - config_dict = deepcopy(config_json_dict) - existing: bool - if instance_dir.exists(): - if not instance_dir.is_dir(): - print( - f"{instance_dir} is not a directory, instantiating {language} " - f"failed!", - file=sys.stderr, - ) - _remove_existing(instance_dir, backup_descriptions) - else: - instance_dir.mkdir(parents=True) - # Copy all except descriptions - _copy_all(template_dir, instance_dir) - # Check test suite - suite_file = template_dir / "evaluation" / config_dict["evaluation"]["test_suite"] - if suite_file.suffix.lower() in (".yml", ".yaml"): - suite_file_new = suite_file.with_suffix(f"{suite_file.suffix}.json") - suite_file_new = instance_dir / "evaluation" / suite_file_new.name - config_dict["evaluation"]["test_suite"] = suite_file_new.name - with open(suite_file_new, "w") as fd: - json.dump( - test_suite, - fd, - default=pydantic_encoder, - indent=2 if human_readable else None, - ) - # Copy or generate descriptions - _instantiate_descriptions( - instance_dir, descriptions, other_files_descriptions, test_suite, language - ) - # Prepare configuration - config_dict["programming_language"] = language - try: - for i18n in config_dict["description"]["names"]: - name = config_dict["description"]["names"][i18n] - config_dict["description"]["names"][i18n] = f"{name} ({language})" - except KeyError: - pass - - -def _instantiate_descriptions( - instance_dir: Path, - descriptions: List[DescriptionFile], - other_files_descriptions: List[Path], - test_suite: Suite, - language: str, -): - """ - Instantiate description directory - - :param instance_dir: The instance directory - :param descriptions: The description files to use - :param other_files_descriptions: All other files and directories to copy - :param test_suite: The test suite to determine the namespace - :param language: Programming language - :return: - """ - description_dir = instance_dir / "description" - description_dir.mkdir() - # Copy the other files - for path in other_files_descriptions: - if path.is_dir(): - shutil.copytree(path, description_dir / path.name) - else: - shutil.copy2(path, description_dir) - # Copy or generate descriptions - for description in descriptions: - # Prepare output file location - if description.is_natural_language_explicit: - file_name = ( - f"description.{description.natural_language}." f"{description.type}" - ) - output_file = description_dir / file_name - else: - output_file = description_dir / f"description.{description.type}" - # Copy or generate - if description.is_template: - # Generate - instance = create_description_instance_from_template( - description.template, # type: ignore - language, - description.natural_language, - test_suite.namespace, - description.type == "html", - ) - with open(output_file, "w") as fd: - print(instance, file=fd) - else: - # Copy files - shutil.copy2(description.location, output_file) - - -def _parser_instance() -> ArgumentParser: - """ - Get argument parser - - :return: the prepared argument parser - """ - info = ( - "Included - Excluded = Programming languages that are candidates to " - "generate instances for" - ) - - parser = ArgumentParser( - description="Script for instantiating all supported languages" - ) - parser.add_argument( - "-i", - "--programming_languages_included", - type=str, - nargs="+", - help="Included programming languages to create instances " - "for (default: all supported languages of " - f"TESTed)\n{info}", - default=[lang for lang in LANGUAGES if lang != "runhaskell"], - ) - - parser.add_argument( - "-e", - "--programming_languages_excluded", - type=str, - nargs="*", - help="Excluded programming languages to create instances " - f"for (default: nothing)\n{info}", - default=[], - ) - parser.add_argument( - "-n", - "--i18n", - type=str, - help="Natural language for descriptions if it can't be " - "derived from the filename (options: 'en' or 'nl', " - "default: 'en')", - default="en", - ) - parser.add_argument( - "-H", - "--human_readable", - action="store_true", - help="Generated test_suite in human readable format", - ) - parser.add_argument( - "-b", - "--backup_descriptions", - action="store_true", - help="Keep old descriptions (with '.bak' extension)", - ) - parser.add_argument("template_dir", type=str, help="Template directory") - parser.add_argument("instances_dir", type=str, help="Instances directory") - return parser - - -def _prepare_templates(descriptions: List[DescriptionFile]): - """ - Prepare all description templates - - :param descriptions: list of the description files - :return: - """ - for description in descriptions: - if not description.is_template: - continue - with open(description.location, "r") as file: - description.template = prepare_template( - file.read(), is_html=description.type == "html" - ) - - -def _read_test_suite(config_dict: Dict[str, Any], evaluation_dir: Path) -> Suite: - """ - Read test suite from JSON or YAML. - - :param config_dict: Configuration information - :param evaluation_dir: Directory containing the test suite - :return: The test suite - """ - - try: - suite_file = evaluation_dir / config_dict["evaluation"]["test_suite"] - except KeyError: - print( - f"Not test suite given in the template configuration file", file=sys.stderr - ) - sys.exit(6) - _check_if_file_exists("Test suite", suite_file) - - with open(suite_file, "r") as file: - loaded_suite = file.read() - - suffix = suite_file.suffixes[-1].lower() - if suffix in (".yml", ".yaml"): - return parse_dsl(loaded_suite) - return parse_test_suite(loaded_suite) - - -def _remove_existing(instance_dir: Path, backup_descriptions: bool = False): - """ - Remove the existing content of the instance directory - - :param instance_dir: The instance directory - :param backup_descriptions: Keep the old descriptions directory or not - :return: - """ - updated = False - for path in instance_dir.iterdir(): - if updated and path.name == "description.bak": - continue - elif backup_descriptions and path.name == "description": - new_name = path.with_name("description.bak") - if new_name.exists(): - if path.is_dir(): - shutil.rmtree(new_name) - else: - new_name.unlink() - path.rename(path.with_name("description.bak")) - updated = True - elif path.is_dir(): - shutil.rmtree(path) - else: - path.unlink() - - -def _select_descriptions(descriptions: List[DescriptionFile]) -> List[DescriptionFile]: - """ - Select best suited descriptions, templates and prefer markdown before html - - :param descriptions: list of all descriptions files - :return: description files to use - """ - - def group_key(file: DescriptionFile) -> Tuple[bool, str]: - return file.is_natural_language_explicit, file.natural_language - - selected = [] - descriptions.sort(key=group_key) - natural_languages_groups = groupby(descriptions, key=group_key) - for _, data_list in natural_languages_groups: - # Select one template first templates and markdown before html - selected.append( - next( - iter( - sorted( - data_list, key=lambda x: (x.is_template, x.type), reverse=True - ) - ) - ) - ) - return selected - - -def instantiate( - template_dir: Path, - instances_dir: Path, - programming_languages: Optional[List[str]] = None, - default_i18n: str = "en", - human_readable: bool = False, - backup_descriptions: bool = False, -): - """ - Instantiate a template exercise for all the supported programming language in - the given list of programming languages - - :param template_dir: The template exercise directory - :param instances_dir: The instances directory - :param programming_languages: An optional list of possible programming language, - if no list given al languages from TESTed - :param default_i18n: The default language for the description files without - language - :param human_readable: Generated JSON test suite must be human-readable - :param backup_descriptions: Keep the old description folder - :return: - """ - - if programming_languages is None: - programming_languages = [lang for lang in LANGUAGES if lang != "runhaskell"] - - evaluation_dir = template_dir / "evaluation" - description_dir = template_dir / "description" - config_template = template_dir / "config.template.json" - - _check_if_all_languages_exists(programming_languages) - - _check_if_directory_exists("Template", template_dir) - _check_if_directory_exists("Instances", instances_dir) - _check_if_directory_exists("Evaluation", evaluation_dir) - _check_if_directory_exists("Description", description_dir) - - _check_if_file_exists("Template config", config_template) - - template_config_dict = _get_config(config_template) - suite = _read_test_suite(template_config_dict, evaluation_dir) - descriptions_files, other_files_description = _analyse_description_dir( - description_dir=description_dir, default_i18n=default_i18n - ) - descriptions_files = _select_descriptions(descriptions_files) - _prepare_templates(descriptions_files) - programming_languages = _filter_valid_languages(programming_languages, suite) - for language in programming_languages: - _instantiate( - template_dir=template_dir, - instance_dir=instances_dir / language, - test_suite=suite, - descriptions=descriptions_files, - other_files_descriptions=other_files_description, - config_json_dict=template_config_dict, - language=language, - human_readable=human_readable, - backup_descriptions=backup_descriptions, - ) - - -if __name__ == "__main__": - args = _parser_instance().parse_args() - temp_dir, inst_dir = Path(args.template_dir), Path(args.instances_dir) - - prog_langs = list( - sorted( - set(args.programming_languages_included) - - set(args.programming_languages_excluded) - ) - ) - - try: - instantiate( - temp_dir, - inst_dir, - prog_langs, - args.i18n, - args.human_readable, - args.backup_descriptions, - ) - except InstantiateError as e: - print(e, file=sys.stderr) - sys.exit(-1) diff --git a/tested/internationalization/en.yaml b/tested/internationalization/en.yaml index 134a8280..137f724c 100644 --- a/tested/internationalization/en.yaml +++ b/tested/internationalization/en.yaml @@ -174,4 +174,59 @@ en: expression: "Parse expressions and statements" return: "Parse YAML return values" return-raw: "Parse return-raw values" + types: + singular: + integer: "integer" + real: "real number" + char: "character" + text: "string" + boolean: "boolean" + nothing: "nothing" + undefined: "nothing" + int8: "8-bit integer" + uint8: "8-bit natural number" + int16: "16-bit integer" + uint16: "16-bit natural number" + int32: "32-bit integer" + uint32: "32-bit natural number" + int64: "64-bit integer" + uint64: "64-bit natural number" + bigint: "big integer" + single_precision: "single precision floating point number" + double_precision: "double precision floating point number" + fixed_precision: "fixed precision number" + any: "any" + list: "list" + tuple: "tuple" + array: "array" + map: "map" + sequence: "sequence" + set: "set" + plural: + integer: "integers" + real: "real numbers" + char: "characters" + text: "strings" + boolean: "booleans" + nothing: "nothing" + undefined: "nothing" + int8: "8-bit integers" + uint8: "8-bit natural numbers" + int16: "16-bit integers" + uint16: "16-bit natural numbers" + int32: "32-bit integers" + uint32: "32-bit natural numbers" + int64: "64-bit integers" + uint64: "64-bit natural numbers" + bigint: "big integers" + single_precision: "single precision floating point numbers" + double_precision: "double precision floating point numbers" + fixed_precision: "fixed precision numbers" + any: "any" + list: "lists" + tuple: "tuples" + array: "arrays" + map: "maps" + sequence: "sequences" + set: "sets" diff --git a/tested/internationalization/nl.yaml b/tested/internationalization/nl.yaml index 6a32eb35..15bf57b5 100644 --- a/tested/internationalization/nl.yaml +++ b/tested/internationalization/nl.yaml @@ -174,3 +174,58 @@ nl: expression: "Parsen van expressies en statements" return: "Parsen van YAML returnwaarden" return-raw: "Parsen return-raw waarden" + types: + singular: + integer: "geheel getal" + real: "reëel getal" + char: "karakter" + text: "string" + boolean: "booleaan" + nothing: "niets" + undefined: "niets" + int8: "8-bit geheel getal" + uint8: "8-bit natuurlijk getal" + int16: "16-bit geheel getal" + uint16: "16-bit natuurlijk getal" + int32: "32-bit geheel getal" + uint32: "32-bit natuurlijk getal" + int64: "64-bit geheel getal" + uint64: "64-bit natuurlijk getal" + bigint: "geheel getal van onbeperkte grootte" + single_precision: "enkele precisie vlottendekommagetal" + double_precision: "dubbele precisie vlottendekommagetal" + fixed_precision: "vast kommagetal" + any: "ieder" + list: "lijst" + tuple: "tuple" + array: "array" + map: "afbeelding" + sequence: "sequentie" + set: "verzameling" + plural: + integer: "gehele getallen" + real: "reële getallen" + char: "karakters" + text: "strings" + boolean: "booleaans" + nothing: "niets" + undefined: "niets" + int8: "8-bit gehele getallen" + uint8: "8-bit natuurlijke getallen" + int16: "16-bit gehele getallen" + uint16: "16-bit natuurlijke getallen" + int32: "32-bit gehele getallen" + uint32: "32-bit natuurlijke getallen" + int64: "64-bit gehele getallen" + uint64: "64-bit natuurlijke getallen" + bigint: "gehele getallen van onbeperkte grootte" + single_precision: "enkele precisie vlottendekommagetallen" + double_precision: "dubbele precisie vlottendekommagetallen" + fixed_precision: "vaste kommagetallen" + any: "ieder" + list: "lijsten" + tuple: "tuples" + array: "arrays" + map: "afbeeldingen" + sequence: "sequenties" + set: "verzamelingen" diff --git a/tested/languages/bash/config.py b/tested/languages/bash/config.py index be6f993a..f98df4aa 100644 --- a/tested/languages/bash/config.py +++ b/tested/languages/bash/config.py @@ -5,7 +5,12 @@ from tested.datatypes import AdvancedStringTypes, AllTypes, BasicStringTypes from tested.dodona import AnnotateCode, Message from tested.features import Construct, TypeSupport -from tested.languages.config import CallbackResult, Command, Language +from tested.languages.config import ( + CallbackResult, + Command, + Language, + TypeDeclarationMetadata, +) from tested.languages.conventionalize import ( EXECUTION_PREFIX, Conventionable, @@ -92,3 +97,12 @@ def generate_encoder(self, values: List[Value]) -> str: from tested.languages.bash import generators return generators.convert_encoder(values) + + def get_declaration_metadata(self) -> TypeDeclarationMetadata: + return { + "names": { # type: ignore + "text": "str", + "char": "str", + }, + "prompt": "$", + } diff --git a/tested/languages/bash/types.json b/tested/languages/bash/types.json deleted file mode 100644 index 7fec64f8..00000000 --- a/tested/languages/bash/types.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "brackets": { - "open": "[", - "close": "]" - }, - "console": { - "name": "bash", - "prompt": "$" - }, - "char": "char", - "text": "text", - "inner": {}, - "natural": { - "singular": { - "en": { - "char": "character", - "text": "string" - }, - "nl": { - "char": "karakter", - "text": "string" - } - }, - "plural": { - "en": { - "char": "characters", - "text": "strings" - }, - "nl": { - "char": "karakters", - "text": "strings" - } - } - } -} diff --git a/tested/languages/c/config.py b/tested/languages/c/config.py index df185e01..7eb077e6 100644 --- a/tested/languages/c/config.py +++ b/tested/languages/c/config.py @@ -6,7 +6,12 @@ from tested.datatypes import AllTypes from tested.dodona import AnnotateCode, Message from tested.features import Construct, TypeSupport -from tested.languages.config import CallbackResult, Command, Language +from tested.languages.config import ( + CallbackResult, + Command, + Language, + TypeDeclarationMetadata, +) from tested.languages.conventionalize import ( EXECUTION_PREFIX, Conventionable, @@ -153,3 +158,28 @@ def generate_encoder(self, values: List[Value]) -> str: from tested.languages.c import generators return generators.convert_encoder(values) + + def get_declaration_metadata(self) -> TypeDeclarationMetadata: + return { + "names": { # type: ignore + "integer": "int", + "real": "double", + "char": "char", + "text": "char*", + "boolean": "bool", + "nothing": "void", + "undefined": "void", + "int8": "signed char", + "uint8": "unsigned char", + "int16": "short", + "uint16": "unsigned short", + "int32": "int", + "uint32": "unsigned int", + "int64": "long", + "uint64": "unsigned long", + "bigint": "long long", + "single_precision": "float", + "double_precision": "double", + "any": "void*", + } + } diff --git a/tested/languages/c/types.json b/tested/languages/c/types.json deleted file mode 100644 index 6cfd6284..00000000 --- a/tested/languages/c/types.json +++ /dev/null @@ -1,117 +0,0 @@ -{ - "console": { - "name": "c", - "prompt": ">" - }, - "integer": "int", - "real": "double", - "char": "char", - "text": "char*", - "boolean": "bool", - "nothing": "void", - "undefined": "void", - "int8": "signed char", - "uint8": "unsigned char", - "int16": "short", - "uint16": "unsigned short", - "int32": "int", - "uint32": "unsigned int", - "int64": "long", - "uint64": "unsigned long", - "bigint": "long long", - "single_precision": "float", - "double_precision": "double", - "any": "void*", - "inner": { - }, - "natural": { - "singular": { - "en": { - "integer": "integer", - "real": "real number", - "char": "character", - "text": "string", - "boolean": "boolean", - "nothing": "nothing", - "undefined": "nothing", - "int8": "8-bit integer", - "uint8": "8-bit natural number", - "int16": "16-bit integer", - "uint16": "16-bit natural number", - "int32": "32-bit integer", - "uint32": "32-bit natural number", - "int64": "64-bit integer", - "uint64": "64-bit natural number", - "bigint": "big integer", - "single_precision": "single precision floating point number", - "double_precision": "double precision floating point number", - "any": "any" - }, - "nl": { - "integer": "geheel getal", - "real": "reëel getal", - "char": "karakter", - "text": "string", - "boolean": "booleaanse waarde", - "nothing": "niets", - "undefined": "niet gedefinieerd", - "int8": "8-bit geheel getal", - "uint8": "8-bit natuurlijk getal", - "int16": "16-bit geheel getal", - "uint16": "16-bit natuurlijk getal", - "int32": "32-bit geheel getal", - "uint32": "32-bit natuurlijk getal", - "int64": "64-bit geheel getal", - "uint64": "64-bit natuurlijk getal", - "bigint": "groot geheel getal", - "single_precision": "enkele precisie vlottende kommagetal", - "double_precision": "dubbele precisie vlottende kommagetal", - "any": "ieder" - } - }, - "plural": { - "en": { - "integer": "integers", - "real": "real numbers", - "char": "characters", - "text": "strings", - "boolean": "booleans", - "nothing": "nothing", - "undefined": "nothing", - "int8": "8-bit integers", - "uint8": "8-bit natural numbers", - "int16": "16-bit integers", - "uint16": "16-bit natural numbers", - "int32": "32-bit integers", - "uint32": "32-bit natural numbers", - "int64": "64-bit integers", - "uint64": "64-bit natural numbers", - "bigint": "big integers", - "single_precision": "single precision floating point numbers", - "double_precision": "double precision floating point numbers", - "any": "any" - }, - "nl": { - "integer": "gehele getallen", - "real": "reële getallen", - "char": "karakters", - "text": "strings", - "boolean": "booleaanse waarden", - "nothing": "niets", - "undefined": "niets", - "int8": "8-bit gehele getallen", - "uint8": "8-bit natuurlijke getallen", - "int16": "16-bit gehele getallen", - "uint16": "16-bit natuurlijke getallen", - "int32": "32-bit gehele getallen", - "uint32": "32-bit natuurlijke getallen", - "int64": "64-bit gehele getallen", - "uint64": "64-bit natuurlijke getallen", - "bigint": "grote gehele getallen", - "single_precision": "enkele precisie vlottende kommagetallen", - "double_precision": "dubbele precisie vlottende kommagetallen", - "any": "ieder" - } - } - } -} diff --git a/tested/languages/config.py b/tested/languages/config.py index bf598d16..6e0a819e 100644 --- a/tested/languages/config.py +++ b/tested/languages/config.py @@ -8,7 +8,18 @@ import typing from abc import ABC, abstractmethod from pathlib import Path -from typing import Callable, Dict, List, Mapping, Optional, Set, Tuple, Union +from typing import ( + Callable, + Dict, + List, + Mapping, + NotRequired, + Optional, + Set, + Tuple, + TypedDict, + Union, +) from tested.datatypes import AllTypes, ExpressionTypes from tested.dodona import AnnotateCode, Message, Status @@ -20,7 +31,6 @@ NamingConventions, conventionalize_namespace, ) -from tested.languages.description_generator import DescriptionGenerator from tested.serialisation import FunctionCall, Statement, Value if typing.TYPE_CHECKING: @@ -34,6 +44,15 @@ _logger = logging.getLogger(__name__) +class TypeDeclarationMetadata(TypedDict): + names: Mapping[AllTypes, str | bool] + inner_names: NotRequired[Mapping[AllTypes, str]] + nested: NotRequired[Tuple[str, str]] + nested_overrides: NotRequired[Mapping[AllTypes, Tuple[str, str]]] + prompt: NotRequired[str] + natural_overrides: NotRequired[Mapping[str, Mapping[AllTypes, Tuple[str, str]]]] + + class Language(ABC): """ Abstract base class for a programming language. @@ -50,14 +69,12 @@ class Language(ABC): """ config: Optional["GlobalConfig"] - _description_generator: Optional[DescriptionGenerator] def __init__(self, config: Optional["GlobalConfig"]): """ :param config: If the config is None, only "config" methods will work. """ self.config = config - self._description_generator = None def compilation(self, files: List[str]) -> CallbackResult: """ @@ -389,16 +406,14 @@ def cleanup_stacktrace(self, stacktrace: str) -> str: """ return stacktrace - def cleanup_description(self, description: str) -> str: - return description + def cleanup_description(self, statement: str) -> str: + """ + Allow the language implementation to modify a generated statement for use + in problem statements. - def get_description_generator(self) -> DescriptionGenerator: - if self._description_generator is None: - assert self.config - lang = self.config.dodona.programming_language - config_dir = self.config.dodona.judge / "tested" / "languages" / lang - self._description_generator = DescriptionGenerator(self, config_dir) - return self._description_generator + :param statement: The generated statement. + """ + return statement @abstractmethod def generate_statement(self, statement: Statement) -> str: @@ -451,6 +466,16 @@ def generate_encoder(self, values: List[Value]) -> str: """ raise NotImplementedError + @abstractmethod + def get_declaration_metadata(self) -> TypeDeclarationMetadata: + """ + Return metadata that can be used to construct type declarations. + + In languages that support it, this declaration should be valid to use on a + variable, for example. In other languages, an approximation can be used. + """ + raise NotImplementedError + def path_to_dependencies(self) -> List[Path]: """ Construct the paths to the folder containing the additional dependencies diff --git a/tested/languages/csharp/config.py b/tested/languages/csharp/config.py index e412e437..f24dd606 100644 --- a/tested/languages/csharp/config.py +++ b/tested/languages/csharp/config.py @@ -7,7 +7,12 @@ from tested.dodona import AnnotateCode, Message, Status from tested.features import Construct, TypeSupport from tested.internationalization import get_i18n_string -from tested.languages.config import CallbackResult, Command, Language +from tested.languages.config import ( + CallbackResult, + Command, + Language, + TypeDeclarationMetadata, +) from tested.languages.conventionalize import ( EXECUTION_PREFIX, Conventionable, @@ -242,3 +247,32 @@ def generate_encoder(self, values: List[Value]) -> str: from tested.languages.csharp import generators return generators.convert_encoder(values) + + def get_declaration_metadata(self) -> TypeDeclarationMetadata: + return { + "names": { # type: ignore + "integer": "Int32", + "real": "Double", + "char": "char", + "text": "string", + "boolean": "Boolean", + "sequence": "List", + "set": "Set", + "map": "Dictionary", + "int8": "Byte", + "uint8": "SByte", + "int16": "Int16", + "uint16": "UInt16", + "int32": "Int32", + "uint32": "UInt32", + "int64": "Int64", + "uint64": "UInt64", + "bigint": "BigInteger", + "single_precision": "Single", + "double_precision": "Double", + "double_extended": "BigInteger", + "list": "List", + "tuple": "Tuple", + "any": "Object", + } + } diff --git a/tested/languages/csharp/types.json b/tested/languages/csharp/types.json deleted file mode 100644 index 4f8af04e..00000000 --- a/tested/languages/csharp/types.json +++ /dev/null @@ -1,159 +0,0 @@ -{ - "brackets": { - "open": "[", - "close": "]" - }, - "console": { - "name": "csharp", - "prompt": ">" - }, - "integer": "integer", - "real": "real", - "char": "char", - "text": "text", - "boolean": "boolean", - "sequence": "sequence", - "set": "set", - "map": "map", - "nothing": "nothing", - "int8": "int8", - "uint8": "uint8", - "int16": "int16", - "uint16": "uint16", - "int32": "int32", - "uint32": "uint32", - "int64": "int64", - "uint64": "uint64", - "bigint": "bigint", - "single_precision": "single_precision", - "double_precision": "double_precision", - "double_extended": "double_extended", - "fixed_precision": "fixed_precision", - "array": "array", - "list": "list", - "tuple": "tuple", - "any": "any", - "inner": {}, - "natural": { - "singular": { - "en": { - "integer": "integer", - "real": "real number", - "char": "character", - "text": "text", - "boolean": "boolean", - "sequence": "sequence", - "set": "set", - "map": "map", - "nothing": "nothing", - "undefined": "undefined", - "int8": "8-bit integer", - "uint8": "8-bit natural number", - "int16": "16-bit integer", - "uint16": "16-bit natural number", - "int32": "32-bit integer", - "uint32": "32-bit natural number", - "int64": "64-bit integer", - "uint64": "64-bit natural number", - "bigint": "big integer", - "single_precision": "single precision floating point number", - "double_precision": "double precision floating point number", - "double_extended": "extended precision floating point number", - "fixed_precision": "fixed point number", - "array": "array", - "list": "list", - "tuple": "tuple", - "any": "any" - }, - "nl": { - "integer": "geheel getal", - "real": "re\u00ebel getal", - "char": "karakter", - "text": "tekst", - "boolean": "booleaanse waarde", - "sequence": "sequentie", - "set": "verzameling", - "map": "afbeelding", - "nothing": "niets", - "undefined": "niet gedefinieerd", - "int8": "8-bit geheel getal", - "uint8": "8-bit natuurlijk getal", - "int16": "16-bit geheel getal", - "uint16": "16-bit natuurlijk getal", - "int32": "32-bit geheel getal", - "uint32": "32-bit natuurlijk getal", - "int64": "64-bit geheel getal", - "uint64": "64-bit natuurlijk getal", - "bigint": "groot geheel getal", - "single_precision": "enkele precisie vlottende kommagetal", - "double_precision": "dubbele precisie vlottende kommagetal", - "double_extended": "uitgebreide precisie vlottende kommagetal", - "fixed_precision": "vast kommagetal", - "array": "array", - "list": "lijst", - "tuple": "tuple", - "any": "ieder" - } - }, - "plural": { - "en": { - "integer": "integers", - "real": "real numbers", - "char": "characters", - "text": "texts", - "boolean": "booleans", - "sequence": "sequences", - "set": "sets", - "map": "maps", - "nothing": "nothing", - "undefined": "undefined", - "int8": "8-bit integers", - "uint8": "8-bit natural numbers", - "int16": "16-bit integers", - "uint16": "16-bit natural numbers", - "int32": "32-bit integers", - "uint32": "32-bit natural numbers", - "int64": "64-bit integers", - "uint64": "64-bit natural numbers", - "bigint": "big integers", - "single_precision": "single precision floating point numbers", - "double_precision": "double precision floating point numbers", - "double_extended": "extended precision floating point numbers", - "fixed_precision": "fixed precision floating point numbers", - "array": "arrays", - "list": "lists", - "tuple": "tuples", - "any": "any" - }, - "nl": { - "integer": "gehele getallen", - "real": "re\u00eble getallen", - "char": "karakters", - "text": "teksten", - "boolean": "booleaanse waarden", - "sequence": "sequenties", - "set": "verzamelingen", - "map": "afbeeldingen", - "nothing": "niets", - "undefined": "niet gedefinieerd", - "int8": "8-bit gehele getallen", - "uint8": "8-bit natuurlijke getallen", - "int16": "16-bit gehele getallen", - "uint16": "16-bit natuurlijke getallen", - "int32": "32-bit gehele getallen", - "uint32": "32-bit natuurlijke getallen", - "int64": "64-bit gehele getallen", - "uint64": "64-bit natuurlijke getallen", - "bigint": "grote gehele getallen", - "single_precision": "enkele precisie vlottende kommagetallen", - "double_precision": "dubbele precisie vlottende kommagetallen", - "double_extended": "uitgebreide precisie vlottende kommagetallen", - "fixed_precision": "vaste kommagetallen", - "array": "arrays", - "list": "lijsten", - "tuple": "tuples", - "any": "ieder" - } - } - } -} diff --git a/tested/languages/description_generator.py b/tested/languages/description_generator.py deleted file mode 100644 index 484570ef..00000000 --- a/tested/languages/description_generator.py +++ /dev/null @@ -1,222 +0,0 @@ -from __future__ import annotations - -import html -import json -import logging -from pathlib import Path -from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Union - -from pygments import highlight -from pygments.formatters.html import HtmlFormatter -from pygments.lexers import get_lexer_by_name - -from tested.configs import Bundle -from tested.dsl import parse_string -from tested.languages.conventionalize import ( - conventionalize_class, - conventionalize_function, - conventionalize_global_identifier, - conventionalize_identifier, - conventionalize_property, -) - -if TYPE_CHECKING: - from .config import Language - -TYPE_ARG = Union[str, Tuple[str, Union[List["TYPE_ARG"], "TYPE_ARG"]]] - -TYPE_CONFIG_NAME = Optional[Dict[str, Dict[str, Union[str, Dict[str, str]]]]] - -logger = logging.getLogger(__name__) - -_html_formatter = HtmlFormatter(nowrap=True) - -_console_lexer = get_lexer_by_name("console") - - -def highlight_console(stmt): - """ - Highlight a console statement. - """ - return highlight(stmt, _console_lexer, _html_formatter) - - -class DescriptionGenerator: - __slots__ = ["language", "types", "_lexer"] - - def __init__( - self, language: Language, config_dir: Path, types_file: str = "types.json" - ): - self.language = language - path_to_types = config_dir / types_file - - with open(path_to_types, "r") as f: - self.types = json.load(f) - - self._lexer = get_lexer_by_name(self.types["console"]["name"], stripall=True) - - def get_natural_type_name( - self, type_name: str, bundle: Bundle, plural: bool = False, is_html: bool = True - ) -> str: - try: - group = self.types["natural"]["plural" if plural else "singular"] - value = group[bundle.config.natural_language][type_name] - except KeyError: - value = type_name - return html.escape(value) if is_html else value - - def get_type_name( - self, - args: TYPE_ARG, - bundle: Bundle, - custom_type_map: TYPE_CONFIG_NAME = None, - is_inner: bool = False, - is_html: bool = True, - recursive_call: bool = False, - ) -> str: - programming_language = bundle.config.programming_language - if custom_type_map is None: - custom_type_map = dict() - - def _get_type(arg: str) -> Union[str, bool]: - try: - return custom_type_map[programming_language][args] # type: ignore - except KeyError: - return self.types[arg] - - def _get_type_or_conventionalize(arg: str) -> str: - try: - return _get_type(arg) # type: ignore - except KeyError: - return conventionalize_class(self.language, arg) - - def _get_type_name(arg: str) -> Union[str, bool]: - if not is_inner: - return _get_type_or_conventionalize(arg) - else: - try: - return custom_type_map[programming_language]["inner"][arg] # type: ignore - except KeyError: - try: - return self.types["inner"][arg] - except KeyError: - return _get_type_or_conventionalize(arg) - - if isinstance(args, str): - name = _get_type_name(args) - type_name = name if isinstance(name, str) else args - else: - main_type = _get_type_name(args[0]) - if isinstance(args[1], str): - types = [ - self.get_type_name( - args[1], bundle, custom_type_map, bool(main_type), is_html, True - ) - ] - elif isinstance(args[1], list): - types = [ - self.get_type_name( - arg, bundle, custom_type_map, bool(main_type), is_html, True - ) - for arg in args[1] - ] - else: - types = [ - self.get_type_name( - args[1], bundle, custom_type_map, bool(main_type), is_html, True - ) - ] - if isinstance(main_type, str): - type_name = ( - f"{self.types[args[0]]}" - f"{self.types['brackets']['open']}" - f"{', '.join(types)}{self.types['brackets']['close']}" - ) - elif main_type: - type_name = ( - f"{self.types['brackets'][args[0]]['open']}" - f"{', '.join(types)}" - f"{self.types['brackets'][args[0]]['close']}" - ) - elif len(types) == 1: - type_name = ( - f"{types[0]}{self.types['brackets'][args[0]]['open']}" - f"{self.types['brackets'][args[0]]['close']}" - ) - else: - raise ValueError(f"Type {main_type} expects only one subtype") - if is_html and not recursive_call: - return html.escape(type_name) - return type_name - - def get_function_name(self, name: str, is_html: bool = True) -> str: - function_name = conventionalize_function(self.language, name) - if is_html: - return html.escape(function_name) - return function_name - - def get_property_name(self, name: str, is_html: bool = True) -> str: - name = conventionalize_property(self.language, name) - if is_html: - return html.escape(name) - return name - - def get_variable_name(self, name: str, is_html: bool = True) -> str: - name = conventionalize_identifier(self.language, name) - if is_html: - return html.escape(name) - return name - - def get_global_variable_name(self, name: str, is_html: bool = True) -> str: - name = conventionalize_global_identifier(self.language, name) - if is_html: - return html.escape(name) - return name - - def get_code( - self, - stmt_str: str, - bundle: Bundle, - statement: bool = False, - is_html: bool = True, - ) -> str: - from .generation import generate_statement - - if statement: - stmt = parse_string(stmt_str) - else: - stmt = parse_string(stmt_str, is_return=True) - - required = stmt.get_used_features() - available = self.language.supported_constructs() - - if not (required.constructs <= available): - logger.warning("This statement is not compatible!") - logger.warning(f"Required constructs are {required.constructs}.") - logger.warning(f"The language supports {available}.") - missing = (required.constructs ^ available) & required.constructs - logger.warning(f"Missing features are: {missing}.") - raise Exception("Missing features") - - stmt = generate_statement(bundle, stmt) - stmt = self.language.cleanup_description(stmt) - if is_html: - prompt = html.escape(self.types["console"]["prompt"]).strip() - stmt = self.generate_html_code(stmt).strip() - return (prompt + " " if statement else "") + stmt - else: - return ( - (self.types["console"]["prompt"].strip() + " " if statement else "") - + stmt - ).strip() - - def generate_html_code(self, stmt: str) -> str: - return highlight(stmt, self._lexer, _html_formatter) - - def get_prompt(self, is_html: bool = True): - value = self.types["console"]["prompt"] - return html.escape(value) if is_html else value - - def get_prompt_language(self, is_html: bool = True): - value = self.types["console"]["name"] - return html.escape(value) if is_html else value diff --git a/tested/languages/generation.py b/tested/languages/generation.py index 53ad22ac..cad7b85b 100644 --- a/tested/languages/generation.py +++ b/tested/languages/generation.py @@ -8,20 +8,22 @@ import re import shlex from pathlib import Path -from typing import TYPE_CHECKING, Iterable, List, Match, Set, Tuple +from typing import TYPE_CHECKING, Iterable, List, Match, Set, Tuple, TypeAlias +from pygments import highlight from pygments.formatters.html import HtmlFormatter +from pygments.lexers import get_lexer_by_name from tested.configs import Bundle -from tested.datatypes import BasicSequenceTypes +from tested.datatypes import AllTypes, BasicObjectTypes, BasicSequenceTypes from tested.dodona import ExtendedMessage from tested.internationalization import get_i18n_string +from tested.languages import Language from tested.languages.conventionalize import ( conventionalize_namespace, selector_file, submission_name, ) -from tested.languages.description_generator import highlight_console from tested.languages.preparation import ( PreparedExecutionUnit, PreparedFunctionCall, @@ -37,6 +39,7 @@ SequenceType, Statement, Value, + VariableType, ) from tested.testsuite import ( Context, @@ -52,7 +55,24 @@ from tested.judge.execution import ExecutionUnit _logger = logging.getLogger(__name__) -_html_formatter = HtmlFormatter() +_html_formatter = HtmlFormatter(nowrap=True) + + +# Alias for type declarations +NestedTypeDeclaration: TypeAlias = ( + AllTypes | Tuple[AllTypes, Tuple["NestedTypeDeclaration", ...]] +) + + +def highlight_code(stmt: str, language: str = "console") -> str: + """ + Highlight code for some programming language. + + :param stmt: The code to highlight. + :param language: The language of the code. + """ + lexer = get_lexer_by_name(language, stripall=True) + return highlight(stmt, lexer, _html_formatter) def generate_execution_unit( @@ -148,10 +168,9 @@ def get_readable_input( if format_ == "text": generated_html = html.escape(text) elif format_ == "console": - generated_html = highlight_console(text) + generated_html = highlight_code(text) else: - generator = bundle.lang_config.get_description_generator() - generated_html = generator.generate_html_code(text) + generated_html = highlight_code(text, bundle.config.programming_language) if case.is_main_testcase(): regex = re.compile( @@ -340,8 +359,55 @@ def generate_custom_evaluator( if destination.is_dir(): destination /= bundle.lang_config.with_extension("EvaluatorExecutor") - # noinspection PyTypeChecker with open(destination, "w") as check_function_file: check_function_file.write(code) return destination.name + + +def _convert_single_type( + language: Language, type_: AllTypes | VariableType, inner: bool +) -> str | bool: + if isinstance(type_, VariableType): + return type_.data + meta = language.get_declaration_metadata() + if inner and type_ in meta.get("inner_names", {}): + return meta["inner_names"][type_] # type: ignore + else: + return meta["names"].get(type_, type_) + + +def generate_type_declaration( + language: Language, declaration: NestedTypeDeclaration, inner: bool = False +) -> str | bool: + if not isinstance(declaration, tuple): + return _convert_single_type(language, declaration, inner) + + meta = language.get_declaration_metadata() + base_type, nested = declaration + base = _convert_single_type(language, base_type, inner) + + start, finish = meta.get("nested", ("[", "]")) + + if base_type in meta.get("nested_overrides", {}): + start, finish = meta["nested_overrides"][base_type] # type: ignore + + if isinstance(base_type, BasicObjectTypes): + assert ( + len(nested) == 2 + ), "Object types require exactly two nested types if present." + + converted_nested = [] + for x in nested: + converted_x = generate_type_declaration(language, x, True) + assert isinstance(converted_x, str) + converted_nested.append(converted_x) + + if base is True: + # This is a type with "inner" types, i.e. no name, such as Tuple[bool] + return f"{start}{', '.join(converted_nested)}{finish}" + elif base is False: + # This is a type with "rigt" types, i.e. no name, such as bool[] + return f"{', '.join(converted_nested)}{start}{finish}" + else: + return f"{base}{start}{', '.join(converted_nested)}{finish}" diff --git a/tested/languages/haskell/config.py b/tested/languages/haskell/config.py index 50451ebc..a1ffc43f 100644 --- a/tested/languages/haskell/config.py +++ b/tested/languages/haskell/config.py @@ -5,7 +5,12 @@ from tested.datatypes import AllTypes from tested.dodona import AnnotateCode, Message from tested.features import Construct, TypeSupport -from tested.languages.config import CallbackResult, Command, Language +from tested.languages.config import ( + CallbackResult, + Command, + Language, + TypeDeclarationMetadata, +) from tested.languages.conventionalize import ( Conventionable, NamingConventions, @@ -179,3 +184,32 @@ def generate_encoder(self, values: List[Value]) -> str: from tested.languages.haskell import generators return generators.convert_encoder(values) + + def get_declaration_metadata(self) -> TypeDeclarationMetadata: + return { + "names": { # type: ignore + "integer": "Int", + "real": "Double", + "char": "Char", + "text": "String", + "boolean": "Bool", + "nothing": "Nothing", + "undefined": "Nothing", + "int8": "Data.Int.Int8", + "uint8": "Data.Word.Word8", + "int16": "Data.Int.Int16", + "uint16": "Data.Word.Word16", + "int32": "Data.Int.Int32", + "uint32": "Data.Word.Word32", + "int64": "Data.Int.Int64", + "uint64": "Data.Word.Word64", + "bigint": "Integer", + "single_precision": "Float", + "double_precision": "Double", + "any": "Object", + "list": True, + "tuple": True, + "sequence": True, + }, + "nested_overrides": {"tuple": ("(", ")")}, # type: ignore + } diff --git a/tested/languages/haskell/types.json b/tested/languages/haskell/types.json deleted file mode 100644 index a64a521a..00000000 --- a/tested/languages/haskell/types.json +++ /dev/null @@ -1,153 +0,0 @@ -{ - "brackets": { - "tuple": { - "open": "(", - "close": ")" - }, - "list": { - "open": "[", - "close": "]" - }, - "sequence": { - "open": "[", - "close": "]" - } - }, - "console": { - "name": "haskell", - "prompt": ">" - }, - "integer": "Int", - "real": "Double", - "char": "Char", - "text": "String", - "boolean": "Bool", - "sequence": true, - "nothing": "Nothing", - "undefined": "Nothing", - "int8": "Data.Int.Int8", - "uint8": "Data.Word.Word8", - "int16": "Data.Int.Int16", - "uint16": "Data.Word.Word16", - "int32": "Data.Int.Int32", - "uint32": "Data.Word.Word32", - "int64": "Data.Int.Int64", - "uint64": "Data.Word.Word64", - "bigint": "Integer", - "single_precision": "Float", - "double_precision": "Double", - "list": true, - "tuple": true, - "inner": { - }, - "natural": { - "singular": { - "en": { - "integer": "integer", - "real": "real number", - "char": "character", - "text": "string", - "boolean": "boolean", - "sequence": "list", - "set": "set", - "map": "map", - "nothing": "nothing", - "undefined": "nothing", - "int8": "8-bit integer", - "uint8": "8-bit natural number", - "int16": "16-bit integer", - "uint16": "16-bit natural number", - "int32": "32-bit integer", - "uint32": "32-bit natural number", - "int64": "64-bit integer", - "uint64": "64-bit natural number", - "bigint": "big integer", - "single_precision": "single precision floating point number", - "double_precision": "double precision floating point number", - "list": "list", - "tuple": "tuple" - }, - "nl": { - "integer": "geheel getal", - "real": "real getal", - "char": "karakter", - "text": "string", - "boolean": "booleaanse waarde", - "sequence": "lijst", - "set": "verzameling", - "map": "afbeelding", - "nothing": "niets", - "undefined": "niets", - "int8": "8-bit geheel getal", - "uint8": "8-bit natuurlijk getal", - "int16": "16-bit geheel getal", - "uint16": "16-bit natuurlijk getal", - "int32": "32-bit geheel getal", - "uint32": "32-bit natuurlijk getal", - "int64": "64-bit geheel getal", - "uint64": "64-bit natuurlijk getal", - "bigint": "groot geheel getal", - "single_precision": "enkele precisie vlottende kommagetal", - "double_precision": "dubbele precisie vlottende kommagetal", - "double_extended": "uitgebreide precisie vlottende kommagetal", - "fixed_precision": "vast kommagetal", - "array": "array", - "list": "lijst", - "tuple": "tuple", - "any": "ieder" - } - }, - "plural": { - "en": { - "integer": "integers", - "real": "real numbers", - "char": "characters", - "text": "strings", - "boolean": "booleans", - "sequence": "lists", - "set": "sets", - "map": "maps", - "nothing": "nothing", - "undefined": "nothing", - "int8": "8-bit integers", - "uint8": "8-bit natural numbers", - "int16": "16-bit integers", - "uint16": "16-bit natural numbers", - "int32": "32-bit integers", - "uint32": "32-bit natural numbers", - "int64": "64-bit integers", - "uint64": "64-bit natural numbers", - "bigint": "big integers", - "single_precision": "single precision floating point numbers", - "double_precision": "double precision floating point numbers", - "list": "lists", - "tuple": "tuples" - }, - "nl": { - "integer": "gehele getallen", - "real": "real getallen", - "char": "karakters", - "text": "strings", - "boolean": "booleaanse waarden", - "sequence": "lijsten", - "set": "verzamelingen", - "map": "afbeeldingen", - "nothing": "niets", - "undefined": "niets", - "int8": "8-bit gehele getallen", - "uint8": "8-bit natuurlijke getallen", - "int16": "16-bit gehele getallen", - "uint16": "16-bit natuurlijke getallen", - "int32": "32-bit gehele getallen", - "uint32": "32-bit natuurlijke getallen", - "int64": "64-bit gehele getallen", - "uint64": "64-bit natuurlijke getallen", - "bigint": "grote gehele getallen", - "single_precision": "enkele precisie vlottende kommagetallen", - "double_precision": "dubbele precisie vlottende kommagetallen", - "list": "lijsten", - "tuple": "tuples" - } - } - } -} diff --git a/tested/languages/java/config.py b/tested/languages/java/config.py index cd8dc38b..243cefd8 100644 --- a/tested/languages/java/config.py +++ b/tested/languages/java/config.py @@ -5,7 +5,12 @@ from tested.datatypes import AllTypes, ExpressionTypes from tested.dodona import AnnotateCode, Message from tested.features import Construct, TypeSupport -from tested.languages.config import CallbackResult, Command, Language +from tested.languages.config import ( + CallbackResult, + Command, + Language, + TypeDeclarationMetadata, +) from tested.languages.conventionalize import ( Conventionable, NamingConventions, @@ -156,3 +161,52 @@ def generate_encoder(self, values: List[Value]) -> str: from tested.languages.java import generators return generators.convert_encoder(values) + + def get_declaration_metadata(self) -> TypeDeclarationMetadata: + return { + "names": { + "integer": "int", + "real": "double", + "char": "char", + "text": "String", + "boolean": "boolean", + "sequence": "List", + "set": "Set", + "map": "Map", + "nothing": "Void", + "undefined": "Void", + "int8": "byte", + "uint8": "short", + "int16": "short", + "uint16": "int", + "int32": "int", + "uint32": "long", + "int64": "long", + "uint64": "BigInteger", + "bigint": "BigInteger", + "single_precision": "float", + "double_precision": "double", + "double_extended": "BigDecimal", + "fixed_precision": "BigDecimal", + "list": "List", + "any": "Object", + "array": False, + }, + "inner_names": { + "boolean": "Boolean", + "char": "Character", + "integer": "Integer", + "real": "Double", + "single_precision": "Float", + "double_precision": "Double", + "int8": "Byte", + "uint8": "Short", + "int16": "Short", + "uint16": "Integer", + "int32": "Integer", + "uint32": "Long", + "int64": "Long", + }, + "nested": ("<", ">"), + "nested_overrides": {"array": ("[", "]")}, # type: ignore + } diff --git a/tested/languages/java/types.json b/tested/languages/java/types.json deleted file mode 100644 index 7b424a2d..00000000 --- a/tested/languages/java/types.json +++ /dev/null @@ -1,173 +0,0 @@ -{ - "brackets": { - "open": "<", - "close": ">", - "array": { - "open": "[", - "close": "]" - } - }, - "console": { - "name": "java", - "prompt": ">" - }, - "integer": "int", - "real": "double", - "char": "char", - "text": "String", - "boolean": "boolean", - "sequence": "List", - "set": "Set", - "map": "Map", - "nothing": "Void", - "undefined": "Void", - "int8": "byte", - "uint8": "short", - "int16": "short", - "uint16": "int", - "int32": "int", - "uint32": "long", - "int64": "long", - "uint64": "BigInteger", - "bigint": "BigInteger", - "single_precision": "float", - "double_precision": "double", - "double_extended": "BigDecimal", - "fixed_precision": "BigDecimal", - "array": false, - "list": "List", - "any": "Object", - "inner": { - "boolean": "Boolean", - "char": "Character", - "integer": "Integer", - "real": "Double", - "single_precision": "Float", - "double_precision": "Double", - "int8": "Byte", - "uint8": "Short", - "int16": "Short", - "uint16": "Integer", - "int32": "Integer", - "uint32": "Long", - "int64": "Long" - }, - "natural": { - "singular": { - "en": { - "integer": "integer", - "real": "real number", - "char": "character", - "text": "string", - "boolean": "boolean", - "sequence": "list", - "set": "set", - "map": "map", - "nothing": "nothing", - "undefined": "nothing", - "int8": "8-bit integer", - "uint8": "8-bit natural number", - "int16": "16-bit integer", - "uint16": "16-bit natural number", - "int32": "32-bit integer", - "uint32": "32-bit natural number", - "int64": "64-bit integer", - "uint64": "64-bit natural number", - "bigint": "big integer", - "single_precision": "single precision floating point number", - "double_precision": "double precision floating point number", - "double_extended": "extended precision floating point number", - "fixed_precision": "fixed point number", - "array": "array", - "list": "list", - "any": "any" - }, - "nl": { - "integer": "geheel getal", - "real": "reëel getal", - "char": "karakter", - "text": "string", - "boolean": "booleaanse waarde", - "sequence": "lijst", - "set": "verzameling", - "map": "map", - "nothing": "niets", - "undefined": "niets", - "int8": "8-bit geheel getal", - "uint8": "8-bit natuurlijk getal", - "int16": "16-bit geheel getal", - "uint16": "16-bit natuurlijk getal", - "int32": "32-bit geheel getal", - "uint32": "32-bit natuurlijk getal", - "int64": "64-bit geheel getal", - "uint64": "64-bit natuurlijk getal", - "bigint": "groot geheel getal", - "single_precision": "enkele precisie vlottende kommagetal", - "double_precision": "dubbele precisie vlottende kommagetal", - "double_extended": "uitgebreide precisie vlottende kommagetal", - "fixed_precision": "vast kommagetal", - "array": "array", - "list": "lijst", - "any": "ieder" - } - }, - "plural": { - "en": { - "integer": "integers", - "real": "real numbers", - "char": "characters", - "text": "strings", - "boolean": "booleans", - "sequence": "lists", - "set": "sets", - "map": "maps", - "nothing": "nothing", - "undefined": "nothing", - "int8": "8-bit integers", - "uint8": "8-bit natural numbers", - "int16": "16-bit integers", - "uint16": "16-bit natural numbers", - "int32": "32-bit integers", - "uint32": "32-bit natural numbers", - "int64": "64-bit integers", - "uint64": "64-bit natural numbers", - "bigint": "big integers", - "single_precision": "single precision floating point numbers", - "double_precision": "double precision floating point numbers", - "double_extended": "extended precision floating point numbers", - "fixed_precision": "fixed precision floating point numbers", - "array": "arrays", - "list": "lists", - "any": "any" - }, - "nl": { - "integer": "gehele getallen", - "real": "reële getallen", - "char": "karakters", - "text": "strings", - "boolean": "booleaanse waarden", - "sequence": "lijsten", - "set": "verzamelingen", - "map": "afbeeldingen", - "nothing": "niets", - "undefined": "niets", - "int8": "8-bit gehele getallen", - "uint8": "8-bit natuurlijke getallen", - "int16": "16-bit gehele getallen", - "uint16": "16-bit natuurlijke getallen", - "int32": "32-bit gehele getallen", - "uint32": "32-bit natuurlijke getallen", - "int64": "64-bit gehele getallen", - "uint64": "64-bit natuurlijke getallen", - "bigint": "grote gehele getallen", - "single_precision": "enkele precisie vlottende kommagetallen", - "double_precision": "dubbele precisie vlottende kommagetallen", - "double_extended": "uitgebreide precisie vlottende kommagetallen", - "fixed_precision": "vaste kommagetallen", - "array": "arrays", - "list": "lijsten", - "any": "ieder" - } - } - } -} diff --git a/tested/languages/javascript/config.py b/tested/languages/javascript/config.py index ad2dd025..b22a8046 100644 --- a/tested/languages/javascript/config.py +++ b/tested/languages/javascript/config.py @@ -6,7 +6,12 @@ from tested.datatypes import AllTypes, BasicStringTypes, ExpressionTypes from tested.dodona import AnnotateCode, Message from tested.features import Construct, TypeSupport -from tested.languages.config import CallbackResult, Command, Language +from tested.languages.config import ( + CallbackResult, + Command, + Language, + TypeDeclarationMetadata, +) from tested.languages.conventionalize import ( EXECUTION_PREFIX, Conventionable, @@ -200,3 +205,36 @@ def generate_encoder(self, values: List[Value]) -> str: from tested.languages.javascript import generators return generators.convert_encoder(values) + + def get_declaration_metadata(self) -> TypeDeclarationMetadata: + return { + "names": { # type: ignore + "integer": "number", + "real": "number", + "char": "string", + "text": "string", + "boolean": "boolean", + "sequence": "array", + "set": "set", + "map": "object", + "nothing": "null", + "undefined": "undefined", + "int8": "number", + "uint8": "number", + "int16": "number", + "uint16": "number", + "int32": "number", + "uint32": "number", + "int64": "number", + "uint64": "number", + "bigint": "number", + "single_precision": "number", + "double_precision": "number", + "double_extended": "number", + "fixed_precision": "number", + "array": "array", + "list": "array", + "any": "object", + }, + "nested": ("<", ">"), + } diff --git a/tested/languages/javascript/types.json b/tested/languages/javascript/types.json deleted file mode 100644 index 9ecd6263..00000000 --- a/tested/languages/javascript/types.json +++ /dev/null @@ -1,156 +0,0 @@ -{ - "brackets": { - "open": "<", - "close": ">" - }, - "console": { - "name": "javascript", - "prompt": ">" - }, - "integer": "number", - "real": "number", - "char": "string", - "text": "string", - "boolean": "boolean", - "sequence": "array", - "set": "set", - "map": "object", - "nothing": "null", - "undefined": "undefined", - "int8": "number", - "uint8": "number", - "int16": "number", - "uint16": "number", - "int32": "number", - "uint32": "number", - "int64": "number", - "uint64": "number", - "bigint": "number", - "single_precision": "number", - "double_precision": "number", - "double_extended": "number", - "fixed_precision": "number", - "array": "array", - "list": "array", - "any": "object", - "inner": { - }, - "natural": { - "singular": { - "en": { - "integer": "integer", - "real": "real number", - "char": "character", - "text": "string", - "boolean": "boolean", - "sequence": "array", - "set": "set", - "map": "object", - "nothing": "nothing", - "undefined": "undefined", - "int8": "8-bit integer", - "uint8": "8-bit natural number", - "int16": "16-bit integer", - "uint16": "16-bit natural number", - "int32": "32-bit integer", - "uint32": "32-bit natural number", - "int64": "64-bit integer", - "uint64": "64-bit natural number", - "bigint": "big integer", - "single_precision": "single precision floating point number", - "double_precision": "double precision floating point number", - "double_extended": "extended precision floating point number", - "fixed_precision": "fixed point number", - "array": "array", - "list": "array", - "any": "any" - }, - "nl": { - "integer": "geheel getal", - "real": "reëel getal", - "char": "karakter", - "text": "string", - "boolean": "booleaanse waarde", - "sequence": "array", - "set": "verzameling", - "map": "object", - "nothing": "niets", - "undefined": "niet gedefinieerd", - "int8": "8-bit geheel getal", - "uint8": "8-bit natuurlijk getal", - "int16": "16-bit geheel getal", - "uint16": "16-bit natuurlijk getal", - "int32": "32-bit geheel getal", - "uint32": "32-bit natuurlijk getal", - "int64": "64-bit geheel getal", - "uint64": "64-bit natuurlijk getal", - "bigint": "groot geheel getal", - "single_precision": "enkele precisie vlottende kommagetal", - "double_precision": "dubbele precisie vlottende kommagetal", - "double_extended": "uitgebreide precisie vlottende kommagetal", - "fixed_precision": "vast kommagetal", - "array": "array", - "list": "array", - "any": "ieder" - } - }, - "plural": { - "en": { - "integer": "integers", - "real": "real numbers", - "char": "characters", - "text": "strings", - "boolean": "booleans", - "sequence": "arrays", - "set": "sets", - "map": "objects", - "nothing": "nothing", - "undefined": "undefined", - "int8": "8-bit integers", - "uint8": "8-bit natural numbers", - "int16": "16-bit integers", - "uint16": "16-bit natural numbers", - "int32": "32-bit integers", - "uint32": "32-bit natural numbers", - "int64": "64-bit integers", - "uint64": "64-bit natural numbers", - "bigint": "big integers", - "single_precision": "single precision floating point numbers", - "double_precision": "double precision floating point numbers", - "double_extended": "extended precision floating point numbers", - "fixed_precision": "fixed precision floating point numbers", - "array": "arrays", - "list": "arrays", - "any": "any" - }, - "nl": { - "integer": "gehele getallen", - "real": "reële getallen", - "char": "karakters", - "text": "strings", - "boolean": "booleaanse waarden", - "sequence": "arrays", - "set": "verzamelingen", - "map": "objects", - "nothing": "niets", - "undefined": "niet gedefinieerd", - "int8": "8-bit gehele getallen", - "uint8": "8-bit natuurlijke getallen", - "int16": "16-bit gehele getallen", - "uint16": "16-bit natuurlijke getallen", - "int32": "32-bit gehele getallen", - "uint32": "32-bit natuurlijke getallen", - "int64": "64-bit gehele getallen", - "uint64": "64-bit natuurlijke getallen", - "bigint": "grote gehele getallen", - "single_precision": "enkele precisie vlottende kommagetallen", - "double_precision": "dubbele precisie vlottende kommagetallen", - "double_extended": "uitgebreide precisie vlottende kommagetallen", - "fixed_precision": "vaste kommagetallen", - "array": "arrays", - "list": "arrays", - "any": "ieder" - } - } - } -} diff --git a/tested/languages/kotlin/config.py b/tested/languages/kotlin/config.py index ec1a4fb0..fa97497c 100644 --- a/tested/languages/kotlin/config.py +++ b/tested/languages/kotlin/config.py @@ -7,7 +7,12 @@ from tested.datatypes import AllTypes, ExpressionTypes from tested.dodona import AnnotateCode, Message, Status from tested.features import Construct, TypeSupport -from tested.languages.config import CallbackResult, Command, Language +from tested.languages.config import ( + CallbackResult, + Command, + Language, + TypeDeclarationMetadata, +) from tested.languages.conventionalize import ( EXECUTION_PREFIX, Conventionable, @@ -229,3 +234,36 @@ def generate_encoder(self, values: List[Value]) -> str: from tested.languages.kotlin import generators return generators.convert_encoder(values) + + def get_declaration_metadata(self) -> TypeDeclarationMetadata: + return { + "names": { # type: ignore + "integer": "Int", + "real": "Double", + "char": "Char", + "text": "String", + "boolean": "Boolean", + "sequence": "List", + "set": "Set", + "map": "Map", + "nothing": "Void", + "undefined": "Void", + "int8": "Byte", + "uint8": "UByte", + "int16": "Short", + "uint16": "UShort", + "int32": "Int", + "uint32": "UInt", + "int64": "Long", + "uint64": "ULong", + "bigint": "BigInteger", + "single_precision": "Float", + "double_precision": "Double", + "double_extended": "BigDecimal", + "fixed_precision": "BigDecimal", + "array": "Array", + "list": "List", + "any": "Any", + }, + "nested": ("<", ">"), + } diff --git a/tested/languages/kotlin/types.json b/tested/languages/kotlin/types.json deleted file mode 100644 index 9ca40199..00000000 --- a/tested/languages/kotlin/types.json +++ /dev/null @@ -1,156 +0,0 @@ -{ - "brackets": { - "open": "<", - "close": ">" - }, - "console": { - "name": "kotlin", - "prompt": ">" - }, - "integer": "Int", - "real": "Double", - "char": "Char", - "text": "String", - "boolean": "Boolean", - "sequence": "List", - "set": "Set", - "map": "Map", - "nothing": "Void", - "undefined": "Void", - "int8": "Byte", - "uint8": "UByte", - "int16": "Short", - "uint16": "UShort", - "int32": "Int", - "uint32": "UInt", - "int64": "Long", - "uint64": "ULong", - "bigint": "BigInteger", - "single_precision": "Float", - "double_precision": "Double", - "double_extended": "BigDecimal", - "fixed_precision": "BigDecimal", - "array": "Array", - "list": "List", - "any": "Any", - "inner": { - }, - "natural": { - "singular": { - "en": { - "integer": "integer", - "real": "real number", - "char": "character", - "text": "string", - "boolean": "boolean", - "sequence": "list", - "set": "set", - "map": "map", - "nothing": "nothing", - "undefined": "nothing", - "int8": "8-bit integer", - "uint8": "8-bit natural number", - "int16": "16-bit integer", - "uint16": "16-bit natural number", - "int32": "32-bit integer", - "uint32": "32-bit natural number", - "int64": "64-bit integer", - "uint64": "64-bit natural number", - "bigint": "big integer", - "single_precision": "single precision floating point number", - "double_precision": "double precision floating point number", - "double_extended": "extended precision floating point number", - "fixed_precision": "fixed point number", - "array": "array", - "list": "list", - "any": "any" - }, - "nl": { - "integer": "geheel getal", - "real": "reëel getal", - "char": "karakter", - "text": "string", - "boolean": "booleaanse waarde", - "sequence": "lijst", - "set": "verzameling", - "map": "map", - "nothing": "niets", - "undefined": "niets", - "int8": "8-bit geheel getal", - "uint8": "8-bit natuurlijk getal", - "int16": "16-bit geheel getal", - "uint16": "16-bit natuurlijk getal", - "int32": "32-bit geheel getal", - "uint32": "32-bit natuurlijk getal", - "int64": "64-bit geheel getal", - "uint64": "64-bit natuurlijk getal", - "bigint": "groot geheel getal", - "single_precision": "enkele precisie vlottende kommagetal", - "double_precision": "dubbele precisie vlottende kommagetal", - "double_extended": "uitgebreide precisie vlottende kommagetal", - "fixed_precision": "vast kommagetal", - "array": "array", - "list": "lijst", - "any": "ieder" - } - }, - "plural": { - "en": { - "integer": "integers", - "real": "real numbers", - "char": "characters", - "text": "strings", - "boolean": "booleans", - "sequence": "lists", - "set": "sets", - "map": "maps", - "nothing": "nothing", - "undefined": "nothing", - "int8": "8-bit integers", - "uint8": "8-bit natural numbers", - "int16": "16-bit integers", - "uint16": "16-bit natural numbers", - "int32": "32-bit integers", - "uint32": "32-bit natural numbers", - "int64": "64-bit integers", - "uint64": "64-bit natural numbers", - "bigint": "big integers", - "single_precision": "single precision floating point numbers", - "double_precision": "double precision floating point numbers", - "double_extended": "extended precision floating point numbers", - "fixed_precision": "fixed precision floating point numbers", - "array": "arrays", - "list": "lists", - "any": "any" - }, - "nl": { - "integer": "gehele getallen", - "real": "reële getallen", - "char": "karakters", - "text": "strings", - "boolean": "booleaanse waarden", - "sequence": "lijsten", - "set": "verzamelingen", - "map": "afbeeldingen", - "nothing": "niets", - "undefined": "niets", - "int8": "8-bit gehele getallen", - "uint8": "8-bit natuurlijke getallen", - "int16": "16-bit gehele getallen", - "uint16": "16-bit natuurlijke getallen", - "int32": "32-bit gehele getallen", - "uint32": "32-bit natuurlijke getallen", - "int64": "64-bit gehele getallen", - "uint64": "64-bit natuurlijke getallen", - "bigint": "grote gehele getallen", - "single_precision": "enkele precisie vlottende kommagetallen", - "double_precision": "dubbele precisie vlottende kommagetallen", - "double_extended": "uitgebreide precisie vlottende kommagetallen", - "fixed_precision": "vaste kommagetallen", - "array": "arrays", - "list": "lijsten", - "any": "ieder" - } - } - } -} diff --git a/tested/languages/python/config.py b/tested/languages/python/config.py index 530cf726..9f036d3b 100644 --- a/tested/languages/python/config.py +++ b/tested/languages/python/config.py @@ -7,7 +7,12 @@ from tested.datatypes import AllTypes, ExpressionTypes from tested.dodona import AnnotateCode, Message, Severity from tested.features import Construct, TypeSupport -from tested.languages.config import CallbackResult, Command, Language +from tested.languages.config import ( + CallbackResult, + Command, + Language, + TypeDeclarationMetadata, +) from tested.languages.conventionalize import ( Conventionable, NamingConventions, @@ -224,3 +229,37 @@ def generate_encoder(self, values: List[Value]) -> str: from tested.languages.python import generators return generators.convert_encoder(values) + + def get_declaration_metadata(self) -> TypeDeclarationMetadata: + return { + "names": { # type: ignore + "integer": "int", + "real": "float", + "char": "str", + "text": "str", + "boolean": "bool", + "sequence": "List", + "set": "Set", + "map": "Dict", + "nothing": "None", + "undefined": "None", + "int8": "int", + "uint8": "int", + "int16": "int", + "uint16": "int", + "int32": "int", + "uint32": "int", + "int64": "int", + "uint64": "int", + "bigint": "int", + "single_precision": "float", + "double_precision": "float", + "double_extended": "Decimal", + "fixed_precision": "Decimal", + "array": "List", + "list": "List", + "tuple": "Tuple", + "any": "Any", + }, + "prompt": ">>>", + } diff --git a/tested/languages/python/types.json b/tested/languages/python/types.json index 24140a81..a5033e79 100644 --- a/tested/languages/python/types.json +++ b/tested/languages/python/types.json @@ -1,41 +1,8 @@ { - "brackets": { - "open": "[", - "close": "]" - }, "console": { "name": "python", "prompt": ">>>" }, - "integer": "int", - "real": "float", - "char": "str", - "text": "str", - "boolean": "bool", - "sequence": "List", - "set": "Set", - "map": "Dict", - "nothing": "None", - "undefined": "None", - "int8": "int", - "uint8": "int", - "int16": "int", - "uint16": "int", - "int32": "int", - "uint32": "int", - "int64": "int", - "uint64": "int", - "bigint": "int", - "single_precision": "float", - "double_precision": "float", - "double_extended": "Decimal", - "fixed_precision": "Decimal", - "array": "List", - "list": "List", - "tuple": "Tuple", - "any": "Any", - "inner": { - }, "natural": { "singular": { "en": { diff --git a/tested/languages/runhaskell/config.py b/tested/languages/runhaskell/config.py index b4d4af4a..1f836b0a 100644 --- a/tested/languages/runhaskell/config.py +++ b/tested/languages/runhaskell/config.py @@ -3,7 +3,6 @@ from tested.languages.config import CallbackResult, Command from tested.languages.conventionalize import submission_file -from tested.languages.description_generator import DescriptionGenerator from tested.languages.haskell.config import Haskell @@ -33,10 +32,3 @@ def path_to_dependencies(self) -> List[Path]: return [ self.config.dodona.judge / "tested" / "languages" / "haskell" / "templates" ] - - def get_description_generator(self) -> DescriptionGenerator: - if self._description_generator is None: - assert self.config - config_dir = self.config.dodona.judge / "tested" / "languages" / "haskell" - self._description_generator = DescriptionGenerator(self, config_dir) - return self._description_generator diff --git a/tests/descriptions/example.haskell.md b/tests/descriptions/example.haskell.md new file mode 100644 index 00000000..733db183 --- /dev/null +++ b/tests/descriptions/example.haskell.md @@ -0,0 +1,12 @@ +Aan de functie `splitsInWoorden` moet een string (`String`) doorgegeven worden. +De functie geeft een lijst van letters (`[Char]`) terug. + +```haskell +aFunctionCall "yes" +``` + +This is haskell. + +```console?lang=haskell&prompt=> +> aFunctionCall "yes" +``` diff --git a/tests/descriptions/example.java.md b/tests/descriptions/example.java.md new file mode 100644 index 00000000..f2a3c0ee --- /dev/null +++ b/tests/descriptions/example.java.md @@ -0,0 +1,12 @@ +Aan de functie `splitsInWoorden` moet een string (`String`) doorgegeven worden. +De functie geeft een lijst van letters (`List`) terug. + +```java +Submission.aFunctionCall("yes") +``` + +This is java. + +```console?lang=java&prompt=> +> Submission.aFunctionCall("yes") +``` diff --git a/tests/descriptions/example.kotlin.md b/tests/descriptions/example.kotlin.md new file mode 100644 index 00000000..111d5d42 --- /dev/null +++ b/tests/descriptions/example.kotlin.md @@ -0,0 +1,12 @@ +Aan de functie `splitsInWoorden` moet een string (`String`) doorgegeven worden. +De functie geeft een lijst van letters (`List`) terug. + +```kotlin +aFunctionCall("yes") +``` + +This is kotlin. + +```console?lang=kotlin&prompt=> +> aFunctionCall("yes") +``` diff --git a/tests/descriptions/example.md.mako b/tests/descriptions/example.md.mako new file mode 100644 index 00000000..8388c676 --- /dev/null +++ b/tests/descriptions/example.md.mako @@ -0,0 +1,20 @@ +Aan de functie `${function("splits_in_woorden")}` moet een string (`${datatype("text")}`) doorgegeven worden. +De functie geeft een lijst van letters (`${datatype("sequence", "char")}`) terug. + +```tested +a_function_call("yes") +``` + +% if programming_language == "python": +This is python. +% elif programming_language == "kotlin": +This is kotlin. +% elif programming_language == "java": +This is java. +% elif programming_language == "haskell": +This is haskell. +% endif + +```console?lang=tested +>>> a_function_call("yes") +``` diff --git a/tests/descriptions/example.python.md b/tests/descriptions/example.python.md new file mode 100644 index 00000000..0eb85d4b --- /dev/null +++ b/tests/descriptions/example.python.md @@ -0,0 +1,12 @@ +Aan de functie `splits_in_woorden` moet een string (`str`) doorgegeven worden. +De functie geeft een lijst van letters (`List[str]`) terug. + +```python +a_function_call('yes') +``` + +This is python. + +```console?lang=python&prompt=>>> +>>> a_function_call('yes') +``` diff --git a/tests/test_problem_statements.py b/tests/test_problem_statements.py new file mode 100644 index 00000000..4796b573 --- /dev/null +++ b/tests/test_problem_statements.py @@ -0,0 +1,243 @@ +from pathlib import Path +from typing import Any + +import pytest + +from tested.descriptions import process_problem_statement +from tested.dsl.ast_translator import InvalidDslError + + +@pytest.mark.parametrize("language", ["python", "kotlin", "java", "haskell"]) +def test_python_description(language: str): + test_dir = Path(__file__).parent + description_template = test_dir / "descriptions" / "example.md.mako" + description_python = test_dir / "descriptions" / f"example.{language}.md" + with open(description_template, "r") as dp: + template = dp.read() + with open(description_python, "r") as dp: + expected = dp.read() + actual = process_problem_statement(template, language) + + assert actual == expected + assert f"This is {language}." in actual + + +@pytest.mark.parametrize( + ("lang", "expected"), + [ + ("python", "this_is_a_function_name"), + ("java", "thisIsAFunctionName"), + ("c", "this_is_a_function_name"), + ("kotlin", "thisIsAFunctionName"), + ("javascript", "thisIsAFunctionName"), + ("haskell", "thisIsAFunctionName"), + ("runhaskell", "thisIsAFunctionName"), + ], +) +def test_template_function_name(lang: str, expected: str): + template = '${function("this_is_a_function_name")}' + instance = process_problem_statement(template, lang) + assert instance == f"{expected}" + + +@pytest.mark.parametrize( + ("lang", "tested_type", "expected"), + [ + # Python + ("python", "'integer'", "int"), + ("python", "'real'", "float"), + ("python", "'text'", "str"), + ("python", '"sequence", "integer"', "List[int]"), + ("python", '"array", ("set", ("integer", ))', "List[Set[int]]"), + ( + "python", + '"tuple", ("sequence", ("real", )), "text"', + "Tuple[List[float], str]", + ), + # Java + ("java", "'integer'", "int"), + ("java", "'real'", "double"), + ("java", "'text'", "String"), + ("java", '"sequence", "integer"', "List"), + ("java", '"array", ("set", ("integer", ))', "Set[]"), + # c + ("c", "'integer'", "int"), + ("c", "'real'", "double"), + ("c", "'text'", "char*"), + # Kotlin + ("kotlin", "'integer'", "Int"), + ("kotlin", "'real'", "Double"), + ("kotlin", "'text'", "String"), + ("kotlin", '"sequence", "integer"', "List"), + ("kotlin", '"array", ("set", ("integer", ))', "Array>"), + # JavaScript + ("javascript", "'integer'", "number"), + ("javascript", "'real'", "number"), + ("javascript", "'text'", "string"), + ("javascript", '"sequence", "integer"', "array"), + ("javascript", '"array", ("set", ("integer", ))', "array>"), + # Haskell + ("haskell", "'integer'", "Int"), + ("haskell", "'real'", "Double"), + ("haskell", "'text'", "String"), + ("haskell", '"sequence", "integer"', "[Int]"), + ( + "haskell", + '"tuple", ("sequence", ("real", )), "text"', + "([Double], String)", + ), + ], +) +def test_template_type_name(lang: str, tested_type: Any, expected: str): + template = f"""${'{'}datatype({tested_type}){'}'}""" + instance = process_problem_statement(template, lang) + assert instance == f"{expected}" + + +@pytest.mark.parametrize( + ("lang", "tested_type", "expected"), + [ + # Python + ("python", "'sequence'", "sequence"), + ("python", "'map'", "map"), + # Java + ("java", "'sequence'", "sequence"), + ("java", "'map'", "map"), + # Kotlin + ("kotlin", "'sequence'", "sequence"), + ("kotlin", "'map'", "map"), + # JavaScript + ("javascript", "'sequence'", "sequence"), + ("javascript", "'map'", "map"), + # Haskell + ("haskell", "'sequence'", "sequence"), + ("haskell", "'list'", "list"), + ], +) +def test_template_natural_type_name(lang: str, tested_type: Any, expected: str): + template = f"""${{datatype_common({tested_type})}}""" + instance = process_problem_statement(template, lang) + assert instance == f"{expected}" + + +@pytest.mark.parametrize( + ("lang", "tested_type", "expected"), + [ + # Python + ("python", "'sequence'", "sequentie"), + ("python", "'map'", "afbeelding"), + # Java + ("java", "'sequence'", "sequentie"), + ("java", "'map'", "afbeelding"), + # Kotlin + ("kotlin", "'sequence'", "sequentie"), + ("kotlin", "'map'", "afbeelding"), + # JavaScript + ("javascript", "'sequence'", "sequentie"), + ("javascript", "'map'", "afbeelding"), + # Haskell + ("haskell", "'sequence'", "sequentie"), + ("haskell", "'list'", "lijst"), + ], +) +def test_template_natural_type_name_nl(lang: str, tested_type: Any, expected: str): + template = f"""${{datatype_common({tested_type})}}""" + instance = process_problem_statement(template, lang, "nl") + 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 :: Int" if lang == "haskell" else "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"), + [ + ( + "python", + "random = Random()\nrandom.new_sequence(10, 10)\n[10, 5, 2, 8, 7, 1, 3, 4, 9, 6]", + ), + ( + "java", + "Random random = new Random()\nrandom.newSequence(10, 10)\nList.of(10, 5, 2, 8, 7, 1, 3, 4, 9, 6)", + ), + ( + "kotlin", + "var random = Random()\nrandom!!.newSequence(10, 10)\nlistOf(10, 5, 2, 8, 7, 1, 3, 4, 9, 6)", + ), + ( + "javascript", + "let random = new Random()\nrandom.newSequence(10, 10)\n[10, 5, 2, 8, 7, 1, 3, 4, 9, 6]", + ), + ], +) +def test_template_statement_expression(lang: str, expected: str): + template = """${t('random = Random()')} +${t('random.new_sequence(10, 10)')} +${t('[10, 5, 2, 8, 7, 1, 3, 4, 9, 6]')}""" + instance = process_problem_statement(template, lang) + assert instance == f"{expected}" + + +@pytest.mark.parametrize( + "lang", + [ + "python", + "java", + "c", + "kotlin", + "javascript", + "haskell", + ], +) +def test_template_escaped_string_code_block_markdown(lang: str): + template = r"""```tested +"alpha\"beta\tname" +```""" + instance = process_problem_statement(template, lang) + expected_str = ( + "'alpha\"beta\\tname'" if lang == "python" else r'"alpha\"beta\tname"' + ) + expected = f"""```{lang} +{expected_str} +```""" + assert instance == expected + + +def test_template_failed_string(): + template = r"""```tested +> integer x = \ +data(1, 2, +"alpha +beta") +```""" + with pytest.raises(InvalidDslError): + process_problem_statement(template, "java") diff --git a/tests/test_serialisation.py b/tests/test_serialisation.py index c7e8a86c..96ab16f4 100644 --- a/tests/test_serialisation.py +++ b/tests/test_serialisation.py @@ -40,6 +40,7 @@ from tested.judge.utils import BaseExecutionResult, copy_from_paths_to_path from tested.languages.conventionalize import conventionalize_namespace from tested.serialisation import ( + Assignment, BooleanType, NothingType, NumberType, @@ -333,3 +334,11 @@ def test_valid_type_map(language: str, tmp_path: Path, pytestconfig): basic_type = resolve_to_basic(advanced_type) basic_value = type_map[basic_type] assert basic_value == TypeSupport.SUPPORTED + + +def test_nested_type_declaration(tmp_path: Path, pytestconfig): + # Get a type map. + conf = configuration(pytestconfig, "", "java", tmp_path) + plan = Suite() + bundle = create_bundle(conf, sys.stdout, plan) + statement = Assignment() diff --git a/tests/test_utils.py b/tests/test_utils.py index 50dd3b46..8d2276cd 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,12 +1,9 @@ -import html import json from pathlib import Path -from typing import Any, Iterator +from typing import List -import pytest import yaml -from tested.description_instance import create_description_instance from tested.utils import sorted_no_duplicates from tests.manual_utils import assert_valid_output, configuration, execute_config @@ -59,360 +56,6 @@ def test_run_doctests_tested_conventionalize(): assert f == 0 -@pytest.mark.parametrize( - ("lang", "expected"), - [ - ("python", "this_is_a_function_name"), - ("java", "thisIsAFunctionName"), - ("c", "this_is_a_function_name"), - ("kotlin", "thisIsAFunctionName"), - ("javascript", "thisIsAFunctionName"), - ("haskell", "thisIsAFunctionName"), - ("runhaskell", "thisIsAFunctionName"), - ], -) -def test_template_function_name(lang: str, expected: str): - template = '${function("this_is_a_function_name")}' - instance = create_description_instance( - template, programming_language=lang, is_html=False - ) - assert instance == f"{expected}" - - -@pytest.mark.parametrize( - ("lang", "tested_type", "expected"), - [ - # Python - ("python", "'integer'", "int"), - ("python", "'real'", "float"), - ("python", "'text'", "str"), - ("python", '("sequence", "integer")', "List[int]"), - ("python", '("array", ("set", "integer"))', "List[Set[int]]"), - ( - "python", - '("tuple", [("sequence", "real"), "text"])', - "Tuple[List[float], str]", - ), - # Java - ("java", "'integer'", "int"), - ("java", "'real'", "double"), - ("java", "'text'", "String"), - ("java", '("sequence", "integer")', "List"), - ("java", '("array", ("set", "integer"))', "Set[]"), - # c - ("c", "'integer'", "int"), - ("c", "'real'", "double"), - ("c", "'text'", "char*"), - # Kotlin - ("kotlin", "'integer'", "Int"), - ("kotlin", "'real'", "Double"), - ("kotlin", "'text'", "String"), - ("kotlin", '("sequence", "integer")', "List"), - ("kotlin", '("array", ("set", "integer"))', "Array>"), - # JavaScript - ("javascript", "'integer'", "number"), - ("javascript", "'real'", "number"), - ("javascript", "'text'", "string"), - ("javascript", '("sequence", "integer")', "array"), - ("javascript", '("array", ("set", "integer"))', "array>"), - # Haskell - ("haskell", "'integer'", "Int"), - ("haskell", "'real'", "Double"), - ("haskell", "'text'", "String"), - ("haskell", '("sequence", "integer")', "[Int]"), - ( - "haskell", - '("tuple", [("sequence", "real"), "text"])', - "([Double], String)", - ), - ], -) -def test_template_type_name(lang: str, tested_type: Any, expected: str): - template = f"""${'{'}datatype({tested_type}){'}'}""" - instance = create_description_instance( - template, programming_language=lang, is_html=False - ) - assert instance == f"{expected}" - - -@pytest.mark.parametrize( - ("lang", "tested_type", "expected"), - [ - # Python - ("python", "'sequence'", "list"), - ("python", "'map'", "dictionary"), - # Java - ("java", "'sequence'", "list"), - ("java", "'map'", "map"), - # Kotlin - ("kotlin", "'sequence'", "list"), - ("kotlin", "'map'", "map"), - # JavaScript - ("javascript", "'sequence'", "array"), - ("javascript", "'map'", "object"), - # Haskell - ("haskell", "'sequence'", "list"), - ("haskell", "'list'", "list"), - ], -) -def test_template_natural_type_name(lang: str, tested_type: Any, expected: str): - template = f"""${{datatype_common({tested_type})}}""" - instance = create_description_instance( - template, programming_language=lang, is_html=False - ) - assert instance == f"{expected}" - - -@pytest.mark.parametrize( - ("lang", "tested_type", "expected"), - [ - # Python - ("python", "'sequence'", "lijst"), - ("python", "'map'", "dictionary"), - # Java - ("java", "'sequence'", "lijst"), - ("java", "'map'", "map"), - # Kotlin - ("kotlin", "'sequence'", "lijst"), - ("kotlin", "'map'", "map"), - # JavaScript - ("javascript", "'sequence'", "array"), - ("javascript", "'map'", "object"), - # Haskell - ("haskell", "'sequence'", "lijst"), - ("haskell", "'list'", "lijst"), - ], -) -def test_template_natural_type_name_nl(lang: str, tested_type: Any, expected: str): - template = f"""${{datatype_common({tested_type})}}""" - instance = create_description_instance( - template, programming_language=lang, is_html=False, natural_language="nl" - ) - assert instance == f"{expected}" - - -def test_template_type_name_override(): - template = """${datatype("integer", {"java": {"integer": "long"}})}""" - instance = create_description_instance( - template, programming_language="java", is_html=False - ) - assert instance == "long" - - -@pytest.mark.parametrize( - ("lang", "prompt"), - [ - ("python", ">>>"), - ("java", ">"), - ("c", ">"), - ("kotlin", ">"), - ("javascript", ">"), - ("haskell", ">"), - ], -) -def test_template_code_block_markdown(lang: str, prompt: str): - template = """```tested -> random() -5 -```""" - expected_stmt = ( - "random" - if lang == "haskell" - else "Submission.random()" - if lang == "java" - else "random()" - ) - expected_expr = "5 :: Int" if lang == "haskell" else "5" - instance = create_description_instance( - template, programming_language=lang, is_html=False - ) - expected = f"""```console?lang={lang}&prompt={prompt} -{prompt} {expected_stmt} -{expected_expr} -```""" - assert instance == expected - - -@pytest.mark.parametrize( - ("lang", "prompt", "expected_stmt", "expected_expr"), - [ - ( - "python", - ">>>", - 'random()', - '5', - ), - ( - "java", - ">", - 'Submission.' - 'random()', - '5', - ), - ( - "c", - ">", - 'random()', - '5', - ), - ( - "kotlin", - ">", - 'random()', - '5', - ), - ( - "javascript", - ">", - 'random()', - '5', - ), - ( - "haskell", - ">", - 'random', - '5 :: Int', - ), - ], -) -def test_template_code_block_html( - lang: str, prompt: str, expected_stmt: str, expected_expr: str -): - template = """ -> random() -5 -""" - instance = create_description_instance( - template, programming_language=lang, is_html=True - ) - expected = f""" -{html.escape(prompt)} {expected_stmt} -{expected_expr} -""" - assert instance == expected - - -@pytest.mark.parametrize( - ("lang", "expected"), - [ - ( - "python", - ">>> random = Random()\n>>> random.new_sequence(10, 10)\n[10, 5, 2, 8, 7, 1, 3, 4, 9, 6]", - ), - ( - "java", - "> Random random = new Random()\n> random.newSequence(10, 10)\nList.of(10, 5, 2, 8, 7, 1, 3, 4, 9, 6)", - ), - ( - "kotlin", - "> var random = Random()\n> random!!.newSequence(10, 10)\nlistOf(10, 5, 2, 8, 7, 1, 3, 4, 9, 6)", - ), - ( - "javascript", - "> let random = new Random()\n> random.newSequence(10, 10)\n[10, 5, 2, 8, 7, 1, 3, 4, 9, 6]", - ), - ], -) -def test_template_statement_expression(lang: str, expected: str): - template = """${statement('random = Random()')} -${statement('random.new_sequence(10, 10)')} -${expression('[10, 5, 2, 8, 7, 1, 3, 4, 9, 6]')}""" - instance = create_description_instance( - template, programming_language=lang, is_html=False - ) - assert instance == f"{expected}" - - -@pytest.mark.parametrize( - ("lang", "prompt", "expected"), - [ - ("python", ">>>", "x = data(1, 2, 'alpha')"), - ("java", ">", 'int x = Submission.data(1, 2, "alpha")'), - ("c", ">", 'long long x = data(1, 2, "alpha");'), - ("kotlin", ">", 'var x = data(1, 2, "alpha")'), - ("javascript", ">", 'let x = data(1, 2, "alpha")'), - ("haskell", ">", 'let x = data 1 :: Int 2 :: Int "alpha"'), - ], -) -def test_template_multi_line_code_block_markdown(lang: str, prompt: str, expected: str): - template = r"""```tested -> x: integer = \ -data(1, 2, -"alpha") -```""" - instance = create_description_instance( - template, programming_language=lang, is_html=False - ) - expected = f"""```console?lang={lang}&prompt={prompt} -{prompt} {expected} -```""" - assert instance == expected - - -@pytest.mark.parametrize( - ("lang", "prompt"), - [ - ("python", ">>>"), - ("java", ">"), - ("c", ">"), - ("kotlin", ">"), - ("javascript", ">"), - ("haskell", ">"), - ], -) -def test_template_escaped_string_code_block_markdown(lang: str, prompt: str): - template = r"""```tested -"alpha\"beta\tname" -```""" - instance = create_description_instance( - template, programming_language=lang, is_html=False - ) - expected_str = ( - "'alpha\"beta\\tname'" if lang == "python" else r'"alpha\"beta\tname"' - ) - expected = f"""```console?lang={lang}&prompt={prompt} -{expected_str} -```""" - assert instance == expected - - -def test_template_failed_string(): - template = r"""```tested -> integer x = \ -data(1, 2, -"alpha -beta") -```""" - with pytest.raises(ValueError): - create_description_instance( - template, programming_language="java", is_html=False - ) - - -def test_template_failed_brackets_mismatch(): - template = r"""```tested -> integer x = \ -data(1, 2, -("alpha beta"}) -```""" - with pytest.raises(ValueError): - create_description_instance( - template, programming_language="java", is_html=False - ) - - -def test_template_failed_unbalanced_brackets(): - template = r"""```tested -> integer x = \ -data(1, 2, -"alpha beta" -```""" - with pytest.raises(ValueError): - create_description_instance( - template, programming_language="java", is_html=False - ) - - def test_sort_no_duplicates(): data = [ "a", @@ -573,7 +216,7 @@ def test_valid_yaml_and_json(): Test to validate if all YAML and JSON can be parsed correctly. """ - def recursive_iter_dir(directory: Path) -> Iterator[Path]: + def recursive_iter_dir(directory: Path) -> List[Path]: yaml_and_json_files = [] for file in directory.iterdir(): if file.is_file() and (