Skip to content

Commit

Permalink
♻️ More better backtrace
Browse files Browse the repository at this point in the history
- Further simplified logic
- Rearranged dependency and centralized the use of a template method to recreate test executable output
- Refined test case / test executable crash messages
- Added a means to encode/decode multiline `gdb` report strings that does not break test results parsing
- Removed a variety of backtrace-specific hacks in test results processing outside of `GeneratorTestResultsBacktrace`
  • Loading branch information
mkarlesky committed Jun 12, 2024
1 parent 1e189b3 commit cacb961
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 177 deletions.
19 changes: 6 additions & 13 deletions lib/ceedling/generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -312,38 +312,31 @@ def generate_test_results(tool:, context:, test_name:, test_filepath:, executabl
@helper.log_test_results_crash( test_name, executable, shell_result )

filename = File.basename( test_filepath )
test_cases = @test_context_extractor.lookup_test_cases( test_filepath )

case @configurator.project_config_hash[:project_use_backtrace]
# If we have the options and tools to learn more, dig into the details
when :gdb
shell_result =
@backtrace.do_gdb(
filename,
executable,
shell_result,
@test_context_extractor.lookup_test_cases( test_filepath )
)
@backtrace.do_gdb( filename, executable, shell_result, test_cases )

# Simple test-case-by-test-case exercise
when :simple
shell_result =
@backtrace.do_simple(
filename,
executable,
shell_result,
@test_context_extractor.lookup_test_cases( test_filepath )
)
@backtrace.do_simple( filename, executable, shell_result, test_cases )

else # :none
# Otherwise, call a crash a single failure so it shows up in the report
shell_result = @generator_test_results.create_crash_failure(
filename,
shell_result
shell_result,
test_cases
)
end
end

processed = @generator_test_results.process_and_write_results(
executable,
shell_result,
arg_hash[:result_file],
@file_finder.find_test_from_file_path(arg_hash[:executable])
Expand Down
106 changes: 50 additions & 56 deletions lib/ceedling/generator_test_results.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,18 @@
require 'rubygems'
require 'rake' # for .ext()
require 'ceedling/constants'
require 'ceedling/exceptions'

class GeneratorTestResults

constructor :configurator, :generator_test_results_sanity_checker, :generator_test_results_backtrace, :yaml_wrapper
constructor :configurator, :generator_test_results_sanity_checker, :yaml_wrapper

def setup()
# Aliases
@sanity_checker = @generator_test_results_sanity_checker
@backtrace = @generator_test_results_backtrace
end

def process_and_write_results(unity_shell_result, results_file, test_file)
def process_and_write_results(executable, unity_shell_result, results_file, test_file)
output_file = results_file

results = get_results_structure
Expand All @@ -29,82 +29,69 @@ def process_and_write_results(unity_shell_result, results_file, test_file)
results[:source][:file] = test_file
results[:time] = unity_shell_result[:time] unless unity_shell_result[:time].nil?

# process test statistics
# Process test statistics
if (unity_shell_result[:output] =~ TEST_STDOUT_STATISTICS_PATTERN)
results[:counts][:total] = $1.to_i
results[:counts][:failed] = $2.to_i
results[:counts][:total] = $1.to_i
results[:counts][:failed] = $2.to_i
results[:counts][:ignored] = $3.to_i
results[:counts][:passed] = (results[:counts][:total] - results[:counts][:failed] - results[:counts][:ignored])
else
if @configurator.project_config_hash[:project_use_backtrace]
# Accessing this code block we expect failure during test execution
# which should be connected with SIGSEGV
results[:counts][:total] = 1 # Set to one as the amount of test is unknown in segfault, and one of the test is failing
results[:counts][:failed] = 1 # Set to one as the one of tests is failing with segfault
results[:counts][:ignored] = 0
results[:counts][:passed] = 0

#Collect function name which cause issue and line number
if unity_shell_result[:output] =~ /\s"(.*)",\sline_num=(\d*)/
results[:failures] << { :test => $1, :line =>$2, :message => unity_shell_result[:output], :unity_test_time => unity_shell_result[:time]}
else
#In case if regex fail write default values
results[:failures] << { :test => '??', :line =>-1, :message => unity_shell_result[:output], :unity_test_time => unity_shell_result[:time]}
end
end
raise CeedlingException.new( "Could not parse output for `#{executable}`: \"#{unity_shell_result[:output]}\"" )
end

# remove test statistics lines
# Remove test statistics lines
output_string = unity_shell_result[:output].sub(TEST_STDOUT_STATISTICS_PATTERN, '')
output_string.lines do |line|
# process unity output
# Process Unity output
case line.chomp
when /(:IGNORE)/
elements = extract_line_elements(line, results[:source][:file])
elements = extract_line_elements( executable, line, results[:source][:file] )
results[:ignores] << elements[0]
results[:stdout] << elements[1] if (!elements[1].nil?)
when /(:PASS$)/
elements = extract_line_elements(line, results[:source][:file])
elements = extract_line_elements( executable, line, results[:source][:file] )
results[:successes] << elements[0]
results[:stdout] << elements[1] if (!elements[1].nil?)
when /(:PASS \(.* ms\)$)/
elements = extract_line_elements(line, results[:source][:file])
elements = extract_line_elements( executable, line, results[:source][:file] )
results[:successes] << elements[0]
results[:stdout] << elements[1] if (!elements[1].nil?)
when /(:FAIL)/
elements = extract_line_elements(line, results[:source][:file])
# TODO: Straighten out :gdb backtrace debugger output
elements[0][:test] = @backtrace.restore_new_line_character_in_flatten_log(elements[0][:test])
elements = extract_line_elements( executable, line, results[:source][:file] )
results[:failures] << elements[0]
results[:stdout] << elements[1] if (!elements[1].nil?)
else # collect up all other
if !@configurator.project_config_hash[:project_use_backtrace]
results[:stdout] << line.chomp
end
else # Collect up all other output
results[:stdout] << line.chomp
end
end

@sanity_checker.verify(results, unity_shell_result[:exit_code])
@sanity_checker.verify( results, unity_shell_result[:exit_code] )

output_file = results_file.ext(@configurator.extension_testfail) if (results[:counts][:failed] > 0)

# TODO: Straighten out :gdb backtrace debugger output
results[:failures].each do |failure|
failure[:message] = @backtrace.unflat_debugger_log(failure[:message])
end
@yaml_wrapper.dump(output_file, results)

return { :result_file => output_file, :result => results }
end

def create_crash_failure(source, shell_result)
# TODO: Filter test cases with command line test case matchers
def create_crash_failure(source, shell_result, test_cases)
count = test_cases.size()

output = []
test_cases.each do |test_case|
output << "#{source}:#{test_case[:line_number]}:#{test_case[:test]}:FAIL: Test executable crashed"
end

shell_result[:output] =
regenerate_test_executable_stdout(
total: 1,
failed: 1,
total: count,
failed: count,
ignored: 0,
output: ["#{source}:1:?<unknown>:FAIL: Test Executable Crashed"])
shell_result[:exit_code] = 1
output: output
)

shell_result[:exit_code] = count

return shell_result
end
Expand Down Expand Up @@ -137,37 +124,44 @@ def get_results_structure
}
end

def extract_line_elements(line, filename)
# handle anything preceding filename in line as extra output to be collected
def extract_line_elements(executable, line, filename)
# Handle anything preceding filename in line as extra output to be collected
stdout = nil
stdout_regex = /(.+)#{Regexp.escape(filename)}.+/i
stdout_regex = /(.+)#{Regexp.escape(filename)}:[0-9]+:(PASS|IGNORE|FAIL).+/i
unity_test_time = 0

if (line =~ stdout_regex)
stdout = $1.clone
unless @configurator.project_config_hash[:project_use_backtrace]
line.sub!(/#{Regexp.escape(stdout)}/, '')
end
line.sub!(/#{Regexp.escape(stdout)}/, '')
end

# collect up test results minus and extra output
# Collect up test results minus any extra output
elements = (line.strip.split(':'))[1..-1]

# find timestamp if available
# Find timestamp if available
if (elements[-1] =~ / \((\d*(?:\.\d*)?) ms\)/)
unity_test_time = $1.to_f / 1000
elements[-1].sub!(/ \((\d*(?:\.\d*)?) ms\)/, '')
end

if elements[3..-1]
message = (elements[3..-1].join(':')).strip
# TODO: Straighten out :gdb backtrace debugger output
message = @backtrace.unflat_debugger_log(message)
else
message = nil
end

return {:test => elements[1], :line => elements[0].to_i, :message => message, :unity_test_time => unity_test_time}, stdout if elements.size >= 3
return {:test => '???', :line => -1, :message => nil, :unity_test_time => unity_test_time} #fallback safe option. TODO better handling
components = {
:test => elements[1],
:line => elements[0].to_i,
# Decode any multline strings
:message => message.nil? ? nil : message.gsub( '\n', "\n" ),
:unity_test_time => unity_test_time
}

return components, stdout if elements.size >= 3

# Fall through failure case
raise CeedlingException.new( "Could not parse results output line \"line\" for `#{executable}`" )
end

end
Loading

0 comments on commit cacb961

Please sign in to comment.