33// This code is released under the SPDX-License-Identifier: `LGPL-3.0-or-later`.
44
55import Foundation
6+ import Testing
7+
68@testable import Tekkon
7- import XCTest
89
910// MARK: - Something Else
1011
@@ -67,11 +68,107 @@ final class SubTestCase: Sendable {
6768 print ( strError)
6869 return false
6970 }
71+
72+ /// 優化版驗證函數,重複使用已建立的 composer 副本
73+ func verify( using composer: inout Tekkon . Composer ) -> Bool {
74+ composer. clear ( )
75+ composer. ensureParser ( arrange: parser)
76+ let strResult = composer. receiveSequence ( typing)
77+ guard strResult != expected else { return true }
78+ let parserTag = composer. parser. nameTag
79+ let strError =
80+ " MISMATCH ( \( parserTag) ): \" \( typing) \" -> \" \( strResult) \" != \" \( expected) \" "
81+ print ( strError)
82+ return false
83+ }
84+ }
85+
86+ // MARK: - TestCaseBatch
87+
88+ /// 批量測試案例處理器,用於減少記憶體分配
89+ actor TestCaseBatch {
90+ // MARK: Lifecycle
91+
92+ init ( parser: Tekkon . MandarinParser , rawData: String ) {
93+ self . parser = parser
94+ var cases : [ ( String , String ) ] = [ ]
95+ var isTitleLine = true
96+ let parserIndex = Self . getParserIndex ( parser)
97+
98+ rawData. parse ( splitee: " \n " ) { theRange in
99+ guard !isTitleLine else {
100+ isTitleLine = false
101+ return
102+ }
103+ let cells = rawData [ theRange] . split ( separator: " " )
104+ guard cells. count > parserIndex else { return }
105+ let expected = cells [ 0 ] . replacingOccurrences ( of: " _ " , with: " " )
106+ let typing = cells [ parserIndex] . description. replacingOccurrences ( of: " _ " , with: " " )
107+ guard typing. first != " ` " else { return }
108+ cases. append ( ( typing: typing, expected: expected) )
109+ }
110+ self . testData = cases
111+ }
112+
113+ // MARK: Internal
114+
115+ let parser : Tekkon . MandarinParser
116+ let testData : [ ( typing: String , expected: String ) ]
117+
118+ func runTests( ) async -> Int {
119+ let indices = testData. indices
120+ let chunkSize = 350
121+ let failedCount = await withTaskGroup ( of: Int . self, returning: Int . self) { group in
122+ var failures = 0
123+ for chunkStartPoint in stride ( from: 0 , to: indices. upperBound, by: chunkSize) {
124+ let subIndiceMax = Swift . min ( indices. upperBound, chunkStartPoint + chunkSize)
125+ let subIndices = chunkStartPoint ..< subIndiceMax
126+ for i in subIndices {
127+ group. addTask {
128+ var subFailures = 0
129+ var composer = Tekkon . Composer ( arrange: self . parser)
130+ let strResult = composer. receiveSequence ( self . testData [ i] . typing)
131+ if strResult != self . testData [ i] . expected {
132+ let parserTag = composer. parser. nameTag
133+ let typingStr = self . testData [ i] . typing
134+ let expectedStr = self . testData [ i] . expected
135+ let strError =
136+ " MISMATCH ( \( parserTag) ): \" \( typingStr) \" -> \" \( strResult) \" != \" \( expectedStr) \" "
137+ print ( strError)
138+ subFailures += 1
139+ }
140+ return subFailures
141+ }
142+ }
143+ for await subFailures in group {
144+ // Set operation name as key and operation result as value
145+ failures += subFailures
146+ }
147+ }
148+ return failures
149+ }
150+ return failedCount
151+ }
152+
153+ // MARK: Private
154+
155+ private static func getParserIndex( _ parser: Tekkon . MandarinParser ) -> Int {
156+ switch parser {
157+ case . ofDachen26: return 1
158+ case . ofETen26: return 2
159+ case . ofHsu: return 3
160+ case . ofStarlight: return 4
161+ case . ofAlvinLiu: return 5
162+ default : return 1
163+ }
164+ }
70165}
71166
72167// MARK: - TekkonTestsKeyboardArrangmentsStatic
73168
74- final class TekkonTestsKeyboardArrangmentsStatic : XCTestCase {
169+ @MainActor
170+ @Suite ( . serialized)
171+ struct TekkonTestsKeyboardArrangmentsStatic {
75172 static func checkEq(
76173 _ counter: inout Int ,
77174 _ composer: inout Tekkon . Composer ,
@@ -87,7 +184,8 @@ final class TekkonTestsKeyboardArrangmentsStatic: XCTestCase {
87184 counter += 1
88185 }
89186
90- func testQwertyDachenKeys( ) {
187+ @Test ( " [Tekkon] StaticKeyLayout_Dachen " )
188+ func testQwertyDachenKeys( ) async throws {
91189 // Testing Dachen Traditional Mapping (QWERTY)
92190 var c = Tekkon . Composer ( arrange: . ofDachen)
93191 var counter = 0
@@ -106,51 +204,44 @@ final class TekkonTestsKeyboardArrangmentsStatic: XCTestCase {
106204 Self . checkEq ( & counter, & c, " hl3 " , " ㄘㄠˇ " )
107205 Self . checkEq ( & counter, & c, " 5 " , " ㄓ " )
108206 Self . checkEq ( & counter, & c, " 193 " , " ㄅㄞˇ " )
109- XCTAssertEqual ( counter, 0 )
207+ #expect ( counter == 0 )
110208 }
111209}
112210
113211// MARK: - TekkonTestsKeyboardArrangmentsDynamic
114212
115- final class TekkonTestsKeyboardArrangmentsDynamic : XCTestCase {
213+ @MainActor
214+ @Suite ( . serialized)
215+ struct TekkonTestsKeyboardArrangmentsDynamic {
116216 typealias Parser = Tekkon . MandarinParser
117217
118- func testDynamicKeyLayouts( ) {
119- // 遍歷所有動態鍵盤配置
120- for (idxRaw, parser) in Parser . allCases. filter ( \. isDynamic) . enumerated ( ) {
121- var cases = [ SubTestCase? ] ( )
122- print ( " -> [Tekkon] Preparing tests for dynamic keyboard handling... " )
123- var isTitleLine = true
124- testTable4DynamicLayouts. parse ( splitee: " \n " ) { theRange in
125- guard !isTitleLine else {
126- isTitleLine = false
127- return
128- }
129- let cells = testTable4DynamicLayouts [ theRange] . split ( separator: " " )
130- let expected = cells [ 0 ]
131- let idx = idxRaw + 1
132- let typing = cells [ idx]
133- let newTestCase = SubTestCase (
134- parser: parser,
135- typing: typing. description,
136- expected: expected. description
137- )
138- cases. append ( newTestCase)
139- }
140- let timeTag = Date . now
141- print ( " -> [Tekkon][( \( parser. nameTag) )] Starting dynamic keyboard handling test ... " )
142- let results = cases. compactMap { testCase in
143- ( testCase? . verify ( ) ?? true ) ? 0 : 1
144- } . reduce ( 0 , + )
145- XCTAssertEqual (
146- results, 0 ,
147- " [Failure] \( parser. nameTag) failed from being handled correctly with \( results) bad results. "
148- )
149- let timeDelta = Date . now. timeIntervalSince1970 - timeTag. timeIntervalSince1970
150- let timeDeltaStr = String ( format: " %.4f " , timeDelta)
151- print (
152- " -> [Tekkon][( \( parser. nameTag) )] Finished within \( timeDeltaStr) seconds. "
153- )
154- }
218+ @Test (
219+ " [Tekkon] DynamicKeyLayouts " ,
220+ arguments: Array ( Parser . allCases. filter ( \. isDynamic) . enumerated ( ) )
221+ )
222+ func testDynamicKeyLayouts(
223+ _ parserEnumerated: EnumeratedSequence < [ Parser ] >
224+ . Element
225+ ) async throws {
226+ let parser = parserEnumerated. element
227+ print ( " -> [Tekkon] Preparing tests for dynamic keyboard handling... " )
228+
229+ // 使用批量處理器以提升效能
230+ let testBatch = TestCaseBatch ( parser: parser, rawData: testTable4DynamicLayouts)
231+
232+ let timeTag = Date . now
233+ print ( " -> [Tekkon][( \( parser. nameTag) )] Starting dynamic keyboard handling test ... " )
234+
235+ let failures = await testBatch. runTests ( )
236+
237+ #expect(
238+ failures == 0 ,
239+ " [Failure] \( parser. nameTag) failed from being handled correctly with \( failures) bad results. "
240+ )
241+ let timeDelta = Date . now. timeIntervalSince1970 - timeTag. timeIntervalSince1970
242+ let timeDeltaStr = String ( format: " %.4f " , timeDelta)
243+ print (
244+ " -> [Tekkon][( \( parser. nameTag) )] Finished within \( timeDeltaStr) seconds. "
245+ )
155246 }
156247}
0 commit comments