Merge pull request #28752 from overleaf/msm-clsi-acceptance-async-await

[clsi] async/await migration in acceptance tests

GitOrigin-RevId: d614fabb6d568dc5c955603fb923fb40b871a703
This commit is contained in:
Miguel Serrano
2025-10-01 10:36:36 +02:00
committed by Copybot
parent c22e44438e
commit e8bc186ca0
13 changed files with 692 additions and 1199 deletions

View File

@@ -3,7 +3,7 @@ const ClsiApp = require('./helpers/ClsiApp')
const { expect } = require('chai')
describe('AllowedImageNames', function () {
beforeEach(function (done) {
beforeEach(async function () {
this.project_id = Client.randomId()
this.request = {
options: {
@@ -21,22 +21,21 @@ Hello world
},
],
}
ClsiApp.ensureRunning(done)
await ClsiApp.ensureRunning()
})
describe('with a valid name', function () {
beforeEach(function (done) {
beforeEach(async function () {
this.request.options.imageName = process.env.TEXLIVE_IMAGE
Client.compile(this.project_id, this.request, (error, res, body) => {
try {
this.body = await Client.compile(this.project_id, this.request)
} catch (error) {
this.error = error
this.res = res
this.body = body
done(error)
})
}
})
it('should return success', function () {
expect(this.res.statusCode).to.equal(200)
expect(this.error).not.to.exist
})
it('should return a PDF', function () {
@@ -49,17 +48,16 @@ Hello world
})
describe('with an invalid name', function () {
beforeEach(function (done) {
beforeEach(async function () {
this.request.options.imageName = 'something/evil:1337'
Client.compile(this.project_id, this.request, (error, res, body) => {
try {
this.body = await Client.compile(this.project_id, this.request)
} catch (error) {
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)
expect(this.error.response.status).to.equal(500)
})
it('should not return a PDF', function () {
@@ -72,118 +70,109 @@ Hello world
})
describe('syncToCode', function () {
beforeEach(function (done) {
Client.compile(this.project_id, this.request, done)
beforeEach(async function () {
await Client.compile(this.project_id, this.request)
})
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 error out with an invalid imageName', async function () {
const rejects = () =>
expect(
Client.syncFromCodeWithImage(
this.project_id,
'main.tex',
3,
5,
'something/evil:1337'
)
).to.eventually.be.rejected
await rejects().and.have.property('body', 'invalid image')
await rejects().and.have.property('info').to.contain({ status: 400 })
})
it('should produce a mapping a valid imageName', function (done) {
Client.syncFromCodeWithImage(
it('should produce a mapping a valid imageName', async function () {
const result = await 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,
},
],
downloadedFromCache: false,
})
done()
}
process.env.TEXLIVE_IMAGE
)
expect(result).to.deep.equal({
pdf: [
{
page: 1,
h: 133.768356,
v: 134.764618,
height: 6.918498,
width: 343.71106,
},
],
downloadedFromCache: false,
})
})
})
describe('syncToPdf', function () {
beforeEach(function (done) {
Client.compile(this.project_id, this.request, done)
beforeEach(async function () {
await Client.compile(this.project_id, this.request)
})
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 error out with an invalid imageName', async function () {
const rejects = () =>
expect(
Client.syncFromPdfWithImage(
this.project_id,
'main.tex',
100,
200,
'something/evil:1337'
)
).to.eventually.be.rejected
await rejects().and.have.property('body', 'invalid image')
await rejects().and.have.property('info').to.contain({ status: 400 })
})
it('should produce a mapping a valid imageName', function (done) {
Client.syncFromPdfWithImage(
it('should produce a mapping a valid imageName', async function () {
const result = await 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 }],
downloadedFromCache: false,
})
done()
}
process.env.TEXLIVE_IMAGE
)
expect(result).to.deep.equal({
code: [{ file: 'main.tex', line: 3, column: -1 }],
downloadedFromCache: false,
})
})
})
describe('wordcount', function () {
beforeEach(function (done) {
Client.compile(this.project_id, this.request, done)
beforeEach(async function () {
await Client.compile(this.project_id, this.request)
})
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 error out with an invalid imageName', async function () {
const rejects = () =>
expect(
Client.wordcountWithImage(
this.project_id,
'main.tex',
'something/evil:1337'
)
).to.eventually.be.rejected
await rejects().and.have.property('body', 'invalid image')
await rejects().and.have.property('info').to.contain({ status: 400 })
})
it('should produce a texcout a valid imageName', function (done) {
Client.wordcountWithImage(
it('should produce a texcout a valid imageName', async function () {
const result = await 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()
}
process.env.TEXLIVE_IMAGE
)
expect(result).to.exist
expect(result.texcount).to.exist
})
})
})

View File

@@ -1,20 +1,9 @@
/* 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) {
before(async function () {
this.broken_request = {
resources: [
{
@@ -41,26 +30,17 @@ Hello world
},
],
}
return ClsiApp.ensureRunning(done)
await ClsiApp.ensureRunning()
})
describe('on first run', function () {
before(function (done) {
before(async function () {
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()
}
)
this.body = await Client.compile(this.project_id, this.broken_request)
})
it('should return a failure status', function () {
return this.body.compile.status.should.equal('failure')
this.body.compile.status.should.equal('failure')
})
it('should return isInitialCompile flag', function () {
@@ -82,25 +62,15 @@ Hello world
})
})
return describe('on second run', function () {
before(function (done) {
describe('on second run', function () {
before(async function () {
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()
}
)
})
await Client.compile(this.project_id, this.correct_request)
this.body = await Client.compile(this.project_id, this.broken_request)
})
it('should return a failure status', function () {
return this.body.compile.status.should.equal('failure')
this.body.compile.status.should.equal('failure')
})
it('should not return isInitialCompile flag', function () {

View File

@@ -1,19 +1,8 @@
/* 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) {
before(async function () {
this.request = {
resources: [
{
@@ -27,45 +16,27 @@ Hello world
},
],
}
return ClsiApp.ensureRunning(done)
await ClsiApp.ensureRunning()
})
return describe('on first run', function () {
before(function (done) {
describe('on first run', function () {
before(async function () {
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()
}
)
this.body = await Client.compile(this.project_id, this.request)
})
it('should return a success status', function () {
return this.body.compile.status.should.equal('success')
this.body.compile.status.should.equal('success')
})
return describe('after file has been deleted', function () {
before(function (done) {
describe('after file has been deleted', function () {
before(async function () {
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()
}
)
this.body = await Client.compile(this.project_id, this.request)
})
return it('should return a failure status', function () {
return this.body.compile.status.should.equal('failure')
it('should return a failure status', function () {
this.body.compile.status.should.equal('failure')
})
})
})

View File

@@ -1,24 +1,11 @@
/* 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 Stream = require('node:stream')
const fs = require('node:fs')
const fsPromises = require('node:fs/promises')
const ChildProcess = require('node:child_process')
const { promisify } = require('node:util')
const ClsiApp = require('./helpers/ClsiApp')
const logger = require('@overleaf/logger')
const Path = require('node:path')
const fixturePath = path => {
if (path.slice(0, 3) === 'tmp') {
@@ -27,6 +14,7 @@ const fixturePath = path => {
return Path.join(__dirname, '../fixtures/', path)
}
const process = require('node:process')
const pipeline = promisify(Stream.pipeline)
console.log(
process.pid,
process.ppid,
@@ -37,249 +25,167 @@ console.log(
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 convertToPng = function (pdfPath, pngPath) {
return new Promise((resolve, reject) => {
const command = `convert ${fixturePath(pdfPath)} ${fixturePath(pngPath)}`
console.log('COMMAND')
console.log(command)
const convert = ChildProcess.exec(command)
convert.stdout.on('data', chunk => console.log('STDOUT', chunk.toString()))
convert.stderr.on('data', chunk => console.log('STDERR', chunk.toString()))
convert.on('exit', () => resolve())
convert.on('error', error => reject(error))
})
}
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)
const compare = function (originalPath, generatedPath) {
return new Promise((resolve, reject) => {
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))
proc.on('exit', () => {
if (stderr.trim() === '0 (0)') {
// remove output diff if test matches expected image
fs.unlink(diffFile, err => {
if (err) {
reject(err)
}
)
})
resolve(true)
} else {
console.log('compare result', stderr)
resolve(false)
}
})
}
return compareNext(0, callback)
})
}
const comparePdf = function (projectId, exampleDir, callback) {
if (callback == null) {
callback = function () {}
const checkPdfInfo = function (pdfPath) {
return new Promise((resolve, reject) => {
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()))
proc.on('exit', () => {
if (stdout.match(/Optimized:\s+yes/)) {
resolve(true)
} else {
resolve(false)
}
})
proc.on('error', error => reject(error))
})
}
const compareMultiplePages = async function (projectId) {
async function compareNext(pageNo) {
const path = `tmp/${projectId}-source-${pageNo}.png`
try {
await fsPromises.stat(fixturePath(path))
} catch (error) {
return
}
const same = await compare(
`tmp/${projectId}-source-${pageNo}.png`,
`tmp/${projectId}-generated-${pageNo}.png`
)
same.should.equal(true)
await compareNext(pageNo + 1)
}
await compareNext(0)
}
const comparePdf = async function (projectId, exampleDir) {
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()
})
}
}
)
}
)
}
await convertToPng(`tmp/${projectId}.pdf`, `tmp/${projectId}-generated.png`)
await convertToPng(
`examples/${exampleDir}/output.pdf`,
`tmp/${projectId}-source.png`
)
try {
await fsPromises.stat(fixturePath(`tmp/${projectId}-source-0.png`))
await compareMultiplePages(projectId)
} catch (error) {
const same = await compare(
`tmp/${projectId}-source.png`,
`tmp/${projectId}-generated.png`
)
same.should.equal(true)
}
}
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)
const downloadAndComparePdf = async function (projectId, exampleDir, url) {
const res = await fetch(url)
if (!res.ok) {
throw new Error('non success response: ' + res.statusText)
}
const dest = fs.createWriteStream(fixturePath(`tmp/${projectId}.pdf`))
await pipeline(res.body, dest)
const optimised = await checkPdfInfo(`tmp/${projectId}.pdf`)
optimised.should.equal(true)
await comparePdf(projectId, exampleDir)
}
describe('Example Documents', function () {
Client.runFakeFilestoreService(fixturePath('examples'))
before(function (done) {
ClsiApp.ensureRunning(done)
before(async function () {
await ClsiApp.ensureRunning()
})
before(function (done) {
fs.rm(fixturePath('tmp'), { force: true, recursive: true }, done)
before(async function () {
await fsPromises.rm(fixturePath('tmp'), { force: true, recursive: true })
})
before(function (done) {
fs.mkdir(fixturePath('tmp'), done)
before(async function () {
await fsPromises.mkdir(fixturePath('tmp'))
})
after(function (done) {
fs.rm(fixturePath('tmp'), { force: true, recursive: true }, done)
after(async function () {
await fsPromises.rm(fixturePath('tmp'), { force: true, recursive: true })
})
return Array.from(fs.readdirSync(fixturePath('examples'))).map(exampleDir =>
return fs.readdirSync(fixturePath('examples')).map(exampleDir =>
(exampleDir =>
describe(exampleDir, function () {
before(function () {
return (this.project_id = Client.randomId() + '_' + exampleDir)
this.project_id = Client.randomId() + '_' + exampleDir
})
it('should generate the correct pdf', function (done) {
it('should generate the correct pdf', async function () {
this.timeout(MOCHA_LATEX_TIMEOUT)
return Client.compileDirectory(
const body = await 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
)
if (body?.compile?.status === 'failure') {
throw new Error('Compile failed')
}
const pdf = Client.getOutputFile(body, 'pdf')
await downloadAndComparePdf(this.project_id, exampleDir, pdf.url)
})
return it('should generate the correct pdf on the second run as well', function (done) {
it('should generate the correct pdf on the second run as well', async function () {
this.timeout(MOCHA_LATEX_TIMEOUT)
return Client.compileDirectory(
const body = await 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
)
if (body?.compile?.status === 'failure') {
throw new Error('Compile failed')
}
const pdf = Client.getOutputFile(body, 'pdf')
await downloadAndComparePdf(this.project_id, exampleDir, pdf.url)
})
}))(exampleDir)
)
})
function __guard__(value, transform) {
return typeof value !== 'undefined' && value !== null
? transform(value)
: undefined
}

View File

@@ -1,17 +1,10 @@
// 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 { fetchString, fetchNothing } = require('@overleaf/fetch-utils')
const ClsiApp = require('./helpers/ClsiApp')
const Settings = require('@overleaf/settings')
describe('Simple LaTeX file', function () {
before(function (done) {
before(async function () {
this.project_id = Client.randomId()
this.request = {
resources: [
@@ -30,62 +23,48 @@ Hello world
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()
}
)
})
await ClsiApp.ensureRunning()
try {
this.body = await Client.compile(this.project_id, this.request)
} catch (error) {
this.error = error
}
})
it('should return the PDF', function () {
const pdf = Client.getOutputFile(this.body, 'pdf')
return pdf.type.should.equal('pdf')
pdf.type.should.equal('pdf')
})
it('should return the log', function () {
const log = Client.getOutputFile(this.body, 'log')
return log.type.should.equal('log')
log.type.should.equal('log')
})
it('should provide the pdf for download', function (done) {
it('should provide the pdf for download', async function () {
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()
})
const response = await fetchNothing(pdf.url)
response.status.should.equal(200)
})
it('should provide the log for download', function (done) {
it('should provide the log for download', async function () {
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()
})
const response = await fetchNothing(log.url)
response.status.should.equal(200)
})
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()
})
it('should gather personalized metrics', async function () {
const body = await fetchString(`${Settings.apis.clsi.url}/metrics`)
body
.split('\n')
.some(line => {
return (
line.startsWith('compile') &&
line.includes('path="clsi-perf"') &&
line.includes('method="priority"')
)
})
.should.equal(true)
})
})

View File

@@ -1,16 +1,8 @@
const request = require('request')
const { fetchString } = require('@overleaf/fetch-utils')
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()
}
)
after(async function () {
const metrics = await fetchString(`${Settings.apis.clsi.url}/metrics`)
console.error('-- metrics --')
console.error(metrics)
console.error('-- metrics --')
})

View File

@@ -1,9 +1,12 @@
const { promisify } = require('node:util')
const Client = require('./helpers/Client')
const ClsiApp = require('./helpers/ClsiApp')
const { expect } = require('chai')
const sleep = promisify(setTimeout)
describe('Stop compile', function () {
before(function (done) {
before(async function () {
this.request = {
options: {
timeout: 100,
@@ -22,25 +25,35 @@ describe('Stop compile', function () {
],
}
this.project_id = Client.randomId()
ClsiApp.ensureRunning(() => {
// start the compile in the background
Client.compile(this.project_id, this.request, (error, res, body) => {
this.compileResult = { error, res, body }
await ClsiApp.ensureRunning()
// start the compile in the background
Client.compile(this.project_id, this.request)
.then(body => {
this.compileResult = { body }
})
// wait for 1 second before stopping the compile
setTimeout(() => {
Client.stopCompile(this.project_id, (error, res, body) => {
this.stopResult = { error, res, body }
setTimeout(done, 1000) // allow time for the compile request to terminate
})
}, 1000)
})
.catch(error => {
this.compileResult = { error }
})
// wait for 1 second before stopping the compile
await sleep(1000)
try {
const res = await Client.stopCompile(this.project_id)
this.stopResult = { res }
} catch (error) {
this.stopResult = { error }
}
// allow time for the compile request to terminate
await sleep(1000)
})
it('should force a compile response with an error status', function () {
expect(this.stopResult.error).to.be.null
expect(this.stopResult.res.statusCode).to.equal(204)
expect(this.compileResult.res.statusCode).to.equal(200)
expect(this.stopResult.error).not.to.exist
expect(this.stopResult.res.status).to.equal(204)
expect(this.compileResult.error).not.to.exist
expect(this.compileResult.body.compile.status).to.equal('terminated')
expect(this.compileResult.body.compile.error).to.equal('terminated')
})

View File

@@ -1,22 +1,9 @@
/* 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) {
before(async function () {
const content = `\
\\documentclass{article}
\\begin{document}
@@ -32,67 +19,45 @@ Hello world
],
}
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()
}
)
})
await ClsiApp.ensureRunning()
this.body = await Client.compile(this.project_id, this.request)
})
describe('from code to pdf', function () {
return it('should return the correct location', function (done) {
return Client.syncFromCode(
it('should return the correct location', async function () {
const pdfPositions = await 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,
},
],
downloadedFromCache: false,
})
return done()
}
5
)
expect(pdfPositions).to.deep.equal({
pdf: [
{
page: 1,
h: 133.768356,
v: 134.764618,
height: 6.918498,
width: 343.71106,
},
],
downloadedFromCache: false,
})
})
})
describe('from pdf to code', function () {
return it('should return the correct location', function (done) {
return Client.syncFromPdf(
it('should return the correct location', async function () {
const codePositions = await 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 }],
downloadedFromCache: false,
})
return done()
}
200
)
expect(codePositions).to.deep.equal({
code: [{ file: 'main.tex', line: 3, column: -1 }],
downloadedFromCache: false,
})
})
})
@@ -101,39 +66,29 @@ Hello world
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()
}
)
it('should return a 404 response', async function () {
const rejects = () =>
expect(Client.syncFromCode(this.other_project_id, 'main.tex', 3, 5))
.to.eventually.be.rejected
await rejects().and.have.property('info').to.contain({ status: 404 })
await rejects().and.have.property('body', 'Not Found')
})
})
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()
}
)
it('should return a 404 response', async function () {
const rejects = () =>
expect(Client.syncFromPdf(this.other_project_id, 1, 100, 200)).to
.eventually.be.rejected
await rejects().and.have.property('info').to.contain({ status: 404 })
await rejects().and.have.property('body', 'Not Found')
})
})
})
describe('when the synctex file is not available', function () {
before(function (done) {
before(async function () {
this.broken_project_id = Client.randomId()
const content = 'this is not valid tex' // not a valid tex file
this.request = {
@@ -144,46 +99,27 @@ Hello world
},
],
}
Client.compile(
this.broken_project_id,
this.request,
(error, res, body) => {
this.error = error
this.res = res
this.body = body
return done()
}
)
this.body = await Client.compile(this.broken_project_id, this.request)
})
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()
}
)
it('should return a 404 response', async function () {
const rejects = () =>
expect(Client.syncFromCode(this.broken_project_id, 'main.tex', 3, 5))
.to.eventually.be.rejected
await rejects().and.have.property('info').to.contain({ status: 404 })
await rejects().and.have.property('body', 'Not Found')
})
})
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()
}
)
it('should return a 404 response', async function () {
const rejects = () =>
expect(Client.syncFromPdf(this.broken_project_id, 1, 100, 200)).to
.eventually.be.rejected
await rejects().and.have.property('info').to.contain({ status: 404 })
await rejects().and.have.property('body', 'Not Found')
})
})
})

View File

@@ -1,20 +1,9 @@
/* 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) {
before(async function () {
this.request = {
options: {
timeout: 10,
@@ -33,34 +22,24 @@ describe('Timed out compile', function () {
],
}
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()
}
)
})
await ClsiApp.ensureRunning()
this.body = await Client.compile(this.project_id, this.request)
})
it('should return a timeout error', function () {
return this.body.compile.error.should.equal('container timed out')
this.body.compile.error.should.equal('container timed out')
})
it('should return a timedout status', function () {
return this.body.compile.status.should.equal('timedout')
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 () {
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')
outputFilePaths.should.include('output.log')
})
})

View File

@@ -1,20 +1,9 @@
/* 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 { fetchString } = require('@overleaf/fetch-utils')
const Settings = require('@overleaf/settings')
const Server = {
@@ -44,18 +33,18 @@ const Server = {
app.get('/project/:projectId/file/:fileId', (req, res, next) => {
this.getFile(req.url)
return res.send(`${req.params.projectId}:${req.params.fileId}`)
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]}`)
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)
staticServer(req, res, next)
})
Client.startFakeFilestoreApp(app)
@@ -72,7 +61,7 @@ describe('Url Caching', function () {
Server.run()
describe('Retries', function () {
before(function (done) {
before(async function () {
this.project_id = Client.randomId()
this.happyFile = `${Server.randomId()}/lion.png`
this.retryFileOnce = `fail/1/${Server.randomId()}`
@@ -110,14 +99,8 @@ describe('Url Caching', function () {
}
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()
})
})
await ClsiApp.ensureRunning()
this.body = await Client.compile(this.project_id, this.request)
})
after(function () {
@@ -139,7 +122,7 @@ describe('Url Caching', function () {
})
describe('Downloading an image for the first time', function () {
before(function (done) {
before(async function () {
this.project_id = Client.randomId()
this.file = `${Server.randomId()}/lion.png`
this.request = {
@@ -162,31 +145,21 @@ describe('Url Caching', function () {
}
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()
}
)
})
await ClsiApp.ensureRunning()
this.body = await Client.compile(this.project_id, this.request)
})
afterEach(function () {
return Server.getFile.restore()
Server.getFile.restore()
})
return it('should download the image', function () {
return Server.getFile.calledWith(`/${this.file}`).should.equal(true)
it('should download the image', function () {
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) {
before(async function () {
this.project_id = Client.randomId()
this.file = `${Server.randomId()}/lion.png`
this.request = {
@@ -209,54 +182,34 @@ describe('Url Caching', function () {
],
}
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()
}
)
}
)
await Client.compile(this.project_id, this.request)
sinon.spy(Server, 'getFile')
await Client.compile(this.project_id, this.request)
})
after(function () {
return Server.getFile.restore()
Server.getFile.restore()
})
it('should not download the image again', function () {
return Server.getFile.called.should.equal(false)
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()
})
it('should gather metrics', async function () {
const body = await fetchString(`${Settings.apis.clsi.url}/metrics`)
body
.split('\n')
.some(line => {
return (
line.startsWith('url_source') && line.includes('path="unknown"')
)
})
.should.equal(true)
})
})
describe('When an image is in the cache and the last modified date is advanced', function () {
before(function (done) {
before(async function () {
this.project_id = Client.randomId()
this.file = `${Server.randomId()}/lion.png`
this.request = {
@@ -279,40 +232,25 @@ describe('Url Caching', function () {
],
}
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()
}
)
}
)
await Client.compile(this.project_id, this.request)
sinon.spy(Server, 'getFile')
this.image_resource.modified = new Date(this.last_modified + 3000)
await Client.compile(this.project_id, this.request)
})
afterEach(function () {
return Server.getFile.restore()
Server.getFile.restore()
})
return it('should download the image again', function () {
return Server.getFile.called.should.equal(true)
it('should download the image again', function () {
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) {
before(async function () {
this.project_id = Client.randomId()
this.file = `${Server.randomId()}/lion.png`
this.request = {
@@ -335,40 +273,25 @@ describe('Url Caching', function () {
],
}
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()
}
)
}
)
await Client.compile(this.project_id, this.request)
sinon.spy(Server, 'getFile')
this.image_resource.modified = new Date(this.last_modified - 3000)
await Client.compile(this.project_id, this.request)
})
afterEach(function () {
return Server.getFile.restore()
Server.getFile.restore()
})
return it('should download the other revision', function () {
return Server.getFile.called.should.equal(true)
it('should download the other revision', function () {
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) {
before(async function () {
this.project_id = Client.randomId()
this.file = `${Server.randomId()}/lion.png`
this.request = {
@@ -391,40 +314,25 @@ describe('Url Caching', function () {
],
}
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()
}
)
}
)
await Client.compile(this.project_id, this.request)
sinon.spy(Server, 'getFile')
delete this.image_resource.modified
await Client.compile(this.project_id, this.request)
})
afterEach(function () {
return Server.getFile.restore()
Server.getFile.restore()
})
return it('should download the image again', function () {
return Server.getFile.called.should.equal(true)
it('should download the image again', function () {
Server.getFile.called.should.equal(true)
})
})
describe('After clearing the cache', function () {
before(function (done) {
before(async function () {
this.project_id = Client.randomId()
this.file = `${Server.randomId()}/lion.png`
this.request = {
@@ -447,41 +355,26 @@ describe('Url Caching', function () {
],
}
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()
}
)
})
})
await Client.compile(this.project_id, this.request)
await Client.clearCache(this.project_id)
sinon.spy(Server, 'getFile')
await Client.compile(this.project_id, this.request)
})
afterEach(function () {
return Server.getFile.restore()
Server.getFile.restore()
})
return it('should download the image again', function () {
return Server.getFile.called.should.equal(true)
it('should download the image again', function () {
Server.getFile.called.should.equal(true)
})
})
describe('fallbackURL', function () {
describe('when the primary resource is available', function () {
before(function (done) {
before(async function () {
this.project_id = Client.randomId()
this.file = `/project/${Server.randomId()}/file/${Server.randomId()}`
this.fallback = `/bucket/project-blobs/key/ab/cd/${Server.randomId()}`
@@ -506,22 +399,12 @@ describe('Url Caching', function () {
}
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()
}
)
})
await ClsiApp.ensureRunning()
await Client.compile(this.project_id, this.request)
})
after(function () {
return Server.getFile.restore()
Server.getFile.restore()
})
it('should download from the primary', function () {
@@ -531,25 +414,22 @@ describe('Url Caching', 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()
})
it('should gather metrics', async function () {
const body = await fetchString(`${Settings.apis.clsi.url}/metrics`)
body
.split('\n')
.some(line => {
return (
line.startsWith('url_source') &&
line.includes('path="user-files"')
)
})
.should.equal(true)
})
})
describe('when the primary resource is not available', function () {
before(function (done) {
before(async function () {
this.project_id = Client.randomId()
this.file = `/project/${Server.randomId()}/file/${Server.randomId()}`
this.fallback = `/bucket/project-blobs/key/ab/cd/${Server.randomId()}`
@@ -574,22 +454,12 @@ describe('Url Caching', function () {
}
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()
}
)
})
await ClsiApp.ensureRunning()
await Client.compile(this.project_id, this.request)
})
after(function () {
return Server.getFile.restore()
Server.getFile.restore()
})
it('should download from the fallback', function () {
@@ -597,20 +467,17 @@ describe('Url Caching', function () {
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()
})
it('should gather metrics', async function () {
const body = await fetchString(`${Settings.apis.clsi.url}/metrics`)
body
.split('\n')
.some(line => {
return (
line.startsWith('url_source') &&
line.includes('path="project-blobs"')
)
})
.should.equal(true)
})
})
})

View File

@@ -1,23 +1,11 @@
/* 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) {
before(async function () {
this.request = {
resources: [
{
@@ -30,41 +18,26 @@ describe('Syncing', function () {
],
}
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()
}
)
})
await ClsiApp.ensureRunning()
this.body = await Client.compile(this.project_id, this.request)
})
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()
describe('wordcount file', function () {
it('should return wordcount info', async function () {
const result = await Client.wordcount(this.project_id, 'main.tex')
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: '',
},
})
})
})

View File

@@ -1,258 +1,195 @@
/* 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 { fetchJson, fetchNothing } = require('@overleaf/fetch-utils')
const fs = require('node:fs')
const fsPromises = require('node:fs/promises')
const Settings = require('@overleaf/settings')
module.exports = Client = {
host: Settings.apis.clsi.url,
const 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
)
},
stopCompile(projectId, callback) {
if (callback == null) {
callback = function () {}
}
return request.post(
{ url: `${this.host}/project/${projectId}/compile/stop` },
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))
}
)
},
function randomId() {
return Math.random().toString(16).slice(2)
}
function compile(projectId, data) {
if (data) {
// Enable pdf caching unless disabled explicitly.
data.options = Object.assign({}, { enablePdfCaching: true }, data.options)
}
return fetchJson(`${host}/project/${projectId}/compile`, {
method: 'POST',
json: {
compile: data,
},
})
}
async function stopCompile(projectId) {
return await fetchNothing(`${host}/project/${projectId}/compile/stop`, {
method: 'POST',
})
}
async function clearCache(projectId) {
await fetchNothing(`${host}/project/${projectId}`, {
method: 'DELETE',
})
}
function getOutputFile(response, type) {
for (const file of response.compile.outputFiles) {
if (file.type === type && file.url.match(`output.${type}`)) {
return file
}
}
return null
}
function runFakeFilestoreService(directory) {
const app = express()
app.use(express.static(directory))
this.startFakeFilestoreApp(app)
}
function 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)
})
}
function syncFromCode(projectId, file, line, column) {
return syncFromCodeWithImage(projectId, file, line, column, '')
}
async function syncFromCodeWithImage(projectId, file, line, column, imageName) {
const url = new URL(`${host}/project/${projectId}/sync/code`)
url.searchParams.append('imageName', imageName)
url.searchParams.append('file', file)
url.searchParams.append('line', line)
url.searchParams.append('column', column)
return await fetchJson(url)
}
function syncFromPdf(projectId, page, h, v) {
return syncFromPdfWithImage(projectId, page, h, v, '')
}
function syncFromPdfWithImage(projectId, page, h, v, imageName) {
const url = new URL(`${host}/project/${projectId}/sync/pdf`)
url.searchParams.append('imageName', imageName)
url.searchParams.append('page', page)
url.searchParams.append('h', h)
url.searchParams.append('v', v)
return fetchJson(url)
}
function wordcount(projectId, file) {
const image = undefined
return wordcountWithImage(projectId, file, image)
}
async function wordcountWithImage(projectId, file, image) {
const url = new URL(`${host}/project/${projectId}/wordcount`)
if (image) {
url.searchParams.append('image', image)
}
url.searchParams.append('file', file)
return await fetchJson(url)
}
async function compileDirectory(projectId, baseDirectory, directory) {
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,
})
}
}
}
const req = {
resources,
rootResourcePath,
}
try {
const options = await fsPromises.readFile(
`${baseDirectory}/${directory}/options.json`
)
req.options = JSON.parse(options)
} catch (error) {
// noop
}
return await compile(projectId, req)
}
module.exports = {
randomId,
compile,
stopCompile,
clearCache,
getOutputFile,
runFakeFilestoreService,
startFakeFilestoreApp,
syncFromCode,
syncFromCodeWithImage,
syncFromPdf,
syncFromPdfWithImage,
compileDirectory,
wordcount,
wordcountWithImage,
}

View File

@@ -1,50 +1,31 @@
// 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
})()
function startApp() {
return new Promise((resolve, reject) => {
app.listen(
Settings.internal.clsi.port,
Settings.internal.clsi.host,
error => {
if (error) {
reject(error)
} else {
resolve()
}
)
}
},
}
)
})
}
let appStartedPromise
async function ensureRunning() {
if (!appStartedPromise) {
appStartedPromise = startApp()
}
await appStartedPromise
}
module.exports = {
ensureRunning,
}