Sapling/Tests/SaplingEditorTests/EditorLargeDocumentValidationTests.swift

120 lines
5 KiB
Swift

import XCTest
@testable import SaplingEditor
final class EditorLargeDocumentValidationTests: XCTestCase {
func testLargeDocumentOpenAndDirtyRenderPlanning() {
for lineCount in [1_000, 5_000, 10_000] {
let source = Self.prototypeDocument(lineCount: lineCount)
let activeLineIndex = lineCount / 2
let openMeasurement = elapsedTime {
_ = EditorState(
document: document(source: source, lineCount: lineCount),
activeLineIndex: activeLineIndex
)
}
let updatedSource = Self.replacingLine(
activeLineIndex,
in: source,
with: "Line \(activeLineIndex + 1) edited with **bold** and `inline code`."
)
let updateMeasurement = elapsedTime {
var state = EditorState(
document: document(source: source, lineCount: lineCount),
activeLineIndex: activeLineIndex
)
state.updateSource(updatedSource)
_ = EditorDirtyLineInvalidator.plan(
previousText: source,
currentText: updatedSource,
previousActiveLineIndex: activeLineIndex,
currentActiveLineIndex: activeLineIndex
)
}
let plan = EditorDirtyLineInvalidator.plan(
previousText: source,
currentText: updatedSource,
previousActiveLineIndex: activeLineIndex,
currentActiveLineIndex: activeLineIndex
)
let updatedLineIndex = DocumentLineIndex(source: updatedSource)
let dirtyRenderMeasurement = elapsedTime {
let renderer = HybridMarkdownLineRenderer()
_ = plan.dirtyLineIndexes
.compactMap { updatedLineIndex.editorLine(at: $0, activeLineIndex: activeLineIndex) }
.map(renderer.renderPlan(for:))
}
XCTAssertEqual(EditorActiveLineTracker.lines(from: source, activeLineIndex: activeLineIndex).count, lineCount)
XCTAssertFalse(plan.isFullRender)
XCTAssertLessThanOrEqual(plan.dirtyLineCount, 3)
XCTAssertLessThan(openMeasurement, 0.25, "Opening \(lineCount) generated lines should remain interactive.")
XCTAssertLessThan(updateMeasurement, 0.50, "State update for \(lineCount) lines should remain bounded.")
XCTAssertLessThan(dirtyRenderMeasurement, 0.05, "Dirty render planning should not scale with the full document.")
if ProcessInfo.processInfo.environment["SAPLING_EDITOR_PRINT_METRICS"] == "1" {
print(
"SaplingEditorMetrics lines=\(lineCount) "
+ "openMs=\(milliseconds(openMeasurement)) "
+ "typingMs=\(milliseconds(updateMeasurement)) "
+ "dirtyRenderMs=\(milliseconds(dirtyRenderMeasurement)) "
+ "dirtyLines=\(plan.dirtyLineCount) "
+ "fullRender=\(plan.isFullRender)"
)
}
}
}
func testLargeDocumentActiveLineNavigationDoesNotScheduleFullRender() {
let source = Self.prototypeDocument(lineCount: 10_000)
let plan = EditorDirtyLineInvalidator.plan(
previousText: source,
currentText: source,
previousActiveLineIndex: 250,
currentActiveLineIndex: 9_750
)
XCTAssertEqual(plan.reason, .activeLineChange)
XCTAssertFalse(plan.isFullRender)
XCTAssertEqual(plan.dirtyLineIndexes, [250, 9_750])
}
private func document(source: String, lineCount: Int) -> EditorDocument {
EditorDocument(
url: URL(fileURLWithPath: "/tmp/EditorLargeDocument-\(lineCount).md"),
title: "EditorLargeDocument-\(lineCount)",
source: source
)
}
private func elapsedTime(_ operation: () -> Void) -> TimeInterval {
let start = Date()
operation()
return Date().timeIntervalSince(start)
}
private func milliseconds(_ interval: TimeInterval) -> String {
String(format: "%.3f", interval * 1000)
}
private static func prototypeDocument(lineCount: Int) -> String {
(1...lineCount).map { index in
if index.isMultiple(of: 25) {
return "## Section \(index / 25)"
}
if index.isMultiple(of: 5) {
return "Line \(index) uses **bold** context and `inline code` for scanning."
}
return "Line \(index) is a realistic paragraph with *light emphasis* and enough text to wrap naturally."
}
.joined(separator: "\n")
}
private static func replacingLine(_ lineIndex: Int, in source: String, with replacement: String) -> String {
var lines = source.components(separatedBy: "\n")
lines[lineIndex] = replacement
return lines.joined(separator: "\n")
}
}