Skip to content

Commit 5d9484e

Browse files
committed
adding unit tests
1 parent 21aa086 commit 5d9484e

File tree

6 files changed

+498
-11
lines changed

6 files changed

+498
-11
lines changed

NRExoPlayerTracker/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ dependencies {
3838
implementation project(path: ':NewRelicVideoCore')
3939
implementation 'androidx.media3:media3-exoplayer:1.1.0'
4040
testImplementation 'junit:junit:4.13.2'
41+
testImplementation 'org.robolectric:robolectric:4.10.3'
4142
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
4243
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
4344
androidTestImplementation 'androidx.test:runner:1.5.2'

NRExoPlayerTracker/src/main/java/com/newrelic/videoagent/exoplayer/tracker/NRTrackerExoPlayer.java

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,9 @@
2626
import java.util.List;
2727
import java.util.Map;
2828
import java.util.concurrent.ConcurrentHashMap;
29-
import java.util.concurrent.atomic.AtomicBoolean;
3029
import java.util.concurrent.atomic.AtomicInteger;
3130
import java.util.concurrent.atomic.AtomicLong;
31+
import java.util.concurrent.atomic.AtomicBoolean;
3232
import java.util.regex.Matcher;
3333
import java.util.regex.Pattern;
3434

@@ -48,7 +48,6 @@ public class NRTrackerExoPlayer extends NRVideoTracker implements Player.Listene
4848
protected int lastWindow;
4949
protected String renditionChangeShift;
5050
protected long actualBitrate;
51-
// Aggregation configuration
5251
private static final long DEFAULT_AGGREGATION_WINDOW_MS = 5000; // 5 seconds
5352
private static final int MAX_EVENTS_PER_AGGREGATE = 50;
5453
private volatile boolean droppedFrameAggregationEnabled = true;
@@ -425,7 +424,6 @@ public void sendDroppedFrame(int count, int elapsed) {
425424
} else {
426425
startNewAggregation(count, elapsed, currentTime);
427426
}
428-
429427
updateLastFrameDropSnapshot(count, elapsed, currentTime);
430428
scheduleDelayedFlush();
431429
}
@@ -437,14 +435,12 @@ private void startNewAggregation(int count, int elapsed, long timestamp) {
437435
lastDropTimestamp.set(timestamp);
438436
hasActiveAggregation.set(true);
439437
}
440-
441438
private void addToCurrentAggregation(int count, int elapsed, long timestamp) {
442439
totalLostFrames.addAndGet(count);
443440
totalLostFramesDuration.addAndGet(elapsed);
444441
eventCount.incrementAndGet();
445442
lastDropTimestamp.set(timestamp);
446443
}
447-
448444
private void updateLastFrameDropSnapshot(int count, int elapsed, long timestamp) {
449445
lastTrackData.put("lastFrameDropCount", count);
450446
lastTrackData.put("lastFrameDropDuration", elapsed);
@@ -455,17 +451,15 @@ private void updateLastFrameDropSnapshot(int count, int elapsed, long timestamp)
455451
lastTrackData.put("currentTotalDuration", totalLostFramesDuration.get());
456452
lastTrackData.put("currentEventCount", eventCount.get());
457453
}
458-
459454
private boolean isAggregationExpired(long currentTime) {
460455
long firstTime = firstDropTimestamp.get();
461456
return firstTime > 0 && (currentTime - firstTime) > DEFAULT_AGGREGATION_WINDOW_MS;
462457
}
463-
464458
private boolean isMaxEventsReached() {
465459
return eventCount.get() >= MAX_EVENTS_PER_AGGREGATE;
466460
}
467461

468-
private void scheduleDelayedFlush() {
462+
protected void scheduleDelayedFlush() {
469463
if (aggregationHandler == null) {
470464
aggregationHandler = new Handler(Looper.getMainLooper());
471465
}
@@ -477,7 +471,7 @@ private void scheduleDelayedFlush() {
477471
flushPendingEvent = this::flushPendingDroppedFrameEvent;
478472
aggregationHandler.postDelayed(flushPendingEvent, DEFAULT_AGGREGATION_WINDOW_MS);
479473
}
480-
private void flushCurrentAggregation() {
474+
protected void flushCurrentAggregation() {
481475
if (!hasActiveAggregation.get()) {
482476
return;
483477
}
@@ -507,15 +501,15 @@ private void flushCurrentAggregation() {
507501
private void flushPendingDroppedFrameEvent() {
508502
flushCurrentAggregation();
509503
}
510-
511-
private void resetAggregationState() {
504+
protected void resetAggregationState() {
512505
hasActiveAggregation.set(false);
513506
totalLostFrames.set(0);
514507
totalLostFramesDuration.set(0);
515508
eventCount.set(0);
516509
firstDropTimestamp.set(0);
517510
lastDropTimestamp.set(0);
518511
}
512+
519513
private void sendDroppedFrameImmediate(int count, int elapsed) {
520514
// Original implementation for backward compatibility
521515
Map<String, Object> attr = new HashMap<>();
@@ -534,7 +528,30 @@ public void setDroppedFrameAggregationEnabled(boolean enabled) {
534528
flushCurrentAggregation();
535529
}
536530
this.droppedFrameAggregationEnabled = enabled;
531+
}
532+
public ConcurrentHashMap<String, Object> getLastTrackData() {
533+
return lastTrackData;
534+
}
535+
public Map<String, Object> getCurrentAggregationStatus() {
536+
boolean hasAggregation = hasActiveAggregation.get();
537+
int lostFrames = totalLostFrames.get();
538+
int lostDuration = totalLostFramesDuration.get();
539+
int count = eventCount.get();
540+
long firstTime = firstDropTimestamp.get();
541+
long lastTime = lastDropTimestamp.get();
537542

543+
Map<String, Object> status = new ConcurrentHashMap<>();
544+
status.put("hasActiveAggregation", hasAggregation);
545+
status.put("totalLostFrames", lostFrames);
546+
status.put("totalLostFramesDuration", lostDuration);
547+
status.put("eventCount", count);
548+
status.put("firstDropTimestamp", firstTime);
549+
status.put("lastDropTimestamp", lastTime);
550+
551+
return status;
552+
}
553+
public boolean isDroppedFrameAggregationEnabled() {
554+
return droppedFrameAggregationEnabled;
538555
}
539556

540557
// ExoPlayer Player.EventListener
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Robolectric configuration for NRExoPlayerTracker tests
2+
sdk=28
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
package com.newrelic.videoagent.exoplayer.tracker;
2+
3+
import java.util.Map;
4+
import java.util.concurrent.ConcurrentHashMap;
5+
public class MockNRTrackerExoPlayer extends NRTrackerExoPlayer {
6+
7+
private boolean eventSent = false;
8+
private String lastEventType = null;
9+
private Map<String, Object> lastEventAttributes = null;
10+
11+
private long currentTimeMs = System.currentTimeMillis();
12+
private final ConcurrentHashMap<String, Object> mockLastTrackData = new ConcurrentHashMap<>();
13+
private final Map<String, Object> mockAggregationStatus = new ConcurrentHashMap<>();
14+
private boolean mockAggregationEnabled = true;
15+
16+
public MockNRTrackerExoPlayer() {
17+
super();
18+
mockLastTrackData.put("lastFrameDropCount", 0);
19+
mockLastTrackData.put("lastFrameDropDuration", 0);
20+
mockLastTrackData.put("lastFrameDropTime", 0L);
21+
mockLastTrackData.put("lastUpdateTime", System.currentTimeMillis());
22+
mockLastTrackData.put("currentTotalFrames", 0);
23+
mockLastTrackData.put("currentTotalDuration", 0);
24+
mockLastTrackData.put("currentEventCount", 0);
25+
26+
mockAggregationStatus.put("hasActiveAggregation", false);
27+
mockAggregationStatus.put("totalLostFrames", 0);
28+
mockAggregationStatus.put("totalLostFramesDuration", 0);
29+
mockAggregationStatus.put("eventCount", 0);
30+
mockAggregationStatus.put("firstDropTimestamp", 0L);
31+
mockAggregationStatus.put("lastDropTimestamp", 0L);
32+
}
33+
34+
public void simulateTimeExpiry() {
35+
this.currentTimeMs += 6000; // Beyond DEFAULT_AGGREGATION_WINDOW_MS (5000)
36+
simulateFlush();
37+
}
38+
39+
@Override
40+
public void sendVideoEvent(String eventType, Map<String, Object> attributes) {
41+
this.eventSent = true;
42+
this.lastEventType = eventType;
43+
this.lastEventAttributes = attributes != null ? new ConcurrentHashMap<>(attributes) : null;
44+
}
45+
46+
@Override
47+
public void sendVideoAdEvent(String eventType, Map<String, Object> attributes) {
48+
this.eventSent = true;
49+
this.lastEventType = eventType;
50+
this.lastEventAttributes = attributes != null ? new ConcurrentHashMap<>(attributes) : null;
51+
}
52+
53+
public boolean wasEventSent() {
54+
return eventSent;
55+
}
56+
57+
public String getLastEventType() {
58+
return lastEventType;
59+
}
60+
61+
public Map<String, Object> getLastEventAttributes() {
62+
return lastEventAttributes;
63+
}
64+
65+
public void clearEvents() {
66+
eventSent = false;
67+
lastEventType = null;
68+
lastEventAttributes = null;
69+
}
70+
71+
@Override
72+
protected void scheduleDelayedFlush() {
73+
// Bypass Android Handler for unit tests - use explicit flush calls instead
74+
}
75+
76+
@Override
77+
public void sendDroppedFrame(int count, int elapsed) {
78+
long currentTime = System.currentTimeMillis();
79+
80+
mockLastTrackData.put("lastFrameDropCount", count);
81+
mockLastTrackData.put("lastFrameDropDuration", elapsed);
82+
mockLastTrackData.put("lastFrameDropTime", currentTime);
83+
mockLastTrackData.put("lastUpdateTime", currentTime);
84+
85+
if (mockAggregationEnabled) {
86+
boolean hasActive = (boolean) mockAggregationStatus.get("hasActiveAggregation");
87+
int currentTotal = (int) mockAggregationStatus.get("totalLostFrames");
88+
int currentDuration = (int) mockAggregationStatus.get("totalLostFramesDuration");
89+
int eventCount = (int) mockAggregationStatus.get("eventCount");
90+
91+
if (!hasActive) {
92+
// Start new aggregation
93+
mockAggregationStatus.put("hasActiveAggregation", true);
94+
mockAggregationStatus.put("totalLostFrames", count);
95+
mockAggregationStatus.put("totalLostFramesDuration", elapsed);
96+
mockAggregationStatus.put("eventCount", 1);
97+
mockAggregationStatus.put("firstDropTimestamp", currentTime);
98+
mockAggregationStatus.put("lastDropTimestamp", currentTime);
99+
} else {
100+
// Add to existing aggregation
101+
int newEventCount = eventCount + 1;
102+
mockAggregationStatus.put("totalLostFrames", currentTotal + count);
103+
mockAggregationStatus.put("totalLostFramesDuration", currentDuration + elapsed);
104+
mockAggregationStatus.put("eventCount", newEventCount);
105+
mockAggregationStatus.put("lastDropTimestamp", currentTime);
106+
107+
if (newEventCount >= 50) {
108+
simulateFlush();
109+
}
110+
}
111+
mockLastTrackData.put("currentTotalFrames", mockAggregationStatus.get("totalLostFrames"));
112+
mockLastTrackData.put("currentTotalDuration", mockAggregationStatus.get("totalLostFramesDuration"));
113+
mockLastTrackData.put("currentEventCount", mockAggregationStatus.get("eventCount"));
114+
} else {
115+
Map<String, Object> attributes = new ConcurrentHashMap<>();
116+
attributes.put("lostFrames", count);
117+
attributes.put("lostFramesDuration", elapsed);
118+
sendVideoEvent("CONTENT_DROPPED_FRAMES", attributes);
119+
}
120+
}
121+
122+
public ConcurrentHashMap<String, Object> getLastTrackData() {
123+
return mockLastTrackData;
124+
}
125+
public Map<String, Object> getCurrentAggregationStatus() {
126+
return mockAggregationStatus;
127+
}
128+
@Override
129+
public void setDroppedFrameAggregationEnabled(boolean enabled) {
130+
if (!enabled && mockAggregationEnabled) {
131+
// Flush current aggregation when disabling
132+
simulateFlush();
133+
}
134+
this.mockAggregationEnabled = enabled;
135+
}
136+
@Override
137+
public boolean isDroppedFrameAggregationEnabled() {
138+
return mockAggregationEnabled;
139+
}
140+
public void resetAggregationState() {
141+
// Reset aggregation state but preserve last track data
142+
mockAggregationStatus.put("hasActiveAggregation", false);
143+
mockAggregationStatus.put("totalLostFrames", 0);
144+
mockAggregationStatus.put("totalLostFramesDuration", 0);
145+
mockAggregationStatus.put("eventCount", 0);
146+
mockAggregationStatus.put("firstDropTimestamp", 0L);
147+
mockAggregationStatus.put("lastDropTimestamp", 0L);
148+
149+
// Reset event capture state - only clear if no event was just sent
150+
// This is needed for external resets, not after flushing
151+
eventSent = false;
152+
lastEventType = null;
153+
lastEventAttributes = null;
154+
}
155+
public void simulateFlush() {
156+
boolean hasActive = (boolean) mockAggregationStatus.get("hasActiveAggregation");
157+
if (hasActive) {
158+
Map<String, Object> attributes = new ConcurrentHashMap<>();
159+
attributes.put("lostFrames", mockAggregationStatus.get("totalLostFrames"));
160+
attributes.put("lostFramesDuration", mockAggregationStatus.get("totalLostFramesDuration"));
161+
attributes.put("eventCount", mockAggregationStatus.get("eventCount"));
162+
attributes.put("aggregationWindowMs", 5000L); // DEFAULT_AGGREGATION_WINDOW_MS
163+
attributes.put("firstDropTimestamp", mockAggregationStatus.get("firstDropTimestamp"));
164+
attributes.put("lastDropTimestamp", mockAggregationStatus.get("lastDropTimestamp"));
165+
166+
long firstTime = (long) mockAggregationStatus.get("firstDropTimestamp");
167+
long lastTime = (long) mockAggregationStatus.get("lastDropTimestamp");
168+
attributes.put("actualAggregationDurationMs", lastTime - firstTime);
169+
170+
sendVideoEvent("CONTENT_DROPPED_FRAMES", attributes);
171+
resetAggregationAfterFlush();
172+
}
173+
}
174+
private void resetAggregationAfterFlush() {
175+
mockAggregationStatus.put("hasActiveAggregation", false);
176+
mockAggregationStatus.put("totalLostFrames", 0);
177+
mockAggregationStatus.put("totalLostFramesDuration", 0);
178+
mockAggregationStatus.put("eventCount", 0);
179+
mockAggregationStatus.put("firstDropTimestamp", 0L);
180+
mockAggregationStatus.put("lastDropTimestamp", 0L);
181+
// DON'T reset eventSent flag here - we want to preserve it after flushing
182+
}
183+
}

0 commit comments

Comments
 (0)