mirror of
https://github.com/Foundry376/Mailspring.git
synced 2024-11-14 21:57:55 +08:00
401 lines
14 KiB
CoffeeScript
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
|