diff --git a/.rubocop.yml b/.rubocop.yml index ff2099a1..07841202 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -31,6 +31,12 @@ Layout/ExtraSpacing: Layout/EndOfLine: EnforcedStyle: lf +Layout/EndAlignment: + EnforcedStyleAlignWith: start_of_line + +Layout/CaseIndentation: + EnforcedStyle: end + Metrics/LineLength: Description: Limit lines to 80 characters. StyleGuide: https://github.com/bbatsov/ruby-style-guide#80-character-limits diff --git a/.travis.yml b/.travis.yml index 2603ca2c..36067457 100644 --- a/.travis.yml +++ b/.travis.yml @@ -27,3 +27,9 @@ script: - cd SampleProjects/TestSomething - bundle install - bundle exec arduino_ci.rb + - cd ../NetworkLib + - cd scripts + - bash -x ./install.sh + - cd .. + - bundle install + - bundle exec arduino_ci.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index db353f1c..a1e3a9ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,21 +7,36 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] ### Added -- Add `__AVR__` to defines when compiling +- `arduino_ci_remote.rb` CLI switch `--skip-examples-compilation` - Add support for `diditalPinToPort()`, `digitalPinToBitMask()`, and `portOutputRegister()` +- `CppLibrary.header_files` to find header files +- `LibraryProperties` to read metadata from Arduino libraries +- `CppLibrary.library_properties_path`, `CppLibrary.library_properties?`, `CppLibrary.library_properties` to expose library properties of a Cpp library +- `CppLibrary.arduino_library_dependencies` to list the dependent libraries specified by the library.properties file +- `CppLibrary.print_stack_dump` prints stack trace dumps (on Windows specifically) to the console if encountered +- Definitions for Arduino zero +- Support for mock EEPROM (but only if board supports it) +- Add stubs for `Client.h`, `IPAddress.h`, `Printable.h`, `Server.h`, and `Udp.h` ### Changed - Move repository from https://github.com/ianfixes/arduino_ci to https://github.com/Arduino-CI/arduino_ci - Revise math macros to avoid name clashes +- `CppLibrary` functions returning C++ header or code files now respect the 1.0/1.5 library specification +- Mocks of built-in macros made more accurate +- NUM_SERIAL_PORTS can now be set explicitly +- Improve SPI header strategy + +### Fixed +- Don't define `ostream& operator<<(nullptr_t)` if already defined by Apple +- `CppLibrary.in_tests_dir?` no longer produces an error if there is no tests directory +- The definition of the `_SFR_IO8` macro no longer produces errors about rvalues ### Deprecated +- `arduino_ci_remote.rb` CLI switch `--skip-compilation` - Deprecated `arduino_ci_remote.rb` in favor of `arduino_ci.rb` ### Removed -### Fixed -- Don't define `ostream& operator<<(nullptr_t)` if already defined by Apple - ### Security diff --git a/REFERENCE.md b/REFERENCE.md index 41925bdc..d54b5828 100644 --- a/REFERENCE.md +++ b/REFERENCE.md @@ -19,9 +19,14 @@ When testing locally, it's often advantageous to limit the number of tests that This completely skips the unit testing portion of the CI script. -### `--skip-compilation` option +### `--skip-compilation` option (deprecated) -This completely skips the compilation tests (of library examples) portion of the CI script. +This completely skips the compilation tests (of library examples) portion of the CI script. It does not skip the compilation of unit tests. + + +### `--skip-examples-compilation` option + +This completely skips the compilation tests (of library examples) portion of the CI script. It does not skip the compilation of unit tests. ### `--testfile-select` option @@ -90,8 +95,8 @@ platforms: ### Control How Examples Are Compiled -Put a file `.arduino-ci.yml` in each example directory where you require a different configuration than default. -The `compile:` section controls the platforms on which the compilation will be attempted, as well as any external libraries that must be installed and included. +Put a file `.arduino-ci.yml` in each example directory where you require a different configuration than default. +The `compile:` section controls the platforms on which the compilation will be attempted, as well as any external libraries that must be installed and included. ```yaml compile: @@ -581,3 +586,40 @@ unittest(spi) { assertEqual("LMNOe", String(inBuf)); } ``` + +### EEPROM + +`EEPROM` is a global with a simple API to read and write bytes to persistent memory (like a tiny hard disk) given an `int` location. Since the Arduino core already provides this as a global, and the core API is sufficient for basic testing (read/write), there is no direct tie to the `GODMODE` API. (If you need more, such as a log of intermediate values, enter a feature request.) + +```C++ +unittest(eeprom) +{ + uint8_t a; + // size + assertEqual(EEPROM_SIZE, EEPROM.length()); + // initial values + a = EEPROM.read(0); + assertEqual(255, a); + // write and read + EEPROM.write(0, 24); + a = EEPROM.read(0); + assertEqual(24, a); + // update + EEPROM.write(1, 14); + EEPROM.update(1, 22); + a = EEPROM.read(1); + assertEqual(22, a); + // put and get + const float f1 = 0.025f; + float f2 = 0.0f; + EEPROM.put(5, f1); + assertEqual(0.0f, f2); + EEPROM.get(5, f2); + assertEqual(0.025f, f2); + // array access + int val = 10; + EEPROM[2] = val; + a = EEPROM[2]; + assertEqual(10, a); +} +``` diff --git a/SampleProjects/DependOnSomething/library.properties b/SampleProjects/DependOnSomething/library.properties new file mode 100644 index 00000000..ea93aeb0 --- /dev/null +++ b/SampleProjects/DependOnSomething/library.properties @@ -0,0 +1 @@ +depends=OnePointOhDummy,OnePointFiveDummy diff --git a/SampleProjects/DependOnSomething/src/YesDeps.cpp b/SampleProjects/DependOnSomething/src/YesDeps.cpp new file mode 100644 index 00000000..e69de29b diff --git a/SampleProjects/DependOnSomething/src/YesDeps.h b/SampleProjects/DependOnSomething/src/YesDeps.h new file mode 100644 index 00000000..e69de29b diff --git a/SampleProjects/DependOnSomething/test/null.cpp b/SampleProjects/DependOnSomething/test/null.cpp new file mode 100644 index 00000000..d58eca29 --- /dev/null +++ b/SampleProjects/DependOnSomething/test/null.cpp @@ -0,0 +1,7 @@ +#include + +unittest(nothing) +{ +} + +unittest_main() diff --git a/SampleProjects/ExcludeSomething/.arduino-ci.yml b/SampleProjects/ExcludeSomething/.arduino-ci.yml new file mode 100644 index 00000000..f35995f0 --- /dev/null +++ b/SampleProjects/ExcludeSomething/.arduino-ci.yml @@ -0,0 +1,3 @@ +unittest: + exclude_dirs: + - src/excludeThis diff --git a/SampleProjects/ExcludeSomething/README.md b/SampleProjects/ExcludeSomething/README.md new file mode 100644 index 00000000..5bcc6553 --- /dev/null +++ b/SampleProjects/ExcludeSomething/README.md @@ -0,0 +1,3 @@ +# ExcludeSomething + +This example exists to test directory-exclusion code of ArduinoCI diff --git a/SampleProjects/ExcludeSomething/library.properties b/SampleProjects/ExcludeSomething/library.properties new file mode 100644 index 00000000..1745537f --- /dev/null +++ b/SampleProjects/ExcludeSomething/library.properties @@ -0,0 +1,10 @@ +name=TestSomething +version=0.1.0 +author=Ian Katz +maintainer=Ian Katz +sentence=Arduino CI unit test example +paragraph=A skeleton library demonstrating file exclusion +category=Other +url=https://github.com/Arduino-CI/arduino_ci/SampleProjects/ExcludeSomething +architectures=avr,esp8266 +includes=do-something.h diff --git a/SampleProjects/ExcludeSomething/src/exclude-something.cpp b/SampleProjects/ExcludeSomething/src/exclude-something.cpp new file mode 100644 index 00000000..951953f7 --- /dev/null +++ b/SampleProjects/ExcludeSomething/src/exclude-something.cpp @@ -0,0 +1,4 @@ +#include "exclude-something.h" +int excludeSomething(void) { + return -1; +}; diff --git a/SampleProjects/ExcludeSomething/src/exclude-something.h b/SampleProjects/ExcludeSomething/src/exclude-something.h new file mode 100644 index 00000000..abacb177 --- /dev/null +++ b/SampleProjects/ExcludeSomething/src/exclude-something.h @@ -0,0 +1,3 @@ +#pragma once +#include +int excludeSomething(void); diff --git a/SampleProjects/TestSomething/excludeThis/exclude-this.cpp b/SampleProjects/ExcludeSomething/src/excludeThis/exclude-this.cpp similarity index 100% rename from SampleProjects/TestSomething/excludeThis/exclude-this.cpp rename to SampleProjects/ExcludeSomething/src/excludeThis/exclude-this.cpp diff --git a/SampleProjects/TestSomething/excludeThis/exclude-this.h b/SampleProjects/ExcludeSomething/src/excludeThis/exclude-this.h similarity index 100% rename from SampleProjects/TestSomething/excludeThis/exclude-this.h rename to SampleProjects/ExcludeSomething/src/excludeThis/exclude-this.h diff --git a/SampleProjects/ExcludeSomething/test/null.cpp b/SampleProjects/ExcludeSomething/test/null.cpp new file mode 100644 index 00000000..d58eca29 --- /dev/null +++ b/SampleProjects/ExcludeSomething/test/null.cpp @@ -0,0 +1,7 @@ +#include + +unittest(nothing) +{ +} + +unittest_main() diff --git a/SampleProjects/NetworkLib/.arduino-ci.yml b/SampleProjects/NetworkLib/.arduino-ci.yml new file mode 100644 index 00000000..a242a79b --- /dev/null +++ b/SampleProjects/NetworkLib/.arduino-ci.yml @@ -0,0 +1,11 @@ +unittest: + platforms: + - mega2560 + libraries: + - "Ethernet" + +compile: + platforms: + - mega2560 + libraries: + - "Ethernet" diff --git a/SampleProjects/NetworkLib/.gitignore b/SampleProjects/NetworkLib/.gitignore new file mode 100644 index 00000000..06de90aa --- /dev/null +++ b/SampleProjects/NetworkLib/.gitignore @@ -0,0 +1 @@ +.bundle \ No newline at end of file diff --git a/SampleProjects/NetworkLib/Gemfile b/SampleProjects/NetworkLib/Gemfile new file mode 100644 index 00000000..b2b3b1fd --- /dev/null +++ b/SampleProjects/NetworkLib/Gemfile @@ -0,0 +1,2 @@ +source 'https://rubygems.org' +gem 'arduino_ci', path: '../../' diff --git a/SampleProjects/NetworkLib/README.md b/SampleProjects/NetworkLib/README.md new file mode 100644 index 00000000..b25d2e14 --- /dev/null +++ b/SampleProjects/NetworkLib/README.md @@ -0,0 +1,3 @@ +# NetworkLib + +This is an example of a library that depends on Ethernet. diff --git a/SampleProjects/NetworkLib/examples/EthernetExample/EthernetExample.ino b/SampleProjects/NetworkLib/examples/EthernetExample/EthernetExample.ino new file mode 100644 index 00000000..127afc76 --- /dev/null +++ b/SampleProjects/NetworkLib/examples/EthernetExample/EthernetExample.ino @@ -0,0 +1,6 @@ +#include +// if it seems bare, that's because it's only meant to +// demonstrate compilation -- that references work +void setup() {} + +void loop() {} diff --git a/SampleProjects/NetworkLib/library.properties b/SampleProjects/NetworkLib/library.properties new file mode 100644 index 00000000..2efc89bd --- /dev/null +++ b/SampleProjects/NetworkLib/library.properties @@ -0,0 +1,10 @@ +name=Ethernet +version=0.1.0 +author=James Foster +maintainer=James Foster +sentence=Sample Ethernet library to validate Client/Server mocks +paragraph=Sample Ethernet library to validate Client/Server mocks +category=Other +url=https://github.com/Arduino-CI/arduino_ci/SampleProjects/Ethernet +architectures=avr,esp8266 +includes=NetworkLib.h diff --git a/SampleProjects/NetworkLib/scripts/install.sh b/SampleProjects/NetworkLib/scripts/install.sh new file mode 100644 index 00000000..b4e2dd40 --- /dev/null +++ b/SampleProjects/NetworkLib/scripts/install.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +# if we don't have an Ethernet library already (say, in new install or for an automated test), +# then get the custom one we want to use for testing +cd $(bundle exec arduino_library_location.rb) +if [ ! -d ./Ethernet ] ; then + git clone https://github.com/Arduino-CI/Ethernet.git +fi diff --git a/SampleProjects/NetworkLib/src/NetworkLib.cpp b/SampleProjects/NetworkLib/src/NetworkLib.cpp new file mode 100644 index 00000000..01e5d5b0 --- /dev/null +++ b/SampleProjects/NetworkLib/src/NetworkLib.cpp @@ -0,0 +1 @@ +#include "Ethernet.h" diff --git a/SampleProjects/NetworkLib/src/NetworkLib.h b/SampleProjects/NetworkLib/src/NetworkLib.h new file mode 100644 index 00000000..9ee81b24 --- /dev/null +++ b/SampleProjects/NetworkLib/src/NetworkLib.h @@ -0,0 +1,3 @@ +#pragma once + +#include diff --git a/SampleProjects/NetworkLib/test/test.cpp b/SampleProjects/NetworkLib/test/test.cpp new file mode 100644 index 00000000..4c2d4eca --- /dev/null +++ b/SampleProjects/NetworkLib/test/test.cpp @@ -0,0 +1,15 @@ +/* +cd SampleProjects/NetworkLib +bundle config --local path vendor/bundle +bundle install +bundle exec arduino_ci_remote.rb --skip-compilation +# bundle exec arduino_ci_remote.rb --skip-examples-compilation +*/ + +#include +#include +#include + +unittest(test) { assertEqual(EthernetNoHardware, Ethernet.hardwareStatus()); } + +unittest_main() diff --git a/SampleProjects/OnePointFiveDummy/NoBase.cpp b/SampleProjects/OnePointFiveDummy/NoBase.cpp new file mode 100644 index 00000000..e69de29b diff --git a/SampleProjects/OnePointFiveDummy/NoBase.h b/SampleProjects/OnePointFiveDummy/NoBase.h new file mode 100644 index 00000000..e69de29b diff --git a/SampleProjects/OnePointFiveDummy/README.md b/SampleProjects/OnePointFiveDummy/README.md new file mode 100644 index 00000000..8ee1e7c5 --- /dev/null +++ b/SampleProjects/OnePointFiveDummy/README.md @@ -0,0 +1 @@ +This project resembles a "1.5 spec" library: it has `library.properties` and a `src/` directory that will be scanned recursively. `utility/`, if present, will be ignored. diff --git a/SampleProjects/OnePointFiveDummy/library.properties b/SampleProjects/OnePointFiveDummy/library.properties new file mode 100644 index 00000000..e69de29b diff --git a/SampleProjects/OnePointFiveDummy/src/YesSrc.cpp b/SampleProjects/OnePointFiveDummy/src/YesSrc.cpp new file mode 100644 index 00000000..e69de29b diff --git a/SampleProjects/OnePointFiveDummy/src/YesSrc.h b/SampleProjects/OnePointFiveDummy/src/YesSrc.h new file mode 100644 index 00000000..e69de29b diff --git a/SampleProjects/OnePointFiveDummy/src/subdir/YesSubdir.cpp b/SampleProjects/OnePointFiveDummy/src/subdir/YesSubdir.cpp new file mode 100644 index 00000000..e69de29b diff --git a/SampleProjects/OnePointFiveDummy/src/subdir/YesSubdir.h b/SampleProjects/OnePointFiveDummy/src/subdir/YesSubdir.h new file mode 100644 index 00000000..e69de29b diff --git a/SampleProjects/OnePointFiveDummy/test/null.cpp b/SampleProjects/OnePointFiveDummy/test/null.cpp new file mode 100644 index 00000000..d58eca29 --- /dev/null +++ b/SampleProjects/OnePointFiveDummy/test/null.cpp @@ -0,0 +1,7 @@ +#include + +unittest(nothing) +{ +} + +unittest_main() diff --git a/SampleProjects/OnePointFiveDummy/utility/ImNotHere.cpp b/SampleProjects/OnePointFiveDummy/utility/ImNotHere.cpp new file mode 100644 index 00000000..e69de29b diff --git a/SampleProjects/OnePointFiveDummy/utility/ImNotHere.h b/SampleProjects/OnePointFiveDummy/utility/ImNotHere.h new file mode 100644 index 00000000..e69de29b diff --git a/SampleProjects/OnePointFiveMalformed/README.md b/SampleProjects/OnePointFiveMalformed/README.md new file mode 100644 index 00000000..905d8336 --- /dev/null +++ b/SampleProjects/OnePointFiveMalformed/README.md @@ -0,0 +1 @@ +This project lacks a `library.properties` and so should be treated as a "1.0 spec" library -- the base and `utility` directories will be scanned for code, non-recursively. `src/`, if present, will be ignored. diff --git a/SampleProjects/OnePointFiveMalformed/YesBase.cpp b/SampleProjects/OnePointFiveMalformed/YesBase.cpp new file mode 100644 index 00000000..e69de29b diff --git a/SampleProjects/OnePointFiveMalformed/YesBase.h b/SampleProjects/OnePointFiveMalformed/YesBase.h new file mode 100644 index 00000000..e69de29b diff --git a/SampleProjects/OnePointFiveMalformed/src/ImNotHere.cpp b/SampleProjects/OnePointFiveMalformed/src/ImNotHere.cpp new file mode 100644 index 00000000..e69de29b diff --git a/SampleProjects/OnePointFiveMalformed/src/ImNotHere.h b/SampleProjects/OnePointFiveMalformed/src/ImNotHere.h new file mode 100644 index 00000000..e69de29b diff --git a/SampleProjects/OnePointFiveMalformed/utility/YesUtil.cpp b/SampleProjects/OnePointFiveMalformed/utility/YesUtil.cpp new file mode 100644 index 00000000..e69de29b diff --git a/SampleProjects/OnePointFiveMalformed/utility/YesUtil.h b/SampleProjects/OnePointFiveMalformed/utility/YesUtil.h new file mode 100644 index 00000000..e69de29b diff --git a/SampleProjects/OnePointOhDummy/README.md b/SampleProjects/OnePointOhDummy/README.md new file mode 100644 index 00000000..8afffdd9 --- /dev/null +++ b/SampleProjects/OnePointOhDummy/README.md @@ -0,0 +1 @@ +This project should resemble "1.0 spec" library -- the base and `utility` directories will be scanned for code, non-recursively. `src/`, if present, will be ignored. diff --git a/SampleProjects/OnePointOhDummy/YesBase.cpp b/SampleProjects/OnePointOhDummy/YesBase.cpp new file mode 100644 index 00000000..e69de29b diff --git a/SampleProjects/OnePointOhDummy/YesBase.h b/SampleProjects/OnePointOhDummy/YesBase.h new file mode 100644 index 00000000..e69de29b diff --git a/SampleProjects/OnePointOhDummy/src/ImNotHere.cpp b/SampleProjects/OnePointOhDummy/src/ImNotHere.cpp new file mode 100644 index 00000000..e69de29b diff --git a/SampleProjects/OnePointOhDummy/src/ImNotHere.h b/SampleProjects/OnePointOhDummy/src/ImNotHere.h new file mode 100644 index 00000000..e69de29b diff --git a/SampleProjects/OnePointOhDummy/test/null.cpp b/SampleProjects/OnePointOhDummy/test/null.cpp new file mode 100644 index 00000000..d58eca29 --- /dev/null +++ b/SampleProjects/OnePointOhDummy/test/null.cpp @@ -0,0 +1,7 @@ +#include + +unittest(nothing) +{ +} + +unittest_main() diff --git a/SampleProjects/OnePointOhDummy/utility/YesUtil.cpp b/SampleProjects/OnePointOhDummy/utility/YesUtil.cpp new file mode 100644 index 00000000..e69de29b diff --git a/SampleProjects/OnePointOhDummy/utility/YesUtil.h b/SampleProjects/OnePointOhDummy/utility/YesUtil.h new file mode 100644 index 00000000..e69de29b diff --git a/SampleProjects/README.md b/SampleProjects/README.md index 6b7ed569..8e5f5b11 100644 --- a/SampleProjects/README.md +++ b/SampleProjects/README.md @@ -1,7 +1,15 @@ Arduino Sample Projects ======================= -This directory contains projects that are meant to be built with and tested by this gem. Although this directory is named `SampleProjects`, it is by no means optional. These project test the testing framework itself, but also provide examples of how you might write your own tests (which should be placed in your system's Arduino `libraries` directory). +This directory contains projects that are intended solely for testing the various features of this gem -- to test the testing framework itself. The RSpec tests refer specifically to these projects. -* "DoSomething" is a simple test of the testing framework (arduino_ci) itself to verfy that passes and failures are properly identified and reported. -* "TestSomething" contains tests for all the mock features of arduino_ci. +Because of this, these projects include some intentional quirks that differ from what a well-formed an Arduino project for testing with `arduino_ci` might contain. See other projects in the "Arduino-CI" GitHub organization for practical examples. + + +* "TestSomething" contains a minimial library, but tests for all the C++ compilation feature-mocks of arduino_ci. +* "DoSomething" is a simple test of the testing framework (arduino_ci) itself to verfy that passes and failures are properly identified and reported. Because of this, it includes test files that are expected to fail -- they are prefixed with "bad-". +* "OnePointOhDummy" is a non-functional library meant to test file inclusion logic on libraries conforming to the "1.0" specification +* "OnePointFiveMalformed" is a non-functional library meant to test file inclusion logic on libraries that attempt to conform to the ["1.5" specfication](https://arduino.github.io/arduino-cli/latest/library-specification/) but fail to include a `src` directory +* "OnePointFiveDummy" is a non-functional library meant to test file inclusion logic on libraries conforming to the ["1.5" specfication](https://arduino.github.io/arduino-cli/latest/library-specification/) +* "DependOnSomething" is a non-functional library meant to test file inclusion logic with dependencies +* "ExcludeSomething" is a non-functional library meant to test directory exclusion logic diff --git a/SampleProjects/TestSomething/.arduino-ci.yml b/SampleProjects/TestSomething/.arduino-ci.yml index c418bdbe..f9890177 100644 --- a/SampleProjects/TestSomething/.arduino-ci.yml +++ b/SampleProjects/TestSomething/.arduino-ci.yml @@ -1,6 +1,4 @@ unittest: - exclude_dirs: - - excludeThis platforms: - uno - due diff --git a/SampleProjects/TestSomething/test-something.cpp b/SampleProjects/TestSomething/src/test-something.cpp similarity index 100% rename from SampleProjects/TestSomething/test-something.cpp rename to SampleProjects/TestSomething/src/test-something.cpp diff --git a/SampleProjects/TestSomething/test-something.h b/SampleProjects/TestSomething/src/test-something.h similarity index 100% rename from SampleProjects/TestSomething/test-something.h rename to SampleProjects/TestSomething/src/test-something.h diff --git a/SampleProjects/TestSomething/test/clientServer.cpp b/SampleProjects/TestSomething/test/clientServer.cpp new file mode 100644 index 00000000..f088c821 --- /dev/null +++ b/SampleProjects/TestSomething/test/clientServer.cpp @@ -0,0 +1,87 @@ +#include +#include +#include +#include +#include +#include +#include + +// Provide some rudamentary tests for these classes +// They get more thoroughly tested in SampleProjects/NetworkLib + +unittest(Client) { + Client client; + assertEqual(0, client.available()); // subclass of Stream + assertEqual(0, client.availableForWrite()); // subclass of Print + String outData = "Hello, world!"; + client.println(outData); + String inData = client.readString(); + assertEqual(outData + "\r\n", inData); +} + +unittest(IPAddress) { + IPAddress ipAddress0; + assertEqual(0, ipAddress0.asWord()); + uint32_t one = 0x01020304; + IPAddress ipAddress1(one); + assertEqual(one, ipAddress1.asWord()); + IPAddress ipAddress2(2, 3, 4, 5); + assertEqual(0x05040302, ipAddress2.asWord()); + uint8_t bytes[] = {3, 4, 5, 6}; + IPAddress ipAddress3(bytes); + assertEqual(0x06050403, ipAddress3.asWord()); + uint8_t *pBytes = ipAddress1.raw_address(); + assertEqual(*(pBytes + 0), 4); + assertEqual(*(pBytes + 1), 3); + assertEqual(*(pBytes + 2), 2); + assertEqual(*(pBytes + 3), 1); + IPAddress ipAddress1a(one); + assertTrue(ipAddress1 == ipAddress1a); + assertTrue(ipAddress1 != ipAddress2); + assertEqual(1, ipAddress1[3]); + ipAddress1[1] = 11; + assertEqual(11, ipAddress1[1]); + assertEqual(1, ipAddress0 + 1); +} + +class TestPrintable : public Printable { +public: + virtual size_t printTo(Print &p) const { + p.print("TestPrintable"); + return 13; + } +}; + +unittest(Printable) { + TestPrintable printable; + Client client; + client.print(printable); + assertEqual("TestPrintable", client.readString()); +} + +class TestServer : public Server { +public: + uint8_t data; + virtual size_t write(uint8_t value) { + data = value; + return 1; + }; +}; + +unittest(Server) { + TestServer server; + server.write(67); + assertEqual(67, server.data); +} + +unittest(Udp) { + UDP udp; + assertEqual(0, udp.available()); // subclass of Stream + assertEqual(0, udp.availableForWrite()); // subclass of Print + String outData = "Hello, world!"; + udp.println(outData); + String inData = udp.readString(); + assertEqual(outData + "\r\n", inData); +} + +unittest_main() diff --git a/SampleProjects/TestSomething/test/defines.cpp b/SampleProjects/TestSomething/test/defines.cpp index 6b09d851..bbfe134d 100644 --- a/SampleProjects/TestSomething/test/defines.cpp +++ b/SampleProjects/TestSomething/test/defines.cpp @@ -8,4 +8,26 @@ unittest(binary) assertEqual(100, B1100100); } +#ifdef __AVR__ +#define DDRE _SFR_IO8(0x02) + +unittest(SFR_IO8) +{ + // in normal arduino code, you can do this. in arduino_ci, you might get an + // error like: cannot take the address of an rvalue of type 'int' + // + // this tests that directly + auto foo = &DDRE; // avoid compiler warning by using the result of an expression +} + +unittest(read_write) +{ + _SFR_IO8(1) = 0x11; + _SFR_IO8(2) = 0x22; + assertEqual((int) 0x11, (int) _SFR_IO8(1)); + assertEqual((int) 0x22, (int) _SFR_IO8(2)); + assertEqual((int) 0x2211, (int) _SFR_IO16(1)); +} +#endif + unittest_main() diff --git a/SampleProjects/TestSomething/test/eeprom.cpp b/SampleProjects/TestSomething/test/eeprom.cpp new file mode 100644 index 00000000..8a844249 --- /dev/null +++ b/SampleProjects/TestSomething/test/eeprom.cpp @@ -0,0 +1,80 @@ +#include +#include +#include + +// Only run EEPROM tests if there is hardware support! +#if defined(EEPROM_SIZE) +#include + +GodmodeState* state = GODMODE(); + +unittest_setup() +{ + state->reset(); +} + +unittest(length) +{ + assertEqual(EEPROM_SIZE, EEPROM.length()); +} + +unittest(firstRead) +{ + uint8_t a = EEPROM.read(0); + assertEqual(255, a); +} + +unittest(writeRead) +{ + EEPROM.write(0, 24); + uint8_t a = EEPROM.read(0); + assertEqual(24, a); + + EEPROM.write(0, 128); + a = EEPROM.read(0); + assertEqual(128, a); + + EEPROM.write(0, 255); + a = EEPROM.read(0); + assertEqual(255, a); + + int addr = EEPROM_SIZE / 2; + EEPROM.write(addr, 63); + a = EEPROM.read(addr); + assertEqual(63, a); + + addr = EEPROM_SIZE - 1; + EEPROM.write(addr, 188); + a = EEPROM.read(addr); + assertEqual(188, a); +} + +unittest(updateWrite) +{ + EEPROM.write(1, 14); + EEPROM.update(1, 22); + uint8_t a = EEPROM.read(1); + assertEqual(22, a); +} + +unittest(putGet) +{ + const float f1 = 0.025f; + float f2 = 0.0f; + EEPROM.put(5, f1); + assertEqual(0.0f, f2); + EEPROM.get(5, f2); + assertEqual(0.025f, f2); +} + +unittest(array) +{ + int val = 10; + EEPROM[2] = val; + uint8_t a = EEPROM[2]; + assertEqual(10, a); +} + +#endif + +unittest_main() diff --git a/SampleProjects/TestSomething/test/godmode.cpp b/SampleProjects/TestSomething/test/godmode.cpp index e6c69502..6e57d9d6 100644 --- a/SampleProjects/TestSomething/test/godmode.cpp +++ b/SampleProjects/TestSomething/test/godmode.cpp @@ -1,5 +1,6 @@ #include #include +#include #include "fibonacciClock.h" GodmodeState* state = GODMODE(); @@ -175,18 +176,35 @@ unittest(spi) { // 8-bit state->reset(); state->spi.dataIn = "LMNO"; + SPI.beginTransaction(SPISettings(14000000, LSBFIRST, SPI_MODE0)); uint8_t out8 = SPI.transfer('a'); + SPI.endTransaction(); assertEqual("a", state->spi.dataOut); assertEqual('L', out8); assertEqual("MNO", state->spi.dataIn); - // 16-bit + // 16-bit MSBFIRST union { uint16_t val; struct { char lsb; char msb; }; } in16, out16; state->reset(); state->spi.dataIn = "LMNO"; in16.lsb = 'a'; in16.msb = 'b'; + SPI.beginTransaction(SPISettings(14000000, MSBFIRST, SPI_MODE0)); out16.val = SPI.transfer16(in16.val); + SPI.endTransaction(); + assertEqual("NO", state->spi.dataIn); + assertEqual('M', out16.lsb); + assertEqual('L', out16.msb); + assertEqual("ba", state->spi.dataOut); + + // 16-bit LSBFIRST + state->reset(); + state->spi.dataIn = "LMNO"; + in16.lsb = 'a'; + in16.msb = 'b'; + SPI.beginTransaction(SPISettings(14000000, LSBFIRST, SPI_MODE0)); + out16.val = SPI.transfer16(in16.val); + SPI.endTransaction(); assertEqual("NO", state->spi.dataIn); assertEqual('L', out16.lsb); assertEqual('M', out16.msb); @@ -196,14 +214,15 @@ unittest(spi) { state->reset(); state->spi.dataIn = "LMNOP"; char inBuf[6] = "abcde"; + SPI.beginTransaction(SPISettings(14000000, LSBFIRST, SPI_MODE0)); SPI.transfer(inBuf, 4); + SPI.endTransaction(); assertEqual("abcd", state->spi.dataOut); assertEqual("LMNOe", String(inBuf)); } - #ifdef HAVE_HWSERIAL0 void smartLightswitchSerialHandler(int pin) { diff --git a/SampleProjects/TestSomething/test/library.cpp b/SampleProjects/TestSomething/test/library.cpp index d80ad2c4..675d83e9 100644 --- a/SampleProjects/TestSomething/test/library.cpp +++ b/SampleProjects/TestSomething/test/library.cpp @@ -1,5 +1,5 @@ #include -#include "../test-something.h" +#include "../src/test-something.h" unittest(library_tests_something) { diff --git a/appveyor.yml b/appveyor.yml index af4bd854..d8576b06 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -25,3 +25,9 @@ test_script: - cd SampleProjects\TestSomething - bundle install - bundle exec arduino_ci.rb + - cd ../NetworkLib + - cd scripts + - install.sh + - cd .. + - bundle install + - bundle exec arduino_ci.rb diff --git a/cpp/arduino/Arduino.h b/cpp/arduino/Arduino.h index e107126e..4d00095b 100644 --- a/cpp/arduino/Arduino.h +++ b/cpp/arduino/Arduino.h @@ -9,13 +9,12 @@ Where possible, variable names from the Arduino library are used to avoid confli #include "ArduinoDefines.h" +#include "IPAddress.h" #include "WCharacter.h" #include "WString.h" #include "Print.h" #include "Stream.h" #include "HardwareSerial.h" -#include "SPI.h" -#include "Wire.h" typedef bool boolean; typedef uint8_t byte; diff --git a/cpp/arduino/Client.h b/cpp/arduino/Client.h new file mode 100644 index 00000000..b08e183e --- /dev/null +++ b/cpp/arduino/Client.h @@ -0,0 +1,26 @@ +#pragma once + +#include + +class Client : public Stream { +public: + Client() { + // The Stream mock defines a String buffer but never puts anyting in it! + if (!mGodmodeDataIn) { + mGodmodeDataIn = new String; + } + } + ~Client() { + if (mGodmodeDataIn) { + delete mGodmodeDataIn; + mGodmodeDataIn = nullptr; + } + } + virtual size_t write(uint8_t value) { + mGodmodeDataIn->concat(value); + return 1; + } + +protected: + uint8_t *rawIPAddress(IPAddress &addr) { return addr.raw_address(); } +}; diff --git a/cpp/arduino/EEPROM.h b/cpp/arduino/EEPROM.h new file mode 100644 index 00000000..05b3daa5 --- /dev/null +++ b/cpp/arduino/EEPROM.h @@ -0,0 +1,64 @@ +#pragma once + +#include +#include +#include + +// Does the current board have EEPROM? +#ifndef EEPROM_SIZE + // In lieu of an "EEPROM.h not found" error for unsupported boards + #error "EEPROM library not available for your board" +#endif + +class EEPROMClass { +private: + GodmodeState* state; +public: + // constructor + EEPROMClass() { + state = GODMODE(); + } + // array subscript operator + uint8_t &operator[](const int index) { + assert(index < EEPROM_SIZE); + return state->eeprom[index]; + } + + uint8_t read(const int index) { + assert(index < EEPROM_SIZE); + return state->eeprom[index]; + } + + void write(const int index, const uint8_t value) { + assert(index < EEPROM_SIZE); + state->eeprom[index] = value; + } + + void update(const int index, const uint8_t value) { + assert(index < EEPROM_SIZE); + state->eeprom[index] = value; + } + + uint16_t length() { return EEPROM_SIZE; } + + // read any object + template T &get(const int index, T &object) { + uint8_t *ptr = (uint8_t *)&object; + for (int i = 0; i < sizeof(T); ++i) { + *ptr++ = read(index + i); + } + return object; + } + + // write any object + template const T &put(const int index, T &object) { + const uint8_t *ptr = (const uint8_t *)&object; + for (int i = 0; i < sizeof(T); ++i) { + write(index + i, *ptr++); + } + return object; + } +}; + +// global available in Godmode.cpp +extern EEPROMClass EEPROM; diff --git a/cpp/arduino/Godmode.cpp b/cpp/arduino/Godmode.cpp index 102afca6..96bdfe6f 100644 --- a/cpp/arduino/Godmode.cpp +++ b/cpp/arduino/Godmode.cpp @@ -113,3 +113,10 @@ SPIClass SPI = SPIClass(&GODMODE()->spi.dataIn, &GODMODE()->spi.dataOut); // defined in Wire.h TwoWire Wire = TwoWire(); + +#if defined(EEPROM_SIZE) + #include + EEPROMClass EEPROM; +#endif + +volatile uint8_t __ARDUINO_CI_SFR_MOCK[1024]; diff --git a/cpp/arduino/Godmode.h b/cpp/arduino/Godmode.h index de7a299e..b748a148 100644 --- a/cpp/arduino/Godmode.h +++ b/cpp/arduino/Godmode.h @@ -1,6 +1,8 @@ #pragma once #include "ArduinoDefines.h" +#if defined(__AVR__) #include +#endif #include "WString.h" #include "PinHistory.h" @@ -18,16 +20,29 @@ unsigned long micros(); #define MOCK_PINS_COUNT 256 -#if defined(UBRR3H) - #define NUM_SERIAL_PORTS 4 -#elif defined(UBRR2H) - #define NUM_SERIAL_PORTS 3 -#elif defined(UBRR1H) - #define NUM_SERIAL_PORTS 2 -#elif defined(UBRRH) || defined(UBRR0H) - #define NUM_SERIAL_PORTS 1 +#if (!defined NUM_SERIAL_PORTS) + #if defined(UBRR3H) + #define NUM_SERIAL_PORTS 4 + #elif defined(UBRR2H) + #define NUM_SERIAL_PORTS 3 + #elif defined(UBRR1H) + #define NUM_SERIAL_PORTS 2 + #elif defined(UBRRH) || defined(UBRR0H) + #define NUM_SERIAL_PORTS 1 + #else + #define NUM_SERIAL_PORTS 0 + #endif +#endif + +// different EEPROM implementations have different macros that leak out +#if !defined(EEPROM_SIZE) && defined(E2END) && (E2END) + // public value indicates that feature is available + #define EEPROM_SIZE (E2END + 1) + // local array size + #define _EEPROM_SIZE EEPROM_SIZE #else - #define NUM_SERIAL_PORTS 0 + // feature is not available but we want to have the array so other code compiles + #define _EEPROM_SIZE (0) #endif class GodmodeState { @@ -56,6 +71,7 @@ class GodmodeState { struct PortDef serialPort[NUM_SERIAL_PORTS]; struct InterruptDef interrupt[MOCK_PINS_COUNT]; // not sure how to get actual number struct PortDef spi; + uint8_t eeprom[_EEPROM_SIZE]; void resetPins() { for (int i = 0; i < MOCK_PINS_COUNT; ++i) { @@ -95,6 +111,14 @@ class GodmodeState { } } + void resetEEPROM() { +#if defined(EEPROM_SIZE) + for(int i = 0; i < EEPROM_SIZE; ++i) { + eeprom[i] = 255; + } +#endif + } + void reset() { resetClock(); resetPins(); @@ -102,6 +126,7 @@ class GodmodeState { resetPorts(); resetSPI(); resetMmapPorts(); + resetEEPROM(); seed = 1; } diff --git a/cpp/arduino/HardwareSerial.h b/cpp/arduino/HardwareSerial.h index d4ea97f9..68c2010c 100644 --- a/cpp/arduino/HardwareSerial.h +++ b/cpp/arduino/HardwareSerial.h @@ -44,19 +44,19 @@ class HardwareSerial : public StreamTape operator bool() { return true; } }; -#if defined(UBRRH) || defined(UBRR0H) +#if NUM_SERIAL_PORTS >= 1 extern HardwareSerial Serial; #define HAVE_HWSERIAL0 #endif -#if defined(UBRR1H) +#if NUM_SERIAL_PORTS >= 2 extern HardwareSerial Serial1; #define HAVE_HWSERIAL1 #endif -#if defined(UBRR2H) +#if NUM_SERIAL_PORTS >= 3 extern HardwareSerial Serial2; #define HAVE_HWSERIAL2 #endif -#if defined(UBRR3H) +#if NUM_SERIAL_PORTS >= 4 extern HardwareSerial Serial3; #define HAVE_HWSERIAL3 #endif diff --git a/cpp/arduino/IPAddress.h b/cpp/arduino/IPAddress.h new file mode 100644 index 00000000..89a343e1 --- /dev/null +++ b/cpp/arduino/IPAddress.h @@ -0,0 +1,59 @@ +#pragma once + +#include + +class IPAddress { +private: + union { + uint8_t bytes[4]; + uint32_t dword; + operator uint8_t *() const { return (uint8_t *)bytes; } + } _address; + +public: + // Constructors + IPAddress() : IPAddress(0, 0, 0, 0) {} + IPAddress(uint8_t octet1, uint8_t octet2, uint8_t octet3, uint8_t octet4) { + _address.bytes[0] = octet1; + _address.bytes[1] = octet2; + _address.bytes[2] = octet3; + _address.bytes[3] = octet4; + } + IPAddress(uint32_t dword) { _address.dword = dword; } + IPAddress(const uint8_t bytes[]) { + _address.bytes[0] = bytes[0]; + _address.bytes[1] = bytes[1]; + _address.bytes[2] = bytes[2]; + _address.bytes[3] = bytes[3]; + } + IPAddress(unsigned long dword) { _address.dword = (uint32_t)dword; } + + // Accessors + uint32_t asWord() const { return _address.dword; } + uint8_t *raw_address() { return _address.bytes; } + + // Comparisons + bool operator==(const IPAddress &rhs) const { + return _address.dword == rhs.asWord(); + } + + bool operator!=(const IPAddress &rhs) const { + return _address.dword != rhs.asWord(); + } + + // Indexing + uint8_t operator[](int index) const { return _address.bytes[index]; } + uint8_t &operator[](int index) { return _address.bytes[index]; } + + // Conversions + operator uint32_t() const { return _address.dword; }; + + friend class EthernetClass; + friend class UDP; + friend class Client; + friend class Server; + friend class DhcpClass; + friend class DNSClient; +}; + +const IPAddress INADDR_NONE(0, 0, 0, 0); diff --git a/cpp/arduino/Print.h b/cpp/arduino/Print.h index b7d8a522..261b116d 100644 --- a/cpp/arduino/Print.h +++ b/cpp/arduino/Print.h @@ -2,6 +2,8 @@ #include #include + +#include "Printable.h" #include "WString.h" #define DEC 10 @@ -12,22 +14,17 @@ #endif #define BIN 2 -class Print; - -class Printable -{ - public: - virtual size_t printTo(Print& p) const = 0; -}; - class Print { + private: + int write_error; + protected: + void setWriteError(int err = 1) { write_error = err; } public: - Print() {} + Print() : write_error(0) {} - // Arduino's version of this is richer but until I see an actual error case I'm not sure how to mock - int getWriteError() { return 0; } - void clearWriteError() { } + int getWriteError() { return write_error; } + void clearWriteError() { setWriteError(0); } virtual int availableForWrite() { return 0; } virtual size_t write(uint8_t) = 0; diff --git a/cpp/arduino/Printable.h b/cpp/arduino/Printable.h new file mode 100644 index 00000000..cdd361d3 --- /dev/null +++ b/cpp/arduino/Printable.h @@ -0,0 +1,8 @@ +#pragma once + +class Print; + +class Printable { +public: + virtual size_t printTo(Print &p) const = 0; +}; diff --git a/cpp/arduino/SPI.h b/cpp/arduino/SPI.h index f10cd201..2096ccb0 100644 --- a/cpp/arduino/SPI.h +++ b/cpp/arduino/SPI.h @@ -40,7 +40,11 @@ class SPISettings { public: - SPISettings(uint32_t clock, uint8_t bitOrder, uint8_t dataMode){}; + uint8_t bitOrder; + + SPISettings(uint32_t clock, uint8_t bitOrder = MSBFIRST, uint8_t dataMode = SPI_MODE0) { + this->bitOrder = bitOrder; + }; SPISettings(){}; }; @@ -68,6 +72,7 @@ class SPIClass: public ObservableDataStream { // and configure the correct settings. void beginTransaction(SPISettings settings) { + this->bitOrder = settings.bitOrder; #ifdef SPI_TRANSACTION_MISMATCH_LED if (inTransactionFlag) { pinMode(SPI_TRANSACTION_MISMATCH_LED, OUTPUT); @@ -94,10 +99,12 @@ class SPIClass: public ObservableDataStream { uint16_t transfer16(uint16_t data) { union { uint16_t val; struct { uint8_t lsb; uint8_t msb; }; } in, out; in.val = data; - if (!(SPCR & (1 << DORD))) { + if (bitOrder == MSBFIRST) { out.msb = transfer(in.msb); out.lsb = transfer(in.lsb); - } else { + } + else + { out.lsb = transfer(in.lsb); out.msb = transfer(in.msb); } @@ -143,6 +150,7 @@ class SPIClass: public ObservableDataStream { #endif bool isStarted = false; + uint8_t bitOrder; String* dataIn; String* dataOut; }; diff --git a/cpp/arduino/Server.h b/cpp/arduino/Server.h new file mode 100644 index 00000000..dd1993ff --- /dev/null +++ b/cpp/arduino/Server.h @@ -0,0 +1,5 @@ +#pragma once + +#include + +class Server : public Print {}; diff --git a/cpp/arduino/Udp.h b/cpp/arduino/Udp.h new file mode 100644 index 00000000..8352f7f6 --- /dev/null +++ b/cpp/arduino/Udp.h @@ -0,0 +1,27 @@ +#pragma once + +#include +#include + +class UDP : public Stream { +protected: + uint8_t *rawIPAddress(IPAddress &addr) { return addr.raw_address(); }; + +public: + UDP() { + // The Stream mock defines a String buffer but never puts anyting in it! + if (!mGodmodeDataIn) { + mGodmodeDataIn = new String; + } + } + ~UDP() { + if (mGodmodeDataIn) { + delete mGodmodeDataIn; + mGodmodeDataIn = nullptr; + } + } + virtual size_t write(uint8_t value) { + mGodmodeDataIn->concat(value); + return 1; + } +}; diff --git a/cpp/arduino/avr/io.h b/cpp/arduino/avr/io.h index f07699a0..337b979e 100644 --- a/cpp/arduino/avr/io.h +++ b/cpp/arduino/avr/io.h @@ -96,7 +96,16 @@ #ifndef _AVR_IO_H_ #define _AVR_IO_H_ -#define _SFR_IO8(io_addr) (io_addr) // this macro is all we need from the sfr file +#include + +// hardware mocks +// this set of macros is all we need from the sfr file +extern volatile uint8_t __ARDUINO_CI_SFR_MOCK[1024]; +#define _SFR_IO8(io_addr) (*(volatile uint8_t *)(__ARDUINO_CI_SFR_MOCK + io_addr)) +#define _SFR_IO16(io_addr) (*(volatile uint16_t *)(__ARDUINO_CI_SFR_MOCK + io_addr)) +#define _SFR_MEM8(io_addr) (*(volatile uint8_t *)(__ARDUINO_CI_SFR_MOCK + io_addr)) +#define _SFR_MEM16(io_addr) (*(volatile uint16_t *)(__ARDUINO_CI_SFR_MOCK + io_addr)) +#define _SFR_MEM32(io_addr) (*(volatile uint32_t *)(__ARDUINO_CI_SFR_MOCK + io_addr)) #if defined (__AVR_AT94K__) # include "ioat94k.h" diff --git a/cpp/arduino/avr/pgmspace.h b/cpp/arduino/avr/pgmspace.h index 6b21287b..cab19057 100644 --- a/cpp/arduino/avr/pgmspace.h +++ b/cpp/arduino/avr/pgmspace.h @@ -14,8 +14,10 @@ out = externs.map {|l| l.split("(")[0].split(" ")[-1].gsub("*", "") }.uniq out.each { |l| puts d(l) } */ -#include #include +#include +#include +#include #define PROGMEM @@ -27,6 +29,11 @@ out.each { |l| puts d(l) } #define PGM_VOID_P const void * #endif +// These are normally 32-bit, but here use (u)intptr_t to ensure a pointer can +// always be safely cast to these types. +typedef intptr_t int_farptr_t; +typedef uintptr_t uint_farptr_t; + // everything's a no-op #define PSTR(s) ((const char *)(s)) @@ -34,13 +41,13 @@ out.each { |l| puts d(l) } #define pgm_read_word_near(address_short) (* (const uint16_t *) (address_short) ) #define pgm_read_dword_near(address_short) (* (const uint32_t *) (address_short) ) #define pgm_read_float_near(address_short) (* (const float *) (address_short) ) -#define pgm_read_ptr_near(address_short) (* (const void *) (address_short) ) +#define pgm_read_ptr_near(address_short) (* (const void **) (address_short) ) #define pgm_read_byte_far(address_long) (* (const uint8_t *) (address_long) ) #define pgm_read_word_far(address_long) (* (const uint16_t *) (address_long) ) #define pgm_read_dword_far(address_long) (* (const uint32_t *) (address_long) ) #define pgm_read_float_far(address_long) (* (const float *) (address_long) ) -#define pgm_read_ptr_far(address_long) (* (const void *) (address_long) ) +#define pgm_read_ptr_far(address_long) (* (const void **) (address_long) ) #define pgm_read_byte(address_short) pgm_read_byte_near(address_short) #define pgm_read_word(address_short) pgm_read_word_near(address_short) @@ -50,46 +57,69 @@ out.each { |l| puts d(l) } #define pgm_get_far_address(var) ( (uint_farptr_t) (&(var)) ) -#define memchr_P(...) ::memchr(__VA_ARGS__) -#define memcmp_P(...) ::memcmp(__VA_ARGS__) -#define memccpy_P(...) ::memccpy(__VA_ARGS__) -#define memcpy_P(...) ::memcpy(__VA_ARGS__) -#define memmem_P(...) ::memmem(__VA_ARGS__) -#define memrchr_P(...) ::memrchr(__VA_ARGS__) -#define strcat_P(...) ::strcat(__VA_ARGS__) -#define strchr_P(...) ::strchr(__VA_ARGS__) -#define strchrnul_P(...) ::strchrnul(__VA_ARGS__) -#define strcmp_P(...) ::strcmp(__VA_ARGS__) -#define strcpy_P(...) ::strcpy(__VA_ARGS__) -#define strcasecmp_P(...) ::strcasecmp(__VA_ARGS__) -#define strcasestr_P(...) ::strcasestr(__VA_ARGS__) -#define strcspn_P(...) ::strcspn(__VA_ARGS__) -#define strlcat_P(...) ::strlcat(__VA_ARGS__) -#define strlcpy_P(...) ::strlcpy(__VA_ARGS__) -#define strnlen_P(...) ::strnlen(__VA_ARGS__) -#define strncmp_P(...) ::strncmp(__VA_ARGS__) -#define strncasecmp_P(...) ::strncasecmp(__VA_ARGS__) -#define strncat_P(...) ::strncat(__VA_ARGS__) -#define strncpy_P(...) ::strncpy(__VA_ARGS__) -#define strpbrk_P(...) ::strpbrk(__VA_ARGS__) -#define strrchr_P(...) ::strrchr(__VA_ARGS__) -#define strsep_P(...) ::strsep(__VA_ARGS__) -#define strspn_P(...) ::strspn(__VA_ARGS__) -#define strstr_P(...) ::strstr(__VA_ARGS__) -#define strtok_P(...) ::strtok(__VA_ARGS__) -#define strtok_P(...) ::strtok(__VA_ARGS__) -#define strlen_P(...) ::strlen(__VA_ARGS__) -#define strnlen_P(...) ::strnlen(__VA_ARGS__) -#define memcpy_P(...) ::memcpy(__VA_ARGS__) -#define strcpy_P(...) ::strcpy(__VA_ARGS__) -#define strncpy_P(...) ::strncpy(__VA_ARGS__) -#define strcat_P(...) ::strcat(__VA_ARGS__) -#define strlcat_P(...) ::strlcat(__VA_ARGS__) -#define strncat_P(...) ::strncat(__VA_ARGS__) -#define strcmp_P(...) ::strcmp(__VA_ARGS__) -#define strncmp_P(...) ::strncmp(__VA_ARGS__) -#define strcasecmp_P(...) ::strcasecmp(__VA_ARGS__) -#define strncasecmp_P(...) ::strncasecmp(__VA_ARGS__) -#define strstr_P(...) ::strstr(__VA_ARGS__) -#define strlcpy_P(...) ::strlcpy(__VA_ARGS__) -#define memcmp_P(...) ::memcmp(__VA_ARGS__) +inline const void * memchr_P(const void *s, int val, size_t len) { return memchr(s, val, len); } +inline int memcmp_P(const void *s1, const void *s2, size_t len) { return memcmp(s1, s2, len); } +inline void *memcpy_P(void *dest, const void *src, size_t n) { return memcpy(dest, src, n); } +inline char *strcat_P(char *dest, const char *src) { return strcat(dest, src); } +inline const char *strchr_P(const char *s, int val) { return strchr(s, val); } +inline int strcmp_P(const char *s1, const char *s2) { return strcmp(s1, s2); } +inline char *strcpy_P(char *dest, const char *src) { return strcpy(dest, src); } +inline size_t strcspn_P(const char *s, const char *reject) { return strcspn(s, reject); } +// strlcat and strlcpy are AVR-specific and not entirely trivial to reimplement using strncat it seems +//inline size_t strlcat_P(char *dst, const char *src, size_t siz) { return strlcat(dst, src, siz); } +//inline size_t strlcpy_P(char *dst, const char *src, size_t siz) { return strlcpy(dst, src, siz); } +//inline size_t strlcat_PF(char *dst, uint_farptr_t src, size_t n) { return strlcat(dst, (const char*)src, n); } +//inline size_t strlcpy_PF(char *dst, uint_farptr_t src, size_t siz) { return strlcpy(dst, (const char*)src, siz); } +inline int strncmp_P(const char *s1, const char *s2, size_t n) { return strncmp(s1, s2, n); } +inline char *strncat_P(char *dest, const char *src, size_t len) { return strncat(dest, src, len); } +inline char *strncpy_P(char *dest, const char *src, size_t n) { return strncpy(dest, src, n); } +inline char *strpbrk_P(const char *s, const char *accept) { return (char*)strpbrk(s, accept); } +inline const char *strrchr_P(const char *s, int val) { return strrchr(s, val); } +inline size_t strspn_P(const char *s, const char *accept) { return strspn(s, accept); } +inline char *strstr_P(const char *s1, const char *s2) { return (char*)strstr(s1, s2); } +inline char *strtok_P(char *s, const char * delim) { return strtok(s, delim); } +inline size_t strlen_PF(uint_farptr_t s) { return strlen((char*)s); } +inline void *memcpy_PF(void *dest, uint_farptr_t src, size_t n) { return memcpy(dest, (const char*)src, n); } +inline char *strcpy_PF(char *dst, uint_farptr_t src) { return strcpy(dst, (const char*)src); } +inline char *strncpy_PF(char *dst, uint_farptr_t src, size_t n) { return strncpy(dst, (const char*)src, n); } +inline char *strcat_PF(char *dst, uint_farptr_t src) { return strcat(dst, (const char*)src); } +inline char *strncat_PF(char *dst, uint_farptr_t src, size_t n) { return strncat(dst, (const char*)src, n); } +inline int strcmp_PF(const char *s1, uint_farptr_t s2) { return strcmp(s1, (const char*)s2); } +inline int strncmp_PF(const char *s1, uint_farptr_t s2, size_t n) { return strncmp(s1, (const char*)s2, n); } +inline char *strstr_PF(const char *s1, uint_farptr_t s2) { return (char*)strstr(s1, (const char*)s2); } +inline int memcmp_PF(const void *s1, uint_farptr_t s2, size_t len) { return memcmp(s1, (const char*)s2, len); } +inline size_t strlen_P(const char *src) { return strlen(src); } + +// TODO: These functions cannot be found on the CYGWIN test build for +// some reason, so disable them for now. Most of these are less common +// and/or GNU-specific addons anyway +//inline void *memccpy_P(void *dest, const void *src, int val, size_t len) { return memccpy(dest, src, val, len); } +//inline void *memmem_P(const void *s1, size_t len1, const void *s2, size_t len2) { return memmem(s1, len1, s2, len2); } +//inline const void *memrchr_P(const void *src, int val, size_t len) { return memrchr(src, val, len); } +//inline const char *strchrnul_P(const char *s, int c) { return strchrnul(s, c); } +//inline int strcasecmp_P(const char *s1, const char *s2) { return strcasecmp(s1, s2); } +//inline char *strcasestr_P(const char *s1, const char *s2) { return (char*)strcasestr(s1, s2); } +//inline int strncasecmp_P(const char *s1, const char *s2, size_t n) { return strncasecmp(s1, s2, n); } +//inline char *strsep_P(char **sp, const char *delim) { return strsep(sp, delim); } +//inline char *strtok_r_P(char *string, const char *delim, char **last) { return strtok_r(string, delim, last); } +//inline int strcasecmp_PF(const char *s1, uint_farptr_t s2) { return strcasecmp(s1, (const char*)s2); } +//inline int strncasecmp_PF(const char *s1, uint_farptr_t s2, size_t n) { return strncasecmp(s1, (const char*)s2, n); } +//inline size_t strnlen_P(uint_farptr_t s, size_t len) { return strnlen((char*)s, len); } + +// These are normally defined by stdio.h on AVR, but we cannot override that +// include file (at least not without no longer being able to include the +// original as well), so just define these here. It seems likely that any +// sketch that uses these progmem-stdio functions will also include pgmspace.h +inline int vfprintf_P(FILE *stream, const char *__fmt, va_list __ap) { return vfprintf(stream, __fmt, __ap); } +inline int printf_P(const char *__fmt, ...) { va_list args; va_start(args, __fmt); return vprintf(__fmt, args); va_end(args); } +inline int sprintf_P(char *s, const char *__fmt, ...) { va_list args; va_start(args, __fmt); return sprintf(s, __fmt, args); va_end(args); } +inline int snprintf_P(char *s, size_t __n, const char *__fmt, ...) { va_list args; va_start(args, __fmt); return vsnprintf(s, __n, __fmt, args); va_end(args); } +inline int vsprintf_P(char *s, const char *__fmt, va_list ap) { return vsprintf(s, __fmt, ap); } +inline int vsnprintf_P(char *s, size_t __n, const char *__fmt, va_list ap) { return vsnprintf(s, __n, __fmt, ap); } +inline int fprintf_P(FILE *stream, const char *__fmt, ...) { va_list args; va_start(args, __fmt); return vfprintf(stream, __fmt, args); va_end(args); } +inline int fputs_P(const char *str, FILE *__stream) { return fputs(str, __stream); } +inline int puts_P(const char *str) { return puts(str); } +inline int vfscanf_P(FILE *stream, const char *__fmt, va_list __ap) { return vfscanf(stream, __fmt, __ap); } +inline int fscanf_P(FILE *stream, const char *__fmt, ...) { va_list args; va_start(args, __fmt); return vfscanf(stream, __fmt, args); va_end(args); } +inline int scanf_P(const char *__fmt, ...) { va_list args; va_start(args, __fmt); return vscanf(__fmt, args); va_end(args); } +inline int sscanf_P(const char *buf, const char *__fmt, ...) { va_list args; va_start(args, __fmt); return vsscanf(buf, __fmt, args); va_end(args); } diff --git a/exe/arduino_ci.rb b/exe/arduino_ci.rb index 12ab224e..4ea2d614 100644 --- a/exe/arduino_ci.rb +++ b/exe/arduino_ci.rb @@ -29,7 +29,12 @@ def self.parse(options) output_options[:skip_unittests] = p end - opts.on("--skip-compilation", "Don't compile example sketches") do |p| + opts.on("--skip-compilation", "Don't compile example sketches (deprecated)") do |p| + puts "The option --skip-compilation has been deprecated in favor of --skip-examples-compilation" + output_options[:skip_compilation] = p + end + + opts.on("--skip-examples-compilation", "Don't compile example sketches") do |p| output_options[:skip_compilation] = p end @@ -205,6 +210,8 @@ def perform_unit_tests(file_config) all_platform_info = {} config.platforms_to_unittest.each { |p| all_platform_info[p] = assured_platform("unittest", p, config) } + inform("Library conforms to Arduino library specification") { cpp_library.one_point_five? ? "1.5" : "1.0" } + # iterate boards / tests if !cpp_library.tests_dir.exist? inform_multiline("Skipping unit tests; no tests dir at #{cpp_library.tests_dir}") do @@ -227,7 +234,7 @@ def perform_unit_tests(file_config) config.allowable_unittest_files(cpp_library.test_files).each do |unittest_path| unittest_name = unittest_path.basename.to_s compilers.each do |gcc_binary| - attempt_multiline("Unit testing #{unittest_name} with #{gcc_binary}") do + attempt_multiline("Unit testing #{unittest_name} with #{gcc_binary} for #{p}") do exe = cpp_library.build_for_test_with_configuration( unittest_path, config.aux_libraries_for_unittest, diff --git a/lib/arduino_ci.rb b/lib/arduino_ci.rb index 14280084..344a1463 100644 --- a/lib/arduino_ci.rb +++ b/lib/arduino_ci.rb @@ -2,6 +2,7 @@ require "arduino_ci/arduino_installation" require "arduino_ci/cpp_library" require "arduino_ci/ci_config" +require "arduino_ci/library_properties" # ArduinoCI contains classes for automated testing of Arduino code on the command line # @author Ian Katz diff --git a/lib/arduino_ci/arduino_installation.rb b/lib/arduino_ci/arduino_installation.rb index 5dfc5c8b..9ca32ada 100644 --- a/lib/arduino_ci/arduino_installation.rb +++ b/lib/arduino_ci/arduino_installation.rb @@ -110,11 +110,11 @@ def autolocate!(output = $stdout) # Forcibly install Arduino from the web # @return [bool] Whether the command succeeded def force_install(output = $stdout, version = DESIRED_ARDUINO_IDE_VERSION) - worker_class = case Host.os - when :osx then ArduinoDownloaderOSX - when :windows then ArduinoDownloaderWindows - when :linux then ArduinoDownloaderLinux - end + worker_class = case Host.os + when :osx then ArduinoDownloaderOSX + when :windows then ArduinoDownloaderWindows + when :linux then ArduinoDownloaderLinux + end worker = worker_class.new(version, output) worker.execute end diff --git a/lib/arduino_ci/cpp_library.rb b/lib/arduino_ci/cpp_library.rb index 42323be2..ea3d62eb 100644 --- a/lib/arduino_ci/cpp_library.rb +++ b/lib/arduino_ci/cpp_library.rb @@ -55,6 +55,32 @@ def initialize(base_dir, arduino_lib_dir, exclude_dirs) @vendor_bundle_cache = nil end + # The expected path to the library.properties file (i.e. even if it does not exist) + # @return [Pathname] + def library_properties_path + @base_dir + "library.properties" + end + + # Whether library.properties definitions for this library exist + # @return [bool] + def library_properties? + lib_props = library_properties_path + lib_props.exist? && lib_props.file? + end + + # Decide whether this is a 1.5-compatible library + # + # according to https://arduino.github.io/arduino-cli/latest/library-specification + # + # Should match logic from https://github.com/arduino/arduino-cli/blob/master/arduino/libraries/loader.go + # @return [bool] + def one_point_five? + return false unless library_properties? + + src_dir = (@base_dir + "src") + src_dir.exist? && src_dir.directory? + end + # Guess whether a file is part of the vendor bundle (indicating we should ignore it). # # A safe way to do this seems to be to check whether any of the installed gems @@ -110,6 +136,8 @@ def vendor_bundle?(path) # @param path [Pathname] The path to check # @return [bool] def in_tests_dir?(path) + return false unless tests_dir.exist? + tests_dir_aliases = [tests_dir, tests_dir.realpath] # we could do this but some rubies don't return an enumerator for ascend # path.ascend.any? { |part| tests_dir_aliases.include?(part) } @@ -150,43 +178,92 @@ def libasan?(gcc_binary) @has_libasan_cache[gcc_binary] end + # Library properties + def library_properties + return nil unless library_properties? + + LibraryProperties.new(library_properties_path) + end + + # Get a list of all dependencies as defined in library.properties + # @return [Array] The library names of the dependencies (not the paths) + def arduino_library_dependencies + return nil unless library_properties? + + library_properties.depends + end + # Get a list of all CPP source files in a directory and its subdirectories # @param some_dir [Pathname] The directory in which to begin the search + # @param extensions [Array] The set of allowable file extensions # @return [Array] The paths of the found files - def cpp_files_in(some_dir) + def code_files_in(some_dir, extensions) raise ArgumentError, 'some_dir is not a Pathname' unless some_dir.is_a? Pathname return [] unless some_dir.exist? && some_dir.directory? - real = some_dir.realpath - files = Find.find(real).map { |p| Pathname.new(p) }.reject(&:directory?) - cpp = files.select { |path| CPP_EXTENSIONS.include?(path.extname.downcase) } + files = some_dir.realpath.children.reject(&:directory?) + cpp = files.select { |path| extensions.include?(path.extname.downcase) } not_hidden = cpp.reject { |path| path.basename.to_s.start_with?(".") } not_hidden.sort_by(&:to_s) end + # Get a list of all CPP source files in a directory and its subdirectories + # @param some_dir [Pathname] The directory in which to begin the search + # @param extensions [Array] The set of allowable file extensions + # @return [Array] The paths of the found files + def code_files_in_recursive(some_dir, extensions) + raise ArgumentError, 'some_dir is not a Pathname' unless some_dir.is_a? Pathname + return [] unless some_dir.exist? && some_dir.directory? + + real = some_dir.realpath + Find.find(real).map { |p| Pathname.new(p) }.select(&:directory?).map { |d| code_files_in(d, extensions) }.flatten + end + + # Header files that are part of the project library under test + # @return [Array] + def header_files + ret = if one_point_five? + code_files_in_recursive(@base_dir + "src", HPP_EXTENSIONS) + else + [@base_dir, @base_dir + "utility"].map { |d| code_files_in(d, HPP_EXTENSIONS) }.flatten + end + + # note to future troubleshooter: some of these tests may not be relevant, but at the moment at + # least some of them are tied to existing features + ret.reject { |p| vendor_bundle?(p) || in_tests_dir?(p) || in_exclude_dir?(p) } + end + # CPP files that are part of the project library under test # @return [Array] def cpp_files - cpp_files_in(@base_dir).reject { |p| vendor_bundle?(p) || in_tests_dir?(p) || in_exclude_dir?(p) } + ret = if one_point_five? + code_files_in_recursive(@base_dir + "src", CPP_EXTENSIONS) + else + [@base_dir, @base_dir + "utility"].map { |d| code_files_in(d, CPP_EXTENSIONS) }.flatten + end + + # note to future troubleshooter: some of these tests may not be relevant, but at the moment at + # least some of them are tied to existing features + ret.reject { |p| vendor_bundle?(p) || in_tests_dir?(p) || in_exclude_dir?(p) } end # CPP files that are part of the arduino mock library we're providing # @return [Array] def cpp_files_arduino - cpp_files_in(ARDUINO_HEADER_DIR) + code_files_in(ARDUINO_HEADER_DIR, CPP_EXTENSIONS) end # CPP files that are part of the unit test library we're providing # @return [Array] def cpp_files_unittest - cpp_files_in(UNITTEST_HEADER_DIR) + code_files_in(UNITTEST_HEADER_DIR, CPP_EXTENSIONS) end # CPP files that are part of the 3rd-party libraries we're including # @param [Array] aux_libraries # @return [Array] def cpp_files_libraries(aux_libraries) - arduino_library_src_dirs(aux_libraries).map { |d| cpp_files_in(d) }.flatten.uniq + arduino_library_src_dirs(aux_libraries).map { |d| code_files_in(d, CPP_EXTENSIONS) }.flatten.uniq end # Returns the Pathnames for all paths to exclude from testing and compilation @@ -204,15 +281,13 @@ def tests_dir # The files provided by the user that contain unit tests # @return [Array] def test_files - cpp_files_in(tests_dir) + code_files_in(tests_dir, CPP_EXTENSIONS) end # Find all directories in the project library that include C++ header files # @return [Array] def header_dirs - real = @base_dir.realpath - all_files = Find.find(real).map { |f| Pathname.new(f) }.reject(&:directory?) - unbundled = all_files.reject { |path| vendor_bundle?(path) } + unbundled = header_files.reject { |path| vendor_bundle?(path) } unexcluded = unbundled.reject { |path| in_exclude_dir?(path) } files = unexcluded.select { |path| HPP_EXTENSIONS.include?(path.extname.downcase) } files.map(&:dirname).uniq @@ -236,23 +311,19 @@ def gcc_version(gcc_binary) @last_err end - # Arduino library directories containing sources + # Arduino library directories containing sources -- only those of the dependencies # @return [Array] def arduino_library_src_dirs(aux_libraries) # Pull in all possible places that headers could live, according to the spec: # https://github.com/arduino/Arduino/wiki/Arduino-IDE-1.5:-Library-specification - # TODO: be smart and implement library spec (library.properties, etc)? - subdirs = ["", "src", "utility"] - all_aux_include_dirs_nested = aux_libraries.map do |libdir| - # library manager coerces spaces in package names to underscores - # see https://github.com/Arduino-CI/arduino_ci/issues/132#issuecomment-518857059 - legal_libdir = libdir.tr(" ", "_") - subdirs.map { |subdir| Pathname.new(@arduino_lib_dir) + legal_libdir + subdir } - end - all_aux_include_dirs_nested.flatten.select(&:exist?).select(&:directory?) + + aux_libraries.map { |d| self.class.new(@arduino_lib_dir + d, @arduino_lib_dir, @exclude_dirs).header_dirs }.flatten.uniq end # GCC command line arguments for including aux libraries + # + # This function recursively collects the library directores of the dependencies + # # @param aux_libraries [Array] The external Arduino libraries required by this project # @return [Array] The GCC command-line flags necessary to include those libraries def include_args(aux_libraries) @@ -315,6 +386,9 @@ def test_args(aux_libraries, ci_gcc_config) end # build a file for running a test of the given unit test file + # + # The dependent libraries configuration is appended with data from library.properties internal to the library under test + # # @param test_file [Pathname] The path to the file containing the unit tests # @param aux_libraries [Array] The external Arduino libraries required by this project # @param ci_gcc_config [Hash] The GCC config object @@ -324,7 +398,7 @@ def build_for_test_with_configuration(test_file, aux_libraries, gcc_binary, ci_g executable = Pathname.new("unittest_#{base}.bin").expand_path File.delete(executable) if File.exist?(executable) arg_sets = [] - arg_sets << ["-std=c++0x", "-o", executable.to_s, "-DARDUINO=100", "-D__AVR__"] + arg_sets << ["-std=c++0x", "-o", executable.to_s, "-DARDUINO=100"] if libasan?(gcc_binary) arg_sets << [ # Stuff to help with dynamic memory mishandling "-g", "-O1", @@ -333,8 +407,12 @@ def build_for_test_with_configuration(test_file, aux_libraries, gcc_binary, ci_g "-fsanitize=address" ] end - arg_sets << test_args(aux_libraries, ci_gcc_config) - arg_sets << cpp_files_libraries(aux_libraries).map(&:to_s) + + # combine library.properties defs (if existing) with config file. + # TODO: as much as I'd like to rely only on the properties file(s), I think that would prevent testing 1.0-spec libs + full_aux_libraries = arduino_library_dependencies.nil? ? aux_libraries : aux_libaries + arduino_library_dependencies + arg_sets << test_args(full_aux_libraries, ci_gcc_config) + arg_sets << cpp_files_libraries(full_aux_libraries).map(&:to_s) arg_sets << [test_file.to_s] args = arg_sets.flatten(1) return nil unless run_gcc(gcc_binary, *args) @@ -343,14 +421,31 @@ def build_for_test_with_configuration(test_file, aux_libraries, gcc_binary, ci_g executable end + # print any found stack dumps + # @param executable [Pathname] the path to the test file + def print_stack_dump(executable) + possible_dumpfiles = [ + executable.sub_ext(executable.extname + ".stackdump") + ] + possible_dumpfiles.select(&:exist?).each do |dump| + puts "========== Stack dump from #{dump}:" + File.foreach(dump) { |line| print " #{line}" } + end + end + # run a test file - # @param [Pathname] the path to the test file + # @param executable [Pathname] the path to the test file # @return [bool] whether all tests were successful def run_test_file(executable) @last_cmd = executable @last_out = "" @last_err = "" - Host.run_and_output(executable.to_s.shellescape) + ret = Host.run_and_output(executable.to_s.shellescape) + + # print any stack traces found during a failure + print_stack_dump(executable) unless ret + + ret end end diff --git a/lib/arduino_ci/library_properties.rb b/lib/arduino_ci/library_properties.rb new file mode 100644 index 00000000..1a080713 --- /dev/null +++ b/lib/arduino_ci/library_properties.rb @@ -0,0 +1,86 @@ +module ArduinoCI + + # Information about an Arduino library package, as specified by the library.properties file + # + # See https://arduino.github.io/arduino-cli/library-specification/#libraryproperties-file-format + class LibraryProperties + + # @return [Hash] The properties file parsed as a hash + attr_reader :fields + + # @param path [Pathname] The path to the library.properties file + def initialize(path) + @fields = {} + File.foreach(path) do |line| + parts = line.split("=", 2) + @fields[parts[0]] = parts[1].chomp unless parts.empty? + end + end + + # Enable a shortcut syntax for library property accessors, in the style of `attr_accessor` metaprogramming. + # This is used to create a named field pointing to a specific property in the file, optionally applying + # a specific formatting function. + # + # The formatting function MUST be a static method on this class. This is a limitation caused by the desire + # to both (1) expose the formatters outside this class, and (2) use them for metaprogramming without the + # having to name the entire function. field_reader is a static method, so if not for the fact that + # `self.class.methods.include? formatter` fails to work for class methods in this context (unlike + # `self.methods.include?`, which properly finds instance methods), I would allow either one and just + # conditionally `define_method` the proper definition + # + # @param name [String] What the accessor will be called + # @param field_num [Integer] The name of the key of the property + # @param formatter [Symbol] The symbol for the formatting function to apply to the field (optional) + # @return [void] + # @macro [attach] field_reader + # @!attribute [r] $1 + # @return property $2 of the library.properties file, formatted with the function {$3} + def self.field_reader(name, formatter = nil) + key = name.to_s + if formatter.nil? + define_method(name) { @fields[key] } + else + define_method(name) { @fields.key?(key) ? self.class.send(formatter.to_sym, @fields[key]) : nil } + end + end + + # Parse a value as a comma-separated array + # @param input [String] + # @return [Array] The individual values + def self._csv(input) + input.split(",").map(&:strip) + end + + # Parse a value as a boolean + # @param input [String] + # @return [Array] The individual values + def self._bool(input) + input == "true" # no indication given in the docs that anything but lowercase "true" indicates boolean true. + end + + field_reader :name + field_reader :version + field_reader :author, :_csv + field_reader :maintainer + field_reader :sentence + field_reader :paragraph + field_reader :category + field_reader :url + field_reader :architectures, :_csv + field_reader :depends, :_csv + field_reader :dot_a_linkage, :_bool + field_reader :includes, :_csv + field_reader :precompiled, :_bool + field_reader :ldflags, :_csv + + # The value of sentence always will be prepended, so you should start by writing the second sentence here + # + # (according to the docs) + # @return [String] the sentence and paragraph together + def full_paragraph + [sentence, paragraph].join(" ") + end + + end + +end diff --git a/misc/default.yml b/misc/default.yml index 67d7f87f..1bef2e27 100644 --- a/misc/default.yml +++ b/misc/default.yml @@ -22,7 +22,10 @@ platforms: gcc: features: defines: + - __AVR__ - __AVR_ATmega328P__ + - ARDUINO_ARCH_AVR + - ARDUINO_AVR_UNO warnings: flags: due: @@ -31,7 +34,10 @@ platforms: gcc: features: defines: - - __AVR_ATmega328__ + - __SAM3X8E__ + - ARDUINO_ARCH_SAM + - ARDUINO_SAM_DUE + - NUM_SERIAL_PORTS=4 warnings: flags: zero: @@ -40,8 +46,11 @@ platforms: gcc: features: defines: - - __SAMD21G18A__ - - ARDUINO_SAMD_ZERO + - __SAMD21G18A__ + - ARDUINO_ARCH_SAMD + - ARDUINO_SAMD_ZERO + # This also has SerialUSB, which is not included here. + - NUM_SERIAL_PORTS=2 warnings: flags: esp32: @@ -50,6 +59,10 @@ platforms: gcc: features: defines: + - ESP32 + - ARDUINO_ARCH_ESP32 + - ARDUINO_FEATHER_ESP32 + - NUM_SERIAL_PORTS=3 warnings: flags: esp8266: @@ -58,6 +71,10 @@ platforms: gcc: features: defines: + - ESP8266 + - ARDUINO_ARCH_ESP8266 + - ARDUINO_ESP8266_ESP12 + - NUM_SERIAL_PORTS=2 warnings: flags: leonardo: @@ -66,7 +83,10 @@ platforms: gcc: features: defines: + - __AVR__ - __AVR_ATmega32U4__ + - ARDUINO_ARCH_AVR + - ARDUINO_AVR_LEONARDO warnings: flags: trinket: @@ -75,6 +95,10 @@ platforms: gcc: features: defines: + - __AVR__ + - __AVR_ATtiny85__ + - ARDUINO_ARCH_AVR + - ARDUINO_AVR_TRINKET5 warnings: flags: gemma: @@ -83,6 +107,10 @@ platforms: gcc: features: defines: + - __AVR__ + - __AVR_ATtiny85__ + - ARDUINO_ARCH_AVR + - ARDUINO_AVR_GEMMA warnings: flags: m4: @@ -91,6 +119,12 @@ platforms: gcc: features: defines: + - __SAMD51__ + - __SAMD51J19A__ + - ARDUINO_ARCH_SAMD + - ARDUINO_METRO_M4 + # Serial is actually USB virtual serial, not HardwareSerial + - NUM_SERIAL_PORTS=2 warnings: flags: mega2560: @@ -99,7 +133,10 @@ platforms: gcc: features: defines: + - __AVR__ - __AVR_ATmega2560__ + - ARDUINO_ARCH_AVR + - ARDUINO_AVR_MEGA2560 warnings: flags: cplayClassic: @@ -108,6 +145,10 @@ platforms: gcc: features: defines: + - __AVR__ + - __AVR_ATmega32U4__ + - ARDUINO_ARCH_AVR + - ARDUINO_AVR_CIRCUITPLAY warnings: flags: cplayExpress: @@ -116,6 +157,11 @@ platforms: gcc: features: defines: + - __SAMD21G18A__ + - ARDUINO_ARCH_SAMD + - ARDUINO_SAMD_CIRCUITPLAYGROUND_EXPRESS + # Serial is actually an alias of SerialUSB, not a HardwareSerial + - NUM_SERIAL_PORTS=2 warnings: flags: diff --git a/spec/cpp_library_spec.rb b/spec/cpp_library_spec.rb index 40b2407a..6a25468f 100644 --- a/spec/cpp_library_spec.rb +++ b/spec/cpp_library_spec.rb @@ -10,49 +10,197 @@ def get_relative_dir(sampleprojects_tests_dir) sampleprojects_tests_dir.relative_path_from(base_dir) end -RSpec.describe ArduinoCI::CppLibrary do - next if skip_ruby_tests - cpp_lib_path = sampleproj_path + "DoSomething" - cpp_library = ArduinoCI::CppLibrary.new(cpp_lib_path, Pathname.new("my_fake_arduino_lib_dir"), []) - context "cpp_files" do - it "finds cpp files in directory" do - dosomething_cpp_files = [Pathname.new("DoSomething") + "do-something.cpp"] - relative_paths = cpp_library.cpp_files.map { |f| get_relative_dir(f) } - expect(relative_paths).to match_array(dosomething_cpp_files) + +RSpec.describe "ExcludeSomething C++" do + next if skip_cpp_tests + + cpp_lib_path = sampleproj_path + "ExcludeSomething" + context "without excludes" do + cpp_library = ArduinoCI::CppLibrary.new(cpp_lib_path, + Pathname.new("my_fake_arduino_lib_dir"), + []) + context "cpp_files" do + it "finds cpp files in directory" do + excludesomething_cpp_files = [ + Pathname.new("ExcludeSomething/src/exclude-something.cpp"), + Pathname.new("ExcludeSomething/src/excludeThis/exclude-this.cpp") + ] + relative_paths = cpp_library.cpp_files.map { |f| get_relative_dir(f) } + expect(relative_paths).to match_array(excludesomething_cpp_files) + end end - end - context "header_dirs" do - it "finds directories containing h files" do - dosomething_header_dirs = [Pathname.new("DoSomething")] - relative_paths = cpp_library.header_dirs.map { |f| get_relative_dir(f) } - expect(relative_paths).to match_array(dosomething_header_dirs) + context "unit tests" do + it "can't build due to files that should have been excluded" do + config = ArduinoCI::CIConfig.default.from_example(cpp_lib_path) + path = config.allowable_unittest_files(cpp_library.test_files).first + compiler = config.compilers_to_use.first + result = cpp_library.build_for_test_with_configuration(path, + [], + compiler, + config.gcc_config("uno")) + expect(result).to be nil + end end end - context "tests_dir" do - it "locates the tests directory" do - # since we don't know where the CI system will install this stuff, - # we need to go looking for a relative path to the SampleProjects directory - # just to get our "expected" value - relative_path = get_relative_dir(cpp_library.tests_dir) - expect(relative_path.to_s).to eq("DoSomething/test") + context "with excludes" do + cpp_library = ArduinoCI::CppLibrary.new(cpp_lib_path, + Pathname.new("my_fake_arduino_lib_dir"), + ["src/excludeThis"].map(&Pathname.method(:new))) + context "cpp_files" do + it "finds cpp files in directory" do + excludesomething_cpp_files = [ + Pathname.new("ExcludeSomething/src/exclude-something.cpp") + ] + relative_paths = cpp_library.cpp_files.map { |f| get_relative_dir(f) } + expect(relative_paths).to match_array(excludesomething_cpp_files) + end end + end - context "test_files" do - it "finds cpp files in directory" do - dosomething_test_files = [ +end + +RSpec.describe ArduinoCI::CppLibrary do + next if skip_ruby_tests + + answers = { + DoSomething: { + one_five: false, + cpp_files: [Pathname.new("DoSomething") + "do-something.cpp"], + cpp_files_libraries: [], + header_dirs: [Pathname.new("DoSomething")], + arduino_library_src_dirs: [], + test_files: [ "DoSomething/test/good-null.cpp", "DoSomething/test/good-library.cpp", "DoSomething/test/bad-null.cpp", ].map { |f| Pathname.new(f) } - relative_paths = cpp_library.test_files.map { |f| get_relative_dir(f) } - expect(relative_paths).to match_array(dosomething_test_files) + }, + OnePointOhDummy: { + one_five: false, + cpp_files: [ + "OnePointOhDummy/YesBase.cpp", + "OnePointOhDummy/utility/YesUtil.cpp", + ].map { |f| Pathname.new(f) }, + cpp_files_libraries: [], + header_dirs: [ + "OnePointOhDummy", + "OnePointOhDummy/utility" + ].map { |f| Pathname.new(f) }, + arduino_library_src_dirs: [], + test_files: [ + "OnePointOhDummy/test/null.cpp", + ].map { |f| Pathname.new(f) } + }, + OnePointFiveMalformed: { + one_five: false, + cpp_files: [ + "OnePointFiveMalformed/YesBase.cpp", + "OnePointFiveMalformed/utility/YesUtil.cpp", + ].map { |f| Pathname.new(f) }, + cpp_files_libraries: [], + header_dirs: [ + "OnePointFiveMalformed", + "OnePointFiveMalformed/utility" + ].map { |f| Pathname.new(f) }, + arduino_library_src_dirs: [], + test_files: [] + }, + OnePointFiveDummy: { + one_five: true, + cpp_files: [ + "OnePointFiveDummy/src/YesSrc.cpp", + "OnePointFiveDummy/src/subdir/YesSubdir.cpp", + ].map { |f| Pathname.new(f) }, + cpp_files_libraries: [], + header_dirs: [ + "OnePointFiveDummy/src", + "OnePointFiveDummy/src/subdir", + ].map { |f| Pathname.new(f) }, + arduino_library_src_dirs: [], + test_files: [ + "OnePointFiveDummy/test/null.cpp", + ].map { |f| Pathname.new(f) } + } + } + + # easier to construct this one from the other test cases + answers[:DependOnSomething] = { + one_five: true, + cpp_files: ["DependOnSomething/src/YesDeps.cpp"].map { |f| Pathname.new(f) }, + cpp_files_libraries: answers[:OnePointOhDummy][:cpp_files] + answers[:OnePointFiveDummy][:cpp_files], + header_dirs: ["DependOnSomething/src"].map { |f| Pathname.new(f) }, # this is not recursive! + arduino_library_src_dirs: answers[:OnePointOhDummy][:header_dirs] + answers[:OnePointFiveDummy][:header_dirs], + test_files: [ + "DependOnSomething/test/null.cpp", + ].map { |f| Pathname.new(f) } + } + + answers.freeze + + answers.each do |sampleproject, expected| + context "#{sampleproject}" do + cpp_lib_path = sampleproj_path + sampleproject.to_s + cpp_library = ArduinoCI::CppLibrary.new(cpp_lib_path, sampleproj_path, []) + dependencies = cpp_library.arduino_library_dependencies.nil? ? [] : cpp_library.arduino_library_dependencies + + it "detects 1.5 format" do + expect(cpp_library.one_point_five?).to eq(expected[:one_five]) + end + + context "cpp_files" do + it "finds cpp files in directory" do + relative_paths = cpp_library.cpp_files.map { |f| get_relative_dir(f) } + expect(relative_paths.map(&:to_s)).to match_array(expected[:cpp_files].map(&:to_s)) + end + end + + context "cpp_files_libraries" do + it "finds cpp files in directories of dependencies" do + relative_paths = cpp_library.cpp_files_libraries(dependencies).map { |f| get_relative_dir(f) } + expect(relative_paths.map(&:to_s)).to match_array(expected[:cpp_files_libraries].map(&:to_s)) + end + end + + context "header_dirs" do + it "finds directories containing h files" do + relative_paths = cpp_library.header_dirs.map { |f| get_relative_dir(f) } + expect(relative_paths.map(&:to_s)).to match_array(expected[:header_dirs].map(&:to_s)) + end + end + + context "tests_dir" do + it "locates the tests directory" do + # since we don't know where the CI system will install this stuff, + # we need to go looking for a relative path to the SampleProjects directory + # just to get our "expected" value + relative_path = get_relative_dir(cpp_library.tests_dir) + expect(relative_path.to_s).to eq("#{sampleproject}/test") + end + end + + context "test_files" do + it "finds cpp files in directory" do + relative_paths = cpp_library.test_files.map { |f| get_relative_dir(f) } + expect(relative_paths.map(&:to_s)).to match_array(expected[:test_files].map(&:to_s)) + end + end + + context "arduino_library_src_dirs" do + it "finds src dirs from dependent libraries" do + # we explicitly feed in the internal dependencies + relative_paths = cpp_library.arduino_library_src_dirs(dependencies).map { |f| get_relative_dir(f) } + expect(relative_paths.map(&:to_s)).to match_array(expected[:arduino_library_src_dirs].map(&:to_s)) + end + end end end context "test" do + cpp_lib_path = sampleproj_path + "DoSomething" + cpp_library = ArduinoCI::CppLibrary.new(cpp_lib_path, Pathname.new("my_fake_arduino_lib_dir"), []) config = ArduinoCI::CIConfig.default after(:each) do |example| diff --git a/spec/library_properties_spec.rb b/spec/library_properties_spec.rb new file mode 100644 index 00000000..3c6de1ee --- /dev/null +++ b/spec/library_properties_spec.rb @@ -0,0 +1,45 @@ +require "spec_helper" + +RSpec.describe ArduinoCI::LibraryProperties do + + context "property extraction" do + library_properties = ArduinoCI::LibraryProperties.new(Pathname.new(__dir__) + "properties/example.library.properties") + + expected = { + string: { + name: "WebServer", + version: "1.0.0", + maintainer: "Cristian Maglie ", + sentence: "A library that makes coding a Webserver a breeze.", + paragraph: "Supports HTTP1.1 and you can do GET and POST.", + category: "Communication", + url: "http://example.com/", + }, + + bool: { + precompiled: true + }, + + csv: { + author: ["Cristian Maglie ", "Pippo Pluto "], + architectures: ["avr"], + includes: ["WebServer.h"], + depends: ["ArduinoHttpClient"], + }, + }.freeze + + expected.each do |atype, values| + values.each do |meth, val| + it "reads #{atype} field #{meth}" do + expect(library_properties.send(meth)).to eq(val) + end + end + end + + it "doesn't crash on nonexistent fields" do + expect(library_properties.dot_a_linkage).to be(nil) + end + end + + +end diff --git a/spec/properties/example.library.properties b/spec/properties/example.library.properties new file mode 100644 index 00000000..f0cd9bb3 --- /dev/null +++ b/spec/properties/example.library.properties @@ -0,0 +1,12 @@ +name=WebServer +version=1.0.0 +author=Cristian Maglie , Pippo Pluto +maintainer=Cristian Maglie +sentence=A library that makes coding a Webserver a breeze. +paragraph=Supports HTTP1.1 and you can do GET and POST. +category=Communication +url=http://example.com/ +architectures=avr +includes=WebServer.h +depends=ArduinoHttpClient +precompiled=true diff --git a/spec/testsomething_unittests_spec.rb b/spec/testsomething_unittests_spec.rb index 5e0f9152..4cb49541 100644 --- a/spec/testsomething_unittests_spec.rb +++ b/spec/testsomething_unittests_spec.rb @@ -10,47 +10,16 @@ def get_relative_dir(sampleprojects_tests_dir) sampleprojects_tests_dir.relative_path_from(base_dir) end -RSpec.describe "TestSomething C++ without excludes" do - next if skip_cpp_tests - cpp_lib_path = sampleproj_path + "TestSomething" - cpp_library = ArduinoCI::CppLibrary.new(cpp_lib_path, - Pathname.new("my_fake_arduino_lib_dir"), - []) - context "cpp_files" do - it "finds cpp files in directory" do - testsomething_cpp_files = [ - Pathname.new("TestSomething/test-something.cpp"), - Pathname.new("TestSomething/excludeThis/exclude-this.cpp") - ] - relative_paths = cpp_library.cpp_files.map { |f| get_relative_dir(f) } - expect(relative_paths).to match_array(testsomething_cpp_files) - end - end - - context "unit tests" do - it "can't build due to files that should have been excluded" do - config = ArduinoCI::CIConfig.default.from_example(cpp_lib_path) - path = config.allowable_unittest_files(cpp_library.test_files).first - compiler = config.compilers_to_use.first - result = cpp_library.build_for_test_with_configuration(path, - [], - compiler, - config.gcc_config("uno")) - expect(result).to be nil - end - end - -end RSpec.describe "TestSomething C++" do next if skip_cpp_tests cpp_lib_path = sampleproj_path + "TestSomething" cpp_library = ArduinoCI::CppLibrary.new(cpp_lib_path, Pathname.new("my_fake_arduino_lib_dir"), - ["excludeThis"].map(&Pathname.method(:new))) + ["src/excludeThis"].map(&Pathname.method(:new))) context "cpp_files" do it "finds cpp files in directory" do - testsomething_cpp_files = [Pathname.new("TestSomething/test-something.cpp")] + testsomething_cpp_files = [Pathname.new("TestSomething/src/test-something.cpp")] relative_paths = cpp_library.cpp_files.map { |f| get_relative_dir(f) } expect(relative_paths).to match_array(testsomething_cpp_files) end