From 3e0fd1b8ec236d9ba391adff59083160d9990f54 Mon Sep 17 00:00:00 2001 From: Hannes Achleitner Date: Thu, 11 Jul 2024 17:34:03 +0200 Subject: [PATCH 1/5] Kotlin MatomoTest --- .../test/java/org/matomo/sdk/MatomoTest.java | 132 ------------------ .../test/java/org/matomo/sdk/MatomoTest.kt | 119 ++++++++++++++++ 2 files changed, 119 insertions(+), 132 deletions(-) delete mode 100644 tracker/src/test/java/org/matomo/sdk/MatomoTest.java create mode 100644 tracker/src/test/java/org/matomo/sdk/MatomoTest.kt diff --git a/tracker/src/test/java/org/matomo/sdk/MatomoTest.java b/tracker/src/test/java/org/matomo/sdk/MatomoTest.java deleted file mode 100644 index 4d1b0871..00000000 --- a/tracker/src/test/java/org/matomo/sdk/MatomoTest.java +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Android SDK for Matomo - * - * @link https://github.com/matomo-org/matomo-android-sdk - * @license https://github.com/matomo-org/matomo-sdk-android/blob/master/LICENSE BSD-3 Clause - */ - -package org.matomo.sdk; - -import android.annotation.SuppressLint; -import android.app.Application; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.matomo.sdk.dispatcher.DefaultDispatcher; -import org.matomo.sdk.dispatcher.DefaultDispatcherFactory; -import org.matomo.sdk.dispatcher.Dispatcher; -import org.matomo.sdk.dispatcher.DispatcherFactory; -import org.matomo.sdk.dispatcher.EventCache; -import org.matomo.sdk.dispatcher.EventDiskCache; -import org.matomo.sdk.dispatcher.Packet; -import org.matomo.sdk.dispatcher.PacketFactory; -import org.matomo.sdk.dispatcher.PacketSender; -import org.matomo.sdk.extra.TrackHelper; -import org.matomo.sdk.tools.Connectivity; -import org.robolectric.annotation.Config; - -import androidx.test.core.app.ApplicationProvider; - -import testhelpers.BaseTest; -import testhelpers.FullEnvTestRunner; -import testhelpers.MatomoTestApplication; - -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.not; -import static org.hamcrest.Matchers.nullValue; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertNotNull; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.timeout; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - - -@Config(sdk = 28, manifest = Config.NONE, application = MatomoTestApplication.class) -@RunWith(FullEnvTestRunner.class) -public class MatomoTest extends BaseTest { - - @Test - public void testNewTracker() { - MatomoTestApplication app = ApplicationProvider.getApplicationContext(); - Tracker tracker = app.onCreateTrackerConfig().build(Matomo.getInstance(ApplicationProvider.getApplicationContext())); - assertNotNull(tracker); - assertEquals(app.onCreateTrackerConfig().getApiUrl(), tracker.getAPIUrl()); - assertEquals(app.onCreateTrackerConfig().getSiteId(), tracker.getSiteId()); - } - - @Test - public void testNormalTracker() { - Matomo matomo = Matomo.getInstance(ApplicationProvider.getApplicationContext()); - Tracker tracker = new TrackerBuilder("http://test/matomo.php", 1, "Default Tracker").build(matomo); - assertEquals("http://test/matomo.php", tracker.getAPIUrl()); - assertEquals(1, tracker.getSiteId()); - } - - @Test - public void testTrackerNaming() { - // TODO can we somehow detect naming collisions on tracker creation? - // Would probably requiring us to track created trackers - } - - @SuppressLint("InlinedApi") - @Test - public void testLowMemoryDispatch() { - MatomoTestApplication app = ApplicationProvider.getApplicationContext(); - final PacketSender packetSender = mock(PacketSender.class); - app.getMatomo().setDispatcherFactory(new DefaultDispatcherFactory() { - @Override - public Dispatcher build(Tracker tracker) { - return new DefaultDispatcher( - new EventCache(new EventDiskCache(tracker)), - new Connectivity(tracker.getMatomo().getContext()), - new PacketFactory(tracker.getAPIUrl()), - packetSender - ); - } - }); - Tracker tracker = app.getTracker(); - assertNotNull(tracker); - tracker.setDispatchInterval(-1); - - tracker.track(TrackHelper.track().screen("test").build()); - tracker.dispatch(); - verify(packetSender, timeout(500).times(1)).send(any(Packet.class)); - - tracker.track(TrackHelper.track().screen("test").build()); - verify(packetSender, timeout(500).times(1)).send(any(Packet.class)); - - app.onTrimMemory(Application.TRIM_MEMORY_UI_HIDDEN); - verify(packetSender, timeout(500).atLeast(2)).send(any(Packet.class)); - } - - @Test - public void testGetSettings() { - Tracker tracker1 = mock(Tracker.class); - when(tracker1.getName()).thenReturn("1"); - Tracker tracker2 = mock(Tracker.class); - when(tracker2.getName()).thenReturn("2"); - Tracker tracker3 = mock(Tracker.class); - when(tracker3.getName()).thenReturn("1"); - - final Matomo matomo = Matomo.getInstance(ApplicationProvider.getApplicationContext()); - assertEquals(matomo.getTrackerPreferences(tracker1), matomo.getTrackerPreferences(tracker1)); - assertNotEquals(matomo.getTrackerPreferences(tracker1), matomo.getTrackerPreferences(tracker2)); - assertEquals(matomo.getTrackerPreferences(tracker1), matomo.getTrackerPreferences(tracker3)); - } - - @Test - public void testSetDispatcherFactory() { - final Matomo matomo = Matomo.getInstance(ApplicationProvider.getApplicationContext()); - Dispatcher dispatcher = mock(Dispatcher.class); - DispatcherFactory factory = mock(DispatcherFactory.class); - when(factory.build(any(Tracker.class))).thenReturn(dispatcher); - assertThat(matomo.getDispatcherFactory(), is(not(nullValue()))); - matomo.setDispatcherFactory(factory); - assertThat(matomo.getDispatcherFactory(), is(factory)); - } - -} diff --git a/tracker/src/test/java/org/matomo/sdk/MatomoTest.kt b/tracker/src/test/java/org/matomo/sdk/MatomoTest.kt new file mode 100644 index 00000000..e7df4e3c --- /dev/null +++ b/tracker/src/test/java/org/matomo/sdk/MatomoTest.kt @@ -0,0 +1,119 @@ +/* + * Android SDK for Matomo + * + * @link https://github.com/matomo-org/matomo-android-sdk + * @license https://github.com/matomo-org/matomo-sdk-android/blob/master/LICENSE BSD-3 Clause + */ +package org.matomo.sdk + +import android.annotation.SuppressLint +import android.app.Application +import androidx.test.core.app.ApplicationProvider +import org.hamcrest.MatcherAssert +import org.hamcrest.Matchers +import org.junit.Assert +import org.junit.Test +import org.junit.runner.RunWith +import org.matomo.sdk.Matomo.Companion.getInstance +import org.matomo.sdk.dispatcher.DefaultDispatcher +import org.matomo.sdk.dispatcher.DefaultDispatcherFactory +import org.matomo.sdk.dispatcher.Dispatcher +import org.matomo.sdk.dispatcher.DispatcherFactory +import org.matomo.sdk.dispatcher.EventCache +import org.matomo.sdk.dispatcher.EventDiskCache +import org.matomo.sdk.dispatcher.Packet +import org.matomo.sdk.dispatcher.PacketFactory +import org.matomo.sdk.dispatcher.PacketSender +import org.matomo.sdk.extra.TrackHelper +import org.matomo.sdk.tools.Connectivity +import org.mockito.ArgumentMatchers +import org.mockito.Mockito +import org.robolectric.annotation.Config +import testhelpers.BaseTest +import testhelpers.FullEnvTestRunner +import testhelpers.MatomoTestApplication + +@Config(sdk = [28], manifest = Config.NONE, application = MatomoTestApplication::class) +@RunWith( + FullEnvTestRunner::class +) +class MatomoTest : BaseTest() { + @Test + fun testNewTracker() { + val app = ApplicationProvider.getApplicationContext() + val tracker = app.onCreateTrackerConfig().build(getInstance(ApplicationProvider.getApplicationContext())) + Assert.assertNotNull(tracker) + Assert.assertEquals(app.onCreateTrackerConfig().apiUrl, tracker.apiUrl) + Assert.assertEquals(app.onCreateTrackerConfig().siteId.toLong(), tracker.siteId.toLong()) + } + + @Test + fun testNormalTracker() { + val matomo = getInstance(ApplicationProvider.getApplicationContext()) + val tracker = TrackerBuilder("http://test/matomo.php", 1, "Default Tracker").build(matomo) + Assert.assertEquals("http://test/matomo.php", tracker.apiUrl) + Assert.assertEquals(1, tracker.siteId.toLong()) + } + + @Test + fun testTrackerNaming() { + // TODO can we somehow detect naming collisions on tracker creation? + // Would probably requiring us to track created trackers + } + + @SuppressLint("InlinedApi") + @Test + fun testLowMemoryDispatch() { + val app = ApplicationProvider.getApplicationContext() + val packetSender = Mockito.mock(PacketSender::class.java) + app.matomo.dispatcherFactory = object : DefaultDispatcherFactory() { + override fun build(tracker: Tracker): Dispatcher { + return DefaultDispatcher( + EventCache(EventDiskCache(tracker)), + Connectivity(tracker.matomo.context), + PacketFactory(tracker.apiUrl), + packetSender + ) + } + } + val tracker = app.tracker + Assert.assertNotNull(tracker) + tracker.setDispatchInterval(-1) + + tracker.track(TrackHelper.track().screen("test").build()) + tracker.dispatch() + Mockito.verify(packetSender, Mockito.timeout(500).times(1)).send(ArgumentMatchers.any(Packet::class.java)) + + tracker.track(TrackHelper.track().screen("test").build()) + Mockito.verify(packetSender, Mockito.timeout(500).times(1)).send(ArgumentMatchers.any(Packet::class.java)) + + app.onTrimMemory(Application.TRIM_MEMORY_UI_HIDDEN) + Mockito.verify(packetSender, Mockito.timeout(500).atLeast(2)).send(ArgumentMatchers.any(Packet::class.java)) + } + + @Test + fun testGetSettings() { + val tracker1 = Mockito.mock(Tracker::class.java) + Mockito.`when`(tracker1.name).thenReturn("1") + val tracker2 = Mockito.mock(Tracker::class.java) + Mockito.`when`(tracker2.name).thenReturn("2") + val tracker3 = Mockito.mock(Tracker::class.java) + Mockito.`when`(tracker3.name).thenReturn("1") + + val matomo = getInstance(ApplicationProvider.getApplicationContext()) + Assert.assertEquals(matomo!!.getTrackerPreferences(tracker1), matomo.getTrackerPreferences(tracker1)) + Assert.assertNotEquals(matomo.getTrackerPreferences(tracker1), matomo.getTrackerPreferences(tracker2)) + Assert.assertEquals(matomo.getTrackerPreferences(tracker1), matomo.getTrackerPreferences(tracker3)) + } + + @Test + fun testSetDispatcherFactory() { + val matomo = getInstance(ApplicationProvider.getApplicationContext()) + val dispatcher = Mockito.mock(Dispatcher::class.java) + val factory = Mockito.mock(DispatcherFactory::class.java) + Mockito.`when`(factory.build(ArgumentMatchers.any(Tracker::class.java))).thenReturn(dispatcher) + MatcherAssert.assertThat(matomo!!.dispatcherFactory, Matchers.`is`(Matchers.not(Matchers.nullValue()))) + matomo.dispatcherFactory = factory + MatcherAssert.assertThat(matomo.dispatcherFactory, Matchers.`is`(factory)) + } +} From 347417f593ad639fcd1aa689ad78a8e62cd213a3 Mon Sep 17 00:00:00 2001 From: Hannes Achleitner Date: Thu, 11 Jul 2024 18:00:43 +0200 Subject: [PATCH 2/5] Kotlin DefaultDispatcherTest --- .../sdk/dispatcher/DefaultDispatcherTest.java | 494 ----------------- .../sdk/dispatcher/DefaultDispatcherTest.kt | 496 ++++++++++++++++++ 2 files changed, 496 insertions(+), 494 deletions(-) delete mode 100644 tracker/src/test/java/org/matomo/sdk/dispatcher/DefaultDispatcherTest.java create mode 100644 tracker/src/test/java/org/matomo/sdk/dispatcher/DefaultDispatcherTest.kt diff --git a/tracker/src/test/java/org/matomo/sdk/dispatcher/DefaultDispatcherTest.java b/tracker/src/test/java/org/matomo/sdk/dispatcher/DefaultDispatcherTest.java deleted file mode 100644 index 62fd8a94..00000000 --- a/tracker/src/test/java/org/matomo/sdk/dispatcher/DefaultDispatcherTest.java +++ /dev/null @@ -1,494 +0,0 @@ -/* - * Android SDK for Matomo - * - * @link https://github.com/matomo-org/matomo-android-sdk - * @license https://github.com/matomo-org/matomo-sdk-android/blob/master/LICENSE BSD-3 Clause - */ -package org.matomo.sdk.dispatcher; - -import org.json.JSONArray; -import org.junit.Before; -import org.junit.Test; -import org.matomo.sdk.QueryParams; -import org.matomo.sdk.TrackMe; -import org.matomo.sdk.tools.Connectivity; -import org.mockito.ArgumentMatchers; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.mockito.stubbing.Answer; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Random; -import java.util.UUID; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.Semaphore; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; - -import testhelpers.BaseTest; -import testhelpers.TestHelper; - -import static org.awaitility.Awaitility.await; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.core.Is.is; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.timeout; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - - -public class DefaultDispatcherTest extends BaseTest { - - DefaultDispatcher mDispatcher; - @Mock EventCache mEventCache; - @Mock PacketSender mPacketSender; - @Mock Connectivity mConnectivity; - final String mApiUrl = "http://example.com"; - - final LinkedBlockingQueue mEventCacheData = new LinkedBlockingQueue<>(); - - @Before - public void setup() throws Exception { - super.setup(); - MockitoAnnotations.openMocks(this); - when(mConnectivity.isConnected()).thenReturn(true); - when(mConnectivity.getType()).thenReturn(Connectivity.Type.MOBILE); - - doAnswer(invocation -> { - mEventCacheData.add(invocation.getArgument(0)); - return null; - }).when(mEventCache).add(any(Event.class)); - when(mEventCache.isEmpty()).then((Answer) invocation -> mEventCacheData.isEmpty()); - when(mEventCache.updateState(anyBoolean())).thenAnswer(invocation -> (Boolean) invocation.getArgument(0) && !mEventCacheData.isEmpty()); - doAnswer(invocation -> { - List drainTarget = invocation.getArgument(0); - mEventCacheData.drainTo(drainTarget); - return null; - }).when(mEventCache).drainTo(ArgumentMatchers.anyList()); - doAnswer(invocation -> { - List toRequeue = invocation.getArgument(0); - mEventCacheData.addAll(toRequeue); - return null; - }).when(mEventCache).requeue(ArgumentMatchers.anyList()); - doAnswer(invocation -> { - mEventCacheData.clear(); - return null; - }).when(mEventCache).clear(); - mDispatcher = new DefaultDispatcher(mEventCache, mConnectivity, new PacketFactory(mApiUrl), mPacketSender); - } - - @Test - public void testClear() { - mDispatcher.clear(); - verify(mEventCache).clear(); - } - - @Test - public void testClear_cleanExit() { - List dryRunData = Collections.synchronizedList(new ArrayList<>()); - mDispatcher.setDryRunTarget(dryRunData); - mDispatcher.submit(getTestEvent()); - mDispatcher.forceDispatch(); - - TestHelper.sleep(100); - assertThat(dryRunData.size(), is(1)); - dryRunData.clear(); - - when(mConnectivity.isConnected()).thenReturn(false); - mDispatcher.submit(getTestEvent()); - - TestHelper.sleep(100); - assertThat(mEventCacheData.size(), is(1)); - - mDispatcher.clear(); - - when(mConnectivity.isConnected()).thenReturn(true); - mDispatcher.forceDispatch(); - - TestHelper.sleep(100); - assertThat(dryRunData.size(), is(0)); - } - - @Test - public void testGetDispatchMode() { - assertEquals(DispatchMode.ALWAYS, mDispatcher.getDispatchMode()); - mDispatcher.setDispatchMode(DispatchMode.WIFI_ONLY); - assertEquals(DispatchMode.WIFI_ONLY, mDispatcher.getDispatchMode()); - } - - @Test - public void testDispatchMode_wifiOnly() { - List dryRunData = Collections.synchronizedList(new ArrayList<>()); - mDispatcher.setDryRunTarget(dryRunData); - when(mConnectivity.getType()).thenReturn(Connectivity.Type.MOBILE); - - mDispatcher.setDispatchMode(DispatchMode.WIFI_ONLY); - mDispatcher.submit(getTestEvent()); - mDispatcher.forceDispatch(); - - verify(mEventCache, timeout(1000)).updateState(false); - verify(mEventCache, never()).drainTo(ArgumentMatchers.anyList()); - - when(mConnectivity.getType()).thenReturn(Connectivity.Type.WIFI); - mDispatcher.forceDispatch(); - await().atMost(1, TimeUnit.SECONDS).until(dryRunData::size, is(1)); - - verify(mEventCache).updateState(true); - verify(mEventCache).drainTo(ArgumentMatchers.anyList()); - } - - @Test - public void testConnectivityChange() { - List dryRunData = Collections.synchronizedList(new ArrayList<>()); - mDispatcher.setDryRunTarget(dryRunData); - when(mConnectivity.isConnected()).thenReturn(false); - - mDispatcher.submit(getTestEvent()); - mDispatcher.forceDispatch(); - - verify(mEventCache, timeout(1000)).add(any()); - verify(mEventCache, never()).drainTo(ArgumentMatchers.anyList()); - assertThat(dryRunData.size(), is(0)); - - when(mConnectivity.isConnected()).thenReturn(true); - mDispatcher.forceDispatch(); - - await().atMost(1, TimeUnit.SECONDS).until(dryRunData::size, is(1)); - - verify(mEventCache).updateState(true); - verify(mEventCache).drainTo(ArgumentMatchers.anyList()); - } - - @Test - public void testGetDispatchGzipped() { - assertFalse(mDispatcher.getDispatchGzipped()); - mDispatcher.setDispatchGzipped(true); - assertTrue(mDispatcher.getDispatchGzipped()); - verify(mPacketSender).setGzipData(true); - } - - @Test - public void testDefaultConnectionTimeout() { - assertEquals(Dispatcher.DEFAULT_CONNECTION_TIMEOUT, mDispatcher.getConnectionTimeOut()); - } - - @Test - public void testSetConnectionTimeout() { - mDispatcher.setConnectionTimeOut(100); - assertEquals(100, mDispatcher.getConnectionTimeOut()); - verify(mPacketSender).setTimeout(100); - } - - @Test - public void testDefaultDispatchInterval() { - assertEquals(Dispatcher.DEFAULT_DISPATCH_INTERVAL, mDispatcher.getDispatchInterval()); - } - - @Test - public void testForceDispatchTwice() { - mDispatcher.setDispatchInterval(-1); - mDispatcher.setConnectionTimeOut(20); - mDispatcher.submit(getTestEvent()); - - assertTrue(mDispatcher.forceDispatch()); - assertFalse(mDispatcher.forceDispatch()); - } - - @Test - public void testMultiThreadDispatch() throws Exception { - List dryRunData = Collections.synchronizedList(new ArrayList<>()); - mDispatcher.setDryRunTarget(dryRunData); - mDispatcher.setDispatchInterval(20); - - final int threadCount = 20; - final int queryCount = 100; - final List createdEvents = Collections.synchronizedList(new ArrayList<>()); - launchTestThreads(mApiUrl, mDispatcher, threadCount, queryCount, createdEvents); - - checkForMIAs(threadCount * queryCount, createdEvents, dryRunData); - } - - @Test - public void testForceDispatch() throws Exception { - List dryRunData = Collections.synchronizedList(new ArrayList<>()); - mDispatcher.setDryRunTarget(dryRunData); - mDispatcher.setDispatchInterval(-1L); - - final int threadCount = 10; - final int queryCount = 10; - final List createdEvents = Collections.synchronizedList(new ArrayList<>()); - launchTestThreads(mApiUrl, mDispatcher, threadCount, queryCount, createdEvents); - TestHelper.sleep(500); - assertEquals(threadCount * queryCount, createdEvents.size()); - assertEquals(0, dryRunData.size()); - mDispatcher.forceDispatch(); - - checkForMIAs(threadCount * queryCount, createdEvents, dryRunData); - } - - @Test - public void testBatchDispatch() throws Exception { - List dryRunData = Collections.synchronizedList(new ArrayList<>()); - mDispatcher.setDryRunTarget(dryRunData); - mDispatcher.setDispatchInterval(1500); - - final int threadCount = 5; - final int queryCount = 5; - final List createdEvents = Collections.synchronizedList(new ArrayList<>()); - launchTestThreads(mApiUrl, mDispatcher, threadCount, queryCount, createdEvents); - - await().atMost(2, TimeUnit.SECONDS).until(createdEvents::size, is(threadCount * queryCount)); - assertEquals(0, dryRunData.size()); - - await().atMost(2, TimeUnit.SECONDS).until(createdEvents::size, is(threadCount * queryCount)); - checkForMIAs(threadCount * queryCount, createdEvents, dryRunData); - } - - @Test - public void testBlockingDispatch() throws Exception { - List dryRunData = Collections.synchronizedList(new ArrayList<>()); - mDispatcher.setDryRunTarget(dryRunData); - mDispatcher.setDispatchInterval(-1); - - final int threadCount = 5; - final int queryCount = 5; - final List createdEvents = Collections.synchronizedList(new ArrayList<>()); - launchTestThreads(mApiUrl, mDispatcher, threadCount, queryCount, createdEvents); - await().atMost(2, TimeUnit.SECONDS).until(createdEvents::size, is(threadCount * queryCount)); - - assertEquals(dryRunData.size(), 0); - assertEquals(createdEvents.size(), threadCount * queryCount); - - mDispatcher.forceDispatchBlocking(); - - List flattenedQueries = getFlattenedQueries(dryRunData); - assertEquals(flattenedQueries.size(), threadCount * queryCount); - } - - @Test - public void testBlockingDispatchInFlight() throws Exception { - List dryRunData = Collections.synchronizedList(new ArrayList<>()); - mDispatcher.setDryRunTarget(dryRunData); - mDispatcher.setDispatchInterval(20); - - final int threadCount = 5; - final int queryCount = 5; - final List createdEvents = Collections.synchronizedList(new ArrayList<>()); - launchTestThreads(mApiUrl, mDispatcher, threadCount, queryCount, createdEvents); - await().atMost(2, TimeUnit.SECONDS).until(createdEvents::size, is(threadCount * queryCount)); - - assertEquals(createdEvents.size(), threadCount * queryCount); - assertNotEquals(new ArrayList(dryRunData).size(), 0); - - mDispatcher.forceDispatchBlocking(); - - List flattenedQueries = getFlattenedQueries(dryRunData); - assertEquals(flattenedQueries.size(), threadCount * queryCount); - } - - @Test - public void testBlockingDispatchCollision() throws Exception { - final Semaphore lock = new Semaphore(0); - final AtomicInteger eventCount = new AtomicInteger(0); - - mDispatcher.setDispatchInterval(-1); - - when(mPacketSender.send(any())).thenAnswer((Answer) invocation -> { - Packet packet = invocation.getArgument(0); - - eventCount.addAndGet(packet.getEventCount()); - - lock.release(); - Thread.sleep(100); - - return true; - }); - - final int threadCount = 7; - final int queryCount = 13; - final List createdEvents = Collections.synchronizedList(new ArrayList<>()); - launchTestThreads(mApiUrl, mDispatcher, threadCount, queryCount, createdEvents); - - await().atMost(2, TimeUnit.SECONDS).until(createdEvents::size, is(threadCount * queryCount)); - - mDispatcher.forceDispatch(); - - lock.acquire(); - - mDispatcher.forceDispatchBlocking(); - - assertEquals(eventCount.get(), threadCount * queryCount); - } - - @Test - public void testBlockingDispatchExceptionMode() { - mDispatcher.setDispatchInterval(200); - - final int threadCount = 5; - final int queryCount = 10; - - final List createdEvents = Collections.synchronizedList(new ArrayList<>()); - launchTestThreads(mApiUrl, mDispatcher, threadCount, queryCount, createdEvents); - - final AtomicInteger sentEvents = new AtomicInteger(0); - - when(mPacketSender.send(any())).thenAnswer((Answer) invocation -> { - Packet packet = invocation.getArgument(0); - sentEvents.addAndGet(packet.getEventCount()); - - mDispatcher.setDispatchMode(DispatchMode.EXCEPTION); - - return true; - }); - - await().atMost(2, TimeUnit.SECONDS).until(createdEvents::size, is(threadCount * queryCount)); - - mDispatcher.forceDispatchBlocking(); - - int sentEventCount = sentEvents.get(); - - assertEquals(sentEventCount, PacketFactory.PAGE_SIZE); - assertEquals(mEventCacheData.size() + sentEventCount, threadCount * queryCount); - } - - @Test - public void testDispatchRetryWithBackoff() { - AtomicInteger cnt = new AtomicInteger(0); - when(mPacketSender.send(any())).then((Answer) invocation -> cnt.incrementAndGet() > 5); - - mDispatcher.setDispatchInterval(100); - mDispatcher.submit(getTestEvent()); - - await().atLeast(100, TimeUnit.MILLISECONDS).until(() -> cnt.get() == 1); - await().atLeast(100, TimeUnit.MILLISECONDS).until(() -> cnt.get() == 2); - - await().atMost(1900, TimeUnit.MILLISECONDS).until(() -> cnt.get() == 5); - - mDispatcher.submit(getTestEvent()); - await().atMost(150, TimeUnit.MILLISECONDS).until(() -> cnt.get() == 5); - } - - @Test - public void testDispatchInterval() { - List dryRunData = Collections.synchronizedList(new ArrayList<>()); - mDispatcher.setDryRunTarget(dryRunData); - mDispatcher.setDispatchInterval(500); - assertThat(dryRunData.isEmpty(), is(true)); - mDispatcher.submit(getTestEvent()); - await().atLeast(500, TimeUnit.MILLISECONDS).until(() -> dryRunData.size() == 1); - } - - @Test - public void testRandomDispatchIntervals() throws Exception { - final List dryRunData = Collections.synchronizedList(new ArrayList<>()); - mDispatcher.setDryRunTarget(dryRunData); - - final int threadCount = 10; - final int queryCount = 100; - final List createdEvents = Collections.synchronizedList(new ArrayList<>()); - - new Thread(() -> { - try { - while (getFlattenedQueries(new ArrayList<>(dryRunData)).size() != threadCount * queryCount) { - mDispatcher.setDispatchInterval(new Random().nextInt(20 + 1) - 1); - } - } catch (Exception e) {e.printStackTrace();} - }).start(); - - launchTestThreads(mApiUrl, mDispatcher, threadCount, queryCount, createdEvents); - - checkForMIAs(threadCount * queryCount, createdEvents, dryRunData); - } - - public static void checkForMIAs(int expectedEvents, List createdEvents, List dryRunOutput) throws Exception { - int previousEventCount = 0; - int previousFlatQueryCount = 0; - List flattenedQueries; - int nothingHappenedCounter = 0; - while (true) { - TestHelper.sleep(100); - flattenedQueries = getFlattenedQueries(new ArrayList<>(dryRunOutput)); - if (flattenedQueries.size() == expectedEvents) { - break; - } else { - flattenedQueries = getFlattenedQueries(new ArrayList<>(dryRunOutput)); - int currentEventCount = createdEvents.size(); - int currentFlatQueryCount = flattenedQueries.size(); - if (previousEventCount != currentEventCount && previousFlatQueryCount != currentFlatQueryCount) { - previousEventCount = currentEventCount; - previousFlatQueryCount = currentFlatQueryCount; - nothingHappenedCounter = 0; - } else { - nothingHappenedCounter++; - if (nothingHappenedCounter > 50) - fail("Test seems stuck, nothing happens"); - } - } - } - - assertEquals(flattenedQueries.size(), expectedEvents); - assertEquals(createdEvents.size(), expectedEvents); - - // We are done, lets make sure can find all send queries in our dispatched results - while (!createdEvents.isEmpty()) { - String query = createdEvents.remove(0); - assertTrue(flattenedQueries.remove(query)); - } - assertTrue(true); - assertTrue(flattenedQueries.isEmpty()); - } - - public static void launchTestThreads(final String apiUrl, final Dispatcher dispatcher, int threadCount, final int queryCount, final List createdQueries) { - for (int i = 0; i < threadCount; i++) { - new Thread(() -> { - try { - for (int j = 0; j < queryCount; j++) { - TestHelper.sleep(new Random().nextInt(20)); - TrackMe trackMe = new TrackMe() - .set(QueryParams.EVENT_ACTION, UUID.randomUUID().toString()) - .set(QueryParams.EVENT_CATEGORY, UUID.randomUUID().toString()) - .set(QueryParams.EVENT_NAME, UUID.randomUUID().toString()) - .set(QueryParams.EVENT_VALUE, j); - dispatcher.submit(trackMe); - createdQueries.add(apiUrl + new Event(trackMe.toMap()).getEncodedQuery()); - } - } catch (Exception e) { - e.printStackTrace(); - fail(); - } - }).start(); - } - } - - public static List getFlattenedQueries(List packets) throws Exception { - List flattenedQueries = new ArrayList<>(); - for (Packet request : packets) { - if (request.getPostData() != null) { - JSONArray batchedRequests = request.getPostData().getJSONArray("requests"); - for (int json = 0; json < batchedRequests.length(); json++) { - String unbatchedRequest = request.getTargetURL() + batchedRequests.get(json).toString(); - flattenedQueries.add(unbatchedRequest); - } - } else { - flattenedQueries.add(request.getTargetURL()); - } - } - return flattenedQueries; - } - - public static TrackMe getTestEvent() { - TrackMe trackMe = new TrackMe(); - trackMe.set(QueryParams.SESSION_START, 1); - return trackMe; - } -} diff --git a/tracker/src/test/java/org/matomo/sdk/dispatcher/DefaultDispatcherTest.kt b/tracker/src/test/java/org/matomo/sdk/dispatcher/DefaultDispatcherTest.kt new file mode 100644 index 00000000..2d2d862d --- /dev/null +++ b/tracker/src/test/java/org/matomo/sdk/dispatcher/DefaultDispatcherTest.kt @@ -0,0 +1,496 @@ +/* + * Android SDK for Matomo + * + * @link https://github.com/matomo-org/matomo-android-sdk + * @license https://github.com/matomo-org/matomo-sdk-android/blob/master/LICENSE BSD-3 Clause + */ +package org.matomo.sdk.dispatcher + +import org.awaitility.Awaitility +import org.hamcrest.MatcherAssert +import org.hamcrest.core.Is +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.matomo.sdk.QueryParams +import org.matomo.sdk.TrackMe +import org.matomo.sdk.tools.Connectivity +import org.mockito.ArgumentMatchers +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.MockitoAnnotations +import org.mockito.invocation.InvocationOnMock +import org.mockito.stubbing.Answer +import testhelpers.BaseTest +import testhelpers.TestHelper +import java.util.Collections +import java.util.Random +import java.util.UUID +import java.util.concurrent.LinkedBlockingQueue +import java.util.concurrent.Semaphore +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicInteger + +class DefaultDispatcherTest : BaseTest() { + private var mDispatcher: DefaultDispatcher? = null + + @Mock + var mEventCache: EventCache? = null + + @Mock + var mPacketSender: PacketSender? = null + + @Mock + var mConnectivity: Connectivity? = null + private val mApiUrl: String = "http://example.com" + + private val mEventCacheData: LinkedBlockingQueue = LinkedBlockingQueue() + + @Before + @Throws(Exception::class) + override fun setup() { + super.setup() + MockitoAnnotations.openMocks(this) + Mockito.`when`(mConnectivity!!.isConnected).thenReturn(true) + Mockito.`when`(mConnectivity!!.type).thenReturn(Connectivity.Type.MOBILE) + + Mockito.doAnswer { invocation: InvocationOnMock -> + mEventCacheData.add(invocation.getArgument(0)) + null + }.`when`(mEventCache)?.add(ArgumentMatchers.any(Event::class.java)) + Mockito.`when`(mEventCache!!.isEmpty).then(Answer { mEventCacheData.isEmpty() } as Answer) + Mockito.`when`(mEventCache!!.updateState(ArgumentMatchers.anyBoolean())) + .thenAnswer { invocation: InvocationOnMock -> invocation.getArgument(0) as Boolean && !mEventCacheData.isEmpty() } + Mockito.doAnswer { invocation: InvocationOnMock -> + val drainTarget = invocation.getArgument>(0) + mEventCacheData.drainTo(drainTarget) + null + }.`when`(mEventCache)?.drainTo(ArgumentMatchers.anyList()) + Mockito.doAnswer { invocation: InvocationOnMock -> + val toRequeue = invocation.getArgument>(0) + mEventCacheData.addAll(toRequeue) + null + }.`when`(mEventCache)?.requeue(ArgumentMatchers.anyList()) + Mockito.doAnswer { + mEventCacheData.clear() + null + }.`when`(mEventCache)?.clear() + mDispatcher = DefaultDispatcher(mEventCache!!, mConnectivity!!, PacketFactory(mApiUrl), mPacketSender!!) + } + + @Test + fun testClear() { + mDispatcher!!.clear() + Mockito.verify(mEventCache)?.clear() + } + + @Test + fun testClear_cleanExit() { + val dryRunData = Collections.synchronizedList(ArrayList()) + mDispatcher!!.setDryRunTarget(dryRunData) + mDispatcher!!.submit(testEvent) + mDispatcher!!.forceDispatch() + + TestHelper.sleep(100) + MatcherAssert.assertThat(dryRunData.size, Is.`is`(1)) + dryRunData.clear() + + Mockito.`when`(mConnectivity!!.isConnected).thenReturn(false) + mDispatcher!!.submit(testEvent) + + TestHelper.sleep(100) + MatcherAssert.assertThat(mEventCacheData.size, Is.`is`(1)) + + mDispatcher!!.clear() + + Mockito.`when`(mConnectivity!!.isConnected).thenReturn(true) + mDispatcher!!.forceDispatch() + + TestHelper.sleep(100) + MatcherAssert.assertThat(dryRunData.size, Is.`is`(0)) + } + + @Test + fun testGetDispatchMode() { + Assert.assertEquals(DispatchMode.ALWAYS, mDispatcher!!.dispatchMode) + mDispatcher!!.dispatchMode = DispatchMode.WIFI_ONLY + Assert.assertEquals(DispatchMode.WIFI_ONLY, mDispatcher!!.dispatchMode) + } + + @Test + fun testDispatchMode_wifiOnly() { + val dryRunData = Collections.synchronizedList(ArrayList()) + mDispatcher!!.setDryRunTarget(dryRunData) + Mockito.`when`(mConnectivity!!.type).thenReturn(Connectivity.Type.MOBILE) + + mDispatcher!!.dispatchMode = DispatchMode.WIFI_ONLY + mDispatcher!!.submit(testEvent) + mDispatcher!!.forceDispatch() + + Mockito.verify(mEventCache, Mockito.timeout(1000))?.updateState(false) + Mockito.verify(mEventCache, Mockito.never())?.drainTo(ArgumentMatchers.anyList()) + + Mockito.`when`(mConnectivity!!.type).thenReturn(Connectivity.Type.WIFI) + mDispatcher!!.forceDispatch() + Awaitility.await().atMost(1, TimeUnit.SECONDS).until({ dryRunData.size }, Is.`is`(1)) + + Mockito.verify(mEventCache)?.updateState(true) + Mockito.verify(mEventCache)?.drainTo(ArgumentMatchers.anyList()) + } + + @Test + fun testConnectivityChange() { + val dryRunData = Collections.synchronizedList(ArrayList()) + mDispatcher!!.setDryRunTarget(dryRunData) + Mockito.`when`(mConnectivity!!.isConnected).thenReturn(false) + + mDispatcher!!.submit(testEvent) + mDispatcher!!.forceDispatch() + + Mockito.verify(mEventCache, Mockito.timeout(1000))?.add(ArgumentMatchers.any()) + Mockito.verify(mEventCache, Mockito.never())?.drainTo(ArgumentMatchers.anyList()) + MatcherAssert.assertThat(dryRunData.size, Is.`is`(0)) + + Mockito.`when`(mConnectivity!!.isConnected).thenReturn(true) + mDispatcher!!.forceDispatch() + + Awaitility.await().atMost(1, TimeUnit.SECONDS).until({ dryRunData.size }, Is.`is`(1)) + + Mockito.verify(mEventCache)?.updateState(true) + Mockito.verify(mEventCache)?.drainTo(ArgumentMatchers.anyList()) + } + + @Test + fun testGetDispatchGzipped() { + Assert.assertFalse(mDispatcher!!.dispatchGzipped) + mDispatcher!!.dispatchGzipped = true + Assert.assertTrue(mDispatcher!!.dispatchGzipped) + Mockito.verify(mPacketSender)?.setGzipData(true) + } + + @Test + fun testDefaultConnectionTimeout() { + Assert.assertEquals(Dispatcher.DEFAULT_CONNECTION_TIMEOUT.toLong(), mDispatcher!!.connectionTimeOut.toLong()) + } + + @Test + fun testSetConnectionTimeout() { + mDispatcher!!.connectionTimeOut = 100 + Assert.assertEquals(100, mDispatcher!!.connectionTimeOut.toLong()) + Mockito.verify(mPacketSender)?.setTimeout(100) + } + + @Test + fun testDefaultDispatchInterval() { + Assert.assertEquals(Dispatcher.DEFAULT_DISPATCH_INTERVAL, mDispatcher!!.dispatchInterval) + } + + @Test + fun testForceDispatchTwice() { + mDispatcher!!.dispatchInterval = -1 + mDispatcher!!.connectionTimeOut = 20 + mDispatcher!!.submit(testEvent) + + Assert.assertTrue(mDispatcher!!.forceDispatch()) + Assert.assertFalse(mDispatcher!!.forceDispatch()) + } + + @Test + @Throws(Exception::class) + fun testMultiThreadDispatch() { + val dryRunData = Collections.synchronizedList(ArrayList()).toMutableList() + mDispatcher!!.setDryRunTarget(dryRunData) + mDispatcher!!.dispatchInterval = 20 + + val threadCount = 20 + val queryCount = 100 + val createdEvents = Collections.synchronizedList(ArrayList()) + launchTestThreads(mApiUrl, mDispatcher, threadCount, queryCount, createdEvents) + + checkForMIAs(threadCount * queryCount, createdEvents, dryRunData) + } + + @Test + @Throws(Exception::class) + fun testForceDispatch() { + val dryRunData = Collections.synchronizedList(ArrayList()).toMutableList() + mDispatcher!!.setDryRunTarget(dryRunData) + mDispatcher!!.dispatchInterval = -1L + + val threadCount = 10 + val queryCount = 10 + val createdEvents = Collections.synchronizedList(ArrayList()) + launchTestThreads(mApiUrl, mDispatcher, threadCount, queryCount, createdEvents) + TestHelper.sleep(500) + Assert.assertEquals((threadCount * queryCount).toLong(), createdEvents.size.toLong()) + Assert.assertEquals(0, dryRunData.size.toLong()) + mDispatcher!!.forceDispatch() + + checkForMIAs(threadCount * queryCount, createdEvents, dryRunData) + } + + @Test + @Throws(Exception::class) + fun testBatchDispatch() { + val dryRunData = Collections.synchronizedList(ArrayList()).toMutableList() + mDispatcher!!.setDryRunTarget(dryRunData) + mDispatcher!!.dispatchInterval = 1500 + + val threadCount = 5 + val queryCount = 5 + val createdEvents = Collections.synchronizedList(ArrayList()) + launchTestThreads(mApiUrl, mDispatcher, threadCount, queryCount, createdEvents) + + Awaitility.await().atMost(2, TimeUnit.SECONDS).until({ createdEvents.size }, Is.`is`(threadCount * queryCount)) + Assert.assertEquals(0, dryRunData.size.toLong()) + + Awaitility.await().atMost(2, TimeUnit.SECONDS).until({ createdEvents.size }, Is.`is`(threadCount * queryCount)) + checkForMIAs(threadCount * queryCount, createdEvents, dryRunData) + } + + @Test + @Throws(Exception::class) + fun testBlockingDispatch() { + val dryRunData = Collections.synchronizedList(ArrayList()).toMutableList() + mDispatcher!!.setDryRunTarget(dryRunData) + mDispatcher!!.dispatchInterval = -1 + + val threadCount = 5 + val queryCount = 5 + val createdEvents = Collections.synchronizedList(ArrayList()) + launchTestThreads(mApiUrl, mDispatcher, threadCount, queryCount, createdEvents) + Awaitility.await().atMost(2, TimeUnit.SECONDS).until({ createdEvents.size }, Is.`is`(threadCount * queryCount)) + + Assert.assertEquals(dryRunData.size.toLong(), 0) + Assert.assertEquals(createdEvents.size.toLong(), (threadCount * queryCount).toLong()) + + mDispatcher!!.forceDispatchBlocking() + + val flattenedQueries: List = getFlattenedQueries(dryRunData) + Assert.assertEquals(flattenedQueries.size.toLong(), (threadCount * queryCount).toLong()) + } + + @Test + @Throws(Exception::class) + fun testBlockingDispatchInFlight() { + val dryRunData = Collections.synchronizedList(ArrayList()).toMutableList() + mDispatcher!!.setDryRunTarget(dryRunData) + mDispatcher!!.dispatchInterval = 20 + + val threadCount = 5 + val queryCount = 5 + val createdEvents = Collections.synchronizedList(ArrayList()) + launchTestThreads(mApiUrl, mDispatcher, threadCount, queryCount, createdEvents) + Awaitility.await().atMost(2, TimeUnit.SECONDS).until({ createdEvents.size }, Is.`is`(threadCount * queryCount)) + + Assert.assertEquals(createdEvents.size.toLong(), (threadCount * queryCount).toLong()) + Assert.assertNotEquals(ArrayList(dryRunData).size.toLong(), 0) + + mDispatcher!!.forceDispatchBlocking() + + val flattenedQueries: List = getFlattenedQueries(dryRunData) + Assert.assertEquals(flattenedQueries.size.toLong(), (threadCount * queryCount).toLong()) + } + + @Test + @Throws(Exception::class) + fun testBlockingDispatchCollision() { + val lock = Semaphore(0) + val eventCount = AtomicInteger(0) + + mDispatcher!!.dispatchInterval = -1 + + Mockito.`when`(mPacketSender!!.send(ArgumentMatchers.any())).thenAnswer { invocation: InvocationOnMock -> + val packet = invocation.getArgument(0) + eventCount.addAndGet(packet.eventCount) + + lock.release() + Thread.sleep(100) + true + } + + val threadCount = 7 + val queryCount = 13 + val createdEvents = Collections.synchronizedList(ArrayList()) + launchTestThreads(mApiUrl, mDispatcher, threadCount, queryCount, createdEvents) + + Awaitility.await().atMost(2, TimeUnit.SECONDS).until({ createdEvents.size }, Is.`is`(threadCount * queryCount)) + + mDispatcher!!.forceDispatch() + + lock.acquire() + + mDispatcher!!.forceDispatchBlocking() + + Assert.assertEquals(eventCount.get().toLong(), (threadCount * queryCount).toLong()) + } + + @Test + fun testBlockingDispatchExceptionMode() { + mDispatcher!!.dispatchInterval = 200 + + val threadCount = 5 + val queryCount = 10 + + val createdEvents = Collections.synchronizedList(ArrayList()) + launchTestThreads(mApiUrl, mDispatcher, threadCount, queryCount, createdEvents) + + val sentEvents = AtomicInteger(0) + + Mockito.`when`(mPacketSender!!.send(ArgumentMatchers.any())).thenAnswer { invocation: InvocationOnMock -> + val packet = invocation.getArgument(0) + sentEvents.addAndGet(packet.eventCount) + + mDispatcher!!.dispatchMode = DispatchMode.EXCEPTION + true + } + + Awaitility.await().atMost(2, TimeUnit.SECONDS).until({ createdEvents.size }, Is.`is`(threadCount * queryCount)) + + mDispatcher!!.forceDispatchBlocking() + + val sentEventCount = sentEvents.get() + + Assert.assertEquals(sentEventCount.toLong(), PacketFactory.PAGE_SIZE.toLong()) + Assert.assertEquals((mEventCacheData.size + sentEventCount).toLong(), (threadCount * queryCount).toLong()) + } + + @Test + fun testDispatchRetryWithBackoff() { + val cnt = AtomicInteger(0) + Mockito.`when`(mPacketSender!!.send(ArgumentMatchers.any())) + .then(Answer { cnt.incrementAndGet() > 5 } as Answer) + + mDispatcher!!.dispatchInterval = 100 + mDispatcher!!.submit(testEvent) + + Awaitility.await().atLeast(100, TimeUnit.MILLISECONDS).until { cnt.get() == 1 } + Awaitility.await().atLeast(100, TimeUnit.MILLISECONDS).until { cnt.get() == 2 } + + Awaitility.await().atMost(1900, TimeUnit.MILLISECONDS).until { cnt.get() == 5 } + + mDispatcher!!.submit(testEvent) + Awaitility.await().atMost(150, TimeUnit.MILLISECONDS).until { cnt.get() == 5 } + } + + @Test + fun testDispatchInterval() { + val dryRunData = Collections.synchronizedList(ArrayList()) + mDispatcher!!.setDryRunTarget(dryRunData) + mDispatcher!!.dispatchInterval = 500 + MatcherAssert.assertThat(dryRunData.isEmpty(), Is.`is`(true)) + mDispatcher!!.submit(testEvent) + Awaitility.await().atLeast(500, TimeUnit.MILLISECONDS).until { dryRunData.size == 1 } + } + + @Test + @Throws(Exception::class) + fun testRandomDispatchIntervals() { + val dryRunData = Collections.synchronizedList(ArrayList()).toMutableList() + mDispatcher!!.setDryRunTarget(dryRunData) + + val threadCount = 10 + val queryCount = 100 + val createdEvents = Collections.synchronizedList(ArrayList()) + + Thread { + try { + while (getFlattenedQueries(ArrayList(dryRunData)).size != threadCount * queryCount) { + mDispatcher!!.dispatchInterval = (Random().nextInt(20 + 1) - 1).toLong() + } + } catch (e: Exception) { + e.printStackTrace() + } + }.start() + + launchTestThreads(mApiUrl, mDispatcher, threadCount, queryCount, createdEvents) + + checkForMIAs(threadCount * queryCount, createdEvents, dryRunData) + } + + companion object { + @Throws(Exception::class) + fun checkForMIAs(expectedEvents: Int, createdEvents: MutableList, dryRunOutput: List) { + var previousEventCount = 0 + var previousFlatQueryCount = 0 + var flattenedQueries: MutableList + var nothingHappenedCounter = 0 + while (true) { + TestHelper.sleep(100) + flattenedQueries = getFlattenedQueries(ArrayList(dryRunOutput)) + if (flattenedQueries.size == expectedEvents) { + break + } else { + flattenedQueries = getFlattenedQueries(ArrayList(dryRunOutput)) + val currentEventCount = createdEvents.size + val currentFlatQueryCount = flattenedQueries.size + if (previousEventCount != currentEventCount && previousFlatQueryCount != currentFlatQueryCount) { + previousEventCount = currentEventCount + previousFlatQueryCount = currentFlatQueryCount + nothingHappenedCounter = 0 + } else { + nothingHappenedCounter++ + if (nothingHappenedCounter > 50) Assert.fail("Test seems stuck, nothing happens") + } + } + } + + Assert.assertEquals(flattenedQueries.size.toLong(), expectedEvents.toLong()) + Assert.assertEquals(createdEvents.size.toLong(), expectedEvents.toLong()) + + // We are done, lets make sure can find all send queries in our dispatched results + while (createdEvents.isNotEmpty()) { + val query: String = createdEvents.removeAt(0) + Assert.assertTrue(flattenedQueries.remove(query)) + } + Assert.assertTrue(true) + Assert.assertTrue(flattenedQueries.isEmpty()) + } + + fun launchTestThreads(apiUrl: String, dispatcher: Dispatcher?, threadCount: Int, queryCount: Int, createdQueries: MutableList) { + for (i in 0 until threadCount) { + Thread { + try { + for (j in 0 until queryCount) { + TestHelper.sleep(Random().nextInt(20).toLong()) + val trackMe = TrackMe() + .set(QueryParams.EVENT_ACTION, UUID.randomUUID().toString()) + .set(QueryParams.EVENT_CATEGORY, UUID.randomUUID().toString()) + .set(QueryParams.EVENT_NAME, UUID.randomUUID().toString()) + .set(QueryParams.EVENT_VALUE, j) + dispatcher!!.submit(trackMe) + createdQueries.add(apiUrl + Event(trackMe.toMap()).encodedQuery) + } + } catch (e: Exception) { + e.printStackTrace() + Assert.fail() + } + }.start() + } + } + + @Throws(Exception::class) + fun getFlattenedQueries(packets: List): MutableList { + val flattenedQueries: MutableList = ArrayList() + for (request in packets) { + if (request!!.postData != null) { + val batchedRequests = request.postData!!.getJSONArray("requests") + for (json in 0 until batchedRequests.length()) { + val unbatchedRequest = request.targetURL + batchedRequests[json].toString() + flattenedQueries.add(unbatchedRequest) + } + } else { + flattenedQueries.add(request.targetURL) + } + } + return flattenedQueries + } + + val testEvent: TrackMe + get() { + val trackMe = TrackMe() + trackMe[QueryParams.SESSION_START] = 1 + return trackMe + } + } +} From f2c0e112ee1a33cbde70a7f7aafa9058f6798030 Mon Sep 17 00:00:00 2001 From: Hannes Achleitner Date: Thu, 11 Jul 2024 16:48:22 +0200 Subject: [PATCH 3/5] Force tracker timeout --- exampleapp/src/main/java/org/matomo/demo/DemoApp.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/exampleapp/src/main/java/org/matomo/demo/DemoApp.kt b/exampleapp/src/main/java/org/matomo/demo/DemoApp.kt index 68b412a0..6df6b8a0 100644 --- a/exampleapp/src/main/java/org/matomo/demo/DemoApp.kt +++ b/exampleapp/src/main/java/org/matomo/demo/DemoApp.kt @@ -40,6 +40,7 @@ class DemoApp : MatomoApplication() { // Track this app install, this will only trigger once per app version. // i.e. "http://org.matomo.demo:1/185DECB5CFE28FDB2F45887022D668B4" + tracker.dispatchTimeout = 1 TrackHelper.track().download().identifier(Extra.ApkChecksum(this)).with(tracker) // Alternative: // i.e. "http://org.matomo.demo:1/com.android.vending" From 743a9d2c769aef83f5e90681e23779463aa511d9 Mon Sep 17 00:00:00 2001 From: Hannes Achleitner Date: Thu, 11 Jul 2024 18:00:43 +0200 Subject: [PATCH 4/5] Kotlin DefaultDispatcherTest --- .../java/org/matomo/sdk/dispatcher/DefaultDispatcherTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tracker/src/test/java/org/matomo/sdk/dispatcher/DefaultDispatcherTest.kt b/tracker/src/test/java/org/matomo/sdk/dispatcher/DefaultDispatcherTest.kt index 2d2d862d..8788060e 100644 --- a/tracker/src/test/java/org/matomo/sdk/dispatcher/DefaultDispatcherTest.kt +++ b/tracker/src/test/java/org/matomo/sdk/dispatcher/DefaultDispatcherTest.kt @@ -300,7 +300,7 @@ class DefaultDispatcherTest : BaseTest() { mDispatcher!!.dispatchInterval = -1 - Mockito.`when`(mPacketSender!!.send(ArgumentMatchers.any())).thenAnswer { invocation: InvocationOnMock -> + Mockito.`when`(mPacketSender!!.send(ArgumentMatchers.any())).thenAnswer { invocation: InvocationOnMock -> val packet = invocation.getArgument(0) eventCount.addAndGet(packet.eventCount) @@ -337,7 +337,7 @@ class DefaultDispatcherTest : BaseTest() { val sentEvents = AtomicInteger(0) - Mockito.`when`(mPacketSender!!.send(ArgumentMatchers.any())).thenAnswer { invocation: InvocationOnMock -> + Mockito.`when`(mPacketSender!!.send(ArgumentMatchers.any())).thenAnswer { invocation: InvocationOnMock -> val packet = invocation.getArgument(0) sentEvents.addAndGet(packet.eventCount) From c3388c225394e264fce3392a25b2d5e5a4e89efd Mon Sep 17 00:00:00 2001 From: Hannes Achleitner Date: Thu, 11 Jul 2024 18:25:05 +0200 Subject: [PATCH 5/5] Introduce error callback --- .../org/matomo/sdk/dispatcher/DefaultDispatcher.kt | 10 +++++++--- .../matomo/sdk/dispatcher/DefaultDispatcherFactory.kt | 2 +- .../org/matomo/sdk/dispatcher/DefaultPacketSender.kt | 7 ++++--- .../java/org/matomo/sdk/dispatcher/PacketSender.kt | 2 +- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/tracker/src/main/java/org/matomo/sdk/dispatcher/DefaultDispatcher.kt b/tracker/src/main/java/org/matomo/sdk/dispatcher/DefaultDispatcher.kt index ac0f039f..744cb487 100644 --- a/tracker/src/main/java/org/matomo/sdk/dispatcher/DefaultDispatcher.kt +++ b/tracker/src/main/java/org/matomo/sdk/dispatcher/DefaultDispatcher.kt @@ -23,8 +23,10 @@ class DefaultDispatcher( private val eventCache: EventCache, private val connectivity: Connectivity, private val packetFactory: PacketFactory, - private val packetSender: PacketSender + private val packetSender: PacketSender, + callback: ((Exception) -> Unit)?// = null ) : Dispatcher { + private val threadControl = Any() private val sleepToken = Semaphore(0) @@ -203,12 +205,13 @@ class DefaultDispatcher( Timber.tag(TAG).d("Drained %s events.", drainedEvents.size) for (packet in packetFactory.buildPackets(drainedEvents)) { var success: Boolean - + var resultException: Exception? = null if (mDryRunTarget != null) { Timber.tag(TAG).d("DryRun, stored HttpRequest, now %d.", mDryRunTarget!!.size) success = mDryRunTarget!!.add(packet) } else { - success = packetSender.send(packet) + resultException = packetSender.send(packet) + success = resultException == null } if (success) { @@ -219,6 +222,7 @@ class DefaultDispatcher( // memory or disk Timber.tag(TAG).d("Failure while trying to send packet") retryCounter++ + callback?.let { resultException?.let { exception -> it(exception) } } break } diff --git a/tracker/src/main/java/org/matomo/sdk/dispatcher/DefaultDispatcherFactory.kt b/tracker/src/main/java/org/matomo/sdk/dispatcher/DefaultDispatcherFactory.kt index 5a0064e0..e22b9ed5 100644 --- a/tracker/src/main/java/org/matomo/sdk/dispatcher/DefaultDispatcherFactory.kt +++ b/tracker/src/main/java/org/matomo/sdk/dispatcher/DefaultDispatcherFactory.kt @@ -9,7 +9,7 @@ open class DefaultDispatcherFactory : DispatcherFactory { EventCache(EventDiskCache(tracker)), Connectivity(tracker.matomo.context), PacketFactory(tracker.apiUrl), - DefaultPacketSender() + DefaultPacketSender(), {} ) } } diff --git a/tracker/src/main/java/org/matomo/sdk/dispatcher/DefaultPacketSender.kt b/tracker/src/main/java/org/matomo/sdk/dispatcher/DefaultPacketSender.kt index 7abddb23..eeb8ff17 100644 --- a/tracker/src/main/java/org/matomo/sdk/dispatcher/DefaultPacketSender.kt +++ b/tracker/src/main/java/org/matomo/sdk/dispatcher/DefaultPacketSender.kt @@ -18,7 +18,7 @@ class DefaultPacketSender : PacketSender { private var mTimeout = Dispatcher.DEFAULT_CONNECTION_TIMEOUT.toLong() private var mGzip = false - override fun send(packet: Packet): Boolean { + override fun send(packet: Packet): Exception? { var urlConnection: HttpURLConnection? = null try { urlConnection = URL(packet.targetURL).openConnection() as HttpURLConnection @@ -114,10 +114,11 @@ class DefaultPacketSender : PacketSender { Timber.tag(TAG).w("Transmission failed (code=%d, reason=%s)", statusCode, errorReason.toString()) } - return successful + return null } catch (e: Exception) { Timber.tag(TAG).e(e, "Transmission failed unexpectedly.") - return false + + return e } finally { urlConnection?.disconnect() } diff --git a/tracker/src/main/java/org/matomo/sdk/dispatcher/PacketSender.kt b/tracker/src/main/java/org/matomo/sdk/dispatcher/PacketSender.kt index ca10b32f..ab18281a 100644 --- a/tracker/src/main/java/org/matomo/sdk/dispatcher/PacketSender.kt +++ b/tracker/src/main/java/org/matomo/sdk/dispatcher/PacketSender.kt @@ -5,7 +5,7 @@ interface PacketSender { /** * @return true if successful */ - fun send(packet: Packet): Boolean + fun send(packet: Packet): Exception? /** * @param timeout in milliseconds