Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ This changelog is complemented by three other documents:

## 💪 Fixed

- Fixed a crashed test case in a parameterized group occasionally being misattributed to a different, shorter-named test case in the same group whose name happened to be a substring of the one actually named in the crash backtrace.
- Fixed a test case's line number occasionally being misattributed to an unrelated helper function whose name merely contained the test's name as a substring, cascading incorrect line numbers to every test case after it in the same file.

### Partials

- [#1262](https://github.com/ThrowTheSwitch/Ceedling/issues/1262) Fixed a Partials-generated header occasionally concatenating two adjacent `#define` lines in one line causing a stray '#' compilation error. The triggering condition involved a `//` or `/* */` comment containing an apostrophe or quote (e.g. `// Don't ...`) that disrupted string literal handling.
Expand Down
21 changes: 11 additions & 10 deletions lib/ceedling/c_extractor/c_extractor_declarations.rb
Original file line number Diff line number Diff line change
Expand Up @@ -402,16 +402,17 @@ def extract_type(clean_text, name)
return $1.strip
end

# Find the name in the text and take everything before it (including pointer specifiers)
idx = text.rindex(name)
return nil if idx.nil?

type_part = text[0...idx]
# Include any pointer specifiers attached to the name
if text[idx..] =~ /^#{Regexp.escape(name)}/
# Look for * immediately before name (with optional space)
type_part = type_part.rstrip
end
# Find the name in the text and take everything before it (including pointer specifiers).
# A plain rindex(name) would find the last substring occurrence of `name` anywhere in
# text, even inside an unrelated longer identifier that merely ends with it (e.g. `name`
# "count" inside a type-side identifier like "recount") -- \b anchors this to a whole
# identifier occurrence. Greedy `.*` in a single match (not scan) naturally finds the
# rightmost such occurrence, since it consumes as much as possible before backtracking
# just enough to let the trailing \bname\b still match.
match = text.match(/\A(.*)\b#{Regexp.escape(name)}\b/m)
return nil if match.nil?

type_part = match[1]
type_part.strip
end

Expand Down
8 changes: 5 additions & 3 deletions lib/ceedling/generators/generator_test_results_backtrace.rb
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,11 @@ def do_gdb(filename, executable, shell_result, test_cases, context:)
# Prefer whichever unresolved member's own C symbol is actually named in the gdb
# backtrace (works regardless of position in the group); fall back to the first
# unresolved member if no member's symbol can be found in the transcript (e.g. a
# brief crash report with no frame information at all).
# brief crash report with no frame information at all). \b anchors the symbol so a
# shorter unresolved symbol that's merely a suffix of a longer one actually named in
# the frame (e.g. "foo" vs "my_foo") can't steal the attribution.
crashed_case = unresolved.find do |tc|
crash_result[:output].match?( /#{Regexp.escape(tc[:symbol])}\s*\(\)\sat/ )
crash_result[:output].match?( /\b#{Regexp.escape(tc[:symbol])}\s*\(\)\sat/ )
end
crashed_case ||= unresolved.first

Expand Down Expand Up @@ -458,7 +460,7 @@ def extract_simple_crash_output(output, filename)
output.lines.filter_map do |line|
line = line.strip
next if line.empty?
next if line =~ /^#{filename}.+:(PASS|FAIL|IGNORE)/
next if line =~ /^#{Regexp.escape(filename)}.+:(PASS|FAIL|IGNORE)/
line
end
end
Expand Down
6 changes: 5 additions & 1 deletion lib/ceedling/generators/generator_test_runner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,11 @@ def remap_line_numbers!(test_cases, original_file_contents)
break if remaining.empty?

next_case = remaining.first
if (line =~ /#{next_case[:test]}/)
# Escaped (a raw test name can contain regex metacharacters) and \b-anchored on both
# sides so an unrelated line that merely contains this test's name as a substring --
# e.g. a helper function like `reset_test_ab_state()` sitting before `test_ab` itself
# -- can't steal its line number and cascade misalignment to every case after it.
if (line =~ /\b#{Regexp.escape(next_case[:test])}\b/)
next_case[:line_number] = line_num
remaining.shift
end
Expand Down
16 changes: 16 additions & 0 deletions spec/units/c_extractor/c_extractor_declarations_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,22 @@ def check_single(variable, name:, type:, decorators: [], text:, array_suffix: ''
expect(rest).to eq("")
end

# #1266 follow-on: same unanchored-substring risk class as #1262/#1266. The type-side
# identifier "counter_t" itself contains "count" as a substring; a plain rindex(name)
# would still find the real trailing occurrence here (rindex searches right-to-left),
# but this pins down that the \b-anchored match extract_type now uses doesn't regress
# this ordinary case, where the type name happens to share a substring with the
# declared name.
it "extracts a variable whose type name contains the variable's own name as a substring" do
content = "counter_t count;"
success, variable, pos, rest = extract_variable.call(content)

expect(success).to be true
check_single(variable, name: 'count', type: 'counter_t', text: 'counter_t count;')
expect(pos).to eq(content.length)
expect(rest).to eq("")
end

it "extracts simple char variable" do
content = "char c;"
success, variable, pos, rest = extract_variable.call(content)
Expand Down
26 changes: 26 additions & 0 deletions spec/units/generators/generator_test_results_backtrace_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,32 @@
expect(crash_line).not_to include('failed to extract')
end

# #1266 follow-on: the same unanchored-substring risk class as #1262/#1266, here in the
# "prefer whichever unresolved member's own symbol is named in the backtrace" search --
# no \b before the escaped symbol, so a shorter unresolved symbol that's a suffix of a
# longer one actually named in the crash frame can false-match and steal the attribution.
it 'attributes a crash to the member whose symbol actually appears in the backtrace, not a shorter symbol that is merely its suffix' do
test_cases_suffix = [
{ test: 'test_x(1)', symbol: 'foo', line_number: 10 },
{ test: 'test_x(2)', symbol: 'my_foo', line_number: 20 }
]
filename_suffix = 'test_module.c'

crash_output = <<~GDB
Program received signal SIGSEGV, Segmentation fault.
0x00005618066ea1fb in my_foo () at test/test_module.c:42
#0 0x00005618066ea1fb in my_foo () at test/test_module.c:42
GDB

allow(@tool_executor).to receive(:exec)
.and_return({ output: crash_output, time: 0.5, exit_code: 139, stderr: '', status: @ok_status })

expect(@file_wrapper).to receive(:write)
.with('/build/logs/test/test_module/test_x(2).gdb.log', /=== test_x\(2\) ===/, 'a')

@backtrace.do_gdb( filename_suffix, executable, shell_result, test_cases_suffix, context: :test )
end

it 'handles a SIGABRT crash from assert() — shows assertion text, no source line' do
test_cases_assert = [{ test: 'test_asserting', symbol: 'test_asserting', line_number: 8 }]

Expand Down
23 changes: 23 additions & 0 deletions spec/units/generators/generator_test_runner_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,29 @@ def build_runner(test_file_contents:, preprocessed_file_contents: nil)

expect( test_cases.first[:line_number] ).to eq( 5 )
end

# #1266 follow-on: the same unanchored-substring risk class as #1262/#1266. A static
# helper function between two test cases whose name merely contains a later test's
# name as a substring (here "test_ab" inside "reset_test_ab_state") must not steal
# that test's line number, and an unescaped test name must not be treated as a regex.
it 'does not false-match a test name against an unrelated line that merely contains it as a substring' do
source = <<~SOURCE
void test_a(void) {}
void reset_test_ab_state(void) {}
void test_ab(void) {}
SOURCE

runner = build_runner( test_file_contents: source, preprocessed_file_contents: source )
test_cases = [
{ test: 'test_a', line_number: 0 },
{ test: 'test_ab', line_number: 0 }
]

runner.send( :remap_line_numbers!, test_cases, source )

expect( test_cases[0][:line_number] ).to eq( 1 )
expect( test_cases[1][:line_number] ).to eq( 3 )
end
end

describe '#initialize / #test_cases' do
Expand Down