Testing¶
The processor is tested by codegen/processor-test/, a JVM test module. It runs the real NavigationCodegenProcessor over inline source strings using
dev.zacsweers.kctfork (a fork of kotlin-compile-testing with KSP2 support), and compares the generated files against checked in golden files.
Test flow¶
ProcessorTestRunner.run(sources) is the only entry point. It builds a KotlinCompilation from the given Map<String, String> of source files and registers
NavigationCodegenProcessorProvider as the only KSP processor. It then runs the compilation under KSP2, walks the KSP output directory, and returns every generated .kt
file as a Map<file name -> contents>, together with the raw JvmCompilationResult.
fun run(sources: Map<String, String>): RunResult {
val compilation = KotlinCompilation().apply {
this.sources = sources.map { (name, content) -> SourceFile.kotlin(name, content) }
useKsp2()
symbolProcessorProviders = mutableListOf(NavigationCodegenProcessorProvider())
kspProcessorOptions = mutableMapOf()
inheritClassPath = true
messageOutputStream = System.out
}
val result = compilation.compile()
val generated = collectGeneratedKotlinFiles(compilation.kspSourcesDir)
return RunResult(result = result, generatedFiles = generated)
}
inheritClassPath = true gives the compilation the test module's runtime classpath. That is how it finds the real codegen-annotations jar, so the processor reads
the actual @NavDestination symbol and not a stub.
Stubs¶
TestStubs.kt holds minimal source fakes of the consumer types the generators use. Each stub is a Pair<String, String> of file name and source
text. Five lists group them by what each set of tests needs.
baseStubsis the common set: DecomposeComponentContext,ActivityScope, the navigation primitives, Metro annotations (including@SingleIn), andkotlinx.serialization. Every test uses these.tabStubsadds theTabChildtype from the home navigation package (com.thomaskioko.tvmaniac.home.nav) and a sampleNavRoot. Tab root tests use these.uiStubsadds the Compose UI annotations and the navigation UI primitives (ScreenContent,SheetContent).@ScreenUiand@SheetUitests use these.tabUiStubsisuiStubsplus theTabChildtype.@TabUitests use these.appRootUiStubsadds the same UI primitives plus a separateandroidx.compose.runtime.Composablestub, because@AppRootUiputs that annotation directly on the generated extension.@AppRootUitests use these.
We keep the stubs as small as possible. Their type signatures must match the constants in
codegen/processor/src/main/kotlin/io/github/thomaskioko/codegen/processor/util/External.kt exactly. Compiling the test suite is what catches drift: if
the stubs and External.kt disagree, the end to end compilation fails. If you update one, update the other.
Goldens¶
Each test asserts through GoldenFileAssert.assertMatches(variant, fileName, actual). Goldens live under
codegen/processor-test/src/test/resources/golden/<variant>/<file>.kt. There are eleven navigation codegen variants
(the feature flag processor keeps its own featureflag/ golden, documented in featureflag.md):
simple/for@NavDestination(kind = SCREEN)with plain@Inject.parameterized/for@NavDestination(kind = SCREEN)with@AssistedInject.tab/for@NavDestination(kind = TAB_ROOT).screen-ui/for@ScreenUi.sheet-ui/for@SheetUi.tab-ui/for@TabUi.child-presenter/for@ChildPresenterpinned to one host (parentScopeis a tab root).child-presenter-embeddable/for@ChildPresentermade reusable (parentScopeis the sharedActivityScope).child-presenter-parameterized/for@ChildPresenteron an@AssistedInjectpresenter, where the graph exposes the assisted factory.app-root/for@AppRoot.app-root-ui/for@AppRootUi.
Tests are grouped by annotation, not by variant:
NavDestinationTestcovers all three@NavDestinationkinds (SCREEN, OVERLAY, TAB_ROOT) plus the parameterized SCREEN variant. OVERLAY has no golden. The test checks for theNavDestination.Overlay(call in the generated binding.ScreenUiTestcovers@ScreenUi,SheetUiTestcovers@SheetUi, andTabUiTestcovers@TabUi.ChildPresenterTestcovers@ChildPresenterin three shapes: flat (pinned), embeddable (sharedActivityScope), and parameterized.AppRootTestcovers@AppRootplus three error paths (missing@AssistedInject, missing nested factory, missing bound interface).AppRootUiTestcovers@AppRootUiplus two error paths (no parameter other than the modifier, presenter type mismatch).ErrorPathTestruns the validation branches of the navigation parsers and asserts on the compilation messages instead of a golden file.
GoldenFileAssert normalises both the expected and the actual text before comparing. It trims trailing whitespace on each line and trims the whole file, so a
trailing newline difference does not cause flaky failures.
Updating goldens¶
Set golden.update=true (system property) or GOLDEN_UPDATE=true (environment variable) and run the suite again. GoldenFileAssert then writes the actual output to the
golden file instead of failing.
Always read the diff before committing. Goldens are the contract you are committing to, and a bulk update nobody reviewed hides regressions.
Adding a fixture¶
Adding a test fixture takes three steps.
- Add a test class under
codegen/processor-test/src/test/kotlin/io/github/thomaskioko/codegen/processor/. Build the input as aMap<String, String>of source files (usuallyTestStubs.baseStubsplus the feature source) and run it throughProcessorTestRunner. Assert that the expected files exist, and callGoldenFileAssert.assertMatcheson each one. - Run the suite with
golden.update=trueto create the golden directory. - Read the generated files. If they look right, commit them. If not, fix the generator (or the parser) and run it again.
Do not commit a golden you have not read.