From bb4768783059f677b6bf00cd19797936e826eeab Mon Sep 17 00:00:00 2001 From: Hoang Pham Date: Sat, 4 Oct 2025 16:43:28 +0700 Subject: [PATCH 1/2] Fix workflow --- .github/workflows/tests.yml | 14 +++++--------- Package.swift | 6 ------ README.md | 2 +- 3 files changed, 6 insertions(+), 16 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2d22f2b..92ef809 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -2,24 +2,20 @@ name: Tests on: push: - branches: [ main ] + branches: [ main, 'feature/**', 'fix/**' ] pull_request: branches: [ main ] + workflow_dispatch: jobs: test: name: Test SwiftUI Query - runs-on: macos-14 - + runs-on: macos-15 + steps: - name: Checkout uses: actions/checkout@v4 - - - name: Setup Swift - uses: swift-actions/setup-swift@v2 - with: - swift-version: "6.2" - + - name: Show Swift version run: swift --version diff --git a/Package.swift b/Package.swift index d4dbefb..3c2b2ed 100644 --- a/Package.swift +++ b/Package.swift @@ -24,18 +24,12 @@ let package = Package( name: "SwiftUIQuery", dependencies: [ .product(name: "Perception", package: "swift-perception") - ], - swiftSettings: [ - .enableUpcomingFeature("StrictConcurrency") ] ), .testTarget( name: "SwiftUIQueryTests", dependencies: [ "SwiftUIQuery", - ], - swiftSettings: [ - .enableUpcomingFeature("StrictConcurrency") ] ), ] diff --git a/README.md b/README.md index 708234e..9a1fb96 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # SwiftUI Query -[![Tests](https://github.com/muzix/swiftui-query/actions/workflows/tests.yml/badge.svg)](https://github.com/muzix/swiftui-query/actions/workflows/tests.yml) +[![Tests](https://github.com/muzix/SwiftUIQuery/actions/workflows/tests.yml/badge.svg)](https://github.com/muzix/SwiftUIQuery/actions/workflows/tests.yml) [![Swift 6.0](https://img.shields.io/badge/Swift-6.0-orange.svg)](https://swift.org) [![Platforms](https://img.shields.io/badge/Platforms-iOS%20|%20macOS%20|%20tvOS%20|%20watchOS-blue.svg)](https://swift.org) [![License](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE) From 6e7ebdd9e91eda8d6bce5cbf2dbc092aabac8348 Mon Sep 17 00:00:00 2001 From: Hoang Pham Date: Sat, 4 Oct 2025 17:31:23 +0700 Subject: [PATCH 2/2] fix workflow --- .github/workflows/tests.yml | 30 +++++---------- .swiftlint.yml | 11 ++---- Makefile | 4 +- .../SwiftUIQuery/Core/GarbageCollector.swift | 24 ++++-------- Sources/SwiftUIQuery/InfiniteQuery.swift | 8 +--- Sources/SwiftUIQuery/Query.swift | 8 +--- Sources/SwiftUIQuery/QueryLogger.swift | 38 +++++++++++++++++++ Tests/SwiftUIQueryTests/KeyTupleTests.swift | 26 ++++++------- .../swiftui_queryTests.swift | 20 ---------- 9 files changed, 78 insertions(+), 91 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 92ef809..5230d34 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -2,7 +2,7 @@ name: Tests on: push: - branches: [ main, 'feature/**', 'fix/**' ] + branches: [ main ] pull_request: branches: [ main ] workflow_dispatch: @@ -18,12 +18,15 @@ jobs: - name: Show Swift version run: swift --version - - - name: Run Tests - run: swift test --parallel - + + - name: Install SwiftLint + run: brew install swiftlint + + - name: Lint Code + run: make lint + - name: Run Tests with Coverage - run: swift test --enable-code-coverage + run: swift test --parallel --enable-code-coverage - name: Generate Coverage Report run: | @@ -39,17 +42,4 @@ jobs: with: file: ./coverage.lcov fail_ci_if_error: false - continue-on-error: true - - lint: - name: Swift Format - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Swift Format Lint - uses: norio-nomura/action-swiftlint@3.2.1 - with: - args: --strict \ No newline at end of file + continue-on-error: true \ No newline at end of file diff --git a/.swiftlint.yml b/.swiftlint.yml index b8d211b..8dd3f2c 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -4,10 +4,10 @@ included: - Sources - Tests - - Example excluded: - Documentation + - Example - .build - .swiftpm - Package.swift @@ -23,7 +23,8 @@ disabled_rules: - function_parameter_count # Allow functions with more than 6 parameters - redundant_string_enum_value # Allow explicit raw values for consistency - raw_value_for_camel_cased_codable_enum # Disable conflicting camel case enum rule - + - closure_parameter_position + - optional_data_string_conversion # Per-path rule customization opt_in_rules: @@ -92,6 +93,7 @@ opt_in_rules: - vertical_whitespace_opening_braces - weak_delegate - yoda_condition + - non_optional_string_data_conversion # Rule configuration analyzer_rules: @@ -184,11 +186,6 @@ custom_rules: severity: warning - observable_usage: - name: "Observable Usage" - regex: 'ObservableObject' - message: "Consider using @Observable instead of ObservableObject for Swift 6" - severity: warning # Reporter type (xcode, json, csv, checkstyle, junit, html, emoji) reporter: "xcode" \ No newline at end of file diff --git a/Makefile b/Makefile index f2f25ce..64f34e7 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ install: ## Install dependencies (SwiftLint and SwiftFormat) .PHONY: lint lint: ## Run SwiftLint - swiftlint + swiftlint --strict .PHONY: lint-fix lint-fix: ## Run SwiftLint with auto-fix @@ -40,7 +40,7 @@ fix: ## Fix all issues (lint auto-fix + format) .PHONY: build build: ## Build the package - swift build -Xswiftc -strict-concurrency=complete + swift build .PHONY: test test: ## Run tests diff --git a/Sources/SwiftUIQuery/Core/GarbageCollector.swift b/Sources/SwiftUIQuery/Core/GarbageCollector.swift index 1e7bc1f..2a26b2d 100644 --- a/Sources/SwiftUIQuery/Core/GarbageCollector.swift +++ b/Sources/SwiftUIQuery/Core/GarbageCollector.swift @@ -49,9 +49,7 @@ public final class GarbageCollector { isRunning = true - #if DEBUG - print("🗑️ SwiftUI Query: Starting GarbageCollector with \(interval)s interval") - #endif + QueryLogger.shared.logGCStart(interval: interval) timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { [weak self] _ in Task { @MainActor [weak self] in @@ -68,9 +66,7 @@ public final class GarbageCollector { timer?.invalidate() timer = nil - #if DEBUG - print("🗑️ SwiftUI Query: Stopping GarbageCollector") - #endif + QueryLogger.shared.logGCStop() } /// Register a query cache for garbage collection monitoring @@ -132,22 +128,16 @@ public final class GarbageCollector { for query in inactiveQueries { cache.remove(query) removedQueries += 1 - - #if DEBUG - print("🗑️ SwiftUI Query: GC removed inactive query \(query.queryHash)") - #endif + QueryLogger.shared.logGCRemoving(hash: query.queryHash, reason: "inactive query") } } let duration = Date().timeIntervalSince(startTime) - #if DEBUG - if removedQueries > 0 { - print( - "🗑️ SwiftUI Query: GC completed - removed \(removedQueries)/\(totalQueries) queries in \(String(format: "%.2f", duration * 1000))ms" - ) - } - #endif + if removedQueries > 0 { + let reason = "removed \(removedQueries)/\(totalQueries) queries in \(String(format: "%.2f", duration * 1000))ms" + QueryLogger.shared.logGCRemoving(hash: "GC-Complete", reason: reason) + } } /// Check if a query is eligible for garbage collection diff --git a/Sources/SwiftUIQuery/InfiniteQuery.swift b/Sources/SwiftUIQuery/InfiniteQuery.swift index 9e9168c..65124ee 100644 --- a/Sources/SwiftUIQuery/InfiniteQuery.swift +++ b/Sources/SwiftUIQuery/InfiniteQuery.swift @@ -867,15 +867,11 @@ public final class InfiniteQuery< public func optionalRemove() { // Only remove if query has no observers and is eligible for GC guard observers.isEmpty, state.fetchStatus == .idle, isEligibleForGC else { - #if DEBUG - print("🗑️ SwiftUI Query: GC cancelled for \(queryHash) - Query is active or not eligible") - #endif + QueryLogger.shared.logGCCancelled(hash: queryHash) return } - #if DEBUG - print("🗑️ SwiftUI Query: Executing GC for \(queryHash)") - #endif + QueryLogger.shared.logGCExecuting(hash: queryHash) // Remove from cache (let cache handle the cleanup) cache?.remove(self) diff --git a/Sources/SwiftUIQuery/Query.swift b/Sources/SwiftUIQuery/Query.swift index 4538057..1eda3ec 100644 --- a/Sources/SwiftUIQuery/Query.swift +++ b/Sources/SwiftUIQuery/Query.swift @@ -709,15 +709,11 @@ public final class Query: AnyQuery { public func optionalRemove() { // Only remove if query has no observers and is eligible for GC guard observers.isEmpty, state.fetchStatus == .idle, isEligibleForGC else { - #if DEBUG - print("🗑️ SwiftUI Query: GC cancelled for \(queryHash) - Query is active or not eligible") - #endif + QueryLogger.shared.logGCCancelled(hash: queryHash) return } - #if DEBUG - print("🗑️ SwiftUI Query: Executing GC for \(queryHash)") - #endif + QueryLogger.shared.logGCExecuting(hash: queryHash) // Remove from cache (let cache handle the cleanup) cache?.remove(self) diff --git a/Sources/SwiftUIQuery/QueryLogger.swift b/Sources/SwiftUIQuery/QueryLogger.swift index f35849f..413e146 100644 --- a/Sources/SwiftUIQuery/QueryLogger.swift +++ b/Sources/SwiftUIQuery/QueryLogger.swift @@ -28,6 +28,9 @@ public final class QueryLogger { /// Enable/disable QueryObserver cache interaction logging public var logQueryObserver = true + /// Enable/disable GarbageCollector logging + public var logGarbageCollector = true + private init() {} // MARK: - Logging Methods @@ -115,6 +118,38 @@ public final class QueryLogger { guard isEnabled, logQueryObserver else { return } print("📊 QueryObserver reading query state for hash: \(hash)") } + + // MARK: - Garbage Collector Logging + + /// Log GarbageCollector starting + func logGCStart(interval: TimeInterval) { + guard isEnabled, logGarbageCollector else { return } + print("🗑️ SwiftUI Query: Starting GarbageCollector with \(interval)s interval") + } + + /// Log GarbageCollector stopping + func logGCStop() { + guard isEnabled, logGarbageCollector else { return } + print("🗑️ SwiftUI Query: Stopping GarbageCollector") + } + + /// Log GC cancelled for active query + func logGCCancelled(hash: String) { + guard isEnabled, logGarbageCollector else { return } + print("🗑️ SwiftUI Query: GC cancelled for \(hash) - Query is active or not eligible") + } + + /// Log GC executing for query + func logGCExecuting(hash: String) { + guard isEnabled, logGarbageCollector else { return } + print("🗑️ SwiftUI Query: Executing GC for \(hash)") + } + + /// Log GC removing query + func logGCRemoving(hash: String, reason: String) { + guard isEnabled, logGarbageCollector else { return } + print("🗑️ SwiftUI Query: GC removing \(hash) - \(reason)") + } } // MARK: - Public API Extensions @@ -126,6 +161,7 @@ extension QueryLogger { logQueryClient = true logQuery = true logQueryObserver = true + logGarbageCollector = true } /// Disable all cache logging (convenience method) @@ -157,3 +193,5 @@ extension QueryLogger { logQueryObserver = true } } + +// swiftlint:enable no_print_statements diff --git a/Tests/SwiftUIQueryTests/KeyTupleTests.swift b/Tests/SwiftUIQueryTests/KeyTupleTests.swift index 885be04..bc84484 100644 --- a/Tests/SwiftUIQueryTests/KeyTupleTests.swift +++ b/Tests/SwiftUIQueryTests/KeyTupleTests.swift @@ -11,7 +11,7 @@ struct KeyTupleTests { let options = QueryOptions>( queryKey: key, queryFn: { (key: KeyTuple2) async throws -> String in - return "User \(key.key2)" + "User \(key.key2)" } ) @@ -27,7 +27,7 @@ struct KeyTupleTests { let options = QueryOptions>( queryKey: key, queryFn: { (key: KeyTuple3) async throws -> String in - return "User \(key.key2) active: \(key.key3)" + "User \(key.key2) active: \(key.key3)" } ) @@ -44,7 +44,7 @@ struct KeyTupleTests { let options = QueryOptions>( queryKey: key, queryFn: { (key: KeyTuple4) async throws -> String in - return "User \(key.key2) active: \(key.key3) role: \(key.key4)" + "User \(key.key2) active: \(key.key3) role: \(key.key4)" } ) @@ -70,7 +70,7 @@ struct KeyTupleTests { let options = QueryOptions>( queryKey: key, queryFn: { (key: KeyTuple2) async throws -> User in - return User(id: key.key2, name: "Test User") + User(id: key.key2, name: "Test User") } ) @@ -93,7 +93,7 @@ struct KeyTupleTests { let options = QueryOptions>( queryKey: key, queryFn: { (key: KeyTuple3) async throws -> Post in - return Post(id: key.key2, title: "Post \(key.key3)") + Post(id: key.key2, title: "Post \(key.key3)") } ) @@ -117,7 +117,7 @@ struct KeyTupleTests { let options = QueryOptions>( queryKey: key, queryFn: { (key: KeyTuple4) async throws -> Comment in - return Comment(id: key.key2, text: "Comment from \(key.key3)") + Comment(id: key.key2, text: "Comment from \(key.key3)") } ) @@ -187,7 +187,7 @@ struct KeyTupleTests { return [Post(id: page, title: "Post \(page)")] }, getNextPageParam: { pages, _ in - return pages.count < 3 ? pages.count : nil + pages.count < 3 ? pages.count : nil }, initialPageParam: 0 ) @@ -204,7 +204,7 @@ struct KeyTupleTests { let options = QueryOptions>( queryKey: key, queryFn: { (key: KeyTuple5) async throws -> String in - return "User \(key.key2) active: \(key.key3) role: \(key.key4) score: \(key.key5)" + "User \(key.key2) active: \(key.key3) role: \(key.key4) score: \(key.key5)" } ) @@ -223,7 +223,7 @@ struct KeyTupleTests { let options = QueryOptions>( queryKey: key, queryFn: { (key: KeyTuple6) async throws -> String in - return "Post \(key.key2) status: \(key.key3) active: \(key.key4) priority: \(key.key5) type: \(key.key6)" + "Post \(key.key2) status: \(key.key3) active: \(key.key4) priority: \(key.key5) type: \(key.key6)" } ) @@ -254,7 +254,7 @@ struct KeyTupleTests { let options = QueryOptions>( queryKey: key, queryFn: { (key: KeyTuple5) async throws -> Order in - return Order(id: key.key2, status: key.key3) + Order(id: key.key2, status: key.key3) } ) @@ -280,7 +280,7 @@ struct KeyTupleTests { let options = QueryOptions>( queryKey: key, queryFn: { (key: KeyTuple6) async throws -> Product in - return Product(id: key.key2, name: "Product \(key.key6)") + Product(id: key.key2, name: "Product \(key.key6)") } ) @@ -339,7 +339,7 @@ struct KeyTupleTests { return [Item(id: page, name: "\(key.key2) item \(page)")] }, getNextPageParam: { pages, _ in - return pages.count < 3 ? pages.count : nil + pages.count < 3 ? pages.count : nil }, initialPageParam: 0 ) @@ -380,7 +380,7 @@ struct KeyTupleTests { return [Review(id: page, rating: key.key4)] }, getNextPageParam: { pages, _ in - return pages.count < 2 ? pages.count : nil + pages.count < 2 ? pages.count : nil }, initialPageParam: 0 ) diff --git a/Tests/SwiftUIQueryTests/swiftui_queryTests.swift b/Tests/SwiftUIQueryTests/swiftui_queryTests.swift index 6ab6d0e..3ae7abc 100644 --- a/Tests/SwiftUIQueryTests/swiftui_queryTests.swift +++ b/Tests/SwiftUIQueryTests/swiftui_queryTests.swift @@ -1,26 +1,6 @@ import Testing @testable import SwiftUIQuery -@Suite("SwiftUI Query Main Test Suite") -@MainActor -struct SwiftUIQueryTests { - @Test("Library initialization") - func libraryInitialization() { - // Basic test to ensure the library compiles and can be imported - #expect(true) - } - - @Test("Swift 6 concurrency compliance") - func swift6ConcurrencyCompliance() { - // This test suite verifies that our library works with - // Swift 6's strict concurrency checking enabled - - // The fact that this compiles with -strict-concurrency=complete - // means we're compliant - #expect(true) - } -} - // MARK: - Example of parameterized tests @Suite("Parameterized Tests Example")