Skip to content

Commit 2155903

Browse files
committed
Issue #145 harden JSON number math test coverage
Audit of the full JSON number-math input space after the 43325738c uplift found the ported upstream tests rigorous except for boundary extremes: Integer.MIN_VALUE / Long.MIN_VALUE as accepted conversions, exponents at the int boundaries (1e2147483647 / 1e2147483648 / 1e-2147483648 / 1e-2147483649) that exercise Math.addExact/subtractExact/negateExact and the Utils.powExact polyfill, parsed -0 value semantics, the JsonNumber.of(String) whitespace contract, and Json.parse("-")/parse(""). New JsonNumberBoundaryTest (10 tests) covers these; expected behaviour is sourced from the JsonValue javadoc ranges, the upstream implementation and RFC 8259 section 6. TestJsonNumberOfDouble now extends JsonTestLoggingConfig and drops its ad-hoc System.out per module test rules. Full suite 1665 -> 1675; ci.yml exp_tests updated. How to verify: /opt/homebrew/bin/mvnd -pl json-java21 test -Djava.util.logging.ConsoleHandler.level=INFO /opt/homebrew/bin/mvnd clean verify -Djava.util.logging.ConsoleHandler.level=INFO (all modules green)
1 parent ef91166 commit 2155903

3 files changed

Lines changed: 133 additions & 6 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ jobs:
3939
for k in totals: totals[k]+=int(r.get(k,'0'))
4040
except Exception:
4141
pass
42-
exp_tests=1665
42+
exp_tests=1675
4343
exp_skipped=0
4444
if totals['tests']!=exp_tests or totals['skipped']!=exp_skipped:
4545
print(f"Unexpected test totals: {totals} != expected tests={exp_tests}, skipped={exp_skipped}")
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
package jdk.incubator.java.util.json;
2+
3+
import org.junit.jupiter.api.Test;
4+
5+
import static org.junit.jupiter.api.Assertions.assertEquals;
6+
import static org.junit.jupiter.api.Assertions.assertThrows;
7+
import static org.junit.jupiter.api.Assertions.assertTrue;
8+
9+
/// Boundary hardening for JSON number math, added by the final-defense audit
10+
/// of issue #145. Complements the ported upstream {@code TestJsonNumber} and
11+
/// the issue #118 {@code JsonNumberOfDoubleMatrixTest} with the extremes that
12+
/// exercise {@code JsonNumberImpl}'s fast path, overflow guards
13+
/// ({@code Math.addExact}/{@code subtractExact}/{@code negateExact} and
14+
/// {@code Utils.powExact}) and the {@code JsonNumber.of(String)} whitespace
15+
/// contract. Expected behaviour is taken from the {@link JsonValue} javadoc
16+
/// ranges ("MIN_VALUE to MAX_VALUE, inclusive"), the upstream implementation
17+
/// and RFC 8259 section 6.
18+
public class JsonNumberBoundaryTest extends JsonTestLoggingConfig {
19+
20+
@Test
21+
void integerMinValueBoundaryIsRepresentable() {
22+
var jn = Json.parse("-2147483648");
23+
assertEquals(Integer.MIN_VALUE, jn.asInt(), "asInt at Integer.MIN_VALUE");
24+
assertEquals(-2147483648L, jn.asLong(), "asLong at Integer.MIN_VALUE");
25+
assertEquals(-2147483648.0d, jn.asDouble(), "asDouble at Integer.MIN_VALUE");
26+
assertEquals("-2147483648", jn.toString(), "toString preservation");
27+
}
28+
29+
@Test
30+
void longMinValueBoundaryIsRepresentable() {
31+
var jn = Json.parse("-9223372036854775808");
32+
assertEquals(Long.MIN_VALUE, jn.asLong(), "asLong at Long.MIN_VALUE");
33+
assertThrows(JsonValueException.class, jn::asInt, "asInt beyond Integer range");
34+
assertEquals(-9.223372036854776E18d, jn.asDouble(), "asDouble at Long.MIN_VALUE");
35+
assertEquals("-9223372036854775808", jn.toString(), "toString preservation");
36+
}
37+
38+
@Test
39+
void exponentAtIntMaxOverflowsAllConversions() {
40+
// 1e2147483647: Integer.parseInt of the exponent succeeds, but
41+
// Utils.powExact(10, 2147483647) overflows long -> ArithmeticException
42+
// -> Optional.empty -> JsonValueException; asDouble is Infinity.
43+
var jn = Json.parse("1e2147483647");
44+
assertThrows(JsonValueException.class, jn::asInt, "asInt for 1e2147483647");
45+
assertThrows(JsonValueException.class, jn::asLong, "asLong for 1e2147483647");
46+
assertThrows(JsonValueException.class, jn::asDouble, "asDouble for 1e2147483647");
47+
assertEquals("1e2147483647", jn.toString(), "toString preservation");
48+
}
49+
50+
@Test
51+
void exponentAboveIntMaxOverflowsAllConversions() {
52+
// 1e2147483648: Integer.parseInt of the exponent itself overflows.
53+
var jn = Json.parse("1e2147483648");
54+
assertThrows(JsonValueException.class, jn::asInt, "asInt for 1e2147483648");
55+
assertThrows(JsonValueException.class, jn::asLong, "asLong for 1e2147483648");
56+
assertThrows(JsonValueException.class, jn::asDouble, "asDouble for 1e2147483648");
57+
assertEquals("1e2147483648", jn.toString(), "toString preservation");
58+
}
59+
60+
@Test
61+
void exponentAtIntMinOverflowsIntegralOnly() {
62+
// 1e-2147483648: Math.negateExact(Integer.MIN_VALUE) overflows during
63+
// the 10^power division path -> JsonValueException for asInt/asLong;
64+
// asDouble underflows to a finite 0.0 via Double.parseDouble.
65+
var jn = Json.parse("1e-2147483648");
66+
assertThrows(JsonValueException.class, jn::asInt, "asInt for 1e-2147483648");
67+
assertThrows(JsonValueException.class, jn::asLong, "asLong for 1e-2147483648");
68+
assertEquals(0.0d, jn.asDouble(), "asDouble for 1e-2147483648");
69+
assertEquals("1e-2147483648", jn.toString(), "toString preservation");
70+
}
71+
72+
@Test
73+
void exponentBelowIntMinOverflowsIntegralOnly() {
74+
// 1e-2147483649: Integer.parseInt of the exponent fails; asDouble
75+
// still underflows to a finite 0.0.
76+
var jn = Json.parse("1e-2147483649");
77+
assertThrows(JsonValueException.class, jn::asInt, "asInt for 1e-2147483649");
78+
assertThrows(JsonValueException.class, jn::asLong, "asLong for 1e-2147483649");
79+
assertEquals(0.0d, jn.asDouble(), "asDouble for 1e-2147483649");
80+
assertEquals("1e-2147483649", jn.toString(), "toString preservation");
81+
}
82+
83+
@Test
84+
void parsedNegativeZeroKeepsTextAndValue() {
85+
var jn = Json.parse("-0");
86+
assertEquals("-0", jn.toString(), "toString preservation");
87+
assertEquals(0, jn.asInt(), "asInt of -0");
88+
assertEquals(0L, jn.asLong(), "asLong of -0");
89+
// Double.compare distinguishes -0.0 from 0.0; RFC 8259 permits "-0"
90+
// and asDouble is specified to properly return negative zero.
91+
assertEquals(0, Double.compare(jn.asDouble(), -0.0d), "asDouble of -0 is negative zero");
92+
}
93+
94+
@Test
95+
void ofStringStripsInsignificantWhitespace() {
96+
// JsonNumber.of(String) javadoc: the representation is equivalent to
97+
// num with any leading or trailing JSON insignificant whitespaces removed.
98+
var jn = JsonNumber.of(" 3 ");
99+
assertEquals("3", jn.toString(), "toString of of(\" 3 \")");
100+
assertEquals(3, jn.asInt(), "asInt of of(\" 3 \")");
101+
var neg = JsonNumber.of("\t-0.5\n");
102+
assertEquals("-0.5", neg.toString(), "toString of of(\"\\t-0.5\\n\")");
103+
assertEquals(-0.5d, neg.asDouble(), "asDouble of of(\"\\t-0.5\\n\")");
104+
}
105+
106+
@Test
107+
void bareMinusAndEmptyTextAreNotJsonNumbers() {
108+
// RFC 8259 section 6: a number must contain at least one integer digit;
109+
// an empty text is not a JSON value (RFC 8259 section 2).
110+
assertThrows(JsonParseException.class, () -> Json.parse("-"), "parse(\"-\")");
111+
assertThrows(JsonParseException.class, () -> Json.parse(""), "parse(\"\")");
112+
assertThrows(IllegalArgumentException.class, () -> JsonNumber.of("-"), "of(\"-\")");
113+
assertThrows(IllegalArgumentException.class, () -> JsonNumber.of(""), "of(\"\")");
114+
}
115+
116+
@Test
117+
void intMinValueViaFactoryStringContract() {
118+
// of(String) must accept the same boundary text parse accepts and be
119+
// behaviourally identical to the parsed value.
120+
var viaFactory = JsonNumber.of("-2147483648");
121+
var viaParse = Json.parse("-2147483648");
122+
assertEquals(viaParse.toString(), viaFactory.toString(), "toString parity");
123+
assertEquals(Integer.MIN_VALUE, viaFactory.asInt(), "asInt parity");
124+
assertEquals(Long.MIN_VALUE, JsonNumber.of("-9223372036854775808").asLong(),
125+
"asLong of of(\"-9223372036854775808\")");
126+
assertTrue(viaFactory instanceof JsonNumber, "factory produces JsonNumber");
127+
}
128+
}

‎json-java21/src/test/java/jdk/incubator/java/util/json/TestJsonNumberOfDouble.java‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,24 @@
33
import org.junit.jupiter.api.Test;
44
import static org.assertj.core.api.Assertions.*;
55

6-
public class TestJsonNumberOfDouble {
7-
6+
public class TestJsonNumberOfDouble extends JsonTestLoggingConfig {
7+
88
@Test
99
void ofDoubleToStringPreservesValue() {
1010
var jn = JsonNumber.of(123.45);
1111
assertThat(jn.toString()).isEqualTo("123.45");
1212
}
13-
13+
1414
@Test
1515
void ofDoubleToDoubleWorks() {
1616
var jn = JsonNumber.of(123.45);
1717
assertThat(jn.asDouble()).isEqualTo(123.45);
1818
}
19-
19+
2020
@Test
2121
void ofDoubleThenToLongForIntegralDouble() {
2222
// 123.0 should be convertible to long 123
2323
var jn = JsonNumber.of(123.0);
24-
System.out.println("toString: " + jn.toString());
2524
assertThat(jn.asLong()).isEqualTo(123L);
2625
}
2726

0 commit comments

Comments
 (0)