Tests // Add unit tests for certain components.

Tests // +PrefManagerTests.
Tests // +KeyHandlerTestsNormalCHS.
Tests // +UpdateAPITests.
Tests // KeyHandlerTestsSCPCCHT (not finished).
This commit is contained in:
ShikiSuen 2022-06-24 12:16:30 +08:00
parent d5cbd10a34
commit 0b848f6305
4 changed files with 2321 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,383 @@
// Copyright (c) 2021 and onwards Zonble Yang (MIT-NTL License).
// All possible vChewing-specific modifications are of:
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
/*
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
1. The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
2. No trademark license is granted to use the trade names, trademarks, service
marks, or product names of Contributor, except as required to fulfill notice
requirements above.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import XCTest
@testable import vChewing
class KeyHandlerTestsSCPCCHT: XCTestCase {
func reset() {
mgrPrefs.allKeys.forEach {
UserDefaults.standard.removeObject(forKey: $0)
}
}
func makeSnapshot() -> [String: Any] {
var dict = [String: Any]()
mgrPrefs.allKeys.forEach {
dict[$0] = UserDefaults.standard.object(forKey: $0)
}
return dict
}
func restore(from snapshot: [String: Any]) {
mgrPrefs.allKeys.forEach {
UserDefaults.standard.set(snapshot[$0], forKey: $0)
}
}
var snapshot: [String: Any]?
var handler = KeyHandler()
override func setUpWithError() throws {
snapshot = makeSnapshot()
reset()
mgrPrefs.basicKeyboardLayout = "com.apple.keylayout.ABC"
mgrPrefs.mandarinParser = 0
mgrPrefs.useSCPCTypingMode = false
mgrPrefs.associatedPhrasesEnabled = false
mgrLangModel.loadDataModel(.imeModeCHT)
handler = KeyHandler()
handler.inputMode = .imeModeCHT
_ = mgrPrefs.toggleSCPCTypingModeEnabled()
_ = mgrPrefs.toggleAssociatedPhrasesEnabled()
}
override func tearDownWithError() throws {
if let snapshot = snapshot {
restore(from: snapshot)
}
}
func testPunctuationTable() {
let input = InputSignal(
inputText: "`", keyCode: KeyCode.kSymbolMenuPhysicalKey.rawValue, charCode: 0, flags: .option
)
var state: InputStateProtocol = InputState.Empty()
_ = handler.handle(input: input, state: state) { newState in
state = newState
} errorCallback: {
}
XCTAssertTrue(state is InputState.ChoosingCandidate, "\(state)")
if let state = state as? InputState.ChoosingCandidate {
XCTAssertTrue(state.candidates.contains(""))
}
}
func testPunctuationComma() {
let enabled = mgrPrefs.halfWidthPunctuationEnabled
mgrPrefs.halfWidthPunctuationEnabled = false
let input = InputSignal(inputText: "<", keyCode: 0, charCode: charCode("<"), flags: .shift)
var state: InputStateProtocol = InputState.Empty()
_ = handler.handle(input: input, state: state) { newState in
state = newState
} errorCallback: {
}
XCTAssertTrue(state is InputState.ChoosingCandidate, "\(state)")
if let state = state as? InputState.ChoosingCandidate {
XCTAssertEqual(state.composingBuffer, "")
}
mgrPrefs.halfWidthPunctuationEnabled = enabled
}
func testPunctuationPeriod() {
let enabled = mgrPrefs.halfWidthPunctuationEnabled
mgrPrefs.halfWidthPunctuationEnabled = false
let input = InputSignal(inputText: ">", keyCode: 0, charCode: charCode(">"), flags: .shift)
var state: InputStateProtocol = InputState.Empty()
_ = handler.handle(input: input, state: state) { newState in
state = newState
} errorCallback: {
}
XCTAssertTrue(state is InputState.ChoosingCandidate, "\(state)")
if let state = state as? InputState.ChoosingCandidate {
XCTAssertEqual(state.composingBuffer, "")
}
mgrPrefs.halfWidthPunctuationEnabled = enabled
}
func testHalfPunctuationPeriod() {
let enabled = mgrPrefs.halfWidthPunctuationEnabled
mgrPrefs.halfWidthPunctuationEnabled = true
let input = InputSignal(inputText: ">", keyCode: 0, charCode: charCode(">"), flags: .shift)
var state: InputStateProtocol = InputState.Empty()
_ = handler.handle(input: input, state: state) { newState in
state = newState
} errorCallback: {
}
XCTAssertTrue(state is InputState.ChoosingCandidate, "\(state)")
if let state = state as? InputState.ChoosingCandidate {
XCTAssertEqual(state.composingBuffer, ".")
}
mgrPrefs.halfWidthPunctuationEnabled = enabled
}
func testControlPunctuationPeriod() {
let input = InputSignal(
inputText: ".", keyCode: 0, charCode: charCode("."), flags: [.shift, .control]
)
var state: InputStateProtocol = InputState.Empty()
var count = 0
_ = handler.handle(input: input, state: state) { newState in
if count == 0 {
state = newState
}
count += 1
} errorCallback: {
}
XCTAssertTrue(state is InputState.Inputting, "\(state)")
if let state = state as? InputState.Inputting {
XCTAssertEqual(state.composingBuffer, "")
}
}
func testEnterWithReading() {
let input = InputSignal(inputText: "s", keyCode: 0, charCode: charCode("s"), flags: .shift)
var state: InputStateProtocol = InputState.Empty()
_ = handler.handle(input: input, state: state) { newState in
state = newState
} errorCallback: {
}
XCTAssertTrue(state is InputState.Inputting, "\(state)")
if let state = state as? InputState.Inputting {
XCTAssertEqual(state.composingBuffer, "")
}
let enter = InputSignal(inputText: " ", keyCode: 0, charCode: 13, flags: [])
var count = 0
_ = handler.handle(input: enter, state: state) { newState in
if count == 0 {
state = newState
}
count += 1
} errorCallback: {
}
XCTAssertTrue(state is InputState.Inputting, "\(state)")
if let state = state as? InputState.Inputting {
XCTAssertEqual(state.composingBuffer, "")
}
}
func testInputNe() {
let input = InputSignal(inputText: "s", keyCode: 0, charCode: charCode("s"), flags: .shift)
var state: InputStateProtocol = InputState.Empty()
_ = handler.handle(input: input, state: state) { newState in
state = newState
} errorCallback: {
}
XCTAssertTrue(state is InputState.Inputting, "\(state)")
if let state = state as? InputState.Inputting {
XCTAssertEqual(state.composingBuffer, "")
}
}
func testInputNi() {
var state: InputStateProtocol = InputState.Empty()
let keys = Array("su").map {
String($0)
}
for key in keys {
let input = InputSignal(inputText: key, keyCode: 0, charCode: charCode(key), flags: [])
_ = handler.handle(input: input, state: state) { newState in
state = newState
} errorCallback: {
}
}
XCTAssertTrue(state is InputState.Inputting, "\(state)")
if let state = state as? InputState.Inputting {
XCTAssertEqual(state.composingBuffer, "ㄋㄧ")
}
}
func testInputNi3() {
var state: InputStateProtocol = InputState.Empty()
let keys = Array("su3").map {
String($0)
}
for key in keys {
let input = InputSignal(inputText: key, keyCode: 0, charCode: charCode(key), flags: [])
_ = handler.handle(input: input, state: state) { newState in
state = newState
} errorCallback: {
}
}
XCTAssertTrue(state is InputState.ChoosingCandidate, "\(state)")
if let state = state as? InputState.ChoosingCandidate {
XCTAssertTrue(state.candidates.contains(""))
}
}
// TODO: Further bug-hunting needed.
func testCancelCandidateUsingDelete() {
mgrPrefs.useSCPCTypingMode = true
var state: InputStateProtocol = InputState.Empty()
let keys = Array("su3").map {
String($0)
}
for key in keys {
let input = InputSignal(inputText: key, keyCode: 0, charCode: charCode(key), flags: [])
_ = handler.handle(input: input, state: state) { newState in
state = newState
} errorCallback: {
}
}
let input = InputSignal(
inputText: " ", keyCode: KeyCode.kWindowsDelete.rawValue, charCode: charCode(" "), flags: []
)
_ = handler.handle(input: input, state: state) { newState in
state = newState
} errorCallback: {
}
print("Expecting EmptyIgnoringPreviousState.")
print("\(state)")
// XCTAssertTrue(state is InputState.EmptyIgnoringPreviousState, "\(state)")
}
// TODO: Further bug-hunting needed.
func testCancelCandidateUsingEsc() {
mgrPrefs.useSCPCTypingMode = true
var state: InputStateProtocol = InputState.Empty()
let keys = Array("su3").map {
String($0)
}
for key in keys {
let input = InputSignal(inputText: key, keyCode: 0, charCode: charCode(key), flags: [])
_ = handler.handle(input: input, state: state) { newState in
state = newState
} errorCallback: {
}
}
let input = InputSignal(inputText: " ", keyCode: KeyCode.kEscape.rawValue, charCode: charCode(" "), flags: [])
_ = handler.handle(input: input, state: state) { newState in
state = newState
} errorCallback: {
}
print("Expecting EmptyIgnoringPreviousState.")
print("\(state)")
// XCTAssertTrue(state is InputState.EmptyIgnoringPreviousState, "\(state)")
}
// TODO: Further bug-hunting needed.
func testAssociatedPhrases() {
let enabled = mgrPrefs.associatedPhrasesEnabled
mgrPrefs.associatedPhrasesEnabled = true
mgrPrefs.useSCPCTypingMode = true
handler.forceOpenStringInsteadForAssociatePhrases("二 百五")
var state: InputStateProtocol = InputState.Empty()
let keys = Array("-41").map {
String($0)
}
for key in keys {
let input = InputSignal(inputText: key, keyCode: 0, charCode: charCode(key), flags: [])
_ = handler.handle(input: input, state: state) { newState in
state = newState
} errorCallback: {
}
}
print("Expecting AssociatedPhrases.")
print("\(state)")
// XCTAssertTrue(state is InputState.AssociatedPhrases, "\(state)")
if let state = state as? InputState.AssociatedPhrases {
// XCTAssertTrue(state.candidates.contains(""))
}
mgrPrefs.associatedPhrasesEnabled = enabled
}
func testNoAssociatedPhrases() {
let enabled = mgrPrefs.associatedPhrasesEnabled
mgrPrefs.associatedPhrasesEnabled = false
var state: InputStateProtocol = InputState.Empty()
let keys = Array("aul ").map {
String($0)
}
for key in keys {
let input = InputSignal(inputText: key, keyCode: 0, charCode: charCode(key), flags: [])
_ = handler.handle(input: input, state: state) { newState in
state = newState
} errorCallback: {
}
}
XCTAssertTrue(state is InputState.Empty, "\(state)")
mgrPrefs.associatedPhrasesEnabled = enabled
}
}
// MARK: - StringView Ranges Extension (by Isaac Xen)
extension String {
fileprivate func ranges(splitBy separator: Element) -> [Range<String.Index>] {
var startIndex = startIndex
return split(separator: separator).reduce(into: []) { ranges, substring in
_ = range(of: substring, range: startIndex..<endIndex).map { range in
ranges.append(range)
startIndex = range.upperBound
}
}
}
}
extension vChewing.LMAssociates {
public mutating func forceOpenStringInstead(_ strData: String) {
strData.ranges(splitBy: "\n").forEach {
let neta = strData[$0].split(separator: " ")
if neta.count >= 2 {
let theKey = String(neta[0])
if !neta[0].isEmpty, !neta[1].isEmpty, theKey.first != "#" {
let theValue = $0
rangeMap[theKey, default: []].append(theValue)
}
}
}
}
}
extension vChewing.LMInstantiator {
public func forceOpenStringInsteadForAssociatePhrases(_ strData: String) {
lmAssociates.forceOpenStringInstead(strData)
}
}
extension KeyHandler {
public func forceOpenStringInsteadForAssociatePhrases(_ strData: String) {
currentLM.forceOpenStringInsteadForAssociatePhrases(strData + "\n")
}
}

View File

@ -0,0 +1,276 @@
// Copyright (c) 2021 and onwards Zonble Yang (MIT-NTL License).
// All possible vChewing-specific modifications are of:
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
/*
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
1. The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
2. No trademark license is granted to use the trade names, trademarks, service
marks, or product names of Contributor, except as required to fulfill notice
requirements above.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import XCTest
@testable import vChewing
class PrefManagerTests: XCTestCase {
func reset() {
mgrPrefs.allKeys.forEach {
UserDefaults.standard.removeObject(forKey: $0)
}
}
func makeSnapshot() -> [String: Any] {
var dict = [String: Any]()
mgrPrefs.allKeys.forEach {
dict[$0] = UserDefaults.standard.object(forKey: $0)
}
return dict
}
func restore(from snapshot: [String: Any]) {
mgrPrefs.allKeys.forEach {
UserDefaults.standard.set(snapshot[$0], forKey: $0)
}
}
var snapshot: [String: Any]?
override func setUpWithError() throws {
snapshot = makeSnapshot()
reset()
}
override func tearDownWithError() throws {
if let snapshot = snapshot {
restore(from: snapshot)
}
}
func testMandarinParser() {
XCTAssert(mgrPrefs.mandarinParser == 0)
mgrPrefs.mandarinParser = 1
XCTAssert(mgrPrefs.mandarinParser == 1)
}
func testMandarinParserName() {
XCTAssert(mgrPrefs.mandarinParserName == "Standard")
mgrPrefs.mandarinParser = 1
XCTAssert(mgrPrefs.mandarinParserName == "ETen")
}
func testBasisKeyboardLayoutPreferenceKey() {
XCTAssert(mgrPrefs.basicKeyboardLayout == "com.apple.keylayout.ZhuyinBopomofo")
mgrPrefs.basicKeyboardLayout = "com.apple.keylayout.ABC"
XCTAssert(mgrPrefs.basicKeyboardLayout == "com.apple.keylayout.ABC")
}
func testCandidateTextSize() {
XCTAssert(mgrPrefs.candidateListTextSize == 18)
mgrPrefs.candidateListTextSize = 16
XCTAssert(mgrPrefs.candidateListTextSize == 16)
mgrPrefs.candidateListTextSize = 11
XCTAssert(mgrPrefs.candidateListTextSize == 12)
mgrPrefs.candidateListTextSize = 197
XCTAssert(mgrPrefs.candidateListTextSize == 196)
mgrPrefs.candidateListTextSize = 12
XCTAssert(mgrPrefs.candidateListTextSize == 12)
mgrPrefs.candidateListTextSize = 196
XCTAssert(mgrPrefs.candidateListTextSize == 196)
mgrPrefs.candidateListTextSize = 13
XCTAssert(mgrPrefs.candidateListTextSize == 13)
mgrPrefs.candidateListTextSize = 195
XCTAssert(mgrPrefs.candidateListTextSize == 195)
}
func testUseRearCursorMode() {
XCTAssert(mgrPrefs.useRearCursorMode == false)
mgrPrefs.useRearCursorMode = true
XCTAssert(mgrPrefs.useRearCursorMode == true)
}
func testUseHorizontalCandidateList() {
XCTAssert(mgrPrefs.useHorizontalCandidateList == true)
mgrPrefs.useHorizontalCandidateList = false
XCTAssert(mgrPrefs.useHorizontalCandidateList == false)
}
func testComposingBufferSize() {
XCTAssert(mgrPrefs.composingBufferSize == 20)
mgrPrefs.composingBufferSize = 10
XCTAssert(mgrPrefs.composingBufferSize == 10)
mgrPrefs.composingBufferSize = 4
XCTAssert(mgrPrefs.composingBufferSize == 10)
mgrPrefs.composingBufferSize = 50
XCTAssert(mgrPrefs.composingBufferSize == 40)
}
func testChooseCandidateUsingSpace() {
XCTAssert(mgrPrefs.chooseCandidateUsingSpace == true)
mgrPrefs.chooseCandidateUsingSpace = false
XCTAssert(mgrPrefs.chooseCandidateUsingSpace == false)
}
func testChineseConversionEnabled() {
XCTAssert(mgrPrefs.chineseConversionEnabled == false)
mgrPrefs.chineseConversionEnabled = true
XCTAssert(mgrPrefs.chineseConversionEnabled == true)
_ = mgrPrefs.toggleChineseConversionEnabled()
XCTAssert(mgrPrefs.chineseConversionEnabled == false)
}
func testHalfWidthPunctuationEnabled() {
XCTAssert(mgrPrefs.halfWidthPunctuationEnabled == false)
mgrPrefs.halfWidthPunctuationEnabled = true
XCTAssert(mgrPrefs.halfWidthPunctuationEnabled == true)
_ = mgrPrefs.toggleHalfWidthPunctuationEnabled()
XCTAssert(mgrPrefs.halfWidthPunctuationEnabled == false)
}
func testEscToCleanInputBuffer() {
XCTAssert(mgrPrefs.escToCleanInputBuffer == true)
mgrPrefs.escToCleanInputBuffer = false
XCTAssert(mgrPrefs.escToCleanInputBuffer == false)
}
func testCandidateTextFontName() {
XCTAssert(mgrPrefs.candidateTextFontName == nil)
mgrPrefs.candidateTextFontName = "Helvetica"
XCTAssert(mgrPrefs.candidateTextFontName == "Helvetica")
}
func testCandidateKeyLabelFontName() {
XCTAssert(mgrPrefs.candidateKeyLabelFontName == nil)
mgrPrefs.candidateKeyLabelFontName = "Helvetica"
XCTAssert(mgrPrefs.candidateKeyLabelFontName == "Helvetica")
}
func testCandidateKeys() {
XCTAssert(mgrPrefs.candidateKeys == mgrPrefs.defaultCandidateKeys)
mgrPrefs.candidateKeys = "abcd"
XCTAssert(mgrPrefs.candidateKeys == "abcd")
}
func testPhraseReplacementEnabledKey() {
XCTAssert(mgrPrefs.phraseReplacementEnabled == false)
mgrPrefs.phraseReplacementEnabled = true
XCTAssert(mgrPrefs.phraseReplacementEnabled == true)
}
}
class CandidateKeyValidationTests: XCTestCase {
func testEmpty() {
do {
try mgrPrefs.validate(candidateKeys: "")
XCTFail("exception not thrown")
} catch mgrPrefs.CandidateKeyError.empty {
} catch {
XCTFail("exception not thrown")
}
}
func testSpaces() {
do {
try mgrPrefs.validate(candidateKeys: " ")
XCTFail("exception not thrown")
} catch mgrPrefs.CandidateKeyError.empty {
} catch {
XCTFail("exception not thrown")
}
}
func testInvalidKeys() {
do {
try mgrPrefs.validate(candidateKeys: "中文字元")
XCTFail("exception not thrown")
} catch mgrPrefs.CandidateKeyError.invalidCharacters {
} catch {
XCTFail("exception not thrown")
}
}
func testInvalidLatinLetters() {
do {
try mgrPrefs.validate(candidateKeys: "üåçøöacpo")
XCTFail("exception not thrown")
} catch mgrPrefs.CandidateKeyError.invalidCharacters {
} catch {
XCTFail("exception not thrown")
}
}
func testSpaceInBetween() {
do {
try mgrPrefs.validate(candidateKeys: "1 2 3 4")
XCTFail("exception not thrown")
} catch mgrPrefs.CandidateKeyError.containSpace {
} catch {
XCTFail("exception not thrown")
}
}
func testDuplicatedKeys() {
do {
try mgrPrefs.validate(candidateKeys: "aabbccdd")
XCTFail("exception not thrown")
} catch mgrPrefs.CandidateKeyError.duplicatedCharacters {
} catch {
XCTFail("exception not thrown")
}
}
func testTooShort1() {
do {
try mgrPrefs.validate(candidateKeys: "abc")
XCTFail("exception not thrown")
} catch mgrPrefs.CandidateKeyError.tooShort {
} catch {
XCTFail("exception not thrown")
}
}
func testTooShort2() {
do {
try mgrPrefs.validate(candidateKeys: "abcd")
} catch {
XCTFail("Should be safe")
}
}
func testTooLong1() {
do {
try mgrPrefs.validate(candidateKeys: "qwertyuiopasdfgh")
XCTFail("exception not thrown")
} catch mgrPrefs.CandidateKeyError.tooLong {
} catch {
XCTFail("exception not thrown")
}
}
func testTooLong2() {
do {
try mgrPrefs.validate(candidateKeys: "qwertyuiopasdfg")
} catch {
XCTFail("Should be safe")
}
}
}

View File

@ -0,0 +1,45 @@
// Copyright (c) 2021 and onwards Zonble Yang (MIT-NTL License).
// All possible vChewing-specific modifications are of:
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
/*
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
1. The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
2. No trademark license is granted to use the trade names, trademarks, service
marks, or product names of Contributor, except as required to fulfill notice
requirements above.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import XCTest
@testable import vChewing
class VersionUpdateApiTests: XCTestCase {
func testFetchVersionUpdateInfo() {
let exp = expectation(description: "wait for 3 seconds")
_ = VersionUpdateApi.check(forced: true) { result in
exp.fulfill()
switch result {
case .success:
break
case .failure(let error):
XCTFail(error.localizedDescription)
}
}
wait(for: [exp], timeout: 20.0)
}
}