| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335 |
- import { vi, expect, describe, beforeEach, it } from 'vitest'
- import sinon from 'sinon'
- import fs from 'node:fs'
- import path from 'node:path'
- const MODULE_PATH = path.join(
- import.meta.dirname,
- '../../../app/js/LatexRunner'
- )
- describe('LatexRunner', () => {
- beforeEach(async ctx => {
- ctx.Settings = {
- docker: {
- socketPath: '/var/run/docker.sock',
- },
- }
- ctx.commandRunnerOutput = {
- stdout: 'this is stdout',
- stderr: 'this is stderr',
- }
- ctx.CommandRunner = {
- run: sinon.stub().yields(null, ctx.commandRunnerOutput),
- }
- ctx.fs = {
- writeFile: sinon.stub().yields(),
- unlink: sinon
- .stub()
- .yields(new Error('ENOENT: no such file or directory, unlink ...')),
- }
- vi.doMock('@overleaf/settings', () => ({
- default: ctx.Settings,
- }))
- vi.doMock('../../../app/js/CommandRunner', () => ({
- default: ctx.CommandRunner,
- }))
- vi.doMock('fs', () => ({
- default: ctx.fs,
- }))
- ctx.LatexRunner = (await import(MODULE_PATH)).default
- ctx.directory = '/local/compile/directory'
- ctx.mainFile = 'main-file.tex'
- ctx.compiler = 'pdflatex'
- ctx.image = 'example.com/image'
- ctx.compileGroup = 'compile-group'
- ctx.callback = sinon.stub()
- ctx.project_id = 'project-id-123'
- ctx.env = { foo: '123' }
- ctx.timeout = 42000
- ctx.flags = []
- ctx.stopOnFirstError = false
- ctx.stats = {}
- ctx.timings = {}
- ctx.call = function (callback) {
- this.LatexRunner.runLatex(
- this.project_id,
- {
- directory: this.directory,
- mainFile: this.mainFile,
- compiler: this.compiler,
- timeout: this.timeout,
- image: this.image,
- environment: this.env,
- compileGroup: this.compileGroup,
- flags: this.flags,
- stopOnFirstError: this.stopOnFirstError,
- timings: this.timings,
- stats: this.stats,
- },
- callback
- )
- }
- })
- describe('runLatex', () => {
- describe('normally', () => {
- beforeEach(async ctx => {
- await new Promise((resolve, reject) => {
- ctx.call(err => {
- if (err) reject(err)
- resolve()
- })
- })
- })
- it('should run the latex command', ctx => {
- ctx.CommandRunner.run.should.have.been.calledWith(
- ctx.project_id,
- [
- 'latexmk',
- '-cd',
- '-jobname=output',
- '-auxdir=$COMPILE_DIR',
- '-outdir=$COMPILE_DIR',
- '-synctex=1',
- '-interaction=batchmode',
- '-time',
- '-f',
- '-pdf',
- '$COMPILE_DIR/main-file.tex',
- ],
- ctx.directory,
- ctx.image,
- ctx.timeout,
- ctx.env,
- ctx.compileGroup
- )
- })
- it('should record the stdout and stderr', ctx => {
- ctx.fs.writeFile.should.have.been.calledWith(
- ctx.directory + '/' + 'output.stdout',
- 'this is stdout',
- { flag: 'wx' }
- )
- ctx.fs.writeFile.should.have.been.calledWith(
- ctx.directory + '/' + 'output.stderr',
- 'this is stderr',
- { flag: 'wx' }
- )
- ctx.fs.unlink.should.have.been.calledWith(
- ctx.directory + '/' + 'output.stdout'
- )
- ctx.fs.unlink.should.have.been.calledWith(
- ctx.directory + '/' + 'output.stderr'
- )
- })
- it('should not record cpu metrics', ctx => {
- expect(ctx.timings['cpu-percent']).to.not.exist
- expect(ctx.timings['cpu-time']).to.not.exist
- expect(ctx.timings['sys-time']).to.not.exist
- })
- })
- describe('with a different compiler', () => {
- beforeEach(async ctx => {
- await new Promise((resolve, reject) => {
- ctx.compiler = 'lualatex'
- ctx.call(err => {
- if (err) reject(err)
- resolve()
- })
- })
- })
- it('should set the appropriate latexmk flag', ctx => {
- ctx.CommandRunner.run.should.have.been.calledWith(ctx.project_id, [
- 'latexmk',
- '-cd',
- '-jobname=output',
- '-auxdir=$COMPILE_DIR',
- '-outdir=$COMPILE_DIR',
- '-synctex=1',
- '-interaction=batchmode',
- '-time',
- '-f',
- '-lualatex',
- '$COMPILE_DIR/main-file.tex',
- ])
- })
- })
- describe('with time -v', () => {
- beforeEach(async ctx => {
- await new Promise((resolve, reject) => {
- ctx.commandRunnerOutput.stderr =
- '\tCommand being timed: "sh -c timeout 1 yes > /dev/null"\n' +
- '\tUser time (seconds): 0.28\n' +
- '\tSystem time (seconds): 0.70\n' +
- '\tPercent of CPU this job got: 98%\n'
- ctx.call(err => {
- if (err) reject(err)
- resolve()
- })
- })
- })
- it('should record cpu metrics', ctx => {
- expect(ctx.timings['cpu-percent']).to.equal(98)
- expect(ctx.timings['cpu-time']).to.equal(0.28)
- expect(ctx.timings['sys-time']).to.equal(0.7)
- })
- })
- describe('with an .Rtex main file', () => {
- beforeEach(async ctx => {
- await new Promise((resolve, reject) => {
- ctx.mainFile = 'main-file.Rtex'
- ctx.call(err => {
- if (err) reject(err)
- resolve()
- })
- })
- })
- it('should run the latex command on the equivalent .tex file', ctx => {
- const command = ctx.CommandRunner.run.args[0][1]
- const mainFile = command.slice(-1)[0]
- mainFile.should.equal('$COMPILE_DIR/main-file.tex')
- })
- })
- describe('with a flags option', () => {
- beforeEach(async ctx => {
- await new Promise((resolve, reject) => {
- ctx.flags = ['-shell-restricted', '-halt-on-error']
- ctx.call(err => {
- if (err) reject(err)
- resolve()
- })
- })
- })
- it('should include the flags in the command', ctx => {
- const command = ctx.CommandRunner.run.args[0][1]
- const flags = command.filter(
- arg => arg === '-shell-restricted' || arg === '-halt-on-error'
- )
- flags.length.should.equal(2)
- flags[0].should.equal('-shell-restricted')
- flags[1].should.equal('-halt-on-error')
- })
- })
- describe('with the stopOnFirstError option', () => {
- beforeEach(async ctx => {
- await new Promise((resolve, reject) => {
- ctx.stopOnFirstError = true
- ctx.call(err => {
- if (err) reject(err)
- resolve()
- })
- })
- })
- it('should set the appropriate flags', ctx => {
- ctx.CommandRunner.run.should.have.been.calledWith(ctx.project_id, [
- 'latexmk',
- '-cd',
- '-jobname=output',
- '-auxdir=$COMPILE_DIR',
- '-outdir=$COMPILE_DIR',
- '-synctex=1',
- '-interaction=batchmode',
- '-time',
- '-halt-on-error',
- '-pdf',
- '$COMPILE_DIR/main-file.tex',
- ])
- })
- })
- describe('with old latexmk timing output', () => {
- beforeEach(async ctx => {
- await new Promise((resolve, reject) => {
- ctx.commandRunnerOutput.stdout = fs.readFileSync(
- path.join(import.meta.dirname, 'fixtures', 'latexmk1.txt'),
- 'utf-8'
- )
- // pass in the `latexmk` property to signal that we want to receive parsed stats
- ctx.stats.latexmk = {}
- ctx.call(err => {
- if (err) reject(err)
- resolve()
- })
- })
- })
- it('should parse latexmk 4.52c (2017) timing information', ctx => {
- expect(ctx.stats.latexmk).to.deep.equal({
- 'latexmk-rule-times': [
- { rule: 'makeindex', time_ms: 30 },
- { rule: 'bibtex', time_ms: 40 },
- { rule: 'latex', time_ms: 690 },
- { rule: 'makeindex', time_ms: 40 },
- { rule: 'bibtex', time_ms: 39 },
- { rule: 'latex', time_ms: 750 },
- { rule: 'makeindex', time_ms: 39 },
- { rule: 'bibtex', time_ms: 20 },
- { rule: 'latex', time_ms: 770 },
- ],
- 'latexmk-rule-signature':
- 'makeindex,bibtex,latex,makeindex,bibtex,latex,makeindex,bibtex,latex',
- 'latexmk-rules-run': 9,
- 'latexmk-time': { total: 2930 },
- 'latexmk-img-times': [],
- })
- })
- })
- describe('with modern latexmk timing output', () => {
- beforeEach(async ctx => {
- await new Promise((resolve, reject) => {
- ctx.commandRunnerOutput.stdout = fs.readFileSync(
- path.join(import.meta.dirname, 'fixtures', 'latexmk2.txt'),
- 'utf-8'
- )
- // pass in the `latexmk` property to signal that we want to receive parsed stats
- ctx.stats.latexmk = {}
- ctx.call(err => {
- if (err) reject(err)
- resolve()
- })
- })
- })
- it('should parse latexmk 4.83 (2024) timing information', ctx => {
- expect(ctx.stats.latexmk).to.deep.equal({
- 'latexmk-rule-times': [
- { rule: 'latex', time_ms: 1880 },
- { rule: 'makeindex', time_ms: 50 },
- { rule: 'bibtex', time_ms: 50 },
- { rule: 'latex', time_ms: 2180 },
- ],
- 'latexmk-rule-signature': 'latex,makeindex,bibtex,latex',
- 'latexmk-time': {
- total: 4770,
- invoked: 4160,
- other: 610,
- },
- 'latexmk-clock-time': 4870,
- 'latexmk-rules-run': 4,
- 'latexmk-img-times': [],
- })
- })
- })
- })
- })
|