diff --git a/.gitattributes b/.gitattributes index 4282322a..640e8d08 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,40 @@ +# https://docs.github.com/en/github/using-git/configuring-git-to-handle-line-endings +# https://git-scm.com/docs/gitattributes +# https://git-scm.com/docs/git-config +# https://adaptivepatchwork.com/2012/03/01/mind-the-end-of-your-line/ + +# Configure this repository to use Git's type detection algorithm to guess +# whether a file is text or binary. Text files will have line endings converted +# as if you had set +# eol=native +# That is, on Windows text files will have CRLF line endings in your working +# directory while on Linux and macOS your text files will have LF line endings +# in your working directory. In either case, they will have LF line endings in +# the Git repository itself. + # Set the default behavior, in case people don't have core.autocrlf set. -* text eol=lf +* text=auto + +# Explicitly declare text files you want to always be normalized and converted +# to native line endings on checkout. Git would likely get these right, but +# we can be sure by adding them here. +*.ino text diff=cpp +*.c text diff=c +*.cc text diff=cpp +*.cxx text diff=cpp +*.cpp text diff=cpp +*.c++ text diff=cpp +*.hpp text diff=cpp +*.h text diff=c +*.h++ text diff=cpp +*.hh text diff=cpp + +*.md text +*.yaml text +*.yml text + + +# Denote all files that are truly binary and should not be modified. +# Even if we don't have any of these, they make a good example. +*.png binary +*.jpg binary diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 7e1b5195..0e2ce744 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -7,4 +7,4 @@ In this project, we define a workflow for each target platform. **If you're loo The reason that all platforms are tested in _this_ project is to ensure that, as a framework, `arduino_ci` will run properly on any developer's personal workstation (regardless of OS). -For testing an individual Arduino library in the context of GitHub, [Linux is the cheapest option](https://docs.github.com/en/free-pro-team@latest/github/setting-up-and-managing-billing-and-payments-on-github/about-billing-for-github-actions) and produces results identical to the other OSes. +For testing an individual Arduino library in the context of GitHub, [Linux is the cheapest option](https://docs.github.com/en/free-pro-team@latest/github/setting-up-and-managing-billing-and-payments-on-github/about-billing-for-github-actions) and should produce results identical to the other OSes. diff --git a/.github/workflows/linux.yaml b/.github/workflows/linux.yaml index 8a05f584..9c9247a8 100644 --- a/.github/workflows/linux.yaml +++ b/.github/workflows/linux.yaml @@ -1,7 +1,7 @@ # This is the name of the workflow, visible on GitHub UI name: linux -on: [pull_request] +on: [push, pull_request] jobs: "unittest_lint_sampleproject": diff --git a/.github/workflows/macos.yaml b/.github/workflows/macos.yaml index ffd88f9d..96357648 100644 --- a/.github/workflows/macos.yaml +++ b/.github/workflows/macos.yaml @@ -1,7 +1,7 @@ # This is the name of the workflow, visible on GitHub UI name: macos -on: [pull_request] +on: [push, pull_request] jobs: "unittest_lint_sampleproject": diff --git a/.github/workflows/windows.yaml b/.github/workflows/windows.yaml index 6001624e..6a4afa72 100644 --- a/.github/workflows/windows.yaml +++ b/.github/workflows/windows.yaml @@ -1,7 +1,7 @@ # This is the name of the workflow, visible on GitHub UI name: windows -on: [pull_request] +on: [push, pull_request] jobs: "unittest_lint_sampleproject": diff --git a/.gitignore b/.gitignore index 2b5aa6a7..0c895baf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.arduino_ci /.bundle/ /.yardoc Gemfile.lock @@ -15,3 +16,5 @@ vendor # C++ stuff *.bin *.bin.dSYM +*.so +*.so.dSYM diff --git a/CHANGELOG.md b/CHANGELOG.md index f29be1a6..1e02a80b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,19 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] ### Added +- Show output from successful compile +- `--min-free-space=N` command-line argument to fail if free space is below requred value +- Support for `dtostrf()` ### Changed - Change 266 files from CRLF to LF. +- Run tests on push as well as on a pull request so developers can see impact +- Update .gitattributes so we have consistent line endings +- Put build artifacts in a separate directory to reduce clutter. +- We now compile a shared library to be used for each test +- Replace `#define yield() _NOP()` with `inline void yield() { _NOP(); }` so that other code can define a `yield()` function. +- Add `_BV()` macro. +- Apply "rule of three" to Client copy constructor and copy assignment operator ### Deprecated @@ -24,9 +34,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Added - Better indications of the build phases in the test runner `arduino_ci.rb` - Better indications of which example sketch is being compiled as part of testing +- Allow use of watchdog timer in application code (though it doesn't do anything) ### Changed - Topmost installtion instructions now suggest `gem install arduino_ci` instead of using a `Gemfile`. Reasons for using a `Gemfile` are listed and discussed separately further down the README. +- Stream::readStreamUntil() no longer returns delimiter ### Removed - scanning of `library.properties`; this can and should now be performed by the standalone [`arduino-lint` tool](https://arduino.github.io/arduino-lint). diff --git a/SampleProjects/TestSomething/.arduino-ci.yml b/SampleProjects/TestSomething/.arduino-ci.yml index f9890177..1754effb 100644 --- a/SampleProjects/TestSomething/.arduino-ci.yml +++ b/SampleProjects/TestSomething/.arduino-ci.yml @@ -1,3 +1,18 @@ +platforms: + uno: + board: arduino:avr:uno + package: arduino:avr + gcc: + features: + defines: + - __AVR__ + - __AVR_ATmega328P__ + - ARDUINO_ARCH_AVR + - ARDUINO_AVR_UNO + warnings: + - no-unknown-attributes + flags: + unittest: platforms: - uno diff --git a/SampleProjects/TestSomething/test/clientServer.cpp b/SampleProjects/TestSomething/test/clientServer.cpp index f088c821..5e66b6b3 100644 --- a/SampleProjects/TestSomething/test/clientServer.cpp +++ b/SampleProjects/TestSomething/test/clientServer.cpp @@ -19,6 +19,13 @@ unittest(Client) { assertEqual(outData + "\r\n", inData); } +unittest(Client_copy_constructor) { + Client client1; + Client client2; + client2 = client1; + assertTrue(true); +} + unittest(IPAddress) { IPAddress ipAddress0; assertEqual(0, ipAddress0.asWord()); diff --git a/SampleProjects/TestSomething/test/stdlib.cpp b/SampleProjects/TestSomething/test/stdlib.cpp index ce0b72fb..31bd0735 100644 --- a/SampleProjects/TestSomething/test/stdlib.cpp +++ b/SampleProjects/TestSomething/test/stdlib.cpp @@ -43,4 +43,12 @@ unittest(library_tests_itoa) } +unittest(library_tests_dtostrf) +{ + float num = 123.456; + char buffer[10]; + dtostrf(num, 7, 3, buffer); + assertEqual(strncmp(buffer, "123.456", sizeof(buffer)), 0); +} + unittest_main() diff --git a/SampleProjects/TestSomething/test/stream.cpp b/SampleProjects/TestSomething/test/stream.cpp index 78f0a0fa..dd218d97 100644 --- a/SampleProjects/TestSomething/test/stream.cpp +++ b/SampleProjects/TestSomething/test/stream.cpp @@ -69,4 +69,16 @@ unittest(stream_parse) } +unittest(readStringUntil) { + String data = ""; + unsigned long micros = 100; + data = "abc:def"; + + Stream s; + s.mGodmodeDataIn = &data; + s.mGodmodeMicrosDelay = µs; + // result should not include delimiter + assertEqual("abc", s.readStringUntil(':')); + assertEqual("def", s.readStringUntil(':')); +} unittest_main() diff --git a/SampleProjects/TestSomething/test/wdt.cpp b/SampleProjects/TestSomething/test/wdt.cpp new file mode 100644 index 00000000..a037363c --- /dev/null +++ b/SampleProjects/TestSomething/test/wdt.cpp @@ -0,0 +1,12 @@ +#include +#include +#include + +unittest(wdt) { + wdt_disable(); + wdt_enable(WDTO_8S); + wdt_reset(); + assertTrue(true); +} + +unittest_main() diff --git a/cpp/arduino/Arduino.h b/cpp/arduino/Arduino.h index ad7d5a99..b301ec21 100644 --- a/cpp/arduino/Arduino.h +++ b/cpp/arduino/Arduino.h @@ -36,11 +36,11 @@ typedef uint8_t byte; #define highByte(w) ((uint8_t) ((w) >> 8)) #define lowByte(w) ((uint8_t) ((w) & 0xff)) -// might as well use that NO-op macro for these, while unit testing -// you need interrupts? interrupt yourself -#define yield() _NOP() -#define interrupts() _NOP() -#define noInterrupts() _NOP() +// using #define for these makes it impossible for other code to use as function +// names! +inline void yield() { _NOP(); } +inline void interrupts() { _NOP(); } +inline void noInterrupts() { _NOP(); } // TODO: correctly establish this per-board! #define F_CPU 1000000UL @@ -50,10 +50,7 @@ typedef uint8_t byte; typedef unsigned int word; -#define bit(b) (1UL << (b)) - - - +#define _BV(bit) (1 << (bit)) // Get the bit location within the hardware port of the given virtual pin. // This comes from the pins_*.c file for the active board configuration. diff --git a/cpp/arduino/Client.h b/cpp/arduino/Client.h index 154e618d..759267a4 100644 --- a/cpp/arduino/Client.h +++ b/cpp/arduino/Client.h @@ -1,7 +1,7 @@ #pragma once -#include #include +#include class Client : public Stream { public: @@ -11,6 +11,25 @@ class Client : public Stream { mGodmodeDataIn = new String; } } + Client(const Client &client) { // copy constructor + if (this != &client) { // not a self-assignment + if (mGodmodeDataIn && + client.mGodmodeDataIn) { // replace what we previously had + delete mGodmodeDataIn; // get rid of previous value + mGodmodeDataIn = new String(client.mGodmodeDataIn->c_str()); + } + } + } + Client &operator=(const Client &client) { // copy assignment operator + if (this != &client) { // not a self-assignment + if (mGodmodeDataIn && + client.mGodmodeDataIn) { // replace what we previously had + delete mGodmodeDataIn; // get rid of previous value + mGodmodeDataIn = new String(client.mGodmodeDataIn->c_str()); + } + } + return *this; + } ~Client() { if (mGodmodeDataIn) { delete mGodmodeDataIn; diff --git a/cpp/arduino/Stream.h b/cpp/arduino/Stream.h index 9f766cbf..ef572095 100644 --- a/cpp/arduino/Stream.h +++ b/cpp/arduino/Stream.h @@ -186,7 +186,7 @@ class Stream : public Print ret = String(*mGodmodeDataIn); mGodmodeDataIn->clear(); } else { - ret = mGodmodeDataIn->substring(0, idxTrm + 1); + ret = mGodmodeDataIn->substring(0, idxTrm); fastforward(idxTrm + 1); } return ret; diff --git a/cpp/arduino/avr/wdt.h b/cpp/arduino/avr/wdt.h new file mode 100644 index 00000000..ab489f6b --- /dev/null +++ b/cpp/arduino/avr/wdt.h @@ -0,0 +1,16 @@ +// Stub for testing that doesn't do anything (but at least compiles!) + +#define wdt_disable() (void)0 +#define wdt_enable(timeout) (void)0 +#define wdt_reset() (void)0 + +#define WDTO_15MS 0 +#define WDTO_30MS 1 +#define WDTO_60MS 2 +#define WDTO_120MS 3 +#define WDTO_250MS 4 +#define WDTO_500MS 5 +#define WDTO_1S 6 +#define WDTO_2S 7 +#define WDTO_4S 8 +#define WDTO_8S 9 diff --git a/cpp/arduino/stdlib.cpp b/cpp/arduino/stdlib.cpp index 931e5138..57b05600 100644 --- a/cpp/arduino/stdlib.cpp +++ b/cpp/arduino/stdlib.cpp @@ -18,6 +18,7 @@ ** is out of range. */ +#include #include #include @@ -59,3 +60,22 @@ char *itoa(int N, char *str, int base) return str; } #endif + +/* +The dtostrf() function converts the double value passed in val into +an ASCII representationthat will be stored under s. The caller is +responsible for providing sufficient storage in s. + +Conversion is done in the format “[-]d.ddd”. The minimum field width +of the output string (including the ‘.’ and the possible sign for +negative values) is given in width, and prec determines the number of +digits after the decimal sign. width is signed value, negative for +left adjustment. + +The dtostrf() function returns the pointer to the converted string s. +*/ + +char *dtostrf(double __val, signed char __width, unsigned char __prec, char *__s) { + sprintf(__s, "%*.*f", __width, __prec, __val); + return __s; +} diff --git a/cpp/arduino/stdlib.h b/cpp/arduino/stdlib.h index c685e645..b7fec869 100644 --- a/cpp/arduino/stdlib.h +++ b/cpp/arduino/stdlib.h @@ -12,3 +12,6 @@ * https://stackoverflow.com/questions/190229/where-is-the-itoa-function-in-linux */ char *itoa(int val, char *s, int radix); + +// another function provided by Arduino +char * dtostrf(double __val, signed char __width, unsigned char __prec, char *__s); diff --git a/exe/arduino_ci.rb b/exe/arduino_ci.rb index 01234e7c..11e9770d 100755 --- a/exe/arduino_ci.rb +++ b/exe/arduino_ci.rb @@ -24,6 +24,7 @@ def self.parse(options) ci_config: { "unittest" => unit_config }, + min_free_space: 0, } opt_parser = OptionParser.new do |opts| @@ -49,6 +50,10 @@ def self.parse(options) unit_config["testfiles"]["reject"] << p end + opts.on("--min-free-space=VALUE", "Minimum free SRAM memory for stack/heap") do |p| + output_options[:min_free_space] = p.to_i + end + opts.on("-h", "--help", "Prints this help") do puts opts puts @@ -416,9 +421,27 @@ def perform_unit_tests(cpp_library, file_config) platforms.each do |p| puts - config.allowable_unittest_files(cpp_library.test_files).each do |unittest_path| - unittest_name = unittest_path.basename.to_s - compilers.each do |gcc_binary| + compilers.each do |gcc_binary| + # before compiling the tests, build a shared library of everything except the test code + attempt_multiline("Build shared library with #{gcc_binary} for #{p}") do + exe = cpp_library.build_for_test_with_configuration( + nil, # nil is a flag that we are building the shared library with everything else + config.aux_libraries_for_unittest, + gcc_binary, + config.gcc_config(p) + ) + puts + unless exe + puts "Last command: #{cpp_library.last_cmd}" + puts cpp_library.last_out + puts cpp_library.last_err + next false + end + true + end + # now build and run each test using the shared library build above + config.allowable_unittest_files(cpp_library.test_files).each do |unittest_path| + unittest_name = unittest_path.basename.to_s attempt_multiline("Unit testing #{unittest_name} with #{gcc_binary} for #{p}") do exe = cpp_library.build_for_test_with_configuration( unittest_path, @@ -478,8 +501,18 @@ def perform_example_compilation_tests(cpp_library, config) board = ovr_config.platform_info[p][:board] attempt("Compiling #{example_name} for #{board}") do ret = @backend.compile_sketch(example_path, board) - unless ret - puts + puts + if ret + output = @backend.last_msg + puts output + i = output.index("leaving") + free_space = output[i + 8..-1].to_i + min_free_space = @cli_options[:min_free_space] + if free_space < min_free_space + puts "Free space of #{free_space} is less than minimum of #{min_free_space}" + ret = false + end + else puts "Last command: #{@backend.last_msg}" puts @backend.last_err end diff --git a/lib/arduino_ci/arduino_backend.rb b/lib/arduino_ci/arduino_backend.rb index 4e04f5b3..e67710bf 100644 --- a/lib/arduino_ci/arduino_backend.rb +++ b/lib/arduino_ci/arduino_backend.rb @@ -164,6 +164,7 @@ def compile_sketch(path, boardname) return false end ret = run_and_capture("compile", "--fqbn", boardname, "--warnings", "all", "--dry-run", path.to_s) + @last_msg = ret[:out] ret[:success] end diff --git a/lib/arduino_ci/cpp_library.rb b/lib/arduino_ci/cpp_library.rb index 4ad7307f..ceaf99e9 100644 --- a/lib/arduino_ci/cpp_library.rb +++ b/lib/arduino_ci/cpp_library.rb @@ -443,7 +443,7 @@ def feature_args(ci_gcc_config) def warning_args(ci_gcc_config) return [] if ci_gcc_config[:warnings].nil? - ci_gcc_config[:features].map { |w| "-W#{w}" } + ci_gcc_config[:warnings].map { |w| "-W#{w}" } end # GCC command line arguments for defines (e.g. -Dhave_something) @@ -464,16 +464,15 @@ def flag_args(ci_gcc_config) ci_gcc_config[:flags] end - # All GCC command line args for building any unit test + # All non-CPP GCC command line args for building any unit test. + # We leave out the CPP files so they can be included or not + # depending on whether we are building a shared library. # @param aux_libraries [Array] The external Arduino libraries required by this project # @param ci_gcc_config [Hash] The GCC config object # @return [Array] GCC command-line flags def test_args(aux_libraries, ci_gcc_config) # TODO: something with libraries? ret = include_args(aux_libraries) - ret += cpp_files_arduino.map(&:to_s) - ret += cpp_files_unittest.map(&:to_s) - ret += cpp_files.map(&:to_s) unless ci_gcc_config.nil? cgc = ci_gcc_config ret = feature_args(cgc) + warning_args(cgc) + define_args(cgc) + flag_args(cgc) + ret @@ -485,16 +484,28 @@ def test_args(aux_libraries, ci_gcc_config) # # 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 test_file [Pathname] The path to the file containing the unit tests (nil to compile application as shared library) # @param aux_libraries [Array] The external Arduino libraries required by this project # @param ci_gcc_config [Hash] The GCC config object # @return [Pathname] path to the compiled test executable def build_for_test_with_configuration(test_file, aux_libraries, gcc_binary, ci_gcc_config) - base = test_file.basename - executable = Pathname.new("unittest_#{base}.bin").expand_path - File.delete(executable) if File.exist?(executable) + # hide build artifacts + build_dir = '.arduino_ci' + Dir.mkdir build_dir unless File.exist?(build_dir) + arg_sets = [] - arg_sets << ["-std=c++0x", "-o", executable.to_s, "-DARDUINO=100"] + arg_sets << ["-std=c++0x"] + if test_file.nil? + executable = Pathname.new("libarduino.so").expand_path + arg_sets << ["-shared", "-fPIC", "-Wl,-undefined,dynamic_lookup"] + else + executable = Pathname.new("#{build_dir}/unittest_#{test_file.basename}.bin").expand_path + end + File.delete(executable) if File.exist?(executable) + ENV["LD_LIBRARY_PATH"] = Dir.pwd + arg_sets << ["-o", executable.to_s, "-L" + Dir.pwd] + File.delete(executable) if File.exist?(executable) + arg_sets << ["-DARDUINO=100"] if libasan?(gcc_binary) arg_sets << [ # Stuff to help with dynamic memory mishandling "-g", "-O1", @@ -506,10 +517,21 @@ 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_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] + # the following two take some time, so are cached when we build the shared library + @full_dependencies ||= all_arduino_library_dependencies!(aux_libraries) + @test_args ||= test_args(@full_dependencies, ci_gcc_config) + arg_sets << @test_args # used cached value since building full set of include directories can take time + + if File.exist?("libarduino.so") # add the test file and the shared library + arg_sets << [test_file.to_s, "-larduino"] + else # CPP files for the shared library + arg_sets << cpp_files_arduino.map(&:to_s) # Arduino.cpp, Godmode.cpp, and stdlib.cpp + arg_sets << cpp_files_unittest.map(&:to_s) # ArduinoUnitTests.cpp + arg_sets << cpp_files.map(&:to_s) # CPP files for the primary application library under test + arg_sets << cpp_files_libraries(@full_dependencies).map(&:to_s) # CPP files for all the libraries we depend on + arg_sets << [test_file.to_s] if test_file + end + args = arg_sets.flatten(1) return nil unless run_gcc(gcc_binary, *args) diff --git a/spec/testsomething_unittests_spec.rb b/spec/testsomething_unittests_spec.rb index c882399f..bf3f9f62 100644 --- a/spec/testsomething_unittests_spec.rb +++ b/spec/testsomething_unittests_spec.rb @@ -72,7 +72,7 @@ context "file #{tfn} (using #{compiler})" do around(:example) { |example| fld.in_pristine_fake_libraries_dir(example) } - before(:all) do + before(:each) do @cpp_library = backend.install_local_library(cpp_lib_path) @exe = @cpp_library.build_for_test_with_configuration(path, [], compiler, config.gcc_config("uno")) end