test(arch): ICU, MaterialShapes and the ambient zone each stay where they belong

Three new rules. `android.icu` may be named only under `data/zones/`, so the
platform's name database stays behind its seam. `MaterialShapes` and
`androidx.graphics.shapes` may be named only under `ui/worldclock/`, so the
showpiece stays the one place that morphs. And nothing under `domain/` or
`ui/` may read `ZoneId.systemDefault()` or `TimeZone.getDefault()` — the
device's zone is an argument, or the tests cannot pin a single one.

All three hold retroactively for M0 through M7.
This commit is contained in:
2026-09-22 13:05:18 +02:00
parent 725682d851
commit 815a14235b
2 changed files with 54 additions and 1 deletions
@@ -260,4 +260,52 @@ class ArchitectureRulesTest {
assertThat(offenders).isEmpty()
}
/**
* M8 D24: ICU is a seam, not an ambient API. Every localised zone name the
* app shows comes through `data/zones/`, so an OEM build with a broken ICU
* degrades one caption instead of crashing a tab.
*/
@Test
fun `ICU is named only under the zone-names seam`() {
val offenders = kotlinSources.usingInCode("android.icu").filterNot {
it.contains("de/jeanlucmakiola/clockula/data/zones/")
}
assertThat(offenders).isEmpty()
}
/**
* M8 D24: `docs/PLAN.md` §8's "not sprinkled everywhere", made mechanical.
* The shape morph is the world clock's one showpiece; the ring screen and
* timer progress are sanctioned too, so widening this allowlist is a
* deliberate edit rather than a drift.
*/
@Test
fun `MaterialShapes is drawn only on the world clock face`() {
val offenders = kotlinSources
.usingInCode("MaterialShapes", "androidx.graphics.shapes")
.filterNot { it.contains("de/jeanlucmakiola/clockula/ui/worldclock/") }
assertThat(offenders).isEmpty()
}
/**
* M8 D24: the zone is a parameter, from `ZoneProvider` — never an ambient
* read at a call site. Scoped to `domain/` and `ui/` on purpose:
* `alarm/ring/RingNotifications.kt` formats a snooze time with
* `systemDefault()` today, and rewriting it is not M8's business.
*/
@Test
fun `the domain and the ui read no ambient time zone`() {
val domainAndUi = kotlinSources.filter {
val path = it.invariantSeparatorsPath
path.contains("de/jeanlucmakiola/clockula/domain/") ||
path.contains("de/jeanlucmakiola/clockula/ui/")
}
val offenders = domainAndUi.usingInCode("ZoneId.systemDefault()", "TimeZone.getDefault()")
assertThat(offenders).isEmpty()
}
}
@@ -138,8 +138,13 @@ class FakeAlarmCapabilities(
}
/** The device's zone, which a test can change under a scheduled alarm. */
class FakeZoneProvider(var zone: ZoneId = BERLIN) : ZoneProvider {
class FakeZoneProvider(
var zone: ZoneId = BERLIN,
var availableIds: Set<String> = ZoneId.getAvailableZoneIds(),
) : ZoneProvider {
override fun current(): ZoneId = zone
override fun available(): Set<String> = availableIds
}
class FakeBootIdProvider(