diff --git a/.gitignore b/.gitignore index fca52d7c..4a1cd8f0 100644 --- a/.gitignore +++ b/.gitignore @@ -73,6 +73,7 @@ ipython_config.py # pyenv .python-version +.data # workdir for judge /workdir/ @@ -82,3 +83,9 @@ venv-*/ .venv/ node_modules/ result/ + +# Nextflow +.nextflow* + +# .NET +obj/ diff --git a/dependencies.md b/dependencies.md index 317c37a2..0cb87004 100644 --- a/dependencies.md +++ b/dependencies.md @@ -84,13 +84,13 @@ $ cabal v1-install aeson | Name | Versions | Installation | |-------------------------|----------|--------------| -| `eslint` | 8.57 | npm package | +| `eslint` | 9.10 | npm package | | `abstract-syntax-tree` | 2.22 | npm package | Install npm packages as follows: ```shell -$ npm install eslint@8.57 abstract-syntax-tree@2.22 +$ npm install eslint@9.10 abstract-syntax-tree@2.22 ``` ## Kotlin diff --git a/flake.lock b/flake.lock index 37e3aa59..213b7d5a 100644 --- a/flake.lock +++ b/flake.lock @@ -2,17 +2,16 @@ "nodes": { "devshell": { "inputs": { - "flake-utils": "flake-utils", "nixpkgs": [ "nixpkgs" ] }, "locked": { - "lastModified": 1713532798, - "narHash": "sha256-wtBhsdMJA3Wa32Wtm1eeo84GejtI43pMrFrmwLXrsEc=", + "lastModified": 1728330715, + "narHash": "sha256-xRJ2nPOXb//u1jaBnDP56M7v5ldavjbtR6lfGqSvcKg=", "owner": "numtide", "repo": "devshell", - "rev": "12e914740a25ea1891ec619bb53cf5e6ca922e40", + "rev": "dd6b80932022cea34a019e2bb32f6fa9e494dfef", "type": "github" }, "original": { @@ -26,11 +25,11 @@ "systems": "systems" }, "locked": { - "lastModified": 1701680307, - "narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=", + "lastModified": 1726560853, + "narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=", "owner": "numtide", "repo": "flake-utils", - "rev": "4022d587cbbfd70fe950c1e2083a02621806a725", + "rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a", "type": "github" }, "original": { @@ -57,24 +56,6 @@ "type": "github" } }, - "flake-utils_3": { - "inputs": { - "systems": "systems_3" - }, - "locked": { - "lastModified": 1710146030, - "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, "nix-github-actions": { "inputs": { "nixpkgs": [ @@ -98,11 +79,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1716293225, - "narHash": "sha256-pU9ViBVE3XYb70xZx+jK6SEVphvt7xMTbm6yDIF4xPs=", + "lastModified": 1728888510, + "narHash": "sha256-nsNdSldaAyu6PE3YUA+YQLqUDJh+gRbBooMMekZJwvI=", "owner": "nixos", "repo": "nixpkgs", - "rev": "3eaeaeb6b1e08a016380c279f8846e0bd8808916", + "rev": "a3c0b3b21515f74fd2665903d4ce6bc4dc81c77c", "type": "github" }, "original": { @@ -114,25 +95,25 @@ }, "poetry2nix": { "inputs": { - "flake-utils": "flake-utils_3", + "flake-utils": "flake-utils_2", "nix-github-actions": "nix-github-actions", "nixpkgs": [ "nixpkgs" ], - "systems": "systems_4", + "systems": "systems_3", "treefmt-nix": "treefmt-nix" }, "locked": { - "lastModified": 1715251496, - "narHash": "sha256-vRBfJCKvJtu5sYev56XStirA3lAOPv0EkoEV2Nfc+tc=", + "lastModified": 1729073785, + "narHash": "sha256-KaDC7emuamQblDdka+gkBUUdEjQf3YGYozMb+zomgSM=", "owner": "nix-community", "repo": "poetry2nix", - "rev": "291a863e866972f356967d0a270b259f46bf987f", + "rev": "795fddefc9f910671c1cf0752c29802ce27322d6", "type": "github" }, "original": { "owner": "nix-community", - "ref": "refs/tags/2024.5.939250", + "ref": "refs/tags/2024.10.1637698", "repo": "poetry2nix", "type": "github" } @@ -140,7 +121,7 @@ "root": { "inputs": { "devshell": "devshell", - "flake-utils": "flake-utils_2", + "flake-utils": "flake-utils", "nixpkgs": "nixpkgs", "poetry2nix": "poetry2nix" } @@ -176,21 +157,6 @@ } }, "systems_3": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - }, - "systems_4": { "locked": { "lastModified": 1681028828, "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", diff --git a/flake.nix b/flake.nix index 5ad3fe51..723eb83b 100644 --- a/flake.nix +++ b/flake.nix @@ -9,7 +9,7 @@ inputs.nixpkgs.follows = "nixpkgs"; }; poetry2nix = { - url = "github:nix-community/poetry2nix?ref=refs/tags/2024.5.939250"; + url = "github:nix-community/poetry2nix?ref=refs/tags/2024.10.1637698"; inputs.nixpkgs.follows = "nixpkgs"; }; }; @@ -70,20 +70,45 @@ }; }; + nextflow-linter = let + codenarc = pkgs.fetchurl { + url = "https://repo1.maven.org/maven2/org/codenarc/CodeNarc/3.5.0/CodeNarc-3.5.0-all.jar"; + hash = "sha256-wTGPxP5sjMPPake3WM6Fcz551WIe8C+c6AtCtjBa3jI="; + }; + linter-rules = pkgs.fetchurl { + url = "https://github.com/awslabs/linter-rules-for-nextflow/releases/download/v0.1.0/linter-rules-0.1.jar"; + hash = "sha256-IHl2jAtZyloZLXybLNBELnTyld+ZA9q5Srta7LeUeHs="; + }; + slf4j-api = pkgs.fetchurl { + url = "https://repo1.maven.org/maven2/org/slf4j/slf4j-api/1.7.36/slf4j-api-1.7.36.jar"; + hash = "sha256-0+9XXj5JeWeNwBvx3M5RAhSTtNEft/G+itmCh3wWocA="; + }; + slf4j-simple = pkgs.fetchurl { + url = "https://repo1.maven.org/maven2/org/slf4j/slf4j-simple/1.7.36/slf4j-simple-1.7.36.jar"; + hash = "sha256-Lzm+2UPWJN+o9BAtBXEoOhCHC2qjbxl6ilBvFHAQwQ8="; + }; + in pkgs.writeScriptBin "nextflow-linter" '' + ${pkgs.jre_minimal}/bin/java \ + -Dorg.slf4j.simpleLogger.defaultLogLevel=error \ + -classpath "${linter-rules}/linter-rules-0.1.jar:${codenarc}/CodeNarc-3.5.0-all.jar:${slf4j-api}/slf4j-api-1.7.36.jar:${slf4j-simple}/slf4j-simple-1.7.36.jar" \ + "org.codenarc.CodeNarc" + ''; + # General dependencies for other languages haskell-deps = [ (pkgs.haskell.packages.ghc96.ghcWithPackages (p: [ p.aeson ])) pkgs.hlint ]; - node-deps = [ nodejs_base pkgs.nodePackages.eslint ast ]; + node-deps = [ nodejs_base pkgs.eslint ast ]; bash-deps = [ pkgs.shellcheck ]; c-deps = [ pkgs.cppcheck pkgs.gcc13 ]; java-deps = [ pkgs.openjdk21 pkgs.checkstyle ]; kotlin-deps = [ pkgs.kotlin pkgs.ktlint ]; csharp-deps = [ pkgs.dotnetCorePackages.sdk_8_0 ]; + nextflow-deps = [ pkgs.nextflow pkgs.groovy nextflow-linter ]; all-other-dependencies = haskell-deps ++ node-deps ++ bash-deps - ++ c-deps ++ java-deps ++ kotlin-deps ++ csharp-deps + ++ c-deps ++ java-deps ++ kotlin-deps ++ csharp-deps ++ nextflow-deps ++ [ pkgs.coreutils ]; python-base-env = { @@ -124,6 +149,8 @@ export DOTNET_CLI_HOME NODE_PATH=${ast}/lib/node_modules export NODE_PATH + ESLINT_USE_FLAT_CONFIG=false + export ESLINT_USE_FLAT_CONFIG poetry run pytest -n auto --cov=tested --cov-report=xml tests/ ''; installPhase = '' @@ -138,7 +165,7 @@ tested = pkgs.devshell.mkShell { name = "TESTed"; - packages = [ python-dev-env pkgs.nodePackages.pyright poetry-package ] + packages = [ python-dev-env pkgs.pyright poetry-package ] ++ all-other-dependencies; devshell.startup.link.text = '' @@ -155,6 +182,19 @@ name = "NODE_PATH"; prefix = "${ast}/lib/node_modules"; } + + { + name = "ESLINT_USE_FLAT_CONFIG"; + eval = "false"; + } + { + name = "NXF_DISABLE_CHECK_LATEST"; + eval = "true"; + } + { + name = "NXF_OFFLINE"; + eval = "true"; + } ]; commands = [ { @@ -172,7 +212,7 @@ ]; }; types = pkgs.mkShell { - packages = [ python-dev-env pkgs.nodePackages.pyright ]; + packages = [ python-dev-env pkgs.pyright ]; }; format = pkgs.mkShell { packages = [ python-dev-env poetry-package ]; }; }; diff --git a/tested/internationalization/en.yaml b/tested/internationalization/en.yaml index a6ff51dc..5b6a244e 100644 --- a/tested/internationalization/en.yaml +++ b/tested/internationalization/en.yaml @@ -107,6 +107,11 @@ en: timeout: "HLint exceeded time limit" memory: "HLint exceeded memory limit" output: "HLint produced bad output." + nextflow: + linter: + timeout: "CodeNarc exceeded time limit" + memory: "CodeNarc exceeded memory limit" + output: "CodeNarc produced bad output." kotlin: linter: timeout: "KTLint exceeded time limit" diff --git a/tested/languages/__init__.py b/tested/languages/__init__.py index a045fb26..ba3e0aae 100644 --- a/tested/languages/__init__.py +++ b/tested/languages/__init__.py @@ -17,6 +17,7 @@ from tested.languages.javascript.config import JavaScript from tested.languages.kotlin.config import Kotlin from tested.languages.language import Language +from tested.languages.nextflow.config import Nextflow from tested.languages.python.config import Python from tested.languages.runhaskell.config import RunHaskell @@ -31,6 +32,7 @@ "java": Java, "javascript": JavaScript, "kotlin": Kotlin, + "nextflow": Nextflow, "python": Python, "runhaskell": RunHaskell, "csharp": CSharp, diff --git a/tested/languages/haskell/linter.py b/tested/languages/haskell/linter.py index cb69342d..b19cbdf9 100644 --- a/tested/languages/haskell/linter.py +++ b/tested/languages/haskell/linter.py @@ -20,7 +20,7 @@ def run_hlint( config: DodonaConfig, remaining: float ) -> tuple[list[Message], list[AnnotateCode]]: """ - Calls eslint to annotate submitted source code and adds resulting score and + Calls hlint to annotate submitted source code and adds resulting score and annotations to tab. """ submission = config.source diff --git a/tested/languages/nextflow/config.py b/tested/languages/nextflow/config.py new file mode 100644 index 00000000..fa3a7ba4 --- /dev/null +++ b/tested/languages/nextflow/config.py @@ -0,0 +1,103 @@ +import re +from pathlib import Path +from typing import TYPE_CHECKING + +from tested.datatypes import ( + AdvancedStringTypes, + AllTypes, + BasicNumericTypes, + BasicStringTypes, +) +from tested.dodona import AnnotateCode, Message +from tested.features import Construct, TypeSupport +from tested.languages.conventionalize import Conventionable, NamingConventions +from tested.languages.language import ( + CallbackResult, + Command, + Language, + TypeDeclarationMetadata, +) +from tested.serialisation import Statement, Value + +if TYPE_CHECKING: + from tested.languages.generation import PreparedExecutionUnit + + +class Nextflow(Language): + def compilation(self, files: list[str]) -> CallbackResult: + return ["groovyc", *files], files + + def execution(self, cwd: Path, file: str, arguments: list[str]) -> Command: + named_arguments = [f"--p{i}={x}" for i, x in enumerate(arguments)] + return ["nextflow", "-quiet", "run", file, *named_arguments] + + def naming_conventions(self) -> dict[Conventionable, NamingConventions]: + return { + "function": "camel_case", + } + + def file_extension(self) -> str: + return "nf" + + def initial_dependencies(self) -> list[str]: + return [] + + def needs_selector(self): + return False + + def supported_constructs(self) -> set[Construct]: + return { + Construct.ASSIGNMENTS, + Construct.FUNCTION_CALLS, + Construct.HETEROGENEOUS_ARGUMENTS, + Construct.NAMED_ARGUMENTS, + } + + def datatype_support(self) -> dict[AllTypes, TypeSupport]: + return { + AdvancedStringTypes.CHAR: TypeSupport.REDUCED, + AdvancedStringTypes.STRING: TypeSupport.SUPPORTED, + BasicStringTypes.TEXT: TypeSupport.SUPPORTED, + BasicNumericTypes.INTEGER: TypeSupport.SUPPORTED, + } + + def modify_solution(self, solution: Path): + with open(solution, "r") as file: + contents = file.read() + # We use regex to find the workflow. + # First, check if we have an unnamed workflow. + # If so, replace it with a named workflow. + no_name = re.compile(r"workflow(\s*{)") + replacement = r"workflow solution_main\1" + contents, nr = re.subn(no_name, replacement, contents, count=1) + if nr == 0: + # There was no unnamed workflow. + # Now we try a named workflow. + with_name = re.compile(r"workflow(\s+)(\w+)(\s*{)") + replacement = r"workflow\1solution_main\3" + contents = re.sub(with_name, replacement, contents, count=1) + with open(solution, "w") as file: + file.write(contents) + + def linter(self, remaining: float) -> tuple[list[Message], list[AnnotateCode]]: + # Import locally to prevent errors. + from tested.languages.nextflow import linter + + assert self.config + return linter.run_codenarc(self.config.dodona, remaining) + + def generate_statement(self, statement: Statement) -> str: + from tested.languages.nextflow import generators + + return generators.convert_statement(statement) + + def generate_execution_unit(self, execution_unit: "PreparedExecutionUnit") -> str: + from tested.languages.nextflow import generators + + return generators.convert_execution_unit(execution_unit) + + def generate_encoder(self, values: list[Value]) -> str: + raise NotImplementedError + + def get_declaration_metadata(self) -> TypeDeclarationMetadata: + raise NotImplementedError diff --git a/tested/languages/nextflow/generators.py b/tested/languages/nextflow/generators.py new file mode 100644 index 00000000..6fa8ba98 --- /dev/null +++ b/tested/languages/nextflow/generators.py @@ -0,0 +1,140 @@ +import json +import shlex +from pathlib import Path +from typing import Literal, cast + +from tested.datatypes import AdvancedStringTypes, BasicNumericTypes, BasicStringTypes +from tested.languages.conventionalize import submission_file +from tested.languages.preparation import ( + PreparedContext, + PreparedExecutionUnit, + PreparedTestcase, + PreparedTestcaseStatement, +) +from tested.languages.utils import convert_unknown_type +from tested.serialisation import ( + Expression, + FunctionCall, + FunctionType, + Identifier, + Statement, + StringType, + Value, + VariableAssignment, +) +from tested.testsuite import MainInput + + +def convert_value(value: Value) -> str: + if value.type in (AdvancedStringTypes.CHAR, BasicStringTypes.TEXT): + return json.dumps(value.data) + elif value.type in BasicNumericTypes.INTEGER: + return str(value.data) + elif value.type == BasicStringTypes.UNKNOWN: + assert isinstance(value, StringType) + return convert_unknown_type(value) + raise AssertionError(f"Invalid literal: {value!r}") + + +def convert_arguments(arguments: list[Expression]) -> str: + return ", ".join(convert_statement(arg) for arg in arguments) + + +def convert_function_call(function: FunctionCall) -> str: + result = function.name + if function.type != FunctionType.PROPERTY: + result += f"({convert_arguments(cast(list[Expression], function.arguments))})" + return result + + +def convert_statement(statement: Statement) -> str: + if isinstance(statement, Identifier): + return f'"${statement}"' + elif isinstance(statement, FunctionCall): + return convert_function_call(statement) + elif isinstance(statement, Value): + return convert_value(statement) + elif isinstance(statement, VariableAssignment): + result = f"{statement.variable} = " + convert_statement(statement.expression) + raise AssertionError(f"Unknown statement: {statement!r}") + + +indent = " " * 4 + + +def convert_execution_unit(pu: PreparedExecutionUnit) -> str: + includes = [] + if any( + isinstance(tc.input, MainInput) for ctx in pu.contexts for tc in ctx.testcases + ): + includes.append("solution_main") + + for ctx in pu.contexts: + for tc in ctx.testcases: + if isinstance(tc.input, PreparedTestcaseStatement) and isinstance( + tc.input.statement, FunctionCall + ): + name = tc.input.statement.name + if name not in includes: + includes.append(name) + result = f"""include {{ {"; ".join(includes)} }} from './{pu.submission_name}' + +value_file = file("${{projectDir}}/{pu.value_file}") +exception_file = file("${{projectDir}}/{pu.exception_file}") + +def writeContextSeparator() {{ + value_file << "--{pu.context_separator_secret}-- SEP" + exception_file << "--{pu.context_separator_secret}-- SEP" + System.out << "--{pu.context_separator_secret}-- SEP" + System.err << "--{pu.context_separator_secret}-- SEP" +}} + +def writeSeparator() {{ + value_file << "--{pu.testcase_separator_secret}-- SEP" + exception_file << "--{pu.testcase_separator_secret}-- SEP" + System.out << "--{pu.testcase_separator_secret}-- SEP" + System.err << "--{pu.testcase_separator_secret}-- SEP" +}} + +process sendValue {{ + input: + val x + + exec: + value_file << groovy.json.JsonOutput.toJson([type: 'text', data: "${{x}}"]) +}} + +""" + # Generate code for each context. + ctx: PreparedContext + for i, ctx in enumerate(pu.contexts): + result += f"workflow context{i} {{\n" + result += indent + ctx.before + "\n" + + # Generate code for each testcase + tc: PreparedTestcase + for j, tc in enumerate(ctx.testcases): + result += indent + "writeSeparator()\n" + # Prepare command arguments if needed. + if tc.testcase.is_main_testcase(): + assert isinstance(tc.input, MainInput) + result += f"{indent}solution_main()\n" + else: + assert isinstance(tc.input, PreparedTestcaseStatement) + result += indent + convert_statement(tc.input.input_statement()) + "\n" + result += "\n" + + result += indent + ctx.after + "\n" + result += "}\n" + + result += f""" +workflow {{ +""" + + for i, ctx in enumerate(pu.contexts): + result += f"{indent}writeContextSeparator()\n" + result += f"{indent}context{i}()\n" + + result += "}\n" + + return result diff --git a/tested/languages/nextflow/linter.py b/tested/languages/nextflow/linter.py new file mode 100644 index 00000000..6babd1be --- /dev/null +++ b/tested/languages/nextflow/linter.py @@ -0,0 +1,90 @@ +import json +import logging + +from tested.configs import DodonaConfig +from tested.dodona import AnnotateCode, ExtendedMessage, Message, Permission, Severity +from tested.internationalization import get_i18n_string +from tested.judge.utils import run_command + +logger = logging.getLogger(__name__) + +message_categories = { + "p1": Severity.ERROR, + "p2": Severity.WARNING, + "p3": Severity.INFO, +} + + +def run_codenarc( + config: DodonaConfig, remaining: float +) -> tuple[list[Message], list[AnnotateCode]]: + """ + Calls CodeNarc with linter rules for Nextflow to annotate submitted source code + and adds resulting score and annotations to tab. + """ + submission = config.source + language_options = config.config_for() + + report_file = str((config.workdir / "report.json").absolute()) + + execution_results = run_command( + directory=submission.parent, + timeout=remaining, + command=[ + "nextflow-linter", + f"-report=json:{report_file}", + "-rulesetfiles=rulesets/general.xml", + "-includes=**/*.nf", + f"-includes={submission.name}", + ], + ) + + if execution_results is None: + return [], [] + + if execution_results.timeout or execution_results.memory: + return [ + ( + get_i18n_string("languages.nextflow.linter.timeout") + if execution_results.timeout + else get_i18n_string("languages.nextflow.linter.memory") + ) + ], [] + + with open(report_file) as json_file: + try: + result = json.load(json_file) + except Exception as e: + logger.warning("CodeNarc produced bad output", exc_info=e) + return [ + get_i18n_string("languages.nextflow.linter.output"), + ExtendedMessage( + description=str(e), format="code", permission=Permission.STAFF + ), + ], [] + annotations = [] + + for package in result.get("packages"): + for file in package.get("files"): + if file.get("name") != submission.name: + continue + for violation in file.get("violations"): + message = violation.get("message") + if not message: + continue + annotations.append( + AnnotateCode( + row=violation.get("line_number", 0) + config.source_offset, + text=message, + externalUrl=None, + column=0, + type=message_categories.get( + violation.get("priority"), + Severity.WARNING, + ), + ) + ) + + # sort linting messages on line, column and code + annotations.sort(key=lambda a: (a.row, a.column, a.text)) + return [], annotations diff --git a/tested/testsuite.py b/tested/testsuite.py index 9d858f9f..8a357005 100644 --- a/tested/testsuite.py +++ b/tested/testsuite.py @@ -73,6 +73,7 @@ class SupportedLanguage(StrEnum): JAVA = auto() JAVASCRIPT = auto() KOTLIN = auto() + NEXTFLOW = auto() PYTHON = auto() RUNHASKELL = auto() CSHARP = auto() diff --git a/tests/exercises/echo-function-additional-source-files/solution/correct.nf b/tests/exercises/echo-function-additional-source-files/solution/correct.nf new file mode 100644 index 00000000..f06fc1ec --- /dev/null +++ b/tests/exercises/echo-function-additional-source-files/solution/correct.nf @@ -0,0 +1,10 @@ +include { my_echo } from './echo' + +process echo { + input: + val x + output: + stdout + exec: + my_echo(x) +} diff --git a/tests/exercises/echo-function-additional-source-files/workdir/echo.nf b/tests/exercises/echo-function-additional-source-files/workdir/echo.nf new file mode 100644 index 00000000..0a5265e1 --- /dev/null +++ b/tests/exercises/echo-function-additional-source-files/workdir/echo.nf @@ -0,0 +1,10 @@ +process my_echo { + input: + val x + output: + stdout + + """ + echo ${x} + """ +} diff --git a/tests/exercises/echo-function-file-input/solution/correct-async.js b/tests/exercises/echo-function-file-input/solution/correct-async.js index d645d22e..b1951cf6 100644 --- a/tests/exercises/echo-function-file-input/solution/correct-async.js +++ b/tests/exercises/echo-function-file-input/solution/correct-async.js @@ -9,5 +9,5 @@ function echoFile(content) { resolve(data); } }); - }).then(c => c.trim()); + }); } diff --git a/tests/exercises/echo-function-file-input/solution/correct.c b/tests/exercises/echo-function-file-input/solution/correct.c index e39f08df..617f5012 100644 --- a/tests/exercises/echo-function-file-input/solution/correct.c +++ b/tests/exercises/echo-function-file-input/solution/correct.c @@ -1,15 +1,5 @@ #include -void trim(char *s) { - char * p = s; - int l = strlen(p); - - while(isspace(p[l - 1])) p[--l] = 0; - while(*p && isspace(* p)) ++p, --l; - - memmove(s, p, l + 1); -} - char* echo_file(const char* filename) { char *buffer = 0; long length; @@ -26,7 +16,6 @@ char* echo_file(const char* filename) { fclose (f); } if (buffer) { - trim(buffer); return buffer; } return ""; diff --git a/tests/exercises/echo-function-file-input/solution/correct.cs b/tests/exercises/echo-function-file-input/solution/correct.cs index 8cef7d3e..ecde0a35 100644 --- a/tests/exercises/echo-function-file-input/solution/correct.cs +++ b/tests/exercises/echo-function-file-input/solution/correct.cs @@ -5,6 +5,6 @@ class Submission { public static string EchoFile(string value) { - return System.IO.File.ReadAllText(value).Trim(); + return System.IO.File.ReadAllText(value); } } diff --git a/tests/exercises/echo-function-file-input/solution/correct.hs b/tests/exercises/echo-function-file-input/solution/correct.hs index f667af47..04ce6ec5 100644 --- a/tests/exercises/echo-function-file-input/solution/correct.hs +++ b/tests/exercises/echo-function-file-input/solution/correct.hs @@ -1,8 +1,4 @@ -strip = lstrip . rstrip -lstrip = dropWhile (`elem` " \t\n") -rstrip = reverse . lstrip . reverse - echoFile :: String -> IO String echoFile d = do s <- readFile d - return $ strip s + return $ s diff --git a/tests/exercises/echo-function-file-input/solution/correct.java b/tests/exercises/echo-function-file-input/solution/correct.java index 9a8df646..f7556fad 100644 --- a/tests/exercises/echo-function-file-input/solution/correct.java +++ b/tests/exercises/echo-function-file-input/solution/correct.java @@ -7,7 +7,7 @@ public class Submission { public static String echoFile(String value) throws IOException { try (BufferedReader r = new BufferedReader(new FileReader(value))) { - return r.lines().collect(Collectors.joining("\n")).trim(); + return r.lines().collect(Collectors.joining("\n")); } } } diff --git a/tests/exercises/echo-function-file-input/solution/correct.js b/tests/exercises/echo-function-file-input/solution/correct.js index 87e768a3..e417caa9 100644 --- a/tests/exercises/echo-function-file-input/solution/correct.js +++ b/tests/exercises/echo-function-file-input/solution/correct.js @@ -1,5 +1,5 @@ const fs = require('fs'); function echoFile(content) { - return fs.readFileSync(content, {encoding:'utf8', flag:'r'}).trim(); + return fs.readFileSync(content, {encoding:'utf8', flag:'r'}); } diff --git a/tests/exercises/echo-function-file-input/solution/correct.kt b/tests/exercises/echo-function-file-input/solution/correct.kt index 1404ee8f..73d975e2 100644 --- a/tests/exercises/echo-function-file-input/solution/correct.kt +++ b/tests/exercises/echo-function-file-input/solution/correct.kt @@ -1,5 +1,5 @@ import java.io.File fun echoFile(content : String) : String { - return File(content).readText().trim() + return File(content).readText() } diff --git a/tests/exercises/echo-function-file-input/solution/correct.nf b/tests/exercises/echo-function-file-input/solution/correct.nf new file mode 100644 index 00000000..a2e82652 --- /dev/null +++ b/tests/exercises/echo-function-file-input/solution/correct.nf @@ -0,0 +1,13 @@ +process echoFile { + input: + val x + output: + stdout + + script: + p = file(x) + + """ + cat "${p}" + """ +} diff --git a/tests/exercises/echo-function-file-input/solution/correct.py b/tests/exercises/echo-function-file-input/solution/correct.py index 8731b49b..0bf2a86f 100644 --- a/tests/exercises/echo-function-file-input/solution/correct.py +++ b/tests/exercises/echo-function-file-input/solution/correct.py @@ -1,2 +1,2 @@ def echo_file(content): - return open(content, "r").read().strip() + return open(content, "r").read() diff --git a/tests/exercises/echo-function-file-input/workdir/data.txt b/tests/exercises/echo-function-file-input/workdir/data.txt index 069d79bf..aa8542f9 100644 --- a/tests/exercises/echo-function-file-input/workdir/data.txt +++ b/tests/exercises/echo-function-file-input/workdir/data.txt @@ -1,2 +1,2 @@ Data file -Invalid line +Invalid line \ No newline at end of file diff --git a/tests/exercises/echo-function-file-output/solution/correct.nf b/tests/exercises/echo-function-file-output/solution/correct.nf new file mode 100644 index 00000000..a6f940f8 --- /dev/null +++ b/tests/exercises/echo-function-file-output/solution/correct.nf @@ -0,0 +1,9 @@ +process echoFunction() { + input: + val x1 + val x2 + + """ + echo "${x2}" > "${projectDir}/${x1}" + """ +} diff --git a/tests/exercises/echo-function/solution/correct.nf b/tests/exercises/echo-function/solution/correct.nf new file mode 100644 index 00000000..8842645b --- /dev/null +++ b/tests/exercises/echo-function/solution/correct.nf @@ -0,0 +1,11 @@ +process echo { + input: + val x + + output: + stdout + + """ + echo -n '${x}' + """ +} diff --git a/tests/exercises/echo/solution/comp-error.nf b/tests/exercises/echo/solution/comp-error.nf new file mode 100644 index 00000000..b18d535a --- /dev/null +++ b/tests/exercises/echo/solution/comp-error.nf @@ -0,0 +1 @@ +def isISBN10(code): diff --git a/tests/exercises/echo/solution/correct.nf b/tests/exercises/echo/solution/correct.nf new file mode 100644 index 00000000..8917a9b0 --- /dev/null +++ b/tests/exercises/echo/solution/correct.nf @@ -0,0 +1,3 @@ +workflow { + println System.in.newReader().readLine() +} diff --git a/tests/exercises/echo/solution/wrong.nf b/tests/exercises/echo/solution/wrong.nf new file mode 100644 index 00000000..6c10d3f5 --- /dev/null +++ b/tests/exercises/echo/solution/wrong.nf @@ -0,0 +1,3 @@ +workflow { + println WRONG +} diff --git a/tests/language_markers.py b/tests/language_markers.py index 3a2acb76..a8b73cc1 100644 --- a/tests/language_markers.py +++ b/tests/language_markers.py @@ -12,10 +12,14 @@ "javascript", "runhaskell", ] -ALL_LANGUAGES = ALL_SPECIFIC_LANGUAGES + ["bash"] +ALL_LANGUAGES = ALL_SPECIFIC_LANGUAGES + ["bash", "nextflow"] EXCEPTION_LANGUAGES = ["python", "java", "kotlin", "csharp", "haskell"] +def all_languages_except(*args): + return [language for language in ALL_LANGUAGES if language not in args] + + def test_no_missing_languages_from_tests(): assert sorted(ALL_LANGUAGES) == sorted(LANGUAGES.keys()) diff --git a/tests/test_functionality.py b/tests/test_functionality.py index 12cff032..bb3ba2a2 100644 --- a/tests/test_functionality.py +++ b/tests/test_functionality.py @@ -23,6 +23,7 @@ ALL_LANGUAGES, ALL_SPECIFIC_LANGUAGES, EXCEPTION_LANGUAGES, + all_languages_except, ) from tests.manual_utils import assert_valid_output, configuration, execute_config @@ -305,7 +306,7 @@ def test_batch_compilation_no_fallback( assert spy.call_count == 1 -@pytest.mark.parametrize("language", ALL_LANGUAGES) +@pytest.mark.parametrize("language", all_languages_except("nextflow")) def test_batch_compilation_no_fallback_runtime( language: str, tmp_path: Path, pytestconfig: pytest.Config ): @@ -323,7 +324,7 @@ def test_batch_compilation_no_fallback_runtime( @pytest.mark.parametrize( - "lang", ["python", "java", "c", "javascript", "kotlin", "bash", "csharp"] + "lang", all_languages_except("haskell", "runhaskell", "nextflow") ) def test_program_params(lang: str, tmp_path: Path, pytestconfig: pytest.Config): conf = configuration(pytestconfig, "sum", lang, tmp_path, "short.tson", "correct") diff --git a/tests/test_io_exercises.py b/tests/test_io_exercises.py index 99bd709e..b4d5ede2 100644 --- a/tests/test_io_exercises.py +++ b/tests/test_io_exercises.py @@ -9,7 +9,7 @@ from tested.languages.language import STRING_QUOTES from tested.testsuite import SupportedLanguage -from tests.language_markers import ALL_LANGUAGES +from tests.language_markers import ALL_LANGUAGES, all_languages_except from tests.manual_utils import assert_valid_output, configuration, execute_config @@ -80,7 +80,7 @@ def test_io_function_file_output_exercise( assert updates.find_status_enum() == ["correct"] -@pytest.mark.parametrize("language", ALL_LANGUAGES) +@pytest.mark.parametrize("language", all_languages_except("nextflow")) def test_io_function_additional_source_files( language: str, tmp_path: Path, pytestconfig: pytest.Config ): @@ -168,7 +168,7 @@ def test_io_function_display_no_multiline_exercise( assert actual[0] == quote and actual[-1] == quote -@pytest.mark.parametrize("language", ALL_LANGUAGES) +@pytest.mark.parametrize("language", all_languages_except("nextflow")) def test_io_function_nested_call_exercise( language: str, tmp_path: Path, pytestconfig: pytest.Config ):