Skip to content

Latest commit

 

History

History
291 lines (201 loc) · 7.5 KB

File metadata and controls

291 lines (201 loc) · 7.5 KB

Observer Java HOWTO

This manual explains how to use Observer's Java-facing provider library.

It is written for projects that want Java-native test authoring while still exposing deterministic provider contracts to Observer.

Quick Start: First 5 Commands

cd lib/java
make test
make example-smoke
cd starter
make list
make verify

That gives you the whole path in order:

  • validate the library itself
  • see the smallest executable example
  • build a real provider host
  • inspect raw provider discovery output
  • verify canonical inventory, suite hash, and JSONL report artifacts

If your application already owns its CLI, use starter-embedded/ instead. That path keeps normal application behavior outside an explicit observe routing point.

If you want a normal consumer-shaped Java app rather than an SDK-local example, see ../../examples/java-consumer/.

The SDK now includes native Java project metadata too:

  • pom.xml for Maven users
  • build.gradle and settings.gradle for Gradle users
  • module-info.java for JPMS-aware projects

Those metadata files now also carry publish-ready release information:

  • MIT license declaration for the library subtree
  • GitHub SCM and issue tracker links
  • source and Javadoc jar generation
  • optional signing hooks for release publication

1. What This Library Is

This library is the Java-facing micro-SDK for Observer's provider contract.

It does not try to replace JUnit or become a general Java test framework.

It gives Java projects a deterministic way to:

  • author Observer-exposed tests in Java
  • assign stable identities explicitly when needed
  • emit bounded observational data
  • expose a standard provider host contract to the Observer CLI

2. Smallest Useful Shape

Use static imports from Observer:

import static io.frogfish.observer.Observer.*;

var tests = collectTests(() -> {
    describe("pkg", () -> {
        test("smoke", ctx -> {
            ctx.stdout("ok\n");
            expect(true).toBeTruthy();
        });
    });
});

That shape is deliberate.

  • Java authors keep lambdas and explicit block structure
  • the library surface stays small
  • the deterministic Observer contract stays visible

3. Deterministic Identity

When you omit id(...), Observer derives an identity from:

  • suite path segments in order
  • the test title
  • duplicate occurrence order when the same derived name appears twice

That means:

  • describe("pkg") + test("smoke") becomes pkg :: smoke
  • a second authored pkg :: smoke becomes pkg :: smoke #2

If a test needs a refactor-stable exported identity, set it explicitly:

test("smoke", ctx -> {
    expect(true).toBeTruthy();
}, id("pkg::smoke"));

4. Author Context

Each test receives a TestContext.

That context owns:

  • stdout(...)
  • stderr(...)
  • setExit(...)
  • fail(...)
  • observe()

Example:

test("rejects overdraft", ctx -> {
    ctx.stdout("denied overdraft\n");
    expect(false).toBe(false);
});

5. Expectations

The expectation API is intentionally small:

  • expect(value).toBe(...)
  • expect(value).toEqual(...)
  • expect(value).toContain(...)
  • expect(value).toMatch(...)
  • expect(value).toBeTruthy()
  • expect(value).toBeFalsy()
  • expect(value).not().toBe(...)

Failed expectations raise ExpectationException, and the runtime host path converts that into a failing test result automatically.

6. Observational Data

Observation is opt-in and bounded.

Use ctx.observe() only when a test has structured data worth recording:

test("profile", ctx -> {
    var observe = ctx.observe();
    observe.metric("wall_time_ns", 104233.0);
    observe.vector("latency_ns", java.util.List.of(1000.0, 1100.0, 980.0));
    observe.tag("fixture", "fixtures/demo.json");
});

The library enforces the same bounded telemetry rules as the other SDKs.

7. Provider Host Transport

The Java library owns the standard provider host transport.

That means the host surface is:

  • list
  • run --target <target> --timeout-ms <u32>
  • observe --target <target> --timeout-ms <u32>

The public SDK entrypoints are:

  • hostMain(...)
  • hostDispatch(...)
  • hostDispatchEmbedded(...)

The common direct-host shape is:

import static io.frogfish.observer.Observer.*;

public final class Host {
    public static void main(String[] args) throws Exception {
        var tests = collectTests(() -> {
            describe("pkg", () -> test("smoke", ctx -> {
                ctx.stdout("ok\n");
                expect(true).toBeTruthy();
            }, id("pkg::smoke")));
        });

        hostMain("java", tests, args);
    }
}

8. Embedded Observe Integration

If a Java application already owns its CLI, it can expose an embedded observe namespace:

if (args.length > 0 && args[0].equals("observe")) {
    hostDispatchEmbedded("java", "observe", tests, args);
    return;
}

runNormalApplication(args);

That keeps the app's normal UX intact while still exposing the standardized Observer host path.

9. Starter Workflow

The standalone starter lives in starter/.

It shows the full path:

  • ordinary Java code under test
  • Observer-authored tests in Java
  • a tiny Java provider host wrapper script in build/
  • raw list output
  • canonical inventory derivation
  • suite execution
  • canonical hash and JSONL verification

The useful commands are:

cd lib/java/starter
make list
make inventory
make host-run TARGET='ledger/rejects-overdraft'
make run
make report
make verify

10. Embedded Starter Workflow

The app-shaped starter lives in starter-embedded/.

That path keeps an application-owned main(...) and routes observe ... through it.

Use it when the Java application should stay the product entrypoint.

11. Library Self-Tests And Build

The repo validates the Java SDK with plain javac and java so the surface stays toolchain-light inside this repository.

Use:

cd lib/java
make test

That compiles the library, runs the stdlib-only self-test, and checks list plus observe transport output.

If you are moving this into a conventional Java build, the matching native entrypoints are:

cd lib/java
mvn test
gradle check

For local packaging without external build tools:

cd lib/java
make jar
make javadoc

13. Optional JUnit 5 Bridge

If your team already has a body of JUnit 5 tests, use ../java-junit5/ as the adaptation layer.

The bridge is intentionally narrow instead of magical.

  • It collects ordinary zero-argument @Test methods.
  • It runs @BeforeEach and @AfterEach around each exported method target.
  • It derives exported Observer ids from fully.qualified.ClassName#methodName unless you supply @ObserverId.
  • It treats @DisplayName as authored title sugar, not as the exported deterministic contract.

That keeps the Java adoption path natural without weakening Observer's determinism rules.

The consumer-shaped examples for that path live outside the SDK subtree in:

  • ../../examples/java-consumer-maven/
  • ../../examples/java-consumer-gradle/

12. Maven And Gradle Shape

For Java teams, the important DX improvement is that this SDK no longer forces a custom-only build story.

  • Maven metadata already defines a Java 17 jar build, source jar, Javadoc jar, and self-test execution.
  • Gradle metadata already defines a Java library build and wires check to the stdlib self-test.
  • The public package is exported through JPMS so modular applications can consume it cleanly.

The repo still validates with make because Maven and Gradle are optional here, not because Java consumers should have to avoid them.