CompileManagerTests.js 22 KB

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