first commit
This commit is contained in:
187
services/clsi/test/acceptance/js/AllowedImageNamesTests.js
Normal file
187
services/clsi/test/acceptance/js/AllowedImageNamesTests.js
Normal file
@@ -0,0 +1,187 @@
|
||||
const Client = require('./helpers/Client')
|
||||
const ClsiApp = require('./helpers/ClsiApp')
|
||||
const { expect } = require('chai')
|
||||
|
||||
describe('AllowedImageNames', function () {
|
||||
beforeEach(function (done) {
|
||||
this.project_id = Client.randomId()
|
||||
this.request = {
|
||||
options: {
|
||||
imageName: undefined,
|
||||
},
|
||||
resources: [
|
||||
{
|
||||
path: 'main.tex',
|
||||
content: `\
|
||||
\\documentclass{article}
|
||||
\\begin{document}
|
||||
Hello world
|
||||
\\end{document}\
|
||||
`,
|
||||
},
|
||||
],
|
||||
}
|
||||
ClsiApp.ensureRunning(done)
|
||||
})
|
||||
|
||||
describe('with a valid name', function () {
|
||||
beforeEach(function (done) {
|
||||
this.request.options.imageName = process.env.TEXLIVE_IMAGE
|
||||
|
||||
Client.compile(this.project_id, this.request, (error, res, body) => {
|
||||
this.error = error
|
||||
this.res = res
|
||||
this.body = body
|
||||
done(error)
|
||||
})
|
||||
})
|
||||
it('should return success', function () {
|
||||
expect(this.res.statusCode).to.equal(200)
|
||||
})
|
||||
|
||||
it('should return a PDF', function () {
|
||||
let pdf
|
||||
try {
|
||||
pdf = Client.getOutputFile(this.body, 'pdf')
|
||||
} catch (e) {}
|
||||
expect(pdf).to.exist
|
||||
})
|
||||
})
|
||||
|
||||
describe('with an invalid name', function () {
|
||||
beforeEach(function (done) {
|
||||
this.request.options.imageName = 'something/evil:1337'
|
||||
Client.compile(this.project_id, this.request, (error, res, body) => {
|
||||
this.error = error
|
||||
this.res = res
|
||||
this.body = body
|
||||
done(error)
|
||||
})
|
||||
})
|
||||
it('should return non success', function () {
|
||||
expect(this.res.statusCode).to.not.equal(200)
|
||||
})
|
||||
|
||||
it('should not return a PDF', function () {
|
||||
let pdf
|
||||
try {
|
||||
pdf = Client.getOutputFile(this.body, 'pdf')
|
||||
} catch (e) {}
|
||||
expect(pdf).to.not.exist
|
||||
})
|
||||
})
|
||||
|
||||
describe('syncToCode', function () {
|
||||
beforeEach(function (done) {
|
||||
Client.compile(this.project_id, this.request, done)
|
||||
})
|
||||
it('should error out with an invalid imageName', function (done) {
|
||||
Client.syncFromCodeWithImage(
|
||||
this.project_id,
|
||||
'main.tex',
|
||||
3,
|
||||
5,
|
||||
'something/evil:1337',
|
||||
(error, body) => {
|
||||
expect(String(error)).to.include('statusCode=400')
|
||||
expect(body).to.equal('invalid image')
|
||||
done()
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('should produce a mapping a valid imageName', function (done) {
|
||||
Client.syncFromCodeWithImage(
|
||||
this.project_id,
|
||||
'main.tex',
|
||||
3,
|
||||
5,
|
||||
process.env.TEXLIVE_IMAGE,
|
||||
(error, result) => {
|
||||
expect(error).to.not.exist
|
||||
expect(result).to.deep.equal({
|
||||
pdf: [
|
||||
{
|
||||
page: 1,
|
||||
h: 133.768356,
|
||||
v: 134.764618,
|
||||
height: 6.918498,
|
||||
width: 343.71106,
|
||||
},
|
||||
],
|
||||
})
|
||||
done()
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('syncToPdf', function () {
|
||||
beforeEach(function (done) {
|
||||
Client.compile(this.project_id, this.request, done)
|
||||
})
|
||||
it('should error out with an invalid imageName', function (done) {
|
||||
Client.syncFromPdfWithImage(
|
||||
this.project_id,
|
||||
'main.tex',
|
||||
100,
|
||||
200,
|
||||
'something/evil:1337',
|
||||
(error, body) => {
|
||||
expect(String(error)).to.include('statusCode=400')
|
||||
expect(body).to.equal('invalid image')
|
||||
done()
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('should produce a mapping a valid imageName', function (done) {
|
||||
Client.syncFromPdfWithImage(
|
||||
this.project_id,
|
||||
1,
|
||||
100,
|
||||
200,
|
||||
process.env.TEXLIVE_IMAGE,
|
||||
(error, result) => {
|
||||
expect(error).to.not.exist
|
||||
expect(result).to.deep.equal({
|
||||
code: [{ file: 'main.tex', line: 3, column: -1 }],
|
||||
})
|
||||
done()
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('wordcount', function () {
|
||||
beforeEach(function (done) {
|
||||
Client.compile(this.project_id, this.request, done)
|
||||
})
|
||||
it('should error out with an invalid imageName', function (done) {
|
||||
Client.wordcountWithImage(
|
||||
this.project_id,
|
||||
'main.tex',
|
||||
'something/evil:1337',
|
||||
(error, body) => {
|
||||
expect(String(error)).to.include('statusCode=400')
|
||||
expect(body).to.equal('invalid image')
|
||||
done()
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('should produce a texcout a valid imageName', function (done) {
|
||||
Client.wordcountWithImage(
|
||||
this.project_id,
|
||||
'main.tex',
|
||||
process.env.TEXLIVE_IMAGE,
|
||||
(error, result) => {
|
||||
expect(error).to.not.exist
|
||||
expect(result).to.exist
|
||||
expect(result.texcount).to.exist
|
||||
done()
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
124
services/clsi/test/acceptance/js/BrokenLatexFileTests.js
Normal file
124
services/clsi/test/acceptance/js/BrokenLatexFileTests.js
Normal file
@@ -0,0 +1,124 @@
|
||||
/* eslint-disable
|
||||
no-unused-vars,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
const Client = require('./helpers/Client')
|
||||
const request = require('request')
|
||||
const ClsiApp = require('./helpers/ClsiApp')
|
||||
const { expect } = require('chai')
|
||||
|
||||
describe('Broken LaTeX file', function () {
|
||||
before(function (done) {
|
||||
this.broken_request = {
|
||||
resources: [
|
||||
{
|
||||
path: 'main.tex',
|
||||
content: `\
|
||||
\\documentclass{articl % :(
|
||||
\\begin{documen % :(
|
||||
Broken
|
||||
\\end{documen % :(\
|
||||
`,
|
||||
},
|
||||
],
|
||||
}
|
||||
this.correct_request = {
|
||||
resources: [
|
||||
{
|
||||
path: 'main.tex',
|
||||
content: `\
|
||||
\\documentclass{article}
|
||||
\\begin{document}
|
||||
Hello world
|
||||
\\end{document}\
|
||||
`,
|
||||
},
|
||||
],
|
||||
}
|
||||
return ClsiApp.ensureRunning(done)
|
||||
})
|
||||
|
||||
describe('on first run', function () {
|
||||
before(function (done) {
|
||||
this.project_id = Client.randomId()
|
||||
return Client.compile(
|
||||
this.project_id,
|
||||
this.broken_request,
|
||||
(error, res, body) => {
|
||||
this.error = error
|
||||
this.res = res
|
||||
this.body = body
|
||||
return done()
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('should return a failure status', function () {
|
||||
return this.body.compile.status.should.equal('failure')
|
||||
})
|
||||
|
||||
it('should return isInitialCompile flag', function () {
|
||||
expect(this.body.compile.stats.isInitialCompile).to.equal(1)
|
||||
})
|
||||
|
||||
it('should return output files', function () {
|
||||
// NOTE: No output.pdf file.
|
||||
this.body.compile.outputFiles
|
||||
.map(f => f.path)
|
||||
.should.deep.equal([
|
||||
'output.aux',
|
||||
'output.fdb_latexmk',
|
||||
'output.fls',
|
||||
'output.log',
|
||||
'output.stderr',
|
||||
'output.stdout',
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
return describe('on second run', function () {
|
||||
before(function (done) {
|
||||
this.project_id = Client.randomId()
|
||||
return Client.compile(this.project_id, this.correct_request, () => {
|
||||
return Client.compile(
|
||||
this.project_id,
|
||||
this.broken_request,
|
||||
(error, res, body) => {
|
||||
this.error = error
|
||||
this.res = res
|
||||
this.body = body
|
||||
return done()
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('should return a failure status', function () {
|
||||
return this.body.compile.status.should.equal('failure')
|
||||
})
|
||||
|
||||
it('should not return isInitialCompile flag', function () {
|
||||
expect(this.body.compile.stats.isInitialCompile).to.not.exist
|
||||
})
|
||||
|
||||
it('should return output files', function () {
|
||||
// NOTE: No output.pdf file.
|
||||
this.body.compile.outputFiles
|
||||
.map(f => f.path)
|
||||
.should.deep.equal([
|
||||
'output.aux',
|
||||
'output.fdb_latexmk',
|
||||
'output.fls',
|
||||
'output.log',
|
||||
'output.stderr',
|
||||
'output.stdout',
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
72
services/clsi/test/acceptance/js/DeleteOldFilesTest.js
Normal file
72
services/clsi/test/acceptance/js/DeleteOldFilesTest.js
Normal file
@@ -0,0 +1,72 @@
|
||||
/* eslint-disable
|
||||
no-unused-vars,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
const Client = require('./helpers/Client')
|
||||
const request = require('request')
|
||||
const ClsiApp = require('./helpers/ClsiApp')
|
||||
|
||||
describe('Deleting Old Files', function () {
|
||||
before(function (done) {
|
||||
this.request = {
|
||||
resources: [
|
||||
{
|
||||
path: 'main.tex',
|
||||
content: `\
|
||||
\\documentclass{article}
|
||||
\\begin{document}
|
||||
Hello world
|
||||
\\end{document}\
|
||||
`,
|
||||
},
|
||||
],
|
||||
}
|
||||
return ClsiApp.ensureRunning(done)
|
||||
})
|
||||
|
||||
return describe('on first run', function () {
|
||||
before(function (done) {
|
||||
this.project_id = Client.randomId()
|
||||
return Client.compile(
|
||||
this.project_id,
|
||||
this.request,
|
||||
(error, res, body) => {
|
||||
this.error = error
|
||||
this.res = res
|
||||
this.body = body
|
||||
return done()
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('should return a success status', function () {
|
||||
return this.body.compile.status.should.equal('success')
|
||||
})
|
||||
|
||||
return describe('after file has been deleted', function () {
|
||||
before(function (done) {
|
||||
this.request.resources = []
|
||||
return Client.compile(
|
||||
this.project_id,
|
||||
this.request,
|
||||
(error, res, body) => {
|
||||
this.error = error
|
||||
this.res = res
|
||||
this.body = body
|
||||
return done()
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
return it('should return a failure status', function () {
|
||||
return this.body.compile.status.should.equal('failure')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
285
services/clsi/test/acceptance/js/ExampleDocumentTests.js
Normal file
285
services/clsi/test/acceptance/js/ExampleDocumentTests.js
Normal file
@@ -0,0 +1,285 @@
|
||||
/* eslint-disable
|
||||
no-return-assign,
|
||||
no-unused-vars,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS101: Remove unnecessary use of Array.from
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* DS103: Rewrite code to no longer use __guard__
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
const Client = require('./helpers/Client')
|
||||
const fetch = require('node-fetch')
|
||||
const { pipeline } = require('node:stream')
|
||||
const fs = require('node:fs')
|
||||
const ChildProcess = require('node:child_process')
|
||||
const ClsiApp = require('./helpers/ClsiApp')
|
||||
const logger = require('@overleaf/logger')
|
||||
const Path = require('node:path')
|
||||
const fixturePath = path => {
|
||||
if (path.slice(0, 3) === 'tmp') {
|
||||
return '/tmp/clsi_acceptance_tests' + path.slice(3)
|
||||
}
|
||||
return Path.join(__dirname, '../fixtures/', path)
|
||||
}
|
||||
const process = require('node:process')
|
||||
console.log(
|
||||
process.pid,
|
||||
process.ppid,
|
||||
process.getuid(),
|
||||
process.getgroups(),
|
||||
'PID'
|
||||
)
|
||||
|
||||
const MOCHA_LATEX_TIMEOUT = 60 * 1000
|
||||
|
||||
const convertToPng = function (pdfPath, pngPath, callback) {
|
||||
if (callback == null) {
|
||||
callback = function () {}
|
||||
}
|
||||
const command = `convert ${fixturePath(pdfPath)} ${fixturePath(pngPath)}`
|
||||
console.log('COMMAND')
|
||||
console.log(command)
|
||||
const convert = ChildProcess.exec(command)
|
||||
const stdout = ''
|
||||
convert.stdout.on('data', chunk => console.log('STDOUT', chunk.toString()))
|
||||
convert.stderr.on('data', chunk => console.log('STDERR', chunk.toString()))
|
||||
return convert.on('exit', () => callback())
|
||||
}
|
||||
|
||||
const compare = function (originalPath, generatedPath, callback) {
|
||||
if (callback == null) {
|
||||
callback = function () {}
|
||||
}
|
||||
const diffFile = `${fixturePath(generatedPath)}-diff.png`
|
||||
const proc = ChildProcess.exec(
|
||||
`compare -metric mae ${fixturePath(originalPath)} ${fixturePath(
|
||||
generatedPath
|
||||
)} ${diffFile}`
|
||||
)
|
||||
let stderr = ''
|
||||
proc.stderr.on('data', chunk => (stderr += chunk))
|
||||
return proc.on('exit', () => {
|
||||
if (stderr.trim() === '0 (0)') {
|
||||
// remove output diff if test matches expected image
|
||||
fs.unlink(diffFile, err => {
|
||||
if (err) {
|
||||
throw err
|
||||
}
|
||||
})
|
||||
return callback(null, true)
|
||||
} else {
|
||||
console.log('compare result', stderr)
|
||||
return callback(null, false)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const checkPdfInfo = function (pdfPath, callback) {
|
||||
if (callback == null) {
|
||||
callback = function () {}
|
||||
}
|
||||
const proc = ChildProcess.exec(`pdfinfo ${fixturePath(pdfPath)}`)
|
||||
let stdout = ''
|
||||
proc.stdout.on('data', chunk => (stdout += chunk))
|
||||
proc.stderr.on('data', chunk => console.log('STDERR', chunk.toString()))
|
||||
return proc.on('exit', () => {
|
||||
if (stdout.match(/Optimized:\s+yes/)) {
|
||||
return callback(null, true)
|
||||
} else {
|
||||
return callback(null, false)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const compareMultiplePages = function (projectId, callback) {
|
||||
if (callback == null) {
|
||||
callback = function () {}
|
||||
}
|
||||
function compareNext(pageNo, callback) {
|
||||
const path = `tmp/${projectId}-source-${pageNo}.png`
|
||||
return fs.stat(fixturePath(path), (error, stat) => {
|
||||
if (error != null) {
|
||||
return callback()
|
||||
} else {
|
||||
return compare(
|
||||
`tmp/${projectId}-source-${pageNo}.png`,
|
||||
`tmp/${projectId}-generated-${pageNo}.png`,
|
||||
(error, same) => {
|
||||
if (error != null) {
|
||||
throw error
|
||||
}
|
||||
same.should.equal(true)
|
||||
return compareNext(pageNo + 1, callback)
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
return compareNext(0, callback)
|
||||
}
|
||||
|
||||
const comparePdf = function (projectId, exampleDir, callback) {
|
||||
if (callback == null) {
|
||||
callback = function () {}
|
||||
}
|
||||
console.log('CONVERT')
|
||||
console.log(`tmp/${projectId}.pdf`, `tmp/${projectId}-generated.png`)
|
||||
return convertToPng(
|
||||
`tmp/${projectId}.pdf`,
|
||||
`tmp/${projectId}-generated.png`,
|
||||
error => {
|
||||
if (error != null) {
|
||||
throw error
|
||||
}
|
||||
return convertToPng(
|
||||
`examples/${exampleDir}/output.pdf`,
|
||||
`tmp/${projectId}-source.png`,
|
||||
error => {
|
||||
if (error != null) {
|
||||
throw error
|
||||
}
|
||||
return fs.stat(
|
||||
fixturePath(`tmp/${projectId}-source-0.png`),
|
||||
(error, stat) => {
|
||||
if (error != null) {
|
||||
return compare(
|
||||
`tmp/${projectId}-source.png`,
|
||||
`tmp/${projectId}-generated.png`,
|
||||
(error, same) => {
|
||||
if (error != null) {
|
||||
throw error
|
||||
}
|
||||
same.should.equal(true)
|
||||
return callback()
|
||||
}
|
||||
)
|
||||
} else {
|
||||
return compareMultiplePages(projectId, error => {
|
||||
if (error != null) {
|
||||
throw error
|
||||
}
|
||||
return callback()
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const downloadAndComparePdf = function (projectId, exampleDir, url, callback) {
|
||||
fetch(url)
|
||||
.then(res => {
|
||||
if (!res.ok) {
|
||||
return callback(new Error('non success response: ' + res.statusText))
|
||||
}
|
||||
|
||||
const dest = fs.createWriteStream(fixturePath(`tmp/${projectId}.pdf`))
|
||||
pipeline(res.body, dest, err => {
|
||||
if (err) return callback(err)
|
||||
|
||||
checkPdfInfo(`tmp/${projectId}.pdf`, (err, optimised) => {
|
||||
if (err) return callback(err)
|
||||
|
||||
optimised.should.equal(true)
|
||||
comparePdf(projectId, exampleDir, callback)
|
||||
})
|
||||
})
|
||||
})
|
||||
.catch(callback)
|
||||
}
|
||||
|
||||
describe('Example Documents', function () {
|
||||
Client.runFakeFilestoreService(fixturePath('examples'))
|
||||
|
||||
before(function (done) {
|
||||
ClsiApp.ensureRunning(done)
|
||||
})
|
||||
before(function (done) {
|
||||
fs.rm(fixturePath('tmp'), { force: true, recursive: true }, done)
|
||||
})
|
||||
before(function (done) {
|
||||
fs.mkdir(fixturePath('tmp'), done)
|
||||
})
|
||||
after(function (done) {
|
||||
fs.rm(fixturePath('tmp'), { force: true, recursive: true }, done)
|
||||
})
|
||||
|
||||
return Array.from(fs.readdirSync(fixturePath('examples'))).map(exampleDir =>
|
||||
(exampleDir =>
|
||||
describe(exampleDir, function () {
|
||||
before(function () {
|
||||
return (this.project_id = Client.randomId() + '_' + exampleDir)
|
||||
})
|
||||
|
||||
it('should generate the correct pdf', function (done) {
|
||||
this.timeout(MOCHA_LATEX_TIMEOUT)
|
||||
return Client.compileDirectory(
|
||||
this.project_id,
|
||||
fixturePath('examples'),
|
||||
exampleDir,
|
||||
(error, res, body) => {
|
||||
if (
|
||||
error ||
|
||||
__guard__(
|
||||
body != null ? body.compile : undefined,
|
||||
x => x.status
|
||||
) === 'failure'
|
||||
) {
|
||||
console.log('DEBUG: error', error, 'body', JSON.stringify(body))
|
||||
return done(new Error('Compile failed'))
|
||||
}
|
||||
const pdf = Client.getOutputFile(body, 'pdf')
|
||||
return downloadAndComparePdf(
|
||||
this.project_id,
|
||||
exampleDir,
|
||||
pdf.url,
|
||||
done
|
||||
)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
return it('should generate the correct pdf on the second run as well', function (done) {
|
||||
this.timeout(MOCHA_LATEX_TIMEOUT)
|
||||
return Client.compileDirectory(
|
||||
this.project_id,
|
||||
fixturePath('examples'),
|
||||
exampleDir,
|
||||
(error, res, body) => {
|
||||
if (
|
||||
error ||
|
||||
__guard__(
|
||||
body != null ? body.compile : undefined,
|
||||
x => x.status
|
||||
) === 'failure'
|
||||
) {
|
||||
console.log('DEBUG: error', error, 'body', JSON.stringify(body))
|
||||
return done(new Error('Compile failed'))
|
||||
}
|
||||
const pdf = Client.getOutputFile(body, 'pdf')
|
||||
return downloadAndComparePdf(
|
||||
this.project_id,
|
||||
exampleDir,
|
||||
pdf.url,
|
||||
done
|
||||
)
|
||||
}
|
||||
)
|
||||
})
|
||||
}))(exampleDir)
|
||||
)
|
||||
})
|
||||
|
||||
function __guard__(value, transform) {
|
||||
return typeof value !== 'undefined' && value !== null
|
||||
? transform(value)
|
||||
: undefined
|
||||
}
|
91
services/clsi/test/acceptance/js/SimpleLatexFileTests.js
Normal file
91
services/clsi/test/acceptance/js/SimpleLatexFileTests.js
Normal file
@@ -0,0 +1,91 @@
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
const Client = require('./helpers/Client')
|
||||
const request = require('request')
|
||||
const ClsiApp = require('./helpers/ClsiApp')
|
||||
const Settings = require('@overleaf/settings')
|
||||
|
||||
describe('Simple LaTeX file', function () {
|
||||
before(function (done) {
|
||||
this.project_id = Client.randomId()
|
||||
this.request = {
|
||||
resources: [
|
||||
{
|
||||
path: 'main.tex',
|
||||
content: `\
|
||||
\\documentclass{article}
|
||||
\\begin{document}
|
||||
Hello world
|
||||
\\end{document}\
|
||||
`,
|
||||
},
|
||||
],
|
||||
options: {
|
||||
metricsPath: 'clsi-perf',
|
||||
metricsMethod: 'priority',
|
||||
},
|
||||
}
|
||||
return ClsiApp.ensureRunning(() => {
|
||||
return Client.compile(
|
||||
this.project_id,
|
||||
this.request,
|
||||
(error, res, body) => {
|
||||
this.error = error
|
||||
this.res = res
|
||||
this.body = body
|
||||
return done()
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('should return the PDF', function () {
|
||||
const pdf = Client.getOutputFile(this.body, 'pdf')
|
||||
return pdf.type.should.equal('pdf')
|
||||
})
|
||||
|
||||
it('should return the log', function () {
|
||||
const log = Client.getOutputFile(this.body, 'log')
|
||||
return log.type.should.equal('log')
|
||||
})
|
||||
|
||||
it('should provide the pdf for download', function (done) {
|
||||
const pdf = Client.getOutputFile(this.body, 'pdf')
|
||||
return request.get(pdf.url, (error, res, body) => {
|
||||
if (error) return done(error)
|
||||
res.statusCode.should.equal(200)
|
||||
return done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should provide the log for download', function (done) {
|
||||
const log = Client.getOutputFile(this.body, 'pdf')
|
||||
return request.get(log.url, (error, res, body) => {
|
||||
if (error) return done(error)
|
||||
res.statusCode.should.equal(200)
|
||||
return done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should gather personalized metrics', function (done) {
|
||||
request.get(`${Settings.apis.clsi.url}/metrics`, (err, res, body) => {
|
||||
if (err) return done(err)
|
||||
body
|
||||
.split('\n')
|
||||
.some(line => {
|
||||
return (
|
||||
line.startsWith('compile') &&
|
||||
line.includes('path="clsi-perf"') &&
|
||||
line.includes('method="priority"')
|
||||
)
|
||||
})
|
||||
.should.equal(true)
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
16
services/clsi/test/acceptance/js/Stats.js
Normal file
16
services/clsi/test/acceptance/js/Stats.js
Normal file
@@ -0,0 +1,16 @@
|
||||
const request = require('request')
|
||||
const Settings = require('@overleaf/settings')
|
||||
after(function (done) {
|
||||
request(
|
||||
{
|
||||
url: `${Settings.apis.clsi.url}/metrics`,
|
||||
},
|
||||
(err, response, body) => {
|
||||
if (err) return done(err)
|
||||
console.error('-- metrics --')
|
||||
console.error(body)
|
||||
console.error('-- metrics --')
|
||||
done()
|
||||
}
|
||||
)
|
||||
})
|
188
services/clsi/test/acceptance/js/SynctexTests.js
Normal file
188
services/clsi/test/acceptance/js/SynctexTests.js
Normal file
@@ -0,0 +1,188 @@
|
||||
/* eslint-disable
|
||||
no-unused-vars,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
const Client = require('./helpers/Client')
|
||||
const request = require('request')
|
||||
const { expect } = require('chai')
|
||||
const ClsiApp = require('./helpers/ClsiApp')
|
||||
const crypto = require('node:crypto')
|
||||
|
||||
describe('Syncing', function () {
|
||||
before(function (done) {
|
||||
const content = `\
|
||||
\\documentclass{article}
|
||||
\\begin{document}
|
||||
Hello world
|
||||
\\end{document}\
|
||||
`
|
||||
this.request = {
|
||||
resources: [
|
||||
{
|
||||
path: 'main.tex',
|
||||
content,
|
||||
},
|
||||
],
|
||||
}
|
||||
this.project_id = Client.randomId()
|
||||
return ClsiApp.ensureRunning(() => {
|
||||
return Client.compile(
|
||||
this.project_id,
|
||||
this.request,
|
||||
(error, res, body) => {
|
||||
this.error = error
|
||||
this.res = res
|
||||
this.body = body
|
||||
return done()
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('from code to pdf', function () {
|
||||
return it('should return the correct location', function (done) {
|
||||
return Client.syncFromCode(
|
||||
this.project_id,
|
||||
'main.tex',
|
||||
3,
|
||||
5,
|
||||
(error, pdfPositions) => {
|
||||
if (error != null) {
|
||||
throw error
|
||||
}
|
||||
expect(pdfPositions).to.deep.equal({
|
||||
pdf: [
|
||||
{
|
||||
page: 1,
|
||||
h: 133.768356,
|
||||
v: 134.764618,
|
||||
height: 6.918498,
|
||||
width: 343.71106,
|
||||
},
|
||||
],
|
||||
})
|
||||
return done()
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('from pdf to code', function () {
|
||||
return it('should return the correct location', function (done) {
|
||||
return Client.syncFromPdf(
|
||||
this.project_id,
|
||||
1,
|
||||
100,
|
||||
200,
|
||||
(error, codePositions) => {
|
||||
if (error != null) {
|
||||
throw error
|
||||
}
|
||||
expect(codePositions).to.deep.equal({
|
||||
code: [{ file: 'main.tex', line: 3, column: -1 }],
|
||||
})
|
||||
return done()
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the project directory is not available', function () {
|
||||
before(function () {
|
||||
this.other_project_id = Client.randomId()
|
||||
})
|
||||
describe('from code to pdf', function () {
|
||||
it('should return a 404 response', function (done) {
|
||||
return Client.syncFromCode(
|
||||
this.other_project_id,
|
||||
'main.tex',
|
||||
3,
|
||||
5,
|
||||
(error, body) => {
|
||||
expect(String(error)).to.include('statusCode=404')
|
||||
expect(body).to.equal('Not Found')
|
||||
return done()
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
describe('from pdf to code', function () {
|
||||
it('should return a 404 response', function (done) {
|
||||
return Client.syncFromPdf(
|
||||
this.other_project_id,
|
||||
1,
|
||||
100,
|
||||
200,
|
||||
(error, body) => {
|
||||
expect(String(error)).to.include('statusCode=404')
|
||||
expect(body).to.equal('Not Found')
|
||||
return done()
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the synctex file is not available', function () {
|
||||
before(function (done) {
|
||||
this.broken_project_id = Client.randomId()
|
||||
const content = 'this is not valid tex' // not a valid tex file
|
||||
this.request = {
|
||||
resources: [
|
||||
{
|
||||
path: 'main.tex',
|
||||
content,
|
||||
},
|
||||
],
|
||||
}
|
||||
Client.compile(
|
||||
this.broken_project_id,
|
||||
this.request,
|
||||
(error, res, body) => {
|
||||
this.error = error
|
||||
this.res = res
|
||||
this.body = body
|
||||
return done()
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
describe('from code to pdf', function () {
|
||||
it('should return a 404 response', function (done) {
|
||||
return Client.syncFromCode(
|
||||
this.broken_project_id,
|
||||
'main.tex',
|
||||
3,
|
||||
5,
|
||||
(error, body) => {
|
||||
expect(String(error)).to.include('statusCode=404')
|
||||
expect(body).to.equal('Not Found')
|
||||
return done()
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
describe('from pdf to code', function () {
|
||||
it('should return a 404 response', function (done) {
|
||||
return Client.syncFromPdf(
|
||||
this.broken_project_id,
|
||||
1,
|
||||
100,
|
||||
200,
|
||||
(error, body) => {
|
||||
expect(String(error)).to.include('statusCode=404')
|
||||
expect(body).to.equal('Not Found')
|
||||
return done()
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
66
services/clsi/test/acceptance/js/TimeoutTests.js
Normal file
66
services/clsi/test/acceptance/js/TimeoutTests.js
Normal file
@@ -0,0 +1,66 @@
|
||||
/* eslint-disable
|
||||
no-unused-vars,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
const Client = require('./helpers/Client')
|
||||
const request = require('request')
|
||||
const ClsiApp = require('./helpers/ClsiApp')
|
||||
const { expect } = require('chai')
|
||||
|
||||
describe('Timed out compile', function () {
|
||||
before(function (done) {
|
||||
this.request = {
|
||||
options: {
|
||||
timeout: 10,
|
||||
}, // seconds
|
||||
resources: [
|
||||
{
|
||||
path: 'main.tex',
|
||||
content: `\
|
||||
\\documentclass{article}
|
||||
\\begin{document}
|
||||
\\def\\x{Hello!\\par\\x}
|
||||
\\x
|
||||
\\end{document}\
|
||||
`,
|
||||
},
|
||||
],
|
||||
}
|
||||
this.project_id = Client.randomId()
|
||||
return ClsiApp.ensureRunning(() => {
|
||||
return Client.compile(
|
||||
this.project_id,
|
||||
this.request,
|
||||
(error, res, body) => {
|
||||
this.error = error
|
||||
this.res = res
|
||||
this.body = body
|
||||
return done()
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('should return a timeout error', function () {
|
||||
return this.body.compile.error.should.equal('container timed out')
|
||||
})
|
||||
|
||||
it('should return a timedout status', function () {
|
||||
return this.body.compile.status.should.equal('timedout')
|
||||
})
|
||||
|
||||
it('should return isInitialCompile flag', function () {
|
||||
expect(this.body.compile.stats.isInitialCompile).to.equal(1)
|
||||
})
|
||||
|
||||
return it('should return the log output file name', function () {
|
||||
const outputFilePaths = this.body.compile.outputFiles.map(x => x.path)
|
||||
return outputFilePaths.should.include('output.log')
|
||||
})
|
||||
})
|
617
services/clsi/test/acceptance/js/UrlCachingTests.js
Normal file
617
services/clsi/test/acceptance/js/UrlCachingTests.js
Normal file
@@ -0,0 +1,617 @@
|
||||
/* eslint-disable
|
||||
no-unused-vars,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
const express = require('express')
|
||||
const Path = require('node:path')
|
||||
const Client = require('./helpers/Client')
|
||||
const sinon = require('sinon')
|
||||
const ClsiApp = require('./helpers/ClsiApp')
|
||||
const request = require('request')
|
||||
const Settings = require('@overleaf/settings')
|
||||
|
||||
const Server = {
|
||||
run() {
|
||||
const app = express()
|
||||
|
||||
const staticServer = express.static(Path.join(__dirname, '../fixtures/'))
|
||||
|
||||
const alreadyFailed = new Map()
|
||||
app.get('/fail/:times/:id', (req, res) => {
|
||||
this.getFile(req.url)
|
||||
|
||||
const soFar = alreadyFailed.get(req.params.id) || 0
|
||||
const wanted = parseInt(req.params.times, 10)
|
||||
if (soFar < wanted) {
|
||||
alreadyFailed.set(req.params.id, soFar + 1)
|
||||
res.status(503).end()
|
||||
} else {
|
||||
res.send('THE CONTENT')
|
||||
}
|
||||
})
|
||||
|
||||
app.get('/not-found', (req, res, next) => {
|
||||
this.getFile(req.url)
|
||||
res.status(404).end()
|
||||
})
|
||||
|
||||
app.get('/project/:projectId/file/:fileId', (req, res, next) => {
|
||||
this.getFile(req.url)
|
||||
return res.send(`${req.params.projectId}:${req.params.fileId}`)
|
||||
})
|
||||
|
||||
app.get('/bucket/:bucket/key/*', (req, res, next) => {
|
||||
this.getFile(req.url)
|
||||
return res.send(`${req.params.bucket}:${req.params[0]}`)
|
||||
})
|
||||
|
||||
app.get('/:random_id/*', (req, res, next) => {
|
||||
this.getFile(req.url)
|
||||
req.url = `/${req.params[0]}`
|
||||
return staticServer(req, res, next)
|
||||
})
|
||||
|
||||
Client.startFakeFilestoreApp(app)
|
||||
},
|
||||
|
||||
getFile() {},
|
||||
|
||||
randomId() {
|
||||
return Math.random().toString(16).slice(2)
|
||||
},
|
||||
}
|
||||
|
||||
describe('Url Caching', function () {
|
||||
Server.run()
|
||||
|
||||
describe('Retries', function () {
|
||||
before(function (done) {
|
||||
this.project_id = Client.randomId()
|
||||
this.happyFile = `${Server.randomId()}/lion.png`
|
||||
this.retryFileOnce = `fail/1/${Server.randomId()}`
|
||||
this.retryFileTwice = `fail/2/${Server.randomId()}`
|
||||
this.fatalFile = `fail/42/${Server.randomId()}`
|
||||
this.request = {
|
||||
resources: [
|
||||
{
|
||||
path: 'main.tex',
|
||||
content: `\
|
||||
\\documentclass{article}
|
||||
\\usepackage{graphicx}
|
||||
\\begin{document}
|
||||
\\includegraphics{lion.png}
|
||||
\\end{document}\
|
||||
`,
|
||||
},
|
||||
{
|
||||
path: 'lion.png',
|
||||
url: `http://filestore/${this.happyFile}`,
|
||||
},
|
||||
{
|
||||
path: 'foo.tex',
|
||||
url: `http://filestore/${this.retryFileOnce}`,
|
||||
},
|
||||
{
|
||||
path: 'foo.tex',
|
||||
url: `http://filestore/${this.retryFileTwice}`,
|
||||
},
|
||||
{
|
||||
path: 'foo.tex',
|
||||
url: `http://filestore/${this.fatalFile}`,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
sinon.spy(Server, 'getFile')
|
||||
ClsiApp.ensureRunning(() => {
|
||||
Client.compile(this.project_id, this.request, (error, res, body) => {
|
||||
this.error = error
|
||||
this.res = res
|
||||
this.body = body
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
after(function () {
|
||||
Server.getFile.restore()
|
||||
})
|
||||
|
||||
function expectNFilestoreRequests(file, count) {
|
||||
Server.getFile.args.filter(a => a[0] === file).should.have.length(count)
|
||||
}
|
||||
|
||||
it('should download the happy file once', function () {
|
||||
expectNFilestoreRequests(`/${this.happyFile}`, 1)
|
||||
})
|
||||
it('should retry the download of the unhappy files', function () {
|
||||
expectNFilestoreRequests(`/${this.retryFileOnce}`, 2)
|
||||
expectNFilestoreRequests(`/${this.retryFileTwice}`, 3)
|
||||
expectNFilestoreRequests(`/${this.fatalFile}`, 3)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Downloading an image for the first time', function () {
|
||||
before(function (done) {
|
||||
this.project_id = Client.randomId()
|
||||
this.file = `${Server.randomId()}/lion.png`
|
||||
this.request = {
|
||||
resources: [
|
||||
{
|
||||
path: 'main.tex',
|
||||
content: `\
|
||||
\\documentclass{article}
|
||||
\\usepackage{graphicx}
|
||||
\\begin{document}
|
||||
\\includegraphics{lion.png}
|
||||
\\end{document}\
|
||||
`,
|
||||
},
|
||||
{
|
||||
path: 'lion.png',
|
||||
url: `http://filestore/${this.file}`,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
sinon.spy(Server, 'getFile')
|
||||
return ClsiApp.ensureRunning(() => {
|
||||
return Client.compile(
|
||||
this.project_id,
|
||||
this.request,
|
||||
(error, res, body) => {
|
||||
this.error = error
|
||||
this.res = res
|
||||
this.body = body
|
||||
return done()
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
return Server.getFile.restore()
|
||||
})
|
||||
|
||||
return it('should download the image', function () {
|
||||
return Server.getFile.calledWith(`/${this.file}`).should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('When an image is in the cache and the last modified date is unchanged', function () {
|
||||
before(function (done) {
|
||||
this.project_id = Client.randomId()
|
||||
this.file = `${Server.randomId()}/lion.png`
|
||||
this.request = {
|
||||
resources: [
|
||||
{
|
||||
path: 'main.tex',
|
||||
content: `\
|
||||
\\documentclass{article}
|
||||
\\usepackage{graphicx}
|
||||
\\begin{document}
|
||||
\\includegraphics{lion.png}
|
||||
\\end{document}\
|
||||
`,
|
||||
},
|
||||
(this.image_resource = {
|
||||
path: 'lion.png',
|
||||
url: `http://filestore/${this.file}`,
|
||||
modified: Date.now(),
|
||||
}),
|
||||
],
|
||||
}
|
||||
|
||||
return Client.compile(
|
||||
this.project_id,
|
||||
this.request,
|
||||
(error, res, body) => {
|
||||
this.error = error
|
||||
this.res = res
|
||||
this.body = body
|
||||
sinon.spy(Server, 'getFile')
|
||||
return Client.compile(
|
||||
this.project_id,
|
||||
this.request,
|
||||
(error1, res1, body1) => {
|
||||
this.error = error1
|
||||
this.res = res1
|
||||
this.body = body1
|
||||
return done()
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
after(function () {
|
||||
return Server.getFile.restore()
|
||||
})
|
||||
|
||||
it('should not download the image again', function () {
|
||||
return Server.getFile.called.should.equal(false)
|
||||
})
|
||||
|
||||
it('should gather metrics', function (done) {
|
||||
request.get(`${Settings.apis.clsi.url}/metrics`, (err, res, body) => {
|
||||
if (err) return done(err)
|
||||
body
|
||||
.split('\n')
|
||||
.some(line => {
|
||||
return (
|
||||
line.startsWith('url_source') && line.includes('path="unknown"')
|
||||
)
|
||||
})
|
||||
.should.equal(true)
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('When an image is in the cache and the last modified date is advanced', function () {
|
||||
before(function (done) {
|
||||
this.project_id = Client.randomId()
|
||||
this.file = `${Server.randomId()}/lion.png`
|
||||
this.request = {
|
||||
resources: [
|
||||
{
|
||||
path: 'main.tex',
|
||||
content: `\
|
||||
\\documentclass{article}
|
||||
\\usepackage{graphicx}
|
||||
\\begin{document}
|
||||
\\includegraphics{lion.png}
|
||||
\\end{document}\
|
||||
`,
|
||||
},
|
||||
(this.image_resource = {
|
||||
path: 'lion.png',
|
||||
url: `http://filestore/${this.file}`,
|
||||
modified: (this.last_modified = Date.now()),
|
||||
}),
|
||||
],
|
||||
}
|
||||
|
||||
return Client.compile(
|
||||
this.project_id,
|
||||
this.request,
|
||||
(error, res, body) => {
|
||||
this.error = error
|
||||
this.res = res
|
||||
this.body = body
|
||||
sinon.spy(Server, 'getFile')
|
||||
this.image_resource.modified = new Date(this.last_modified + 3000)
|
||||
return Client.compile(
|
||||
this.project_id,
|
||||
this.request,
|
||||
(error1, res1, body1) => {
|
||||
this.error = error1
|
||||
this.res = res1
|
||||
this.body = body1
|
||||
return done()
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
return Server.getFile.restore()
|
||||
})
|
||||
|
||||
return it('should download the image again', function () {
|
||||
return Server.getFile.called.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('When an image is in the cache and the last modified date is further in the past', function () {
|
||||
before(function (done) {
|
||||
this.project_id = Client.randomId()
|
||||
this.file = `${Server.randomId()}/lion.png`
|
||||
this.request = {
|
||||
resources: [
|
||||
{
|
||||
path: 'main.tex',
|
||||
content: `\
|
||||
\\documentclass{article}
|
||||
\\usepackage{graphicx}
|
||||
\\begin{document}
|
||||
\\includegraphics{lion.png}
|
||||
\\end{document}\
|
||||
`,
|
||||
},
|
||||
(this.image_resource = {
|
||||
path: 'lion.png',
|
||||
url: `http://filestore/${this.file}`,
|
||||
modified: (this.last_modified = Date.now()),
|
||||
}),
|
||||
],
|
||||
}
|
||||
|
||||
return Client.compile(
|
||||
this.project_id,
|
||||
this.request,
|
||||
(error, res, body) => {
|
||||
this.error = error
|
||||
this.res = res
|
||||
this.body = body
|
||||
sinon.spy(Server, 'getFile')
|
||||
this.image_resource.modified = new Date(this.last_modified - 3000)
|
||||
return Client.compile(
|
||||
this.project_id,
|
||||
this.request,
|
||||
(error1, res1, body1) => {
|
||||
this.error = error1
|
||||
this.res = res1
|
||||
this.body = body1
|
||||
return done()
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
return Server.getFile.restore()
|
||||
})
|
||||
|
||||
return it('should download the other revision', function () {
|
||||
return Server.getFile.called.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('When an image is in the cache and the last modified date is not specified', function () {
|
||||
before(function (done) {
|
||||
this.project_id = Client.randomId()
|
||||
this.file = `${Server.randomId()}/lion.png`
|
||||
this.request = {
|
||||
resources: [
|
||||
{
|
||||
path: 'main.tex',
|
||||
content: `\
|
||||
\\documentclass{article}
|
||||
\\usepackage{graphicx}
|
||||
\\begin{document}
|
||||
\\includegraphics{lion.png}
|
||||
\\end{document}\
|
||||
`,
|
||||
},
|
||||
(this.image_resource = {
|
||||
path: 'lion.png',
|
||||
url: `http://filestore/${this.file}`,
|
||||
modified: (this.last_modified = Date.now()),
|
||||
}),
|
||||
],
|
||||
}
|
||||
|
||||
return Client.compile(
|
||||
this.project_id,
|
||||
this.request,
|
||||
(error, res, body) => {
|
||||
this.error = error
|
||||
this.res = res
|
||||
this.body = body
|
||||
sinon.spy(Server, 'getFile')
|
||||
delete this.image_resource.modified
|
||||
return Client.compile(
|
||||
this.project_id,
|
||||
this.request,
|
||||
(error1, res1, body1) => {
|
||||
this.error = error1
|
||||
this.res = res1
|
||||
this.body = body1
|
||||
return done()
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
return Server.getFile.restore()
|
||||
})
|
||||
|
||||
return it('should download the image again', function () {
|
||||
return Server.getFile.called.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('After clearing the cache', function () {
|
||||
before(function (done) {
|
||||
this.project_id = Client.randomId()
|
||||
this.file = `${Server.randomId()}/lion.png`
|
||||
this.request = {
|
||||
resources: [
|
||||
{
|
||||
path: 'main.tex',
|
||||
content: `\
|
||||
\\documentclass{article}
|
||||
\\usepackage{graphicx}
|
||||
\\begin{document}
|
||||
\\includegraphics{lion.png}
|
||||
\\end{document}\
|
||||
`,
|
||||
},
|
||||
(this.image_resource = {
|
||||
path: 'lion.png',
|
||||
url: `http://filestore/${this.file}`,
|
||||
modified: (this.last_modified = Date.now()),
|
||||
}),
|
||||
],
|
||||
}
|
||||
|
||||
return Client.compile(this.project_id, this.request, error => {
|
||||
if (error != null) {
|
||||
throw error
|
||||
}
|
||||
return Client.clearCache(this.project_id, (error, res, body) => {
|
||||
if (error != null) {
|
||||
throw error
|
||||
}
|
||||
sinon.spy(Server, 'getFile')
|
||||
return Client.compile(
|
||||
this.project_id,
|
||||
this.request,
|
||||
(error1, res1, body1) => {
|
||||
this.error = error1
|
||||
this.res = res1
|
||||
this.body = body1
|
||||
return done()
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
return Server.getFile.restore()
|
||||
})
|
||||
|
||||
return it('should download the image again', function () {
|
||||
return Server.getFile.called.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('fallbackURL', function () {
|
||||
describe('when the primary resource is available', function () {
|
||||
before(function (done) {
|
||||
this.project_id = Client.randomId()
|
||||
this.file = `/project/${Server.randomId()}/file/${Server.randomId()}`
|
||||
this.fallback = `/bucket/project-blobs/key/ab/cd/${Server.randomId()}`
|
||||
this.request = {
|
||||
resources: [
|
||||
{
|
||||
path: 'main.tex',
|
||||
content: `\
|
||||
\\documentclass{article}
|
||||
\\usepackage{graphicx}
|
||||
\\begin{document}
|
||||
\\includegraphics{lion.png}
|
||||
\\end{document}\
|
||||
`,
|
||||
},
|
||||
{
|
||||
path: 'lion.png',
|
||||
url: `http://filestore${this.file}`,
|
||||
fallbackURL: `http://filestore${this.fallback}`,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
sinon.spy(Server, 'getFile')
|
||||
return ClsiApp.ensureRunning(() => {
|
||||
return Client.compile(
|
||||
this.project_id,
|
||||
this.request,
|
||||
(error, res, body) => {
|
||||
this.error = error
|
||||
this.res = res
|
||||
this.body = body
|
||||
return done()
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
after(function () {
|
||||
return Server.getFile.restore()
|
||||
})
|
||||
|
||||
it('should download from the primary', function () {
|
||||
Server.getFile.calledWith(this.file).should.equal(true)
|
||||
})
|
||||
it('should not download from the fallback', function () {
|
||||
Server.getFile.calledWith(this.fallback).should.equal(false)
|
||||
})
|
||||
|
||||
it('should gather metrics', function (done) {
|
||||
request.get(`${Settings.apis.clsi.url}/metrics`, (err, res, body) => {
|
||||
if (err) return done(err)
|
||||
body
|
||||
.split('\n')
|
||||
.some(line => {
|
||||
return (
|
||||
line.startsWith('url_source') &&
|
||||
line.includes('path="user-files"')
|
||||
)
|
||||
})
|
||||
.should.equal(true)
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the primary resource is not available', function () {
|
||||
before(function (done) {
|
||||
this.project_id = Client.randomId()
|
||||
this.file = `/project/${Server.randomId()}/file/${Server.randomId()}`
|
||||
this.fallback = `/bucket/project-blobs/key/ab/cd/${Server.randomId()}`
|
||||
this.request = {
|
||||
resources: [
|
||||
{
|
||||
path: 'main.tex',
|
||||
content: `\
|
||||
\\documentclass{article}
|
||||
\\usepackage{graphicx}
|
||||
\\begin{document}
|
||||
\\includegraphics{lion.png}
|
||||
\\end{document}\
|
||||
`,
|
||||
},
|
||||
{
|
||||
path: 'lion.png',
|
||||
url: `http://filestore/not-found`,
|
||||
fallbackURL: `http://filestore${this.fallback}`,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
sinon.spy(Server, 'getFile')
|
||||
return ClsiApp.ensureRunning(() => {
|
||||
return Client.compile(
|
||||
this.project_id,
|
||||
this.request,
|
||||
(error, res, body) => {
|
||||
this.error = error
|
||||
this.res = res
|
||||
this.body = body
|
||||
return done()
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
after(function () {
|
||||
return Server.getFile.restore()
|
||||
})
|
||||
|
||||
it('should download from the fallback', function () {
|
||||
Server.getFile.calledWith(`/not-found`).should.equal(true)
|
||||
Server.getFile.calledWith(this.fallback).should.equal(true)
|
||||
})
|
||||
|
||||
it('should gather metrics', function (done) {
|
||||
request.get(`${Settings.apis.clsi.url}/metrics`, (err, res, body) => {
|
||||
if (err) return done(err)
|
||||
body
|
||||
.split('\n')
|
||||
.some(line => {
|
||||
return (
|
||||
line.startsWith('url_source') &&
|
||||
line.includes('path="project-blobs"')
|
||||
)
|
||||
})
|
||||
.should.equal(true)
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
71
services/clsi/test/acceptance/js/WordcountTests.js
Normal file
71
services/clsi/test/acceptance/js/WordcountTests.js
Normal file
@@ -0,0 +1,71 @@
|
||||
/* eslint-disable
|
||||
no-unused-vars,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
const Client = require('./helpers/Client')
|
||||
const request = require('request')
|
||||
const { expect } = require('chai')
|
||||
const path = require('node:path')
|
||||
const fs = require('node:fs')
|
||||
const ClsiApp = require('./helpers/ClsiApp')
|
||||
|
||||
describe('Syncing', function () {
|
||||
before(function (done) {
|
||||
this.request = {
|
||||
resources: [
|
||||
{
|
||||
path: 'main.tex',
|
||||
content: fs.readFileSync(
|
||||
path.join(__dirname, '../fixtures/naugty_strings.txt'),
|
||||
'utf-8'
|
||||
),
|
||||
},
|
||||
],
|
||||
}
|
||||
this.project_id = Client.randomId()
|
||||
return ClsiApp.ensureRunning(() => {
|
||||
return Client.compile(
|
||||
this.project_id,
|
||||
this.request,
|
||||
(error, res, body) => {
|
||||
this.error = error
|
||||
this.res = res
|
||||
this.body = body
|
||||
return done()
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
return describe('wordcount file', function () {
|
||||
return it('should return wordcount info', function (done) {
|
||||
return Client.wordcount(this.project_id, 'main.tex', (error, result) => {
|
||||
if (error != null) {
|
||||
throw error
|
||||
}
|
||||
expect(result).to.deep.equal({
|
||||
texcount: {
|
||||
encode: 'utf8',
|
||||
textWords: 2281,
|
||||
headWords: 2,
|
||||
outside: 0,
|
||||
headers: 2,
|
||||
elements: 0,
|
||||
mathInline: 6,
|
||||
mathDisplay: 0,
|
||||
errors: 0,
|
||||
messages: '',
|
||||
},
|
||||
})
|
||||
return done()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
248
services/clsi/test/acceptance/js/helpers/Client.js
Normal file
248
services/clsi/test/acceptance/js/helpers/Client.js
Normal file
@@ -0,0 +1,248 @@
|
||||
/* eslint-disable
|
||||
no-unused-vars,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS101: Remove unnecessary use of Array.from
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
let Client
|
||||
const express = require('express')
|
||||
const request = require('request')
|
||||
const fs = require('node:fs')
|
||||
const Settings = require('@overleaf/settings')
|
||||
|
||||
module.exports = Client = {
|
||||
host: Settings.apis.clsi.url,
|
||||
|
||||
randomId() {
|
||||
return Math.random().toString(16).slice(2)
|
||||
},
|
||||
|
||||
compile(projectId, data, callback) {
|
||||
if (callback == null) {
|
||||
callback = function () {}
|
||||
}
|
||||
if (data) {
|
||||
// Enable pdf caching unless disabled explicitly.
|
||||
data.options = Object.assign({}, { enablePdfCaching: true }, data.options)
|
||||
}
|
||||
return request.post(
|
||||
{
|
||||
url: `${this.host}/project/${projectId}/compile`,
|
||||
json: {
|
||||
compile: data,
|
||||
},
|
||||
},
|
||||
callback
|
||||
)
|
||||
},
|
||||
|
||||
clearCache(projectId, callback) {
|
||||
if (callback == null) {
|
||||
callback = function () {}
|
||||
}
|
||||
return request.del(`${this.host}/project/${projectId}`, callback)
|
||||
},
|
||||
|
||||
getOutputFile(response, type) {
|
||||
for (const file of Array.from(response.compile.outputFiles)) {
|
||||
if (file.type === type && file.url.match(`output.${type}`)) {
|
||||
return file
|
||||
}
|
||||
}
|
||||
return null
|
||||
},
|
||||
|
||||
runFakeFilestoreService(directory) {
|
||||
const app = express()
|
||||
app.use(express.static(directory))
|
||||
this.startFakeFilestoreApp(app)
|
||||
},
|
||||
|
||||
startFakeFilestoreApp(app) {
|
||||
let server
|
||||
before(function (done) {
|
||||
server = app.listen(error => {
|
||||
if (error) {
|
||||
done(new Error('error starting server: ' + error.message))
|
||||
} else {
|
||||
const addr = server.address()
|
||||
Settings.filestoreDomainOveride = `http://127.0.0.1:${addr.port}`
|
||||
done()
|
||||
}
|
||||
})
|
||||
})
|
||||
after(function (done) {
|
||||
server.close(done)
|
||||
})
|
||||
},
|
||||
|
||||
syncFromCode(projectId, file, line, column, callback) {
|
||||
Client.syncFromCodeWithImage(projectId, file, line, column, '', callback)
|
||||
},
|
||||
|
||||
syncFromCodeWithImage(projectId, file, line, column, imageName, callback) {
|
||||
if (callback == null) {
|
||||
callback = function () {}
|
||||
}
|
||||
return request.get(
|
||||
{
|
||||
url: `${this.host}/project/${projectId}/sync/code`,
|
||||
qs: {
|
||||
imageName,
|
||||
file,
|
||||
line,
|
||||
column,
|
||||
},
|
||||
json: true,
|
||||
},
|
||||
(error, response, body) => {
|
||||
if (error != null) {
|
||||
return callback(error)
|
||||
}
|
||||
if (response.statusCode !== 200) {
|
||||
return callback(new Error(`statusCode=${response.statusCode}`), body)
|
||||
}
|
||||
return callback(null, body)
|
||||
}
|
||||
)
|
||||
},
|
||||
|
||||
syncFromPdf(projectId, page, h, v, callback) {
|
||||
Client.syncFromPdfWithImage(projectId, page, h, v, '', callback)
|
||||
},
|
||||
|
||||
syncFromPdfWithImage(projectId, page, h, v, imageName, callback) {
|
||||
if (callback == null) {
|
||||
callback = function () {}
|
||||
}
|
||||
return request.get(
|
||||
{
|
||||
url: `${this.host}/project/${projectId}/sync/pdf`,
|
||||
qs: {
|
||||
imageName,
|
||||
page,
|
||||
h,
|
||||
v,
|
||||
},
|
||||
json: true,
|
||||
},
|
||||
(error, response, body) => {
|
||||
if (error != null) {
|
||||
return callback(error)
|
||||
}
|
||||
if (response.statusCode !== 200) {
|
||||
return callback(new Error(`statusCode=${response.statusCode}`), body)
|
||||
}
|
||||
return callback(null, body)
|
||||
}
|
||||
)
|
||||
},
|
||||
|
||||
compileDirectory(projectId, baseDirectory, directory, callback) {
|
||||
if (callback == null) {
|
||||
callback = function () {}
|
||||
}
|
||||
const resources = []
|
||||
let entities = fs.readdirSync(`${baseDirectory}/${directory}`)
|
||||
let rootResourcePath = 'main.tex'
|
||||
while (entities.length > 0) {
|
||||
const entity = entities.pop()
|
||||
const stat = fs.statSync(`${baseDirectory}/${directory}/${entity}`)
|
||||
if (stat.isDirectory()) {
|
||||
entities = entities.concat(
|
||||
fs
|
||||
.readdirSync(`${baseDirectory}/${directory}/${entity}`)
|
||||
.map(subEntity => {
|
||||
if (subEntity === 'main.tex') {
|
||||
rootResourcePath = `${entity}/${subEntity}`
|
||||
}
|
||||
return `${entity}/${subEntity}`
|
||||
})
|
||||
)
|
||||
} else if (stat.isFile() && entity !== 'output.pdf') {
|
||||
const extension = entity.split('.').pop()
|
||||
if (
|
||||
[
|
||||
'tex',
|
||||
'bib',
|
||||
'cls',
|
||||
'sty',
|
||||
'pdf_tex',
|
||||
'Rtex',
|
||||
'ist',
|
||||
'md',
|
||||
'Rmd',
|
||||
'Rnw',
|
||||
].indexOf(extension) > -1
|
||||
) {
|
||||
resources.push({
|
||||
path: entity,
|
||||
content: fs
|
||||
.readFileSync(`${baseDirectory}/${directory}/${entity}`)
|
||||
.toString(),
|
||||
})
|
||||
} else if (
|
||||
['eps', 'ttf', 'png', 'jpg', 'pdf', 'jpeg'].indexOf(extension) > -1
|
||||
) {
|
||||
resources.push({
|
||||
path: entity,
|
||||
url: `http://filestore/${directory}/${entity}`,
|
||||
modified: stat.mtime,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fs.readFile(
|
||||
`${baseDirectory}/${directory}/options.json`,
|
||||
(error, body) => {
|
||||
const req = {
|
||||
resources,
|
||||
rootResourcePath,
|
||||
}
|
||||
|
||||
if (error == null) {
|
||||
body = JSON.parse(body)
|
||||
req.options = body
|
||||
}
|
||||
|
||||
return this.compile(projectId, req, callback)
|
||||
}
|
||||
)
|
||||
},
|
||||
|
||||
wordcount(projectId, file, callback) {
|
||||
const image = undefined
|
||||
Client.wordcountWithImage(projectId, file, image, callback)
|
||||
},
|
||||
|
||||
wordcountWithImage(projectId, file, image, callback) {
|
||||
if (callback == null) {
|
||||
callback = function () {}
|
||||
}
|
||||
return request.get(
|
||||
{
|
||||
url: `${this.host}/project/${projectId}/wordcount`,
|
||||
qs: {
|
||||
image,
|
||||
file,
|
||||
},
|
||||
},
|
||||
(error, response, body) => {
|
||||
if (error != null) {
|
||||
return callback(error)
|
||||
}
|
||||
if (response.statusCode !== 200) {
|
||||
return callback(new Error(`statusCode=${response.statusCode}`), body)
|
||||
}
|
||||
return callback(null, JSON.parse(body))
|
||||
}
|
||||
)
|
||||
},
|
||||
}
|
50
services/clsi/test/acceptance/js/helpers/ClsiApp.js
Normal file
50
services/clsi/test/acceptance/js/helpers/ClsiApp.js
Normal file
@@ -0,0 +1,50 @@
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS101: Remove unnecessary use of Array.from
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* DS103: Rewrite code to no longer use __guard__
|
||||
* DS205: Consider reworking code to avoid use of IIFEs
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
const app = require('../../../../app')
|
||||
const Settings = require('@overleaf/settings')
|
||||
|
||||
module.exports = {
|
||||
running: false,
|
||||
initing: false,
|
||||
callbacks: [],
|
||||
ensureRunning(callback) {
|
||||
if (callback == null) {
|
||||
callback = function () {}
|
||||
}
|
||||
if (this.running) {
|
||||
return callback()
|
||||
} else if (this.initing) {
|
||||
return this.callbacks.push(callback)
|
||||
} else {
|
||||
this.initing = true
|
||||
this.callbacks.push(callback)
|
||||
return app.listen(
|
||||
Settings.internal.clsi.port,
|
||||
Settings.internal.clsi.host,
|
||||
error => {
|
||||
if (error != null) {
|
||||
throw error
|
||||
}
|
||||
this.running = true
|
||||
|
||||
return (() => {
|
||||
const result = []
|
||||
for (callback of Array.from(this.callbacks)) {
|
||||
result.push(callback())
|
||||
}
|
||||
return result
|
||||
})()
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
}
|
Reference in New Issue
Block a user