MIDI: Add test for MidiEventMultiScheduler

This CL adds a new folder in frameworks/base for MIDI tests.
This folder can act as a folder for unit tests.

A new test is added for MidiEventMultiScheduler.
com.android.internal.midi is used so this test has to be here.

Bug: 266473128
Test: atest MidiTests --rebuild-module-info
Change-Id: If0d33bd46f8c8adf3a3425888f469f942594aca9
diff --git a/tests/MidiTests/Android.bp b/tests/MidiTests/Android.bp
new file mode 100644
index 0000000..254770d
--- /dev/null
+++ b/tests/MidiTests/Android.bp
@@ -0,0 +1,40 @@
+//
+// Copyright (C) 2023 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 {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_base_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_test {
+    name: "MidiTests",
+    srcs: ["**/*.java"],
+    static_libs: [
+        "androidx.test.rules",
+        "mockito-target-inline-minus-junit4",
+        "platform-test-annotations",
+        "services.midi",
+        "truth-prebuilt",
+    ],
+    jni_libs: ["libdexmakerjvmtiagent"],
+    certificate: "platform",
+    platform_apis: true,
+    test_suites: ["device-tests"],
+}
diff --git a/tests/MidiTests/AndroidManifest.xml b/tests/MidiTests/AndroidManifest.xml
new file mode 100644
index 0000000..0ee1b449
--- /dev/null
+++ b/tests/MidiTests/AndroidManifest.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2023 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.server.midi" >
+
+    <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" />
+    <uses-permission android:name="android.permission.MANAGE_USERS" />
+
+    <application android:debuggable="true">
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="com.android.server.midi"
+                     android:label="MidiTests"/>
+</manifest>
diff --git a/tests/MidiTests/AndroidTest.xml b/tests/MidiTests/AndroidTest.xml
new file mode 100644
index 0000000..9320f0a
--- /dev/null
+++ b/tests/MidiTests/AndroidTest.xml
@@ -0,0 +1,30 @@
+<!-- Copyright (C) 2023 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.
+-->
+<configuration description="Runs sample instrumentation test.">
+    <target_preparer class="com.android.tradefed.targetprep.TestFilePushSetup"/>
+    <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
+        <option name="test-file-name" value="MidiTests.apk"/>
+    </target_preparer>
+    <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer"/>
+    <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer"/>
+    <option name="test-suite-tag" value="apct"/>
+    <option name="test-tag" value="MidiTests"/>
+
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest">
+        <option name="package" value="com.android.server.midi"/>
+        <option name="runner" value="androidx.test.runner.AndroidJUnitRunner"/>
+        <option name="hidden-api-checks" value="false"/>
+    </test>
+</configuration>
diff --git a/tests/MidiTests/OWNERS b/tests/MidiTests/OWNERS
new file mode 100644
index 0000000..af273a6
--- /dev/null
+++ b/tests/MidiTests/OWNERS
@@ -0,0 +1 @@
+include /services/midi/OWNERS
diff --git a/tests/MidiTests/TEST_MAPPING b/tests/MidiTests/TEST_MAPPING
new file mode 100644
index 0000000..60416a8
--- /dev/null
+++ b/tests/MidiTests/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "MidiTests"
+    }
+  ]
+}
diff --git a/tests/MidiTests/src/com/android/server/midi/MidiEventMultiSchedulerTest.java b/tests/MidiTests/src/com/android/server/midi/MidiEventMultiSchedulerTest.java
new file mode 100644
index 0000000..1659cc0
--- /dev/null
+++ b/tests/MidiTests/src/com/android/server/midi/MidiEventMultiSchedulerTest.java
@@ -0,0 +1,342 @@
+/*
+ * Copyright (C) 2023 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 com.android.server.midi;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.midi.MidiEventMultiScheduler;
+import com.android.internal.midi.MidiEventScheduler;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Random;
+
+/**
+ * Unit tests for com.android.internal.midi.MidiEventMultiScheduler.
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class MidiEventMultiSchedulerTest {
+    private byte[] generateRandomByteStream(Random rnd, int size) {
+        byte[] output = new byte[size];
+        rnd.nextBytes(output);
+        return output;
+    }
+
+    private void compareByteArrays(byte[] expectedArray, byte[] outputArray) {
+        assertEquals(expectedArray.length, outputArray.length);
+        for (int i = 0; i < outputArray.length; i++) {
+            assertEquals(expectedArray[i], outputArray[i]);
+        }
+    }
+
+    private long timeFromNow(long milliseconds) {
+        return System.nanoTime() + 1000000L * milliseconds;
+    }
+
+    @Test
+    public void testMultiScheduler() {
+        try {
+            MidiEventMultiScheduler multiScheduler = new MidiEventMultiScheduler(3);
+            assertEquals(3, multiScheduler.getNumEventSchedulers());
+            MidiEventScheduler scheduler0 = multiScheduler.getEventScheduler(0);
+            MidiEventScheduler scheduler1 = multiScheduler.getEventScheduler(1);
+            MidiEventScheduler scheduler2 = multiScheduler.getEventScheduler(2);
+
+            scheduler0.add(scheduler0.createScheduledEvent(new byte[]{(byte) 0xf0, (byte) 0xf7},
+                    0, 2, timeFromNow(100)));
+            scheduler1.add(scheduler1.createScheduledEvent(new byte[]{(byte) 0xf1, (byte) 0xf2},
+                    0, 2, timeFromNow(200)));
+            scheduler2.add(scheduler2.createScheduledEvent(new byte[]{(byte) 0xf3, (byte) 0xf4},
+                    0, 2, timeFromNow(300)));
+            scheduler0.add(scheduler0.createScheduledEvent(new byte[]{(byte) 0xf5, (byte) 0xf6},
+                    0, 2, timeFromNow(400)));
+            assertTrue(multiScheduler.waitNextEvent());
+            assertNotNull(scheduler0.getNextEvent(System.nanoTime()));
+            assertNull(scheduler1.getNextEvent(System.nanoTime()));
+            assertNull(scheduler2.getNextEvent(System.nanoTime()));
+            assertTrue(multiScheduler.waitNextEvent());
+            assertNull(scheduler0.getNextEvent(System.nanoTime()));
+            assertNotNull(scheduler1.getNextEvent(System.nanoTime()));
+            assertNull(scheduler2.getNextEvent(System.nanoTime()));
+            assertTrue(multiScheduler.waitNextEvent());
+            assertNull(scheduler0.getNextEvent(System.nanoTime()));
+            assertNull(scheduler1.getNextEvent(System.nanoTime()));
+            assertNotNull(scheduler2.getNextEvent(System.nanoTime()));
+            assertTrue(multiScheduler.waitNextEvent());
+            assertNotNull(scheduler0.getNextEvent(System.nanoTime()));
+            assertNull(scheduler1.getNextEvent(System.nanoTime()));
+            assertNull(scheduler2.getNextEvent(System.nanoTime()));
+        } catch (InterruptedException ex) {
+
+        }
+    }
+
+    @Test
+    public void testSchedulerLargeData() {
+        try {
+            MidiEventMultiScheduler multiScheduler = new MidiEventMultiScheduler(1);
+            assertEquals(1, multiScheduler.getNumEventSchedulers());
+            MidiEventScheduler scheduler0 = multiScheduler.getEventScheduler(0);
+
+            Random rnd = new Random(42);
+
+            final int arraySize = 1000;
+            byte[] expectedArray = generateRandomByteStream(rnd, arraySize);
+
+            scheduler0.add(scheduler0.createScheduledEvent(expectedArray, 0, arraySize,
+                    timeFromNow(100)));
+            assertTrue(multiScheduler.waitNextEvent());
+            MidiEventScheduler.MidiEvent event =
+                    (MidiEventScheduler.MidiEvent) scheduler0.getNextEvent(System.nanoTime());
+            assertNotNull(event);
+            compareByteArrays(expectedArray, event.data);
+        } catch (InterruptedException ex) {
+
+        }
+    }
+
+    @Test
+    public void testSchedulerClose() {
+        try {
+            MidiEventMultiScheduler multiScheduler = new MidiEventMultiScheduler(1);
+            assertEquals(1, multiScheduler.getNumEventSchedulers());
+            MidiEventScheduler scheduler0 = multiScheduler.getEventScheduler(0);
+            scheduler0.close();
+            // After all schedulers are closed, waitNextEvent() should return false.
+            assertFalse(multiScheduler.waitNextEvent());
+        } catch (InterruptedException ex) {
+
+        }
+    }
+
+    @Test
+    public void testSchedulerMultiClose() {
+        try {
+            MidiEventMultiScheduler multiScheduler = new MidiEventMultiScheduler(3);
+            assertEquals(3, multiScheduler.getNumEventSchedulers());
+            multiScheduler.close();
+            // After all schedulers are closed, waitNextEvent() should return false.
+            assertFalse(multiScheduler.waitNextEvent());
+        } catch (InterruptedException ex) {
+
+        }
+    }
+
+    @Test
+    public void testSchedulerNoPreemptiveClose() {
+        try {
+            MidiEventMultiScheduler multiScheduler = new MidiEventMultiScheduler(3);
+            assertEquals(3, multiScheduler.getNumEventSchedulers());
+            MidiEventScheduler scheduler0 = multiScheduler.getEventScheduler(0);
+            MidiEventScheduler scheduler1 = multiScheduler.getEventScheduler(1);
+            MidiEventScheduler scheduler2 = multiScheduler.getEventScheduler(2);
+            scheduler0.close();
+            scheduler1.close();
+            scheduler2.add(scheduler2.createScheduledEvent(new byte[]{(byte) 0xf5, (byte) 0xf6},
+                    0, 2, timeFromNow(100)));
+            assertTrue(multiScheduler.waitNextEvent());
+            scheduler2.close();
+            // After all schedulers are closed, waitNextEvent() should return false.
+            assertFalse(multiScheduler.waitNextEvent());
+        } catch (InterruptedException ex) {
+
+        }
+    }
+
+    @Test
+    public void testSchedulerSpamEvents() {
+        MidiEventMultiScheduler multiScheduler = new MidiEventMultiScheduler(1);
+        assertEquals(1, multiScheduler.getNumEventSchedulers());
+        MidiEventScheduler scheduler0 = multiScheduler.getEventScheduler(0);
+        // Create a msg with size 1
+        byte[] msg = new byte[1];
+        for (int i = 0; i < 1000; i++) {
+            msg[0] = (byte) i;
+            scheduler0.add(scheduler0.createScheduledEvent(msg, 0, 1, timeFromNow(0)));
+            MidiEventScheduler.MidiEvent event =
+                    (MidiEventScheduler.MidiEvent) scheduler0.getNextEvent(System.nanoTime());
+            assertNotNull(event);
+            assertEquals(1, event.count);
+            assertEquals(msg[0], event.data[0]);
+        }
+        assertNull(scheduler0.getNextEvent(System.nanoTime()));
+    }
+
+    @Test
+    public void testSchedulerSpamEventsPullLater() {
+        MidiEventMultiScheduler multiScheduler = new MidiEventMultiScheduler(1);
+        assertEquals(1, multiScheduler.getNumEventSchedulers());
+        MidiEventScheduler scheduler0 = multiScheduler.getEventScheduler(0);
+        // Create a msg with size 1
+        byte[] msg = new byte[1];
+        for (int i = 0; i < 1000; i++) {
+            msg[0] = (byte) i;
+            scheduler0.add(scheduler0.createScheduledEvent(msg, 0, 1, timeFromNow(0)));
+        }
+
+        for (int i = 0; i < 1000; i++) {
+            MidiEventScheduler.MidiEvent event =
+                    (MidiEventScheduler.MidiEvent) scheduler0.getNextEvent(System.nanoTime());
+            assertNotNull(event);
+            assertEquals(1, event.count);
+            assertEquals((byte) i, event.data[0]);
+        }
+        assertNull(scheduler0.getNextEvent(System.nanoTime()));
+    }
+
+    @Test
+    public void testSchedulerSpamEventsCallbackLater() {
+        MidiEventMultiScheduler multiScheduler = new MidiEventMultiScheduler(1);
+        assertEquals(1, multiScheduler.getNumEventSchedulers());
+        MidiEventScheduler scheduler0 = multiScheduler.getEventScheduler(0);
+        // Create a msg with size 1
+        byte[] msg = new byte[1];
+        for (int i = 0; i < 1000; i++) {
+            msg[0] = (byte) i;
+            scheduler0.add(scheduler0.createScheduledEvent(msg, 0, 1, timeFromNow(0)));
+        }
+
+        for (int i = 0; i < 1000; i++) {
+            try {
+                assertTrue(multiScheduler.waitNextEvent());
+            } catch (InterruptedException ex) {
+            }
+            MidiEventScheduler.MidiEvent event =
+                    (MidiEventScheduler.MidiEvent) scheduler0.getNextEvent(System.nanoTime());
+            assertNotNull(event);
+            assertEquals(1, event.count);
+            assertEquals((byte) i, event.data[0]);
+        }
+        assertNull(scheduler0.getNextEvent(System.nanoTime()));
+    }
+
+    @Test
+    public void testMultiSchedulerOutOfOrder() {
+        try {
+            MidiEventMultiScheduler multiScheduler = new MidiEventMultiScheduler(3);
+            assertEquals(3, multiScheduler.getNumEventSchedulers());
+            MidiEventScheduler scheduler0 = multiScheduler.getEventScheduler(0);
+            MidiEventScheduler scheduler1 = multiScheduler.getEventScheduler(1);
+            MidiEventScheduler scheduler2 = multiScheduler.getEventScheduler(2);
+
+            scheduler0.add(scheduler0.createScheduledEvent(new byte[]{(byte) 0xf3},
+                    0, 1,
+                    timeFromNow(400)));
+            scheduler2.add(scheduler2.createScheduledEvent(new byte[]{(byte) 0xf2},
+                    0, 1,
+                    timeFromNow(300)));
+            scheduler1.add(scheduler1.createScheduledEvent(new byte[]{(byte) 0xf1},
+                    0, 1,
+                    timeFromNow(200)));
+            scheduler0.add(scheduler0.createScheduledEvent(new byte[]{(byte) 0xf0},
+                    0, 1,
+                    timeFromNow(100)));
+
+            assertTrue(multiScheduler.waitNextEvent());
+            MidiEventScheduler.MidiEvent event =
+                    (MidiEventScheduler.MidiEvent) scheduler0.getNextEvent(System.nanoTime());
+            assertNotNull(event);
+            assertEquals(1, event.count);
+            assertEquals((byte) 0xf0, event.data[0]);
+            assertNull(scheduler1.getNextEvent(System.nanoTime()));
+            assertNull(scheduler2.getNextEvent(System.nanoTime()));
+            assertTrue(multiScheduler.waitNextEvent());
+            assertNull(scheduler0.getNextEvent(System.nanoTime()));
+            event = (MidiEventScheduler.MidiEvent) scheduler1.getNextEvent(System.nanoTime());
+            assertNotNull(event);
+            assertEquals(1, event.count);
+            assertEquals((byte) 0xf1, event.data[0]);
+            assertNull(scheduler2.getNextEvent(System.nanoTime()));
+            assertTrue(multiScheduler.waitNextEvent());
+            assertNull(scheduler0.getNextEvent(System.nanoTime()));
+            assertNull(scheduler1.getNextEvent(System.nanoTime()));
+            event = (MidiEventScheduler.MidiEvent) scheduler2.getNextEvent(System.nanoTime());
+            assertNotNull(event);
+            assertEquals(1, event.count);
+            assertEquals((byte) 0xf2, event.data[0]);
+            assertTrue(multiScheduler.waitNextEvent());
+            event = (MidiEventScheduler.MidiEvent) scheduler0.getNextEvent(System.nanoTime());
+            assertNotNull(event);
+            assertEquals(1, event.count);
+            assertEquals((byte) 0xf3, event.data[0]);
+            assertNull(scheduler1.getNextEvent(System.nanoTime()));
+            assertNull(scheduler2.getNextEvent(System.nanoTime()));
+        } catch (InterruptedException ex) {
+
+        }
+    }
+
+    @Test
+    public void testMultiSchedulerOutOfOrderNegativeTime() {
+        try {
+            MidiEventMultiScheduler multiScheduler = new MidiEventMultiScheduler(3);
+            assertEquals(3, multiScheduler.getNumEventSchedulers());
+            MidiEventScheduler scheduler0 = multiScheduler.getEventScheduler(0);
+            MidiEventScheduler scheduler1 = multiScheduler.getEventScheduler(1);
+            MidiEventScheduler scheduler2 = multiScheduler.getEventScheduler(2);
+
+            scheduler0.add(scheduler0.createScheduledEvent(new byte[]{(byte) 0xf3},
+                    0, 1,
+                    timeFromNow(-100)));
+            scheduler2.add(scheduler2.createScheduledEvent(new byte[]{(byte) 0xf2},
+                    0, 1,
+                    timeFromNow(-200)));
+            scheduler1.add(scheduler1.createScheduledEvent(new byte[]{(byte) 0xf1},
+                    0, 1,
+                    timeFromNow(-300)));
+            scheduler0.add(scheduler0.createScheduledEvent(new byte[]{(byte) 0xf0},
+                    0, 1,
+                    timeFromNow(-400)));
+
+            assertTrue(multiScheduler.waitNextEvent());
+            MidiEventScheduler.MidiEvent event =
+                    (MidiEventScheduler.MidiEvent) scheduler0.getNextEvent(System.nanoTime());
+            assertNotNull(event);
+            assertEquals(1, event.count);
+            assertEquals((byte) 0xf0, event.data[0]);
+            assertTrue(multiScheduler.waitNextEvent());
+            event = (MidiEventScheduler.MidiEvent) scheduler1.getNextEvent(System.nanoTime());
+            assertNotNull(event);
+            assertEquals(1, event.count);
+            assertEquals((byte) 0xf1, event.data[0]);
+            assertTrue(multiScheduler.waitNextEvent());
+            event = (MidiEventScheduler.MidiEvent) scheduler2.getNextEvent(System.nanoTime());
+            assertNotNull(event);
+            assertEquals(1, event.count);
+            assertEquals((byte) 0xf2, event.data[0]);
+            assertNull(scheduler1.getNextEvent(System.nanoTime()));
+            assertTrue(multiScheduler.waitNextEvent());
+            event = (MidiEventScheduler.MidiEvent) scheduler0.getNextEvent(System.nanoTime());
+            assertNotNull(event);
+            assertEquals(1, event.count);
+            assertEquals((byte) 0xf3, event.data[0]);
+            assertNull(scheduler1.getNextEvent(System.nanoTime()));
+            assertNull(scheduler2.getNextEvent(System.nanoTime()));
+        } catch (InterruptedException ex) {
+
+        }
+    }
+}