// Tests for the Nano firmware (src/nano), run on the PC: pio test -e native // // The real Arduino, EEPROM and display driver are replaced by the fakes in test/mocks. // Tests push bytes into Serial.in, call loop() and check what the display was asked to do. #include #include "Arduino.h" #include "BROSE9323.h" #include "EEPROM.h" // From src/nano/main.cpp extern BROSE9323 display; extern uint32_t last_refresh; void setup(); void loop(); // Send bytes to the Nano and let it process them static void send(const std::string &data) { for (char c : data) Serial.in.push_back(c); loop(); } static bool replied(const char *text) { return Serial.out.find(text) != std::string::npos; } void setUp() { // Finish any half-received line from the previous test send("\n"); fake_millis += 1; last_refresh = fake_millis; // no full redraw due unless a test asks for one display.calls.clear(); display.direct = false; Serial.out.clear(); } void tearDown() {} // --------------------------------------------------------------------------- // Boot // --------------------------------------------------------------------------- void test_boot_does_not_flip_dots() { EEPROM.erase(); setup(); TEST_ASSERT_EQUAL(1, display.count("begin")); TEST_ASSERT_EQUAL(0, display.count("display")); TEST_ASSERT_EQUAL(0, display.count("fill")); } void test_boot_sends_ready() { setup(); TEST_ASSERT_TRUE(replied("READY\n")); } void test_boot_uses_default_flip_time_with_empty_eeprom() { EEPROM.erase(); setup(); TEST_ASSERT_EQUAL(200, display.last("timing")->a); } // --------------------------------------------------------------------------- // Commands // --------------------------------------------------------------------------- void test_pixel_runs_without_waiting_for_next_command() { send("s0a051\n"); TEST_ASSERT_EQUAL(1, display.count("pixel")); TEST_ASSERT_EQUAL(10, display.last("pixel")->a); TEST_ASSERT_EQUAL(5, display.last("pixel")->b); TEST_ASSERT_EQUAL(1, display.last("pixel")->c); } void test_direct_pixel_flips_immediately_and_resets_direct_mode() { send("S010200\n"); TEST_ASSERT_EQUAL(1, display.count("directPixel")); TEST_ASSERT_FALSE(display.direct); } void test_fill() { send("F1\n"); TEST_ASSERT_EQUAL(1, display.last("fill")->a); send("F0\n"); TEST_ASSERT_EQUAL(0, display.last("fill")->a); } void test_display() { send("D\n"); TEST_ASSERT_EQUAL(1, display.count("display")); TEST_ASSERT_EQUAL(0, display.last("display")->a); // not forced } void test_lowercase_and_uppercase_commands() { send("f1\nd\n"); TEST_ASSERT_EQUAL(1, display.count("fill")); TEST_ASSERT_EQUAL(1, display.count("display")); } void test_command_split_over_several_reads_with_crlf() { send("s0"); send("1"); send("021\r\n"); TEST_ASSERT_EQUAL(1, display.count("pixel")); TEST_ASSERT_EQUAL(1, display.last("pixel")->a); TEST_ASSERT_EQUAL(2, display.last("pixel")->b); } void test_print_text_with_newline_escape() { send("P0102115\\nab\n"); TEST_ASSERT_EQUAL(1, display.last("cursor")->a); TEST_ASSERT_EQUAL(2, display.last("cursor")->b); TEST_ASSERT_EQUAL(1, display.last("textColor")->a); TEST_ASSERT_EQUAL(0, display.last("textColor")->b); TEST_ASSERT_EQUAL(1, display.last("textSize")->a); TEST_ASSERT_EQUAL(3, display.count("write")); // '5', 'a', 'b' and no garbage TEST_ASSERT_EQUAL(1, display.count("newline")); } void test_print_buffer() { send("R\n"); TEST_ASSERT_EQUAL(1, display.count("printBuffer")); } // --------------------------------------------------------------------------- // Bitmaps // --------------------------------------------------------------------------- void test_bitmap_pixels() { // 8x2 at (4,3), color 1, rows F0 and 81 send("B040308021F081\n"); const int expected[][2] = {{4, 3}, {5, 3}, {6, 3}, {7, 3}, {4, 4}, {11, 4}}; TEST_ASSERT_EQUAL(6, display.count("pixel")); int i = 0; for (const DisplayCall &call : display.calls) { if (call.what != "pixel") continue; TEST_ASSERT_EQUAL(expected[i][0], call.a); TEST_ASSERT_EQUAL(expected[i][1], call.b); TEST_ASSERT_EQUAL(1, call.c); i++; } } void test_full_screen_bitmap_is_streamed() { // 84x19 = 399 hex digits, much longer than the 64 character line buffer send("B000054131" + std::string(399, 'F') + "\n"); TEST_ASSERT_EQUAL(84 * 19, display.count("pixel")); } void test_invalid_bitmap_data_is_not_run_as_commands() { // width 5 is invalid; the data contains the command letters B, D and F send("B0000050100BDFDFDF\nD\n"); TEST_ASSERT_TRUE(replied("ERR")); TEST_ASSERT_EQUAL(0, display.count("fill")); TEST_ASSERT_EQUAL(1, display.count("display")); } void test_incomplete_bitmap_header() { send("B00\nD\n"); TEST_ASSERT_TRUE(replied("ERR")); TEST_ASSERT_EQUAL(1, display.count("display")); } // --------------------------------------------------------------------------- // Recovering from bad input // --------------------------------------------------------------------------- void test_too_long_line_is_rejected() { send(std::string(200, 'x') + "\nF1\n"); TEST_ASSERT_TRUE(replied("Line too long")); TEST_ASSERT_EQUAL(1, display.count("fill")); } void test_truncated_command_is_ignored() { send("s01\nF1\n"); TEST_ASSERT_EQUAL(0, display.count("pixel")); TEST_ASSERT_EQUAL(1, display.count("fill")); } void test_unknown_command_is_ignored() { send("xyz\nF1\n"); TEST_ASSERT_EQUAL(1, display.count("fill")); } // --------------------------------------------------------------------------- // Flip time // --------------------------------------------------------------------------- void test_timing_is_applied_immediately() { send("T225\n"); TEST_ASSERT_EQUAL(225, display.last("timing")->a); } void test_timing_above_255_is_stored_and_restored() { send("T300\n"); TEST_ASSERT_EQUAL(300, display.last("timing")->a); display.calls.clear(); setup(); // reboot TEST_ASSERT_EQUAL(300, display.last("timing")->a); } void test_timing_out_of_range_is_rejected() { send("T5\n"); send("T99999\n"); TEST_ASSERT_EQUAL(0, display.count("timing")); TEST_ASSERT_TRUE(replied("ERR")); } // --------------------------------------------------------------------------- // Periodic full redraw // --------------------------------------------------------------------------- void test_refresh_waits_for_end_of_frame() { last_refresh = fake_millis; fake_millis += 120000; send("s000001\n"); TEST_ASSERT_EQUAL(0, display.count("display")); send("D\n"); TEST_ASSERT_EQUAL(2, display.count("display")); TEST_ASSERT_EQUAL(1, display.calls.back().a); // forced full redraw after the frame } void test_refresh_only_once_per_interval() { last_refresh = fake_millis; fake_millis += 120000; send("D\n"); display.calls.clear(); send("D\n"); TEST_ASSERT_EQUAL(1, display.count("display")); } void test_refresh_when_serial_is_idle() { last_refresh = fake_millis; fake_millis += 120000; loop(); TEST_ASSERT_EQUAL(1, display.count("display")); TEST_ASSERT_EQUAL(1, display.last("display")->a); } void test_no_refresh_storm_after_millis_wraps() { fake_millis = 0xFFFFFFFFu - 1000; last_refresh = fake_millis; fake_millis += 5000; // wraps past 0 for (int i = 0; i < 5; i++) send("D\n"); TEST_ASSERT_EQUAL(5, display.count("display")); fake_millis += 120000; display.calls.clear(); send("D\n"); TEST_ASSERT_EQUAL(2, display.count("display")); } int main() { UNITY_BEGIN(); RUN_TEST(test_boot_does_not_flip_dots); RUN_TEST(test_boot_sends_ready); RUN_TEST(test_boot_uses_default_flip_time_with_empty_eeprom); RUN_TEST(test_pixel_runs_without_waiting_for_next_command); RUN_TEST(test_direct_pixel_flips_immediately_and_resets_direct_mode); RUN_TEST(test_fill); RUN_TEST(test_display); RUN_TEST(test_lowercase_and_uppercase_commands); RUN_TEST(test_command_split_over_several_reads_with_crlf); RUN_TEST(test_print_text_with_newline_escape); RUN_TEST(test_print_buffer); RUN_TEST(test_bitmap_pixels); RUN_TEST(test_full_screen_bitmap_is_streamed); RUN_TEST(test_invalid_bitmap_data_is_not_run_as_commands); RUN_TEST(test_incomplete_bitmap_header); RUN_TEST(test_too_long_line_is_rejected); RUN_TEST(test_truncated_command_is_ignored); RUN_TEST(test_unknown_command_is_ignored); RUN_TEST(test_timing_is_applied_immediately); RUN_TEST(test_timing_above_255_is_stored_and_restored); RUN_TEST(test_timing_out_of_range_is_rejected); RUN_TEST(test_refresh_waits_for_end_of_frame); RUN_TEST(test_refresh_only_once_per_interval); RUN_TEST(test_refresh_when_serial_is_idle); RUN_TEST(test_no_refresh_storm_after_millis_wraps); return UNITY_END(); }