Skip to content

Commit fa57ddb

Browse files
committed
Release 1.0.0: public API SemVer-stable; JaCoCo + SpotBugs + GR-8 benchmark
1 parent 28c5f44 commit fa57ddb

6 files changed

Lines changed: 312 additions & 2 deletions

File tree

.github/workflows/ci.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,16 @@ jobs:
3939
diff -ru "$RUNNER_TEMP/conformance/fixtures" "src/test/resources/conformance/fixtures"
4040
diff -ru "$RUNNER_TEMP/conformance/schema" "src/test/resources/conformance/schema"
4141
echo "Vendored conformance is in sync with the canonical suite."
42+
43+
ci-green:
44+
name: CI green
45+
runs-on: ubuntu-latest
46+
needs: [test, conformance]
47+
if: ${{ always() }}
48+
steps:
49+
- name: Fail if any required job did not pass
50+
run: |
51+
if ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}; then
52+
echo "A required job failed or was cancelled."
53+
exit 1
54+
fi

CHANGELOG.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,22 @@ The envelope wire format is versioned separately by `meta.schema_version`
99

1010
## [Unreleased]
1111

12+
## [1.0.0] - 2026-06-07
13+
14+
**1.0.0 — the public API is now SemVer-stable**: breaking changes require a MAJOR,
15+
following the deprecation policy. The wire envelope is unchanged
16+
(`schema_version: 1`). Full reference at [babelqueue.com](https://babelqueue.com).
17+
18+
### Internal
19+
- Build adds **JaCoCo** (line-coverage gate ≥90%, bound to `verify`) and
20+
**SpotBugs** (`effort=Max`, `threshold=Medium`); both run in CI via `mvn verify`.
21+
Added JSON codec edge-case + exception tests to clear the gate. A documented
22+
`spotbugs-exclude.xml` waives the EI_EXPOSE patterns on the read-only envelope
23+
records (no hot-path defensive copy — GR-8).
24+
- **GR-8 latency benchmark** (`OverheadBenchmarkTest`) — asserts the envelope
25+
encode/decode path adds **≤2%** over plain-JSON serialization vs a conservative
26+
750µs broker round-trip.
27+
1228
## [0.1.0] - 2026-06-06
1329

1430
### Added
@@ -34,5 +50,6 @@ The envelope wire format is versioned separately by `meta.schema_version`
3450
- Pre-1.0: the public API may change before the `1.0.0` tag.
3551
- **Zero runtime dependencies** (pure JDK); requires Java **17+**.
3652

37-
[Unreleased]: https://github.com/BabelQueue/babelqueue-java/compare/v0.1.0...HEAD
53+
[Unreleased]: https://github.com/BabelQueue/babelqueue-java/compare/v1.0.0...HEAD
54+
[1.0.0]: https://github.com/BabelQueue/babelqueue-java/compare/v0.1.0...v1.0.0
3855
[0.1.0]: https://github.com/BabelQueue/babelqueue-java/releases/tag/v0.1.0

pom.xml

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
<groupId>com.babelqueue</groupId>
88
<artifactId>babelqueue-core</artifactId>
9-
<version>0.1.0</version>
9+
<version>1.0.0</version>
1010
<packaging>jar</packaging>
1111

1212
<name>BabelQueue Core</name>
@@ -74,6 +74,70 @@
7474
<artifactId>maven-surefire-plugin</artifactId>
7575
<version>3.2.5</version>
7676
</plugin>
77+
78+
<!-- Coverage: instrument tests and gate the build at >=90% line coverage. -->
79+
<plugin>
80+
<groupId>org.jacoco</groupId>
81+
<artifactId>jacoco-maven-plugin</artifactId>
82+
<version>0.8.12</version>
83+
<executions>
84+
<execution>
85+
<id>prepare-agent</id>
86+
<goals>
87+
<goal>prepare-agent</goal>
88+
</goals>
89+
</execution>
90+
<execution>
91+
<id>report</id>
92+
<phase>test</phase>
93+
<goals>
94+
<goal>report</goal>
95+
</goals>
96+
</execution>
97+
<execution>
98+
<id>check</id>
99+
<phase>verify</phase>
100+
<goals>
101+
<goal>check</goal>
102+
</goals>
103+
<configuration>
104+
<rules>
105+
<rule>
106+
<element>BUNDLE</element>
107+
<limits>
108+
<limit>
109+
<counter>LINE</counter>
110+
<value>COVEREDRATIO</value>
111+
<minimum>0.90</minimum>
112+
</limit>
113+
</limits>
114+
</rule>
115+
</rules>
116+
</configuration>
117+
</execution>
118+
</executions>
119+
</plugin>
120+
121+
<!-- Static analysis: fail the build on SpotBugs findings. -->
122+
<plugin>
123+
<groupId>com.github.spotbugs</groupId>
124+
<artifactId>spotbugs-maven-plugin</artifactId>
125+
<version>4.9.8.3</version>
126+
<configuration>
127+
<effort>Max</effort>
128+
<threshold>Medium</threshold>
129+
<excludeFilterFile>spotbugs-exclude.xml</excludeFilterFile>
130+
</configuration>
131+
<executions>
132+
<execution>
133+
<id>spotbugs-check</id>
134+
<phase>verify</phase>
135+
<goals>
136+
<goal>check</goal>
137+
</goals>
138+
</execution>
139+
</executions>
140+
</plugin>
77141
</plugins>
78142
</build>
79143

spotbugs-exclude.xml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<FindBugsFilter>
3+
<!--
4+
The wire envelope (Envelope/Meta/DeadLetter records) is an internal, read-only
5+
DTO produced by the codec. We deliberately do NOT defensive-copy its `data`
6+
map or nested collections: the envelope is on the per-message hot path and
7+
BabelQueue holds a <=2% codec-overhead budget (GR-8). A decoded envelope is to
8+
be treated as immutable by callers. Suppress only the representation-exposure
9+
patterns, nothing else.
10+
-->
11+
<Match>
12+
<Bug pattern="EI_EXPOSE_REP,EI_EXPOSE_REP2"/>
13+
</Match>
14+
</FindBugsFilter>
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
package com.babelqueue;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertNull;
5+
import static org.junit.jupiter.api.Assertions.assertSame;
6+
import static org.junit.jupiter.api.Assertions.assertThrows;
7+
import static org.junit.jupiter.api.Assertions.assertTrue;
8+
9+
import java.math.BigInteger;
10+
import java.util.Arrays;
11+
import java.util.List;
12+
import java.util.Map;
13+
import org.junit.jupiter.api.Test;
14+
15+
/** Exercises the hand-rolled JSON codec's error/escape/number paths and the exceptions. */
16+
class JsonEdgeCasesTest {
17+
18+
// ---- writer ----------------------------------------------------------------
19+
20+
@Test
21+
void writesArraysAndNestedStructures() {
22+
assertEquals("[1,2,3]", Json.write(List.of(1, 2, 3)));
23+
assertEquals("{\"a\":[true,null]}", Json.write(Map.of("a", Arrays.asList(true, null))));
24+
}
25+
26+
@Test
27+
void writeRejectsNonFiniteNumbers() {
28+
assertThrows(BabelQueueException.class, () -> Json.write(Double.NaN));
29+
assertThrows(BabelQueueException.class, () -> Json.write(Double.POSITIVE_INFINITY));
30+
}
31+
32+
@Test
33+
void writeFallsBackToToStringForUnknownTypes() {
34+
Object weird = new Object() {
35+
@Override
36+
public String toString() {
37+
return "X";
38+
}
39+
};
40+
assertEquals("\"X\"", Json.write(weird));
41+
}
42+
43+
@Test
44+
void writeEscapesAllControlCharacters() {
45+
assertEquals("\"\\b\\f\\n\\r\\t\"", Json.write("\b\f\n\r\t"));
46+
assertEquals("\"\\u0001\"", Json.write(String.valueOf((char) 1)));
47+
}
48+
49+
@Test
50+
void writesIntegersDoublesAndBigIntegers() {
51+
assertEquals("42", Json.write(42L));
52+
assertEquals("3.5", Json.write(3.5));
53+
assertEquals(
54+
"123456789012345678901234567890",
55+
Json.write(new BigInteger("123456789012345678901234567890")));
56+
}
57+
58+
// ---- parser: strings / escapes --------------------------------------------
59+
60+
@Test
61+
void parsesAllStringEscapes() {
62+
assertEquals("\"", Json.parse("\"\\\"\""));
63+
assertEquals("/", Json.parse("\"\\/\""));
64+
assertEquals("\b\f\n\r\t", Json.parse("\"\\b\\f\\n\\r\\t\""));
65+
assertEquals("A", Json.parse("\"\\u0041\""));
66+
}
67+
68+
@Test
69+
void rejectsInvalidAndTruncatedEscapes() {
70+
assertThrows(BabelQueueException.class, () -> Json.parse("\"\\x\""));
71+
assertThrows(BabelQueueException.class, () -> Json.parse("\"\\u00\""));
72+
assertThrows(BabelQueueException.class, () -> Json.parse("\"abc"));
73+
}
74+
75+
// ---- parser: numbers -------------------------------------------------------
76+
77+
@Test
78+
void parsesNumberForms() {
79+
assertEquals(-7L, Json.parse("-7"));
80+
assertEquals(3.14, Json.parse("3.14"));
81+
assertEquals(1.5e3, Json.parse("1.5E+3"));
82+
assertEquals(2.0e-2, Json.parse("2e-2"));
83+
assertEquals(
84+
new BigInteger("123456789012345678901234567890"),
85+
Json.parse("123456789012345678901234567890"));
86+
}
87+
88+
// ---- parser: literals ------------------------------------------------------
89+
90+
@Test
91+
void parsesBooleansAndNull() {
92+
assertEquals(Boolean.TRUE, Json.parse("true"));
93+
assertEquals(Boolean.FALSE, Json.parse("false"));
94+
assertNull(Json.parse("null"));
95+
}
96+
97+
@Test
98+
void rejectsBadLiterals() {
99+
assertThrows(BabelQueueException.class, () -> Json.parse("tru"));
100+
assertThrows(BabelQueueException.class, () -> Json.parse("nul"));
101+
}
102+
103+
// ---- parser: structure -----------------------------------------------------
104+
105+
@Test
106+
void parsesEmptyObjectAndArray() {
107+
assertEquals(Map.of(), Json.parse("{}"));
108+
assertEquals(List.of(), Json.parse("[]"));
109+
}
110+
111+
@Test
112+
void rejectsObjectStructureErrors() {
113+
assertThrows(BabelQueueException.class, () -> Json.parse("{1:2}"));
114+
assertThrows(BabelQueueException.class, () -> Json.parse("{\"a\" 1}"));
115+
assertThrows(BabelQueueException.class, () -> Json.parse("{\"a\":1 \"b\":2}"));
116+
}
117+
118+
@Test
119+
void rejectsArrayStructureErrors() {
120+
assertThrows(BabelQueueException.class, () -> Json.parse("[1 2]"));
121+
}
122+
123+
@Test
124+
void rejectsTrailingContentUnexpectedCharAndEnd() {
125+
assertThrows(BabelQueueException.class, () -> Json.parse("1 2"));
126+
assertThrows(BabelQueueException.class, () -> Json.parse("@"));
127+
assertThrows(BabelQueueException.class, () -> Json.parse(""));
128+
}
129+
130+
// ---- exceptions ------------------------------------------------------------
131+
132+
@Test
133+
void exceptionConstructors() {
134+
UnknownUrnException u = new UnknownUrnException("urn:babel:x:y");
135+
assertTrue(u.getMessage().contains("urn:babel:x:y"));
136+
137+
Throwable cause = new IllegalStateException("boom");
138+
BabelQueueException e = new BabelQueueException("wrapped", cause);
139+
assertSame(cause, e.getCause());
140+
}
141+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package com.babelqueue;
2+
3+
import static org.junit.jupiter.api.Assertions.assertTrue;
4+
5+
import java.util.LinkedHashMap;
6+
import java.util.Map;
7+
import org.junit.jupiter.api.Test;
8+
9+
/**
10+
* GR-8 budget: the envelope encode/decode path must add no more than 2% over plain
11+
* JSON serialization (the baseline a publisher already pays), measured against a
12+
* conservative broker round-trip. Pure CPU — no broker — so the gate is stable and
13+
* environment-independent in CI. Same methodology + reference as every other SDK.
14+
*/
15+
class OverheadBenchmarkTest {
16+
17+
// Conservative networked broker round-trip (ns): local loopback Redis measures
18+
// ~300µs; production brokers are slower, so 750µs is conservative.
19+
private static final long REFERENCE_BROKER_ROUNDTRIP_NS = 750_000L;
20+
21+
private static Map<String, Object> data() {
22+
Map<String, Object> m = new LinkedHashMap<>();
23+
m.put("order_id", 1042L);
24+
m.put("amount", 99.9);
25+
m.put("currency", "USD");
26+
m.put("note", "café ☕");
27+
return m;
28+
}
29+
30+
@Test
31+
void codecOverheadWithinBudget() {
32+
Map<String, Object> data = data();
33+
34+
Runnable envelope = () -> {
35+
String body = EnvelopeCodec.encode(EnvelopeCodec.make("urn:babel:orders:created", data));
36+
EnvelopeCodec.decode(body);
37+
};
38+
Runnable bare = () -> Json.parse(Json.write(data));
39+
40+
double marginal = Math.max(0.0, nsPerOp(envelope) - nsPerOp(bare));
41+
double overhead = marginal / REFERENCE_BROKER_ROUNDTRIP_NS * 100;
42+
43+
assertTrue(
44+
overhead <= 2.0,
45+
String.format(
46+
"codec overhead %.2f%% exceeds the 2%% GR-8 budget (marginal %.0f ns)",
47+
overhead, marginal));
48+
}
49+
50+
private static double nsPerOp(Runnable fn) {
51+
for (int i = 0; i < 50_000; i++) { // warm up (JIT)
52+
fn.run();
53+
}
54+
int iterations = 200_000;
55+
long start = System.nanoTime();
56+
for (int i = 0; i < iterations; i++) {
57+
fn.run();
58+
}
59+
return (double) (System.nanoTime() - start) / iterations;
60+
}
61+
}

0 commit comments

Comments
 (0)