diff --git a/Dockerfile b/Dockerfile index d8eb696..a6e50fa 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,6 +12,6 @@ RUN wget https://github.com/godotengine/godot/releases/download/4.4.1-stable/God mv Godot_v4.4.1-stable_linux.x86_64 /usr/bin/godot && \ rm Godot_v4.4.1-stable_linux.x86_64.zip -WORKDIR /opt/test-runner +WORKDIR /opt/exercism/gdscript/test-runner COPY . . -ENTRYPOINT ["/opt/test-runner/bin/run.sh"] +ENTRYPOINT ["/opt/exercism/gdscript/test-runner/bin/run.sh"] diff --git a/bin/run-tests-in-docker.sh b/bin/run-tests-in-docker.sh index 8078a67..1e1e09f 100755 --- a/bin/run-tests-in-docker.sh +++ b/bin/run-tests-in-docker.sh @@ -23,9 +23,9 @@ docker run \ --rm \ --network none \ --read-only \ - --mount type=bind,src="${PWD}/tests",dst=/opt/test-runner/tests \ + --mount type=bind,src="${PWD}/tests",dst=/opt/exercism/gdscript/test-runner/tests \ --mount type=tmpfs,dst=/tmp \ - --volume "${PWD}/bin/run-tests.sh:/opt/test-runner/bin/run-tests.sh" \ - --workdir /opt/test-runner \ - --entrypoint /opt/test-runner/bin/run-tests.sh \ + --volume "${PWD}/bin/run-tests.sh:/opt/exercism/gdscript/test-runner/bin/run-tests.sh" \ + --workdir /opt/exercism/gdscript/test-runner \ + --entrypoint /opt/exercism/gdscript/test-runner/bin/run-tests.sh \ exercism/test-runner diff --git a/bin/run.sh b/bin/run.sh index 38ffa15..e1336b9 100755 --- a/bin/run.sh +++ b/bin/run.sh @@ -38,7 +38,15 @@ mkdir -p "$XDG_CONFIG_HOME" "$XDG_DATA_HOME" "$XDG_CACHE_HOME" echo "${slug}: testing..." +# Switch to where the script lives in case called from elsewhere +OLD_DIR=$(pwd) +SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +cd "$SCRIPT_DIR" || exit 1 + # Run the tests for the provided implementation file -godot --headless -s bin/test_runner.gd 2>/tmp/stderr -- "${slug}" "${solution_dir}" "${output_dir}" +godot --headless -s ./test_runner.gd 2>/tmp/stderr -- "${slug}" "${solution_dir}" "${output_dir}" + +# Switch back to calling dir +cd "$OLD_DIR" || exit 1 echo "${slug}: done" diff --git a/bin/test-local-gdscript-solution.sh b/bin/test-local-gdscript-solution.sh index 51993c6..13b6eff 100755 --- a/bin/test-local-gdscript-solution.sh +++ b/bin/test-local-gdscript-solution.sh @@ -12,38 +12,25 @@ missing_gd_msg="Normally, the exercise subdirectory created by the exercism down general_help_msg="Please see https://exercism.org/docs/tracks/gdscript/installation for details." if [ ! -f "${slug//-/_}_test.gd" ]; then echo "Missing test file: ${slug//-/_}_test.gd" - echo $missing_gd_msg - echo $general_help_msg + echo "$missing_gd_msg" + echo "$general_help_msg" exit 1 fi if [ ! -f "${slug//-/_}.gd" ]; then echo "Missing solution file: ${slug//-/_}.gd" - echo $missing_gd_msg - echo $general_help_msg + echo "$missing_gd_msg" + echo "$general_help_msg" exit 1 fi -if [ ! -f "/opt/exercism/gdscript/test-runner/bin/run.sh" ]; then - echo "Missing test runner file: /opt/exercism/gdscript/test-runner/bin/run.sh" - echo $general_help_msg +if [ ! -f "/opt/exercism/gdscript/test-runner/bin/test_runner.gd" ]; then + echo "Missing test runner file: /opt/exercism/gdscript/test-runner/bin/test_runner.gd" + echo "$general_help_msg" exit 1 fi solution_dir="$(pwd)" -output_dir="${solution_dir}/.test-output" -results_file="${output_dir}/results.json" -mkdir -p "${output_dir}" -(cd /opt/exercism/gdscript/test-runner && bin/run.sh "$slug" "$solution_dir" "$output_dir") || { +(cd /opt/exercism/gdscript/test-runner && godot --headless -s bin/test_runner.gd -- "$slug" "$solution_dir") || { echo "Test runner script failed." exit 1 -} - -test_status="$(jq -r '.status' "${results_file}")" -if [ "$test_status" != "pass" ]; then - echo "Tests for $slug have failed:" - cat "${results_file}" - exit 1 -else - echo - echo "Tests for $slug passed!" -fi \ No newline at end of file +} \ No newline at end of file diff --git a/bin/test_runner.gd b/bin/test_runner.gd index 3b5f693..cac94cb 100644 --- a/bin/test_runner.gd +++ b/bin/test_runner.gd @@ -63,13 +63,17 @@ func parse_args() -> Error: global variables, based on the given args: * `solution_script_path` * `test_suite_script_path` - * `output_dir_path` + * `output_dir_path` (optional for local test runner, required for standard JSON test runner behaviour) """ var args = OS.get_cmdline_user_args() - if len(args) != 3: + # This script still conforms to [1] but allows 2 arguments for a local test + # runner with non-JSON output to eliminate dependence on 'jq' + # + # [1] https://github.com/exercism/docs/blob/main/building/tooling/test-runners/interface.md#execution + if len(args) not in [2,3]: push_error( - "The scrips needs exactly 3 arguments, was called with %s: %s" % [ + "The script needs exactly 2 or exactly 3 arguments, was called with %s: %s" % [ len(args), str(args) ] ) @@ -77,7 +81,8 @@ func parse_args() -> Error: var slug = args[0] var solution_dir_path = args[1] - output_dir_path = args[2] + if len(args) == 3: + output_dir_path = args[2] # Test folders use dashes, but test files use underscores var gdscript_path = solution_dir_path.path_join(slug.replace("-", "_")) @@ -127,7 +132,7 @@ func load_solution_script() -> Error: "message": "The solution file could not be parsed.", "tests": [], } - file_utils.write_results_file(results, output_dir_path) + file_utils.output_results(results, output_dir_path) return ERR_PARSE_ERROR return OK @@ -170,4 +175,4 @@ func run_tests() -> void: results["status"] = "fail" break - file_utils.write_results_file(results, output_dir_path) + file_utils.output_results(results, output_dir_path) diff --git a/bin/utils/file_utils.gd b/bin/utils/file_utils.gd index cd9ccb0..eaaca37 100644 --- a/bin/utils/file_utils.gd +++ b/bin/utils/file_utils.gd @@ -89,6 +89,37 @@ func load_script(script_path: String) -> Object: return script +func output_results(results: Dictionary, output_dir_path: String) -> Error: + """ + If output_dir_path is empty, outputs friendly-formatted test results to + stdout. + + Otherwise, writes JSON results to results.json in the given path. + """ + if output_dir_path == "": + return print_friendly_results(results) + else: + return write_results_file(results, output_dir_path) + + +func print_friendly_results(results: Dictionary) -> Error: + """ + Prints rich results to stdout. + """ + var rich_results = [""] + if results.status == "pass": + rich_results.push_back("[color=yellow]Exercise passed! Nicely done![/color]") + else: + rich_results.push_back("Exercise has at least one failed test:") + for test in results.tests: + if test.status == "pass": + rich_results.push_back(" %s [color=yellow]passed ✔[/color]" % test.name) + else: + rich_results.push_back(" %s [color=pink]failed: %s[/color]" % [test.name, test.message]) + print_rich("\n".join(rich_results)) + return OK + + func write_results_file(results: Dictionary, output_dir_path: String) -> Error: """ Saves a dictionary as a `results.json` file in the output directory. The file diff --git a/tests/example-empty-file/expected_results.json b/tests/example-empty-file/expected_results.json index 32b3456..3cadf74 100644 --- a/tests/example-empty-file/expected_results.json +++ b/tests/example-empty-file/expected_results.json @@ -5,12 +5,12 @@ { "name": "test_add_1_and_2", "status": "error", - "message": "SCRIPT ERROR: Invalid call. Nonexistent function 'add_2_numbers' in base 'RefCounted (example_empty_file.gd)'.\n at: test_add_1_and_2 (res://tests/example-empty-file/example_empty_file_test.gd:2)\n" + "message": "SCRIPT ERROR: Invalid call. Nonexistent function 'add_2_numbers' in base 'RefCounted (example_empty_file.gd)'.\n at: test_add_1_and_2 (/opt/exercism/gdscript/test-runner/tests/example-empty-file/example_empty_file_test.gd:2)\n" }, { "name": "test_add_10_and_20", "status": "error", - "message": "SCRIPT ERROR: Invalid call. Nonexistent function 'add_2_numbers' in base 'RefCounted (example_empty_file.gd)'.\n at: test_add_10_and_20 (res://tests/example-empty-file/example_empty_file_test.gd:6)\n" + "message": "SCRIPT ERROR: Invalid call. Nonexistent function 'add_2_numbers' in base 'RefCounted (example_empty_file.gd)'.\n at: test_add_10_and_20 (/opt/exercism/gdscript/test-runner/tests/example-empty-file/example_empty_file_test.gd:6)\n" } ] } diff --git a/tests/example-execution-error/expected_results.json b/tests/example-execution-error/expected_results.json index 549c15c..5afdd7c 100644 --- a/tests/example-execution-error/expected_results.json +++ b/tests/example-execution-error/expected_results.json @@ -10,7 +10,7 @@ { "name": "test_0", "status": "error", - "message": "SCRIPT ERROR: Division by zero error in operator '/'.\n at: divide (res://tests/example-execution-error/example_execution_error.gd:2)\n" + "message": "SCRIPT ERROR: Division by zero error in operator '/'.\n at: divide (/opt/exercism/gdscript/test-runner/tests/example-execution-error/example_execution_error.gd:2)\n" } ] } diff --git a/tests/example-incorrect-number-of-arguments/expected_results.json b/tests/example-incorrect-number-of-arguments/expected_results.json index 106ed52..d429cf0 100644 --- a/tests/example-incorrect-number-of-arguments/expected_results.json +++ b/tests/example-incorrect-number-of-arguments/expected_results.json @@ -5,12 +5,12 @@ { "name": "test_too_few_args", "status": "error", - "message": "SCRIPT ERROR: Invalid call to function 'add_numbers' in base 'RefCounted (example_incorrect_number_of_arguments.gd)'. Expected 2 arguments.\n at: test_too_few_args (res://tests/example-incorrect-number-of-arguments/example_incorrect_number_of_arguments_test.gd:2)\n" + "message": "SCRIPT ERROR: Invalid call to function 'add_numbers' in base 'RefCounted (example_incorrect_number_of_arguments.gd)'. Expected 2 arguments.\n at: test_too_few_args (/opt/exercism/gdscript/test-runner/tests/example-incorrect-number-of-arguments/example_incorrect_number_of_arguments_test.gd:2)\n" }, { "name": "test_too_many_args", "status": "error", - "message": "SCRIPT ERROR: Invalid call to function 'add_numbers' in base 'RefCounted (example_incorrect_number_of_arguments.gd)'. Expected 2 arguments.\n at: test_too_many_args (res://tests/example-incorrect-number-of-arguments/example_incorrect_number_of_arguments_test.gd:6)\n" + "message": "SCRIPT ERROR: Invalid call to function 'add_numbers' in base 'RefCounted (example_incorrect_number_of_arguments.gd)'. Expected 2 arguments.\n at: test_too_many_args (/opt/exercism/gdscript/test-runner/tests/example-incorrect-number-of-arguments/example_incorrect_number_of_arguments_test.gd:6)\n" } ] } diff --git a/tests/example-invalid-argument-type/expected_results.json b/tests/example-invalid-argument-type/expected_results.json index a89b03e..c1d1a7f 100644 --- a/tests/example-invalid-argument-type/expected_results.json +++ b/tests/example-invalid-argument-type/expected_results.json @@ -15,7 +15,7 @@ { "name": "test_integer", "status": "error", - "message": "SCRIPT ERROR: Invalid call. Nonexistent function 'ends_with' in base 'int'.\n at: check_something (res://tests/example-invalid-argument-type/example_invalid_argument_type.gd:3)\n" + "message": "SCRIPT ERROR: Invalid call. Nonexistent function 'ends_with' in base 'int'.\n at: check_something (/opt/exercism/gdscript/test-runner/tests/example-invalid-argument-type/example_invalid_argument_type.gd:3)\n" } ] } diff --git a/tests/example-multiple-methods-partial-fail/expected_results.json b/tests/example-multiple-methods-partial-fail/expected_results.json index b03f9c4..4331f2c 100644 --- a/tests/example-multiple-methods-partial-fail/expected_results.json +++ b/tests/example-multiple-methods-partial-fail/expected_results.json @@ -15,7 +15,7 @@ { "name": "test_hello", "status": "error", - "message": "SCRIPT ERROR: Invalid call. Nonexistent function 'return_hello' in base 'RefCounted (example_multiple_methods_partial_fail.gd)'.\n at: test_hello (res://tests/example-multiple-methods-partial-fail/example_multiple_methods_partial_fail_test.gd:10)\n" + "message": "SCRIPT ERROR: Invalid call. Nonexistent function 'return_hello' in base 'RefCounted (example_multiple_methods_partial_fail.gd)'.\n at: test_hello (/opt/exercism/gdscript/test-runner/tests/example-multiple-methods-partial-fail/example_multiple_methods_partial_fail_test.gd:10)\n" } ] } diff --git a/tests/example-not-a-callable/expected_results.json b/tests/example-not-a-callable/expected_results.json index 54ef28c..efc618a 100644 --- a/tests/example-not-a-callable/expected_results.json +++ b/tests/example-not-a-callable/expected_results.json @@ -5,7 +5,7 @@ { "name": "test_call_a_method", "status": "error", - "message": "SCRIPT ERROR: Invalid call. Nonexistent function 'add_2_numbers' in base 'RefCounted (example_not_a_callable.gd)'.\n at: test_call_a_method (res://tests/example-not-a-callable/example_not_a_callable_test.gd:2)\n" + "message": "SCRIPT ERROR: Invalid call. Nonexistent function 'add_2_numbers' in base 'RefCounted (example_not_a_callable.gd)'.\n at: test_call_a_method (/opt/exercism/gdscript/test-runner/tests/example-not-a-callable/example_not_a_callable_test.gd:2)\n" } ] } diff --git a/tests/example-type-hints/expected_results.json b/tests/example-type-hints/expected_results.json index e26932a..80e1bc1 100644 --- a/tests/example-type-hints/expected_results.json +++ b/tests/example-type-hints/expected_results.json @@ -10,7 +10,7 @@ { "name": "test_return_string_incorrect", "status": "error", - "message": "SCRIPT ERROR: Trying to return value of type \"int\" from a function whose return type is \"String\".\n at: return_string (res://tests/example-type-hints/example_type_hints.gd:2)\n" + "message": "SCRIPT ERROR: Trying to return value of type \"int\" from a function whose return type is \"String\".\n at: return_string (/opt/exercism/gdscript/test-runner/tests/example-type-hints/example_type_hints.gd:2)\n" }, { "name": "test_accept_int_correct", @@ -20,7 +20,7 @@ { "name": "test_accept_int_incorrect", "status": "error", - "message": "SCRIPT ERROR: Invalid type in function 'accept_int' in base 'RefCounted (example_type_hints.gd)'. Cannot convert argument 1 from String to int.\n at: test_accept_int_incorrect (res://tests/example-type-hints/example_type_hints_test.gd:14)\n" + "message": "SCRIPT ERROR: Invalid type in function 'accept_int' in base 'RefCounted (example_type_hints.gd)'. Cannot convert argument 1 from String to int.\n at: test_accept_int_incorrect (/opt/exercism/gdscript/test-runner/tests/example-type-hints/example_type_hints_test.gd:14)\n" } ] } diff --git a/tests/example-wrong-method-name/expected_results.json b/tests/example-wrong-method-name/expected_results.json index 922bea2..b61afde 100644 --- a/tests/example-wrong-method-name/expected_results.json +++ b/tests/example-wrong-method-name/expected_results.json @@ -5,12 +5,12 @@ { "name": "test_1_and_2_equals_3", "status": "error", - "message": "SCRIPT ERROR: Invalid call. Nonexistent function 'add_2_numbers' in base 'RefCounted (example_wrong_method_name.gd)'.\n at: test_1_and_2_equals_3 (res://tests/example-wrong-method-name/example_wrong_method_name_test.gd:2)\n" + "message": "SCRIPT ERROR: Invalid call. Nonexistent function 'add_2_numbers' in base 'RefCounted (example_wrong_method_name.gd)'.\n at: test_1_and_2_equals_3 (/opt/exercism/gdscript/test-runner/tests/example-wrong-method-name/example_wrong_method_name_test.gd:2)\n" }, { "name": "test_10_and_20_and_40_equals_70", "status": "error", - "message": "SCRIPT ERROR: Invalid call. Nonexistent function 'add_3_numbers' in base 'RefCounted (example_wrong_method_name.gd)'.\n at: test_10_and_20_and_40_equals_70 (res://tests/example-wrong-method-name/example_wrong_method_name_test.gd:6)\n" + "message": "SCRIPT ERROR: Invalid call. Nonexistent function 'add_3_numbers' in base 'RefCounted (example_wrong_method_name.gd)'.\n at: test_10_and_20_and_40_equals_70 (/opt/exercism/gdscript/test-runner/tests/example-wrong-method-name/example_wrong_method_name_test.gd:6)\n" } ] }