Mailspring/spec-inbox/stores/task-queue-spec.coffee
Evan Morikawa 132263c38c refactor(tasks): refactor task store
Summary:
task store fixes

more task store fixes

check for blocked tasks

retry tasks

add spec descriptions

more test stubs

Test Plan: edgehill --test

Reviewers: bengotow

Reviewed By: bengotow

Differential Revision: https://review.inboxapp.com/D1209
2015-02-20 12:24:15 -08:00

401 lines
14 KiB
CoffeeScript

Actions = require '../../src/flux/actions'
TaskQueue = require '../../src/flux/stores/task-queue'
Task = require '../../src/flux/tasks/task'
{isTempId} = require '../../src/flux/models/utils'
{APIError,
OfflineError,
TimeoutError} = require '../../src/flux/errors'
class TaskSubclassA extends Task
constructor: (val) -> @aProp = val # forgot to call super
class TaskSubclassB extends Task
constructor: (val) -> @bProp = val; super
describe "TaskQueue", ->
makeUnstartedTask = (task) ->
TaskQueue._initializeTask(task)
return task
makeLocalStarted = (task) ->
TaskQueue._initializeTask(task)
task.queueState.isProcessing = true
return task
makeLocalFailed = (task) ->
TaskQueue._initializeTask(task)
task.queueState.performedLocal = Date.now()
return task
makeRemoteStarted = (task) ->
TaskQueue._initializeTask(task)
task.queueState.isProcessing = true
task.queueState.remoteAttempts = 1
task.queueState.performedLocal = Date.now()
return task
makeRemoteSuccess = (task) ->
TaskQueue._initializeTask(task)
task.queueState.remoteAttempts = 1
task.queueState.performedLocal = Date.now()
task.queueState.performedRemote = Date.now()
return task
makeRemoteFailed = (task) ->
TaskQueue._initializeTask(task)
task.queueState.remoteAttempts = 1
task.queueState.performedLocal = Date.now()
return task
beforeEach ->
TaskQueue._onlineStatus = true
@task = new Task()
@unstartedTask = makeUnstartedTask(new Task())
@localStarted = makeLocalStarted(new Task())
@localFailed = makeLocalFailed(new Task())
@remoteStarted = makeRemoteStarted(new Task())
@remoteSuccess = makeRemoteSuccess(new Task())
@remoteFailed = makeRemoteFailed(new Task())
unstartedTask = (task) ->
taks.queueState.shouldRetry = false
taks.queueState.isProcessing = false
taks.queueState.remoteAttempts = 0
taks.queueState.perfomredLocal = false
taks.queueState.performedRemote = false
taks.queueState.notifiedOffline = false
startedTask = (task) ->
taks.queueState.shouldRetry = false
taks.queueState.isProcessing = true
taks.queueState.remoteAttempts = 0
taks.queueState.perfomredLocal = false
taks.queueState.performedRemote = false
taks.queueState.notifiedOffline = false
localTask = (task) ->
taks.queueState.shouldRetry = false
taks.queueState.isProcessing = true
taks.queueState.remoteAttempts = 0
taks.queueState.perfomredLocal = false
taks.queueState.performedRemote = false
taks.queueState.notifiedOffline = false
afterEach ->
TaskQueue._queue = []
TaskQueue._completed = []
describe "enqueue", ->
it "makes sure you've queued a real task", ->
expect( -> TaskQueue.enqueue("asamw")).toThrow()
it "adds it to the queue", ->
TaskQueue.enqueue(@task)
expect(TaskQueue._queue.length).toBe 1
it "notifies the queue should be processed", ->
spyOn(TaskQueue, "_processTask")
spyOn(TaskQueue, "_processQueue").andCallThrough()
TaskQueue.enqueue(@task)
expect(TaskQueue._processQueue).toHaveBeenCalled()
expect(TaskQueue._processTask).toHaveBeenCalledWith(@task)
expect(TaskQueue._processTask.calls.length).toBe 1
it "ensures all tasks have an id", ->
TaskQueue.enqueue(new TaskSubclassA())
TaskQueue.enqueue(new TaskSubclassB())
expect(isTempId(TaskQueue._queue[0].id)).toBe true
expect(isTempId(TaskQueue._queue[1].id)).toBe true
it "dequeues Obsolete tasks", ->
class KillsTaskA extends Task
constructor: ->
shouldDequeueOtherTask: (other) -> other instanceof TaskSubclassA
taskToDie = makeRemoteFailed(new TaskSubclassA())
spyOn(TaskQueue, "dequeue").andCallThrough()
spyOn(taskToDie, "abort")
TaskQueue._queue = [taskToDie, @remoteFailed]
TaskQueue.enqueue(new KillsTaskA())
expect(TaskQueue._queue.length).toBe 2
expect(TaskQueue.dequeue).toHaveBeenCalledWith(taskToDie, silent: true)
expect(TaskQueue.dequeue.calls.length).toBe 1
expect(taskToDie.abort).toHaveBeenCalled()
describe "dequeue", ->
beforeEach ->
TaskQueue._queue = [@unstartedTask,
@localStarted,
@remoteStarted,
@remoteFailed]
it "grabs the task by object", ->
found = TaskQueue._parseArgs(@remoteStarted)
expect(found).toBe @remoteStarted
it "grabs the task by id", ->
found = TaskQueue._parseArgs(@remoteStarted.id)
expect(found).toBe @remoteStarted
it "throws an error if the task isn't found", ->
expect( -> TaskQueue.dequeue("bad")).toThrow()
it "doesn't abort unstarted tasks", ->
spyOn(@unstartedTask, "abort")
TaskQueue.dequeue(@unstartedTask, silent: true)
expect(@unstartedTask.abort).not.toHaveBeenCalled()
it "aborts local tasks in progress", ->
spyOn(@localStarted, "abort")
TaskQueue.dequeue(@localStarted, silent: true)
expect(@localStarted.abort).toHaveBeenCalled()
it "aborts remote tasks in progress", ->
spyOn(@remoteStarted, "abort")
TaskQueue.dequeue(@remoteStarted, silent: true)
expect(@remoteStarted.abort).toHaveBeenCalled()
it "calls cleanup on aborted tasks", ->
spyOn(@remoteStarted, "cleanup")
TaskQueue.dequeue(@remoteStarted, silent: true)
expect(@remoteStarted.cleanup).toHaveBeenCalled()
it "aborts stalled remote tasks", ->
spyOn(@remoteFailed, "abort")
TaskQueue.dequeue(@remoteFailed, silent: true)
expect(@remoteFailed.abort).toHaveBeenCalled()
it "doesn't abort if it's fully done", ->
TaskQueue._queue.push @remoteSuccess
spyOn(@remoteSuccess, "abort")
TaskQueue.dequeue(@remoteSuccess, silent: true)
expect(@remoteSuccess.abort).not.toHaveBeenCalled()
it "moves it from the queue", ->
TaskQueue.dequeue(@remoteStarted, silent: true)
expect(TaskQueue._queue.length).toBe 3
expect(TaskQueue._completed.length).toBe 1
it "marks it as no longer processing", ->
TaskQueue.dequeue(@remoteStarted, silent: true)
expect(@remoteStarted.queueState.isProcessing).toBe false
it "notifies the queue has been updated", ->
spyOn(TaskQueue, "_processQueue")
TaskQueue.dequeue(@remoteStarted)
expect(TaskQueue._processQueue).toHaveBeenCalled()
expect(TaskQueue._processQueue.calls.length).toBe 1
describe "process Task", ->
it "doesn't process processing tasks", ->
spyOn(@remoteStarted, "performLocal")
spyOn(@remoteStarted, "performRemote")
TaskQueue._processTask(@remoteStarted)
expect(@remoteStarted.performLocal).not.toHaveBeenCalled()
expect(@remoteStarted.performRemote).not.toHaveBeenCalled()
it "doesn't process blocked tasks", ->
class BlockedByTaskA extends Task
constructor: ->
shouldWaitForTask: (other) -> other instanceof TaskSubclassA
blockedByTask = new BlockedByTaskA()
spyOn(blockedByTask, "performLocal")
spyOn(blockedByTask, "performRemote")
blockingTask = makeRemoteFailed(new TaskSubclassA())
TaskQueue._queue = [blockingTask, @remoteFailed]
TaskQueue.enqueue(blockedByTask)
expect(TaskQueue._queue.length).toBe 3
expect(blockedByTask.performLocal).not.toHaveBeenCalled()
expect(blockedByTask.performRemote).not.toHaveBeenCalled()
it "doesn't block itself", ->
class BlockingTask extends Task
constructor: ->
shouldWaitForTask: (other) -> other instanceof BlockingTask
blockedByTask = new BlockingTask()
spyOn(blockedByTask, "performLocal")
spyOn(blockedByTask, "performRemote")
blockingTask = makeRemoteFailed(new BlockingTask())
TaskQueue._queue = [blockingTask, @remoteFailed]
TaskQueue.enqueue(blockedByTask)
expect(TaskQueue._queue.length).toBe 3
expect(blockedByTask.performLocal).not.toHaveBeenCalled()
expect(blockedByTask.performRemote).not.toHaveBeenCalled()
it "sets the processing bit", ->
spyOn(@unstartedTask, "performLocal").andCallFake -> Promise.resolve()
TaskQueue._processTask(@unstartedTask)
expect(@unstartedTask.queueState.isProcessing).toBe true
it "performs local if it's a fresh task", ->
spyOn(@unstartedTask, "performLocal").andCallFake -> Promise.resolve()
TaskQueue._processTask(@unstartedTask)
expect(@unstartedTask.performLocal).toHaveBeenCalled()
describe "performLocal", ->
it "on success it marks it as complete with the timestamp", ->
spyOn(@unstartedTask, "performLocal").andCallFake -> Promise.resolve()
spyOn(@unstartedTask, "performRemote").andCallFake -> Promise.resolve()
runs ->
TaskQueue.enqueue(@unstartedTask)
waitsFor =>
@unstartedTask.queueState.performedLocal isnt false
runs ->
expect(@unstartedTask.queueState.performedLocal).toBeGreaterThan 0
it "throws an error if it fails", ->
spyOn(@unstartedTask, "performLocal").andCallFake -> Promise.reject("boo")
spyOn(@unstartedTask, "performRemote").andCallFake -> Promise.resolve()
runs ->
TaskQueue.enqueue(@unstartedTask)
waitsFor =>
@unstartedTask.queueState.isProcessing == false
runs ->
expect(@unstartedTask.queueState.localError).toBe "boo"
expect(@unstartedTask.performLocal).toHaveBeenCalled()
expect(@unstartedTask.performRemote).not.toHaveBeenCalled()
it "dequeues the task if it fails locally", ->
spyOn(@unstartedTask, "performLocal").andCallFake -> Promise.reject("boo")
spyOn(@unstartedTask, "performRemote").andCallFake -> Promise.resolve()
runs ->
TaskQueue.enqueue(@unstartedTask)
waitsFor =>
@unstartedTask.queueState.isProcessing == false
runs ->
expect(TaskQueue._queue.length).toBe 0
expect(TaskQueue._completed.length).toBe 1
describe "performRemote", ->
beforeEach ->
spyOn(@unstartedTask, "performLocal").andCallFake -> Promise.resolve()
it "performs remote properly", ->
spyOn(@unstartedTask, "performRemote").andCallFake -> Promise.resolve()
runs ->
TaskQueue.enqueue(@unstartedTask)
waitsFor =>
@unstartedTask.queueState.performedRemote isnt false
runs ->
expect(@unstartedTask.performLocal).toHaveBeenCalled()
expect(@unstartedTask.performRemote).toHaveBeenCalled()
it "dequeues on success", ->
spyOn(@unstartedTask, "performRemote").andCallFake -> Promise.resolve()
runs ->
TaskQueue.enqueue(@unstartedTask)
waitsFor =>
@unstartedTask.queueState.isProcessing is false and
@unstartedTask.queueState.performedRemote > 0
runs ->
expect(TaskQueue._queue.length).toBe 0
expect(TaskQueue._completed.length).toBe 1
it "notifies we're offline the first time", ->
spyOn(TaskQueue, "_isOnline").andReturn false
spyOn(@unstartedTask, "performRemote").andCallFake -> Promise.resolve()
spyOn(@unstartedTask, "onError")
runs ->
TaskQueue.enqueue(@unstartedTask)
waitsFor =>
@unstartedTask.queueState.notifiedOffline == true
runs ->
expect(@unstartedTask.performLocal).toHaveBeenCalled()
expect(@unstartedTask.performRemote).not.toHaveBeenCalled()
expect(@unstartedTask.onError).toHaveBeenCalled()
expect(@unstartedTask.queueState.isProcessing).toBe false
expect(@unstartedTask.onError.calls[0].args[0] instanceof OfflineError).toBe true
it "doesn't notify we're offline the second+ time", ->
spyOn(TaskQueue, "_isOnline").andReturn false
spyOn(@remoteFailed, "performLocal").andCallFake -> Promise.resolve()
spyOn(@remoteFailed, "performRemote").andCallFake -> Promise.resolve()
spyOn(@remoteFailed, "onError")
@remoteFailed.queueState.notifiedOffline = true
TaskQueue._queue = [@remoteFailed]
runs ->
TaskQueue._processQueue()
waitsFor =>
@remoteFailed.queueState.isProcessing is false
runs ->
expect(@remoteFailed.performLocal).not.toHaveBeenCalled()
expect(@remoteFailed.performRemote).not.toHaveBeenCalled()
expect(@remoteFailed.onError).not.toHaveBeenCalled()
it "marks performedRemote on success", ->
spyOn(@unstartedTask, "performRemote").andCallFake -> Promise.resolve()
runs ->
TaskQueue.enqueue(@unstartedTask)
waitsFor =>
@unstartedTask.queueState.performedRemote isnt false
runs ->
expect(@unstartedTask.queueState.performedRemote).toBeGreaterThan 0
it "on failure it notifies of the error", ->
err = new APIError
spyOn(@unstartedTask, "performRemote").andCallFake -> Promise.reject(err)
spyOn(@unstartedTask, "onError")
runs ->
TaskQueue.enqueue(@unstartedTask)
waitsFor =>
@unstartedTask.queueState.isProcessing is false
runs ->
expect(@unstartedTask.performLocal).toHaveBeenCalled()
expect(@unstartedTask.performRemote).toHaveBeenCalled()
expect(@unstartedTask.onError).toHaveBeenCalledWith(err)
it "dequeues on failure", ->
err = new APIError
spyOn(@unstartedTask, "performRemote").andCallFake -> Promise.reject(err)
runs ->
TaskQueue.enqueue(@unstartedTask)
waitsFor =>
@unstartedTask.queueState.isProcessing is false
runs ->
expect(TaskQueue._queue.length).toBe 0
expect(TaskQueue._completed.length).toBe 1
it "on failure it sets the appropriate bits", ->
err = new APIError
spyOn(@unstartedTask, "performRemote").andCallFake -> Promise.reject(err)
spyOn(@unstartedTask, "onError")
runs ->
TaskQueue.enqueue(@unstartedTask)
waitsFor =>
@unstartedTask.queueState.isProcessing is false
runs ->
expect(@unstartedTask.queueState.notifiedOffline).toBe false
expect(@unstartedTask.queueState.remoteError).toBe err
describe "under stress", ->
beforeEach ->
TaskQueue._queue = [@unstartedTask,
@remoteFailed]
it "when all tasks pass it processes all items", ->
for task in TaskQueue._queue
spyOn(task, "performLocal").andCallFake -> Promise.resolve()
spyOn(task, "performRemote").andCallFake -> Promise.resolve()
runs ->
TaskQueue.enqueue(new Task)
waitsFor ->
TaskQueue._queue.length is 0
runs ->
expect(TaskQueue._completed.length).toBe 3