diff --git a/.gitignore b/.gitignore index dec931d0..4bb3c08b 100644 --- a/.gitignore +++ b/.gitignore @@ -16,7 +16,8 @@ Release/ *.vsarduino.h __vm/ *.user -*.vcxproj -*.vcxproj.filters +Grbl_Esp32.vcxproj +Grbl_Esp32.vcxproj.filters *.suo Grbl_Esp32.ino.cpp +packages/ diff --git a/Grbl_Esp32.sln b/Grbl_Esp32.sln index 0ae0bade..ed5750dd 100644 --- a/Grbl_Esp32.sln +++ b/Grbl_Esp32.sln @@ -5,6 +5,8 @@ VisualStudioVersion = 16.0.29306.81 MinimumVisualStudioVersion = 10.0.40219.1 Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Grbl_Esp32", "Grbl_Esp32.vcxproj", "{11C8A44F-A303-4885-B5AD-5B65F7FE41C0}" EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "UnitTests", "UnitTests.vcxproj", "{33ECE513-60D1-4949-A4A9-C95D353C2CF0}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|x64 = Debug|x64 @@ -21,6 +23,14 @@ Global {11C8A44F-A303-4885-B5AD-5B65F7FE41C0}.Release|x64.Build.0 = Release|x64 {11C8A44F-A303-4885-B5AD-5B65F7FE41C0}.Release|x86.ActiveCfg = Release|Win32 {11C8A44F-A303-4885-B5AD-5B65F7FE41C0}.Release|x86.Build.0 = Release|Win32 + {33ECE513-60D1-4949-A4A9-C95D353C2CF0}.Debug|x64.ActiveCfg = Debug|x64 + {33ECE513-60D1-4949-A4A9-C95D353C2CF0}.Debug|x64.Build.0 = Debug|x64 + {33ECE513-60D1-4949-A4A9-C95D353C2CF0}.Debug|x86.ActiveCfg = Debug|Win32 + {33ECE513-60D1-4949-A4A9-C95D353C2CF0}.Debug|x86.Build.0 = Debug|Win32 + {33ECE513-60D1-4949-A4A9-C95D353C2CF0}.Release|x64.ActiveCfg = Release|x64 + {33ECE513-60D1-4949-A4A9-C95D353C2CF0}.Release|x64.Build.0 = Release|x64 + {33ECE513-60D1-4949-A4A9-C95D353C2CF0}.Release|x86.ActiveCfg = Release|Win32 + {33ECE513-60D1-4949-A4A9-C95D353C2CF0}.Release|x86.Build.0 = Release|Win32 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Grbl_Esp32/Grbl_Esp32.ino b/Grbl_Esp32/Grbl_Esp32.ino index 9c907733..3a4d2ff6 100644 --- a/Grbl_Esp32/Grbl_Esp32.ino +++ b/Grbl_Esp32/Grbl_Esp32.ino @@ -18,7 +18,8 @@ along with Grbl. If not, see . */ -#include "src/Grbl.h" +#ifndef UNIT_TEST +# include "src/Grbl.h" void setup() { grbl_init(); @@ -27,3 +28,5 @@ void setup() { void loop() { run_once(); } + +#endif diff --git a/Grbl_Esp32/src/Assert.h b/Grbl_Esp32/src/Assert.h index 468f652d..5cbf55da 100644 --- a/Grbl_Esp32/src/Assert.h +++ b/Grbl_Esp32/src/Assert.h @@ -1,5 +1,9 @@ #pragma once +#include "StackTrace/AssertionFailed.h" + +class AssertionFailed; + #undef Assert #define Stringify(x) #x @@ -7,7 +11,7 @@ #define Assert(condition, ...) \ { \ if (!(condition)) { \ - const char* ch = #condition " (@line " Stringify2(__LINE__) ")"; \ - throw ch; \ + const char* ch = #condition " (@line " Stringify2(__LINE__) ")"; \ + throw AssertionFailed::create(ch, ##__VA_ARGS__); \ } \ } diff --git a/Grbl_Esp32/src/Grbl.cpp b/Grbl_Esp32/src/Grbl.cpp index 7a29bc4f..49372bcb 100644 --- a/Grbl_Esp32/src/Grbl.cpp +++ b/Grbl_Esp32/src/Grbl.cpp @@ -110,11 +110,16 @@ static void reset_variables() { } void run_once() { - reset_variables(); - // Start Grbl main loop. Processes program inputs and executes them. - // This can exit on a system abort condition, in which case run_once() - // is re-executed by an enclosing loop. - protocol_main_loop(); + try { + reset_variables(); + // Start Grbl main loop. Processes program inputs and executes them. + // This can exit on a system abort condition, in which case run_once() + // is re-executed by an enclosing loop. + protocol_main_loop(); + } catch (AssertionFailed ex) { + // This means something is terribly broken: + grbl_sendf(CLIENT_ALL, "Critical error: %s", ex.stackTrace.c_str()); + } } /* diff --git a/Grbl_Esp32/src/Pins/PinAttributes.cpp b/Grbl_Esp32/src/Pins/PinAttributes.cpp index ee56d1df..f67e6295 100644 --- a/Grbl_Esp32/src/Pins/PinAttributes.cpp +++ b/Grbl_Esp32/src/Pins/PinAttributes.cpp @@ -35,7 +35,7 @@ namespace Pins { } // If it's exclusive, we are not allowed to set it again: - if (_value != Undefined && this->has(Exclusive) && _value != t._value) { + if (_value != Undefined._value && this->has(Exclusive) && _value != t._value) { return true; } diff --git a/Grbl_Esp32/src/StackTrace/AssertionFailed.cpp b/Grbl_Esp32/src/StackTrace/AssertionFailed.cpp new file mode 100644 index 00000000..a4000acf --- /dev/null +++ b/Grbl_Esp32/src/StackTrace/AssertionFailed.cpp @@ -0,0 +1,83 @@ +#include "AssertionFailed.h" + +#include +#include + +#ifdef ESP32 +# ifdef UNIT_TEST + +# include "debug_helpers.h" +# include "WString.h" +# include "stdio.h" + +AssertionFailed AssertionFailed::create(const char* condition, const char* msg, ...) { + String st = condition; + st += ": "; + + char tmp[255]; + va_list arg; + va_start(arg, msg); + size_t len = vsnprintf(tmp, 255, msg, arg); + tmp[254] = 0; + st += tmp; + + st += " at: "; + st += esp_backtrace_print(10); + + return AssertionFailed(st); +} + +# else + +# include "stdio.h" + +AssertionFailed AssertionFailed::create(const char* condition, const char* msg, ...) { + String st = "\r\nError "; + st += condition; + st += " failed: "; + + char tmp[255]; + va_list arg; + va_start(arg, msg); + size_t len = vsnprintf(tmp, 255, msg, arg); + tmp[254] = 0; + st += tmp; + + return AssertionFailed(st); +} + +# endif + +#else + +# include +# include +# include +# include "WString.h" + +extern void DumpStackTrace(std::ostringstream& builder); + +String stackTrace; + +std::exception AssertionFailed::create(const char* condition, const char* msg, ...) { + std::ostringstream oss; + oss << std::endl; + oss << "Error: " << std::endl; + + char tmp[255]; + va_list arg; + va_start(arg, msg); + size_t len = vsnprintf(tmp, 255, msg, arg); + tmp[254] = 0; + oss << tmp; + + oss << " at "; + DumpStackTrace(oss); + + // Store in a static temp: + static std::string info; + info = oss.str(); + throw std::exception(info.c_str()); +} + +#endif diff --git a/Grbl_Esp32/src/StackTrace/AssertionFailed.h b/Grbl_Esp32/src/StackTrace/AssertionFailed.h new file mode 100644 index 00000000..d8766690 --- /dev/null +++ b/Grbl_Esp32/src/StackTrace/AssertionFailed.h @@ -0,0 +1,32 @@ +#pragma once + +#include "WString.h" + +#ifdef ESP32 +class AssertionFailed { +public: + String stackTrace; + + AssertionFailed(String st) : stackTrace(st) {} + + static AssertionFailed create(const char* condition) { + return create(condition, "Assertion failed"); + } + static AssertionFailed create(const char* condition, const char* msg, ...); + +}; + +#else +# include + +class AssertionFailed { +public: + String stackTrace; + + static std::exception create(const char* condition) { + return create(condition, "Assertion failed"); + } + static std::exception create(const char* condition, const char* msg, ...); +}; + +#endif diff --git a/Grbl_Esp32/src/StackTrace/debug_helpers.cpp b/Grbl_Esp32/src/StackTrace/debug_helpers.cpp new file mode 100644 index 00000000..d19d46c4 --- /dev/null +++ b/Grbl_Esp32/src/StackTrace/debug_helpers.cpp @@ -0,0 +1,92 @@ +#ifdef ESP32 +# ifdef UNIT_TEST + +// Copyright 2015-2019 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +# include +# include + +# include "esp_types.h" +# include "esp_attr.h" +# include "esp_err.h" +# include "debug_helpers.h" +// #include "esp32/rom/ets_sys.h" +# include "soc/soc_memory_layout.h" +# include "soc/cpu.h" + +static inline bool esp_stack_ptr_is_sane(uint32_t sp) { + return !(sp < 0x3ffae010UL || sp > 0x3ffffff0UL || ((sp & 0xf) != 0)); +} + +static inline uint32_t esp_cpu_process_stack_pc(uint32_t pc) { + if (pc & 0x80000000) { + //Top two bits of a0 (return address) specify window increment. Overwrite to map to address space. + pc = (pc & 0x3fffffff) | 0x40000000; + } + //Minus 3 to get PC of previous instruction (i.e. instruction executed before return address) + return pc - 3; +} + +bool IRAM_ATTR esp_backtrace_get_next_frame(esp_backtrace_frame_t* frame) { + //Use frame(i-1)'s BS area located below frame(i)'s sp to get frame(i-1)'s sp and frame(i-2)'s pc + void* base_save = (void*)frame->sp; //Base save area consists of 4 words under SP + frame->pc = frame->next_pc; + frame->next_pc = *((uint32_t*)(((char*)base_save) - 16)); //If next_pc = 0, indicates frame(i-1) is the last frame on the stack + frame->sp = *((uint32_t*)(((char*)base_save) - 12)); + + //Return true if both sp and pc of frame(i-1) are sane, false otherwise + return (esp_stack_ptr_is_sane(frame->sp) && esp_ptr_executable((void*)esp_cpu_process_stack_pc(frame->pc))); +} + +String IRAM_ATTR esp_backtrace_print(int depth) { + char buf[80]; + + //Check arguments + if (depth <= 0) { + return ""; + } + + //Initialize stk_frame with first frame of stack + esp_backtrace_frame_t stk_frame; + esp_backtrace_get_start(&(stk_frame.pc), &(stk_frame.sp), &(stk_frame.next_pc)); + //esp_cpu_get_backtrace_start(&stk_frame); + String s = "backtrace:"; + snprintf(buf, 80, "0x%08X:0x%08X ", esp_cpu_process_stack_pc(stk_frame.pc), stk_frame.sp); + s += buf; + + //Check if first frame is valid + bool corrupted = (esp_stack_ptr_is_sane(stk_frame.sp) && esp_ptr_executable((void*)esp_cpu_process_stack_pc(stk_frame.pc))) ? false : + true; + + uint32_t i = (depth <= 0) ? INT32_MAX : depth; + while (i-- > 0 && stk_frame.next_pc != 0 && !corrupted) { + if (!esp_backtrace_get_next_frame(&stk_frame)) { //Get previous stack frame + corrupted = true; + } + snprintf(buf, 80, "0x%08X:0x%08X ", esp_cpu_process_stack_pc(stk_frame.pc), stk_frame.sp); + s += buf; + } + + //Print backtrace termination marker + if (corrupted) { + s += " |<-CORRUPTED"; + } else if (stk_frame.next_pc != 0) { //Backtrace continues + s += " |<-CONTINUES"; + } + return s; +} + +# endif +#endif diff --git a/Grbl_Esp32/src/StackTrace/debug_helpers.h b/Grbl_Esp32/src/StackTrace/debug_helpers.h new file mode 100644 index 00000000..a22286d4 --- /dev/null +++ b/Grbl_Esp32/src/StackTrace/debug_helpers.h @@ -0,0 +1,106 @@ +#ifdef ESP32 +# ifdef UNIT_TEST +// Copyright 2015-2019 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +# pragma once + +# ifdef __cplusplus +extern "C" { +# endif + +# ifndef __ASSEMBLER__ + +# include +# include "esp_err.h" +# include "soc/soc.h" + +# define ESP_WATCHPOINT_LOAD 0x40000000 +# define ESP_WATCHPOINT_STORE 0x80000000 +# define ESP_WATCHPOINT_ACCESS 0xC0000000 + +/* + * @brief Structure used for backtracing + * + * This structure stores the backtrace information of a particular stack frame + * (i.e. the PC and SP). This structure is used iteratively with the + * esp_cpu_get_next_backtrace_frame() function to traverse each frame within a + * single stack. The next_pc represents the PC of the current frame's caller, thus + * a next_pc of 0 indicates that the current frame is the last frame on the stack. + * + * @note Call esp_backtrace_get_start() to obtain initialization values for + * this structure + */ +typedef struct { + uint32_t pc; /* PC of the current frame */ + uint32_t sp; /* SP of the current frame */ + uint32_t next_pc; /* PC of the current frame's caller */ +} esp_backtrace_frame_t; + +/** + * Get the first frame of the current stack's backtrace + * + * Given the following function call flow (B -> A -> X -> esp_backtrace_get_start), + * this function will do the following. + * - Flush CPU registers and window frames onto the current stack + * - Return PC and SP of function A (i.e. start of the stack's backtrace) + * - Return PC of function B (i.e. next_pc) + * + * @note This function is implemented in assembly + * + * @param[out] pc PC of the first frame in the backtrace + * @param[out] sp SP of the first frame in the backtrace + * @param[out] next_pc PC of the first frame's caller + */ +extern void esp_backtrace_get_start(uint32_t* pc, uint32_t* sp, uint32_t* next_pc); + +/** + * Get the next frame on a stack for backtracing + * + * Given a stack frame(i), this function will obtain the next stack frame(i-1) + * on the same call stack (i.e. the caller of frame(i)). This function is meant to be + * called iteratively when doing a backtrace. + * + * Entry Conditions: Frame structure containing valid SP and next_pc + * Exit Conditions: + * - Frame structure updated with SP and PC of frame(i-1). next_pc now points to frame(i-2). + * - If a next_pc of 0 is returned, it indicates that frame(i-1) is last frame on the stack + * + * @param[inout] frame Pointer to frame structure + * + * @return + * - True if the SP and PC of the next frame(i-1) are sane + * - False otherwise + */ +bool esp_backtrace_get_next_frame(esp_backtrace_frame_t* frame); + +/** + * @brief Print the backtrace of the current stack + * + * @param depth The maximum number of stack frames to print (should be > 0) + * + * @return + * - ESP_OK Backtrace successfully printed to completion or to depth limit + * - ESP_FAIL Backtrace is corrupted + */ + +String esp_backtrace_print(int depth); + +# endif +# ifdef __cplusplus +} +# endif + +# endif +#endif diff --git a/Grbl_Esp32/src/StackTrace/debug_helpers_asm.S b/Grbl_Esp32/src/StackTrace/debug_helpers_asm.S new file mode 100644 index 00000000..1232a6de --- /dev/null +++ b/Grbl_Esp32/src/StackTrace/debug_helpers_asm.S @@ -0,0 +1,63 @@ +#ifdef ESP32 +#ifdef UNIT_TEST + +// Copyright 2015-2019 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include +#include + +/* + * esp_backtrace_get_start(uint32_t *pc, uint32_t *sp, uint32_t *next_pc) + * + * High Addr + * .................. + * | i-3 BS | + * | i-1 locals | Function B + * .................. i-1 SP + * | i-2 BS | + * | i locals | Function A (Start of backtrace) + * ------------------ i SP + * | i-1 BS | + * | i+1 locals | Backtracing function (e.g. esp_backtrace_print()) + * ------------------ i+1 SP + * | i BS | + * | i+2 locals | esp_backtrace_get_start() <- This function + * ------------------ i+2 SP + * | i+1 BS | + * | i+3 locals | xthal_window_spill() + * ------------------ i+3 SP + * .................. Low Addr + */ + .section .iram1, "ax" + .align 4 + .global esp_backtrace_get_start + .type esp_backtrace_get_start, @function +esp_backtrace_get_start: + entry a1, 32 + call8 xthal_window_spill //Spill registers onto stack (excluding this function) + //a2, a3, a4 should be out arguments for i SP, i PC, i-1 PC respectively. Use a5 and a6 as scratch + l32e a5, sp, -16 //Get i PC, which is ret addres of i+1 + s32i a5, a2, 0 //Store i PC to arg *pc + l32e a6, sp, -12 //Get i+1 SP. Used to access i BS + l32e a5, a6, -12 //Get i SP + s32i a5, a3, 0 //Store i SP to arg *sp + l32e a5, a6, -16 //Get i-1 PC, which is ret address of i + s32i a5, a4, 0 //Store i-1 PC to arg *next_pc + retw + +#endif +#endif diff --git a/Grbl_Esp32/test/Pins/BasicGPIO.cpp b/Grbl_Esp32/test/Pins/BasicGPIO.cpp new file mode 100644 index 00000000..a660368c --- /dev/null +++ b/Grbl_Esp32/test/Pins/BasicGPIO.cpp @@ -0,0 +1,41 @@ +#include "../TestFramework.h" + +#include + +#ifdef ESP32 + +extern "C" int __digitalRead(uint8_t pin); +extern "C" void __pinMode(uint8_t pin, uint8_t mode); +extern "C" void __digitalWrite(uint8_t pin, uint8_t val); + +namespace Pins { + Test(BasicGPIO, ReadGPIORaw) { + auto pin = 26; + + // Enable driver, write high/low. + __pinMode(pin, OUTPUT); + + __digitalWrite(pin, HIGH); + auto value = __digitalRead(pin); + Assert(value != 0); + + __digitalWrite(pin, LOW); + value = __digitalRead(pin); + Assert(value == 0); + + __digitalWrite(pin, HIGH); + value = __digitalRead(pin); + Assert(value != 0); + + __digitalWrite(pin, LOW); + value = __digitalRead(pin); + Assert(value == 0); + + // Disable driver, should read the last value (low). + __pinMode(pin, INPUT); + value = __digitalRead(pin); + Assert(value == 0); + } +} + +#endif diff --git a/Grbl_Esp32/test/Pins/Error.cpp b/Grbl_Esp32/test/Pins/Error.cpp new file mode 100644 index 00000000..5435dc1a --- /dev/null +++ b/Grbl_Esp32/test/Pins/Error.cpp @@ -0,0 +1,26 @@ +#include "../TestFramework.h" + +#include + +namespace Pins { + Test(Error, Pins) { + // Error pins should throw whenever they are used. + + Pin errorPin = Pin::ERROR; + + AssertThrow(errorPin.write(true)); + AssertThrow(errorPin.read()); + + errorPin.setAttr(Pin::Attr::None); + + AssertThrow(errorPin.write(true)); + AssertThrow(errorPin.read()); + + AssertThrow(errorPin.attachInterrupt([](void* arg) {}, CHANGE)); + AssertThrow(errorPin.detachInterrupt()); + + Assert(errorPin.capabilities() == Pin::Capabilities::None, "Incorrect caps"); + + Assert(errorPin.name() == "ERROR_PIN"); + } +} diff --git a/Grbl_Esp32/test/Pins/GPIO.cpp b/Grbl_Esp32/test/Pins/GPIO.cpp new file mode 100644 index 00000000..15f79355 --- /dev/null +++ b/Grbl_Esp32/test/Pins/GPIO.cpp @@ -0,0 +1,250 @@ +#include "../TestFramework.h" + +#include + +#ifdef ESP32 +extern "C" void __pinMode(uint8_t pin, uint8_t mode); +extern "C" int __digitalRead(uint8_t pin); +extern "C" void __digitalWrite(uint8_t pin, uint8_t val); + +struct GPIONative { + inline static void initialize() { + for (int i = 16; i <= 17; ++i) { + __pinMode(i, OUTPUT); + __digitalWrite(i, LOW); + } + } + inline static void mode(int pin, uint8_t mode) { __pinMode(pin, mode); } + inline static void write(int pin, bool val) { __digitalWrite(pin, val ? HIGH : LOW); } + inline static bool read(int pin) { return __digitalRead(pin) != LOW; } +}; +#else +# include + +struct GPIONative { + // We test GPIO pin 16 and 17, and GPIO 16 is wired directly to 17: + static void WriteVirtualCircuitHystesis(SoftwarePin* pins, int pin, bool value) { + switch (pin) { + case 16: + case 17: + pins[16].handlePadChange(value); + pins[17].handlePadChange(value); + break; + } + } + + inline static void initialize() { SoftwareGPIO::instance().reset(WriteVirtualCircuitHystesis, false); } + inline static void mode(int pin, uint8_t mode) { SoftwareGPIO::instance().setMode(pin, mode); } + inline static void write(int pin, bool val) { SoftwareGPIO::instance().writeOutput(pin, val); } + inline static bool read(int pin) { return SoftwareGPIO::instance().read(pin); } +}; + +void digitalWrite(uint8_t pin, uint8_t val); +void pinMode(uint8_t pin, uint8_t mode); +int digitalRead(uint8_t pin); + +#endif + +namespace Pins { + Test(GPIO, BasicInputOutput1) { + GPIONative::initialize(); + PinLookup::ResetAllPins(); + + Pin gpio16 = Pin::create("gpio.16"); + Pin gpio17 = Pin::create("gpio.17"); + + gpio16.setAttr(Pin::Attr::Output); + gpio17.setAttr(Pin::Attr::Input); + + Assert(false == gpio16.read()); + Assert(false == gpio17.read()); + Assert(false == GPIONative::read(16)); + Assert(false == GPIONative::read(17)); + + gpio16.on(); + + Assert(true == gpio16.read()); + Assert(true == gpio17.read()); + Assert(true == GPIONative::read(16)); + Assert(true == GPIONative::read(17)); + + gpio16.off(); + + Assert(false == gpio16.read()); + Assert(false == gpio17.read()); + Assert(false == GPIONative::read(16)); + Assert(false == GPIONative::read(17)); + } + + Test(GPIO, BasicInputOutput2) { + GPIONative::initialize(); + PinLookup::ResetAllPins(); + + Pin gpio16 = Pin::create("gpio.16"); + Pin gpio17 = Pin::create("gpio.17"); + + gpio16.setAttr(Pin::Attr::Input); + gpio17.setAttr(Pin::Attr::Output); + + Assert(false == gpio16.read()); + Assert(false == gpio17.read()); + Assert(false == GPIONative::read(16)); + Assert(false == GPIONative::read(17)); + + gpio17.on(); + + Assert(true == gpio16.read()); + Assert(true == gpio17.read()); + Assert(true == GPIONative::read(16)); + Assert(true == GPIONative::read(17)); + + gpio17.off(); + + Assert(false == gpio16.read()); + Assert(false == gpio17.read()); + Assert(false == GPIONative::read(16)); + Assert(false == GPIONative::read(17)); + } + + void TestISR(int deltaRising, int deltaFalling, int mode) { + GPIONative::initialize(); + PinLookup::ResetAllPins(); + + Pin gpio16 = Pin::create("gpio.16"); + Pin gpio17 = Pin::create("gpio.17"); + + gpio16.setAttr(Pin::Attr::Input | Pin::Attr::ISR); + gpio17.setAttr(Pin::Attr::Output); + + int hitCount = 0; + int expected = 0; + gpio16.attachInterrupt( + [](void* arg) { + int* hc = static_cast(arg); + ++(*hc); + }, + mode, + &hitCount); + + // Two ways to set I/O: + // 1. using on/off + // 2. external source (e.g. set softwareio pin value) + // + // We read as well, because that shouldn't modify the state. + // + // NOTE: Hysteresis tells us that we get changes a lot during a small + // window in time. Unfortunately, it's practically impossible to test + // because it bounces all over the place... TODO FIXME, some mechanism + // to cope with that. + + for (int i = 0; i < 10; ++i) { + if (deltaRising) { + auto oldCount = hitCount; + gpio17.on(); + delay(1); + auto newCount = hitCount; + + Assert(oldCount < newCount, "Expected rise after set state"); + } else { + gpio17.on(); + } + + if (deltaFalling) { + auto oldCount = hitCount; + gpio17.off(); + delay(1); + auto newCount = hitCount; + + Assert(oldCount < newCount, "Expected rise after set state"); + } else { + gpio17.off(); + } + } + + // Detach interrupt. Regardless of what we do, it shouldn't change hitcount anymore. + gpio16.detachInterrupt(); + + auto oldCount = hitCount; + gpio17.on(); + gpio17.off(); + delay(1); + auto newCount = hitCount; + + Assert(oldCount == newCount, "ISR hitcount error"); + } + + Test(GPIO, ISRRisingPin) { TestISR(1, 0, RISING); } + + Test(GPIO, ISRFallingPin) { TestISR(0, 1, FALLING); } + + Test(GPIO, ISRChangePin) { TestISR(1, 1, CHANGE); } + + Test(GPIO, NativeForwardingInput) { + GPIONative::initialize(); + PinLookup::ResetAllPins(); + + Pin gpio16 = Pin::create("gpio.16"); + Pin gpio17 = Pin::create("gpio.17"); + + pinMode(16, INPUT); + gpio17.setAttr(Pin::Attr::Output); + + Assert(LOW == digitalRead(16)); + Assert(false == gpio17.read()); + Assert(false == GPIONative::read(16)); + Assert(false == GPIONative::read(17)); + + gpio17.on(); + + Assert(HIGH == digitalRead(16)); + Assert(true == gpio17.read()); + Assert(true == GPIONative::read(16)); + Assert(true == GPIONative::read(17)); + + gpio17.off(); + + Assert(LOW == digitalRead(16)); + Assert(false == gpio17.read()); + Assert(false == GPIONative::read(16)); + Assert(false == GPIONative::read(17)); + } + + Test(GPIO, NativeForwardingOutput) { + GPIONative::initialize(); + PinLookup::ResetAllPins(); + + Pin gpio16 = Pin::create("gpio.16"); + Pin gpio17 = Pin::create("gpio.17"); + + pinMode(16, OUTPUT); + gpio17.setAttr(Pin::Attr::Input); + + digitalWrite(16, LOW); + Assert(LOW == digitalRead(16)); + Assert(false == gpio17.read()); + Assert(false == GPIONative::read(16)); + Assert(false == GPIONative::read(17)); + + digitalWrite(16, HIGH); + + Assert(HIGH == digitalRead(16)); + Assert(true == gpio17.read()); + Assert(true == GPIONative::read(16)); + Assert(true == GPIONative::read(17)); + + digitalWrite(16, LOW); + + Assert(LOW == digitalRead(16)); + Assert(false == gpio17.read()); + Assert(false == GPIONative::read(16)); + Assert(false == GPIONative::read(17)); + } + + Test(GPIO, Name) { + GPIONative::initialize(); + PinLookup::ResetAllPins(); + + Pin gpio16 = Pin::create("gpio.16"); + Assert(gpio16.name().equals("GPIO.16"), "Name is %s", gpio16.name().c_str()); + } +} diff --git a/Grbl_Esp32/test/Pins/Undefined.cpp b/Grbl_Esp32/test/Pins/Undefined.cpp new file mode 100644 index 00000000..41fc6e33 --- /dev/null +++ b/Grbl_Esp32/test/Pins/Undefined.cpp @@ -0,0 +1,30 @@ +#include "../TestFramework.h" + +#include + +namespace Pins { + Test(Undefined, Pins) { + // Unassigned pins are not doing much... + + Pin unassigned = Pin::UNDEFINED; + Assert(Pin::UNDEFINED == unassigned, "Undefined has wrong pin id"); + + { + unassigned.write(true); + auto result = unassigned.read(); + Assert(0 == result, "Result value incorrect"); + } + + { + unassigned.write(false); + auto result = unassigned.read(); + Assert(0 == result, "Result value incorrect"); + } + + AssertThrow(unassigned.attachInterrupt([](void* arg) {}, CHANGE)); + AssertThrow(unassigned.detachInterrupt()); + + Assert(unassigned.capabilities() == Pin::Capabilities::None); + Assert(unassigned.name().equals("UNDEFINED_PIN")); + } +} diff --git a/Grbl_Esp32/test/TestFactory.cpp b/Grbl_Esp32/test/TestFactory.cpp new file mode 100644 index 00000000..de16befb --- /dev/null +++ b/Grbl_Esp32/test/TestFactory.cpp @@ -0,0 +1,101 @@ +#include "TestFactory.h" + +#include +#include + +#ifdef ESP32 + +#include "unity.h" +#include + +void TestFactory::runAll() { + int index = 0; + auto current = first; + const char* prev = nullptr; + while (current) { + ++index; + auto curTestName = current->unitTestName(); + auto curTestCase = current->unitTestCase(); + + char fullName[80]; + snprintf(fullName, 80, "%s:%s", curTestName, curTestCase); + + auto function = current->getFunction(); + UnityDefaultTestRun(function, fullName, index); + + current = current->next; + } +} + +#else + +# include +# include +# include + +# if defined _WIN32 || defined _WIN64 +# define WIN32_LEAN_AND_MEAN +# include +void setColor(int colorIndex) { // 10 = green, 12 = red, 7 = gray, 15 = white + HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE); + // you can loop k higher to see more color choices + // pick the colorattribute k you want + SetConsoleTextAttribute(hConsole, colorIndex); +} +# else +void setColor(int colorIndex) {} +# endif + +void TestFactory::runAll() { + const int Indent = 80; + + char spaces[Indent]; + memset(spaces, ' ', Indent - 1); + spaces[Indent - 1] = '\0'; + + auto current = first; + const char* prev = nullptr; + while (current) { + auto curTest = current->unitTestName(); + + setColor(15); + + if (prev == nullptr || !strcmp(prev, curTest)) { + printf("- Test: %s\r\n", curTest); + prev = curTest; + } + + setColor(7); + + printf(" - Case: %s", current->unitTestCase()); + + int len = int(strlen(current->unitTestCase())); + if (len >= (Indent - 5)) { + len = (Indent - 5); + } + printf(spaces + len); // pad. + + try { + setColor(10); + + current->run(); + printf("Passed.\r\n"); + } catch (AssertionFailed& ex) { + setColor(12); + printf("FAILED!\r\n"); + printf(ex.stackTrace.c_str()); + printf("\r\n"); + } catch (...) { + setColor(12); + printf("FAILED!\r\n"); + // We don't know where unfortunately... + } + current = current->next; + + setColor(7); + } + + printf("\r\nDone.\r\n"); +} + +#endif diff --git a/Grbl_Esp32/test/TestFactory.h b/Grbl_Esp32/test/TestFactory.h new file mode 100644 index 00000000..8a0a28bc --- /dev/null +++ b/Grbl_Esp32/test/TestFactory.h @@ -0,0 +1,43 @@ +#pragma once + +#include + +struct TestBase { + TestBase() : next(nullptr) {} + TestBase* next; + + virtual const char* unitTestCase() const = 0; + virtual const char* unitTestName() const = 0; + +#ifdef ESP32 + typedef void (*TestFunction)(); + virtual TestFunction getFunction() = 0; +#else +#endif + virtual void run() = 0; +}; + +class TestFactory { + TestBase* first; + TestBase* last; + + TestFactory() : first(nullptr), last(nullptr) {} + TestFactory(const TestFactory& o) = default; + +public: + static TestFactory& instance() { + static TestFactory instance_; + return instance_; + } + + void registerTest(TestBase* test) { + if (last == nullptr) { + first = last = test; + } else { + last->next = test; + last = test; + } + } + + void runAll(); +}; diff --git a/Grbl_Esp32/test/TestFramework.h b/Grbl_Esp32/test/TestFramework.h new file mode 100644 index 00000000..90f996a0 --- /dev/null +++ b/Grbl_Esp32/test/TestFramework.h @@ -0,0 +1,133 @@ +#pragma once + +#ifdef ESP32 + +# include +# include "unity.h" + +# include +# include "TestFactory.h" + +# define TEST_CLASS_NAME(testCase, testName) testCase##_##testName##_Test +# define TEST_INST_NAME(testCase, testName) testCase##_##testName##_Test_Instance + +// Defines a single unit test. Basically creates a small test class. + +# define Test(testCase, testName) \ + struct TEST_CLASS_NAME(testCase, testName) : TestBase { \ + TEST_CLASS_NAME(testCase, testName)() { TestFactory::instance().registerTest(this); } \ + \ + const char* unitTestCase() const override { return #testCase; } \ + const char* unitTestName() const override { return #testName; } \ + \ + static void runDetail(); \ + static void runWrap() { \ + try { \ + runDetail(); \ + } catch (AssertionFailed ex) { TEST_FAIL_MESSAGE(ex.stackTrace.c_str()); } catch (...) { \ + TEST_FAIL_MESSAGE("Failed for unknown reason."); \ + } \ + } \ + void run() override { runWrap(); } \ + \ + TestFunction getFunction() override { return runWrap; } \ + }; \ + \ + TEST_CLASS_NAME(testCase, testName) TEST_INST_NAME(testCase, testName); \ + \ + void TEST_CLASS_NAME(testCase, testName)::runDetail() + +# define NativeTest(testCase, testName) TEST_INST_NAME(testCase, testName) +# define PlatformTest(testCase, testName) Test(testCase, testName) + +inline void PrintSerial(const char* format, ...) { + va_list arg; + va_list copy; + va_start(arg, format); + va_copy(copy, arg); + size_t len = vsnprintf(NULL, 0, format, arg); + auto tmp = new char[len + 1]; + va_end(copy); + len = vsnprintf(tmp, len + 1, format, arg); + Serial.println(tmp); + va_end(arg); + delete[] tmp; +} + +# define Debug(fmt, ...) PrintSerial(fmt, __VA_ARGS__); + +# define AssertThrow(statement) \ + try { \ + statement; \ + Assert(false, "Expected statement to throw."); \ + } catch (...) {} + +#elif defined _WIN32 || defined _WIN64 + +# include + +// Use 'Assert(...)' please. + +# define GTEST_DONT_DEFINE_TEST 1 +# define GTEST_DONT_DEFINE_ASSERT_EQ 1 +# define GTEST_DONT_DEFINE_ASSERT_NE 1 +# define GTEST_DONT_DEFINE_ASSERT_LT 1 +# define GTEST_DONT_DEFINE_ASSERT_LE 1 +# define GTEST_DONT_DEFINE_ASSERT_GE 1 +# define GTEST_DONT_DEFINE_ASSERT_GT 1 +# define GTEST_DONT_DEFINE_FAIL 1 +# define GTEST_DONT_DEFINE_SUCCEED 1 + +# include "gtest/gtest.h" + +# undef EXPECT_THROW +# undef EXPECT_NO_THROW +# undef EXPECT_ANY_THROW +# undef ASSERT_THROW +# undef ASSERT_NO_THROW +# undef ASSERT_ANY_THROW + +# define Test(test_case_name, test_name) GTEST_TEST(test_case_name, test_name) + +# define NativeTest(testCase, testName) Test(testCase, testName) +# define PlatformTest(testCase, testName) TEST_INST_NAME(testCase, testName) + +# define Debug(fmt, ...) printf(fmt, __VA_ARGS__); printf("\r\n"); + +# define AssertThrow(statement) GTEST_TEST_ANY_THROW_(statement, GTEST_FATAL_FAILURE_) + +#else + +# include +# include "TestFactory.h" + +# define TEST_CLASS_NAME(testCase, testName) testCase##_##testName##_Test +# define TEST_INST_NAME(testCase, testName) testCase##_##testName##_Test_Instance + +// Defines a single unit test. Basically creates a small test class. + +# define Test(testCase, testName) \ + struct TEST_CLASS_NAME(testCase, testName) : TestBase { \ + TEST_CLASS_NAME(testCase, testName)() { TestFactory::instance().registerTest(this); } \ + \ + const char* unitTestCase() const override { return #testCase; } \ + const char* unitTestName() const override { return #testName; } \ + void run() override; \ + }; \ + \ + TEST_CLASS_NAME(testCase, testName) TEST_INST_NAME(testCase, testName); \ + \ + void TEST_CLASS_NAME(testCase, testName)::run() + +# define NativeTest(testCase, testName) Test(testCase, testName) +# define PlatformTest(testCase, testName) TEST_INST_NAME(testCase, testName) + +# define Debug(fmt, ...) printf(fmt, __VA_ARGS__); + +# define AssertThrow(statement) \ + try { \ + statement; \ + Assert(false, "Expected statement to throw."); \ + } catch (...) {} + +#endif diff --git a/Grbl_Esp32/test/TestFrameworkTest.cpp b/Grbl_Esp32/test/TestFrameworkTest.cpp new file mode 100644 index 00000000..c7bffcf9 --- /dev/null +++ b/Grbl_Esp32/test/TestFrameworkTest.cpp @@ -0,0 +1,21 @@ +#include "TestFramework.h" + +/* Normally you don't want these: + +Test(PassingTest, TestFrameworkTest) { + Assert(1 == 1); +} + +Test(FailingTest1, TestFrameworkTest) { + Assert(1 != 1); +} + +Test(FailingTest2, TestFrameworkTest) { + Assert(1 != 1, "Oops"); +} + +Test(FailingTest3, TestFrameworkTest) { + throw "oops"; +} + +*/ diff --git a/Grbl_Esp32/test/UnitTests.md b/Grbl_Esp32/test/UnitTests.md new file mode 100644 index 00000000..47360adf --- /dev/null +++ b/Grbl_Esp32/test/UnitTests.md @@ -0,0 +1,41 @@ +# Google test code + +These unit tests are designed to test a small portion of the GRBL_ESP32 +code, directly from your desktop PC. This is not a complete test of +GRBL_ESP32, but a starting point from which we can move on. Testing and +debugging on a desktop machine is obviously much more convenient than +it is on an ESP32 with a multitude of different configurations, not to +mention the fact that you can use a large variety of tools such as +code coverage, profiling, and so forth. + +Code here is split into two parts: +1. A subset of the GRBL code is compiled. Over time, this will become more. +2. Unit tests are executed on this code. + +## Prerequisites + +Google test framework. + +## Folders and how this works + +Support libraries are implemented that sort-of mimick the Arduino API where +appropriate. This functionality might be extended in the future, and is by +no means intended to be or a complete or even a "working" version; it's +designed to be _testable_. + +Generally speaking that means that most features are simply not available. +Things like GPIO just put stuff in a buffer, things like pins can be logged +for analysis and so forth. + +The "Support" folder is the main thing that gives this mimicking ability, +so that the code in the Grbl_Esp32 folder is able to compile. For example, +when including ``, in fact `Support/Arduino.h` is included. + +The include folders that have to be passed to the x86/x64 compiler are: + +- X86TestSupport +- ..\Grbl_Esp32 + +## Test code + +Google tests can be found in the `Tests` folder. diff --git a/Grbl_Esp32/test/test_main.cpp b/Grbl_Esp32/test/test_main.cpp new file mode 100644 index 00000000..8540a30f --- /dev/null +++ b/Grbl_Esp32/test/test_main.cpp @@ -0,0 +1,29 @@ +#ifdef ESP32 + +# include "TestFactory.h" +# include +# include +# include "unity.h" + +void test_blank() { + int i = 5; + TEST_ASSERT_EQUAL(i, 5); +} + +void setup() { + delay(500); // Let's give it some time first, in case it triggers a reboot. + + UNITY_BEGIN(); + + // calls to tests will go here + // RUN_TEST(test_blank); + + // Run all tests: + TestFactory::instance().runAll(); + + UNITY_END(); // stop unit testing +} + +void loop() {} + +#endif diff --git a/UnitTests.vcxproj b/UnitTests.vcxproj new file mode 100644 index 00000000..f321cc27 --- /dev/null +++ b/UnitTests.vcxproj @@ -0,0 +1,159 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + {33ece513-60d1-4949-a4a9-c95d353c2cf0} + Win32Proj + 10.0.18362.0 + Application + v142 + Unicode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Use + pch.h + Disabled + WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) + EnableFastChecks + MultiThreadedDebugDLL + Level3 + + + true + Console + + + + + NotUsing + pch.h + Disabled + X64;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) + EnableFastChecks + MultiThreadedDebugDLL + Level3 + true + $(MSBuildThisFileDirectory)X86TestSupport;$(MSBuildThisFileDirectory)GRBL_Esp32;%(AdditionalIncludeDirectories) + + + true + Console + + + + + Use + pch.h + WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + MultiThreadedDLL + Level3 + ProgramDatabase + + + true + Console + true + true + + + + + Use + pch.h + X64;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + MultiThreadedDLL + Level3 + ProgramDatabase + + + true + Console + true + true + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + \ No newline at end of file diff --git a/UnitTests.vcxproj.filters b/UnitTests.vcxproj.filters new file mode 100644 index 00000000..0d9c89de --- /dev/null +++ b/UnitTests.vcxproj.filters @@ -0,0 +1,156 @@ + + + + + {d251c8e1-710b-4299-bf32-211cbc23e1d2} + + + {0a063b22-59ea-4f6c-a573-7e4aef718f3b} + + + {cc13daa8-4fd4-4995-a724-77db03c229af} + + + {2259029a-8770-45f8-889a-f9c25becf189} + + + {a068458b-6148-4377-9d86-41337ee3d689} + + + {a166a529-634f-48dc-b368-6c03f6f0a36f} + + + + + test + + + test + + + src + + + src + + + src\Pins + + + src\Pins + + + src\Pins + + + src\Pins + + + src\Pins + + + src\Pins + + + src\Pins + + + src\Pins + + + src\Pins + + + src\StackTrace + + + src\StackTrace + + + X86TestSupport + + + X86TestSupport + + + X86TestSupport + + + + + test + + + test + + + test + + + test\Pins + + + test\Pins + + + test\Pins + + + test\Pins + + + src + + + src\Pins + + + src\Pins + + + src\Pins + + + src\Pins + + + src\Pins + + + src\Pins + + + src\Pins + + + src\Pins + + + src\Pins + + + src\StackTrace + + + src\StackTrace + + + X86TestSupport + + + X86TestSupport + + + X86TestSupport + + + + + test + + + src\StackTrace + + + + \ No newline at end of file diff --git a/generate_vcxproj.py b/generate_vcxproj.py index d0662e8c..c2624a6a 100644 --- a/generate_vcxproj.py +++ b/generate_vcxproj.py @@ -37,7 +37,7 @@ def FilterFromPath(path): class Vcxproj: # configuration, platform ConfigurationFmt = '\n'.join([ - ' ', + ' ', ' {0}', ' {1}', ' ']) @@ -138,7 +138,12 @@ class Generator: if filters != '': self.Folders.add(filters) - def AddFile(self, path): + def AddFile(self, path): + if path.find('/test/') >= 0: + return + elif path.find('\\test\\') >= 0: + return + (root, ext) = os.path.splitext(path) if ext in HEADER_EXT: self.Headers.add(path) @@ -148,7 +153,7 @@ class Generator: self.Others.add(path) else: return - + self.AddFolder(path) def Walk(self, path): diff --git a/packages.config b/packages.config new file mode 100644 index 00000000..6c6422e8 --- /dev/null +++ b/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/platformio.ini b/platformio.ini index cdd2e8a0..071e8ba9 100644 --- a/platformio.ini +++ b/platformio.ini @@ -1,6 +1,7 @@ [platformio] src_dir=Grbl_Esp32 lib_dir=libraries +test_dir=Grbl_Esp32/test data_dir=Grbl_Esp32/data default_envs=release ;extra_configs=debug.ini @@ -28,8 +29,6 @@ build_flags = -DCORE_DEBUG_LEVEL=0 -Wno-unused-variable -Wno-unused-function - ;-DDEBUG_REPORT_HEAP_SIZE - ;-DDEBUG_REPORT_STACK_FREE [env] lib_deps = @@ -51,9 +50,13 @@ board_build.flash_mode = qio build_flags = ${common.build_flags} src_filter = +<*.h> +<*.s> +<*.S> +<*.cpp> +<*.c> +<*.ino> + - -<.git/> - - - + -<.git/> - - - - [env:release] [env:debug] build_type = debug + +[env:test] +build_type = debug +test_build_project_src = true \ No newline at end of file