/** * Copyright (c) 2026 Dominic Masters * * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ #include "dusktest.h" #include "animation/keyframe.h" static void test_keyframeGetValueSingleSegmentLinear(void **state) { keyframe_t keyframes[2] = { { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR }, { .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR }, }; assert_float_equal(keyframeGetValue(keyframes, 2, 0.0f), 0.0f, 0.0001f); assert_float_equal(keyframeGetValue(keyframes, 2, 0.25f), 2.5f, 0.0001f); assert_float_equal(keyframeGetValue(keyframes, 2, 0.5f), 5.0f, 0.0001f); assert_float_equal(keyframeGetValue(keyframes, 2, 0.75f), 7.5f, 0.0001f); } static void test_keyframeGetValueMultiSegment(void **state) { keyframe_t keyframes[3] = { { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR }, { .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR }, { .time = 2.0f, .value = 20.0f, .easing = EASING_LINEAR }, }; // Within the first segment. assert_float_equal(keyframeGetValue(keyframes, 3, 0.5f), 5.0f, 0.0001f); // Exactly on the interior keyframe, resolved as the end of segment one. assert_float_equal(keyframeGetValue(keyframes, 3, 1.0f), 10.0f, 0.0001f); // Within the second segment. assert_float_equal(keyframeGetValue(keyframes, 3, 1.5f), 15.0f, 0.0001f); } static void test_keyframeGetValueDescendingValues(void **state) { keyframe_t keyframes[2] = { { .time = 0.0f, .value = 100.0f, .easing = EASING_LINEAR }, { .time = 1.0f, .value = 0.0f, .easing = EASING_LINEAR }, }; assert_float_equal(keyframeGetValue(keyframes, 2, 0.0f), 100.0f, 0.0001f); assert_float_equal(keyframeGetValue(keyframes, 2, 0.5f), 50.0f, 0.0001f); } static void test_keyframeGetValueAppliesEasing(void **state) { keyframe_t keyframes[2] = { { .time = 0.0f, .value = 0.0f, .easing = EASING_IN_QUAD }, { .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR }, }; // EASING_IN_QUAD is t * t, so halfway through time should be a quarter of // the way through the value range, not half. assert_float_equal(keyframeGetValue(keyframes, 2, 0.5f), 2.5f, 0.0001f); } static void test_keyframeGetValueNonZeroStartTime(void **state) { keyframe_t keyframes[2] = { { .time = 5.0f, .value = 0.0f, .easing = EASING_LINEAR }, { .time = 10.0f, .value = 100.0f, .easing = EASING_LINEAR }, }; assert_float_equal(keyframeGetValue(keyframes, 2, 5.0f), 0.0f, 0.0001f); assert_float_equal(keyframeGetValue(keyframes, 2, 7.5f), 50.0f, 0.0001f); } // NOTE: keyframeGetValue does not clamp to the first keyframe's value when // queried before the first keyframe's time - the start and end pointers // collapse onto the same keyframe, so the interpolation divides by a zero // time delta. This test documents that current behavior rather than // asserting it is desirable; flag to the maintainer if this should instead // clamp like the last-keyframe case does. static void test_keyframeGetValueBeforeFirstKeyframeIsNaN(void **state) { keyframe_t keyframes[2] = { { .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR }, { .time = 3.0f, .value = 30.0f, .easing = EASING_LINEAR }, }; assert_true(isnan(keyframeGetValue(keyframes, 2, 0.0f))); } static void test_keyframeGetValueAtOrAfterLastKeyframeClampsToLastValue(void **state) { keyframe_t keyframes[2] = { { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR }, { .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR }, }; assert_float_equal(keyframeGetValue(keyframes, 2, 1.0f), 10.0f, 0.0001f); assert_float_equal(keyframeGetValue(keyframes, 2, 2.0f), 10.0f, 0.0001f); } static void test_keyframeGetValueSingleKeyframeAtOrAfterClampsToValue(void **state) { keyframe_t keyframes[1] = { { .time = 5.0f, .value = 42.0f, .easing = EASING_LINEAR }, }; assert_float_equal(keyframeGetValue(keyframes, 1, 5.0f), 42.0f, 0.0001f); assert_float_equal(keyframeGetValue(keyframes, 1, 10.0f), 42.0f, 0.0001f); // Before the single keyframe's time is still the documented NaN case. assert_true(isnan(keyframeGetValue(keyframes, 1, 0.0f))); } static void test_keyframeGetValueNullKeyframesAsserts(void **state) { expect_assert_failure(keyframeGetValue(NULL, 1, 0.0f)); } static void test_keyframeGetValueZeroCountAsserts(void **state) { keyframe_t keyframes[1] = { { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR }, }; expect_assert_failure(keyframeGetValue(keyframes, 0, 0.0f)); } static void test_keyframeGetValueNegativeTimeAsserts(void **state) { keyframe_t keyframes[2] = { { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR }, { .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR }, }; expect_assert_failure(keyframeGetValue(keyframes, 2, -1.0f)); } static void test_keyframeGetValueUnsortedKeyframesAsserts(void **state) { keyframe_t keyframes[2] = { { .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR }, { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR }, }; expect_assert_failure(keyframeGetValue(keyframes, 2, 0.0f)); } static void test_keyframeGetValueLaterKeyframeOutOfOrderAsserts(void **state) { // The first pair is sorted, but the third keyframe is out of order - the // check must walk the whole array, not just the first pair. keyframe_t keyframes[3] = { { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR }, { .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR }, { .time = 0.5f, .value = 5.0f, .easing = EASING_LINEAR }, }; expect_assert_failure(keyframeGetValue(keyframes, 3, 0.0f)); } static void test_keyframeGetValueEqualConsecutiveTimesDoesNotAssert(void **state) { // Equal (non-decreasing) times are allowed - only strictly decreasing // times should trigger the sorted check. keyframe_t keyframes[3] = { { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR }, { .time = 0.0f, .value = 5.0f, .easing = EASING_LINEAR }, { .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR }, }; assert_float_equal(keyframeGetValue(keyframes, 3, 0.0f), 5.0f, 0.0001f); } int main(int argc, char **argv) { const struct CMUnitTest tests[] = { cmocka_unit_test(test_keyframeGetValueSingleSegmentLinear), cmocka_unit_test(test_keyframeGetValueMultiSegment), cmocka_unit_test(test_keyframeGetValueDescendingValues), cmocka_unit_test(test_keyframeGetValueAppliesEasing), cmocka_unit_test(test_keyframeGetValueNonZeroStartTime), cmocka_unit_test(test_keyframeGetValueBeforeFirstKeyframeIsNaN), cmocka_unit_test(test_keyframeGetValueAtOrAfterLastKeyframeClampsToLastValue), cmocka_unit_test(test_keyframeGetValueSingleKeyframeAtOrAfterClampsToValue), cmocka_unit_test(test_keyframeGetValueNullKeyframesAsserts), cmocka_unit_test(test_keyframeGetValueZeroCountAsserts), cmocka_unit_test(test_keyframeGetValueNegativeTimeAsserts), cmocka_unit_test(test_keyframeGetValueUnsortedKeyframesAsserts), cmocka_unit_test(test_keyframeGetValueLaterKeyframeOutOfOrderAsserts), cmocka_unit_test(test_keyframeGetValueEqualConsecutiveTimesDoesNotAssert), }; return cmocka_run_group_tests(tests, NULL, NULL); }