LatexRunner.test.js 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  1. import { vi, expect, describe, beforeEach, it } from 'vitest'
  2. import sinon from 'sinon'
  3. import fs from 'node:fs'
  4. import path from 'node:path'
  5. const MODULE_PATH = path.join(
  6. import.meta.dirname,
  7. '../../../app/js/LatexRunner'
  8. )
  9. describe('LatexRunner', () => {
  10. beforeEach(async ctx => {
  11. ctx.Settings = {
  12. docker: {
  13. socketPath: '/var/run/docker.sock',
  14. },
  15. }
  16. ctx.commandRunnerOutput = {
  17. stdout: 'this is stdout',
  18. stderr: 'this is stderr',
  19. }
  20. ctx.CommandRunner = {
  21. run: sinon.stub().yields(null, ctx.commandRunnerOutput),
  22. }
  23. ctx.fs = {
  24. writeFile: sinon.stub().yields(),
  25. unlink: sinon
  26. .stub()
  27. .yields(new Error('ENOENT: no such file or directory, unlink ...')),
  28. }
  29. vi.doMock('@overleaf/settings', () => ({
  30. default: ctx.Settings,
  31. }))
  32. vi.doMock('../../../app/js/CommandRunner', () => ({
  33. default: ctx.CommandRunner,
  34. }))
  35. vi.doMock('fs', () => ({
  36. default: ctx.fs,
  37. }))
  38. ctx.LatexRunner = (await import(MODULE_PATH)).default
  39. ctx.directory = '/local/compile/directory'
  40. ctx.mainFile = 'main-file.tex'
  41. ctx.compiler = 'pdflatex'
  42. ctx.image = 'example.com/image'
  43. ctx.compileGroup = 'compile-group'
  44. ctx.callback = sinon.stub()
  45. ctx.project_id = 'project-id-123'
  46. ctx.env = { foo: '123' }
  47. ctx.timeout = 42000
  48. ctx.flags = []
  49. ctx.stopOnFirstError = false
  50. ctx.stats = {}
  51. ctx.timings = {}
  52. ctx.call = function (callback) {
  53. this.LatexRunner.runLatex(
  54. this.project_id,
  55. {
  56. directory: this.directory,
  57. mainFile: this.mainFile,
  58. compiler: this.compiler,
  59. timeout: this.timeout,
  60. image: this.image,
  61. environment: this.env,
  62. compileGroup: this.compileGroup,
  63. flags: this.flags,
  64. stopOnFirstError: this.stopOnFirstError,
  65. timings: this.timings,
  66. stats: this.stats,
  67. },
  68. callback
  69. )
  70. }
  71. })
  72. describe('runLatex', () => {
  73. describe('normally', () => {
  74. beforeEach(async ctx => {
  75. await new Promise((resolve, reject) => {
  76. ctx.call(err => {
  77. if (err) reject(err)
  78. resolve()
  79. })
  80. })
  81. })
  82. it('should run the latex command', ctx => {
  83. ctx.CommandRunner.run.should.have.been.calledWith(
  84. ctx.project_id,
  85. [
  86. 'latexmk',
  87. '-cd',
  88. '-jobname=output',
  89. '-auxdir=$COMPILE_DIR',
  90. '-outdir=$COMPILE_DIR',
  91. '-synctex=1',
  92. '-interaction=batchmode',
  93. '-time',
  94. '-f',
  95. '-pdf',
  96. '$COMPILE_DIR/main-file.tex',
  97. ],
  98. ctx.directory,
  99. ctx.image,
  100. ctx.timeout,
  101. ctx.env,
  102. ctx.compileGroup
  103. )
  104. })
  105. it('should record the stdout and stderr', ctx => {
  106. ctx.fs.writeFile.should.have.been.calledWith(
  107. ctx.directory + '/' + 'output.stdout',
  108. 'this is stdout',
  109. { flag: 'wx' }
  110. )
  111. ctx.fs.writeFile.should.have.been.calledWith(
  112. ctx.directory + '/' + 'output.stderr',
  113. 'this is stderr',
  114. { flag: 'wx' }
  115. )
  116. ctx.fs.unlink.should.have.been.calledWith(
  117. ctx.directory + '/' + 'output.stdout'
  118. )
  119. ctx.fs.unlink.should.have.been.calledWith(
  120. ctx.directory + '/' + 'output.stderr'
  121. )
  122. })
  123. it('should not record cpu metrics', ctx => {
  124. expect(ctx.timings['cpu-percent']).to.not.exist
  125. expect(ctx.timings['cpu-time']).to.not.exist
  126. expect(ctx.timings['sys-time']).to.not.exist
  127. })
  128. })
  129. describe('with a different compiler', () => {
  130. beforeEach(async ctx => {
  131. await new Promise((resolve, reject) => {
  132. ctx.compiler = 'lualatex'
  133. ctx.call(err => {
  134. if (err) reject(err)
  135. resolve()
  136. })
  137. })
  138. })
  139. it('should set the appropriate latexmk flag', ctx => {
  140. ctx.CommandRunner.run.should.have.been.calledWith(ctx.project_id, [
  141. 'latexmk',
  142. '-cd',
  143. '-jobname=output',
  144. '-auxdir=$COMPILE_DIR',
  145. '-outdir=$COMPILE_DIR',
  146. '-synctex=1',
  147. '-interaction=batchmode',
  148. '-time',
  149. '-f',
  150. '-lualatex',
  151. '$COMPILE_DIR/main-file.tex',
  152. ])
  153. })
  154. })
  155. describe('with time -v', () => {
  156. beforeEach(async ctx => {
  157. await new Promise((resolve, reject) => {
  158. ctx.commandRunnerOutput.stderr =
  159. '\tCommand being timed: "sh -c timeout 1 yes > /dev/null"\n' +
  160. '\tUser time (seconds): 0.28\n' +
  161. '\tSystem time (seconds): 0.70\n' +
  162. '\tPercent of CPU this job got: 98%\n'
  163. ctx.call(err => {
  164. if (err) reject(err)
  165. resolve()
  166. })
  167. })
  168. })
  169. it('should record cpu metrics', ctx => {
  170. expect(ctx.timings['cpu-percent']).to.equal(98)
  171. expect(ctx.timings['cpu-time']).to.equal(0.28)
  172. expect(ctx.timings['sys-time']).to.equal(0.7)
  173. })
  174. })
  175. describe('with an .Rtex main file', () => {
  176. beforeEach(async ctx => {
  177. await new Promise((resolve, reject) => {
  178. ctx.mainFile = 'main-file.Rtex'
  179. ctx.call(err => {
  180. if (err) reject(err)
  181. resolve()
  182. })
  183. })
  184. })
  185. it('should run the latex command on the equivalent .tex file', ctx => {
  186. const command = ctx.CommandRunner.run.args[0][1]
  187. const mainFile = command.slice(-1)[0]
  188. mainFile.should.equal('$COMPILE_DIR/main-file.tex')
  189. })
  190. })
  191. describe('with a flags option', () => {
  192. beforeEach(async ctx => {
  193. await new Promise((resolve, reject) => {
  194. ctx.flags = ['-shell-restricted', '-halt-on-error']
  195. ctx.call(err => {
  196. if (err) reject(err)
  197. resolve()
  198. })
  199. })
  200. })
  201. it('should include the flags in the command', ctx => {
  202. const command = ctx.CommandRunner.run.args[0][1]
  203. const flags = command.filter(
  204. arg => arg === '-shell-restricted' || arg === '-halt-on-error'
  205. )
  206. flags.length.should.equal(2)
  207. flags[0].should.equal('-shell-restricted')
  208. flags[1].should.equal('-halt-on-error')
  209. })
  210. })
  211. describe('with the stopOnFirstError option', () => {
  212. beforeEach(async ctx => {
  213. await new Promise((resolve, reject) => {
  214. ctx.stopOnFirstError = true
  215. ctx.call(err => {
  216. if (err) reject(err)
  217. resolve()
  218. })
  219. })
  220. })
  221. it('should set the appropriate flags', ctx => {
  222. ctx.CommandRunner.run.should.have.been.calledWith(ctx.project_id, [
  223. 'latexmk',
  224. '-cd',
  225. '-jobname=output',
  226. '-auxdir=$COMPILE_DIR',
  227. '-outdir=$COMPILE_DIR',
  228. '-synctex=1',
  229. '-interaction=batchmode',
  230. '-time',
  231. '-halt-on-error',
  232. '-pdf',
  233. '$COMPILE_DIR/main-file.tex',
  234. ])
  235. })
  236. })
  237. describe('with old latexmk timing output', () => {
  238. beforeEach(async ctx => {
  239. await new Promise((resolve, reject) => {
  240. ctx.commandRunnerOutput.stdout = fs.readFileSync(
  241. path.join(import.meta.dirname, 'fixtures', 'latexmk1.txt'),
  242. 'utf-8'
  243. )
  244. // pass in the `latexmk` property to signal that we want to receive parsed stats
  245. ctx.stats.latexmk = {}
  246. ctx.call(err => {
  247. if (err) reject(err)
  248. resolve()
  249. })
  250. })
  251. })
  252. it('should parse latexmk 4.52c (2017) timing information', ctx => {
  253. expect(ctx.stats.latexmk).to.deep.equal({
  254. 'latexmk-rule-times': [
  255. { rule: 'makeindex', time_ms: 30 },
  256. { rule: 'bibtex', time_ms: 40 },
  257. { rule: 'latex', time_ms: 690 },
  258. { rule: 'makeindex', time_ms: 40 },
  259. { rule: 'bibtex', time_ms: 39 },
  260. { rule: 'latex', time_ms: 750 },
  261. { rule: 'makeindex', time_ms: 39 },
  262. { rule: 'bibtex', time_ms: 20 },
  263. { rule: 'latex', time_ms: 770 },
  264. ],
  265. 'latexmk-rule-signature':
  266. 'makeindex,bibtex,latex,makeindex,bibtex,latex,makeindex,bibtex,latex',
  267. 'latexmk-rules-run': 9,
  268. 'latexmk-time': { total: 2930 },
  269. 'latexmk-img-times': [],
  270. })
  271. })
  272. })
  273. describe('with modern latexmk timing output', () => {
  274. beforeEach(async ctx => {
  275. await new Promise((resolve, reject) => {
  276. ctx.commandRunnerOutput.stdout = fs.readFileSync(
  277. path.join(import.meta.dirname, 'fixtures', 'latexmk2.txt'),
  278. 'utf-8'
  279. )
  280. // pass in the `latexmk` property to signal that we want to receive parsed stats
  281. ctx.stats.latexmk = {}
  282. ctx.call(err => {
  283. if (err) reject(err)
  284. resolve()
  285. })
  286. })
  287. })
  288. it('should parse latexmk 4.83 (2024) timing information', ctx => {
  289. expect(ctx.stats.latexmk).to.deep.equal({
  290. 'latexmk-rule-times': [
  291. { rule: 'latex', time_ms: 1880 },
  292. { rule: 'makeindex', time_ms: 50 },
  293. { rule: 'bibtex', time_ms: 50 },
  294. { rule: 'latex', time_ms: 2180 },
  295. ],
  296. 'latexmk-rule-signature': 'latex,makeindex,bibtex,latex',
  297. 'latexmk-time': {
  298. total: 4770,
  299. invoked: 4160,
  300. other: 610,
  301. },
  302. 'latexmk-clock-time': 4870,
  303. 'latexmk-rules-run': 4,
  304. 'latexmk-img-times': [],
  305. })
  306. })
  307. })
  308. })
  309. })