DockerRunnerTests.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663
  1. /* eslint-disable
  2. handle-callback-err,
  3. no-return-assign,
  4. no-unused-vars,
  5. */
  6. // TODO: This file was created by bulk-decaffeinate.
  7. // Fix any style issues and re-enable lint.
  8. /*
  9. * decaffeinate suggestions:
  10. * DS101: Remove unnecessary use of Array.from
  11. * DS102: Remove unnecessary code created because of implicit returns
  12. * DS206: Consider reworking classes to avoid initClass
  13. * DS207: Consider shorter variations of null checks
  14. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  15. */
  16. const SandboxedModule = require('sandboxed-module');
  17. const sinon = require('sinon');
  18. require('chai').should();
  19. const { expect } = require('chai');
  20. require("coffee-script");
  21. const modulePath = require('path').join(__dirname, '../../../app/coffee/DockerRunner');
  22. const Path = require("path");
  23. describe("DockerRunner", function() {
  24. beforeEach(function() {
  25. let container, Docker, Timer;
  26. this.container = (container = {});
  27. this.DockerRunner = SandboxedModule.require(modulePath, { requires: {
  28. "settings-sharelatex": (this.Settings = {
  29. clsi: { docker: {}
  30. },
  31. path: {}
  32. }),
  33. "logger-sharelatex": (this.logger = {
  34. log: sinon.stub(),
  35. error: sinon.stub(),
  36. info: sinon.stub(),
  37. warn: sinon.stub()
  38. }),
  39. "dockerode": (Docker = (function() {
  40. Docker = class Docker {
  41. static initClass() {
  42. this.prototype.getContainer = sinon.stub().returns(container);
  43. this.prototype.createContainer = sinon.stub().yields(null, container);
  44. this.prototype.listContainers = sinon.stub();
  45. }
  46. };
  47. Docker.initClass();
  48. return Docker;
  49. })()),
  50. "fs": (this.fs = { stat: sinon.stub().yields(null,{isDirectory(){ return true; }}) }),
  51. "./Metrics": {
  52. Timer: (Timer = class Timer {
  53. done() {}
  54. })
  55. },
  56. "./LockManager": {
  57. runWithLock(key, runner, callback) { return runner(callback); }
  58. }
  59. }
  60. }
  61. );
  62. this.Docker = Docker;
  63. this.getContainer = Docker.prototype.getContainer;
  64. this.createContainer = Docker.prototype.createContainer;
  65. this.listContainers = Docker.prototype.listContainers;
  66. this.directory = "/local/compile/directory";
  67. this.mainFile = "main-file.tex";
  68. this.compiler = "pdflatex";
  69. this.image = "example.com/sharelatex/image:2016.2";
  70. this.env = {};
  71. this.callback = sinon.stub();
  72. this.project_id = "project-id-123";
  73. this.volumes =
  74. {"/local/compile/directory": "/compile"};
  75. this.Settings.clsi.docker.image = (this.defaultImage = "default-image");
  76. return this.Settings.clsi.docker.env = {PATH: "mock-path"};
  77. });
  78. describe("run", function() {
  79. beforeEach(function(done){
  80. this.DockerRunner._getContainerOptions = sinon.stub().returns(this.options = {mockoptions: "foo"});
  81. this.DockerRunner._fingerprintContainer = sinon.stub().returns(this.fingerprint = "fingerprint");
  82. this.name = `project-${this.project_id}-${this.fingerprint}`;
  83. this.command = ["mock", "command", "--outdir=$COMPILE_DIR"];
  84. this.command_with_dir = ["mock", "command", "--outdir=/compile"];
  85. this.timeout = 42000;
  86. return done();
  87. });
  88. describe("successfully", function() {
  89. beforeEach(function(done){
  90. this.DockerRunner._runAndWaitForContainer = sinon.stub().callsArgWith(3, null, (this.output = "mock-output"));
  91. return this.DockerRunner.run(this.project_id, this.command, this.directory, this.image, this.timeout, this.env, (err, output)=> {
  92. this.callback(err, output);
  93. return done();
  94. });
  95. });
  96. it("should generate the options for the container", function() {
  97. return this.DockerRunner._getContainerOptions
  98. .calledWith(this.command_with_dir, this.image, this.volumes, this.timeout)
  99. .should.equal(true);
  100. });
  101. it("should generate the fingerprint from the returned options", function() {
  102. return this.DockerRunner._fingerprintContainer
  103. .calledWith(this.options)
  104. .should.equal(true);
  105. });
  106. it("should do the run", function() {
  107. return this.DockerRunner._runAndWaitForContainer
  108. .calledWith(this.options, this.volumes, this.timeout)
  109. .should.equal(true);
  110. });
  111. return it("should call the callback", function() {
  112. return this.callback.calledWith(null, this.output).should.equal(true);
  113. });
  114. });
  115. describe('when path.sandboxedCompilesHostDir is set', function() {
  116. beforeEach(function() {
  117. this.Settings.path.sandboxedCompilesHostDir = '/some/host/dir/compiles';
  118. this.directory = '/var/lib/sharelatex/data/compiles/xyz';
  119. this.DockerRunner._runAndWaitForContainer = sinon.stub().callsArgWith(3, null, (this.output = "mock-output"));
  120. return this.DockerRunner.run(this.project_id, this.command, this.directory, this.image, this.timeout, this.env, this.callback);
  121. });
  122. it('should re-write the bind directory', function() {
  123. const volumes = this.DockerRunner._runAndWaitForContainer.lastCall.args[1];
  124. return expect(volumes).to.deep.equal({
  125. '/some/host/dir/compiles/xyz': '/compile'
  126. });
  127. });
  128. return it("should call the callback", function() {
  129. return this.callback.calledWith(null, this.output).should.equal(true);
  130. });
  131. });
  132. describe("when the run throws an error", function() {
  133. beforeEach(function() {
  134. let firstTime = true;
  135. this.output = "mock-output";
  136. this.DockerRunner._runAndWaitForContainer = (options, volumes, timeout, callback) => {
  137. if (callback == null) { callback = function(error, output){}; }
  138. if (firstTime) {
  139. firstTime = false;
  140. return callback(new Error("HTTP code is 500 which indicates error: server error"));
  141. } else {
  142. return callback(null, this.output);
  143. }
  144. };
  145. sinon.spy(this.DockerRunner, "_runAndWaitForContainer");
  146. this.DockerRunner.destroyContainer = sinon.stub().callsArg(3);
  147. return this.DockerRunner.run(this.project_id, this.command, this.directory, this.image, this.timeout, this.env, this.callback);
  148. });
  149. it("should do the run twice", function() {
  150. return this.DockerRunner._runAndWaitForContainer
  151. .calledTwice.should.equal(true);
  152. });
  153. it("should destroy the container in between", function() {
  154. return this.DockerRunner.destroyContainer
  155. .calledWith(this.name, null)
  156. .should.equal(true);
  157. });
  158. return it("should call the callback", function() {
  159. return this.callback.calledWith(null, this.output).should.equal(true);
  160. });
  161. });
  162. describe("with no image", function() {
  163. beforeEach(function() {
  164. this.DockerRunner._runAndWaitForContainer = sinon.stub().callsArgWith(3, null, (this.output = "mock-output"));
  165. return this.DockerRunner.run(this.project_id, this.command, this.directory, null, this.timeout, this.env, this.callback);
  166. });
  167. return it("should use the default image", function() {
  168. return this.DockerRunner._getContainerOptions
  169. .calledWith(this.command_with_dir, this.defaultImage, this.volumes, this.timeout)
  170. .should.equal(true);
  171. });
  172. });
  173. return describe("with image override", function() {
  174. beforeEach(function() {
  175. this.Settings.texliveImageNameOveride = "overrideimage.com/something";
  176. this.DockerRunner._runAndWaitForContainer = sinon.stub().callsArgWith(3, null, (this.output = "mock-output"));
  177. return this.DockerRunner.run(this.project_id, this.command, this.directory, this.image, this.timeout, this.env, this.callback);
  178. });
  179. return it("should use the override and keep the tag", function() {
  180. const image = this.DockerRunner._getContainerOptions.args[0][1];
  181. return image.should.equal("overrideimage.com/something/image:2016.2");
  182. });
  183. });
  184. });
  185. describe("_runAndWaitForContainer", function() {
  186. beforeEach(function() {
  187. this.options = {mockoptions: "foo", name: (this.name = "mock-name")};
  188. this.DockerRunner.startContainer = (options, volumes, attachStreamHandler, callback) => {
  189. attachStreamHandler(null, (this.output = "mock-output"));
  190. return callback(null, (this.containerId = "container-id"));
  191. };
  192. sinon.spy(this.DockerRunner, "startContainer");
  193. this.DockerRunner.waitForContainer = sinon.stub().callsArgWith(2, null, (this.exitCode = 42));
  194. return this.DockerRunner._runAndWaitForContainer(this.options, this.volumes, this.timeout, this.callback);
  195. });
  196. it("should create/start the container", function() {
  197. return this.DockerRunner.startContainer
  198. .calledWith(this.options, this.volumes)
  199. .should.equal(true);
  200. });
  201. it("should wait for the container to finish", function() {
  202. return this.DockerRunner.waitForContainer
  203. .calledWith(this.name, this.timeout)
  204. .should.equal(true);
  205. });
  206. return it("should call the callback with the output", function() {
  207. return this.callback.calledWith(null, this.output).should.equal(true);
  208. });
  209. });
  210. describe("startContainer", function() {
  211. beforeEach(function() {
  212. this.attachStreamHandler = sinon.stub();
  213. this.attachStreamHandler.cock = true;
  214. this.options = {mockoptions: "foo", name: "mock-name"};
  215. this.container.inspect = sinon.stub().callsArgWith(0);
  216. this.DockerRunner.attachToContainer = (containerId, attachStreamHandler, cb)=> {
  217. attachStreamHandler();
  218. return cb();
  219. };
  220. return sinon.spy(this.DockerRunner, "attachToContainer");
  221. });
  222. describe("when the container exists", function() {
  223. beforeEach(function() {
  224. this.container.inspect = sinon.stub().callsArgWith(0);
  225. this.container.start = sinon.stub().yields();
  226. return this.DockerRunner.startContainer(this.options, this.volumes, this.callback, () => {});
  227. });
  228. it("should start the container with the given name", function() {
  229. this.getContainer
  230. .calledWith(this.options.name)
  231. .should.equal(true);
  232. return this.container.start
  233. .called
  234. .should.equal(true);
  235. });
  236. it("should not try to create the container", function() {
  237. return this.createContainer.called.should.equal(false);
  238. });
  239. it("should attach to the container", function() {
  240. return this.DockerRunner.attachToContainer.called.should.equal(true);
  241. });
  242. it("should call the callback", function() {
  243. return this.callback.called.should.equal(true);
  244. });
  245. return it("should attach before the container starts", function() {
  246. return sinon.assert.callOrder(this.DockerRunner.attachToContainer, this.container.start);
  247. });
  248. });
  249. describe("when the container does not exist", function() {
  250. beforeEach(function(){
  251. const exists = false;
  252. this.container.start = sinon.stub().yields();
  253. this.container.inspect = sinon.stub().callsArgWith(0, {statusCode:404});
  254. return this.DockerRunner.startContainer(this.options, this.volumes, this.attachStreamHandler, this.callback);
  255. });
  256. it("should create the container", function() {
  257. return this.createContainer
  258. .calledWith(this.options)
  259. .should.equal(true);
  260. });
  261. it("should call the callback and stream handler", function() {
  262. this.attachStreamHandler.called.should.equal(true);
  263. return this.callback.called.should.equal(true);
  264. });
  265. it("should attach to the container", function() {
  266. return this.DockerRunner.attachToContainer.called.should.equal(true);
  267. });
  268. return it("should attach before the container starts", function() {
  269. return sinon.assert.callOrder(this.DockerRunner.attachToContainer, this.container.start);
  270. });
  271. });
  272. describe("when the container is already running", function() {
  273. beforeEach(function() {
  274. const error = new Error(`HTTP code is 304 which indicates error: server error - start: Cannot start container ${this.name}: The container MOCKID is already running.`);
  275. error.statusCode = 304;
  276. this.container.start = sinon.stub().yields(error);
  277. this.container.inspect = sinon.stub().callsArgWith(0);
  278. return this.DockerRunner.startContainer(this.options, this.volumes, this.attachStreamHandler, this.callback);
  279. });
  280. it("should not try to create the container", function() {
  281. return this.createContainer.called.should.equal(false);
  282. });
  283. return it("should call the callback and stream handler without an error", function() {
  284. this.attachStreamHandler.called.should.equal(true);
  285. return this.callback.called.should.equal(true);
  286. });
  287. });
  288. describe("when a volume does not exist", function() {
  289. beforeEach(function(){
  290. this.fs.stat = sinon.stub().yields(new Error("no such path"));
  291. return this.DockerRunner.startContainer(this.options, this.volumes, this.attachStreamHandler, this.callback);
  292. });
  293. it("should not try to create the container", function() {
  294. return this.createContainer.called.should.equal(false);
  295. });
  296. return it("should call the callback with an error", function() {
  297. return this.callback.calledWith(new Error()).should.equal(true);
  298. });
  299. });
  300. describe("when a volume exists but is not a directory", function() {
  301. beforeEach(function() {
  302. this.fs.stat = sinon.stub().yields(null, {isDirectory() { return false; }});
  303. return this.DockerRunner.startContainer(this.options, this.volumes, this.attachStreamHandler, this.callback);
  304. });
  305. it("should not try to create the container", function() {
  306. return this.createContainer.called.should.equal(false);
  307. });
  308. return it("should call the callback with an error", function() {
  309. return this.callback.calledWith(new Error()).should.equal(true);
  310. });
  311. });
  312. describe("when a volume does not exist, but sibling-containers are used", function() {
  313. beforeEach(function() {
  314. this.fs.stat = sinon.stub().yields(new Error("no such path"));
  315. this.Settings.path.sandboxedCompilesHostDir = '/some/path';
  316. this.container.start = sinon.stub().yields();
  317. return this.DockerRunner.startContainer(this.options, this.volumes, this.callback);
  318. });
  319. afterEach(function() {
  320. return delete this.Settings.path.sandboxedCompilesHostDir;
  321. });
  322. it("should start the container with the given name", function() {
  323. this.getContainer
  324. .calledWith(this.options.name)
  325. .should.equal(true);
  326. return this.container.start
  327. .called
  328. .should.equal(true);
  329. });
  330. it("should not try to create the container", function() {
  331. return this.createContainer.called.should.equal(false);
  332. });
  333. return it("should call the callback", function() {
  334. this.callback.called.should.equal(true);
  335. return this.callback.calledWith(new Error()).should.equal(false);
  336. });
  337. });
  338. return describe("when the container tries to be created, but already has been (race condition)", function() {});
  339. });
  340. describe("waitForContainer", function() {
  341. beforeEach(function() {
  342. this.containerId = "container-id";
  343. this.timeout = 5000;
  344. this.container.wait = sinon.stub().yields(null, {StatusCode: (this.statusCode = 42)});
  345. return this.container.kill = sinon.stub().yields();
  346. });
  347. describe("when the container returns in time", function() {
  348. beforeEach(function() {
  349. return this.DockerRunner.waitForContainer(this.containerId, this.timeout, this.callback);
  350. });
  351. it("should wait for the container", function() {
  352. this.getContainer
  353. .calledWith(this.containerId)
  354. .should.equal(true);
  355. return this.container.wait
  356. .called
  357. .should.equal(true);
  358. });
  359. return it("should call the callback with the exit", function() {
  360. return this.callback
  361. .calledWith(null, this.statusCode)
  362. .should.equal(true);
  363. });
  364. });
  365. return describe("when the container does not return before the timeout", function() {
  366. beforeEach(function(done) {
  367. this.container.wait = function(callback) {
  368. if (callback == null) { callback = function(error, exitCode) {}; }
  369. return setTimeout(() => callback(null, {StatusCode: 42})
  370. , 100);
  371. };
  372. this.timeout = 5;
  373. return this.DockerRunner.waitForContainer(this.containerId, this.timeout, (...args) => {
  374. this.callback(...Array.from(args || []));
  375. return done();
  376. });
  377. });
  378. it("should call kill on the container", function() {
  379. this.getContainer
  380. .calledWith(this.containerId)
  381. .should.equal(true);
  382. return this.container.kill
  383. .called
  384. .should.equal(true);
  385. });
  386. return it("should call the callback with an error", function() {
  387. const error = new Error("container timed out");
  388. error.timedout = true;
  389. return this.callback
  390. .calledWith(error)
  391. .should.equal(true);
  392. });
  393. });
  394. });
  395. describe("destroyOldContainers", function() {
  396. beforeEach(function(done) {
  397. const oneHourInSeconds = 60 * 60;
  398. const oneHourInMilliseconds = oneHourInSeconds * 1000;
  399. const nowInSeconds = Date.now()/1000;
  400. this.containers = [{
  401. Name: "/project-old-container-name",
  402. Id: "old-container-id",
  403. Created: nowInSeconds - oneHourInSeconds - 100
  404. }, {
  405. Name: "/project-new-container-name",
  406. Id: "new-container-id",
  407. Created: (nowInSeconds - oneHourInSeconds) + 100
  408. }, {
  409. Name: "/totally-not-a-project-container",
  410. Id: "some-random-id",
  411. Created: nowInSeconds - (2 * oneHourInSeconds )
  412. }];
  413. this.DockerRunner.MAX_CONTAINER_AGE = oneHourInMilliseconds;
  414. this.listContainers.callsArgWith(1, null, this.containers);
  415. this.DockerRunner.destroyContainer = sinon.stub().callsArg(3);
  416. return this.DockerRunner.destroyOldContainers(error => {
  417. this.callback(error);
  418. return done();
  419. });
  420. });
  421. it("should list all containers", function() {
  422. return this.listContainers
  423. .calledWith({all: true})
  424. .should.equal(true);
  425. });
  426. it("should destroy old containers", function() {
  427. this.DockerRunner.destroyContainer
  428. .callCount
  429. .should.equal(1);
  430. return this.DockerRunner.destroyContainer
  431. .calledWith("/project-old-container-name", "old-container-id")
  432. .should.equal(true);
  433. });
  434. it("should not destroy new containers", function() {
  435. return this.DockerRunner.destroyContainer
  436. .calledWith("/project-new-container-name", "new-container-id")
  437. .should.equal(false);
  438. });
  439. it("should not destroy non-project containers", function() {
  440. return this.DockerRunner.destroyContainer
  441. .calledWith("/totally-not-a-project-container", "some-random-id")
  442. .should.equal(false);
  443. });
  444. return it("should callback the callback", function() {
  445. return this.callback.called.should.equal(true);
  446. });
  447. });
  448. describe('_destroyContainer', function() {
  449. beforeEach(function() {
  450. this.containerId = 'some_id';
  451. this.fakeContainer =
  452. {remove: sinon.stub().callsArgWith(1, null)};
  453. return this.Docker.prototype.getContainer = sinon.stub().returns(this.fakeContainer);
  454. });
  455. it('should get the container', function(done) {
  456. return this.DockerRunner._destroyContainer(this.containerId, false, err => {
  457. this.Docker.prototype.getContainer.callCount.should.equal(1);
  458. this.Docker.prototype.getContainer.calledWith(this.containerId).should.equal(true);
  459. return done();
  460. });
  461. });
  462. it('should try to force-destroy the container when shouldForce=true', function(done) {
  463. return this.DockerRunner._destroyContainer(this.containerId, true, err => {
  464. this.fakeContainer.remove.callCount.should.equal(1);
  465. this.fakeContainer.remove.calledWith({force: true}).should.equal(true);
  466. return done();
  467. });
  468. });
  469. it('should not try to force-destroy the container when shouldForce=false', function(done) {
  470. return this.DockerRunner._destroyContainer(this.containerId, false, err => {
  471. this.fakeContainer.remove.callCount.should.equal(1);
  472. this.fakeContainer.remove.calledWith({force: false}).should.equal(true);
  473. return done();
  474. });
  475. });
  476. it('should not produce an error', function(done) {
  477. return this.DockerRunner._destroyContainer(this.containerId, false, err => {
  478. expect(err).to.equal(null);
  479. return done();
  480. });
  481. });
  482. describe('when the container is already gone', function() {
  483. beforeEach(function() {
  484. this.fakeError = new Error('woops');
  485. this.fakeError.statusCode = 404;
  486. this.fakeContainer =
  487. {remove: sinon.stub().callsArgWith(1, this.fakeError)};
  488. return this.Docker.prototype.getContainer = sinon.stub().returns(this.fakeContainer);
  489. });
  490. return it('should not produce an error', function(done) {
  491. return this.DockerRunner._destroyContainer(this.containerId, false, err => {
  492. expect(err).to.equal(null);
  493. return done();
  494. });
  495. });
  496. });
  497. return describe('when container.destroy produces an error', function(done) {
  498. beforeEach(function() {
  499. this.fakeError = new Error('woops');
  500. this.fakeError.statusCode = 500;
  501. this.fakeContainer =
  502. {remove: sinon.stub().callsArgWith(1, this.fakeError)};
  503. return this.Docker.prototype.getContainer = sinon.stub().returns(this.fakeContainer);
  504. });
  505. return it('should produce an error', function(done) {
  506. return this.DockerRunner._destroyContainer(this.containerId, false, err => {
  507. expect(err).to.not.equal(null);
  508. expect(err).to.equal(this.fakeError);
  509. return done();
  510. });
  511. });
  512. });
  513. });
  514. return describe('kill', function() {
  515. beforeEach(function() {
  516. this.containerId = 'some_id';
  517. this.fakeContainer =
  518. {kill: sinon.stub().callsArgWith(0, null)};
  519. return this.Docker.prototype.getContainer = sinon.stub().returns(this.fakeContainer);
  520. });
  521. it('should get the container', function(done) {
  522. return this.DockerRunner.kill(this.containerId, err => {
  523. this.Docker.prototype.getContainer.callCount.should.equal(1);
  524. this.Docker.prototype.getContainer.calledWith(this.containerId).should.equal(true);
  525. return done();
  526. });
  527. });
  528. it('should try to force-destroy the container', function(done) {
  529. return this.DockerRunner.kill(this.containerId, err => {
  530. this.fakeContainer.kill.callCount.should.equal(1);
  531. return done();
  532. });
  533. });
  534. it('should not produce an error', function(done) {
  535. return this.DockerRunner.kill(this.containerId, err => {
  536. expect(err).to.equal(undefined);
  537. return done();
  538. });
  539. });
  540. describe('when the container is not actually running', function() {
  541. beforeEach(function() {
  542. this.fakeError = new Error('woops');
  543. this.fakeError.statusCode = 500;
  544. this.fakeError.message = 'Cannot kill container <whatever> is not running';
  545. this.fakeContainer =
  546. {kill: sinon.stub().callsArgWith(0, this.fakeError)};
  547. return this.Docker.prototype.getContainer = sinon.stub().returns(this.fakeContainer);
  548. });
  549. return it('should not produce an error', function(done) {
  550. return this.DockerRunner.kill(this.containerId, err => {
  551. expect(err).to.equal(undefined);
  552. return done();
  553. });
  554. });
  555. });
  556. return describe('when container.kill produces a legitimate error', function(done) {
  557. beforeEach(function() {
  558. this.fakeError = new Error('woops');
  559. this.fakeError.statusCode = 500;
  560. this.fakeError.message = 'Totally legitimate reason to throw an error';
  561. this.fakeContainer =
  562. {kill: sinon.stub().callsArgWith(0, this.fakeError)};
  563. return this.Docker.prototype.getContainer = sinon.stub().returns(this.fakeContainer);
  564. });
  565. return it('should produce an error', function(done) {
  566. return this.DockerRunner.kill(this.containerId, err => {
  567. expect(err).to.not.equal(undefined);
  568. expect(err).to.equal(this.fakeError);
  569. return done();
  570. });
  571. });
  572. });
  573. });
  574. });