CompileManagerTests.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753
  1. const Path = require('node:path')
  2. const SandboxedModule = require('sandboxed-module')
  3. const { expect } = require('chai')
  4. const sinon = require('sinon')
  5. const Metrics = require('../../../app/js/Metrics')
  6. const MODULE_PATH = require('node:path').join(
  7. __dirname,
  8. '../../../app/js/CompileManager'
  9. )
  10. describe('CompileManager', function () {
  11. beforeEach(function () {
  12. this.projectId = 'project-id-123'
  13. this.userId = '1234'
  14. this.resources = 'mock-resources'
  15. this.outputFiles = [
  16. {
  17. path: 'output.log',
  18. type: 'log',
  19. },
  20. {
  21. path: 'output.pdf',
  22. type: 'pdf',
  23. },
  24. ]
  25. this.buildFiles = [
  26. {
  27. path: 'output.log',
  28. type: 'log',
  29. build: 1234,
  30. },
  31. {
  32. path: 'output.pdf',
  33. type: 'pdf',
  34. build: 1234,
  35. },
  36. ]
  37. this.buildId = '00000000000-0000000000000000'
  38. this.commandOutput = 'Dummy output'
  39. this.compileBaseDir = '/compile/dir'
  40. this.outputBaseDir = '/output/dir'
  41. this.compileDir = `${this.compileBaseDir}/${this.projectId}-${this.userId}`
  42. this.outputDir = `${this.outputBaseDir}/${this.projectId}-${this.userId}`
  43. this.LatexRunner = {
  44. promises: {
  45. runLatex: sinon.stub().resolves({}),
  46. },
  47. }
  48. this.ResourceWriter = {
  49. promises: {
  50. syncResourcesToDisk: sinon.stub().resolves(this.resources),
  51. },
  52. }
  53. this.OutputFileFinder = {
  54. promises: {
  55. findOutputFiles: sinon.stub().resolves({
  56. outputFiles: this.outputFiles,
  57. allEntries: this.outputFiles.map(f => f.path).concat(['main.tex']),
  58. }),
  59. },
  60. }
  61. this.OutputCacheManager = {
  62. BUILD_REGEX: /^[0-9a-f]+-[0-9a-f]+$/,
  63. CACHE_SUBDIR: 'generated-files',
  64. promises: {
  65. queueDirOperation: sinon.stub().callsArg(1),
  66. saveOutputFiles: sinon
  67. .stub()
  68. .resolves({ outputFiles: this.buildFiles, buildId: this.buildId }),
  69. },
  70. }
  71. this.Settings = {
  72. path: {
  73. compilesDir: this.compileBaseDir,
  74. outputDir: this.outputBaseDir,
  75. synctexBaseDir: sinon.stub(),
  76. },
  77. clsi: {
  78. docker: {
  79. image: 'SOMEIMAGE',
  80. },
  81. },
  82. }
  83. this.Settings.path.synctexBaseDir
  84. .withArgs(`${this.projectId}-${this.userId}`)
  85. .returns(this.compileDir)
  86. this.child_process = {
  87. exec: sinon.stub(),
  88. execFile: sinon.stub().yields(),
  89. }
  90. this.CommandRunner = {
  91. canRunSyncTeXInOutputDir: sinon.stub().returns(false),
  92. promises: {
  93. run: sinon.stub().callsFake((_1, _2, _3, _4, _5, _6, compileGroup) => {
  94. if (compileGroup === 'synctex' || compileGroup === 'synctex-output') {
  95. return Promise.resolve({ stdout: this.commandOutput })
  96. } else {
  97. return Promise.resolve({
  98. stdout: 'Encoding: ascii\nWords in text: 2',
  99. })
  100. }
  101. }),
  102. },
  103. }
  104. this.DraftModeManager = {
  105. promises: {
  106. injectDraftMode: sinon.stub().resolves(),
  107. },
  108. }
  109. this.TikzManager = {
  110. promises: {
  111. checkMainFile: sinon.stub().resolves(false),
  112. },
  113. }
  114. this.lock = {
  115. release: sinon.stub(),
  116. }
  117. this.LockManager = {
  118. acquire: sinon.stub().returns(this.lock),
  119. }
  120. this.SynctexOutputParser = {
  121. parseViewOutput: sinon.stub(),
  122. parseEditOutput: sinon.stub(),
  123. }
  124. this.dirStats = {
  125. isDirectory: sinon.stub().returns(true),
  126. }
  127. this.fileStats = {
  128. isFile: sinon.stub().returns(true),
  129. }
  130. this.fsPromises = {
  131. lstat: sinon.stub(),
  132. stat: sinon.stub(),
  133. readFile: sinon.stub(),
  134. mkdir: sinon.stub().resolves(),
  135. rm: sinon.stub().resolves(),
  136. unlink: sinon.stub().resolves(),
  137. rmdir: sinon.stub().resolves(),
  138. }
  139. this.fsPromises.lstat.withArgs(this.compileDir).resolves(this.dirStats)
  140. this.fsPromises.stat
  141. .withArgs(Path.join(this.compileDir, 'output.synctex.gz'))
  142. .resolves(this.fileStats)
  143. this.CLSICacheHandler = {
  144. notifyCLSICacheAboutBuild: sinon.stub(),
  145. downloadLatestCompileCache: sinon.stub().resolves(),
  146. downloadOutputDotSynctexFromCompileCache: sinon.stub().resolves(),
  147. }
  148. this.LatexMetrics = { enableLatexMkMetrics: sinon.stub() }
  149. this.StatsManager = { sampleRequest: sinon.stub().returns(false) }
  150. this.CompileManager = SandboxedModule.require(MODULE_PATH, {
  151. requires: {
  152. './LatexRunner': this.LatexRunner,
  153. './ResourceWriter': this.ResourceWriter,
  154. './OutputFileFinder': this.OutputFileFinder,
  155. './OutputCacheManager': this.OutputCacheManager,
  156. '@overleaf/settings': this.Settings,
  157. '@overleaf/metrics': {
  158. inc: sinon.stub(),
  159. timing: sinon.stub(),
  160. gauge: sinon.stub(),
  161. Timer: sinon.stub().returns({ done: sinon.stub() }),
  162. },
  163. child_process: this.child_process,
  164. './CommandRunner': this.CommandRunner,
  165. './DraftModeManager': this.DraftModeManager,
  166. './TikzManager': this.TikzManager,
  167. './LockManager': this.LockManager,
  168. './SynctexOutputParser': this.SynctexOutputParser,
  169. 'fs/promises': this.fsPromises,
  170. './CLSICacheHandler': this.CLSICacheHandler,
  171. './LatexMetrics': this.LatexMetrics,
  172. './StatsManager': this.StatsManager,
  173. './Metrics': Metrics,
  174. },
  175. })
  176. })
  177. describe('doCompileWithLock', function () {
  178. beforeEach(function () {
  179. this.request = {
  180. resources: this.resources,
  181. rootResourcePath: (this.rootResourcePath = 'main.tex'),
  182. project_id: this.projectId,
  183. user_id: this.userId,
  184. compiler: (this.compiler = 'pdflatex'),
  185. timeout: (this.timeout = 42000),
  186. imageName: (this.image = 'example.com/image'),
  187. flags: (this.flags = ['-file-line-error']),
  188. compileGroup: (this.compileGroup = 'compile-group'),
  189. stopOnFirstError: false,
  190. metricsOpts: {
  191. path: 'clsi-perf',
  192. method: 'minimal',
  193. compile: 'initial',
  194. },
  195. }
  196. this.env = {
  197. OVERLEAF_PROJECT_ID: this.projectId,
  198. }
  199. })
  200. describe('when the project is locked', function () {
  201. beforeEach(async function () {
  202. const error = new Error('locked')
  203. this.LockManager.acquire.throws(error)
  204. await expect(
  205. this.CompileManager.promises.doCompileWithLock(this.request, {}, {})
  206. ).to.be.rejectedWith(error)
  207. })
  208. it('should ensure that the compile directory exists', function () {
  209. expect(this.fsPromises.mkdir).to.have.been.calledWith(this.compileDir, {
  210. recursive: true,
  211. })
  212. })
  213. it('should not run LaTeX', function () {
  214. expect(this.LatexRunner.promises.runLatex).not.to.have.been.called
  215. })
  216. })
  217. describe('normally', function () {
  218. beforeEach(async function () {
  219. this.result = await this.CompileManager.promises.doCompileWithLock(
  220. this.request,
  221. {},
  222. {}
  223. )
  224. })
  225. it('should ensure that the compile directory exists', function () {
  226. expect(this.fsPromises.mkdir).to.have.been.calledWith(this.compileDir, {
  227. recursive: true,
  228. })
  229. })
  230. it('should write the resources to disk', function () {
  231. expect(
  232. this.ResourceWriter.promises.syncResourcesToDisk
  233. ).to.have.been.calledWith(this.request, this.compileDir)
  234. })
  235. it('should run LaTeX', function () {
  236. expect(this.LatexRunner.promises.runLatex).to.have.been.calledWith(
  237. `${this.projectId}-${this.userId}`,
  238. {
  239. directory: this.compileDir,
  240. mainFile: this.rootResourcePath,
  241. compiler: this.compiler,
  242. timeout: this.timeout,
  243. image: this.image,
  244. flags: this.flags,
  245. environment: this.env,
  246. compileGroup: this.compileGroup,
  247. stopOnFirstError: this.request.stopOnFirstError,
  248. stats: sinon.match.object,
  249. timings: sinon.match.object,
  250. }
  251. )
  252. })
  253. it('should find the output files', function () {
  254. expect(
  255. this.OutputFileFinder.promises.findOutputFiles
  256. ).to.have.been.calledWith(this.resources, this.compileDir)
  257. })
  258. it('should return the output files', function () {
  259. expect(this.result.outputFiles).to.equal(this.buildFiles)
  260. })
  261. it('should not inject draft mode by default', function () {
  262. expect(this.DraftModeManager.promises.injectDraftMode).not.to.have.been
  263. .called
  264. })
  265. })
  266. describe('with performance metric collection', function () {
  267. it('should enable latexmk metrics when sampleRequest returns true', async function () {
  268. this.StatsManager.sampleRequest.returns(true)
  269. await this.CompileManager.promises.doCompileWithLock(
  270. this.request,
  271. {},
  272. {}
  273. )
  274. expect(this.LatexMetrics.enableLatexMkMetrics).to.have.been.calledWith(
  275. sinon.match.object
  276. )
  277. })
  278. it('should enable latexmk metrics when sampleRequest returns false', async function () {
  279. this.StatsManager.sampleRequest.returns(false)
  280. await this.CompileManager.promises.doCompileWithLock(
  281. this.request,
  282. {},
  283. {}
  284. )
  285. expect(this.LatexMetrics.enableLatexMkMetrics).to.have.been.calledWith(
  286. sinon.match.object
  287. )
  288. })
  289. it('should enable latexmk metrics when sampleRequest returns undefined', async function () {
  290. this.StatsManager.sampleRequest.returns(undefined)
  291. await this.CompileManager.promises.doCompileWithLock(
  292. this.request,
  293. {},
  294. {}
  295. )
  296. expect(this.LatexMetrics.enableLatexMkMetrics).to.have.been.calledWith(
  297. sinon.match.object
  298. )
  299. })
  300. })
  301. describe('with draft mode', function () {
  302. beforeEach(async function () {
  303. this.request.draft = true
  304. await this.CompileManager.promises.doCompileWithLock(
  305. this.request,
  306. {},
  307. {}
  308. )
  309. })
  310. it('should inject the draft mode header', function () {
  311. expect(
  312. this.DraftModeManager.promises.injectDraftMode
  313. ).to.have.been.calledWith(this.compileDir + '/' + this.rootResourcePath)
  314. })
  315. })
  316. describe('with a check option', function () {
  317. beforeEach(async function () {
  318. this.request.check = 'error'
  319. await this.CompileManager.promises.doCompileWithLock(
  320. this.request,
  321. {},
  322. {}
  323. )
  324. })
  325. it('should run chktex', function () {
  326. expect(this.LatexRunner.promises.runLatex).to.have.been.calledWith(
  327. `${this.projectId}-${this.userId}`,
  328. {
  329. directory: this.compileDir,
  330. mainFile: this.rootResourcePath,
  331. compiler: this.compiler,
  332. timeout: this.timeout,
  333. image: this.image,
  334. flags: this.flags,
  335. environment: {
  336. CHKTEX_OPTIONS: '-nall -e9 -e10 -w15 -w16',
  337. CHKTEX_EXIT_ON_ERROR: 1,
  338. CHKTEX_ULIMIT_OPTIONS: '-t 5 -v 64000',
  339. OVERLEAF_PROJECT_ID: this.projectId,
  340. },
  341. compileGroup: this.compileGroup,
  342. stopOnFirstError: this.request.stopOnFirstError,
  343. stats: sinon.match.object,
  344. timings: sinon.match.object,
  345. }
  346. )
  347. })
  348. })
  349. describe('with a knitr file and check options', function () {
  350. beforeEach(async function () {
  351. this.request.rootResourcePath = 'main.Rtex'
  352. this.request.check = 'error'
  353. await this.CompileManager.promises.doCompileWithLock(
  354. this.request,
  355. {},
  356. {}
  357. )
  358. })
  359. it('should not run chktex', function () {
  360. expect(this.LatexRunner.promises.runLatex).to.have.been.calledWith(
  361. `${this.projectId}-${this.userId}`,
  362. {
  363. directory: this.compileDir,
  364. mainFile: 'main.Rtex',
  365. compiler: this.compiler,
  366. timeout: this.timeout,
  367. image: this.image,
  368. flags: this.flags,
  369. environment: this.env,
  370. compileGroup: this.compileGroup,
  371. stopOnFirstError: this.request.stopOnFirstError,
  372. stats: sinon.match.object,
  373. timings: sinon.match.object,
  374. }
  375. )
  376. })
  377. })
  378. describe('when the compile times out', function () {
  379. beforeEach(async function () {
  380. const error = new Error('timed out!')
  381. error.timedout = true
  382. this.LatexRunner.promises.runLatex.rejects(error)
  383. await expect(
  384. this.CompileManager.promises.doCompileWithLock(this.request, {}, {})
  385. ).to.be.rejected
  386. })
  387. it('should clear the compile directory', function () {
  388. for (const { path } of this.buildFiles) {
  389. expect(this.fsPromises.unlink).to.have.been.calledWith(
  390. this.compileDir + '/' + path
  391. )
  392. }
  393. expect(this.fsPromises.unlink).to.have.been.calledWith(
  394. this.compileDir + '/main.tex'
  395. )
  396. expect(this.fsPromises.rmdir).to.have.been.calledWith(this.compileDir)
  397. })
  398. })
  399. describe('when the compile is manually stopped', function () {
  400. beforeEach(async function () {
  401. const error = new Error('terminated!')
  402. error.terminated = true
  403. this.LatexRunner.promises.runLatex.rejects(error)
  404. await expect(
  405. this.CompileManager.promises.doCompileWithLock(this.request, {}, {})
  406. ).to.be.rejected
  407. })
  408. it('should clear the compile directory', function () {
  409. for (const { path } of this.buildFiles) {
  410. expect(this.fsPromises.unlink).to.have.been.calledWith(
  411. this.compileDir + '/' + path
  412. )
  413. }
  414. expect(this.fsPromises.unlink).to.have.been.calledWith(
  415. this.compileDir + '/main.tex'
  416. )
  417. expect(this.fsPromises.rmdir).to.have.been.calledWith(this.compileDir)
  418. })
  419. })
  420. })
  421. describe('clearProject', function () {
  422. it('should clear the compile directory', async function () {
  423. await this.CompileManager.promises.clearProject(
  424. this.projectId,
  425. this.userId
  426. )
  427. expect(this.fsPromises.rm).to.have.been.calledWith(this.compileDir, {
  428. force: true,
  429. recursive: true,
  430. })
  431. })
  432. })
  433. describe('syncing', function () {
  434. beforeEach(function () {
  435. this.page = 1
  436. this.h = 42.23
  437. this.v = 87.56
  438. this.width = 100.01
  439. this.height = 234.56
  440. this.line = 5
  441. this.column = 3
  442. this.filename = 'main.tex'
  443. })
  444. describe('syncFromCode', function () {
  445. beforeEach(function () {
  446. this.records = [{ page: 1, h: 2, v: 3, width: 4, height: 5 }]
  447. this.SynctexOutputParser.parseViewOutput
  448. .withArgs(this.commandOutput)
  449. .returns(this.records)
  450. })
  451. describe('normal case', function () {
  452. beforeEach(async function () {
  453. this.result = await this.CompileManager.promises.syncFromCode(
  454. this.projectId,
  455. this.userId,
  456. this.filename,
  457. this.line,
  458. this.column,
  459. ''
  460. )
  461. })
  462. it('should execute the synctex binary', function () {
  463. const outputFilePath = `${this.compileDir}/output.pdf`
  464. const inputFilePath = `${this.compileDir}/${this.filename}`
  465. expect(this.CommandRunner.promises.run).to.have.been.calledWith(
  466. `${this.projectId}-${this.userId}`,
  467. [
  468. 'synctex',
  469. 'view',
  470. '-i',
  471. `${this.line}:${this.column}:${inputFilePath}`,
  472. '-o',
  473. outputFilePath,
  474. ],
  475. this.compileDir,
  476. this.Settings.clsi.docker.image,
  477. 60000,
  478. {},
  479. 'synctex'
  480. )
  481. })
  482. it('should return the parsed output', function () {
  483. expect(this.result).to.deep.equal({
  484. codePositions: this.records,
  485. downloadedFromCache: false,
  486. })
  487. })
  488. })
  489. describe('from cache in docker', function () {
  490. beforeEach(async function () {
  491. this.CommandRunner.canRunSyncTeXInOutputDir.returns(true)
  492. this.Settings.path.synctexBaseDir
  493. .withArgs(`${this.projectId}-${this.userId}`)
  494. .returns('/compile')
  495. const errNotFound = new Error()
  496. errNotFound.code = 'ENOENT'
  497. this.outputDir = `${this.outputBaseDir}/${this.projectId}-${this.userId}/${this.OutputCacheManager.CACHE_SUBDIR}/${this.buildId}`
  498. const filename = Path.join(this.outputDir, 'output.synctex.gz')
  499. this.fsPromises.stat
  500. .withArgs(this.outputDir)
  501. .onFirstCall()
  502. .rejects(errNotFound)
  503. this.fsPromises.stat
  504. .withArgs(this.outputDir)
  505. .onSecondCall()
  506. .resolves(this.dirStats)
  507. this.fsPromises.stat.withArgs(filename).resolves(this.fileStats)
  508. this.CLSICacheHandler.downloadOutputDotSynctexFromCompileCache.resolves(
  509. true
  510. )
  511. this.result = await this.CompileManager.promises.syncFromCode(
  512. this.projectId,
  513. this.userId,
  514. this.filename,
  515. this.line,
  516. this.column,
  517. {
  518. imageName: 'image',
  519. editorId: '00000000-0000-0000-0000-000000000000',
  520. buildId: this.buildId,
  521. compileFromClsiCache: true,
  522. }
  523. )
  524. })
  525. it('should run in output dir', function () {
  526. const outputFilePath = '/compile/output.pdf'
  527. const inputFilePath = `/compile/${this.filename}`
  528. expect(this.CommandRunner.promises.run).to.have.been.calledWith(
  529. `${this.projectId}-${this.userId}`,
  530. [
  531. 'synctex',
  532. 'view',
  533. '-i',
  534. `${this.line}:${this.column}:${inputFilePath}`,
  535. '-o',
  536. outputFilePath,
  537. ],
  538. this.outputDir,
  539. 'image',
  540. 60000,
  541. {},
  542. 'synctex-output'
  543. )
  544. })
  545. it('should return the parsed output', function () {
  546. expect(this.result).to.deep.equal({
  547. codePositions: this.records,
  548. downloadedFromCache: true,
  549. })
  550. })
  551. })
  552. describe('with a custom imageName', function () {
  553. const customImageName = 'foo/bar:tag-0'
  554. beforeEach(async function () {
  555. await this.CompileManager.promises.syncFromCode(
  556. this.projectId,
  557. this.userId,
  558. this.filename,
  559. this.line,
  560. this.column,
  561. { imageName: customImageName }
  562. )
  563. })
  564. it('should execute the synctex binary in a custom docker image', function () {
  565. const outputFilePath = `${this.compileDir}/output.pdf`
  566. const inputFilePath = `${this.compileDir}/${this.filename}`
  567. expect(this.CommandRunner.promises.run).to.have.been.calledWith(
  568. `${this.projectId}-${this.userId}`,
  569. [
  570. 'synctex',
  571. 'view',
  572. '-i',
  573. `${this.line}:${this.column}:${inputFilePath}`,
  574. '-o',
  575. outputFilePath,
  576. ],
  577. this.compileDir,
  578. customImageName,
  579. 60000,
  580. {},
  581. 'synctex'
  582. )
  583. })
  584. })
  585. })
  586. describe('syncFromPdf', function () {
  587. beforeEach(function () {
  588. this.records = [{ file: 'main.tex', line: 1, column: 1 }]
  589. this.SynctexOutputParser.parseEditOutput
  590. .withArgs(this.commandOutput, this.compileDir)
  591. .returns(this.records)
  592. })
  593. describe('normal case', function () {
  594. beforeEach(async function () {
  595. this.result = await this.CompileManager.promises.syncFromPdf(
  596. this.projectId,
  597. this.userId,
  598. this.page,
  599. this.h,
  600. this.v,
  601. { imageName: '' }
  602. )
  603. })
  604. it('should execute the synctex binary', function () {
  605. const outputFilePath = `${this.compileDir}/output.pdf`
  606. expect(this.CommandRunner.promises.run).to.have.been.calledWith(
  607. `${this.projectId}-${this.userId}`,
  608. [
  609. 'synctex',
  610. 'edit',
  611. '-o',
  612. `${this.page}:${this.h}:${this.v}:${outputFilePath}`,
  613. ],
  614. this.compileDir,
  615. this.Settings.clsi.docker.image,
  616. 60000,
  617. {}
  618. )
  619. })
  620. it('should return the parsed output', function () {
  621. expect(this.result).to.deep.equal({
  622. pdfPositions: this.records,
  623. downloadedFromCache: false,
  624. })
  625. })
  626. })
  627. describe('with a custom imageName', function () {
  628. const customImageName = 'foo/bar:tag-1'
  629. beforeEach(async function () {
  630. await this.CompileManager.promises.syncFromPdf(
  631. this.projectId,
  632. this.userId,
  633. this.page,
  634. this.h,
  635. this.v,
  636. { imageName: customImageName }
  637. )
  638. })
  639. it('should execute the synctex binary in a custom docker image', function () {
  640. const outputFilePath = `${this.compileDir}/output.pdf`
  641. expect(this.CommandRunner.promises.run).to.have.been.calledWith(
  642. `${this.projectId}-${this.userId}`,
  643. [
  644. 'synctex',
  645. 'edit',
  646. '-o',
  647. `${this.page}:${this.h}:${this.v}:${outputFilePath}`,
  648. ],
  649. this.compileDir,
  650. customImageName,
  651. 60000,
  652. {}
  653. )
  654. })
  655. })
  656. })
  657. })
  658. describe('wordcount', function () {
  659. beforeEach(async function () {
  660. this.timeout = 60 * 1000
  661. this.filename = 'main.tex'
  662. this.image = 'example.com/image'
  663. this.result = await this.CompileManager.promises.wordcount(
  664. this.projectId,
  665. this.userId,
  666. this.filename,
  667. this.image
  668. )
  669. })
  670. it('should run the texcount command', function () {
  671. this.filePath = `$COMPILE_DIR/${this.filename}`
  672. this.command = ['texcount', '-nocol', '-inc', this.filePath]
  673. expect(this.CommandRunner.promises.run).to.have.been.calledWith(
  674. `${this.projectId}-${this.userId}`,
  675. this.command,
  676. this.compileDir,
  677. this.image,
  678. this.timeout,
  679. {}
  680. )
  681. })
  682. it('should return the parsed output', function () {
  683. expect(this.result).to.deep.equal({
  684. encode: 'ascii',
  685. textWords: 2,
  686. headWords: 0,
  687. outside: 0,
  688. headers: 0,
  689. elements: 0,
  690. mathInline: 0,
  691. mathDisplay: 0,
  692. errors: 0,
  693. messages: '',
  694. })
  695. })
  696. })
  697. })