Ships v0.5.0 via kmp-manager 6-phase flow. 13 plan tasks (D1, T1-T10, D3, D4), 14 agent calls, 6 P-groups. 107 → 207 tests on iosSimulatorArm64. Toolchain (D1): - Kotlin 2.1.0 → 2.3.21 - Compose-Multiplatform 1.8.0 → 1.11.0 - Android Gradle Plugin 8.7.3 → 9.2.0 - Gradle 8.11.1 → 9.5.1 - coroutines 1.9.0 → 1.11.0, kotlin-test → 2.3.21 - Drop kotlinx-datetime; use stdlib kotlin.time.Clock - Drop iosX64 target (Compose-MP 1.11.0 has no ios_x64 variant) Architecture (T1-T6, T10): - TieredBuffer.snapshotWindow: bisect-based windowed snapshot - New lod/ package: LodStrategy interface + MinMax/Lttb/MinMaxLttb impls (MinMaxLttb SOTA per arXiv 2305.00332, 1.80× faster than pure LTTB) - New render/SignalRenderer: public interface + LineSignalRenderer object - New render/AxisFormatter: 4 default impls (Time, Decimal, DateTime, Unit) - HARD BREAK: deleted LodMode, LodDecimator, ChartConfig.targetFps - ChartConfig split: DataConfig + AxisConfig + RenderConfig + FrameRate sealed - @Immutable/@Stable on all public types (0 unstable) - RealtimeChartState.clear() API Interaction layer (T7): - New interaction/ package - ChartInteractionState + rememberChartInteractionState() - ViewportMode sealed: Following / Frozen / History(anchorMs) - Pinch zoom + drag pan + tap crosshair gestures - Swipe-to-edge resumes Following - InverseProjection: pixel → ms + bisect nearest-sample Perf finishing (T8, T9, D3): - LineSignalRenderer Stroke cache, AxisRenderer TextStyle cache - resolveYRange Pair<Float,Float> → FloatArray out-param - RealtimeChartState.signalsArray cached (invalidated on add/remove only) - LTTB upper-bound aligned to half-open [start, start+windowMs) semantic Correctness (D4): - NumberFormat.formatFixed Long overflow guard @ |v|≥1e19 ABI baseline regenerated: - chart-realtime.api: 161 → 428 LOC - chart-realtime.klib.api: 211 → 501 LOC Modules touched: chart-realtime (lib), app (consumer), gradle (toolchain), .paul (state). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
85 lines
3.5 KiB
Kotlin
85 lines
3.5 KiB
Kotlin
package dev.dtrentin.chart.model
|
|
|
|
import dev.dtrentin.chart.lod.MinMaxLttbLodStrategy
|
|
import dev.dtrentin.chart.render.DecimalAxisFormatter
|
|
import dev.dtrentin.chart.render.LineSignalRenderer
|
|
import dev.dtrentin.chart.render.TimeAxisFormatter
|
|
import androidx.compose.ui.graphics.Color
|
|
import kotlin.test.*
|
|
|
|
class ChartConfigTest {
|
|
|
|
@Test fun ChartConfig_defaultsConstruct() {
|
|
// Default no-arg construction must succeed with all sub-config defaults.
|
|
val cfg = ChartConfig()
|
|
assertEquals(10f, cfg.data.xWindowSeconds)
|
|
assertTrue(cfg.data.yRange is YRange.Auto)
|
|
assertEquals(T0.FirstSample, cfg.data.t0)
|
|
assertEquals(AxisLabelMode.INSIDE, cfg.axis.xLabelMode)
|
|
assertEquals(AxisLabelMode.INSIDE, cfg.axis.yLabelMode)
|
|
assertEquals(2, cfg.axis.yLabelDecimals)
|
|
assertTrue(cfg.axis.showGrid)
|
|
assertEquals(ChartTheme(), cfg.render.theme)
|
|
assertEquals(FrameRate.Display, cfg.render.frameRate)
|
|
}
|
|
|
|
@Test fun ChartConfig_subConfigsOverride() {
|
|
// Override only DataConfig.xWindowSeconds; other fields remain default.
|
|
val cfg = ChartConfig(data = DataConfig(xWindowSeconds = 30f))
|
|
assertEquals(30f, cfg.data.xWindowSeconds)
|
|
// Other DataConfig defaults preserved.
|
|
assertTrue(cfg.data.yRange is YRange.Auto)
|
|
assertEquals(T0.FirstSample, cfg.data.t0)
|
|
// AxisConfig and RenderConfig untouched → equal to defaults.
|
|
assertEquals(AxisConfig(), cfg.axis)
|
|
// RenderConfig has lodStrategy with fresh allocation each call; compare type only.
|
|
assertEquals(FrameRate.Display, cfg.render.frameRate)
|
|
assertEquals(ChartTheme(), cfg.render.theme)
|
|
}
|
|
|
|
@Test fun ChartConfig_topLevelAccessors() {
|
|
// Convenience accessors delegate to nested configs.
|
|
val theme = ChartTheme(backgroundColor = Color.Magenta)
|
|
val cfg = ChartConfig(
|
|
data = DataConfig(xWindowSeconds = 42f),
|
|
render = RenderConfig(theme = theme),
|
|
)
|
|
assertEquals(42f, cfg.xWindowSeconds)
|
|
assertEquals(theme, cfg.theme)
|
|
}
|
|
|
|
@Test fun ChartConfig_equality_isStructural() {
|
|
// Two ChartConfig() with default DataConfig + AxisConfig should be structurally equal.
|
|
// RenderConfig default contains a fresh MinMaxLttbLodStrategy() — strategies do not
|
|
// override equals, so identity differs → render sub-configs are NOT equal.
|
|
// Pass an explicit shared RenderConfig to assert structural equality on the rest.
|
|
val sharedRender = RenderConfig()
|
|
val a = ChartConfig(render = sharedRender)
|
|
val b = ChartConfig(render = sharedRender)
|
|
assertEquals(a, b)
|
|
assertEquals(a.hashCode(), b.hashCode())
|
|
}
|
|
|
|
@Test fun FrameRate_FixedValidation_rejectsZero() {
|
|
assertFailsWith<IllegalArgumentException> { FrameRate.Fixed(0) }
|
|
}
|
|
|
|
@Test fun RenderConfig_lodStrategyDefault_isMinMaxLttb() {
|
|
val rc = RenderConfig()
|
|
assertTrue(
|
|
rc.lodStrategy is MinMaxLttbLodStrategy,
|
|
"default lodStrategy must be MinMaxLttbLodStrategy, got ${rc.lodStrategy::class}"
|
|
)
|
|
}
|
|
|
|
@Test fun AxisConfig_formatterDefaults() {
|
|
val ac = AxisConfig()
|
|
assertSame(TimeAxisFormatter, ac.xFormatter)
|
|
assertEquals(DecimalAxisFormatter(decimals = 2), ac.yFormatter)
|
|
}
|
|
|
|
@Test fun SignalConfig_defaultRenderer_isLineSignalRenderer() {
|
|
val sc = SignalConfig(color = Color.Red)
|
|
assertSame(LineSignalRenderer, sc.renderer, "default renderer must be LineSignalRenderer singleton")
|
|
}
|
|
}
|