From 781aa6767f9225491d6e902e28bd0cc2916a7702 Mon Sep 17 00:00:00 2001 From: Compot Developer Date: Sat, 31 Oct 2026 21:34:42 +0000 Subject: [PATCH 1/2] test: fix two flaky tests (#257) RtpListenerTest read the status and payload-type lists with Collections.frequency() and for-each while the session thread added to them; a synchronizedList does not guard iteration, hence the ConcurrentModificationException. The recorder now uses CopyOnWriteArrayList. JieliTest kept the diagnostic port for diagnosticMs = 500, so the port lived about a second (until the next receive timeout); a pause of more than that on a loaded runner before the test sent its packet lost it and failed assertNotNull(d) at line 825 (reproduced with a 1.2 s pause). The port now lives 3 s and the test waits up to 6 s for it to be freed. --- .../app/src/test/java/com/openipc/decoder/JieliTest.java | 6 ++++-- .../test/java/com/openipc/decoder/RtpListenerTest.java | 9 ++++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/android/app/src/test/java/com/openipc/decoder/JieliTest.java b/android/app/src/test/java/com/openipc/decoder/JieliTest.java index 0576f43f..616f9e6f 100644 --- a/android/app/src/test/java/com/openipc/decoder/JieliTest.java +++ b/android/app/src/test/java/com/openipc/decoder/JieliTest.java @@ -799,7 +799,9 @@ public void noVideoListensElsewhereAndAsksAgain() throws Exception { Jieli.noVideoMs = 300; Jieli.reopenMs = 700; Jieli.diagnosticPorts = new int[] {diag}; - Jieli.diagnosticMs = 500; + // The port lives this long (then up to a receive timeout more): long enough for the + // packet below to arrive on a loaded CI runner, where a pause of a second failed it (#257) + Jieli.diagnosticMs = 3000; try (FakeToy toy = new FakeToy(60000, true)) { toy.rtpPort = rtp; toy.sendVideo = false; @@ -827,7 +829,7 @@ public void noVideoListensElsewhereAndAsksAgain() throws Exception { Map one = (Map) d.get(String.valueOf(diag)); assertEquals(1, one.get("packets")); // The diagnostic port is freed after diagnosticMs while the session goes on - until = now() + 3000; + until = now() + 6000; boolean freed = false; while (now() < until && !freed) { try (DatagramSocket again = new DatagramSocket(diag, LOOPBACK)) { diff --git a/android/app/src/test/java/com/openipc/decoder/RtpListenerTest.java b/android/app/src/test/java/com/openipc/decoder/RtpListenerTest.java index bcf8cfbe..34636b9c 100644 --- a/android/app/src/test/java/com/openipc/decoder/RtpListenerTest.java +++ b/android/app/src/test/java/com/openipc/decoder/RtpListenerTest.java @@ -28,6 +28,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -101,9 +102,11 @@ public void badUrlsAreRefused() { // ====================== Session ====================== private static final class Recorder implements RtspSession.Callback { - final List statuses = Collections.synchronizedList(new ArrayList<>()); - final List sdps = Collections.synchronizedList(new ArrayList<>()); - final List pts = Collections.synchronizedList(new ArrayList<>()); + // Filled by the session thread while the test reads them: Collections.frequency() and + // for-each iterate, which a synchronizedList does not guard (#257) + final List statuses = new CopyOnWriteArrayList<>(); + final List sdps = new CopyOnWriteArrayList<>(); + final List pts = new CopyOnWriteArrayList<>(); volatile CountDownLatch latch = new CountDownLatch(3); @Override public void onStatus(String status) { statuses.add(status); } From fd1e71bb1f1c4dcbe5c61e936b346ad1d342b378 Mon Sep 17 00:00:00 2001 From: Compot Developer Date: Sat, 31 Oct 2026 21:51:28 +0000 Subject: [PATCH 2/2] test: JieliTest waits up to 8 s and asserts on the snapshot it waited for (Qodo on #259) --- .../app/src/test/java/com/openipc/decoder/JieliTest.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/android/app/src/test/java/com/openipc/decoder/JieliTest.java b/android/app/src/test/java/com/openipc/decoder/JieliTest.java index 616f9e6f..e3d460a5 100644 --- a/android/app/src/test/java/com/openipc/decoder/JieliTest.java +++ b/android/app/src/test/java/com/openipc/decoder/JieliTest.java @@ -815,13 +815,16 @@ public void noVideoListensElsewhereAndAsksAgain() throws Exception { byte[] p = {(byte) 0x80, 97, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 9}; u.send(new DatagramPacket(p, p.length, new InetSocketAddress(LOOPBACK, diag))); } - until = now() + 3000; + // Generous on a loaded runner; the asserts read the snapshot the loop stopped on + // (Qodo on #259) + until = now() + 8000; + Map st = r.client.stats(); while (now() < until && (Collections.frequency(toy.topics(), Jieli.OPEN_RT_STREAM) < 2 - || r.client.stats().get("diagnostic_ports") == null)) { + || st.get("diagnostic_ports") == null)) { Thread.sleep(20); + st = r.client.stats(); } assertTrue(Collections.frequency(toy.topics(), Jieli.OPEN_RT_STREAM) >= 2); - Map st = r.client.stats(); @SuppressWarnings("unchecked") Map d = (Map) st.get("diagnostic_ports"); assertNotNull(d);