ceedling 1.1.1 → 1.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/GIT_COMMIT_SHA +1 -1
  3. data/README.md +3 -3
  4. data/assets/broken_uncovered_file.c +13 -0
  5. data/assets/test_example_file_2.c +22 -0
  6. data/assets/test_example_file_crash_sigsegv_with_param.c +32 -0
  7. data/assets/tests_with_partials_coverage/src/blanks.c +10 -0
  8. data/assets/tests_with_partials_coverage/src/blanks.h +1 -0
  9. data/assets/tests_with_partials_coverage/src/counter.c +12 -0
  10. data/assets/tests_with_partials_coverage/src/counter.h +1 -0
  11. data/assets/tests_with_partials_coverage/src/decorators.c +11 -0
  12. data/assets/tests_with_partials_coverage/src/decorators.h +4 -0
  13. data/assets/tests_with_partials_coverage/test/test_blanks.c +8 -0
  14. data/assets/tests_with_partials_coverage/test/test_counter.c +9 -0
  15. data/assets/tests_with_partials_coverage/test/test_decorators.c +8 -0
  16. data/bin/actions_wrapper.rb +7 -6
  17. data/docs/Changelog.md +25 -3
  18. data/lib/ceedling/c_extractor/c_extractor_declarations.rb +3 -1
  19. data/lib/ceedling/c_extractor/c_extractor_preprocessing.rb +20 -7
  20. data/lib/ceedling/config/configurator.rb +7 -1
  21. data/lib/ceedling/defaults.rb +8 -2
  22. data/lib/ceedling/generators/generator_partials.rb +13 -3
  23. data/lib/ceedling/generators/generator_test_results_backtrace.rb +136 -68
  24. data/lib/ceedling/generators/generator_test_runner.rb +20 -2
  25. data/lib/ceedling/objects.yml +1 -0
  26. data/lib/ceedling/preprocess/preprocessinator_file_assembler.rb +10 -2
  27. data/lib/ceedling/preprocess/preprocessinator_reconstructor.rb +23 -15
  28. data/lib/version.rb +1 -1
  29. data/plugins/gcov/lib/gcov.rb +2 -2
  30. data/site-local/sitemap.xml.gz +0 -0
  31. data/spec/support/system/system_context.rb +15 -0
  32. data/spec/system/deployment_as_gem_spec.rb +2 -0
  33. data/spec/system/deployment_as_vendor_spec.rb +2 -0
  34. data/spec/system/gcov_deployment_spec.rb +20 -6
  35. data/spec/system/support/common_test_cases.rb +90 -0
  36. data/spec/system/support/gcov_common_test_cases.rb +31 -45
  37. data/spec/system/support/gcov_partials_test_cases.rb +116 -0
  38. data/spec/units/c_extractor/c_extractor_declarations_spec.rb +128 -0
  39. data/spec/units/c_extractor/c_extractor_definitions_spec.rb +7 -0
  40. data/spec/units/c_extractor/c_extractor_integration_spec.rb +17 -0
  41. data/spec/units/c_extractor/c_extractor_preprocessing_spec.rb +46 -0
  42. data/spec/units/configurator_spec.rb +45 -0
  43. data/spec/units/generators/generator_partials_spec.rb +32 -6
  44. data/spec/units/generators/generator_test_results_backtrace_spec.rb +49 -5
  45. data/spec/units/generators/generator_test_runner_spec.rb +26 -2
  46. data/spec/units/preprocess/preprocessinator_file_assembler_spec.rb +96 -2
  47. data/spec/units/preprocess/preprocessinator_reconstructor_spec.rb +151 -1
  48. data/spec/units/test_context_extractor_spec.rb +3 -3
  49. metadata +16 -2
@@ -13,7 +13,8 @@ class GeneratorTestResultsBacktrace
13
13
  @RESULTS_COLLECTOR = Struct.new( :passed, :failed, :ignored, :output, keyword_init:true )
14
14
  end
15
15
 
16
- # Re-runs each test case under gdb to identify which ones crashed and why.
16
+ # Re-runs each test case (or, for a parameterized test, each group of parameterized
17
+ # cases -- see `group_test_cases`) under gdb to identify which one(s) crashed and why.
17
18
  # Writes the full gdb transcript to a per-test-case log file and assembles a
18
19
  # terse crash label (signal + description, optional source line in backticks)
19
20
  # for each failing test case. Returns a modified shell_result with regenerated output.
@@ -28,54 +29,83 @@ class GeneratorTestResultsBacktrace
28
29
 
29
30
  test_name = File.basename( filename, '.*' )
30
31
 
31
- # Iterate on test cases
32
- test_cases.each do |test_case|
33
- # Per-test-case log file: <log_path>/<context>/<test_name>/<test_case>.gdb.log
34
- log_path = @file_path_utils.form_test_gdb_log( test_name, context: context, name: test_case[:test] )
35
- @file_wrapper.mkdir( File.dirname( log_path ) )
36
-
37
- # Build the test fixture to run with our test case of interest
32
+ # Iterate on test cases, one sub-process run per group (see `group_test_cases`)
33
+ group_test_cases( test_cases ).each do |group|
34
+ # Build the test fixture to run with our test case (or parameterized group) of interest
38
35
  command = @tool_executor.build_command_line(
39
36
  @configurator.tools_test_backtrace_gdb, [],
40
37
  gdb_script_filepath,
41
38
  executable,
42
- test_case[:test]
39
+ unity_filter_arg( group )
43
40
  )
44
41
  # Things are gonna go boom, so ignore booms to get output
45
42
  command[:options][:boom] = false
46
43
 
47
44
  crash_result = @tool_executor.exec( command )
48
45
 
49
- # Sum execution time for each test case
46
+ # Sum execution time for each sub-process run
50
47
  # Note: Running tests separately increases total execution time
51
48
  shell_result[:time] += crash_result[:time].to_f()
52
49
 
53
- test_output = ''
50
+ unresolved = []
54
51
 
55
- # Process single test case stats
56
- case crash_result[:output]
57
- # Success test case
58
- when /(^#{filename}.+:PASS\s*$)/
59
- test_case_results[:passed] += 1
60
- test_output = $1 # Grab regex match
52
+ # Attribute each group member its own real result line, if Unity printed one
53
+ group.each do |test_case|
54
+ case crash_result[:output]
55
+ # Success test case
56
+ when /(^#{Regexp.escape(filename)}:\d+:#{Regexp.escape(test_case[:test])}:PASS\s*$)/
57
+ test_case_results[:passed] += 1
58
+ test_case_results[:output] << $1
61
59
 
62
- # Ignored test case
63
- when /(^#{filename}.+:IGNORE\s*$)/
64
- test_case_results[:ignored] += 1
65
- test_output = $1 # Grab regex match
60
+ # Ignored test case
61
+ when /(^#{Regexp.escape(filename)}:\d+:#{Regexp.escape(test_case[:test])}:IGNORE\s*$)/
62
+ test_case_results[:ignored] += 1
63
+ test_case_results[:output] << $1
66
64
 
67
- when /(^#{filename}.+:FAIL(:.+)?\s*$)/
68
- test_case_results[:failed] += 1
69
- test_output = $1 # Grab regex match
65
+ when /(^#{Regexp.escape(filename)}:\d+:#{Regexp.escape(test_case[:test])}:FAIL(:.+)?\s*$)/
66
+ test_case_results[:failed] += 1
67
+ test_case_results[:output] << $1
70
68
 
71
- else # Crash failure case
72
- test_case_results[:failed] += 1
69
+ # No result line for this member -- either it crashed, or it never got to run
70
+ # because an earlier member in this same group crashed. Resolved below.
71
+ else
72
+ unresolved << test_case
73
+ end
74
+ end
73
75
 
74
- # Append full gdb output for this test case to the log
75
- @file_wrapper.write( log_path, "=== #{test_case[:test]} ===\n#{crash_result[:output]}\n", 'a' )
76
+ next if unresolved.empty?
76
77
 
77
- # Collect file_name and line in which crash occurred
78
- matched = crash_result[:output].match( /#{test_case[:test]}\s*\(\)\sat.+#{filename}:(\d+)\n/ )
78
+ # Prefer whichever unresolved member's own C symbol is actually named in the gdb
79
+ # backtrace (works regardless of position in the group); fall back to the first
80
+ # unresolved member if no member's symbol can be found in the transcript (e.g. a
81
+ # brief crash report with no frame information at all).
82
+ crashed_case = unresolved.find do |tc|
83
+ crash_result[:output].match?( /#{Regexp.escape(tc[:symbol])}\s*\(\)\sat/ )
84
+ end
85
+ crashed_case ||= unresolved.first
86
+
87
+ # Per-test-case log file: <log_path>/<context>/<test_name>/<test_case>.gdb.log
88
+ log_path = @file_path_utils.form_test_gdb_log( test_name, context: context, name: crashed_case[:test] )
89
+ @file_wrapper.mkdir( File.dirname( log_path ) )
90
+ @file_wrapper.write( log_path, "=== #{crashed_case[:test]} ===\n#{crash_result[:output]}\n", 'a' )
91
+
92
+ unresolved.each do |test_case|
93
+ test_case_results[:failed] += 1
94
+
95
+ if !test_case.equal?( crashed_case )
96
+ # An earlier case in this same parameterized group already crashed the
97
+ # process -- this member never got a chance to run.
98
+ test_case_results[:output] <<
99
+ "#{filename}:#{test_case[:line_number]}:#{test_case[:test]}:FAIL: " \
100
+ "Test case not run -- an earlier case in this parameterized test group crashed"
101
+ next
102
+ end
103
+
104
+ # Collect file_name and line in which crash occurred.
105
+ # Match against the actual C symbol (`:symbol`), not the human-facing test name
106
+ # (`:test`): a parameterized test case crashes inside a generated wrapper function
107
+ # (`runner_args<N>_<test>`), not a function literally named `<test>(<args>)`.
108
+ matched = crash_result[:output].match( /#{Regexp.escape(test_case[:symbol])}\s*\(\)\sat.+#{filename}:(\d+)\n/ )
79
109
 
80
110
  # If we found an error report line containing `test_case() at filename.c:###` in `gdb` output
81
111
  if matched
@@ -86,7 +116,7 @@ class GeneratorTestResultsBacktrace
86
116
  signal_label = format_signal_label( crash_result[:output] )
87
117
 
88
118
  # Extract the offending source line (nil for assertion crashes or when unavailable)
89
- source_line = extract_source_line( crash_result[:output], test_case[:test], filename )
119
+ source_line = extract_source_line( crash_result[:output], test_case[:symbol], filename )
90
120
 
91
121
  # Unity's test executable output is line oriented.
92
122
  # Multi-line output is not possible (it looks like random `printf()` statements to the results parser).
@@ -94,7 +124,7 @@ class GeneratorTestResultsBacktrace
94
124
  crash_detail = source_line ? "#{NEWLINE_TOKEN}`#{source_line}`" : ''
95
125
 
96
126
  # Log path appears on its own encoded line so the results parser treats it separately
97
- test_output =
127
+ test_case_results[:output] <<
98
128
  "#{filename}:#{line_number}:#{test_case[:test]}:FAIL: Test case crashed" \
99
129
  " >> #{signal_label}" \
100
130
  "#{crash_detail}" \
@@ -106,20 +136,18 @@ class GeneratorTestResultsBacktrace
106
136
  label = format_signal_label( crash_result[:output] )
107
137
 
108
138
  if !label.empty?
109
- test_output =
139
+ test_case_results[:output] <<
110
140
  "#{filename}:#{test_case[:line_number]}:#{test_case[:test]}:FAIL: Test case crashed" \
111
141
  " >> #{label}" \
112
142
  "#{NEWLINE_TOKEN}(#{log_path})"
113
143
  else
114
- test_output =
144
+ test_case_results[:output] <<
115
145
  "#{filename}:#{test_case[:line_number]}:#{test_case[:test]}:FAIL: " \
116
146
  "Test case crashed (failed to extract `gdb` report)" \
117
147
  "#{NEWLINE_TOKEN}(#{log_path})"
118
148
  end
119
149
  end
120
150
  end
121
-
122
- test_case_results[:output] << test_output
123
151
  end
124
152
 
125
153
  # Reset shell result exit code and output
@@ -135,7 +163,8 @@ class GeneratorTestResultsBacktrace
135
163
  return shell_result
136
164
  end
137
165
 
138
- # Re-runs each test case individually to determine which ones crashed.
166
+ # Re-runs each test case (or, for a parameterized test, each group of parameterized
167
+ # cases -- see `group_test_cases`) individually to determine which one(s) crashed.
139
168
  # For crash cases, captures any extra output from the test binary (e.g.
140
169
  # assertion messages on stderr) and includes it in the failure report.
141
170
  # Returns a modified shell_result with regenerated output.
@@ -146,50 +175,61 @@ class GeneratorTestResultsBacktrace
146
175
  # Reset time
147
176
  shell_result[:time] = 0
148
177
 
149
- # Iterate on test cases
150
- test_cases.each do |test_case|
151
- # Build the test fixture to run with our test case of interest
178
+ # Iterate on test cases, one sub-process run per group (see `group_test_cases`)
179
+ group_test_cases( test_cases ).each do |group|
180
+ # Build the test fixture to run with our test case (or parameterized group) of interest
152
181
  command = @tool_executor.build_command_line(
153
182
  @configurator.tools_test_fixture_simple_backtrace, [],
154
183
  executable,
155
- test_case[:test]
184
+ unity_filter_arg( group )
156
185
  )
157
186
  # Things are gonna go boom, so ignore booms to get output
158
187
  command[:options][:boom] = false
159
188
 
160
189
  crash_result = @tool_executor.exec( command )
161
190
 
162
- # Sum execution time for each test case
191
+ # Sum execution time for each sub-process run
163
192
  # Note: Running tests separately increases total execution time
164
193
  shell_result[:time] += crash_result[:time].to_f()
165
194
 
166
- # Process single test case stats
167
- case crash_result[:output]
168
- # Success test case
169
- when /(^#{filename}.+:PASS\s*$)/
170
- test_case_results[:passed] += 1
171
- test_output = $1 # Grab regex match
172
-
173
- # Ignored test case
174
- when /(^#{filename}.+:IGNORE\s*$)/
175
- test_case_results[:ignored] += 1
176
- test_output = $1 # Grab regex match
177
-
178
- when /(^#{filename}.+:FAIL(:.+)?\s*$)/
179
- test_case_results[:failed] += 1
180
- test_output = $1 # Grab regex match
181
-
182
- else # Crash failure case
183
- test_case_results[:failed] += 1
184
-
185
- # Collect any non-result, non-blank lines (e.g. assertion messages on stderr)
186
- extra = extract_simple_crash_output( crash_result[:output], filename )
187
- test_output = "#{filename}:#{test_case[:line_number]}:#{test_case[:test]}:FAIL: Test case crashed"
188
- test_output += " >> #{extra.join(NEWLINE_TOKEN)}" unless extra.empty?
189
- end
195
+ crashed = false # Has the actual crash in this group already been attributed?
196
+
197
+ # Attribute each group member its own real result line, if Unity printed one
198
+ group.each do |test_case|
199
+ case crash_result[:output]
200
+ # Success test case
201
+ when /(^#{Regexp.escape(filename)}:\d+:#{Regexp.escape(test_case[:test])}:PASS\s*$)/
202
+ test_case_results[:passed] += 1
203
+ test_case_results[:output] << $1
190
204
 
191
- # Collect up real and stand-in test results output
192
- test_case_results[:output] << test_output
205
+ # Ignored test case
206
+ when /(^#{Regexp.escape(filename)}:\d+:#{Regexp.escape(test_case[:test])}:IGNORE\s*$)/
207
+ test_case_results[:ignored] += 1
208
+ test_case_results[:output] << $1
209
+
210
+ when /(^#{Regexp.escape(filename)}:\d+:#{Regexp.escape(test_case[:test])}:FAIL(:.+)?\s*$)/
211
+ test_case_results[:failed] += 1
212
+ test_case_results[:output] << $1
213
+
214
+ # No result line for this member -- either it crashed, or it never got to run
215
+ # because an earlier member in this same group crashed.
216
+ else
217
+ test_case_results[:failed] += 1
218
+
219
+ if crashed
220
+ test_case_results[:output] <<
221
+ "#{filename}:#{test_case[:line_number]}:#{test_case[:test]}:FAIL: " \
222
+ "Test case not run -- an earlier case in this parameterized test group crashed"
223
+ else
224
+ crashed = true
225
+ # Collect any non-result, non-blank lines (e.g. assertion messages on stderr)
226
+ extra = extract_simple_crash_output( crash_result[:output], filename )
227
+ test_output = "#{filename}:#{test_case[:line_number]}:#{test_case[:test]}:FAIL: Test case crashed"
228
+ test_output += " >> #{extra.join(NEWLINE_TOKEN)}" unless extra.empty?
229
+ test_case_results[:output] << test_output
230
+ end
231
+ end
232
+ end
193
233
  end
194
234
 
195
235
  # Reset shell result exit code and output
@@ -208,6 +248,34 @@ class GeneratorTestResultsBacktrace
208
248
  ### Private ###
209
249
  private
210
250
 
251
+ # Groups test cases so each parameterized test's cases are isolated together as one
252
+ # sub-process run instead of one run per case. Unity's `-n`/`-f` command-line filter
253
+ # parser treats a comma as a separator between multiple OR'd filter clauses, so an
254
+ # exact filter can never match a parameterized case's runtime name once it has more
255
+ # than one argument (e.g. `name(5, 3, 2)`). Grouping by base name and matching each
256
+ # member's own result line out of the group's single shared sub-run output sidesteps
257
+ # that limitation entirely. Non-parameterized test cases are unaffected: each forms
258
+ # its own single-member group, isolated exactly as before.
259
+ def group_test_cases(test_cases)
260
+ test_cases.group_by { |test_case| test_case[:test].sub(/\(.*\)\z/, '') }.values
261
+ end
262
+
263
+ # Builds the Unity command-line filter argument for isolating one group (see
264
+ # `group_test_cases`). A non-parameterized (single-member, no-args) group is isolated
265
+ # with an exact-match filter as before. A parameterized group is isolated with a
266
+ # non-strict prefix filter on its shared base name -- the only reliable way to select
267
+ # such a group, since its members' runtime names contain commas.
268
+ def unity_filter_arg(group)
269
+ test_name = group.first[:test]
270
+
271
+ if test_name.include?('(')
272
+ base_name = test_name.sub(/\(.*\)\z/, '')
273
+ %(-f "#{base_name}")
274
+ else
275
+ %(-n "#{test_name}")
276
+ end
277
+ end
278
+
211
279
  # Builds a terse crash label from gdb output.
212
280
  # Rules:
213
281
  # - Named signal explicitly in output → "[SIGNAL] Description"
@@ -62,8 +62,26 @@ class GeneratorTestRunner
62
62
  # Unity's runner generator `find_tests()` produces an array of hashes with the following keys...
63
63
  # { test:, args:, call:, params:, line_number: }
64
64
 
65
- # For external use of test case names and line numbers, keep only those pieces of info
66
- @test_cases = @test_cases_internal.map {|hash| hash.slice( :test, :line_number )}
65
+ # For external use, reduce down to test name, runtime C symbol, and line number.
66
+ # A parameterized test (`:args` populated) is expanded into one entry per TEST_CASE /
67
+ # TEST_RANGE / TEST_MATRIX row, since Unity's generated runner registers each invocation
68
+ # under its own name (matching `generate_test_runner.rb`'s `"#{test}(#{args})"` naming) and
69
+ # runs it through its own wrapper function (`runner_args<N>_<test>`). Collapsing these to a
70
+ # single bare-name entry breaks exact-match test case isolation (crash handling) for
71
+ # parameterized tests.
72
+ @test_cases = @test_cases_internal.flat_map do |hash|
73
+ if hash[:args].nil? || hash[:args].empty?
74
+ [ { test: hash[:test], symbol: hash[:test], line_number: hash[:line_number] } ]
75
+ else
76
+ hash[:args].each_with_index.map do |args, idx|
77
+ {
78
+ test: "#{hash[:test]}(#{args})",
79
+ symbol: "runner_args#{idx + 1}_#{hash[:test]}",
80
+ line_number: hash[:line_number]
81
+ }
82
+ end
83
+ end
84
+ end
67
85
  end
68
86
 
69
87
  def extract_test_cases(source_contents)
@@ -288,6 +288,7 @@ preprocessinator_comment_stripper:
288
288
  preprocessinator_reconstructor:
289
289
  compose:
290
290
  - parsing_parcels
291
+ - file_wrapper
291
292
 
292
293
  preprocessinator:
293
294
  compose:
@@ -217,7 +217,11 @@ class PreprocessinatorFileAssembler
217
217
 
218
218
  # Write contents of final preprocessed file a line at a time
219
219
  # ----------------------------------------------------------
220
- @file_wrapper.open( preprocessed_filepath, 'w' ) do |file|
220
+ # Binary mode: `contents` lines below come from an upstream preprocessed
221
+ # file and may already carry their own line endings. Windows text mode
222
+ # rewrites "\n" on write, which would alter a line ending already present
223
+ # in that content instead of passing it through unchanged.
224
+ @file_wrapper.open( preprocessed_filepath, 'wb' ) do |file|
221
225
  # Add include guards and extra blank lines to beginning of file contents
222
226
  file << "#ifndef #{guardname}\n"
223
227
  file << "#define #{guardname}\n\n"
@@ -361,7 +365,11 @@ class PreprocessinatorFileAssembler
361
365
  def assemble_preprocessed_code_file(filename:, preprocessed_filepath:, contents:, extras:, includes:)
362
366
  # Write contents of final preprocessed file a line at a time
363
367
  # ----------------------------------------------------------
364
- @file_wrapper.open( preprocessed_filepath, 'w' ) do |file|
368
+ # Binary mode: `contents` lines below come from an upstream preprocessed
369
+ # file and may already carry their own line endings. Windows text mode
370
+ # rewrites "\n" on write, which would alter a line ending already present
371
+ # in that content instead of passing it through unchanged.
372
+ @file_wrapper.open( preprocessed_filepath, 'wb' ) do |file|
365
373
  # Reinsert #include statements into stripped down file
366
374
  # Rely on Include object stringification for formatting of incudes
367
375
  includes.each { |include| file << "#{include}\n" }
@@ -9,9 +9,9 @@ require 'ceedling/constants'
9
9
  require 'ceedling/encodinator'
10
10
  require 'ceedling/parsing_parcels'
11
11
 
12
- class PreprocessinatorReconstructor
13
-
14
- constructor :parsing_parcels
12
+ class PreprocessinatorReconstructor
13
+
14
+ constructor :parsing_parcels, :file_wrapper
15
15
 
16
16
  ##
17
17
  ## Preprocessor Expansion Output Handling
@@ -104,10 +104,13 @@ class PreprocessinatorReconstructor
104
104
  # Opens `input_filepath` for reading and `output_filepath` for writing,
105
105
  # then delegates to `compact_from_expansion` with the resulting IO objects.
106
106
  def compact_file_from_expansion(input_filepath:, source_filepath:, output_filepath:)
107
- # Open input in binary mode: GCC output under non-C locale contains non-ASCII bytes
108
- # (localized markers). Per-line clean_encoding in _scan_expansion_for_file handles content.
109
- File.open( input_filepath, 'rb' ) do |input|
110
- File.open( output_filepath, 'w' ) do |output|
107
+ # Binary mode on both ends: GCC output under non-C locale contains non-ASCII
108
+ # bytes (localized markers; per-line clean_encoding in _scan_expansion_for_file
109
+ # handles content), and the compacted output is read again downstream by code
110
+ # that depends on its line count being exact. Windows text mode would rewrite
111
+ # "\n" to "\r\n" on write, which this content must not be subject to.
112
+ @file_wrapper.open( input_filepath, 'rb' ) do |input|
113
+ @file_wrapper.open( output_filepath, 'wb' ) do |output|
111
114
  compact_from_expansion( input: input, filepath: source_filepath, output: output )
112
115
  end
113
116
  end
@@ -280,16 +283,21 @@ class PreprocessinatorReconstructor
280
283
 
281
284
  # Buffer for the last logical line (may still receive aggregated content)
282
285
  pending_line = nil
283
- # Whether a blank line should follow pending_line when flushed
284
- pending_blank = false
285
-
286
- # Yields pending_line (and optional trailing blank) then clears the buffer
286
+ # Count of blank lines to emit after pending_line when flushed. Tracked as
287
+ # an exact count, not just "was there a blank" -- callers that depend on
288
+ # this method for line-for-line fidelity with the original source (so
289
+ # that unrelated line-number math downstream stays correct) need every
290
+ # blank line preserved, including runs that used to be a multi-line
291
+ # comment before an earlier pass turned it into equivalent blank lines.
292
+ pending_blank_count = 0
293
+
294
+ # Yields pending_line (and any pending blank lines) then clears the buffer
287
295
  flush = lambda do
288
296
  unless pending_line.nil?
289
297
  block.call( pending_line )
290
- block.call( '' ) if pending_blank
298
+ pending_blank_count.times { block.call( '' ) }
291
299
  pending_line = nil
292
- pending_blank = false
300
+ pending_blank_count = 0
293
301
  end
294
302
  end
295
303
 
@@ -308,9 +316,9 @@ class PreprocessinatorReconstructor
308
316
  # Strip a line so we can omit useless blank lines
309
317
  _line = line.strip()
310
318
 
311
- # Skip processing blank lines, but mark a pending blank unless we already have one
319
+ # Skip processing blank lines, but track an exact count of them so runs of blanks survive intact
312
320
  if _line.empty?
313
- pending_blank = true if !pending_line.nil? && !pending_line.empty?
321
+ pending_blank_count += 1 if !pending_line.nil? && !pending_line.empty?
314
322
  next
315
323
  end
316
324
 
data/lib/version.rb CHANGED
@@ -15,7 +15,7 @@
15
15
  module Ceedling
16
16
  module Version
17
17
  # Convenience constants for gem building, etc.
18
- GEM = '1.1.1'
18
+ GEM = '1.1.3'
19
19
  TAG = GEM
20
20
 
21
21
  # If run as a script print Ceedling's version to $stdout
@@ -354,8 +354,8 @@ class Gcov < Plugin
354
354
 
355
355
  shell_result = @tool_executor.exec( command )
356
356
 
357
- # First line of gcc --version: "gcc (...platform info...) major.minor.patch"
358
- version_match = shell_result[:output].match(/^gcc\s+.*\s+(\d+)\.(\d+)\.\d+/)
357
+ # First line of gcc --version: "gcc[.exe] (...platform info...) major.minor.patch"
358
+ version_match = shell_result[:output].match(/^gcc(?:#{Regexp.escape(EXTENSION_WIN_EXE)})?\s+.*\s+(\d+)\.(\d+)\.\d+/)
359
359
 
360
360
  if version_match.nil? || version_match[1].nil? || version_match[2].nil?
361
361
  raise CeedlingException.new("Could not collect `gcc` version from its command line")
Binary file
@@ -279,6 +279,21 @@ class SystemContext
279
279
  sections << stderr.strip
280
280
  end
281
281
 
282
+ # Loginator routes `Verbosity::DEBUG` messages (including the backtrace
283
+ # `log_debug_backtrace` emits for a caught exception) to stdout rather than
284
+ # stderr, and this method's stdout scan above only pulls lines containing
285
+ # the literal substrings 'ERROR'/'EXCEPTION' -- a bare backtrace line
286
+ # (file:line:in `method') contains neither, so without surfacing it here
287
+ # explicitly, the one piece of stdout most useful for diagnosing *why* a
288
+ # command failed would otherwise only ever reach the full raw-output log
289
+ # file, not this console-visible summary (what CI output actually shows).
290
+ label = 'Debug Backtrace ==>'
291
+ backtrace_block = extract_section(stdout, label)
292
+ unless backtrace_block.empty?
293
+ sections << ">> DEBUG BACKTRACE"
294
+ sections.concat(backtrace_block)
295
+ end
296
+
282
297
  label = 'FAILED TEST SUMMARY'
283
298
  failed_block = extract_section(stdout, label)
284
299
  unless failed_block.empty?
@@ -91,6 +91,7 @@ ceedling_system_tests do
91
91
  test_case :crash_none_writes_fail_results_file
92
92
  test_case :crash_simple_sigsegv_all_test_cases
93
93
  test_case :crash_simple_sigabrt_assert_failure
94
+ test_case :crash_simple_sigsegv_with_parameterized_test
94
95
  end
95
96
 
96
97
  describe "Backtrace with GDB" do
@@ -100,6 +101,7 @@ ceedling_system_tests do
100
101
  test_case :crash_gdb_sigsegv_targets_test_case_filter
101
102
  test_case :crash_gdb_sigsegv_excludes_test_case_filter
102
103
  test_case :crash_gdb_sigabrt_assert_failure
104
+ test_case :crash_gdb_sigsegv_with_parameterized_test
103
105
  end
104
106
 
105
107
  describe "Test filtering" do
@@ -94,6 +94,7 @@ ceedling_system_tests do
94
94
  test_case :crash_none_writes_fail_results_file
95
95
  test_case :crash_simple_sigsegv_all_test_cases
96
96
  test_case :crash_simple_sigabrt_assert_failure
97
+ test_case :crash_simple_sigsegv_with_parameterized_test
97
98
  end
98
99
 
99
100
  describe "Backtrace with GDB" do
@@ -103,6 +104,7 @@ ceedling_system_tests do
103
104
  test_case :crash_gdb_sigsegv_targets_test_case_filter
104
105
  test_case :crash_gdb_sigsegv_excludes_test_case_filter
105
106
  test_case :crash_gdb_sigabrt_assert_failure
107
+ test_case :crash_gdb_sigsegv_with_parameterized_test
106
108
  end
107
109
 
108
110
  describe "Test filtering" do
@@ -7,11 +7,13 @@
7
7
 
8
8
  require 'spec_system_helper'
9
9
  require_relative 'support/gcov_common_test_cases'
10
+ require_relative 'support/gcov_partials_test_cases'
10
11
 
11
12
  ceedling_system_tests do
12
13
  describe "Gcov" do
13
14
  include CommonSystemTestCases
14
15
  include GcovCommonTestCases
16
+ include GcovPartialsTestCases
15
17
  before :all do
16
18
  determine_reports_to_test
17
19
  @c = SystemContext.new
@@ -50,6 +52,18 @@ ceedling_system_tests do
50
52
  test_case :create_html_report_100_coverage_excluding_crashing_test_case
51
53
  end
52
54
 
55
+ describe "Coverage reporting with Partials" do
56
+ before do
57
+ @c.with_context do
58
+ @c.ceedling_appcmd_exec("new --local #{@proj_name}")
59
+ end
60
+ end
61
+
62
+ test_case :gcov_partials_coverage_blank_lines_in_function_body
63
+ test_case :gcov_partials_coverage_decorators_on_own_lines
64
+ test_case :gcov_partials_coverage_function_scope_static_promotion
65
+ end
66
+
53
67
  describe "Backtrace with GDB" do
54
68
  include_context "requires gdb"
55
69
 
@@ -78,7 +92,7 @@ ceedling_system_tests do
78
92
  # than resetting, crashing the test executable before Unity can print
79
93
  # its statistics line.
80
94
  FileUtils.rm_rf('temp_sensor')
81
- output = `bundle exec ruby -S ceedling example temp_sensor 2>&1`
95
+ output = @c.ceedling_appcmd_exec("example temp_sensor")
82
96
  expect(output).to match(/created/)
83
97
  end
84
98
  end
@@ -86,7 +100,7 @@ ceedling_system_tests do
86
100
  it "should be testable" do
87
101
  @c.with_context do
88
102
  Dir.chdir "temp_sensor" do
89
- @output = `bundle exec ruby -S ceedling --mixin=add_gcov gcov:all 2>&1`
103
+ @output = @c.ceedling_build_exec("gcov:all --mixin=add_gcov")
90
104
  # Validate full test suite results
91
105
  expect(@output).to match(/TESTED:\s+86/)
92
106
  expect(@output).to match(/PASSED:\s+86/)
@@ -107,7 +121,7 @@ ceedling_system_tests do
107
121
  it "should be able to test a single module (it should INHERIT file-specific flags)" do
108
122
  @c.with_context do
109
123
  Dir.chdir "temp_sensor" do
110
- @output = `bundle exec ruby -S ceedling --mixin=add_gcov gcov:TemperatureCalculator 2>&1`
124
+ @output = @c.ceedling_build_exec("gcov:TemperatureCalculator --mixin=add_gcov")
111
125
  expect(@output).to match(/TESTED:\s+2/)
112
126
  expect(@output).to match(/PASSED:\s+2/)
113
127
 
@@ -119,7 +133,7 @@ ceedling_system_tests do
119
133
  it "should be able to test multiple files matching a pattern" do
120
134
  @c.with_context do
121
135
  Dir.chdir "temp_sensor" do
122
- @output = `bundle exec ruby -S ceedling --mixin=add_gcov gcov:pattern[Temp] 2>&1`
136
+ @output = @c.ceedling_build_exec("gcov:pattern[Temp] --mixin=add_gcov")
123
137
  expect(@output).to match(/TESTED:\s+6/)
124
138
  expect(@output).to match(/PASSED:\s+6/)
125
139
 
@@ -132,7 +146,7 @@ ceedling_system_tests do
132
146
  it "should be able to test all files matching in a path" do
133
147
  @c.with_context do
134
148
  Dir.chdir "temp_sensor" do
135
- @output = `bundle exec ruby -S ceedling --mixin=add_gcov gcov:path[adc] 2>&1`
149
+ @output = @c.ceedling_build_exec("gcov:path[adc] --mixin=add_gcov")
136
150
  expect(@output).to match(/TESTED:\s+24/)
137
151
  expect(@output).to match(/PASSED:\s+24/)
138
152
 
@@ -147,7 +161,7 @@ ceedling_system_tests do
147
161
  it "should be able to test specific test cases in a file" do
148
162
  @c.with_context do
149
163
  Dir.chdir "temp_sensor" do
150
- @output = `bundle exec ruby -S ceedling --mixin=add_gcov gcov:path[adc] --test-case="RunShouldNot" 2>&1`
164
+ @output = @c.ceedling_build_exec('gcov:path[adc] --mixin=add_gcov --test-case="RunShouldNot"')
151
165
  expect(@output).to match(/TESTED:\s+2/)
152
166
  expect(@output).to match(/PASSED:\s+2/)
153
167