pi-remote-ios/Tests/CoreTests/ScrollbackCacheTests.swift

182 lines
5.9 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ScrollbackCacheTests.swift
// Unit tests for ScrollbackCache the rolling 5 MB on-disk ANSI cache.
//
// Each test creates its own ScrollbackCache with a unique UUID session ID so
// instances never share the same file. All test files are deleted in tearDown.
//
// Tests are synchronous because ScrollbackCache's public API is
// synchronous (guarded internally by a serial DispatchQueue).
import XCTest
@testable import piRemote
final class ScrollbackCacheTests: XCTestCase {
// Track session IDs created so tearDown can clean every one up.
private var sessionIds: [String] = []
override func setUp() {
super.setUp()
sessionIds = []
}
override func tearDown() {
// Remove every test cache file, regardless of test outcome.
for id in sessionIds {
ScrollbackCache(sessionId: id).clear()
}
sessionIds.removeAll()
super.tearDown()
}
// Convenience: create a cache for a fresh UUID session and track it.
private func makeCache() -> ScrollbackCache {
let id = UUID().uuidString
sessionIds.append(id)
return ScrollbackCache(sessionId: id)
}
// MARK: - Append + Read round-trip
/// A single `append` followed by `read` returns exactly the same bytes.
func testAppendAndRead_roundTrip() {
let cache = makeCache()
let data = Data("hello world".utf8)
cache.append(data)
XCTAssertEqual(cache.read(), data)
}
/// Multiple successive appends are concatenated in order.
func testMultipleAppends_areOrdered() {
let cache = makeCache()
cache.append(Data("foo".utf8))
cache.append(Data("bar".utf8))
XCTAssertEqual(cache.read(), Data("foobar".utf8))
}
/// `sizeBytes` reflects the total bytes written.
func testSizeBytes_reflectsAppend() {
let cache = makeCache()
let data = Data(repeating: 0xAB, count: 1_024)
cache.append(data)
XCTAssertEqual(cache.sizeBytes, 1_024)
}
// MARK: - Cap enforcement
private let fiveMB = 5 * 1024 * 1024
/// Writing > 5 MB of data keeps the on-disk size at or below 5 MB.
func testCap_sizeStaysWithinFiveMB() {
let cache = makeCache()
// Write three 2 MB chunks (6 MB total) must trim.
let chunk = Data(repeating: 0xCC, count: 2 * 1_024 * 1_024)
cache.append(chunk)
cache.append(chunk)
cache.append(chunk)
XCTAssertLessThanOrEqual(
cache.sizeBytes, fiveMB,
"sizeBytes (\(cache.sizeBytes)) should be ≤ 5 MB after overflow"
)
}
/// After overflow, the last bytes in the file come from the newest chunk (oldest dropped).
func testCap_dropsOldestBytes() {
let cache = makeCache()
// 3 MB of 'A' then 3 MB of 'B' = 6 MB total; trim must keep the tail.
let chunkA = Data(repeating: 0x41, count: 3 * 1_024 * 1_024) // 'A'
let chunkB = Data(repeating: 0x42, count: 3 * 1_024 * 1_024) // 'B'
cache.append(chunkA)
cache.append(chunkB)
let result = cache.read()
XCTAssertFalse(result.isEmpty, "result must not be empty after overflow")
XCTAssertLessThanOrEqual(result.count, fiveMB)
// The tail of the retained data must be the newest bytes ('B').
XCTAssertEqual(result.last, 0x42, "last byte should be from the newest chunk")
// The very first byte of 'A'-only content should have been trimmed.
XCTAssertEqual(result.first, 0x41,
"some 'A' bytes may remain at the head, but 'A'-only prefix was trimmed")
}
/// `read()` count stays 5 MB even after many small appends that accumulate.
func testCap_manySmallAppends() {
let cache = makeCache()
// 1 024 appends × 6 KB = ~6 MB
let chunk = Data(repeating: 0x55, count: 6 * 1_024)
for _ in 0..<1_024 {
cache.append(chunk)
}
XCTAssertLessThanOrEqual(cache.read().count, fiveMB)
}
// MARK: - clear
/// After `clear()`, `sizeBytes` is 0.
func testClear_zeroesSizeBytes() {
let cache = makeCache()
cache.append(Data(repeating: 0x00, count: 1_024))
XCTAssertGreaterThan(cache.sizeBytes, 0) // precondition
cache.clear()
XCTAssertEqual(cache.sizeBytes, 0)
}
/// After `clear()`, `read()` returns empty Data.
func testClear_emptiesData() {
let cache = makeCache()
cache.append(Data("test".utf8))
cache.clear()
XCTAssertTrue(cache.read().isEmpty)
}
/// Appending after `clear()` works as if the cache were freshly created.
func testClear_thenAppend_worksCorrectly() {
let cache = makeCache()
cache.append(Data("old".utf8))
cache.clear()
cache.append(Data("new".utf8))
XCTAssertEqual(cache.read(), Data("new".utf8))
XCTAssertEqual(cache.sizeBytes, 3)
}
// MARK: - Isolation between instances
/// Two caches with different session IDs do not share state.
func testTwoInstances_doNotInterfere() {
let cacheA = makeCache()
let cacheB = makeCache()
cacheA.append(Data("alpha".utf8))
cacheB.append(Data("beta".utf8))
XCTAssertEqual(cacheA.read(), Data("alpha".utf8))
XCTAssertEqual(cacheB.read(), Data("beta".utf8))
}
/// Clearing one cache does not affect another.
func testClearOneCache_doesNotAffectOther() {
let cacheA = makeCache()
let cacheB = makeCache()
cacheA.append(Data("kept".utf8))
cacheB.append(Data("cleared".utf8))
cacheB.clear()
XCTAssertEqual(cacheA.read(), Data("kept".utf8))
XCTAssertTrue(cacheB.read().isEmpty)
}
// MARK: - Edge cases
/// Appending empty Data is a no-op (no file created, size stays 0).
func testAppendEmptyData_isNoOp() {
let cache = makeCache()
cache.append(Data())
XCTAssertEqual(cache.sizeBytes, 0)
XCTAssertTrue(cache.read().isEmpty)
}
}