mirror of
https://github.com/phranck/TUIkit.git
synced 2026-06-20 09:54:37 +00:00
Add @MainActor to View, ViewModifier, Renderable, App, Scene protocols and all builders (ViewBuilder, SceneBuilder, ChildInfoProvider). Update renderToBuffer(), makeChildInfo(), resolveChildInfos() for main-actor isolation. Mark generic Equatable conformances with @preconcurrency. Update all 45 test suites with @MainActor annotation.
307 lines
12 KiB
Swift
307 lines
12 KiB
Swift
// 🖥️ TUIKit — Terminal UI Kit for Swift
|
|
// RenderCacheTests.swift
|
|
//
|
|
// Created by LAYERED.work
|
|
// License: MIT
|
|
|
|
import Testing
|
|
|
|
@testable import TUIkit
|
|
|
|
@MainActor
|
|
@Suite("RenderCache Tests")
|
|
struct RenderCacheTests {
|
|
|
|
// MARK: - Store and Lookup
|
|
|
|
@Test("Lookup returns cached buffer when view and size match")
|
|
func lookupHitOnEqualViewAndSize() {
|
|
let cache = RenderCache()
|
|
let identity = ViewIdentity(path: "Root/MyView")
|
|
let view = "Hello"
|
|
let buffer = FrameBuffer(text: "rendered")
|
|
|
|
cache.store(identity: identity, view: view, buffer: buffer, contextWidth: 80, contextHeight: 24)
|
|
|
|
let result = cache.lookup(identity: identity, view: view, contextWidth: 80, contextHeight: 24)
|
|
#expect(result != nil)
|
|
#expect(result?.lines == ["rendered"])
|
|
}
|
|
|
|
@Test("Lookup returns nil when view value differs")
|
|
func lookupMissOnDifferentView() {
|
|
let cache = RenderCache()
|
|
let identity = ViewIdentity(path: "Root/MyView")
|
|
let buffer = FrameBuffer(text: "old")
|
|
|
|
cache.store(identity: identity, view: "Hello", buffer: buffer, contextWidth: 80, contextHeight: 24)
|
|
|
|
let result = cache.lookup(identity: identity, view: "World", contextWidth: 80, contextHeight: 24)
|
|
#expect(result == nil)
|
|
}
|
|
|
|
@Test("Lookup returns nil when context width differs")
|
|
func lookupMissOnDifferentWidth() {
|
|
let cache = RenderCache()
|
|
let identity = ViewIdentity(path: "Root/MyView")
|
|
let view = "same"
|
|
let buffer = FrameBuffer(text: "content")
|
|
|
|
cache.store(identity: identity, view: view, buffer: buffer, contextWidth: 80, contextHeight: 24)
|
|
|
|
let result = cache.lookup(identity: identity, view: view, contextWidth: 120, contextHeight: 24)
|
|
#expect(result == nil)
|
|
}
|
|
|
|
@Test("Lookup returns nil when context height differs")
|
|
func lookupMissOnDifferentHeight() {
|
|
let cache = RenderCache()
|
|
let identity = ViewIdentity(path: "Root/MyView")
|
|
let view = "same"
|
|
let buffer = FrameBuffer(text: "content")
|
|
|
|
cache.store(identity: identity, view: view, buffer: buffer, contextWidth: 80, contextHeight: 24)
|
|
|
|
let result = cache.lookup(identity: identity, view: view, contextWidth: 80, contextHeight: 40)
|
|
#expect(result == nil)
|
|
}
|
|
|
|
@Test("Lookup returns nil for unknown identity")
|
|
func lookupMissOnUnknownIdentity() {
|
|
let cache = RenderCache()
|
|
let identity = ViewIdentity(path: "Root/Unknown")
|
|
|
|
let result = cache.lookup(identity: identity, view: "any", contextWidth: 80, contextHeight: 24)
|
|
#expect(result == nil)
|
|
}
|
|
|
|
@Test("Store overwrites existing entry for same identity")
|
|
func storeOverwritesExisting() {
|
|
let cache = RenderCache()
|
|
let identity = ViewIdentity(path: "Root/MyView")
|
|
|
|
cache.store(identity: identity, view: "old", buffer: FrameBuffer(text: "first"), contextWidth: 80, contextHeight: 24)
|
|
cache.store(identity: identity, view: "new", buffer: FrameBuffer(text: "second"), contextWidth: 80, contextHeight: 24)
|
|
|
|
#expect(cache.count == 1)
|
|
let result = cache.lookup(identity: identity, view: "new", contextWidth: 80, contextHeight: 24)
|
|
#expect(result?.lines == ["second"])
|
|
}
|
|
|
|
// MARK: - clearAll
|
|
|
|
@Test("clearAll removes all entries")
|
|
func clearAllRemovesEverything() {
|
|
let cache = RenderCache()
|
|
cache.store(identity: ViewIdentity(path: "A"), view: 1, buffer: FrameBuffer(text: "a"), contextWidth: 80, contextHeight: 24)
|
|
cache.store(identity: ViewIdentity(path: "B"), view: 2, buffer: FrameBuffer(text: "b"), contextWidth: 80, contextHeight: 24)
|
|
|
|
#expect(cache.count == 2)
|
|
cache.clearAll()
|
|
#expect(cache.isEmpty)
|
|
}
|
|
|
|
// MARK: - Garbage Collection
|
|
|
|
@Test("removeInactive removes entries not marked active")
|
|
func removeInactiveGarbageCollects() {
|
|
let cache = RenderCache()
|
|
let activeIdentity = ViewIdentity(path: "Root/Active")
|
|
let staleIdentity = ViewIdentity(path: "Root/Stale")
|
|
|
|
cache.store(identity: activeIdentity, view: "a", buffer: FrameBuffer(text: "active"), contextWidth: 80, contextHeight: 24)
|
|
cache.store(identity: staleIdentity, view: "s", buffer: FrameBuffer(text: "stale"), contextWidth: 80, contextHeight: 24)
|
|
|
|
cache.beginRenderPass()
|
|
cache.markActive(activeIdentity)
|
|
cache.removeInactive()
|
|
|
|
#expect(cache.count == 1)
|
|
#expect(cache.lookup(identity: activeIdentity, view: "a", contextWidth: 80, contextHeight: 24) != nil)
|
|
#expect(cache.lookup(identity: staleIdentity, view: "s", contextWidth: 80, contextHeight: 24) == nil)
|
|
}
|
|
|
|
@Test("beginRenderPass clears active set for fresh GC tracking")
|
|
func beginRenderPassClearsActiveSet() {
|
|
let cache = RenderCache()
|
|
let identity = ViewIdentity(path: "Root/View")
|
|
|
|
cache.store(identity: identity, view: "v", buffer: FrameBuffer(text: "content"), contextWidth: 80, contextHeight: 24)
|
|
|
|
// First pass: mark active
|
|
cache.beginRenderPass()
|
|
cache.markActive(identity)
|
|
cache.removeInactive()
|
|
#expect(cache.count == 1)
|
|
|
|
// Second pass: don't mark active — entry should be removed
|
|
cache.beginRenderPass()
|
|
cache.removeInactive()
|
|
#expect(cache.isEmpty)
|
|
}
|
|
|
|
// MARK: - Type Safety
|
|
|
|
@Test("Lookup returns nil when snapshot type does not match")
|
|
func lookupMissOnTypeMismatch() {
|
|
let cache = RenderCache()
|
|
let identity = ViewIdentity(path: "Root/View")
|
|
|
|
cache.store(identity: identity, view: 42, buffer: FrameBuffer(text: "int"), contextWidth: 80, contextHeight: 24)
|
|
|
|
// Try to look up with String type — should fail
|
|
let result = cache.lookup(identity: identity, view: "42", contextWidth: 80, contextHeight: 24)
|
|
#expect(result == nil)
|
|
}
|
|
|
|
// MARK: - Stats
|
|
|
|
@Test("Stats start at zero")
|
|
func statsInitiallyZero() {
|
|
let cache = RenderCache()
|
|
#expect(cache.stats == RenderCache.Stats())
|
|
#expect(cache.stats.hits == 0)
|
|
#expect(cache.stats.misses == 0)
|
|
#expect(cache.stats.stores == 0)
|
|
#expect(cache.stats.clears == 0)
|
|
}
|
|
|
|
@Test("Cache hit increments hits counter")
|
|
func statsCountHits() {
|
|
let cache = RenderCache()
|
|
let identity = ViewIdentity(path: "Root/View")
|
|
cache.store(identity: identity, view: "A", buffer: FrameBuffer(text: "a"), contextWidth: 80, contextHeight: 24)
|
|
|
|
_ = cache.lookup(identity: identity, view: "A", contextWidth: 80, contextHeight: 24)
|
|
_ = cache.lookup(identity: identity, view: "A", contextWidth: 80, contextHeight: 24)
|
|
|
|
#expect(cache.stats.hits == 2)
|
|
#expect(cache.stats.misses == 0)
|
|
}
|
|
|
|
@Test("Cache miss increments misses counter")
|
|
func statsCountMisses() {
|
|
let cache = RenderCache()
|
|
let identity = ViewIdentity(path: "Root/View")
|
|
cache.store(identity: identity, view: "A", buffer: FrameBuffer(text: "a"), contextWidth: 80, contextHeight: 24)
|
|
|
|
// Miss: different view value
|
|
_ = cache.lookup(identity: identity, view: "B", contextWidth: 80, contextHeight: 24)
|
|
// Miss: unknown identity
|
|
_ = cache.lookup(identity: ViewIdentity(path: "Root/Other"), view: "A", contextWidth: 80, contextHeight: 24)
|
|
// Miss: different size
|
|
_ = cache.lookup(identity: identity, view: "A", contextWidth: 120, contextHeight: 24)
|
|
|
|
#expect(cache.stats.misses == 3)
|
|
#expect(cache.stats.hits == 0)
|
|
}
|
|
|
|
@Test("Store increments stores counter")
|
|
func statsCountStores() {
|
|
let cache = RenderCache()
|
|
cache.store(identity: ViewIdentity(path: "A"), view: 1, buffer: FrameBuffer(text: "a"), contextWidth: 80, contextHeight: 24)
|
|
cache.store(identity: ViewIdentity(path: "B"), view: 2, buffer: FrameBuffer(text: "b"), contextWidth: 80, contextHeight: 24)
|
|
// Overwrite existing entry
|
|
cache.store(identity: ViewIdentity(path: "A"), view: 3, buffer: FrameBuffer(text: "c"), contextWidth: 80, contextHeight: 24)
|
|
|
|
#expect(cache.stats.stores == 3)
|
|
}
|
|
|
|
@Test("clearAll increments clears counter")
|
|
func statsCountClears() {
|
|
let cache = RenderCache()
|
|
cache.store(identity: ViewIdentity(path: "A"), view: 1, buffer: FrameBuffer(text: "a"), contextWidth: 80, contextHeight: 24)
|
|
|
|
cache.clearAll()
|
|
cache.clearAll()
|
|
|
|
#expect(cache.stats.clears == 2)
|
|
}
|
|
|
|
@Test("Stats accumulate across multiple operations")
|
|
func statsAccumulateAcrossOperations() {
|
|
let cache = RenderCache()
|
|
let identity = ViewIdentity(path: "Root/View")
|
|
|
|
// 1 store
|
|
cache.store(identity: identity, view: "A", buffer: FrameBuffer(text: "a"), contextWidth: 80, contextHeight: 24)
|
|
// 1 hit
|
|
_ = cache.lookup(identity: identity, view: "A", contextWidth: 80, contextHeight: 24)
|
|
// 1 miss (view changed)
|
|
_ = cache.lookup(identity: identity, view: "B", contextWidth: 80, contextHeight: 24)
|
|
// 1 clear
|
|
cache.clearAll()
|
|
// 1 miss (cache empty)
|
|
_ = cache.lookup(identity: identity, view: "A", contextWidth: 80, contextHeight: 24)
|
|
|
|
#expect(cache.stats.hits == 1)
|
|
#expect(cache.stats.misses == 2)
|
|
#expect(cache.stats.stores == 1)
|
|
#expect(cache.stats.clears == 1)
|
|
#expect(cache.stats.lookups == 3)
|
|
}
|
|
|
|
@Test("Hit rate is calculated correctly")
|
|
func statsHitRate() {
|
|
let cache = RenderCache()
|
|
let identity = ViewIdentity(path: "Root/View")
|
|
cache.store(identity: identity, view: "A", buffer: FrameBuffer(text: "a"), contextWidth: 80, contextHeight: 24)
|
|
|
|
// 3 hits
|
|
_ = cache.lookup(identity: identity, view: "A", contextWidth: 80, contextHeight: 24)
|
|
_ = cache.lookup(identity: identity, view: "A", contextWidth: 80, contextHeight: 24)
|
|
_ = cache.lookup(identity: identity, view: "A", contextWidth: 80, contextHeight: 24)
|
|
// 1 miss
|
|
_ = cache.lookup(identity: identity, view: "B", contextWidth: 80, contextHeight: 24)
|
|
|
|
#expect(cache.stats.hitRate == 0.75)
|
|
}
|
|
|
|
@Test("Hit rate is zero when no lookups occurred")
|
|
func statsHitRateZeroWithoutLookups() {
|
|
let cache = RenderCache()
|
|
#expect(cache.stats.hitRate == 0)
|
|
}
|
|
|
|
@Test("resetStats clears all counters")
|
|
func resetStatsClearsCounters() {
|
|
let cache = RenderCache()
|
|
let identity = ViewIdentity(path: "Root/View")
|
|
cache.store(identity: identity, view: "A", buffer: FrameBuffer(text: "a"), contextWidth: 80, contextHeight: 24)
|
|
_ = cache.lookup(identity: identity, view: "A", contextWidth: 80, contextHeight: 24)
|
|
cache.clearAll()
|
|
|
|
#expect(cache.stats.hits > 0)
|
|
cache.resetStats()
|
|
#expect(cache.stats == RenderCache.Stats())
|
|
}
|
|
|
|
@Test("reset() also clears statistics")
|
|
func resetClearsStats() {
|
|
let cache = RenderCache()
|
|
let identity = ViewIdentity(path: "Root/View")
|
|
cache.store(identity: identity, view: "A", buffer: FrameBuffer(text: "a"), contextWidth: 80, contextHeight: 24)
|
|
_ = cache.lookup(identity: identity, view: "A", contextWidth: 80, contextHeight: 24)
|
|
cache.clearAll()
|
|
|
|
#expect(cache.stats.hits > 0)
|
|
cache.reset()
|
|
#expect(cache.stats == RenderCache.Stats())
|
|
#expect(cache.isEmpty)
|
|
}
|
|
|
|
@Test("Stats delta computes per-frame difference")
|
|
func statsDelta() {
|
|
let earlier = RenderCache.Stats(hits: 10, misses: 5, stores: 8, clears: 2)
|
|
let current = RenderCache.Stats(hits: 13, misses: 7, stores: 9, clears: 3)
|
|
|
|
let delta = current.delta(since: earlier)
|
|
|
|
#expect(delta.hits == 3)
|
|
#expect(delta.misses == 2)
|
|
#expect(delta.stores == 1)
|
|
#expect(delta.clears == 1)
|
|
#expect(delta.lookups == 5)
|
|
}
|
|
}
|