From f5ce76955bb3e1e94f01f67b641d4b84a7f71d37 Mon Sep 17 00:00:00 2001 From: Dennis Guse Date: Fri, 7 Jan 2022 23:41:43 +0100 Subject: [PATCH] TrackRecordingService: add tests for state changes. --- .../io/file/importer/TrackPointAssert.java | 1 + .../services/TrackRecordingServiceTest.java | 98 ------ ...> TrackRecordingServiceTestRecording.java} | 13 +- ...TrackRecordingServiceTestStateMachine.java | 300 ++++++++++++++++++ .../services/TrackRecordingService.java | 39 ++- 5 files changed, 348 insertions(+), 103 deletions(-) rename src/androidTest/java/de/dennisguse/opentracks/services/{TrackRecordingServiceTestLocation.java => TrackRecordingServiceTestRecording.java} (98%) create mode 100644 src/androidTest/java/de/dennisguse/opentracks/services/TrackRecordingServiceTestStateMachine.java diff --git a/src/androidTest/java/de/dennisguse/opentracks/io/file/importer/TrackPointAssert.java b/src/androidTest/java/de/dennisguse/opentracks/io/file/importer/TrackPointAssert.java index 6000d71ac..d4125b2c6 100644 --- a/src/androidTest/java/de/dennisguse/opentracks/io/file/importer/TrackPointAssert.java +++ b/src/androidTest/java/de/dennisguse/opentracks/io/file/importer/TrackPointAssert.java @@ -100,6 +100,7 @@ public class TrackPointAssert { } } + @Deprecated //Try to mock time instead. public TrackPointAssert ignoreTime() { this.assertTime = false; return this; diff --git a/src/androidTest/java/de/dennisguse/opentracks/services/TrackRecordingServiceTest.java b/src/androidTest/java/de/dennisguse/opentracks/services/TrackRecordingServiceTest.java index 66ecc163d..97632442f 100644 --- a/src/androidTest/java/de/dennisguse/opentracks/services/TrackRecordingServiceTest.java +++ b/src/androidTest/java/de/dennisguse/opentracks/services/TrackRecordingServiceTest.java @@ -24,14 +24,12 @@ import static org.junit.Assert.assertTrue; import android.content.Context; import android.content.Intent; -import android.os.IBinder; import android.os.Looper; import androidx.annotation.NonNull; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.MediumTest; -import androidx.test.filters.SmallTest; import androidx.test.rule.GrantPermissionRule; import androidx.test.rule.ServiceTestRule; @@ -123,67 +121,6 @@ public class TrackRecordingServiceTest { contentProviderUtils.deleteAllTracks(context); } - @SmallTest - @Test - public void testStartable() throws TimeoutException { - mServiceRule.startService(createStartIntent(context)); - assertNotNull(mServiceRule.bindService(createStartIntent(context))); - } - - @MediumTest - @Test - public void testBindable() throws TimeoutException { - IBinder service = mServiceRule.bindService(createStartIntent(context)); - assertNotNull(service); - } - - @MediumTest - @Test - public void testRecording_noTracks() throws TimeoutException { - // given - List tracks = contentProviderUtils.getTracks(); - assertTrue(tracks.isEmpty()); - - // when - Intent startIntent = createStartIntent(context); - mServiceRule.startService(startIntent); - TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(startIntent)) - .getService(); - - // then - // Test if we start in no-recording mode by default. - assertFalse(service.isRecording()); - } - - @MediumTest - @Test - public void testRecording_oldTracks() throws TimeoutException { - // given - createDummyTrack(trackId); - - // when - TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(createStartIntent(context))) - .getService(); - - // then - assertFalse(service.isRecording()); - } - - @MediumTest - @Test - public void testRecording_serviceRestart_whileRecording() throws TimeoutException { - // given - createDummyTrack(trackId); - - //when - TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(createStartIntent(context))) - .getService(); - service.resumeTrack(trackId); - - // then - assertTrue(service.isRecording()); - } - @MediumTest @Test public void testRecording_stop() throws TimeoutException { @@ -335,41 +272,6 @@ public class TrackRecordingServiceTest { ), trackPoints); } - @MediumTest - @Test - public void testStartNewTrack_alreadyRecording() throws TimeoutException { - // given - TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(createStartIntent(context))) - .getService(); - Track.Id trackId = service.startNewTrack(); - service.stopUpdateRecordingData(); - - assertTrue(service.isRecording()); - - // when - Track.Id newTrackId = service.startNewTrack(); - service.stopUpdateRecordingData(); - - // then - assertNotNull(trackId); - assertNull(newTrackId); - } - - @MediumTest - @Test - public void testEndCurrentTrack_noRecording() throws TimeoutException { - // given - TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(createStartIntent(context))) - .getService(); - assertFalse(service.isRecording()); - - // when - // Ending the current track when there is no recording should not result in any error. - service.endCurrentTrack(); - - // then - assertFalse(service.isRecording()); - } @MediumTest @Test diff --git a/src/androidTest/java/de/dennisguse/opentracks/services/TrackRecordingServiceTestLocation.java b/src/androidTest/java/de/dennisguse/opentracks/services/TrackRecordingServiceTestRecording.java similarity index 98% rename from src/androidTest/java/de/dennisguse/opentracks/services/TrackRecordingServiceTestLocation.java rename to src/androidTest/java/de/dennisguse/opentracks/services/TrackRecordingServiceTestRecording.java index 16f777242..f3ae9dc1d 100644 --- a/src/androidTest/java/de/dennisguse/opentracks/services/TrackRecordingServiceTestLocation.java +++ b/src/androidTest/java/de/dennisguse/opentracks/services/TrackRecordingServiceTestRecording.java @@ -3,6 +3,7 @@ package de.dennisguse.opentracks.services; import static org.junit.Assert.assertFalse; import android.content.Context; +import android.content.Intent; import android.os.Looper; import androidx.annotation.NonNull; @@ -41,8 +42,7 @@ import de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager; * Tests insert location. */ @RunWith(AndroidJUnit4.class) -//TODO Implement as mock test; no need to store data in database -public class TrackRecordingServiceTestLocation { +public class TrackRecordingServiceTestRecording { @Rule public final ServiceTestRule mServiceRule = ServiceTestRule.withTimeout(5, TimeUnit.SECONDS); @@ -74,13 +74,18 @@ public class TrackRecordingServiceTestLocation { if (Looper.myLooper() != null) Looper.myLooper().quit(); } + private TrackRecordingService startService() throws TimeoutException { + Intent startIntent = new Intent(context, TrackRecordingService.class); + return ((TrackRecordingService.Binder) mServiceRule.bindService(startIntent)) + .getService(); + } + @Before public void setUp() throws TimeoutException { contentProviderUtils = new ContentProviderUtils(context); tearDown(); - service = ((TrackRecordingService.Binder) mServiceRule.bindService(TrackRecordingServiceTest.createStartIntent(context))) - .getService(); + service = startService(); service.getTrackPointCreator().stopGPS(); } diff --git a/src/androidTest/java/de/dennisguse/opentracks/services/TrackRecordingServiceTestStateMachine.java b/src/androidTest/java/de/dennisguse/opentracks/services/TrackRecordingServiceTestStateMachine.java new file mode 100644 index 000000000..6739ad044 --- /dev/null +++ b/src/androidTest/java/de/dennisguse/opentracks/services/TrackRecordingServiceTestStateMachine.java @@ -0,0 +1,300 @@ +/* + * Copyright 2010 Google Inc. + * + * 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 de.dennisguse.opentracks.services; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import android.content.Context; +import android.content.Intent; +import android.os.Looper; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.MediumTest; +import androidx.test.rule.GrantPermissionRule; +import androidx.test.rule.ServiceTestRule; + +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.time.Clock; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import de.dennisguse.opentracks.content.data.Track; +import de.dennisguse.opentracks.content.provider.ContentProviderUtils; +import de.dennisguse.opentracks.services.handlers.GpsStatusValue; +import de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager; +import de.dennisguse.opentracks.settings.PreferencesUtils; + +/** + * Testing the states of TrackRecordingService. + * As states are checked against MutableLiveData that are updated via `postValue()` (is asynchronous to be inform other Threads), Thread.sleep() is required. + */ +@RunWith(AndroidJUnit4.class) +public class TrackRecordingServiceTestStateMachine { + + @Rule + public final ServiceTestRule mServiceRule = ServiceTestRule.withTimeout(5, TimeUnit.SECONDS); + + @Rule + public GrantPermissionRule mRuntimePermissionRule = GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION); + + @BeforeClass + public static void preSetUp() { + // Prepare looper for Android's message queue + if (Looper.myLooper() == null) Looper.prepare(); + } + + @AfterClass + public static void finalTearDown() { + if (Looper.myLooper() != null) Looper.myLooper().quit(); + } + + private TrackRecordingService service; + private final Context context = ApplicationProvider.getApplicationContext(); + private ContentProviderUtils contentProviderUtils; + + private TrackRecordingService startService() throws TimeoutException { + Intent startIntent = new Intent(context, TrackRecordingService.class); + return ((TrackRecordingService.Binder) mServiceRule.bindService(startIntent)) + .getService(); + } + + @Before + public void setUp() throws TimeoutException { + contentProviderUtils = new ContentProviderUtils(context); + service = startService(); + tearDown(); + } + + @After + public void tearDown() throws TimeoutException { + TrackRecordingServiceTestStateMachine.resetService(mServiceRule, context); + // Ensure that the database is empty after every test + contentProviderUtils.deleteAllTracks(context); + } + + @MediumTest + @Test + public void initialState() { + // given + List tracks = contentProviderUtils.getTracks(); + assertTrue(tracks.isEmpty()); + + // when + // noop + + // then + assertFalse(service.isRecording()); + assertEquals(TrackRecordingService.STATUS_DEFAULT, service.getRecordingStatusObservable().getValue()); + assertEquals(TrackRecordingService.NOT_RECORDING, service.getRecordingDataObservable().getValue()); + assertEquals(GpsStatusValue.GPS_NONE, service.getGpsStatusObservable().getValue()); + } + + @MediumTest + @Test + public void gps_startStop() throws InterruptedException { + // given + assertEquals(GpsStatusValue.GPS_NONE, service.getGpsStatusObservable().getValue()); + + // when + service.tryStartGps(); + Thread.sleep(1000); + + // then + assertEquals(TrackRecordingService.STATUS_DEFAULT, service.getRecordingStatusObservable().getValue()); + assertEquals(TrackRecordingService.NOT_RECORDING, service.getRecordingDataObservable().getValue()); + assertEquals(GpsStatusValue.GPS_ENABLED, service.getGpsStatusObservable().getValue()); + + // when + service.stopGps(true); + Thread.sleep(1000); + + // then + assertEquals(TrackRecordingService.STATUS_DEFAULT, service.getRecordingStatusObservable().getValue()); + assertEquals(TrackRecordingService.NOT_RECORDING, service.getRecordingDataObservable().getValue()); +// assertEquals(GpsStatusValue.GPS_NONE, service.getGpsStatusObservable().getValue()); TODO BUG: GPS can only be stopped if a service is recording. + } + + @MediumTest + @Test + public void recording_startPauseStopResume_no_data() throws InterruptedException { + // given + assertFalse(service.isRecording()); + + // when + Track.Id trackId = service.startNewTrack(); + Thread.sleep(1000); + + // then + assertTrue(service.isRecording()); + assertEquals(new TrackRecordingService.RecordingStatus(trackId, false), service.getRecordingStatusObservable().getValue()); + assertNotEquals(TrackRecordingService.NOT_RECORDING, service.getRecordingDataObservable().getValue()); + assertEquals(GpsStatusValue.GPS_ENABLED, service.getGpsStatusObservable().getValue()); + + + // when + service.pauseCurrentTrack(); + Thread.sleep(1000); + + // then + assertTrue(service.isRecording()); + assertTrue(service.isPaused()); + assertEquals(new TrackRecordingService.RecordingStatus(trackId, true), service.getRecordingStatusObservable().getValue()); + assertNotEquals(TrackRecordingService.NOT_RECORDING, service.getRecordingDataObservable().getValue()); + assertEquals(GpsStatusValue.GPS_NONE, service.getGpsStatusObservable().getValue()); //TODO The GPS is kept in pause, right? Might be a bug. + + + // when + service.resumeCurrentTrack(); + Thread.sleep(1000); + + // then + assertTrue(service.isRecording()); + assertFalse(service.isPaused()); + assertEquals(new TrackRecordingService.RecordingStatus(trackId, false), service.getRecordingStatusObservable().getValue()); + assertNotEquals(TrackRecordingService.NOT_RECORDING, service.getRecordingDataObservable().getValue()); + assertEquals(GpsStatusValue.GPS_ENABLED, service.getGpsStatusObservable().getValue()); + + + // when + service.endCurrentTrack(); + Thread.sleep(1000); + + // then + assertFalse(service.isRecording()); + assertFalse(service.isPaused()); + assertEquals(TrackRecordingService.STATUS_DEFAULT, service.getRecordingStatusObservable().getValue()); + assertEquals(TrackRecordingService.NOT_RECORDING, service.getRecordingDataObservable().getValue()); + assertEquals(GpsStatusValue.GPS_NONE, service.getGpsStatusObservable().getValue()); + + + // when + service.resumeTrack(trackId); + Thread.sleep(1000); + + // then + assertTrue(service.isRecording()); + assertFalse(service.isPaused()); + assertEquals(new TrackRecordingService.RecordingStatus(trackId, false), service.getRecordingStatusObservable().getValue()); + Thread.sleep(1000); //TODO Figure out why we need to wait here until the update is happening + assertNotEquals(TrackRecordingService.NOT_RECORDING, service.getRecordingDataObservable().getValue()); + assertEquals(GpsStatusValue.GPS_ENABLED, service.getGpsStatusObservable().getValue()); + } + + @MediumTest + @Test + public void invalidStateChanges() throws InterruptedException { + // given + assertFalse(service.isRecording()); + + // when + service.pauseCurrentTrack(); + Thread.sleep(1000); + + // then + assertEquals(TrackRecordingService.STATUS_DEFAULT, service.getRecordingStatusObservable().getValue()); + assertEquals(TrackRecordingService.NOT_RECORDING, service.getRecordingDataObservable().getValue()); + assertEquals(GpsStatusValue.GPS_NONE, service.getGpsStatusObservable().getValue()); + + + // when + service.resumeCurrentTrack(); + Thread.sleep(1000); + + // then + assertEquals(TrackRecordingService.STATUS_DEFAULT, service.getRecordingStatusObservable().getValue()); + assertEquals(TrackRecordingService.NOT_RECORDING, service.getRecordingDataObservable().getValue()); + assertEquals(GpsStatusValue.GPS_NONE, service.getGpsStatusObservable().getValue()); + + + // when + service.endCurrentTrack(); + Thread.sleep(1000); + + // then + assertEquals(TrackRecordingService.STATUS_DEFAULT, service.getRecordingStatusObservable().getValue()); + assertEquals(TrackRecordingService.NOT_RECORDING, service.getRecordingDataObservable().getValue()); + assertEquals(GpsStatusValue.GPS_NONE, service.getGpsStatusObservable().getValue()); + } + + @Ignore("TODO Bug: GPS can be stopped although the current track is recording") + @MediumTest + @Test + public void recording_stopGPS_noop() throws InterruptedException { + // given + Track.Id trackId = service.startNewTrack(); + assertEquals(GpsStatusValue.GPS_ENABLED, service.getGpsStatusObservable().getValue()); + + // when + service.stopGps(false); //TODO Should be ignored as service is recording + Thread.sleep(1000); + + // then + assertTrue(service.isRecording()); + assertFalse(service.isPaused()); + assertEquals(new TrackRecordingService.RecordingStatus(trackId, false), service.getRecordingStatusObservable().getValue()); + Thread.sleep(1000); //TODO Figure out why we need to wait here until the update is happening + assertNotEquals(TrackRecordingService.NOT_RECORDING, service.getRecordingDataObservable().getValue()); + assertEquals(GpsStatusValue.GPS_ENABLED, service.getGpsStatusObservable().getValue()); + } + + @MediumTest + @Test + public void recording_startRecording_alreadyRecording() throws InterruptedException { + // given + Track.Id trackId = service.startNewTrack(); + assertTrue(service.isRecording()); + + // when + Track.Id newTrackId = service.startNewTrack(); + Thread.sleep(1000); + + // then + assertNotNull(trackId); + assertNull(newTrackId); + } + + //TODO Workaround as service is not stopped on API23; thus sharedpreferences are not reset between tests. + //TODO Anyhow, the service should re-create all it's resources if a recording starts and makes sure that there is no leftovers from previous recordings. + @Deprecated + public static void resetService(ServiceTestRule mServiceRule, Context context) throws TimeoutException { + // Let's use default values. + PreferencesUtils.clear(); + + // Reset service (if some previous test failed) + TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(new Intent(context, TrackRecordingService.class))) + .getService(); + + service.getTrackPointCreator().setRemoteSensorManager(new BluetoothRemoteSensorManager(context, service.getTrackPointCreator())); + service.getTrackPointCreator().setClock(Clock.systemUTC()); + service.endCurrentTrack(); + service.sharedPreferenceChangeListener.onSharedPreferenceChanged(null, null); + } +} diff --git a/src/main/java/de/dennisguse/opentracks/services/TrackRecordingService.java b/src/main/java/de/dennisguse/opentracks/services/TrackRecordingService.java index 2283c7d56..6bb136e79 100644 --- a/src/main/java/de/dennisguse/opentracks/services/TrackRecordingService.java +++ b/src/main/java/de/dennisguse/opentracks/services/TrackRecordingService.java @@ -35,6 +35,7 @@ import androidx.lifecycle.MutableLiveData; import java.time.Duration; import java.time.ZoneOffset; +import java.util.Objects; import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.TrackListActivity; @@ -515,7 +516,8 @@ public class TrackRecordingService extends Service implements TrackPointCreator. private final Track.Id trackId; private final boolean paused; - private RecordingStatus(Track.Id trackId, boolean paused) { + @VisibleForTesting + RecordingStatus(Track.Id trackId, boolean paused) { this.trackId = trackId; this.paused = paused; } @@ -560,6 +562,19 @@ public class TrackRecordingService extends Service implements TrackPointCreator. ", paused=" + paused + '}'; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + RecordingStatus that = (RecordingStatus) o; + return paused == that.paused && Objects.equals(trackId, that.trackId); + } + + @Override + public int hashCode() { + return Objects.hash(trackId, paused); + } } public static class RecordingData { @@ -606,5 +621,27 @@ public class TrackRecordingService extends Service implements TrackPointCreator. public SensorDataSet getSensorDataSet() { return sensorDataSet; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + RecordingData that = (RecordingData) o; + return Objects.equals(track, that.track) && Objects.equals(latestTrackPoint, that.latestTrackPoint) && Objects.equals(sensorDataSet, that.sensorDataSet); + } + + @Override + public int hashCode() { + return Objects.hash(track, latestTrackPoint, sensorDataSet); + } + + @Override + public String toString() { + return "RecordingData{" + + "track=" + track + + ", latestTrackPoint=" + latestTrackPoint + + ", sensorDataSet=" + sensorDataSet + + '}'; + } } } \ No newline at end of file