From 3e3d9dcbe02c7d180881d357db27f818f260b8c2 Mon Sep 17 00:00:00 2001 From: Pim van Pelt Date: Sun, 28 Oct 2018 12:47:42 +0100 Subject: [PATCH] Add timespec -- add tests for it too. --- include/timespec.h | 46 ++++++++++ src/timespec.c | 193 ++++++++++++++++++++++++++++++++++++++ unittest/Makefile | 2 +- unittest/main.c | 20 ++-- unittest/test.c | 22 +++++ unittest/test.h | 44 +++++---- unittest/test_buttons.c | 4 +- unittest/test_timespec.c | 194 +++++++++++++++++++++++++++++++++++++++ 8 files changed, 492 insertions(+), 33 deletions(-) create mode 100644 include/timespec.h create mode 100644 src/timespec.c create mode 100644 unittest/test.c create mode 100644 unittest/test_timespec.c diff --git a/include/timespec.h b/include/timespec.h new file mode 100644 index 0000000..a65a9f1 --- /dev/null +++ b/include/timespec.h @@ -0,0 +1,46 @@ +#pragma once +#include "mgos.h" + +/* A simple time specifier for windows of time throughout a 24 hour day. + * + * Callers construct a struct mgos_timespec and then add time range + * specifications to the object. Ranges consist of ':', '-' and [0-9] + * characters and describe a [start-stop> time range where 'start' is + * included but 'stop' is not. Start and stop can have hours, minutes + * and seconds separated by ':'. Example specs: + * "8:00-10:00" (from 8am to 10am -- 7200 seconds) + * "23-01" (from 11pm to 1am -- 7200 seconds) + * "01:02:03-02" (from 01:02:03 to 2am -- 3477 seconds) + * + * Example to demonstrate the usage: + * + * struct tm target; + * struct mgos_timespec *ts = timespec_create(); + * timespec_add_spec(ts, "8:00-10:00"); // 7200 seconds, [08:00:00 - 10:00:00> + * timespec_add_spec(ts, "23-01"); // 7200 seconds, [23:00:00 - 01:00:00> + * timespec_add_spec(ts, "01:02:03-02:03:04"); // 3661 seconds, [01:02:03 - 02:03:04> + * + * target.tm_hour=8; target.tm_min=0; target.tm_sec=0; // 08:00:00 + * timespec_match(ts, &target); // TRUE; start of first spec + * + * target.tm_hour=10; target.tm_min=0; target.tm_sec=0; // 10:00:00 + * timespec_match(ts, &target); // FALSE; end of first spec; end times are excluded! + * + * target.tm_hour=0; target.tm_min=0; target.tm_sec=0; // 00:00:00 + * timespec_match(ts, &target); // TRUE; in the middle of the second spec + * + * target.tm_hour=1; target.tm_min=2; target.tm_sec=3; // 01:02:03 + * timespec_match(ts, &target); // TRUE; the first second of the third spec + * + * target.tm_hour=2; target.tm_min=3; target.tm_sec=3; // 02:03:03 + * timespec_match(ts, &target); // TRUE; the last second of the third spec + * + * timespec_destroy(&ts); + */ + +struct mgos_timespec; + +struct mgos_timespec *timespec_create(); +bool timespec_destroy(struct mgos_timespec **ts); +bool timespec_add_spec(struct mgos_timespec *ts, const char *spec); +bool timespec_match(const struct mgos_timespec *ts, const struct tm *tm); diff --git a/src/timespec.c b/src/timespec.c new file mode 100644 index 0000000..48cd154 --- /dev/null +++ b/src/timespec.c @@ -0,0 +1,193 @@ +#include "timespec.h" + +/* Private prototypes and declarations */ +struct mgos_timespec_spec { + uint8_t start_h, start_m, start_s; + uint8_t stop_h, stop_m, stop_s; + SLIST_ENTRY(mgos_timespec_spec) entries; +}; + +struct mgos_timespec { + SLIST_HEAD(mgos_timespec_specs, mgos_timespec_spec) specs; +}; + +static bool timespec_spec_parse(const char *spec, struct mgos_timespec_spec *out) { + uint8_t start_h = 0, start_m = 0, start_s = 0; + uint8_t stop_h = 0, stop_m = 0, stop_s = 0; + char * p; + int i; + + if (!spec || strlen(spec) == 0) { + LOG(LL_ERROR, ("spec cannot be NULL or empty")); + return false; + } + for (i = 1; i < (int) strlen(spec); i++) { + if (spec[i] != ':' && spec[i] != '-' && !(spec[i] >= '0' && spec[i] <= '9')) { + LOG(LL_ERROR, ("spec='%s': illegal chars, only want [:\\-0-9]", spec)); + return false; + } + } + + + p = (char *)spec; + if (!isdigit((int)*p)) { + LOG(LL_ERROR, ("spec='%s': start time must begin with a digit", spec)); + return false; + } + // start_h + i = strtol(p, &p, 10); + if (i < 0 || i > 23) { + LOG(LL_ERROR, ("spec='%s': start hour must be [0..23]", spec)); + return false; + } + start_h = i; + if (*p == ':') { + // start_m + i = strtol(p + 1, &p, 10); + if (i < 0 || i > 59) { + LOG(LL_ERROR, ("spec='%s': start minute must be [0..59]", spec)); + return false; + } + start_m = i; + } + if (*p == ':') { + // start_s + i = strtol(p + 1, &p, 10); + if (i < 0 || i > 59) { + LOG(LL_ERROR, ("spec='%s': start second must be [0..59]", spec)); + return false; + } + start_s = i; + } + if (*p != '-') { + LOG(LL_ERROR, ("spec='%s': No separator '-' found after start", spec)); + return false; + } + + // stop_h + if (!isdigit((int)*(p + 1))) { + LOG(LL_ERROR, ("spec='%s': stop time must begin with a digit", spec)); + return false; + } + i = strtol(p + 1, &p, 10); + if (i < 0 || i > 23) { + LOG(LL_ERROR, ("spec='%s': stop hour must be [0..23]", spec)); + return false; + } + stop_h = i; + if (*p == ':') { + // start_m + i = strtol(p + 1, &p, 10); + if (i < 0 || i > 59) { + LOG(LL_ERROR, ("spec='%s': stop minute must be [0..59]", spec)); + return false; + } + stop_m = i; + } + if (*p == ':') { + // stop_s + i = strtol(p + 1, &p, 10); + if (i < 0 || i > 59) { + LOG(LL_ERROR, ("spec='%s': stop second must be [0..59]", spec)); + return false; + } + stop_s = i; + } + if (*p) { + LOG(LL_ERROR, ("spec='%s': dangling characters after stop time", spec)); + return false; + } + +// LOG(LL_DEBUG, ("start=%d stop=%d", start, stop)); + if (out) { + out->start_h = start_h; + out->start_m = start_m; + out->start_s = start_s; + out->stop_h = stop_h; + out->stop_m = stop_m; + out->stop_s = stop_s; + } + return true; +} + +struct mgos_timespec *timespec_create() { + struct mgos_timespec *ts = calloc(1, sizeof(struct mgos_timespec)); + + if (!ts) { + return NULL; + } + + return ts; +} + +bool timespec_destroy(struct mgos_timespec **ts) { + if (!ts) { + return false; + } + while (!SLIST_EMPTY(&(*ts)->specs)) { + struct mgos_timespec_spec *t_spec; + + t_spec = SLIST_FIRST(&(*ts)->specs); + SLIST_REMOVE_HEAD(&(*ts)->specs, entries); + if (t_spec) { +// LOG(LL_DEBUG, ("Removed mgos_timespec_spec")); + free(t_spec); + } + } + + free(*ts); + *ts = NULL; + return true; +} + +bool timespec_add_spec(struct mgos_timespec *ts, const char *spec) { + struct mgos_timespec_spec *t_spec = calloc(1, sizeof(struct mgos_timespec_spec)); + + if (!ts) { + return false; + } + + if (!t_spec) { + LOG(LL_ERROR, ("Could not alloc memory for struct mgos_timespec_spec")); + return false; + } + if (!timespec_spec_parse(spec, t_spec)) { + LOG(LL_ERROR, ("spec='%s' is malformed, refusing to add", spec)); + return false; + } + SLIST_INSERT_HEAD(&ts->specs, t_spec, entries); + return true; +} + +bool timespec_match(const struct mgos_timespec *ts, const struct tm *tm) { + struct mgos_timespec_spec *t_spec; + + if (!ts) { + return false; + } + + SLIST_FOREACH(t_spec, &ts->specs, entries) { + uint32_t start, stop, target; + + start = t_spec->start_h * 3600 + t_spec->start_m * 60 + t_spec->start_s; + stop = t_spec->stop_h * 3600 + t_spec->stop_m * 60 + t_spec->stop_s; + target = tm->tm_hour * 3600 + tm->tm_min * 60 + tm->tm_sec; + if (start == stop) { + continue; + } + if (stop >= start) { + if (target >= start && target < stop) { +// LOG(LL_DEBUG, ("start=%d stop=%d target=%d matched", start, stop, target)); + return true; + } + } else { + if (target >= start || target < stop) { +// LOG(LL_DEBUG, ("start=%d stop=%d target=%d matched", start, stop, target)); + return true; + } + } +// LOG(LL_DEBUG, ("start=%d stop=%d target=%d did not match", start, stop, target)); + } + (void)tm; + return false; +} diff --git a/unittest/Makefile b/unittest/Makefile index 9ffb75e..aecb3cc 100644 --- a/unittest/Makefile +++ b/unittest/Makefile @@ -9,7 +9,7 @@ default: $(TARGET) all: default OBJECTS = $(patsubst %.c, %.o, $(wildcard *.c)) -SRCS = frozen/frozen.c ../src/channel.c ../src/mqtt.c ../src/statusled.c +SRCS = frozen/frozen.c ../src/channel.c ../src/mqtt.c ../src/statusled.c ../src/timespec.c HEADERS = $(wildcard *.h) %.o: %.c $(HEADERS) diff --git a/unittest/main.c b/unittest/main.c index fabf31f..2190b40 100644 --- a/unittest/main.c +++ b/unittest/main.c @@ -1,18 +1,14 @@ #include "test.h" -int test_failures = 0; -int assert_count = 0; - -uint32_t mqtt_pub_count; -uint32_t mqtt_sub_count; - int main() { - test_buttons(); + test_init(); - if (test_failures) { - LOG(LL_ERROR, ("%d test failures", test_failures)); - return -1; + test_buttons(); + test_timespec(); + + LOG(LL_INFO, ("Tests run=%d (success=%d fail=%d)", test_count_total(), test_count_success(), test_count_fail())); + if (test_count_total() == test_count_success()) { + return 0; } - LOG(LL_INFO, ("All tests passed, %d assertions OK", assert_count)); - return 0; + return -1; } diff --git a/unittest/test.c b/unittest/test.c new file mode 100644 index 0000000..5a8217b --- /dev/null +++ b/unittest/test.c @@ -0,0 +1,22 @@ +#include "test.h" + +uint32_t s_test_count_total; +uint32_t s_test_count_fail; + +void test_init() { + s_test_count_total = 0; + s_test_count_fail = 0; +} + +uint32_t test_count_total() { + return s_test_count_total; +} + +uint32_t test_count_success() { + return s_test_count_total - s_test_count_fail; +} + +uint32_t test_count_fail() { + return s_test_count_fail; +} + diff --git a/unittest/test.h b/unittest/test.h index a519e4b..f1ef479 100644 --- a/unittest/test.h +++ b/unittest/test.h @@ -1,26 +1,34 @@ -#ifndef __TEST_H -#define __TEST_H - +#pragma once +#include "mgos.h" #include #include -#include "frozen/frozen.h" -#include "mgos_mock.h" -#include "main.h" +#include +#include -extern int test_failures; -extern int assert_count; +extern uint32_t s_test_count_fail, s_test_count_total; -#define ASSERT(expr, errstr) \ - do { \ - if (!(expr)) { \ - LOG(LL_ERROR, ("ASSERT FAIL: "errstr)); \ - test_failures++; \ - } \ - assert_count++; \ +#define ASSERT(expr, msg) \ + do { \ + s_test_count_total++; \ + if (!(expr)) { \ + LOG(LL_ERROR, ("TEST FAILURE line=%d expr='%s' msg='%s'", __LINE__, #expr, msg)); \ + s_test_count_fail++; \ + } else { \ + LOG(LL_INFO, ("TEST PASS line=%d expr='%s' msg='%s'", __LINE__, #expr, msg)); \ + } \ } while (0) +#define RUN_TEST(test) \ + do { \ + const char *msg = test(); \ + if (msg) { return msg; } \ + } while (0) +void test_init(); +uint32_t test_count_total(); +uint32_t test_count_success(); +uint32_t test_count_fail(); -int test_buttons(void); - -#endif // __TEST_H +// Insert test methods here +void test_buttons(); +void test_timespec(); diff --git a/unittest/test_buttons.c b/unittest/test_buttons.c index 953842c..2684674 100644 --- a/unittest/test_buttons.c +++ b/unittest/test_buttons.c @@ -1,7 +1,7 @@ #include "test.h" +#include "main.h" -int test_buttons() { +void test_buttons() { channel_init("testdata/testconfig0.json"); channel_init("testdata/testconfig1.json"); - return 0; } diff --git a/unittest/test_timespec.c b/unittest/test_timespec.c new file mode 100644 index 0000000..f452b55 --- /dev/null +++ b/unittest/test_timespec.c @@ -0,0 +1,194 @@ +#include "mgos.h" +#include "test.h" +#include "timespec.h" + +static void test_timespec_pass(void) { + struct mgos_timespec *ts; + static const char * tests[] = { + "08-20", + "8-20", + "08:01-20:59", + "8:00-20", + "08:00:00-20:00:00", + "8:00:00-20:00:00", + "0-0:01", + "0-23:59:59", + "08-20:30", + "20-08", + "20:01:02-08:01:02", + "00:00-00:00", + NULL + }; + + ts = timespec_create(); + ASSERT(ts, "Created timespec"); + for (int i = 0; tests[i]; i++) { + ASSERT(timespec_add_spec(ts, tests[i]), tests[i]); + } + ASSERT(timespec_destroy(&ts), "Destroyed timespec"); +} + +static void test_timespec_fail(void) { + struct mgos_timespec *ts; + static const char * tests[] = { + "08", // No endtime given + "08-", // No endtime given + "-20", // No begintime given + "08:00:00:00-20", // Malformed begintime + "08:00-20:00:00:00", // Malformed endtime + "24-01", // Hours >23 + "08:60-20:00", // Minutes >59 + "08:59:60-20:00", // Seconds >59 + "08:59:59-24:00:00", // Hours >23 + "08:59:59-23:60:00", // Minutes >59 + "08:59:59-23:59:60", // Seconds >59 + "abc", // Invalid chars + "", // Empty spec + "-", // No times specified + NULL + }; + + ts = timespec_create(); + ASSERT(ts, "Created timespec"); + for (int i = 0; tests[i]; i++) { + ASSERT(!timespec_add_spec(ts, tests[i]), tests[i]); + } + ASSERT(timespec_destroy(&ts), "Destroyed timespec"); +} + +static void test_timespec_flips_and_seconds(const struct mgos_timespec *ts, const uint32_t expected_flips, const uint32_t expected_seconds_on) { + struct tm target; + bool state = false; + int state_flips = 0; + int state_seconds_on = 0; + int state_seconds_off = 0; + + for (int i = 0; i < 86400; i++) { + target.tm_sec = i % 60; + target.tm_min = (i / 60) % 60; + target.tm_hour = (i / 3600) % 24; + if (state != timespec_match(ts, &target)) { + state = !state; + state_flips++; + LOG(LL_DEBUG, ("State flipped to %s at %02d:%02d:%02d", state ? "On" : "Off", target.tm_hour, target.tm_min, target.tm_sec)); + } + if (state) { + state_seconds_on++; + } else{ + state_seconds_off++; + } + } + LOG(LL_DEBUG, ("state_flips=%d state_seconds_on=%d", state_flips, state_seconds_on)); + + ASSERT(state_flips == expected_flips, "Want to see expected_flips number of state flips"); + ASSERT(state_seconds_on == expected_seconds_on, "State seconds on should be expected_seconds_on"); + ASSERT(state_seconds_on + state_seconds_off == 86400, "State seconds on + off should be 86400 seconds"); +} + +static void test_timespec_state(void) { + struct mgos_timespec *ts = timespec_create(); + struct tm target; + + ASSERT(ts, "Created timespec"); + ASSERT(timespec_add_spec(ts, "08-10"), "Added timespec"); + ASSERT(timespec_add_spec(ts, "12:00-13:00"), "Added timespec"); + ASSERT(timespec_add_spec(ts, "14:11:05-14:59:52"), "Added timespec"); + ASSERT(timespec_add_spec(ts, "23:01:02-01:02:03"), "Added timespec"); + ASSERT(!timespec_add_spec(ts, "14:60-15:01"), "Illegal timespec"); + + target.tm_sec = 59; target.tm_min = 59; target.tm_hour = 7; + ASSERT(!timespec_match(ts, &target), "No match at 07:59:59"); + + target.tm_sec = 0; target.tm_min = 0; target.tm_hour = 8; + ASSERT(timespec_match(ts, &target), "Match at 08:00:00"); + + target.tm_sec = 59; target.tm_min = 59; target.tm_hour = 9; + ASSERT(timespec_match(ts, &target), "Match at 09:59:59"); + + target.tm_sec = 0; target.tm_min = 0; target.tm_hour = 10; + ASSERT(!timespec_match(ts, &target), "No Match at 10:00:00"); + + target.tm_sec = 59; target.tm_min = 59; target.tm_hour = 11; + ASSERT(!timespec_match(ts, &target), "No match at 11:59:59"); + + target.tm_sec = 0; target.tm_min = 0; target.tm_hour = 12; + ASSERT(timespec_match(ts, &target), "Match at 12:00:00"); + + target.tm_sec = 59; target.tm_min = 59; target.tm_hour = 12; + ASSERT(timespec_match(ts, &target), "Match at 12:59:59"); + + target.tm_sec = 0; target.tm_min = 0; target.tm_hour = 13; + ASSERT(!timespec_match(ts, &target), "No match at 13:00:00"); + + target.tm_sec = 4; target.tm_min = 11; target.tm_hour = 14; + ASSERT(!timespec_match(ts, &target), "No match at 14:11:04"); + + target.tm_sec = 5; target.tm_min = 11; target.tm_hour = 14; + ASSERT(timespec_match(ts, &target), "Match at 14:11:05"); + + target.tm_sec = 51; target.tm_min = 59; target.tm_hour = 14; + ASSERT(timespec_match(ts, &target), "Match at 14:59:51"); + + target.tm_sec = 52; target.tm_min = 59; target.tm_hour = 14; + ASSERT(!timespec_match(ts, &target), "No match at 14:59:52"); + + target.tm_sec = 1; target.tm_min = 1; target.tm_hour = 23; + ASSERT(!timespec_match(ts, &target), "No match at 23:01:01"); + + target.tm_sec = 2; target.tm_min = 1; target.tm_hour = 23; + ASSERT(timespec_match(ts, &target), "Match at 23:01:02"); + + target.tm_sec = 59; target.tm_min = 59; target.tm_hour = 23; + ASSERT(timespec_match(ts, &target), "Match at 23:59:59"); + + target.tm_sec = 0; target.tm_min = 0; target.tm_hour = 0; + ASSERT(timespec_match(ts, &target), "Match at 00:00:00"); + + target.tm_sec = 2; target.tm_min = 2; target.tm_hour = 1; + ASSERT(timespec_match(ts, &target), "Match at 01:02:02"); + + target.tm_sec = 3; target.tm_min = 2; target.tm_hour = 1; + ASSERT(!timespec_match(ts, &target), "No match at 01:02:03"); + + ASSERT(timespec_destroy(&ts), "Destroyed timespec"); + + // Some time counting tests + ts = timespec_create(); + ASSERT(ts, "Created timespec"); + ASSERT(timespec_add_spec(ts, "08-10"), "Added timespec"); + test_timespec_flips_and_seconds(ts, 2, 7200); + + // Zero seconds long + ASSERT(timespec_add_spec(ts, "00:00:00-00:00:00"), "Added timespec"); + test_timespec_flips_and_seconds(ts, 2, 7200 + 0); + + // Add two hours wrapping midnight + ASSERT(timespec_add_spec(ts, "23:00:00-01:00:00"), "Added timespec"); + test_timespec_flips_and_seconds(ts, 5, 7200 + 0 + 7200); + + // Add single second + ASSERT(timespec_add_spec(ts, "02:00:00-02:00:01"), "Added timespec"); + test_timespec_flips_and_seconds(ts, 7, 7200 + 0 + 7200 + 1); + + // Add an hour, a minute and a second + ASSERT(timespec_add_spec(ts, "03:02:03-04:03:04"), "Added timespec"); + test_timespec_flips_and_seconds(ts, 9, 7200 + 0 + 7200 + 1 + 3661); + + ASSERT(timespec_destroy(&ts), "Destroyed timespec"); + + // Some other time counting tests + ts = timespec_create(); + ASSERT(ts, "Created timespec"); + ASSERT(timespec_add_spec(ts, "00-12"), "Added timespec"); + test_timespec_flips_and_seconds(ts, 2, 12 * 60 * 60); + ASSERT(timespec_add_spec(ts, "12-00"), "Added timespec"); + test_timespec_flips_and_seconds(ts, 1, 24 * 60 * 60); + + ASSERT(timespec_destroy(&ts), "Destroyed timespec"); +} + +void test_timespec() { + test_timespec_pass(); + test_timespec_fail(); + test_timespec_state(); +}