| /* |
| * Copyright 2020 Peter Bigot Consulting |
| * |
| * SPDX-License-Identifier: Apache-2.0 |
| */ |
| |
| /* Tests for the time_sync data structures */ |
| |
| #include <string.h> |
| #include <zephyr/ztest.h> |
| #include "timeutil_test.h" |
| |
| static const struct timeutil_sync_config cfg1 = { |
| .ref_Hz = USEC_PER_SEC, |
| .local_Hz = 32768, |
| }; |
| |
| static const struct timeutil_sync_config cfg2 = { |
| .ref_Hz = NSEC_PER_SEC, |
| .local_Hz = 100, |
| }; |
| |
| static inline uint64_t scale_ref(uint32_t factor, |
| const struct timeutil_sync_config *cfg) |
| { |
| return (uint64_t)factor * (uint64_t)cfg->ref_Hz; |
| } |
| |
| static inline uint64_t scale_local(uint32_t factor, |
| const struct timeutil_sync_config *cfg) |
| { |
| return (uint64_t)factor * (uint64_t)cfg->local_Hz; |
| } |
| |
| static inline int64_t scale_local_signed(int32_t factor, |
| const struct timeutil_sync_config *cfg) |
| { |
| return (int64_t)factor * (int64_t)cfg->local_Hz; |
| } |
| |
| |
| static void test_state_update(void) |
| { |
| struct timeutil_sync_instant si = { 0 }; |
| struct timeutil_sync_state ss = { 0 }; |
| int rv = timeutil_sync_state_update(&ss, &si); |
| |
| zassert_equal(rv, -EINVAL, |
| "invalid init got: %d", rv); |
| zassert_equal(ss.base.ref, 0, |
| "unexpected base ref"); |
| zassert_equal(ss.skew, 0, |
| "unexpected skew"); |
| |
| si.ref = 1; |
| rv = timeutil_sync_state_update(&ss, &si); |
| zassert_equal(rv, 0, |
| "valid first init got: %d", rv); |
| zassert_equal(ss.base.ref, 1, |
| "base not updated"); |
| zassert_equal(ss.latest.ref, 0, |
| "unexpected latest ref"); |
| zassert_equal(ss.skew, 1.0, |
| "unexpected skew"); |
| |
| rv = timeutil_sync_state_update(&ss, &si); |
| zassert_equal(rv, -EINVAL, |
| "non-increasing ref got: %d", rv); |
| zassert_equal(ss.base.ref, 1, |
| "unexpected base ref"); |
| zassert_equal(ss.base.local, 0, |
| "unexpected base local"); |
| zassert_equal(ss.latest.ref, 0, |
| "unexpected latest ref"); |
| |
| si.ref += 1; |
| rv = timeutil_sync_state_update(&ss, &si); |
| zassert_equal(rv, -EINVAL, |
| "non-increasing local got: %d", rv); |
| zassert_equal(ss.latest.ref, 0, |
| "unexpected latest ref"); |
| |
| si.local += 20; |
| rv = timeutil_sync_state_update(&ss, &si); |
| zassert_equal(rv, 1, |
| "increasing got: %d", rv); |
| zassert_equal(ss.base.ref, 1, |
| "unexpected base ref"); |
| zassert_equal(ss.base.local, 0, |
| "unexpected base local"); |
| zassert_equal(ss.latest.ref, si.ref, |
| "unexpected latest ref"); |
| zassert_equal(ss.latest.local, si.local, |
| "unexpected latest local"); |
| } |
| |
| static void test_state_set_skew(void) |
| { |
| struct timeutil_sync_instant si = { |
| .ref = 1, |
| }; |
| struct timeutil_sync_state ss = { |
| .cfg = &cfg1, |
| }; |
| float skew = 0.99; |
| int rv = timeutil_sync_state_update(&ss, &si); |
| |
| zassert_equal(rv, 0, |
| "valid first init got: %d", rv); |
| zassert_equal(ss.skew, 1.0, |
| "unexpected skew"); |
| |
| rv = timeutil_sync_state_set_skew(&ss, -1.0, NULL); |
| zassert_equal(rv, -EINVAL, |
| "negative skew set got: %d", rv); |
| zassert_equal(ss.skew, 1.0, |
| "unexpected skew"); |
| |
| rv = timeutil_sync_state_set_skew(&ss, 0.0, NULL); |
| zassert_equal(rv, -EINVAL, |
| "zero skew set got: %d", rv); |
| zassert_equal(ss.skew, 1.0, |
| "unexpected skew"); |
| |
| rv = timeutil_sync_state_set_skew(&ss, skew, NULL); |
| zassert_equal(rv, 0, |
| "valid skew set got: %d", rv); |
| zassert_equal(ss.skew, skew, |
| "unexpected skew"); |
| zassert_equal(ss.base.ref, si.ref, |
| "unexpected base ref"); |
| zassert_equal(ss.base.local, si.local, |
| "unexpected base ref"); |
| |
| skew = 1.01; |
| si.ref += 5; |
| si.local += 3; |
| |
| rv = timeutil_sync_state_set_skew(&ss, skew, &si); |
| zassert_equal(rv, 0, |
| "valid skew set got: %d", rv); |
| zassert_equal(ss.skew, skew, |
| "unexpected skew"); |
| zassert_equal(ss.base.ref, si.ref, |
| "unexpected base ref"); |
| zassert_equal(ss.base.local, si.local, |
| "unexpected base ref"); |
| zassert_equal(ss.latest.ref, 0, |
| "uncleared latest ref"); |
| zassert_equal(ss.latest.local, 0, |
| "uncleared latest local"); |
| } |
| |
| static void test_estimate_skew(void) |
| { |
| struct timeutil_sync_state ss = { |
| .cfg = &cfg1, |
| }; |
| struct timeutil_sync_instant si0 = { |
| .ref = cfg1.ref_Hz, |
| }; |
| struct timeutil_sync_instant si1 = { |
| .ref = si0.ref + cfg1.ref_Hz, |
| .local = si0.local + cfg1.local_Hz, |
| }; |
| float skew = 0.0; |
| |
| skew = timeutil_sync_estimate_skew(&ss); |
| zassert_equal(skew, 0, |
| "unexpected uninit skew: %f", skew); |
| |
| int rv = timeutil_sync_state_update(&ss, &si0); |
| |
| zassert_equal(rv, 0, |
| "valid init got: %d", rv); |
| |
| skew = timeutil_sync_estimate_skew(&ss); |
| zassert_equal(skew, 0, |
| "unexpected base-only skew: %f", skew); |
| |
| rv = timeutil_sync_state_update(&ss, &si1); |
| zassert_equal(rv, 1, |
| "valid update got: %d", rv); |
| |
| zassert_equal(ss.base.ref, si0.ref, |
| "unexpected base ref"); |
| zassert_equal(ss.base.local, si0.local, |
| "unexpected base local"); |
| zassert_equal(ss.latest.ref, si1.ref, |
| "unexpected latest ref"); |
| zassert_equal(ss.latest.local, si1.local, |
| "unexpected latest local"); |
| |
| skew = timeutil_sync_estimate_skew(&ss); |
| zassert_equal(skew, 1.0, |
| "unexpected linear skew: %f", skew); |
| |
| /* Local advanced half as far as it should: scale by 2 to |
| * correct. |
| */ |
| ss.latest.local = scale_local(1, ss.cfg) / 2; |
| skew = timeutil_sync_estimate_skew(&ss); |
| zassert_equal(skew, 2.0, |
| "unexpected half skew: %f", skew); |
| |
| /* Local advanced twice as far as it should: scale by 1/2 to |
| * correct. |
| */ |
| ss.latest.local = scale_local(2, ss.cfg); |
| skew = timeutil_sync_estimate_skew(&ss); |
| zassert_equal(skew, 0.5, |
| "unexpected double skew: %f", skew); |
| } |
| |
| static void tref_from_local(const char *tag, |
| const struct timeutil_sync_config *cfg) |
| { |
| struct timeutil_sync_state ss = { |
| .cfg = cfg, |
| }; |
| struct timeutil_sync_instant si0 = { |
| /* Absolute local 0 is 5 s ref */ |
| .ref = scale_ref(10, cfg), |
| .local = scale_local(5, cfg), |
| }; |
| uint64_t ref = 0; |
| int rv = timeutil_sync_ref_from_local(&ss, 0, &ref); |
| |
| zassert_equal(rv, -EINVAL, |
| "%s: unexpected uninit convert: %d", tag, rv); |
| |
| rv = timeutil_sync_state_update(&ss, &si0); |
| zassert_equal(rv, 0, |
| "%s: unexpected init: %d", tag, rv); |
| zassert_equal(ss.skew, 1.0, |
| "%s: unexpected skew"); |
| |
| rv = timeutil_sync_ref_from_local(&ss, ss.base.local, NULL); |
| zassert_equal(rv, -EINVAL, |
| "%s: unexpected missing dest: %d", tag, rv); |
| |
| rv = timeutil_sync_ref_from_local(&ss, ss.base.local, &ref); |
| zassert_equal(rv, 0, |
| "%s: unexpected fail", tag, rv); |
| zassert_equal(ref, ss.base.ref, |
| "%s: unexpected base convert", tag); |
| |
| rv = timeutil_sync_ref_from_local(&ss, 0, &ref); |
| zassert_equal(rv, 0, |
| "%s: unexpected local=0 fail", tag, rv); |
| zassert_equal(ref, scale_ref(5, cfg), |
| "%s: unexpected local=0 ref", tag); |
| |
| rv = timeutil_sync_ref_from_local(&ss, ss.base.local, &ref); |
| zassert_equal(rv, 0, |
| "%s: unexpected local=base fail", tag, rv); |
| zassert_equal(ref, ss.base.ref, |
| "%s: unexpected local=base ref", tag); |
| |
| rv = timeutil_sync_ref_from_local(&ss, ss.base.local |
| + scale_local(2, cfg), &ref); |
| zassert_equal(rv, 0, |
| "%s: unexpected local=base+2s fail %d", tag, rv); |
| zassert_equal(ref, ss.base.ref + scale_ref(2, cfg), |
| "%s: unexpected local=base+2s ref", tag); |
| |
| rv = timeutil_sync_ref_from_local(&ss, (int64_t)ss.base.local |
| - scale_local(12, cfg), &ref); |
| zassert_equal(rv, -ERANGE, |
| "%s: unexpected local=base-12s res %u", tag, rv); |
| |
| /* Skew of 0.5 means local runs at double speed */ |
| rv = timeutil_sync_state_set_skew(&ss, 0.5, NULL); |
| zassert_equal(rv, 0, |
| "%s: failed set skew", tag); |
| |
| /* Local at double speed corresponds to half advance in ref */ |
| rv = timeutil_sync_ref_from_local(&ss, ss.base.local |
| + scale_local(2, cfg), &ref); |
| zassert_equal(rv, 1, |
| "%s: unexpected skew adj fail", tag); |
| zassert_equal(ref, ss.base.ref + cfg->ref_Hz, |
| "%s: unexpected skew adj convert", tag); |
| } |
| |
| static void test_ref_from_local(void) |
| { |
| tref_from_local("std", &cfg1); |
| tref_from_local("ext", &cfg2); |
| } |
| |
| static void tlocal_from_ref(const char *tag, |
| const struct timeutil_sync_config *cfg) |
| { |
| struct timeutil_sync_state ss = { |
| .cfg = cfg, |
| }; |
| struct timeutil_sync_instant si0 = { |
| /* Absolute local 0 is 5 s ref */ |
| .ref = scale_ref(10, cfg), |
| .local = scale_local(5, cfg), |
| }; |
| int64_t local = 0; |
| int rv = timeutil_sync_local_from_ref(&ss, 0, &local); |
| |
| zassert_equal(rv, -EINVAL, |
| "%s: unexpected uninit convert: %d", tag, rv); |
| |
| rv = timeutil_sync_state_update(&ss, &si0); |
| zassert_equal(rv, 0, |
| "%s: unexpected init: %d", tag, rv); |
| zassert_equal(ss.skew, 1.0, |
| "%s: unexpected skew", tag); |
| |
| rv = timeutil_sync_local_from_ref(&ss, ss.base.ref, NULL); |
| zassert_equal(rv, -EINVAL, |
| "%s: unexpected missing dest", tag, rv); |
| |
| rv = timeutil_sync_local_from_ref(&ss, ss.base.ref, &local); |
| zassert_equal(rv, 0, |
| "%s: unexpected fail", tag, rv); |
| zassert_equal(local, ss.base.local, |
| "%s: unexpected base convert", tag); |
| |
| rv = timeutil_sync_local_from_ref(&ss, ss.base.ref |
| + scale_ref(2, cfg), &local); |
| zassert_equal(rv, 0, |
| "%s: unexpected base+2s fail", tag); |
| zassert_equal(local, ss.base.local + scale_local(2, cfg), |
| "%s: unexpected base+2s convert", tag); |
| |
| rv = timeutil_sync_local_from_ref(&ss, ss.base.ref |
| - scale_ref(7, cfg), &local); |
| zassert_equal(rv, 0, |
| "%s: unexpected base-7s fail", tag); |
| zassert_equal(local, scale_local_signed(-2, cfg), |
| "%s: unexpected base-7s convert", tag); |
| |
| |
| /* Skew of 0.5 means local runs at double speed */ |
| rv = timeutil_sync_state_set_skew(&ss, 0.5, NULL); |
| zassert_equal(rv, 0, |
| "%s: failed set skew", tag); |
| |
| /* Local at double speed corresponds to half advance in ref */ |
| rv = timeutil_sync_local_from_ref(&ss, ss.base.ref |
| + scale_ref(1, cfg) / 2, &local); |
| zassert_equal(rv, 1, |
| "%s: unexpected skew adj fail", tag); |
| zassert_equal(local, ss.base.local + scale_local(1, cfg), |
| "%s: unexpected skew adj convert", tag); |
| } |
| |
| static void test_local_from_ref(void) |
| { |
| tlocal_from_ref("std", &cfg1); |
| tlocal_from_ref("ext", &cfg2); |
| } |
| |
| static void test_large_linearity(void) |
| { |
| uint64_t inputs[] = { |
| 1000ULL, |
| 3999999999ULL, |
| 4000000000ULL, |
| 4000000001ULL, |
| UINT64_MAX / 10000000 |
| }; |
| uint64_t ref_out; |
| int64_t loc_out; |
| int rv; |
| |
| const struct timeutil_sync_config unity = { |
| .ref_Hz = 1000, |
| .local_Hz = 1000, |
| }; |
| struct timeutil_sync_instant inst = { |
| .ref = 200, |
| .local = 100 |
| }; |
| struct timeutil_sync_state ss = { |
| .cfg = &unity, |
| }; |
| uint64_t offset = inst.ref - inst.local; |
| |
| timeutil_sync_state_set_skew(&ss, 1.0f, &inst); |
| |
| for (int i = 0; i < ARRAY_SIZE(inputs); i++) { |
| rv = timeutil_sync_ref_from_local(&ss, inputs[i], &ref_out); |
| zassert_equal(rv, 0, "Unexpected conversion fail"); |
| zassert_equal(ref_out, inputs[i] + offset, |
| "Large unity local->ref conversion fail"); |
| |
| rv = timeutil_sync_local_from_ref(&ss, inputs[i], &loc_out); |
| zassert_equal(rv, 0, "Unexpected conversion fail"); |
| zassert_equal(loc_out, inputs[i] - offset, |
| "Large unity ref->local conversion fail"); |
| } |
| } |
| |
| static void test_skew_to_ppb(void) |
| { |
| float skew = 1.0; |
| int32_t ppb = timeutil_sync_skew_to_ppb(skew); |
| |
| zassert_equal(ppb, 0, |
| "unexpected perfect: %d", ppb); |
| |
| skew = 0.999976; |
| ppb = timeutil_sync_skew_to_ppb(skew); |
| zassert_equal(ppb, 24020, |
| "unexpected fast: %d", ppb); |
| |
| skew = 1.000022; |
| ppb = timeutil_sync_skew_to_ppb(skew); |
| zassert_equal(ppb, -22053, |
| "unexpected slow: %d", ppb); |
| |
| skew = 3.147483587; |
| ppb = timeutil_sync_skew_to_ppb(skew); |
| zassert_equal(ppb, -2147483587, |
| "unexpected near limit: %.10g %d", skew, ppb); |
| skew = 3.147483826; |
| ppb = timeutil_sync_skew_to_ppb(skew); |
| zassert_equal(ppb, INT32_MIN, |
| "unexpected above limit: %.10g %d", skew, ppb); |
| } |
| |
| ZTEST(timeutil_api, test_sync) |
| { |
| test_state_update(); |
| test_state_set_skew(); |
| test_estimate_skew(); |
| test_ref_from_local(); |
| test_local_from_ref(); |
| test_large_linearity(); |
| test_skew_to_ppb(); |
| } |