isocov — request #97 GitHub

showing: 97 of 100  |  classes: 3
Request history (100 requests) [all]
1. 1edf4a73 (16 hits, 3 classes)
2. 433d3a1f (20 hits, 3 classes)
3. fd97f85e (20 hits, 3 classes)
4. 8df5c785 (26 hits, 3 classes)
5. 61cad80e (20 hits, 3 classes)
6. a743fc88 (26 hits, 3 classes)
7. 5b3b45f3 (20 hits, 3 classes)
8. 2ff09cd1 (20 hits, 3 classes)
9. 41ac690c (20 hits, 3 classes)
10. e4722cc7 (20 hits, 3 classes)
11. 840fd4ce (26 hits, 3 classes)
12. 1b89f318 (20 hits, 3 classes)
13. 6a893088 (20 hits, 3 classes)
14. 2d05e140 (27 hits, 3 classes)
15. 8b066dfc (27 hits, 3 classes)
16. 80acc3f5 (26 hits, 3 classes)
17. 597cd655 (24 hits, 3 classes)
18. 373b52b5 (20 hits, 3 classes)
19. 1dd7ecfd (20 hits, 3 classes)
20. cd2869c9 (17 hits, 3 classes)
21. 9eedb04d (27 hits, 3 classes)
22. 30e52da0 (26 hits, 3 classes)
23. bd1c8225 (22 hits, 3 classes)
24. 07191567 (27 hits, 3 classes)
25. e54af174 (26 hits, 3 classes)
26. 49dd67fd (24 hits, 3 classes)
27. 4356bcb5 (26 hits, 3 classes)
28. e398d0fc (27 hits, 3 classes)
29. 01a2a69f (20 hits, 3 classes)
30. 659dee2e (20 hits, 3 classes)
31. 5885dd19 (27 hits, 3 classes)
32. edd542db (20 hits, 3 classes)
33. 886d55b6 (27 hits, 3 classes)
34. d139bf10 (20 hits, 3 classes)
35. 1270d940 (15 hits, 3 classes)
36. afeeae63 (20 hits, 3 classes)
37. 057f1a03 (26 hits, 3 classes)
38. 1e8c0f52 (26 hits, 3 classes)
39. aad0b191 (27 hits, 3 classes)
40. 152966c8 (20 hits, 3 classes)
41. 258d8ef8 (27 hits, 3 classes)
42. f92f6811 (26 hits, 3 classes)
43. 2ef2598b (26 hits, 3 classes)
44. 1ce28bec (27 hits, 3 classes)
45. 4a38ff34 (20 hits, 3 classes)
46. 244c6c84 (26 hits, 3 classes)
47. 2849cac0 (20 hits, 3 classes)
48. cab7aa1f (15 hits, 3 classes)
49. be84d8e8 (20 hits, 3 classes)
50. d4dfaba5 (20 hits, 3 classes)
51. 6dcb89e9 (24 hits, 3 classes)
52. d8d38ca6 (27 hits, 3 classes)
53. f9a877af (26 hits, 3 classes)
54. 85d1b621 (20 hits, 3 classes)
55. 9cc3953c (26 hits, 3 classes)
56. 3dd8255f (20 hits, 3 classes)
57. 3d5e31a8 (26 hits, 3 classes)
58. daad7c68 (27 hits, 3 classes)
59. f59f446e (20 hits, 3 classes)
60. 76266acc (20 hits, 3 classes)
61. 020458d2 (27 hits, 3 classes)
62. 7d50edc4 (16 hits, 3 classes)
63. 7dd73782 (27 hits, 3 classes)
64. eb0fed4f (26 hits, 3 classes)
65. dc13bf6c (20 hits, 3 classes)
66. fe6c757c (20 hits, 3 classes)
67. 7092f66b (20 hits, 3 classes)
68. 2fdeb3bb (20 hits, 3 classes)
69. 47d9a7c3 (20 hits, 3 classes)
70. a7ff00d7 (27 hits, 3 classes)
71. 81639331 (20 hits, 3 classes)
72. 85a15de8 (26 hits, 3 classes)
73. 0331067c (20 hits, 3 classes)
74. 2a4fc4c2 (27 hits, 3 classes)
75. a051e8d5 (27 hits, 3 classes)
76. ab7f2250 (16 hits, 3 classes)
77. 306476f0 (15 hits, 3 classes)
78. f2508e86 (16 hits, 3 classes)
79. f42d249d (20 hits, 3 classes)
80. dd024278 (20 hits, 3 classes)
81. dfe15c7a (26 hits, 3 classes)
82. 18ad1141 (20 hits, 3 classes)
83. b3f26193 (15 hits, 3 classes)
84. 0f5daa3e (27 hits, 3 classes)
85. df316c1c (27 hits, 3 classes)
86. 2c5127e8 (20 hits, 3 classes)
87. e96a49b9 (27 hits, 3 classes)
88. 7e3a9551 (20 hits, 3 classes)
89. f7a4d437 (27 hits, 3 classes)
90. 2f049562 (26 hits, 3 classes)
91. 05897674 (17 hits, 3 classes)
92. 398e6405 (20 hits, 3 classes)
93. 8a656cf6 (20 hits, 3 classes)
94. 0dc3a263 (26 hits, 3 classes)
95. bd57e60c (17 hits, 3 classes)
96. 49ab47a0 (20 hits, 3 classes)
97. 169463ad (13 hits, 3 classes)
98. e61d63ac (19 hits, 3 classes)
99. 17936a58 (20 hits, 3 classes)
100. d10bb2c0 (10 hits, 3 classes)

DemoServer$Response.java

✓ 1 lines✗ 0 lines100% covered

GradeService.java

✓ 7 lines✗ 11 lines38% covered
1package org.isocov.demo.service;
2
3/**
4 * Assigns letter grades based on numeric score.
5 * Rich branching — good for coverage demos.
6 */
7public final class GradeService {
8
9 private GradeService() {}
10
11 public static String grade(final int score) {
12 if (score < 0 || score > 100) {
13 return "INVALID";
14 }
15 if (score >= 90) {
16 return "A";
17 }
18 if (score >= 80) {
19 return "B";
20 }
21 if (score >= 70) {
22 return "C";
23 }
24 if (score >= 60) {
25 return "D";
26 }
27 return "F";
28 }
29
30 public static String comment(final int score) {
31 return switch (grade(score)) {
32 case "A" -> "Excellent!";
33 case "B" -> "Good job.";
34 case "C" -> "Average.";
35 case "D" -> "Needs improvement.";
36 case "F" -> "Failed.";
37 default -> "Invalid score.";
38 };
39 }
40}

DemoServer.java

✓ 8 lines✗ 111 lines6% covered
1package org.isocov.demo;
2
3import com.sun.net.httpserver.HttpExchange;
4import com.sun.net.httpserver.HttpServer;
5import org.isocov.core.model.CompactSnapshot;
6import org.isocov.core.model.CoverageCodec;
7import org.isocov.core.runtime.CoverageRuntime;
8import org.isocov.demo.service.FizzBuzzService;
9import org.isocov.demo.service.GradeService;
10import org.isocov.demo.service.PrimeService;
11
12import java.io.IOException;
13import java.io.OutputStream;
14import java.net.InetSocketAddress;
15import java.nio.charset.StandardCharsets;
16import java.util.Arrays;
17import java.util.Collections;
18import java.util.List;
19import java.util.UUID;
20import java.util.concurrent.CopyOnWriteArrayList;
21import java.util.concurrent.Executors;
22import java.util.concurrent.ScheduledExecutorService;
23import java.util.concurrent.TimeUnit;
24
25/**
26 * Demo HTTP server showing per-request coverage with isocov.
27 *
28 * <p>Coverage flow (fully non-blocking on request thread):
29 * <ol>
30 * <li>Request thread: {@code startRequest → handle → endRequest} (auto-enqueues internally)</li>
31 * <li>Dump thread(s): dequeue → pack to {@link CompactSnapshot} → release boolean[] to pool</li>
32 * <li>{@code /coverage}: read from accumulated {@link CompactSnapshot} list</li>
33 * </ol>
34 */
35public final class DemoServer {
36
37 /** Max snapshots to retain in memory (ring buffer). */
38 private static final int MAX_SNAPSHOTS = 100;
39
40 /** Accumulated compact snapshots — written by dump threads, read by /coverage. */
41 private static final List<CompactSnapshot> STORE = new CopyOnWriteArrayList<>();
42
43 public static void main(final String[] args) throws Exception {
44 final int port = args.length > 0 ? Integer.parseInt(args[0]) : 8080;
45
46 // Periodically drain binary data and decode into compact snapshots
47 // (dump threads start automatically on first endRequest())
48 final ScheduledExecutorService drainer = Executors.newSingleThreadScheduledExecutor(r -> {
49 final Thread t = new Thread(r, "isocov-drainer");
50 t.setDaemon(true);
51 return t;
52 });
53 drainer.scheduleAtFixedRate(() -> {
54 final byte[] data = CoverageRuntime.getRequestData();
55 if (data.length > 0) {
56 STORE.addAll(CoverageCodec.decode(data));
57 while (STORE.size() > MAX_SNAPSHOTS) {
58 STORE.remove(0);
59 }
60 }
61 }, 500, 500, TimeUnit.MILLISECONDS);
62
63 final HttpServer server = HttpServer.create(new InetSocketAddress(port), 0);
64 server.setExecutor(Executors.newFixedThreadPool(4));
65
66 server.createContext("/api/grade", DemoServer::handleGrade);
67 server.createContext("/api/fizzbuzz", DemoServer::handleFizzBuzz);
68 server.createContext("/api/primes", DemoServer::handlePrimes);
69 server.createContext("/api/isprime", DemoServer::handleIsPrime);
70 server.createContext("/coverage", DemoServer::handleCoverage);
71 server.createContext("/", DemoServer::handleIndex);
72
73 server.start();
74 System.out.println("[isocov-demo] Listening on http://localhost:" + port);
75 System.out.println("[isocov-demo] Try:");
76 System.out.println(" curl 'localhost:" + port + "/api/grade?score=85'");
77 System.out.println(" curl 'localhost:" + port + "/api/fizzbuzz?n=20'");
78 System.out.println(" curl 'localhost:" + port + "/api/primes?limit=50'");
79 System.out.println(" open http://localhost:" + port + "/coverage");
80 }
81
82 // ─── Handlers ─────────────────────────────────────────────────────────────
83
84 private static void handleGrade(final HttpExchange ex) throws IOException {
85 withCoverage(ex, () -> {
86 final int score = intParam(ex, "score", 75);
87 final String g = GradeService.grade(score);
88 final String c = GradeService.comment(score);
89 return json("{\"score\":" + score + ",\"grade\":\"" + g + "\",\"comment\":\"" + c + "\"}");
90 });
91 }
92
93 private static void handleFizzBuzz(final HttpExchange ex) throws IOException {
94 withCoverage(ex, () -> {
95 final int n = intParam(ex, "n", 15);
96 final var list = FizzBuzzService.compute(n);
97 return json("[" + String.join(",", list.stream().map(s -> "\"" + s + "\"").toList()) + "]");
98 });
99 }
100
101 private static void handlePrimes(final HttpExchange ex) throws IOException {
102 withCoverage(ex, () -> {
103 final int limit = intParam(ex, "limit", 50);
104 final var primes = PrimeService.sieve(limit);
105 return json("{\"limit\":" + limit + ",\"count\":" + primes.size()
106 + ",\"primes\":" + primes + "}");
107 });
108 }
109
110 private static void handleIsPrime(final HttpExchange ex) throws IOException {
111 withCoverage(ex, () -> {
112 final int n = intParam(ex, "n", 17);
113 return json("{\"n\":" + n + ",\"prime\":" + PrimeService.isPrime(n) + "}");
114 });
115 }
116
117 private static void handleCoverage(final HttpExchange ex) throws IOException {
118 final List<CompactSnapshot> all = Collections.unmodifiableList(STORE);
119 if (all.isEmpty()) {
120 send(ex, 200, "text/plain", "No coverage yet. Hit an /api/* endpoint first.");
121 return;
122 }
123 try {
124 final String reqParam = stringParam(ex, "req", "all");
125 final int selected;
126 if ("all".equals(reqParam)) {
127 selected = -1;
128 } else {
129 try {
130 selected = Integer.parseInt(reqParam);
131 } catch (final NumberFormatException e) {
132 send(ex, 400, "text/plain", "Invalid req param: " + reqParam);
133 return;
134 }
135 if (selected < 1 || selected > all.size()) {
136 send(ex, 400, "text/plain",
137 "req must be between 1 and " + all.size());
138 return;
139 }
140 }
141
142 // Include background coverage in flat view
143 CompactSnapshot background = null;
144 if (selected == -1) {
145 final byte[] bgData = CoverageRuntime.getBackgroundData(false);
146 if (bgData.length > 0) {
147 final List<CompactSnapshot> bgSnaps = CoverageCodec.decode(bgData);
148 if (!bgSnaps.isEmpty()) {
149 background = bgSnaps.get(0);
150 }
151 }
152 }
153
154 final String view = stringParam(ex, "view", "coverage");
155 final String html = CoverageReporter.toHtml(
156 all, selected, background, "heatmap".equals(view));
157 send(ex, 200, "text/html; charset=UTF-8", html);
158 } catch (final Exception e) {
159 send(ex, 500, "text/plain", "Error generating report: " + e.getMessage());
160 }
161 }
162
163 private static void handleIndex(final HttpExchange ex) throws IOException {
164 send(ex, 200, "text/html", """
165 <html><body style="font-family:monospace;padding:20px">
166 <h2>isocov demo</h2>
167 <ul>
168 <li><a href="/api/grade?score=85">/api/grade?score=85</a></li>
169 <li><a href="/api/fizzbuzz?n=20">/api/fizzbuzz?n=20</a></li>
170 <li><a href="/api/primes?limit=50">/api/primes?limit=50</a></li>
171 <li><a href="/api/isprime?n=17">/api/isprime?n=17</a></li>
172 <li><a href="/coverage">/coverage</a> ← hit an endpoint first</li>
173 </ul>
174 </body></html>
175 """);
176 }
177
178 // ─── Coverage wrapper ──────────────────────────────────────────────────────
179
180 @FunctionalInterface
181 interface Handler { Response handle() throws Exception; }
182 record Response(String contentType, String body) {}
183
184 private static Response json(final String body) {
185 return new Response("application/json", body);
186 }
187
188 /**
189 * Non-blocking coverage collection:
190 * startRequest → handle → endRequest (auto-enqueues to dump thread internally)
191 * Request thread never blocks on coverage processing.
192 */
193 private static void withCoverage(final HttpExchange ex, final Handler handler)
194 throws IOException {
195 final String reqId = UUID.randomUUID().toString().substring(0, 8);
196 CoverageRuntime.startRequest(reqId);
197 String body;
198 String contentType;
199 try {
200 final Response resp = handler.handle();
201 body = resp.body();
202 contentType = resp.contentType();
203 } catch (final Exception e) {
204 body = "{\"error\":\"" + e.getMessage() + "\"}";
205 contentType = "application/json";
206 } finally {
207 CoverageRuntime.endRequest(); // non-blocking: enqueues to dump thread internally
208 }
209 send(ex, 200, contentType, body);
210 }
211
212 // ─── Helpers ──────────────────────────────────────────────────────────────
213
214 private static String stringParam(final HttpExchange ex, final String key, final String defaultVal) {
215 final String query = ex.getRequestURI().getQuery();
216 if (query == null) {
217 return defaultVal;
218 }
219 return Arrays.stream(query.split("&"))
220 .filter(p -> p.startsWith(key + "="))
221 .map(p -> p.substring(key.length() + 1))
222 .findFirst().orElse(defaultVal);
223 }
224
225 private static int intParam(final HttpExchange ex, final String key, final int defaultVal) {
226 final String query = ex.getRequestURI().getQuery();
227 if (query == null) {
228 return defaultVal;
229 }
230 return Arrays.stream(query.split("&"))
231 .filter(p -> p.startsWith(key + "="))
232 .map(p -> p.substring(key.length() + 1))
233 .mapToInt(v -> {
234 try {
235 return Integer.parseInt(v);
236 } catch (Exception e) {
237 return defaultVal;
238 }
239 })
240 .findFirst().orElse(defaultVal);
241 }
242
243 private static void send(final HttpExchange ex, final int status,
244 final String contentType, final String body) throws IOException {
245 final byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
246 ex.getResponseHeaders().set("Content-Type", contentType);
247 ex.sendResponseHeaders(status, bytes.length);
248 try (OutputStream os = ex.getResponseBody()) {
249 os.write(bytes);
250 }
251 }
252}