diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml new file mode 100644 index 00000000..651c9816 --- /dev/null +++ b/.github/workflows/unit-tests.yml @@ -0,0 +1,138 @@ +name: Unit Tests + +on: + push: + branches: [ master, develop ] + pull_request: + branches: [ master, develop ] + +jobs: + native-tests: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Cache pip packages + uses: actions/cache@v3 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} + + - name: Cache PlatformIO packages + uses: actions/cache@v3 + with: + path: ~/.platformio + key: ${{ runner.os }}-platformio-${{ hashFiles('**/platformio.ini') }} + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.x' + + - name: Install PlatformIO + run: | + python -m pip install --upgrade pip + pip install platformio + + - name: Install GUI dependencies + run: | + cd gui-v2 + npm install + npm run build + + - name: Install project dependencies + run: pio lib install + + - name: Run native unit tests + run: pio test -e native_test + + - name: Upload test results + uses: actions/upload-artifact@v3 + if: always() + with: + name: native-test-results + path: .pio/test/ + + esp32-build-test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Cache pip packages + uses: actions/cache@v3 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} + + - name: Cache PlatformIO packages + uses: actions/cache@v3 + with: + path: ~/.platformio + key: ${{ runner.os }}-platformio-${{ hashFiles('**/platformio.ini') }} + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.x' + + - name: Install PlatformIO + run: | + python -m pip install --upgrade pip + pip install platformio + + - name: Install GUI dependencies + run: | + cd gui-v2 + npm install + npm run build + + - name: Install project dependencies + run: pio lib install + + - name: Build test firmware for ESP32 + run: pio run -e test_esp32dev + + - name: Build test firmware for OpenEVSE WiFi v1 + run: pio run -e test_openevse_wifi_v1 + + - name: Upload build artifacts + uses: actions/upload-artifact@v3 + with: + name: esp32-test-firmware + path: .pio/build/*/firmware.bin + + python-tests: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.x' + + - name: Install Python dependencies + run: | + cd divert_sim + pip install -r requirements.txt + + - name: Run Python divert simulation tests + run: | + cd divert_sim + pytest -v + + - name: Upload Python test results + uses: actions/upload-artifact@v3 + if: always() + with: + name: python-test-results + path: divert_sim/ \ No newline at end of file diff --git a/platformio.ini b/platformio.ini index 23e839a8..f7c08ecf 100644 --- a/platformio.ini +++ b/platformio.ini @@ -27,6 +27,7 @@ [platformio] data_dir = src/data default_envs = openevse_wifi_v1 +test_dir = test_embedded [common] lib_deps = @@ -77,9 +78,7 @@ debug_flags = src_build_flags = # -D ARDUINOJSON_USE_LONG_LONG # -D ENABLE_ASYNC_WIFI_SCAN -build_flags = - -D ESP32 - -D CS_PLATFORM=CS_P_ESP32 +feature_flags = -D MG_ENABLE_SSL=1 -D MG_ENABLE_HTTP_STREAMING_MULTIPART=1 -D MG_ENABLE_EXTRA_ERRORS_DESC=1 @@ -94,7 +93,6 @@ build_flags = #-D MBEDTLS_DEBUG_C -D MG_ENABLE_SNTP=1 #-D ENABLE_DEBUG_MICROTASKS - -D CS_PLATFORM=CS_P_ESP32 -D MO_CUSTOM_WS ; MicroOcpp: don't use built-in WS library -D MO_CUSTOM_CONSOLE ; MicroOcpp: use custom debug out -D MO_DBG_LEVEL=MO_DL_INFO @@ -104,10 +102,16 @@ build_flags = #-D ENABLE_DEBUG_MONGOOSE_HTTP_CLIENT -D RAPI_MAX_COMMANDS=20 -D BUILD_ENV_NAME="$PIOENV" + -D MG_MAX_HTTP_REQUEST_SIZE=8196 + +build_flags = + -D ESP32 + -D CS_PLATFORM=CS_P_ESP32 -D ARDUINO_ARCH_ESP32 -D USE_ESP32 -D USE_ESP32_FRAMEWORK_ARDUINO - -D MG_MAX_HTTP_REQUEST_SIZE=8196 + ${common.feature_flags} + build_flags_openevse_tft = ${common.build_flags} ${common.src_build_flags} @@ -518,3 +522,69 @@ build_flags = #upload_protocol = custom #upload_command = curl -F firmware=@$SOURCE http://$UPLOAD_PORT/update build_type = debug + +[env:native] +platform = native +framework = +lib_deps = + ${common.lib_deps} + https://github.com/jeremypoulter/EpoxyDuino + EpoxyFS + EpoxyEepromEsp + EpoxyMockWiFi +lib_extra_dirs = .pio/libdeps/native_test/EpoxyDuino/libraries +build_flags = + -std=c++14 + ${common.feature_flags} + #${common.src_build_flags} + -D UNIT_TEST + -D ARDUINO=100 + -D EPOXY_DUINO + -D CS_PLATFORM=CS_P_UNIX + -D RAPI_PORT=SerialStdio + -D DEBUG_PORT=SerialStdio + +; ============================================== +; Test Environments +; ============================================== + +[env:native_test] +platform = native +framework = +lib_deps = + ${common.lib_deps} + https://github.com/jeremypoulter/EpoxyDuino + EpoxyFS + EpoxyEepromEsp + EpoxyMockWiFi +lib_extra_dirs = .pio/libdeps/native_test/EpoxyDuino/libraries +build_flags = + -std=c++14 + -D UNIT_TEST + -D ARDUINO=100 + -D EPOXY_DUINO + #-D EPOXY_CORE_ESP32 + #-D ESP32 + -D EPOXY_CORE_ESP8266 + -D ESP8266 + -D CS_PLATFORM=CS_P_UNIX +test_framework = unity + +[env:esp32_test] +platform = espressif32@6.12.0 +framework = arduino +board = esp32dev +lib_deps = + ${common.lib_deps} +build_flags = + ${common.build_flags} + -D UNIT_TEST + -D ESP32_TEST + -D ENABLE_DEBUG + -Wl,--wrap=millis +test_framework = unity +monitor_speed = 115200 +monitor_filters = esp32_exception_decoder +build_src_filter = + + + + diff --git a/src/debug.cpp b/src/debug.cpp index 803f617e..bc335149 100644 --- a/src/debug.cpp +++ b/src/debug.cpp @@ -1,3 +1,5 @@ +#ifndef UNIT_TEST + #include #ifndef DEBUG_PORT @@ -27,11 +29,15 @@ StreamSpy SerialDebug(DEBUG_PORT); StreamSpy SerialEvse(RAPI_PORT); +#endif // UNIT_TEST + void debug_setup() { DEBUG_PORT.begin(115200); - SerialDebug.begin(2048); - RAPI_PORT.begin(115200); + +#ifndef UNIT_TEST + SerialDebug.begin(2048); SerialEvse.begin(2048); +#endif // UNIT_TEST } diff --git a/src/debug.h b/src/debug.h index d0ae35c9..afd3f4c6 100644 --- a/src/debug.h +++ b/src/debug.h @@ -1,18 +1,23 @@ #ifndef __DEBUG_H #define __DEBUG_H -#undef DEBUG_PORT -#define DEBUG_PORT SerialDebug +#include "MicroDebug.h" -#undef RAPI_PORT -#define RAPI_PORT SerialEvse +#ifndef UNIT_TEST -#include "MicroDebug.h" #include "StreamSpy.h" extern StreamSpy SerialDebug; extern StreamSpy SerialEvse; +#undef DEBUG_PORT +#define DEBUG_PORT SerialDebug + +#undef RAPI_PORT +#define RAPI_PORT SerialEvse + extern void debug_setup(); +#endif // UNIT_TEST + #endif // __DEBUG_H diff --git a/test_embedded/README.md b/test_embedded/README.md new file mode 100644 index 00000000..0d13635b --- /dev/null +++ b/test_embedded/README.md @@ -0,0 +1,287 @@ +# OpenEVSE ESP32 WiFi Unit Tests + +This directory contains comprehensive unit tests for the OpenEVSE ESP32 WiFi firmware using the PlatformIO Unity test framework. + +## Test Structure + +### Test Files + +- **test_main.cpp** - Main test runner that coordinates all test suites +- **test_input_filter.cpp** - Tests for the InputFilter class (exponential smoothing) +- **test_evse_state.cpp** - Tests for the EvseState enum class +- **test_divert_mode.cpp** - Tests for the DivertMode enum class +- **test_limit.cpp** - Tests for LimitType and LimitProperties classes +- **test_energy_meter.cpp** - Tests for EnergyMeterData structure and serialization +- **test_app_config.cpp** - Tests for configuration system inline functions and flags +- **test_integration.cpp** - Integration tests for component interactions + +### Test Coverage + +#### InputFilter Class + +- Mathematical correctness of exponential smoothing +- Edge cases (zero tau, minimum tau) +- Step response and stability +- Time-dependent filtering behavior +- Multiple filter instances + +#### EvseState Enum + +- String parsing (fromString/toString) +- Enum value conversions +- Operator overloading +- Type safety +- Roundtrip conversions + +#### DivertMode Enum + +- Value assignments and conversions +- Long integer compatibility +- Operator functionality +- Type safety features + +#### Limit System + +- LimitType string conversions +- LimitProperties serialization/deserialization +- JSON data validation +- Error handling for invalid data + +#### Energy Meter + +- EnergyMeterData structure operations +- Reset functionality (partial/full) +- JSON serialization roundtrips +- Boundary value handling +- Date structure operations + +#### Configuration System + +- Flag bit operations +- Multi-bit field extraction +- Service enable/disable functions +- Flag isolation and interaction +- Configuration persistence simulation + +#### Integration Tests + +- Component interaction scenarios +- Configuration persistence simulation +- Error handling across components +- Real-world usage patterns + +## Running Tests + +### Prerequisites + +1. Install PlatformIO Core or use PlatformIO IDE +2. Ensure all project dependencies are installed: + + ```bash + pio lib install + ``` + +### Test Environments + +The platformio.ini file defines several test environments: + +#### Native Testing (Recommended for Development) + +```bash +pio test -e native_test +``` + +Runs tests on the host machine without ESP32 hardware. Fast execution for development. + +#### ESP32 Hardware Testing + +```bash +pio test -e esp32_test +``` + +Runs tests on actual ESP32 hardware. Required for hardware-specific behavior validation. + +#### Specific Board Testing + +```bash +pio test -e test_openevse_wifi_v1 +``` + +Tests on the specific OpenEVSE WiFi v1 board configuration. + +#### Generic ESP32 Testing + +```bash +pio test -e test_esp32dev +``` + +Tests on a generic ESP32 development board. + +### Running Specific Tests + +To run only specific test files: + +```bash +pio test -e native_test --filter test_input_filter +pio test -e esp32_test --filter test_integration +``` + +To run all tests: + +```bash +pio test +``` + +## Test Design Patterns + +### Mocking + +- **Time Mocking**: Tests use mock `millis()` function for deterministic time-dependent behavior +- **Hardware Abstraction**: Tests avoid direct hardware dependencies where possible + +### Test Organization + +- **Arrange-Act-Assert**: Tests follow clear setup, execution, and verification phases +- **Edge Case Coverage**: Tests include boundary conditions and error cases +- **Integration Scenarios**: Higher-level tests verify component interactions + +### Assertions + +- **Floating Point**: Uses `TEST_ASSERT_DOUBLE_WITHIN()` for floating-point comparisons +- **Enums**: Tests enum values directly and through conversions +- **Strings**: Validates string operations with exact matching +- **JSON**: Verifies serialization/deserialization correctness + +## Adding New Tests + +### For New Components + +1. Create a new test file: `test_component_name.cpp` +2. Include necessary headers and the component under test +3. Implement `setUp()` and `tearDown()` functions +4. Write individual test functions with descriptive names +5. Create a `run_component_tests()` function +6. Add the test runner to `test_main.cpp` + +### Test Function Naming Convention + +```cpp +void test_component_specific_behavior(void) { + // Test implementation +} +``` + +Use descriptive names that explain what is being tested. + +### Example Test Structure + +```cpp +#include +#include "component.h" + +void setUp(void) { + // Initialize before each test +} + +void tearDown(void) { + // Clean up after each test +} + +void test_component_basic_functionality(void) { + // Arrange + Component comp; + + // Act + bool result = comp.doSomething(); + + // Assert + TEST_ASSERT_TRUE(result); +} + +void run_component_tests(void) { + RUN_TEST(test_component_basic_functionality); +} +``` + +## Debugging Tests + +### Serial Output + +Tests running on ESP32 hardware will output results to the serial monitor: + +```bash +pio test -e esp32_test --monitor +``` + +### Verbose Mode + +For detailed test execution information: + +```bash +pio test -e native_test -v +``` + +### Test Debugging + +Use debug builds for detailed information: + +```bash +pio test -e esp32_test --debug +``` + +## Continuous Integration + +These tests are designed to run in CI/CD environments: + +- **Native tests** run quickly without hardware requirements +- **ESP32 tests** can run on hardware-in-the-loop CI setups +- **JSON output** available for CI result parsing + +## Test Maintenance + +### When Adding New Features + +1. Add unit tests for new classes/functions +2. Add integration tests for component interactions +3. Update existing tests if interfaces change +4. Ensure backward compatibility tests pass + +### When Fixing Bugs + +1. Add regression tests that reproduce the bug +2. Verify the fix resolves the test +3. Ensure existing tests still pass + +### Performance Considerations + +- Native tests run in seconds +- ESP32 tests may take several minutes +- Use native tests for rapid development cycles +- Use ESP32 tests for final validation + +## Dependencies + +Tests depend on the same libraries as the main firmware: + +- ArduinoJson for JSON handling +- Unity test framework (built into PlatformIO) +- ESP32 Arduino framework for embedded tests +- All project library dependencies + +## Known Limitations + +1. **Hardware Dependencies**: Some components require actual ESP32 hardware for complete testing +2. **Timing Dependencies**: Real-time behavior may vary between native and embedded tests +3. **Memory Constraints**: ESP32 tests must fit within device memory limits +4. **Floating Point**: Precision may vary between platforms + +## Contributing + +When contributing new tests: + +1. Follow existing naming conventions +2. Include both positive and negative test cases +3. Test edge cases and error conditions +4. Add integration tests for component interactions +5. Ensure tests run on both native and ESP32 environments +6. Update this documentation for new test patterns diff --git a/test_embedded/test_app_config.cpp b/test_embedded/test_app_config.cpp new file mode 100644 index 00000000..9fce9f28 --- /dev/null +++ b/test_embedded/test_app_config.cpp @@ -0,0 +1,355 @@ +#include +#include +#include "test_utils.h" + +// Include the config system under test +#include "app_config.h" + + + +// ============================================== +// Configuration Flag Tests +// ============================================== + +void test_config_emoncms_enabled(void) { + // Test disabled + flags = 0; + TEST_ASSERT_FALSE(config_emoncms_enabled()); + + // Test enabled + flags = CONFIG_SERVICE_EMONCMS; + TEST_ASSERT_TRUE(config_emoncms_enabled()); + + // Test with other flags set + flags = CONFIG_SERVICE_MQTT | CONFIG_SERVICE_EMONCMS; + TEST_ASSERT_TRUE(config_emoncms_enabled()); + + // Test with only other flags + flags = CONFIG_SERVICE_MQTT; + TEST_ASSERT_FALSE(config_emoncms_enabled()); +} + +void test_config_mqtt_enabled(void) { + // Test disabled + flags = 0; + TEST_ASSERT_FALSE(config_mqtt_enabled()); + + // Test enabled + flags = CONFIG_SERVICE_MQTT; + TEST_ASSERT_TRUE(config_mqtt_enabled()); + + // Test with other flags + flags = CONFIG_SERVICE_EMONCMS | CONFIG_SERVICE_MQTT; + TEST_ASSERT_TRUE(config_mqtt_enabled()); +} + +void test_config_ohm_enabled(void) { + // Test disabled + flags = 0; + TEST_ASSERT_FALSE(config_ohm_enabled()); + + // Test enabled + flags = CONFIG_SERVICE_OHM; + TEST_ASSERT_TRUE(config_ohm_enabled()); +} + +void test_config_sntp_enabled(void) { + // Test disabled + flags = 0; + TEST_ASSERT_FALSE(config_sntp_enabled()); + + // Test enabled + flags = CONFIG_SERVICE_SNTP; + TEST_ASSERT_TRUE(config_sntp_enabled()); +} + +void test_config_mqtt_protocol(void) { + // Test default protocol (0) + flags = 0; + TEST_ASSERT_EQUAL(0, config_mqtt_protocol()); + + // Test protocol 1 + flags = (1 << 4); + TEST_ASSERT_EQUAL(1, config_mqtt_protocol()); + + // Test protocol 7 (max 3-bit value) + flags = (7 << 4); + TEST_ASSERT_EQUAL(7, config_mqtt_protocol()); + + // Test with other flags set + flags = CONFIG_SERVICE_MQTT | (3 << 4); + TEST_ASSERT_EQUAL(3, config_mqtt_protocol()); +} + +void test_config_mqtt_retained(void) { + // Test disabled + flags = 0; + TEST_ASSERT_FALSE(config_mqtt_retained()); + + // Test enabled + flags = CONFIG_MQTT_RETAINED; + TEST_ASSERT_TRUE(config_mqtt_retained()); +} + +void test_config_mqtt_reject_unauthorized(void) { + // Test default (should reject unauthorized) + flags = 0; + TEST_ASSERT_TRUE(config_mqtt_reject_unauthorized()); + + // Test allow any cert + flags = CONFIG_MQTT_ALLOW_ANY_CERT; + TEST_ASSERT_FALSE(config_mqtt_reject_unauthorized()); +} + +void test_config_ocpp_enabled(void) { + // Test disabled + flags = 0; + TEST_ASSERT_FALSE(config_ocpp_enabled()); + + // Test enabled + flags = CONFIG_SERVICE_OCPP; + TEST_ASSERT_TRUE(config_ocpp_enabled()); +} + +void test_config_ocpp_access_functions(void) { + // Test suspend capability + flags = 0; + TEST_ASSERT_FALSE(config_ocpp_access_can_suspend()); + + flags = CONFIG_OCPP_ACCESS_SUSPEND; + TEST_ASSERT_TRUE(config_ocpp_access_can_suspend()); + + // Test energize capability + flags = 0; + TEST_ASSERT_FALSE(config_ocpp_access_can_energize()); + + flags = CONFIG_OCPP_ACCESS_ENERGIZE; + TEST_ASSERT_TRUE(config_ocpp_access_can_energize()); +} + +void test_config_ocpp_authorization_functions(void) { + // Test auto authorization + flags = 0; + TEST_ASSERT_FALSE(config_ocpp_auto_authorization()); + + flags = CONFIG_OCPP_AUTO_AUTH; + TEST_ASSERT_TRUE(config_ocpp_auto_authorization()); + + // Test offline authorization + flags = 0; + TEST_ASSERT_FALSE(config_ocpp_offline_authorization()); + + flags = CONFIG_OCPP_OFFLINE_AUTH; + TEST_ASSERT_TRUE(config_ocpp_offline_authorization()); +} + +void test_config_divert_enabled(void) { + // Test disabled + flags = 0; + TEST_ASSERT_FALSE(config_divert_enabled()); + + // Test enabled + flags = CONFIG_SERVICE_DIVERT; + TEST_ASSERT_TRUE(config_divert_enabled()); +} + +void test_config_current_shaper_enabled(void) { + // Test disabled + flags = 0; + TEST_ASSERT_FALSE(config_current_shaper_enabled()); + + // Test enabled + flags = CONFIG_SERVICE_CUR_SHAPER; + TEST_ASSERT_TRUE(config_current_shaper_enabled()); +} + +void test_config_charge_mode(void) { + // Test default mode (0) + flags = 0; + TEST_ASSERT_EQUAL(0, config_charge_mode()); + + // Test mode 1 + flags = (1 << 10); + TEST_ASSERT_EQUAL(1, config_charge_mode()); + + // Test mode 7 (max 3-bit value) + flags = (7 << 10); + TEST_ASSERT_EQUAL(7, config_charge_mode()); + + // Test with other flags set + flags = CONFIG_SERVICE_MQTT | (5 << 10); + TEST_ASSERT_EQUAL(5, config_charge_mode()); +} + +void test_config_pause_uses_disabled(void) { + // Test default + flags = 0; + TEST_ASSERT_FALSE(config_pause_uses_disabled()); + + // Test enabled + flags = CONFIG_PAUSE_USES_DISABLED; + TEST_ASSERT_TRUE(config_pause_uses_disabled()); +} + +void test_config_vehicle_range_miles(void) { + // Test default (should be false - km) + flags = 0; + TEST_ASSERT_FALSE(config_vehicle_range_miles()); + + // Test miles enabled + flags = CONFIG_VEHICLE_RANGE_MILES; + TEST_ASSERT_TRUE(config_vehicle_range_miles()); +} + +void test_config_rfid_enabled(void) { + // Test disabled + flags = 0; + TEST_ASSERT_FALSE(config_rfid_enabled()); + + // Test enabled + flags = CONFIG_RFID; + TEST_ASSERT_TRUE(config_rfid_enabled()); +} + +void test_config_factory_write_lock(void) { + // Test unlocked + flags = 0; + TEST_ASSERT_FALSE(config_factory_write_lock()); + + // Test locked + flags = CONFIG_FACTORY_WRITE_LOCK; + TEST_ASSERT_TRUE(config_factory_write_lock()); +} + +void test_config_threephase_enabled(void) { + // Test disabled + flags = 0; + TEST_ASSERT_FALSE(config_threephase_enabled()); + + // Test enabled + flags = CONFIG_THREEPHASE; + TEST_ASSERT_TRUE(config_threephase_enabled()); +} + +void test_config_wizard_passed(void) { + // Test not passed + flags = 0; + TEST_ASSERT_FALSE(config_wizard_passed()); + + // Test passed + flags = CONFIG_WIZARD; + TEST_ASSERT_TRUE(config_wizard_passed()); +} + +void test_config_default_state(void) { + // Test default disabled state + flags = 0; + EvseState state = config_default_state(); + TEST_ASSERT_EQUAL(EvseState::Disabled, state); + + // Test active state + flags = CONFIG_DEFAULT_STATE; + state = config_default_state(); + TEST_ASSERT_EQUAL(EvseState::Active, state); +} + +void test_multiple_flags_interaction(void) { + // Test multiple flags can be set simultaneously + flags = CONFIG_SERVICE_MQTT | CONFIG_SERVICE_EMONCMS | CONFIG_SERVICE_DIVERT; + + TEST_ASSERT_TRUE(config_mqtt_enabled()); + TEST_ASSERT_TRUE(config_emoncms_enabled()); + TEST_ASSERT_TRUE(config_divert_enabled()); + TEST_ASSERT_FALSE(config_ocpp_enabled()); +} + +void test_flag_bit_positions(void) { + // Test that flag values are powers of 2 (single bit set) + uint32_t flag_values[] = { + CONFIG_SERVICE_EMONCMS, + CONFIG_SERVICE_MQTT, + CONFIG_SERVICE_OHM, + CONFIG_SERVICE_SNTP, + CONFIG_MQTT_ALLOW_ANY_CERT, + CONFIG_SERVICE_TESLA, + CONFIG_SERVICE_DIVERT, + CONFIG_PAUSE_USES_DISABLED, + CONFIG_SERVICE_OCPP, + CONFIG_OCPP_ACCESS_SUSPEND, + CONFIG_OCPP_ACCESS_ENERGIZE, + CONFIG_VEHICLE_RANGE_MILES, + CONFIG_RFID, + CONFIG_SERVICE_CUR_SHAPER, + CONFIG_MQTT_RETAINED, + CONFIG_FACTORY_WRITE_LOCK, + CONFIG_OCPP_AUTO_AUTH, + CONFIG_OCPP_OFFLINE_AUTH, + CONFIG_THREEPHASE, + CONFIG_WIZARD, + CONFIG_DEFAULT_STATE + }; + + size_t num_flags = sizeof(flag_values) / sizeof(flag_values[0]); + + for (size_t i = 0; i < num_flags; i++) { + uint32_t flag = flag_values[i]; + + // Check that only one bit is set (power of 2) + TEST_ASSERT_TRUE((flag & (flag - 1)) == 0); + + // Check that flag is not zero + TEST_ASSERT_NOT_EQUAL(0, flag); + } +} + +void test_multi_bit_fields(void) { + // Test CONFIG_MQTT_PROTOCOL field (3 bits) + uint32_t protocol_mask = CONFIG_MQTT_PROTOCOL; + TEST_ASSERT_EQUAL(7 << 4, protocol_mask); // Should be 3 bits at position 4 + + // Test CONFIG_CHARGE_MODE field (3 bits) + uint32_t charge_mask = CONFIG_CHARGE_MODE; + TEST_ASSERT_EQUAL(7 << 10, charge_mask); // Should be 3 bits at position 10 +} + +void test_flag_isolation(void) { + // Test that setting one flag doesn't affect others + flags = 0; + + flags |= CONFIG_SERVICE_MQTT; + TEST_ASSERT_TRUE(config_mqtt_enabled()); + TEST_ASSERT_FALSE(config_emoncms_enabled()); + TEST_ASSERT_FALSE(config_divert_enabled()); + + flags |= CONFIG_SERVICE_DIVERT; + TEST_ASSERT_TRUE(config_mqtt_enabled()); + TEST_ASSERT_TRUE(config_divert_enabled()); + TEST_ASSERT_FALSE(config_emoncms_enabled()); +} + +void run_app_config_tests(void) { + RUN_TEST(test_config_emoncms_enabled); + RUN_TEST(test_config_mqtt_enabled); + RUN_TEST(test_config_ohm_enabled); + RUN_TEST(test_config_sntp_enabled); + RUN_TEST(test_config_mqtt_protocol); + RUN_TEST(test_config_mqtt_retained); + RUN_TEST(test_config_mqtt_reject_unauthorized); + RUN_TEST(test_config_ocpp_enabled); + RUN_TEST(test_config_ocpp_access_functions); + RUN_TEST(test_config_ocpp_authorization_functions); + RUN_TEST(test_config_divert_enabled); + RUN_TEST(test_config_current_shaper_enabled); + RUN_TEST(test_config_charge_mode); + RUN_TEST(test_config_pause_uses_disabled); + RUN_TEST(test_config_vehicle_range_miles); + RUN_TEST(test_config_rfid_enabled); + RUN_TEST(test_config_factory_write_lock); + RUN_TEST(test_config_threephase_enabled); + RUN_TEST(test_config_wizard_passed); + RUN_TEST(test_config_default_state); + RUN_TEST(test_multiple_flags_interaction); + RUN_TEST(test_flag_bit_positions); + RUN_TEST(test_multi_bit_fields); + RUN_TEST(test_flag_isolation); +} \ No newline at end of file diff --git a/test_embedded/test_basic.cpp b/test_embedded/test_basic.cpp new file mode 100644 index 00000000..ea981d2f --- /dev/null +++ b/test_embedded/test_basic.cpp @@ -0,0 +1,45 @@ +#include +#include "test_utils.h" + +// Only include headers for files we have source for +#include "app_config.h" + +// Simple tests that can work with the available source files + +void test_basic_app_config_functions(void) { + // Test config flag functions that should be available + // These are simple functions that should work + TEST_ASSERT_TRUE(true); // Placeholder +} + +void test_mock_millis_function(void) { + // Test our mock millis function + set_mock_millis(1000); + TEST_ASSERT_EQUAL_UINT32(1000, get_mock_millis()); + + advance_mock_millis(500); + TEST_ASSERT_EQUAL_UINT32(1500, get_mock_millis()); +} + +void test_basic_functionality(void) { + // Very basic test that should always pass + TEST_ASSERT_EQUAL_INT(1, 1); + TEST_ASSERT_TRUE(true); + TEST_ASSERT_FALSE(false); +} + +// Unity test runner +void setup() { + delay(2000); // Give time for serial monitor to connect + UNITY_BEGIN(); + + RUN_TEST(test_basic_functionality); + RUN_TEST(test_mock_millis_function); + RUN_TEST(test_basic_app_config_functions); + + UNITY_END(); +} + +void loop() { + // Empty loop +} \ No newline at end of file diff --git a/test_embedded/test_divert_mode.cpp b/test_embedded/test_divert_mode.cpp new file mode 100644 index 00000000..aa000bda --- /dev/null +++ b/test_embedded/test_divert_mode.cpp @@ -0,0 +1,173 @@ +#include +#include +#include "test_utils.h" + +// Include the enum under test +#include "divert.h" + + + +void test_divert_mode_constructor_default(void) { + DivertMode mode; + // Default constructor creates mode with uninitialized value + // We can't test the exact value since it's not specified + TEST_PASS_MESSAGE("DivertMode default constructor test"); +} + +void test_divert_mode_constructor_with_value(void) { + DivertMode mode_normal(DivertMode::Normal); + DivertMode mode_eco(DivertMode::Eco); + + TEST_ASSERT_EQUAL(DivertMode::Normal, mode_normal); + TEST_ASSERT_EQUAL(DivertMode::Eco, mode_eco); +} + +void test_divert_mode_constructor_with_long(void) { + DivertMode mode_normal(1L); + DivertMode mode_eco(2L); + + TEST_ASSERT_EQUAL(DivertMode::Normal, mode_normal); + TEST_ASSERT_EQUAL(DivertMode::Eco, mode_eco); +} + +void test_divert_mode_assignment_operator_value(void) { + DivertMode mode; + + mode = DivertMode::Normal; + TEST_ASSERT_EQUAL(DivertMode::Normal, mode); + + mode = DivertMode::Eco; + TEST_ASSERT_EQUAL(DivertMode::Eco, mode); +} + +void test_divert_mode_assignment_operator_long(void) { + DivertMode mode; + + mode = 1L; + TEST_ASSERT_EQUAL(DivertMode::Normal, mode); + + mode = 2L; + TEST_ASSERT_EQUAL(DivertMode::Eco, mode); +} + +void test_divert_mode_conversion_operator(void) { + DivertMode mode_normal(DivertMode::Normal); + DivertMode mode_eco(DivertMode::Eco); + + // Test implicit conversion to Value + DivertMode::Value val_normal = mode_normal; + DivertMode::Value val_eco = mode_eco; + + TEST_ASSERT_EQUAL(DivertMode::Normal, val_normal); + TEST_ASSERT_EQUAL(DivertMode::Eco, val_eco); +} + +void test_divert_mode_comparison(void) { + DivertMode mode1(DivertMode::Normal); + DivertMode mode2(DivertMode::Normal); + DivertMode mode3(DivertMode::Eco); + + // Test equality comparison + TEST_ASSERT_TRUE(mode1 == DivertMode::Normal); + TEST_ASSERT_TRUE(mode1 == mode2); + TEST_ASSERT_FALSE(mode1 == mode3); + TEST_ASSERT_FALSE(mode1 == DivertMode::Eco); +} + +void test_divert_mode_switch_statement(void) { + DivertMode mode(DivertMode::Normal); + + int result = 0; + switch(mode) { + case DivertMode::Normal: + result = 1; + break; + case DivertMode::Eco: + result = 2; + break; + } + + TEST_ASSERT_EQUAL(1, result); + + mode = DivertMode::Eco; + result = 0; + switch(mode) { + case DivertMode::Normal: + result = 1; + break; + case DivertMode::Eco: + result = 2; + break; + } + + TEST_ASSERT_EQUAL(2, result); +} + +void test_divert_mode_value_constants(void) { + // Test that the enum values have the expected numeric values + TEST_ASSERT_EQUAL(1, static_cast(DivertMode::Normal)); + TEST_ASSERT_EQUAL(2, static_cast(DivertMode::Eco)); +} + +void test_divert_mode_copy_constructor(void) { + DivertMode original(DivertMode::Eco); + DivertMode copy(original); + + TEST_ASSERT_EQUAL(DivertMode::Eco, copy); + TEST_ASSERT_EQUAL(original, copy); +} + +void test_divert_mode_assignment_chain(void) { + DivertMode mode1, mode2, mode3; + + // Test assignment chain + mode3 = mode2 = mode1 = DivertMode::Normal; + + TEST_ASSERT_EQUAL(DivertMode::Normal, mode1); + TEST_ASSERT_EQUAL(DivertMode::Normal, mode2); + TEST_ASSERT_EQUAL(DivertMode::Normal, mode3); +} + +void test_divert_mode_assignment_return_value(void) { + DivertMode mode; + + // Test that assignment returns the assigned object + DivertMode result = (mode = DivertMode::Eco); + TEST_ASSERT_EQUAL(DivertMode::Eco, result); + TEST_ASSERT_EQUAL(DivertMode::Eco, mode); +} + +void test_divert_mode_invalid_long_values(void) { + // Test behavior with invalid long values + DivertMode mode_invalid(99L); + + // The mode should store the value even if it's not a valid enum + TEST_ASSERT_EQUAL(99, static_cast(mode_invalid)); +} + +void test_divert_mode_type_safety(void) { + DivertMode mode(DivertMode::Normal); + + // Test that we can't accidentally use mode as boolean + // This should not compile if the bool operator delete is working + // We can't test compilation failures in unit tests, but we can + // document the expected behavior + TEST_PASS_MESSAGE("DivertMode prevents boolean conversion"); +} + +void run_divert_mode_tests(void) { + RUN_TEST(test_divert_mode_constructor_default); + RUN_TEST(test_divert_mode_constructor_with_value); + RUN_TEST(test_divert_mode_constructor_with_long); + RUN_TEST(test_divert_mode_assignment_operator_value); + RUN_TEST(test_divert_mode_assignment_operator_long); + RUN_TEST(test_divert_mode_conversion_operator); + RUN_TEST(test_divert_mode_comparison); + RUN_TEST(test_divert_mode_switch_statement); + RUN_TEST(test_divert_mode_value_constants); + RUN_TEST(test_divert_mode_copy_constructor); + RUN_TEST(test_divert_mode_assignment_chain); + RUN_TEST(test_divert_mode_assignment_return_value); + RUN_TEST(test_divert_mode_invalid_long_values); + RUN_TEST(test_divert_mode_type_safety); +} \ No newline at end of file diff --git a/test_embedded/test_energy_meter.cpp b/test_embedded/test_energy_meter.cpp new file mode 100644 index 00000000..f2db6453 --- /dev/null +++ b/test_embedded/test_energy_meter.cpp @@ -0,0 +1,353 @@ +#include +#include +#include +#include "test_utils.h" + +// Include the class under test +#include "energy_meter.h" + + + +// ============================================== +// EnergyMeterDate Tests +// ============================================== + +void test_energy_meter_date_struct(void) { + EnergyMeterDate date; + + // Test that we can assign values + date.day = 15; + date.month = 6; + date.year = 2023; + + TEST_ASSERT_EQUAL(15, date.day); + TEST_ASSERT_EQUAL(6, date.month); + TEST_ASSERT_EQUAL(2023, date.year); +} + +void test_energy_meter_date_copy(void) { + EnergyMeterDate date1; + date1.day = 25; + date1.month = 12; + date1.year = 2023; + + EnergyMeterDate date2 = date1; + + TEST_ASSERT_EQUAL(date1.day, date2.day); + TEST_ASSERT_EQUAL(date1.month, date2.month); + TEST_ASSERT_EQUAL(date1.year, date2.year); +} + +// ============================================== +// EnergyMeterData Tests +// ============================================== + +void test_energy_meter_data_constructor(void) { + EnergyMeterData data; + + // Constructor should initialize values to zero + TEST_ASSERT_EQUAL_DOUBLE(0.0, data.session); + TEST_ASSERT_EQUAL_DOUBLE(0.0, data.total); + TEST_ASSERT_EQUAL_DOUBLE(0.0, data.daily); + TEST_ASSERT_EQUAL_DOUBLE(0.0, data.weekly); + TEST_ASSERT_EQUAL_DOUBLE(0.0, data.monthly); + TEST_ASSERT_EQUAL_DOUBLE(0.0, data.yearly); + TEST_ASSERT_EQUAL_DOUBLE(0.0, data.elapsed); + TEST_ASSERT_EQUAL(0, data.switches); + TEST_ASSERT_FALSE(data.imported); +} + +void test_energy_meter_data_assignment(void) { + EnergyMeterData data; + + // Test that we can assign values + data.session = 15.5; + data.total = 1000.75; + data.daily = 25.0; + data.weekly = 150.25; + data.monthly = 600.0; + data.yearly = 7200.5; + data.elapsed = 3600.0; + data.switches = 42; + data.imported = true; + + data.date.day = 15; + data.date.month = 6; + data.date.year = 2023; + + TEST_ASSERT_EQUAL_DOUBLE(15.5, data.session); + TEST_ASSERT_EQUAL_DOUBLE(1000.75, data.total); + TEST_ASSERT_EQUAL_DOUBLE(25.0, data.daily); + TEST_ASSERT_EQUAL_DOUBLE(150.25, data.weekly); + TEST_ASSERT_EQUAL_DOUBLE(600.0, data.monthly); + TEST_ASSERT_EQUAL_DOUBLE(7200.5, data.yearly); + TEST_ASSERT_EQUAL_DOUBLE(3600.0, data.elapsed); + TEST_ASSERT_EQUAL(42, data.switches); + TEST_ASSERT_TRUE(data.imported); + + TEST_ASSERT_EQUAL(15, data.date.day); + TEST_ASSERT_EQUAL(6, data.date.month); + TEST_ASSERT_EQUAL(2023, data.date.year); +} + +void test_energy_meter_data_reset_partial(void) { + EnergyMeterData data; + + // Set some test values + data.session = 50.0; + data.total = 1000.0; + data.daily = 25.0; + data.weekly = 150.0; + data.monthly = 600.0; + data.yearly = 7200.0; + data.elapsed = 3600.0; + data.switches = 100; + data.imported = true; + + // Partial reset (fullreset = false, import = false) + data.reset(false, false); + + // Session data should be reset + TEST_ASSERT_EQUAL_DOUBLE(0.0, data.session); + TEST_ASSERT_EQUAL_DOUBLE(0.0, data.elapsed); + + // Total and switches should be preserved + TEST_ASSERT_EQUAL_DOUBLE(1000.0, data.total); + TEST_ASSERT_EQUAL(100, data.switches); + + // Periodic counters should be reset + TEST_ASSERT_EQUAL_DOUBLE(0.0, data.daily); + TEST_ASSERT_EQUAL_DOUBLE(0.0, data.weekly); + TEST_ASSERT_EQUAL_DOUBLE(0.0, data.monthly); + TEST_ASSERT_EQUAL_DOUBLE(0.0, data.yearly); + + // Import flag should be reset when import = false + TEST_ASSERT_FALSE(data.imported); +} + +void test_energy_meter_data_reset_full(void) { + EnergyMeterData data; + + // Set some test values + data.session = 50.0; + data.total = 1000.0; + data.switches = 100; + data.imported = true; + + // Full reset (fullreset = true, import = false) + data.reset(true, false); + + // Everything should be reset to zero + TEST_ASSERT_EQUAL_DOUBLE(0.0, data.session); + TEST_ASSERT_EQUAL_DOUBLE(0.0, data.total); + TEST_ASSERT_EQUAL_DOUBLE(0.0, data.elapsed); + TEST_ASSERT_EQUAL(0, data.switches); + TEST_ASSERT_FALSE(data.imported); +} + +void test_energy_meter_data_reset_with_import(void) { + EnergyMeterData data; + + // Set imported to false initially + data.imported = false; + + // Reset with import = true + data.reset(false, true); + + // Import flag should be set to true + TEST_ASSERT_TRUE(data.imported); +} + +void test_energy_meter_data_serialize(void) { + EnergyMeterData data; + + // Set test values + data.session = 15.5; + data.total = 1000.75; + data.daily = 25.0; + data.weekly = 150.25; + data.monthly = 600.0; + data.yearly = 7200.5; + data.elapsed = 3600.0; + data.switches = 42; + data.imported = true; + + data.date.day = 15; + data.date.month = 6; + data.date.year = 2023; + + // Serialize to JSON + StaticJsonDocument doc; + data.serialize(doc); + + // Check serialized values + TEST_ASSERT_EQUAL_DOUBLE(15.5, doc["session"]); + TEST_ASSERT_EQUAL_DOUBLE(1000.75, doc["total"]); + TEST_ASSERT_EQUAL_DOUBLE(25.0, doc["daily"]); + TEST_ASSERT_EQUAL_DOUBLE(150.25, doc["weekly"]); + TEST_ASSERT_EQUAL_DOUBLE(600.0, doc["monthly"]); + TEST_ASSERT_EQUAL_DOUBLE(7200.5, doc["yearly"]); + TEST_ASSERT_EQUAL_DOUBLE(3600.0, doc["elapsed"]); + TEST_ASSERT_EQUAL(42, doc["switches"]); + TEST_ASSERT_TRUE(doc["imported"]); + + // Check date object + JsonObject dateObj = doc["date"]; + TEST_ASSERT_EQUAL(15, dateObj["day"]); + TEST_ASSERT_EQUAL(6, dateObj["month"]); + TEST_ASSERT_EQUAL(2023, dateObj["year"]); +} + +void test_energy_meter_data_deserialize(void) { + EnergyMeterData data; + + // Create JSON document + StaticJsonDocument doc; + doc["session"] = 25.5; + doc["total"] = 2000.5; + doc["daily"] = 50.0; + doc["weekly"] = 300.0; + doc["monthly"] = 1200.0; + doc["yearly"] = 14400.0; + doc["elapsed"] = 7200.0; + doc["switches"] = 84; + doc["imported"] = false; + + JsonObject dateObj = doc.createNestedObject("date"); + dateObj["day"] = 20; + dateObj["month"] = 12; + dateObj["year"] = 2023; + + // Deserialize + data.deserialize(doc); + + // Check deserialized values + TEST_ASSERT_EQUAL_DOUBLE(25.5, data.session); + TEST_ASSERT_EQUAL_DOUBLE(2000.5, data.total); + TEST_ASSERT_EQUAL_DOUBLE(50.0, data.daily); + TEST_ASSERT_EQUAL_DOUBLE(300.0, data.weekly); + TEST_ASSERT_EQUAL_DOUBLE(1200.0, data.monthly); + TEST_ASSERT_EQUAL_DOUBLE(14400.0, data.yearly); + TEST_ASSERT_EQUAL_DOUBLE(7200.0, data.elapsed); + TEST_ASSERT_EQUAL(84, data.switches); + TEST_ASSERT_FALSE(data.imported); + + TEST_ASSERT_EQUAL(20, data.date.day); + TEST_ASSERT_EQUAL(12, data.date.month); + TEST_ASSERT_EQUAL(2023, data.date.year); +} + +void test_energy_meter_data_deserialize_missing_fields(void) { + EnergyMeterData data; + + // Set initial values + data.session = 100.0; + data.total = 5000.0; + data.switches = 200; + + // Create JSON with only some fields + StaticJsonDocument doc; + doc["session"] = 50.0; + // Missing other fields + + // Deserialize should handle missing fields gracefully + data.deserialize(doc); + + // Present field should be updated + TEST_ASSERT_EQUAL_DOUBLE(50.0, data.session); + + // Missing fields should retain original values or be set to defaults + // (depending on implementation) +} + +void test_energy_meter_data_roundtrip_serialization(void) { + EnergyMeterData data1, data2; + + // Set values in first object + data1.session = 33.33; + data1.total = 4444.44; + data1.daily = 55.55; + data1.weekly = 666.66; + data1.monthly = 7777.77; + data1.yearly = 88888.88; + data1.elapsed = 9999.99; + data1.switches = 123; + data1.imported = true; + + data1.date.day = 31; + data1.date.month = 12; + data1.date.year = 2023; + + // Serialize + StaticJsonDocument doc; + data1.serialize(doc); + + // Deserialize into second object + data2.deserialize(doc); + + // Check values match (within floating point precision) + TEST_ASSERT_EQUAL_DOUBLE(data1.session, data2.session); + TEST_ASSERT_EQUAL_DOUBLE(data1.total, data2.total); + TEST_ASSERT_EQUAL_DOUBLE(data1.daily, data2.daily); + TEST_ASSERT_EQUAL_DOUBLE(data1.weekly, data2.weekly); + TEST_ASSERT_EQUAL_DOUBLE(data1.monthly, data2.monthly); + TEST_ASSERT_EQUAL_DOUBLE(data1.yearly, data2.yearly); + TEST_ASSERT_EQUAL_DOUBLE(data1.elapsed, data2.elapsed); + TEST_ASSERT_EQUAL(data1.switches, data2.switches); + TEST_ASSERT_EQUAL(data1.imported, data2.imported); + + TEST_ASSERT_EQUAL(data1.date.day, data2.date.day); + TEST_ASSERT_EQUAL(data1.date.month, data2.date.month); + TEST_ASSERT_EQUAL(data1.date.year, data2.date.year); +} + +void test_energy_meter_data_boundary_values(void) { + EnergyMeterData data; + + // Test with boundary values + data.session = 0.0; + data.total = 999999.999; + data.elapsed = 0.001; + data.switches = 0; + data.imported = false; + + data.date.day = 1; + data.date.month = 1; + data.date.year = 1900; + + // Serialize and deserialize + StaticJsonDocument doc; + data.serialize(doc); + + EnergyMeterData data2; + data2.deserialize(doc); + + // Values should be preserved + TEST_ASSERT_EQUAL_DOUBLE(data.session, data2.session); + TEST_ASSERT_EQUAL_DOUBLE(data.total, data2.total); + TEST_ASSERT_EQUAL_DOUBLE(data.elapsed, data2.elapsed); + TEST_ASSERT_EQUAL(data.switches, data2.switches); + TEST_ASSERT_EQUAL(data.imported, data2.imported); + + TEST_ASSERT_EQUAL(data.date.day, data2.date.day); + TEST_ASSERT_EQUAL(data.date.month, data2.date.month); + TEST_ASSERT_EQUAL(data.date.year, data2.date.year); +} + +void run_energy_meter_tests(void) { + // EnergyMeterDate tests + RUN_TEST(test_energy_meter_date_struct); + RUN_TEST(test_energy_meter_date_copy); + + // EnergyMeterData tests + RUN_TEST(test_energy_meter_data_constructor); + RUN_TEST(test_energy_meter_data_assignment); + RUN_TEST(test_energy_meter_data_reset_partial); + RUN_TEST(test_energy_meter_data_reset_full); + RUN_TEST(test_energy_meter_data_reset_with_import); + RUN_TEST(test_energy_meter_data_serialize); + RUN_TEST(test_energy_meter_data_deserialize); + RUN_TEST(test_energy_meter_data_deserialize_missing_fields); + RUN_TEST(test_energy_meter_data_roundtrip_serialization); + RUN_TEST(test_energy_meter_data_boundary_values); +} \ No newline at end of file diff --git a/test_embedded/test_evse_state.cpp b/test_embedded/test_evse_state.cpp new file mode 100644 index 00000000..dfbfdcb5 --- /dev/null +++ b/test_embedded/test_evse_state.cpp @@ -0,0 +1,198 @@ +#include +#include +#include "test_utils.h" + +// Include the enum under test +#include "evse_state.h" + + + +void test_evse_state_constructor_default(void) { + EvseState state; + // Default constructor should create None state + TEST_ASSERT_EQUAL(EvseState::None, state); +} + +void test_evse_state_constructor_with_value(void) { + EvseState state_active(EvseState::Active); + EvseState state_disabled(EvseState::Disabled); + EvseState state_none(EvseState::None); + + TEST_ASSERT_EQUAL(EvseState::Active, state_active); + TEST_ASSERT_EQUAL(EvseState::Disabled, state_disabled); + TEST_ASSERT_EQUAL(EvseState::None, state_none); +} + +void test_evse_state_from_string_active(void) { + EvseState state; + + // Test various forms of "active" + TEST_ASSERT_TRUE(state.fromString("active")); + TEST_ASSERT_EQUAL(EvseState::Active, state); + + TEST_ASSERT_TRUE(state.fromString("a")); + TEST_ASSERT_EQUAL(EvseState::Active, state); + + TEST_ASSERT_TRUE(state.fromString("activate")); + TEST_ASSERT_EQUAL(EvseState::Active, state); +} + +void test_evse_state_from_string_disabled(void) { + EvseState state; + + // Test various forms of "disabled" + TEST_ASSERT_TRUE(state.fromString("disabled")); + TEST_ASSERT_EQUAL(EvseState::Disabled, state); + + TEST_ASSERT_TRUE(state.fromString("d")); + TEST_ASSERT_EQUAL(EvseState::Disabled, state); + + TEST_ASSERT_TRUE(state.fromString("disable")); + TEST_ASSERT_EQUAL(EvseState::Disabled, state); +} + +void test_evse_state_from_string_invalid(void) { + EvseState state; + + // Test invalid strings + TEST_ASSERT_FALSE(state.fromString("invalid")); + TEST_ASSERT_FALSE(state.fromString("none")); + TEST_ASSERT_FALSE(state.fromString("")); + TEST_ASSERT_FALSE(state.fromString("x")); + TEST_ASSERT_FALSE(state.fromString("123")); +} + +void test_evse_state_from_string_case_sensitivity(void) { + EvseState state; + + // Test that it only checks first character and is case sensitive + TEST_ASSERT_TRUE(state.fromString("active")); + TEST_ASSERT_FALSE(state.fromString("Active")); // Capital A should fail + TEST_ASSERT_FALSE(state.fromString("DISABLED")); // Capital D should fail +} + +void test_evse_state_to_string_active(void) { + EvseState state(EvseState::Active); + const char* str = state.toString(); + TEST_ASSERT_EQUAL_STRING("active", str); +} + +void test_evse_state_to_string_disabled(void) { + EvseState state(EvseState::Disabled); + const char* str = state.toString(); + TEST_ASSERT_EQUAL_STRING("disabled", str); +} + +void test_evse_state_to_string_none(void) { + EvseState state(EvseState::None); + const char* str = state.toString(); + TEST_ASSERT_EQUAL_STRING("none", str); +} + +void test_evse_state_to_string_unknown(void) { + EvseState state; + // Force an invalid state value + state = (EvseState::Value)99; + const char* str = state.toString(); + TEST_ASSERT_EQUAL_STRING("unknown", str); +} + +void test_evse_state_assignment_operator(void) { + EvseState state; + + // Test assignment with Value enum + state = EvseState::Active; + TEST_ASSERT_EQUAL(EvseState::Active, state); + + state = EvseState::Disabled; + TEST_ASSERT_EQUAL(EvseState::Disabled, state); + + state = EvseState::None; + TEST_ASSERT_EQUAL(EvseState::None, state); +} + +void test_evse_state_conversion_operator(void) { + EvseState state_active(EvseState::Active); + EvseState state_disabled(EvseState::Disabled); + EvseState state_none(EvseState::None); + + // Test implicit conversion to Value + EvseState::Value val_active = state_active; + EvseState::Value val_disabled = state_disabled; + EvseState::Value val_none = state_none; + + TEST_ASSERT_EQUAL(EvseState::Active, val_active); + TEST_ASSERT_EQUAL(EvseState::Disabled, val_disabled); + TEST_ASSERT_EQUAL(EvseState::None, val_none); +} + +void test_evse_state_comparison(void) { + EvseState state1(EvseState::Active); + EvseState state2(EvseState::Active); + EvseState state3(EvseState::Disabled); + + // Test equality comparison + TEST_ASSERT_TRUE(state1 == EvseState::Active); + TEST_ASSERT_TRUE(state1 == state2); + TEST_ASSERT_FALSE(state1 == state3); + TEST_ASSERT_FALSE(state1 == EvseState::Disabled); +} + +void test_evse_state_switch_statement(void) { + EvseState state(EvseState::Active); + + int result = 0; + switch(state) { + case EvseState::Active: + result = 1; + break; + case EvseState::Disabled: + result = 2; + break; + case EvseState::None: + result = 3; + break; + } + + TEST_ASSERT_EQUAL(1, result); +} + +void test_evse_state_roundtrip_conversion(void) { + // Test that fromString and toString are consistent + EvseState state; + + // Active roundtrip + TEST_ASSERT_TRUE(state.fromString("active")); + TEST_ASSERT_EQUAL_STRING("active", state.toString()); + + // Disabled roundtrip + TEST_ASSERT_TRUE(state.fromString("disabled")); + TEST_ASSERT_EQUAL_STRING("disabled", state.toString()); +} + +void test_evse_state_copy_constructor(void) { + EvseState original(EvseState::Active); + EvseState copy(original); + + TEST_ASSERT_EQUAL(EvseState::Active, copy); + TEST_ASSERT_EQUAL(original, copy); +} + +void run_evse_state_tests(void) { + RUN_TEST(test_evse_state_constructor_default); + RUN_TEST(test_evse_state_constructor_with_value); + RUN_TEST(test_evse_state_from_string_active); + RUN_TEST(test_evse_state_from_string_disabled); + RUN_TEST(test_evse_state_from_string_invalid); + RUN_TEST(test_evse_state_from_string_case_sensitivity); + RUN_TEST(test_evse_state_to_string_active); + RUN_TEST(test_evse_state_to_string_disabled); + RUN_TEST(test_evse_state_to_string_none); + RUN_TEST(test_evse_state_to_string_unknown); + RUN_TEST(test_evse_state_assignment_operator); + RUN_TEST(test_evse_state_conversion_operator); + RUN_TEST(test_evse_state_comparison); + RUN_TEST(test_evse_state_switch_statement); + RUN_TEST(test_evse_state_roundtrip_conversion); + RUN_TEST(test_evse_state_copy_constructor); +} diff --git a/test_embedded/test_input_filter.cpp b/test_embedded/test_input_filter.cpp new file mode 100644 index 00000000..ca7f5eff --- /dev/null +++ b/test_embedded/test_input_filter.cpp @@ -0,0 +1,197 @@ +#include +#include +#include +#include "test_utils.h" + +// Include the class under test +#include "input_filter.h" + + + +void test_input_filter_constructor(void) { + InputFilter filter; + // Constructor should initialize properly - we can only test behavior through public interface + TEST_PASS(); +} + +void test_filter_first_call(void) { + InputFilter filter; + set_mock_millis(1000); + + // First call should work and return some filtered value + double result = filter.filter(10.0, 5.0, 20); + + // Should return some value between input and previous filtered + TEST_ASSERT_NOT_EQUAL(0.0, result); + TEST_ASSERT_GREATER_OR_EQUAL(5.0, result); + TEST_ASSERT_LESS_OR_EQUAL(10.0, result); +} + +void test_filter_subsequent_calls(void) { + InputFilter filter; + + // Set initial time + set_mock_millis(1000); + double result1 = filter.filter(10.0, 5.0, 20); + + // Advance time and filter again + set_mock_millis(2000); + double result2 = filter.filter(15.0, result1, 20); + + // Result should be different and move towards new input + TEST_ASSERT_NOT_EQUAL(result1, result2); + TEST_ASSERT_GREATER_THAN(result1, result2); // Should move towards 15.0 +} + +void test_filter_no_time_elapsed(void) { + InputFilter filter; + + // Set initial time + set_mock_millis(1000); + double result1 = filter.filter(10.0, 5.0, 20); + + // Same time - delta = 0 + set_mock_millis(1000); + double result2 = filter.filter(15.0, result1, 20); + + // With delta = 0, should get minimal change + double diff = abs(result2 - result1); + TEST_ASSERT_LESS_THAN(1.0, diff); // Should be very small change +} + +void test_filter_zero_tau(void) { + InputFilter filter; + set_mock_millis(1000); + + // When tau is 0, should pass input through directly + double result = filter.filter(100.0, 50.0, 0); + TEST_ASSERT_EQUAL_DOUBLE(100.0, result); +} + +void test_filter_step_response(void) { + InputFilter filter; + double filtered = 0.0; + double input = 10.0; + uint32_t tau = 5; + + // Simulate step response + set_mock_millis(0); + + for (int i = 1; i <= 20; i++) { + set_mock_millis(i * 1000); + filtered = filter.filter(input, filtered, tau); + } + + // After many time constants, should approach input value + TEST_ASSERT_DOUBLE_WITHIN(1.0, input, filtered); +} + +void test_filter_stability(void) { + InputFilter filter; + double filtered = 5.0; + double input = 5.0; // Same as initial filtered value + uint32_t tau = 10; + + set_mock_millis(0); + + // Multiple calls with same input + for (int i = 1; i <= 5; i++) { + set_mock_millis(i * 1000); + filtered = filter.filter(input, filtered, tau); + } + + // Should remain stable at input value + TEST_ASSERT_DOUBLE_WITHIN(0.1, input, filtered); +} + +void test_filter_different_tau_convergence(void) { + InputFilter filter1, filter2; + double filtered1 = 0.0, filtered2 = 0.0; + double input = 10.0; + + set_mock_millis(0); + + // Run both filters for same time period but different tau + for (int i = 1; i <= 10; i++) { + set_mock_millis(i * 1000); + filtered1 = filter1.filter(input, filtered1, 5); // Fast filter + filtered2 = filter2.filter(input, filtered2, 20); // Slow filter + } + + // Fast filter should be closer to input value + double diff1 = abs(input - filtered1); + double diff2 = abs(input - filtered2); + TEST_ASSERT_LESS_THAN(diff2, diff1); +} + +void test_filter_minimum_tau_behavior(void) { + InputFilter filter; + double filtered = 0.0; + double input = 10.0; + + set_mock_millis(0); + + // Test with tau less than minimum (should be clamped to minimum) + set_mock_millis(1000); + double result1 = filter.filter(input, filtered, 5); // Less than INPUT_FILTER_MIN_TAU + + set_mock_millis(2000); + double result2 = filter.filter(input, result1, INPUT_FILTER_MIN_TAU); + + // Both should behave similarly since tau should be clamped + // We can't test exact equality but can test that both converge + TEST_ASSERT_GREATER_THAN(0.0, result1); + TEST_ASSERT_GREATER_THAN(0.0, result2); +} + +void test_filter_large_time_gaps(void) { + InputFilter filter; + double filtered = 0.0; + double input = 10.0; + uint32_t tau = 5; + + // Start at time 1000 + set_mock_millis(1000); + double result1 = filter.filter(input, filtered, tau); + + // Large time gap (100 seconds) + set_mock_millis(101000); + double result2 = filter.filter(input, result1, tau); + + // After a very large time gap, should be very close to input + TEST_ASSERT_DOUBLE_WITHIN(0.1, input, result2); +} + +void test_filter_mathematical_properties(void) { + InputFilter filter; + double filtered = 5.0; + + set_mock_millis(1000); + + // Test that filter output is between input and previous filtered value + double result = filter.filter(10.0, filtered, 10); + + TEST_ASSERT_GREATER_OR_EQUAL(5.0, result); + TEST_ASSERT_LESS_OR_EQUAL(10.0, result); + + // Test with input less than filtered + set_mock_millis(2000); + result = filter.filter(3.0, result, 10); + + TEST_ASSERT_GREATER_OR_EQUAL(3.0, result); + TEST_ASSERT_LESS_OR_EQUAL(10.0, result); +} + +void run_input_filter_tests(void) { + RUN_TEST(test_input_filter_constructor); + RUN_TEST(test_filter_first_call); + RUN_TEST(test_filter_subsequent_calls); + RUN_TEST(test_filter_no_time_elapsed); + RUN_TEST(test_filter_zero_tau); + RUN_TEST(test_filter_step_response); + RUN_TEST(test_filter_stability); + RUN_TEST(test_filter_different_tau_convergence); + RUN_TEST(test_filter_minimum_tau_behavior); + RUN_TEST(test_filter_large_time_gaps); + RUN_TEST(test_filter_mathematical_properties); +} \ No newline at end of file diff --git a/test_embedded/test_integration.cpp b/test_embedded/test_integration.cpp new file mode 100644 index 00000000..9021894d --- /dev/null +++ b/test_embedded/test_integration.cpp @@ -0,0 +1,269 @@ +#include +#include +#include +#include "test_utils.h" + +// Include components for integration testing +#include "input_filter.h" +#include "evse_state.h" +#include "limit.h" +#include "app_config.h" +#include "energy_meter.h" + + + +// ============================================== +// Integration Tests +// ============================================== + +void test_input_filter_with_config_flags(void) { + // Test that input filter works correctly when divert is enabled via config + flags = CONFIG_SERVICE_DIVERT; + TEST_ASSERT_TRUE(config_divert_enabled()); + + InputFilter filter; + double filtered = 0.0; + double input = 10.0; + uint32_t tau = 5; + + // Simulate filtering over time + for (int i = 1; i <= 5; i++) { + set_mock_millis(i * 1000); + filtered = filter.filter(input, filtered, tau); + } + + // Should converge towards input when divert is enabled + TEST_ASSERT_GREATER_THAN(8.0, filtered); +} + +void test_limit_properties_with_evse_state_integration(void) { + LimitProperties props; + EvseState state; + + // Test setting up time limit when EVSE is active + TEST_ASSERT_TRUE(state.fromString("active")); + TEST_ASSERT_EQUAL(EvseState::Active, state); + + props.setType(LimitType::Time); + props.setValue(3600); // 1 hour + props.setAutoRelease(true); + + // Verify limit is properly configured + TEST_ASSERT_EQUAL(LimitType::Time, props.getType()); + TEST_ASSERT_EQUAL(3600, props.getValue()); + TEST_ASSERT_TRUE(props.getAutoRelease()); + + // Test JSON serialization/deserialization + DynamicJsonDocument doc(512); + JsonObject obj = doc.to(); + props.serialize(obj); + + LimitProperties props2; + TEST_ASSERT_TRUE(props2.deserialize(obj)); + + TEST_ASSERT_EQUAL(props.getType(), props2.getType()); + TEST_ASSERT_EQUAL(props.getValue(), props2.getValue()); + TEST_ASSERT_EQUAL(props.getAutoRelease(), props2.getAutoRelease()); +} + +void test_energy_meter_with_configuration_integration(void) { + // Enable energy meter via configuration + flags = CONFIG_SERVICE_EMONCMS; + TEST_ASSERT_TRUE(config_emoncms_enabled()); + + EnergyMeterData data; + + // Simulate charging session + data.session = 25.5; // 25.5 kWh session + data.total = 1000.0; // 1000 kWh total + data.switches = 50; // 50 relay switches + data.elapsed = 3600; // 1 hour elapsed + + // Test serialization + StaticJsonDocument doc; + data.serialize(doc); + + // Verify data is preserved + TEST_ASSERT_EQUAL_DOUBLE(25.5, doc["session"]); + TEST_ASSERT_EQUAL_DOUBLE(1000.0, doc["total"]); + TEST_ASSERT_EQUAL(50, doc["switches"]); + TEST_ASSERT_EQUAL_DOUBLE(3600, doc["elapsed"]); +} + +void test_multiple_services_enabled(void) { + // Enable multiple services + flags = CONFIG_SERVICE_MQTT | CONFIG_SERVICE_DIVERT | CONFIG_RFID; + + TEST_ASSERT_TRUE(config_mqtt_enabled()); + TEST_ASSERT_TRUE(config_divert_enabled()); + TEST_ASSERT_TRUE(config_rfid_enabled()); + TEST_ASSERT_FALSE(config_emoncms_enabled()); + TEST_ASSERT_FALSE(config_ocpp_enabled()); + + // Test that limit system works with these services + LimitProperties props; + props.setType(LimitType::Energy); + props.setValue(50); // 50 kWh limit + + // Serialize to simulate saving configuration + DynamicJsonDocument doc(512); + JsonObject obj = doc.to(); + props.serialize(obj); + + TEST_ASSERT_EQUAL_STRING("energy", obj["type"]); + TEST_ASSERT_EQUAL(50, obj["value"]); +} + +void test_evse_state_transitions_with_limits(void) { + EvseState state; + LimitProperties limit_props; + + // Start in disabled state + TEST_ASSERT_TRUE(state.fromString("disabled")); + TEST_ASSERT_EQUAL(EvseState::Disabled, state); + + // Set up energy limit + limit_props.setType(LimitType::Energy); + limit_props.setValue(100); // 100 kWh + limit_props.setAutoRelease(true); + + // Activate EVSE + TEST_ASSERT_TRUE(state.fromString("active")); + TEST_ASSERT_EQUAL(EvseState::Active, state); + + // Verify limit is still valid + TEST_ASSERT_EQUAL(LimitType::Energy, limit_props.getType()); + TEST_ASSERT_EQUAL(100, limit_props.getValue()); + TEST_ASSERT_TRUE(limit_props.getAutoRelease()); + + // Test state string conversion + TEST_ASSERT_EQUAL_STRING("active", state.toString()); +} + +void test_filter_with_energy_meter_data(void) { + InputFilter power_filter, current_filter; + EnergyMeterData meter_data; + + // Simulate varying power readings that need filtering + double power_readings[] = {1000, 1200, 950, 1100, 1050, 1000}; + double current_readings[] = {4.5, 5.0, 4.2, 4.8, 4.6, 4.4}; + int num_readings = sizeof(power_readings) / sizeof(power_readings[0]); + + double filtered_power = 0.0; + double filtered_current = 0.0; + uint32_t tau = 10; // 10 second time constant + + // Process readings + for (int i = 0; i < num_readings; i++) { + set_mock_millis((i + 1) * 2000); // 2 second intervals + + filtered_power = power_filter.filter(power_readings[i], filtered_power, tau); + filtered_current = current_filter.filter(current_readings[i], filtered_current, tau); + + // Update meter data (simplified) + meter_data.session += power_readings[i] * 2.0 / 3600.0; // Convert to kWh + meter_data.elapsed += 2.0; // 2 seconds per reading + } + + // Verify filtering worked + TEST_ASSERT_GREATER_THAN(900.0, filtered_power); + TEST_ASSERT_LESS_THAN(1300.0, filtered_power); + + TEST_ASSERT_GREATER_THAN(4.0, filtered_current); + TEST_ASSERT_LESS_THAN(5.5, filtered_current); + + // Verify meter accumulated data + TEST_ASSERT_GREATER_THAN(0.0, meter_data.session); + TEST_ASSERT_EQUAL_DOUBLE(12.0, meter_data.elapsed); +} + +void test_configuration_persistence_simulation(void) { + // Simulate saving and loading configuration + flags = CONFIG_SERVICE_MQTT | CONFIG_SERVICE_DIVERT | (3 << 10); // Charge mode 3 + + // Create limit configuration + LimitProperties limit_config; + limit_config.setType(LimitType::Time); + limit_config.setValue(7200); // 2 hours + limit_config.setAutoRelease(false); + + // Create energy meter data + EnergyMeterData meter_data; + meter_data.total = 5000.0; + meter_data.switches = 200; + meter_data.imported = true; + + // Serialize all data (simulating save to flash) + DynamicJsonDocument config_doc(1024); + JsonObject config_obj = config_doc.to(); + config_obj["flags"] = flags; + + JsonObject limit_obj = config_obj.createNestedObject("limit"); + limit_config.serialize(limit_obj); + + StaticJsonDocument meter_doc; + meter_data.serialize(meter_doc); + config_obj["energy_meter"] = meter_doc.as(); + + // Simulate restart - clear everything + flags = 0; + LimitProperties restored_limit; + EnergyMeterData restored_meter; + + // Restore from "saved" data + flags = config_obj["flags"]; + restored_limit.deserialize(limit_obj); + + JsonObject saved_meter = config_obj["energy_meter"]; + StaticJsonDocument meter_restore_doc; + meter_restore_doc.set(saved_meter); + restored_meter.deserialize(meter_restore_doc); + + // Verify restoration + TEST_ASSERT_TRUE(config_mqtt_enabled()); + TEST_ASSERT_TRUE(config_divert_enabled()); + TEST_ASSERT_EQUAL(3, config_charge_mode()); + + TEST_ASSERT_EQUAL(LimitType::Time, restored_limit.getType()); + TEST_ASSERT_EQUAL(7200, restored_limit.getValue()); + TEST_ASSERT_FALSE(restored_limit.getAutoRelease()); + + TEST_ASSERT_EQUAL_DOUBLE(5000.0, restored_meter.total); + TEST_ASSERT_EQUAL(200, restored_meter.switches); + TEST_ASSERT_TRUE(restored_meter.imported); +} + +void test_error_handling_integration(void) { + // Test graceful handling of invalid configurations + LimitProperties props; + + // Try to deserialize invalid JSON + DynamicJsonDocument doc(512); + JsonObject obj = doc.to(); + obj["type"] = "invalid_type"; + obj["value"] = -1; // Invalid negative value + + // Should handle gracefully + bool result = props.deserialize(obj); + TEST_ASSERT_FALSE(result); // Should fail validation + + // Test EvseState with invalid input + EvseState state; + TEST_ASSERT_FALSE(state.fromString("invalid_state")); + + // Test InputFilter with edge cases + InputFilter filter; + double filtered = filter.filter(1000.0, 0.0, 0); // tau = 0 + TEST_ASSERT_EQUAL_DOUBLE(1000.0, filtered); // Should pass through +} + +void run_integration_tests(void) { + RUN_TEST(test_input_filter_with_config_flags); + RUN_TEST(test_limit_properties_with_evse_state_integration); + RUN_TEST(test_energy_meter_with_configuration_integration); + RUN_TEST(test_multiple_services_enabled); + RUN_TEST(test_evse_state_transitions_with_limits); + RUN_TEST(test_filter_with_energy_meter_data); + RUN_TEST(test_configuration_persistence_simulation); + RUN_TEST(test_error_handling_integration); +} diff --git a/test_embedded/test_limit.cpp b/test_embedded/test_limit.cpp new file mode 100644 index 00000000..5e14c6ac --- /dev/null +++ b/test_embedded/test_limit.cpp @@ -0,0 +1,313 @@ +#include +#include +#include +#include "test_utils.h" + +// Include the classes under test +#include "limit.h" + + + +// ============================================== +// LimitType Tests +// ============================================== + +void test_limit_type_constructor_default(void) { + LimitType type; + // Default constructor creates type with uninitialized value + TEST_PASS(); +} + +void test_limit_type_constructor_with_value(void) { + LimitType type_none(LimitType::None); + LimitType type_time(LimitType::Time); + LimitType type_energy(LimitType::Energy); + LimitType type_soc(LimitType::Soc); + LimitType type_range(LimitType::Range); + + TEST_ASSERT_EQUAL(LimitType::None, type_none); + TEST_ASSERT_EQUAL(LimitType::Time, type_time); + TEST_ASSERT_EQUAL(LimitType::Energy, type_energy); + TEST_ASSERT_EQUAL(LimitType::Soc, type_soc); + TEST_ASSERT_EQUAL(LimitType::Range, type_range); +} + +void test_limit_type_from_string_valid(void) { + LimitType type; + + // Test valid strings (checks first character) + TEST_ASSERT_EQUAL(LimitType::None, type.fromString("none")); + TEST_ASSERT_EQUAL(LimitType::None, type); + + TEST_ASSERT_EQUAL(LimitType::Time, type.fromString("time")); + TEST_ASSERT_EQUAL(LimitType::Time, type); + + TEST_ASSERT_EQUAL(LimitType::Energy, type.fromString("energy")); + TEST_ASSERT_EQUAL(LimitType::Energy, type); + + TEST_ASSERT_EQUAL(LimitType::Soc, type.fromString("soc")); + TEST_ASSERT_EQUAL(LimitType::Soc, type); + + TEST_ASSERT_EQUAL(LimitType::Range, type.fromString("range")); + TEST_ASSERT_EQUAL(LimitType::Range, type); +} + +void test_limit_type_from_string_first_char_only(void) { + LimitType type; + + // Test that only first character matters + TEST_ASSERT_EQUAL(LimitType::None, type.fromString("n")); + TEST_ASSERT_EQUAL(LimitType::Time, type.fromString("t")); + TEST_ASSERT_EQUAL(LimitType::Energy, type.fromString("e")); + TEST_ASSERT_EQUAL(LimitType::Soc, type.fromString("s")); + TEST_ASSERT_EQUAL(LimitType::Range, type.fromString("r")); + + // Test with longer strings starting with correct character + TEST_ASSERT_EQUAL(LimitType::Time, type.fromString("timer")); + TEST_ASSERT_EQUAL(LimitType::Energy, type.fromString("electrical")); + TEST_ASSERT_EQUAL(LimitType::Soc, type.fromString("state_of_charge")); + TEST_ASSERT_EQUAL(LimitType::Range, type.fromString("radius")); +} + +void test_limit_type_from_string_invalid(void) { + LimitType type; + + // Set initial value to ensure it doesn't change + type = LimitType::Time; + + // Test invalid strings (should not change the value) + type.fromString("invalid"); + // Note: The current implementation doesn't handle invalid cases properly + // It would leave the value unchanged, but this behavior isn't guaranteed +} + +void test_limit_type_to_string(void) { + LimitType type_none(LimitType::None); + LimitType type_time(LimitType::Time); + LimitType type_energy(LimitType::Energy); + LimitType type_soc(LimitType::Soc); + LimitType type_range(LimitType::Range); + + TEST_ASSERT_EQUAL_STRING("none", type_none.toString()); + TEST_ASSERT_EQUAL_STRING("time", type_time.toString()); + TEST_ASSERT_EQUAL_STRING("energy", type_energy.toString()); + TEST_ASSERT_EQUAL_STRING("soc", type_soc.toString()); + TEST_ASSERT_EQUAL_STRING("range", type_range.toString()); +} + +void test_limit_type_to_string_invalid(void) { + LimitType type; + // Force an invalid value + type = (LimitType::Value)99; + + // Should return "none" for invalid values + TEST_ASSERT_EQUAL_STRING("none", type.toString()); +} + +void test_limit_type_assignment_operator(void) { + LimitType type; + + // Test assignment and return value + LimitType result = (type = LimitType::Energy); + TEST_ASSERT_EQUAL(LimitType::Energy, type); + TEST_ASSERT_EQUAL(LimitType::Energy, result); +} + +void test_limit_type_roundtrip_conversion(void) { + LimitType type; + + // Test that fromString and toString are consistent + type.fromString("time"); + TEST_ASSERT_EQUAL_STRING("time", type.toString()); + + type.fromString("energy"); + TEST_ASSERT_EQUAL_STRING("energy", type.toString()); + + type.fromString("soc"); + TEST_ASSERT_EQUAL_STRING("soc", type.toString()); + + type.fromString("range"); + TEST_ASSERT_EQUAL_STRING("range", type.toString()); + + type.fromString("none"); + TEST_ASSERT_EQUAL_STRING("none", type.toString()); +} + +// ============================================== +// LimitProperties Tests +// ============================================== + +void test_limit_properties_constructor(void) { + LimitProperties props; + + // Check default values + TEST_ASSERT_EQUAL(LimitType::None, props.getType()); + TEST_ASSERT_EQUAL(0, props.getValue()); + TEST_ASSERT_TRUE(props.getAutoRelease()); +} + +void test_limit_properties_init(void) { + LimitProperties props; + + // Modify values + props.setType(LimitType::Time); + props.setValue(100); + props.setAutoRelease(false); + + // Reset to defaults + props.init(); + + // Check values are reset + TEST_ASSERT_EQUAL(LimitType::None, props.getType()); + TEST_ASSERT_EQUAL(0, props.getValue()); + TEST_ASSERT_TRUE(props.getAutoRelease()); +} + +void test_limit_properties_setters_getters(void) { + LimitProperties props; + + // Test type + TEST_ASSERT_TRUE(props.setType(LimitType::Energy)); + TEST_ASSERT_EQUAL(LimitType::Energy, props.getType()); + + // Test value + TEST_ASSERT_TRUE(props.setValue(5000)); + TEST_ASSERT_EQUAL(5000, props.getValue()); + + // Test auto release + TEST_ASSERT_TRUE(props.setAutoRelease(false)); + TEST_ASSERT_FALSE(props.getAutoRelease()); + + TEST_ASSERT_TRUE(props.setAutoRelease(true)); + TEST_ASSERT_TRUE(props.getAutoRelease()); +} + +void test_limit_properties_serialize(void) { + LimitProperties props; + + // Set test values + props.setType(LimitType::Time); + props.setValue(3600); + props.setAutoRelease(false); + + // Create JSON document and serialize + DynamicJsonDocument doc(512); + JsonObject obj = doc.to(); + + TEST_ASSERT_TRUE(props.serialize(obj)); + + // Check serialized values + TEST_ASSERT_EQUAL_STRING("time", obj["type"]); + TEST_ASSERT_EQUAL(3600, obj["value"]); + TEST_ASSERT_FALSE(obj["auto_release"]); +} + +void test_limit_properties_deserialize_complete(void) { + LimitProperties props; + + // Create JSON with all fields + DynamicJsonDocument doc(512); + JsonObject obj = doc.to(); + obj["type"] = "energy"; + obj["value"] = 5000; + obj["auto_release"] = false; + + // Deserialize should succeed (type > 0 && value > 0) + TEST_ASSERT_TRUE(props.deserialize(obj)); + + // Check deserialized values + TEST_ASSERT_EQUAL(LimitType::Energy, props.getType()); + TEST_ASSERT_EQUAL(5000, props.getValue()); + TEST_ASSERT_FALSE(props.getAutoRelease()); +} + +void test_limit_properties_deserialize_partial(void) { + LimitProperties props; + + // Set initial values + props.setType(LimitType::Time); + props.setValue(1000); + props.setAutoRelease(true); + + // Create JSON with only some fields + DynamicJsonDocument doc(512); + JsonObject obj = doc.to(); + obj["value"] = 2000; + + // Should fail because type is not in JSON and won't be > 0 + TEST_ASSERT_FALSE(props.deserialize(obj)); + + // But value should still be updated + TEST_ASSERT_EQUAL(2000, props.getValue()); +} + +void test_limit_properties_deserialize_missing_fields(void) { + LimitProperties props; + + // Empty JSON object + DynamicJsonDocument doc(512); + JsonObject obj = doc.to(); + + // Should fail validation (type and value not > 0) + TEST_ASSERT_FALSE(props.deserialize(obj)); +} + +void test_limit_properties_deserialize_zero_values(void) { + LimitProperties props; + + // JSON with zero values + DynamicJsonDocument doc(512); + JsonObject obj = doc.to(); + obj["type"] = "none"; // LimitType::None = 0 + obj["value"] = 0; + obj["auto_release"] = true; + + // Should fail validation (type == 0 || value == 0) + TEST_ASSERT_FALSE(props.deserialize(obj)); +} + +void test_limit_properties_roundtrip_serialization(void) { + LimitProperties props1, props2; + + // Set values in first object + props1.setType(LimitType::Soc); + props1.setValue(80); + props1.setAutoRelease(false); + + // Serialize + DynamicJsonDocument doc(512); + JsonObject obj = doc.to(); + props1.serialize(obj); + + // Deserialize into second object + props2.deserialize(obj); + + // Check values match + TEST_ASSERT_EQUAL(props1.getType(), props2.getType()); + TEST_ASSERT_EQUAL(props1.getValue(), props2.getValue()); + TEST_ASSERT_EQUAL(props1.getAutoRelease(), props2.getAutoRelease()); +} + +void run_limit_tests(void) { + // LimitType tests + RUN_TEST(test_limit_type_constructor_default); + RUN_TEST(test_limit_type_constructor_with_value); + RUN_TEST(test_limit_type_from_string_valid); + RUN_TEST(test_limit_type_from_string_first_char_only); + RUN_TEST(test_limit_type_from_string_invalid); + RUN_TEST(test_limit_type_to_string); + RUN_TEST(test_limit_type_to_string_invalid); + RUN_TEST(test_limit_type_assignment_operator); + RUN_TEST(test_limit_type_roundtrip_conversion); + + // LimitProperties tests + RUN_TEST(test_limit_properties_constructor); + RUN_TEST(test_limit_properties_init); + RUN_TEST(test_limit_properties_setters_getters); + RUN_TEST(test_limit_properties_serialize); + RUN_TEST(test_limit_properties_deserialize_complete); + RUN_TEST(test_limit_properties_deserialize_partial); + RUN_TEST(test_limit_properties_deserialize_missing_fields); + RUN_TEST(test_limit_properties_deserialize_zero_values); + RUN_TEST(test_limit_properties_roundtrip_serialization); +} diff --git a/test_embedded/test_native_basic.cpp b/test_embedded/test_native_basic.cpp new file mode 100644 index 00000000..de748de6 --- /dev/null +++ b/test_embedded/test_native_basic.cpp @@ -0,0 +1,54 @@ +#include +#include +#include +#include + +// Basic native tests that don't require Arduino +void test_basic_math() { + TEST_ASSERT_EQUAL(4, 2 + 2); + TEST_ASSERT_EQUAL(10, 5 * 2); +} + +void test_string_operations() { + const char* test_str = "Hello"; + TEST_ASSERT_EQUAL_STRING("Hello", test_str); + TEST_ASSERT_EQUAL(5, strlen(test_str)); +} + +void test_data_structures() { + int array[3] = {1, 2, 3}; + TEST_ASSERT_EQUAL(1, array[0]); + TEST_ASSERT_EQUAL(2, array[1]); + TEST_ASSERT_EQUAL(3, array[2]); +} + +// Test native environment capabilities +void test_native_environment() { + // Test that we can use standard library functions + TEST_ASSERT_TRUE(true); + TEST_ASSERT_FALSE(false); + + // Test memory allocation + void* ptr = malloc(100); + TEST_ASSERT_NOT_NULL(ptr); + free(ptr); +} + +void setUp(void) { + // Set up native test environment +} + +void tearDown(void) { + // Clean up after tests +} + +int main(void) { + UNITY_BEGIN(); + + RUN_TEST(test_basic_math); + RUN_TEST(test_string_operations); + RUN_TEST(test_data_structures); + RUN_TEST(test_native_environment); + + return UNITY_END(); +} \ No newline at end of file diff --git a/test_embedded/test_utils.cpp b/test_embedded/test_utils.cpp new file mode 100644 index 00000000..5a11a511 --- /dev/null +++ b/test_embedded/test_utils.cpp @@ -0,0 +1,32 @@ +#include "test_utils.h" + +#include "test_utils.h" + +// Mock millis implementation +unsigned long mock_millis_value = 0; + +void set_mock_millis(unsigned long value) { + mock_millis_value = value; +} + +void advance_mock_millis(unsigned long increment) { + mock_millis_value += increment; +} + +unsigned long get_mock_millis() { + return mock_millis_value; +} + +// Wrapped millis function (using linker wrap) +extern "C" unsigned long __wrap_millis() { + return mock_millis_value; +} + +// Global Unity setUp and tearDown functions +void setUp(void) { + common_setUp(); +} + +void tearDown(void) { + common_tearDown(); +} \ No newline at end of file diff --git a/test_embedded/test_utils.h b/test_embedded/test_utils.h new file mode 100644 index 00000000..8f129181 --- /dev/null +++ b/test_embedded/test_utils.h @@ -0,0 +1,29 @@ +#ifndef TEST_UTILS_H +#define TEST_UTILS_H + +#include + +// Mock time variable for deterministic testing +extern unsigned long mock_millis_value; + +// Mock time management functions +void set_mock_millis(unsigned long value); +void advance_mock_millis(unsigned long increment); +unsigned long get_mock_millis(); + +// Global Unity setUp and tearDown functions - will be called automatically +void setUp(void); +void tearDown(void); + +// Common setUp and tearDown functions +// These are declared inline to avoid multiple definitions +inline void common_setUp(void) { + // Reset mock time for each test + set_mock_millis(0); +} + +inline void common_tearDown(void) { + // Nothing to clean up +} + +#endif // TEST_UTILS_H \ No newline at end of file