summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-04-13 08:59:34 +0300
committerPaul Buetow <paul@buetow.org>2026-04-13 08:59:34 +0300
commit5628e43ae771cfe3bc057375966621b3bfb48846 (patch)
treee2a3b3b9dde58ce7eefb5835db81fe657cc02069
parenta04d9d3520404066100600cc90cb2274822ad2cb (diff)
q2: add host-side unit test harness
-rw-r--r--AGENTS.md1
-rw-r--r--Justfile4
-rw-r--r--tests/.gitignore1
-rw-r--r--tests/Makefile32
-rw-r--r--tests/test_csv.c70
-rw-r--r--tests/test_helpers.h21
-rw-r--r--tests/test_logic.c160
-rw-r--r--tests/test_main.c40
-rw-r--r--tests/vendor/unity/src/unity.c2637
-rw-r--r--tests/vendor/unity/src/unity.h698
-rw-r--r--tests/vendor/unity/src/unity_internals.h1283
11 files changed, 4947 insertions, 0 deletions
diff --git a/AGENTS.md b/AGENTS.md
index e78bead..359d5ae 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -41,6 +41,7 @@ Available commands:
- `just quick`: quick rebuild + install
- `just screenshot`: screenshot of emulator
- `just sdk-version`: print Pebble SDK/tool version
+- `just test`: run host unit tests (expects `TZ=UTC` for deterministic date/streak results)
Daily loop after reboot:
diff --git a/Justfile b/Justfile
index 53268f5..54d1235 100644
--- a/Justfile
+++ b/Justfile
@@ -67,6 +67,10 @@ rebuild:
quick:
just build && just install
+# Run host unit tests (TZ=UTC for deterministic localtime/streak checks)
+test:
+ make -C tests test
+
# Build with DEBUG=1 compile flag
debug-build:
DEBUG=1 pebble build
diff --git a/tests/.gitignore b/tests/.gitignore
new file mode 100644
index 0000000..ef1c74a
--- /dev/null
+++ b/tests/.gitignore
@@ -0,0 +1 @@
+test_fastforge
diff --git a/tests/Makefile b/tests/Makefile
new file mode 100644
index 0000000..ef3efc6
--- /dev/null
+++ b/tests/Makefile
@@ -0,0 +1,32 @@
+CC ?= gcc
+CFLAGS ?= -std=c11 -Wall -Wextra -Werror -pedantic -g
+
+TZ ?= UTC
+export TZ
+
+ROOT := ..
+UNITY_DIR := vendor/unity/src
+SRC_DIR := $(ROOT)/src/c
+
+TARGET := test_fastforge
+
+SRCS := \
+ $(UNITY_DIR)/unity.c \
+ $(SRC_DIR)/fastforge_logic.c \
+ $(SRC_DIR)/fastforge_csv.c \
+ $(wildcard test_*.c)
+
+INCLUDES := -I. -I$(UNITY_DIR) -I$(SRC_DIR)
+
+.PHONY: all test clean
+
+all: test
+
+test: $(TARGET)
+ TZ=$(TZ) ./$(TARGET)
+
+$(TARGET): $(SRCS)
+ $(CC) $(CFLAGS) $(INCLUDES) $(SRCS) -o $@
+
+clean:
+ rm -f $(TARGET)
diff --git a/tests/test_csv.c b/tests/test_csv.c
new file mode 100644
index 0000000..cbf206b
--- /dev/null
+++ b/tests/test_csv.c
@@ -0,0 +1,70 @@
+#include "vendor/unity/src/unity.h"
+
+#include "../src/c/fastforge_csv.h"
+
+#include <string.h>
+
+void test_csv_append_text_plain_and_escaped_values(void) {
+ char buffer[128];
+ size_t offset;
+
+ buffer[0] = '\0';
+ offset = csv_append_text(buffer, sizeof(buffer), 0, "plain");
+ TEST_ASSERT_EQUAL_size_t(strlen("plain"), offset);
+ TEST_ASSERT_EQUAL_STRING("plain", buffer);
+
+ buffer[0] = '\0';
+ offset = csv_append_text(buffer, sizeof(buffer), 0, "alpha,beta");
+ TEST_ASSERT_EQUAL_STRING("\"alpha,beta\"", buffer);
+ TEST_ASSERT_EQUAL_size_t(strlen("\"alpha,beta\""), offset);
+
+ buffer[0] = '\0';
+ offset = csv_append_text(buffer, sizeof(buffer), 0, "say \"hi\"");
+ TEST_ASSERT_EQUAL_STRING("\"say \"\"hi\"\"\"", buffer);
+ TEST_ASSERT_EQUAL_size_t(strlen("\"say \"\"hi\"\"\""), offset);
+
+ strcpy(buffer, "seed");
+ offset = csv_append_text(buffer, sizeof(buffer), 4, NULL);
+ TEST_ASSERT_EQUAL_size_t(4, offset);
+ TEST_ASSERT_EQUAL_STRING("seed", buffer);
+}
+
+void test_csv_append_int_appends_number_text(void) {
+ char buffer[64];
+ size_t offset;
+
+ buffer[0] = '\0';
+ offset = csv_append_int(buffer, sizeof(buffer), 0, -42);
+ TEST_ASSERT_EQUAL_size_t(strlen("-42"), offset);
+ TEST_ASSERT_EQUAL_STRING("-42", buffer);
+}
+
+void test_format_history_csv_header_and_row_with_escaping(void) {
+ FastEntry entry;
+ char row[256];
+ char header[128];
+
+ memset(&entry, 0, sizeof(entry));
+ entry.start_time = 100;
+ entry.end_time = 200;
+ entry.target_minutes = 960;
+ entry.max_stage_reached = 2;
+ snprintf(entry.note, sizeof(entry.note), "meal, \"keto\"");
+
+ format_history_csv_header(header, sizeof(header));
+ TEST_ASSERT_EQUAL_STRING(
+ "start_time,end_time,target_minutes,note,max_stage_reached",
+ header);
+
+ format_history_csv_row(&entry, row, sizeof(row));
+ TEST_ASSERT_EQUAL_STRING("100,200,960,\"meal, \"\"keto\"\"\",2", row);
+}
+
+void test_format_history_csv_row_null_entry_returns_empty(void) {
+ char row[32];
+ strcpy(row, "not-empty");
+
+ format_history_csv_row(NULL, row, sizeof(row));
+
+ TEST_ASSERT_EQUAL_STRING("", row);
+}
diff --git a/tests/test_helpers.h b/tests/test_helpers.h
new file mode 100644
index 0000000..6065de8
--- /dev/null
+++ b/tests/test_helpers.h
@@ -0,0 +1,21 @@
+#ifndef FASTFORGE_TEST_HELPERS_H
+#define FASTFORGE_TEST_HELPERS_H
+
+#include <time.h>
+
+static inline time_t make_utc_time(int year, int month, int day,
+ int hour, int minute, int second) {
+ struct tm tm_value;
+ tm_value.tm_year = year - 1900;
+ tm_value.tm_mon = month - 1;
+ tm_value.tm_mday = day;
+ tm_value.tm_hour = hour;
+ tm_value.tm_min = minute;
+ tm_value.tm_sec = second;
+ tm_value.tm_wday = 0;
+ tm_value.tm_yday = 0;
+ tm_value.tm_isdst = -1;
+ return mktime(&tm_value);
+}
+
+#endif
diff --git a/tests/test_logic.c b/tests/test_logic.c
new file mode 100644
index 0000000..74978a3
--- /dev/null
+++ b/tests/test_logic.c
@@ -0,0 +1,160 @@
+#include "vendor/unity/src/unity.h"
+
+#include "../src/c/fastforge_logic.h"
+
+#include "test_helpers.h"
+
+#include <string.h>
+
+void test_entry_duration_seconds_handles_invalid_ranges(void) {
+ FastEntry entry = {0};
+
+ TEST_ASSERT_EQUAL_INT64(0, entry_duration_seconds(NULL));
+
+ entry.start_time = 0;
+ entry.end_time = 100;
+ TEST_ASSERT_EQUAL_INT64(0, entry_duration_seconds(&entry));
+
+ entry.start_time = 200;
+ entry.end_time = 200;
+ TEST_ASSERT_EQUAL_INT64(0, entry_duration_seconds(&entry));
+
+ entry.start_time = 200;
+ entry.end_time = 150;
+ TEST_ASSERT_EQUAL_INT64(0, entry_duration_seconds(&entry));
+
+ entry.start_time = 200;
+ entry.end_time = 280;
+ TEST_ASSERT_EQUAL_INT64(80, entry_duration_seconds(&entry));
+}
+
+void test_stage_thresholds_and_labels(void) {
+ TEST_ASSERT_EQUAL_UINT8(0, stage_level_for_elapsed(0));
+ TEST_ASSERT_EQUAL_UINT8(0, stage_level_for_elapsed((12 * 3600) - 1));
+ TEST_ASSERT_EQUAL_UINT8(1, stage_level_for_elapsed(12 * 3600));
+ TEST_ASSERT_EQUAL_UINT8(2, stage_level_for_elapsed(18 * 3600));
+ TEST_ASSERT_EQUAL_UINT8(3, stage_level_for_elapsed(24 * 3600));
+
+ TEST_ASSERT_EQUAL_STRING("GLYCOGEN", stage_text_for_elapsed(11 * 3600));
+ TEST_ASSERT_EQUAL_STRING("FAT BURN", stage_text_for_elapsed(12 * 3600));
+ TEST_ASSERT_EQUAL_STRING("EARLY KETOSIS", stage_text_for_elapsed(18 * 3600));
+ TEST_ASSERT_EQUAL_STRING("DEEP KETOSIS", stage_text_for_elapsed(24 * 3600));
+}
+
+void test_formatters_clamp_negative_and_format_values(void) {
+ char buffer[32];
+
+ format_hhmmss(-1, buffer, sizeof(buffer));
+ TEST_ASSERT_EQUAL_STRING("00:00:00", buffer);
+
+ format_hhmmss(3661, buffer, sizeof(buffer));
+ TEST_ASSERT_EQUAL_STRING("01:01:01", buffer);
+
+ format_duration_hours_minutes(-120, buffer, sizeof(buffer));
+ TEST_ASSERT_EQUAL_STRING("0h 00m", buffer);
+
+ format_duration_hours_minutes(7260, buffer, sizeof(buffer));
+ TEST_ASSERT_EQUAL_STRING("2h 01m", buffer);
+}
+
+void test_running_fast_at_target_logic(void) {
+ FastEntry entry = {0};
+
+ TEST_ASSERT_FALSE(running_fast_is_at_target(NULL, 0));
+
+ entry.start_time = 0;
+ entry.target_minutes = 16 * 60;
+ TEST_ASSERT_FALSE(running_fast_is_at_target(&entry, 100));
+
+ entry.start_time = 100;
+ entry.end_time = 200;
+ TEST_ASSERT_FALSE(running_fast_is_at_target(&entry, 200));
+
+ entry.end_time = 0;
+ entry.target_minutes = 0;
+ TEST_ASSERT_FALSE(running_fast_is_at_target(&entry, 1000));
+
+ entry.target_minutes = 10;
+ TEST_ASSERT_FALSE(running_fast_is_at_target(&entry, 699));
+ TEST_ASSERT_TRUE(running_fast_is_at_target(&entry, 700));
+}
+
+void test_local_day_start_returns_midnight_utc(void) {
+ const time_t ts = make_utc_time(2026, 4, 5, 17, 20, 10);
+ const time_t expected_day_start = make_utc_time(2026, 4, 5, 0, 0, 0);
+
+ TEST_ASSERT_EQUAL_INT64(0, local_day_start(0));
+ TEST_ASSERT_EQUAL_INT64(0, local_day_start(-1));
+ TEST_ASSERT_EQUAL_INT64(expected_day_start, local_day_start(ts));
+}
+
+void test_streak_empty_input_resets_all_fields(void) {
+ StreakData streak;
+ const time_t now = make_utc_time(2026, 4, 10, 9, 0, 0);
+
+ streak.current_streak = 77;
+ streak.longest_streak = 88;
+ streak.last_completed_fast_end = 99;
+
+ fastforge_streak_recompute(NULL, 0, now, &streak);
+
+ TEST_ASSERT_EQUAL_UINT16(0, streak.current_streak);
+ TEST_ASSERT_EQUAL_UINT16(0, streak.longest_streak);
+ TEST_ASSERT_EQUAL_INT64(0, streak.last_completed_fast_end);
+}
+
+void test_streak_single_recent_completion_sets_current_and_longest(void) {
+ FastEntry entries[1];
+ StreakData streak;
+ const time_t now = make_utc_time(2026, 4, 10, 12, 0, 0);
+
+ memset(entries, 0, sizeof(entries));
+ entries[0].start_time = make_utc_time(2026, 4, 9, 8, 0, 0);
+ entries[0].end_time = make_utc_time(2026, 4, 9, 20, 0, 0);
+
+ fastforge_streak_recompute(entries, 1, now, &streak);
+
+ TEST_ASSERT_EQUAL_UINT16(1, streak.current_streak);
+ TEST_ASSERT_EQUAL_UINT16(1, streak.longest_streak);
+ TEST_ASSERT_EQUAL_INT64(entries[0].end_time, streak.last_completed_fast_end);
+}
+
+void test_streak_longest_can_exceed_current_when_sequence_breaks(void) {
+ FastEntry entries[4];
+ StreakData streak;
+ const time_t now = make_utc_time(2026, 4, 4, 23, 0, 0);
+
+ memset(entries, 0, sizeof(entries));
+ entries[0].start_time = make_utc_time(2026, 4, 1, 8, 0, 0);
+ entries[0].end_time = make_utc_time(2026, 4, 1, 20, 0, 0);
+ entries[1].start_time = make_utc_time(2026, 4, 2, 8, 0, 0);
+ entries[1].end_time = make_utc_time(2026, 4, 2, 20, 0, 0);
+ entries[2].start_time = make_utc_time(2026, 4, 4, 8, 0, 0);
+ entries[2].end_time = make_utc_time(2026, 4, 4, 20, 0, 0);
+ entries[3].start_time = make_utc_time(2026, 4, 4, 6, 0, 0);
+ entries[3].end_time = make_utc_time(2026, 4, 4, 7, 0, 0);
+
+ fastforge_streak_recompute(entries, 4, now, &streak);
+
+ TEST_ASSERT_EQUAL_UINT16(1, streak.current_streak);
+ TEST_ASSERT_EQUAL_UINT16(2, streak.longest_streak);
+ TEST_ASSERT_EQUAL_INT64(entries[2].end_time, streak.last_completed_fast_end);
+}
+
+void test_streak_drops_to_zero_after_missing_more_than_one_day(void) {
+ FastEntry entries[2];
+ StreakData streak;
+ const time_t now = make_utc_time(2026, 4, 10, 12, 0, 0);
+
+ memset(entries, 0, sizeof(entries));
+ entries[0].start_time = make_utc_time(2026, 4, 6, 8, 0, 0);
+ entries[0].end_time = make_utc_time(2026, 4, 6, 20, 0, 0);
+ entries[1].start_time = make_utc_time(2026, 4, 7, 8, 0, 0);
+ entries[1].end_time = make_utc_time(2026, 4, 7, 20, 0, 0);
+
+ fastforge_streak_recompute(entries, 2, now, &streak);
+
+ TEST_ASSERT_EQUAL_UINT16(0, streak.current_streak);
+ TEST_ASSERT_EQUAL_UINT16(2, streak.longest_streak);
+ TEST_ASSERT_EQUAL_INT64(entries[1].end_time, streak.last_completed_fast_end);
+}
diff --git a/tests/test_main.c b/tests/test_main.c
new file mode 100644
index 0000000..e16435d
--- /dev/null
+++ b/tests/test_main.c
@@ -0,0 +1,40 @@
+#include "vendor/unity/src/unity.h"
+
+void test_entry_duration_seconds_handles_invalid_ranges(void);
+void test_stage_thresholds_and_labels(void);
+void test_formatters_clamp_negative_and_format_values(void);
+void test_running_fast_at_target_logic(void);
+void test_local_day_start_returns_midnight_utc(void);
+void test_streak_empty_input_resets_all_fields(void);
+void test_streak_single_recent_completion_sets_current_and_longest(void);
+void test_streak_longest_can_exceed_current_when_sequence_breaks(void);
+void test_streak_drops_to_zero_after_missing_more_than_one_day(void);
+
+void test_csv_append_text_plain_and_escaped_values(void);
+void test_csv_append_int_appends_number_text(void);
+void test_format_history_csv_header_and_row_with_escaping(void);
+void test_format_history_csv_row_null_entry_returns_empty(void);
+
+void setUp(void) {}
+void tearDown(void) {}
+
+int main(void) {
+ UNITY_BEGIN();
+
+ RUN_TEST(test_entry_duration_seconds_handles_invalid_ranges);
+ RUN_TEST(test_stage_thresholds_and_labels);
+ RUN_TEST(test_formatters_clamp_negative_and_format_values);
+ RUN_TEST(test_running_fast_at_target_logic);
+ RUN_TEST(test_local_day_start_returns_midnight_utc);
+ RUN_TEST(test_streak_empty_input_resets_all_fields);
+ RUN_TEST(test_streak_single_recent_completion_sets_current_and_longest);
+ RUN_TEST(test_streak_longest_can_exceed_current_when_sequence_breaks);
+ RUN_TEST(test_streak_drops_to_zero_after_missing_more_than_one_day);
+
+ RUN_TEST(test_csv_append_text_plain_and_escaped_values);
+ RUN_TEST(test_csv_append_int_appends_number_text);
+ RUN_TEST(test_format_history_csv_header_and_row_with_escaping);
+ RUN_TEST(test_format_history_csv_row_null_entry_returns_empty);
+
+ return UNITY_END();
+}
diff --git a/tests/vendor/unity/src/unity.c b/tests/vendor/unity/src/unity.c
new file mode 100644
index 0000000..84d6729
--- /dev/null
+++ b/tests/vendor/unity/src/unity.c
@@ -0,0 +1,2637 @@
+/* =========================================================================
+ Unity - A Test Framework for C
+ ThrowTheSwitch.org
+ Copyright (c) 2007-26 Mike Karlesky, Mark VanderVoord, & Greg Williams
+ SPDX-License-Identifier: MIT
+========================================================================= */
+
+#include "unity.h"
+
+#ifndef UNITY_PROGMEM
+#define UNITY_PROGMEM
+#endif
+
+/* If omitted from header, declare overrideable prototypes here so they're ready for use */
+#ifdef UNITY_OMIT_OUTPUT_CHAR_HEADER_DECLARATION
+void UNITY_OUTPUT_CHAR(int);
+#endif
+
+/* Helpful macros for us to use here in Assert functions */
+#define UNITY_FAIL_AND_BAIL do { Unity.CurrentTestFailed = 1; UNITY_OUTPUT_FLUSH(); TEST_ABORT(); } while (0)
+#define UNITY_IGNORE_AND_BAIL do { Unity.CurrentTestIgnored = 1; UNITY_OUTPUT_FLUSH(); TEST_ABORT(); } while (0)
+#define RETURN_IF_FAIL_OR_IGNORE do { if (Unity.CurrentTestFailed || Unity.CurrentTestIgnored) { TEST_ABORT(); } } while (0)
+
+struct UNITY_STORAGE_T Unity;
+
+#ifdef UNITY_OUTPUT_COLOR
+const char UNITY_PROGMEM UnityStrOk[] = "\033[42mOK\033[0m";
+const char UNITY_PROGMEM UnityStrPass[] = "\033[42mPASS\033[0m";
+const char UNITY_PROGMEM UnityStrFail[] = "\033[41mFAIL\033[0m";
+const char UNITY_PROGMEM UnityStrIgnore[] = "\033[43mIGNORE\033[0m";
+#else
+const char UNITY_PROGMEM UnityStrOk[] = "OK";
+const char UNITY_PROGMEM UnityStrPass[] = "PASS";
+const char UNITY_PROGMEM UnityStrFail[] = "FAIL";
+const char UNITY_PROGMEM UnityStrIgnore[] = "IGNORE";
+#endif
+static const char UNITY_PROGMEM UnityStrNull[] = "NULL";
+static const char UNITY_PROGMEM UnityStrSpacer[] = UNITY_FAILURE_DETAIL_SEPARATOR;
+static const char UNITY_PROGMEM UnityStrExpected[] = " Expected ";
+static const char UNITY_PROGMEM UnityStrWas[] = " Was ";
+static const char UNITY_PROGMEM UnityStrGt[] = " to be greater than ";
+static const char UNITY_PROGMEM UnityStrLt[] = " to be less than ";
+static const char UNITY_PROGMEM UnityStrOrEqual[] = "or equal to ";
+static const char UNITY_PROGMEM UnityStrNotEqual[] = " to be not equal to ";
+static const char UNITY_PROGMEM UnityStrElement[] = " Element ";
+static const char UNITY_PROGMEM UnityStrByte[] = " Byte ";
+static const char UNITY_PROGMEM UnityStrMemory[] = " Memory Mismatch.";
+static const char UNITY_PROGMEM UnityStrDelta[] = " Values Not Within Delta ";
+static const char UNITY_PROGMEM UnityStrPointless[] = " You Asked Me To Compare Nothing, Which Was Pointless.";
+static const char UNITY_PROGMEM UnityStrNullPointerForExpected[] = " Expected pointer to be NULL";
+static const char UNITY_PROGMEM UnityStrNullPointerForActual[] = " Actual pointer was NULL";
+#ifndef UNITY_EXCLUDE_FLOAT
+static const char UNITY_PROGMEM UnityStrNot[] = "Not ";
+static const char UNITY_PROGMEM UnityStrInf[] = "Infinity";
+static const char UNITY_PROGMEM UnityStrNegInf[] = "Negative Infinity";
+static const char UNITY_PROGMEM UnityStrNaN[] = "NaN";
+static const char UNITY_PROGMEM UnityStrDet[] = "Determinate";
+static const char UNITY_PROGMEM UnityStrInvalidFloatTrait[] = "Invalid Float Trait";
+#endif
+const char UNITY_PROGMEM UnityStrErrShorthand[] = "Unity Shorthand Support Disabled";
+const char UNITY_PROGMEM UnityStrErrFloat[] = "Unity Floating Point Disabled";
+const char UNITY_PROGMEM UnityStrErrDouble[] = "Unity Double Precision Disabled";
+const char UNITY_PROGMEM UnityStrErr64[] = "Unity 64-bit Support Disabled";
+const char UNITY_PROGMEM UnityStrErrDetailStack[] = "Unity Detail Stack Support Disabled";
+static const char UNITY_PROGMEM UnityStrBreaker[] = "-----------------------";
+static const char UNITY_PROGMEM UnityStrResultsTests[] = " Tests ";
+static const char UNITY_PROGMEM UnityStrResultsFailures[] = " Failures ";
+static const char UNITY_PROGMEM UnityStrResultsIgnored[] = " Ignored ";
+#ifndef UNITY_EXCLUDE_DETAILS
+#ifdef UNITY_DETAIL_STACK_SIZE
+static const char* UNITY_PROGMEM UnityStrDetailLabels[] = UNITY_DETAIL_LABEL_NAMES;
+static const UNITY_COUNTER_TYPE UNITY_PROGMEM UnityStrDetailLabelsCount = sizeof(UnityStrDetailLabels) / sizeof(const char*);
+static const char UNITY_PROGMEM UnityStrErrDetailStackEmpty[] = " Detail Stack Empty";
+static const char UNITY_PROGMEM UnityStrErrDetailStackFull[] = " Detail Stack Full";
+static const char UNITY_PROGMEM UnityStrErrDetailStackLabel[] = " Detail Label Outside Of UNITY_DETAIL_LABEL_NAMES: ";
+static const char UNITY_PROGMEM UnityStrErrDetailStackPop[] = " Detail Pop With Unexpected Arguments";
+#else
+static const char UNITY_PROGMEM UnityStrDetail1Name[] = UNITY_DETAIL1_NAME " ";
+static const char UNITY_PROGMEM UnityStrDetail2Name[] = " " UNITY_DETAIL2_NAME " ";
+#endif
+#endif
+/*-----------------------------------------------
+ * Pretty Printers & Test Result Output Handlers
+ *-----------------------------------------------*/
+
+/*-----------------------------------------------*/
+/* Local helper function to print characters. */
+static void UnityPrintChar(const char* pch)
+{
+ /* printable characters plus CR & LF are printed */
+ if ((*pch <= 126) && (*pch >= 32))
+ {
+ UNITY_OUTPUT_CHAR(*pch);
+ }
+ /* write escaped carriage returns */
+ else if (*pch == 13)
+ {
+ UNITY_OUTPUT_CHAR('\\');
+ UNITY_OUTPUT_CHAR('r');
+ }
+ /* write escaped line feeds */
+ else if (*pch == 10)
+ {
+ UNITY_OUTPUT_CHAR('\\');
+ UNITY_OUTPUT_CHAR('n');
+ }
+ /* unprintable characters are shown as codes */
+ else
+ {
+ UNITY_OUTPUT_CHAR('\\');
+ UNITY_OUTPUT_CHAR('x');
+ UnityPrintNumberHex((UNITY_UINT)*pch, 2);
+ }
+}
+
+/*-----------------------------------------------*/
+/* Local helper function to print ANSI escape strings e.g. "\033[42m". */
+#ifdef UNITY_OUTPUT_COLOR
+static UNITY_UINT UnityPrintAnsiEscapeString(const char* string)
+{
+ const char* pch = string;
+ UNITY_UINT count = 0;
+
+ while (*pch && (*pch != 'm'))
+ {
+ UNITY_OUTPUT_CHAR(*pch);
+ pch++;
+ count++;
+ }
+ UNITY_OUTPUT_CHAR('m');
+ count++;
+
+ return count;
+}
+#endif
+
+/*-----------------------------------------------*/
+void UnityPrint(const char* string)
+{
+ const char* pch = string;
+
+ if (pch != NULL)
+ {
+ while (*pch)
+ {
+#ifdef UNITY_OUTPUT_COLOR
+ /* print ANSI escape code */
+ if ((*pch == 27) && (*(pch + 1) == '['))
+ {
+ pch += UnityPrintAnsiEscapeString(pch);
+ continue;
+ }
+#endif
+ UnityPrintChar(pch);
+ pch++;
+ }
+ }
+}
+/*-----------------------------------------------*/
+void UnityPrintLen(const char* string, const UNITY_UINT32 length)
+{
+ const char* pch = string;
+
+ if (pch != NULL)
+ {
+ while (*pch && ((UNITY_UINT32)(pch - string) < length))
+ {
+ /* printable characters plus CR & LF are printed */
+ if ((*pch <= 126) && (*pch >= 32))
+ {
+ UNITY_OUTPUT_CHAR(*pch);
+ }
+ /* write escaped carriage returns */
+ else if (*pch == 13)
+ {
+ UNITY_OUTPUT_CHAR('\\');
+ UNITY_OUTPUT_CHAR('r');
+ }
+ /* write escaped line feeds */
+ else if (*pch == 10)
+ {
+ UNITY_OUTPUT_CHAR('\\');
+ UNITY_OUTPUT_CHAR('n');
+ }
+ /* unprintable characters are shown as codes */
+ else
+ {
+ UNITY_OUTPUT_CHAR('\\');
+ UNITY_OUTPUT_CHAR('x');
+ UnityPrintNumberHex((UNITY_UINT)*pch, 2);
+ }
+ pch++;
+ }
+ }
+}
+
+/*-----------------------------------------------*/
+void UnityPrintIntNumberByStyle(const UNITY_INT number, const UNITY_DISPLAY_STYLE_T style)
+{
+ if (style == UNITY_DISPLAY_STYLE_CHAR)
+ {
+ /* printable characters plus CR & LF are printed */
+ UNITY_OUTPUT_CHAR('\'');
+ if ((number <= 126) && (number >= 32))
+ {
+ UNITY_OUTPUT_CHAR((int)number);
+ }
+ /* write escaped carriage returns */
+ else if (number == 13)
+ {
+ UNITY_OUTPUT_CHAR('\\');
+ UNITY_OUTPUT_CHAR('r');
+ }
+ /* write escaped line feeds */
+ else if (number == 10)
+ {
+ UNITY_OUTPUT_CHAR('\\');
+ UNITY_OUTPUT_CHAR('n');
+ }
+ /* unprintable characters are shown as codes */
+ else
+ {
+ UNITY_OUTPUT_CHAR('\\');
+ UNITY_OUTPUT_CHAR('x');
+ UnityPrintNumberHex((UNITY_UINT)number, 2);
+ }
+ UNITY_OUTPUT_CHAR('\'');
+ }
+ else if ((style & UNITY_DISPLAY_RANGE_INT) == UNITY_DISPLAY_RANGE_INT)
+ {
+ UnityPrintNumber(number);
+ }
+ else if ((style & UNITY_DISPLAY_RANGE_UINT) == UNITY_DISPLAY_RANGE_UINT)
+ {
+ UnityPrintNumberUnsigned((UNITY_UINT)number);
+ }
+ else
+ {
+ UNITY_OUTPUT_CHAR('0');
+ UNITY_OUTPUT_CHAR('x');
+ UnityPrintNumberHex((UNITY_UINT)number, (char)((style & 0xF) * 2));
+ }
+}
+
+void UnityPrintUintNumberByStyle(const UNITY_UINT number, const UNITY_DISPLAY_STYLE_T style)
+{
+ if ((style & UNITY_DISPLAY_RANGE_UINT) == UNITY_DISPLAY_RANGE_UINT)
+ {
+ UnityPrintNumberUnsigned(number);
+ }
+ else
+ {
+ UNITY_OUTPUT_CHAR('0');
+ UNITY_OUTPUT_CHAR('x');
+ UnityPrintNumberHex((UNITY_UINT)number, (char)((style & 0xF) * 2));
+ }
+}
+
+/*-----------------------------------------------*/
+void UnityPrintNumber(const UNITY_INT number_to_print)
+{
+ UNITY_UINT number = (UNITY_UINT)number_to_print;
+
+ if (number_to_print < 0)
+ {
+ /* A negative number, including MIN negative */
+ UNITY_OUTPUT_CHAR('-');
+ number = (~number) + 1;
+ }
+ UnityPrintNumberUnsigned(number);
+}
+
+/*-----------------------------------------------
+ * basically do an itoa using as little ram as possible */
+void UnityPrintNumberUnsigned(const UNITY_UINT number)
+{
+ UNITY_UINT divisor = 1;
+
+ /* figure out initial divisor */
+ while (number / divisor > 9)
+ {
+ divisor *= 10;
+ }
+
+ /* now mod and print, then divide divisor */
+ do
+ {
+ UNITY_OUTPUT_CHAR((char)('0' + (number / divisor % 10)));
+ divisor /= 10;
+ } while (divisor > 0);
+}
+
+/*-----------------------------------------------*/
+void UnityPrintNumberHex(const UNITY_UINT number, const char nibbles_to_print)
+{
+ int nibble;
+ char nibbles = nibbles_to_print;
+
+ if ((unsigned)nibbles > UNITY_MAX_NIBBLES)
+ {
+ nibbles = UNITY_MAX_NIBBLES;
+ }
+
+ while (nibbles > 0)
+ {
+ nibbles--;
+ nibble = (int)(number >> (nibbles * 4)) & 0x0F;
+ if (nibble <= 9)
+ {
+ UNITY_OUTPUT_CHAR((char)('0' + nibble));
+ }
+ else
+ {
+ UNITY_OUTPUT_CHAR((char)('A' - 10 + nibble));
+ }
+ }
+}
+
+/*-----------------------------------------------*/
+void UnityPrintMask(const UNITY_UINT mask, const UNITY_UINT number)
+{
+ UNITY_UINT current_bit = (UNITY_UINT)1 << (UNITY_INT_WIDTH - 1);
+ UNITY_INT32 i;
+
+ for (i = 0; i < UNITY_INT_WIDTH; i++)
+ {
+ if (current_bit & mask)
+ {
+ if (current_bit & number)
+ {
+ UNITY_OUTPUT_CHAR('1');
+ }
+ else
+ {
+ UNITY_OUTPUT_CHAR('0');
+ }
+ }
+ else
+ {
+ UNITY_OUTPUT_CHAR('X');
+ }
+ current_bit = current_bit >> 1;
+ }
+}
+
+/*-----------------------------------------------*/
+#ifndef UNITY_EXCLUDE_FLOAT_PRINT
+/*
+ * This function prints a floating-point value in a format similar to
+ * printf("%.7g") on a single-precision machine or printf("%.9g") on a
+ * double-precision machine. The 7th digit won't always be totally correct
+ * in single-precision operation (for that level of accuracy, a more
+ * complicated algorithm would be needed).
+ */
+void UnityPrintFloat(const UNITY_DOUBLE input_number)
+{
+#ifdef UNITY_INCLUDE_DOUBLE
+ static const int sig_digits = 9;
+ static const UNITY_INT32 min_scaled = 100000000;
+ static const UNITY_INT32 max_scaled = 1000000000;
+#else
+ static const int sig_digits = 7;
+ static const UNITY_INT32 min_scaled = 1000000;
+ static const UNITY_INT32 max_scaled = 10000000;
+#endif
+
+ UNITY_DOUBLE number = input_number;
+
+ /* handle zero, NaN, and +/- infinity */
+ if (number == 0.0f)
+ {
+ UnityPrint("0");
+ }
+ else if (UNITY_IS_NAN(number))
+ {
+ UnityPrint(UnityStrNaN);
+ }
+ else if (UNITY_IS_INF(number))
+ {
+ if (number < 0.0f)
+ {
+ UnityPrint(UnityStrNegInf);
+ }
+ else
+ {
+ UnityPrint(UnityStrInf);
+ }
+ }
+ else
+ {
+ UNITY_INT32 n_int = 0;
+ UNITY_INT32 n;
+ int exponent = 0;
+ int decimals;
+ int digits;
+ char buf[16] = {0};
+
+ if (number < 0.0f)
+ {
+ UNITY_OUTPUT_CHAR('-');
+ number = -number;
+ }
+ /*
+ * Scale up or down by powers of 10. To minimize rounding error,
+ * start with a factor/divisor of 10^10, which is the largest
+ * power of 10 that can be represented exactly. Finally, compute
+ * (exactly) the remaining power of 10 and perform one more
+ * multiplication or division.
+ */
+ if (number < 1.0f)
+ {
+ UNITY_DOUBLE factor = 1.0f;
+
+ while (number < (UNITY_DOUBLE)max_scaled / 1e10f) { number *= 1e10f; exponent -= 10; }
+ while (number * factor < (UNITY_DOUBLE)min_scaled) { factor *= 10.0f; exponent--; }
+
+ number *= factor;
+ }
+ else if (number > (UNITY_DOUBLE)max_scaled)
+ {
+ UNITY_DOUBLE divisor = 1.0f;
+
+ while (number > (UNITY_DOUBLE)min_scaled * 1e10f) { number /= 1e10f; exponent += 10; }
+ while (number / divisor > (UNITY_DOUBLE)max_scaled) { divisor *= 10.0f; exponent++; }
+
+ number /= divisor;
+ }
+ else
+ {
+ /*
+ * In this range, we can split off the integer part before
+ * doing any multiplications. This reduces rounding error by
+ * freeing up significant bits in the fractional part.
+ */
+ UNITY_DOUBLE factor = 1.0f;
+ n_int = (UNITY_INT32)number;
+ number -= (UNITY_DOUBLE)n_int;
+
+ while (n_int < min_scaled) { n_int *= 10; factor *= 10.0f; exponent--; }
+
+ number *= factor;
+ }
+
+ /* round to nearest integer */
+ n = ((UNITY_INT32)(number + number) + 1) / 2;
+
+#ifndef UNITY_ROUND_TIES_AWAY_FROM_ZERO
+ /* round to even if exactly between two integers */
+ if ((n & 1) && (((UNITY_DOUBLE)n - number) == 0.5f))
+ n--;
+#endif
+
+ n += n_int;
+
+ if (n >= max_scaled)
+ {
+ n = min_scaled;
+ exponent++;
+ }
+
+ /* determine where to place decimal point */
+ decimals = ((exponent <= 0) && (exponent >= -(sig_digits + 3))) ? (-exponent) : (sig_digits - 1);
+ exponent += decimals;
+
+ /* truncate trailing zeroes after decimal point */
+ while ((decimals > 0) && ((n % 10) == 0))
+ {
+ n /= 10;
+ decimals--;
+ }
+
+ /* build up buffer in reverse order */
+ digits = 0;
+ while ((n != 0) || (digits <= decimals))
+ {
+ buf[digits++] = (char)('0' + n % 10);
+ n /= 10;
+ }
+
+ /* print out buffer (backwards) */
+ while (digits > 0)
+ {
+ if (digits == decimals)
+ {
+ UNITY_OUTPUT_CHAR('.');
+ }
+ UNITY_OUTPUT_CHAR(buf[--digits]);
+ }
+
+ /* print exponent if needed */
+ if (exponent != 0)
+ {
+ UNITY_OUTPUT_CHAR('e');
+
+ if (exponent < 0)
+ {
+ UNITY_OUTPUT_CHAR('-');
+ exponent = -exponent;
+ }
+ else
+ {
+ UNITY_OUTPUT_CHAR('+');
+ }
+
+ digits = 0;
+ while ((exponent != 0) || (digits < 2))
+ {
+ buf[digits++] = (char)('0' + exponent % 10);
+ exponent /= 10;
+ }
+ while (digits > 0)
+ {
+ UNITY_OUTPUT_CHAR(buf[--digits]);
+ }
+ }
+ }
+}
+#endif /* ! UNITY_EXCLUDE_FLOAT_PRINT */
+
+/*-----------------------------------------------*/
+static void UnityTestResultsBegin(const char* file, const UNITY_LINE_TYPE line)
+{
+#ifdef UNITY_OUTPUT_FOR_ECLIPSE
+ UNITY_OUTPUT_CHAR('(');
+ UnityPrint(file);
+ UNITY_OUTPUT_CHAR(':');
+ UnityPrintNumber((UNITY_INT)line);
+ UNITY_OUTPUT_CHAR(')');
+ UNITY_OUTPUT_CHAR(' ');
+ UnityPrint(Unity.CurrentTestName);
+ UNITY_OUTPUT_CHAR(':');
+#else
+#ifdef UNITY_OUTPUT_FOR_IAR_WORKBENCH
+ UnityPrint("<SRCREF line=");
+ UnityPrintNumber((UNITY_INT)line);
+ UnityPrint(" file=\"");
+ UnityPrint(file);
+ UNITY_OUTPUT_CHAR('"');
+ UNITY_OUTPUT_CHAR('>');
+ UnityPrint(Unity.CurrentTestName);
+ UnityPrint("</SRCREF> ");
+#else
+#ifdef UNITY_OUTPUT_FOR_QT_CREATOR
+ UnityPrint("file://");
+ UnityPrint(file);
+ UNITY_OUTPUT_CHAR(':');
+ UnityPrintNumber((UNITY_INT)line);
+ UNITY_OUTPUT_CHAR(' ');
+ UnityPrint(Unity.CurrentTestName);
+ UNITY_OUTPUT_CHAR(':');
+#else
+ UnityPrint(file);
+ UNITY_OUTPUT_CHAR(':');
+ UnityPrintNumber((UNITY_INT)line);
+ UNITY_OUTPUT_CHAR(':');
+ UnityPrint(Unity.CurrentTestName);
+ UNITY_OUTPUT_CHAR(':');
+#endif
+#endif
+#endif
+}
+
+/*-----------------------------------------------*/
+static void UnityTestResultsFailBegin(const UNITY_LINE_TYPE line)
+{
+ UnityTestResultsBegin(Unity.TestFile, line);
+ UnityPrint(UnityStrFail);
+ UNITY_OUTPUT_CHAR(':');
+}
+
+/*-----------------------------------------------*/
+void UnityConcludeTest(void)
+{
+ if (Unity.CurrentTestIgnored)
+ {
+ Unity.TestIgnores++;
+ }
+ else if (!Unity.CurrentTestFailed)
+ {
+ UnityTestResultsBegin(Unity.TestFile, Unity.CurrentTestLineNumber);
+ UnityPrint(UnityStrPass);
+ }
+ else
+ {
+ Unity.TestFailures++;
+ }
+
+ Unity.CurrentTestFailed = 0;
+ Unity.CurrentTestIgnored = 0;
+ UNITY_PRINT_EXEC_TIME();
+ UNITY_PRINT_EOL();
+ UNITY_FLUSH_CALL();
+}
+
+/*-----------------------------------------------*/
+static void UnityAddMsgIfSpecified(const char* msg)
+{
+#ifdef UNITY_PRINT_TEST_CONTEXT
+ UnityPrint(UnityStrSpacer);
+ UNITY_PRINT_TEST_CONTEXT();
+#endif
+#ifndef UNITY_EXCLUDE_DETAILS
+#ifdef UNITY_DETAIL_STACK_SIZE
+ {
+ UNITY_COUNTER_TYPE c;
+ for (c = 0; (c < Unity.CurrentDetailStackSize) && (c < UNITY_DETAIL_STACK_SIZE); c++) {
+ const char* label;
+ if ((Unity.CurrentDetailStackLabels[c] == UNITY_DETAIL_NONE) || (Unity.CurrentDetailStackLabels[c] > UnityStrDetailLabelsCount)) {
+ break