mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-23 09:09:36 +02:00
* [web] consolidate clsi downloads and add zod validation * [validation-tools] make prettier happy * [web] make clsiServerId optional * [web] fix type of buildId * [web] gracefully handle ObjectId * [web] fix type of buildId * [monorepo] address review feedback - cjs export - update module path in comments - skip adding ?clsiserverid if not set - allow nested output file download for submissions and add tests * [web] address review feedback * [web] cache one more zod schema * [web] fix unit tests GitOrigin-RevId: 0a1e618955983e035defd6d3c0528b81e0e85c95
774 lines
22 KiB
JavaScript
774 lines
22 KiB
JavaScript
import { vi, expect, describe, beforeEach, it } from 'vitest'
|
|
import Path from 'node:path'
|
|
import sinon from 'sinon'
|
|
import Metrics from '../../../app/js/Metrics.js'
|
|
|
|
const MODULE_PATH = Path.join(
|
|
import.meta.dirname,
|
|
'../../../app/js/CompileManager'
|
|
)
|
|
|
|
describe('CompileManager', () => {
|
|
beforeEach(async ctx => {
|
|
ctx.projectId = 'project-id-123'
|
|
ctx.userId = '1234'
|
|
ctx.resources = 'mock-resources'
|
|
ctx.outputFiles = [
|
|
{
|
|
path: 'output.log',
|
|
type: 'log',
|
|
},
|
|
{
|
|
path: 'output.pdf',
|
|
type: 'pdf',
|
|
},
|
|
]
|
|
ctx.buildFiles = [
|
|
{
|
|
path: 'output.log',
|
|
type: 'log',
|
|
build: '1234-5678',
|
|
},
|
|
{
|
|
path: 'output.pdf',
|
|
type: 'pdf',
|
|
build: '1234-5678',
|
|
},
|
|
]
|
|
ctx.buildId = '00000000000-0000000000000000'
|
|
ctx.commandOutput = 'Dummy output'
|
|
ctx.compileBaseDir = '/compile/dir'
|
|
ctx.outputBaseDir = '/output/dir'
|
|
ctx.compileDir = `${ctx.compileBaseDir}/${ctx.projectId}-${ctx.userId}`
|
|
ctx.outputDir = `${ctx.outputBaseDir}/${ctx.projectId}-${ctx.userId}`
|
|
|
|
ctx.LatexRunner = {
|
|
promises: {
|
|
runLatex: sinon.stub().resolves({}),
|
|
},
|
|
}
|
|
ctx.ResourceWriter = {
|
|
promises: {
|
|
syncResourcesToDisk: sinon.stub().resolves(ctx.resources),
|
|
},
|
|
}
|
|
ctx.OutputFileFinder = {
|
|
promises: {
|
|
findOutputFiles: sinon.stub().resolves({
|
|
outputFiles: ctx.outputFiles,
|
|
allEntries: ctx.outputFiles.map(f => f.path).concat(['main.tex']),
|
|
}),
|
|
},
|
|
}
|
|
ctx.OutputCacheManager = {
|
|
BUILD_REGEX: /^[0-9a-f]+-[0-9a-f]+$/,
|
|
CACHE_SUBDIR: 'generated-files',
|
|
promises: {
|
|
queueDirOperation: sinon.stub().callsArg(1),
|
|
saveOutputFiles: sinon
|
|
.stub()
|
|
.resolves({ outputFiles: ctx.buildFiles, buildId: ctx.buildId }),
|
|
},
|
|
}
|
|
ctx.Settings = {
|
|
path: {
|
|
compilesDir: ctx.compileBaseDir,
|
|
outputDir: ctx.outputBaseDir,
|
|
synctexBaseDir: sinon.stub(),
|
|
},
|
|
clsi: {
|
|
docker: {
|
|
image: 'SOMEIMAGE',
|
|
},
|
|
},
|
|
}
|
|
ctx.Settings.path.synctexBaseDir
|
|
.withArgs(`${ctx.projectId}-${ctx.userId}`)
|
|
.returns(ctx.compileDir)
|
|
ctx.child_process = {
|
|
exec: sinon.stub(),
|
|
execFile: sinon.stub().yields(),
|
|
}
|
|
ctx.CommandRunner = {
|
|
canRunSyncTeXInOutputDir: sinon.stub().returns(false),
|
|
promises: {
|
|
run: sinon.stub().callsFake((_1, _2, _3, _4, _5, _6, compileGroup) => {
|
|
if (compileGroup === 'synctex' || compileGroup === 'synctex-output') {
|
|
return Promise.resolve({ stdout: ctx.commandOutput })
|
|
} else {
|
|
return Promise.resolve({
|
|
stdout: 'Encoding: ascii\nWords in text: 2',
|
|
})
|
|
}
|
|
}),
|
|
},
|
|
}
|
|
ctx.DraftModeManager = {
|
|
promises: {
|
|
injectDraftMode: sinon.stub().resolves(),
|
|
},
|
|
}
|
|
ctx.TikzManager = {
|
|
promises: {
|
|
checkMainFile: sinon.stub().resolves(false),
|
|
},
|
|
}
|
|
ctx.lock = {
|
|
release: sinon.stub(),
|
|
}
|
|
ctx.LockManager = {
|
|
acquire: sinon.stub().returns(ctx.lock),
|
|
}
|
|
ctx.SynctexOutputParser = {
|
|
parseViewOutput: sinon.stub(),
|
|
parseEditOutput: sinon.stub(),
|
|
}
|
|
|
|
ctx.dirStats = {
|
|
isDirectory: sinon.stub().returns(true),
|
|
}
|
|
ctx.fileStats = {
|
|
isFile: sinon.stub().returns(true),
|
|
}
|
|
ctx.fsPromises = {
|
|
lstat: sinon.stub(),
|
|
stat: sinon.stub(),
|
|
readFile: sinon.stub(),
|
|
mkdir: sinon.stub().resolves(),
|
|
rm: sinon.stub().resolves(),
|
|
unlink: sinon.stub().resolves(),
|
|
rmdir: sinon.stub().resolves(),
|
|
}
|
|
ctx.fsPromises.lstat.withArgs(ctx.compileDir).resolves(ctx.dirStats)
|
|
ctx.fsPromises.stat
|
|
.withArgs(Path.join(ctx.compileDir, 'output.synctex.gz'))
|
|
.resolves(ctx.fileStats)
|
|
|
|
ctx.CLSICacheHandler = {
|
|
notifyCLSICacheAboutBuild: sinon.stub(),
|
|
downloadLatestCompileCache: sinon.stub().resolves(),
|
|
downloadOutputDotSynctexFromCompileCache: sinon.stub().resolves(),
|
|
}
|
|
|
|
ctx.LatexMetrics = { enableLatexMkMetrics: sinon.stub() }
|
|
|
|
ctx.StatsManager = { sampleRequest: sinon.stub().returns(false) }
|
|
|
|
vi.doMock('../../../app/js/LatexRunner', () => ({
|
|
default: ctx.LatexRunner,
|
|
}))
|
|
|
|
vi.doMock('../../../app/js/ResourceWriter', () => ({
|
|
default: ctx.ResourceWriter,
|
|
}))
|
|
|
|
vi.doMock('../../../app/js/OutputFileFinder', () => ({
|
|
default: ctx.OutputFileFinder,
|
|
}))
|
|
|
|
vi.doMock('../../../app/js/OutputCacheManager', () => ({
|
|
default: ctx.OutputCacheManager,
|
|
}))
|
|
|
|
vi.doMock('@overleaf/settings', () => ({
|
|
default: ctx.Settings,
|
|
}))
|
|
|
|
vi.doMock('@overleaf/metrics', () => ({
|
|
default: {
|
|
inc: sinon.stub(),
|
|
timing: sinon.stub(),
|
|
gauge: sinon.stub(),
|
|
Timer: sinon.stub().returns({ done: sinon.stub() }),
|
|
},
|
|
}))
|
|
|
|
vi.doMock('child_process', () => ({
|
|
default: ctx.child_process,
|
|
}))
|
|
|
|
vi.doMock('../../../app/js/CommandRunner', () => ({
|
|
default: ctx.CommandRunner,
|
|
}))
|
|
|
|
vi.doMock('../../../app/js/DraftModeManager', () => ({
|
|
default: ctx.DraftModeManager,
|
|
}))
|
|
|
|
vi.doMock('../../../app/js/TikzManager', () => ({
|
|
default: ctx.TikzManager,
|
|
}))
|
|
|
|
vi.doMock('../../../app/js/LockManager', () => ({
|
|
default: ctx.LockManager,
|
|
}))
|
|
|
|
vi.doMock('../../../app/js/SynctexOutputParser', () => ({
|
|
default: ctx.SynctexOutputParser,
|
|
}))
|
|
|
|
vi.doMock('fs/promises', () => ({
|
|
default: ctx.fsPromises,
|
|
}))
|
|
|
|
vi.doMock('../../../app/js/CLSICacheHandler', () => ({
|
|
default: ctx.CLSICacheHandler,
|
|
}))
|
|
|
|
vi.doMock('../../../app/js/LatexMetrics', () => ({
|
|
default: ctx.LatexMetrics,
|
|
}))
|
|
|
|
vi.doMock('../../../app/js/StatsManager', () => ({
|
|
default: ctx.StatsManager,
|
|
}))
|
|
|
|
vi.doMock('../../../app/js/Metrics', () => ({
|
|
default: Metrics,
|
|
}))
|
|
|
|
ctx.CompileManager = (await import(MODULE_PATH)).default
|
|
})
|
|
|
|
describe('doCompileWithLock', () => {
|
|
beforeEach(ctx => {
|
|
ctx.request = {
|
|
resources: ctx.resources,
|
|
rootResourcePath: (ctx.rootResourcePath = 'main.tex'),
|
|
project_id: ctx.projectId,
|
|
user_id: ctx.userId,
|
|
compiler: (ctx.compiler = 'pdflatex'),
|
|
timeout: (ctx.timeout = 42000),
|
|
imageName: (ctx.image = 'example.com/image'),
|
|
flags: (ctx.flags = ['-file-line-error']),
|
|
compileGroup: (ctx.compileGroup = 'compile-group'),
|
|
stopOnFirstError: false,
|
|
metricsOpts: {
|
|
path: 'clsi-perf',
|
|
method: 'minimal',
|
|
compile: 'initial',
|
|
},
|
|
}
|
|
ctx.env = {
|
|
OVERLEAF_PROJECT_ID: ctx.projectId,
|
|
}
|
|
})
|
|
|
|
describe('when the project is locked', () => {
|
|
beforeEach(async ctx => {
|
|
const error = new Error('locked')
|
|
ctx.LockManager.acquire.throws(error)
|
|
await expect(
|
|
ctx.CompileManager.promises.doCompileWithLock(ctx.request, {}, {})
|
|
).to.be.rejectedWith(error)
|
|
})
|
|
|
|
it('should ensure that the compile directory exists', ctx => {
|
|
expect(ctx.fsPromises.mkdir).to.have.been.calledWith(ctx.compileDir, {
|
|
recursive: true,
|
|
})
|
|
})
|
|
|
|
it('should not run LaTeX', ctx => {
|
|
expect(ctx.LatexRunner.promises.runLatex).not.to.have.been.called
|
|
})
|
|
})
|
|
|
|
describe('normally', () => {
|
|
beforeEach(async ctx => {
|
|
ctx.result = await ctx.CompileManager.promises.doCompileWithLock(
|
|
ctx.request,
|
|
{},
|
|
{}
|
|
)
|
|
})
|
|
|
|
it('should ensure that the compile directory exists', ctx => {
|
|
expect(ctx.fsPromises.mkdir).to.have.been.calledWith(ctx.compileDir, {
|
|
recursive: true,
|
|
})
|
|
})
|
|
|
|
it('should write the resources to disk', ctx => {
|
|
expect(
|
|
ctx.ResourceWriter.promises.syncResourcesToDisk
|
|
).to.have.been.calledWith(ctx.request, ctx.compileDir)
|
|
})
|
|
|
|
it('should run LaTeX', ctx => {
|
|
expect(ctx.LatexRunner.promises.runLatex).to.have.been.calledWith(
|
|
`${ctx.projectId}-${ctx.userId}`,
|
|
{
|
|
directory: ctx.compileDir,
|
|
mainFile: ctx.rootResourcePath,
|
|
compiler: ctx.compiler,
|
|
timeout: ctx.timeout,
|
|
image: ctx.image,
|
|
flags: ctx.flags,
|
|
environment: ctx.env,
|
|
compileGroup: ctx.compileGroup,
|
|
stopOnFirstError: ctx.request.stopOnFirstError,
|
|
stats: sinon.match.object,
|
|
timings: sinon.match.object,
|
|
}
|
|
)
|
|
})
|
|
|
|
it('should find the output files', ctx => {
|
|
expect(
|
|
ctx.OutputFileFinder.promises.findOutputFiles
|
|
).to.have.been.calledWith(ctx.resources, ctx.compileDir)
|
|
})
|
|
|
|
it('should return the output files', ctx => {
|
|
expect(ctx.result.outputFiles).to.equal(ctx.buildFiles)
|
|
})
|
|
|
|
it('should not inject draft mode by default', ctx => {
|
|
expect(ctx.DraftModeManager.promises.injectDraftMode).not.to.have.been
|
|
.called
|
|
})
|
|
})
|
|
|
|
describe('with performance metric collection', () => {
|
|
it('should enable latexmk metrics when sampleRequest returns true', async ctx => {
|
|
ctx.StatsManager.sampleRequest.returns(true)
|
|
await ctx.CompileManager.promises.doCompileWithLock(ctx.request, {}, {})
|
|
expect(ctx.LatexMetrics.enableLatexMkMetrics).to.have.been.calledWith(
|
|
sinon.match.object
|
|
)
|
|
})
|
|
|
|
it('should enable latexmk metrics when sampleRequest returns false', async ctx => {
|
|
ctx.StatsManager.sampleRequest.returns(false)
|
|
await ctx.CompileManager.promises.doCompileWithLock(ctx.request, {}, {})
|
|
expect(ctx.LatexMetrics.enableLatexMkMetrics).to.have.been.calledWith(
|
|
sinon.match.object
|
|
)
|
|
})
|
|
|
|
it('should enable latexmk metrics when sampleRequest returns undefined', async ctx => {
|
|
ctx.StatsManager.sampleRequest.returns(undefined)
|
|
await ctx.CompileManager.promises.doCompileWithLock(ctx.request, {}, {})
|
|
expect(ctx.LatexMetrics.enableLatexMkMetrics).to.have.been.calledWith(
|
|
sinon.match.object
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('with draft mode', () => {
|
|
beforeEach(async ctx => {
|
|
ctx.request.draft = true
|
|
await ctx.CompileManager.promises.doCompileWithLock(ctx.request, {}, {})
|
|
})
|
|
|
|
it('should inject the draft mode header', ctx => {
|
|
expect(
|
|
ctx.DraftModeManager.promises.injectDraftMode
|
|
).to.have.been.calledWith(ctx.compileDir + '/' + ctx.rootResourcePath)
|
|
})
|
|
})
|
|
|
|
describe('with a check option', () => {
|
|
beforeEach(async ctx => {
|
|
ctx.request.check = 'error'
|
|
await ctx.CompileManager.promises.doCompileWithLock(ctx.request, {}, {})
|
|
})
|
|
|
|
it('should run chktex', ctx => {
|
|
expect(ctx.LatexRunner.promises.runLatex).to.have.been.calledWith(
|
|
`${ctx.projectId}-${ctx.userId}`,
|
|
{
|
|
directory: ctx.compileDir,
|
|
mainFile: ctx.rootResourcePath,
|
|
compiler: ctx.compiler,
|
|
timeout: ctx.timeout,
|
|
image: ctx.image,
|
|
flags: ctx.flags,
|
|
environment: {
|
|
CHKTEX_OPTIONS: '-nall -e9 -e10 -w15 -w16',
|
|
CHKTEX_EXIT_ON_ERROR: 1,
|
|
CHKTEX_ULIMIT_OPTIONS: '-t 5 -v 64000',
|
|
OVERLEAF_PROJECT_ID: ctx.projectId,
|
|
},
|
|
compileGroup: ctx.compileGroup,
|
|
stopOnFirstError: ctx.request.stopOnFirstError,
|
|
stats: sinon.match.object,
|
|
timings: sinon.match.object,
|
|
}
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('with a knitr file and check options', () => {
|
|
beforeEach(async ctx => {
|
|
ctx.request.rootResourcePath = 'main.Rtex'
|
|
ctx.request.check = 'error'
|
|
await ctx.CompileManager.promises.doCompileWithLock(ctx.request, {}, {})
|
|
})
|
|
|
|
it('should not run chktex', ctx => {
|
|
expect(ctx.LatexRunner.promises.runLatex).to.have.been.calledWith(
|
|
`${ctx.projectId}-${ctx.userId}`,
|
|
{
|
|
directory: ctx.compileDir,
|
|
mainFile: 'main.Rtex',
|
|
compiler: ctx.compiler,
|
|
timeout: ctx.timeout,
|
|
image: ctx.image,
|
|
flags: ctx.flags,
|
|
environment: ctx.env,
|
|
compileGroup: ctx.compileGroup,
|
|
stopOnFirstError: ctx.request.stopOnFirstError,
|
|
stats: sinon.match.object,
|
|
timings: sinon.match.object,
|
|
}
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('when the compile times out', () => {
|
|
beforeEach(async ctx => {
|
|
const error = new Error('timed out!')
|
|
error.timedout = true
|
|
ctx.LatexRunner.promises.runLatex.rejects(error)
|
|
await expect(
|
|
ctx.CompileManager.promises.doCompileWithLock(ctx.request, {}, {})
|
|
).to.be.rejected
|
|
})
|
|
|
|
it('should clear the compile directory', ctx => {
|
|
for (const { path } of ctx.buildFiles) {
|
|
expect(ctx.fsPromises.unlink).to.have.been.calledWith(
|
|
ctx.compileDir + '/' + path
|
|
)
|
|
}
|
|
expect(ctx.fsPromises.unlink).to.have.been.calledWith(
|
|
ctx.compileDir + '/main.tex'
|
|
)
|
|
expect(ctx.fsPromises.rmdir).to.have.been.calledWith(ctx.compileDir)
|
|
})
|
|
})
|
|
|
|
describe('when the compile is manually stopped', () => {
|
|
beforeEach(async ctx => {
|
|
const error = new Error('terminated!')
|
|
error.terminated = true
|
|
ctx.LatexRunner.promises.runLatex.rejects(error)
|
|
await expect(
|
|
ctx.CompileManager.promises.doCompileWithLock(ctx.request, {}, {})
|
|
).to.be.rejected
|
|
})
|
|
|
|
it('should clear the compile directory', ctx => {
|
|
for (const { path } of ctx.buildFiles) {
|
|
expect(ctx.fsPromises.unlink).to.have.been.calledWith(
|
|
ctx.compileDir + '/' + path
|
|
)
|
|
}
|
|
expect(ctx.fsPromises.unlink).to.have.been.calledWith(
|
|
ctx.compileDir + '/main.tex'
|
|
)
|
|
expect(ctx.fsPromises.rmdir).to.have.been.calledWith(ctx.compileDir)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('clearProject', () => {
|
|
it('should clear the compile directory', async ctx => {
|
|
await ctx.CompileManager.promises.clearProject(ctx.projectId, ctx.userId)
|
|
|
|
expect(ctx.fsPromises.rm).to.have.been.calledWith(ctx.compileDir, {
|
|
force: true,
|
|
recursive: true,
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('syncing', () => {
|
|
beforeEach(ctx => {
|
|
ctx.page = 1
|
|
ctx.h = 42.23
|
|
ctx.v = 87.56
|
|
ctx.width = 100.01
|
|
ctx.height = 234.56
|
|
ctx.line = 5
|
|
ctx.column = 3
|
|
ctx.filename = 'main.tex'
|
|
})
|
|
|
|
describe('syncFromCode', () => {
|
|
beforeEach(ctx => {
|
|
ctx.records = [{ page: 1, h: 2, v: 3, width: 4, height: 5 }]
|
|
ctx.SynctexOutputParser.parseViewOutput
|
|
.withArgs(ctx.commandOutput)
|
|
.returns(ctx.records)
|
|
})
|
|
|
|
describe('normal case', () => {
|
|
beforeEach(async ctx => {
|
|
ctx.result = await ctx.CompileManager.promises.syncFromCode(
|
|
ctx.projectId,
|
|
ctx.userId,
|
|
ctx.filename,
|
|
ctx.line,
|
|
ctx.column,
|
|
''
|
|
)
|
|
})
|
|
|
|
it('should execute the synctex binary', ctx => {
|
|
const outputFilePath = `${ctx.compileDir}/output.pdf`
|
|
const inputFilePath = `${ctx.compileDir}/${ctx.filename}`
|
|
expect(ctx.CommandRunner.promises.run).to.have.been.calledWith(
|
|
`${ctx.projectId}-${ctx.userId}`,
|
|
[
|
|
'synctex',
|
|
'view',
|
|
'-i',
|
|
`${ctx.line}:${ctx.column}:${inputFilePath}`,
|
|
'-o',
|
|
outputFilePath,
|
|
],
|
|
ctx.compileDir,
|
|
ctx.Settings.clsi.docker.image,
|
|
60000,
|
|
{},
|
|
'synctex'
|
|
)
|
|
})
|
|
|
|
it('should return the parsed output', ctx => {
|
|
expect(ctx.result).to.deep.equal({
|
|
codePositions: ctx.records,
|
|
downloadedFromCache: false,
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('from cache in docker', () => {
|
|
beforeEach(async ctx => {
|
|
ctx.CommandRunner.canRunSyncTeXInOutputDir.returns(true)
|
|
ctx.Settings.path.synctexBaseDir
|
|
.withArgs(`${ctx.projectId}-${ctx.userId}`)
|
|
.returns('/compile')
|
|
|
|
const errNotFound = new Error()
|
|
errNotFound.code = 'ENOENT'
|
|
ctx.outputDir = `${ctx.outputBaseDir}/${ctx.projectId}-${ctx.userId}/${ctx.OutputCacheManager.CACHE_SUBDIR}/${ctx.buildId}`
|
|
const filename = Path.join(ctx.outputDir, 'output.synctex.gz')
|
|
ctx.fsPromises.stat
|
|
.withArgs(ctx.outputDir)
|
|
.onFirstCall()
|
|
.rejects(errNotFound)
|
|
ctx.fsPromises.stat
|
|
.withArgs(ctx.outputDir)
|
|
.onSecondCall()
|
|
.resolves(ctx.dirStats)
|
|
ctx.fsPromises.stat.withArgs(filename).resolves(ctx.fileStats)
|
|
ctx.CLSICacheHandler.downloadOutputDotSynctexFromCompileCache.resolves(
|
|
true
|
|
)
|
|
ctx.result = await ctx.CompileManager.promises.syncFromCode(
|
|
ctx.projectId,
|
|
ctx.userId,
|
|
ctx.filename,
|
|
ctx.line,
|
|
ctx.column,
|
|
{
|
|
imageName: 'image',
|
|
editorId: '00000000-0000-0000-0000-000000000000',
|
|
buildId: ctx.buildId,
|
|
compileFromClsiCache: true,
|
|
}
|
|
)
|
|
})
|
|
|
|
it('should run in output dir', ctx => {
|
|
const outputFilePath = '/compile/output.pdf'
|
|
const inputFilePath = `/compile/${ctx.filename}`
|
|
expect(ctx.CommandRunner.promises.run).to.have.been.calledWith(
|
|
`${ctx.projectId}-${ctx.userId}`,
|
|
[
|
|
'synctex',
|
|
'view',
|
|
'-i',
|
|
`${ctx.line}:${ctx.column}:${inputFilePath}`,
|
|
'-o',
|
|
outputFilePath,
|
|
],
|
|
ctx.outputDir,
|
|
'image',
|
|
60000,
|
|
{},
|
|
'synctex-output'
|
|
)
|
|
})
|
|
|
|
it('should return the parsed output', ctx => {
|
|
expect(ctx.result).to.deep.equal({
|
|
codePositions: ctx.records,
|
|
downloadedFromCache: true,
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('with a custom imageName', () => {
|
|
const customImageName = 'foo/bar:tag-0'
|
|
beforeEach(async ctx => {
|
|
await ctx.CompileManager.promises.syncFromCode(
|
|
ctx.projectId,
|
|
ctx.userId,
|
|
ctx.filename,
|
|
ctx.line,
|
|
ctx.column,
|
|
{ imageName: customImageName }
|
|
)
|
|
})
|
|
|
|
it('should execute the synctex binary in a custom docker image', ctx => {
|
|
const outputFilePath = `${ctx.compileDir}/output.pdf`
|
|
const inputFilePath = `${ctx.compileDir}/${ctx.filename}`
|
|
expect(ctx.CommandRunner.promises.run).to.have.been.calledWith(
|
|
`${ctx.projectId}-${ctx.userId}`,
|
|
[
|
|
'synctex',
|
|
'view',
|
|
'-i',
|
|
`${ctx.line}:${ctx.column}:${inputFilePath}`,
|
|
'-o',
|
|
outputFilePath,
|
|
],
|
|
ctx.compileDir,
|
|
customImageName,
|
|
60000,
|
|
{},
|
|
'synctex'
|
|
)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('syncFromPdf', () => {
|
|
beforeEach(ctx => {
|
|
ctx.records = [{ file: 'main.tex', line: 1, column: 1 }]
|
|
ctx.SynctexOutputParser.parseEditOutput
|
|
.withArgs(ctx.commandOutput, ctx.compileDir)
|
|
.returns(ctx.records)
|
|
})
|
|
|
|
describe('normal case', () => {
|
|
beforeEach(async ctx => {
|
|
ctx.result = await ctx.CompileManager.promises.syncFromPdf(
|
|
ctx.projectId,
|
|
ctx.userId,
|
|
ctx.page,
|
|
ctx.h,
|
|
ctx.v,
|
|
{ imageName: '' }
|
|
)
|
|
})
|
|
|
|
it('should execute the synctex binary', ctx => {
|
|
const outputFilePath = `${ctx.compileDir}/output.pdf`
|
|
expect(ctx.CommandRunner.promises.run).to.have.been.calledWith(
|
|
`${ctx.projectId}-${ctx.userId}`,
|
|
[
|
|
'synctex',
|
|
'edit',
|
|
'-o',
|
|
`${ctx.page}:${ctx.h}:${ctx.v}:${outputFilePath}`,
|
|
],
|
|
ctx.compileDir,
|
|
ctx.Settings.clsi.docker.image,
|
|
60000,
|
|
{}
|
|
)
|
|
})
|
|
|
|
it('should return the parsed output', ctx => {
|
|
expect(ctx.result).to.deep.equal({
|
|
pdfPositions: ctx.records,
|
|
downloadedFromCache: false,
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('with a custom imageName', () => {
|
|
const customImageName = 'foo/bar:tag-1'
|
|
beforeEach(async ctx => {
|
|
await ctx.CompileManager.promises.syncFromPdf(
|
|
ctx.projectId,
|
|
ctx.userId,
|
|
ctx.page,
|
|
ctx.h,
|
|
ctx.v,
|
|
{ imageName: customImageName }
|
|
)
|
|
})
|
|
|
|
it('should execute the synctex binary in a custom docker image', ctx => {
|
|
const outputFilePath = `${ctx.compileDir}/output.pdf`
|
|
expect(ctx.CommandRunner.promises.run).to.have.been.calledWith(
|
|
`${ctx.projectId}-${ctx.userId}`,
|
|
[
|
|
'synctex',
|
|
'edit',
|
|
'-o',
|
|
`${ctx.page}:${ctx.h}:${ctx.v}:${outputFilePath}`,
|
|
],
|
|
ctx.compileDir,
|
|
customImageName,
|
|
60000,
|
|
{}
|
|
)
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('wordcount', () => {
|
|
beforeEach(async ctx => {
|
|
ctx.timeout = 60 * 1000
|
|
ctx.filename = 'main.tex'
|
|
ctx.image = 'example.com/image'
|
|
|
|
ctx.result = await ctx.CompileManager.promises.wordcount(
|
|
ctx.projectId,
|
|
ctx.userId,
|
|
ctx.filename,
|
|
ctx.image
|
|
)
|
|
})
|
|
|
|
it('should run the texcount command', ctx => {
|
|
ctx.filePath = `$COMPILE_DIR/${ctx.filename}`
|
|
ctx.command = ['texcount', '-nocol', '-inc', ctx.filePath]
|
|
|
|
expect(ctx.CommandRunner.promises.run).to.have.been.calledWith(
|
|
`${ctx.projectId}-${ctx.userId}`,
|
|
ctx.command,
|
|
ctx.compileDir,
|
|
ctx.image,
|
|
ctx.timeout,
|
|
{}
|
|
)
|
|
})
|
|
|
|
it('should return the parsed output', ctx => {
|
|
expect(ctx.result).to.deep.equal({
|
|
encode: 'ascii',
|
|
textWords: 2,
|
|
headWords: 0,
|
|
outside: 0,
|
|
headers: 0,
|
|
elements: 0,
|
|
mathInline: 0,
|
|
mathDisplay: 0,
|
|
errors: 0,
|
|
messages: '',
|
|
})
|
|
})
|
|
})
|
|
})
|