./codecov.yml .swift-version Cartfile Cartfile.resolved Dangerfile Gemfile Gemfile.lock LICENSE Package.resolved Package.swift Sources/SwiftQueue/Constraint+Charging.swift Sources/SwiftQueue/Constraint+Deadline.swift Sources/SwiftQueue/Constraint+Delay.swift Sources/SwiftQueue/Constraint+Network.swift Sources/SwiftQueue/Constraint+Persister.swift Sources/SwiftQueue/Constraint+Repeat.swift Sources/SwiftQueue/Constraint+Retry.swift Sources/SwiftQueue/Constraint+Tag.swift Sources/SwiftQueue/Constraint+Timeout.swift Sources/SwiftQueue/Constraint+UniqueUUID.swift Sources/SwiftQueue/Constraint.swift Sources/SwiftQueue/ConstraintMaker.swift Sources/SwiftQueue/JobBuilder.swift Sources/SwiftQueue/JobInfo.swift Sources/SwiftQueue/JobInfoSerializer+Decodable.swift Sources/SwiftQueue/SqOperation.swift Sources/SwiftQueue/SqOperationQueue.swift Sources/SwiftQueue/SwiftQueue.swift Sources/SwiftQueue/SwiftQueueLogger.swift Sources/SwiftQueue/SwiftQueueManager.swift Sources/SwiftQueue/UserDefaultsPersister.swift Sources/SwiftQueue/Utils.swift Sources/ios/SwiftQueueManager+BackgroundTask.swift SwiftQueue.podspec SwiftQueue.xcodeproj/SwiftQueueTests_Info.plist SwiftQueue.xcodeproj/SwiftQueue_Info.plist SwiftQueue.xcodeproj/project.pbxproj SwiftQueue.xcodeproj/project.xcworkspace/contents.xcworkspacedata SwiftQueue.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist SwiftQueue.xcodeproj/xcshareddata/xcschemes/SwiftQueue iOS.xcscheme SwiftQueue.xcodeproj/xcshareddata/xcschemes/SwiftQueue macOS.xcscheme SwiftQueue.xcodeproj/xcshareddata/xcschemes/SwiftQueue tvOS.xcscheme SwiftQueue.xcodeproj/xcshareddata/xcschemes/SwiftQueue watchOS.xcscheme SwiftQueue.xcodeproj/xcshareddata/xcschemes/xcschememanagement.plist Tests/LinuxMain.swift Tests/SwiftQueueTests/BackgroundTasksTest.swift Tests/SwiftQueueTests/BasicConstraintTest.swift Tests/SwiftQueueTests/ConstraintTest+Charging.swift Tests/SwiftQueueTests/ConstraintTest+Custom.swift Tests/SwiftQueueTests/ConstraintTest+Deadline.swift Tests/SwiftQueueTests/ConstraintTest+Delay.swift Tests/SwiftQueueTests/ConstraintTest+Network.swift Tests/SwiftQueueTests/ConstraintTest+Repeat.swift Tests/SwiftQueueTests/ConstraintTest+Retry.swift Tests/SwiftQueueTests/ConstraintTest+Tag.swift Tests/SwiftQueueTests/ConstraintTest+Timeout.swift Tests/SwiftQueueTests/ConstraintTest+UniqueUUID.swift Tests/SwiftQueueTests/LoggerTests.swift Tests/SwiftQueueTests/PersisterTests.swift Tests/SwiftQueueTests/SqOperationTest.swift Tests/SwiftQueueTests/StartStopTests.swift Tests/SwiftQueueTests/SwiftQueueBuilderTests.swift Tests/SwiftQueueTests/SwiftQueueManagerTests.swift Tests/SwiftQueueTests/TestUtils.swift Tests/SwiftQueueTests/XCTestManifests.swift <<<<<< network # path=./SwiftQueue.framework.coverage.txt /Users/runner/work/SwiftQueue/SwiftQueue/Sources/SwiftQueue/Constraint+Charging.swift: 1| |// The MIT License (MIT) 2| |// 3| |// Copyright (c) 2019 Lucas Nelaupe 4| |// 5| |// Permission is hereby granted, free of charge, to any person obtaining a copy 6| |// of this software and associated documentation files (the "Software"), to deal 7| |// in the Software without restriction, including without limitation the rights 8| |// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9| |// copies of the Software, and to permit persons to whom the Software is 10| |// furnished to do so, subject to the following conditions: 11| |// 12| |// The above copyright notice and this permission notice shall be included in all 13| |// copies or substantial portions of the Software. 14| |// 15| |// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16| |// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17| |// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18| |// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19| |// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20| |// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21| |// SOFTWARE. 22| | 23| |import Foundation 24| |#if os(iOS) 25| |import UIKit 26| |#endif 27| | 28| |#if os(iOS) 29| |internal final class BatteryChargingConstraint: SimpleConstraint, CodableConstraint { 30| | 31| | // To avoid cyclic ref 32| | private weak var actual: SqOperation? 33| | 34| | convenience init?(from decoder: Decoder) throws { 35| | let container = try decoder.container(keyedBy: ChargingConstraintKey.self) 36| | if container.contains(.charging) { 37| | self.init() 38| | } else { return nil } 39| | } 40| | 41| | func batteryStateDidChange(notification: NSNotification) { 42| | if let job = actual, UIDevice.current.batteryState == .charging { 43| | // Avoid job to run multiple times 44| | actual = nil 45| | job.run() 46| | } 47| | } 48| | 49| | override func willSchedule(queue: SqOperationQueue, operation: SqOperation) throws { 50| | /// Start listening 51| | NotificationCenter.default.addObserver( 52| | self, 53| | selector: Selector(("batteryStateDidChange:")), 54| | name: UIDevice.batteryStateDidChangeNotification, 55| | object: nil 56| | ) 57| | } 58| | 59| | override func run(operation: SqOperation) -> Bool { 60| | guard UIDevice.current.batteryState != .charging else { 61| | return true 62| | } 63| | 64| | operation.logger.log(.verbose, jobId: operation.name, message: "Unsatisfied charging requirement") 65| | 66| | /// Keep actual job 67| | actual = operation 68| | return false 69| | } 70| | 71| | deinit { 72| | NotificationCenter.default.removeObserver(self) 73| | } 74| | 75| | private enum ChargingConstraintKey: String, CodingKey { 76| | case charging 77| | } 78| | 79| | func encode(to encoder: Encoder) throws { 80| | var container = encoder.container(keyedBy: ChargingConstraintKey.self) 81| | try container.encode(true, forKey: .charging) 82| | } 83| | 84| | func unregister() { 85| | NotificationCenter.default.removeObserver(self) 86| | } 87| | 88| |} 89| | 90| |#else 91| | 92| |internal final class BatteryChargingConstraint: SimpleConstraint { 93| | 94| 1| func unregister() {} 95| | 96| |} 97| | 98| |#endif /Users/runner/work/SwiftQueue/SwiftQueue/Sources/SwiftQueue/Constraint+Deadline.swift: 1| |// The MIT License (MIT) 2| |// 3| |// Copyright (c) 2019 Lucas Nelaupe 4| |// 5| |// Permission is hereby granted, free of charge, to any person obtaining a copy 6| |// of this software and associated documentation files (the "Software"), to deal 7| |// in the Software without restriction, including without limitation the rights 8| |// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9| |// copies of the Software, and to permit persons to whom the Software is 10| |// furnished to do so, subject to the following conditions: 11| |// 12| |// The above copyright notice and this permission notice shall be included in all 13| |// copies or substantial portions of the Software. 14| |// 15| |// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16| |// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17| |// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18| |// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19| |// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20| |// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21| |// SOFTWARE. 22| | 23| |import Foundation 24| | 25| |internal final class DeadlineConstraint: JobConstraint, CodableConstraint { 26| | 27| | /// Cancel the job after a certain date 28| | internal let deadline: Date 29| | 30| 6| required init(deadline: Date) { 31| 6| self.deadline = deadline 32| 6| } 33| | 34| 4| convenience init?(from decoder: Decoder) throws { 35| 4| let container = try decoder.container(keyedBy: DeadlineConstraintKey.self) 36| 4| if container.contains(.deadline) { 37| 1| try self.init(deadline: container.decode(Date.self, forKey: .deadline)) 38| 4| } else { return nil } 39| 1| } 40| | 41| 4| func willSchedule(queue: SqOperationQueue, operation: SqOperation) throws { 42| 4| try check() 43| 4| } 44| | 45| 0| func willRun(operation: SqOperation) throws { 46| 0| try check() 47| 0| } 48| | 49| 0| func run(operation: SqOperation) -> Bool { 50| 0| operation.dispatchQueue.runAfter(deadline.timeIntervalSinceNow, callback: { [weak operation] in 51| 0| if operation?.isFinished != false { 52| 0| operation?.cancel(with: SwiftQueueError.deadline) 53| 0| } 54| 0| }) 55| 0| return true 56| 0| } 57| | 58| 4| private func check() throws { 59| 4| if deadline < Date() { 60| 4| throw SwiftQueueError.deadline 61| 4| } 62| 0| } 63| | 64| | private enum DeadlineConstraintKey: String, CodingKey { 65| | case deadline 66| | } 67| | 68| 1| func encode(to encoder: Encoder) throws { 69| 1| var container = encoder.container(keyedBy: DeadlineConstraintKey.self) 70| 1| try container.encode(deadline, forKey: .deadline) 71| 1| } 72| | 73| |} /Users/runner/work/SwiftQueue/SwiftQueue/Sources/SwiftQueue/Constraint+Delay.swift: 1| |// The MIT License (MIT) 2| |// 3| |// Copyright (c) 2019 Lucas Nelaupe 4| |// 5| |// Permission is hereby granted, free of charge, to any person obtaining a copy 6| |// of this software and associated documentation files (the "Software"), to deal 7| |// in the Software without restriction, including without limitation the rights 8| |// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9| |// copies of the Software, and to permit persons to whom the Software is 10| |// furnished to do so, subject to the following conditions: 11| |// 12| |// The above copyright notice and this permission notice shall be included in all 13| |// copies or substantial portions of the Software. 14| |// 15| |// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16| |// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17| |// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18| |// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19| |// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20| |// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21| |// SOFTWARE. 22| | 23| |import Foundation 24| | 25| |internal final class DelayConstraint: SimpleConstraint, CodableConstraint { 26| | 27| | /// Delay for the first execution of the job 28| | internal let delay: TimeInterval 29| | 30| 12| required init(delay: TimeInterval) { 31| 12| self.delay = delay 32| 12| } 33| | 34| 4| convenience init?(from decoder: Decoder) throws { 35| 4| let container = try decoder.container(keyedBy: DelayConstraintKey.self) 36| 4| if container.contains(.delay) { 37| 0| try self.init(delay: container.decode(TimeInterval.self, forKey: .delay)) 38| 4| } else { return nil } 39| 0| } 40| | 41| 4| override func run(operation: SqOperation) -> Bool { 42| 4| let epoch = Date().timeIntervalSince(operation.info.createTime) 43| 4| guard epoch < delay else { 44| 2| // Epoch already greater than delay 45| 2| return true 46| 2| } 47| 2| 48| 2| let time: Double = abs(epoch - delay) 49| 2| 50| 2| operation.nextRunSchedule = Date().addingTimeInterval(time) 51| 2| operation.dispatchQueue.runAfter(time, callback: { [weak operation] in 52| 2| // If the operation in already deInit, it may have been canceled 53| 2| // It's safe to ignore the nil check 54| 2| // This is mostly to prevent job retention when cancelling operation with delay 55| 2| operation?.run() 56| 2| }) 57| 2| 58| 2| operation.logger.log(.verbose, jobId: operation.name, message: "Job delayed by \(time)s") 59| 2| return false 60| 4| } 61| | 62| | private enum DelayConstraintKey: String, CodingKey { 63| | case delay 64| | } 65| | 66| 3| func encode(to encoder: Encoder) throws { 67| 3| var container = encoder.container(keyedBy: DelayConstraintKey.self) 68| 3| try container.encode(delay, forKey: .delay) 69| 3| } 70| | 71| |} /Users/runner/work/SwiftQueue/SwiftQueue/Sources/SwiftQueue/Constraint+Network.swift: 1| |// The MIT License (MIT) 2| |// 3| |// Copyright (c) 2019 Lucas Nelaupe 4| |// 5| |// Permission is hereby granted, free of charge, to any person obtaining a copy 6| |// of this software and associated documentation files (the "Software"), to deal 7| |// in the Software without restriction, including without limitation the rights 8| |// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9| |// copies of the Software, and to permit persons to whom the Software is 10| |// furnished to do so, subject to the following conditions: 11| |// 12| |// The above copyright notice and this permission notice shall be included in all 13| |// copies or substantial portions of the Software. 14| |// 15| |// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16| |// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17| |// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18| |// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19| |// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20| |// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21| |// SOFTWARE. 22| | 23| |import Foundation 24| |#if os(iOS) || os(macOS) || os(tvOS) 25| |import Reachability 26| |#endif 27| | 28| |/// Kind of connectivity required for the job to run 29| |public enum NetworkType: Int, Codable { 30| | /// Job will run regardless the connectivity of the platform 31| | case any = 0 32| | /// Requires at least cellular such as 2G, 3G, 4G, LTE or Wifi 33| | case cellular = 1 34| | /// Device has to be connected to Wifi or Lan 35| | case wifi = 2 36| |} 37| | 38| |#if os(iOS) || os(macOS) || os(tvOS) 39| |internal final class NetworkConstraint: SimpleConstraint, CodableConstraint { 40| | 41| | /// Require a certain connectivity type 42| | internal let networkType: NetworkType 43| | 44| | private var reachability: Reachability? 45| | 46| 5| required init(networkType: NetworkType) { 47| 5| assert(networkType != .any) 48| 5| self.networkType = networkType 49| 5| } 50| | 51| 4| convenience init?(from decoder: Decoder) throws { 52| 4| let container = try decoder.container(keyedBy: NetworkConstraintKey.self) 53| 4| if container.contains(.requireNetwork) { 54| 0| try self.init(networkType: container.decode(NetworkType.self, forKey: .requireNetwork)) 55| 4| } else { return nil } 56| 0| } 57| | 58| 2| override func willSchedule(queue: SqOperationQueue, operation: SqOperation) throws { 59| 2| assert(operation.dispatchQueue != .main) 60| 2| self.reachability = try Reachability(targetQueue: operation.dispatchQueue, notificationQueue: operation.dispatchQueue) 61| 2| } 62| | 63| 2| override func run(operation: SqOperation) -> Bool { 64| 2| guard let reachability = reachability else { return true } 65| 2| 66| 2| if hasCorrectNetwork(reachability: reachability) { 67| 2| return true 68| 2| } 69| 0| 70| 0| operation.logger.log(.verbose, jobId: operation.name, message: "Unsatisfied network requirement") 71| 0| 72| 0| reachability.whenReachable = { reachability in 73| 0| reachability.stopNotifier() 74| 0| reachability.whenReachable = nil 75| 0| operation.run() 76| 0| } 77| 0| 78| 0| do { 79| 0| try reachability.startNotifier() 80| 0| return false 81| 0| } catch { 82| 0| operation.logger.log(.verbose, jobId: operation.name, message: "Unable to start network listener. Job will run.") 83| 0| operation.logger.log(.error, jobId: operation.name, message: error.localizedDescription) 84| 0| return true 85| 0| } 86| 0| } 87| | 88| 2| private func hasCorrectNetwork(reachability: Reachability) -> Bool { 89| 2| switch networkType { 90| 2| case .any: 91| 0| return true 92| 2| case .cellular: 93| 1| return reachability.connection != .unavailable 94| 2| case .wifi: 95| 1| return reachability.connection == .wifi 96| 2| } 97| 2| } 98| | 99| | private enum NetworkConstraintKey: String, CodingKey { 100| | case requireNetwork 101| | } 102| | 103| 0| func encode(to encoder: Encoder) throws { 104| 0| var container = encoder.container(keyedBy: NetworkConstraintKey.self) 105| 0| try container.encode(networkType, forKey: .requireNetwork) 106| 0| } 107| | 108| |} 109| |#else 110| | 111| |internal final class NetworkConstraint: SimpleConstraint, CodableConstraint { 112| | 113| | init(networkType: NetworkType) {} 114| | 115| | convenience init?(from decoder: Decoder) throws { nil } 116| | 117| |} 118| | 119| |#endif /Users/runner/work/SwiftQueue/SwiftQueue/Sources/SwiftQueue/Constraint+Persister.swift: 1| |// The MIT License (MIT) 2| |// 3| |// Copyright (c) 2019 Lucas Nelaupe 4| |// 5| |// Permission is hereby granted, free of charge, to any person obtaining a copy 6| |// of this software and associated documentation files (the "Software"), to deal 7| |// in the Software without restriction, including without limitation the rights 8| |// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9| |// copies of the Software, and to permit persons to whom the Software is 10| |// furnished to do so, subject to the following conditions: 11| |// 12| |// The above copyright notice and this permission notice shall be included in all 13| |// copies or substantial portions of the Software. 14| |// 15| |// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16| |// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17| |// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18| |// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19| |// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20| |// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21| |// SOFTWARE. 22| | 23| |import Foundation 24| | 25| |internal class PersisterConstraint: SimpleConstraint { 26| | 27| | private let serializer: JobInfoSerializer 28| | 29| | private let persister: JobPersister 30| | 31| 6| init(serializer: JobInfoSerializer, persister: JobPersister) { 32| 6| self.serializer = serializer 33| 6| self.persister = persister 34| 6| } 35| | 36| 7| override func willSchedule(queue: SqOperationQueue, operation: SqOperation) throws { 37| 7| let data = try serializer.serialize(info: operation.info) 38| 7| let name = operation.name ?? "" 39| 7| let queueName = queue.name ?? "" 40| 7| assertNotEmptyString(name) 41| 7| assertNotEmptyString(queueName) 42| 7| persister.put(queueName: queueName, taskId: name, data: data) 43| 7| } 44| | 45| 6| func remove(queueName: String, taskId: String) { 46| 6| persister.remove(queueName: queueName, taskId: taskId) 47| 6| } 48| | 49| |} /Users/runner/work/SwiftQueue/SwiftQueue/Sources/SwiftQueue/Constraint+Repeat.swift: 1| |// The MIT License (MIT) 2| |// 3| |// Copyright (c) 2019 Lucas Nelaupe 4| |// 5| |// Permission is hereby granted, free of charge, to any person obtaining a copy 6| |// of this software and associated documentation files (the "Software"), to deal 7| |// in the Software without restriction, including without limitation the rights 8| |// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9| |// copies of the Software, and to permit persons to whom the Software is 10| |// furnished to do so, subject to the following conditions: 11| |// 12| |// The above copyright notice and this permission notice shall be included in all 13| |// copies or substantial portions of the Software. 14| |// 15| |// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16| |// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17| |// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18| |// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19| |// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20| |// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21| |// SOFTWARE. 22| | 23| |import Foundation 24| | 25| |internal final class RepeatConstraint: SimpleConstraint, CodableConstraint { 26| | 27| | /// Number of run maximum 28| | internal let maxRun: Limit 29| | 30| | /// Time between each repetition of the job 31| | internal let interval: TimeInterval 32| | 33| | /// Executor to run job in foreground or background 34| | internal let executor: Executor 35| | 36| | /// Current number of run 37| | private var runCount: Double = 0 38| | 39| 8| required init(maxRun: Limit, interval: TimeInterval, executor: Executor) { 40| 8| self.maxRun = maxRun 41| 8| self.interval = interval 42| 8| self.executor = executor 43| 8| } 44| | 45| 4| convenience init?(from decoder: Decoder) throws { 46| 4| let container = try decoder.container(keyedBy: RepeatConstraintKey.self) 47| 4| if container.contains(.maxRun) && container.contains(.interval) && container.contains(.executor) { 48| 1| try self.init( 49| 1| maxRun: container.decode(Limit.self, forKey: .maxRun), 50| 1| interval: container.decode(TimeInterval.self, forKey: .interval), 51| 1| executor: container.decode(Executor.self, forKey: .executor) 52| 1| ) 53| 4| } else { return nil } 54| 1| } 55| | 56| 115| override func run(operation: SqOperation) -> Bool { 57| 115| switch executor { 58| 115| case .background: 59| 0| return false 60| 115| case .foreground: 61| 115| return true 62| 115| case.any: 63| 0| return true 64| 115| } 65| 115| } 66| | 67| 112| func completionSuccess(sqOperation: SqOperation) { 68| 112| if case .limited(let limit) = maxRun { 69| 13| // Reached run limit 70| 13| guard runCount + 1 < limit else { 71| 4| sqOperation.onTerminate() 72| 4| return 73| 9| } 74| 108| } 75| 108| 76| 108| guard interval > 0 else { 77| 106| // Run immediately 78| 106| runCount += 1 79| 106| sqOperation.run() 80| 106| return 81| 106| } 82| 2| 83| 2| // Schedule run after interval 84| 2| sqOperation.nextRunSchedule = Date().addingTimeInterval(interval) 85| 2| sqOperation.dispatchQueue.runAfter(interval, callback: { [weak self, weak sqOperation] in 86| 2| self?.runCount += 1 87| 2| sqOperation?.run() 88| 2| }) 89| 2| } 90| | 91| | private enum RepeatConstraintKey: String, CodingKey { 92| | case maxRun 93| | case interval 94| | case executor 95| | } 96| | 97| 1| func encode(to encoder: Encoder) throws { 98| 1| var container = encoder.container(keyedBy: RepeatConstraintKey.self) 99| 1| try container.encode(maxRun, forKey: .maxRun) 100| 1| try container.encode(interval, forKey: .interval) 101| 1| try container.encode(executor, forKey: .executor) 102| 1| } 103| | 104| |} 105| | 106| |/// Enum to specify background and foreground restriction 107| |public enum Executor: Int { 108| | 109| | /// Job will only run only when the app is in foreground 110| | case foreground = 0 111| | 112| | /// Job will only run only when the app is in background 113| | case background = 1 114| | 115| | /// Job can run in both background and foreground 116| | case any = 2 117| | 118| |} 119| | 120| |internal extension Executor { 121| | 122| 1| static func fromRawValue(value: Int) -> Executor { 123| 1| assert(value == 0 || value == 1 || value == 2) 124| 1| switch value { 125| 1| case 1: 126| 0| return Executor.background 127| 1| case 2: 128| 0| return Executor.any 129| 1| default: 130| 1| return Executor.foreground 131| 1| } 132| 1| } 133| | 134| |} 135| | 136| |extension Executor: Codable { 137| | 138| | private enum CodingKeys: String, CodingKey { case value } 139| | 140| 1| public init(from decoder: Decoder) throws { 141| 1| let values = try decoder.container(keyedBy: CodingKeys.self) 142| 1| let value = try values.decode(Int.self, forKey: .value) 143| 1| self = Executor.fromRawValue(value: value) 144| 1| } 145| | 146| 1| public func encode(to encoder: Encoder) throws { 147| 1| var container = encoder.container(keyedBy: CodingKeys.self) 148| 1| try container.encode(self.rawValue, forKey: .value) 149| 1| } 150| | 151| |} /Users/runner/work/SwiftQueue/SwiftQueue/Sources/SwiftQueue/Constraint+Retry.swift: 1| |// The MIT License (MIT) 2| |// 3| |// Copyright (c) 2019 Lucas Nelaupe 4| |// 5| |// Permission is hereby granted, free of charge, to any person obtaining a copy 6| |// of this software and associated documentation files (the "Software"), to deal 7| |// in the Software without restriction, including without limitation the rights 8| |// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9| |// copies of the Software, and to permit persons to whom the Software is 10| |// furnished to do so, subject to the following conditions: 11| |// 12| |// The above copyright notice and this permission notice shall be included in all 13| |// copies or substantial portions of the Software. 14| |// 15| |// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16| |// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17| |// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18| |// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19| |// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20| |// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21| |// SOFTWARE. 22| | 23| |import Foundation 24| | 25| |internal final class JobRetryConstraint: SimpleConstraint, CodableConstraint { 26| | 27| | /// Maximum number of authorised retried 28| | internal var limit: Limit 29| | 30| 12| required init(limit: Limit) { 31| 12| self.limit = limit 32| 12| } 33| | 34| 4| convenience init?(from decoder: Decoder) throws { 35| 4| let container = try decoder.container(keyedBy: RetryConstraintKey.self) 36| 4| if container.contains(.retryLimit) { 37| 0| try self.init(limit: container.decode(Limit.self, forKey: .retryLimit)) 38| 4| } else { return nil } 39| 0| } 40| | 41| 114| func onCompletionFail(sqOperation: SqOperation, error: Error) { 42| 114| switch limit { 43| 114| case .limited(let value): 44| 15| if value > 0 { 45| 10| sqOperation.retryJob(actual: self, retry: sqOperation.handler.onRetry(error: error), origin: error) 46| 15| } else { 47| 5| sqOperation.onTerminate() 48| 114| } 49| 114| case .unlimited: 50| 99| sqOperation.retryJob(actual: self, retry: sqOperation.handler.onRetry(error: error), origin: error) 51| 114| } 52| 114| } 53| | 54| | private enum RetryConstraintKey: String, CodingKey { 55| | case retryLimit 56| | } 57| | 58| 0| func encode(to encoder: Encoder) throws { 59| 0| var container = encoder.container(keyedBy: RetryConstraintKey.self) 60| 0| try container.encode(limit, forKey: .retryLimit) 61| 0| } 62| |} 63| | 64| |fileprivate extension SqOperation { 65| | 66| 109| func retryJob(actual: JobRetryConstraint, retry: RetryConstraint, origin: Error) { 67| 109| 68| 109| func exponentialBackoff(initial: TimeInterval) -> TimeInterval { 69| 5| currentRepetition += 1 70| 5| return currentRepetition == 1 ? initial : initial * pow(2, Double(currentRepetition - 1)) 71| 5| } 72| 109| 73| 109| func retryInBackgroundAfter(_ delay: TimeInterval) { 74| 7| nextRunSchedule = Date().addingTimeInterval(delay) 75| 7| dispatchQueue.runAfter(delay) { [weak actual, weak self] in 76| 7| actual?.limit.decreaseValue(by: 1) 77| 7| self?.run() 78| 7| } 79| 7| } 80| 109| 81| 109| switch retry { 82| 109| case .cancel: 83| 1| lastError = SwiftQueueError.onRetryCancel(origin) 84| 1| onTerminate() 85| 109| case .retry(let after): 86| 103| guard after > 0 else { 87| 101| // Retry immediately 88| 101| actual.limit.decreaseValue(by: 1) 89| 101| self.run() 90| 101| return 91| 101| } 92| 2| 93| 2| // Retry after time in parameter 94| 2| retryInBackgroundAfter(after) 95| 109| case .exponential(let initial): 96| 3| retryInBackgroundAfter(exponentialBackoff(initial: initial)) 97| 109| case .exponentialWithLimit(let initial, let maxDelay): 98| 2| retryInBackgroundAfter(min(maxDelay, exponentialBackoff(initial: initial))) 99| 109| } 100| 109| } 101| | 102| |} 103| | 104| |/// Behaviour for retrying the job 105| |public enum RetryConstraint { 106| | /// Retry after a certain time. If set to 0 it will retry immediately 107| | case retry(delay: TimeInterval) 108| | /// Will not retry, onRemoved will be called immediately 109| | case cancel 110| | /// Exponential back-off 111| | case exponential(initial: TimeInterval) 112| | /// Exponential back-off with max delay 113| | case exponentialWithLimit(initial: TimeInterval, maxDelay: TimeInterval) 114| |} /Users/runner/work/SwiftQueue/SwiftQueue/Sources/SwiftQueue/Constraint+Tag.swift: 1| |// The MIT License (MIT) 2| |// 3| |// Copyright (c) 2019 Lucas Nelaupe 4| |// 5| |// Permission is hereby granted, free of charge, to any person obtaining a copy 6| |// of this software and associated documentation files (the "Software"), to deal 7| |// in the Software without restriction, including without limitation the rights 8| |// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9| |// copies of the Software, and to permit persons to whom the Software is 10| |// furnished to do so, subject to the following conditions: 11| |// 12| |// The above copyright notice and this permission notice shall be included in all 13| |// copies or substantial portions of the Software. 14| |// 15| |// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16| |// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17| |// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18| |// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19| |// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20| |// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21| |// SOFTWARE. 22| | 23| |import Foundation 24| | 25| |internal final class TagConstraint: SimpleConstraint, CodableConstraint { 26| | 27| | internal var tags: Set 28| | 29| 5| required init(tags: Set) { 30| 5| self.tags = tags 31| 5| } 32| | 33| 4| convenience init?(from decoder: Decoder) throws { 34| 4| let container = try decoder.container(keyedBy: TagConstraintKey.self) 35| 4| if container.contains(.tags) { 36| 0| try self.init(tags: container.decode(Set.self, forKey: .tags)) 37| 4| } else { return nil } 38| 0| } 39| | 40| 1| func insert(tag: String) { 41| 1| tags.insert(tag) 42| 1| } 43| | 44| 3| func contains(tag: String) -> Bool { 45| 3| return tags.contains(tag) 46| 3| } 47| | 48| | private enum TagConstraintKey: String, CodingKey { 49| | case tags 50| | } 51| | 52| 1| func encode(to encoder: Encoder) throws { 53| 1| var container = encoder.container(keyedBy: TagConstraintKey.self) 54| 1| try container.encode(tags, forKey: .tags) 55| 1| } 56| |} /Users/runner/work/SwiftQueue/SwiftQueue/Sources/SwiftQueue/Constraint+Timeout.swift: 1| |// The MIT License (MIT) 2| |// 3| |// Copyright (c) 2019 Lucas Nelaupe 4| |// 5| |// Permission is hereby granted, free of charge, to any person obtaining a copy 6| |// of this software and associated documentation files (the "Software"), to deal 7| |// in the Software without restriction, including without limitation the rights 8| |// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9| |// copies of the Software, and to permit persons to whom the Software is 10| |// furnished to do so, subject to the following conditions: 11| |// 12| |// The above copyright notice and this permission notice shall be included in all 13| |// copies or substantial portions of the Software. 14| |// 15| |// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16| |// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17| |// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18| |// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19| |// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20| |// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21| |// SOFTWARE. 22| | 23| |import Foundation 24| | 25| |internal final class TimeoutConstraint: SimpleConstraint, CodableConstraint { 26| | 27| | /// Auto cancel job if not completed after this time 28| | internal let timeout: TimeInterval 29| | 30| 1| required init(timeout: TimeInterval) { 31| 1| self.timeout = timeout 32| 1| } 33| | 34| 4| convenience init?(from decoder: Decoder) throws { 35| 4| let container = try decoder.container(keyedBy: TimeoutConstraintKey.self) 36| 4| if container.contains(.timeout) { 37| 0| try self.init(timeout: container.decode(TimeInterval.self, forKey: .timeout)) 38| 4| } else { return nil } 39| 0| } 40| | 41| 1| override func run(operation: SqOperation) -> Bool { 42| 1| operation.dispatchQueue.runAfter(timeout) { 43| 1| if operation.isExecuting && !operation.isFinished { 44| 1| operation.cancel(with: SwiftQueueError.timeout) 45| 1| } 46| 1| } 47| 1| 48| 1| return true 49| 1| } 50| | 51| | private enum TimeoutConstraintKey: String, CodingKey { 52| | case timeout 53| | } 54| | 55| 0| func encode(to encoder: Encoder) throws { 56| 0| var container = encoder.container(keyedBy: TimeoutConstraintKey.self) 57| 0| try container.encode(timeout, forKey: .timeout) 58| 0| } 59| | 60| |} /Users/runner/work/SwiftQueue/SwiftQueue/Sources/SwiftQueue/Constraint+UniqueUUID.swift: 1| |// The MIT License (MIT) 2| |// 3| |// Copyright (c) 2019 Lucas Nelaupe 4| |// 5| |// Permission is hereby granted, free of charge, to any person obtaining a copy 6| |// of this software and associated documentation files (the "Software"), to deal 7| |// in the Software without restriction, including without limitation the rights 8| |// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9| |// copies of the Software, and to permit persons to whom the Software is 10| |// furnished to do so, subject to the following conditions: 11| |// 12| |// The above copyright notice and this permission notice shall be included in all 13| |// copies or substantial portions of the Software. 14| |// 15| |// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16| |// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17| |// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18| |// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19| |// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20| |// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21| |// SOFTWARE. 22| | 23| |import Foundation 24| | 25| |internal final class UniqueUUIDConstraint: SimpleConstraint, CodableConstraint { 26| | 27| | /// Unique identifier for a job 28| | internal let uuid: String 29| | 30| | /// Override job when scheduling a job with same uuid 31| | /// True = Override, False = Abort job with duplicate failure 32| | internal let override: Bool 33| | 34| | /// Including job that are executing when scheduling with same uuid 35| | private let includeExecutingJob: Bool 36| | 37| 23| required init(uuid: String, override: Bool, includeExecutingJob: Bool) { 38| 23| self.uuid = uuid 39| 23| self.override = override 40| 23| self.includeExecutingJob = includeExecutingJob 41| 23| } 42| | 43| 4| convenience init?(from decoder: Decoder) throws { 44| 4| let container = try decoder.container(keyedBy: UUIDConstraintKey.self) 45| 4| if container.contains(.uuid) && container.contains(.override) && container.contains(.includeExecutingJob) { 46| 3| try self.init( 47| 3| uuid: container.decode(String.self, forKey: .uuid), 48| 3| override: container.decode(Bool.self, forKey: .override), 49| 3| includeExecutingJob: container.decode(Bool.self, forKey: .includeExecutingJob) 50| 3| ) 51| 4| } else { return nil } 52| 3| } 53| | 54| 19| override func willSchedule(queue: SqOperationQueue, operation: SqOperation) throws { 55| 19| for ope in queue.operations where ope.name == uuid { 56| 16| if shouldAbort(ope: ope, operation: operation) { 57| 2| if override { 58| 1| ope.cancel() 59| 1| break 60| 1| } else { 61| 1| throw SwiftQueueError.duplicate 62| 1| } 63| 14| } 64| 18| } 65| 18| } 66| | 67| 2| private func shouldAbort(ope: Operation, operation: SqOperation) -> Bool { 68| 2| return (ope.isExecuting && includeExecutingJob) || !ope.isExecuting 69| 2| } 70| | 71| | private enum UUIDConstraintKey: String, CodingKey { 72| | case uuid 73| | case override 74| | case includeExecutingJob 75| | } 76| | 77| 8| func encode(to encoder: Encoder) throws { 78| 8| var container = encoder.container(keyedBy: UUIDConstraintKey.self) 79| 8| try container.encode(uuid, forKey: .uuid) 80| 8| try container.encode(override, forKey: .override) 81| 8| try container.encode(includeExecutingJob, forKey: .includeExecutingJob) 82| 8| } 83| | 84| |} /Users/runner/work/SwiftQueue/SwiftQueue/Sources/SwiftQueue/Constraint.swift: 1| |// The MIT License (MIT) 2| |// 3| |// Copyright (c) 2019 Lucas Nelaupe 4| |// 5| |// Permission is hereby granted, free of charge, to any person obtaining a copy 6| |// of this software and associated documentation files (the "Software"), to deal 7| |// in the Software without restriction, including without limitation the rights 8| |// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9| |// copies of the Software, and to permit persons to whom the Software is 10| |// furnished to do so, subject to the following conditions: 11| |// 12| |// The above copyright notice and this permission notice shall be included in all 13| |// copies or substantial portions of the Software. 14| |// 15| |// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16| |// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17| |// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18| |// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19| |// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20| |// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21| |// SOFTWARE. 22| | 23| |import Foundation 24| | 25| |public protocol JobConstraint { 26| | 27| | /** 28| | - Operation will be added to the queue 29| | Raise exception if the job cannot run 30| | */ 31| | func willSchedule(queue: SqOperationQueue, operation: SqOperation) throws 32| | 33| | /** 34| | - Operation will run 35| | Raise exception if the job cannot run anymore 36| | */ 37| | func willRun(operation: SqOperation) throws 38| | 39| | /** 40| | - Operation will run 41| | Return false if the job cannot run immediately 42| | */ 43| | func run(operation: SqOperation) -> Bool 44| | 45| |} 46| | 47| |public protocol CodableConstraint: Encodable { 48| | 49| | /** 50| | Build constraint when deserialize 51| | Return nil if the constraint does not apply 52| | */ 53| | init?(from decoder: Decoder) throws 54| | 55| |} 56| | 57| |public class SimpleConstraint: JobConstraint { 58| | 59| 31| public func willSchedule(queue: SqOperationQueue, operation: SqOperation) throws {} 60| | 61| 254| public func willRun(operation: SqOperation) throws {} 62| | 63| 131| public func run(operation: SqOperation) -> Bool { true } 64| | 65| |} /Users/runner/work/SwiftQueue/SwiftQueue/Sources/SwiftQueue/ConstraintMaker.swift: 1| |// The MIT License (MIT) 2| |// 3| |// Copyright (c) 2019 Lucas Nelaupe 4| |// 5| |// Permission is hereby granted, free of charge, to any person obtaining a copy 6| |// of this software and associated documentation files (the "Software"), to deal 7| |// in the Software without restriction, including without limitation the rights 8| |// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9| |// copies of the Software, and to permit persons to whom the Software is 10| |// furnished to do so, subject to the following conditions: 11| |// 12| |// The above copyright notice and this permission notice shall be included in all 13| |// copies or substantial portions of the Software. 14| |// 15| |// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16| |// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17| |// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18| |// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19| |// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20| |// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21| |// SOFTWARE. 22| | 23| |import Foundation 24| | 25| |public protocol ConstraintMaker { 26| | 27| | func make(from decoder: Decoder) throws -> [JobConstraint] 28| | 29| |} 30| | 31| |class DefaultConstraintMaker: ConstraintMaker { 32| | 33| 4| func make(from decoder: Decoder) throws -> [JobConstraint] { 34| 4| var constraints: [JobConstraint] = [] 35| 4| 36| 4| #if os(iOS) 37| 4| if let deadline = try BatteryChargingConstraint(from: decoder) { constraints.append(deadline) } 38| 4| #endif 39| 4| 40| 4| if let constraint = try DeadlineConstraint(from: decoder) { constraints.append(constraint) } 41| 4| if let constraint = try DelayConstraint(from: decoder) { constraints.append(constraint) } 42| 4| if let constraint = try TimeoutConstraint(from: decoder) { constraints.append(constraint) } 43| 4| if let constraint = try NetworkConstraint(from: decoder) { constraints.append(constraint) } 44| 4| if let constraint = try RepeatConstraint(from: decoder) { constraints.append(constraint) } 45| 4| if let constraint = try JobRetryConstraint(from: decoder) { constraints.append(constraint) } 46| 4| if let constraint = try UniqueUUIDConstraint(from: decoder) { constraints.append(constraint) } 47| 4| if let constraint = try TagConstraint(from: decoder) { constraints.append(constraint) } 48| 4| 49| 4| return constraints 50| 4| } 51| | 52| |} /Users/runner/work/SwiftQueue/SwiftQueue/Sources/SwiftQueue/JobBuilder.swift: 1| |// The MIT License (MIT) 2| |// 3| |// Copyright (c) 2019 Lucas Nelaupe 4| |// 5| |// Permission is hereby granted, free of charge, to any person obtaining a copy 6| |// of this software and associated documentation files (the "Software"), to deal 7| |// in the Software without restriction, including without limitation the rights 8| |// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9| |// copies of the Software, and to permit persons to whom the Software is 10| |// furnished to do so, subject to the following conditions: 11| |// 12| |// The above copyright notice and this permission notice shall be included in all 13| |// copies or substantial portions of the Software. 14| |// 15| |// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16| |// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17| |// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18| |// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19| |// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20| |// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21| |// SOFTWARE. 22| | 23| |import Foundation 24| | 25| |/// Builder to create your job with behaviour 26| |public final class JobBuilder { 27| | 28| | internal var info: JobInfo 29| | 30| | /// Type of your job that you will receive in JobCreator.create(type) 31| 164| public init(type: String) { 32| 164| assertNotEmptyString(type) 33| 164| self.info = JobInfo(type: type) 34| 164| } 35| | 36| | /// Create a copy of another job params 37| 1| public init(jobBuilder: JobBuilder) { 38| 1| self.info = jobBuilder.info 39| 1| } 40| | 41| | /// Allow only 1 job at the time with this ID scheduled or running if includeExecutingJob is true 42| | /// Same job scheduled with same id will result in onRemove(SwiftQueueError.duplicate) if override = false 43| | /// If override = true the previous job will be canceled and the new job will be scheduled 44| 19| public func singleInstance(forId: String, override: Bool = false, includeExecutingJob: Bool = true) -> Self { 45| 19| assertNotEmptyString(forId) 46| 19| info.constraints.append(UniqueUUIDConstraint(uuid: forId, override: override, includeExecutingJob: includeExecutingJob)) 47| 19| return self 48| 19| } 49| | 50| | /// Job in different groups can run in parallel 51| 114| public func parallel(queueName: String) -> Self { 52| 114| assertNotEmptyString(queueName) 53| 114| info.queueName = queueName 54| 114| return self 55| 114| } 56| | 57| | /// Delay the execution of the job. 58| | /// Base on the job creation, when the job is supposed to run, 59| | /// If the delay is already pass (longer job before) it will run immediately 60| | /// Otherwise it will wait for the remaining time 61| 12| public func delay(time: TimeInterval) -> Self { 62| 12| assert(time >= 0) 63| 12| info.constraints.append(DelayConstraint(delay: time)) 64| 12| return self 65| 12| } 66| | 67| | /// If the job hasn't run after the date, It will be removed 68| | /// will call onRemove(SwiftQueueError.deadline) 69| 5| public func deadline(date: Date) -> Self { 70| 5| info.constraints.append(DeadlineConstraint(deadline: date)) 71| 5| return self 72| 5| } 73| | 74| | /// Repeat job a certain number of time and with a interval between each run 75| | /// Limit of period to reproduce 76| | /// interval between each run. Does not affect the first iteration. Please add delay if so 77| | /// executor will make the job being scheduling to run in background with BackgroundTask API 78| 7| public func periodic(limit: Limit = .unlimited, interval: TimeInterval = 0) -> Self { 79| 7| assert(limit.validate) 80| 7| assert(interval >= 0) 81| 7| info.constraints.append(RepeatConstraint(maxRun: limit, interval: interval, executor: .foreground)) 82| 7| return self 83| 7| } 84| | 85| | @available(iOS 13.0, tvOS 13.0, macOS 10.15, *) 86| 0| public func periodic(limit: Limit = .unlimited, interval: TimeInterval = 0, executor: Executor = .foreground) -> Self { 87| 0| assert(limit.validate) 88| 0| assert(interval >= 0) 89| 0| info.constraints.append(RepeatConstraint(maxRun: limit, interval: interval, executor: executor)) 90| 0| return self 91| 0| } 92| | 93| | /// Connectivity constraint. 94| 5| public func internet(atLeast: NetworkType) -> Self { 95| 5| assert(atLeast != .any) 96| 5| info.constraints.append(NetworkConstraint(networkType: atLeast)) 97| 5| return self 98| 5| } 99| | 100| | private var requirePersist = false 101| | 102| | /// Job should be persisted. 103| 6| public func persist() -> Self { 104| 6| requirePersist = true 105| 6| return self 106| 6| } 107| | 108| | /// Limit number of retry. Overall for the lifecycle of the SwiftQueueManager. 109| | /// For a periodic job, the retry count will not be reset at each period. 110| 12| public func retry(limit: Limit) -> Self { 111| 12| assert(limit.validate) 112| 12| info.constraints.append(JobRetryConstraint(limit: limit)) 113| 12| return self 114| 12| } 115| | 116| | /// Custom tag to mark the job 117| 6| public func addTag(tag: String) -> Self { 118| 6| if let constraint: TagConstraint = getConstraint(info.constraints) { 119| 1| constraint.insert(tag: tag) 120| 1| return self 121| 5| } 122| 5| 123| 5| var set = Set() 124| 5| set.insert(tag) 125| 5| 126| 5| info.constraints.append(TagConstraint(tags: set)) 127| 5| return self 128| 6| } 129| | 130| | /// Custom parameters will be forwarded to create method 131| 1| public func with(params: [String: Any]) -> Self { 132| 1| info.params = params 133| 1| return self 134| 1| } 135| | 136| | /// Set priority of the job. May affect execution order 137| 1| public func priority(priority: Operation.QueuePriority) -> Self { 138| 1| info.priority = priority 139| 1| return self 140| 1| } 141| | 142| | /// Set quality of service to define importance of the job system wise 143| 1| public func service(quality: QualityOfService) -> Self { 144| 1| info.qualityOfService = quality 145| 1| return self 146| 1| } 147| | 148| | /// Call if job can only run when the device is charging 149| 2| public func requireCharging() -> Self { 150| 2| info.constraints.append(BatteryChargingConstraint()) 151| 2| return self 152| 2| } 153| | 154| | /// Maximum time in second that the job is allowed to run 155| 1| public func timeout(value: TimeInterval) -> Self { 156| 1| info.constraints.append(TimeoutConstraint(timeout: value)) 157| 1| return self 158| 1| } 159| | 160| | /// Add custom constraint. If your job is persisted, your constraint may extend CodableConstraint 161| | /// and should be registered with ConstraintMaker 162| 4| public func add(constraint: JobConstraint) -> Self { 163| 4| info.constraints.append(constraint) 164| 4| return self 165| 4| } 166| | 167| | /// Create copy of current job builder 168| 1| public func copy() -> JobBuilder { 169| 1| return JobBuilder(jobBuilder: self) 170| 1| } 171| | 172| | /// Get the JobInfo built 173| 6| public func build() -> JobInfo { 174| 6| return info 175| 6| } 176| | 177| | /// Add job to the JobQueue 178| 144| public func schedule(manager: SwiftQueueManager) { 179| 144| if requirePersist { 180| 6| let constraint: UniqueUUIDConstraint? = getConstraint(info) 181| 6| if constraint == nil { 182| 1| info.constraints.append(UniqueUUIDConstraint(uuid: UUID().uuidString, override: false, includeExecutingJob: false)) 183| 6| } 184| 6| assert(JSONSerialization.isValidJSONObject(info.params)) 185| 6| info.constraints.append(PersisterConstraint(serializer: manager.params.serializer, persister: manager.params.persister)) 186| 144| } 187| 144| 188| 144| manager.enqueue(info: info) 189| 144| } 190| | 191| |} /Users/runner/work/SwiftQueue/SwiftQueue/Sources/SwiftQueue/JobInfo.swift: 1| |// The MIT License (MIT) 2| |// 3| |// Copyright (c) 2019 Lucas Nelaupe 4| |// 5| |// Permission is hereby granted, free of charge, to any person obtaining a copy 6| |// of this software and associated documentation files (the "Software"), to deal 7| |// in the Software without restriction, including without limitation the rights 8| |// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9| |// copies of the Software, and to permit persons to whom the Software is 10| |// furnished to do so, subject to the following conditions: 11| |// 12| |// The above copyright notice and this permission notice shall be included in all 13| |// copies or substantial portions of the Software. 14| |// 15| |// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16| |// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17| |// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18| |// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19| |// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20| |// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21| |// SOFTWARE. 22| | 23| |import Foundation 24| | 25| |/// Info related to a single job. Those information may be serialized and persisted 26| |/// In order to re-create the job in the future. 27| |public struct JobInfo { 28| | 29| | /// Type of job to create actual `Job` instance 30| | public let type: String 31| | 32| | /// Queue name 33| | public var queueName: String 34| | 35| | /// Custom params set by the user 36| | public var params: [String: Any] 37| | 38| | /// This value is used to influence the order in which operations are dequeued and executed 39| | public var priority: Operation.QueuePriority 40| | 41| | /// The relative amount of importance for granting system resources to the operation. 42| | public var qualityOfService: QualityOfService 43| | 44| | /// Date of the job's creation 45| | public let createTime: Date 46| | 47| | internal var constraints: [JobConstraint] 48| | 49| 0| mutating func setupConstraints(_ maker: ConstraintMaker, from decoder: Decoder) throws { 50| 0| constraints = try maker.make(from: decoder) 51| 0| } 52| | 53| 174| init(type: String) { 54| 174| self.init( 55| 174| type: type, 56| 174| queueName: "GLOBAL", 57| 174| createTime: Date(), 58| 174| priority: .normal, 59| 174| qualityOfService: .utility, 60| 174| params: [:], 61| 174| constraints: [] 62| 174| ) 63| 174| } 64| | 65| | init(type: String, 66| | queueName: String, 67| | createTime: Date, 68| | priority: Operation.QueuePriority, 69| | qualityOfService: QualityOfService, 70| | params: [String: Any], 71| | constraints: [JobConstraint] 72| 178| ) { 73| 178| self.type = type 74| 178| self.queueName = queueName 75| 178| self.createTime = createTime 76| 178| self.priority = priority 77| 178| self.qualityOfService = qualityOfService 78| 178| self.params = params 79| 178| self.constraints = constraints 80| 178| } 81| |} 82| | 83| |extension JobInfo: Codable { 84| | 85| | internal enum JobInfoKeys: String, CodingKey { 86| | case type 87| | case queueName 88| | case params 89| | case priority 90| | case qualityOfService 91| | case createTime 92| | case constraints 93| | } 94| | 95| 4| public init(from decoder: Decoder) throws { 96| 4| let container = try decoder.container(keyedBy: JobInfoKeys.self) 97| 4| 98| 4| let type = try container.decode(String.self, forKey: .type) 99| 4| let queueName = try container.decode(String.self, forKey: .queueName) 100| 4| let params: [String: Any] = try container.decode([String: Any].self, forKey: .params) 101| 4| let priority: Int = try container.decode(Int.self, forKey: .priority) 102| 4| let qualityOfService: Int = try container.decode(Int.self, forKey: .qualityOfService) 103| 4| let createTime = try container.decode(Date.self, forKey: .createTime) 104| 4| let constraintMaker = decoder.userInfo[.constraintMaker] as? ConstraintMaker ?? DefaultConstraintMaker() 105| 4| 106| 4| try self.init( 107| 4| type: type, 108| 4| queueName: queueName, 109| 4| createTime: createTime, 110| 4| priority: Operation.QueuePriority(fromValue: priority), 111| 4| qualityOfService: QualityOfService(fromValue: qualityOfService), 112| 4| params: params, 113| 4| constraints: constraintMaker.make(from: decoder) 114| 4| ) 115| 4| } 116| | 117| 9| public func encode(to encoder: Encoder) throws { 118| 9| var container = encoder.container(keyedBy: JobInfoKeys.self) 119| 9| 120| 9| try container.encode(type, forKey: .type) 121| 9| try container.encode(queueName, forKey: .queueName) 122| 9| try container.encode(params, forKey: .params) 123| 9| try container.encode(priority.rawValue, forKey: .priority) 124| 9| try container.encode(qualityOfService.rawValue, forKey: .qualityOfService) 125| 9| try container.encode(createTime, forKey: .createTime) 126| 19| for case let constraint as CodableConstraint in constraints { 127| 19| try constraint.encode(to: encoder) 128| 19| } 129| 9| } 130| |} /Users/runner/work/SwiftQueue/SwiftQueue/Sources/SwiftQueue/JobInfoSerializer+Decodable.swift: 1| |// The MIT License (MIT) 2| |// 3| |// Copyright (c) 2019 Lucas Nelaupe 4| |// 5| |// Permission is hereby granted, free of charge, to any person obtaining a copy 6| |// of this software and associated documentation files (the "Software"), to deal 7| |// in the Software without restriction, including without limitation the rights 8| |// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9| |// copies of the Software, and to permit persons to whom the Software is 10| |// furnished to do so, subject to the following conditions: 11| |// 12| |// The above copyright notice and this permission notice shall be included in all 13| |// copies or substantial portions of the Software. 14| |// 15| |// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16| |// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17| |// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18| |// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19| |// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20| |// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21| |// SOFTWARE. 22| | 23| |import Foundation 24| | 25| |/// `JSONEncoder` and `JSONDecoder` to serialize JobInfo 26| |public class DecodableSerializer: JobInfoSerializer { 27| | 28| | private let encoder: JSONEncoder 29| | private let decoder: JSONDecoder 30| | 31| | /// Init decodable with custom `JSONEncoder` and `JSONDecoder` 32| 52| public init(maker: ConstraintMaker, encoder: JSONEncoder = JSONEncoder(), decoder: JSONDecoder = JSONDecoder()) { 33| 52| self.encoder = encoder 34| 52| self.decoder = decoder 35| 52| self.encoder.userInfo[.constraintMaker] = maker 36| 52| self.decoder.userInfo[.constraintMaker] = maker 37| 52| } 38| | 39| 9| public func serialize(info: JobInfo) throws -> String { 40| 9| try String.fromUTF8(data: encoder.encode(info)) 41| 9| } 42| | 43| 4| public func deserialize(json: String) throws -> JobInfo { 44| 4| try decoder.decode(JobInfo.self, from: json.utf8Data()) 45| 4| } 46| | 47| |} 48| | 49| |internal extension KeyedDecodingContainer { 50| | 51| 4| func decode(_ type: Data.Type, forKey key: KeyedDecodingContainer.Key) throws -> Data { 52| 4| try self.decode(String.self, forKey: key).utf8Data() 53| 4| } 54| | 55| 4| func decode(_ type: [String: Any].Type, forKey key: KeyedDecodingContainer.Key) throws -> [String: Any] { 56| 4| let data = try self.decode(Data.self, forKey: key) 57| 4| guard let dict = try JSONSerialization.jsonObject(with: data) as? [String: Any] else { 58| 0| throw DecodingError.dataCorrupted(DecodingError.Context( 59| 0| codingPath: [key], 60| 0| debugDescription: "Decoded value is not a dictionary") 61| 0| ) 62| 4| } 63| 4| return dict 64| 4| } 65| | 66| |} 67| | 68| |internal extension KeyedEncodingContainer { 69| | 70| 9| mutating func encode(_ value: [String: Any], forKey key: KeyedEncodingContainer.Key) throws { 71| 9| let data = try JSONSerialization.data(withJSONObject: value) 72| 9| try self.encode(String.fromUTF8(data: data, key: [key]), forKey: key) 73| 9| } 74| | 75| |} 76| | 77| |extension CodingUserInfoKey { 78| | internal static let constraintMaker: CodingUserInfoKey = CodingUserInfoKey(rawValue: "constraints")! 79| |} /Users/runner/work/SwiftQueue/SwiftQueue/Sources/SwiftQueue/SqOperation.swift: 1| |// The MIT License (MIT) 2| |// 3| |// Copyright (c) 2019 Lucas Nelaupe 4| |// 5| |// Permission is hereby granted, free of charge, to any person obtaining a copy 6| |// of this software and associated documentation files (the "Software"), to deal 7| |// in the Software without restriction, including without limitation the rights 8| |// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9| |// copies of the Software, and to permit persons to whom the Software is 10| |// furnished to do so, subject to the following conditions: 11| |// 12| |// The above copyright notice and this permission notice shall be included in all 13| |// copies or substantial portions of the Software. 14| |// 15| |// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16| |// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17| |// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18| |// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19| |// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20| |// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21| |// SOFTWARE. 22| | 23| |import Foundation 24| | 25| |public final class SqOperation: Operation { 26| | 27| | public let info: JobInfo 28| | public let logger: SwiftQueueLogger 29| | public let dispatchQueue: DispatchQueue 30| | 31| | public var nextRunSchedule: Date? 32| | 33| | internal let handler: Job 34| | internal var lastError: Error? 35| | 36| | private let constraints: [JobConstraint] 37| | private let listener: JobListener? 38| | 39| | /// Current number of repetition. Transient value 40| | internal var currentRepetition: Int = 0 41| | 42| | public override var name: String? { 43| 87| get { 44| 87| let constraint: UniqueUUIDConstraint? = getConstraint(info) 45| 87| return constraint?.uuid 46| 87| } set { } 47| | } 48| 5| public override var queuePriority: QueuePriority { get { info.priority } set { } } 49| | 50| | @available(iOS 8.0, macCatalyst 13.0, *) 51| 5| public override var qualityOfService: QualityOfService { get { info.qualityOfService } set { } } 52| | 53| | private var jobIsExecuting: Bool = false 54| | public override var isExecuting: Bool { 55| 208| get { jobIsExecuting } 56| 43| set { 57| 43| willChangeValue(forKey: "isExecuting") 58| 43| jobIsExecuting = newValue 59| 43| didChangeValue(forKey: "isExecuting") 60| 43| } 61| | } 62| | 63| | private var jobIsFinished: Bool = false 64| | public override var isFinished: Bool { 65| 859| get { jobIsFinished } 66| 43| set { 67| 43| willChangeValue(forKey: "isFinished") 68| 43| jobIsFinished = newValue 69| 43| didChangeValue(forKey: "isFinished") 70| 43| } 71| | } 72| | 73| | internal init(_ job: Job, 74| | _ info: JobInfo, 75| | _ logger: SwiftQueueLogger, 76| | _ listener: JobListener?, 77| | _ dispatchQueue: DispatchQueue, 78| | _ constraints: [JobConstraint] 79| 165| ) { 80| 165| self.handler = job 81| 165| self.info = info 82| 165| self.logger = logger 83| 165| self.listener = listener 84| 165| self.dispatchQueue = dispatchQueue 85| 165| self.constraints = constraints 86| 165| 87| 165| super.init() 88| 165| } 89| | 90| 43| public override func start() { 91| 43| super.start() 92| 43| logger.log(.verbose, jobId: name, message: "Job has been started by the system") 93| 43| isExecuting = true 94| 43| run() 95| 43| } 96| | 97| 10| public override func cancel() { 98| 10| self.cancel(with: SwiftQueueError.canceled) 99| 10| } 100| | 101| 12| func cancel(with: Error) { 102| 12| logger.log(.verbose, jobId: name, message: "Job has been canceled") 103| 12| lastError = with 104| 12| onTerminate() 105| 12| super.cancel() 106| 12| } 107| | 108| 43| func onTerminate() { 109| 43| logger.log(.verbose, jobId: name, message: "Job will not run anymore") 110| 43| if isExecuting { 111| 36| isFinished = true 112| 43| } 113| 43| } 114| | 115| | // cancel before schedule and serialize 116| 6| internal func abort(error: Error) { 117| 6| logger.log(.verbose, jobId: name, message: "Job has not been scheduled due to \(error.localizedDescription)") 118| 6| lastError = error 119| 6| // Need to be called manually since the task is actually not in the queue. So cannot call cancel() 120| 6| handler.onRemove(result: .fail(error)) 121| 6| listener?.onTerminated(job: info, result: .fail(error)) 122| 6| } 123| | 124| 260| public func run() { 125| 260| if isCancelled && !isFinished { 126| 7| isFinished = true 127| 260| } 128| 260| if isFinished { 129| 7| return 130| 253| } 131| 253| 132| 253| do { 133| 253| try self.willRunJob() 134| 253| } catch let error { 135| 1| logger.log(.warning, jobId: name, message: "Job cannot run due to \(error.localizedDescription)") 136| 1| // Will never run again 137| 1| cancel(with: error) 138| 1| return 139| 252| } 140| 252| 141| 252| guard self.checkIfJobCanRunNow() else { 142| 2| // Constraint fail. 143| 2| // Constraint will call run when it's ready 144| 2| logger.log(.verbose, jobId: name, message: "Job cannot run now. Execution is postponed") 145| 2| return 146| 250| } 147| 250| 148| 250| logger.log(.verbose, jobId: name, message: "Job is running") 149| 250| listener?.onBeforeRun(job: info) 150| 250| handler.onRun(callback: self) 151| 250| } 152| | 153| 42| internal func remove() { 154| 42| let result = lastError.map(JobCompletion.fail) ?? JobCompletion.success 155| 42| logger.log(.verbose, jobId: name, message: "Job is removed from the queue result=\(result)") 156| 42| handler.onRemove(result: result) 157| 42| listener?.onTerminated(job: info, result: result) 158| 42| } 159| | 160| |} 161| | 162| |extension SqOperation: JobResult { 163| | 164| 249| public func done(_ result: JobCompletion) { 165| 249| guard !isFinished else { return } 166| 247| 167| 247| listener?.onAfterRun(job: info, result: result) 168| 247| 169| 247| switch result { 170| 247| case .success: 171| 131| completionSuccess() 172| 247| case .fail(let error): 173| 116| completionFail(error: error) 174| 247| } 175| 247| } 176| | 177| 116| private func completionFail(error: Error) { 178| 116| logger.log(.warning, jobId: name, message: "Job completed with error \(error.localizedDescription)") 179| 116| lastError = error 180| 116| 181| 116| if let constraint: JobRetryConstraint = getConstraint(info) { 182| 114| constraint.onCompletionFail(sqOperation: self, error: error) 183| 116| } else { 184| 2| onTerminate() 185| 116| } 186| 116| } 187| | 188| 131| private func completionSuccess() { 189| 131| logger.log(.verbose, jobId: name, message: "Job completed successfully") 190| 131| lastError = nil 191| 131| currentRepetition = 0 192| 131| 193| 131| if let constraint: RepeatConstraint = getConstraint(info) { 194| 112| constraint.completionSuccess(sqOperation: self) 195| 131| } else { 196| 19| onTerminate() 197| 131| } 198| 131| } 199| | 200| |} 201| | 202| |extension SqOperation { 203| | 204| 151| func willScheduleJob(queue: SqOperationQueue) throws { 205| 151| for constraint in info.constraints { 206| 67| try constraint.willSchedule(queue: queue, operation: self) 207| 151| } 208| 151| } 209| | 210| 253| func willRunJob() throws { 211| 257| for constraint in info.constraints { 212| 257| try constraint.willRun(operation: self) 213| 257| } 214| 253| } 215| | 216| 252| func checkIfJobCanRunNow() -> Bool { 217| 255| for constraint in info.constraints where constraint.run(operation: self) == false { 218| 255| return false 219| 18.4E| } 220| 18.4E| return true 221| 252| } 222| | 223| |} /Users/runner/work/SwiftQueue/SwiftQueue/Sources/SwiftQueue/SqOperationQueue.swift: 1| |// The MIT License (MIT) 2| |// 3| |// Copyright (c) 2019 Lucas Nelaupe 4| |// 5| |// Permission is hereby granted, free of charge, to any person obtaining a copy 6| |// of this software and associated documentation files (the "Software"), to deal 7| |// in the Software without restriction, including without limitation the rights 8| |// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9| |// copies of the Software, and to permit persons to whom the Software is 10| |// furnished to do so, subject to the following conditions: 11| |// 12| |// The above copyright notice and this permission notice shall be included in all 13| |// copies or substantial portions of the Software. 14| |// 15| |// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16| |// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17| |// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18| |// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19| |// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20| |// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21| |// SOFTWARE. 22| | 23| |import Foundation 24| | 25| |public final class SqOperationQueue: OperationQueue { 26| | 27| | private let creator: JobCreator 28| | private let queue: Queue 29| | 30| | private let dispatchQueue: DispatchQueue 31| | private let persister: JobPersister 32| | private let serializer: JobInfoSerializer 33| | private let logger: SwiftQueueLogger 34| | private let listener: JobListener? 35| | 36| 147| private let trigger: Operation = TriggerOperation() 37| | 38| 147| init(_ params: SqManagerParams, _ queue: Queue, _ isSuspended: Bool) { 39| 147| self.queue = queue 40| 147| self.creator = params.jobCreator 41| 147| self.dispatchQueue = params.dispatchQueue 42| 147| 43| 147| self.persister = params.persister 44| 147| self.serializer = params.serializer 45| 147| self.logger = params.logger 46| 147| self.listener = params.listener 47| 147| 48| 147| super.init() 49| 147| 50| 147| self.isSuspended = isSuspended 51| 147| 52| 147| self.name = queue.name 53| 147| self.maxConcurrentOperationCount = queue.maxConcurrent 54| 147| 55| 147| if params.initInBackground { 56| 0| params.dispatchQueue.async { [weak self] in 57| 0| self?.loadSerializedTasks(name: queue.name) 58| 0| } 59| 147| } else { 60| 147| self.loadSerializedTasks(name: queue.name) 61| 147| } 62| 147| } 63| | 64| 147| private func loadSerializedTasks(name: String) { 65| 147| persister.restore(queueName: name).compactMap { string -> SqOperation? in 66| 5| do { 67| 5| let info = try serializer.deserialize(json: string) 68| 5| let job = creator.create(type: info.type, params: info.params) 69| 5| let constraints = info.constraints 70| 5| 71| 5| return SqOperation(job, info, logger, listener, dispatchQueue, constraints) 72| 5| } catch let error { 73| 0| logger.log(.error, jobId: "UNKNOWN", message: "Unable to deserialize job error=\(error.localizedDescription)") 74| 0| return nil 75| 0| } 76| 1| }.sorted { operation, operation2 in 77| 1| operation.info.createTime < operation2.info.createTime 78| 5| }.forEach { operation in 79| 5| self.addOperationInternal(operation, wait: false) 80| 5| } 81| 147| super.addOperation(trigger) 82| 147| } 83| | 84| 147| public override func addOperation(_ ope: Operation) { 85| 147| self.addOperationInternal(ope, wait: true) 86| 147| } 87| | 88| 152| private func addOperationInternal(_ ope: Operation, wait: Bool) { 89| 152| guard !ope.isFinished else { return } 90| 152| 91| 152| if wait { 92| 147| ope.addDependency(trigger) 93| 152| } 94| 152| 95| 152| guard let job = ope as? SqOperation else { 96| 1| // Not a job Task I don't care 97| 1| super.addOperation(ope) 98| 1| return 99| 151| } 100| 151| 101| 151| do { 102| 151| try job.willScheduleJob(queue: self) 103| 151| } catch let error { 104| 6| job.abort(error: error) 105| 6| return 106| 145| } 107| 145| 108| 145| job.completionBlock = { [weak self] in 109| 145| self?.completed(job) 110| 145| } 111| 145| super.addOperation(job) 112| 145| } 113| | 114| 3| func cancelOperations(tag: String) { 115| 4| for case let operation as SqOperation in operations { 116| 8| for case let constraint as TagConstraint in operation.info.constraints where constraint.contains(tag: tag) { 117| 8| operation.cancel() 118| 8| } 119| 4| } 120| 3| } 121| | 122| 1| func cancelOperations(uuid: String) { 123| 1| for case let operation as SqOperation in operations { 124| 1| for case let constraint as UniqueUUIDConstraint in operation.info.constraints where constraint.uuid == uuid { 125| 1| operation.cancel() 126| 1| return 127| 1| } 128| 0| } 129| 0| } 130| | 131| 42| private func completed(_ job: SqOperation) { 132| 42| // Remove this operation from serialization 133| 42| if let taskId = job.name { 134| 16| let persisterConstraint: PersisterConstraint? = getConstraint(job.info) 135| 16| persisterConstraint?.remove(queueName: queue.name, taskId: taskId) 136| 42| } 137| 42| 138| 42| let chargingConstraint: BatteryChargingConstraint? = getConstraint(job.info) 139| 42| chargingConstraint?.unregister() 140| 42| 141| 42| job.remove() 142| 42| } 143| | 144| 146| func createHandler(type: String, params: [String: Any]?) -> Job { 145| 146| return creator.create(type: type, params: params) 146| 146| } 147| | 148| |} 149| | 150| |internal class TriggerOperation: Operation {} /Users/runner/work/SwiftQueue/SwiftQueue/Sources/SwiftQueue/SwiftQueue.swift: 1| |// The MIT License (MIT) 2| |// 3| |// Copyright (c) 2019 Lucas Nelaupe 4| |// 5| |// Permission is hereby granted, free of charge, to any person obtaining a copy 6| |// of this software and associated documentation files (the "Software"), to deal 7| |// in the Software without restriction, including without limitation the rights 8| |// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9| |// copies of the Software, and to permit persons to whom the Software is 10| |// furnished to do so, subject to the following conditions: 11| |// 12| |// The above copyright notice and this permission notice shall be included in all 13| |// copies or substantial portions of the Software. 14| |// 15| |// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16| |// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17| |// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18| |// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19| |// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20| |// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21| |// SOFTWARE. 22| | 23| |import Foundation 24| | 25| |/// Protocol to create instance of your job 26| |public protocol JobCreator { 27| | 28| | /// method called when a job has be to instantiate 29| | /// Type as specified in JobBuilder.init(type) and params as JobBuilder.with(params) 30| | func create(type: String, params: [String: Any]?) -> Job 31| | 32| |} 33| | 34| |public protocol QueueCreator { 35| | 36| | func create(queueName: String) -> Queue 37| | 38| |} 39| | 40| |/// Method to implement to have a custom persister 41| |public protocol JobPersister { 42| | 43| | /// Return an array of QueueName persisted 44| | func restore() -> [String] 45| | 46| | /// Restore all job in a single queue 47| | func restore(queueName: String) -> [String] 48| | 49| | /// Add a single job to a single queue with custom params 50| | func put(queueName: String, taskId: String, data: String) 51| | 52| | /// Remove a single job for a single queue 53| | func remove(queueName: String, taskId: String) 54| | 55| | /// Remove all task 56| | func clearAll() 57| | 58| |} 59| | 60| |/// Class to serialize and deserialize `JobInfo` 61| |public protocol JobInfoSerializer { 62| | 63| | /// Convert `JobInfo` into a representable string 64| | func serialize(info: JobInfo) throws -> String 65| | 66| | /// Convert back a string to a `JobInfo` 67| | func deserialize(json: String) throws -> JobInfo 68| | 69| |} 70| | 71| |/// Callback to give result in synchronous or asynchronous job 72| |public protocol JobResult { 73| | 74| | /// Method callback to notify the completion of your 75| | func done(_ result: JobCompletion) 76| | 77| |} 78| | 79| |/// Enum to define possible Job completion values 80| |public enum JobCompletion { 81| | 82| | /// Job completed successfully 83| | case success 84| | 85| | /// Job completed with error 86| | case fail(Error) 87| | 88| |} 89| | 90| |/// Protocol to implement to run a job 91| |public protocol Job { 92| | 93| | /// Perform your operation 94| | /// Will be called in background thread 95| | func onRun(callback: JobResult) 96| | 97| | /// Fail has failed with the 98| | /// Will only gets called if the job can be retried 99| | /// Not applicable for 'ConstraintError' 100| | /// Not application if the retry(value) is less than 2 which is the case by default 101| | /// Will be called in background thread 102| | func onRetry(error: Error) -> RetryConstraint 103| | 104| | /// Job is removed from the queue and will never run again 105| | /// May be called in background or main thread 106| | func onRemove(result: JobCompletion) 107| | 108| |} 109| | 110| |public protocol Queue { 111| | 112| | var name: String { get } 113| | 114| | var maxConcurrent: Int { get } 115| | 116| |} 117| | 118| |public enum BasicQueue { 119| | case synchronous 120| | case concurrent 121| | case custom(String) 122| |} 123| | 124| |public class BasicQueueCreator: QueueCreator { 125| | 126| 48| public init() {} 127| | 128| 146| public func create(queueName: String) -> Queue { 129| 146| switch queueName { 130| 146| case "GLOBAL": return BasicQueue.synchronous 131| 146| case "MULTIPLE": return BasicQueue.concurrent 132| 146| default: return BasicQueue.custom(queueName) 133| 146| } 134| 146| } 135| | 136| |} 137| | 138| |extension BasicQueue: Queue { 139| | 140| 300| public var name: String { 141| 300| switch self { 142| 300| case .synchronous : return "GLOBAL" 143| 300| case .concurrent : return "MULTIPLE" 144| 300| case .custom(let variable) : return variable 145| 300| } 146| 300| } 147| | 148| 147| public var maxConcurrent: Int { 149| 147| switch self { 150| 147| case .synchronous : return 1 151| 147| case .concurrent : return 2 152| 147| case .custom : return 1 153| 147| } 154| 147| } 155| | 156| |} 157| | 158| |/// Listen from job status 159| |public protocol JobListener { 160| | 161| | /// Job will start executing 162| | func onBeforeRun(job: JobInfo) 163| | 164| | /// Job completed execution 165| | func onAfterRun(job: JobInfo, result: JobCompletion) 166| | 167| | /// Job is removed from the queue and will not run anymore 168| | func onTerminated(job: JobInfo, result: JobCompletion) 169| | 170| |} 171| | 172| |/// Enum to specify a limit 173| |public enum Limit { 174| | 175| | /// No limit 176| | case unlimited 177| | 178| | /// Limited to a specific number 179| | case limited(Double) 180| | 181| |} 182| | 183| |/// Generic class for any constraint violation 184| |public enum SwiftQueueError: Error { 185| | 186| | /// Job has been canceled 187| | case canceled 188| | 189| | /// Deadline has been reached 190| | case deadline 191| | 192| | /// Exception thrown when you try to schedule a job with a same ID as one currently scheduled 193| | case duplicate 194| | 195| | /// Job canceled inside onError. Parameter contains the origin error 196| | case onRetryCancel(Error) 197| | 198| | /// Job took too long to run 199| | case timeout 200| | 201| |} /Users/runner/work/SwiftQueue/SwiftQueue/Sources/SwiftQueue/SwiftQueueLogger.swift: 1| |// The MIT License (MIT) 2| |// 3| |// Copyright (c) 2019 Lucas Nelaupe 4| |// 5| |// Permission is hereby granted, free of charge, to any person obtaining a copy 6| |// of this software and associated documentation files (the "Software"), to deal 7| |// in the Software without restriction, including without limitation the rights 8| |// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9| |// copies of the Software, and to permit persons to whom the Software is 10| |// furnished to do so, subject to the following conditions: 11| |// 12| |// The above copyright notice and this permission notice shall be included in all 13| |// copies or substantial portions of the Software. 14| |// 15| |// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16| |// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17| |// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18| |// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19| |// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20| |// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21| |// SOFTWARE. 22| | 23| |import Foundation 24| | 25| |/// Specify different importance of logging 26| |public enum LogLevel: Int { 27| | 28| | /// Basic information about scheduling, running job and completion 29| | case verbose = 1 30| | /// Important but non fatal information 31| | case warning = 2 32| | /// Something went wrong during the scheduling or the execution 33| | case error = 3 34| | 35| |} 36| | 37| |public extension LogLevel { 38| | 39| | /// Describe type of level in human-way 40| 17| var description: String { 41| 17| switch self { 42| 17| case .verbose: 43| 7| return "verbose" 44| 17| case .warning: 45| 4| return "warning" 46| 17| case .error: 47| 6| return "error" 48| 17| } 49| 17| } 50| |} 51| | 52| |/// Protocol to implement for implementing your custom logger 53| |public protocol SwiftQueueLogger { 54| | 55| | /// Function called by the library to log an event 56| | func log(_ level: LogLevel, jobId: @autoclosure () -> String?, message: @autoclosure () -> String) 57| | 58| |} 59| | 60| |/// Class to compute the log and print to the console 61| |open class ConsoleLogger: SwiftQueueLogger { 62| | 63| | private let min: LogLevel 64| | 65| | /// Define minimum level to log. By default, it will log everything 66| 4| public init(min: LogLevel = .verbose) { 67| 4| self.min = min 68| 4| } 69| | 70| | /// Check for log level and create the output message 71| 14| public final func log(_ level: LogLevel, jobId: @autoclosure () -> String?, message: @autoclosure () -> String) { 72| 14| if min.rawValue <= level.rawValue { 73| 11| printComputed(output: "[SwiftQueue] level=\(level.description) jobId=\(jobId() ?? "nil") message=\(message())") 74| 14| } 75| 14| } 76| | 77| | /// Print with default `print()` function. Can be override to changed the output 78| 0| open func printComputed(output: String) { 79| 0| print(output) 80| 0| } 81| | 82| |} 83| | 84| |/// Class to ignore all kind of logs 85| |public class NoLogger: SwiftQueueLogger { 86| | 87| | /// Singleton instance to avoid multiple instance across all queues 88| | public static let shared = NoLogger() 89| | 90| 1| private init() {} 91| | 92| | /// Default implementation that will not log anything 93| 643| public func log(_ level: LogLevel, jobId: @autoclosure () -> String?, message: @autoclosure () -> String) { 94| 643| // Nothing to do 95| 643| } 96| |} /Users/runner/work/SwiftQueue/SwiftQueue/Sources/SwiftQueue/SwiftQueueManager.swift: 1| |// The MIT License (MIT) 2| |// 3| |// Copyright (c) 2019 Lucas Nelaupe 4| |// 5| |// Permission is hereby granted, free of charge, to any person obtaining a copy 6| |// of this software and associated documentation files (the "Software"), to deal 7| |// in the Software without restriction, including without limitation the rights 8| |// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9| |// copies of the Software, and to permit persons to whom the Software is 10| |// furnished to do so, subject to the following conditions: 11| |// 12| |// The above copyright notice and this permission notice shall be included in all 13| |// copies or substantial portions of the Software. 14| |// 15| |// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16| |// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17| |// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18| |// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19| |// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20| |// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21| |// SOFTWARE. 22| | 23| |import Foundation 24| |import Dispatch 25| | 26| |/// Global manager to perform operations on all your queues/ 27| |/// You will have to keep this instance. We highly recommend you to store this instance in a Singleton 28| |/// Creating and instance of this class will automatically un-serialize your jobs and schedule them 29| |public final class SwiftQueueManager { 30| | 31| | internal let params: SqManagerParams 32| | 33| | /// Allow jobs in queue to be executed. 34| | public var isSuspended: Bool { 35| 10| didSet { 36| 10| for element in manage.values { 37| 9| element.isSuspended = isSuspended 38| 10| } 39| 10| } 40| | } 41| | 42| 47| private var manage = [String: SqOperationQueue]() 43| | 44| 47| internal init(params: SqManagerParams, isSuspended: Bool) { 45| 47| self.params = params 46| 47| self.isSuspended = isSuspended 47| 47| 48| 47| for queueName in params.persister.restore() { 49| 4| _ = createQueue(queueName: queueName, initInBackground: params.initInBackground) 50| 47| } 51| 47| } 52| | 53| 146| internal func getQueue(queueName: String) -> SqOperationQueue { 54| 146| return manage[queueName] ?? createQueue(queueName: queueName, initInBackground: false) 55| 146| } 56| | 57| 146| private func createQueue(queueName: String, initInBackground: Bool) -> SqOperationQueue { 58| 146| let operationQueue = SqOperationQueue(params, params.queueCreator.create(queueName: queueName), isSuspended) 59| 146| manage[queueName] = operationQueue 60| 146| return operationQueue 61| 146| } 62| | 63| | /// Schedule a job to the queue 64| | /// TODO Need to remove this method 65| 146| public func enqueue(info: JobInfo) { 66| 146| let queue = getQueue(queueName: info.queueName) 67| 146| let job = queue.createHandler(type: info.type, params: info.params) 68| 146| let constraints = info.constraints 69| 146| 70| 146| let operation = SqOperation(job, info, params.logger, params.listener, params.dispatchQueue, constraints) 71| 146| 72| 146| queue.addOperation(operation) 73| 146| } 74| | 75| | /// All operations in all queues will be removed 76| 5| public func cancelAllOperations() { 77| 5| for element in manage.values { 78| 5| element.cancelAllOperations() 79| 5| } 80| 5| } 81| | 82| | /// All operations with this tag in all queues will be removed 83| 3| public func cancelOperations(tag: String) { 84| 3| assertNotEmptyString(tag) 85| 3| for element in manage.values { 86| 3| element.cancelOperations(tag: tag) 87| 3| } 88| 3| } 89| | 90| | /// All operations with this uuid in all queues will be removed 91| 1| public func cancelOperations(uuid: String) { 92| 1| assertNotEmptyString(uuid) 93| 1| for element in manage.values { 94| 1| element.cancelOperations(uuid: uuid) 95| 1| } 96| 1| } 97| | 98| | /// Blocks the current thread until all of the receiver’s queued and executing operations finish executing. 99| 9| public func waitUntilAllOperationsAreFinished() { 100| 9| for element in manage.values { 101| 9| element.waitUntilAllOperationsAreFinished() 102| 9| } 103| 9| } 104| | 105| |} 106| | 107| |/// Extension to query Job Manager 108| |public extension SwiftQueueManager { 109| | /// number of queue 110| 0| func queueCount() -> Int { 111| 0| return manage.values.count 112| 0| } 113| | 114| | /// Return queues UUID with the list of Jobs inside 115| 1| func getAll() -> [String: [JobInfo]] { 116| 1| var result = [String: [JobInfo]]() 117| 1| for (queueUuid, queue) in manage { 118| 0| var infos = [JobInfo]() 119| 0| for case let operation as SqOperation in queue.operations { 120| 0| infos.append(operation.info) 121| 0| } 122| 0| result[queueUuid] = infos 123| 1| } 124| 1| return result 125| 1| } 126| |} 127| | 128| |internal extension SwiftQueueManager { 129| | 130| 0| func getAllAllowBackgroundOperation() -> [SqOperation] { 131| 0| return manage.values 132| 0| .flatMap { $0.operations } 133| 0| .compactMap { $0 as? SqOperation } 134| 0| .filter { 135| 0| if let constraint: RepeatConstraint = getConstraint($0.info) { 136| 0| return constraint.executor.rawValue > Executor.foreground.rawValue 137| 0| } 138| 0| return false 139| 0| } 140| 0| } 141| | 142| 1| func getOperation(forUUID: String) -> SqOperation? { 143| 29| for queue: SqOperationQueue in manage.values { 144| 58| for operation in queue.operations where operation.name == forUUID { 145| 58| return operation as? SqOperation 146| 18.4E| } 147| 18.4E| } 148| 18.4E| return nil 149| 1| } 150| |} 151| | 152| |internal struct SqManagerParams { 153| | 154| | let jobCreator: JobCreator 155| | 156| | let queueCreator: QueueCreator 157| | 158| | var persister: JobPersister 159| | 160| | var serializer: JobInfoSerializer 161| | 162| | var logger: SwiftQueueLogger 163| | 164| | var listener: JobListener? 165| | 166| | var dispatchQueue: DispatchQueue 167| | 168| | var initInBackground: Bool 169| | 170| | init(jobCreator: JobCreator, 171| | queueCreator: QueueCreator, 172| | persister: JobPersister = UserDefaultsPersister(), 173| | serializer: JobInfoSerializer = DecodableSerializer(maker: DefaultConstraintMaker()), 174| | logger: SwiftQueueLogger = NoLogger.shared, 175| | listener: JobListener? = nil, 176| | initInBackground: Bool = false, 177| | dispatchQueue: DispatchQueue = DispatchQueue.global(qos: DispatchQoS.QoSClass.utility) 178| 48| ) { 179| 48| self.jobCreator = jobCreator 180| 48| self.queueCreator = queueCreator 181| 48| self.persister = persister 182| 48| self.serializer = serializer 183| 48| self.logger = logger 184| 48| self.listener = listener 185| 48| self.initInBackground = initInBackground 186| 48| self.dispatchQueue = dispatchQueue 187| 48| } 188| | 189| |} 190| | 191| |/// Entry point to create a `SwiftQueueManager` 192| |public final class SwiftQueueManagerBuilder { 193| | 194| | private var params: SqManagerParams 195| | private var isSuspended: Bool = false 196| | 197| | /// Creator to convert `JobInfo.type` to `Job` instance 198| 47| public init(creator: JobCreator, queueCreator: QueueCreator = BasicQueueCreator()) { 199| 47| params = SqManagerParams(jobCreator: creator, queueCreator: queueCreator) 200| 47| } 201| | 202| | /// Custom way of saving `JobInfo`. Will use `UserDefaultsPersister` by default 203| 47| public func set(persister: JobPersister) -> Self { 204| 47| params.persister = persister 205| 47| return self 206| 47| } 207| | 208| | /// Custom way of serializing `JobInfo`. Will use `DecodableSerializer` by default 209| 2| public func set(serializer: JobInfoSerializer) -> Self { 210| 2| params.serializer = serializer 211| 2| return self 212| 2| } 213| | 214| | /// Internal event logger. `NoLogger` by default 215| | /// Use `ConsoleLogger` to print to the console. This is not recommended since the print is synchronous 216| | /// and it can be and expensive operation. Prefer using a asynchronous logger like `SwiftyBeaver`. 217| 1| public func set(logger: SwiftQueueLogger) -> Self { 218| 1| params.logger = logger 219| 1| return self 220| 1| } 221| | 222| | /// Start jobs directly when they are scheduled or not. `false` by default 223| 5| public func set(isSuspended: Bool) -> Self { 224| 5| self.isSuspended = isSuspended 225| 5| return self 226| 5| } 227| | 228| | /// Deserialize jobs synchronously after creating the `SwiftQueueManager` instance. `true` by default 229| 0| public func set(initInBackground: Bool) -> Self { 230| 0| params.initInBackground = initInBackground 231| 0| return self 232| 0| } 233| | 234| | /// Listen for job 235| 1| public func set(listener: JobListener) -> Self { 236| 1| params.listener = listener 237| 1| return self 238| 1| } 239| | 240| 1| public func set(dispatchQueue: DispatchQueue) -> Self { 241| 1| params.dispatchQueue = dispatchQueue 242| 1| return self 243| 1| } 244| | 245| | /// Get an instance of `SwiftQueueManager` 246| 47| public func build() -> SwiftQueueManager { 247| 47| return SwiftQueueManager(params: params, isSuspended: isSuspended) 248| 47| } 249| | 250| |} /Users/runner/work/SwiftQueue/SwiftQueue/Sources/SwiftQueue/UserDefaultsPersister.swift: 1| |// The MIT License (MIT) 2| |// 3| |// Copyright (c) 2019 Lucas Nelaupe 4| |// 5| |// Permission is hereby granted, free of charge, to any person obtaining a copy 6| |// of this software and associated documentation files (the "Software"), to deal 7| |// in the Software without restriction, including without limitation the rights 8| |// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9| |// copies of the Software, and to permit persons to whom the Software is 10| |// furnished to do so, subject to the following conditions: 11| |// 12| |// The above copyright notice and this permission notice shall be included in all 13| |// copies or substantial portions of the Software. 14| |// 15| |// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16| |// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17| |// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18| |// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19| |// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20| |// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21| |// SOFTWARE. 22| | 23| |import Foundation 24| | 25| |/// Persist jobs in UserDefaults 26| |public class UserDefaultsPersister: JobPersister { 27| | 28| 61| private let store = UserDefaults() 29| | private let key: String 30| | 31| | /// Create a Job persister with a custom key 32| 61| public init(key: String = "SwiftQueueInfo") { 33| 61| self.key = key 34| 61| } 35| | 36| | // Structure as follow 37| | // [group:[id:data]] 38| 17| public func restore() -> [String] { 39| 17| let values: [String: Any] = store.value(forKey: key) as? [String: Any] ?? [:] 40| 17| return Array(values.keys) 41| 17| } 42| | 43| | /// Restore jobs for a single queue 44| | /// Returns an array of String. serialized job 45| 114| public func restore(queueName: String) -> [String] { 46| 114| let values: [String: [String: String]] = store.value(forKey: key) as? [String: [String: String]] ?? [:] 47| 114| let tasks: [String: String] = values[queueName] ?? [:] 48| 114| return Array(tasks.values) 49| 114| } 50| | 51| | /// Insert a job to a specific queue 52| 11| public func put(queueName: String, taskId: String, data: String) { 53| 11| var values: [String: [String: String]] = store.value(forKey: key) as? [String: [String: String]] ?? [:] 54| 11| if values[queueName] != nil { 55| 3| values[queueName]?[taskId] = data 56| 11| } else { 57| 8| values[queueName] = [taskId: data] 58| 11| } 59| 11| store.setValue(values, forKey: key) 60| 11| } 61| | 62| | /// Remove a specific task from a queue 63| 6| public func remove(queueName: String, taskId: String) { 64| 6| var values: [String: [String: String]]? = store.value(forKey: key) as? [String: [String: String]] 65| 6| values?[queueName]?.removeValue(forKey: taskId) 66| 6| store.setValue(values, forKey: key) 67| 6| } 68| | 69| | /// Remove all tasks 70| 1| public func clearAll() { 71| 1| store.removeObject(forKey: key) 72| 1| } 73| | 74| |} /Users/runner/work/SwiftQueue/SwiftQueue/Sources/SwiftQueue/Utils.swift: 1| |// The MIT License (MIT) 2| |// 3| |// Copyright (c) 2019 Lucas Nelaupe 4| |// 5| |// Permission is hereby granted, free of charge, to any person obtaining a copy 6| |// of this software and associated documentation files (the "Software"), to deal 7| |// in the Software without restriction, including without limitation the rights 8| |// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9| |// copies of the Software, and to permit persons to whom the Software is 10| |// furnished to do so, subject to the following conditions: 11| |// 12| |// The above copyright notice and this permission notice shall be included in all 13| |// copies or substantial portions of the Software. 14| |// 15| |// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16| |// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17| |// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18| |// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19| |// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20| |// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21| |// SOFTWARE. 22| | 23| |import Foundation 24| |import Dispatch 25| | 26| |internal extension DispatchQueue { 27| | 28| 12| func runAfter(_ seconds: TimeInterval, callback: @escaping () -> Void) { 29| 12| let delta = DispatchTime.now() + seconds 30| 12| asyncAfter(deadline: delta, execute: callback) 31| 12| } 32| | 33| |} 34| | 35| 317|func assertNotEmptyString(_ string: @autoclosure () -> String, file: StaticString = #file, line: UInt = #line) { 36| 317| assert(!string().isEmpty, file: file, line: line) 37| 317|} 38| | 39| |internal extension Limit { 40| | 41| 19| var validate: Bool { 42| 19| switch self { 43| 19| case .unlimited: 44| 7| return true 45| 19| case .limited(let val): 46| 12| return val >= 0 47| 19| } 48| 19| } 49| | 50| 108| mutating func decreaseValue(by: Double) { 51| 108| if case .limited(let limit) = self { 52| 9| let value = limit - by 53| 9| assert(value >= 0) 54| 9| self = Limit.limited(value) 55| 108| } 56| 108| } 57| | 58| |} 59| | 60| |extension Limit: Codable { 61| | 62| | private enum CodingKeys: String, CodingKey { case value } 63| | 64| 1| public init(from decoder: Decoder) throws { 65| 1| let values = try decoder.container(keyedBy: CodingKeys.self) 66| 1| let value = try values.decode(Double.self, forKey: .value) 67| 1| self = value < 0 ? Limit.unlimited : Limit.limited(value) 68| 1| } 69| | 70| 1| public func encode(to encoder: Encoder) throws { 71| 1| var container = encoder.container(keyedBy: CodingKeys.self) 72| 1| switch self { 73| 1| case .unlimited: 74| 0| try container.encode(-1, forKey: .value) 75| 1| case .limited(let value): 76| 1| assert(value >= 0) 77| 1| try container.encode(value, forKey: .value) 78| 1| } 79| 1| } 80| | 81| |} 82| | 83| |extension Limit: Equatable { 84| | 85| 11| public static func == (lhs: Limit, rhs: Limit) -> Bool { 86| 11| switch (lhs, rhs) { 87| 11| case let (.limited(lValue), .limited(rValue)): 88| 5| return lValue == rValue 89| 11| case (.unlimited, .unlimited): 90| 3| return true 91| 11| default: 92| 3| return false 93| 11| } 94| 11| } 95| |} 96| | 97| |internal extension Operation.QueuePriority { 98| | 99| 4| init(fromValue: Int?) { 100| 4| guard let value = fromValue, let priority = Operation.QueuePriority(rawValue: value) else { 101| 0| self = Operation.QueuePriority.normal 102| 0| return 103| 4| } 104| 4| self = priority 105| 4| } 106| | 107| |} 108| | 109| |internal extension QualityOfService { 110| | 111| 4| init(fromValue: Int?) { 112| 4| guard let value = fromValue, let service = QualityOfService(rawValue: value) else { 113| 0| self = QualityOfService.default 114| 0| return 115| 4| } 116| 4| self = service 117| 4| } 118| | 119| |} 120| | 121| |internal extension String { 122| | 123| 18| static func fromUTF8(data: Data, key: [CodingKey] = []) throws -> String { 124| 18| guard let utf8 = String(data: data, encoding: .utf8) else { 125| 0| throw DecodingError.dataCorrupted(DecodingError.Context( 126| 0| codingPath: key, 127| 0| debugDescription: "Unexpected error") 128| 0| ) 129| 18| } 130| 18| return utf8 131| 18| } 132| | 133| 8| func utf8Data() throws -> Data { 134| 8| guard let data = self.data(using: .utf8) else { 135| 0| throw DecodingError.dataCorrupted(DecodingError.Context( 136| 0| codingPath: [], 137| 0| debugDescription: "Unexpected error") 138| 0| ) 139| 8| } 140| 8| return data 141| 8| } 142| | 143| |} 144| | 145| 412|internal func getConstraint(_ operation: JobInfo?) -> T? { 146| 412| guard let constraints = operation?.constraints else { return nil } 147| 412| return getConstraint(constraints) 148| 412|} 149| | 150| 419|internal func getConstraint(_ constraints: [JobConstraint]) -> T? { 151| 420| for case let constraint as T in constraints { 152| 420| return constraint 153| 18.4E| } 154| 18.4E| return nil 155| 419|} <<<<<< EOF # path=./SwiftQueueTests.xctest.coverage.txt /Users/runner/work/SwiftQueue/SwiftQueue/Tests/SwiftQueueTests/BackgroundTasksTest.swift: 1| |// The MIT License (MIT) 2| |// 3| |// Copyright (c) 2019 Lucas Nelaupe 4| |// 5| |// Permission is hereby granted, free of charge, to any person obtaining a copy 6| |// of this software and associated documentation files (the "Software"), to deal 7| |// in the Software without restriction, including without limitation the rights 8| |// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9| |// copies of the Software, and to permit persons to whom the Software is 10| |// furnished to do so, subject to the following conditions: 11| |// 12| |// The above copyright notice and this permission notice shall be included in all 13| |// copies or substantial portions of the Software. 14| |// 15| |// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16| |// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17| |// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18| |// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19| |// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20| |// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21| |// SOFTWARE. 22| | 23| |import XCTest 24| |import Dispatch 25| |@testable import SwiftQueue 26| | 27| |@available(iOS 13.0, tvOS 13.0, macOS 10.15, *) 28| |class BackgroundTasksTest { 29| | 30| | @available(iOS 13.0, tvOS 13.0, macOS 10.15, *) 31| 0| public func testBackgroundOperationShouldNotRun() { 32| 0| let (type, job) = (UUID().uuidString, TestJob()) 33| 0| 34| 0| let creator = TestCreator([type: job]) 35| 0| 36| 0| let manager = SwiftQueueManagerBuilder(creator: creator).set(persister: NoPersister.shared).build() 37| 0| JobBuilder(type: type) 38| 0| .periodic(executor: .background) 39| 0| .internet(atLeast: .wifi) 40| 0| .priority(priority: .veryHigh) 41| 0| .service(quality: .background) 42| 0| .schedule(manager: manager) 43| 0| 44| 0| job.assertNoRun() 45| 0| } 46| | 47| | @available(iOS 13.0, tvOS 13.0, macOS 10.15, *) 48| 0| public func testBuilderPeriodicLimited() throws { 49| 0| let type = UUID().uuidString 50| 0| let limited: Double = 123 51| 0| let interval: Double = 12342 52| 0| let executor = Executor.any 53| 0| 54| 0| let jobInfo = JobBuilder(type: type).periodic(limit: .limited(limited), interval: interval, executor: .any).info 55| 0| 56| 0| let constraint: RepeatConstraint? = getConstraint(jobInfo) 57| 0| XCTAssertNotNil(constraint) 58| 0| 59| 0| XCTAssertEqual(constraint?.maxRun, Limit.limited(limited)) 60| 0| XCTAssertEqual(constraint?.interval, interval) 61| 0| XCTAssertEqual(constraint?.executor, executor) 62| 0| 63| 0| } 64| | 65| | @available(iOS 13.0, tvOS 13.0, macOS 10.15, *) 66| 0| public func testBuilderPeriodicBackground() throws { 67| 0| let type = UUID().uuidString 68| 0| let limited: Double = 123 69| 0| let interval: Double = 12342 70| 0| let executor = Executor.background 71| 0| 72| 0| let jobInfo = JobBuilder(type: type).periodic(limit: .limited(limited), interval: interval, executor: .background).info 73| 0| 74| 0| let constraint: RepeatConstraint? = getConstraint(jobInfo) 75| 0| XCTAssertNotNil(constraint) 76| 0| 77| 0| XCTAssertEqual(constraint?.maxRun, Limit.limited(limited)) 78| 0| XCTAssertEqual(constraint?.interval, interval) 79| 0| XCTAssertEqual(constraint?.executor, executor) 80| 0| } 81| | 82| | @available(iOS 13.0, tvOS 13.0, macOS 10.15, *) 83| 0| public func testGetAllAllowBackgroundOperation() { 84| 0| let (type, job) = (UUID().uuidString, TestJob()) 85| 0| 86| 0| let id = UUID().uuidString 87| 0| let id2 = UUID().uuidString 88| 0| 89| 0| let group = UUID().uuidString 90| 0| let group2 = UUID().uuidString 91| 0| 92| 0| let creator = TestCreator([type: job]) 93| 0| 94| 0| let persister = PersisterTracker(key: UUID().uuidString) 95| 0| 96| 0| let manager = SwiftQueueManagerBuilder(creator: creator).set(isSuspended: true).set(persister: persister).build() 97| 0| 98| 0| JobBuilder(type: type).periodic(executor: .foreground).parallel(queueName: group).schedule(manager: manager) 99| 0| JobBuilder(type: type).periodic(executor: .foreground).parallel(queueName: group2).schedule(manager: manager) 100| 0| 101| 0| JobBuilder(type: type).singleInstance(forId: id).periodic(executor: .background).parallel(queueName: group).schedule(manager: manager) 102| 0| JobBuilder(type: type).singleInstance(forId: id2).periodic(executor: .any).parallel(queueName: group2).schedule(manager: manager) 103| 0| 104| 0| let result = manager.getAllAllowBackgroundOperation() 105| 0| 106| 0| XCTAssertEqual(2, result.count) 107| 0| } 108| | 109| |} /Users/runner/work/SwiftQueue/SwiftQueue/Tests/SwiftQueueTests/BasicConstraintTest.swift: 1| |// The MIT License (MIT) 2| |// 3| |// Copyright (c) 2019 Lucas Nelaupe 4| |// 5| |// Permission is hereby granted, free of charge, to any person obtaining a copy 6| |// of this software and associated documentation files (the "Software"), to deal 7| |// in the Software without restriction, including without limitation the rights 8| |// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9| |// copies of the Software, and to permit persons to whom the Software is 10| |// furnished to do so, subject to the following conditions: 11| |// 12| |// The above copyright notice and this permission notice shall be included in all 13| |// copies or substantial portions of the Software. 14| |// 15| |// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16| |// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17| |// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18| |// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19| |// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20| |// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21| |// SOFTWARE. 22| | 23| |import Foundation 24| | 25| |import Foundation 26| |import XCTest 27| |@testable import SwiftQueue 28| | 29| |class BasicConstraintTest: XCTestCase { 30| | 31| 1| func testContraintThrowExceptionShouldCancelOperation() { 32| 1| let (type, job) = (UUID().uuidString, TestJob()) 33| 1| 34| 1| let creator = TestCreator([type: job]) 35| 1| let constraint = BasicConstraint(onWillSchedule: { 36| 1| throw JobError() 37| 1| }) 38| 1| 39| 1| let manager = SwiftQueueManagerBuilder(creator: creator).set(persister: NoPersister.shared).build() 40| 1| JobBuilder(type: type) 41| 1| .add(constraint: constraint) 42| 1| .schedule(manager: manager) 43| 1| 44| 1| job.awaitForRemoval() 45| 1| job.assertError() 46| 1| 47| 1| XCTAssertTrue(constraint.willScheduleCalled) 48| 1| XCTAssertFalse(constraint.willRunCalled) 49| 1| XCTAssertFalse(constraint.runCalled) 50| 1| } 51| | 52| 1| func testJobShouldBeCancelIfThrowExceptionInConstraintOnWillRun() { 53| 1| let (type, job) = (UUID().uuidString, TestJob()) 54| 1| 55| 1| let creator = TestCreator([type: job]) 56| 1| let constraint = BasicConstraint(onWillRun: { 57| 1| throw JobError() 58| 1| }) 59| 1| 60| 1| let manager = SwiftQueueManagerBuilder(creator: creator).set(persister: NoPersister.shared).build() 61| 1| JobBuilder(type: type) 62| 1| .add(constraint: constraint) 63| 1| .schedule(manager: manager) 64| 1| 65| 1| job.assertNoRun() 66| 1| 67| 1| job.awaitForRemoval() 68| 1| job.assertError() 69| 1| 70| 1| XCTAssertTrue(constraint.willScheduleCalled) 71| 1| XCTAssertTrue(constraint.willRunCalled) 72| 1| XCTAssertFalse(constraint.runCalled) 73| 1| } 74| | 75| 1| func testOperationRunWhenConstraintTrigger() { 76| 1| let (type, job) = (UUID().uuidString, TestJob()) 77| 1| var operation: SqOperation? 78| 1| 79| 1| let creator = TestCreator([type: job]) 80| 1| let constraint = BasicConstraint(onRun: { ope in 81| 1| print("Operation") 82| 1| if operation != nil { 83| 0| return true 84| 1| } 85| 1| operation = ope 86| 1| return true 87| 1| }) 88| 1| 89| 1| let manager = SwiftQueueManagerBuilder(creator: creator).set(persister: NoPersister.shared).build() 90| 1| JobBuilder(type: type) 91| 1| .add(constraint: constraint) 92| 1| .schedule(manager: manager) 93| 1| 94| 1| job.assertNoRun() 95| 1| 96| 1| operation?.run() 97| 1| 98| 1| job.awaitForRemoval() 99| 1| job.assertSingleCompletion() 100| 1| 101| 1| XCTAssertTrue(constraint.willScheduleCalled) 102| 1| XCTAssertTrue(constraint.willRunCalled) 103| 1| XCTAssertTrue(constraint.runCalled) 104| 1| } 105| | 106| |} 107| | 108| |class BasicConstraint: CustomConstraint { 109| | 110| | let onWillSchedule: () throws-> Void 111| | let onWillRun: () throws -> Void 112| | let onRun: (SqOperation) -> Bool 113| | 114| 3| required init(onWillSchedule: @escaping () throws -> Void = {}, onWillRun: @escaping () throws -> Void = {}, onRun: @escaping (SqOperation) -> Bool = { _ in true}) { 115| 3| self.onWillSchedule = onWillSchedule 116| 3| self.onWillRun = onWillRun 117| 3| self.onRun = onRun 118| 3| } 119| | 120| 3| override func willSchedule(queue: SqOperationQueue, operation: SqOperation) throws { 121| 3| try super.willSchedule(queue: queue, operation: operation) 122| 3| try onWillSchedule() 123| 3| } 124| | 125| 2| override func willRun(operation: SqOperation) throws { 126| 2| try super.willRun(operation: operation) 127| 2| try onWillRun() 128| 2| } 129| | 130| 1| override func run(operation: SqOperation) -> Bool { 131| 1| super.run(operation: operation) 132| 1| return onRun(operation) 133| 1| } 134| | 135| |} /Users/runner/work/SwiftQueue/SwiftQueue/Tests/SwiftQueueTests/ConstraintTest+Charging.swift: 1| |// The MIT License (MIT) 2| |// 3| |// Copyright (c) 2019 Lucas Nelaupe 4| |// 5| |// Permission is hereby granted, free of charge, to any person obtaining a copy 6| |// of this software and associated documentation files (the "Software"), to deal 7| |// in the Software without restriction, including without limitation the rights 8| |// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9| |// copies of the Software, and to permit persons to whom the Software is 10| |// furnished to do so, subject to the following conditions: 11| |// 12| |// The above copyright notice and this permission notice shall be included in all 13| |// copies or substantial portions of the Software. 14| |// 15| |// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16| |// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17| |// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18| |// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19| |// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20| |// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21| |// SOFTWARE. 22| | 23| |import Foundation 24| |import XCTest 25| |@testable import SwiftQueue 26| | 27| |class ConstraintTestCharging: XCTestCase { 28| | 29| 1| func testChargingConstraintShouldRunNow() { 30| 1| let (type, job) = (UUID().uuidString, TestJob()) 31| 1| 32| 1| let creator = TestCreator([type: job]) 33| 1| 34| 1| let manager = SwiftQueueManagerBuilder(creator: creator).set(persister: NoPersister.shared).build() 35| 1| JobBuilder(type: type) 36| 1| .requireCharging() 37| 1| .schedule(manager: manager) 38| 1| 39| 1| job.awaitForRemoval() 40| 1| job.assertSingleCompletion() 41| 1| } 42| | 43| |} /Users/runner/work/SwiftQueue/SwiftQueue/Tests/SwiftQueueTests/ConstraintTest+Custom.swift: 1| |// The MIT License (MIT) 2| |// 3| |// Copyright (c) 2019 Lucas Nelaupe 4| |// 5| |// Permission is hereby granted, free of charge, to any person obtaining a copy 6| |// of this software and associated documentation files (the "Software"), to deal 7| |// in the Software without restriction, including without limitation the rights 8| |// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9| |// copies of the Software, and to permit persons to whom the Software is 10| |// furnished to do so, subject to the following conditions: 11| |// 12| |// The above copyright notice and this permission notice shall be included in all 13| |// copies or substantial portions of the Software. 14| |// 15| |// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16| |// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17| |// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18| |// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19| |// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20| |// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21| |// SOFTWARE. 22| | 23| |import Foundation 24| |import XCTest 25| |@testable import SwiftQueue 26| | 27| |class CustomConstraintTest: XCTestCase { 28| | 29| 1| func testCustomConstraint() { 30| 1| let (type, job) = (UUID().uuidString, TestJob()) 31| 1| 32| 1| let creator = TestCreator([type: job]) 33| 1| let constraint = CustomConstraint() 34| 1| 35| 1| let manager = SwiftQueueManagerBuilder(creator: creator).set(persister: NoPersister.shared).build() 36| 1| JobBuilder(type: type) 37| 1| .add(constraint: constraint) 38| 1| .schedule(manager: manager) 39| 1| 40| 1| job.awaitForRemoval() 41| 1| job.assertSingleCompletion() 42| 1| 43| 1| XCTAssertTrue(constraint.willScheduleCalled) 44| 1| XCTAssertTrue(constraint.willRunCalled) 45| 1| XCTAssertTrue(constraint.runCalled) 46| 1| } 47| | 48| |} 49| | 50| |class CustomConstraint: JobConstraint { 51| | 52| | var willScheduleCalled = false 53| | var willRunCalled = false 54| | var runCalled = false 55| | 56| 4| func willSchedule(queue: SqOperationQueue, operation: SqOperation) throws { 57| 4| willScheduleCalled = true 58| 4| } 59| | 60| 3| func willRun(operation: SqOperation) throws { 61| 3| willRunCalled = true 62| 3| } 63| | 64| 2| func run(operation: SqOperation) -> Bool { 65| 2| runCalled = true 66| 2| return true 67| 2| } 68| | 69| |} /Users/runner/work/SwiftQueue/SwiftQueue/Tests/SwiftQueueTests/ConstraintTest+Deadline.swift: 1| |// The MIT License (MIT) 2| |// 3| |// Copyright (c) 2019 Lucas Nelaupe 4| |// 5| |// Permission is hereby granted, free of charge, to any person obtaining a copy 6| |// of this software and associated documentation files (the "Software"), to deal 7| |// in the Software without restriction, including without limitation the rights 8| |// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9| |// copies of the Software, and to permit persons to whom the Software is 10| |// furnished to do so, subject to the following conditions: 11| |// 12| |// The above copyright notice and this permission notice shall be included in all 13| |// copies or substantial portions of the Software. 14| |// 15| |// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16| |// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17| |// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18| |// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19| |// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20| |// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21| |// SOFTWARE. 22| | 23| |import Foundation 24| |import XCTest 25| |@testable import SwiftQueue 26| | 27| |class ConstraintTestDeadline: XCTestCase { 28| | 29| 1| func testDeadlineWhenSchedule() { 30| 1| let (type, job) = (UUID().uuidString, TestJob()) 31| 1| 32| 1| let creator = TestCreator([type: job]) 33| 1| 34| 1| let manager = SwiftQueueManagerBuilder(creator: creator).set(persister: NoPersister.shared).build() 35| 1| JobBuilder(type: type) 36| 1| .deadline(date: Date(timeIntervalSinceNow: TimeInterval(-10))) 37| 1| .schedule(manager: manager) 38| 1| 39| 1| job.awaitForRemoval() 40| 1| job.assertRemovedBeforeRun(reason: .deadline) 41| 1| } 42| | 43| 1| func testDeadlineWhenRun() { 44| 1| let (type1, job1) = (UUID().uuidString, TestJob()) 45| 1| let (type2, job2) = (UUID().uuidString, TestJob()) 46| 1| 47| 1| let creator = TestCreator([type1: job1, type2: job2]) 48| 1| let manager = SwiftQueueManagerBuilder(creator: creator).set(persister: NoPersister.shared).build() 49| 1| 50| 1| JobBuilder(type: type1) 51| 1| .delay(time: Double.leastNonzeroMagnitude) 52| 1| .retry(limit: .limited(5)) 53| 1| .schedule(manager: manager) 54| 1| 55| 1| JobBuilder(type: type2) 56| 1| .deadline(date: Date()) // After 1 second should fail 57| 1| .retry(limit: .unlimited) 58| 1| .schedule(manager: manager) 59| 1| 60| 1| manager.waitUntilAllOperationsAreFinished() 61| 1| 62| 1| job1.awaitForRemoval() 63| 1| job1.assertSingleCompletion() 64| 1| 65| 1| job2.awaitForRemoval() 66| 1| job2.assertRemovedBeforeRun(reason: .deadline) 67| 1| } 68| | 69| 1| func testDeadlineWhenDeserialize() { 70| 1| let (type, job) = (UUID().uuidString, TestJob()) 71| 1| 72| 1| let creator = TestCreator([type: job]) 73| 1| 74| 1| let group = UUID().uuidString 75| 1| 76| 1| let json = JobBuilder(type: type) 77| 1| .parallel(queueName: group) 78| 1| .deadline(date: Date()) 79| 1| .build(job: job) 80| 1| .toJSONStringSafe() 81| 1| 82| 1| let persister = PersisterTracker(key: UUID().uuidString) 83| 1| persister.put(queueName: group, taskId: UUID().uuidString, data: json) 84| 1| 85| 1| let manager = SwiftQueueManagerBuilder(creator: creator).set(persister: persister).build() 86| 1| 87| 1| job.awaitForRemoval() 88| 1| job.assertRemovedBeforeRun(reason: .deadline) 89| 1| 90| 1| XCTAssertEqual(group, persister.restoreQueueName) 91| 1| 92| 1| manager.waitUntilAllOperationsAreFinished() 93| 1| } 94| | 95| 1| func testDeadlineAfterSchedule() { 96| 1| let (type, job) = (UUID().uuidString, TestJob()) 97| 1| 98| 1| let creator = TestCreator([type: job]) 99| 1| 100| 1| let manager = SwiftQueueManagerBuilder(creator: creator).set(persister: NoPersister.shared).build() 101| 1| JobBuilder(type: type) 102| 1| .delay(time: 60) 103| 1| .deadline(date: Date(timeIntervalSinceNow: Double.leastNonzeroMagnitude)) 104| 1| .retry(limit: .unlimited) 105| 1| .schedule(manager: manager) 106| 1| 107| 1| manager.waitUntilAllOperationsAreFinished() 108| 1| 109| 1| job.awaitForRemoval() 110| 1| job.assertRemovedBeforeRun(reason: .deadline) 111| 1| } 112| | 113| |} /Users/runner/work/SwiftQueue/SwiftQueue/Tests/SwiftQueueTests/ConstraintTest+Delay.swift: 1| |// The MIT License (MIT) 2| |// 3| |// Copyright (c) 2019 Lucas Nelaupe 4| |// 5| |// Permission is hereby granted, free of charge, to any person obtaining a copy 6| |// of this software and associated documentation files (the "Software"), to deal 7| |// in the Software without restriction, including without limitation the rights 8| |// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9| |// copies of the Software, and to permit persons to whom the Software is 10| |// furnished to do so, subject to the following conditions: 11| |// 12| |// The above copyright notice and this permission notice shall be included in all 13| |// copies or substantial portions of the Software. 14| |// 15| |// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16| |// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17| |// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18| |// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19| |// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20| |// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21| |// SOFTWARE. 22| | 23| |import Foundation 24| |import XCTest 25| |@testable import SwiftQueue 26| | 27| |class ConstraintTestDelay: XCTestCase { 28| | 29| 1| func testDelay() { 30| 1| let (type, job) = (UUID().uuidString, TestJob()) 31| 1| 32| 1| let creator = TestCreator([type: job]) 33| 1| 34| 1| let manager = SwiftQueueManagerBuilder(creator: creator).set(persister: NoPersister.shared).build() 35| 1| JobBuilder(type: type) 36| 1| .delay(time: 0.1) 37| 1| .schedule(manager: manager) 38| 1| 39| 1| manager.waitUntilAllOperationsAreFinished() 40| 1| 41| 1| job.awaitForRemoval() 42| 1| job.assertSingleCompletion() 43| 1| } 44| | 45| |} /Users/runner/work/SwiftQueue/SwiftQueue/Tests/SwiftQueueTests/ConstraintTest+Network.swift: 1| |// The MIT License (MIT) 2| |// 3| |// Copyright (c) 2019 Lucas Nelaupe 4| |// 5| |// Permission is hereby granted, free of charge, to any person obtaining a copy 6| |// of this software and associated documentation files (the "Software"), to deal 7| |// in the Software without restriction, including without limitation the rights 8| |// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9| |// copies of the Software, and to permit persons to whom the Software is 10| |// furnished to do so, subject to the following conditions: 11| |// 12| |// The above copyright notice and this permission notice shall be included in all 13| |// copies or substantial portions of the Software. 14| |// 15| |// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16| |// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17| |// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18| |// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19| |// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20| |// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21| |// SOFTWARE. 22| | 23| |import Foundation 24| |import XCTest 25| |@testable import SwiftQueue 26| | 27| |class ConstraintTestNetwork: XCTestCase { 28| | 29| 1| func testNetworkConstraint() { 30| 1| let (type, job) = (UUID().uuidString, TestJob()) 31| 1| 32| 1| let creator = TestCreator([type: job]) 33| 1| 34| 1| let manager = SwiftQueueManagerBuilder(creator: creator).set(persister: NoPersister.shared).build() 35| 1| JobBuilder(type: type) 36| 1| .internet(atLeast: .cellular) 37| 1| .schedule(manager: manager) 38| 1| 39| 1| job.awaitForRemoval() 40| 1| job.assertSingleCompletion() 41| 1| } 42| | 43| 1| func testNetworkConstraintWifi() { 44| 1| let (type, job) = (UUID().uuidString, TestJob()) 45| 1| 46| 1| let creator = TestCreator([type: job]) 47| 1| 48| 1| let manager = SwiftQueueManagerBuilder(creator: creator).set(persister: NoPersister.shared).build() 49| 1| JobBuilder(type: type) 50| 1| .internet(atLeast: .wifi) 51| 1| .schedule(manager: manager) 52| 1| 53| 1| job.awaitForRemoval() 54| 1| job.assertSingleCompletion() 55| 1| } 56| | 57| |} /Users/runner/work/SwiftQueue/SwiftQueue/Tests/SwiftQueueTests/ConstraintTest+Repeat.swift: 1| |// The MIT License (MIT) 2| |// 3| |// Copyright (c) 2019 Lucas Nelaupe 4| |// 5| |// Permission is hereby granted, free of charge, to any person obtaining a copy 6| |// of this software and associated documentation files (the "Software"), to deal 7| |// in the Software without restriction, including without limitation the rights 8| |// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9| |// copies of the Software, and to permit persons to whom the Software is 10| |// furnished to do so, subject to the following conditions: 11| |// 12| |// The above copyright notice and this permission notice shall be included in all 13| |// copies or substantial portions of the Software. 14| |// 15| |// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16| |// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17| |// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18| |// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19| |// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20| |// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21| |// SOFTWARE. 22| | 23| |import Foundation 24| |import XCTest 25| |@testable import SwiftQueue 26| | 27| |class ConstraintTestRepeat: XCTestCase { 28| | 29| 1| func testPeriodicJob() { 30| 1| let (type, job) = (UUID().uuidString, TestJob()) 31| 1| 32| 1| let creator = TestCreator([type: job]) 33| 1| 34| 1| let manager = SwiftQueueManagerBuilder(creator: creator).set(persister: NoPersister.shared).build() 35| 1| JobBuilder(type: type) 36| 1| .periodic(limit: .limited(5)) 37| 1| .schedule(manager: manager) 38| 1| 39| 1| job.awaitForRemoval() 40| 1| job.assertRunCount(expected: 5) 41| 1| job.assertCompletedCount(expected: 1) 42| 1| job.assertRetriedCount(expected: 0) 43| 1| job.assertCanceledCount(expected: 0) 44| 1| job.assertNoError() 45| 1| } 46| | 47| 1| func testPeriodicJobUnlimited() { 48| 1| let runLimit = 100 49| 1| let type = UUID().uuidString 50| 1| 51| 1| var runCount = 0 52| 1| 53| 100| let job = TestJob(retry: .retry(delay: 0)) { 54| 100| runCount += 1 55| 100| if runCount == runLimit { 56| 1| $0.done(.fail(JobError())) 57| 100| } else { 58| 99| $0.done(.success) 59| 100| } 60| 100| } 61| 1| 62| 1| let creator = TestCreator([type: job]) 63| 1| 64| 1| let manager = SwiftQueueManagerBuilder(creator: creator).set(persister: NoPersister.shared).build() 65| 1| JobBuilder(type: type) 66| 1| .periodic(limit: .unlimited) 67| 1| .schedule(manager: manager) 68| 1| 69| 1| job.awaitForRemoval() 70| 1| job.assertRunCount(expected: runLimit) 71| 1| job.assertCompletedCount(expected: 0) 72| 1| job.assertRetriedCount(expected: 0) 73| 1| job.assertCanceledCount(expected: 1) 74| 1| job.assertError() 75| 1| } 76| | 77| 1| func testRepeatableJobWithDelay() { 78| 1| let (type, job) = (UUID().uuidString, TestJob()) 79| 1| 80| 1| let creator = TestCreator([type: job]) 81| 1| 82| 1| let manager = SwiftQueueManagerBuilder(creator: creator).set(persister: NoPersister.shared).build() 83| 1| JobBuilder(type: type) 84| 1| .periodic(limit: .limited(2), interval: Double.leastNonzeroMagnitude) 85| 1| .schedule(manager: manager) 86| 1| 87| 1| job.awaitForRemoval() 88| 1| job.assertRunCount(expected: 2) 89| 1| job.assertCompletedCount(expected: 1) 90| 1| job.assertRetriedCount(expected: 0) 91| 1| job.assertCanceledCount(expected: 0) 92| 1| job.assertNoError() 93| 1| } 94| | 95| 1| func testRepeatSerialisation() { 96| 1| let (type, job, jobId) = (UUID().uuidString, TestJob(), UUID().uuidString) 97| 1| let queueId = UUID().uuidString 98| 1| let creator = TestCreator([type: job]) 99| 1| 100| 1| let task = JobBuilder(type: type) 101| 1| .singleInstance(forId: jobId) 102| 1| .periodic(limit: .limited(2), interval: Double.leastNonzeroMagnitude) 103| 1| .build(job: job) 104| 1| .toJSONStringSafe() 105| 1| 106| 1| // Should invert when deserialize 107| 1| let persister = PersisterTracker(key: UUID().uuidString) 108| 1| persister.put(queueName: queueId, taskId: jobId, data: task) 109| 1| 110| 1| let restore = persister.restore() 111| 1| XCTAssertEqual(restore.count, 1) 112| 1| XCTAssertEqual(restore[0], queueId) 113| 1| 114| 1| let manager = SwiftQueueManagerBuilder(creator: creator).set(persister: persister).build() 115| 1| 116| 1| XCTAssertEqual(queueId, persister.restoreQueueName) 117| 1| 118| 1| job.awaitForRemoval() 119| 1| job.assertRunCount(expected: 2) 120| 1| job.assertCompletedCount(expected: 1) 121| 1| job.assertRetriedCount(expected: 0) 122| 1| job.assertCanceledCount(expected: 0) 123| 1| job.assertNoError() 124| 1| 125| 1| manager.waitUntilAllOperationsAreFinished() 126| 1| } 127| | 128| |} /Users/runner/work/SwiftQueue/SwiftQueue/Tests/SwiftQueueTests/ConstraintTest+Retry.swift: 1| |// The MIT License (MIT) 2| |// 3| |// Copyright (c) 2019 Lucas Nelaupe 4| |// 5| |// Permission is hereby granted, free of charge, to any person obtaining a copy 6| |// of this software and associated documentation files (the "Software"), to deal 7| |// in the Software without restriction, including without limitation the rights 8| |// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9| |// copies of the Software, and to permit persons to whom the Software is 10| |// furnished to do so, subject to the following conditions: 11| |// 12| |// The above copyright notice and this permission notice shall be included in all 13| |// copies or substantial portions of the Software. 14| |// 15| |// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16| |// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17| |// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18| |// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19| |// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20| |// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21| |// SOFTWARE. 22| | 23| |import Foundation 24| |import XCTest 25| |@testable import SwiftQueue 26| | 27| |class ConstraintTestRetry: XCTestCase { 28| | 29| 1| func testRetryUnlimitedShouldRetryManyTimes() { 30| 1| let runLimit = 100 31| 1| var runCount = 0 32| 1| 33| 100| let job = TestJob(retry: .retry(delay: 0)) { 34| 100| runCount += 1 35| 100| if runCount == runLimit { 36| 1| $0.done(.success) 37| 100| } else { 38| 99| $0.done(.fail(JobError())) 39| 100| } 40| 100| } 41| 1| 42| 1| let type = UUID().uuidString 43| 1| 44| 1| let creator = TestCreator([type: job]) 45| 1| 46| 1| let manager = SwiftQueueManagerBuilder(creator: creator).set(persister: NoPersister.shared).build() 47| 1| JobBuilder(type: type) 48| 1| .retry(limit: .unlimited) 49| 1| .schedule(manager: manager) 50| 1| 51| 1| job.awaitForRemoval() 52| 1| job.assertRunCount(expected: runLimit) 53| 1| job.assertCompletedCount(expected: 1) 54| 1| job.assertRetriedCount(expected: runLimit - 1) 55| 1| job.assertCanceledCount(expected: 0) 56| 1| job.assertNoError() 57| 1| } 58| | 59| 1| func testRetryFailJobWithCancelConstraint() { 60| 1| let error = JobError() 61| 1| 62| 1| let (type, job) = (UUID().uuidString, TestJobFail(retry: .cancel, error: error)) 63| 1| 64| 1| let creator = TestCreator([type: job]) 65| 1| 66| 1| let manager = SwiftQueueManagerBuilder(creator: creator).set(persister: NoPersister.shared).build() 67| 1| JobBuilder(type: type) 68| 1| .retry(limit: .limited(2)) 69| 1| .schedule(manager: manager) 70| 1| 71| 1| job.awaitForRemoval() 72| 1| job.assertRunCount(expected: 1) 73| 1| job.assertCompletedCount(expected: 0) 74| 1| job.assertRetriedCount(expected: 1) 75| 1| job.assertCanceledCount(expected: 1) 76| 1| job.assertError(queueError: .onRetryCancel(error)) 77| 1| } 78| | 79| 1| func testRetryFailJobWithExponentialConstraint() { 80| 1| let job = TestJobFail(retry: .exponential(initial: 0)) 81| 1| let type = UUID().uuidString 82| 1| 83| 1| let creator = TestCreator([type: job]) 84| 1| 85| 1| let manager = SwiftQueueManagerBuilder(creator: creator).set(persister: NoPersister.shared).build() 86| 1| JobBuilder(type: type) 87| 1| .retry(limit: .limited(2)) 88| 1| .schedule(manager: manager) 89| 1| 90| 1| job.awaitForRemoval() 91| 1| job.assertRunCount(expected: 3) 92| 1| job.assertCompletedCount(expected: 0) 93| 1| job.assertRetriedCount(expected: 2) 94| 1| job.assertCanceledCount(expected: 1) 95| 1| job.assertError() 96| 1| } 97| | 98| 1| func testRetryFailJobWithExponentialMaxDelayConstraint() { 99| 1| let job = TestJobFail(retry: .exponentialWithLimit(initial: 0, maxDelay: 1)) 100| 1| let type = UUID().uuidString 101| 1| 102| 1| let creator = TestCreator([type: job]) 103| 1| 104| 1| let manager = SwiftQueueManagerBuilder(creator: creator).set(persister: NoPersister.shared).build() 105| 1| JobBuilder(type: type) 106| 1| .retry(limit: .limited(2)) 107| 1| .schedule(manager: manager) 108| 1| 109| 1| job.awaitForRemoval() 110| 1| job.assertRunCount(expected: 3) 111| 1| job.assertCompletedCount(expected: 0) 112| 1| job.assertRetriedCount(expected: 2) 113| 1| job.assertCanceledCount(expected: 1) 114| 1| job.assertError() 115| 1| } 116| | 117| 1| func testRepeatableJobWithExponentialBackoffRetry() { 118| 1| let type = UUID().uuidString 119| 1| let job = TestJobFail(retry: .exponential(initial: Double.leastNonzeroMagnitude)) 120| 1| 121| 1| let creator = TestCreator([type: job]) 122| 1| 123| 1| let manager = SwiftQueueManagerBuilder(creator: creator).set(persister: NoPersister.shared).build() 124| 1| JobBuilder(type: type) 125| 1| .retry(limit: .limited(1)) 126| 1| .periodic() 127| 1| .schedule(manager: manager) 128| 1| 129| 1| job.awaitForRemoval() 130| 1| job.assertRunCount(expected: 2) 131| 1| job.assertCompletedCount(expected: 0) 132| 1| job.assertRetriedCount(expected: 1) 133| 1| job.assertCanceledCount(expected: 1) 134| 1| job.assertError() 135| 1| } 136| | 137| 1| func testRetryFailJobWithRetryConstraint() { 138| 1| let (type, job) = (UUID().uuidString, TestJobFail(retry: .retry(delay: 0))) 139| 1| 140| 1| let creator = TestCreator([type: job]) 141| 1| 142| 1| let manager = SwiftQueueManagerBuilder(creator: creator).set(persister: NoPersister.shared).build() 143| 1| JobBuilder(type: type) 144| 1| .retry(limit: .limited(2)) 145| 1| .schedule(manager: manager) 146| 1| 147| 1| job.awaitForRemoval() 148| 1| job.assertRunCount(expected: 3) 149| 1| job.assertCompletedCount(expected: 0) 150| 1| job.assertRetriedCount(expected: 2) 151| 1| job.assertCanceledCount(expected: 1) 152| 1| job.assertError() 153| 1| } 154| | 155| 1| func testRetryFailJobWithRetryDelayConstraint() { 156| 1| let job = TestJobFail(retry: .retry(delay: Double.leastNonzeroMagnitude)) 157| 1| let type = UUID().uuidString 158| 1| 159| 1| let creator = TestCreator([type: job]) 160| 1| 161| 1| let manager = SwiftQueueManagerBuilder(creator: creator).set(persister: NoPersister.shared).build() 162| 1| JobBuilder(type: type) 163| 1| .retry(limit: .limited(2)) 164| 1| .schedule(manager: manager) 165| 1| 166| 1| job.awaitForRemoval() 167| 1| job.assertRunCount(expected: 3) 168| 1| job.assertCompletedCount(expected: 0) 169| 1| job.assertRetriedCount(expected: 2) 170| 1| job.assertCanceledCount(expected: 1) 171| 1| job.assertError() 172| 1| } 173| | 174| |} /Users/runner/work/SwiftQueue/SwiftQueue/Tests/SwiftQueueTests/ConstraintTest+Tag.swift: 1| |// The MIT License (MIT) 2| |// 3| |// Copyright (c) 2019 Lucas Nelaupe 4| |// 5| |// Permission is hereby granted, free of charge, to any person obtaining a copy 6| |// of this software and associated documentation files (the "Software"), to deal 7| |// in the Software without restriction, including without limitation the rights 8| |// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9| |// copies of the Software, and to permit persons to whom the Software is 10| |// furnished to do so, subject to the following conditions: 11| |// 12| |// The above copyright notice and this permission notice shall be included in all 13| |// copies or substantial portions of the Software. 14| |// 15| |// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16| |// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17| |// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18| |// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19| |// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20| |// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21| |// SOFTWARE. 22| | 23| |import Foundation 24| |import XCTest 25| |@testable import SwiftQueue 26| | 27| |class ConstraintTestTag: XCTestCase { 28| | 29| 1| func testCancelRunningOperationByTag() { 30| 1| let tag = UUID().uuidString 31| 1| 32| 1| var manager: SwiftQueueManager? 33| 1| 34| 1| let (type, job) = (UUID().uuidString, TestJob { 35| 1| manager?.cancelOperations(tag: tag) 36| 1| $0.done(.fail(JobError())) 37| 1| }) 38| 1| 39| 1| let creator = TestCreator([type: job]) 40| 1| 41| 1| manager = SwiftQueueManagerBuilder(creator: creator).set(persister: NoPersister.shared).build() 42| 1| 43| 1| manager?.enqueue(info: JobBuilder(type: type).addTag(tag: tag).build()) 44| 1| 45| 1| job.awaitForRemoval() 46| 1| job.assertRunCount(expected: 1) 47| 1| job.assertCompletedCount(expected: 0) 48| 1| job.assertRetriedCount(expected: 0) 49| 1| job.assertCanceledCount(expected: 1) 50| 1| job.assertError(queueError: .canceled) 51| 1| } 52| | 53| |} /Users/runner/work/SwiftQueue/SwiftQueue/Tests/SwiftQueueTests/ConstraintTest+Timeout.swift: 1| |// The MIT License (MIT) 2| |// 3| |// Copyright (c) 2019 Lucas Nelaupe 4| |// 5| |// Permission is hereby granted, free of charge, to any person obtaining a copy 6| |// of this software and associated documentation files (the "Software"), to deal 7| |// in the Software without restriction, including without limitation the rights 8| |// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9| |// copies of the Software, and to permit persons to whom the Software is 10| |// furnished to do so, subject to the following conditions: 11| |// 12| |// The above copyright notice and this permission notice shall be included in all 13| |// copies or substantial portions of the Software. 14| |// 15| |// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16| |// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17| |// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18| |// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19| |// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20| |// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21| |// SOFTWARE. 22| | 23| |import Foundation 24| |import XCTest 25| |@testable import SwiftQueue 26| | 27| |class ConstraintTestTimeout: XCTestCase { 28| | 29| 1| func testRunTimeoutConstraint() { 30| 1| let (type, job) = (UUID().uuidString, TestJob(onRunCallback: { _ in }) ) 31| 1| 32| 1| let creator = TestCreator([type: job]) 33| 1| 34| 1| let manager = SwiftQueueManagerBuilder(creator: creator).set(persister: NoPersister.shared).build() 35| 1| 36| 1| JobBuilder(type: type) 37| 1| .timeout(value: 0) 38| 1| .schedule(manager: manager) 39| 1| 40| 1| job.awaitForRemoval() 41| 1| job.assertRunCount(expected: 1) 42| 1| job.assertCompletedCount(expected: 0) 43| 1| job.assertRetriedCount(expected: 0) 44| 1| job.assertCanceledCount(expected: 1) 45| 1| job.assertError(queueError: .timeout) 46| 1| } 47| | 48| |} /Users/runner/work/SwiftQueue/SwiftQueue/Tests/SwiftQueueTests/ConstraintTest+UniqueUUID.swift: 1| |// The MIT License (MIT) 2| |// 3| |// Copyright (c) 2019 Lucas Nelaupe 4| |// 5| |// Permission is hereby granted, free of charge, to any person obtaining a copy 6| |// of this software and associated documentation files (the "Software"), to deal 7| |// in the Software without restriction, including without limitation the rights 8| |// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9| |// copies of the Software, and to permit persons to whom the Software is 10| |// furnished to do so, subject to the following conditions: 11| |// 12| |// The above copyright notice and this permission notice shall be included in all 13| |// copies or substantial portions of the Software. 14| |// 15| |// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16| |// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17| |// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18| |// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19| |// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20| |// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21| |// SOFTWARE. 22| | 23| |import Foundation 24| |import XCTest 25| |@testable import SwiftQueue 26| | 27| |class ConstraintTestUniqueUUID: XCTestCase { 28| | 29| 1| func testUniqueIdConstraintShouldCancelTheSecond() { 30| 1| let (type1, job1) = (UUID().uuidString, TestJob()) 31| 1| let (type2, job2) = (UUID().uuidString, TestJob()) 32| 1| 33| 1| let id = UUID().uuidString 34| 1| 35| 1| let creator = TestCreator([type1: job1, type2: job2]) 36| 1| 37| 1| let manager = SwiftQueueManagerBuilder(creator: creator).set(persister: NoPersister.shared).build() 38| 1| JobBuilder(type: type1) 39| 1| .singleInstance(forId: id) 40| 1| .delay(time: 3600) 41| 1| .schedule(manager: manager) 42| 1| 43| 1| JobBuilder(type: type2).singleInstance(forId: id).schedule(manager: manager) 44| 1| 45| 1| job2.awaitForRemoval() 46| 1| job2.assertRemovedBeforeRun(reason: .duplicate) 47| 1| 48| 1| manager.cancelAllOperations() 49| 1| manager.waitUntilAllOperationsAreFinished() 50| 1| } 51| | 52| 1| func testUniqueIdConstraintShouldCancelTheFirst() { 53| 1| let (type1, job1) = (UUID().uuidString, TestJob()) 54| 1| let (type2, job2) = (UUID().uuidString, TestJob()) 55| 1| 56| 1| let id = UUID().uuidString 57| 1| 58| 1| let creator = TestCreator([type1: job1, type2: job2]) 59| 1| 60| 1| let manager = SwiftQueueManagerBuilder(creator: creator).set(persister: NoPersister.shared).build() 61| 1| JobBuilder(type: type1) 62| 1| .singleInstance(forId: id) 63| 1| .delay(time: 3600) 64| 1| .schedule(manager: manager) 65| 1| 66| 1| JobBuilder(type: type2) 67| 1| .singleInstance(forId: id, override: true) 68| 1| .schedule(manager: manager) 69| 1| 70| 1| job1.awaitForRemoval() 71| 1| job1.assertRemovedBeforeRun(reason: .canceled) 72| 1| 73| 1| job2.awaitForRemoval() 74| 1| job2.assertSingleCompletion() 75| 1| 76| 1| manager.cancelAllOperations() 77| 1| manager.waitUntilAllOperationsAreFinished() 78| 1| } 79| | 80| |} /Users/runner/work/SwiftQueue/SwiftQueue/Tests/SwiftQueueTests/LoggerTests.swift: 1| |// The MIT License (MIT) 2| |// 3| |// Copyright (c) 2019 Lucas Nelaupe 4| |// 5| |// Permission is hereby granted, free of charge, to any person obtaining a copy 6| |// of this software and associated documentation files (the "Software"), to deal 7| |// in the Software without restriction, including without limitation the rights 8| |// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9| |// copies of the Software, and to permit persons to whom the Software is 10| |// furnished to do so, subject to the following conditions: 11| |// 12| |// The above copyright notice and this permission notice shall be included in all 13| |// copies or substantial portions of the Software. 14| |// 15| |// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16| |// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17| |// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18| |// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19| |// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20| |// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21| |// SOFTWARE. 22| | 23| |import Foundation 24| |import XCTest 25| |import Dispatch 26| |@testable import SwiftQueue 27| | 28| |class LoggerTests: XCTestCase { 29| | 30| 1| func testRunSuccessJobLogger() { 31| 1| let id = UUID().uuidString 32| 1| 33| 1| let debugLogger = DebugLogger() 34| 1| 35| 1| let (type, job) = (UUID().uuidString, TestJob()) 36| 1| 37| 1| let creator = TestCreator([type: job]) 38| 1| 39| 1| let manager = SwiftQueueManagerBuilder(creator: creator).set(persister: NoPersister.shared).set(logger: debugLogger).build() 40| 1| JobBuilder(type: type) 41| 1| .singleInstance(forId: id) 42| 1| .schedule(manager: manager) 43| 1| 44| 1| job.awaitForRemoval() 45| 1| job.assertSingleCompletion() 46| 1| 47| 1| let outputs = debugLogger.outputs 48| 1| 49| 1| XCTAssertEqual(5, outputs.count) 50| 1| XCTAssertEqual(outputs[0], "[SwiftQueue] level=verbose jobId=\(id) message=Job has been started by the system") 51| 1| XCTAssertEqual(outputs[1], "[SwiftQueue] level=verbose jobId=\(id) message=Job is running") 52| 1| XCTAssertEqual(outputs[2], "[SwiftQueue] level=verbose jobId=\(id) message=Job completed successfully") 53| 1| XCTAssertEqual(outputs[3], "[SwiftQueue] level=verbose jobId=\(id) message=Job will not run anymore") 54| 1| XCTAssertEqual(outputs[4], "[SwiftQueue] level=verbose jobId=\(id) message=Job is removed from the queue result=success") 55| 1| } 56| | 57| 1| func testLoggerLevel() { 58| 1| 59| 1| let verbose = DebugLogger(min: .verbose) 60| 1| let warning = DebugLogger(min: .warning) 61| 1| let error = DebugLogger(min: .error) 62| 1| 63| 1| let verbose1 = UUID().uuidString 64| 1| let verbose2 = UUID().uuidString 65| 1| 66| 1| verbose.log(.verbose, jobId: verbose1, message: verbose2) 67| 1| warning.log(.verbose, jobId: verbose1, message: verbose2) 68| 1| error.log(.verbose, jobId: verbose1, message: verbose2) 69| 1| 70| 1| let warning1 = UUID().uuidString 71| 1| let warning2 = UUID().uuidString 72| 1| 73| 1| verbose.log(.warning, jobId: warning1, message: warning2) 74| 1| warning.log(.warning, jobId: warning1, message: warning2) 75| 1| error.log(.warning, jobId: warning1, message: warning2) 76| 1| 77| 1| let error1 = UUID().uuidString 78| 1| let error2 = UUID().uuidString 79| 1| 80| 1| verbose.log(.error, jobId: error1, message: error2) 81| 1| warning.log(.error, jobId: error1, message: error2) 82| 1| error.log(.error, jobId: error1, message: error2) 83| 1| 84| 1| XCTAssertEqual(3, verbose.outputs.count) 85| 1| XCTAssertEqual(2, warning.outputs.count) 86| 1| XCTAssertEqual(1, error.outputs.count) 87| 1| 88| 1| XCTAssertEqual(verbose.outputs[0], "[SwiftQueue] level=\(LogLevel.verbose.description) jobId=\(verbose1) message=\(verbose2)") 89| 1| XCTAssertEqual(verbose.outputs[1], "[SwiftQueue] level=\(LogLevel.warning.description) jobId=\(warning1) message=\(warning2)") 90| 1| XCTAssertEqual(verbose.outputs[2], "[SwiftQueue] level=\(LogLevel.error.description) jobId=\(error1) message=\(error2)") 91| 1| 92| 1| XCTAssertEqual(warning.outputs[0], "[SwiftQueue] level=\(LogLevel.warning.description) jobId=\(warning1) message=\(warning2)") 93| 1| XCTAssertEqual(warning.outputs[1], "[SwiftQueue] level=\(LogLevel.error.description) jobId=\(error1) message=\(error2)") 94| 1| 95| 1| XCTAssertEqual(error.outputs[0], "[SwiftQueue] level=\(LogLevel.error.description) jobId=\(error1) message=\(error2)") 96| 1| } 97| | 98| |} 99| | 100| |class DebugLogger: ConsoleLogger { 101| | 102| 4| var outputs = [String]() 103| | 104| 11| override func printComputed(output: String) { 105| 11| outputs.append(output) 106| 11| } 107| | 108| |} /Users/runner/work/SwiftQueue/SwiftQueue/Tests/SwiftQueueTests/PersisterTests.swift: 1| |// The MIT License (MIT) 2| |// 3| |// Copyright (c) 2019 Lucas Nelaupe 4| |// 5| |// Permission is hereby granted, free of charge, to any person obtaining a copy 6| |// of this software and associated documentation files (the "Software"), to deal 7| |// in the Software without restriction, including without limitation the rights 8| |// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9| |// copies of the Software, and to permit persons to whom the Software is 10| |// furnished to do so, subject to the following conditions: 11| |// 12| |// The above copyright notice and this permission notice shall be included in all 13| |// copies or substantial portions of the Software. 14| |// 15| |// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16| |// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17| |// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18| |// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19| |// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20| |// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21| |// SOFTWARE. 22| | 23| |import Foundation 24| |import XCTest 25| |import Dispatch 26| |@testable import SwiftQueue 27| | 28| |class PersisterTests: XCTestCase { 29| | 30| 1| func testLoadSerializedSortedJobShouldRunSuccess() { 31| 1| let (type1, job1, job1Id) = (UUID().uuidString, TestJob(), UUID().uuidString) 32| 1| let (type2, job2, job2Id) = (UUID().uuidString, TestJob(), UUID().uuidString) 33| 1| 34| 1| let queueId = UUID().uuidString 35| 1| 36| 1| let creator = TestCreator([type1: job1, type2: job2]) 37| 1| 38| 1| let task1 = JobBuilder(type: type1) 39| 1| .singleInstance(forId: job1Id) 40| 1| .parallel(queueName: queueId) 41| 1| .build(job: job1) 42| 1| .toJSONStringSafe() 43| 1| 44| 1| let task2 = JobBuilder(type: type2) 45| 1| .singleInstance(forId: job2Id) 46| 1| .parallel(queueName: queueId) 47| 1| .build(job: job2) 48| 1| .toJSONStringSafe() 49| 1| 50| 1| // Should invert when deserialize 51| 1| let persister = PersisterTracker(key: UUID().uuidString) 52| 1| persister.put(queueName: queueId, taskId: job2Id, data: task2) 53| 1| 54| 1| let restore = persister.restore() 55| 1| XCTAssertEqual(restore.count, 1) 56| 1| XCTAssertEqual(restore[0], queueId) 57| 1| 58| 1| persister.put(queueName: queueId, taskId: job1Id, data: task1) 59| 1| 60| 1| let restore2 = persister.restore() 61| 1| XCTAssertEqual(restore2.count, 1) 62| 1| XCTAssertEqual(restore2[0], queueId) 63| 1| 64| 1| let manager = SwiftQueueManagerBuilder(creator: creator).set(persister: persister).build() 65| 1| 66| 1| XCTAssertEqual(queueId, persister.restoreQueueName) 67| 1| 68| 1| job1.awaitForRemoval() 69| 1| job1.assertSingleCompletion() 70| 1| 71| 1| job2.awaitForRemoval() 72| 1| job2.assertSingleCompletion() 73| 1| 74| 1| manager.waitUntilAllOperationsAreFinished() 75| 1| } 76| | 77| 1| func testCancelAllShouldRemoveFromPersister() { 78| 1| let (type1, job1, job1Id) = (UUID().uuidString, TestJob(), UUID().uuidString) 79| 1| let (type2, job2, job2Id) = (UUID().uuidString, TestJob(), UUID().uuidString) 80| 1| 81| 1| let group = UUID().uuidString 82| 1| 83| 1| let creator = TestCreator([type1: job1, type2: job2]) 84| 1| 85| 1| let persister = PersisterTracker(key: UUID().uuidString) 86| 1| 87| 1| let manager = SwiftQueueManagerBuilder(creator: creator).set(persister: persister).build() 88| 1| 89| 1| JobBuilder(type: type1) 90| 1| .singleInstance(forId: job1Id) 91| 1| .parallel(queueName: group) 92| 1| .delay(time: 3600) 93| 1| .persist() 94| 1| .schedule(manager: manager) 95| 1| 96| 1| JobBuilder(type: type2) 97| 1| .singleInstance(forId: job2Id) 98| 1| .parallel(queueName: group) 99| 1| .delay(time: 3600) 100| 1| .persist() 101| 1| .schedule(manager: manager) 102| 1| 103| 1| manager.cancelAllOperations() 104| 1| 105| 1| job1.awaitForRemoval() 106| 1| job2.awaitForRemoval() 107| 1| 108| 1| job1.assertRemovedBeforeRun(reason: .canceled) 109| 1| job2.assertRemovedBeforeRun(reason: .canceled) 110| 1| 111| 1| XCTAssertEqual([job1Id, job2Id], persister.removeJobUUID) 112| 1| XCTAssertEqual([group, group], persister.removeQueueName) 113| 1| } 114| | 115| 1| func testCompleteJobRemoveFromSerializer() { 116| 1| let (type, job) = (UUID().uuidString, TestJob()) 117| 1| 118| 1| let queueId = UUID().uuidString 119| 1| let taskID = UUID().uuidString 120| 1| 121| 1| let creator = TestCreator([type: job]) 122| 1| let persister = PersisterTracker(key: UUID().uuidString) 123| 1| 124| 1| let manager = SwiftQueueManagerBuilder(creator: creator).set(persister: persister).build() 125| 1| JobBuilder(type: type) 126| 1| .singleInstance(forId: taskID) 127| 1| .parallel(queueName: queueId) 128| 1| .persist() 129| 1| .schedule(manager: manager) 130| 1| 131| 1| job.awaitForRemoval() 132| 1| job.assertSingleCompletion() 133| 1| 134| 1| XCTAssertEqual([taskID], persister.removeJobUUID) 135| 1| XCTAssertEqual([queueId], persister.removeQueueName) 136| 1| } 137| | 138| 1| func testCompleteFailTaskRemoveFromSerializer() { 139| 1| let queueId = UUID().uuidString 140| 1| 141| 1| let job = TestJobFail() 142| 1| let type = UUID().uuidString 143| 1| 144| 1| let creator = TestCreator([type: job]) 145| 1| 146| 1| let taskID = UUID().uuidString 147| 1| 148| 1| let persister = PersisterTracker(key: UUID().uuidString) 149| 1| 150| 1| let manager = SwiftQueueManagerBuilder(creator: creator).set(persister: persister).build() 151| 1| JobBuilder(type: type) 152| 1| .singleInstance(forId: taskID) 153| 1| .parallel(queueName: queueId) 154| 1| .persist() 155| 1| .schedule(manager: manager) 156| 1| 157| 1| job.awaitForRemoval() 158| 1| job.assertRunCount(expected: 1) 159| 1| job.assertCompletedCount(expected: 0) 160| 1| job.assertRetriedCount(expected: 0) 161| 1| job.assertCanceledCount(expected: 1) 162| 1| job.assertError() 163| 1| 164| 1| XCTAssertEqual([taskID], persister.removeJobUUID) 165| 1| XCTAssertEqual([queueId], persister.removeQueueName) 166| 1| } 167| | 168| 1| func testNonPersistedJobShouldNotBePersisted() { 169| 1| let (type, job) = (UUID().uuidString, TestJob()) 170| 1| 171| 1| let creator = TestCreator([type: job]) 172| 1| let persister = PersisterTracker(key: UUID().uuidString) 173| 1| 174| 1| let manager = SwiftQueueManagerBuilder(creator: creator).set(persister: persister).build() 175| 1| JobBuilder(type: type) 176| 1| .schedule(manager: manager) 177| 1| 178| 1| job.awaitForRemoval() 179| 1| 180| 1| job.assertSingleCompletion() 181| 1| 182| 1| XCTAssertEqual(0, persister.putQueueName.count) 183| 1| XCTAssertEqual(0, persister.putJobUUID.count) 184| 1| XCTAssertEqual(0, persister.putData.count) 185| 1| XCTAssertEqual(0, persister.removeQueueName.count) 186| 1| XCTAssertEqual(0, persister.removeJobUUID.count) 187| 1| } 188| | 189| 1| func testCancelWithTagShouldRemoveFromPersister() { 190| 1| let (type, job) = (UUID().uuidString, TestJob()) 191| 1| 192| 1| let id = UUID().uuidString 193| 1| let tag = UUID().uuidString 194| 1| let group = UUID().uuidString 195| 1| 196| 1| let creator = TestCreator([type: job]) 197| 1| 198| 1| let persister = PersisterTracker(key: UUID().uuidString) 199| 1| 200| 1| let manager = SwiftQueueManagerBuilder(creator: creator).set(persister: persister).build() 201| 1| 202| 1| JobBuilder(type: type) 203| 1| .singleInstance(forId: id) 204| 1| .parallel(queueName: group) 205| 1| .delay(time: 3600) 206| 1| .addTag(tag: tag) 207| 1| .persist() 208| 1| .schedule(manager: manager) 209| 1| 210| 1| manager.cancelOperations(tag: tag) 211| 1| 212| 1| job.awaitForRemoval() 213| 1| job.assertRemovedBeforeRun(reason: .canceled) 214| 1| 215| 1| XCTAssertEqual([id], persister.removeJobUUID) 216| 1| XCTAssertEqual([group], persister.removeQueueName) 217| 1| } 218| | 219| 1| func testCustomSerializer() { 220| 1| let (type1, job1) = (UUID().uuidString, TestJob()) 221| 1| 222| 1| let persister = PersisterTracker(key: UUID().uuidString) 223| 1| let serializer = MemorySerializer() 224| 1| 225| 1| let manager = SwiftQueueManagerBuilder(creator: TestCreator([type1: job1])) 226| 1| .set(persister: persister) 227| 1| .set(serializer: serializer) 228| 1| .set(isSuspended: true) 229| 1| .build() 230| 1| 231| 1| JobBuilder(type: type1) 232| 1| .parallel(queueName: UUID().uuidString) 233| 1| .persist() 234| 1| .schedule(manager: manager) 235| 1| 236| 1| // at this point the job should have been serialised 237| 1| job1.assertNoRun() 238| 1| 239| 1| // Re-create manager 240| 1| let manager2 = SwiftQueueManagerBuilder(creator: TestCreator([type1: job1])) 241| 1| .set(persister: persister) 242| 1| .set(serializer: serializer) 243| 1| .set(isSuspended: false) 244| 1| .build() 245| 1| 246| 1| manager2.waitUntilAllOperationsAreFinished() 247| 1| 248| 1| job1.awaitForRemoval() 249| 1| job1.assertSingleCompletion() 250| 1| } 251| | 252| 1| func testRemoveAllJob() { 253| 1| let persister = PersisterTracker(key: UUID().uuidString) 254| 1| 255| 1| // Nothing to assert since we don't rely on the actual one in test cases 256| 1| persister.clearAll() 257| 1| } 258| | 259| |} /Users/runner/work/SwiftQueue/SwiftQueue/Tests/SwiftQueueTests/SqOperationTest.swift: 1| |// The MIT License (MIT) 2| |// 3| |// Copyright (c) 2019 Lucas Nelaupe 4| |// 5| |// Permission is hereby granted, free of charge, to any person obtaining a copy 6| |// of this software and associated documentation files (the "Software"), to deal 7| |// in the Software without restriction, including without limitation the rights 8| |// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9| |// copies of the Software, and to permit persons to whom the Software is 10| |// furnished to do so, subject to the following conditions: 11| |// 12| |// The above copyright notice and this permission notice shall be included in all 13| |// copies or substantial portions of the Software. 14| |// 15| |// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16| |// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17| |// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18| |// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19| |// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20| |// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21| |// SOFTWARE. 22| | 23| |import Foundation 24| | 25| |import Foundation 26| |import XCTest 27| |@testable import SwiftQueue 28| | 29| |class SqOperationTest: XCTestCase { 30| | 31| 1| func testQueuePriority() { 32| 1| let priorities = [ 33| 1| Operation.QueuePriority.veryLow, 34| 1| Operation.QueuePriority.low, 35| 1| Operation.QueuePriority.normal, 36| 1| Operation.QueuePriority.high, 37| 1| Operation.QueuePriority.veryHigh 38| 1| ] 39| 1| 40| 5| for priority in priorities { 41| 5| var jobInfo = JobInfo.init(type: "") 42| 5| jobInfo.priority = priority 43| 5| let operation = SqOperation(TestJob(), jobInfo, NoLogger.shared, nil, .main, []) 44| 5| 45| 5| XCTAssertEqual(priority, operation.queuePriority) 46| 5| } 47| 1| } 48| | 49| 1| func testQualityOfService() { 50| 1| let services = [ 51| 1| QualityOfService.userInteractive, 52| 1| QualityOfService.userInitiated, 53| 1| QualityOfService.utility, 54| 1| QualityOfService.background, 55| 1| QualityOfService.default 56| 1| ] 57| 1| 58| 5| for service in services { 59| 5| var jobInfo = JobInfo.init(type: "") 60| 5| jobInfo.qualityOfService = service 61| 5| let operation = SqOperation(TestJob(), jobInfo, NoLogger.shared, nil, .main, []) 62| 5| 63| 5| XCTAssertEqual(service, operation.qualityOfService) 64| 5| } 65| 1| } 66| | 67| |} /Users/runner/work/SwiftQueue/SwiftQueue/Tests/SwiftQueueTests/StartStopTests.swift: 1| |// The MIT License (MIT) 2| |// 3| |// Copyright (c) 2019 Lucas Nelaupe 4| |// 5| |// Permission is hereby granted, free of charge, to any person obtaining a copy 6| |// of this software and associated documentation files (the "Software"), to deal 7| |// in the Software without restriction, including without limitation the rights 8| |// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9| |// copies of the Software, and to permit persons to whom the Software is 10| |// furnished to do so, subject to the following conditions: 11| |// 12| |// The above copyright notice and this permission notice shall be included in all 13| |// copies or substantial portions of the Software. 14| |// 15| |// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16| |// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17| |// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18| |// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19| |// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20| |// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21| |// SOFTWARE. 22| | 23| |import XCTest 24| |@testable import SwiftQueue 25| | 26| |class StartStopTests: XCTestCase { 27| | 28| 1| func testScheduleWhenQueueStop() { 29| 1| let (type, job) = (UUID().uuidString, TestJob()) 30| 1| 31| 1| let creator = TestCreator([type: job]) 32| 1| 33| 1| let manager = SwiftQueueManagerBuilder(creator: creator) 34| 1| .set(persister: NoPersister.shared) 35| 1| .set(isSuspended: true) 36| 1| .build() 37| 1| 38| 1| JobBuilder(type: type).schedule(manager: manager) 39| 1| 40| 1| // No run 41| 1| job.assertNoRun() 42| 1| 43| 1| manager.isSuspended = false 44| 1| 45| 1| job.awaitForRemoval() 46| 1| job.assertSingleCompletion() 47| 1| } 48| | 49| 1| func testSchedulePeriodicJobThenStart() { 50| 1| let (type, job) = (UUID().uuidString, TestJob()) 51| 1| 52| 1| let creator = TestCreator([type: job]) 53| 1| 54| 1| let manager = SwiftQueueManagerBuilder(creator: creator).set(persister: NoPersister.shared).build() 55| 1| 56| 1| manager.isSuspended = true 57| 1| 58| 1| JobBuilder(type: type).periodic(limit: .limited(4), interval: 0).schedule(manager: manager) 59| 1| 60| 1| // No run 61| 1| job.assertNoRun() 62| 1| 63| 1| manager.isSuspended = false 64| 1| manager.isSuspended = false 65| 1| manager.isSuspended = false 66| 1| manager.isSuspended = false 67| 1| manager.isSuspended = false 68| 1| manager.isSuspended = false 69| 1| manager.isSuspended = false 70| 1| 71| 1| job.awaitForRemoval() 72| 1| 73| 1| job.assertRunCount(expected: 4) 74| 1| job.assertCompletedCount(expected: 1) 75| 1| job.assertRetriedCount(expected: 0) 76| 1| job.assertCanceledCount(expected: 0) 77| 1| job.assertNoError() 78| 1| } 79| | 80| |} /Users/runner/work/SwiftQueue/SwiftQueue/Tests/SwiftQueueTests/SwiftQueueBuilderTests.swift: 1| |// The MIT License (MIT) 2| |// 3| |// Copyright (c) 2019 Lucas Nelaupe 4| |// 5| |// Permission is hereby granted, free of charge, to any person obtaining a copy 6| |// of this software and associated documentation files (the "Software"), to deal 7| |// in the Software without restriction, including without limitation the rights 8| |// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9| |// copies of the Software, and to permit persons to whom the Software is 10| |// furnished to do so, subject to the following conditions: 11| |// 12| |// The above copyright notice and this permission notice shall be included in all 13| |// copies or substantial portions of the Software. 14| |// 15| |// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16| |// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17| |// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18| |// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19| |// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20| |// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21| |// SOFTWARE. 22| | 23| |import Foundation 24| | 25| |import XCTest 26| |import Dispatch 27| |@testable import SwiftQueue 28| | 29| |class SwiftQueueBuilderTests: XCTestCase { 30| | 31| 1| public func testBuilderJobType() throws { 32| 1| let type = UUID().uuidString 33| 1| 34| 1| let jobInfo = JobBuilder(type: type).info 35| 1| XCTAssertEqual(jobInfo.type, type) 36| 1| 37| 1| } 38| | 39| 1| public func testBuilderSingleInstance() throws { 40| 1| let type = UUID().uuidString 41| 1| let uuid = UUID().uuidString 42| 1| 43| 1| let jobInfo = JobBuilder(type: type).singleInstance(forId: uuid).info 44| 1| 45| 1| let constraint: UniqueUUIDConstraint? = getConstraint(jobInfo) 46| 1| XCTAssertNotNil(constraint) 47| 1| XCTAssertEqual(constraint?.uuid, uuid) 48| 1| XCTAssertFalse(constraint?.override ?? false) 49| 1| } 50| | 51| 1| public func testBuilderSingleInstanceOverride() throws { 52| 1| let type = UUID().uuidString 53| 1| let uuid = UUID().uuidString 54| 1| 55| 1| let jobInfo = JobBuilder(type: type).singleInstance(forId: uuid, override: true).info 56| 1| 57| 1| let constraint: UniqueUUIDConstraint? = getConstraint(jobInfo) 58| 1| XCTAssertNotNil(constraint) 59| 1| XCTAssertEqual(constraint?.uuid, uuid) 60| 1| XCTAssertTrue(constraint?.override ?? false) 61| 1| } 62| | 63| 1| public func testBuilderGroup() throws { 64| 1| let type = UUID().uuidString 65| 1| let groupName = UUID().uuidString 66| 1| 67| 1| let jobInfo = JobBuilder(type: type).parallel(queueName: groupName).info 68| 1| XCTAssertEqual(jobInfo.queueName, groupName) 69| 1| } 70| | 71| 1| public func testBuilderDelay() throws { 72| 1| let type = UUID().uuidString 73| 1| let delay: Double = 1234 74| 1| 75| 1| let jobInfo = JobBuilder(type: type).delay(time: delay).info 76| 1| let constraint: DelayConstraint? = getConstraint(jobInfo) 77| 1| XCTAssertNotNil(constraint) 78| 1| XCTAssertEqual(constraint?.delay, delay) 79| 1| } 80| | 81| 1| public func testBuilderDeadlineCodable() throws { 82| 1| let type = UUID().uuidString 83| 1| let deadline = Date(timeIntervalSinceNow: TimeInterval(30)) 84| 1| let jobInfo = JobBuilder(type: type).deadline(date: deadline).info 85| 1| 86| 1| let constraint: DeadlineConstraint? = getConstraint(jobInfo) 87| 1| XCTAssertNotNil(constraint) 88| 1| XCTAssertEqual(constraint?.deadline, deadline) 89| 1| } 90| | 91| 1| public func testBuilderPeriodicUnlimited() throws { 92| 1| let type = UUID().uuidString 93| 1| let interval: Double = 12341 94| 1| let executor = Executor.foreground 95| 1| 96| 1| let jobInfo = JobBuilder(type: type).periodic(limit: .unlimited, interval: interval).info 97| 1| 98| 1| let constraint: RepeatConstraint? = getConstraint(jobInfo) 99| 1| XCTAssertNotNil(constraint) 100| 1| 101| 1| XCTAssertEqual(constraint?.maxRun, Limit.unlimited) 102| 1| XCTAssertEqual(constraint?.interval, interval) 103| 1| XCTAssertEqual(constraint?.executor, executor) 104| 1| } 105| | 106| 1| public func testBuilderInternetCellular() throws { 107| 1| let type = UUID().uuidString 108| 1| let network: NetworkType = .cellular 109| 1| 110| 1| let jobInfo = JobBuilder(type: type).internet(atLeast: network).info 111| 1| 112| 1| let constraint: NetworkConstraint? = getConstraint(jobInfo) 113| 1| XCTAssertNotNil(constraint) 114| 1| XCTAssertEqual(constraint?.networkType, NetworkType.cellular) 115| 1| } 116| | 117| 1| public func testBuilderInternetWifi() throws { 118| 1| let type = UUID().uuidString 119| 1| let network: NetworkType = .wifi 120| 1| 121| 1| let jobInfo = JobBuilder(type: type).internet(atLeast: network).info 122| 1| 123| 1| let constraint: NetworkConstraint? = getConstraint(jobInfo) 124| 1| XCTAssertNotNil(constraint) 125| 1| XCTAssertEqual(constraint?.networkType, NetworkType.wifi) 126| 1| } 127| | 128| 1| public func testBuilderRetryUnlimited() throws { 129| 1| let type = UUID().uuidString 130| 1| 131| 1| let jobInfo = JobBuilder(type: type).retry(limit: .unlimited).info 132| 1| 133| 1| let constraint: JobRetryConstraint? = getConstraint(jobInfo) 134| 1| XCTAssertNotNil(constraint) 135| 1| XCTAssertEqual(constraint?.limit, Limit.unlimited) 136| 1| } 137| | 138| 1| public func testBuilderRetryLimited() throws { 139| 1| let type = UUID().uuidString 140| 1| let limited: Double = 123 141| 1| 142| 1| let jobInfo = JobBuilder(type: type).retry(limit: .limited(limited)).info 143| 1| 144| 1| let constraint: JobRetryConstraint? = getConstraint(jobInfo) 145| 1| XCTAssertNotNil(constraint) 146| 1| XCTAssertEqual(constraint?.limit, Limit.limited(limited)) 147| 1| } 148| | 149| 1| public func testBuilderAddTag() throws { 150| 1| let type = UUID().uuidString 151| 1| let tag1 = UUID().uuidString 152| 1| let tag2 = UUID().uuidString 153| 1| 154| 1| let jobInfo = JobBuilder(type: type).addTag(tag: tag1).addTag(tag: tag2).info 155| 1| 156| 1| let constraint: TagConstraint? = getConstraint(jobInfo) 157| 1| XCTAssertNotNil(constraint) 158| 1| XCTAssertEqual(constraint?.tags.contains(tag1), true) 159| 1| XCTAssertEqual(constraint?.tags.contains(tag2), true) 160| 1| } 161| | 162| 1| public func testBuilderWithFreeArgs() { 163| 1| let type = UUID().uuidString 164| 1| let params: [String: Any] = [UUID().uuidString: [UUID().uuidString: self]] 165| 1| 166| 1| let creator = TestCreator([type: TestJob()]) 167| 1| let manager = SwiftQueueManagerBuilder(creator: creator) 168| 1| .set(persister: NoPersister.shared) 169| 1| .build() 170| 1| 171| 1| // No assert expected 172| 1| // This is just to test if the serialization failed on self 173| 1| JobBuilder(type: type).with(params: params).schedule(manager: manager) 174| 1| } 175| | 176| 1| public func testBuilderRequireCharging() throws { 177| 1| let type = UUID().uuidString 178| 1| 179| 1| let jobInfo = JobBuilder(type: type).requireCharging().info 180| 1| 181| 1| let constraint: BatteryChargingConstraint? = getConstraint(jobInfo) 182| 1| XCTAssertNotNil(constraint) 183| 1| } 184| | 185| 1| func testCopyBuilder() { 186| 1| var origin = JobBuilder(type: UUID().uuidString) 187| 1| let builder = origin.copy() 188| 1| 189| 1| origin = origin.internet(atLeast: .wifi) 190| 1| 191| 1| let constraint: NetworkConstraint? = getConstraint(builder.info) 192| 1| XCTAssertNil(constraint) 193| 1| } 194| | 195| |} /Users/runner/work/SwiftQueue/SwiftQueue/Tests/SwiftQueueTests/SwiftQueueManagerTests.swift: 1| |// The MIT License (MIT) 2| |// 3| |// Copyright (c) 2019 Lucas Nelaupe 4| |// 5| |// Permission is hereby granted, free of charge, to any person obtaining a copy 6| |// of this software and associated documentation files (the "Software"), to deal 7| |// in the Software without restriction, including without limitation the rights 8| |// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9| |// copies of the Software, and to permit persons to whom the Software is 10| |// furnished to do so, subject to the following conditions: 11| |// 12| |// The above copyright notice and this permission notice shall be included in all 13| |// copies or substantial portions of the Software. 14| |// 15| |// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16| |// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17| |// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18| |// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19| |// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20| |// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21| |// SOFTWARE. 22| | 23| |import XCTest 24| |import Dispatch 25| |@testable import SwiftQueue 26| | 27| |class SwiftQueueManagerTests: XCTestCase { 28| | 29| 1| func testRunSuccessJob() { 30| 1| let (type, job) = (UUID().uuidString, TestJob()) 31| 1| 32| 1| let creator = TestCreator([type: job]) 33| 1| 34| 1| let manager = SwiftQueueManagerBuilder(creator: creator).set(persister: NoPersister.shared).build() 35| 1| JobBuilder(type: type) 36| 1| .priority(priority: .veryHigh) 37| 1| .service(quality: .background) 38| 1| .schedule(manager: manager) 39| 1| 40| 1| job.awaitForRemoval() 41| 1| job.assertSingleCompletion() 42| 1| } 43| | 44| 1| func testJobListener() { 45| 1| let (type, job) = (UUID().uuidString, TestJob()) 46| 1| 47| 1| let creator = TestCreator([type: job]) 48| 1| let listener = JobListenerTest() 49| 1| 50| 1| let manager = SwiftQueueManagerBuilder(creator: creator) 51| 1| .set(persister: NoPersister.shared) 52| 1| .set(isSuspended: true) 53| 1| .set(listener: listener) 54| 1| .build() 55| 1| 56| 1| JobBuilder(type: type).schedule(manager: manager) 57| 1| 58| 1| // No run 59| 1| job.assertNoRun() 60| 1| XCTAssertEqual(0, listener.onBeforeRun.count) 61| 1| XCTAssertEqual(0, listener.onAfterRun.count) 62| 1| XCTAssertEqual(0, listener.onTerminated.count) 63| 1| 64| 1| manager.isSuspended = false 65| 1| 66| 1| job.awaitForRemoval() 67| 1| job.assertSingleCompletion() 68| 1| 69| 1| XCTAssertEqual(1, listener.onBeforeRun.count) 70| 1| XCTAssertEqual(1, listener.onAfterRun.count) 71| 1| XCTAssertEqual(1, listener.onTerminated.count) 72| 1| } 73| | 74| 1| func testCancelWithTag() { 75| 1| let (type, job) = (UUID().uuidString, TestJob()) 76| 1| 77| 1| let id = UUID().uuidString 78| 1| let tag = UUID().uuidString 79| 1| let group = UUID().uuidString 80| 1| 81| 1| let creator = TestCreator([type: job]) 82| 1| 83| 1| let persister = PersisterTracker(key: UUID().uuidString) 84| 1| 85| 1| let manager = SwiftQueueManagerBuilder(creator: creator).set(persister: persister).build() 86| 1| 87| 1| JobBuilder(type: type) 88| 1| .singleInstance(forId: id) 89| 1| .parallel(queueName: group) 90| 1| .delay(time: 3600) 91| 1| .addTag(tag: tag) 92| 1| .schedule(manager: manager) 93| 1| 94| 1| manager.cancelOperations(tag: tag) 95| 1| 96| 1| job.awaitForRemoval() 97| 1| job.assertRemovedBeforeRun(reason: .canceled) 98| 1| 99| 1| XCTAssertEqual(0, persister.putQueueName.count) 100| 1| XCTAssertEqual(0, persister.putJobUUID.count) 101| 1| XCTAssertEqual(0, persister.putData.count) 102| 1| 103| 1| XCTAssertEqual(0, persister.removeJobUUID.count) 104| 1| XCTAssertEqual(0, persister.removeQueueName.count) 105| 1| } 106| | 107| 1| func testCancelWithUUID() { 108| 1| let (type, job) = (UUID().uuidString, TestJob()) 109| 1| 110| 1| let id = UUID().uuidString 111| 1| let group = UUID().uuidString 112| 1| 113| 1| let creator = TestCreator([type: job]) 114| 1| 115| 1| let persister = PersisterTracker(key: UUID().uuidString) 116| 1| 117| 1| let manager = SwiftQueueManagerBuilder(creator: creator).set(persister: persister).build() 118| 1| 119| 1| JobBuilder(type: type) 120| 1| .singleInstance(forId: id) 121| 1| .parallel(queueName: group) 122| 1| .delay(time: 3600) 123| 1| .schedule(manager: manager) 124| 1| 125| 1| manager.cancelOperations(uuid: id) 126| 1| 127| 1| job.awaitForRemoval() 128| 1| job.assertRemovedBeforeRun(reason: .canceled) 129| 1| 130| 1| XCTAssertEqual(0, persister.putQueueName.count) 131| 1| XCTAssertEqual(0, persister.putJobUUID.count) 132| 1| XCTAssertEqual(0, persister.putData.count) 133| 1| 134| 1| XCTAssertEqual(0, persister.removeJobUUID.count) 135| 1| XCTAssertEqual(0, persister.removeQueueName.count) 136| 1| } 137| | 138| 1| func testCancelAll() { 139| 1| let (type, job) = (UUID().uuidString, TestJob()) 140| 1| 141| 1| let id = UUID().uuidString 142| 1| let tag = UUID().uuidString 143| 1| let group = UUID().uuidString 144| 1| 145| 1| let creator = TestCreator([type: job]) 146| 1| 147| 1| let persister = PersisterTracker(key: UUID().uuidString) 148| 1| 149| 1| let manager = SwiftQueueManagerBuilder(creator: creator).set(persister: persister).build() 150| 1| 151| 1| JobBuilder(type: type) 152| 1| .singleInstance(forId: id) 153| 1| .parallel(queueName: group) 154| 1| .delay(time: 3600) 155| 1| .addTag(tag: tag) 156| 1| .schedule(manager: manager) 157| 1| 158| 1| manager.cancelAllOperations() 159| 1| 160| 1| job.awaitForRemoval() 161| 1| job.assertRemovedBeforeRun(reason: .canceled) 162| 1| 163| 1| XCTAssertEqual(0, persister.putQueueName.count) 164| 1| XCTAssertEqual(0, persister.putJobUUID.count) 165| 1| XCTAssertEqual(0, persister.putData.count) 166| 1| 167| 1| XCTAssertEqual(0, persister.removeJobUUID.count) 168| 1| XCTAssertEqual(0, persister.removeQueueName.count) 169| 1| } 170| | 171| 1| func testAddOperationNotJobTask() { 172| 1| let params = SqManagerParams( 173| 1| jobCreator: TestCreator([:]), 174| 1| queueCreator: BasicQueueCreator(), 175| 1| persister: NoPersister.shared, 176| 1| serializer: DecodableSerializer(maker: DefaultConstraintMaker()), 177| 1| logger: NoLogger.shared, 178| 1| listener: nil, 179| 1| initInBackground: false 180| 1| ) 181| 1| let queue = SqOperationQueue(params, BasicQueue.synchronous, true) 182| 1| let operation = Operation() 183| 1| queue.addOperation(operation) // Should not crash 184| 1| } 185| | 186| 1| func testLimitEquatable() { 187| 1| XCTAssertEqual(Limit.unlimited, Limit.unlimited) 188| 1| XCTAssertEqual(Limit.limited(-1), Limit.limited(-1)) 189| 1| XCTAssertEqual(Limit.limited(0), Limit.limited(0)) 190| 1| XCTAssertEqual(Limit.limited(1), Limit.limited(1)) 191| 1| XCTAssertNotEqual(Limit.limited(1), Limit.limited(2)) 192| 1| 193| 1| XCTAssertNotEqual(Limit.unlimited, Limit.limited(1)) 194| 1| XCTAssertNotEqual(Limit.unlimited, Limit.limited(0)) 195| 1| XCTAssertNotEqual(Limit.unlimited, Limit.limited(-1)) 196| 1| } 197| | 198| 1| public func testGetOperation() { 199| 1| let (type, job) = (UUID().uuidString, TestJob()) 200| 1| let id = UUID().uuidString 201| 1| let creator = TestCreator([type: job]) 202| 1| let persister = PersisterTracker(key: UUID().uuidString) 203| 1| let manager = SwiftQueueManagerBuilder(creator: creator).set(isSuspended: true).set(persister: persister).build() 204| 1| 205| 100| for _ in 0..<100 { 206| 100| JobBuilder(type: type).parallel(queueName: UUID().uuidString).schedule(manager: manager) 207| 100| } 208| 1| 209| 1| JobBuilder(type: type).singleInstance(forId: id).parallel(queueName: UUID().uuidString).schedule(manager: manager) 210| 1| 211| 1| let operation = manager.getOperation(forUUID: id)?.info.constraints ?? [] 212| 1| 213| 1| let constraint: UniqueUUIDConstraint? = getConstraint(operation) 214| 1| XCTAssertTrue(constraint?.uuid == id) 215| 1| } 216| | 217| 1| public func testGetAll() { 218| 1| let creator = TestCreator([:]) 219| 1| let manager = SwiftQueueManagerBuilder(creator: creator).set(persister: NoPersister.shared).build() 220| 1| 221| 1| XCTAssertEqual(0, manager.getAll().count) 222| 1| } 223| | 224| 1| func testCancelRunningOperation() { 225| 1| var manager: SwiftQueueManager? 226| 1| 227| 1| let (type, job) = (UUID().uuidString, TestJob { 228| 1| manager?.cancelAllOperations() 229| 1| $0.done(.fail(JobError())) 230| 1| }) 231| 1| 232| 1| let creator = TestCreator([type: job]) 233| 1| 234| 1| manager = SwiftQueueManagerBuilder(creator: creator) 235| 1| .set(persister: NoPersister.shared) 236| 1| .set(dispatchQueue: DispatchQueue.main) 237| 1| .build() 238| 1| 239| 1| manager?.enqueue(info: JobBuilder(type: type).build()) 240| 1| 241| 1| job.awaitForRemoval() 242| 1| job.assertRunCount(expected: 1) 243| 1| job.assertCompletedCount(expected: 0) 244| 1| job.assertRetriedCount(expected: 0) 245| 1| job.assertCanceledCount(expected: 1) 246| 1| job.assertError(queueError: .canceled) 247| 1| } 248| | 249| |} /Users/runner/work/SwiftQueue/SwiftQueue/Tests/SwiftQueueTests/TestUtils.swift: 1| |// The MIT License (MIT) 2| |// 3| |// Copyright (c) 2019 Lucas Nelaupe 4| |// 5| |// Permission is hereby granted, free of charge, to any person obtaining a copy 6| |// of this software and associated documentation files (the "Software"), to deal 7| |// in the Software without restriction, including without limitation the rights 8| |// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9| |// copies of the Software, and to permit persons to whom the Software is 10| |// furnished to do so, subject to the following conditions: 11| |// 12| |// The above copyright notice and this permission notice shall be included in all 13| |// copies or substantial portions of the Software. 14| |// 15| |// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16| |// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17| |// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18| |// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19| |// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20| |// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21| |// SOFTWARE. 22| | 23| |import Foundation 24| |import XCTest 25| |import Dispatch 26| |@testable import SwiftQueue 27| | 28| |class TestJob: Job { 29| | 30| | private let onRunCallback: (JobResult) -> Void 31| | private var withRetry: RetryConstraint 32| | 33| | private var onRunCount = 0 34| | private var onRetryCount = 0 35| | private var onCompletedCount = 0 36| | private var onCanceledCount = 0 37| | 38| 60| private let onRemoveSemaphore = DispatchSemaphore(value: 0) 39| | 40| | private var runSemaphoreValue = 0 41| | 42| | private var lastError: Error? 43| | 44| 60| init(retry: RetryConstraint = .retry(delay: 0), onRunCallback: @escaping (JobResult) -> Void = { $0.done(.success) }) { 45| 60| self.onRunCallback = onRunCallback 46| 60| self.withRetry = retry 47| 60| } 48| | 49| 250| func onRun(callback: JobResult) { 50| 250| XCTAssertFalse(Thread.isMainThread) 51| 250| onRunCount += 1 52| 250| onRunCallback(callback) 53| 250| } 54| | 55| 109| func onRetry(error: Error) -> RetryConstraint { 56| 109| XCTAssertFalse(Thread.isMainThread) 57| 109| lastError = error 58| 109| onRetryCount += 1 59| 109| return withRetry 60| 109| } 61| | 62| 48| func onRemove(result: JobCompletion) { 63| 48| switch result { 64| 48| case .success: 65| 22| lastError = nil 66| 22| onCompletedCount += 1 67| 22| onRemoveSemaphore.signal() 68| 48| 69| 48| case .fail(let error): 70| 26| lastError = error 71| 26| onCanceledCount += 1 72| 26| onRemoveSemaphore.signal() 73| 48| } 74| 48| } 75| | 76| | // Wait 77| | 78| 47| func awaitForRemoval() { 79| 47| onRemoveSemaphore.wait() 80| 47| } 81| | 82| | // Assertion 83| | 84| 51| public func assertRunCount(expected: Int, file: StaticString = #file, line: UInt = #line) { 85| 51| XCTAssertEqual(expected, onRunCount) 86| 51| } 87| 51| public func assertCompletedCount(expected: Int, file: StaticString = #file, line: UInt = #line) { 88| 51| XCTAssertEqual(expected, onCompletedCount) 89| 51| } 90| 51| public func assertRetriedCount(expected: Int, file: StaticString = #file, line: UInt = #line) { 91| 51| XCTAssertEqual(expected, onRetryCount) 92| 51| } 93| 51| public func assertCanceledCount(expected: Int, file: StaticString = #file, line: UInt = #line) { 94| 51| XCTAssertEqual(expected, onCanceledCount) 95| 51| } 96| 9| public func assertError(file: StaticString = #file, line: UInt = #line) { 97| 9| XCTAssertTrue(lastError is JobError) 98| 9| } 99| 16| public func assertError(queueError: SwiftQueueError, file: StaticString = #file, line: UInt = #line) { 100| 16| XCTAssertTrue(lastError is SwiftQueueError) 101| 16| guard let base: SwiftQueueError = lastError as? SwiftQueueError else { return } 102| 16| switch (base, queueError) { 103| 16| 104| 16| case let (.onRetryCancel(lErr), .onRetryCancel(rErr)): 105| 1| XCTAssertEqual(lErr as? JobError, rErr as? JobError) 106| 16| 107| 16| case (.duplicate, .duplicate): return 108| 16| case (.deadline, .deadline): return 109| 16| case (.canceled, .canceled): return 110| 16| case (.timeout, .timeout): return 111| 16| 112| 16| default: XCTFail("Type mismatch") 113| 16| } 114| 16| } 115| | 116| 28| public func assertNoError(file: StaticString = #file, line: UInt = #line) { 117| 28| XCTAssertNil(lastError) 118| 28| } 119| 6| public func assertNoRun(file: StaticString = #file, line: UInt = #line) { 120| 6| self.assertRunCount(expected: 0) 121| 6| self.assertCompletedCount(expected: 0) 122| 6| self.assertRetriedCount(expected: 0) 123| 6| self.assertCanceledCount(expected: 0) 124| 6| self.assertNoError(file: file, line: line) 125| 6| } 126| | // Job has run once without error and completed 127| 17| public func assertSingleCompletion(file: StaticString = #file, line: UInt = #line) { 128| 17| self.assertRunCount(expected: 1) 129| 17| self.assertCompletedCount(expected: 1) 130| 17| self.assertRetriedCount(expected: 0) 131| 17| self.assertCanceledCount(expected: 0) 132| 17| self.assertNoError(file: file, line: line) 133| 17| } 134| | 135| 12| public func assertRemovedBeforeRun(reason: SwiftQueueError, file: StaticString = #file, line: UInt = #line) { 136| 12| self.assertRunCount(expected: 0) 137| 12| self.assertCompletedCount(expected: 0) 138| 12| self.assertRetriedCount(expected: 0) 139| 12| self.assertCanceledCount(expected: 1) 140| 12| self.assertError(queueError: reason) 141| 12| } 142| | 143| |} 144| | 145| |class TestJobFail: TestJob { 146| | 147| 7| required init(retry: RetryConstraint = .retry(delay: 0), error: Error = JobError()) { 148| 16| super.init(retry: retry) { $0.done(.fail(error))} 149| 7| } 150| | 151| |} 152| | 153| |class TestCreator: JobCreator { 154| | private let job: [String: TestJob] 155| | 156| 48| public init(_ job: [String: TestJob]) { 157| 48| self.job = job 158| 48| } 159| | 160| 151| func create(type: String, params: [String: Any]?) -> Job { 161| 151| return job[type]! 162| 151| } 163| |} 164| | 165| |class PersisterTracker: UserDefaultsPersister { 166| | var restoreQueueName = "" 167| | 168| 14| var putQueueName: [String] = [String]() 169| 14| var putJobUUID: [String] = [String]() 170| 14| var putData: [String] = [String]() 171| | 172| 14| var removeQueueName: [String] = [String]() 173| 14| var removeJobUUID: [String] = [String]() 174| | 175| 114| override func restore(queueName: String) -> [String] { 176| 114| restoreQueueName = queueName 177| 114| return super.restore(queueName: queueName) 178| 114| } 179| | 180| 11| override func put(queueName: String, taskId: String, data: String) { 181| 11| putQueueName.append(queueName) 182| 11| putJobUUID.append(taskId) 183| 11| putData.append(data) 184| 11| super.put(queueName: queueName, taskId: taskId, data: data) 185| 11| } 186| | 187| 6| override func remove(queueName: String, taskId: String) { 188| 6| removeQueueName.append(queueName) 189| 6| removeJobUUID.append(taskId) 190| 6| super.remove(queueName: queueName, taskId: taskId) 191| 6| } 192| |} 193| | 194| |class JobListenerTest: JobListener { 195| | 196| 1| var onBeforeRun: [JobInfo] = [JobInfo]() 197| 1| var onAfterRun: [(JobInfo, JobCompletion)] = [(JobInfo, JobCompletion)]() 198| 1| var onTerminated: [(JobInfo, JobCompletion)] = [(JobInfo, JobCompletion)]() 199| | 200| 1| func onBeforeRun(job: JobInfo) { 201| 1| onBeforeRun.append(job) 202| 1| } 203| | 204| 1| func onAfterRun(job: JobInfo, result: JobCompletion) { 205| 1| onAfterRun.append((job, result)) 206| 1| } 207| | 208| 1| func onTerminated(job: JobInfo, result: JobCompletion) { 209| 1| onTerminated.append((job, result)) 210| 1| } 211| |} 212| | 213| |class JobError: Error { 214| | 215| 111| let id = UUID().uuidString 216| | 217| |} 218| | 219| |extension JobError: Equatable { 220| | 221| 1| public static func == (lhs: JobError, rhs: JobError) -> Bool { 222| 1| return lhs.id == rhs.id 223| 1| } 224| |} 225| | 226| |extension SqOperation { 227| | 228| 4| func toJSONStringSafe() -> String { 229| 4| (try? DecodableSerializer(maker: DefaultConstraintMaker()).serialize(info: self.info)) ?? "{}" 230| 4| } 231| | 232| |} 233| | 234| |class NoPersister: JobPersister { 235| | 236| | public static let shared = NoPersister() 237| | 238| 1| private init() {} 239| | 240| 33| func restore() -> [String] { return [] } 241| | 242| 33| func restore(queueName: String) -> [String] { return [] } 243| | 244| 0| func put(queueName: String, taskId: String, data: String) {} 245| | 246| 0| func remove(queueName: String, taskId: String) {} 247| | 248| 0| func clearAll() {} 249| | 250| |} 251| | 252| |class MemorySerializer: JobInfoSerializer { 253| | 254| 1| private var data: [String: JobInfo] = [:] 255| | 256| 2| func serialize(info: JobInfo) throws -> String { 257| 2| let constraint: UniqueUUIDConstraint? = getConstraint(info) 258| 2| let uuid = constraint?.uuid ?? "" 259| 2| assertNotEmptyString(uuid) 260| 2| data[uuid] = info 261| 2| return uuid 262| 2| } 263| | 264| 1| func deserialize(json: String) throws -> JobInfo { 265| 1| return data[json] ?? JobInfo(type: json) 266| 1| } 267| |} 268| | 269| |extension JobBuilder { 270| | 271| 4| internal func build(job: Job, logger: SwiftQueueLogger = NoLogger.shared, listener: JobListener? = nil) -> SqOperation { 272| 4| let info = build() 273| 4| let constraints = info.constraints 274| 4| return SqOperation(job, info, logger, listener, DispatchQueue.global(qos: DispatchQoS.QoSClass.utility), constraints) 275| 4| } 276| | 277| |} <<<<<< EOF # path=fixes ./Tests/SwiftQueueTests/ConstraintTest+Deadline.swift:22,26,28,31,33,38,41,42,46,49,54,59,61,64,67,68,71,73,75,81,84,86,89,91,93,94,97,99,106,108,111,112,113 ./Tests/SwiftQueueTests/BackgroundTasksTest.swift:22,26,29,33,35,43,45,46,53,55,58,62,63,64,71,73,76,80,81,85,88,91,93,95,97,100,103,105,107,108,109 ./Tests/SwiftQueueTests/SwiftQueueManagerTests.swift:22,26,28,31,33,39,42,43,46,49,55,57,63,65,68,72,73,76,80,82,84,86,93,95,98,102,105,106,109,112,114,116,118,124,126,129,133,136,137,140,144,146,148,150,157,159,162,166,169,170,184,185,192,196,197,204,207,208,210,212,215,216,220,222,223,226,231,233,238,240,247,248,249 ./Tests/SwiftQueueTests/SqOperationTest.swift:22,24,28,30,39,44,46,47,48,57,62,64,65,66,67 ./Tests/SwiftQueueTests/ConstraintTest+Custom.swift:22,26,28,31,34,39,42,46,47,48,49,51,55,58,59,62,63,67,68,69 ./Tests/SwiftQueueTests/BasicConstraintTest.swift:22,24,28,30,33,38,43,46,50,51,54,59,64,66,69,73,74,78,84,88,93,95,97,100,104,105,106,107,109,113,118,119,123,124,128,129,133,134,135 ./Tests/SwiftQueueTests/ConstraintTest+Charging.swift:22,26,28,31,33,38,41,42,43 ./Tests/SwiftQueueTests/ConstraintTest+UniqueUUID.swift:22,26,28,32,34,36,42,44,47,50,51,55,57,59,65,69,72,75,78,79,80 ./Tests/SwiftQueueTests/ConstraintTest+Delay.swift:22,26,28,31,33,38,40,43,44,45 ./Tests/SwiftQueueTests/PersisterTests.swift:22,27,29,33,35,37,43,49,53,57,59,63,65,67,70,73,75,76,80,82,84,86,88,95,102,104,107,110,113,114,117,120,123,130,133,136,137,140,143,145,147,149,156,163,166,167,170,173,177,179,181,187,188,191,195,197,199,201,209,211,214,217,218,221,224,230,235,238,245,247,250,251,254,257,258,259 ./Tests/SwiftQueueTests/LoggerTests.swift:22,27,29,32,34,36,38,43,46,48,55,56,58,62,65,69,72,76,79,83,87,91,94,96,97,98,99,101,103,106,107,108 ./Tests/SwiftQueueTests/XCTestManifests.swift:3,13,14,22,23,34,35,43,44,53,54,65,66,80,81,89,90,98,99,108,109,117,118,127,128,143,144,153,154,163,164,186,187,204,205,226 ./Tests/SwiftQueueTests/ConstraintTest+Network.swift:22,26,28,31,33,38,41,42,45,47,52,55,56,57 ./Tests/SwiftQueueTests/SwiftQueueBuilderTests.swift:22,24,28,30,33,36,37,38,42,44,49,50,54,56,61,62,66,69,70,74,79,80,85,89,90,95,97,100,104,105,109,111,115,116,120,122,126,127,130,132,136,137,141,143,147,148,153,155,160,161,165,170,174,175,178,180,183,184,188,190,193,194,195 ./Tests/SwiftQueueTests/ConstraintTest+Retry.swift:22,26,28,32,39,40,41,43,45,50,57,58,61,63,65,70,77,78,82,84,89,96,97,101,103,108,115,116,120,122,128,135,136,139,141,146,153,154,158,160,165,172,173,174 ./Tests/SwiftQueueTests/StartStopTests.swift:22,25,27,30,32,37,39,42,44,47,48,51,53,55,57,59,62,70,72,78,79,80 ./Tests/SwiftQueueTests/ConstraintTest+Repeat.swift:22,26,28,31,33,38,45,46,50,52,59,60,61,63,68,75,76,79,81,86,93,94,99,105,109,113,115,117,124,126,127,128 ./Tests/SwiftQueueTests/ConstraintTest+Tag.swift:22,26,28,31,33,38,40,42,44,51,52,53 ./Tests/SwiftQueueTests/ConstraintTest+Timeout.swift:22,26,28,31,33,35,39,46,47,48 ./Tests/SwiftQueueTests/TestUtils.swift:22,27,29,32,37,39,41,43,47,48,53,54,60,61,68,73,74,75,77,80,81,83,86,89,92,95,98,103,106,111,113,114,115,118,125,133,134,141,142,143,144,146,149,150,151,152,155,158,159,162,163,164,167,171,174,178,179,185,186,191,192,193,195,199,202,203,206,207,210,211,212,214,216,217,218,220,223,224,225,227,230,231,232,233,235,237,239,241,243,245,247,249,250,251,253,255,262,263,266,267,268,270,275,276,277 ./Tests/LinuxMain.swift:2,4,7 ./Carthage/Checkouts/Reachability.swift/Tests/ReachabilityTests.swift:8,11,13,16,19,20,24,27,31,32,37,38,40,42,43,47,49,52,53,57,58,62,63,68,69,71,73,74,75 ./Carthage/Checkouts/Reachability.swift/ReachabilityAppleTVSample/ViewController.swift:8,11,13,16,20,23,25,26,33,34,35,44,47,51,54,62,63,64,73,74,75,81,82,89,90,92,93,96,98,100,101,102,105,110,111,112,115,116 ./Carthage/Checkouts/Reachability.swift/ReachabilityAppleTVSample/AppDelegate.swift:8,10,13,15,18,19 ./Carthage/Checkouts/Reachability.swift/ReachabilityMacSample/ViewController.swift:8,11,15,19,23,25,26,33,34,35,44,47,51,54,62,63,64,73,74,75,81,82,89,90,92,93,96,98,100,101,104,109,110,111 ./Carthage/Checkouts/Reachability.swift/ReachabilityMacSample/AppDelegate.swift:8,10,14,15,17,18 ./Carthage/Checkouts/Reachability.swift/ReachabilitySample/ViewController.swift:8,11,13,16,20,23,25,26,33,34,35,44,47,51,54,62,63,64,73,74,75,81,82,89,90,92,93,96,98,100,101,104,109,110,111,114,115 ./Carthage/Checkouts/Reachability.swift/ReachabilitySample/AppDelegate.swift:8,10,13,15,18,19,20 ./Carthage/Checkouts/Reachability.swift/Package.swift:3,5 ./Carthage/Checkouts/Reachability.swift/Sources/Reachability.swift:4,7,10,14,27,30,37,38,41,44,45,47,50,59,60,61,62,73,74,75,76,79,82,85,88,92,93,97,98,102,103,109,110,111,119,128,129,130,139,140,147,149,150,157,160,161,163,164,167,168,169,171,175,178,182,186,187,190,208,210,214,215,219,220,223,225,226,229,232,233,238,239,244,245,249,250,253,254,255,257,264,265,267,268,269,270,276,277,280,281,282,284,286,289,295,298,299,303,304,305,308,309,312,313,320,323,326,329,332,335,338,341,344,347,350,351,362,364,365,366,370,374,376,382,384,391,393,405,406 ./Carthage/Checkouts/Reachability.swift/Sources/Reachability.h:8,10,13,16 ./Carthage/Build/watchOS/SwiftQueue.framework/Headers/SwiftQueue-Swift.h:8,21,25,31,53,68,131,136,140,195,203,210,211,224,225,226,227,228,229,230,231,238,243,250,263,267,273,295,310,373,378,382,437,445,452,453,466,467,468,469,470,471,472,473,480,485,490,503,507,513,535,550,613,618,622,677,685,692,693,706,707,708,709,710,711,712,713,720,725,727 ./Carthage/Build/iOS/SwiftQueue.framework/Headers/SwiftQueue-Swift.h:10,23,27,33,55,70,133,138,142,197,205,212,213,226,227,228,229,230,231,232,233,234,235,242,247,252,265,269,275,297,312,375,380,384,439,447,454,455,468,469,470,471,472,473,474,475,476,477,484,489,491,498,511,515,521,543,558,621,626,630,685,693,700,701,714,715,716,717,718,719,720,721,722,723,730,735,740,753,757,763,785,800,863,868,872,927,935,942,943,956,957,958,959,960,961,962,963,964,965,972,977,979 ./Carthage/Build/iOS/Reachability.framework/Headers/Reachability-Swift.h:10,23,27,33,55,70,133,138,142,196,204,211,216,221,234,238,244,266,281,344,349,353,407,415,422,427,429,436,449,453,459,481,496,559,564,568,622,630,637,642,647,660,664,670,692,707,770,775,779,833,841,848,853,855 ./Carthage/Build/iOS/Reachability.framework/Headers/Reachability.h:8,10,13,16 ./Carthage/Build/Mac/SwiftQueue.framework/Versions/A/Headers/SwiftQueue-Swift.h:4,17,21,27,49,64,127,132,136,191,199,206,207,220,221,222,223,224,225,226,227,234 ./Carthage/Build/Mac/Reachability.framework/Versions/A/Headers/Reachability-Swift.h:4,17,21,27,49,64,127,132,136,190,198,205 ./Carthage/Build/Mac/Reachability.framework/Versions/A/Headers/Reachability.h:8,10,13,16 ./Carthage/Build/tvOS/SwiftQueue.framework/Headers/SwiftQueue-Swift.h:8,21,25,31,53,68,131,136,140,195,203,210,211,224,225,226,227,228,229,230,231,232,233,240,245,250,263,267,273,295,310,373,378,382,437,445,452,453,466,467,468,469,470,471,472,473,474,475,482,487 ./Carthage/Build/tvOS/Reachability.framework/Headers/Reachability-Swift.h:8,21,25,31,53,68,131,136,140,194,202,209,214,219,232,236,242,264,279,342,347,351,405,413,420,425 ./Carthage/Build/tvOS/Reachability.framework/Headers/Reachability.h:8,10,13,16 ./Package.swift:3,5 ./Sources/SwiftQueue/SqOperation.swift:22,24,26,30,32,35,38,41,47,49,52,60,61,62,70,71,72,86,88,89,95,96,99,100,106,107,112,113,114,122,123,127,130,131,139,140,146,147,151,152,158,159,160,161,163,166,168,174,175,176,180,185,186,187,192,197,198,199,200,201,203,207,208,209,213,214,215,219,221,222,223 ./Sources/SwiftQueue/UserDefaultsPersister.swift:22,24,27,30,34,35,41,42,49,50,58,60,61,67,68,72,73,74 ./Sources/SwiftQueue/JobInfoSerializer+Decodable.swift:22,24,27,30,37,38,41,42,45,46,47,48,50,53,54,62,64,65,66,67,69,73,74,75,76,79 ./Sources/SwiftQueue/Constraint+Retry.swift:22,24,26,29,32,33,39,40,48,51,52,53,56,57,61,62,63,65,67,71,72,78,79,80,91,92,99,100,101,102,103,114 ./Sources/SwiftQueue/SwiftQueue.swift:22,24,27,31,32,33,35,37,38,39,42,45,48,51,54,57,58,59,62,65,68,69,70,73,76,77,78,81,84,87,88,89,92,96,103,107,108,109,111,113,115,116,117,122,123,125,127,133,134,135,136,137,139,145,146,147,153,154,155,156,157,160,163,166,169,170,171,174,177,180,181,182,185,188,191,194,197,200,201 ./Sources/SwiftQueue/Constraint+Persister.swift:22,24,26,28,30,34,35,43,44,47,48,49 ./Sources/SwiftQueue/SwiftQueueLogger.swift:22,24,27,34,35,36,38,48,49,50,51,54,57,58,59,62,64,68,69,74,75,76,80,81,82,83,86,89,91,95,96 ./Sources/SwiftQueue/JobBuilder.swift:22,24,27,29,34,35,39,40,48,49,55,56,65,66,72,73,83,84,91,92,98,99,101,106,107,114,115,121,122,125,128,129,134,135,140,141,146,147,152,153,158,159,165,166,170,171,175,176,183,186,187,189,190,191 ./Sources/SwiftQueue/Constraint+Repeat.swift:22,24,26,29,32,35,38,43,44,54,55,64,65,66,73,74,75,81,82,89,90,95,96,102,103,104,105,108,111,114,117,118,119,121,131,132,133,134,135,137,139,144,145,149,150,151 ./Sources/SwiftQueue/JobInfo.swift:22,24,28,31,34,37,40,43,46,48,51,52,63,64,80,81,82,84,93,94,97,105,115,116,119,128,129,130 ./Sources/SwiftQueue/Constraint+Charging.swift:22,27,30,33,39,40,46,47,48,57,58,62,63,65,69,70,73,74,77,78,82,83,86,87,88,89,91,93,95,96,97 ./Sources/SwiftQueue/Constraint+Timeout.swift:22,24,26,29,32,33,39,40,45,46,47,49,50,53,54,58,59,60 ./Sources/SwiftQueue/SwiftQueueManager.swift:22,25,30,32,38,39,40,41,43,47,50,51,52,55,56,61,62,69,71,73,74,79,80,81,87,88,89,95,96,97,102,103,104,105,106,112,113,121,123,125,126,127,129,137,139,140,141,146,147,149,150,151,153,155,157,159,161,163,165,167,169,187,188,189,190,193,196,200,201,206,207,212,213,220,221,226,227,232,233,238,239,243,244,248,249,250 ./Sources/SwiftQueue/SqOperationQueue.swift:22,24,26,29,35,37,42,47,49,51,54,58,61,62,63,70,75,80,82,83,86,87,90,93,94,99,100,106,107,110,112,113,118,119,120,121,127,128,129,130,136,137,140,142,143,146,147,148,149 ./Sources/SwiftQueue/Utils.swift:22,25,27,31,32,33,34,37,38,40,47,48,49,55,56,57,58,59,61,63,68,69,78,79,80,81,82,84,93,94,95,96,98,103,105,106,107,108,110,115,117,118,119,120,122,129,131,132,139,141,142,143,144,148,149,153,155 ./Sources/SwiftQueue/Constraint+Network.swift:22,27,36,37,40,43,45,49,50,56,57,61,62,65,68,69,71,76,77,85,86,87,96,97,98,101,102,106,107,108,110,112,114,116,117,118 ./Sources/SwiftQueue/Constraint+Delay.swift:22,24,26,29,32,33,39,40,46,47,49,57,60,61,64,65,69,70,71 ./Sources/SwiftQueue/ConstraintMaker.swift:22,24,26,28,29,30,32,35,39,48,50,51,52 ./Sources/SwiftQueue/Constraint+Deadline.swift:22,24,26,29,32,33,39,40,43,44,47,48,53,56,57,61,62,63,66,67,71,72,73 ./Sources/SwiftQueue/Constraint.swift:22,24,26,32,38,44,45,46,48,54,55,56,58,60,62,64,65 ./Sources/SwiftQueue/Constraint+UniqueUUID.swift:22,24,26,29,33,36,41,42,52,53,62,63,64,65,66,69,70,75,76,82,83,84 ./Sources/SwiftQueue/Constraint+Tag.swift:22,24,26,28,31,32,38,39,42,43,46,47,50,51,55,56 ./Sources/ios/SwiftQueueManager+BackgroundTask.swift:22,26,30,39,41,42,43,44,49,50,51,57,58,59,60,61,64,67,69,72,73,76,77,79,84,85,86,87,90,93,97,98,101,107,108,109 <<<<<< EOF