Replace munit with zio-test #12
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "chore/zio-test"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
munit is gone from the build and from every source.
munit-scalacheckwent with it, because property testing comes with zio-test rather than as a second dependency.signalsJVM/signalsNativesignalsJSThreadGuardSpec, JS has no threadscoreJVM/coreJS/coreNativeeffectZioJVMshimGenCore's 73 is exactly the 73 that existed before, so nothing was quietly dropped in the conversion.
What the migration found
The reason to read this PR rather than skim it: three latent problems fell out, none of them guessed at.
1. The signal graph's tracking context is two process-global
vars. zio-test runs a suite's tests in parallel by default; munit does not. That broke the graph on the very first run —NullPointerExceptiononRuntime.collected,ConcurrentModificationExceptioninsiderelink. This is not a bug: the graph is owned by exactly one thread, whichThreadGuardenforces in a running app, and the globals are deliberate because anOptionallocation per tracked read is measurable against the 1 µs/node budget. But nothing in the test suite said so, and munit had been hiding it behindparallelExecution := false. Every graph-touching spec now carries@@ TestAspect.sequentialwith the reason written down.2.
TestAspect.sequentialorders tests within a spec and promises nothing between specs, which zio-test runs concurrently.BridgeSpecandRemoteScreenSpecboth installUiThreadandThreadGuard; they raced, andRemoteScreen's error test timed out waiting for a state the other suite's fibres were competing for. Suites sharing process-global state cannot be separate specs — they are now one,EffectZioSpec.3. Phase 2 shipped a test that only ever passed on the JVM. Scala.js has no int/double distinction, so
7.0.toStringis"7"there and"7.0"on the JVM.CatalogueSuitecompared rendered strings forProp.Value,Prop.RangeandProp.Progress— and had never been run on JS or Native, where it fails.TestRenderernow keeps numeric props in a separatenums: Map[String, Double]instead of stringifying them.That third one is a process failure as much as a bug: I had only ever run
coreJVM/testOnly. The rule is now written down indocs/12§12.8 — running one backend is not running the tests.Mechanics worth a look in review
Checksaccumulates assertions made during a test. These tests drive a mutable graph and check it after each step, because the interesting property is usually how many times something happened between two writes, which the end state cannot show. Restructuring every test around a single final assertion would have discarded exactly that.beforeEach/afterEachbecameTestAspect.before/after, which has the advantage of being visible at the suite's declaration rather than buried in an override.signalsdependency, so rather than duplicateChecks, its two specs collect every mismatch and assert once. That is better regardless: a drifting ABI usually drifts in several places, and a check that stops at the first makes you run it N times to learn how bad it is.testis ambiguous under bothimport zio.*(the packagezio.test) andimport zio.test.*(the method). Scala 3 treats the wildcards as equal precedence whichever order they appear in, soRemoteScreenTestsqualifies its call sites.test-interface0.5.10 against this build's 0.5.12. Binary compatible across 0.5.x patches, so one narrowlibraryDependencySchemesentry says so rather than disabling eviction errors build-wide.Also in this branch
docs/12§12.8 documents how tests are run and the three rules above.decisions.mdgains two entries: never stringify a number in a cross-built test, and sbt 2's disk cache does not notice a deleted test source — renaming a suite leftnativeLinkfailing on the old name throughclean, through deletingtarget/out, and through touching sources. Clearing~/.cache/sbt/v2andtarget/out/<platform>is what fixes it. That cost me twenty minutes twice.CLAUDE.mdfiles gain the standing rule that I never merge a PR. Unrelated to the migration, and swept into the same commit — say the word if you would rather it were split out.🤖 Generated with Claude Code
munit is gone from the build and from every source; munit-scalacheck went with it, because property testing comes with zio-test rather than as a second dependency. signalsJVM/signalsNative 21 coreJVM/coreJS/coreNative 73 signalsJS 19 effectZioJVM 15 shimGen 10 Core's 73 is exactly the 73 that existed before, so nothing was quietly dropped in the conversion. Two more things the migration found, on top of the global-tracking-context problem the signals commit described: 1. TestAspect.sequential orders tests *within* a spec and promises nothing *between* specs, which zio-test runs concurrently. BridgeSpec and RemoteScreenSpec both install UiThread and ThreadGuard, raced, and RemoteScreen's error test timed out waiting for a state the other suite's fibres were competing for. Suites sharing process-global state cannot be separate specs; they are now one, EffectZioSpec, and sequential finally covers them. munit never showed this because it runs test classes one at a time under parallelExecution := false. 2. Phase 2 shipped a test that only ever passed on the JVM. Scala.js has no int/double distinction, so 7.0.toString is "7" there and "7.0" on the JVM, and CatalogueSuite compared rendered strings for Value, Range and Progress. It had never been run on JS or Native. TestRenderer now keeps numeric props in a separate nums: Map[String, Double] rather than stringifying them. The process lesson is the larger one and is written down in docs/12 section 12.8: running coreJVM/testOnly is not running the tests. Mechanics worth knowing: - Checks accumulates assertions made during a test. These tests drive a mutable graph and check it after each step, because the interesting property is usually how many times something happened between two writes, which the end state cannot show. Restructuring around a single final assertion would have discarded exactly that. - munit's beforeEach/afterEach became TestAspect.before/after, which has the advantage of being visible at the suite declaration rather than buried in an override. - shim-gen has no signals dependency, so rather than duplicate Checks its two specs collect all mismatches and assert once. That is better anyway: a drifting ABI usually drifts in several places, and a check that stops at the first makes you run it N times to learn how bad it is. - `test` is ambiguous under both `import zio.*` and `import zio.test.*` - Scala 3 treats the wildcards as equal precedence whichever order they appear in - so RemoteScreenTests qualifies its call sites. Also in decisions.md: never stringify a number in a cross-built test, and sbt 2's disk cache does not notice a deleted test source. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>