diff --git a/.gitignore b/.gitignore index 4f89af25..45a5d7c6 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,4 @@ vendor .rspec_status # C++ stuff -arduino_ci_built.bin +*.bin diff --git a/.rubocop.yml b/.rubocop.yml index f89ea5b5..04aadfbc 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -78,3 +78,6 @@ Style/StringLiterals: Style/TrailingCommaInLiteral: Enabled: false + +Style/SymbolArray: + Enabled: false diff --git a/.travis.yml b/.travis.yml index e404d04b..8f0105cc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,9 +8,12 @@ language: ruby # - rbx # - "2.5.0" -before_install: gem install bundler -v 1.15.4 +#before_install: gem install bundler -v 1.15.4 script: + - bundle install - bundle exec rubocop --version - bundle exec rubocop -D . - bundle exec rspec - - bundle exec ci_system_check.rb + - cd SampleProjects/DoSomething + - bundle install + - bundle exec arduino_ci_remote.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index f58dd6fa..617250e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] ### Added +- Unit testing support +- Documentation for all Ruby methods - `ArduinoInstallation` class for managing lib / executable paths - `DisplayManager` class for managing Xvfb instance if needed - `ArduinoCmd` captures and caches preferences @@ -15,6 +17,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - `ArduinoCmd` installs libraries - `ArduinoCmd` selects boards (compiler preference) - `ArduinoCmd` verifies sketches +- `CppLibrary` manages GCC for unittests +- `CIConfig` manages overridable config for all testing ### Changed - `DisplayManger.with_display` doesn't `disable` if the display was enabled prior to starting the block diff --git a/README.md b/README.md index 238a9c8b..f4a10ee3 100644 --- a/README.md +++ b/README.md @@ -7,26 +7,38 @@ [Arduino CI](https://github.com/ifreecarve/arduino_ci) is a Ruby gem for executing Continuous Integration (CI) tests on an Arduino library -- both locally and as part of a service like Travis CI. -## Installation +## Installation In Your GitHub Project And Using Travis CI -Add this line to your application's Gemfile: +Add a file called `Gemfile` (no extension) to your Arduino project: ```ruby +source 'https://rubygems.org' gem 'arduino_ci' ``` -And then execute: +Next, you need this in `.travis.yml` - $ bundle +```yaml +sudo: false +language: ruby +script: + - bundle install + - bundle exec arduino_ci_remote.rb +``` + +That's literally all there is to it on the repository side. You'll need to go to https://travis-ci.org/profile/ and enable testing for your Arduino project. Once that happens, you should be all set. -Or install it yourself as: - $ gem install arduino_ci +## More Documentation +This software is in alpha. But [SampleProjects/DoSomething](SampleProjects/DoSomething) has a decent writeup and is a good bare-bones example of all the features. -## Usage +## Known Problems -TODO: Write usage instructions here, based on other TODO of writing the actual gem. +* The Arduino library is not fully mocked. +* I don't have preprocessor defines for all the Arduino board flavors +* Arduino Zero boards don't work in CI. I'm confused. +* https://github.com/ifreecarve/arduino_ci/issues ## Author @@ -37,3 +49,5 @@ This gem was written by Ian Katz (ifreecarve@gmail.com) in 2018. It's released ## See Also * [Contributing](CONTRIBUTING.md) +* [Adafruit/travis-ci-arduino](https://github.com/adafruit/travis-ci-arduino) which inspired this project +* [mmurdoch/arduinounit](https://github.com/mmurdoch/arduinounit) from which the unit test macros were adopted diff --git a/SampleProjects/DoSomething/Gemfile.lock b/SampleProjects/DoSomething/Gemfile.lock index 39057fe5..960bb950 100644 --- a/SampleProjects/DoSomething/Gemfile.lock +++ b/SampleProjects/DoSomething/Gemfile.lock @@ -2,10 +2,12 @@ PATH remote: /Users/ikatz/Development/non-wayfair/arduino_ci specs: arduino_ci (0.0.1) + os (~> 1.0) GEM remote: https://rubygems.org/ specs: + os (1.0.0) PLATFORMS ruby diff --git a/SampleProjects/DoSomething/README.md b/SampleProjects/DoSomething/README.md index 3012fec2..9b0cd5d2 100644 --- a/SampleProjects/DoSomething/README.md +++ b/SampleProjects/DoSomething/README.md @@ -1,4 +1,4 @@ -# Arduino CI and Unit Tests HOWTO [![Build Status](https://travis-ci.org/ifreecarve/arduino-ci-unit-tests.svg?branch=master)](https://travis-ci.org/ifreecarve/arduino-ci-unit-tests) +# Arduino CI and Unit Tests HOWTO This project is a template for a CI-enabled (and unit testable) Arduino project of your own. @@ -7,17 +7,105 @@ This project is a template for a CI-enabled (and unit testable) Arduino project * Travis CI * Unit tests -* Development workflow matches CI workflow - the `platformio ci` line at the bottom of `.travis.yml` can be run on your local terminal (just append the name of the file you want to compile). +* Development workflow matches CI workflow # Where The Magic Happens Here is the minimal set of files that you will need to adapt to your own project: -* `.travis.yml` - You'll need to fill in the `env` section with files relevant to your project, and list out all the `--board`s under the `script` section. -* `platformio.ini` - You'll need to add information for any architectures you plan to support. -* `library.properties` - You'll need to update the `architectures` and `includes` lines as appropriate + +### `Gemfile` Ruby gem configuration + +```ruby +source 'https://rubygems.org' +gem 'arduino_ci' +``` + +You'll need this to get access to the functionality. + +> This is different from the `Gemfile` that's included in this directory! That's for purposes of testing the ruby gem that also lives in this repository. So "do as I say, not as I do", in this case. + + +### `.travis.yml` Travis CI configuration + +At a minimum, you will need the following lines in your file: + +```yaml +language: ruby +script: + - bundle install + - bundle exec arduino_ci_remote.rb +``` + +This will install the necessary ruby gem, and run it. There are no command line arguments as of this writing, because all configuration is provided by... + +### `.arduino-ci.yaml` ArduinoCI configuration + +This file controls all aspects of building and unit testing. The (relatively-complete) defaults can be found [in the base project](../../misc/default.yaml). + +The most relevant sections for most projects will be as follows: + +```yaml +compile: + libraries: ~ + platforms: + - uno + - due + - leonardo + +unittest: + libraries: ~ + platforms: + - uno + - due + - leonardo +``` + +This does nothing but specify a list of what platforms should run each set of tests. The platforms themselves can also be defined and/or extended in the yaml file. For example, `esp8266` as shown here: + +```yaml +platforms: + esp8266: + board: esp8266:esp8266:huzzah + package: esp8266:esp8266 + gcc: + features: + defines: + warnings: + flags: +``` + +Of course, this wouldn't work by itself -- the Arduino IDE would have to know how to install the package via the board manager. So there's a section for packages too: + +```yaml +packages: + esp8266:esp8266: + url: http://arduino.esp8266.com/stable/package_esp8266com_index.json +``` + +### Unit tests in `test/` + +All `.cpp` files in the `test/` directory are assumed to contain unit tests. Each and every one will be compiled and executed on its own. + +The most basic unit test file is as follows: + +```C++ +#include +#include "../do-something.h" + +unittest(your_test_name) +{ + assertEqual(4, doSomething()); +} + +int main(int argc, char *argv[]) { + return Test::run_and_report(argc, argv); +} +``` + +This test defines one `unittest` (a macro provided by `ArduionUnitTests.h`), called `your_test_name`, which makes some assertions on the target library. The `int main` section is boilerplate. # Credits -This Arduino example was created in January 2018 by Ian Katz . +This Arduino example was created in January 2018 by Ian Katz . diff --git a/SampleProjects/DoSomething/do-something.cpp b/SampleProjects/DoSomething/do-something.cpp index 2ceefa55..8f5e178b 100644 --- a/SampleProjects/DoSomething/do-something.cpp +++ b/SampleProjects/DoSomething/do-something.cpp @@ -1,5 +1,5 @@ #include -#include +#include "do-something.h" int doSomething(void) { millis(); // this line is only here to test that we're able to refer to the builtins return 4; diff --git a/SampleProjects/DoSomething/do-something.h b/SampleProjects/DoSomething/do-something.h index f39ecdff..13322823 100644 --- a/SampleProjects/DoSomething/do-something.h +++ b/SampleProjects/DoSomething/do-something.h @@ -1,2 +1,3 @@ +#pragma once #include int doSomething(void); diff --git a/SampleProjects/DoSomething/library.properties b/SampleProjects/DoSomething/library.properties index cfa484dc..38b2967a 100644 --- a/SampleProjects/DoSomething/library.properties +++ b/SampleProjects/DoSomething/library.properties @@ -1,10 +1,10 @@ -name=arduino-ci-unit-tests +name=DoSomething version=0.1.0 author=Ian Katz maintainer=Ian Katz sentence=Arduino CI unit test example paragraph=A skeleton library demonstrating CI and unit tests category=Other -url=https://github.com/ifreecarve/arduino-ci-unit-tests +url=https://github.com/ifreecarve/arduino_ci/SampleProjects/DoSomething architectures=avr includes=do-something.h diff --git a/SampleProjects/DoSomething/test/good-library.cpp b/SampleProjects/DoSomething/test/good-library.cpp new file mode 100644 index 00000000..dbfaa6e9 --- /dev/null +++ b/SampleProjects/DoSomething/test/good-library.cpp @@ -0,0 +1,11 @@ +#include +#include "../do-something.h" + +unittest(library_does_something) +{ + assertEqual(4, doSomething()); +} + +int main(int argc, char *argv[]) { + return Test::run_and_report(argc, argv); +} diff --git a/SampleProjects/DoSomething/test/good-null.cpp b/SampleProjects/DoSomething/test/good-null.cpp new file mode 100644 index 00000000..3df30eab --- /dev/null +++ b/SampleProjects/DoSomething/test/good-null.cpp @@ -0,0 +1,24 @@ +#include + +unittest(equality_as_vars) +{ + int x = 3; + int y = 3; + int z = 3; + assertEqual(x, y); + assertEqual(x, z); +} + +unittest(equality_as_values) +{ + assertEqual(1, 1); + assertEqual(4, 4); +} + +unittest(nothing) +{ +} + +int main(int argc, char *argv[]) { + return Test::run_and_report(argc, argv); +} diff --git a/SampleProjects/README.md b/SampleProjects/README.md index 892f9af4..580ccfbb 100644 --- a/SampleProjects/README.md +++ b/SampleProjects/README.md @@ -1,4 +1,4 @@ Arduino Sample Projects ======================= -This directory contains example projects that are meant to be built with this gem. +This directory contains example projects that are meant to be built with this gem. Except "TestSomething". That one's just a beater for CI testing and includes some tests that are designed to fail (to test negative inputs). diff --git a/SampleProjects/TestSomething/Gemfile b/SampleProjects/TestSomething/Gemfile new file mode 100644 index 00000000..b2b3b1fd --- /dev/null +++ b/SampleProjects/TestSomething/Gemfile @@ -0,0 +1,2 @@ +source 'https://rubygems.org' +gem 'arduino_ci', path: '../../' diff --git a/SampleProjects/TestSomething/Gemfile.lock b/SampleProjects/TestSomething/Gemfile.lock new file mode 100644 index 00000000..960bb950 --- /dev/null +++ b/SampleProjects/TestSomething/Gemfile.lock @@ -0,0 +1,19 @@ +PATH + remote: /Users/ikatz/Development/non-wayfair/arduino_ci + specs: + arduino_ci (0.0.1) + os (~> 1.0) + +GEM + remote: https://rubygems.org/ + specs: + os (1.0.0) + +PLATFORMS + ruby + +DEPENDENCIES + arduino_ci! + +BUNDLED WITH + 1.16.0 diff --git a/SampleProjects/TestSomething/README.md b/SampleProjects/TestSomething/README.md new file mode 100644 index 00000000..37c6559a --- /dev/null +++ b/SampleProjects/TestSomething/README.md @@ -0,0 +1,3 @@ +# TestSomething + +This is a "beater" example that is referenced by tests of the Arduino CI module itself. diff --git a/SampleProjects/TestSomething/examples/TestSomethingExample/TestSomethingExample.ino b/SampleProjects/TestSomething/examples/TestSomethingExample/TestSomethingExample.ino new file mode 100644 index 00000000..edffa0fe --- /dev/null +++ b/SampleProjects/TestSomething/examples/TestSomethingExample/TestSomethingExample.ino @@ -0,0 +1,9 @@ +#include +// if it seems bare, that's because it's only meant to +// demonstrate compilation -- that references work +void setup() { +} + +void loop() { + testSomething(); +} diff --git a/SampleProjects/TestSomething/library.properties b/SampleProjects/TestSomething/library.properties new file mode 100644 index 00000000..be60a51f --- /dev/null +++ b/SampleProjects/TestSomething/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 CI and unit tests +category=Other +url=https://github.com/ifreecarve/arduino_ci/SampleProjects/TestSomething +architectures=avr,esp8266 +includes=do-something.h diff --git a/SampleProjects/TestSomething/test-something.cpp b/SampleProjects/TestSomething/test-something.cpp new file mode 100644 index 00000000..93dc9fe3 --- /dev/null +++ b/SampleProjects/TestSomething/test-something.cpp @@ -0,0 +1,5 @@ +#include "test-something.h" +int testSomething(void) { + millis(); // this line is only here to test that we're able to refer to the builtins + return 4; +}; diff --git a/SampleProjects/TestSomething/test-something.h b/SampleProjects/TestSomething/test-something.h new file mode 100644 index 00000000..5a45db34 --- /dev/null +++ b/SampleProjects/TestSomething/test-something.h @@ -0,0 +1,3 @@ +#pragma once +#include +int testSomething(void); diff --git a/SampleProjects/TestSomething/test/bad-null.cpp b/SampleProjects/TestSomething/test/bad-null.cpp new file mode 100644 index 00000000..d2d1cd1e --- /dev/null +++ b/SampleProjects/TestSomething/test/bad-null.cpp @@ -0,0 +1,12 @@ +#include + +unittest(pretend_equal_things_arent) +{ + int x = 3; + int y = 3; + assertNotEqual(x, y); +} + +int main(int argc, char *argv[]) { + return Test::run_and_report(argc, argv); +} diff --git a/SampleProjects/TestSomething/test/good-library.cpp b/SampleProjects/TestSomething/test/good-library.cpp new file mode 100644 index 00000000..342b0680 --- /dev/null +++ b/SampleProjects/TestSomething/test/good-library.cpp @@ -0,0 +1,11 @@ +#include +#include "../test-something.h" + +unittest(library_tests_something) +{ + assertEqual(4, testSomething()); +} + +int main(int argc, char *argv[]) { + return Test::run_and_report(argc, argv); +} diff --git a/SampleProjects/TestSomething/test/good-null.cpp b/SampleProjects/TestSomething/test/good-null.cpp new file mode 100644 index 00000000..3df30eab --- /dev/null +++ b/SampleProjects/TestSomething/test/good-null.cpp @@ -0,0 +1,24 @@ +#include + +unittest(equality_as_vars) +{ + int x = 3; + int y = 3; + int z = 3; + assertEqual(x, y); + assertEqual(x, z); +} + +unittest(equality_as_values) +{ + assertEqual(1, 1); + assertEqual(4, 4); +} + +unittest(nothing) +{ +} + +int main(int argc, char *argv[]) { + return Test::run_and_report(argc, argv); +} diff --git a/cpp/Arduino.h b/cpp/Arduino.h deleted file mode 100644 index 12cdc863..00000000 --- a/cpp/Arduino.h +++ /dev/null @@ -1,30 +0,0 @@ -/* -Mock Arduino.h library. - -Where possible, variable names from the Arduino library are used to avoid conflicts - -*/ - - -#ifndef ARDUINO_CI_ARDUINO - -#include "math.h" -#define ARDUINO_CI_ARDUINO - -struct unit_test_state { - unsigned long micros; -}; - -struct unit_test_state godmode { - 0, // micros -}; - -unsigned long millis() { - return godmode.micros / 1000; -} - -unsigned long micros() { - return godmode.micros; -} - -#endif diff --git a/cpp/arduino/Arduino.cpp b/cpp/arduino/Arduino.cpp new file mode 100644 index 00000000..d342c190 --- /dev/null +++ b/cpp/arduino/Arduino.cpp @@ -0,0 +1,13 @@ +#include "Arduino.h" + +struct unit_test_state godmode = { + 0, // micros +}; + +unsigned long millis() { + return godmode.micros / 1000; +} + +unsigned long micros() { + return godmode.micros; +} diff --git a/cpp/arduino/Arduino.h b/cpp/arduino/Arduino.h new file mode 100644 index 00000000..ba18b248 --- /dev/null +++ b/cpp/arduino/Arduino.h @@ -0,0 +1,19 @@ +#pragma once +/* +Mock Arduino.h library. + +Where possible, variable names from the Arduino library are used to avoid conflicts + +*/ + + + +#include "AvrMath.h" + +struct unit_test_state { + unsigned long micros; +}; + +unsigned long millis(); + +unsigned long micros(); diff --git a/cpp/arduino/AvrMath.h b/cpp/arduino/AvrMath.h new file mode 100644 index 00000000..1d6aabe4 --- /dev/null +++ b/cpp/arduino/AvrMath.h @@ -0,0 +1,37 @@ +//#include +#pragma once + +template inline A abs(A x) { return x > 0 ? x : -x; } + +//max +template inline float max(A a, float b) { return a > b ? a : b; } +template inline float max(float a, A b) { return a > b ? a : b; } +template inline long max(A a, B b) { return a > b ? a : b; } + +//min +template inline float min(A a, float b) { return a < b ? a : b; } +template inline float min(float a, A b) { return a < b ? a : b; } +template inline long min(A a, B b) { return a < b ? a : b; } + +//constrain +template inline A constrain(A x, A a, A b) { return max(a, min(b, x)); } + +//map +template inline A map(A x, A in_min, A in_max, A out_min, A out_max) +{ + return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min; +} + +//sq +template A inline sq(A x) { return x * x; } + +// ??? too lazy to sort these now +//pow +//sqrt + +// http://www.ganssle.com/approx.htm +// http://www.ganssle.com/approx/sincos.cpp +//cos +//sin +//tan + diff --git a/cpp/math.h b/cpp/math.h deleted file mode 100644 index f7f6e372..00000000 --- a/cpp/math.h +++ /dev/null @@ -1,41 +0,0 @@ -//abs -long abs(long x) { return x > 0 ? x : -x; } -double fabs(double x) { return x > 0 ? x : -x; } - -//max -long max(long a, long b) { return a > b ? a : b; } -double fmax(double a, double b) { return a > b ? a : b; } - -//min -long min(long a, long b) { return a < b ? a : b; } -double fmin(double a, double b) { return a < b ? a : b; } - -//constrain -long constrain(long x, long a, long b) { return max(a, min(b, x)); } -double constrain(double x, double a, double b) { return max(a, min(b, x)); } - -//map -long map(long x, long in_min, long in_max, long out_min, long out_max) -{ - return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min; -} - -double map(double x, double in_min, double in_max, double out_min, double out_max) -{ - return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min; -} - -//sq -long sq(long x) { return x * x; } -double sq(double x) { return x * x; } - -// ??? too lazy to sort these now -//pow -//sqrt - -// http://www.ganssle.com/approx.htm -// http://www.ganssle.com/approx/sincos.cpp -//cos -//sin -//tan - diff --git a/cpp/unittest/ArduinoUnitTests.cpp b/cpp/unittest/ArduinoUnitTests.cpp new file mode 100644 index 00000000..7f3d0827 --- /dev/null +++ b/cpp/unittest/ArduinoUnitTests.cpp @@ -0,0 +1,3 @@ +#include "ArduinoUnitTests.h" + +Test* Test::sRoot = 0; diff --git a/cpp/unittest/ArduinoUnitTests.h b/cpp/unittest/ArduinoUnitTests.h new file mode 100644 index 00000000..3f92d29d --- /dev/null +++ b/cpp/unittest/ArduinoUnitTests.h @@ -0,0 +1,212 @@ +#pragma once + +#include "Assertion.h" +#include +using namespace std; + +struct Results { + int passed; + int failed; + int skipped; // TODO: not sure about this +}; + +struct TestData { + const char* name; + int result; +}; + +class Test +{ + public: + class ReporterTAP { + private: + int mTestCounter; + int mAssertCounter; + + public: + ReporterTAP() {} + ~ReporterTAP() {} + + void onTestRunInit(int numTests) { + cout << "TAP version 13" << endl; + cout << 1 << ".." << numTests << endl; // we know how many tests, in advance + mTestCounter = 0; + } + + void onTestStart(TestData td) { + mAssertCounter = 0; + ++mTestCounter; + cout << "# Subtest: " << td.name << endl; + } + + void onTestEnd(TestData td) { + cout << " 1.." << mAssertCounter << endl; + if (td.result == RESULT_PASS) { + cout << "ok " << mTestCounter << " - " << td.name << endl; + } else { + cout << "not ok " << mTestCounter << " - " << td.name << endl; + } + } + + template void onAssert( + const char* file, + int line, + const char* description, + bool pass, + const char* lhsRelevance, + const char* lhsLabel, + const A &lhs, + const char* opLabel, + const char* rhsRelevance, + const char* rhsLabel, + const B &rhs + ) { + cout << " " << (pass ? "" : "not ") << "ok " << ++mAssertCounter << " - "; + cout << description << " " << lhsLabel << " " << opLabel << " " << rhsLabel << endl; + if (!pass) { + cout << " ---" << endl; + cout << " operator: " << opLabel << endl; + cout << " " << lhsRelevance << ": " << lhs << endl; + cout << " " << rhsRelevance << ": " << rhs << endl; + cout << " at:" << endl; + cout << " file: " << file << endl; + cout << " line: " << line << endl; + cout << " ..." << endl; + } + } + }; + + private: + ReporterTAP* mReporter; + const char* mName; + + // linked list structure for active tests + static Test* sRoot; + Test* mNext; + + void append() { + if (!sRoot) return (void)(sRoot = this); + Test* p; + for (p = sRoot; p->mNext; p = p->mNext); + p->mNext = this; + } + + void excise() { + for (Test **p = &sRoot; *p != 0; p=&((*p)->mNext)) { + if (*p == this) return (void)(*p = (*p)->mNext); + } + } + + static int numTests() { + if (!sRoot) return 0; + int i = 1; + for (Test* p = sRoot; p->mNext; ++i && (p = p->mNext)); + return i; + } + + // current test result + int mResult; + + public: + static const int RESULT_NONE = 0; + static const int RESULT_PASS = 1; + static const int RESULT_FAIL = 2; + static const int RESULT_SKIP = 3; + + const inline char *name() { return mName; } + const inline int result() { return mResult; } + + Test(const char* _name) : mName(_name) { + mResult = RESULT_NONE; + mReporter = 0; + append(); + } + + inline void fail() { mResult = RESULT_FAIL; } + inline void skip() { mResult = RESULT_SKIP; } + + static Results run(ReporterTAP* reporter) { + if (reporter) reporter->onTestRunInit(numTests()); + Results results = {0, 0, 0}; + + for (Test *p = sRoot; p; p = p->mNext) { + TestData td = {p->name(), p->result()}; + p->mReporter = reporter; + if (reporter) reporter->onTestStart(td); + p->test(); + if (p->mResult == RESULT_PASS) ++results.passed; + if (p->mResult == RESULT_FAIL) ++results.failed; + if (p->mResult == RESULT_SKIP) ++results.skipped; + if (reporter) reporter->onTestEnd(td); + } + + return results; + } + + // TODO: figure out TAP output like + // https://api.travis-ci.org/v3/job/283745834/log.txt + // https://testanything.org/tap-specification.html + // parse input and decide how to report + static int run_and_report(int argc, char *argv[]) { + // TODO: pick a reporter based on args + ReporterTAP rep; + Results results = run(&rep); + return results.failed + results.skipped; + } + + void test() { + mResult = RESULT_PASS; // not None, and not fail unless we hear otherwise + task(); + } + + virtual void task() = 0; + + virtual ~Test() { + excise(); + } + + template + bool assertion( + const char *file, + int line, + const char *description, + const char *lhsRelevance, + const char *lhsLabel, + const A &lhs, + + const char *ops, + + bool (*op)( + const A &lhs, + const B &rhs), + + const char *rhsRelevance, + const char *rhsLabel, + const B &rhs) + { + bool ok = op(lhs, rhs); + + if (mReporter) { + mReporter->onAssert(file, line, description, ok, + lhsRelevance, lhsLabel, lhs, ops, rhsRelevance, rhsLabel, rhs); + } + + if (!ok) + fail(); + return ok; + } + +}; + +/** + * Extend the class into a struct. + * The implementation of task() will follow the macro + * + */ +#define unittest(name) \ + struct test_##name : Test \ + { \ + test_##name() : Test(#name){}; \ + void task(); \ + } test_##name##_instance; \ + void test_##name ::task() diff --git a/cpp/unittest/Assertion.h b/cpp/unittest/Assertion.h new file mode 100644 index 00000000..1a53bff3 --- /dev/null +++ b/cpp/unittest/Assertion.h @@ -0,0 +1,27 @@ +#pragma once +#include "Compare.h" + +// helper define for the operators below +#define assertOp(desc, relevance1, arg1, op, op_name, relevance2, arg2) \ + do \ + { \ + if (!assertion(__FILE__, __LINE__, \ + desc, \ + relevance1, #arg1, (arg1), \ + op_name, op, \ + relevance2, #arg2, (arg2))) \ + { \ + return; \ + } \ + } while (0) + +/** macro generates optional output and calls fail() followed by a return if false. */ +#define assertEqual(arg1,arg2) assertOp("assertEqual","expected",arg1,compareEqual,"==","actual",arg2) +#define assertNotEqual(arg1,arg2) assertOp("assertNotEqual","unwanted",arg1,compareNotEqual,"!=","actual",arg2) +#define assertLess(arg1,arg2) assertOp("assertLess","lowerBound",arg1,compareLess,"<","upperBound",arg2) +#define assertMore(arg1,arg2) assertOp("assertMore","upperBound",arg1,compareMore,">","lowerBound",arg2) +#define assertLessOrEqual(arg1,arg2) assertOp("assertLessOrEqual","lowerBound",arg1,compareLessOrEqual,"<=","upperBound",arg2) +#define assertMoreOrEqual(arg1,arg2) assertOp("assertMoreOrEqual","upperBound",arg1,compareMoreOrEqual,">=","lowerBound",arg2) +#define assertTrue(arg) assertEqual(arg,true) +#define assertFalse(arg) assertEqual(arg,false) + diff --git a/cpp/unittest/Compare.h b/cpp/unittest/Compare.h new file mode 100644 index 00000000..14bd9399 --- /dev/null +++ b/cpp/unittest/Compare.h @@ -0,0 +1,334 @@ +#pragma once +#include "string.h" + + +template < typename A, typename B > struct Compare +{ + inline static int between(const A &a,const B &b) + { + if (a struct Compare; +template < > struct Compare; +template < long M > struct Compare; +template < > struct Compare; +template < > struct Compare; +template < long M > struct Compare; +template < long N > struct Compare; +template < long N > struct Compare; +template < long N, long M > struct Compare; + +template < > struct Compare +{ + inline static int between(const char * const &a,const char * const &b) + { + return strcmp(a,b); + } // between + inline static bool equal(const char * const &a,const char * const &b) + { + return between(a,b) == 0; + } // equal + inline static bool notEqual(const char * const &a,const char * const &b) + { + return between(a,b) != 0; + } // notEqual + inline static bool less(const char * const &a,const char * const &b) + { + return between(a,b) < 0; + } // less + inline static bool more(const char * const &a,const char * const &b) + { + return between(a,b) > 0; + } // more + inline static bool lessOrEqual(const char * const &a,const char * const &b) + { + return between(a,b) <= 0; + } // lessOrEqual + inline static bool moreOrEqual(const char * const &a,const char * const &b) + { + return between(a,b) >= 0; + } // moreOrEqual +}; +template < > struct Compare +{ + inline static int between(const char * const &a,char * const &b) + { + return strcmp(a,b); + } // between + inline static bool equal(const char * const &a,char * const &b) + { + return between(a,b) == 0; + } // equal + inline static bool notEqual(const char * const &a,char * const &b) + { + return between(a,b) != 0; + } // notEqual + inline static bool less(const char * const &a,char * const &b) + { + return between(a,b) < 0; + } // less + inline static bool more(const char * const &a,char * const &b) + { + return between(a,b) > 0; + } // more + inline static bool lessOrEqual(const char * const &a,char * const &b) + { + return between(a,b) <= 0; + } // lessOrEqual + inline static bool moreOrEqual(const char * const &a,char * const &b) + { + return between(a,b) >= 0; + } // moreOrEqual +}; +template < long M > struct Compare +{ + inline static int between(const char * const &a,const char (&b)[M]) + { + return strcmp(a,b); + } // between + inline static bool equal(const char * const &a,const char (&b)[M]) + { + return between(a,b) == 0; + } // equal + inline static bool notEqual(const char * const &a,const char (&b)[M]) + { + return between(a,b) != 0; + } // notEqual + inline static bool less(const char * const &a,const char (&b)[M]) + { + return between(a,b) < 0; + } // less + inline static bool more(const char * const &a,const char (&b)[M]) + { + return between(a,b) > 0; + } // more + inline static bool lessOrEqual(const char * const &a,const char (&b)[M]) + { + return between(a,b) <= 0; + } // lessOrEqual + inline static bool moreOrEqual(const char * const &a,const char (&b)[M]) + { + return between(a,b) >= 0; + } // moreOrEqual +}; +template < > struct Compare +{ + inline static int between(char * const &a,const char * const &b) + { + return strcmp(a,b); + } // between + inline static bool equal(char * const &a,const char * const &b) + { + return between(a,b) == 0; + } // equal + inline static bool notEqual(char * const &a,const char * const &b) + { + return between(a,b) != 0; + } // notEqual + inline static bool less(char * const &a,const char * const &b) + { + return between(a,b) < 0; + } // less + inline static bool more(char * const &a,const char * const &b) + { + return between(a,b) > 0; + } // more + inline static bool lessOrEqual(char * const &a,const char * const &b) + { + return between(a,b) <= 0; + } // lessOrEqual + inline static bool moreOrEqual(char * const &a,const char * const &b) + { + return between(a,b) >= 0; + } // moreOrEqual +}; +template < > struct Compare +{ + inline static int between(char * const &a,char * const &b) + { + return strcmp(a,b); + } // between + inline static bool equal(char * const &a,char * const &b) + { + return between(a,b) == 0; + } // equal + inline static bool notEqual(char * const &a,char * const &b) + { + return between(a,b) != 0; + } // notEqual + inline static bool less(char * const &a,char * const &b) + { + return between(a,b) < 0; + } // less + inline static bool more(char * const &a,char * const &b) + { + return between(a,b) > 0; + } // more + inline static bool lessOrEqual(char * const &a,char * const &b) + { + return between(a,b) <= 0; + } // lessOrEqual + inline static bool moreOrEqual(char * const &a,char * const &b) + { + return between(a,b) >= 0; + } // moreOrEqual +}; +template < long M > struct Compare +{ + inline static int between(char * const &a,const char (&b)[M]) + { + return strcmp(a,b); + } // between + inline static bool equal(char * const &a,const char (&b)[M]) + { + return between(a,b) == 0; + } // equal + inline static bool notEqual(char * const &a,const char (&b)[M]) + { + return between(a,b) != 0; + } // notEqual + inline static bool less(char * const &a,const char (&b)[M]) + { + return between(a,b) < 0; + } // less + inline static bool more(char * const &a,const char (&b)[M]) + { + return between(a,b) > 0; + } // more + inline static bool lessOrEqual(char * const &a,const char (&b)[M]) + { + return between(a,b) <= 0; + } // lessOrEqual + inline static bool moreOrEqual(char * const &a,const char (&b)[M]) + { + return between(a,b) >= 0; + } // moreOrEqual +}; +template < long N > struct Compare +{ + inline static int between(const char (&a)[N],const char * const &b) + { + return strcmp(a,b); + } // between + inline static bool equal(const char (&a)[N],const char * const &b) + { + return between(a,b) == 0; + } // equal + inline static bool notEqual(const char (&a)[N],const char * const &b) + { + return between(a,b) != 0; + } // notEqual + inline static bool less(const char (&a)[N],const char * const &b) + { + return between(a,b) < 0; + } // less + inline static bool more(const char (&a)[N],const char * const &b) + { + return between(a,b) > 0; + } // more + inline static bool lessOrEqual(const char (&a)[N],const char * const &b) + { + return between(a,b) <= 0; + } // lessOrEqual + inline static bool moreOrEqual(const char (&a)[N],const char * const &b) + { + return between(a,b) >= 0; + } // moreOrEqual +}; +template < long N > struct Compare +{ + inline static int between(const char (&a)[N],char * const &b) + { + return strcmp(a,b); + } // between + inline static bool equal(const char (&a)[N],char * const &b) + { + return between(a,b) == 0; + } // equal + inline static bool notEqual(const char (&a)[N],char * const &b) + { + return between(a,b) != 0; + } // notEqual + inline static bool less(const char (&a)[N],char * const &b) + { + return between(a,b) < 0; + } // less + inline static bool more(const char (&a)[N],char * const &b) + { + return between(a,b) > 0; + } // more + inline static bool lessOrEqual(const char (&a)[N],char * const &b) + { + return between(a,b) <= 0; + } // lessOrEqual + inline static bool moreOrEqual(const char (&a)[N],char * const &b) + { + return between(a,b) >= 0; + } // moreOrEqual +}; +template < long N, long M > struct Compare +{ + inline static int between(const char (&a)[N],const char (&b)[M]) + { + return strcmp(a,b); + } // between + inline static bool equal(const char (&a)[N],const char (&b)[M]) + { + return between(a,b) == 0; + } // equal + inline static bool notEqual(const char (&a)[N],const char (&b)[M]) + { + return between(a,b) != 0; + } // notEqual + inline static bool less(const char (&a)[N],const char (&b)[M]) + { + return between(a,b) < 0; + } // less + inline static bool more(const char (&a)[N],const char (&b)[M]) + { + return between(a,b) > 0; + } // more + inline static bool lessOrEqual(const char (&a)[N],const char (&b)[M]) + { + return between(a,b) <= 0; + } // lessOrEqual + inline static bool moreOrEqual(const char (&a)[N],const char (&b)[M]) + { + return between(a,b) >= 0; + } // moreOrEqual +}; +template int compareBetween(const A &a, const B &b) { return Compare::between(a,b); } +template bool compareEqual(const A &a, const B &b) { return Compare::equal(a,b); } +template bool compareNotEqual(const A &a, const B &b) { return Compare::notEqual(a,b); } +template bool compareLess(const A &a, const B &b) { return Compare::less(a,b); } +template bool compareMore(const A &a, const B &b) { return Compare::more(a,b); } +template bool compareLessOrEqual(const A &a, const B &b) { return Compare::lessOrEqual(a,b); } +template bool compareMoreOrEqual(const A &a, const B &b) { return Compare::moreOrEqual(a,b); } diff --git a/exe/arduino_ci_remote.rb b/exe/arduino_ci_remote.rb new file mode 100755 index 00000000..cdeef346 --- /dev/null +++ b/exe/arduino_ci_remote.rb @@ -0,0 +1,132 @@ +#!/usr/bin/env ruby +require 'arduino_ci' +require 'set' + +WIDTH = 80 + +@failure_count = 0 + +# terminate after printing any debug info. TODO: capture debug info +def terminate(final = nil) + puts "Failures: #{@failure_count}" + unless @failure_count.zero? || final + puts "Last message: #{@arduino_cmd.last_msg}" + puts "========== Stdout:" + puts @arduino_cmd.last_out + puts "========== Stderr:" + puts @arduino_cmd.last_err + end + retcode = @failure_count.zero? ? 0 : 1 + exit(retcode) +end + +# make a nice status line for an action and react to the action +def perform_action(message, on_fail_msg, abort_on_fail) + line = "#{message}..." + print line + result = yield + mark = result ? "✓" : "X" + puts mark.rjust(WIDTH - line.length, " ") + unless result + puts on_fail_msg unless on_fail_msg.nil? + @failure_count += 1 + # print out error messaging here if we've captured it + terminate if abort_on_fail + end + result +end + +# Make a nice status for something that defers any failure code until script exit +def attempt(message, &block) + perform_action(message, nil, false, &block) +end + +# Make a nice status for something that kills the script immediately on failure +def assure(message, &block) + perform_action(message, "This may indicate a problem with ArduinoCI!", true, &block) +end + +# initialize command and config +config = ArduinoCI::CIConfig.default.from_project_library +@arduino_cmd = ArduinoCI::ArduinoInstallation.autolocate! + +# initialize library under test +installed_library_path = assure("Installing library under test") { @arduino_cmd.install_local_library(".") } +library_examples = @arduino_cmd.library_examples(installed_library_path) +cpp_library = ArduinoCI::CppLibrary.new(installed_library_path) +attempt("Library installed at #{installed_library_path}") { true } + +# gather up all required boards 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 +all_platforms = {} +aux_libraries = Set.new(config.aux_libraries_for_unittest + config.aux_libraries_for_build) +config.platforms_to_unittest.each { |p| all_platforms[p] = config.platform_definition(p) } +library_examples.each do |path| + ovr_config = config.from_example(path) + ovr_config.platforms_to_build.each { |p| all_platforms[p] = config.platform_definition(p) } + aux_libraries.merge(ovr_config.aux_libraries_for_build) +end + +# with all platform info, we can extract unique packages and their urls +# do that, set the URLs, and download the packages +all_packages = all_platforms.values.map { |v| v[:package] }.uniq.reject(&:nil?) +all_urls = all_packages.map { |p| config.package_url(p) }.uniq.reject(&:nil?) +assure("Setting board manager URLs") do + @arduino_cmd.set_pref("boardsmanager.additional.urls", all_urls.join(",")) +end + +all_packages.each do |p| + assure("Installing board package #{p}") do + @arduino_cmd.install_boards(p) + end +end + +aux_libraries.each do |l| + assure("Installing aux library '#{l}'") { @arduino_cmd.install_library(l) } +end + +attempt("Setting compiler warning level") { @arduino_cmd.set_pref("compiler.warning_level", "all") } + +library_examples.each do |example_path| + ovr_config = config.from_example(example_path) + ovr_config.platforms_to_build.each do |p| + board = all_platforms[p][:board] + assure("Switching to board for #{p} (#{board})") { @arduino_cmd.use_board(board) } + example_name = File.basename(example_path) + attempt("Verifying #{example_name}") do + ret = @arduino_cmd.verify_sketch(example_path) + unless ret + puts + puts "Last command: #{@arduino_cmd.last_msg}" + puts @arduino_cmd.last_err + end + ret + end + end +end + +config.platforms_to_unittest.each do |p| + board = all_platforms[p][:board] + assure("Switching to board for #{p} (#{board})") { @arduino_cmd.use_board(board) } + cpp_library.test_files.each do |unittest_path| + unittest_name = File.basename(unittest_path) + attempt("Unit testing #{unittest_name}") do + exe = cpp_library.build_for_test_with_configuration( + unittest_path, + config.aux_libraries_for_unittest, + config.gcc_config(p) + ) + puts + unless exe + puts "Last command: #{cpp_library.last_cmd}" + puts cpp_library.last_out + puts cpp_library.last_err + next false + end + cpp_library.run_test_file(exe) + end + end +end + +terminate(true) diff --git a/exe/ci_system_check.rb b/exe/ci_system_check.rb deleted file mode 100755 index fea71cb5..00000000 --- a/exe/ci_system_check.rb +++ /dev/null @@ -1,66 +0,0 @@ -require 'arduino_ci' - -puts "Autlocating Arduino command" -arduino_cmd = ArduinoCI::ArduinoInstallation.autolocate! - -board_tests = { - "arduino:avr:uno" => true, - "eggs:milk:wheat" => false, -} - -got_problem = false -board_tests.each do |k, v| - puts "I expect arduino_cmd.board_installed?(#{k}) to be #{v}" - result = arduino_cmd.board_installed?(k) - puts " board_installed?(#{k}) returns #{result}. expected #{v}" - got_problem = true if v != result -end - -urls = [ - "https://adafruit.github.io/arduino-board-index/package_adafruit_index.json", - "http://arduino.esp8266.com/stable/package_esp8266com_index.json" -] - -puts "Setting additional URLs" -got_problem = true unless arduino_cmd.set_pref("boardsmanager.additional.urls", urls.join(",")) - -puts "Installing arduino:sam" -got_problem = true unless arduino_cmd.install_board("arduino:sam") -puts "Installing USBHost" -got_problem = true unless arduino_cmd.install_library("USBHost") -puts "checking that library is indexed" -got_problem = true unless arduino_cmd.library_is_indexed - -my_board = "arduino:sam:arduino_due_x" - -puts "use board! (install board)" -got_problem = true unless arduino_cmd.use_board!(my_board) -puts "assert that board has been installed" -got_problem = true unless arduino_cmd.board_installed?(my_board) - -puts "setting compiler warning level" -got_problem = true unless arduino_cmd.set_pref("compiler.warning_level", "all") - -simple_sketch = File.join(File.dirname(File.dirname(__FILE__)), "spec", "FakeSketch", "FakeSketch.ino") - -puts "verify a simple sketch" -got_problem = true unless arduino_cmd.verify_sketch(simple_sketch) - -library_path = File.join(File.dirname(File.dirname(__FILE__)), "SampleProjects", "DoSomething") - -puts "verify a library with arduino mocks" -cpp_library = ArduinoCI::CppLibrary.new(library_path) -got_problem = true unless cpp_library.build(arduino_cmd) - -puts "verify the examples of a library (#{library_path})..." -puts " - Install the library" -installed_library_path = arduino_cmd.install_local_library(library_path) -got_problem = true if installed_library_path.nil? -puts " - Iterate over the examples" -arduino_cmd.each_library_example(installed_library_path) do |example_path| - puts "Iterating #{example_path}" - got_problem = true unless arduino_cmd.verify_sketch(example_path) -end - -abort if got_problem -exit(0) diff --git a/lib/arduino_ci.rb b/lib/arduino_ci.rb index bfd11647..c187680d 100644 --- a/lib/arduino_ci.rb +++ b/lib/arduino_ci.rb @@ -1,6 +1,7 @@ require "arduino_ci/version" require "arduino_ci/arduino_installation" require "arduino_ci/cpp_library" +require "arduino_ci/ci_config" # ArduinoCI contains classes for automated testing of Arduino code on the command line # @author Ian Katz diff --git a/lib/arduino_ci/arduino_cmd.rb b/lib/arduino_ci/arduino_cmd.rb index f6fcbd76..bc896d93 100644 --- a/lib/arduino_ci/arduino_cmd.rb +++ b/lib/arduino_ci/arduino_cmd.rb @@ -9,19 +9,30 @@ class ArduinoCmd # @param name [String] What the flag will be called (prefixed with 'flag_') # @return [void] # @macro [attach] flag + # The text of the command line flag for $1 # @!attribute [r] flag_$1 - # @return String $2 the text of the command line flag + # @return [String] the text of the command line flag (`$2` in this case) def self.flag(name, text = nil) text = "(flag #{name} not defined)" if text.nil? self.class_eval("def flag_#{name};\"#{text}\";end") end - attr_accessor :installation + # the path to the Arduino executable + # @return [String] attr_accessor :base_cmd - attr_accessor :gcc_cmd + # part of a workaround for https://github.com/arduino/Arduino/issues/3535 attr_reader :library_is_indexed + # @return [String] STDOUT of the most recently-run command + attr_reader :last_out + + # @return [String] STDERR of the most recently-run command + attr_reader :last_err + + # @return [String] the most recently-run command + attr_reader :last_msg + # set the command line flags (undefined for now). # These vary between gui/cli flag :get_pref @@ -33,10 +44,17 @@ def self.flag(name, text = nil) flag :verify def initialize - @prefs_cache = nil - @library_is_indexed = false + @prefs_cache = {} + @prefs_fetched = false + @library_is_indexed = false + @last_out = "" + @last_err = "" + @last_msg = "" end + # Convert a preferences dump into a flat hash + # @param arduino_output [String] The raw Arduino executable output + # @return [Hash] preferences as a hash def parse_pref_string(arduino_output) lines = arduino_output.split("\n").select { |l| l.include? "=" } ret = lines.each_with_object({}) do |e, acc| @@ -47,31 +65,39 @@ def parse_pref_string(arduino_output) ret end + # @return [String] the path to the Arduino libraries directory def _lib_dir "" end - # fetch preferences to a string + # fetch preferences in their raw form + # @return [String] Preferences as a set of lines def _prefs_raw resp = run_and_capture(flag_get_pref) return nil unless resp[:success] resp[:out] end + # Get the Arduino preferences, from cache if possible + # @return [Hash] The full set of preferences def prefs - prefs_raw = _prefs_raw if @prefs_cache.nil? + prefs_raw = _prefs_raw unless @prefs_fetched return nil if prefs_raw.nil? @prefs_cache = parse_pref_string(prefs_raw) @prefs_cache.clone end # get a preference key + # @param key [String] The preferences key to look up + # @return [String] The preference value def get_pref(key) - data = @prefs_cache.nil? ? prefs : @prefs_cache + data = @prefs_fetched ? @prefs_cache : prefs data[key] end # underlying preference-setter. + # @param key [String] The preference name + # @param value [String] The value to set to # @return [bool] whether the command succeeded def _set_pref(key, value) run_and_capture(flag_set_pref, "#{key}=#{value}", flag_save_prefs)[:success] @@ -94,15 +120,16 @@ def _run(*args, **kwargs) # build and run the arduino command def run(*args, **kwargs) - # TODO: detect env!! - full_args = @base_cmd + args - _run(*full_args, **kwargs) - end - - def run_gcc(*args, **kwargs) - # TODO: detect env!! - full_args = @gcc_cmd + args - _run(*full_args, **kwargs) + # do some work to extract & merge environment variables if they exist + has_env = !args.empty? && args[0].class == Hash + env_vars = has_env ? args[0] : {} + actual_args = has_env ? args[1..-1] : args # need to shift over if we extracted args + full_args = @base_cmd + actual_args + full_cmd = env_vars.empty? ? full_args : [env_vars] + full_args + + shell_vars = env_vars.map { |k, v| "#{k}=#{v}" }.join(" ") + @last_msg = " $ #{shell_vars} #{full_args.join(' ')}" + _run(*full_cmd, **kwargs) end # run a command and capture its output @@ -119,6 +146,8 @@ def run_and_capture(*args, **kwargs) str_err = pipe_err.read pipe_out.close pipe_err.close + @last_err = str_err + @last_out = str_out { out: str_out, err: str_err, success: success } end @@ -132,6 +161,8 @@ def run_wrap(*args, **kwargs) # check whether a board is installed # we do this by just selecting a board. # the arduino binary will error if unrecognized and do a successful no-op if it's installed + # @param boardname [String] The board to test + # @return [bool] Whether the board is installed def board_installed?(boardname) run_and_capture(flag_use_board, boardname)[:success] end @@ -139,26 +170,42 @@ def board_installed?(boardname) # install a board by name # @param name [String] the board name # @return [bool] whether the command succeeded - def install_board(boardname) + def install_boards(boardfamily) # TODO: find out why IO.pipe fails but File::NULL succeeds :( - run_and_capture(flag_install_boards, boardname, out: File::NULL)[:success] + result = run_and_capture(flag_install_boards, boardfamily) + already_installed = result[:err].include?("Platform is already installed!") + result[:success] || already_installed end # install a library by name # @param name [String] the library name # @return [bool] whether the command succeeded def install_library(library_name) + # workaround for https://github.com/arduino/Arduino/issues/3535 + # use a dummy library name but keep open the possiblity that said library + # might be selected by choice for installation + workaround_lib = "USBHost" + unless @library_is_indexed || workaround_lib == library_name + @library_is_indexed = run_and_capture(flag_install_library, workaround_lib) + end + + # actual installation result = run_and_capture(flag_install_library, library_name) - @library_is_indexed = true if result[:success] + + # update flag if necessary + @library_is_indexed = (@library_is_indexed || result[:success]) if library_name == workaround_lib result[:success] end # generate the (very likely) path of a library given its name + # @param library_name [String] The name of the library + # @return [String] The fully qualified library name def library_path(library_name) File.join(_lib_dir, library_name) end # update the library index + # @return [bool] Whether the update succeeded def update_library_index # install random lib so the arduino IDE grabs a new library index # see: https://github.com/arduino/Arduino/issues/3535 @@ -166,68 +213,80 @@ def update_library_index end # use a particular board for compilation + # @param boardname [String] The board to use + # @return [bool] whether the command succeeded def use_board(boardname) run_and_capture(flag_use_board, boardname, flag_save_prefs)[:success] end # use a particular board for compilation, installing it if necessary + # @param boardname [String] The board to use + # @return [bool] whether the command succeeded def use_board!(boardname) return true if use_board(boardname) boardfamily = boardname.split(":")[0..1].join(":") puts "Board '#{boardname}' not found; attempting to install '#{boardfamily}'" - return false unless install_board(boardfamily) # guess board family from first 2 :-separated fields + return false unless install_boards(boardfamily) # guess board family from first 2 :-separated fields use_board(boardname) end + # @param path [String] The sketch to verify + # @return [bool] whether the command succeeded def verify_sketch(path) ext = File.extname path unless ext.casecmp(".ino").zero? - puts "Refusing to verify sketch with '#{ext}' extension -- rename it to '.ino'!" + @last_msg = "Refusing to verify sketch with '#{ext}' extension -- rename it to '.ino'!" return false end unless File.exist? path - puts "Can't verify nonexistent Sketch at '#{path}'!" + @last_msg = "Can't verify Sketch at nonexistent path '#{path}'!" return false end - run(flag_verify, path, err: :out) + ret = run_and_capture(flag_verify, path) + ret[:success] end # ensure that the given library is installed, or symlinked as appropriate # return the path of the prepared library, or nil + # @param path [String] library to use + # @return [String] the path of the installed library def install_local_library(path) - library_name = File.basename(path) + realpath = File.expand_path(path) + library_name = File.basename(realpath) destination_path = library_path(library_name) # things get weird if the sketchbook contains the library. # check that first if File.exist? destination_path uhoh = "There is already a library '#{library_name}' in the library directory" - return destination_path if destination_path == path + return destination_path if destination_path == realpath # maybe it's a symlink? that would be OK if File.symlink?(destination_path) - return destination_path if File.readlink(destination_path) == path - puts "#{uhoh} and it's not symlinked to #{path}" + return destination_path if File.readlink(destination_path) == realpath + @last_msg = "#{uhoh} and it's not symlinked to #{realpath}" return nil end - puts "#{uhoh}. It may need to be removed manually." + @last_msg = "#{uhoh}. It may need to be removed manually." return nil end # install the library - FileUtils.ln_s(path, destination_path) + FileUtils.ln_s(realpath, destination_path) destination_path end - def each_library_example(installed_library_path) + # @param installed_library_path [String] The library to query + # @return [Array] Example sketch files + def library_examples(installed_library_path) example_path = File.join(installed_library_path, "examples") examples = Pathname.new(example_path).children.select(&:directory?).map(&:to_path).map(&File.method(:basename)) - examples.each do |e| + files = examples.map do |e| proj_file = File.join(example_path, e, "#{e}.ino") - puts "Considering #{proj_file}" - yield proj_file if File.exist?(proj_file) + File.exist?(proj_file) ? proj_file : nil end + files.reject(&:nil?) end end end diff --git a/lib/arduino_ci/arduino_cmd_linux.rb b/lib/arduino_ci/arduino_cmd_linux.rb index a57d8dcb..a31d6598 100644 --- a/lib/arduino_ci/arduino_cmd_linux.rb +++ b/lib/arduino_ci/arduino_cmd_linux.rb @@ -22,7 +22,8 @@ def initialize @display_mgr = DisplayManager::instance end - # fetch preferences to a hash + # fetch preferences in their raw form + # @return [String] Preferences as a set of lines def _prefs_raw start = Time.now resp = run_and_capture(flag_get_pref) @@ -31,6 +32,8 @@ def _prefs_raw resp[:out] end + # implementation for Arduino library dir location + # @return [String] the path to the Arduino libraries directory def _lib_dir File.join(get_pref("sketchbook.path"), "libraries") end @@ -66,11 +69,14 @@ def _set_pref(key, value) # check whether a board is installed # we do this by just selecting a board. # the arduino binary will error if unrecognized and do a successful no-op if it's installed + # @param boardname [String] The name of the board + # @return [bool] def board_installed?(boardname) run_with_gui_guess(" about board not installed", flag_use_board, boardname) end # use a particular board for compilation + # @param boardname [String] The name of the board def use_board(boardname) run_with_gui_guess(" about board not installed", flag_use_board, boardname, flag_save_prefs) end diff --git a/lib/arduino_ci/arduino_cmd_linux_builder.rb b/lib/arduino_ci/arduino_cmd_linux_builder.rb index c31f63f7..4740489d 100644 --- a/lib/arduino_ci/arduino_cmd_linux_builder.rb +++ b/lib/arduino_ci/arduino_cmd_linux_builder.rb @@ -14,11 +14,15 @@ class ArduinoCmdLinuxBuilder < ArduinoCmd flag :install_library, "--install-library" # apparently doesn't exist flag :verify, "-compile" + # linux-specific implementation + # @return [String] The path to the library dir def _lib_dir File.join(get_pref("sketchbook.path"), "libraries") end # run the arduino command + # @param [Array] Arguments for the run command + # @return [bool] Whether the command succeeded def _run(*args, **kwargs) Host.run(*args, **kwargs) end diff --git a/lib/arduino_ci/arduino_installation.rb b/lib/arduino_ci/arduino_installation.rb index c7baee0d..ea78eb3b 100644 --- a/lib/arduino_ci/arduino_installation.rb +++ b/lib/arduino_ci/arduino_installation.rb @@ -12,11 +12,13 @@ module ArduinoCI class ArduinoInstallation class << self + # @return [String] The location where a forced install will go def force_install_location File.join(ENV['HOME'], 'arduino_ci_ide') end # attempt to find a workable Arduino executable across platforms + # @return [ArduinoCI::ArduinoCmd] an instance of the command def autolocate case Host.os when :osx then autolocate_osx @@ -24,6 +26,7 @@ def autolocate end end + # @return [ArduinoCI::ArduinoCmdOSX] an instance of a command def autolocate_osx osx_root = "/Applications/Arduino.app/Contents" old_way = false @@ -48,21 +51,17 @@ def autolocate_osx "processing.app.Base", ] end - - hardware_dir = File.join(osx_root, "Java", "hardware") - ret.gcc_cmd = [File.join(hardware_dir, "tools", "avr", "bin", "avr-gcc")] ret end + # @return [ArduinoCI::ArduinoCmdLinux] an instance of a command def autolocate_linux - forced_avr = File.join(force_install_location, "hardware", "tools", "avr") if USE_BUILDER builder_name = "arduino-builder" cli_place = Host.which(builder_name) unless cli_place.nil? ret = ArduinoCmdLinuxBuilder.new ret.base_cmd = [cli_place] - ret.gcc_cmd = [Host.which("avr-gcc")] return ret end @@ -70,7 +69,6 @@ def autolocate_linux if File.exist?(forced_builder) ret = ArduinoCmdLinuxBuilder.new ret.base_cmd = [forced_builder] - ret.gcc_cmd = [File.join(forced_avr, "bin", "avr-gcc")] return ret end end @@ -80,7 +78,6 @@ def autolocate_linux unless gui_place.nil? ret = ArduinoCmdLinux.new ret.base_cmd = [gui_place] - ret.gcc_cmd = [Host.which("avr-gcc")] return ret end @@ -88,13 +85,13 @@ def autolocate_linux if File.exist?(forced_arduino) ret = ArduinoCmdLinux.new ret.base_cmd = [forced_arduino] - ret.gcc_cmd = [File.join(forced_avr, "bin", "avr-gcc")] return ret end nil end # Attempt to find a workable Arduino executable across platforms, and install it if we don't + # @return [ArduinoCI::ArduinoCmd] an instance of a command def autolocate! candidate = autolocate return candidate unless candidate.nil? @@ -104,6 +101,8 @@ def autolocate! autolocate end + # Forcibly install Arduino from the web + # @return [bool] Whether the command succeeded def force_install case Host.os when :linux diff --git a/lib/arduino_ci/ci_config.rb b/lib/arduino_ci/ci_config.rb new file mode 100644 index 00000000..0f752c44 --- /dev/null +++ b/lib/arduino_ci/ci_config.rb @@ -0,0 +1,218 @@ +require 'yaml' + +# base config (platforms) +# project config - .arduino_ci_platforms.yml +# example config - .arduino_ci_plan.yml + +PACKAGE_SCHEMA = { + url: String +}.freeze + +PLATFORM_SCHEMA = { + board: String, + package: String, + gcc: { + features: Array, + defines: Array, + warnings: Array, + flags: Array, + } +}.freeze + +COMPILE_SCHEMA = { + platforms: Array, + libraries: Array, +}.freeze + +UNITTEST_SCHEMA = { + platforms: Array, + libraries: Array, +}.freeze +module ArduinoCI + + # The filename controlling (overriding) the defaults for testing. + # Files with this name can be used in the root directory of the Arduino library and in any/all of the example directories + CONFIG_FILENAME = ".arduino-ci.yaml".freeze + + # Provide the configuration and CI plan + # - Read from a base config with default platforms defined + # - Allow project-specific overrides of platforms + # - Allow example-specific allowance of platforms to test + class CIConfig + + class << self + + # load the default set of platforms + # @return [ArudinoCI::CIConfig] The configuration with defaults filled in + def default + ret = new + ret.load_yaml(File.expand_path("../../../misc/default.yaml", __FILE__)) + ret + end + end + + attr_accessor :package_info + attr_accessor :platform_info + attr_accessor :compile_info + attr_accessor :unittest_info + def initialize + @package_info = {} + @platform_info = {} + @compile_info = {} + @unittest_info = {} + end + + # Deep-clone a hash + # @param hash [Hash] the source data + # @return [Hash] a copy + def deep_clone(hash) + Marshal.load(Marshal.dump(hash)) + end + + # validate a data source according to a schema + # print out warnings for bad fields, and return only the good ones + # @param rootname [String] the name, for printing + # @param source [Hash] source data + # @param schema [Hash] a mapping of field names to their expected type + # @return [Hash] a copy, containing only expected & valid data + def validate_data(rootname, source, schema) + return nil if source.nil? + good_data = {} + source.each do |key, value| + ksym = key.to_sym + expected_type = schema[ksym].class == Class ? schema[ksym] : Hash + if !schema.include?(ksym) + puts "Warning: unknown field '#{ksym}' under definition for #{rootname}" + elsif value.nil? + # unspecificed, that's fine + elsif value.class != expected_type + puts "Warning: expected field '#{ksym}' of #{rootname} to be '#{expected_type}', got '#{value.class}'" + else + good_data[ksym] = value.class == Hash ? validate_data(key, value, schema[ksym]) : value + end + end + good_data + end + + # Load configuration yaml from a file + # @param path [String] the source file + # @return [ArduinoCI::CIConfig] a reference to self + def load_yaml(path) + yml = YAML.load_file(path) + if yml.include?("packages") + yml["packages"].each do |k, v| + valid_data = validate_data("packages", v, PACKAGE_SCHEMA) + @package_info[k] = valid_data + end + end + + if yml.include?("platforms") + yml["platforms"].each do |k, v| + valid_data = validate_data("platforms", v, PLATFORM_SCHEMA) + @platform_info[k] = valid_data + end + end + + if yml.include?("compile") + valid_data = validate_data("compile", yml["compile"], COMPILE_SCHEMA) + @compile_info = valid_data + end + + if yml.include?("unittest") + valid_data = validate_data("unittest", yml["unittest"], UNITTEST_SCHEMA) + @unittest_info = valid_data + end + + self + end + + # Override these settings with settings from another file + # @param path [String] the path to the settings yaml file + # @return [ArduinoCI::CIConfig] the new settings object + def with_override(path) + overridden_config = self.class.new + overridden_config.package_info = deep_clone(@package_info) + overridden_config.platform_info = deep_clone(@platform_info) + overridden_config.compile_info = deep_clone(@compile_info) + overridden_config.unittest_info = deep_clone(@unittest_info) + overridden_config.load_yaml(path) + overridden_config + end + + # Try to override config with a file at a given location (if it exists) + # @param path [String] the path to the settings yaml file + # @return [ArduinoCI::CIConfig] the new settings object + def attempt_override(config_path) + return self unless File.exist? config_path + with_override(config_path) + end + + # Produce a configuration, assuming the CI script runs from the working directory of the base project + # @return [ArduinoCI::CIConfig] the new settings object + def from_project_library + attempt_override(CONFIG_FILENAME) + end + + # Produce a configuration override taken from an Arduino library example path + # handle either path to example file or example dir + # @param path [String] the path to the settings yaml file + # @return [ArduinoCI::CIConfig] the new settings object + def from_example(example_path) + base_dir = File.directory?(example_path) ? example_path : File.dirname(example_path) + attempt_override(File.join(base_dir, CONFIG_FILENAME)) + end + + # get information about a given platform: board name, package name, compiler stuff, etc + # @param platform_name [String] The name of the platform as defined in yaml + # @return [Hash] the settings object + def platform_definition(platform_name) + defn = @platform_info[platform_name] + return nil if defn.nil? + deep_clone(defn) + end + + # the URL that gives the download info for a given package (a JSON file). + # this is NOT where the package comes from. + # @param package [String] the package name (e.g. "arduino:avr") + # @return [String] the URL defined for this package + def package_url(package) + return nil if @package_info[package].nil? + @package_info[package][:url] + end + + # platforms to build [the examples on] + # @return [Array] The platforms to build + def platforms_to_build + @compile_info[:platforms] + end + + # platforms to unit test [the tests on] + # @return [Array] The platforms to unit test on + def platforms_to_unittest + @unittest_info[:platforms] + end + + # @return [Array] The aux libraries required for building/verifying + def aux_libraries_for_build + return [] if @compile_info[:libraries].nil? + @compile_info[:libraries] + end + + # @return [Array] The aux libraries required for unit testing + def aux_libraries_for_unittest + return [] if @unittest_info[:libraries].nil? + @unittest_info[:libraries] + end + + # get GCC configuration for a given platform + # @param platform_name [String] The name of the platform as defined in yaml + # @return [Hash] the settings + def gcc_config(platform_name) + plat = @platform_info[platform_name] + return {} if plat.nil? + return {} if plat[:gcc].nil? + plat[:gcc] + end + end + +end diff --git a/lib/arduino_ci/cpp_library.rb b/lib/arduino_ci/cpp_library.rb index ec516f78..8f634583 100644 --- a/lib/arduino_ci/cpp_library.rb +++ b/lib/arduino_ci/cpp_library.rb @@ -3,35 +3,182 @@ HPP_EXTENSIONS = [".hpp", ".hh", ".h", ".hxx", ".h++"].freeze CPP_EXTENSIONS = [".cpp", ".cc", ".c", ".cxx", ".c++"].freeze -ARDUINO_HEADER_DIR = File.expand_path("../../../cpp", __FILE__) +ARDUINO_HEADER_DIR = File.expand_path("../../../cpp/arduino", __FILE__) +UNITTEST_HEADER_DIR = File.expand_path("../../../cpp/unittest", __FILE__) module ArduinoCI # Information about an Arduino CPP library, specifically for compilation purposes class CppLibrary + # @return [String] The path to the library being tested attr_reader :base_dir + # @return [Array] The set of artifacts created by this class (note: incomplete!) + attr_reader :artifacts + + # @return [String] STDERR from the last command + attr_reader :last_err + + # @return [String] STDOUT from the last command + attr_reader :last_out + + # @return [String] the last command + attr_reader :last_cmd + + # @param base_dir [String] The path to the library being tested def initialize(base_dir) - @base_dir = base_dir + @base_dir = File.expand_path(base_dir) + @artifacts = [] + @last_err = "" + @last_out = "" + @last_msg = "" end + # Get a list of all CPP source files in a directory and its subdirectories + # @param some_dir [String] The directory in which to begin the search + # @return [Array] The paths of the found files + def cpp_files_in(some_dir) + real = File.realpath(some_dir) + Find.find(real).select { |path| CPP_EXTENSIONS.include?(File.extname(path)) } + end + + # CPP files that are part of the project library under test + # @return [Array] def cpp_files - Find.find(@base_dir).select { |path| CPP_EXTENSIONS.include?(File.extname(path)) } + real_tests_dir = File.realpath(tests_dir) + cpp_files_in(@base_dir).reject do |p| + next true if File.dirname(p).include?(tests_dir) + next true if File.dirname(p).include?(real_tests_dir) + end + 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) 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) + end + + # The directory where we expect to find unit test defintions provided by the user + # @return [String] + def tests_dir + File.join(@base_dir, "test") + end + + # The files provided by the user that contain unit tests + # @return [Array] + def test_files + cpp_files_in(tests_dir) + end + + # Find all directories in the project library that include C++ header files + # @return [Array] def header_dirs files = Find.find(@base_dir).select { |path| HPP_EXTENSIONS.include?(File.extname(path)) } files.map { |path| File.dirname(path) }.uniq end - def build_args - ["-I#{ARDUINO_HEADER_DIR}"] + header_dirs.map { |d| "-I#{d}" } + cpp_files + # wrapper for the GCC command + def run_gcc(*args, **kwargs) + pipe_out, pipe_out_wr = IO.pipe + pipe_err, pipe_err_wr = IO.pipe + full_args = ["g++"] + args + @last_cmd = " $ #{full_args.join(' ')}" + our_kwargs = { out: pipe_out_wr, err: pipe_err_wr } + eventual_kwargs = our_kwargs.merge(kwargs) + success = Host.run(*full_args, **eventual_kwargs) + + pipe_out_wr.close + pipe_err_wr.close + str_out = pipe_out.read + str_err = pipe_err.read + pipe_out.close + pipe_err.close + @last_err = str_err + @last_out = str_out + success + end + + # GCC command line arguments for including aux libraries + # @param aux_libraries [String] 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) + places = [ARDUINO_HEADER_DIR, UNITTEST_HEADER_DIR] + header_dirs + aux_libraries + places.map { |d| "-I#{d}" } + end + + # GCC command line arguments for features (e.g. -fno-weak) + # @param ci_gcc_config [Hash] The GCC config object + # @return [Array] GCC command-line flags + def feature_args(ci_gcc_config) + return [] if ci_gcc_config[:features].nil? + ci_gcc_config[:features].map { |f| "-f#{f}" } + end + + # GCC command line arguments for warning (e.g. -Wall) + # @param ci_gcc_config [Hash] The GCC config object + # @return [Array] GCC command-line flags + def warning_args(ci_gcc_config) + return [] if ci_gcc_config[:warnings].nil? + ci_gcc_config[:features].map { |w| "-W#{w}" } + end + + # GCC command line arguments for defines (e.g. -Dhave_something) + # @param ci_gcc_config [Hash] The GCC config object + # @return [Array] GCC command-line flags + def define_args(ci_gcc_config) + return [] if ci_gcc_config[:defines].nil? + ci_gcc_config[:defines].map { |d| "-D#{d}" } + end + + # GCC command line arguments as-is + # @param ci_gcc_config [Hash] The GCC config object + # @return [Array] GCC command-line flags + def flag_args(ci_gcc_config) + return [] if ci_gcc_config[:flags].nil? + ci_gcc_config[:flags] + end + + # All GCC command line args for building any unit test + # @param aux_libraries [String] The external Arduino libraries required by this project + # @param ci_gcc_config [Hash] The GCC config object + # @return [Array] GCC command-line flags + def test_args(aux_libraries, ci_gcc_config) + # TODO: something with libraries? + ret = include_args(aux_libraries) + cpp_files_arduino + cpp_files_unittest + cpp_files + unless ci_gcc_config.nil? + cgc = ci_gcc_config + ret = feature_args(cgc) + warning_args(cgc) + define_args(cgc) + flag_args(cgc) + ret + end + ret + end + + # build a file for running a test of the given unit test file + # @param test_file [String] The path to the file containing the unit tests + # @param aux_libraries [String] The external Arduino libraries required by this project + # @param ci_gcc_config [Hash] The GCC config object + # @return [String] path to the compiled test executable + def build_for_test_with_configuration(test_file, aux_libraries, ci_gcc_config) + base = File.basename(test_file) + executable = File.expand_path("unittest_#{base}.bin") + File.delete(executable) if File.exist?(executable) + args = ["-o", executable] + test_args(aux_libraries, ci_gcc_config) + [test_file] + return nil unless run_gcc(*args) + artifacts << executable + executable end - def build(arduino_cmd) - args = ["-c", "-o", "arduino_ci_built.bin"] + build_args - arduino_cmd.run_gcc(*args) + # run a test file + # @param [String] the path to the test file + # @return [bool] whether all tests were successful + def run_test_file(executable) + Host.run(executable) end end diff --git a/lib/arduino_ci/display_manager.rb b/lib/arduino_ci/display_manager.rb index 372af9ae..2445fd09 100644 --- a/lib/arduino_ci/display_manager.rb +++ b/lib/arduino_ci/display_manager.rb @@ -10,7 +10,11 @@ module ArduinoCI # This class handles the setup of that display, if needed. class DisplayManager include Singleton + + # @return [bool] whether the display manager is currently active attr_reader :enabled + + # @return [bool] whether to log messages to the terminal attr_accessor :debug def initialize @@ -19,6 +23,7 @@ def initialize @pid = nil @debug = false + # pipes for input and output @xv_pipe_out_wr = nil @xv_pipe_err_wr = nil @xv_pipe_out = nil @@ -26,6 +31,7 @@ def initialize end # attempt to determine if the machine is running a graphical display (i.e. not Travis) + # @return [bool] whether there is already a GUI that can accept windows def existing_display? return true if RUBY_PLATFORM.include? "darwin" return false if ENV["DISPLAY"].nil? @@ -35,6 +41,8 @@ def existing_display? # check whether a process is alive # https://stackoverflow.com/a/32513298/2063546 + # @param pid [Int] the process ID + # @return [bool] def alive?(pid) Process.kill(0, pid) true @@ -43,6 +51,8 @@ def alive?(pid) end # check whether an X server is taking connections + # @param display [String] the display variable as it would be specified in the environment + # @return [bool] def xserver_exist?(display) system({ "DISPLAY" => display }, "xdpyinfo", out: File::NULL, err: File::NULL) end @@ -128,6 +138,8 @@ def disable end # Enable a virtual display for the duration of the given block + # @yield [environment] The code to execute within the display environment + # @yieldparam [Hash] the environment variables relating to the display def with_display was_enabled = @enabled enable unless was_enabled @@ -139,6 +151,7 @@ def with_display end # run a command in a display + # @return [bool] def run(*args, **kwargs) ret = false # do some work to extract & merge environment variables if they exist @@ -154,10 +167,12 @@ def run(*args, **kwargs) end # run a command in a display with no output + # @return [bool] def run_silent(*args) run(*args, out: File::NULL, err: File::NULL) end + # @return [Hash] the environment variables for the display def environment return nil unless @existing || @enabled return { "EXISTING_DISPLAY" => "YES" } if @existing diff --git a/lib/arduino_ci/host.rb b/lib/arduino_ci/host.rb index 1eb83948..34819406 100644 --- a/lib/arduino_ci/host.rb +++ b/lib/arduino_ci/host.rb @@ -7,6 +7,8 @@ class Host # Cross-platform way of finding an executable in the $PATH. # via https://stackoverflow.com/a/5471032/2063546 # which('ruby') #=> /usr/bin/ruby + # @param cmd [String] the command to search for + # @return [String] the full path to the command if it exists def self.which(cmd) exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : [''] ENV['PATH'].split(File::PATH_SEPARATOR).each do |path| @@ -20,19 +22,10 @@ def self.which(cmd) # run a command in a display def self.run(*args, **kwargs) - # do some work to extract & merge environment variables if they exist - has_env = !args.empty? && args[0].class == Hash - env_vars = has_env ? args[0] : {} - actual_args = has_env ? args[1..-1] : args # need to shift over if we extracted args - full_cmd = env_vars.empty? ? actual_args : [env_vars] + actual_args - shell_vars = env_vars.map { |k, v| "#{k}=#{v}" }.join(" ") - puts " $ #{shell_vars} #{actual_args.join(' ')}" - ret = system(*full_cmd, **kwargs) - status = ret ? "succeeded" : "failed" - puts "Command '#{File.basename(actual_args[0])}' has #{status}" - ret + system(*args, **kwargs) end + # return [Symbol] the operating system of the host def self.os return :osx if OS.osx? return :linux if OS.linux? diff --git a/misc/default.yaml b/misc/default.yaml new file mode 100644 index 00000000..4eb439aa --- /dev/null +++ b/misc/default.yaml @@ -0,0 +1,77 @@ +packages: + esp8266:esp8266: + url: http://arduino.esp8266.com/stable/package_esp8266com_index.json + adafruit:avr: + url: https://adafruit.github.io/arduino-board-index/package_adafruit_index.json + +platforms: + uno: + board: arduino:avr:uno + package: ~ + gcc: + features: + defines: + warnings: + flags: + due: + board: arduino:sam:arduino_due_x + package: arduino:sam + gcc: + features: + defines: + warnings: + flags: + zero: + board: arduino:samd:zero + package: arduino:samd + gcc: + features: + defines: + warnings: + flags: + esp8266: + board: esp8266:esp8266:huzzah + package: esp8266:esp8266 + gcc: + features: + defines: + warnings: + flags: + leonardo: + board: arduino:avr:leonardo + package: ~ + gcc: + features: + defines: + warnings: + flags: + trinket: + board: adafruit:avr:trinket5 + package: adafruit:avr + gcc: + features: + defines: + warnings: + flags: + gemma: + board: arduino:avr:gemma + package: adafruit:avr + gcc: + features: + defines: + warnings: + flags: + +compile: + libraries: ~ + platforms: + - uno + - due + - leonardo + +unittest: + libraries: ~ + platforms: + - uno + - due + - leonardo diff --git a/spec/arduino_cmd_spec.rb b/spec/arduino_cmd_spec.rb index 139a43be..325aea6b 100644 --- a/spec/arduino_cmd_spec.rb +++ b/spec/arduino_cmd_spec.rb @@ -7,6 +7,17 @@ def get_sketch(dir, file) RSpec.describe ArduinoCI::ArduinoCmd do arduino_cmd = ArduinoCI::ArduinoInstallation.autolocate! + + after(:each) do |example| + if example.exception + puts "Last message: #{arduino_cmd.last_msg}" + puts "========== Stdout:" + puts arduino_cmd.last_out + puts "========== Stderr:" + puts arduino_cmd.last_err + end + end + context "initialize" do it "sets base vars" do expect(arduino_cmd.base_cmd).not_to be nil @@ -28,6 +39,13 @@ def get_sketch(dir, file) end end + context "installation of boards" do + it "installs and sets boards" do + expect(arduino_cmd.install_boards("arduino:sam")).to be true + expect(arduino_cmd.use_board("arduino:sam:arduino_due_x")).to be true + end + end + context "set_pref" do it "Sets key to what it was before" do diff --git a/spec/arduino_installation_spec.rb b/spec/arduino_installation_spec.rb index 861053fa..c95ba7d3 100644 --- a/spec/arduino_installation_spec.rb +++ b/spec/arduino_installation_spec.rb @@ -17,14 +17,8 @@ arduino_cmd = ArduinoCI::ArduinoInstallation.autolocate! it "doesn't fail" do expect(arduino_cmd.base_cmd).not_to be nil - expect(arduino_cmd.gcc_cmd).not_to be nil expect(arduino_cmd._lib_dir).not_to be nil end - - it "produces a working AVR-GCC" do - expect(arduino_cmd.gcc_cmd).not_to be nil - expect(arduino_cmd.run_gcc("--version")).to be true - end end end diff --git a/spec/ci_config_spec.rb b/spec/ci_config_spec.rb new file mode 100644 index 00000000..929fa166 --- /dev/null +++ b/spec/ci_config_spec.rb @@ -0,0 +1,70 @@ +require "spec_helper" + +RSpec.describe ArduinoCI::CIConfig do + context "default" do + it "loads from yaml" do + default_config = ArduinoCI::CIConfig.default + expect(default_config).not_to be nil + uno = default_config.platform_definition("uno") + expect(uno.class).to eq(Hash) + expect(uno[:board]).to eq("arduino:avr:uno") + expect(uno[:package]).to be nil + expect(uno[:gcc].class).to eq(Hash) + + due = default_config.platform_definition("due") + expect(due.class).to eq(Hash) + expect(due[:board]).to eq("arduino:sam:arduino_due_x") + expect(due[:package]).to eq("arduino:sam") + expect(due[:gcc].class).to eq(Hash) + + zero = default_config.platform_definition("zero") + expect(zero.class).to eq(Hash) + expect(zero[:board]).to eq("arduino:samd:zero") + expect(zero[:package]).to eq("arduino:samd") + expect(zero[:gcc].class).to eq(Hash) + + expect(default_config.package_url("adafruit:avr")).to eq("https://adafruit.github.io/arduino-board-index/package_adafruit_index.json") + expect(default_config.platforms_to_build).to match(["uno", "due", "leonardo"]) + expect(default_config.platforms_to_unittest).to match(["uno", "due", "leonardo"]) + expect(default_config.aux_libraries_for_build).to match([]) + expect(default_config.aux_libraries_for_unittest).to match([]) + end + end + + 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) + expect(combined_config).not_to be nil + uno = combined_config.platform_definition("uno") + expect(uno.class).to eq(Hash) + expect(uno[:board]).to eq("arduino:avr:uno") + expect(uno[:package]).to be nil + expect(uno[:gcc].class).to eq(Hash) + + zero = combined_config.platform_definition("zero") + expect(zero).to be nil + + esp = combined_config.platform_definition("esp8266") + expect(esp[:board]).to eq("esp8266:esp8266:booo") + expect(esp[:package]).to eq("esp8266:esp8266") + + bogo = combined_config.platform_definition("bogo") + expect(bogo.class).to eq(Hash) + expect(bogo[:package]).to eq("potato:salad") + expect(bogo[:gcc].class).to eq(Hash) + expect(bogo[:gcc][:features]).to match(["a", "b"]) + expect(bogo[:gcc][:defines]).to match(["c", "d"]) + expect(bogo[:gcc][:warnings]).to match(["e", "f"]) + expect(bogo[:gcc][:flags]).to match(["g", "h"]) + + expect(combined_config.package_url("adafruit:avr")).to eq("https://adafruit.github.io/arduino-board-index/package_adafruit_index.json") + expect(combined_config.platforms_to_build).to match(["esp8266"]) + expect(combined_config.platforms_to_unittest).to match(["bogo"]) + expect(combined_config.aux_libraries_for_build).to match(["Adafruit FONA Library"]) + expect(combined_config.aux_libraries_for_unittest).to match(["abc123", "def456"]) + end + end + +end + diff --git a/spec/cpp_library_spec.rb b/spec/cpp_library_spec.rb index 238a90fe..176cd0a7 100644 --- a/spec/cpp_library_spec.rb +++ b/spec/cpp_library_spec.rb @@ -3,28 +3,68 @@ sampleproj_path = File.join(File.dirname(File.dirname(__FILE__)), "SampleProjects") RSpec.describe ArduinoCI::CppLibrary do - cpp_lib_path = File.join(sampleproj_path, "DoSomething") + cpp_lib_path = File.join(sampleproj_path, "TestSomething") cpp_library = ArduinoCI::CppLibrary.new(cpp_lib_path) context "cpp_files" do it "finds cpp files in directory" do - dosomething_cpp_files = ["DoSomething/do-something.cpp"] + testsomething_cpp_files = ["TestSomething/test-something.cpp"] relative_paths = cpp_library.cpp_files.map { |f| f.split("SampleProjects/", 2)[1] } - expect(relative_paths).to match_array(dosomething_cpp_files) + expect(relative_paths).to match_array(testsomething_cpp_files) end end context "header_dirs" do it "finds directories containing h files" do - dosomething_header_dirs = ["DoSomething"] + testsomething_header_dirs = ["TestSomething"] relative_paths = cpp_library.header_dirs.map { |f| f.split("SampleProjects/", 2)[1] } - expect(relative_paths).to match_array(dosomething_header_dirs) + expect(relative_paths).to match_array(testsomething_header_dirs) end end - context "build" do - arduino_cmd = ArduinoCI::ArduinoInstallation.autolocate! - it "builds libraries" do - expect(cpp_library.build(arduino_cmd)).to be true + context "tests_dir" do + it "locate the tests directory" do + testsomething_header_dirs = ["TestSomething"] + relative_path = cpp_library.tests_dir.split("SampleProjects/", 2)[1] + expect(relative_path).to eq("TestSomething/test") + end + end + + context "test_files" do + it "finds cpp files in directory" do + testsomething_test_files = [ + "TestSomething/test/good-null.cpp", + "TestSomething/test/good-library.cpp", + "TestSomething/test/bad-null.cpp" + ] + relative_paths = cpp_library.test_files.map { |f| f.split("SampleProjects/", 2)[1] } + expect(relative_paths).to match_array(testsomething_test_files) + end + end + + context "test" do + after(:each) do |example| + if example.exception + puts "Last command: #{cpp_library.last_cmd}" + puts "========== Stdout:" + puts cpp_library.last_out + puts "========== Stderr:" + puts cpp_library.last_err + end + end + + it "is going to test more than one library" do + test_files = cpp_library.test_files + expect(test_files.empty?).to be false + end + + test_files = cpp_library.test_files + test_files.each do |path| + expected = path.include?("good") + it "tests #{File.basename(path)} expecting #{expected}" do + exe = cpp_library.build_for_test_with_configuration(path, [], nil) + expect(exe).not_to be nil + expect(cpp_library.run_test_file(exe)).to eq(expected) + end end end diff --git a/spec/yaml/o1.yaml b/spec/yaml/o1.yaml new file mode 100644 index 00000000..bb9abe87 --- /dev/null +++ b/spec/yaml/o1.yaml @@ -0,0 +1,39 @@ +platforms: + bogo: + board: fakeduino:beep:bogo + package: potato:salad + gcc: + features: + - a + - b + defines: + - c + - d + warnings: + - e + - f + flags: + - g + - h + zero: ~ + esp8266: + board: esp8266:esp8266:booo + package: esp8266:esp8266 + gcc: + features: + defines: + warnings: + flags: + +compile: + libraries: + - "Adafruit FONA Library" + platforms: + - esp8266 + +unittest: + libraries: + - "abc123" + - "def456" + platforms: + - bogo