diff --git a/.github/workflows/README.md b/.github/workflows/README.md new file mode 100644 index 00000000..7e1b5195 --- /dev/null +++ b/.github/workflows/README.md @@ -0,0 +1,10 @@ +## Note to `arduino_ci` users + +In this project, we define a workflow for each target platform. **If you're looking for an example you can copy from, take only `linux.yaml`.** + + +### Long version + +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. diff --git a/.rubocop.yml b/.rubocop.yml index 5f8ab4ae..205a48fc 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -45,6 +45,10 @@ Layout/EmptyLinesAroundMethodBody: Layout/EmptyLinesAroundModuleBody: Enabled: false +# This can add clarity +Style/CommentedKeyword: + Enabled: false + # Configuration parameters: AllowForAlignment. Layout/ExtraSpacing: Enabled: false diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d8207c6..5629bff7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,11 +15,18 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - `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 +- `shiftIn()` and `shiftOut()` +- `CIConfig.is_default` to detect when the default configuration is used +- `ArduinoBackend.boards_installed?` to detect whether a board family (or package, like `arduino:avr`) is installed +- `ArduinoBackend.library_available?` to detect whether the library manager knows of a library +- Sanity checks for `library.properties` `includes=` and `depends=` entries ### 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") +- `LibraryProperties.to_h` now properly uses formatters and symbolic keys, in order to support a `.to_s` +- Architectures from `library.properties` are considered when iterating over unit test or examples compilation, as well as the configured platforms ### Deprecated @@ -28,6 +35,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### 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 +- `esp8266:huzzah` options updated as per upstream +- Errors about `'_NOP' was not declared in this scope` (test added) +- `pinMode()` and `analogReference()` are now functions (no longer macros), because that conflicted with actual function names in the wild +- `analogReadResolution()` and `analogWriteResolution()` are also no longer macros ### Security diff --git a/REFERENCE.md b/REFERENCE.md index c4f3527a..506f3d63 100644 --- a/REFERENCE.md +++ b/REFERENCE.md @@ -29,6 +29,11 @@ This completely skips the compilation tests (of library examples) portion of the This completely skips the compilation tests (of library examples) portion of the CI script. It does not skip the compilation of unit tests. +### `--skip-library-properties` option + +This completely skips validation of entries in `library.properties`. + + ### `--testfile-select` option This allows a file (or glob) pattern to be executed in your tests directory, creating a whitelist of files to test. E.g. `--testfile-select=test_animal_*.cpp` would match `test_animal_cat.cpp` and `test_animal_dog.cpp` (testing only those) and not `test_plant_rose.cpp`. @@ -59,6 +64,11 @@ If set, testing will fail if no unit test files are detected (or if the director If set, testing will fail if no example sketches are detected. This is to avoid communicating a passing status in cases where a commit may have accidentally moved or deleted the examples. +### `SKIP_LIBRARY_PROPERTIES` environment variable + +If set, testing will skip validating `library.properties` entries. This is to work around any possible bugs in `arduino_ci`'s interpretation of what is "correct". + + ## Indirectly Overriding Build Behavior (medium term use), and Advanced Options For build behavior that you'd like to persist across commits (e.g. defining the set of platforms to test against, disabling a test that you expect to re-enable at some future point), a special configuration file called `.arduino-ci.yml` can be used. There are 3 places you can put them: @@ -84,6 +94,8 @@ packages: To define a platform called `bogo` that uses a board called `potato:salad:bogo` (based on the `potato:salad` family), set it up in the `plaforms:` section. Note that this will override any default configuration of `bogo` if it had existed in `arduino_ci`'s `misc/default.yml` file. If this board defines particular features in the compiler, you can set those here. +> Note that the platform names are arbitrary -- just keys in this yaml file and in the [`default.yml`](https://github.com/Arduino-CI/arduino_ci/blob/master/misc/default.yml) file included in this gem. That said, they are also case sensitive; defining the `bogo` platform will not let you refer to it as `Bogo` nor `BOGO`. + ```yaml platforms: # our custom definition of the "bogo" platform @@ -117,7 +129,9 @@ 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. +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. This works by _overriding_ portions of the default configuration. + +> Note that the platform names _must_ match (case-sensitive) the platform names in the underlying [`default.yml`](https://github.com/Arduino-CI/arduino_ci/blob/master/misc/default.yml), or else match platforms that you have defined yourself in your `.arduino-ci.yml` override. ```yaml compile: diff --git a/SampleProjects/TestSomething/test/godmode.cpp b/SampleProjects/TestSomething/test/godmode.cpp index f7da6a36..024b9915 100644 --- a/SampleProjects/TestSomething/test/godmode.cpp +++ b/SampleProjects/TestSomething/test/godmode.cpp @@ -222,6 +222,72 @@ unittest(spi) { assertEqual("LMNOe", String(inBuf)); } +unittest(shift_in) { + + uint8_t dataPin = 2; + uint8_t clockPin = 3; + uint8_t input; + bool actualClock[16]; + uint8_t originalSize; + + // verify data + state->reset(); + state->digitalPin[dataPin].fromAscii("|", true); // 0111 1100 + originalSize = state->digitalPin[clockPin].historySize(); + + input = shiftIn(dataPin, clockPin, MSBFIRST); + assertEqual(0x7C, (uint)input); // 0111 1100 + assertEqual('|', input); // 0111 1100 + assertEqual((uint)'|', (uint)input); // 0111 1100 + + // now verify clock + assertEqual(16, state->digitalPin[clockPin].historySize() - originalSize); + int numMoved = state->digitalPin[clockPin].toArray(actualClock, 16); + assertEqual(16, numMoved); + for (int i = 0; i < 16; ++i) assertEqual(i % 2, actualClock[i]); + + state->reset(); + state->digitalPin[dataPin].fromAscii("|", true); // 0111 1100 + input = shiftIn(dataPin, clockPin, LSBFIRST); // <- note the LSB/MSB flip + assertEqual(0x3E, (uint)input); // 0011 1110 + assertEqual('>', input); // 0011 1110 + assertEqual((uint)'>', (uint)input); // 0011 1110 + + // test setting MSB + state->reset(); + state->digitalPin[dataPin].fromAscii("U", true); // 0101 0101 + input = shiftIn(dataPin, clockPin, LSBFIRST); // <- note the LSB/MSB flip + assertEqual(0xAA, (uint)input); // 1010 1010 +} + +unittest(shift_out) { + + uint8_t dataPin = 2; + uint8_t clockPin = 3; + uint8_t output; + bool actualClock[16]; + uint8_t originalSize; + + state->reset(); + originalSize = state->digitalPin[clockPin].historySize(); + shiftOut(dataPin, clockPin, MSBFIRST, '|'); + assertEqual("|", state->digitalPin[dataPin].toAscii(1, true)); + assertEqual(16, state->digitalPin[clockPin].historySize() - originalSize); + int numMoved = state->digitalPin[clockPin].toArray(actualClock, 16); + for (int i = 0; i < 16; ++i) assertEqual(i % 2, actualClock[i]); + + state->reset(); + shiftOut(dataPin, clockPin, LSBFIRST, '|'); + assertEqual(">", state->digitalPin[dataPin].toAscii(1, true)); + +} + +unittest(no_ops) { + pinMode(1, INPUT); + analogReference(3); + analogReadResolution(4); + analogWriteResolution(5); +} #ifdef HAVE_HWSERIAL0 diff --git a/cpp/arduino/Arduino.h b/cpp/arduino/Arduino.h index 4d00095b..ad7d5a99 100644 --- a/cpp/arduino/Arduino.h +++ b/cpp/arduino/Arduino.h @@ -36,9 +36,6 @@ typedef uint8_t byte; #define highByte(w) ((uint8_t) ((w) >> 8)) #define lowByte(w) ((uint8_t) ((w) & 0xff)) -// Arduino defines this -#define _NOP() do { 0; } while (0) - // might as well use that NO-op macro for these, while unit testing // you need interrupts? interrupt yourself #define yield() _NOP() @@ -70,5 +67,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/cpp/arduino/ArduinoDefines.h b/cpp/arduino/ArduinoDefines.h index f34aca6d..9490d469 100644 --- a/cpp/arduino/ArduinoDefines.h +++ b/cpp/arduino/ArduinoDefines.h @@ -92,3 +92,6 @@ #if defined(__AVR_ATmega328P__) || defined(__AVR_ATmega32U4__) || defined(__AVR_ATmega328__) || defined(__AVR_ATmega168__) || defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) #define LED_BUILTIN 13 #endif + +// Arduino defines this +#define _NOP() do { 0; } while (0) diff --git a/cpp/arduino/Godmode.h b/cpp/arduino/Godmode.h index 642cac71..ee332025 100644 --- a/cpp/arduino/Godmode.h +++ b/cpp/arduino/Godmode.h @@ -160,21 +160,78 @@ class GodmodeState { }; // io pins -#define pinMode(...) _NOP() -#define analogReference(...) _NOP() +inline void pinMode(uint8_t pin, uint8_t mode) { _NOP(); } +inline void analogReference(uint8_t mode) { _NOP(); } void digitalWrite(uint8_t, uint8_t); int digitalRead(uint8_t); int analogRead(uint8_t); void analogWrite(uint8_t, int); -#define analogReadResolution(...) _NOP() -#define analogWriteResolution(...) _NOP() +inline void analogReadResolution(uint8_t bits) { _NOP(); } +inline void analogWriteResolution(uint8_t bits) { _NOP(); } void attachInterrupt(uint8_t interrupt, void ISR(void), uint8_t mode); void detachInterrupt(uint8_t interrupt); // TODO: issue #26 to track the commanded state here -inline void tone(uint8_t _pin, unsigned int frequency, unsigned long duration = 0) {} -inline void noTone(uint8_t _pin) {} +inline void tone(uint8_t _pin, unsigned int frequency, unsigned long duration = 0) { throw "Not Yet Implemented"; } +inline void noTone(uint8_t _pin) { throw "Not Yet Implemented"; } +inline uint8_t pulseIn(uint8_t _pin, uint8_t _value, uint32_t _timeout) { throw "Not Yet Implemented"; } +inline uint8_t pulseIn(uint8_t pin, uint8_t value) { return pulseIn(pin, value, (uint32_t) 1000000); } +inline uint32_t pulseInLong(uint8_t _pin, uint8_t _value, uint32_t _timeout) { throw "Not Yet Implemented"; } +inline uint32_t pulseInLong(uint8_t pin, uint8_t value) { return pulseInLong(pin, value, (uint32_t) 1000000); } + +/** + * Shifts in a byte of data one bit at a time. + * + * Starts from either the most (i.e. the leftmost) or least (rightmost) + * significant bit. For each bit, the clock pin is pulled high, the next bit is + * read from the data line, and then the clock pin is taken low. + * + * @param dataPin the pin on which to input each bit + * @param clockPin the pin to toggle to signal a read from dataPin + * @param bitOrder which order to shift in the bits; either MSBFIRST or LSBFIRST. B=Bit, not byte + * + * @return The value read + */ +inline uint8_t shiftIn(uint8_t dataPin, uint8_t clockPin, bool bitOrder) { + bool mFirst = bitOrder == MSBFIRST; + uint8_t ret = 0x00; + for (uint8_t i = 0, mask = (bitOrder == MSBFIRST ? 0x80 : 0x01); i < 8; ++i) { + digitalWrite(clockPin, HIGH); + uint8_t setBit = mFirst ? 0x80 : 0x01; + uint8_t val = (mFirst ? (setBit >> i) : (setBit << i)); + ret = ret | (digitalRead(dataPin) ? val : 0x00); + digitalWrite(clockPin, LOW); + } + return ret; +} + +/** + * Shifts out a byte of data one bit at a time. + * + * Starts from either the most (i.e. the leftmost) or least (rightmost) + * significant bit. Each bit is written in turn to a data pin, after which a + * clock pin is pulsed (taken high, then low) to indicate that the bit is + * available. + * + * @param dataPin the pin on which to input each bit + * @param clockPin the pin to toggle to signal a write from dataPin + * @param bitOrder which order to shift in the bits; either MSBFIRST or LSBFIRST. B=Bit, not byte + * @param value the data to shift out + * + * @return The value read + */ +inline void shiftOut(uint8_t dataPin, uint8_t clockPin, bool bitOrder, uint8_t value) { + bool mFirst = bitOrder == MSBFIRST; + uint8_t ret = 0x00; + for (uint8_t i = 0, mask = (bitOrder == MSBFIRST ? 0x80 : 0x01); i < 8; ++i) { + uint8_t setBit = mFirst ? 0x80 : 0x01; + uint8_t val = (mFirst ? (setBit >> i) : (setBit << i)); + digitalWrite(dataPin, (value & val) ? HIGH : LOW); + digitalWrite(clockPin, HIGH); + digitalWrite(clockPin, LOW); + } +} // These definitions allow the following to compile (see issue #193): // https://github.com/arduino-libraries/Ethernet/blob/master/src/utility/w5100.h:341 diff --git a/cpp/unittest/Assertion.h b/cpp/unittest/Assertion.h index a05f1f48..58dfb621 100644 --- a/cpp/unittest/Assertion.h +++ b/cpp/unittest/Assertion.h @@ -6,7 +6,7 @@ #include "Compare.h" -#define testBehaviorExp(die, desc, pass) \ +#define arduinoCITestBehaviorExp(die, desc, pass) \ do \ { \ if (!assertion(__FILE__, __LINE__, \ @@ -16,7 +16,7 @@ } \ } while (0) -#define testBehaviorOp(die, desc, rel1, arg1, op, op_name, rel2, arg2) \ +#define arduinoCITestBehaviorOp(die, desc, rel1, arg1, op, op_name, rel2, arg2) \ do \ { \ if (!assertion(__FILE__, __LINE__, \ @@ -32,52 +32,52 @@ // helper define for the operators below -#define assertOp(desc, rel1, arg1, op, op_name, rel2, arg2) \ - testBehaviorOp(false, desc, rel1, arg1, op, op_name, rel2, arg2) +#define arduinoCIAssertOp(desc, rel1, arg1, op, op_name, rel2, arg2) \ + arduinoCITestBehaviorOp(false, "assert" desc, rel1, arg1, op, op_name, rel2, arg2) -#define assureOp(desc, rel1, arg1, op, op_name, rel2, arg2) \ - testBehaviorOp(true, desc, rel1, arg1, op, op_name, rel2, arg2) +#define arduinoCIAssureOp(desc, rel1, arg1, op, op_name, rel2, arg2) \ + arduinoCITestBehaviorOp(true, "assure" desc, rel1, arg1, op, op_name, rel2, arg2) /** macro generates optional output and calls fail() but does not return if false. */ -#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 assertTrue(arg) arduinoCITestBehaviorExp(false, "True " #arg, (arg)) +#define assertFalse(arg) arduinoCITestBehaviorExp(false, "False " #arg, !(arg)) +#define assertNull(arg) arduinoCITestBehaviorExp(false, "Null " #arg, ((void*)NULL == (void*)(arg))) +#define assertNotNull(arg) arduinoCITestBehaviorExp(false, "NotNull " #arg, ((void*)NULL != (void*)(arg))) +#define assertEqual(arg1,arg2) arduinoCIAssertOp("Equal","expected",arg1,compareEqual,"==","actual",arg2) +#define assertNotEqual(arg1,arg2) arduinoCIAssertOp("NotEqual","unwanted",arg1,compareNotEqual,"!=","actual",arg2) +#define assertComparativeEquivalent(arg1,arg2) arduinoCIAssertOp("ComparativeEquivalent","expected",arg1,compareEquivalent,"!<>","actual",arg2) +#define assertComparativeNotEquivalent(arg1,arg2) arduinoCIAssertOp("ComparativeNotEquivalent","unwanted",arg1,compareNotEquivalent,"<>","actual",arg2) +#define assertLess(arg1,arg2) arduinoCIAssertOp("Less","lowerBound",arg1,compareLess,"<","actual",arg2) +#define assertMore(arg1,arg2) arduinoCIAssertOp("More","upperBound",arg1,compareMore,">","actual",arg2) +#define assertLessOrEqual(arg1,arg2) arduinoCIAssertOp("LessOrEqual","lowerBound",arg1,compareLessOrEqual,"<=","actual",arg2) +#define assertMoreOrEqual(arg1,arg2) arduinoCIAssertOp("MoreOrEqual","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)) +#define assertEqualFloat(arg1, arg2, arg3) arduinoCIAssertOp("EqualFloat", "epsilon", arg3, compareMoreOrEqual, ">=", "actualDifference", fabs(arg1 - arg2)) +#define assertNotEqualFloat(arg1, arg2, arg3) arduinoCIAssertOp("NotEqualFloat", "epsilon", arg3, compareLessOrEqual, "<=", "insufficientDifference", fabs(arg1 - arg2)) +#define assertInfinity(arg) arduinoCITestBehaviorExp(false, "Infinity " #arg, isinf(arg)) +#define assertNotInfinity(arg) arduinoCITestBehaviorExp(false, "NotInfinity " #arg, !isinf(arg)) +#define assertNAN(arg) arduinoCITestBehaviorExp(false, "NAN " #arg, isnan(arg)) +#define assertNotNAN(arg) arduinoCITestBehaviorExp(false, "NotNAN " #arg, !isnan(arg)) /** macro generates optional output and calls fail() followed by a return if false. */ -#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 assureTrue(arg) arduinoCITestBehaviorExp(true, "True " #arg, (arg)) +#define assureFalse(arg) arduinoCITestBehaviorExp(true, "False " #arg, !(arg)) +#define assureNull(arg) arduinoCITestBehaviorExp(true, "Null " #arg, ((void*)NULL == (void*)(arg))) +#define assureNotNull(arg) arduinoCITestBehaviorExp(true, "NotNull " #arg, ((void*)NULL != (void*)(arg))) +#define assureEqual(arg1,arg2) arduinoCIAssureOp("Equal","expected",arg1,compareEqual,"==","actual",arg2) +#define assureNotEqual(arg1,arg2) arduinoCIAssureOp("NotEqual","unwanted",arg1,compareNotEqual,"!=","actual",arg2) +#define assureComparativeEquivalent(arg1,arg2) arduinoCIAssureOp("ComparativeEquivalent","expected",arg1,compareEquivalent,"!<>","actual",arg2) +#define assureComparativeNotEquivalent(arg1,arg2) arduinoCIAssureOp("ComparativeNotEquivalent","unwanted",arg1,compareNotEquivalent,"<>","actual",arg2) +#define assureLess(arg1,arg2) arduinoCIAssureOp("Less","lowerBound",arg1,compareLess,"<","actual",arg2) +#define assureMore(arg1,arg2) arduinoCIAssureOp("More","upperBound",arg1,compareMore,">","actual",arg2) +#define assureLessOrEqual(arg1,arg2) arduinoCIAssureOp("LessOrEqual","lowerBound",arg1,compareLessOrEqual,"<=","actual",arg2) +#define assureMoreOrEqual(arg1,arg2) arduinoCIAssureOp("MoreOrEqual","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)) +#define assureEqualFloat(arg1, arg2, arg3) arduinoCIAssureOp("EqualFloat", "epsilon", arg3, compareMoreOrEqual, ">=", "actualDifference", fabs(arg1 - arg2)) +#define assureNotEqualFloat(arg1, arg2, arg3) arduinoCIAssureOp("NotEqualFloat", "epsilon", arg3, compareLessOrEqual, "<=", "insufficientDifference", fabs(arg1 - arg2)) +#define assureInfinity(arg) arduinoCITestBehaviorExp(true, "Infinity " #arg, isinf(arg)) +#define assureNotInfinity(arg) arduinoCITestBehaviorExp(true, "NotInfinity " #arg, !isinf(arg)) +#define assureNAN(arg) arduinoCITestBehaviorExp(true, "NAN " #arg, isnan(arg)) +#define assureNotNAN(arg) arduinoCITestBehaviorExp(true, "NotNAN " #arg, !isnan(arg)) diff --git a/exe/arduino_ci.rb b/exe/arduino_ci.rb index 9ecaecdc..3de501b1 100755 --- a/exe/arduino_ci.rb +++ b/exe/arduino_ci.rb @@ -9,6 +9,7 @@ VAR_USE_SUBDIR = "USE_SUBDIR".freeze VAR_EXPECT_EXAMPLES = "EXPECT_EXAMPLES".freeze VAR_EXPECT_UNITTESTS = "EXPECT_UNITTESTS".freeze +VAR_SKIP_LIBPROPS = "SKIP_LIBRARY_PROPERTIES".freeze @failure_count = 0 @passfail = proc { |result| result ? "✓" : "✗" } @@ -21,6 +22,7 @@ def self.parse(options) output_options = { skip_unittests: false, skip_compilation: false, + skip_library_properties: false, ci_config: { "unittest" => unit_config }, @@ -37,6 +39,10 @@ def self.parse(options) output_options[:skip_compilation] = p end + opts.on("--skip-library-properties", "Don't validate library.properties entries") do |p| + output_options[:skip_compilation] = p + end + opts.on("--testfile-select=GLOB", "Unit test file (or glob) to select") do |p| unit_config["testfiles"] ||= {} unit_config["testfiles"]["select"] ||= [] @@ -58,6 +64,7 @@ def self.parse(options) 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" + puts " - #{VAR_SKIP_LIBPROPS} - if set, testing will skip [experimental] library.properties validation" exit end end @@ -74,10 +81,11 @@ def self.parse(options) def terminate(final = nil) puts "Failures: #{@failure_count}" unless @failure_count.zero? || final || @backend.nil? - puts "Last message: #{@backend.last_msg}" - puts "========== Stdout:" + puts "========== Last backend command (if relevant):" + puts @backend.last_msg.to_s + puts "========== Backend Stdout:" puts @backend.last_out - puts "========== Stderr:" + puts "========== Backend Stderr:" puts @backend.last_err end retcode = @failure_count.zero? ? 0 : 1 @@ -146,6 +154,10 @@ def inform_multiline(message, &block) perform_action(message, true, nil, nil, false, false, &block) end +def warn(message) + inform("WARNING") { message } +end + # Assure that a platform exists and return its definition def assured_platform(purpose, name, config) platform_definition = config.platform_definition(name) @@ -195,28 +207,20 @@ def install_arduino_library_dependencies(library_names, on_behalf_of, already_in installed end -# @param example_platform_info [Hash] mapping of platform name to package information -# @param board_package_url [Hash] mapping of package name to URL -def install_all_packages(example_platform_info, board_package_url) - # with all platform info, we can extract unique packages and their urls - # do that, set the URLs, and download the packages - all_packages = example_platform_info.values.map { |v| v[:package] }.uniq.reject(&:nil?) - - # make sure any non-builtin package has a URL defined - all_packages.each { |p| assure("Board package #{p} has a defined URL") { board_package_url[p] } } - - # set up all the board manager URLs. - # we can safely reject nils now, they would be for the builtins - all_urls = all_packages.map { |p| board_package_url[p] }.uniq.reject(&:nil?) - unless all_urls.empty? - assure_multiline("Setting board manager URLs") do - @backend.board_manager_urls = all_urls - result = @backend.board_manager_urls - result.each { |u| puts " #{u}" } - (all_urls - result).empty? # check that all_urls is completely contained in the result - end +# @param platforms [Array] list of platforms to consider +# @param specific_config [CIConfig] configuration to use +def install_all_packages(platforms, specific_config) + + # get packages from platforms + all_packages = specific_config.platform_info.select { |p, _| platforms.include?(p) }.values.map { |v| v[:package] }.compact.uniq + + all_packages.each do |pkg| + next if @backend.boards_installed?(pkg) + + url = assure("Board package #{pkg} has a defined URL") { specific_config.package_url(pkg) } + @backend.board_manager_urls = [url] + assure("Installing board package #{pkg}") { @backend.install_boards(pkg) } end - all_packages.each { |p| assure("Installing board package #{p}") { @backend.install_boards(p) } } end # @param expectation_envvar [String] the name of the env var to check @@ -248,17 +252,25 @@ def handle_expectation_of_files(expectation_envvar, operation, filegroup_name, d end inform(problem) { dir_path } + explain_and_exercise_envvar(expectation_envvar, operation, "contents of #{dir_desc}") { display_files(dir) } +end + +# @param expectation_envvar [String] the name of the env var to check +# @param operation [String] a description of what operation we might be skipping +# @param block_desc [String] a description of what information will be dumped to assist the user +# @param block [Proc] a function that dumps information +def explain_and_exercise_envvar(expectation_envvar, operation, block_desc, &block) inform("Environment variable #{expectation_envvar} is") { "(#{ENV[expectation_envvar].class}) #{ENV[expectation_envvar]}" } if ENV[expectation_envvar].nil? inform_multiline("Skipping #{operation}") do - puts " In case that's an error, this is what was found in the #{dir_desc}:" - display_files(dir) + puts " In case that's an error, displaying #{block_desc}:" + block.call puts " To force an error in this case, set the environment variable #{expectation_envvar}" true end else - assure_multiline("Dumping project's #{dir_desc} before exit") do - display_files(dir) + assure_multiline("Displaying #{block_desc} before exit") do + block.call false end end @@ -305,6 +317,81 @@ def perform_custom_initialization(_config) end end +# Auto-select some platforms to test based on the information available +# +# Top choice is always library.properties -- otherwise use the default. +# But filter that through any non-default config +# +# @param config [CIConfig] the overridden config object +# @param reason [String] description of why we might use this platform (i.e. unittest or compilation) +# @param desired_platforms [Array] the platform names specified +# @param library_properties [Hash] the library properties defined by the library +# @return [Array] platforms to use +def choose_platform_set(config, reason, desired_platforms, library_properties) + + # if there are no properties or no architectures, defer entirely to desired platforms + if library_properties.nil? || library_properties.architectures.nil? || library_properties.architectures.empty? + # verify that all platforms exist + desired_platforms.each { |p| assured_platform(reason, p, config) } + return inform_multiline("No architectures listed in library.properties, using configured platforms") do + desired_platforms.each { |p| puts " #{p}" } # this returns desired_platforms + end + end + + if library_properties.architectures.include?("*") + return inform_multiline("Wildcard architecture in library.properties, using configured platforms") do + desired_platforms.each { |p| puts " #{p}" } # this returns desired_platforms + end + end + + platform_architecture = config.platform_info.transform_values { |v| v[:board].split(":")[1] } + supported_platforms = platform_architecture.select { |_, a| library_properties.architectures.include?(a) } + + if config.is_default + # completely ignore default config, opting for brute-force library matches + # OTOH, we don't need to assure platforms because we defined them + return inform_multiline("Default config, platforms matching architectures in library.properties") do + supported_platforms.each_key do |p| + puts " #{p}" + end # this returns supported_platforms + end + end + + desired_supported_platforms = supported_platforms.select { |p, _| desired_platforms.include?(p) }.keys + desired_supported_platforms.each { |p| assured_platform(reason, p, config) } + inform_multiline("Configured platforms that match architectures in library.properties") do + desired_supported_platforms.each do |p| + puts " #{p}" + end # this returns supported_platforms + end +end + +# tests of sane library.properties values +def perform_property_tests(cpp_library) + return inform("Skipping library.properties tests") { "as requested via command line" } if @cli_options[:skip_library_properties] + return inform("Skipping library.properties tests") { "as requested via environment" } unless ENV[VAR_SKIP_LIBPROPS].nil? + return inform("Skipping library.properties tests") { "file not found" } unless cpp_library.library_properties? + + props = cpp_library.library_properties + + props.depends&.each do |l| + assure("library.properties 'depends=' entry '#{l}' is available via the library manager") { @backend.library_available?(l) } + end + + # the IDE would add these entries to a sketch (as "#include <...>" lines), they are nothing to do with the compioler + props.includes&.map(&:strip)&.map(&Pathname::method(:new))&.each do |f| + if (cpp_library.path + f).exist? + inform("library.properties 'includes=' entry found") { f } + elsif (cpp_library.path + "src" + f).exist? + inform("library.properties 'includes=' entry found") { Pathname.new("src") + f } + else + # this is if they want to "#include " or something -- may or may not be valid! so just warn. + warn("library.properties 'includes=' entry '#{f}' does not refer to a file in the library") + end + end + +end + # Unit test procedure def perform_unit_tests(cpp_library, file_config) if @cli_options[:skip_unittests] @@ -314,7 +401,6 @@ def perform_unit_tests(cpp_library, file_config) config = file_config.with_override_config(@cli_options[:ci_config]) compilers = get_annotated_compilers(config, cpp_library) - config.platforms_to_unittest.each_with_object({}) { |p, acc| acc[p] = assured_platform("unittest", p, config) } inform("Library conforms to Arduino library specification") { cpp_library.one_point_five? ? "1.5" : "1.0" } @@ -324,15 +410,20 @@ def perform_unit_tests(cpp_library, file_config) return end - # Handle lack of platforms - if config.platforms_to_unittest.empty? - inform("Skipping unit tests") { "no platforms were requested" } - return + # Get platforms, handle lack of them + platforms = choose_platform_set(config, "unittest", config.platforms_to_unittest, cpp_library.library_properties) + if platforms.empty? + explain_and_exercise_envvar(VAR_EXPECT_UNITTESTS, "unit tests", "platforms and architectures") do + puts " Configured platforms: #{config.platforms_to_unittest}" + puts " Configuration is default: #{config.is_default}" + arches = cpp_library.library_properties.nil? ? nil : cpp_library.library_properties.architectures + puts " Architectures in library.properties: #{arches}" + end end install_arduino_library_dependencies(config.aux_libraries_for_unittest, "") - config.platforms_to_unittest.each do |p| + platforms.each do |p| config.allowable_unittest_files(cpp_library.test_files).each do |unittest_path| unittest_name = unittest_path.basename.to_s compilers.each do |gcc_binary| @@ -363,47 +454,33 @@ def perform_example_compilation_tests(cpp_library, config) return end - # gather up all required boards for compilation so we can install them up front. - # start with the "platforms to unittest" and add the examples - # while we're doing that, get the aux libraries as well - example_platform_info = {} - board_package_url = {} - aux_libraries = Set.new(config.aux_libraries_for_build) - # while collecting the platforms, ensure they're defined - library_examples = cpp_library.example_sketches - library_examples.each do |path| - ovr_config = config.from_example(path) - ovr_config.platforms_to_build.each do |platform| - # assure the platform if we haven't already - next if example_platform_info.key?(platform) - - platform_info = assured_platform("library example", platform, config) - next if platform_info.nil? - - example_platform_info[platform] = platform_info - package = platform_info[:package] - board_package_url[package] = ovr_config.package_url(package) - end - aux_libraries.merge(ovr_config.aux_libraries_for_build) - end - install_all_packages(example_platform_info, board_package_url) - install_arduino_library_dependencies(aux_libraries, "") - - if config.platforms_to_build.empty? - inform("Skipping builds") { "no platforms were requested" } - return - elsif library_examples.empty? + if library_examples.empty? handle_expectation_of_files(VAR_EXPECT_EXAMPLES, "builds", "examples", "the examples directory", cpp_library.examples_dir) return end library_examples.each do |example_path| + example_name = File.basename(example_path) ovr_config = config.from_example(example_path) - ovr_config.platforms_to_build.each do |p| - board = example_platform_info[p][:board] - example_name = File.basename(example_path) + platforms = choose_platform_set(ovr_config, "library example", ovr_config.platforms_to_build, cpp_library.library_properties) + + if platforms.empty? + explain_and_exercise_envvar(VAR_EXPECT_EXAMPLES, "examples compilation", "platforms and architectures") do + puts " Configured platforms: #{config.platforms_to_build}" + puts " Configuration is default: #{config.is_default}" + arches = cpp_library.library_properties.nil? ? nil : cpp_library.library_properties.architectures + puts " Architectures in library.properties: #{arches}" + end + end + + install_all_packages(platforms, ovr_config) + + platforms.each do |p| + install_arduino_library_dependencies(ovr_config.aux_libraries_for_build, "") + + board = ovr_config.platform_info[p][:board] attempt("Compiling #{example_name} for #{board}") do ret = @backend.compile_sketch(example_path, board) unless ret @@ -435,9 +512,7 @@ def perform_example_compilation_tests(cpp_library, config) # Warn if the library name isn't obvious assumed_name = @backend.name_of_library(cpp_library_path) 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 +warn("Installed library named '#{assumed_name}' has directory name '#{ondisk_name}'") if assumed_name != ondisk_name if !cpp_library.nil? inform("Library installed at") { cpp_library.path.to_s } @@ -449,6 +524,8 @@ def perform_example_compilation_tests(cpp_library, config) end end +perform_property_tests(cpp_library) + install_arduino_library_dependencies( cpp_library.arduino_library_dependencies, "<#{ArduinoCI::CppLibrary::LIBRARY_PROPERTIES_FILE}>" diff --git a/lib/arduino_ci/arduino_backend.rb b/lib/arduino_ci/arduino_backend.rb index 42090d38..4e04f5b3 100644 --- a/lib/arduino_ci/arduino_backend.rb +++ b/lib/arduino_ci/arduino_backend.rb @@ -113,10 +113,17 @@ def board_manager_urls=(all_urls) # @param boardname [String] The board to test # @return [bool] Whether the board is installed def board_installed?(boardname) - # capture_json("core", "list")[:json].find { |b| b["ID"] == boardname } # nope, this is for the family run_and_capture("board", "details", "--fqbn", boardname)[:success] end + # check whether a board family is installed (e.g. arduino:avr) + # + # @param boardfamily_name [String] The board family to test + # @return [bool] Whether the board is installed + def boards_installed?(boardfamily_name) + capture_json("core", "list")[:json].any? { |b| b["ID"] == boardfamily_name } + end + # install a board by name # @param name [String] the board name # @return [bool] whether the command succeeded @@ -129,6 +136,15 @@ def install_boards(boardfamily) result[:success] end + # Find out if a library is available + # + # @param name [String] the library name + # @return [bool] whether the library can be installed via the library manager + def library_available?(name) + # the --names flag limits the size of the response to just the name field + capture_json("lib", "search", "--names", name)[:json]["libraries"].any? { |l| l["name"] == name } + end + # @return [Hash] information about installed libraries via the CLI def installed_libraries capture_json("lib", "list")[:json] diff --git a/lib/arduino_ci/ci_config.rb b/lib/arduino_ci/ci_config.rb index 3c5d2a9c..623100a0 100644 --- a/lib/arduino_ci/ci_config.rb +++ b/lib/arduino_ci/ci_config.rb @@ -57,20 +57,23 @@ class << self # @return [ArudinoCI::CIConfig] The configuration with defaults filled in def default ret = new + ret.instance_variable_set("@is_default", true) ret.load_yaml(File.expand_path("../../misc/default.yml", __dir__)) ret end end + attr_reader :is_default attr_accessor :package_info attr_accessor :platform_info attr_accessor :compile_info attr_accessor :unittest_info def initialize - @package_info = {} + @is_default = false + @package_info = {} @platform_info = {} - @compile_info = {} + @compile_info = {} @unittest_info = {} end diff --git a/lib/arduino_ci/library_properties.rb b/lib/arduino_ci/library_properties.rb index a28c6937..f1fb98d9 100644 --- a/lib/arduino_ci/library_properties.rb +++ b/lib/arduino_ci/library_properties.rb @@ -24,7 +24,12 @@ def initialize(path) # @return [Hash] the properties as a hash, all strings def to_h - @fields.clone + Hash[@fields.map { |k, _| [k.to_sym, send(k)] }] + end + + # @return [String] the string representation + def to_s + to_h.to_s end # Enable a shortcut syntax for library property accessors, in the style of `attr_accessor` metaprogramming. diff --git a/misc/default.yml b/misc/default.yml index 4d08ab9b..befbfa6f 100644 --- a/misc/default.yml +++ b/misc/default.yml @@ -70,7 +70,7 @@ platforms: warnings: flags: esp8266: - board: esp8266:esp8266:huzzah:FlashSize=4M3M,CpuFrequency=80 + board: esp8266:esp8266:huzzah:eesz=4M3M,xtal=80 package: esp8266:esp8266 gcc: features: diff --git a/spec/arduino_backend_spec.rb b/spec/arduino_backend_spec.rb index 1fbfa97e..fcf4dab1 100644 --- a/spec/arduino_backend_spec.rb +++ b/spec/arduino_backend_spec.rb @@ -56,6 +56,13 @@ def get_sketch(dir, file) expect(fake_lib.path).to eq(expected_dir) expect(fake_lib.installed?).to be false end + + it "knows whether libraries exist in the manager" do + expect(backend.library_available?("OneWire")).to be true + + # TODO: replace with a less offensive library name guaranteed never to exist? + expect(backend.library_available?("fuck")).to be false + end end context "board_manager" do diff --git a/spec/ci_config_spec.rb b/spec/ci_config_spec.rb index bab8293c..4db68968 100644 --- a/spec/ci_config_spec.rb +++ b/spec/ci_config_spec.rb @@ -9,6 +9,7 @@ it "loads from yaml" do default_config = ArduinoCI::CIConfig.default expect(default_config).not_to be nil + expect(default_config.is_default).to be true uno = default_config.platform_definition("uno") expect(uno.class).to eq(Hash) expect(uno[:board]).to eq("arduino:avr:uno") @@ -39,8 +40,8 @@ end end - context "clone" do - it "creates a copy" do + context "hash" do + it "converts to hash" do base = ArduinoCI::CIConfig.new base.load_yaml(File.join(File.dirname(__FILE__), "yaml", "o2.yaml")) @@ -82,8 +83,11 @@ context "with_override" do it "loads from yaml" do override_file = File.join(File.dirname(__FILE__), "yaml", "o1.yaml") - combined_config = ArduinoCI::CIConfig.default.with_override(override_file) + base = ArduinoCI::CIConfig.default + expect(base.is_default).to be true + combined_config = base.with_override(override_file) expect(combined_config).not_to be nil + expect(combined_config.is_default).to be false uno = combined_config.platform_definition("uno") expect(uno.class).to eq(Hash) expect(uno[:board]).to eq("arduino:avr:uno") diff --git a/spec/library_properties_spec.rb b/spec/library_properties_spec.rb index 84797077..5e4b04fd 100644 --- a/spec/library_properties_spec.rb +++ b/spec/library_properties_spec.rb @@ -46,6 +46,14 @@ it "doesn't crash on nonexistent fields" do expect(library_properties.dot_a_linkage).to be(nil) end + + it "converts to hash" do + h = library_properties.to_h + expect(h[:name].class).to eq(String) + expect(h[:name]).to eq("WebServer") + expect(h[:architectures].class).to eq(Array) + expect(h[:architectures]).to contain_exactly("avr") + end end context "Input handling" do @@ -65,5 +73,4 @@ end end - end