Eine aufbereitete Darstellung der Quelle

 
     
 
 
Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 

Benutzer

Quelle  CoreDataContactListProviderTests.swift   Sprache: JAVA

 

import Combine
import CoreData
import Foundation
import os
import Testing
@testable import Threema
@testable import ThreemaFramework

@Suite("CoreDataContactListProvider", .serialized)
@MainActor
final class CoreDataContactListProviderTests {

    // MARK: - Nested types

    /// Records the threads snapshots are delivered on, from any thread
    private final class DeliveryRecorder: Sendable {
        private let deliveredOnMainThread = OSAllocatedUnfairLock(initialState: [Bool]())

        var deliveries: [Bool] {
            deliveredOnMainThread.withLock { $0 }
        }

        func record() {
            let isMainThread = Thread.isMainThread
            deliveredOnMainThread.withLock { $0.append(isMainThread) }
        }

        func reset() {
            deliveredOnMainThread.withLock { $0.removeAll() }
        }
    }

    /// Counts `performFetch()` calls across all controllers sharing it
    private final class FetchCounter: Sendable {
        private let lockedCount = OSAllocatedUnfairLock(initialState: 0)

        var count: Int {
            lockedCount.withLock { $0 }
        }

        func increment() {
            lockedCount.withLock { $0 += 1 }
        }
    }

    private final class CountingFetchedResultsController: NSFetchedResultsController<NSFetchRequestResult> {
        var fetchCounter: FetchCounter?

        override func performFetch() throws {
            fetchCounter?.increment()
            try super.performFetch()
        }
    }

    // MARK: - Private properties

    private let testDatabase: TestDatabase
    private let userSettings: UserSettings

    // MARK: - Lifecycle

    init() {
        AppGroup.setGroupID("group.ch.threema")
        testDatabase = TestDatabase()
        userSettings = UserSettings.shared()
    }

    // MARK: - Tests

    @Test("A setting written on a background thread does not wait for the main thread")
    func backgroundSettingChangeDoesNotWaitForMainThread() async throws {
        let initialHideStaleContacts = userSettings.hideStaleContacts
        defer {
            userSettings.hideStaleContacts = initialHideStaleContacts
        }

        let provider = makeProvider()
        await drainMainQueue()

        let recorder = DeliveryRecorder()
        let subscription = provider.currentSnapshot
            .dropFirst()
            .sink { _ in
                recorder.record()
                // Stand-in for `UITableViewDiffableDataSource.apply(_:)`, which waits for main when called off main
                if !Thread.isMainThread {
                    DispatchQueue.main.sync { }
                }
            }
        defer {
            subscription.cancel()
        }

        let setterReturned = DispatchSemaphore(value: 0)
        let userSettings = userSettings
        let newValue = !initialHideStaleContacts
        DispatchQueue.global(qos: .userInitiated).async {
            userSettings.hideStaleContacts = newValue
            setterReturned.signal()
        }

        // Keep main busy, as in the crash where main was writing the same setting. A setter that waits for main
        // times out here instead of deadlocking the test.
        #expect(
            setterReturned.wait(timeout: .now() + 2) == .success,
            "The setter on the background thread waited for the main thread"
        )

        try await waitForDelivery(of: recorder)
        // swiftformat:disable:next preferKeyPath
        #expect(recorder.deliveries.allSatisfy { $0 }, "Snapshots must only be delivered on the main thread")
    }

    @Test("Each observed setting publishes a new snapshot on the main thread")
    func observedSettingsPublishOnMainThread() async throws {
        let initialHideStaleContacts = userSettings.hideStaleContacts
        let initialSortOrderFirstName = userSettings.sortOrderFirstName
        defer {
            userSettings.hideStaleContacts = initialHideStaleContacts
            userSettings.sortOrderFirstName = initialSortOrderFirstName
        }

        let provider = makeProvider()
        await drainMainQueue()

        let recorder = DeliveryRecorder()
        let subscription = provider.currentSnapshot
            .dropFirst()
            .sink { _ in
                recorder.record()
            }
        defer {
            subscription.cancel()
        }

        let userSettings = userSettings

        await runOnBackgroundThread {
            userSettings.hideStaleContacts = !initialHideStaleContacts
        }
        try await waitForDelivery(of: recorder, "hideStaleContacts")
        // swiftformat:disable:next preferKeyPath
        #expect(recorder.deliveries.allSatisfy { $0 }, "hideStaleContacts: delivered off the main thread")

        recorder.reset()

        await runOnBackgroundThread {
            userSettings.sortOrderFirstName = !initialSortOrderFirstName
        }
        try await waitForDelivery(of: recorder, "sortOrderFirstName")
        // swiftformat:disable:next preferKeyPath
        #expect(recorder.deliveries.allSatisfy { $0 }, "sortOrderFirstName: delivered off the main thread")
    }

    @Test("Init fetches once, block list changes do not fetch again")
    func initFetchesOnce() async throws {
        let fetchCounter = FetchCounter()
        let provider = makeProvider(fetchCounter: fetchCounter)
        await drainMainQueue()

        #expect(fetchCounter.count == 1)

        // Blocking goes through `UserSettings` and its stores, which the provider no longer observes
        let initialBlacklist = userSettings.blacklist
        defer {
            userSettings.blacklist = initialBlacklist
        }
        userSettings.blacklist = NSOrderedSet(array: ["ECHOECHO", "AAAAAAAA", "BBBBBBBB"])
        await drainMainQueue()

        #expect(fetchCounter.count == 1)

        withExtendedLifetime(provider) { }
    }

    // MARK: - Private helpers

    private func makeProvider(
        fetchCounter: FetchCounter
    ) -> CoreDataContactListProvider<ContactEntity, Contact> {
        let entityFetcher = testDatabase.entityManager.entityFetcher
        let userSettings = userSettings

        return CoreDataContactListProvider(
            entityFetcher: entityFetcher,
            userSettings: userSettings,
            fetchedResultsController: {
                let template = entityFetcher.contactsResultController(
                    sortOrderFirstName: userSettings.sortOrderFirstName,
                    hideStaleContacts: userSettings.hideStaleContacts
                )
                let controller = CountingFetchedResultsController(
                    fetchRequest: template.fetchRequest,
                    managedObjectContext: template.managedObjectContext,
                    sectionNameKeyPath: template.sectionNameKeyPath,
                    cacheName: nil
                )
                controller.fetchCounter = fetchCounter
                return controller
            },
            entityResolver: { Contact(contactEntity: $0) }
        )
    }

    private func makeProvider() -> CoreDataContactListProvider<ContactEntity, Contact> {
        let entityFetcher = testDatabase.entityManager.entityFetcher
        let userSettings = userSettings

        return CoreDataContactListProvider(
            entityFetcher: entityFetcher,
            userSettings: userSettings,
            fetchedResultsController: {
                entityFetcher.contactsResultController(
                    sortOrderFirstName: userSettings.sortOrderFirstName,
                    hideStaleContacts: userSettings.hideStaleContacts
                )
            },
            entityResolver: { Contact(contactEntity: $0) }
        )
    }

    private func runOnBackgroundThread(_ work: @escaping @Sendable () -> Void) async {
        await withCheckedContinuation { continuation in
            DispatchQueue.global(qos: .userInitiated).async {
                work()
                continuation.resume()
            }
        }
    }

    /// Returns after everything enqueued on the main queue so far ran
    private func drainMainQueue() async {
        await withCheckedContinuation { continuation in
            DispatchQueue.main.async {
                continuation.resume()
            }
        }
    }

    /// Suspends (letting the main queue run) until a delivery was recorded or the timeout elapsed, then collects
    /// the deliveries still enqueued on the main queue
    private func waitForDelivery(
        of recorder: DeliveryRecorder,
        _ comment: Comment? = nil,
        timeout: Duration = .seconds(5)
    ) async throws {
        let deadline = ContinuousClock.now + timeout
        while recorder.deliveries.isEmpty, ContinuousClock.now < deadline {
            try await Task.sleep(for: .milliseconds(10))
        }
        await drainMainQueue()
        try #require(!recorder.deliveries.isEmpty, comment ?? "Expected a snapshot delivery")
    }
}

Messung
CHZG
1009610098

¤ Dauer der Verarbeitung: 0.6 Sekunden  ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

PVS Prover

Isabelle Prover


Cephes Mathematical Library

Vienna Development Method

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.






                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Open Source Software

     Quellcodebibliothek
     Eigene Quellcodes
     Fremde Quellcodes
     Suchen

Jenseits des Üblichen ....
    

Besucherstatistik

Besucherstatistik

Statistik
#Sources=1104842
#Domains=499078