Skip to content

Commit

Permalink
♻️ Crash detection instead of only segfault check
Browse files Browse the repository at this point in the history
- Renamed LogLabels to address crashes
- Reworked test executable handling for general crash detection and useful logging
- Updated spec tests for segfault detection to reference crash detection instead
  • Loading branch information
mkarlesky committed May 14, 2024
1 parent de8821b commit c32b506
Show file tree
Hide file tree
Showing 7 changed files with 116 additions and 85 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ void test_add_numbers_adds_numbers(void) {
}

void test_add_numbers_will_fail(void) {
// Platform-independent way of creating a segmentation fault
// Platform-independent way of forcing a crash
uint32_t* nullptr = (void*) 0;
uint32_t i = *nullptr;
TEST_ASSERT_EQUAL_INT(2, add_numbers(i,2));
Expand Down
2 changes: 1 addition & 1 deletion lib/ceedling/constants.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class LogLabels
EXCEPTION = 5 # decorator + 'EXCEPTION:'
CONSTRUCT = 6 # decorator only
RUN = 7 # decorator only
SEGFAULT = 8 # decorator only
CRASH = 8 # decorator only
PASS = 9 # decorator only
FAIL = 10 # decorator only
TITLE = 11 # decorator only
Expand Down
43 changes: 25 additions & 18 deletions lib/ceedling/generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ class Generator
:unity_utils


def setup()
# Alias
@helper = @generator_helper
end

def generate_mock(context:, mock:, test:, input_filepath:, output_path:)
arg_hash = {
:header_file => input_filepath,
Expand Down Expand Up @@ -299,42 +304,44 @@ def generate_test_results(tool:, context:, test_name:, test_filepath:, executabl
# Apply additional test case filters
command[:line] += @unity_utils.collect_test_runner_additional_args

# Enable collecting GCOV results even when segmenatation fault is appearing
# The gcda and gcno files will be generated for a test cases which doesn't
# cause segmentation fault
# Enable collecting GCOV results even for crashes
# The gcda and gcno files will be generated for test executable that doesn't cause a crash
@debugger_utils.enable_gcov_with_gdb_and_cmdargs(command)

# Run the test itself (allow it to fail. we'll analyze it in a moment)
# Run the test executable itself
# We allow it to fail without an exception.
# We'll analyze its results apart from tool_executor
command[:options][:boom] = false
shell_result = @tool_executor.exec( command )

# Handle SegFaults
if @tool_executor.segfault?( shell_result )
@loginator.log( "Test executable #{test_name} encountered a segmentation fault", Verbosity::OBNOXIOUS, LogLabels::SEGFAULT )
# Handle crashes
if @helper.test_crash?( shell_result )
@helper.log_test_results_crash( test_name, executable, shell_result )

if @configurator.project_config_hash[:project_use_backtrace] && @configurator.project_config_hash[:test_runner_cmdline_args]
# If we have the options and tools to learn more, dig into the details
shell_result = @debugger_utils.gdb_output_collector(shell_result)
shell_result = @debugger_utils.gdb_output_collector( shell_result )
else
# Otherwise, call a segfault a single failure so it shows up in the report
# Otherwise, call a crash a single failure so it shows up in the report
source = File.basename(executable).ext(@configurator.extension_source)
shell_result[:output] = "#{source}:1:test_Unknown:FAIL:Segmentation Fault"
shell_result[:output] = "#{source}:1:test_Unknown:FAIL:Test Executable Crashed"
shell_result[:output] += "\n-----------------------\n1 Tests 1 Failures 0 Ignored\nFAIL\n"
shell_result[:exit_code] = 1
end
else
# Don't Let The Failure Count Make Us Believe Things Aren't Working
@generator_helper.test_results_error_handler(executable, shell_result)
end

processed = @generator_test_results.process_and_write_results( shell_result,
arg_hash[:result_file],
@file_finder.find_test_from_file_path(arg_hash[:executable]) )
processed = @generator_test_results.process_and_write_results(
shell_result,
arg_hash[:result_file],
@file_finder.find_test_from_file_path(arg_hash[:executable])
)

arg_hash[:result_file] = processed[:result_file]
arg_hash[:results] = processed[:results]
arg_hash[:shell_result] = shell_result # for raw output display if no plugins for formatted display
# For raw output display if no plugins enabled for nice display
arg_hash[:shell_result] = shell_result

@plugin_manager.post_test_fixture_execute(arg_hash)
@plugin_manager.post_test_fixture_execute( arg_hash )
end

end
60 changes: 41 additions & 19 deletions lib/ceedling/generator_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,32 +13,54 @@ class GeneratorHelper

constructor :loginator

def test_crash?(shell_result)
return true if (shell_result[:output] =~ /\s*Segmentation\sfault.*/i)

def test_results_error_handler(executable, shell_result)
notice = ''
error = false

# Unix Signal 11 ==> SIGSEGV
# Applies to Unix-like systems including MSYS on Windows
return true if (shell_result[:status].termsig == 11)

# No test results found in test executable output
return true if (shell_result[:output] =~ TEST_STDOUT_STATISTICS_PATTERN).nil?

return false
end

def log_test_results_crash(test_name, executable, shell_result)
runner = File.basename(executable)

notice = "Test executable #{test_name} [`#{runner}`] seems to have crashed"
@loginator.log( notice, Verbosity::ERRORS, LogLabels::CRASH )

log = false

# Check for empty output
if (shell_result[:output].nil? or shell_result[:output].strip.empty?)
error = true
# mirror style of generic tool_executor failure output
notice = "Test executable \"#{File.basename(executable)}\" failed.\n" +
"> Produced no output to $stdout.\n"
# Mirror style of generic tool_executor failure output
notice = "Test executable `#{runner}` failed.\n" +
"> Produced no output\n"

log = true

# Check for no test results
elsif ((shell_result[:output] =~ TEST_STDOUT_STATISTICS_PATTERN).nil?)
error = true
# mirror style of generic tool_executor failure output
notice = "Test executable \"#{File.basename(executable)}\" failed.\n" +
"> Produced no final test result counts in $stdout:\n" +
"#{shell_result[:output].strip}\n"
# Mirror style of generic tool_executor failure output
notice = "Test executable `#{runner}` failed.\n" +
"> Output contains no test result counts\n"

log = true
end

if (error)
# since we told the tool executor to ignore the exit code, handle it explicitly here
notice += "> And exited with status: [#{shell_result[:exit_code]}] (count of failed tests).\n" if (shell_result[:exit_code] != nil)
notice += "> And then likely crashed.\n" if (shell_result[:exit_code] == nil)
if (log)
if (shell_result[:exit_code] != nil)
notice += "> And terminated with exit code: [#{shell_result[:exit_code]}] (failed test case count).\n"
end

notice += "> This is often a symptom of a bad memory access in source or test code.\n\n"
notice += "> Causes can include a bad memory access, stack overflow, heap error, or bad branch in source or test code.\n"

raise CeedlingException.new( notice )
@loginator.log( '', Verbosity::OBNOXIOUS )
@loginator.log( notice, Verbosity::OBNOXIOUS, LogLabels::ERROR )
@loginator.log( '', Verbosity::OBNOXIOUS )
end
end

Expand Down
4 changes: 3 additions & 1 deletion lib/ceedling/loginator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ def decorate(str, label=LogLabels::NONE)
prepend = '🧨 '
when LogLabels::CONSTRUCT
prepend = '🚧 '
when LogLabels::SEGFAULT
when LogLabels::CRASH
prepend = '☠️ '
when LogLabels::RUN
prepend = '👟 '
Expand Down Expand Up @@ -212,6 +212,8 @@ def format(string, verbosity, label, decorate)
prepend += 'ERROR: '
when LogLabels::EXCEPTION
prepend += 'EXCEPTION: '
when LogLabels::CRASH
prepend += 'ERROR: '
# Otherwise no headings for decorator-only messages
end

Expand Down
32 changes: 16 additions & 16 deletions spec/gcov/gcov_test_cases_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -206,19 +206,19 @@ def can_create_gcov_html_report_from_crashing_test_runner_with_enabled_debug_and
Dir.chdir @proj_name do
FileUtils.cp test_asset_path("example_file.h"), 'src/'
FileUtils.cp test_asset_path("example_file.c"), 'src/'
FileUtils.cp test_asset_path("test_example_file_sigsegv.c"), 'test/'
FileUtils.cp test_asset_path("test_example_file_crash.c"), 'test/'
FileUtils.cp test_asset_path("project_with_guts_gcov.yml"), 'project.yml'

@c.merge_project_yml_for_test({:project => { :use_backtrace => true },
:test_runner => { :cmdline_args => true }})

output = `bundle exec ruby -S ceedling gcov:all 2>&1`
expect($?.exitstatus).to match(1) # Test should fail as sigsegv is called
expect(output).to match(/Segmentation fault/i)
expect($?.exitstatus).to match(1) # Test should fail because of crash
expect(output).to match(/Test Executable Crashed/i)
expect(output).to match(/Unit test failures./)
expect(File.exist?('./build/gcov/results/test_example_file_sigsegv.fail'))
output_rd = File.read('./build/gcov/results/test_example_file_sigsegv.fail')
expect(output_rd =~ /test_add_numbers_will_fail \(\) at test\/test_example_file_sigsegv.c\:14/ )
expect(File.exist?('./build/gcov/results/test_example_file_crash.fail'))
output_rd = File.read('./build/gcov/results/test_example_file_crash.fail')
expect(output_rd =~ /test_add_numbers_will_fail \(\) at test\/test_example_file_crash.c\:14/ )
expect(output).to match(/TESTED:\s+2/)
expect(output).to match(/PASSED:\s+(?:0|1)/)
expect(output).to match(/FAILED:\s+(?:1|2)/)
Expand All @@ -237,19 +237,19 @@ def can_create_gcov_html_report_from_crashing_test_runner_with_enabled_debug_and
Dir.chdir @proj_name do
FileUtils.cp test_asset_path("example_file.h"), 'src/'
FileUtils.cp test_asset_path("example_file.c"), 'src/'
FileUtils.cp test_asset_path("test_example_file_sigsegv.c"), 'test/'
FileUtils.cp test_asset_path("test_example_file_crash.c"), 'test/'
FileUtils.cp test_asset_path("project_with_guts_gcov.yml"), 'project.yml'

@c.merge_project_yml_for_test({:project => { :use_backtrace => true },
:test_runner => { :cmdline_args => true }})

output = `bundle exec ruby -S ceedling gcov:all --exclude_test_case=test_add_numbers_adds_numbers 2>&1`
expect($?.exitstatus).to match(1) # Test should fail as sigsegv is called
expect(output).to match(/Segmentation fault/i)
expect($?.exitstatus).to match(1) # Test should fail because of crash
expect(output).to match(/Test Executable Crashed/i)
expect(output).to match(/Unit test failures./)
expect(File.exist?('./build/gcov/results/test_example_file_sigsegv.fail'))
output_rd = File.read('./build/gcov/results/test_example_file_sigsegv.fail')
expect(output_rd =~ /test_add_numbers_will_fail \(\) at test\/test_example_file_sigsegv.c\:14/ )
expect(File.exist?('./build/gcov/results/test_example_file_crash.fail'))
output_rd = File.read('./build/gcov/results/test_example_file_crash.fail')
expect(output_rd =~ /test_add_numbers_will_fail \(\) at test\/test_example_file_crash.c\:14/ )
expect(output).to match(/TESTED:\s+1/)
expect(output).to match(/PASSED:\s+0/)
expect(output).to match(/FAILED:\s+1/)
Expand All @@ -269,7 +269,7 @@ def can_create_gcov_html_report_from_test_runner_with_enabled_debug_and_cmd_args
Dir.chdir @proj_name do
FileUtils.cp test_asset_path("example_file.h"), 'src/'
FileUtils.cp test_asset_path("example_file.c"), 'src/'
FileUtils.cp test_asset_path("test_example_file_sigsegv.c"), 'test/'
FileUtils.cp test_asset_path("test_example_file_crash.c"), 'test/'
FileUtils.cp test_asset_path("project_with_guts_gcov.yml"), 'project.yml'

@c.merge_project_yml_for_test({:test_runner => { :cmdline_args => true }})
Expand All @@ -279,13 +279,13 @@ def can_create_gcov_html_report_from_test_runner_with_enabled_debug_and_cmd_args
" TEST_ASSERT_EQUAL_INT(0, difference_between_numbers(1,1));\n" \
"}\n"

updated_test_file = File.read('test/test_example_file_sigsegv.c').split("\n")
updated_test_file = File.read('test/test_example_file_crash.c').split("\n")
updated_test_file.insert(updated_test_file.length(), add_test_case)
File.write('test/test_example_file_sigsegv.c', updated_test_file.join("\n"), mode: 'w')
File.write('test/test_example_file_crash.c', updated_test_file.join("\n"), mode: 'w')

output = `bundle exec ruby -S ceedling gcov:all --exclude_test_case=test_add_numbers_will_fail 2>&1`
expect($?.exitstatus).to match(0)
expect(File.exist?('./build/gcov/results/test_example_file_sigsegv.pass'))
expect(File.exist?('./build/gcov/results/test_example_file_crash.pass'))
expect(output).to match(/TESTED:\s+2/)
expect(output).to match(/PASSED:\s+2/)
expect(output).to match(/FAILED:\s+0/)
Expand Down
Loading

0 comments on commit c32b506

Please sign in to comment.