summaryrefslogtreecommitdiff
path: root/SoraTests/ViewDerivedDataTests.swift
diff options
context:
space:
mode:
Diffstat (limited to 'SoraTests/ViewDerivedDataTests.swift')
-rw-r--r--SoraTests/ViewDerivedDataTests.swift192
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)
+ }
+}