From b6e3ac184d1b18d25505d4c20249c65a8671b9bd Mon Sep 17 00:00:00 2001 From: rankun Date: Sat, 20 Jun 2020 10:54:10 +0800 Subject: [PATCH] feat: update server --- server/build.gradle | 25 +- .../genymobile/scrcpy/CodecOptionsTest.java | 114 ++++++ .../scrcpy/ControlMessageReaderTest.java | 374 ++++++++++++++++++ .../scrcpy/DeviceMessageWriterTest.java | 35 ++ .../genymobile/scrcpy/StringUtilsTest.java | 42 ++ 5 files changed, 567 insertions(+), 23 deletions(-) create mode 100644 server/src/test/java/com/genymobile/scrcpy/CodecOptionsTest.java create mode 100644 server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java create mode 100644 server/src/test/java/com/genymobile/scrcpy/DeviceMessageWriterTest.java create mode 100644 server/src/test/java/com/genymobile/scrcpy/StringUtilsTest.java diff --git a/server/build.gradle b/server/build.gradle index 66705208b..c8ff85d64 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -1,34 +1,13 @@ apply plugin: 'com.android.application' -buildscript { - - repositories { - google() - jcenter() - } - dependencies { - classpath 'com.android.tools.build:gradle:3.1.1' - - // NOTE: Do not place your application dependencies here; they belong - // in the individual module build.gradle files - } -} - -allprojects { - repositories { - google() - jcenter() - } -} - android { compileSdkVersion 29 defaultConfig { applicationId "com.genymobile.scrcpy" minSdkVersion 21 targetSdkVersion 29 - versionCode 5 - versionName "1.12.1" + versionCode 16 + versionName "1.14" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/server/src/test/java/com/genymobile/scrcpy/CodecOptionsTest.java b/server/src/test/java/com/genymobile/scrcpy/CodecOptionsTest.java new file mode 100644 index 000000000..ad8022587 --- /dev/null +++ b/server/src/test/java/com/genymobile/scrcpy/CodecOptionsTest.java @@ -0,0 +1,114 @@ +package com.genymobile.scrcpy; + +import org.junit.Assert; +import org.junit.Test; + +import java.util.List; + +public class CodecOptionsTest { + + @Test + public void testIntegerImplicit() { + List codecOptions = CodecOption.parse("some_key=5"); + + Assert.assertEquals(1, codecOptions.size()); + + CodecOption option = codecOptions.get(0); + Assert.assertEquals("some_key", option.getKey()); + Assert.assertEquals(5, option.getValue()); + } + + @Test + public void testInteger() { + List codecOptions = CodecOption.parse("some_key:int=5"); + + Assert.assertEquals(1, codecOptions.size()); + + CodecOption option = codecOptions.get(0); + Assert.assertEquals("some_key", option.getKey()); + Assert.assertTrue(option.getValue() instanceof Integer); + Assert.assertEquals(5, option.getValue()); + } + + @Test + public void testLong() { + List codecOptions = CodecOption.parse("some_key:long=5"); + + Assert.assertEquals(1, codecOptions.size()); + + CodecOption option = codecOptions.get(0); + Assert.assertEquals("some_key", option.getKey()); + Assert.assertTrue(option.getValue() instanceof Long); + Assert.assertEquals(5L, option.getValue()); + } + + @Test + public void testFloat() { + List codecOptions = CodecOption.parse("some_key:float=4.5"); + + Assert.assertEquals(1, codecOptions.size()); + + CodecOption option = codecOptions.get(0); + Assert.assertEquals("some_key", option.getKey()); + Assert.assertTrue(option.getValue() instanceof Float); + Assert.assertEquals(4.5f, option.getValue()); + } + + @Test + public void testString() { + List codecOptions = CodecOption.parse("some_key:string=some_value"); + + Assert.assertEquals(1, codecOptions.size()); + + CodecOption option = codecOptions.get(0); + Assert.assertEquals("some_key", option.getKey()); + Assert.assertTrue(option.getValue() instanceof String); + Assert.assertEquals("some_value", option.getValue()); + } + + @Test + public void testStringEscaped() { + List codecOptions = CodecOption.parse("some_key:string=warning\\,this_is_not=a_new_key"); + + Assert.assertEquals(1, codecOptions.size()); + + CodecOption option = codecOptions.get(0); + Assert.assertEquals("some_key", option.getKey()); + Assert.assertTrue(option.getValue() instanceof String); + Assert.assertEquals("warning,this_is_not=a_new_key", option.getValue()); + } + + @Test + public void testList() { + List codecOptions = CodecOption.parse("a=1,b:int=2,c:long=3,d:float=4.5,e:string=a\\,b=c"); + + Assert.assertEquals(5, codecOptions.size()); + + CodecOption option; + + option = codecOptions.get(0); + Assert.assertEquals("a", option.getKey()); + Assert.assertTrue(option.getValue() instanceof Integer); + Assert.assertEquals(1, option.getValue()); + + option = codecOptions.get(1); + Assert.assertEquals("b", option.getKey()); + Assert.assertTrue(option.getValue() instanceof Integer); + Assert.assertEquals(2, option.getValue()); + + option = codecOptions.get(2); + Assert.assertEquals("c", option.getKey()); + Assert.assertTrue(option.getValue() instanceof Long); + Assert.assertEquals(3L, option.getValue()); + + option = codecOptions.get(3); + Assert.assertEquals("d", option.getKey()); + Assert.assertTrue(option.getValue() instanceof Float); + Assert.assertEquals(4.5f, option.getValue()); + + option = codecOptions.get(4); + Assert.assertEquals("e", option.getKey()); + Assert.assertTrue(option.getValue() instanceof String); + Assert.assertEquals("a,b=c", option.getValue()); + } +} diff --git a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java new file mode 100644 index 000000000..f5fa4d09f --- /dev/null +++ b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java @@ -0,0 +1,374 @@ +package com.genymobile.scrcpy; + +import android.view.KeyEvent; +import android.view.MotionEvent; + +import org.junit.Assert; +import org.junit.Test; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + + +public class ControlMessageReaderTest { + + @Test + public void testParseKeycodeEvent() throws IOException { + ControlMessageReader reader = new ControlMessageReader(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE); + dos.writeByte(KeyEvent.ACTION_UP); + dos.writeInt(KeyEvent.KEYCODE_ENTER); + dos.writeInt(KeyEvent.META_CTRL_ON); + byte[] packet = bos.toByteArray(); + + // The message type (1 byte) does not count + Assert.assertEquals(ControlMessageReader.INJECT_KEYCODE_PAYLOAD_LENGTH, packet.length - 1); + + reader.readFrom(new ByteArrayInputStream(packet)); + ControlMessage event = reader.next(); + + Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType()); + Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction()); + Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode()); + Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); + } + + @Test + public void testParseTextEvent() throws IOException { + ControlMessageReader reader = new ControlMessageReader(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(ControlMessage.TYPE_INJECT_TEXT); + byte[] text = "testé".getBytes(StandardCharsets.UTF_8); + dos.writeShort(text.length); + dos.write(text); + byte[] packet = bos.toByteArray(); + + reader.readFrom(new ByteArrayInputStream(packet)); + ControlMessage event = reader.next(); + + Assert.assertEquals(ControlMessage.TYPE_INJECT_TEXT, event.getType()); + Assert.assertEquals("testé", event.getText()); + } + + @Test + public void testParseLongTextEvent() throws IOException { + ControlMessageReader reader = new ControlMessageReader(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(ControlMessage.TYPE_INJECT_TEXT); + byte[] text = new byte[ControlMessageReader.INJECT_TEXT_MAX_LENGTH]; + Arrays.fill(text, (byte) 'a'); + dos.writeShort(text.length); + dos.write(text); + byte[] packet = bos.toByteArray(); + + reader.readFrom(new ByteArrayInputStream(packet)); + ControlMessage event = reader.next(); + + Assert.assertEquals(ControlMessage.TYPE_INJECT_TEXT, event.getType()); + Assert.assertEquals(new String(text, StandardCharsets.US_ASCII), event.getText()); + } + + @Test + public void testParseTouchEvent() throws IOException { + ControlMessageReader reader = new ControlMessageReader(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(ControlMessage.TYPE_INJECT_TOUCH_EVENT); + dos.writeByte(MotionEvent.ACTION_DOWN); + dos.writeLong(-42); // pointerId + dos.writeInt(100); + dos.writeInt(200); + dos.writeShort(1080); + dos.writeShort(1920); + dos.writeShort(0xffff); // pressure + dos.writeInt(MotionEvent.BUTTON_PRIMARY); + + byte[] packet = bos.toByteArray(); + + // The message type (1 byte) does not count + Assert.assertEquals(ControlMessageReader.INJECT_TOUCH_EVENT_PAYLOAD_LENGTH, packet.length - 1); + + reader.readFrom(new ByteArrayInputStream(packet)); + ControlMessage event = reader.next(); + + Assert.assertEquals(ControlMessage.TYPE_INJECT_TOUCH_EVENT, event.getType()); + Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction()); + Assert.assertEquals(-42, event.getPointerId()); + Assert.assertEquals(100, event.getPosition().getPoint().getX()); + Assert.assertEquals(200, event.getPosition().getPoint().getY()); + Assert.assertEquals(1080, event.getPosition().getScreenSize().getWidth()); + Assert.assertEquals(1920, event.getPosition().getScreenSize().getHeight()); + Assert.assertEquals(1f, event.getPressure(), 0f); // must be exact + Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getButtons()); + } + + @Test + public void testParseScrollEvent() throws IOException { + ControlMessageReader reader = new ControlMessageReader(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(ControlMessage.TYPE_INJECT_SCROLL_EVENT); + dos.writeInt(260); + dos.writeInt(1026); + dos.writeShort(1080); + dos.writeShort(1920); + dos.writeInt(1); + dos.writeInt(-1); + + byte[] packet = bos.toByteArray(); + + // The message type (1 byte) does not count + Assert.assertEquals(ControlMessageReader.INJECT_SCROLL_EVENT_PAYLOAD_LENGTH, packet.length - 1); + + reader.readFrom(new ByteArrayInputStream(packet)); + ControlMessage event = reader.next(); + + Assert.assertEquals(ControlMessage.TYPE_INJECT_SCROLL_EVENT, event.getType()); + Assert.assertEquals(260, event.getPosition().getPoint().getX()); + Assert.assertEquals(1026, event.getPosition().getPoint().getY()); + Assert.assertEquals(1080, event.getPosition().getScreenSize().getWidth()); + Assert.assertEquals(1920, event.getPosition().getScreenSize().getHeight()); + Assert.assertEquals(1, event.getHScroll()); + Assert.assertEquals(-1, event.getVScroll()); + } + + @Test + public void testParseBackOrScreenOnEvent() throws IOException { + ControlMessageReader reader = new ControlMessageReader(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(ControlMessage.TYPE_BACK_OR_SCREEN_ON); + + byte[] packet = bos.toByteArray(); + + reader.readFrom(new ByteArrayInputStream(packet)); + ControlMessage event = reader.next(); + + Assert.assertEquals(ControlMessage.TYPE_BACK_OR_SCREEN_ON, event.getType()); + } + + @Test + public void testParseExpandNotificationPanelEvent() throws IOException { + ControlMessageReader reader = new ControlMessageReader(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL); + + byte[] packet = bos.toByteArray(); + + reader.readFrom(new ByteArrayInputStream(packet)); + ControlMessage event = reader.next(); + + Assert.assertEquals(ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL, event.getType()); + } + + @Test + public void testParseCollapseNotificationPanelEvent() throws IOException { + ControlMessageReader reader = new ControlMessageReader(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL); + + byte[] packet = bos.toByteArray(); + + reader.readFrom(new ByteArrayInputStream(packet)); + ControlMessage event = reader.next(); + + Assert.assertEquals(ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL, event.getType()); + } + + @Test + public void testParseGetClipboardEvent() throws IOException { + ControlMessageReader reader = new ControlMessageReader(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(ControlMessage.TYPE_GET_CLIPBOARD); + + byte[] packet = bos.toByteArray(); + + reader.readFrom(new ByteArrayInputStream(packet)); + ControlMessage event = reader.next(); + + Assert.assertEquals(ControlMessage.TYPE_GET_CLIPBOARD, event.getType()); + } + + @Test + public void testParseSetClipboardEvent() throws IOException { + ControlMessageReader reader = new ControlMessageReader(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(ControlMessage.TYPE_SET_CLIPBOARD); + dos.writeByte(1); // paste + byte[] text = "testé".getBytes(StandardCharsets.UTF_8); + dos.writeShort(text.length); + dos.write(text); + + byte[] packet = bos.toByteArray(); + + reader.readFrom(new ByteArrayInputStream(packet)); + ControlMessage event = reader.next(); + + Assert.assertEquals(ControlMessage.TYPE_SET_CLIPBOARD, event.getType()); + Assert.assertEquals("testé", event.getText()); + + boolean parse = (event.getFlags() & ControlMessage.FLAGS_PASTE) != 0; + Assert.assertTrue(parse); + } + + @Test + public void testParseBigSetClipboardEvent() throws IOException { + ControlMessageReader reader = new ControlMessageReader(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(ControlMessage.TYPE_SET_CLIPBOARD); + + byte[] rawText = new byte[ControlMessageReader.CLIPBOARD_TEXT_MAX_LENGTH]; + dos.writeByte(1); // paste + Arrays.fill(rawText, (byte) 'a'); + String text = new String(rawText, 0, rawText.length); + + dos.writeShort(rawText.length); + dos.write(rawText); + + byte[] packet = bos.toByteArray(); + + reader.readFrom(new ByteArrayInputStream(packet)); + ControlMessage event = reader.next(); + + Assert.assertEquals(ControlMessage.TYPE_SET_CLIPBOARD, event.getType()); + Assert.assertEquals(text, event.getText()); + + boolean parse = (event.getFlags() & ControlMessage.FLAGS_PASTE) != 0; + Assert.assertTrue(parse); + } + + @Test + public void testParseSetScreenPowerMode() throws IOException { + ControlMessageReader reader = new ControlMessageReader(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(ControlMessage.TYPE_SET_SCREEN_POWER_MODE); + dos.writeByte(Device.POWER_MODE_NORMAL); + + byte[] packet = bos.toByteArray(); + + // The message type (1 byte) does not count + Assert.assertEquals(ControlMessageReader.SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH, packet.length - 1); + + reader.readFrom(new ByteArrayInputStream(packet)); + ControlMessage event = reader.next(); + + Assert.assertEquals(ControlMessage.TYPE_SET_SCREEN_POWER_MODE, event.getType()); + Assert.assertEquals(Device.POWER_MODE_NORMAL, event.getAction()); + } + + @Test + public void testParseRotateDevice() throws IOException { + ControlMessageReader reader = new ControlMessageReader(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(ControlMessage.TYPE_ROTATE_DEVICE); + + byte[] packet = bos.toByteArray(); + + reader.readFrom(new ByteArrayInputStream(packet)); + ControlMessage event = reader.next(); + + Assert.assertEquals(ControlMessage.TYPE_ROTATE_DEVICE, event.getType()); + } + + @Test + public void testMultiEvents() throws IOException { + ControlMessageReader reader = new ControlMessageReader(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + + dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE); + dos.writeByte(KeyEvent.ACTION_UP); + dos.writeInt(KeyEvent.KEYCODE_ENTER); + dos.writeInt(KeyEvent.META_CTRL_ON); + + dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE); + dos.writeByte(MotionEvent.ACTION_DOWN); + dos.writeInt(MotionEvent.BUTTON_PRIMARY); + dos.writeInt(KeyEvent.META_CTRL_ON); + + byte[] packet = bos.toByteArray(); + reader.readFrom(new ByteArrayInputStream(packet)); + + ControlMessage event = reader.next(); + Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType()); + Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction()); + Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode()); + Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); + + event = reader.next(); + Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType()); + Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction()); + Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getKeycode()); + Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); + } + + @Test + public void testPartialEvents() throws IOException { + ControlMessageReader reader = new ControlMessageReader(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + + dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE); + dos.writeByte(KeyEvent.ACTION_UP); + dos.writeInt(KeyEvent.KEYCODE_ENTER); + dos.writeInt(KeyEvent.META_CTRL_ON); + + dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE); + dos.writeByte(MotionEvent.ACTION_DOWN); + + byte[] packet = bos.toByteArray(); + reader.readFrom(new ByteArrayInputStream(packet)); + + ControlMessage event = reader.next(); + Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType()); + Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction()); + Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode()); + Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); + + event = reader.next(); + Assert.assertNull(event); // the event is not complete + + bos.reset(); + dos.writeInt(MotionEvent.BUTTON_PRIMARY); + dos.writeInt(KeyEvent.META_CTRL_ON); + packet = bos.toByteArray(); + reader.readFrom(new ByteArrayInputStream(packet)); + + // the event is now complete + event = reader.next(); + Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType()); + Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction()); + Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getKeycode()); + Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); + } +} diff --git a/server/src/test/java/com/genymobile/scrcpy/DeviceMessageWriterTest.java b/server/src/test/java/com/genymobile/scrcpy/DeviceMessageWriterTest.java new file mode 100644 index 000000000..df12f647b --- /dev/null +++ b/server/src/test/java/com/genymobile/scrcpy/DeviceMessageWriterTest.java @@ -0,0 +1,35 @@ +package com.genymobile.scrcpy; + +import org.junit.Assert; +import org.junit.Test; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +public class DeviceMessageWriterTest { + + @Test + public void testSerializeClipboard() throws IOException { + DeviceMessageWriter writer = new DeviceMessageWriter(); + + String text = "aéûoç"; + byte[] data = text.getBytes(StandardCharsets.UTF_8); + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(DeviceMessage.TYPE_CLIPBOARD); + dos.writeShort(data.length); + dos.write(data); + + byte[] expected = bos.toByteArray(); + + DeviceMessage msg = DeviceMessage.createClipboard(text); + bos = new ByteArrayOutputStream(); + writer.writeTo(msg, bos); + + byte[] actual = bos.toByteArray(); + + Assert.assertArrayEquals(expected, actual); + } +} diff --git a/server/src/test/java/com/genymobile/scrcpy/StringUtilsTest.java b/server/src/test/java/com/genymobile/scrcpy/StringUtilsTest.java new file mode 100644 index 000000000..89799c5ec --- /dev/null +++ b/server/src/test/java/com/genymobile/scrcpy/StringUtilsTest.java @@ -0,0 +1,42 @@ +package com.genymobile.scrcpy; + +import org.junit.Assert; +import org.junit.Test; + +import java.nio.charset.StandardCharsets; + +public class StringUtilsTest { + + @Test + public void testUtf8Truncate() { + String s = "aÉbÔc"; + byte[] utf8 = s.getBytes(StandardCharsets.UTF_8); + Assert.assertEquals(7, utf8.length); + + int count; + + count = StringUtils.getUtf8TruncationIndex(utf8, 1); + Assert.assertEquals(1, count); + + count = StringUtils.getUtf8TruncationIndex(utf8, 2); + Assert.assertEquals(1, count); // É is 2 bytes-wide + + count = StringUtils.getUtf8TruncationIndex(utf8, 3); + Assert.assertEquals(3, count); + + count = StringUtils.getUtf8TruncationIndex(utf8, 4); + Assert.assertEquals(4, count); + + count = StringUtils.getUtf8TruncationIndex(utf8, 5); + Assert.assertEquals(4, count); // Ô is 2 bytes-wide + + count = StringUtils.getUtf8TruncationIndex(utf8, 6); + Assert.assertEquals(6, count); + + count = StringUtils.getUtf8TruncationIndex(utf8, 7); + Assert.assertEquals(7, count); + + count = StringUtils.getUtf8TruncationIndex(utf8, 8); + Assert.assertEquals(7, count); // no more chars + } +}