|
16 | 16 | require 'reportgenerator_reportinator' |
17 | 17 |
|
18 | 18 | class Gcov < Plugin |
19 | | - |
| 19 | + |
| 20 | + include ToolVersionGating |
| 21 | + |
| 22 | + # Checked lazily (see validate_mcdc_gcc_version!, called from the gcov-context build |
| 23 | + # hooks below) rather than here in setup(), which runs for every build this plugin is |
| 24 | + # merely *enabled* for -- not only real `gcov:` builds. Shelling out to `gcc --version` |
| 25 | + # here would make GCC-version validation (and a build-breaking exception on too-old a |
| 26 | + # GCC) fire even for builds that never touch :mcdc at all, e.g. plain `test:all`. |
| 27 | + MCDC_GCC_VERSION_GATE = { |
| 28 | + :mcdc => { |
| 29 | + min: [14, 0], |
| 30 | + message: ":gcov ↳ :mcdc ➡️ Modified condition/decision coverage requires GCC %{req} or higher (found %{found})" |
| 31 | + } |
| 32 | + }.freeze |
| 33 | + |
20 | 34 | # `Plugin` setup() |
21 | 35 | def setup |
22 | 36 | @result_list = [] |
@@ -80,16 +94,9 @@ def setup |
80 | 94 |
|
81 | 95 | @mutex = Mutex.new() |
82 | 96 |
|
83 | | - # Validate MC/DC configuration against GCC version (only incurs gcc --version when :mcdc: TRUE) |
84 | | - if @project_config[:gcov_mcdc] |
85 | | - gcc_version = get_gcc_version() |
86 | | - if gcc_version.major < 14 |
87 | | - raise CeedlingException.new( |
88 | | - ":gcov ↳ :mcdc ➡️ Modified condition/decision coverage requires GCC 14 or higher " \ |
89 | | - "(found #{gcc_version.major}.#{gcc_version.minor})" |
90 | | - ) |
91 | | - end |
92 | | - end |
| 97 | + # See validate_mcdc_gcc_version! and MCDC_GCC_VERSION_GATE above for why this isn't |
| 98 | + # checked eagerly here. |
| 99 | + @mcdc_gcc_checked = false |
93 | 100 | end |
94 | 101 |
|
95 | 102 | # Called within class and also externally by plugin Rakefile |
@@ -138,6 +145,8 @@ def process_untested_sources(sources:, guidance: true) |
138 | 145 | @loginator.log_list( untested_sources.sort, header, Verbosity::COMPLAIN, LogLabels::WARNING ) |
139 | 146 |
|
140 | 147 | when GCOV_UNTESTED_SOURCES_COMPILE |
| 148 | + validate_mcdc_gcc_version! |
| 149 | + |
141 | 150 | msg = 'Processing Untested Sources' |
142 | 151 | msg = @reportinator.generate_heading( @loginator.decorate( msg, LogLabels::RUN ) ) |
143 | 152 | @loginator.log( msg ) |
@@ -241,6 +250,8 @@ def pre_test_compile_register(arg_hash) |
241 | 250 | return unless arg_hash[:context] == GCOV_SYM |
242 | 251 | return if EXTENSION_ASSEMBLY.match?(arg_hash[:source]) |
243 | 252 |
|
| 253 | + validate_mcdc_gcc_version! |
| 254 | + |
244 | 255 | arg_hash[:tool] = TOOLS_GCOV_COMPILER |
245 | 256 | arg_hash[:flags] += ['-fcondition-coverage'] if @project_config[:gcov_mcdc] |
246 | 257 |
|
@@ -270,6 +281,8 @@ def pre_compile_execute(arg_hash) |
270 | 281 | def pre_test_link_register(arg_hash) |
271 | 282 | return unless arg_hash[:context] == GCOV_SYM |
272 | 283 |
|
| 284 | + validate_mcdc_gcc_version! |
| 285 | + |
273 | 286 | @cli_gcov_task = true |
274 | 287 | arg_hash[:tool] = TOOLS_GCOV_LINKER |
275 | 288 | arg_hash[:flags] += ['-fcondition-coverage'] if @project_config[:gcov_mcdc] |
@@ -453,23 +466,35 @@ def build_reportinators(config) |
453 | 466 | return reportinators |
454 | 467 | end |
455 | 468 |
|
456 | | - def get_gcc_version() |
457 | | - command = @tool_executor.build_command_line( TOOLS_GCOV_GCC_VERSION, []) |
| 469 | + # Lazily validates :mcdc against the installed GCC version -- called from every |
| 470 | + # gcov-context build hook that could otherwise emit -fcondition-coverage (compile, |
| 471 | + # untested-source compile, link), memoized so `gcc --version` runs at most once |
| 472 | + # regardless of how many source files or hooks fire before it. See MCDC_GCC_VERSION_GATE |
| 473 | + # above for why this isn't checked eagerly in setup(). |
| 474 | + def validate_mcdc_gcc_version! |
| 475 | + return if @mcdc_gcc_checked |
| 476 | + @mcdc_gcc_checked = true |
| 477 | + return unless @project_config[:gcov_mcdc] |
| 478 | + |
| 479 | + enforce_version_gates!( |
| 480 | + { mcdc: @project_config[:gcov_mcdc] }, |
| 481 | + get_gcc_version(), |
| 482 | + MCDC_GCC_VERSION_GATE |
| 483 | + ) |
| 484 | + end |
458 | 485 |
|
| 486 | + # Get the GCC version number as a ToolVersion struct. |
| 487 | + def get_gcc_version() |
459 | 488 | @loginator.lazy( Verbosity::OBNOXIOUS ) do |
460 | 489 | @reportinator.generate_progress("Collecting GCC version for conditional feature handling") |
461 | 490 | end |
462 | 491 |
|
463 | | - shell_result = @tool_executor.exec( command ) |
464 | | - |
465 | 492 | # First line of gcc --version: "gcc[.exe] (...platform info...) major.minor.patch" |
466 | | - version_match = shell_result[:output].match(/^gcc(?:#{Regexp.escape(EXTENSION_WIN_EXE)})?\s+.*\s+(\d+)\.(\d+)\.\d+/) |
467 | | - |
468 | | - if version_match.nil? || version_match[1].nil? || version_match[2].nil? |
469 | | - raise CeedlingException.new("Could not collect `gcc` version from its command line") |
470 | | - end |
471 | | - |
472 | | - return GcovToolVersion.new( version_match[1].to_i, version_match[2].to_i ) |
| 493 | + detect_tool_version( |
| 494 | + TOOLS_GCOV_GCC_VERSION, |
| 495 | + /^gcc(?:#{Regexp.escape(EXTENSION_WIN_EXE)})?\s+.*\s+(\d+)\.(\d+)\.\d+/, |
| 496 | + tool_label: 'gcc' |
| 497 | + ) |
473 | 498 | end |
474 | 499 |
|
475 | 500 | end |
|
0 commit comments