---
title: 'Build, Testing and the Ecosystem'
source: 'https://academia.sh/en/courses/java-ecosystem'
language: en
updated: '2026-09-13T07:20:05+00:00'
license: 'CC BY-SA 4.0'
lessons: 12
outcomes:
    - 'Manages dependencies and the build life cycle with a build tool'
    - 'Sets up unit and integration tests with mock objects'
    - 'Applies the separation between the logging facade and its implementation'
    - 'Determines by measuring whether a type is required on the compile path or the run path'
---

# Build, Testing and the Ecosystem

A Java program lives against two separate class paths: the one it was compiled against and the one it runs on, and the source text can see neither. The course measures the build tool model, dependency scopes, the test setup and the logging facade with a single question — which path must this type be present on, and when the two paths diverge, does the defect appear at compile time, at run time, or not at all?

## Build Tools

The build tool's life cycle, target and task model; which class path a dependency scope makes a type visible on, that a transitive dependency can be required at run time without appearing at compile time, and what determines resolution when two candidates for the same name turn up; the compile-time counterpart of module boundaries; how the executable output that packaging produces carries the runtime path.

### Build Tool Model

Source: https://academia.sh/en/courses/java-ecosystem/build-tool-model

The phase sequence a build tool derives from a declaration is fixed and unaffected by the declaration's writing order: three declared items yield four phases, and both a forward and a reversed writing of the same declaration derive the same four phases. Two class paths coming out of the same declaration — one for compiling, one for running — are separated by an annotation type kept at the source level: it never appears in the class file's constant pool, and it is never required on the runtime class path either.

The closing lesson of the JVM, Concurrency and Performance course measured a single question:
which thread runs a step could not be read from the call syntax — it could only be seen by
running it. The same line was the same syntax whether it ran on one thread or another; what
decided the running side sat outside the source text.

This lesson asks the same question one layer out. A Java program does not live against a single
class path, it lives against two separate class paths: the one it was compiled against and the
one it runs on. The source text — even the compiled class file itself — does not say what these
two paths need to contain; what says it is a declaration standing outside the source. What the
compiler needs to see a type and what the runtime needs to find that same type are not the same
set, and the tool that produces this distinction is this lesson's and this topic's question.

The tool's name is not written down — which product, which syntax is used is not this lesson's
question. What is asked is this: how much of a few lines of declaration does a build tool
derive on its own, and why does what it derives split into two separate lists that never appear
in the source at all?

All three topics in this course are fields usually referred to by third-party tool names; this
is why the build tool itself is built here as a **behavior**, not a product. What is measured is
the rule that derives a phase sequence from a declaration and the two class paths that come out
at the end of those phases — not which product accepts which flag. The standard library itself
provides the real, running part of this model: the compiler is called from inside the program
with `ToolProvider`, and it actually compiles.

#### What a Build Tool Derives

What a declaration derives is not a single list, it is two separate things: a **phase
sequence** (what happens in what order) and the **class paths** built within those phases
(which directories each phase looks at). These two things must not be confused with each other.
The "compile sources" phase runs a single compile task: it calls the compiler once. The "build
compile class path" phase runs a task that goes through every dependency in the declaration one
by one — it adds every dependency whose scope is compile or both to the list, and skips every
dependency whose scope is runtime only. The same phase, "build runtime class path," filters the
same dependency list this time from the opposite side. The phase itself is one thing, but the
compile task within it does a varying amount of work depending on the declaration's length —
this also explains why the two numbers seen separately in the measurement below (declared item,
derived phase) need not turn out equal.

#### The Phase Sequence Derived from a Declaration

What is given to a build tool is not a step-by-step procedure, it is a **declaration**: which
source directory is to be compiled, which dependencies are to be used with which **dependency
scope**. The tool itself derives a **build lifecycle** from this declaration — a **phase**
sequence that runs in a fixed order. Each phase runs one or more **tasks**; only the phases
themselves are measured here, not the content of the tasks.

The `Declaration` class below holds a dependency only by its name and its scope; the scope
takes one of three values: required only at compile time, required only at run time, or both.

- **BT1.** The build tool is built here as a model; no real product's name, target name,
  command-line flag, or declaration syntax is used. The phase names and the declaration's shape
  are this lesson's own decision.
- **BT2.** Declared item count is a total in which every line of the declaration — the source
  directory counts as one, each dependency counts as one — counts as a single item. A
  dependency's scope does not change the item count.
- **BT3.** The derived phase sequence consists of a fixed four phases and is unaffected by the
  dependency count or by the declaration's writing order; the order is the tool's own rule, not
  the declaration's preference.

```java
// Declaration.java — the declaration given to a build tool: source directory name and dependency scope labels
import java.util.*;

public class Declaration {
    record Dependency(String name, String scope) {}

    final String sourceName;
    final List<Dependency> dependencies;

    Declaration(String sourceName, List<Dependency> dependencies) {
        this.sourceName = sourceName;
        this.dependencies = dependencies;
    }

    int declaredItemCount() {
        return 1 + dependencies.size();
    }
}
```

```java
// Phases.java — the phase sequence derived from the declaration: order does not look at the declaration's writing order
import java.util.*;

public class Phases {
    static List<String> derive(Declaration d) {
        List<String> sequence = new ArrayList<>();
        sequence.add("clean");
        sequence.add("compile sources");
        sequence.add("build compile class path");
        sequence.add("build runtime class path");
        return sequence;
    }
}
```

`Phases.derive` produces four phases without ever looking at the declaration's content:
cleaning, compiling sources, building the compile class path, building the runtime class path.
Whether the declaration's dependency count is one, two, or ten, the length of the returned
sequence does not change — because what is measured is not the dependencies themselves, it is
what the tool derives from them.

#### Two Numbers, Two Class Paths from the Same Declaration

The measurement starts with two separate declarations: the same two dependencies, one written
in forward order, the other in reverse order. `Tag` is marked as required only at compile time,
`Service` as required only at run time. `Tag` is an annotation type kept at the source level by
its retention policy: the compiler uses it in type checking, but it leaves no trace at all in
the class file. Access to `Service` is built the exact opposite way: `Program` never names it
directly at all, it only looks it up by a name string — which is why it never needs to be on the
compile class path at all.

- **BT4.** Tag is an annotation type kept at the source level by its retention policy; the
  compiler uses it in type checking and leaves no reference to it in the class file.
- **BT5.** Access to Service is built not through a direct type reference but through a call
  that looks it up by name; this is why it never enters the compile class path at all.
- **BT6.** The compile class path and the runtime class path are built as two separate directory
  lists; there is no directory shared between them at all — Tag's directory is given only at
  compile time, Service's directory only at run time.

```java
// ToolModel.java — the phase sequence derived from a declaration is independent of declaration order, and one declaration yields two class paths
import java.io.*;
import java.lang.classfile.*;
import java.lang.classfile.constantpool.*;
import java.net.*;
import java.nio.file.*;
import java.util.*;
import java.util.spi.ToolProvider;

public class ToolModel {

    static Path write(Path dir, String name, String source) throws IOException {
        Files.createDirectories(dir);
        Path p = dir.resolve(name + ".java");
        Files.writeString(p, source);
        return p;
    }

    static boolean compile(Path output, List<Path> classPath, Path source) throws IOException {
        Files.createDirectories(output);
        List<String> arg = new ArrayList<>(List.of("-d", output.toString()));
        if (!classPath.isEmpty()) {
            StringJoiner sj = new StringJoiner(File.pathSeparator);
            for (Path p : classPath) sj.add(p.toString());
            arg.add("-classpath");
            arg.add(sj.toString());
        }
        arg.add(source.toString());
        ToolProvider compiler = ToolProvider.findFirst("javac").orElseThrow();
        StringWriter silent = new StringWriter();
        int result = compiler.run(new PrintWriter(silent), new PrintWriter(silent), arg.toArray(new String[0]));
        return result == 0;
    }

    static boolean constantPoolContainsName(Path classFile, String name) throws IOException {
        ClassModel model = ClassFile.of().parse(classFile);
        for (PoolEntry e : model.constantPool()) {
            if (e instanceof ClassEntry ce && ce.asInternalName().equals(name)) return true;
        }
        return false;
    }

    public static void main(String[] args) throws Exception {
        var tag = new Declaration.Dependency("Tag", "compile");
        var service = new Declaration.Dependency("Service", "runtime");

        Declaration forward = new Declaration("source", List.of(tag, service));
        Declaration reverse = new Declaration("source", List.of(service, tag));

        List<String> phasesForward = Phases.derive(forward);
        List<String> phasesReverse = Phases.derive(reverse);

        System.out.println("-- phase sequence, independent of declaration order --");
        System.out.println("phases derived from the forward declaration: " + phasesForward);
        System.out.println("phases derived from the reverse declaration: " + phasesReverse);
        System.out.println("are the two phase sequences the same        : " + phasesForward.equals(phasesReverse));
        System.out.println("declared item count                         : " + forward.declaredItemCount());
        System.out.println("derived phase count                         : " + phasesForward.size());

        Path root = Files.createTempDirectory("tool");
        Path tagD = root.resolve("tag"), serviceD = root.resolve("service"), programD = root.resolve("program");

        Path sTag = write(root.resolve("source"), "Tag",
                "import java.lang.annotation.*;\n"
              + "@Retention(RetentionPolicy.SOURCE)\n"
              + "public @interface Tag { String value(); }");
        Path sService = write(root.resolve("source"), "Service",
                "public class Service { public static String run() { return \"service ran\"; } }");
        Path sProgram = write(root.resolve("source"), "Program",
                "@Tag(value = \"example\")\n"
              + "public class Program {\n"
              + "  public static void main(String[] a) throws Exception {\n"
              + "    Class<?> s = Class.forName(\"Service\");\n"
              + "    String r = (String) s.getMethod(\"run\").invoke(null);\n"
              + "    System.out.println(\"service (reflection): \" + r);\n"
              + "  }\n"
              + "}");

        compile(tagD, List.of(), sTag);
        compile(serviceD, List.of(), sService);

        List<Path> compileClassPath = List.of(tagD);
        boolean programCompiled = compile(programD, compileClassPath, sProgram);
        System.out.println();
        System.out.println("-- one declaration, two class paths --");
        System.out.println("did Program compile against the compile class path (Tag only)        : " + programCompiled);
        System.out.println("does Program.class's constant pool hold a reference to Tag            : "
                + constantPoolContainsName(programD.resolve("Program.class"), "Tag"));

        List<Path> runtimeClassPath = List.of(programD, serviceD);
        URL[] url = new URL[runtimeClassPath.size()];
        for (int i = 0; i < url.length; i++) url[i] = runtimeClassPath.get(i).toUri().toURL();
        try (URLClassLoader loader = new URLClassLoader(url, ToolModel.class.getClassLoader().getParent())) {
            Class<?> cls = Class.forName("Program", true, loader);
            System.out.println("does Program run against the runtime class path (no Tag, has Service):");
            cls.getMethod("main", String[].class).invoke(null, (Object) new String[0]);
        }
    }
}
```

```
-- phase sequence, independent of declaration order --
phases derived from the forward declaration: [clean, compile sources, build compile class path, build runtime class path]
phases derived from the reverse declaration: [clean, compile sources, build compile class path, build runtime class path]
are the two phase sequences the same        : true
declared item count                         : 3
derived phase count                         : 4

-- one declaration, two class paths --
did Program compile against the compile class path (Tag only)        : true
does Program.class's constant pool hold a reference to Tag            : false
does Program run against the runtime class path (no Tag, has Service):
service (reflection): service ran
```

The first half of the output shows that declaration order does not affect the phase sequence:
the phase sequences derived from the forward and the reverse declaration are exactly the same;
the `equals` comparison confirms it correctly. Declared item count is **3** (one source
directory, two dependencies), derived phase count is **4** — the two numbers are independent of
each other, neither determines the other. What the tool derives depends not on the
declaration's length but on its own rule.

The second half measures the actual claim. `Program` is compiled against a compile class path
holding only `Tag`, and the compile succeeds — the compiler used `Tag` in type checking but left
no trace in the class file; the name `Tag` never appears in the constant pool at all; this line
is confirmed by reading the class file, not by guessing. Then the same compiled `Program` is run
against an entirely different runtime class path: on this path, `Tag` does not exist at all, but
`Service` does. The program runs without error and finds `Service` through reflection. The set
of names the compiler saw (`Tag`, `Program`) and the set of names the runtime found (`Service`,
`Program`) do not overlap — from the same single declaration, from the same single compiled
class, two separate class paths came out.

These two results must be read together, on the same line. The constant pool check gives
negative evidence: `Tag` is not there, therefore the runtime class path has no need to contain
it — this rests directly on the class file itself, not on a guess. The runtime trial gives
positive evidence: `Service` really is found on the runtime class path and really is called. The
two pieces of evidence together show that the compile class path and the runtime class path are
two separate sets coming out of the same declaration, and that neither is required to be a
subset or a superset of the other.

#### Limiting Measurement: Order Is a Rule, Not a Preference

The first line of the measurement above sets a limit: the phase order turning out independent
of declaration order is not an observation, it is a consequence of the tool's model.
`Phases.derive` never looks at the declaration's `dependencies` list at all; it only knows that
the declaration exists. This is why the phase sequence does not change even when the same
dependency set is given with its writing order reversed — if order were a preference, two
declarations would have produced two separate sequences.

This distinction matters, because the relationship between a declaration and a phase sequence
is not a mapping, it is a rule application. Deleting or adding a line in the declaration does
not change the derived phases' **content**, it only changes the scope of the compile tasks
**inside** those phases — for example, the phase that builds the compile class path now
contains one more dependency. The phase order itself stays fixed.

The same rule is also independent of the declaration's length. The declaration in the
measurement carried two dependencies; if the declaration had carried five, ten, or none at all,
`Phases.derive` would still return four phases, because the method's body never reads the
dependency list at all. This shows that the tool is not an **interpreter**, it is a **rule
applier**: no matter how much the input changes, the phase set stays the same, and the only thing
that changes is how much data the compile task running inside each phase works with. The six
types measured in the next lesson will also pass **through** this same fixed phase set — both
the "build compile class path" and "build runtime class path" phases will see the same six
dependencies, but each will select a different subset from among them.

The next lesson deepens this distinction: a dependency's scope determines not only the list
inside that one phase but also the content of the two class paths — and that determination will
give the full measure of the two separate sets (one required at compile time, one at run time)
seen in this lesson.

#### Summary

- A build tool derives a fixed phase sequence from a declaration; the order is independent of
  the declaration's writing order and is the tool's own rule.
- Declared item count (the number of lines in the declaration) and derived phase count are
  separate concepts; neither determines the other — here 3 declared items yield 4 phases.
- Two separate class paths come out of the same declaration: the compile class path and the
  runtime class path; which one a type is required on depends on how the type is used in the
  source (a direct reference or a lookup by name).
- An annotation type kept at the source level by its retention policy (`Tag`) is an example of
  a dependency the compiler sees but the class file never carries at all; this is confirmed
  directly by reading the constant pool.

#### Next Step

This lesson showed, with two examples — one at compile time, one at run time — which class path
a dependency is required on. But when six separate dependency types, including a direct
dependency and a transitive dependency, a single interface and two of its implementations, are
all present together, the picture becomes far more complicated. The next lesson measures
dependency scope itself: of six types, how many are required only at compile time, how many
only at run time, how many at both — and of the four defects that appear once the two class
paths diverge, how many make no sound at all.

### Dependency Management

Source: https://academia.sh/en/courses/java-ecosystem/dependency-management

The same source is first compiled against a compile class path and then run against a separate runtime class path: of six types, 1 turns out required only at compile time, 3 only at run time, 2 at both. Of four defects, 1 is a compile error, 1 is a runtime error, and 2 silently produce a wrong result without saying anything — one a choice tied to the order of two implementations of the same interface, the other the price of changing a constant without recompiling the caller.

The previous lesson showed which class path a dependency is required on with two extreme
examples: one required only at compile time, one only at run time. But a real declaration
usually also carries dependencies sitting in between the two — required at both compile time
and run time, or never appearing at compile time and leaking into the run only through another
dependency. This lesson puts six dependency types side by side and asks all of them the same
question: which class path must this type be present on? The answer cannot be extracted from
the source text — if it could, there would be no need for a concept called dependency scope at
all.

#### Dependency Scope: Three Values, One Question

A dependency's **dependency scope** takes one of three values: required only on the compile
class path, required only on the runtime class path, or required on both. The scope is not a
guess, it is a decision written in the declaration — but whether that decision is correct can
only be tested by actually separating the two paths. The six types below give six different
answers to the same question, and none of them can be extracted from a surface reading of the
source text:

- `Constant` — a class carrying a compile-time constant.
- `Tool` — a helper class that calls `Deep` directly, not `Constant`.
- `Deep` — a class only `Tool` calls, never named directly by `User`.
- `Facade` — a single-method interface.
- `ProviderA`, `ProviderB` — two separate implementations of `Facade`; they are present through
  a service declaration without ever being named in the source text.

- **BT7.** The class path requirement of the six types is measured by first compiling the same
  source against a compile class path and then running it against a separate runtime class
  path; a trial done with only one path does not show this distinction.
- **BT8.** `Constant` being a compile-time constant (`static final int`) comes from the same
  source as the value-embedding mechanism established in the Object-Oriented Java course; it is
  not re-established here, it is only used as a tool for the class path result.
- **BT9.** `Facade` is an interface; `ProviderA` and `ProviderB` are bound to it through a
  service declaration (under `META-INF/services`). `Caller` never writes any implementation
  name in its source text at all, it only looks for `Facade`.

```java
// ClassPath.java — a helper that counts which class path a type is required on and where a divergence is caught
import java.io.*;
import java.net.*;
import java.nio.file.*;
import java.util.*;
import java.util.spi.ToolProvider;

public class ClassPath {
    static int compile = 0, runtime = 0, both = 0;
    static int compileError = 0, runtimeError = 0, silent = 0;

    static Path write(Path dir, String name, String source) throws IOException {
        Files.createDirectories(dir);
        Path p = dir.resolve(name + ".java");
        Files.writeString(p, source);
        return p;
    }

    static boolean compile(Path output, List<Path> classPath, Path source) throws IOException {
        Files.createDirectories(output);
        List<String> arg = new ArrayList<>(List.of("-d", output.toString()));
        if (!classPath.isEmpty()) {
            StringJoiner sj = new StringJoiner(File.pathSeparator);
            for (Path p : classPath) sj.add(p.toString());
            arg.add("-classpath");
            arg.add(sj.toString());
        }
        arg.add(source.toString());
        ToolProvider compiler = ToolProvider.findFirst("javac").orElseThrow();
        StringWriter silentOutput = new StringWriter();
        int result = compiler.run(new PrintWriter(silentOutput), new PrintWriter(silentOutput), arg.toArray(new String[0]));
        return result == 0;
    }

    static String run(List<Path> classPath, String mainClass) throws Exception {
        URL[] url = new URL[classPath.size()];
        for (int i = 0; i < url.length; i++) url[i] = classPath.get(i).toUri().toURL();
        try (URLClassLoader loader = new URLClassLoader(url, ClassPath.class.getClassLoader().getParent())) {
            Class<?> cls = Class.forName(mainClass, true, loader);
            cls.getMethod("main", String[].class).invoke(null, (Object) new String[0]);
            return "ok";
        } catch (java.lang.reflect.InvocationTargetException e) {
            return e.getCause().getClass().getSimpleName();
        } catch (Throwable t) {
            return t.getClass().getSimpleName();
        }
    }

    static void declareProvider(Path dir, String provider) throws IOException {
        Path d = dir.resolve("META-INF").resolve("services");
        Files.createDirectories(d);
        Files.writeString(d.resolve("Facade"), provider + "\n");
    }

    static String selectedProvider(List<Path> classPath) throws Exception {
        URL[] url = new URL[classPath.size()];
        for (int i = 0; i < url.length; i++) url[i] = classPath.get(i).toUri().toURL();
        try (URLClassLoader loader = new URLClassLoader(url, ClassPath.class.getClassLoader().getParent())) {
            Class<?> cls = Class.forName("Caller", true, loader);
            cls.getMethod("main", String[].class).invoke(null, (Object) new String[0]);
            return (String) cls.getField("selected").get(null);
        }
    }

    static int readField(List<Path> classPath, String mainClass, String field) throws Exception {
        URL[] url = new URL[classPath.size()];
        for (int i = 0; i < url.length; i++) url[i] = classPath.get(i).toUri().toURL();
        try (URLClassLoader loader = new URLClassLoader(url, ClassPath.class.getClassLoader().getParent())) {
            Class<?> cls = Class.forName(mainClass, true, loader);
            cls.getMethod("main", String[].class).invoke(null, (Object) new String[0]);
            return cls.getField(field).getInt(null);
        }
    }

    static void classify(String name, boolean compileRequired, boolean runtimeRequired) {
        String where;
        if (compileRequired && runtimeRequired) { both++; where = "both"; }
        else if (compileRequired) { compile++; where = "compile"; }
        else { runtime++; where = "runtime"; }
        System.out.printf("%-32s %s%n", name, where);
    }

    static void defect(String name, String kind) {
        if (kind.equals("compile")) compileError++;
        else if (kind.equals("runtime")) runtimeError++;
        else silent++;
        System.out.printf("%-32s %s%n", name, kind);
    }

    static void summary() {
        System.out.printf("type   : %d compile only, %d runtime only, %d both%n", compile, runtime, both);
        System.out.printf("defect : %d compile error, %d runtime error, %d silent%n", compileError, runtimeError, silent);
    }
}
```

`ClassPath` gathers three separate measurements under one roof: which class path a type is
required on (`classify`), which stage a defect falls at (`defect`), and a total of both
(`summary`). `compile` and `run` do the same operations as in the previous lesson;
`declareProvider` writes a service declaration to disk, and `selectedProvider` runs `Caller` and
reads which implementation was selected.

#### Six Types, One Measurement

- **BT10.** The transitive dependency (`Deep`) is measured never entering the compile class
  path at all, even though the direct dependency (`Tool`) is present on it; the source
  compiling is not proof of this.
- **BT11.** Conflict resolution is measured by putting two implementations of the same
  interface (`ProviderA`, `ProviderB`) on the runtime class path at the same time and swapping
  their order; which order they are given in is the only variable.

```java
// DependencyMeasurement.java — measures which class path six types are required on and where four defects appear
import java.nio.file.*;
import java.util.*;

public class DependencyMeasurement {
    public static void main(String[] args) throws Exception {
        Path root = Files.createTempDirectory("dependency");
        Path source = root.resolve("source");
        Path constantD = root.resolve("constant"), toolD = root.resolve("tool"), deepD = root.resolve("deep");
        Path facadeD = root.resolve("facade"), aD = root.resolve("providerA"), bD = root.resolve("providerB");
        Path userD = root.resolve("user"), callerD = root.resolve("caller");

        Path sConstant = ClassPath.write(source, "Constant", "public class Constant { public static final int VALUE = 7; }");
        Path sDeep = ClassPath.write(source, "Deep", "public class Deep { public static int three() { return 3; } }");
        Path sTool = ClassPath.write(source, "Tool", "public class Tool { public static int two() { return Deep.three() - 1; } }");
        Path sUser = ClassPath.write(source, "User",
                "public class User { public static void main(String[] a) { int t = Constant.VALUE + Tool.two(); if (t < 0) throw new IllegalStateException(); } }");
        Path sFacade = ClassPath.write(source, "Facade", "public interface Facade { String name(); }");
        Path sA = ClassPath.write(source, "ProviderA", "public class ProviderA implements Facade { public String name() { return \"A\"; } }");
        Path sB = ClassPath.write(source, "ProviderB", "public class ProviderB implements Facade { public String name() { return \"B\"; } }");
        Path sCaller = ClassPath.write(source, "Caller",
                "import java.util.ServiceLoader;\n"
              + "public class Caller {\n"
              + "  public static String selected = \"none\";\n"
              + "  public static void main(String[] a) {\n"
              + "    for (Facade f : ServiceLoader.load(Facade.class, Caller.class.getClassLoader())) { selected = f.name(); return; }\n"
              + "  }\n"
              + "}");

        ClassPath.compile(constantD, List.of(), sConstant);
        ClassPath.compile(deepD, List.of(), sDeep);
        ClassPath.compile(toolD, List.of(deepD), sTool);
        ClassPath.compile(facadeD, List.of(), sFacade);
        ClassPath.compile(aD, List.of(facadeD), sA);
        ClassPath.compile(bD, List.of(facadeD), sB);
        ClassPath.declareProvider(aD, "ProviderA");
        ClassPath.declareProvider(bD, "ProviderB");
        ClassPath.compile(userD, List.of(constantD, toolD), sUser);
        ClassPath.compile(callerD, List.of(facadeD), sCaller);

        System.out.println("-- six types, which class path each is required on --");

        boolean constantCompile = !ClassPath.compile(root.resolve("d1"), List.of(toolD), sUser);
        boolean constantRuntime = !ClassPath.run(List.of(userD, toolD, deepD), "User").equals("ok");
        ClassPath.classify("Constant (compile-time constant)", constantCompile, constantRuntime);

        boolean toolCompile = !ClassPath.compile(root.resolve("d2"), List.of(constantD), sUser);
        boolean toolRuntime = !ClassPath.run(List.of(userD, constantD, deepD), "User").equals("ok");
        ClassPath.classify("Tool (direct dependency)", toolCompile, toolRuntime);

        boolean deepCompile = !ClassPath.compile(root.resolve("d3"), List.of(constantD, toolD), sUser);
        boolean deepRuntime = !ClassPath.run(List.of(userD, constantD, toolD), "User").equals("ok");
        ClassPath.classify("Deep (transitive dependency)", deepCompile, deepRuntime);

        boolean facadeCompile = !ClassPath.compile(root.resolve("d4"), List.of(), sCaller);
        boolean facadeRuntime = !ClassPath.run(List.of(callerD, aD), "Caller").equals("ok");
        ClassPath.classify("Facade (interface)", facadeCompile, facadeRuntime);

        boolean aCompile = !ClassPath.compile(root.resolve("d5"), List.of(facadeD), sCaller);
        ClassPath.classify("ProviderA (implementation)", aCompile, true);
        ClassPath.classify("ProviderB (second implementation)", aCompile, true);

        System.out.println();
        System.out.println("-- transitive dependency: Deep never enters the compile class path via Tool --");
        System.out.println("did User compile against the [Constant, Tool] compile class path: " + ClassPath.compile(root.resolve("transit"), List.of(constantD, toolD), sUser));
        System.out.println("Deep is not on this compile class path at all, yet User compiles");

        System.out.println();
        System.out.println("-- conflict resolution: two implementations on the runtime path, order decides the choice --");
        String firstAB = ClassPath.selectedProvider(List.of(callerD, facadeD, aD, bD));
        String firstBA = ClassPath.selectedProvider(List.of(callerD, facadeD, bD, aD));
        System.out.println("selected when A is given first: " + firstAB);
        System.out.println("selected when B is given first: " + firstBA);

        System.out.println();
        System.out.println("-- where the defect appears once the paths diverge --");
        ClassPath.defect("type absent on the compile path", ClassPath.compile(root.resolve("d6"), List.of(constantD), sUser) ? "silent" : "compile");
        ClassPath.defect("type absent on the runtime path", ClassPath.run(List.of(userD, constantD), "User").equals("ok") ? "silent" : "runtime");
        ClassPath.defect("two implementations on the runtime path", firstAB.equals(firstBA) ? "runtime" : "silent");

        Path constantNew = root.resolve("constantNew");
        ClassPath.compile(constantNew, List.of(), ClassPath.write(root.resolve("source2"), "Constant",
                "public class Constant { public static final int VALUE = 8; }"));
        Path measured = root.resolve("measured");
        Path sMeasured = ClassPath.write(source, "Measured",
                "public class Measured { public static int seen = -1;\n"
              + "  public static void main(String[] a) { seen = Constant.VALUE; } }");
        ClassPath.compile(measured, List.of(constantD), sMeasured);
        int oldValue = ClassPath.readField(List.of(measured, constantNew), "Measured", "seen");
        ClassPath.defect("constant changed, caller not recompiled", oldValue == 8 ? "runtime" : "silent");
        System.out.println("constant on the new class path : 8");
        System.out.println("value seen by the caller        : " + oldValue);

        System.out.println();
        ClassPath.summary();
    }
}
```

```
-- six types, which class path each is required on --
Constant (compile-time constant) compile
Tool (direct dependency)         both
Deep (transitive dependency)     runtime
Facade (interface)               both
ProviderA (implementation)       runtime
ProviderB (second implementation) runtime

-- transitive dependency: Deep never enters the compile class path via Tool --
did User compile against the [Constant, Tool] compile class path: true
Deep is not on this compile class path at all, yet User compiles

-- conflict resolution: two implementations on the runtime path, order decides the choice --
selected when A is given first: A
selected when B is given first: B

-- where the defect appears once the paths diverge --
type absent on the compile path  compile
type absent on the runtime path  runtime
two implementations on the runtime path silent
constant changed, caller not recompiled silent
constant on the new class path : 8
value seen by the caller        : 7

type   : 1 compile only, 3 runtime only, 2 both
defect : 1 compile error, 1 runtime error, 2 silent
```

The first four of the six lines were obtained by compiling and running each type one at a
time; the runtime column of the last two lines is given without a separate measurement. The
first three lines run `User`, the next three run `Caller` source, first trying to compile then
run it against a class path that lacks that type. If compiling fails, the type is required at
compile time; if running fails, it is required at run time. `Constant` turns out required only
at compile time — its value is embedded at the call site, so it is never looked for on the
runtime class path at all. `Tool` and `Facade` are required on both paths: both are named
directly in the source, both have their class files looked for at both the compile and the
runtime stage. `Deep`, `ProviderA`, and `ProviderB` are required only at run time — none of the
three is ever written directly anywhere in the source text.

Result: **of the six types, one is required only on the compile class path, three only on the
runtime class path, two on both.** This number is the shared measurement of this topic and
stays unchanged throughout it.

#### Transitive Dependency and Conflict Resolution

`Deep`'s situation is the most misleading of all. `Tool` calls `Deep` directly — but `User`,
when it calls `Tool`, never names `Deep` at all. When the compile class path contains only
`Constant` and `Tool`, `User` compiles without trouble; `Deep` is not on that path at all. This
is direct proof that a **transitive dependency** can be required at run time without ever
appearing on the compile class path — a compile succeeding does not show that the runtime class
path is complete.

The same source text also brings out a separate question: when two implementations of `Facade`
(`ProviderA`, `ProviderB`) are both present on the runtime class path at the same time, which
one is selected? `Caller` scans the service declaration and takes the first one it finds; which
one is found first depends on the **order** they were given to the class path in. When `A` is
given first, `A` is selected; when `B` is given first, `B` is selected — **conflict
resolution** here is not a rule, it is the result of an ordering. The code never throws an error
anywhere; two different setups produce two different answers from the same program.

#### Limiting Measurement: The Two Faces of a Silent Defect

- **BT12.** The "silent" defect class gathers cases where neither the compiler nor the runtime
  says anything, but the result comes out different from what was expected; the expected value
  is also written down on every silent line.
- **BT13.** When `Constant` is changed and the version on the runtime class path is changed
  without `User` being recompiled, the difference between the two sides is measured only by
  the value of the field being read; no error is thrown.

The same measurement sorts four separate defects into three classes: the absence of a type on
the compile class path is rejected by the compiler (`compile`), the absence of a type on the
runtime class path throws an error at run time (`runtime`), and the remaining two say nothing at
all (`silent`). Two implementations being found on the runtime path is one of these — the
program produces two different answers without ever throwing an error, and which answer is
"correct" cannot be told from the code itself.

The second silent defect is more subtle: `Constant`'s value is raised to **8**, but `User` is
not recompiled against this new version. Even though the `Constant` on the runtime class path
now carries **8**, the value already embedded in `User`'s class file stays at **7** — because
the embedding happened at compile time, not at run time. The class path looks updated, but the
compiled class still carries the old value; this line falls into the silent class because
neither compiling nor running produces a complaint — the result comes out wrong, nothing more.

In total: **of four defects, one is a compile error, one is a runtime error, two are silent.**
"It compiled" is therefore not proof — a transitive dependency can be entirely absent from the
compile class path while the source compiles without trouble, yet still be required at run
time; a compile succeeding does not show that the runtime class path is complete. A type not
being required on the runtime class path does not mean it is not depended on either — the
absence of `Constant` means it was **copied**, and that copy silently produces a wrong result
once the two sides diverge.

#### Summary

- Dependency scope takes three values (compile, runtime, both); measured across six types, the
  distribution comes out **1 compile only, 3 runtime only, 2 both**, and it cannot be extracted
  from the source text.
- A transitive dependency (`Deep`) compiles without trouble while never appearing on the
  compile class path at all, but is required at run time; "it compiled" is not proof.
- Conflict resolution becomes order-dependent once two implementations of the same interface
  are both present on the runtime class path; there is no error, the result changes from setup
  to setup.
- Of four defects, one is a compile error, one is a runtime error, two are silent: in silent
  defects neither the compiler nor the runtime says anything, and the result comes out wrong,
  nothing more.
- A type not being required on the runtime class path does not mean it is not depended on: its
  value may have been embedded, and that embedded value silently goes stale once the source
  changes.

#### Next Step

This lesson measured six standalone types — each stayed within the boundary of a single class
or interface. But a real project is made not of single-file classes, it is made of interdependent
**modules**, and the question of what a module's boundary corresponds to at compile time has not
yet been asked. The next lesson measures that: when a type a module considers "internal" is
reached across the boundary, is it caught at compile time, at run time, or not caught at all —
and is the source of that catch really a directory separation.

### Multi-Module Projects

Source: https://academia.sh/en/courses/java-ecosystem/multi-module-projects

Of three boundary setups, one is caught at compile time, one at run time, one crosses the boundary silently without ever being caught. The limiting measurement shows why: Public and Internal are compiled into the same directory, and what actually builds the boundary is not the directory but a loader's allow list — of two class files in the same directory, one can be loaded while the other is rejected with a ClassNotFoundException.

The previous lesson measured which class path each of six standalone types is required on —
but every type stood on its own, staying within the boundary of a single class or interface. A
real project is not built from single-file classes, it is built from interdependent
**modules**, and the line between the surface a module considers "open to others" and the
surface it keeps "to itself" is not a language rule, it is a compile decision. This lesson
measures the compile-time counterpart of that line: what happens when a module boundary is
crossed, and where does that catch come from?

The **module boundary** here is built in its smallest possible form for a multi-module project:
two sides, one "providing," one "consuming." The providing side has a public surface (`Public`)
and an internal implementation (`Internal`) within itself; the consuming side should only use
the public surface and never touch the internal implementation at all. In a real multi-module
project this separation usually shows up as separate directories, separate compile outputs —
but the compile itself never says **why** it enforces this separation. This lesson extracts
that reason by measuring it.

#### The Compile-Time Counterpart of a Module Boundary

A module's public surface and its internal implementation can be compiled from the same source
directory, with the same compile call, into the same output directory — Java itself does not
separate them. The `Public` and `Internal` classes below are exactly like this: both are
written, in a single compile call, into a single output directory (`allOutput`). `Public`
represents the side considered open to the outside, `Internal` represents the side that should
stay only within the module itself — but so far this separation is only a naming decision, the
compile itself is not yet enforcing anything at all.

- **BT14.** All three boundary setups are measured through the same internal type
  (`Internal.secret`); what differs is only the access style (direct reference / reflection)
  and which directory is put on the runtime class path.
- **BT15.** `Public` and `Internal` are compiled from the same source directory, with the same
  compile call, into the same output directory; there is no physical separation between them
  at all.

The step that makes the boundary "real" is copying a subset of the output into a separate
directory: only `Public.class` is copied into a chosen directory named `publicOnly`. The class
path given to a module's consumer points at this chosen directory, not at the full output where
`Internal` also lives.

```java
// ModuleHelper.java — shared operations for compiling a source and running it against a class path list
import java.io.*;
import java.net.*;
import java.nio.file.*;
import java.util.*;
import java.util.spi.ToolProvider;

public class ModuleHelper {
    static Path write(Path dir, String name, String source) throws IOException {
        Files.createDirectories(dir);
        Path p = dir.resolve(name + ".java");
        Files.writeString(p, source);
        return p;
    }

    static boolean compile(Path output, List<Path> classPath, Path... sources) throws IOException {
        Files.createDirectories(output);
        List<String> arg = new ArrayList<>(List.of("-d", output.toString()));
        if (!classPath.isEmpty()) {
            StringJoiner sj = new StringJoiner(File.pathSeparator);
            for (Path p : classPath) sj.add(p.toString());
            arg.add("-classpath");
            arg.add(sj.toString());
        }
        for (Path s : sources) arg.add(s.toString());
        ToolProvider compiler = ToolProvider.findFirst("javac").orElseThrow();
        StringWriter silent = new StringWriter();
        int result = compiler.run(new PrintWriter(silent), new PrintWriter(silent), arg.toArray(new String[0]));
        return result == 0;
    }

    static String run(List<Path> classPath, String mainClass) throws Exception {
        URL[] url = new URL[classPath.size()];
        for (int i = 0; i < url.length; i++) url[i] = classPath.get(i).toUri().toURL();
        try (URLClassLoader loader = new URLClassLoader(url, ModuleHelper.class.getClassLoader().getParent())) {
            Class<?> cls = Class.forName(mainClass, true, loader);
            cls.getMethod("main", String[].class).invoke(null, (Object) new String[0]);
            return "ok";
        } catch (java.lang.reflect.InvocationTargetException e) {
            return e.getCause().getClass().getSimpleName();
        } catch (Throwable t) {
            return t.getClass().getSimpleName();
        }
    }
}
```

`compile` does the same operation as in the previous lessons: it compiles sources against a
given class path. `run` runs the compiled class against a separate class path and returns the
name of the exception class thrown — if it is not "ok," that means the boundary was caught at
run time.

Outside these two methods, `ModuleHelper` knows nothing at all — it recognizes neither the
concept of "public" nor "internal," nor what a module is. The boundary itself lives outside
this class, in the decision the calling code makes about which directories go onto which class
path. This foreshadows this lesson's core observation: a module boundary is not a language
feature, it is a usage decision.

#### Three Boundary Setups

`Internal.secret` represents a module's internal implementation: calling it from the outside is
"crossing the boundary." The three setups below try this crossing in three different ways; all
of them use the same `Internal` source; the first two use the `publicOnly` class path trap, the
third uses `allOutput` instead — but one accesses it by writing a type name in the source text,
two by giving a string.

- **BT16.** The "not caught" result is not just a reference compared against setup 1's compile
  failure; in setup 3 the program runs without error and the internal type's value is actually
  read.
- **BT17.** The restricted loader (`RestrictedLoader`) uses the same principle as the custom
  loader mechanism built in the Class Loading lesson of the JVM, Concurrency and Performance
  course; it only adds an allow list to its own declaration.
- **BT18.** The allow list looks at the class name, not the directory; of two class files in
  the same directory, one can be loaded while the other is rejected with a
  `ClassNotFoundException`.
- **BT19.** The `compute()` method never mentions the `Internal` type at all in the restricted
  loader's measurement; this is why `Public` loading and running is measured independently of
  whether `Internal` is reachable or not.

```java
// RestrictedLoader.java — a loader that, from class files in the same directory, only defines allowed names
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Set;

class RestrictedLoader extends ClassLoader {
    private final Path dir;
    private final Set<String> allowed;

    RestrictedLoader(Path dir, Set<String> allowed) {
        super(RestrictedLoader.class.getClassLoader().getParent());
        this.dir = dir;
        this.allowed = allowed;
    }

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        if (!allowed.contains(name)) throw new ClassNotFoundException(name);
        try {
            byte[] bytes = Files.readAllBytes(dir.resolve(name + ".class"));
            return defineClass(name, bytes, 0, bytes.length);
        } catch (IOException e) {
            throw new ClassNotFoundException(name);
        }
    }
}
```

```java
// ModuleBoundary.java — the compile-time counterpart of a module boundary: the boundary is a class path and loader decision
import java.io.*;
import java.nio.file.*;
import java.util.*;

public class ModuleBoundary {
    public static void main(String[] args) throws Exception {
        Path root = Files.createTempDirectory("module");
        Path source = root.resolve("source");

        Path sInternal = ModuleHelper.write(source, "Internal",
                "public class Internal { public static int secret() { return 99; } }");
        Path sPublic = ModuleHelper.write(source, "Public",
                "public class Public { public static int compute() { return 41; } }");

        Path allOutput = root.resolve("allOutput");
        ModuleHelper.compile(allOutput, List.of(), sInternal, sPublic);

        Path publicOnly = root.resolve("publicOnly");
        Files.createDirectories(publicOnly);
        Files.copy(allOutput.resolve("Public.class"), publicOnly.resolve("Public.class"));

        System.out.println("-- three boundary setups, how many are caught at compile time, how many at run time --");

        Path sB1 = ModuleHelper.write(root.resolve("b1"), "B1",
                "public class B1 { public static void main(String[] a) { System.out.println(Internal.secret()); } }");
        boolean b1Compiled = ModuleHelper.compile(root.resolve("b1Output"), List.of(publicOnly), sB1);
        System.out.println("setup 1 - direct reference, internal dir not on the class path: compiled = " + b1Compiled
                + " -> " + (b1Compiled ? "not caught" : "caught at compile time"));

        Path sB2 = ModuleHelper.write(root.resolve("b2"), "B2",
                "public class B2 { public static void main(String[] a) throws Exception {\n"
              + "  Class<?> c = Class.forName(\"Internal\");\n"
              + "  c.getMethod(\"secret\").invoke(null); } }");
        Path b2Output = root.resolve("b2Output");
        boolean b2Compiled = ModuleHelper.compile(b2Output, List.of(publicOnly), sB2);
        System.out.println("setup 2 - access via reflection, internal dir not on the class path: compiled = " + b2Compiled);
        String setup2Run = ModuleHelper.run(List.of(b2Output, publicOnly), "B2");
        System.out.println("setup 2 run result: " + setup2Run
                + " -> " + (setup2Run.equals("ok") ? "not caught" : "caught at run time"));

        String setup3Run = ModuleHelper.run(List.of(b2Output, allOutput), "B2");
        System.out.println("setup 3 - same reflective class, the full internal dir added to the runtime class path: result = "
                + setup3Run + " -> " + (setup3Run.equals("ok") ? "not caught at all, boundary silently crossed" : "caught"));

        System.out.println();
        System.out.println("-- limiting measurement: the boundary is not a directory separation --");
        System.out.println("Public and Internal are in the same directory (allOutput): " + Files.exists(allOutput.resolve("Public.class"))
                + ", " + Files.exists(allOutput.resolve("Internal.class")));
        RestrictedLoader restricted = new RestrictedLoader(allOutput, Set.of("Public"));
        Class<?> publicClass = Class.forName("Public", true, restricted);
        System.out.println("did the restricted loader load Public: true, compute() = "
                + publicClass.getMethod("compute").invoke(null));
        String internalResult;
        try {
            Class.forName("Internal", true, restricted);
            internalResult = "loaded";
        } catch (ClassNotFoundException e) {
            internalResult = "ClassNotFoundException";
        }
        System.out.println("could the same restricted loader load Internal (despite being in the same dir): " + internalResult);
    }
}
```

```
-- three boundary setups, how many are caught at compile time, how many at run time --
setup 1 - direct reference, internal dir not on the class path: compiled = false -> caught at compile time
setup 2 - access via reflection, internal dir not on the class path: compiled = true
setup 2 run result: ClassNotFoundException -> caught at run time
setup 3 - same reflective class, the full internal dir added to the runtime class path: result = ok -> not caught at all, boundary silently crossed

-- limiting measurement: the boundary is not a directory separation --
Public and Internal are in the same directory (allOutput): true, true
did the restricted loader load Public: true, compute() = 41
could the same restricted loader load Internal (despite being in the same dir): ClassNotFoundException
```

The first setup measures the boundary in its strictest form: `B1` calls `Internal.secret`
through a direct type reference, and `Internal` is not on the class path at all. The compiler
cannot resolve the symbol; the compile collapses — the boundary is caught **at compile time**.
The second setup makes the same call through reflection: the name `Internal` never appears
directly anywhere in `B2`'s source, so the compile finishes without a problem. But the runtime
class path does not contain `Internal` either, and `Class.forName` throws a
`ClassNotFoundException` at run time — this time the boundary is caught **at run time**.

The third setup runs the same compiled `B2` class unchanged, only adding a different directory
to the runtime class path: `allOutput` — the module's entire internal output — instead of
`publicOnly`. This is a common mistake in a real project: someone puts the entire compile
output, not just "the public interface alone," on the runtime class path. Result: the program
runs without error, `Internal` is found and called — the boundary is **not caught at all**, it
is silently crossed.

**Of three boundary setups, one is caught at compile time, one at run time, one is never
caught at all.** What determines which catch happens is not how `B1`/`B2`'s source is written,
it is which directory was put on the runtime class path.

The order of this trio is also meaningful. The strictest catch (compile) is the earliest one to
fall — the compiler stops the moment it cannot resolve the symbol; no extra step is needed. The
middle catch (runtime) is one step more permissive: the source compiles, the program starts,
the error only falls on the line where `Internal` is actually reached — if the program never
reached that line, the error would never even appear. The loosest case is no catch at all: in
the third setup the code both compiles and runs, and calling `Internal` produces no warning at
all. How strict a module boundary is depends on which of these three setups it resembles — and
this is not a language guarantee, it is an operational decision about what gets put on the
class path.

#### Limiting Measurement: The Boundary Is Not a Directory Separation

The three setups above built the boundary by copying into separate directories — this is also
what a real build process does: carrying only the public surface into a separate output. But
relying on this method misrepresents **why** the boundary works: it makes it look as if the
boundary were a physical directory difference. The final measurement shows this is wrong.

`Public` and `Internal` still stand in the **same** directory in this measurement — `allOutput`
carries both of their class files, there is no copying at all. What builds the boundary here is
`RestrictedLoader`'s own allow list: `Set.of("Public")`. When `Public` is requested, this loader
reads it from disk and defines it — `compute()` can be called, it returns `41`. But when
`Internal` is requested from the same loader, `findClass` sees it is not on the allow list and
throws a `ClassNotFoundException`; `Internal.class` sits in the exact same directory, a few
bytes away, but the loader never even looks at it.

This confirms, in reverse, what the previous three setups measured: a module boundary is not a
**directory separation**, it is a **class path and loader decision**. Two types compiled from
the same directory can be given separate access levels by being placed on different class paths
(or given different allow lists), and this separation works just as well even with no physical
directory separation at all. The escape shown by setup 3 is the mirror image of the same fact:
directory separation is not a sufficient guarantee, because the real mechanism carrying the
boundary is always which class path (or which loader) is allowed to see which name.

Read together, these two findings show where a multi-module project's boundary is actually
placed: the boundary depends neither on which folder the source file sits in, nor on which
package name the class carries. The boundary depends on which subset of a providing module's
compile output — which directory, which archive, which allow list — is given to the consuming
module. Two modules can be produced in the same repository, with the same compile call, even
into the same output directory; the only thing required for the boundary to actually work is
what the class path given to the consuming side does and does not contain.

#### Summary

- The compile-time counterpart of a module boundary is a class path decision: "public" and
  "internal" types compiled from the same source directory are separated by giving the
  consumer different class paths.
- Of three boundary setups, one is caught at compile time (direct reference, type not on the
  class path), one is caught at run time (reflection, type not on the runtime class path), one
  is never caught (reflection, but the full internal output was mistakenly added to the
  runtime class path).
- A class loader's allow list looks at the class name, not the directory: of two class files in
  the same directory, one can be loaded and run while the other is rejected with a
  `ClassNotFoundException`.
- Limiting measurement: `Public` and `Internal` keep standing in the physically same directory;
  what builds the boundary is not the directory, it is the loader's own decision.

#### Next Step

In this lesson, a module's boundary was measured through which directory or which loader was
put on the runtime class path — we built the boundary, we made the decision. But this
declaration has not yet turned into a single running program; the runtime class path has so far
always been a list of scattered directories. The next lesson turns that list into a single
portable product: the compiled classes are placed into an archive, the entry point lives not in
the source but in the archive's own declaration, and whether the archive contains its
dependencies or not remains a separate question.

### Packaging and Executable Output

Source: https://academia.sh/en/courses/java-ecosystem/packaging-and-executable-output

The same archiving call produces both a self-sufficient archive and an archive dependent on an outside class: the first is sufficient on its own on the runtime class path, the second drops with a NoClassDefFoundError on its own and only runs once the missing directory is added. The entry point, too, lives not in the source but in the archive's own manifest: an undeclared archive produced from the same classes has no such information at all.

The previous lesson measured that the compile-time counterpart of a module boundary is a class
path and loader decision: the boundary depended neither on the source's directory structure nor
on its package name, only on which class files were given to the consuming side. But that whole
measurement still worked through scattered directories — the runtime class path was a list of
separately built, standalone directories. This lesson turns that list into a single portable
product: the compiled classes are placed into an **archive**, and the archive itself can now
travel as a single item of the runtime class path.

In the first lesson, the phase sequence derived from a declaration was limited to four phases:
clean, compile sources, build the compile class path, build the runtime class path. Packaging
is a natural extension of this sequence — a fifth step that, after the source is compiled and
the two class paths are built, compresses part of the runtime class path into a single portable
file. This step's input is always the same: a directory holding previously compiled class
files. Its output is something never seen so far: a single file that can be opened and read on
its own, and that can carry an **entry point** within itself — **executable output**.

#### Archiving and the Manifest

An archive is a single file into which the class files of a directory have been packaged; the
**archiver** (a part of the standard library, a tool provider called from inside the program)
really does this packaging. The only input given to the archiver is which directory is to be
packaged and, optionally, the name of an **entry point**; the archiver produces an archive file
and a **manifest** from these two.

- **BT20.** The archiver call packages the directory holding the class files exactly as it is;
  no dependency directory is added to this call. What is packaged is only the content of the
  given directory.

The `archive` method below runs this single compile task with three parameters: which archive
file is to be produced, which directory is to be packaged, and, optionally, which class counts
as the entry point. No information beyond these three enters the archiving call — in
particular, no dependency list, no other class path. This restriction is deliberate: the second
half of this lesson measures exactly the consequence of this restriction.

```java
// PackageHelper.java — shared operations for writing sources, compiling, and archiving
import java.io.*;
import java.net.*;
import java.nio.file.*;
import java.util.*;
import java.util.spi.ToolProvider;

public class PackageHelper {
    static Path write(Path dir, String name, String source) throws IOException {
        Files.createDirectories(dir);
        Path p = dir.resolve(name + ".java");
        Files.writeString(p, source);
        return p;
    }

    static boolean compile(Path output, List<Path> classPath, Path... sources) throws IOException {
        Files.createDirectories(output);
        List<String> arg = new ArrayList<>(List.of("-d", output.toString()));
        if (!classPath.isEmpty()) {
            StringJoiner sj = new StringJoiner(File.pathSeparator);
            for (Path p : classPath) sj.add(p.toString());
            arg.add("-classpath");
            arg.add(sj.toString());
        }
        for (Path s : sources) arg.add(s.toString());
        ToolProvider compiler = ToolProvider.findFirst("javac").orElseThrow();
        StringWriter silent = new StringWriter();
        int result = compiler.run(new PrintWriter(silent), new PrintWriter(silent), arg.toArray(new String[0]));
        return result == 0;
    }

    static void archive(Path jarPath, Path classesDir, String mainClass) throws IOException {
        Files.createDirectories(jarPath.getParent());
        List<String> arg = new ArrayList<>(List.of("--create", "--file", jarPath.toString()));
        if (mainClass != null) { arg.add("--main-class"); arg.add(mainClass); }
        arg.add("-C"); arg.add(classesDir.toString()); arg.add(".");
        ToolProvider archiver = ToolProvider.findFirst("jar").orElseThrow();
        StringWriter silent = new StringWriter();
        archiver.run(new PrintWriter(silent), new PrintWriter(silent), arg.toArray(new String[0]));
    }

    static String run(List<Path> classPath, String mainClass) throws Exception {
        URL[] url = new URL[classPath.size()];
        for (int i = 0; i < url.length; i++) url[i] = classPath.get(i).toUri().toURL();
        try (URLClassLoader loader = new URLClassLoader(url, PackageHelper.class.getClassLoader().getParent())) {
            Class<?> cls = Class.forName(mainClass, true, loader);
            cls.getMethod("main", String[].class).invoke(null, (Object) new String[0]);
            return "ok";
        } catch (java.lang.reflect.InvocationTargetException e) {
            return e.getCause().getClass().getSimpleName();
        } catch (Throwable t) {
            return t.getClass().getSimpleName();
        }
    }
}
```

`archive` does only two things: it packages the given directory, and if one is given, it writes
an entry point name into the manifest. `run` does the same operation as in the previous
lessons — with one difference: an item on the class path can now be an archive file, not just a
directory; `URLClassLoader` accepts an archive file's location the same way it accepts a
directory.

The manifest sits inside the archive as a separate record — alongside the class files, but not
one of them. A manifest can carry many fields; this lesson uses only one: Main-Class. This
field carries a class name (a string), it contains no executable code at all. The side reading
the manifest — here our own code, in a real setup a launcher — takes this string, finds the
class carrying that name inside the archive, and calls its `main` method. The entry point
itself is still an ordinary Java class; the only thing that changes is where its name is read
from.

#### The Entry Point Lives Not in the Source, but in the Manifest

An archive can carry dozens of classes, and only one of them (or none) may contain a `main`
method. The side that wants to run the archive itself — a launcher, or in this lesson our own
code — has to know which class to start from, but it does not find this information by scanning
the archive's class files one by one. Where it looks is fixed: a single field in the archive's
manifest.

- **BT21.** Entry point information is read only by looking at the manifest's Main-Class field;
  this information is never searched for in any of the source files.

```java
// ExecutableOutput.java — the archiver is really called, the entry point lives in the manifest, an archive does not include dependencies
import java.io.*;
import java.net.*;
import java.nio.file.*;
import java.util.*;
import java.util.jar.*;

public class ExecutableOutput {
    public static void main(String[] args) throws Exception {
        Path root = Files.createTempDirectory("package");
        Path source = root.resolve("source");

        Path sStandalone = PackageHelper.write(source, "Standalone",
                "public class Standalone { public static void main(String[] a) { } }");
        Path standaloneD = root.resolve("standaloneD");
        PackageHelper.compile(standaloneD, List.of(), sStandalone);

        Path jarWithEntryPoint = root.resolve("withEntryPoint.jar");
        PackageHelper.archive(jarWithEntryPoint, standaloneD, "Standalone");

        Path jarWithoutEntryPoint = root.resolve("withoutEntryPoint.jar");
        PackageHelper.archive(jarWithoutEntryPoint, standaloneD, null);

        System.out.println("-- entry point is not in the source, it is in the archive's manifest --");
        try (JarFile jf = new JarFile(jarWithEntryPoint.toFile())) {
            String main = jf.getManifest().getMainAttributes().getValue("Main-Class");
            System.out.println("entry point of the archive with a manifest    : " + main);
        }
        try (JarFile jf = new JarFile(jarWithoutEntryPoint.toFile())) {
            Manifest m = jf.getManifest();
            String main = (m == null) ? null : m.getMainAttributes().getValue("Main-Class");
            System.out.println("entry point of the archive without one        : " + main);
        }
        System.out.println("does the archive with a manifest run from itself: "
                + PackageHelper.run(List.of(jarWithEntryPoint), "Standalone"));

        System.out.println();
        System.out.println("-- limiting measurement: an archive does not include its dependencies --");

        Path sHelper = PackageHelper.write(source, "Helper",
                "public class Helper { public static String help() { return \"helper\"; } }");
        Path sDependent = PackageHelper.write(source, "Dependent",
                "public class Dependent { public static void main(String[] a) { Helper.help(); } }");
        Path dependentD = root.resolve("dependentD");
        PackageHelper.compile(dependentD, List.of(), sHelper, sDependent);

        Path dependentOnlyDir = root.resolve("dependentOnly");
        Files.createDirectories(dependentOnlyDir);
        Files.copy(dependentD.resolve("Dependent.class"), dependentOnlyDir.resolve("Dependent.class"));
        Path dependentJar = root.resolve("dependent.jar");
        PackageHelper.archive(dependentJar, dependentOnlyDir, "Dependent");

        Path helperD = root.resolve("helperD");
        Files.createDirectories(helperD);
        Files.copy(dependentD.resolve("Helper.class"), helperD.resolve("Helper.class"));

        String standaloneResult = PackageHelper.run(List.of(jarWithEntryPoint), "Standalone");
        String dependentAlone = PackageHelper.run(List.of(dependentJar), "Dependent");
        String dependentPlusHelper = PackageHelper.run(List.of(dependentJar, helperD), "Dependent");

        System.out.printf("%-48s %s%n", "Standalone archive, runtime class path only the archive:", standaloneResult);
        System.out.printf("%-48s %s%n", "Dependent archive, runtime class path only the archive:", dependentAlone);
        System.out.printf("%-48s %s%n", "Dependent archive, once Helper dir is added:", dependentPlusHelper);
    }
}
```

```
-- entry point is not in the source, it is in the archive's manifest --
entry point of the archive with a manifest    : Standalone
entry point of the archive without one        : null
does the archive with a manifest run from itself: ok

-- limiting measurement: an archive does not include its dependencies --
Standalone archive, runtime class path only the archive: ok
Dependent archive, runtime class path only the archive: NoClassDefFoundError
Dependent archive, once Helper dir is added:     ok
```

The first three lines measure where the entry point stands. Two archives are produced from the
same `Standalone` class: one with an entry point (`--main-class Standalone`), the other with no
entry point given at all. When the manifest-bearing archive's manifest is read, the Main-Class
field carries the value `Standalone`; the manifest-less archive's manifest has no such field at
all, the value read is `null`. Both archives carry the exact same class file, coming from the
exact same compile — the only difference between them is a single line in the manifest. The
source file itself carries no such thing as entry point information at all; `Standalone.java`
neither says "I am the entry point" nor carries any such marker. This information is written
only at archiving time, into the archive's own manifest — and, as the third line shows, the
archive can run on its own using this manifest.

This is one layer beyond the entry point concept established in the Java Fundamentals course.
There, the entry point's signature (the shape of a `main` method) was fixed; here the question
asked is different: in an archive holding more than one class, **which** class is the entry
point? The answer to this question no longer lives in the source, it lives in the manifest of
the archive being worked on. A manifest-less archive is also a valid archive — it carries class
files, it can be opened, its contents can be read — but the answer to "which class to start
from" is not in that archive. This gap only becomes visible when the manifest is read; the
archive itself never says so explicitly anywhere, it just stands as a field that returns `null`.

#### Limiting Measurement: An Archive Does Not Include Its Dependencies

- **BT22.** The `Standalone` archive never mentions any outside type; the `Dependent` archive
  needs the `Helper` type outside itself at call time. Both are produced with the same
  archiving call.
- **BT23.** The `Dependent` archive contains only its own class file; `Helper`'s class file is
  deliberately left out, so that the need for the rest of the runtime class path can be
  measured.
- **BT24.** The runtime error arises not from the archive itself but from the class path the
  archive was placed on alone; the same archive placed on a different class path does not
  produce the same error.
- **BT25.** A manifest-less archive is also a valid archive and carries class files; only the
  entry point information is missing, and this absence is only visible once the manifest is
  read.

The last three lines run two archives side by side. The `Standalone` archive, when placed alone
on the runtime class path, runs without trouble — because `Standalone.main` never mentions any
outside type at all, everything it needs is in its own class file. The `Dependent` archive, in
the same way, drops with a `NoClassDefFoundError` when placed alone on the runtime class path:
`Dependent.main` calls `Helper.help()`, but `Helper`'s class file is not inside this archive —
only `Dependent.class` was deliberately packaged during archiving. The same `Dependent` archive
runs without trouble once the directory holding `Helper` is also added to the runtime class
path; the archive itself did not change, only the class path placed alongside it did.

These three lines confirm a single rule: **an archive does not include its dependencies.** The
archiver call only packages the given directory; no class file outside that directory is added
automatically. Whether an archive is "self-sufficient" or not depends not on the archive's own
content but on which types the source code mentions — and this is a gap between something known
at compile time (`Dependent` required `Helper` on the compile class path) and something that
can be forgotten at packaging time (`Helper`'s class file never entered the archive at all).
A compile having succeeded is no guarantee that the archive will be self-sufficient.

This gap belongs to the same family as the silent defect measured in the Dependency Management
lesson, but in reverse form. There, a type not being required on the runtime class path meant
it had been **copied** — the constant's value was embedded at the call site and the source file
could disappear. Here, a type not entering the archive means it is still **required**, only
packaging forgot it. In both cases, the error surfaces not at the moment itself but at a later
step: there, when the constant changes; here, when the archive is placed alone on the runtime
class path. The difference is the class of the defect: `Dependent`'s missing dependency is not
silent, it drops openly with a `NoClassDefFoundError` — because the call to `Helper` is a method
call, too dynamic to be embedded at compile time, not a constant.

This also clarifies the archiving step's own responsibility: an archive should not be expected
to gather its dependencies "automatically." Which class files enter an archive, just like which
dependency enters which class path, is a decision determined by the input given to the
packaging call — and giving that decision incompletely can turn into a runtime error that never
shows up at compile time at all.

The summary of what has been measured across four lessons is completed here. A fixed phase
sequence was derived from a declaration; two of those phases built two separate class paths by
filtering every dependency in the declaration according to a scope label; the boundary between
a module's public and internal surfaces was determined by which directory entered one of these
two paths; and now one of these paths — the runtime class path — has been packaged into a
single portable file. In none of the four steps did the source text itself change; what changed
was always the declaration, always the class path.

#### Summary

- An archive is what results from the class files in a directory being packaged by the
  archiver into a single file; it can travel as a single item of the runtime class path.
- Entry point information lives not in the source file but in the Main-Class field of the
  archive's own manifest; a manifest-less archive is valid but its entry point information is
  missing.
- An archive does not include its dependencies: the archiving call only packages the given
  directory, no class file outside that directory is added automatically.
- A self-sufficient archive (one that mentions no outside type) works when placed alone on the
  runtime class path; a dependent archive drops with a runtime error under the same condition
  and only works once the missing class path is completed.
- A compile having succeeded is not a guarantee that an archive will be self-sufficient; a type
  required on the compile class path may never have entered the archive at all during
  packaging.

#### Next Step

A single question was followed across this topic's four lessons: a declaration, a dependency, a
module boundary, an archive — the same two-way class path distinction came out every time. The
runtime class path has now become a portable product, and what goes into it is a decision. The
next topic gives the same decision for a third path — the path a test sees is not the path
production sees.

## Testing

The test framework's life cycle and its assertions, where the class path the test sees diverges from the one production sees, fake dependencies taking the real type's place on the runtime class path, the same test body multiplied with data, the load setups with an external system add to the test path, and the executable counterpart of a test written in scenario language.

### Unit Test Framework

Source: https://academia.sh/en/courses/java-ecosystem/unit-test-framework

A small runner that calls setup, body and teardown calls in sequence is built by hand; the same assertion gives two different results when checked before and after teardown, and both are counted as run. When the same compiled test class runs against a class path missing the framework type, it measures that the test class path is a third, independent path.

The previous topic turned the runtime class path into a portable artifact and showed that
what goes into it is a decision. That decision was bounded there by two paths: the compile
class path and the runtime class path. This lesson makes the same decision for a third path.
While writing tests, one more class path sits at hand — the path the test sees — and this
path is not the same as the path production sees. Our question is: what does this third path
carry, and what happens when a type that never appears on the production class path is
missing?

The second half of the question is a separate topic, established in the M21/K02 Unit Testing
and Test-Driven Development course: test classification, assertion writing, test naming —
none of these are established again here. The one new question in this lesson is which class
path the types the test needs to run stand on. Before answering that question, we first need
to build by hand the framework itself, on top of which we will build a class path question —
the runner that calls setup, body and teardown in sequence.

These two questions stand closer together than they appear. A framework applies a sequence
that is never written in the source, over and over for every case; this repetition means a
series of calls that appears nowhere in a file's own source. For those calls to compile and
run, they must be written somewhere — and that place is not the class path production code
reads. Moving to the class path question without building the framework would mean measuring
an empty abstraction; this is why the lesson first writes the calls by hand, then counts which
type those calls need.

#### The Sequence the Framework Establishes

- **TE1.** Setup and teardown are each modeled here as a `Runnable`; a real framework's test
  discovery (file scanning, name matching) is not built here — only the setup, body, teardown
  order and the way a failure is reported are modeled.
- **TE2.** Assertion failure is reported by throwing an `AssertionError`; the runner catches
  this and logs the case as "failed," and otherwise writes "passed." The teardown step runs
  under every condition — whether the body fails or not.

From the outside, what a test framework does amounts to making three calls in a fixed order:
setup first, then the body, then teardown last. An assertion is a comparison and reports its
failure with an exception; the framework catches this exception and marks the case as
"failed," and if it does not catch one, it writes "passed." Setup runs again for every case —
the state one case leaves behind does not leak into the next — and teardown runs whether the
case passes or fails; otherwise one case failing would corrupt the next one's setup. The
runner below builds these three calls and the assertion by hand.

```java
// Framework.java — a small runner that calls setup, body and teardown in sequence
public class Framework {

    static void runCase(String name, Runnable setup, Runnable body, Runnable teardown) {
        setup.run();
        try {
            body.run();
            System.out.println(pad(name) + "passed");
        } catch (AssertionError error) {
            System.out.println(pad(name) + "failed: " + error.getMessage());
        } finally {
            teardown.run();
        }
    }

    static void assertEqual(String field, int expected, int actual) {
        if (expected != actual) throw new AssertionError(field + " (expected " + expected + ", got " + actual + ")");
    }

    static String pad(String s) { return String.format("%-16s", s); }

    public static void main(String[] args) {
        int[] quantity = {0};
        runCase("store-filled",
                () -> quantity[0] = 10,
                () -> assertEqual("quantity", 10, quantity[0]),
                () -> quantity[0] = 0);

        int[] field = {0};
        Runnable setup = () -> field[0] = 5;
        Runnable teardown = () -> field[0] = 0;

        runCase("before-check", setup, () -> assertEqual("field", 5, field[0]), teardown);

        setup.run();
        runCase("after-check", () -> {}, () -> { teardown.run(); assertEqual("field", 5, field[0]); }, () -> {});
    }
}
```

```
store-filled    passed
before-check    passed
after-check     failed: field (expected 5, got 0)
```

The first case is a plain pass: setup makes `quantity` 10, the body reads and confirms it,
teardown resets it to zero. The real question is in the second and third case. Both carry the
same assertion — `field` must equal 5 — but one checks the assertion **before** teardown, the
other **after** teardown. Teardown resets `field` to zero; this is why the same assertion
passes in the first case and fails in the second. The runner itself does not know this
distinction — it only makes the three calls in sequence and catches whatever exception the
body throws. `before-check` and `after-check` each appear as one line in the output, and both
are counted as "run" in the runner's list; the difference between them can only be read in the
result column.

The conclusion that follows is this: the order the framework calls is not a guarantee, it is a
contract. The setup-body-teardown order always stays the same, but **when** the body reads —
the state setup left behind, or the state teardown left behind — is in the hands of whoever
writes it. The same assertion text measures two different truths when written in two
different positions, and the framework does not arbitrate between them; it only records which
one threw and which one did not. A case being counted as "run" does not mean that case was set
up **correctly** — it only means the three calls were made in order and a result was produced.
The test classification and assertion writing that M21/K02 established do not change here; the
only thing that changes is that where the assertion is written inside the body determines the
result.

The source of this distinction is the `finally` block: the teardown call runs independently of
the body's result, because the runner placed it inside `finally`, not outside the `try` block.
The teardown step therefore works the same way both in a passing case like `store-filled` and
a failing case like `after-check` — it resets `field` to zero. The difference is **whether**
the assertion runs before or after this reset, and this is not information the runner itself
holds — it is a decision made by whoever writes the body. The same holds for a real framework:
the order in which setup and teardown are called is documented and guaranteed, but how the
body uses that order — which moment, which state it reads — is not under the framework's
control.

#### The Test Class Path: A Third Path

- **TE3.** The distinction between the compile class path and the runtime class path is
  carried over from the previous topic (Build Tools) and is not rebuilt here; the one question
  this lesson adds is what a third class path carries.
- **TE4.** "Type count" here is a count of compiled `.class` files. Each of the three sources
  below contains no inner class, so each produces exactly one `.class` file; the count
  therefore lines up with the number of sources, but in general the type count can differ from
  the number of source files.
- **TE5.** Running "without the framework type" means a real class loader searches the given
  class path for that type and does not find it — the loader here is a `URLClassLoader` and is
  built on top of the platform class loader, so it does not inherit anything from the
  application's own class path.

The runner above and the assertion helper inside it were part of the `Framework` class — but
in a real program, a helper of this kind stands **apart** from the code under test. The
measurement below actually compiles three separate classes: `Store` (production code),
`Assertion` (the framework itself, just an equality check the test needs) and `StoreTest` (the
test that uses both). `Store` compiles only on its own class path; `Assertion` compiles in a
separate directory; `StoreTest` compiles against both. It then runs against two separate class
paths: one contains all three, the other leaves `Assertion` out.

```java
// ThirdPath.java — the test class path really carries extra types the production class path does not
import java.io.*;
import java.lang.reflect.InvocationTargetException;
import java.net.*;
import java.nio.file.*;
import java.util.*;
import java.util.spi.ToolProvider;

public class ThirdPath {

    static Path write(Path dir, String name, String source) throws IOException {
        Files.createDirectories(dir);
        Path p = dir.resolve(name + ".java");
        Files.writeString(p, source);
        return p;
    }

    static boolean compile(Path output, List<Path> classPath, List<Path> sources) throws IOException {
        Files.createDirectories(output);
        List<String> arg = new ArrayList<>(List.of("-d", output.toString()));
        if (!classPath.isEmpty()) {
            StringJoiner sj = new StringJoiner(File.pathSeparator);
            for (Path p : classPath) sj.add(p.toString());
            arg.add("-classpath");
            arg.add(sj.toString());
        }
        for (Path s : sources) arg.add(s.toString());
        ToolProvider compiler = ToolProvider.findFirst("javac").orElseThrow();
        StringWriter silentOutput = new StringWriter();
        return compiler.run(new PrintWriter(silentOutput), new PrintWriter(silentOutput),
                arg.toArray(new String[0])) == 0;
    }

    static String run(List<Path> classPath, String mainClass) {
        URL[] url = new URL[classPath.size()];
        try {
            for (int i = 0; i < url.length; i++) url[i] = classPath.get(i).toUri().toURL();
            try (URLClassLoader loader = new URLClassLoader(url, ThirdPath.class.getClassLoader().getPlatformClassLoader())) {
                Class<?> cls = Class.forName(mainClass, true, loader);
                cls.getMethod("main", String[].class).invoke(null, (Object) new String[0]);
                return "ok";
            }
        } catch (InvocationTargetException e) {
            return e.getCause().getClass().getSimpleName();
        } catch (Throwable t) {
            return t.getClass().getSimpleName();
        }
    }

    static long typeCount(Path dir) throws IOException {
        try (var walk = Files.walk(dir)) {
            return walk.filter(p -> p.toString().endsWith(".class")).count();
        }
    }

    public static void main(String[] args) throws Exception {
        Path root = Files.createTempDirectory("thirdpath");
        Path source = root.resolve("source");
        Path prodDir = root.resolve("production"), frameworkDir = root.resolve("framework"), testDir = root.resolve("test");

        Path sStore = write(source, "Store",
                "public class Store {\n"
              + "  private int quantity;\n"
              + "  public Store(int initial) { quantity = initial; }\n"
              + "  public void add(int amount) { quantity += amount; }\n"
              + "  public void reduce(int amount) {\n"
              + "    if (amount > quantity) throw new IllegalStateException(\"insufficient quantity\");\n"
              + "    quantity -= amount;\n"
              + "  }\n"
              + "  public int quantity() { return quantity; }\n"
              + "}\n");
        Path sAssertion = write(source, "Assertion",
                "public class Assertion {\n"
              + "  public static void equal(int expected, int actual) {\n"
              + "    if (expected != actual) throw new AssertionError(\"expected \" + expected + \", got \" + actual);\n"
              + "  }\n"
              + "}\n");
        Path sStoreTest = write(source, "StoreTest",
                "public class StoreTest {\n"
              + "  public static void main(String[] a) {\n"
              + "    Store s = new Store(10);\n"
              + "    s.add(5);\n"
              + "    s.reduce(3);\n"
              + "    Assertion.equal(12, s.quantity());\n"
              + "    System.out.println(\"case passed\");\n"
              + "  }\n"
              + "}\n");

        compile(prodDir, List.of(), List.of(sStore));
        compile(frameworkDir, List.of(), List.of(sAssertion));
        compile(testDir, List.of(prodDir, frameworkDir), List.of(sStoreTest));

        System.out.println("type count on the production class path : " + typeCount(prodDir));
        System.out.println("type count on the test class path       : "
                + (typeCount(prodDir) + typeCount(frameworkDir) + typeCount(testDir)));

        System.out.println("full test class path run                 : " + run(List.of(prodDir, frameworkDir, testDir), "StoreTest"));
        System.out.println("without framework type (production+test) : " + run(List.of(prodDir, testDir), "StoreTest"));
    }
}
```

```
type count on the production class path : 1
type count on the test class path       : 3
case passed
full test class path run                 : ok
without framework type (production+test) : NoClassDefFoundError
```

The numbers confirm the claim. When `Store` compiles and runs on its own, the production class
path carries exactly one type. The full set needed to compile and run `StoreTest` is three
types: the production code, the framework itself, and the test itself. These two extra types —
`Assertion` and `StoreTest` — never appear anywhere in the production program; they do not
need to go into the distributed executable output, and if they do, it grows for no reason. The
test class path is therefore not a superset of the production class path, it is a **separate**
path: it carries types production does not need, and it also covers the types production does
carry.

The direction is one-sided. Compiling `StoreTest` requires putting `Store` and `Assertion` on
the class path, because the test names both of them by name in its source. Compiling `Store`,
on the other hand, needs neither `Assertion` nor `StoreTest` — the production code was written
with no knowledge that test types exist. The distinction the packaging topic established
applies here without change: if an executable output is produced from the production class
path, no type like `Assertion` or `StoreTest` ever enters it; if one does, that output is
bigger than it needs to be and carries more types than it should. The test class path being a
third path also means it must not mix into the production output — the two paths mixing
together is the most common form a leaked dependency takes.

Placing the three class paths side by side makes the ordering clear: the compile class path
asks for `Store` together with `Assertion` to compile `StoreTest`, the runtime class path
carries only `Store` and knows nothing of `Assertion`, and the test class path carries all
three at once. All three paths can point to the same `.class` files on the same disk; the
difference between them is not in the files' content, it is in which directories are given to
which command. A type being "required on the test class path" means that type was never
written in the production code, and arose only from the test's own need — and this distinction
is invisible without actually building the two paths separately.

In this lesson, `Assertion` and `StoreTest` depended on each other at compile time, but
`Assertion` being present **on the runtime class path** at run time was a separate requirement
from compiling — and the last line indeed separates the two. The next lesson carries this
observation one step further: when the type the test needs replaces a type the code under test
**already** uses, the same compiled test can give two different results across two different
runtime class paths.

The last two lines show this by actually running. When `StoreTest` runs on the full class
path — production, framework and test types together — the `Assertion.equal` call is found
and the case returns "ok." The same `StoreTest.class` file, run this time only with the
production and test directories, **without** the framework directory, has the loader find the
`Assertion` name nowhere, and it throws `NoClassDefFoundError`. The file itself did not change,
it was not recompiled; only the class path given to it narrowed. This is the most concrete
proof of the claim that "the test class path is a third path": the type the framework supplies
is required both for the test to **compile** and for it to **run**, but is required for
neither compiling nor running production.

#### Summary

- The test framework was modeled here as a small runner that calls setup, body and teardown
  in sequence and catches assertion failure with an exception.
- The same assertion passed when checked before teardown and failed when checked after
  teardown; the runner counted both as "run," and the order itself did not guarantee
  correctness.
- The production class path carried a single type; the test class path carried the
  framework's and the test's own types in addition to that same type — three types in total.
- The same compiled `StoreTest.class`, run on a class path missing the framework type, gave
  `NoClassDefFoundError` without being recompiled; the file did not change, only the class
  path changed.
- Test classification and assertion writing were established in the M21/K02 Unit Testing and
  Test-Driven Development course and were not repeated here.

#### Next Step

In this lesson, the types that entered the test class path were types we wrote by hand
ourselves, standing entirely apart from the code under test. But a dependency the test needs
can end up having to take the place of a type the production code **already** uses — a type
that carries the same name, provides the same signature, but behaves differently. The next
lesson runs the same compiled test once against a class path where the real type stands, and
again against another class path where a fake type carrying the same name stands, and counts
that the test gets two different results without being recompiled.

### Mocking

Source: https://academia.sh/en/courses/java-ecosystem/mocking

The same compiled test is run once against a runtime class path where the real supplier type stands, and again against another runtime class path where a same-named fake type stands. The test gives two different results without being recompiled, and no error occurs; once the signature diverges, the same setup fails at run time and the cause does not appear in the test text.

In the previous lesson, the types that entered the test class path — the framework and the
test itself — stood entirely apart from the code under test; production code never named them
at all. But a dependency the test frequently substitutes takes the place of a type the
production code **already** uses: it carries the same name, answers the same call, but
behaves differently. This lesson runs the same compiled test against two separate
implementations of such a type — one real, the other fake — and counts that the test gets two
different results without ever being recompiled. This is a gap in the exact opposite direction
from the one the previous lesson tested: there, it looked at what happens when a type is never
present at all; here, it looks at what happens when a type is present **in place of another
type**.

The test double family — mock object, stub, spy object, dummy object — was established in the
M21/K02 Unit Testing and Test-Driven Development course and is not reintroduced here; which
kind answers which question is written there. The one question this lesson adds sits on a
different axis: **which class path** does the fake type stand on, and does it take the real
type's place at compile time or at run time?

This question sits in a different place from the test class path question the previous lesson
established. There, the test class path carried two types the production class path **never
knew at all** — the framework and the test itself. Here, the situation is reversed: the fake
type takes the place of a dependency the test code **already** names. The test code wrote the
name `Supplier`; which class file that name would resolve to was left to the class path. Same
name, same call, two separate bodies — and what decides which body runs is not the source
text, it is **which** directory is given to the class path in that particular run.

#### Two Implementations Carrying the Same Name

- **TE6.** The fake type here is a separate, hand-written class with the same name and a
  compatible signature. The proxy machinery that mocking libraries use to generate bytecode at
  run time is not built here — the behavior produces the same result with a hand-written
  class.
- **TE7.** The test source looks up the name `Supplier` only through reflection
  (`Class.forName`); its source is not bound to any implementation at compile time. This is
  why the test compiles once and runs unchanged across different runtime class paths.

The behavior under test is a store-fill call: a type named `Supplier` takes a requested
quantity and returns how much of it was fulfilled. Behind this call in production code stands
a real supply process — it is not modeled here, only the call itself and the number it returns
matter. A test going to the real supply process every time it runs this call would be both
slow and unstable; this is why the test needs another type that can take the real
implementation's place, carrying the same name and the same call shape. We write three
separate implementations. The production implementation fulfills the entire requested
quantity. The fake implementation fulfills one unit less on every call and keeps how many
times it was called in a static field — the spy object's call record is carried by this field
here. The third implementation carries the same name but changes its first parameter's type
from `int` to `long`; this is used afterward to show what a signature mismatch does at run
time. The test is bound to **none** of the three implementations at compile time — it only
looks up the name `Supplier` and the `fill(int)` signature through reflection.

```java
// TwoRuns.java — the same compiled test gives a different result when the run class path swaps the real type for a same-named fake type
import java.io.*;
import java.lang.reflect.InvocationTargetException;
import java.net.*;
import java.nio.file.*;
import java.util.*;
import java.util.spi.ToolProvider;

public class TwoRuns {

    static Path write(Path dir, String name, String source) throws IOException {
        Files.createDirectories(dir);
        Path p = dir.resolve(name + ".java");
        Files.writeString(p, source);
        return p;
    }

    static boolean compile(Path output, List<Path> classPath, List<Path> sources) throws IOException {
        Files.createDirectories(output);
        List<String> arg = new ArrayList<>(List.of("-d", output.toString()));
        if (!classPath.isEmpty()) {
            StringJoiner sj = new StringJoiner(File.pathSeparator);
            for (Path p : classPath) sj.add(p.toString());
            arg.add("-classpath");
            arg.add(sj.toString());
        }
        for (Path k : sources) arg.add(k.toString());
        ToolProvider compiler = ToolProvider.findFirst("javac").orElseThrow();
        StringWriter silentOutput = new StringWriter();
        return compiler.run(new PrintWriter(silentOutput), new PrintWriter(silentOutput),
                arg.toArray(new String[0])) == 0;
    }

    static String run(List<Path> classPath, String mainClass) {
        URL[] url = new URL[classPath.size()];
        try {
            for (int i = 0; i < url.length; i++) url[i] = classPath.get(i).toUri().toURL();
            try (URLClassLoader loader = new URLClassLoader(url, TwoRuns.class.getClassLoader().getPlatformClassLoader())) {
                Class<?> cls = Class.forName(mainClass, true, loader);
                cls.getMethod("main", String[].class).invoke(null, (Object) new String[0]);
                return "ok";
            }
        } catch (InvocationTargetException e) {
            return e.getCause().getClass().getSimpleName();
        } catch (Throwable t) {
            return t.getClass().getSimpleName();
        }
    }

    public static void main(String[] args) throws Exception {
        Path root = Files.createTempDirectory("tworuns");
        Path source = root.resolve("source");
        Path testDir = root.resolve("test"), prodDir = root.resolve("production"),
             fakeDir = root.resolve("fake"), brokenDir = root.resolve("broken");

        Path sTest = write(source, "SupplierTest",
                "import java.lang.reflect.Field;\n"
              + "import java.lang.reflect.Method;\n"
              + "public class SupplierTest {\n"
              + "  public static void main(String[] a) throws Exception {\n"
              + "    Class<?> c = Class.forName(\"Supplier\");\n"
              + "    Object obj = c.getDeclaredConstructor().newInstance();\n"
              + "    Method m = c.getMethod(\"fill\", int.class);\n"
              + "    int[] requests = {5, 3, 8};\n"
              + "    int total = 0;\n"
              + "    for (int request : requests) total += (int) m.invoke(obj, request);\n"
              + "    int expected = 5 + 3 + 8;\n"
              + "    System.out.println(\"expected    : \" + expected);\n"
              + "    System.out.println(\"actual      : \" + total);\n"
              + "    System.out.println(\"assertion   : \" + (total == expected));\n"
              + "    try {\n"
              + "      Field f = c.getField(\"callCount\");\n"
              + "      System.out.println(\"call count  : \" + f.getInt(null));\n"
              + "    } catch (NoSuchFieldException e) {\n"
              + "      System.out.println(\"call count  : not tracked\");\n"
              + "    }\n"
              + "  }\n"
              + "}\n");

        Path sProd = write(source.resolve("production"), "Supplier",
                "public class Supplier {\n"
              + "  public int fill(int requested) { return requested; }\n"
              + "}\n");
        Path sFake = write(source.resolve("fake"), "Supplier",
                "public class Supplier {\n"
              + "  public static int callCount = 0;\n"
              + "  public int fill(int requested) { callCount++; return requested - 1; }\n"
              + "}\n");
        Path sBroken = write(source.resolve("broken"), "Supplier",
                "public class Supplier {\n"
              + "  public int fill(long requested) { return (int) requested; }\n"
              + "}\n");

        compile(testDir, List.of(), List.of(sTest));
        compile(prodDir, List.of(), List.of(sProd));
        compile(fakeDir, List.of(), List.of(sFake));
        compile(brokenDir, List.of(), List.of(sBroken));

        System.out.println("-- with the real type --");
        System.out.println("run result: " + run(List.of(testDir, prodDir), "SupplierTest"));
        System.out.println("-- with the fake type (test not recompiled) --");
        System.out.println("run result: " + run(List.of(testDir, fakeDir), "SupplierTest"));
        System.out.println("-- with the signature-changed type --");
        System.out.println("run result: " + run(List.of(testDir, brokenDir), "SupplierTest"));
    }
}
```

```
-- with the real type --
expected    : 16
actual      : 16
assertion   : true
call count  : not tracked
run result: ok
-- with the fake type (test not recompiled) --
expected    : 16
actual      : 13
assertion   : false
call count  : 3
run result: ok
-- with the signature-changed type --
run result: NoSuchMethodException
```

`SupplierTest` compiles once (`testDir`) and runs three times with a different second class
path component each time: first `prodDir`, then `fakeDir`, finally `brokenDir`. The only
difference is the second element of the list given to the `run` call; the test's own `.class`
file carries exactly the same bytecode in all three.

Compiling `SupplierTest` needs no implementation at all — the name `Supplier` appears in its
source only as a string constant, not as an `import`ed type. This sharpens even further the
distinction the first lesson established between the compile class path and the runtime class
path: here, the test compiles needing **no** compile class path component at all, yet needs a
different runtime class path component for each of its three separate runs. A dependency
invisible at compile time becomes mandatory at run time — this is the test class path's
counterpart to the "compiled is not proof" reading the previous topic established.

Real mocking libraries usually make this swap interface-based: the code under test binds to an
interface, the fake type implements that interface, and the compiler checks that the two are
compatible. The path chosen here is more bare — direct reflection — because the lesson has to
produce the same result without writing a third-party framework name. The result is the same
with the interface-based approach too: which implementation gets used is still left to the
runtime class path, and the compiler only checks that **one** implementation exists — it does
not check how many there are or which one gets chosen.

#### Silent Divergence: No Error, Different Result

- **TE8.** The call record is kept here as a static field (`callCount`). Because each run uses
  its own `URLClassLoader`, this field starts from zero on every run; the counter never gets
  mixed up between two runs.

The first run, executed with the real type, passes as expected: the sum of the three requests
is 16, the returned total is also 16, the assertion is true. The second run runs **the same
test file, with the same bytecode**, only making the second class path component `fakeDir`.
The fake implementation fulfills one unit less than requested on every call; the sum of the
three calls therefore drops to 13 and the assertion line prints `false`. No exception is
thrown here, there is no compile error — the `run` call returns "ok" in both cases. The only
difference is in the content of one line: the `assertion` field drops from `true` to `false`.
This is the course's cleanest example of the silent defect family: both runs complete
successfully, both are counted as "ran," but the result they produce is not the same.

The call record the spy object keeps confirms this distinction. The production implementation
has no field named `callCount` — the test catches this and writes "not tracked." In the fake
implementation, that same field carries the value `3`: `fill` was called three times, and all
three incremented the record. This number carries proof on its own, without ever looking at
the test's return value — the fake type really was called three times, no fewer and no more.
The `assertion` field says **what** was returned, `callCount` says **how many times** it was
called; the two answer separate questions, and both can be read together in the same run.

The difference these two runs produce shows what a test suite saying "passed" does not
guarantee. The suite run with the real implementation passes, the suite run with the fake
implementation fails — both carry the same test file, the same assertion. If a delivery
pipeline runs its tests only on a class path where the fake implementation stands, it never
sees a regression in the production implementation; the tests keep passing because none of
them ever ran with the real type. This is the most direct counterpart to the course's
measuring question in the test topic: a test "passing" cannot be read without looking at
**which class path** it ran on.

#### When the Signature Diverges, the Defect Falls to Run Time

- **TE9.** The signature comparison here looks only at the first parameter's type
  (`getMethod("fill", int.class)`); a real reflection lookup matches the name, the parameter
  count and the parameter types together, and does not look at the return type.
- **TE10.** Mocking libraries' verification (verify) call is not built here; the signal the spy
  object produces in this lesson is only a counter that gets read, not a self-verifying
  object.

The third run runs the same `SupplierTest.class`, this time on a class path where an
implementation stands whose `fill`'s first parameter is `long`, not `int`. `Class.forName
("Supplier")` finds the class — the name is the same, the class really exists — but the call
`c.getMethod("fill", int.class)` cannot find a method with exactly this signature, and throws
`NoSuchMethodException`. This exception exits the test's own `main` uncaught, and the `run`
helper catches it and returns its name: `run result: NoSuchMethodException`.

The distinction here falls into a different defect class than the previous two runs. The run
with the fake type was **silent** — both sides produced a valid result, the difference carried
only in a number. The run with the diverged signature, on the other hand, fails **at run
time**: the program stops with an exception. But it is worth noting what this exception
actually says — the `NoSuchMethodException` message says which class was searched for which
method, but it does not say **why** that method is no longer there. The test text itself did
not change at all; the cause of the defect is visible only in the changed implementation's
source, not in the test file. When a test depends this heavily on which type stands on the
runtime class path, the signature mismatch is caught not at compile time but at run time, and
all that is left in hand at that moment is the name of an exception class.

The order of these three runs is deliberate: the real type, then a compatible fake type, and
finally an incompatible type. The difference between them is **how closely** the fake type
resembles the real type. Because the compatible fake type's signature matches the real type's
signature exactly, it can take its place with the test never knowing — the defect is carried
only by a number in the result. The incompatible type, even though its name is the same, has a
diverged signature, so the test's reflection call cannot find it at all — the defect is no
longer a number, it is an exception. The difference between these two extremes shows the real
risk of mocking: keeping a fake type current requires keeping not just its name but also its
signature matching the real type, and this match is not checked by the compiler, because the
two are never present together in the same compilation.

All three runs together sample the same three defect classes: the run with the real type is
trouble-free, the run with the fake type silently gives a different result, and the run with
the diverged signature fails with an exception. None of them was caught during compilation —
all three were a decision invisible on the compile class path surfacing on the runtime class
path. A test suite's reliability therefore depends not only on which assertions it writes, but
also on which class path those assertions run against.

#### Summary

- The test double family was established in M21/K02; the one question this lesson adds is
  which class path the fake type stands on.
- The same compiled test ran without being recompiled against real and fake implementations
  carrying the same name and gave two different results; both completed without error.
- The call counter the fake implementation kept carried the spy object's signal through a
  field the production implementation does not have.
- A run with an implementation whose signature had changed gave `NoSuchMethodException`; this
  exception failed at run time and its cause did not appear in the test text.
- Two types carrying the same name can take each other's place only as long as their
  signatures match; once the signature diverges, the defect surfaces at run time, not at
  compile time.

#### Next Step

In this lesson, a single test body ran against three separate class paths, but the same three
numbers — 5, 3, 8 — were used in every run. Multiplying a test body across several data sets
opens a separate question: same body, same assertion, different inputs — how many separate
runs, how many separate failures, and how does the record capture which data row a failure
came from? The next lesson counts this by running a single body repeatedly over a data set.

### Parameterized Tests

Source: https://academia.sh/en/courses/java-ecosystem/parameterized-tests

A single body is called three times over a declared data set, and each call records whether its own row passed or failed. When setup is moved outside the loop and a single Store instance is shared across rows, which row passes becomes dependent on the data set's order, and the same three rows in reverse order make a different row pass.

In the previous lesson, a single test body ran against three separate class paths, but the
same three numbers were used in every run. In this lesson the body stays fixed, and the side
that changes is the data: instead of writing the body three times to run the same check with
three separate inputs, we write the data set as a declaration and build the body **once**. The
question is: when a body is called against an N-row data set, how many separate results does
it produce, how many fail, and how do we know which row is the one that failed?

The second half of this question looks separate at first glance, but it is not: running the
same body N times **assumes** that the N runs are independent of each other. Because the body
is written once, one row's setup passes through the same code as the next row's setup — and
whether that code touches a shared variable never shows up in the data set itself. This lesson
first counts three independent results with a correctly built body, then runs the same data
with a broken setup and shows when the assumption collapses.

The data set itself is kept to three rows here, but the number itself is not what matters —
what matters is that the same body can be called N times regardless of what N is, and that
each call can produce its own result. Three rows was chosen because it is the smallest number
that can still be traced by hand; the same setup bug surfaces the same way in a hundred-row
data set, only tracing it gets harder.

#### The Data Set Stands in a Declaration, the Body Is Written Once

- **TE11.** The data set is declared here as an array of `record`s (`Row[]`). Real frameworks'
  mechanism for reading a data source from a file, an annotation or a method reference is not
  built here — the data stands directly in the source code.
- **TE12.** The body here is a loop that walks the data array; the same three rows are called
  once each, in the same order, each with its own input. The call count equals the row count.

The `ROWS` array below carries three rows; each row holds an initial quantity, a reduction and
the expected result together. The `independentRun` body processes these three rows in order
and builds **its own** `Store` instance for each row — setup lives inside the row and runs
again on every turn of the loop. Below, the same data set is also run with a broken version
where setup is moved **outside** the loop; the difference between the two is this lesson's
real subject.

```java
// DataSet.java — one body, a run multiplied by the data set, and shared state's order dependency
import java.util.*;

public class DataSet {

    static class Store {
        private int quantity;
        Store(int initial) { quantity = initial; }
        void reduce(int amount) { quantity -= amount; }
        int quantity() { return quantity; }
    }

    record Row(int initial, int reduce, int expected) {}

    static final Row[] ROWS = {
        new Row(10, 3, 7),
        new Row(5, 2, 3),
        new Row(8, 8, 0),
    };

    // -- body written once, each of the N runs builds its own Store ------------------------
    static List<String> independentRun(Row[] rows) {
        List<String> log = new ArrayList<>();
        int failed = 0;
        for (int i = 0; i < rows.length; i++) {
            Row r = rows[i];
            Store s = new Store(r.initial());
            s.reduce(r.reduce());
            boolean passed = s.quantity() == r.expected();
            if (!passed) failed++;
            log.add("row " + i + ": " + (passed ? "passed" : "failed, actual=" + s.quantity()));
        }
        log.add("failed count: " + failed);
        return log;
    }

    // -- setup mistakenly moved outside the loop, Store is shared across runs --------------
    static List<String> sharedRun(Row[] rows) {
        List<String> log = new ArrayList<>();
        int failed = 0;
        Store s = new Store(rows[0].initial());
        for (int i = 0; i < rows.length; i++) {
            Row r = rows[i];
            s.reduce(r.reduce());
            boolean passed = s.quantity() == r.expected();
            if (!passed) failed++;
            log.add("row " + i + ": " + (passed ? "passed" : "failed, actual=" + s.quantity()));
        }
        log.add("failed count: " + failed);
        return log;
    }

    public static void main(String[] args) {
        System.out.println("-- independent run: each row builds its own Store --");
        independentRun(ROWS).forEach(System.out::println);

        System.out.println("-- shared run, original data order --");
        sharedRun(ROWS).forEach(System.out::println);

        Row[] reversedRows = { ROWS[2], ROWS[1], ROWS[0] };
        System.out.println("-- shared run, same data, order reversed --");
        sharedRun(reversedRows).forEach(System.out::println);
    }
}
```

```
-- independent run: each row builds its own Store --
row 0: passed
row 1: passed
row 2: passed
failed count: 0
-- shared run, original data order --
row 0: passed
row 1: failed, actual=5
row 2: failed, actual=-3
failed count: 2
-- shared run, same data, order reversed --
row 0: passed
row 1: failed, actual=-2
row 2: failed, actual=-5
failed count: 2
```

`independentRun` passes all three rows. This is the expected result, because each row builds
its own `Store` from scratch — all three are unaware of each other, none touches another's
state. The body was written in one place (the loop body), but ran three times and produced
three separate results. The failure count (`failed count`) and each row's own status (`row i:
passed`/`failed`) are recorded separately in the log — when a row fails, which row it was and
the value it returned stay on record, not just a single total number.

This is a different way of counting from the "one assertion" idea in the previous lesson.
There, one test produced one result; here, one body is called N times and gives rise to N
separate assertions. A three-row data set means three separate assertions, and all three must
be evaluated independently — one failing while the other two pass is still a meaningful
result, because each row tests its own input on its own. Writing the body once is a shorthand
here, reducing repetition in the source text; but it does not change the fact that there are
three separate calls in the compiled or running program. The `ROWS` array and the
`independentRun`/`sharedRun` methods are not outside the distinction the previous lesson
established either — neither of them is written in production code, both stand only on the
test class path.

#### Shared Setup, an Order-Dependent Result

- **TE13.** Which row a failure came from is accumulated here as a list element. Real
  frameworks' report format (file name, line number, stack trace) is not built here; the
  record is only the row index and the resulting value.
- **TE14.** The shared state here is a single `Store` instance moved outside the loop. Where
  in the loop setup runs — on every turn, or once — determines whether the rows are
  independent of each other.
- **TE15.** Order dependency is shown here with a reversed copy of the same data set. In a
  real test suite, the order in which rows (or tests) run is usually undocumented and can
  change from run to run; this lesson samples only two possible orders.

`sharedRun` carries the same three rows, the same assertion — the only difference is that
`Store` is built just once, **outside** the loop. This models a bug where setup should run
again for every row but mistakenly runs only for the first row: the `s` object is built from
the first row's initial value, and then every row's `reduce` call accumulates on the **same**
object. In the original order, the first row passes — because the shared `Store` was built
with exactly that row's expected initial value — but the second and third rows no longer see
their own expected initial state, they see the state the previous rows left behind, and both
fail.

Tracking the numbers makes the difference clear. The shared `Store` is built with `10`, which
is `ROWS[0].initial()`. The first row calls `reduce(3)`, the quantity becomes 7, the expected
value is also 7 — it passes. The second row never sees its own initial value of 5; instead it
sees the 7 the first row left behind, and calling `reduce(2)` makes the quantity 5, which fails
because the expected value is 3. The third row also does not see its own initial value of 8,
but the 5 the second row left behind; calling `reduce(8)` drops the quantity to -3, far from
the expected 0. Each row's own data is correct, each row's own arithmetic is correct — what is
wrong is which state they start from.

The real demonstration is in the last two runs. The same three data rows are run again with
only their order reversed. The result again takes the shape "one row passes, two fail" — but
the row that passes **changes**. In the original order, the passing row was the data set's
first row (10 → 7); in the reversed order, the passing row is whichever row arrives first in
that order (8 → 0). The data set itself did not change, none of the rows' content was edited —
only which row was processed **first** changed, and this single change flipped which row
passes from top to bottom. Whether a row passes or not is no longer tied to that row's own
data, but to its position.

The same tracing can be done in the reversed order. The shared `Store` is now built with `8` —
the initial value of the third row, the first element of the reversed array. That row calls
`reduce(8)`, the quantity becomes `0`, the expected value is also `0` — it passes. The second
data row, now arriving second, does not see its own initial value of 5, but the 0 the previous
step left behind; calling `reduce(2)` makes the quantity -2, far from the expected 3, and it
fails. Finally, the first data row does not see its own initial value of 10, but -2; calling
`reduce(3)` makes the quantity -5, far from the expected 7, and it fails. In both runs, only
the **first processed** row passes — because the shared `Store` is built with exactly that
row's expected initial value — and the rest inherit the state the previous rows left behind
and fail. Which row is "processed first" depends on the data array's order, not on the row's
own content.

This is a concrete source of the flaky test concept established in the M21/K02 Unit Testing
and Test-Driven Development course: state shared between tests (or, as here, between the same
test's rows) makes the result dependent on run order. The concept itself was established
there; what is new in this lesson is **where** the source stands — whether setup runs inside
or outside the loop is a decision that never shows up in the declared data set itself. It is
not possible to recover this decision by looking at the two runs' logs; it only becomes
visible by changing the data order and running again.

This is why most real test frameworks deliberately shuffle the order in which rows or tests
run: two separate runs giving the same order every time keeps a bug arising from shared state
hidden. If the order stays fixed on every run, a divergence like the one this lesson showed in
the second run might never appear — the suite says "passed," only because the order is always
the same. Once the order is shuffled, the same buggy setup passes sometimes one row, sometimes
another, and the total failure count can appear to change from run to run. This variability is
the symptom of the bug — a sign that it must be sought not in the test itself, but in state
shared between tests.

The rule that follows is this: parameterization multiplies a body, but it does not guarantee
that the multiplied runs are **independent** of each other. What guarantees independence is
each run executing its own setup on its own — as in `independentRun`. When setup runs once and
is shared across N runs, no matter how correctly the data set is written, the result becomes
dependent on the data set's **order**. A row passing no longer shows that the row is correct,
only that the row's position happened to fit.

The root of the bug is an aliasing problem: where two separate rows expect two separate
`Store`s, a single `Store` reference is shared between them. This sharing is not visible in
the source text — the name `s` appears on the same line on every turn of the loop, the only
thing that changes is which `Row` record is being processed. The variable's **scope** (outside
the loop) and its **intended lifetime** (a single row) are separated here, and this separation
cannot be seen from the source text; it is seen only by running with several rows, in several
orders. This observation brings the course's measuring question down to the level of test
data: which scope a setup runs in is, just like which class path a type stands on, a decision
that is not written explicitly in the source text but that determines the result.

It would also be misleading to think that moving setup outside the loop has no cost — quite
the opposite, that move is itself usually the result of chasing some gain. `independentRun`
builds a new `Store` for every row; in an N-row data set this means N separate setup calls. If
setup is an expensive operation — opening a real connection, building a large structure — this
repetition can look costly, and sharing setup by running it once becomes tempting. This lesson
shows the price of that temptation: the speed gained from shared setup is paid for by losing
independence between rows. Wanting both properties at once — a setup that is both shared and
independent — is a contradiction; which one takes priority is a decision made by whoever
writes the body, and this lesson only measures the outcome of that decision.

There is no single correct answer to this choice, only conscious and unconscious forms of it.
If a body shares setup deliberately — for example, when rows read a common, unchanging
resource that none of them writes to — sharing is not a bug. The bug is sharing happening,
without anyone realizing it, in a setup that involves **writing**. This lesson's `sharedRun`
method models the second case: when setup was moved outside the loop, which row would see
which state was never considered, the only goal was reducing repetition.

#### Summary

- The data set was written as a declaration, the body was built once and called as many
  times as there was data — three times; each call recorded its own row's result.
- With independent setup, all three rows passed; the failure count and which row it came
  from were logged separately.
- When setup was moved outside the loop and a single `Store` was shared between rows, two
  rows failed, but which row passed became dependent on data order.
- When the same three rows ran in reverse order, the passing row changed; the data content
  stayed the same, only the processing order changed.
- Shared state making the result order-dependent is a concrete source, materialized in this
  lesson, of the flaky test concept established in M21/K02.

#### Next Step

In this lesson, the thing that was shared was an object a test body built itself — a bug
belonging to the test's own arrangement, outside the code under test. The code under test
(`Store`) itself never changed; only the test side's setup changed. The next lesson moves
shared state **outside** the code under test: a dependency mocked in a unit test is put back
with its real implementation in an integration setup, and the same test body now runs against
a larger class path. Where the defect moves — how a divergence that stays silent in the unit
setup turns into a failure in the integration setup — is the next lesson's question.

### Integration Tests

Source: https://academia.sh/en/courses/java-ecosystem/integration-tests

The supplier mocked in the unit setup is put back on the runtime class path here with its real implementation, together with a second type the real implementation depends on; the type count the path carries rises from one to two. The same test body runs in two setups: in the unit setup a limit violation passes silently, in the integration setup the same request gives a run failure.

In the previous lesson, the thing that was shared was an object a test body built itself; the
code under test itself never changed. This lesson moves shared state **outside** the code
under test: the `Supplier` dependency mocked in the second lesson comes back here with its
real implementation, and a second type the real implementation itself depends on ends up added
to the class path. The question is: when the same test body runs against a class path whose
content is growing, where does a divergence that previously stayed silent land?

This fourth lesson's axis proceeds through the same name `Supplier` as the second lesson, but
it seeks an answer to a different question. The second lesson asked the "same name, two
bodies" question and measured when one type taking another's place stays silent, and when it
gives a run failure. This lesson goes one step behind that question: when one of the
implementations carries a dependency the other does not, how does the class path itself
change, and which defect does that change make visible?

The subject area of integration testing — test environment management, mocking external
services — was established in the M21/K03 Integration, Contract and End-to-End Testing course
and is not rebuilt here. The one question this lesson adds is the relationship between the
**size** of the class path the test sees and where the defect falls.

The difference between unit and integration setups is usually explained through setup time or
access to external systems — one fast and isolated, the other slow and real. This lesson reads
the same difference through the class path: the real difference between the two setups is the
set of types the test **never sees at compile time** but **encounters at run time**. What
makes a setup "unit" or "integration" is not a label, it is which types are put on the class
path in that particular run.

#### The Same Dependency, With Its Real Implementation

- **TE16.** The external system here is built as an ordinary Java class named `Inventory`,
  modeling a real-world stock limit. No product, driver or protocol name is written; no real
  network call, file system access or external process is made either.
- **TE17.** "The type count the path carries" here is a count of compiled `.class` files — the
  same measuring method the first lesson established.

In the second lesson, a type named `Supplier` existed with a production implementation that
fulfilled the entire requested quantity and a fake implementation that fulfilled one unit less
than requested; the test was not bound to either at compile time, it only looked up the name
through reflection. This lesson reuses the same name, but this time builds the real
implementation not **on its own**, but together with a limit check it depends on:
`Inventory.checkLimit` throws an exception when the requested quantity exceeds 100. The fake
implementation in the unit setup carries no such check — it fulfills every request silently.

The difference between these two implementations is a deliberate design decision. The unit
setup carries no limit check, because the unit test's purpose is to test the flow of the code
that calls `Supplier` — the limit itself is `Inventory`'s responsibility, and `Inventory`'s own
test is written separately. The integration setup, by running the two types together, tests
the real interaction between them: does `Supplier` really pass the call on to `Inventory`,
does the exception `Inventory` throws really reach the caller? Neither of these questions can
be asked in the unit setup, because `Inventory` is not there at all.

```java
// Growing.java — integration setup puts real types back onto the runtime class path, the path's type count grows
import java.io.*;
import java.lang.reflect.InvocationTargetException;
import java.net.*;
import java.nio.file.*;
import java.util.*;
import java.util.spi.ToolProvider;

public class Growing {

    static Path write(Path dir, String name, String source) throws IOException {
        Files.createDirectories(dir);
        Path p = dir.resolve(name + ".java");
        Files.writeString(p, source);
        return p;
    }

    static boolean compile(Path output, List<Path> classPath, List<Path> sources) throws IOException {
        Files.createDirectories(output);
        List<String> arg = new ArrayList<>(List.of("-d", output.toString()));
        if (!classPath.isEmpty()) {
            StringJoiner sj = new StringJoiner(File.pathSeparator);
            for (Path p : classPath) sj.add(p.toString());
            arg.add("-classpath");
            arg.add(sj.toString());
        }
        for (Path k : sources) arg.add(k.toString());
        ToolProvider compiler = ToolProvider.findFirst("javac").orElseThrow();
        StringWriter silentOutput = new StringWriter();
        return compiler.run(new PrintWriter(silentOutput), new PrintWriter(silentOutput),
                arg.toArray(new String[0])) == 0;
    }

    static String run(List<Path> classPath, String mainClass) {
        URL[] url = new URL[classPath.size()];
        try {
            for (int i = 0; i < url.length; i++) url[i] = classPath.get(i).toUri().toURL();
            try (URLClassLoader loader = new URLClassLoader(url, Growing.class.getClassLoader().getPlatformClassLoader())) {
                Class<?> cls = Class.forName(mainClass, true, loader);
                cls.getMethod("main", String[].class).invoke(null, (Object) new String[0]);
                return "ok";
            }
        } catch (InvocationTargetException e) {
            return e.getCause().getClass().getSimpleName();
        } catch (Throwable t) {
            return t.getClass().getSimpleName();
        }
    }

    static long typeCount(Path dir) throws IOException {
        try (var walk = Files.walk(dir)) {
            return walk.filter(p -> p.toString().endsWith(".class")).count();
        }
    }

    public static void main(String[] args) throws Exception {
        Path root = Files.createTempDirectory("growing");
        Path source = root.resolve("source");
        Path testDir = root.resolve("test"), unitDir = root.resolve("unit"), integrationDir = root.resolve("integration");

        Path sTest = write(source, "IntegrationTest",
                "import java.lang.reflect.InvocationTargetException;\n"
              + "import java.lang.reflect.Method;\n"
              + "public class IntegrationTest {\n"
              + "  public static void main(String[] a) throws Throwable {\n"
              + "    Class<?> c = Class.forName(\"Supplier\");\n"
              + "    Object obj = c.getDeclaredConstructor().newInstance();\n"
              + "    Method m = c.getMethod(\"fill\", int.class);\n"
              + "    int[] requests = {30, 150};\n"
              + "    for (int request : requests) {\n"
              + "      try {\n"
              + "        int result = (int) m.invoke(obj, request);\n"
              + "        System.out.println(\"request \" + request + \" -> result \" + result);\n"
              + "      } catch (InvocationTargetException e) {\n"
              + "        throw e.getCause();\n"
              + "      }\n"
              + "    }\n"
              + "    System.out.println(\"all requests completed\");\n"
              + "  }\n"
              + "}\n");

        Path sUnit = write(source.resolve("unit"), "Supplier",
                "public class Supplier {\n"
              + "  public int fill(int requested) { return requested; }\n"
              + "}\n");

        Path integrationSourceDir = source.resolve("integration");
        Path sInventory = write(integrationSourceDir, "Inventory",
                "public class Inventory {\n"
              + "  static void checkLimit(int requested) {\n"
              + "    if (requested > 100) throw new IllegalStateException(\"stock limit exceeded\");\n"
              + "  }\n"
              + "}\n");
        Path sReal = write(integrationSourceDir, "Supplier",
                "public class Supplier {\n"
              + "  public int fill(int requested) { Inventory.checkLimit(requested); return requested; }\n"
              + "}\n");

        compile(testDir, List.of(), List.of(sTest));
        compile(unitDir, List.of(), List.of(sUnit));
        compile(integrationDir, List.of(), List.of(sInventory, sReal));

        System.out.println("-- type count on the path --");
        System.out.println("unit setup        : " + typeCount(unitDir) + " types");
        System.out.println("integration setup : " + typeCount(integrationDir) + " types");

        System.out.println("-- same test body, two setups --");
        System.out.println("unit run result       : " + run(List.of(testDir, unitDir), "IntegrationTest"));
        System.out.println("integration run result: " + run(List.of(testDir, integrationDir), "IntegrationTest"));
    }
}
```

```
-- type count on the path --
unit setup        : 1 types
integration setup : 2 types
-- same test body, two setups --
request 30 -> result 30
request 150 -> result 150
all requests completed
unit run result       : ok
request 30 -> result 30
integration run result: IllegalStateException
```

The unit setup carries a single `.class` file: the fake `Supplier`, dependent on nothing else.
The integration setup carries two `.class` files: the real `Supplier` and the `Inventory` it
calls. `IntegrationTest` is, for the third time, the same file, the same bytecode — the only
thing that changes is which directories are given to the class path. The path's content grows
because the real implementation brings with it a dependency the fake implementation never
carried at all.

This growth proceeds in a different direction from the growth counted in the first lesson.
There, the test class path carried two types the production class path **never knew at
all** — the framework and the test itself, both outside the production code. Here, the side
that grows is the production side: `Inventory` is a **real** part of the production code, but
it never entered the class path in the unit setup. A class path growing does not mean a single
thing — sometimes it grows because the test carries its own tools, sometimes because it puts a
real part of production back in. Both are measured with the same counting method — counting
compiled `.class` files — but what they point to is separate.

#### The Same Test Body, Two Setups, Two Defect Classes

- **TE18.** The same request sequence — `30` and `150` — is run against both the unit and
  integration setups, without recompiling the test.
- **TE19.** The test itself carries no assertion here, it only makes the calls and prints the
  result. This sharpens the "silent" result even further: even if an assertion had been added,
  that assertion would only look at the return value, and would stay unaware of the limit
  violation.
- **TE20.** The exception the real implementation throws is an unchecked exception. The
  reflective call wraps it inside `InvocationTargetException`; the test source unwraps this
  wrapper and rethrows the actual cause, otherwise the harness would only see the wrapper
  class's name.

Both requests are sent in the same order: 30 first, then 150. In the unit setup both return
without a problem — the fake implementation recognizes no limit, the line "request 150 ->
result 150" is written the same way as the others, and the program ends by printing "all
requests completed." The fact that a limit exists appears nowhere; the test passed because the
test was already unaware of the limit.

In the integration setup, the first request (30) returns the same way, but the second request
(150) exceeds `Inventory`'s limit and `Supplier.fill` throws an exception. This exception is
caught and rethrown in the test's own source, and then the helper that runs it catches it and
reports its name: `IllegalStateException`. The same two requests, in the same order, the same
test file — the only difference is which `Supplier` and which extra dependency stand on the
class path.

This exception's exit from the test's own source is two-layered, and this lesson deliberately
keeps these two layers apart. `Supplier.fill` throws an `IllegalStateException`; because the
test triggers this through reflection with the call `m.invoke(obj, request)`, reflection
itself first wraps this exception inside an `InvocationTargetException`. The test source
catches this wrapper with `catch (InvocationTargetException e)` and says `throw e.getCause()`,
exposing the actual exception and rethrowing it. Had this step been skipped, the helper that
runs it would only report the name "InvocationTargetException" — a name that is correct but
says little. Unwrapping the wrapper in the test's own source lets the reported name be
`IllegalStateException`, keeping what went wrong one step more visible.

These two results are the test topic's counterpart to the course's limiting reading: putting
the real type back does not make the test **more correct**, it only makes **a different defect
class** visible. The unit setup was already a valid setup — fast, isolated, independent of
whether a real limit exists. The integration setup does not invalidate it, it adds a layer on
top: it also brings the constraints the real implementation carries onto the class path. The
divergence that stayed "silent" in the unit setup — a request exceeding the limit passing
unnoticed — turns here into an exception, that is, a "run" defect. Both processed the same two
requests, in the same order; the difference between them was not in the assertions the test
wrote, but in the content of the class path the test ran on. Both setups can be correctly
built; what separates them is not the response they give to the same input, but **which
reality** that input is tested against.

The practical consequence of this distinction is that there is no single answer to which
setup is "enough." The unit setup quickly confirms whether a request like 150 is processed in
the normal flow; it keeps working even when `Inventory` itself changes or does not exist at
all, because it is not bound to it in any way. The integration setup, on the other hand,
confirms that `Inventory` really is called and really does enforce a limit, but the price of
this is including a second type on the class path and becoming dependent on that type's
behavior. Which question is being asked determines which setup is chosen; both test different
realities that carry the same name `Supplier`.

It is worth stressing that the two setups do not replace each other, and both remain
necessary. The unit setup is still valuable: if a heavy or slow dependency like `Inventory`
were really built on every run, testing the logic of the code that calls `Supplier` would
carry the cost of building that dependency every single time. The unit setup eliminates this
cost entirely, and in exchange only tests a world where `Inventory` does not exist. The
integration setup, on the other hand, accepts that cost and actually runs how the two types
behave together. A test suite usually keeps both together: a large number of unit setups run
quickly on every change, while a smaller number of integration setups confirm less often that
the real dependencies work together. What this lesson shows is that the two layers do **not**
test the same behavior twice — each carries a defect class the other cannot see.

At this point the class path measure turns into a concrete decision tool: instead of arguing
over whether a setup is "unit" or "integration," it is enough to look at which types enter
that setup's class path. If `Inventory` is on the class path, `Inventory`'s behavior is being
tested; if it is not, it is not being tested — the difference between the two is not a
statement of intent, it is a compiled fact, and it can be read directly with a count like
`typeCount`.

This measure also carries a warning: every real type added to the class path brings the world
the test sees a little closer to the world production sees, but never makes the two fully
equal. `Inventory` itself could also have depended on something else — it was not built that
way in this lesson, the limit itself is complete on its own. In a real system, the integration
setup also stops at its own boundary; past a certain point, bringing up the entire system under
test is the job of end-to-end testing, not integration testing, and that boundary is M21/K03's
subject.

#### Summary

- The unit setup carried a single type, the integration setup carried a second type the real
  implementation depends on along with it; the path's content rose from one to two.
- The same compiled test body ran, with the same two requests, against two separate runtime
  class paths without being recompiled.
- In the unit setup, the request exceeding the limit passed silently and the test said
  "completed"; no warning appeared anywhere.
- In the integration setup, the same request threw `IllegalStateException`; the defect was no
  longer silent, it was a run failure.
- Putting the real implementation back did not make the test more correct, it only made
  visible a defect class that was invisible in the unit setup.

#### Next Step

In this lesson, the test sent a number and read the returned value; the assertion lived
inside the body, in a form that was not prose. Some tests, however, approach the body not with
code that has no prose at all, but with text written in a scenario language — describing steps
with ordinary sentences and expecting those sentences to have a counterpart. This carries the
question of which class path the test sees toward yet another direction: what is now being
sought is no longer a mismatch between two code bases, but a mismatch between a prose text and
that text's executable counterpart. The next lesson treats such a text as a declaration, and
its question is: does every step in the text have an executable counterpart, and when some
steps do not have that counterpart, are they silently skipped.

### Behavior-Driven Testing

Source: https://academia.sh/en/courses/java-ecosystem/behavior-driven-testing

A four-line scenario text is run against a step map; a step with no counterpart in the map is silently skipped in one run and halts the program in another. Even when the same four steps match completely, running against a map whose check steps have had their assertion removed still says "passed" over a wrong value.

In the previous lesson, the system under test grew: a dependency mocked in the unit setup
came back with its real implementation, and the class path the test sees rose to two types. In
this lesson, the side that grows is different: it is not the code under test, it is the test
**itself** that moves closer to prose. A scenario is a list of steps written in ordinary
sentences — "10 units were added to the store," "the store quantity must be 7," and so on —
and these sentences are expected to have an executable counterpart in a step map. The question
is: how many of the text's four steps have a real counterpart, how does a step with no
counterpart affect the run, and does every step matching mean this scenario runs
**correctly**?

The appeal of a scenario language is that a non-technical reader can also understand the
steps — the sentence "10 units were added to the store" carries meaning even for someone who
does not know Java. But this readability carries a cost: prose is not a syntax the compiler
can check. When a method name is misspelled, the compiler stops immediately; when a scenario
line is misspelled or never implemented, only the run itself notices. This lesson shows, by
actually running, how that difference gets noticed and what it means when it does not.

The concept of behavior-driven development was established in the M21/K02 Unit Testing and
Test-Driven Development course and is not reintroduced here. The one question this lesson
adds is the **match** between the steps the scenario text declares and the steps that actually
exist in the step map.

This question is really a variation on the question this topic has been asking from the
start. In the first lesson the question was "does this type exist on the test class path,"
and the type was concrete, a compiled `.class` file. In this fifth lesson the type gives way
to a sentence: every line in the scenario text needs a behavior in the step map to answer for
it. The difference is not merely a naming issue — the compiler tells us whether a type is
present on the class path, but no one tells us whether a text line matches a behavior; this
lesson builds and counts that match by hand.

#### A Scenario Is a Declaration, the Step Map Provides Its Counterpart

- **TE21.** Scenario steps are matched here as plain strings (`Map<String, ...>`). Real
  frameworks' mechanism for pattern matching, defining steps with regular expressions, and
  extracting parameters is not built here.
- **TE22.** A step "matching" means its text is found with an exact key in the map. This shows
  that the step has an executable counterpart, not that the step runs **correctly** — the two
  are separate questions.
- **TE23.** Two separate run strategies, "silent" and "halting," are written by hand here.
  Which of the two real frameworks pick as a default, or how it is configured, is not modeled
  here.

The scenario text consists of four lines and is declared as an array of `record`s — the same
method the third lesson used to declare its data set, carrying prose steps here instead of
data. The step map, in turn, binds each line's text to an executable body. The `missingMap`
below contains no entry at all for the fourth step's text — a deliberate omission. `silentRun`
skips a step whose counterpart cannot be found and continues; `haltingRun` throws an exception
in the same situation and cuts the run short.

The map itself is a `LinkedHashMap` — not an ordinary map, but one that preserves insertion
order. This preserves the map's own **registration** order, not the scenario's order; the map
itself does not decide which order the steps run in, that decision is made by the order in the
`STEPS` array. The only practical benefit of order preservation here is being able to see, when
reading the two maps side by side, in which order the steps were defined — the run itself
walks the `STEPS` array from start to end and does not look at the map's internal order.

The step map itself is a natural extension of the test class path concept the first lesson
established. `missingMap` and `blankAssertionMap` appear nowhere in production code; both are
mappings known only to the test side and run only by the test side. The scenario text itself
is the same — the `STEPS` array is not data production code reads, it is a declaration the
test reads. What is new in this lesson is that this declaration stands in **prose** form, and
its counterpart is a string key, not a type name.

```java
// Scenario.java — a scenario text is a declaration; does a step with no counterpart in the step map stay silent or halt the run
import java.util.*;
import java.util.function.Consumer;

public class Scenario {

    record Step(String text) {}

    static final Step[] STEPS = {
        new Step("10 units were added to the store"),
        new Step("3 units were removed from the store"),
        new Step("the store quantity must be 7"),
        new Step("the store quantity must not drop below zero"),
    };

    // -- step map is missing: the fourth step has no record at all -------------------------
    static Map<String, Consumer<int[]>> missingMap() {
        Map<String, Consumer<int[]>> m = new LinkedHashMap<>();
        m.put("10 units were added to the store", state -> state[0] += 10);
        m.put("3 units were removed from the store", state -> state[0] -= 3);
        m.put("the store quantity must be 7", state -> { if (state[0] != 7) throw new AssertionError("quantity is not 7: " + state[0]); });
        return m;
    }

    // -- step map is complete: all four are registered, but the check steps' assertion is removed --
    static Map<String, Consumer<int[]>> blankAssertionMap() {
        Map<String, Consumer<int[]>> m = new LinkedHashMap<>();
        m.put("10 units were added to the store", state -> state[0] += 10);
        m.put("3 units were removed from the store", state -> state[0] -= 2); // deliberate bug: removes 2, not 3
        m.put("the store quantity must be 7", state -> {});                   // assertion removed
        m.put("the store quantity must not drop below zero", state -> {});    // assertion removed
        return m;
    }

    static void silentRun(Step[] scenario, Map<String, Consumer<int[]>> map, int[] state) {
        int matched = 0;
        for (Step s : scenario) {
            Consumer<int[]> action = map.get(s.text());
            if (action == null) { System.out.println("skipping     : " + s.text()); continue; }
            action.accept(state);
            matched++;
        }
        System.out.println("matched steps: " + matched + "/" + scenario.length);
        System.out.println("result       : passed, store=" + state[0]);
    }

    static void haltingRun(Step[] scenario, Map<String, Consumer<int[]>> map, int[] state) {
        int matched = 0;
        for (Step s : scenario) {
            Consumer<int[]> action = map.get(s.text());
            if (action == null) throw new IllegalStateException("no counterpart for step: " + s.text());
            action.accept(state);
            matched++;
        }
        System.out.println("matched steps: " + matched + "/" + scenario.length);
        System.out.println("result       : passed, store=" + state[0]);
    }

    public static void main(String[] args) {
        System.out.println("-- missing map, silent run --");
        silentRun(STEPS, missingMap(), new int[]{0});

        System.out.println("-- missing map, halting run --");
        try {
            haltingRun(STEPS, missingMap(), new int[]{0});
        } catch (IllegalStateException e) {
            System.out.println("stopped      : " + e.getMessage());
        }

        System.out.println("-- complete map, assertion removed from check steps --");
        silentRun(STEPS, blankAssertionMap(), new int[]{0});
    }
}
```

```
-- missing map, silent run --
skipping     : the store quantity must not drop below zero
matched steps: 3/4
result       : passed, store=7
-- missing map, halting run --
stopped      : no counterpart for step: the store quantity must not drop below zero
-- complete map, assertion removed from check steps --
matched steps: 4/4
result       : passed, store=8
```

The first two runs use the same `missingMap`, the same four-line scenario; the only difference
is the reaction given to the step whose counterpart cannot be found. `silentRun` skips the
fourth step, runs three steps, says "3/4" and finishes with "passed" — even though the scenario
text has four lines, the run actually executed only three of them. `haltingRun`, in contrast,
turns the same gap into an exception: which step's text was left without a counterpart appears
directly in the error message, and the run stops right there, with the remaining steps never
executed.

The difference between these two strategies is the scenario-text-level counterpart of the
silent/run-failure distinction seen in the course's earlier lessons. The silent strategy lets
a missing step pass unnoticed — a three-step scenario gets reported as if it were a four-step
scenario, and this is never visible at all unless the "3/4" line is read. The halting strategy,
on the other hand, turns the gap into a run failure and writes what is missing right into its
name. Neither is the "correct" strategy; one catches a misspelled step early, the other lets
the rest of the scenario still be seen running. The choice depends on who writes the scenario
text and how often that text changes.

The risk of the silent strategy is that missing the "3/4" line is not hard at all. Once a
scenario suite accumulates dozens of lines, reading the match ratio each run produces is not
realistic; most reports show only a "passed" or "failed" summary, not the match count. The
silent strategy can then carry a step that was never implemented, unnoticed, for months — the
text looks as though it declares a behavior that has never actually run. The halting strategy
removes this risk, but in exchange turns every small spelling difference (a missing space, a
changed number) into a run failure; if the text changes often, this reports text-maintenance
mistakes more than real defects. The two strategies coexisting is therefore not accidental:
which mistake gets caught early and which can be tolerated is an engineering decision that
depends on who writes the scenario text and how often.

#### A Full Step Count Does Not Show Correctness

- **TE24.** The deliberate bug — applying a reduction of 2 instead of 3 — is introduced only
  in the third run, to show the limiting measure. The first two runs use a correctly working
  reduction; only a single variable is changed so the three remain comparable.
- **TE25.** An "assertion-removed" step is modeled here with an empty body (`state -> {}`).
  The step has a counterpart in the map and the match is counted, only that counterpart
  checks nothing at all.

The third run departs from the first two in two respects. First, `blankAssertionMap` contains
all four of the four steps — no step is skipped, no exception is thrown, the matched step
count is "4/4." Second, the reduction step in this map is deliberately wrong: it reduces by 2
instead of 3. Had it worked correctly, the store quantity would be 7; in this run it stays at
8. In the first run, using `missingMap`, the step "the store quantity must be 7" carried a
real check and would have caught a wrong value. In `blankAssertionMap`, the same text stands in
the map, it matches, but its body is empty — it makes no comparison at all. The result line
again says "passed, store=8"; the word "passed" here means only that no exception was thrown,
not that the value is correct.

This third run sharing the same scenario text with the first two is a deliberate choice. The
text never changed — four lines, the same order, the same words. What changes is only the
implementation behind that text: in one version the third step carried a real numeric
comparison, in the other the same step does nothing. Someone reading the scenario text cannot
see this difference — the text carries the exact same line in both. The only way to see the
difference is to read the step map itself, or to run it and observe the real value; the text
itself never guarantees what the implementation behind it does.

This is the closing observation of the course's fifth topic: the matched step count being
full — every line of the scenario having a counterpart — does not show that the scenario
**runs correctly**. Every step of a text having a counterpart in the step map does not mean an
assertion is actually written inside that counterpart; an entry in the step map counts as a
fully valid match even with an empty body. "4/4 matched" and "the scenario ran correctly" are
two separate propositions, and this lesson shows the two apart within the same run: even when
the count is full, a bug can pass silently.

Placing these three runs side by side brings out the final form of the course's measuring
question. In the earlier lessons the question took the shape "which class path must this type
be found on," and the type was a concrete `.class` file. Here a **step** stands in place of
"type," a **step map** stands in place of "class path," but the question settles into the same
mold: does this step have a counterpart in the step map, and does that counterpart actually
check anything. A scenario text is a declaration; the step map is that declaration's
executable counterpart, and when a gap remains between the two — whether as a missing entry
or an empty body — that gap is seen not by reading the text, but only by running it.

Across this topic's five lessons, the same question was asked in five separate forms. In the
first lesson the question was whether a framework type was present on the test class path; in
the second, which of two implementations carrying the same name stood on the runtime class
path; in the third, which scope a setup ran in; in the fourth, whether a real dependency
entered the class path. In this last lesson, the question was reduced to whether a behavior
has a counterpart in the step map. What all five had in common was that the question was
answered not from the source text, but by actually running — what a type, a setup or a step
did was read not from how it was written, but from the result it produced when run.

A second pattern shared by all five lessons was that a "complete" result does not mean
sufficient. In the first, a class path carrying production and test types collapsed because
the framework type was missing; in the second, an error-free run carried a wrong number; in
the third, a passing row passed only because its position fit; in the fourth, a silent run
never saw a limit violation at all. This last lesson repeated the pattern through step count:
a "4/4" scenario could carry no real check behind it. This repetition is not accidental — it
is the course's own thesis: a compiled, running program that says "ok" cannot be read without
being told which question it answers.

#### Summary

- The scenario text was written as a four-step declaration; the step map provided these
  steps' executable counterparts.
- A step whose counterpart could not be found was silently skipped in one run (3/4 matched),
  and halted the run with an exception in another.
- When all four of the four steps matched ("4/4"), this alone did not show that the scenario
  ran correctly.
- A scenario run with a map whose check steps' assertion had been removed still said "passed"
  over a wrong value (8, expected 7).
- The concept of behavior-driven development was established in M21/K02; the one question
  this lesson added was the match between the scenario text and its counterpart in the step
  map.

#### Next Step

In this topic's five lessons, we built the class path the test sees ourselves: we knew from
the start which type, which step would stand on that path, because we wrote all of it with
our own hands — the side writing the test decided both the fake type's name and the step map's
keys. The next topic asks the exact opposite. A logging call never writes the name of the
implementation that will answer it — its source text addresses only a facade, and never states
which class will answer that call. How that name enters the runtime class path, who puts it
there, and which one gets chosen when two implementations are present at once, is the next
topic's question.

## Logging and Other JVM Languages

The trace the logging facade leaves in the caller's class file and the implementation's entry onto the runtime path through a declaration, the effect of log levels and structured records on readability in production, and other languages sharing the same runtime meeting on the same path through the class file format.

### Logging Facade and Implementation

Source: https://academia.sh/en/courses/java-ecosystem/logging-facade-and-implementation

The measurement reads the caller's class file's constant pool to show that it references the facade but never the implementation; the implementation enters the runtime class path through a service declaration, and when two implementations are present, the class path order decides which one is selected. The limiting measurement shows that when no implementation is found at all, the caller still compiles and runs, but produces no records.

We built the class path the test saw and knew exactly what to put on it: which fake type
stood in for which real type, which setup added which dependency to the test class path, all
written step by step by us. In this lesson, the writing side changes. The caller compiles
against an interface and never writes the name of the class that will stand behind it — it
does not import it, does not instantiate it, does not even mention it as a string. Yet when
the program runs, that nameless class is found and called. The question is: how does a name
that was never written in the source enter the runtime class path, and when more than one
candidate is present there, what decides which one wins?

This topic covers logging across three lessons, but this first lesson is not concerned with
keeping a log itself — it is concerned with the dependency direction that a logging facade
sets up. A facade — here, a single-method interface — becomes the only type the caller
speaks to; the implementation behind it never becomes visible to the caller. The measurement
below builds this in four steps: first, that compilation passes without ever seeing the
implementation and that the class file does not reference it; then, by what kind of
declaration the implementation enters the runtime class path; then, when more than one
implementation is present, what decides the winner; and finally, what happens when no
implementation is present at all.

#### Only the Facade Is on the Compile Class Path

**LO1.** The caller's source text writes only the name of the facade interface; the names of
the implementation classes never appear in the source, so the compiler has no need to see
them. **LO2.** The class file's constant pool carries only the types whose names appear in
the source; a name never written in the source is absent from the constant pool as well.

The rest of the measurement repeats four tasks: writing a source file to disk, compiling it
against a given class path, running a compiled class with a given class path, and reading
which types are named in a class file's constant pool. These four are gathered in the helper
class below; the topic's remaining two lessons use the same four.

```java
// Tool.java — shared routines for writing sources, compiling, running and reading class files (helper class, no main)
import java.io.*;
import java.lang.classfile.*;
import java.lang.classfile.constantpool.*;
import java.net.*;
import java.nio.file.*;
import java.util.*;
import java.util.spi.ToolProvider;

class Tool {
    static Path write(Path dir, String name, String source) throws IOException {
        Files.createDirectories(dir);
        Path p = dir.resolve(name + ".java");
        Files.writeString(p, source);
        return p;
    }

    static boolean compile(Path output, List<Path> classPath, List<Path> sources) throws IOException {
        Files.createDirectories(output);
        List<String> args = new ArrayList<>(List.of("-d", output.toString()));
        if (!classPath.isEmpty()) {
            StringJoiner sj = new StringJoiner(File.pathSeparator);
            for (Path p : classPath) sj.add(p.toString());
            args.add("-classpath");
            args.add(sj.toString());
        }
        for (Path s : sources) args.add(s.toString());
        ToolProvider compiler = ToolProvider.findFirst("javac").orElseThrow();
        StringWriter silentOutput = new StringWriter();
        int result = compiler.run(new PrintWriter(silentOutput), new PrintWriter(silentOutput),
                args.toArray(new String[0]));
        return result == 0;
    }

    static void run(List<Path> classPath, String mainClass) throws Exception {
        URL[] url = new URL[classPath.size()];
        for (int i = 0; i < url.length; i++) url[i] = classPath.get(i).toUri().toURL();
        try (URLClassLoader loader = new URLClassLoader(url, Tool.class.getClassLoader().getParent())) {
            Class<?> cls = Class.forName(mainClass, true, loader);
            cls.getMethod("main", String[].class).invoke(null, (Object) new String[0]);
        }
    }

    static Object field(List<Path> classPath, String mainClass, String fieldName) throws Exception {
        URL[] url = new URL[classPath.size()];
        for (int i = 0; i < url.length; i++) url[i] = classPath.get(i).toUri().toURL();
        try (URLClassLoader loader = new URLClassLoader(url, Tool.class.getClassLoader().getParent())) {
            Class<?> cls = Class.forName(mainClass, true, loader);
            cls.getMethod("main", String[].class).invoke(null, (Object) new String[0]);
            return cls.getField(fieldName).get(null);
        }
    }

    static Set<String> references(Path classFile) throws IOException {
        Set<String> names = new TreeSet<>();
        ClassModel model = ClassFile.of().parse(classFile);
        for (PoolEntry e : model.constantPool()) {
            if (e instanceof ClassEntry ce) names.add(ce.asInternalName().replace('/', '.'));
        }
        return names;
    }

    static void show(String label, Object value) {
        System.out.printf("%-62s: %s%n", label, value);
    }

    static void declare(Path dir, String service, String provider) throws IOException {
        Path d = dir.resolve("META-INF").resolve("services");
        Files.createDirectories(d);
        Files.writeString(d.resolve(service), provider + "\n");
    }

    static String facadeSource() {
        return "public interface LoggingFacade { void write(String message); int recordCount(); }";
    }

    static String callerSource() {
        return "import java.util.ServiceLoader;\n"
             + "public class Caller {\n"
             + "  static final class Silent implements LoggingFacade {\n"
             + "    public void write(String message) { }\n"
             + "    public int recordCount() { return 0; }\n"
             + "  }\n"
             + "  public static String selectedClass = \"none\";\n"
             + "  public static int recordsProduced = -1;\n"
             + "  static LoggingFacade find() {\n"
             + "    for (LoggingFacade g : ServiceLoader.load(LoggingFacade.class, Caller.class.getClassLoader())) return g;\n"
             + "    return new Silent();\n"
             + "  }\n"
             + "  public static void main (String[] a) {\n"
             + "    LoggingFacade g = find();\n"
             + "    selectedClass = g.getClass().getSimpleName();\n"
             + "    g.write(\"start\"); g.write(\"progress\"); g.write(\"end\");\n"
             + "    recordsProduced = g.recordCount();\n"
             + "  }\n"
             + "}";
    }

    static String implementationSource(String name) {
        return "public class " + name + " implements LoggingFacade {\n"
             + "  private int counter = 0;\n"
             + "  public void write(String message) { counter++; }\n"
             + "  public int recordCount() { return counter; }\n"
             + "}";
    }
}
```

`callerSource()` produces the caller's source: `Caller` imports only the `LoggingFacade`
interface and `ServiceLoader`, and writes the name of no implementation class. If it finds
none, it falls back to the `Silent` inner class it defines itself — this, too, is an
implementation, but it lives in the caller's own source, not brought in from outside.

```java
// OnlyFacade.java — does the caller compile when only the facade is on the compile class path, and which types does the class file reference
import java.nio.file.*;
import java.util.*;

public class OnlyFacade {
    public static void main(String[] args) throws Exception {
        Path root = Files.createTempDirectory("gl1");
        Path source = root.resolve("source");

        Path sFacade = Tool.write(source, "LoggingFacade", Tool.facadeSource());
        Path sCaller = Tool.write(source, "Caller", Tool.callerSource());
        Path sImplA = Tool.write(source, "ImplementationA", Tool.implementationSource("ImplementationA"));
        Path sImplB = Tool.write(source, "ImplementationB", Tool.implementationSource("ImplementationB"));

        Path facadeDir = root.resolve("facade");
        Path callerDir = root.resolve("caller");
        Path dirA = root.resolve("recordA");
        Path dirB = root.resolve("recordB");

        Tool.compile(facadeDir, List.of(), List.of(sFacade));
        Tool.compile(dirA, List.of(facadeDir), List.of(sImplA));
        Tool.compile(dirB, List.of(facadeDir), List.of(sImplB));
        boolean callerCompiled = Tool.compile(callerDir, List.of(facadeDir), List.of(sCaller));

        Set<String> refs = Tool.references(callerDir.resolve("Caller.class"));

        Tool.show("did the caller compile with no implementation on the compile class path", callerCompiled);
        Tool.show("does the caller's class file reference LoggingFacade", refs.contains("LoggingFacade"));
        Tool.show("does the caller's class file reference ImplementationA", refs.contains("ImplementationA"));
        Tool.show("does the caller's class file reference ImplementationB", refs.contains("ImplementationB"));
    }
}
```

```
did the caller compile with no implementation on the compile class path: true
does the caller's class file reference LoggingFacade          : true
does the caller's class file reference ImplementationA        : false
does the caller's class file reference ImplementationB        : false
```

`ImplementationA` and `ImplementationB` really exist in this measurement: both are compiled,
and both class files sit on disk. Even `callerDir`'s compile class path does not include
them — only `facadeDir` is there — and `Caller` still compiles (LO1). When the constant pool
is read, `LoggingFacade` is there, but `ImplementationA` and `ImplementationB` are not (LO2).
This is not a guess: a name never written in the source appears in none of the instructions
the compiler could produce, because a class file is a translation of the source and invents
nothing the source did not know. This is exactly where a facade's whole power lies: the
caller does not even need to know, at compile time, whether the class that will stand behind
it exists at all.

#### The Implementation Enters the Path via a Declaration

**LO3.** The implementation enters the runtime class path through a service declaration
file: a file named `META-INF/services/<facade-interface's-full-name>` carries the provider
class's name as a line of text. **LO4.** `ServiceLoader.load(...)` reads this file to find
and instantiate the provider; the caller never calls the provider's constructor anywhere in
its own code.

```java
// ServiceDeclaration.java — the implementation enters the path through a service declaration, the caller finds it with ServiceLoader
import java.nio.file.*;
import java.util.*;

public class ServiceDeclaration {
    public static void main(String[] args) throws Exception {
        Path root = Files.createTempDirectory("gl2");
        Path source = root.resolve("source");

        Path sFacade = Tool.write(source, "LoggingFacade", Tool.facadeSource());
        Path sCaller = Tool.write(source, "Caller", Tool.callerSource());
        Path sImplA = Tool.write(source, "ImplementationA", Tool.implementationSource("ImplementationA"));

        Path facadeDir = root.resolve("facade");
        Path callerDir = root.resolve("caller");
        Path dirA = root.resolve("recordA");

        Tool.compile(facadeDir, List.of(), List.of(sFacade));
        Tool.compile(callerDir, List.of(facadeDir), List.of(sCaller));
        Tool.compile(dirA, List.of(facadeDir), List.of(sImplA));
        Tool.declare(dirA, "LoggingFacade", "ImplementationA");

        Object selectedBefore = Tool.field(List.of(callerDir, facadeDir), "Caller", "selectedClass");
        Tool.show("class the caller falls back to when no implementation is on the runtime path", selectedBefore);

        List<Path> runtimePathAfter = List.of(callerDir, facadeDir, dirA);
        Object selectedAfter = Tool.field(runtimePathAfter, "Caller", "selectedClass");
        Object recordsAfter = Tool.field(runtimePathAfter, "Caller", "recordsProduced");
        Tool.show("class selected once the implementation enters the path through a declaration", selectedAfter);
        Tool.show("number of records produced", recordsAfter);
    }
}
```

```
class the caller falls back to when no implementation is on the runtime path: Silent
class selected once the implementation enters the path through a declaration: ImplementationA
number of records produced                                    : 3
```

The `dirA` directory carries two things: `ImplementationA.class` and a declaration file
named `META-INF/services/LoggingFacade` that holds the single line `ImplementationA`. Once
`dirA` is added to the runtime class path, the `ServiceLoader.load` call inside `Caller`
reads this file, loads `ImplementationA`, and instantiates it — without a single line of code
ever writing `new ImplementationA()` (LO3, LO4). All three `write` calls reach this instance,
and the counter climbs to three. Once `dirA` is removed from the runtime class path, the same
`Caller.class`, without any recompilation, falls back to its own `Silent` again. The only
thing that changes is the content of the runtime class path; the `Caller.class` file is the
exact same byte sequence in both cases.

#### With Two Implementations, the Path Order Decides

**LO5.** When more than one provider is declared for the same service on the runtime class
path, the first provider `ServiceLoader` returns depends on the order of the class path
entries. **LO6.** When the order changes, the selected implementation changes, but neither
compilation nor execution fails — both end with an "OK" result.

```java
// OrderWinner.java — with two implementations on the runtime class path, the path order decides the winner
import java.nio.file.*;
import java.util.*;

public class OrderWinner {
    public static void main(String[] args) throws Exception {
        Path root = Files.createTempDirectory("gl3");
        Path source = root.resolve("source");

        Path sFacade = Tool.write(source, "LoggingFacade", Tool.facadeSource());
        Path sCaller = Tool.write(source, "Caller", Tool.callerSource());
        Path sImplA = Tool.write(source, "ImplementationA", Tool.implementationSource("ImplementationA"));
        Path sImplB = Tool.write(source, "ImplementationB", Tool.implementationSource("ImplementationB"));

        Path facadeDir = root.resolve("facade");
        Path callerDir = root.resolve("caller");
        Path dirA = root.resolve("recordA");
        Path dirB = root.resolve("recordB");

        Tool.compile(facadeDir, List.of(), List.of(sFacade));
        Tool.compile(callerDir, List.of(facadeDir), List.of(sCaller));
        Tool.compile(dirA, List.of(facadeDir), List.of(sImplA));
        Tool.compile(dirB, List.of(facadeDir), List.of(sImplB));
        Tool.declare(dirA, "LoggingFacade", "ImplementationA");
        Tool.declare(dirB, "LoggingFacade", "ImplementationB");

        Object aFirst = Tool.field(List.of(callerDir, facadeDir, dirA, dirB), "Caller", "selectedClass");
        Object bFirst = Tool.field(List.of(callerDir, facadeDir, dirB, dirA), "Caller", "selectedClass");

        Tool.show("selected class when A is given first on the runtime class path", aFirst);
        Tool.show("selected class when B is given first on the runtime class path", bFirst);
        Tool.show("did both run without error and differ only by order", !aFirst.equals(bFirst));
    }
}
```

```
selected class when A is given first on the runtime class path: ImplementationA
selected class when B is given first on the runtime class path: ImplementationB
did both run without error and differ only by order           : true
```

`dirA` and `dirB` are two directories unaware of each other; each carries its own class and
its own service declaration. When the runtime class path is set up in the order
`[callerDir, facadeDir, dirA, dirB]`, `ServiceLoader` reaches `dirA`'s declaration first and
returns `ImplementationA`; when the same two directories are given in the order `dirB, dirA`,
the winner becomes `ImplementationB` (LO5). `Caller`'s source, the `LoggingFacade` interface,
and the two implementations themselves — none of these changed between the two runs; only the
order of the list changed. Nowhere in the code is there a conflict warning or an error (LO6):
finding two providers falls into the same pattern as K05's shared definition — "when two
implementations are found on the runtime class path, the selection changes with the path
order, with no error" — seen once more here at the interface level.

#### When No Implementation Is Found

**LO7.** When no provider is found on the runtime class path, `ServiceLoader.load(...)`
returns an empty iterator; the caller meets this not with an exception, but with a default it
defines itself. **LO8.** In this case the program runs without error, but because no
implementation increments `recordCount()`, the number of records produced is zero.

```java
// NoImplementation.java — with no implementation on the runtime path at all, the caller runs without error but produces no records
import java.nio.file.*;
import java.util.*;

public class NoImplementation {
    public static void main(String[] args) throws Exception {
        Path root = Files.createTempDirectory("gl4");
        Path source = root.resolve("source");

        Path sFacade = Tool.write(source, "LoggingFacade", Tool.facadeSource());
        Path sCaller = Tool.write(source, "Caller", Tool.callerSource());

        Path facadeDir = root.resolve("facade");
        Path callerDir = root.resolve("caller");
        Tool.compile(facadeDir, List.of(), List.of(sFacade));
        Tool.compile(callerDir, List.of(facadeDir), List.of(sCaller));

        List<Path> runtimePath = List.of(callerDir, facadeDir);
        boolean ranWithoutError;
        try {
            Tool.run(runtimePath, "Caller");
            ranWithoutError = true;
        } catch (Exception e) {
            ranWithoutError = false;
        }
        Object selected = Tool.field(runtimePath, "Caller", "selectedClass");
        Object records = Tool.field(runtimePath, "Caller", "recordsProduced");

        Tool.show("does it run without error when no implementation is on the runtime path", ranWithoutError);
        Tool.show("class fallen back to", selected);
        Tool.show("number of records produced", records);
    }
}
```

```
does it run without error when no implementation is on the runtime path: true
class fallen back to                                          : Silent
number of records produced                                    : 0
```

This measurement's runtime class path has neither `ImplementationA` nor `ImplementationB` —
only the facade and the caller are there. The program still completes, throwing no exception
(LO7), and `recordsProduced` stays at zero (LO8). This is the lesson's limiting measurement:
the facade does not remove the dependency between caller and implementation, it reverses its
direction. When no implementation exists at all, the caller sees this not as an error but as
a gap, and fills it with its own `Silent` fallback. The number of records produced being zero
is not the sign of a defect here; it is the outcome of a rule the caller wrote in its own
source. A logging facade's real-world implementations carry the same promise: when no
implementation is wired in, the program does not crash, it just stays silent. This is the
real guarantee that the facade mechanism gives the caller — the caller itself can be written
without knowing which implementation will enter the runtime class path, because the worst
case is "no records at all," not "the program does not run." This guarantee carries a cost
as well: when the runtime class path is set up incorrectly, there is no error message
anywhere, so noticing the records that are missing is far harder than noticing a line the
compiler rejected.

#### Summary

- The caller's source text writes only the name of the facade interface; the caller compiles
  even when no implementation at all is present on the compile class path.
- The class file's constant pool carries the types written in the source: `LoggingFacade` is
  there, but `ImplementationA` and `ImplementationB`, which never appear in the source, are
  not.
- The implementation enters the runtime class path through a service declaration file;
  `ServiceLoader` reads this declaration to find and instantiate the provider without its
  name ever appearing in a single line of the caller.
- When more than one provider is declared on the runtime class path, the order of the class
  path entries decides the winner; the selection changes when the order changes, but no
  error occurs.
- Limiting measurement: when no implementation is found at all, the caller still compiles and
  runs without error, but produces no records — the facade does not remove the dependency, it
  reverses its direction.

#### Next Step

What this lesson measured was how a name enters the path: through a declaration, through
order, or not at all. The next lesson holds the same set of calls fixed and turns the one
thing that changes into a threshold — when the same three calls are run against two different
thresholds, how many of them actually turn into a record, and does a call that falls below
the threshold still carry a cost?

### Log Levels and Structured Logging

Source: https://academia.sh/en/courses/java-ecosystem/log-levels-and-structured-logging

How many records the same set of calls produces at two different thresholds is measured by running it, showing that the threshold is decided at runtime, not at the call site; the lesson demonstrates that a structured record's readability is measured not by field count but by whether the same field is present in every record. The limiting measurement shows that a call below the threshold is still evaluated when written with string concatenation, but not when written with a Supplier.

The previous lesson measured how a name enters the path: through a declaration, through
order, or not at all. This lesson holds the same set of calls fixed and turns the one thing
that changes into a threshold. The facade carried a logging call to the implementation; the
question now is who decides whether that call actually turns into a record. Is it the call
site, or a setting given by whoever runs the program?

The concepts of log level and structured logging are not new in this course — Observability
and Operations and Performance Anti-Patterns and Monitoring both established them as
concepts: that levels form an ordered scale, and how a structured record differs from free
text. This lesson does not redefine those concepts; the only thing it asks is how these two
concepts behave at runtime in Java's standard logging library. The measurement is done with
`java.util.logging` — not a third-party logging implementation, but the language's own
standard library. The facade/implementation distinction from the previous lesson still holds
here: a call site writing `logger.info(...)` is no guarantee that the call will turn into a
record. The difference is that the previous lesson looked at the question "was an
implementation found," while this lesson looks at "did it pass the level threshold" — both
are answers the call site does not know, given by the runtime setup.

#### The Threshold Is Decided at Runtime, Not at the Call Site

**LO9.** Whether a log call is processed is decided by comparing the level the call carries
against the threshold currently assigned to the logger. **LO10.** This comparison happens at
runtime; the same compiled set of calls produces a different number of records depending on
the threshold given at execution time.

```java
// ThresholdCounter.java — how many records the same set of calls produces at two different runtime thresholds
import java.util.logging.*;
import java.util.ArrayList;
import java.util.List;

public class ThresholdCounter {
    static class CountingHandler extends Handler {
        List<String> published = new ArrayList<>();
        public void publish(LogRecord r) { published.add(r.getLevel() + " " + r.getMessage()); }
        public void flush() { }
        public void close() { }
    }

    static int run(Level threshold) {
        Logger logger = Logger.getLogger("ThresholdTrial-" + threshold);
        logger.setUseParentHandlers(false);
        CountingHandler h = new CountingHandler();
        h.setLevel(Level.ALL);
        logger.addHandler(h);
        logger.setLevel(threshold);

        logger.fine("detail");
        logger.info("progress");
        logger.warning("warning");
        logger.severe("critical");

        return h.published.size();
    }

    public static void main(String[] args) {
        int infoThresholdCount = run(Level.INFO);
        int warningThresholdCount = run(Level.WARNING);

        System.out.println("four calls: fine, info, warning, severe");
        System.out.println("records produced at threshold INFO   : " + infoThresholdCount);
        System.out.println("records produced at threshold WARNING: " + warningThresholdCount);
    }
}
```

```
four calls: fine, info, warning, severe
records produced at threshold INFO   : 3
records produced at threshold WARNING: 2
```

The `run` method makes exactly the same four calls in both cases: `fine`, `info`, `warning`,
`severe`. The only thing that changes is the value given to the `logger.setLevel(threshold)`
line. At the `INFO` threshold, the `fine` call is filtered out; the remaining three (`info`,
`warning`, `severe`) reach the handler (LO9). When the threshold is raised to `WARNING`,
`info` is filtered out too; only two remain. Both runs use the same class file — there is no
recompilation at all — and produce a different number of records solely from a value given
at execution time (LO10). The `logger.info(...)` line at the call site knows neither whether
`INFO` will pass nor whether it will not; the party that knows this is the runtime setup that
assigns the threshold to the logger.

**LO11.** The threshold is not a single setting: the logger's own threshold and the
threshold of each handler attached to it are applied separately; for a record to be
published, it must pass both.

```java
// TwoTierThreshold.java — the logger and the handler have separate thresholds, a record must pass both
import java.util.logging.*;
import java.util.ArrayList;
import java.util.List;

public class TwoTierThreshold {
    static class FilteringHandler extends Handler {
        List<String> published = new ArrayList<>();
        public void publish(LogRecord r) {
            if (!isLoggable(r)) return;
            published.add(r.getLevel() + " " + r.getMessage());
        }
        public void flush() { }
        public void close() { }
    }

    static int run(Level loggerThreshold, Level handlerThreshold) {
        Logger logger = Logger.getLogger("TwoTier-" + loggerThreshold + "-" + handlerThreshold);
        logger.setUseParentHandlers(false);
        FilteringHandler h = new FilteringHandler();
        h.setLevel(handlerThreshold);
        logger.addHandler(h);
        logger.setLevel(loggerThreshold);

        logger.info("progress");
        logger.warning("warning");
        logger.severe("critical");

        return h.published.size();
    }

    public static void main(String[] args) {
        System.out.println("three calls: info, warning, severe");
        System.out.println("logger ALL, handler ALL     -> produced: " + run(Level.ALL, Level.ALL));
        System.out.println("logger ALL, handler WARNING -> produced: " + run(Level.ALL, Level.WARNING));
        System.out.println("logger WARNING, handler ALL -> produced: " + run(Level.WARNING, Level.ALL));
    }
}
```

```
three calls: info, warning, severe
logger ALL, handler ALL     -> produced: 3
logger ALL, handler WARNING -> produced: 2
logger WARNING, handler ALL -> produced: 2
```

All three setups make the same three calls. When both the logger and the handler have their
threshold at `ALL`, all three pass. When only one of the two is raised to `WARNING` — it does
not matter which — the result is the same: two records (LO11). Reading the threshold as
belonging only to the logger would be wrong; if `FilteringHandler.publish` did not run its
own `isLoggable` check, the handler's `setLevel` call would filter nothing, and the last two
lines would also produce three records. The threshold being decided at runtime is therefore
not a single number, but a decision made independently by every link in the chain. This has
a practical consequence when more than one handler is attached to a logger: the same set of
records can appear in one handler and not in another, because each handler applies its own
threshold on its own, and the logger's threshold is only the first filter.

#### Structured Records: Field Consistency, Not Field Count

**LO12.** A structured record's field names are not chosen arbitrarily at the call site;
they sit in a fixed declaration the producing side commits to, and the reading side knows
which field to look for by consulting that declaration. **LO13.** A log stream's readability
is measured not by the number of fields a single record carries, but by whether the same
field is present in every record.

The measurement below compares two designs. In the first, every record carries the same two
fields: event and count. In the second, call sites act on their own: some add an extra source
field, some never write the count field at all. The formatter is the same in both — the
record is already built as full text at the call site, and the formatter only passes it
along without adding a time field (the ban on machine-dependent data applies here too: no
timestamp is ever printed).

```java
// FieldConsistency.java — readability is measured not by total field count but by whether the same field is present in every record
import java.util.logging.*;
import java.util.ArrayList;
import java.util.List;

public class FieldConsistency {
    static class FieldFormatter extends Formatter {
        public String format(LogRecord r) { return r.getMessage() + System.lineSeparator(); }
    }

    static class CollectingHandler extends Handler {
        List<String> records = new ArrayList<>();
        Formatter b = new FieldFormatter();
        public void publish(LogRecord r) { records.add(b.format(r)); }
        public void flush() { }
        public void close() { }
    }

    static boolean hasBothFields(String record) {
        return record.contains("event=") && record.contains("count=");
    }

    static int totalFieldCount(List<String> records) {
        int total = 0;
        for (String r : records) {
            for (String part : r.trim().split(" ")) if (part.contains("=")) total++;
        }
        return total;
    }

    public static void main(String[] args) {
        List<String> consistent = new ArrayList<>();
        for (int i = 1; i <= 5; i++) consistent.add("event=order count=" + i);

        List<String> inconsistent = List.of(
                "event=order count=1 source=web",
                "event=order count=2 source=web",
                "event=order count=3 source=mobile",
                "event=order",
                "event=order");

        long consistentComplete = consistent.stream().filter(FieldConsistency::hasBothFields).count();
        long inconsistentComplete = inconsistent.stream().filter(FieldConsistency::hasBothFields).count();

        System.out.println("consistent design   - total fields: " + totalFieldCount(consistent)
                + ", records carrying both fields: " + consistentComplete + "/" + consistent.size());
        System.out.println("inconsistent design - total fields: " + totalFieldCount(inconsistent)
                + ", records carrying both fields: " + inconsistentComplete + "/" + inconsistent.size());
    }
}
```

```
consistent design   - total fields: 10, records carrying both fields: 5/5
inconsistent design - total fields: 11, records carrying both fields: 3/5
```

`FieldFormatter` and `CollectingHandler` are not doing the real work in this measurement —
they collect the record as is, because the real difference is built at the call site. The
inconsistent design's total field count is higher than the consistent one's: 11 against 10
(LO12's obligation to follow a declaration does not apply here; each call site makes its own
decision). Even so, a parser that looks for the count field in every record reads all five of
the five records in the consistent design, but only three of them in the inconsistent design
(LO13). Two records carry only event; these records have no count, and for the parser,
"absent" and "zero" are not the same thing. Adding an extra field — source, in the third and
fourth records — does not increase parseability; it only reduces how many fields are
missing. What makes a log stream machine-readable is that every record carries the same
fixed set of fields, not which fields belong to that set.

This distinction's practical consequence shows up in the behavior of whatever later
processes a log stream. A reader that wants to compute a total over the count field across
thousands of records can safely process all five of the five records in the consistent
stream; in the inconsistent stream, it has to either skip two records or treat them as a
special case. The field count changing from record to record is not a bug — no exception is
thrown anywhere in the `FieldConsistency` example, no call is rejected — but the reader's
inability to assume every field is present forces it to read that stream by hand, record by
record. This is where structured logging's real payoff lies: tying field names to a fixed
declaration frees the reader from that assumption.

#### Limiting Measurement: A Call Below the Threshold Is Not Free

**LO14.** The expression carried by a call that falls below the threshold is evaluated
regardless of level when the call is written with string concatenation. **LO15.** When the
same call is written with a Supplier, the expression is evaluated only while the level is
enabled; for calls that fall below the threshold, the evaluation never runs at all.

The previous two measurements showed that the threshold decides how many records are
produced. From this it looks as if the conclusion "a call below the threshold is free" could
be drawn, but that is wrong: the threshold decides only whether the record is published, not
whether the argument given to the call is computed — the timing of that computation is a
matter of Java's method-invocation rule, not of the logging framework.

```java
// CostlyCall.java — the argument of a call below the threshold is always evaluated with string concatenation, only while the level is enabled with a Supplier
import java.util.logging.*;

public class CostlyCall {
    static int evaluatedExpressions = 0;

    static String expensiveExpression() {
        evaluatedExpressions++;
        return "value=" + evaluatedExpressions;
    }

    public static void main(String[] args) {
        Logger logger = Logger.getLogger("CostTrial");
        logger.setUseParentHandlers(false);
        logger.addHandler(new Handler() {
            public void publish(LogRecord r) { }
            public void flush() { }
            public void close() { }
        });
        logger.setLevel(Level.WARNING);

        evaluatedExpressions = 0;
        for (int i = 0; i < 5; i++) {
            logger.fine("detail: " + expensiveExpression());
        }
        int eagerEvaluationCount = evaluatedExpressions;

        evaluatedExpressions = 0;
        for (int i = 0; i < 5; i++) {
            logger.log(Level.FINE, () -> "detail: " + expensiveExpression());
        }
        int lazyEvaluationCount = evaluatedExpressions;

        System.out.println("the logger's threshold is WARNING, all five calls are at level FINE");
        System.out.println("expressions evaluated with string concatenation (eager) : " + eagerEvaluationCount);
        System.out.println("expressions evaluated with a Supplier (lazy)            : " + lazyEvaluationCount);
    }
}
```

```
the logger's threshold is WARNING, all five calls are at level FINE
expressions evaluated with string concatenation (eager) : 5
expressions evaluated with a Supplier (lazy)            : 0
```

The logger's threshold is `WARNING`; all five calls are at the lower level `FINE`, and none
of them turns into a record. Even so, on the line
`logger.fine("detail: " + expensiveExpression())`, `expensiveExpression()` runs all five
times, and the counter climbs to five (LO14): Java computes a method's arguments before
calling it, and this order does not change even when the call itself will fall below the
threshold. The second loop makes the same five calls with the `Logger.log(Level,
Supplier<String>)` overload; this overloaded form calls the Supplier only if the level is
enabled — for all five calls that fall below the threshold, `get()` never runs at all, and
the counter stays at zero (LO15). Both forms use the same four levels and the same
threshold; the only difference is when the argument is computed. What this lesson measures
is not a duration — no time is measured anywhere — only the number of expressions evaluated.

This difference comes from Java's method-invocation rule, not from a shortcoming of the
logging framework: before `logger.fine(message)` is called, the value of the message
expression must be ready, because Java computes a call's arguments before the call itself
happens — this order is independent of whether the `fine` method performs a threshold check
internally. A Supplier does not change this order; it only hides the computation behind
another method — `get()` — and moves the decision of whether to call that method into the
`fine` method, after the threshold check. The facade itself is open to the same choice: if a
logging facade offers the caller only a signature that takes a Supplier, the expression
never runs at all when the implementation rejects the threshold; if the facade offers only a
signature that takes a string, the caller cannot escape paying that cost no matter how
carefully it writes the call. This shows that a facade's choice of which methods to offer,
not only which methods to call, is a measurable consequence too.

#### Summary

- Whether a log call turns into a record is decided by comparing the call's level against
  the threshold assigned to the logger at runtime; the same compiled set of calls produces a
  different number of records at different thresholds.
- A structured record's field names form a fixed set the producing side commits to in a
  declaration; the reading side knows which field to look for by consulting that set.
- A log stream's readability is measured not by total field count but by whether the same
  field is present in every record; adding extra fields does not compensate for a field that
  is missing.
- Limiting measurement: the argument of a call below the threshold is still computed when
  written with string concatenation; it is computed only while the level is enabled when
  written with a Supplier.
- What is measured is not a duration but a count: how many records were produced, how many
  fields were consistent, how many expressions were evaluated.

#### Next Step

Everything measured across these two lessons lived inside the same runtime: the same
virtual machine, the same class file format, sources coming from a single source language.
The next lesson removes this last assumption: is a class file that enters the runtime class
path met the same way even when its source was never written in the Java language at all,
and does the caller notice?

### Other JVM Languages

Source: https://academia.sh/en/courses/java-ecosystem/other-jvm-languages

It is shown by running that a class produced directly in class file format, with no source text, is loaded and called just like a class compiled with javac, as long as it implements an interface; the caller's class file references the name of neither the real nor the generated implementation. The limiting measurement shows that a generated class file can carry a member name with no counterpart in Java syntax, and that this member can only be called through reflection.

Everything measured across these two lessons lived inside the same runtime: the facade, the
implementation, and the logger were all classes whose source was Java. This lesson removes
that last assumption: does a class file that enters the runtime class path get treated the
same way when its source was never written in Java, and does the caller notice? The question
that carries the whole course is asked here one last time: the runtime class path never
knows which language produced the thing placed on it, only whether it matches the class file
format. This lesson builds that as a run, not a claim: it produces a class file directly in
class file format without going through javac, places it on the runtime class path, and
shows a Java-written caller calling it. No language name, vendor name, or product name
appears — the topic is framed as "another producer of the same class file format."

#### Producing a Class File Without Source Text

**LO16.** Writing source text and compiling it is not the only way to produce a class file;
a procedure that directly produces the class file format can also produce one the virtual
machine will accept. **LO17.** A class file produced this way is loaded and called just like
a class compiled from source, as long as it declares an interface and carries the signature
that interface requires.

```java
// Tool.java — shared routines for writing sources, compiling, reading the constant pool and producing sourceless class files (helper class, no main)
import java.io.*;
import java.lang.classfile.*;
import java.lang.classfile.constantpool.*;
import java.lang.constant.*;
import java.net.*;
import java.nio.file.*;
import java.util.*;
import java.util.spi.ToolProvider;

class Tool {
    static Path write(Path dir, String name, String source) throws IOException {
        Files.createDirectories(dir);
        Path p = dir.resolve(name + ".java");
        Files.writeString(p, source);
        return p;
    }

    static boolean compile(Path output, List<Path> classPath, List<Path> sources) throws IOException {
        Files.createDirectories(output);
        List<String> args = new ArrayList<>(List.of("-d", output.toString()));
        if (!classPath.isEmpty()) {
            StringJoiner sj = new StringJoiner(File.pathSeparator);
            for (Path p : classPath) sj.add(p.toString());
            args.add("-classpath");
            args.add(sj.toString());
        }
        for (Path s : sources) args.add(s.toString());
        ToolProvider compiler = ToolProvider.findFirst("javac").orElseThrow();
        StringWriter silentOutput = new StringWriter();
        int result = compiler.run(new PrintWriter(silentOutput), new PrintWriter(silentOutput),
                args.toArray(new String[0]));
        return result == 0;
    }

    static void show(String label, Object value) {
        System.out.printf("%-58s: %s%n", label, value);
    }

    static Set<String> references(Path classFile) throws IOException {
        Set<String> names = new TreeSet<>();
        ClassModel model = ClassFile.of().parse(classFile);
        for (PoolEntry e : model.constantPool()) {
            if (e instanceof ClassEntry ce) names.add(ce.asInternalName().replace('/', '.'));
        }
        return names;
    }

    /** Produces a single-method class file, with no source at all, that implements the given interface. */
    static byte[] generate(String className, String interfaceName, String methodName, int constant) {
        ClassDesc self = ClassDesc.of(className);
        ClassDesc iface = ClassDesc.of(interfaceName);
        return ClassFile.of().build(self, cb -> {
            cb.withFlags(ClassFile.ACC_PUBLIC);
            cb.withInterfaceSymbols(iface);
            cb.withMethodBody(ConstantDescs.INIT_NAME, ConstantDescs.MTD_void, ClassFile.ACC_PUBLIC, code -> {
                code.aload(0);
                code.invokespecial(ConstantDescs.CD_Object, ConstantDescs.INIT_NAME, ConstantDescs.MTD_void);
                code.return_();
            });
            cb.withMethodBody(methodName,
                    MethodTypeDesc.of(ConstantDescs.CD_int, ConstantDescs.CD_int),
                    ClassFile.ACC_PUBLIC,
                    code -> {
                        code.iload(1);
                        code.loadConstant(constant);
                        code.iadd();
                        code.ireturn();
                    });
        });
    }
}
```

`Tool.generate` writes no source file at all; it builds the class file's byte sequence
directly — weaving the constant pool, the method table, and the code array by hand. This is
the exact opposite of `Tool.compile`, which calls javac through `ToolProvider` in the
previous two lessons: here the compiler itself is never involved.

```java
// SourcelessGeneration.java — the class file is produced without ever writing source text, but it behaves like a compiled class
import java.net.*;
import java.nio.file.*;
import java.util.*;

public class SourcelessGeneration {
    static String calculatorSource() {
        return "public interface Calculator { int compute(int input); }";
    }

    public static void main(String[] args) throws Exception {
        Path root = Files.createTempDirectory("gl17");
        Path source = root.resolve("source");
        Path interfaceDir = root.resolve("interface");
        Path generatedDir = root.resolve("generated");

        Path sCalculator = Tool.write(source, "Calculator", calculatorSource());
        Tool.compile(interfaceDir, List.of(), List.of(sCalculator));

        Files.createDirectories(generatedDir);
        byte[] bytes = Tool.generate("GeneratedClass", "Calculator", "compute", 1);
        Path classFile = generatedDir.resolve("GeneratedClass.class");
        Files.write(classFile, bytes);

        boolean sourceExists = Files.exists(generatedDir.resolve("GeneratedClass.java"));
        boolean classExists = Files.exists(classFile);

        URL[] url = { interfaceDir.toUri().toURL(), generatedDir.toUri().toURL() };
        Object result;
        try (URLClassLoader y = new URLClassLoader(url, SourcelessGeneration.class.getClassLoader().getParent())) {
            Class<?> iface = Class.forName("Calculator", true, y);
            Class<?> cls = Class.forName("GeneratedClass", true, y);
            Object instance = cls.getDeclaredConstructor().newInstance();
            result = iface.getMethod("compute", int.class).invoke(instance, 10);
        }

        Tool.show("does the GeneratedClass.java source file exist on disk", sourceExists);
        Tool.show("does the GeneratedClass.class class file exist on disk", classExists);
        Tool.show("result of the generated class's compute(10)", result);
    }
}
```

```
does the GeneratedClass.java source file exist on disk    : false
does the GeneratedClass.class class file exist on disk    : true
result of the generated class's compute(10)               : 11
```

`Calculator`'s interface has a source file and was compiled with javac; `GeneratedClass`
has none at all (LO16) — only a `.class` file sits in `generatedDir`. Even so, this class
can be assigned to `Calculator`, and `compute` can be called through the interface, with the
correct result: `10 + 1 = 11` (LO17). What matters to the virtual machine is that the class
file matches the format and carries the required signature; who produced that file, from
which source language, with which tool, never enters into its class-loading decision.

#### The Caller Calls Without Noticing a Difference

**LO18.** Only the interface is present on the caller's compile class path; which class will
supply the implementation is a decision the caller does not know at compile time — including
the name of the source language. **LO19.** A caller compiled against the same interface sees
no behavioral difference between an implementation whose source is written in Java and one
with no source at all.

```java
// NoDifferenceSeen.java — an implementation whose source is Java and a generated class with no source at all cannot be told apart by the caller
import java.net.*;
import java.nio.file.*;
import java.util.*;

public class NoDifferenceSeen {
    static String calculatorSource() {
        return "public interface Calculator { int compute(int input); }";
    }

    static String realClassSource() {
        return "public class RealClass implements Calculator {\n"
             + "  public int compute(int input) { return input + 1; }\n"
             + "}";
    }

    static String callerSource() {
        return "public class Caller {\n"
             + "  public static java.util.List<Integer> results = new java.util.ArrayList<>();\n"
             + "  public static void main (String[] a) throws Exception {\n"
             + "    String className = a[0];\n"
             + "    Calculator h = (Calculator) Class.forName(className).getDeclaredConstructor().newInstance();\n"
             + "    for (int input : new int[]{ 1, 5, 100 }) results.add(h.compute(input));\n"
             + "  }\n"
             + "}";
    }

    public static void main(String[] args) throws Exception {
        Path root = Files.createTempDirectory("gl18");
        Path source = root.resolve("source");

        Path sCalculator = Tool.write(source, "Calculator", calculatorSource());
        Path sReal = Tool.write(source, "RealClass", realClassSource());
        Path sCaller = Tool.write(source, "Caller", callerSource());

        Path interfaceDir = root.resolve("interface");
        Path realDir = root.resolve("real");
        Path callerDir = root.resolve("caller");
        Path generatedDir = root.resolve("generated");

        Tool.compile(interfaceDir, List.of(), List.of(sCalculator));
        Tool.compile(realDir, List.of(interfaceDir), List.of(sReal));
        Tool.compile(callerDir, List.of(interfaceDir), List.of(sCaller));

        Files.createDirectories(generatedDir);
        byte[] bytes = Tool.generate("GeneratedClass", "Calculator", "compute", 1);
        Files.write(generatedDir.resolve("GeneratedClass.class"), bytes);

        Object realResult = runAndRead(List.of(callerDir, interfaceDir, realDir), "RealClass");
        Object generatedResult = runAndRead(List.of(callerDir, interfaceDir, generatedDir), "GeneratedClass");

        Tool.show("results for RealClass, whose source is written in Java", realResult);
        Tool.show("results for GeneratedClass, which has no source at all", generatedResult);
        Tool.show("are the two exactly identical", realResult.equals(generatedResult));

        Set<String> refs = Tool.references(callerDir.resolve("Caller.class"));
        Tool.show("does the caller's class file reference Calculator", refs.contains("Calculator"));
        Tool.show("does the caller's class file reference RealClass", refs.contains("RealClass"));
        Tool.show("does the caller's class file reference GeneratedClass", refs.contains("GeneratedClass"));
    }

    static Object runAndRead(List<Path> classPath, String argClassName) throws Exception {
        URL[] url = new URL[classPath.size()];
        for (int i = 0; i < url.length; i++) url[i] = classPath.get(i).toUri().toURL();
        try (URLClassLoader y = new URLClassLoader(url, NoDifferenceSeen.class.getClassLoader().getParent())) {
            Class<?> caller = Class.forName("Caller", true, y);
            caller.getMethod("main", String[].class).invoke(null, (Object) new String[]{ argClassName });
            return caller.getField("results").get(null);
        }
    }
}
```

```
results for RealClass, whose source is written in Java    : [2, 6, 101]
results for GeneratedClass, which has no source at all    : [2, 6, 101]
are the two exactly identical                             : true
does the caller's class file reference Calculator         : true
does the caller's class file reference RealClass          : false
does the caller's class file reference GeneratedClass     : false
```

Neither `RealClass` nor `GeneratedClass` ever appears in `Caller`'s source; which class to
use is decided by the command-line argument `className`, and this comes from outside,
together with the runtime class path (LO18, the same pattern as the facade/implementation
distinction in the first lesson). The result list over three inputs — `[2, 6, 101]` — is
exactly the same in both runs (LO19). Reading the constant pool confirms this: `Caller.class`
references only `Calculator`, neither `RealClass` nor `GeneratedClass`. From the caller's
view there is no difference between the two implementations, because the only thing it sees
is the interface and its promise; whether the source came from a compiler or a byte-sequence
constructor is not part of that promise.

#### Limiting Measurement: A Member With No Counterpart in Syntax

**LO20.** The constraint the virtual machine places on a member name is far looser than the
constraint the Java language places on an identifier; almost any string other than a handful
of reserved characters (`.`, `;`, `[`, `/`) is a valid member name, and method names
additionally cannot carry `<` and `>` — these two characters occur only in the special names
`<init>` and `<clinit>`. **LO21.** A method carrying such a name can only be called by its
producer, through reflection; no call line in source text can write this name, because the
compiler's syntax does not accept it as an identifier.

```java
// SyntaxlessMember.java — a class file's member can carry a name that no Java source can ever write
import java.lang.classfile.*;
import java.lang.constant.*;
import java.lang.reflect.*;
import java.net.*;
import java.nio.file.*;

public class SyntaxlessMember {
    public static void main(String[] args) throws Exception {
        Path root = Files.createTempDirectory("gl20");
        Path generatedDir = root.resolve("generated");
        Files.createDirectories(generatedDir);

        String validName = "compute";
        String invalidName = "2#double";

        ClassDesc self = ClassDesc.of("TwoMethodClass");
        byte[] bytes = ClassFile.of().build(self, cb -> {
            cb.withFlags(ClassFile.ACC_PUBLIC);
            cb.withMethodBody(ConstantDescs.INIT_NAME, ConstantDescs.MTD_void, ClassFile.ACC_PUBLIC, code -> {
                code.aload(0);
                code.invokespecial(ConstantDescs.CD_Object, ConstantDescs.INIT_NAME, ConstantDescs.MTD_void);
                code.return_();
            });
            cb.withMethodBody(validName, MethodTypeDesc.of(ConstantDescs.CD_int, ConstantDescs.CD_int),
                    ClassFile.ACC_PUBLIC, code -> {
                        code.iload(1); code.iconst_1(); code.iadd(); code.ireturn();
                    });
            cb.withMethodBody(invalidName, MethodTypeDesc.of(ConstantDescs.CD_int, ConstantDescs.CD_int),
                    ClassFile.ACC_PUBLIC, code -> {
                        code.iload(1); code.loadConstant(2); code.imul(); code.ireturn();
                    });
        });
        Path classFile = generatedDir.resolve("TwoMethodClass.class");
        Files.write(classFile, bytes);

        URL[] url = { generatedDir.toUri().toURL() };
        try (URLClassLoader y = new URLClassLoader(url, SyntaxlessMember.class.getClassLoader().getParent())) {
            Class<?> cls = Class.forName("TwoMethodClass", true, y);
            Object instance = cls.getDeclaredConstructor().newInstance();

            Method valid = cls.getMethod(validName, int.class);
            Method invalid = cls.getMethod(invalidName, int.class);

            boolean validIdentifier = invalidName.length() > 0
                    && Character.isJavaIdentifierStart(invalidName.charAt(0))
                    && invalidName.chars().allMatch(Character::isJavaIdentifierPart);

            Tool.show("call result of the method at the valid name (" + validName + ")", valid.invoke(instance, 10));
            Tool.show("call result of the method at the invalid name (" + invalidName + ")", invalid.invoke(instance, 10));
            Tool.show("\"" + invalidName + "\" is a valid Java identifier", validIdentifier);
        }
    }
}
```

```
call result of the method at the valid name (compute)     : 11
call result of the method at the invalid name (2#double)  : 20
"2#double" is a valid Java identifier                     : false
```

`TwoMethodClass` carries two methods: `compute`, a valid Java identifier, and `2#double`,
which is not — it starts with a digit and carries the `#` character, which an identifier
cannot. The `Character.isJavaIdentifierStart`/`isJavaIdentifierPart` check confirms this:
`false` (LO20). Even so, the virtual machine does not reject the name; the class loads,
`getMethod("2#double", ...)` finds it, `invoke` calls it, and the result is correct:
`10 * 2 = 20`. This is the lesson's limiting measurement: a member entering the runtime class
path only has to follow the class file format's rules, not the source language's syntax. The
distinction is one of format, not semantics: the virtual machine has no idea what
`2#double` means or what contract it fulfills, it only accepts it as a callable member. No
Java source file can ever write this line (LO21); the producing side wrote it directly into
the byte sequence, and the caller found it only through reflection, supplying its name as a
string.

#### Summary

- Writing source text and compiling it is not the only way to produce a class file; a
  procedure that directly produces the class file format can also produce a class file the
  virtual machine will load.
- A class produced this way is loaded and called just like a class compiled from source, as
  long as it declares that it implements an interface and carries the signature the
  interface requires.
- Only the interface is present on the caller's compile class path; there is no difference,
  either seen by the caller or referenced by the class file, between an implementation whose
  source is Java and one with no source at all.
- The constraint the virtual machine places on a member name is far looser than the
  constraint the Java language places on an identifier; almost any string other than a
  handful of reserved characters is a valid member name.
- Limiting measurement: a generated class file can carry a member name that no Java source
  could ever write; this member can only be called through reflection, supplying its name as
  a string.

#### Course Wrap-Up

The course opened with a single question: a Java program lives against two separate class
paths — the one it compiles against and the one it runs against — and the source text
cannot see either. Twelve lessons asked this question across four mechanisms: the build tool
deriving two paths from a declaration, testing adding a third path, the logging facade
deliberately building this distinction, and this final lesson showing that the runtime class
path never sees the source language at all.

The first claim was confirmed: compiled text does not determine the program that will run —
in this lesson, the caller saw only an interface, and it made no difference whether the class
behind it had passed through a compiler. The second claim — that the defect is mostly not
noisy — was seen again in the logging facade: when no implementation is found, the caller
does not crash, it stays silent. The third claim — that dependency direction is a fact
readable in the compiled product — was tested by reading the constant pool across all three
topics of the course. The boundary was the same from the start: "it compiled" is not proof.

| Lesson | Behavior measured | Path required (compile · runtime · both) | Limiting measurement |
|---|---|---|---|
| Build Tool Model | 3 declared items derive 4 phases; the order is independent of the order they were written in the declaration | both (the same declaration yields two paths at once) | An annotation type kept only in source never appears in the constant pool |
| Dependency Management | Distribution across six types: 1 compile, 3 runtime, 2 both; two of four defects are silent | compile (1) · runtime (3) · both (2) | Not being required on the runtime path is not the absence of a dependency — it can mean the value was inlined |
| Multi-Module Projects | Of three boundary setups, one is caught at compile time, one at runtime, one is never caught | compile · runtime (two uses of the same type) | The boundary is set not by the directory but by the loader's decision; two classes stay in the same directory |
| Packaging and Executable Output | A self-sufficient archive runs on its own; an archive dependent on the outside fails with a runtime error | runtime (the path the archive carries) | The entry point lives in the declaration, not the source; passing compilation does not show the archive is sufficient |
| Unit Test Framework | The production path has one type, the test path has three; with the framework type missing, the same class fails at runtime | both (the test path is a third path) | The order in which the framework calls does not give correctness: the same assertion passes before teardown, then fails after it |
| Mocking | The same compiled test gives two different results through two runtime paths, neither with an error | runtime (where the fake type sits) | When the signature diverges, the defect surfaces at runtime, not at compile time, and the cause is invisible in the test text |
| Parameterized Tests | A single body is called against three data rows; all three pass under an independent setup | runtime (the data declaration) | When the setup is shared, which row passes becomes dependent on data order |
| Integration Tests | The path's content grows from one type to two; the same request is silent in the unit test and throws in the integration test | runtime (the real implementation and its dependency) | Putting the real type back does not validate the test, it only makes another class of defect visible |
| Behavior-Driven Testing | Three of four steps match; the missing step is silent in one setup and halts the run in the other | runtime (the step map) | A full match does not show correctness: a map with its assertion removed still reports a pass with the wrong value |
| Logging Facade and Implementation | The caller's class file references the facade but not the implementation; the implementation enters the runtime class path through a service declaration and is selected by order | both (the facade) · runtime (the implementation) | When no implementation is found at all, the caller compiles and runs without error but produces no records |
| Log Levels and Structured Logging | The number of records the same set of calls produces at two different runtime thresholds; field consistency's effect on readability | runtime (the threshold and handler setting, the field declaration) | A call below the threshold has its argument still evaluated with string concatenation, but not evaluated with a Supplier |
| Other JVM Languages | A class produced with no source text being called just like a compiled class, as long as it implements the interface | runtime (the generated class file) | A generated class file can carry a member name with no counterpart in Java syntax; the caller can only call it through reflection |

Build, Testing and the Ecosystem closes here. The question the course asked — which class
path a given type must be found on — keeps recurring at every step between a Java program's
compilation and its run; the source text never gives the answer to this question on its own.

