Skip to content

Commit 64f8ed1

Browse files
committed
JShellWrapper updated StringOutputStream to be more correct and added tests for it
1 parent c24e28b commit 64f8ed1

4 files changed

Lines changed: 153 additions & 16 deletions

File tree

JShellWrapper/build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ sourceCompatibility = 19
1313
run { // Debugging only
1414
standardInput = System.in
1515
systemProperty 'line.separator', '\n'
16+
systemProperty 'file.encoding', 'UTF-8'
1617
}
1718

1819
var outputImage = 'togetherjava.org:5001/togetherjava/jshellwrapper:master' ?: 'latest'
@@ -53,4 +54,5 @@ tasks.named('test') {
5354
}
5455
test {
5556
systemProperty 'line.separator', '\n'
57+
systemProperty 'file.encoding', 'UTF-8'
5658
}

JShellWrapper/src/main/java/JShellWrapper.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,8 +166,9 @@ private List<String> writeEvalResult(EvalResult result, StringOutputStream jshel
166166
outBuffer.add("");
167167
}
168168

169-
outBuffer.add(String.valueOf(jshellOut.isOverflow()));
170-
outBuffer.add(sanitize(jshellOut.readAll()));
169+
StringOutputStream.Result out = jshellOut.readAll();
170+
outBuffer.add(String.valueOf(out.isOverflow()));
171+
outBuffer.add(sanitize(out.content()));
171172
return outBuffer;
172173
}
173174

Lines changed: 52 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,72 @@
1-
import java.io.IOException;
21
import java.io.OutputStream;
2+
import java.nio.charset.StandardCharsets;
33
import java.util.Arrays;
4+
import java.util.Objects;
45

56
public class StringOutputStream extends OutputStream {
6-
private final byte[] bytes;
7+
private static final char UNKNOWN_CHAR = '\uFFFD';
8+
private final int maxSize;
9+
private byte[] bytes;
710
private int index;
8-
private boolean overflow;
11+
private boolean byteOverflow;
912

10-
public StringOutputStream(int sizeLimit) {
11-
this.bytes = new byte[sizeLimit];
13+
/**
14+
* Constructs a new StringOutputStream.
15+
* @param maxSize the limit in terms of java char, so two bytes per unit
16+
*/
17+
public StringOutputStream(int maxSize) {
18+
this.bytes = new byte[maxSize*2];
19+
this.maxSize = maxSize;
1220
this.index = 0;
13-
this.overflow = false;
21+
this.byteOverflow = false;
1422
}
1523

1624
@Override
17-
public void write(int b) throws IOException {
18-
if(index >= bytes.length) {
19-
overflow = true;
25+
public void write(int b) {
26+
if(index == bytes.length) {
27+
byteOverflow = true;
2028
return;
2129
}
2230
bytes[index++] = (byte)b;
2331
}
2432

25-
public String readAll() {
26-
String s = new String(isOverflow() ? bytes : Arrays.copyOf(bytes, index));
33+
@Override
34+
public void write(byte[] b) {
35+
write(b, 0, b.length);
36+
}
37+
38+
@Override
39+
public void write(byte[] b, int off, int len) {
40+
Objects.checkFromIndexSize(off, len, b.length);
41+
if(index == bytes.length) {
42+
byteOverflow = true;
43+
return;
44+
}
45+
int actualLen = Math.min(bytes.length - index, len);
46+
System.arraycopy(b, off, bytes, index, actualLen);
47+
index += actualLen;
48+
if(len != actualLen) {
49+
byteOverflow = true;
50+
}
51+
}
52+
53+
public Result readAll() {
54+
if(index > bytes.length) throw new IllegalStateException(); // Should never happen
55+
String s = new String(index == bytes.length ? bytes : Arrays.copyOf(bytes, index), StandardCharsets.UTF_8);
2756
index = 0;
28-
return s;
57+
if(byteOverflow) {
58+
byteOverflow = false;
59+
return new Result(s.charAt(s.length()-1) == UNKNOWN_CHAR ? s.substring(0, s.length()-1) : s, true);
60+
}
61+
if(s.length() > maxSize) return new Result(s.substring(0, maxSize), true);
62+
return new Result(s, false);
63+
}
64+
65+
@Override
66+
public void close() {
67+
bytes = null;
2968
}
3069

31-
public boolean isOverflow() {
32-
return overflow;
70+
public record Result(String content, boolean isOverflow) {
3371
}
3472
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import org.junit.jupiter.api.AfterEach;
2+
import org.junit.jupiter.api.BeforeAll;
3+
import org.junit.jupiter.api.BeforeEach;
4+
import org.junit.jupiter.api.Test;
5+
6+
import java.nio.charset.StandardCharsets;
7+
8+
import static org.junit.jupiter.api.Assertions.*;
9+
10+
class StringOutputStreamTest {
11+
static final String E_ACUTE = "\u00E9";
12+
static final String SMILEY = "\uD83D\uDE0A";
13+
14+
StringOutputStream stream;
15+
16+
@BeforeEach
17+
void setUp() {
18+
stream = new StringOutputStream(10);
19+
}
20+
21+
@AfterEach
22+
void tearDown() {
23+
stream.close();
24+
}
25+
26+
@Test
27+
void testNoOverflow() {
28+
final String hello = "HelloWorld"; // length = 10
29+
for(byte b : hello.getBytes(StandardCharsets.UTF_8)) {
30+
stream.write(b);
31+
}
32+
assertResult(false, hello, stream.readAll());
33+
34+
final String eAcuteX10 = E_ACUTE.repeat(10);
35+
for(byte b : eAcuteX10.getBytes(StandardCharsets.UTF_8)) {
36+
stream.write(b);
37+
}
38+
assertResult(false, eAcuteX10, stream.readAll());
39+
40+
final String smileyX5 = SMILEY.repeat(5);
41+
for(byte b : smileyX5.getBytes(StandardCharsets.UTF_8)) {
42+
stream.write(b);
43+
}
44+
assertResult(false, smileyX5, stream.readAll());
45+
}
46+
@Test
47+
void testOverflow() {
48+
final String hello = "Hello World"; // length = 11
49+
for(byte b : hello.getBytes(StandardCharsets.UTF_8)) {
50+
stream.write(b);
51+
}
52+
assertResult(true, "Hello Worl", stream.readAll());
53+
54+
final String eAcuteX11 = E_ACUTE.repeat(11);
55+
for(byte b : eAcuteX11.getBytes(StandardCharsets.UTF_8)) {
56+
stream.write(b);
57+
}
58+
assertResult(true, E_ACUTE.repeat(10), stream.readAll());
59+
}
60+
@Test
61+
void testOverflowWithHalfCharacter() {
62+
final String aAndSmileyX5 = 'a' + SMILEY.repeat(5);
63+
for(byte b : aAndSmileyX5.getBytes(StandardCharsets.UTF_8)) {
64+
stream.write(b);
65+
}
66+
assertResult(true, 'a' + SMILEY.repeat(4), stream.readAll());
67+
}
68+
69+
@Test
70+
void testWriteOverload() {
71+
final String hello = "HelloWorld"; // length = 10
72+
stream.write(hello.getBytes(StandardCharsets.UTF_8));
73+
assertResult(false, hello, stream.readAll());
74+
75+
final String eAcuteX15 = E_ACUTE.repeat(15);
76+
stream.write(eAcuteX15.getBytes(StandardCharsets.UTF_8), 0, 2*10);
77+
assertResult(false, E_ACUTE.repeat(10), stream.readAll());
78+
79+
final String eAcuteX11 = E_ACUTE.repeat(11);
80+
stream.write(eAcuteX11.getBytes(StandardCharsets.UTF_8), 0, 2*11);
81+
assertResult(true, E_ACUTE.repeat(10), stream.readAll());
82+
83+
final String eAcuteX5AndSmileyX5 = E_ACUTE.repeat(5) + SMILEY.repeat(5);
84+
stream.write(eAcuteX5AndSmileyX5.getBytes(StandardCharsets.UTF_8), 2*3, (2*2)+(4*4));
85+
assertResult(false, E_ACUTE.repeat(2) + SMILEY.repeat(4), stream.readAll());
86+
87+
final String eAcuteX5AndSmileyX5AndA = E_ACUTE.repeat(5) + SMILEY.repeat(5) + 'a';
88+
stream.write(eAcuteX5AndSmileyX5AndA.getBytes(StandardCharsets.UTF_8), 2*3, (2*2)+(4*4)+1);
89+
assertResult(true, E_ACUTE.repeat(2) + SMILEY.repeat(4), stream.readAll());
90+
}
91+
92+
void assertResult(boolean isOverflow, String expected, StringOutputStream.Result result) {
93+
assertEquals(isOverflow, result.isOverflow());
94+
assertEquals(expected, result.content());
95+
}
96+
}

0 commit comments

Comments
 (0)