From 852a8a00273c9128efeed0217a8e5d3fcd8bf780 Mon Sep 17 00:00:00 2001 From: Lena Date: Sat, 1 Aug 2026 00:00:00 +0000 Subject: app: harden mirroring lifecycle and state --- .../java/invalid/lena/scrcpy/AtomicFilesTest.java | 12 +++ .../java/invalid/lena/scrcpy/AudioStreamTest.java | 13 +++- .../invalid/lena/scrcpy/ControlMessagesTest.java | 29 +++++++- .../invalid/lena/scrcpy/ControlStreamTest.java | 10 +-- .../java/invalid/lena/scrcpy/CrashlogTest.java | 36 +++++++++ .../test/java/invalid/lena/scrcpy/DevicesTest.java | 16 +++- .../invalid/lena/scrcpy/TouchGeometryTest.java | 48 ++++++++++++ .../java/invalid/lena/scrcpy/TouchMapTest.java | 85 ++++++++++++++++++++++ .../java/invalid/lena/scrcpy/VideoQueueTest.java | 53 ++++++++++++++ .../java/invalid/lena/scrcpy/VideoStreamTest.java | 61 +--------------- .../test/java/invalid/lena/scrcpy/WireTest.java | 58 +++++++++++---- 11 files changed, 339 insertions(+), 82 deletions(-) create mode 100644 app/src/test/java/invalid/lena/scrcpy/CrashlogTest.java create mode 100644 app/src/test/java/invalid/lena/scrcpy/TouchGeometryTest.java create mode 100644 app/src/test/java/invalid/lena/scrcpy/TouchMapTest.java create mode 100644 app/src/test/java/invalid/lena/scrcpy/VideoQueueTest.java (limited to 'app/src/test') diff --git a/app/src/test/java/invalid/lena/scrcpy/AtomicFilesTest.java b/app/src/test/java/invalid/lena/scrcpy/AtomicFilesTest.java index 7124fd2..1cafb0c 100644 --- a/app/src/test/java/invalid/lena/scrcpy/AtomicFilesTest.java +++ b/app/src/test/java/invalid/lena/scrcpy/AtomicFilesTest.java @@ -51,4 +51,16 @@ public class AtomicFilesTest { assertArrayEquals("good2".getBytes(), Files.readAllBytes(f.toPath())); assertFalse(new File(dir.getRoot(), "x.tmp").exists()); } + + @Test + public void writeRemovesEveryStaleTemp() throws Exception { + File f = new File(dir.getRoot(), "x"); + Files.write(new File(dir.getRoot(), "x.tmp-old").toPath(), new byte[]{1}); + Files.write(new File(dir.getRoot(), "x.tmp-new").toPath(), new byte[]{2}); + + AtomicFiles.write(f, "good".getBytes()); + + assertFalse(new File(dir.getRoot(), "x.tmp-old").exists()); + assertFalse(new File(dir.getRoot(), "x.tmp-new").exists()); + } } diff --git a/app/src/test/java/invalid/lena/scrcpy/AudioStreamTest.java b/app/src/test/java/invalid/lena/scrcpy/AudioStreamTest.java index 076d27c..005a2e3 100644 --- a/app/src/test/java/invalid/lena/scrcpy/AudioStreamTest.java +++ b/app/src/test/java/invalid/lena/scrcpy/AudioStreamTest.java @@ -11,6 +11,7 @@ import java.io.DataOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; public class AudioStreamTest { @@ -116,8 +117,18 @@ public class AudioStreamTest { public void errorCodecDoesNotStart() throws Exception { byte[] bytes = fourcc(1); RecordingFrames sink = new RecordingFrames(); - new AudioStream(new ByteArrayInputStream(bytes), sink).run(); + AtomicInteger fatal = new AtomicInteger(); + new AudioStream(new ByteArrayInputStream(bytes), sink, fatal::incrementAndGet).run(); assertEquals(0, sink.starts); assertEquals(0, sink.feeds.size()); + assertEquals(1, fatal.get()); + } + + @Test + public void disabledCodecIsNotFatal() throws Exception { + AtomicInteger fatal = new AtomicInteger(); + new AudioStream(new ByteArrayInputStream(fourcc(0)), + new RecordingFrames(), fatal::incrementAndGet).run(); + assertEquals(0, fatal.get()); } } diff --git a/app/src/test/java/invalid/lena/scrcpy/ControlMessagesTest.java b/app/src/test/java/invalid/lena/scrcpy/ControlMessagesTest.java index 1890860..142bb34 100644 --- a/app/src/test/java/invalid/lena/scrcpy/ControlMessagesTest.java +++ b/app/src/test/java/invalid/lena/scrcpy/ControlMessagesTest.java @@ -15,6 +15,28 @@ public class ControlMessagesTest { ControlMessages.setClipboard(0, false, new String(chars)); } + @Test + public void clipboardLimitsMatchScrcpyMessageSize() { + // The server caps a whole control message at MESSAGE_MAX_SIZE and + // derives each direction's text limit by subtracting that + // direction's header. Derive it the same way rather than copying + // the numbers, so a server bump shows up here as a failure. + final int messageMax = 1 << 18; // 256 KiB + assertEquals(messageMax - (1 + 8 + 1 + 4), // SET_CLIPBOARD + ControlMessages.MAX_CLIPBOARD_BYTES); + assertEquals(messageMax - (1 + 4), // DeviceMessage + ControlMessages.MAX_DEVICE_CLIPBOARD_BYTES); + } + + @Test + public void clipboardAcceptsExactlyTheLimit() { + char[] chars = new char[ControlMessages.MAX_CLIPBOARD_BYTES]; + java.util.Arrays.fill(chars, 'a'); // 1 byte per char in UTF-8 + byte[] m = ControlMessages.setClipboard(0, false, new String(chars)); + assertEquals(14 + ControlMessages.MAX_CLIPBOARD_BYTES, m.length); + assertEquals(ControlMessages.MAX_CLIPBOARD_BYTES, Wire.readBe32(m, 10)); + } + @Test public void touchByteLayout() { // ACTION_DOWN=0, single finger, target 1080x2400, x=100, y=200, full pressure @@ -46,9 +68,14 @@ public class ControlMessagesTest { assertEquals(0, Wire.readBe32(m, 10)); } + @Test + public void resetVideoByteLayout() { + assertArrayEquals(new byte[]{17}, ControlMessages.resetVideo()); + } + @Test public void setClipboardWithUtf8Payload() { - String text = "héllo 🎉"; // includes a 4-byte surrogate pair + String text = "héllo \ud83c\udf89"; // includes a 4-byte surrogate pair byte[] expected = text.getBytes(StandardCharsets.UTF_8); byte[] m = ControlMessages.setClipboard(0xdeadbeefcafeL, /* paste */ true, text); assertEquals(1 + 8 + 1 + 4 + expected.length, m.length); diff --git a/app/src/test/java/invalid/lena/scrcpy/ControlStreamTest.java b/app/src/test/java/invalid/lena/scrcpy/ControlStreamTest.java index 8573d3b..f3359e4 100644 --- a/app/src/test/java/invalid/lena/scrcpy/ControlStreamTest.java +++ b/app/src/test/java/invalid/lena/scrcpy/ControlStreamTest.java @@ -36,7 +36,7 @@ public class ControlStreamTest { RecordingSink sink = new RecordingSink(); ControlStream cs = new ControlStream( new ByteArrayInputStream(bos.toByteArray()), - new ByteArrayOutputStream()); + new ByteArrayOutputStream(), null); cs.setInboundSink(sink); cs.runReader(); // synchronous; returns on EOF @@ -55,7 +55,7 @@ public class ControlStreamTest { RecordingSink sink = new RecordingSink(); ControlStream cs = new ControlStream( new ByteArrayInputStream(bos.toByteArray()), - new ByteArrayOutputStream()); + new ByteArrayOutputStream(), null); cs.setInboundSink(sink); cs.runReader(); @@ -73,7 +73,7 @@ public class ControlStreamTest { RecordingSink sink = new RecordingSink(); ControlStream cs = new ControlStream( new ByteArrayInputStream(bos.toByteArray()), - new ByteArrayOutputStream()); + new ByteArrayOutputStream(), null); cs.setInboundSink(sink); cs.runReader(); assertEquals(0, sink.clipboards.size()); @@ -87,7 +87,7 @@ public class ControlStreamTest { // outbox is empty AND we've signalled stop. ByteArrayOutputStream out = new ByteArrayOutputStream(); ControlStream cs = new ControlStream( - new ByteArrayInputStream(new byte[0]), out); + new ByteArrayInputStream(new byte[0]), out, null); byte[] a = new byte[]{1, 2, 3}; byte[] b = new byte[]{4, 5}; @@ -118,7 +118,7 @@ public class ControlStreamTest { // event must be preserved and end up last in the drain. ByteArrayOutputStream out = new ByteArrayOutputStream(); ControlStream cs = new ControlStream( - new ByteArrayInputStream(new byte[0]), out); + new ByteArrayInputStream(new byte[0]), out, null); for (int i = 0; i < 256; i++) { cs.send(ControlMessages.touch(/* MOVE */ 2, 0L, i, i, 1080, 2400, 0xffff, 0, 0)); diff --git a/app/src/test/java/invalid/lena/scrcpy/CrashlogTest.java b/app/src/test/java/invalid/lena/scrcpy/CrashlogTest.java new file mode 100644 index 0000000..41c05a1 --- /dev/null +++ b/app/src/test/java/invalid/lena/scrcpy/CrashlogTest.java @@ -0,0 +1,36 @@ +package invalid.lena.scrcpy; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.File; +import java.nio.file.Files; + +public class CrashlogTest { + + @Rule + public TemporaryFolder dir = new TemporaryFolder(); + + @Test + public void pruneKeepsNewestCrashlogsOnly() throws Exception { + for (int i = 0; i < 7; i++) { + File file = new File(dir.getRoot(), "crash-" + i + ".log"); + Files.write(file.toPath(), new byte[]{(byte) i}); + assertTrue(file.setLastModified(1_000L + i)); + } + File unrelated = dir.newFile("notes.txt"); + + Crashlog.prune(dir.getRoot(), 5); + + File[] logs = dir.getRoot().listFiles((parent, name) -> name.endsWith(".log")); + assertEquals(5, logs.length); + assertFalse(new File(dir.getRoot(), "crash-0.log").exists()); + assertFalse(new File(dir.getRoot(), "crash-1.log").exists()); + assertTrue(unrelated.exists()); + } +} diff --git a/app/src/test/java/invalid/lena/scrcpy/DevicesTest.java b/app/src/test/java/invalid/lena/scrcpy/DevicesTest.java index dcd04ca..bde8af8 100644 --- a/app/src/test/java/invalid/lena/scrcpy/DevicesTest.java +++ b/app/src/test/java/invalid/lena/scrcpy/DevicesTest.java @@ -2,6 +2,7 @@ package invalid.lena.scrcpy; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import org.junit.Test; @@ -20,9 +21,10 @@ public class DevicesTest { } @Test - public void parseNonArrayReturnsEmpty() { - assertTrue(Devices.parse("{not json").isEmpty()); - assertTrue(Devices.parse("{\"host\":\"x\"}").isEmpty()); + public void parseNonArrayFailsLoudly() { + assertThrows(IllegalArgumentException.class, () -> Devices.parse("{not json")); + assertThrows(IllegalArgumentException.class, + () -> Devices.parse("{\"host\":\"x\"}")); } @Test @@ -53,6 +55,14 @@ public class DevicesTest { assertEquals(43210, out.get(1).port); } + @Test + public void serializeRejectsInvalidRows() { + assertThrows(IllegalStateException.class, () -> Devices.serialize(Arrays.asList( + new Devices.Device("bad host", 5555)))); + assertThrows(IllegalStateException.class, () -> Devices.serialize(Arrays.asList( + new Devices.Device("target", 0)))); + } + @Test public void parseAddressSplitsHostAndPort() { Devices.Device d = Devices.parseAddress(" 192.168.1.42:41234 "); diff --git a/app/src/test/java/invalid/lena/scrcpy/TouchGeometryTest.java b/app/src/test/java/invalid/lena/scrcpy/TouchGeometryTest.java new file mode 100644 index 0000000..5db6d32 --- /dev/null +++ b/app/src/test/java/invalid/lena/scrcpy/TouchGeometryTest.java @@ -0,0 +1,48 @@ +package invalid.lena.scrcpy; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import org.junit.Test; + +public class TouchGeometryTest { + + @Test + public void viewportBeforeTargetDoesNotEnableInput() { + TouchGeometry geometry = new TouchGeometry(); + geometry.setViewport(0, 0, 0, 1080, 608); + assertNull(geometry.snapshot()); + } + + @Test + public void targetResizeInvalidatesViewport() { + TouchGeometry geometry = new TouchGeometry(); + long first = 7; + geometry.setTargetSize(first, 1920, 1080); + geometry.setViewport(first, 0, 100, 1080, 608); + + long second = 8; + geometry.setTargetSize(second, 1080, 1920); + + assertNull(geometry.snapshot()); + geometry.setViewport(first, 100, 0, 608, 1080); + assertNull(geometry.snapshot()); + + geometry.setViewport(second, 100, 0, 608, 1080); + TouchGeometry.Snapshot snapshot = geometry.snapshot(); + assertEquals(1080, snapshot.targetW); + assertEquals(1920, snapshot.targetH); + assertEquals(100, snapshot.x); + assertEquals(608, snapshot.w); + } + + @Test + public void emptyViewportDisablesInput() { + TouchGeometry geometry = new TouchGeometry(); + long version = 1; + geometry.setTargetSize(version, 1920, 1080); + geometry.setViewport(version, 0, 0, 1080, 608); + geometry.setViewport(version, 0, 0, 0, 0); + assertNull(geometry.snapshot()); + } +} diff --git a/app/src/test/java/invalid/lena/scrcpy/TouchMapTest.java b/app/src/test/java/invalid/lena/scrcpy/TouchMapTest.java new file mode 100644 index 0000000..becacc1 --- /dev/null +++ b/app/src/test/java/invalid/lena/scrcpy/TouchMapTest.java @@ -0,0 +1,85 @@ +package invalid.lena.scrcpy; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +// The arithmetic that decides where a tap lands. Until this existed the +// mapping was verified by nothing: the only coverage was an emulator +// self-mirror, where the target and the view are the same size, so the +// transform is the identity and every scaling or offset bug passes. +public class TouchMapTest { + + // A 1080x2400 phone showing a 1080x1920 target: fitted to 864x1920, + // leaving 108px bars left and right. + private static final int VIEW_X = 108, VIEW_W = 864, TARGET_W = 1080; + + @Test + public void leftEdgeOfTheVideoIsTheLeftEdgeOfTheTarget() { + assertEquals(0, TouchMap.map(VIEW_X, VIEW_X, VIEW_W, TARGET_W)); + } + + // The view has fewer pixels than the target, so the last column the + // mapping can produce is floor((span-1) * target / span) - 1078 here, + // not 1079. That is granularity, not an off-by-one: a 864px-wide view + // cannot address all 1080 target columns. What matters is that the + // right edge lands on the last addressable column and stays inside. + @Test + public void rightEdgeIsTheLastAddressableColumn() { + int got = TouchMap.map(VIEW_X + VIEW_W - 1, VIEW_X, VIEW_W, TARGET_W); + assertEquals((VIEW_W - 1) * TARGET_W / VIEW_W, got); + assertTrue("must stay inside the target", got < TARGET_W); + assertTrue("must be within one step of the far edge", + got >= TARGET_W - (TARGET_W / VIEW_W) - 1); + } + + @Test + public void centreMapsToCentre() { + assertEquals(TARGET_W / 2, TouchMap.map(VIEW_X + VIEW_W / 2, VIEW_X, VIEW_W, TARGET_W)); + } + + @Test + public void theLetterboxBarIsClampedNotWrapped() { + // Touches on the bars must land on the near edge. Subtracting the + // origin without clamping would make the left bar negative and the + // right bar overflow past the target's width. + assertEquals(0, TouchMap.map(0, VIEW_X, VIEW_W, TARGET_W)); + assertEquals(0, TouchMap.map(VIEW_X - 50, VIEW_X, VIEW_W, TARGET_W)); + int far = (VIEW_W - 1) * TARGET_W / VIEW_W; // last addressable column + assertEquals(far, TouchMap.map(VIEW_X + VIEW_W + 50, VIEW_X, VIEW_W, TARGET_W)); + assertEquals(far, TouchMap.map(2000, VIEW_X, VIEW_W, TARGET_W)); + } + + @Test + public void neverEscapesTheTargetForAnyCoordinate() { + for (int c = -500; c < 3000; c++) { + int t = TouchMap.map(c, VIEW_X, VIEW_W, TARGET_W); + if (t < 0 || t >= TARGET_W) { + throw new AssertionError("coord " + c + " mapped outside the target: " + t); + } + } + } + + @Test + public void noOverflowAtFourK() { + // 3840 * 2160 exceeds int when multiplied in 32 bits; the mapping + // has to widen before it scales. + assertEquals(2159, TouchMap.map(3839, 0, 3840, 2160)); + assertEquals(0, TouchMap.map(0, 0, 3840, 2160)); + } + + @Test + public void identityWhenTheViewMatchesTheTarget() { + // The self-mirror case the e2e exercises: nothing should move. + for (int c : new int[]{0, 1, 539, 1079}) { + assertEquals(c, TouchMap.map(c, 0, 1080, 1080)); + } + } + + @Test + public void degenerateGeometryIsRefusedRatherThanDividingByZero() { + assertEquals(0, TouchMap.map(100, 0, 0, 1080)); + assertEquals(0, TouchMap.map(100, 0, 1080, 0)); + } +} diff --git a/app/src/test/java/invalid/lena/scrcpy/VideoQueueTest.java b/app/src/test/java/invalid/lena/scrcpy/VideoQueueTest.java new file mode 100644 index 0000000..00b19a5 --- /dev/null +++ b/app/src/test/java/invalid/lena/scrcpy/VideoQueueTest.java @@ -0,0 +1,53 @@ +package invalid.lena.scrcpy; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +public class VideoQueueTest { + + private static VideoQueue.Frame frame(int id, boolean config, boolean keyframe) { + return new VideoQueue.Frame(new byte[]{(byte) id}, id, config, keyframe); + } + + @Test + public void rejectsDeltaUntilKeyframe() { + VideoQueue queue = new VideoQueue(4, 4); + assertFalse(queue.offer(frame(1, false, false))); + assertTrue(queue.offer(frame(2, false, true))); + assertTrue(queue.offer(frame(3, false, false))); + } + + @Test + public void overflowKeepsQueuedDecoderGeneration() { + VideoQueue queue = new VideoQueue(3, 3); + VideoQueue.Frame keyframe = frame(1, false, true); + VideoQueue.Frame delta = frame(2, false, false); + assertTrue(queue.offer(keyframe)); + assertTrue(queue.offer(delta)); + assertTrue(queue.offer(frame(3, false, false))); + assertFalse(queue.offer(frame(4, false, false))); + assertTrue(queue.needsKeyframe()); + assertFalse(queue.offer(frame(5, false, false))); + assertSame(keyframe, queue.poll()); + assertSame(delta, queue.poll()); + } + + @Test + public void newKeyframeReplacesOldMediaButKeepsConfig() { + VideoQueue queue = new VideoQueue(4, 4); + VideoQueue.Frame config = frame(1, true, false); + VideoQueue.Frame nextKeyframe = frame(4, false, true); + assertTrue(queue.offer(config)); + assertTrue(queue.offer(frame(2, false, true))); + assertTrue(queue.offer(frame(3, false, false))); + + assertTrue(queue.offer(nextKeyframe)); + + assertSame(config, queue.poll()); + assertSame(nextKeyframe, queue.poll()); + assertTrue(queue.isEmpty()); + } +} diff --git a/app/src/test/java/invalid/lena/scrcpy/VideoStreamTest.java b/app/src/test/java/invalid/lena/scrcpy/VideoStreamTest.java index 9d132f6..38d7416 100644 --- a/app/src/test/java/invalid/lena/scrcpy/VideoStreamTest.java +++ b/app/src/test/java/invalid/lena/scrcpy/VideoStreamTest.java @@ -19,8 +19,8 @@ public class VideoStreamTest { private static final class RecordingFrames implements VideoFrames { static final class Cfg { final int fourcc, w, h; Cfg(int f,int w,int h){this.fourcc=f;this.w=w;this.h=h;} } - static final class Feed { final byte[] data; final long ptsUs; final boolean isCfg; - Feed(byte[] d,long p,boolean c){data=d;ptsUs=p;isCfg=c;} } + static final class Feed { final byte[] data; final long ptsUs; final boolean isCfg, isKey; + Feed(byte[] d,long p,boolean c,boolean k){data=d;ptsUs=p;isCfg=c;isKey=k;} } final List configs = new ArrayList<>(); final List reconfigs = new ArrayList<>(); @@ -29,7 +29,7 @@ public class VideoStreamTest { @Override public void configure(int f, int w, int h) { configs.add(new Cfg(f, w, h)); } @Override public void reconfigure(int f, int w, int h) { reconfigs.add(new Cfg(f, w, h)); } - @Override public void feed(byte[] d, long pts, boolean c){ feeds.add(new Feed(d, pts, c)); } + @Override public void feed(byte[] d, long pts, boolean c, boolean k){ feeds.add(new Feed(d, pts, c, k)); } @Override public void release() { releases++; } } @@ -106,6 +106,7 @@ public class VideoStreamTest { // keyframe: keyframe flag is stripped from ptsUs assertEquals(1_000_000L, sink.feeds.get(1).ptsUs); assertEquals(5, sink.feeds.get(1).data.length); + assertTrue("second is keyframe", sink.feeds.get(1).isKey); assertEquals(2_000_000L, sink.feeds.get(2).ptsUs); } @@ -170,60 +171,6 @@ public class VideoStreamTest { assertEquals(0, sink.configs.size()); } - @Test - public void recorderTapSeesMetaAndFramesWithFlags() throws Exception { - byte[] csd = new byte[]{0x67, 0x42, (byte)0xe0, 0x1e}; - byte[] keyframe = new byte[]{10, 11, 12}; - byte[] delta = new byte[]{20, 21}; - long PTS_CFG = 1L << 62; - long PTS_KEY = (1L << 61) | 1_000_000L; - long PTS_DELT = 2_000_000L; - - byte[] bytes = cat( - fourcc(Wire.CODEC_H264), - sessionMeta(800, 600, false), - frame(PTS_CFG, csd), - frame(PTS_KEY, keyframe), - frame(PTS_DELT, delta)); - - RecordingFrames sink = new RecordingFrames(); - RecordingRecorder rec = new RecordingRecorder(); - VideoStream s = new VideoStream(new ByteArrayInputStream(bytes), sink, null); - s.setRecorder(rec); - s.run(); - - assertEquals(1, rec.metas.size()); - assertEquals(800, rec.metas.get(0)[1]); - assertEquals(600, rec.metas.get(0)[2]); - - assertEquals(3, rec.frames.size()); - assertTrue("cfg frame", rec.frames.get(0).isConfig); - assertTrue("key frame", rec.frames.get(1).isKeyframe); - assertTrue("delta is not config or key", - !rec.frames.get(2).isConfig && !rec.frames.get(2).isKeyframe); - assertEquals(csd.length, rec.frames.get(0).data.length); - assertEquals(keyframe.length, rec.frames.get(1).data.length); - assertEquals(delta.length, rec.frames.get(2).data.length); - } - - private static final class RecordingRecorder implements VideoRecorder { - static final class FrameRec { - final byte[] data; final long ptsUs; - final boolean isConfig, isKeyframe; - FrameRec(byte[] d, long p, boolean c, boolean k) { - data = d; ptsUs = p; isConfig = c; isKeyframe = k; - } - } - final List metas = new ArrayList<>(); - final List frames = new ArrayList<>(); - int closes = 0; - @Override public void onMeta(int f, int w, int h) { metas.add(new int[]{f, w, h}); } - @Override public void onFrame(byte[] d, long p, boolean c, boolean k) { - frames.add(new FrameRec(d, p, c, k)); - } - @Override public void close() { closes++; } - } - @Test public void payloadIntegrity() throws Exception { byte[] payload = new byte[1024]; diff --git a/app/src/test/java/invalid/lena/scrcpy/WireTest.java b/app/src/test/java/invalid/lena/scrcpy/WireTest.java index 9cee6c5..e4ada33 100644 --- a/app/src/test/java/invalid/lena/scrcpy/WireTest.java +++ b/app/src/test/java/invalid/lena/scrcpy/WireTest.java @@ -2,15 +2,32 @@ package invalid.lena.scrcpy; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import org.junit.Test; import java.io.ByteArrayInputStream; import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; public class WireTest { + @Test + public void readFullyRejectsZeroProgress() throws Exception { + InputStream in = new InputStream() { + @Override public int read() { return 0; } + @Override public int read(byte[] b, int off, int len) { return 0; } + }; + try { + Wire.readFully(in, new byte[1]); + fail("zero-progress input accepted"); + } catch (IOException expected) { + assertTrue(expected.getMessage().contains("no progress")); + } + } + @Test public void be32RoundTrip() { byte[] b = new byte[4]; @@ -67,28 +84,39 @@ public class WireTest { } } + // Derive the id the way scrcpy's Codec enums do: the ASCII name, right + // aligned in a big-endian uint32, NUL-padded on the left. Deriving it + // from the name is the whole point - the previous version of this test + // asserted the constants against hand-copied literals, so it passed + // while CODEC_AV1 held 'av01' instead of '\0av1' and AV1 could never + // negotiate. + private static int idOf(String name) { + if (name.length() > 4) throw new IllegalArgumentException(name); + int v = 0; + for (int i = 0; i < 4 - name.length(); i++) v <<= 8; + for (int i = 0; i < name.length(); i++) v = (v << 8) | name.charAt(i); + return v; + } + @Test public void fourccConstantsMatchScrcpy() { - // Big-endian uint32 of the 4-char ASCII tag, NUL-padded on the LEFT - // for short names (raw, aac) - mirrors AudioCodec / VideoCodec. - assertEquals(0x68_32_36_34, Wire.CODEC_H264); // "h264" - assertEquals(0x68_32_36_35, Wire.CODEC_H265); // "h265" - assertEquals(0x61_76_30_31, Wire.CODEC_AV1); // "av01" - assertEquals(0x6f_70_75_73, Wire.CODEC_OPUS); // "opus" - assertEquals(0x66_6c_61_63, Wire.CODEC_FLAC); // "flac" - assertEquals(0x00_61_61_63, Wire.CODEC_AAC); // "\0aac" - assertEquals(0x00_72_61_77, Wire.CODEC_RAW); // "\0raw" + // Names as spelled in scrcpy's VideoCodec / AudioCodec enums. + assertEquals(idOf("h264"), Wire.CODEC_H264); + assertEquals(idOf("h265"), Wire.CODEC_H265); + assertEquals(idOf("av1"), Wire.CODEC_AV1); + assertEquals(idOf("opus"), Wire.CODEC_OPUS); + assertEquals(idOf("raw"), Wire.CODEC_RAW); } @Test - public void fourccName() { + public void fourccNameRoundTripsEveryCodec() { assertEquals("h264", Wire.fourccName(Wire.CODEC_H264)); + assertEquals("h265", Wire.fourccName(Wire.CODEC_H265)); + assertEquals("av1", Wire.fourccName(Wire.CODEC_AV1)); assertEquals("opus", Wire.fourccName(Wire.CODEC_OPUS)); + assertEquals("raw", Wire.fourccName(Wire.CODEC_RAW)); + // Left-NUL padding is the rule, not a special case for raw. + assertEquals("aac", Wire.fourccName(0x00_61_61_63)); } - @Test - public void fourccNameTrimsLeadingNuls() { - assertEquals("raw", Wire.fourccName(Wire.CODEC_RAW)); - assertEquals("aac", Wire.fourccName(Wire.CODEC_AAC)); - } } -- cgit v1.2.3