From 042890e02c00fe4b45ffc37585b62461842b2f6b Mon Sep 17 00:00:00 2001 From: Niko Strijbol Date: Wed, 20 Dec 2023 14:32:37 +0100 Subject: [PATCH 1/2] Use here-doc when using both arguments and stdin --- tested/languages/generation.py | 26 ++++++++++++++----- tests/test_functionality.py | 47 +++++++++++++++++++++++++++++++++- 2 files changed, 66 insertions(+), 7 deletions(-) diff --git a/tested/languages/generation.py b/tested/languages/generation.py index bf5241c7..c5b61b10 100644 --- a/tested/languages/generation.py +++ b/tested/languages/generation.py @@ -89,6 +89,14 @@ def _handle_link_files(link_files: Iterable[FileUrl], language: str) -> tuple[st ) +def _get_heredoc_token(stdin: str) -> str: + delimiter = "STDIN" + stdin_lines = stdin.splitlines() + while delimiter in stdin: + delimiter = delimiter + "N" + return delimiter + + def get_readable_input( bundle: Bundle, case: Testcase ) -> tuple[ExtendedMessage, set[FileUrl]]: @@ -113,20 +121,26 @@ def get_readable_input( assert isinstance(case.input, MainInput) # See https://rouge-ruby.github.io/docs/Rouge/Lexers/ConsoleLexer.html format_ = "console" + # Determine the command (with arguments) submission = submission_name(bundle.language) command = shlex.join([submission] + case.input.arguments) args = f"$ {command}" + # Determine the stdin if isinstance(case.input.stdin, TextData): stdin = case.input.stdin.get_data_as_string(bundle.config.resources) else: stdin = "" - if not stdin: - text = args + + # If we have both stdin and arguments, we use a here-document. + if case.input.arguments and stdin: + assert stdin[-1] == "\n", "stdin must end with a newline" + delimiter = _get_heredoc_token(stdin) + text = f"{args} << '{delimiter}'\n{stdin}{delimiter}" + elif stdin: + assert not case.input.arguments + text = stdin else: - if case.input.arguments: - text = f"{args}\n{stdin}" - else: - text = stdin + text = args elif isinstance(case.input, Statement): format_ = bundle.config.programming_language text = generate_statement(bundle, case.input) diff --git a/tests/test_functionality.py b/tests/test_functionality.py index dc6cb8ec..361fc67f 100644 --- a/tests/test_functionality.py +++ b/tests/test_functionality.py @@ -25,7 +25,7 @@ NumberType, StringType, ) -from tested.testsuite import Context, MainInput, Suite, Tab, Testcase +from tested.testsuite import Context, MainInput, Suite, Tab, Testcase, TextData from tests.manual_utils import assert_valid_output, configuration, execute_config COMPILE_LANGUAGES = [ @@ -1036,3 +1036,48 @@ def test_main_call_quotes(tmp_path: Path, pytestconfig): assert ( actual.description == "$ submission hello 'it'\"'\"'s' '$yes' --hello=no -hello" ) + + +def test_stdin_and_arguments_use_heredoc(tmp_path: Path, pytestconfig): + conf = configuration( + pytestconfig, + "echo-function", + "bash", + tmp_path, + "two.yaml", + "top-level-output", + ) + the_input = Testcase( + input=MainInput( + arguments=["hello"], stdin=TextData(data="One line\nSecond line\n") + ) + ) + suite = Suite(tabs=[Tab(contexts=[Context(testcases=[the_input])], name="hallo")]) + bundle = create_bundle(conf, sys.stdout, suite) + actual, _ = get_readable_input(bundle, the_input) + + assert ( + actual.description + == "$ submission hello << 'STDIN'\nOne line\nSecond line\nSTDIN" + ) + + +def test_stdin_token_is_unique(tmp_path: Path, pytestconfig): + conf = configuration( + pytestconfig, + "echo-function", + "bash", + tmp_path, + "two.yaml", + "top-level-output", + ) + the_input = Testcase( + input=MainInput(arguments=["hello"], stdin=TextData(data="One line\nSTDIN\n")) + ) + suite = Suite(tabs=[Tab(contexts=[Context(testcases=[the_input])], name="hallo")]) + bundle = create_bundle(conf, sys.stdout, suite) + actual, _ = get_readable_input(bundle, the_input) + + assert ( + actual.description == "$ submission hello << 'STDINN'\nOne line\nSTDIN\nSTDINN" + ) From eeb408daf61b4630ff9b763f331724af894d2778 Mon Sep 17 00:00:00 2001 From: Niko Strijbol Date: Wed, 20 Dec 2023 14:33:05 +0100 Subject: [PATCH 2/2] Ensure stdin ends with a newline This is needed for the here-doc to display correctly. --- tested/dsl/translate_parser.py | 9 ++++++++- tests/test_dsl_yaml.py | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/tested/dsl/translate_parser.py b/tested/dsl/translate_parser.py index 16324483..5bd72bf1 100644 --- a/tested/dsl/translate_parser.py +++ b/tested/dsl/translate_parser.py @@ -90,6 +90,13 @@ class ReturnOracle: ) +def _ensure_trailing_newline(text: str) -> str: + if text[-1] != "\n": + return text + "\n" + else: + return text + + def _parse_yaml_value(loader: yaml.Loader, node: yaml.Node) -> Any: if isinstance(node, yaml.MappingNode): result = loader.construct_mapping(node) @@ -442,7 +449,7 @@ def _convert_testcase(testcase: YamlDict, context: DslContext) -> Testcase: else: if "stdin" in testcase: assert isinstance(testcase["stdin"], str) - stdin = TextData(data=testcase["stdin"]) + stdin = TextData(data=_ensure_trailing_newline(testcase["stdin"])) else: stdin = EmptyChannel.NONE arguments = testcase.get("arguments", []) diff --git a/tests/test_dsl_yaml.py b/tests/test_dsl_yaml.py index 12155eec..6bdc9d31 100644 --- a/tests/test_dsl_yaml.py +++ b/tests/test_dsl_yaml.py @@ -62,7 +62,7 @@ def test_parse_one_tab_ctx(): assert len(context.testcases) == 1 tc = context.testcases[0] assert tc.is_main_testcase() - assert tc.input.stdin.data == "Input string" + assert tc.input.stdin.data == "Input string\n" assert tc.input.arguments == ["--arg", "argument"] assert tc.output.stderr.data == "Error string" assert tc.output.stdout.data == "Output string"