Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 9a8e8b7b authored by Luca Zuccarini's avatar Luca Zuccarini
Browse files

[2/3] Create a new motion testing toolkit based on AnimatorTestRule.

This toolkit will allow us to test all Animator and Spring-based motion
using AnimatorTestRule.

Bug: 323863002
Flag: EXEMPT test only
Test: atest AnimatorTestRuleToolkitTest
Change-Id: Id5eb04abe896d6d58920ff3eaf1867e401f2b5ee
parent 16a7bf59
Loading
Loading
Loading
Loading
+4 −0
Original line number Diff line number Diff line
@@ -35,4 +35,8 @@ java_library {
        "androidx.test.rules",
        "mockito-target-inline-minus-junit4",
    ],
    static_libs: [
        "PlatformMotionTesting",
        "kotlinx_coroutines_test",
    ],
}
+159 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.animation

import android.animation.AnimatorTestRuleToolkit.Companion.TAG
import android.util.Log
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.flow.takeWhile
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import platform.test.motion.MotionTestRule
import platform.test.motion.RecordedMotion
import platform.test.motion.RecordedMotion.Companion.create
import platform.test.motion.golden.DataPoint
import platform.test.motion.golden.Feature
import platform.test.motion.golden.FrameId
import platform.test.motion.golden.TimeSeries
import platform.test.motion.golden.TimeSeriesCaptureScope
import platform.test.motion.golden.TimestampFrameId

class AnimatorTestRuleToolkit(val animatorTestRule: AnimatorTestRule, val testScope: TestScope) {
    internal companion object {
        const val TAG = "AnimatorRuleToolkit"
    }
}

/**
 * Controls the timing of the motion recording.
 *
 * The time series is recorded while the [recording] function is running.
 */
class MotionControl(val recording: MotionControlFn)

typealias MotionControlFn = suspend MotionControlScope.() -> Unit

interface MotionControlScope {
    /** Waits until [check] returns true. Invoked on each frame. */
    suspend fun awaitCondition(check: () -> Boolean)

    /** Waits for [count] frames to be processed. */
    suspend fun awaitFrames(count: Int = 1)
}

/** Defines the sampling of features during a test run. */
data class AnimatorRuleRecordingSpec<T>(
    /** The root `observing` object, available in [timeSeriesCapture]'s [TimeSeriesCaptureScope]. */
    val captureRoot: T,

    /** The timing for the recording. */
    val motionControl: MotionControl,

    /** Time interval between frame captures, in milliseconds. */
    val frameDurationMs: Long = 16L,

    /**  Produces the time-series, invoked on each animation frame. */
    val timeSeriesCapture: TimeSeriesCaptureScope<T>.() -> Unit,
)

/** Records the time-series of the features specified in [recordingSpec]. */
fun <T> MotionTestRule<AnimatorTestRuleToolkit>.recordMotion(
    recordingSpec: AnimatorRuleRecordingSpec<T>,
): RecordedMotion {
    with(toolkit.animatorTestRule) {
        val frameIdCollector = mutableListOf<FrameId>()
        val propertyCollector = mutableMapOf<String, MutableList<DataPoint<*>>>()

        fun recordFrame(frameId: FrameId) {
            Log.i(TAG, "recordFrame($frameId)")
            frameIdCollector.add(frameId)
            recordingSpec.timeSeriesCapture.invoke(
                TimeSeriesCaptureScope(recordingSpec.captureRoot, propertyCollector)
            )
        }

        val motionControl =
            MotionControlImpl(
                toolkit.animatorTestRule,
                toolkit.testScope,
                recordingSpec.frameDurationMs,
                recordingSpec.motionControl,
            )

        Log.i(TAG, "recordMotion() begin recording")

        val startFrameTime = currentTime
        while (!motionControl.recordingEnded) {
            recordFrame(TimestampFrameId(currentTime - startFrameTime))
            motionControl.nextFrame()
        }

        Log.i(TAG, "recordMotion() end recording")

        val timeSeries =
            TimeSeries(
                frameIdCollector.toList(),
                propertyCollector.entries.map { entry -> Feature(entry.key, entry.value) },
            )

        return create(timeSeries, null)
    }
}

@OptIn(ExperimentalCoroutinesApi::class)
private class MotionControlImpl(
    val animatorTestRule: AnimatorTestRule,
    val testScope: TestScope,
    val frameMs: Long,
    motionControl: MotionControl,
) : MotionControlScope {
    private val recordingJob = motionControl.recording.launch()

    private val frameEmitter = MutableStateFlow<Long>(0)
    private val onFrame = frameEmitter.asStateFlow()

    var recordingEnded: Boolean = false

    fun nextFrame() {
        animatorTestRule.advanceTimeBy(frameMs)

        frameEmitter.tryEmit(animatorTestRule.currentTime)
        testScope.runCurrent()

        if (recordingJob.isCompleted) {
            recordingEnded = true
        }
    }

    override suspend fun awaitCondition(check: () -> Boolean) {
        onFrame.takeWhile { !check() }.collect {}
    }

    override suspend fun awaitFrames(count: Int) {
        onFrame.take(count).collect {}
    }

    private fun MotionControlFn.launch(): Job {
        val function = this
        return testScope.launch { function() }
    }
}
+4 −0
Original line number Diff line number Diff line
@@ -29,13 +29,17 @@ android_test {
        "src/**/*.kt",
        "src/**/I*.aidl",
    ],
    asset_dirs: ["goldens"],
    resource_dirs: ["res"],
    static_libs: [
        "PlatformMotionTesting",
        "androidx.core_core-animation",
        "androidx.core_core-ktx",
        "androidx.test.ext.junit",
        "androidx.test.rules",
        "androidx.test.ext.junit",
        "hamcrest-library",
        "kotlinx_coroutines_test",
        "mockito-target-inline-minus-junit4",
        "testables",
        "truth",
+64 −0
Original line number Diff line number Diff line
{
  "frame_ids": [
    0,
    20,
    40,
    60,
    80,
    100,
    120,
    140,
    160,
    180,
    200,
    220,
    240,
    260,
    280,
    300,
    320,
    340,
    360,
    380,
    400,
    420,
    440,
    460,
    480,
    500
  ],
  "features": [
    {
      "name": "value",
      "type": "float",
      "data_points": [
        1,
        0.9960574,
        0.98429155,
        0.9648882,
        0.9381534,
        0.9045085,
        0.8644843,
        0.818712,
        0.76791346,
        0.7128896,
        0.65450853,
        0.5936906,
        0.5313952,
        0.46860474,
        0.40630943,
        0.34549147,
        0.2871104,
        0.23208654,
        0.181288,
        0.13551569,
        0.09549153,
        0.061846733,
        0.035111785,
        0.015708387,
        0.003942609,
        0
      ]
    }
  ]
}
+48 −0
Original line number Diff line number Diff line
{
  "frame_ids": [
    0,
    16,
    32,
    48,
    64,
    80,
    96,
    112,
    128,
    144,
    160,
    176,
    192,
    208,
    224,
    240,
    256,
    272
  ],
  "features": [
    {
      "name": "value",
      "type": "float",
      "data_points": [
        1,
        0.9488604,
        0.83574325,
        0.7016156,
        0.5691678,
        0.4497436,
        0.34789434,
        0.26431116,
        0.19766562,
        0.14572789,
        0.10601636,
        0.076149896,
        0.05401709,
        0.037837274,
        0.026161024,
        0.017839976,
        0.011983856,
        0.007914998
      ]
    }
  ]
}
Loading