Repository navigation
Core Test Suite
CoreTestSuiteis the single@RunWith(Suite.class)entry point thatcore/build-tests.xmlruns; it went from 8 classes / 203 tests to 14 classes / 272 runnable tests in one week by converting strandedmain()and JUnit 3 classes into host-runnable JUnit 4.
core/src/test/org/jnode/test/CoreTestSuite.java is the only class core/build-tests.xml's all-junit target names in its <batchtest> fileset, so a test class that is not listed in @SuiteClasses does not run at all — it compiles into core/build/testclasses and silently disappears. This is the same failure mode that hit the shell subproject in Testing.
@RunWith(Suite.class)
@SuiteClasses({
NumberUtilsTest.class, ResourceTest.class, SignatureTest.class,
VersionTest.class, TryFinallyTest.class, VarArgsTest.class,
ForEachTest.class, ViewMethodTest.class, DoubleTest.class,
WaitTest.class, JavaCommandParserTest.class,
ThreadingTestJUnit4.class, X86RegisterPoolTest.class,
X86StreamTest.class,
})
public class CoreTestSuite { }| Run | Classes |
@Test methods |
|---|---|---|
| Before 2026-09-27 | 8 | 203 |
| After (2026-10-03) | 14 | 273 annotations / 272 runnable (X86StreamTest.testDumpStreamToFile is @Ignored) |
VersionTest alone contributes 95 of those.
These tests run in the build host's OpenJDK against the JNode core jar, so they can only assert behaviour that is true on any conforming JVM. Three consequences shape the whole suite:
- Anything needing a live JNode VM (
VmType.fromClass,VmMethod.getNativeCodeOptLevel(),VmSystem,InitialNaming.lookup) must either be dropped or guarded byAssume. - Any production class reached through
BootLogInstance.get()NPEs off-VM becauseNAME_SPACEis null — that, not test quality, is whyResourceTestwas excluded from the suite for its whole JUnit 3 life. - The pure-Java parts of the VM are fair game:
X86BinaryAssemblerneeds no VM, which is what made 21 assembler tests possible.
| Class | @Test |
Role |
|---|---|---|
org.jnode.test.util.VersionTest |
95 | Version parsing/ordering/hashCode; the reference example for a banner-grouped expansion |
org.jnode.test.util.NumberUtilsTest |
33 |
org.jnode.util.NumberUtils hex/size formatting |
org.jnode.test.ResourceTest |
25 |
ClassLoader/Class/ResourceBundle resource lookup and delegation |
org.jnode.test.X86StreamTest |
22 (21 run) |
org.jnode.assembler.x86.X86BinaryAssembler encoding |
org.jnode.test.SignatureTest |
17 |
VmType-independent signature parsing |
org.jnode.test.VarArgsTest |
15 | varargs bytecode generation |
org.jnode.test.JavaCommandParserTest |
11 | command-line parsing helpers |
org.jnode.test.TryFinallyTest |
9 | try/finally and exception-table codegen |
org.jnode.test.DoubleTest |
9 | JLS §5.1.3 double → int narrowing rules |
org.jnode.test.threads.ThreadingTestJUnit4 |
10 | thread start/join against the host JVM |
org.jnode.vm.x86.compiler.l1a.X86RegisterPoolTest |
8 | L1A register allocation spill bookkeeping |
org.jnode.test.ForEachTest |
7 | for-each over arrays and collections |
org.jnode.test.WaitTest |
6 |
Object.wait/notify/notifyAll semantics |
org.jnode.test.ViewMethodTest |
6 | method metadata; the one class with a guest-only test |
Five issues (#489, #695, #696, #697 and the suite-wiring follow-ups) converted every remaining stranded class. The recurring blockers were always the same three JUnit facts.
A junit.framework.TestCase subclass or a plain class with only main() in a @SuiteClasses list fails with initializationError: No runnable methods. Conversion means: drop extends TestCase, drop suite(), replace failUnless/assertTrue with statically imported org.junit.Assert.*, and rename testXxx() → @Test public void x() or keep the name and annotate it.
ViewMethodTest is the template. The original dumped compiled code through VmType, which only exists inside JNode. The rewrite checks metadata by reflection (host-runnable) and keeps one cross-check behind a VM probe:
@Test
public void testVmTypeReportsTheSameMethods() throws Exception {
Assume.assumeTrue(isInsideJNodeVm()); // ViewMethodTest.java:117
...
}
private static boolean isInsideJNodeVm() { // :137-139
return "JNode".equals(System.getProperty("java.vm.name"));
}java.vm.name is set by JNode in core/src/core/org/jnode/vm/VmSystem.java:315, so the probe is a property read, not a boot-logic call. Five of the six tests therefore still run in CI; the sixth is skipped, not failed.
getDeclaredMethods() order is JVM-unspecified, so testDeclaredMethodNames collects into a HashSet and compares sets rather than arrays.
WaitTest was a main() stress loop that sequenced 10 threads with a fixed Thread.sleep(2000). The rewrite is a counter plus a deadline:
private static final long JOIN_TIMEOUT = 20000; // WaitTest.java:43
private static final long WAIT_TIMEOUT = 300; // :44
private void waitUntilEntered(WaitTest wt, int n) throws InterruptedException { // :185-192
long deadline = System.currentTimeMillis() + JOIN_TIMEOUT;
while (wt.getEnteredCount() < n) {
assertTrue("waiter never entered wait()", System.currentTimeMillis() < deadline);
Thread.sleep(1);
}
}Every join is join(JOIN_TIMEOUT) followed by assertFalse(t.isAlive()), so a lost wakeup is a failure, not a hang. Five orthogonal counters (entered, finished, woken, timedOut, interrupted, :46-51) let a test assert how a thread left the wait; all accesses are inside synchronized methods, which is why the fields do not need volatile.
Coverage added: notifyAll releases all 10 waiters, notify wakes a waiter (deliberately started with exactly one waiter so the assertion does not depend on JVM waiter-selection policy), timed wait expiry, timed wait woken by notifyAll, interruption (InterruptedException → interrupted == 1, woken == 0), and the already-signalled fast path returning without incrementing entered.
ResourceTest's JUnit 3 version left Locale.getDefault() mutated at Locale.FRENCH, leaking into whichever test ran next in the suite. The JUnit 4 version brackets every locale change:
@Before public void saveLocale() { saved = Locale.getDefault(); }
@After public void restoreLocale() { Locale.setDefault(saved); }Locale.setDefault still goes through AccessController.doPrivileged (ResourceTest.java:332-339) so it works under a security manager.
25 tests in five banner groups (:78, :116, :150, :182, :236): ClassLoader lookup, system-classloader lookup, parent delegation, Class.getResource, and ResourceBundle.
The single most important encoded fact:
ClassLoader.getResourcedoes NOT strip a leading/.Class.getResourcedoes.
The JUnit 3 version carried two "absolute path" ClassLoader cases that could never pass; they were deleted rather than fixed. Every ClassLoader test therefore passes addRoot = false to the relativeToAbsolutePath helper (:301-305), while only the Class.getResource tests pass addRoot = true.
Other pinned behaviour:
-
ClassLoader.getSystemResource(name)returns the sameURL.toExternalForm()asClass.getClassLoader().getResource(name)(:126-133). - Delegation: an anonymous
new ClassLoader(parent) {}must resolve the parent's URL and must returnnullfor an unknown resource — a child must not fabricate. -
ResourceBundle.getBundle("messages")with a relative base name throwsMissingResourceException; base names must be fully qualified. - The loaded bundle must be exactly
PropertyResourceBundle.class(:327). Fixtures aremessages.properties,messages_en_US.properties,messages_fr.propertiesinorg/jnode/test/; becausemessages_en_UScarries the sametestokvalue, theLocale.UScase cannot distinguish base from_en_USload. -
ResourceBundlecaches per (baseName, locale, loader), so theUS-then-FRENCHsequence order matters.
Dropped coverage: the JUnit 3 doGetLocalizedMessage test for PluginUtils.getLocalizedMessage is gone. It calls BootLogInstance.get() → InitialNaming.lookup(BootLog.class), and NAME_SPACE is null on a host JVM, so it NPEs. org.jnode.util.PluginUtils is no longer imported by the file. Restoring it needs a BootLog binding.
The assembler is pure Java, so newAssembler always builds against a synthetic CPU, never the live one:
private static X86BinaryAssembler newAssembler(Mode mode) { // X86StreamTest.java:60-62
return new X86BinaryAssembler(X86CpuID.createID("pentium4"), mode);
}Both Mode.CODE32 and Mode.CODE64 are exercised. The suite went from 0 methods / 0 assertions to 22/40, and the dead code it removed was hiding a real bug:
- The old
testCode32hadif (true) { return; }right after the IDIV loop, disabling roughly 100 instructions. Deleting the short-circuit exposedwriteMOV(BITS8, ECX, EBX, 1, 4, ESI)—ESIhas no 8-bit sub-register, soX86BinaryAssembler.testSuitableForBits8(X86BinaryAssembler.java:773, called from:3063) rejects it. Fixed toEDXatX86StreamTest.java:455. - The old
main()dumped a binary file into the working directory. That survives astestDumpStreamToFilewith@Ignore("manual debugging aid: dumps the 32 bit stream to test.bin").
Encoding facts pinned by golden-byte assertions:
| Fact | Assertion |
|---|---|
| Relative displacement is measured from the end of the jump instruction | rel32 back-patch get32(1) == 3; rel8 get8(2) == 1; short forward jump (byte) -2; distant jump get32(201) == -205
|
| A zero-distance rel jump shrinks to NOPs |
0xE9 rel32 becomes 0x90 0x90 0x90 0x90 0x90, total length 5 |
| Indirect jump table entries are biased by the opcode size |
writeJMP(label, 2, false) → 0xFF 0x25 + labelOffset + 2, 6 bytes total |
writeJECXZ0 emits an address-size prefix |
0x67 before 0xE3
|
A label stays unresolved until a later setObjectRef back-patches it |
the jump is emitted first, then addUnresolvedLink → resolve8/resolve32 (X86BinaryAssembler.java:180, :286, :318). Pre-resolving the label before writeJMP was rejected in review because it never reaches the resolver |
| IDIV is exactly 2 bytes | 5 registers → getLength() == 10, first bytes 0xF7 0xF8
|
| 64-bit forms |
writeCDQE → 48 98; mov rax, imm64 → 48 B8 + 8 little-endian bytes, total 12; get64(4) == 0x1234L
|
clear() resets the cursor and the object-ref table |
allocate(4) then allocate(8) returns 0 then 4, total 12 |
| Duplicate labels are rejected |
@Test(expected = RuntimeException.class); the message text is not asserted |
An unresolved ObjectRef throws on getOffset()
|
only the non-empty message is asserted, not the wording |
writeTo(OutputStream) is compared against getBytes() truncated to getLength(), which proves the stream write is repeatable and length-consistent.
9 methods, but loop-driven, so ~1,000 dynamic evaluations. All use one helper so the narrowing happens in exactly one place:
private static int d2i(double d) { return (int) d; } // DoubleTest.java:40-42Pinned JLS §5.1.3 rules:
- Truncation is toward zero, not floor:
-0.5 → 0,-1.9 → -1,-2.5 → -2(encoded asexpected = (i < 0) ? i + 1 : i,:74). - NaN → 0.
- Out-of-range saturates rather than wraps:
+Inf → Integer.MAX_VALUE,-Inf → Integer.MIN_VALUE;±1e18the same. - The sweep
d += 0.125accumulates float error, sotestFractionalPartIsAlwaysBelowOneasserts inequalities —truncated <= dford >= 0,truncated >= dford < 0,|d - truncated| < 1.0— not exact values. -
i + 0.5is exact in binary64 for integrali, which is what makes the expected values in the truncation sweep reliable.
testNarrowingRoundTrip is loosely named: d - 0.75 == i exactly (both operands representable, |i| ≤ 50), so it degenerates into an int→double→int check.
7 tests, host-pure java.lang bytecode-generation checks. Determinism comes from comparing against list.get(i) or a monotonic counter inside the loop rather than a hardcoded expectation list, so the tests survive whichever for-each desugaring the compiler picks.
-
testEmptyArray— the body must never run (counter stays 0); zero length must not throw. -
testNullElementsArePassedThrough— anullelement is delivered asnull, not skipped and not NPE'd. -
testArrayIsCopiedBeforeIteration— labelled as the JLS 5.1.3 copy-at-loop-entry case, but vacuous with respect to copy semantics: it mutatesa[0]after index 0 has been read and never touchesa[1]/a[2], so the expected1,2,3holds under both a snapshot copy and index-by-index reading. Mutatinga[1]would be the discriminating mutation. (The governing spec section for for-each is 14.4.2/15.11; §5.1.3 is Binary Numeric Promotion.)
33 methods / ~155 assertions, up from 13 methods with one assertion each. Shared fixtures (:33-37): BYTES = {0x00,0x0F,0xA5,0xFF}, INTS = {1,255,65535,-1,0x12345678}, CHARS = {'A',0x01,0xFFFE,'z'}.
Now covered: both toUnsigned primitive overloads, hex(int,int)/hex(long,int) plus the single-argument forms, hex(byte[]), hex(byte[],int,int) with line wrapping, hexCompact(byte[],int,int), hex(int[],int,int), hex(char[],int,int), the deprecated size(long), both toDecimalByte and both toBinaryByte nbDecimals overloads, and the null/blank paths of getSize/getSizeUnit.
Non-obvious pinned behaviour:
-
toStringtruncates, never rounds —toString(15.5f, 2) == "15.5", andmaxFractionLength == 0yields the degenerate"15."with the trailing dot kept. -
prefixZerotruncates from the left:hex(255, 0) == ""andhex(0xFFFFFFFF, 4) == "ffff"keeps the LOW 4 digits. - The
% 16 == 0line wrap: 20 bytes → 16 on the first line,11 12 13 14on the second. The compact form wraps too but emits no separator space. - Negative bytes wrap via
& 0xFF;hex(int[])renders-1asffffffff;hex(char[])passes the char straight tohex(int,int), which is why0xFFFEprints asfeand notfffe. -
getSizeUnitmatches suffixes case-sensitively:"1k"→null, while"100"/"0"returnB.getSizetrims (" 1K "→ 1024) and maps null/blank to0.
Two real production bugs were found while deriving the expected values and are deliberately not asserted, because the fixes belong in core/src/core/org/jnode/util/NumberUtils.java:
getSizeUnit (NumberUtils.java:327-332) iterates SizeUnit.values() in declaration order and returns the first suffix match. B is declared first (core/src/core/org/jnode/util/SizeUnit.java:30), and "1KB".endsWith("B") is true, so K at SizeUnit.java:31 is never reached. getSize then takes the sizeUnit != null branch, sets multiplier = 1, strips only one character (NumberUtils.java:316), and calls Long.parseLong("1K") at :319:
getSizeUnit("1KB") = B
getSize("1KB") -> java.lang.NumberFormatException: For input string: "1K"
A fix is longest-suffix-wins in getSizeUnit, or stripping the full unit in getSize.
NumberUtils.java:38 computes:
final int len = Math.min(s.length(), idx + maxFractionLength + 1);With maxFractionLength == Integer.MAX_VALUE and idx == 2 (for "15.2365"), the sum overflows to -2147483646, so s.substring(0, -2147483646) at :39 throws:
toString(15.2365f, Integer.MAX_VALUE) -> StringIndexOutOfBoundsException: String index out of range: -2147483646
toDecimalByte(1234567, Integer.MAX_VALUE) -> StringIndexOutOfBoundsException: ... -2147483647
This contradicts the documented contract: NumberUtils.java:274 and :285 both say "use Integer.MAX_VALUE for all" for nbDecimals, and DecimalScaleFactor.apply / BinaryScaleFactor.apply pass nbDecimals straight through, so those two are broken too. The fix belongs in toString (clamp to s.length(), or use long arithmetic).
Consequence for the suite: every nbDecimals test uses a small value (0-4), so the Integer.MAX_VALUE sentinel path is left uncovered rather than pinned.
-
build-tests.xmlnames exactly one class.core/build-tests.xml's<batchtest>fileset isorg/jnode/test/CoreTestSuite.java. Being a@Testclass in the right directory is not enough. -
No runnable methodsis the signature of a JUnit 3TestCaseor amain()-only class left in a JUnit 4 suite. -
Off-VM
BootLogInstance.get()NPEs. Any production path that starts at a named-object lookup cannot be unit tested off-VM. -
java.vm.name == "JNode"is the cheapest available "am I in the guest" probe (VmSystem.java:315). -
getDeclaredMethods()order is unspecified — compareHashSets. - A sleeping test is not a synchronised test. Use an entry counter plus a deadline so a lost wakeup fails instead of hanging.
-
Locale.getDefault()is global mutable state. Bracket it with@Before/@Afteror it leaks into the next suite member. -
ClassLoader.getResourcekeeps a leading/;Class.getResourcestrips it. A single path helper with anaddRootflag is what keeps the two halves honest. - Relative
ResourceBundlebase names always throw. - Relative branch displacements are measured from the end of the instruction, which is why a golden-byte test is worth more than a "does it assemble" test.
-
An assembler label is only resolved by a later
setObjectRef. Resolving it before the jump skips the whole back-patch path. -
Sizing the test-only merge allowance is a live trade-off — see Agent-Issue-Pipeline;
isDiffSafecurrently allows 600 added lines when every changed path matches/(^|\/)(src\/test\/|tests\/)/, still capped at 5 files.
-
Testing — Hub: subproject
build-tests.xmlconvention, boot testing, JDWP suite. -
JNAsm-Instruction-Encoding — the textual assembler's encoding tables, as opposed to
X86BinaryAssembler's runtime emit path thatX86StreamTestcovers. -
X87-FPU-Support — the guest-side
main()test programs (ConversionTest,RintTest,StrictMathTest) that cover what host JUnit cannot. - Agent-Issue-Pipeline — the automation that produced these conversions and the merge gate that admits them.
- Code-Conventions — Java 1.6 target that these tests are written against.