CompileManagerTests.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565
  1. /* eslint-disable
  2. camelcase,
  3. chai-friendly/no-unused-expressions,
  4. no-path-concat,
  5. no-return-assign,
  6. no-unused-vars,
  7. */
  8. // TODO: This file was created by bulk-decaffeinate.
  9. // Fix any style issues and re-enable lint.
  10. /*
  11. * decaffeinate suggestions:
  12. * DS101: Remove unnecessary use of Array.from
  13. * DS102: Remove unnecessary code created because of implicit returns
  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 modulePath = require('path').join(
  20. __dirname,
  21. '../../../app/js/CompileManager'
  22. )
  23. const tk = require('timekeeper')
  24. const { EventEmitter } = require('events')
  25. const Path = require('path')
  26. describe('CompileManager', function () {
  27. beforeEach(function () {
  28. this.CompileManager = SandboxedModule.require(modulePath, {
  29. requires: {
  30. './LatexRunner': (this.LatexRunner = {}),
  31. './ResourceWriter': (this.ResourceWriter = {}),
  32. './OutputFileFinder': (this.OutputFileFinder = {}),
  33. './OutputCacheManager': (this.OutputCacheManager = {}),
  34. 'settings-sharelatex': (this.Settings = {
  35. path: {
  36. compilesDir: '/compiles/dir',
  37. outputDir: '/output/dir'
  38. },
  39. synctexBaseDir() {
  40. return '/compile'
  41. },
  42. clsi: {
  43. docker: {
  44. image: 'SOMEIMAGE'
  45. }
  46. }
  47. }),
  48. 'logger-sharelatex': (this.logger = { log: sinon.stub(), info() {} }),
  49. child_process: (this.child_process = {}),
  50. './CommandRunner': (this.CommandRunner = {}),
  51. './DraftModeManager': (this.DraftModeManager = {}),
  52. './TikzManager': (this.TikzManager = {}),
  53. './LockManager': (this.LockManager = {}),
  54. fs: (this.fs = {}),
  55. 'fs-extra': (this.fse = { ensureDir: sinon.stub().callsArg(1) })
  56. }
  57. })
  58. this.callback = sinon.stub()
  59. this.project_id = 'project-id-123'
  60. return (this.user_id = '1234')
  61. })
  62. describe('doCompileWithLock', function () {
  63. beforeEach(function () {
  64. this.request = {
  65. resources: (this.resources = 'mock-resources'),
  66. project_id: this.project_id,
  67. user_id: this.user_id
  68. }
  69. this.output_files = ['foo', 'bar']
  70. this.Settings.compileDir = 'compiles'
  71. this.compileDir = `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}`
  72. this.CompileManager.doCompile = sinon
  73. .stub()
  74. .callsArgWith(1, null, this.output_files)
  75. return (this.LockManager.runWithLock = (lockFile, runner, callback) =>
  76. runner((err, ...result) => callback(err, ...Array.from(result))))
  77. })
  78. describe('when the project is not locked', function () {
  79. beforeEach(function () {
  80. return this.CompileManager.doCompileWithLock(
  81. this.request,
  82. this.callback
  83. )
  84. })
  85. it('should ensure that the compile directory exists', function () {
  86. return this.fse.ensureDir.calledWith(this.compileDir).should.equal(true)
  87. })
  88. it('should call doCompile with the request', function () {
  89. return this.CompileManager.doCompile
  90. .calledWith(this.request)
  91. .should.equal(true)
  92. })
  93. return it('should call the callback with the output files', function () {
  94. return this.callback
  95. .calledWithExactly(null, this.output_files)
  96. .should.equal(true)
  97. })
  98. })
  99. return describe('when the project is locked', function () {
  100. beforeEach(function () {
  101. this.error = new Error('locked')
  102. this.LockManager.runWithLock = (lockFile, runner, callback) => {
  103. return callback(this.error)
  104. }
  105. return this.CompileManager.doCompileWithLock(
  106. this.request,
  107. this.callback
  108. )
  109. })
  110. it('should ensure that the compile directory exists', function () {
  111. return this.fse.ensureDir.calledWith(this.compileDir).should.equal(true)
  112. })
  113. it('should not call doCompile with the request', function () {
  114. return this.CompileManager.doCompile.called.should.equal(false)
  115. })
  116. return it('should call the callback with the error', function () {
  117. return this.callback.calledWithExactly(this.error).should.equal(true)
  118. })
  119. })
  120. })
  121. describe('doCompile', function () {
  122. beforeEach(function () {
  123. this.output_files = [
  124. {
  125. path: 'output.log',
  126. type: 'log'
  127. },
  128. {
  129. path: 'output.pdf',
  130. type: 'pdf'
  131. }
  132. ]
  133. this.build_files = [
  134. {
  135. path: 'output.log',
  136. type: 'log',
  137. build: 1234
  138. },
  139. {
  140. path: 'output.pdf',
  141. type: 'pdf',
  142. build: 1234
  143. }
  144. ]
  145. this.request = {
  146. resources: (this.resources = 'mock-resources'),
  147. rootResourcePath: (this.rootResourcePath = 'main.tex'),
  148. project_id: this.project_id,
  149. user_id: this.user_id,
  150. compiler: (this.compiler = 'pdflatex'),
  151. timeout: (this.timeout = 42000),
  152. imageName: (this.image = 'example.com/image'),
  153. flags: (this.flags = ['-file-line-error']),
  154. compileGroup: (this.compileGroup = 'compile-group')
  155. }
  156. this.env = {}
  157. this.Settings.compileDir = 'compiles'
  158. this.compileDir = `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}`
  159. this.outputDir = `${this.Settings.path.outputDir}/${this.project_id}-${this.user_id}`
  160. this.ResourceWriter.syncResourcesToDisk = sinon
  161. .stub()
  162. .callsArgWith(2, null, this.resources)
  163. this.LatexRunner.runLatex = sinon.stub().callsArg(2)
  164. this.OutputFileFinder.findOutputFiles = sinon
  165. .stub()
  166. .callsArgWith(2, null, this.output_files)
  167. this.OutputCacheManager.saveOutputFiles = sinon
  168. .stub()
  169. .callsArgWith(3, null, this.build_files)
  170. this.DraftModeManager.injectDraftMode = sinon.stub().callsArg(1)
  171. return (this.TikzManager.checkMainFile = sinon.stub().callsArg(3, false))
  172. })
  173. describe('normally', function () {
  174. beforeEach(function () {
  175. return this.CompileManager.doCompile(this.request, this.callback)
  176. })
  177. it('should write the resources to disk', function () {
  178. return this.ResourceWriter.syncResourcesToDisk
  179. .calledWith(this.request, this.compileDir)
  180. .should.equal(true)
  181. })
  182. it('should run LaTeX', function () {
  183. return this.LatexRunner.runLatex
  184. .calledWith(`${this.project_id}-${this.user_id}`, {
  185. directory: this.compileDir,
  186. mainFile: this.rootResourcePath,
  187. compiler: this.compiler,
  188. timeout: this.timeout,
  189. image: this.image,
  190. flags: this.flags,
  191. environment: this.env,
  192. compileGroup: this.compileGroup
  193. })
  194. .should.equal(true)
  195. })
  196. it('should find the output files', function () {
  197. return this.OutputFileFinder.findOutputFiles
  198. .calledWith(this.resources, this.compileDir)
  199. .should.equal(true)
  200. })
  201. it('should return the output files', function () {
  202. return this.callback
  203. .calledWith(null, this.build_files)
  204. .should.equal(true)
  205. })
  206. return it('should not inject draft mode by default', function () {
  207. return this.DraftModeManager.injectDraftMode.called.should.equal(false)
  208. })
  209. })
  210. describe('with draft mode', function () {
  211. beforeEach(function () {
  212. this.request.draft = true
  213. return this.CompileManager.doCompile(this.request, this.callback)
  214. })
  215. return it('should inject the draft mode header', function () {
  216. return this.DraftModeManager.injectDraftMode
  217. .calledWith(this.compileDir + '/' + this.rootResourcePath)
  218. .should.equal(true)
  219. })
  220. })
  221. describe('with a check option', function () {
  222. beforeEach(function () {
  223. this.request.check = 'error'
  224. return this.CompileManager.doCompile(this.request, this.callback)
  225. })
  226. return it('should run chktex', function () {
  227. return this.LatexRunner.runLatex
  228. .calledWith(`${this.project_id}-${this.user_id}`, {
  229. directory: this.compileDir,
  230. mainFile: this.rootResourcePath,
  231. compiler: this.compiler,
  232. timeout: this.timeout,
  233. image: this.image,
  234. flags: this.flags,
  235. environment: {
  236. CHKTEX_OPTIONS: '-nall -e9 -e10 -w15 -w16',
  237. CHKTEX_EXIT_ON_ERROR: 1,
  238. CHKTEX_ULIMIT_OPTIONS: '-t 5 -v 64000'
  239. },
  240. compileGroup: this.compileGroup
  241. })
  242. .should.equal(true)
  243. })
  244. })
  245. return describe('with a knitr file and check options', function () {
  246. beforeEach(function () {
  247. this.request.rootResourcePath = 'main.Rtex'
  248. this.request.check = 'error'
  249. return this.CompileManager.doCompile(this.request, this.callback)
  250. })
  251. return it('should not run chktex', function () {
  252. return this.LatexRunner.runLatex
  253. .calledWith(`${this.project_id}-${this.user_id}`, {
  254. directory: this.compileDir,
  255. mainFile: 'main.Rtex',
  256. compiler: this.compiler,
  257. timeout: this.timeout,
  258. image: this.image,
  259. flags: this.flags,
  260. environment: this.env,
  261. compileGroup: this.compileGroup
  262. })
  263. .should.equal(true)
  264. })
  265. })
  266. })
  267. describe('clearProject', function () {
  268. describe('succesfully', function () {
  269. beforeEach(function () {
  270. this.Settings.compileDir = 'compiles'
  271. this.fs.lstat = sinon.stub().callsArgWith(1, null, {
  272. isDirectory() {
  273. return true
  274. }
  275. })
  276. this.proc = new EventEmitter()
  277. this.proc.stdout = new EventEmitter()
  278. this.proc.stderr = new EventEmitter()
  279. this.proc.stderr.setEncoding = sinon.stub().returns(this.proc.stderr)
  280. this.child_process.spawn = sinon.stub().returns(this.proc)
  281. this.CompileManager.clearProject(
  282. this.project_id,
  283. this.user_id,
  284. this.callback
  285. )
  286. return this.proc.emit('close', 0)
  287. })
  288. it('should remove the project directory', function () {
  289. return this.child_process.spawn
  290. .calledWith('rm', [
  291. '-r',
  292. '-f',
  293. '--',
  294. `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}`,
  295. `${this.Settings.path.outputDir}/${this.project_id}-${this.user_id}`
  296. ])
  297. .should.equal(true)
  298. })
  299. return it('should call the callback', function () {
  300. return this.callback.called.should.equal(true)
  301. })
  302. })
  303. return describe('with a non-success status code', function () {
  304. beforeEach(function () {
  305. this.Settings.compileDir = 'compiles'
  306. this.fs.lstat = sinon.stub().callsArgWith(1, null, {
  307. isDirectory() {
  308. return true
  309. }
  310. })
  311. this.proc = new EventEmitter()
  312. this.proc.stdout = new EventEmitter()
  313. this.proc.stderr = new EventEmitter()
  314. this.proc.stderr.setEncoding = sinon.stub().returns(this.proc.stderr)
  315. this.child_process.spawn = sinon.stub().returns(this.proc)
  316. this.CompileManager.clearProject(
  317. this.project_id,
  318. this.user_id,
  319. this.callback
  320. )
  321. this.proc.stderr.emit('data', (this.error = 'oops'))
  322. return this.proc.emit('close', 1)
  323. })
  324. it('should remove the project directory', function () {
  325. return this.child_process.spawn
  326. .calledWith('rm', [
  327. '-r',
  328. '-f',
  329. '--',
  330. `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}`,
  331. `${this.Settings.path.outputDir}/${this.project_id}-${this.user_id}`
  332. ])
  333. .should.equal(true)
  334. })
  335. it('should call the callback with an error from the stderr', function () {
  336. this.callback.calledWithExactly(sinon.match(Error)).should.equal(true)
  337. this.callback.args[0][0].message.should.equal(
  338. `rm -r ${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id} ${this.Settings.path.outputDir}/${this.project_id}-${this.user_id} failed: ${this.error}`
  339. )
  340. })
  341. })
  342. })
  343. describe('syncing', function () {
  344. beforeEach(function () {
  345. this.page = 1
  346. this.h = 42.23
  347. this.v = 87.56
  348. this.width = 100.01
  349. this.height = 234.56
  350. this.line = 5
  351. this.column = 3
  352. this.file_name = 'main.tex'
  353. this.child_process.execFile = sinon.stub()
  354. return (this.Settings.path.synctexBaseDir = (project_id) =>
  355. `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}`)
  356. })
  357. describe('syncFromCode', function () {
  358. beforeEach(function () {
  359. this.fs.stat = sinon.stub().callsArgWith(1, null, {
  360. isFile() {
  361. return true
  362. }
  363. })
  364. this.stdout = `NODE\t${this.page}\t${this.h}\t${this.v}\t${this.width}\t${this.height}\n`
  365. this.CommandRunner.run = sinon
  366. .stub()
  367. .callsArgWith(7, null, { stdout: this.stdout })
  368. return this.CompileManager.syncFromCode(
  369. this.project_id,
  370. this.user_id,
  371. this.file_name,
  372. this.line,
  373. this.column,
  374. this.callback
  375. )
  376. })
  377. it('should execute the synctex binary', function () {
  378. const bin_path = Path.resolve(__dirname + '/../../../bin/synctex')
  379. const synctex_path = `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}/output.pdf`
  380. const file_path = `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}/${this.file_name}`
  381. return this.CommandRunner.run
  382. .calledWith(
  383. `${this.project_id}-${this.user_id}`,
  384. [
  385. '/opt/synctex',
  386. 'code',
  387. synctex_path,
  388. file_path,
  389. this.line,
  390. this.column
  391. ],
  392. `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}`,
  393. this.Settings.clsi.docker.image,
  394. 60000,
  395. {}
  396. )
  397. .should.equal(true)
  398. })
  399. return it('should call the callback with the parsed output', function () {
  400. return this.callback
  401. .calledWith(null, [
  402. {
  403. page: this.page,
  404. h: this.h,
  405. v: this.v,
  406. height: this.height,
  407. width: this.width
  408. }
  409. ])
  410. .should.equal(true)
  411. })
  412. })
  413. return describe('syncFromPdf', function () {
  414. beforeEach(function () {
  415. this.fs.stat = sinon.stub().callsArgWith(1, null, {
  416. isFile() {
  417. return true
  418. }
  419. })
  420. this.stdout = `NODE\t${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}/${this.file_name}\t${this.line}\t${this.column}\n`
  421. this.CommandRunner.run = sinon
  422. .stub()
  423. .callsArgWith(7, null, { stdout: this.stdout })
  424. return this.CompileManager.syncFromPdf(
  425. this.project_id,
  426. this.user_id,
  427. this.page,
  428. this.h,
  429. this.v,
  430. this.callback
  431. )
  432. })
  433. it('should execute the synctex binary', function () {
  434. const bin_path = Path.resolve(__dirname + '/../../../bin/synctex')
  435. const synctex_path = `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}/output.pdf`
  436. return this.CommandRunner.run
  437. .calledWith(
  438. `${this.project_id}-${this.user_id}`,
  439. ['/opt/synctex', 'pdf', synctex_path, this.page, this.h, this.v],
  440. `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}`,
  441. this.Settings.clsi.docker.image,
  442. 60000,
  443. {}
  444. )
  445. .should.equal(true)
  446. })
  447. return it('should call the callback with the parsed output', function () {
  448. return this.callback
  449. .calledWith(null, [
  450. {
  451. file: this.file_name,
  452. line: this.line,
  453. column: this.column
  454. }
  455. ])
  456. .should.equal(true)
  457. })
  458. })
  459. })
  460. return describe('wordcount', function () {
  461. beforeEach(function () {
  462. this.CommandRunner.run = sinon.stub().callsArg(7)
  463. this.fs.readFile = sinon
  464. .stub()
  465. .callsArgWith(
  466. 2,
  467. null,
  468. (this.stdout = 'Encoding: ascii\nWords in text: 2')
  469. )
  470. this.callback = sinon.stub()
  471. this.project_id
  472. this.timeout = 60 * 1000
  473. this.file_name = 'main.tex'
  474. this.Settings.path.compilesDir = '/local/compile/directory'
  475. this.image = 'example.com/image'
  476. return this.CompileManager.wordcount(
  477. this.project_id,
  478. this.user_id,
  479. this.file_name,
  480. this.image,
  481. this.callback
  482. )
  483. })
  484. it('should run the texcount command', function () {
  485. this.directory = `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}`
  486. this.file_path = `$COMPILE_DIR/${this.file_name}`
  487. this.command = [
  488. 'texcount',
  489. '-nocol',
  490. '-inc',
  491. this.file_path,
  492. `-out=${this.file_path}.wc`
  493. ]
  494. return this.CommandRunner.run
  495. .calledWith(
  496. `${this.project_id}-${this.user_id}`,
  497. this.command,
  498. this.directory,
  499. this.image,
  500. this.timeout,
  501. {}
  502. )
  503. .should.equal(true)
  504. })
  505. return it('should call the callback with the parsed output', function () {
  506. return this.callback
  507. .calledWith(null, {
  508. encode: 'ascii',
  509. textWords: 2,
  510. headWords: 0,
  511. outside: 0,
  512. headers: 0,
  513. elements: 0,
  514. mathInline: 0,
  515. mathDisplay: 0,
  516. errors: 0,
  517. messages: ''
  518. })
  519. .should.equal(true)
  520. })
  521. })
  522. })