diff options
Diffstat (limited to 'SoraTests/ViewDerivedDataTests.swift')
| -rw-r--r-- | SoraTests/ViewDerivedDataTests.swift | 192 |
1 files changed, 192 insertions, 0 deletions
diff --git a/SoraTests/ViewDerivedDataTests.swift b/SoraTests/ViewDerivedDataTests.swift new file mode 100644 index 0000000..6737265 --- /dev/null +++ b/SoraTests/ViewDerivedDataTests.swift @@ -0,0 +1,192 @@ +import Foundation +import XCTest + +final class ViewDerivedDataTests: XCTestCase { + func testGenericListViewDerivedCollectionsAreReferencedOncePerRenderPass() throws { + let source = try loadSource(at: "Sora/Views/Generic/GenericListView.swift") + let normalizedSource = strippingCommentsAndStrings(from: source) + + let filteredItemsUsages = referenceCount( + for: "filteredItems", + in: normalizedSource + ) + let sortedFilteredItemsUsages = referenceCount( + for: "sortedFilteredItems", + in: normalizedSource + ) + + // swiftlint:disable:next prefer_nimble + XCTAssertLessThanOrEqual( + filteredItemsUsages, + 1, + "filteredItems should be consumed once per dependency change." + ) + // swiftlint:disable:next prefer_nimble + XCTAssertLessThanOrEqual( + sortedFilteredItemsUsages, + 1, + "sortedFilteredItems should be consumed once per dependency change." + ) + } + + func testPostGridViewDerivedCollectionsAreReferencedOncePerRenderPass() throws { + let source = try loadSource(at: "Sora/Views/Post/Grid/PostGridView.swift") + let normalizedSource = strippingCommentsAndStrings(from: source) + + let activePostsUsages = referenceCount( + for: "activePosts", + in: normalizedSource + ) + let getColumnsDataUsages = invocationCount( + forFunction: "getColumnsData", + in: normalizedSource + ) + + // swiftlint:disable:next prefer_nimble + XCTAssertLessThanOrEqual( + activePostsUsages, + 1, + "activePosts-derived data should be consumed once per dependency change." + ) + // swiftlint:disable:next prefer_nimble + XCTAssertLessThanOrEqual( + getColumnsDataUsages, + 1, + "getColumnsData should be invoked once per dependency change." + ) + } + + private func loadSource(at relativePath: String) throws -> String { + let currentFile = URL(fileURLWithPath: #filePath) + let repositoryRoot = currentFile.deletingLastPathComponent().deletingLastPathComponent() + let fileURL = repositoryRoot.appendingPathComponent(relativePath) + + return try String(contentsOf: fileURL, encoding: .utf8) + } + + // swiftlint:disable:next cyclomatic_complexity + private func strippingCommentsAndStrings(from source: String) -> String { + let characters = Array(source) + var result: [Character] = [] + result.reserveCapacity(characters.count) + + var currentOffset = 0 + var inLineComment = false + var blockCommentDepth = 0 + var inString = false + var isEscaped = false + + while currentOffset < characters.count { + let current = characters[currentOffset] + let next: Character? = currentOffset + 1 < characters.count ? characters[currentOffset + 1] : nil + + if inLineComment { + if current == "\n" { + inLineComment = false + result.append("\n") + } else { + result.append(" ") + } + currentOffset += 1 + continue + } + + if blockCommentDepth > 0 { + if current == "/", next == "*" { + blockCommentDepth += 1 + result.append(" ") + result.append(" ") + currentOffset += 2 + continue + } + + if current == "*", next == "/" { + blockCommentDepth -= 1 + result.append(" ") + result.append(" ") + currentOffset += 2 + continue + } + + result.append(current == "\n" ? "\n" : " ") + currentOffset += 1 + continue + } + + if inString { + if isEscaped { + isEscaped = false + } else if current == "\\" { + isEscaped = true + } else if current == "\"" { + inString = false + } + + result.append(current == "\n" ? "\n" : " ") + currentOffset += 1 + continue + } + + if current == "/", next == "/" { + inLineComment = true + result.append(" ") + result.append(" ") + currentOffset += 2 + continue + } + + if current == "/", next == "*" { + blockCommentDepth = 1 + result.append(" ") + result.append(" ") + currentOffset += 2 + continue + } + + if current == "\"" { + inString = true + result.append(" ") + currentOffset += 1 + continue + } + + result.append(current) + currentOffset += 1 + } + + return String(result) + } + + private func referenceCount(for symbol: String, in source: String) -> Int { + let totalMatches = tokenCount( + matching: #"\b\#(symbol)\b"#, + in: source + ) + let declarationMatches = tokenCount( + matching: #"\b(?:var|let)\s+\#(symbol)\b"#, + in: source + ) + + return max(0, totalMatches - declarationMatches) + } + + private func invocationCount(forFunction name: String, in source: String) -> Int { + let totalMatches = tokenCount( + matching: #"\b\#(name)\s*\("#, + in: source + ) + let declarationMatches = tokenCount( + matching: #"\bfunc\s+\#(name)\s*\("#, + in: source + ) + + return max(0, totalMatches - declarationMatches) + } + + private func tokenCount(matching pattern: String, in source: String) -> Int { + guard let regex = try? NSRegularExpression(pattern: pattern) else { return 0 } + let range = NSRange(source.startIndex..<source.endIndex, in: source) + + return regex.numberOfMatches(in: source, range: range) + } +} |