diff options
author | Naoto Ono <onoto1998@gmail.com> | 2024-01-31 16:45:11 +0900 |
---|---|---|
committer | Jun Aruga <junaruga@users.noreply.github.com> | 2024-02-23 14:10:01 +0100 |
commit | 3371936b6f863ab0aae0ad5a106cad03b377b88e (patch) | |
tree | 14b1a3e2fef49bdbc9231dc4685fdf9c758de9a5 /tool/lib | |
parent | 7da3f8dcd34a58ce806cf2d8b22edb3261dea131 (diff) |
Add Launchable into CI
Diffstat (limited to 'tool/lib')
-rw-r--r-- | tool/lib/test/unit.rb | 330 | ||||
-rw-r--r-- | tool/lib/test/unit/parallel.rb | 2 |
2 files changed, 198 insertions, 134 deletions
diff --git a/tool/lib/test/unit.rb b/tool/lib/test/unit.rb index fc51e7e97c..a068ae6b9d 100644 --- a/tool/lib/test/unit.rb +++ b/tool/lib/test/unit.rb @@ -842,7 +842,7 @@ module Test end end - def record(suite, method, assertions, time, error) + def record(suite, method, assertions, time, error, source_location = nil) if @options.values_at(:longest, :most_asserted).any? @tops ||= {} rec = [suite.name, method, assertions, time, error] @@ -854,38 +854,6 @@ module Test end end # (((@record ||= {})[suite] ||= {})[method]) = [assertions, time, error] - if writer = @options[:launchable_test_reports] - location = suite.instance_method(method).source_location - if location && path = location.first - # Launchable JSON schema is defined at - # https://github.com/search?q=repo%3Alaunchableinc%2Fcli+https%3A%2F%2Flaunchableinc.com%2Fschema%2FRecordTestInput&type=code. - e = case error - when nil - status = 'TEST_PASSED' - nil - when Test::Unit::PendedError - status = 'TEST_SKIPPED' - "Skipped:\n#{klass}##{meth} [#{location e}]:\n#{e.message}\n" - when Test::Unit::AssertionFailedError - status = 'TEST_FAILED' - "Failure:\n#{klass}##{meth} [#{location e}]:\n#{e.message}\n" - when Timeout::Error - status = 'TEST_FAILED' - "Timeout:\n#{klass}##{meth}\n" - else - status = 'TEST_FAILED' - bt = Test::filter_backtrace(e.backtrace).join "\n " - "Error:\n#{klass}##{meth}:\n#{e.class}: #{e.message.b}\n #{bt}\n" - end - writer.write_object do - writer.write_key_value('testPath', "file=#{path}#class=#{suite.name}#testcase=#{method}",) - writer.write_key_value('status', status) - writer.write_key_value('duration', time) - writer.write_key_value('createdAt', Time.now) - writer.write_key_value('stderr', e) if e - end - end - end super end @@ -914,104 +882,6 @@ module Test opts.on '--most-asserted=N', Integer, 'Show most asserted N tests' do |n| options[:most_asserted] = n end - opts.on '--launchable-test-reports=PATH', String, 'Report test results in Launchable JSON format' do |path| - require 'json' - options[:launchable_test_reports] = writer = JsonStreamWriter.new(path) - writer.write_array('testCases') - at_exit{ writer.close } - end - end - ## - # JsonStreamWriter writes a JSON file using a stream. - # By utilizing a stream, we can minimize memory usage, especially for large files. - class JsonStreamWriter - def initialize(path) - @file = File.open(path, "w") - @file.write("{") - @indent_level = 0 - @is_first_key_val = true - @is_first_obj = true - write_new_line - end - - def write_object - if @is_first_obj - @is_first_obj = false - else - write_comma - write_new_line - end - @indent_level += 1 - write_indent - @file.write("{") - write_new_line - @indent_level += 1 - yield - @indent_level -= 1 - write_new_line - write_indent - @file.write("}") - @indent_level -= 1 - @is_first_key_val = true - end - - def write_array(key) - @indent_level += 1 - write_indent - @file.write(to_json_str(key)) - write_colon - @file.write(" ", "[") - write_new_line - end - - def write_key_value(key, value) - if @is_first_key_val - @is_first_key_val = false - else - write_comma - write_new_line - end - write_indent - @file.write(to_json_str(key)) - write_colon - @file.write(" ") - @file.write(to_json_str(value)) - end - - def close - close_array - @indent_level -= 1 - write_new_line - @file.write("}") - end - - private - def to_json_str(obj) - JSON.dump(obj) - end - - def write_indent - @file.write(" " * 2 * @indent_level) - end - - def write_new_line - @file.write("\n") - end - - def write_comma - @file.write(',') - end - - def write_colon - @file.write(":") - end - - def close_array - write_new_line - write_indent - @file.write("]") - @indent_level -= 1 - end end end @@ -1483,6 +1353,198 @@ module Test end end + module LaunchableOption + module Nothing + private + def setup_options(opts, options) + super + opts.define_tail 'Launchable options:' + # This is expected to be called by Test::Unit::Worker. + opts.on_tail '--launchable-test-reports=PATH', String, 'Do nothing' + end + end + + def record(suite, method, assertions, time, error, source_location = nil) + if writer = @options[:launchable_test_reports] + if path = (source_location || suite.instance_method(method).source_location).first + # Launchable JSON schema is defined at + # https://github.com/search?q=repo%3Alaunchableinc%2Fcli+https%3A%2F%2Flaunchableinc.com%2Fschema%2FRecordTestInput&type=code. + e = case error + when nil + status = 'TEST_PASSED' + nil + when Test::Unit::PendedError + status = 'TEST_SKIPPED' + "Skipped:\n#{suite.name}##{method} [#{location error}]:\n#{error.message}\n" + when Test::Unit::AssertionFailedError + status = 'TEST_FAILED' + "Failure:\n#{suite.name}##{method} [#{location error}]:\n#{error.message}\n" + when Timeout::Error + status = 'TEST_FAILED' + "Timeout:\n#{suite.name}##{method}\n" + else + status = 'TEST_FAILED' + bt = Test::filter_backtrace(error.backtrace).join "\n " + "Error:\n#{suite.name}##{method}:\n#{error.class}: #{error.message.b}\n #{bt}\n" + end + repo_path = File.expand_path("#{__dir__}/../../../") + relative_path = path.delete_prefix("#{repo_path}/") + # The test path is a URL-encoded representation. + # https://github.com/launchableinc/cli/blob/v1.81.0/launchable/testpath.py#L18 + test_path = {file: relative_path, class: suite.name, testcase: method}.map{|key, val| + "#{encode_test_path_component(key)}=#{encode_test_path_component(val)}" + }.join('#') + end + end + super + ensure + if writer && test_path && status + # Occasionally, the file writing operation may be paused, especially when `--repeat-count` is specified. + # In such cases, we proceed to execute the operation here. + writer.write_object do + writer.write_key_value('testPath', test_path) + writer.write_key_value('status', status) + writer.write_key_value('duration', time) + writer.write_key_value('createdAt', Time.now.to_s) + writer.write_key_value('stderr', e) + writer.write_key_value('stdout', nil) + end + end + end + + private + def setup_options(opts, options) + super + opts.on_tail '--launchable-test-reports=PATH', String, 'Report test results in Launchable JSON format' do |path| + require 'json' + require 'uri' + options[:launchable_test_reports] = writer = JsonStreamWriter.new(path) + writer.write_array('testCases') + main_pid = Process.pid + at_exit { + # This block is executed when the fork block in a test is completed. + # Therefore, we need to verify whether all tests have been completed. + stack = caller + if stack.size == 0 && main_pid == Process.pid && $!.is_a?(SystemExit) + writer.close + end + } + end + + def encode_test_path_component component + component.to_s.gsub('%', '%25').gsub('=', '%3D').gsub('#', '%23').gsub('&', '%26') + end + end + + ## + # JsonStreamWriter writes a JSON file using a stream. + # By utilizing a stream, we can minimize memory usage, especially for large files. + class JsonStreamWriter + def initialize(path) + @file = File.open(path, "w") + @file.write("{") + @indent_level = 0 + @is_first_key_val = true + @is_first_obj = true + write_new_line + end + + def write_object + if @is_first_obj + @is_first_obj = false + else + write_comma + write_new_line + end + @indent_level += 1 + write_indent + @file.write("{") + write_new_line + @indent_level += 1 + yield + @indent_level -= 1 + write_new_line + write_indent + @file.write("}") + @indent_level -= 1 + @is_first_key_val = true + # Occasionally, invalid JSON will be created as shown below, especially when `--repeat-count` is specified. + # { + # "testPath": "file=test%2Ftest_timeout.rb&class=TestTimeout&testcase=test_allows_zero_seconds", + # "status": "TEST_PASSED", + # "duration": 2.7e-05, + # "createdAt": "2024-02-09 12:21:07 +0000", + # "stderr": null, + # "stdout": null + # }: null <- here + # }, + # To prevent this, IO#flush is called here. + @file.flush + end + + def write_array(key) + @indent_level += 1 + write_indent + @file.write(to_json_str(key)) + write_colon + @file.write(" ", "[") + write_new_line + end + + def write_key_value(key, value) + if @is_first_key_val + @is_first_key_val = false + else + write_comma + write_new_line + end + write_indent + @file.write(to_json_str(key)) + write_colon + @file.write(" ") + @file.write(to_json_str(value)) + end + + def close + return if @file.closed? + close_array + @indent_level -= 1 + write_new_line + @file.write("}") + @file.flush + @file.close + end + + private + def to_json_str(obj) + JSON.dump(obj) + end + + def write_indent + @file.write(" " * 2 * @indent_level) + end + + def write_new_line + @file.write("\n") + end + + def write_comma + @file.write(',') + end + + def write_colon + @file.write(":") + end + + def close_array + write_new_line + write_indent + @file.write("]") + @indent_level -= 1 + end + end + end + class Runner # :nodoc: all attr_accessor :report, :failures, :errors, :skips # :nodoc: @@ -1720,13 +1782,13 @@ module Test # failure or error in teardown, it will be sent again with the # error or failure. - def record suite, method, assertions, time, error + def record suite, method, assertions, time, error, source_location = nil end def location e # :nodoc: last_before_assertion = "" - return '<empty>' unless e.backtrace # SystemStackError can return nil. + return '<empty>' unless e&.backtrace # SystemStackError can return nil. e.backtrace.reverse_each do |s| break if s =~ /in .(?:Test::Unit::(?:Core)?Assertions#)?(assert|refute|flunk|pass|fail|raise|must|wont)/ @@ -1811,6 +1873,7 @@ module Test prepend Test::Unit::ExcludesOption prepend Test::Unit::TimeoutOption prepend Test::Unit::RunCount + prepend Test::Unit::LaunchableOption::Nothing ## # Begins the full test run. Delegates to +runner+'s #_run method. @@ -1867,6 +1930,7 @@ module Test class AutoRunner # :nodoc: all class Runner < Test::Unit::Runner include Test::Unit::RequireFiles + include Test::Unit::LaunchableOption end attr_accessor :to_run, :options diff --git a/tool/lib/test/unit/parallel.rb b/tool/lib/test/unit/parallel.rb index f2244ec20a..ac297d4a0e 100644 --- a/tool/lib/test/unit/parallel.rb +++ b/tool/lib/test/unit/parallel.rb @@ -180,7 +180,7 @@ module Test else error = ProxyError.new(error) end - _report "record", Marshal.dump([suite.name, method, assertions, time, error]) + _report "record", Marshal.dump([suite.name, method, assertions, time, error, suite.instance_method(method).source_location]) super end end |