|
|
@@ -0,0 +1,499 @@
|
|
|
+SandboxedModule = require('sandboxed-module')
|
|
|
+sinon = require('sinon')
|
|
|
+require('chai').should()
|
|
|
+expect = require('chai').expect
|
|
|
+require "coffee-script"
|
|
|
+modulePath = require('path').join __dirname, '../../../app/coffee/DockerRunner'
|
|
|
+Path = require "path"
|
|
|
+
|
|
|
+describe "DockerRunner", ->
|
|
|
+ beforeEach ->
|
|
|
+ @container = container = {}
|
|
|
+ @DockerRunner = SandboxedModule.require modulePath, requires:
|
|
|
+ "settings-sharelatex": @Settings =
|
|
|
+ clsi: docker: {}
|
|
|
+ path: {}
|
|
|
+ "logger-sharelatex": @logger = {
|
|
|
+ log: sinon.stub(),
|
|
|
+ error: sinon.stub(),
|
|
|
+ info: sinon.stub(),
|
|
|
+ warn: sinon.stub()
|
|
|
+ }
|
|
|
+ "dockerode": class Docker
|
|
|
+ getContainer: sinon.stub().returns(container)
|
|
|
+ createContainer: sinon.stub().yields(null, container)
|
|
|
+ listContainers: sinon.stub()
|
|
|
+ "fs": @fs = { stat: sinon.stub().yields(null,{isDirectory:()->true}) }
|
|
|
+ "./Metrics":
|
|
|
+ Timer: class Timer
|
|
|
+ done: () ->
|
|
|
+ "./LockManager":
|
|
|
+ runWithLock: (key, runner, callback) -> runner(callback)
|
|
|
+ @Docker = Docker
|
|
|
+ @getContainer = Docker::getContainer
|
|
|
+ @createContainer = Docker::createContainer
|
|
|
+ @listContainers = Docker::listContainers
|
|
|
+
|
|
|
+ @directory = "/local/compile/directory"
|
|
|
+ @mainFile = "main-file.tex"
|
|
|
+ @compiler = "pdflatex"
|
|
|
+ @image = "example.com/sharelatex/image:2016.2"
|
|
|
+ @env = {}
|
|
|
+ @callback = sinon.stub()
|
|
|
+ @project_id = "project-id-123"
|
|
|
+ @volumes =
|
|
|
+ "/local/compile/directory": "/compile"
|
|
|
+ @Settings.clsi.docker.image = @defaultImage = "default-image"
|
|
|
+ @Settings.clsi.docker.env = PATH: "mock-path"
|
|
|
+
|
|
|
+ describe "run", ->
|
|
|
+ beforeEach (done)->
|
|
|
+ @DockerRunner._getContainerOptions = sinon.stub().returns(@options = {mockoptions: "foo"})
|
|
|
+ @DockerRunner._fingerprintContainer = sinon.stub().returns(@fingerprint = "fingerprint")
|
|
|
+
|
|
|
+ @name = "project-#{@project_id}-#{@fingerprint}"
|
|
|
+
|
|
|
+ @command = ["mock", "command", "--outdir=$COMPILE_DIR"]
|
|
|
+ @command_with_dir = ["mock", "command", "--outdir=/compile"]
|
|
|
+ @timeout = 42000
|
|
|
+ done()
|
|
|
+
|
|
|
+ describe "successfully", ->
|
|
|
+ beforeEach (done)->
|
|
|
+ @DockerRunner._runAndWaitForContainer = sinon.stub().callsArgWith(3, null, @output = "mock-output")
|
|
|
+ @DockerRunner.run @project_id, @command, @directory, @image, @timeout, @env, (err, output)=>
|
|
|
+ @callback(err, output)
|
|
|
+ done()
|
|
|
+
|
|
|
+ it "should generate the options for the container", ->
|
|
|
+ @DockerRunner._getContainerOptions
|
|
|
+ .calledWith(@command_with_dir, @image, @volumes, @timeout)
|
|
|
+ .should.equal true
|
|
|
+
|
|
|
+ it "should generate the fingerprint from the returned options", ->
|
|
|
+ @DockerRunner._fingerprintContainer
|
|
|
+ .calledWith(@options)
|
|
|
+ .should.equal true
|
|
|
+
|
|
|
+ it "should do the run", ->
|
|
|
+ @DockerRunner._runAndWaitForContainer
|
|
|
+ .calledWith(@options, @volumes, @timeout)
|
|
|
+ .should.equal true
|
|
|
+
|
|
|
+ it "should call the callback", ->
|
|
|
+ @callback.calledWith(null, @output).should.equal true
|
|
|
+
|
|
|
+ describe 'when path.sandboxedCompilesHostDir is set', ->
|
|
|
+
|
|
|
+ beforeEach ->
|
|
|
+ @Settings.path.sandboxedCompilesHostDir = '/some/host/dir/compiles'
|
|
|
+ @directory = '/var/lib/sharelatex/data/compiles/xyz'
|
|
|
+ @DockerRunner._runAndWaitForContainer = sinon.stub().callsArgWith(3, null, @output = "mock-output")
|
|
|
+ @DockerRunner.run @project_id, @command, @directory, @image, @timeout, @env, @callback
|
|
|
+
|
|
|
+ it 'should re-write the bind directory', ->
|
|
|
+ volumes = @DockerRunner._runAndWaitForContainer.lastCall.args[1]
|
|
|
+ expect(volumes).to.deep.equal {
|
|
|
+ '/some/host/dir/compiles/xyz': '/compile'
|
|
|
+ }
|
|
|
+
|
|
|
+ it "should call the callback", ->
|
|
|
+ @callback.calledWith(null, @output).should.equal true
|
|
|
+
|
|
|
+ describe "when the run throws an error", ->
|
|
|
+ beforeEach ->
|
|
|
+ firstTime = true
|
|
|
+ @output = "mock-output"
|
|
|
+ @DockerRunner._runAndWaitForContainer = (options, volumes, timeout, callback = (error, output)->) =>
|
|
|
+ if firstTime
|
|
|
+ firstTime = false
|
|
|
+ callback new Error("HTTP code is 500 which indicates error: server error")
|
|
|
+ else
|
|
|
+ callback(null, @output)
|
|
|
+ sinon.spy @DockerRunner, "_runAndWaitForContainer"
|
|
|
+ @DockerRunner.destroyContainer = sinon.stub().callsArg(3)
|
|
|
+ @DockerRunner.run @project_id, @command, @directory, @image, @timeout, @env, @callback
|
|
|
+
|
|
|
+ it "should do the run twice", ->
|
|
|
+ @DockerRunner._runAndWaitForContainer
|
|
|
+ .calledTwice.should.equal true
|
|
|
+
|
|
|
+ it "should destroy the container in between", ->
|
|
|
+ @DockerRunner.destroyContainer
|
|
|
+ .calledWith(@name, null)
|
|
|
+ .should.equal true
|
|
|
+
|
|
|
+ it "should call the callback", ->
|
|
|
+ @callback.calledWith(null, @output).should.equal true
|
|
|
+
|
|
|
+ describe "with no image", ->
|
|
|
+ beforeEach ->
|
|
|
+ @DockerRunner._runAndWaitForContainer = sinon.stub().callsArgWith(3, null, @output = "mock-output")
|
|
|
+ @DockerRunner.run @project_id, @command, @directory, null, @timeout, @env, @callback
|
|
|
+
|
|
|
+ it "should use the default image", ->
|
|
|
+ @DockerRunner._getContainerOptions
|
|
|
+ .calledWith(@command_with_dir, @defaultImage, @volumes, @timeout)
|
|
|
+ .should.equal true
|
|
|
+
|
|
|
+ describe "_runAndWaitForContainer", ->
|
|
|
+ beforeEach ->
|
|
|
+ @options = {mockoptions: "foo", name: @name = "mock-name"}
|
|
|
+ @DockerRunner.startContainer = (options, volumes, attachStreamHandler, callback) =>
|
|
|
+ attachStreamHandler(null, @output = "mock-output")
|
|
|
+ callback(null, @containerId = "container-id")
|
|
|
+ sinon.spy @DockerRunner, "startContainer"
|
|
|
+ @DockerRunner.waitForContainer = sinon.stub().callsArgWith(2, null, @exitCode = 42)
|
|
|
+ @DockerRunner._runAndWaitForContainer @options, @volumes, @timeout, @callback
|
|
|
+
|
|
|
+ it "should create/start the container", ->
|
|
|
+ @DockerRunner.startContainer
|
|
|
+ .calledWith(@options, @volumes)
|
|
|
+ .should.equal true
|
|
|
+
|
|
|
+ it "should wait for the container to finish", ->
|
|
|
+ @DockerRunner.waitForContainer
|
|
|
+ .calledWith(@name, @timeout)
|
|
|
+ .should.equal true
|
|
|
+
|
|
|
+ it "should call the callback with the output", ->
|
|
|
+ @callback.calledWith(null, @output).should.equal true
|
|
|
+
|
|
|
+ describe "startContainer", ->
|
|
|
+ beforeEach ->
|
|
|
+ @attachStreamHandler = sinon.stub()
|
|
|
+ @attachStreamHandler.cock = true
|
|
|
+ @options = {mockoptions: "foo", name: "mock-name"}
|
|
|
+ @container.inspect = sinon.stub().callsArgWith(0)
|
|
|
+ @DockerRunner.attachToContainer = (containerId, attachStreamHandler, cb)=>
|
|
|
+ attachStreamHandler()
|
|
|
+ cb()
|
|
|
+ sinon.spy @DockerRunner, "attachToContainer"
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ describe "when the container exists", ->
|
|
|
+ beforeEach ->
|
|
|
+ @container.inspect = sinon.stub().callsArgWith(0)
|
|
|
+ @container.start = sinon.stub().yields()
|
|
|
+
|
|
|
+ @DockerRunner.startContainer @options, @volumes, @callback, ->
|
|
|
+
|
|
|
+ it "should start the container with the given name", ->
|
|
|
+ @getContainer
|
|
|
+ .calledWith(@options.name)
|
|
|
+ .should.equal true
|
|
|
+ @container.start
|
|
|
+ .called
|
|
|
+ .should.equal true
|
|
|
+
|
|
|
+ it "should not try to create the container", ->
|
|
|
+ @createContainer.called.should.equal false
|
|
|
+
|
|
|
+ it "should attach to the container", ->
|
|
|
+ @DockerRunner.attachToContainer.called.should.equal true
|
|
|
+
|
|
|
+ it "should call the callback", ->
|
|
|
+ @callback.called.should.equal true
|
|
|
+
|
|
|
+ it "should attach before the container starts", ->
|
|
|
+ sinon.assert.callOrder(@DockerRunner.attachToContainer, @container.start)
|
|
|
+
|
|
|
+ describe "when the container does not exist", ->
|
|
|
+ beforeEach ()->
|
|
|
+ exists = false
|
|
|
+ @container.start = sinon.stub().yields()
|
|
|
+ @container.inspect = sinon.stub().callsArgWith(0, {statusCode:404})
|
|
|
+ @DockerRunner.startContainer @options, @volumes, @attachStreamHandler, @callback
|
|
|
+
|
|
|
+ it "should create the container", ->
|
|
|
+ @createContainer
|
|
|
+ .calledWith(@options)
|
|
|
+ .should.equal true
|
|
|
+
|
|
|
+ it "should call the callback and stream handler", ->
|
|
|
+ @attachStreamHandler.called.should.equal true
|
|
|
+ @callback.called.should.equal true
|
|
|
+
|
|
|
+ it "should attach to the container", ->
|
|
|
+ @DockerRunner.attachToContainer.called.should.equal true
|
|
|
+
|
|
|
+ it "should attach before the container starts", ->
|
|
|
+ sinon.assert.callOrder(@DockerRunner.attachToContainer, @container.start)
|
|
|
+
|
|
|
+
|
|
|
+ describe "when the container is already running", ->
|
|
|
+ beforeEach ->
|
|
|
+ error = new Error("HTTP code is 304 which indicates error: server error - start: Cannot start container #{@name}: The container MOCKID is already running.")
|
|
|
+ error.statusCode = 304
|
|
|
+ @container.start = sinon.stub().yields(error)
|
|
|
+ @container.inspect = sinon.stub().callsArgWith(0)
|
|
|
+ @DockerRunner.startContainer @options, @volumes, @attachStreamHandler, @callback
|
|
|
+
|
|
|
+ it "should not try to create the container", ->
|
|
|
+ @createContainer.called.should.equal false
|
|
|
+
|
|
|
+ it "should call the callback and stream handler without an error", ->
|
|
|
+ @attachStreamHandler.called.should.equal true
|
|
|
+ @callback.called.should.equal true
|
|
|
+
|
|
|
+ describe "when a volume does not exist", ->
|
|
|
+ beforeEach ()->
|
|
|
+ @fs.stat = sinon.stub().yields(new Error("no such path"))
|
|
|
+ @DockerRunner.startContainer @options, @volumes, @attachStreamHandler, @callback
|
|
|
+
|
|
|
+ it "should not try to create the container", ->
|
|
|
+ @createContainer.called.should.equal false
|
|
|
+
|
|
|
+ it "should call the callback with an error", ->
|
|
|
+ @callback.calledWith(new Error()).should.equal true
|
|
|
+
|
|
|
+ describe "when a volume exists but is not a directory", ->
|
|
|
+ beforeEach ->
|
|
|
+ @fs.stat = sinon.stub().yields(null, {isDirectory: () -> return false})
|
|
|
+ @DockerRunner.startContainer @options, @volumes, @attachStreamHandler, @callback
|
|
|
+
|
|
|
+ it "should not try to create the container", ->
|
|
|
+ @createContainer.called.should.equal false
|
|
|
+
|
|
|
+ it "should call the callback with an error", ->
|
|
|
+ @callback.calledWith(new Error()).should.equal true
|
|
|
+
|
|
|
+ describe "when a volume does not exist, but sibling-containers are used", ->
|
|
|
+ beforeEach ->
|
|
|
+ @fs.stat = sinon.stub().yields(new Error("no such path"))
|
|
|
+ @Settings.path.sandboxedCompilesHostDir = '/some/path'
|
|
|
+ @container.start = sinon.stub().yields()
|
|
|
+ @DockerRunner.startContainer @options, @volumes, @callback
|
|
|
+
|
|
|
+ afterEach ->
|
|
|
+ delete @Settings.path.sandboxedCompilesHostDir
|
|
|
+
|
|
|
+ it "should start the container with the given name", ->
|
|
|
+ @getContainer
|
|
|
+ .calledWith(@options.name)
|
|
|
+ .should.equal true
|
|
|
+ @container.start
|
|
|
+ .called
|
|
|
+ .should.equal true
|
|
|
+
|
|
|
+ it "should not try to create the container", ->
|
|
|
+ @createContainer.called.should.equal false
|
|
|
+
|
|
|
+ it "should call the callback", ->
|
|
|
+ @callback.called.should.equal true
|
|
|
+ @callback.calledWith(new Error()).should.equal false
|
|
|
+
|
|
|
+ describe "when the container tries to be created, but already has been (race condition)", ->
|
|
|
+
|
|
|
+ describe "waitForContainer", ->
|
|
|
+ beforeEach ->
|
|
|
+ @containerId = "container-id"
|
|
|
+ @timeout = 5000
|
|
|
+ @container.wait = sinon.stub().yields(null, StatusCode: @statusCode = 42)
|
|
|
+ @container.kill = sinon.stub().yields()
|
|
|
+
|
|
|
+ describe "when the container returns in time", ->
|
|
|
+ beforeEach ->
|
|
|
+ @DockerRunner.waitForContainer @containerId, @timeout, @callback
|
|
|
+
|
|
|
+ it "should wait for the container", ->
|
|
|
+ @getContainer
|
|
|
+ .calledWith(@containerId)
|
|
|
+ .should.equal true
|
|
|
+ @container.wait
|
|
|
+ .called
|
|
|
+ .should.equal true
|
|
|
+
|
|
|
+ it "should call the callback with the exit", ->
|
|
|
+ @callback
|
|
|
+ .calledWith(null, @statusCode)
|
|
|
+ .should.equal true
|
|
|
+
|
|
|
+ describe "when the container does not return before the timeout", ->
|
|
|
+ beforeEach (done) ->
|
|
|
+ @container.wait = (callback = (error, exitCode) ->) ->
|
|
|
+ setTimeout () ->
|
|
|
+ callback(null, StatusCode: 42)
|
|
|
+ , 100
|
|
|
+ @timeout = 5
|
|
|
+ @DockerRunner.waitForContainer @containerId, @timeout, (args...) =>
|
|
|
+ @callback(args...)
|
|
|
+ done()
|
|
|
+
|
|
|
+ it "should call kill on the container", ->
|
|
|
+ @getContainer
|
|
|
+ .calledWith(@containerId)
|
|
|
+ .should.equal true
|
|
|
+ @container.kill
|
|
|
+ .called
|
|
|
+ .should.equal true
|
|
|
+
|
|
|
+ it "should call the callback with an error", ->
|
|
|
+ error = new Error("container timed out")
|
|
|
+ error.timedout = true
|
|
|
+ @callback
|
|
|
+ .calledWith(error)
|
|
|
+ .should.equal true
|
|
|
+
|
|
|
+ describe "destroyOldContainers", ->
|
|
|
+ beforeEach (done) ->
|
|
|
+ oneHourInSeconds = 60 * 60
|
|
|
+ oneHourInMilliseconds = oneHourInSeconds * 1000
|
|
|
+ nowInSeconds = Date.now()/1000
|
|
|
+ @containers = [{
|
|
|
+ Name: "/project-old-container-name"
|
|
|
+ Id: "old-container-id"
|
|
|
+ Created: nowInSeconds - oneHourInSeconds - 100
|
|
|
+ }, {
|
|
|
+ Name: "/project-new-container-name"
|
|
|
+ Id: "new-container-id"
|
|
|
+ Created: nowInSeconds - oneHourInSeconds + 100
|
|
|
+ }, {
|
|
|
+ Name: "/totally-not-a-project-container"
|
|
|
+ Id: "some-random-id"
|
|
|
+ Created: nowInSeconds - (2 * oneHourInSeconds )
|
|
|
+ }]
|
|
|
+ @DockerRunner.MAX_CONTAINER_AGE = oneHourInMilliseconds
|
|
|
+ @listContainers.callsArgWith(1, null, @containers)
|
|
|
+ @DockerRunner.destroyContainer = sinon.stub().callsArg(3)
|
|
|
+ @DockerRunner.destroyOldContainers (error) =>
|
|
|
+ @callback(error)
|
|
|
+ done()
|
|
|
+
|
|
|
+ it "should list all containers", ->
|
|
|
+ @listContainers
|
|
|
+ .calledWith(all: true)
|
|
|
+ .should.equal true
|
|
|
+
|
|
|
+ it "should destroy old containers", ->
|
|
|
+ @DockerRunner.destroyContainer
|
|
|
+ .callCount
|
|
|
+ .should.equal 1
|
|
|
+ @DockerRunner.destroyContainer
|
|
|
+ .calledWith("/project-old-container-name", "old-container-id")
|
|
|
+ .should.equal true
|
|
|
+
|
|
|
+ it "should not destroy new containers", ->
|
|
|
+ @DockerRunner.destroyContainer
|
|
|
+ .calledWith("/project-new-container-name", "new-container-id")
|
|
|
+ .should.equal false
|
|
|
+
|
|
|
+ it "should not destroy non-project containers", ->
|
|
|
+ @DockerRunner.destroyContainer
|
|
|
+ .calledWith("/totally-not-a-project-container", "some-random-id")
|
|
|
+ .should.equal false
|
|
|
+
|
|
|
+ it "should callback the callback", ->
|
|
|
+ @callback.called.should.equal true
|
|
|
+
|
|
|
+
|
|
|
+ describe '_destroyContainer', ->
|
|
|
+ beforeEach ->
|
|
|
+ @containerId = 'some_id'
|
|
|
+ @fakeContainer =
|
|
|
+ remove: sinon.stub().callsArgWith(1, null)
|
|
|
+ @Docker::getContainer = sinon.stub().returns(@fakeContainer)
|
|
|
+
|
|
|
+ it 'should get the container', (done) ->
|
|
|
+ @DockerRunner._destroyContainer @containerId, false, (err) =>
|
|
|
+ @Docker::getContainer.callCount.should.equal 1
|
|
|
+ @Docker::getContainer.calledWith(@containerId).should.equal true
|
|
|
+ done()
|
|
|
+
|
|
|
+ it 'should try to force-destroy the container when shouldForce=true', (done) ->
|
|
|
+ @DockerRunner._destroyContainer @containerId, true, (err) =>
|
|
|
+ @fakeContainer.remove.callCount.should.equal 1
|
|
|
+ @fakeContainer.remove.calledWith({force: true}).should.equal true
|
|
|
+ done()
|
|
|
+
|
|
|
+ it 'should not try to force-destroy the container when shouldForce=false', (done) ->
|
|
|
+ @DockerRunner._destroyContainer @containerId, false, (err) =>
|
|
|
+ @fakeContainer.remove.callCount.should.equal 1
|
|
|
+ @fakeContainer.remove.calledWith({force: false}).should.equal true
|
|
|
+ done()
|
|
|
+
|
|
|
+ it 'should not produce an error', (done) ->
|
|
|
+ @DockerRunner._destroyContainer @containerId, false, (err) =>
|
|
|
+ expect(err).to.equal null
|
|
|
+ done()
|
|
|
+
|
|
|
+ describe 'when the container is already gone', ->
|
|
|
+ beforeEach ->
|
|
|
+ @fakeError = new Error('woops')
|
|
|
+ @fakeError.statusCode = 404
|
|
|
+ @fakeContainer =
|
|
|
+ remove: sinon.stub().callsArgWith(1, @fakeError)
|
|
|
+ @Docker::getContainer = sinon.stub().returns(@fakeContainer)
|
|
|
+
|
|
|
+ it 'should not produce an error', (done) ->
|
|
|
+ @DockerRunner._destroyContainer @containerId, false, (err) =>
|
|
|
+ expect(err).to.equal null
|
|
|
+ done()
|
|
|
+
|
|
|
+ describe 'when container.destroy produces an error', (done) ->
|
|
|
+ beforeEach ->
|
|
|
+ @fakeError = new Error('woops')
|
|
|
+ @fakeError.statusCode = 500
|
|
|
+ @fakeContainer =
|
|
|
+ remove: sinon.stub().callsArgWith(1, @fakeError)
|
|
|
+ @Docker::getContainer = sinon.stub().returns(@fakeContainer)
|
|
|
+
|
|
|
+ it 'should produce an error', (done) ->
|
|
|
+ @DockerRunner._destroyContainer @containerId, false, (err) =>
|
|
|
+ expect(err).to.not.equal null
|
|
|
+ expect(err).to.equal @fakeError
|
|
|
+ done()
|
|
|
+
|
|
|
+
|
|
|
+ describe 'kill', ->
|
|
|
+ beforeEach ->
|
|
|
+ @containerId = 'some_id'
|
|
|
+ @fakeContainer =
|
|
|
+ kill: sinon.stub().callsArgWith(0, null)
|
|
|
+ @Docker::getContainer = sinon.stub().returns(@fakeContainer)
|
|
|
+
|
|
|
+ it 'should get the container', (done) ->
|
|
|
+ @DockerRunner.kill @containerId, (err) =>
|
|
|
+ @Docker::getContainer.callCount.should.equal 1
|
|
|
+ @Docker::getContainer.calledWith(@containerId).should.equal true
|
|
|
+ done()
|
|
|
+
|
|
|
+ it 'should try to force-destroy the container', (done) ->
|
|
|
+ @DockerRunner.kill @containerId, (err) =>
|
|
|
+ @fakeContainer.kill.callCount.should.equal 1
|
|
|
+ done()
|
|
|
+
|
|
|
+ it 'should not produce an error', (done) ->
|
|
|
+ @DockerRunner.kill @containerId, (err) =>
|
|
|
+ expect(err).to.equal undefined
|
|
|
+ done()
|
|
|
+
|
|
|
+ describe 'when the container is not actually running', ->
|
|
|
+ beforeEach ->
|
|
|
+ @fakeError = new Error('woops')
|
|
|
+ @fakeError.statusCode = 500
|
|
|
+ @fakeError.message = 'Cannot kill container <whatever> is not running'
|
|
|
+ @fakeContainer =
|
|
|
+ kill: sinon.stub().callsArgWith(0, @fakeError)
|
|
|
+ @Docker::getContainer = sinon.stub().returns(@fakeContainer)
|
|
|
+
|
|
|
+ it 'should not produce an error', (done) ->
|
|
|
+ @DockerRunner.kill @containerId, (err) =>
|
|
|
+ expect(err).to.equal undefined
|
|
|
+ done()
|
|
|
+
|
|
|
+ describe 'when container.kill produces a legitimate error', (done) ->
|
|
|
+ beforeEach ->
|
|
|
+ @fakeError = new Error('woops')
|
|
|
+ @fakeError.statusCode = 500
|
|
|
+ @fakeError.message = 'Totally legitimate reason to throw an error'
|
|
|
+ @fakeContainer =
|
|
|
+ kill: sinon.stub().callsArgWith(0, @fakeError)
|
|
|
+ @Docker::getContainer = sinon.stub().returns(@fakeContainer)
|
|
|
+
|
|
|
+ it 'should produce an error', (done) ->
|
|
|
+ @DockerRunner.kill @containerId, (err) =>
|
|
|
+ expect(err).to.not.equal undefined
|
|
|
+ expect(err).to.equal @fakeError
|
|
|
+ done()
|