summaryrefslogtreecommitdiff
path: root/tool/lib
diff options
context:
space:
mode:
authorNaoto Ono <onoto1998@gmail.com>2024-01-31 16:45:11 +0900
committerJun Aruga <junaruga@users.noreply.github.com>2024-02-23 14:10:01 +0100
commit3371936b6f863ab0aae0ad5a106cad03b377b88e (patch)
tree14b1a3e2fef49bdbc9231dc4685fdf9c758de9a5 /tool/lib
parent7da3f8dcd34a58ce806cf2d8b22edb3261dea131 (diff)
Add Launchable into CI
Diffstat (limited to 'tool/lib')
-rw-r--r--tool/lib/test/unit.rb330
-rw-r--r--tool/lib/test/unit/parallel.rb2
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