diff --git a/.devcontainer/dodona-tested.dockerfile b/.devcontainer/dodona-tested.dockerfile index 230e5d58..d7172452 100644 --- a/.devcontainer/dodona-tested.dockerfile +++ b/.devcontainer/dodona-tested.dockerfile @@ -65,6 +65,15 @@ RUN < - - - Exe - net8.0 - enable - enable - - - - - - - + + + + Exe + net8.0 + enable + enable + + + + + + + diff --git a/tested/languages/language.py b/tested/languages/language.py index f78eb4ba..a6432f82 100644 --- a/tested/languages/language.py +++ b/tested/languages/language.py @@ -138,7 +138,6 @@ def compilation(self, files: list[str]) -> CallbackResult: :param files: A suggestion containing the dependencies TESTed thinks might be useful to compile. By convention, the last file in the list is the file containing the "main" function. - :return: The compilation command and either the resulting files or a filter for the resulting files. """ diff --git a/tested/languages/typescript/config.py b/tested/languages/typescript/config.py new file mode 100644 index 00000000..18ebca24 --- /dev/null +++ b/tested/languages/typescript/config.py @@ -0,0 +1,258 @@ +import json +import logging +import os +import re +from pathlib import Path +from typing import TYPE_CHECKING + +from tested.datatypes import ( + AdvancedObjectTypes, + AllTypes, + BasicStringTypes, + ExpressionTypes, +) +from tested.dodona import AnnotateCode, Message, Status +from tested.features import Construct, TypeSupport +from tested.languages.conventionalize import ( + EXECUTION_PREFIX, + Conventionable, + NamingConventions, + submission_file, + submission_name, +) +from tested.languages.language import ( + CallbackResult, + Command, + Language, + TypeDeclarationMetadata, +) +from tested.languages.utils import cleanup_description +from tested.serialisation import Statement, Value + +if TYPE_CHECKING: + from tested.languages.generation import PreparedExecutionUnit + +logger = logging.getLogger(__name__) + + +class TypeScript(Language): + + def initial_dependencies(self) -> list[str]: + return ["values.ts"] + + def needs_selector(self) -> bool: + return False + + def file_extension(self) -> str: + return "ts" + + def naming_conventions(self) -> dict[Conventionable, NamingConventions]: + return { + "namespace": "camel_case", + "function": "camel_case", + "identifier": "camel_case", + "global_identifier": "macro_case", + "property": "camel_case", + "class": "pascal_case", + } + + def supported_constructs(self) -> set[Construct]: + return { + Construct.OBJECTS, + Construct.EXCEPTIONS, + Construct.FUNCTION_CALLS, + Construct.ASSIGNMENTS, + Construct.HETEROGENEOUS_COLLECTIONS, + Construct.HETEROGENEOUS_ARGUMENTS, + Construct.EVALUATION, + Construct.DEFAULT_PARAMETERS, + Construct.GLOBAL_VARIABLES, + Construct.NAMED_ARGUMENTS, + } + + def datatype_support(self) -> dict[AllTypes, TypeSupport]: + return { # type: ignore + "integer": "supported", + "real": "supported", + "char": "reduced", + "text": "supported", + "string": "supported", + "boolean": "supported", + "sequence": "supported", + "set": "supported", + "map": "supported", + "dictionary": "supported", + "object": "supported", + "nothing": "supported", + "undefined": "supported", + "null": "supported", + "int8": "reduced", + "uint8": "reduced", + "int16": "reduced", + "uint16": "reduced", + "int32": "reduced", + "uint32": "reduced", + "int64": "reduced", + "uint64": "reduced", + "bigint": "supported", + "single_precision": "reduced", + "double_precision": "reduced", + "double_extended": "reduced", + "array": "reduced", + "list": "reduced", + "tuple": "reduced", + } + + def collection_restrictions(self) -> dict[AllTypes, set[ExpressionTypes]]: + return {AdvancedObjectTypes.OBJECT: {BasicStringTypes.TEXT}} + + def compilation(self, files: list[str]) -> CallbackResult: + submission = submission_file(self) + main_file = self.find_main_file( + list(map(lambda name: Path(name), files)), submission + ) + + if main_file != Status.COMPILATION_ERROR: + path_to_modules = os.environ["NODE_PATH"] + return ( + [ + "tsc", + "--target", + "esnext", + "--module", + "nodenext", + "--allowJs", + "--allowImportingTsExtensions", + "--noEmit", + "--esModuleInterop", + "--typeRoots", + f"{path_to_modules}/@types", + str(main_file.name), + ], + files, + ) + else: + return [], files + + def execution(self, cwd: Path, file: str, arguments: list[str]) -> Command: + return ["tsx", file, *arguments] + + def modify_solution(self, solution: Path): + # import local to prevent errors + from tested.judge.utils import run_command + + assert self.config + + parse_file = str(Path(__file__).parent / "parseAst.ts") + output = run_command( + solution.parent, + timeout=None, + command=["tsx", parse_file, str(solution.absolute())], + check=True, + ) + assert output, "Missing output from TypesScript's modify_solution" + namings = output.stdout.strip() + with open(solution, "a") as file: + print(f"\nexport {{{namings}}};", file=file) + + # Add strict mode to the script. + with open(solution, "r") as file: + non_strict = file.read() + + # TODO: This may be deleted in the future. + with open(solution, "w") as file: + file.write('"use strict";\n' + non_strict) + self.config.dodona.source_offset -= 2 + + def linter(self, remaining: float) -> tuple[list[Message], list[AnnotateCode]]: + # Import locally to prevent errors. + from tested.languages.typescript import linter + + assert self.config + return linter.run_eslint(self.config.dodona, remaining) + + def cleanup_stacktrace(self, stacktrace: str) -> str: + assert self.config + # What this does: + # 1a. While inside the submission code, replace all references to the location with + # 1b. Remove any "submission.SOMETHING" -> "SOMETHING" + # 2. Once we encounter a line with the execution location, skip all lines. + execution_submission_location_regex = f"{self.config.dodona.workdir}/{EXECUTION_PREFIX}[_0-9]+/{submission_file(self)}" + submission_location = ( + self.config.dodona.workdir / "common" / submission_file(self) + ) + compilation_submission_location = str(submission_location.resolve()) + execution_location_regex = f"{self.config.dodona.workdir}/{EXECUTION_PREFIX}[_0-9]+/{EXECUTION_PREFIX}[_0-9]+.ts" + submission_namespace = f"{submission_name(self)}." + + resulting_lines = "" + for line in stacktrace.splitlines(keepends=True): + # If we encounter an execution location, we are done. + if re.search(execution_location_regex, line): + break + + # Replace any reference to the submission. + line = re.sub(execution_submission_location_regex, "", line) + line = line.replace(compilation_submission_location, "") + # Remove any references of the form "submission.SOMETHING" + line = line.replace(submission_namespace, "") + + resulting_lines += line + + return resulting_lines + + def cleanup_description(self, statement: str) -> str: + statement = cleanup_description(self, statement) + await_regex = re.compile(r"await\s+") + return await_regex.sub("", statement) + + def generate_statement(self, statement: Statement) -> str: + from tested.languages.typescript import generators + + return generators.convert_statement(statement, full=True) + + def generate_execution_unit(self, execution_unit: "PreparedExecutionUnit") -> str: + from tested.languages.typescript import generators + + return generators.convert_execution_unit(execution_unit) + + def generate_encoder(self, values: list[Value]) -> str: + from tested.languages.typescript 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", + "string": "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", + "tuple": "array", + "any": "object", + }, + "nested": ("<", ">"), + "exception": "Error", + } diff --git a/tested/languages/typescript/eslintrc.yml b/tested/languages/typescript/eslintrc.yml new file mode 100644 index 00000000..ccb38a66 --- /dev/null +++ b/tested/languages/typescript/eslintrc.yml @@ -0,0 +1,16 @@ +parser: "@typescript-eslint/parser" +parserOptions: + ecmaVersion: "latest" + sourceType: "module" + ecmaFeatures: {} +plugins: + - "@typescript-eslint" +extends: "plugin:@typescript-eslint/recommended" +env: + node: yes + es2020: yes +rules: + no-var: "warn" + semi: "warn" + '@typescript-eslint/no-unused-expressions': "off" + diff --git a/tested/languages/typescript/generators.py b/tested/languages/typescript/generators.py new file mode 100644 index 00000000..20a462b9 --- /dev/null +++ b/tested/languages/typescript/generators.py @@ -0,0 +1,309 @@ +import json + +from tested.datatypes import ( + AdvancedNothingTypes, + AdvancedNumericTypes, + BasicBooleanTypes, + BasicNothingTypes, + BasicNumericTypes, + BasicObjectTypes, + BasicSequenceTypes, + BasicStringTypes, +) +from tested.datatypes.advanced import AdvancedObjectTypes +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, + NamedArgument, + ObjectType, + PropertyAssignment, + SequenceType, + SpecialNumbers, + Statement, + StringType, + Value, + VariableAssignment, + as_basic_type, +) +from tested.testsuite import MainInput + + +def convert_arguments(arguments: list[NamedArgument | Expression]) -> str: + results = [] + for arg in arguments: + if isinstance(arg, NamedArgument): + results.append(f"{arg.name}={convert_statement(arg.value, True)}") + else: + results.append(convert_statement(arg, True)) + return ", ".join(results) + + +def convert_value(value: Value) -> str: + # Handle some advanced types. + if value.type == AdvancedNothingTypes.UNDEFINED: + return "undefined" + elif value.type == AdvancedNumericTypes.DOUBLE_EXTENDED: + raise AssertionError("Double extended values are not supported in ts.") + elif value.type == AdvancedNumericTypes.FIXED_PRECISION: + raise AssertionError("Fixed precision values are not supported in ts.") + elif value.type == AdvancedObjectTypes.OBJECT: + assert isinstance(value, ObjectType) + result = "{" + for i, pair in enumerate(value.data): + result += convert_statement(pair.key, True) + result += ": " + result += convert_statement(pair.value, True) + if i != len(value.data) - 1: + result += ", " + result += "}" + return result + elif value.type in ( + AdvancedNumericTypes.INT_64, + AdvancedNumericTypes.U_INT_64, + AdvancedNumericTypes.BIG_INT, + ): + return f'BigInt("{value.data}")' + # Handle basic types + value = as_basic_type(value) + if value.type in (BasicNumericTypes.INTEGER, BasicNumericTypes.REAL): + if not isinstance(value.data, SpecialNumbers): + return str(value.data) + elif value.data == SpecialNumbers.NOT_A_NUMBER: + return "NaN" + elif value.data == SpecialNumbers.POS_INFINITY: + return "Infinity" + else: + return "(-Infinity)" + elif value.type == BasicStringTypes.TEXT: + return json.dumps(value.data, ensure_ascii=False) + elif value.type == BasicBooleanTypes.BOOLEAN: + return str(value.data).lower() + elif value.type == BasicNothingTypes.NOTHING: + return "null" + elif value.type == BasicSequenceTypes.SEQUENCE: + assert isinstance(value, SequenceType) + return f"[{convert_arguments(value.data)}]" # pyright: ignore + elif value.type == BasicSequenceTypes.SET: + assert isinstance(value, SequenceType) + return f"new Set([{convert_arguments(value.data)}])" # pyright: ignore + elif value.type == BasicObjectTypes.MAP: + assert isinstance(value, ObjectType) + result = "new Map([" + for i, pair in enumerate(value.data): + result += "[" + result += convert_statement(pair.key, True) + result += ", " + result += convert_statement(pair.value, True) + result += "]" + if i != len(value.data) - 1: + result += ", " + result += "])" + return result + elif value.type == BasicStringTypes.UNKNOWN: + assert isinstance(value, StringType) + return convert_unknown_type(value) + raise AssertionError(f"Invalid literal: {value!r}") + + +def convert_function_call(call: FunctionCall, internal=False) -> str: + result = "" + if not internal: + result += "await " + if call.type == FunctionType.CONSTRUCTOR: + result += "new " + if call.namespace: + result += convert_statement(call.namespace, True) + "." + result += call.name + if call.type != FunctionType.PROPERTY: + result += f"({convert_arguments(call.arguments)})" # pyright: ignore + return result + + +def convert_statement(statement: Statement, internal=False, full=False) -> str: + if isinstance(statement, Identifier): + return statement + elif isinstance(statement, FunctionCall): + return convert_function_call(statement, internal) + elif isinstance(statement, Value): + return convert_value(statement) + elif isinstance(statement, PropertyAssignment): + return ( + f"{convert_statement(statement.property, True)} = " + f"{convert_statement(statement.expression, True)}" + ) + elif isinstance(statement, VariableAssignment): + if full: + prefix = "let " + else: + prefix = "" + return ( + f"{prefix}{statement.variable} = " + f"{convert_statement(statement.expression, True)}" + ) + raise AssertionError(f"Unknown statement: {statement!r}") + + +def _generate_internal_context(ctx: PreparedContext, pu: PreparedExecutionUnit) -> str: + result = ctx.before + "\n" + + # Import the submission if there is no main call. + if not ctx.context.has_main_testcase(): + result += f""" + writeSeparator(); + delete require.cache[require.resolve("./{submission_file(pu.language)}")]; + let {pu.submission_name} = await import('./{submission_file(pu.language)}'); + """ + + # Generate code for each testcase + tc: PreparedTestcase + for i, tc in enumerate(ctx.testcases): + # Prepare command arguments if needed. + if tc.testcase.is_main_testcase(): + assert isinstance(tc.input, MainInput) + wrapped = [json.dumps(a) for a in tc.input.arguments] + result += f""" + writeSeparator(); + let new_args = [process.argv[0]]; + new_args = new_args.concat([{", ".join(wrapped)}]); + process.argv = new_args; + """ + elif i != 0: + result += "writeSeparator();\n" + + # We need special code to make variables available outside of the try-catch block. + if ( + not tc.testcase.is_main_testcase() + and isinstance(tc.input, PreparedTestcaseStatement) + and isinstance(tc.input.statement, VariableAssignment) + ): + result += f"let {tc.input.statement.variable}\n" + + result += "try {\n" + if tc.testcase.is_main_testcase(): + assert isinstance(tc.input, MainInput) + result += f""" + delete require.cache[require.resolve("./{pu.submission_name}.ts")]; + let {pu.submission_name} = await import('./{pu.submission_name}.ts'); + """ + else: + assert isinstance(tc.input, PreparedTestcaseStatement) + result += " " * 4 + convert_statement(tc.input.input_statement()) + ";\n" + + result += f""" + {convert_statement(tc.exception_statement())}; + }} catch(e) {{ + {convert_statement(tc.exception_statement("e"))}; + }} + """ + + result += ctx.after + + return result + + +def convert_execution_unit(pu: PreparedExecutionUnit) -> str: + result = f""" + import * as fs from 'fs'; + import * as values from './values.ts'; + """ + + # Import the language specific functions we will need. + for name in pu.evaluator_names: + result += f"import * as {name} from './{name}.ts';\n" + + # We now open files for results and define some functions. + result += f""" + const valueFile = fs.openSync("{pu.value_file}", "w"); + const exceptionFile = fs.openSync("{pu.exception_file}", "w"); + + function writeSeparator() {{ + fs.writeSync(valueFile, "--{pu.testcase_separator_secret}-- SEP"); + fs.writeSync(exceptionFile, "--{pu.testcase_separator_secret}-- SEP"); + fs.writeSync(process.stdout.fd, "--{pu.testcase_separator_secret}-- SEP"); + fs.writeSync(process.stderr.fd, "--{pu.testcase_separator_secret}-- SEP"); + }} + + function writeContextSeparator() {{ + fs.writeSync(valueFile, "--{pu.context_separator_secret}-- SEP"); + fs.writeSync(exceptionFile, "--{pu.context_separator_secret}-- SEP"); + fs.writeSync(process.stdout.fd, "--{pu.context_separator_secret}-- SEP"); + fs.writeSync(process.stderr.fd, "--{pu.context_separator_secret}-- SEP"); + }} + + async function sendValue(value: unknown) {{ + values.sendValue(valueFile, await value); + }} + + async function sendException(exception: unknown) {{ + values.sendException(exceptionFile, await exception); + }} + + async function sendSpecificValue(value: unknown) {{ + values.sendEvaluated(valueFile, await value); + }} + + async function sendSpecificException(exception: unknown) {{ + values.sendEvaluated(exceptionFile, await exception); + }} + """ + + # Generate code for each context. + ctx: PreparedContext + for i, ctx in enumerate(pu.contexts): + result += f""" + async function context{i}() {{ + {_generate_internal_context(ctx, pu)} + }} + """ + + # Functions to write separators + result += f"(async () => {{\n" + + for i, ctx in enumerate(pu.contexts): + result += f""" + writeContextSeparator(); + await context{i}(); + """ + + result += f""" + fs.closeSync(valueFile); + fs.closeSync(exceptionFile); + }})(); + """ + + return result + + +def convert_check_function(evaluator: str, function: FunctionCall) -> str: + return f""" + (async () => {{ + import * as {evaluator} from './{evaluator}.ts'; + import * as values from './values.ts'; + + const result = {convert_function_call(function)}; + values.sendEvaluated(process.stdout.fd, result); + }})(); + """ + + +def convert_encoder(values: list[Value]) -> str: + result = f""" + import * as values from './values.ts'; + import * as fs from 'fs'; + """ + + for value in values: + result += f"values.sendValue(process.stdout.fd, {convert_value(value)});\n" + result += f'fs.writeSync(process.stdout.fd, "␞");\n' + + return result diff --git a/tested/languages/typescript/linter.py b/tested/languages/typescript/linter.py new file mode 100644 index 00000000..f15ffec7 --- /dev/null +++ b/tested/languages/typescript/linter.py @@ -0,0 +1,101 @@ +import json +import logging +from pathlib import Path + +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__) +severity = [Severity.INFO, Severity.WARNING, Severity.ERROR] + + +def run_eslint( + config: DodonaConfig, remaining: float +) -> tuple[list[Message], list[AnnotateCode]]: + """ + Calls eslint to annotate submitted source code and adds resulting score and + annotations to tab. + """ + submission = config.source + language_options = config.config_for() + if path := language_options.get("eslint_config", None): + assert isinstance(path, str) + config_path = config.resources / path + else: + # Use the default file. + config_path = config.judge / "tested/languages/typescript/eslintrc.yml" + config_path = str(config_path.absolute()) + + execution_results = run_command( + directory=submission.parent, + timeout=remaining, + command=[ + "eslint", + "-f", + "json", + "--no-inline-config", + "-c", + config_path, + str(submission.absolute()), + ], + ) + + if execution_results is None: + return [], [] + + if execution_results.timeout or execution_results.memory: + return [ + ( + get_i18n_string("languages.typescript.linter.timeout") + if execution_results.timeout + else get_i18n_string("languages.typescript.linter.memory") + ) + ], [] + + try: + eslint_objects = json.loads(execution_results.stdout) + except Exception as e: + logger.warning("ESLint produced bad output", exc_info=e) + return [ + get_i18n_string("languages.typescript.linter.output"), + ExtendedMessage( + description=str(e), format="code", permission=Permission.STAFF + ), + ], [] + annotations = [] + + for eslint_object in eslint_objects: + if Path(eslint_object.get("filePath", submission)).name != submission.name: + continue + for message in eslint_object.get("messages", []): + text = message.get("message", None) + if not text: + continue + rule_id = message.get("ruleId") + external = None + if rule_id: + external = f"https://eslint.org/docs/rules/{rule_id}" + + start_row = message.get("line", 1) + end_row = message.get("endLine") + rows = end_row - start_row if end_row and end_row > start_row else None + start_col = message.get("column", 1) + end_col = message.get("endColumn") + cols = end_col - start_col if end_col and end_col > start_col else None + annotations.append( + AnnotateCode( + row=start_row - 1 + config.source_offset, + rows=rows, + text=text, + externalUrl=external, + column=start_col - 1, + columns=cols, + type=severity[int(message.get("severity", 1))], + ) + ) + + # 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/languages/typescript/parseAst.ts b/tested/languages/typescript/parseAst.ts new file mode 100644 index 00000000..fa043ac2 --- /dev/null +++ b/tested/languages/typescript/parseAst.ts @@ -0,0 +1,61 @@ +import * as ts from 'typescript'; +import * as fs from 'fs'; +const source = fs.readFileSync(process.argv[2], 'utf-8'); + +const ast = ts.createSourceFile( + process.argv[2], // File name + source, // Source code + ts.ScriptTarget.ESNext, // Target language version + true // SetParentNodes option to preserve parent-child relationships +); + +// Helper function to extract relevant identifiers from AST nodes +function mapSubTreeToIds(node: ts.Node): Array { + // Make sure that no definitions inside a block of some kind are accounted with. + if (ts.isForOfStatement(node) || ts.isForInStatement(node) || ts.isIfStatement(node) || + ts.isForStatement(node) || ts.isWhileStatement(node) || ts.isBlock(node)) { + return [] + } + + if (ts.isVariableDeclaration(node)) { + return [node.name]; + } else if (ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node)) { + return [node.name]; + } else if (ts.isExpressionStatement(node) && + ts.isBinaryExpression(node.expression) && + node.expression.operatorToken.kind === ts.SyntaxKind.EqualsToken) { + return [node.expression.left]; + } else { + const ids: Array = []; + ts.forEachChild(node, (child: ts.Node) => { + ids.push(...mapSubTreeToIds(child)); + }); + return ids; + } +} + +// Convert node to identifier names, handling patterns (Array/Object destructuring) +function mapIdToName(node: ts.Node|undefined): Array { + if (!node) { + return []; + } + + if (ts.isIdentifier(node)) { + return [node.text]; + } else if (ts.isArrayBindingPattern(node)) { + return node.elements.flatMap(element => ts.isBindingElement(element) ? mapIdToName(element.name) : []); + } else if (ts.isObjectBindingPattern(node)) { + return node.elements.flatMap(prop => mapIdToName(prop.name)); + } else { + return []; + } +} + +try { + const array = Array.from(new Set(mapSubTreeToIds(ast).flatMap(mapIdToName))); + console.log(array.join(', ')); +} catch (e) { + // Assume this is invalid TypeScript at this point. + console.error(e); + process.exit(0); +} diff --git a/tested/languages/typescript/templates/values.ts b/tested/languages/typescript/templates/values.ts new file mode 100644 index 00000000..34c34bb9 --- /dev/null +++ b/tested/languages/typescript/templates/values.ts @@ -0,0 +1,135 @@ +import * as fs from 'fs'; + +function isVanillaObject(value: Object) : boolean { + try { + return Reflect.getPrototypeOf(value) === null; + } catch { + return false; + } +} + +function encode(value: Object): { data: Object; diagnostic: any; type: string } { + let diagnostic = null; + let type = null; + + if ( typeof value === "undefined") { + type = "undefined"; + } else if (typeof value === "boolean") { + type = "boolean"; + } else if (typeof value === "number") { + if (Number.isInteger(value)) { + type = "integer"; + } else { + type = "real"; + if (Number.isNaN(value)) { + value = "nan"; + } else if (!Number.isFinite(value)) { + if (value < 0) { + value = "-inf"; + } else { + value = "inf"; + } + } + } + } else if (typeof value === "string") { + type = "text"; + } else if (typeof value === "bigint") { + type = "bigint"; + value = value.toString(); + } else if (typeof value === "symbol") { + type = "unknown"; + value = value.toString(); + } else if (typeof value === "object") { + if (value === null) { + type = "null"; + } else if (Array.isArray(value)) { + type = "list"; + // Handle holes in arrays... + const unholed = []; + for (let i = 0; i < value.length; i++) { + if (!(i in value)) { + unholed.push(``) + } else { + unholed.push(value[i]); + } + } + value = unholed.map(encode); + } else if (value instanceof Set) { + type = "set"; + value = Array.from(value).map(encode); + } else if (value instanceof Map) { + type = "dictionary"; + value = Array + .from(value) + .map(([key, value]) => { + return { + key: encode(key), + value: encode(value) + }; + } + ); + } else if (value?.constructor === Object || isVanillaObject(value)) { + // Plain objects + type = "object"; + // Process the elements of the object. + + value = Object.keys(value).map(key => { + return { + key: encode(key), + value: encode((value as Record)[key]) + }; + }); + } else { + type = "unknown"; + diagnostic = value?.constructor?.name; + value = JSON.stringify(value); + } + } else { + type = "unknown"; + diagnostic = (value as Object)?.constructor?.name; + value = Object.prototype.toString.call(value); + } + + return { + type: type, + data: value, + diagnostic: diagnostic + }; + +} + +// Send a value to the given stream. +function sendValue(stream: number, value: Object) { + fs.writeSync(stream, JSON.stringify(encode(value))); +} + +// Send an exception to the given stream. +function sendException(stream: number, exception: Error | Object | {constructor: {name: any}}): void { + if (!exception) { + return; + } + if (exception instanceof Error) { + // We have a proper error... + fs.writeSync(stream, JSON.stringify({ + "message": exception.message, + "stacktrace": exception.stack ?? "", + "type": exception.constructor.name + })); + } else { + // Comes out of the values.js: + // We have something else, so we cannot rely on stuff being present. + fs.writeSync(stream, JSON.stringify({ + "message": JSON.stringify(exception), + "stacktrace": "", + "type": (exception as Object).constructor.name ?? (Object.prototype.toString.call(exception)), + "additional_message_keys": ["languages.typescript.runtime.invalid.exception"] + })); + } +} + +// Send an evaluation result to the given stream. +function sendEvaluated(stream: number, result: Object) { + fs.writeSync(stream, JSON.stringify(result)); +} + +export { sendValue, sendException, sendEvaluated }; diff --git a/tested/testsuite.py b/tested/testsuite.py index 9d858f9f..a617fcb9 100644 --- a/tested/testsuite.py +++ b/tested/testsuite.py @@ -72,6 +72,7 @@ class SupportedLanguage(StrEnum): HASKELL = auto() JAVA = auto() JAVASCRIPT = auto() + TYPESCRIPT = auto() KOTLIN = auto() PYTHON = auto() RUNHASKELL = auto() diff --git a/tests/exercises/counter/solution/solution-eslint.ts b/tests/exercises/counter/solution/solution-eslint.ts new file mode 100644 index 00000000..d240ad10 --- /dev/null +++ b/tests/exercises/counter/solution/solution-eslint.ts @@ -0,0 +1,15 @@ +class Counter { + private count: number + + constructor() { + this.count = 0 + } + + add() { + this.count++; + } + + get() { + this.count--; + } +} diff --git a/tests/exercises/counter/solution/solution.ts b/tests/exercises/counter/solution/solution.ts new file mode 100644 index 00000000..4594d2fb --- /dev/null +++ b/tests/exercises/counter/solution/solution.ts @@ -0,0 +1,16 @@ +class Counter { + private count: number; + + constructor() { + this.count = 0; + } + + add() { + this.count++; + return this; + } + + get() { + return this.count; + } +} diff --git a/tests/exercises/echo-function-additional-source-files/solution/correct.ts b/tests/exercises/echo-function-additional-source-files/solution/correct.ts new file mode 100644 index 00000000..ee38b7dd --- /dev/null +++ b/tests/exercises/echo-function-additional-source-files/solution/correct.ts @@ -0,0 +1,5 @@ +import * as e from "./echo.ts"; + +function echo(content: Object): Object { + return e.echo(content); +} diff --git a/tests/exercises/echo-function-additional-source-files/workdir/echo.ts b/tests/exercises/echo-function-additional-source-files/workdir/echo.ts new file mode 100644 index 00000000..177b2c36 --- /dev/null +++ b/tests/exercises/echo-function-additional-source-files/workdir/echo.ts @@ -0,0 +1,5 @@ +function echo(content: Object): Object { + return content; +} + +export { echo }; diff --git a/tests/exercises/echo-function-file-input/solution/correct-async.ts b/tests/exercises/echo-function-file-input/solution/correct-async.ts new file mode 100644 index 00000000..8c91fad4 --- /dev/null +++ b/tests/exercises/echo-function-file-input/solution/correct-async.ts @@ -0,0 +1,13 @@ +import * as fs from "fs"; + +function echoFile(content: string) { + return new Promise((resolve: (value: unknown) => void, reject: (reason?: any) => void) => { + fs.readFile(content, {encoding:'utf8', flag:'r'}, (err: any, data: unknown) => { + if (err) { + reject(err); + } else { + resolve(data); + } + }); + }).then((c: string) => c.trim()); +} diff --git a/tests/exercises/echo-function-file-input/solution/correct.ts b/tests/exercises/echo-function-file-input/solution/correct.ts new file mode 100644 index 00000000..b9added1 --- /dev/null +++ b/tests/exercises/echo-function-file-input/solution/correct.ts @@ -0,0 +1,5 @@ +import * as fs from 'fs'; + +function echoFile(content) { + return fs.readFileSync(content, {encoding:'utf8', flag:'r'}).trim(); +} diff --git a/tests/exercises/echo-function-file-output/solution/correct.ts b/tests/exercises/echo-function-file-output/solution/correct.ts new file mode 100644 index 00000000..e7bacf3d --- /dev/null +++ b/tests/exercises/echo-function-file-output/solution/correct.ts @@ -0,0 +1,5 @@ +import * as fs from 'fs'; + +function echoFunction(filename, stringToWrite) { + fs.writeFileSync(filename, stringToWrite + '\n', { flag: 'w' }); +} diff --git a/tests/exercises/echo-function/evaluation/evaluator.ts b/tests/exercises/echo-function/evaluation/evaluator.ts new file mode 100644 index 00000000..4cb3a9bf --- /dev/null +++ b/tests/exercises/echo-function/evaluation/evaluator.ts @@ -0,0 +1,22 @@ +function evaluate(actual: string) { + const correct = actual === "correct"; + return { + "result": correct, + "readable_expected": "correct", + "readable_actual": actual.toString(), + "messages": [{"description": "Hallo", "format": "text"}] + } +} + +function evaluateSum(actual: { toString: () => any; }, sum: number) { + const correct = sum == 10; + return { + "result": correct, + "readable_expected": "correct", + "readable_actual": actual.toString(), + "messages": [{"description": "Hallo", "format": "text"}] + } +} + +exports.evaluate = evaluate; +exports.evaluateSum = evaluateSum; diff --git a/tests/exercises/echo-function/evaluation/one-language-literals.yaml b/tests/exercises/echo-function/evaluation/one-language-literals.yaml index 20718509..667b8f38 100644 --- a/tests/exercises/echo-function/evaluation/one-language-literals.yaml +++ b/tests/exercises/echo-function/evaluation/one-language-literals.yaml @@ -6,6 +6,7 @@ runhaskell: "Submission.toString (1+1)" java: "Submission.toString(1+1)" javascript: "submission.toString(1+1)" + typescript: "submission.toString(1+1)" kotlin: "toString(1+1)" python: "submission.to_string(1+1)" csharp: "Submission.toString(1+1)" diff --git a/tests/exercises/echo-function/evaluation/one-specific-argument.tson b/tests/exercises/echo-function/evaluation/one-specific-argument.tson index 12bbb4c1..1f0dc54f 100644 --- a/tests/exercises/echo-function/evaluation/one-specific-argument.tson +++ b/tests/exercises/echo-function/evaluation/one-specific-argument.tson @@ -51,6 +51,10 @@ "file" : "evaluator.js", "name" : "evaluate_sum" }, + "typescript" : { + "file" : "evaluator.ts", + "name" : "evaluate_sum" + }, "csharp" : { "file" : "Evaluator.cs", "name" : "evaluate_sum" @@ -78,6 +82,9 @@ "javascript" : [ "5 + 5" ], + "typescript" : [ + "5 + 5" + ], "csharp" : [ "5 + 5" ] diff --git a/tests/exercises/echo-function/evaluation/one-specific-argument.yaml b/tests/exercises/echo-function/evaluation/one-specific-argument.yaml index c098a1ab..a3b116f9 100644 --- a/tests/exercises/echo-function/evaluation/one-specific-argument.yaml +++ b/tests/exercises/echo-function/evaluation/one-specific-argument.yaml @@ -25,6 +25,9 @@ javascript: file: evaluator.js name: evaluate_sum + typescript: + file: evaluator.ts + name: evaluate_sum csharp: file: Evaluator.cs name: evaluate_sum @@ -43,6 +46,8 @@ - 5 + 5 javascript: - 5 + 5 + typescript: + - 5 + 5 csharp: - 5 + 5 diff --git a/tests/exercises/echo-function/evaluation/two-specific.tson b/tests/exercises/echo-function/evaluation/two-specific.tson index 3cdec8b9..5710b879 100644 --- a/tests/exercises/echo-function/evaluation/two-specific.tson +++ b/tests/exercises/echo-function/evaluation/two-specific.tson @@ -44,6 +44,9 @@ "javascript": { "file": "evaluator.js" }, + "typescript": { + "file": "evaluator.ts" + }, "csharp": { "file": "Evaluator.cs" } @@ -89,6 +92,9 @@ "javascript": { "file": "evaluator.js" }, + "typescript": { + "file": "evaluator.ts" + }, "csharp": { "file": "Evaluator.cs" } diff --git a/tests/exercises/echo-function/evaluation/typescript-object.yaml b/tests/exercises/echo-function/evaluation/typescript-object.yaml new file mode 100644 index 00000000..fb6eb476 --- /dev/null +++ b/tests/exercises/echo-function/evaluation/typescript-object.yaml @@ -0,0 +1,5 @@ +- tab: "My tab" + contexts: + - testcases: + - expression: 'echo("input-1")' + return: !object {} diff --git a/tests/exercises/echo-function/solution/correct-async.ts b/tests/exercises/echo-function/solution/correct-async.ts new file mode 100644 index 00000000..b95165c1 --- /dev/null +++ b/tests/exercises/echo-function/solution/correct-async.ts @@ -0,0 +1,3 @@ +async function echo(content: any) { + return content; +} diff --git a/tests/exercises/echo-function/solution/correct.ts b/tests/exercises/echo-function/solution/correct.ts new file mode 100644 index 00000000..c381c461 --- /dev/null +++ b/tests/exercises/echo-function/solution/correct.ts @@ -0,0 +1,11 @@ +function echo(content: Object) { + return content; +} + +function noEcho(content: Object) { + // Do nothing. +} + +function toString(number: Object): string { + return number.toString(); +} diff --git a/tests/exercises/echo-function/solution/top-level-output.ts b/tests/exercises/echo-function/solution/top-level-output.ts new file mode 100644 index 00000000..4325cd2c --- /dev/null +++ b/tests/exercises/echo-function/solution/top-level-output.ts @@ -0,0 +1,13 @@ +function echo(content: Object) { + return content; +} + +function noEcho(content: Object) { + // Do nothing. +} + +function toString(number: Object): string { + return number.toString(); +} + +console.log("This is top-level output"); diff --git a/tests/exercises/echo-function/solution/typescript-object.ts b/tests/exercises/echo-function/solution/typescript-object.ts new file mode 100644 index 00000000..2f83d9b6 --- /dev/null +++ b/tests/exercises/echo-function/solution/typescript-object.ts @@ -0,0 +1,3 @@ +function echo(_ignored: any) { + return Object.create(null); +} diff --git a/tests/exercises/echo-function/solution/unknown-return-type.ts b/tests/exercises/echo-function/solution/unknown-return-type.ts new file mode 100644 index 00000000..85b5d277 --- /dev/null +++ b/tests/exercises/echo-function/solution/unknown-return-type.ts @@ -0,0 +1,15 @@ +class Coord { + + public x: number; + public y: number; + + constructor(x: number, y: number ) { + this.x = x; + this.y = y; + } + +} + +function echo(content: unknown) { + return new Coord(5, 7); +} diff --git a/tests/exercises/echo/solution/comp-error.ts b/tests/exercises/echo/solution/comp-error.ts new file mode 100644 index 00000000..a34a07a4 --- /dev/null +++ b/tests/exercises/echo/solution/comp-error.ts @@ -0,0 +1 @@ +mfzej àryhg çyh aiogharuio ghqgh diff --git a/tests/exercises/echo/solution/correct.ts b/tests/exercises/echo/solution/correct.ts new file mode 100644 index 00000000..37e0cbfb --- /dev/null +++ b/tests/exercises/echo/solution/correct.ts @@ -0,0 +1,3 @@ +import * as fs from 'fs'; +const stdinBuffer = fs.readFileSync(0); // STDIN_FILENO = 0 +console.log(stdinBuffer.toString().trimEnd()); diff --git a/tests/exercises/echo/solution/run-error.ts b/tests/exercises/echo/solution/run-error.ts new file mode 100644 index 00000000..0bc2bdd4 --- /dev/null +++ b/tests/exercises/echo/solution/run-error.ts @@ -0,0 +1,6 @@ +function throw_error() { + const obj: any = null; + console.log(obj.does_not_exist()); +} + +throw_error() diff --git a/tests/exercises/echo/solution/wrong.ts b/tests/exercises/echo/solution/wrong.ts new file mode 100644 index 00000000..2e1bb218 --- /dev/null +++ b/tests/exercises/echo/solution/wrong.ts @@ -0,0 +1 @@ +console.log("WRONG"); diff --git a/tests/exercises/global/solution/correct.ts b/tests/exercises/global/solution/correct.ts new file mode 100644 index 00000000..ba25496b --- /dev/null +++ b/tests/exercises/global/solution/correct.ts @@ -0,0 +1 @@ +const GLOBAL_VAR = "GLOBAL"; diff --git a/tests/exercises/isbn-list/solution/solution.js b/tests/exercises/isbn-list/solution/solution.js new file mode 100644 index 00000000..fad85c78 --- /dev/null +++ b/tests/exercises/isbn-list/solution/solution.js @@ -0,0 +1,54 @@ +function isIsbn10(code) { + + function checkDigit(code) { + let check = 0; + for (let i = 0; i < 9; i++) { + check += parseInt(code[i]) * (i + 1); + } + check %= 11; + return check === 10 ? 'X' : check.toString(); + } + + if (code.length !== 10) { + return false; + } + + if (isNaN(Number(code.substring(0, 9)))) { + return false; + } + + return code[9] === checkDigit(code); +} + + +function isIsbn13(code) { + + function checkDigit(code) { + let check = 0; + for (let i = 0; i < 12; i++) { + check += parseInt(code[i]) * (i % 2 === 0 ? 1 : 3); + } + return ((((10 - check) % 10) + 10) % 10).toString(); + } + + if (code.length !== 13) { + return false; + } + + if (isNaN(Number(code.substring(0, 12)))) { + return false; + } + + return code[12] === checkDigit(code); +} + +function isIsbn(code, isbn13=true) { + return isbn13 ? isIsbn13(code) : isIsbn10(code); +} + +function areIsbn(codes, isbn13=undefined) { + if (isbn13 === undefined) { + return codes.map((code) => typeof code === 'string' ? isIsbn(code, code.length === 13) : false); + } + return codes.map((code) => typeof code === 'string' ? isIsbn(code, isbn13) : false); +} diff --git a/tests/exercises/isbn-list/solution/solution.ts b/tests/exercises/isbn-list/solution/solution.ts new file mode 100644 index 00000000..263bdc91 --- /dev/null +++ b/tests/exercises/isbn-list/solution/solution.ts @@ -0,0 +1,54 @@ +function isIsbn10(code: string): boolean { + + function checkDigit(code: string) { + let check: number = 0; + for (let i = 0; i < 9; i++) { + check += parseInt(code[i]) * (i + 1); + } + check %= 11; + return check === 10 ? 'X' : check.toString(); + } + + if (code.length !== 10) { + return false; + } + + if (isNaN(Number(code.substring(0, 9)))) { + return false; + } + + return code[9] === checkDigit(code); +} + + +function isIsbn13(code: string): boolean { + + function checkDigit(code: string) { + let check: number = 0; + for (let i = 0; i < 12; i++) { + check += parseInt(code[i]) * (i % 2 === 0 ? 1 : 3); + } + return ((((10 - check) % 10) + 10) % 10).toString(); + } + + if (code.length !== 13) { + return false; + } + + if (isNaN(Number(code.substring(0, 12)))) { + return false; + } + + return code[12] === checkDigit(code); +} + +function isIsbn(code: string, isbn13: boolean=true): boolean { + return isbn13 ? isIsbn13(code) : isIsbn10(code); +} + +function areIsbn(codes: Array, isbn13: boolean | undefined=undefined): Array { + if (isbn13 === undefined) { + return codes.map((code:unknown) => typeof code === 'string' ? isIsbn(code, code.length === 13) : false); + } + return codes.map((code:unknown) => typeof code === 'string' ? isIsbn(code, isbn13) : false); +} diff --git a/tests/exercises/isbn/evaluation/one-with-crashing-assignment-javascript.tson b/tests/exercises/isbn/evaluation/one-with-crashing-assignment-javascript.tson new file mode 100644 index 00000000..afc3b9b0 --- /dev/null +++ b/tests/exercises/isbn/evaluation/one-with-crashing-assignment-javascript.tson @@ -0,0 +1,88 @@ +{ + "tabs": [ + { + "name": "are_isbn", + "runs": [ + { + "contexts": [ + { + "before": { + "typescript": { + "data": "const ex = () => {throw new Error('AssertionError');};" + } + }, + "testcases": [ + { + "input": { + "type": "sequence", + "variable": "codes01", + "expression": { + "type": "function", + "namespace": "ex", + "name": "get", + "arguments": [] + } + } + }, + { + "input": { + "type": "function", + "name": "are_isbn", + "arguments": [ + "codes01" + ] + }, + "output": { + "result": { + "value": { + "data": [ + { + "data": false, + "type": "boolean" + }, + { + "data": true, + "type": "boolean" + }, + { + "data": true, + "type": "boolean" + }, + { + "data": true, + "type": "boolean" + }, + { + "data": false, + "type": "boolean" + }, + { + "data": false, + "type": "boolean" + }, + { + "data": false, + "type": "boolean" + }, + { + "data": true, + "type": "boolean" + }, + { + "data": false, + "type": "boolean" + } + ], + "type": "sequence" + } + } + } + } + ] + } + ] + } + ] + } + ] +} diff --git a/tests/exercises/isbn/evaluation/one-with-crashing-assignment-typescript.tson b/tests/exercises/isbn/evaluation/one-with-crashing-assignment-typescript.tson new file mode 100644 index 00000000..30d8566d --- /dev/null +++ b/tests/exercises/isbn/evaluation/one-with-crashing-assignment-typescript.tson @@ -0,0 +1,88 @@ +{ + "tabs": [ + { + "name": "are_isbn", + "runs": [ + { + "contexts": [ + { + "before": { + "typescript": { + "data": "const ex: () => Array = () => {throw new Error('AssertionError');};" + } + }, + "testcases": [ + { + "input": { + "type": "sequence", + "variable": "codes01", + "expression": { + "type": "function", + "namespace": "ex", + "name": "get", + "arguments": [] + } + } + }, + { + "input": { + "type": "function", + "name": "are_isbn", + "arguments": [ + "codes01" + ] + }, + "output": { + "result": { + "value": { + "data": [ + { + "data": false, + "type": "boolean" + }, + { + "data": true, + "type": "boolean" + }, + { + "data": true, + "type": "boolean" + }, + { + "data": true, + "type": "boolean" + }, + { + "data": false, + "type": "boolean" + }, + { + "data": false, + "type": "boolean" + }, + { + "data": false, + "type": "boolean" + }, + { + "data": true, + "type": "boolean" + }, + { + "data": false, + "type": "boolean" + } + ], + "type": "sequence" + } + } + } + } + ] + } + ] + } + ] + } + ] +} diff --git a/tests/exercises/isbn/solution/solution.js b/tests/exercises/isbn/solution/solution.js new file mode 100644 index 00000000..fad85c78 --- /dev/null +++ b/tests/exercises/isbn/solution/solution.js @@ -0,0 +1,54 @@ +function isIsbn10(code) { + + function checkDigit(code) { + let check = 0; + for (let i = 0; i < 9; i++) { + check += parseInt(code[i]) * (i + 1); + } + check %= 11; + return check === 10 ? 'X' : check.toString(); + } + + if (code.length !== 10) { + return false; + } + + if (isNaN(Number(code.substring(0, 9)))) { + return false; + } + + return code[9] === checkDigit(code); +} + + +function isIsbn13(code) { + + function checkDigit(code) { + let check = 0; + for (let i = 0; i < 12; i++) { + check += parseInt(code[i]) * (i % 2 === 0 ? 1 : 3); + } + return ((((10 - check) % 10) + 10) % 10).toString(); + } + + if (code.length !== 13) { + return false; + } + + if (isNaN(Number(code.substring(0, 12)))) { + return false; + } + + return code[12] === checkDigit(code); +} + +function isIsbn(code, isbn13=true) { + return isbn13 ? isIsbn13(code) : isIsbn10(code); +} + +function areIsbn(codes, isbn13=undefined) { + if (isbn13 === undefined) { + return codes.map((code) => typeof code === 'string' ? isIsbn(code, code.length === 13) : false); + } + return codes.map((code) => typeof code === 'string' ? isIsbn(code, isbn13) : false); +} diff --git a/tests/exercises/isbn/solution/solution.ts b/tests/exercises/isbn/solution/solution.ts new file mode 100644 index 00000000..263bdc91 --- /dev/null +++ b/tests/exercises/isbn/solution/solution.ts @@ -0,0 +1,54 @@ +function isIsbn10(code: string): boolean { + + function checkDigit(code: string) { + let check: number = 0; + for (let i = 0; i < 9; i++) { + check += parseInt(code[i]) * (i + 1); + } + check %= 11; + return check === 10 ? 'X' : check.toString(); + } + + if (code.length !== 10) { + return false; + } + + if (isNaN(Number(code.substring(0, 9)))) { + return false; + } + + return code[9] === checkDigit(code); +} + + +function isIsbn13(code: string): boolean { + + function checkDigit(code: string) { + let check: number = 0; + for (let i = 0; i < 12; i++) { + check += parseInt(code[i]) * (i % 2 === 0 ? 1 : 3); + } + return ((((10 - check) % 10) + 10) % 10).toString(); + } + + if (code.length !== 13) { + return false; + } + + if (isNaN(Number(code.substring(0, 12)))) { + return false; + } + + return code[12] === checkDigit(code); +} + +function isIsbn(code: string, isbn13: boolean=true): boolean { + return isbn13 ? isIsbn13(code) : isIsbn10(code); +} + +function areIsbn(codes: Array, isbn13: boolean | undefined=undefined): Array { + if (isbn13 === undefined) { + return codes.map((code:unknown) => typeof code === 'string' ? isIsbn(code, code.length === 13) : false); + } + return codes.map((code:unknown) => typeof code === 'string' ? isIsbn(code, isbn13) : false); +} diff --git a/tests/exercises/js-exceptions/evaluation/plan.yaml b/tests/exercises/js-ts-exceptions/evaluation/plan.yaml similarity index 100% rename from tests/exercises/js-exceptions/evaluation/plan.yaml rename to tests/exercises/js-ts-exceptions/evaluation/plan.yaml diff --git a/tests/exercises/js-exceptions/solution/correct-temp.js b/tests/exercises/js-ts-exceptions/solution/correct-temp.js similarity index 100% rename from tests/exercises/js-exceptions/solution/correct-temp.js rename to tests/exercises/js-ts-exceptions/solution/correct-temp.js diff --git a/tests/exercises/js-ts-exceptions/solution/correct-temp.ts b/tests/exercises/js-ts-exceptions/solution/correct-temp.ts new file mode 100644 index 00000000..55cfdfa1 --- /dev/null +++ b/tests/exercises/js-ts-exceptions/solution/correct-temp.ts @@ -0,0 +1,4 @@ +throw { + name: "AssertionError", + message: "Valid exceptions" +} diff --git a/tests/exercises/js-exceptions/solution/correct.js b/tests/exercises/js-ts-exceptions/solution/correct.js similarity index 100% rename from tests/exercises/js-exceptions/solution/correct.js rename to tests/exercises/js-ts-exceptions/solution/correct.js diff --git a/tests/exercises/js-ts-exceptions/solution/correct.ts b/tests/exercises/js-ts-exceptions/solution/correct.ts new file mode 100644 index 00000000..4ccbb484 --- /dev/null +++ b/tests/exercises/js-ts-exceptions/solution/correct.ts @@ -0,0 +1,2 @@ + +throw new Error("Valid exceptions"); diff --git a/tests/exercises/js-exceptions/solution/wrong-message.js b/tests/exercises/js-ts-exceptions/solution/wrong-message.js similarity index 100% rename from tests/exercises/js-exceptions/solution/wrong-message.js rename to tests/exercises/js-ts-exceptions/solution/wrong-message.js diff --git a/tests/exercises/js-ts-exceptions/solution/wrong-message.ts b/tests/exercises/js-ts-exceptions/solution/wrong-message.ts new file mode 100644 index 00000000..17810d49 --- /dev/null +++ b/tests/exercises/js-ts-exceptions/solution/wrong-message.ts @@ -0,0 +1,4 @@ +throw { + "name": "AssertionError", + "boodschap": "Valid exceptions" +}; diff --git a/tests/exercises/js-exceptions/solution/wrong-null.js b/tests/exercises/js-ts-exceptions/solution/wrong-null.js similarity index 100% rename from tests/exercises/js-exceptions/solution/wrong-null.js rename to tests/exercises/js-ts-exceptions/solution/wrong-null.js diff --git a/tests/exercises/js-ts-exceptions/solution/wrong-null.ts b/tests/exercises/js-ts-exceptions/solution/wrong-null.ts new file mode 100644 index 00000000..37d3d14b --- /dev/null +++ b/tests/exercises/js-ts-exceptions/solution/wrong-null.ts @@ -0,0 +1 @@ +throw null; diff --git a/tests/exercises/js-exceptions/solution/wrong.js b/tests/exercises/js-ts-exceptions/solution/wrong.js similarity index 100% rename from tests/exercises/js-exceptions/solution/wrong.js rename to tests/exercises/js-ts-exceptions/solution/wrong.js diff --git a/tests/exercises/js-ts-exceptions/solution/wrong.ts b/tests/exercises/js-ts-exceptions/solution/wrong.ts new file mode 100644 index 00000000..1acaa8b3 --- /dev/null +++ b/tests/exercises/js-ts-exceptions/solution/wrong.ts @@ -0,0 +1 @@ +throw "Valid exceptions"; diff --git a/tests/exercises/lotto/solution/correct.ts b/tests/exercises/lotto/solution/correct.ts new file mode 100644 index 00000000..a14f6082 --- /dev/null +++ b/tests/exercises/lotto/solution/correct.ts @@ -0,0 +1,7 @@ +function loterij(aantal=6, maximum = 42) { + const getallen = new Set(); + while (getallen.size < aantal) { + getallen.add(Math.floor(Math.random() * maximum) + 1); + } + return Array.from(getallen).sort((x: number, y: number) => x - y).join(" - "); +} diff --git a/tests/exercises/lotto/solution/wrong.ts b/tests/exercises/lotto/solution/wrong.ts new file mode 100644 index 00000000..4a7b33fd --- /dev/null +++ b/tests/exercises/lotto/solution/wrong.ts @@ -0,0 +1,7 @@ +function loterij(aantal=6, maximum = 42) { + const getallen = new Set(); + while (getallen.size < aantal) { + getallen.add(Math.floor(Math.random() * maximum) + 1); + } + return Array.from(getallen).sort((x: number, y: number) => y - x).join(" - "); +} diff --git a/tests/exercises/objects/solution/correct.ts b/tests/exercises/objects/solution/correct.ts new file mode 100644 index 00000000..ce09a081 --- /dev/null +++ b/tests/exercises/objects/solution/correct.ts @@ -0,0 +1,16 @@ +class EqualChecker { + + private number: number; + + constructor(number) { + this.number = number; + } + + check(other) { + return other === this.number; + } +} + +function setTest() { + return new Set([[1, 2], [2, 3]]); +} diff --git a/tests/exercises/sum/solution/correct.ts b/tests/exercises/sum/solution/correct.ts new file mode 100644 index 00000000..4781d7f3 --- /dev/null +++ b/tests/exercises/sum/solution/correct.ts @@ -0,0 +1,14 @@ +const getallen = process.argv.slice(1) +let som = 0 + +for (const getal of getallen) { + const r = parseInt(getal) + if (isNaN(r)) { + console.error("som: ongeldige argumenten") + process.exit(1); + } else { + som += r + } +} + +console.log(som); diff --git a/tests/language_markers.py b/tests/language_markers.py index 3a2acb76..ca96cd37 100644 --- a/tests/language_markers.py +++ b/tests/language_markers.py @@ -10,6 +10,7 @@ ] ALL_SPECIFIC_LANGUAGES = COMPILE_LANGUAGES + [ "javascript", + "typescript", "runhaskell", ] ALL_LANGUAGES = ALL_SPECIFIC_LANGUAGES + ["bash"] diff --git a/tests/testTypeScriptAstParserFile.ts b/tests/testTypeScriptAstParserFile.ts new file mode 100644 index 00000000..bae1efab --- /dev/null +++ b/tests/testTypeScriptAstParserFile.ts @@ -0,0 +1,43 @@ +// Test ObjectPattern +const {c, d} = {c: 5, d: 7}; +// Test Array Pattern +let [a, b] = ["alpha", "beta"]; +// Test normal variables +var x = 5, y = 6; +// Test first reassignment +x = y; + +// Test function +function demoFunction() { +} + +// Test simple class +class SimpleClass { + constructor() { + } +} + +// Test class with static variables +class StaticClass extends SimpleClass { + data = ["Static data"]; + constants; +} + +// Test try-catch +function tryCatch() { + try { + let demo = 5; + } catch { + // Do nothing + } +} + +// Test async function +async function asyncFunction() { + return 5; +} + +// Test second Reassignment +x = 5; +// Assignment to not defined var +z = x + y; diff --git a/tests/test_functionality.py b/tests/test_functionality.py index 12cff032..29b0beb5 100644 --- a/tests/test_functionality.py +++ b/tests/test_functionality.py @@ -105,7 +105,9 @@ def test_generic_exception_wrong_error( assert updates.find_status_enum() == ["wrong"] -@pytest.mark.parametrize("lang", ["python", "java", "kotlin", "csharp"]) +@pytest.mark.parametrize( + "lang", ["python", "java", "kotlin", "csharp", "typescript", "javascript"] +) def test_assignment_and_use_in_expression( lang: str, tmp_path: Path, pytestconfig: pytest.Config ): @@ -129,6 +131,8 @@ def test_assignment_and_use_in_expression( "java", "kotlin", "csharp", + "typescript", + "javascript", pytest.param("haskell", marks=pytest.mark.haskell), pytest.param("runhaskell", marks=pytest.mark.haskell), ], @@ -154,7 +158,9 @@ def test_assignment_and_use_in_expression_list( assert len(updates.find_all("start-test")) == 1 -@pytest.mark.parametrize("lang", ["python", "java", "kotlin", "csharp"]) +@pytest.mark.parametrize( + "lang", ["python", "java", "kotlin", "csharp", "typescript", "javascript"] +) def test_crashing_assignment_with_before( lang: str, tmp_path: Path, pytestconfig: pytest.Config ): @@ -236,6 +242,38 @@ def test_missing_key_types_detected_js_dictionary( assert updates.find_status_enum() == ["correct"] +def test_missing_key_types_detected_ts_object( + tmp_path: Path, pytestconfig: pytest.Config +): + conf = configuration( + pytestconfig, + "objects", + "typescript", + tmp_path, + "missing_key_types_js_object.yaml", + "correct", + ) + result = execute_config(conf) + updates = assert_valid_output(result, pytestconfig) + assert len(updates.find_all("start-testcase")) == 0 + assert updates.find_status_enum() == ["internal error"] + + +@pytest.mark.parametrize( + "suite", ["missing_key_types_js_dictionary", "missing_key_types"] +) +def test_missing_key_types_detected_ts_dictionary( + suite: str, tmp_path: Path, pytestconfig: pytest.Config +): + conf = configuration( + pytestconfig, "objects", "typescript", tmp_path, f"{suite}.yaml", "correct" + ) + result = execute_config(conf) + updates = assert_valid_output(result, pytestconfig) + assert len(updates.find_all("start-testcase")) == 1 + assert updates.find_status_enum() == ["correct"] + + @pytest.mark.parametrize("lang", ["java"]) def test_advanced_types_are_allowed( lang: str, tmp_path: Path, pytestconfig: pytest.Config @@ -323,7 +361,8 @@ def test_batch_compilation_no_fallback_runtime( @pytest.mark.parametrize( - "lang", ["python", "java", "c", "javascript", "kotlin", "bash", "csharp"] + "lang", + ["python", "java", "c", "javascript", "typescript", "kotlin", "bash", "csharp"], ) def test_program_params(lang: str, tmp_path: Path, pytestconfig: pytest.Config): conf = configuration(pytestconfig, "sum", lang, tmp_path, "short.tson", "correct") @@ -335,7 +374,7 @@ def test_program_params(lang: str, tmp_path: Path, pytestconfig: pytest.Config): @pytest.mark.parametrize( - "language", ["python", "java", "kotlin", "javascript", "csharp"] + "language", ["python", "java", "kotlin", "javascript", "typescript", "csharp"] ) def test_objects(language: str, tmp_path: Path, pytestconfig: pytest.Config): conf = configuration( @@ -348,7 +387,7 @@ def test_objects(language: str, tmp_path: Path, pytestconfig: pytest.Config): @pytest.mark.parametrize( - "language", ["python", "java", "kotlin", "javascript", "csharp"] + "language", ["python", "java", "kotlin", "javascript", "typescript", "csharp"] ) def test_objects_chained(language: str, tmp_path: Path, pytestconfig: pytest.Config): conf = configuration( @@ -361,7 +400,7 @@ def test_objects_chained(language: str, tmp_path: Path, pytestconfig: pytest.Con @pytest.mark.parametrize( - "language", ["python", "java", "kotlin", "javascript", "csharp"] + "language", ["python", "java", "kotlin", "javascript", "typescript", "csharp"] ) def test_property_assignment( language: str, tmp_path: Path, pytestconfig: pytest.Config @@ -381,7 +420,7 @@ def test_property_assignment( @pytest.mark.parametrize( - "language", ["python", "java", "kotlin", "javascript", "csharp"] + "language", ["python", "java", "kotlin", "javascript", "typescript", "csharp"] ) def test_counter(language: str, tmp_path: Path, pytestconfig: pytest.Config): conf = configuration( @@ -394,7 +433,7 @@ def test_counter(language: str, tmp_path: Path, pytestconfig: pytest.Config): @pytest.mark.parametrize( - "language", ["python", "java", "kotlin", "javascript", "csharp"] + "language", ["python", "java", "kotlin", "javascript", "typescript", "csharp"] ) def test_counter_chained(language: str, tmp_path: Path, pytestconfig: pytest.Config): conf = configuration( @@ -407,7 +446,7 @@ def test_counter_chained(language: str, tmp_path: Path, pytestconfig: pytest.Con @pytest.mark.parametrize( - "language", ["python", "java", "kotlin", "javascript", "csharp"] + "language", ["python", "java", "kotlin", "javascript", "typescript", "csharp"] ) def test_objects_yaml(language: str, tmp_path: Path, pytestconfig: pytest.Config): conf = configuration( @@ -444,6 +483,7 @@ def test_objects_error(language: str, tmp_path: Path, pytestconfig: pytest.Confi ("java", ["internal error"]), ("c", ["internal error"]), ("javascript", ["correct"]), + ("typescript", ["correct"]), ("haskell", ["internal error"]), ("runhaskell", ["internal error"]), ], @@ -489,6 +529,7 @@ def test_timeouts_propagate_to_contexts(): ("csharp", '(Coords) {"X":5.5,"Y":7.5}'), ("java", "Coord[x=5, y=7]"), ("javascript", '(Coord) {"x":5,"y":7}'), + ("typescript", '(Coord) {"x":5,"y":7}'), ("kotlin", "Coord(x=5, y=6)"), ("python", "() Coord(x=5, y=6)"), ], @@ -621,7 +662,7 @@ def test_language_literals_work( # Check that the test suite is valid with a correct submission. # This test suite is used for the test below "test_output_in_script_is_caught". -@pytest.mark.parametrize("language", ["python", "javascript", "bash"]) +@pytest.mark.parametrize("language", ["python", "javascript", "typescript", "bash"]) def test_two_suite_is_valid(language: str, tmp_path: Path, pytestconfig: pytest.Config): conf = configuration( pytestconfig, @@ -636,7 +677,7 @@ def test_two_suite_is_valid(language: str, tmp_path: Path, pytestconfig: pytest. assert updates.find_status_enum() == ["correct"] * 2 -@pytest.mark.parametrize("language", ["python", "javascript", "bash"]) +@pytest.mark.parametrize("language", ["python", "javascript", "typescript", "bash"]) def test_output_in_script_is_caught( language: str, tmp_path: Path, pytestconfig: pytest.Config ): diff --git a/tests/test_language_quircks.py b/tests/test_language_quircks.py index 9af65f88..9b5c1392 100644 --- a/tests/test_language_quircks.py +++ b/tests/test_language_quircks.py @@ -37,6 +37,20 @@ def test_javascript_vanilla_object(tmp_path: Path, pytestconfig: pytest.Config): assert updates.find_status_enum() == ["correct"] +def test_typescript_vanilla_object(tmp_path: Path, pytestconfig: pytest.Config): + conf = configuration( + pytestconfig, + "echo-function", + "typescript", + tmp_path, + "typescript-object.yaml", + "typescript-object", + ) + result = execute_config(conf) + updates = assert_valid_output(result, pytestconfig) + assert updates.find_status_enum() == ["correct"] + + def test_python_input_prompt_is_ignored(tmp_path: Path, pytestconfig: pytest.Config): conf = configuration( pytestconfig, @@ -76,11 +90,14 @@ def test_haskell_function_arguments_without_brackets( ) -def test_javascript_exception_correct(tmp_path: Path, pytestconfig: pytest.Config): +@pytest.mark.parametrize("lang", ["javascript", "typescript"]) +def test_js_ts_exception_correct( + lang: str, tmp_path: Path, pytestconfig: pytest.Config +): conf = configuration( pytestconfig, - "js-exceptions", - "javascript", + "js-ts-exceptions", + lang, tmp_path, "plan.yaml", "correct", @@ -94,7 +111,7 @@ def test_javascript_exception_correct(tmp_path: Path, pytestconfig: pytest.Confi def test_javascript_exception_correct_temp(tmp_path: Path, pytestconfig: pytest.Config): conf = configuration( pytestconfig, - "js-exceptions", + "js-ts-exceptions", "javascript", tmp_path, "plan.yaml", @@ -106,11 +123,12 @@ def test_javascript_exception_correct_temp(tmp_path: Path, pytestconfig: pytest. assert len(updates.find_all("append-message")) == 0 -def test_javascript_exception_wrong(tmp_path: Path, pytestconfig: pytest.Config): +@pytest.mark.parametrize("lang", ["javascript", "typescript"]) +def test_js_ts_exception_wrong(lang: str, tmp_path: Path, pytestconfig: pytest.Config): conf = configuration( pytestconfig, - "js-exceptions", - "javascript", + "js-ts-exceptions", + lang, tmp_path, "plan.yaml", "wrong", @@ -121,11 +139,14 @@ def test_javascript_exception_wrong(tmp_path: Path, pytestconfig: pytest.Config) assert len(updates.find_all("append-message")) == 1 -def test_javascript_exception_wrong_null(tmp_path: Path, pytestconfig: pytest.Config): +@pytest.mark.parametrize("lang", ["javascript", "typescript"]) +def test_js_ts_exception_wrong_null( + lang: str, tmp_path: Path, pytestconfig: pytest.Config +): conf = configuration( pytestconfig, - "js-exceptions", - "javascript", + "js-ts-exceptions", + lang, tmp_path, "plan.yaml", "wrong-null", @@ -136,13 +157,14 @@ def test_javascript_exception_wrong_null(tmp_path: Path, pytestconfig: pytest.Co assert len(updates.find_all("append-message")) == 0 -def test_javascript_exception_missing_message( - tmp_path: Path, pytestconfig: pytest.Config +@pytest.mark.parametrize("lang", ["javascript", "typescript"]) +def test_js_ts_exception_missing_message( + lang: str, tmp_path: Path, pytestconfig: pytest.Config ): conf = configuration( pytestconfig, - "js-exceptions", - "javascript", + "js-ts-exceptions", + lang, tmp_path, "plan.yaml", "wrong-message", @@ -163,3 +185,16 @@ def test_javascript_async(exercise: str, tmp_path: Path, pytestconfig: pytest.Co result = execute_config(conf) updates = assert_valid_output(result, pytestconfig) assert updates.find_status_enum() == ["correct"] + + +@pytest.mark.parametrize("exercise", ["echo-function-file-input", "echo-function"]) +def test_typescript_async(exercise: str, tmp_path: Path, pytestconfig: pytest.Config): + conf = configuration( + pytestconfig, exercise, "typescript", tmp_path, "one.tson", "correct-async" + ) + workdir = Path(conf.resources).parent / "workdir" + if workdir.exists(): + shutil.copytree(workdir, tmp_path, dirs_exist_ok=True) + result = execute_config(conf) + updates = assert_valid_output(result, pytestconfig) + assert updates.find_status_enum() == ["correct"] diff --git a/tests/test_linters.py b/tests/test_linters.py index 00db95ec..ff3037df 100644 --- a/tests/test_linters.py +++ b/tests/test_linters.py @@ -60,6 +60,23 @@ def test_eslint(tmp_path: Path, config: dict, pytestconfig: pytest.Config): assert len(updates.find_all("annotate-code")) > 0 +@pytest.mark.parametrize("config", _get_config_options("typescript")) +def test_eslint_typescript(tmp_path: Path, config: dict, pytestconfig: pytest.Config): + conf = configuration( + pytestconfig, + "counter", + "typescript", + tmp_path, + "plan.yaml", + "solution-eslint", + config, + ) + result = execute_config(conf) + updates = assert_valid_output(result, pytestconfig) + print(updates) + assert len(updates.find_all("annotate-code")) > 0 + + @pytest.mark.parametrize( ("language", "config"), [ diff --git a/tests/test_oracles_programmed.py b/tests/test_oracles_programmed.py index 166a721c..f3f491dd 100644 --- a/tests/test_oracles_programmed.py +++ b/tests/test_oracles_programmed.py @@ -105,7 +105,9 @@ def test_missing_custom_check_function(tmp_path: Path, pytestconfig: pytest.Conf assert len(updates.find_all("append-message")) == 4 -@pytest.mark.parametrize("lang", ["python", "java", "kotlin", "javascript", "csharp"]) +@pytest.mark.parametrize( + "lang", ["python", "java", "kotlin", "javascript", "typescript", "csharp"] +) def test_custom_check_function_lotto_correct( lang: str, tmp_path: Path, pytestconfig: pytest.Config ): @@ -118,7 +120,9 @@ def test_custom_check_function_lotto_correct( assert updates.find_status_enum() == ["correct"] -@pytest.mark.parametrize("lang", ["python", "java", "kotlin", "javascript", "csharp"]) +@pytest.mark.parametrize( + "lang", ["python", "java", "kotlin", "javascript", "typescript", "csharp"] +) def test_custom_check_function_lotto_wrong( lang: str, tmp_path: Path, pytestconfig: pytest.Config ): diff --git a/tests/test_problem_statements.py b/tests/test_problem_statements.py index f4a095b3..fb161aa1 100644 --- a/tests/test_problem_statements.py +++ b/tests/test_problem_statements.py @@ -30,6 +30,7 @@ def test_small_descriptions(language: str): ("c", "this_is_a_function_name"), ("kotlin", "thisIsAFunctionName"), ("javascript", "thisIsAFunctionName"), + ("typescript", "thisIsAFunctionName"), ("haskell", "thisIsAFunctionName"), ("runhaskell", "thisIsAFunctionName"), ], @@ -71,6 +72,11 @@ def test_template_function_name(lang: str, expected: str): ("javascript", "'text'", "string"), ("javascript", '"sequence", "integer"', "array"), ("javascript", '"array", ("set", ("integer", ))', "array>"), + ("typescript", "'integer'", "number"), + ("typescript", "'real'", "number"), + ("typescript", "'text'", "string"), + ("typescript", '"sequence", "integer"', "array"), + ("typescript", '"array", ("set", ("integer", ))', "array>"), ("haskell", "'integer'", "Int"), ("haskell", "'real'", "Double"), ("haskell", "'text'", "String"), @@ -99,6 +105,8 @@ def test_template_type_name(lang: str, tested_type: Any, expected: str): ("kotlin", "'map'", "map"), ("javascript", "'sequence'", "sequence"), ("javascript", "'map'", "map"), + ("typescript", "'sequence'", "sequence"), + ("typescript", "'map'", "map"), ("haskell", "'sequence'", "sequence"), ("haskell", "'list'", "list"), ], @@ -120,6 +128,8 @@ def test_template_natural_type_name(lang: str, tested_type: Any, expected: str): ("kotlin", "'map'", "afbeelding"), ("javascript", "'sequence'", "sequentie"), ("javascript", "'map'", "afbeelding"), + ("typescript", "'sequence'", "sequentie"), + ("typescript", "'map'", "afbeelding"), ("haskell", "'sequence'", "sequentie"), ("haskell", "'list'", "lijst"), ], @@ -149,6 +159,10 @@ def test_template_natural_type_name_nl(lang: str, tested_type: Any, expected: st "javascript", "let random = new Random()\nrandom.newSequence(10, 10)\n[10, 5, 2, 8, 7, 1, 3, 4, 9, 6]", ), + ( + "typescript", + "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): @@ -167,6 +181,7 @@ def test_template_statement_expression(lang: str, expected: str): "c", "kotlin", "javascript", + "typescript", "haskell", ], ) diff --git a/tests/test_serialisation.py b/tests/test_serialisation.py index bf86a12c..8feec03b 100644 --- a/tests/test_serialisation.py +++ b/tests/test_serialisation.py @@ -62,6 +62,7 @@ "java", "c", "javascript", + "typescript", "kotlin", pytest.param("runhaskell", marks=pytest.mark.haskell), "bash", diff --git a/tests/test_stacktrace_cleaners.py b/tests/test_stacktrace_cleaners.py index 8b284c76..ae75c416 100644 --- a/tests/test_stacktrace_cleaners.py +++ b/tests/test_stacktrace_cleaners.py @@ -56,6 +56,27 @@ def test_javascript_assertion_error(): assert actual_cleaned == expected_cleaned +def test_typescript_assertion_error(): + workdir = "/home/bliep/bloep/universal-judge/workdir" + language_config = get_language(workdir, "typescript") + original = f"""AssertionError [ERR_ASSERTION]: ongeldig bericht + at bigram2letter ({workdir}/execution00/submission.ts:86:13) + at {workdir}/execution00/submission.ts:98:32 + at Array.map () + at Codeersleutel.decodeer ({workdir}/execution00/submission.ts:98:18) + at context0 ({workdir}/execution00/execution00.ts:78:31) + at async {workdir}/execution00/execution00.ts:1515:13 +""" + expected_cleaned = f"""AssertionError [ERR_ASSERTION]: ongeldig bericht + at bigram2letter (:86:13) + at :98:32 + at Array.map () + at Codeersleutel.decodeer (:98:18) +""" + actual_cleaned = language_config.cleanup_stacktrace(original) + assert actual_cleaned == expected_cleaned + + def test_javascript_type_error(): workdir = "/home/bliep/bloep/universal-judge/workdir" language_config = get_language(workdir, "javascript") @@ -75,6 +96,25 @@ def test_javascript_type_error(): assert actual_cleaned == expected_cleaned +def test_typescript_type_error(): + workdir = "/home/bliep/bloep/universal-judge/workdir" + language_config = get_language(workdir, "typescript") + original = f"""TypeError: submission.Codeersleutel is not a constructor + at context0 ({workdir}/execution00/execution00.ts:46:17) + at {workdir}/execution00.ts:1515:19 + at Object. ({workdir}/execution00/execution00.ts:1573:7) + at Module._compile (node:internal/modules/cjs/loader:1254:14) + at Module._extensions..ts (node:internal/modules/cjs/loader:1308:10) + at Module.load (node:internal/modules/cjs/loader:1117:32) + at Module._load (node:internal/modules/cjs/loader:958:12) + at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12) + at node:internal/main/run_main_module:23:47 + """ + expected_cleaned = f"TypeError: Codeersleutel is not a constructor\n" + actual_cleaned = language_config.cleanup_stacktrace(original) + assert actual_cleaned == expected_cleaned + + @pytest.mark.parametrize("language", ALL_LANGUAGES) def test_empty_stacktrace(language): workdir = "/home/bliep/bloep/universal-judge/workdir" diff --git a/tests/test_utils.py b/tests/test_utils.py index a72ba04b..d97dff96 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -45,6 +45,38 @@ def test_javascript_ast_parse(): assert namings == expected +def test_typescript_ast_parse(): + expected = frozenset( + [ + "c", + "d", + "a", + "b", + "x", + "y", + "demoFunction", + "SimpleClass", + "StaticClass", + "tryCatch", + "z", + "asyncFunction", + ] + ) + from tested.judge.utils import run_command + + test_dir = Path(__file__).parent + parse_file = test_dir.parent / "tested" / "languages" / "typescript" / "parseAst.js" + demo_file = test_dir / "testTypeScriptAstParserFile.ts" + output = run_command( + demo_file.parent, + timeout=None, + command=["tsx", str(parse_file), str(demo_file.absolute())], + ) + assert output + namings = frozenset(output.stdout.strip().split(", ")) + assert namings == expected + + def test_run_doctests_tested_utils(): import doctest @@ -241,10 +273,14 @@ def test_valid_yaml_and_json(): def recursive_iter_dir(directory: Path) -> list[Path]: yaml_and_json_files = [] for file in directory.iterdir(): - if file.is_file() and ( - file.name.endswith(".yml") - or file.name.endswith(".yaml") - or file.name.endswith(".json") + if ( + file.is_file() + and not file.name.startswith("tsconfig") + and ( + file.name.endswith(".yml") + or file.name.endswith(".yaml") + or file.name.endswith(".json") + ) ): yaml_and_json_files.append(file) elif file.is_dir():