mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-23 09:09:36 +02:00
[clsi] Add pandoc arguments for better conversions GitOrigin-RevId: 76cddc5959237d6d2801c56471d8d3f63d111200
564 lines
17 KiB
JavaScript
564 lines
17 KiB
JavaScript
import Path from 'node:path'
|
|
import sinon from 'sinon'
|
|
import { vi, describe, beforeEach, afterEach, it, expect } from 'vitest'
|
|
const MODULE_PATH = Path.join(
|
|
import.meta.dirname,
|
|
'../../../app/js/ConversionManager'
|
|
)
|
|
|
|
describe('ConversionManager', function () {
|
|
beforeEach(async function (ctx) {
|
|
ctx.CommandRunner = {
|
|
promises: {
|
|
run: sinon.stub().resolves({ stdout: '', stderr: '', exitCode: 0 }),
|
|
},
|
|
}
|
|
|
|
ctx.lock = {
|
|
release: sinon.stub(),
|
|
}
|
|
|
|
ctx.LockManager = {
|
|
acquire: sinon.stub().returns(ctx.lock),
|
|
}
|
|
|
|
ctx.Settings = {
|
|
pandocImage: 'mock-pandoc-image',
|
|
conversionTimeoutSeconds: 60,
|
|
path: { compilesDir: '/compiles' },
|
|
}
|
|
|
|
ctx.fs = {
|
|
mkdir: sinon.stub().resolves(),
|
|
copyFile: sinon.stub().resolves(),
|
|
rm: sinon.stub().resolves(),
|
|
unlink: sinon.stub().resolves(),
|
|
}
|
|
|
|
ctx.conversionId = 'test-conversion-id'
|
|
ctx.conversionDir = '/compiles/test-conversion-id'
|
|
ctx.outputPath = '/compiles/test-conversion-id/output-uuid.zip'
|
|
|
|
ctx.uuidStub = sinon
|
|
.stub(globalThis.crypto, 'randomUUID')
|
|
.returns('output-uuid')
|
|
|
|
vi.doMock('../../../app/js/LockManager', () => ({
|
|
default: ctx.LockManager,
|
|
}))
|
|
|
|
vi.doMock('@overleaf/settings', () => ({
|
|
default: ctx.Settings,
|
|
}))
|
|
|
|
vi.doMock('../../../app/js/CommandRunner', () => ({
|
|
default: ctx.CommandRunner,
|
|
}))
|
|
|
|
vi.doMock('node:fs/promises', () => ({ default: ctx.fs }))
|
|
|
|
ctx.ConversionManager = (await import(MODULE_PATH)).default
|
|
})
|
|
|
|
afterEach(function (ctx) {
|
|
ctx.uuidStub.restore()
|
|
})
|
|
|
|
describe('convertToLaTeXWithLock', function () {
|
|
describe('with conversionType=docx', function () {
|
|
beforeEach(function (ctx) {
|
|
ctx.inputPath = '/path/to/input.docx'
|
|
})
|
|
|
|
describe('file setup and pandoc args', function () {
|
|
beforeEach(async function (ctx) {
|
|
ctx.result =
|
|
await ctx.ConversionManager.promises.convertToLaTeXWithLock(
|
|
ctx.conversionId,
|
|
ctx.inputPath,
|
|
'docx'
|
|
)
|
|
})
|
|
|
|
it('should acquire a lock', async function (ctx) {
|
|
sinon.assert.calledWith(ctx.LockManager.acquire, ctx.conversionDir)
|
|
})
|
|
|
|
it('should copy the input file to the conversion directory with docx filename', async function (ctx) {
|
|
sinon.assert.calledWith(ctx.fs.mkdir, ctx.conversionDir, {
|
|
recursive: true,
|
|
})
|
|
sinon.assert.calledWith(
|
|
ctx.fs.copyFile,
|
|
ctx.inputPath,
|
|
Path.join(ctx.conversionDir, 'input.docx')
|
|
)
|
|
})
|
|
|
|
it('should convert conversion timeout to milliseconds', async function (ctx) {
|
|
expect(ctx.CommandRunner.promises.run.firstCall.args[4]).toBe(60_000)
|
|
expect(ctx.CommandRunner.promises.run.secondCall.args[4]).toBe(60_000)
|
|
})
|
|
|
|
it('should run pandoc with docx args followed by zip', function (ctx) {
|
|
expect(ctx.CommandRunner.promises.run.callCount).toBe(2)
|
|
expect(ctx.CommandRunner.promises.run.firstCall.args).toEqual([
|
|
ctx.conversionId,
|
|
[
|
|
'pandoc',
|
|
'input.docx',
|
|
'--output',
|
|
'main.tex',
|
|
'--to',
|
|
'latex',
|
|
'--standalone',
|
|
'--extract-media=.',
|
|
'--from',
|
|
'docx+citations',
|
|
'--citeproc',
|
|
],
|
|
ctx.conversionDir,
|
|
ctx.Settings.pandocImage,
|
|
60_000,
|
|
{},
|
|
'conversions',
|
|
])
|
|
expect(ctx.CommandRunner.promises.run.secondCall.args).toEqual([
|
|
ctx.conversionId,
|
|
['zip', '-r', 'output-uuid.zip', '.'],
|
|
ctx.conversionDir,
|
|
ctx.Settings.pandocImage,
|
|
60_000,
|
|
{},
|
|
'conversions',
|
|
])
|
|
})
|
|
})
|
|
|
|
describe('successful conversion', function () {
|
|
beforeEach(async function (ctx) {
|
|
ctx.CommandRunner.promises.run.resolves({
|
|
stdout: 'mock-stdout',
|
|
stderr: 'mock-stderr',
|
|
exitCode: 0,
|
|
})
|
|
|
|
ctx.result =
|
|
await ctx.ConversionManager.promises.convertToLaTeXWithLock(
|
|
ctx.conversionId,
|
|
ctx.inputPath,
|
|
'docx'
|
|
)
|
|
})
|
|
|
|
it('should remove the source document after conversion', async function (ctx) {
|
|
sinon.assert.calledWith(
|
|
ctx.fs.unlink,
|
|
Path.join(ctx.conversionDir, 'input.docx')
|
|
)
|
|
})
|
|
|
|
it('should return the output zip path', function (ctx) {
|
|
expect(ctx.result).toBe(ctx.outputPath)
|
|
})
|
|
|
|
it('should release the lock', function (ctx) {
|
|
sinon.assert.called(ctx.lock.release)
|
|
})
|
|
})
|
|
|
|
describe('unsuccessful conversion (exitcode)', function () {
|
|
beforeEach(async function (ctx) {
|
|
ctx.CommandRunner.promises.run.resolves({
|
|
stdout: 'mock-stdout',
|
|
stderr: 'mock-stderr',
|
|
exitCode: 63,
|
|
})
|
|
|
|
await expect(
|
|
ctx.ConversionManager.promises.convertToLaTeXWithLock(
|
|
ctx.conversionId,
|
|
ctx.inputPath,
|
|
'docx'
|
|
)
|
|
).to.be.rejectedWith('pandoc conversion failed')
|
|
})
|
|
|
|
it('should remove the entire conversion directory', async function (ctx) {
|
|
sinon.assert.calledWith(ctx.fs.rm, ctx.conversionDir, {
|
|
force: true,
|
|
recursive: true,
|
|
})
|
|
})
|
|
|
|
it('should release the lock', function (ctx) {
|
|
sinon.assert.called(ctx.lock.release)
|
|
})
|
|
})
|
|
|
|
describe('unsuccessful compression (exitcode)', function () {
|
|
beforeEach(async function (ctx) {
|
|
ctx.CommandRunner.promises.run
|
|
.onFirstCall()
|
|
.resolves({
|
|
stdout: 'mock-pandoc-stdout',
|
|
stderr: 'mock-pandoc-stderr',
|
|
exitCode: 0,
|
|
})
|
|
.onSecondCall()
|
|
.resolves({
|
|
stdout: 'mock-zip-stdout',
|
|
stderr: 'mock-zip-stderr',
|
|
exitCode: 12,
|
|
})
|
|
|
|
await expect(
|
|
ctx.ConversionManager.promises.convertToLaTeXWithLock(
|
|
ctx.conversionId,
|
|
ctx.inputPath,
|
|
'docx'
|
|
)
|
|
).to.be.rejectedWith('pandoc conversion failed')
|
|
})
|
|
|
|
it('should remove the entire conversion directory', async function (ctx) {
|
|
sinon.assert.calledWith(ctx.fs.rm, ctx.conversionDir, {
|
|
force: true,
|
|
recursive: true,
|
|
})
|
|
})
|
|
|
|
it('should release the lock', function (ctx) {
|
|
sinon.assert.called(ctx.lock.release)
|
|
})
|
|
})
|
|
|
|
describe('unsuccessful conversion (throws)', function () {
|
|
beforeEach(async function (ctx) {
|
|
ctx.CommandRunner.promises.run.rejects(
|
|
new Error('mock conversion error')
|
|
)
|
|
await expect(
|
|
ctx.ConversionManager.promises.convertToLaTeXWithLock(
|
|
ctx.conversionId,
|
|
ctx.inputPath,
|
|
'docx'
|
|
)
|
|
).to.be.rejectedWith('pandoc conversion failed')
|
|
})
|
|
|
|
it('should remove the entire conversion directory', async function (ctx) {
|
|
sinon.assert.calledWith(ctx.fs.rm, ctx.conversionDir, {
|
|
force: true,
|
|
recursive: true,
|
|
})
|
|
})
|
|
|
|
it('should release the lock', function (ctx) {
|
|
sinon.assert.called(ctx.lock.release)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('with conversionType=markdown', function () {
|
|
beforeEach(function (ctx) {
|
|
ctx.inputPath = '/path/to/input.md'
|
|
})
|
|
|
|
describe('file setup and pandoc args', function () {
|
|
beforeEach(async function (ctx) {
|
|
ctx.result =
|
|
await ctx.ConversionManager.promises.convertToLaTeXWithLock(
|
|
ctx.conversionId,
|
|
ctx.inputPath,
|
|
'markdown'
|
|
)
|
|
})
|
|
|
|
it('should copy the input file to the conversion directory with md filename', async function (ctx) {
|
|
sinon.assert.calledWith(ctx.fs.mkdir, ctx.conversionDir, {
|
|
recursive: true,
|
|
})
|
|
sinon.assert.calledWith(
|
|
ctx.fs.copyFile,
|
|
ctx.inputPath,
|
|
Path.join(ctx.conversionDir, 'input.md')
|
|
)
|
|
})
|
|
|
|
it('should run pandoc with markdown args followed by zip', function (ctx) {
|
|
expect(ctx.CommandRunner.promises.run.callCount).toBe(2)
|
|
expect(ctx.CommandRunner.promises.run.firstCall.args).toEqual([
|
|
ctx.conversionId,
|
|
[
|
|
'pandoc',
|
|
'input.md',
|
|
'--output',
|
|
'main.tex',
|
|
'--to',
|
|
'latex',
|
|
'--standalone',
|
|
'--from',
|
|
'markdown',
|
|
],
|
|
ctx.conversionDir,
|
|
ctx.Settings.pandocImage,
|
|
60_000,
|
|
{},
|
|
'conversions',
|
|
])
|
|
expect(ctx.CommandRunner.promises.run.secondCall.args).toEqual([
|
|
ctx.conversionId,
|
|
['zip', '-r', 'output-uuid.zip', '.'],
|
|
ctx.conversionDir,
|
|
ctx.Settings.pandocImage,
|
|
60_000,
|
|
{},
|
|
'conversions',
|
|
])
|
|
})
|
|
})
|
|
|
|
describe('successful conversion', function () {
|
|
beforeEach(async function (ctx) {
|
|
ctx.CommandRunner.promises.run.resolves({
|
|
stdout: 'mock-stdout',
|
|
stderr: 'mock-stderr',
|
|
exitCode: 0,
|
|
})
|
|
|
|
ctx.result =
|
|
await ctx.ConversionManager.promises.convertToLaTeXWithLock(
|
|
ctx.conversionId,
|
|
ctx.inputPath,
|
|
'markdown'
|
|
)
|
|
})
|
|
|
|
it('should remove the source document after conversion', async function (ctx) {
|
|
sinon.assert.calledWith(
|
|
ctx.fs.unlink,
|
|
Path.join(ctx.conversionDir, 'input.md')
|
|
)
|
|
})
|
|
|
|
it('should return the output zip path', function (ctx) {
|
|
expect(ctx.result).toBe(ctx.outputPath)
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('convertLaTeXToDocumentInDirWithLock', function () {
|
|
describe('successfully', function () {
|
|
beforeEach(async function (ctx) {
|
|
ctx.compileDir = '/compiles/test-compile-dir'
|
|
ctx.rootDocPath = 'main.tex'
|
|
ctx.type = 'docx'
|
|
ctx.extension = 'docx'
|
|
|
|
ctx.result =
|
|
await ctx.ConversionManager.promises.convertLaTeXToDocumentInDirWithLock(
|
|
ctx.conversionId,
|
|
ctx.compileDir,
|
|
ctx.rootDocPath,
|
|
ctx.type
|
|
)
|
|
})
|
|
|
|
it('should acquire a lock on the compile dir', function (ctx) {
|
|
sinon.assert.calledWith(ctx.LockManager.acquire, ctx.compileDir)
|
|
})
|
|
|
|
it('should release the lock', function (ctx) {
|
|
sinon.assert.called(ctx.lock.release)
|
|
})
|
|
|
|
it('should run pandoc with correct arguments', function (ctx) {
|
|
expect(ctx.CommandRunner.promises.run.callCount).toBe(1)
|
|
expect(ctx.CommandRunner.promises.run.firstCall.args).toEqual([
|
|
ctx.conversionId,
|
|
[
|
|
'pandoc',
|
|
ctx.rootDocPath,
|
|
'--output',
|
|
`output-uuid.${ctx.extension}`,
|
|
'--from',
|
|
'latex',
|
|
'--to',
|
|
ctx.type,
|
|
'--citeproc',
|
|
'--number-sections',
|
|
'--resource-path=.',
|
|
],
|
|
ctx.compileDir,
|
|
ctx.Settings.pandocImage,
|
|
60_000,
|
|
{},
|
|
'conversions',
|
|
])
|
|
})
|
|
|
|
it('should convert conversion timeout to milliseconds', function (ctx) {
|
|
expect(ctx.CommandRunner.promises.run.firstCall.args[4]).toBe(60_000)
|
|
})
|
|
|
|
it('should return path to the output document', function (ctx) {
|
|
expect(ctx.result).toBe(
|
|
Path.join(ctx.compileDir, `output-uuid.${ctx.extension}`)
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('when pandoc fails (non-zero exit code)', function () {
|
|
it('should reject with an error and release the lock', async function (ctx) {
|
|
ctx.compileDir = '/compiles/test-compile-dir'
|
|
|
|
ctx.CommandRunner.promises.run.resolves({
|
|
stdout: 'mock-stdout',
|
|
stderr: 'mock-stderr',
|
|
exitCode: 1,
|
|
})
|
|
|
|
await expect(
|
|
ctx.ConversionManager.promises.convertLaTeXToDocumentInDirWithLock(
|
|
ctx.conversionId,
|
|
ctx.compileDir,
|
|
'main.tex',
|
|
'docx'
|
|
)
|
|
).to.be.rejectedWith('pandoc latex-to-document conversion failed')
|
|
|
|
sinon.assert.called(ctx.lock.release)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('convertLaTeXToDocumentInDirWithLock (type=markdown)', function () {
|
|
describe('successfully', function () {
|
|
beforeEach(async function (ctx) {
|
|
ctx.compileDir = '/compiles/test-compile-dir'
|
|
ctx.rootDocPath = 'main.tex'
|
|
|
|
ctx.result =
|
|
await ctx.ConversionManager.promises.convertLaTeXToDocumentInDirWithLock(
|
|
ctx.conversionId,
|
|
ctx.compileDir,
|
|
ctx.rootDocPath,
|
|
'markdown'
|
|
)
|
|
})
|
|
|
|
it('should acquire a lock on the compile dir', function (ctx) {
|
|
sinon.assert.calledWith(ctx.LockManager.acquire, ctx.compileDir)
|
|
})
|
|
|
|
it('should release the lock', function (ctx) {
|
|
sinon.assert.called(ctx.lock.release)
|
|
})
|
|
|
|
it('should create a UUID-named subdirectory', function (ctx) {
|
|
sinon.assert.calledWith(
|
|
ctx.fs.mkdir,
|
|
Path.join(ctx.compileDir, 'output-uuid'),
|
|
{ recursive: true }
|
|
)
|
|
})
|
|
|
|
it('should run pandoc then zip (two commands total)', function (ctx) {
|
|
expect(ctx.CommandRunner.promises.run.callCount).toBe(2)
|
|
})
|
|
|
|
it('should run pandoc outputting main.md into the UUID-named subdir', function (ctx) {
|
|
expect(ctx.CommandRunner.promises.run.firstCall.args).toEqual([
|
|
ctx.conversionId,
|
|
[
|
|
'pandoc',
|
|
ctx.rootDocPath,
|
|
'--output',
|
|
Path.join('output-uuid', 'main.md'),
|
|
'--from',
|
|
'latex',
|
|
'--to',
|
|
'markdown',
|
|
'--resource-path=.',
|
|
'--extract-media=output-uuid',
|
|
],
|
|
ctx.compileDir,
|
|
ctx.Settings.pandocImage,
|
|
60_000,
|
|
{},
|
|
'conversions',
|
|
])
|
|
})
|
|
|
|
it('should zip the project-named subdirectory', function (ctx) {
|
|
expect(ctx.CommandRunner.promises.run.secondCall.args).toEqual([
|
|
ctx.conversionId,
|
|
['sh', '-c', 'cd output-uuid && zip -r ../output-uuid.zip .'],
|
|
ctx.compileDir,
|
|
ctx.Settings.pandocImage,
|
|
60_000,
|
|
{},
|
|
'conversions',
|
|
])
|
|
})
|
|
|
|
it('should return the path to the zip file', function (ctx) {
|
|
expect(ctx.result).toBe(Path.join(ctx.compileDir, 'output-uuid.zip'))
|
|
})
|
|
|
|
it('should convert conversion timeout to milliseconds', function (ctx) {
|
|
expect(ctx.CommandRunner.promises.run.firstCall.args[4]).toBe(60_000)
|
|
expect(ctx.CommandRunner.promises.run.secondCall.args[4]).toBe(60_000)
|
|
})
|
|
})
|
|
|
|
describe('when pandoc fails (non-zero exit code)', function () {
|
|
it('should reject with an error and release the lock', async function (ctx) {
|
|
ctx.compileDir = '/compiles/test-compile-dir'
|
|
|
|
ctx.CommandRunner.promises.run.resolves({
|
|
stdout: 'mock-stdout',
|
|
stderr: 'mock-stderr',
|
|
exitCode: 1,
|
|
})
|
|
|
|
await expect(
|
|
ctx.ConversionManager.promises.convertLaTeXToDocumentInDirWithLock(
|
|
ctx.conversionId,
|
|
ctx.compileDir,
|
|
'main.tex',
|
|
'markdown'
|
|
)
|
|
).to.be.rejectedWith('pandoc latex-to-document conversion failed')
|
|
|
|
sinon.assert.called(ctx.lock.release)
|
|
})
|
|
})
|
|
|
|
describe('when zip fails (non-zero exit code)', function () {
|
|
it('should reject with an error and release the lock', async function (ctx) {
|
|
ctx.compileDir = '/compiles/test-compile-dir'
|
|
|
|
ctx.CommandRunner.promises.run
|
|
.onFirstCall()
|
|
.resolves({ stdout: '', stderr: '', exitCode: 0 })
|
|
.onSecondCall()
|
|
.resolves({ stdout: '', stderr: 'zip error', exitCode: 1 })
|
|
|
|
await expect(
|
|
ctx.ConversionManager.promises.convertLaTeXToDocumentInDirWithLock(
|
|
ctx.conversionId,
|
|
ctx.compileDir,
|
|
'main.tex',
|
|
'markdown'
|
|
)
|
|
).to.be.rejectedWith('zip compression of export failed')
|
|
|
|
sinon.assert.called(ctx.lock.release)
|
|
})
|
|
})
|
|
})
|
|
})
|