@@ -30,6 +30,7 @@ import com.datadog.tools.unit.extensions.config.TestConfiguration
3030import com.datadog.tools.unit.forge.aThrowable
3131import fr.xgouchet.elmyr.Forge
3232import fr.xgouchet.elmyr.annotation.Forgery
33+ import fr.xgouchet.elmyr.annotation.IntForgery
3334import fr.xgouchet.elmyr.annotation.StringForgery
3435import fr.xgouchet.elmyr.junit5.ForgeConfiguration
3536import fr.xgouchet.elmyr.junit5.ForgeExtension
@@ -46,6 +47,7 @@ import org.mockito.kotlin.anyOrNull
4647import org.mockito.kotlin.argumentCaptor
4748import org.mockito.kotlin.doReturn
4849import org.mockito.kotlin.doThrow
50+ import org.mockito.kotlin.never
4951import org.mockito.kotlin.times
5052import org.mockito.kotlin.verify
5153import org.mockito.kotlin.verifyNoInteractions
@@ -334,16 +336,252 @@ internal class RumDataWriterTest {
334336
335337 @Test
336338 fun `M persist the event into the NDK crash folder W onDataWritten(){ViewEvent+dir exists}` (
337- @Forgery viewEvent : ViewEvent
339+ @Forgery fakeViewEvent : ViewEvent
338340 ) {
341+ // Given - the first event of a view
342+ val fakeViewStartEvent = fakeViewEvent.copy(
343+ dd = fakeViewEvent.dd.copy(documentVersion = RumDataWriter .FIRST_VIEW_DOCUMENT_VERSION )
344+ )
345+ testedWriter.onViewEventSubmitted(fakeViewStartEvent)
346+
339347 // When
340- testedWriter.onDataWritten(viewEvent , fakeSerializedData)
348+ testedWriter.onDataWritten(fakeViewStartEvent , fakeSerializedData)
341349
342350 // Then
343351 verify(rumMonitor.mockSdkCore).writeLastViewEvent(fakeSerializedData)
344352 verifyNoInteractions(mockInternalLogger)
345353 }
346354
355+ @Test
356+ fun `M call writeLastViewEvent W onDataWritten() { ViewEvent of a new view }` (
357+ @Forgery fakeViewEvent : ViewEvent
358+ ) {
359+ // Given - view A is persisted
360+ writeViewStart(fakeViewEvent, VIEW_A_ID )
361+
362+ // When - view B starts and emits its first event
363+ val fakeViewBData = writeViewStart(fakeViewEvent, VIEW_B_ID )
364+
365+ // Then - the new view takes over the persisted snapshot
366+ verify(rumMonitor.mockSdkCore).writeLastViewEvent(fakeViewBData)
367+ }
368+
369+ @Test
370+ fun `M call writeLastViewEvent W onDataWritten() { ViewEvent, update of the persisted view }` (
371+ @Forgery fakeViewEvent : ViewEvent
372+ ) {
373+ // Given - view A is persisted
374+ writeViewStart(fakeViewEvent, VIEW_A_ID )
375+
376+ // When - the same view completes (stopped, no newer view)
377+ val fakeCompletedViewAData = writeViewUpdate(fakeViewEvent, VIEW_A_ID , isActive = false , marker = " complete" )
378+
379+ // Then - the snapshot is refreshed in place with the final event
380+ verify(rumMonitor.mockSdkCore).writeLastViewEvent(fakeCompletedViewAData)
381+ }
382+
383+ @Test
384+ fun `M NOT call writeLastViewEvent W onDataWritten() { ViewEvent, stale view still marked active }` (
385+ @Forgery fakeViewEvent : ViewEvent
386+ ) {
387+ // Given - view A was stopped while resources were still pending, then view B started
388+ writeViewStart(fakeViewEvent, VIEW_A_ID )
389+ writeViewStart(fakeViewEvent, VIEW_B_ID )
390+
391+ // When - a pending resource of view A completes: view A is not complete yet, so it still
392+ // emits an event with isActive = true
393+ val fakeStaleViewAData = writeViewUpdate(fakeViewEvent, VIEW_A_ID , isActive = true , marker = " stale" )
394+
395+ // Then - the stale view does not overwrite the snapshot of view B
396+ verify(rumMonitor.mockSdkCore, never()).writeLastViewEvent(fakeStaleViewAData)
397+ }
398+
399+ @Test
400+ fun `M NOT call writeLastViewEvent W onDataWritten() { ViewEvent, stale view completing }` (
401+ @Forgery fakeViewEvent : ViewEvent
402+ ) {
403+ // Given - view A was stopped while resources were still pending, view B started, then one
404+ // of view A's pending resources completed
405+ writeViewStart(fakeViewEvent, VIEW_A_ID )
406+ writeViewStart(fakeViewEvent, VIEW_B_ID )
407+ writeViewUpdate(fakeViewEvent, VIEW_A_ID , isActive = true , marker = " stale" )
408+
409+ // When - the last pending event of view A completes it
410+ val fakeCompletedViewAData = writeViewUpdate(fakeViewEvent, VIEW_A_ID , isActive = false , marker = " complete" )
411+
412+ // Then - the completion of the stale view does not overwrite the snapshot of view B
413+ verify(rumMonitor.mockSdkCore, never()).writeLastViewEvent(fakeCompletedViewAData)
414+ }
415+
416+ @Test
417+ fun `M NOT call writeLastViewEvent W onDataWritten() { ViewEvent, stale view, no active view }` (
418+ @Forgery fakeViewEvent : ViewEvent
419+ ) {
420+ // Given - view A was stopped while resources were still pending, view B started and then
421+ // completed too, so no view is active anymore
422+ writeViewStart(fakeViewEvent, VIEW_A_ID )
423+ writeViewStart(fakeViewEvent, VIEW_B_ID )
424+ val fakeCompletedViewBData = writeViewUpdate(fakeViewEvent, VIEW_B_ID , isActive = false , marker = " complete" )
425+
426+ // When - a pending event of view A completes it, after the last view completed
427+ val fakeCompletedViewAData = writeViewUpdate(fakeViewEvent, VIEW_A_ID , isActive = false , marker = " complete" )
428+
429+ // Then - view A does not overwrite the snapshot of the last view
430+ verify(rumMonitor.mockSdkCore).writeLastViewEvent(fakeCompletedViewBData)
431+ verify(rumMonitor.mockSdkCore, never()).writeLastViewEvent(fakeCompletedViewAData)
432+ }
433+
434+ @Test
435+ fun `M NOT call writeLastViewEvent W onDataWritten() { ViewEvent, stale view after many views }` (
436+ @Forgery fakeViewEvent : ViewEvent ,
437+ @IntForgery(min = 20 , max = 100 ) fakeViewCount : Int
438+ ) {
439+ // Given - view A was stopped while a resource was still pending, then the user navigated
440+ // through many other views
441+ writeViewStart(fakeViewEvent, VIEW_A_ID )
442+ repeat(fakeViewCount) {
443+ writeViewStart(fakeViewEvent, " view-$it " )
444+ }
445+
446+ // When - the pending resource of view A finally completes
447+ val fakeStaleViewAData = writeViewUpdate(fakeViewEvent, VIEW_A_ID , isActive = true , marker = " stale" )
448+
449+ // Then - the stale view does not overwrite the snapshot of the newest view
450+ verify(rumMonitor.mockSdkCore, never()).writeLastViewEvent(fakeStaleViewAData)
451+ }
452+
453+ @Test
454+ fun `M call writeLastViewEvent W onDataWritten() { ViewEvent, first write of the view failed }` (
455+ @Forgery fakeViewEvent : ViewEvent
456+ ) {
457+ // Given - view A is persisted, then view B starts but its first event is not written
458+ writeViewStart(fakeViewEvent, VIEW_A_ID )
459+ submitViewStartWithFailedWrite(fakeViewEvent, VIEW_B_ID )
460+
461+ // When - a later event of view B is written
462+ val fakeViewBUpdateData = writeViewUpdate(fakeViewEvent, VIEW_B_ID , isActive = true , marker = " update" )
463+
464+ // Then - view B still establishes its snapshot
465+ verify(rumMonitor.mockSdkCore).writeLastViewEvent(fakeViewBUpdateData)
466+ }
467+
468+ @Test
469+ fun `M NOT call writeLastViewEvent W onDataWritten() { ViewEvent, stale view, first write failed }` (
470+ @Forgery fakeViewEvent : ViewEvent
471+ ) {
472+ // Given - view A was stopped while resources were still pending, then view B started but
473+ // its first event was not written
474+ writeViewStart(fakeViewEvent, VIEW_A_ID )
475+ submitViewStartWithFailedWrite(fakeViewEvent, VIEW_B_ID )
476+
477+ // When - a pending event of view A completes
478+ val fakeStaleViewAData = writeViewUpdate(fakeViewEvent, VIEW_A_ID , isActive = true , marker = " stale" )
479+
480+ // Then - view A is not the current view anymore, even though nothing was persisted for B
481+ verify(rumMonitor.mockSdkCore, never()).writeLastViewEvent(fakeStaleViewAData)
482+ }
483+
484+ @Test
485+ fun `M keep the newest view persisted W onDataWritten() { ViewEvent, interleaved views }` (
486+ @Forgery fakeViewEvent : ViewEvent
487+ ) {
488+ // When
489+ writeViewStart(fakeViewEvent, VIEW_A_ID )
490+ writeViewStart(fakeViewEvent, VIEW_B_ID )
491+ val fakeStaleViewAData = writeViewUpdate(fakeViewEvent, VIEW_A_ID , isActive = true , marker = " stale" )
492+ val fakeViewBUpdateData = writeViewUpdate(fakeViewEvent, VIEW_B_ID , isActive = false , marker = " complete" )
493+
494+ // Then
495+ verify(rumMonitor.mockSdkCore, never()).writeLastViewEvent(fakeStaleViewAData)
496+ argumentCaptor<ByteArray > {
497+ verify(rumMonitor.mockSdkCore, times(3 )).writeLastViewEvent(capture())
498+ assertThat(lastValue).isEqualTo(fakeViewBUpdateData)
499+ }
500+ }
501+
502+ // endregion
503+
504+ // region Internal
505+
506+ /* *
507+ * Notifies the writer that the first event of the given view was submitted and written, and
508+ * returns the serialized data used for that event.
509+ */
510+ private fun writeViewStart (fakeViewEvent : ViewEvent , viewId : String ): ByteArray {
511+ return writeViewEvent(
512+ fakeViewEvent = fakeViewEvent,
513+ viewId = viewId,
514+ isActive = true ,
515+ documentVersion = RumDataWriter .FIRST_VIEW_DOCUMENT_VERSION ,
516+ marker = " start"
517+ )
518+ }
519+
520+ /* *
521+ * Notifies the writer that the first event of the given view was submitted, but that its batch
522+ * write failed, so it was never written.
523+ */
524+ private fun submitViewStartWithFailedWrite (fakeViewEvent : ViewEvent , viewId : String ) {
525+ testedWriter.onViewEventSubmitted(
526+ forgeViewEvent(
527+ fakeViewEvent = fakeViewEvent,
528+ viewId = viewId,
529+ isActive = true ,
530+ documentVersion = RumDataWriter .FIRST_VIEW_DOCUMENT_VERSION
531+ )
532+ )
533+ }
534+
535+ /* *
536+ * Notifies the writer that a subsequent event of the given view was submitted and written, and
537+ * returns the serialized data used for that event.
538+ */
539+ private fun writeViewUpdate (
540+ fakeViewEvent : ViewEvent ,
541+ viewId : String ,
542+ isActive : Boolean ,
543+ marker : String
544+ ): ByteArray {
545+ return writeViewEvent(
546+ fakeViewEvent = fakeViewEvent,
547+ viewId = viewId,
548+ isActive = isActive,
549+ documentVersion = FAKE_VIEW_UPDATE_DOCUMENT_VERSION ,
550+ marker = marker
551+ )
552+ }
553+
554+ private fun forgeViewEvent (
555+ fakeViewEvent : ViewEvent ,
556+ viewId : String ,
557+ isActive : Boolean ,
558+ documentVersion : Long
559+ ): ViewEvent {
560+ return fakeViewEvent.copy(
561+ view = fakeViewEvent.view.copy(id = viewId, isActive = isActive),
562+ dd = fakeViewEvent.dd.copy(documentVersion = documentVersion)
563+ )
564+ }
565+
566+ private fun writeViewEvent (
567+ fakeViewEvent : ViewEvent ,
568+ viewId : String ,
569+ isActive : Boolean ,
570+ documentVersion : Long ,
571+ marker : String
572+ ): ByteArray {
573+ val serializedData = " $viewId -$marker " .toByteArray(Charsets .UTF_8 )
574+ val event = forgeViewEvent(
575+ fakeViewEvent = fakeViewEvent,
576+ viewId = viewId,
577+ isActive = isActive,
578+ documentVersion = documentVersion
579+ )
580+ testedWriter.onViewEventSubmitted(event)
581+ testedWriter.onDataWritten(event, serializedData)
582+ return serializedData
583+ }
584+
347585 // endregion
348586
349587 // region accessibility
@@ -401,6 +639,10 @@ internal class RumDataWriterTest {
401639 // endregion
402640
403641 companion object {
642+ private const val VIEW_A_ID = " view-a"
643+ private const val VIEW_B_ID = " view-b"
644+ private const val FAKE_VIEW_UPDATE_DOCUMENT_VERSION = 7L
645+
404646 val rumMonitor = GlobalRumMonitorTestConfiguration ()
405647
406648 @TestConfigurationsProvider
0 commit comments