Skip to content
This repository has been archived by the owner on Aug 30, 2023. It is now read-only.

removed Thread.sleep from LifecycleWatcher tests, using awaitility and DateProvider #392

Merged
merged 2 commits into from
May 7, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion buildSrc/src/main/java/Config.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ object Config {
val leakCanary = "com.squareup.leakcanary:leakcanary-android:2.1"

val lifecycleVersion = "2.2.0"
val lifecycleProcessor = "androidx.lifecycle:lifecycle-process:$lifecycleVersion"
val lifecycleProcess = "androidx.lifecycle:lifecycle-process:$lifecycleVersion"
val lifecycleCommonJava8 = "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion"
}

Expand All @@ -38,6 +38,7 @@ object Config {
val androidxJunit = "androidx.test.ext:junit:1.1.1"
val robolectric = "org.robolectric:robolectric:4.3.1"
val mockitoKotlin = "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0"
val awaitility = "org.awaitility:awaitility-kotlin:4.0.2"
}

object QualityPlugins {
Expand Down
3 changes: 2 additions & 1 deletion sentry-android-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ dependencies {
implementation(Config.Libs.gson)

// lifecycle processor, session tracking
implementation(Config.Libs.lifecycleProcessor)
implementation(Config.Libs.lifecycleProcess)
implementation(Config.Libs.lifecycleCommonJava8)

compileOnly(Config.CompileOnly.nopen)
Expand All @@ -96,6 +96,7 @@ dependencies {
testImplementation(Config.TestLibs.androidxRunner)
testImplementation(Config.TestLibs.androidxJunit)
testImplementation(Config.TestLibs.mockitoKotlin)
testImplementation(Config.TestLibs.awaitility)
}

//TODO: move thse blocks to parent gradle file, DRY
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@
import io.sentry.core.Breadcrumb;
import io.sentry.core.IHub;
import io.sentry.core.SentryLevel;
import io.sentry.core.transport.CurrentDateProvider;
import io.sentry.core.transport.ICurrentDateProvider;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.atomic.AtomicBoolean;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;

final class LifecycleWatcher implements DefaultLifecycleObserver {

Expand All @@ -21,16 +25,34 @@ final class LifecycleWatcher implements DefaultLifecycleObserver {
private final @NotNull IHub hub;
private final boolean enableSessionTracking;
private final boolean enableAppLifecycleBreadcrumbs;
private final @NotNull AtomicBoolean runningSession = new AtomicBoolean();

private final @NotNull ICurrentDateProvider currentDateProvider;

LifecycleWatcher(
final @NotNull IHub hub,
final long sessionIntervalMillis,
final boolean enableSessionTracking,
final boolean enableAppLifecycleBreadcrumbs) {
this(
hub,
sessionIntervalMillis,
enableSessionTracking,
enableAppLifecycleBreadcrumbs,
CurrentDateProvider.getInstance());
}

LifecycleWatcher(
final @NotNull IHub hub,
final long sessionIntervalMillis,
final boolean enableSessionTracking,
final boolean enableAppLifecycleBreadcrumbs,
final @NotNull ICurrentDateProvider currentDateProvider) {
this.sessionIntervalMillis = sessionIntervalMillis;
this.enableSessionTracking = enableSessionTracking;
this.enableAppLifecycleBreadcrumbs = enableAppLifecycleBreadcrumbs;
this.hub = hub;
this.currentDateProvider = currentDateProvider;
}

// App goes to foreground
Expand All @@ -42,12 +64,13 @@ public void onStart(final @NotNull LifecycleOwner owner) {

private void startSession() {
if (enableSessionTracking) {
final long currentTimeMillis = System.currentTimeMillis();
final long currentTimeMillis = currentDateProvider.getCurrentTimeMillis();
cancelTask();
if (lastStartedSession == 0L
|| (lastStartedSession + sessionIntervalMillis) <= currentTimeMillis) {
addSessionBreadcrumb("start");
hub.startSession();
runningSession.set(true);
}
lastStartedSession = currentTimeMillis;
}
Expand All @@ -72,6 +95,7 @@ private void scheduleEndSession() {
public void run() {
addSessionBreadcrumb("end");
hub.endSession();
runningSession.set(false);
}
};

Expand All @@ -81,6 +105,7 @@ public void run() {
private void cancelTask() {
if (timerTask != null) {
bruno-garcia marked this conversation as resolved.
Show resolved Hide resolved
timerTask.cancel();
timerTask = null;
}
}

Expand All @@ -103,4 +128,16 @@ private void addSessionBreadcrumb(final @NotNull String state) {
breadcrumb.setLevel(SentryLevel.INFO);
hub.addBreadcrumb(breadcrumb);
}

@TestOnly
@NotNull
AtomicBoolean isRunningSession() {
return runningSession;
}

@TestOnly
@Nullable
TimerTask getTimerTask() {
return timerTask;
}
}
Original file line number Diff line number Diff line change
@@ -1,93 +1,105 @@
package io.sentry.android.core

import androidx.lifecycle.LifecycleOwner
import com.nhaarman.mockitokotlin2.any
import com.nhaarman.mockitokotlin2.check
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.never
import com.nhaarman.mockitokotlin2.times
import com.nhaarman.mockitokotlin2.verify
import com.nhaarman.mockitokotlin2.whenever
import io.sentry.core.Breadcrumb
import io.sentry.core.IHub
import io.sentry.core.SentryLevel
import kotlin.test.Ignore
import io.sentry.core.transport.ICurrentDateProvider
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import org.awaitility.kotlin.await

class LifecycleWatcherTest {

private class Fixture {
val ownerMock = mock<LifecycleOwner>()
val hub = mock<IHub>()
val dateProvider = mock<ICurrentDateProvider>()

fun getSUT(sessionIntervalMillis: Long = 0L, enableSessionTracking: Boolean = true, enableAppLifecycleBreadcrumbs: Boolean = true): LifecycleWatcher {
return LifecycleWatcher(hub, sessionIntervalMillis, enableSessionTracking, enableAppLifecycleBreadcrumbs, dateProvider)
}
}

private val fixture = Fixture()

@Test
fun `if last started session is 0, start new session`() {
val hub = mock<IHub>()
val watcher = LifecycleWatcher(hub, 100L, true, false)
watcher.onStart(mock())
verify(hub).startSession()
val watcher = fixture.getSUT(enableAppLifecycleBreadcrumbs = false)
watcher.onStart(fixture.ownerMock)
verify(fixture.hub).startSession()
}

@Test
fun `if last started session is after interval, start new session`() {
val hub = mock<IHub>()
val watcher = LifecycleWatcher(hub, 100L, true, false)
watcher.onStart(mock())
Thread.sleep(150L)
watcher.onStart(mock())
verify(hub, times(2)).startSession()
val watcher = fixture.getSUT(enableAppLifecycleBreadcrumbs = false)
whenever(fixture.dateProvider.currentTimeMillis).thenReturn(1L, 2L)
watcher.onStart(fixture.ownerMock)
watcher.onStart(fixture.ownerMock)
verify(fixture.hub, times(2)).startSession()
}

@Test
fun `if last started session is before interval, it should not start a new session`() {
val hub = mock<IHub>()
val watcher = LifecycleWatcher(hub, 1000L, true, false)
watcher.onStart(mock())
Thread.sleep(100)
watcher.onStart(mock())
verify(hub).startSession()
val watcher = fixture.getSUT(enableAppLifecycleBreadcrumbs = false)
whenever(fixture.dateProvider.currentTimeMillis).thenReturn(2L, 1L)
watcher.onStart(fixture.ownerMock)
watcher.onStart(fixture.ownerMock)
verify(fixture.hub).startSession()
}

@Ignore("for some reason this is flaky only on appveyor")
@Test
fun `if app goes to background, end session after interval`() {
val hub = mock<IHub>()
val watcher = LifecycleWatcher(hub, 100L, true, false)
watcher.onStart(mock())
watcher.onStop(mock())
Thread.sleep(500L)
verify(hub).endSession()
val watcher = fixture.getSUT(enableAppLifecycleBreadcrumbs = false)
watcher.onStart(fixture.ownerMock)
watcher.onStop(fixture.ownerMock)
await.untilFalse(watcher.isRunningSession)
verify(fixture.hub).endSession()
}

@Test
fun `if app goes to background and foreground again, dont end the session`() {
val hub = mock<IHub>()
val watcher = LifecycleWatcher(hub, 1000L, true, false)
watcher.onStart(mock())
watcher.onStop(mock())
Thread.sleep(150)
watcher.onStart(mock())
verify(hub, never()).endSession()
val watcher = fixture.getSUT(sessionIntervalMillis = 30000L, enableAppLifecycleBreadcrumbs = false)
watcher.onStart(fixture.ownerMock)

watcher.onStop(fixture.ownerMock)
assertNotNull(watcher.timerTask)

watcher.onStart(fixture.ownerMock)
assertNull(watcher.timerTask)

verify(fixture.hub, never()).endSession()
}

@Test
fun `When session tracking is disabled, do not start session`() {
val hub = mock<IHub>()
val watcher = LifecycleWatcher(hub, 1000L, false, false)
watcher.onStart(mock())
verify(hub, never()).startSession()
val watcher = fixture.getSUT(enableSessionTracking = false, enableAppLifecycleBreadcrumbs = false)
watcher.onStart(fixture.ownerMock)
verify(fixture.hub, never()).startSession()
}

@Test
fun `When session tracking is disabled, do not end session`() {
val hub = mock<IHub>()
val watcher = LifecycleWatcher(hub, 0L, false, false)
watcher.onStart(mock())
verify(hub, never()).endSession()
val watcher = fixture.getSUT(enableSessionTracking = false, enableAppLifecycleBreadcrumbs = false)
watcher.onStop(fixture.ownerMock)
assertNull(watcher.timerTask)
verify(fixture.hub, never()).endSession()
}

@Test
fun `When session tracking is enabled, add breadcrumb on start`() {
val hub = mock<IHub>()
val watcher = LifecycleWatcher(hub, 0L, true, false)
watcher.onStart(mock())
Thread.sleep(150)
verify(hub).addBreadcrumb(check<Breadcrumb> {
val watcher = fixture.getSUT(enableAppLifecycleBreadcrumbs = false)
watcher.onStart(fixture.ownerMock)
verify(fixture.hub).addBreadcrumb(check<Breadcrumb> {
assertEquals("app.lifecycle", it.category)
assertEquals("session", it.type)
assertEquals(SentryLevel.INFO, it.level)
Expand All @@ -97,11 +109,11 @@ class LifecycleWatcherTest {

@Test
fun `When session tracking is enabled, add breadcrumb on stop`() {
val hub = mock<IHub>()
val watcher = LifecycleWatcher(hub, 0L, true, false)
watcher.onStop(mock())
Thread.sleep(150)
verify(hub).addBreadcrumb(check<Breadcrumb> {
val watcher = fixture.getSUT(enableAppLifecycleBreadcrumbs = false)
watcher.isRunningSession.set(true)
watcher.onStop(fixture.ownerMock)
await.untilFalse(watcher.isRunningSession)
verify(fixture.hub).addBreadcrumb(check<Breadcrumb> {
assertEquals("app.lifecycle", it.category)
assertEquals("session", it.type)
assertEquals(SentryLevel.INFO, it.level)
Expand All @@ -111,26 +123,24 @@ class LifecycleWatcherTest {

@Test
fun `When session tracking is disabled, do not add breadcrumb on start`() {
val hub = mock<IHub>()
val watcher = LifecycleWatcher(hub, 0L, false, false)
watcher.onStart(mock())
verify(hub, never()).addBreadcrumb(any<Breadcrumb>())
val watcher = fixture.getSUT(enableSessionTracking = false, enableAppLifecycleBreadcrumbs = false)
watcher.onStart(fixture.ownerMock)
verify(fixture.hub, never()).addBreadcrumb(any<Breadcrumb>())
}

@Test
fun `When session tracking is disabled, do not add breadcrumb on stop`() {
val hub = mock<IHub>()
val watcher = LifecycleWatcher(hub, 0L, false, false)
watcher.onStop(mock())
verify(hub, never()).addBreadcrumb(any<Breadcrumb>())
val watcher = fixture.getSUT(enableSessionTracking = false, enableAppLifecycleBreadcrumbs = false)
watcher.onStop(fixture.ownerMock)
assertNull(watcher.timerTask)
verify(fixture.hub, never()).addBreadcrumb(any<Breadcrumb>())
}

@Test
fun `When app lifecycle breadcrumbs is enabled, add breadcrumb on start`() {
val hub = mock<IHub>()
val watcher = LifecycleWatcher(hub, 0L, false, true)
watcher.onStart(mock())
verify(hub).addBreadcrumb(check<Breadcrumb> {
val watcher = fixture.getSUT(enableSessionTracking = false)
watcher.onStart(fixture.ownerMock)
verify(fixture.hub).addBreadcrumb(check<Breadcrumb> {
assertEquals("app.lifecycle", it.category)
assertEquals("navigation", it.type)
assertEquals(SentryLevel.INFO, it.level)
Expand All @@ -140,18 +150,16 @@ class LifecycleWatcherTest {

@Test
fun `When app lifecycle breadcrumbs is disabled, do not add breadcrumb on start`() {
val hub = mock<IHub>()
val watcher = LifecycleWatcher(hub, 0L, false, false)
watcher.onStart(mock())
verify(hub, never()).addBreadcrumb(any<Breadcrumb>())
val watcher = fixture.getSUT(enableSessionTracking = false, enableAppLifecycleBreadcrumbs = false)
watcher.onStart(fixture.ownerMock)
verify(fixture.hub, never()).addBreadcrumb(any<Breadcrumb>())
}

@Test
fun `When app lifecycle breadcrumbs is enabled, add breadcrumb on stop`() {
val hub = mock<IHub>()
val watcher = LifecycleWatcher(hub, 0L, false, true)
watcher.onStop(mock())
verify(hub).addBreadcrumb(check<Breadcrumb> {
val watcher = fixture.getSUT(enableSessionTracking = false)
watcher.onStop(fixture.ownerMock)
verify(fixture.hub).addBreadcrumb(check<Breadcrumb> {
assertEquals("app.lifecycle", it.category)
assertEquals("navigation", it.type)
assertEquals(SentryLevel.INFO, it.level)
Expand All @@ -161,9 +169,8 @@ class LifecycleWatcherTest {

@Test
fun `When app lifecycle breadcrumbs is disabled, do not add breadcrumb on stop`() {
val hub = mock<IHub>()
val watcher = LifecycleWatcher(hub, 0L, false, false)
watcher.onStop(mock())
verify(hub, never()).addBreadcrumb(any<Breadcrumb>())
val watcher = fixture.getSUT(enableSessionTracking = false, enableAppLifecycleBreadcrumbs = false)
watcher.onStop(fixture.ownerMock)
verify(fixture.hub, never()).addBreadcrumb(any<Breadcrumb>())
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
package io.sentry.core.transport;

final class CurrentDateProvider implements ICurrentDateProvider {
import org.jetbrains.annotations.ApiStatus;

@ApiStatus.Internal
public final class CurrentDateProvider implements ICurrentDateProvider {

private static final ICurrentDateProvider instance = new CurrentDateProvider();

public static ICurrentDateProvider getInstance() {
return instance;
}

private CurrentDateProvider() {}

@Override
public final long getCurrentTimeMillis() {
Expand Down
Loading