diff --git a/.rubocop.yml b/.rubocop.yml index ff2099a1..07841202 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -31,6 +31,12 @@ Layout/ExtraSpacing: Layout/EndOfLine: EnforcedStyle: lf +Layout/EndAlignment: + EnforcedStyleAlignWith: start_of_line + +Layout/CaseIndentation: + EnforcedStyle: end + Metrics/LineLength: Description: Limit lines to 80 characters. StyleGuide: https://github.com/bbatsov/ruby-style-guide#80-character-limits diff --git a/CHANGELOG.md b/CHANGELOG.md index 035d64eb..6f637a74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,19 +11,29 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Add stubs for `Client.h`, `IPAddress.h`, `Printable.h`, `Server.h`, and `Udp.h` - Add support for `digitalPinToPort()`, `digitalPinToBitMask()`, and `portOutputRegister()` - Support for mock EEPROM (but only if board supports it) +- `arduino_ci_remote.rb` CLI switch `--skip-examples-compilation` +- `CppLibrary.header_files` to find header files +- `LibraryProperties` to read metadata from Arduino libraries +- `CppLibrary.library_properties_path`, `CppLibrary.library_properties?`, `CppLibrary.library_properties` to expose library properties of a Cpp library +- `CppLibrary.arduino_library_dependencies` to list the dependent libraries specified by the library.properties file +- `CppLibrary.print_stack_dump` prints stack trace dumps (on Windows specifically) to the console if encountered ### Changed - Move repository from https://github.com/ianfixes/arduino_ci to https://github.com/Arduino-CI/arduino_ci +- `CppLibrary` functions returning C++ header or code files now respect the 1.0/1.5 library specification - Revise math macros to avoid name clashes +### Fixed +- Don't define `ostream& operator<<(nullptr_t)` if already defined by Apple +- `CppLibrary.in_tests_dir?` no longer produces an error if there is no tests directory +- The definition of the `_SFR_IO8` macro no longer produces errors about rvalues + ### Deprecated -- Deprecated `arduino_ci_remote.rb` in favor of `arduino_ci.rb` +- `arduino_ci_remote.rb` in favor of `arduino_ci.rb` +- `arduino_ci_remote.rb` CLI switch `--skip-compilation` ### Removed -### Fixed -- Don't define `ostream& operator<<(nullptr_t)` if already defined by Apple - ### Security diff --git a/REFERENCE.md b/REFERENCE.md index c231bba4..d54b5828 100644 --- a/REFERENCE.md +++ b/REFERENCE.md @@ -19,9 +19,14 @@ When testing locally, it's often advantageous to limit the number of tests that This completely skips the unit testing portion of the CI script. -### `--skip-compilation` option +### `--skip-compilation` option (deprecated) -This completely skips the compilation tests (of library examples) portion of the CI script. +This completely skips the compilation tests (of library examples) portion of the CI script. It does not skip the compilation of unit tests. + + +### `--skip-examples-compilation` option + +This completely skips the compilation tests (of library examples) portion of the CI script. It does not skip the compilation of unit tests. ### `--testfile-select` option @@ -90,8 +95,8 @@ platforms: ### Control How Examples Are Compiled -Put a file `.arduino-ci.yml` in each example directory where you require a different configuration than default. -The `compile:` section controls the platforms on which the compilation will be attempted, as well as any external libraries that must be installed and included. +Put a file `.arduino-ci.yml` in each example directory where you require a different configuration than default. +The `compile:` section controls the platforms on which the compilation will be attempted, as well as any external libraries that must be installed and included. ```yaml compile: diff --git a/SampleProjects/DependOnSomething/library.properties b/SampleProjects/DependOnSomething/library.properties new file mode 100644 index 00000000..ea93aeb0 --- /dev/null +++ b/SampleProjects/DependOnSomething/library.properties @@ -0,0 +1 @@ +depends=OnePointOhDummy,OnePointFiveDummy diff --git a/SampleProjects/DependOnSomething/src/YesDeps.cpp b/SampleProjects/DependOnSomething/src/YesDeps.cpp new file mode 100644 index 00000000..e69de29b diff --git a/SampleProjects/DependOnSomething/src/YesDeps.h b/SampleProjects/DependOnSomething/src/YesDeps.h new file mode 100644 index 00000000..e69de29b diff --git a/SampleProjects/DependOnSomething/test/null.cpp b/SampleProjects/DependOnSomething/test/null.cpp new file mode 100644 index 00000000..d58eca29 --- /dev/null +++ b/SampleProjects/DependOnSomething/test/null.cpp @@ -0,0 +1,7 @@ +#include + +unittest(nothing) +{ +} + +unittest_main() diff --git a/SampleProjects/ExcludeSomething/.arduino-ci.yml b/SampleProjects/ExcludeSomething/.arduino-ci.yml new file mode 100644 index 00000000..f35995f0 --- /dev/null +++ b/SampleProjects/ExcludeSomething/.arduino-ci.yml @@ -0,0 +1,3 @@ +unittest: + exclude_dirs: + - src/excludeThis diff --git a/SampleProjects/ExcludeSomething/README.md b/SampleProjects/ExcludeSomething/README.md new file mode 100644 index 00000000..5bcc6553 --- /dev/null +++ b/SampleProjects/ExcludeSomething/README.md @@ -0,0 +1,3 @@ +# ExcludeSomething + +This example exists to test directory-exclusion code of ArduinoCI diff --git a/SampleProjects/ExcludeSomething/library.properties b/SampleProjects/ExcludeSomething/library.properties new file mode 100644 index 00000000..1745537f --- /dev/null +++ b/SampleProjects/ExcludeSomething/library.properties @@ -0,0 +1,10 @@ +name=TestSomething +version=0.1.0 +author=Ian Katz +maintainer=Ian Katz +sentence=Arduino CI unit test example +paragraph=A skeleton library demonstrating file exclusion +category=Other +url=https://github.com/Arduino-CI/arduino_ci/SampleProjects/ExcludeSomething +architectures=avr,esp8266 +includes=do-something.h diff --git a/SampleProjects/ExcludeSomething/src/exclude-something.cpp b/SampleProjects/ExcludeSomething/src/exclude-something.cpp new file mode 100644 index 00000000..951953f7 --- /dev/null +++ b/SampleProjects/ExcludeSomething/src/exclude-something.cpp @@ -0,0 +1,4 @@ +#include "exclude-something.h" +int excludeSomething(void) { + return -1; +}; diff --git a/SampleProjects/ExcludeSomething/src/exclude-something.h b/SampleProjects/ExcludeSomething/src/exclude-something.h new file mode 100644 index 00000000..abacb177 --- /dev/null +++ b/SampleProjects/ExcludeSomething/src/exclude-something.h @@ -0,0 +1,3 @@ +#pragma once +#include +int excludeSomething(void); diff --git a/SampleProjects/TestSomething/excludeThis/exclude-this.cpp b/SampleProjects/ExcludeSomething/src/excludeThis/exclude-this.cpp similarity index 100% rename from SampleProjects/TestSomething/excludeThis/exclude-this.cpp rename to SampleProjects/ExcludeSomething/src/excludeThis/exclude-this.cpp diff --git a/SampleProjects/TestSomething/excludeThis/exclude-this.h b/SampleProjects/ExcludeSomething/src/excludeThis/exclude-this.h similarity index 100% rename from SampleProjects/TestSomething/excludeThis/exclude-this.h rename to SampleProjects/ExcludeSomething/src/excludeThis/exclude-this.h diff --git a/SampleProjects/ExcludeSomething/test/null.cpp b/SampleProjects/ExcludeSomething/test/null.cpp new file mode 100644 index 00000000..d58eca29 --- /dev/null +++ b/SampleProjects/ExcludeSomething/test/null.cpp @@ -0,0 +1,7 @@ +#include + +unittest(nothing) +{ +} + +unittest_main() diff --git a/SampleProjects/OnePointFiveDummy/NoBase.cpp b/SampleProjects/OnePointFiveDummy/NoBase.cpp new file mode 100644 index 00000000..e69de29b diff --git a/SampleProjects/OnePointFiveDummy/NoBase.h b/SampleProjects/OnePointFiveDummy/NoBase.h new file mode 100644 index 00000000..e69de29b diff --git a/SampleProjects/OnePointFiveDummy/README.md b/SampleProjects/OnePointFiveDummy/README.md new file mode 100644 index 00000000..8ee1e7c5 --- /dev/null +++ b/SampleProjects/OnePointFiveDummy/README.md @@ -0,0 +1 @@ +This project resembles a "1.5 spec" library: it has `library.properties` and a `src/` directory that will be scanned recursively. `utility/`, if present, will be ignored. diff --git a/SampleProjects/OnePointFiveDummy/library.properties b/SampleProjects/OnePointFiveDummy/library.properties new file mode 100644 index 00000000..e69de29b diff --git a/SampleProjects/OnePointFiveDummy/src/YesSrc.cpp b/SampleProjects/OnePointFiveDummy/src/YesSrc.cpp new file mode 100644 index 00000000..e69de29b diff --git a/SampleProjects/OnePointFiveDummy/src/YesSrc.h b/SampleProjects/OnePointFiveDummy/src/YesSrc.h new file mode 100644 index 00000000..e69de29b diff --git a/SampleProjects/OnePointFiveDummy/src/subdir/YesSubdir.cpp b/SampleProjects/OnePointFiveDummy/src/subdir/YesSubdir.cpp new file mode 100644 index 00000000..e69de29b diff --git a/SampleProjects/OnePointFiveDummy/src/subdir/YesSubdir.h b/SampleProjects/OnePointFiveDummy/src/subdir/YesSubdir.h new file mode 100644 index 00000000..e69de29b diff --git a/SampleProjects/OnePointFiveDummy/test/null.cpp b/SampleProjects/OnePointFiveDummy/test/null.cpp new file mode 100644 index 00000000..d58eca29 --- /dev/null +++ b/SampleProjects/OnePointFiveDummy/test/null.cpp @@ -0,0 +1,7 @@ +#include + +unittest(nothing) +{ +} + +unittest_main() diff --git a/SampleProjects/OnePointFiveDummy/utility/ImNotHere.cpp b/SampleProjects/OnePointFiveDummy/utility/ImNotHere.cpp new file mode 100644 index 00000000..e69de29b diff --git a/SampleProjects/OnePointFiveDummy/utility/ImNotHere.h b/SampleProjects/OnePointFiveDummy/utility/ImNotHere.h new file mode 100644 index 00000000..e69de29b diff --git a/SampleProjects/OnePointFiveMalformed/README.md b/SampleProjects/OnePointFiveMalformed/README.md new file mode 100644 index 00000000..905d8336 --- /dev/null +++ b/SampleProjects/OnePointFiveMalformed/README.md @@ -0,0 +1 @@ +This project lacks a `library.properties` and so should be treated as a "1.0 spec" library -- the base and `utility` directories will be scanned for code, non-recursively. `src/`, if present, will be ignored. diff --git a/SampleProjects/OnePointFiveMalformed/YesBase.cpp b/SampleProjects/OnePointFiveMalformed/YesBase.cpp new file mode 100644 index 00000000..e69de29b diff --git a/SampleProjects/OnePointFiveMalformed/YesBase.h b/SampleProjects/OnePointFiveMalformed/YesBase.h new file mode 100644 index 00000000..e69de29b diff --git a/SampleProjects/OnePointFiveMalformed/src/ImNotHere.cpp b/SampleProjects/OnePointFiveMalformed/src/ImNotHere.cpp new file mode 100644 index 00000000..e69de29b diff --git a/SampleProjects/OnePointFiveMalformed/src/ImNotHere.h b/SampleProjects/OnePointFiveMalformed/src/ImNotHere.h new file mode 100644 index 00000000..e69de29b diff --git a/SampleProjects/OnePointFiveMalformed/utility/YesUtil.cpp b/SampleProjects/OnePointFiveMalformed/utility/YesUtil.cpp new file mode 100644 index 00000000..e69de29b diff --git a/SampleProjects/OnePointFiveMalformed/utility/YesUtil.h b/SampleProjects/OnePointFiveMalformed/utility/YesUtil.h new file mode 100644 index 00000000..e69de29b diff --git a/SampleProjects/OnePointOhDummy/README.md b/SampleProjects/OnePointOhDummy/README.md new file mode 100644 index 00000000..8afffdd9 --- /dev/null +++ b/SampleProjects/OnePointOhDummy/README.md @@ -0,0 +1 @@ +This project should resemble "1.0 spec" library -- the base and `utility` directories will be scanned for code, non-recursively. `src/`, if present, will be ignored. diff --git a/SampleProjects/OnePointOhDummy/YesBase.cpp b/SampleProjects/OnePointOhDummy/YesBase.cpp new file mode 100644 index 00000000..e69de29b diff --git a/SampleProjects/OnePointOhDummy/YesBase.h b/SampleProjects/OnePointOhDummy/YesBase.h new file mode 100644 index 00000000..e69de29b diff --git a/SampleProjects/OnePointOhDummy/src/ImNotHere.cpp b/SampleProjects/OnePointOhDummy/src/ImNotHere.cpp new file mode 100644 index 00000000..e69de29b diff --git a/SampleProjects/OnePointOhDummy/src/ImNotHere.h b/SampleProjects/OnePointOhDummy/src/ImNotHere.h new file mode 100644 index 00000000..e69de29b diff --git a/SampleProjects/OnePointOhDummy/test/null.cpp b/SampleProjects/OnePointOhDummy/test/null.cpp new file mode 100644 index 00000000..d58eca29 --- /dev/null +++ b/SampleProjects/OnePointOhDummy/test/null.cpp @@ -0,0 +1,7 @@ +#include + +unittest(nothing) +{ +} + +unittest_main() diff --git a/SampleProjects/OnePointOhDummy/utility/YesUtil.cpp b/SampleProjects/OnePointOhDummy/utility/YesUtil.cpp new file mode 100644 index 00000000..e69de29b diff --git a/SampleProjects/OnePointOhDummy/utility/YesUtil.h b/SampleProjects/OnePointOhDummy/utility/YesUtil.h new file mode 100644 index 00000000..e69de29b diff --git a/SampleProjects/README.md b/SampleProjects/README.md index 6b7ed569..8e5f5b11 100644 --- a/SampleProjects/README.md +++ b/SampleProjects/README.md @@ -1,7 +1,15 @@ Arduino Sample Projects ======================= -This directory contains projects that are meant to be built with and tested by this gem. Although this directory is named `SampleProjects`, it is by no means optional. These project test the testing framework itself, but also provide examples of how you might write your own tests (which should be placed in your system's Arduino `libraries` directory). +This directory contains projects that are intended solely for testing the various features of this gem -- to test the testing framework itself. The RSpec tests refer specifically to these projects. -* "DoSomething" is a simple test of the testing framework (arduino_ci) itself to verfy that passes and failures are properly identified and reported. -* "TestSomething" contains tests for all the mock features of arduino_ci. +Because of this, these projects include some intentional quirks that differ from what a well-formed an Arduino project for testing with `arduino_ci` might contain. See other projects in the "Arduino-CI" GitHub organization for practical examples. + + +* "TestSomething" contains a minimial library, but tests for all the C++ compilation feature-mocks of arduino_ci. +* "DoSomething" is a simple test of the testing framework (arduino_ci) itself to verfy that passes and failures are properly identified and reported. Because of this, it includes test files that are expected to fail -- they are prefixed with "bad-". +* "OnePointOhDummy" is a non-functional library meant to test file inclusion logic on libraries conforming to the "1.0" specification +* "OnePointFiveMalformed" is a non-functional library meant to test file inclusion logic on libraries that attempt to conform to the ["1.5" specfication](https://arduino.github.io/arduino-cli/latest/library-specification/) but fail to include a `src` directory +* "OnePointFiveDummy" is a non-functional library meant to test file inclusion logic on libraries conforming to the ["1.5" specfication](https://arduino.github.io/arduino-cli/latest/library-specification/) +* "DependOnSomething" is a non-functional library meant to test file inclusion logic with dependencies +* "ExcludeSomething" is a non-functional library meant to test directory exclusion logic diff --git a/SampleProjects/TestSomething/.arduino-ci.yml b/SampleProjects/TestSomething/.arduino-ci.yml index c418bdbe..f9890177 100644 --- a/SampleProjects/TestSomething/.arduino-ci.yml +++ b/SampleProjects/TestSomething/.arduino-ci.yml @@ -1,6 +1,4 @@ unittest: - exclude_dirs: - - excludeThis platforms: - uno - due diff --git a/SampleProjects/TestSomething/test-something.cpp b/SampleProjects/TestSomething/src/test-something.cpp similarity index 100% rename from SampleProjects/TestSomething/test-something.cpp rename to SampleProjects/TestSomething/src/test-something.cpp diff --git a/SampleProjects/TestSomething/test-something.h b/SampleProjects/TestSomething/src/test-something.h similarity index 100% rename from SampleProjects/TestSomething/test-something.h rename to SampleProjects/TestSomething/src/test-something.h diff --git a/SampleProjects/TestSomething/test/library.cpp b/SampleProjects/TestSomething/test/library.cpp index d80ad2c4..675d83e9 100644 --- a/SampleProjects/TestSomething/test/library.cpp +++ b/SampleProjects/TestSomething/test/library.cpp @@ -1,5 +1,5 @@ #include -#include "../test-something.h" +#include "../src/test-something.h" unittest(library_tests_something) { diff --git a/cpp/arduino/Arduino.h b/cpp/arduino/Arduino.h index 2c4f347e..213efa6b 100644 --- a/cpp/arduino/Arduino.h +++ b/cpp/arduino/Arduino.h @@ -5,6 +5,7 @@ Mock Arduino.h library. Where possible, variable names from the Arduino library are used to avoid conflicts */ + // Chars and strings #include "ArduinoDefines.h" @@ -72,5 +73,3 @@ inline unsigned int makeWord(unsigned int w) { return w; } inline unsigned int makeWord(unsigned char h, unsigned char l) { return (h << 8) | l; } #define word(...) makeWord(__VA_ARGS__) - - diff --git a/lib/arduino_ci.rb b/lib/arduino_ci.rb index 14280084..344a1463 100644 --- a/lib/arduino_ci.rb +++ b/lib/arduino_ci.rb @@ -2,6 +2,7 @@ require "arduino_ci/arduino_installation" require "arduino_ci/cpp_library" require "arduino_ci/ci_config" +require "arduino_ci/library_properties" # ArduinoCI contains classes for automated testing of Arduino code on the command line # @author Ian Katz diff --git a/lib/arduino_ci/arduino_installation.rb b/lib/arduino_ci/arduino_installation.rb index 5dfc5c8b..9ca32ada 100644 --- a/lib/arduino_ci/arduino_installation.rb +++ b/lib/arduino_ci/arduino_installation.rb @@ -110,11 +110,11 @@ def autolocate!(output = $stdout) # Forcibly install Arduino from the web # @return [bool] Whether the command succeeded def force_install(output = $stdout, version = DESIRED_ARDUINO_IDE_VERSION) - worker_class = case Host.os - when :osx then ArduinoDownloaderOSX - when :windows then ArduinoDownloaderWindows - when :linux then ArduinoDownloaderLinux - end + worker_class = case Host.os + when :osx then ArduinoDownloaderOSX + when :windows then ArduinoDownloaderWindows + when :linux then ArduinoDownloaderLinux + end worker = worker_class.new(version, output) worker.execute end diff --git a/lib/arduino_ci/cpp_library.rb b/lib/arduino_ci/cpp_library.rb index 42323be2..e61201c2 100644 --- a/lib/arduino_ci/cpp_library.rb +++ b/lib/arduino_ci/cpp_library.rb @@ -55,6 +55,32 @@ def initialize(base_dir, arduino_lib_dir, exclude_dirs) @vendor_bundle_cache = nil 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" + end + + # Whether library.properties definitions for this library exist + # @return [bool] + def library_properties? + lib_props = library_properties_path + lib_props.exist? && lib_props.file? + 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 + # @return [bool] + def one_point_five? + return false unless library_properties? + + src_dir = (@base_dir + "src") + src_dir.exist? && src_dir.directory? + end + # Guess whether a file is part of the vendor bundle (indicating we should ignore it). # # A safe way to do this seems to be to check whether any of the installed gems @@ -110,6 +136,8 @@ def vendor_bundle?(path) # @param path [Pathname] The path to check # @return [bool] def in_tests_dir?(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) } @@ -150,43 +178,92 @@ 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 cpp_files_in(some_dir) + 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? - real = some_dir.realpath - files = Find.find(real).map { |p| Pathname.new(p) }.reject(&:directory?) - cpp = files.select { |path| CPP_EXTENSIONS.include?(path.extname.downcase) } + files = some_dir.realpath.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) 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_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 + end + + # Header files that are part of the project library under test + # @return [Array] + def header_files + ret = if one_point_five? + code_files_in_recursive(@base_dir + "src", HPP_EXTENSIONS) + else + [@base_dir, @base_dir + "utility"].map { |d| code_files_in(d, HPP_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) } + end + # CPP files that are part of the project library under test # @return [Array] def cpp_files - cpp_files_in(@base_dir).reject { |p| vendor_bundle?(p) || in_tests_dir?(p) || in_exclude_dir?(p) } + 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) } end # CPP files that are part of the arduino mock library we're providing # @return [Array] def cpp_files_arduino - cpp_files_in(ARDUINO_HEADER_DIR) + code_files_in(ARDUINO_HEADER_DIR, CPP_EXTENSIONS) end # CPP files that are part of the unit test library we're providing # @return [Array] def cpp_files_unittest - cpp_files_in(UNITTEST_HEADER_DIR) + code_files_in(UNITTEST_HEADER_DIR, CPP_EXTENSIONS) end # CPP files that are part of the 3rd-party libraries we're including # @param [Array] aux_libraries # @return [Array] def cpp_files_libraries(aux_libraries) - arduino_library_src_dirs(aux_libraries).map { |d| cpp_files_in(d) }.flatten.uniq + arduino_library_src_dirs(aux_libraries).map { |d| code_files_in(d, CPP_EXTENSIONS) }.flatten.uniq end # Returns the Pathnames for all paths to exclude from testing and compilation @@ -204,15 +281,13 @@ def tests_dir # The files provided by the user that contain unit tests # @return [Array] def test_files - cpp_files_in(tests_dir) + code_files_in(tests_dir, CPP_EXTENSIONS) end # Find all directories in the project library that include C++ header files # @return [Array] def header_dirs - real = @base_dir.realpath - all_files = Find.find(real).map { |f| Pathname.new(f) }.reject(&:directory?) - unbundled = all_files.reject { |path| vendor_bundle?(path) } + unbundled = header_files.reject { |path| vendor_bundle?(path) } unexcluded = unbundled.reject { |path| in_exclude_dir?(path) } files = unexcluded.select { |path| HPP_EXTENSIONS.include?(path.extname.downcase) } files.map(&:dirname).uniq @@ -236,23 +311,19 @@ def gcc_version(gcc_binary) @last_err end - # Arduino library directories containing sources + # Arduino library directories containing sources -- only those of the dependencies # @return [Array] def arduino_library_src_dirs(aux_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 - # TODO: be smart and implement library spec (library.properties, etc)? - subdirs = ["", "src", "utility"] - all_aux_include_dirs_nested = aux_libraries.map do |libdir| - # library manager coerces spaces in package names to underscores - # see https://github.com/Arduino-CI/arduino_ci/issues/132#issuecomment-518857059 - legal_libdir = libdir.tr(" ", "_") - subdirs.map { |subdir| Pathname.new(@arduino_lib_dir) + legal_libdir + subdir } - end - all_aux_include_dirs_nested.flatten.select(&:exist?).select(&:directory?) + + aux_libraries.map { |d| self.class.new(@arduino_lib_dir + d, @arduino_lib_dir, @exclude_dirs).header_dirs }.flatten.uniq end # GCC command line arguments for including aux libraries + # + # This function recursively collects the library directores of the dependencies + # # @param aux_libraries [Array] The external Arduino libraries required by this project # @return [Array] The GCC command-line flags necessary to include those libraries def include_args(aux_libraries) @@ -315,6 +386,9 @@ def test_args(aux_libraries, ci_gcc_config) end # build a file for running a test of the given unit test file + # + # The dependent libraries configuration is appended with data from library.properties internal to the library under test + # # @param test_file [Pathname] The path to the file containing the unit tests # @param aux_libraries [Array] The external Arduino libraries required by this project # @param ci_gcc_config [Hash] The GCC config object @@ -333,8 +407,12 @@ def build_for_test_with_configuration(test_file, aux_libraries, gcc_binary, ci_g "-fsanitize=address" ] end - arg_sets << test_args(aux_libraries, ci_gcc_config) - arg_sets << cpp_files_libraries(aux_libraries).map(&:to_s) + + # 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_libaries + arduino_library_dependencies + arg_sets << test_args(full_aux_libraries, ci_gcc_config) + arg_sets << cpp_files_libraries(full_aux_libraries).map(&:to_s) arg_sets << [test_file.to_s] args = arg_sets.flatten(1) return nil unless run_gcc(gcc_binary, *args) @@ -343,14 +421,31 @@ def build_for_test_with_configuration(test_file, aux_libraries, gcc_binary, ci_g executable end + # print any found stack dumps + # @param executable [Pathname] the path to the test file + def print_stack_dump(executable) + possible_dumpfiles = [ + executable.sub_ext(executable.extname + ".stackdump") + ] + possible_dumpfiles.select(&:exist?).each do |dump| + puts "========== Stack dump from #{dump}:" + File.foreach(dump) { |line| print " #{line}" } + end + end + # run a test file - # @param [Pathname] the path to the test file + # @param executable [Pathname] the path to the test file # @return [bool] whether all tests were successful def run_test_file(executable) @last_cmd = executable @last_out = "" @last_err = "" - Host.run_and_output(executable.to_s.shellescape) + ret = Host.run_and_output(executable.to_s.shellescape) + + # print any stack traces found during a failure + print_stack_dump(executable) unless ret + + ret end end diff --git a/lib/arduino_ci/library_properties.rb b/lib/arduino_ci/library_properties.rb new file mode 100644 index 00000000..1a080713 --- /dev/null +++ b/lib/arduino_ci/library_properties.rb @@ -0,0 +1,86 @@ +module ArduinoCI + + # Information about an Arduino library package, as specified by the library.properties file + # + # See https://arduino.github.io/arduino-cli/library-specification/#libraryproperties-file-format + class LibraryProperties + + # @return [Hash] The properties file parsed as a hash + attr_reader :fields + + # @param path [Pathname] The path to the library.properties file + def initialize(path) + @fields = {} + File.foreach(path) do |line| + parts = line.split("=", 2) + @fields[parts[0]] = parts[1].chomp unless parts.empty? + end + 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. + # + # The formatting function MUST be a static method on this class. This is a limitation caused by the desire + # to both (1) expose the formatters outside this class, and (2) use them for metaprogramming without the + # having to name the entire function. field_reader is a static method, so if not for the fact that + # `self.class.methods.include? formatter` fails to work for class methods in this context (unlike + # `self.methods.include?`, which properly finds instance methods), I would allow either one and just + # conditionally `define_method` the proper definition + # + # @param name [String] What the accessor will be called + # @param field_num [Integer] The name of the key of the property + # @param formatter [Symbol] The symbol for the formatting function to apply to the field (optional) + # @return [void] + # @macro [attach] field_reader + # @!attribute [r] $1 + # @return property $2 of the library.properties file, formatted with the function {$3} + def self.field_reader(name, formatter = nil) + key = name.to_s + if formatter.nil? + define_method(name) { @fields[key] } + else + define_method(name) { @fields.key?(key) ? self.class.send(formatter.to_sym, @fields[key]) : nil } + end + end + + # Parse a value as a comma-separated array + # @param input [String] + # @return [Array] The individual values + def self._csv(input) + input.split(",").map(&:strip) + end + + # Parse a value as a boolean + # @param input [String] + # @return [Array] The individual values + def self._bool(input) + input == "true" # no indication given in the docs that anything but lowercase "true" indicates boolean true. + end + + field_reader :name + field_reader :version + field_reader :author, :_csv + field_reader :maintainer + field_reader :sentence + field_reader :paragraph + field_reader :category + field_reader :url + field_reader :architectures, :_csv + field_reader :depends, :_csv + field_reader :dot_a_linkage, :_bool + field_reader :includes, :_csv + field_reader :precompiled, :_bool + field_reader :ldflags, :_csv + + # The value of sentence always will be prepended, so you should start by writing the second sentence here + # + # (according to the docs) + # @return [String] the sentence and paragraph together + def full_paragraph + [sentence, paragraph].join(" ") + end + + end + +end diff --git a/spec/cpp_library_spec.rb b/spec/cpp_library_spec.rb index 40b2407a..6a25468f 100644 --- a/spec/cpp_library_spec.rb +++ b/spec/cpp_library_spec.rb @@ -10,49 +10,197 @@ def get_relative_dir(sampleprojects_tests_dir) sampleprojects_tests_dir.relative_path_from(base_dir) end -RSpec.describe ArduinoCI::CppLibrary do - next if skip_ruby_tests - cpp_lib_path = sampleproj_path + "DoSomething" - 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 - dosomething_cpp_files = [Pathname.new("DoSomething") + "do-something.cpp"] - relative_paths = cpp_library.cpp_files.map { |f| get_relative_dir(f) } - expect(relative_paths).to match_array(dosomething_cpp_files) + +RSpec.describe "ExcludeSomething C++" do + next if skip_cpp_tests + + cpp_lib_path = sampleproj_path + "ExcludeSomething" + 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 + 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) } + expect(relative_paths).to match_array(excludesomething_cpp_files) + end end - end - context "header_dirs" do - it "finds directories containing h files" do - dosomething_header_dirs = [Pathname.new("DoSomething")] - relative_paths = cpp_library.header_dirs.map { |f| get_relative_dir(f) } - expect(relative_paths).to match_array(dosomething_header_dirs) + 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")) + expect(result).to be nil + end end end - context "tests_dir" do - it "locates the tests directory" do - # 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) - expect(relative_path.to_s).to eq("DoSomething/test") + 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 + excludesomething_cpp_files = [ + Pathname.new("ExcludeSomething/src/exclude-something.cpp") + ] + relative_paths = cpp_library.cpp_files.map { |f| get_relative_dir(f) } + expect(relative_paths).to match_array(excludesomething_cpp_files) + end end + end - context "test_files" do - it "finds cpp files in directory" do - dosomething_test_files = [ +end + +RSpec.describe ArduinoCI::CppLibrary do + next if skip_ruby_tests + + answers = { + DoSomething: { + one_five: false, + cpp_files: [Pathname.new("DoSomething") + "do-something.cpp"], + cpp_files_libraries: [], + header_dirs: [Pathname.new("DoSomething")], + arduino_library_src_dirs: [], + test_files: [ "DoSomething/test/good-null.cpp", "DoSomething/test/good-library.cpp", "DoSomething/test/bad-null.cpp", ].map { |f| Pathname.new(f) } - relative_paths = cpp_library.test_files.map { |f| get_relative_dir(f) } - expect(relative_paths).to match_array(dosomething_test_files) + }, + OnePointOhDummy: { + one_five: false, + cpp_files: [ + "OnePointOhDummy/YesBase.cpp", + "OnePointOhDummy/utility/YesUtil.cpp", + ].map { |f| Pathname.new(f) }, + cpp_files_libraries: [], + header_dirs: [ + "OnePointOhDummy", + "OnePointOhDummy/utility" + ].map { |f| Pathname.new(f) }, + arduino_library_src_dirs: [], + test_files: [ + "OnePointOhDummy/test/null.cpp", + ].map { |f| Pathname.new(f) } + }, + OnePointFiveMalformed: { + one_five: false, + cpp_files: [ + "OnePointFiveMalformed/YesBase.cpp", + "OnePointFiveMalformed/utility/YesUtil.cpp", + ].map { |f| Pathname.new(f) }, + cpp_files_libraries: [], + header_dirs: [ + "OnePointFiveMalformed", + "OnePointFiveMalformed/utility" + ].map { |f| Pathname.new(f) }, + arduino_library_src_dirs: [], + test_files: [] + }, + OnePointFiveDummy: { + one_five: true, + cpp_files: [ + "OnePointFiveDummy/src/YesSrc.cpp", + "OnePointFiveDummy/src/subdir/YesSubdir.cpp", + ].map { |f| Pathname.new(f) }, + cpp_files_libraries: [], + header_dirs: [ + "OnePointFiveDummy/src", + "OnePointFiveDummy/src/subdir", + ].map { |f| Pathname.new(f) }, + arduino_library_src_dirs: [], + test_files: [ + "OnePointFiveDummy/test/null.cpp", + ].map { |f| Pathname.new(f) } + } + } + + # easier to construct this one from the other test cases + answers[:DependOnSomething] = { + one_five: 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! + arduino_library_src_dirs: answers[:OnePointOhDummy][:header_dirs] + answers[:OnePointFiveDummy][:header_dirs], + test_files: [ + "DependOnSomething/test/null.cpp", + ].map { |f| Pathname.new(f) } + } + + answers.freeze + + answers.each do |sampleproject, expected| + 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 + + it "detects 1.5 format" do + expect(cpp_library.one_point_five?).to eq(expected[:one_five]) + end + + context "cpp_files" do + it "finds cpp files in directory" do + relative_paths = cpp_library.cpp_files.map { |f| get_relative_dir(f) } + 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) } + 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) } + expect(relative_paths.map(&:to_s)).to match_array(expected[:header_dirs].map(&:to_s)) + end + end + + context "tests_dir" do + it "locates the tests directory" do + # 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) + 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) } + expect(relative_paths.map(&:to_s)).to match_array(expected[:test_files].map(&:to_s)) + end + end + + 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) } + expect(relative_paths.map(&:to_s)).to match_array(expected[:arduino_library_src_dirs].map(&:to_s)) + end + end end end context "test" do + 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 after(:each) do |example| diff --git a/spec/library_properties_spec.rb b/spec/library_properties_spec.rb new file mode 100644 index 00000000..3c6de1ee --- /dev/null +++ b/spec/library_properties_spec.rb @@ -0,0 +1,45 @@ +require "spec_helper" + +RSpec.describe ArduinoCI::LibraryProperties do + + context "property extraction" do + library_properties = ArduinoCI::LibraryProperties.new(Pathname.new(__dir__) + "properties/example.library.properties") + + expected = { + string: { + name: "WebServer", + version: "1.0.0", + maintainer: "Cristian Maglie ", + sentence: "A library that makes coding a Webserver a breeze.", + paragraph: "Supports HTTP1.1 and you can do GET and POST.", + category: "Communication", + url: "http://example.com/", + }, + + bool: { + precompiled: true + }, + + csv: { + author: ["Cristian Maglie ", "Pippo Pluto "], + architectures: ["avr"], + includes: ["WebServer.h"], + depends: ["ArduinoHttpClient"], + }, + }.freeze + + expected.each do |atype, values| + values.each do |meth, val| + it "reads #{atype} field #{meth}" do + expect(library_properties.send(meth)).to eq(val) + end + end + end + + it "doesn't crash on nonexistent fields" do + expect(library_properties.dot_a_linkage).to be(nil) + end + end + + +end diff --git a/spec/properties/example.library.properties b/spec/properties/example.library.properties new file mode 100644 index 00000000..f0cd9bb3 --- /dev/null +++ b/spec/properties/example.library.properties @@ -0,0 +1,12 @@ +name=WebServer +version=1.0.0 +author=Cristian Maglie , Pippo Pluto +maintainer=Cristian Maglie +sentence=A library that makes coding a Webserver a breeze. +paragraph=Supports HTTP1.1 and you can do GET and POST. +category=Communication +url=http://example.com/ +architectures=avr +includes=WebServer.h +depends=ArduinoHttpClient +precompiled=true diff --git a/spec/testsomething_unittests_spec.rb b/spec/testsomething_unittests_spec.rb index 5e0f9152..4cb49541 100644 --- a/spec/testsomething_unittests_spec.rb +++ b/spec/testsomething_unittests_spec.rb @@ -10,47 +10,16 @@ def get_relative_dir(sampleprojects_tests_dir) sampleprojects_tests_dir.relative_path_from(base_dir) end -RSpec.describe "TestSomething C++ without excludes" 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"), - []) - context "cpp_files" do - it "finds cpp files in directory" do - testsomething_cpp_files = [ - Pathname.new("TestSomething/test-something.cpp"), - Pathname.new("TestSomething/excludeThis/exclude-this.cpp") - ] - relative_paths = cpp_library.cpp_files.map { |f| get_relative_dir(f) } - expect(relative_paths).to match_array(testsomething_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")) - expect(result).to be nil - end - end - -end 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"), - ["excludeThis"].map(&Pathname.method(:new))) + ["src/excludeThis"].map(&Pathname.method(:new))) context "cpp_files" do it "finds cpp files in directory" do - testsomething_cpp_files = [Pathname.new("TestSomething/test-something.cpp")] + testsomething_cpp_files = [Pathname.new("TestSomething/src/test-something.cpp")] relative_paths = cpp_library.cpp_files.map { |f| get_relative_dir(f) } expect(relative_paths).to match_array(testsomething_cpp_files) end