From 5d1ed4f6e382aa445c904b9f97214b09d44f9eee Mon Sep 17 00:00:00 2001 From: Ian Katz Date: Thu, 18 Jan 2018 13:41:57 -0500 Subject: [PATCH 01/39] find test files, separate from cpp files --- SampleProjects/DoSomething/test/basic.cpp | 0 lib/arduino_ci/cpp_library.rb | 11 ++++++++++- spec/cpp_library_spec.rb | 16 ++++++++++++++++ 3 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 SampleProjects/DoSomething/test/basic.cpp diff --git a/SampleProjects/DoSomething/test/basic.cpp b/SampleProjects/DoSomething/test/basic.cpp new file mode 100644 index 00000000..e69de29b diff --git a/lib/arduino_ci/cpp_library.rb b/lib/arduino_ci/cpp_library.rb index ec516f78..3b7862c5 100644 --- a/lib/arduino_ci/cpp_library.rb +++ b/lib/arduino_ci/cpp_library.rb @@ -17,7 +17,16 @@ def initialize(base_dir) end def cpp_files - Find.find(@base_dir).select { |path| CPP_EXTENSIONS.include?(File.extname(path)) } + all_cpp = Find.find(@base_dir).select { |path| CPP_EXTENSIONS.include?(File.extname(path)) } + all_cpp.reject { |p| p.start_with?(tests_dir) } + end + + def tests_dir + File.join(@base_dir, "test") + end + + def test_files + Find.find(tests_dir).select { |path| CPP_EXTENSIONS.include?(File.extname(path)) } end def header_dirs diff --git a/spec/cpp_library_spec.rb b/spec/cpp_library_spec.rb index 238a90fe..6aceb8b5 100644 --- a/spec/cpp_library_spec.rb +++ b/spec/cpp_library_spec.rb @@ -21,6 +21,22 @@ end end + context "tests_dir" do + it "locate the tests directory" do + dosomething_header_dirs = ["DoSomething"] + relative_path = cpp_library.tests_dir.split("SampleProjects/", 2)[1] + expect(relative_path).to eq("DoSomething/test") + end + end + + context "test_files" do + it "finds cpp files in directory" do + dosomething_test_files = ["DoSomething/test/basic.cpp"] + relative_paths = cpp_library.test_files.map { |f| f.split("SampleProjects/", 2)[1] } + expect(relative_paths).to match_array(dosomething_test_files) + end + end + context "build" do arduino_cmd = ArduinoCI::ArduinoInstallation.autolocate! it "builds libraries" do From 64736b4980f2b78fdbff36a0185a1bbf661b4484 Mon Sep 17 00:00:00 2001 From: Ian Katz Date: Thu, 18 Jan 2018 13:49:30 -0500 Subject: [PATCH 02/39] move cpp files in preparation for alternate test files dir --- cpp/{ => arduino}/Arduino.h | 0 cpp/{ => arduino}/math.h | 0 lib/arduino_ci/cpp_library.rb | 2 +- 3 files changed, 1 insertion(+), 1 deletion(-) rename cpp/{ => arduino}/Arduino.h (100%) rename cpp/{ => arduino}/math.h (100%) diff --git a/cpp/Arduino.h b/cpp/arduino/Arduino.h similarity index 100% rename from cpp/Arduino.h rename to cpp/arduino/Arduino.h diff --git a/cpp/math.h b/cpp/arduino/math.h similarity index 100% rename from cpp/math.h rename to cpp/arduino/math.h diff --git a/lib/arduino_ci/cpp_library.rb b/lib/arduino_ci/cpp_library.rb index 3b7862c5..1230ac26 100644 --- a/lib/arduino_ci/cpp_library.rb +++ b/lib/arduino_ci/cpp_library.rb @@ -3,7 +3,7 @@ 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__) module ArduinoCI From bfc33628d01169fafc4ccbde84bb70946bbeabac Mon Sep 17 00:00:00 2001 From: Ian Katz Date: Fri, 19 Jan 2018 14:13:47 -0500 Subject: [PATCH 03/39] unit tests working, rough cut --- SampleProjects/DoSomething/do-something.cpp | 2 +- SampleProjects/DoSomething/test/bad-null.cpp | 12 + SampleProjects/DoSomething/test/basic.cpp | 0 .../DoSomething/test/good-library.cpp | 11 + SampleProjects/DoSomething/test/good-null.cpp | 24 ++ cpp/arduino/Arduino.cpp | 13 + cpp/arduino/Arduino.h | 19 +- cpp/arduino/AvrMath.h | 37 ++ cpp/arduino/math.h | 41 --- cpp/unittest/ArduinoUnitTests.cpp | 6 + cpp/unittest/ArduinoUnitTests.h | 219 ++++++++++++ cpp/unittest/Assertion.h | 41 +++ cpp/unittest/Compare.h | 334 ++++++++++++++++++ lib/arduino_ci/cpp_library.rb | 48 ++- spec/cpp_library_spec.rb | 22 +- 15 files changed, 763 insertions(+), 66 deletions(-) create mode 100644 SampleProjects/DoSomething/test/bad-null.cpp delete mode 100644 SampleProjects/DoSomething/test/basic.cpp create mode 100644 SampleProjects/DoSomething/test/good-library.cpp create mode 100644 SampleProjects/DoSomething/test/good-null.cpp create mode 100644 cpp/arduino/Arduino.cpp create mode 100644 cpp/arduino/AvrMath.h delete mode 100644 cpp/arduino/math.h create mode 100644 cpp/unittest/ArduinoUnitTests.cpp create mode 100644 cpp/unittest/ArduinoUnitTests.h create mode 100644 cpp/unittest/Assertion.h create mode 100644 cpp/unittest/Compare.h 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/test/bad-null.cpp b/SampleProjects/DoSomething/test/bad-null.cpp new file mode 100644 index 00000000..d2d1cd1e --- /dev/null +++ b/SampleProjects/DoSomething/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/DoSomething/test/basic.cpp b/SampleProjects/DoSomething/test/basic.cpp deleted file mode 100644 index e69de29b..00000000 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/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 index 12cdc863..ba18b248 100644 --- a/cpp/arduino/Arduino.h +++ b/cpp/arduino/Arduino.h @@ -1,3 +1,4 @@ +#pragma once /* Mock Arduino.h library. @@ -6,25 +7,13 @@ Where possible, variable names from the Arduino library are used to avoid confli */ -#ifndef ARDUINO_CI_ARDUINO -#include "math.h" -#define ARDUINO_CI_ARDUINO +#include "AvrMath.h" 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; -} +unsigned long millis(); -#endif +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/arduino/math.h b/cpp/arduino/math.h deleted file mode 100644 index f7f6e372..00000000 --- a/cpp/arduino/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..5769f5f1 --- /dev/null +++ b/cpp/unittest/ArduinoUnitTests.cpp @@ -0,0 +1,6 @@ +#include "ArduinoUnitTests.h" + +Test* Test::sRoot = 0; +Test* Test::sCurrent = 0; +int Test::mAssertCounter = 0; +int Test::mTestCounter = 0; diff --git a/cpp/unittest/ArduinoUnitTests.h b/cpp/unittest/ArduinoUnitTests.h new file mode 100644 index 00000000..64cccd74 --- /dev/null +++ b/cpp/unittest/ArduinoUnitTests.h @@ -0,0 +1,219 @@ +#pragma once + +#include "Assertion.h" +#include +using namespace std; + +struct Results { + int passed; + int failed; + int skipped; // TODO: not sure about this +}; + +class Test +{ + + + private: + const char* mName; + + // linked list structure for active tests + static Test* sRoot; + static Test* sCurrent; + 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; + int mAssertions; + + 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; + append(); + } + + inline void fail() { mResult = RESULT_FAIL; } + inline void skip() { mResult = RESULT_SKIP; } + + + static int mTestCounter; + static int mAssertCounter; + + static void onTestRunInit(int numTests) { + cout << "TAP version 13" << endl; + cout << 1 << ".." << numTests << endl; // we know how many tests, in advance + mTestCounter = 0; + } + + static void onTestStart(Test* test) { + mAssertCounter = 0; + ++mTestCounter; + cout << "# Subtest: " << test->name() << endl; + } + + static void onTestEnd(Test* test) { + cout << " 1.." << mAssertCounter << endl; + if (test->result() == RESULT_PASS) { + cout << "ok " << mTestCounter << " - " << test->name() << endl; + } else { + cout << "not ok " << mTestCounter << " - " << test->name() << endl; + } + } + + template + static void onAssert( + const char* file, + int line, + const char* description, + bool pass, + const char* lhsLabel, + const A &lhs, + const char* opLabel, + 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 << " expected: " << lhsLabel << endl; + cout << " actual: " << endl; + cout << " at:" << endl; + cout << " file: " << file << endl; + cout << " line: " << line << endl; + cout << " ..." << endl; + } + } + + static Results run() { + onTestRunInit(numTests()); + Results results = {0, 0, 0}; + + for (Test *p = sRoot; p; p = p->mNext) { + sCurrent = p; + onTestStart(p); + p->test(); + if (p->mResult == RESULT_PASS) ++results.passed; + if (p->mResult == RESULT_FAIL) ++results.failed; + if (p->mResult == RESULT_SKIP) ++results.skipped; + onTestEnd(p); + } + + 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 deicde how to report + static int run_and_report(int argc, char *argv[]) { + Results results = run(); + 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(); + } + + // FYI + // #define assertOp(arg1,op,op_name,arg2) do { if (!assertion(__FILE__,__LINE__,#arg1,(arg1),op_name,op,#arg2,(arg2))) return; } while (0) + // #define assertEqual(arg1,arg2) assertOp(arg1,compareEqual,"==",arg2) + + template + bool assertion( + const char *file, + int line, + const char *description, + const char *lhss, + const A &lhs, + + const char *ops, + + bool (*op)( + const A &lhs, + const B &rhs), + + const char *rhss, + const B &rhs) + { + ++mAssertCounter; + bool ok = op(lhs, rhs); + onAssert(file, line, description, ok, lhss, lhs, ops, rhss, rhs); + + if (!ok) + sCurrent->fail(); + return ok; + } + + public: + class Reporter { + public: + Reporter() {} + virtual ~Reporter() {} + + void onInit(int numTests) {} + void onTest(Test* test) {} + void onTestEnd(Test* test) {} + void onAssert() {} + void onFinish(Results results) {} + }; + + class ReporterTAP : Reporter { + private: + + public: + ReporterTAP() : Reporter() {} + ~ReporterTAP() {} + }; + +}; + +/** + * 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..d4702849 --- /dev/null +++ b/cpp/unittest/Assertion.h @@ -0,0 +1,41 @@ +#pragma once +#include "Compare.h" + +// helper define for the operators below +#define assertOp(desc, arg1, op, op_name, arg2) \ + do \ + { \ + if (!assertion(__FILE__, __LINE__, \ + desc, \ + #arg1, (arg1), \ + op_name, op, \ + #arg2, (arg2))) \ + { \ + return; \ + } \ + } while (0) + +/** macro generates optional output and calls fail() followed by a return if false. */ +#define assertEqual(arg1,arg2) assertOp("assertEqual",arg1,compareEqual,"==",arg2) + +/** macro generates optional output and calls fail() followed by a return if false. */ +#define assertNotEqual(arg1,arg2) assertOp("assertNotEqual",arg1,compareNotEqual,"!=",arg2) + +/** macro generates optional output and calls fail() followed by a return if false. */ +#define assertLess(arg1,arg2) assertOp("assertLess",arg1,compareLess,"<",arg2) + +/** macro generates optional output and calls fail() followed by a return if false. */ +#define assertMore(arg1,arg2) assertOp("assertMore",arg1,compareMore,">",arg2) + +/** macro generates optional output and calls fail() followed by a return if false. */ +#define assertLessOrEqual(arg1,arg2) assertOp("assertLessOrEqual",arg1,compareLessOrEqual,"<=",arg2) + +/** macro generates optional output and calls fail() followed by a return if false. */ +#define assertMoreOrEqual(arg1,arg2) assertOp("assertMoreOrEqual",arg1,compareMoreOrEqual,">=",arg2) + +/** macro generates optional output and calls fail() followed by a return if false. */ +#define assertTrue(arg) assertEqual(arg,true) + +/** macro generates optional output and calls fail() followed by a return if false. */ +#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/lib/arduino_ci/cpp_library.rb b/lib/arduino_ci/cpp_library.rb index 1230ac26..0b2ad1e1 100644 --- a/lib/arduino_ci/cpp_library.rb +++ b/lib/arduino_ci/cpp_library.rb @@ -4,6 +4,7 @@ HPP_EXTENSIONS = [".hpp", ".hh", ".h", ".hxx", ".h++"].freeze CPP_EXTENSIONS = [".cpp", ".cc", ".c", ".cxx", ".c++"].freeze ARDUINO_HEADER_DIR = File.expand_path("../../../cpp/arduino", __FILE__) +UNITTEST_HEADER_DIR = File.expand_path("../../../cpp/unittest", __FILE__) module ArduinoCI @@ -11,14 +12,27 @@ module ArduinoCI class CppLibrary attr_reader :base_dir + attr_reader :artifacts def initialize(base_dir) @base_dir = base_dir + @artifacts = [] + end + + def cpp_files_in(some_dir) + Find.find(some_dir).select { |path| CPP_EXTENSIONS.include?(File.extname(path)) } end def cpp_files - all_cpp = Find.find(@base_dir).select { |path| CPP_EXTENSIONS.include?(File.extname(path)) } - all_cpp.reject { |p| p.start_with?(tests_dir) } + cpp_files_in(@base_dir).reject { |p| p.start_with?(tests_dir) } + end + + def cpp_files_arduino + cpp_files_in(ARDUINO_HEADER_DIR) + end + + def cpp_files_unittest + cpp_files_in(UNITTEST_HEADER_DIR) end def tests_dir @@ -26,7 +40,7 @@ def tests_dir end def test_files - Find.find(tests_dir).select { |path| CPP_EXTENSIONS.include?(File.extname(path)) } + cpp_files_in(tests_dir) end def header_dirs @@ -34,13 +48,33 @@ def header_dirs files.map { |path| File.dirname(path) }.uniq end + def run_gcc(*args, **kwargs) + # TODO: detect env!! + full_args = ["g++"] + args + Host.run(*full_args, **kwargs) + end + def build_args - ["-I#{ARDUINO_HEADER_DIR}"] + header_dirs.map { |d| "-I#{d}" } + cpp_files + ["-I#{ARDUINO_HEADER_DIR}"] + header_dirs.map { |d| "-I#{d}" } + end + + def build(target_file) + args = ["-c", "-o", "arduino_ci_built.bin"] + build_args + [target_file] + run_gcc(*args) + end + + def test_args + ["-I#{UNITTEST_HEADER_DIR}"] + build_args + cpp_files_arduino + cpp_files_unittest + cpp_files end - def build(arduino_cmd) - args = ["-c", "-o", "arduino_ci_built.bin"] + build_args - arduino_cmd.run_gcc(*args) + def test(test_file) + base = File.basename(test_file) + executable = File.expand_path("unittest_#{base}.bin") + File.delete(executable) if File.exist?(executable) + args = ["-o", executable] + test_args + [test_file] + return false unless run_gcc(*args) + artifacts << executable + Host.run(executable) end end diff --git a/spec/cpp_library_spec.rb b/spec/cpp_library_spec.rb index 6aceb8b5..baf7ecac 100644 --- a/spec/cpp_library_spec.rb +++ b/spec/cpp_library_spec.rb @@ -31,7 +31,11 @@ context "test_files" do it "finds cpp files in directory" do - dosomething_test_files = ["DoSomething/test/basic.cpp"] + dosomething_test_files = [ + "DoSomething/test/good-null.cpp", + "DoSomething/test/good-library.cpp", + "DoSomething/test/bad-null.cpp" + ] relative_paths = cpp_library.test_files.map { |f| f.split("SampleProjects/", 2)[1] } expect(relative_paths).to match_array(dosomething_test_files) end @@ -40,7 +44,21 @@ context "build" do arduino_cmd = ArduinoCI::ArduinoInstallation.autolocate! it "builds libraries" do - expect(cpp_library.build(arduino_cmd)).to be true + cpp_library.cpp_files.each do |path| + expect(cpp_library.build(path)).to be true + end + + end + end + + context "test" do + arduino_cmd = ArduinoCI::ArduinoInstallation.autolocate! + it "tests libraries" do + test_files = cpp_library.test_files + expect(test_files.empty?).to be false + test_files.each do |path| + expect(cpp_library.test(path)).to eq(path.include?("good")) + end end end From 740b57a32618f07928069c2453fb6e99e756ac86 Mon Sep 17 00:00:00 2001 From: Ian Katz Date: Sun, 21 Jan 2018 21:08:14 -0500 Subject: [PATCH 04/39] unit tests with TAP output --- cpp/unittest/ArduinoUnitTests.cpp | 3 - cpp/unittest/ArduinoUnitTests.h | 179 ++++++++++++++---------------- cpp/unittest/Assertion.h | 32 ++---- 3 files changed, 95 insertions(+), 119 deletions(-) diff --git a/cpp/unittest/ArduinoUnitTests.cpp b/cpp/unittest/ArduinoUnitTests.cpp index 5769f5f1..7f3d0827 100644 --- a/cpp/unittest/ArduinoUnitTests.cpp +++ b/cpp/unittest/ArduinoUnitTests.cpp @@ -1,6 +1,3 @@ #include "ArduinoUnitTests.h" Test* Test::sRoot = 0; -Test* Test::sCurrent = 0; -int Test::mAssertCounter = 0; -int Test::mTestCounter = 0; diff --git a/cpp/unittest/ArduinoUnitTests.h b/cpp/unittest/ArduinoUnitTests.h index 64cccd74..3f92d29d 100644 --- a/cpp/unittest/ArduinoUnitTests.h +++ b/cpp/unittest/ArduinoUnitTests.h @@ -10,16 +10,78 @@ struct Results { 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; - static Test* sCurrent; Test* mNext; void append() { @@ -44,7 +106,6 @@ class Test // current test result int mResult; - int mAssertions; public: static const int RESULT_NONE = 0; @@ -55,78 +116,28 @@ class Test const inline char *name() { return mName; } const inline int result() { return mResult; } - Test(const char *_name) : mName(_name) - { + 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 int mTestCounter; - static int mAssertCounter; - - static void onTestRunInit(int numTests) { - cout << "TAP version 13" << endl; - cout << 1 << ".." << numTests << endl; // we know how many tests, in advance - mTestCounter = 0; - } - - static void onTestStart(Test* test) { - mAssertCounter = 0; - ++mTestCounter; - cout << "# Subtest: " << test->name() << endl; - } - - static void onTestEnd(Test* test) { - cout << " 1.." << mAssertCounter << endl; - if (test->result() == RESULT_PASS) { - cout << "ok " << mTestCounter << " - " << test->name() << endl; - } else { - cout << "not ok " << mTestCounter << " - " << test->name() << endl; - } - } - - template - static void onAssert( - const char* file, - int line, - const char* description, - bool pass, - const char* lhsLabel, - const A &lhs, - const char* opLabel, - 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 << " expected: " << lhsLabel << endl; - cout << " actual: " << endl; - cout << " at:" << endl; - cout << " file: " << file << endl; - cout << " line: " << line << endl; - cout << " ..." << endl; - } - } - - static Results run() { - onTestRunInit(numTests()); + static Results run(ReporterTAP* reporter) { + if (reporter) reporter->onTestRunInit(numTests()); Results results = {0, 0, 0}; for (Test *p = sRoot; p; p = p->mNext) { - sCurrent = p; - onTestStart(p); + 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; - onTestEnd(p); + if (reporter) reporter->onTestEnd(td); } return results; @@ -135,9 +146,11 @@ class Test // 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 deicde how to report + // parse input and decide how to report static int run_and_report(int argc, char *argv[]) { - Results results = run(); + // TODO: pick a reporter based on args + ReporterTAP rep; + Results results = run(&rep); return results.failed + results.skipped; } @@ -152,16 +165,13 @@ class Test excise(); } - // FYI - // #define assertOp(arg1,op,op_name,arg2) do { if (!assertion(__FILE__,__LINE__,#arg1,(arg1),op_name,op,#arg2,(arg2))) return; } while (0) - // #define assertEqual(arg1,arg2) assertOp(arg1,compareEqual,"==",arg2) - template bool assertion( const char *file, int line, const char *description, - const char *lhss, + const char *lhsRelevance, + const char *lhsLabel, const A &lhs, const char *ops, @@ -170,38 +180,21 @@ class Test const A &lhs, const B &rhs), - const char *rhss, + const char *rhsRelevance, + const char *rhsLabel, const B &rhs) { - ++mAssertCounter; bool ok = op(lhs, rhs); - onAssert(file, line, description, ok, lhss, lhs, ops, rhss, rhs); + + if (mReporter) { + mReporter->onAssert(file, line, description, ok, + lhsRelevance, lhsLabel, lhs, ops, rhsRelevance, rhsLabel, rhs); + } if (!ok) - sCurrent->fail(); + fail(); return ok; - } - - public: - class Reporter { - public: - Reporter() {} - virtual ~Reporter() {} - - void onInit(int numTests) {} - void onTest(Test* test) {} - void onTestEnd(Test* test) {} - void onAssert() {} - void onFinish(Results results) {} - }; - - class ReporterTAP : Reporter { - private: - - public: - ReporterTAP() : Reporter() {} - ~ReporterTAP() {} - }; + } }; diff --git a/cpp/unittest/Assertion.h b/cpp/unittest/Assertion.h index d4702849..1a53bff3 100644 --- a/cpp/unittest/Assertion.h +++ b/cpp/unittest/Assertion.h @@ -2,40 +2,26 @@ #include "Compare.h" // helper define for the operators below -#define assertOp(desc, arg1, op, op_name, arg2) \ +#define assertOp(desc, relevance1, arg1, op, op_name, relevance2, arg2) \ do \ { \ if (!assertion(__FILE__, __LINE__, \ desc, \ - #arg1, (arg1), \ + relevance1, #arg1, (arg1), \ op_name, op, \ - #arg2, (arg2))) \ + 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",arg1,compareEqual,"==",arg2) - -/** macro generates optional output and calls fail() followed by a return if false. */ -#define assertNotEqual(arg1,arg2) assertOp("assertNotEqual",arg1,compareNotEqual,"!=",arg2) - -/** macro generates optional output and calls fail() followed by a return if false. */ -#define assertLess(arg1,arg2) assertOp("assertLess",arg1,compareLess,"<",arg2) - -/** macro generates optional output and calls fail() followed by a return if false. */ -#define assertMore(arg1,arg2) assertOp("assertMore",arg1,compareMore,">",arg2) - -/** macro generates optional output and calls fail() followed by a return if false. */ -#define assertLessOrEqual(arg1,arg2) assertOp("assertLessOrEqual",arg1,compareLessOrEqual,"<=",arg2) - -/** macro generates optional output and calls fail() followed by a return if false. */ -#define assertMoreOrEqual(arg1,arg2) assertOp("assertMoreOrEqual",arg1,compareMoreOrEqual,">=",arg2) - -/** 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) - -/** macro generates optional output and calls fail() followed by a return if false. */ #define assertFalse(arg) assertEqual(arg,false) From 12e3680e5b19a2d5441a33f406e7840fed1d8c38 Mon Sep 17 00:00:00 2001 From: Ian Katz Date: Sun, 21 Jan 2018 21:55:18 -0500 Subject: [PATCH 05/39] remove unused gcc / avr-gcc code --- lib/arduino_ci/arduino_cmd.rb | 7 ------- lib/arduino_ci/arduino_installation.rb | 8 -------- spec/arduino_installation_spec.rb | 6 ------ 3 files changed, 21 deletions(-) diff --git a/lib/arduino_ci/arduino_cmd.rb b/lib/arduino_ci/arduino_cmd.rb index f6fcbd76..e5911e21 100644 --- a/lib/arduino_ci/arduino_cmd.rb +++ b/lib/arduino_ci/arduino_cmd.rb @@ -18,7 +18,6 @@ def self.flag(name, text = nil) attr_accessor :installation attr_accessor :base_cmd - attr_accessor :gcc_cmd attr_reader :library_is_indexed @@ -99,12 +98,6 @@ def run(*args, **kwargs) _run(*full_args, **kwargs) end - def run_gcc(*args, **kwargs) - # TODO: detect env!! - full_args = @gcc_cmd + args - _run(*full_args, **kwargs) - end - # run a command and capture its output # @return [Hash] {:out => String, :err => String, :success => bool} def run_and_capture(*args, **kwargs) diff --git a/lib/arduino_ci/arduino_installation.rb b/lib/arduino_ci/arduino_installation.rb index c7baee0d..6ce0a49e 100644 --- a/lib/arduino_ci/arduino_installation.rb +++ b/lib/arduino_ci/arduino_installation.rb @@ -48,21 +48,16 @@ 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 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 +65,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 +74,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,7 +81,6 @@ 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 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 From f469faac2e26f5ee4026820f9b6c5e2ea6be9dec Mon Sep 17 00:00:00 2001 From: Ian Katz Date: Mon, 22 Jan 2018 15:07:53 -0500 Subject: [PATCH 06/39] more rubocop rules muted --- .rubocop.yml | 3 +++ 1 file changed, 3 insertions(+) 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 From b8a25d04fc3e63e1835aebda8c756a4db736e001 Mon Sep 17 00:00:00 2001 From: Ian Katz Date: Mon, 22 Jan 2018 15:09:02 -0500 Subject: [PATCH 07/39] add overridable config --- CHANGELOG.md | 2 + lib/arduino_ci.rb | 1 + lib/arduino_ci/ci_config.rb | 115 ++++++++++++++++++++++++++++++++++++ misc/default.yaml | 73 +++++++++++++++++++++++ spec/ci_config_spec.rb | 58 ++++++++++++++++++ spec/yaml/o1.yaml | 34 +++++++++++ 6 files changed, 283 insertions(+) create mode 100644 lib/arduino_ci/ci_config.rb create mode 100644 misc/default.yaml create mode 100644 spec/ci_config_spec.rb create mode 100644 spec/yaml/o1.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md index f58dd6fa..7bb55897 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,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/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/ci_config.rb b/lib/arduino_ci/ci_config.rb new file mode 100644 index 00000000..570b95fc --- /dev/null +++ b/lib/arduino_ci/ci_config.rb @@ -0,0 +1,115 @@ +require 'yaml' + +# base config (platforms) +# project config - .arduino_ci_platforms.yml +# example config - .arduino_ci_plan.yml + +PLATFORM_SCHEMA = { + board: String, + package: String, + gcc: { + features: Array, + defines: Array, + warnings: Array, + flags: Array, + } +}.freeze + +COMPILE_SCHEMA = { + platforms: Array, +}.freeze + +UNITTEST_SCHEMA = { + platforms: Array, +}.freeze +module ArduinoCI + + # 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 + def default + ret = new + ret.load_yaml(File.expand_path("../../../misc/default.yaml", __FILE__)) + ret + end + end + + attr_accessor :platform_info + attr_accessor :compile_info + attr_accessor :unittest_info + def initialize + @platform_info = {} + @compile_info = {} + @unittest_info = {} + end + + 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 + 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 + + def load_yaml(path) + yml = YAML.load_file(path) + 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 + + def with_override(path) + overridden_config = self.class.new + overridden_config.platform_info = deep_clone(@platform_info) + overridden_config.unittest_info = deep_clone(@unittest_info) + overridden_config.load_yaml(path) + overridden_config + end + + def platform_definition(platform_name) + defn = @platform_info[platform_name] + return nil if defn.nil? + deep_clone(defn) + end + + end + +end diff --git a/misc/default.yaml b/misc/default.yaml new file mode 100644 index 00000000..e08693f6 --- /dev/null +++ b/misc/default.yaml @@ -0,0 +1,73 @@ +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: ~ + gcc: + features: + defines: + warnings: + flags: + +compile: + platforms: + - uno + - due + - zero + - esp8266 + - leonardo + +unittest: + platforms: + - uno + - due + - zero + - esp8266 + - leonardo diff --git a/spec/ci_config_spec.rb b/spec/ci_config_spec.rb new file mode 100644 index 00000000..d4b173b6 --- /dev/null +++ b/spec/ci_config_spec.rb @@ -0,0 +1,58 @@ +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) + 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"]) + end + end + +end + diff --git a/spec/yaml/o1.yaml b/spec/yaml/o1.yaml new file mode 100644 index 00000000..1d57fa53 --- /dev/null +++ b/spec/yaml/o1.yaml @@ -0,0 +1,34 @@ +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: + platforms: + - esp8266 + +unittest: + platforms: + - bogo From 55a9ef64cd21fcae9c3aef912f74d27689757b08 Mon Sep 17 00:00:00 2001 From: Ian Katz Date: Mon, 22 Jan 2018 21:17:49 -0500 Subject: [PATCH 08/39] ignore all bin artifacts --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 545f4374ebaf9ef230e1d0dd4b30abd0bf66a4b7 Mon Sep 17 00:00:00 2001 From: Ian Katz Date: Mon, 22 Jan 2018 21:18:34 -0500 Subject: [PATCH 09/39] add config section for packages and urls --- lib/arduino_ci/ci_config.rb | 28 ++++++++++++++++++++++++++++ misc/default.yaml | 8 +++++++- spec/ci_config_spec.rb | 8 ++++++++ 3 files changed, 43 insertions(+), 1 deletion(-) diff --git a/lib/arduino_ci/ci_config.rb b/lib/arduino_ci/ci_config.rb index 570b95fc..777971ad 100644 --- a/lib/arduino_ci/ci_config.rb +++ b/lib/arduino_ci/ci_config.rb @@ -4,6 +4,10 @@ # project config - .arduino_ci_platforms.yml # example config - .arduino_ci_plan.yml +PACKAGE_SCHEMA = { + url: String +}.freeze + PLATFORM_SCHEMA = { board: String, package: String, @@ -40,10 +44,12 @@ def default 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 = {} @@ -76,6 +82,13 @@ def validate_data(rootname, source, schema) 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) @@ -98,7 +111,9 @@ def load_yaml(path) 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 @@ -110,6 +125,19 @@ def platform_definition(platform_name) deep_clone(defn) end + def package_url(package) + return nil if @package_info[package].nil? + @package_info[package][:url] + end + + def build_platforms + @compile_info[:platforms] + end + + def unittest_platforms + @unittest_info[:platforms] + end + end end diff --git a/misc/default.yaml b/misc/default.yaml index e08693f6..d7cab34b 100644 --- a/misc/default.yaml +++ b/misc/default.yaml @@ -1,3 +1,9 @@ +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 @@ -49,7 +55,7 @@ platforms: flags: gemma: board: arduino:avr:gemma - package: ~ + package: adafruit:avr gcc: features: defines: diff --git a/spec/ci_config_spec.rb b/spec/ci_config_spec.rb index d4b173b6..c58a363a 100644 --- a/spec/ci_config_spec.rb +++ b/spec/ci_config_spec.rb @@ -22,6 +22,10 @@ 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.build_platforms).to match(["uno", "due", "zero", "esp8266", "leonardo"]) + expect(default_config.unittest_platforms).to match(["uno", "due", "zero", "esp8266", "leonardo"]) end end @@ -51,6 +55,10 @@ 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.build_platforms).to match(["esp8266"]) + expect(combined_config.unittest_platforms).to match(["bogo"]) end end From ec47c44a9ac39fc4338074df53736f0bdeef6afd Mon Sep 17 00:00:00 2001 From: Ian Katz Date: Mon, 22 Jan 2018 21:40:47 -0500 Subject: [PATCH 10/39] better logic around library installation --- exe/ci_system_check.rb | 6 ++++-- lib/arduino_ci/arduino_cmd.rb | 13 ++++++++++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/exe/ci_system_check.rb b/exe/ci_system_check.rb index fea71cb5..ae9fdd15 100755 --- a/exe/ci_system_check.rb +++ b/exe/ci_system_check.rb @@ -26,10 +26,12 @@ 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 "Installing Adafruit FONA library" +got_problem = true unless arduino_cmd.install_library("Adafruit FONA Library") puts "checking that library is indexed" got_problem = true unless arduino_cmd.library_is_indexed +puts "Installing Adafruit FONA library a second time to see what happens" +got_problem = true unless arduino_cmd.install_library("Adafruit FONA Library") my_board = "arduino:sam:arduino_due_x" diff --git a/lib/arduino_ci/arduino_cmd.rb b/lib/arduino_ci/arduino_cmd.rb index e5911e21..dd360de9 100644 --- a/lib/arduino_ci/arduino_cmd.rb +++ b/lib/arduino_ci/arduino_cmd.rb @@ -141,8 +141,19 @@ def install_board(boardname) # @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 From 1706e92013b68c6a7de7351146ab01f484cf04bc Mon Sep 17 00:00:00 2001 From: Ian Katz Date: Mon, 22 Jan 2018 22:27:30 -0500 Subject: [PATCH 11/39] remove build-with-gcc; we want test-with-gcc only --- exe/ci_system_check.rb | 1 - lib/arduino_ci/cpp_library.rb | 9 --------- spec/cpp_library_spec.rb | 10 ---------- 3 files changed, 20 deletions(-) diff --git a/exe/ci_system_check.rb b/exe/ci_system_check.rb index ae9fdd15..5015e72d 100755 --- a/exe/ci_system_check.rb +++ b/exe/ci_system_check.rb @@ -52,7 +52,6 @@ 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" diff --git a/lib/arduino_ci/cpp_library.rb b/lib/arduino_ci/cpp_library.rb index 0b2ad1e1..ea9d5049 100644 --- a/lib/arduino_ci/cpp_library.rb +++ b/lib/arduino_ci/cpp_library.rb @@ -54,15 +54,6 @@ def run_gcc(*args, **kwargs) Host.run(*full_args, **kwargs) end - def build_args - ["-I#{ARDUINO_HEADER_DIR}"] + header_dirs.map { |d| "-I#{d}" } - end - - def build(target_file) - args = ["-c", "-o", "arduino_ci_built.bin"] + build_args + [target_file] - run_gcc(*args) - end - def test_args ["-I#{UNITTEST_HEADER_DIR}"] + build_args + cpp_files_arduino + cpp_files_unittest + cpp_files end diff --git a/spec/cpp_library_spec.rb b/spec/cpp_library_spec.rb index baf7ecac..f7a5b734 100644 --- a/spec/cpp_library_spec.rb +++ b/spec/cpp_library_spec.rb @@ -41,16 +41,6 @@ end end - context "build" do - arduino_cmd = ArduinoCI::ArduinoInstallation.autolocate! - it "builds libraries" do - cpp_library.cpp_files.each do |path| - expect(cpp_library.build(path)).to be true - end - - end - end - context "test" do arduino_cmd = ArduinoCI::ArduinoInstallation.autolocate! it "tests libraries" do From 77b1e117adcbbc0fa698bc7c42713a5c45a50b91 Mon Sep 17 00:00:00 2001 From: Ian Katz Date: Mon, 22 Jan 2018 22:30:57 -0500 Subject: [PATCH 12/39] rename methods for build / unittest platform lists --- lib/arduino_ci/ci_config.rb | 4 ++-- spec/ci_config_spec.rb | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/arduino_ci/ci_config.rb b/lib/arduino_ci/ci_config.rb index 777971ad..a5085a5f 100644 --- a/lib/arduino_ci/ci_config.rb +++ b/lib/arduino_ci/ci_config.rb @@ -130,11 +130,11 @@ def package_url(package) @package_info[package][:url] end - def build_platforms + def platforms_to_build @compile_info[:platforms] end - def unittest_platforms + def platforms_to_unittest @unittest_info[:platforms] end diff --git a/spec/ci_config_spec.rb b/spec/ci_config_spec.rb index c58a363a..e91453e5 100644 --- a/spec/ci_config_spec.rb +++ b/spec/ci_config_spec.rb @@ -24,8 +24,8 @@ 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.build_platforms).to match(["uno", "due", "zero", "esp8266", "leonardo"]) - expect(default_config.unittest_platforms).to match(["uno", "due", "zero", "esp8266", "leonardo"]) + expect(default_config.platforms_to_build).to match(["uno", "due", "zero", "esp8266", "leonardo"]) + expect(default_config.platforms_to_unittest).to match(["uno", "due", "zero", "esp8266", "leonardo"]) end end @@ -57,8 +57,8 @@ 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.build_platforms).to match(["esp8266"]) - expect(combined_config.unittest_platforms).to match(["bogo"]) + expect(combined_config.platforms_to_build).to match(["esp8266"]) + expect(combined_config.platforms_to_unittest).to match(["bogo"]) end end From dd05470660351e156e55849213e97d5ee1990054 Mon Sep 17 00:00:00 2001 From: Ian Katz Date: Mon, 22 Jan 2018 22:42:10 -0500 Subject: [PATCH 13/39] add ability to specify aux libraries --- lib/arduino_ci/ci_config.rb | 12 ++++++++++++ misc/default.yaml | 2 ++ spec/ci_config_spec.rb | 4 ++++ spec/yaml/o1.yaml | 5 +++++ 4 files changed, 23 insertions(+) diff --git a/lib/arduino_ci/ci_config.rb b/lib/arduino_ci/ci_config.rb index a5085a5f..d7199a77 100644 --- a/lib/arduino_ci/ci_config.rb +++ b/lib/arduino_ci/ci_config.rb @@ -21,10 +21,12 @@ COMPILE_SCHEMA = { platforms: Array, + libraries: Array, }.freeze UNITTEST_SCHEMA = { platforms: Array, + libraries: Array, }.freeze module ArduinoCI @@ -138,6 +140,16 @@ def platforms_to_unittest @unittest_info[:platforms] end + def aux_libraries_for_build + return [] if @compile_info[:libraries].nil? + @compile_info[:libraries] + end + + def aux_libraries_for_unittest + return [] if @unittest_info[:libraries].nil? + @unittest_info[:libraries] + end + end end diff --git a/misc/default.yaml b/misc/default.yaml index d7cab34b..6dc18bab 100644 --- a/misc/default.yaml +++ b/misc/default.yaml @@ -63,6 +63,7 @@ platforms: flags: compile: + libraries: ~ platforms: - uno - due @@ -71,6 +72,7 @@ compile: - leonardo unittest: + libraries: ~ platforms: - uno - due diff --git a/spec/ci_config_spec.rb b/spec/ci_config_spec.rb index e91453e5..15568ed4 100644 --- a/spec/ci_config_spec.rb +++ b/spec/ci_config_spec.rb @@ -26,6 +26,8 @@ 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", "zero", "esp8266", "leonardo"]) expect(default_config.platforms_to_unittest).to match(["uno", "due", "zero", "esp8266", "leonardo"]) + expect(default_config.aux_libraries_for_build).to match([]) + expect(default_config.aux_libraries_for_unittest).to match([]) end end @@ -59,6 +61,8 @@ 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 diff --git a/spec/yaml/o1.yaml b/spec/yaml/o1.yaml index 1d57fa53..bb9abe87 100644 --- a/spec/yaml/o1.yaml +++ b/spec/yaml/o1.yaml @@ -26,9 +26,14 @@ platforms: flags: compile: + libraries: + - "Adafruit FONA Library" platforms: - esp8266 unittest: + libraries: + - "abc123" + - "def456" platforms: - bogo From df56e6780b67790f6f96320c801849c2ca5a65cd Mon Sep 17 00:00:00 2001 From: Ian Katz Date: Tue, 23 Jan 2018 16:45:17 -0500 Subject: [PATCH 14/39] better prefs cache especially on read before write --- lib/arduino_ci/arduino_cmd.rb | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/arduino_ci/arduino_cmd.rb b/lib/arduino_ci/arduino_cmd.rb index dd360de9..a4825741 100644 --- a/lib/arduino_ci/arduino_cmd.rb +++ b/lib/arduino_ci/arduino_cmd.rb @@ -32,7 +32,8 @@ def self.flag(name, text = nil) flag :verify def initialize - @prefs_cache = nil + @prefs_cache = {} + @prefs_fetched = false @library_is_indexed = false end @@ -58,7 +59,7 @@ def _prefs_raw end 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 @@ -66,7 +67,7 @@ def prefs # get a preference key def get_pref(key) - data = @prefs_cache.nil? ? prefs : @prefs_cache + data = @prefs_fetched ? @prefs_cache : prefs data[key] end From 227f3209dbee684be29811844beb799be0f5b2a2 Mon Sep 17 00:00:00 2001 From: Ian Katz Date: Tue, 23 Jan 2018 16:46:35 -0500 Subject: [PATCH 15/39] rename method to reflect boards not board being installed --- lib/arduino_ci/arduino_cmd.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/arduino_ci/arduino_cmd.rb b/lib/arduino_ci/arduino_cmd.rb index a4825741..78f4b5d6 100644 --- a/lib/arduino_ci/arduino_cmd.rb +++ b/lib/arduino_ci/arduino_cmd.rb @@ -133,9 +133,9 @@ 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] + run_and_capture(flag_install_boards, boardfamily, out: File::NULL)[:success] end # install a library by name @@ -180,7 +180,7 @@ 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 From a7c420d5cc1485565ec0c87951f6e5b2a673b48d Mon Sep 17 00:00:00 2001 From: Ian Katz Date: Tue, 23 Jan 2018 16:47:31 -0500 Subject: [PATCH 16/39] make sure paths are expanded when linking libraries --- lib/arduino_ci/arduino_cmd.rb | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/arduino_ci/arduino_cmd.rb b/lib/arduino_ci/arduino_cmd.rb index 78f4b5d6..da0cebd0 100644 --- a/lib/arduino_ci/arduino_cmd.rb +++ b/lib/arduino_ci/arduino_cmd.rb @@ -200,19 +200,20 @@ def verify_sketch(path) # ensure that the given library is installed, or symlinked as appropriate # return the path of the prepared library, or nil 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 + puts "#{uhoh} and it's not symlinked to #{realpath}" return nil end @@ -221,7 +222,7 @@ def install_local_library(path) end # install the library - FileUtils.ln_s(path, destination_path) + FileUtils.ln_s(realpath, destination_path) destination_path end From 66f3fff1a7df05176cd0ef3253a2aa3504b4b9fb Mon Sep 17 00:00:00 2001 From: Ian Katz Date: Tue, 23 Jan 2018 16:48:33 -0500 Subject: [PATCH 17/39] return an array, don't yield to a block --- lib/arduino_ci/arduino_cmd.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/arduino_ci/arduino_cmd.rb b/lib/arduino_ci/arduino_cmd.rb index da0cebd0..15b874f5 100644 --- a/lib/arduino_ci/arduino_cmd.rb +++ b/lib/arduino_ci/arduino_cmd.rb @@ -226,14 +226,14 @@ def install_local_library(path) destination_path end - def each_library_example(installed_library_path) + 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 From 066c79aae4c1ce72aed5c45600d4b316b9870915 Mon Sep 17 00:00:00 2001 From: Ian Katz Date: Tue, 23 Jan 2018 16:50:42 -0500 Subject: [PATCH 18/39] allow overriding config, set standard naming convention --- lib/arduino_ci/ci_config.rb | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/lib/arduino_ci/ci_config.rb b/lib/arduino_ci/ci_config.rb index d7199a77..3caff0dd 100644 --- a/lib/arduino_ci/ci_config.rb +++ b/lib/arduino_ci/ci_config.rb @@ -30,6 +30,8 @@ }.freeze module ArduinoCI + 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 @@ -121,6 +123,24 @@ def with_override(path) overridden_config end + # Try to override config with a file at a given location (if it exists) + def attempt_override(config_path) + return self unless File.exist? config_path + with_override(config_path) + end + + # assume the script runs from the working directory of the base project + def from_project_library + attempt_override(CONFIG_FILENAME) + end + + # handle either path to example file or example dir + 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 def platform_definition(platform_name) defn = @platform_info[platform_name] return nil if defn.nil? From e9eb4afb1608d0047e5d3bb8f11765af151e64ef Mon Sep 17 00:00:00 2001 From: Ian Katz Date: Tue, 23 Jan 2018 16:51:12 -0500 Subject: [PATCH 19/39] commenting --- lib/arduino_ci/ci_config.rb | 4 ++++ lib/arduino_ci/cpp_library.rb | 7 +++++++ 2 files changed, 11 insertions(+) diff --git a/lib/arduino_ci/ci_config.rb b/lib/arduino_ci/ci_config.rb index 3caff0dd..86e4f6e4 100644 --- a/lib/arduino_ci/ci_config.rb +++ b/lib/arduino_ci/ci_config.rb @@ -147,15 +147,19 @@ def platform_definition(platform_name) 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. def package_url(package) return nil if @package_info[package].nil? @package_info[package][:url] end + # platforms to build [the examples on] def platforms_to_build @compile_info[:platforms] end + # platforms to unit test [the tests on] def platforms_to_unittest @unittest_info[:platforms] end diff --git a/lib/arduino_ci/cpp_library.rb b/lib/arduino_ci/cpp_library.rb index ea9d5049..f5915eb3 100644 --- a/lib/arduino_ci/cpp_library.rb +++ b/lib/arduino_ci/cpp_library.rb @@ -23,31 +23,38 @@ def cpp_files_in(some_dir) Find.find(some_dir).select { |path| CPP_EXTENSIONS.include?(File.extname(path)) } end + # CPP files that are part of the project library under test def cpp_files cpp_files_in(@base_dir).reject { |p| p.start_with?(tests_dir) } end + # CPP files that are part of the arduino mock library we're providing def cpp_files_arduino cpp_files_in(ARDUINO_HEADER_DIR) end + # CPP files that are part of the unit test library we're providing 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 def tests_dir File.join(@base_dir, "test") end + # The files provided by the user that contain unit tests def test_files cpp_files_in(tests_dir) end + # Find all directories in the project library that include C++ header files def header_dirs files = Find.find(@base_dir).select { |path| HPP_EXTENSIONS.include?(File.extname(path)) } files.map { |path| File.dirname(path) }.uniq end + # wrapper for the GCC command def run_gcc(*args, **kwargs) # TODO: detect env!! full_args = ["g++"] + args From 9409f08e04d8e0ce670383215b85a5def7b064e4 Mon Sep 17 00:00:00 2001 From: Ian Katz Date: Tue, 23 Jan 2018 16:51:29 -0500 Subject: [PATCH 20/39] factor out gcc args --- lib/arduino_ci/ci_config.rb | 32 ++++++++++++++++++++++ lib/arduino_ci/cpp_library.rb | 51 ++++++++++++++++++++++++++++++++--- 2 files changed, 79 insertions(+), 4 deletions(-) diff --git a/lib/arduino_ci/ci_config.rb b/lib/arduino_ci/ci_config.rb index 86e4f6e4..b3b93e92 100644 --- a/lib/arduino_ci/ci_config.rb +++ b/lib/arduino_ci/ci_config.rb @@ -174,6 +174,38 @@ def aux_libraries_for_unittest @unittest_info[:libraries] end + def features(platform_name) + plat = @platform_info[platform_name] + return [] if plat.nil? + return [] if plat[:gcc].nil? + return [] if plat[:gcc][:features].nil? + plat[:gcc][:features] + end + + def warnings(platform_name) + plat = @platform_info[platform_name] + return [] if plat.nil? + return [] if plat[:gcc].nil? + return [] if plat[:gcc][:warnings].nil? + plat[:gcc][:warnings] + end + + def flags(platform_name) + plat = @platform_info[platform_name] + return [] if plat.nil? + return [] if plat[:gcc].nil? + return [] if plat[:gcc][:flags].nil? + plat[:gcc][:flags] + end + + def defines(platform_name) + plat = @platform_info[platform_name] + return [] if plat.nil? + return [] if plat[:gcc].nil? + return [] if plat[:gcc][:flags].nil? + plat[:gcc][:flags] + end + end end diff --git a/lib/arduino_ci/cpp_library.rb b/lib/arduino_ci/cpp_library.rb index f5915eb3..719601b4 100644 --- a/lib/arduino_ci/cpp_library.rb +++ b/lib/arduino_ci/cpp_library.rb @@ -61,20 +61,63 @@ def run_gcc(*args, **kwargs) Host.run(*full_args, **kwargs) end - def test_args - ["-I#{UNITTEST_HEADER_DIR}"] + build_args + cpp_files_arduino + cpp_files_unittest + cpp_files + # GCC command line arguments for including aux libraries + def include_args(aux_libraries) + places = [ARDUINO_HEADER_DIR, UNITTEST_HEADER_DIR] + header_dirs + aux_libraries + places.map { |d| "-I#{d}" } end - def test(test_file) + # GCC command line arguments for features (e.g. -fno-weak) + 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) + 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) + 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 + 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 + def test_args(aux_libraries, ci_gcc_config) + # TODO: something with libraries? + cgc = ci_gcc_config + ret = include_args(aux_libraries) + cpp_files_arduino + cpp_files_unittest + cpp_files + unless ci_gcc_config.nil? + ret = feature_args(cgc) + warning_args(cgc) + define_args(cgc) + flag_args(cgc) + ret + end + ret + end + + # run a test of the given unit test file + def 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 + [test_file] + args = ["-o", executable] + test_args(aux_libraries, ci_gcc_config) + [test_file] return false unless run_gcc(*args) artifacts << executable Host.run(executable) end + # legacy shortcut for rspec + def test(test_file) + test_with_configuration(test_file, [], nil) + end + end end From f8ce3ba8927f605d87d77e8ae84e508712ddfd93 Mon Sep 17 00:00:00 2001 From: Ian Katz Date: Tue, 23 Jan 2018 16:52:39 -0500 Subject: [PATCH 21/39] try new CI script --- .travis.yml | 7 +- SampleProjects/DoSomething/Gemfile.lock | 2 + exe/arduino_ci_remote.rb | 99 +++++++++++++++++++++++++ exe/ci_system_check.rb | 3 - 4 files changed, 106 insertions(+), 5 deletions(-) create mode 100755 exe/arduino_ci_remote.rb 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/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/exe/arduino_ci_remote.rb b/exe/arduino_ci_remote.rb new file mode 100755 index 00000000..479e8686 --- /dev/null +++ b/exe/arduino_ci_remote.rb @@ -0,0 +1,99 @@ +#!/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 + puts "Failures: #{@failure_count}" + 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) + +# 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_command.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}") { arduino_cmd.verify_sketch(example_path) } + 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}") { cpp_library.test(unittest_path) } + end +end diff --git a/exe/ci_system_check.rb b/exe/ci_system_check.rb index 5015e72d..cf1f8652 100755 --- a/exe/ci_system_check.rb +++ b/exe/ci_system_check.rb @@ -50,9 +50,6 @@ 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) - puts "verify the examples of a library (#{library_path})..." puts " - Install the library" installed_library_path = arduino_cmd.install_local_library(library_path) From bac46d71c9cb8649f645b87e47d2b72e1d88b076 Mon Sep 17 00:00:00 2001 From: Ian Katz Date: Wed, 24 Jan 2018 09:13:16 -0500 Subject: [PATCH 22/39] handle case where board package is already installed, and return success --- lib/arduino_ci/arduino_cmd.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/arduino_ci/arduino_cmd.rb b/lib/arduino_ci/arduino_cmd.rb index 15b874f5..eae57370 100644 --- a/lib/arduino_ci/arduino_cmd.rb +++ b/lib/arduino_ci/arduino_cmd.rb @@ -135,7 +135,9 @@ def board_installed?(boardname) # @return [bool] whether the command succeeded def install_boards(boardfamily) # TODO: find out why IO.pipe fails but File::NULL succeeds :( - run_and_capture(flag_install_boards, boardfamily, 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 From d696dbd11c0345b126ea0bf2ce31aef03c5c16c3 Mon Sep 17 00:00:00 2001 From: Ian Katz Date: Wed, 24 Jan 2018 10:03:08 -0500 Subject: [PATCH 23/39] hide chattiness of system commands behind member variables --- lib/arduino_ci/arduino_cmd.rb | 35 +++++++++++++++++++++++++---------- lib/arduino_ci/host.rb | 12 +----------- 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/lib/arduino_ci/arduino_cmd.rb b/lib/arduino_ci/arduino_cmd.rb index eae57370..5d434b2f 100644 --- a/lib/arduino_ci/arduino_cmd.rb +++ b/lib/arduino_ci/arduino_cmd.rb @@ -20,6 +20,9 @@ def self.flag(name, text = nil) attr_accessor :base_cmd attr_reader :library_is_indexed + attr_reader :last_out + attr_reader :last_err + attr_reader :last_msg # set the command line flags (undefined for now). # These vary between gui/cli @@ -32,9 +35,12 @@ def self.flag(name, text = nil) flag :verify def initialize - @prefs_cache = {} - @prefs_fetched = false - @library_is_indexed = false + @prefs_cache = {} + @prefs_fetched = false + @library_is_indexed = false + @last_out = "" + @last_err = "" + @last_msg = "" end def parse_pref_string(arduino_output) @@ -94,9 +100,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) + # 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 @@ -113,6 +126,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 @@ -189,11 +204,11 @@ def use_board!(boardname) 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) @@ -215,11 +230,11 @@ def install_local_library(path) # maybe it's a symlink? that would be OK if File.symlink?(destination_path) return destination_path if File.readlink(destination_path) == realpath - puts "#{uhoh} and it's not symlinked to #{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 diff --git a/lib/arduino_ci/host.rb b/lib/arduino_ci/host.rb index 1eb83948..b9b6c1db 100644 --- a/lib/arduino_ci/host.rb +++ b/lib/arduino_ci/host.rb @@ -20,17 +20,7 @@ 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 def self.os From b1ea18b450a082e2f94036de91ede99478b684d8 Mon Sep 17 00:00:00 2001 From: Ian Katz Date: Wed, 24 Jan 2018 10:03:32 -0500 Subject: [PATCH 24/39] print errors on failure --- exe/arduino_ci_remote.rb | 27 +++++++++++++++++---------- spec/arduino_cmd_spec.rb | 18 ++++++++++++++++++ 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/exe/arduino_ci_remote.rb b/exe/arduino_ci_remote.rb index 479e8686..50bbcb42 100755 --- a/exe/arduino_ci_remote.rb +++ b/exe/arduino_ci_remote.rb @@ -9,6 +9,13 @@ # terminate after printing any debug info. TODO: capture debug info def terminate puts "Failures: #{@failure_count}" + unless failure_count.zero? + 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 @@ -41,11 +48,11 @@ def assure(message, &block) # initialize command and config config = ArduinoCI::CIConfig.default.from_project_library -arduino_cmd = ArduinoCI::ArduinoInstallation.autolocate! +@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) +installed_library_path = assure("Installing library under test") { @arduino_cmd.install_local_library(".") } +library_examples = @arduino_cmd.library_examples(installed_library_path) # gather up all required boards so we can install them up front. # start with the "platforms to unittest" and add the examples @@ -64,34 +71,34 @@ def assure(message, &block) 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(",")) + @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) + @arduino_cmd.install_boards(p) end end aux_libraries.each do |l| - assure("Installing aux library '#{l}'") { arduino_command.install_library(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") } +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) } + assure("Switching to board for #{p} (#{board})") { @arduino_cmd.use_board(board) } example_name = File.basename(example_path) - attempt("Verifying #{example_name}") { arduino_cmd.verify_sketch(example_path) } + attempt("Verifying #{example_name}") { @arduino_cmd.verify_sketch(example_path) } 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) } + 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}") { cpp_library.test(unittest_path) } 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 From bb4b9dd6f6410c50d64e41bcb83b0033ab05ac60 Mon Sep 17 00:00:00 2001 From: Ian Katz Date: Wed, 24 Jan 2018 10:18:46 -0500 Subject: [PATCH 25/39] stop testing on failing platform arduino:samd:zero --- misc/default.yaml | 2 -- spec/ci_config_spec.rb | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/misc/default.yaml b/misc/default.yaml index 6dc18bab..b92b0014 100644 --- a/misc/default.yaml +++ b/misc/default.yaml @@ -67,7 +67,6 @@ compile: platforms: - uno - due - - zero - esp8266 - leonardo @@ -76,6 +75,5 @@ unittest: platforms: - uno - due - - zero - esp8266 - leonardo diff --git a/spec/ci_config_spec.rb b/spec/ci_config_spec.rb index 15568ed4..07e9cb10 100644 --- a/spec/ci_config_spec.rb +++ b/spec/ci_config_spec.rb @@ -24,8 +24,8 @@ 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", "zero", "esp8266", "leonardo"]) - expect(default_config.platforms_to_unittest).to match(["uno", "due", "zero", "esp8266", "leonardo"]) + expect(default_config.platforms_to_build).to match(["uno", "due", "esp8266", "leonardo"]) + expect(default_config.platforms_to_unittest).to match(["uno", "due", "esp8266", "leonardo"]) expect(default_config.aux_libraries_for_build).to match([]) expect(default_config.aux_libraries_for_unittest).to match([]) end From d7f95b09fa049070b5f07079f3f591ad2d5eb934 Mon Sep 17 00:00:00 2001 From: Ian Katz Date: Wed, 24 Jan 2018 10:41:41 -0500 Subject: [PATCH 26/39] more handling of stdout --- exe/arduino_ci_remote.rb | 12 +++++++++++- lib/arduino_ci/arduino_cmd.rb | 3 ++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/exe/arduino_ci_remote.rb b/exe/arduino_ci_remote.rb index 50bbcb42..25e17d99 100755 --- a/exe/arduino_ci_remote.rb +++ b/exe/arduino_ci_remote.rb @@ -92,7 +92,17 @@ def assure(message, &block) 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}") { @arduino_cmd.verify_sketch(example_path) } + attempt("Verifying #{example_name}") do + ret = @arduino_cmd.verify_sketch(example_path) + unless ret + puts "Last message: #{@arduino_cmd.last_msg}" + puts "========== Stdout:" + puts @arduino_cmd.last_out + puts "========== Stderr:" + puts @arduino_cmd.last_err + end + ret + end end end diff --git a/lib/arduino_ci/arduino_cmd.rb b/lib/arduino_ci/arduino_cmd.rb index 5d434b2f..1f1ec60c 100644 --- a/lib/arduino_ci/arduino_cmd.rb +++ b/lib/arduino_ci/arduino_cmd.rb @@ -211,7 +211,8 @@ def verify_sketch(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 From 13c85649bba0374d46b5899f5ed04bec22e3e605 Mon Sep 17 00:00:00 2001 From: Ian Katz Date: Wed, 24 Jan 2018 10:43:40 -0500 Subject: [PATCH 27/39] fix cpp_library reference in script --- exe/arduino_ci_remote.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/exe/arduino_ci_remote.rb b/exe/arduino_ci_remote.rb index 25e17d99..070b4e9a 100755 --- a/exe/arduino_ci_remote.rb +++ b/exe/arduino_ci_remote.rb @@ -53,6 +53,7 @@ def assure(message, &block) # 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) # gather up all required boards so we can install them up front. # start with the "platforms to unittest" and add the examples From 8be5e244e1fb96fbb5b8b09fe57f64adf88a4f1d Mon Sep 17 00:00:00 2001 From: Ian Katz Date: Wed, 24 Jan 2018 11:15:48 -0500 Subject: [PATCH 28/39] Track command and output of GCC --- exe/arduino_ci_remote.rb | 26 ++++++++++++++++++------ lib/arduino_ci/ci_config.rb | 34 ++++---------------------------- lib/arduino_ci/cpp_library.rb | 37 ++++++++++++++++++++++++++++++++--- 3 files changed, 58 insertions(+), 39 deletions(-) diff --git a/exe/arduino_ci_remote.rb b/exe/arduino_ci_remote.rb index 070b4e9a..6778e8da 100755 --- a/exe/arduino_ci_remote.rb +++ b/exe/arduino_ci_remote.rb @@ -9,7 +9,7 @@ # terminate after printing any debug info. TODO: capture debug info def terminate puts "Failures: #{@failure_count}" - unless failure_count.zero? + unless @failure_count.zero? puts "Last message: #{@arduino_cmd.last_msg}" puts "========== Stdout:" puts @arduino_cmd.last_out @@ -96,10 +96,8 @@ def assure(message, &block) attempt("Verifying #{example_name}") do ret = @arduino_cmd.verify_sketch(example_path) unless ret - puts "Last message: #{@arduino_cmd.last_msg}" - puts "========== Stdout:" - puts @arduino_cmd.last_out - puts "========== Stderr:" + puts + puts "Last command: #{@arduino_cmd.last_msg}" puts @arduino_cmd.last_err end ret @@ -112,6 +110,22 @@ def assure(message, &block) 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}") { cpp_library.test(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) + ) + unless exe + puts + 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 diff --git a/lib/arduino_ci/ci_config.rb b/lib/arduino_ci/ci_config.rb index b3b93e92..9777d296 100644 --- a/lib/arduino_ci/ci_config.rb +++ b/lib/arduino_ci/ci_config.rb @@ -174,38 +174,12 @@ def aux_libraries_for_unittest @unittest_info[:libraries] end - def features(platform_name) + def gcc_config(platform_name) plat = @platform_info[platform_name] - return [] if plat.nil? - return [] if plat[:gcc].nil? - return [] if plat[:gcc][:features].nil? - plat[:gcc][:features] + return {} if plat.nil? + return {} if plat[:gcc].nil? + plat[:gcc] end - - def warnings(platform_name) - plat = @platform_info[platform_name] - return [] if plat.nil? - return [] if plat[:gcc].nil? - return [] if plat[:gcc][:warnings].nil? - plat[:gcc][:warnings] - end - - def flags(platform_name) - plat = @platform_info[platform_name] - return [] if plat.nil? - return [] if plat[:gcc].nil? - return [] if plat[:gcc][:flags].nil? - plat[:gcc][:flags] - end - - def defines(platform_name) - plat = @platform_info[platform_name] - return [] if plat.nil? - return [] if plat[:gcc].nil? - return [] if plat[:gcc][:flags].nil? - plat[:gcc][:flags] - end - end end diff --git a/lib/arduino_ci/cpp_library.rb b/lib/arduino_ci/cpp_library.rb index 719601b4..b9304c90 100644 --- a/lib/arduino_ci/cpp_library.rb +++ b/lib/arduino_ci/cpp_library.rb @@ -14,9 +14,16 @@ class CppLibrary attr_reader :base_dir attr_reader :artifacts + attr_reader :last_err + attr_reader :last_out + attr_reader :last_cmd + def initialize(base_dir) @base_dir = base_dir @artifacts = [] + @last_err = "" + @last_out = "" + @last_msg = "" end def cpp_files_in(some_dir) @@ -56,9 +63,23 @@ def header_dirs # wrapper for the GCC command def run_gcc(*args, **kwargs) - # TODO: detect env!! + pipe_out, pipe_out_wr = IO.pipe + pipe_err, pipe_err_wr = IO.pipe full_args = ["g++"] + args - Host.run(*full_args, **kwargs) + @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 @@ -104,12 +125,22 @@ def test_args(aux_libraries, ci_gcc_config) # run a test of the given unit test file def test_with_configuration(test_file, aux_libraries, ci_gcc_config) + executable = build_for_test_with_configuration(test_file, aux_libraries, ci_gcc_config) + run_test_file(executable) + end + + # run a test of the given unit test file + 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 false unless run_gcc(*args) + return nil unless run_gcc(*args) artifacts << executable + executable + end + + def run_test_file(executable) Host.run(executable) end From 8c1aabed8601219efabc99b9bc7cb9ca36495a96 Mon Sep 17 00:00:00 2001 From: Ian Katz Date: Wed, 24 Jan 2018 13:52:11 -0500 Subject: [PATCH 29/39] conditionally print logs -- not on final failure --- exe/arduino_ci_remote.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/exe/arduino_ci_remote.rb b/exe/arduino_ci_remote.rb index 6778e8da..83e340e0 100755 --- a/exe/arduino_ci_remote.rb +++ b/exe/arduino_ci_remote.rb @@ -7,9 +7,9 @@ @failure_count = 0 # terminate after printing any debug info. TODO: capture debug info -def terminate +def terminate(final = nil) puts "Failures: #{@failure_count}" - unless @failure_count.zero? + unless @failure_count.zero? && final.nil? puts "Last message: #{@arduino_cmd.last_msg}" puts "========== Stdout:" puts @arduino_cmd.last_out @@ -128,4 +128,4 @@ def assure(message, &block) end end -terminate +terminate(true) From 9dc9d4e1186ed34d2612a01530db086a45549aaf Mon Sep 17 00:00:00 2001 From: Ian Katz Date: Wed, 24 Jan 2018 13:52:39 -0500 Subject: [PATCH 30/39] don't suppress code files that begin with the word test --- lib/arduino_ci/cpp_library.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/arduino_ci/cpp_library.rb b/lib/arduino_ci/cpp_library.rb index b9304c90..fbd9c771 100644 --- a/lib/arduino_ci/cpp_library.rb +++ b/lib/arduino_ci/cpp_library.rb @@ -32,7 +32,7 @@ def cpp_files_in(some_dir) # CPP files that are part of the project library under test def cpp_files - cpp_files_in(@base_dir).reject { |p| p.start_with?(tests_dir) } + cpp_files_in(@base_dir).reject { |p| p.start_with?(tests_dir + File::SEPARATOR) } end # CPP files that are part of the arduino mock library we're providing From 32f80c475d1e0cb30befc5f817f16784af85a3ef Mon Sep 17 00:00:00 2001 From: Ian Katz Date: Wed, 24 Jan 2018 13:54:11 -0500 Subject: [PATCH 31/39] separate example project for CI from example project for rspec --- SampleProjects/TestSomething/Gemfile | 2 + SampleProjects/TestSomething/Gemfile.lock | 19 ++++++++ SampleProjects/TestSomething/README.md | 3 ++ .../TestSomethingExample.ino | 9 ++++ .../TestSomething/library.properties | 10 ++++ .../TestSomething/test-something.cpp | 5 ++ SampleProjects/TestSomething/test-something.h | 3 ++ .../test/bad-null.cpp | 0 .../TestSomething/test/good-library.cpp | 11 +++++ .../TestSomething/test/good-null.cpp | 24 ++++++++++ lib/arduino_ci/cpp_library.rb | 14 +----- spec/cpp_library_spec.rb | 48 ++++++++++++------- 12 files changed, 120 insertions(+), 28 deletions(-) create mode 100644 SampleProjects/TestSomething/Gemfile create mode 100644 SampleProjects/TestSomething/Gemfile.lock create mode 100644 SampleProjects/TestSomething/README.md create mode 100644 SampleProjects/TestSomething/examples/TestSomethingExample/TestSomethingExample.ino create mode 100644 SampleProjects/TestSomething/library.properties create mode 100644 SampleProjects/TestSomething/test-something.cpp create mode 100644 SampleProjects/TestSomething/test-something.h rename SampleProjects/{DoSomething => TestSomething}/test/bad-null.cpp (100%) create mode 100644 SampleProjects/TestSomething/test/good-library.cpp create mode 100644 SampleProjects/TestSomething/test/good-null.cpp 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..7815c646 --- /dev/null +++ b/SampleProjects/TestSomething/library.properties @@ -0,0 +1,10 @@ +name=arduino-ci-unit-tests +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/DoSomething +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/DoSomething/test/bad-null.cpp b/SampleProjects/TestSomething/test/bad-null.cpp similarity index 100% rename from SampleProjects/DoSomething/test/bad-null.cpp rename to SampleProjects/TestSomething/test/bad-null.cpp 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/lib/arduino_ci/cpp_library.rb b/lib/arduino_ci/cpp_library.rb index fbd9c771..c915ac83 100644 --- a/lib/arduino_ci/cpp_library.rb +++ b/lib/arduino_ci/cpp_library.rb @@ -123,13 +123,7 @@ def test_args(aux_libraries, ci_gcc_config) ret end - # run a test of the given unit test file - def test_with_configuration(test_file, aux_libraries, ci_gcc_config) - executable = build_for_test_with_configuration(test_file, aux_libraries, ci_gcc_config) - run_test_file(executable) - end - - # run a test of the given unit test file + # build a file for running a test of the given unit test file 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") @@ -140,15 +134,11 @@ def build_for_test_with_configuration(test_file, aux_libraries, ci_gcc_config) executable end + # run a test file def run_test_file(executable) Host.run(executable) end - # legacy shortcut for rspec - def test(test_file) - test_with_configuration(test_file, [], nil) - end - end end diff --git a/spec/cpp_library_spec.rb b/spec/cpp_library_spec.rb index f7a5b734..176cd0a7 100644 --- a/spec/cpp_library_spec.rb +++ b/spec/cpp_library_spec.rb @@ -3,51 +3,67 @@ 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 "tests_dir" do it "locate the tests directory" do - dosomething_header_dirs = ["DoSomething"] + testsomething_header_dirs = ["TestSomething"] relative_path = cpp_library.tests_dir.split("SampleProjects/", 2)[1] - expect(relative_path).to eq("DoSomething/test") + expect(relative_path).to eq("TestSomething/test") end end context "test_files" do it "finds cpp files in directory" do - dosomething_test_files = [ - "DoSomething/test/good-null.cpp", - "DoSomething/test/good-library.cpp", - "DoSomething/test/bad-null.cpp" + 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(dosomething_test_files) + expect(relative_paths).to match_array(testsomething_test_files) end end context "test" do - arduino_cmd = ArduinoCI::ArduinoInstallation.autolocate! - it "tests libraries" 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 - test_files.each do |path| - expect(cpp_library.test(path)).to eq(path.include?("good")) + 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 From e0135ffcfdfb94f23f921ea915da7d35df29a896 Mon Sep 17 00:00:00 2001 From: Ian Katz Date: Wed, 24 Jan 2018 14:05:10 -0500 Subject: [PATCH 32/39] pragma once --- SampleProjects/DoSomething/do-something.h | 1 + 1 file changed, 1 insertion(+) 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); From bd97bcfb364eee66fb4afc0d81b9a1e91af299e9 Mon Sep 17 00:00:00 2001 From: Ian Katz Date: Wed, 24 Jan 2018 14:05:34 -0500 Subject: [PATCH 33/39] fix library.properties --- SampleProjects/DoSomething/library.properties | 4 ++-- SampleProjects/TestSomething/library.properties | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) 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/TestSomething/library.properties b/SampleProjects/TestSomething/library.properties index 7815c646..be60a51f 100644 --- a/SampleProjects/TestSomething/library.properties +++ b/SampleProjects/TestSomething/library.properties @@ -1,10 +1,10 @@ -name=arduino-ci-unit-tests +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/DoSomething +url=https://github.com/ifreecarve/arduino_ci/SampleProjects/TestSomething architectures=avr,esp8266 includes=do-something.h From 62934bfbae144d1e8732eb6c79c8ba4f6631a925 Mon Sep 17 00:00:00 2001 From: Ian Katz Date: Wed, 24 Jan 2018 14:22:52 -0500 Subject: [PATCH 34/39] fixes for test runner script --- exe/arduino_ci_remote.rb | 1 + lib/arduino_ci/cpp_library.rb | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/exe/arduino_ci_remote.rb b/exe/arduino_ci_remote.rb index 83e340e0..87c2a687 100755 --- a/exe/arduino_ci_remote.rb +++ b/exe/arduino_ci_remote.rb @@ -54,6 +54,7 @@ def assure(message, &block) 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 diff --git a/lib/arduino_ci/cpp_library.rb b/lib/arduino_ci/cpp_library.rb index c915ac83..472655e5 100644 --- a/lib/arduino_ci/cpp_library.rb +++ b/lib/arduino_ci/cpp_library.rb @@ -19,7 +19,7 @@ class CppLibrary attr_reader :last_cmd def initialize(base_dir) - @base_dir = base_dir + @base_dir = File.expand_path(base_dir) @artifacts = [] @last_err = "" @last_out = "" @@ -115,9 +115,9 @@ def flag_args(ci_gcc_config) # All GCC command line args for building any unit test def test_args(aux_libraries, ci_gcc_config) # TODO: something with libraries? - cgc = ci_gcc_config 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 From 04fa5743dcb73752239564579a1a8ea98984d91d Mon Sep 17 00:00:00 2001 From: Ian Katz Date: Wed, 24 Jan 2018 14:32:59 -0500 Subject: [PATCH 35/39] scale back default architectures --- misc/default.yaml | 2 -- spec/ci_config_spec.rb | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/misc/default.yaml b/misc/default.yaml index b92b0014..4eb439aa 100644 --- a/misc/default.yaml +++ b/misc/default.yaml @@ -67,7 +67,6 @@ compile: platforms: - uno - due - - esp8266 - leonardo unittest: @@ -75,5 +74,4 @@ unittest: platforms: - uno - due - - esp8266 - leonardo diff --git a/spec/ci_config_spec.rb b/spec/ci_config_spec.rb index 07e9cb10..929fa166 100644 --- a/spec/ci_config_spec.rb +++ b/spec/ci_config_spec.rb @@ -24,8 +24,8 @@ 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", "esp8266", "leonardo"]) - expect(default_config.platforms_to_unittest).to match(["uno", "due", "esp8266", "leonardo"]) + 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 From a57414664814cffe95e76f58a899b8f15ed8a443 Mon Sep 17 00:00:00 2001 From: Ian Katz Date: Wed, 24 Jan 2018 14:35:19 -0500 Subject: [PATCH 36/39] avoid problems related to finding a symbolic link --- lib/arduino_ci/cpp_library.rb | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/arduino_ci/cpp_library.rb b/lib/arduino_ci/cpp_library.rb index 472655e5..e81cb5ee 100644 --- a/lib/arduino_ci/cpp_library.rb +++ b/lib/arduino_ci/cpp_library.rb @@ -27,12 +27,17 @@ def initialize(base_dir) end def cpp_files_in(some_dir) - Find.find(some_dir).select { |path| CPP_EXTENSIONS.include?(File.extname(path)) } + 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 def cpp_files - cpp_files_in(@base_dir).reject { |p| p.start_with?(tests_dir + File::SEPARATOR) } + 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 From d828085be71a4b6d6d244ec47f3cb3371119d6f0 Mon Sep 17 00:00:00 2001 From: Ian Katz Date: Wed, 24 Jan 2018 14:52:00 -0500 Subject: [PATCH 37/39] tweaks to CI --- exe/arduino_ci_remote.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/exe/arduino_ci_remote.rb b/exe/arduino_ci_remote.rb index 87c2a687..cdeef346 100755 --- a/exe/arduino_ci_remote.rb +++ b/exe/arduino_ci_remote.rb @@ -9,7 +9,7 @@ # terminate after printing any debug info. TODO: capture debug info def terminate(final = nil) puts "Failures: #{@failure_count}" - unless @failure_count.zero? && final.nil? + unless @failure_count.zero? || final puts "Last message: #{@arduino_cmd.last_msg}" puts "========== Stdout:" puts @arduino_cmd.last_out @@ -117,8 +117,8 @@ def assure(message, &block) config.aux_libraries_for_unittest, config.gcc_config(p) ) + puts unless exe - puts puts "Last command: #{cpp_library.last_cmd}" puts cpp_library.last_out puts cpp_library.last_err From ae63752c9f63c2019f1c8ed6fbf0d476db4cd009 Mon Sep 17 00:00:00 2001 From: Ian Katz Date: Wed, 24 Jan 2018 19:28:33 -0500 Subject: [PATCH 38/39] add comments for yard docs --- exe/ci_system_check.rb | 64 --------------------- lib/arduino_ci/arduino_cmd.rb | 41 ++++++++++++- lib/arduino_ci/arduino_cmd_linux.rb | 8 ++- lib/arduino_ci/arduino_cmd_linux_builder.rb | 4 ++ lib/arduino_ci/arduino_installation.rb | 7 +++ lib/arduino_ci/ci_config.rb | 35 ++++++++++- lib/arduino_ci/cpp_library.rb | 37 ++++++++++++ lib/arduino_ci/display_manager.rb | 15 +++++ lib/arduino_ci/host.rb | 3 + 9 files changed, 145 insertions(+), 69 deletions(-) delete mode 100755 exe/ci_system_check.rb diff --git a/exe/ci_system_check.rb b/exe/ci_system_check.rb deleted file mode 100755 index cf1f8652..00000000 --- a/exe/ci_system_check.rb +++ /dev/null @@ -1,64 +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 Adafruit FONA library" -got_problem = true unless arduino_cmd.install_library("Adafruit FONA Library") -puts "checking that library is indexed" -got_problem = true unless arduino_cmd.library_is_indexed -puts "Installing Adafruit FONA library a second time to see what happens" -got_problem = true unless arduino_cmd.install_library("Adafruit FONA Library") - -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 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/arduino_cmd.rb b/lib/arduino_ci/arduino_cmd.rb index 1f1ec60c..bc896d93 100644 --- a/lib/arduino_ci/arduino_cmd.rb +++ b/lib/arduino_ci/arduino_cmd.rb @@ -9,19 +9,28 @@ 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 + # 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). @@ -43,6 +52,9 @@ def initialize @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| @@ -53,17 +65,21 @@ 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 unless @prefs_fetched return nil if prefs_raw.nil? @@ -72,12 +88,16 @@ def prefs 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_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] @@ -141,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 @@ -176,11 +198,14 @@ def install_library(library_name) 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 @@ -188,11 +213,15 @@ 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(":") @@ -201,6 +230,8 @@ def use_board!(boardname) 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? @@ -217,6 +248,8 @@ def verify_sketch(path) # 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) realpath = File.expand_path(path) library_name = File.basename(realpath) @@ -244,6 +277,8 @@ def install_local_library(path) destination_path end + # @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)) 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 6ce0a49e..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 @@ -51,6 +54,7 @@ def autolocate_osx ret end + # @return [ArduinoCI::ArduinoCmdLinux] an instance of a command def autolocate_linux if USE_BUILDER builder_name = "arduino-builder" @@ -87,6 +91,7 @@ def autolocate_linux 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? @@ -96,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 index 9777d296..0f752c44 100644 --- a/lib/arduino_ci/ci_config.rb +++ b/lib/arduino_ci/ci_config.rb @@ -30,6 +30,8 @@ }.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 @@ -41,6 +43,7 @@ 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__)) @@ -59,12 +62,19 @@ def initialize @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 = {} @@ -84,6 +94,9 @@ def validate_data(rootname, source, schema) 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") @@ -113,6 +126,9 @@ def load_yaml(path) 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) @@ -124,23 +140,31 @@ def with_override(path) 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 - # assume the script runs from the working directory of the base project + # 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? @@ -149,31 +173,40 @@ def platform_definition(platform_name) # 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? diff --git a/lib/arduino_ci/cpp_library.rb b/lib/arduino_ci/cpp_library.rb index e81cb5ee..8f634583 100644 --- a/lib/arduino_ci/cpp_library.rb +++ b/lib/arduino_ci/cpp_library.rb @@ -11,13 +11,22 @@ 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 = File.expand_path(base_dir) @artifacts = [] @@ -26,12 +35,16 @@ def initialize(base_dir) @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 real_tests_dir = File.realpath(tests_dir) cpp_files_in(@base_dir).reject do |p| @@ -41,26 +54,31 @@ def cpp_files 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 @@ -88,36 +106,49 @@ def run_gcc(*args, **kwargs) 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 @@ -129,6 +160,10 @@ def test_args(aux_libraries, ci_gcc_config) 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") @@ -140,6 +175,8 @@ def build_for_test_with_configuration(test_file, aux_libraries, ci_gcc_config) end # 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 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 b9b6c1db..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| @@ -23,6 +25,7 @@ def self.run(*args, **kwargs) 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? From 7289056c75c47cf02b55f396515bfb3006c544dd Mon Sep 17 00:00:00 2001 From: Ian Katz Date: Wed, 24 Jan 2018 20:24:27 -0500 Subject: [PATCH 39/39] update READMEs --- CHANGELOG.md | 2 + README.md | 30 +++++--- SampleProjects/DoSomething/README.md | 100 +++++++++++++++++++++++++-- SampleProjects/README.md | 2 +- 4 files changed, 119 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7bb55897..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 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/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/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).