• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 @file:OptIn(ExperimentalCoroutinesApi::class)
18 
19 package com.android.mechanics
20 
21 import android.util.Log
22 import android.util.Log.TerribleFailureHandler
23 import androidx.compose.runtime.LaunchedEffect
24 import androidx.compose.runtime.mutableFloatStateOf
25 import androidx.compose.runtime.mutableStateOf
26 import androidx.compose.runtime.snapshotFlow
27 import androidx.compose.ui.test.ExperimentalTestApi
28 import androidx.compose.ui.test.TestMonotonicFrameClock
29 import androidx.compose.ui.test.junit4.createComposeRule
30 import androidx.test.ext.junit.runners.AndroidJUnit4
31 import com.android.mechanics.spec.BreakpointKey
32 import com.android.mechanics.spec.DirectionalMotionSpec
33 import com.android.mechanics.spec.Guarantee
34 import com.android.mechanics.spec.InputDirection
35 import com.android.mechanics.spec.Mapping
36 import com.android.mechanics.spec.MotionSpec
37 import com.android.mechanics.spec.builder
38 import com.android.mechanics.spec.reverseBuilder
39 import com.android.mechanics.testing.DefaultSprings.matStandardDefault
40 import com.android.mechanics.testing.DefaultSprings.matStandardFast
41 import com.android.mechanics.testing.MotionValueToolkit
42 import com.android.mechanics.testing.MotionValueToolkit.Companion.dataPoints
43 import com.android.mechanics.testing.MotionValueToolkit.Companion.input
44 import com.android.mechanics.testing.MotionValueToolkit.Companion.isStable
45 import com.android.mechanics.testing.MotionValueToolkit.Companion.output
46 import com.android.mechanics.testing.VerifyTimeSeriesResult.AssertTimeSeriesMatchesGolden
47 import com.android.mechanics.testing.VerifyTimeSeriesResult.SkipGoldenVerification
48 import com.android.mechanics.testing.goldenTest
49 import com.google.common.truth.Truth.assertThat
50 import com.google.common.truth.Truth.assertWithMessage
51 import kotlinx.coroutines.CoroutineScope
52 import kotlinx.coroutines.ExperimentalCoroutinesApi
53 import kotlinx.coroutines.launch
54 import kotlinx.coroutines.test.TestCoroutineScheduler
55 import kotlinx.coroutines.test.TestScope
56 import kotlinx.coroutines.test.runTest
57 import kotlinx.coroutines.withContext
58 import org.junit.Rule
59 import org.junit.Test
60 import org.junit.rules.ExternalResource
61 import org.junit.runner.RunWith
62 import platform.test.motion.MotionTestRule
63 import platform.test.motion.testing.createGoldenPathManager
64 
65 @RunWith(AndroidJUnit4::class)
66 class MotionValueTest {
67     private val goldenPathManager =
68         createGoldenPathManager("frameworks/libs/systemui/mechanics/tests/goldens")
69 
70     @get:Rule(order = 0) val rule = createComposeRule()
71     @get:Rule(order = 1) val motion = MotionTestRule(MotionValueToolkit(rule), goldenPathManager)
72     @get:Rule(order = 2) val wtfLog = WtfLogRule()
73 
74     @Test
75     fun emptySpec_outputMatchesInput_withoutAnimation() =
76         motion.goldenTest(
77             spec = MotionSpec.Empty,
78             verifyTimeSeries = {
79                 // Output always matches the input
80                 assertThat(output).containsExactlyElementsIn(input).inOrder()
81                 // There must never be an ongoing animation.
82                 assertThat(isStable).doesNotContain(false)
83 
84                 AssertTimeSeriesMatchesGolden
85             },
86         ) {
87             animateValueTo(100f)
88         }
89 
90     // TODO the tests should describe the expected values not only in terms of goldens, but
91     // also explicitly in verifyTimeSeries
92 
93     @Test
94     fun changingInput_addsAnimationToMapping_becomesStable() =
95         motion.goldenTest(
96             spec =
97                 specBuilder(Mapping.Zero)
98                     .toBreakpoint(1f)
99                     .completeWith(Mapping.Linear(factor = 0.5f))
100         ) {
101             animateValueTo(1.1f, changePerFrame = 0.5f)
102             while (underTest.isStable) {
103                 updateValue(input + 0.5f)
104                 awaitFrames()
105             }
106         }
107 
108     @Test
109     fun segmentChange_inMaxDirection_animatedWhenReachingBreakpoint() =
110         motion.goldenTest(
111             spec = specBuilder(Mapping.Zero).toBreakpoint(1f).completeWith(Mapping.One)
112         ) {
113             animateValueTo(1f, changePerFrame = 0.5f)
114             awaitStable()
115         }
116 
117     @Test
118     fun segmentChange_inMinDirection_animatedWhenReachingBreakpoint() =
119         motion.goldenTest(
120             initialValue = 2f,
121             initialDirection = InputDirection.Min,
122             spec = specBuilder(Mapping.Zero).toBreakpoint(1f).completeWith(Mapping.One),
123         ) {
124             animateValueTo(1f, changePerFrame = 0.5f)
125             awaitStable()
126         }
127 
128     @Test
129     fun segmentChange_inMaxDirection_springAnimationStartedRetroactively() =
130         motion.goldenTest(
131             spec = specBuilder(Mapping.Zero).toBreakpoint(.75f).completeWith(Mapping.One)
132         ) {
133             animateValueTo(1f, changePerFrame = 0.5f)
134             awaitStable()
135         }
136 
137     @Test
138     fun segmentChange_inMinDirection_springAnimationStartedRetroactively() =
139         motion.goldenTest(
140             initialValue = 2f,
141             initialDirection = InputDirection.Min,
142             spec = specBuilder(Mapping.Zero).toBreakpoint(1.25f).completeWith(Mapping.One),
143         ) {
144             animateValueTo(1f, changePerFrame = 0.5f)
145             awaitStable()
146         }
147 
148     @Test
149     fun segmentChange_guaranteeNone_springAnimatesIndependentOfInput() =
150         motion.goldenTest(
151             spec =
152                 specBuilder(Mapping.Zero)
153                     .toBreakpoint(1f)
154                     .completeWith(Mapping.One, guarantee = Guarantee.None)
155         ) {
156             animateValueTo(5f, changePerFrame = 0.5f)
157             awaitStable()
158         }
159 
160     @Test
161     fun segmentChange_guaranteeInputDelta_springCompletesWithinDistance() =
162         motion.goldenTest(
163             spec =
164                 specBuilder(Mapping.Zero)
165                     .toBreakpoint(1f)
166                     .completeWith(Mapping.One, guarantee = Guarantee.InputDelta(3f))
167         ) {
168             animateValueTo(4f, changePerFrame = 0.5f)
169         }
170 
171     @Test
172     fun segmentChange_guaranteeGestureDragDelta_springCompletesWithinDistance() =
173         motion.goldenTest(
174             spec =
175                 specBuilder(Mapping.Zero)
176                     .toBreakpoint(1f)
177                     .completeWith(Mapping.One, guarantee = Guarantee.GestureDragDelta(3f))
178         ) {
179             animateValueTo(1f, changePerFrame = 0.5f)
180             while (!underTest.isStable) {
181                 gestureContext.dragOffset += 0.5f
182                 awaitFrames()
183             }
184         }
185 
186     @Test
187     fun segmentChange_appliesOutputVelocity_atSpringStart() =
188         motion.goldenTest(spec = specBuilder().toBreakpoint(10f).completeWith(Mapping.Fixed(20f))) {
189             animateValueTo(11f, changePerFrame = 3f)
190             awaitStable()
191         }
192 
193     @Test
194     fun segmentChange_appliesOutputVelocity_springVelocityIsNotAppliedTwice() =
195         motion.goldenTest(
196             spec =
197                 specBuilder()
198                     .toBreakpoint(10f)
199                     .continueWith(Mapping.Linear(factor = 1f, offset = 20f))
200                     .toBreakpoint(20f)
201                     .completeWith(Mapping.Fixed(40f))
202         ) {
203             animateValueTo(21f, changePerFrame = 3f)
204             awaitStable()
205         }
206 
207     @Test
208     fun specChange_shiftSegmentBackwards_doesNotAnimateWithinSegment_animatesSegmentChange() {
209         fun generateSpec(offset: Float) =
210             specBuilder(Mapping.Zero)
211                 .toBreakpoint(offset, B1)
212                 .jumpTo(1f)
213                 .continueWithTargetValue(2f)
214                 .toBreakpoint(offset + 1f, B2)
215                 .completeWith(Mapping.Zero)
216 
217         motion.goldenTest(spec = generateSpec(0f), initialValue = .5f) {
218             var offset = 0f
219             repeat(4) {
220                 offset -= .2f
221                 underTest.spec = generateSpec(offset)
222                 awaitFrames()
223             }
224             awaitStable()
225         }
226     }
227 
228     @Test
229     fun specChange_shiftSegmentForward_doesNotAnimateWithinSegment_animatesSegmentChange() {
230         fun generateSpec(offset: Float) =
231             specBuilder(Mapping.Zero)
232                 .toBreakpoint(offset, B1)
233                 .jumpTo(1f)
234                 .continueWithTargetValue(2f)
235                 .toBreakpoint(offset + 1f, B2)
236                 .completeWith(Mapping.Zero)
237 
238         motion.goldenTest(spec = generateSpec(0f), initialValue = .5f) {
239             var offset = 0f
240             repeat(4) {
241                 offset += .2f
242                 underTest.spec = generateSpec(offset)
243                 awaitFrames()
244             }
245             awaitStable()
246         }
247     }
248 
249     @Test
250     fun directionChange_maxToMin_changesSegmentWithDirectionChange() =
251         motion.goldenTest(
252             spec = specBuilder(Mapping.Zero).toBreakpoint(1f).completeWith(Mapping.One),
253             initialValue = 2f,
254             initialDirection = InputDirection.Max,
255             directionChangeSlop = 3f,
256         ) {
257             animateValueTo(-2f, changePerFrame = 0.5f)
258             awaitStable()
259         }
260 
261     @Test
262     fun directionChange_minToMax_changesSegmentWithDirectionChange() =
263         motion.goldenTest(
264             spec = specBuilder(Mapping.Zero).toBreakpoint(1f).completeWith(Mapping.One),
265             initialValue = 0f,
266             initialDirection = InputDirection.Min,
267             directionChangeSlop = 3f,
268         ) {
269             animateValueTo(4f, changePerFrame = 0.5f)
270             awaitStable()
271         }
272 
273     @Test
274     fun directionChange_maxToMin_appliesGuarantee_afterDirectionChange() =
275         motion.goldenTest(
276             spec =
277                 specBuilder(Mapping.Zero)
278                     .toBreakpoint(1f)
279                     .completeWith(Mapping.One, guarantee = Guarantee.InputDelta(1f)),
280             initialValue = 2f,
281             initialDirection = InputDirection.Max,
282             directionChangeSlop = 3f,
283         ) {
284             animateValueTo(-2f, changePerFrame = 0.5f)
285             awaitStable()
286         }
287 
288     @Test
289     fun traverseSegments_maxDirection_noGuarantee_addsDiscontinuityToOngoingAnimation() =
290         motion.goldenTest(
291             spec =
292                 specBuilder(Mapping.Zero)
293                     .toBreakpoint(1f)
294                     .continueWith(Mapping.One)
295                     .toBreakpoint(2f)
296                     .completeWith(Mapping.Two)
297         ) {
298             animateValueTo(3f, changePerFrame = 0.2f)
299             awaitStable()
300         }
301 
302     @Test
303     fun traverseSegmentsInOneFrame_noGuarantee_combinesDiscontinuity() =
304         motion.goldenTest(
305             spec =
306                 specBuilder(Mapping.Zero)
307                     .toBreakpoint(1f)
308                     .continueWith(Mapping.One)
309                     .toBreakpoint(2f)
310                     .completeWith(Mapping.Two)
311         ) {
312             updateValue(2.5f)
313             awaitStable()
314         }
315 
316     @Test
317     fun traverseSegmentsInOneFrame_withGuarantee_appliesGuarantees() =
318         motion.goldenTest(
319             spec =
320                 specBuilder(Mapping.Zero)
321                     .toBreakpoint(1f)
322                     .jumpBy(5f, guarantee = Guarantee.InputDelta(.9f))
323                     .continueWithConstantValue()
324                     .toBreakpoint(2f)
325                     .jumpBy(1f, guarantee = Guarantee.InputDelta(.9f))
326                     .continueWithConstantValue()
327                     .complete()
328         ) {
329             updateValue(2.1f)
330             awaitStable()
331         }
332 
333     @Test
334     fun traverseSegmentsInOneFrame_withDirectionChange_appliesGuarantees() =
335         motion.goldenTest(
336             spec =
337                 specBuilder(Mapping.Zero)
338                     .toBreakpoint(1f)
339                     .continueWith(Mapping.One, guarantee = Guarantee.InputDelta(1f))
340                     .toBreakpoint(2f)
341                     .completeWith(Mapping.Two),
342             initialValue = 2.5f,
343             initialDirection = InputDirection.Max,
344             directionChangeSlop = 1f,
345         ) {
346             updateValue(.5f)
347             animateValueTo(0f)
348             awaitStable()
349         }
350 
351     @Test
352     fun changeDirection_flipsBetweenDirectionalSegments() {
353         val spec =
354             MotionSpec(
355                 maxDirection = forwardSpecBuilder(Mapping.Zero).complete(),
356                 minDirection = reverseSpecBuilder(Mapping.One).complete(),
357             )
358 
359         motion.goldenTest(
360             spec = spec,
361             initialValue = 2f,
362             initialDirection = InputDirection.Max,
363             directionChangeSlop = 1f,
364         ) {
365             animateValueTo(0f)
366             awaitStable()
367         }
368     }
369 
370     @Test
371     fun derivedValue_reflectsInputChangeInSameFrame() {
372         motion.goldenTest(
373             spec = specBuilder(Mapping.Zero).toBreakpoint(0.5f).completeWith(Mapping.One),
374             createDerived = { primary ->
375                 listOf(MotionValue.createDerived(primary, MotionSpec.Empty, label = "derived"))
376             },
377             verifyTimeSeries = {
378                 // the output of the derived value must match the primary value
379                 assertThat(output)
380                     .containsExactlyElementsIn(dataPoints<Float>("derived-output"))
381                     .inOrder()
382                 // and its never animated.
383                 assertThat(dataPoints<Float>("derived-isStable")).doesNotContain(false)
384 
385                 AssertTimeSeriesMatchesGolden
386             },
387         ) {
388             animateValueTo(1f, changePerFrame = 0.1f)
389             awaitStable()
390         }
391     }
392 
393     @Test
394     fun derivedValue_hasAnimationLifecycleOnItsOwn() {
395         motion.goldenTest(
396             spec = specBuilder(Mapping.Zero).toBreakpoint(0.5f).completeWith(Mapping.One),
397             createDerived = { primary ->
398                 listOf(
399                     MotionValue.createDerived(
400                         primary,
401                         specBuilder(Mapping.One).toBreakpoint(0.5f).completeWith(Mapping.Zero),
402                         label = "derived",
403                     )
404                 )
405             },
406         ) {
407             animateValueTo(1f, changePerFrame = 0.1f)
408             awaitStable()
409         }
410     }
411 
412     @Test
413     fun nonFiniteNumbers_producesNaN_recoversOnSubsequentFrames() {
414         motion.goldenTest(
415             spec = specBuilder(Mapping { if (it >= 1f) Float.NaN else 0f }).complete(),
416             verifyTimeSeries = {
417                 assertThat(output.drop(1).take(5))
418                     .containsExactlyElementsIn(listOf(0f, Float.NaN, Float.NaN, 0f, 0f))
419                     .inOrder()
420                 SkipGoldenVerification
421             },
422         ) {
423             animatedInputSequence(0f, 1f, 1f, 0f, 0f)
424         }
425 
426         assertThat(wtfLog.loggedFailures).isEmpty()
427     }
428 
429     @Test
430     fun nonFiniteNumbers_segmentChange_skipsAnimation() {
431         motion.goldenTest(
432             spec = MotionSpec.Empty,
433             verifyTimeSeries = {
434                 // The mappings produce a non-finite number during a segment change.
435                 // The animation thereof is skipped to avoid poisoning the state with non-finite
436                 // numbers
437                 assertThat(output.drop(1).take(5))
438                     .containsExactlyElementsIn(listOf(0f, 1f, Float.NaN, 0f, 0f))
439                     .inOrder()
440                 SkipGoldenVerification
441             },
442         ) {
443             animatedInputSequence(0f, 1f)
444             underTest.spec =
445                 specBuilder()
446                     .toBreakpoint(0f)
447                     .completeWith(Mapping { if (it >= 1f) Float.NaN else 0f })
448             awaitFrames()
449 
450             animatedInputSequence(0f, 0f)
451         }
452 
453         assertThat(wtfLog.loggedFailures).hasSize(1)
454         assertThat(wtfLog.loggedFailures.first()).startsWith("Delta between mappings is undefined")
455     }
456 
457     @Test
458     fun nonFiniteNumbers_segmentTraverse_skipsAnimation() {
459         motion.goldenTest(
460             spec =
461                 specBuilder(Mapping.Zero)
462                     .toBreakpoint(1f)
463                     .completeWith(Mapping { if (it < 2f) Float.NaN else 2f }),
464             verifyTimeSeries = {
465                 // The mappings produce a non-finite number during a breakpoint traversal.
466                 // The animation thereof is skipped to avoid poisoning the state with non-finite
467                 // numbers
468                 assertThat(output.drop(1).take(6))
469                     .containsExactlyElementsIn(listOf(0f, 0f, Float.NaN, Float.NaN, 2f, 2f))
470                     .inOrder()
471                 SkipGoldenVerification
472             },
473         ) {
474             animatedInputSequence(0f, 0.5f, 1f, 1.5f, 2f, 3f)
475         }
476         assertThat(wtfLog.loggedFailures).hasSize(1)
477         assertThat(wtfLog.loggedFailures.first())
478             .startsWith("Delta between breakpoints is undefined")
479     }
480 
481     @Test
482     fun keepRunning_concurrentInvocationThrows() = runTestWithFrameClock { testScheduler, _ ->
483         val underTest = MotionValue({ 1f }, FakeGestureContext, label = "Foo")
484         val realJob = launch { underTest.keepRunning() }
485         testScheduler.runCurrent()
486 
487         assertThat(realJob.isActive).isTrue()
488         try {
489             underTest.keepRunning()
490             // keepRunning returns Nothing, will never get here
491         } catch (e: Throwable) {
492             assertThat(e).isInstanceOf(IllegalStateException::class.java)
493             assertThat(e).hasMessageThat().contains("MotionValue(Foo) is already running")
494         }
495         assertThat(realJob.isActive).isTrue()
496         realJob.cancel()
497     }
498 
499     @Test
500     fun keepRunning_suspendsWithoutAnAnimation() = runTest {
501         val input = mutableFloatStateOf(0f)
502         val spec = specBuilder(Mapping.Zero).toBreakpoint(1f).completeWith(Mapping.One)
503         val underTest = MotionValue(input::value, FakeGestureContext, spec)
504         rule.setContent { LaunchedEffect(Unit) { underTest.keepRunning() } }
505 
506         val inspector = underTest.debugInspector()
507         var framesCount = 0
508         backgroundScope.launch { snapshotFlow { inspector.frame }.collect { framesCount++ } }
509 
510         rule.awaitIdle()
511         framesCount = 0
512         rule.mainClock.autoAdvance = false
513 
514         assertThat(inspector.isActive).isTrue()
515         assertThat(inspector.isAnimating).isFalse()
516 
517         // Update the value, but WITHOUT causing an animation
518         input.floatValue = 0.5f
519         rule.awaitIdle()
520 
521         // Still on the old frame..
522         assertThat(framesCount).isEqualTo(0)
523         // ... [underTest] is now waiting for an animation frame
524         assertThat(inspector.isAnimating).isTrue()
525 
526         rule.mainClock.advanceTimeByFrame()
527         rule.awaitIdle()
528 
529         // Produces the frame..
530         assertThat(framesCount).isEqualTo(1)
531         // ... and is suspended again.
532         assertThat(inspector.isAnimating).isTrue()
533 
534         rule.mainClock.advanceTimeByFrame()
535         rule.awaitIdle()
536 
537         // Produces the frame..
538         assertThat(framesCount).isEqualTo(2)
539         // ... and is suspended again.
540         assertThat(inspector.isAnimating).isFalse()
541 
542         rule.mainClock.autoAdvance = true
543         rule.awaitIdle()
544         // Ensure that no more frames are produced
545         assertThat(framesCount).isEqualTo(2)
546     }
547 
548     @Test
549     fun keepRunning_remainsActiveWhileAnimating() = runTest {
550         val input = mutableFloatStateOf(0f)
551         val spec = specBuilder(Mapping.Zero).toBreakpoint(1f).completeWith(Mapping.One)
552         val underTest = MotionValue(input::value, FakeGestureContext, spec)
553         rule.setContent { LaunchedEffect(Unit) { underTest.keepRunning() } }
554 
555         val inspector = underTest.debugInspector()
556         var framesCount = 0
557         backgroundScope.launch { snapshotFlow { inspector.frame }.collect { framesCount++ } }
558 
559         rule.awaitIdle()
560         framesCount = 0
561         rule.mainClock.autoAdvance = false
562 
563         assertThat(inspector.isActive).isTrue()
564         assertThat(inspector.isAnimating).isFalse()
565 
566         // Update the value, WITH triggering an animation
567         input.floatValue = 1.5f
568         rule.awaitIdle()
569 
570         // Still on the old frame..
571         assertThat(framesCount).isEqualTo(0)
572         // ... [underTest] is now waiting for an animation frame
573         assertThat(inspector.isAnimating).isTrue()
574 
575         // A couple frames should be generated without pausing
576         repeat(5) {
577             rule.mainClock.advanceTimeByFrame()
578             rule.awaitIdle()
579 
580             // The spring is still settling...
581             assertThat(inspector.frame.isStable).isFalse()
582             // ... animation keeps going ...
583             assertThat(inspector.isAnimating).isTrue()
584             // ... and frames are produces...
585             assertThat(framesCount).isEqualTo(it + 1)
586         }
587 
588         val timeBeforeAutoAdvance = rule.mainClock.currentTime
589 
590         // But this will stop as soon as the animation is finished. Skip forward.
591         rule.mainClock.autoAdvance = true
592         rule.awaitIdle()
593 
594         // At which point the spring is stable again...
595         assertThat(inspector.frame.isStable).isTrue()
596         // ... and animations are suspended again.
597         assertThat(inspector.isAnimating).isFalse()
598 
599         rule.awaitIdle()
600 
601         // Stabilizing the spring during awaitIdle() took 160ms (obtained from looking at reference
602         // test runs). That time is expected to be 100% reproducible, given the starting
603         // state/configuration of the spring before awaitIdle().
604         assertThat(rule.mainClock.currentTime).isEqualTo(timeBeforeAutoAdvance + 160)
605     }
606 
607     @Test
608     fun keepRunningWhile_stopRunningWhileStable_endsImmediately() = runTest {
609         val input = mutableFloatStateOf(0f)
610         val spec = specBuilder(Mapping.Zero).toBreakpoint(1f).completeWith(Mapping.One)
611         val underTest = MotionValue(input::value, FakeGestureContext, spec)
612 
613         val continueRunning = mutableStateOf(true)
614 
615         rule.setContent {
616             LaunchedEffect(Unit) { underTest.keepRunningWhile { continueRunning.value } }
617         }
618 
619         val inspector = underTest.debugInspector()
620 
621         rule.awaitIdle()
622 
623         assertWithMessage("isActive").that(inspector.isActive).isTrue()
624         assertWithMessage("isAnimating").that(inspector.isAnimating).isFalse()
625 
626         val timeBeforeStopRunning = rule.mainClock.currentTime
627         continueRunning.value = false
628         rule.awaitIdle()
629 
630         assertWithMessage("isActive").that(inspector.isActive).isFalse()
631         assertWithMessage("isAnimating").that(inspector.isAnimating).isFalse()
632         assertThat(rule.mainClock.currentTime).isEqualTo(timeBeforeStopRunning)
633     }
634 
635     @Test
636     fun debugInspector_sameInstance_whileInUse() {
637         val underTest = MotionValue({ 1f }, FakeGestureContext)
638 
639         val originalInspector = underTest.debugInspector()
640         assertThat(underTest.debugInspector()).isSameInstanceAs(originalInspector)
641     }
642 
643     @Test
644     fun debugInspector_newInstance_afterUnused() {
645         val underTest = MotionValue({ 1f }, FakeGestureContext)
646 
647         val originalInspector = underTest.debugInspector()
648         originalInspector.dispose()
649         assertThat(underTest.debugInspector()).isNotSameInstanceAs(originalInspector)
650     }
651 
652     @OptIn(ExperimentalTestApi::class)
653     private fun runTestWithFrameClock(
654         testBody:
655             suspend CoroutineScope.(
656                 testScheduler: TestCoroutineScheduler, backgroundScope: CoroutineScope,
657             ) -> Unit
658     ) = runTest {
659         val testScope: TestScope = this
660         withContext(TestMonotonicFrameClock(testScope, FrameDelayNanos)) {
661             testBody(testScope.testScheduler, testScope.backgroundScope)
662         }
663     }
664 
665     class WtfLogRule : ExternalResource() {
666         val loggedFailures = mutableListOf<String>()
667 
668         private lateinit var oldHandler: TerribleFailureHandler
669 
670         override fun before() {
671             oldHandler =
672                 Log.setWtfHandler { tag, what, _ ->
673                     if (tag == MotionValue.TAG) {
674                         loggedFailures.add(checkNotNull(what.message))
675                     }
676                 }
677         }
678 
679         override fun after() {
680             Log.setWtfHandler(oldHandler)
681         }
682     }
683 
684     companion object {
685         val B1 = BreakpointKey("breakpoint1")
686         val B2 = BreakpointKey("breakpoint2")
687         val FakeGestureContext =
688             object : GestureContext {
689                 override val direction: InputDirection
690                     get() = InputDirection.Max
691 
692                 override val dragOffset: Float
693                     get() = 0f
694             }
695         private val FrameDelayNanos: Long = 16_000_000L
696 
697         fun specBuilder(firstSegment: Mapping = Mapping.Identity) =
698             MotionSpec.builder(
699                 defaultSpring = matStandardDefault,
700                 resetSpring = matStandardFast,
701                 initialMapping = firstSegment,
702             )
703 
704         fun forwardSpecBuilder(firstSegment: Mapping = Mapping.Identity) =
705             DirectionalMotionSpec.builder(
706                 defaultSpring = matStandardDefault,
707                 initialMapping = firstSegment,
708             )
709 
710         fun reverseSpecBuilder(firstSegment: Mapping = Mapping.Identity) =
711             DirectionalMotionSpec.reverseBuilder(
712                 defaultSpring = matStandardDefault,
713                 initialMapping = firstSegment,
714             )
715     }
716 }
717