diff --git a/Dockerfile b/Dockerfile index 2613a88..b2b04f3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM crystallang/crystal:1.10.1-alpine +FROM crystallang/crystal:1.11.2-alpine # install packages required to run the tests RUN apk add --no-cache bash jq coreutils diff --git a/bin/build.sh b/bin/build.sh index 933c2b4..2dfcd3d 100755 --- a/bin/build.sh +++ b/bin/build.sh @@ -2,9 +2,7 @@ set -euo pipefail -echo "Building json scaffold helper" -crystal build helpers/scaffold_json.cr --release -o bin/scaffold_json -echo "Building results json helper" -crystal build helpers/result_to_json.cr --release -o bin/result_to_json +echo "Building test_runner" +crystal build src/test_runner.cr --release -o bin/test_runner echo "Building setup test file helper" crystal build helpers/setup_test_file.cr --release -o bin/setup_test_file diff --git a/bin/run.sh b/bin/run.sh index 877b2f5..83fc2d4 100755 --- a/bin/run.sh +++ b/bin/run.sh @@ -43,7 +43,6 @@ echo "${slug}: testing..." # stderr to capture it crystal spec "${modified_spec_file}" --junit_output="${output_dir}" --no-color &> "${capture_file}" -./bin/scaffold_json "${spec_file}" "${scaffold_file}" -./bin/result_to_json "${capture_file}" "${junit_file}" "${scaffold_file}" "${results_file}" +./bin/test_runner "${spec_file}" "${capture_file}" "${junit_file}" "${results_file}" echo "${slug}: done" diff --git a/changelog.md b/changelog.md index f879aef..ade4277 100644 --- a/changelog.md +++ b/changelog.md @@ -1,6 +1,16 @@ +# 2.0 + +- Move to Crystal 1.11.2 +- Refactored test runner + - Going from three exectuables down to two + - Improved build time + - Simplify the scripts to make them easier to maintain. + This includes around a 40-50 percent reduction of lines of code +- Improved documentation + # 1.2 -- Move to Crystal 1.10.x +- Move to Crystal 1.10.1 - Limit output to 500 characters # 1.1.0 diff --git a/helpers/result_to_json.cr b/helpers/result_to_json.cr deleted file mode 100644 index 4033620..0000000 --- a/helpers/result_to_json.cr +++ /dev/null @@ -1,179 +0,0 @@ -require "json" -require "xml" - -spec_output = ARGV[0]? -junit_file = ARGV[1]? -scaffold_json = ARGV[2]? -output_file = ARGV[3]? - -PASS = "pass" -FAIL = "fail" -ERROR = "error" - -TAG_TESTSUITE = "testsuite" -TAG_TESTCASE = "testcase" -TAG_FAILURE = "failure" -TAG_ERROR = "error" -TAG_SKIPPED = "skipped" - -ATTR_MESSAGE = "message" -ATTR_NAME = "name" - -class TestCase - include JSON::Serializable - - getter name : String - getter test_code : String? - property status : String? - property message : String? - property output : String? - property task_id : Int32? - - def initialize(@name, @test_code, @status, @message, @output, @task_id = nil) - end -end - -class TestRun - include JSON::Serializable - - getter version : Int32 - property status : String? - property message : String? - property tests : Array(TestCase) -end - -def find_element(node : XML::Node, name : String) - node.children.find do |child| - child.name == name - end -end - -def find_testsuite(document : XML::Node) - find_element(document, TAG_TESTSUITE).not_nil! -end - -def convert_to_test_cases(test_suite : XML::Node, json_file : JSON::Any) - i = -1 - test_suite.children - .map do |test_case| - if test_case.name != TAG_TESTCASE - next nil - end - - error = find_element(test_case, TAG_ERROR) - failure = find_element(test_case, TAG_FAILURE) - skipped = find_element(test_case, TAG_SKIPPED) - - status = - case - when error then ERROR - when failure then FAIL - when skipped then ERROR - else PASS - end - - message = - case - when error then "#{error[ATTR_MESSAGE]}\n#{test_case.content}".strip - when failure then failure[ATTR_MESSAGE].strip - when skipped then "Test case unexpectedly skipped" - else nil - end - i += 1 - output = json_file[i].to_s.empty? ? nil : json_file[i].to_s - output = unless json_file[i].to_s.empty? - output = json_file[i].to_s - if output.size >= 500 - output = output[..455] + "Output was truncated. Please limit to 500 chars" - end - output - else - nil - end - TestCase.new( - test_case[ATTR_NAME], - nil, - status, - message, - output, - test_case["task_id"]? ? test_case["task_id"].to_i : nil - ) - end - .compact -end - -def merge_test_cases(a : Array(TestCase), b : Array(TestCase)) - a.zip(b).map do |a, b| - a.status = b.status - a.message = b.message - a.output = b.output - a - end -end - -def set_test_run_status(test_run : TestRun, document : XML::Node) - testcase_count = document["tests"].not_nil!.to_i - skipped_count = document["skipped"].not_nil!.to_i - errors_count = document["errors"].not_nil!.to_i - failures_count = document["failures"].not_nil!.to_i - - status = (testcase_count - skipped_count - failures_count - errors_count) == testcase_count ? PASS : FAIL - - test_run.status = status -end - -def set_test_run_error(test_run : TestRun, spec_output : String?) - test_run.status = ERROR - test_run.message = spec_output - test_run.tests.each do |test_case| - test_case.status = FAIL - end -end - -# -# Main start -# - -unless spec_output && junit_file && scaffold_json && output_file - puts <<-USAGE - Usage: - > result_to_json - USAGE - exit 1 -end - -puts "* Reading scaffold json 📖" - -test_run = TestRun.from_json(File.read(scaffold_json)) - -puts "* Checking if junit xml exists 🔍" - -if !File.exists?(junit_file) - puts "* Failed finding junit xml ❌" - - set_test_run_error(test_run, File.read(spec_output)) - - puts "* Writing error result json to: #{output_file} 🖊" - - File.write(output_file, test_run.to_json) - exit -end - -puts "* Reading junit xml ✅" - -junit_xml = File.read(junit_file) -junit_document = XML.parse(junit_xml) -junit_testsuite = find_testsuite(junit_document) - -json_output = JSON.parse(File.read("/tmp/output.json")) -File.delete("/tmp/output.json") - -test_cases = convert_to_test_cases(junit_testsuite, json_output) -test_run.tests = merge_test_cases(test_run.tests, test_cases) -set_test_run_status(test_run, junit_testsuite) - -puts "* Writing merged result json to: #{output_file} 🖊" - -File.write(output_file, test_run.to_json) - -puts "* All done! 🏁" diff --git a/helpers/scaffold_json.cr b/helpers/scaffold_json.cr deleted file mode 100644 index 2ae00eb..0000000 --- a/helpers/scaffold_json.cr +++ /dev/null @@ -1,155 +0,0 @@ -require "compiler/crystal/syntax" -require "json" - -class TestCase - property name : String - property source_code : Array(String) - property snippet : String - property start_location : Crystal::Location? - property end_location : Crystal::Location? - property count : Int32? - - def initialize( - name : String, - source_code : Array(String), - snippet : String, - start_location : Crystal::Location?, - end_location : Crystal::Location?, - count : Int32? - ) - @name = name - @source_code = source_code - @snippet = snippet - @start_location = start_location - @end_location = end_location - @count = count - end - - def test_code : String - start_location = self.start_location - end_location = self.end_location - return snippet if start_location.nil? || end_location.nil? - - start_column_index = start_location.column_number - 1 - start_line_index = start_location.line_number - 1 - end_line_index = end_location.line_number - 1 - - source_code[start_line_index..end_line_index] - .map { |line| line[start_column_index..-1]? || "" } - .join("\n") - end - - def to_json(json) - json.object do - json.field "name", name - json.field "test_code", test_code - json.field "status", nil - json.field "message", nil - json.field "output", nil - json.field "task_id", count - end - end -end - -class TestVisitor < Crystal::Visitor - property tests, breadcrumbs, source_code, test_id - - def initialize(source_code : Array(String)) - @tests = Array(TestCase).new - @breadcrumbs = Array(String).new - @source_code = source_code - @test_id = Array(Int32).new - @count = 0 - end - - def current_test_name_prefix - breadcrumbs.join(" ") - end - - def visit(node : Crystal::Call) - case node.name - when "describe" - handle_visit_describe_call(node) - when "it" - handle_visit_it_call(node) - when "pending" - handle_visit_it_call(node) - end - - false - end - - def end_visit(node : Crystal::Call) - case node.name - when "describe" - handle_end_visit_describe_call(node) - end - end - - def visit(node) - true - end - - private def handle_visit_describe_call(node : Crystal::Call) - @count += 1 if @breadcrumbs.size == 1 - case arg = node.args[0] - when Crystal::StringLiteral - @breadcrumbs << arg.value - when Crystal::Path - @breadcrumbs << arg.to_s - end - accept(node.block.not_nil!) - end - - private def handle_end_visit_describe_call(node : Crystal::Call) - breadcrumbs.pop - return true - end - - private def handle_visit_it_call(node : Crystal::Call) - label = node.args[0].not_nil!.as(Crystal::StringLiteral).value - name = "#{current_test_name_prefix} #{label}" - - snippet = node.block.not_nil!.body.to_s - start_location = node.block.not_nil!.body.location - end_location = node.block.not_nil!.body.end_location - @tests << TestCase.new(name, source_code, snippet, start_location, end_location, @count != 0 ? @count : nil) - end -end - -test_file = ARGV[0]? -output_file = ARGV[1]? - -unless test_file && output_file - puts <<-USAGE - Usage: - > scaffold_json - USAGE - exit 1 -end - -puts "* Reading spec file: #{test_file} 📖" - -test_file_content = File - .read(test_file) - -parser = Crystal::Parser.new(test_file_content) -ast = parser.parse - -visitor = TestVisitor.new(test_file_content.lines) -visitor.accept(ast) - -scaffold = - JSON.build do |json| - json.object do - json.field "version", 3 - json.field "status", nil - json.field "message", nil - json.field "tests", visitor.tests - end - end - -puts "* Writing scaffold json to: #{output_file} 🖊" - -File.write(output_file, scaffold) -puts "* All done converting spec to scaffold json 🏁" diff --git a/src/test_runner.cr b/src/test_runner.cr new file mode 100644 index 0000000..cf9be16 --- /dev/null +++ b/src/test_runner.cr @@ -0,0 +1,234 @@ +require "compiler/crystal/syntax" +require "json" +require "./test_visitor.cr" +require "xml" + +module TestRunner + + # The TestCase class is used to represent a single test case, + # following the version 3 of the exercisms test runner interface. + # + # The class is [json serializable](https://crystal-lang.org/api/JSON/Serializable.html), and can thereby have methods + # to modify the state of the object. + # The class does not feature any other methods then json serialization. + class TestCase + include JSON::Serializable + property name : String + property status : String + property test_code : String + property message : String? + property output : String? + property task_id : Int32? + + # Initializes a new TestCase object. + # + # The class takes the following arguments when initialized: + # - *name* - The name of the test case + # - *status* - The status of the test case, can be either "pass", "fail" or "error" + # - *test_code* - The code that was tested + # - *message* - The error message if the test case failed or errored + # - *output* - The output of the test case + # - *task_id* - The id of the task that the test case belongs to + # + # Example: + # ``` + # TestCase.new("test case name", "pass", "test code", nil, "test output", 1) + # ``` + def initialize(name : String, status : String, test_code : String, message : String?, output : String?, task_id : Int32?) + @name = name + @status = status + @test_code = test_code + @message = message + @output = output + @task_id = task_id + end + end + + # The TestSuite class is used to represent a test suite, + # following the version 3 of the exercisms test runner interface. + # + # The class is [json serializable](https://crystal-lang.org/api/JSON/Serializable.html), and can thereby have methods + # to modify the state of the object. + # The class does not feature any other methods then json serialization. + class TestSuite + include JSON::Serializable + property version : Int32 = 3 # The version of the test runner interface + property status : String + property message : String? + property tests : Array(TestCase)? + + # Initializes a new TestSuite object. + # + # The class takes the following arguments when initialized: + # - *status* - The status of the test suite, can be either "pass", "fail" or "error" + # - *message* - The error message if the test suite failed or errored + # - *tests* - The tests that the test suite contains + # + # Example: + # ``` + # TestSuite.new("pass", nil, [TestCase.new("test case name", "pass", "test code", nil, "test output", 1)]) + # ``` + def initialize(status : String, message : String?, tests : Array(TestCase)?) + @status = status + @tests = tests + @message = message + end + end + + # The execute method is the entry point of the test runner + # and is used to execute the test runner and write the result file. + # + # The method takes the following arguments: + # - *spec_file* - The path to the spec file + # - *capture_file* - The path to the capture file + # - *junit_file* - The path to the junit xml file + # - *result_file* - The path to the result file + # + # Example: + # ``` + # TestRunner.execute("/tmp/spec.cr", "/tmp/capture.txt", "/tmp/junit.xml", "/tmp/result.json") + # ``` + def self.execute(spec_file_path : String, capture_file_path : String, junit_file_path : String, result_file_path : String) + included_failing = false # This determines if the test suite should be marked as passing or failing + + # If the junit xml file does not exist, then it is assumed that the test suite errored. + # The error message is read from the capture file and written to the result file and + # execution is stopped. + unless File.exists?(junit_file_path) + puts "* Failed finding junit xml ❌" + error_message = File.read(capture_file_path) + File.write(result_file_path, TestSuite.new("error", error_message, nil).to_json) + exit(0) + end + + # If the junit xml file exists it is parsed using the parse_xml method. + junit_testsuite = parse_xml(junit_file_path) + puts "* Reading junit xml ✅" + + parsed_spec_file = parse_spec_file(spec_file_path) + puts "* Reading spec file ✅" + + # The standard output has a set path defined by the **setup_test_file.cr** file. + parsed_standard_output = parse_standard_output + puts "* Parsing standard output ✅" + + # Assinging the testcases through looping through the ast of the spec file. + test_cases = parsed_spec_file.map_with_index do |snippet, index| + + # Finding the test case in the junit xml file that matches the current test case. + # This expression returns the test case node if it is found, otherwise it returns nil. + test_case = junit_testsuite.children.find do |test_case| + name = test_case["name"]? + # if the name is nil, then it is assumed that the xml node is not a test case node. + if name.nil? + false + else + name == snippet[:name] + end + end + + # If the test case isnt found then it is taken for granted that an error occured during exection. + if test_case + # If the standard output is empty, then the output is set to nil. + if parsed_standard_output[index].to_s.empty? + output = nil + else + output = parsed_standard_output[index].to_s + end + + # Checking if there is a subnode called failure. + failure = test_case.children.find { |node| node.name == "failure" } + + if failure.nil? + TestCase.new(snippet[:name], "pass", snippet[:snippet], nil, output, snippet[:task_id]) + else + # If there is a failure node, then the test case is marked as failing. + # Which marks the test suite as failing. + included_failing = true + TestCase.new(snippet[:name], "fail", snippet[:snippet], failure["message"]?, output, snippet[:task_id]) + end + else + included_failing = true + TestCase.new(snippet[:name], "error", snippet[:snippet], "Test case not found", nil, snippet[:task_id]) + end + end + + puts "* Grouping solution ✅" + + # If the result array contains any failing test cases, then the test suite is marked as failing. + if included_failing + File.write(result_file_path, TestSuite.new("fail", nil, test_cases).to_json) + else + File.write(result_file_path, TestSuite.new("pass", nil, test_cases).to_json) + end + + puts "* Writing result file ✅" + end + + # The parse_spec_file method is used to parse the spec file. + # It parses it by using ast parsing and then traversing the ast. + # The information is gathered by using the TestVisitor class. + # + # This method takes the following arguments: + # - *spec_file_path* - The path to the spec file + # + # Example: + # ``` + # parse_spec_file("/tmp/spec.cr") + # ``` + private def self.parse_spec_file(spec_file_path : String) : Array(NamedTuple(snippet: String, name: String, task_id: Int32?)) + spec_file_content = File.read(spec_file_path) + parser = Crystal::Parser.new(spec_file_content) + parsed_spec_file = parser.parse + test_visitor = TestVisitor.new(spec_file_content.split("\n")) + test_visitor.accept(parsed_spec_file) + test_visitor.result + end + + # Parses the standard output file and returns the parsed result. + # The file is a json file that is written by the **setup_test_file.cr** file. + # + # After the file is parsed it is deleted. + # This is because the file is always appended to, + # Thereby if not deleted, the file would include the output from previous test runs. + private def self.parse_standard_output : JSON::Any + standard_output = File.read("/tmp/output.json") + File.delete("/tmp/output.json") + JSON.parse(standard_output) + end + + # The parse_xml method is used to parse the junit xml file. + # The method returns the testsuite node of the xml file. + # + # If the file is not a valid junit xml file, then an error is raised. + # + # This method takes the following arguments: + # - *junit_file_path* - The path to the junit xml file + # + # Example: + # ``` + # parse_xml("/tmp/junit.xml") + # ``` + private def self.parse_xml(junit_file_path : String) : XML::Node + junit_xml_content = File.read(junit_file_path) + junit_xml = XML.parse(junit_xml_content) + testsuit = junit_xml.children.find { |node| node.name == "testsuite" } + if testsuit.nil? + raise "Invalid junit xml" + end + testsuit + end +end + +# Assiging ARGV values: +spec_output = ARGV[0]? +output_file = ARGV[1]? +junit_file = ARGV[2]? +result_file = ARGV[3]? + +# If any of the arguments are nil (isn't given), then the usage is printed. +if spec_output.nil? || output_file.nil? || junit_file.nil? || result_file.nil? + puts "Usage: crystal run test_runner.cr -- " +else + TestRunner.execute(spec_output, output_file, junit_file, result_file) +end diff --git a/src/test_visitor.cr b/src/test_visitor.cr new file mode 100644 index 0000000..e1f9846 --- /dev/null +++ b/src/test_visitor.cr @@ -0,0 +1,80 @@ +require "compiler/crystal/syntax" + +# This visitor is used to extract the test snippets from the source code. +# It is used to get the test_code snippets for the test cases. +# But also to get a list of the name of all the test cases. +# Last but not least it also adds the task_id to the test cases. +class TestVisitor < Crystal::Visitor + getter result = [] of NamedTuple(snippet: String, name: String, task_id: Int32?) + + # The initilization of the visitor. + # The source_code is used to get the test snippets. + # The level is used to give the test cases a task_id. + # The breadcrumbs are used to get the name of the test cases. + def initialize(source_code : Array(String)) + @level = 0 + @breadcrumbs = [] of String + @source_code = source_code + end + + # This method is used to visit the nodes of the AST, specifically the Call nodes. + # If the name is either describe, it or pending it will call the corresponding method. + # Otherwise will it not dig deeper into the AST. + def visit(node : Crystal::Call) + case node.name + when "describe" + handle_visit_describe_call(node) + when "it", "pending" + handle_visit_it_call(node) + end + + false + end + + # This method is used to end the visit of the nodes of the AST, specifically the Call nodes. + def end_visit(node : Crystal::Call) + if node.name == "describe" + handle_end_visit_describe_call(node) + end + end + + # This method is used to visit the nodes of the AST, which hasn't been handled by the other visit methods. + # It tells the visitor to dig deeper into the AST. + def visit(node) + true + end + + private def handle_visit_describe_call(node : Crystal::Call) + @level += 1 if @breadcrumbs.size == 1 + case arg = node.args[0] + when Crystal::StringLiteral + @breadcrumbs << arg.value + when Crystal::Path + @breadcrumbs << arg.to_s + end + accept(node.block.not_nil!) + end + + private def handle_end_visit_describe_call(node : Crystal::Call) + @breadcrumbs.pop + true + end + + private def handle_visit_it_call(node : Crystal::Call) + label = node.args[0].not_nil!.as(Crystal::StringLiteral).value + current_test_name_prefix = @breadcrumbs.join(" ") + name = "#{current_test_name_prefix} #{label}" + + start_location = node.block.not_nil!.body.location.not_nil! + end_location = node.block.not_nil!.body.end_location.not_nil! + + start_column_index = start_location.column_number - 1 + start_line_index = start_location.line_number - 1 + end_line_index = end_location.line_number - 1 + + snippet = @source_code[start_line_index..end_line_index] + .map { |line| line[start_column_index..-1]? || "" } + .join("\n") + @result << {snippet: snippet, name: name, task_id: @level != 0 ? @level : nil} + end +end diff --git a/tests/example-all-fail/expected_results.json b/tests/example-all-fail/expected_results.json index 833af3e..4624566 100644 --- a/tests/example-all-fail/expected_results.json +++ b/tests/example-all-fail/expected_results.json @@ -4,8 +4,8 @@ "tests": [ { "name": "Bob responds to stating something", - "test_code": "Bob.hey(\"Tom-ay-to, tom-aaaah-to.\").should eq \"Whatever.\"", "status": "fail", + "test_code": "Bob.hey(\"Tom-ay-to, tom-aaaah-to.\").should eq \"Whatever.\"", "message": "Expected: \"Whatever.\" got: \"Whatevah.\"" } ] diff --git a/tests/example-empty-file/expected_results.json b/tests/example-empty-file/expected_results.json index 31cdbc0..b95cb31 100644 --- a/tests/example-empty-file/expected_results.json +++ b/tests/example-empty-file/expected_results.json @@ -1,12 +1,5 @@ { "version": 3, "status": "error", - "message": "Showing last frame. Use --error-trace for full trace.\n\nIn tests/example-empty-file/spec/modified_test_spec.cr:25:5\n\n 25 | Bob.hey(\"Tom-ay-to, tom-aaaah-to.\").should eq \"Whatever.\"\n ^--\nError: undefined constant Bob\n\nDid you mean 'Box'?\n", - "tests": [ - { - "name": "Bob responds to stating something", - "test_code": "Bob.hey(\"Tom-ay-to, tom-aaaah-to.\").should eq \"Whatever.\"", - "status": "fail" - } - ] + "message": "Showing last frame. Use --error-trace for full trace.\n\nIn tests/example-empty-file/spec/modified_test_spec.cr:25:5\n\n 25 | Bob.hey(\"Tom-ay-to, tom-aaaah-to.\").should eq \"Whatever.\"\n ^--\nError: undefined constant Bob\n\nDid you mean 'Box'?\n" } diff --git a/tests/example-partial-fail/expected_results.json b/tests/example-partial-fail/expected_results.json index 2a21b2f..b860be5 100644 --- a/tests/example-partial-fail/expected_results.json +++ b/tests/example-partial-fail/expected_results.json @@ -4,14 +4,14 @@ "tests": [ { "name": "Bob responds to stating something", - "test_code": "Bob.hey(\"Tom-ay-to, tom-aaaah-to.\").should eq \"Whatever.\"", "status": "fail", + "test_code": "Bob.hey(\"Tom-ay-to, tom-aaaah-to.\").should eq \"Whatever.\"", "message": "Expected: \"Whatever.\" got: \"Whatevah.\"" }, { "name": "Bob responds to blank input", - "test_code": "Bob.hey(\" \").should eq \"Fine. Be that way!\"", - "status": "pass" + "status": "pass", + "test_code": "Bob.hey(\" \").should eq \"Fine. Be that way!\"" } ] } diff --git a/tests/example-success-with-path/expected_results.json b/tests/example-success-with-path/expected_results.json index 3f1aeb8..209edaa 100644 --- a/tests/example-success-with-path/expected_results.json +++ b/tests/example-success-with-path/expected_results.json @@ -4,8 +4,8 @@ "tests": [ { "name": "Bob::Bob1 responds to stating something", - "test_code": "Bob::Bob1.hey(\"Tom-ay-to, tom-aaaah-to.\").should eq \"Whatever.\"", - "status": "pass" + "status": "pass", + "test_code": "Bob::Bob1.hey(\"Tom-ay-to, tom-aaaah-to.\").should eq \"Whatever.\"" } ] } diff --git a/tests/example-success-with-sub-group/expected_results.json b/tests/example-success-with-sub-group/expected_results.json index d4e82bf..067a3ca 100644 --- a/tests/example-success-with-sub-group/expected_results.json +++ b/tests/example-success-with-sub-group/expected_results.json @@ -4,20 +4,20 @@ "tests": [ { "name": "Bob hey responds to stating something", - "test_code": "Bob.hey(\"Tom-ay-to, tom-aaaah-to.\").should eq \"Whatever. Tom-ay-to, tom-aaaah-to.\"", "status": "pass", + "test_code": "Bob.hey(\"Tom-ay-to, tom-aaaah-to.\").should eq \"Whatever. Tom-ay-to, tom-aaaah-to.\"", "task_id": 1 }, { "name": "Bob hey doesnt add extra part when not given", - "test_code": "Bob.hey(\"\").should eq \"Whatever. \"", "status": "pass", + "test_code": "Bob.hey(\"\").should eq \"Whatever. \"", "task_id": 1 }, { "name": "Bob bye says bye when calling bye", - "test_code": "Bob.bye(\"\").should eq \"Bye.\"", "status": "pass", + "test_code": "Bob.bye(\"\").should eq \"Bye.\"", "task_id": 2 } ] diff --git a/tests/example-success/expected_results.json b/tests/example-success/expected_results.json index 8d788af..984424a 100644 --- a/tests/example-success/expected_results.json +++ b/tests/example-success/expected_results.json @@ -4,8 +4,8 @@ "tests": [ { "name": "Bob responds to stating something", - "test_code": "Bob.hey(\"Tom-ay-to, tom-aaaah-to.\").should eq \"Whatever.\"", - "status": "pass" + "status": "pass", + "test_code": "Bob.hey(\"Tom-ay-to, tom-aaaah-to.\").should eq \"Whatever.\"" } ] } diff --git a/tests/example-syntax-error/expected_results.json b/tests/example-syntax-error/expected_results.json index a68a24c..06ca1d5 100644 --- a/tests/example-syntax-error/expected_results.json +++ b/tests/example-syntax-error/expected_results.json @@ -1,12 +1,5 @@ { "version": 3, "status": "error", - "message": "In tests/example-syntax-error/src/mini_bob.cr:6:1\n\n 6 | end\n ^\nError: expecting token 'EOF', not 'end'\n", - "tests": [ - { - "name": "Bob responds to stating something", - "test_code": "Bob.hey(\"Tom-ay-to, tom-aaaah-to.\").should eq \"Whatever.\"", - "status": "fail" - } - ] + "message": "In tests/example-syntax-error/src/mini_bob.cr:6:1\n\n 6 | end\n ^\nError: expecting token 'EOF', not 'end'\n" } diff --git a/tests/example-with-other-stuff-in-spec/expected_results.json b/tests/example-with-other-stuff-in-spec/expected_results.json index b44f8bc..40fa58e 100644 --- a/tests/example-with-other-stuff-in-spec/expected_results.json +++ b/tests/example-with-other-stuff-in-spec/expected_results.json @@ -4,8 +4,8 @@ "tests": [ { "name": "Leap year not divisible by 4 in common year", - "test_code": "Year.leap?(2015).should be_false", - "status": "pass" + "status": "pass", + "test_code": "Year.leap?(2015).should be_false" } ] } diff --git a/tests/example-with-output/expected_results.json b/tests/example-with-output/expected_results.json index 060fa7c..dce1bea 100644 --- a/tests/example-with-output/expected_results.json +++ b/tests/example-with-output/expected_results.json @@ -4,22 +4,22 @@ "tests": [ { "name": "Bob hey responds to stating something", - "test_code": "Bob.hey(\"Tom-ay-to, tom-aaaah-to.\").should eq \"Whatever. Tom-ay-to, tom-aaaah-to.\"", "status": "pass", + "test_code": "Bob.hey(\"Tom-ay-to, tom-aaaah-to.\").should eq \"Whatever. Tom-ay-to, tom-aaaah-to.\"", "output": "1\n1\n5\n5\n6\n6\n5\n5\n10\n10\n2\n2", "task_id": 1 }, { "name": "Bob hey doesnt add extra part when not given", - "test_code": "Bob.hey(\"\").should eq \"Whatever. \"", "status": "pass", + "test_code": "Bob.hey(\"\").should eq \"Whatever. \"", "output": "1\n1\n5\n5\n6\n6\n5\n5\n10\n10\n2\n2", "task_id": 1 }, { "name": "Bob bye says bye when calling bye", - "test_code": "Bob.bye(\"\").should eq \"Bye.\"", "status": "pass", + "test_code": "Bob.bye(\"\").should eq \"Bye.\"", "output": "\"Bye.\"\n5", "task_id": 2 }