mirror of https://github.com/oxen-io/session-ios
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1277 lines
59 KiB
Swift
1277 lines
59 KiB
Swift
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
|
|
|
|
import Foundation
|
|
import GRDB
|
|
|
|
import Quick
|
|
import Nimble
|
|
|
|
@testable import SessionUtilitiesKit
|
|
|
|
class JobRunnerSpec: QuickSpec {
|
|
enum TestSuccessfulJob: JobExecutor {
|
|
static let maxFailureCount: Int = 0
|
|
static let requiresThreadId: Bool = false
|
|
static let requiresInteractionId: Bool = false
|
|
|
|
static func run(
|
|
_ job: Job,
|
|
queue: DispatchQueue,
|
|
success: @escaping (Job, Bool, Dependencies) -> (),
|
|
failure: @escaping (Job, Error?, Bool, Dependencies) -> (),
|
|
deferred: @escaping (Job, Dependencies) -> (),
|
|
dependencies: Dependencies
|
|
) {
|
|
guard dependencies.date.timeIntervalSinceNow > 0 else { return success(job, true, dependencies) }
|
|
|
|
queue.asyncAfter(deadline: .now() + .milliseconds(Int(dependencies.date.timeIntervalSinceNow * 1000))) {
|
|
success(job, true, dependencies)
|
|
}
|
|
}
|
|
}
|
|
|
|
enum TestFailedJob: JobExecutor {
|
|
static let maxFailureCount: Int = 1
|
|
static let requiresThreadId: Bool = false
|
|
static let requiresInteractionId: Bool = false
|
|
|
|
static func run(
|
|
_ job: Job,
|
|
queue: DispatchQueue,
|
|
success: @escaping (Job, Bool, Dependencies) -> (),
|
|
failure: @escaping (Job, Error?, Bool, Dependencies) -> (),
|
|
deferred: @escaping (Job, Dependencies) -> (),
|
|
dependencies: Dependencies
|
|
) {
|
|
guard dependencies.date.timeIntervalSinceNow > 0 else { return failure(job, nil, false, dependencies) }
|
|
|
|
queue.asyncAfter(deadline: .now() + .milliseconds(Int(dependencies.date.timeIntervalSinceNow * 1000))) {
|
|
failure(job, nil, false, dependencies)
|
|
}
|
|
}
|
|
}
|
|
|
|
enum TestPermanentFailureJob: JobExecutor {
|
|
static let maxFailureCount: Int = 1
|
|
static let requiresThreadId: Bool = false
|
|
static let requiresInteractionId: Bool = false
|
|
|
|
static func run(
|
|
_ job: Job,
|
|
queue: DispatchQueue,
|
|
success: @escaping (Job, Bool, Dependencies) -> (),
|
|
failure: @escaping (Job, Error?, Bool, Dependencies) -> (),
|
|
deferred: @escaping (Job, Dependencies) -> (),
|
|
dependencies: Dependencies
|
|
) {
|
|
guard dependencies.date.timeIntervalSinceNow > 0 else { return failure(job, nil, true, dependencies) }
|
|
|
|
queue.asyncAfter(deadline: .now() + .milliseconds(Int(dependencies.date.timeIntervalSinceNow * 1000))) {
|
|
failure(job, nil, true, dependencies)
|
|
}
|
|
}
|
|
}
|
|
|
|
enum TestDeferredJob: JobExecutor {
|
|
static let maxFailureCount: Int = 0
|
|
static let requiresThreadId: Bool = false
|
|
static let requiresInteractionId: Bool = false
|
|
|
|
static func run(
|
|
_ job: Job,
|
|
queue: DispatchQueue,
|
|
success: @escaping (Job, Bool, Dependencies) -> (),
|
|
failure: @escaping (Job, Error?, Bool, Dependencies) -> (),
|
|
deferred: @escaping (Job, Dependencies) -> (),
|
|
dependencies: Dependencies
|
|
) {
|
|
guard dependencies.date.timeIntervalSinceNow > 0 else { return deferred(job, dependencies) }
|
|
|
|
queue.asyncAfter(deadline: .now() + .milliseconds(Int(dependencies.date.timeIntervalSinceNow * 1000))) {
|
|
deferred(job, dependencies)
|
|
}
|
|
}
|
|
}
|
|
|
|
struct TestDetails: Codable {
|
|
public let intValue: Int64
|
|
public let stringValue: String
|
|
}
|
|
|
|
struct InvalidDetails: Codable {
|
|
func encode(to encoder: Encoder) throws { throw HTTP.Error.parsingFailed }
|
|
}
|
|
|
|
// MARK: - Spec
|
|
|
|
override func spec() {
|
|
var jobRunner: JobRunnerType!
|
|
var job1: Job!
|
|
var job2: Job!
|
|
var jobDetails: TestDetails!
|
|
var mockStorage: Storage!
|
|
var dependencies: Dependencies!
|
|
|
|
// MARK: - JobRunner
|
|
|
|
describe("a JobRunner") {
|
|
beforeEach {
|
|
mockStorage = Storage(
|
|
customWriter: try! DatabaseQueue(),
|
|
customMigrations: [
|
|
SNUtilitiesKit.migrations()
|
|
]
|
|
)
|
|
dependencies = Dependencies(
|
|
storage: mockStorage,
|
|
date: Date(timeIntervalSince1970: 1234567890)
|
|
)
|
|
|
|
// Migrations add jobs which we don't want so delete them
|
|
mockStorage.write { db in try Job.deleteAll(db) }
|
|
|
|
job1 = Job(
|
|
id: 100,
|
|
failureCount: 0,
|
|
variant: .messageSend,
|
|
behaviour: .runOnce,
|
|
shouldBlock: false,
|
|
shouldSkipLaunchBecomeActive: false,
|
|
nextRunTimestamp: 0,
|
|
threadId: nil,
|
|
interactionId: nil,
|
|
details: nil
|
|
)
|
|
jobDetails = TestDetails(
|
|
intValue: 100,
|
|
stringValue: "200"
|
|
)
|
|
job2 = Job(
|
|
id: 101,
|
|
failureCount: 0,
|
|
variant: .attachmentUpload,
|
|
behaviour: .runOnce,
|
|
shouldBlock: false,
|
|
shouldSkipLaunchBecomeActive: false,
|
|
nextRunTimestamp: 0,
|
|
threadId: nil,
|
|
interactionId: nil,
|
|
details: try! JSONEncoder().encode(jobDetails)
|
|
)
|
|
|
|
jobRunner = JobRunner(isTestingJobRunner: true, dependencies: dependencies)
|
|
|
|
// Need to assign this to ensure it's used by nested dependencies
|
|
dependencies.jobRunner = jobRunner
|
|
}
|
|
|
|
afterEach {
|
|
jobRunner.stopAndClearPendingJobs()
|
|
jobRunner = nil
|
|
mockStorage = nil
|
|
dependencies = nil
|
|
}
|
|
// MARK: -- when configuring
|
|
|
|
context("when configuring") {
|
|
it("adds an executor correctly") {
|
|
jobRunner.appDidFinishLaunching(dependencies: dependencies)
|
|
|
|
// First check that it fails to start
|
|
dependencies.date = Date().addingTimeInterval(20 / 1000) // Complete job after delay
|
|
|
|
mockStorage.write { db in
|
|
jobRunner.upsert(
|
|
db,
|
|
job: job1,
|
|
canStartJob: true,
|
|
dependencies: dependencies
|
|
)
|
|
}
|
|
|
|
expect(jobRunner.isCurrentlyRunning(job1))
|
|
.toEventually(
|
|
beFalse(),
|
|
timeout: .milliseconds(10)
|
|
)
|
|
|
|
jobRunner.setExecutor(TestSuccessfulJob.self, for: .messageSend)
|
|
|
|
// Then check that it succeeded to start
|
|
dependencies.date = Date().addingTimeInterval(20 / 1000) // Complete job after delay
|
|
|
|
mockStorage.write { db in
|
|
jobRunner.upsert(
|
|
db,
|
|
job: job1,
|
|
canStartJob: true,
|
|
dependencies: dependencies
|
|
)
|
|
}
|
|
|
|
expect(jobRunner.isCurrentlyRunning(job1))
|
|
.toEventually(
|
|
beFalse(),
|
|
timeout: .milliseconds(10)
|
|
)
|
|
}
|
|
}
|
|
|
|
// MARK: -- when managing state
|
|
|
|
context("when managing state") {
|
|
|
|
// MARK: ---- by checking if a job is currently running
|
|
|
|
context("by checking if a job is currently running") {
|
|
beforeEach {
|
|
jobRunner.setExecutor(TestSuccessfulJob.self, for: .messageSend)
|
|
}
|
|
|
|
it("returns false when not given a job") {
|
|
expect(jobRunner.isCurrentlyRunning(nil)).to(beFalse())
|
|
}
|
|
|
|
it("returns false when given a job that has not been persisted") {
|
|
job1 = Job(variant: .messageSend)
|
|
|
|
expect(jobRunner.isCurrentlyRunning(job1)).to(beFalse())
|
|
}
|
|
|
|
it("returns false when given a job that is not running") {
|
|
expect(jobRunner.isCurrentlyRunning(job1)).to(beFalse())
|
|
}
|
|
|
|
it("returns true when given a non blocking job that is running") {
|
|
jobRunner.appDidFinishLaunching(dependencies: dependencies)
|
|
|
|
dependencies.date = Date().addingTimeInterval(20 / 1000) // Complete job after delay
|
|
|
|
mockStorage.write { db in
|
|
jobRunner.upsert(
|
|
db,
|
|
job: job1,
|
|
canStartJob: true,
|
|
dependencies: dependencies
|
|
)
|
|
}
|
|
|
|
expect(jobRunner.isCurrentlyRunning(job1))
|
|
.toEventually(
|
|
beTrue(),
|
|
timeout: .milliseconds(10)
|
|
)
|
|
}
|
|
|
|
it("returns true when given a blocking job that is running") {
|
|
job2 = Job(
|
|
id: 101,
|
|
failureCount: 0,
|
|
variant: .messageSend,
|
|
behaviour: .runOnceNextLaunch,
|
|
shouldBlock: true,
|
|
shouldSkipLaunchBecomeActive: false,
|
|
nextRunTimestamp: 0,
|
|
threadId: nil,
|
|
interactionId: nil,
|
|
details: nil
|
|
)
|
|
|
|
mockStorage.write { db in
|
|
try job2.insert(db)
|
|
|
|
jobRunner.upsert(
|
|
db,
|
|
job: job2,
|
|
canStartJob: true,
|
|
dependencies: dependencies
|
|
)
|
|
}
|
|
|
|
dependencies.date = Date().addingTimeInterval(20 / 1000) // Complete job after delay
|
|
jobRunner.appDidFinishLaunching(dependencies: dependencies)
|
|
|
|
expect(jobRunner.isCurrentlyRunning(job2))
|
|
.toEventually(
|
|
beTrue(),
|
|
timeout: .milliseconds(10)
|
|
)
|
|
}
|
|
}
|
|
|
|
// MARK: ---- by getting the details for jobs
|
|
|
|
context("by getting the details for jobs") {
|
|
beforeEach {
|
|
jobRunner.setExecutor(TestSuccessfulJob.self, for: .messageSend)
|
|
jobRunner.setExecutor(TestSuccessfulJob.self, for: .attachmentUpload)
|
|
jobRunner.setExecutor(TestSuccessfulJob.self, for: .attachmentDownload)
|
|
}
|
|
|
|
it("returns an empty dictionary when there are no jobs") {
|
|
expect(jobRunner.details()).to(equal([:]))
|
|
}
|
|
|
|
it("returns an empty dictionary when there are no jobs matching the filters") {
|
|
jobRunner.appDidFinishLaunching(dependencies: dependencies)
|
|
|
|
dependencies.date = Date().addingTimeInterval(20 / 1000) // Complete job after delay
|
|
|
|
mockStorage.write { db in
|
|
jobRunner.upsert(
|
|
db,
|
|
job: job2,
|
|
canStartJob: true,
|
|
dependencies: dependencies
|
|
)
|
|
}
|
|
|
|
expect(jobRunner.detailsFor(state: .running, variant: .messageSend))
|
|
.toEventually(
|
|
equal([:]),
|
|
timeout: .milliseconds(10)
|
|
)
|
|
}
|
|
|
|
it("can filter to specific jobs") {
|
|
mockStorage.write { db in
|
|
jobRunner.upsert(
|
|
db,
|
|
job: job2,
|
|
/// The `canStartJob` value needs to be `true` for the job to be added to the queue but as
|
|
/// long as `appDidFinishLaunching` hasn't been called it won't actually start running and
|
|
/// as a result we can test the "pending" state
|
|
canStartJob: true,
|
|
dependencies: dependencies
|
|
)
|
|
}
|
|
|
|
// Wait for there to be data and the validate the filtering works
|
|
expect(jobRunner.details())
|
|
.toEventuallyNot(
|
|
beEmpty(),
|
|
timeout: .milliseconds(10)
|
|
)
|
|
expect(jobRunner.detailsFor(jobs: [job1])).to(equal([:]))
|
|
expect(jobRunner.detailsFor(jobs: [job2])).to(equal([101: job2.details]))
|
|
}
|
|
|
|
it("can filter to running jobs") {
|
|
job1 = Job(
|
|
id: 100,
|
|
failureCount: 0,
|
|
variant: .attachmentDownload,
|
|
behaviour: .runOnce,
|
|
shouldBlock: false,
|
|
shouldSkipLaunchBecomeActive: false,
|
|
nextRunTimestamp: 0,
|
|
threadId: nil,
|
|
interactionId: nil,
|
|
details: try! JSONEncoder().encode(jobDetails)
|
|
)
|
|
job2 = Job(
|
|
id: 101,
|
|
failureCount: 0,
|
|
variant: .attachmentDownload,
|
|
behaviour: .runOnce,
|
|
shouldBlock: false,
|
|
shouldSkipLaunchBecomeActive: false,
|
|
nextRunTimestamp: 0,
|
|
threadId: nil,
|
|
interactionId: nil,
|
|
details: nil
|
|
)
|
|
dependencies.date = Date().addingTimeInterval(20 / 1000) // Complete job after delay
|
|
jobRunner.appDidFinishLaunching(dependencies: dependencies)
|
|
|
|
mockStorage.write { db in
|
|
try job1.insert(db)
|
|
try job2.insert(db)
|
|
|
|
jobRunner.upsert(
|
|
db,
|
|
job: job1,
|
|
canStartJob: true,
|
|
dependencies: dependencies
|
|
)
|
|
|
|
jobRunner.upsert(
|
|
db,
|
|
job: job2,
|
|
canStartJob: true,
|
|
dependencies: dependencies
|
|
)
|
|
}
|
|
|
|
// Wait for there to be data and the validate the filtering works
|
|
expect(jobRunner.detailsFor(state: .running))
|
|
.toEventually(
|
|
equal([100: try! JSONEncoder().encode(jobDetails)]),
|
|
timeout: .milliseconds(10)
|
|
)
|
|
expect(Array(jobRunner.details().keys).sorted()).to(equal([100, 101]))
|
|
}
|
|
|
|
it("can filter to pending jobs") {
|
|
job1 = Job(
|
|
id: 100,
|
|
failureCount: 0,
|
|
variant: .attachmentDownload,
|
|
behaviour: .runOnce,
|
|
shouldBlock: false,
|
|
shouldSkipLaunchBecomeActive: false,
|
|
nextRunTimestamp: 0,
|
|
threadId: nil,
|
|
interactionId: nil,
|
|
details: nil
|
|
)
|
|
job2 = Job(
|
|
id: 101,
|
|
failureCount: 0,
|
|
variant: .attachmentDownload,
|
|
behaviour: .runOnce,
|
|
shouldBlock: false,
|
|
shouldSkipLaunchBecomeActive: false,
|
|
nextRunTimestamp: 0,
|
|
threadId: nil,
|
|
interactionId: nil,
|
|
details: try! JSONEncoder().encode(jobDetails)
|
|
)
|
|
dependencies.date = Date().addingTimeInterval(20 / 1000) // Complete job after delay
|
|
jobRunner.appDidFinishLaunching(dependencies: dependencies)
|
|
|
|
mockStorage.write { db in
|
|
try job1.insert(db)
|
|
try job2.insert(db)
|
|
|
|
jobRunner.upsert(
|
|
db,
|
|
job: job1,
|
|
canStartJob: true,
|
|
dependencies: dependencies
|
|
)
|
|
|
|
jobRunner.upsert(
|
|
db,
|
|
job: job2,
|
|
canStartJob: true,
|
|
dependencies: dependencies
|
|
)
|
|
}
|
|
|
|
// Wait for there to be data and the validate the filtering works
|
|
expect(jobRunner.detailsFor(state: .pending))
|
|
.toEventually(
|
|
equal([101: try! JSONEncoder().encode(jobDetails)]),
|
|
timeout: .milliseconds(10)
|
|
)
|
|
expect(Array(jobRunner.details().keys).sorted()).to(equal([100, 101]))
|
|
}
|
|
|
|
it("can filter to specific variants") {
|
|
dependencies.date = Date().addingTimeInterval(20 / 1000) // Complete job after delay
|
|
jobRunner.appDidFinishLaunching(dependencies: dependencies)
|
|
|
|
mockStorage.write { db in
|
|
try job1.insert(db)
|
|
try job2.insert(db)
|
|
|
|
jobRunner.upsert(
|
|
db,
|
|
job: job1,
|
|
canStartJob: true,
|
|
dependencies: dependencies
|
|
)
|
|
|
|
jobRunner.upsert(
|
|
db,
|
|
job: job2,
|
|
canStartJob: true,
|
|
dependencies: dependencies
|
|
)
|
|
}
|
|
|
|
// Wait for there to be data and the validate the filtering works
|
|
expect(jobRunner.detailsFor(variant: .attachmentUpload))
|
|
.toEventually(
|
|
equal([101: try! JSONEncoder().encode(jobDetails)]),
|
|
timeout: .milliseconds(10)
|
|
)
|
|
expect(Array(jobRunner.details().keys).sorted()).to(equal([100, 101]))
|
|
}
|
|
|
|
it("includes non blocking jobs") {
|
|
jobRunner.appDidFinishLaunching(dependencies: dependencies)
|
|
|
|
dependencies.date = Date().addingTimeInterval(20 / 1000) // Complete job after delay
|
|
|
|
mockStorage.write { db in
|
|
jobRunner.upsert(
|
|
db,
|
|
job: job2,
|
|
canStartJob: true,
|
|
dependencies: dependencies
|
|
)
|
|
}
|
|
|
|
expect(jobRunner.detailsFor(state: .running, variant: .attachmentUpload))
|
|
.toEventually(
|
|
equal([101: try! JSONEncoder().encode(jobDetails)]),
|
|
timeout: .milliseconds(10)
|
|
)
|
|
}
|
|
|
|
it("includes blocking jobs") {
|
|
job2 = Job(
|
|
id: 101,
|
|
failureCount: 0,
|
|
variant: .attachmentUpload,
|
|
behaviour: .runOnceNextLaunch,
|
|
shouldBlock: true,
|
|
shouldSkipLaunchBecomeActive: false,
|
|
nextRunTimestamp: 0,
|
|
threadId: nil,
|
|
interactionId: nil,
|
|
details: try! JSONEncoder().encode(jobDetails)
|
|
)
|
|
|
|
mockStorage.write { db in
|
|
try job2.insert(db)
|
|
|
|
jobRunner.upsert(
|
|
db,
|
|
job: job2,
|
|
canStartJob: true,
|
|
dependencies: dependencies
|
|
)
|
|
}
|
|
|
|
dependencies.date = Date().addingTimeInterval(20 / 1000) // Complete job after delay
|
|
jobRunner.appDidFinishLaunching(dependencies: dependencies)
|
|
|
|
expect(jobRunner.detailsFor(state: .running, variant: .attachmentUpload))
|
|
.toEventually(
|
|
equal([101: try! JSONEncoder().encode(jobDetails)]),
|
|
timeout: .milliseconds(10)
|
|
)
|
|
}
|
|
}
|
|
|
|
// MARK: ---- by checking for an existing job
|
|
|
|
context("by checking for an existing job") {
|
|
beforeEach {
|
|
jobRunner.setExecutor(TestSuccessfulJob.self, for: .attachmentUpload)
|
|
}
|
|
|
|
it("returns false for a queue that doesn't exist") {
|
|
jobRunner = JobRunner(
|
|
isTestingJobRunner: true,
|
|
variantsToExclude: [.attachmentUpload],
|
|
dependencies: dependencies
|
|
)
|
|
|
|
expect(jobRunner.hasJob(of: .attachmentUpload, with: jobDetails))
|
|
.to(beFalse())
|
|
}
|
|
|
|
it("returns false when the provided details fail to decode") {
|
|
expect(jobRunner.hasJob(of: .attachmentUpload, with: InvalidDetails()))
|
|
.to(beFalse())
|
|
}
|
|
|
|
it("returns false when there is not a pending or running job") {
|
|
expect(jobRunner.hasJob(of: .attachmentUpload, with: jobDetails))
|
|
.to(beFalse())
|
|
}
|
|
|
|
it("returns true when there is a pending job") {
|
|
mockStorage.write { db in
|
|
jobRunner.upsert(
|
|
db,
|
|
job: job2,
|
|
/// The `canStartJob` value needs to be `true` for the job to be added to the queue but as
|
|
/// long as `appDidFinishLaunching` hasn't been called it won't actually start running and
|
|
/// as a result we can test the "pending" state
|
|
canStartJob: true,
|
|
dependencies: dependencies
|
|
)
|
|
}
|
|
|
|
expect(Array(jobRunner.detailsFor(state: .pending, variant: .attachmentUpload).keys))
|
|
.toEventually(
|
|
equal([101]),
|
|
timeout: .milliseconds(10)
|
|
)
|
|
expect(jobRunner.hasJob(of: .attachmentUpload, with: jobDetails))
|
|
.to(beTrue())
|
|
}
|
|
|
|
it("returns true when there is a running job") {
|
|
jobRunner.appDidFinishLaunching(dependencies: dependencies)
|
|
|
|
dependencies.date = Date().addingTimeInterval(20 / 1000) // Complete job after delay
|
|
|
|
mockStorage.write { db in
|
|
jobRunner.upsert(
|
|
db,
|
|
job: job2,
|
|
canStartJob: true,
|
|
dependencies: dependencies
|
|
)
|
|
}
|
|
|
|
expect(Array(jobRunner.detailsFor(state: .running, variant: .attachmentUpload).keys))
|
|
.toEventually(
|
|
equal([101]),
|
|
timeout: .milliseconds(10)
|
|
)
|
|
expect(jobRunner.hasJob(of: .attachmentUpload, with: jobDetails))
|
|
.to(beTrue())
|
|
}
|
|
|
|
it("returns true when there is a blocking job") {
|
|
job2 = Job(
|
|
id: 101,
|
|
failureCount: 0,
|
|
variant: .attachmentUpload,
|
|
behaviour: .runOnceNextLaunch,
|
|
shouldBlock: true,
|
|
shouldSkipLaunchBecomeActive: false,
|
|
nextRunTimestamp: 0,
|
|
threadId: nil,
|
|
interactionId: nil,
|
|
details: try! JSONEncoder().encode(jobDetails)
|
|
)
|
|
|
|
mockStorage.write { db in
|
|
try job2.insert(db)
|
|
|
|
jobRunner.upsert(
|
|
db,
|
|
job: job2,
|
|
canStartJob: true,
|
|
dependencies: dependencies
|
|
)
|
|
}
|
|
|
|
dependencies.date = Date().addingTimeInterval(20 / 1000) // Complete job after delay
|
|
jobRunner.appDidFinishLaunching(dependencies: dependencies)
|
|
|
|
expect(Array(jobRunner.detailsFor(state: .running, variant: .attachmentUpload).keys))
|
|
.toEventually(
|
|
equal([101]),
|
|
timeout: .milliseconds(10)
|
|
)
|
|
expect(jobRunner.hasJob(of: .attachmentUpload, with: jobDetails))
|
|
.to(beTrue())
|
|
}
|
|
|
|
it("returns true when there is a non blocking job") {
|
|
dependencies.date = Date().addingTimeInterval(20 / 1000) // Complete job after delay
|
|
jobRunner.appDidFinishLaunching(dependencies: dependencies)
|
|
|
|
mockStorage.write { db in
|
|
jobRunner.upsert(
|
|
db,
|
|
job: job2,
|
|
canStartJob: true,
|
|
dependencies: dependencies
|
|
)
|
|
}
|
|
|
|
expect(Array(jobRunner.detailsFor(state: .running, variant: .attachmentUpload).keys))
|
|
.toEventually(
|
|
equal([101]),
|
|
timeout: .milliseconds(10)
|
|
)
|
|
expect(jobRunner.hasJob(of: .attachmentUpload, with: jobDetails))
|
|
.to(beTrue())
|
|
}
|
|
}
|
|
|
|
// MARK: ---- by being notified of app launch
|
|
|
|
context("by being notified of app launch") {
|
|
beforeEach {
|
|
jobRunner.setExecutor(TestSuccessfulJob.self, for: .messageSend)
|
|
}
|
|
|
|
it("does not start a job before getting the app launch call") {
|
|
dependencies.date = Date().addingTimeInterval(20 / 1000) // Complete job after delay
|
|
|
|
mockStorage.write { db in
|
|
jobRunner.upsert(
|
|
db,
|
|
job: job1,
|
|
canStartJob: true,
|
|
dependencies: dependencies
|
|
)
|
|
}
|
|
|
|
expect(jobRunner.isCurrentlyRunning(job1))
|
|
.toEventually(
|
|
beFalse(),
|
|
timeout: .milliseconds(10)
|
|
)
|
|
}
|
|
|
|
it("does nothing if there are no app launch jobs") {
|
|
dependencies.date = Date().addingTimeInterval(20 / 1000) // Complete job after delay
|
|
|
|
mockStorage.write { db in
|
|
jobRunner.upsert(
|
|
db,
|
|
job: job1,
|
|
canStartJob: true,
|
|
dependencies: dependencies
|
|
)
|
|
}
|
|
|
|
jobRunner.appDidFinishLaunching(dependencies: dependencies)
|
|
|
|
expect(jobRunner.isCurrentlyRunning(job1))
|
|
.toEventually(
|
|
beFalse(),
|
|
timeout: .milliseconds(10)
|
|
)
|
|
}
|
|
|
|
it("starts the job queues after completing blocking app launch jobs") {
|
|
job2 = Job(
|
|
id: 101,
|
|
failureCount: 0,
|
|
variant: .messageSend,
|
|
behaviour: .runOnceNextLaunch,
|
|
shouldBlock: true,
|
|
shouldSkipLaunchBecomeActive: false,
|
|
nextRunTimestamp: 0,
|
|
threadId: nil,
|
|
interactionId: nil,
|
|
details: nil
|
|
)
|
|
|
|
mockStorage.write { db in
|
|
try job2.insert(db)
|
|
|
|
jobRunner.upsert(
|
|
db,
|
|
job: job1,
|
|
canStartJob: true,
|
|
dependencies: dependencies
|
|
)
|
|
jobRunner.upsert(
|
|
db,
|
|
job: job2,
|
|
canStartJob: true,
|
|
dependencies: dependencies
|
|
)
|
|
}
|
|
|
|
// Not currently running
|
|
expect(jobRunner.isCurrentlyRunning(job1))
|
|
.toEventually(
|
|
beFalse(),
|
|
timeout: .milliseconds(10)
|
|
)
|
|
expect(jobRunner.isCurrentlyRunning(job2)).to(beFalse())
|
|
|
|
// Make sure it starts
|
|
dependencies.date = Date().addingTimeInterval(20 / 1000) // Complete job after delay
|
|
jobRunner.appDidFinishLaunching(dependencies: dependencies)
|
|
|
|
// Blocking job running but blocked job not
|
|
expect(jobRunner.isCurrentlyRunning(job2))
|
|
.toEventually(
|
|
beTrue(),
|
|
timeout: .milliseconds(10)
|
|
)
|
|
expect(jobRunner.isCurrentlyRunning(job1)).to(beFalse())
|
|
|
|
// Blocked job eventually starts
|
|
dependencies.date = Date().addingTimeInterval(20 / 1000) // Complete job after delay
|
|
expect(jobRunner.isCurrentlyRunning(job1))
|
|
.toEventually(
|
|
beTrue(),
|
|
timeout: .milliseconds(20)
|
|
)
|
|
}
|
|
|
|
it("starts the job queues alongside non blocking app launch jobs") {
|
|
job2 = Job(
|
|
id: 101,
|
|
failureCount: 0,
|
|
variant: .messageSend,
|
|
behaviour: .runOnceNextLaunch,
|
|
shouldBlock: false,
|
|
shouldSkipLaunchBecomeActive: false,
|
|
nextRunTimestamp: 0,
|
|
threadId: nil,
|
|
interactionId: nil,
|
|
details: nil
|
|
)
|
|
|
|
mockStorage.write { db in
|
|
try job2.insert(db)
|
|
|
|
jobRunner.upsert(
|
|
db,
|
|
job: job1,
|
|
canStartJob: true,
|
|
dependencies: dependencies
|
|
)
|
|
jobRunner.upsert(
|
|
db,
|
|
job: job2,
|
|
canStartJob: true,
|
|
dependencies: dependencies
|
|
)
|
|
}
|
|
|
|
// Not currently running
|
|
expect(jobRunner.isCurrentlyRunning(job1))
|
|
.toEventually(
|
|
beFalse(),
|
|
timeout: .milliseconds(10)
|
|
)
|
|
|
|
// Make sure it starts
|
|
dependencies.date = Date().addingTimeInterval(20 / 1000) // Complete job after delay
|
|
jobRunner.appDidFinishLaunching(dependencies: dependencies)
|
|
|
|
expect(jobRunner.isCurrentlyRunning(job1))
|
|
.toEventually(
|
|
beTrue(),
|
|
timeout: .milliseconds(10)
|
|
)
|
|
expect(jobRunner.isCurrentlyRunning(job2))
|
|
.toEventually(
|
|
beTrue(),
|
|
timeout: .milliseconds(10)
|
|
)
|
|
}
|
|
}
|
|
|
|
// MARK: ---- by being notified of app becoming active
|
|
|
|
context("by being notified of app becoming active") {
|
|
beforeEach {
|
|
jobRunner.setExecutor(TestSuccessfulJob.self, for: .messageSend)
|
|
}
|
|
|
|
it("does not start a job before getting the app active call") {
|
|
dependencies.date = Date().addingTimeInterval(20 / 1000) // Complete job after delay
|
|
|
|
mockStorage.write { db in
|
|
jobRunner.upsert(
|
|
db,
|
|
job: job1,
|
|
canStartJob: true,
|
|
dependencies: dependencies
|
|
)
|
|
}
|
|
|
|
expect(jobRunner.isCurrentlyRunning(job1))
|
|
.toEventually(
|
|
beFalse(),
|
|
timeout: .milliseconds(10)
|
|
)
|
|
}
|
|
|
|
it("does not start the job queues if there are no app active jobs and blocking jobs are running") {
|
|
job2 = Job(
|
|
id: 101,
|
|
failureCount: 0,
|
|
variant: .messageSend,
|
|
behaviour: .runOnceNextLaunch,
|
|
shouldBlock: true,
|
|
shouldSkipLaunchBecomeActive: false,
|
|
nextRunTimestamp: 0,
|
|
threadId: nil,
|
|
interactionId: nil,
|
|
details: nil
|
|
)
|
|
|
|
mockStorage.write { db in
|
|
try job2.insert(db)
|
|
|
|
jobRunner.upsert(
|
|
db,
|
|
job: job1,
|
|
canStartJob: true,
|
|
dependencies: dependencies
|
|
)
|
|
jobRunner.upsert(
|
|
db,
|
|
job: job2,
|
|
canStartJob: true,
|
|
dependencies: dependencies
|
|
)
|
|
}
|
|
|
|
// Not currently running
|
|
expect(jobRunner.isCurrentlyRunning(job1))
|
|
.toEventually(
|
|
beFalse(),
|
|
timeout: .milliseconds(10)
|
|
)
|
|
|
|
// Start the blocking job
|
|
dependencies.date = Date().addingTimeInterval(20 / 1000) // Complete job after delay
|
|
jobRunner.appDidFinishLaunching(dependencies: dependencies)
|
|
|
|
// Make sure the other queues don't start
|
|
dependencies.date = Date().addingTimeInterval(30 / 1000) // Complete job after delay
|
|
jobRunner.appDidBecomeActive(dependencies: dependencies)
|
|
|
|
expect(jobRunner.isCurrentlyRunning(job1))
|
|
.toEventually(
|
|
beFalse(),
|
|
timeout: .milliseconds(10)
|
|
)
|
|
expect(jobRunner.isCurrentlyRunning(job1))
|
|
.toEventually(
|
|
beFalse(),
|
|
timeout: .milliseconds(20)
|
|
)
|
|
}
|
|
|
|
it("does not start the job queues if there are app active jobs and blocking jobs are running") {
|
|
job1 = Job(
|
|
id: 100,
|
|
failureCount: 0,
|
|
variant: .messageSend,
|
|
behaviour: .recurringOnActive,
|
|
shouldBlock: false,
|
|
shouldSkipLaunchBecomeActive: false,
|
|
nextRunTimestamp: 0,
|
|
threadId: nil,
|
|
interactionId: nil,
|
|
details: nil
|
|
)
|
|
job2 = Job(
|
|
id: 101,
|
|
failureCount: 0,
|
|
variant: .messageSend,
|
|
behaviour: .runOnceNextLaunch,
|
|
shouldBlock: true,
|
|
shouldSkipLaunchBecomeActive: false,
|
|
nextRunTimestamp: 0,
|
|
threadId: nil,
|
|
interactionId: nil,
|
|
details: nil
|
|
)
|
|
|
|
mockStorage.write { db in
|
|
try job1.insert(db)
|
|
try job2.insert(db)
|
|
|
|
jobRunner.upsert(
|
|
db,
|
|
job: job1,
|
|
canStartJob: true,
|
|
dependencies: dependencies
|
|
)
|
|
jobRunner.upsert(
|
|
db,
|
|
job: job2,
|
|
canStartJob: true,
|
|
dependencies: dependencies
|
|
)
|
|
}
|
|
|
|
// Not currently running
|
|
expect(jobRunner.isCurrentlyRunning(job1))
|
|
.toEventually(
|
|
beFalse(),
|
|
timeout: .milliseconds(10)
|
|
)
|
|
|
|
// Start the blocking queue
|
|
dependencies.date = Date().addingTimeInterval(20 / 1000) // Complete job after delay
|
|
jobRunner.appDidFinishLaunching(dependencies: dependencies)
|
|
|
|
// Make sure the other queues don't start
|
|
dependencies.date = Date().addingTimeInterval(30 / 1000) // Complete job after delay
|
|
jobRunner.appDidBecomeActive(dependencies: dependencies)
|
|
|
|
expect(jobRunner.isCurrentlyRunning(job1))
|
|
.toEventually(
|
|
beFalse(),
|
|
timeout: .milliseconds(10)
|
|
)
|
|
expect(jobRunner.isCurrentlyRunning(job1))
|
|
.toEventually(
|
|
beFalse(),
|
|
timeout: .milliseconds(20)
|
|
)
|
|
}
|
|
|
|
it("starts the job queues if there are no app active jobs") {
|
|
dependencies.date = Date().addingTimeInterval(20 / 1000) // Complete job after delay
|
|
|
|
mockStorage.write { db in
|
|
jobRunner.upsert(
|
|
db,
|
|
job: job1,
|
|
canStartJob: true,
|
|
dependencies: dependencies
|
|
)
|
|
}
|
|
|
|
jobRunner.appDidBecomeActive(dependencies: dependencies)
|
|
|
|
expect(jobRunner.isCurrentlyRunning(job1))
|
|
.toEventually(
|
|
beTrue(),
|
|
timeout: .milliseconds(10)
|
|
)
|
|
}
|
|
|
|
it("starts the job queues if there are app active jobs") {
|
|
job1 = Job(
|
|
id: 100,
|
|
failureCount: 0,
|
|
variant: .messageSend,
|
|
behaviour: .recurringOnActive,
|
|
shouldBlock: false,
|
|
shouldSkipLaunchBecomeActive: false,
|
|
nextRunTimestamp: 0,
|
|
threadId: nil,
|
|
interactionId: nil,
|
|
details: nil
|
|
)
|
|
job2 = Job(
|
|
id: 101,
|
|
failureCount: 0,
|
|
variant: .messageSend,
|
|
behaviour: .runOnce,
|
|
shouldBlock: false,
|
|
shouldSkipLaunchBecomeActive: false,
|
|
nextRunTimestamp: 0,
|
|
threadId: nil,
|
|
interactionId: nil,
|
|
details: nil
|
|
)
|
|
|
|
mockStorage.write { db in
|
|
try job1.insert(db)
|
|
try job2.insert(db)
|
|
|
|
jobRunner.upsert(
|
|
db,
|
|
job: job1,
|
|
canStartJob: true,
|
|
dependencies: dependencies
|
|
)
|
|
jobRunner.upsert(
|
|
db,
|
|
job: job2,
|
|
canStartJob: true,
|
|
dependencies: dependencies
|
|
)
|
|
}
|
|
|
|
// Not currently running
|
|
expect(jobRunner.isCurrentlyRunning(job1))
|
|
.toEventually(
|
|
beFalse(),
|
|
timeout: .milliseconds(10)
|
|
)
|
|
|
|
// Make sure the queues are started
|
|
dependencies.date = Date().addingTimeInterval(20 / 1000) // Complete job after delay
|
|
jobRunner.appDidBecomeActive(dependencies: dependencies)
|
|
|
|
expect(jobRunner.isCurrentlyRunning(job1))
|
|
.toEventually(
|
|
beTrue(),
|
|
timeout: .milliseconds(10)
|
|
)
|
|
expect(jobRunner.isCurrentlyRunning(job2)).to(beTrue())
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: -- when running jobs
|
|
|
|
context("when running jobs") {
|
|
beforeEach {
|
|
jobRunner.setExecutor(TestSuccessfulJob.self, for: .messageSend)
|
|
jobRunner.setExecutor(TestSuccessfulJob.self, for: .attachmentUpload)
|
|
jobRunner.appDidFinishLaunching(dependencies: dependencies)
|
|
}
|
|
|
|
// MARK: ---- with dependencies
|
|
|
|
context("with dependencies") {
|
|
it("starts dependencies first") {
|
|
dependencies.date = Date().addingTimeInterval(20 / 1000) // Complete job after delay
|
|
|
|
mockStorage.write { db in
|
|
try job1.insert(db)
|
|
try job2.insert(db)
|
|
try JobDependencies(jobId: job1.id!, dependantId: job2.id!).insert(db)
|
|
|
|
jobRunner.upsert(db, job: job1, canStartJob: true, dependencies: dependencies)
|
|
}
|
|
|
|
// Make sure the dependency is run
|
|
expect(Array(jobRunner.detailsFor(state: .running, variant: .attachmentUpload).keys))
|
|
.toEventually(
|
|
equal([101]),
|
|
timeout: .milliseconds(10)
|
|
)
|
|
}
|
|
|
|
it("removes the initial job from the queue") {
|
|
dependencies.date = Date().addingTimeInterval(20 / 1000) // Complete job after delay
|
|
|
|
mockStorage.write { db in
|
|
try job1.insert(db)
|
|
try job2.insert(db)
|
|
try JobDependencies(jobId: job1.id!, dependantId: job2.id!).insert(db)
|
|
|
|
jobRunner.upsert(db, job: job1, canStartJob: true, dependencies: dependencies)
|
|
}
|
|
|
|
// Make sure the initial job is removed from the queue
|
|
expect(Array(jobRunner.detailsFor(state: .running, variant: .attachmentUpload).keys))
|
|
.toEventually(
|
|
equal([101]),
|
|
timeout: .milliseconds(10)
|
|
)
|
|
expect(jobRunner.detailsFor(state: .running, variant: .messageSend).keys).toNot(contain(100))
|
|
}
|
|
|
|
it("starts the initial job when the dependencies succeed") {
|
|
dependencies.date = Date().addingTimeInterval(20 / 1000) // Complete job after delay
|
|
|
|
mockStorage.write { db in
|
|
try job1.insert(db)
|
|
try job2.insert(db)
|
|
try JobDependencies(jobId: job1.id!, dependantId: job2.id!).insert(db)
|
|
|
|
jobRunner.upsert(db, job: job1, canStartJob: true, dependencies: dependencies)
|
|
}
|
|
|
|
// Make sure the dependency is run
|
|
expect(Array(jobRunner.detailsFor(state: .running, variant: .attachmentUpload).keys))
|
|
.toEventually(
|
|
equal([101]),
|
|
timeout: .milliseconds(10)
|
|
)
|
|
expect(jobRunner.detailsFor(state: .running, variant: .messageSend).keys).toNot(contain(100))
|
|
|
|
// Make sure the initial job starts
|
|
dependencies.date = Date().addingTimeInterval(20 / 1000) // Complete job after delay
|
|
expect(Array(jobRunner.detailsFor(state: .running, variant: .messageSend).keys))
|
|
.toEventually(
|
|
equal([100]),
|
|
timeout: .milliseconds(20)
|
|
)
|
|
}
|
|
|
|
it("does not start the initial job if the dependencies fail") {
|
|
jobRunner.setExecutor(TestFailedJob.self, for: .attachmentUpload)
|
|
|
|
dependencies.date = Date().addingTimeInterval(20 / 1000) // Fail job after delay
|
|
|
|
mockStorage.write { db in
|
|
try job1.insert(db)
|
|
try job2.insert(db)
|
|
try JobDependencies(jobId: job1.id!, dependantId: job2.id!).insert(db)
|
|
|
|
jobRunner.upsert(db, job: job1, canStartJob: true, dependencies: dependencies)
|
|
}
|
|
|
|
// Make sure the dependency is run
|
|
expect(Array(jobRunner.detailsFor(state: .running, variant: .attachmentUpload).keys))
|
|
.toEventually(
|
|
equal([101]),
|
|
timeout: .milliseconds(10)
|
|
)
|
|
expect(jobRunner.detailsFor(state: .running, variant: .messageSend).keys).toNot(contain(100))
|
|
|
|
// Make sure there are no running jobs
|
|
expect(Array(jobRunner.detailsFor(state: .running).keys))
|
|
.toEventually(
|
|
beEmpty(),
|
|
timeout: .milliseconds(20)
|
|
)
|
|
}
|
|
|
|
it("does not delete the initial job if the dependencies fail") {
|
|
jobRunner.setExecutor(TestFailedJob.self, for: .attachmentUpload)
|
|
|
|
dependencies.date = Date().addingTimeInterval(20 / 1000) // Fail job after delay
|
|
|
|
mockStorage.write { db in
|
|
try job1.insert(db)
|
|
try job2.insert(db)
|
|
try JobDependencies(jobId: job1.id!, dependantId: job2.id!).insert(db)
|
|
|
|
jobRunner.upsert(db, job: job1, canStartJob: true, dependencies: dependencies)
|
|
}
|
|
|
|
// Make sure the dependency is run
|
|
expect(Array(jobRunner.detailsFor(state: .running, variant: .attachmentUpload).keys))
|
|
.toEventually(
|
|
equal([101]),
|
|
timeout: .milliseconds(10)
|
|
)
|
|
expect(jobRunner.detailsFor(state: .running, variant: .messageSend).keys).toNot(contain(100))
|
|
|
|
// Make sure there are no running jobs
|
|
dependencies.date = Date().addingTimeInterval(20 / 1000) // Delay subsequent runs
|
|
expect(Array(jobRunner.detailsFor(state: .running, variant: .attachmentUpload).keys))
|
|
.toEventually(
|
|
beEmpty(),
|
|
timeout: .milliseconds(20)
|
|
)
|
|
|
|
// Stop the queues so it doesn't run out of retry attempts
|
|
jobRunner.stopAndClearPendingJobs(exceptForVariant: nil, onComplete: nil)
|
|
|
|
// Make sure the jobs still exist
|
|
expect(mockStorage.read { db in try Job.fetchCount(db) }).to(equal(2))
|
|
}
|
|
|
|
it("deletes the initial job if the dependencies permanently fail") {
|
|
jobRunner.setExecutor(TestPermanentFailureJob.self, for: .attachmentUpload)
|
|
|
|
dependencies.date = Date().addingTimeInterval(20 / 1000) // Fail job after delay
|
|
|
|
mockStorage.write { db in
|
|
try job1.insert(db)
|
|
try job2.insert(db)
|
|
try JobDependencies(jobId: job1.id!, dependantId: job2.id!).insert(db)
|
|
|
|
jobRunner.upsert(db, job: job1, canStartJob: true, dependencies: dependencies)
|
|
}
|
|
|
|
// Make sure the dependency is run
|
|
expect(Array(jobRunner.detailsFor(state: .running, variant: .attachmentUpload).keys))
|
|
.toEventually(
|
|
equal([101]),
|
|
timeout: .milliseconds(10)
|
|
)
|
|
expect(jobRunner.detailsFor(state: .running, variant: .messageSend).keys).toNot(contain(100))
|
|
|
|
// Make sure there are no running jobs
|
|
expect(Array(jobRunner.detailsFor(state: .running, variant: .attachmentUpload).keys))
|
|
.toEventually(
|
|
beEmpty(),
|
|
timeout: .milliseconds(20)
|
|
)
|
|
|
|
// Make sure the jobs were deleted
|
|
expect(mockStorage.read { db in try Job.fetchCount(db) }).to(equal(0))
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
}
|