Skip to content

Commit

Permalink
Merge pull request #218 from ianfixes/2020-11-16_arduino-cli
Browse files Browse the repository at this point in the history
Use `arduino-cli` backend
  • Loading branch information
ianfixes committed Nov 28, 2020
2 parents becf990 + 1f8ba56 commit 7e3ba42
Show file tree
Hide file tree
Showing 38 changed files with 1,043 additions and 1,004 deletions.
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,31 @@ and this project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]
### Added
- Special handling of attempts to run the `arduino_ci.rb` CI script against the ruby library instead of an actual Arduino project
- Explicit checks for attemping to test `arduino_ci` itself as if it were a library, resolving a minor annoyance to this developer.
- Code coverage tooling
- Explicit check and warning for library directory names that do not match our guess of what the library should/would be called
- Symlink tests for `Host`

### Changed
- Arduino backend is now `arduino-cli` version `0.13.0`
- `ArduinoCmd` is now `ArduinoBackend`
- `CppLibrary` now relies largely on `ArduinoBackend` instead of making its own judgements about libraries (metadata, includes, and examples)
- `ArduinoBackend` functionality related to `CppLibrary` now lives in `CppLibrary`
- `CppLibrary` now works in an installation-first manner for exposure to `arduino-cli`'s logic -- without installation, there is no ability to reason about libraries
- `CppLibrary` forces just-in-time recursive dependency installation in order to work sensibly
- `ArduinoBackend` maintains the central "best guess" logic on what a library (on disk) might be named

### Deprecated
- `arduino_ci_remote.rb` CLI switch `--skip-compilation`
- Deprecated `arduino_ci_remote.rb` in favor of `arduino_ci.rb`

### Removed
- `ARDUINO_CI_SKIP_SPLASH_SCREEN_RSPEC_TESTS` no longer affects any tests because there are no longer splash screens since switching to `arduino-cli`

### Fixed
- Mismatches between library names in `library.properties` and the directory names, which can cause cryptic failures
- `LibraryProperties` skips over parse errors instead of crashing: only lines with non-empty keys and non-nil values are recorded

### Security

Expand Down
1 change: 0 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ See `SampleProjects/TestSomething/test/*.cpp` for the existing tests (run by rsp

To speed up testing by targeting only the files you're working on, you can set several environment variables that `bundle exec rspec` will respond to:

* `ARDUINO_CI_SKIP_SPLASH_SCREEN_RSPEC_TESTS`: if set, this will avoid any rspec test that calls the arduino executable (and as such, causes the splash screen to pop up).
* `ARDUINO_CI_SKIP_RUBY_RSPEC_TESTS`: if set, this will skip all tests against ruby code (useful if you are not changing Ruby code).
* `ARDUINO_CI_SKIP_CPP_RSPEC_TESTS`: if set, this will skip all tests against the `TestSomething` sample project (useful if you are not changing C++ code).
* `ARDUINO_CI_SELECT_CPP_TESTS=<glob>`: if set, this will skip all C++ unit tests whose filenames don't match the provided glob (executed in the tests directory)
Expand Down
7 changes: 7 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,10 @@ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }

# Specify your gem's dependencies in arduino_ci.gemspec
gemspec

gem "bundler", "> 1.15", require: false, group: :test
gem "keepachangelog_manager", "~> 0.0.2", require: false, group: :test
gem "rspec", "~> 3.0", require: false, group: :test
gem 'rubocop', '~>0.59.0', require: false, group: :test
gem 'simplecov', require: false, group: :test
gem 'yard', '~>0.9.11', require: false, group: :test
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@

# ArduinoCI Ruby gem (`arduino_ci`)
[![Gem Version](https://badge.fury.io/rb/arduino_ci.svg)](https://rubygems.org/gems/arduino_ci)
# ArduinoCI Ruby gem (`arduino_ci`)
[![Gem Version](https://badge.fury.io/rb/arduino_ci.svg)](https://rubygems.org/gems/arduino_ci)
[![Documentation](http://img.shields.io/badge/docs-rdoc.info-blue.svg)](http://www.rubydoc.info/gems/arduino_ci/0.4.0)
[![Gitter](https://badges.gitter.im/Arduino-CI/arduino_ci.svg)](https://gitter.im/Arduino-CI/arduino_ci?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)

Expand Down Expand Up @@ -36,6 +36,9 @@ For a bare-bones example that you can copy from, see [SampleProjects/DoSomething

The complete set of C++ unit tests for the `arduino_ci` library itself are in the [SampleProjects/TestSomething](SampleProjects/TestSomething) project. The [test files](SampleProjects/TestSomething/test/) are named after the type of feature being tested.

> Arduino expects all libraries to be in a specific `Arduino/libraries` directory on your system. If your library is elsewhere, `arduino_ci` will _automatically_ create a symbolic link in the `libraries` directory that points to the directory of the project being tested. This simplifieds working with project dependencies, but **it can have unintended consequences on Windows systems** because [in some cases deleting a folder that contains a symbolic link to another folder can cause the _entire linked folder_ to be removed instead of just the link itself](https://superuser.com/a/306618).
>
> If you use a Windows system **it is recommended that you only run `arduino_ci` from project directories that are already inside the `libraries` directory**
### You Need Ruby and Bundler

Expand Down
2 changes: 1 addition & 1 deletion SampleProjects/ExcludeSomething/library.properties
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name=TestSomething
name=ExcludeSomething
version=0.1.0
author=Ian Katz <[email protected]>
maintainer=Ian Katz <[email protected]>
Expand Down
2 changes: 1 addition & 1 deletion SampleProjects/NetworkLib/library.properties
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name=Ethernet
name=NetworkLib
version=0.1.0
author=James Foster <[email protected]>
maintainer=James Foster <[email protected]>
Expand Down
6 changes: 0 additions & 6 deletions arduino_ci.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,4 @@ Gem::Specification.new do |spec|

spec.add_dependency "os", "~> 1.0"
spec.add_dependency "rubyzip", "~> 1.2"

spec.add_development_dependency "bundler", "> 1.15"
spec.add_development_dependency "keepachangelog_manager", "~> 0.0.2"
spec.add_development_dependency "rspec", "~> 3.0"
spec.add_development_dependency 'rubocop', '~>0.59.0'
spec.add_development_dependency 'yard', '~>0.9.11'
end
174 changes: 90 additions & 84 deletions exe/arduino_ci.rb
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

@failure_count = 0
@passfail = proc { |result| result ? "✓" : "✗" }
@backend = nil

# Use some basic parsing to allow command-line overrides of config
class Parser
Expand All @@ -29,11 +30,6 @@ def self.parse(options)
output_options[:skip_unittests] = p
end

opts.on("--skip-compilation", "Don't compile example sketches (deprecated)") do |p|
puts "The option --skip-compilation has been deprecated in favor of --skip-examples-compilation"
output_options[:skip_compilation] = p
end

opts.on("--skip-examples-compilation", "Don't compile example sketches") do |p|
output_options[:skip_compilation] = p
end
Expand Down Expand Up @@ -68,11 +64,11 @@ def self.parse(options)
def terminate(final = nil)
puts "Failures: #{@failure_count}"
unless @failure_count.zero? || final
puts "Last message: #{@arduino_cmd.last_msg}"
puts "Last message: #{@backend.last_msg}"
puts "========== Stdout:"
puts @arduino_cmd.last_out
puts @backend.last_out
puts "========== Stderr:"
puts @arduino_cmd.last_err
puts @backend.last_err
end
retcode = @failure_count.zero? ? 0 : 1
exit(retcode)
Expand Down Expand Up @@ -161,7 +157,7 @@ def file_is_hidden_somewhere?(path)
# print out some files
def display_files(pathname)
# `find` doesn't follow symlinks, so we should instead
realpath = pathname.symlink? ? pathname.readlink : pathname
realpath = Host.symlink?(pathname) ? Host.readlink(pathname) : pathname

# suppress directories and dotfile-based things
all_files = realpath.find.select(&:file?)
Expand All @@ -172,25 +168,33 @@ def display_files(pathname)
non_hidden.each { |p| puts "#{margin}#{p}" }
end

def install_arduino_library_dependencies(aux_libraries)
aux_libraries.each do |l|
if @arduino_cmd.library_present?(l)
inform("Using pre-existing library") { l.to_s }
# @return [Array<String>] The list of installed libraries
def install_arduino_library_dependencies(library_names, on_behalf_of, already_installed = [])
installed = already_installed.clone
library_names.map { |n| @backend.library_of_name(n) }.each do |l|
if installed.include?(l)
# do nothing
elsif l.installed?
inform("Using pre-existing dependency of #{on_behalf_of}") { l.name }
else
assure("Installing aux library '#{l}'") { @arduino_cmd.install_library(l) }
assure("Installing dependency of #{on_behalf_of}: '#{l.name}'") do
next nil unless l.install

l.name
end
end
installed << l.name
installed += install_arduino_library_dependencies(l.arduino_library_dependencies, l.name, installed)
end
installed
end

def perform_unit_tests(file_config)
def perform_unit_tests(cpp_library, file_config)
if @cli_options[:skip_unittests]
inform("Skipping unit tests") { "as requested via command line" }
return
end
config = file_config.with_override_config(@cli_options[:ci_config])
cpp_library = ArduinoCI::CppLibrary.new(Pathname.new("."),
@arduino_cmd.lib_dir,
config.exclude_dirs.map(&Pathname.method(:new)))

# check GCC
compilers = config.compilers_to_use
Expand All @@ -214,10 +218,25 @@ def perform_unit_tests(file_config)

# iterate boards / tests
if !cpp_library.tests_dir.exist?
inform_multiline("Skipping unit tests; no tests dir at #{cpp_library.tests_dir}") do
puts " In case that's an error, this is what was found in the library:"
display_files(cpp_library.tests_dir.parent)
true
# alert future me about running the script from the wrong directory, instead of doing the huge file dump
# otherwise, assume that the user might be running the script on a library with no actual unit tests
if Pathname.new(__dir__).parent == Pathname.new(Dir.pwd)
inform_multiline("arduino_ci seems to be trying to test itself") do
[
"arduino_ci (the ruby gem) isn't an arduino project itself, so running the CI test script against",
"the core library isn't really a valid thing to do... but it's easy for a developer (including the",
"owner) to mistakenly do just that. Hello future me, you probably meant to run this against one of",
"the sample projects in SampleProjects/ ... if not, please submit a bug report; what a wild case!"
].each { |l| puts " #{l}" }
false
end
exit(1)
else
inform_multiline("Skipping unit tests; no tests dir at #{cpp_library.tests_dir}") do
puts " In case that's an error, this is what was found in the library:"
display_files(cpp_library.tests_dir.parent)
true
end
end
elsif cpp_library.test_files.empty?
inform_multiline("Skipping unit tests; no test files were found in #{cpp_library.tests_dir}") do
Expand All @@ -228,7 +247,7 @@ def perform_unit_tests(file_config)
elsif config.platforms_to_unittest.empty?
inform("Skipping unit tests") { "no platforms were requested" }
else
install_arduino_library_dependencies(config.aux_libraries_for_unittest)
install_arduino_library_dependencies(config.aux_libraries_for_unittest, "<unittest/libraries>")

config.platforms_to_unittest.each do |p|
config.allowable_unittest_files(cpp_library.test_files).each do |unittest_path|
Expand Down Expand Up @@ -256,39 +275,12 @@ def perform_unit_tests(file_config)
end
end

def perform_compilation_tests(config)
def perform_example_compilation_tests(cpp_library, config)
if @cli_options[:skip_compilation]
inform("Skipping compilation of examples") { "as requested via command line" }
return
end

# index the existing libraries
attempt("Indexing libraries") { @arduino_cmd.index_libraries } unless @arduino_cmd.libraries_indexed

# initialize library under test
installed_library_path = attempt("Installing library under test") do
@arduino_cmd.install_local_library(Pathname.new("."))
end

if !installed_library_path.nil? && installed_library_path.exist?
inform("Library installed at") { installed_library_path.to_s }
else
assure_multiline("Library installed successfully") do
if installed_library_path.nil?
puts @arduino_cmd.last_msg
else
# print out the contents of the deepest directory we actually find
@arduino_cmd.lib_dir.ascend do |path_part|
next unless path_part.exist?

break display_files(path_part)
end
false
end
end
end
library_examples = @arduino_cmd.library_examples(installed_library_path)

# gather up all required boards for compilation so we can install them up front.
# start with the "platforms to unittest" and add the examples
# while we're doing that, get the aux libraries as well
Expand All @@ -297,6 +289,7 @@ def perform_compilation_tests(config)
aux_libraries = Set.new(config.aux_libraries_for_build)
# while collecting the platforms, ensure they're defined

library_examples = cpp_library.example_sketches
library_examples.each do |path|
ovr_config = config.from_example(path)
ovr_config.platforms_to_build.each do |platform|
Expand All @@ -317,35 +310,36 @@ def perform_compilation_tests(config)
# do that, set the URLs, and download the packages
all_packages = example_platform_info.values.map { |v| v[:package] }.uniq.reject(&:nil?)

builtin_packages, external_packages = all_packages.partition { |p| config.package_builtin?(p) }

# inform about builtin packages
all_packages.select { |p| config.package_builtin?(p) }.each do |p|
builtin_packages.each do |p|
inform("Using built-in board package") { p }
end

# make sure any non-builtin package has a URL defined
all_packages.reject { |p| config.package_builtin?(p) }.each do |p|
external_packages.each do |p|
assure("Board package #{p} has a defined URL") { board_package_url[p] }
end

# set up all the board manager URLs.
# we can safely reject nils now, they would be for the builtins
all_urls = all_packages.map { |p| board_package_url[p] }.uniq.reject(&:nil?)
all_urls = external_packages.map { |p| board_package_url[p] }.uniq.reject(&:nil?)

unless all_urls.empty?
assure("Setting board manager URLs") do
@arduino_cmd.board_manager_urls = all_urls
@backend.board_manager_urls = all_urls
end
end

all_packages.each do |p|
external_packages.each do |p|
assure("Installing board package #{p}") do
@arduino_cmd.install_boards(p)
@backend.install_boards(p)
end
end

install_arduino_library_dependencies(aux_libraries)
install_arduino_library_dependencies(aux_libraries, "<compile/libraries>")

last_board = nil
if config.platforms_to_build.empty?
inform("Skipping builds") { "no platforms were requested" }
return
Expand All @@ -356,46 +350,58 @@ def perform_compilation_tests(config)
return
end

attempt("Setting compiler warning level") { @arduino_cmd.set_pref("compiler.warning_level", "all") }

# switching boards takes time, so iterate board first
# _then_ whichever examples match it
examples_by_platform = library_examples.each_with_object({}) do |example_path, acc|
library_examples.each do |example_path|
ovr_config = config.from_example(example_path)
ovr_config.platforms_to_build.each do |p|
acc[p] = [] unless acc.key?(p)
acc[p] << example_path
end
end

examples_by_platform.each do |platform, example_paths|
board = example_platform_info[platform][:board]
assure("Switching to board for #{platform} (#{board})") { @arduino_cmd.use_board(board) } unless last_board == board
last_board = board

example_paths.each do |example_path|
board = example_platform_info[p][:board]
example_name = File.basename(example_path)
attempt("Verifying #{example_name}") do
ret = @arduino_cmd.verify_sketch(example_path)
attempt("Compiling #{example_name} for #{board}") do
ret = @backend.compile_sketch(example_path, board)
unless ret
puts
puts "Last command: #{@arduino_cmd.last_msg}"
puts @arduino_cmd.last_err
puts "Last command: #{@backend.last_msg}"
puts @backend.last_err
end
ret
end
end
end

end

# initialize command and config
config = ArduinoCI::CIConfig.default.from_project_library

@arduino_cmd = ArduinoCI::ArduinoInstallation.autolocate!
inform("Located Arduino binary") { @arduino_cmd.binary_path.to_s }
@backend = ArduinoCI::ArduinoInstallation.autolocate!
inform("Located arduino-cli binary") { @backend.binary_path.to_s }

# initialize library under test
cpp_library_path = Pathname.new(".")
cpp_library = assure("Installing library under test") do
@backend.install_local_library(cpp_library_path)
end

assumed_name = @backend.name_of_library(cpp_library_path)
ondisk_name = cpp_library_path.realpath.basename
if assumed_name != ondisk_name
inform("WARNING") { "Installed library named '#{assumed_name}' has directory name '#{ondisk_name}'" }
end

if !cpp_library.nil?
inform("Library installed at") { cpp_library.path.to_s }
else
# this is a longwinded way of failing, we aren't really "assuring" anything at this point
assure_multiline("Library installed successfully") do
puts @backend.last_msg
false
end
end

install_arduino_library_dependencies(
cpp_library.arduino_library_dependencies,
"<#{ArduinoCI::CppLibrary::LIBRARY_PROPERTIES_FILE}>"
)

perform_unit_tests(config)
perform_compilation_tests(config)
perform_unit_tests(cpp_library, config)
perform_example_compilation_tests(cpp_library, config)

terminate(true)
Loading

0 comments on commit 7e3ba42

Please sign in to comment.