diff options
| author | Paul Buetow <paul@buetow.org> | 2026-04-13 08:59:34 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-04-13 08:59:34 +0300 |
| commit | 5628e43ae771cfe3bc057375966621b3bfb48846 (patch) | |
| tree | e2a3b3b9dde58ce7eefb5835db81fe657cc02069 | |
| parent | a04d9d3520404066100600cc90cb2274822ad2cb (diff) | |
q2: add host-side unit test harness
| -rw-r--r-- | AGENTS.md | 1 | ||||
| -rw-r--r-- | Justfile | 4 | ||||
| -rw-r--r-- | tests/.gitignore | 1 | ||||
| -rw-r--r-- | tests/Makefile | 32 | ||||
| -rw-r--r-- | tests/test_csv.c | 70 | ||||
| -rw-r--r-- | tests/test_helpers.h | 21 | ||||
| -rw-r--r-- | tests/test_logic.c | 160 | ||||
| -rw-r--r-- | tests/test_main.c | 40 | ||||
| -rw-r--r-- | tests/vendor/unity/src/unity.c | 2637 | ||||
| -rw-r--r-- | tests/vendor/unity/src/unity.h | 698 | ||||
| -rw-r--r-- | tests/vendor/unity/src/unity_internals.h | 1283 |
11 files changed, 4947 insertions, 0 deletions
@@ -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: @@ -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; + } |
