diff --git a/.github/workflows/linux.yaml b/.github/workflows/linux.yaml index f8e6a5d1..054bdb0a 100644 --- a/.github/workflows/linux.yaml +++ b/.github/workflows/linux.yaml @@ -11,7 +11,7 @@ jobs: - uses: ruby/setup-ruby@v1 with: ruby-version: 2.6 - - name: Check style, funcionality, and usage + - name: Check style, functionality, and usage run: | g++ -v bundle install diff --git a/.github/workflows/macos.yaml b/.github/workflows/macos.yaml index 4e426e88..e69ded2b 100644 --- a/.github/workflows/macos.yaml +++ b/.github/workflows/macos.yaml @@ -11,7 +11,7 @@ jobs: - uses: ruby/setup-ruby@v1 with: ruby-version: 2.6 - - name: Check style, funcionality, and usage + - name: Check style, functionality, and usage run: | g++ -v bundle install diff --git a/.github/workflows/windows.yaml b/.github/workflows/windows.yaml index 2bb9d3d7..6d7e03a7 100644 --- a/.github/workflows/windows.yaml +++ b/.github/workflows/windows.yaml @@ -11,7 +11,7 @@ jobs: - uses: ruby/setup-ruby@v1 with: ruby-version: 2.6 - - name: Check style, funcionality, and usage + - name: Check style, functionality, and usage run: | g++ -v bundle install diff --git a/.rubocop.yml b/.rubocop.yml index 3d3c64f9..5f8ab4ae 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,5 +1,5 @@ AllCops: - TargetRubyVersion: 2.6 + TargetRubyVersion: 2.5 NewCops: enable SuggestExtensions: false Exclude: @@ -65,15 +65,15 @@ Layout/LineLength: # Configuration parameters: CountComments. Metrics/ClassLength: Enabled: false - Max: 400 Metrics/AbcSize: Enabled: false - Max: 50 Metrics/MethodLength: Enabled: false - Max: 50 + +Metrics/BlockLength: + Enabled: false # Configuration parameters: CountKeywordArgs. Metrics/ParameterLists: diff --git a/CHANGELOG.md b/CHANGELOG.md index 019627b9..1d8207c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,14 +7,27 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] ### Added +- Environment variable to run a custom initialization script during CI testing: `CUSTOM_INIT_SCRIPT` +- Environment variable to run from a subdirectory during CI testing: `USE_SUBDIR` +- `assertComparativeEquivalent()` and `assertComparativeNotEquivalent()` to evaluate equality on an `a - b == 0` basis (and/or `!(a > b) && !(a < b)`) +- `assertEqualFloat()` and `assertNotEqualFloat()` for comparing floats with epsilon +- `assertInfinity()` and `assertNotInfinity()` for comparing floats to infinity +- `assertNAN()` and `assertNotNAN()` for comparing floats to `NaN` +- `assertion()`, `ReporterTAP.onAssert()`, and `testBehaviorExp` macro to handle simple expression evaluation (is true, is false, etc) +- `Wire.resetMocks()` and documentation ### Changed +- Rubocop expected syntax downgraded from ruby 2.6 to 2.5 +- `assertEqual()` and `assertNotEqual()` use actual `==` and `!=` -- they no longer require a type to be totally ordered just to do equality tests +- Evaluative assertions (is true/false/null/etc) now produce simpler error messages instead of masquerading as an operation (e.g. "== true") ### Deprecated ### Removed ### Fixed +- Warnings about directory name mismatches are now based on proper comparison of strings +- Now using the recommended "stable" URL for the `esp32` board family ### Security @@ -22,7 +35,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [1.1.0] - 2020-12-02 ### Added - `ensure_arduino_installation.rb` now ensures the existence of the library directory as well -- Environment variables to escalate unit tests or examples not being found during CI testing +- Environment variables to escalate unit tests or examples not being found during CI testing - `EXPECT_EXAMPLES` and `EXPECT_UNITTESTS` ### Changed - Conserve CI testing minutes by grouping CI into fewer runs diff --git a/README.md b/README.md index 64a6bc6d..ecd7287f 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ [![Gem Version](https://badge.fury.io/rb/arduino_ci.svg)](https://rubygems.org/gems/arduino_ci) [![Documentation](http://img.shields.io/badge/docs-rdoc.info-blue.svg)](http://www.rubydoc.info/gems/arduino_ci/1.1.0) [![Gitter](https://badges.gitter.im/Arduino-CI/arduino_ci.svg)](https://gitter.im/Arduino-CI/arduino_ci?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) +[![GitHub Marketplace](https://img.shields.io/badge/Get_it-on_Marketplace-informational.svg)](https://github.com/marketplace/actions/arduino_ci) You want to run tests on your Arduino library (bonus: without hardware present), but the IDE doesn't support that. Arduino CI provides that ability. @@ -12,6 +13,8 @@ You want your Arduino library to be automatically built and tested every time so `arduino_ci` is a cross-platform build/test system, consisting of a Ruby gem and a series of C++ mocks. It enables tests to be run both locally and as part of a CI service like GitHub Actions, TravisCI, Appveyor, etc. Any OS that can run the Arduino IDE can run `arduino_ci`. +> Note: for running tests in response to [GitHub events](https://docs.github.com/en/free-pro-team@latest/developers/webhooks-and-events/github-event-types), an [Arduino CI GitHub Action](https://github.com/marketplace/actions/arduino_ci) is available for your convenience. This method of running `arduino_ci` is driven by Docker, which may also serve your local testing needs (as it does not require a ruby environment to be installed). + Platform | CI Status ---------|:--------- @@ -20,20 +23,9 @@ Linux | [![Linux Build Status](https://github.com/Arduino-CI/arduino_ci/workf Windows | [![Windows Build status](https://github.com/Arduino-CI/arduino_ci/workflows/windows/badge.svg)](https://github.com/Arduino-CI/arduino_ci/actions?workflow=windows) -## Comparison to Other Arduino Testing Tools - -| Project | CI | Builds Examples | Unittest | Arduino Mocks | Windows | OSX | Linux | License | -|-----------------------------------------------------------------------------|:--:|:---------------:|:--------:|:-------------:|:-------:|:---:|:-----:|:--------| -|[ArduinoCI](https://github.com/Arduino-CI/arduino_ci) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |Free (Apache-2.0)| -|[ArduinoUnit](https://github.com/mmurdoch/arduinounit) | ❌ | ❌ | ⚠️ Hardware-based|❌ | ✅ | ✅ | ✅ |Free (MIT)| -|[Adafruit `ci-arduino`](https://github.com/adafruit/ci-arduino)| ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ |Free (MIT)| -|[PlatformIO](https://platformio.org) | ✅ | ✅ | ⚠️ Paid only | ❌ | ✅ | ✅ | ✅ |⚠️ EULA| -|Official [Arduino IDE](https://www.arduino.cc/en/main/software) | ❌ | ⚠️ Manually | ❌ |N/A 😉| ✅ | ✅ | ✅ |Free (GPLv2)| - - ## Quick Start -For a bare-bones example that you can copy from, see [SampleProjects/DoSomething](SampleProjects/DoSomething). +For a fairly minimal practical example that you can copy from, see [the `Arduino-CI/Blink` repository](https://github.com/Arduino-CI/Blink). The complete set of C++ unit tests for the `arduino_ci` library itself are in the [SampleProjects/TestSomething](SampleProjects/TestSomething) project. The [test files](SampleProjects/TestSomething/test/) are named after the type of feature being tested. @@ -55,13 +47,20 @@ For unit testing, you will need a compiler; [g++](https://gcc.gnu.org/) is prefe * **Windows**: you will need Cygwin, and the `mingw-gcc-g++` package. A full set of (working) install instructions can be found in `appveyor.yml`, as this is how CI runs for this project. +### You _May_ Need `python` + +ESP32 and ESP8266 boards have [a dependency on `python` that they don't install themselves](https://github.com/Arduino-CI/arduino_ci/issues/235#issuecomment-739629243). If you intend to test on these platforms (which are included in the default list of platforms to test against), you will need to make `python` (and possibly `pyserial`) available in the test environment. + +Alternately, you might configure `arduino_ci` to simply not test against these. Consult the reference for those details. + + ### Changes to Your Repo Add a file called `Gemfile` (no extension) to your Arduino project: ```ruby source 'https://rubygems.org' -gem 'arduino_ci' +gem 'arduino_ci' '~> 1.1' ``` It would also make sense to add the following to your `.gitignore`, or copy [the `.gitignore` used by this project](.gitignore): diff --git a/REFERENCE.md b/REFERENCE.md index 6ae67bdc..c4f3527a 100644 --- a/REFERENCE.md +++ b/REFERENCE.md @@ -39,6 +39,16 @@ This allows a file (or glob) pattern to be executed in your tests directory, cre This allows a file (or glob) pattern to be executed in your tests directory, creating a blacklist of files to skip. E.g. `--testfile-reject=test_animal_*.cpp` would match `test_animal_cat.cpp` and `test_animal_dog.cpp` (skipping those) and test only `test_plant_rose.cpp`, `test_plant_daisy.cpp`, etc. +### `CUSTOM_INIT_SCRIPT` environment variable + +If set, testing will execute (using `/bin/sh`) the script referred to by this variable -- relative to the current working directory. This enables use cases like the GitHub action to install custom library versions (i.e. a version of a library that is different than what the library manager would automatically install by name) prior to CI test runs. + + +### `USE_SUBDIR` environment variable + +If set, testing will be conducted in this subdirectory (relative to the working directory). This is for monorepos or other layouts where the library directory and project root directory are different. + + ### `EXPECT_UNITTESTS` environment variable If set, testing will fail if no unit test files are detected (or if the directory does not exist). This is to avoid communicating a passing status in cases where a commit may have accidentally moved or deleted the test files. @@ -198,15 +208,27 @@ This test defines one `unittest` (a macro provided by `ArduinoUnitTests.h`), cal The following assertion functions are available in unit tests. -* `assertEqual(expected, actual)` -* `assertNotEqual(expected, actual)` -* `assertLess(expected, actual)` -* `assertMore(expected, actual)` -* `assertLessOrEqual(expected, actual)` -* `assertMoreOrEqual(expected, actual)` -* `assertTrue(actual)` -* `assertFalse(actual)` -* `assertNull(actual)` +```c++ +assertEqual(expected, actual); // a == b +assertNotEqual(unwanted, actual); // a != b +assertComparativeEquivalent(expected, actual); // abs(a - b) == 0 or (!(a > b) && !(a < b)) +assertComparativeNotEquivalent(unwanted, actual); // abs(a - b) > 0 or ((a > b) || (a < b)) +assertLess(upperBound, actual); // a < b +assertMore(lowerBound, actual); // a > b +assertLessOrEqual(upperBound, actual); // a <= b +assertMoreOrEqual(lowerBound, actual); // a >= b +assertTrue(actual); +assertFalse(actual); +assertNull(actual); + +// special cases for floats +assertEqualFloat(expected, actual, epsilon); // fabs(a - b) <= epsilon +assertNotEqualFloat(unwanted, actual, epsilon); // fabs(a - b) >= epsilon +assertInfinity(actual); // isinf(a) +assertNotInfinity(actual); // !isinf(a) +assertNAN(arg); // isnan(a) +assertNotNAN(arg); // !isnan(a) +``` These functions will report the result of the test to the console, and the testing will continue if they fail. @@ -327,7 +349,7 @@ unittest(pin_history) // we expect 6 values in that queue (5 that we set plus one // initial value), which we'll hard-code here for convenience. // (we'll actually assert those 6 values in the next block) - assertEqual(6, state->digitalPin[1].queueSize)); + assertEqual(6, state->digitalPin[1].queueSize()); bool expected[6] = {LOW, HIGH, LOW, LOW, HIGH, HIGH}; bool actual[6]; @@ -634,3 +656,47 @@ unittest(eeprom) assertEqual(10, a); } ``` + + +### Wire + +This library allows communication with I2C / TWI devices. + +The interface the library has been fully mocked, with the addition of several functions for debugging + +* `Wire.resetMocks()`: Initializes all mocks, and for test repeatability should be called at the top of any unit tests that use Wire. +* `Wire.didBegin()`: returns whether `Wire.begin()` was called at any point +* `Wire.getMosi(address)`: returns a pointer to a `deque` that represents the history of data sent to `address` +* `Wire.getMiso(address)`: returns a pointer to a `deque` that defines what the master will read from `address` (i.e. for you to supply) + +```c++ +unittest(wire_basics) { + // ensure known starting state + Wire.resetMocks(); + + // in case you need to check that your library is properly calling .begin() + assertFalse(Wire.didBegin()); + Wire.begin(); + assertTrue(Wire.didBegin()); + + // pick a random device. master write buffer should be empty + const uint8_t randomSlaveAddr = 14; + deque* mosi = Wire.getMosi(randomSlaveAddr); + assertEqual(0, mosi->size()); + + // write some random data to random device + const uint8_t randomData[] = { 0x07, 0x0E }; + Wire.beginTransmission(randomSlaveAddr); + Wire.write(randomData[0]); + Wire.write(randomData[1]); + Wire.endTransmission(); + + // check master write buffer values + assertEqual(2, mosi->size()); + assertEqual(randomData[0], mosi->front()); + mosi->pop_front(); + assertEqual(randomData[1], mosi->front()); + mosi->pop_front(); + assertEqual(0, mosi->size()); +} +``` diff --git a/SampleProjects/DoSomething/test/bad-errormessages.cpp b/SampleProjects/DoSomething/test/bad-errormessages.cpp new file mode 100644 index 00000000..a20ebda8 --- /dev/null +++ b/SampleProjects/DoSomething/test/bad-errormessages.cpp @@ -0,0 +1,31 @@ +#include + + +#pragma once + + + +unittest(check_that_assertion_error_messages_are_comprehensible) +{ + assertEqual(1, 2); + assertNotEqual(2, 2); + assertComparativeEquivalent(1, 2); + assertComparativeNotEquivalent(2, 2); + assertLess(2, 1); + assertMore(1, 2); + assertLessOrEqual(2, 1); + assertMoreOrEqual(1, 2); + assertTrue(false); + assertFalse(true); + assertNull(3); + assertNotNull(NULL); + + assertEqualFloat(1.2, 1.0, 0.01); + assertNotEqualFloat(1.0, 1.02, 0.1); + assertInfinity(42); + assertNotInfinity(INFINITY); + assertNAN(42); + assertNotNAN(0.0/0.0); +} + +unittest_main() diff --git a/SampleProjects/DoSomething/test/good-assert.cpp b/SampleProjects/DoSomething/test/good-assert.cpp new file mode 100644 index 00000000..2d0010f3 --- /dev/null +++ b/SampleProjects/DoSomething/test/good-assert.cpp @@ -0,0 +1,57 @@ +#include +#include "../do-something.h" + +class NonOrderedType { + public: + int x; // ehh why not + NonOrderedType(int some_x) : x(some_x) {} + + bool operator==(const NonOrderedType &that) const { + return that.x == x; + } + + bool operator!=(const NonOrderedType &that) const { + return that.x != x; + } +}; +inline std::ostream& operator << ( std::ostream& out, const NonOrderedType& n ) { + out << "NonOrderedType(" << n.x << ")"; + return out; +} + + +unittest(assert_equal_without_total_ordering) +{ + NonOrderedType a(3); + NonOrderedType b(3); + NonOrderedType c(4); + + assertEqual(a, b); + assertEqual(a, a); + assertNotEqual(a, c); + +} + +unittest(float_assertions) +{ + assertEqualFloat(1.0, 1.02, 0.1); + assertNotEqualFloat(1.2, 1.0, 0.01); + + assertInfinity(exp(800)); + assertInfinity(1.0/0.0); + assertNotInfinity(42); + + assertNAN(INFINITY - INFINITY); + assertNAN(0.0/0.0); + assertNotNAN(42); + + assertComparativeEquivalent(exp(800), INFINITY); + assertComparativeEquivalent(0.0/0.0, INFINITY - INFINITY); + assertComparativeNotEquivalent(INFINITY, -INFINITY); + + assertLess(0, INFINITY); + assertLess(-INFINITY, 0); + assertLess(-INFINITY, INFINITY); +} + +unittest_main() diff --git a/SampleProjects/TestSomething/test/godmode.cpp b/SampleProjects/TestSomething/test/godmode.cpp index 6e57d9d6..f7da6a36 100644 --- a/SampleProjects/TestSomething/test/godmode.cpp +++ b/SampleProjects/TestSomething/test/godmode.cpp @@ -146,7 +146,7 @@ unittest(analog_pin_write_history) { } unittest(ascii_pin_write_history) { - // digitial history as serial data, big-endian + // digital history as serial data, big-endian bool binaryAscii[24] = { 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, @@ -157,7 +157,7 @@ unittest(ascii_pin_write_history) { assertEqual("Yes", state->digitalPin[2].toAscii(1, true)); - // digitial history as serial data, little-endian + // digital history as serial data, little-endian bool binaryAscii2[16] = { 0, 1, 1, 1, 0, 0, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0}; diff --git a/SampleProjects/TestSomething/test/wire.cpp b/SampleProjects/TestSomething/test/wire.cpp index 720afb8c..455cb28d 100644 --- a/SampleProjects/TestSomething/test/wire.cpp +++ b/SampleProjects/TestSomething/test/wire.cpp @@ -3,14 +3,27 @@ #include using std::deque; +unittest(wire_init) { + Wire.resetMocks(); + assertFalse(Wire.didBegin()); + Wire.begin(); + assertTrue(Wire.didBegin()); + Wire.resetMocks(); + assertFalse(Wire.didBegin()); +} + unittest(begin_write_end) { + Wire.resetMocks(); + + const uint8_t randomSlaveAddr = 14; + const uint8_t randomData[] = { 0x07, 0x0E }; + // master write buffer should be empty - deque* mosi = Wire.getMosi(14); + deque* mosi = Wire.getMosi(randomSlaveAddr); assertEqual(0, mosi->size()); - + // write some random data to random slave - const uint8_t randomSlaveAddr = 14; - const uint8_t randomData[] = { 0x07, 0x0E }; + Wire.begin(); Wire.beginTransmission(randomSlaveAddr); Wire.write(randomData[0]); @@ -27,6 +40,8 @@ unittest(begin_write_end) { } unittest(readTwo_writeOne) { + Wire.resetMocks(); + Wire.begin(); deque* miso; // place some values on random slaves' read buffers diff --git a/cpp/arduino/Wire.h b/cpp/arduino/Wire.h index c77667a8..5053b8bc 100644 --- a/cpp/arduino/Wire.h +++ b/cpp/arduino/Wire.h @@ -54,19 +54,52 @@ struct wireData_t { class TwoWire : public ObservableDataStream { private: bool _didBegin = false; - wireData_t *in = nullptr; // pointer to current slave for writing - wireData_t *out = nullptr; // pointer to current slave for reading + wireData_t* in = nullptr; // pointer to current slave for writing + wireData_t* out = nullptr; // pointer to current slave for reading wireData_t slaves[SLAVE_COUNT]; public: - // constructor initializes internal data - TwoWire() { + + ////////////////////////////////////////////////////////////////////////////////////////////// + // testing methods + ////////////////////////////////////////////////////////////////////////////////////////////// + + // initialize all the mocks + void resetMocks() { + _didBegin = false; + in = nullptr; // pointer to current slave for writing + out = nullptr; // pointer to current slave for reading for (int i = 0; i < SLAVE_COUNT; ++i) { slaves[i].misoSize = 0; slaves[i].mosiSize = 0; + slaves[i].misoBuffer.clear(); + slaves[i].mosiBuffer.clear(); } } + // to verify that Wire.begin() was called at some point + bool didBegin() { return _didBegin; } + + // to access the MISO buffer, which allows you to mock what the master will read in a request + deque* getMiso(uint8_t address) { + return &slaves[address].misoBuffer; + } + + // to access the MOSI buffer, which records what the master sends during a write + deque* getMosi(uint8_t address) { + return &slaves[address].mosiBuffer; + } + + + ////////////////////////////////////////////////////////////////////////////////////////////// + // mock implementation + ////////////////////////////////////////////////////////////////////////////////////////////// + + // constructor initializes internal data + TwoWire() { + resetMocks(); + } + // https://www.arduino.cc/en/Reference/WireBegin // Initiate the Wire library and join the I2C bus as a master or slave. This // should normally be called only once. @@ -220,15 +253,6 @@ class TwoWire : public ObservableDataStream { // We don't (yet) support the slave role in the mock void onRequest(void (*callback)(void)) { assert(false); } - // testing methods - bool didBegin() { return _didBegin; } - - deque *getMiso(uint8_t address) { - return &slaves[address].misoBuffer; - } - deque *getMosi(uint8_t address) { - return &slaves[address].mosiBuffer; - } }; extern TwoWire Wire; diff --git a/cpp/unittest/ArduinoUnitTests.h b/cpp/unittest/ArduinoUnitTests.h index fbeb4394..3c34a786 100644 --- a/cpp/unittest/ArduinoUnitTests.h +++ b/cpp/unittest/ArduinoUnitTests.h @@ -69,6 +69,23 @@ class Test } } + // non-comparative assert + void onAssert( + const char* file, + int line, + const char* description, + bool pass + ) { + cerr << " " << (pass ? "" : "not ") << "ok " << ++mAssertCounter << " - " << description << endl; + if (!pass) { + cerr << " ---" << endl; + cerr << " at:" << endl; + cerr << " file: " << file << endl; + cerr << " line: " << line << endl; + cerr << " ..." << endl; + } + } + template void onAssert( const char* file, int line, @@ -194,6 +211,21 @@ class Test excise(); } + bool assertion( + const char *file, + int line, + const char *description, + bool ok) + { + if (mReporter) { + mReporter->onAssert(file, line, description, ok); + } + + if (!ok) + fail(); + return ok; + } + template bool assertion( const char *file, diff --git a/cpp/unittest/Assertion.h b/cpp/unittest/Assertion.h index f5e1b129..a05f1f48 100644 --- a/cpp/unittest/Assertion.h +++ b/cpp/unittest/Assertion.h @@ -6,9 +6,19 @@ #include "Compare.h" +#define testBehaviorExp(die, desc, pass) \ + do \ + { \ + if (!assertion(__FILE__, __LINE__, \ + desc, pass)) \ + { \ + if (die) return; \ + } \ + } while (0) + #define testBehaviorOp(die, desc, rel1, arg1, op, op_name, rel2, arg2) \ do \ - { \ + { \ if (!assertion(__FILE__, __LINE__, \ desc, \ rel1, #arg1, (arg1), \ @@ -30,26 +40,44 @@ /** macro generates optional output and calls fail() but does not return if false. */ -#define assertEqual(arg1,arg2) assertOp("assertEqual","expected",arg1,compareEqual,"==","actual",arg2) -#define assertNotEqual(arg1,arg2) assertOp("assertNotEqual","unwanted",arg1,compareNotEqual,"!=","actual",arg2) -#define assertLess(arg1,arg2) assertOp("assertLess","lowerBound",arg1,compareLess,"<","upperBound",arg2) -#define assertMore(arg1,arg2) assertOp("assertMore","upperBound",arg1,compareMore,">","lowerBound",arg2) -#define assertLessOrEqual(arg1,arg2) assertOp("assertLessOrEqual","lowerBound",arg1,compareLessOrEqual,"<=","upperBound",arg2) -#define assertMoreOrEqual(arg1,arg2) assertOp("assertMoreOrEqual","upperBound",arg1,compareMoreOrEqual,">=","lowerBound",arg2) -#define assertTrue(arg) assertEqual(true, arg) -#define assertFalse(arg) assertEqual(false, arg) -#define assertNull(arg) assertEqual((void*)NULL, (void*)arg) -#define assertNotNull(arg) assertNotEqual((void*)NULL, (void*)arg) +#define assertTrue(arg) testBehaviorExp(false, "assertTrue " #arg, (arg)) +#define assertFalse(arg) testBehaviorExp(false, "assertFalse " #arg, !(arg)) +#define assertNull(arg) testBehaviorExp(false, "assertNull " #arg, ((void*)NULL == (void*)(arg))) +#define assertNotNull(arg) testBehaviorExp(false, "assertNotNull " #arg, ((void*)NULL != (void*)(arg))) +#define assertEqual(arg1,arg2) assertOp("assertEqual","expected",arg1,compareEqual,"==","actual",arg2) +#define assertNotEqual(arg1,arg2) assertOp("assertNotEqual","unwanted",arg1,compareNotEqual,"!=","actual",arg2) +#define assertComparativeEquivalent(arg1,arg2) assertOp("assertComparativeEquivalent","expected",arg1,compareEquivalent,"!<>","actual",arg2) +#define assertComparativeNotEquivalent(arg1,arg2) assertOp("assertComparativeNotEquivalent","unwanted",arg1,compareNotEquivalent,"<>","actual",arg2) +#define assertLess(arg1,arg2) assertOp("assertLess","lowerBound",arg1,compareLess,"<","actual",arg2) +#define assertMore(arg1,arg2) assertOp("assertMore","upperBound",arg1,compareMore,">","actual",arg2) +#define assertLessOrEqual(arg1,arg2) assertOp("assertLessOrEqual","lowerBound",arg1,compareLessOrEqual,"<=","actual",arg2) +#define assertMoreOrEqual(arg1,arg2) assertOp("assertMoreOrEqual","upperBound",arg1,compareMoreOrEqual,">=","actual",arg2) + +#define assertEqualFloat(arg1, arg2, arg3) assertOp("assertEqualFloat", "epsilon", arg3, compareMoreOrEqual, ">=", "actualDifference", fabs(arg1 - arg2)) +#define assertNotEqualFloat(arg1, arg2, arg3) assertOp("assertNotEqualFloat", "epsilon", arg3, compareLessOrEqual, "<=", "insufficientDifference", fabs(arg1 - arg2)) +#define assertInfinity(arg) testBehaviorExp(false, "assertInfinity " #arg, isinf(arg)) +#define assertNotInfinity(arg) testBehaviorExp(false, "assertNotInfinity " #arg, !isinf(arg)) +#define assertNAN(arg) testBehaviorExp(false, "assertNAN " #arg, isnan(arg)) +#define assertNotNAN(arg) testBehaviorExp(false, "assertNotNAN " #arg, !isnan(arg)) + /** macro generates optional output and calls fail() followed by a return if false. */ -#define assureEqual(arg1,arg2) assureOp("assureEqual","expected",arg1,compareEqual,"==","actual",arg2) -#define assureNotEqual(arg1,arg2) assureOp("assureNotEqual","unwanted",arg1,compareNotEqual,"!=","actual",arg2) -#define assureLess(arg1,arg2) assureOp("assureLess","lowerBound",arg1,compareLess,"<","upperBound",arg2) -#define assureMore(arg1,arg2) assureOp("assureMore","upperBound",arg1,compareMore,">","lowerBound",arg2) -#define assureLessOrEqual(arg1,arg2) assureOp("assureLessOrEqual","lowerBound",arg1,compareLessOrEqual,"<=","upperBound",arg2) -#define assureMoreOrEqual(arg1,arg2) assureOp("assureMoreOrEqual","upperBound",arg1,compareMoreOrEqual,">=","lowerBound",arg2) -#define assureTrue(arg) assureEqual(true, arg) -#define assureFalse(arg) assureEqual(false, arg) -#define assureNull(arg) assureEqual((void*)NULL, (void*)arg) -#define assureNotNull(arg) assureNotEqual((void*)NULL, (void*)arg) +#define assureTrue(arg) testBehaviorExp(true, "assertTrue " #arg, (arg)) +#define assureFalse(arg) testBehaviorExp(true, "assertFalse " #arg, !(arg)) +#define assureNull(arg) testBehaviorExp(true, "assertNull " #arg, ((void*)NULL == (void*)(arg))) +#define assureNotNull(arg) testBehaviorExp(true, "assertNotNull " #arg, ((void*)NULL != (void*)(arg))) +#define assureEqual(arg1,arg2) assureOp("assureEqual","expected",arg1,compareEqual,"==","actual",arg2) +#define assureNotEqual(arg1,arg2) assureOp("assureNotEqual","unwanted",arg1,compareNotEqual,"!=","actual",arg2) +#define assureComparativeEquivalent(arg1,arg2) assertOp("assureComparativeEquivalent","expected",arg1,compareEquivalent,"!<>","actual",arg2) +#define assureComparativeNotEquivalent(arg1,arg2) assertOp("assureComparativeNotEquivalent","unwanted",arg1,compareNotEquivalent,"<>","actual",arg2) +#define assureLess(arg1,arg2) assureOp("assureLess","lowerBound",arg1,compareLess,"<","actual",arg2) +#define assureMore(arg1,arg2) assureOp("assureMore","upperBound",arg1,compareMore,">","actual",arg2) +#define assureLessOrEqual(arg1,arg2) assureOp("assureLessOrEqual","lowerBound",arg1,compareLessOrEqual,"<=","actual",arg2) +#define assureMoreOrEqual(arg1,arg2) assureOp("assureMoreOrEqual","upperBound",arg1,compareMoreOrEqual,">=","actual",arg2) +#define assureEqualFloat(arg1, arg2, arg3) assureOp("assureEqualFloat", "epsilon", arg3, compareMoreOrEqual, ">=", "actualDifference", fabs(arg1 - arg2)) +#define assureNotEqualFloat(arg1, arg2, arg3) assureOp("assureNotEqualFloat", "epsilon", arg3, compareLessOrEqual, "<=", "insufficientDifference", fabs(arg1 - arg2)) +#define assureInfinity(arg) testBehaviorExp(true, "assertInfinity " #arg, isinf(arg)) +#define assureNotInfinity(arg) testBehaviorExp(true, "assertNotInfinity " #arg, !isinf(arg)) +#define assureNAN(arg) testBehaviorExp(true, "assertNAN " #arg, isnan(arg)) +#define assureNotNAN(arg) testBehaviorExp(true, "assertNotNAN " #arg, !isnan(arg)) diff --git a/cpp/unittest/Compare.h b/cpp/unittest/Compare.h index 882b9c83..c355b1db 100644 --- a/cpp/unittest/Compare.h +++ b/cpp/unittest/Compare.h @@ -10,12 +10,14 @@ template < typename A, typename B > struct Compare if (b struct Compare; \ - template < __VA_ARGS__ > struct Compare \ - { \ - inline static int between( T1 const (&a)T1m, T2 const (&b)T2m) { return betweenImpl; } \ - inline static bool equal( T1 const (&a)T1m, T2 const (&b)T2m) { return between(a, b) == 0; } \ - inline static bool notEqual( T1 const (&a)T1m, T2 const (&b)T2m) { return between(a, b) != 0; } \ - inline static bool less( T1 const (&a)T1m, T2 const (&b)T2m) { return between(a, b) < 0; } \ - inline static bool more( T1 const (&a)T1m, T2 const (&b)T2m) { return between(a, b) > 0; } \ - inline static bool lessOrEqual(T1 const (&a)T1m, T2 const (&b)T2m) { return between(a, b) <= 0; } \ - inline static bool moreOrEqual(T1 const (&a)T1m, T2 const (&b)T2m) { return between(a, b) >= 0; } \ +#define eqComparisonTemplateMacro(T1, T1m, T2, T2m, betweenImpl, ...) \ + template < __VA_ARGS__ > struct Compare; \ + template < __VA_ARGS__ > struct Compare \ + { \ + inline static int between( T1 const (&a)T1m, T2 const (&b)T2m) { return betweenImpl; } \ + inline static bool equal( T1 const (&a)T1m, T2 const (&b)T2m) { return between(a, b) == 0; } \ + inline static bool notEqual( T1 const (&a)T1m, T2 const (&b)T2m) { return between(a, b) != 0; } \ + inline static bool equivalent( T1 const (&a)T1m, T2 const (&b)T2m) { return between(a, b) == 0; } \ + inline static bool notEquivalent(T1 const (&a)T1m, T2 const (&b)T2m) { return between(a, b) != 0; } \ + inline static bool less( T1 const (&a)T1m, T2 const (&b)T2m) { return between(a, b) < 0; } \ + inline static bool more( T1 const (&a)T1m, T2 const (&b)T2m) { return between(a, b) > 0; } \ + inline static bool lessOrEqual( T1 const (&a)T1m, T2 const (&b)T2m) { return between(a, b) <= 0; } \ + inline static bool moreOrEqual( T1 const (&a)T1m, T2 const (&b)T2m) { return between(a, b) >= 0; } \ }; -comparisonTemplateMacro(String, , String, , a.compareTo(b)) -comparisonTemplateMacro(String, , const char *, , a.compareTo(b)) +eqComparisonTemplateMacro(String, , String, , a.compareTo(b)) +eqComparisonTemplateMacro(String, , const char *, , a.compareTo(b)) #if defined(F) -comparisonTemplateMacro(String, , const __FlashStringHelper *, , arduinoCICompareBetween(a, b)) -comparisonTemplateMacro(const char *,, const __FlashStringHelper *, , strcmp_P(a,(const char *)b)) -comparisonTemplateMacro(const __FlashStringHelper *, , String, , -arduinoCICompareBetween(b, a)) -comparisonTemplateMacro(const __FlashStringHelper *, , const char *, , -strcmp_P(b,(const char *)a)) -comparisonTemplateMacro(const __FlashStringHelper *, , const __FlashStringHelper *, , arduinoCICompareBetween(a, b)) -comparisonTemplateMacro(const __FlashStringHelper *, , char *, , -strcmp_P(b,(const char *)a)) -comparisonTemplateMacro(char *, , const __FlashStringHelper *, , strcmp_P(a,(const char *)b)) -comparisonTemplateMacro(const __FlashStringHelper *, , char, [M], -strcmp_P(b,(const char *)a), size_t M) -comparisonTemplateMacro(char, [N], const __FlashStringHelper *, , strcmp_P(a,(const char *)b), size_t N) +eqComparisonTemplateMacro(String, , const __FlashStringHelper *, , arduinoCICompareBetween(a, b)) +eqComparisonTemplateMacro(const char *,, const __FlashStringHelper *, , strcmp_P(a,(const char *)b)) +eqComparisonTemplateMacro(const __FlashStringHelper *, , String, , -arduinoCICompareBetween(b, a)) +eqComparisonTemplateMacro(const __FlashStringHelper *, , const char *, , -strcmp_P(b,(const char *)a)) +eqComparisonTemplateMacro(const __FlashStringHelper *, , const __FlashStringHelper *, , arduinoCICompareBetween(a, b)) +eqComparisonTemplateMacro(const __FlashStringHelper *, , char *, , -strcmp_P(b,(const char *)a)) +eqComparisonTemplateMacro(char *, , const __FlashStringHelper *, , strcmp_P(a,(const char *)b)) +eqComparisonTemplateMacro(const __FlashStringHelper *, , char, [M], -strcmp_P(b,(const char *)a), size_t M) +eqComparisonTemplateMacro(char, [N], const __FlashStringHelper *, , strcmp_P(a,(const char *)b), size_t N) #endif -comparisonTemplateMacro(String, , char *, , a.compareTo(b)) -comparisonTemplateMacro(const char *, , String, , -b.compareTo(a)) -comparisonTemplateMacro(const char *, , const char *, , strcmp(a,b)) -comparisonTemplateMacro(const char *, , char *, , strcmp(a,b)) -comparisonTemplateMacro(char *, , String, , -b.compareTo(a)) -comparisonTemplateMacro(char *, , const char *, , strcmp(a,b)) -comparisonTemplateMacro(char *, , char *, , strcmp(a,b)) -comparisonTemplateMacro(String, , char, [M], a.compareTo(b), size_t M) -comparisonTemplateMacro(const char *, , char, [M], strcmp(a,b), size_t M) -comparisonTemplateMacro(char *, , char, [M], strcmp(a,b), size_t M) -comparisonTemplateMacro(char, [N], String, , -b.compareTo(a), size_t N) -comparisonTemplateMacro(char, [N], const char *, , strcmp(a,b), size_t N) -comparisonTemplateMacro(char, [N], char *, , strcmp(a,b), size_t N) -comparisonTemplateMacro(char, [N], char, [M], strcmp(a,b), size_t N, size_t M) +eqComparisonTemplateMacro(String, , char *, , a.compareTo(b)) +eqComparisonTemplateMacro(const char *, , String, , -b.compareTo(a)) +eqComparisonTemplateMacro(const char *, , const char *, , strcmp(a,b)) +eqComparisonTemplateMacro(const char *, , char *, , strcmp(a,b)) +eqComparisonTemplateMacro(char *, , String, , -b.compareTo(a)) +eqComparisonTemplateMacro(char *, , const char *, , strcmp(a,b)) +eqComparisonTemplateMacro(char *, , char *, , strcmp(a,b)) +eqComparisonTemplateMacro(String, , char, [M], a.compareTo(b), size_t M) +eqComparisonTemplateMacro(const char *, , char, [M], strcmp(a,b), size_t M) +eqComparisonTemplateMacro(char *, , char, [M], strcmp(a,b), size_t M) +eqComparisonTemplateMacro(char, [N], String, , -b.compareTo(a), size_t N) +eqComparisonTemplateMacro(char, [N], const char *, , strcmp(a,b), size_t N) +eqComparisonTemplateMacro(char, [N], char *, , strcmp(a,b), size_t N) +eqComparisonTemplateMacro(char, [N], char, [M], strcmp(a,b), size_t N, size_t M) -comparisonTemplateMacro(A, , std::nullptr_t, , a ? 1 : 0, typename A) -comparisonTemplateMacro(std::nullptr_t, , B, , b ? -1 : 0, typename B) +eqComparisonTemplateMacro(A, , std::nullptr_t, , a ? 1 : 0, typename A) +eqComparisonTemplateMacro(std::nullptr_t, , B, , b ? -1 : 0, typename B) // super general comparisons -template int compareBetween( const A &a, const B &b) { return Compare::between( a, b); } -template bool compareEqual( const A &a, const B &b) { return Compare::equal( a, b); } -template bool compareNotEqual( const A &a, const B &b) { return Compare::notEqual( a, b); } -template bool compareLess( const A &a, const B &b) { return Compare::less( a, b); } -template bool compareMore( const A &a, const B &b) { return Compare::more( a, b); } -template bool compareLessOrEqual(const A &a, const B &b) { return Compare::lessOrEqual(a, b); } -template bool compareMoreOrEqual(const A &a, const B &b) { return Compare::moreOrEqual(a, b); } +template int compareBetween( const A &a, const B &b) { return Compare::between( a, b); } +template bool compareEqual( const A &a, const B &b) { return Compare::equal( a, b); } +template bool compareNotEqual( const A &a, const B &b) { return Compare::notEqual( a, b); } +template bool compareEquivalent( const A &a, const B &b) { return Compare::equivalent( a, b); } +template bool compareNotEquivalent(const A &a, const B &b) { return Compare::notEquivalent(a, b); } +template bool compareLess( const A &a, const B &b) { return Compare::less( a, b); } +template bool compareMore( const A &a, const B &b) { return Compare::more( a, b); } +template bool compareLessOrEqual( const A &a, const B &b) { return Compare::lessOrEqual( a, b); } +template bool compareMoreOrEqual( const A &a, const B &b) { return Compare::moreOrEqual( a, b); } diff --git a/exe/arduino_ci.rb b/exe/arduino_ci.rb index a1142cd8..9ecaecdc 100755 --- a/exe/arduino_ci.rb +++ b/exe/arduino_ci.rb @@ -5,8 +5,10 @@ require 'optparse' WIDTH = 80 -VAR_EXPECT_EXAMPLES = "EXPECT_EXAMPLES".freeze -VAR_EXPECT_UNITTESTS = "EXPECT_UNITTESTS".freeze +VAR_CUSTOM_INIT_SCRIPT = "CUSTOM_INIT_SCRIPT".freeze +VAR_USE_SUBDIR = "USE_SUBDIR".freeze +VAR_EXPECT_EXAMPLES = "EXPECT_EXAMPLES".freeze +VAR_EXPECT_UNITTESTS = "EXPECT_UNITTESTS".freeze @failure_count = 0 @passfail = proc { |result| result ? "✓" : "✗" } @@ -51,6 +53,9 @@ def self.parse(options) puts opts puts puts "Additionally, the following environment variables control the script:" + puts " - #{VAR_CUSTOM_INIT_SCRIPT} - if set, this script will be run from the Arduino/libraries directory" + puts " prior to any automated library installation or testing (e.g. to install unoffical libraries)" + puts " - #{VAR_USE_SUBDIR} - if set, the script will install the library from this subdirectory of the cwd" puts " - #{VAR_EXPECT_EXAMPLES} - if set, testing will fail if no example sketches are present" puts " - #{VAR_EXPECT_UNITTESTS} - if set, testing will fail if no unit tests are present" exit @@ -68,7 +73,7 @@ def self.parse(options) # terminate after printing any debug info. TODO: capture debug info def terminate(final = nil) puts "Failures: #{@failure_count}" - unless @failure_count.zero? || final + unless @failure_count.zero? || final || @backend.nil? puts "Last message: #{@backend.last_msg}" puts "========== Stdout:" puts @backend.last_out @@ -277,6 +282,30 @@ def get_annotated_compilers(config, cpp_library) compilers end +# Handle existence or nonexistence of custom initialization script -- run it if you have it +# +# This feature is to drive GitHub actions / docker image installation where the container is +# in a clean-slate state but needs some way to have custom library versions injected into it. +# In this case, the user provided script would fetch a git repo or some other method +def perform_custom_initialization(_config) + script_path = ENV[VAR_CUSTOM_INIT_SCRIPT] + inform("Environment variable #{VAR_CUSTOM_INIT_SCRIPT}") { "'#{script_path}'" } + return if script_path.nil? + return if script_path.empty? + + script_pathname = Pathname.getwd + script_path + assure("Script at #{VAR_CUSTOM_INIT_SCRIPT} exists") { script_pathname.exist? } + + assure_multiline("Running #{script_pathname} with sh in libraries working dir") do + Dir.chdir(@backend.lib_dir) do + IO.popen(["/bin/sh", script_pathname.to_s], err: [:child, :out]) do |io| + io.each_line { |line| puts " #{line}" } + end + end + end +end + +# Unit test procedure def perform_unit_tests(cpp_library, file_config) if @cli_options[:skip_unittests] inform("Skipping unit tests") { "as requested via command line" } @@ -390,18 +419,22 @@ def perform_example_compilation_tests(cpp_library, config) # initialize command and config config = ArduinoCI::CIConfig.default.from_project_library - @backend = ArduinoCI::ArduinoInstallation.autolocate! inform("Located arduino-cli binary") { @backend.binary_path.to_s } +# run any library init scripts from the library itself. +perform_custom_initialization(config) + # initialize library under test -cpp_library_path = Pathname.new(".") +inform("Environment variable #{VAR_USE_SUBDIR}") { "'#{ENV[VAR_USE_SUBDIR]}'" } +cpp_library_path = Pathname.new(ENV[VAR_USE_SUBDIR].nil? ? "." : ENV[VAR_USE_SUBDIR]) cpp_library = assure("Installing library under test") do @backend.install_local_library(cpp_library_path) end +# Warn if the library name isn't obvious assumed_name = @backend.name_of_library(cpp_library_path) -ondisk_name = cpp_library_path.realpath.basename +ondisk_name = cpp_library_path.realpath.basename.to_s if assumed_name != ondisk_name inform("WARNING") { "Installed library named '#{assumed_name}' has directory name '#{ondisk_name}'" } end diff --git a/lib/arduino_ci/arduino_backend.rb b/lib/arduino_ci/arduino_backend.rb index cda28752..42090d38 100644 --- a/lib/arduino_ci/arduino_backend.rb +++ b/lib/arduino_ci/arduino_backend.rb @@ -52,7 +52,7 @@ def _wrap_run(work_fn, *args, **kwargs) # do some work to extract & merge environment variables if they exist has_env = !args.empty? && args[0].instance_of?(Hash) env_vars = has_env ? args[0] : {} - actual_args = has_env ? args[1..] : args # need to shift over if we extracted args + actual_args = has_env ? args[1..-1] : args # need to shift over if we extracted 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 diff --git a/lib/arduino_ci/host.rb b/lib/arduino_ci/host.rb index 2cf82c59..dc8e2911 100644 --- a/lib/arduino_ci/host.rb +++ b/lib/arduino_ci/host.rb @@ -103,7 +103,7 @@ def self.readlink(path) the_file = path.basename.to_s stdout, _stderr, _exitstatus = Open3.capture3('cmd.exe', "/c dir /al #{the_dir}") - symlinks = stdout.lines.map { |l| DIR_SYMLINK_REGEX.match(l) }.compact + symlinks = stdout.lines.map { |l| DIR_SYMLINK_REGEX.match(l.scrub) }.compact our_link = symlinks.find { |m| m[1] == the_file } return nil if our_link.nil? diff --git a/misc/default.yml b/misc/default.yml index 4d621a79..4d08ab9b 100644 --- a/misc/default.yml +++ b/misc/default.yml @@ -16,7 +16,7 @@ packages: adafruit:samd: url: https://adafruit.github.io/arduino-board-index/package_adafruit_index.json esp32:esp32: - url: https://dl.espressif.com/dl/package_esp32_index.json + url: https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json platforms: diff --git a/spec/ci_config_spec.rb b/spec/ci_config_spec.rb index a0d53bde..bab8293c 100644 --- a/spec/ci_config_spec.rb +++ b/spec/ci_config_spec.rb @@ -29,7 +29,7 @@ expect(default_config.package_url("adafruit:avr")).to eq("https://adafruit.github.io/arduino-board-index/package_adafruit_index.json") expect(default_config.package_url("adafruit:samd")).to eq("https://adafruit.github.io/arduino-board-index/package_adafruit_index.json") - expect(default_config.package_url("esp32:esp32")).to eq("https://dl.espressif.com/dl/package_esp32_index.json") + expect(default_config.package_url("esp32:esp32")).to eq("https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json") expect(default_config.platforms_to_build).to match(["uno", "due", "zero", "leonardo", "m4", "esp32", "esp8266", "mega2560"]) expect(default_config.platforms_to_unittest).to match(["uno", "due", "zero", "leonardo"]) expect(default_config.aux_libraries_for_build).to match([]) diff --git a/spec/cpp_library_spec.rb b/spec/cpp_library_spec.rb index 3e4a4a95..050e0555 100644 --- a/spec/cpp_library_spec.rb +++ b/spec/cpp_library_spec.rb @@ -85,9 +85,11 @@ def verified_install(backend, path) header_dirs: [Pathname.new("DoSomething")], arduino_library_src_dirs: [], test_files: [ - "DoSomething/test/good-null.cpp", - "DoSomething/test/good-library.cpp", + "DoSomething/test/bad-errormessages.cpp", "DoSomething/test/bad-null.cpp", + "DoSomething/test/good-assert.cpp", + "DoSomething/test/good-library.cpp", + "DoSomething/test/good-null.cpp", ].map { |f| Pathname.new(f) } }, OnePointOhDummy: {