From d322524028e6557b938e4297c4eb86907df15566 Mon Sep 17 00:00:00 2001 From: Armin Date: Tue, 19 Mar 2024 15:48:55 +0100 Subject: [PATCH] Add accidentally removed tests, fix connected tests and disable two flaky tests --- .../src/androidTest/AndroidManifest.xml | 13 + .../MovebisDataCapturingService.kt | 2 +- .../datacapturing/DataCapturingServiceTest.kt | 1232 +++++++++++++++++ .../de/cyface/datacapturing/PingPongTest.kt | 254 ++++ persistence/build.gradle | 8 + .../content/DatabaseMigratorTest.kt | 7 +- .../kotlin/de/cyface/persistence/Database.kt | 1 + .../src/androidTest/AndroidManifest.xml | 25 + .../CyfaceAuthenticatorTest.java | 3 +- .../synchronization/SetAccountFlagTest.java | 5 +- ...rTest.java => SyncAdapterAndroidTest.java} | 4 +- .../de/cyface/synchronization/MockAuth.kt | 2 - .../synchronization/SyncPerformerTest.kt | 4 +- .../SyncPerformerTest_withoutAuth.kt | 4 +- 14 files changed, 1549 insertions(+), 15 deletions(-) create mode 100644 datacapturing/src/androidTest/kotlin/de/cyface/datacapturing/DataCapturingServiceTest.kt create mode 100644 datacapturing/src/androidTest/kotlin/de/cyface/datacapturing/PingPongTest.kt rename synchronization/src/androidTest/java/de/cyface/synchronization/{SyncAdapterTest.java => SyncAdapterAndroidTest.java} (98%) diff --git a/datacapturing/src/androidTest/AndroidManifest.xml b/datacapturing/src/androidTest/AndroidManifest.xml index 56457133b..2cfa7ccf0 100644 --- a/datacapturing/src/androidTest/AndroidManifest.xml +++ b/datacapturing/src/androidTest/AndroidManifest.xml @@ -12,5 +12,18 @@ android:process=":persistence_process" android:syncable="true" tools:replace="android:authorities"/> + + + + + + + + + diff --git a/datacapturing/src/androidTest/java/de/cyface/datacapturing/MovebisDataCapturingService.kt b/datacapturing/src/androidTest/java/de/cyface/datacapturing/MovebisDataCapturingService.kt index ac19e58b5..4d137c127 100644 --- a/datacapturing/src/androidTest/java/de/cyface/datacapturing/MovebisDataCapturingService.kt +++ b/datacapturing/src/androidTest/java/de/cyface/datacapturing/MovebisDataCapturingService.kt @@ -43,7 +43,6 @@ import de.cyface.persistence.model.MeasurementStatus import de.cyface.persistence.model.Modality import de.cyface.persistence.strategy.DefaultDistanceCalculation import de.cyface.persistence.strategy.DefaultLocationCleaning -import de.cyface.synchronization.Constants.AUTH_TOKEN_TYPE import de.cyface.uploader.exception.SynchronisationException import de.cyface.utils.Validate @@ -105,6 +104,7 @@ class MovebisDataCapturingService internal constructor( * running. */ private val preMeasurementLocationManager: LocationManager? + private val AUTH_TOKEN_TYPE = "de.cyface.jwt" /** * A listener for location updates, which it passes through to the user interface. diff --git a/datacapturing/src/androidTest/kotlin/de/cyface/datacapturing/DataCapturingServiceTest.kt b/datacapturing/src/androidTest/kotlin/de/cyface/datacapturing/DataCapturingServiceTest.kt new file mode 100644 index 000000000..fdb6c6955 --- /dev/null +++ b/datacapturing/src/androidTest/kotlin/de/cyface/datacapturing/DataCapturingServiceTest.kt @@ -0,0 +1,1232 @@ +/* + * Copyright 2017-2023 Cyface GmbH + * + * This file is part of the Cyface SDK for Android. + * + * The Cyface SDK for Android is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Cyface SDK for Android is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with the Cyface SDK for Android. If not, see . + */ +package de.cyface.datacapturing + +import android.Manifest +import android.accounts.Account +import android.accounts.AccountManager +import android.app.Activity +import android.content.Context +import android.util.Log +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.FlakyTest +import androidx.test.filters.LargeTest +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.rule.GrantPermissionRule +import androidx.test.rule.ServiceTestRule +import de.cyface.datacapturing.backend.TestCallback +import de.cyface.datacapturing.exception.CorruptedMeasurementException +import de.cyface.datacapturing.exception.DataCapturingException +import de.cyface.datacapturing.exception.MissingPermissionException +import de.cyface.persistence.SetupException +import de.cyface.datacapturing.persistence.CapturingPersistenceBehaviour +import de.cyface.persistence.DefaultPersistenceBehaviour +import de.cyface.persistence.DefaultPersistenceLayer +import de.cyface.persistence.PersistenceBehaviour +import de.cyface.persistence.PersistenceLayer +import de.cyface.persistence.exception.NoSuchMeasurementException +import de.cyface.persistence.model.EventType +import de.cyface.persistence.model.Measurement +import de.cyface.persistence.model.MeasurementStatus +import de.cyface.persistence.model.Modality +import de.cyface.synchronization.CyfaceAuthenticator +import de.cyface.testutils.SharedTestUtils.clearPersistenceLayer +import de.cyface.utils.Validate +import kotlinx.coroutines.runBlocking +import org.hamcrest.CoreMatchers +import org.hamcrest.MatcherAssert.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Ignore +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import java.util.concurrent.TimeUnit +import java.util.concurrent.locks.Lock +import java.util.concurrent.locks.ReentrantLock + +/** + * Tests whether the [DataCapturingService] works correctly. This is a flaky test since it starts a service that + * relies on external sensors and the availability of a GNSS signal. Each tests waits a few seconds to actually capture + * some data, but it might still fail if you use a real device indoors. + * + * @author Klemens Muthmann + * @author Armin Schnabel + * @version 5.7.8 + * @since 2.0.0 + */ +@RunWith(AndroidJUnit4::class) +@LargeTest +class DataCapturingServiceTest { + + /** + * Rule used to run + */ + @get:Rule + val serviceTestRule = ServiceTestRule() + + /** + * Grants the access location permission to this test. + */ + @get:Rule + val grantPermissionRule: GrantPermissionRule = GrantPermissionRule + .grant(Manifest.permission.ACCESS_FINE_LOCATION)!! + + /** + * The object of class under test. + */ + private var oocut: DataCapturingService? = null + + /** + * Listener for messages from the service. This is used to assert correct service startup and shutdown. + */ + private var testListener: TestListener? = null + + /** + * The [Context] needed to access the persistence layer + */ + private var context: Context? = null + + /** + * [DefaultPersistenceLayer] required to access stored [Measurement]s. + */ + private var persistence: PersistenceLayer? = null + + /** + * Initializes the super class as well as the object of the class under test and the synchronization lock. This is + * called prior to every single test case. + */ + @Before + fun setUp() = runBlocking { + context = InstrumentationRegistry.getInstrumentation().targetContext + persistence = DefaultPersistenceLayer(context!!, DefaultPersistenceBehaviour()) + clearPersistenceLayer(context!!, persistence!!) + + // The LOGIN_ACTIVITY is normally set to the LoginActivity of the SDK implementing app + CyfaceAuthenticator.LOGIN_ACTIVITY = Activity::class.java + + // Add test account + val requestAccount = Account(TestUtils.DEFAULT_USERNAME, TestUtils.ACCOUNT_TYPE) + AccountManager.get(context) + .addAccountExplicitly(requestAccount, TestUtils.DEFAULT_PASSWORD, null) + + // Start DataCapturingService + testListener = TestListener() + InstrumentationRegistry.getInstrumentation().runOnMainSync { + oocut = try { + CyfaceDataCapturingService( + context!!, + TestUtils.AUTHORITY, + TestUtils.ACCOUNT_TYPE, + //"https://localhost:8080/api/v3", + //TestUtils.oauthConfig(), + IgnoreEventsStrategy(), + testListener!!, + 100 + ) + } catch (e: SetupException) { + throw IllegalStateException(e) + } + } + + // Making sure there is no service instance of a previous test running + Validate.isTrue(!isDataCapturingServiceRunning) + } + + /** + * Tries to stop the DataCapturingService if a test failed to do so. + * + * @throws NoSuchMeasurementException If no measurement was [MeasurementStatus.OPEN] or + * [MeasurementStatus.PAUSED] while stopping the service. This usually occurs if + * there was no call to + * [DataCapturingService.start] + * prior to stopping. + */ + @After + @Throws(NoSuchMeasurementException::class) + fun tearDown() { + if (oocut != null && isDataCapturingServiceRunning) { + + // Stop zombie + // Do not reuse the lock/condition! + val lock: Lock = ReentrantLock() + val condition = lock.newCondition() + val shutDownFinishedHandler = TestShutdownFinishedHandler( + lock, + condition, MessageCodes.LOCAL_BROADCAST_SERVICE_STOPPED + ) + oocut!!.stop(shutDownFinishedHandler) + + // Ensure the zombie sent a stopped message back to the DataCapturingService + TestUtils.lockAndWait( + 2, TimeUnit.SECONDS, shutDownFinishedHandler.lock, + shutDownFinishedHandler.condition + ) + assertThat( + shutDownFinishedHandler.receivedServiceStopped(), CoreMatchers.`is`( + CoreMatchers.equalTo(true) + ) + ) + + // Ensure that the zombie was not running during the callCheckForRunning + val isRunning = isDataCapturingServiceRunning + assertThat(isRunning, CoreMatchers.`is`(CoreMatchers.equalTo(false))) + } + runBlocking { clearPersistenceLayer(context!!, persistence!!) } + } + + /** + * Makes sure a test did not forget to stop the capturing. + */ + private val isDataCapturingServiceRunning: Boolean + get() { + + // Get the current isRunning state (i.e. updates runningStatusCallback). This is important, see #MOV-484. + // Do not reuse the lock/condition/runningStatusCallback! + val runningStatusCallbackLock: Lock = ReentrantLock() + val runningStatusCallbackCondition = runningStatusCallbackLock.newCondition() + val runningStatusCallback = TestCallback( + "Default Callback", runningStatusCallbackLock, + runningStatusCallbackCondition + ) + TestUtils.callCheckForRunning(oocut!!, runningStatusCallback) + TestUtils.lockAndWait( + 2, TimeUnit.SECONDS, runningStatusCallback.lock, + runningStatusCallback.condition + ) + return runningStatusCallback.wasRunning() && !runningStatusCallback.didTimeOut() + } + + /** + * Starts a [DataCapturingService] and checks that it's running afterwards. + * + * @return the measurement id of the started capturing + * @throws DataCapturingException If the asynchronous background service did not start successfully or no valid + * Android context was available. + * @throws MissingPermissionException If no Android `ACCESS_FINE_LOCATION` has been granted. You may + * register a [de.cyface.datacapturing.ui.UIListener] to ask the user for this permission and prevent the + * `Exception`. If the `Exception` was thrown the service does not start. + */ + @Throws( + MissingPermissionException::class, + DataCapturingException::class, + CorruptedMeasurementException::class + ) + private fun startAndCheckThatLaunched(): Long { + + // Do not reuse the lock/condition! + val lock: Lock = ReentrantLock() + val condition = lock.newCondition() + val startUpFinishedHandler = TestStartUpFinishedHandler( + lock, condition, + MessageCodes.GLOBAL_BROADCAST_SERVICE_STARTED + ) + oocut!!.start(Modality.UNKNOWN, startUpFinishedHandler) + return checkThatLaunched(startUpFinishedHandler) + } + + /** + * Pauses a [DataCapturingService] and checks that it's not running afterwards. + * + * @param measurementIdentifier The if of the measurement expected to be closed. + * @throws NoSuchMeasurementException If no measurement was [MeasurementStatus.OPEN] while pausing the + * service. This usually occurs if there was no call to + * [DataCapturingService.start] prior to + * pausing. + */ + @Throws(NoSuchMeasurementException::class) + private fun pauseAndCheckThatStopped(measurementIdentifier: Long) { + + // Do not reuse the lock/condition! + val lock: Lock = ReentrantLock() + val condition = lock.newCondition() + val shutDownFinishedHandler = TestShutdownFinishedHandler( + lock, condition, + MessageCodes.LOCAL_BROADCAST_SERVICE_STOPPED + ) + oocut!!.pause(shutDownFinishedHandler) + checkThatStopped(shutDownFinishedHandler, measurementIdentifier) + } + + /** + * Resumes a [DataCapturingService] and checks that it's running afterwards. + * + * @param measurementIdentifier The id of the measurement which is expected to be resumed + * @throws DataCapturingException If starting the background service was not successful. + * @throws MissingPermissionException If permission to access geo location via satellite has not been granted or + * revoked. The current measurement is closed if you receive this `Exception`. If you get the + * permission in the future you need to start a new measurement and not call `resumeSync` + * again. + * @throws NoSuchMeasurementException If no measurement was [MeasurementStatus.OPEN] while pausing the + * service. This usually occurs if there was no call to + * [DataCapturingService.start] prior to + * pausing. + */ + @Throws( + MissingPermissionException::class, + DataCapturingException::class, + NoSuchMeasurementException::class + ) + private fun resumeAndCheckThatLaunched(measurementIdentifier: Long) { + + // Do not reuse the lock/condition! + val lock: Lock = ReentrantLock() + val condition = lock.newCondition() + val startUpFinishedHandler = TestStartUpFinishedHandler( + lock, condition, + MessageCodes.GLOBAL_BROADCAST_SERVICE_STARTED + ) + oocut!!.resume(startUpFinishedHandler) + val resumedMeasurementId = checkThatLaunched(startUpFinishedHandler) + assertThat(resumedMeasurementId, CoreMatchers.`is`(measurementIdentifier)) + } + + /** + * Stops a [DataCapturingService] and checks that it's not running afterwards. + * + * @param measurementIdentifier The if of the measurement expected to be closed. + * + * @throws NoSuchMeasurementException If no measurement was [MeasurementStatus.OPEN] or + * [MeasurementStatus.PAUSED] while stopping the service. This usually occurs if + * there was no call to + * [DataCapturingService.start] + * prior to stopping. + */ + @Throws(NoSuchMeasurementException::class) + private fun stopAndCheckThatStopped(measurementIdentifier: Long) { + + // Do not reuse the lock/condition! + val lock: Lock = ReentrantLock() + val condition = lock.newCondition() + val shutDownFinishedHandler = TestShutdownFinishedHandler( + lock, condition, + MessageCodes.LOCAL_BROADCAST_SERVICE_STOPPED + ) + oocut!!.stop(shutDownFinishedHandler) + checkThatStopped(shutDownFinishedHandler, measurementIdentifier) + } + + /** + * Checks that a [DataCapturingService] actually started after calling the life-cycle method + * [DataCapturingService.start] or + * [DataCapturingService.resume] + * + * @param startUpFinishedHandler The [TestStartUpFinishedHandler] which was used to start the service + * @return The id of the measurement which was started + */ + private fun checkThatLaunched(startUpFinishedHandler: TestStartUpFinishedHandler): Long { + + // Ensure the DataCapturingBackgroundService sent a started message back to the DataCapturingService + TestUtils.lockAndWait( + 2, TimeUnit.SECONDS, startUpFinishedHandler.lock, + startUpFinishedHandler.condition + ) + assertThat( + startUpFinishedHandler.receivedServiceStarted(), CoreMatchers.`is`( + CoreMatchers.equalTo(true) + ) + ) + + // Ensure that the DataCapturingBackgroundService was running during the callCheckForRunning + val isRunning = isDataCapturingServiceRunning + assertThat(isRunning, CoreMatchers.`is`(CoreMatchers.equalTo(true))) + + // Return the id of the started measurement + assertThat( + startUpFinishedHandler.receivedMeasurementIdentifier, CoreMatchers.`is`( + CoreMatchers.not(CoreMatchers.equalTo(-1L)) + ) + ) + return startUpFinishedHandler.receivedMeasurementIdentifier + } + + /** + * Checks that a [DataCapturingService] actually stopped after calling the life-cycle method + * [DataCapturingService.stop] or + * [DataCapturingService.pause]. + * + * Also checks that the measurement which was stopped is the expected measurement. + * + * @param shutDownFinishedHandler The [TestShutdownFinishedHandler] which was used to stop the service + * @param measurementIdentifier The id of the measurement which was expected to be stopped by the references + * life-cycle call + */ + private fun checkThatStopped( + shutDownFinishedHandler: TestShutdownFinishedHandler, + measurementIdentifier: Long + ) { + + // Ensure the DataCapturingBackgroundService sent a stopped message back to the DataCapturingService + TestUtils.lockAndWait( + 2, TimeUnit.SECONDS, shutDownFinishedHandler.lock, + shutDownFinishedHandler.condition + ) + assertThat( + shutDownFinishedHandler.receivedServiceStopped(), CoreMatchers.`is`( + CoreMatchers.equalTo(true) + ) + ) + + // Ensure that the DataCapturingBackgroundService was not running during the callCheckForRunning + val isRunning = isDataCapturingServiceRunning + assertThat(isRunning, CoreMatchers.`is`(CoreMatchers.equalTo(false))) + + // Ensure that the expected measurement stopped + assertThat( + shutDownFinishedHandler.receivedMeasurementIdentifier, CoreMatchers.`is`( + CoreMatchers.equalTo(measurementIdentifier) + ) + ) + } + + /** + * Tests a common service run. + * + * @throws DataCapturingException On any error during running the capturing process. + * @throws MissingPermissionException If an Android permission is missing. + * @throws NoSuchMeasurementException Fails the test if the capturing measurement is lost somewhere. + */ + @Test + @Throws( + DataCapturingException::class, + MissingPermissionException::class, + NoSuchMeasurementException::class, + CorruptedMeasurementException::class + ) + fun testStartStop() { + val receivedMeasurementIdentifier = startAndCheckThatLaunched() + stopAndCheckThatStopped(receivedMeasurementIdentifier) + } + + /** + * Tests that a double start-stop combination with waiting for the callback does not break the service. + * + * Makes sure the [DataCapturingService.pause] and + * [DataCapturingService.resume] work correctly. + * + * @throws DataCapturingException Happens on unexpected states during data capturing. + * @throws MissingPermissionException Should not happen since a `GrantPermissionRule` is used. + * @throws NoSuchMeasurementException Fails the test if the capturing measurement is lost somewhere. + */ + @Test + @Throws( + DataCapturingException::class, + MissingPermissionException::class, + NoSuchMeasurementException::class, + CorruptedMeasurementException::class + ) + fun testMultipleStartStopWithDelay() { + val measurementIdentifier = startAndCheckThatLaunched() + var measurements: List = persistence!!.loadMeasurements() + assertThat(measurements.size, CoreMatchers.`is`(CoreMatchers.equalTo(1))) + stopAndCheckThatStopped(measurementIdentifier) + val measurementIdentifier2 = startAndCheckThatLaunched() + measurements = persistence!!.loadMeasurements() + assertThat(measurements.size, CoreMatchers.`is`(CoreMatchers.equalTo(2))) + stopAndCheckThatStopped(measurementIdentifier2) + } + + /** + * Tests that a double start-stop combination without waiting for the callback does not break the service. + * + * @throws DataCapturingException On any error during running the capturing process. + * @throws MissingPermissionException If an Android permission is missing. + * @throws NoSuchMeasurementException Fails the test if the capturing measurement is lost somewhere. + */ + @Test + @Ignore( + """This test fails as our library currently runs lifecycle tasks (start/stop) in parallel. +To fix this we need to re-use a handler for a sequential execution. See CY-4098, MOV-378 +We should consider refactoring the code before to use startCommandReceived as intended CY-4097.""" + ) + @Throws( + DataCapturingException::class, + MissingPermissionException::class, + NoSuchMeasurementException::class, + CorruptedMeasurementException::class + ) + fun testMultipleStartStopWithoutDelay() { + + // Do not reuse the lock/condition! + val lock1: Lock = ReentrantLock() + val condition1 = lock1.newCondition() + val startUpFinishedHandler1 = TestStartUpFinishedHandler( + lock1, condition1, + MessageCodes.GLOBAL_BROADCAST_SERVICE_STARTED + ) + // Do not reuse the lock/condition! + val lock2: Lock = ReentrantLock() + val condition2 = lock2.newCondition() + val startUpFinishedHandler2 = TestStartUpFinishedHandler( + lock2, condition2, + MessageCodes.GLOBAL_BROADCAST_SERVICE_STARTED + ) + // Do not reuse the lock/condition! + val lock3: Lock = ReentrantLock() + val condition3 = lock3.newCondition() + val startUpFinishedHandler3 = TestStartUpFinishedHandler( + lock3, condition3, + MessageCodes.GLOBAL_BROADCAST_SERVICE_STARTED + ) + // Do not reuse the lock/condition! + val lock4: Lock = ReentrantLock() + val condition4 = lock4.newCondition() + val shutDownFinishedHandler1 = TestShutdownFinishedHandler( + lock4, condition4, + MessageCodes.LOCAL_BROADCAST_SERVICE_STOPPED + ) + // Do not reuse the lock/condition! + val lock5: Lock = ReentrantLock() + val condition5 = lock5.newCondition() + val shutDownFinishedHandler2 = TestShutdownFinishedHandler( + lock5, condition5, + MessageCodes.LOCAL_BROADCAST_SERVICE_STOPPED + ) + // Do not reuse the lock/condition! + val lock6: Lock = ReentrantLock() + val condition6 = lock6.newCondition() + val shutDownFinishedHandler3 = TestShutdownFinishedHandler( + lock6, condition6, + MessageCodes.LOCAL_BROADCAST_SERVICE_STOPPED + ) + + // First Start/stop without waiting + oocut!!.start(Modality.UNKNOWN, startUpFinishedHandler1) + oocut!!.stop(shutDownFinishedHandler1) + // Second start/stop without waiting + oocut!!.start(Modality.UNKNOWN, startUpFinishedHandler2) + oocut!!.stop(shutDownFinishedHandler2) + // Second start/stop without waiting + oocut!!.start(Modality.UNKNOWN, startUpFinishedHandler3) + oocut!!.stop(shutDownFinishedHandler3) + + // Now let's make sure all measurements started and stopped as expected + TestUtils.lockAndWait( + 2, TimeUnit.SECONDS, startUpFinishedHandler1.lock, + startUpFinishedHandler1.condition + ) + TestUtils.lockAndWait( + 2, TimeUnit.SECONDS, shutDownFinishedHandler1.lock, + shutDownFinishedHandler1.condition + ) + TestUtils.lockAndWait( + 2, TimeUnit.SECONDS, startUpFinishedHandler2.lock, + startUpFinishedHandler2.condition + ) + TestUtils.lockAndWait( + 2, TimeUnit.SECONDS, shutDownFinishedHandler2.lock, + shutDownFinishedHandler2.condition + ) + TestUtils.lockAndWait( + 2, TimeUnit.SECONDS, startUpFinishedHandler3.lock, + startUpFinishedHandler3.condition + ) + TestUtils.lockAndWait( + 2, TimeUnit.SECONDS, shutDownFinishedHandler3.lock, + shutDownFinishedHandler3.condition + ) + val measurements = persistence!!.loadMeasurements() + assertThat(measurements.size, CoreMatchers.`is`(CoreMatchers.equalTo(3))) + val measurementId1 = startUpFinishedHandler1.receivedMeasurementIdentifier + assertThat( + measurements[0].id, + CoreMatchers.`is`(CoreMatchers.equalTo(measurementId1)) + ) + val measurementId2 = startUpFinishedHandler2.receivedMeasurementIdentifier + assertThat( + measurements[1].id, + CoreMatchers.`is`(CoreMatchers.equalTo(measurementId2)) + ) + val measurementId3 = startUpFinishedHandler3.receivedMeasurementIdentifier + assertThat( + measurements[2].id, + CoreMatchers.`is`(CoreMatchers.equalTo(measurementId3)) + ) + assertThat( + measurementId1, + CoreMatchers.`is`(CoreMatchers.not(CoreMatchers.equalTo(-1L))) + ) + assertThat( + shutDownFinishedHandler1.receivedMeasurementIdentifier, CoreMatchers.`is`( + CoreMatchers.equalTo(measurementId1) + ) + ) + assertThat( + measurementId2, + CoreMatchers.`is`(CoreMatchers.not(CoreMatchers.equalTo(-1L))) + ) + assertThat( + shutDownFinishedHandler2.receivedMeasurementIdentifier, CoreMatchers.`is`( + CoreMatchers.equalTo(measurementId2) + ) + ) + assertThat( + measurementId3, + CoreMatchers.`is`(CoreMatchers.not(CoreMatchers.equalTo(-1L))) + ) + assertThat( + shutDownFinishedHandler3.receivedMeasurementIdentifier, CoreMatchers.`is`( + CoreMatchers.equalTo(measurementId3) + ) + ) + } + + /** + * Tests a common service run with an intermediate disconnect and reconnect by the application. No problems should + * occur and some points should be captured. + * + * @throws DataCapturingException On any error during running the capturing process. + * @throws MissingPermissionException If an Android permission is missing. + * @throws NoSuchMeasurementException Fails the test if the capturing measurement is lost somewhere. + */ + @Test + @Throws( + DataCapturingException::class, + MissingPermissionException::class, + NoSuchMeasurementException::class, + CorruptedMeasurementException::class + ) + fun testDisconnectReconnect() { + val measurementIdentifier = startAndCheckThatLaunched() + oocut!!.disconnect() + assertThat( + oocut!!.reconnect(DataCapturingService.IS_RUNNING_CALLBACK_TIMEOUT), CoreMatchers.`is`( + CoreMatchers.equalTo(true) + ) + ) + stopAndCheckThatStopped(measurementIdentifier) + } + + /** + * Tests that running startSync twice does not break the system. This test succeeds if no `Exception` + * occurs. Must be supported (#MOV-460). + * + * @throws DataCapturingException On any error during running the capturing process. + * @throws MissingPermissionException If an Android permission is missing. + * @throws NoSuchMeasurementException Fails the test if the capturing measurement is lost somewhere. + */ + @Test + @Throws( + DataCapturingException::class, + MissingPermissionException::class, + NoSuchMeasurementException::class, + CorruptedMeasurementException::class + ) + fun testDoubleStart() { + val measurementIdentifier = startAndCheckThatLaunched() + + // Second start - should not launch anything + // Do not reuse the lock/condition! + val lock: Lock = ReentrantLock() + val condition = lock.newCondition() + val startUpFinishedHandler = TestStartUpFinishedHandler( + lock, condition, + MessageCodes.GLOBAL_BROADCAST_SERVICE_STARTED + ) + oocut!!.start(Modality.UNKNOWN, startUpFinishedHandler) + TestUtils.lockAndWait(2, TimeUnit.SECONDS, lock, condition) + assertThat( + startUpFinishedHandler.receivedServiceStarted(), CoreMatchers.`is`( + CoreMatchers.equalTo(false) + ) + ) + stopAndCheckThatStopped(measurementIdentifier) + } + + /** + * Tests that stopping a stopped service throws the expected exception. + * + * @throws DataCapturingException On any error during running the capturing process. + * @throws MissingPermissionException If an Android permission is missing. + * @throws NoSuchMeasurementException Fails the test if the capturing measurement is lost somewhere. + */ + @Test(expected = NoSuchMeasurementException::class) + @Throws( + DataCapturingException::class, + MissingPermissionException::class, + NoSuchMeasurementException::class, + CorruptedMeasurementException::class + ) + fun testDoubleStop() { + val measurementId = startAndCheckThatLaunched() + stopAndCheckThatStopped(measurementId) + // Do not reuse the lock/condition! + val lock: Lock = ReentrantLock() + val condition = lock.newCondition() + // must throw NoSuchMeasurementException + oocut!!.stop( + TestShutdownFinishedHandler( + lock, + condition, + MessageCodes.LOCAL_BROADCAST_SERVICE_STOPPED + ) + ) + } + + /** + * Tests for the correct `Exception` if you try to disconnect from a disconnected service. + * + * @throws DataCapturingException On any error during running the capturing process. + * @throws MissingPermissionException If an Android permission is missing. + * @throws NoSuchMeasurementException Fails the test if the capturing measurement is lost somewhere. + */ + @Test(expected = DataCapturingException::class) + @Throws( + DataCapturingException::class, + MissingPermissionException::class, + NoSuchMeasurementException::class, + CorruptedMeasurementException::class + ) + fun testDoubleDisconnect() { + val measurementIdentifier = startAndCheckThatLaunched() + oocut!!.disconnect() + oocut!!.disconnect() // must throw DataCapturingException + stopAndCheckThatStopped(measurementIdentifier) // is called by tearDown + } + + /** + * Tests that no `Exception` occurs if you stop a disconnected service. + * + * @throws DataCapturingException On any error during running the capturing process. + * @throws MissingPermissionException If an Android permission is missing. + * @throws NoSuchMeasurementException Fails the test if the capturing measurement is lost somewhere. + */ + @Test + @Throws( + DataCapturingException::class, + MissingPermissionException::class, + NoSuchMeasurementException::class, + CorruptedMeasurementException::class + ) + fun testStopNonConnectedService() { + val measurementIdentifier = startAndCheckThatLaunched() + oocut!!.disconnect() + stopAndCheckThatStopped(measurementIdentifier) + } + + /** + * Tests that no `Exception` is thrown when we try to connect to the same service twice. + * + * @throws DataCapturingException On any error during running the capturing process. + * @throws MissingPermissionException If an Android permission is missing. + * @throws NoSuchMeasurementException Fails the test if the capturing measurement is lost somewhere. + */ + @Test + @Throws( + DataCapturingException::class, + MissingPermissionException::class, + NoSuchMeasurementException::class, + CorruptedMeasurementException::class + ) + fun testDoubleReconnect() { + val measurementIdentifier = startAndCheckThatLaunched() + oocut!!.disconnect() + assertThat( + oocut!!.reconnect(DataCapturingService.IS_RUNNING_CALLBACK_TIMEOUT), + CoreMatchers.`is`(true) + ) + assertThat( + oocut!!.reconnect(DataCapturingService.IS_RUNNING_CALLBACK_TIMEOUT), + CoreMatchers.`is`(true) + ) + stopAndCheckThatStopped(measurementIdentifier) + } + + /** + * Tests that two correct cycles of disconnect and reconnect on a running service work fine. + * + * @throws DataCapturingException On any error during running the capturing process. + * @throws MissingPermissionException If an Android permission is missing. + * @throws NoSuchMeasurementException Fails the test if the capturing measurement is lost somewhere. + */ + @Test + @Throws( + DataCapturingException::class, + MissingPermissionException::class, + NoSuchMeasurementException::class, + CorruptedMeasurementException::class + ) + fun testDisconnectReconnectTwice() { + val measurementIdentifier = startAndCheckThatLaunched() + oocut!!.disconnect() + assertThat( + oocut!!.reconnect(DataCapturingService.IS_RUNNING_CALLBACK_TIMEOUT), + CoreMatchers.`is`(true) + ) + oocut!!.disconnect() + assertThat( + oocut!!.reconnect(DataCapturingService.IS_RUNNING_CALLBACK_TIMEOUT), + CoreMatchers.`is`(true) + ) + stopAndCheckThatStopped(measurementIdentifier) + } + + /** + * Tests that starting a service twice throws no `Exception`. + * + * @throws DataCapturingException On any error during running the capturing process. + * @throws MissingPermissionException If an Android permission is missing. + * @throws NoSuchMeasurementException Fails the test if the capturing measurement is lost somewhere. + */ + @Test + @Throws( + DataCapturingException::class, + MissingPermissionException::class, + NoSuchMeasurementException::class, + CorruptedMeasurementException::class + ) + fun testRestart() { + val measurementIdentifier = startAndCheckThatLaunched() + stopAndCheckThatStopped(measurementIdentifier) + val measurementIdentifier2 = startAndCheckThatLaunched() + assertThat( + measurementIdentifier2, + CoreMatchers.not(CoreMatchers.equalTo(measurementIdentifier)) + ) + stopAndCheckThatStopped(measurementIdentifier2) + } + + /** + * Tests that calling resume two times in a row works without causing any errors. The second call to resume should + * just do nothing. Must be supported (#MOV-460). + * + * @throws MissingPermissionException If permission to access geo location sensor is missing. + * @throws DataCapturingException If any unexpected error occurs during the test. + * @throws NoSuchMeasurementException Fails the test if the capturing measurement is lost somewhere. + */ + @Test + @Throws( + MissingPermissionException::class, + DataCapturingException::class, + NoSuchMeasurementException::class, + CorruptedMeasurementException::class + ) + fun testResumeTwice() { + + // Start, pause + val measurementIdentifier = startAndCheckThatLaunched() + pauseAndCheckThatStopped(measurementIdentifier) + + // Resume 1 + resumeAndCheckThatLaunched(measurementIdentifier) + + // Resume 2: must be ignored by resumeAsync + val persistence = DefaultPersistenceLayer( + context!!, CapturingPersistenceBehaviour() + ) + // Do not reuse the lock/condition! + val lock: Lock = ReentrantLock() + val condition = lock.newCondition() + val startUpFinishedHandler = TestStartUpFinishedHandler( + lock, condition, + MessageCodes.GLOBAL_BROADCAST_SERVICE_STARTED + ) + oocut!!.resume(startUpFinishedHandler) + val isRunning = isDataCapturingServiceRunning + assertThat(isRunning, CoreMatchers.`is`(CoreMatchers.equalTo(true))) + assertThat( + persistence.loadMeasurementStatus(measurementIdentifier), CoreMatchers.`is`( + CoreMatchers.equalTo(MeasurementStatus.OPEN) + ) + ) + stopAndCheckThatStopped(measurementIdentifier) + assertThat( + persistence.loadMeasurementStatus(measurementIdentifier), CoreMatchers.`is`( + CoreMatchers.equalTo(MeasurementStatus.FINISHED) + ) + ) + } + + /** + * Tests that stopping a paused service does work successfully. + * + * @throws MissingPermissionException If the test is missing the permission to access the geo location sensor. + * @throws DataCapturingException If any unexpected error occurs. + * @throws NoSuchMeasurementException Fails the test if the capturing measurement is lost somewhere. + */ + @Test + @Throws( + MissingPermissionException::class, + DataCapturingException::class, + NoSuchMeasurementException::class, + CorruptedMeasurementException::class + ) + fun testStartPauseStop() { + val measurementIdentifier = startAndCheckThatLaunched() + pauseAndCheckThatStopped(measurementIdentifier) + stopAndCheckThatStopped(measurementIdentifier) // stop paused returns mid, too [STAD-333] + } + + /** + * Tests that stopping a paused service does work successfully. + * + * As this test was flaky MOV-527, we have this test here which executes it multiple times. + * + * @throws MissingPermissionException If the test is missing the permission to access the geo location sensor. + * @throws DataCapturingException If any unexpected error occurs. + * @throws NoSuchMeasurementException Fails the test if the capturing measurement is lost somewhere. + */ + @Test + @Ignore("Not needed to be executed automatically as MOV-527 made the normal tests flaky") + @Throws( + MissingPermissionException::class, + DataCapturingException::class, + NoSuchMeasurementException::class, + CorruptedMeasurementException::class + ) + fun testStartPauseStop_MultipleTimes() { + for (i in 0..19) { + Log.d(TestUtils.TAG, "ITERATION: $i") + val measurementIdentifier = startAndCheckThatLaunched() + pauseAndCheckThatStopped(measurementIdentifier) + stopAndCheckThatStopped(measurementIdentifier) // stop paused returns mid, too [STAD-333] + } + } + + /** + * Tests that removing the [DataCapturingListener] during capturing does not stop the + * [de.cyface.datacapturing.backend.DataCapturingBackgroundService]. + * + * @throws MissingPermissionException If the test is missing the permission to access the geo location sensor. + * @throws DataCapturingException If any unexpected error occurs. + * @throws NoSuchMeasurementException Fails the test if the capturing measurement is lost somewhere. + */ + @Test + @Throws( + MissingPermissionException::class, + DataCapturingException::class, + NoSuchMeasurementException::class, + CorruptedMeasurementException::class + ) + fun testRemoveDataCapturingListener() { + val measurementIdentifier = startAndCheckThatLaunched() + // This happens in SDK implementing apps (SR) when the app is paused and resumed + oocut!!.removeDataCapturingListener(testListener!!) + oocut!!.addDataCapturingListener(testListener!!) + // Should not happen, we test it anyways + oocut!!.addDataCapturingListener(testListener!!) + pauseAndCheckThatStopped(measurementIdentifier) + // Should not happen, we test it anyways + oocut!!.removeDataCapturingListener(testListener!!) + // Should not happen, we test it anyways + oocut!!.removeDataCapturingListener(testListener!!) + resumeAndCheckThatLaunched(measurementIdentifier) + stopAndCheckThatStopped(measurementIdentifier) + } + + /** + * Tests if the service lifecycle is running successfully and that the life-cycle [de.cyface.persistence.model.Event]s are logged. + * + * Makes sure the [DataCapturingService.pause] and + * [DataCapturingService.resume] work correctly. + * + * @throws DataCapturingException Happens on unexpected states during data capturing. + * @throws MissingPermissionException Should not happen since a `GrantPermissionRule` is used. + * @throws NoSuchMeasurementException Fails the test if the capturing measurement is lost somewhere. + */ + @Test + @Throws( + DataCapturingException::class, + MissingPermissionException::class, + NoSuchMeasurementException::class, + CorruptedMeasurementException::class + ) + fun testStartPauseResumeStop_EventsAreLogged() { + val measurementIdentifier = startPauseResumeStop() + val events = oocut!!.persistenceLayer.loadEvents(measurementIdentifier) + // start, pause, resume, stop and initial MODALITY_TYPE_CHANGE event + assertThat(events.size, CoreMatchers.`is`(CoreMatchers.equalTo(5))) + assertThat( + events[0]!!.type, + CoreMatchers.`is`(CoreMatchers.equalTo(EventType.LIFECYCLE_START)) + ) + assertThat( + events[1]!!.type, + CoreMatchers.`is`(CoreMatchers.equalTo(EventType.MODALITY_TYPE_CHANGE)) + ) + assertThat( + events[2]!!.type, + CoreMatchers.`is`(CoreMatchers.equalTo(EventType.LIFECYCLE_PAUSE)) + ) + assertThat( + events[3]!!.type, + CoreMatchers.`is`(CoreMatchers.equalTo(EventType.LIFECYCLE_RESUME)) + ) + assertThat( + events[4]!!.type, + CoreMatchers.`is`(CoreMatchers.equalTo(EventType.LIFECYCLE_STOP)) + ) + } + + /** + * Tests if the service lifecycle is running successfully and that the life-cycle [de.cyface.persistence.model.Event]s are logged. + * + * Makes sure the [DataCapturingService.pause] and + * [DataCapturingService.resume] work correctly. + * + * As this test was flaky MOV-527, we have this test here which executes it multiple times. + * + * @throws DataCapturingException Happens on unexpected states during data capturing. + * @throws MissingPermissionException Should not happen since a `GrantPermissionRule` is used. + * @throws NoSuchMeasurementException Fails the test if the capturing measurement is lost somewhere. + */ + @Ignore("Not needed to be executed automatically as MOV-527 made the normal tests flaky") + @Test + @Throws( + DataCapturingException::class, + MissingPermissionException::class, + NoSuchMeasurementException::class, + CorruptedMeasurementException::class + ) + fun testStartPauseResumeStop_MultipleTimes() { + for (i in 0..49) { + Log.d(TestUtils.TAG, "ITERATION: $i") + startPauseResumeStop() + + // For for-i-loops within this test + runBlocking { + clearPersistenceLayer(context!!, persistence!!) + } + } + } + + @Throws( + DataCapturingException::class, + NoSuchMeasurementException::class, + CorruptedMeasurementException::class, + MissingPermissionException::class + ) + private fun startPauseResumeStop(): Long { + val measurementIdentifier = startAndCheckThatLaunched() + val measurements = persistence!!.loadMeasurements() + assertThat(measurements.size, CoreMatchers.`is`(CoreMatchers.equalTo(1))) + pauseAndCheckThatStopped(measurementIdentifier) + resumeAndCheckThatLaunched(measurementIdentifier) + val newMeasurements = persistence!!.loadMeasurements() + assertThat( + measurements.size == newMeasurements.size, CoreMatchers.`is`( + CoreMatchers.equalTo(true) + ) + ) + stopAndCheckThatStopped(measurementIdentifier) + + runBlocking { + // Check Events + val events = + persistence!!.eventRepository!!.loadAllByMeasurementId(measurementIdentifier) + assertThat(events!!.size, CoreMatchers.`is`(CoreMatchers.equalTo(5))) + assertThat( + events[0].type, + CoreMatchers.`is`(CoreMatchers.equalTo(EventType.LIFECYCLE_START)) + ) + assertThat( + events[1].type, + CoreMatchers.`is`(CoreMatchers.equalTo(EventType.MODALITY_TYPE_CHANGE)) + ) + assertThat( + events[1].value, + CoreMatchers.`is`(CoreMatchers.equalTo(Modality.UNKNOWN.databaseIdentifier)) + ) + assertThat( + events[2].type, + CoreMatchers.`is`(CoreMatchers.equalTo(EventType.LIFECYCLE_PAUSE)) + ) + assertThat( + events[3].type, + CoreMatchers.`is`(CoreMatchers.equalTo(EventType.LIFECYCLE_RESUME)) + ) + assertThat( + events[4].type, + CoreMatchers.`is`(CoreMatchers.equalTo(EventType.LIFECYCLE_STOP)) + ) + } + return measurementIdentifier + } + + /** + * Tests whether actual sensor data is captured after running the method + * [CyfaceDataCapturingService.start] ()}. + * In bug #CY-3862 only the [DataCapturingService] was started and measurements created + * but no sensor data was captured as the [de.cyface.datacapturing.backend.DataCapturingBackgroundService] + * was not started. The cause was: disables sensor capturing. + * + * This test is Flaky because it's success depends on if sensor data was captured during the + * lockAndWait timeout. It's large because multiple seconds are waited until during the test. + * + * @throws DataCapturingException If any unexpected errors occur during data capturing. + * @throws MissingPermissionException If an Android permission is missing. + * @throws NoSuchMeasurementException Fails the test if the capturing measurement is lost somewhere. + */ + @Test + @LargeTest + @FlakyTest + @Throws( + DataCapturingException::class, + MissingPermissionException::class, + NoSuchMeasurementException::class, + CorruptedMeasurementException::class, + InterruptedException::class + ) + fun testSensorDataCapturing() { + val measurementIdentifier = startAndCheckThatLaunched() + + // Check sensor data + val measurements = persistence!!.loadMeasurements() + assertThat( + measurements.isNotEmpty(), + CoreMatchers.`is`(CoreMatchers.equalTo(true)) + ) + Thread.sleep(3000L) + assertThat( + testListener!!.capturedData.size > 0, CoreMatchers.`is`( + CoreMatchers.equalTo(true) + ) + ) + stopAndCheckThatStopped(measurementIdentifier) + } + + /** + * Tests whether reconnect throws no exception when called without a running background service and leaves the + * DataCapturingService in the correct state (`isDataCapturingServiceRunning` is `false`. + */ + @Test + fun testReconnectOnNonRunningServer() { + assertThat( + oocut!!.reconnect(DataCapturingService.IS_RUNNING_CALLBACK_TIMEOUT), + CoreMatchers.`is`(false) + ) + assertThat(oocut!!.isRunning, CoreMatchers.`is`(CoreMatchers.equalTo(false))) + } + + /** + * Tests that starting a new `Measurement` and changing the `Modality` during runtime creates two + * [EventType.MODALITY_TYPE_CHANGE] entries. + * + * @throws MissingPermissionException If the test is missing the permission to access the geo location sensor. + * @throws DataCapturingException If any unexpected error occurs. + * @throws NoSuchMeasurementException Fails the test if the capturing measurement is lost somewhere. + */ + @Test + @Throws( + MissingPermissionException::class, + DataCapturingException::class, + NoSuchMeasurementException::class, + CorruptedMeasurementException::class + ) + fun testChangeModality_EventLogContainsTwoModalities() { + val measurementIdentifier = startAndCheckThatLaunched() + oocut!!.changeModalityType(Modality.CAR) + stopAndCheckThatStopped(measurementIdentifier) + val modalityTypeChanges = oocut!!.persistenceLayer.loadEvents( + measurementIdentifier, + EventType.MODALITY_TYPE_CHANGE + ) + assertThat( + modalityTypeChanges!!.size, + CoreMatchers.`is`(CoreMatchers.equalTo(2)) + ) + assertThat( + modalityTypeChanges[0].value, CoreMatchers.`is`( + CoreMatchers.equalTo( + Modality.UNKNOWN.databaseIdentifier + ) + ) + ) + assertThat( + modalityTypeChanges[1].value, CoreMatchers.`is`( + CoreMatchers.equalTo( + Modality.CAR.databaseIdentifier + ) + ) + ) + } + + /** + * Tests that changing to the same `Modality` twice does not produce a new + * [EventType.MODALITY_TYPE_CHANGE] `Event`. + * + * @throws MissingPermissionException If the test is missing the permission to access the geo location sensor. + * @throws DataCapturingException If any unexpected error occurs. + * @throws NoSuchMeasurementException Fails the test if the capturing measurement is lost somewhere. + */ + @Test + @Throws( + MissingPermissionException::class, + DataCapturingException::class, + NoSuchMeasurementException::class, + CorruptedMeasurementException::class + ) + fun testChangeModalityToSameModalityTwice_EventLogStillContainsOnlyTwoModalities() { + val measurementIdentifier = startAndCheckThatLaunched() + oocut!!.changeModalityType(Modality.CAR) + oocut!!.changeModalityType(Modality.CAR) + stopAndCheckThatStopped(measurementIdentifier) + val modalityTypeChanges = oocut!!.persistenceLayer.loadEvents( + measurementIdentifier, + EventType.MODALITY_TYPE_CHANGE + ) + assertThat( + modalityTypeChanges!!.size, + CoreMatchers.`is`(CoreMatchers.equalTo(2)) + ) + } + + /** + * Tests that changing `Modality` during a [EventType.LIFECYCLE_PAUSE] works as expected. + * + * @throws MissingPermissionException If the test is missing the permission to access the geo location sensor. + * @throws DataCapturingException If any unexpected error occurs. + * @throws NoSuchMeasurementException Fails the test if the capturing measurement is lost somewhere. + */ + @Test + @Throws( + MissingPermissionException::class, + DataCapturingException::class, + NoSuchMeasurementException::class, + CorruptedMeasurementException::class + ) + fun testChangeModalityWhilePaused_EventLogStillContainsModalityChange() { + val measurementIdentifier = startAndCheckThatLaunched() + pauseAndCheckThatStopped(measurementIdentifier) + oocut!!.changeModalityType(Modality.CAR) + stopAndCheckThatStopped(measurementIdentifier) // stop paused returns mid, too [STAD-333] + val modalityTypeChanges = oocut!!.persistenceLayer.loadEvents( + measurementIdentifier, + EventType.MODALITY_TYPE_CHANGE + ) + assertThat( + modalityTypeChanges!!.size, + CoreMatchers.`is`(CoreMatchers.equalTo(2)) + ) + assertThat( + modalityTypeChanges[0].value, CoreMatchers.`is`( + CoreMatchers.equalTo( + Modality.UNKNOWN.databaseIdentifier + ) + ) + ) + assertThat( + modalityTypeChanges[1].value, CoreMatchers.`is`( + CoreMatchers.equalTo( + Modality.CAR.databaseIdentifier + ) + ) + ) + } +} diff --git a/datacapturing/src/androidTest/kotlin/de/cyface/datacapturing/PingPongTest.kt b/datacapturing/src/androidTest/kotlin/de/cyface/datacapturing/PingPongTest.kt new file mode 100644 index 000000000..747c59777 --- /dev/null +++ b/datacapturing/src/androidTest/kotlin/de/cyface/datacapturing/PingPongTest.kt @@ -0,0 +1,254 @@ +/* + * Copyright 2017-2023 Cyface GmbH + * + * This file is part of the Cyface SDK for Android. + * + * The Cyface SDK for Android is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Cyface SDK for Android is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with the Cyface SDK for Android. If not, see . + */ +package de.cyface.datacapturing + +import android.Manifest +import android.app.Activity +import android.content.Context +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.MediumTest +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.rule.GrantPermissionRule +import de.cyface.datacapturing.backend.TestCallback +import de.cyface.datacapturing.exception.CorruptedMeasurementException +import de.cyface.datacapturing.exception.DataCapturingException +import de.cyface.datacapturing.exception.MissingPermissionException +import de.cyface.persistence.DefaultPersistenceBehaviour +import de.cyface.persistence.DefaultPersistenceLayer +import de.cyface.persistence.PersistenceBehaviour +import de.cyface.persistence.PersistenceLayer +import de.cyface.persistence.SetupException +import de.cyface.persistence.exception.NoSuchMeasurementException +import de.cyface.persistence.model.Modality +import de.cyface.synchronization.CyfaceAuthenticator +import de.cyface.testutils.SharedTestUtils.clearPersistenceLayer +import kotlinx.coroutines.runBlocking +import org.hamcrest.CoreMatchers +import org.hamcrest.MatcherAssert +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import java.util.concurrent.TimeUnit +import java.util.concurrent.locks.Condition +import java.util.concurrent.locks.Lock +import java.util.concurrent.locks.ReentrantLock + +/** + * This test checks that the ping pong mechanism works as expected. This mechanism ist used to check if a service, in + * this case the ´DataCapturingBackgroundService`, is running or not. + * + * @author Klemens Muthmann + * @author Armin Schnabel + * @version 1.2.13 + * @since 2.3.2 + */ +@RunWith(AndroidJUnit4::class) +@MediumTest +class PingPongTest { + /** + * Grants the permission required by the [DataCapturingService]. + */ + @get:Rule + var mRuntimePermissionRule: GrantPermissionRule = GrantPermissionRule + .grant(Manifest.permission.ACCESS_FINE_LOCATION) + + /** + * An instance of the class under test (object of class under test). + */ + private var oocut: PongReceiver? = null + + /** + * Lock used to synchronize the asynchronous calls to the [DataCapturingService] with the test thread. + */ + private var lock: Lock? = null + + /** + * Condition used to synchronize the asynchronous calls to the [DataCapturingService] with the test thread. + */ + private var condition: Condition? = null + + /** + * The [DataCapturingService] instance used by the test to check whether a pong can be received. + */ + private var dcs: DataCapturingService? = null + + /** + * The [Context] required to send unique broadcasts and to start the capturing service. + */ + private var context: Context? = null + private lateinit var persistence: PersistenceLayer + + /** + * Sets up all the instances required by all tests in this test class. + * + */ + @Before + fun setUp() { + lock = ReentrantLock() + condition = lock!!.newCondition() + context = InstrumentationRegistry.getInstrumentation().targetContext + persistence = DefaultPersistenceLayer(context!!, DefaultPersistenceBehaviour()) + oocut = PongReceiver( + context!!, + MessageCodes.GLOBAL_BROADCAST_PING, + MessageCodes.GLOBAL_BROADCAST_PONG + ) + } + + @After + fun tearDown() { + runBlocking { clearPersistenceLayer(context!!, persistence) } + } + + /** + * Tests the ping pong with a running service. In that case it should successfully finish one round of ping/pong + * with that service. + * + * @throws MissingPermissionException Should not happen, since there is a JUnit rule to prevent it. + * @throws DataCapturingException If data capturing was not possible after starting the service. + * @throws NoSuchMeasurementException If the service lost track of the measurement. + */ + @Test + @Throws( + MissingPermissionException::class, + DataCapturingException::class, + NoSuchMeasurementException::class, + CorruptedMeasurementException::class + ) + fun testWithRunningService() { + + // Arrange + // Instantiate DataCapturingService + val testListener: DataCapturingListener = TestListener() + // The LOGIN_ACTIVITY is normally set to the LoginActivity of the SDK implementing app + CyfaceAuthenticator.LOGIN_ACTIVITY = Activity::class.java + InstrumentationRegistry.getInstrumentation().runOnMainSync { + dcs = try { + CyfaceDataCapturingService( + context!!, + TestUtils.AUTHORITY, + TestUtils.ACCOUNT_TYPE, + //"https://upload.fake/", + //TestUtils.oauthConfig(), + IgnoreEventsStrategy(), + testListener, + 100 + ) + } catch (e: SetupException) { + throw IllegalStateException(e) + } + } + + // Start Capturing + val finishedHandler: StartUpFinishedHandler = TestStartUpFinishedHandler( + lock!!, condition!!, + MessageCodes.GLOBAL_BROADCAST_SERVICE_STARTED + ) + dcs!!.start(Modality.UNKNOWN, finishedHandler) + + // Give the async start some time to start the DataCapturingBackgroundService + lock!!.lock() + try { + condition!!.await(TestUtils.TIMEOUT_TIME, TimeUnit.SECONDS) + } catch (e: InterruptedException) { + throw IllegalStateException(e) + } finally { + lock!!.unlock() + } + + // Act + // Check if DataCapturingBackgroundService is running + val testCallback = TestCallback("testWithRunningService", lock!!, condition!!) + oocut!!.checkIsRunningAsync(TestUtils.TIMEOUT_TIME, TimeUnit.SECONDS, testCallback) + + // Give the async call some time + lock!!.lock() + try { + condition!!.await(2 * TestUtils.TIMEOUT_TIME, TimeUnit.SECONDS) + } catch (e: InterruptedException) { + throw IllegalStateException(e) + } finally { + lock!!.unlock() + } + + // Assert + // Ensure DataCapturingBackgroundService was running during the async check + MatcherAssert.assertThat( + testCallback.wasRunning(), + CoreMatchers.`is`(CoreMatchers.equalTo(true)) + ) + MatcherAssert.assertThat( + testCallback.didTimeOut(), + CoreMatchers.`is`(CoreMatchers.equalTo(false)) + ) + + // Cleanup + // Stop Capturing + val shutdownHandler = TestShutdownFinishedHandler( + lock!!, condition!!, + MessageCodes.LOCAL_BROADCAST_SERVICE_STOPPED + ) + dcs!!.stop(shutdownHandler) + + // Give the async stop some time to stop gracefully + lock!!.lock() + try { + condition!!.await(TestUtils.TIMEOUT_TIME, TimeUnit.SECONDS) + } catch (e: InterruptedException) { + throw IllegalStateException(e) + } finally { + lock!!.unlock() + } + } + + /** + * Tests that the [PongReceiver] works without crashing as expected even when no service runs. + */ + @Test + fun testWithNonRunningService() { + + // Act + // Check if DataCapturingBackgroundService is running + val testCallback = TestCallback("testWithNonRunningService", lock!!, condition!!) + oocut!!.checkIsRunningAsync(TestUtils.TIMEOUT_TIME, TimeUnit.SECONDS, testCallback) + + // Give the async call some time + lock!!.lock() + try { + condition!!.await(2 * TestUtils.TIMEOUT_TIME, TimeUnit.SECONDS) + } catch (e: InterruptedException) { + throw IllegalStateException(e) + } finally { + lock!!.unlock() + } + + // Assert + // Ensure DataCapturingBackgroundService was running during the async check + MatcherAssert.assertThat( + testCallback.didTimeOut(), + CoreMatchers.`is`(CoreMatchers.equalTo(true)) + ) + MatcherAssert.assertThat( + testCallback.wasRunning(), + CoreMatchers.`is`(CoreMatchers.equalTo(false)) + ) + } +} diff --git a/persistence/build.gradle b/persistence/build.gradle index ccb03b839..ed1770115 100644 --- a/persistence/build.gradle +++ b/persistence/build.gradle @@ -89,6 +89,14 @@ android { }*/ } +// Required when executing connected tests +configurations { + configureEach { + // collides with hamcrest-all + exclude group: "org.hamcrest", module: "hamcrest-core" + } +} + dependencies { // Android dependencies implementation "androidx.annotation:annotation:$rootProject.ext.androidxAnnotationVersion" diff --git a/persistence/src/androidTest/kotlin/de/cyface/persistence/content/DatabaseMigratorTest.kt b/persistence/src/androidTest/kotlin/de/cyface/persistence/content/DatabaseMigratorTest.kt index 8f35bd3a2..02a57b7d8 100644 --- a/persistence/src/androidTest/kotlin/de/cyface/persistence/content/DatabaseMigratorTest.kt +++ b/persistence/src/androidTest/kotlin/de/cyface/persistence/content/DatabaseMigratorTest.kt @@ -99,7 +99,8 @@ class DatabaseMigratorTest { DatabaseMigrator.MIGRATION_14_15, DatabaseMigrator.MIGRATION_15_16, DatabaseMigrator.MIGRATION_16_17, - migrator!!.MIGRATION_17_18 + migrator!!.MIGRATION_17_18, + DatabaseMigrator.MIGRATION_18_19, ) } @@ -849,7 +850,7 @@ class DatabaseMigratorTest { // Assert // Loading from the newly added table must work (STAD-85) - db!!.execSQL("SELECT * FROM events;") + db.execSQL("SELECT * FROM events;") } /** @@ -895,7 +896,7 @@ class DatabaseMigratorTest { // Assert // Loading from the newly added table must work (STAD-85) - db!!.execSQL("SELECT * FROM events;") + db.execSQL("SELECT * FROM events;") // Make sure the relevant data from before the upgrade still exists var cursor: Cursor? = null diff --git a/persistence/src/main/kotlin/de/cyface/persistence/Database.kt b/persistence/src/main/kotlin/de/cyface/persistence/Database.kt index 504e3a1d1..104b8d8f6 100644 --- a/persistence/src/main/kotlin/de/cyface/persistence/Database.kt +++ b/persistence/src/main/kotlin/de/cyface/persistence/Database.kt @@ -63,6 +63,7 @@ import de.cyface.persistence.model.Pressure Attachment::class ], // version 18 imported data from `v6.1` database into `measures.17` and migrated `measures` to Room + // version 19 adds the attachments table version = 19 //autoMigrations = [] // test this feature on the next version change ) diff --git a/synchronization/src/androidTest/AndroidManifest.xml b/synchronization/src/androidTest/AndroidManifest.xml index 56c69b876..3cec98d87 100644 --- a/synchronization/src/androidTest/AndroidManifest.xml +++ b/synchronization/src/androidTest/AndroidManifest.xml @@ -15,5 +15,30 @@ android:process=":persistence_process" android:syncable="true" tools:replace="android:authorities"/> + + + + + + + + + + + + + + + + diff --git a/synchronization/src/androidTest/java/de/cyface/synchronization/CyfaceAuthenticatorTest.java b/synchronization/src/androidTest/java/de/cyface/synchronization/CyfaceAuthenticatorTest.java index 16772235d..4ff1dddc7 100644 --- a/synchronization/src/androidTest/java/de/cyface/synchronization/CyfaceAuthenticatorTest.java +++ b/synchronization/src/androidTest/java/de/cyface/synchronization/CyfaceAuthenticatorTest.java @@ -18,6 +18,7 @@ */ package de.cyface.synchronization; +import static de.cyface.synchronization.CyfaceSyncService.AUTH_TOKEN_TYPE; import static de.cyface.synchronization.TestUtils.ACCOUNT_TYPE; import static de.cyface.synchronization.TestUtils.AUTHORITY; import static org.hamcrest.CoreMatchers.is; @@ -95,7 +96,7 @@ public void testGetAuthToken() throws NetworkErrorException { // Act // Explicitly calling CyfaceAuthenticator.getAuthToken(), see its documentation Bundle bundle = new CyfaceAuthenticator(context) - .getAuthToken(null, requestAccount, Constants.AUTH_TOKEN_TYPE, null); + .getAuthToken(null, requestAccount, AUTH_TOKEN_TYPE, null); // Assert String authToken = bundle.getString(AccountManager.KEY_AUTHTOKEN); diff --git a/synchronization/src/androidTest/java/de/cyface/synchronization/SetAccountFlagTest.java b/synchronization/src/androidTest/java/de/cyface/synchronization/SetAccountFlagTest.java index 1d5a3b6b5..d7c8b5faf 100644 --- a/synchronization/src/androidTest/java/de/cyface/synchronization/SetAccountFlagTest.java +++ b/synchronization/src/androidTest/java/de/cyface/synchronization/SetAccountFlagTest.java @@ -18,8 +18,6 @@ */ package de.cyface.synchronization; -import static de.cyface.synchronization.TestUtils.ACCOUNT_TYPE; -import static de.cyface.synchronization.TestUtils.AUTHORITY; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; @@ -32,6 +30,7 @@ import org.junit.After; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; @@ -50,7 +49,6 @@ import androidx.test.filters.FlakyTest; import androidx.test.platform.app.InstrumentationRegistry; -import de.cyface.synchronization.WiFiSurveyor; import de.cyface.testutils.SharedTestUtils; import de.cyface.utils.Validate; @@ -64,6 +62,7 @@ * @since 4.0.0 */ @RunWith(AndroidJUnit4.class) +@Ignore("This test is flaky on a local emulator and physical device") public class SetAccountFlagTest { /** diff --git a/synchronization/src/androidTest/java/de/cyface/synchronization/SyncAdapterTest.java b/synchronization/src/androidTest/java/de/cyface/synchronization/SyncAdapterAndroidTest.java similarity index 98% rename from synchronization/src/androidTest/java/de/cyface/synchronization/SyncAdapterTest.java rename to synchronization/src/androidTest/java/de/cyface/synchronization/SyncAdapterAndroidTest.java index 98215772e..f7ac360cc 100644 --- a/synchronization/src/androidTest/java/de/cyface/synchronization/SyncAdapterTest.java +++ b/synchronization/src/androidTest/java/de/cyface/synchronization/SyncAdapterAndroidTest.java @@ -34,6 +34,7 @@ import org.junit.After; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; @@ -70,7 +71,7 @@ */ @RunWith(AndroidJUnit4.class) @LargeTest -public final class SyncAdapterTest { +public final class SyncAdapterAndroidTest { private Context context; private AccountManager accountManager; @@ -108,6 +109,7 @@ public void tearDown() { * @throws InterruptedException Thrown if waiting for the sync adapter to report back is interrupted. */ @Test + @Ignore("This test is flaky on the local emulator") public void testRequestSync() throws InterruptedException { // Enable auto sync diff --git a/synchronization/src/androidTest/kotlin/de/cyface/synchronization/MockAuth.kt b/synchronization/src/androidTest/kotlin/de/cyface/synchronization/MockAuth.kt index 8633f4e58..7d2530c17 100644 --- a/synchronization/src/androidTest/kotlin/de/cyface/synchronization/MockAuth.kt +++ b/synchronization/src/androidTest/kotlin/de/cyface/synchronization/MockAuth.kt @@ -1,7 +1,5 @@ package de.cyface.synchronization -import de.cyface.synchronization.Auth - class MockAuth: Auth { override fun performActionWithFreshTokens(action: (accessToken: String?, idToken: String?, ex: Exception?) -> Unit) { action("eyTestToken", null, null) diff --git a/synchronization/src/androidTest/kotlin/de/cyface/synchronization/SyncPerformerTest.kt b/synchronization/src/androidTest/kotlin/de/cyface/synchronization/SyncPerformerTest.kt index 27f53fd19..f36f633a0 100644 --- a/synchronization/src/androidTest/kotlin/de/cyface/synchronization/SyncPerformerTest.kt +++ b/synchronization/src/androidTest/kotlin/de/cyface/synchronization/SyncPerformerTest.kt @@ -162,7 +162,7 @@ class SyncPerformerTest { progressListener, "testToken", fileName, - uploader.measurementsEndpoint() + UploadType.MEASUREMENT ) // Assert @@ -328,7 +328,7 @@ class SyncPerformerTest { progressListener, "testToken", fileName, - mockedUploader.measurementsEndpoint() + UploadType.MEASUREMENT ) // Assert: diff --git a/synchronization/src/androidTest/kotlin/de/cyface/synchronization/SyncPerformerTest_withoutAuth.kt b/synchronization/src/androidTest/kotlin/de/cyface/synchronization/SyncPerformerTest_withoutAuth.kt index 6bed96314..89c4406da 100644 --- a/synchronization/src/androidTest/kotlin/de/cyface/synchronization/SyncPerformerTest_withoutAuth.kt +++ b/synchronization/src/androidTest/kotlin/de/cyface/synchronization/SyncPerformerTest_withoutAuth.kt @@ -149,11 +149,11 @@ class SyncPerformerTestWithoutAuth { ) file = loadSerializedCompressed(persistence, measurementIdentifier) val metaData: RequestMetaData = - loadMetaData(persistence, measurement, locationCount) + loadMetaData(persistence, measurement, locationCount, 0, 0, 0, 0) val url = "$TEST_API_URL/api/v3/measurements" // Act - val result = DefaultUploader(url).upload( + val result = DefaultUploader(url).uploadMeasurement( TEST_TOKEN, metaData, file!!, object : UploadProgressListener { override fun updatedProgress(percent: Float) { // Nothing to do