diff --git a/.github/workflows/linter.yaml b/.github/workflows/linter.yaml index 78bb3234..002829ed 100644 --- a/.github/workflows/linter.yaml +++ b/.github/workflows/linter.yaml @@ -15,7 +15,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: # Full git history is needed to get a proper list of changed files within `super-linter` fetch-depth: 0 diff --git a/.github/workflows/linux.yaml b/.github/workflows/linux.yaml index 1b29e833..d3a35a75 100644 --- a/.github/workflows/linux.yaml +++ b/.github/workflows/linux.yaml @@ -7,7 +7,7 @@ jobs: "rubocop": runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: ruby/setup-ruby@v1 with: ruby-version: 2.6 @@ -21,7 +21,7 @@ jobs: "rspec-linux": runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: ruby/setup-ruby@v1 with: ruby-version: 2.6 @@ -34,7 +34,7 @@ jobs: "TestSomething": runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: ruby/setup-ruby@v1 with: ruby-version: 2.6 @@ -44,12 +44,13 @@ jobs: bundle install cd SampleProjects/TestSomething bundle install + bundle exec arduino_ci.rb --help bundle exec arduino_ci.rb NetworkLib: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: ruby/setup-ruby@v1 with: ruby-version: 2.6 @@ -65,7 +66,7 @@ jobs: SharedLibrary: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: ruby/setup-ruby@v1 with: ruby-version: 2.6 diff --git a/.github/workflows/macos.yaml b/.github/workflows/macos.yaml index 6d8eaa2c..857f547b 100644 --- a/.github/workflows/macos.yaml +++ b/.github/workflows/macos.yaml @@ -7,7 +7,7 @@ jobs: "rubocop": runs-on: macos-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: ruby/setup-ruby@v1 with: ruby-version: 2.6 @@ -21,7 +21,7 @@ jobs: "rspec-macos": runs-on: macos-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: ruby/setup-ruby@v1 with: ruby-version: 2.6 @@ -34,7 +34,7 @@ jobs: "TestSomething": runs-on: macos-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: ruby/setup-ruby@v1 with: ruby-version: 2.6 @@ -44,12 +44,13 @@ jobs: bundle install cd SampleProjects/TestSomething bundle install + bundle exec arduino_ci.rb --help bundle exec arduino_ci.rb NetworkLib: runs-on: macos-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: ruby/setup-ruby@v1 with: ruby-version: 2.6 @@ -65,7 +66,7 @@ jobs: SharedLibrary: runs-on: macos-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: ruby/setup-ruby@v1 with: ruby-version: 2.6 diff --git a/.github/workflows/spelling.yaml b/.github/workflows/spelling.yaml index 3450600c..7beff1e1 100644 --- a/.github/workflows/spelling.yaml +++ b/.github/workflows/spelling.yaml @@ -15,7 +15,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: # Full git history is needed to get a proper list of changed files within `super-linter` fetch-depth: 0 diff --git a/.github/workflows/windows.yaml b/.github/workflows/windows.yaml index 3ebf158b..a67dbda0 100644 --- a/.github/workflows/windows.yaml +++ b/.github/workflows/windows.yaml @@ -7,7 +7,7 @@ jobs: "rubocop": runs-on: windows-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: ruby/setup-ruby@v1 with: ruby-version: 2.6 @@ -21,7 +21,7 @@ jobs: "rspec-windows": runs-on: windows-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: ruby/setup-ruby@v1 with: ruby-version: 2.6 @@ -34,7 +34,7 @@ jobs: TestSomething: runs-on: windows-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: ruby/setup-ruby@v1 with: ruby-version: 2.6 @@ -44,12 +44,13 @@ jobs: bundle install cd SampleProjects/TestSomething bundle install + bundle exec arduino_ci.rb --help bundle exec arduino_ci.rb NetworkLib: runs-on: windows-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: ruby/setup-ruby@v1 with: ruby-version: 2.6 @@ -65,7 +66,7 @@ jobs: SharedLibrary: runs-on: windows-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: ruby/setup-ruby@v1 with: ruby-version: 2.6 diff --git a/CHANGELOG.md b/CHANGELOG.md index c4f710a0..b10b9822 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,16 +7,25 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] ### Added +- C++ definitions of `ARDUINO_CI_COMPILATION_MOCKS` and `ARDUINO_CI_GODMODE` to aid in compilation macros +- `CIConfig.available_override_config_path()` to search for available override files in standard locations +- `CIConfig.override_file_from_project_library` and `CIConfig.override_file_from_example` to expose config locations +- CI runner script now expliclty informs about config overrides +- A project `examples/` directory can now provide its own configuration override file, which provides no new flexibility but simply mirrors the behavior for `tests/`. ### Changed +- `CIConfig` now uses `Pathname` instead of strings ### Deprecated ### Removed +- `CIConfig.with_config`, which was only used internally ### Fixed +- `arduino_ci.rb --help` no longer crashes - Fix missing `LED_BUILTIN` definition for Arduino Due, Zero and Circuit Playground. +- No longer ignore failures if the first step of compiling files for the unit test fails. ### Security @@ -30,7 +39,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Support for `dtostrf()` - Added a CI workflow to lint the code base - Added a CI workflow to check for spelling errors -- Extraction of byes usage in a compiled sketch is now calculated in a method: `ArduinoBackend.last_bytes_usage` +- Extraction of bytes usage in a compiled sketch is now calculated in a method: `ArduinoBackend.last_bytes_usage` - Added ```nano_every``` platform to represent ```arduino:megaavr``` architecture - Working directory is now printed in test runner output - Explicitly include `irb` via rubygems diff --git a/README.md b/README.md index 70a0e04f..cafd0f22 100644 --- a/README.md +++ b/README.md @@ -118,11 +118,11 @@ gem 'arduino_ci', path: '/path/to/development/dir/for/arduino_ci' ### Installing the Dependencies -Fulfilling the `arduino_ci` library dependency is as easy as running either of these two commands: +Fulfilling the `arduino_ci` library dependency is as easy as running one or both of these commands: ```console -$ bundle install # adds packages to global library (may require admin rights) -$ bundle install --path vendor/bundle # adds packages to local library +$ bundle config set --local path 'vendor/bundle' # if you lack administrative privileges to install globally +$ bundle install ``` This will create a `Gemfile.lock` in your project directory, which you may optionally check into source control. A broader introduction to ruby dependencies is outside the scope of this document. @@ -169,7 +169,7 @@ jobs: runTest: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: ruby/setup-ruby@v1 with: ruby-version: 2.6 diff --git a/REFERENCE.md b/REFERENCE.md index 1253e914..7767251e 100644 --- a/REFERENCE.md +++ b/REFERENCE.md @@ -44,6 +44,11 @@ This allows a file (or glob) pattern to be executed in your tests directory, cre This allows a file (or glob) pattern to be executed in your tests directory, creating a blacklist of files to skip. E.g. `--testfile-reject=test_animal_*.cpp` would match `test_animal_cat.cpp` and `test_animal_dog.cpp` (skipping those) and test only `test_plant_rose.cpp`, `test_plant_daisy.cpp`, etc. +### `--min-free-space` option + +This specifies the minimum free SRAM memory for stack/heap, in bytes, that _must_ be leftover after compilation. This value applies globally -- to _all_ platforms that will be included in a test run. + + ### `CUSTOM_INIT_SCRIPT` environment variable If set, testing will execute (using `/bin/sh`) the script referred to by this variable -- relative to the current working directory (i.e. the root directory of the library). The script will _run_ in the Arduino Libraries directory (changing to the Libraries directory, running the script, and returning to the individual library root afterward). This enables use cases like the GitHub action to install custom library versions (i.e. a version of a library that is different than what the library manager would automatically install by name) prior to CI test runs. diff --git a/SampleProjects/TestSomething/examples/.arduino-ci.yml b/SampleProjects/TestSomething/examples/.arduino-ci.yml new file mode 100644 index 00000000..5f233439 --- /dev/null +++ b/SampleProjects/TestSomething/examples/.arduino-ci.yml @@ -0,0 +1,4 @@ +compile: + platforms: + - uno + - due diff --git a/SampleProjects/TestSomething/examples/TestSomethingExample/.arduino-ci.yml b/SampleProjects/TestSomething/examples/TestSomethingExample/.arduino-ci.yml new file mode 100644 index 00000000..5f233439 --- /dev/null +++ b/SampleProjects/TestSomething/examples/TestSomethingExample/.arduino-ci.yml @@ -0,0 +1,4 @@ +compile: + platforms: + - uno + - due diff --git a/cpp/arduino/Arduino.h b/cpp/arduino/Arduino.h index b301ec21..4b433e2b 100644 --- a/cpp/arduino/Arduino.h +++ b/cpp/arduino/Arduino.h @@ -5,6 +5,11 @@ Mock Arduino.h library. Where possible, variable names from the Arduino library are used to avoid conflicts */ + +// signal to the developer that we are in an arduino_ci mocked environment +#define ARDUINO_CI_COMPILATION_MOCKS + + // Chars and strings #include "ArduinoDefines.h" diff --git a/cpp/arduino/ArduinoDefines.h b/cpp/arduino/ArduinoDefines.h index ef80d546..3f4de7ad 100644 --- a/cpp/arduino/ArduinoDefines.h +++ b/cpp/arduino/ArduinoDefines.h @@ -90,6 +90,7 @@ #define TIMER5C 18 #if defined(__AVR_ATmega328P__) || defined(__AVR_ATmega32U4__) || defined(__AVR_ATmega328__) || defined(__AVR_ATmega168__) || defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) || defined(__SAM3X8E__) || defined(__SAMD21G18A__) + // Verified on these platforms, see https://github.com/Arduino-CI/arduino_ci/pull/341#issuecomment-1368118880 #define LED_BUILTIN 13 #endif diff --git a/cpp/arduino/Godmode.h b/cpp/arduino/Godmode.h index ee332025..cac197d5 100644 --- a/cpp/arduino/Godmode.h +++ b/cpp/arduino/Godmode.h @@ -6,6 +6,9 @@ #include "WString.h" #include "PinHistory.h" +// signal to the developer that we are in an arduino_ci mocked environment +#define ARDUINO_CI_GODMODE + // random void randomSeed(unsigned long seed); long random(long vmax); diff --git a/exe/arduino_ci.rb b/exe/arduino_ci.rb index c73522dd..72d455ed 100755 --- a/exe/arduino_ci.rb +++ b/exe/arduino_ci.rb @@ -69,7 +69,6 @@ def self.parse(options) puts " - #{VAR_USE_SUBDIR} - if set, the script will install the library from this subdirectory of the cwd" puts " - #{VAR_EXPECT_EXAMPLES} - if set, testing will fail if no example sketches are present" puts " - #{VAR_EXPECT_UNITTESTS} - if set, testing will fail if no unit tests are present" - puts " - #{VAR_SKIP_LIBPROPS} - if set, testing will skip [experimental] library.properties validation" exit end end @@ -194,6 +193,13 @@ def assured_platform(purpose, name, config) platform_definition end +def inform_override(from_where, &block) + inform("Using configuration override from #{from_where}") do + file = block.call + file.nil? ? "" : file + end +end + # Return true if the file (or one of the dirs containing it) is hidden def file_is_hidden_somewhere?(path) # this is clunkly but pre-2.2-ish ruby doesn't return ascend as an enumerator @@ -436,7 +442,7 @@ def perform_unit_tests(cpp_library, file_config) puts compilers.each do |gcc_binary| # before compiling the tests, build a shared library of everything except the test code - next unless build_shared_library(gcc_binary, p, config, cpp_library) + next @failure_count += 1 unless build_shared_library(gcc_binary, p, config, cpp_library) # now build and run each test using the shared library build above config.allowable_unittest_files(cpp_library.test_files).each do |unittest_path| @@ -489,12 +495,17 @@ def perform_example_compilation_tests(cpp_library, config) return end + inform_override("examples") { config.override_file_from_example(cpp_library.examples_dir) } + ex_config = config.from_example(cpp_library.examples_dir) + library_examples.each do |example_path| example_name = File.basename(example_path) puts inform("Discovered example sketch") { example_name } - ovr_config = config.from_example(example_path) + inform_override("example") { ex_config.override_file_from_example(example_path) } + ovr_config = ex_config.from_example(example_path) + platforms = choose_platform_set(ovr_config, "library example", ovr_config.platforms_to_build, cpp_library.library_properties) # having no platforms defined is probably an error @@ -542,9 +553,13 @@ def perform_example_compilation_tests(cpp_library, config) inform("Working directory") { Dir.pwd } # initialize command and config -config = ArduinoCI::CIConfig.default.from_project_library +default_config = ArduinoCI::CIConfig.default +inform_override("project") { default_config.override_file_from_project_library } +config = default_config.from_project_library + @backend = ArduinoCI::ArduinoInstallation.autolocate! inform("Located arduino-cli binary") { @backend.binary_path.to_s } +inform("Using arduino-cli version") { @backend.version.to_s } if @backend.lib_dir.exist? inform("Found libraries directory") { @backend.lib_dir } else diff --git a/lib/arduino_ci/arduino_backend.rb b/lib/arduino_ci/arduino_backend.rb index 96fca45b..98d190e9 100644 --- a/lib/arduino_ci/arduino_backend.rb +++ b/lib/arduino_ci/arduino_backend.rb @@ -182,7 +182,10 @@ def install_boards(boardfamily) result = if @additional_urls.empty? run_and_capture("core", "install", boardfamily) else - run_and_capture("core", "install", boardfamily, "--additional-urls", @additional_urls.join(",")) + urls = @additional_urls.join(",") + # update the index, then install. if the update step fails, return that result + updater = run_and_capture("core", "update-index", "--additional-urls", urls) + updater[:success] ? run_and_capture("core", "install", boardfamily, "--additional-urls", urls) : updater end result[:success] end diff --git a/lib/arduino_ci/ci_config.rb b/lib/arduino_ci/ci_config.rb index 623100a0..fa69b192 100644 --- a/lib/arduino_ci/ci_config.rb +++ b/lib/arduino_ci/ci_config.rb @@ -1,4 +1,5 @@ require 'yaml' +require 'pathname' # base config (platforms) # project config - .arduino_ci_platforms.yml @@ -58,7 +59,7 @@ class << self def default ret = new ret.instance_variable_set("@is_default", true) - ret.load_yaml(File.expand_path("../../misc/default.yml", __dir__)) + ret.load_yaml((Pathname.new(__dir__) + "../../misc/default.yml").realpath) ret end end @@ -195,35 +196,45 @@ def with_override_config(config_hash) overridden_config end - # Get the config file at a given path, if it exists, and pass that to a block. - # Many config files may exist, but only the first match is used + # Get available configuration file, if one exists # @param base_dir [String] The directory in which to search for a config file - # @param val_when_no_match [Object] The value to return if no config files are found - # @yield [path] Process the configuration file at the given path - # @yieldparam [String] The path of an existing config file - # @yieldreturn [ArduinoCI::CIConfig] a settings object - # @return [ArduinoCI::CIConfig] - def with_config(base_dir, val_when_no_match) - CONFIG_FILENAMES.each do |f| - path = base_dir.nil? ? f : File.join(base_dir, f) - return (yield path) if File.exist?(path) - end - val_when_no_match + # @return [Pathname] the first available config file we could find, or nil + def available_override_config_path(base_dir = nil) + CONFIG_FILENAMES.map { |f| base_dir.nil? ? Pathname.new(f) : base_dir + f }.find(&:exist?) + end + + # Find an available override file from the project directory + # + # @todo this is currently reliant on launching the arduino_ci.rb test runner from + # the correct working directory + # @return [Pathname] A file that can override project config, or nil if none was found + def override_file_from_project_library + available_override_config_path(nil) + end + + # Find an available override file from an example sketch + # + # @param path [Pathname] the path to the example or example directory + # @return [Pathname] A file that can override project config, or nil if none was found + def override_file_from_example(example_path) + base_dir = example_path.directory? ? example_path : example_path.dirname + available_override_config_path(base_dir) end # Produce a configuration, assuming the CI script runs from the working directory of the base project # @return [ArduinoCI::CIConfig] the new settings object def from_project_library - with_config(nil, self) { |path| with_override(path) } + ovr = override_file_from_project_library + ovr.nil? ? self : with_override(ovr) end # Produce a configuration override taken from an Arduino library example path # handle either path to example file or example dir - # @param path [String] the path to the settings yaml file + # @param path [Pathname] the path to the settings yaml file # @return [ArduinoCI::CIConfig] the new settings object def from_example(example_path) - base_dir = File.directory?(example_path) ? example_path : File.dirname(example_path) - with_config(base_dir, self) { |path| with_override(path) } + ovr = override_file_from_example(example_path) + ovr.nil? ? self : with_override(ovr) end # get information about a given platform: board name, package name, compiler stuff, etc diff --git a/lib/arduino_ci/host.rb b/lib/arduino_ci/host.rb index 4cd5e243..1bf9b314 100644 --- a/lib/arduino_ci/host.rb +++ b/lib/arduino_ci/host.rb @@ -30,11 +30,31 @@ def self.which(cmd) nil end + # Execute a shell command and capture stdout, stderr, and status + # + # @see Process.spawn + # @see https://docs.ruby-lang.org/en/2.0.0/Process.html#method-c-spawn + # @return [Hash] with keys "stdout" (String), "stderr" (String), and "success" (bool) def self.run_and_capture(*args, **kwargs) stdout, stderr, status = Open3.capture3(*args, **kwargs) { out: stdout, err: stderr, success: status.exitstatus.zero? } end + # Merge multiple capture results into one aggregate value + # + # @param args [Array] Array of hashes from `run_and_capture` + # @return [Hash] with keys "stdout" (String), "stderr" (String), and "success" (bool) + def self.merge_capture_results(*args) + { + out: args.map { |a| a[:out] }.join, + err: args.map { |a| a[:err] }.join, + success: args.all? { |a| a[:success] } + } + end + + # Execute a shell command + # + # @see system def self.run_and_output(*args, **kwargs) system(*args, **kwargs) end diff --git a/spec/ci_config_spec.rb b/spec/ci_config_spec.rb index c838a6e1..7b00c692 100644 --- a/spec/ci_config_spec.rb +++ b/spec/ci_config_spec.rb @@ -43,7 +43,7 @@ context "hash" do it "converts to hash" do base = ArduinoCI::CIConfig.new - base.load_yaml(File.join(File.dirname(__FILE__), "yaml", "o2.yaml")) + base.load_yaml(Pathname.new(__dir__) + "yaml" + "o2.yaml") expect(base.to_h).to eq( packages: {}, @@ -82,7 +82,7 @@ context "with_override" do it "loads from yaml" do - override_file = File.join(File.dirname(__FILE__), "yaml", "o1.yaml") + override_file = Pathname.new(__dir__) + "yaml" + "o1.yaml" base = ArduinoCI::CIConfig.default expect(base.is_default).to be true combined_config = base.with_override(override_file) @@ -121,10 +121,14 @@ end end - context "with_config" do + context "with overrides from files" do it "loads from yaml" do - override_dir = File.join(File.dirname(__FILE__), "yaml", "override1") + override_dir = Pathname.new(__dir__) + "yaml" + "override1" base_config = ArduinoCI::CIConfig.default + found_override_file = base_config.override_file_from_example(override_dir) + expect(found_override_file).to_not be(nil) + expect(found_override_file).to be_a(Pathname) + expect(found_override_file).to exist combined_config = base_config.from_example(override_dir) expect(combined_config).not_to be nil @@ -188,7 +192,7 @@ "mars.cpp" ]) - override_file = File.join(File.dirname(__FILE__), "yaml", "o1.yaml") + override_file = Pathname.new(__dir__) + "yaml" + "o1.yaml" combined_config = ArduinoCI::CIConfig.default.with_override(override_file) expect(combined_config.unittest_info[:testfiles][:select]).to match_array(["*-*.*"]) expect(combined_config.unittest_info[:testfiles][:reject]).to match_array(["sam-squamsh.*"]) diff --git a/spec/host_spec.rb b/spec/host_spec.rb index b1883461..5d94b93e 100644 --- a/spec/host_spec.rb +++ b/spec/host_spec.rb @@ -71,4 +71,23 @@ def with_tmpdir(path) end end + context "merge_capture_results" do + it "merges results" do + a1 = { out: "one", err: "ONE", success: true } + a2 = { out: "two", err: "TWO", success: false } + a3 = { out: "three", err: "THREE", success: true } + res = ArduinoCI::Host.merge_capture_results(a1, a2, a3) + expect(res[:out]).to eq("onetwothree") + expect(res[:err]).to eq("ONETWOTHREE") + expect(res[:success]).to eq(false) + end + + it "handles empty input" do + res = ArduinoCI::Host.merge_capture_results() + expect(res[:out]).to eq("") + expect(res[:err]).to eq("") + expect(res[:success]).to eq(true) + end + end + end