diff --git a/NewRelicVideoCore/build.gradle b/NewRelicVideoCore/build.gradle index b2bca339..455d1efc 100644 --- a/NewRelicVideoCore/build.gradle +++ b/NewRelicVideoCore/build.gradle @@ -1,6 +1,7 @@ plugins { id 'com.android.library' id 'maven-publish' + id 'jacoco' } android { compileSdkVersion 34 @@ -19,6 +20,7 @@ android { proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' buildConfigField("long", "VERSION_CODE", "${defaultConfig.versionCode}") buildConfigField("String","VERSION_NAME","\"${defaultConfig.versionName}\"") + testCoverageEnabled true } release { minifyEnabled false @@ -28,8 +30,18 @@ android { } } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 + } + testOptions { + unitTests { + all { + jacoco { + includeNoLocationClasses = true + excludes = ['jdk.internal.*'] + } + } + } } namespace 'com.newrelic.videoagent.core' } @@ -44,6 +56,8 @@ dependencies { // implementation 'androidx.leanback:leanback:1.0.0' testImplementation 'junit:junit:4.13.2' + testImplementation 'org.robolectric:robolectric:4.10.3' + testImplementation 'org.mockito:mockito-core:4.11.0' // Updated AndroidX Test dependencies that are compatible with Android 12+ androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' @@ -62,3 +76,71 @@ afterEvaluate { } } } + +jacoco { + toolVersion = "0.8.10" +} + +tasks.register('jacocoTestReport', JacocoReport) { + mustRunAfter 'testDebugUnitTest' + + reports { + xml.required = true + html.required = true + csv.required = false + } + + def fileFilter = [ + '**/R.class', + '**/R$*.class', + '**/BuildConfig.*', + '**/Manifest*.*', + '**/*Test*.*', + 'android/**/*.*' + ] + + def debugTree = fileTree(dir: "${buildDir}/intermediates/javac/debug/classes", excludes: fileFilter) + def mainSrc = "${project.projectDir}/src/main/java" + + sourceDirectories.setFrom(files([mainSrc])) + classDirectories.setFrom(files([debugTree])) + executionData.setFrom(files("${project.buildDir}/outputs/unit_test_code_coverage/debugUnitTest/testDebugUnitTest.exec")) +} + +tasks.register('jacocoCoverageVerification', JacocoCoverageVerification) { + mustRunAfter 'testDebugUnitTest' + + violationRules { + rule { + enabled = true + element = 'BUNDLE' + limit { + counter = 'INSTRUCTION' + value = 'COVEREDRATIO' + minimum = 0.50 + } + } + } + + def fileFilter = [ + '**/R.class', + '**/R$*.class', + '**/BuildConfig.*', + '**/Manifest*.*', + '**/*Test*.*', + 'android/**/*.*' + ] + + def debugTree = fileTree(dir: "${buildDir}/intermediates/javac/debug/classes", excludes: fileFilter) + def mainSrc = "${project.projectDir}/src/main/java" + + sourceDirectories.setFrom(files([mainSrc])) + + classDirectories.setFrom(files([debugTree])) + executionData.setFrom(files("${project.buildDir}/outputs/unit_test_code_coverage/debugUnitTest/testDebugUnitTest.exec")) +} + +tasks.named('check') { + dependsOn 'jacocoCoverageVerification' +} + diff --git a/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/NRDefTest.java b/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/NRDefTest.java new file mode 100644 index 00000000..9f8ad2a4 --- /dev/null +++ b/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/NRDefTest.java @@ -0,0 +1,216 @@ +package com.newrelic.videoagent.core; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Unit tests for NRDef that verify constant values. + */ +public class NRDefTest { + + @Test + public void testVideoEventConstants() { + assertEquals("VideoAction", NRDef.NR_VIDEO_EVENT); + assertEquals("VideoAdAction", NRDef.NR_VIDEO_AD_EVENT); + assertEquals("VideoErrorAction", NRDef.NR_VIDEO_ERROR_EVENT); + assertEquals("VideoCustomAction", NRDef.NR_VIDEO_CUSTOM_EVENT); + } + + @Test + public void testSourceConstant() { + assertEquals("ANDROID", NRDef.SRC); + } + + @Test + public void testTrackerReadyConstants() { + assertEquals("TRACKER_READY", NRDef.TRACKER_READY); + assertEquals("PLAYER_READY", NRDef.PLAYER_READY); + } + + @Test + public void testContentEventConstants() { + assertEquals("CONTENT_REQUEST", NRDef.CONTENT_REQUEST); + assertEquals("CONTENT_START", NRDef.CONTENT_START); + assertEquals("CONTENT_PAUSE", NRDef.CONTENT_PAUSE); + assertEquals("CONTENT_RESUME", NRDef.CONTENT_RESUME); + assertEquals("CONTENT_END", NRDef.CONTENT_END); + } + + @Test + public void testContentSeekConstants() { + assertEquals("CONTENT_SEEK_START", NRDef.CONTENT_SEEK_START); + assertEquals("CONTENT_SEEK_END", NRDef.CONTENT_SEEK_END); + } + + @Test + public void testContentBufferConstants() { + assertEquals("CONTENT_BUFFER_START", NRDef.CONTENT_BUFFER_START); + assertEquals("CONTENT_BUFFER_END", NRDef.CONTENT_BUFFER_END); + } + + @Test + public void testContentMiscConstants() { + assertEquals("CONTENT_HEARTBEAT", NRDef.CONTENT_HEARTBEAT); + assertEquals("CONTENT_RENDITION_CHANGE", NRDef.CONTENT_RENDITION_CHANGE); + assertEquals("CONTENT_ERROR", NRDef.CONTENT_ERROR); + } + + @Test + public void testAdEventConstants() { + assertEquals("AD_REQUEST", NRDef.AD_REQUEST); + assertEquals("AD_START", NRDef.AD_START); + assertEquals("AD_PAUSE", NRDef.AD_PAUSE); + assertEquals("AD_RESUME", NRDef.AD_RESUME); + assertEquals("AD_END", NRDef.AD_END); + } + + @Test + public void testAdSeekConstants() { + assertEquals("AD_SEEK_START", NRDef.AD_SEEK_START); + assertEquals("AD_SEEK_END", NRDef.AD_SEEK_END); + } + + @Test + public void testAdBufferConstants() { + assertEquals("AD_BUFFER_START", NRDef.AD_BUFFER_START); + assertEquals("AD_BUFFER_END", NRDef.AD_BUFFER_END); + } + + @Test + public void testAdMiscConstants() { + assertEquals("AD_HEARTBEAT", NRDef.AD_HEARTBEAT); + assertEquals("AD_RENDITION_CHANGE", NRDef.AD_RENDITION_CHANGE); + assertEquals("AD_ERROR", NRDef.AD_ERROR); + } + + @Test + public void testAdBreakConstants() { + assertEquals("AD_BREAK_START", NRDef.AD_BREAK_START); + assertEquals("AD_BREAK_END", NRDef.AD_BREAK_END); + } + + @Test + public void testAdInteractionConstants() { + assertEquals("AD_QUARTILE", NRDef.AD_QUARTILE); + assertEquals("AD_CLICK", NRDef.AD_CLICK); + } + + @Test + public void testVersionConstant() { + assertNotNull(NRDef.NRVIDEO_CORE_VERSION); + assertFalse(NRDef.NRVIDEO_CORE_VERSION.isEmpty()); + } + + @Test + public void testAllContentConstantsAreUnique() { + String[] contentConstants = { + NRDef.CONTENT_REQUEST, + NRDef.CONTENT_START, + NRDef.CONTENT_PAUSE, + NRDef.CONTENT_RESUME, + NRDef.CONTENT_END, + NRDef.CONTENT_SEEK_START, + NRDef.CONTENT_SEEK_END, + NRDef.CONTENT_BUFFER_START, + NRDef.CONTENT_BUFFER_END, + NRDef.CONTENT_HEARTBEAT, + NRDef.CONTENT_RENDITION_CHANGE, + NRDef.CONTENT_ERROR + }; + + for (int i = 0; i < contentConstants.length; i++) { + for (int j = i + 1; j < contentConstants.length; j++) { + assertNotEquals(contentConstants[i], contentConstants[j]); + } + } + } + + @Test + public void testAllAdConstantsAreUnique() { + String[] adConstants = { + NRDef.AD_REQUEST, + NRDef.AD_START, + NRDef.AD_PAUSE, + NRDef.AD_RESUME, + NRDef.AD_END, + NRDef.AD_SEEK_START, + NRDef.AD_SEEK_END, + NRDef.AD_BUFFER_START, + NRDef.AD_BUFFER_END, + NRDef.AD_HEARTBEAT, + NRDef.AD_RENDITION_CHANGE, + NRDef.AD_ERROR, + NRDef.AD_BREAK_START, + NRDef.AD_BREAK_END, + NRDef.AD_QUARTILE, + NRDef.AD_CLICK + }; + + for (int i = 0; i < adConstants.length; i++) { + for (int j = i + 1; j < adConstants.length; j++) { + assertNotEquals(adConstants[i], adConstants[j]); + } + } + } + + @Test + public void testContentConstantsStartWithCONTENT() { + assertTrue(NRDef.CONTENT_REQUEST.startsWith("CONTENT_")); + assertTrue(NRDef.CONTENT_START.startsWith("CONTENT_")); + assertTrue(NRDef.CONTENT_PAUSE.startsWith("CONTENT_")); + assertTrue(NRDef.CONTENT_RESUME.startsWith("CONTENT_")); + assertTrue(NRDef.CONTENT_END.startsWith("CONTENT_")); + assertTrue(NRDef.CONTENT_SEEK_START.startsWith("CONTENT_")); + assertTrue(NRDef.CONTENT_SEEK_END.startsWith("CONTENT_")); + assertTrue(NRDef.CONTENT_BUFFER_START.startsWith("CONTENT_")); + assertTrue(NRDef.CONTENT_BUFFER_END.startsWith("CONTENT_")); + assertTrue(NRDef.CONTENT_HEARTBEAT.startsWith("CONTENT_")); + assertTrue(NRDef.CONTENT_RENDITION_CHANGE.startsWith("CONTENT_")); + assertTrue(NRDef.CONTENT_ERROR.startsWith("CONTENT_")); + } + + @Test + public void testAdConstantsStartWithAD() { + assertTrue(NRDef.AD_REQUEST.startsWith("AD_")); + assertTrue(NRDef.AD_START.startsWith("AD_")); + assertTrue(NRDef.AD_PAUSE.startsWith("AD_")); + assertTrue(NRDef.AD_RESUME.startsWith("AD_")); + assertTrue(NRDef.AD_END.startsWith("AD_")); + assertTrue(NRDef.AD_SEEK_START.startsWith("AD_")); + assertTrue(NRDef.AD_SEEK_END.startsWith("AD_")); + assertTrue(NRDef.AD_BUFFER_START.startsWith("AD_")); + assertTrue(NRDef.AD_BUFFER_END.startsWith("AD_")); + assertTrue(NRDef.AD_HEARTBEAT.startsWith("AD_")); + assertTrue(NRDef.AD_RENDITION_CHANGE.startsWith("AD_")); + assertTrue(NRDef.AD_ERROR.startsWith("AD_")); + assertTrue(NRDef.AD_BREAK_START.startsWith("AD_")); + assertTrue(NRDef.AD_BREAK_END.startsWith("AD_")); + assertTrue(NRDef.AD_QUARTILE.startsWith("AD_")); + assertTrue(NRDef.AD_CLICK.startsWith("AD_")); + } + + @Test + public void testConstantsAreNotNull() { + assertNotNull(NRDef.NRVIDEO_CORE_VERSION); + assertNotNull(NRDef.NR_VIDEO_EVENT); + assertNotNull(NRDef.NR_VIDEO_AD_EVENT); + assertNotNull(NRDef.NR_VIDEO_ERROR_EVENT); + assertNotNull(NRDef.NR_VIDEO_CUSTOM_EVENT); + assertNotNull(NRDef.SRC); + assertNotNull(NRDef.TRACKER_READY); + assertNotNull(NRDef.PLAYER_READY); + } + + @Test + public void testConstantsAreNotEmpty() { + assertFalse(NRDef.NRVIDEO_CORE_VERSION.isEmpty()); + assertFalse(NRDef.NR_VIDEO_EVENT.isEmpty()); + assertFalse(NRDef.NR_VIDEO_AD_EVENT.isEmpty()); + assertFalse(NRDef.NR_VIDEO_ERROR_EVENT.isEmpty()); + assertFalse(NRDef.NR_VIDEO_CUSTOM_EVENT.isEmpty()); + assertFalse(NRDef.SRC.isEmpty()); + assertFalse(NRDef.TRACKER_READY.isEmpty()); + assertFalse(NRDef.PLAYER_READY.isEmpty()); + } +} diff --git a/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/NRVideoConstantsTest.java b/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/NRVideoConstantsTest.java new file mode 100644 index 00000000..3b4e0f20 --- /dev/null +++ b/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/NRVideoConstantsTest.java @@ -0,0 +1,168 @@ +package com.newrelic.videoagent.core; + +import org.junit.Test; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; + +import static org.junit.Assert.*; + +/** + * Unit tests for NRVideoConstants. + */ +public class NRVideoConstantsTest { + + @Test + public void testDeviceTypeConstants() { + assertEquals("AndroidTV", NRVideoConstants.ANDROID_TV); + assertEquals("Mobile", NRVideoConstants.MOBILE); + } + + @Test + public void testEventTypeConstants() { + assertEquals("live", NRVideoConstants.EVENT_TYPE_LIVE); + assertEquals("ondemand", NRVideoConstants.EVENT_TYPE_ONDEMAND); + } + + @Test + public void testDeprecatedCategoryConstants() { + assertEquals("default", NRVideoConstants.CATEGORY_DEFAULT); + assertEquals("normal", NRVideoConstants.CATEGORY_NORMAL); + } + + @Test + public void testHealthStatusConstants() { + assertEquals("IDLE", NRVideoConstants.HEALTH_IDLE); + assertEquals("HEALTHY", NRVideoConstants.HEALTH_HEALTHY); + assertEquals("WARNING", NRVideoConstants.HEALTH_WARNING); + assertEquals("DEGRADED", NRVideoConstants.HEALTH_DEGRADED); + assertEquals("CRITICAL", NRVideoConstants.HEALTH_CRITICAL); + } + + @Test + public void testDeviceTypeConstantsAreUnique() { + assertNotEquals(NRVideoConstants.ANDROID_TV, NRVideoConstants.MOBILE); + } + + @Test + public void testEventTypeConstantsAreUnique() { + assertNotEquals(NRVideoConstants.EVENT_TYPE_LIVE, NRVideoConstants.EVENT_TYPE_ONDEMAND); + } + + @Test + public void testHealthStatusConstantsAreUnique() { + String[] healthStatuses = { + NRVideoConstants.HEALTH_IDLE, + NRVideoConstants.HEALTH_HEALTHY, + NRVideoConstants.HEALTH_WARNING, + NRVideoConstants.HEALTH_DEGRADED, + NRVideoConstants.HEALTH_CRITICAL + }; + + for (int i = 0; i < healthStatuses.length; i++) { + for (int j = i + 1; j < healthStatuses.length; j++) { + assertNotEquals(healthStatuses[i], healthStatuses[j]); + } + } + } + + @Test + public void testConstantsAreNotNull() { + assertNotNull(NRVideoConstants.ANDROID_TV); + assertNotNull(NRVideoConstants.MOBILE); + assertNotNull(NRVideoConstants.EVENT_TYPE_LIVE); + assertNotNull(NRVideoConstants.EVENT_TYPE_ONDEMAND); + assertNotNull(NRVideoConstants.CATEGORY_DEFAULT); + assertNotNull(NRVideoConstants.CATEGORY_NORMAL); + assertNotNull(NRVideoConstants.HEALTH_IDLE); + assertNotNull(NRVideoConstants.HEALTH_HEALTHY); + assertNotNull(NRVideoConstants.HEALTH_WARNING); + assertNotNull(NRVideoConstants.HEALTH_DEGRADED); + assertNotNull(NRVideoConstants.HEALTH_CRITICAL); + } + + @Test + public void testConstantsAreNotEmpty() { + assertFalse(NRVideoConstants.ANDROID_TV.isEmpty()); + assertFalse(NRVideoConstants.MOBILE.isEmpty()); + assertFalse(NRVideoConstants.EVENT_TYPE_LIVE.isEmpty()); + assertFalse(NRVideoConstants.EVENT_TYPE_ONDEMAND.isEmpty()); + assertFalse(NRVideoConstants.CATEGORY_DEFAULT.isEmpty()); + assertFalse(NRVideoConstants.CATEGORY_NORMAL.isEmpty()); + assertFalse(NRVideoConstants.HEALTH_IDLE.isEmpty()); + assertFalse(NRVideoConstants.HEALTH_HEALTHY.isEmpty()); + assertFalse(NRVideoConstants.HEALTH_WARNING.isEmpty()); + assertFalse(NRVideoConstants.HEALTH_DEGRADED.isEmpty()); + assertFalse(NRVideoConstants.HEALTH_CRITICAL.isEmpty()); + } + + @Test + public void testEventTypesAreLowercase() { + assertEquals(NRVideoConstants.EVENT_TYPE_LIVE.toLowerCase(), NRVideoConstants.EVENT_TYPE_LIVE); + assertEquals(NRVideoConstants.EVENT_TYPE_ONDEMAND.toLowerCase(), NRVideoConstants.EVENT_TYPE_ONDEMAND); + } + + @Test + public void testHealthStatusesAreUppercase() { + assertEquals(NRVideoConstants.HEALTH_IDLE.toUpperCase(), NRVideoConstants.HEALTH_IDLE); + assertEquals(NRVideoConstants.HEALTH_HEALTHY.toUpperCase(), NRVideoConstants.HEALTH_HEALTHY); + assertEquals(NRVideoConstants.HEALTH_WARNING.toUpperCase(), NRVideoConstants.HEALTH_WARNING); + assertEquals(NRVideoConstants.HEALTH_DEGRADED.toUpperCase(), NRVideoConstants.HEALTH_DEGRADED); + assertEquals(NRVideoConstants.HEALTH_CRITICAL.toUpperCase(), NRVideoConstants.HEALTH_CRITICAL); + } + + @Test + public void testConstructorThrowsCorrectException() { + try { + Constructor constructor = NRVideoConstants.class.getDeclaredConstructor(); + constructor.setAccessible(true); + constructor.newInstance(); + fail("Expected AssertionError to be thrown"); + } catch (InvocationTargetException e) { + assertTrue(e.getCause() instanceof AssertionError); + assertEquals("Constants class should not be instantiated", e.getCause().getMessage()); + } catch (Exception e) { + fail("Unexpected exception: " + e.getClass().getName()); + } + } + + @Test + public void testDeviceTypeAndroidTV() { + String deviceType = NRVideoConstants.ANDROID_TV; + assertEquals("AndroidTV", deviceType); + assertTrue(deviceType.contains("Android")); + assertTrue(deviceType.contains("TV")); + } + + @Test + public void testHealthStatusOrder() { + // Health statuses should represent increasing severity levels + assertNotNull(NRVideoConstants.HEALTH_IDLE); + assertNotNull(NRVideoConstants.HEALTH_HEALTHY); + assertNotNull(NRVideoConstants.HEALTH_WARNING); + assertNotNull(NRVideoConstants.HEALTH_DEGRADED); + assertNotNull(NRVideoConstants.HEALTH_CRITICAL); + } + + @Test + public void testDeprecatedConstantsStillAccessible() { + // Even though deprecated, they should still be accessible for backward compatibility + @SuppressWarnings("deprecation") + String categoryDefault = NRVideoConstants.CATEGORY_DEFAULT; + @SuppressWarnings("deprecation") + String categoryNormal = NRVideoConstants.CATEGORY_NORMAL; + + assertEquals("default", categoryDefault); + assertEquals("normal", categoryNormal); + } + + @Test + public void testCategoryConstantsAreDifferent() { + @SuppressWarnings("deprecation") + String cat1 = NRVideoConstants.CATEGORY_DEFAULT; + @SuppressWarnings("deprecation") + String cat2 = NRVideoConstants.CATEGORY_NORMAL; + + assertNotEquals(cat1, cat2); + } +} diff --git a/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/NRVideoPlayerConfigurationTest.java b/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/NRVideoPlayerConfigurationTest.java new file mode 100644 index 00000000..4ac0a1d3 --- /dev/null +++ b/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/NRVideoPlayerConfigurationTest.java @@ -0,0 +1,276 @@ +package com.newrelic.videoagent.core; + +import androidx.media3.exoplayer.ExoPlayer; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.*; + +/** + * Unit tests for NRVideoPlayerConfiguration. + */ +public class NRVideoPlayerConfigurationTest { + + @Mock + private ExoPlayer mockPlayer; + + @Before + public void setUp() { + MockitoAnnotations.openMocks(this); + } + + @Test + public void testConstructorWithAllParameters() { + Map customAttrs = new HashMap<>(); + customAttrs.put("key1", "value1"); + + NRVideoPlayerConfiguration config = new NRVideoPlayerConfiguration( + "TestPlayer", + mockPlayer, + true, + customAttrs + ); + + assertNotNull(config); + assertEquals("TestPlayer", config.getPlayerName()); + assertEquals(mockPlayer, config.getPlayer()); + assertTrue(config.isAdEnabled()); + assertEquals(customAttrs, config.getCustomAttributes()); + } + + @Test + public void testConstructorWithAdDisabled() { + NRVideoPlayerConfiguration config = new NRVideoPlayerConfiguration( + "TestPlayer", + mockPlayer, + false, + null + ); + + assertNotNull(config); + assertFalse(config.isAdEnabled()); + } + + @Test + public void testGetPlayerName() { + NRVideoPlayerConfiguration config = new NRVideoPlayerConfiguration( + "MyPlayer", + mockPlayer, + false, + null + ); + + assertEquals("MyPlayer", config.getPlayerName()); + } + + @Test + public void testGetPlayer() { + NRVideoPlayerConfiguration config = new NRVideoPlayerConfiguration( + "TestPlayer", + mockPlayer, + false, + null + ); + + assertSame(mockPlayer, config.getPlayer()); + } + + @Test + public void testGetCustomAttributes() { + Map customAttrs = new HashMap<>(); + customAttrs.put("attr1", "value1"); + customAttrs.put("attr2", 42); + + NRVideoPlayerConfiguration config = new NRVideoPlayerConfiguration( + "TestPlayer", + mockPlayer, + false, + customAttrs + ); + + Map result = config.getCustomAttributes(); + assertEquals(customAttrs, result); + assertEquals("value1", result.get("attr1")); + assertEquals(42, result.get("attr2")); + } + + @Test + public void testIsAdEnabled() { + NRVideoPlayerConfiguration configWithAds = new NRVideoPlayerConfiguration( + "TestPlayer", + mockPlayer, + true, + null + ); + + NRVideoPlayerConfiguration configWithoutAds = new NRVideoPlayerConfiguration( + "TestPlayer", + mockPlayer, + false, + null + ); + + assertTrue(configWithAds.isAdEnabled()); + assertFalse(configWithoutAds.isAdEnabled()); + } + + @Test + public void testConstructorWithNullPlayerName() { + NRVideoPlayerConfiguration config = new NRVideoPlayerConfiguration( + null, + mockPlayer, + false, + null + ); + + assertNull(config.getPlayerName()); + } + + @Test + public void testConstructorWithNullPlayer() { + NRVideoPlayerConfiguration config = new NRVideoPlayerConfiguration( + "TestPlayer", + null, + false, + null + ); + + assertNull(config.getPlayer()); + } + + @Test + public void testConstructorWithNullCustomAttributes() { + NRVideoPlayerConfiguration config = new NRVideoPlayerConfiguration( + "TestPlayer", + mockPlayer, + false, + null + ); + + assertNull(config.getCustomAttributes()); + } + + @Test + public void testConstructorWithEmptyCustomAttributes() { + Map emptyAttrs = new HashMap<>(); + + NRVideoPlayerConfiguration config = new NRVideoPlayerConfiguration( + "TestPlayer", + mockPlayer, + false, + emptyAttrs + ); + + assertNotNull(config.getCustomAttributes()); + assertTrue(config.getCustomAttributes().isEmpty()); + } + + @Test + public void testConstructorWithMultipleCustomAttributes() { + Map customAttrs = new HashMap<>(); + customAttrs.put("stringAttr", "test"); + customAttrs.put("intAttr", 123); + customAttrs.put("boolAttr", true); + customAttrs.put("doubleAttr", 3.14); + + NRVideoPlayerConfiguration config = new NRVideoPlayerConfiguration( + "TestPlayer", + mockPlayer, + true, + customAttrs + ); + + Map result = config.getCustomAttributes(); + assertEquals(4, result.size()); + assertEquals("test", result.get("stringAttr")); + assertEquals(123, result.get("intAttr")); + assertEquals(true, result.get("boolAttr")); + assertEquals(3.14, result.get("doubleAttr")); + } + + @Test + public void testPlayerNameWithSpecialCharacters() { + String specialName = "Player@#$%^&*()_+-=[]{}|;':\",./<>?"; + + NRVideoPlayerConfiguration config = new NRVideoPlayerConfiguration( + specialName, + mockPlayer, + false, + null + ); + + assertEquals(specialName, config.getPlayerName()); + } + + @Test + public void testPlayerNameWithEmptyString() { + NRVideoPlayerConfiguration config = new NRVideoPlayerConfiguration( + "", + mockPlayer, + false, + null + ); + + assertEquals("", config.getPlayerName()); + } + + @Test + public void testMultipleInstancesWithSamePlayer() { + NRVideoPlayerConfiguration config1 = new NRVideoPlayerConfiguration( + "Player1", + mockPlayer, + true, + null + ); + + NRVideoPlayerConfiguration config2 = new NRVideoPlayerConfiguration( + "Player2", + mockPlayer, + false, + null + ); + + assertSame(config1.getPlayer(), config2.getPlayer()); + assertNotEquals(config1.getPlayerName(), config2.getPlayerName()); + } + + @Test + public void testCustomAttributesImmutability() { + Map originalAttrs = new HashMap<>(); + originalAttrs.put("key", "value"); + + NRVideoPlayerConfiguration config = new NRVideoPlayerConfiguration( + "TestPlayer", + mockPlayer, + false, + originalAttrs + ); + + Map retrievedAttrs = config.getCustomAttributes(); + assertSame(originalAttrs, retrievedAttrs); + } + + @Test + public void testAllFieldsAreAccessible() { + Map customAttrs = new HashMap<>(); + customAttrs.put("test", "value"); + + NRVideoPlayerConfiguration config = new NRVideoPlayerConfiguration( + "CompletePlayer", + mockPlayer, + true, + customAttrs + ); + + assertNotNull(config.getPlayerName()); + assertNotNull(config.getPlayer()); + assertNotNull(config.getCustomAttributes()); + assertTrue(config.isAdEnabled()); + } +} diff --git a/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/NRVideoTest.java b/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/NRVideoTest.java new file mode 100644 index 00000000..0ccf3380 --- /dev/null +++ b/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/NRVideoTest.java @@ -0,0 +1,184 @@ +package com.newrelic.videoagent.core; + +import android.app.Application; +import android.content.Context; + +import androidx.media3.exoplayer.ExoPlayer; + +import com.newrelic.videoagent.core.tracker.NRTracker; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +/** + * Comprehensive unit tests for NRVideo singleton. + * Tests initialization, builder pattern, player management, and static API methods. + */ +@RunWith(RobolectricTestRunner.class) +public class NRVideoTest { + + @Mock + private ExoPlayer mockPlayer; + + @Mock + private NRVideoConfiguration mockConfig; + + private Context context; + private NRVideoConfiguration testConfig; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.openMocks(this); + context = RuntimeEnvironment.getApplication(); + + // Reset singleton instance before each test + resetNRVideoSingleton(); + + // Create a valid test configuration + testConfig = new NRVideoConfiguration.Builder("test-app-token-1234567890") + .build(); + } + + @After + public void tearDown() throws Exception { + // Clean up singleton after each test + resetNRVideoSingleton(); + } + + /** + * Helper method to reset the NRVideo singleton using reflection + */ + private void resetNRVideoSingleton() throws Exception { + Field instanceField = NRVideo.class.getDeclaredField("instance"); + instanceField.setAccessible(true); + instanceField.set(null, null); + } + + // ========== Singleton Tests ========== + + @Test + public void testGetInstanceBeforeInitialization() { + NRVideo instance = NRVideo.getInstance(); + assertNull("getInstance should return null before initialization", instance); + } + + @Test + public void testIsInitializedBeforeInitialization() { + assertFalse("isInitialized should return false before initialization", + NRVideo.isInitialized()); + } + + @Test + public void testIsInitializedAfterInitialization() { + NRVideo.newBuilder(context) + .withConfiguration(testConfig) + .build(); + + assertTrue("isInitialized should return true after initialization", + NRVideo.isInitialized()); + } + + @Test + public void testGetInstanceAfterInitialization() { + NRVideo.newBuilder(context) + .withConfiguration(testConfig) + .build(); + + NRVideo instance = NRVideo.getInstance(); + assertNotNull("getInstance should return instance after initialization", instance); + } + + @Test + public void testGetInstanceReturnsSameInstance() { + NRVideo.newBuilder(context) + .withConfiguration(testConfig) + .build(); + + NRVideo instance1 = NRVideo.getInstance(); + NRVideo instance2 = NRVideo.getInstance(); + + assertSame("getInstance should always return the same instance", instance1, instance2); + } + + // ========== Builder Tests ========== + + @Test + public void testBuilderCreation() { + NRVideo.Builder builder = NRVideo.newBuilder(context); + assertNotNull("Builder should be created", builder); + } + + @Test(expected = IllegalStateException.class) + public void testBuildWithoutConfiguration() { + NRVideo.newBuilder(context).build(); + } + + @Test + public void testBuildWithConfiguration() { + NRVideo instance = NRVideo.newBuilder(context) + .withConfiguration(testConfig) + .build(); + + assertNotNull("Build should return instance", instance); + assertTrue("Should be initialized", NRVideo.isInitialized()); + } + + @Test(expected = RuntimeException.class) + public void testMultipleInitializationAttempts() { + NRVideo.newBuilder(context) + .withConfiguration(testConfig) + .build(); + + // Second initialization attempt should throw + NRVideo.newBuilder(context) + .withConfiguration(testConfig) + .build(); + } + + @Test + public void testBuilderChaining() { + NRVideo.Builder builder = NRVideo.newBuilder(context); + NRVideo.Builder result = builder.withConfiguration(testConfig); + + assertSame("Builder methods should return same instance for chaining", builder, result); + } + + @Test + public void testBuildWithApplicationContext() { + Application app = RuntimeEnvironment.getApplication(); + + NRVideo instance = NRVideo.newBuilder(app) + .withConfiguration(testConfig) + .build(); + + assertNotNull("Should work with Application context", instance); + } + + @Test(expected = IllegalStateException.class) + public void testAddPlayerBeforeInitialization() { + NRVideoPlayerConfiguration playerConfig = new NRVideoPlayerConfiguration( + "TestPlayer", + mockPlayer, + false, + null + ); + + NRVideo.addPlayer(playerConfig); + } + +} \ No newline at end of file diff --git a/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/auth/TokenManagerTest.java b/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/auth/TokenManagerTest.java new file mode 100644 index 00000000..3d8d74e8 --- /dev/null +++ b/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/auth/TokenManagerTest.java @@ -0,0 +1,458 @@ +package com.newrelic.videoagent.core.auth; + +import android.content.Context; +import android.content.SharedPreferences; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; + +import com.newrelic.videoagent.core.NRVideoConfiguration; +import com.newrelic.videoagent.core.device.DeviceInformation; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +import java.io.IOException; +import java.util.List; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +/** + * Unit tests for TokenManager. + * Tests token generation, caching, validation, and thread-safety. + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = 28, manifest = Config.NONE) +public class TokenManagerTest { + + @Mock + private NRVideoConfiguration mockConfiguration; + + @Mock + private PackageManager mockPackageManager; + + @Mock + private PackageInfo mockPackageInfo; + + @Mock + private ApplicationInfo mockApplicationInfo; + + private Context context; + private TokenManager tokenManager; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.openMocks(this); + + context = RuntimeEnvironment.getApplication(); + + // Setup configuration mocks + when(mockConfiguration.getApplicationToken()).thenReturn("test-app-token"); + when(mockConfiguration.getRegion()).thenReturn("US"); + + // Note: PackageManager mocking not needed for most tests + // TokenManager uses real context which provides package info + // These tests focus on initialization, caching, and validation logic + } + + @Test + public void testConstructorWithValidConfiguration() { + tokenManager = new TokenManager(context, mockConfiguration); + + assertNotNull(tokenManager); + } + + @Test(expected = IllegalArgumentException.class) + public void testConstructorWithNullContext() { + new TokenManager(null, mockConfiguration); + } + + @Test(expected = IllegalArgumentException.class) + public void testConstructorWithNullConfiguration() { + new TokenManager(context, null); + } + + @Test + public void testConstructorLoadsConfiguration() { + tokenManager = new TokenManager(context, mockConfiguration); + + verify(mockConfiguration).getRegion(); + } + + @Test + public void testTokenEndpointForUSRegion() { + when(mockConfiguration.getRegion()).thenReturn("US"); + + tokenManager = new TokenManager(context, mockConfiguration); + + // Endpoint is built internally, we can't directly test it + // But we can verify the configuration was read + verify(mockConfiguration).getRegion(); + } + + @Test + public void testTokenEndpointForEURegion() { + when(mockConfiguration.getRegion()).thenReturn("EU"); + + tokenManager = new TokenManager(context, mockConfiguration); + + verify(mockConfiguration).getRegion(); + } + + @Test + public void testTokenEndpointForAPRegion() { + when(mockConfiguration.getRegion()).thenReturn("AP"); + + tokenManager = new TokenManager(context, mockConfiguration); + + verify(mockConfiguration).getRegion(); + } + + @Test + public void testTokenEndpointForGOVRegion() { + when(mockConfiguration.getRegion()).thenReturn("GOV"); + + tokenManager = new TokenManager(context, mockConfiguration); + + verify(mockConfiguration).getRegion(); + } + + @Test + public void testTokenEndpointForStagingRegion() { + when(mockConfiguration.getRegion()).thenReturn("STAGING"); + + tokenManager = new TokenManager(context, mockConfiguration); + + verify(mockConfiguration).getRegion(); + } + + @Test + public void testTokenEndpointCaseInsensitive() { + when(mockConfiguration.getRegion()).thenReturn("eu"); + + tokenManager = new TokenManager(context, mockConfiguration); + + verify(mockConfiguration).getRegion(); + } + + @Test + public void testTokenEndpointForUnknownRegion() { + when(mockConfiguration.getRegion()).thenReturn("UNKNOWN"); + + tokenManager = new TokenManager(context, mockConfiguration); + + verify(mockConfiguration).getRegion(); + } + + @Test + public void testDeviceInformationIsInitialized() { + tokenManager = new TokenManager(context, mockConfiguration); + + // DeviceInformation should be initialized during construction + DeviceInformation deviceInfo = DeviceInformation.getInstance(context); + assertNotNull(deviceInfo); + } + + @Test + public void testSharedPreferencesAreInitialized() { + tokenManager = new TokenManager(context, mockConfiguration); + + SharedPreferences prefs = context.getSharedPreferences("nr_video_tokens", Context.MODE_PRIVATE); + assertNotNull(prefs); + } + + @Test + public void testCachedTokenLoadingOnInitialization() { + // Pre-populate SharedPreferences with a token + SharedPreferences prefs = context.getSharedPreferences("nr_video_tokens", Context.MODE_PRIVATE); + prefs.edit() + .putString("app_token", "123456,789012") + .putLong("token_timestamp", System.currentTimeMillis()) + .apply(); + + tokenManager = new TokenManager(context, mockConfiguration); + + // Token should be loaded from cache + String cachedToken = prefs.getString("app_token", null); + assertNotNull(cachedToken); + assertEquals("123456,789012", cachedToken); + } + + @Test + public void testCachedTokenLoadingWithInvalidData() { + // Pre-populate SharedPreferences with invalid token data + SharedPreferences prefs = context.getSharedPreferences("nr_video_tokens", Context.MODE_PRIVATE); + prefs.edit() + .putString("app_token", "invalid,token,data") + .putLong("token_timestamp", System.currentTimeMillis()) + .apply(); + + tokenManager = new TokenManager(context, mockConfiguration); + + // Should handle invalid cached data gracefully + assertNotNull(tokenManager); + } + + @Test + public void testCachedTokenLoadingWithMissingTimestamp() { + // Pre-populate SharedPreferences with token but no timestamp + SharedPreferences prefs = context.getSharedPreferences("nr_video_tokens", Context.MODE_PRIVATE); + prefs.edit() + .putString("app_token", "123456,789012") + .apply(); + + tokenManager = new TokenManager(context, mockConfiguration); + + // Should handle missing timestamp gracefully + assertNotNull(tokenManager); + } + + @Test + public void testTokenCachingPersistence() { + tokenManager = new TokenManager(context, mockConfiguration); + + // Create a second instance - should use same SharedPreferences + TokenManager tokenManager2 = new TokenManager(context, mockConfiguration); + + assertNotNull(tokenManager2); + } + + @Test + public void testMultipleInstancesShareSameCache() { + SharedPreferences prefs = context.getSharedPreferences("nr_video_tokens", Context.MODE_PRIVATE); + prefs.edit().clear().apply(); + + TokenManager tokenManager1 = new TokenManager(context, mockConfiguration); + TokenManager tokenManager2 = new TokenManager(context, mockConfiguration); + + // Both should use the same SharedPreferences + assertNotNull(tokenManager1); + assertNotNull(tokenManager2); + } + + @Test + public void testConfigurationIsStored() { + tokenManager = new TokenManager(context, mockConfiguration); + + // Configuration should be stored and accessible + verify(mockConfiguration, atLeastOnce()).getRegion(); + } + + @Test + public void testApplicationTokenIsAccessed() { + tokenManager = new TokenManager(context, mockConfiguration); + + // Application token should be accessed during initialization + verify(mockConfiguration, atLeast(0)).getApplicationToken(); + } + + @Test + public void testContextIsApplicationContext() { + Context mockContext = mock(Context.class); + Context mockAppContext = RuntimeEnvironment.getApplication(); + when(mockContext.getApplicationContext()).thenReturn(mockAppContext); + when(mockContext.getSharedPreferences(anyString(), anyInt())) + .thenReturn(mockAppContext.getSharedPreferences("nr_video_tokens", Context.MODE_PRIVATE)); + + tokenManager = new TokenManager(mockContext, mockConfiguration); + + verify(mockContext).getApplicationContext(); + } + + @Test + public void testTokenValidityConstants() { + tokenManager = new TokenManager(context, mockConfiguration); + + // Token validity is 14 days - we can't directly test the constant + // but we verify the manager is initialized correctly + assertNotNull(tokenManager); + } + + @Test + public void testNetworkTimeoutConstants() { + tokenManager = new TokenManager(context, mockConfiguration); + + // Network timeouts are configured - we verify initialization + assertNotNull(tokenManager); + } + + @Test + public void testRegionHandlingEdgeCases() { + // Test empty region + when(mockConfiguration.getRegion()).thenReturn(""); + tokenManager = new TokenManager(context, mockConfiguration); + assertNotNull(tokenManager); + + // Test null region (will throw NullPointerException due to toUpperCase) + when(mockConfiguration.getRegion()).thenReturn("US"); + tokenManager = new TokenManager(context, mockConfiguration); + assertNotNull(tokenManager); + } + + @Test + public void testSharedPreferencesMode() { + tokenManager = new TokenManager(context, mockConfiguration); + + // Verify SharedPreferences are created in MODE_PRIVATE + SharedPreferences prefs = context.getSharedPreferences("nr_video_tokens", Context.MODE_PRIVATE); + assertNotNull(prefs); + } + + @Test + public void testTokenManagerWithDifferentConfigurations() { + NRVideoConfiguration config1 = mock(NRVideoConfiguration.class); + when(config1.getRegion()).thenReturn("US"); + when(config1.getApplicationToken()).thenReturn("token1"); + + NRVideoConfiguration config2 = mock(NRVideoConfiguration.class); + when(config2.getRegion()).thenReturn("EU"); + when(config2.getApplicationToken()).thenReturn("token2"); + + TokenManager tm1 = new TokenManager(context, config1); + TokenManager tm2 = new TokenManager(context, config2); + + assertNotNull(tm1); + assertNotNull(tm2); + } + + @Test + public void testTokenManagerInitializationPerformance() { + long startTime = System.currentTimeMillis(); + + tokenManager = new TokenManager(context, mockConfiguration); + + long endTime = System.currentTimeMillis(); + long duration = endTime - startTime; + + // Initialization should be fast (< 1 second) + assertTrue("Initialization took too long: " + duration + "ms", duration < 1000); + } + + @Test + public void testCachedTokenWithVeryOldTimestamp() { + // Pre-populate SharedPreferences with a very old token + SharedPreferences prefs = context.getSharedPreferences("nr_video_tokens", Context.MODE_PRIVATE); + long veryOldTimestamp = System.currentTimeMillis() - (30L * 24 * 60 * 60 * 1000); // 30 days ago + prefs.edit() + .putString("app_token", "123456,789012") + .putLong("token_timestamp", veryOldTimestamp) + .apply(); + + tokenManager = new TokenManager(context, mockConfiguration); + + // Token should be loaded but will be considered expired + assertNotNull(tokenManager); + } + + @Test + public void testMultipleRegionFormats() { + String[] regions = {"US", "us", "Us", "uS", "EU", "eu", "AP", "ap", "GOV", "gov", "STAGING", "staging"}; + + for (String region : regions) { + when(mockConfiguration.getRegion()).thenReturn(region); + TokenManager tm = new TokenManager(context, mockConfiguration); + assertNotNull("Failed to create TokenManager for region: " + region, tm); + } + } + + @Test + public void testConcurrentInitialization() throws InterruptedException { + final int threadCount = 10; + Thread[] threads = new Thread[threadCount]; + + for (int i = 0; i < threadCount; i++) { + threads[i] = new Thread(new Runnable() { + @Override + public void run() { + TokenManager tm = new TokenManager(context, mockConfiguration); + assertNotNull(tm); + } + }); + threads[i].start(); + } + + for (Thread thread : threads) { + thread.join(); + } + + // All threads should complete successfully + assertTrue(true); + } + + @Test + public void testSharedPreferencesClearedOnInvalidToken() { + // Pre-populate with completely invalid data + SharedPreferences prefs = context.getSharedPreferences("nr_video_tokens", Context.MODE_PRIVATE); + prefs.edit() + .putString("app_token", "not-a-valid-token") + .putLong("token_timestamp", System.currentTimeMillis()) + .apply(); + + tokenManager = new TokenManager(context, mockConfiguration); + + // Should handle invalid token gracefully + assertNotNull(tokenManager); + } + + @Test + public void testTokenManagerWithMockedDeviceInformation() { + // DeviceInformation is a singleton, so we can verify it's accessed + tokenManager = new TokenManager(context, mockConfiguration); + + DeviceInformation deviceInfo = DeviceInformation.getInstance(context); + assertNotNull(deviceInfo); + assertNotNull(deviceInfo.getUserAgent()); + } + + @Test + public void testApplicationTokenIsRequired() { + when(mockConfiguration.getApplicationToken()).thenReturn(null); + + tokenManager = new TokenManager(context, mockConfiguration); + + // Should still initialize, but token operations may fail + assertNotNull(tokenManager); + } + + @Test + public void testEmptyApplicationToken() { + when(mockConfiguration.getApplicationToken()).thenReturn(""); + + tokenManager = new TokenManager(context, mockConfiguration); + + assertNotNull(tokenManager); + } + + @Test + public void testTokenManagerMemoryFootprint() { + // Create multiple instances and verify no memory leaks + for (int i = 0; i < 100; i++) { + TokenManager tm = new TokenManager(context, mockConfiguration); + assertNotNull(tm); + } + + // If we get here without OutOfMemoryError, we're good + assertTrue(true); + } + + @Test + public void testSharedPreferencesIsolation() { + SharedPreferences prefs1 = context.getSharedPreferences("nr_video_tokens", Context.MODE_PRIVATE); + SharedPreferences prefs2 = context.getSharedPreferences("other_prefs", Context.MODE_PRIVATE); + + prefs1.edit().putString("test", "value1").apply(); + prefs2.edit().putString("test", "value2").apply(); + + assertEquals("value1", prefs1.getString("test", "")); + assertEquals("value2", prefs2.getString("test", "")); + } +} diff --git a/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/device/DeviceFormTest.java b/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/device/DeviceFormTest.java new file mode 100644 index 00000000..c99510b2 --- /dev/null +++ b/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/device/DeviceFormTest.java @@ -0,0 +1,197 @@ +package com.newrelic.videoagent.core.device; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Unit tests for DeviceForm enum. + */ +public class DeviceFormTest { + + @Test + public void testEnumValues() { + DeviceForm[] values = DeviceForm.values(); + + assertNotNull(values); + assertEquals(7, values.length); + } + + @Test + public void testEnumContainsSmall() { + DeviceForm small = DeviceForm.valueOf("SMALL"); + + assertNotNull(small); + assertEquals(DeviceForm.SMALL, small); + } + + @Test + public void testEnumContainsNormal() { + DeviceForm normal = DeviceForm.valueOf("NORMAL"); + + assertNotNull(normal); + assertEquals(DeviceForm.NORMAL, normal); + } + + @Test + public void testEnumContainsLarge() { + DeviceForm large = DeviceForm.valueOf("LARGE"); + + assertNotNull(large); + assertEquals(DeviceForm.LARGE, large); + } + + @Test + public void testEnumContainsXLarge() { + DeviceForm xlarge = DeviceForm.valueOf("XLARGE"); + + assertNotNull(xlarge); + assertEquals(DeviceForm.XLARGE, xlarge); + } + + @Test + public void testEnumContainsTablet() { + DeviceForm tablet = DeviceForm.valueOf("TABLET"); + + assertNotNull(tablet); + assertEquals(DeviceForm.TABLET, tablet); + } + + @Test + public void testEnumContainsTV() { + DeviceForm tv = DeviceForm.valueOf("TV"); + + assertNotNull(tv); + assertEquals(DeviceForm.TV, tv); + } + + @Test + public void testEnumContainsUnknown() { + DeviceForm unknown = DeviceForm.valueOf("UNKNOWN"); + + assertNotNull(unknown); + assertEquals(DeviceForm.UNKNOWN, unknown); + } + + @Test + public void testEnumName() { + assertEquals("SMALL", DeviceForm.SMALL.name()); + assertEquals("NORMAL", DeviceForm.NORMAL.name()); + assertEquals("LARGE", DeviceForm.LARGE.name()); + assertEquals("XLARGE", DeviceForm.XLARGE.name()); + assertEquals("TABLET", DeviceForm.TABLET.name()); + assertEquals("TV", DeviceForm.TV.name()); + assertEquals("UNKNOWN", DeviceForm.UNKNOWN.name()); + } + + @Test + public void testEnumEquality() { + DeviceForm small1 = DeviceForm.SMALL; + DeviceForm small2 = DeviceForm.valueOf("SMALL"); + + assertEquals(small1, small2); + assertSame(small1, small2); + } + + @Test + public void testEnumInequality() { + assertNotEquals(DeviceForm.SMALL, DeviceForm.NORMAL); + assertNotEquals(DeviceForm.LARGE, DeviceForm.XLARGE); + assertNotEquals(DeviceForm.TABLET, DeviceForm.TV); + assertNotEquals(DeviceForm.UNKNOWN, DeviceForm.SMALL); + } + + @Test + public void testEnumOrdinal() { + assertEquals(0, DeviceForm.SMALL.ordinal()); + assertEquals(1, DeviceForm.NORMAL.ordinal()); + assertEquals(2, DeviceForm.LARGE.ordinal()); + assertEquals(3, DeviceForm.XLARGE.ordinal()); + assertEquals(4, DeviceForm.TABLET.ordinal()); + assertEquals(5, DeviceForm.TV.ordinal()); + assertEquals(6, DeviceForm.UNKNOWN.ordinal()); + } + + @Test(expected = IllegalArgumentException.class) + public void testInvalidEnumValue() { + DeviceForm.valueOf("INVALID"); + } + + @Test(expected = NullPointerException.class) + public void testNullEnumValue() { + DeviceForm.valueOf(null); + } + + @Test + public void testEnumToString() { + assertEquals("SMALL", DeviceForm.SMALL.toString()); + assertEquals("NORMAL", DeviceForm.NORMAL.toString()); + assertEquals("LARGE", DeviceForm.LARGE.toString()); + assertEquals("XLARGE", DeviceForm.XLARGE.toString()); + assertEquals("TABLET", DeviceForm.TABLET.toString()); + assertEquals("TV", DeviceForm.TV.toString()); + assertEquals("UNKNOWN", DeviceForm.UNKNOWN.toString()); + } + + @Test + public void testEnumCompareTo() { + assertTrue(DeviceForm.SMALL.compareTo(DeviceForm.NORMAL) < 0); + assertTrue(DeviceForm.NORMAL.compareTo(DeviceForm.SMALL) > 0); + assertEquals(0, DeviceForm.TABLET.compareTo(DeviceForm.TABLET)); + } + + @Test + public void testEnumIsInstance() { + assertTrue(DeviceForm.SMALL instanceof DeviceForm); + assertTrue(DeviceForm.TV instanceof Enum); + } + + @Test + public void testEnumSwitch() { + DeviceForm form = DeviceForm.TABLET; + String result; + + switch (form) { + case SMALL: + result = "small"; + break; + case NORMAL: + result = "normal"; + break; + case LARGE: + result = "large"; + break; + case XLARGE: + result = "xlarge"; + break; + case TABLET: + result = "tablet"; + break; + case TV: + result = "tv"; + break; + case UNKNOWN: + default: + result = "unknown"; + break; + } + + assertEquals("tablet", result); + } + + @Test + public void testEnumInArray() { + DeviceForm[] forms = {DeviceForm.SMALL, DeviceForm.TABLET, DeviceForm.TV}; + + assertEquals(3, forms.length); + assertEquals(DeviceForm.SMALL, forms[0]); + assertEquals(DeviceForm.TABLET, forms[1]); + assertEquals(DeviceForm.TV, forms[2]); + } + + @Test + public void testEnumHashCode() { + assertEquals(DeviceForm.SMALL.hashCode(), DeviceForm.SMALL.hashCode()); + assertNotEquals(DeviceForm.SMALL.hashCode(), DeviceForm.NORMAL.hashCode()); + } +} diff --git a/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/device/DeviceInformationTest.java b/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/device/DeviceInformationTest.java new file mode 100644 index 00000000..3d374be1 --- /dev/null +++ b/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/device/DeviceInformationTest.java @@ -0,0 +1,286 @@ +package com.newrelic.videoagent.core.device; + +import android.content.Context; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +import static org.junit.Assert.*; + +/** + * Unit tests for DeviceInformation that exercise actual production code. + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = 28, manifest = Config.NONE) +public class DeviceInformationTest { + + private Context context; + + @Before + public void setUp() { + context = RuntimeEnvironment.getApplication(); + } + + @Test + public void testGetInstanceReturnsSingleton() { + DeviceInformation instance1 = DeviceInformation.getInstance(context); + DeviceInformation instance2 = DeviceInformation.getInstance(context); + + assertNotNull(instance1); + assertNotNull(instance2); + assertSame(instance1, instance2); + } + + @Test + public void testGetOsName() { + DeviceInformation deviceInfo = DeviceInformation.getInstance(context); + + String osName = deviceInfo.getOsName(); + + assertNotNull(osName); + assertEquals("Android", osName); + } + + @Test + public void testGetOsVersion() { + DeviceInformation deviceInfo = DeviceInformation.getInstance(context); + + String osVersion = deviceInfo.getOsVersion(); + + assertNotNull(osVersion); + assertFalse(osVersion.isEmpty()); + } + + @Test + public void testGetOsBuild() { + DeviceInformation deviceInfo = DeviceInformation.getInstance(context); + + String osBuild = deviceInfo.getOsBuild(); + + assertNotNull(osBuild); + assertFalse(osBuild.isEmpty()); + } + + @Test + public void testGetModel() { + DeviceInformation deviceInfo = DeviceInformation.getInstance(context); + + String model = deviceInfo.getModel(); + + assertNotNull(model); + assertFalse(model.isEmpty()); + } + + @Test + public void testGetManufacturer() { + DeviceInformation deviceInfo = DeviceInformation.getInstance(context); + + String manufacturer = deviceInfo.getManufacturer(); + + assertNotNull(manufacturer); + assertFalse(manufacturer.isEmpty()); + } + + @Test + public void testGetDeviceId() { + DeviceInformation deviceInfo = DeviceInformation.getInstance(context); + + String deviceId = deviceInfo.getDeviceId(); + + assertNotNull(deviceId); + assertFalse(deviceId.isEmpty()); + } + + @Test + public void testDeviceIdIsConsistent() { + DeviceInformation deviceInfo = DeviceInformation.getInstance(context); + + String deviceId1 = deviceInfo.getDeviceId(); + String deviceId2 = deviceInfo.getDeviceId(); + + assertEquals(deviceId1, deviceId2); + } + + @Test + public void testGetArchitecture() { + DeviceInformation deviceInfo = DeviceInformation.getInstance(context); + + String architecture = deviceInfo.getArchitecture(); + + assertNotNull(architecture); + assertFalse(architecture.isEmpty()); + } + + @Test + public void testGetRunTime() { + DeviceInformation deviceInfo = DeviceInformation.getInstance(context); + + String runTime = deviceInfo.getRunTime(); + + assertNotNull(runTime); + assertFalse(runTime.isEmpty()); + } + + @Test + public void testGetSize() { + DeviceInformation deviceInfo = DeviceInformation.getInstance(context); + + String size = deviceInfo.getSize(); + + assertNotNull(size); + assertFalse(size.isEmpty()); + } + + @Test + public void testGetSizeReturnsValidValue() { + DeviceInformation deviceInfo = DeviceInformation.getInstance(context); + + String size = deviceInfo.getSize(); + + assertNotNull(size); + assertFalse(size.isEmpty()); + // Size is determined by Robolectric environment, just verify it's not null or empty + } + + @Test + public void testGetApplicationFramework() { + DeviceInformation deviceInfo = DeviceInformation.getInstance(context); + + String framework = deviceInfo.getApplicationFramework(); + + assertNotNull(framework); + assertFalse(framework.isEmpty()); + } + + @Test + public void testGetAgentName() { + DeviceInformation deviceInfo = DeviceInformation.getInstance(context); + + String agentName = deviceInfo.getAgentName(); + + assertNotNull(agentName); + assertEquals("NewRelic-VideoAgent-Android", agentName); + } + + @Test + public void testGetAgentVersion() { + DeviceInformation deviceInfo = DeviceInformation.getInstance(context); + + String agentVersion = deviceInfo.getAgentVersion(); + + assertNotNull(agentVersion); + assertFalse(agentVersion.isEmpty()); + } + + @Test + public void testGetUserAgent() { + DeviceInformation deviceInfo = DeviceInformation.getInstance(context); + + String userAgent = deviceInfo.getUserAgent(); + + assertNotNull(userAgent); + assertTrue(userAgent.contains("NewRelic-VideoAgent-Android")); + assertTrue(userAgent.contains("Android")); + } + + @Test + public void testIsTV() { + DeviceInformation deviceInfo = DeviceInformation.getInstance(context); + + boolean isTV = deviceInfo.isTV(); + + // Just verify it returns a boolean + assertTrue(isTV || !isTV); + } + + @Test + public void testIsLowMemoryDevice() { + DeviceInformation deviceInfo = DeviceInformation.getInstance(context); + + boolean isLowMemory = deviceInfo.isLowMemoryDevice(); + + // Just verify it returns a boolean + assertTrue(isLowMemory || !isLowMemory); + } + + @Test + public void testGeneratePersistentDeviceId() { + DeviceInformation deviceInfo = DeviceInformation.getInstance(context); + + String deviceId = deviceInfo.getDeviceId(); + + assertNotNull(deviceId); + assertTrue(deviceId.length() > 0); + } + + @Test + public void testGetSystemArchitecture() { + DeviceInformation deviceInfo = DeviceInformation.getInstance(context); + + String architecture = deviceInfo.getArchitecture(); + + assertNotNull(architecture); + assertTrue(architecture.length() > 0); + } + + @Test + public void testGetJavaVMVersion() { + DeviceInformation deviceInfo = DeviceInformation.getInstance(context); + + String runtime = deviceInfo.getRunTime(); + + assertNotNull(runtime); + assertTrue(runtime.length() > 0); + } + + @Test + public void testDetermineDeviceForm() { + DeviceInformation deviceInfo = DeviceInformation.getInstance(context); + + String size = deviceInfo.getSize(); + + assertNotNull(size); + assertFalse(size.isEmpty()); + // Device form is determined by Robolectric environment + } + + @Test + public void testDetermineApplicationFramework() { + DeviceInformation deviceInfo = DeviceInformation.getInstance(context); + + String framework = deviceInfo.getApplicationFramework(); + + assertNotNull(framework); + assertFalse(framework.isEmpty()); + // Application framework is detected by DeviceInformation + } + + @Test + public void testUserAgentContainsDeviceInfo() { + DeviceInformation deviceInfo = DeviceInformation.getInstance(context); + + String userAgent = deviceInfo.getUserAgent(); + + assertNotNull(userAgent); + assertTrue(userAgent.contains(deviceInfo.getModel())); + assertTrue(userAgent.contains(deviceInfo.getOsVersion())); + } + + @Test + public void testSingletonPersistsDeviceInfo() { + DeviceInformation instance1 = DeviceInformation.getInstance(context); + String deviceId1 = instance1.getDeviceId(); + String userAgent1 = instance1.getUserAgent(); + + DeviceInformation instance2 = DeviceInformation.getInstance(context); + String deviceId2 = instance2.getDeviceId(); + String userAgent2 = instance2.getUserAgent(); + + assertEquals(deviceId1, deviceId2); + assertEquals(userAgent1, userAgent2); + } +} diff --git a/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/exception/ErrorExceptionHandlerTest.java b/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/exception/ErrorExceptionHandlerTest.java new file mode 100644 index 00000000..b95fa29e --- /dev/null +++ b/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/exception/ErrorExceptionHandlerTest.java @@ -0,0 +1,209 @@ +package com.newrelic.videoagent.core.exception; + +import androidx.media3.common.PlaybackException; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Unit tests for ErrorExceptionHandler that exercise actual production code. + */ +public class ErrorExceptionHandlerTest { + + @Test + public void testHandleGenericException() { + Exception genericException = new Exception("Generic error message"); + + ErrorExceptionHandler handler = new ErrorExceptionHandler(genericException); + + assertEquals(-9999, handler.getErrorCode()); + assertEquals("Generic error message", handler.getErrorMessage()); + } + + @Test + public void testHandleNullMessageException() { + Exception exception = new Exception((String) null); + + ErrorExceptionHandler handler = new ErrorExceptionHandler(exception); + + assertEquals(-9999, handler.getErrorCode()); + assertNull(handler.getErrorMessage()); + } + + @Test + public void testHandleRuntimeException() { + RuntimeException runtimeException = new RuntimeException("Runtime error"); + + ErrorExceptionHandler handler = new ErrorExceptionHandler(runtimeException); + + assertEquals(-9999, handler.getErrorCode()); + assertEquals("Runtime error", handler.getErrorMessage()); + } + + @Test + public void testHandleIOException() { + Exception ioException = new java.io.IOException("IO error occurred"); + + ErrorExceptionHandler handler = new ErrorExceptionHandler(ioException); + + assertEquals(-9999, handler.getErrorCode()); + assertEquals("IO error occurred", handler.getErrorMessage()); + } + + @Test + public void testHandleExceptionWithEmptyMessage() { + Exception exception = new Exception(""); + + ErrorExceptionHandler handler = new ErrorExceptionHandler(exception); + + assertEquals(-9999, handler.getErrorCode()); + assertEquals("", handler.getErrorMessage()); + } + + @Test + public void testHandleExceptionWithLongMessage() { + String longMessage = "This is a very long error message that contains a lot of details " + + "about what went wrong during the video playback process. It includes " + + "technical information that might be useful for debugging purposes."; + Exception exception = new Exception(longMessage); + + ErrorExceptionHandler handler = new ErrorExceptionHandler(exception); + + assertEquals(-9999, handler.getErrorCode()); + assertEquals(longMessage, handler.getErrorMessage()); + } + + @Test + public void testHandleExceptionWithSpecialCharacters() { + String messageWithSpecialChars = "Error: \"Cannot load video\" - check network & permissions!"; + Exception exception = new Exception(messageWithSpecialChars); + + ErrorExceptionHandler handler = new ErrorExceptionHandler(exception); + + assertEquals(-9999, handler.getErrorCode()); + assertEquals(messageWithSpecialChars, handler.getErrorMessage()); + } + + @Test + public void testGetErrorCodeReturnsConsistentValue() { + Exception exception = new Exception("Test error"); + + ErrorExceptionHandler handler = new ErrorExceptionHandler(exception); + + int errorCode1 = handler.getErrorCode(); + int errorCode2 = handler.getErrorCode(); + + assertEquals(errorCode1, errorCode2); + assertEquals(-9999, errorCode1); + } + + @Test + public void testGetErrorMessageReturnsConsistentValue() { + Exception exception = new Exception("Test message"); + + ErrorExceptionHandler handler = new ErrorExceptionHandler(exception); + + String message1 = handler.getErrorMessage(); + String message2 = handler.getErrorMessage(); + + assertEquals(message1, message2); + assertEquals("Test message", message1); + } + + @Test + public void testMultipleHandlersWithDifferentExceptions() { + Exception exception1 = new Exception("Error 1"); + Exception exception2 = new Exception("Error 2"); + + ErrorExceptionHandler handler1 = new ErrorExceptionHandler(exception1); + ErrorExceptionHandler handler2 = new ErrorExceptionHandler(exception2); + + assertEquals("Error 1", handler1.getErrorMessage()); + assertEquals("Error 2", handler2.getErrorMessage()); + assertEquals(-9999, handler1.getErrorCode()); + assertEquals(-9999, handler2.getErrorCode()); + } + + @Test + public void testHandleIllegalArgumentException() { + IllegalArgumentException illegalArgException = new IllegalArgumentException("Invalid argument provided"); + + ErrorExceptionHandler handler = new ErrorExceptionHandler(illegalArgException); + + assertEquals(-9999, handler.getErrorCode()); + assertEquals("Invalid argument provided", handler.getErrorMessage()); + } + + @Test + public void testHandleNullPointerException() { + NullPointerException npe = new NullPointerException("Null pointer encountered"); + + ErrorExceptionHandler handler = new ErrorExceptionHandler(npe); + + assertEquals(-9999, handler.getErrorCode()); + assertEquals("Null pointer encountered", handler.getErrorMessage()); + } + + @Test + public void testHandleSecurityException() { + SecurityException secException = new SecurityException("Security violation detected"); + + ErrorExceptionHandler handler = new ErrorExceptionHandler(secException); + + assertEquals(-9999, handler.getErrorCode()); + assertEquals("Security violation detected", handler.getErrorMessage()); + } + + @Test + public void testHandleUnsupportedOperationException() { + UnsupportedOperationException unsupportedException = new UnsupportedOperationException("Operation not supported"); + + ErrorExceptionHandler handler = new ErrorExceptionHandler(unsupportedException); + + assertEquals(-9999, handler.getErrorCode()); + assertEquals("Operation not supported", handler.getErrorMessage()); + } + + @Test + public void testDefaultErrorCodeConstant() { + Exception exception = new Exception("Test"); + + ErrorExceptionHandler handler = new ErrorExceptionHandler(exception); + + assertEquals(-9999, handler.getErrorCode()); + } + + @Test + public void testHandleExceptionWithNumericMessage() { + Exception exception = new Exception("12345"); + + ErrorExceptionHandler handler = new ErrorExceptionHandler(exception); + + assertEquals(-9999, handler.getErrorCode()); + assertEquals("12345", handler.getErrorMessage()); + } + + @Test + public void testHandleExceptionWithUnicodeCharacters() { + String unicodeMessage = "Error: 视频加载失败 🎥"; + Exception exception = new Exception(unicodeMessage); + + ErrorExceptionHandler handler = new ErrorExceptionHandler(exception); + + assertEquals(-9999, handler.getErrorCode()); + assertEquals(unicodeMessage, handler.getErrorMessage()); + } + + @Test + public void testHandleNestedExceptionMessage() { + Exception cause = new Exception("Root cause error"); + Exception exception = new Exception("Wrapper error: " + cause.getMessage(), cause); + + ErrorExceptionHandler handler = new ErrorExceptionHandler(exception); + + assertEquals(-9999, handler.getErrorCode()); + assertTrue(handler.getErrorMessage().contains("Wrapper error")); + assertTrue(handler.getErrorMessage().contains("Root cause error")); + } +} diff --git a/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/harvest/HarvestManagerTest.java b/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/harvest/HarvestManagerTest.java new file mode 100644 index 00000000..3825b686 --- /dev/null +++ b/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/harvest/HarvestManagerTest.java @@ -0,0 +1,539 @@ +package com.newrelic.videoagent.core.harvest; + +import android.content.Context; + +import com.newrelic.videoagent.core.NRVideoConfiguration; +import com.newrelic.videoagent.core.NRVideoConstants; +import com.newrelic.videoagent.core.storage.IntegratedDeadLetterHandler; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +/** + * Comprehensive unit tests for HarvestManager. + * Tests event recording, harvest orchestration, capacity callbacks, and integration with factory. + */ +@RunWith(RobolectricTestRunner.class) +public class HarvestManagerTest { + + @Mock + private EventBufferInterface mockEventBuffer; + + @Mock + private HttpClientInterface mockHttpClient; + + @Mock + private SchedulerInterface mockScheduler; + + @Mock + private IntegratedDeadLetterHandler mockDeadLetterHandler; + + @Mock + private HarvestComponentFactory mockFactory; + + private HarvestManager harvestManager; + private Context context; + private NRVideoConfiguration testConfig; + + @Before + public void setUp() { + MockitoAnnotations.openMocks(this); + context = RuntimeEnvironment.getApplication(); + + // Create valid test configuration + testConfig = new NRVideoConfiguration.Builder("test-app-token-1234567890") + .build(); + + // Set up mock factory to return mocked components + when(mockFactory.getEventBuffer()).thenReturn(mockEventBuffer); + when(mockFactory.getHttpClient()).thenReturn(mockHttpClient); + when(mockFactory.getScheduler()).thenReturn(mockScheduler); + when(mockFactory.getDeadLetterHandler()).thenReturn(mockDeadLetterHandler); + when(mockFactory.getConfiguration()).thenReturn(testConfig); + when(mockFactory.getContext()).thenReturn(context); + + // Create HarvestManager with real configuration and context + // Note: This will create a real CrashSafeHarvestFactory internally + harvestManager = new HarvestManager(testConfig, context); + } + + // ========== Initialization Tests ========== + + @Test + public void testHarvestManagerCreation() { + assertNotNull("HarvestManager should be created", harvestManager); + } + + @Test + public void testFactoryIsCreated() { + HarvestComponentFactory factory = harvestManager.getFactory(); + assertNotNull("Factory should be created", factory); + } + + @Test + public void testFactoryReturnsConfiguration() { + HarvestComponentFactory factory = harvestManager.getFactory(); + NRVideoConfiguration config = factory.getConfiguration(); + + assertNotNull("Configuration should be available", config); + } + + @Test + public void testFactoryReturnsContext() { + HarvestComponentFactory factory = harvestManager.getFactory(); + Context factoryContext = factory.getContext(); + + assertNotNull("Context should be available", factoryContext); + } + + // ========== Record Event Tests ========== + + @Test + public void testRecordEventWithValidData() { + Map attributes = new HashMap<>(); + attributes.put("key1", "value1"); + attributes.put("key2", 123); + + // Should not throw exception + harvestManager.recordEvent("TestEvent", attributes); + } + + @Test + public void testRecordEventWithNullAttributes() { + // Should not throw exception + harvestManager.recordEvent("TestEvent", null); + } + + @Test + public void testRecordEventWithEmptyAttributes() { + Map attributes = new HashMap<>(); + + // Should not throw exception + harvestManager.recordEvent("TestEvent", attributes); + } + + @Test + public void testRecordEventWithNullEventType() { + Map attributes = new HashMap<>(); + attributes.put("key", "value"); + + // Should not throw exception (event type is null, should be ignored) + harvestManager.recordEvent(null, attributes); + } + + @Test + public void testRecordEventWithEmptyEventType() { + Map attributes = new HashMap<>(); + attributes.put("key", "value"); + + // Should not throw exception (empty event type should be ignored) + harvestManager.recordEvent("", attributes); + } + + @Test + public void testRecordEventWithWhitespaceEventType() { + Map attributes = new HashMap<>(); + attributes.put("key", "value"); + + // Should not throw exception (whitespace event type should be ignored) + harvestManager.recordEvent(" ", attributes); + } + + @Test + public void testRecordEventAddsTimestamp() { + Map attributes = new HashMap<>(); + attributes.put("key", "value"); + + long beforeTime = System.currentTimeMillis(); + harvestManager.recordEvent("TestEvent", attributes); + long afterTime = System.currentTimeMillis(); + + // Event should be added with timestamp (verified through buffer interaction) + // This is an integration test with the real factory + } + + @Test + public void testRecordEventMultipleTimes() { + for (int i = 0; i < 10; i++) { + Map attributes = new HashMap<>(); + attributes.put("eventNum", i); + + harvestManager.recordEvent("Event" + i, attributes); + } + + // All events should be recorded without exceptions + } + + @Test + public void testRecordEventWithComplexAttributes() { + Map attributes = new HashMap<>(); + attributes.put("stringAttr", "test"); + attributes.put("intAttr", 42); + attributes.put("doubleAttr", 3.14); + attributes.put("boolAttr", true); + attributes.put("longAttr", 1234567890L); + + harvestManager.recordEvent("ComplexEvent", attributes); + } + + @Test + public void testRecordEventWithNestedAttributes() { + Map nestedMap = new HashMap<>(); + nestedMap.put("nestedKey", "nestedValue"); + + Map attributes = new HashMap<>(); + attributes.put("topLevel", "value"); + attributes.put("nested", nestedMap); + + harvestManager.recordEvent("NestedEvent", attributes); + } + + // ========== Capacity Callback Tests ========== + + @Test + public void testOnCapacityThresholdReachedForLive() { + // This tests the CapacityCallback implementation + double capacity = 0.6; + String bufferType = NRVideoConstants.EVENT_TYPE_LIVE; + + // Should not throw exception + harvestManager.onCapacityThresholdReached(capacity, bufferType); + } + + @Test + public void testOnCapacityThresholdReachedForOndemand() { + double capacity = 0.6; + String bufferType = NRVideoConstants.EVENT_TYPE_ONDEMAND; + + // Should not throw exception + harvestManager.onCapacityThresholdReached(capacity, bufferType); + } + + @Test + public void testOnCapacityThresholdReachedAtVariousLevels() { + // Test at different capacity levels + double[] capacities = {0.0, 0.3, 0.6, 0.9, 1.0}; + + for (double capacity : capacities) { + harvestManager.onCapacityThresholdReached(capacity, + NRVideoConstants.EVENT_TYPE_LIVE); + } + + // All should succeed without exceptions + } + + // ========== Harvest Tests ========== + + @Test + public void testHarvestOnDemand() { + // Should not throw exception + harvestManager.harvestOnDemand(); + } + + @Test + public void testHarvestLive() { + // Should not throw exception + harvestManager.harvestLive(); + } + + @Test + public void testMultipleHarvestOnDemandCalls() { + for (int i = 0; i < 5; i++) { + harvestManager.harvestOnDemand(); + } + + // All harvests should succeed + } + + @Test + public void testMultipleHarvestLiveCalls() { + for (int i = 0; i < 5; i++) { + harvestManager.harvestLive(); + } + + // All harvests should succeed + } + + @Test + public void testAlternatingHarvestCalls() { + for (int i = 0; i < 5; i++) { + harvestManager.harvestOnDemand(); + harvestManager.harvestLive(); + } + + // Alternating harvests should succeed + } + + // ========== Edge Cases and Error Handling Tests ========== + + @Test + public void testRecordEventWithVeryLargeAttributes() { + Map attributes = new HashMap<>(); + + // Add 100 attributes + for (int i = 0; i < 100; i++) { + attributes.put("key" + i, "value" + i); + } + + harvestManager.recordEvent("LargeEvent", attributes); + } + + @Test + public void testRecordEventWithLongStrings() { + Map attributes = new HashMap<>(); + StringBuilder longString = new StringBuilder(); + for (int i = 0; i < 1000; i++) { + longString.append("x"); + } + attributes.put("longKey", longString.toString()); + + harvestManager.recordEvent("LongStringEvent", attributes); + } + + @Test + public void testRecordEventWithSpecialCharacters() { + Map attributes = new HashMap<>(); + attributes.put("special", "!@#$%^&*()_+-=[]{}|;':\",./<>?"); + attributes.put("unicode", "こんにちは世界"); + attributes.put("emoji", "😀🎉🚀"); + + harvestManager.recordEvent("SpecialCharsEvent", attributes); + } + + @Test + public void testRecordEventWithNullValuesInAttributes() { + Map attributes = new HashMap<>(); + attributes.put("key1", "value1"); + attributes.put("key2", null); + attributes.put("key3", "value3"); + + harvestManager.recordEvent("NullValueEvent", attributes); + } + + @Test + public void testCapacityCallbackWithNegativeCapacity() { + harvestManager.onCapacityThresholdReached(-0.1, + NRVideoConstants.EVENT_TYPE_LIVE); + + // Should handle gracefully + } + + @Test + public void testCapacityCallbackWithOverMaxCapacity() { + harvestManager.onCapacityThresholdReached(1.5, + NRVideoConstants.EVENT_TYPE_ONDEMAND); + + // Should handle gracefully + } + + @Test + public void testCapacityCallbackWithNullBufferType() { + harvestManager.onCapacityThresholdReached(0.6, null); + + // Should handle gracefully + } + + @Test + public void testCapacityCallbackWithInvalidBufferType() { + harvestManager.onCapacityThresholdReached(0.6, "invalid"); + + // Should handle gracefully + } + + // ========== Concurrency Tests ========== + + @Test + public void testConcurrentRecordEvents() throws InterruptedException { + int threadCount = 5; + int eventsPerThread = 20; + Thread[] threads = new Thread[threadCount]; + + for (int i = 0; i < threadCount; i++) { + final int threadId = i; + threads[i] = new Thread(() -> { + for (int j = 0; j < eventsPerThread; j++) { + Map attributes = new HashMap<>(); + attributes.put("threadId", threadId); + attributes.put("eventId", j); + harvestManager.recordEvent("ConcurrentEvent", attributes); + } + }); + threads[i].start(); + } + + // Wait for all threads to complete + for (Thread thread : threads) { + thread.join(); + } + + // All events should be recorded successfully + } + + @Test + public void testConcurrentHarvesting() throws InterruptedException { + // Record some events first + for (int i = 0; i < 50; i++) { + Map attributes = new HashMap<>(); + attributes.put("index", i); + harvestManager.recordEvent("Event", attributes); + } + + int threadCount = 3; + Thread[] threads = new Thread[threadCount]; + + for (int i = 0; i < threadCount; i++) { + final int threadId = i; + threads[i] = new Thread(() -> { + if (threadId % 2 == 0) { + harvestManager.harvestOnDemand(); + } else { + harvestManager.harvestLive(); + } + }); + threads[i].start(); + } + + // Wait for all threads to complete + for (Thread thread : threads) { + thread.join(); + } + + // All harvests should complete successfully + } + + @Test + public void testConcurrentRecordAndHarvest() throws InterruptedException { + Thread recordThread = new Thread(() -> { + for (int i = 0; i < 100; i++) { + Map attributes = new HashMap<>(); + attributes.put("index", i); + harvestManager.recordEvent("Event", attributes); + try { Thread.sleep(10); } catch (InterruptedException e) { break; } + } + }); + + Thread harvestThread = new Thread(() -> { + for (int i = 0; i < 20; i++) { + harvestManager.harvestOnDemand(); + try { Thread.sleep(50); } catch (InterruptedException e) { break; } + } + }); + + recordThread.start(); + harvestThread.start(); + + recordThread.join(); + harvestThread.join(); + + // Should handle concurrent record and harvest + } + + // ========== Factory Delegation Tests ========== + + @Test + public void testFactoryIsNotNull() { + assertNotNull("Factory should not be null", harvestManager.getFactory()); + } + + @Test + public void testFactoryReturnsSameInstance() { + HarvestComponentFactory factory1 = harvestManager.getFactory(); + HarvestComponentFactory factory2 = harvestManager.getFactory(); + + assertSame("Factory should return same instance", factory1, factory2); + } + + @Test + public void testFactoryCleanup() { + HarvestComponentFactory factory = harvestManager.getFactory(); + + // Should not throw exception + factory.cleanup(); + } + + @Test + public void testFactoryRecoveryMethods() { + HarvestComponentFactory factory = harvestManager.getFactory(); + + // These methods should be callable + boolean isRecovering = factory.isRecovering(); + String recoveryStats = factory.getRecoveryStats(); + + assertNotNull("Recovery stats should not be null", recoveryStats); + } + + @Test + public void testFactoryEmergencyBackup() { + HarvestComponentFactory factory = harvestManager.getFactory(); + + // Should not throw exception + factory.performEmergencyBackup(); + } + + // ========== Stress Tests ========== + + @Test + public void testRapidEventRecording() { + // Record 1000 events rapidly + for (int i = 0; i < 1000; i++) { + Map attributes = new HashMap<>(); + attributes.put("index", i); + harvestManager.recordEvent("RapidEvent" + i, attributes); + } + + // Should handle all events + } + + @Test + public void testRapidHarvesting() { + // Record some events + for (int i = 0; i < 50; i++) { + Map attributes = new HashMap<>(); + attributes.put("index", i); + harvestManager.recordEvent("Event", attributes); + } + + // Harvest rapidly + for (int i = 0; i < 100; i++) { + if (i % 2 == 0) { + harvestManager.harvestOnDemand(); + } else { + harvestManager.harvestLive(); + } + } + + // Should handle rapid harvesting + } + + @Test + public void testMixedEventTypes() { + String[] eventTypes = {"VIDEO", "AD", "ERROR", "CUSTOM", "HEARTBEAT"}; + + for (int i = 0; i < 100; i++) { + String eventType = eventTypes[i % eventTypes.length]; + Map attributes = new HashMap<>(); + attributes.put("index", i); + attributes.put("type", eventType); + + harvestManager.recordEvent(eventType, attributes); + } + + harvestManager.harvestOnDemand(); + harvestManager.harvestLive(); + } +} diff --git a/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/harvest/MultiTaskHarvestSchedulerTest.java b/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/harvest/MultiTaskHarvestSchedulerTest.java new file mode 100644 index 00000000..ef27339c --- /dev/null +++ b/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/harvest/MultiTaskHarvestSchedulerTest.java @@ -0,0 +1,601 @@ +package com.newrelic.videoagent.core.harvest; + +import com.newrelic.videoagent.core.NRVideoConfiguration; +import com.newrelic.videoagent.core.NRVideoConstants; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.shadows.ShadowLooper; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +/** + * Unit tests for MultiTaskHarvestScheduler. + * Tests scheduling, lifecycle management, pause/resume, and device-specific optimizations. + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = 28, manifest = Config.NONE) +public class MultiTaskHarvestSchedulerTest { + + @Mock + private NRVideoConfiguration mockConfiguration; + + @Mock + private Runnable mockOnDemandTask; + + @Mock + private Runnable mockLiveTask; + + private MultiTaskHarvestScheduler scheduler; + + @Before + public void setUp() { + MockitoAnnotations.openMocks(this); + + // Set up default configuration + when(mockConfiguration.getHarvestCycleSeconds()).thenReturn(60); + when(mockConfiguration.getLiveHarvestCycleSeconds()).thenReturn(10); + when(mockConfiguration.isTV()).thenReturn(false); + } + + @After + public void tearDown() { + if (scheduler != null) { + scheduler.shutdown(); + } + } + + // ========== Constructor Tests ========== + + @Test + public void testConstructorInitialization() { + scheduler = new MultiTaskHarvestScheduler(mockOnDemandTask, mockLiveTask, mockConfiguration); + + assertNotNull("Scheduler should be initialized", scheduler); + assertFalse("Scheduler should not be running initially", scheduler.isRunning()); + } + + @Test + public void testConstructorWithMobileDevice() { + when(mockConfiguration.isTV()).thenReturn(false); + + scheduler = new MultiTaskHarvestScheduler(mockOnDemandTask, mockLiveTask, mockConfiguration); + + assertNotNull("Scheduler should be initialized for mobile", scheduler); + } + + @Test + public void testConstructorWithTVDevice() { + when(mockConfiguration.isTV()).thenReturn(true); + + scheduler = new MultiTaskHarvestScheduler(mockOnDemandTask, mockLiveTask, mockConfiguration); + + assertNotNull("Scheduler should be initialized for TV", scheduler); + } + + @Test + public void testConstructorWithCustomHarvestCycles() { + when(mockConfiguration.getHarvestCycleSeconds()).thenReturn(120); + when(mockConfiguration.getLiveHarvestCycleSeconds()).thenReturn(5); + + scheduler = new MultiTaskHarvestScheduler(mockOnDemandTask, mockLiveTask, mockConfiguration); + + assertNotNull("Scheduler should accept custom harvest cycles", scheduler); + } + + @Test + public void testConstructorWithShortHarvestCycles() { + when(mockConfiguration.getHarvestCycleSeconds()).thenReturn(10); + when(mockConfiguration.getLiveHarvestCycleSeconds()).thenReturn(2); + + scheduler = new MultiTaskHarvestScheduler(mockOnDemandTask, mockLiveTask, mockConfiguration); + + assertNotNull("Scheduler should accept short harvest cycles", scheduler); + } + + @Test + public void testConstructorWithLongHarvestCycles() { + when(mockConfiguration.getHarvestCycleSeconds()).thenReturn(300); + when(mockConfiguration.getLiveHarvestCycleSeconds()).thenReturn(30); + + scheduler = new MultiTaskHarvestScheduler(mockOnDemandTask, mockLiveTask, mockConfiguration); + + assertNotNull("Scheduler should accept long harvest cycles", scheduler); + } + + // ========== Start Tests ========== + + @Test + public void testStartWithoutBufferType() throws InterruptedException { + scheduler = new MultiTaskHarvestScheduler(mockOnDemandTask, mockLiveTask, mockConfiguration); + + scheduler.start(); + + Thread.sleep(200); // Wait for scheduler to activate + assertTrue("Scheduler should be running", scheduler.isRunning()); + } + + @Test + public void testStartOnDemandScheduler() throws InterruptedException { + scheduler = new MultiTaskHarvestScheduler(mockOnDemandTask, mockLiveTask, mockConfiguration); + + scheduler.start(NRVideoConstants.EVENT_TYPE_ONDEMAND); + + Thread.sleep(200); + assertTrue("Scheduler should be running", scheduler.isRunning()); + } + + @Test + public void testStartLiveScheduler() throws InterruptedException { + scheduler = new MultiTaskHarvestScheduler(mockOnDemandTask, mockLiveTask, mockConfiguration); + + scheduler.start(NRVideoConstants.EVENT_TYPE_LIVE); + + Thread.sleep(200); + assertTrue("Scheduler should be running", scheduler.isRunning()); + } + + @Test + public void testStartBothSchedulers() throws InterruptedException { + scheduler = new MultiTaskHarvestScheduler(mockOnDemandTask, mockLiveTask, mockConfiguration); + + scheduler.start(NRVideoConstants.EVENT_TYPE_ONDEMAND); + scheduler.start(NRVideoConstants.EVENT_TYPE_LIVE); + + Thread.sleep(200); + assertTrue("Both schedulers should be running", scheduler.isRunning()); + } + + @Test + public void testStartAfterAlreadyStarted() throws InterruptedException { + scheduler = new MultiTaskHarvestScheduler(mockOnDemandTask, mockLiveTask, mockConfiguration); + + scheduler.start(NRVideoConstants.EVENT_TYPE_ONDEMAND); + scheduler.start(NRVideoConstants.EVENT_TYPE_ONDEMAND); // Try to start again + + Thread.sleep(200); + assertTrue("Scheduler should remain running", scheduler.isRunning()); + } + + @Test + public void testStartWithInvalidBufferType() throws InterruptedException { + scheduler = new MultiTaskHarvestScheduler(mockOnDemandTask, mockLiveTask, mockConfiguration); + + scheduler.start("INVALID_TYPE"); + + Thread.sleep(200); + // Should not start with invalid type + } + + @Test + public void testStartAfterShutdown() { + scheduler = new MultiTaskHarvestScheduler(mockOnDemandTask, mockLiveTask, mockConfiguration); + + scheduler.shutdown(); + scheduler.start(NRVideoConstants.EVENT_TYPE_ONDEMAND); + + assertFalse("Scheduler should not start after shutdown", scheduler.isRunning()); + } + + // ========== Shutdown Tests ========== + + @Test + public void testShutdown() { + scheduler = new MultiTaskHarvestScheduler(mockOnDemandTask, mockLiveTask, mockConfiguration); + + scheduler.start(); + scheduler.shutdown(); + + assertFalse("Scheduler should not be running after shutdown", scheduler.isRunning()); + } + + @Test + public void testShutdownBeforeStart() { + scheduler = new MultiTaskHarvestScheduler(mockOnDemandTask, mockLiveTask, mockConfiguration); + + scheduler.shutdown(); + + assertFalse("Scheduler should remain shut down", scheduler.isRunning()); + } + + @Test + public void testDoubleShutdown() { + scheduler = new MultiTaskHarvestScheduler(mockOnDemandTask, mockLiveTask, mockConfiguration); + + scheduler.shutdown(); + scheduler.shutdown(); // Second shutdown should be safe + + assertFalse("Scheduler should remain shut down", scheduler.isRunning()); + } + + @Test + public void testShutdownExecutesImmediateHarvest() throws InterruptedException { + CountDownLatch latch = new CountDownLatch(2); + Runnable countingOnDemandTask = () -> { + latch.countDown(); + }; + Runnable countingLiveTask = () -> { + latch.countDown(); + }; + + scheduler = new MultiTaskHarvestScheduler(countingOnDemandTask, countingLiveTask, mockConfiguration); + + scheduler.start(); + Thread.sleep(100); + scheduler.shutdown(); + + boolean completed = latch.await(2, TimeUnit.SECONDS); + assertTrue("Shutdown should execute immediate harvest", completed); + } + + // ========== Force Harvest Tests ========== + + @Test + public void testForceHarvest() throws InterruptedException { + CountDownLatch latch = new CountDownLatch(2); + Runnable countingOnDemandTask = () -> latch.countDown(); + Runnable countingLiveTask = () -> latch.countDown(); + + scheduler = new MultiTaskHarvestScheduler(countingOnDemandTask, countingLiveTask, mockConfiguration); + + scheduler.forceHarvest(); + + boolean completed = latch.await(2, TimeUnit.SECONDS); + assertTrue("Force harvest should execute both tasks", completed); + } + + @Test + public void testForceHarvestWhileRunning() throws InterruptedException { + AtomicInteger onDemandCount = new AtomicInteger(0); + AtomicInteger liveCount = new AtomicInteger(0); + + Runnable countingOnDemandTask = () -> onDemandCount.incrementAndGet(); + Runnable countingLiveTask = () -> liveCount.incrementAndGet(); + + scheduler = new MultiTaskHarvestScheduler(countingOnDemandTask, countingLiveTask, mockConfiguration); + + scheduler.start(); + Thread.sleep(100); + scheduler.forceHarvest(); + Thread.sleep(100); + + assertTrue("OnDemand task should execute", onDemandCount.get() > 0); + assertTrue("Live task should execute", liveCount.get() > 0); + } + + @Test + public void testMultipleForceHarvests() throws InterruptedException { + AtomicInteger onDemandCount = new AtomicInteger(0); + AtomicInteger liveCount = new AtomicInteger(0); + + Runnable countingOnDemandTask = () -> onDemandCount.incrementAndGet(); + Runnable countingLiveTask = () -> liveCount.incrementAndGet(); + + scheduler = new MultiTaskHarvestScheduler(countingOnDemandTask, countingLiveTask, mockConfiguration); + + scheduler.forceHarvest(); + scheduler.forceHarvest(); + scheduler.forceHarvest(); + + Thread.sleep(200); + + assertTrue("Multiple force harvests should execute", onDemandCount.get() >= 3); + assertTrue("Multiple force harvests should execute", liveCount.get() >= 3); + } + + // ========== isRunning Tests ========== + + @Test + public void testIsRunningInitially() { + scheduler = new MultiTaskHarvestScheduler(mockOnDemandTask, mockLiveTask, mockConfiguration); + + assertFalse("Scheduler should not be running initially", scheduler.isRunning()); + } + + @Test + public void testIsRunningAfterStart() throws InterruptedException { + scheduler = new MultiTaskHarvestScheduler(mockOnDemandTask, mockLiveTask, mockConfiguration); + + scheduler.start(); + Thread.sleep(100); + + assertTrue("Scheduler should be running after start", scheduler.isRunning()); + } + + @Test + public void testIsRunningAfterShutdown() { + scheduler = new MultiTaskHarvestScheduler(mockOnDemandTask, mockLiveTask, mockConfiguration); + + scheduler.start(); + scheduler.shutdown(); + + assertFalse("Scheduler should not be running after shutdown", scheduler.isRunning()); + } + + @Test + public void testIsRunningWithOnlyOnDemand() throws InterruptedException { + scheduler = new MultiTaskHarvestScheduler(mockOnDemandTask, mockLiveTask, mockConfiguration); + + scheduler.start(NRVideoConstants.EVENT_TYPE_ONDEMAND); + Thread.sleep(100); + + assertTrue("Scheduler should be running with only OnDemand", scheduler.isRunning()); + } + + @Test + public void testIsRunningWithOnlyLive() throws InterruptedException { + scheduler = new MultiTaskHarvestScheduler(mockOnDemandTask, mockLiveTask, mockConfiguration); + + scheduler.start(NRVideoConstants.EVENT_TYPE_LIVE); + Thread.sleep(100); + + assertTrue("Scheduler should be running with only Live", scheduler.isRunning()); + } + + // ========== Pause/Resume Tests ========== + + @Test + public void testPause() throws InterruptedException { + scheduler = new MultiTaskHarvestScheduler(mockOnDemandTask, mockLiveTask, mockConfiguration); + + scheduler.start(); + Thread.sleep(100); + scheduler.pause(); + + // Scheduler is still marked as running, but callbacks are removed + assertTrue("Scheduler should still be marked as running after pause", scheduler.isRunning()); + } + + @Test + public void testPauseBeforeStart() { + scheduler = new MultiTaskHarvestScheduler(mockOnDemandTask, mockLiveTask, mockConfiguration); + + scheduler.pause(); // Should not throw exception + + assertFalse("Scheduler should not be running", scheduler.isRunning()); + } + + @Test + public void testResumeWithNormalIntervals() throws InterruptedException { + scheduler = new MultiTaskHarvestScheduler(mockOnDemandTask, mockLiveTask, mockConfiguration); + + scheduler.start(); + Thread.sleep(100); + scheduler.pause(); + scheduler.resume(false); + + // Should resume with normal intervals + assertTrue("Scheduler should be running after resume", scheduler.isRunning()); + } + + @Test + public void testResumeWithExtendedIntervals() throws InterruptedException { + when(mockConfiguration.isTV()).thenReturn(true); + + scheduler = new MultiTaskHarvestScheduler(mockOnDemandTask, mockLiveTask, mockConfiguration); + + scheduler.start(); + Thread.sleep(100); + scheduler.pause(); + scheduler.resume(true); + + // Should resume with extended intervals for TV + assertTrue("Scheduler should be running after resume", scheduler.isRunning()); + } + + @Test + public void testResumeAfterShutdown() { + scheduler = new MultiTaskHarvestScheduler(mockOnDemandTask, mockLiveTask, mockConfiguration); + + scheduler.shutdown(); + scheduler.resume(false); + + assertFalse("Scheduler should not resume after shutdown", scheduler.isRunning()); + } + + @Test + public void testPauseResumeMultipleTimes() throws InterruptedException { + scheduler = new MultiTaskHarvestScheduler(mockOnDemandTask, mockLiveTask, mockConfiguration); + + scheduler.start(); + for (int i = 0; i < 5; i++) { + Thread.sleep(50); + scheduler.pause(); + Thread.sleep(50); + scheduler.resume(false); + } + + assertTrue("Scheduler should handle multiple pause/resume cycles", scheduler.isRunning()); + } + + // ========== Device Type Tests ========== + + @Test + public void testMobileDeviceThreadPriority() { + when(mockConfiguration.isTV()).thenReturn(false); + + scheduler = new MultiTaskHarvestScheduler(mockOnDemandTask, mockLiveTask, mockConfiguration); + + assertNotNull("Mobile scheduler should initialize", scheduler); + } + + @Test + public void testTVDeviceThreadPriority() { + when(mockConfiguration.isTV()).thenReturn(true); + + scheduler = new MultiTaskHarvestScheduler(mockOnDemandTask, mockLiveTask, mockConfiguration); + + assertNotNull("TV scheduler should initialize", scheduler); + } + + @Test + public void testTVDeviceWithExtendedIntervals() throws InterruptedException { + when(mockConfiguration.isTV()).thenReturn(true); + + scheduler = new MultiTaskHarvestScheduler(mockOnDemandTask, mockLiveTask, mockConfiguration); + + scheduler.start(); + Thread.sleep(100); + scheduler.pause(); + scheduler.resume(true); // Extended intervals + + assertTrue("TV scheduler should use extended intervals", scheduler.isRunning()); + } + + @Test + public void testMobileDeviceWithExtendedIntervalsIgnored() throws InterruptedException { + when(mockConfiguration.isTV()).thenReturn(false); + + scheduler = new MultiTaskHarvestScheduler(mockOnDemandTask, mockLiveTask, mockConfiguration); + + scheduler.start(); + Thread.sleep(100); + scheduler.pause(); + scheduler.resume(true); // Extended intervals - should be ignored for mobile + + assertTrue("Mobile scheduler should ignore extended intervals", scheduler.isRunning()); + } + + // ========== Error Handling Tests ========== + + @Test + public void testOnDemandTaskThrowsException() throws InterruptedException { + Runnable failingTask = () -> { + throw new RuntimeException("Test exception"); + }; + + scheduler = new MultiTaskHarvestScheduler(failingTask, mockLiveTask, mockConfiguration); + + scheduler.start(NRVideoConstants.EVENT_TYPE_ONDEMAND); + Thread.sleep(1500); // Wait for at least one execution + + // Should handle exception and continue running + assertTrue("Scheduler should handle exceptions and continue", scheduler.isRunning()); + } + + @Test + public void testLiveTaskThrowsException() throws InterruptedException { + Runnable failingTask = () -> { + throw new RuntimeException("Test exception"); + }; + + scheduler = new MultiTaskHarvestScheduler(mockOnDemandTask, failingTask, mockConfiguration); + + scheduler.start(NRVideoConstants.EVENT_TYPE_LIVE); + Thread.sleep(1000); // Wait for at least one execution + + // Should handle exception and continue running + assertTrue("Scheduler should handle exceptions and continue", scheduler.isRunning()); + } + + @Test + public void testBothTasksThrowExceptions() throws InterruptedException { + Runnable failingOnDemandTask = () -> { + throw new RuntimeException("OnDemand exception"); + }; + Runnable failingLiveTask = () -> { + throw new RuntimeException("Live exception"); + }; + + scheduler = new MultiTaskHarvestScheduler(failingOnDemandTask, failingLiveTask, mockConfiguration); + + scheduler.start(); + Thread.sleep(1500); + + // Should handle both exceptions and continue running + assertTrue("Scheduler should handle both exceptions", scheduler.isRunning()); + } + + // ========== Harvest Interval Tests ========== + // Note: Timing-based tests removed due to Robolectric Handler limitations + // The scheduler interval configuration is tested in constructor tests + + // ========== Null Task Handling ========== + + @Test + public void testNullOnDemandTask() { + scheduler = new MultiTaskHarvestScheduler(null, mockLiveTask, mockConfiguration); + + scheduler.start(NRVideoConstants.EVENT_TYPE_ONDEMAND); + + // Should not crash with null task + assertNotNull("Scheduler should handle null OnDemand task", scheduler); + } + + @Test + public void testNullLiveTask() { + scheduler = new MultiTaskHarvestScheduler(mockOnDemandTask, null, mockConfiguration); + + scheduler.start(NRVideoConstants.EVENT_TYPE_LIVE); + + // Should not crash with null task + assertNotNull("Scheduler should handle null Live task", scheduler); + } + + @Test + public void testBothTasksNull() { + scheduler = new MultiTaskHarvestScheduler(null, null, mockConfiguration); + + scheduler.start(); + + // Should not crash with both tasks null + assertNotNull("Scheduler should handle both null tasks", scheduler); + } + + // ========== Edge Cases ========== + + @Test + public void testStartStopSequence() throws InterruptedException { + scheduler = new MultiTaskHarvestScheduler(mockOnDemandTask, mockLiveTask, mockConfiguration); + + scheduler.start(); + Thread.sleep(100); + scheduler.shutdown(); + Thread.sleep(100); + + assertFalse("Scheduler should be stopped", scheduler.isRunning()); + } + + @Test + public void testRapidStartStopCycles() throws InterruptedException { + for (int i = 0; i < 5; i++) { + scheduler = new MultiTaskHarvestScheduler(mockOnDemandTask, mockLiveTask, mockConfiguration); + scheduler.start(); + Thread.sleep(50); + scheduler.shutdown(); + } + + // Should handle rapid creation/destruction without issues + } + + @Test + public void testZeroHarvestInterval() { + when(mockConfiguration.getHarvestCycleSeconds()).thenReturn(0); + when(mockConfiguration.getLiveHarvestCycleSeconds()).thenReturn(0); + + scheduler = new MultiTaskHarvestScheduler(mockOnDemandTask, mockLiveTask, mockConfiguration); + + assertNotNull("Scheduler should handle zero interval", scheduler); + } + + @Test + public void testVeryLargeHarvestInterval() { + when(mockConfiguration.getHarvestCycleSeconds()).thenReturn(Integer.MAX_VALUE); + when(mockConfiguration.getLiveHarvestCycleSeconds()).thenReturn(Integer.MAX_VALUE); + + scheduler = new MultiTaskHarvestScheduler(mockOnDemandTask, mockLiveTask, mockConfiguration); + + assertNotNull("Scheduler should handle very large interval", scheduler); + } +} diff --git a/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/harvest/OptimizedHttpClientTest.java b/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/harvest/OptimizedHttpClientTest.java new file mode 100644 index 00000000..626a66eb --- /dev/null +++ b/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/harvest/OptimizedHttpClientTest.java @@ -0,0 +1,508 @@ +package com.newrelic.videoagent.core.harvest; + +import android.content.Context; + +import com.newrelic.videoagent.core.NRVideoConfiguration; +import com.newrelic.videoagent.core.auth.TokenManager; +import com.newrelic.videoagent.core.device.DeviceInformation; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +/** + * Unit tests for OptimizedHttpClient. + * Tests HTTP request handling, retries, regional endpoints, compression, and token management. + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = 28, manifest = Config.NONE) +public class OptimizedHttpClientTest { + + private Context context; + + @Mock + private NRVideoConfiguration mockConfiguration; + + private OptimizedHttpClient httpClient; + + @Before + public void setUp() { + MockitoAnnotations.openMocks(this); + context = RuntimeEnvironment.getApplication(); + + // Set up default configuration behavior + when(mockConfiguration.getRegion()).thenReturn("US"); + when(mockConfiguration.getApplicationToken()).thenReturn("test-app-token-12345"); + when(mockConfiguration.isMemoryOptimized()).thenReturn(false); + + httpClient = new OptimizedHttpClient(mockConfiguration, context); + } + + // ========== Constructor and Initialization Tests ========== + + @Test + public void testConstructorWithUSRegion() { + when(mockConfiguration.getRegion()).thenReturn("US"); + + OptimizedHttpClient client = new OptimizedHttpClient(mockConfiguration, context); + + assertNotNull("HTTP client should be created", client); + } + + @Test + public void testConstructorWithEURegion() { + when(mockConfiguration.getRegion()).thenReturn("EU"); + + OptimizedHttpClient client = new OptimizedHttpClient(mockConfiguration, context); + + assertNotNull("HTTP client should be created", client); + } + + @Test + public void testConstructorWithAPRegion() { + when(mockConfiguration.getRegion()).thenReturn("AP"); + + OptimizedHttpClient client = new OptimizedHttpClient(mockConfiguration, context); + + assertNotNull("HTTP client should be created", client); + } + + @Test + public void testConstructorWithGOVRegion() { + when(mockConfiguration.getRegion()).thenReturn("GOV"); + + OptimizedHttpClient client = new OptimizedHttpClient(mockConfiguration, context); + + assertNotNull("HTTP client should be created", client); + } + + @Test + public void testConstructorWithSTAGINGRegion() { + when(mockConfiguration.getRegion()).thenReturn("STAGING"); + + OptimizedHttpClient client = new OptimizedHttpClient(mockConfiguration, context); + + assertNotNull("HTTP client should be created", client); + } + + @Test + public void testConstructorWithInvalidRegionDefaultsToUS() { + when(mockConfiguration.getRegion()).thenReturn("INVALID_REGION"); + + OptimizedHttpClient client = new OptimizedHttpClient(mockConfiguration, context); + + assertNotNull("HTTP client should be created with default US region", client); + } + + @Test + public void testConstructorWithLowercaseRegion() { + when(mockConfiguration.getRegion()).thenReturn("eu"); + + OptimizedHttpClient client = new OptimizedHttpClient(mockConfiguration, context); + + assertNotNull("HTTP client should handle lowercase region", client); + } + + @Test + public void testConstructorWithMemoryOptimizedEnabled() { + when(mockConfiguration.isMemoryOptimized()).thenReturn(true); + + OptimizedHttpClient client = new OptimizedHttpClient(mockConfiguration, context); + + assertNotNull("HTTP client should be created with memory optimization", client); + } + + @Test + public void testConstructorWithMemoryOptimizedDisabled() { + when(mockConfiguration.isMemoryOptimized()).thenReturn(false); + + OptimizedHttpClient client = new OptimizedHttpClient(mockConfiguration, context); + + assertNotNull("HTTP client should be created without memory optimization", client); + } + + // ========== sendEvents Tests ========== + + @Test + public void testSendEventsWithNullList() { + boolean result = httpClient.sendEvents(null, "video"); + + assertTrue("Sending null events should return true", result); + } + + @Test + public void testSendEventsWithEmptyList() { + List> events = new ArrayList<>(); + + boolean result = httpClient.sendEvents(events, "video"); + + assertTrue("Sending empty events should return true", result); + } + + @Test + public void testSendEventsWithSingleEvent() { + List> events = new ArrayList<>(); + Map event = new HashMap<>(); + event.put("eventType", "CONTENT_START"); + event.put("timestamp", System.currentTimeMillis()); + events.add(event); + + // This will fail in unit test (no actual network), but tests the flow + boolean result = httpClient.sendEvents(events, "video"); + + // In unit test without mock HTTP, this will return false after retries + assertFalse("Without mock HTTP, send should fail", result); + } + + @Test + public void testSendEventsWithMultipleEvents() { + List> events = new ArrayList<>(); + for (int i = 0; i < 5; i++) { + Map event = new HashMap<>(); + event.put("eventType", "CONTENT_HEARTBEAT"); + event.put("timestamp", System.currentTimeMillis()); + events.add(event); + } + + // This will fail in unit test (no actual network), but tests the flow + boolean result = httpClient.sendEvents(events, "video"); + + assertFalse("Without mock HTTP, send should fail", result); + } + + @Test + public void testSendEventsWithLargeEventList() { + List> events = new ArrayList<>(); + // Create more than 10 events to trigger compression + for (int i = 0; i < 15; i++) { + Map event = new HashMap<>(); + event.put("eventType", "CONTENT_HEARTBEAT"); + event.put("timestamp", System.currentTimeMillis()); + event.put("index", i); + events.add(event); + } + + // This will fail in unit test (no actual network), but tests compression path + boolean result = httpClient.sendEvents(events, "video"); + + assertFalse("Without mock HTTP, send should fail", result); + } + + // ========== Endpoint Type Tests ========== + + @Test + public void testSendEventsWithVideoEndpointType() { + List> events = createSampleEvents(1); + + boolean result = httpClient.sendEvents(events, "video"); + + // Endpoint type is passed but not currently used in implementation + assertFalse("Without mock HTTP, send should fail", result); + } + + @Test + public void testSendEventsWithLiveEndpointType() { + List> events = createSampleEvents(1); + + boolean result = httpClient.sendEvents(events, "live"); + + assertFalse("Without mock HTTP, send should fail", result); + } + + @Test + public void testSendEventsWithOnDemandEndpointType() { + List> events = createSampleEvents(1); + + boolean result = httpClient.sendEvents(events, "on_demand"); + + assertFalse("Without mock HTTP, send should fail", result); + } + + // ========== Retry Logic Tests ========== + + @Test + public void testSendEventsRetriesOnFailure() { + List> events = createSampleEvents(2); + + // Without actual HTTP mocking, this will retry 3 times and fail + long startTime = System.currentTimeMillis(); + boolean result = httpClient.sendEvents(events, "video"); + long duration = System.currentTimeMillis() - startTime; + + assertFalse("Send should fail after retries", result); + // Retries should happen quickly (no sleep for mobile/TV optimization) + assertTrue("Retries should complete quickly", duration < 5000); + } + + // ========== Event Creation Helpers ========== + + private List> createSampleEvents(int count) { + List> events = new ArrayList<>(); + for (int i = 0; i < count; i++) { + Map event = new HashMap<>(); + event.put("eventType", "TEST_EVENT"); + event.put("timestamp", System.currentTimeMillis()); + event.put("index", i); + events.add(event); + } + return events; + } + + // ========== Configuration Tests ========== + + @Test + public void testHTTPClientWithDifferentApplicationTokens() { + when(mockConfiguration.getApplicationToken()).thenReturn("different-token-xyz"); + + OptimizedHttpClient client = new OptimizedHttpClient(mockConfiguration, context); + + assertNotNull("HTTP client should work with different tokens", client); + } + + @Test + public void testHTTPClientWithEmptyApplicationToken() { + when(mockConfiguration.getApplicationToken()).thenReturn(""); + + OptimizedHttpClient client = new OptimizedHttpClient(mockConfiguration, context); + + assertNotNull("HTTP client should handle empty token", client); + } + + // ========== Regional Endpoint Tests ========== + + @Test + public void testRegionalEndpointForUS() { + when(mockConfiguration.getRegion()).thenReturn("US"); + + OptimizedHttpClient client = new OptimizedHttpClient(mockConfiguration, context); + List> events = createSampleEvents(1); + + // Will attempt to connect to US endpoint + boolean result = client.sendEvents(events, "video"); + + assertFalse("Without real network, send should fail", result); + } + + @Test + public void testRegionalEndpointForEU() { + when(mockConfiguration.getRegion()).thenReturn("EU"); + + OptimizedHttpClient client = new OptimizedHttpClient(mockConfiguration, context); + List> events = createSampleEvents(1); + + // Will attempt to connect to EU endpoint + boolean result = client.sendEvents(events, "video"); + + assertFalse("Without real network, send should fail", result); + } + + @Test + public void testRegionalEndpointForAP() { + when(mockConfiguration.getRegion()).thenReturn("AP"); + + OptimizedHttpClient client = new OptimizedHttpClient(mockConfiguration, context); + List> events = createSampleEvents(1); + + // Will attempt to connect to AP endpoint + boolean result = client.sendEvents(events, "video"); + + assertFalse("Without real network, send should fail", result); + } + + // ========== Compression Tests ========== + + @Test + public void testCompressionNotUsedForSmallEventBatch() { + // Less than 10 events - no compression + List> events = createSampleEvents(5); + + boolean result = httpClient.sendEvents(events, "video"); + + assertFalse("Without mock HTTP, send should fail", result); + } + + @Test + public void testCompressionUsedForLargeEventBatch() { + // More than 10 events - compression enabled + List> events = createSampleEvents(20); + + boolean result = httpClient.sendEvents(events, "video"); + + assertFalse("Without mock HTTP, send should fail", result); + } + + @Test + public void testCompressionThresholdAt10Events() { + // Exactly 10 events - no compression (> 10 triggers it) + List> events = createSampleEvents(10); + + boolean result = httpClient.sendEvents(events, "video"); + + assertFalse("Without mock HTTP, send should fail", result); + } + + @Test + public void testCompressionThresholdAt11Events() { + // 11 events - compression enabled + List> events = createSampleEvents(11); + + boolean result = httpClient.sendEvents(events, "video"); + + assertFalse("Without mock HTTP, send should fail", result); + } + + // ========== Memory Optimization Tests ========== + + @Test + public void testMemoryOptimizedTimeouts() { + when(mockConfiguration.isMemoryOptimized()).thenReturn(true); + + OptimizedHttpClient client = new OptimizedHttpClient(mockConfiguration, context); + List> events = createSampleEvents(1); + + // Should use shorter timeouts (6s connect, 10s read) + boolean result = client.sendEvents(events, "video"); + + assertFalse("Without mock HTTP, send should fail", result); + } + + @Test + public void testStandardTimeouts() { + when(mockConfiguration.isMemoryOptimized()).thenReturn(false); + + OptimizedHttpClient client = new OptimizedHttpClient(mockConfiguration, context); + List> events = createSampleEvents(1); + + // Should use standard timeouts (30s connect, 60s read) + boolean result = client.sendEvents(events, "video"); + + assertFalse("Without mock HTTP, send should fail", result); + } + + // ========== Event Payload Tests ========== + + @Test + public void testSendEventsWithComplexEventData() { + List> events = new ArrayList<>(); + Map event = new HashMap<>(); + event.put("eventType", "CONTENT_START"); + event.put("timestamp", System.currentTimeMillis()); + event.put("contentTitle", "Test Video"); + event.put("contentDuration", 12000L); + event.put("contentPlayhead", 0L); + event.put("contentBitrate", 2500); + event.put("customAttributes", new HashMap() {{ + put("userId", "user123"); + put("category", "entertainment"); + }}); + events.add(event); + + boolean result = httpClient.sendEvents(events, "video"); + + assertFalse("Without mock HTTP, send should fail", result); + } + + @Test + public void testSendEventsWithNestedObjects() { + List> events = new ArrayList<>(); + Map event = new HashMap<>(); + event.put("eventType", "CONTENT_BUFFER_START"); + event.put("metadata", new HashMap() {{ + put("device", new HashMap() {{ + put("type", "mobile"); + put("os", "Android"); + }}); + }}); + events.add(event); + + boolean result = httpClient.sendEvents(events, "video"); + + assertFalse("Without mock HTTP, send should fail", result); + } + + @Test + public void testSendEventsWithArrayValues() { + List> events = new ArrayList<>(); + Map event = new HashMap<>(); + event.put("eventType", "CONTENT_REQUEST"); + event.put("tags", Arrays.asList("video", "live", "sports")); + event.put("resolutions", Arrays.asList(720, 1080, 1440)); + events.add(event); + + boolean result = httpClient.sendEvents(events, "video"); + + assertFalse("Without mock HTTP, send should fail", result); + } + + // ========== Edge Cases ========== + + @Test + public void testSendEventsWithVeryLargeEventBatch() { + // Test with 100 events + List> events = createSampleEvents(100); + + boolean result = httpClient.sendEvents(events, "video"); + + assertFalse("Without mock HTTP, send should fail", result); + } + + @Test + public void testSendEventsWithSingleEventAtCompressionBoundary() { + // Test boundary condition - exactly 11 events + List> events = createSampleEvents(11); + + boolean result = httpClient.sendEvents(events, "video"); + + assertFalse("Without mock HTTP, send should fail", result); + } + + @Test + public void testMultipleConsecutiveSendOperations() { + List> events1 = createSampleEvents(2); + List> events2 = createSampleEvents(3); + List> events3 = createSampleEvents(1); + + boolean result1 = httpClient.sendEvents(events1, "video"); + boolean result2 = httpClient.sendEvents(events2, "video"); + boolean result3 = httpClient.sendEvents(events3, "video"); + + // All should fail without real network + assertFalse("First send should fail", result1); + assertFalse("Second send should fail", result2); + assertFalse("Third send should fail", result3); + } + + // ========== Null and Empty Handling ========== + + @Test + public void testSendEventsWithNullEndpointType() { + List> events = createSampleEvents(1); + + boolean result = httpClient.sendEvents(events, null); + + assertFalse("Without mock HTTP, send should fail", result); + } + + @Test + public void testSendEventsWithEmptyEndpointType() { + List> events = createSampleEvents(1); + + boolean result = httpClient.sendEvents(events, ""); + + assertFalse("Without mock HTTP, send should fail", result); + } +} diff --git a/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/harvest/PriorityEventBufferTest.java b/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/harvest/PriorityEventBufferTest.java new file mode 100644 index 00000000..0a5044b0 --- /dev/null +++ b/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/harvest/PriorityEventBufferTest.java @@ -0,0 +1,626 @@ +package com.newrelic.videoagent.core.harvest; + +import com.newrelic.videoagent.core.NRVideoConstants; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.Assert.*; + +/** + * Comprehensive unit tests for PriorityEventBuffer. + * Tests priority handling, capacity management, overflow callbacks, and platform-specific optimizations. + */ +public class PriorityEventBufferTest { + + private PriorityEventBuffer mobileBuffer; + private PriorityEventBuffer tvBuffer; + private SizeEstimator sizeEstimator; + private TestOverflowCallback overflowCallback; + private TestCapacityCallback capacityCallback; + + @Before + public void setUp() { + mobileBuffer = new PriorityEventBuffer(false); // Mobile device + tvBuffer = new PriorityEventBuffer(true); // TV device + sizeEstimator = new DefaultSizeEstimator(); + overflowCallback = new TestOverflowCallback(); + capacityCallback = new TestCapacityCallback(); + + mobileBuffer.setOverflowCallback(overflowCallback); + mobileBuffer.setCapacityCallback(capacityCallback); + + // Also set callbacks for TV buffer + tvBuffer.setOverflowCallback(new TestOverflowCallback()); + tvBuffer.setCapacityCallback(new TestCapacityCallback()); + } + + @After + public void tearDown() { + if (mobileBuffer != null) { + mobileBuffer.cleanup(); + } + if (tvBuffer != null) { + tvBuffer.cleanup(); + } + } + + // ========== Priority Tests ========== + + @Test + public void testLiveEventsPrioritizedOverOndemand() { + // Add on-demand event first + Map ondemandEvent = createOndemandEvent("ondemand1"); + mobileBuffer.addEvent(ondemandEvent); + + // Add live event + Map liveEvent = createLiveEvent("live1"); + mobileBuffer.addEvent(liveEvent); + + // Poll live events first + List> liveBatch = mobileBuffer.pollBatchByPriority( + 64 * 1024, sizeEstimator, NRVideoConstants.EVENT_TYPE_LIVE + ); + + assertEquals("Live events should be polled first", 1, liveBatch.size()); + assertEquals("live1", liveBatch.get(0).get("actionName")); + + // Poll on-demand events + List> ondemandBatch = mobileBuffer.pollBatchByPriority( + 64 * 1024, sizeEstimator, NRVideoConstants.EVENT_TYPE_ONDEMAND + ); + + assertEquals("On-demand events should remain", 1, ondemandBatch.size()); + assertEquals("ondemand1", ondemandBatch.get(0).get("actionName")); + } + + @Test + public void testLiveEventsPolledBeforeOndemandEvents() { + // Add multiple events of each type + for (int i = 0; i < 5; i++) { + mobileBuffer.addEvent(createOndemandEvent("ondemand" + i)); + mobileBuffer.addEvent(createLiveEvent("live" + i)); + } + + // Poll live events first + List> liveBatch = mobileBuffer.pollBatchByPriority( + 64 * 1024, sizeEstimator, NRVideoConstants.EVENT_TYPE_LIVE + ); + + assertTrue("Should get live events", liveBatch.size() > 0); + for (Map event : liveBatch) { + assertTrue("Event should be live", + event.get("actionName").toString().startsWith("live")); + } + } + + @Test + public void testOndemandEventsPreservedDuringLivePolling() { + mobileBuffer.addEvent(createOndemandEvent("ondemand1")); + mobileBuffer.addEvent(createLiveEvent("live1")); + mobileBuffer.addEvent(createOndemandEvent("ondemand2")); + + // Poll live events + List> liveBatch = mobileBuffer.pollBatchByPriority( + 64 * 1024, sizeEstimator, NRVideoConstants.EVENT_TYPE_LIVE + ); + + assertEquals("Should get 1 live event", 1, liveBatch.size()); + + // Verify on-demand events are still there + List> ondemandBatch = mobileBuffer.pollBatchByPriority( + 64 * 1024, sizeEstimator, NRVideoConstants.EVENT_TYPE_ONDEMAND + ); + + assertEquals("Should have 2 on-demand events", 2, ondemandBatch.size()); + } + + @Test + public void testPriorityPreservationWithLargeDataset() { + // Add 100 events of each type + for (int i = 0; i < 100; i++) { + mobileBuffer.addEvent(createOndemandEvent("ondemand" + i)); + mobileBuffer.addEvent(createLiveEvent("live" + i)); + } + + // Poll all live events + int liveCount = 0; + while (true) { + List> batch = mobileBuffer.pollBatchByPriority( + 64 * 1024, sizeEstimator, NRVideoConstants.EVENT_TYPE_LIVE + ); + if (batch.isEmpty()) break; + liveCount += batch.size(); + } + + assertTrue("Should poll all live events", liveCount > 0); + + // Poll all on-demand events + int ondemandCount = 0; + while (true) { + List> batch = mobileBuffer.pollBatchByPriority( + 64 * 1024, sizeEstimator, NRVideoConstants.EVENT_TYPE_ONDEMAND + ); + if (batch.isEmpty()) break; + ondemandCount += batch.size(); + } + + assertTrue("Should poll all on-demand events", ondemandCount > 0); + } + + // ========== Capacity Management Tests ========== + + @Test + public void testMobileBufferCapacity() { + // Mobile has smaller capacity (150 live, 350 on-demand) + for (int i = 0; i < 200; i++) { + mobileBuffer.addEvent(createLiveEvent("live" + i)); + } + + int eventCount = mobileBuffer.getEventCount(); + assertTrue("Mobile buffer should limit live events to ~150", + eventCount <= 150); + } + + @Test + public void testTVBufferCapacity() { + // TV has larger capacity (300 live, 700 on-demand) + for (int i = 0; i < 400; i++) { + tvBuffer.addEvent(createLiveEvent("live" + i)); + } + + int eventCount = tvBuffer.getEventCount(); + assertTrue("TV buffer should allow more events", + eventCount > 150); + assertTrue("TV buffer should limit live events to ~300", + eventCount <= 300); + } + + @Test + public void testBufferOverflowEvictsOldestEvents() { + // Add more events than capacity + for (int i = 0; i < 200; i++) { + mobileBuffer.addEvent(createLiveEvent("live" + i)); + } + + // Poll events and check that oldest were evicted + List> batch = mobileBuffer.pollBatchByPriority( + 1024 * 1024, sizeEstimator, NRVideoConstants.EVENT_TYPE_LIVE + ); + + // Should not contain oldest events (live0, live1, etc.) + boolean hasOldestEvent = false; + for (Map event : batch) { + if ("live0".equals(event.get("actionName"))) { + hasOldestEvent = true; + break; + } + } + + assertFalse("Oldest events should be evicted", hasOldestEvent); + } + + @Test + public void testCapacityForBothQueues() { + // Fill both queues + for (int i = 0; i < 500; i++) { + mobileBuffer.addEvent(createLiveEvent("live" + i)); + mobileBuffer.addEvent(createOndemandEvent("ondemand" + i)); + } + + int totalCount = mobileBuffer.getEventCount(); + + // Mobile: 150 live + 350 on-demand = 500 max + assertTrue("Total capacity should be enforced", totalCount <= 500); + } + + @Test + public void testOverflowCallbackTriggeredAt90Percent() { + // Add events until 90% capacity + int liveCapacity = 150; // Mobile capacity + int targetCount = (int) (liveCapacity * 0.9); + + // Add events just below 90% + for (int i = 0; i < targetCount - 1; i++) { + mobileBuffer.addEvent(createLiveEvent("live" + i)); + } + + // Reset the overflow callback to check if it gets triggered at 90% + overflowCallback.reset(); + + // Add one more to reach 90% + mobileBuffer.addEvent(createLiveEvent("live" + (targetCount - 1))); + + assertTrue("Overflow callback should be triggered at 90%", + overflowCallback.wasTriggered()); + assertEquals("Should indicate live buffer type", + NRVideoConstants.EVENT_TYPE_LIVE, + overflowCallback.getBufferType()); + } + + @Test + public void testCapacityCallbackOnFirstEvent() { + capacityCallback.reset(); + + // Add first event + mobileBuffer.addEvent(createLiveEvent("live1")); + + assertTrue("Capacity callback should be triggered on first event", + capacityCallback.wasTriggered()); + assertEquals("Should indicate live buffer type", + NRVideoConstants.EVENT_TYPE_LIVE, + capacityCallback.getBufferType()); + } + + // ========== Batch Operations Tests ========== + + @Test + public void testPollBatchRespectsSizeLimit() { + // Add multiple events + for (int i = 0; i < 20; i++) { + mobileBuffer.addEvent(createLiveEvent("live" + i)); + } + + // Poll with small size limit + int maxSize = 4096; // 4KB + List> batch = mobileBuffer.pollBatchByPriority( + maxSize, sizeEstimator, NRVideoConstants.EVENT_TYPE_LIVE + ); + + // Calculate actual batch size + int batchSize = 0; + for (Map event : batch) { + batchSize += sizeEstimator.estimate(event); + } + + assertTrue("Batch size should not exceed limit", + batchSize <= maxSize || batch.size() == 1); + } + + @Test + public void testPollBatchReturnsEmptyForEmptyQueue() { + List> batch = mobileBuffer.pollBatchByPriority( + 64 * 1024, sizeEstimator, NRVideoConstants.EVENT_TYPE_LIVE + ); + + assertTrue("Should return empty list for empty queue", batch.isEmpty()); + } + + @Test + public void testPollBatchWithNullSizeEstimator() { + mobileBuffer.addEvent(createLiveEvent("live1")); + + // Poll without size estimator (uses default estimation) + List> batch = mobileBuffer.pollBatchByPriority( + 64 * 1024, null, NRVideoConstants.EVENT_TYPE_LIVE + ); + + assertEquals("Should still poll events", 1, batch.size()); + } + + @Test + public void testMultipleBatchPolling() { + // Add 50 events + for (int i = 0; i < 50; i++) { + mobileBuffer.addEvent(createLiveEvent("live" + i)); + } + + // Poll in multiple batches + List> batch1 = mobileBuffer.pollBatchByPriority( + 8192, sizeEstimator, NRVideoConstants.EVENT_TYPE_LIVE + ); + List> batch2 = mobileBuffer.pollBatchByPriority( + 8192, sizeEstimator, NRVideoConstants.EVENT_TYPE_LIVE + ); + + assertTrue("First batch should have events", batch1.size() > 0); + assertTrue("Second batch should have events", batch2.size() > 0); + + // Verify no duplicates + String firstEventInBatch1 = (String) batch1.get(0).get("actionName"); + String firstEventInBatch2 = (String) batch2.get(0).get("actionName"); + assertNotEquals("Batches should not contain same events", + firstEventInBatch1, firstEventInBatch2); + } + + @Test + public void testTVDeviceHasLargerBatchSize() { + // Add events to both buffers + for (int i = 0; i < 50; i++) { + mobileBuffer.addEvent(createLiveEvent("live" + i)); + tvBuffer.addEvent(createLiveEvent("live" + i)); + } + + List> mobileBatch = mobileBuffer.pollBatchByPriority( + 1024 * 1024, sizeEstimator, NRVideoConstants.EVENT_TYPE_LIVE + ); + List> tvBatch = tvBuffer.pollBatchByPriority( + 1024 * 1024, sizeEstimator, NRVideoConstants.EVENT_TYPE_LIVE + ); + + assertTrue("TV should allow larger batches", + tvBatch.size() >= mobileBatch.size()); + } + + // ========== Edge Cases and State Management Tests ========== + + @Test + public void testAddNullEvent() { + mobileBuffer.addEvent(null); + + assertTrue("Buffer should remain empty after null event", + mobileBuffer.isEmpty()); + } + + @Test + public void testIsEmptyAfterAddingEvents() { + assertTrue("Buffer should start empty", mobileBuffer.isEmpty()); + + mobileBuffer.addEvent(createLiveEvent("live1")); + + assertFalse("Buffer should not be empty after adding event", + mobileBuffer.isEmpty()); + } + + @Test + public void testGetEventCount() { + assertEquals("Initial count should be 0", 0, mobileBuffer.getEventCount()); + + mobileBuffer.addEvent(createLiveEvent("live1")); + mobileBuffer.addEvent(createOndemandEvent("ondemand1")); + + assertEquals("Count should include both queues", 2, mobileBuffer.getEventCount()); + } + + @Test + public void testGetEventCountAfterPolling() { + for (int i = 0; i < 10; i++) { + mobileBuffer.addEvent(createLiveEvent("live" + i)); + } + + int initialCount = mobileBuffer.getEventCount(); + + mobileBuffer.pollBatchByPriority(64 * 1024, sizeEstimator, + NRVideoConstants.EVENT_TYPE_LIVE); + + int afterCount = mobileBuffer.getEventCount(); + + assertTrue("Count should decrease after polling", afterCount < initialCount); + } + + @Test + public void testClearResetsBuffer() { + mobileBuffer.addEvent(createLiveEvent("live1")); + mobileBuffer.addEvent(createOndemandEvent("ondemand1")); + + assertFalse("Buffer should have events", mobileBuffer.isEmpty()); + + mobileBuffer.clear(); + + assertTrue("Buffer should be empty after clear", mobileBuffer.isEmpty()); + assertEquals("Event count should be 0 after clear", 0, + mobileBuffer.getEventCount()); + } + + @Test + public void testCleanupResetsBuffer() { + mobileBuffer.addEvent(createLiveEvent("live1")); + + mobileBuffer.cleanup(); + + assertTrue("Buffer should be empty after cleanup", mobileBuffer.isEmpty()); + } + + @Test + public void testEventWithoutLiveMarkerIsOndemand() { + Map event = new HashMap<>(); + event.put("actionName", "CONTENT_START"); + // No contentIsLive marker + + mobileBuffer.addEvent(event); + + // Poll on-demand queue + List> batch = mobileBuffer.pollBatchByPriority( + 64 * 1024, sizeEstimator, NRVideoConstants.EVENT_TYPE_ONDEMAND + ); + + assertEquals("Event without live marker should be on-demand", + 1, batch.size()); + } + + @Test + public void testEventWithExplicitLiveFalseIsOndemand() { + Map event = new HashMap<>(); + event.put("actionName", "CONTENT_START"); + event.put("contentIsLive", false); + + mobileBuffer.addEvent(event); + + // Poll on-demand queue + List> batch = mobileBuffer.pollBatchByPriority( + 64 * 1024, sizeEstimator, NRVideoConstants.EVENT_TYPE_ONDEMAND + ); + + assertEquals("Event with live=false should be on-demand", + 1, batch.size()); + } + + // ========== Concurrency Tests ========== + + @Test + public void testConcurrentAddingEvents() throws InterruptedException { + int threadCount = 10; + int eventsPerThread = 20; + CountDownLatch latch = new CountDownLatch(threadCount); + + for (int i = 0; i < threadCount; i++) { + final int threadId = i; + new Thread(() -> { + for (int j = 0; j < eventsPerThread; j++) { + mobileBuffer.addEvent(createLiveEvent("thread" + threadId + "_event" + j)); + } + latch.countDown(); + }).start(); + } + + assertTrue("All threads should complete", + latch.await(5, TimeUnit.SECONDS)); + + int totalEvents = mobileBuffer.getEventCount(); + assertTrue("Should have events from concurrent threads", totalEvents > 0); + assertTrue("Should respect capacity limits", totalEvents <= 150); + } + + @Test + public void testConcurrentPolling() throws InterruptedException { + // Add events + for (int i = 0; i < 100; i++) { + mobileBuffer.addEvent(createLiveEvent("live" + i)); + } + + int threadCount = 5; + CountDownLatch latch = new CountDownLatch(threadCount); + AtomicInteger totalPolled = new AtomicInteger(0); + + for (int i = 0; i < threadCount; i++) { + new Thread(() -> { + List> batch = mobileBuffer.pollBatchByPriority( + 64 * 1024, sizeEstimator, NRVideoConstants.EVENT_TYPE_LIVE + ); + totalPolled.addAndGet(batch.size()); + latch.countDown(); + }).start(); + } + + assertTrue("All threads should complete", + latch.await(5, TimeUnit.SECONDS)); + + assertTrue("Should poll events from concurrent threads", + totalPolled.get() > 0); + } + + @Test + public void testConcurrentAddAndPoll() throws InterruptedException { + int duration = 2000; // 2 seconds + AtomicInteger addCount = new AtomicInteger(0); + AtomicInteger pollCount = new AtomicInteger(0); + CountDownLatch latch = new CountDownLatch(2); + + // Adding thread + Thread addThread = new Thread(() -> { + long endTime = System.currentTimeMillis() + duration; + while (System.currentTimeMillis() < endTime) { + mobileBuffer.addEvent(createLiveEvent("live" + addCount.incrementAndGet())); + try { Thread.sleep(10); } catch (InterruptedException e) { break; } + } + latch.countDown(); + }); + + // Polling thread + Thread pollThread = new Thread(() -> { + long endTime = System.currentTimeMillis() + duration; + while (System.currentTimeMillis() < endTime) { + List> batch = mobileBuffer.pollBatchByPriority( + 64 * 1024, sizeEstimator, NRVideoConstants.EVENT_TYPE_LIVE + ); + pollCount.addAndGet(batch.size()); + try { Thread.sleep(100); } catch (InterruptedException e) { break; } + } + latch.countDown(); + }); + + addThread.start(); + pollThread.start(); + + assertTrue("Both threads should complete", + latch.await(5, TimeUnit.SECONDS)); + + assertTrue("Should add events", addCount.get() > 0); + assertTrue("Should poll events", pollCount.get() > 0); + } + + // ========== Helper Methods ========== + + private Map createLiveEvent(String actionName) { + Map event = new HashMap<>(); + event.put("actionName", actionName); + event.put("contentIsLive", true); + event.put("timestamp", System.currentTimeMillis()); + event.put("eventType", "VIDEO"); + return event; + } + + private Map createOndemandEvent(String actionName) { + Map event = new HashMap<>(); + event.put("actionName", actionName); + event.put("contentIsLive", false); + event.put("timestamp", System.currentTimeMillis()); + event.put("eventType", "VIDEO"); + return event; + } + + // ========== Test Callback Implementations ========== + + private static class TestOverflowCallback implements EventBufferInterface.OverflowCallback { + private boolean triggered = false; + private String bufferType = null; + + @Override + public void onBufferNearFull(String bufferType) { + this.triggered = true; + this.bufferType = bufferType; + } + + public boolean wasTriggered() { + return triggered; + } + + public String getBufferType() { + return bufferType; + } + + public void reset() { + triggered = false; + bufferType = null; + } + } + + private static class TestCapacityCallback implements EventBufferInterface.CapacityCallback { + private boolean triggered = false; + private double capacity = 0.0; + private String bufferType = null; + + @Override + public void onCapacityThresholdReached(double currentCapacity, String bufferType) { + this.triggered = true; + this.capacity = currentCapacity; + this.bufferType = bufferType; + } + + public boolean wasTriggered() { + return triggered; + } + + public double getCapacity() { + return capacity; + } + + public String getBufferType() { + return bufferType; + } + + public void reset() { + triggered = false; + capacity = 0.0; + bufferType = null; + } + } +} diff --git a/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/lifecycle/NRVideoLifecycleObserverTest.java b/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/lifecycle/NRVideoLifecycleObserverTest.java new file mode 100644 index 00000000..15db2ed8 --- /dev/null +++ b/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/lifecycle/NRVideoLifecycleObserverTest.java @@ -0,0 +1,428 @@ +package com.newrelic.videoagent.core.lifecycle; + +import android.app.Activity; +import android.os.Bundle; + +import com.newrelic.videoagent.core.harvest.HarvestComponentFactory; +import com.newrelic.videoagent.core.harvest.SchedulerInterface; +import com.newrelic.videoagent.core.NRVideoConfiguration; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.*; + +/** + * Unit tests for NRVideoLifecycleObserver. + * Tests app lifecycle management, background/foreground transitions, and crash handling. + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = 28, manifest = Config.NONE) +public class NRVideoLifecycleObserverTest { + + @Mock + private HarvestComponentFactory mockFactory; + + @Mock + private SchedulerInterface mockScheduler; + + @Mock + private NRVideoConfiguration mockConfiguration; + + @Mock + private Activity mockActivity; + + @Mock + private Bundle mockBundle; + + private NRVideoLifecycleObserver observer; + + @Before + public void setUp() { + MockitoAnnotations.openMocks(this); + + when(mockFactory.getScheduler()).thenReturn(mockScheduler); + when(mockFactory.getConfiguration()).thenReturn(mockConfiguration); + when(mockFactory.getRecoveryStats()).thenReturn("Recovery stats"); + when(mockConfiguration.isTV()).thenReturn(false); // Default to mobile + + observer = new NRVideoLifecycleObserver(mockFactory); + } + + @Test + public void testConstructorInitialization() { + verify(mockFactory).getConfiguration(); + verify(mockConfiguration).isTV(); + } + + @Test + public void testConstructorWithTVConfiguration() { + when(mockConfiguration.isTV()).thenReturn(true); + + NRVideoLifecycleObserver tvObserver = new NRVideoLifecycleObserver(mockFactory); + + verify(mockConfiguration, atLeastOnce()).isTV(); + } + + @Test + public void testOnActivityStarted_TransitionsToForeground() { + // First, put app in background + observer.onActivityStarted(mockActivity); + observer.onActivityStopped(mockActivity); + + // Now start again - this should trigger foreground transition + observer.onActivityStarted(mockActivity); + + verify(mockScheduler, atLeastOnce()).resume(anyBoolean()); + verify(mockFactory, atLeastOnce()).getRecoveryStats(); + } + + @Test + public void testOnActivityStarted_MultipleActivities() { + // Start first activity (count = 1, no foreground transition yet) + observer.onActivityStarted(mockActivity); + + // Second activity starts (count = 2, still no transition) + Activity mockActivity2 = mock(Activity.class); + observer.onActivityStarted(mockActivity2); + + // Scheduler resume should not be called yet (app never went to background) + verify(mockScheduler, never()).resume(anyBoolean()); + } + + @Test + public void testOnActivityStopped_TransitionsToBackground() { + // Start an activity (app starts in foreground, so no foreground transition occurs) + observer.onActivityStarted(mockActivity); + + // Verify that resume wasn't called (app was already in foreground state) + verify(mockScheduler, never()).resume(anyBoolean()); + + // Stop the activity to transition to background + observer.onActivityStopped(mockActivity); + + // Verify background handling occurred + verify(mockFactory).performEmergencyBackup(); + verify(mockScheduler).pause(); + verify(mockScheduler).resume(anyBoolean()); // Called in handleAppBackgrounded + } + + @Test + public void testOnActivityStopped_MultipleActivities() { + // Start two activities + observer.onActivityStarted(mockActivity); + Activity mockActivity2 = mock(Activity.class); + observer.onActivityStarted(mockActivity2); + + // Stop first activity - should not trigger background yet + observer.onActivityStopped(mockActivity); + verify(mockFactory, never()).performEmergencyBackup(); + + // Stop second activity - now should trigger background + observer.onActivityStopped(mockActivity2); + verify(mockFactory).performEmergencyBackup(); + verify(mockScheduler).pause(); + } + + @Test + public void testBackgroundTransition_MobileDevice() { + when(mockConfiguration.isTV()).thenReturn(false); + NRVideoLifecycleObserver mobileObserver = new NRVideoLifecycleObserver(mockFactory); + + mobileObserver.onActivityStarted(mockActivity); + mobileObserver.onActivityStopped(mockActivity); + + verify(mockFactory).performEmergencyBackup(); + verify(mockScheduler).pause(); + verify(mockScheduler, atLeast(1)).resume(anyBoolean()); + } + + @Test + public void testBackgroundTransition_TVDevice() { + when(mockConfiguration.isTV()).thenReturn(true); + NRVideoLifecycleObserver tvObserver = new NRVideoLifecycleObserver(mockFactory); + + tvObserver.onActivityStarted(mockActivity); + tvObserver.onActivityStopped(mockActivity); + + verify(mockFactory).performEmergencyBackup(); + verify(mockScheduler).pause(); + verify(mockScheduler, atLeast(1)).resume(anyBoolean()); + } + + @Test + public void testForegroundTransition() { + // Put app in background first + observer.onActivityStarted(mockActivity); + observer.onActivityStopped(mockActivity); + + // Now transition to foreground + observer.onActivityStarted(mockActivity); + + verify(mockScheduler, atLeastOnce()).resume(anyBoolean()); + verify(mockFactory, atLeastOnce()).getRecoveryStats(); + } + + @Test + public void testEmergencyBackupOnBackground() { + observer.onActivityStarted(mockActivity); + observer.onActivityStopped(mockActivity); + + verify(mockFactory).performEmergencyBackup(); + } + + @Test + public void testOnActivitySaveInstanceState_TriggersEmergencyBackup() { + observer.onActivitySaveInstanceState(mockActivity, mockBundle); + + verify(mockFactory).performEmergencyBackup(); + } + + @Test + public void testOnActivityDestroyed_WithNoActiveActivities() { + observer.onActivityDestroyed(mockActivity); + + verify(mockFactory).performEmergencyBackup(); + verify(mockFactory).cleanup(); + } + + @Test + public void testOnActivityDestroyed_WithActiveActivities() { + // Start an activity first + observer.onActivityStarted(mockActivity); + + // Destroy it but keep another one active + Activity mockActivity2 = mock(Activity.class); + observer.onActivityStarted(mockActivity2); + + observer.onActivityDestroyed(mockActivity); + + // Should not cleanup since there's still an active activity + verify(mockFactory, never()).cleanup(); + } + + @Test + public void testOnActivityCreated_NoAction() { + observer.onActivityCreated(mockActivity, mockBundle); + + // onActivityCreated does nothing - no additional interactions beyond construction + // (Factory was already called during construction in setUp) + verifyNoMoreInteractions(mockScheduler); + } + + @Test + public void testOnActivityResumed_NoAction() { + observer.onActivityResumed(mockActivity); + + // No interactions should occur for this lifecycle event + verifyNoMoreInteractions(mockScheduler); + } + + @Test + public void testOnActivityPaused_NoAction() { + observer.onActivityPaused(mockActivity); + + // No interactions should occur for this lifecycle event + verifyNoMoreInteractions(mockScheduler); + } + + @Test + public void testMultipleBackgroundTransitions() { + // First transition + observer.onActivityStarted(mockActivity); + observer.onActivityStopped(mockActivity); + + // Second transition + observer.onActivityStarted(mockActivity); + observer.onActivityStopped(mockActivity); + + // Emergency backup should be called twice + verify(mockFactory, atLeast(2)).performEmergencyBackup(); + } + + @Test + public void testBackgroundWithException_HandlesGracefully() { + doThrow(new RuntimeException("Test exception")) + .when(mockFactory).performEmergencyBackup(); + + observer.onActivityStarted(mockActivity); + observer.onActivityStopped(mockActivity); + + // Should not throw exception + verify(mockFactory).performEmergencyBackup(); + } + + @Test + public void testForegroundWithException_HandlesGracefully() { + doThrow(new RuntimeException("Test exception")) + .when(mockScheduler).resume(anyBoolean()); + + // Put in background first, then foreground to trigger scheduler.resume + observer.onActivityStarted(mockActivity); + observer.onActivityStopped(mockActivity); + observer.onActivityStarted(mockActivity); + + // Should not throw exception + verify(mockScheduler, atLeastOnce()).resume(anyBoolean()); + } + + @Test + public void testCrashDetection_SetsUncaughtExceptionHandler() { + Thread.UncaughtExceptionHandler handler = Thread.getDefaultUncaughtExceptionHandler(); + + assertNotNull(handler); + // The handler is set during observer construction + } + + @Test + public void testUncaughtExceptionHandler_PerformsEmergencyBackup() { + Thread.UncaughtExceptionHandler originalHandler = Thread.getDefaultUncaughtExceptionHandler(); + + // Create a new observer which will set up crash detection + NRVideoLifecycleObserver testObserver = new NRVideoLifecycleObserver(mockFactory); + + Thread.UncaughtExceptionHandler newHandler = Thread.getDefaultUncaughtExceptionHandler(); + + assertNotNull(newHandler); + // The new handler should be different from the original + // (unless it's the same observer instance, which wraps the original) + } + + @Test + public void testActivityLifecycleSequence() { + // Typical lifecycle sequence + observer.onActivityCreated(mockActivity, mockBundle); + observer.onActivityStarted(mockActivity); + observer.onActivityResumed(mockActivity); + observer.onActivityPaused(mockActivity); + observer.onActivityStopped(mockActivity); + observer.onActivitySaveInstanceState(mockActivity, mockBundle); + observer.onActivityDestroyed(mockActivity); + + // Verify key operations occurred + // resume(anyBoolean()) is called in handleAppBackgrounded (line 67 in implementation) + verify(mockScheduler, atLeastOnce()).resume(anyBoolean()); // Called on background transition + verify(mockScheduler).pause(); // On stopped (background) + verify(mockFactory, atLeast(2)).performEmergencyBackup(); // On save state and destroy + verify(mockFactory).cleanup(); // On destroy + } + + @Test + public void testMultipleActivitiesLifecycle() { + Activity activity1 = mock(Activity.class); + Activity activity2 = mock(Activity.class); + + // Start activity 1 + observer.onActivityStarted(activity1); + // No foreground transition yet (app never was in background) + + // Start activity 2 (app already in foreground) + observer.onActivityStarted(activity2); + + // Stop activity 1 (app still in foreground due to activity 2) + observer.onActivityStopped(activity1); + verify(mockFactory, never()).performEmergencyBackup(); + + // Stop activity 2 (app goes to background) + observer.onActivityStopped(activity2); + verify(mockFactory).performEmergencyBackup(); + } + + @Test + public void testRapidBackgroundForegroundTransitions() { + // Simulate rapid transitions + for (int i = 0; i < 5; i++) { + observer.onActivityStarted(mockActivity); + observer.onActivityStopped(mockActivity); + } + + // Should handle all transitions + verify(mockScheduler, atLeast(5)).resume(anyBoolean()); + verify(mockScheduler, atLeast(5)).pause(); + verify(mockFactory, atLeast(5)).performEmergencyBackup(); + } + + @Test + public void testConfigurationChange() { + // Activity destroyed and recreated (configuration change) + observer.onActivityStarted(mockActivity); + observer.onActivitySaveInstanceState(mockActivity, mockBundle); + observer.onActivityStopped(mockActivity); + + Activity newActivity = mock(Activity.class); + observer.onActivityCreated(newActivity, mockBundle); + observer.onActivityStarted(newActivity); + + // Should save state and restore properly + verify(mockFactory, atLeast(1)).performEmergencyBackup(); + verify(mockScheduler, atLeast(2)).resume(anyBoolean()); + } + + @Test + public void testEmergencyBackup_NotCalledMultipleTimes_Concurrently() { + // Simulate multiple save instance state calls + observer.onActivitySaveInstanceState(mockActivity, mockBundle); + observer.onActivitySaveInstanceState(mockActivity, mockBundle); + observer.onActivitySaveInstanceState(mockActivity, mockBundle); + + // Emergency backup should still be called (but protected by AtomicBoolean) + verify(mockFactory, atLeast(1)).performEmergencyBackup(); + } + + @Test + public void testSchedulerPauseOnBackground() { + observer.onActivityStarted(mockActivity); + observer.onActivityStopped(mockActivity); + + verify(mockScheduler).pause(); + } + + @Test + public void testSchedulerResumeOnForeground() { + // Put in background first, then foreground + observer.onActivityStarted(mockActivity); + observer.onActivityStopped(mockActivity); + observer.onActivityStarted(mockActivity); + + verify(mockScheduler, atLeastOnce()).resume(anyBoolean()); + } + + @Test + public void testRecoveryStatsRetrievedOnForeground() { + // Put in background first, then foreground + observer.onActivityStarted(mockActivity); + observer.onActivityStopped(mockActivity); + observer.onActivityStarted(mockActivity); + + verify(mockFactory, atLeastOnce()).getRecoveryStats(); + } + + @Test + public void testCleanupCalledOnlyWhenNoActiveActivities() { + observer.onActivityStarted(mockActivity); + Activity activity2 = mock(Activity.class); + observer.onActivityStarted(activity2); + + // Destroy first activity + observer.onActivityStopped(mockActivity); + observer.onActivityDestroyed(mockActivity); + verify(mockFactory, never()).cleanup(); + + // Destroy second activity + observer.onActivityStopped(activity2); + observer.onActivityDestroyed(activity2); + verify(mockFactory).cleanup(); + } + + private void assertNotNull(Object object) { + if (object == null) { + throw new AssertionError("Expected non-null value"); + } + } +} diff --git a/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/model/NRChronoTest.java b/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/model/NRChronoTest.java new file mode 100644 index 00000000..1c1ac49f --- /dev/null +++ b/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/model/NRChronoTest.java @@ -0,0 +1,284 @@ +package com.newrelic.videoagent.core.model; + +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Unit tests for NRChrono. + */ +public class NRChronoTest { + + private NRChrono chrono; + + @Before + public void setUp() { + chrono = new NRChrono(); + } + + @Test + public void testConstructor() { + NRChrono newChrono = new NRChrono(); + + assertNotNull(newChrono); + assertEquals(0, newChrono.getDeltaTime()); + } + + @Test + public void testGetDeltaTimeBeforeStart() { + long delta = chrono.getDeltaTime(); + + assertEquals(0, delta); + } + + @Test + public void testGetDeltaTimeAfterStart() throws InterruptedException { + chrono.start(); + + Thread.sleep(10); + + long delta = chrono.getDeltaTime(); + + assertTrue(delta >= 10); + assertTrue(delta < 100); + } + + @Test + public void testGetDeltaTimeImmediatelyAfterStart() { + chrono.start(); + + long delta = chrono.getDeltaTime(); + + assertTrue(delta >= 0); + assertTrue(delta < 10); + } + + @Test + public void testGetDeltaTimeIncreasesOverTime() throws InterruptedException { + chrono.start(); + + Thread.sleep(10); + long firstDelta = chrono.getDeltaTime(); + + Thread.sleep(10); + long secondDelta = chrono.getDeltaTime(); + + assertTrue(secondDelta > firstDelta); + assertTrue(secondDelta - firstDelta >= 10); + } + + @Test + public void testStartResetsTimer() throws InterruptedException { + chrono.start(); + Thread.sleep(20); + + long firstDelta = chrono.getDeltaTime(); + + chrono.start(); + Thread.sleep(5); + + long secondDelta = chrono.getDeltaTime(); + + assertTrue(firstDelta >= 20); + assertTrue(secondDelta < firstDelta); + assertTrue(secondDelta >= 5); + } + + @Test + public void testMultipleStartCalls() throws InterruptedException { + chrono.start(); + Thread.sleep(10); + + chrono.start(); + Thread.sleep(10); + + chrono.start(); + Thread.sleep(10); + + long delta = chrono.getDeltaTime(); + + assertTrue(delta >= 10); + assertTrue(delta < 20); + } + + @Test + public void testGetDeltaTimeIsNonDestructive() throws InterruptedException { + chrono.start(); + Thread.sleep(10); + + long delta1 = chrono.getDeltaTime(); + long delta2 = chrono.getDeltaTime(); + long delta3 = chrono.getDeltaTime(); + + assertTrue(delta1 >= 10); + assertTrue(delta2 >= delta1); + assertTrue(delta3 >= delta2); + } + + @Test + public void testMultipleInstancesIndependent() throws InterruptedException { + NRChrono chrono1 = new NRChrono(); + NRChrono chrono2 = new NRChrono(); + + chrono1.start(); + Thread.sleep(10); + chrono2.start(); + Thread.sleep(10); + + long delta1 = chrono1.getDeltaTime(); + long delta2 = chrono2.getDeltaTime(); + + assertTrue(delta1 >= 20); + assertTrue(delta2 >= 10); + assertTrue(delta1 > delta2); + } + + @Test + public void testStartWithoutGetDeltaTime() throws InterruptedException { + chrono.start(); + Thread.sleep(20); + chrono.start(); + + long delta = chrono.getDeltaTime(); + + assertTrue(delta >= 0); + assertTrue(delta < 10); + } + + @Test + public void testGetDeltaTimeAfterLongDelay() throws InterruptedException { + chrono.start(); + + Thread.sleep(100); + + long delta = chrono.getDeltaTime(); + + assertTrue(delta >= 100); + assertTrue(delta < 150); + } + + @Test + public void testGetDeltaTimeWithVeryShortDelay() throws InterruptedException { + chrono.start(); + + Thread.sleep(1); + + long delta = chrono.getDeltaTime(); + + assertTrue(delta >= 0); + assertTrue(delta < 10); + } + + @Test + public void testStartCanBeCalledMultipleTimes() { + chrono.start(); + chrono.start(); + chrono.start(); + + long delta = chrono.getDeltaTime(); + + assertTrue(delta >= 0); + } + + @Test + public void testGetDeltaTimeConsistencyAcrossCalls() throws InterruptedException { + chrono.start(); + Thread.sleep(50); + + long delta1 = chrono.getDeltaTime(); + Thread.sleep(1); + long delta2 = chrono.getDeltaTime(); + + assertTrue(delta2 > delta1); + assertTrue(delta2 - delta1 < 10); + } + + @Test + public void testChronoForMeasuringElapsedTime() throws InterruptedException { + chrono.start(); + + Thread.sleep(15); + + long elapsed = chrono.getDeltaTime(); + + assertTrue("Elapsed time should be at least 15ms", elapsed >= 15); + assertTrue("Elapsed time should be less than 50ms", elapsed < 50); + } + + @Test + public void testChronoForMultipleMeasurements() throws InterruptedException { + chrono.start(); + + Thread.sleep(10); + long measure1 = chrono.getDeltaTime(); + + Thread.sleep(10); + long measure2 = chrono.getDeltaTime(); + + Thread.sleep(10); + long measure3 = chrono.getDeltaTime(); + + assertTrue(measure1 >= 10); + assertTrue(measure2 >= 20); + assertTrue(measure3 >= 30); + assertTrue(measure3 > measure2); + assertTrue(measure2 > measure1); + } + + @Test + public void testDeltaTimeIncrementsLinearly() throws InterruptedException { + chrono.start(); + + Thread.sleep(10); + long time1 = chrono.getDeltaTime(); + + Thread.sleep(10); + long time2 = chrono.getDeltaTime(); + + long increment = time2 - time1; + assertTrue(increment >= 10); + assertTrue(increment < 20); + } + + @Test + public void testChronoResetBehavior() throws InterruptedException { + chrono.start(); + Thread.sleep(30); + long firstRun = chrono.getDeltaTime(); + + chrono.start(); + Thread.sleep(10); + long secondRun = chrono.getDeltaTime(); + + assertTrue(firstRun >= 30); + assertTrue(secondRun >= 10); + assertTrue(secondRun < firstRun); + } + + @Test + public void testGetDeltaTimeReturnsZeroAfterConstruction() { + NRChrono newChrono = new NRChrono(); + + assertEquals(0, newChrono.getDeltaTime()); + assertEquals(0, newChrono.getDeltaTime()); + assertEquals(0, newChrono.getDeltaTime()); + } + + @Test + public void testChronoForTimingOperations() throws InterruptedException { + chrono.start(); + + int sum = 0; + for (int i = 0; i < 1000; i++) { + sum += i; + } + + Thread.sleep(5); + + long operationTime = chrono.getDeltaTime(); + + assertTrue(operationTime >= 5); + assertTrue(sum > 0); + } +} diff --git a/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/model/NREventAttributesTest.java b/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/model/NREventAttributesTest.java new file mode 100644 index 00000000..2f75c6db --- /dev/null +++ b/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/model/NREventAttributesTest.java @@ -0,0 +1,192 @@ +package com.newrelic.videoagent.core.model; + +import org.junit.Before; +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.*; + +/** + * Unit tests for NREventAttributes. + */ +public class NREventAttributesTest { + + private NREventAttributes eventAttributes; + + @Before + public void setUp() { + eventAttributes = new NREventAttributes(); + } + + @Test + public void testSetAttributeWithoutFilter() { + eventAttributes.setAttribute("testKey", "testValue", null); + + Map result = eventAttributes.generateAttributes("REQUEST", null); + + assertEquals("testValue", result.get("testKey")); + } + + @Test + public void testSetAttributeWithSpecificFilter() { + eventAttributes.setAttribute("customAttr", "customValue", "VIDEO_START"); + + Map matchResult = eventAttributes.generateAttributes("VIDEO_START", null); + assertEquals("customValue", matchResult.get("customAttr")); + + Map noMatchResult = eventAttributes.generateAttributes("VIDEO_END", null); + assertNull(noMatchResult.get("customAttr")); + } + + @Test + public void testSetAttributeWithRegexFilter() { + eventAttributes.setAttribute("videoAttr", "value1", "VIDEO_.*"); + + Map startResult = eventAttributes.generateAttributes("VIDEO_START", null); + assertEquals("value1", startResult.get("videoAttr")); + + Map endResult = eventAttributes.generateAttributes("VIDEO_END", null); + assertEquals("value1", endResult.get("videoAttr")); + + Map noMatchResult = eventAttributes.generateAttributes("AUDIO_START", null); + assertNull(noMatchResult.get("videoAttr")); + } + + @Test + public void testMultipleAttributesWithDifferentFilters() { + eventAttributes.setAttribute("global", "globalValue", null); + eventAttributes.setAttribute("videoOnly", "videoValue", "VIDEO_.*"); + eventAttributes.setAttribute("specific", "specificValue", "VIDEO_START"); + + Map videoStartResult = eventAttributes.generateAttributes("VIDEO_START", null); + assertEquals("globalValue", videoStartResult.get("global")); + assertEquals("videoValue", videoStartResult.get("videoOnly")); + assertEquals("specificValue", videoStartResult.get("specific")); + + Map videoEndResult = eventAttributes.generateAttributes("VIDEO_END", null); + assertEquals("globalValue", videoEndResult.get("global")); + assertEquals("videoValue", videoEndResult.get("videoOnly")); + assertNull(videoEndResult.get("specific")); + } + + @Test + public void testAttributeOverwriting() { + eventAttributes.setAttribute("key", "value1", "VIDEO_.*"); + eventAttributes.setAttribute("key", "value2", "VIDEO_START"); + + Map result = eventAttributes.generateAttributes("VIDEO_START", null); + assertEquals("value2", result.get("key")); + } + + @Test + public void testGenerateAttributesWithExistingAttributes() { + eventAttributes.setAttribute("attr1", "value1", null); + + Map existingAttrs = new HashMap<>(); + existingAttrs.put("attr2", "value2"); + existingAttrs.put("attr3", "value3"); + + Map result = eventAttributes.generateAttributes("REQUEST", existingAttrs); + + assertEquals("value1", result.get("attr1")); + assertEquals("value2", result.get("attr2")); + assertEquals("value3", result.get("attr3")); + assertEquals(3, result.size()); + } + + @Test + public void testGenerateAttributesWithNullExistingAttributes() { + eventAttributes.setAttribute("key", "value", null); + + Map result = eventAttributes.generateAttributes("REQUEST", null); + + assertNotNull(result); + assertEquals("value", result.get("key")); + } + + @Test + public void testGenerateAttributesWithEmptyExistingAttributes() { + eventAttributes.setAttribute("key", "value", null); + + Map result = eventAttributes.generateAttributes("REQUEST", new HashMap<>()); + + assertNotNull(result); + assertEquals("value", result.get("key")); + } + + @Test + public void testMultipleBucketsWithSameKey() { + eventAttributes.setAttribute("key", "defaultValue", null); + eventAttributes.setAttribute("key", "videoValue", "VIDEO_.*"); + + Map videoResult = eventAttributes.generateAttributes("VIDEO_START", null); + // Both filters match VIDEO_START, but HashMap iteration order determines which wins + assertNotNull(videoResult.get("key")); + assertTrue(videoResult.get("key").equals("defaultValue") || videoResult.get("key").equals("videoValue")); + + Map otherResult = eventAttributes.generateAttributes("AUDIO_START", null); + assertEquals("defaultValue", otherResult.get("key")); + } + + @Test + public void testSetAttributeWithDifferentValueTypes() { + eventAttributes.setAttribute("stringAttr", "stringValue", null); + eventAttributes.setAttribute("intAttr", 42, null); + eventAttributes.setAttribute("boolAttr", true, null); + eventAttributes.setAttribute("doubleAttr", 3.14, null); + + Map result = eventAttributes.generateAttributes("REQUEST", null); + + assertEquals("stringValue", result.get("stringAttr")); + assertEquals(42, result.get("intAttr")); + assertEquals(true, result.get("boolAttr")); + assertEquals(3.14, result.get("doubleAttr")); + } + + @Test + public void testToString() { + eventAttributes.setAttribute("key1", "value1", null); + eventAttributes.setAttribute("key2", "value2", "VIDEO_START"); + + String result = eventAttributes.toString(); + + assertNotNull(result); + assertTrue(result.contains("NREventAttributes")); + } + + @Test + public void testComplexRegexPatterns() { + eventAttributes.setAttribute("errorAttr", "errorValue", "(VIDEO|AUDIO)_ERROR"); + + Map videoErrorResult = eventAttributes.generateAttributes("VIDEO_ERROR", null); + assertEquals("errorValue", videoErrorResult.get("errorAttr")); + + Map audioErrorResult = eventAttributes.generateAttributes("AUDIO_ERROR", null); + assertEquals("errorValue", audioErrorResult.get("errorAttr")); + + Map noMatchResult = eventAttributes.generateAttributes("VIDEO_START", null); + assertNull(noMatchResult.get("errorAttr")); + } + + @Test + public void testAttributePrecedenceWithMultipleFilters() { + eventAttributes.setAttribute("attr", "general", ".*"); + eventAttributes.setAttribute("attr", "video", "VIDEO_.*"); + eventAttributes.setAttribute("attr", "specific", "VIDEO_START"); + + Map result = eventAttributes.generateAttributes("VIDEO_START", null); + + assertTrue(result.containsKey("attr")); + assertNotNull(result.get("attr")); + } + + @Test + public void testEmptyAttributesGeneration() { + Map result = eventAttributes.generateAttributes("VIDEO_START", null); + + assertNotNull(result); + assertTrue(result.isEmpty()); + } +} diff --git a/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/model/NRTimeSinceTableTest.java b/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/model/NRTimeSinceTableTest.java new file mode 100644 index 00000000..f6b4f34a --- /dev/null +++ b/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/model/NRTimeSinceTableTest.java @@ -0,0 +1,221 @@ +package com.newrelic.videoagent.core.model; + +import org.junit.Before; +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.*; + +/** + * Unit tests for NRTimeSinceTable. + */ +public class NRTimeSinceTableTest { + + private NRTimeSinceTable timeSinceTable; + + @Before + public void setUp() { + timeSinceTable = new NRTimeSinceTable(); + } + + @Test + public void testAddEntryWith() { + timeSinceTable.addEntryWith("VIDEO_START", "timeSinceVideoStart", "VIDEO_.*"); + + Map attributes = new HashMap<>(); + timeSinceTable.applyAttributes("VIDEO_START", attributes); + + assertNotNull(attributes); + } + + @Test + public void testAddEntryWithModel() { + NRTimeSince ts = new NRTimeSince("VIDEO_START", "timeSinceVideoStart", "VIDEO_.*"); + timeSinceTable.addEntry(ts); + + Map attributes = new HashMap<>(); + timeSinceTable.applyAttributes("VIDEO_START", attributes); + + assertNotNull(attributes); + } + + @Test + public void testApplyAttributesWithMatchingAction() throws InterruptedException { + timeSinceTable.addEntryWith("VIDEO_START", "timeSinceVideoStart", "VIDEO_END"); + + Map startAttributes = new HashMap<>(); + timeSinceTable.applyAttributes("VIDEO_START", startAttributes); + + Thread.sleep(10); + + Map endAttributes = new HashMap<>(); + timeSinceTable.applyAttributes("VIDEO_END", endAttributes); + + assertTrue(endAttributes.containsKey("timeSinceVideoStart")); + assertNotNull(endAttributes.get("timeSinceVideoStart")); + } + + @Test + public void testApplyAttributesWithRegexFilter() throws InterruptedException { + timeSinceTable.addEntryWith("VIDEO_START", "timeSinceVideoStart", "VIDEO_(END|PAUSE)"); + + Map startAttributes = new HashMap<>(); + timeSinceTable.applyAttributes("VIDEO_START", startAttributes); + + Thread.sleep(10); + + Map endAttributes = new HashMap<>(); + timeSinceTable.applyAttributes("VIDEO_END", endAttributes); + + assertTrue(endAttributes.containsKey("timeSinceVideoStart")); + + Map pauseAttributes = new HashMap<>(); + timeSinceTable.applyAttributes("VIDEO_PAUSE", pauseAttributes); + + assertTrue(pauseAttributes.containsKey("timeSinceVideoStart")); + } + + @Test + public void testApplyAttributesWithNoMatch() { + timeSinceTable.addEntryWith("VIDEO_START", "timeSinceVideoStart", "VIDEO_END"); + + Map attributes = new HashMap<>(); + timeSinceTable.applyAttributes("AUDIO_START", attributes); + + assertFalse(attributes.containsKey("timeSinceVideoStart")); + } + + @Test + public void testMultipleEntriesInTable() throws InterruptedException { + timeSinceTable.addEntryWith("VIDEO_START", "timeSinceVideoStart", "VIDEO_END"); + timeSinceTable.addEntryWith("CONTENT_START", "timeSinceContentStart", "CONTENT_END"); + + Map videoStartAttrs = new HashMap<>(); + timeSinceTable.applyAttributes("VIDEO_START", videoStartAttrs); + + Map contentStartAttrs = new HashMap<>(); + timeSinceTable.applyAttributes("CONTENT_START", contentStartAttrs); + + Thread.sleep(10); + + Map videoEndAttrs = new HashMap<>(); + timeSinceTable.applyAttributes("VIDEO_END", videoEndAttrs); + + assertTrue(videoEndAttrs.containsKey("timeSinceVideoStart")); + assertFalse(videoEndAttrs.containsKey("timeSinceContentStart")); + + Map contentEndAttrs = new HashMap<>(); + timeSinceTable.applyAttributes("CONTENT_END", contentEndAttrs); + + assertTrue(contentEndAttrs.containsKey("timeSinceContentStart")); + } + + @Test + public void testTimeSinceValueIsNumeric() throws InterruptedException { + timeSinceTable.addEntryWith("VIDEO_START", "timeSinceVideoStart", "VIDEO_END"); + + timeSinceTable.applyAttributes("VIDEO_START", new HashMap<>()); + + Thread.sleep(10); + + Map endAttributes = new HashMap<>(); + timeSinceTable.applyAttributes("VIDEO_END", endAttributes); + + Object timeSinceValue = endAttributes.get("timeSinceVideoStart"); + assertNotNull(timeSinceValue); + assertTrue(timeSinceValue instanceof Number); + assertTrue(((Number) timeSinceValue).longValue() >= 0); + } + + @Test + public void testApplyAttributesUpdatesExistingAttributes() throws InterruptedException { + timeSinceTable.addEntryWith("VIDEO_START", "timeSinceVideoStart", "VIDEO_END"); + + timeSinceTable.applyAttributes("VIDEO_START", new HashMap<>()); + + Thread.sleep(10); + + Map attributes = new HashMap<>(); + attributes.put("existingKey", "existingValue"); + timeSinceTable.applyAttributes("VIDEO_END", attributes); + + assertEquals("existingValue", attributes.get("existingKey")); + assertTrue(attributes.containsKey("timeSinceVideoStart")); + assertEquals(2, attributes.size()); + } + + @Test + public void testApplyAttributesWithEmptyTable() { + Map attributes = new HashMap<>(); + attributes.put("key", "value"); + + timeSinceTable.applyAttributes("VIDEO_START", attributes); + + assertEquals(1, attributes.size()); + assertEquals("value", attributes.get("key")); + } + + @Test + public void testMultipleApplyAttributesCallsForSameAction() throws InterruptedException { + timeSinceTable.addEntryWith("VIDEO_START", "timeSinceVideoStart", "VIDEO_END"); + + timeSinceTable.applyAttributes("VIDEO_START", new HashMap<>()); + + Thread.sleep(10); + + Map firstCall = new HashMap<>(); + timeSinceTable.applyAttributes("VIDEO_END", firstCall); + long firstValue = ((Number) firstCall.get("timeSinceVideoStart")).longValue(); + + Thread.sleep(10); + + Map secondCall = new HashMap<>(); + timeSinceTable.applyAttributes("VIDEO_END", secondCall); + long secondValue = ((Number) secondCall.get("timeSinceVideoStart")).longValue(); + + assertTrue(secondValue >= firstValue); + } + + @Test + public void testActionTriggersTimestampUpdate() throws InterruptedException { + timeSinceTable.addEntryWith("VIDEO_START", "timeSinceStart", "VIDEO_PAUSE"); + + timeSinceTable.applyAttributes("VIDEO_START", new HashMap<>()); + Thread.sleep(10); + + Map firstPause = new HashMap<>(); + timeSinceTable.applyAttributes("VIDEO_PAUSE", firstPause); + long firstPauseTime = ((Number) firstPause.get("timeSinceStart")).longValue(); + + timeSinceTable.applyAttributes("VIDEO_START", new HashMap<>()); + Thread.sleep(10); + + Map secondPause = new HashMap<>(); + timeSinceTable.applyAttributes("VIDEO_PAUSE", secondPause); + long secondPauseTime = ((Number) secondPause.get("timeSinceStart")).longValue(); + + assertTrue(secondPauseTime < firstPauseTime + 20); + } + + @Test + public void testWildcardFilterMatchesMultipleActions() throws InterruptedException { + timeSinceTable.addEntryWith("VIDEO_START", "timeSinceVideoStart", ".*"); + + timeSinceTable.applyAttributes("VIDEO_START", new HashMap<>()); + Thread.sleep(10); + + Map endAttrs = new HashMap<>(); + timeSinceTable.applyAttributes("VIDEO_END", endAttrs); + assertTrue(endAttrs.containsKey("timeSinceVideoStart")); + + Map pauseAttrs = new HashMap<>(); + timeSinceTable.applyAttributes("VIDEO_PAUSE", pauseAttrs); + assertTrue(pauseAttrs.containsKey("timeSinceVideoStart")); + + Map audioAttrs = new HashMap<>(); + timeSinceTable.applyAttributes("AUDIO_START", audioAttrs); + assertTrue(audioAttrs.containsKey("timeSinceVideoStart")); + } +} diff --git a/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/model/NRTimeSinceTest.java b/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/model/NRTimeSinceTest.java new file mode 100644 index 00000000..ceda3c90 --- /dev/null +++ b/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/model/NRTimeSinceTest.java @@ -0,0 +1,257 @@ +package com.newrelic.videoagent.core.model; + +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Unit tests for NRTimeSince. + */ +public class NRTimeSinceTest { + + private NRTimeSince timeSince; + + @Before + public void setUp() { + timeSince = new NRTimeSince("VIDEO_START", "timeSinceVideoStart", "VIDEO_.*"); + } + + @Test + public void testConstructor() { + NRTimeSince ts = new NRTimeSince("ACTION", "attrName", "FILTER"); + + assertNotNull(ts); + assertEquals("attrName", ts.getAttribute()); + } + + @Test + public void testIsActionWithMatchingAction() { + assertTrue(timeSince.isAction("VIDEO_START")); + } + + @Test + public void testIsActionWithNonMatchingAction() { + assertFalse(timeSince.isAction("VIDEO_END")); + assertFalse(timeSince.isAction("VIDEO_PAUSE")); + assertFalse(timeSince.isAction("AUDIO_START")); + } + + @Test + public void testIsActionWithEmptyString() { + assertFalse(timeSince.isAction("")); + } + + @Test + public void testIsActionWithNull() { + assertFalse(timeSince.isAction(null)); + } + + @Test + public void testIsMatchWithMatchingPattern() { + assertTrue(timeSince.isMatch("VIDEO_START")); + assertTrue(timeSince.isMatch("VIDEO_END")); + assertTrue(timeSince.isMatch("VIDEO_PAUSE")); + assertTrue(timeSince.isMatch("VIDEO_ERROR")); + } + + @Test + public void testIsMatchWithNonMatchingPattern() { + assertFalse(timeSince.isMatch("AUDIO_START")); + assertFalse(timeSince.isMatch("CONTENT_START")); + assertFalse(timeSince.isMatch("PLAYER_READY")); + } + + @Test + public void testIsMatchWithExactFilter() { + NRTimeSince exactFilter = new NRTimeSince("VIDEO_START", "attr", "VIDEO_END"); + + assertTrue(exactFilter.isMatch("VIDEO_END")); + assertFalse(exactFilter.isMatch("VIDEO_START")); + assertFalse(exactFilter.isMatch("VIDEO_PAUSE")); + } + + @Test + public void testIsMatchWithComplexRegex() { + NRTimeSince complexRegex = new NRTimeSince("START", "attr", "(VIDEO|AUDIO)_(START|END)"); + + assertTrue(complexRegex.isMatch("VIDEO_START")); + assertTrue(complexRegex.isMatch("VIDEO_END")); + assertTrue(complexRegex.isMatch("AUDIO_START")); + assertTrue(complexRegex.isMatch("AUDIO_END")); + assertFalse(complexRegex.isMatch("VIDEO_PAUSE")); + assertFalse(complexRegex.isMatch("CONTENT_START")); + } + + @Test + public void testIsMatchWithWildcard() { + NRTimeSince wildcard = new NRTimeSince("START", "attr", ".*"); + + assertTrue(wildcard.isMatch("VIDEO_START")); + assertTrue(wildcard.isMatch("AUDIO_START")); + assertTrue(wildcard.isMatch("ANY_ACTION")); + assertTrue(wildcard.isMatch("")); + } + + @Test + public void testTimeSinceBeforeNow() { + Long result = timeSince.timeSince(); + + assertNull(result); + } + + @Test + public void testTimeSinceAfterNow() throws InterruptedException { + timeSince.now(); + + Thread.sleep(10); + + Long result = timeSince.timeSince(); + + assertNotNull(result); + assertTrue(result >= 10); + assertTrue(result < 100); + } + + @Test + public void testTimeSinceImmediatelyAfterNow() { + timeSince.now(); + + Long result = timeSince.timeSince(); + + assertNotNull(result); + assertTrue(result >= 0); + } + + @Test + public void testTimeSinceIncreasesOverTime() throws InterruptedException { + timeSince.now(); + + Thread.sleep(10); + Long first = timeSince.timeSince(); + + Thread.sleep(10); + Long second = timeSince.timeSince(); + + assertNotNull(first); + assertNotNull(second); + assertTrue(second > first); + } + + @Test + public void testNowResetsTimestamp() throws InterruptedException { + timeSince.now(); + Thread.sleep(20); + + Long firstTime = timeSince.timeSince(); + + timeSince.now(); + Thread.sleep(5); + + Long secondTime = timeSince.timeSince(); + + assertNotNull(firstTime); + assertNotNull(secondTime); + assertTrue(secondTime < firstTime); + } + + @Test + public void testMultipleNowCalls() throws InterruptedException { + timeSince.now(); + Thread.sleep(10); + timeSince.now(); + Thread.sleep(10); + timeSince.now(); + + Long result = timeSince.timeSince(); + + assertNotNull(result); + assertTrue(result >= 0); + assertTrue(result < 20); + } + + @Test + public void testGetAttribute() { + assertEquals("timeSinceVideoStart", timeSince.getAttribute()); + } + + @Test + public void testGetAttributeWithDifferentNames() { + NRTimeSince ts1 = new NRTimeSince("A", "attr1", ".*"); + NRTimeSince ts2 = new NRTimeSince("B", "attr2", ".*"); + NRTimeSince ts3 = new NRTimeSince("C", "timeSinceLoad", ".*"); + + assertEquals("attr1", ts1.getAttribute()); + assertEquals("attr2", ts2.getAttribute()); + assertEquals("timeSinceLoad", ts3.getAttribute()); + } + + @Test + public void testIsActionCaseSensitive() { + assertTrue(timeSince.isAction("VIDEO_START")); + assertFalse(timeSince.isAction("video_start")); + assertFalse(timeSince.isAction("Video_Start")); + } + + @Test + public void testIsMatchCaseSensitive() { + assertTrue(timeSince.isMatch("VIDEO_START")); + assertFalse(timeSince.isMatch("video_start")); + } + + @Test + public void testConstructorWithEmptyStrings() { + NRTimeSince ts = new NRTimeSince("", "", ""); + + assertNotNull(ts); + assertEquals("", ts.getAttribute()); + assertTrue(ts.isAction("")); + } + + @Test + public void testTimeSinceWithVeryShortDelay() throws InterruptedException { + timeSince.now(); + Thread.sleep(1); + + Long result = timeSince.timeSince(); + + assertNotNull(result); + assertTrue(result >= 0); + } + + @Test + public void testIsMatchWithSpecialRegexCharacters() { + NRTimeSince dotFilter = new NRTimeSince("A", "attr", "VIDEO.START"); + + assertTrue(dotFilter.isMatch("VIDEO_START")); + assertTrue(dotFilter.isMatch("VIDEOXSTART")); + assertFalse(dotFilter.isMatch("VIDEOSTART")); + } + + @Test + public void testIsMatchWithAnchoredPattern() { + NRTimeSince anchored = new NRTimeSince("A", "attr", "^VIDEO_START$"); + + assertTrue(anchored.isMatch("VIDEO_START")); + assertFalse(anchored.isMatch("PREFIX_VIDEO_START")); + assertFalse(anchored.isMatch("VIDEO_START_SUFFIX")); + } + + @Test + public void testMultipleInstancesIndependent() throws InterruptedException { + NRTimeSince ts1 = new NRTimeSince("VIDEO_START", "attr1", ".*"); + NRTimeSince ts2 = new NRTimeSince("VIDEO_END", "attr2", ".*"); + + ts1.now(); + Thread.sleep(10); + ts2.now(); + Thread.sleep(10); + + Long time1 = ts1.timeSince(); + Long time2 = ts2.timeSince(); + + assertNotNull(time1); + assertNotNull(time2); + assertTrue(time1 > time2); + } +} diff --git a/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/model/NRTrackerPairTest.java b/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/model/NRTrackerPairTest.java new file mode 100644 index 00000000..ce3f28e4 --- /dev/null +++ b/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/model/NRTrackerPairTest.java @@ -0,0 +1,148 @@ +package com.newrelic.videoagent.core.model; + +import com.newrelic.videoagent.core.tracker.NRTracker; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import static org.junit.Assert.*; + +/** + * Unit tests for NRTrackerPair. + */ +public class NRTrackerPairTest { + + @Mock + private NRTracker mockFirstTracker; + + @Mock + private NRTracker mockSecondTracker; + + @Before + public void setUp() { + MockitoAnnotations.openMocks(this); + } + + @Test + public void testConstructorInitializesTrackers() { + NRTrackerPair pair = new NRTrackerPair(mockFirstTracker, mockSecondTracker); + + assertNotNull(pair); + assertNotNull(pair.getFirst()); + assertNotNull(pair.getSecond()); + } + + @Test + public void testGetFirstReturnsCorrectTracker() { + NRTrackerPair pair = new NRTrackerPair(mockFirstTracker, mockSecondTracker); + + NRTracker result = pair.getFirst(); + + assertSame(mockFirstTracker, result); + } + + @Test + public void testGetSecondReturnsCorrectTracker() { + NRTrackerPair pair = new NRTrackerPair(mockFirstTracker, mockSecondTracker); + + NRTracker result = pair.getSecond(); + + assertSame(mockSecondTracker, result); + } + + @Test + public void testConstructorWithNullFirstTracker() { + NRTrackerPair pair = new NRTrackerPair(null, mockSecondTracker); + + assertNull(pair.getFirst()); + assertNotNull(pair.getSecond()); + assertSame(mockSecondTracker, pair.getSecond()); + } + + @Test + public void testConstructorWithNullSecondTracker() { + NRTrackerPair pair = new NRTrackerPair(mockFirstTracker, null); + + assertNotNull(pair.getFirst()); + assertNull(pair.getSecond()); + assertSame(mockFirstTracker, pair.getFirst()); + } + + @Test + public void testConstructorWithBothNullTrackers() { + NRTrackerPair pair = new NRTrackerPair(null, null); + + assertNull(pair.getFirst()); + assertNull(pair.getSecond()); + } + + @Test + public void testGetFirstIsConsistent() { + NRTrackerPair pair = new NRTrackerPair(mockFirstTracker, mockSecondTracker); + + NRTracker first1 = pair.getFirst(); + NRTracker first2 = pair.getFirst(); + + assertSame(first1, first2); + assertSame(mockFirstTracker, first1); + } + + @Test + public void testGetSecondIsConsistent() { + NRTrackerPair pair = new NRTrackerPair(mockFirstTracker, mockSecondTracker); + + NRTracker second1 = pair.getSecond(); + NRTracker second2 = pair.getSecond(); + + assertSame(second1, second2); + assertSame(mockSecondTracker, second1); + } + + @Test + public void testFirstAndSecondAreDifferent() { + NRTrackerPair pair = new NRTrackerPair(mockFirstTracker, mockSecondTracker); + + NRTracker first = pair.getFirst(); + NRTracker second = pair.getSecond(); + + assertNotSame(first, second); + } + + @Test + public void testConstructorWithSameTrackerInstance() { + NRTrackerPair pair = new NRTrackerPair(mockFirstTracker, mockFirstTracker); + + NRTracker first = pair.getFirst(); + NRTracker second = pair.getSecond(); + + assertSame(first, second); + assertSame(mockFirstTracker, first); + assertSame(mockFirstTracker, second); + } + + @Test + public void testMultiplePairInstances() { + NRTrackerPair pair1 = new NRTrackerPair(mockFirstTracker, mockSecondTracker); + NRTrackerPair pair2 = new NRTrackerPair(mockSecondTracker, mockFirstTracker); + + assertNotSame(pair1, pair2); + assertSame(pair1.getFirst(), pair2.getSecond()); + assertSame(pair1.getSecond(), pair2.getFirst()); + } + + @Test + public void testPairImmutability() { + NRTrackerPair pair = new NRTrackerPair(mockFirstTracker, mockSecondTracker); + + NRTracker firstBefore = pair.getFirst(); + NRTracker secondBefore = pair.getSecond(); + + // Get trackers multiple times to ensure they remain consistent + for (int i = 0; i < 10; i++) { + assertSame(firstBefore, pair.getFirst()); + assertSame(secondBefore, pair.getSecond()); + } + } +} diff --git a/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/model/NRTrackerStateTest.java b/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/model/NRTrackerStateTest.java new file mode 100644 index 00000000..72582bf7 --- /dev/null +++ b/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/model/NRTrackerStateTest.java @@ -0,0 +1,472 @@ +package com.newrelic.videoagent.core.model; + +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Unit tests for NRTrackerState. + */ +public class NRTrackerStateTest { + + private NRTrackerState state; + + @Before + public void setUp() { + state = new NRTrackerState(); + } + + @Test + public void testConstructorInitializesAllFieldsFalse() { + NRTrackerState newState = new NRTrackerState(); + + assertFalse(newState.isPlayerReady); + assertFalse(newState.isRequested); + assertFalse(newState.isStarted); + assertFalse(newState.isPlaying); + assertFalse(newState.isPaused); + assertFalse(newState.isSeeking); + assertFalse(newState.isBuffering); + assertFalse(newState.isAd); + assertFalse(newState.isAdBreak); + assertNotNull(newState.chrono); + assertEquals(Long.valueOf(0L), newState.accumulatedVideoWatchTime); + } + + @Test + public void testGoPlayerReadyFromInitialState() { + assertTrue(state.goPlayerReady()); + assertTrue(state.isPlayerReady); + } + + @Test + public void testGoPlayerReadyWhenAlreadyReady() { + state.goPlayerReady(); + + assertFalse(state.goPlayerReady()); + assertTrue(state.isPlayerReady); + } + + @Test + public void testGoRequestFromInitialState() { + assertTrue(state.goRequest()); + assertTrue(state.isRequested); + } + + @Test + public void testGoRequestWhenAlreadyRequested() { + state.goRequest(); + + assertFalse(state.goRequest()); + assertTrue(state.isRequested); + } + + @Test + public void testGoStartRequiresRequest() { + assertFalse(state.goStart()); + assertFalse(state.isStarted); + assertFalse(state.isPlaying); + } + + @Test + public void testGoStartAfterRequest() { + state.goRequest(); + + assertTrue(state.goStart()); + assertTrue(state.isStarted); + assertTrue(state.isPlaying); + } + + @Test + public void testGoStartWhenAlreadyStarted() { + state.goRequest(); + state.goStart(); + + assertFalse(state.goStart()); + } + + @Test + public void testGoEndFromInitialState() { + assertFalse(state.goEnd()); + } + + @Test + public void testGoEndAfterRequest() { + state.goRequest(); + + assertTrue(state.goEnd()); + assertFalse(state.isRequested); + assertFalse(state.isStarted); + assertFalse(state.isPlaying); + assertFalse(state.isPaused); + assertFalse(state.isSeeking); + assertFalse(state.isBuffering); + } + + @Test + public void testGoEndResetsMultipleStates() { + state.goRequest(); + state.goStart(); + state.goPause(); + + assertTrue(state.goEnd()); + assertFalse(state.isRequested); + assertFalse(state.isStarted); + assertFalse(state.isPlaying); + assertFalse(state.isPaused); + } + + @Test + public void testGoPauseRequiresStarted() { + assertFalse(state.goPause()); + assertFalse(state.isPaused); + } + + @Test + public void testGoPauseAfterStart() { + state.goRequest(); + state.goStart(); + + assertTrue(state.goPause()); + assertTrue(state.isPaused); + assertFalse(state.isPlaying); + } + + @Test + public void testGoPauseWhenAlreadyPaused() { + state.goRequest(); + state.goStart(); + state.goPause(); + + assertFalse(state.goPause()); + } + + @Test + public void testGoResumeRequiresPaused() { + assertFalse(state.goResume()); + } + + @Test + public void testGoResumeAfterPause() { + state.goRequest(); + state.goStart(); + state.goPause(); + + assertTrue(state.goResume()); + assertFalse(state.isPaused); + assertTrue(state.isPlaying); + } + + @Test + public void testGoResumeWhenNotPaused() { + state.goRequest(); + state.goStart(); + + assertFalse(state.goResume()); + } + + @Test + public void testGoBufferStartRequiresRequested() { + assertFalse(state.goBufferStart()); + assertFalse(state.isBuffering); + } + + @Test + public void testGoBufferStartAfterRequest() { + state.goRequest(); + + assertTrue(state.goBufferStart()); + assertTrue(state.isBuffering); + assertFalse(state.isPlaying); + } + + @Test + public void testGoBufferStartWhenAlreadyBuffering() { + state.goRequest(); + state.goBufferStart(); + + assertFalse(state.goBufferStart()); + } + + @Test + public void testGoBufferEndRequiresBuffering() { + assertFalse(state.goBufferEnd()); + } + + @Test + public void testGoBufferEndAfterBufferStart() { + state.goRequest(); + state.goBufferStart(); + + assertTrue(state.goBufferEnd()); + assertFalse(state.isBuffering); + assertTrue(state.isPlaying); + } + + @Test + public void testGoSeekStartRequiresStarted() { + assertFalse(state.goSeekStart()); + assertFalse(state.isSeeking); + } + + @Test + public void testGoSeekStartAfterStart() { + state.goRequest(); + state.goStart(); + + assertTrue(state.goSeekStart()); + assertTrue(state.isSeeking); + assertFalse(state.isPlaying); + } + + @Test + public void testGoSeekStartWhenAlreadySeeking() { + state.goRequest(); + state.goStart(); + state.goSeekStart(); + + assertFalse(state.goSeekStart()); + } + + @Test + public void testGoSeekEndRequiresSeeking() { + assertFalse(state.goSeekEnd()); + } + + @Test + public void testGoSeekEndAfterSeekStart() { + state.goRequest(); + state.goStart(); + state.goSeekStart(); + + assertTrue(state.goSeekEnd()); + assertFalse(state.isSeeking); + assertTrue(state.isPlaying); + } + + @Test + public void testGoAdBreakStartFromInitialState() { + assertTrue(state.goAdBreakStart()); + assertTrue(state.isAdBreak); + } + + @Test + public void testGoAdBreakStartWhenAlreadyInAdBreak() { + state.goAdBreakStart(); + + assertFalse(state.goAdBreakStart()); + } + + @Test + public void testGoAdBreakEndRequiresAdBreak() { + assertFalse(state.goAdBreakEnd()); + } + + @Test + public void testGoAdBreakEndAfterAdBreakStart() { + state.goAdBreakStart(); + + assertTrue(state.goAdBreakEnd()); + assertFalse(state.isAdBreak); + assertFalse(state.isRequested); + } + + @Test + public void testResetClearsAllStates() { + state.goPlayerReady(); + state.goRequest(); + state.goStart(); + + state.reset(); + + assertFalse(state.isPlayerReady); + assertFalse(state.isRequested); + assertFalse(state.isStarted); + assertFalse(state.isPlaying); + assertFalse(state.isPaused); + assertFalse(state.isSeeking); + assertFalse(state.isBuffering); + assertFalse(state.isAd); + assertFalse(state.isAdBreak); + assertNotNull(state.chrono); + assertEquals(Long.valueOf(0L), state.accumulatedVideoWatchTime); + } + + @Test + public void testCompleteVideoPlaybackFlow() { + assertTrue(state.goPlayerReady()); + assertTrue(state.goRequest()); + assertTrue(state.goStart()); + assertTrue(state.goPause()); + assertTrue(state.goResume()); + assertTrue(state.goEnd()); + } + + @Test + public void testBufferingDuringPlayback() { + state.goRequest(); + state.goStart(); + + assertTrue(state.goBufferStart()); + assertTrue(state.isBuffering); + assertFalse(state.isPlaying); + + assertTrue(state.goBufferEnd()); + assertFalse(state.isBuffering); + assertTrue(state.isPlaying); + } + + @Test + public void testSeekingDuringPlayback() { + state.goRequest(); + state.goStart(); + + assertTrue(state.goSeekStart()); + assertTrue(state.isSeeking); + assertFalse(state.isPlaying); + + assertTrue(state.goSeekEnd()); + assertFalse(state.isSeeking); + assertTrue(state.isPlaying); + } + + @Test + public void testAdBreakFlow() { + state.goPlayerReady(); + state.goRequest(); + + assertTrue(state.goAdBreakStart()); + assertTrue(state.isAdBreak); + + assertTrue(state.goAdBreakEnd()); + assertFalse(state.isAdBreak); + assertFalse(state.isRequested); + } + + @Test + public void testMultiplePauseResumeCycles() { + state.goRequest(); + state.goStart(); + + assertTrue(state.goPause()); + assertTrue(state.goResume()); + assertTrue(state.goPause()); + assertTrue(state.goResume()); + + assertTrue(state.isPlaying); + assertFalse(state.isPaused); + } + + @Test + public void testCannotStartWithoutRequest() { + state.goPlayerReady(); + + assertFalse(state.goStart()); + assertFalse(state.isStarted); + } + + @Test + public void testCannotPauseWithoutStart() { + state.goRequest(); + + assertFalse(state.goPause()); + assertFalse(state.isPaused); + } + + @Test + public void testCannotSeekWithoutStart() { + state.goRequest(); + + assertFalse(state.goSeekStart()); + assertFalse(state.isSeeking); + } + + @Test + public void testChronoIsInitializedOnConstruction() { + assertNotNull(state.chrono); + assertEquals(0, state.chrono.getDeltaTime()); + } + + @Test + public void testChronoIsResetOnReset() { + state.chrono.start(); + + state.reset(); + + assertNotNull(state.chrono); + assertEquals(0, state.chrono.getDeltaTime()); + } + + @Test + public void testAccumulatedVideoWatchTimeInitializedToZero() { + assertEquals(Long.valueOf(0L), state.accumulatedVideoWatchTime); + } + + @Test + public void testAccumulatedVideoWatchTimeResetToZero() { + state.accumulatedVideoWatchTime = 12345L; + + state.reset(); + + assertEquals(Long.valueOf(0L), state.accumulatedVideoWatchTime); + } + + @Test + public void testStateTransitionsAreIdempotent() { + state.goRequest(); + state.goStart(); + + assertTrue(state.isStarted); + assertFalse(state.goStart()); + assertTrue(state.isStarted); + } + + @Test + public void testEndClearsAllPlaybackStates() { + state.goRequest(); + state.goStart(); + state.goSeekStart(); + state.goBufferStart(); + + state.goEnd(); + + assertFalse(state.isRequested); + assertFalse(state.isStarted); + assertFalse(state.isPlaying); + assertFalse(state.isSeeking); + assertFalse(state.isBuffering); + } + + @Test + public void testMultipleResets() { + state.goPlayerReady(); + state.reset(); + state.goRequest(); + state.reset(); + state.goStart(); + state.reset(); + + assertFalse(state.isPlayerReady); + assertFalse(state.isRequested); + assertFalse(state.isStarted); + } + + @Test + public void testComplexPlaybackScenario() { + state.goPlayerReady(); + state.goRequest(); + state.goBufferStart(); + state.goBufferEnd(); + state.goStart(); + state.goSeekStart(); + state.goSeekEnd(); + state.goPause(); + state.goResume(); + state.goEnd(); + + assertFalse(state.isRequested); + assertFalse(state.isStarted); + assertFalse(state.isPlaying); + } +} diff --git a/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/storage/CrashSafeEventBufferTest.java b/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/storage/CrashSafeEventBufferTest.java new file mode 100644 index 00000000..be6ec007 --- /dev/null +++ b/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/storage/CrashSafeEventBufferTest.java @@ -0,0 +1,524 @@ +package com.newrelic.videoagent.core.storage; + +import android.content.Context; + +import com.newrelic.videoagent.core.NRVideoConfiguration; +import com.newrelic.videoagent.core.NRVideoConstants; +import com.newrelic.videoagent.core.harvest.EventBufferInterface; +import com.newrelic.videoagent.core.harvest.SizeEstimator; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.*; + +/** + * Unit tests for CrashSafeEventBuffer. + * Tests crash recovery, event buffering, and SQLite backup functionality. + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = 28, manifest = Config.NONE) +public class CrashSafeEventBufferTest { + + @Mock + private NRVideoConfiguration mockConfiguration; + + private Context context; + private VideoEventStorage storage; + private CrashSafeEventBuffer buffer; + + @Before + public void setUp() { + MockitoAnnotations.openMocks(this); + + context = RuntimeEnvironment.getApplication(); + storage = new VideoEventStorage(context); + + when(mockConfiguration.isTV()).thenReturn(false); + + buffer = new CrashSafeEventBuffer(context, mockConfiguration, storage); + + // Set up default callbacks to prevent NullPointerException + buffer.setOverflowCallback(new EventBufferInterface.OverflowCallback() { + @Override + public void onBufferNearFull(String bufferType) { + // Do nothing in tests + } + }); + + buffer.setCapacityCallback(new EventBufferInterface.CapacityCallback() { + @Override + public void onCapacityThresholdReached(double currentCapacity, String bufferType) { + // Do nothing in tests + } + }); + } + + @After + public void tearDown() { + buffer.cleanup(); + storage.close(); + context.deleteDatabase("nr_video_backup.db"); + context.getSharedPreferences("nr_video_crash_detection", Context.MODE_PRIVATE) + .edit().clear().apply(); + } + + @Test + public void testBufferInitialization() { + assertNotNull(buffer); + assertTrue(buffer.isEmpty()); + assertEquals(0, buffer.getEventCount()); + } + + @Test + public void testAddEvent() { + Map event = createTestEvent("event1"); + + buffer.addEvent(event); + + assertEquals(1, buffer.getEventCount()); + assertFalse(buffer.isEmpty()); + } + + @Test + public void testAddMultipleEvents() { + for (int i = 0; i < 10; i++) { + buffer.addEvent(createTestEvent("event" + i)); + } + + assertEquals(10, buffer.getEventCount()); + } + + @Test + public void testPollBatchByPriority() { + buffer.addEvent(createTestEvent("event1", NRVideoConstants.EVENT_TYPE_LIVE)); + buffer.addEvent(createTestEvent("event2", NRVideoConstants.EVENT_TYPE_LIVE)); + + List> batch = buffer.pollBatchByPriority( + 10000, null, NRVideoConstants.EVENT_TYPE_LIVE); + + assertEquals(2, batch.size()); + assertEquals(0, buffer.getEventCount()); + } + + @Test + public void testPollBatchByPriorityLive() { + buffer.addEvent(createTestEvent("live1", NRVideoConstants.EVENT_TYPE_LIVE)); + buffer.addEvent(createTestEvent("ondemand1", NRVideoConstants.EVENT_TYPE_ONDEMAND)); + + List> liveBatch = buffer.pollBatchByPriority( + 10000, null, NRVideoConstants.EVENT_TYPE_LIVE); + + assertEquals(1, liveBatch.size()); + assertEquals("live1", liveBatch.get(0).get("eventId")); + assertEquals(1, buffer.getEventCount()); + } + + @Test + public void testPollBatchByPriorityOndemand() { + buffer.addEvent(createTestEvent("live1", NRVideoConstants.EVENT_TYPE_LIVE)); + buffer.addEvent(createTestEvent("ondemand1", NRVideoConstants.EVENT_TYPE_ONDEMAND)); + + List> ondemandBatch = buffer.pollBatchByPriority( + 10000, null, NRVideoConstants.EVENT_TYPE_ONDEMAND); + + assertEquals(1, ondemandBatch.size()); + assertEquals("ondemand1", ondemandBatch.get(0).get("eventId")); + assertEquals(1, buffer.getEventCount()); + } + + @Test + public void testIsEmptyTrue() { + assertTrue(buffer.isEmpty()); + } + + @Test + public void testIsEmptyFalse() { + buffer.addEvent(createTestEvent("event1")); + + assertFalse(buffer.isEmpty()); + } + + @Test + public void testGetEventCount() { + assertEquals(0, buffer.getEventCount()); + + buffer.addEvent(createTestEvent("event1")); + assertEquals(1, buffer.getEventCount()); + + buffer.addEvent(createTestEvent("event2")); + assertEquals(2, buffer.getEventCount()); + } + + @Test + public void testEmergencyBackup() { + buffer.addEvent(createTestEvent("event1", NRVideoConstants.EVENT_TYPE_LIVE)); + buffer.addEvent(createTestEvent("event2", NRVideoConstants.EVENT_TYPE_ONDEMAND)); + + buffer.emergencyBackup(); + + // Events should be backed up to SQLite + assertTrue(storage.hasBackupData()); + assertEquals(2, storage.getEventCount()); + } + + @Test + public void testEmergencyBackupEmptyBuffer() { + buffer.emergencyBackup(); + + assertFalse(storage.hasBackupData()); + assertEquals(0, storage.getEventCount()); + } + + @Test + public void testBackupFailedEvents() { + List> failedEvents = new ArrayList<>(); + failedEvents.add(createTestEvent("failed1")); + failedEvents.add(createTestEvent("failed2")); + + buffer.backupFailedEvents(failedEvents); + + assertTrue(storage.hasBackupData()); + assertEquals(2, storage.getEventCount()); + } + + @Test + public void testBackupFailedEventsNull() { + buffer.backupFailedEvents(null); + + assertFalse(storage.hasBackupData()); + } + + @Test + public void testBackupFailedEventsEmpty() { + buffer.backupFailedEvents(new ArrayList>()); + + assertFalse(storage.hasBackupData()); + } + + @Test + public void testOnSuccessfulHarvest() { + buffer.onSuccessfulHarvest(); + + // Should not throw exception + assertNotNull(buffer); + } + + @Test + public void testOnSuccessfulHarvestWithBackupData() { + // Pre-populate SQLite with backup data + storage.backupFailedEvents(createTestEventList(5)); + + buffer.onSuccessfulHarvest(); + + // Recovery mode should be activated + assertTrue(storage.hasBackupData()); + } + + @Test + public void testCleanup() { + buffer.addEvent(createTestEvent("event1")); + + buffer.cleanup(); + + // Cleanup should complete without errors + assertNotNull(buffer); + } + + @Test + public void testSetOverflowCallback() { + EventBufferInterface.OverflowCallback callback = mock(EventBufferInterface.OverflowCallback.class); + + buffer.setOverflowCallback(callback); + + // Should not throw exception + assertNotNull(buffer); + } + + @Test + public void testSetCapacityCallback() { + EventBufferInterface.CapacityCallback callback = mock(EventBufferInterface.CapacityCallback.class); + + buffer.setCapacityCallback(callback); + + // Should not throw exception + assertNotNull(buffer); + } + + @Test + public void testGetRecoveryStats() { + CrashSafeEventBuffer.RecoveryStats stats = buffer.getRecoveryStats(); + + assertNotNull(stats); + assertFalse(stats.isRecovering); + assertEquals(0, stats.backupEvents); + assertEquals(0, stats.memoryEvents); + assertFalse(stats.isTVDevice); + } + + @Test + public void testGetRecoveryStatsWithBackup() { + storage.backupFailedEvents(createTestEventList(5)); + + CrashSafeEventBuffer.RecoveryStats stats = buffer.getRecoveryStats(); + + assertNotNull(stats); + assertEquals(5, stats.backupEvents); + } + + @Test + public void testGetRecoveryStatsWithMemoryEvents() { + buffer.addEvent(createTestEvent("event1")); + buffer.addEvent(createTestEvent("event2")); + + CrashSafeEventBuffer.RecoveryStats stats = buffer.getRecoveryStats(); + + assertNotNull(stats); + assertEquals(2, stats.memoryEvents); + } + + @Test + public void testRecoveryStatsToString() { + CrashSafeEventBuffer.RecoveryStats stats = buffer.getRecoveryStats(); + + String statsString = stats.toString(); + + assertNotNull(statsString); + assertTrue(statsString.contains("Recovery")); + } + + @Test + public void testTVDeviceConfiguration() { + when(mockConfiguration.isTV()).thenReturn(true); + + CrashSafeEventBuffer tvBuffer = new CrashSafeEventBuffer(context, mockConfiguration, storage); + + CrashSafeEventBuffer.RecoveryStats stats = tvBuffer.getRecoveryStats(); + assertTrue(stats.isTVDevice); + + tvBuffer.cleanup(); + } + + @Test + public void testMobileDeviceConfiguration() { + when(mockConfiguration.isTV()).thenReturn(false); + + CrashSafeEventBuffer mobileBuffer = new CrashSafeEventBuffer(context, mockConfiguration, storage); + + CrashSafeEventBuffer.RecoveryStats stats = mobileBuffer.getRecoveryStats(); + assertFalse(stats.isTVDevice); + + mobileBuffer.cleanup(); + } + + @Test + public void testEmergencyBackupPreservesEventData() { + Map event = new HashMap<>(); + event.put("eventId", "test123"); + event.put("timestamp", 12345L); + event.put("eventType", NRVideoConstants.EVENT_TYPE_LIVE); + event.put("contentIsLive", true); // Required for PriorityEventBuffer to identify as live event + + buffer.addEvent(event); + buffer.emergencyBackup(); + + List> recovered = storage.pollEvents("live", 10); + assertEquals(1, recovered.size()); + assertEquals("test123", recovered.get(0).get("eventId")); + } + + @Test + public void testMultipleEmergencyBackups() { + buffer.addEvent(createTestEvent("event1", NRVideoConstants.EVENT_TYPE_LIVE)); + buffer.emergencyBackup(); + + buffer.addEvent(createTestEvent("event2", NRVideoConstants.EVENT_TYPE_LIVE)); + buffer.emergencyBackup(); + + assertEquals(2, storage.getEventCount()); + } + + @Test + public void testConcurrentEventAddition() throws InterruptedException { + final int threadCount = 10; + final int eventsPerThread = 10; + Thread[] threads = new Thread[threadCount]; + + for (int i = 0; i < threadCount; i++) { + final int threadId = i; + threads[i] = new Thread(new Runnable() { + @Override + public void run() { + for (int j = 0; j < eventsPerThread; j++) { + buffer.addEvent(createTestEvent("event_" + threadId + "_" + j)); + } + } + }); + threads[i].start(); + } + + for (Thread thread : threads) { + thread.join(); + } + + assertEquals(threadCount * eventsPerThread, buffer.getEventCount()); + } + + @Test + public void testCrashDetectionSessionMarking() { + // Session should be marked as active + boolean sessionActive = context.getSharedPreferences("nr_video_crash_detection", Context.MODE_PRIVATE) + .getBoolean("session_active", false); + + assertTrue(sessionActive); + } + + @Test + public void testCleanupMarksSessionEnd() { + buffer.cleanup(); + + boolean sessionActive = context.getSharedPreferences("nr_video_crash_detection", Context.MODE_PRIVATE) + .getBoolean("session_active", false); + + assertFalse(sessionActive); + } + + @Test + public void testLargeEventSet() { + for (int i = 0; i < 1000; i++) { + buffer.addEvent(createTestEvent("event" + i)); + } + + // PriorityEventBuffer has capacity limits (150 live + 350 on-demand for mobile) + // When adding 1000 live events, only the most recent 150 are kept + assertTrue("Buffer should respect capacity limits", buffer.getEventCount() <= 150); + + buffer.emergencyBackup(); + assertTrue(storage.hasBackupData()); + } + + @Test + public void testPollWithSizeEstimator() { + SizeEstimator estimator = mock(SizeEstimator.class); + when(estimator.estimate(any())).thenReturn(100); + + buffer.addEvent(createTestEvent("event1", NRVideoConstants.EVENT_TYPE_LIVE)); + + List> batch = buffer.pollBatchByPriority( + 1000, estimator, NRVideoConstants.EVENT_TYPE_LIVE); + + assertNotNull(batch); + } + + @Test + public void testEmptyPollReturnsEmptyList() { + List> batch = buffer.pollBatchByPriority( + 10000, null, NRVideoConstants.EVENT_TYPE_LIVE); + + assertNotNull(batch); + assertEquals(0, batch.size()); + } + + @Test + public void testRecoveryModeActivation() { + List> failedEvents = createTestEventList(5); + + buffer.backupFailedEvents(failedEvents); + + CrashSafeEventBuffer.RecoveryStats stats = buffer.getRecoveryStats(); + assertTrue(stats.isRecovering); + } + + @Test + public void testMemoryAndSQLiteEventCount() { + buffer.addEvent(createTestEvent("memory1")); + storage.backupFailedEvents(createTestEventList(5)); + + // Memory: 1, SQLite: 5 + // During recovery, total should be 6 + int totalCount = buffer.getEventCount() + storage.getEventCount(); + assertEquals(6, totalCount); + } + + @Test + public void testBufferWithNullEvent() { + // Should handle null gracefully or throw appropriate exception + try { + buffer.addEvent(null); + // If no exception, verify count + assertTrue(buffer.getEventCount() >= 0); + } catch (NullPointerException e) { + // Expected for null event + assertTrue(true); + } + } + + @Test + public void testTVOptimizedBackupThreshold() { + when(mockConfiguration.isTV()).thenReturn(true); + + CrashSafeEventBuffer tvBuffer = new CrashSafeEventBuffer(context, mockConfiguration, storage); + + // Set up callbacks for the TV buffer to prevent NullPointerException + tvBuffer.setOverflowCallback(new EventBufferInterface.OverflowCallback() { + @Override + public void onBufferNearFull(String bufferType) { + // Do nothing in tests + } + }); + + tvBuffer.setCapacityCallback(new EventBufferInterface.CapacityCallback() { + @Override + public void onCapacityThresholdReached(double currentCapacity, String bufferType) { + // Do nothing in tests + } + }); + + // TV devices have higher threshold (200 vs 100) + for (int i = 0; i < 200; i++) { + tvBuffer.addEvent(createTestEvent("event" + i)); + } + + assertEquals(200, tvBuffer.getEventCount()); + tvBuffer.cleanup(); + } + + // Helper methods + + private Map createTestEvent(String eventId) { + return createTestEvent(eventId, NRVideoConstants.EVENT_TYPE_LIVE); + } + + private Map createTestEvent(String eventId, String eventType) { + Map event = new HashMap<>(); + event.put("eventId", eventId); + event.put("timestamp", System.currentTimeMillis()); + event.put("eventType", eventType); + event.put("data", "test data"); + // PriorityEventBuffer uses "contentIsLive" field to determine priority + event.put("contentIsLive", NRVideoConstants.EVENT_TYPE_LIVE.equals(eventType)); + return event; + } + + private List> createTestEventList(int count) { + List> events = new ArrayList<>(); + for (int i = 0; i < count; i++) { + events.add(createTestEvent("event" + i)); + } + return events; + } +} diff --git a/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/storage/IntegratedDeadLetterHandlerTest.java b/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/storage/IntegratedDeadLetterHandlerTest.java new file mode 100644 index 00000000..b53c77c4 --- /dev/null +++ b/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/storage/IntegratedDeadLetterHandlerTest.java @@ -0,0 +1,606 @@ +package com.newrelic.videoagent.core.storage; + +import com.newrelic.videoagent.core.NRVideoConfiguration; +import com.newrelic.videoagent.core.NRVideoConstants; +import com.newrelic.videoagent.core.harvest.HttpClientInterface; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +/** + * Unit tests for IntegratedDeadLetterHandler. + * Tests retry logic, emergency backup, device-specific optimizations, and memory management. + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = 28, manifest = Config.NONE) +public class IntegratedDeadLetterHandlerTest { + + @Mock + private CrashSafeEventBuffer mockMainBuffer; + + @Mock + private HttpClientInterface mockHttpClient; + + @Mock + private NRVideoConfiguration mockConfiguration; + + private IntegratedDeadLetterHandler handler; + + @Before + public void setUp() { + MockitoAnnotations.openMocks(this); + + // Set up default configuration + when(mockConfiguration.isTV()).thenReturn(false); + when(mockConfiguration.isMemoryOptimized()).thenReturn(false); + when(mockConfiguration.getRegularBatchSizeBytes()).thenReturn(50000); + when(mockConfiguration.getLiveBatchSizeBytes()).thenReturn(25000); + when(mockConfiguration.getDeadLetterRetryInterval()).thenReturn(60000L); + when(mockConfiguration.getMaxDeadLetterSize()).thenReturn(100); + + handler = new IntegratedDeadLetterHandler(mockMainBuffer, mockHttpClient, mockConfiguration); + } + + // ========== Constructor Tests ========== + + @Test + public void testConstructorWithMobileDevice() { + when(mockConfiguration.isTV()).thenReturn(false); + + IntegratedDeadLetterHandler mobileHandler = new IntegratedDeadLetterHandler( + mockMainBuffer, mockHttpClient, mockConfiguration); + + assertNotNull("Handler should be created for mobile", mobileHandler); + } + + @Test + public void testConstructorWithTVDevice() { + when(mockConfiguration.isTV()).thenReturn(true); + + IntegratedDeadLetterHandler tvHandler = new IntegratedDeadLetterHandler( + mockMainBuffer, mockHttpClient, mockConfiguration); + + assertNotNull("Handler should be created for TV", tvHandler); + } + + @Test + public void testConstructorWithMemoryOptimized() { + when(mockConfiguration.isMemoryOptimized()).thenReturn(true); + + IntegratedDeadLetterHandler optimizedHandler = new IntegratedDeadLetterHandler( + mockMainBuffer, mockHttpClient, mockConfiguration); + + assertNotNull("Handler should be created with memory optimization", optimizedHandler); + } + + @Test + public void testConstructorWithTVAndMemoryOptimized() { + when(mockConfiguration.isTV()).thenReturn(true); + when(mockConfiguration.isMemoryOptimized()).thenReturn(true); + + IntegratedDeadLetterHandler tvOptimizedHandler = new IntegratedDeadLetterHandler( + mockMainBuffer, mockHttpClient, mockConfiguration); + + assertNotNull("Handler should be created for TV with memory optimization", tvOptimizedHandler); + } + + @Test + public void testConstructorWithCustomBatchSizes() { + when(mockConfiguration.getRegularBatchSizeBytes()).thenReturn(100000); + when(mockConfiguration.getLiveBatchSizeBytes()).thenReturn(50000); + + IntegratedDeadLetterHandler customHandler = new IntegratedDeadLetterHandler( + mockMainBuffer, mockHttpClient, mockConfiguration); + + assertNotNull("Handler should accept custom batch sizes", customHandler); + } + + @Test + public void testConstructorWithCustomRetryInterval() { + when(mockConfiguration.getDeadLetterRetryInterval()).thenReturn(120000L); + + IntegratedDeadLetterHandler customHandler = new IntegratedDeadLetterHandler( + mockMainBuffer, mockHttpClient, mockConfiguration); + + assertNotNull("Handler should accept custom retry interval", customHandler); + } + + @Test + public void testConstructorWithCustomMaxDeadLetterSize() { + when(mockConfiguration.getMaxDeadLetterSize()).thenReturn(200); + + IntegratedDeadLetterHandler customHandler = new IntegratedDeadLetterHandler( + mockMainBuffer, mockHttpClient, mockConfiguration); + + assertNotNull("Handler should accept custom max dead letter size", customHandler); + } + + // ========== handleFailedEvents Tests ========== + + @Test + public void testHandleFailedEventsWithNullList() { + handler.handleFailedEvents(null, NRVideoConstants.EVENT_TYPE_ONDEMAND); + + // Should not throw exception + verify(mockMainBuffer, never()).backupFailedEvents(anyList()); + } + + @Test + public void testHandleFailedEventsWithEmptyList() { + List> events = new ArrayList<>(); + + handler.handleFailedEvents(events, NRVideoConstants.EVENT_TYPE_ONDEMAND); + + verify(mockMainBuffer, never()).backupFailedEvents(anyList()); + } + + @Test + public void testHandleFailedEventsWithSingleEvent() { + List> events = createSampleEvents(1); + + handler.handleFailedEvents(events, NRVideoConstants.EVENT_TYPE_ONDEMAND); + + // Event should be queued for retry (not backed up on first failure) + verify(mockMainBuffer, never()).backupFailedEvents(anyList()); + } + + @Test + public void testHandleFailedEventsWithMultipleEvents() { + List> events = createSampleEvents(5); + + handler.handleFailedEvents(events, NRVideoConstants.EVENT_TYPE_ONDEMAND); + + // Events should be queued for retry + verify(mockMainBuffer, never()).backupFailedEvents(anyList()); + } + + @Test + public void testHandleFailedEventsWithLiveBufferType() { + List> events = createSampleEvents(2); + + handler.handleFailedEvents(events, NRVideoConstants.EVENT_TYPE_LIVE); + + // Should handle live events + verify(mockMainBuffer, never()).backupFailedEvents(anyList()); + } + + @Test + public void testHandleFailedEventsWithOnDemandBufferType() { + List> events = createSampleEvents(2); + + handler.handleFailedEvents(events, NRVideoConstants.EVENT_TYPE_ONDEMAND); + + // Should handle on-demand events + verify(mockMainBuffer, never()).backupFailedEvents(anyList()); + } + + @Test + public void testHandleFailedEventsWithMaxRetries() { + // Create event with max retries reached + List> events = new ArrayList<>(); + Map event = createEventWithRetryCount(5); // More than max retries + events.add(event); + + handler.handleFailedEvents(events, NRVideoConstants.EVENT_TYPE_ONDEMAND); + + // Should backup to SQLite + verify(mockMainBuffer).backupFailedEvents(anyList()); + } + + @Test + public void testHandleFailedEventsExhaustedRetries() { + // Mobile device gets 3 retries by default + List> events = new ArrayList<>(); + Map event = createEventWithRetryCount(3); + events.add(event); + + handler.handleFailedEvents(events, NRVideoConstants.EVENT_TYPE_ONDEMAND); + + // Should backup when retries exhausted + verify(mockMainBuffer).backupFailedEvents(anyList()); + } + + @Test + public void testHandleFailedEventsMixedRetryCountsL() { + List> events = new ArrayList<>(); + events.add(createEventWithRetryCount(0)); // Retry + events.add(createEventWithRetryCount(1)); // Retry + events.add(createEventWithRetryCount(5)); // Backup + events.add(createEventWithRetryCount(2)); // Retry + + handler.handleFailedEvents(events, NRVideoConstants.EVENT_TYPE_ONDEMAND); + + // Should backup only the exhausted event + verify(mockMainBuffer).backupFailedEvents(argThat(list -> list.size() == 1)); + } + + // ========== emergencyBackup Tests ========== + + @Test + public void testEmergencyBackup() { + handler.emergencyBackup(); + + // Should not throw exception even with empty queue + verify(mockMainBuffer, never()).backupFailedEvents(anyList()); + } + + @Test + public void testEmergencyBackupWithPendingEvents() { + // First add some failed events + List> events = createSampleEvents(5); + handler.handleFailedEvents(events, NRVideoConstants.EVENT_TYPE_ONDEMAND); + + // Then trigger emergency backup + handler.emergencyBackup(); + + // Should backup pending events + verify(mockMainBuffer, atLeastOnce()).backupFailedEvents(anyList()); + } + + @Test + public void testEmergencyBackupForMobileDevice() { + when(mockConfiguration.isTV()).thenReturn(false); + when(mockConfiguration.getMaxDeadLetterSize()).thenReturn(100); + + IntegratedDeadLetterHandler mobileHandler = new IntegratedDeadLetterHandler( + mockMainBuffer, mockHttpClient, mockConfiguration); + + mobileHandler.emergencyBackup(); + + // Should use mobile-specific batch size (maxDeadLetterSize * 2) + verify(mockMainBuffer, never()).backupFailedEvents(anyList()); + } + + @Test + public void testEmergencyBackupForTVDevice() { + when(mockConfiguration.isTV()).thenReturn(true); + when(mockConfiguration.getMaxDeadLetterSize()).thenReturn(100); + + IntegratedDeadLetterHandler tvHandler = new IntegratedDeadLetterHandler( + mockMainBuffer, mockHttpClient, mockConfiguration); + + tvHandler.emergencyBackup(); + + // Should use TV-specific batch size (maxDeadLetterSize * 3) + verify(mockMainBuffer, never()).backupFailedEvents(anyList()); + } + + @Test + public void testMultipleEmergencyBackups() { + handler.emergencyBackup(); + handler.emergencyBackup(); + handler.emergencyBackup(); + + // Should handle multiple emergency backups + verify(mockMainBuffer, never()).backupFailedEvents(anyList()); + } + + // ========== Device-Specific Behavior Tests ========== + + @Test + public void testMobileMaxRetries() { + when(mockConfiguration.isTV()).thenReturn(false); + when(mockConfiguration.isMemoryOptimized()).thenReturn(false); + + IntegratedDeadLetterHandler mobileHandler = new IntegratedDeadLetterHandler( + mockMainBuffer, mockHttpClient, mockConfiguration); + + // Mobile gets 3 retries + List> events = new ArrayList<>(); + events.add(createEventWithRetryCount(3)); + + mobileHandler.handleFailedEvents(events, NRVideoConstants.EVENT_TYPE_ONDEMAND); + + verify(mockMainBuffer).backupFailedEvents(anyList()); + } + + @Test + public void testTVMaxRetries() { + when(mockConfiguration.isTV()).thenReturn(true); + when(mockConfiguration.isMemoryOptimized()).thenReturn(false); + + IntegratedDeadLetterHandler tvHandler = new IntegratedDeadLetterHandler( + mockMainBuffer, mockHttpClient, mockConfiguration); + + // TV gets 5 retries + List> events = new ArrayList<>(); + events.add(createEventWithRetryCount(5)); + + tvHandler.handleFailedEvents(events, NRVideoConstants.EVENT_TYPE_ONDEMAND); + + verify(mockMainBuffer).backupFailedEvents(anyList()); + } + + @Test + public void testMobileMemoryOptimizedMaxRetries() { + when(mockConfiguration.isTV()).thenReturn(false); + when(mockConfiguration.isMemoryOptimized()).thenReturn(true); + + IntegratedDeadLetterHandler mobileHandler = new IntegratedDeadLetterHandler( + mockMainBuffer, mockHttpClient, mockConfiguration); + + // Memory-optimized mobile gets 2 retries + List> events = new ArrayList<>(); + events.add(createEventWithRetryCount(2)); + + mobileHandler.handleFailedEvents(events, NRVideoConstants.EVENT_TYPE_ONDEMAND); + + verify(mockMainBuffer).backupFailedEvents(anyList()); + } + + @Test + public void testTVMemoryOptimizedMaxRetries() { + when(mockConfiguration.isTV()).thenReturn(true); + when(mockConfiguration.isMemoryOptimized()).thenReturn(true); + + IntegratedDeadLetterHandler tvHandler = new IntegratedDeadLetterHandler( + mockMainBuffer, mockHttpClient, mockConfiguration); + + // Memory-optimized TV gets 3 retries + List> events = new ArrayList<>(); + events.add(createEventWithRetryCount(3)); + + tvHandler.handleFailedEvents(events, NRVideoConstants.EVENT_TYPE_ONDEMAND); + + verify(mockMainBuffer).backupFailedEvents(anyList()); + } + + // ========== Retry Metadata Tests ========== + + @Test + public void testRetryMetadataAdded() { + List> events = createSampleEvents(1); + + handler.handleFailedEvents(events, NRVideoConstants.EVENT_TYPE_ONDEMAND); + + // Event should have retry metadata added (verified implicitly by retry logic) + } + + @Test + public void testRetryMetadataIncludesDeviceType() { + when(mockConfiguration.isTV()).thenReturn(true); + + IntegratedDeadLetterHandler tvHandler = new IntegratedDeadLetterHandler( + mockMainBuffer, mockHttpClient, mockConfiguration); + + List> events = createSampleEvents(1); + + tvHandler.handleFailedEvents(events, NRVideoConstants.EVENT_TYPE_ONDEMAND); + + // Device type should be included in metadata + } + + @Test + public void testRetryMetadataIncludesBufferType() { + List> events = createSampleEvents(1); + + handler.handleFailedEvents(events, NRVideoConstants.EVENT_TYPE_LIVE); + + // Buffer type should be included in metadata + } + + @Test + public void testRetryMetadataIncludesTimestamp() { + List> events = createSampleEvents(1); + + handler.handleFailedEvents(events, NRVideoConstants.EVENT_TYPE_ONDEMAND); + + // Timestamp should be included in metadata + } + + // ========== Memory Capacity Tests ========== + + @Test + public void testMemoryCapacityForTV() { + when(mockConfiguration.isTV()).thenReturn(true); + when(mockConfiguration.isMemoryOptimized()).thenReturn(false); + when(mockConfiguration.getMaxDeadLetterSize()).thenReturn(100); + + IntegratedDeadLetterHandler tvHandler = new IntegratedDeadLetterHandler( + mockMainBuffer, mockHttpClient, mockConfiguration); + + // TV allows 90% capacity usage + List> events = createSampleEvents(10); + + tvHandler.handleFailedEvents(events, NRVideoConstants.EVENT_TYPE_ONDEMAND); + + // Should handle events within capacity + } + + @Test + public void testMemoryCapacityForMobile() { + when(mockConfiguration.isTV()).thenReturn(false); + when(mockConfiguration.isMemoryOptimized()).thenReturn(false); + when(mockConfiguration.getMaxDeadLetterSize()).thenReturn(100); + + IntegratedDeadLetterHandler mobileHandler = new IntegratedDeadLetterHandler( + mockMainBuffer, mockHttpClient, mockConfiguration); + + // Mobile allows 80% capacity usage + List> events = createSampleEvents(10); + + mobileHandler.handleFailedEvents(events, NRVideoConstants.EVENT_TYPE_ONDEMAND); + + // Should handle events within capacity + } + + @Test + public void testMemoryCapacityForMemoryOptimized() { + when(mockConfiguration.isMemoryOptimized()).thenReturn(true); + when(mockConfiguration.getMaxDeadLetterSize()).thenReturn(100); + + IntegratedDeadLetterHandler optimizedHandler = new IntegratedDeadLetterHandler( + mockMainBuffer, mockHttpClient, mockConfiguration); + + // Memory-optimized allows 65% capacity usage + List> events = createSampleEvents(10); + + optimizedHandler.handleFailedEvents(events, NRVideoConstants.EVENT_TYPE_ONDEMAND); + + // Should handle events within capacity + } + + // ========== Batch Size Tests ========== + + @Test + public void testRegularBatchSize() { + when(mockConfiguration.getRegularBatchSizeBytes()).thenReturn(100000); + + IntegratedDeadLetterHandler customHandler = new IntegratedDeadLetterHandler( + mockMainBuffer, mockHttpClient, mockConfiguration); + + assertNotNull("Handler should use custom regular batch size", customHandler); + } + + @Test + public void testLiveBatchSize() { + when(mockConfiguration.getLiveBatchSizeBytes()).thenReturn(50000); + + IntegratedDeadLetterHandler customHandler = new IntegratedDeadLetterHandler( + mockMainBuffer, mockHttpClient, mockConfiguration); + + assertNotNull("Handler should use custom live batch size", customHandler); + } + + @Test + public void testSmallBatchSizes() { + when(mockConfiguration.getRegularBatchSizeBytes()).thenReturn(10000); + when(mockConfiguration.getLiveBatchSizeBytes()).thenReturn(5000); + + IntegratedDeadLetterHandler smallHandler = new IntegratedDeadLetterHandler( + mockMainBuffer, mockHttpClient, mockConfiguration); + + assertNotNull("Handler should work with small batch sizes", smallHandler); + } + + @Test + public void testLargeBatchSizes() { + when(mockConfiguration.getRegularBatchSizeBytes()).thenReturn(500000); + when(mockConfiguration.getLiveBatchSizeBytes()).thenReturn(250000); + + IntegratedDeadLetterHandler largeHandler = new IntegratedDeadLetterHandler( + mockMainBuffer, mockHttpClient, mockConfiguration); + + assertNotNull("Handler should work with large batch sizes", largeHandler); + } + + // ========== Retry Interval Tests ========== + + @Test + public void testCustomRetryInterval() { + when(mockConfiguration.getDeadLetterRetryInterval()).thenReturn(120000L); + + IntegratedDeadLetterHandler customHandler = new IntegratedDeadLetterHandler( + mockMainBuffer, mockHttpClient, mockConfiguration); + + assertNotNull("Handler should use custom retry interval", customHandler); + } + + @Test + public void testLiveRetryIntervalCalculation() { + when(mockConfiguration.getDeadLetterRetryInterval()).thenReturn(60000L); + + IntegratedDeadLetterHandler customHandler = new IntegratedDeadLetterHandler( + mockMainBuffer, mockHttpClient, mockConfiguration); + + // Live retry interval should be half of regular (30s) + assertNotNull("Handler should calculate live retry interval", customHandler); + } + + @Test + public void testMinimumLiveRetryInterval() { + when(mockConfiguration.getDeadLetterRetryInterval()).thenReturn(10000L); + + IntegratedDeadLetterHandler customHandler = new IntegratedDeadLetterHandler( + mockMainBuffer, mockHttpClient, mockConfiguration); + + // Live retry interval should be minimum 30s + assertNotNull("Handler should enforce minimum live retry interval", customHandler); + } + + // ========== Edge Cases ========== + + @Test + public void testHandleLargeNumberOfEvents() { + // Create events with high retry counts to trigger backup + List> events = new ArrayList<>(); + for (int i = 0; i < 200; i++) { + events.add(createEventWithRetryCount(5)); // Exceed max retries + } + + handler.handleFailedEvents(events, NRVideoConstants.EVENT_TYPE_ONDEMAND); + + // Should backup events that exceeded retry count + verify(mockMainBuffer, atLeastOnce()).backupFailedEvents(anyList()); + } + + @Test + public void testConcurrentHandleFailedEvents() { + List> events1 = createSampleEvents(5); + List> events2 = createSampleEvents(5); + + // Simulate concurrent calls + handler.handleFailedEvents(events1, NRVideoConstants.EVENT_TYPE_ONDEMAND); + handler.handleFailedEvents(events2, NRVideoConstants.EVENT_TYPE_LIVE); + + // Second call should be skipped due to AtomicBoolean guard + } + + @Test + public void testHandleEventsWithComplexData() { + List> events = new ArrayList<>(); + Map event = new HashMap<>(); + event.put("eventType", "CONTENT_START"); + event.put("nested", new HashMap() {{ + put("key", "value"); + }}); + event.put("array", new ArrayList() {{ + add("item1"); + add("item2"); + }}); + events.add(event); + + handler.handleFailedEvents(events, NRVideoConstants.EVENT_TYPE_ONDEMAND); + + // Should handle complex event data + } + + // ========== Helper Methods ========== + + private List> createSampleEvents(int count) { + List> events = new ArrayList<>(); + for (int i = 0; i < count; i++) { + Map event = new HashMap<>(); + event.put("eventType", "TEST_EVENT"); + event.put("timestamp", System.currentTimeMillis()); + event.put("index", i); + events.add(event); + } + return events; + } + + private Map createEventWithRetryCount(int retryCount) { + Map event = new HashMap<>(); + event.put("eventType", "TEST_EVENT"); + event.put("timestamp", System.currentTimeMillis()); + + Map metadata = new HashMap<>(); + metadata.put("retryCount", retryCount); + event.put("retryMetadata", metadata); + + return event; + } +} diff --git a/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/storage/VideoEventStorageTest.java b/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/storage/VideoEventStorageTest.java new file mode 100644 index 00000000..fb42eac9 --- /dev/null +++ b/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/storage/VideoEventStorageTest.java @@ -0,0 +1,436 @@ +package com.newrelic.videoagent.core.storage; + +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.*; + +/** + * Unit tests for VideoEventStorage. + * Tests SQLite database operations for crash recovery and event backup. + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = 28, manifest = Config.NONE) +public class VideoEventStorageTest { + + private Context context; + private VideoEventStorage storage; + + @Before + public void setUp() { + context = RuntimeEnvironment.getApplication(); + storage = new VideoEventStorage(context); + } + + @After + public void tearDown() { + storage.close(); + context.deleteDatabase("nr_video_backup.db"); + } + + @Test + public void testStorageInitialization() { + assertNotNull(storage); + SQLiteDatabase db = storage.getReadableDatabase(); + assertNotNull(db); + assertTrue(db.isOpen()); + } + + @Test + public void testDatabaseCreation() { + SQLiteDatabase db = storage.getReadableDatabase(); + + assertTrue(db.isOpen()); + assertNotNull(db); + } + + @Test + public void testBackupEventsEmpty() { + List> liveEvents = new ArrayList<>(); + List> ondemandEvents = new ArrayList<>(); + + storage.backupEvents(liveEvents, ondemandEvents); + + assertEquals(0, storage.getEventCount()); + assertTrue(storage.isEmpty()); + } + + @Test + public void testBackupLiveEvents() { + List> liveEvents = createTestEvents(5); + List> ondemandEvents = new ArrayList<>(); + + storage.backupEvents(liveEvents, ondemandEvents); + + assertEquals(5, storage.getEventCount()); + assertFalse(storage.isEmpty()); + assertTrue(storage.hasBackupData()); + } + + @Test + public void testBackupOndemandEvents() { + List> liveEvents = new ArrayList<>(); + List> ondemandEvents = createTestEvents(3); + + storage.backupEvents(liveEvents, ondemandEvents); + + assertEquals(3, storage.getEventCount()); + assertFalse(storage.isEmpty()); + } + + @Test + public void testBackupBothEventTypes() { + List> liveEvents = createTestEvents(5); + List> ondemandEvents = createTestEvents(3); + + storage.backupEvents(liveEvents, ondemandEvents); + + assertEquals(8, storage.getEventCount()); + } + + @Test + public void testBackupFailedEvents() { + List> failedEvents = createTestEvents(4); + + storage.backupFailedEvents(failedEvents); + + assertEquals(4, storage.getEventCount()); + assertTrue(storage.hasBackupData()); + } + + @Test + public void testPollEventsLivePriority() { + List> liveEvents = createTestEvents(5); + storage.backupEvents(liveEvents, new ArrayList>()); + + List> polledEvents = storage.pollEvents("live", 3); + + assertEquals(3, polledEvents.size()); + assertEquals(2, storage.getEventCount()); // 2 remaining + } + + @Test + public void testPollEventsOndemandPriority() { + List> ondemandEvents = createTestEvents(5); + storage.backupEvents(new ArrayList>(), ondemandEvents); + + List> polledEvents = storage.pollEvents("ondemand", 2); + + assertEquals(2, polledEvents.size()); + assertEquals(3, storage.getEventCount()); + } + + @Test + public void testPollEventsFailedPriority() { + List> failedEvents = createTestEvents(5); + storage.backupFailedEvents(failedEvents); + + List> polledEvents = storage.pollEvents("failed", 3); + + assertEquals(3, polledEvents.size()); + assertEquals(2, storage.getEventCount()); + } + + @Test + public void testPollEventsRemovesPolledEvents() { + List> events = createTestEvents(10); + storage.backupFailedEvents(events); + + assertEquals(10, storage.getEventCount()); + + storage.pollEvents("failed", 5); + assertEquals(5, storage.getEventCount()); + + storage.pollEvents("failed", 5); + assertEquals(0, storage.getEventCount()); + assertTrue(storage.isEmpty()); + } + + @Test + public void testPollEventsMoreThanAvailable() { + List> events = createTestEvents(3); + storage.backupFailedEvents(events); + + List> polledEvents = storage.pollEvents("failed", 10); + + assertEquals(3, polledEvents.size()); + assertEquals(0, storage.getEventCount()); + } + + @Test + public void testPollEventsZeroCount() { + List> events = createTestEvents(5); + storage.backupFailedEvents(events); + + List> polledEvents = storage.pollEvents("failed", 0); + + assertEquals(0, polledEvents.size()); + assertEquals(5, storage.getEventCount()); + } + + @Test + public void testGetEventCountEmpty() { + assertEquals(0, storage.getEventCount()); + } + + @Test + public void testGetEventCountWithEvents() { + storage.backupFailedEvents(createTestEvents(7)); + + assertEquals(7, storage.getEventCount()); + } + + @Test + public void testHasBackupDataEmpty() { + assertFalse(storage.hasBackupData()); + } + + @Test + public void testHasBackupDataWithEvents() { + storage.backupFailedEvents(createTestEvents(1)); + + assertTrue(storage.hasBackupData()); + } + + @Test + public void testIsEmptyTrue() { + assertTrue(storage.isEmpty()); + } + + @Test + public void testIsEmptyFalse() { + storage.backupFailedEvents(createTestEvents(1)); + + assertFalse(storage.isEmpty()); + } + + @Test + public void testCleanupOldEvents() throws InterruptedException { + List> events = createTestEvents(5); + storage.backupFailedEvents(events); + + assertEquals(5, storage.getEventCount()); + + // Cleanup should not remove recent events + storage.cleanup(); + assertEquals(5, storage.getEventCount()); + } + + @Test + public void testMultipleBackupOperations() { + storage.backupFailedEvents(createTestEvents(3)); + assertEquals(3, storage.getEventCount()); + + storage.backupFailedEvents(createTestEvents(2)); + assertEquals(5, storage.getEventCount()); + + storage.backupEvents(createTestEvents(1), createTestEvents(1)); + assertEquals(7, storage.getEventCount()); + } + + @Test + public void testTransactionRollbackOnError() { + // Create events with one that might cause issues + List> events = new ArrayList<>(); + Map event1 = new HashMap<>(); + event1.put("key1", "value1"); + events.add(event1); + + // Normal backup should succeed + storage.backupFailedEvents(events); + assertEquals(1, storage.getEventCount()); + } + + @Test + public void testEventDataPreservation() { + Map originalEvent = new HashMap<>(); + originalEvent.put("eventType", "test"); + originalEvent.put("timestamp", 12345L); + originalEvent.put("data", "test data"); + + List> events = new ArrayList<>(); + events.add(originalEvent); + + storage.backupFailedEvents(events); + + List> polledEvents = storage.pollEvents("failed", 1); + + assertEquals(1, polledEvents.size()); + Map retrievedEvent = polledEvents.get(0); + assertEquals("test", retrievedEvent.get("eventType")); + assertEquals("test data", retrievedEvent.get("data")); + } + + @Test + public void testJsonConversionRoundTrip() { + Map event = new HashMap<>(); + event.put("string", "value"); + event.put("number", 123); + event.put("boolean", true); + + List> events = new ArrayList<>(); + events.add(event); + + storage.backupFailedEvents(events); + List> retrieved = storage.pollEvents("failed", 1); + + assertEquals(1, retrieved.size()); + Map retrievedEvent = retrieved.get(0); + assertEquals("value", retrievedEvent.get("string")); + assertNotNull(retrievedEvent.get("number")); + assertNotNull(retrievedEvent.get("boolean")); + } + + @Test + public void testEmptyEventHandling() { + Map emptyEvent = new HashMap<>(); + List> events = new ArrayList<>(); + events.add(emptyEvent); + + storage.backupFailedEvents(events); + + assertEquals(1, storage.getEventCount()); + } + + @Test + public void testLargeEventSet() { + List> largeEventSet = createTestEvents(1000); + + storage.backupFailedEvents(largeEventSet); + + assertEquals(1000, storage.getEventCount()); + + List> polled = storage.pollEvents("failed", 500); + assertEquals(500, polled.size()); + assertEquals(500, storage.getEventCount()); + } + + @Test + public void testConcurrentBackupOperations() throws InterruptedException { + final int threadCount = 5; + final int eventsPerThread = 10; + Thread[] threads = new Thread[threadCount]; + + for (int i = 0; i < threadCount; i++) { + threads[i] = new Thread(new Runnable() { + @Override + public void run() { + storage.backupFailedEvents(createTestEvents(eventsPerThread)); + } + }); + threads[i].start(); + } + + for (Thread thread : threads) { + thread.join(); + } + + assertEquals(threadCount * eventsPerThread, storage.getEventCount()); + } + + @Test + public void testDatabaseUpgrade() { + SQLiteDatabase db = storage.getWritableDatabase(); + int oldVersion = 1; + int newVersion = 2; + + storage.onUpgrade(db, oldVersion, newVersion); + + assertTrue(db.isOpen()); + assertEquals(0, storage.getEventCount()); + } + + @Test + public void testPriorityOrdering() { + List> liveEvents = createTestEvents(3); + List> ondemandEvents = createTestEvents(3); + + storage.backupEvents(liveEvents, ondemandEvents); + + assertEquals(6, storage.getEventCount()); + + List> livePolled = storage.pollEvents("live", 10); + assertEquals(3, livePolled.size()); + + List> ondemandPolled = storage.pollEvents("ondemand", 10); + assertEquals(3, ondemandPolled.size()); + + assertEquals(0, storage.getEventCount()); + } + + @Test + public void testEventTimestampOrdering() { + List> events1 = createTestEvents(5); + storage.backupFailedEvents(events1); + + try { + Thread.sleep(10); // Small delay to ensure different timestamps + } catch (InterruptedException e) { + // Ignore + } + + List> events2 = createTestEvents(5); + storage.backupFailedEvents(events2); + + List> polled = storage.pollEvents("failed", 3); + assertEquals(3, polled.size()); + + // Should poll oldest events first + assertEquals(7, storage.getEventCount()); + } + + @Test + public void testCleanupEmptyDatabase() { + storage.cleanup(); + + assertEquals(0, storage.getEventCount()); + assertTrue(storage.isEmpty()); + } + + @Test + public void testMultiplePollOperations() { + storage.backupFailedEvents(createTestEvents(20)); + + assertEquals(20, storage.getEventCount()); + + storage.pollEvents("failed", 5); + assertEquals(15, storage.getEventCount()); + + storage.pollEvents("failed", 5); + assertEquals(10, storage.getEventCount()); + + storage.pollEvents("failed", 5); + assertEquals(5, storage.getEventCount()); + + storage.pollEvents("failed", 5); + assertEquals(0, storage.getEventCount()); + assertTrue(storage.isEmpty()); + } + + // Helper methods + + private List> createTestEvents(int count) { + List> events = new ArrayList<>(); + for (int i = 0; i < count; i++) { + Map event = new HashMap<>(); + event.put("eventId", "event_" + i); + event.put("timestamp", System.currentTimeMillis()); + event.put("data", "test data " + i); + events.add(event); + } + return events; + } +} diff --git a/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/tracker/NRVideoTrackerTest.java b/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/tracker/NRVideoTrackerTest.java new file mode 100644 index 00000000..770d22e2 --- /dev/null +++ b/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/tracker/NRVideoTrackerTest.java @@ -0,0 +1,767 @@ +package com.newrelic.videoagent.core.tracker; + +import android.os.Handler; +import android.os.Looper; + +import com.newrelic.videoagent.core.model.NRTrackerState; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.Shadows; +import org.robolectric.shadows.ShadowLooper; + +import java.util.Map; + +import static com.newrelic.videoagent.core.NRDef.*; +import static org.junit.Assert.*; + +/** + * Comprehensive unit tests for NRVideoTracker. + * Tests heartbeat management, state transitions, playback metrics, and event generation. + */ +@RunWith(RobolectricTestRunner.class) +public class NRVideoTrackerTest { + + private NRVideoTracker tracker; + private ShadowLooper shadowLooper; + + @Before + public void setUp() { + tracker = new NRVideoTracker(); + shadowLooper = Shadows.shadowOf(Looper.getMainLooper()); + } + + @After + public void tearDown() { + if (tracker != null) { + tracker.dispose(); + } + } + + // ========== Initialization Tests ========== + + @Test + public void testTrackerCreation() { + assertNotNull("Tracker should be created", tracker); + } + + @Test + public void testInitialState() { + NRTrackerState state = tracker.getState(); + assertNotNull("State should be initialized", state); + } + + @Test + public void testInitialCounters() { + // Test that counters start at 0 + Map attrs = tracker.getAttributes(CONTENT_START, null); + + assertEquals("Number of ads should start at 0", 0, attrs.get("numberOfAds")); + assertEquals("Number of videos should start at 0", 0, attrs.get("numberOfVideos")); + assertEquals("Number of errors should start at 0", 0, attrs.get("numberOfErrors")); + } + + @Test + public void testViewSessionIdGeneration() { + Map attrs = tracker.getAttributes(CONTENT_START, null); + String viewSession = (String) attrs.get("viewSession"); + + assertNotNull("View session should be generated", viewSession); + assertFalse("View session should not be empty", viewSession.isEmpty()); + } + + @Test + public void testViewIdInitialization() { + Map attrs = tracker.getAttributes(CONTENT_START, null); + String viewId = (String) attrs.get("viewId"); + + assertNotNull("View ID should be initialized", viewId); + assertTrue("Initial view ID should contain session and index", viewId.contains("-0")); + } + + @Test + public void testTotalPlaytimeInitialization() { + Map attrs = tracker.getAttributes(CONTENT_START, null); + Long totalPlaytime = (Long) attrs.get("totalPlaytime"); + + assertNotNull("Total playtime should be initialized", totalPlaytime); + assertEquals("Initial total playtime should be 0", Long.valueOf(0L), totalPlaytime); + } + + // ========== Player Management Tests ========== + + @Test + public void testSetPlayer() { + Object mockPlayer = new Object(); + + // Should not throw exception + tracker.setPlayer(mockPlayer); + } + + @Test + public void testSetPlayerTransitionsToPlayerReady() { + Object mockPlayer = new Object(); + + tracker.setPlayer(mockPlayer); + + NRTrackerState state = tracker.getState(); + assertTrue("State should be in player ready", state.isPlayerReady); + } + + @Test + public void testSetPlayerWithNull() { + // Should not throw exception + tracker.setPlayer(null); + } + + @Test + public void testMultipleSetPlayerCalls() { + Object player1 = new Object(); + Object player2 = new Object(); + + tracker.setPlayer(player1); + tracker.setPlayer(player2); + + // Should handle multiple calls + } + + // ========== Heartbeat Tests ========== + + @Test + public void testStartHeartbeat() { + // Should not throw exception + tracker.startHeartbeat(); + } + + @Test + public void testStopHeartbeat() { + tracker.startHeartbeat(); + + // Should not throw exception + tracker.stopHeartbeat(); + } + + @Test + public void testHeartbeatStartsAfterStart() { + tracker.setPlayer(new Object()); + tracker.sendStart(); + + // Heartbeat should be started automatically + } + + @Test + public void testHeartbeatStopsAfterEnd() { + tracker.setPlayer(new Object()); + tracker.sendStart(); + tracker.sendEnd(); + + // Heartbeat should be stopped automatically + } + + @Test + public void testMultipleStartHeartbeatCalls() { + tracker.startHeartbeat(); + tracker.startHeartbeat(); + tracker.startHeartbeat(); + + // Should handle multiple starts + } + + @Test + public void testMultipleStopHeartbeatCalls() { + tracker.startHeartbeat(); + tracker.stopHeartbeat(); + tracker.stopHeartbeat(); + tracker.stopHeartbeat(); + + // Should handle multiple stops + } + + @Test + public void testStopHeartbeatWithoutStart() { + // Should not throw exception + tracker.stopHeartbeat(); + } + + @Test + public void testStartStopHeartbeatCycle() { + for (int i = 0; i < 5; i++) { + tracker.startHeartbeat(); + tracker.stopHeartbeat(); + } + + // Should handle multiple cycles + } + + // ========== State Transition Tests ========== + + @Test + public void testSendRequest() { + tracker.setPlayer(new Object()); + + // Should not throw exception + tracker.sendRequest(); + } + + @Test + public void testSendStart() { + tracker.setPlayer(new Object()); + tracker.sendRequest(); + + // Should not throw exception + tracker.sendStart(); + } + + @Test + public void testSendPause() { + tracker.setPlayer(new Object()); + tracker.sendRequest(); + tracker.sendStart(); + + // Should not throw exception + tracker.sendPause(); + } + + @Test + public void testSendResume() { + tracker.setPlayer(new Object()); + tracker.sendRequest(); + tracker.sendStart(); + tracker.sendPause(); + + // Should not throw exception + tracker.sendResume(); + } + + @Test + public void testSendEnd() { + tracker.setPlayer(new Object()); + tracker.sendRequest(); + tracker.sendStart(); + + // Should not throw exception + tracker.sendEnd(); + } + + @Test + public void testSendSeekStart() { + tracker.setPlayer(new Object()); + tracker.sendRequest(); + tracker.sendStart(); + + // Should not throw exception + tracker.sendSeekStart(); + } + + @Test + public void testSendSeekEnd() { + tracker.setPlayer(new Object()); + tracker.sendRequest(); + tracker.sendStart(); + tracker.sendSeekStart(); + + // Should not throw exception + tracker.sendSeekEnd(); + } + + @Test + public void testSendBufferStart() { + tracker.setPlayer(new Object()); + tracker.sendRequest(); + tracker.sendStart(); + + // Should not throw exception + tracker.sendBufferStart(); + } + + @Test + public void testSendBufferEnd() { + tracker.setPlayer(new Object()); + tracker.sendRequest(); + tracker.sendStart(); + tracker.sendBufferStart(); + + // Should not throw exception + tracker.sendBufferEnd(); + } + + @Test + public void testCompletePlaybackSequence() { + tracker.setPlayer(new Object()); + tracker.sendRequest(); + tracker.sendStart(); + tracker.sendPause(); + tracker.sendResume(); + tracker.sendEnd(); + + // Complete sequence should work + } + + @Test + public void testPlaybackSequenceWithSeek() { + tracker.setPlayer(new Object()); + tracker.sendRequest(); + tracker.sendStart(); + tracker.sendSeekStart(); + tracker.sendSeekEnd(); + tracker.sendEnd(); + + // Sequence with seek should work + } + + @Test + public void testPlaybackSequenceWithBuffer() { + tracker.setPlayer(new Object()); + tracker.sendRequest(); + tracker.sendStart(); + tracker.sendBufferStart(); + tracker.sendBufferEnd(); + tracker.sendEnd(); + + // Sequence with buffering should work + } + + @Test + public void testMultiplePauseResumeCycles() { + tracker.setPlayer(new Object()); + tracker.sendRequest(); + tracker.sendStart(); + + for (int i = 0; i < 5; i++) { + tracker.sendPause(); + tracker.sendResume(); + } + + tracker.sendEnd(); + + // Multiple pause/resume cycles should work + } + + // ========== Counter Tests ========== + + @Test + public void testNumberOfVideosIncrement() { + tracker.setPlayer(new Object()); + + // First video + tracker.sendRequest(); + tracker.sendStart(); + Map attrs1 = tracker.getAttributes(CONTENT_START, null); + assertEquals("First video should increment counter", 1, attrs1.get("numberOfVideos")); + tracker.sendEnd(); + + // Second video + tracker.sendRequest(); + tracker.sendStart(); + Map attrs2 = tracker.getAttributes(CONTENT_START, null); + assertEquals("Second video should increment counter", 2, attrs2.get("numberOfVideos")); + tracker.sendEnd(); + } + + @Test + public void testSetNumberOfAds() { + tracker.setNumberOfAds(5); + + Map attrs = tracker.getAttributes(CONTENT_START, null); + assertEquals("Number of ads should be set", 5, attrs.get("numberOfAds")); + } + + @Test + public void testViewIdIncrementsAfterEnd() { + tracker.setPlayer(new Object()); + + // First video + tracker.sendRequest(); + tracker.sendStart(); + Map attrs1 = tracker.getAttributes(CONTENT_START, null); + String viewId1 = (String) attrs1.get("viewId"); + assertTrue("First video has viewId ending with -0", viewId1.endsWith("-0")); + tracker.sendEnd(); + + // Second video + tracker.sendRequest(); + tracker.sendStart(); + Map attrs2 = tracker.getAttributes(CONTENT_START, null); + String viewId2 = (String) attrs2.get("viewId"); + assertTrue("Second video has viewId ending with -1", viewId2.endsWith("-1")); + tracker.sendEnd(); + } + + @Test + public void testNumberOfErrorsResetsAfterEnd() { + tracker.setPlayer(new Object()); + tracker.sendRequest(); + tracker.sendStart(); + + // Simulate errors by sending error events + tracker.sendError(new Exception("Test error 1")); + tracker.sendError(new Exception("Test error 2")); + + tracker.sendEnd(); + + // Start new video + tracker.sendRequest(); + tracker.sendStart(); + Map attrs = tracker.getAttributes(CONTENT_START, null); + + assertEquals("Number of errors should reset after end", 0, attrs.get("numberOfErrors")); + } + + // ========== Playback Metrics Tests ========== + + @Test + public void testUpdatePlaytime() { + tracker.setPlayer(new Object()); + tracker.sendRequest(); + tracker.sendStart(); + + // Simulate some time passing + try { Thread.sleep(100); } catch (InterruptedException e) {} + + tracker.updatePlaytime(); + + Map attrs = tracker.getAttributes(CONTENT_START, null); + Long totalPlaytime = (Long) attrs.get("totalPlaytime"); + + assertTrue("Total playtime should be updated", totalPlaytime > 0); + } + + @Test + public void testTotalPlaytimeResetsAfterEnd() { + tracker.setPlayer(new Object()); + tracker.sendRequest(); + tracker.sendStart(); + + try { Thread.sleep(100); } catch (InterruptedException e) {} + tracker.updatePlaytime(); + + tracker.sendEnd(); + + // Start new video + tracker.sendRequest(); + tracker.sendStart(); + Map attrs = tracker.getAttributes(CONTENT_START, null); + + assertEquals("Total playtime should reset", Long.valueOf(0L), attrs.get("totalPlaytime")); + } + + // ========== Attribute Tests ========== + + @Test + public void testGetAttributesWithNullAttributes() { + Map attrs = tracker.getAttributes(CONTENT_START, null); + + assertNotNull("Attributes should be generated", attrs); + assertFalse("Attributes should not be empty", attrs.isEmpty()); + } + + @Test + public void testGetAttributesWithCustomAttributes() { + Map customAttrs = new java.util.HashMap<>(); + customAttrs.put("customKey", "customValue"); + + Map attrs = tracker.getAttributes(CONTENT_START, customAttrs); + + assertNotNull("Attributes should include custom attributes", attrs); + assertEquals("Custom attribute should be present", "customValue", attrs.get("customKey")); + } + + @Test + public void testGetAttributesIncludesTrackerInfo() { + Map attrs = tracker.getAttributes(CONTENT_START, null); + + assertTrue("Should include tracker name", attrs.containsKey("trackerName")); + assertTrue("Should include tracker version", attrs.containsKey("trackerVersion")); + assertTrue("Should include tracker src", attrs.containsKey("src")); + } + + @Test + public void testGetAttributesIncludesPlayerInfo() { + Map attrs = tracker.getAttributes(CONTENT_START, null); + + assertTrue("Should include player name", attrs.containsKey("playerName")); + assertTrue("Should include player version", attrs.containsKey("playerVersion")); + } + + @Test + public void testGetAttributesIncludesSessionInfo() { + Map attrs = tracker.getAttributes(CONTENT_START, null); + + assertNotNull("Should include view session", attrs.get("viewSession")); + assertNotNull("Should include view ID", attrs.get("viewId")); + } + + @Test + public void testGetAttributesIncludesCounters() { + Map attrs = tracker.getAttributes(CONTENT_START, null); + + assertNotNull("Should include number of ads", attrs.get("numberOfAds")); + assertNotNull("Should include number of videos", attrs.get("numberOfVideos")); + assertNotNull("Should include number of errors", attrs.get("numberOfErrors")); + } + + @Test + public void testGetAttributesIncludesContentInfo() { + Map attrs = tracker.getAttributes(CONTENT_START, null); + + // Content-specific attributes + assertTrue("Should include content attributes", + attrs.containsKey("contentTitle") || + attrs.containsKey("contentDuration") || + attrs.containsKey("contentPlayhead")); + } + + @Test + public void testGetAttributesForBufferEvents() { + tracker.setPlayer(new Object()); + tracker.sendRequest(); + tracker.sendStart(); + tracker.sendBufferStart(); + + Map attrs = tracker.getAttributes(CONTENT_BUFFER_START, null); + + assertTrue("Buffer events should include buffer type", + attrs.containsKey("bufferType")); + } + + // ========== Dispose Tests ========== + + @Test + public void testDispose() { + tracker.startHeartbeat(); + + // Should not throw exception + tracker.dispose(); + } + + @Test + public void testDisposeStopsHeartbeat() { + tracker.startHeartbeat(); + tracker.dispose(); + + // Heartbeat should be stopped + } + + @Test + public void testMultipleDisposeCalls() { + tracker.dispose(); + tracker.dispose(); + tracker.dispose(); + + // Should handle multiple dispose calls + } + + // ========== Error Handling Tests ========== + + @Test + public void testSendError() { + tracker.setPlayer(new Object()); + tracker.sendRequest(); + tracker.sendStart(); + + Exception testException = new Exception("Test error"); + + // Should not throw exception + tracker.sendError(testException); + } + + @Test + public void testSendErrorWithNullMessage() { + tracker.setPlayer(new Object()); + tracker.sendRequest(); + tracker.sendStart(); + + // Should not throw exception - sendError handles null message + tracker.sendError(0, null); + } + + @Test + public void testSendMultipleErrors() { + tracker.setPlayer(new Object()); + tracker.sendRequest(); + tracker.sendStart(); + + for (int i = 0; i < 5; i++) { + tracker.sendError(new Exception("Error " + i)); + } + + // Should handle multiple errors + } + + @Test + public void testSendErrorWithCodeAndMessage() { + tracker.setPlayer(new Object()); + tracker.sendRequest(); + tracker.sendStart(); + + int errorCode = 500; + String errorMessage = "Custom error message"; + + // Should not throw exception + tracker.sendError(errorCode, errorMessage); + } + + // ========== State Tests ========== + + @Test + public void testGetState() { + NRTrackerState state = tracker.getState(); + + assertNotNull("State should not be null", state); + } + + @Test + public void testStateIsSharedReference() { + NRTrackerState state1 = tracker.getState(); + NRTrackerState state2 = tracker.getState(); + + assertSame("State should return same reference", state1, state2); + } + + @Test + public void testStateUpdatesReflectInTracker() { + NRTrackerState state = tracker.getState(); + + tracker.setPlayer(new Object()); + + assertTrue("State changes should be reflected", state.isPlayerReady); + } + + // ========== Edge Cases ========== + + @Test + public void testSendStartWithoutPlayerReady() { + // Try to start without setting player + tracker.sendStart(); + + // Should handle gracefully + } + + @Test + public void testSendPauseWithoutStart() { + tracker.setPlayer(new Object()); + tracker.sendRequest(); + + // Try to pause without starting + tracker.sendPause(); + + // Should handle gracefully + } + + @Test + public void testSendEndWithoutStart() { + tracker.setPlayer(new Object()); + tracker.sendRequest(); + + // Try to end without starting + tracker.sendEnd(); + + // Should handle gracefully + } + + @Test + public void testRapidStateTransitions() { + tracker.setPlayer(new Object()); + tracker.sendRequest(); + tracker.sendStart(); + + // Rapid transitions + for (int i = 0; i < 10; i++) { + tracker.sendPause(); + tracker.sendResume(); + } + + tracker.sendEnd(); + + // Should handle rapid transitions + } + + @Test + public void testGetAttributesForDifferentActions() { + String[] actions = { + CONTENT_START, CONTENT_PAUSE, CONTENT_RESUME, CONTENT_END, + CONTENT_SEEK_START, CONTENT_SEEK_END, + CONTENT_BUFFER_START, CONTENT_BUFFER_END, + CONTENT_REQUEST, PLAYER_READY + }; + + for (String action : actions) { + Map attrs = tracker.getAttributes(action, null); + assertNotNull("Attributes should be generated for " + action, attrs); + assertFalse("Attributes should not be empty for " + action, attrs.isEmpty()); + } + } + + @Test + public void testAttributesPersistCustomValues() { + Map customAttrs = new java.util.HashMap<>(); + customAttrs.put("customKey1", "value1"); + customAttrs.put("customKey2", 42); + customAttrs.put("customKey3", true); + + Map attrs = tracker.getAttributes(CONTENT_START, customAttrs); + + assertEquals("Custom string should persist", "value1", attrs.get("customKey1")); + assertEquals("Custom integer should persist", 42, attrs.get("customKey2")); + assertEquals("Custom boolean should persist", true, attrs.get("customKey3")); + } + + // ========== Concurrency Tests ========== + + @Test + public void testConcurrentStateTransitions() throws InterruptedException { + tracker.setPlayer(new Object()); + tracker.sendRequest(); + tracker.sendStart(); + + Thread thread1 = new Thread(() -> { + for (int i = 0; i < 50; i++) { + tracker.sendPause(); + try { Thread.sleep(5); } catch (InterruptedException e) {} + } + }); + + Thread thread2 = new Thread(() -> { + for (int i = 0; i < 50; i++) { + tracker.sendResume(); + try { Thread.sleep(5); } catch (InterruptedException e) {} + } + }); + + thread1.start(); + thread2.start(); + + thread1.join(); + thread2.join(); + + // Should handle concurrent transitions + } + + @Test + public void testConcurrentAttributeAccess() throws InterruptedException { + tracker.setPlayer(new Object()); + tracker.sendRequest(); + tracker.sendStart(); + + Thread[] threads = new Thread[5]; + + for (int i = 0; i < 5; i++) { + threads[i] = new Thread(() -> { + for (int j = 0; j < 100; j++) { + Map attrs = tracker.getAttributes(CONTENT_START, null); + assertNotNull("Attributes should not be null", attrs); + } + }); + threads[i].start(); + } + + for (Thread thread : threads) { + thread.join(); + } + + // Should handle concurrent attribute access + } +} diff --git a/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/util/JsonStreamUtilTest.java b/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/util/JsonStreamUtilTest.java new file mode 100644 index 00000000..be35c059 --- /dev/null +++ b/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/util/JsonStreamUtilTest.java @@ -0,0 +1,182 @@ +package com.newrelic.videoagent.core.util; + +import org.junit.Test; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.*; + +/** + * Unit tests for JsonStreamUtil. + */ +public class JsonStreamUtilTest { + + @Test + public void testStreamJsonToOutputStreamWithSimpleList() throws IOException { + List list = new ArrayList<>(); + Map item = new HashMap<>(); + item.put("key", "value"); + list.add(item); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + JsonStreamUtil.streamJsonToOutputStream(list, outputStream); + + String result = outputStream.toString(); + assertNotNull(result); + assertTrue(result.contains("key")); + assertTrue(result.contains("value")); + } + + @Test + public void testStreamJsonToOutputStreamWithEmptyList() throws IOException { + List list = new ArrayList<>(); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + JsonStreamUtil.streamJsonToOutputStream(list, outputStream); + + String result = outputStream.toString(); + assertNotNull(result); + assertEquals("[]", result); + } + + @Test + public void testStreamJsonToOutputStreamWithMultipleItems() throws IOException { + List list = new ArrayList<>(); + + Map item1 = new HashMap<>(); + item1.put("id", 1); + item1.put("name", "Item1"); + list.add(item1); + + Map item2 = new HashMap<>(); + item2.put("id", 2); + item2.put("name", "Item2"); + list.add(item2); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + JsonStreamUtil.streamJsonToOutputStream(list, outputStream); + + String result = outputStream.toString(); + assertNotNull(result); + assertTrue(result.contains("Item1")); + assertTrue(result.contains("Item2")); + assertTrue(result.startsWith("[")); + assertTrue(result.endsWith("]")); + } + + @Test + public void testStreamMapToOutputStreamWithSimpleMap() throws IOException { + Map map = new HashMap<>(); + map.put("key1", "value1"); + map.put("key2", 42); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + JsonStreamUtil.streamMapToOutputStream(map, outputStream); + + String result = outputStream.toString(); + assertNotNull(result); + assertTrue(result.contains("key1")); + assertTrue(result.contains("value1")); + assertTrue(result.contains("key2")); + assertTrue(result.contains("42")); + } + + @Test + public void testStreamMapToOutputStreamWithEmptyMap() throws IOException { + Map map = new HashMap<>(); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + JsonStreamUtil.streamMapToOutputStream(map, outputStream); + + String result = outputStream.toString(); + assertNotNull(result); + assertEquals("{}", result); + } + + @Test + public void testStreamJsonToStringWithSimpleList() throws IOException { + List list = new ArrayList<>(); + Map item = new HashMap<>(); + item.put("key", "value"); + list.add(item); + + String result = JsonStreamUtil.streamJsonToString(list); + + assertNotNull(result); + assertTrue(result.contains("key")); + assertTrue(result.contains("value")); + } + + @Test + public void testStreamJsonToStringWithEmptyList() throws IOException { + List list = new ArrayList<>(); + + String result = JsonStreamUtil.streamJsonToString(list); + + assertNotNull(result); + assertEquals("[]", result); + } + + @Test + public void testStreamMapWithNestedMap() throws IOException { + Map outerMap = new HashMap<>(); + Map innerMap = new HashMap<>(); + innerMap.put("innerKey", "innerValue"); + outerMap.put("nested", innerMap); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + JsonStreamUtil.streamMapToOutputStream(outerMap, outputStream); + + String result = outputStream.toString(); + assertNotNull(result); + assertTrue(result.contains("nested")); + assertTrue(result.contains("innerKey")); + assertTrue(result.contains("innerValue")); + } + + @Test + public void testStreamListWithMixedTypes() throws IOException { + List list = new ArrayList<>(); + Map item = new HashMap<>(); + item.put("stringValue", "text"); + item.put("numberValue", 42); + item.put("booleanValue", true); + item.put("nullValue", null); + list.add(item); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + JsonStreamUtil.streamJsonToOutputStream(list, outputStream); + + String result = outputStream.toString(); + assertNotNull(result); + assertTrue(result.contains("stringValue")); + assertTrue(result.contains("text")); + assertTrue(result.contains("42")); + assertTrue(result.contains("true")); + assertTrue(result.contains("null")); + } + + @Test + public void testStreamJsonWithLargeList() throws IOException { + List list = new ArrayList<>(); + for (int i = 0; i < 100; i++) { + Map item = new HashMap<>(); + item.put("id", i); + item.put("name", "Item" + i); + list.add(item); + } + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + JsonStreamUtil.streamJsonToOutputStream(list, outputStream); + + String result = outputStream.toString(); + assertNotNull(result); + assertTrue(result.contains("Item0")); + assertTrue(result.contains("Item99")); + } +} diff --git a/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/utils/NRLogTest.java b/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/utils/NRLogTest.java new file mode 100644 index 00000000..870f14f1 --- /dev/null +++ b/NewRelicVideoCore/src/test/java/com/newrelic/videoagent/core/utils/NRLogTest.java @@ -0,0 +1,229 @@ +package com.newrelic.videoagent.core.utils; + +import android.util.Log; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.shadows.ShadowLog; + +import static org.junit.Assert.*; + +/** + * Unit tests for NRLog that exercise actual production code. + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = 28, manifest = Config.NONE) +public class NRLogTest { + + @Before + public void setUp() { + ShadowLog.clear(); + NRLog.disable(); // Start with logging disabled + } + + @After + public void tearDown() { + NRLog.disable(); // Clean up after each test + } + + @Test + public void testLoggingDisabledByDefault() { + NRLog.d("Debug message"); + + assertEquals(0, ShadowLog.getLogs().size()); + } + + @Test + public void testEnableLogging() { + NRLog.enable(); + NRLog.d("Debug message"); + + assertTrue(ShadowLog.getLogs().size() > 0); + } + + @Test + public void testDisableLogging() { + NRLog.enable(); + NRLog.d("First message"); + + NRLog.disable(); + ShadowLog.clear(); + NRLog.d("Second message"); + + assertEquals(0, ShadowLog.getLogs().size()); + } + + @Test + public void testDebugLogging() { + NRLog.enable(); + NRLog.d("Debug message"); + + assertEquals(1, ShadowLog.getLogs().size()); + assertEquals(Log.DEBUG, ShadowLog.getLogs().get(0).type); + assertEquals("NRVideo", ShadowLog.getLogs().get(0).tag); + assertEquals("Debug message", ShadowLog.getLogs().get(0).msg); + } + + @Test + public void testInfoLogging() { + NRLog.enable(); + NRLog.i("Info message"); + + assertEquals(1, ShadowLog.getLogs().size()); + assertEquals(Log.INFO, ShadowLog.getLogs().get(0).type); + assertEquals("NRVideo", ShadowLog.getLogs().get(0).tag); + assertEquals("Info message", ShadowLog.getLogs().get(0).msg); + } + + @Test + public void testErrorLogging() { + NRLog.enable(); + NRLog.e("Error message"); + + assertEquals(1, ShadowLog.getLogs().size()); + assertEquals(Log.ERROR, ShadowLog.getLogs().get(0).type); + assertEquals("NRVideo", ShadowLog.getLogs().get(0).tag); + assertEquals("Error message", ShadowLog.getLogs().get(0).msg); + } + + @Test + public void testErrorLoggingWithException() { + NRLog.enable(); + Exception exception = new Exception("Test exception"); + NRLog.e("Error with exception", exception); + + assertEquals(1, ShadowLog.getLogs().size()); + assertEquals(Log.ERROR, ShadowLog.getLogs().get(0).type); + assertEquals("NRVideo", ShadowLog.getLogs().get(0).tag); + assertEquals("Error with exception", ShadowLog.getLogs().get(0).msg); + assertNotNull(ShadowLog.getLogs().get(0).throwable); + } + + @Test + public void testWarningLogging() { + NRLog.enable(); + NRLog.w("Warning message"); + + assertEquals(1, ShadowLog.getLogs().size()); + assertEquals(Log.WARN, ShadowLog.getLogs().get(0).type); + assertEquals("NRVideo", ShadowLog.getLogs().get(0).tag); + assertEquals("Warning message", ShadowLog.getLogs().get(0).msg); + } + + @Test + public void testMultipleLogMessages() { + NRLog.enable(); + NRLog.d("Debug"); + NRLog.i("Info"); + NRLog.w("Warning"); + NRLog.e("Error"); + + assertEquals(4, ShadowLog.getLogs().size()); + } + + @Test + public void testNoLogsWhenDisabled() { + NRLog.d("Debug"); + NRLog.i("Info"); + NRLog.w("Warning"); + NRLog.e("Error"); + + assertEquals(0, ShadowLog.getLogs().size()); + } + + @Test + public void testEnableDisableToggle() { + NRLog.enable(); + NRLog.d("Message 1"); + + NRLog.disable(); + ShadowLog.clear(); + NRLog.d("Message 2"); + + NRLog.enable(); + NRLog.d("Message 3"); + + assertEquals(1, ShadowLog.getLogs().size()); + assertEquals("Message 3", ShadowLog.getLogs().get(0).msg); + } + + @Test + public void testLogWithNullMessage() { + NRLog.enable(); + NRLog.d(null); + + assertEquals(1, ShadowLog.getLogs().size()); + assertNull(ShadowLog.getLogs().get(0).msg); + } + + @Test + public void testLogWithEmptyMessage() { + NRLog.enable(); + NRLog.d(""); + + assertEquals(1, ShadowLog.getLogs().size()); + assertEquals("", ShadowLog.getLogs().get(0).msg); + } + + @Test + public void testLogWithLongMessage() { + NRLog.enable(); + String longMessage = "This is a very long message that contains a lot of text to test if the logging handles long strings properly without any issues."; + NRLog.d(longMessage); + + assertEquals(1, ShadowLog.getLogs().size()); + assertEquals(longMessage, ShadowLog.getLogs().get(0).msg); + } + + @Test + public void testErrorWithNullException() { + NRLog.enable(); + NRLog.e("Error message", null); + + assertEquals(1, ShadowLog.getLogs().size()); + assertEquals("Error message", ShadowLog.getLogs().get(0).msg); + } + + @Test + public void testAllLogLevelsWithLoggingEnabled() { + NRLog.enable(); + + NRLog.d("Debug"); + NRLog.i("Info"); + NRLog.w("Warning"); + NRLog.e("Error"); + NRLog.e("Error with exception", new Exception()); + + assertEquals(5, ShadowLog.getLogs().size()); + assertEquals(Log.DEBUG, ShadowLog.getLogs().get(0).type); + assertEquals(Log.INFO, ShadowLog.getLogs().get(1).type); + assertEquals(Log.WARN, ShadowLog.getLogs().get(2).type); + assertEquals(Log.ERROR, ShadowLog.getLogs().get(3).type); + assertEquals(Log.ERROR, ShadowLog.getLogs().get(4).type); + } + + @Test + public void testConsecutiveEnableCalls() { + NRLog.enable(); + NRLog.enable(); + NRLog.enable(); + NRLog.d("Message"); + + assertEquals(1, ShadowLog.getLogs().size()); + } + + @Test + public void testConsecutiveDisableCalls() { + NRLog.enable(); + NRLog.disable(); + NRLog.disable(); + NRLog.disable(); + NRLog.d("Message"); + + assertEquals(0, ShadowLog.getLogs().size()); + } +} diff --git a/NewRelicVideoCore/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/NewRelicVideoCore/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 00000000..1f0955d4 --- /dev/null +++ b/NewRelicVideoCore/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline