From c6a5abb6573cbdcd127720dbdef95dca88d7dcc7 Mon Sep 17 00:00:00 2001 From: Ian Katz Date: Sun, 22 Nov 2020 14:22:11 -0500 Subject: [PATCH] base CppLibrary on ArduinoBackend --- CHANGELOG.md | 5 + exe/arduino_ci.rb | 133 +++++++-------- exe/arduino_library_location.rb | 4 +- lib/arduino_ci/arduino_backend.rb | 134 +++++++-------- lib/arduino_ci/cpp_library.rb | 240 ++++++++++++++++++--------- lib/arduino_ci/library_properties.rb | 5 + spec/arduino_backend_spec.rb | 39 ++--- spec/arduino_installation_spec.rb | 8 +- spec/ci_config_spec.rb | 17 +- spec/cpp_library_spec.rb | 114 ++++++++----- spec/fake_lib_dir.rb | 44 +++++ spec/library_properties_spec.rb | 7 + spec/testsomething_unittests_spec.rb | 51 +++--- 13 files changed, 495 insertions(+), 306 deletions(-) create mode 100644 spec/fake_lib_dir.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 3815308c..d20b4021 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### 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` diff --git a/exe/arduino_ci.rb b/exe/arduino_ci.rb index 07878014..706edd6e 100755 --- a/exe/arduino_ci.rb +++ b/exe/arduino_ci.rb @@ -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 @@ -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 @@ -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_backend.last_msg}" + puts "Last message: #{@backend.last_msg}" puts "========== Stdout:" - puts @arduino_backend.last_out + puts @backend.last_out puts "========== Stderr:" - puts @arduino_backend.last_err + puts @backend.last_err end retcode = @failure_count.zero? ? 0 : 1 exit(retcode) @@ -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_backend.library_present?(l) - inform("Using pre-existing library") { l.to_s } +# @return [Array] 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_backend.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_backend.lib_dir, - config.exclude_dirs.map(&Pathname.method(:new))) # check GCC compilers = config.compilers_to_use @@ -216,7 +220,7 @@ def perform_unit_tests(file_config) if !cpp_library.tests_dir.exist? # 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)) + 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", @@ -243,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, "") config.platforms_to_unittest.each do |p| config.allowable_unittest_files(cpp_library.test_files).each do |unittest_path| @@ -271,36 +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 - # initialize library under test - installed_library_path = attempt("Installing library under test") do - @arduino_backend.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_backend.last_msg - else - # print out the contents of the deepest directory we actually find - @arduino_backend.lib_dir.ascend do |path_part| - next unless path_part.exist? - - break display_files(path_part) - end - false - end - end - end - library_examples = @arduino_backend.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 @@ -309,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| @@ -329,33 +310,35 @@ 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_backend.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_backend.install_boards(p) + @backend.install_boards(p) end end - install_arduino_library_dependencies(aux_libraries) + install_arduino_library_dependencies(aux_libraries, "") if config.platforms_to_build.empty? inform("Skipping builds") { "no platforms were requested" } @@ -367,41 +350,51 @@ def perform_compilation_tests(config) return end - # 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] - example_paths.each do |example_path| + board = example_platform_info[p][:board] example_name = File.basename(example_path) attempt("Compiling #{example_name} for #{board}") do - ret = @arduino_backend.compile_sketch(example_path, board) + ret = @backend.compile_sketch(example_path, board) unless ret puts - puts "Last command: #{@arduino_backend.last_msg}" - puts @arduino_backend.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_backend = ArduinoCI::ArduinoInstallation.autolocate! -inform("Located arduino-cli binary") { @arduino_backend.binary_path.to_s } +@backend = ArduinoCI::ArduinoInstallation.autolocate! +inform("Located arduino-cli binary") { @backend.binary_path.to_s } + +# initialize library under test +cpp_library = assure("Installing library under test") do + @backend.install_local_library(Pathname.new(".")) +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) diff --git a/exe/arduino_library_location.rb b/exe/arduino_library_location.rb index 0ec9462a..f11e9fc6 100755 --- a/exe/arduino_library_location.rb +++ b/exe/arduino_library_location.rb @@ -2,6 +2,6 @@ require 'arduino_ci' # locate and/or forcibly install Arduino, keep stdout clean -@arduino_backend = ArduinoCI::ArduinoInstallation.autolocate!($stderr) +@backend = ArduinoCI::ArduinoInstallation.autolocate!($stderr) -puts @arduino_backend.lib_dir +puts @backend.lib_dir diff --git a/lib/arduino_ci/arduino_backend.rb b/lib/arduino_ci/arduino_backend.rb index 3d3a69b6..66c0f115 100644 --- a/lib/arduino_ci/arduino_backend.rb +++ b/lib/arduino_ci/arduino_backend.rb @@ -13,22 +13,20 @@ class ArduinoExecutionError < StandardError; end # Wrap the Arduino executable. This requires, in some cases, a faked display. class ArduinoBackend - # Enable a shortcut syntax for command line flags - # @param name [String] What the flag will be called (prefixed with 'flag_') - # @return [void] - # @macro [attach] flag - # The text of the command line flag for $1 - # @!attribute [r] flag_$1 - # @return [String] the text of the command line flag (`$2` in this case) - def self.flag(name, text = nil) - text = "(flag #{name} not defined)" if text.nil? - self.class_eval("def flag_#{name};\"#{text}\";end", __FILE__, __LINE__) - end + # We never even use this in code, it's just here for reference because the backend is picky about it. Used for testing + # @return [String] the only allowable name for the arduino-cli config file. + CONFIG_FILE_NAME = "arduino-cli.yaml".freeze # the actual path to the executable on this platform # @return [Pathname] attr_accessor :binary_path + # If a custom config is deired (i.e. for testing), specify it here. + # Note https://github.com/arduino/arduino-cli/issues/753 : the --config-file option + # is really the director that contains the file + # @return [Pathname] + attr_accessor :config_dir + # @return [String] STDOUT of the most recently-run command attr_reader :last_out @@ -41,14 +39,9 @@ def self.flag(name, text = nil) # @return [Array] Additional URLs for the boards manager attr_reader :additional_urls - # set the command line flags (undefined for now). - # These vary between gui/cli. Inline comments added for greppability - flag :install_boards # flag_install_boards - flag :install_library # flag_install_library - flag :verify # flag_verify - def initialize(binary_path) @binary_path = binary_path + @config_dir = nil @additional_urls = [] @last_out = "" @last_err = "" @@ -60,7 +53,8 @@ def _wrap_run(work_fn, *args, **kwargs) has_env = !args.empty? && args[0].class == Hash env_vars = has_env ? args[0] : {} actual_args = has_env ? args[1..-1] : args # need to shift over if we extracted args - full_args = [binary_path.to_s, "--format", "json"] + actual_args + custom_config = @config_dir.nil? ? [] : ["--config-file", @config_dir.to_s] + full_args = [binary_path.to_s, "--format", "json"] + custom_config + actual_args full_cmd = env_vars.empty? ? full_args : [env_vars] + full_args shell_vars = env_vars.map { |k, v| "#{k}=#{v}" }.join(" ") @@ -85,12 +79,13 @@ def run_and_capture(*args, **kwargs) def capture_json(*args, **kwargs) ret = run_and_capture(*args, **kwargs) ret[:json] = JSON.parse(ret[:out]) + ret end # Get a dump of the entire config # @return [Hash] The configuration def config_dump - capture_json("config", "dump") + capture_json("config", "dump")[:json] end # @return [String] the path to the Arduino libraries directory @@ -135,36 +130,6 @@ def installed_libraries capture_json("lib", "list")[:json] end - # install a library by name - # @param name [String] the library name - # @param version [String] the version to install - # @return [bool] whether the command succeeded - def install_library(library_name, version = nil) - return true if library_present?(library_name) - - fqln = version.nil? ? library_name : "#{library_name}@#{version}" - result = run_and_capture("lib", "install", fqln) - result[:success] - end - - # generate the (very likely) path of a library given its name - # @param library_name [String] The name of the library - # @return [Pathname] The fully qualified library name - def library_path(library_name) - Pathname.new(lib_dir) + library_name - end - - # Determine whether a library is present in the lib dir - # - # Note that `true` doesn't guarantee that the library is valid/installed - # and `false` doesn't guarantee that the library isn't built-in - # - # @param library_name [String] The name of the library - # @return [bool] - def library_present?(library_name) - library_path(library_name).exist? - end - # @param path [String] The sketch to compile # @param boardname [String] The board to use # @return [bool] whether the command succeeded @@ -178,28 +143,61 @@ def compile_sketch(path, boardname) @last_msg = "Can't compile Sketch at nonexistent path '#{path}'!" return false end - ret = run_and_capture("compile", "--fqbn", boardname, "--warnings", "all", "--dry-run", path) + ret = run_and_capture("compile", "--fqbn", boardname, "--warnings", "all", "--dry-run", path.to_s) ret[:success] end - # ensure that the given library is installed, or symlinked as appropriate - # return the path of the prepared library, or nil + # Guess the name of a library + # @param path [Pathname] The path to the library (installed or not) + # @return [String] the probable library name + def name_of_library(path) + src_path = path.realpath + properties_file = src_path + CppLibrary::LIBRARY_PROPERTIES_FILE + return src_path.basename.to_s unless properties_file.exist? + return src_path.basename.to_s if LibraryProperties.new(properties_file).name.nil? + + LibraryProperties.new(properties_file).name + end + + # Create a handle to an Arduino library by name + # @param name [String] The library "real name" + # @return [CppLibrary] The library object + def library_of_name(name) + raise ArgumentError, "name is not a String (got #{name.class})" unless name.is_a? String + + CppLibrary.new(name, self) + end + + # Create a handle to an Arduino library by path + # @param path [Pathname] The path to the library + # @return [CppLibrary] The library object + def library_of_path(path) + # the path must exist... and if it does, brute-force search the installed libs for it + realpath = path.realpath # should produce error if the path doesn't exist to begin with + entry = installed_libraries.find { |l| Pathname.new(l["library"]["install_dir"]).realpath == realpath } + probable_name = entry["real_name"].nil? ? realpath.basename.to_s : entry["real_name"] + CppLibrary.new(probable_name, self) + end + + # install a library from a path on the local machine (not via library manager), by symlink or no-op as appropriate # @param path [Pathname] library to use - # @return [String] the path of the installed library + # @return [CppLibrary] the installed library, or nil def install_local_library(path) - src_path = path.realpath - library_name = src_path.basename - destination_path = library_path(library_name) + src_path = path.realpath + library_name = name_of_library(path) + cpp_library = library_of_name(library_name) + destination_path = cpp_library.path # things get weird if the sketchbook contains the library. # check that first - if destination_path.exist? - uhoh = "There is already a library '#{library_name}' in the library directory" - return destination_path if destination_path == src_path + if cpp_library.installed? + # maybe the project has always lived in the libraries directory, no need to symlink + return cpp_library if destination_path == src_path + uhoh = "There is already a library '#{library_name}' in the library directory (#{destination_path})" # maybe it's a symlink? that would be OK if destination_path.symlink? - return destination_path if destination_path.readlink == src_path + return cpp_library if destination_path.readlink == src_path @last_msg = "#{uhoh} and it's not symlinked to #{src_path}" return nil @@ -211,21 +209,7 @@ def install_local_library(path) # install the library Host.symlink(src_path, destination_path) - destination_path - end - - # @param installed_library_path [String] The library to query - # @return [Array] Example sketch files - def library_examples(installed_library_path) - example_path = Pathname.new(installed_library_path) + "examples" - return [] unless File.exist?(example_path) - - examples = example_path.children.select(&:directory?).map(&:to_path).map(&File.method(:basename)) - files = examples.map do |e| - proj_file = example_path + e + "#{e}.ino" - proj_file.exist? ? proj_file.to_s : nil - end - files.reject(&:nil?).sort_by(&:to_s) + cpp_library end end end diff --git a/lib/arduino_ci/cpp_library.rb b/lib/arduino_ci/cpp_library.rb index 810d32be..48d67e9d 100644 --- a/lib/arduino_ci/cpp_library.rb +++ b/lib/arduino_ci/cpp_library.rb @@ -14,15 +14,21 @@ module ArduinoCI # Information about an Arduino CPP library, specifically for compilation purposes class CppLibrary - # @return [Pathname] The path to the library being tested - attr_reader :base_dir + # @return [String] The official library properties file name + LIBRARY_PROPERTIES_FILE = "library.properties".freeze - # @return [Pathname] The path to the Arduino 3rd-party library directory - attr_reader :arduino_lib_dir + # @return [String] The "official" name of the library, which can include spaces (in a way that the lib dir won't) + attr_reader :name + + # @return [ArduinoBackend] The backend support for this library + attr_reader :backend # @return [Array] The set of artifacts created by this class (note: incomplete!) attr_reader :artifacts + # @return [Array] The set of directories that should be excluded from compilation + attr_reader :exclude_dirs + # @return [String] STDERR from the last command attr_reader :last_err @@ -35,30 +41,100 @@ class CppLibrary # @return [Array] Directories suspected of being vendor-bundle attr_reader :vendor_bundle_cache - # @param base_dir [Pathname] The path to the library being tested - # @param arduino_lib_dir [Pathname] The path to the libraries directory - # @param exclude_dirs [Array] Directories that should be excluded from compilation - def initialize(base_dir, arduino_lib_dir, exclude_dirs) - raise ArgumentError, 'base_dir is not a Pathname' unless base_dir.is_a? Pathname - raise ArgumentError, 'arduino_lib_dir is not a Pathname' unless arduino_lib_dir.is_a? Pathname - raise ArgumentError, 'exclude_dir is not an array of Pathnames' unless exclude_dirs.is_a?(Array) - raise ArgumentError, 'exclude_dir array contains non-Pathname elements' unless exclude_dirs.all? { |p| p.is_a? Pathname } - - @base_dir = base_dir - @exclude_dirs = exclude_dirs - @arduino_lib_dir = arduino_lib_dir.expand_path + # @param friendly_name [String] The "official" name of the library, which can contain spaces + # @param backend [ArduinoBackend] The support backend + def initialize(friendly_name, backend) + raise ArgumentError, "friendly_name is not a String (got #{friendly_name.class})" unless friendly_name.is_a? String + raise ArgumentError, 'backend is not a ArduinoBackend' unless backend.is_a? ArduinoBackend + + @name = friendly_name + @backend = backend + @info_cache = nil @artifacts = [] @last_err = "" @last_out = "" @last_msg = "" @has_libasan_cache = {} @vendor_bundle_cache = nil + @exclude_dirs = [] + end + + # Generate a guess as to the on-disk (coerced character) name of this library + # + # @TODO: delegate this to the backend in some way? It uses "official" names for install, but dir names in lists :( + # @param friendly_name [String] The library name as it might appear in library manager + # @return [String] How the path will be stored on disk -- spaces are coerced to underscores + def self.library_directory_name(friendly_name) + friendly_name.tr(" ", "_") + end + + # Generate a guess as to the on-disk (coerced character) name of this library + # + # @TODO: delegate this to the backend in some way? It uses "official" names for install, but dir names in lists :( + # @return [String] How the path will be stored on disk -- spaces are coerced to underscores + def name_on_disk + self.class.library_directory_name(@name) + end + + # Get the path to this library, whether or not it exists + # @return [Pathname] The fully qualified library path + def path + @backend.lib_dir + name_on_disk + end + + # Determine whether a library is present in the lib dir + # + # Note that `true` doesn't guarantee that the library is valid/installed + # and `false` doesn't guarantee that the library isn't built-in + # + # @return [bool] + def installed? + path.exist? + end + + # install a library by name + # @param version [String] the version to install + # @param recursive [bool] whether to also install its dependencies + # @return [bool] whether the command succeeded + def install(version = nil, recursive = false) + return true if installed? && !recursive + + fqln = version.nil? ? @name : "#{@name}@#{version}" + result = if recursive + @backend.run_and_capture("lib", "install", fqln) + else + @backend.run_and_capture("lib", "install", "--no-deps", fqln) + end + result[:success] + end + + # information about the library as reported by the backend + # @return [Hash] the metadata object + def info + return nil unless installed? + + # note that if the library isn't found, we're going to do a lot of cache attempts... + if @info_cache.nil? + @info_cache = @backend.installed_libraries.find do |l| + lib_info = l["library"] + Pathname.new(lib_info["install_dir"]).realpath == path.realpath + end + end + + @info_cache + end + + # @param installed_library_path [String] The library to query + # @return [Array] Example sketch files + def example_sketches + reported_dirs = info["library"]["examples"].map(&Pathname::method(:new)) + reported_dirs.map { |e| e + e.basename.sub_ext(".ino") }.select(&:exist?).sort_by(&:to_s) end # The expected path to the library.properties file (i.e. even if it does not exist) # @return [Pathname] def library_properties_path - @base_dir + "library.properties" + path + LIBRARY_PROPERTIES_FILE end # Whether library.properties definitions for this library exist @@ -68,16 +144,29 @@ def library_properties? lib_props.exist? && lib_props.file? end + # Library properties + # @return [LibraryProperties] The library.properties metadata wrapper for this library + def library_properties + return nil unless library_properties? + + LibraryProperties.new(library_properties_path) + end + + # Set directories that should be excluded from compilation + # @param rval [Array] Array of strings or pathnames that will be coerced to pathnames + def exclude_dirs=(rval) + @exclude_dirs = rval.map { |d| d.is_a?(Pathname) ? d : Pathname.new(d) } + end + # Decide whether this is a 1.5-compatible library # - # according to https://arduino.github.io/arduino-cli/latest/library-specification - # - # Should match logic from https://github.com/arduino/arduino-cli/blob/master/arduino/libraries/loader.go + # This should be according to https://arduino.github.io/arduino-cli/latest/library-specification + # but we rely on the cli to decide for us # @return [bool] def one_point_five? return false unless library_properties? - src_dir = (@base_dir + "src") + src_dir = path + "src" src_dir.exist? && src_dir.directory? end @@ -88,9 +177,9 @@ def one_point_five? # That gets us the vendor directory (or multiple directories). We can check # if the given path is contained by any of those. # - # @param path [Pathname] The path to check + # @param some_path [Pathname] The path to check # @return [bool] - def vendor_bundle?(path) + def vendor_bundle?(some_path) # Cache bundle information, as it is (1) time consuming to fetch and (2) not going to change while we run if @vendor_bundle_cache.nil? bundle_info = Host.run_and_capture("bundle show --paths") @@ -125,7 +214,7 @@ def vendor_bundle?(path) # With vendor bundles located, check this file against those @vendor_bundle_cache.any? do |gem_path| - path.ascend do |part| + some_path.ascend do |part| break true if gem_path == part end end @@ -135,13 +224,13 @@ def vendor_bundle?(path) # # @param path [Pathname] The path to check # @return [bool] - def in_tests_dir?(path) + def in_tests_dir?(sourcefile_path) return false unless tests_dir.exist? tests_dir_aliases = [tests_dir, tests_dir.realpath] # we could do this but some rubies don't return an enumerator for ascend # path.ascend.any? { |part| tests_dir_aliases.include?(part) } - path.ascend do |part| + sourcefile_path.ascend do |part| return true if tests_dir_aliases.include?(part) end false @@ -151,11 +240,11 @@ def in_tests_dir?(path) # # @param path [Pathname] The path to check # @return [bool] - def in_exclude_dir?(path) + def in_exclude_dir?(sourcefile_path) # we could do this but some rubies don't return an enumerator for ascend # path.ascend.any? { |part| tests_dir_aliases.include?(part) } - path.ascend do |part| - return true if exclude_dir.any? { |p| p.realpath == part } + sourcefile_path.ascend do |part| + return true if exclude_dir.any? { |p| p.realpath == part.realpath } end false end @@ -178,30 +267,17 @@ def libasan?(gcc_binary) @has_libasan_cache[gcc_binary] end - # Library properties - def library_properties - return nil unless library_properties? - - LibraryProperties.new(library_properties_path) - end - - # Get a list of all dependencies as defined in library.properties - # @return [Array] The library names of the dependencies (not the paths) - def arduino_library_dependencies - return nil unless library_properties? - - library_properties.depends - end - # Get a list of all CPP source files in a directory and its subdirectories # @param some_dir [Pathname] The directory in which to begin the search # @param extensions [Array] The set of allowable file extensions # @return [Array] The paths of the found files def code_files_in(some_dir, extensions) raise ArgumentError, 'some_dir is not a Pathname' unless some_dir.is_a? Pathname - return [] unless some_dir.exist? && some_dir.directory? - files = some_dir.realpath.children.reject(&:directory?) + full_dir = path + some_dir + return [] unless full_dir.exist? && full_dir.directory? + + files = full_dir.children.reject(&:directory?) cpp = files.select { |path| extensions.include?(path.extname.downcase) } not_hidden = cpp.reject { |path| path.basename.to_s.start_with?(".") } not_hidden.sort_by(&:to_s) @@ -215,17 +291,18 @@ def code_files_in_recursive(some_dir, extensions) raise ArgumentError, 'some_dir is not a Pathname' unless some_dir.is_a? Pathname return [] unless some_dir.exist? && some_dir.directory? - real = some_dir.realpath - Find.find(real).map { |p| Pathname.new(p) }.select(&:directory?).map { |d| code_files_in(d, extensions) }.flatten + Find.find(some_dir).map { |p| Pathname.new(p) }.select(&:directory?).map { |d| code_files_in(d, extensions) }.flatten end - # Header files that are part of the project library under test + # Source files that are part of the library under test + # @param extensions [Array] the allowed extensions (or, the ones we're looking for) # @return [Array] - def header_files + def source_files(extensions) + source_dir = Pathname.new(info["library"]["source_dir"]) ret = if one_point_five? - code_files_in_recursive(@base_dir + "src", HPP_EXTENSIONS) + code_files_in_recursive(source_dir, extensions) else - [@base_dir, @base_dir + "utility"].map { |d| code_files_in(d, HPP_EXTENSIONS) }.flatten + [source_dir, source_dir + "utility"].map { |d| code_files_in(d, extensions) }.flatten end # note to future troubleshooter: some of these tests may not be relevant, but at the moment at @@ -233,18 +310,16 @@ def header_files ret.reject { |p| vendor_bundle?(p) || in_tests_dir?(p) || in_exclude_dir?(p) } end + # Header files that are part of the project library under test + # @return [Array] + def header_files + source_files(HPP_EXTENSIONS) + end + # CPP files that are part of the project library under test # @return [Array] def cpp_files - ret = if one_point_five? - code_files_in_recursive(@base_dir + "src", CPP_EXTENSIONS) - else - [@base_dir, @base_dir + "utility"].map { |d| code_files_in(d, CPP_EXTENSIONS) }.flatten - end - - # note to future troubleshooter: some of these tests may not be relevant, but at the moment at - # least some of them are tied to existing features - ret.reject { |p| vendor_bundle?(p) || in_tests_dir?(p) || in_exclude_dir?(p) } + source_files(CPP_EXTENSIONS) end # CPP files that are part of the arduino mock library we're providing @@ -269,13 +344,13 @@ def cpp_files_libraries(aux_libraries) # Returns the Pathnames for all paths to exclude from testing and compilation # @return [Array] def exclude_dir - @exclude_dirs.map { |p| Pathname.new(@base_dir) + p }.select(&:exist?) + @exclude_dirs.map { |p| Pathname.new(path) + p }.select(&:exist?) end # The directory where we expect to find unit test defintions provided by the user # @return [Pathname] def tests_dir - Pathname.new(@base_dir) + "test" + Pathname.new(path) + "test" end # The files provided by the user that contain unit tests @@ -311,18 +386,33 @@ def gcc_version(gcc_binary) @last_err end - # Arduino library directories containing sources -- only those of the dependencies - # @return [Array] - def arduino_library_src_dirs(aux_libraries) + # Get a list of all dependencies as defined in library.properties + # @return [Array] The library names of the dependencies (not the paths) + def arduino_library_dependencies + return [] unless library_properties? + return [] if library_properties.depends.nil? + + library_properties.depends + end + + # Arduino library dependencies all the way down, installing if they are not present + # @return [Array] The library names of the dependencies (not the paths) + def all_arduino_library_dependencies!(additional_libraries = []) # Pull in all possible places that headers could live, according to the spec: # https://github.com/arduino/Arduino/wiki/Arduino-IDE-1.5:-Library-specification + recursive = (additional_libraries + arduino_library_dependencies).map do |n| + other_lib = self.class.new(n, @backend) + other_lib.install unless other_lib.installed? + other_lib.all_arduino_library_dependencies! + end.flatten + ret = (additional_libraries + recursive).uniq + ret + end - aux_libraries.map do |d| - # library manager coerces spaces in package names to underscores - # see https://github.com/ianfixes/arduino_ci/issues/132#issuecomment-518857059 - legal_dir = d.tr(" ", "_") - self.class.new(@arduino_lib_dir + legal_dir, @arduino_lib_dir, @exclude_dirs).header_dirs - end.flatten.uniq + # Arduino library directories containing sources -- only those of the dependencies + # @return [Array] + def arduino_library_src_dirs(aux_libraries) + all_arduino_library_dependencies!(aux_libraries).map { |l| self.class.new(l, @backend).header_dirs }.flatten.uniq end # GCC command line arguments for including aux libraries @@ -415,9 +505,9 @@ def build_for_test_with_configuration(test_file, aux_libraries, gcc_binary, ci_g # combine library.properties defs (if existing) with config file. # TODO: as much as I'd like to rely only on the properties file(s), I think that would prevent testing 1.0-spec libs - full_aux_libraries = arduino_library_dependencies.nil? ? aux_libraries : aux_libraries + arduino_library_dependencies - arg_sets << test_args(full_aux_libraries, ci_gcc_config) - arg_sets << cpp_files_libraries(full_aux_libraries).map(&:to_s) + full_dependencies = all_arduino_library_dependencies!(aux_libraries) + arg_sets << test_args(full_dependencies, ci_gcc_config) + arg_sets << cpp_files_libraries(full_dependencies).map(&:to_s) arg_sets << [test_file.to_s] args = arg_sets.flatten(1) return nil unless run_gcc(gcc_binary, *args) diff --git a/lib/arduino_ci/library_properties.rb b/lib/arduino_ci/library_properties.rb index 1a080713..97d1da8e 100644 --- a/lib/arduino_ci/library_properties.rb +++ b/lib/arduino_ci/library_properties.rb @@ -17,6 +17,11 @@ def initialize(path) end end + # @return [Hash] the properties as a hash, all strings + def to_h + @fields.clone + end + # Enable a shortcut syntax for library property accessors, in the style of `attr_accessor` metaprogramming. # This is used to create a named field pointing to a specific property in the file, optionally applying # a specific formatting function. diff --git a/spec/arduino_backend_spec.rb b/spec/arduino_backend_spec.rb index d17098c1..afb2fa32 100644 --- a/spec/arduino_backend_spec.rb +++ b/spec/arduino_backend_spec.rb @@ -9,33 +9,33 @@ def get_sketch(dir, file) RSpec.describe ArduinoCI::ArduinoBackend do next if skip_ruby_tests - arduino_backend = ArduinoCI::ArduinoInstallation.autolocate! + backend = ArduinoCI::ArduinoInstallation.autolocate! after(:each) do |example| if example.exception - puts "Last message: #{arduino_backend.last_msg}" + puts "Last message: #{backend.last_msg}" puts "========== Stdout:" - puts arduino_backend.last_out + puts backend.last_out puts "========== Stderr:" - puts arduino_backend.last_err + puts backend.last_err end end context "initialize" do it "sets base vars" do - expect(arduino_backend.binary_path).not_to be nil + expect(backend.binary_path).not_to be nil end end context "board_installed?" do it "Finds installed boards" do - uno_installed = arduino_backend.board_installed? "arduino:avr:uno" + uno_installed = backend.board_installed? "arduino:avr:uno" expect(uno_installed).to be true expect(uno_installed).not_to be nil end it "Doesn't find bogus boards" do - bogus_installed = arduino_backend.board_installed? "eggs:milk:wheat" + bogus_installed = backend.board_installed? "eggs:milk:wheat" expect(bogus_installed).to be false expect(bogus_installed).not_to be nil end @@ -43,30 +43,31 @@ def get_sketch(dir, file) context "installation of boards" do it "installs and sets boards" do - expect(arduino_backend.install_boards("arduino:sam")).to be true + expect(backend.install_boards("arduino:sam")).to be true end end context "libraries" do it "knows where to find libraries" do - fake_lib = "_____nope" - expected_dir = Pathname.new(arduino_backend.lib_dir) + fake_lib - expect(arduino_backend.library_path(fake_lib)).to eq(expected_dir) - expect(arduino_backend.library_present?(fake_lib)).to be false + fake_lib_name = "_____nope" + expected_dir = Pathname.new(backend.lib_dir) + fake_lib_name + fake_lib = backend.library_of_name(fake_lib_name) + expect(fake_lib.path).to eq(expected_dir) + expect(fake_lib.installed?).to be false end end context "board_manager" do it "Reads and writes board_manager URLs" do fake_urls = ["http://foo.bar", "http://arduino.ci"] - existing_urls = arduino_backend.board_manager_urls + existing_urls = backend.board_manager_urls # try to ensure maxiumum variability in the test test_url_sets = (existing_urls.empty? ? [fake_urls, []] : [[], fake_urls]) + [existing_urls] test_url_sets.each do |urls| - arduino_backend.board_manager_urls = urls - expect(arduino_backend.board_manager_urls).to match_array(urls) + backend.board_manager_urls = urls + expect(backend.board_manager_urls).to match_array(urls) end end end @@ -80,19 +81,19 @@ def get_sketch(dir, file) sketch_path_bad = get_sketch("BadSketch", "BadSketch.ino") it "Rejects a PDE sketch at #{sketch_path_pde}" do - expect(arduino_backend.compile_sketch(sketch_path_pde, "arduino:avr:uno")).to be false + expect(backend.compile_sketch(sketch_path_pde, "arduino:avr:uno")).to be false end it "Fails a missing sketch at #{sketch_path_mia}" do - expect(arduino_backend.compile_sketch(sketch_path_mia, "arduino:avr:uno")).to be false + expect(backend.compile_sketch(sketch_path_mia, "arduino:avr:uno")).to be false end it "Fails a bad sketch at #{sketch_path_bad}" do - expect(arduino_backend.compile_sketch(sketch_path_bad, "arduino:avr:uno")).to be false + expect(backend.compile_sketch(sketch_path_bad, "arduino:avr:uno")).to be false end it "Passes a simple INO sketch at #{sketch_path_ino}" do - expect(arduino_backend.compile_sketch(sketch_path_ino, "arduino:avr:uno")).to be true + expect(backend.compile_sketch(sketch_path_ino, "arduino:avr:uno")).to be true end end end diff --git a/spec/arduino_installation_spec.rb b/spec/arduino_installation_spec.rb index b8531a71..f11bdaab 100644 --- a/spec/arduino_installation_spec.rb +++ b/spec/arduino_installation_spec.rb @@ -10,10 +10,10 @@ end context "autolocate!" do - arduino_backend = ArduinoCI::ArduinoInstallation.autolocate! + backend = ArduinoCI::ArduinoInstallation.autolocate! it "doesn't fail" do - expect(arduino_backend.binary_path).not_to be nil - expect(arduino_backend.lib_dir).not_to be nil + expect(backend.binary_path).not_to be nil + expect(backend.lib_dir).not_to be nil end end @@ -23,7 +23,7 @@ output.rewind expect(output.read.empty?).to be true # install a bogus version to save time downloading - arduino_backend = ArduinoCI::ArduinoInstallation.force_install(output, "BOGUS VERSION") + backend = ArduinoCI::ArduinoInstallation.force_install(output, "BOGUS VERSION") output.rewind expect(output.read.empty?).to be false end diff --git a/spec/ci_config_spec.rb b/spec/ci_config_spec.rb index 23d01e40..57a80f50 100644 --- a/spec/ci_config_spec.rb +++ b/spec/ci_config_spec.rb @@ -1,6 +1,8 @@ require "spec_helper" require "pathname" +require "fake_lib_dir" + RSpec.describe ArduinoCI::CIConfig do next if skip_ruby_tests context "default" do @@ -156,11 +158,20 @@ end context "allowable_unittest_files" do + + # we will need to install some dummy libraries into a fake location, so do that on demand + fld = FakeLibDir.new + backend = fld.backend cpp_lib_path = Pathname.new(__dir__) + "fake_library" - cpp_library = ArduinoCI::CppLibrary.new(cpp_lib_path, Pathname.new("my_fake_arduino_lib_dir"), []) + + around(:example) { |example| fld.in_pristine_fake_libraries_dir(example) } + before(:each) { @cpp_library = backend.install_local_library(cpp_lib_path) } it "starts with a known set of files" do - expect(cpp_library.test_files.map { |f| File.basename(f) }).to match_array([ + expect(cpp_lib_path.exist?).to be(true) + expect(@cpp_library).to_not be(nil) + expect(@cpp_library.path.exist?).to be(true) + expect(@cpp_library.test_files.map { |f| File.basename(f) }).to match_array([ "sam-squamsh.cpp", "yes-good.cpp", "mars.cpp" @@ -170,7 +181,7 @@ it "filters that set of files" do override_file = File.join(File.dirname(__FILE__), "yaml", "o1.yaml") combined_config = ArduinoCI::CIConfig.default.with_override(override_file) - expect(combined_config.allowable_unittest_files(cpp_library.test_files).map { |f| File.basename(f) }).to match_array([ + expect(combined_config.allowable_unittest_files(@cpp_library.test_files).map { |f| File.basename(f) }).to match_array([ "yes-good.cpp", ]) end diff --git a/spec/cpp_library_spec.rb b/spec/cpp_library_spec.rb index 6a25468f..f3422dbb 100644 --- a/spec/cpp_library_spec.rb +++ b/spec/cpp_library_spec.rb @@ -1,59 +1,64 @@ require "spec_helper" require "pathname" +require 'tmpdir' -sampleproj_path = Pathname.new(__dir__).parent + "SampleProjects" - -def get_relative_dir(sampleprojects_tests_dir) - base_dir = sampleprojects_tests_dir.ascend do |path| - break path if path.split[1].to_s == "SampleProjects" - end - sampleprojects_tests_dir.relative_path_from(base_dir) -end +require 'fake_lib_dir' +sampleproj_path = Pathname.new(__dir__).parent + "SampleProjects" RSpec.describe "ExcludeSomething C++" do next if skip_cpp_tests - cpp_lib_path = sampleproj_path + "ExcludeSomething" + # we will need to install some dummy libraries into a fake location, so do that on demand + fld = FakeLibDir.new + backend = fld.backend + test_lib_name = "ExcludeSomething" + cpp_lib_path = sampleproj_path + test_lib_name + around(:example) { |example| fld.in_pristine_fake_libraries_dir(example) } + before(:each) do + @base_dir = fld.libraries_dir + @cpp_library = backend.install_local_library(cpp_lib_path) + end + context "without excludes" do - cpp_library = ArduinoCI::CppLibrary.new(cpp_lib_path, - Pathname.new("my_fake_arduino_lib_dir"), - []) context "cpp_files" do it "finds cpp files in directory" do + expect(@cpp_library).to_not be(nil) excludesomething_cpp_files = [ Pathname.new("ExcludeSomething/src/exclude-something.cpp"), Pathname.new("ExcludeSomething/src/excludeThis/exclude-this.cpp") ] - relative_paths = cpp_library.cpp_files.map { |f| get_relative_dir(f) } + relative_paths = @cpp_library.cpp_files.map { |f| f.relative_path_from(@base_dir) } expect(relative_paths).to match_array(excludesomething_cpp_files) end end context "unit tests" do it "can't build due to files that should have been excluded" do - config = ArduinoCI::CIConfig.default.from_example(cpp_lib_path) - path = config.allowable_unittest_files(cpp_library.test_files).first - compiler = config.compilers_to_use.first - result = cpp_library.build_for_test_with_configuration(path, - [], - compiler, - config.gcc_config("uno")) + @cpp_library = backend.install_local_library(cpp_lib_path) + config = ArduinoCI::CIConfig.default.from_example(cpp_lib_path) + path = config.allowable_unittest_files(@cpp_library.test_files).first + compiler = config.compilers_to_use.first + result = @cpp_library.build_for_test_with_configuration(path, + [], + compiler, + config.gcc_config("uno")) expect(result).to be nil end end end context "with excludes" do - cpp_library = ArduinoCI::CppLibrary.new(cpp_lib_path, - Pathname.new("my_fake_arduino_lib_dir"), - ["src/excludeThis"].map(&Pathname.method(:new))) + context "cpp_files" do it "finds cpp files in directory" do + @cpp_library = backend.install_local_library(cpp_lib_path) + @cpp_library.exclude_dirs = ["src/excludeThis"].map(&Pathname.method(:new)) + excludesomething_cpp_files = [ Pathname.new("ExcludeSomething/src/exclude-something.cpp") ] - relative_paths = cpp_library.cpp_files.map { |f| get_relative_dir(f) } + relative_paths = @cpp_library.cpp_files.map { |f| f.relative_path_from(@base_dir) } expect(relative_paths).to match_array(excludesomething_cpp_files) end end @@ -68,6 +73,7 @@ def get_relative_dir(sampleprojects_tests_dir) answers = { DoSomething: { one_five: false, + library_properties: true, cpp_files: [Pathname.new("DoSomething") + "do-something.cpp"], cpp_files_libraries: [], header_dirs: [Pathname.new("DoSomething")], @@ -80,6 +86,7 @@ def get_relative_dir(sampleprojects_tests_dir) }, OnePointOhDummy: { one_five: false, + library_properties: false, cpp_files: [ "OnePointOhDummy/YesBase.cpp", "OnePointOhDummy/utility/YesUtil.cpp", @@ -96,6 +103,7 @@ def get_relative_dir(sampleprojects_tests_dir) }, OnePointFiveMalformed: { one_five: false, + library_properties: false, cpp_files: [ "OnePointFiveMalformed/YesBase.cpp", "OnePointFiveMalformed/utility/YesUtil.cpp", @@ -110,6 +118,7 @@ def get_relative_dir(sampleprojects_tests_dir) }, OnePointFiveDummy: { one_five: true, + library_properties: true, cpp_files: [ "OnePointFiveDummy/src/YesSrc.cpp", "OnePointFiveDummy/src/subdir/YesSubdir.cpp", @@ -129,6 +138,7 @@ def get_relative_dir(sampleprojects_tests_dir) # easier to construct this one from the other test cases answers[:DependOnSomething] = { one_five: true, + library_properties: true, cpp_files: ["DependOnSomething/src/YesDeps.cpp"].map { |f| Pathname.new(f) }, cpp_files_libraries: answers[:OnePointOhDummy][:cpp_files] + answers[:OnePointFiveDummy][:cpp_files], header_dirs: ["DependOnSomething/src"].map { |f| Pathname.new(f) }, # this is not recursive! @@ -141,32 +151,52 @@ def get_relative_dir(sampleprojects_tests_dir) answers.freeze answers.each do |sampleproject, expected| + + # we will need to install some dummy libraries into a fake location, so do that on demand + fld = FakeLibDir.new + backend = fld.backend + context "#{sampleproject}" do cpp_lib_path = sampleproj_path + sampleproject.to_s - cpp_library = ArduinoCI::CppLibrary.new(cpp_lib_path, sampleproj_path, []) - dependencies = cpp_library.arduino_library_dependencies.nil? ? [] : cpp_library.arduino_library_dependencies + around(:example) { |example| fld.in_pristine_fake_libraries_dir(example) } + before(:each) do + @base_dir = fld.libraries_dir + @cpp_library = backend.install_local_library(cpp_lib_path) + end + + it "is a sane test env" do + expect(sampleproject.to_s).to eq(@cpp_library.name) + end it "detects 1.5 format" do - expect(cpp_library.one_point_five?).to eq(expected[:one_five]) + expect(@cpp_library.one_point_five?).to eq(expected[:one_five]) end + it "detects library.properties" do + expect(@cpp_library.library_properties?).to eq(expected[:library_properties]) + end + + context "cpp_files" do it "finds cpp files in directory" do - relative_paths = cpp_library.cpp_files.map { |f| get_relative_dir(f) } + relative_paths = @cpp_library.cpp_files.map { |f| f.relative_path_from(@base_dir) } expect(relative_paths.map(&:to_s)).to match_array(expected[:cpp_files].map(&:to_s)) end end context "cpp_files_libraries" do it "finds cpp files in directories of dependencies" do - relative_paths = cpp_library.cpp_files_libraries(dependencies).map { |f| get_relative_dir(f) } + @cpp_library.all_arduino_library_dependencies! # side effect: installs them + dependencies = @cpp_library.arduino_library_dependencies.nil? ? [] : @cpp_library.arduino_library_dependencies + dependencies.each { |d| backend.install_local_library(sampleproj_path + d) } + relative_paths = @cpp_library.cpp_files_libraries(dependencies).map { |f| f.relative_path_from(@base_dir) } expect(relative_paths.map(&:to_s)).to match_array(expected[:cpp_files_libraries].map(&:to_s)) end end context "header_dirs" do it "finds directories containing h files" do - relative_paths = cpp_library.header_dirs.map { |f| get_relative_dir(f) } + relative_paths = @cpp_library.header_dirs.map { |f| f.relative_path_from(@base_dir) } expect(relative_paths.map(&:to_s)).to match_array(expected[:header_dirs].map(&:to_s)) end end @@ -176,14 +206,14 @@ def get_relative_dir(sampleprojects_tests_dir) # since we don't know where the CI system will install this stuff, # we need to go looking for a relative path to the SampleProjects directory # just to get our "expected" value - relative_path = get_relative_dir(cpp_library.tests_dir) + relative_path = @cpp_library.tests_dir.relative_path_from(@base_dir) expect(relative_path.to_s).to eq("#{sampleproject}/test") end end context "test_files" do it "finds cpp files in directory" do - relative_paths = cpp_library.test_files.map { |f| get_relative_dir(f) } + relative_paths = @cpp_library.test_files.map { |f| f.relative_path_from(@base_dir) } expect(relative_paths.map(&:to_s)).to match_array(expected[:test_files].map(&:to_s)) end end @@ -191,7 +221,9 @@ def get_relative_dir(sampleprojects_tests_dir) context "arduino_library_src_dirs" do it "finds src dirs from dependent libraries" do # we explicitly feed in the internal dependencies - relative_paths = cpp_library.arduino_library_src_dirs(dependencies).map { |f| get_relative_dir(f) } + dependencies = @cpp_library.arduino_library_dependencies.nil? ? [] : @cpp_library.arduino_library_dependencies + dependencies.each { |d| backend.install_local_library(sampleproj_path + d) } + relative_paths = @cpp_library.arduino_library_src_dirs(dependencies).map { |f| f.relative_path_from(@base_dir) } expect(relative_paths.map(&:to_s)).to match_array(expected[:arduino_library_src_dirs].map(&:to_s)) end end @@ -199,10 +231,16 @@ def get_relative_dir(sampleprojects_tests_dir) end context "test" do + + # we will need to install some dummy libraries into a fake location, so do that on demand + fld = FakeLibDir.new + backend = fld.backend cpp_lib_path = sampleproj_path + "DoSomething" - cpp_library = ArduinoCI::CppLibrary.new(cpp_lib_path, Pathname.new("my_fake_arduino_lib_dir"), []) config = ArduinoCI::CIConfig.default + around(:example) { |example| fld.in_pristine_fake_libraries_dir(example) } + before(:each) { @cpp_library = backend.install_local_library(cpp_lib_path) } + after(:each) do |example| if example.exception puts "Last command: #{cpp_library.last_cmd}" @@ -214,18 +252,18 @@ def get_relative_dir(sampleprojects_tests_dir) end it "is going to test more than one library" do - test_files = cpp_library.test_files + test_files = @cpp_library.test_files expect(test_files.empty?).to be false end - test_files = cpp_library.test_files + test_files = Pathname.glob(Pathname.new(cpp_lib_path) + "test" + "*.cpp") test_files.each do |path| expected = path.basename.to_s.include?("good") config.compilers_to_use.each do |compiler| it "tests #{File.basename(path)} with #{compiler} expecting #{expected}" do - exe = cpp_library.build_for_test_with_configuration(path, [], compiler, config.gcc_config("uno")) + exe = @cpp_library.build_for_test_with_configuration(path, [], compiler, config.gcc_config("uno")) expect(exe).not_to be nil - expect(cpp_library.run_test_file(exe)).to eq(expected) + expect(@cpp_library.run_test_file(exe)).to eq(expected) end end end diff --git a/spec/fake_lib_dir.rb b/spec/fake_lib_dir.rb new file mode 100644 index 00000000..fb9cabc5 --- /dev/null +++ b/spec/fake_lib_dir.rb @@ -0,0 +1,44 @@ +require "arduino_ci" + +class FakeLibDir + + attr_reader :config_dir + attr_reader :config_file + attr_reader :backend + attr_reader :arduino_dir + attr_reader :libraries_dir + + def initialize + # we will need to install some dummy libraries into a fake location, so do that on demand + @config_dir = Pathname.new(Dir.pwd).realpath + @config_file = @config_dir + ArduinoCI::ArduinoBackend::CONFIG_FILE_NAME + @backend = ArduinoCI::ArduinoInstallation.autolocate! + @backend.config_dir = @config_dir + end + + # designed to be called by rspec's "around" function + def in_pristine_fake_libraries_dir(example) + Dir.mktmpdir do |d| + # write a yaml file containing the current directory + dummy_config = { "directories" => { "user" => d.to_s } } + @arduino_dir = Pathname.new(d) + @libraries_dir = @arduino_dir + "libraries" + Dir.mkdir(@libraries_dir) + + f = File.open(@config_file, "w") + begin + f.write dummy_config.to_yaml + f.close + example.run + ensure + begin + File.unlink(@config_file) + rescue Errno::ENOENT + # cool, already done + end + end + end + end + + +end diff --git a/spec/library_properties_spec.rb b/spec/library_properties_spec.rb index 3c6de1ee..d9b65780 100644 --- a/spec/library_properties_spec.rb +++ b/spec/library_properties_spec.rb @@ -36,6 +36,13 @@ end end + it "reads full_paragraph" do + expect(library_properties.full_paragraph).to eq ([ + expected[:string][:sentence], + expected[:string][:paragraph] + ].join(" ")) + end + it "doesn't crash on nonexistent fields" do expect(library_properties.dot_a_linkage).to be(nil) end diff --git a/spec/testsomething_unittests_spec.rb b/spec/testsomething_unittests_spec.rb index 4cb49541..c5cb4a94 100644 --- a/spec/testsomething_unittests_spec.rb +++ b/spec/testsomething_unittests_spec.rb @@ -1,40 +1,48 @@ require "spec_helper" require "pathname" -sampleproj_path = Pathname.new(__dir__).parent + "SampleProjects" - -def get_relative_dir(sampleprojects_tests_dir) - base_dir = sampleprojects_tests_dir.ascend do |path| - break path if path.split[1].to_s == "SampleProjects" - end - sampleprojects_tests_dir.relative_path_from(base_dir) -end +require 'fake_lib_dir' +sampleproj_path = Pathname.new(__dir__).parent + "SampleProjects" RSpec.describe "TestSomething C++" do next if skip_cpp_tests - cpp_lib_path = sampleproj_path + "TestSomething" - cpp_library = ArduinoCI::CppLibrary.new(cpp_lib_path, - Pathname.new("my_fake_arduino_lib_dir"), - ["src/excludeThis"].map(&Pathname.method(:new))) + + # we will need to install some dummy libraries into a fake location, so do that on demand + fld = FakeLibDir.new + backend = fld.backend + test_lib_name = "TestSomething" + cpp_lib_path = sampleproj_path + test_lib_name + context "cpp_files" do + around(:example) { |example| fld.in_pristine_fake_libraries_dir(example) } + before(:each) do + @base_dir = fld.libraries_dir + @cpp_library = backend.install_local_library(cpp_lib_path) + end + it "finds cpp files in directory" do testsomething_cpp_files = [Pathname.new("TestSomething/src/test-something.cpp")] - relative_paths = cpp_library.cpp_files.map { |f| get_relative_dir(f) } + relative_paths = @cpp_library.cpp_files.map { |f| f.relative_path_from(@base_dir) } expect(relative_paths).to match_array(testsomething_cpp_files) end end config = ArduinoCI::CIConfig.default.from_example(cpp_lib_path) context "unit tests" do + around(:example) { |example| fld.in_pristine_fake_libraries_dir(example) } + before(:each) do + @base_dir = fld.libraries_dir + @cpp_library = backend.install_local_library(cpp_lib_path) + end it "is going to test more than one library" do - test_files = cpp_library.test_files + test_files = @cpp_library.test_files expect(test_files.empty?).to be false end it "has some allowable test files" do - allowed_files = config.allowable_unittest_files(cpp_library.test_files) + allowed_files = config.allowable_unittest_files(@cpp_library.test_files) expect(allowed_files.empty?).to be false end @@ -46,11 +54,12 @@ def get_relative_dir(sampleprojects_tests_dir) expect(config.platforms_to_unittest.length.zero?).to be(false) end + cpp_library = backend.install_local_library(cpp_lib_path) test_files = config.allowable_unittest_files(cpp_library.test_files) # filter the list based on a glob, if provided unless ENV["ARDUINO_CI_SELECT_CPP_TESTS"].nil? - Dir.chdir(cpp_library.tests_dir) do + Dir.chdir(@cpp_library.tests_dir) do globbed = Pathname.glob(ENV["ARDUINO_CI_SELECT_CPP_TESTS"]) test_files.select! { |p| globbed.include?(p.basename) } end @@ -61,9 +70,11 @@ def get_relative_dir(sampleprojects_tests_dir) config.compilers_to_use.each do |compiler| context "file #{tfn} (using #{compiler})" do + around(:example) { |example| fld.in_pristine_fake_libraries_dir(example) } before(:all) do - @exe = cpp_library.build_for_test_with_configuration(path, [], compiler, config.gcc_config("uno")) + @cpp_library = backend.install_local_library(cpp_lib_path) + @exe = @cpp_library.build_for_test_with_configuration(path, [], compiler, config.gcc_config("uno")) end # extra debug for c++ failures @@ -71,9 +82,9 @@ def get_relative_dir(sampleprojects_tests_dir) if example.exception puts "Last command: #{cpp_library.last_cmd}" puts "========== Stdout:" - puts cpp_library.last_out + puts @cpp_library.last_out puts "========== Stderr:" - puts cpp_library.last_err + puts @cpp_library.last_err end end @@ -82,7 +93,7 @@ def get_relative_dir(sampleprojects_tests_dir) end it "#{tfn} passes tests" do skip "Can't run the test program because it failed to build" if @exe.nil? - expect(cpp_library.run_test_file(@exe)).to_not be_falsey + expect(@cpp_library.run_test_file(@exe)).to_not be_falsey end end end