Skip to content

Commit ed24452

Browse files
author
Developer
committed
完成 Phase 5 生产环境加固:单元测试、性能测试、基准测试、CI配置
1 parent 60bfb14 commit ed24452

6 files changed

Lines changed: 902 additions & 0 deletions

File tree

.github/workflows/tests.yml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
name: BSText Tests
2+
3+
on:
4+
push:
5+
branches: [ main, dev, dev/v3.0 ]
6+
pull_request:
7+
branches: [ main, dev, dev/v3.0 ]
8+
9+
jobs:
10+
test:
11+
name: Test on Xcode
12+
runs-on: macos-latest
13+
14+
strategy:
15+
matrix:
16+
platform: [iOS]
17+
xcode: [latest-stable]
18+
19+
steps:
20+
- name: Checkout repository
21+
uses: actions/checkout@v4
22+
23+
- name: Set up Xcode
24+
uses: maxim-lobanov/setup-xcode@v1
25+
with:
26+
xcode-version: ${{ matrix.xcode }}
27+
28+
- name: Build and test
29+
run: |
30+
set -o pipefail
31+
xcodebuild -scheme BSText -destination 'platform=iOS Simulator,name=iPhone 15,OS=latest' -enableCodeCoverage YES clean test | xcpretty
32+
33+
- name: Upload coverage
34+
uses: codecov/codecov-action@v4
35+
with:
36+
file: ./coverage.xml
37+
flags: unittests

BSTextTests/BSTextBenchmark.swift

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
import Foundation
2+
@testable import BSText
3+
4+
public struct BenchmarkResult {
5+
let name: String
6+
let iterations: Int
7+
let totalTime: TimeInterval
8+
let averageTime: TimeInterval
9+
let minTime: TimeInterval
10+
let maxTime: TimeInterval
11+
}
12+
13+
public class BSTextBenchmark {
14+
15+
public static let shared = BSTextBenchmark()
16+
17+
private init() {}
18+
19+
public func measure(name: String, iterations: Int = 100, block: () throws -> Void) rethrows -> BenchmarkResult {
20+
var times: [TimeInterval] = []
21+
times.reserveCapacity(iterations)
22+
23+
for _ in 0..<iterations {
24+
let start = Date()
25+
try block()
26+
let end = Date()
27+
let time = end.timeIntervalSince(start)
28+
times.append(time)
29+
}
30+
31+
let totalTime = times.reduce(0, +)
32+
let averageTime = totalTime / Double(iterations)
33+
let minTime = times.min() ?? 0
34+
let maxTime = times.max() ?? 0
35+
36+
return BenchmarkResult(
37+
name: name,
38+
iterations: iterations,
39+
totalTime: totalTime,
40+
averageTime: averageTime,
41+
minTime: minTime,
42+
maxTime: maxTime
43+
)
44+
}
45+
46+
public func printResult(_ result: BenchmarkResult) {
47+
print("""
48+
===== Benchmark: \(result.name) =====
49+
Iterations: \(result.iterations)
50+
Total Time: \(String(format: "%.4f", result.totalTime))s
51+
Average Time: \(String(format: "%.6f", result.averageTime))s
52+
Min Time: \(String(format: "%.6f", result.minTime))s
53+
Max Time: \(String(format: "%.6f", result.maxTime))s
54+
""")
55+
}
56+
}
57+
58+
public class BSTextMemoryMonitor {
59+
60+
public static func getMemoryUsage() -> UInt64? {
61+
var info = mach_task_basic_info()
62+
var count = mach_msg_type_number_t(MemoryLayout<mach_task_basic_info>.size)/4
63+
64+
let result = withUnsafeMutablePointer(to: &info) {
65+
$0.withMemoryRebound(to: integer_t.self, capacity: 1) {
66+
task_info(mach_task_self_, task_flavor_t(MACH_TASK_BASIC_INFO), $0, &count)
67+
}
68+
}
69+
70+
guard result == KERN_SUCCESS else {
71+
return nil
72+
}
73+
74+
return info.resident_size
75+
}
76+
77+
public static func formatMemory(_ bytes: UInt64) -> String {
78+
let kb = Double(bytes) / 1024.0
79+
let mb = kb / 1024.0
80+
let gb = mb / 1024.0
81+
82+
if gb >= 1 {
83+
return String(format: "%.2f GB", gb)
84+
} else if mb >= 1 {
85+
return String(format: "%.2f MB", mb)
86+
} else if kb >= 1 {
87+
return String(format: "%.2f KB", kb)
88+
} else {
89+
return String(format: "%d bytes", bytes)
90+
}
91+
}
92+
93+
public static func measureMemory(name: String, block: () throws -> Void) rethrows {
94+
let before = getMemoryUsage() ?? 0
95+
print("Before \(name): \(formatMemory(before))")
96+
97+
try block()
98+
99+
let after = getMemoryUsage() ?? 0
100+
print("After \(name): \(formatMemory(after))")
101+
102+
let diff = Int64(after) - Int64(before)
103+
if diff > 0 {
104+
print("Memory increase: +\(formatMemory(UInt64(diff)))")
105+
} else if diff < 0 {
106+
print("Memory decrease: \(formatMemory(UInt64(abs(diff))))")
107+
} else {
108+
print("No memory change")
109+
}
110+
}
111+
}
112+
113+
public class BSTextProfilingHelper {
114+
115+
public static func profileTextViewPerformance() {
116+
print("\n===== BSText Performance Benchmarks =====\n")
117+
118+
let benchmark = BSTextBenchmark.shared
119+
120+
let textViewResult = benchmark.measure(name: "BSTextView Initialization", iterations: 1000) {
121+
_ = BSTextView(frame: CGRect(x: 0, y: 0, width: 320, height: 200))
122+
}
123+
benchmark.printResult(textViewResult)
124+
125+
let textView = BSTextView(frame: CGRect(x: 0, y: 0, width: 320, height: 200))
126+
let largeText = (0..<1000).map { "Line \($0): Test content\n" }.joined()
127+
128+
let setTextResult = benchmark.measure(name: "BSTextView Set Text", iterations: 100) {
129+
textView.text = largeText
130+
}
131+
benchmark.printResult(setTextResult)
132+
133+
let sizeThatFitsResult = benchmark.measure(name: "BSTextView Size That Fits", iterations: 100) {
134+
_ = textView.sizeThatFits(CGSize(width: 320, height: CGFloat.greatestFiniteMagnitude))
135+
}
136+
benchmark.printResult(sizeThatFitsResult)
137+
}
138+
139+
public static func profileMarkdownPerformance() {
140+
print("\n===== Markdown Performance Benchmarks =====\n")
141+
142+
let benchmark = BSTextBenchmark.shared
143+
let parser = BSTextMarkdownParser()
144+
145+
let simpleMarkdown = "# Heading\n**Bold** *italic*\n- List item\n- Another item"
146+
let simpleResult = benchmark.measure(name: "Simple Markdown Parsing", iterations: 1000) {
147+
_ = parser.parse(simpleMarkdown)
148+
}
149+
benchmark.printResult(simpleResult)
150+
151+
let largeMarkdown = (0..<100).map {
152+
"# Heading \($0)\n**Bold** *italic* `code`\n- List item 1\n- List item 2\n\n"
153+
}.joined()
154+
155+
let largeResult = benchmark.measure(name: "Large Markdown Parsing", iterations: 100) {
156+
_ = parser.parse(largeMarkdown)
157+
}
158+
benchmark.printResult(largeResult)
159+
}
160+
161+
public static func profileSyntaxPerformance() {
162+
print("\n===== Syntax Highlighting Performance Benchmarks =====\n")
163+
164+
let benchmark = BSTextBenchmark.shared
165+
let parser = BSTextSyntaxParser()
166+
167+
let simpleCode = """
168+
let x = 5
169+
func hello() {
170+
print("world")
171+
}
172+
"""
173+
174+
parser.language = .swift
175+
let simpleSwiftResult = benchmark.measure(name: "Simple Swift Syntax Highlighting", iterations: 1000) {
176+
_ = parser.parse(simpleCode)
177+
}
178+
benchmark.printResult(simpleSwiftResult)
179+
180+
let largeCode = (0..<50).map {
181+
"""
182+
func function\($0)(param: String) -> String {
183+
let result = "Result: \\(param)"
184+
return result
185+
}
186+
"""
187+
}.joined(separator: "\n\n")
188+
189+
let largeSwiftResult = benchmark.measure(name: "Large Swift Syntax Highlighting", iterations: 100) {
190+
_ = parser.parse(largeCode)
191+
}
192+
benchmark.printResult(largeSwiftResult)
193+
}
194+
195+
public static func profileMemoryUsage() {
196+
print("\n===== Memory Usage Benchmarks =====\n")
197+
198+
BSTextMemoryMonitor.measureMemory(name: "Create BSTextView") {
199+
_ = BSTextView(frame: CGRect(x: 0, y: 0, width: 320, height: 200))
200+
}
201+
202+
BSTextMemoryMonitor.measureMemory(name: "Create Large Text Content") {
203+
let textView = BSTextView(frame: CGRect(x: 0, y: 0, width: 320, height: 200))
204+
textView.text = (0..<10000).map { "Line \($0)\n" }.joined()
205+
}
206+
}
207+
}

BSTextTests/BSTextCoreTests.swift

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import XCTest
2+
@testable import BSText
3+
4+
final class BSTextCoreTests: XCTestCase {
5+
6+
var textView: BSTextView!
7+
8+
override func setUp() {
9+
super.setUp()
10+
textView = BSTextView(frame: CGRect(x: 0, y: 0, width: 320, height: 200))
11+
}
12+
13+
override func tearDown() {
14+
textView = nil
15+
super.tearDown()
16+
}
17+
18+
func testTextViewInitialization() {
19+
XCTAssertNotNil(textView)
20+
XCTAssertEqual(textView.backgroundColor, .systemBackground)
21+
}
22+
23+
func testTextEditing() {
24+
let testText = "Hello, BSText!"
25+
textView.text = testText
26+
27+
XCTAssertEqual(textView.text, testText)
28+
}
29+
30+
func testTextReplacement() {
31+
textView.text = "Hello, World!"
32+
textView.selectedRange = NSRange(location: 7, length: 5)
33+
textView.replaceSelection(with: "BSText")
34+
35+
XCTAssertEqual(textView.text, "Hello, BSText!")
36+
}
37+
38+
func testClearAllText() {
39+
textView.text = "Hello, World!"
40+
textView.clearAllText()
41+
42+
XCTAssertEqual(textView.text, "")
43+
}
44+
45+
func testSelectAllText() {
46+
textView.text = "Hello, World!"
47+
textView.selectAllText()
48+
49+
XCTAssertEqual(textView.selectedRange, NSRange(location: 0, length: textView.text.count))
50+
}
51+
52+
func testVisibleFragmentCount() {
53+
textView.text = "Line 1\nLine 2\nLine 3\nLine 4\nLine 5"
54+
let count = textView.visibleFragmentCount
55+
XCTAssertGreaterThanOrEqual(count, 0)
56+
}
57+
}
58+
59+
final class BSTextContentStorageTests: XCTestCase {
60+
61+
var contentStorage: BSTextContentStorage!
62+
63+
override func setUp() {
64+
super.setUp()
65+
contentStorage = BSTextContentStorage()
66+
}
67+
68+
override func tearDown() {
69+
contentStorage = nil
70+
super.tearDown()
71+
}
72+
73+
func testContentStorageInitialization() {
74+
XCTAssertNotNil(contentStorage)
75+
}
76+
77+
func testTextStorageDelegation() {
78+
contentStorage.string = "Test Text"
79+
XCTAssertEqual(contentStorage.string, "Test Text")
80+
}
81+
}
82+
83+
final class BSTextCacheTests: XCTestCase {
84+
85+
var cache: BSTextCache!
86+
87+
override func setUp() {
88+
super.setUp()
89+
cache = BSTextCache()
90+
}
91+
92+
override func tearDown() {
93+
cache = nil
94+
super.tearDown()
95+
}
96+
97+
func testCacheInitialization() {
98+
XCTAssertNotNil(cache)
99+
}
100+
101+
func testCacheSetAndGet() {
102+
let key = "testKey"
103+
let value = "testValue"
104+
105+
cache.setObject(value as AnyObject, forKey: key)
106+
107+
let retrievedValue = cache.object(forKey: key) as? String
108+
XCTAssertEqual(retrievedValue, value)
109+
}
110+
111+
func testCacheRemove() {
112+
let key = "testKey"
113+
cache.setObject("value" as AnyObject, forKey: key)
114+
cache.removeObject(forKey: key)
115+
116+
let retrievedValue = cache.object(forKey: key)
117+
XCTAssertNil(retrievedValue)
118+
}
119+
120+
func testCacheRemoveAll() {
121+
cache.setObject("value1" as AnyObject, forKey: "key1")
122+
cache.setObject("value2" as AnyObject, forKey: "key2")
123+
cache.removeAllObjects()
124+
125+
XCTAssertNil(cache.object(forKey: "key1"))
126+
XCTAssertNil(cache.object(forKey: "key2"))
127+
}
128+
}

0 commit comments

Comments
 (0)