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..