LatexRunnerTests.js 8.6 KB

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