From 7dc509dd2eecd5417cc0b24eb4fafc73cade3285 Mon Sep 17 00:00:00 2001 From: Michael Karlesky Date: Wed, 19 Jun 2024 13:52:47 -0400 Subject: [PATCH 1/6] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20More=20better=20config?= =?UTF-8?q?=20set=20up=20ordering?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Implemented the logic and ordering of PR #818. To oversimplify, user configuration is assembled first and then all defaults are applied. A handful of methods have been broken into two renamed methods, one for default handling and another for user configuration handling. - Added lots of comments and updated `Configurator` method names to use _populate_, _merge_, etc. consistently. - Refactored `TestRunnerManager` to move configuration validation into Configurator & friends - Renamed Backtrace `gdb` tool definition to match other tools and added support for the tool argument shortcut in project configuration. --- lib/ceedling/configurator.rb | 173 ++++++++++-------- lib/ceedling/configurator_builder.rb | 13 +- lib/ceedling/configurator_setup.rb | 20 +- lib/ceedling/constants.rb | 2 + lib/ceedling/defaults.rb | 41 +++-- .../generator_test_results_backtrace.rb | 2 +- lib/ceedling/objects.yml | 2 - lib/ceedling/setupinator.rb | 153 +++++++++++----- lib/ceedling/test_runner_manager.rb | 58 ++---- 9 files changed, 283 insertions(+), 181 deletions(-) diff --git a/lib/ceedling/configurator.rb b/lib/ceedling/configurator.rb index d1eea979..ed6d38fa 100644 --- a/lib/ceedling/configurator.rb +++ b/lib/ceedling/configurator.rb @@ -59,7 +59,8 @@ def reset_defaults(config) :release_compiler, :release_assembler, :release_linker, - :release_dependencies_generator].each do |tool| + :release_dependencies_generator + ].each do |tool| config[:tools].delete(tool) if (not (config[:tools][tool].nil?)) end end @@ -69,8 +70,10 @@ def reset_defaults(config) # We do this because early config validation failures may need access to verbosity, # but the accessors won't be available until after configuration is validated. def set_verbosity(config) - # PROJECT_VERBOSITY and PROJECT_DEBUG were set at command line processing - # before Ceedling is even loaded. + # PROJECT_VERBOSITY and PROJECT_DEBUG set at command line processing before Ceedling is loaded + + # Configurator will later try to create these accessors automatically but will silently + # fail if they already exist. if (!!defined?(PROJECT_DEBUG) and PROJECT_DEBUG) or (config[:project][:debug]) eval("def project_debug() return true end", binding()) @@ -81,63 +84,99 @@ def set_verbosity(config) if !!defined?(PROJECT_VERBOSITY) eval("def project_verbosity() return #{PROJECT_VERBOSITY} end", binding()) end - - # Configurator will try to create these accessors automatically but will silently - # fail if they already exist. end # The default values defined in defaults.rb (eg. DEFAULT_TOOLS_TEST) are populated # into @param config - def populate_defaults(config) - new_config = DEFAULT_CEEDLING_CONFIG.deep_clone - new_config.deep_merge!(config) - config.replace(new_config) + def merge_tools_defaults(config, default_config) + default_config.deep_merge( DEFAULT_TOOLS_TEST.deep_clone() ) - @configurator_builder.populate_defaults( config, DEFAULT_TOOLS_TEST ) - @configurator_builder.populate_defaults( config, DEFAULT_TOOLS_TEST_PREPROCESSORS ) if (config[:project][:use_test_preprocessor]) - @configurator_builder.populate_defaults( config, DEFAULT_TOOLS_TEST_ASSEMBLER ) if (config[:test_build][:use_assembly]) + default_config.deep_merge( DEFAULT_TOOLS_TEST_PREPROCESSORS.deep_clone() ) if (config[:project][:use_test_preprocessor]) + default_config.deep_merge( DEFAULT_TOOLS_TEST_ASSEMBLER.deep_clone() ) if (config[:test_build][:use_assembly]) - @configurator_builder.populate_defaults( config, DEFAULT_TOOLS_RELEASE ) if (config[:project][:release_build]) - @configurator_builder.populate_defaults( config, DEFAULT_TOOLS_RELEASE_ASSEMBLER ) if (config[:project][:release_build] and config[:release_build][:use_assembly]) + default_config.deep_merge( DEFAULT_TOOLS_RELEASE.deep_clone() ) if (config[:project][:release_build]) + default_config.deep_merge( DEFAULT_TOOLS_RELEASE_ASSEMBLER.deep_clone() ) if (config[:project][:release_build] and config[:release_build][:use_assembly]) end - def populate_unity_defaults(config) - unity = config[:unity] || {} - - unity[:defines] = [] if (unity[:defines].nil?) - end - - - def populate_cmock_defaults(config) + def populate_cmock_defaults(config, default_config) # Cmock has its own internal defaults handling, but we need to set these specific values # so they're present for the build environment to access; - # Note: These need to end up in the hash given to initialize cmock for this to be successful - cmock = config[:cmock] || {} + # Note: these need to end up in the hash given to initialize cmock for this to be successful + + # Populate defaults with CMock internal settings + default_cmock = default_config[:cmock] || {} # Yes, we're duplicating the defaults in CMock, but it's because: # (A) We always need CMOCK_MOCK_PREFIX in Ceedling's environment # (B) Test runner generator uses these same configuration values - cmock[:mock_prefix] = 'Mock' if (cmock[:mock_prefix].nil?) - cmock[:mock_suffix] = '' if (cmock[:mock_suffix].nil?) + default_cmock[:mock_prefix] = 'Mock' if (default_cmock[:mock_prefix].nil?) + default_cmock[:mock_suffix] = '' if (default_cmock[:mock_suffix].nil?) + + # Just because strict ordering is the way to go + default_cmock[:enforce_strict_ordering] = true if (default_cmock[:enforce_strict_ordering].nil?) + + default_cmock[:mock_path] = File.join(config[:project][:build_root], TESTS_BASE_PATH, 'mocks') if (default_cmock[:mock_path].nil?) + + default_cmock[:verbosity] = project_verbosity() if (default_cmock[:verbosity].nil?) + end + + + def prepare_plugins_load_paths(plugins_load_path, config) + # Plugins must be loaded before generic path evaluation & magic that happen later. + # So, perform path magic here as discrete step. + config[:plugins][:load_paths].each do |path| + path.replace( @system_wrapper.module_eval( path ) ) if (path =~ RUBY_STRING_REPLACEMENT_PATTERN) + FilePathUtils::standardize( path ) + end + + # Add Ceedling's plugins path as load path so built-in plugins can be found + config[:plugins][:load_paths] << plugins_load_path + config[:plugins][:load_paths].uniq! + + return @configurator_plugins.process_aux_load_paths( config ) + end - # just because strict ordering is the way to go - cmock[:enforce_strict_ordering] = true if (cmock[:enforce_strict_ordering].nil?) - cmock[:mock_path] = File.join(config[:project][:build_root], TESTS_BASE_PATH, 'mocks') if (cmock[:mock_path].nil?) + def merge_plugins_defaults(paths_hash, config, default_config) + # Config YAML defaults plugins + plugin_yml_defaults = @configurator_plugins.find_plugin_yml_defaults( config, paths_hash ) + + # Config Ruby-based hash defaults plugins + plugin_hash_defaults = @configurator_plugins.find_plugin_hash_defaults( config, paths_hash ) - # Use dynamically defined accessor - cmock[:verbosity] = project_verbosity() if (cmock[:verbosity].nil?) + # Load base configuration values (defaults) from YAML + plugin_yml_defaults.each do |defaults| + default_config.deep_merge( @yaml_wrapper.load( defaults ) ) + end - cmock[:plugins] = [] if (cmock[:plugins].nil?) - cmock[:plugins].map! { |plugin| plugin.to_sym } + # Load base configuration values (defaults) as hash from Ruby + plugin_hash_defaults.each do |defaults| + default_config.deep_merge( defaults ) + end + end + + + def merge_ceedling_runtime_config(config, runtime_config) + # Merge Ceedling's internal runtime configuration settings + config.deep_merge( runtime_config ) + end + + + def populate_cmock_config(config) + # Populate config with CMock config + cmock = config[:cmock] || {} + + cmock[:plugins] = [] if (cmock[:plugins].nil?) + cmock[:plugins].map! { |plugin| plugin.to_sym() } cmock[:plugins].uniq! - cmock[:unity_helper] = false if (cmock[:unity_helper].nil?) + cmock[:unity_helper] = false if (cmock[:unity_helper].nil?) if (cmock[:unity_helper]) cmock[:unity_helper] = [cmock[:unity_helper]] if cmock[:unity_helper].is_a? String + cmock[:includes] = [] if (cmock[:includes].nil?) cmock[:includes] += cmock[:unity_helper].map{|helper| File.basename(helper) } cmock[:includes].uniq! end @@ -146,11 +185,11 @@ def populate_cmock_defaults(config) end - def configure_test_runner_generation(config) + def populate_test_runner_generation_config(config) use_backtrace = config[:project][:use_backtrace] - # TODO: Potentially update once :gdb and :simple are disentangled - if (use_backtrace == :gdb) or (use_backtrace == :simple) + # Force command line argument option for any backtrace option + if use_backtrace != :none config[:test_runner][:cmdline_args] = true end @@ -186,7 +225,7 @@ def get_cmock_config # - Handle inline Ruby string substitution # - Handle needed defaults # - Configure test runner from backtrace configuration - def tools_setup(config) + def populate_tools_config(config) config[:tools].each_key do |name| tool = config[:tools][name] @@ -211,7 +250,7 @@ def tools_setup(config) end - def tools_supplement_arguments(config) + def populate_tools_supplemental_arguments(config) tools_name_prefix = 'tools_' config[:tools].each_key do |name| tool = @project_config_hash[(tools_name_prefix + name.to_s).to_sym] @@ -223,50 +262,21 @@ def tools_supplement_arguments(config) if (not config[top_level_tool].nil?) # Adding and flattening is not a good idea -- might over-flatten if # there's array nesting in tool args. - tool[:arguments].concat config[top_level_tool][:arguments] + tool[:arguments].concat( config[top_level_tool][:arguments] ) end end end - def find_and_merge_plugins(plugins_load_path, config) - # Plugins must be loaded before generic path evaluation & magic that happen later. - # So, perform path magic here as discrete step. - config[:plugins][:load_paths].each do |path| - path.replace( @system_wrapper.module_eval(path) ) if (path =~ RUBY_STRING_REPLACEMENT_PATTERN) - FilePathUtils::standardize(path) - end - - # Add Ceedling's plugins path as load path so built-in plugins can be found - config[:plugins][:load_paths] << plugins_load_path - config[:plugins][:load_paths].uniq! - - paths_hash = @configurator_plugins.process_aux_load_paths(config) - + def merge_plugins_config(paths_hash, plugins_load_path, config) # Rake-based plugins @rake_plugins = @configurator_plugins.find_rake_plugins( config, paths_hash ) - # Ruby `PLugin` subclass programmatic plugins + # Ruby `Plugin` subclass programmatic plugins @programmatic_plugins = @configurator_plugins.find_programmatic_plugins( config, paths_hash ) - # Config YAML defaults plugins - plugin_yml_defaults = @configurator_plugins.find_plugin_yml_defaults( config, paths_hash ) - - # Config Ruby-based hash defaults plugins - plugin_hash_defaults = @configurator_plugins.find_plugin_hash_defaults( config, paths_hash ) - # Config plugins - config_plugins = @configurator_plugins.find_config_plugins( config, paths_hash ) - - # Load base configuration values (defaults) from YAML - plugin_yml_defaults.each do |defaults| - @configurator_builder.populate_defaults( config, @yaml_wrapper.load(defaults) ) - end - - # Load base configuration values (defaults) as hash from Ruby - plugin_hash_defaults.each do |defaults| - @configurator_builder.populate_defaults( config, defaults ) - end + config_plugins = @configurator_plugins.find_config_plugins( config, paths_hash ) # Merge plugin configuration values (like Ceedling project file) config_plugins.each do |plugin| @@ -274,9 +284,9 @@ def find_and_merge_plugins(plugins_load_path, config) # Special handling for plugin paths if (plugin_config.include?( :paths )) - plugin_config[:paths].update(plugin_config[:paths]) do |k,v| - plugin_path = plugin.match(/(.*)[\/]config[\/]\w+\.yml/)[1] - v.map {|vv| File.expand_path(vv.gsub!(/\$PLUGIN_PATH/,plugin_path)) } + plugin_config[:paths].update( plugin_config[:paths] ) do |k,v| + plugin_path = plugin.match( /(.*)[\/]config[\/]\w+\.yml/ )[1] + v.map {|vv| File.expand_path( vv.gsub!( /\$PLUGIN_PATH/, plugin_path) ) } end end @@ -292,8 +302,10 @@ def find_and_merge_plugins(plugins_load_path, config) # Process environment variables set in configuration file - # (Each entry beneath :environment is another hash) + # (Each entry within the :environment array is a hash) def eval_environment_variables(config) + return if config[:environment].nil? + config[:environment].each do |hash| key = hash.keys[0] # Get first (should be only) environment variable entry value = hash[key] # Get associated value @@ -406,11 +418,16 @@ def validate_essential(config) end - def validate_final(config) + def validate_final(config, app_cfg) # Collect all infractions, everybody on probation until final adjudication blotter = true blotter &= @configurator_setup.validate_paths( config ) blotter &= @configurator_setup.validate_tools( config ) + blotter &= @configurator_setup.validate_test_runner_generation( + config, + app_cfg[:include_test_case], + app_cfg[:exclude_test_case] + ) blotter &= @configurator_setup.validate_backtrace( config ) blotter &= @configurator_setup.validate_threads( config ) blotter &= @configurator_setup.validate_plugins( config ) diff --git a/lib/ceedling/configurator_builder.rb b/lib/ceedling/configurator_builder.rb index a873d0ad..af2ef49b 100644 --- a/lib/ceedling/configurator_builder.rb +++ b/lib/ceedling/configurator_builder.rb @@ -85,11 +85,18 @@ def flattenify(config) end + # If config lacks an entry that defaults posseses, add a config entry with the default value def populate_defaults(config, defaults) defaults.keys.sort.each do |section| - defaults[section].keys.sort.each do |entry| - config[section] = {} if config[section].nil? - config[section][entry] = defaults[section][entry].deep_clone if (config[section][entry].nil?) + case defaults[section] + when Hash + defaults[section].keys.sort.each do |entry| + config[section] = {} if config[section].nil? + config[section][entry] = defaults[section][entry].deep_clone if (config[section][entry].nil?) + end + + when Array + config[section] = defaults[section] end end end diff --git a/lib/ceedling/configurator_setup.rb b/lib/ceedling/configurator_setup.rb index 92d84d7f..7ed7ef9f 100644 --- a/lib/ceedling/configurator_setup.rb +++ b/lib/ceedling/configurator_setup.rb @@ -164,7 +164,7 @@ def validate_tools(config) if config[:project][:use_backtrace] == :gdb valid &= @configurator_validator.validate_tool( config:config, - key: :backtrace_reporter, + key: :test_backtrace_gdb, respect_optional: false ) end @@ -172,6 +172,22 @@ def validate_tools(config) return valid end + def validate_test_runner_generation(config, include_test_case, exclude_test_case) + cmdline_args = config[:test_runner][:cmdline_args] + + # Test case filters in use + test_case_filters = !include_test_case.empty? || !exclude_test_case.empty? + + # Test case filters are in use but test runner command line arguments are not enabled + if (test_case_filters and !cmdline_args) + msg = 'Test case filters cannot be used -- enable :test_runner ↳ :cmdline_args in your project configuration' + @loginator.log( msg, Verbosity::ERRORS ) + return false + end + + return true + end + def validate_backtrace(config) valid = true @@ -185,7 +201,7 @@ def validate_backtrace(config) when :gdb # Do nothing else - @loginator.log( ":project ↳ :use_backtrace is '#{use_backtrace}' but must be :none, :simple, or :gdb", Verbosity::ERRORS ) + @loginator.log( ":project ↳ :use_backtrace is '#{use_backtrace}' but must be :none, :simple, or :gdb", Verbosity::ERRORS ) valid = false end diff --git a/lib/ceedling/constants.rb b/lib/ceedling/constants.rb index 2e1fef56..8a4813a8 100644 --- a/lib/ceedling/constants.rb +++ b/lib/ceedling/constants.rb @@ -96,6 +96,8 @@ class StdErrRedirect UNITY_TEST_SOURCE_FILE = 'TEST_SOURCE_FILE' UNITY_TEST_INCLUDE_PATH = 'TEST_INCLUDE_PATH' +RUNNER_BUILD_CMDLINE_ARGS_DEFINE = 'UNITY_USE_COMMAND_LINE_ARGS' + CMOCK_SYM = :cmock CMOCK_ROOT_PATH = 'cmock' CMOCK_LIB_PATH = "#{CMOCK_ROOT_PATH}/src" diff --git a/lib/ceedling/defaults.rb b/lib/ceedling/defaults.rb index 115d2bcd..c82681d2 100644 --- a/lib/ceedling/defaults.rb +++ b/lib/ceedling/defaults.rb @@ -231,9 +231,9 @@ ].freeze } -DEFAULT_BACKTRACE_TOOL = { +DEFAULT_TEST_BACKTRACE_GDB_TOOL = { :executable => ENV['GDB'].nil? ? FilePathUtils.os_executable_ext('gdb').freeze : ENV['GDB'], - :name => 'default_backtrace_reporter'.freeze, + :name => 'default_test_backtrace_gdb'.freeze, # Must be optional because validation is contingent on backtrace configuration. # (Don't break a build if `gdb` is unavailable but backtrace does not require it.) :optional => true.freeze, @@ -254,7 +254,7 @@ :test_linker => DEFAULT_TEST_LINKER_TOOL, :test_fixture => DEFAULT_TEST_FIXTURE_TOOL, :test_fixture_simple_backtrace => DEFAULT_TEST_FIXTURE_SIMPLE_BACKTRACE_TOOL, - :backtrace_reporter => DEFAULT_BACKTRACE_TOOL, + :test_backtrace_gdb => DEFAULT_TEST_BACKTRACE_GDB_TOOL, } } @@ -300,7 +300,7 @@ DEFAULT_RELEASE_TARGET_NAME = 'project' -DEFAULT_CEEDLING_CONFIG = { +DEFAULT_CEEDLING_PROJECT_CONFIG = { :project => { # :build_root must be set by user :use_mocks => true, @@ -323,6 +323,9 @@ :use_assembly => false }, + # Unlike other top-level entries, :environment is an array (of hashes) to preserve order + :environment => [], + :paths => { :test => [], # Must be populated by user :source => [], # Should be populated by user but TEST_INCLUDE_PATH() could be used exclusively instead @@ -341,9 +344,6 @@ :include => [], }, - # unlike other top-level entries, environment's value is an array to preserve order - :environment => [], - :defines => { :use_test_definition => false, :test => [], # A hash/sub-hashes in config file can include operations and test executable matchers as keys @@ -380,18 +380,15 @@ }, :unity => { - :vendor_path => CEEDLING_VENDOR, :defines => [] }, :cmock => { - :vendor_path => CEEDLING_VENDOR, :includes => [], :defines => [] }, :cexception => { - :vendor_path => CEEDLING_VENDOR, :defines => [] }, @@ -402,11 +399,11 @@ :file_suffix => '_runner', }, - # all tools populated while building up config structure + # All tools populated while building up config / defaults structure :tools => {}, - # empty argument lists for default tools - # (these can be overridden in project file to add arguments to tools without totally redefining tools) + # Empty argument lists for default tools + # Note: These can be overridden in project file to add arguments totally redefining tools :test_compiler => { :arguments => [] }, :test_assembler => { :arguments => [] }, :test_linker => { :arguments => [] }, @@ -414,6 +411,7 @@ :arguments => [], :link_objects => [], # compiled object files to always be linked in (e.g. cmock.o if using mocks) }, + :test_backtrace_gdb => { :arguments => [] }, :test_includes_preprocessor => { :arguments => [] }, :test_file_preprocessor => { :arguments => [] }, :test_file_preprocessor_directives => { :arguments => [] }, @@ -421,7 +419,22 @@ :release_compiler => { :arguments => [] }, :release_linker => { :arguments => [] }, :release_assembler => { :arguments => [] }, - :release_dependencies_generator => { :arguments => [] }, + :release_dependencies_generator => { :arguments => [] } + }.freeze + + +CEEDLING_RUNTIME_CONFIG = { + :unity => { + :vendor_path => CEEDLING_VENDOR + }, + + :cmock => { + :vendor_path => CEEDLING_VENDOR + }, + + :cexception => { + :vendor_path => CEEDLING_VENDOR + }, :plugins => { :load_paths => [], diff --git a/lib/ceedling/generator_test_results_backtrace.rb b/lib/ceedling/generator_test_results_backtrace.rb index 98cf6388..a83b1bc0 100644 --- a/lib/ceedling/generator_test_results_backtrace.rb +++ b/lib/ceedling/generator_test_results_backtrace.rb @@ -88,7 +88,7 @@ def do_gdb(filename, executable, shell_result, test_cases) test_cases.each do |test_case| # Build the test fixture to run with our test case of interest command = @tool_executor.build_command_line( - @configurator.tools_backtrace_reporter, [], + @configurator.tools_test_backtrace_gdb, [], gdb_script_filepath, executable, test_case[:test] diff --git a/lib/ceedling/objects.yml b/lib/ceedling/objects.yml index 04e4e3f7..691dc0f6 100644 --- a/lib/ceedling/objects.yml +++ b/lib/ceedling/objects.yml @@ -41,8 +41,6 @@ file_path_collection_utils: - file_wrapper test_runner_manager: - compose: - - configurator cacheinator: compose: diff --git a/lib/ceedling/setupinator.rb b/lib/ceedling/setupinator.rb index fd1fe4ae..0ad5125a 100644 --- a/lib/ceedling/setupinator.rb +++ b/lib/ceedling/setupinator.rb @@ -8,10 +8,8 @@ class Setupinator attr_reader :config_hash - attr_writer :ceedling def setup - @ceedling = {} @config_hash = {} end @@ -24,56 +22,129 @@ def inspect end + def ceedling=(value) + # Application objects hash + @ceedling = value + + # References for brevity + @configurator = value[:configurator] + @loginator = value[:loginator] + @configurator_builder = value[:configurator_builder] + @plugin_manager = value[:plugin_manager] + @plugin_reportinator = value[:plugin_reportinator] + @test_runner_manager = value[:test_runner_manager] + end + + + # Load up all the constants and accessors our rake files, objects, & external scripts will need. def do_setup( app_cfg ) @config_hash = app_cfg[:project_config] - log_filepath = app_cfg[:log_filepath] - - @ceedling[:configurator].include_test_case = app_cfg[:include_test_case] - @ceedling[:configurator].exclude_test_case = app_cfg[:exclude_test_case] - - # Load up all the constants and accessors our rake files, objects, & external scripts will need. - # Note: Configurator modifies the cmock section of the hash with a couple defaults to tie - # projects together -- the modified hash is used to build the cmock object. - @ceedling[:configurator].set_verbosity( config_hash ) - @ceedling[:configurator].validate_essential( config_hash ) - @ceedling[:configurator].populate_defaults( config_hash ) - @ceedling[:configurator].populate_unity_defaults( config_hash ) - @ceedling[:configurator].populate_cmock_defaults( config_hash ) - @ceedling[:configurator].configure_test_runner_generation( config_hash ) - # Evaluate environment vars before sections that might reference them with inline Ruby string expansion - @ceedling[:configurator].eval_environment_variables( config_hash ) - @ceedling[:configurator].eval_paths( config_hash ) - @ceedling[:configurator].eval_flags( config_hash ) - @ceedling[:configurator].eval_defines( config_hash ) - @ceedling[:configurator].standardize_paths( config_hash ) - @ceedling[:configurator].find_and_merge_plugins( app_cfg[:ceedling_plugins_path], config_hash ) - @ceedling[:configurator].tools_setup( config_hash ) - @ceedling[:configurator].validate_final( config_hash ) + + ## + ## 1. Miscellaneous handling and essential configuration prep + ## + + # Set special purpose test case filters (from command line) + @configurator.include_test_case = app_cfg[:include_test_case] + @configurator.exclude_test_case = app_cfg[:exclude_test_case] + + # Verbosity handling + @configurator.set_verbosity( config_hash ) + + # Logging configuration + @loginator.set_logfile( form_log_filepath( app_cfg[:log_filepath] ) ) + @configurator.project_logging = @loginator.project_logging + + # Complain early about anything essential that's missing + @configurator.validate_essential( config_hash ) + + # Merge any needed runtime settings into user configuration + @configurator.merge_ceedling_runtime_config( config_hash, CEEDLING_RUNTIME_CONFIG.deep_clone ) + + ## + ## 2. Handle core user configuration + ## + + # Evaluate environment vars before plugin configurations that might reference with inline Ruby string expansion + @configurator.eval_environment_variables( config_hash ) + + # Standardize paths and add to Ruby load paths + plugins_paths_hash = @configurator.prepare_plugins_load_paths( app_cfg[:ceedling_plugins_path], config_hash ) + + # Populate CMock configuration with values to tie vendor tool configurations together + @configurator.populate_cmock_config( config_hash ) + + @configurator.merge_plugins_config( plugins_paths_hash, app_cfg[:ceedling_plugins_path], config_hash ) + + ## + ## 3. Collect and apply defaults to user configuration + ## + + # Assemble defaults + defaults_hash = DEFAULT_CEEDLING_PROJECT_CONFIG.deep_clone() + @configurator.merge_tools_defaults( config_hash, defaults_hash ) + @configurator.populate_cmock_defaults( config_hash, defaults_hash ) + @configurator.merge_plugins_defaults( plugins_paths_hash, config_hash, defaults_hash ) + + # Set any essential missing or plugin values in configuration with assembled default values + @configurator_builder.populate_defaults( config_hash, defaults_hash ) + + ## + ## 4. Fill out / modify remaining configuration from user configuration + defaults + ## + + # Configure test runner generation + @configurator.populate_test_runner_generation_config( config_hash ) + + # Evaluate environment vars again before subsequent configurations that might reference with inline Ruby string expansion + @configurator.eval_environment_variables( config_hash ) + + # Standardize values and expand inline Ruby string substitutions + @configurator.eval_paths( config_hash ) + @configurator.eval_flags( config_hash ) + @configurator.eval_defines( config_hash ) + @configurator.standardize_paths( config_hash ) + + # Fill out any missing tool config value / supplement arguments + @configurator.populate_tools_config( config_hash ) + @configurator.populate_tools_supplemental_arguments( config_hash ) + + # Configure test runner build & runtime options + @test_runner_manager.configure_build_options( config_hash ) + @test_runner_manager.configure_runtime_options( app_cfg[:include_test_case], app_cfg[:exclude_test_case] ) + + ## + ## 5. Validate configuration + ## + + @configurator.validate_final( config_hash, app_cfg ) + + ## + ## 6. Flatten configuration + process it into globals and accessors + ## + # Partially flatten config + build Configurator accessors and globals - @ceedling[:configurator].build( app_cfg[:ceedling_lib_path], config_hash, :environment ) + @configurator.build( app_cfg[:ceedling_lib_path], config_hash, :environment ) - @ceedling[:configurator].insert_rake_plugins( @ceedling[:configurator].rake_plugins ) - @ceedling[:configurator].tools_supplement_arguments( config_hash ) + ## + ## 7. Final plugins handling + ## + + @configurator.insert_rake_plugins( @configurator.rake_plugins ) # Merge in any environment variables that plugins specify after the main build - @ceedling[:plugin_manager].load_programmatic_plugins( @ceedling[:configurator].programmatic_plugins, @ceedling ) do |env| - @ceedling[:configurator].eval_environment_variables( env ) - @ceedling[:configurator].build_supplement( config_hash, env ) + @plugin_manager.load_programmatic_plugins( @configurator.programmatic_plugins, @ceedling ) do |env| + # Evaluate environment vars that plugins may have added + @configurator.eval_environment_variables( env ) + @configurator.build_supplement( config_hash, env ) end # Inject dependencies for plugin needs - @ceedling[:plugin_reportinator].set_system_objects( @ceedling ) - - # Process options for additional test runner #defines and test runner command line arguments - @ceedling[:test_runner_manager].validate_and_configure_options() - - # Logging set up - @ceedling[:loginator].set_logfile( form_log_filepath( log_filepath ) ) - @ceedling[:configurator].project_logging = @ceedling[:loginator].project_logging + @plugin_reportinator.set_system_objects( @ceedling ) end def reset_defaults(config_hash) - @ceedling[:configurator].reset_defaults( config_hash ) + @configurator.reset_defaults( config_hash ) end ### Private @@ -86,7 +157,7 @@ def form_log_filepath( log_filepath ) # If there's no directory path, put named log file in default location if File.dirname( log_filepath ).empty?() - return File.join( @ceedling[:configurator].project_log_path, log_filepath ) + return File.join( @configurator.project_log_path, log_filepath ) end # Otherwise, log filepath includes a directory (that's already been created) diff --git a/lib/ceedling/test_runner_manager.rb b/lib/ceedling/test_runner_manager.rb index af7c0be8..47c2ee35 100644 --- a/lib/ceedling/test_runner_manager.rb +++ b/lib/ceedling/test_runner_manager.rb @@ -5,65 +5,43 @@ # SPDX-License-Identifier: MIT # ========================================================================= -require 'ceedling/exceptions' +require 'ceedling/constants' class TestRunnerManager - constructor :configurator - - def setup + def initialize() @test_case_incl = nil @test_case_excl = nil @test_runner_defines = [] end - # Return test case arguments (empty if not set) - def collect_cmdline_args() - return [ @test_case_incl, @test_case_excl ].compact() - end + def configure_build_options(config) + cmdline_args = config[:test_runner][:cmdline_args] - def validate_and_configure_options() - # Blow up immediately if things aren't right - return if !validated_and_configured?() + # Should never happen because of external config handling, but... + return if cmdline_args.nil? - @test_runner_defines << 'UNITY_USE_COMMAND_LINE_ARGS' + @test_runner_defines << RUNNER_BUILD_CMDLINE_ARGS_DEFINE if cmdline_args + end - if !@configurator.include_test_case.empty? - @test_case_incl = "-f #{@configurator.include_test_case}" + def configure_runtime_options(include_test_case, exclude_test_case) + if !include_test_case.empty? + @test_case_incl = "-f #{include_test_case}" end - if !@configurator.exclude_test_case.empty? - @test_case_excl = "-x #{@configurator.exclude_test_case}" + if !exclude_test_case.empty? + @test_case_excl = "-x #{exclude_test_case}" end end + # Return test case arguments (empty if not set) + def collect_cmdline_args() + return [ @test_case_incl, @test_case_excl ].compact() + end + # Return ['UNITY_USE_COMMAND_LINE_ARGS'] #define required by Unity to enable cmd line arguments def collect_defines() return @test_runner_defines end - ### Private ### - - private - - # Raise exception if lacking support for test case matching - def validated_and_configured?() - # Command line arguments configured - cmdline_args = @configurator.test_runner_cmdline_args - - # Test case filters in use - test_case_filters = (!@configurator.include_test_case.nil? && !@configurator.include_test_case.empty?) || - (!@configurator.exclude_test_case.nil? && !@configurator.exclude_test_case.empty?) - - # Test case filters are in use but test runner command line arguments are not enabled - if test_case_filters and !cmdline_args - # Blow up if filters are in use but test runner command line arguments are not enabled - msg = 'Unity test case filters cannot be used as configured. ' + - 'Enable :test_runner ↳ :cmdline_args in your project configuration.' - - raise CeedlingException.new( msg ) - end - - return cmdline_args - end end From d419df4bc19466e16f23b8ca4c2723ce67d2d779 Mon Sep 17 00:00:00 2001 From: Michael Karlesky Date: Wed, 19 Jun 2024 16:53:34 -0400 Subject: [PATCH 2/6] =?UTF-8?q?=E2=9C=A8=20Added=20obnoxious=20&=20debug?= =?UTF-8?q?=20logging=20to=20config=20setup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/ceedling/configurator.rb | 123 +++++++++++++++++++++++---- lib/ceedling/configurator_plugins.rb | 21 +++-- lib/ceedling/objects.yml | 2 + lib/ceedling/setupinator.rb | 37 +++++++- 4 files changed, 154 insertions(+), 29 deletions(-) diff --git a/lib/ceedling/configurator.rb b/lib/ceedling/configurator.rb index ed6d38fa..82f93846 100644 --- a/lib/ceedling/configurator.rb +++ b/lib/ceedling/configurator.rb @@ -16,12 +16,12 @@ class Configurator attr_reader :project_config_hash, :programmatic_plugins, :rake_plugins attr_accessor :project_logging, :sanity_checks, :include_test_case, :exclude_test_case - constructor(:configurator_setup, :configurator_builder, :configurator_plugins, :yaml_wrapper, :system_wrapper) do + constructor(:configurator_setup, :configurator_builder, :configurator_plugins, :yaml_wrapper, :system_wrapper, :loginator, :reportinator) do @project_logging = false @sanity_checks = TestResultsSanityChecks::NORMAL end - def setup + def setup() # Cmock config reference to provide to CMock for mock generation @cmock_config = {} # Default empty hash, replaced by reference below @@ -90,6 +90,9 @@ def set_verbosity(config) # The default values defined in defaults.rb (eg. DEFAULT_TOOLS_TEST) are populated # into @param config def merge_tools_defaults(config, default_config) + msg = @reportinator.generate_progress( 'Collecting default tool configurations' ) + @loginator.log( msg, Verbosity::OBNOXIOUS ) + default_config.deep_merge( DEFAULT_TOOLS_TEST.deep_clone() ) default_config.deep_merge( DEFAULT_TOOLS_TEST_PREPROCESSORS.deep_clone() ) if (config[:project][:use_test_preprocessor]) @@ -105,6 +108,11 @@ def populate_cmock_defaults(config, default_config) # so they're present for the build environment to access; # Note: these need to end up in the hash given to initialize cmock for this to be successful + return if !config[:project][:use_mocks] + + msg = @reportinator.generate_progress( 'Collecting CMock defaults' ) + @loginator.log( msg, Verbosity::OBNOXIOUS ) + # Populate defaults with CMock internal settings default_cmock = default_config[:cmock] || {} @@ -139,18 +147,33 @@ def prepare_plugins_load_paths(plugins_load_path, config) end - def merge_plugins_defaults(paths_hash, config, default_config) + def merge_plugins_defaults(paths_hash, config, default_config) # Config YAML defaults plugins plugin_yml_defaults = @configurator_plugins.find_plugin_yml_defaults( config, paths_hash ) # Config Ruby-based hash defaults plugins plugin_hash_defaults = @configurator_plugins.find_plugin_hash_defaults( config, paths_hash ) + if (!plugin_yml_defaults.empty? or !plugin_hash_defaults.empty?) + msg = @reportinator.generate_progress( 'Collecting plugin defaults' ) + @loginator.log( msg, Verbosity::OBNOXIOUS ) + end + + if !@configurator_plugins.plugin_yml_defaults.empty? + msg = " > Plugin YAML defaults: " + @configurator_plugins.plugin_yml_defaults.join( ', ' ) + @loginator.log( msg, Verbosity::DEBUG ) + end + # Load base configuration values (defaults) from YAML plugin_yml_defaults.each do |defaults| default_config.deep_merge( @yaml_wrapper.load( defaults ) ) end + if !@configurator_plugins.plugin_hash_defaults.empty? + msg = " > Plugin Ruby hash defaults: " + @configurator_plugins.plugin_hash_defaults.join( ', ' ) + @loginator.log( msg, Verbosity::DEBUG ) + end + # Load base configuration values (defaults) as hash from Ruby plugin_hash_defaults.each do |defaults| default_config.deep_merge( defaults ) @@ -167,6 +190,12 @@ def merge_ceedling_runtime_config(config, runtime_config) def populate_cmock_config(config) # Populate config with CMock config cmock = config[:cmock] || {} + @cmock_config = cmock + + return if !config[:project][:use_mocks] + + msg = @reportinator.generate_progress( 'Processing CMock configuration' ) + @loginator.log( msg, Verbosity::OBNOXIOUS ) cmock[:plugins] = [] if (cmock[:plugins].nil?) cmock[:plugins].map! { |plugin| plugin.to_sym() } @@ -181,11 +210,22 @@ def populate_cmock_config(config) cmock[:includes].uniq! end - @cmock_config = cmock + @loginator.log( "CMock configuration: #{cmock}", Verbosity::DEBUG ) + end + + + def populate_defaults( config_hash, defaults_hash ) + msg = @reportinator.generate_progress( 'Populating project configuration with collected default values' ) + @loginator.log( msg, Verbosity::OBNOXIOUS ) + + @configurator_builder.populate_defaults( config_hash, defaults_hash ) end def populate_test_runner_generation_config(config) + msg = @reportinator.generate_progress( 'Populating test runner generation settings' ) + @loginator.log( msg, Verbosity::OBNOXIOUS ) + use_backtrace = config[:project][:use_backtrace] # Force command line argument option for any backtrace option @@ -226,6 +266,9 @@ def get_cmock_config # - Handle needed defaults # - Configure test runner from backtrace configuration def populate_tools_config(config) + msg = @reportinator.generate_progress( 'Populating tool definition settings and expanding any string replacements' ) + @loginator.log( msg, Verbosity::OBNOXIOUS ) + config[:tools].each_key do |name| tool = config[:tools][name] @@ -250,33 +293,62 @@ def populate_tools_config(config) end + # Smoosh in extra arguments specified at top-level of config. + # This is useful for tweaking arguments for tools (where argument order does not matter). + # Arguments are squirted in at *end* of list. def populate_tools_supplemental_arguments(config) - tools_name_prefix = 'tools_' - config[:tools].each_key do |name| - tool = @project_config_hash[(tools_name_prefix + name.to_s).to_sym] - - # Smoosh in extra arguments specified at top-level of config - # (useful for plugins & default gcc tools if argument order does not matter). - # Arguments are squirted in at *end* of list. - top_level_tool = (tools_name_prefix + name.to_s).to_sym - if (not config[top_level_tool].nil?) - # Adding and flattening is not a good idea -- might over-flatten if - # there's array nesting in tool args. - tool[:arguments].concat( config[top_level_tool][:arguments] ) + msg = @reportinator.generate_progress( 'Processing tool definition supplemental arguments' ) + @loginator.log( msg, Verbosity::OBNOXIOUS ) + + prefix = 'tools_' + config[:tools].each do |key, tool| + name = key.to_s() + + # Supplemental tool definition + supplemental = config[(prefix + name).to_sym] + + if (not supplemental.nil?) + args_to_add = supplemental[:arguments] + + msg = " > #{name}: Arguments " + args_to_add.map{|arg| "\"#{arg}\""}.join( ', ' ) + @loginator.log( msg, Verbosity::DEBUG ) + + # Adding and flattening is not a good idea -- might over-flatten if array nesting in tool args + tool[:arguments].concat( args_to_add ) end end end def merge_plugins_config(paths_hash, plugins_load_path, config) + msg = @reportinator.generate_progress( 'Discovering plugins' ) + @loginator.log( msg, Verbosity::OBNOXIOUS ) + # Rake-based plugins @rake_plugins = @configurator_plugins.find_rake_plugins( config, paths_hash ) + if !@configurator_plugins.rake_plugins.empty? + msg = " > Rake plugins: " + @configurator_plugins.rake_plugins.join( ', ' ) + @loginator.log( msg, Verbosity::DEBUG ) + end # Ruby `Plugin` subclass programmatic plugins @programmatic_plugins = @configurator_plugins.find_programmatic_plugins( config, paths_hash ) + if !@configurator_plugins.programmatic_plugins.empty? + msg = " > Programmatic plugins: " + @configurator_plugins.programmatic_plugins.map{|p| p[:plugin]}.join( ', ' ) + @loginator.log( msg, Verbosity::DEBUG ) + end # Config plugins config_plugins = @configurator_plugins.find_config_plugins( config, paths_hash ) + if !@configurator_plugins.config_plugins.empty? + msg = " > Config plugins: " + @configurator_plugins.config_plugins.join( ', ' ) + @loginator.log( msg, Verbosity::DEBUG ) + end + + if !config_plugins.empty? + msg = @reportinator.generate_progress( 'Merging plugin configurations' ) + @loginator.log( msg, Verbosity::OBNOXIOUS ) + end # Merge plugin configuration values (like Ceedling project file) config_plugins.each do |plugin| @@ -290,7 +362,7 @@ def merge_plugins_config(paths_hash, plugins_load_path, config) end end - config.deep_merge(plugin_config) + config.deep_merge( plugin_config ) end # Set special plugin setting for results printing if unset @@ -306,6 +378,9 @@ def merge_plugins_config(paths_hash, plugins_load_path, config) def eval_environment_variables(config) return if config[:environment].nil? + msg = @reportinator.generate_progress( 'Processing environment variables' ) + @loginator.log( msg, Verbosity::OBNOXIOUS ) + config[:environment].each do |hash| key = hash.keys[0] # Get first (should be only) environment variable entry value = hash[key] # Get associated value @@ -339,10 +414,13 @@ def eval_environment_variables(config) end - # Eval config path lists (convert strings to array of size 1) and handle any Ruby string replacement + # Eval config path lists (convert any strings to array of size 1) and handle any Ruby string replacement def eval_paths(config) # :plugins ↳ :load_paths already handled + msg = @reportinator.generate_progress( 'Processing path entries and expanding any string replacements' ) + @loginator.log( msg, Verbosity::OBNOXIOUS ) + eval_path_entries( config[:project][:build_root] ) eval_path_entries( config[:release_build][:artifacts] ) @@ -366,6 +444,9 @@ def eval_paths(config) # Handle any Ruby string replacement for :flags string arrays def eval_flags(config) + msg = @reportinator.generate_progress( 'Expanding any string replacements in :flags entries' ) + @loginator.log( msg, Verbosity::OBNOXIOUS ) + # Descend down to array of command line flags strings regardless of depth in config block traverse_hash_eval_string_arrays( config[:flags] ) end @@ -373,12 +454,18 @@ def eval_flags(config) # Handle any Ruby string replacement for :defines string arrays def eval_defines(config) + msg = @reportinator.generate_progress( 'Expanding any string replacements in :defines entries' ) + @loginator.log( msg, Verbosity::OBNOXIOUS ) + # Descend down to array of #define strings regardless of depth in config block traverse_hash_eval_string_arrays( config[:defines] ) end def standardize_paths(config) + msg = @reportinator.generate_progress( 'Standardizing all paths' ) + @loginator.log( msg, Verbosity::OBNOXIOUS ) + # Individual paths that don't follow `_path` convention processed here paths = [ config[:project][:build_root], diff --git a/lib/ceedling/configurator_plugins.rb b/lib/ceedling/configurator_plugins.rb index 80f0164a..12620e0c 100644 --- a/lib/ceedling/configurator_plugins.rb +++ b/lib/ceedling/configurator_plugins.rb @@ -11,11 +11,14 @@ class ConfiguratorPlugins constructor :file_wrapper, :system_wrapper - attr_reader :rake_plugins, :programmatic_plugins + attr_reader :rake_plugins, :programmatic_plugins, :config_plugins, :plugin_yml_defaults, :plugin_hash_defaults def setup - @rake_plugins = [] + @rake_plugins = [] @programmatic_plugins = [] + @config_plugins = [] + @plugin_yml_defaults = [] + @plugin_hash_defaults = [] end @@ -74,7 +77,7 @@ def find_rake_plugins(config, plugin_paths) config[:plugins][:enabled].each do |plugin| if path = plugin_paths[(plugin + '_path').to_sym] rake_plugin_path = File.join(path, "#{plugin}.rake") - if (@file_wrapper.exist?(rake_plugin_path)) + if @file_wrapper.exist?( rake_plugin_path ) plugins_with_path << rake_plugin_path @rake_plugins << plugin end @@ -93,7 +96,7 @@ def find_programmatic_plugins(config, plugin_paths) if path = plugin_paths[(plugin + '_path').to_sym] plugin_path = File.join( path, "lib", "#{plugin}.rb" ) - if @file_wrapper.exist?(plugin_path) + if @file_wrapper.exist?( plugin_path ) @programmatic_plugins << {:plugin => plugin, :root_path => path} end end @@ -105,13 +108,15 @@ def find_programmatic_plugins(config, plugin_paths) # Gather up and return config .yml filepaths that exist in plugin paths + config/ def find_config_plugins(config, plugin_paths) + @config_plugins = [] plugins_with_path = [] config[:plugins][:enabled].each do |plugin| if path = plugin_paths[(plugin + '_path').to_sym] config_plugin_path = File.join(path, "config", "#{plugin}.yml") - if @file_wrapper.exist?(config_plugin_path) + if @file_wrapper.exist?( config_plugin_path ) + @config_plugins << plugin plugins_with_path << config_plugin_path end end @@ -129,8 +134,9 @@ def find_plugin_yml_defaults(config, plugin_paths) if path = plugin_paths[(plugin + '_path').to_sym] default_path = File.join(path, 'config', 'defaults.yml') - if @file_wrapper.exist?(default_path) + if @file_wrapper.exist?( default_path ) defaults_with_path << default_path + @plugin_yml_defaults << plugin end end end @@ -145,11 +151,12 @@ def find_plugin_hash_defaults(config, plugin_paths) config[:plugins][:enabled].each do |plugin| if path = plugin_paths[(plugin + '_path').to_sym] default_path = File.join(path, "config", "defaults_#{plugin}.rb") - if @file_wrapper.exist?(default_path) + if @file_wrapper.exist?( default_path ) @system_wrapper.require_file( "defaults_#{plugin}.rb" ) object = eval("get_default_config()") defaults_hash << object + @plugin_hash_defaults << plugin end end end diff --git a/lib/ceedling/objects.yml b/lib/ceedling/objects.yml index 691dc0f6..1af8ef7b 100644 --- a/lib/ceedling/objects.yml +++ b/lib/ceedling/objects.yml @@ -83,6 +83,8 @@ configurator: - configurator_builder - yaml_wrapper - system_wrapper + - loginator + - reportinator configurator_setup: compose: diff --git a/lib/ceedling/setupinator.rb b/lib/ceedling/setupinator.rb index 0ad5125a..909fa812 100644 --- a/lib/ceedling/setupinator.rb +++ b/lib/ceedling/setupinator.rb @@ -22,14 +22,16 @@ def inspect end + # Injector method for setting Ceedling application object hash def ceedling=(value) - # Application objects hash + # Capture application objects hash as instance variable @ceedling = value - # References for brevity + # Get our dependencies from object hash rather than DIY constructor + # This is a shortcut / validates aspects of our setup @configurator = value[:configurator] @loginator = value[:loginator] - @configurator_builder = value[:configurator_builder] + @reportinator = value[:reportinator] @plugin_manager = value[:plugin_manager] @plugin_reportinator = value[:plugin_reportinator] @test_runner_manager = value[:test_runner_manager] @@ -55,6 +57,8 @@ def do_setup( app_cfg ) @loginator.set_logfile( form_log_filepath( app_cfg[:log_filepath] ) ) @configurator.project_logging = @loginator.project_logging + log_step( 'Validating configuration contains minimum required sections', heading:false ) + # Complain early about anything essential that's missing @configurator.validate_essential( config_hash ) @@ -65,6 +69,8 @@ def do_setup( app_cfg ) ## 2. Handle core user configuration ## + log_step( 'Core Project Configuration Handling' ) + # Evaluate environment vars before plugin configurations that might reference with inline Ruby string expansion @configurator.eval_environment_variables( config_hash ) @@ -80,6 +86,8 @@ def do_setup( app_cfg ) ## 3. Collect and apply defaults to user configuration ## + log_step( 'Assembling Default Settings' ) + # Assemble defaults defaults_hash = DEFAULT_CEEDLING_PROJECT_CONFIG.deep_clone() @configurator.merge_tools_defaults( config_hash, defaults_hash ) @@ -87,12 +95,14 @@ def do_setup( app_cfg ) @configurator.merge_plugins_defaults( plugins_paths_hash, config_hash, defaults_hash ) # Set any essential missing or plugin values in configuration with assembled default values - @configurator_builder.populate_defaults( config_hash, defaults_hash ) + @configurator.populate_defaults( config_hash, defaults_hash ) ## ## 4. Fill out / modify remaining configuration from user configuration + defaults ## + log_step( 'Completing Project Configuration' ) + # Configure test runner generation @configurator.populate_test_runner_generation_config( config_hash ) @@ -117,12 +127,16 @@ def do_setup( app_cfg ) ## 5. Validate configuration ## + log_step( 'Validating final project configuration', heading:false ) + @configurator.validate_final( config_hash, app_cfg ) ## ## 6. Flatten configuration + process it into globals and accessors ## + # Skip logging this step as the end user doesn't care about this internal preparation + # Partially flatten config + build Configurator accessors and globals @configurator.build( app_cfg[:ceedling_lib_path], config_hash, :environment ) @@ -130,6 +144,9 @@ def do_setup( app_cfg ) ## 7. Final plugins handling ## + # Detailed logging already happend for plugin processing + log_step( 'Loading plugins', heading:false ) + @configurator.insert_rake_plugins( @configurator.rake_plugins ) # Merge in any environment variables that plugins specify after the main build @@ -164,4 +181,16 @@ def form_log_filepath( log_filepath ) return log_filepath end + # Neaten up a build step with progress message and some scope encapsulation + def log_step(msg, heading:true) + if heading + msg = @reportinator.generate_heading( @loginator.decorate( msg, LogLabels::CONSTRUCT ) ) + else # Progress message + msg = "\n" + @reportinator.generate_progress( @loginator.decorate( msg, LogLabels::CONSTRUCT ) ) + end + + @loginator.log( msg, Verbosity::OBNOXIOUS ) + end + + end From 2bfbef04576e91ff993b95f96cc62bb0add8d135 Mon Sep 17 00:00:00 2001 From: Michael Karlesky Date: Wed, 19 Jun 2024 17:03:32 -0400 Subject: [PATCH 3/6] =?UTF-8?q?=E2=9C=85=20Test=20updates?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spec/generator_test_results_sanity_checker_spec.rb | 12 ++++++++++-- spec/generator_test_results_spec.rb | 11 ++++++++++- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/spec/generator_test_results_sanity_checker_spec.rb b/spec/generator_test_results_sanity_checker_spec.rb index b6810969..56526018 100644 --- a/spec/generator_test_results_sanity_checker_spec.rb +++ b/spec/generator_test_results_sanity_checker_spec.rb @@ -13,9 +13,17 @@ describe GeneratorTestResultsSanityChecker do before(:each) do - # this will always be mocked - @configurator = Configurator.new({:configurator_setup => nil, :configurator_builder => nil, :configurator_plugins => nil, :yaml_wrapper => nil, :system_wrapper => nil}) + # These will always be mocked @loginator = Loginator.new({:verbosinator => nil, :file_wrapper => nil, :system_wrapper => nil}) + @configurator = Configurator.new({ + :configurator_setup => nil, + :configurator_builder => nil, + :configurator_plugins => nil, + :yaml_wrapper => nil, + :system_wrapper => nil, + :loginator => @loginator, + :reportinator => nil + }) @sanity_checker = described_class.new({:configurator => @configurator, :loginator => @loginator}) diff --git a/spec/generator_test_results_spec.rb b/spec/generator_test_results_spec.rb index c94e1b54..5548e67b 100644 --- a/spec/generator_test_results_spec.rb +++ b/spec/generator_test_results_spec.rb @@ -62,8 +62,17 @@ describe GeneratorTestResults do before(:each) do # these will always be mocked - @configurator = Configurator.new({:configurator_setup => nil, :configurator_builder => nil, :configurator_plugins => nil, :yaml_wrapper => nil, :system_wrapper => nil}) + # this will always be mocked @loginator = Loginator.new({:verbosinator => nil, :file_wrapper => nil, :system_wrapper => nil}) + @configurator = Configurator.new({ + :configurator_setup => nil, + :configurator_builder => nil, + :configurator_plugins => nil, + :yaml_wrapper => nil, + :system_wrapper => nil, + :loginator => @loginator, + :reportinator => nil + }) # these will always be used as is. @yaml_wrapper = YamlWrapper.new From a2c159b2d3678296e235ad787034a1638cdd6ff2 Mon Sep 17 00:00:00 2001 From: Michael Karlesky Date: Wed, 19 Jun 2024 17:04:54 -0400 Subject: [PATCH 4/6] =?UTF-8?q?=F0=9F=93=9D=20Updates=20to=20config=20&=20?= =?UTF-8?q?defaults=20processing=20order?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/BreakingChanges.md | 8 ++++++- docs/CeedlingPacket.md | 12 ++++++---- docs/Changelog.md | 6 ++++- docs/PluginDevelopmentGuide.md | 44 ++++++++++++++++++++++++---------- docs/ReleaseNotes.md | 6 ++++- 5 files changed, 56 insertions(+), 20 deletions(-) diff --git a/docs/BreakingChanges.md b/docs/BreakingChanges.md index 6c6646d7..4b3260a4 100644 --- a/docs/BreakingChanges.md +++ b/docs/BreakingChanges.md @@ -7,7 +7,7 @@ These breaking changes are complemented by two other documents: --- -# [1.0.0 pre-release] — 2024-04-02 +# [1.0.0 pre-release] — 2024-06-19 ## Explicit `:paths` ↳ `:include` entries in the project file @@ -75,6 +75,12 @@ Each test is now treated as its own mini-project. Differentiating components of Release build object files were previously segregated by their source. The release build output directory had subdirectories `c/` and `asm/`. These subdirectories are no longer in use. +## Configuration defaults and configuration set up order + +Ceedling’s previous handling of defaults and configuration processing order certainly worked, but it was not as proper as it could be. To oversimplify, default values were applied in an ordering that caused complications for advanced plugins and advanced users. This has been rectified. Default settings are now processed after all user configurations and plugins. + +For some users and some custom plugins, the new ordering may cause unexpected results. The changes had no known impact on existing plugins and typical project configurations. + ## Changes to global constants & accessors Some global constant “collections” that were previously key elements of Ceedling have changed or gone away as the build pipeline is now able to process a configuration for each individual test executable in favor of for the entire suite. diff --git a/docs/CeedlingPacket.md b/docs/CeedlingPacket.md index 14c82aad..b8f306dd 100644 --- a/docs/CeedlingPacket.md +++ b/docs/CeedlingPacket.md @@ -1697,7 +1697,7 @@ YAML. The next section details Ceedling’s project configuration options in YAML. This section explains all your options for loading and modifying -project configuration from files to begin with. +project configuration. ## Overview of Project Configuration Loading & Smooshing @@ -1707,13 +1707,17 @@ this: 1. Load the base project configuration from a YAML file. 1. Merge the base configuration with zero or more Mixins from YAML files. -1. Load zero or more plugins that alter or merge additional configuration. -1. Merge in default values to ensure all necessary configuration to run - is present. +1. Load zero or more plugins that provide default configuration values + or alter the base project configuration. +1. Populate the configuration with default values to ensure all necessary + configuration to run is present. Ceedling provides reasonably verbose logging at startup telling you which configuration files were used and in what order they were merged. +For nitty-gritty details on plugin configuration behavior, see the +_[Plugin Development Guide](PluginDevelopmentGuide.md)_ + ## Options for Loading Your Base Project Configuration You have three options for telling Ceedling what single base project diff --git a/docs/Changelog.md b/docs/Changelog.md index 468daaee..c1fcf3bb 100644 --- a/docs/Changelog.md +++ b/docs/Changelog.md @@ -9,7 +9,7 @@ This changelog is complemented by two other documents: --- -# [1.0.0 pre-release] — 2024-06-12 +# [1.0.0 pre-release] — 2024-06-19 ## 🌟 Added @@ -191,6 +191,10 @@ The environment variable option for pointing Ceedling to a project file other th Documentation on Mixins and the new options for loading a project configuration are thoroughly documented in _[CeedlingPacket](CeedlingPacket.md))_. +### Configuration defaults and configuration set up order + +Ceedling’s previous handling of defaults and configuration processing order certainly worked, but it was not as proper as it could be. To oversimplify, default values were applied in an ordering that caused complications for advanced plugins and advanced users. This has been rectified. Default settings are now processed after all user configurations and plugins. + ### Plugin system improvements 1. The plugin subsystem has incorporated logging to trace plugin activities at high verbosity levels. diff --git a/docs/PluginDevelopmentGuide.md b/docs/PluginDevelopmentGuide.md index 95edf01c..1dad1dc0 100644 --- a/docs/PluginDevelopmentGuide.md +++ b/docs/PluginDevelopmentGuide.md @@ -111,7 +111,7 @@ programmatic `Plugin` subclasses (Ceedling plugins options #2). that Ceedling incorporates into its configuration defaults during startup. This provides the greatest flexibility in creating configuration values. 1. **YAML configurations.** The data of a simple YAML file is incorporated into - Ceedling's configuration much like Ceedling's actual configuration file. + Ceedling's configuration much like your project configuration file. ## Example configuration plugin layout @@ -141,21 +141,39 @@ project/ ## Ceedling configuration build & use -Configuration is developed at startup in this order: +Configuration is developed at startup by assembling defaults, collecting +user-configured settings, and then populating any missing values with defaults. -1. Ceedling loads its own defaults -1. Any plugin defaults are inserted, if they do not already exist -1. Any plugin configuration is merged -1. Project file configuration is merged +Defaults: -Merging means that any existing configuration is replaced. If no such -key/value pairs already exist, they are inserted into the configuration. +1. Ceedling loads its own defaults separately from your project configuration +1. Supporting framework defaults such as for CMock are populated into (1) +1. Any plugin defaults are merged with (2). -A plugin may further implement its own code to use custom configuration added to -the Ceedling project file. In such cases, it's typically wise to make use of a -plugin's option for defining default values. Configuration handling code is -greatly simplified if strucures and values are guaranteed to exist in some -form. +Final project configuration: + +1. Your project file is loaded and any mixins merged +1. Supporting framework settings that depend on project configuration are populated +1. Plugin configurations are merged with the result of (1) and (2) +1. Defaults are populated into your project configuration +1. Path standardization, string replacement, and related occur throughout the final + configuration + +Merging means that existing simple configuration valuees are replaced or, in the +case of containers such as lists and hashes, values are added to. If no such +key/value pairs already exist, they are simply inserted into the configuration. + +Populating means inserting a configuration value if none already exists. As an +example, if Ceedling finds no compiler defined for test builds in your project +configuration, it populates your configuration with its own internal tool definition. + +A plugin may implement its own code to use extract custom configuration from +the Ceedling project file. See the built-in plugins for examples. For instance, the +Beep plugin makes use of a top-level `:beep` section in project configuration. In +such cases, it's typically wise to make use of a plugin's option for defining +default values. Configuration handling code is greatly simplified if values are +guaranteed to exist in some form. This elimiates a great deal of presence checking +and related code. ## Configuration Plugin Flavors diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index af230eb9..e6cc686a 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -7,7 +7,7 @@ These release notes are complemented by two other documents: --- -# 1.0.0 pre-release — June 12, 2024 +# 1.0.0 pre-release — June 19, 2024 ## 🏴‍☠️ Avast, Breaking Changes, Ye Scallywags! @@ -159,6 +159,10 @@ The `:simple` and `:gdb` options for this feature fully and correctly report eac See _[CeedlingPacket](CeedlingPacket.md))_ for the new `:project` ↳ `:use_backtrace` feature to control how much detail is extracted from a crashed test executable to help you find the cause. +#### Configuration defaults and configuration set up order + +Ceedling’s previous handling of defaults and configuration processing order certainly worked, but it was not as proper as it could be. To oversimplify, default values were applied in an ordering that caused complications for advanced plugins and advanced users. This has been rectified. Default settings are now processed after all user configurations and plugins. + #### Documentation The Ceedling user guide, _[CeedlingPacket](CeedlingPacket.md)_, has been significantly revised and expanded. We will expand it further in future releases and eventually break it up into multiple documents or migrate it to a full documentation management system. From 7dd8bb51f66b1a9e18f43734c4f5f961bf437e0f Mon Sep 17 00:00:00 2001 From: Michael Karlesky Date: Wed, 19 Jun 2024 22:25:07 -0400 Subject: [PATCH 5/6] =?UTF-8?q?=F0=9F=93=9D=20Added=20logging=20decorators?= =?UTF-8?q?=20example?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/CeedlingPacket.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/CeedlingPacket.md b/docs/CeedlingPacket.md index b8f306dd..eb47b1b9 100644 --- a/docs/CeedlingPacket.md +++ b/docs/CeedlingPacket.md @@ -1109,6 +1109,17 @@ holds all that stuff if you want). Ceedling attempts to bring more joy to your console logging. This may include fancy Unicode characters, emoji, or color. +Example: +``` +----------------------- +❌ OVERALL TEST SUMMARY +----------------------- +TESTED: 6 +PASSED: 5 +FAILED: 1 +IGNORED: 0 +``` + By default, Ceedling makes an educated guess as to which platforms can best support this. Some platforms (we’re looking at you, Windows) do not typically have default font support in their terminals for these features. So, by default From 7865b0fcb3653fefdb1d7820a9ce097b0aee1399 Mon Sep 17 00:00:00 2001 From: Michael Karlesky Date: Wed, 19 Jun 2024 22:27:46 -0400 Subject: [PATCH 6/6] =?UTF-8?q?=E2=9C=A8=20Completed=20Setupinator-based?= =?UTF-8?q?=20logging?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/ceedling/configurator.rb | 53 +++++++++++++++++----------- lib/ceedling/configurator_plugins.rb | 10 +++--- lib/ceedling/plugin_manager.rb | 4 +++ lib/ceedling/setupinator.rb | 27 +++++++++----- 4 files changed, 58 insertions(+), 36 deletions(-) diff --git a/lib/ceedling/configurator.rb b/lib/ceedling/configurator.rb index 82f93846..3f9bd5d8 100644 --- a/lib/ceedling/configurator.rb +++ b/lib/ceedling/configurator.rb @@ -320,14 +320,14 @@ def populate_tools_supplemental_arguments(config) end - def merge_plugins_config(paths_hash, plugins_load_path, config) - msg = @reportinator.generate_progress( 'Discovering plugins' ) + def discover_plugins(paths_hash, config) + msg = @reportinator.generate_progress( 'Discovering all plugins' ) @loginator.log( msg, Verbosity::OBNOXIOUS ) # Rake-based plugins @rake_plugins = @configurator_plugins.find_rake_plugins( config, paths_hash ) if !@configurator_plugins.rake_plugins.empty? - msg = " > Rake plugins: " + @configurator_plugins.rake_plugins.join( ', ' ) + msg = " > Rake plugins: " + @configurator_plugins.rake_plugins.map{|p| p[:plugin]}.join( ', ' ) @loginator.log( msg, Verbosity::DEBUG ) end @@ -341,35 +341,43 @@ def merge_plugins_config(paths_hash, plugins_load_path, config) # Config plugins config_plugins = @configurator_plugins.find_config_plugins( config, paths_hash ) if !@configurator_plugins.config_plugins.empty? - msg = " > Config plugins: " + @configurator_plugins.config_plugins.join( ', ' ) + msg = " > Config plugins: " + @configurator_plugins.config_plugins.map{|p| p[:plugin]}.join( ', ' ) @loginator.log( msg, Verbosity::DEBUG ) end - if !config_plugins.empty? - msg = @reportinator.generate_progress( 'Merging plugin configurations' ) - @loginator.log( msg, Verbosity::OBNOXIOUS ) - end + return config_plugins + end + + + def populate_plugins_config(paths_hash, config) + # Set special plugin setting for results printing if unset + config[:plugins][:display_raw_test_results] = true if (config[:plugins][:display_raw_test_results].nil?) + + # Add corresponding path to each plugin's configuration + paths_hash.each_pair { |name, path| config[:plugins][name] = path } + end + + + def merge_config_plugins(config) + return if @configurator_plugins.config_plugins.empty? # Merge plugin configuration values (like Ceedling project file) - config_plugins.each do |plugin| - plugin_config = @yaml_wrapper.load( plugin ) + @configurator_plugins.config_plugins.each do |hash| + _config = @yaml_wrapper.load( hash[:path] ) + + msg = @reportinator.generate_progress( "Merging configuration from plugin #{hash[:plugin]}" ) + @loginator.log( msg, Verbosity::OBNOXIOUS ) # Special handling for plugin paths - if (plugin_config.include?( :paths )) - plugin_config[:paths].update( plugin_config[:paths] ) do |k,v| + if (_config.include?( :paths )) + _config[:paths].update( _config[:paths] ) do |k,v| plugin_path = plugin.match( /(.*)[\/]config[\/]\w+\.yml/ )[1] v.map {|vv| File.expand_path( vv.gsub!( /\$PLUGIN_PATH/, plugin_path) ) } end end - config.deep_merge( plugin_config ) + config.deep_merge( _config ) end - - # Set special plugin setting for results printing if unset - config[:plugins][:display_raw_test_results] = true if (config[:plugins][:display_raw_test_results].nil?) - - # Add corresponding path to each plugin's configuration - paths_hash.each_pair { |name, path| config[:plugins][name] = path } end @@ -591,8 +599,11 @@ def build_supplement(config_base, config_more) def insert_rake_plugins(plugins) - plugins.each do |plugin| - @project_config_hash[:project_rakefile_component_files] << plugin + plugins.each do |hash| + msg = @reportinator.generate_progress( "Adding plugin #{hash[:plugin]} to Rake load list" ) + @loginator.log( msg, Verbosity::OBNOXIOUS ) + + @project_config_hash[:project_rakefile_component_files] << hash[:path] end end diff --git a/lib/ceedling/configurator_plugins.rb b/lib/ceedling/configurator_plugins.rb index 12620e0c..1af76400 100644 --- a/lib/ceedling/configurator_plugins.rb +++ b/lib/ceedling/configurator_plugins.rb @@ -78,13 +78,12 @@ def find_rake_plugins(config, plugin_paths) if path = plugin_paths[(plugin + '_path').to_sym] rake_plugin_path = File.join(path, "#{plugin}.rake") if @file_wrapper.exist?( rake_plugin_path ) - plugins_with_path << rake_plugin_path - @rake_plugins << plugin + @rake_plugins << {:plugin => plugin, :path => rake_plugin_path} end end end - return plugins_with_path + return @rake_plugins end @@ -116,13 +115,12 @@ def find_config_plugins(config, plugin_paths) config_plugin_path = File.join(path, "config", "#{plugin}.yml") if @file_wrapper.exist?( config_plugin_path ) - @config_plugins << plugin - plugins_with_path << config_plugin_path + @config_plugins << {:plugin => plugin, :path => config_plugin_path} end end end - return plugins_with_path + return @config_plugins end diff --git a/lib/ceedling/plugin_manager.rb b/lib/ceedling/plugin_manager.rb index 2bab9229..85a402c9 100644 --- a/lib/ceedling/plugin_manager.rb +++ b/lib/ceedling/plugin_manager.rb @@ -22,6 +22,10 @@ def load_programmatic_plugins(plugins, system_objects) plugins.each do |hash| # Protect against instantiating object multiple times due to processing config multiple times (option files, etc) next if (@plugin_manager_helper.include?( @plugin_objects, hash[:plugin] ) ) + + msg = @reportinator.generate_progress( "Instantiating plugin class #{camelize( hash[:plugin] )}" ) + @loginator.log( msg, Verbosity::OBNOXIOUS ) + begin @system_wrapper.require_file( "#{hash[:plugin]}.rb" ) object = @plugin_manager_helper.instantiate_plugin( diff --git a/lib/ceedling/setupinator.rb b/lib/ceedling/setupinator.rb index 909fa812..bdb0ad61 100644 --- a/lib/ceedling/setupinator.rb +++ b/lib/ceedling/setupinator.rb @@ -66,10 +66,10 @@ def do_setup( app_cfg ) @configurator.merge_ceedling_runtime_config( config_hash, CEEDLING_RUNTIME_CONFIG.deep_clone ) ## - ## 2. Handle core user configuration + ## 2. Handle basic configuration ## - log_step( 'Core Project Configuration Handling' ) + log_step( 'Project Configuration Handling' ) # Evaluate environment vars before plugin configurations that might reference with inline Ruby string expansion @configurator.eval_environment_variables( config_hash ) @@ -80,10 +80,19 @@ def do_setup( app_cfg ) # Populate CMock configuration with values to tie vendor tool configurations together @configurator.populate_cmock_config( config_hash ) - @configurator.merge_plugins_config( plugins_paths_hash, app_cfg[:ceedling_plugins_path], config_hash ) + ## + ## 3. Plugin Handling + ## + + log_step( 'Plugin Handling' ) + + # Plugin handling + @configurator.discover_plugins( plugins_paths_hash, config_hash ) + @configurator.populate_plugins_config( plugins_paths_hash, config_hash ) + @configurator.merge_config_plugins( config_hash ) ## - ## 3. Collect and apply defaults to user configuration + ## 4. Collect and apply defaults to user configuration ## log_step( 'Assembling Default Settings' ) @@ -98,7 +107,7 @@ def do_setup( app_cfg ) @configurator.populate_defaults( config_hash, defaults_hash ) ## - ## 4. Fill out / modify remaining configuration from user configuration + defaults + ## 5. Fill out / modify remaining configuration from user configuration + defaults ## log_step( 'Completing Project Configuration' ) @@ -124,7 +133,7 @@ def do_setup( app_cfg ) @test_runner_manager.configure_runtime_options( app_cfg[:include_test_case], app_cfg[:exclude_test_case] ) ## - ## 5. Validate configuration + ## 6. Validate configuration ## log_step( 'Validating final project configuration', heading:false ) @@ -132,7 +141,7 @@ def do_setup( app_cfg ) @configurator.validate_final( config_hash, app_cfg ) ## - ## 6. Flatten configuration + process it into globals and accessors + ## 7. Flatten configuration + process it into globals and accessors ## # Skip logging this step as the end user doesn't care about this internal preparation @@ -141,11 +150,11 @@ def do_setup( app_cfg ) @configurator.build( app_cfg[:ceedling_lib_path], config_hash, :environment ) ## - ## 7. Final plugins handling + ## 8. Final plugins handling ## # Detailed logging already happend for plugin processing - log_step( 'Loading plugins', heading:false ) + log_step( 'Loading Plugins' ) @configurator.insert_rake_plugins( @configurator.rake_plugins )