Repository navigation
Expand file tree
/
Copy pathTextEncodingServiceTest.java
More file actions
131 lines (109 loc) · 5.33 KB
/
Copy pathTextEncodingServiceTest.java
File metadata and controls
131 lines (109 loc) · 5.33 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
package se.deversity.asynctest.example;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import se.deversity.asynctest.diagnostics.SharedCharsetCoderDetector;
import se.deversity.asynctest.example.service.TextEncodingService;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.StandardCharsets;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* Test for TextEncodingService.
*
* ========================================================================
* DETECTOR: SharedCharsetCoderDetector (DetectorType.SHARED_CHARSET_CODER)
* ========================================================================
*
* Charset is immutable and thread-safe. CharsetEncoder and CharsetDecoder
* are state machines: they track surrogate-pair carry-over, end-of-input,
* and the flush stage. reset() exists because that state survives calls.
*
* THE BUG:
* - one CharsetEncoder/CharsetDecoder cached in a field, used by every
* thread; the documented reset -> encode -> flush protocol is per coder,
* not per thread
*
* THE FIX:
* - Charset.newEncoder() per call (it is a small allocation), a
* ThreadLocal<CharsetEncoder> if profiling really says otherwise, or
* String.getBytes(charset) / new String(bytes, charset) when the coder's
* error actions are not needed — those allocate their own coder.
*/
class TextEncodingServiceTest {
private SharedCharsetCoderDetector detector;
@BeforeEach
void setUp() {
detector = new SharedCharsetCoderDetector();
}
// -----------------------------------------------------------------------
// Part 1: a coder per call, single thread. Nothing to report.
// -----------------------------------------------------------------------
@Test
void encoderPerCall_isClean() throws Exception {
var service = new TextEncodingService();
for (int i = 0; i < 3; i++) {
CharsetEncoder perCall = StandardCharsets.UTF_8.newEncoder();
detector.recordAccess(perCall, "encode", Thread.currentThread());
assertArrayEquals("hello".getBytes(StandardCharsets.UTF_8),
service.encodeSafely("hello"));
}
var report = detector.analyze();
assertFalse(report.hasIssues(), () -> "Expected clean usage:\n" + report);
}
// -----------------------------------------------------------------------
// Part 2: one encoder, two threads — flagged.
// -----------------------------------------------------------------------
@Test
void sharedEncoderAcrossThreads_isDetected() throws Exception {
var service = new TextEncodingService();
CharsetEncoder shared = StandardCharsets.UTF_8.newEncoder(); // models the field
Runnable worker = () -> {
detector.recordAccess(shared, "reset", Thread.currentThread());
detector.recordAccess(shared, "encode", Thread.currentThread());
};
Thread a = new Thread(worker, "encoder-a");
Thread b = new Thread(worker, "encoder-b");
a.start();
b.start();
a.join();
b.join();
assertArrayEquals("payload".getBytes(StandardCharsets.UTF_8), service.encode("payload"));
var report = detector.analyze();
assertTrue(report.hasIssues(), () -> "Expected shared-encoder violation:\n" + report);
String violation = report.violations.get(0);
assertTrue(violation.contains("CharsetEncoder"), violation);
assertTrue(violation.contains("2 threads"), violation);
assertTrue(violation.contains("encoder-a"), violation);
}
// -----------------------------------------------------------------------
// Part 3: decoders are tracked too, and separately from encoders.
// -----------------------------------------------------------------------
@Test
void sharedDecoderAcrossThreads_isDetectedSeparately() throws Exception {
CharsetDecoder shared = StandardCharsets.UTF_8.newDecoder();
Runnable worker = () -> detector.recordAccess(shared, "decode", Thread.currentThread());
Thread a = new Thread(worker, "decoder-a");
Thread b = new Thread(worker, "decoder-b");
a.start();
b.start();
a.join();
b.join();
var report = detector.analyze();
assertTrue(report.hasIssues(), () -> "Expected shared-decoder violation:\n" + report);
assertEquals(1, report.violations.size(), "one instance, one violation");
assertTrue(report.violations.get(0).contains("CharsetDecoder"), report.violations.get(0));
}
// -----------------------------------------------------------------------
// Part 4: the service still round-trips correctly single-threaded — which
// is exactly why this bug survives code review and unit tests.
// -----------------------------------------------------------------------
@Test
void sharedCoders_lookCorrectSingleThreaded() throws Exception {
var service = new TextEncodingService();
byte[] encoded = service.encode("round trip åäö");
assertEquals("round trip åäö", service.decode(encoded));
}
}