diff --git a/README.md b/README.md index 4f475737..898a3b4c 100644 --- a/README.md +++ b/README.md @@ -336,6 +336,12 @@ Matt Chernosky’s **[detailed tutorial][tutorial]** demonstrates using Ceedling ## The basics +### Docker image + +A fully packaged [Ceedling Docker image][docker-image] containing Ruby, Ceedling, the GCC toolchain, and some helper scripts is also available. A Docker container is a self-contained, portable, well managed alternative to a local installation of Ceedling. + +[docker-image]: https://hub.docker.com/r/throwtheswitch/madsciencelab + ### Local installation 1. Install [Ruby]. (Only Ruby 3+ supported.) @@ -350,12 +356,6 @@ Matt Chernosky’s **[detailed tutorial][tutorial]** demonstrates using Ceedling > ceedling test:all release ``` -### Docker image - -A fully packaged [Ceedling Docker image][docker-image] containing Ruby, Ceedling, the GCC toolchain, and some helper scripts is also available. A Docker container is a self-contained, portable, well managed alternative to a local installation of Ceedling. - -[docker-image]: https://hub.docker.com/r/throwtheswitch/madsciencelab - ### Example super-duper simple Ceedling configuration file ```yaml @@ -441,19 +441,21 @@ from within the root directory of your project. Are you afraid of losing all your local changes when this happens? You can prevent Ceedling from updating your project file by adding -`--no_configs`. +`--no-configs`. ```shell - > ceedling upgrade --local --no_configs YourSweetProject + > ceedling upgrade --local --no-configs YourSweetProject ``` ## Git integration Are you using Git? You might want Ceedling to create a `.gitignore` -file for you by adding `--gitignore` to your `new` call. +which ignores the build folder (while retaining control of the artifacts +folder). This will also add a `.gitkeep` file to your `test/support` folder. +You can enable this by adding `--gitsupport` to your `new` call. ```shell - > ceedling new --gitignore YourNewProjectName + > ceedling new --gitsupport YourNewProjectName ```
diff --git a/assets/project_as_gem.yml b/assets/project_as_gem.yml index 589ca451..bd1128ed 100644 --- a/assets/project_as_gem.yml +++ b/assets/project_as_gem.yml @@ -20,9 +20,6 @@ :test_threads: 8 :compile_threads: 8 - # you can specify different yaml config files which modify the existing one - :options_paths: [] - # enable release build (more details in release_build section below) :release_build: FALSE diff --git a/assets/project_with_guts.yml b/assets/project_with_guts.yml index 807a72a7..32f403ca 100644 --- a/assets/project_with_guts.yml +++ b/assets/project_with_guts.yml @@ -20,9 +20,6 @@ :test_threads: 8 :compile_threads: 8 - # you can specify different yaml config files which modify the existing one - :options_paths: [] - # enable release build (more details in release_build section below) :release_build: FALSE diff --git a/assets/project_with_guts_gcov.yml b/assets/project_with_guts_gcov.yml index 74bd3e38..788e7fc1 100644 --- a/assets/project_with_guts_gcov.yml +++ b/assets/project_with_guts_gcov.yml @@ -20,9 +20,6 @@ :test_threads: 8 :compile_threads: 8 - # you can specify different yaml config files which modify the existing one - :options_paths: [] - # enable release build (more details in release_build section below) :release_build: FALSE diff --git a/bin/actions_wrapper.rb b/bin/actions_wrapper.rb new file mode 100755 index 00000000..e194010a --- /dev/null +++ b/bin/actions_wrapper.rb @@ -0,0 +1,35 @@ +require 'thor' +require 'fileutils' + +# Wrapper for handy Thor Actions +class ActionsWrapper + include Thor::Base + include Thor::Actions + + source_root( CEEDLING_ROOT ) + + def _directory(src, *args) + directory( src, *args ) + end + + def _copy_file(src, *args) + copy_file( src, *args ) + end + + def _touch_file(src) + FileUtils.touch(src) + end + + def _chmod(src, mode, *args) + chmod( src, mode, *args ) + end + + def _empty_directory(dest, *args) + empty_directory( dest, *args ) + end + + def _gsub_file(path, flag, *args, &block) + gsub_file( path, flag, *args, &block ) + end + +end diff --git a/bin/app_cfg.rb b/bin/app_cfg.rb new file mode 100755 index 00000000..17389255 --- /dev/null +++ b/bin/app_cfg.rb @@ -0,0 +1,31 @@ + +# Create our global application configuration option set +# This approach bridges clean Ruby and Rake +def get_app_cfg() + app_cfg = { + # Blank initial value for completeness + :project_config => {}, + + # Default, blank value + :log_filepath => '', + + # Only specified in project config (no command line or environment variable) + :default_tasks => ['test:all'], + + # Basic check from working directory + # If vendor/ceedling exists, default to running vendored Ceedling + :which_ceedling => (Dir.exist?( 'vendor/ceedling' ) ? 'vendor/ceedling' : 'gem'), + + # Default, blank test case filters + :include_test_case => '', + :exclude_test_case => '', + + # Default to no duration logging for setup & build ops in Rake context + :stopwatch => false, + + # Default to `exit(1)` upon failing test cases + :tests_graceful_fail => false, + } + + return app_cfg +end diff --git a/bin/ceedling b/bin/ceedling index 59e64209..62bceaf4 100755 --- a/bin/ceedling +++ b/bin/ceedling @@ -1,401 +1,15 @@ #!/usr/bin/env ruby -# Always used require 'rubygems' -require 'fileutils' -# Check for the main project file (either the one defined in the ENV or the default) -main_filepath = ENV['CEEDLING_MAIN_PROJECT_FILE'] -project_found = (!main_filepath.nil? && File.exist?(main_filepath)) -if (!project_found) - main_filepath = "project.yml" - project_found = File.exist?(main_filepath) -end +CEEDLING_ROOT = File.expand_path( File.join( File.dirname( __FILE__ ), ".." ) ) +CEEDLING_BIN = File.join( CEEDLING_ROOT, 'bin' ) +CEEDLING_LIB_BASE = File.join( CEEDLING_ROOT, 'lib' ) +CEEDLING_LIB = File.join( CEEDLING_LIB_BASE, 'ceedling' ) +CEEDLING_VENDOR = File.join( CEEDLING_ROOT, 'vendor' ) -def is_windows? - return ((RbConfig::CONFIG['host_os'] =~ /mswin|mingw/) ? true : false) if defined?(RbConfig) - return ((Config::CONFIG['host_os'] =~ /mswin|mingw/) ? true : false) -end +# Add load path for `require 'ceedling/*'` statements and bin/ code +$LOAD_PATH.unshift( CEEDLING_BIN, CEEDLING_LIB_BASE ) -def here - File.join(File.expand_path(File.dirname(__FILE__)),"/..") -end - -unless (project_found) -#===================================== We Do Not Have A Project ================================================ - - puts "Welcome to Ceedling!" - require 'thor' - - class CeedlingTasks < Thor - include Thor::Actions - - desc "new PROJECT_NAME", "create a new ceedling project" - method_option :docs, :type => :boolean, :default => false, :desc => "Add docs in project vendor directory" - method_option :local, :type => :boolean, :default => false, :desc => "Create a copy of Ceedling in the project vendor directory" - method_option :gitignore, :type => :boolean, :default => false, :desc => "Create a gitignore file for ignoring ceedling generated files" - method_option :no_configs, :type => :boolean, :default => false, :desc => "Don't install starter configuration files" - method_option :noconfigs, :type => :boolean, :default => false - - #deprecated: - method_option :no_docs, :type => :boolean, :default => false - method_option :nodocs, :type => :boolean, :default => false - method_option :as_gem, :type => :boolean, :default => false - method_option :asgem, :type => :boolean, :default => false - method_option :with_ignore, :type => :boolean, :default => false - method_option :withignore, :type => :boolean, :default => false - def new(name, silent = false) - copy_assets_and_create_structure(name, silent, false, options) - end - - desc "upgrade PROJECT_NAME", "upgrade ceedling for a project (not req'd if gem used)" - def upgrade(name, silent = false) - as_local = true - yaml_path = File.join(name, "project.yml") - begin - require File.join(here,"lib","ceedling","yaml_wrapper.rb") - as_local = (YamlWrapper.new.load(yaml_path)[:project][:which_ceedling] != 'gem') - rescue - raise "ERROR: Could not find valid project file '#{yaml_path}'" - end - found_docs = File.exist?( File.join(name, "docs", "CeedlingPacket.md") ) - copy_assets_and_create_structure(name, silent, true, {:upgrade => true, :no_configs => true, :local => as_local, :docs => found_docs}) - end - - no_commands do - def copy_assets_and_create_structure(name, silent=false, force=false, options = {}) - - puts "WARNING: --no_docs deprecated. It is now the default. Specify -docs if you want docs installed." if (options[:no_docs] || options[:nodocs]) - puts "WARNING: --as_gem deprecated. It is now the default. Specify -local if you want ceedling installed to this project." if (options[:as_gem] || options[:asgem]) - puts "WARNING: --with_ignore deprecated. It is now called -gitignore" if (options[:with_ignore] || options[:withignore]) - - use_docs = options[:docs] || false - use_configs = !(options[:no_configs] || options[:noconfigs] || false) - use_gem = !(options[:local]) - use_ignore = options[:gitignore] || false - is_upgrade = options[:upgrade] || false - - ceedling_path = File.join(name, 'vendor', 'ceedling') - source_path = File.join(name, 'src') - test_path = File.join(name, 'test') - test_support_path = File.join(name, 'test/support') - - # If it's not an upgrade, make sure we have the paths we expect - if (!is_upgrade) - [source_path, test_path, test_support_path].each do |d| - FileUtils.mkdir_p d - end - else - prj_yaml = YamlWrapper.new.load(File.join(name, 'project.yml')) - test_support_path = if prj_yaml.key?(:path) && \ - prj_yaml[:path].key?(:support) - prj_yaml.key?[:path][:support] - else - '' - end - end - - # Genarate gitkeep in test support path - FileUtils.touch(File.join(test_support_path, '.gitkeep')) unless \ - test_support_path.empty? - - # If documentation requested, create a place to dump them and do so - doc_path = '' - if use_docs - doc_path = use_gem ? File.join(name, 'docs') : File.join(ceedling_path, 'docs') - FileUtils.mkdir_p doc_path - - in_doc_path = lambda {|f| File.join(doc_path, f)} - - # Add documentation from main projects to list - doc_files = {} - ['docs','vendor/unity/docs','vendor/cmock/docs','vendor/cexception/docs'].each do |p| - Dir[ File.expand_path(File.join(here, p, '*.md')) ].each do |f| - doc_files[ File.basename(f) ] = f unless(doc_files.include? f) - end - end - - # Add documentation from plugins to list - Dir[ File.join(here, 'plugins/**/README.md') ].each do |plugin_path| - k = "plugin_" + plugin_path.split(/\\|\//)[-2] + ".md" - doc_files[ k ] = File.expand_path(plugin_path) - end - - # Copy all documentation - doc_files.each_pair do |k, v| - copy_file(v, in_doc_path.call(k), :force => force) - end - end - - # If installed locally to project, copy ceedling, unity, cmock, & supports to vendor - unless use_gem - FileUtils.mkdir_p ceedling_path - - #copy full folders from ceedling gem into project - %w{plugins lib bin}.map do |f| - {:src => f, :dst => File.join(ceedling_path, f)} - end.each do |f| - directory(f[:src], f[:dst], :force => force) - end - - # mark ceedling as an executable - File.chmod(0755, File.join(ceedling_path, 'bin', 'ceedling')) unless is_windows? - - #copy necessary subcomponents from ceedling gem into project - sub_components = [ - {:src => 'vendor/c_exception/lib/', :dst => 'vendor/c_exception/lib'}, - {:src => 'vendor/cmock/config/', :dst => 'vendor/cmock/config'}, - {:src => 'vendor/cmock/lib/', :dst => 'vendor/cmock/lib'}, - {:src => 'vendor/cmock/src/', :dst => 'vendor/cmock/src'}, - {:src => 'vendor/diy/lib', :dst => 'vendor/diy/lib'}, - {:src => 'vendor/unity/auto/', :dst => 'vendor/unity/auto'}, - {:src => 'vendor/unity/src/', :dst => 'vendor/unity/src'}, - ] - - sub_components.each do |c| - directory(c[:src], File.join(ceedling_path, c[:dst]), :force => force) - end - end - - # We're copying in a configuration file if we haven't said not to - if (use_configs) - dst_yaml = File.join(name, 'project.yml') - src_yaml = if use_gem - File.join(here, 'assets', 'project_as_gem.yml') - else - if is_windows? - copy_file(File.join('assets', 'ceedling.cmd'), File.join(name, 'ceedling.cmd'), :force => force) - else - copy_file(File.join('assets', 'ceedling'), File.join(name, 'ceedling'), :force => force) - File.chmod(0755, File.join(name, 'ceedling')) - end - File.join(here, 'assets', 'project_with_guts.yml') - end - - # Perform the actual clone of the config file, while updating the version - File.open(dst_yaml,'w') do |dst| - require File.expand_path(File.join(File.dirname(__FILE__),"..","lib","ceedling","version.rb")) - dst << File.read(src_yaml).gsub(":ceedling_version: '?'",":ceedling_version: #{Ceedling::Version::CEEDLING}") - puts " create #{dst_yaml}" - end - end - - # Copy the gitignore file if requested - if (use_ignore) - copy_file(File.join('assets', 'default_gitignore'), File.join(name, '.gitignore'), :force => force) - end - - unless silent - puts "\n" - puts "Project '#{name}' #{force ? "upgraded" : "created"}!" - puts " - Tool documentation is located in #{doc_path}" if use_docs - puts " - Execute 'ceedling help' from #{name} to view available test & build tasks" - puts '' - end - end - end - - desc "examples", "list available example projects" - def examples() - puts "Available sample projects:" - FileUtils.cd(File.join(here, "examples")) do - Dir["*"].each {|proj| puts " #{proj}"} - end - end - - desc "example PROJ_NAME [DEST]", "new specified example project (in DEST, if specified)" - def example(proj_name, dest=nil) - if dest.nil? then dest = proj_name end - - copy_assets_and_create_structure(dest, true, false, {:local=>true, :docs=>true}) - - dest_src = File.join(dest,'src') - dest_test = File.join(dest,'test') - dest_project = File.join(dest,'project.yml') - - directory "examples/#{proj_name}/src", dest_src - directory "examples/#{proj_name}/test", dest_test - remove_file dest_project - copy_file "examples/#{proj_name}/project.yml", dest_project - - puts "\n" - puts "Example project '#{proj_name}' created!" - puts " - Tool documentation is located in vendor/ceedling/docs" - puts " - Execute 'ceedling help' to view available test & build tasks" - puts '' - end - - desc "version", "return the version of the tools installed" - def version() - require File.expand_path(File.join(File.dirname(__FILE__),"..","lib","ceedling","version.rb")) - puts " Ceedling:: #{Ceedling::Version::CEEDLING}" - puts " CMock:: #{Ceedling::Version::CMOCK}" - puts " Unity:: #{Ceedling::Version::UNITY}" - puts " CException:: #{Ceedling::Version::CEXCEPTION}" - end - end - - if (ARGV[0] =~ /^\-T$/) - puts "\n(No Project Detected, Therefore Showing Options to Create Projects)" - CeedlingTasks.tasks.each_pair do |k,v| - puts v.usage.ljust(25,' ') + v.description - end - puts "\n" - else - CeedlingTasks.source_root here - CeedlingTasks.start - end - -#===================================== We Have A Project Already ================================================ -else - require File.join(here, "lib", "ceedling", "yaml_wrapper.rb") - require File.join(here, "lib", "ceedling", "constants.rb") - require 'rbconfig' - - # Determine platform - platform = begin - case(RbConfig::CONFIG['host_os']) - when /mswin|mingw|cygwin/i - :mswin - when /darwin/ - :osx - else - :linux - end - rescue - :linux - end - - # Create our default meta-runner option set - options = { - :pretest => nil, - :args => [], - :add_path => [], - :path_connector => (platform == :mswin) ? ";" : ":", - :graceful_fail => false, - :which_ceedling => (Dir.exist?("vendor/ceedling") ? "vendor/ceedling" : 'gem'), - :default_tasks => [ 'test:all' ], - :list_tasks => false - } - - # Guess that we need a special script file first if it exists - if (platform == :mswin) - options[:pretest] = File.exist?("#{ platform }_setup.bat") ? "#{ platform }_setup.bat" : nil - else - options[:pretest] = File.exist?("#{ platform }_setup.sh") ? "source #{ platform }_setup.sh" : nil - end - - # Merge in project settings if they can be found here - yaml_options = YamlWrapper.new.load(main_filepath) - if (yaml_options[:paths]) - options[:add_path] = yaml_options[:paths][:tools] || [] - else - options[:add_path] = [] - end - options[:graceful_fail] = yaml_options[:graceful_fail] if yaml_options[:graceful_fail] - options[:which_ceedling] = yaml_options[:project][:which_ceedling] if (yaml_options[:project] && yaml_options[:project][:which_ceedling]) - options[:default_tasks] = yaml_options[:default_tasks] if yaml_options[:default_tasks] - - # Sort through command line options - ARGV.each do |v| - case(v) - when /^(?:new|examples?|templates?)$/ - puts "\nOops. You called ceedling with argument '#{v}'.\n" + - " This is an operation that will create a new project... \n" + - " but it looks like you're already in a project. If you really \n" + - " want to do this, try moving to an empty folder.\n\n" - abort - when /^help$/ - options[:list_tasks] = true - when /^-T$/ - options[:list_tasks] = true - when /^--tasks$/ - options[:list_tasks] = true - when /^project:(\w+)/ - ENV['CEEDLING_USER_PROJECT_FILE'] = "#{$1}.yml" - when /^--test_case=(\w+)/ - ENV['CEEDLING_INCLUDE_TEST_CASE_NAME'] = $1 - when /^--exclude_test_case=(\w+)/ - ENV['CEEDLING_EXCLUDE_TEST_CASE_NAME'] = $1 - else - options[:args].push(v) - end - end - - # Set global consts for verbosoty and debug from command line. - # By moving this up before Rake tasks are processed, - # logging and other startup features are decoupled from Rake and Configurator instantiation. - verbosity = Verbosity::NORMAL - - # Iterate through all command line options, and process any verbosity arguments - options[:args].each do |option| - _option = option.downcase() - next if not _option.start_with?( 'verbosity' ) - - # Process verbosity as string names `verbosity:` - if matches = _option.match(/verbosity:(\w+)/) - # Get level string and convert to symbol - _verbosity = matches[1].to_sym - # Look up in verbosity hash - _verbosity = VERBOSITY_OPTIONS[_verbosity] - # If it's nil, there was a typo (empty Rake task will handle this) - verbosity = _verbosity if not _verbosity.nil? - break - end - - # Process verbosity as a Rake numeric argument `verbosity[#]` - if matches = _option.match(/verbosity\[([0-5])\]/) - verbosity = matches[1].to_i - break - end - end - - # Set global const and freeze - PROJECT_VERBOSITY = verbosity - PROJECT_VERBOSITY.freeze() - - # Set global const and freeze - PROJECT_DEBUG = (PROJECT_VERBOSITY == Verbosity::DEBUG) - PROJECT_DEBUG.freeze() - - # Add to the path - if (options[:add_path] && !options[:add_path].empty?) - path = ENV["PATH"] - options[:add_path].each do |p| - f = File.expand_path(File.dirname(__FILE__),p) - path = (f + options[:path_connector] + path) unless path.include? f - end - ENV["PATH"] = path - end - - # Load Ceedling (either through the rakefile OR directly) - if (File.exist?("rakefile.rb")) - load 'rakefile.rb' - else - if (options[:which_ceedling] == 'gem') - require 'ceedling' - else - load "#{options[:which_ceedling]}/lib/ceedling.rb" - end - Ceedling.load_project - end - - Rake.application.standard_exception_handling do - if options[:list_tasks] - # Display helpful task list when requested. (This required digging into Rake internals a bit.) - Rake.application.define_singleton_method(:name=) {|n| @name = n} - Rake.application.name = 'ceedling' - Rake.application.options.show_tasks = :tasks - Rake.application.options.show_task_pattern = /^(?!.*build).*$/ - Rake.application.display_tasks_and_comments() - else - task :default => options[:default_tasks] - - # Run our Tasks! - Rake.application.collect_command_line_tasks(options[:args]) - Rake.application.top_level - end - end - true -#=================================================================================================================== -end +# Load "bootloader" / command line handling in bin/ +require 'main' diff --git a/bin/cli.rb b/bin/cli.rb new file mode 100755 index 00000000..c7fadcf6 --- /dev/null +++ b/bin/cli.rb @@ -0,0 +1,417 @@ +require 'thor' +require 'ceedling/constants' # From Ceedling application + +## +## Command Line Handling +## ===================== +## +## OVERVIEW +## -------- +## Ceedling's command line handling marries Thor and Rake. Thor does not call +## Rake. Rather, a handful of command line conventions, edge case handling, +## and Thor features are stitched together to ensure a given command line is +## processed by Thor and/or Rake. +## +## Ceedling's command line is processed with these mechanisms: +## 1. Special / edge case hacking of ARGV directly. +## 2. Thor for all application commands and flags. +## 3. Handing off to Rake from either (1) or (2) for task listing or running +## build tasks. +## +## EDGE CASE HACKING +## ----------------- +## Special / edge cases: +## 1. Silent backwards compatibility support for Rake's `-T`. +## 2. Thor does not recognize "naked" build tasks as application commands +## (`ceedling test:all` instead of `ceedling build test:all`). We catch +## this exception and provide the command line back to Thor as a `build` +## command line. This also allows us to ensure Thor processes `build` flags +## following naked build tasks that would otherwise be ignored. +## +## THOR +## ---- +## Thor is configured or overridden with these special attributes: +## * The default task is `build`. This means that if the `build` keyword is +## omitted but Thor otherwise recognizes the command line (a `build` flag is +## the first item on the command line), it will process it as the `build` +## command. The build command takes flags and tasks. Tasks are handed off to +## Rake to process. If no `build` keyword is present and `build` flags come +## after tasks, Thor sees the command line as unhandled commands. +## * The PermissiveCLI code handles unrecognized command exception so as to +## eat the Thor complaint and re-throw the exception for edge case handling. +## +## NOTES +## ----- +## * Ultimately, any unrecognized command or task is processed by Rake, and +## Rake makes the complaint. +## + + + +# Special handler to prevent Thor from barfing on unrecognized CLI arguments +# (i.e. Rake tasks) +module PermissiveCLI + def self.extended(base) + super + base.check_unknown_options! + end + + def start(args, config={}) + config[:shell] ||= Thor::Base.shell.new + dispatch(nil, args, nil, config) + rescue Thor::UndefinedCommandError + # Eat unhandled command errors + # - No error message + # - No `exit()` + # - Re-raise to allow Rake task CLI handling elsewhere + raise + end +end + +module CeedlingTasks + + VERBOSITY_NORMAL = 'normal' + VERBOSITY_DEBUG = 'debug' + + DOC_LOCAL_FLAG = "Install Ceedling plus supporting tools to vendor/" + + DOC_DOCS_FLAG = "Copy documentation to docs/" + + DOC_PROJECT_FLAG = "Loads the filepath as your base project configuration" + + DOC_MIXIN_FLAG = "Merges the configuration mixin by name or filepath." + + LONGDOC_LOCAL_FLAG = "`--local` copies Ceedling and its dependencies to a vendor/ + subdirectory in the root of the project. It also installs a + platform-appropriate executable script `ceedling` at the root of the + project." + + LONGDOC_DOCS_FLAG = "`--docs` copies all tool documentation to a docs/ + subdirectory in the root of the project." + + LONGDOC_PROJECT_FLAG = "`--project` loads the specified project file as your + base configuration." + + LONGDOC_MIXIN_FLAG = "`--mixin` merges the specified configuration mixin. This + flag may be repeated for multiple mixins. A simple mixin name will initiate a + lookup from within mixin load paths specified in your project file and among + Ceedling’s internal mixin load path. A filepath and/or filename (having an + extension) will instead merge the specified mixin configuration YAML file. + See documentation for complete details on mixins. + \x5> --mixin my_compiler --mixin my/path/mixin.yml" + + CEEDLING_EXAMPLES_PATH = File.join( CEEDLING_ROOT, 'examples' ) + + class CLI < Thor + include Thor::Actions + extend PermissiveCLI + + # Ensure we bail out with non-zero exit code if the command line is wrong + def self.exit_on_failure?() true end + + # Allow `build` to be omitted in command line + default_task :build + + # Intercept construction to extract configuration and injected dependencies + def initialize(args, config, options) + super(args, config, options) + + @app_cfg = options[:app_cfg] + @handler = options[:objects][:cli_handler] + end + + + # Override Thor help to list Rake tasks as well + desc "help [COMMAND]", "Describe available commands and list build operations" + method_option :project, :type => :string, :default => nil, :aliases => ['-p'], :desc => DOC_PROJECT_FLAG + method_option :mixin, :type => :string, :default => [], :repeatable => true, :aliases => ['-m'], :desc => DOC_MIXIN_FLAG + method_option :debug, :type => :boolean, :default => false, :hide => true + long_desc <<-LONGDESC + `ceedling help` provides standard help for all available application commands + and build tasks. + + COMMAND is optional and will produce detailed help for a specific command. + + `ceedling help` also lists the available build operations from loading your + project configuration. Optionally, a project filepath and/or mixins may be + provided (see below) to load a different project configuration. If not + provided, the default options for loading project configuration will be used. + + Optional Flags: + + • #{LONGDOC_PROJECT_FLAG} + + • #{LONGDOC_MIXIN_FLAG} + LONGDESC + def help(command=nil) + # Get unfrozen copies so we can add / modify + _options = options.dup() + _options[:project] = options[:project].dup() if !options[:project].nil? + _options[:mixin] = [] + options[:mixin].each {|mixin| _options[:mixin] << mixin.dup() } + + _options[:verbosity] = options[:debug] ? VERBOSITY_DEBUG : nil + + # Call application help with block to execute Thor's built-in help in the help logic + @handler.app_help( ENV, @app_cfg, _options, command ) { |command| super(command) } + end + + + desc "new NAME [DEST]", "Create a new project structure at optional DEST path" + method_option :local, :type => :boolean, :default => false, :desc => DOC_LOCAL_FLAG + method_option :docs, :type => :boolean, :default => false, :desc => DOC_DOCS_FLAG + method_option :configs, :type => :boolean, :default => true, :desc => "Install starter configuration files" + method_option :force, :type => :boolean, :default => false, :desc => "Ignore any existing project and recreate destination" + method_option :debug, :type => :boolean, :default => false, :hide => true + method_option :gitsupport, :type => :boolean, :default => false, :desc => "Create .gitignore / .gitkeep files for convenience" + long_desc <<-LONGDESC + `ceedling new` creates a new project structure. + + NAME is required and will be the containing directory for the new project. + + DEST is an optional directory path in which to place the new project (e.g. + /). The default desintation is your working directory. If the + containing path does not exist, it will be created. + + Optional Flags: + + • #{LONGDOC_LOCAL_FLAG} + + • #{LONGDOC_DOCS_FLAG} + + • `--configs` add a starter project configuration file into the root of the + new project. + + • `--force` overrides protectons preventing a new project from overwriting an + existing project. This flag completely destroys anything found in the target + path for the new project. + LONGDESC + def new(name, dest=nil) + # Get unfrozen copies so we can add / modify + _options = options.dup() + _dest = dest.dup() if !dest.nil? + + _options[:verbosity] = options[:debug] ? VERBOSITY_DEBUG : nil + + @handler.new_project( CEEDLING_ROOT, _options, name, _dest ) + end + + + desc "upgrade PATH", "Upgrade vendored installation of Ceedling for a project at PATH" + method_option :project, :type => :string, :default => DEFAULT_PROJECT_FILENAME, :desc => "Project filename" + method_option :debug, :type => :boolean, :default => false, :hide => true + long_desc <<-LONGDESC + `ceedling upgrade` updates an existing project. + + PATH is required and should be the root of the project to upgrade. + + This command only meaningfully operates on projects wth a local vendored copy + of Ceedlng (in /vendor/) and optionally a local copy of the + documentation (in /docs/). + + Running this command will replace vendored Ceedling with the version carrying + out this command. If documentation is found, it will replace it with the bundle + accompanying the version of Ceedling carrying out this command. + + A basic check for project existence looks for vendored ceedlng and a project + configuration file. + + Optional Flags: + + • `--project` specifies a filename (optionally with leading path) for the + project configuration file used in the project existence check. Otherwise, + the default ./#{DEFAULT_PROJECT_FILENAME} at the root of the project is + checked. + LONGDESC + def upgrade(path) + # Get unfrozen copies so we can add / modify + _options = options.dup() + _options[:project] = options[:project].dup() + _path = path.dup() + + _options[:verbosity] = options[:debug] ? VERBOSITY_DEBUG : nil + + @handler.upgrade_project( CEEDLING_ROOT, _options, _path ) + end + + + desc "build [TASKS...]", "Run build tasks (`build` keyword not required)" + method_option :project, :type => :string, :default => nil, :aliases => ['-p'], :desc => DOC_PROJECT_FLAG + method_option :mixin, :type => :string, :default => [], :repeatable => true, :aliases => ['-m'], :desc => DOC_MIXIN_FLAG + method_option :verbosity, :enum => ['silent', 'errors', 'warnings', VERBOSITY_NORMAL, 'obnoxious', VERBOSITY_DEBUG], :default => VERBOSITY_NORMAL, :aliases => ['-v'] + method_option :log, :type => :boolean, :default => false, :aliases => ['-l'], :desc => "Enable logging to default filepath" + method_option :logfile, :type => :string, :default => '', :desc => "Enable logging to given filepath" + method_option :graceful_fail, :type => :boolean, :default => nil, :desc => "Force exit code of 0 for unit test failures" + method_option :test_case, :type => :string, :default => '', :desc => "Filter for individual unit test names" + method_option :exclude_test_case, :type => :string, :default => '', :desc => "Prevent matched unit test names from running" + # Include for consistency with other commands (override --verbosity) + method_option :debug, :type => :boolean, :default => false, :hide => true + long_desc <<-LONGDESC + `ceedling build` executes build tasks created from your project configuration. + + NOTE: `build` is not required to run tasks. The following usages are equivalent: + \x5 > ceedling test:all + \x5 > ceedling build test:all + + TASKS are zero or more build operations created from your project configuration. + If no tasks are provided, the built-in default tasks or your :project ↳ + :default_tasks will be executed. + + Optional Flags: + + • #{LONGDOC_PROJECT_FLAG} + + • #{LONGDOC_MIXIN_FLAG} + + • `--verbosity` sets the logging level. + + • `--log` enables logging to the default filename and path location within your + project build directory. + + • `--logfile` enables logging to the specified log filepath + (ex: my/path/file.log). + + • `--graceful-fail` ensures an exit code of 0 even when unit tests fail. See + documentation for full details. + + • `--test-case` sets a test case name matcher to run only a subset of test + suite’s unit test cases. See documentation for full details. + + • `--exclude-test-case` is the inverse of `--test-case`. See documentation for + full details. + LONGDESC + def build(*tasks) + # Get unfrozen copies so we can add / modify + _options = options.dup() + _options[:project] = options[:project].dup() if !options[:project].nil? + _options[:mixin] = [] + options[:mixin].each {|mixin| _options[:mixin] << mixin.dup() } + _options[:verbosity] = VERBOSITY_DEBUG if options[:debug] + + @handler.build( env:ENV, app_cfg:@app_cfg, options:_options, tasks:tasks ) + end + + + desc "dumpconfig FILEPATH [SECTIONS...]", "Process project configuration and write final config to a YAML file" + method_option :project, :type => :string, :default => nil, :aliases => ['-p'], :desc => DOC_PROJECT_FLAG + method_option :mixin, :type => :string, :default => [], :repeatable => true, :aliases => ['-m'], :desc => DOC_MIXIN_FLAG + method_option :app, :type => :boolean, :default => true, :desc => "Runs Ceedling application and its config manipulations" + method_option :debug, :type => :boolean, :default => false, :hide => true + long_desc <<-LONGDESC + `ceedling dumpconfig` loads your project configuration, including all manipulations & merges, + and writes the final config to a YAML file. + + FILEPATH is a required path to a destination YAML file. If the containing path does not exist, + it will be created. + + SECTIONS is an optional configuration section “path” that extracts only a portion of a + configuration. The resulting top-level YAML container will be the last element of the path. + The following example will produce a config.yml containing ':test_compiler: {...}'. + No section path produces a complete configuration. + \x5> ceedling dumpconfig my/path/config.yml tools test_compiler + + Optional Flags: + + • #{LONGDOC_PROJECT_FLAG} + + • #{LONGDOC_MIXIN_FLAG} + + • `--app` loads the Ceedling application that adds various settings, merges defaults, loads + configration changes due to plugins, and validates the configuration. Disabling the application + dumps the project configuration after any mixins but before any application manipulations. + LONGDESC + def dumpconfig(filepath, *sections) + # Get unfrozen copies so we can add / modify + _options = options.dup() + _options[:project] = options[:project].dup() if !options[:project].nil? + _options[:mixin] = [] + options[:mixin].each {|mixin| _options[:mixin] << mixin.dup() } + _filepath = filepath.dup() + + _options[:verbosity] = options[:debug] ? VERBOSITY_DEBUG : nil + + @handler.dumpconfig( ENV, @app_cfg, _options, _filepath, sections ) + end + + + desc "environment", "List all configured environment variable names with values." + method_option :project, :type => :string, :default => nil, :aliases => ['-p'], :desc => DOC_PROJECT_FLAG + method_option :mixin, :type => :string, :default => [], :repeatable => true, :aliases => ['-m'], :desc => DOC_MIXIN_FLAG + method_option :debug, :type => :boolean, :default => false, :hide => true + long_desc <<-LONGDESC + `ceedling environment` displays all environment variables that have been set for project use. + + Optional Flags: + + • #{LONGDOC_PROJECT_FLAG} + + • #{LONGDOC_MIXIN_FLAG} + LONGDESC + def environment() + # Get unfrozen copies so we can add / modify + _options = options.dup() + _options[:project] = options[:project].dup() if !options[:project].nil? + _options[:mixin] = [] + options[:mixin].each {|mixin| _options[:mixin] << mixin.dup() } + + _options[:verbosity] = options[:debug] ? VERBOSITY_DEBUG : nil + + @handler.environment( ENV, @app_cfg, _options ) + end + + desc "examples", "List available example projects" + long_desc <<-LONGDESC + `ceedling examples` lists the names of the example projects that come packaged with Ceedling. + + The output of this list is most useful when used by the `ceedling example` (no ‘s’) command + to extract an example project to your filesystem. + LONGDESC + def examples() + @handler.list_examples( CEEDLING_EXAMPLES_PATH ) + end + + + desc "example NAME [DEST]", "Create named example project in optional DEST path" + method_option :local, :type => :boolean, :default => false, :desc => DOC_LOCAL_FLAG + method_option :docs, :type => :boolean, :default => false, :desc => DOC_DOCS_FLAG + method_option :debug, :type => :boolean, :default => false, :hide => true + long_desc <<-LONGDESC + `ceedling example` extracts the named example project from within Ceedling to + your filesystem. + + NAME is required to specify the example to extract. A list of example projects + is available with the `examples` command. NAME will be the containing directory + for the extracted project. + + DEST is an optional directory path in which to place the example project (ex: + /). The default desintation is your working directory. If the + containing path does not exist, it will be created. + + Optional Flags: + + • #{LONGDOC_LOCAL_FLAG} + + • #{LONGDOC_DOCS_FLAG} + + NOTE: `example` is destructive. If the destination path is a previoulsy created + example project, `ceedling example` will forcibly overwrite the contents. + LONGDESC + def example(name, dest=nil) + # Get unfrozen copies so we can add / modify + _options = options.dup() + _dest = dest.dup() if !dest.nil? + + _options[:verbosity] = options[:debug] ? VERBOSITY_DEBUG : nil + + @handler.create_example( CEEDLING_ROOT, CEEDLING_EXAMPLES_PATH, _options, name, _dest ) + end + + + desc "version", "Display version details for Ceedling components" + # No long_desc() needed + def version() + @handler.version() + end + + end +end diff --git a/bin/cli_handler.rb b/bin/cli_handler.rb new file mode 100755 index 00000000..5213c9d1 --- /dev/null +++ b/bin/cli_handler.rb @@ -0,0 +1,342 @@ +require 'ceedling/constants' # From Ceedling application + +class CliHandler + + constructor :configinator, :projectinator, :cli_helper, :path_validator, :actions_wrapper, :logger + + # Override to prevent exception handling from walking & stringifying the object variables. + # Object variables are lengthy and produce a flood of output. + def inspect + return this.class.name + end + + def setup() + # Aliases + @helper = @cli_helper + @actions = @actions_wrapper + end + + + # Thor application help + Rake help (if available) + def app_help(env, app_cfg, options, command, &thor_help) + @helper.set_verbosity( options[:verbosity] ) + + # If help requested for a command, show it and skip listing build tasks + if !command.nil? + # Block handler + @logger._print( '🌱 Application ' ) + thor_help.call( command ) if block_given? + return + end + + # Display Thor-generated help listing + @logger._print( '🌱 Application ' ) + thor_help.call( command ) if block_given? + + # If it was help for a specific command, we're done + return if !command.nil? + + # If project configuration is available, also display Rake tasks + @path_validator.standardize_paths( options[:project], *options[:mixin], ) + return if !@projectinator.config_available?( filepath:options[:project], env:env ) + + list_rake_tasks( env:env, app_cfg:app_cfg, filepath:options[:project], mixins:options[:mixin] ) + end + + + # Public to be used by `-T` ARGV hack handling + def rake_help( env:, app_cfg:) + @helper.set_verbosity() # Default to normal + + list_rake_tasks( env:env, app_cfg:app_cfg ) + end + + + def new_project(ceedling_root, options, name, dest) + @helper.set_verbosity( options[:verbosity] ) + + @path_validator.standardize_paths( dest ) + + # If destination is nil, reassign it to name + # Otherwise, join the destination and name into a new path + dest = dest.nil? ? ('./' + name) : File.join( dest, name ) + + # Check for existing project (unless --force) + if @helper.project_exists?( dest, :|, DEFAULT_PROJECT_FILENAME, 'src', 'test' ) + msg = "It appears a project already exists at #{dest}/. Use --force to destroy it and create a new project." + raise msg + end unless options[:force] + + # Blow away any existing directories and contents if --force + @actions.remove_dir( dest ) if options[:force] + + # Create blank directory structure + ['.', 'src', 'test', 'test/support'].each do |path| + @actions._empty_directory( File.join( dest, path) ) + end + + # Vendor the tools and install command line helper scripts + @helper.vendor_tools( ceedling_root, dest ) if options[:local] + + # Copy in documentation + @helper.copy_docs( ceedling_root, dest ) if options[:docs] + + # Copy / set up project file + @helper.create_project_file( ceedling_root, dest, options[:local] ) if options[:configs] + + # Copy Git Ignore file + if options[:gitsupport] + @actions._copy_file( File.join(ceedling_root,'assets','default_gitignore'), File.join(dest,'.gitignore'), :force => true ) + @actions._touch_file( File.join(dest, 'test/support', '.gitkeep') ) + end + + @logger.log( "\n🌱 New project '#{name}' created at #{dest}/\n" ) + end + + + def upgrade_project(ceedling_root, options, path) + @path_validator.standardize_paths( path, options[:project] ) + + # Check for existing project + if !@helper.project_exists?( path, :&, options[:project], 'vendor/ceedling/lib/ceedling.rb' ) + msg = "Could not find an existing project at #{path}/." + raise msg + end + + project_filepath = File.join( path, options[:project] ) + _, config = @projectinator.load( filepath:project_filepath, silent:true ) + + if (@helper.which_ceedling?( config ) == 'gem') + msg = "Project configuration specifies the Ceedling gem, not vendored Ceedling" + raise msg + end + + # Recreate vendored tools + vendor_path = File.join( path, 'vendor', 'ceedling' ) + @actions.remove_dir( vendor_path ) + @helper.vendor_tools( ceedling_root, path ) + + # Recreate documentation if we find docs/ subdirectory + docs_path = File.join( path, 'docs' ) + founds_docs = @helper.project_exists?( path, :&, File.join( 'docs', 'CeedlingPacket.md' ) ) + if founds_docs + @actions.remove_dir( docs_path ) + @helper.copy_docs( ceedling_root, path ) + end + + @logger.log( "\n🌱 Upgraded project at #{path}/\n" ) + end + + + def build(env:, app_cfg:, options:{}, tasks:) + @helper.set_verbosity( options[:verbosity] ) + + @path_validator.standardize_paths( options[:project], options[:logfile], *options[:mixin] ) + + project_filepath, config = @configinator.loadinate( filepath:options[:project], mixins:options[:mixin], env:env ) + + default_tasks = @configinator.default_tasks( config:config, default_tasks:app_cfg[:default_tasks] ) + + @helper.process_testcase_filters( + config: config, + include: options[:test_case], + exclude: options[:exclude_test_case], + tasks: tasks, + default_tasks: default_tasks + ) + + log_filepath = @helper.process_logging( options[:log], options[:logfile] ) + + # Save references + app_cfg[:project_config] = config + app_cfg[:log_filepath] = log_filepath + app_cfg[:include_test_case] = options[:test_case] + app_cfg[:exclude_test_case] = options[:exclude_test_case] + + # Set graceful_exit from command line & configuration options + app_cfg[:tests_graceful_fail] = + @helper.process_graceful_fail( + config: config, + cmdline_graceful_fail: options[:graceful_fail], + tasks: tasks, + default_tasks: default_tasks + ) + + # Enable setup / operations duration logging in Rake context + app_cfg[:stopwatch] = @helper.process_stopwatch( tasks:tasks, default_tasks:default_tasks ) + + @helper.load_ceedling( + project_filepath: project_filepath, + config: config, + which: app_cfg[:which_ceedling], + default_tasks: default_tasks + ) + + # Hand Rake tasks off to be executed + @helper.run_rake_tasks( tasks ) + end + + + def dumpconfig(env, app_cfg, options, filepath, sections) + @helper.set_verbosity( options[:verbosity] ) + + @path_validator.standardize_paths( filepath, options[:project], *options[:mixin] ) + + project_filepath, config = @configinator.loadinate( filepath:options[:project], mixins:options[:mixin], env:env ) + + # Exception handling to ensure we dump the configuration regardless of config validation errors + begin + # If enabled, process the configuration through Ceedling automatic settings, defaults, plugins, etc. + if options[:app] + default_tasks = @configinator.default_tasks( config:config, default_tasks:app_cfg[:default_tasks] ) + + # Save references + app_cfg[:project_config] = config + + config = @helper.load_ceedling( + project_filepath: project_filepath, + config: config, + which: app_cfg[:which_ceedling], + default_tasks: default_tasks + ) + else + @logger.log( " > Skipped loading Ceedling application" ) + end + ensure + @helper.dump_yaml( config, filepath, sections ) + @logger.log( "\n🌱 Dumped project configuration to #{filepath}\n" ) + end + end + + + def environment(env, app_cfg, options) + @helper.set_verbosity( options[:verbosity] ) + + @path_validator.standardize_paths( options[:project], *options[:mixin] ) + + project_filepath, config = @configinator.loadinate( filepath:options[:project], mixins:options[:mixin], env:env ) + + # Save references + app_cfg[:project_config] = config + + config = @helper.load_ceedling( + project_filepath: project_filepath, + config: config, + which: app_cfg[:which_ceedling] + ) + + env_list = [] + + # Process external environment -- filter for Ceedling variables + env.each do |var, value| + next if !(var =~ /ceedling/i) + name = var.to_s + env_list << "#{name}: \"#{value}\"" + end + + # Process environment created by configuration + config[:environment].each do |env| + env.each_key do |key| + name = key.to_s + env_list << "#{name}: \"#{env[key]}\"" + end + end + + output = "\n🌱 Environment variables:\n" + + env_list.sort.each do |line| + output << " • #{line}\n" + end + + @logger.log( output + "\n") + end + + + def list_examples(examples_path) + examples = @helper.lookup_example_projects( examples_path ) + + raise( "No examples projects found") if examples.empty? + + output = "\n🌱 Available example projects:\n" + + examples.each {|example| output << " • #{example}\n" } + + @logger.log( output + "\n" ) + end + + + def create_example(ceedling_root, examples_path, options, name, dest) + @helper.set_verbosity( options[:verbosity] ) + + @path_validator.standardize_paths( dest ) + + examples = @helper.lookup_example_projects( examples_path ) + + if !examples.include?( name ) + raise( "No example project '#{name}' could be found" ) + end + + # If destination is nil, reassign it to name + # Otherwise, join the destination and name into a new path + dest = dest.nil? ? ('./' + name) : File.join( dest, name ) + + dest_src = File.join( dest, 'src' ) + dest_test = File.join( dest, 'test' ) + dest_project = File.join( dest, DEFAULT_PROJECT_FILENAME ) + + @actions._directory( "examples/#{name}/src", dest_src, :force => true ) + @actions._directory( "examples/#{name}/test", dest_test, :force => true ) + @actions._copy_file( "examples/#{name}/#{DEFAULT_PROJECT_FILENAME}", dest_project, :force => true ) + + # Vendor the tools and install command line helper scripts + @helper.vendor_tools( ceedling_root, dest ) if options[:local] + + # Copy in documentation + @helper.copy_docs( ceedling_root, dest ) if options[:docs] + + @logger.log( "\n🌱 Example project '#{name}' created at #{dest}/\n" ) + end + + + def version() + require 'ceedling/version' + version = <<~VERSION + 🌱 Ceedling => #{Ceedling::Version::CEEDLING} + CMock => #{Ceedling::Version::CMOCK} + Unity => #{Ceedling::Version::UNITY} + CException => #{Ceedling::Version::CEXCEPTION} + VERSION + @logger.log( version ) + end + + + ### Private ### + + private + + def list_rake_tasks(env:, app_cfg:, filepath:nil, mixins:[]) + project_filepath, config = + @configinator.loadinate( + filepath: filepath, + mixins: mixins, + env: env, + silent: true # Suppress project config load logging + ) + + # Save reference to loaded configuration + app_cfg[:project_config] = config + + @logger.log( "🌱 Build & Plugin Tasks:\n(Parameterized tasks tend to require enclosing quotes and/or escape sequences in most shells)" ) + + @helper.load_ceedling( + project_filepath: project_filepath, + config: config, + which: app_cfg[:which_ceedling], + default_tasks: app_cfg[:default_tasks], + silent: true + ) + + @helper.print_rake_tasks() + end + +end diff --git a/bin/cli_helper.rb b/bin/cli_helper.rb new file mode 100755 index 00000000..e064a76c --- /dev/null +++ b/bin/cli_helper.rb @@ -0,0 +1,404 @@ +require 'rbconfig' +require 'ceedling/constants' # From Ceedling application + +class CliHelper + + constructor :file_wrapper, :actions_wrapper, :config_walkinator, :path_validator, :logger + + def setup + #Aliases + @actions = @actions_wrapper + end + + + def project_exists?( path, op, *components ) + exists = [] + + components.each do |f| + _path = File.join( path, f ) + exists << (@file_wrapper.exist?( _path ) or @file_wrapper.directory?( _path )) + end + + return exists.reduce(op) + end + + + def create_project_file(ceedling_root, dest, local) + project_filepath = File.join( dest, DEFAULT_PROJECT_FILENAME ) + source_filepath = '' + + if local + source_filepath = File.join( ceedling_root, 'assets', 'project_with_guts.yml' ) + else + source_filepath = File.join( ceedling_root, 'assets', 'project_as_gem.yml' ) + end + + # Clone the project file and update internal version + require 'ceedling/version' + @actions._copy_file( source_filepath, project_filepath, :force => true) + @actions._gsub_file( project_filepath, /:ceedling_version:\s+'\?'/, ":ceedling_version: #{Ceedling::Version::CEEDLING}" ) + end + + + def which_ceedling?(config) + walked = @config_walkinator.fetch_value( config, :project, :which_ceedling ) + return 'gem' if walked[:value].nil? + return walked[:value] + end + + + def load_ceedling(project_filepath:, config:, which:, default_tasks:[], silent:false) + # Determine which Ceedling we're running + # 1. Copy the which value passed in (most likely a default determined in the first moments of startup) + # 2. If a :project ↳ :which_ceedling entry exists in the config, use it instead + _which = which.dup() + walked = @config_walkinator.fetch_value( config, :project, :which_ceedling ) + _which = walked[:value] if !walked[:value].nil? + + @path_validator.standardize_paths( _which ) + + # Load Ceedling from the gem + if (_which == 'gem') + require 'ceedling' + + # Load Ceedling from a path + else + # If a relative :which_ceedling, load in relation to project file location + if @file_wrapper.relative?( _which ) + project_path = File.dirname( project_filepath ) + ceedling_path = File.join( project_path, _which ) + ceedling_path = File.expand_path( ceedling_path ) + + if !@file_wrapper.directory?( ceedling_path ) + raise "Configuration value :project ↳ :which_ceedling => '#{_which}' points to a path relative to your project file that contains no Ceedling installation" + end + + # Otherwise, :which_ceedling is an absolute path + else + if !@file_wrapper.exist?( ceedling_path ) + raise "Configuration value :project ↳ :which_ceedling => '#{_which}' points to a path that contains no Ceedling installation" + end + end + + require( File.join( ceedling_path, '/lib/ceedling.rb' ) ) + @logger.log( " > Running Ceedling from #{ceedling_path}/" ) if !silent + end + + # Set default tasks + Rake::Task.define_task(:default => default_tasks) if !default_tasks.empty? + + # Load Ceedling + Ceedling.load_rakefile() + + # Processing the Rakefile in the preceeding line processes the config hash + return config + end + + + def process_testcase_filters(config:, include:, exclude:, tasks:, default_tasks:) + # Do nothing if no test case filters + return if (include.nil? || include.empty?) && (exclude.nil? || exclude.empty?) + + # TODO: When we can programmatically check if a task is a test task, + # raise an exception if --graceful-fail is set without test operations + + # Add test runner configuration setting necessary to use test case filters + walked = @config_walkinator.fetch_value( config, :test_runner ) + if walked[:value].nil? + # If no :test_runner section, create the whole thing + config[:test_runner] = {:cmdline_args => true} + else + # If a :test_runner section, just set :cmdlne_args + walked[:value][:cmdline_args] = true + end + end + + + def process_graceful_fail(config:, cmdline_graceful_fail:, tasks:, default_tasks:) + # TODO: When we can programmatically check if a task is a test task, + # raise an exception if --graceful-fail is set without test operations + + # Precedence + # 1. Command line option + # 2. Configuration entry + + # If command line option was set, use it + return cmdline_graceful_fail if !cmdline_graceful_fail.nil? + + # If configuration contains :graceful_fail, use it + walked = @config_walkinator.fetch_value( config, :test_build, :graceful_fail ) + return walked[:value] if !walked[:value].nil? + + return false + end + + + def process_logging(enabled, filepath) + # No log file if neither enabled nor a specific filename/filepath + return '' if !enabled && (filepath.nil? || filepath.empty?()) + + # Default logfile name (to be placed in default location) if enabled but no filename/filepath + return DEFAULT_CEEDLING_LOGFILE if enabled && filepath.empty?() + + # Otherwise, a filename/filepath was provided that implicitly enables logging + dir = File.dirname( filepath ) + + # Ensure logging directory path exists + if not dir.empty? + @file_wrapper.mkdir( dir ) + end + + # Return filename/filepath + return filepath + end + + + def process_stopwatch(tasks:, default_tasks:) + _tasks = tasks.empty?() ? default_tasks.dup() : tasks.dup() + + # Namespace-less (clobber, clean, etc.), files:, and paths: tasks should not have stopwatch logging + # 1. Filter out tasks lacking a namespace + # 2. Look for any tasks other than paths: or files: + _tasks.select! {|t| t.include?( ':') } + _tasks.reject! {|t| t =~ /(^files:|^paths:)/} + + return !_tasks.empty? + end + + def print_rake_tasks() + Rake.application.standard_exception_handling do + # (This required digging into Rake internals a bit.) + Rake.application.define_singleton_method(:name=) {|n| @name = n} + Rake.application.name = 'ceedling' + Rake.application.options.show_tasks = :tasks + Rake.application.options.show_task_pattern = /^(?!.*build).*$/ + Rake.application.display_tasks_and_comments() + end + end + + + def run_rake_tasks(tasks) + Rake.application.standard_exception_handling do + Rake.application.collect_command_line_tasks( tasks ) + Rake.application.top_level() + end + end + + + # Set global consts for verbosity and debug + def set_verbosity(verbosity=nil) + verbosity = verbosity.nil? ? Verbosity::NORMAL : VERBOSITY_OPTIONS[verbosity.to_sym()] + + # Create global constant PROJECT_VERBOSITY + Object.module_eval("PROJECT_VERBOSITY = verbosity") + PROJECT_VERBOSITY.freeze() + + # Create global constant PROJECT_DEBUG + debug = (verbosity == Verbosity::DEBUG) + Object.module_eval("PROJECT_DEBUG = debug") + PROJECT_DEBUG.freeze() + end + + + def dump_yaml(config, filepath, sections) + # Default to dumping entire configuration + _config = config + + # If sections were provided, process them + if !sections.empty? + # Symbolify section names + _sections = sections.map {|section| section.to_sym} + + # Try to extract subconfig from section path + walked = @config_walkinator.fetch_value( config, *_sections ) + + # If we fail to find the section path, blow up + if walked[:value].nil? + # Reformat list of symbols to list of :
s + _sections.map! {|section| ":#{section.to_s}"} + msg = "Cound not find configuration section #{_sections.join(' ↳ ')}" + raise(msg) + end + + # Update _config to subconfig with final sections path element as container + _config = { _sections.last => walked[:value] } + end + + File.open( filepath, 'w' ) {|out| YAML.dump( _config, out )} + end + + + def lookup_example_projects(examples_path) + examples = [] + + # Examples directory listing glob + glob = File.join( examples_path, '*' ) + + @file_wrapper.directory_listing(glob).each do |path| + # Skip anything that's not a directory + next if !@file_wrapper.directory?( path ) + + # Split the directory path into elements, indexing the last one + project = (path.split( File::SEPARATOR ))[-1] + + examples << project + end + + return examples + end + + + def copy_docs(ceedling_root, dest) + docs_path = File.join( dest, 'docs' ) + + # Hash that will hold documentation copy paths + # - Key: (modified) destination documentation path + # - Value: source path + doc_files = {} + + # Add docs to list from Ceedling (docs/) and supporting projects (docs/) + { # Source path => docs/ destination path + 'docs' => '.', + 'vendor/unity/docs' => 'unity', + 'vendor/cmock/docs' => 'cmock', + 'vendor/c_exception/docs' => 'c_exception' + }.each do |src, dest| + # Form glob to collect all markdown files + glob = File.join( ceedling_root, src, '*.md' ) + # Look up markdown files + listing = @file_wrapper.directory_listing( glob ) # Already case-insensitive + # For each markdown filepath, add to hash + listing.each do |filepath| + # Reassign destination + _dest = File.join( dest, File.basename(filepath) ) + doc_files[ _dest ] = filepath + end + end + + # Add docs to list from Ceedling plugins (docs/plugins) + glob = File.join( ceedling_root, 'plugins/**/README.md' ) + listing = @file_wrapper.directory_listing( glob ) # Already case-insensitive + listing.each do |path| + # 'README.md' => '.md' where name extracted from containing path + rename = path.split(/\\|\//)[-2] + '.md' + # For each Ceedling plugin readme, add to hash + dest = File.join( 'plugins', rename ) + doc_files[ dest ] = path + end + + # Add licenses from Ceedling (docs/) and supporting projects (docs/) + { # Destination path => Source path + '.' => '.', # Ceedling + 'unity' => 'vendor/unity', + 'cmock' => 'vendor/cmock', + 'c_exception' => 'vendor/c_exception', + }.each do |dest, src| + glob = File.join( ceedling_root, src, 'license.txt' ) + # Look up licenses (use glob as capitalization can be inconsistent) + listing = @file_wrapper.directory_listing( glob ) # Already case-insensitive + # Safety check on nil references since we explicitly reference first element + next if listing.empty? + filepath = listing.first + # Reassign dest + dest = File.join( dest, File.basename( filepath ) ) + doc_files[ dest ] = filepath + end + + # Copy all documentation + doc_files.each_pair do |dest, src| + @actions._copy_file(src, File.join( docs_path, dest ), :force => true) + end + end + + + def vendor_tools(ceedling_root, dest) + vendor_path = File.join( dest, 'vendor', 'ceedling' ) + assets_path = File.join( ceedling_root, 'assets' ) + + # Copy folders from current Ceedling into project + %w{plugins lib bin mixins}.each do |folder| + @actions._directory( + File.join( ceedling_root, folder ), + File.join( vendor_path, folder ), + :force => true + ) + end + + # Mark ceedling as an executable + @actions._chmod( File.join( vendor_path, 'bin', 'ceedling' ), 0755 ) unless windows? + + # Assembly necessary subcomponent dirs + components = [ + 'vendor/c_exception/lib/', + 'vendor/cmock/config/', + 'vendor/cmock/lib/', + 'vendor/cmock/src/', + 'vendor/diy/lib/', + 'vendor/unity/auto/', + 'vendor/unity/src/', + ] + + # Copy necessary subcomponent dirs into project + components.each do |path| + _src = File.join( ceedling_root, path ) + _dest = File.join( vendor_path, path ) + @actions._directory( _src, _dest, :force => true ) + end + + # Add licenses from Ceedling and supporting projects + license_files = {} + [ # Source paths + '.', # Ceedling + 'vendor/unity', + 'vendor/cmock', + 'vendor/c_exception', + ].each do |src| + glob = File.join( ceedling_root, src, 'license.txt' ) + + # Look up licenses (use glob as capitalization can be inconsistent) + listing = @file_wrapper.directory_listing( glob ) # Already case-insensitive + + # Safety check on nil references since we explicitly reference first element + next if listing.empty? + + # Add license copying to hash + license = listing.first + filepath = File.join( vendor_path, src, File.basename( license ) ) + license_files[ filepath ] = license + end + + # Copy license files into place + license_files.each_pair do |dest, src| + @actions._copy_file( src, dest, :force => true) + end + + # Create executable helper scripts in project root + if windows? + # Windows command prompt launch script + @actions._copy_file( + File.join( assets_path, 'ceedling.cmd'), + File.join( dest, 'ceedling.cmd'), + :force => true + ) + else + # Unix shell launch script + launch = File.join( dest, 'ceedling') + @actions._copy_file( + File.join( assets_path, 'ceedling'), + launch, + :force => true + ) + @actions._chmod( launch, 0755 ) + end + end + + ### Private ### + + private + +def windows? + return ((RbConfig::CONFIG['host_os'] =~ /mswin|mingw/) ? true : false) if defined?( RbConfig ) + return ((Config::CONFIG['host_os'] =~ /mswin|mingw/) ? true : false) +end + +end diff --git a/bin/configinator.rb b/bin/configinator.rb new file mode 100755 index 00000000..54d01e8f --- /dev/null +++ b/bin/configinator.rb @@ -0,0 +1,105 @@ +require 'deep_merge' + +class Configinator + + MIXINS_BASE_PATH = File.join( CEEDLING_ROOT, 'mixins' ) + + constructor :config_walkinator, :projectinator, :mixinator + + def loadinate(filepath:nil, mixins:[], env:{}, silent:false) + # Aliases for clarity + cmdline_filepath = filepath + cmdline_mixins = mixins || [] + + # Load raw config from command line, environment variable, or default filepath + project_filepath, config = @projectinator.load( filepath:cmdline_filepath, env:env, silent:silent ) + + # Extract cfg_enabled_mixins mixins list plus load paths list from config + cfg_enabled_mixins, cfg_load_paths = @projectinator.extract_mixins( + config: config, + mixins_base_path: MIXINS_BASE_PATH + ) + + # Get our YAML file extension + yaml_ext = @projectinator.lookup_yaml_extension( config:config ) + + # Remove any silly redundancies + cfg_enabled_mixins.uniq! + # Use absolute path to ensure proper deduplication + cfg_load_paths.uniq! { |path| File.expand_path(path) } + cmdline_mixins.uniq! + + # Validate :cfg_load_paths from :mixins section of project configuration + @projectinator.validate_mixin_load_paths( cfg_load_paths ) + + # Validate enabled mixins from :mixins section of project configuration + if not @projectinator.validate_mixins( + mixins: cfg_enabled_mixins, + load_paths: cfg_load_paths, + source: 'Config :mixins ↳ :enabled =>', + yaml_extension: yaml_ext + ) + raise 'Project configuration file section :mixins failed validation' + end + + # Validate command line mixins + if not @projectinator.validate_mixins( + mixins: cmdline_mixins, + load_paths: cfg_load_paths, + source: 'Mixin', + yaml_extension: yaml_ext + ) + raise 'Command line failed validation' + end + + # Find mixins from project file among load paths + # Return ordered list of filepaths + config_mixins = @projectinator.lookup_mixins( + mixins: cfg_enabled_mixins, + load_paths: cfg_load_paths, + yaml_extension: yaml_ext + ) + + # Find mixins from command line among load paths + # Return ordered list of filepaths + cmdline_mixins = @projectinator.lookup_mixins( + mixins: cmdline_mixins, + load_paths: cfg_load_paths, + yaml_extension: yaml_ext + ) + + # Fetch CEEDLING_MIXIN_# environment variables + # Sort into ordered list of hash tuples [{env variable => filepath}...] + env_mixins = @mixinator.fetch_env_filepaths( env ) + @mixinator.validate_env_filepaths( env_mixins ) + + # Eliminate duplicate mixins and return list of mixins in merge order + # [{source => filepath}...] + mixins_assembled = @mixinator.assemble_mixins( + config: config_mixins, + env: env_mixins, + cmdline: cmdline_mixins + ) + + # Merge mixins + @mixinator.merge( config:config, mixins:mixins_assembled, silent:silent ) + + return project_filepath, config + end + + def default_tasks(config:, default_tasks:) + # 1. If :default_tasks set in config, use it + # 2. Otherwise use the function argument (most likely a default set in the first moments of startup) + walked = @config_walkinator.fetch_value( config, :project, :default_tasks ) + if walked[:value] + # Update method parameter to config value + default_tasks = walked[:value].dup() + else + # Set key/value in config if it's not set + config.deep_merge( {:project => {:default_tasks => default_tasks}} ) + end + + return default_tasks + end + +end \ No newline at end of file diff --git a/bin/logger.rb b/bin/logger.rb new file mode 100755 index 00000000..6749c6a5 --- /dev/null +++ b/bin/logger.rb @@ -0,0 +1,12 @@ + +class Logger + + def _print(str) + print( str ) + end + + def log(str) + puts( str ) + end + +end \ No newline at end of file diff --git a/bin/main.rb b/bin/main.rb new file mode 100755 index 00000000..0a2193e1 --- /dev/null +++ b/bin/main.rb @@ -0,0 +1,70 @@ +require 'cli' # Located alongside this file in CEEDLING_BIN +require 'constructor' # Assumed installed via Ceedling gem dependencies +require 'app_cfg' # Located alongside this file in CEEDLING_BIN + +CEEDLING_APPCFG = get_app_cfg() + + +# Entry point +begin + # Construct all bootloader objects + # 1. Add full path to $LOAD_PATH to simplify objects.yml + # 2. Add vendored DIY to $LOAD_PATH so we can use it + # 3. Require DIY (used by Ceedling application too) + # 4. Perform object construction + dependency injection from bin/objects.yml + # 5. Remove unneeded / potentially problematic paths from $LOAD_PATH + $LOAD_PATH.unshift( CEEDLING_LIB ) + $LOAD_PATH.unshift( File.join(CEEDLING_VENDOR, 'diy/lib') ) + + require 'diy' + objects = DIY::Context.from_yaml( File.read( File.join( CEEDLING_BIN, 'objects.yml' ) ) ) + objects.build_everything() + + $LOAD_PATH.delete( CEEDLING_BIN ) # Loaded in top-level `ceedling` script + $LOAD_PATH.delete( CEEDLING_LIB ) + + # Keep a copy of the command line for edge case CLI hacking (Thor consumes ARGV) + _ARGV = ARGV.clone + + # + # NOTE: See comment block in cli.rb to understand CLI handling + # ------------------------------------------------------------ + # + + # Backwards compatibility command line hack to silently preserve Rake `-T` CLI handling + if (ARGV.size() == 1 and ARGV[0] == '-T') + # Call Rake task listing handler w/ default handling of project file and mixins + objects[:cli_handler].rake_help( env:ENV, app_cfg:CEEDLING_APPCFG ) + + # Run command line args through Thor (including "naked" Rake tasks) + else + CeedlingTasks::CLI.start( ARGV, + { + :app_cfg => CEEDLING_APPCFG, + :objects => objects, + } + ) + end + +# Handle case of Thor application CLI failing to handle command line arguments. +rescue Thor::UndefinedCommandError + # Marrying Thor & Rake command line handling creates a gap (see comments in CLI handling). + # If a user enters only Rake build tasks at the command line followed by Thor flags, + # our Thor configuration doesn't see those flags. + # We catch the exception of unrecognized Thor commands here (i.e. any "naked" Rake tasks), + # and try again by forcing the Thor `build` command at the beginning of the command line. + # This way, our Thor handling will process option flags and properly pass the Rake tasks + # along as well. + CeedlingTasks::CLI.start( _ARGV.unshift( 'build' ), + { + :app_cfg => CEEDLING_APPCFG, + :objects => objects, + } + ) + +# Bootloader boom handling +rescue StandardError => e + $stderr.puts( "\n🌱 ERROR: #{e.message}" ) + $stderr.puts( e.backtrace ) if ( defined?( PROJECT_DEBUG ) and PROJECT_DEBUG ) + exit(1) +end diff --git a/bin/mixinator.rb b/bin/mixinator.rb new file mode 100755 index 00000000..cd39063b --- /dev/null +++ b/bin/mixinator.rb @@ -0,0 +1,108 @@ +require 'deep_merge' + +class Mixinator + + constructor :path_validator, :yaml_wrapper, :logger + + def setup + # ... + end + + def validate_cmdline_filepaths(paths) + validated = @path_validator.validate( + paths: paths, + source: 'Filepath argument', + ) + + if !validated + raise 'Mixins command line failed validation' + end + end + + def fetch_env_filepaths(env) + var_names = [] + + env.each do |var, filepath| + # Explicitly ignores CEEDLING_MIXIN_0 + var_names << var if var =~ /CEEDLING_MIXIN_[1-9]\d*/ + end + + # Extract numeric string (guranteed to exist) and convert to integer for ascending sorting + var_names.sort_by! {|name| name.match(/\d+$/)[0].to_i() } + + _vars = [] + # Iterate over sorted environment variable names + var_names.each do |name| + # Duplicate the filepath string to get unfrozen copy + # Handle any Windows path shenanigans + # Insert in array {env var name => filepath} + path = env[name].dup() + @path_validator.standardize_paths( path ) + _vars << {name => path} + end + + # Remove any duplicate filepaths by comparing the full absolute path + # Higher numbered environment variables removed + _vars.uniq! {|entry| File.expand_path( entry.values.first )} + + return _vars + end + + def validate_env_filepaths(vars) + validated = true + + vars.each do |entry| + validated &= @path_validator.validate( + paths: [entry.values.first], + source: "Environment variable `#{entry.keys.first}` filepath", + ) + end + + if !validated + raise 'Mixins environment variables failed validation' + end + end + + def assemble_mixins(config:, env:, cmdline:) + assembly = [] + + # Build list of hashses to facilitate deduplication + cmdline.each {|filepath| assembly << {'command line' => filepath}} + assembly += env + config.each {|filepath| assembly << {'project configuration' => filepath}} + + # Remove duplicates inline + # 1. Expand filepaths to absolute paths for correct deduplication + # 2. Remove duplicates + assembly.uniq! {|entry| File.expand_path( entry.values.first )} + + # Return the compacted list (in merge order) + return assembly + end + + def merge(config:, mixins:, silent:) + mixins.each do |mixin| + source = mixin.keys.first + filepath = mixin.values.first + + _mixin = @yaml_wrapper.load( filepath ) + + # Hnadle an empty mixin (it's unlikely but logically coherent) + _mixin = {} if _mixin.nil? + + # Sanitize the mixin config by removing any :mixins section (these should not end up in merges) + _mixin.delete(:mixins) + + # Merge this bad boy + config.deep_merge( _mixin ) + + # Log what filepath we used for this mixin + @logger.log( " + Merged #{'(empty) ' if _mixin.empty?}#{source} mixin using #{filepath}" ) if !silent + end + + # Validate final configuration + msg = "Final configuration is empty" + raise msg if config.empty? + end + +end \ No newline at end of file diff --git a/bin/objects.yml b/bin/objects.yml new file mode 100755 index 00000000..78664b4b --- /dev/null +++ b/bin/objects.yml @@ -0,0 +1,55 @@ + +# Loaded from ceedling/lib +file_wrapper: + +# Loaded from ceedling/lib +yaml_wrapper: + +# Loaded from ceedling/lib +config_walkinator: + +actions_wrapper: + +logger: + +# Separation of logic from CLI user interface +cli_handler: + compose: + - configinator + - projectinator + - cli_helper + - path_validator + - actions_wrapper + - logger + +cli_helper: + compose: + - file_wrapper + - config_walkinator + - path_validator + - actions_wrapper + - logger + +path_validator: + compose: + - file_wrapper + - logger + +mixinator: + compose: + - path_validator + - yaml_wrapper + - logger + +projectinator: + compose: + - file_wrapper + - path_validator + - yaml_wrapper + - logger + +configinator: + compose: + - config_walkinator + - projectinator + - mixinator diff --git a/bin/path_validator.rb b/bin/path_validator.rb new file mode 100755 index 00000000..68a5ec8c --- /dev/null +++ b/bin/path_validator.rb @@ -0,0 +1,42 @@ + +class PathValidator + + constructor :file_wrapper, :logger + + def validate(paths:, source:, type: :filepath) + validated = true + + paths.each do |path| + # Error out on empty paths + if path.empty? + validated = false + @logger.log( "ERROR: #{source} contains an empty path" ) + next + end + + # Error out if path is not a directory / does not exist + if (type == :directory) and !@file_wrapper.directory?( path ) + validated = false + @logger.log( "ERROR: #{source} '#{path}' does not exist as a directory in the filesystem" ) + end + + # Error out if filepath does not exist + if (type == :filepath) and !@file_wrapper.exist?( path ) + validated = false + @logger.log( "ERROR: #{source} '#{path}' does not exist in the filesystem" ) + end + end + + return validated + end + + # Ensure any Windows backslashes are converted to Ruby path forward slashes + # Santization happens inline + def standardize_paths( *paths ) + paths.each do |path| + next if path.nil? or path.empty? + path.gsub!( "\\", '/' ) + end + end + +end \ No newline at end of file diff --git a/bin/projectinator.rb b/bin/projectinator.rb new file mode 100755 index 00000000..a6b4782b --- /dev/null +++ b/bin/projectinator.rb @@ -0,0 +1,211 @@ +require 'ceedling/constants' # From Ceedling application + +class Projectinator + + PROJECT_FILEPATH_ENV_VAR = 'CEEDLING_PROJECT_FILE' + DEFAULT_PROJECT_FILEPATH = './' + DEFAULT_PROJECT_FILENAME + DEFAULT_YAML_FILE_EXTENSION = '.yml' + + constructor :file_wrapper, :path_validator, :yaml_wrapper, :logger + + # Discovers project file path and loads configuration. + # Precendence of attempts: + # 1. Explcit flepath from argument + # 2. Environment variable + # 3. Default filename in working directory + # Returns: + # - Absolute path of project file found and used + # - Config hash loaded from project file + def load(filepath:nil, env:{}, silent:false) + # Highest priority: command line argument + if filepath + config = load_filepath( filepath, 'from command line argument', silent ) + return File.expand_path( filepath ), config + + # Next priority: environment variable + elsif env[PROJECT_FILEPATH_ENV_VAR] + filepath = env[PROJECT_FILEPATH_ENV_VAR] + @path_validator.standardize_paths( filepath ) + config = load_filepath( + filepath, + "from environment variable `#{PROJECT_FILEPATH_ENV_VAR}`", + silent + ) + return File.expand_path( filepath ), config + + # Final option: default filepath + elsif @file_wrapper.exist?( DEFAULT_PROJECT_FILEPATH ) + filepath = DEFAULT_PROJECT_FILEPATH + config = load_filepath( filepath, "at default location", silent ) + return File.expand_path( filepath ), config + + # If no user provided filepath and the default filepath does not exist, + # we have a big problem + else + raise "No project filepath provided and default #{DEFAULT_PROJECT_FILEPATH} not found" + end + + # We'll never get here but return nil/empty for completeness + return nil, {} + end + + + # Determine if project configuration is available. + # - Simplest, default case simply tries to load default project file location. + # - Otherwise, attempts to load a filepath, the default environment variable, + # or both can be specified. + def config_available?(filepath:nil, env:{}) + available = true + + begin + load(filepath:filepath, env:env, silent:true) + rescue + available = false + end + + return available + end + + + def lookup_yaml_extension(config:) + return DEFAULT_YAML_FILE_EXTENSION if config[:extension].nil? + + return DEFAULT_YAML_FILE_EXTENSION if config[:extension][:yaml].nil? + + return config[:extension][:yaml] + end + + + # Pick apart a :mixins projcet configuration section and return components + # Layout mirrors :plugins section + def extract_mixins(config:, mixins_base_path:) + # Check if our base path exists + mixins_base_path = nil unless File.directory?(mixins_base_path) + + # Get mixins config hash + _mixins = config[:mixins] + + # If no :mixins section, return: + # - Empty enabled list + # - Load paths with only the built-in Ceedling mixins/ path + return [], [mixins_base_path].compact if _mixins.nil? + + # Build list of load paths + # Configured load paths are higher in search path ordering + load_paths = _mixins[:load_paths] || [] + load_paths += [mixins_base_path].compact # += forces a copy of configuration section + + # Get list of mixins + enabled = _mixins[:enabled] || [] + enabled = enabled.clone # Ensure it's a copy of configuration section + + # Remove the :mixins section of the configuration + config.delete( :mixins ) + + return enabled, load_paths + end + + + # Validate :load_paths from :mixins section in project configuration + def validate_mixin_load_paths(load_paths) + validated = @path_validator.validate( + paths: load_paths, + source: 'Config :mixins ↳ :load_paths', + type: :directory + ) + + if !validated + raise 'Project configuration file section :mixins failed validation' + end + end + + + # Validate mixins list + def validate_mixins(mixins:, load_paths:, source:, yaml_extension:) + validated = true + + mixins.each do |mixin| + # Validate mixin filepaths + if !File.extname( mixin ).empty? or mixin.include?( File::SEPARATOR ) + if !@file_wrapper.exist?( mixin ) + @logger.log( "ERROR: Cannot find mixin at #{mixin}" ) + validated = false + end + + # Otherwise, validate that mixin name can be found among the load paths + else + found = false + load_paths.each do |path| + if @file_wrapper.exist?( File.join( path, mixin + yaml_extension) ) + found = true + break + end + end + + if !found + @logger.log( "ERROR: #{source} '#{mixin}' cannot be found in the mixin load paths as '#{mixin + yaml_extension}'" ) + validated = false + end + end + end + + return validated + end + + + # Yield ordered list of filepaths + def lookup_mixins(mixins:, load_paths:, yaml_extension:) + filepaths = [] + + # Fill results hash with mixin name => mixin filepath + # Already validated, so we know the mixin filepath exists + mixins.each do |mixin| + # Handle explicit filepaths + if !File.extname( mixin ).empty? or mixin.include?( File::SEPARATOR ) + filepaths << mixin + + # Find name in load_paths (we already know it exists from previous validation) + else + load_paths.each do |path| + filepath = File.join( path, mixin + yaml_extension ) + if @file_wrapper.exist?( filepath ) + filepaths << filepath + break + end + end + end + end + + return filepaths + end + + ### Private ### + + private + + def load_filepath(filepath, method, silent) + begin + # Load the filepath we settled on as our project configuration + config = @yaml_wrapper.load( filepath ) + + # A blank configuration file is technically an option (assuming mixins are merged) + # Redefine config as empty hash + config = {} if config.nil? + + # Log what the heck we loaded + @logger.log( "🌱 Loaded #{'(empty) ' if config.empty?}project configuration #{method} using #{filepath}" ) if !silent + + return config + rescue Errno::ENOENT + # Handle special case of user-provided blank filepath + filepath = filepath.empty?() ? '' : filepath + raise "Could not find project filepath #{filepath} #{method}" + + rescue StandardError => e + # Catch-all error handling + raise "Error loading project filepath #{filepath} #{method}: #{e.message}" + end + + end + +end \ No newline at end of file diff --git a/ceedling.gemspec b/ceedling.gemspec index b63ac3cb..20245170 100644 --- a/ceedling.gemspec +++ b/ceedling.gemspec @@ -49,7 +49,7 @@ Ceedling projects are created with a YAML configuration file. A variety of conve s.files += Dir['**/*'] s.test_files = Dir['test/**/*', 'spec/**/*', 'features/**/*'] - s.executables = Dir['bin/**/*'].map{|f| File.basename(f)} + s.executables = ['ceedling'] # bin/ceedling s.require_paths = ["lib", "vendor/cmock/lib"] end diff --git a/docs/BreakingChanges.md b/docs/BreakingChanges.md index 25c88876..6c6646d7 100644 --- a/docs/BreakingChanges.md +++ b/docs/BreakingChanges.md @@ -1,11 +1,15 @@ +# 🌱 Ceedling Breaking Changes -# 💔 Breaking Changes for 0.32 Release Candidate +These breaking changes are complemented by two other documents: -**Version:** 0.32 pre-release incremental build +1. 🔊 **[Release Notes](ReleaseNotes.md)** for announcements, education, acknowledgements, and known issues. +1. 🪵 **[Changelog](Changelog.md)** for a structured list of additions, fixes, changes, and removals. -**Date:** February 26, 2024 +--- -# Explicit `:paths` ↳ `:include` entries in the project file +# [1.0.0 pre-release] — 2024-04-02 + +## Explicit `:paths` ↳ `:include` entries in the project file The `:paths` ↳ `:include` entries in the project file must now be explicit and complete. @@ -18,19 +22,19 @@ This behavior is no more. Why? For two interrelated reasons. Using 0.32 Ceedling with older project files can lead to errors when generating mocks or compiler errors on finding header files. Add all relevant header file search paths to the `:paths` ↳ `:include` project file entry to fix this problem. -# Format change for `:defines` in the project file +## Format change for `:defines` in the project file To better support per-test-executable configurations, the format of `:defines` has changed. See the [official documentation](CeedlingPacket.md) for specifics. In brief: 1. A more logically named hierarchy differentiates `#define`s for test preprocessing, test compilation, and release compilation. -1. Previously, compilation symbols could be specified for a specific C file by name, but these symbols were only defined when compiling that specific file. Further, this matching was only against a file's full name. Now, pattern matching is also an option. +1. Previously, compilation symbols could be specified for a specific C file by name, but these symbols were only defined when compiling that specific file. Further, this matching was only against a file’s full name. Now, pattern matching is also an option. 1. Filename matching for test compilation symbols happens against _only test file names_. More importantly, the configured symbols are applied in compilation of each C file that comprises a test executable. Each test executable is treated as a mini-project. Symbols specified for release builds are applied to all files in the release build. -# Format change for `:flags` in the project file +## Format change for `:flags` in the project file To better support per-test-executable configurations, the format and function of `:flags` has changed somewhat. See the [official documentation](CeedlingPacket.md) for specifics. @@ -41,11 +45,11 @@ In brief: Flags specified for release builds are applied to all files in the release build. -# `TEST_FILE()` ➡️ `TEST_SOURCE_FILE()` +## `TEST_FILE()` ➡️ `TEST_SOURCE_FILE()` The previously undocumented `TEST_FILE()` build directive macro (#796) available within test files has been renamed and is now officially documented. See earlier section on this. -# Quoted executables in tool definitions +## Quoted executables in tool definitions While unusual, some executables have names with spaces. This is more common on Windows than Unix derivatives, particularly with proprietary compiler toolchains. @@ -61,17 +65,17 @@ Automatic quoting has been removed. If you need a quoted executable, simply expl :executable: \"Code Cranker\" ``` -# Build output directory structure changes +## Build output directory structure changes -## Test builds +### Test builds Each test is now treated as its own mini-project. Differentiating components of the same name that are a part of multiple test executables required further subdirectories in the build directory structure. Generated mocks, compiled object files, linked executables, and preprocessed output all end up one directory deeper than in previous versions of Ceedling. In each case, these files are found inside a subdirectory named for their containing test. -## Release builds +### Release builds 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. -# Changes to global constants & accessors +## 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. @@ -79,17 +83,17 @@ Similarly, various global constant project file accessors have changed, specific See the [official documentation](CeedlingPacket.md) on global constants & accessors for updated lists and information. -# `raw_output_report` plugin +## `raw_output_report` plugin This plugin (renamed -- see next section) no longer generates empty log files and no longer generates log files with _test_ and _pass_ in their filenames. Log files are now simply named `.raw.log`. -# Consolidation of test report generation plugins ➡️ `report_tests_log_factory` +## Consolidation of test report generation plugins ➡️ `report_tests_log_factory` The individual `json_tests_report`, `xml_tests_report`, and `junit_tests_report` plugins are superseded by a single plugin `report_tests_log_factory` able to generate each or all of the previous test reports as well as an HTML report and user-defined tests reports. The new plugin requires a small amount of extra configuration the previous individual plugins did not. See the [`report_tests_log_factory` documentation](../plugins/report_tests_log_factory). In addition, all references and naming connected to the previous `xml_tests_report` plugin have been updated to refer to _CppUnit_ rather than generic _XML_ as this is the actual format of the report that is processed. -# Built-in Plugin Name Changes +## Built-in Plugin Name Changes The following plugin names must be updated in the `:plugins` ↳ `:enabled` list of your Ceedling project file. @@ -108,7 +112,7 @@ Some test report generation plugins were not simply renamed but superseded by a - `warnings_report` ➡️ `report_build_warnings_log` - `test_suite_reporter` ➡️ `report_tests_log_factory` -# `gcov` plugin coverage report generation name and behavior changes +## `gcov` plugin coverage report generation name and behavior changes The `gcov` plugin and its [documentation](../plugins/gcov) has been significantly revised. See [release notes](ReleaseNotes.md) for all the details. @@ -121,6 +125,35 @@ Coverage reports are now generated automatically unless the manual report genera :report_task: TRUE ``` +## Exit code handling (a.k.a. `:graceful_fail`) + +Be default Ceedling terminates with an exit code of `1` when a build succeeds but unit tests fail. + +A previously undocumented project configuration option `:graceful_fail` could force a Ceedling exit code of `0` upon test failures. + +This configuration option has moved (and is now [documented](CeedlingPacket.md)). + +Previously: +```yaml +:graceful_fail: TRUE +``` + +Now: +```yaml +:test_build: + :graceful_fail: TRUE +``` + +## Project file environment variable name change `CEEDLING_MAIN_PROJECT_FILE` ➡️ `CEEDLING_PROJECT_FILE` + +Options and support for loading a project configuration have expanded significantly, mostly notably with the addition of Mixins. + +The environment variable option for pointing Ceedling to a project file other than _project.yml_ in your working directory has been renamed `CEEDLING_MAIN_PROJECT_FILE` ➡️ `CEEDLING_PROJECT_FILE`. + +In addition, a previously undocumented feature for merging a second configuration via environment variable `CEEDLING_USER_PROJECT_FILE` has been removed. This feature has been superseded by the new Mixins functionality. + +Thorough documentation on Mixins and the new options for loading a project configuration can be found in _[CeedlingPacket](CeedlingPacket.md))_. + # Subprojects Plugin Replaced diff --git a/docs/CeedlingPacket.md b/docs/CeedlingPacket.md index 5faac1fc..2e023435 100644 --- a/docs/CeedlingPacket.md +++ b/docs/CeedlingPacket.md @@ -44,7 +44,7 @@ Once you have Ceedling installed and a project file, Ceedling tasks go like this * `ceedling test:all`, or * `ceedling release`, or, if you fancy, -* `ceedling clobber verbosity:obnoxious test:all gcov:all release` +* `ceedling --verbosity=obnoxious clobber test:all gcov:all release` ## Quick Start Documentation @@ -57,7 +57,7 @@ Once you have Ceedling installed and a project file, Ceedling tasks go like this [quick-start-1]: #ceedling-installation--set-up [quick-start-2]: #commented-sample-test-file [quick-start-3]: #simple-sample-project-file -[quick-start-4]: #now-what-how-do-i-make-it-go +[quick-start-4]: #now-what-how-do-i-make-it-go-the-command-line [quick-start-5]: #the-almighty-project-configuration-file-in-glorious-yaml
@@ -104,9 +104,9 @@ It's just all mixed together. This one is pretty self explanatory. -1. **[Now What? How Do I Make It _GO_?][packet-section-7]** +1. **[Now What? How Do I Make It _GO_? The Command Line.][packet-section-7]** - Ceedling’s many command line tasks and some of the rules about using them. + Ceedling’s command line. 1. **[Important Conventions & Behaviors][packet-section-8]** @@ -119,24 +119,33 @@ It's just all mixed together. together several key tools and frameworks. Those can require configuration of their own. Ceedling facilitates this. -1. **[The Almighty Ceedling Project Configuration File (in Glorious YAML)][packet-section-10]** +1. **[How to Load a Project Configuration. You Have Options, My Friend.][packet-section-10]** + + You can use a command line flag, an environment variable, or rely on a default + file in your working directory to load your base configuration. + + Once your base project configuration is loaded, you have **_Mixins_** for merging + additional configuration for different build scenarios as needed via command line, + environment variable, and/or your project configuration file. + +1. **[The Almighty Ceedling Project Configuration File (in Glorious YAML)][packet-section-11]** This is the exhaustive documentation for all of Ceedling’s project file configuration options — from project paths to command line tools to plugins and much, much more. -1. **[Build Directive Macros][packet-section-11]** +1. **[Build Directive Macros][packet-section-12]** These code macros can help you accomplish your build goals When Ceedling’s conventions aren't enough. -1. **[Ceedling Plugins][packet-section-12]** +1. **[Ceedling Plugins][packet-section-13]** Ceedling is extensible. It includes a number of built-in plugins for code coverage, test report generation, continuous integration reporting, test file scaffolding generation, sophisticated release builds, and more. -1. **[Global Collections][packet-section-13]** +1. **[Global Collections][packet-section-14]** Ceedling is built in Ruby. Collections are globally available Ruby lists of paths, files, and more that can be useful for advanced customization of a Ceedling project @@ -148,13 +157,14 @@ It's just all mixed together. [packet-section-4]: #commented-sample-test-file [packet-section-5]: #anatomy-of-a-test-suite [packet-section-6]: #ceedling-installation--set-up -[packet-section-7]: #now-what-how-do-i-make-it-go +[packet-section-7]: #now-what-how-do-i-make-it-go-the-command-line [packet-section-8]: #important-conventions--behaviors [packet-section-9]: #using-unity-cmock--cexception -[packet-section-10]: #the-almighty-ceedling-project-configuration-file-in-glorious-yaml -[packet-section-11]: #build-directive-macros -[packet-section-12]: #ceedling-plugins -[packet-section-13]: #global-collections +[packet-section-10]: #how-to-load-a-project-configuration-you-have-options-my-friend +[packet-section-11]: #the-almighty-ceedling-project-configuration-file-in-glorious-yaml +[packet-section-12]: #build-directive-macros +[packet-section-13]: #ceedling-plugins +[packet-section-14]: #global-collections --- @@ -681,12 +691,16 @@ to be reported to the developer at the command line. ## Incidentally, Ceedling comes with example projects -If you run Ceedling without a project file (that is, from a working directory -with no project file present), you can generate entire example projects. +Ceedling comes with entire example projects you can extract. -- `ceedling examples` to list available example projects -- `ceedling example [destination]` to generate the - named example project +1. Execute `ceedling examples` in your terminal to list available example + projects. +1. Execute `ceedling example [destination]` to extract the + named example project. + +You can inspect the _project.yml_ file and source & test code. Run +`ceedling help` from the root of the example projects to see what you can +do, or just go nuts with `ceedling test:all`.
@@ -799,12 +813,6 @@ are completed once, only step 3 is needed for each new project. `:environment` ↳ `:path` in your project file (see `:environment` section later in this document). -1. To use a project file name other than the default `project.yml` - or place the project file in a directory other than the one - in which you'll run Rake, create an environment variable - `CEEDLING_MAIN_PROJECT_FILE` with your desired project - file path. - 1. To better understand Rake conventions, Rake execution, and Rakefiles, consult the [Rake tutorial, examples, and user guide](http://rubyrake.org/). @@ -816,42 +824,126 @@ are completed once, only step 3 is needed for each new project.
-# Now What? How Do I Make It _GO_? +# Now What? How Do I Make It _GO_? The Command Line. We're getting a little ahead of ourselves here, but it's good context on how to drive this bus. Everything is done via the command -line. We'll cover conventions and how to actually configure +line. We'll cover project conventions and how to actually configure your project in later sections. -To run tests, build your release artifact, etc., you will be interacting -with Rake under the hood on the command line. Ceedling works with Rake -to present you with named tasks that coordinate the file generation and -build steps needed to accomplish something useful. You can also -add your own independent Rake tasks or create plugins to extend -Ceedling (more on this later). +For now, let's talk about the command line. + +To run tests, build your release artifact, etc., you will be using the +trusty command line. Ceedling is transitioning away from being built +around Rake. As such, right now, interacting with Ceedling at the +command line involves two different conventions: + +1. **Application Commands.** Application commands tell Ceedling what to + to do with your project. These create projects, load project files, + begin builds, output version information, etc. These include rich + help and operate similarly to popular command line tools like `git`. +1. **Build & Plugin Tasks.** Build tasks actually execute test suites, + run release builds, etc. These tasks are created from your project + file. These are generated through Ceedling's Rake-based code and + conform to its conventions — simplistic help, no option flags, but + bracketed arguments. + +In the case of running builds, both come into play at the command line. + +## Quick command line example to get you started + +To exercise the Ceedling command line quickly, follow these steps after +[installing Ceedling](#ceedling-installation--set-up): -## Ceedling command line tasks +1. Open a terminal and chnage directories to a location suitable for + an example project. +1. Execute `ceedling example temp_sensor` in your terminal. +1. Change directories into the new _temp_sensor/_ directory. +1. Execute `ceedling test:all` in your terminal. +1. Take a look at the build and test suite console output as well as + the _project.yml_ file in the root of the example project. + +## Ceedling application commands + +Ceedling provides robust command line help for application commands. +Execute `ceedling help` for a summary view of all application commands. +Execute `ceedling help ` for detailed help. + +_Note:_ Because the built-in command line help is thorough, we will only briefly +list and explain the available application commands. * `ceedling [no arguments]`: - Run the default Rake task (conveniently recognized by the name default - by Rake). Neither Rake nor Ceedling provide a default task. Rake will - abort if run without arguments when no default task is defined. You can - conveniently define a default task in the Rakefile discussed in the - preceding setup & installation section of this document. + Runs the default build tasks. Unless set in the project file, Ceedling + uses a default task of `test:all`. To override this behavior, set your + own default tasks in the project file (see later section). + +* `ceedling build ` or `ceedling `: + + Runs the named build tasks. `build` is optional. Various option flags + exist to control what project configuration is loaded, verbosity + levels, logging, etc. See next section for build tasks. Of note, + this application command provides optional test case filters using + traditional option flags (ex. `--test-case=`) whose contents + are provided to Rake test tasks behind the scenes. -* `ceedling -T`: +* `ceedling dumpconfig`: - List all available Rake tasks with descriptions (Rake tasks without - descriptions are not listed). -T is a command line switch for Rake and - not the same as tasks that follow. + Process project configuration and write final result to a YAML file. + Various option flags exist to control what project configuration is + loaded. * `ceedling environment`: - List all configured environment variable names and string values. This - task is helpful in verifying the evaluation of any Ruby expressions in - the `:environment` section of your config file. *Note: Ceedling may - set some convenience environment variables by default.* + Lists project related environment variables: + + * All configured environment variable names and string values added to + your environment from within Ceedling and through the `:environment` + section of your configuration. This is especially helpful in + verifying the evaluation of any string replacement expressions in + your config entries. + * All existing Ceedling-related environment variables set before you + ran Ceedling from the command line. + +* `ceedling example`: + + Extracts an example project from within Ceedling to your local + filesystem. The available examples are listed with + `ceedling examples`. Various option flags control whether the example + contains vendored Ceedling and/or a documentation bundle. + +* `ceedling examples`: + + Lists the available examples within Ceedling. To extract an example, + use `ceedling example`. + +* `ceedling help`: + + Displays summary help for all application commands and detailed help + for each command. `ceedling help` also loads your project + configuration (if available) and lists all build tasks from it. + Various option flags control what project configuration is loaded. + +* `ceedling new`: + + Creates a new project structure. Various option flags control whether + the new project contains vendored Ceedling, a documentation bundle, + and/or a starter project configuration file. + +* `ceedling upgrade`: + + Upgrade vendored installation of Ceedling for an existing project + along with any locally installed documentation bundles. + +* `ceedling version`: + + Displays version information for Ceedling and its components. + +## Ceedling build tasks + +Build task are loaded from your project configuration. Unlike +application commands that are fixed, build tasks vary depending on your +project configuration and the files within your project structure. * `ceedling paths:*`: @@ -869,23 +961,12 @@ Ceedling (more on this later). List all files and file counts collected from the relevant search paths specified by the `:paths` entries of your YAML config file. The `files:assembly` task will only be available if assembly support is - enabled in the `:release_build` section of your configuration file. - -* `ceedling options:*`: - - Load and merge configuration settings into the main project - configuration. Each task is named after a `*.yml` file found in the - configured options directory. See documentation for the configuration - setting `:project` ↳ `:options_paths` and for options files in advanced - topics. + enabled in the `:release_build` or `:test_build` sections of your + configuration file. * `ceedling test:all`: - Run all unit tests (rebuilding anything that's changed along the way). - -* `ceedling test:build_only`: - - Build all unit tests, object files and executable but not run them. + Run all unit tests. * `ceedling test:*`: @@ -893,32 +974,10 @@ Ceedling (more on this later). accompanying test. No path. Examples: `ceedling test:foo`, `ceedling test:foo.c` or `ceedling test:test_foo.c` -* `ceedling test:pattern[*]`: - - Execute any tests whose name and/or path match the regular expression - pattern (case sensitive). Example: `ceedling "test:pattern[(I|i)nit]"` will - execute all tests named for initialization testing. Note: quotes may - be necessary around the ceedling parameter to distinguish regex characters - from command line operators. - -* `ceedling test:path[*]`: - - Execute any tests whose path contains the given string (case - sensitive). Example: `ceedling test:path[foo/bar]` will execute all tests - whose path contains foo/bar. Note: both directory separator characters - / and \ are valid. +* `ceedling test:* --test-case= ` + Execute individual test cases which match `test_case_name`. -* `ceedling test:* --test_case= ` - Execute test cases which do not match **`test_case_name`**. This option - is available only after setting `:cmdline_args` to `true` under - `:test_runner` in the project file: - - ```yaml - :test_runner: - :cmdline_args: true - ``` - - For instance, if you have a test file test_gpio.c containing the following + For instance, if you have a test file _test_gpio.c_ containing the following test cases (test cases are simply `void test_name(void)`: - `test_gpio_start` @@ -927,24 +986,16 @@ Ceedling (more on this later). … and you want to run only _configure_ tests, you can call: - `ceedling test:gpio --test_case=configure` + `ceedling test:gpio --test-case=configure` **Test case matching notes** - Test case matching is on sub-strings. `--test_case=configure` matches on + * Test case matching is on sub-strings. `--test_case=configure` matches on the test cases including the word _configure_, naturally. - `--test_case=gpio` would match all three test cases. - + `--test-case=gpio` would match all three test cases. * `ceedling test:* --exclude_test_case= ` - Execute test cases which do not match **`test_case_name`**. This option - is available only after setting `:cmdline_args` to `true` under - `:test_runner` in the project file: - - ```yaml - :test_runner: - :cmdline_args: true - ``` + Execute test cases which do not match `test_case_name`. For instance, if you have file test_gpio.c with defined 3 tests: @@ -958,9 +1009,28 @@ Ceedling (more on this later). **Test case exclusion matching notes** - Exclude matching follows the same sub-string logic as discussed in the + * Exclude matching follows the same sub-string logic as discussed in the preceding section. +* `ceedling test:pattern[*]`: + + Execute any tests whose name and/or path match the regular expression + pattern (case sensitive). Example: `ceedling "test:pattern[(I|i)nit]"` + will execute all tests named for initialization testing. + + _Note:_ Quotes are likely necessary around the regex characters or + entire task to distinguish characters from shell command line operators. + +* `ceedling test:path[*]`: + + Execute any tests whose path contains the given string (case + sensitive). Example: `ceedling test:path[foo/bar]` will execute all tests + whose path contains foo/bar. _Notes:_ + + 1. Both directory separator characters `/` and `\` are valid. + 1. Quotes may be necessary around the task to distinguish the parameter's + characters from shell command line operators. + * `ceedling release`: Build all source into a release artifact (if the release build option @@ -976,61 +1046,6 @@ Ceedling (more on this later). Sometimes you just need to assemble a single file doggonit. Example: `ceedling release:assemble:foo.s` -* `ceedling module:create[Filename]`: -* `ceedling module:create[Filename]`: - - It's often helpful to create a file automatically. What's better than - that? Creating a source file, a header file, and a corresponding test - file all in one step! - - There are also patterns which can be specified to automatically generate - a bunch of files. Try `ceedling module:create[Poodles,mch]` for example! - - The module generator has several options you can configure. - F.e. Generating the source/header/test file in a sub-directory (by adding - when calling `module:create`). For more info, refer to the - [Module Generator][#module-generator] section. - -* `ceedling module:stub[Filename]`: -* `ceedling module:stub[Filename]`: - - So what happens if you've created your API in your header (maybe even using - TDD to do so?) and now you need to start to implement the corresponding C - module? Why not get a head start by using `ceedling module:stub[headername]` - to automatically create a function skeleton for every function declared in - that header? Better yet, you can call this again whenever you add new functions - to that header to add just the new functions, leaving the old ones alone! - -* `ceedling logging `: - - Enable logging to /logs. Must come before test and release - tasks to log their steps and output. Log names are a concatenation of - project, user, and option files loaded. User and option files are - documented in another section. - -* `ceedling verbosity[x] `: - - Change default verbosity level. `[x]` ranges from `0` (quiet) to `4` - (obnoxious) with `5` reserved for debugging output. Level `3` is the - default. - - The verbosity task must precede all tasks in the command line task list - for which output is desired to be seen. Verbosity settings are generally - most meaningful in conjunction with test and release tasks. - -* `ceedling verbosity: `: - - Alternative verbosity task scheme using the name of each level. - - | Numeric Level | Named Level | - |---------------|---------------------| - | verbosity[0] | verbosity:silent | - | verbosity[1] | verbosity:errors | - | verbosity[2] | verbosity:warnings | - | verbosity[3] | verbosity:normal | - | verbosity[4] | verbosity:obnoxious | - | verbosity[5] | verbosity:debug | - * `ceedling summary`: If plugins are enabled, this task will execute the summary method of @@ -1050,31 +1065,11 @@ Ceedling (more on this later). runners, mocks, preprocessor output. Clobber produces no output at the command line unless verbosity has been set to an appreciable level. -* `ceedling options:export`: - - This allows you to export a snapshot of your current tool configuration - as a yaml file. You can specify the name of the file in brackets `[blah.yml]` - or let it default to `tools.yml`. In either case, the contents of the file - can be used as the tool configuration for your project if desired, and - modified as you wish. - ## Ceedling Command Line Tasks, Extra Credit -### Rake - -To better understand Rake conventions, Rake execution, and -Rakefiles, consult the [Rake tutorial, examples, and user guide][rake-guide]. - -[rake-guide]: http://rubyrake.org/ - -### File Tasks Are Not Advertised - -Individual test and release file tasks are not listed in `-T` output. -Because so many files may be present it's unwieldy to list them all. - ### Combining Tasks At the Command Line -Multiple Rake tasks can be executed at the command line. +Multiple build tasks can be executed at the command line. For example, `ceedling clobber test:all release` will remove all generated files; @@ -1082,10 +1077,8 @@ build and run all tests; and then build all source — in that order. If any task fails along the way, execution halts before the next task. -Task order is executed as provided and can be important! This is a -limitation of Rake. For instance, you won't get much useful information -from executing `ceedling test:foo 'verbosity[4]'`. Instead, you -probably want `ceedling 'verbosity[4]' test:foo`. +Task order is executed as provided and can be important! Running +`clobber` after a `test:` or `release:` task will not accomplish much. ### Build Directory and Revision Control @@ -1256,6 +1249,35 @@ A test case function signature must have these elements: In other words, a test function signature should look like this: `void test(void)`. +### Preprocessing behavior for tests + +Ceedling and CMock are advanced tools that both perform fairly sophisticated +parsing. + +However, neither of these tools fully understand the entire C language, +especially C's preprocessing statements. + +If your test files rely on macros and `#ifdef` conditionals, there's a good +chance that Ceedling will break on trying to process your test files, or, +alternatively, your test suite will not execute as expected. + +Similarly, generating mocks of header files with macros and `#ifdef` +conditionals can get weird. + +Ceedling includes an optional ability to preprocess test files and header files +before executing any operations on them. See the `:project` ↳ +`:use_test_preprocessor`). That is, Ceedling will expand preprocessor +statements in test files before generating test runners from them and will +expand preprocessor statements in header files before generating mocks from +them. + +This ability uses `gcc`'s preprocessing mode and the `cpp` preprocessor tool to +strip down / expand test files and headers to their applicable content which +can then be processed by Ceedling and CMock. They must be in your search path +if Ceedling’s preprocessing is enabled. Further, Ceedling’s features are +directly tied to these tools' abilities and options. They should not be +redefined for other toolchains. + ### Execution time (duration) reporting in Ceedling operations & test suites #### Ceedling’s logged run times @@ -1267,14 +1289,14 @@ files, build an internal representation of your project, etc. This duration does not capture the time necessary to load the Ruby runtime itself. ``` -Ceedling set up completed in 223 milliseconds +🌱 Ceedling set up completed in 223 milliseconds ``` Secondly, each Ceedling run also logs the time necessary to run all the tasks you specify at the command line. ``` -Ceedling operations completed in 1.03 seconds +🌱 Ceedling operations completed in 1.03 seconds ``` #### Ceedling test suite and Unity test executable run durations @@ -1400,7 +1422,8 @@ all test case statistics. ### Ceedling Exit Codes -In its default configuration, Ceedling produces an exit code of `1`: +In its default configuration, Ceedling terminates with an exit code +of `1`: * On any build error and immediately terminates upon that build error. @@ -1414,13 +1437,13 @@ upon either build errors or test failures. If this exit code convention for test failures does not work for you, no problem-o. You may be of the mind that running a test suite to completion should yield a successful exit code (even if tests failed). -Add the following at the top-level of your project file (i.e. all the -way to the left — not nested) to force Ceedling to finish a build -with an exit code of 0 even upon test case failures. +Add the following to your project file to force Ceedling to finish a +build with an exit code of 0 even upon test case failures. ```yaml -# Ceedling will terminate with happy `exit(0)` even if test cases fail -:graceful_fail: true +# Ceedling terminates with happy `exit(0)` even if test cases fail +:test_build: + :graceful_fail: true ``` If you use the option for graceful failures in CI, you'll want to @@ -1634,6 +1657,371 @@ if you wish to use it in your project.
+# How to Load a Project Configuration. You Have Options, My Friend. + +Ceedling needs a project configuration to accomplish anything for you. +Ceedling's project configuration is a large in-memory data structure. +That data structure is loaded from a human-readable file format called +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. + +## Overview of Project Configuration Loading & Smooshing + +Ceedling has a certain pipeline for loading and manipulating the +configuration it uses to build your projects. It goes something like +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. + +Ceedling provides reasonably verbose logging at startup telling you which +configuration files were used and in what order they were merged. + +## Options for Loading Your Base Project Configuration + +You have three options for telling Ceedling what single base project +configuration to load. These options are ordered below according to their +precedence. If an option higher in the list is present, it is used. + +1. Command line option flags +1. Environment variable +1. Default file in working directory + +### `--project` command line flags + +Many of Ceedling's [application commands][packet-section-7] include an +optional `--project` flag. When provided, Ceedling will load as its base +configuration the YAML filepath provided. + +Example: `ceedling --project=my/path/build.yml test:all` + +_Note:_ Ceedling loads any relative paths within your configuration in +relation to your working directory. This can cause a disconnect between +configuration paths, working directory, and the path to your project +file. + +If the filepath does not exist, Ceedling terminates with an error. + +### Environment variable `CEEDLING_PROJECT_FILE` + +If a `--project` flag is not used at the command line, but the +environment variable `CEEDLING_PROJECT_FILE` is set, Ceedling will use +the path it contains to load your project configuration. The path can +be absolute or relative (to your working directory). + +If the filepath does not exist, Ceedling terminates with an error. + +### Default _project.yml_ in your working directory + +If neither a `--project` command line flag nor the environment variable +`CEEDLING_PROJECT_FILE` are set, then Ceedling tries to load a file +named _project.yml_ in your working directory. + +If this file does not exist, Ceedling terminates with an error. + +## Applying Mixins to Your Base Project Configuration + +Once you have a base configuation loaded, you may want to modify it for +any number of reasons. Some example scenarios: + +* A single project actually contains mutiple build variations. You would + like to maintain a common configuration that is shared among build + variations. +* Your repository contains the configuration needed by your Continuous + Integration server setup, but this is not fun to run locally. You would + like to modify the configuration locally with sources external to your + repository. +* Ceedling's default `gcc` tools do not work for your project needs. You + would like the complex tooling configurations you most often need to + be maintained separately and shared among projects. + +Mixins allow you to merge configuration with your project configuration +just after the base project file is loaded. The merge is so low-level +and generic that you can, in fact, load an empty base configuration +and merge in entire project configurations through mixins. + +## Mixins Example Plus Merging Rules + +Let’s start with an example that also explains how mixins are merged. +Then, the documentation sections that follow will discuss everything +in detail. + +### Mixins Example: Scenario + +In this example, we will load a base project configuration and then +apply three mixins using each of the available means — command line, +envionment variable, and `:mixins` section in the base project +configuration file. + +#### Example environment variable + +`CEEDLING_MIXIN_1` = `./env.yml` + +#### Example command line + +`ceedling --project=base.yml --mixin=support/mixins/cmdline.yml ` + +_Note:_ The `--mixin` flag supports more than filepaths and can be used +multiple times in the same command line for multiple mixins (see later +documentation section). + +The example command line above will produce the following logging output. + +``` +🌱 Loaded project configuration from command line argument using base.yml + + Merged command line mixin using support/mixins/cmdline.yml + + Merged CEEDLING_MIXIN_1 mixin using ./env.yml + + Merged project configuration mixin using ./enabled.yml +``` + +_Notes_ + +* The logging output above referencing _enabled.yml_ comes from the + `:mixins` section within the base project configuration file provided below. +* The resulting configuration in this example is missing settings required + by Ceedling. This will cause a validation build error that is not shown + here. + +### Mixins Example: Configuration files + +#### _base.yml_ — Our base project configuration file + +Our base project configuration file: + +1. Sets up a configuration file-baesd mixin. Ceedling will look for a mixin + named _enabled_ in the specified load paths. In this simple configuration + that means Ceedling looks for and merges _support/mixins/enabled.yml_. +1. Creates a `:project` section in our configuration. +1. Creates a `:plugins` section in our configuration and enables the standard + console test report output plugin. + +```yaml +:mixins: # `:mixins` section only recognized in base project configuration + :enabled: # `:enabled` list supports names and filepaths + - enabled # Ceedling looks for name as enabled.yml in load paths and merges if found + :load_paths: + - support/mixins + +:project: + :build_root: build/ + +:plugins: + :enabled: + - report_tests_pretty_stdout +``` + +#### _support/mixins/cmdline.yml_ — Mixin via command line filepath flag + +This mixin will merge a `:project` section with the existing `:project` +section from the base project file per the deep merge rules (noted after +the examples). + +```yaml +:project: + :use_test_preprocessor: TRUE + :test_file_prefix: Test +``` + +#### _env.yml_ — Mixin via environment variable filepath + +This mixin will merge a `:plugins` section with the existing `:plugins` +section from the base project file per the deep merge rules (noted +after the examples). + +```yaml +:plugins: + :enabled: + - compile_commands_json_db +``` + +#### _support/mixins/enabled.yml_ — Mixin via base project configuration file `:mixins` section + +This mixin listed in the base configuration project file will merge +`:project` and `:plugins` sections with those that already exist from +the base configuration plus earlier mixin merges per the deep merge +rules (noted after the examples). + +```yaml +:project: + :use_test_preprocessor: FALSE + +:plugins: + :enabled: + - gcov +``` + +### Mixins Example: Resulting project configuration + +Behold the project configuration following mixin merges: + +```yaml +:project: + :build_root: build/ # From base.yml + :use_test_preprocessor: TRUE # Value in support/mixins/cmdline.yml overwrote value from support/mixins/enabled.yml + :test_file_prefix: Test # Added to :project from support/mixins/cmdline.yml + +:plugins: + :enabled: # :plugins ↳ :enabled from two mixins merged with oringal list in base.yml + - report_tests_pretty_stdout # From base.yml + - compile_commands_json_db # From env.yml + - gcov # From support/mixins/enabled.yml + +# Note: Original :mixins section is filtered out of resulting config +``` + +### Mixins deep merge rules + +Mixins are merged in a specific order. See the next documentation +sections for details. + +Smooshing of mixin configurations into the base project configuration +follows a few basic rules: + +* If a configuration key/value pair does not already exist at the time + of merging, it is added to the configuration. +* If a simple value — e.g. boolean, string, numeric — already exists + at the time of merging, that value is replaced by the value being + merged in. +* If a container — e.g. list or hash — already exists at the time of a + merge, the contents are _combined_. In the case of lists, merged + values are added to the end of the existing list. + +## Options for Loading Mixins + +You have three options for telling Ceedling what mixins to load. These +options are ordered below according to their precedence. A Mixin higher +in the list is merged earlier. In addition, options higher in the list +force duplicate mixin filepaths to be ignored lower in the list. + +Unlike base project file loading that resolves to a single filepath, +multiple mixins can be specified using any or all of these options. + +1. Command line option flags +1. Environment variables +1. Base project configuration file entries + +### `--mixin` command line flags + +As already discussed above, many of Ceedling's application commands +include an optional `--project` flag. Most of these same commands +also recognize optional `--mixin` flags. Note that `--mixin` can be +used multiple times in a single command line. + +When provided, Ceedling will load the specified YAML file and merge +it with the base project configuration. + +A Mixin flag can contain one of two types of values: + +1. A filename or filepath to a mixin yaml file. A filename contains + a file extension. A filepath includes a leading directory path. +1. A simple name (no file extension and no path). This name is used + as a lookup in Ceedling's mixin load paths. + +Example: `ceedling --project=build.yml --mixin=foo --mixin=bar/mixin.yaml test:all` + +Simple mixin names (#2 above) require mixin load paths to search. +A default mixin load path is always in the list and points to within +Ceedling itself (in order to host eventual built-in mixins like +built-in plugins). User-specified load paths must be added through +the `:mixins` section of the base configuration project file. See +the [documentation for the `:mixins` section of your project +configuration][mixins-config-section] for more details. + +Order of precedence is set by the command line mixin order +left-to-right. + +Filepaths may be relative (in relation to the working directory) or +absolute. + +If the `--mixin` filename or filepath does not exist, Ceedling +terminates with an error. If Ceedling cannot find a mixin name in +any load paths, it terminates with an error. + +[mixins-config-section]: #base-project-configuration-file-mixins-section-entries + +### Mixin environment variables + +Mixins can also be loaded through environment variables. Ceedling +recognizes environment variables with a naming scheme of +`CEEDLING_MIXIN_#`, where `#` is any number greater than 0. + +Precedence among the environment variables is a simple ascending +sort of the trailing numeric value in the environment variable name. +For example, `CEEDLING_MIXIN_5` will be merged before +`CEEDLING_MIXIN_99`. + +Mixin environment variables only hold filepaths. Filepaths may be +relative (in relation to the working directory) or absolute. + +If the filepath specified by an environment variable does not exist, +Ceedling terminates with an error. + +### Base project configuration file `:mixins` section entries + +Ceedling only recognizes a `:mixins` section in your base project +configuration file. A `:mixins` section in a mixin is ignored. The +`:mixins` section of a base project configuration file is filtered +out of the resulting configuration. + +The `:mixins` configuration section contains two subsections. Both +are optional. + +* `:enabled` + + An optional array of mixin filenames/filepaths and/or mixin names: + + 1. A filename contains a file extension. A filepath includes a + leading directory path. The file content is YAML. + 1. A simple name (no file extension and no path) is used + as a lookup in Ceedling's mixin load paths. + + **Default**: `[]` + +* `:load_paths` + + Paths containing mixin files to be searched via mixin names. A mixin + filename in a load path has the form _.yml_ by default. If + an alternate filename extension has been specified in your project + configuration (`:extension` ↳ `:yaml`) it will be used for file + lookups in the mixin load paths instead of _.yml_. + + Searches start in the path at the top of the list and end in the + default internal mixin search path. + + Both mixin names in the `:enabled` list (above) and on the command + line via `--mixin` flag use this list of load paths for searches. + + **Default**: `[]` (This default is + always present as the last path in the `:load_paths` list) + +Example `:mixins` YAML blurb: + +```yaml +:mixins: + :enabled: + - foo # Ceedling looks for foo.yml in proj/mixins & support/ + - path/bar.yaml # Ceedling merges this file with base project conig + :load_paths: + - proj/mixins + - support +``` + +Relating the above example to command line `--mixin` flag handling: + +* A command line flag of `--mixin=foo` is equivalent to the `foo` + entry in the `:enabled` mixin configuration. +* A command line flag of `--mixin=path/bay.yaml` is equivalent to the + `path/bay.yaml` entry in the `:enabled` mixin configuration. +* Note that while command line `--mixin` flags work identifically to + entries in `:mixins` ↳ `:enabled`, they are merged first instead of + last in the mixin precedence. + # The Almighty Ceedling Project Configuration File (in Glorious YAML) ## Some YAML Learnin’ @@ -1761,6 +2149,13 @@ migrated to the `:test_build` and `:release_build` sections. **Default**: (none) +* `:default_tasks` + + An array of default build / plugin tasks Ceedling should execute if + none are provided at the command line. + + **Default**: ['test:all'] + * `:use_mocks` Configures the build environment to make use of CMock. Note that if @@ -1772,20 +2167,16 @@ migrated to the `:test_build` and `:release_build` sections. * `:use_test_preprocessor` This option allows Ceedling to work with test files that contain - conditional compilation statements (e.g. #ifdef) and header files you + conditional compilation statements (e.g. `#ifdef`) and header files you wish to mock that contain conditional preprocessor statements and/or macros. - Ceedling and CMock are advanced tools with sophisticated parsers. - However, they do not include entire C language preprocessors. - Consequently, with this option enabled, Ceedling will use `gcc`'s - preprocessing mode and the cpp preprocessor tool to strip down / - expand test files and headers to their applicable content which can - then be processed by Ceedling and CMock. + See the [documentation on test preprocessing][test-preprocessing] for more. With this option enabled, the `gcc` & `cpp` tools must exist in an - accessible system search path and test runner files are always - regenerated. + accessible system search path. + + [test-preprocessing]: #preprocessing-behavior-for-tests **Default**: FALSE @@ -1801,23 +2192,6 @@ migrated to the `:test_build` and `:release_build` sections. **Default**: "test_" -* `:options_paths` - - Just as you may have various build configurations for your source - codebase, you may need variations of your project configuration. - - By specifying options paths, Ceedling will search for other project - YAML files, make command line tasks available (ceedling options:variation - for a variation.yml file), and merge the project configuration of - these option files in with the main project file at runtime. See - advanced topics. - - Note these Rake tasks at the command line - like verbosity or logging - control - must come before the test or release task they are meant to - modify. - - **Default**: `[]` (empty) - * `:release_build` When enabled, a release Rake task is exposed. This configuration @@ -1924,8 +2298,8 @@ migrated to the `:test_build` and `:release_build` sections. code under test case causing the segmetation fault will be omitted from Coverage Report. The default debugger (gdb)[https://www.sourceware.org/gdb/] can be switched to other - debug engines via setting a new configuration under the tool node in project.yml. - By default, this tool is set as follows: + debug engines via setting a new configuration under the `:tools` node in your project + configuration. By default, this tool is set as follows: ```yaml :tools: @@ -1942,6 +2316,14 @@ migrated to the `:test_build` and `:release_build` sections. It is important that the debugging tool should be run as a background task, and with the option to pass additional arguments to the test executable. +## `:mixins` Configuring mixins to merge + +This section of a project configuration file is documented in the +[discussion of project files and mixins][mixins-config-section]. + +**_Note:_** A `:mixins` section is only recognized within a base project +configuration file. Any `:mixins` sections within mixin files are ignored. + ## `:test_build` Configuring a test build **_Note:_** In future versions of Ceedling, test-related settings presently @@ -2136,10 +2518,12 @@ the various path-related documentation sections. *

:paths:libraries

- Library search paths. See `:libraries` section. + Library search paths. [See `:libraries` section][libraries]. **Default**: `[]` (empty) + [libraries]: #libraries + *

:paths:<custom>

Any paths you specify for custom list. List is available to tool @@ -2622,8 +3006,8 @@ matchers and the simpler list format cannot be mixed for `:defines` ↳ `:test`. YAML key plus symbol list. Both are documented below. _Note:_ Left unspecified, `:preprocess` symbols default to be identical to `:test` - symbols. Override this behavior by adding `:defines` ↳ `:preprocess` flags. If you want - no additional flags for preprocessing regardless of `test` symbols, simply specify an + symbols. Override this behavior by adding `:defines` ↳ `:preprocess` symbols. If you want + no additional symbols for preprocessing regardless of `test` symbols, simply specify an empty list `[]`. **Default**: `[]` (empty) @@ -3198,34 +3582,6 @@ test file matchers. Please see the discussion in `:defines` for a complete example. -## `:import` Load additional project config files - -In some cases it is nice to have config files (project.yml, options files) which can -load other config files, for commonly re-used definitions (target processor, -common code modules, etc). - -These can be recursively nested, the included files can include other files. - -To import config files, either provide an array of files to import, or use hashes to set imports. The former is useful if you do not anticipate needing to replace a given file for different configurations (project: or options:). If you need to replace/remove imports based on different configuration files, use the hashed version. The two methods cannot be mixed in the same .yml. - -### Example `:import` YAML blurbs - -Using array: - -```yaml -:import: - - path/to/config.yml - - path/to/another/config.yml -``` - -Using hashes: - -```yaml -:import: - :configA: path/to/config.yml - :configB: path/to/another/config.yml -``` - ## `:cexception` Configure CException’s features * `:defines`: @@ -3380,10 +3736,104 @@ Example configuration: ## `:tools` Configuring command line tools used for build steps Ceedling requires a variety of tools to work its magic. By default, the GNU -toolchain (`gcc`, `cpp`, `as`) are configured and ready for use with no -additions to the project configuration YAML file. However, as most work will -require a project-specific toolchain, Ceedling provides a generic means for -specifying / overriding tools. +toolchain (`gcc`, `cpp`, `as` — and `gcov` via plugin) are configured and ready +for use with no additions to your project configuration YAML file. + +A few items before we dive in: + +1. Sometimes Ceedling’s built-in tools are _nearly_ what you need but not + quite. If you only need to add some arguments to all uses of tool's command + line, Ceedling offers a shortcut to do so. See the + [final section of the `:tools`][tool-args-shortcut] documentation for + details. +1. If you need fine-grained control of the arguments Ceedling uses in the build + steps for test executables, see the documentation for [`:flags`][flags]. + Ceedling allows you to control the command line arguments for each test + executable build — with a variety of pattern matching options. +1. If you need to link libraries — your own or standard options — please see + the [top-level `:libraries` section][libraries] available for your + configuration file. Ceedling supports a number of useful options for working + with pre-compiled libraries. If your library linking needs are super simple, + the shortcut in (1) might be the simplest option. + +[flags]: #flags-configure-preprocessing-compilation--linking-command-line-flags +[tool-args-shortcut]: #ceedling-tool-arguments-addition-shortcut + +### Ceedling tools for test suite builds + +Our recommended approach to writing and executing test suites relies on the GNU +toolchain. _*Yes, even for embedded system work on platforms with their own, +proprietary C toolchain.*_ Please see +[this section of documentation][sweet-suite] to understand this recommendation +among all your options. + +You can and sometimes must run a Ceedling test suite in an emulator or on +target, and Ceedling allows you to do this through tool definitions documented +here. Generally, you'll likely want to rely on the default definitions. + +[sweet-suite]: #all-your-sweet-sweet-test-suite-options + +### Ceedling tools for release builds + +More often than not, release builds require custom tool definitions. The GNU +toolchain is configured for Ceeding release builds by default just as with test +builds. You'll likely need your own definitions for `:release_compiler`, +`:release_linker`, and possibly `:release_assembler`. + +### Ceedling plugin tools + +Ceedling plugins are free to define their own tools that are loaded into your +project configuration at startup. Plugin tools are defined using the same +mechanisns as Ceedling’s built-in tools and are called the same way. That is, +all features available to you for working with tools as an end users are +generally available for working with plugin-based tools. This presumes a +plugin author followed guidance and convention in creating any command line +actions. + +### Ceedling tool definitions + +Contained in this section are details on Ceedling’s default tool definitions. +For sake of space, the entirety of a given definition is not shown. If you need +to get in the weeds or want a full example, see the file `defaults.rb` in +Ceedling’s lib/ directory. + +#### Tool definition overview + +Listed below are the built-in tool names, corresponding to build steps along +with the numbered parameters that Ceedling uses to fill out a full command line +for the named tool. The full list of fundamental elements for a tool definition +are documented in the sections that follow along with examples. + +Not every numbered parameter listed immediately below must be referenced in a +Ceedling tool definition. If `${4}` isn't referenced by your custom tool, +Ceedling simply skips it while expanding a tool definition into a command line. + +The numbered parameters below are references that expand / are replaced with +actual values when the corresponding command line is constructed. If the values +behind these parameters are lists, Ceedling expands the containing reference +multiple times with the contents of the value. A conceptual example is +instructive… + +#### Simplified tool definition / expansion example + +A partial tool definition: + +```yaml +:tools: + :power_drill: + :executable: dewalt.exe + :arguments: + - "--X${3}" +``` + +Let's say that `${3}` is a list inside Ceedling, `[2, 3, 7]`. The expanded tool +command line for `:tools` ↳ `:power_drill` would look like this: + +```shell + > dewalt.exe --X2 --X3 --X7 +``` + +#### Ceedling’s default build step tool definitions * `:test_compiler`: @@ -3466,7 +3916,7 @@ specifying / overriding tools. **Default**: `gcc` -### Tool configurable elements: +#### Tool defintion configurable elements 1. `:executable` - Command line executable (required). @@ -3485,7 +3935,7 @@ specifying / overriding tools. 1. `:name` - Simple name (i.e. "nickname") of tool beyond its executable name. This is optional. If not explicitly set - then Ceedling will form a name from the tool's YAML entry. + then Ceedling will form a name from the tool's YAML entry key. 1. `:stderr_redirect` - Control of capturing `$stderr` messages {`:none`, `:auto`, `:win`, `:unix`, `:tcsh`}. @@ -3498,7 +3948,7 @@ specifying / overriding tools. you can set this to `true` if it's not needed for testing (e.g. as part of a plugin). -### Tool element runtime substitution +#### Tool element runtime substitution To accomplish useful work on multiple files, a configured tool will most often require that some number of its arguments or even the executable itself change @@ -3614,7 +4064,7 @@ decorated in any way needed. To use a literal `$`, escape it as `\\$`. * The built-in preprocessing tools _can_ be overridden with non-GCC equivalents. However, this is highly impractical to do - as preprocessing features are highly dependent on the + as preprocessing features are quite dependent on the idiosyncrasies and features of the GCC toolchain. #### Example Test Compiler Tooling @@ -3671,6 +4121,40 @@ Notes on test fixture tooling example: 1. We're using `$stderr` redirection to allow us to capture simulator error messages to `$stdout` for display at the run's conclusion. +### Ceedling tool arguments addition shortcut + +Sometimes Ceedling’s default tool defininitions are _this close_ to being just +what you need. But, darn, you need one extra argument on the command line, and +you'd love to not override an entire tool definition to tweak it. + +We got you. Now, this little feature only allows you to add arguments to the +end of a tool command line. Not the beginning. And, you can't remove arguments +with this hack. + +Further, this little feature is a blanket application across all uses of a +tool. If you need fine-grained control of command line flags in build steps per +test executable, please see the [`:flags` configuration documentation][flags]. + +To use this shortcut, simply add a configuration section to your project file +at the top-level, `:tools_` ↳ `:arguments`. See the list of +tool names at the beginning of the `:tools` documentation to identify the named +options. Plugins can also include their own tool definitions that can be +modified with this same hack. + +This example YAML: + +```yaml +:tools_test_compiler: + :arguments: + - --flag # Add `--flag` to the end of all test C file compilation +``` + +...will produce this command line: + +```shell + > gcc --flag +``` + ## `:plugins` Ceedling extensions See the section below dedicated to plugins for more information. This section diff --git a/docs/Changelog.md b/docs/Changelog.md new file mode 100644 index 00000000..c167fa7f --- /dev/null +++ b/docs/Changelog.md @@ -0,0 +1,256 @@ +# 🌱 Ceedling Changelog + +This format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +This changelog is complemented by two other documents: + +1. 🔊 **[Release Notes](ReleaseNotes.md)** for announcements, education, acknowledgements, and known issues. +1. 💔 **[Breaking Changes](BreakingChanges.md)** for a list of impacts to existing Ceedling projects. + +--- + +# [1.0.0 pre-release] — 2024-04-02 + +## 🌟 Added + +### Parallel execution of build steps + +As was explained in the _[Highlights](#-Highlights)_, Ceedling can now run its internal tasks in parallel and take full advantage of your build system’s resources. Even lacking various optimizations (see _[Known Issues](#-Known-Issues)_) builds are now often quite speedy. + +Enabling this speedup requires either or both of two simple configuration settings. See Ceedling’s [documentation](CeedlingPacket.md) for `:project` ↳ `:compile_threads` and `:project` ↳ `:test_threads`. + +### `TEST_INCLUDE_PATH(...)` & `TEST_SOURCE_FILE(...)` + +Issue [#743](https://github.com/ThrowTheSwitch/Ceedling/issues/743) + +Using what we are calling build directive macros, you can now provide Ceedling certain configuration details from inside a test file. + +See the [documentation](CeedlingPacket.md) discussion on include paths, Ceedling conventions, and these macros to understand all the details. + +_Note:_ Ceedling is not yet capable of preserving build directive macros through preprocessing of test files. If, for example, you wrap these macros in + conditional compilation preprocessing statements, they will not work as you expect. + +#### `TEST_INCLUDE_PATH(...)` + +In short, `TEST_INCLUDE_PATH()` allows you to add a header file search path to the build of the test executable in which it is found. This can mean much shorter compilation command lines and good flexibility for complicated projects. + +#### `TEST_SOURCE_FILE(...)` + +In short, `TEST_SOURCE_FILE()` allows you to be explicit as to which source C files should be compiled and linked into a test executable. Sometimes Ceedling’s convention for matching source files with test files by way of `#include`d header files does not meet the need. This solves the problems of those scenarios. + +### Mixins for modifying your configuration + +Thorough documentation on Mixins can be found in _[CeedlingPacket](CeedlingPacket.md)_. + +### Additional options for loading a base configuration from a project file + +Once upon a time, you could load a project configuration in just two simple ways — _project.yml_ in your working directory and an environment variable pointing to a different file. Those days are over. + +You may now: + +* Load your project configuration from a filepath provided at the command line. +* Load your project configuration from an environment variable hoding a filepath. +* Load your project configuration from the default _project.yml_ in your working directory. +* Modify your configuration with Mixins loaded from your project file, environment variables, and/or from the command line. + +All the options for loading and modifying a project configuration are thoroughly documented in _[CeedlingPacket](CeedlingPacket.md))_. + +### More better `:flags` handling + +Issue [#43](https://github.com/ThrowTheSwitch/Ceedling/issues/43) + +Each test executable is now built as a mini project. Using improved `:flags` handling and an updated section format within Ceedling’s project file, you have much better options for specifying flags presented to the various tools within your build, particulary within test builds. + +### More better `:defines` handling + +Each test executable is now built as a mini project. Using improved `:defines` handling and an updated section format within Ceedling’s project file, you have much better options for specifying symbols used in your builds' compilation steps, particulary within test builds. + +One powerful new feature is the ability to test the same source file built differently for different tests. Imagine a source file has three different conditional compilation sections. You can now write unit tests for each of those sections without complicated gymnastics to cause your test suite to build and run properly. + +### `report_tests_log_factory` plugin + +This new plugin consolidates a handful of previously discrete report gernation plugins into a single plugin that also enables low-code, custom, end-user created reports. + +The output of these prior plugins are now simply configuration options for this new plugin: + +1. `junit_tests_report` +1. `json_tests_report` +1. `xml_tests_report` + +This new plugin also includes the option to generate an HTML report (see next section). + +### HTML tests report + +A community member submitted an [HTML report generation plugin](https://github.com/ThrowTheSwitch/Ceedling/pull/756/) that was not officially released before 0.32. It has been absorbed into the new `report_tests_log_factory` plugin (see previous section). + +## 💪 Fixed + +### `:paths` and `:files` handling bug fixes and clarification + +Most project configurations are relatively simple, and Ceedling’s features for collecting paths worked fine enough. However, bugs and ambiguities lurked. Further, insufficient validation left users resorting to old fashioned trial-and-error troubleshooting. + +Much glorious filepath and pathfile handling now abounds: + +* The purpose and use of `:paths` and `:files` has been clarified in both code and documentation. `:paths` are directory-oriented while `:files` are filepath-oriented. +* [Documentation](CeedlingPacket.md) is now accurate and complete. +* Path handling edge cases have been properly resolved (`./foo/bar` is the same as `foo/bar` but was not always processed as such). +* Matching globs were advertised in the documentation (erroneously, incidentally) but lacked full programmatic support. +* Ceedling now tells you if your matching patterns don't work. Unfortunately, all Ceedling can determine is if a particular pattern yielded 0 results. + +### Bug fixes for command line tasks `files:header` and `files:support` + +Longstanding bugs produced duplicate and sometimes incorrect lists of header files. Similarly, support file lists were not properly expanded from globs. Both of these problems have been fixed. The `files:header` command line task has replaced the `files:include` task. + +### Dashed filename handling bug fix + +Issue [#780](https://github.com/ThrowTheSwitch/Ceedling/issues/780) + +In certain combinations of Ceedling features, a dash in a C filename could cause Ceedling to exit with an exception. This has been fixed. + +### Source filename extension handling bug fix + +Issue [#110](https://github.com/ThrowTheSwitch/Ceedling/issues/110) + +Ceedling has long had the ability to configure a source filename extension other than `.c` (`:extension` ↳ `:source`). However, in most circumstances this ability would lead to broken builds. Regardless of user-provided source files and filename extenion settings, Ceedling’s supporting frameworks — Unity, CMock, and CException — all have `.c` file components. Ceedling also generates mocks and test runners with `.c` filename extensions regardless of any filename extension setting. Changing the source filename extension would cause Ceedling to miss its own core source files. This has been fixed. + +### Bug fixes for `gcov` plugin + +The most commonly reported bugs have been fixed: + +* `nil` references +* Exit code issues with recent releases of `gcov` +* Empty coverage results and related build failures + +### Bug fixes for `beep` plugin + +A handful of small bugs in using shell `echo` with the ASCII bell character have been fixed. + +## ⚠️ Changed + +### Preprocessing improvements + +Issues [#806](https://github.com/ThrowTheSwitch/Ceedling/issues/806) + [#796](https://github.com/ThrowTheSwitch/Ceedling/issues/796) + +Preprocessing refers to expanding macros and other related code file text manipulations often needed in sophisticated projects before key test suite generation steps. Without (optional) preprocessing, generating test funners from test files and generating mocks from header files lead to all manner of build shenanigans. + +The preprocessing needed by Ceedling for sophisticated projects has always been a difficult feature to implement. The most significant reason is simply that there is no readily available cross-platform C code preprocessing tool that provides Ceedling everything it needs to do its job. Even gcc’s `cpp` preprocessor tool comes up short. Over time Ceedling’s attempt at preprocessing grew more brittle and complicated as community contribturs attempted to fix it or cause it to work properly with other new features. + +This release of Ceedling stripped the feature back to basics and largely rewrote it within the context of the new build pipeline. Complicated regular expressions and Ruby-generated temporary files have been eliminated. Instead, Ceedling now blends two reports from gcc' `cpp` tool and complements this with additional context. In addition, preprocessing now occurs at the right moments in the overall build pipeline. + +While this new approach is not 100% foolproof, it is far more robust and far simpler than previous attempts. Other new Ceedling features should be able to address shortcomings in edge cases. + +### Project file environment variable name change `CEEDLING_MAIN_PROJECT_FILE` ➡️ `CEEDLING_PROJECT_FILE` + +Options and support for loading a project configuration have expanded significantly, mostly notably with the addition of Mixins. + +The environment variable option for pointing Ceedling to a project file other than _project.yml_ in your working directory has been renamed `CEEDLING_MAIN_PROJECT_FILE` ➡️ `CEEDLING_PROJECT_FILE`. + +Documentation on Mixins and the new options for loading a project configuration are thoroughly documented in _[CeedlingPacket](CeedlingPacket.md))_. + +### Plugin system improvements + +1. The plugin subsystem has incorporated logging to trace plugin activities at high verbosity levels. +1. Additional events have been added for test preprocessing steps (the popular and useful [`command_hooks` plugin](plugins/command_hooks/README.md) has been updated accordingly). +1. Built-in plugins have been updated for thread-safety as Ceedling is now able to execute builds with multiple threads. + +### Exit code options for test suite failures + +Be default Ceedling terminates with an exit code of `1` when a build succeeds but unit tests fail. + +A previously undocumented project configuration option `:graceful_fail` could force a Ceedling exit code of `0` upon test failures. + +This configuration option has moved but is now [documented](CeedlingPacket.md). It is also available as a new command line argument (`--graceful-fail`). + +```yaml +:test_build: + :graceful_fail: TRUE +``` + +### JUnit, XML & JSON test report plugins consolidation + +The three previously discrete plugins listed below have been consolidated into a single new plugin, `report_tests_log_factory`: + +1. `junit_tests_report` +1. `json_tests_report` +1. `xml_tests_report` + +`report_tests_log_factory` is able to generate all 3 reports of the plugins it replaces, a new HTML report, and custom report formats with a small amount of user-written Ruby code (i.e. not an entire Ceedling plugun). See its [documentation](../plugins/report_tests_log_factory) for more. + +The report format of the previously independent `xml_tests_report` plugin has been renamed from _XML_ in all instances to _CppUnit_ as this is the specific test reporting format the former plugin and new `report_tests_log_factory` plugin outputs. + +In some circumstances, JUnit report generation would yield an exception in its routines for reorganizing test results (Issues [#829](https://github.com/ThrowTheSwitch/Ceedling/issues/829) & [#833](https://github.com/ThrowTheSwitch/Ceedling/issues/833)). The true source of the nil test results entries has likely been fixed but protections have also been added in JUnit report generation as well. + +### Improvements and changes for `gcov` plugin + +1. Documentation has been significantly updated including a _Troubleshooting_ for common issues. +1. Compilation with coverage now only occurs for the source files under test and no longer for all C files (i.e. coverage for unity.c, mocks, and test files that is meaningless noise has been eliminated). +1. Coverage summaries printed to the console after `gcov:` test task runs now only concern the source files exercised instead of all source files. A final coverage tally has been restored. +1. Coverage summaries can now be disabled. +1. Coverage reports are now automatically generated after `gcov:` test tasks are executed. This behvaior can be disabled with a new configuration option. When enabled, a separate task is made available to trigger report generation. +1. To maintain consistency, repports generated by `gcovr` and `reportgenerator` are written to subdirectories named for the respective tools benath the `gcov/` artifacts path. + +See the [gcov plugin’s documentation](plugins/gcov/README.md). + +### Improvements for `compile_commands_json_db` plugin + +1. The plugin creates a compilation database that distinguishes the same code file compiled multiple times with different configurations as part of the new test suite build structure. It has been updated to work with other Ceedling changes. +1. Documentation has been greatly revised. + +### Improvements for `beep` plugin + +1. Additional sound tools — `:tput`, `:beep`, and `:say` — have been added for more platform sound output options and fun. +1. Documentation has been greatly revised. +1. The plugin more properly uses looging and system shell calls. + +## 👋 Removed + +### `options:` tasks + +Options files were a simple but limited way to merge configuration with your base configuration from the command line. This feature has been superseded by Ceedling Mixins. + +### Test suite smart rebuilds + +All “smart” rebuild features built around Rake no longer exist. That is, incremental test suite builds for only changed files are no longer possible. Any test build is a full rebuild of its components (the speed increase due to parallel build tasks more than makes up for this). + +These project configuration options related to smart builds are no longer recognized: + - `:use_deep_dependencies` + - `:generate_deep_dependencies` + - `:auto_link_deep_dependencies` + +In future revisions of Ceedling, smart rebuilds will be brought back (without relying on Rake) and without a list of possibly conflicting configuation options to control related features. + +Note that release builds do retain a fair amount of smart rebuild capabilities. Release builds continue to rely on Rake (for now). + +### Preprocessor support for Unity’s `TEST_CASE()` and `TEST_RANGE()` + +The project configuration option `:use_preprocessor_directives` is no longer recognized. + +**_Note:_** Unity’s features `TEST_CASE()` and `TEST_RANGE()` continue to work but only when `:use_test_preprocessor` is disabled. + +`TEST_CASE()` and `TEST_RANGE()` are do-nothing macros that disappear when the preprocessor digests a test file. + +In future revisions of Ceedling, support for `TEST_CASE()` and `TEST_RANGE()` when preprocessing is enabled will be brought back. + +### Removed background task execution + +Background task execution for tool configurations (`:background_exec`) has been deprecated. This option was one of Ceedling’s earliest features attempting to speed up builds within the constraints of relying on Rake. This feature has rarely, if ever, been used in practice, and other, better options exist to manage any scenario that might motivate a background task. + +### Removed `colour_report` plugin + +Colored build output and test results in your terminal is glorious. Long ago the `colour_report` plugin provided this. It was a simple plugin that hooked into Ceedling in a somewhat messy way. Its approach to coloring output was also fairly brittle. It long ago stopped coloring build output as intended. It has been removed. + +Ceedling’s logging will eventually be updated to rely on a proper logging library. This will provide a number of important features along with greater speed and stability for the tool as a whole. This will also be the opportunity to add robust terminal text coloring support. + +### Bullseye Plugin temporarily disabled + +The gcov plugin has been updated and improved, but its proprietary counterpart, the [Bullseye](https://www.bullseye.com) plugin, is not presently functional. The needed fixes and updates require a software license that we do not (yet) have. + +### Gcov Plugin’s support for deprecated features removed + +The configuration format for the `gcovr` utility changed when support for the `reportgenerator` utility was added. A format that accomodated a more uniform and common layout was adopted. However, support for the older, deprecated `gcovr`-only configuration was maintained. This support for the deprecated `gcovr` configuration format has been removed. + +Please consult the [gcov plugin’s documentation](plugins/gcov/README.md) to update any old-style `gcovr` configurations. + +### Undocumented environment variable `CEEDLING_USER_PROJECT_FILE` support removed + +A previously undocumented feature for merging a second configuration via environment variable `CEEDLING_USER_PROJECT_FILE` has been removed. This feature has been superseded by the new Mixins functionality. diff --git a/docs/PluginDevelopmentGuide.md b/docs/PluginDevelopmentGuide.md index b34b7612..95edf01c 100644 --- a/docs/PluginDevelopmentGuide.md +++ b/docs/PluginDevelopmentGuide.md @@ -320,6 +320,39 @@ If your plugin defines this method, it will be called during plugin creation at Ceedling startup. It is effectively your constructor for your custom `Plugin` subclass. +## `Plugin` hook methods `pre_` and `post_` conventions & concerns + +### Multi-threaded protections + +Because Ceedling can run build operations in multiple threads, build step hook +handliers must be thread safe. Practically speaking, this generally requires +a `Mutex` object `synchronize()`d around any code that writes to or reads from +a common data structure instantiated within a plugin. + +A common example is collecting test results filepaths from the +`post_test_fixture_execute()` hook. A hash or array accumulating these +filepaths as text executables complete their runs must have appropriate +threading protections. + +### Command line tool shell results + +Pre and post build step hooks are often called on either side of a command line +tool operation. If a command line tool is executed for a build step (e.g. test +compilation), the `arg_hash` will be the same for the pre and post hooks with +one difference. + +In the `post_` hook, the `arg_hash` parameter will contain a `shell_result` key +whose associated value is itself a hash with the following contents: + +```ruby +{ + :output => "", # String holding any $stdout / redirected $stderr output + :status => , # Ruby object of type Process::Status + :exit_code => , # Command line exit code (extracted from :status object) + :time => # Seconds elapsed for shell operation +} +``` + ## `Plugin` hook methods `pre_mock_preprocess(arg_hash)` and `post_mock_preprocess(arg_hash)` These methods are called before and after execution of preprocessing for header @@ -549,6 +582,39 @@ This method is called when invoking the summary task, `ceedling summary`. This method facilitates logging the results of the last build without running the previous build again. +## Validating a plugin’s tools + +By default, Ceedling validates configured tools at startup according to a +simple setting within the tool definition. This works just fine for default +core tools and options. However, in the case of plugins, tools may not be even +relevant to a plugin's operation depending on its configurable options. It's +a bit silly for a tool not needed by your project to fail validation if +Ceedling can't find it in your `$PATH`. Similarly, it's irresponsible to skip +validating a tool just because it may not be needed. + +Ceedling provides optional, programmatic tool validation for these cases. +`@ceedling]:tool_validator].validate()` can be forced to ignore a tool's +`required:` setting to validate it. In such a scenario, a plugin should +configure its own tools as `:optional => true` but forcibly validate them at +plugin startup if the plugin's configuration options require said tool. + +An example from the `gcov` plugin illustrates this. + +```ruby +# Validate gcov summary tool if coverage summaries are enabled (summaries rely on the `gcov` tool) +if summaries_enabled?( @project_config ) + @ceedling[:tool_validator].validate( + tool: TOOLS_GCOV_SUMMARY, # Tool defintion as Ruby hash + boom: true # Ignore optional status (raise exception if invalid) + ) +end +``` + +The tool `TOOLS_GCOV_SUMMARY` is defined with a Ruby hash in the plugin code. +It is configured with `:optional => true`. At plugin startup, configuration +options determine if the tool is needed. It is forcibly validated if the plugin +configuration requires it. + ## Collecting test results from within `Plugin` subclass Some testing-specific plugins need access to test results to do their work. A diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index a436ce9b..3968115f 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -1,20 +1,23 @@ -# Ceedling Release Notes for 0.32 Release Candidate +# 🌱 Ceedling Release Notes -**Version:** 0.32 pre-release incremental build +These release notes are complemented by two other documents: -**Date:** February 28, 2024 +1. 🪵 **[Changelog](Changelog.md)** for a structured list of additions, fixes, changes, and removals. +1. 💔 **[Breaking Changes](BreakingChanges.md)** for a list of impacts to existing Ceedling projects. -
+--- -## 👀 Highlights +# 1.0.0 pre-release — April 2, 2024 -This Ceedling release is probably the most significant since the project was first posted to [SourceForge][sourceforge] in 2009. +## 🏴‍☠️ Avast, Breaking Changes, Ye Scallywags! -Ceedling now runs in Ruby 3. Builds can now run much faster than previous versions because of parallelized tasks. For test suites, header file search paths, code defines, and tool run flags are now customizable per test executable. +**_Ahoy!_** There be plenty o’ **[breaking changes](BreakingChanges.md)** ahead, mateys! Arrr… -### Avast, Breaking Changes, Ye Scallywags! 🏴‍☠️ +## 👀 Highlights -**_Ahoy!_** There be **[breaking changes](BreakingChanges.md)** ahead, mateys! Arrr… +This Ceedling release is probably the most significant since the project was first [posted to SourceForge in 2009][sourceforge]. See the [Changelog](Changelog.md) for all the details. + +Ceedling now runs in Ruby 3. Builds can now run much faster than previous versions because of parallelized tasks. For test suites, header file search paths, code defines, and tool run flags are now customizable per test executable. ### Big Deal Highlights 🏅 @@ -26,7 +29,7 @@ Ceedling now runs in Ruby3. This latest version of Ceedling is _not_ backwards c Previously, Ceedling builds were depth-first and limited to a single line of execution. This limitation was an artifact of how Ceedling was architected and relying on general purpose Rake for the build pipeline. Rake does, in fact, support multi-threaded builds, but, Ceedling was unable to take advantage of this. As such, builds were limited to a single line of execution no matter how many CPU resources were available. -Ceedling 0.32 introduces a new build pipeline that batches build steps breadth-first. This means all test preprocessor steps, all compilation steps, all linking steps, etc. can benefit from concurrent and parallel execution. This speedup applies to both test suite and release builds. +Ceedling 1.0.0 introduces a new build pipeline that batches build steps breadth-first. This means all test preprocessor steps, all compilation steps, all linking steps, etc. can benefit from concurrent and parallel execution. This speedup applies to both test suite and release builds. #### Per-test-executable configurations @@ -42,11 +45,37 @@ The following new features (discussed in later sections) contribute to this new - `:defines` handling. `#define`s are now specified for the compilation of all modules comprising a test executable. Matching is only against test file names but now includes wildcard and regular expression options. - `:flags` handling. Flags (e.g. `-std=c99`) are now specified for the build steps—preprocessing, compilation, and linking—of all modules comprising a test executable. Matching is only against test file names and now includes more sensible and robust wildcard and regular expression options. +#### Mixins for configuration variations + +Ever wanted to smoosh in some extra configuration selectively? Let’s say you have different build scenarios and you'd like to run different variations of your project for them. Maybe you have core configuration that is common to all those scenarios. Previous versions of Ceedling included a handful of features that partially met these sorts of needs. + +All such features have been superseded by _Mixins_. Mixins are simply additional YAML that gets merged into you base project configuration. However, Mixins provide several key improvements over previous features: + +1. Mixins can be as little or as much configuration as you want. You could push all your configuration into mixins with a base project file including nothing but a `:mixins` section. +1. Mixins can be specified in your project configuration, via environment variables, and from the command line. A clear order of precedence controls the order of merging. Any conflicts or duplicates are automatically resolved. +1. Logging makes clear what proejct file and mixins are loaded and merged at startup. +1. Like built-in plugins, Ceedling will soon come with built-in mixins available for common build scenarios. + +#### A proper command line + +Until this release, Ceedling depended on Rake for most of its command line handling. Rake’s task conventions provide poor command line handling abilities. The core problems with Rake command line handling include: + +1. Only brief, limited help statements. +1. No optional flags to modify a task — verbosity, logging, etc. were their own tasks. +1. Complex/limited parameterization (e.g. `verbosity[3]` instead of `--verbosity normal`). +1. Tasks are order-dependent. So, for example, `test:all verbosity[5]` changes verbosity after the tests are run. + +Ceedling now offers a full command line interface with rich help, useful order-independent option flags, and more. + +The existing `new`, `upgrade`, and `example` commands remain but have been improved. You may now even specify the project file to load, log file to write to, and exit code handling behavior from the command line. + +Try `ceedling help` and then `ceedling help ` to get started. + ### Medium Deal Highlights 🥈 #### `TEST_SOURCE_FILE(...)` -In previous versions of Ceedling, a new, undocumented build directive feature was introduced. Adding a call to the macro `TEST_FILE(...)` with a C file's name added that C file to the compilation and linking list for a test executable. +In previous versions of Ceedling, a new, undocumented build directive feature was introduced. Adding a call to the macro `TEST_FILE(...)` with a C file’s name added that C file to the compilation and linking list for a test executable. This approach was helpful when relying on a Ceedling convention was problematic. Specifically, `#include`ing a header file would cause any correspondingly named source file to be added to the build list for a test executable. This convention could cause problems if, for example, the header file defined symbols that complicated test compilation or behavior. Similarly, if a source file did not have a corresponding header file of the same name, sometimes the only option was to `#include` it directly; this was ugly and problematic in its own way. @@ -54,15 +83,15 @@ The previously undocumented build directive macro `TEST_FILE(...)` has been rena #### Preprocessing improvements -Ceedling has been around for a number of years and has had the benefit of many contributors over that time. Preprocessing (expanding macros in test files and header files to be mocked) is quite tricky to get right but is essential for big, complicated test suites. Over Ceedling's long life various patches and incremental improvements have evolved in such a way that preprocessing had become quite complicated and often did the wrong thing. Much of this has been fixed and improved in this release. +Ceedling has been around for a number of years and has had the benefit of many contributors over that time. Preprocessing (expanding macros in test files and header files to be mocked) is quite tricky to get right but is essential for big, complicated test suites. Over Ceedling’s long life various patches and incremental improvements have evolved in such a way that preprocessing had become quite complicated and often did the wrong thing. Much of this has been fixed and improved in this release. #### Documentation -The [Ceedling user guide](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. +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. Many of the plugins have received documentation updates as well. -There's more to be done, but Ceedling's documentation is more complete and accurate than it's ever been. +There’s more to be done, but Ceedling’s documentation is more complete and accurate than it’s ever been. ### Small Deal Highlights 🥉 @@ -70,11 +99,11 @@ There's more to be done, but Ceedling's documentation is more complete and accur - Logical ambiguity and functional bugs within `:paths` and `:files` configuration handling have been resolved along with updated documentation. - A variety of small improvements and fixes have been made throughout the plugin system and to many plugins. - The historically unwieldy `verbosity` command line task now comes in two flavors. The original recipe numeric parameterized version (e.g. `[4]`) exist as is. The new extra crispy recipe includes — funny enough — verbose task names `verbosity:silent`, `verbosity:errors`, `verbosity:complain`, `verbosity:normal`, `verbosity:obnoxious`, `verbosity:debug`. -- This release marks the beginning of the end for Rake as a backbone of Ceedling. Over many years it has become clear that Rake's design assumptions hamper building the sorts of features Ceedling's users want, Rake's command line structure creates a messy user experience for a full application built around it, and Rake's quirks cause maintenance challenges. Particularly for test suites, much of Ceedling's (invisible) dependence on Rake has been removed in this release. Much more remains to be done, including replicating some of the abilities Rake offers. -- This is the first ever release of Ceedling with proper release notes. Hello, there! Release notes will be a regular part of future Ceedling updates. If you haven't noticed already, this edition of the notes are detailed and quite lengthy. This is entirely due to how extensive the changes are in the 0.32 release. Future releases will have far shorter notes. +- This release marks the beginning of the end for Rake as a backbone of Ceedling. Over many years it has become clear that Rake’s design assumptions hamper building the sorts of features Ceedling’s users want, Rake’s command line structure creates a messy user experience for a full application built around it, and Rake’s quirks cause maintenance challenges. Particularly for test suites, much of Ceedling’s (invisible) dependence on Rake has been removed in this release. Much more remains to be done, including replicating some of the abilities Rake offers. +- This is the first ever release of Ceedling with proper release notes. Hello, there! Release notes will be a regular part of future Ceedling updates. If you haven't noticed already, this edition of the notes are detailed and quite lengthy. This is entirely due to how extensive the changes are in the 1.0.0 release. Future releases will have far shorter notes. - The `fake_function_framework` plugin has been renamed simply `fff` -### Important Changes in Behavior to Be Aware Of 🚨 +## 🚨 Important Changes in Behavior to Be Aware Of - **Test suite build order 🔢.** Ceedling no longer builds each test executable one at a time. From the tasks you provide at the command line, Ceedling now collects up and batches all preprocessing steps, all mock generation, all test runner generation, all compilation, etc. Previously you would see each of these done for a single test executable and then repeated for the next executable and so on. Now, each build step happens to completion for all specified tests before moving on to the next build step. - **Logging output order 🔢.** When multi-threaded builds are enabled, logging output may not be what you expect. Progress statements may be all batched together or interleaved in ways that are misleading. The steps are happening in the correct order. How you are informed of them may be somewhat out of order. @@ -83,209 +112,12 @@ There's more to be done, but Ceedling's documentation is more complete and accur Together, these changes may cause you to think that Ceedling is running steps out of order or duplicating work. While bugs are always possible, more than likely, the output you see and the build ordering is expected. -
- -### 👋 Deprecated or Temporarily Removed Abilities - -#### Test suite smart rebuilds - -All “smart” rebuild features built around Rake no longer exist. That is, incremental test suite builds for only changed files are no longer possible. Any test build is a full rebuild of its components (the speed increase due to parallel build tasks more than makes up for this). - -These project configuration options related to smart builds are no longer recognized: - - `:use_deep_dependencies` - - `:generate_deep_dependencies` - - `:auto_link_deep_dependencies` - -In future revisions of Ceedling, smart rebuilds will be brought back (without relying on Rake) and without a list of possibly conflicting configuation options to control related features. - -Note that release builds do retain a fair amount of smart rebuild capabilities. Release builds continue to rely on Rake (for now). - -#### Preprocessor support for Unity's `TEST_CASE()` and `TEST_RANGE()` - -The project configuration option `:use_preprocessor_directives` is no longer recognized. - -**_Note:_** Unity's features `TEST_CASE()` and `TEST_RANGE()` continue to work but only when `:use_test_preprocessor` is disabled. - -`TEST_CASE()` and `TEST_RANGE()` are do-nothing macros that disappear when the preprocessor digests a test file. - -In future revisions of Ceedling, support for `TEST_CASE()` and `TEST_RANGE()` when preprocessing is enabled will be brought back. - -#### Removed background task execution - -Background task execution for tool configurations (`:background_exec`) has been deprecated. This option was one of Ceedling's earliest features attempting to speed up builds within the constraints of relying on Rake. This feature has rarely, if ever, been used in practice, and other, better options exist to manage any scenario that might motivate a background task. - -#### Removed `colour_report` plugin - -Colored build output and test results in your terminal is glorious. Long ago the `colour_report` plugin provided this. It was a simple plugin that hooked into Ceedling in a somewhat messy way. Its approach to coloring output was also fairly brittle. It long ago stopped coloring build output as intended. It has been removed. - -Ceedling's logging will eventually be updated to rely on a proper logging library. This will provide a number of important features along with greater speed and stability for the tool as a whole. This will also be the opportunity to add robust terminal text coloring support. - -#### Bullseye Plugin temporarily disabled - -The gcov plugin has been updated and improved, but its proprietary counterpart, the [Bullseye](https://www.bullseye.com) plugin, is not presently functional. The needed fixes and updates require a software license that we do not (yet) have. - -#### Gcov Plugin's support for deprecated features removed - -The configuration format for the `gcovr` utility changed when support for the `reportgenerator` utility was added. A format that accomodated a more uniform and common layout was adopted. However, support for the older, deprecated `gcovr`-only configuration was maintained. This support for the deprecated `gcovr` configuration format has been removed. - -Please consult the [gcov plugin's documentation](plugins/gcov/README.md) to update any old-style `gcovr` configurations. - -
- -## 🌟 New Features - -### Parallel execution of build steps - -As was explained in the _[Highlights](#-Highlights)_, Ceedling can now run its internal tasks in parallel and take full advantage of your build system's resources. Even lacking various optimizations (see _[Known Issues](#-Known-Issues)_) builds are now often quite speedy. - -Enabling this speedup requires either or both of two simple configuration settings. See Ceedling's [documentation](CeedlingPacket.md) for `:project` ↳ `:compile_threads` and `:project` ↳ `:test_threads`. - -### `TEST_INCLUDE_PATH(...)` & `TEST_SOURCE_FILE(...)` - -Issue [#743](https://github.com/ThrowTheSwitch/Ceedling/issues/743) - -Using what we are calling build directive macros, you can now provide Ceedling certain configuration details from inside a test file. - -See the [documentation](CeedlingPacket.md) discussion on include paths, Ceedling conventions, and these macros to understand all the details. - -_Note:_ Ceedling is not yet capable of preserving build directive macros through preprocessing of test files. If, for example, you wrap these macros in - conditional compilation preprocessing statements, they will not work as you expect. - -#### `TEST_INCLUDE_PATH(...)` - -In short, `TEST_INCLUDE_PATH()` allows you to add a header file search path to the build of the test executable in which it is found. This can mean much shorter compilation command lines and good flexibility for complicated projects. - -#### `TEST_SOURCE_FILE(...)` - -In short, `TEST_SOURCE_FILE()` allows you to be explicit as to which source C files should be compiled and linked into a test executable. Sometimes Ceedling's convention for matching source files with test files by way of `#include`d header files does not meet the need. This solves the problems of those scenarios. - -### More better `:flags` handling - -Issue [#43](https://github.com/ThrowTheSwitch/Ceedling/issues/43) - -Each test executable is now built as a mini project. Using improved `:flags` handling and an updated section format within Ceedling's project file, you have much better options for specifying flags presented to the various tools within your build, particulary within test builds. - -### More better `:defines` handling - -Each test executable is now built as a mini project. Using improved `:defines` handling and an updated section format within Ceedling's project file, you have much better options for specifying symbols used in your builds' compilation steps, particulary within test builds. - -One powerful new feature is the ability to test the same source file built differently for different tests. Imagine a source file has three different conditional compilation sections. You can now write unit tests for each of those sections without complicated gymnastics to cause your test suite to build and run properly. - -### `report_tests_log_factory` plugin - -This new plugin consolidates a handful of previously discrete report gernation plugins into a single plugin that also enables low-code, custom, end-user created reports. - -The output of these prior plugins are now simply configuration options for this new plugin: - -1. `junit_tests_report` -1. `json_tests_report` -1. `xml_tests_report` - -This new plugin also includes the option to generate an HTML report (see next section). - -### HTML tests report - -A community member submitted an [HTML report generation plugin](https://github.com/ThrowTheSwitch/Ceedling/pull/756/) that was not officially released before 0.32. It has been absorbed into the new `report_tests_log_factory` plugin (see previous section). - -
- -## 💪 Improvements and 🪲 Bug Fixes - -### Preprocessing improvements - -Issues [#806](https://github.com/ThrowTheSwitch/Ceedling/issues/806) + [#796](https://github.com/ThrowTheSwitch/Ceedling/issues/796) - -Preprocessing refers to expanding macros and other related code file text manipulations often needed in sophisticated projects before key test suite generation steps. Without (optional) preprocessing, generating test funners from test files and generating mocks from header files lead to all manner of build shenanigans. - -The preprocessing needed by Ceedling for sophisticated projects has always been a difficult feature to implement. The most significant reason is simply that there is no readily available cross-platform C code preprocessing tool that provides Ceedling everything it needs to do its job. Even gcc's `cpp` preprocessor tool comes up short. Over time Ceedling's attempt at preprocessing grew more brittle and complicated as community contribturs attempted to fix it or cause it to work properly with other new features. - -This release of Ceedling stripped the feature back to basics and largely rewrote it within the context of the new build pipeline. Complicated regular expressions and Ruby-generated temporary files have been eliminated. Instead, Ceedling now blends two reports from gcc' `cpp` tool and complements this with additional context. In addition, preprocessing now occurs at the right moments in the overall build pipeline. - -While this new approach is not 100% foolproof, it is far more robust and far simpler than previous attempts. Other new Ceedling features should be able to address shortcomings in edge cases. - -### `:paths` and `:files` handling bug fixes and clarification - -Most project configurations are relatively simple, and Ceedling's features for collecting paths worked fine enough. However, bugs and ambiguities lurked. Further, insufficient validation left users resorting to old fashioned trial-and-error troubleshooting. - -Much glorious filepath and pathfile handling now abounds: - -* The purpose and use of `:paths` and `:files` has been clarified in both code and documentation. `:paths` are directory-oriented while `:files` are filepath-oriented. -* [Documentation](CeedlingPacket.md) is now accurate and complete. -* Path handling edge cases have been properly resolved (`./foo/bar` is the same as `foo/bar` but was not always processed as such). -* Matching globs were advertised in the documentation (erroneously, incidentally) but lacked full programmatic support. -* Ceedling now tells you if your matching patterns don't work. Unfortunately, all Ceedling can determine is if a particular pattern yielded 0 results. - -### Plugin system improvements - -1. The plugin subsystem has incorporated logging to trace plugin activities at high verbosity levels. -1. Additional events have been added for test preprocessing steps (the popular and useful [`command_hooks` plugin](plugins/command_hooks/README.md) has been updated accordingly). -1. Built-in plugins have been updated for thread-safety as Ceedling is now able to execute builds with multiple threads. - -### Improvements, changes, and bug fixes for `gcov` plugin - -1. Documentation has been significantly updated including a _Troubleshooting_ for common issues. -1. Compilation with coverage now only occurs for the source files under test and no longer for all C files (i.e. coverage for unity.c, mocks, and test files that is meaningless noise has been eliminated). -1. Coverage summaries printed to the console after `gcov:` test task runs now only concern the source files exercised instead of all source files. A final coverage tally has been restored. -1. Coverage summaries can now be disabled. -1. Coverage reports are now automatically generated after `gcov:` test tasks are executed. This behvaior can be disabled with a new configuration option. When enabled, a separate task is made available to trigger report generation. -1. To maintain consistency, repports generated by `gcovr` and `reportgenerator` are written to subdirectories named for the respective tools benath the `gcov/` artifacts path. - -See the [gcov plugin's documentation](plugins/gcov/README.md). - -### Bug fixes for command line tasks `files:header` and `files:support` - -Longstanding bugs produced duplicate and sometimes incorrect lists of header files. Similarly, support file lists were not properly expanded from globs. Both of these problems have been fixed. The `files:header` command line task has replaced the `files:include` task. - -### Improvements and bug fixes for `compile_commands_json_db` plugin - -1. The plugin creates a compilation database that distinguishes the same code file compiled multiple times with different configurations as part of the new test suite build structure. It has been updated to work with other Ceedling changes and small bugs have been fixed. -1. Documentation has been greatly revised. - -### Improvements and bug fixes for `beep` plugin - -1. Additional sound tools — `:tput`, `:beep`, and `:say` — have been added for more platform sound output options and fun. -1. Documentation has been greatly revised. -1. The plugin more properly uses looging and system shell calls. -1. Small bugs in using `echo` and the ASCII bell character have been fixed. - -### JUnit, XML & JSON test report plugins: Bug fixes and consolidation - -When used with other plugins, these test reporting plugins' generated report could end up in a location within `build/artifacts/` that was inconsistent and confusing. This has been fixed. - -The three previously discrete plugins listed below have been consolidated into a single new plugin, `report_tests_log_factory`: - -1. `junit_tests_report` -1. `json_tests_report` -1. `xml_tests_report` - -`report_tests_log_factory` is able to generate all 3 reports of the plugins it replaces as well as custom report formats with a small amount of user-written Ruby code (i.e. not an entire Ceedling plugun). See its [documentation](../plugins/report_tests_log_factory) for more. - -The report format of the previously independent `xml_tests_report` plugin has been renamed from _XML_ in all instances to _CppUnit_ as this is the specific test reporting format the former plugin and new `report_tests_log_factory` plugin outputs. - -In some circumstances, JUnit report generation would yield an exception in its routines for reorganizing test results (Issues [#829](https://github.com/ThrowTheSwitch/Ceedling/issues/829) & [#833](https://github.com/ThrowTheSwitch/Ceedling/issues/833)). The true source of the nil test results entries has likely been fixed but protections have also been added in JUnit report generation as well. - -### Dashed filename handling bug fix - -Issue [#780](https://github.com/ThrowTheSwitch/Ceedling/issues/780) - -In certain combinations of Ceedling features, a dash in a C filename could cause Ceedling to exit with an exception. This has been fixed. - -### Source filename extension handling bug fix - -Issue [#110](https://github.com/ThrowTheSwitch/Ceedling/issues/110) - -Ceedling has long had the ability to configure a source filename extension other than `.c` (`:extension` ↳ `:source`). However, in most circumstances this ability would lead to broken builds. Regardless of user-provided source files and filename extenion settings, Ceedling's supporting frameworks — Unity, CMock, and CException — all have `.c` file components. Ceedling also generates mocks and test runners with `.c` filename extensions regardless of any filename extension setting. Changing the source filename extension would cause Ceedling to miss its own core source files. This has been fixed. - -
- ## 🩼 Known Issues 1. The new internal pipeline that allows builds to be parallelized and configured per-test-executable can mean a fair amount of duplication of steps. A header file may be mocked identically multiple times. The same source file may be compiled identically multiple times. The speed gains due to parallelization more than make up for this. Future releases will concentrate on optimizing away duplication of build steps. 1. While header file search paths are now customizable per executable, this currently only applies to the search paths the compiler uses. Distinguishing test files or header files of the same name in different directories for test runner and mock generation respectively continues to rely on educated guesses in Ceedling code. 1. Any path for a C file specified with `TEST_SOURCE_FILE(...)` is in relation to **_project root_** — that is, from where you execute `ceedling` at the command line. If you move source files or change your directory structure, many of your `TEST_SOURCE_FILE(...)` calls may need to be updated. A more flexible and dynamic approach to path handling will come in a future update. -
- ## 📚 Background Knowledge ### Parallel execution of build steps @@ -302,11 +134,9 @@ When a native thread blocks for I/O, Ruby allows the OS scheduler to context swi #### Process spawning -Ruby's process spawning abilities have always mapped directly to OS capabilities. When a processor has multiple cores available, the OS tends to spread multiple child processes across those cores in true parallel execution. - -Much of Ceedling's workload is executing a tool — such as a compiler — in a child process. With multiple threads enabled, each thread can spawn a child process for a build tool used by a build step. These child processes can be spread across multiple cores in true parallel execution. +Ruby’s process spawning abilities have always mapped directly to OS capabilities. When a processor has multiple cores available, the OS tends to spread multiple child processes across those cores in true parallel execution. -
+Much of Ceedling’s workload is executing a tool — such as a compiler — in a child process. With multiple threads enabled, each thread can spawn a child process for a build tool used by a build step. These child processes can be spread across multiple cores in true parallel execution. ## 📣 Shoutouts @@ -315,4 +145,4 @@ Thank yous and acknowledgments: - … -[sourceforge]: https://sourceforge.net/projects/ceedling/ "Ceedling's public debut" \ No newline at end of file +[sourceforge]: https://sourceforge.net/projects/ceedling/ "Ceedling’s public debut" \ No newline at end of file diff --git a/examples/temp_sensor/project.yml b/examples/temp_sensor/project.yml index c34bb028..00bcb0e0 100644 --- a/examples/temp_sensor/project.yml +++ b/examples/temp_sensor/project.yml @@ -20,9 +20,6 @@ :test_threads: 8 :compile_threads: 8 - # you can specify different yaml config files which modify the existing one - :options_paths: [] - # enable release build (more details in release_build section below) :release_build: FALSE @@ -140,7 +137,7 @@ int8: INT8 bool: UINT8 -# Configuration options specify to Unity's test runner generator +# Configuration options specific to Unity's test runner generator :test_runner: :cmdline_args: TRUE diff --git a/lib/ceedling.rb b/lib/ceedling.rb index f76c9edd..5c56c2bc 100644 --- a/lib/ceedling.rb +++ b/lib/ceedling.rb @@ -6,7 +6,7 @@ module Ceedling # Returns the location where the gem is installed. # === Return # _String_ - The location where the gem lives. - def self.location + def self.location() # Ensure parent path traversal is expanded away File.absolute_path( File.join( File.dirname(__FILE__), '..') ) end @@ -15,7 +15,7 @@ def self.location # Return the path to the "built-in" plugins. # === Return # _String_ - The path where the default plugins live. - def self.plugins_load_path + def self.plugins_load_path() File.join( self.location, 'plugins') end @@ -23,78 +23,13 @@ def self.plugins_load_path # Return the path to the Ceedling Rakefile # === Return # _String_ - def self.rakefile + def self.rakefile() File.join( self.location, 'lib', 'ceedling', 'rakefile.rb' ) end - ## - # This method selects the project file that Ceedling will use by setting the - # CEEDLING_MAIN_PROJECT_FILE environment variable before loading the ceedling - # rakefile. A path supplied as an argument to this method will override the - # current value of the environment variable. If no path is supplied as an - # argument then the existing value of the environment variable is used. If - # the environment variable has not been set and no argument has been supplied - # then a default path of './project.yml' will be used. - # - # === Arguments - # +options+ _Hash_:: - # A hash containing the options for ceedling. Currently the following - # options are supported: - # * +config+ - The path to the project YAML configuration file. - # * +root+ - The root of the project directory. - # * +prefix+ - A prefix to prepend to plugin names in order to determine the - # corresponding gem name. - # * +plugins+ - The list of ceedling plugins to load - def self.load_project(options = {}) - # Make sure our path to the yaml file is setup - if options.has_key? :config - ENV['CEEDLING_MAIN_PROJECT_FILE'] = options[:config] - elsif ENV['CEEDLING_MAIN_PROJECT_FILE'].nil? - ENV['CEEDLING_MAIN_PROJECT_FILE'] = './project.yml' - end - - # Register the plugins - if options.has_key? :plugins - options[:plugins].each do |plugin| - register_plugin( plugin, options[:prefix] ) - end - end - - # Define the root of the project if specified - Object.const_set('PROJECT_ROOT', options[:root]) if options.has_key? :root - - # Load ceedling - load "#{self.rakefile}" + def self.load_rakefile() + require "#{self.rakefile}" end - ## - # Register a plugin for ceedling to use when a project is loaded. This method - # *must* be called prior to calling the _load_project_ method. - # - # This method is intended to be used for loading plugins distributed via the - # RubyGems mechanism. As such, the following gem structure is assumed for - # plugins. - # - # * The gem name must be prefixed with 'ceedling-' followed by the plugin - # name (ex. 'ceedling-bullseye') - # - # * The contents of the plugin must be isntalled into a subdirectory of - # the gem with the same name as the plugin (ex. 'bullseye/') - # - # === Arguments - # +name+ _String_:: The name of the plugin to load. - # +prefix+ _String_:: - # (optional, default = nil) The prefix to use for the full gem name. - def self.register_plugin(name, prefix=nil) - # Figure out the full name of the gem and location - prefix ||= 'ceedling-' - gem_name = prefix + name - gem_dir = Gem::Specification.find_by_name(gem_name).gem_dir() - - # Register the plugin with Ceedling - require 'ceedling/defaults' - DEFAULT_CEEDLING_CONFIG[:plugins][:enabled] << name - DEFAULT_CEEDLING_CONFIG[:plugins][:load_paths] << gem_dir - end end diff --git a/lib/ceedling/configurator.rb b/lib/ceedling/configurator.rb index a7154d3d..fb3f40c9 100644 --- a/lib/ceedling/configurator.rb +++ b/lib/ceedling/configurator.rb @@ -9,13 +9,11 @@ class Configurator attr_reader :project_config_hash, :programmatic_plugins, :rake_plugins - attr_accessor :project_logging, :project_debug, :project_verbosity, :sanity_checks + attr_accessor :project_logging, :sanity_checks, :include_test_case, :exclude_test_case constructor(:configurator_setup, :configurator_builder, :configurator_plugins, :yaml_wrapper, :system_wrapper) do - @project_logging = false - @project_debug = false - @project_verbosity = Verbosity::NORMAL - @sanity_checks = TestResultsSanityChecks::NORMAL + @project_logging = false + @sanity_checks = TestResultsSanityChecks::NORMAL end def setup @@ -25,10 +23,9 @@ def setup # Runner config reference to provide to runner generation @runner_config = {} # Default empty hash, replaced by reference below - # note: project_config_hash is an instance variable so constants and accessors created + # Note: project_config_hash is an instance variable so constants and accessors created # in eval() statements in build() have something of proper scope and persistence to reference @project_config_hash = {} - @project_config_hash_backup = {} @programmatic_plugins = [] @rake_plugins = [] @@ -47,24 +44,12 @@ def replace_flattened_config(config) end - def store_config - @project_config_hash_backup = @project_config_hash.clone - end - - - def restore_config - @project_config_hash = @project_config_hash_backup - @configurator_setup.build_constants_and_accessors(@project_config_hash, binding()) - end - - def reset_defaults(config) [:test_compiler, :test_linker, :test_fixture, :test_includes_preprocessor, :test_file_preprocessor, - :test_file_preprocessor_directives, :test_dependencies_generator, :release_compiler, :release_assembler, @@ -132,7 +117,8 @@ def populate_cmock_defaults(config) cmock[:mock_path] = File.join(config[:project][:build_root], TESTS_BASE_PATH, 'mocks') if (cmock[:mock_path].nil?) - cmock[:verbosity] = @project_verbosity if (cmock[:verbosity].nil?) + # Use dynamically defined accessor + cmock[:verbosity] = project_verbosity() if (cmock[:verbosity].nil?) cmock[:plugins] = [] if (cmock[:plugins].nil?) cmock[:plugins].map! { |plugin| plugin.to_sym } @@ -269,44 +255,37 @@ def find_and_merge_plugins(config) end - def merge_imports(config) - if config[:import] - if config[:import].is_a? Array - until config[:import].empty? - path = config[:import].shift - path = @system_wrapper.module_eval(path) if (path =~ RUBY_STRING_REPLACEMENT_PATTERN) - config.deep_merge!(@yaml_wrapper.load(path)) - end - else - config[:import].each_value do |path| - if !path.nil? - path = @system_wrapper.module_eval(path) if (path =~ RUBY_STRING_REPLACEMENT_PATTERN) - config.deep_merge!(@yaml_wrapper.load(path)) - end - end - end - end - config.delete(:import) - end - - + # Process environment variables set in configuration file + # (Each entry beneath :environment is another hash) def eval_environment_variables(config) config[:environment].each do |hash| - key = hash.keys[0] - value = hash[key] + key = hash.keys[0] # Get first (should be only) environment variable entry + value = hash[key] # Get associated value items = [] + # Special case handling for :path environment variable entry + # File::PATH_SEPARATOR => ':' (Unix-ish) or ';' (Windows) interstitial = ((key == :path) ? File::PATH_SEPARATOR : '') + + # Create an array container for the value of this entry + # - If the value is an array, get it + # - Otherwise, place value in a single item array items = ((value.class == Array) ? hash[key] : [value]) + # Process value array items.each do |item| + # Process each item for Ruby string replacement if item.is_a? String and item =~ RUBY_STRING_REPLACEMENT_PATTERN item.replace( @system_wrapper.module_eval( item ) ) end end + # Join any value items (become a flattened string) + # - With path separator if the key was :path + # - With nothing otherwise hash[key] = items.join( interstitial ) + # Set the environment variable for our session @system_wrapper.env_set( key.to_s.upcase, hash[key] ) end end @@ -364,15 +343,22 @@ def standardize_paths(config) end - def validate(config) - # Collect felonies and go straight to jail - if (not @configurator_setup.validate_required_sections( config )) + def validate_essential(config) + # Collect all infractions, everybody on probation until final adjudication + blotter = true + + blotter &= @configurator_setup.validate_required_sections( config ) + blotter &= @configurator_setup.validate_required_section_values( config ) + + if !blotter raise CeedlingException.new("ERROR: Ceedling configuration failed validation") end + end - # Collect all misdemeanors, everybody on probation + + def validate_final(config) + # Collect all infractions, everybody on probation until final adjudication blotter = true - blotter &= @configurator_setup.validate_required_section_values( config ) blotter &= @configurator_setup.validate_paths( config ) blotter &= @configurator_setup.validate_tools( config ) blotter &= @configurator_setup.validate_threads( config ) @@ -398,7 +384,6 @@ def build(config, *keys) @configurator_setup.build_project_collections( flattened_config ) @project_config_hash = flattened_config.clone - store_config() @configurator_setup.build_constants_and_accessors( flattened_config, binding() ) @@ -425,9 +410,6 @@ def redefine_element(elem, value) # Update global constant @configurator_builder.build_global_constant(elem, value) - - # Update backup config - store_config end @@ -441,7 +423,6 @@ def build_supplement(config_base, config_more) # merge our flattened hash with built hash from previous build @project_config_hash.deep_merge!( config_more_flattened ) - store_config() # create more constants and accessors @configurator_setup.build_constants_and_accessors(config_more_flattened, binding()) diff --git a/lib/ceedling/configurator_builder.rb b/lib/ceedling/configurator_builder.rb index a0d296ba..a2eb0b70 100644 --- a/lib/ceedling/configurator_builder.rb +++ b/lib/ceedling/configurator_builder.rb @@ -170,13 +170,13 @@ def set_build_paths(in_hash) def set_rakefile_components(in_hash) out_hash = { :project_rakefile_component_files => - [File.join(CEEDLING_LIB, 'ceedling', 'tasks_base.rake'), - File.join(CEEDLING_LIB, 'ceedling', 'tasks_filesystem.rake'), - File.join(CEEDLING_LIB, 'ceedling', 'tasks_tests.rake'), - File.join(CEEDLING_LIB, 'ceedling', 'rules_tests.rake')]} + [File.join(CEEDLING_LIB, 'tasks_base.rake'), + File.join(CEEDLING_LIB, 'tasks_filesystem.rake'), + File.join(CEEDLING_LIB, 'tasks_tests.rake'), + File.join(CEEDLING_LIB, 'rules_tests.rake')]} - out_hash[:project_rakefile_component_files] << File.join(CEEDLING_LIB, 'ceedling', 'rules_release.rake') if (in_hash[:project_release_build]) - out_hash[:project_rakefile_component_files] << File.join(CEEDLING_LIB, 'ceedling', 'tasks_release.rake') if (in_hash[:project_release_build]) + out_hash[:project_rakefile_component_files] << File.join(CEEDLING_LIB, 'rules_release.rake') if (in_hash[:project_release_build]) + out_hash[:project_rakefile_component_files] << File.join(CEEDLING_LIB, 'tasks_release.rake') if (in_hash[:project_release_build]) return out_hash end @@ -227,19 +227,6 @@ def set_build_thread_counts(in_hash) end - def collect_project_options(in_hash) - options = [] - - in_hash[:project_options_paths].each do |path| - options << @file_wrapper.directory_listing( File.join(path, '*' + in_hash[:extension_yaml]) ) - end - - return { - :collection_project_options => options.flatten - } - end - - def expand_all_path_globs(in_hash) out_hash = {} path_keys = [] diff --git a/lib/ceedling/configurator_setup.rb b/lib/ceedling/configurator_setup.rb index 0448712a..50640dec 100644 --- a/lib/ceedling/configurator_setup.rb +++ b/lib/ceedling/configurator_setup.rb @@ -66,8 +66,6 @@ def vendor_frameworks(flattened_config) end def build_project_collections(flattened_config) - flattened_config.merge!( @configurator_builder.collect_project_options( flattened_config ) ) - ### iterate through all entries in paths section and expand any & all globs to actual paths flattened_config.merge!( @configurator_builder.expand_all_path_globs( flattened_config ) ) @@ -126,10 +124,6 @@ def validate_paths(config) end end - config[:project][:options_paths].each do |path| - valid &= @configurator_validator.validate_filepath_simple( path, :project, :options_paths ) - end - config[:plugins][:load_paths].each do |path| valid &= @configurator_validator.validate_filepath_simple( path, :plugins, :load_paths ) end diff --git a/lib/ceedling/configurator_validator.rb b/lib/ceedling/configurator_validator.rb index fb4ebfa4..9d8584e9 100644 --- a/lib/ceedling/configurator_validator.rb +++ b/lib/ceedling/configurator_validator.rb @@ -14,7 +14,7 @@ def exists?(config, *keys) exist = !hash[:value].nil? if (not exist) - walk = @reportinator.generate_config_walk( keys, hash[:depth] ) + walk = @reportinator.generate_config_walk( keys ) @streaminator.stderr_puts("ERROR: Required config file entry #{walk} does not exist.", Verbosity::ERRORS ) end diff --git a/lib/ceedling/constants.rb b/lib/ceedling/constants.rb index 6ad6493f..ab525d6b 100644 --- a/lib/ceedling/constants.rb +++ b/lib/ceedling/constants.rb @@ -38,9 +38,7 @@ class StdErrRedirect TCSH = :tcsh end -unless defined?(PROJECT_ROOT) - PROJECT_ROOT = Dir.pwd() -end +DEFAULT_PROJECT_FILENAME = 'project.yml' GENERATED_DIR_PATH = [['vendor', 'ceedling'], 'src', "test", ['test', 'support'], 'build'].each{|p| File.join(*p)} @@ -74,9 +72,7 @@ class StdErrRedirect CMOCK_C_FILE = 'cmock.c' CMOCK_H_FILE = 'cmock.h' - -DEFAULT_CEEDLING_MAIN_PROJECT_FILE = 'project.yml' unless defined?(DEFAULT_CEEDLING_MAIN_PROJECT_FILE) # main project file -DEFAULT_CEEDLING_USER_PROJECT_FILE = 'user.yml' unless defined?(DEFAULT_CEEDLING_USER_PROJECT_FILE) # supplemental user config file +DEFAULT_CEEDLING_LOGFILE = 'ceedling.log' INPUT_CONFIGURATION_CACHE_FILE = 'input.yml' unless defined?(INPUT_CONFIGURATION_CACHE_FILE) # input configuration file dump DEFINES_DEPENDENCY_CACHE_FILE = 'defines_dependency.yml' unless defined?(DEFINES_DEPENDENCY_CACHE_FILE) # preprocessor definitions for files diff --git a/lib/ceedling/debugger_utils.rb b/lib/ceedling/debugger_utils.rb index b258281c..408bf603 100644 --- a/lib/ceedling/debugger_utils.rb +++ b/lib/ceedling/debugger_utils.rb @@ -63,19 +63,19 @@ def collect_cmd_output_with_gdb(command, cmd, test_case=nil) # @return Array - list of the test_cases defined in test_file_runner def collect_list_of_test_cases(command) all_test_names = command.clone - all_test_names[:line] += @unity_utils.additional_test_run_args('', 'list_test_cases') + all_test_names[:line] += @unity_utils.additional_test_run_args( '', :list_test_cases ) test_list = @tool_executor.exec(all_test_names) test_runner_tc = test_list[:output].split("\n").drop(1) # Clean collected test case names # Filter tests which contain test_case_name passed by `--test_case` argument - if ENV['CEEDLING_INCLUDE_TEST_CASE_NAME'] - test_runner_tc.delete_if { |i| !(i =~ /#{ENV['CEEDLING_INCLUDE_TEST_CASE_NAME']}/) } + if !@configurator.include_test_case.empty? + test_runner_tc.delete_if { |i| !(i =~ /#{@configurator.include_test_case}/) } end # Filter tests which contain test_case_name passed by `--exclude_test_case` argument - if ENV['CEEDLING_EXCLUDE_TEST_CASE_NAME'] - test_runner_tc.delete_if { |i| i =~ /#{ENV['CEEDLING_EXCLUDE_TEST_CASE_NAME']}/ } + if !@configurator.exclude_test_case.empty? + test_runner_tc.delete_if { |i| i =~ /#{@configurator.exclude_test_case}/ } end test_runner_tc @@ -117,7 +117,7 @@ def gdb_output_collector(shell_result) test_case_list_to_execute = collect_list_of_test_cases(@command_line) test_case_list_to_execute.each do |test_case_name| test_run_cmd = @command_line.clone - test_run_cmd_with_args = test_run_cmd[:line] + @unity_utils.additional_test_run_args(test_case_name, 'test_case') + test_run_cmd_with_args = test_run_cmd[:line] + @unity_utils.additional_test_run_args( test_case_name, :test_case ) test_output, exec_time = collect_cmd_output_with_gdb(test_run_cmd, test_run_cmd_with_args, test_case_name) # Concatenate execution time between tests @@ -126,8 +126,9 @@ def gdb_output_collector(shell_result) # Concatenate test results from single test runs, which not crash # to create proper output for further parser - if test_output =~ /([\S]+):(\d+):([\S]+):(IGNORE|PASS|FAIL:)(.*)/ - test_output = "#{Regexp.last_match(1)}:#{Regexp.last_match(2)}:#{Regexp.last_match(3)}:#{Regexp.last_match(4)}#{Regexp.last_match(5)}" + m = test_output.match /([\S]+):(\d+):([\S]+):(IGNORE|PASS|FAIL:)(.*)/ + if m + test_output = "#{m[1]}:#{m[2]}:#{m[3]}:#{m[4]}#{m[5]}" if test_output =~ /:PASS/ test_case_result_collector[:passed] += 1 elsif test_output =~ /:IGNORE/ @@ -137,27 +138,23 @@ def gdb_output_collector(shell_result) end else # <-- Parse Segmentatation Fault output section --> - - # Withdraw test_name from gdb output - test_name = if test_output =~ /<(.*)>/ - Regexp.last_match(1) - else - '' - end - - # Collect file_name and line in which Segmentation fault have his beginning - if test_output =~ /#{test_name}\s\(\)\sat\s(.*):(\d+)\n/ + + # Collect file_name and line in which Segmentation faulted test is beginning + m = test_output.match /#{test_case_name}\s*\(\)\sat\s(.*):(\d+)\n/ + if m # Remove path from file_name - file_name = Regexp.last_match(1).to_s.split('/').last.split('\\').last + file_name = m[1].to_s.split('/').last.split('\\').last # Save line number - line = Regexp.last_match(2) + line = m[2] # Replace: # - '\n' by @new_line_tag to make gdb output flat # - ':' by @colon_tag to avoid test results problems # to enable parsing output for default generator_test_results regex test_output = test_output.gsub("\n", @new_line_tag).gsub(':', @colon_tag) - test_output = "#{file_name}:#{line}:#{test_name}:FAIL: #{test_output}" + test_output = "#{file_name}:#{line}:#{test_case_name}:FAIL: #{test_output}" + else + test_output = "ERR:1:#{test_case_name}:FAIL: Segmentation Fault" end # Mark test as failure diff --git a/lib/ceedling/defaults.rb b/lib/ceedling/defaults.rb index c832c233..4131734d 100644 --- a/lib/ceedling/defaults.rb +++ b/lib/ceedling/defaults.rb @@ -7,12 +7,11 @@ CEEDLING_PLUGINS = [] unless defined? CEEDLING_PLUGINS DEFAULT_TEST_COMPILER_TOOL = { - :executable => ENV['CC'].nil? ? FilePathUtils.os_executable_ext('gcc').freeze : ENV['CC'], + :executable => ENV['TEST_CC'].nil? ? FilePathUtils.os_executable_ext('gcc').freeze : ENV['TEST_CC'], :name => 'default_test_compiler'.freeze, :stderr_redirect => StdErrRedirect::NONE.freeze, :optional => false.freeze, :arguments => [ - ENV['CC'].nil? ? "" : ENV['CC'].split[1..-1], ENV['CPPFLAGS'].nil? ? "" : ENV['CPPFLAGS'].split, "-I\"${5}\"".freeze, # Per-test executable search paths "-D\"${6}\"".freeze, # Per-test executable defines @@ -28,13 +27,12 @@ } DEFAULT_TEST_ASSEMBLER_TOOL = { - :executable => ENV['AS'].nil? ? FilePathUtils.os_executable_ext('as').freeze : ENV['AS'], + :executable => ENV['TEST_AS'].nil? ? FilePathUtils.os_executable_ext('as').freeze : ENV['TEST_AS'], :name => 'default_test_assembler'.freeze, :stderr_redirect => StdErrRedirect::NONE.freeze, :optional => false.freeze, :arguments => [ - ENV['AS'].nil? ? "" : ENV['AS'].split[1..-1], - ENV['ASFLAGS'].nil? ? "" : ENV['ASFLAGS'].split, + ENV['TEST_ASFLAGS'].nil? ? "" : ENV['TEST_ASFLAGS'].split, "-I\"${3}\"".freeze, # Search paths # Anny defines (${4}) are not included since GNU assembler ignores them "\"${1}\"".freeze, @@ -43,20 +41,19 @@ } DEFAULT_TEST_LINKER_TOOL = { - :executable => ENV['CCLD'].nil? ? FilePathUtils.os_executable_ext('gcc').freeze : ENV['CCLD'], + :executable => ENV['TEST_CCLD'].nil? ? FilePathUtils.os_executable_ext('gcc').freeze : ENV['TEST_CCLD'], :name => 'default_test_linker'.freeze, :stderr_redirect => StdErrRedirect::NONE.freeze, :optional => false.freeze, :arguments => [ - ENV['CCLD'].nil? ? "" : ENV['CCLD'].split[1..-1], - ENV['CFLAGS'].nil? ? "" : ENV['CFLAGS'].split, - ENV['LDFLAGS'].nil? ? "" : ENV['LDFLAGS'].split, + ENV['TEST_CFLAGS'].nil? ? "" : ENV['TEST_CFLAGS'].split, + ENV['TEST_LDFLAGS'].nil? ? "" : ENV['TEST_LDFLAGS'].split, "${1}".freeze, "${5}".freeze, "-o \"${2}\"".freeze, "".freeze, "${4}".freeze, - ENV['LDLIBS'].nil? ? "" : ENV['LDLIBS'].split + ENV['TEST_LDLIBS'].nil? ? "" : ENV['TEST_LDLIBS'].split ].freeze } @@ -69,13 +66,12 @@ } DEFAULT_TEST_SHALLOW_INCLUDES_PREPROCESSOR_TOOL = { - :executable => ENV['CC'].nil? ? FilePathUtils.os_executable_ext('gcc').freeze : ENV['CC'], - :name => 'default_test_includes_preprocessor'.freeze, + :executable => ENV['TEST_CC'].nil? ? FilePathUtils.os_executable_ext('gcc').freeze : ENV['TEST_CC'], + :name => 'default_test_shallow_includes_preprocessor'.freeze, :stderr_redirect => StdErrRedirect::NONE.freeze, :optional => false.freeze, :arguments => [ - ENV['CC'].nil? ? "" : ENV['CC'].split[1..-1], - ENV['CPPFLAGS'].nil? ? "" : ENV['CPPFLAGS'].split, + ENV['TEST_CPPFLAGS'].nil? ? "" : ENV['TEST_CPPFLAGS'].split, '-E'.freeze, # Run only through preprocessor stage with its output '-MM'.freeze, # Output make rule + suppress header files found in system header directories '-MG'.freeze, # Assume missing header files are generated files (do not discard) @@ -89,13 +85,12 @@ } DEFAULT_TEST_NESTED_INCLUDES_PREPROCESSOR_TOOL = { - :executable => ENV['CC'].nil? ? FilePathUtils.os_executable_ext('gcc').freeze : ENV['CC'], - :name => 'default_test_includes_preprocessor'.freeze, + :executable => ENV['TEST_CC'].nil? ? FilePathUtils.os_executable_ext('gcc').freeze : ENV['TEST_CC'], + :name => 'default_test_nested_includes_preprocessor'.freeze, :stderr_redirect => StdErrRedirect::NONE.freeze, :optional => false.freeze, :arguments => [ - ENV['CC'].nil? ? "" : ENV['CC'].split[1..-1], - ENV['CPPFLAGS'].nil? ? "" : ENV['CPPFLAGS'].split, + ENV['TEST_CPPFLAGS'].nil? ? "" : ENV['TEST_CPPFLAGS'].split, '-E'.freeze, # Run only through preprocessor stage with its output '-MM'.freeze, # Output make rule + suppress header files found in system header directories '-MG'.freeze, # Assume missing header files are generated files (do not discard) @@ -110,13 +105,12 @@ } DEFAULT_TEST_FILE_PREPROCESSOR_TOOL = { - :executable => ENV['CC'].nil? ? FilePathUtils.os_executable_ext('gcc').freeze : ENV['CC'], + :executable => ENV['TEST_CC'].nil? ? FilePathUtils.os_executable_ext('gcc').freeze : ENV['TEST_CC'], :name => 'default_test_file_preprocessor'.freeze, :stderr_redirect => StdErrRedirect::NONE.freeze, :optional => false.freeze, :arguments => [ - ENV['CC'].nil? ? "" : ENV['CC'].split[1..-1], - ENV['CPPFLAGS'].nil? ? "" : ENV['CPPFLAGS'].split, + ENV['TEST_CPPFLAGS'].nil? ? "" : ENV['TEST_CPPFLAGS'].split, '-E'.freeze, "-I\"${4}\"".freeze, # Per-test executable search paths "-D\"${3}\"".freeze, # Per-test executable defines @@ -128,24 +122,6 @@ ].freeze } -DEFAULT_TEST_FILE_PREPROCESSOR_DIRECTIVES_TOOL = { - :executable => FilePathUtils.os_executable_ext('gcc').freeze, - :name => 'default_test_file_preprocessor_directives'.freeze, - :stderr_redirect => StdErrRedirect::NONE.freeze, - :optional => false.freeze, - :arguments => [ - '-E'.freeze, - "-I\"${4}\"".freeze, # Per-test executable search paths - "-D\"${3}\"".freeze, # Per-test executable defines - "-DGNU_COMPILER".freeze, - '-fdirectives-only'.freeze, - # '-nostdinc'.freeze, # disabled temporarily due to stdio access violations on OSX - "-x c".freeze, # Force C language - "\"${1}\"".freeze, - "-o \"${2}\"".freeze - ].freeze - } - # Disable the -MD flag for OSX LLVM Clang, since unsupported if RUBY_PLATFORM =~ /darwin/ && `gcc --version 2> /dev/null` =~ /Apple LLVM version .* \(clang/m # OSX w/LLVM Clang MD_FLAG = '' # Clang doesn't support the -MD flag @@ -154,13 +130,12 @@ end DEFAULT_TEST_DEPENDENCIES_GENERATOR_TOOL = { - :executable => ENV['CC'].nil? ? FilePathUtils.os_executable_ext('gcc').freeze : ENV['CC'], + :executable => ENV['TEST_CC'].nil? ? FilePathUtils.os_executable_ext('gcc').freeze : ENV['TEST_CC'], :name => 'default_test_dependencies_generator'.freeze, :stderr_redirect => StdErrRedirect::NONE.freeze, :optional => false.freeze, :arguments => [ - ENV['CC'].nil? ? "" : ENV['CC'].split[1..-1], - ENV['CPPFLAGS'].nil? ? "" : ENV['CPPFLAGS'].split, + ENV['TEST_CPPFLAGS'].nil? ? "" : ENV['TEST_CPPFLAGS'].split, '-E'.freeze, "-I\"${5}\"".freeze, # Per-test executable search paths "-D\"${4}\"".freeze, # Per-test executable defines @@ -177,13 +152,12 @@ } DEFAULT_RELEASE_DEPENDENCIES_GENERATOR_TOOL = { - :executable => ENV['CC'].nil? ? FilePathUtils.os_executable_ext('gcc').freeze : ENV['CC'], + :executable => ENV['RELEASE_CC'].nil? ? FilePathUtils.os_executable_ext('gcc').freeze : ENV['RELEASE_CC'], :name => 'default_release_dependencies_generator'.freeze, :stderr_redirect => StdErrRedirect::NONE.freeze, :optional => false.freeze, :arguments => [ - ENV['CC'].nil? ? "" : ENV['CC'].split[1..-1], - ENV['CPPFLAGS'].nil? ? "" : ENV['CPPFLAGS'].split, + ENV['RELEASE_CPPFLAGS'].nil? ? "" : ENV['RELEASE_CPPFLAGS'].split, '-E'.freeze, {"-I\"$\"" => 'COLLECTION_PATHS_SOURCE_INCLUDE_VENDOR'}.freeze, {"-I\"$\"" => 'COLLECTION_PATHS_RELEASE_TOOLCHAIN_INCLUDE'}.freeze, @@ -202,17 +176,16 @@ } DEFAULT_RELEASE_COMPILER_TOOL = { - :executable => ENV['CC'].nil? ? FilePathUtils.os_executable_ext('gcc').freeze : ENV['CC'], + :executable => ENV['RELEASE_CC'].nil? ? FilePathUtils.os_executable_ext('gcc').freeze : ENV['RELEASE_CC'], :name => 'default_release_compiler'.freeze, :stderr_redirect => StdErrRedirect::NONE.freeze, :optional => false.freeze, :arguments => [ - ENV['CC'].nil? ? "" : ENV['CC'].split[1..-1], - ENV['CPPFLAGS'].nil? ? "" : ENV['CPPFLAGS'].split, + ENV['RELEASE_CPPFLAGS'].nil? ? "" : ENV['RELEASE_CPPFLAGS'].split, "-I\"${5}\"".freeze, # Search paths "-D\"${6}\"".freeze, # Defines "-DGNU_COMPILER".freeze, - ENV['CFLAGS'].nil? ? "" : ENV['CFLAGS'].split, + ENV['RELEASE_CFLAGS'].nil? ? "" : ENV['RELEASE_CFLAGS'].split, "-c \"${1}\"".freeze, "-o \"${2}\"".freeze, # gcc's list file output options are complex; no use of ${3} parameter in default config @@ -222,13 +195,12 @@ } DEFAULT_RELEASE_ASSEMBLER_TOOL = { - :executable => ENV['AS'].nil? ? FilePathUtils.os_executable_ext('as').freeze : ENV['AS'], + :executable => ENV['RELEASE_AS'].nil? ? FilePathUtils.os_executable_ext('as').freeze : ENV['RELEASE_AS'], :name => 'default_release_assembler'.freeze, :stderr_redirect => StdErrRedirect::NONE.freeze, :optional => false.freeze, :arguments => [ - ENV['AS'].nil? ? "" : ENV['AS'].split[1..-1], - ENV['ASFLAGS'].nil? ? "" : ENV['ASFLAGS'].split, + ENV['RELEASE_ASFLAGS'].nil? ? "" : ENV['RELEASE_ASFLAGS'].split, "-I\"${3}\"".freeze, # Search paths "-D\"${4}\"".freeze, # Defines (FYI--allowed with GNU assembler but ignored) "\"${1}\"".freeze, @@ -237,20 +209,19 @@ } DEFAULT_RELEASE_LINKER_TOOL = { - :executable => ENV['CCLD'].nil? ? FilePathUtils.os_executable_ext('gcc').freeze : ENV['CCLD'], + :executable => ENV['RELEASE_CCLD'].nil? ? FilePathUtils.os_executable_ext('gcc').freeze : ENV['RELEASE_CCLD'], :name => 'default_release_linker'.freeze, :stderr_redirect => StdErrRedirect::NONE.freeze, :optional => false.freeze, :arguments => [ - ENV['CCLD'].nil? ? "" : ENV['CCLD'].split[1..-1], - ENV['CFLAGS'].nil? ? "" : ENV['CFLAGS'].split, - ENV['LDFLAGS'].nil? ? "" : ENV['LDFLAGS'].split, + ENV['RELEASE_CFLAGS'].nil? ? "" : ENV['RELEASE_CFLAGS'].split, + ENV['RELEASE_LDFLAGS'].nil? ? "" : ENV['RELEASE_LDFLAGS'].split, "\"${1}\"".freeze, "${5}".freeze, "-o \"${2}\"".freeze, "".freeze, "${4}".freeze, - ENV['LDLIBS'].nil? ? "" : ENV['LDLIBS'].split + ENV['RELEASE_LDLIBS'].nil? ? "" : ENV['RELEASE_LDLIBS'].split ].freeze } @@ -289,7 +260,6 @@ :test_shallow_includes_preprocessor => DEFAULT_TEST_SHALLOW_INCLUDES_PREPROCESSOR_TOOL, :test_nested_includes_preprocessor => DEFAULT_TEST_NESTED_INCLUDES_PREPROCESSOR_TOOL, :test_file_preprocessor => DEFAULT_TEST_FILE_PREPROCESSOR_TOOL, - :test_file_preprocessor_directives => DEFAULT_TEST_FILE_PREPROCESSOR_DIRECTIVES_TOOL, } } @@ -329,7 +299,6 @@ :test_threads => 1, :use_test_preprocessor => false, :test_file_prefix => 'test_', - :options_paths => [], :release_build => false, :use_backtrace => false, :debug => false @@ -421,6 +390,7 @@ }, :test_runner => { + :cmdline_args => false, :includes => [], :file_suffix => '_runner', }, diff --git a/lib/ceedling/dependinator.rb b/lib/ceedling/dependinator.rb index f62b4baf..72eabfb7 100644 --- a/lib/ceedling/dependinator.rb +++ b/lib/ceedling/dependinator.rb @@ -1,7 +1,7 @@ class Dependinator - constructor :configurator, :project_config_manager, :test_context_extractor, :file_path_utils, :rake_wrapper, :file_wrapper + constructor :configurator, :test_context_extractor, :file_path_utils, :rake_wrapper, :file_wrapper def load_release_object_deep_dependencies(dependencies_list) diff --git a/lib/ceedling/file_path_collection_utils.rb b/lib/ceedling/file_path_collection_utils.rb index 7d3e5a9c..76b4adc3 100644 --- a/lib/ceedling/file_path_collection_utils.rb +++ b/lib/ceedling/file_path_collection_utils.rb @@ -33,7 +33,8 @@ def collect_paths(paths) # Expand paths using Ruby's Dir.glob() # - A simple path will yield that path # - A path glob will expand to one or more paths - @file_wrapper.directory_listing( _reformed ).each do |entry| + # Note: `sort()` becuase of Github Issue #860 + @file_wrapper.directory_listing( _reformed ).sort.each do |entry| # For each result, add it to the working list *only* if it's a directory # Previous validation has already made warnings about filepaths in the list dirs << entry if @file_wrapper.directory?(entry) @@ -102,7 +103,8 @@ def revise_filelist(list, revisions) filepaths = [] # Expand path by pattern as needed and add only filepaths to working list - @file_wrapper.directory_listing( path ).each do |entry| + # Note: `sort()` becuase of Github Issue #860 + @file_wrapper.directory_listing( path ).sort.each do |entry| filepaths << File.expand_path( entry ) if !@file_wrapper.directory?( entry ) end diff --git a/lib/ceedling/file_wrapper.rb b/lib/ceedling/file_wrapper.rb index 59662fca..37cc85ac 100644 --- a/lib/ceedling/file_wrapper.rb +++ b/lib/ceedling/file_wrapper.rb @@ -1,6 +1,7 @@ require 'rubygems' require 'rake' # for FileList require 'fileutils' +require 'pathname' require 'ceedling/constants' @@ -24,16 +25,21 @@ def extname(filepath) return File.extname(filepath) end + # Is path a directory and does it exist? def directory?(path) return File.directory?(path) end + def relative?(path) + return Pathname.new( path).relative? + end + def dirname(path) return File.dirname(path) end def directory_listing(glob) - return Dir.glob(glob, File::FNM_PATHNAME) + return Dir.glob(glob, File::FNM_PATHNAME) # Case insensitive globs end def rm_f(filepath, options={}) diff --git a/lib/ceedling/loginator.rb b/lib/ceedling/loginator.rb index 29a5ea2b..d31c7212 100644 --- a/lib/ceedling/loginator.rb +++ b/lib/ceedling/loginator.rb @@ -1,24 +1,28 @@ class Loginator - attr_accessor :project_logging, :project_log_filepath + attr_reader :project_logging constructor :file_wrapper, :system_wrapper - def setup() @project_logging = false - @project_log_filepath = nil + @log_filepath = nil + end + + def set_logfile( log_filepath ) + if !log_filepath.empty? + @project_logging = true + @log_filepath = log_filepath + end end - def log(string, heading=nil) - return if (not @project_logging) or @project_log_filepath.nil? + def log(string, heading='') + return if not @project_logging - output = "\n[#{@system_wrapper.time_now}]" - output += " :: #{heading}" if (not heading.nil?) - output += "\n#{string.strip}\n" + output = "#{heading} | #{@system_wrapper.time_now}\n#{string.strip}\n" - @file_wrapper.write( @project_log_filepath, output, 'a' ) + @file_wrapper.write( @log_filepath, output, 'a' ) end end diff --git a/lib/ceedling/objects.yml b/lib/ceedling/objects.yml index c12e47b8..e7a73845 100644 --- a/lib/ceedling/objects.yml +++ b/lib/ceedling/objects.yml @@ -36,13 +36,6 @@ file_path_collection_utils: compose: - file_wrapper -project_file_loader: - compose: - - yaml_wrapper - - streaminator - - system_wrapper - - file_wrapper - unity_utils: compose: - configurator @@ -53,12 +46,6 @@ debugger_utils: - tool_executor - unity_utils -project_config_manager: - compose: - - cacheinator - - yaml_wrapper - - file_wrapper - cacheinator: compose: - cacheinator_helper @@ -200,7 +187,6 @@ task_invoker: - build_batchinator - rake_utils - rake_wrapper - - project_config_manager config_matchinator: compose: @@ -268,7 +254,6 @@ generator_test_runner: dependinator: compose: - configurator - - project_config_manager - test_context_extractor - file_path_utils - rake_wrapper @@ -284,7 +269,6 @@ preprocessinator: - file_wrapper - yaml_wrapper - plugin_manager - - project_config_manager - configurator - test_context_extractor - streaminator diff --git a/lib/ceedling/preprocessinator.rb b/lib/ceedling/preprocessinator.rb index 8b292a77..1d820d9c 100644 --- a/lib/ceedling/preprocessinator.rb +++ b/lib/ceedling/preprocessinator.rb @@ -9,7 +9,6 @@ class Preprocessinator :file_wrapper, :yaml_wrapper, :plugin_manager, - :project_config_manager, :configurator, :test_context_extractor, :streaminator, diff --git a/lib/ceedling/preprocessinator_includes_handler.rb b/lib/ceedling/preprocessinator_includes_handler.rb index ec8be552..80417d29 100644 --- a/lib/ceedling/preprocessinator_includes_handler.rb +++ b/lib/ceedling/preprocessinator_includes_handler.rb @@ -9,6 +9,7 @@ class PreprocessinatorIncludesHandler ## ============================ ## ## BACKGROUND + ## -------- ## #include extraction is hard to do. In simple cases a regex approach suffices, but nested header files, ## clever macros, and conditional preprocessing statements easily introduce high complexity. ## diff --git a/lib/ceedling/project_config_manager.rb b/lib/ceedling/project_config_manager.rb deleted file mode 100644 index 32c8dd5f..00000000 --- a/lib/ceedling/project_config_manager.rb +++ /dev/null @@ -1,29 +0,0 @@ -require 'ceedling/constants' - - -class ProjectConfigManager - - attr_reader :options_files, :release_config_changed - attr_accessor :config_hash - - constructor :cacheinator, :yaml_wrapper, :file_wrapper - - - def setup - @options_files = [] - @release_config_changed = false - end - - - def merge_options(config_hash, option_filepath) - @options_files << File.basename( option_filepath ) - config_hash.deep_merge!( @yaml_wrapper.load( option_filepath ) ) - end - - - def process_release_config_change - # has project configuration changed since last release build - @release_config_changed = @cacheinator.diff_cached_release_config?( @config_hash ) - end - -end diff --git a/lib/ceedling/project_file_loader.rb b/lib/ceedling/project_file_loader.rb deleted file mode 100644 index 06085b2d..00000000 --- a/lib/ceedling/project_file_loader.rb +++ /dev/null @@ -1,97 +0,0 @@ -require 'ceedling/constants' - - -class ProjectFileLoader - - attr_reader :main_file, :user_file - - constructor :yaml_wrapper, :streaminator, :system_wrapper, :file_wrapper - - def setup - @main_file = nil - @mixin_files = [] - @user_file = nil - - @main_project_filepath = '' - @mixin_project_filepaths = [] - @user_project_filepath = '' - end - - - def find_project_files - # first go hunting for optional user project file by looking for environment variable and then default location on disk - user_filepath = @system_wrapper.env_get('CEEDLING_USER_PROJECT_FILE') - - if ( not user_filepath.nil? and @file_wrapper.exist?(user_filepath) ) - @user_project_filepath = user_filepath - elsif (@file_wrapper.exist?(DEFAULT_CEEDLING_USER_PROJECT_FILE)) - @user_project_filepath = DEFAULT_CEEDLING_USER_PROJECT_FILE - end - - # next check for mixin project files by looking for environment variable - mixin_filepaths = @system_wrapper.env_get('CEEDLING_MIXIN_PROJECT_FILES') - if ( not mixin_filepaths.nil? ) - mixin_filepaths.split(File::PATH_SEPARATOR).each do |filepath| - if ( @file_wrapper.exist?(filepath) ) - @mixin_project_filepaths.push(filepath) - end - end - end - - # next check for main project file by looking for environment variable and then default location on disk; - # blow up if we don't find this guy -- like, he's so totally important - main_filepath = @system_wrapper.env_get('CEEDLING_MAIN_PROJECT_FILE') - - if ( not main_filepath.nil? and @file_wrapper.exist?(main_filepath) ) - @main_project_filepath = main_filepath - elsif (@file_wrapper.exist?(DEFAULT_CEEDLING_MAIN_PROJECT_FILE)) - @main_project_filepath = DEFAULT_CEEDLING_MAIN_PROJECT_FILE - else - @streaminator.stderr_puts( 'ERROR: Found no Ceedling project file (*.yml)', Verbosity::ERRORS ) - raise - end - - @main_file = File.basename( @main_project_filepath ) - @mixin_project_filepaths.each do |filepath| - @mixin_files.push(File.basename( filepath )) - end - @user_file = File.basename( @user_project_filepath ) if ( not @user_project_filepath.empty? ) - end - - def yaml_merger(y1, y2) - o1 = y1 - y2.each_pair do |k,v| - if o1[k].nil? - o1[k] = v - else - if (o1[k].instance_of? Hash) - o1[k] = yaml_merger(o1[k], v) - elsif (o1[k].instance_of? Array) - o1[k] += v - else - o1[k] = v - end - end - end - return o1 - end - - def load_project_config - config_hash = @yaml_wrapper.load(@main_project_filepath) - - # if there are mixin project files, then use them - @mixin_project_filepaths.each do |filepath| - mixin = @yaml_wrapper.load(filepath) - config_hash = yaml_merger( config_hash, mixin ) - end - - # if there's a user project file, then use it - if ( not @user_project_filepath.empty? ) - user_hash = @yaml_wrapper.load(@user_project_filepath) - config_hash = yaml_merger( config_hash, user_hash ) - end - - return config_hash - end - -end diff --git a/lib/ceedling/rakefile.rb b/lib/ceedling/rakefile.rb index 6c0cfb1c..27aa5d7e 100644 --- a/lib/ceedling/rakefile.rb +++ b/lib/ceedling/rakefile.rb @@ -1,14 +1,6 @@ require 'fileutils' -# get directory containing this here file, back up one directory, and expand to full path -CEEDLING_ROOT = File.expand_path(File.dirname(__FILE__) + '/../..') -CEEDLING_LIB = File.join(CEEDLING_ROOT, 'lib') -CEEDLING_VENDOR = File.join(CEEDLING_ROOT, 'vendor') -CEEDLING_RELEASE = File.join(CEEDLING_ROOT, 'release') - -$LOAD_PATH.unshift( CEEDLING_LIB ) $LOAD_PATH.unshift( File.join(CEEDLING_VENDOR, 'unity/auto') ) -$LOAD_PATH.unshift( File.join(CEEDLING_VENDOR, 'diy/lib') ) $LOAD_PATH.unshift( File.join(CEEDLING_VENDOR, 'cmock/lib') ) require 'rake' @@ -16,15 +8,12 @@ # Let's make sure we remember the task descriptions in case we need them Rake::TaskManager.record_task_metadata = true -require 'diy' -require 'constructor' -require 'ceedling/constants' -require 'ceedling/target_loader' require 'ceedling/system_wrapper' require 'ceedling/reportinator' -require 'deep_merge' -def log_runtime(run, start_time_s, end_time_s) +# Operation duration logging +def log_runtime(run, start_time_s, end_time_s, enabled) + return if !enabled return if !defined?(PROJECT_VERBOSITY) return if (PROJECT_VERBOSITY < Verbosity::ERRORS) @@ -32,50 +21,41 @@ def log_runtime(run, start_time_s, end_time_s) return if duration.empty? - puts( "\nCeedling #{run} completed in #{duration}" ) + puts( "\n🌱 Ceedling #{run} completed in #{duration}" ) end +# Centralized last resort, outer exception handling def boom_handler(exception:, debug:) - $stderr.puts("#{exception.class} ==> #{exception.message}") + $stderr.puts("🌱 #{exception.class} ==> #{exception.message}") if debug $stderr.puts("Backtrace ==>") $stderr.puts(exception.backtrace) end - abort # Rake's abort + exit(1) end -# Exists in external scope -start_time = nil +start_time = nil # Outside scope of exception handling # Top-level exception handling for any otherwise un-handled exceptions, particularly around startup begin # Redefine start_time with actual timestamp before set up begins start_time = SystemWrapper.time_stopwatch_s() - # construct all our objects - # ensure load path contains all libraries needed first - lib_ceedling_load_path_temp = File.join(CEEDLING_LIB, 'ceedling') - $LOAD_PATH.unshift( lib_ceedling_load_path_temp ) - @ceedling = DIY::Context.from_yaml( File.read( File.join(lib_ceedling_load_path_temp, 'objects.yml') ) ) - @ceedling.build_everything - # now that all objects are built, delete 'lib/ceedling' from load path - $LOAD_PATH.delete(lib_ceedling_load_path_temp) - # one-stop shopping for all our setup and such after construction - @ceedling[:setupinator].ceedling = @ceedling - - project_config = - begin - cfg = @ceedling[:setupinator].load_project_files - TargetLoader.inspect(cfg, ENV['TARGET']) - rescue TargetLoader::NoTargets - cfg - rescue TargetLoader::RequestReload - @ceedling[:setupinator].load_project_files - end + # Construct all objects + # 1. Add full path to $LOAD_PATH to simplify objects.yml + # 2. Perform object construction + dependency injection + # 3. Remove full path from $LOAD_PATH + $LOAD_PATH.unshift( CEEDLING_LIB ) + @ceedling = DIY::Context.from_yaml( File.read( File.join( CEEDLING_LIB, 'objects.yml' ) ) ) + @ceedling.build_everything() + $LOAD_PATH.delete( CEEDLING_LIB ) - @ceedling[:setupinator].do_setup( project_config ) + # One-stop shopping for all our setup and such after construction + @ceedling[:setupinator].ceedling = @ceedling + @ceedling[:setupinator].do_setup( CEEDLING_APPCFG ) - log_runtime( 'set up', start_time, SystemWrapper.time_stopwatch_s() ) + setup_done = SystemWrapper.time_stopwatch_s() + log_runtime( 'set up', start_time, setup_done, CEEDLING_APPCFG[:stopwatch] ) # Configure high-level verbosity unless defined?(PROJECT_DEBUG) and PROJECT_DEBUG @@ -99,7 +79,7 @@ def boom_handler(exception:, debug:) # Reset start_time before operations begins start_time = SystemWrapper.time_stopwatch_s() - # tell all our plugins we're about to do something + # Tell all our plugins we're about to do something @ceedling[:plugin_manager].pre_build # load rakefile component files (*.rake) @@ -108,6 +88,13 @@ def boom_handler(exception:, debug:) boom_handler( exception:e, debug:(defined?(PROJECT_DEBUG) && PROJECT_DEBUG) ) end +def test_failures_handler() + graceful_fail = CEEDLING_APPCFG[:tests_graceful_fail] + + # $stdout test reporting plugins store test failures + exit(1) if @ceedling[:plugin_manager].plugins_failed? && !graceful_fail +end + # End block always executed following rake run END { $stdout.flush unless $stdout.nil? @@ -117,25 +104,25 @@ def boom_handler(exception:, debug:) @ceedling[:cacheinator].cache_test_config( @ceedling[:setupinator].config_hash ) if (@ceedling[:task_invoker].test_invoked?) @ceedling[:cacheinator].cache_release_config( @ceedling[:setupinator].config_hash ) if (@ceedling[:task_invoker].release_invoked?) - graceful_fail = @ceedling[:setupinator].config_hash[:graceful_fail] - # Only perform these final steps if we got here without runtime exceptions or errors if (@ceedling[:application].build_succeeded?) # Tell all our plugins the build is done and process results begin @ceedling[:plugin_manager].post_build @ceedling[:plugin_manager].print_plugin_failures - log_runtime( 'operations', start_time, SystemWrapper.time_stopwatch_s() ) - exit(1) if @ceedling[:plugin_manager].plugins_failed? && !graceful_fail + ops_done = SystemWrapper.time_stopwatch_s() + log_runtime( 'operations', start_time, ops_done, CEEDLING_APPCFG[:stopwatch] ) + test_failures_handler() if (@ceedling[:task_invoker].test_invoked? || @ceedling[:task_invoker].invoked?(/^gcov:/)) rescue => ex - log_runtime( 'operations', start_time, SystemWrapper.time_stopwatch_s() ) + ops_done = SystemWrapper.time_stopwatch_s() + log_runtime( 'operations', start_time, ops_done, CEEDLING_APPCFG[:stopwatch] ) boom_handler( exception:ex, debug:(defined?(PROJECT_DEBUG) && PROJECT_DEBUG) ) exit(1) end exit(0) else - puts("\nCeedling could not complete operations because of errors.") + puts("\n🌱 Ceedling could not complete operations because of errors.") begin @ceedling[:plugin_manager].post_error rescue => ex diff --git a/lib/ceedling/rules_release.rake b/lib/ceedling/rules_release.rake index 6477c72d..9eb93b12 100644 --- a/lib/ceedling/rules_release.rake +++ b/lib/ceedling/rules_release.rake @@ -77,7 +77,6 @@ namespace RELEASE_SYM do end ]) do |compile| @ceedling[:rake_wrapper][:prepare].invoke - @ceedling[:project_config_manager].process_release_config_change @ceedling[:release_invoker].setup_and_invoke_objects( [compile.source] ) end end @@ -91,7 +90,6 @@ namespace RELEASE_SYM do end ]) do |assemble| @ceedling[:rake_wrapper][:prepare].invoke - @ceedling[:project_config_manager].process_release_config_change @ceedling[:release_invoker].setup_and_invoke_objects( [assemble.source] ) end end diff --git a/lib/ceedling/rules_tests.rake b/lib/ceedling/rules_tests.rake index a37d8041..a44d2ffb 100644 --- a/lib/ceedling/rules_tests.rake +++ b/lib/ceedling/rules_tests.rake @@ -31,8 +31,6 @@ namespace TEST_SYM do :test_fixture => TOOLS_TEST_FIXTURE } - @ceedling[:unity_utils].create_test_runner_additional_args - # use rules to increase efficiency for large projects (instead of iterating through all sources and creating defined tasks) rule(/^#{TEST_TASK_ROOT}\S+$/ => [ # test task names by regex proc do |task_name| diff --git a/lib/ceedling/setupinator.rb b/lib/ceedling/setupinator.rb index c2e94107..731a41b2 100644 --- a/lib/ceedling/setupinator.rb +++ b/lib/ceedling/setupinator.rb @@ -18,18 +18,18 @@ def inspect end - def load_project_files - @ceedling[:project_file_loader].find_project_files - return @ceedling[:project_file_loader].load_project_config - end + def do_setup( app_cfg ) + @config_hash = app_cfg[:project_config] + log_filepath = app_cfg[:log_filepath] - def do_setup(config_hash) - @config_hash = config_hash + @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 ) @@ -37,9 +37,8 @@ def do_setup(config_hash) @ceedling[:configurator].eval_paths( config_hash ) @ceedling[:configurator].standardize_paths( config_hash ) @ceedling[:configurator].find_and_merge_plugins( config_hash ) - @ceedling[:configurator].merge_imports( config_hash ) @ceedling[:configurator].tools_setup( config_hash ) - @ceedling[:configurator].validate( config_hash ) + @ceedling[:configurator].validate_final( config_hash ) # Partially flatten config + build Configurator accessors and globals @ceedling[:configurator].build( config_hash, :environment ) @@ -52,9 +51,15 @@ def do_setup(config_hash) @ceedling[:configurator].build_supplement( config_hash, env ) end + # Inject dependencies for plugin needs @ceedling[:plugin_reportinator].set_system_objects( @ceedling ) - @ceedling[:loginator].project_log_filepath = form_log_filepath() - @ceedling[:project_config_manager].config_hash = config_hash + + # Process options for additional test runner #defines and test runner command line arguments + @ceedling[:unity_utils].process_test_runner_build_options() + + # Logging set up + @ceedling[:loginator].set_logfile( form_log_filepath( log_filepath ) ) + @ceedling[:configurator].project_logging = @ceedling[:loginator].project_logging end def reset_defaults(config_hash) @@ -65,24 +70,17 @@ def reset_defaults(config_hash) private - def form_log_filepath() - # Various project files and options files can combine to create different configurations. - # Different configurations means different behaviors. - # As these variations are easy to run from the command line, a resulting log file - # should differentiate its context. - # We do this by concatenating config/options names into a log filename. + def form_log_filepath( log_filepath ) + # Bail out early if logging is disabled + return log_filepath if log_filepath.empty?() - config_files = [] - - config_files << @ceedling[:project_file_loader].main_file - config_files << @ceedling[:project_file_loader].user_file - config_files += @ceedling[:project_config_manager].options_files - config_files.compact! # Remove empties - - # Drop component file name extensions and smoosh together with underscores - log_name = config_files.map{ |file| file.ext('') }.join( '_' ) + # 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 ) + end - return File.join( @ceedling[:configurator].project_log_path, log_name.ext('.log') ) + # Otherwise, log filepath includes a directory (that's already been created) + return log_filepath end end diff --git a/lib/ceedling/target_loader.rb b/lib/ceedling/target_loader.rb deleted file mode 100644 index f1e95120..00000000 --- a/lib/ceedling/target_loader.rb +++ /dev/null @@ -1,38 +0,0 @@ -module TargetLoader - class NoTargets < RuntimeError; end - class NoDirectory < RuntimeError; end - class NoDefault < RuntimeError; end - class NoSuchTarget < RuntimeError; end - - class RequestReload < RuntimeError; end - - def self.inspect(config, target_name=nil) - unless config[:targets] - raise NoTargets - end - - targets = config[:targets] - unless targets[:targets_directory] - raise NoDirectory.new("No targets directory specified.") - end - unless targets[:default_target] - raise NoDefault.new("No default target specified.") - end - - target_path = lambda {|name| File.join(targets[:targets_directory], name + ".yml")} - - target = if target_name - target_path.call(target_name) - else - target_path.call(targets[:default_target]) - end - - unless File.exist? target - raise NoSuchTarget.new("No such target: #{target}") - end - - ENV['CEEDLING_MAIN_PROJECT_FILE'] = target - - raise RequestReload - end -end diff --git a/lib/ceedling/task_invoker.rb b/lib/ceedling/task_invoker.rb index d1b5d525..33cd1b51 100644 --- a/lib/ceedling/task_invoker.rb +++ b/lib/ceedling/task_invoker.rb @@ -3,7 +3,7 @@ class TaskInvoker attr_accessor :first_run - constructor :dependinator, :build_batchinator, :rake_utils, :rake_wrapper, :project_config_manager + constructor :dependinator, :build_batchinator, :rake_utils, :rake_wrapper def setup @test_regexs = [/^#{TEST_ROOT_NAME}:/] diff --git a/lib/ceedling/tasks_base.rake b/lib/ceedling/tasks_base.rake index eb273792..99fcc783 100644 --- a/lib/ceedling/tasks_base.rake +++ b/lib/ceedling/tasks_base.rake @@ -1,6 +1,5 @@ require 'ceedling/constants' require 'ceedling/file_path_utils' -require 'ceedling/version' # Set Rake verbosity using global constant verbosity set before Rake is loaded if !!defined?(PROJECT_VERBOSITY) @@ -11,63 +10,6 @@ if !!defined?(PROJECT_VERBOSITY) end end -desc "Display build environment version info." -task :version do - puts " Ceedling:: #{Ceedling::Version::CEEDLING}" - puts " Unity:: #{Ceedling::Version::UNITY}" - puts " CMock:: #{Ceedling::Version::CMOCK}" - puts " CException:: #{Ceedling::Version::CEXCEPTION}" -end - -desc "Set verbose output numerically (silent:[#{Verbosity::SILENT}] - debug:[#{Verbosity::DEBUG}])." -task :verbosity, :level do |t, args| - # Most of setting verbosity has been moved to command line processing before Rake. - level = args.level.to_i - - if level >= Verbosity::OBNOXIOUS - Rake.application.options.silent = false - Rake.application.options.suppress_backtrace_pattern = nil - end - - if level < Verbosity::SILENT or level > Verbosity::DEBUG - puts("WARNING: Verbosity level #{level} is outside of the recognized range [#{Verbosity::SILENT}-#{Verbosity::DEBUG}]") - end -end - -namespace :verbosity do - desc "Set verbose output by named level." - task :* do - message = "\nOops! 'verbosity:*' isn't a real task. " + - "Replace '*' with a named level (see verbosity:list).\n\n" - - @ceedling[:streaminator].stdout_puts( message, Verbosity::ERRORS ) - end - - # Most of setting verbosity has been moved to command line processing before Rake. - VERBOSITY_OPTIONS.each_pair do |key, val| - task key do - if val >= Verbosity::OBNOXIOUS - Rake.application.options.silent = false - Rake.application.options.suppress_backtrace_pattern = nil - end - end - end - - # Offer a handy list of verbosity levels - desc "Available verbosity levels by name" - task :list do - VERBOSITY_OPTIONS.keys.each do |key| - puts " - verbosity:#{key}" - end - end -end - -desc "Enable logging" -task :logging do - @ceedling[:configurator].project_logging = true - @ceedling[:loginator].project_logging = true -end - # Non-advertised debug task task :debug do Rake.application.options.trace = true @@ -85,59 +27,7 @@ task :upgrade do puts "again if you'd like to perform an upgrade." end -# list expanded environment variables -if (not ENVIRONMENT.empty?) -desc "List all configured environment variables." -task :environment do - env_list = [] - ENVIRONMENT.each do |env| - env.each_key do |key| - name = key.to_s.upcase - env_list.push(" - #{name}: \"#{env[key]}\"") - end - end - env_list.sort.each do |env_line| - puts env_line - end -end -end - -namespace :options do - - COLLECTION_PROJECT_OPTIONS.each do |option_path| - option = File.basename(option_path, '.yml') - - desc "Merge #{option} project options." - task option.to_sym do - hash = @ceedling[:project_config_manager].merge_options( @ceedling[:setupinator].config_hash, option_path ) - @ceedling[:setupinator].do_setup( hash ) - if @ceedling[:configurator].project_release_build - load(File.join(CEEDLING_LIB, 'ceedling', 'rules_release.rake')) - end - end - end - - # This is to give nice errors when typing options - rule /^options:.*/ do |t, args| - filename = t.to_s.split(':')[-1] + '.yml' - filelist = COLLECTION_PROJECT_OPTIONS.map{|s| File.basename(s) } - @ceedling[:file_finder].find_file_from_list(filename, filelist, :error) - end - - # This will output the fully-merged tools options to their own project.yml file - desc "Export tools options to a new project file" - task :export, :filename do |t, args| - outfile = args.filename || 'tools.yml' - toolcfg = {} - @ceedling[:configurator].project_config_hash.each_pair do |k,v| - toolcfg[k] = v if (k.to_s[0..5] == 'tools_') - end - File.open(outfile,'w') {|f| f << toolcfg.to_yaml({:indentation => 2})} - end -end - - -# do not present task if there's no plugins +# Do not present task if there's no plugins if (not PLUGINS_ENABLED.empty?) desc "Execute plugin result summaries (no build triggering)." task :summary do diff --git a/lib/ceedling/tasks_release.rake b/lib/ceedling/tasks_release.rake index 9101d16d..5288c059 100644 --- a/lib/ceedling/tasks_release.rake +++ b/lib/ceedling/tasks_release.rake @@ -13,7 +13,6 @@ task RELEASE_SYM => [:prepare] do core_objects = [] extra_objects = @ceedling[:file_path_utils].form_release_build_objects_filelist( COLLECTION_RELEASE_ARTIFACT_EXTRA_LINK_OBJECTS ) - @ceedling[:project_config_manager].process_release_config_change() core_objects.concat( @ceedling[:release_invoker].setup_and_invoke_objects( COLLECTION_RELEASE_BUILD_INPUT ) ) # If we're using libraries, we need to add those to our collection as well @@ -24,7 +23,7 @@ task RELEASE_SYM => [:prepare] do rescue StandardError => e @ceedling[:application].register_build_failure - @ceedling[:streaminator].stderr_puts("#{e.class} ==> #{e.message}", Verbosity::ERRORS) + @ceedling[:streaminator].stderr_puts(🌱 "#{e.class} ==> #{e.message}", Verbosity::ERRORS) # Debug backtrace @ceedling[:streaminator].stderr_puts("Backtrace ==>", Verbosity::DEBUG) diff --git a/lib/ceedling/test_invoker.rb b/lib/ceedling/test_invoker.rb index ceec6a90..9838482b 100644 --- a/lib/ceedling/test_invoker.rb +++ b/lib/ceedling/test_invoker.rb @@ -350,7 +350,7 @@ def setup_and_invoke(tests:, context:TEST_SYM, options:{}) # Runtime errors (parent is Exception) continue on up to be caught by Ruby itself. rescue StandardError => e @application.register_build_failure - @streaminator.stderr_puts("#{e.class} ==> #{e.message}", Verbosity::ERRORS) + @streaminator.stderr_puts("🌱 #{e.class} ==> #{e.message}", Verbosity::ERRORS) # Debug backtrace @streaminator.stderr_puts("Backtrace ==>", Verbosity::DEBUG) diff --git a/lib/ceedling/tool_validator.rb b/lib/ceedling/tool_validator.rb index aa29186c..e1bcc6b1 100644 --- a/lib/ceedling/tool_validator.rb +++ b/lib/ceedling/tool_validator.rb @@ -8,7 +8,7 @@ class ToolValidator constructor :file_wrapper, :streaminator, :system_wrapper, :reportinator - def validate(tool:, name:nil, extension:, respect_optional:false, boom:false) + def validate(tool:, name:nil, extension:EXTENSION_EXECUTABLE, respect_optional:false, boom:false) # Redefine name with name inside tool hash if it's not provided # If the name is provided it's likely the formatted key path into the configuration file name = tool[:name] if name.nil? or name.empty? diff --git a/lib/ceedling/unity_utils.rb b/lib/ceedling/unity_utils.rb index bf69f7e4..4945b1af 100644 --- a/lib/ceedling/unity_utils.rb +++ b/lib/ceedling/unity_utils.rb @@ -1,20 +1,16 @@ +require 'ceedling/exceptions' + # The Unity utils class, # Store functions to enable test execution of single test case under test file # and additional warning definitions class UnityUtils - attr_reader :test_runner_disabled_replay, :arg_option_map - attr_accessor :test_case_incl, :test_case_excl, :not_supported constructor :configurator def setup - @test_runner_disabled_replay = "NOTICE: \n" \ - "The option[s]: %.s \ncannot be applied." \ - 'To enable it, please add `:cmdline_args` under' \ - ' :test_runner option in your project.yml.' @test_case_incl = '' @test_case_excl = '' - @not_supported = '' + @test_runner_defines = [] # Refering to Unity implementation of the parser implemented in the unit.c : # @@ -25,10 +21,10 @@ def setup # case 'x': /* exclude tests with name including this string */ @arg_option_map = { - 'test_case' => 'f', - 'list_test_cases' => 'l', - 'run_tests_verbose' => 'v', - 'exclude_test_case' => 'x' + :test_case => 'f', + :list_test_cases => 'l', + :run_tests_verbose => 'v', + :exclude_test_case => 'x' } end @@ -50,12 +46,13 @@ def additional_test_run_args(argument, option) return nil if argument.nil? - raise TypeError, 'option expects an arg_option_map key' unless \ - option.is_a?(String) - raise 'Unknown Unity argument option' unless \ - @arg_option_map.key?(option) + if !@arg_option_map.key?(option) + keys = @arg_option_map.keys.map{|key| ':' + key.to_s}.join(', ') + error = "option argument must be a known key {#{keys}}" + raise TypeError.new( error ) + end - " -#{@arg_option_map[option]} #{argument} " + return " -#{@arg_option_map[option]} #{argument}" end # Return test case arguments @@ -66,43 +63,50 @@ def collect_test_runner_additional_args end # Parse passed by user arguments - def create_test_runner_additional_args - if ENV['CEEDLING_INCLUDE_TEST_CASE_NAME'] - if @configurator.project_config_hash[:test_runner_cmdline_args] - @test_case_incl += additional_test_run_args( - ENV['CEEDLING_INCLUDE_TEST_CASE_NAME'], - 'test_case') - else - @not_supported += "\n\t--test_case" - end - end + def process_test_runner_build_options() + # Blow up immediately if things aren't right + return if !test_runner_cmdline_args_configured?() - if ENV['CEEDLING_EXCLUDE_TEST_CASE_NAME'] - if @configurator.project_config_hash[:test_runner_cmdline_args] - @test_case_excl += additional_test_run_args( - ENV['CEEDLING_EXCLUDE_TEST_CASE_NAME'], - 'exclude_test_case') - else - @not_supported += "\n\t--exclude_test_case" - end + @test_runner_defines << 'UNITY_USE_COMMAND_LINE_ARGS' + + if !@configurator.include_test_case.nil? && !@configurator.include_test_case.empty? + @test_case_incl += additional_test_run_args( @configurator.include_test_case, :test_case ) end - if ENV['CEEDLING_EXCLUDE_TEST_CASE_NAME'] || ENV['CEEDLING_INCLUDE_TEST_CASE_NAME'] - print_warning_about_not_enabled_cmdline_args + if !@configurator.exclude_test_case.nil? && !@configurator.exclude_test_case.empty? + @test_case_excl += additional_test_run_args( @configurator.exclude_test_case, :exclude_test_case ) end end - # Return UNITY_USE_COMMAND_LINE_ARGS define required by Unity to - # compile unity with enabled cmd line arguments + # Return UNITY_USE_COMMAND_LINE_ARGS define required by Unity to compile unity with enabled cmd line arguments # # @return [Array] - empty if cmdline_args is not set def grab_additional_defines_based_on_configuration() - @configurator.project_config_hash[:test_runner_cmdline_args] ? ['UNITY_USE_COMMAND_LINE_ARGS'] : [] + return @test_runner_defines end - # Print on output console warning about lack of support for single test run - # if cmdline_args is not set to true in project.yml file, that - def print_warning_about_not_enabled_cmdline_args - puts(format(@test_runner_disabled_replay, opt: @not_supported)) unless @not_supported.empty? + ### Private ### + + private + + # Raise exception if lacking support for test case matching + def test_runner_cmdline_args_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 diff --git a/mixins/.gitignore b/mixins/.gitignore new file mode 100644 index 00000000..e69de29b diff --git a/plugins/beep/lib/beep.rb b/plugins/beep/lib/beep.rb index 05acdda7..4aa76c9a 100755 --- a/plugins/beep/lib/beep.rb +++ b/plugins/beep/lib/beep.rb @@ -39,13 +39,11 @@ def setup # Do not validate the `:bell` tool as it relies on `echo` that could be a shell feature rather than executable @ceedling[:tool_validator].validate( tool: @tools[:beep_on_done], - extension: EXTENSION_EXECUTABLE, boom: true ) if tools[:on_done] != :bell @ceedling[:tool_validator].validate( tool: @tools[:beep_on_error], - extension: EXTENSION_EXECUTABLE, boom: true ) if tools[:on_error] != :bell end diff --git a/plugins/fff/examples/fff_example/project.yml b/plugins/fff/examples/fff_example/project.yml index 25f8ea3e..60fa2275 100644 --- a/plugins/fff/examples/fff_example/project.yml +++ b/plugins/fff/examples/fff_example/project.yml @@ -20,9 +20,6 @@ :test_threads: 8 :compile_threads: 8 - # you can specify different yaml config files which modify the existing one - :options_paths: [] - # enable release build (more details in release_build section below) :release_build: FALSE diff --git a/plugins/gcov/lib/gcov.rb b/plugins/gcov/lib/gcov.rb index 438466f0..ae888ec0 100755 --- a/plugins/gcov/lib/gcov.rb +++ b/plugins/gcov/lib/gcov.rb @@ -14,12 +14,11 @@ def setup @project_config = @ceedling[:configurator].project_config_hash @reports_enabled = reports_enabled?( @project_config[:gcov_reports] ) - # Validate the gcov tools if coverage summaries are enabled (summaries rely on the gcov tool) + # Validate the gcov tools if coverage summaries are enabled (summaries rely on the `gcov` tool) # Note: This gcov tool is a different configuration than the gcov tool used by ReportGenerator if summaries_enabled?( @project_config ) @ceedling[:tool_validator].validate( tool: TOOLS_GCOV_SUMMARY, - extension: EXTENSION_EXECUTABLE, boom: true ) end diff --git a/plugins/gcov/lib/gcovr_reportinator.rb b/plugins/gcov/lib/gcovr_reportinator.rb index edb4b171..43e2331f 100644 --- a/plugins/gcov/lib/gcovr_reportinator.rb +++ b/plugins/gcov/lib/gcovr_reportinator.rb @@ -14,7 +14,6 @@ def initialize(system_objects) # Validate the gcovr tool since it's used to generate reports @ceedling[:tool_validator].validate( tool: TOOLS_GCOV_GCOVR_REPORT, - extension: EXTENSION_EXECUTABLE, boom: true ) diff --git a/plugins/gcov/lib/reportgenerator_reportinator.rb b/plugins/gcov/lib/reportgenerator_reportinator.rb index 734a782c..3d3c8407 100644 --- a/plugins/gcov/lib/reportgenerator_reportinator.rb +++ b/plugins/gcov/lib/reportgenerator_reportinator.rb @@ -15,7 +15,6 @@ def initialize(system_objects) # Validate the `reportgenerator` tool since it's used to generate reports @ceedling[:tool_validator].validate( tool: TOOLS_GCOV_REPORTGENERATOR_REPORT, - extension: EXTENSION_EXECUTABLE, boom: true ) @@ -23,7 +22,6 @@ def initialize(system_objects) # Note: This gcov tool is a different configuration than the gcov tool used for coverage summaries @ceedling[:tool_validator].validate( tool: TOOLS_GCOV_REPORT, - extension: EXTENSION_EXECUTABLE, boom: true ) diff --git a/plugins/module_generator/example/project.yml b/plugins/module_generator/example/project.yml index 80ba0096..0586956e 100644 --- a/plugins/module_generator/example/project.yml +++ b/plugins/module_generator/example/project.yml @@ -20,9 +20,6 @@ :test_threads: 8 :compile_threads: 8 - # you can specify different yaml config files which modify the existing one - :options_paths: [] - # enable release build (more details in release_build section below) :release_build: FALSE @@ -146,7 +143,7 @@ :defines: - UNITY_EXCLUDE_FLOAT -# Configuration options specify to Unity's test runner generator +# Configuration options specific to Unity's test runner generator :test_runner: :cmdline_args: FALSE diff --git a/plugins/report_build_warnings_log/lib/report_build_warnings_log.rb b/plugins/report_build_warnings_log/lib/report_build_warnings_log.rb index e41964dd..9d8e62de 100644 --- a/plugins/report_build_warnings_log/lib/report_build_warnings_log.rb +++ b/plugins/report_build_warnings_log/lib/report_build_warnings_log.rb @@ -16,7 +16,7 @@ def setup # Ceedling can run with multiple threads, provide a lock to use around @warnings @mutex = Mutex.new() - # Get default (default.yml) / user-set log filename in project.yml + # Get default (default.yml) / user-set log filename in project configuration @log_filename = @ceedling[:configurator].report_build_warnings_log_filename # Convenient instance variable references diff --git a/plugins/report_tests_teamcity_stdout/README.md b/plugins/report_tests_teamcity_stdout/README.md index 9297ffff..82204db0 100644 --- a/plugins/report_tests_teamcity_stdout/README.md +++ b/plugins/report_tests_teamcity_stdout/README.md @@ -49,11 +49,11 @@ This may seem silly, right? Why enable the plugin and then disable it, cancelling it out? The answer has to do with _where_ you use the second YAML blurb configuration setting. -Ceedling provides features for applying configurations settings on top of your -core project file. These include options files and user project files. -See _[CeedlingPacket][ceedling-packet]_ for full details. +Ceedling provides Mixins for applying configurations settings on top of your +base project configuraiton file. +See the [Mixins documentation][ceedling-mixins] for full details. -[ceedling-packet]: ../docs/CeedlingPacket.md +[ceedling-mixins]: ../docs/CeedlingPacket.md#base-project-configuration-file-mixins-section-entries As an example, you might enable the plugin in the main project file that is committed to your repository while disabling the plugin in your local user diff --git a/spec/ceedling_spec.rb b/spec/ceedling_spec.rb index 3cb8beb3..d5648a58 100644 --- a/spec/ceedling_spec.rb +++ b/spec/ceedling_spec.rb @@ -39,116 +39,5 @@ expect(location).to eq(rakefile_path) end end - - context 'load_project' do - it 'should load the project with the default yaml file' do - # create test state/variables - ENV.delete('CEEDLING_MAIN_PROJECT_FILE') - rakefile_path = File.expand_path( File.join( File.dirname(__FILE__), '..' ).gsub( 'spec','lib' ) ) - rakefile_path = File.join( rakefile_path, 'lib', 'ceedling', 'rakefile.rb' ) - # mocks/stubs/expected calls - expect(Ceedling).to receive(:load).with(rakefile_path) - # execute method - Ceedling.load_project - # validate results - expect(ENV['CEEDLING_MAIN_PROJECT_FILE']).to eq('./project.yml') - end - - it 'should load the project with the specified yaml file' do - # create test state/variables - ENV.delete('CEEDLING_MAIN_PROJECT_FILE') - rakefile_path = File.expand_path( File.join( File.dirname(__FILE__), '..' ).gsub( 'spec','lib' ) ) - rakefile_path = File.join( rakefile_path, 'lib', 'ceedling', 'rakefile.rb' ) - # mocks/stubs/expected calls - expect(Ceedling).to receive(:load).with(rakefile_path) - # execute method - Ceedling.load_project(config: './foo.yml') - # validate results - expect(ENV['CEEDLING_MAIN_PROJECT_FILE']).to eq('./foo.yml') - end - - it 'should load the project with the yaml file specified by the existing environment variable' do - # create test state/variables - ENV['CEEDLING_MAIN_PROJECT_FILE'] = './bar.yml' - rakefile_path = File.expand_path( File.join( File.dirname(__FILE__), '..' ).gsub( 'spec','lib' ) ) - rakefile_path = File.join( rakefile_path, 'lib', 'ceedling', 'rakefile.rb' ) - # mocks/stubs/expected calls - expect(Ceedling).to receive(:load).with(rakefile_path) - # execute method - Ceedling.load_project - # validate results - expect(ENV['CEEDLING_MAIN_PROJECT_FILE']).to eq('./bar.yml') - end - - it 'should load the project with the specified plugins enabled' do - # create test state/variables - DEFAULT_CEEDLING_CONFIG[:plugins][:enabled].clear() - DEFAULT_CEEDLING_CONFIG[:plugins][:load_paths].clear() - spec_double = double('spec-double') - rakefile_path = File.expand_path( File.join( File.dirname(__FILE__), '..' ).gsub( 'spec','lib' ) ) - rakefile_path = File.join( rakefile_path, 'lib', 'ceedling', 'rakefile.rb' ) - # mocks/stubs/expected calls - expect(Gem::Specification).to receive(:find_by_name).with('ceedling-foo').and_return(spec_double) - expect(spec_double).to receive(:gem_dir).and_return('dummy/path') - expect(Ceedling).to receive(:require).with('ceedling/defaults') - expect(Ceedling).to receive(:load).with(rakefile_path) - # execute method - Ceedling.load_project( config: './foo.yml', - plugins: ['foo']) - # validate results - expect(ENV['CEEDLING_MAIN_PROJECT_FILE']).to eq('./foo.yml') - end - - it 'should set the project root if the root key is provided' do - # create test state/variables - Object.send(:remove_const, :PROJECT_ROOT) - DEFAULT_CEEDLING_CONFIG[:plugins][:enabled].clear() - DEFAULT_CEEDLING_CONFIG[:plugins][:load_paths].clear() - rakefile_path = File.expand_path( File.join( File.dirname(__FILE__), '..' ).gsub( 'spec','lib' ) ) - rakefile_path = File.join( rakefile_path, 'lib', 'ceedling', 'rakefile.rb' ) - # mocks/stubs/expected calls - expect(Ceedling).to receive(:load).with(rakefile_path) - # execute method - Ceedling.load_project( config: './foo.yml', - root: './') - # validate results - expect(ENV['CEEDLING_MAIN_PROJECT_FILE']).to eq('./foo.yml') - expect(PROJECT_ROOT).to eq('./') - end - end - - context 'register_plugin' do - it 'should register a plugin' do - # create test state/variables - DEFAULT_CEEDLING_CONFIG[:plugins][:enabled].clear() - DEFAULT_CEEDLING_CONFIG[:plugins][:load_paths].clear() - spec_double = double('spec-double') - # mocks/stubs/expected calls - expect(Gem::Specification).to receive(:find_by_name).with('ceedling-foo').and_return(spec_double) - expect(spec_double).to receive(:gem_dir).and_return('dummy/path') - expect(Ceedling).to receive(:require).with('ceedling/defaults') - # execute method - Ceedling.register_plugin('foo') - # validate results - expect(DEFAULT_CEEDLING_CONFIG[:plugins][:enabled]).to eq ["foo"] - expect(DEFAULT_CEEDLING_CONFIG[:plugins][:load_paths]).to eq(["dummy/path"]) - end - - it 'should register a plugin with an alternative prefix' do - # create test state/variables - DEFAULT_CEEDLING_CONFIG[:plugins][:enabled].clear() - DEFAULT_CEEDLING_CONFIG[:plugins][:load_paths].clear() - spec_double = double('spec-double') - # mocks/stubs/expected calls - expect(Gem::Specification).to receive(:find_by_name).with('prefix-foo').and_return(spec_double) - expect(spec_double).to receive(:gem_dir).and_return('dummy/path') - expect(Ceedling).to receive(:require).with('ceedling/defaults') - # execute method - Ceedling.register_plugin('foo','prefix-') - # validate results - expect(DEFAULT_CEEDLING_CONFIG[:plugins][:enabled]).to eq(["foo"]) - expect(DEFAULT_CEEDLING_CONFIG[:plugins][:load_paths]).to eq(["dummy/path"]) - end - end end diff --git a/spec/gcov/gcov_deployment_spec.rb b/spec/gcov/gcov_deployment_spec.rb index e15bd3f8..8a1e0693 100644 --- a/spec/gcov/gcov_deployment_spec.rb +++ b/spec/gcov/gcov_deployment_spec.rb @@ -38,12 +38,12 @@ end - describe "command: `ceedling example [example]`" do + describe "command: `ceedling example temp_sensor`" do describe "temp_sensor" do before do @c.with_context do output = `bundle exec ruby -S ceedling example temp_sensor 2>&1` - expect(output).to match(/created!/) + expect(output).to match(/created/) end end diff --git a/spec/gcov/gcov_test_cases_spec.rb b/spec/gcov/gcov_test_cases_spec.rb index f5e58580..310d0893 100644 --- a/spec/gcov/gcov_test_cases_spec.rb +++ b/spec/gcov/gcov_test_cases_spec.rb @@ -58,7 +58,7 @@ def can_test_projects_with_gcov_with_success FileUtils.cp test_asset_path("example_file.c"), 'src/' FileUtils.cp test_asset_path("test_example_file_success.c"), 'test/' - output = `bundle exec ruby -S ceedling gcov:all` + output = `bundle exec ruby -S ceedling gcov:all 2>&1` expect($?.exitstatus).to match(0) # Since a test either pass or are ignored, we return success here expect(output).to match(/TESTED:\s+\d/) expect(output).to match(/PASSED:\s+\d/) @@ -211,10 +211,10 @@ def can_create_gcov_html_report_from_crashing_test_runner_with_enabled_debug_and output_rd = File.read('./build/gcov/results/test_example_file_sigsegv.fail') expect(output_rd =~ /test_add_numbers_will_fail \(\) at test\/test_example_file_sigsegv.c\:14/ ) expect(output).to match(/TESTED:\s+2/) - expect(output).to match(/PASSED:\s+1/) - expect(output).to match(/FAILED:\s+1/) + expect(output).to match(/PASSED:\s+(?:0|1)/) + expect(output).to match(/FAILED:\s+(?:1|2)/) expect(output).to match(/IGNORED:\s+0/) - expect(output).to match(/example_file.c \| Lines executed:50.00% of 4/) + expect(output).to match(/example_file.c \| Lines executed:5?0.00% of 4/) expect(output).to match(/Generating HTML coverage report in 'build\/artifacts\/gcov\/gcovr'\.\.\./) expect(output).to match(/Done/) diff --git a/spec/spec_system_helper.rb b/spec/spec_system_helper.rb index 32579f9d..5d8c355e 100644 --- a/spec/spec_system_helper.rb +++ b/spec/spec_system_helper.rb @@ -56,6 +56,10 @@ def deploy_gem git_repo = File.expand_path(File.join(File.dirname(__FILE__), '..')) bundler_gem_file_data = [ %Q{source "http://rubygems.org/"}, %Q{gem "rake"}, + %Q{gem "constructor"}, + %Q{gem "diy"}, + %Q{gem "thor"}, + %Q{gem "deep_merge"}, %Q{gem "ceedling", :path => '#{git_repo}'} ].join("\n") @@ -169,15 +173,15 @@ def can_create_projects expect(File.exist?("src")).to eq true expect(File.exist?("test")).to eq true expect(File.exist?("test/support")).to eq true - expect(File.exist?("test/support/.gitkeep")).to eq true end end end - def has_an_ignore + def has_git_support @c.with_context do Dir.chdir @proj_name do expect(File.exist?(".gitignore")).to eq true + expect(File.exist?("test/support/.gitkeep")).to eq true end end end @@ -186,7 +190,7 @@ def can_upgrade_projects @c.with_context do output = `bundle exec ruby -S ceedling upgrade #{@proj_name} 2>&1` expect($?.exitstatus).to match(0) - expect(output).to match(/upgraded!/i) + expect(output).to match(/Upgraded/i) Dir.chdir @proj_name do expect(File.exist?("project.yml")).to eq true expect(File.exist?("src")).to eq true @@ -208,7 +212,7 @@ def can_upgrade_projects_even_if_test_support_folder_does_not_exists File.write("#{@proj_name}/project.yml", updated_prj_yml.join("\n"), mode: 'w') expect($?.exitstatus).to match(0) - expect(output).to match(/upgraded!/i) + expect(output).to match(/Upgraded/i) Dir.chdir @proj_name do expect(File.exist?("project.yml")).to eq true expect(File.exist?("src")).to eq true @@ -222,7 +226,7 @@ def cannot_upgrade_non_existing_project @c.with_context do output = `bundle exec ruby -S ceedling upgrade #{@proj_name} 2>&1` expect($?.exitstatus).to match(1) - expect(output).to match(/rescue in upgrade/i) + expect(output).to match(/Could not find an existing project/i) end end @@ -512,7 +516,6 @@ def can_fetch_project_help expect($?.exitstatus).to match(0) expect(output).to match(/ceedling clean/i) expect(output).to match(/ceedling clobber/i) - expect(output).to match(/ceedling logging/i) expect(output).to match(/ceedling module:create/i) expect(output).to match(/ceedling module:destroy/i) expect(output).to match(/ceedling summary/i) @@ -652,7 +655,7 @@ def exclude_test_case_name_filter_works_and_only_one_test_case_is_executed end end - def run_all_test_when_test_case_name_is_passed_but_cmdline_args_are_disabled_with_success + def run_all_test_when_test_case_name_is_passed_it_will_autoset_cmdline_args @c.with_context do Dir.chdir @proj_name do FileUtils.cp test_asset_path("example_file.h"), 'src/' @@ -662,11 +665,10 @@ def run_all_test_when_test_case_name_is_passed_but_cmdline_args_are_disabled_wit output = `bundle exec ruby -S ceedling test:test_example_file_success --test_case=_adds_numbers 2>&1` expect($?.exitstatus).to match(0) # Since a test either pass or are ignored, we return success here - expect(output).to match(/TESTED:\s+2/) + expect(output).to match(/TESTED:\s+1/) expect(output).to match(/PASSED:\s+1/) expect(output).to match(/FAILED:\s+0/) - expect(output).to match(/IGNORED:\s+1/) - expect(output).to match(/please add `:cmdline_args` under :test_runner option/) + expect(output).to match(/IGNORED:\s+0/) end end end @@ -726,8 +728,8 @@ def execute_all_test_cases_from_crashing_test_runner_and_return_test_report_with output_rd = File.read('./build/test/results/test_example_file_sigsegv.fail') expect(output_rd =~ /test_add_numbers_will_fail \(\) at test\/test_example_file_sigsegv.c\:14/ ) expect(output).to match(/TESTED:\s+2/) - expect(output).to match(/PASSED:\s+1/) - expect(output).to match(/FAILED:\s+1/) + expect(output).to match(/PASSED:\s+(?:0|1)/) + expect(output).to match(/FAILED:\s+(?:1|2)/) expect(output).to match(/IGNORED:\s+0/) end end @@ -751,8 +753,8 @@ def execute_and_collect_debug_logs_from_crashing_test_case_defined_by_test_case_ output_rd = File.read('./build/test/results/test_example_file_sigsegv.fail') expect(output_rd =~ /test_add_numbers_will_fail \(\) at test\/test_example_file_sigsegv.c\:14/ ) expect(output).to match(/TESTED:\s+1/) - expect(output).to match(/PASSED:\s+0/) - expect(output).to match(/FAILED:\s+1/) + expect(output).to match(/PASSED:\s+(?:0|1)/) + expect(output).to match(/FAILED:\s+(?:1|2)/) expect(output).to match(/IGNORED:\s+0/) end end @@ -776,8 +778,8 @@ def execute_and_collect_debug_logs_from_crashing_test_case_defined_by_exclude_te output_rd = File.read('./build/test/results/test_example_file_sigsegv.fail') expect(output_rd =~ /test_add_numbers_will_fail \(\) at test\/test_example_file_sigsegv.c\:14/ ) expect(output).to match(/TESTED:\s+1/) - expect(output).to match(/PASSED:\s+0/) - expect(output).to match(/FAILED:\s+1/) + expect(output).to match(/PASSED:\s+(?:0|1)/) + expect(output).to match(/FAILED:\s+(?:1|2)/) expect(output).to match(/IGNORED:\s+0/) end end diff --git a/spec/system/deployment_spec.rb b/spec/system/deployment_spec.rb index 10608253..808e7628 100644 --- a/spec/system/deployment_spec.rb +++ b/spec/system/deployment_spec.rb @@ -51,25 +51,23 @@ it { exclude_test_case_name_filter_works_and_only_one_test_case_is_executed } it { none_of_test_is_executed_if_test_case_name_passed_does_not_fit_defined_in_test_file_and_cmdline_args_are_enabled } it { none_of_test_is_executed_if_test_case_name_and_exclude_test_case_name_is_the_same } - it { run_all_test_when_test_case_name_is_passed_but_cmdline_args_are_disabled_with_success } + it { run_all_test_when_test_case_name_is_passed_it_will_autoset_cmdline_args } end - describe "deployed in a project's `vendor` directory with gitignore." do + describe "deployed in a project's `vendor` directory with git support." do before do @c.with_context do - `bundle exec ruby -S ceedling new --local --docs --gitignore #{@proj_name} 2>&1` + `bundle exec ruby -S ceedling new --local --docs --gitsupport #{@proj_name} 2>&1` end end it { can_create_projects } - it { has_an_ignore } + it { has_git_support } it { contains_a_vendor_directory } it { contains_documentation } it { can_test_projects_with_success } end - - describe "deployed in a project's `vendor` directory without docs." do before do @c.with_context do @@ -183,19 +181,19 @@ end end - describe "command: `ceedling example [example]`" do + describe "command: `ceedling example temp_sensor`" do describe "temp_sensor" do before do @c.with_context do output = `bundle exec ruby -S ceedling example temp_sensor 2>&1` - expect(output).to match(/created!/) + expect(output).to match(/created/) end end it "should be testable" do @c.with_context do Dir.chdir "temp_sensor" do - @output = `bundle exec ruby -S ceedling test:all` + @output = `bundle exec ruby -S ceedling test:all 2>&1` expect(@output).to match(/TESTED:\s+47/) expect(@output).to match(/PASSED:\s+47/) end diff --git a/spec/target_loader_spec.rb b/spec/target_loader_spec.rb deleted file mode 100644 index c247931e..00000000 --- a/spec/target_loader_spec.rb +++ /dev/null @@ -1,30 +0,0 @@ -require 'spec_helper' -require 'ceedling/target_loader' - - -describe TargetLoader do - describe '.inspect' do - - it 'raises NoTargets if targets does not exist' do - expect{TargetLoader.inspect({})}.to raise_error(TargetLoader::NoTargets) - end - - it 'raises NoDirectory if targets_directory inside of targets does not exist' do - expect{TargetLoader.inspect({:targets => {}})}.to raise_error(TargetLoader::NoDirectory) - end - - it 'raises NoDefault if default_target inside of targets does not exist' do - expect{TargetLoader.inspect({:targets => {:targets_directory => File.join('spec', 'support')}})}.to raise_error(TargetLoader::NoDefault) - end - - it 'raises NoSuchTarget if file does not exist' do - expect{TargetLoader.inspect({:targets => {:targets_directory => File.join('spec', 'other'), :default_target => 'target'}})}.to raise_error(TargetLoader::NoSuchTarget) - end - - it 'raises RequestReload if file exists' do - expect{TargetLoader.inspect({:targets => {:targets_directory => File.join('spec', 'support'), :default_target => 'target'}})}.to raise_error(TargetLoader::RequestReload) - expect{TargetLoader.inspect({:targets => {:targets_directory => File.join('spec', 'support'), :default_target => 'target'}}, 'other_target')}.to raise_error(TargetLoader::RequestReload) - end - - end -end