Add Launchable into CI
This commit is contained in:
parent
7da3f8dcd3
commit
3371936b6f
71
.github/workflows/macos.yml
vendored
71
.github/workflows/macos.yml
vendored
@ -13,6 +13,21 @@ on:
|
|||||||
# https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/collaborating-on-repositories-with-code-quality-features/troubleshooting-required-status-checks#handling-skipped-but-required-checks
|
# https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/collaborating-on-repositories-with-code-quality-features/troubleshooting-required-status-checks#handling-skipped-but-required-checks
|
||||||
merge_group:
|
merge_group:
|
||||||
|
|
||||||
|
env:
|
||||||
|
# GITHUB_PULL_REQUEST_URL are used for commenting test reports in Launchable Github App.
|
||||||
|
# https://github.com/launchableinc/cli/blob/v1.80.1/launchable/utils/link.py#L42
|
||||||
|
GITHUB_PULL_REQUEST_URL: ${{ github.event.pull_request.html_url }}
|
||||||
|
# The following envs are necessary in Launchable tokenless authentication.
|
||||||
|
# https://github.com/launchableinc/cli/blob/v1.80.1/launchable/utils/authentication.py#L20
|
||||||
|
LAUNCHABLE_ORGANIZATION: ${{ github.repository_owner }}
|
||||||
|
LAUNCHABLE_WORKSPACE: ${{ github.event.repository.name }}
|
||||||
|
# https://github.com/launchableinc/cli/blob/v1.80.1/launchable/utils/authentication.py#L71
|
||||||
|
GITHUB_PR_HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||||
|
# This secret setting is needed if you want to run Launchable on your forked
|
||||||
|
# repository.
|
||||||
|
# See https://github.com/ruby/ruby/wiki/CI-Servers#launchable-ci for details.
|
||||||
|
LAUNCHABLE_TOKEN: ${{ secrets.LAUNCHABLE_TOKEN }}
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }} / ${{ startsWith(github.event_name, 'pull') && github.ref_name || github.sha }}
|
group: ${{ github.workflow }} / ${{ startsWith(github.event_name, 'pull') && github.ref_name || github.sha }}
|
||||||
cancel-in-progress: ${{ startsWith(github.event_name, 'pull') }}
|
cancel-in-progress: ${{ startsWith(github.event_name, 'pull') }}
|
||||||
@ -25,12 +40,14 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
test_task: ['check']
|
test_task: ['check']
|
||||||
|
test_opts: ['']
|
||||||
os:
|
os:
|
||||||
- macos-12
|
- macos-12
|
||||||
- macos-13
|
- macos-13
|
||||||
- ${{ github.repository == 'ruby/ruby' && 'macos-arm-oss' || 'macos-14' }}
|
- ${{ github.repository == 'ruby/ruby' && 'macos-arm-oss' || 'macos-14' }}
|
||||||
include:
|
include:
|
||||||
- test_task: test-all TESTS=--repeat-count=2
|
- test_task: test-all
|
||||||
|
test_opts: --repeat-count=2
|
||||||
os: ${{ github.repository == 'ruby/ruby' && 'macos-arm-oss' || 'macos-14' }}
|
os: ${{ github.repository == 'ruby/ruby' && 'macos-arm-oss' || 'macos-14' }}
|
||||||
- test_task: test-bundled-gems
|
- test_task: test-bundled-gems
|
||||||
os: ${{ github.repository == 'ruby/ruby' && 'macos-arm-oss' || 'macos-14' }}
|
os: ${{ github.repository == 'ruby/ruby' && 'macos-arm-oss' || 'macos-14' }}
|
||||||
@ -50,10 +67,23 @@ jobs:
|
|||||||
)}}
|
)}}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
- name: Enable Launchable conditionally
|
||||||
|
id: enable_launchable
|
||||||
|
run: echo "enable_launchable=true" >> $GITHUB_OUTPUT
|
||||||
|
working-directory:
|
||||||
|
if: >-
|
||||||
|
${{
|
||||||
|
(github.repository == 'ruby/ruby' ||
|
||||||
|
(github.repository != 'ruby/ruby' && env.LAUNCHABLE_TOKEN)) &&
|
||||||
|
(matrix.test_task == 'check' || matrix.test_task == 'test-all')
|
||||||
|
}}
|
||||||
|
|
||||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
with:
|
with:
|
||||||
sparse-checkout-cone-mode: false
|
sparse-checkout-cone-mode: false
|
||||||
sparse-checkout: /.github
|
sparse-checkout: /.github
|
||||||
|
# Set fetch-depth: 0 so that Launchable can receive commits information.
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Install libraries
|
- name: Install libraries
|
||||||
uses: ./.github/actions/setup/macos
|
uses: ./.github/actions/setup/macos
|
||||||
@ -80,6 +110,41 @@ jobs:
|
|||||||
echo "TESTS=${TESTS}" >> $GITHUB_ENV
|
echo "TESTS=${TESTS}" >> $GITHUB_ENV
|
||||||
if: ${{ matrix.test_task == 'check' && matrix.skipped_tests }}
|
if: ${{ matrix.test_task == 'check' && matrix.skipped_tests }}
|
||||||
|
|
||||||
|
# Launchable CLI requires Python and Java
|
||||||
|
# https://www.launchableinc.com/docs/resources/cli-reference/
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@871daa956ca9ea99f3c3e30acb424b7960676734 # v5.0.0
|
||||||
|
with:
|
||||||
|
python-version: "3.10"
|
||||||
|
if: steps.enable_launchable.outputs.enable_launchable
|
||||||
|
|
||||||
|
- name: Set up Java
|
||||||
|
uses: actions/setup-java@7a445ee88d4e23b52c33fdc7601e40278616c7f8 # v4.0.0
|
||||||
|
with:
|
||||||
|
distribution: 'temurin'
|
||||||
|
java-version: '17'
|
||||||
|
if: steps.enable_launchable.outputs.enable_launchable
|
||||||
|
|
||||||
|
- name: Set up Launchable
|
||||||
|
run: |
|
||||||
|
set -x
|
||||||
|
pip install launchable
|
||||||
|
launchable verify
|
||||||
|
: # The build name cannot include a slash, so we replace the string here.
|
||||||
|
github_ref="$(echo ${{ github.ref }} | sed 's/\//_/g')"
|
||||||
|
: # With the --name option, we need to configure a unique identifier for this build.
|
||||||
|
: # To avoid setting the same build name as the CI which runs on other branches, we use the branch name here.
|
||||||
|
: #
|
||||||
|
: # FIXME: Need to fix `WARNING: Failed to process a change to a file`.
|
||||||
|
: # https://github.com/launchableinc/cli/issues/786
|
||||||
|
launchable record build --name ${github_ref}_${GITHUB_PR_HEAD_SHA}
|
||||||
|
echo "TESTS=${TESTS} --launchable-test-reports=launchable_reports.json" >> $GITHUB_ENV
|
||||||
|
if: steps.enable_launchable.outputs.enable_launchable
|
||||||
|
|
||||||
|
- name: Set extra test options
|
||||||
|
run: echo "TESTS=$TESTS ${{ matrix.test_opts }}" >> $GITHUB_ENV
|
||||||
|
if: matrix.test_opts
|
||||||
|
|
||||||
- name: make ${{ matrix.test_task }}
|
- name: make ${{ matrix.test_task }}
|
||||||
run: |
|
run: |
|
||||||
make -s ${{ matrix.test_task }} ${TESTS:+TESTS="$TESTS"}
|
make -s ${{ matrix.test_task }} ${TESTS:+TESTS="$TESTS"}
|
||||||
@ -99,6 +164,10 @@ jobs:
|
|||||||
if: ${{ matrix.test_task == 'check' && matrix.skipped_tests }}
|
if: ${{ matrix.test_task == 'check' && matrix.skipped_tests }}
|
||||||
continue-on-error: ${{ matrix.continue-on-skipped_tests || false }}
|
continue-on-error: ${{ matrix.continue-on-skipped_tests || false }}
|
||||||
|
|
||||||
|
- name: Launchable - record tests
|
||||||
|
run: launchable record tests --flavor os=${{ matrix.os }} --flavor test_task=${{ matrix.test_task }} raw launchable_reports.json
|
||||||
|
if: ${{ always() && steps.enable_launchable.outputs.enable_launchable }}
|
||||||
|
|
||||||
- uses: ./.github/actions/slack
|
- uses: ./.github/actions/slack
|
||||||
with:
|
with:
|
||||||
label: ${{ matrix.os }} / ${{ matrix.test_task }}
|
label: ${{ matrix.os }} / ${{ matrix.test_task }}
|
||||||
|
@ -842,7 +842,7 @@ module Test
|
|||||||
end
|
end
|
||||||
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?
|
if @options.values_at(:longest, :most_asserted).any?
|
||||||
@tops ||= {}
|
@tops ||= {}
|
||||||
rec = [suite.name, method, assertions, time, error]
|
rec = [suite.name, method, assertions, time, error]
|
||||||
@ -854,38 +854,6 @@ module Test
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
# (((@record ||= {})[suite] ||= {})[method]) = [assertions, time, error]
|
# (((@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
|
super
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -914,104 +882,6 @@ module Test
|
|||||||
opts.on '--most-asserted=N', Integer, 'Show most asserted N tests' do |n|
|
opts.on '--most-asserted=N', Integer, 'Show most asserted N tests' do |n|
|
||||||
options[:most_asserted] = n
|
options[:most_asserted] = n
|
||||||
end
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -1483,6 +1353,198 @@ module Test
|
|||||||
end
|
end
|
||||||
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
|
class Runner # :nodoc: all
|
||||||
|
|
||||||
attr_accessor :report, :failures, :errors, :skips # :nodoc:
|
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
|
# failure or error in teardown, it will be sent again with the
|
||||||
# error or failure.
|
# error or failure.
|
||||||
|
|
||||||
def record suite, method, assertions, time, error
|
def record suite, method, assertions, time, error, source_location = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
def location e # :nodoc:
|
def location e # :nodoc:
|
||||||
last_before_assertion = ""
|
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|
|
e.backtrace.reverse_each do |s|
|
||||||
break if s =~ /in .(?:Test::Unit::(?:Core)?Assertions#)?(assert|refute|flunk|pass|fail|raise|must|wont)/
|
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::ExcludesOption
|
||||||
prepend Test::Unit::TimeoutOption
|
prepend Test::Unit::TimeoutOption
|
||||||
prepend Test::Unit::RunCount
|
prepend Test::Unit::RunCount
|
||||||
|
prepend Test::Unit::LaunchableOption::Nothing
|
||||||
|
|
||||||
##
|
##
|
||||||
# Begins the full test run. Delegates to +runner+'s #_run method.
|
# Begins the full test run. Delegates to +runner+'s #_run method.
|
||||||
@ -1867,6 +1930,7 @@ module Test
|
|||||||
class AutoRunner # :nodoc: all
|
class AutoRunner # :nodoc: all
|
||||||
class Runner < Test::Unit::Runner
|
class Runner < Test::Unit::Runner
|
||||||
include Test::Unit::RequireFiles
|
include Test::Unit::RequireFiles
|
||||||
|
include Test::Unit::LaunchableOption
|
||||||
end
|
end
|
||||||
|
|
||||||
attr_accessor :to_run, :options
|
attr_accessor :to_run, :options
|
||||||
|
@ -180,7 +180,7 @@ module Test
|
|||||||
else
|
else
|
||||||
error = ProxyError.new(error)
|
error = ProxyError.new(error)
|
||||||
end
|
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
|
super
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
Loading…
x
Reference in New Issue
Block a user