first commit
This commit is contained in:
497
services/clsi/test/unit/js/CompileControllerTests.js
Normal file
497
services/clsi/test/unit/js/CompileControllerTests.js
Normal file
@@ -0,0 +1,497 @@
|
||||
const SandboxedModule = require('sandboxed-module')
|
||||
const sinon = require('sinon')
|
||||
const modulePath = require('node:path').join(
|
||||
__dirname,
|
||||
'../../../app/js/CompileController'
|
||||
)
|
||||
const Errors = require('../../../app/js/Errors')
|
||||
|
||||
describe('CompileController', function () {
|
||||
beforeEach(function () {
|
||||
this.buildId = 'build-id-123'
|
||||
this.CompileController = SandboxedModule.require(modulePath, {
|
||||
requires: {
|
||||
'./CompileManager': (this.CompileManager = {}),
|
||||
'./RequestParser': (this.RequestParser = {}),
|
||||
'@overleaf/settings': (this.Settings = {
|
||||
apis: {
|
||||
clsi: {
|
||||
url: 'http://clsi.example.com',
|
||||
outputUrlPrefix: '/zone/b',
|
||||
downloadHost: 'http://localhost:3013',
|
||||
},
|
||||
clsiCache: {
|
||||
enabled: false,
|
||||
url: 'http://localhost:3044',
|
||||
},
|
||||
},
|
||||
}),
|
||||
'@overleaf/metrics': {
|
||||
Timer: sinon.stub().returns({ done: sinon.stub() }),
|
||||
},
|
||||
'./ProjectPersistenceManager': (this.ProjectPersistenceManager = {}),
|
||||
'./CLSICacheHandler': {
|
||||
notifyCLSICacheAboutBuild: sinon.stub(),
|
||||
downloadLatestCompileCache: sinon.stub().resolves(),
|
||||
downloadOutputDotSynctexFromCompileCache: sinon.stub().resolves(),
|
||||
},
|
||||
'./Errors': (this.Erros = Errors),
|
||||
},
|
||||
})
|
||||
this.Settings.externalUrl = 'http://www.example.com'
|
||||
this.req = {}
|
||||
this.res = {}
|
||||
this.next = sinon.stub()
|
||||
})
|
||||
|
||||
describe('compile', function () {
|
||||
beforeEach(function () {
|
||||
this.req.body = {
|
||||
compile: 'mock-body',
|
||||
}
|
||||
this.req.params = { project_id: (this.project_id = 'project-id-123') }
|
||||
this.request = {
|
||||
compile: 'mock-parsed-request',
|
||||
}
|
||||
this.request_with_project_id = {
|
||||
compile: this.request.compile,
|
||||
project_id: this.project_id,
|
||||
}
|
||||
this.output_files = [
|
||||
{
|
||||
path: 'output.pdf',
|
||||
type: 'pdf',
|
||||
size: 1337,
|
||||
build: 1234,
|
||||
},
|
||||
{
|
||||
path: 'output.log',
|
||||
type: 'log',
|
||||
build: 1234,
|
||||
},
|
||||
]
|
||||
this.RequestParser.parse = sinon
|
||||
.stub()
|
||||
.callsArgWith(1, null, this.request)
|
||||
this.ProjectPersistenceManager.markProjectAsJustAccessed = sinon
|
||||
.stub()
|
||||
.callsArg(1)
|
||||
this.stats = { foo: 1 }
|
||||
this.timings = { bar: 2 }
|
||||
this.res.status = sinon.stub().returnsThis()
|
||||
this.res.send = sinon.stub()
|
||||
|
||||
this.CompileManager.doCompileWithLock = sinon
|
||||
.stub()
|
||||
.callsFake((_req, stats, timings, cb) => {
|
||||
Object.assign(stats, this.stats)
|
||||
Object.assign(timings, this.timings)
|
||||
cb(null, {
|
||||
outputFiles: this.output_files,
|
||||
buildId: this.buildId,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('successfully', function () {
|
||||
beforeEach(function () {
|
||||
this.CompileController.compile(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should parse the request', function () {
|
||||
this.RequestParser.parse.calledWith(this.req.body).should.equal(true)
|
||||
})
|
||||
|
||||
it('should run the compile for the specified project', function () {
|
||||
this.CompileManager.doCompileWithLock
|
||||
.calledWith(this.request_with_project_id)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should mark the project as accessed', function () {
|
||||
this.ProjectPersistenceManager.markProjectAsJustAccessed
|
||||
.calledWith(this.project_id)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should return the JSON response', function () {
|
||||
this.res.status.calledWith(200).should.equal(true)
|
||||
this.res.send
|
||||
.calledWith({
|
||||
compile: {
|
||||
status: 'success',
|
||||
error: null,
|
||||
stats: this.stats,
|
||||
timings: this.timings,
|
||||
buildId: this.buildId,
|
||||
outputUrlPrefix: '/zone/b',
|
||||
outputFiles: this.output_files.map(file => ({
|
||||
url: `${this.Settings.apis.clsi.url}/project/${this.project_id}/build/${file.build}/output/${file.path}`,
|
||||
...file,
|
||||
})),
|
||||
},
|
||||
})
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('without a outputUrlPrefix', function () {
|
||||
beforeEach(function () {
|
||||
this.Settings.apis.clsi.outputUrlPrefix = ''
|
||||
this.CompileController.compile(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should return the JSON response with empty outputUrlPrefix', function () {
|
||||
this.res.status.calledWith(200).should.equal(true)
|
||||
this.res.send
|
||||
.calledWith({
|
||||
compile: {
|
||||
status: 'success',
|
||||
error: null,
|
||||
stats: this.stats,
|
||||
timings: this.timings,
|
||||
buildId: this.buildId,
|
||||
outputUrlPrefix: '',
|
||||
outputFiles: this.output_files.map(file => ({
|
||||
url: `${this.Settings.apis.clsi.url}/project/${this.project_id}/build/${file.build}/output/${file.path}`,
|
||||
...file,
|
||||
})),
|
||||
},
|
||||
})
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with user provided fake_output.pdf', function () {
|
||||
beforeEach(function () {
|
||||
this.output_files = [
|
||||
{
|
||||
path: 'fake_output.pdf',
|
||||
type: 'pdf',
|
||||
build: 1234,
|
||||
},
|
||||
{
|
||||
path: 'output.log',
|
||||
type: 'log',
|
||||
build: 1234,
|
||||
},
|
||||
]
|
||||
this.CompileManager.doCompileWithLock = sinon
|
||||
.stub()
|
||||
.callsFake((_req, stats, timings, cb) => {
|
||||
Object.assign(stats, this.stats)
|
||||
Object.assign(timings, this.timings)
|
||||
cb(null, {
|
||||
outputFiles: this.output_files,
|
||||
buildId: this.buildId,
|
||||
})
|
||||
})
|
||||
this.CompileController.compile(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should return the JSON response with status failure', function () {
|
||||
this.res.status.calledWith(200).should.equal(true)
|
||||
this.res.send.should.have.been.calledWith({
|
||||
compile: {
|
||||
status: 'failure',
|
||||
error: null,
|
||||
stats: this.stats,
|
||||
timings: this.timings,
|
||||
outputUrlPrefix: '/zone/b',
|
||||
buildId: this.buildId,
|
||||
outputFiles: this.output_files.map(file => ({
|
||||
url: `${this.Settings.apis.clsi.url}/project/${this.project_id}/build/${file.build}/output/${file.path}`,
|
||||
...file,
|
||||
})),
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('with an empty output.pdf', function () {
|
||||
beforeEach(function () {
|
||||
this.output_files = [
|
||||
{
|
||||
path: 'output.pdf',
|
||||
type: 'pdf',
|
||||
size: 0,
|
||||
build: 1234,
|
||||
},
|
||||
{
|
||||
path: 'output.log',
|
||||
type: 'log',
|
||||
build: 1234,
|
||||
},
|
||||
]
|
||||
this.CompileManager.doCompileWithLock = sinon
|
||||
.stub()
|
||||
.callsFake((_req, stats, timings, cb) => {
|
||||
Object.assign(stats, this.stats)
|
||||
Object.assign(timings, this.timings)
|
||||
cb(null, {
|
||||
outputFiles: this.output_files,
|
||||
buildId: this.buildId,
|
||||
})
|
||||
})
|
||||
this.CompileController.compile(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should return the JSON response with status failure', function () {
|
||||
this.res.status.calledWith(200).should.equal(true)
|
||||
this.res.send.should.have.been.calledWith({
|
||||
compile: {
|
||||
status: 'failure',
|
||||
error: null,
|
||||
stats: this.stats,
|
||||
buildId: this.buildId,
|
||||
timings: this.timings,
|
||||
outputUrlPrefix: '/zone/b',
|
||||
outputFiles: this.output_files.map(file => ({
|
||||
url: `${this.Settings.apis.clsi.url}/project/${this.project_id}/build/${file.build}/output/${file.path}`,
|
||||
...file,
|
||||
})),
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('with an error', function () {
|
||||
beforeEach(function () {
|
||||
const error = new Error((this.message = 'error message'))
|
||||
error.buildId = this.buildId
|
||||
this.CompileManager.doCompileWithLock = sinon
|
||||
.stub()
|
||||
.callsFake((_req, stats, timings, cb) => {
|
||||
Object.assign(stats, this.stats)
|
||||
Object.assign(timings, this.timings)
|
||||
cb(error)
|
||||
})
|
||||
this.CompileController.compile(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should return the JSON response with the error', function () {
|
||||
this.res.status.calledWith(500).should.equal(true)
|
||||
this.res.send
|
||||
.calledWith({
|
||||
compile: {
|
||||
status: 'error',
|
||||
error: this.message,
|
||||
outputUrlPrefix: '/zone/b',
|
||||
outputFiles: [],
|
||||
buildId: this.buildId,
|
||||
stats: this.stats,
|
||||
timings: this.timings,
|
||||
},
|
||||
})
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with too many compile requests error', function () {
|
||||
beforeEach(function () {
|
||||
const error = new Errors.TooManyCompileRequestsError(
|
||||
'too many concurrent compile requests'
|
||||
)
|
||||
this.CompileManager.doCompileWithLock = sinon
|
||||
.stub()
|
||||
.callsFake((_req, stats, timings, cb) => {
|
||||
Object.assign(stats, this.stats)
|
||||
Object.assign(timings, this.timings)
|
||||
cb(error)
|
||||
})
|
||||
this.CompileController.compile(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should return the JSON response with the error', function () {
|
||||
this.res.status.calledWith(503).should.equal(true)
|
||||
this.res.send
|
||||
.calledWith({
|
||||
compile: {
|
||||
status: 'unavailable',
|
||||
error: 'too many concurrent compile requests',
|
||||
outputUrlPrefix: '/zone/b',
|
||||
outputFiles: [],
|
||||
stats: this.stats,
|
||||
timings: this.timings,
|
||||
// JSON.stringify will omit these undefined values
|
||||
buildId: undefined,
|
||||
},
|
||||
})
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the request times out', function () {
|
||||
beforeEach(function () {
|
||||
this.error = new Error((this.message = 'container timed out'))
|
||||
this.error.timedout = true
|
||||
this.CompileManager.doCompileWithLock = sinon
|
||||
.stub()
|
||||
.callsFake((_req, stats, timings, cb) => {
|
||||
Object.assign(stats, this.stats)
|
||||
Object.assign(timings, this.timings)
|
||||
cb(this.error)
|
||||
})
|
||||
this.CompileController.compile(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should return the JSON response with the timeout status', function () {
|
||||
this.res.status.calledWith(200).should.equal(true)
|
||||
this.res.send
|
||||
.calledWith({
|
||||
compile: {
|
||||
status: 'timedout',
|
||||
error: this.message,
|
||||
outputUrlPrefix: '/zone/b',
|
||||
outputFiles: [],
|
||||
stats: this.stats,
|
||||
timings: this.timings,
|
||||
// JSON.stringify will omit these undefined values
|
||||
buildId: undefined,
|
||||
},
|
||||
})
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the request returns no output files', function () {
|
||||
beforeEach(function () {
|
||||
this.CompileManager.doCompileWithLock = sinon
|
||||
.stub()
|
||||
.callsFake((_req, stats, timings, cb) => {
|
||||
Object.assign(stats, this.stats)
|
||||
Object.assign(timings, this.timings)
|
||||
cb(null, {})
|
||||
})
|
||||
this.CompileController.compile(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should return the JSON response with the failure status', function () {
|
||||
this.res.status.calledWith(200).should.equal(true)
|
||||
this.res.send
|
||||
.calledWith({
|
||||
compile: {
|
||||
error: null,
|
||||
status: 'failure',
|
||||
outputUrlPrefix: '/zone/b',
|
||||
outputFiles: [],
|
||||
stats: this.stats,
|
||||
timings: this.timings,
|
||||
// JSON.stringify will omit these undefined values
|
||||
buildId: undefined,
|
||||
},
|
||||
})
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('syncFromCode', function () {
|
||||
beforeEach(function () {
|
||||
this.file = 'main.tex'
|
||||
this.line = 42
|
||||
this.column = 5
|
||||
this.project_id = 'mock-project-id'
|
||||
this.req.params = { project_id: this.project_id }
|
||||
this.req.query = {
|
||||
file: this.file,
|
||||
line: this.line.toString(),
|
||||
column: this.column.toString(),
|
||||
}
|
||||
this.res.json = sinon.stub()
|
||||
|
||||
this.CompileManager.syncFromCode = sinon
|
||||
.stub()
|
||||
.yields(null, (this.pdfPositions = ['mock-positions']))
|
||||
this.CompileController.syncFromCode(this.req, this.res, this.next)
|
||||
})
|
||||
|
||||
it('should find the corresponding location in the PDF', function () {
|
||||
this.CompileManager.syncFromCode
|
||||
.calledWith(
|
||||
this.project_id,
|
||||
undefined,
|
||||
this.file,
|
||||
this.line,
|
||||
this.column
|
||||
)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should return the positions', function () {
|
||||
this.res.json
|
||||
.calledWith({
|
||||
pdf: this.pdfPositions,
|
||||
})
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('syncFromPdf', function () {
|
||||
beforeEach(function () {
|
||||
this.page = 5
|
||||
this.h = 100.23
|
||||
this.v = 45.67
|
||||
this.project_id = 'mock-project-id'
|
||||
this.req.params = { project_id: this.project_id }
|
||||
this.req.query = {
|
||||
page: this.page.toString(),
|
||||
h: this.h.toString(),
|
||||
v: this.v.toString(),
|
||||
}
|
||||
this.res.json = sinon.stub()
|
||||
|
||||
this.CompileManager.syncFromPdf = sinon
|
||||
.stub()
|
||||
.yields(null, (this.codePositions = ['mock-positions']))
|
||||
this.CompileController.syncFromPdf(this.req, this.res, this.next)
|
||||
})
|
||||
|
||||
it('should find the corresponding location in the code', function () {
|
||||
this.CompileManager.syncFromPdf
|
||||
.calledWith(this.project_id, undefined, this.page, this.h, this.v)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should return the positions', function () {
|
||||
this.res.json
|
||||
.calledWith({
|
||||
code: this.codePositions,
|
||||
})
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('wordcount', function () {
|
||||
beforeEach(function () {
|
||||
this.file = 'main.tex'
|
||||
this.project_id = 'mock-project-id'
|
||||
this.req.params = { project_id: this.project_id }
|
||||
this.req.query = {
|
||||
file: this.file,
|
||||
image: (this.image = 'example.com/image'),
|
||||
}
|
||||
this.res.json = sinon.stub()
|
||||
|
||||
this.CompileManager.wordcount = sinon
|
||||
.stub()
|
||||
.callsArgWith(4, null, (this.texcount = ['mock-texcount']))
|
||||
})
|
||||
|
||||
it('should return the word count of a file', function () {
|
||||
this.CompileController.wordcount(this.req, this.res, this.next)
|
||||
this.CompileManager.wordcount
|
||||
.calledWith(this.project_id, undefined, this.file, this.image)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should return the texcount info', function () {
|
||||
this.CompileController.wordcount(this.req, this.res, this.next)
|
||||
this.res.json
|
||||
.calledWith({
|
||||
texcount: this.texcount,
|
||||
})
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
})
|
627
services/clsi/test/unit/js/CompileManagerTests.js
Normal file
627
services/clsi/test/unit/js/CompileManagerTests.js
Normal file
@@ -0,0 +1,627 @@
|
||||
const Path = require('node:path')
|
||||
const SandboxedModule = require('sandboxed-module')
|
||||
const { expect } = require('chai')
|
||||
const sinon = require('sinon')
|
||||
|
||||
const MODULE_PATH = require('node:path').join(
|
||||
__dirname,
|
||||
'../../../app/js/CompileManager'
|
||||
)
|
||||
|
||||
describe('CompileManager', function () {
|
||||
beforeEach(function () {
|
||||
this.projectId = 'project-id-123'
|
||||
this.userId = '1234'
|
||||
this.resources = 'mock-resources'
|
||||
this.outputFiles = [
|
||||
{
|
||||
path: 'output.log',
|
||||
type: 'log',
|
||||
},
|
||||
{
|
||||
path: 'output.pdf',
|
||||
type: 'pdf',
|
||||
},
|
||||
]
|
||||
this.buildFiles = [
|
||||
{
|
||||
path: 'output.log',
|
||||
type: 'log',
|
||||
build: 1234,
|
||||
},
|
||||
{
|
||||
path: 'output.pdf',
|
||||
type: 'pdf',
|
||||
build: 1234,
|
||||
},
|
||||
]
|
||||
this.buildId = 'build-id-123'
|
||||
this.commandOutput = 'Dummy output'
|
||||
this.compileBaseDir = '/compile/dir'
|
||||
this.outputBaseDir = '/output/dir'
|
||||
this.compileDir = `${this.compileBaseDir}/${this.projectId}-${this.userId}`
|
||||
this.outputDir = `${this.outputBaseDir}/${this.projectId}-${this.userId}`
|
||||
|
||||
this.LatexRunner = {
|
||||
promises: {
|
||||
runLatex: sinon.stub().resolves({}),
|
||||
},
|
||||
}
|
||||
this.ResourceWriter = {
|
||||
promises: {
|
||||
syncResourcesToDisk: sinon.stub().resolves(this.resources),
|
||||
},
|
||||
}
|
||||
this.OutputFileFinder = {
|
||||
promises: {
|
||||
findOutputFiles: sinon.stub().resolves({
|
||||
outputFiles: this.outputFiles,
|
||||
allEntries: this.outputFiles.map(f => f.path).concat(['main.tex']),
|
||||
}),
|
||||
},
|
||||
}
|
||||
this.OutputCacheManager = {
|
||||
promises: {
|
||||
queueDirOperation: sinon.stub().callsArg(1),
|
||||
saveOutputFiles: sinon
|
||||
.stub()
|
||||
.resolves({ outputFiles: this.buildFiles, buildId: this.buildId }),
|
||||
},
|
||||
}
|
||||
this.Settings = {
|
||||
path: {
|
||||
compilesDir: this.compileBaseDir,
|
||||
outputDir: this.outputBaseDir,
|
||||
synctexBaseDir: sinon.stub(),
|
||||
},
|
||||
clsi: {
|
||||
docker: {
|
||||
image: 'SOMEIMAGE',
|
||||
},
|
||||
},
|
||||
}
|
||||
this.Settings.path.synctexBaseDir
|
||||
.withArgs(`${this.projectId}-${this.userId}`)
|
||||
.returns(this.compileDir)
|
||||
this.child_process = {
|
||||
exec: sinon.stub(),
|
||||
execFile: sinon.stub().yields(),
|
||||
}
|
||||
this.CommandRunner = {
|
||||
promises: {
|
||||
run: sinon.stub().callsFake((_1, _2, _3, _4, _5, _6, compileGroup) => {
|
||||
if (compileGroup === 'synctex') {
|
||||
return Promise.resolve({ stdout: this.commandOutput })
|
||||
} else {
|
||||
return Promise.resolve({
|
||||
stdout: 'Encoding: ascii\nWords in text: 2',
|
||||
})
|
||||
}
|
||||
}),
|
||||
},
|
||||
}
|
||||
this.DraftModeManager = {
|
||||
promises: {
|
||||
injectDraftMode: sinon.stub().resolves(),
|
||||
},
|
||||
}
|
||||
this.TikzManager = {
|
||||
promises: {
|
||||
checkMainFile: sinon.stub().resolves(false),
|
||||
},
|
||||
}
|
||||
this.lock = {
|
||||
release: sinon.stub(),
|
||||
}
|
||||
this.LockManager = {
|
||||
acquire: sinon.stub().returns(this.lock),
|
||||
}
|
||||
this.SynctexOutputParser = {
|
||||
parseViewOutput: sinon.stub(),
|
||||
parseEditOutput: sinon.stub(),
|
||||
}
|
||||
|
||||
this.dirStats = {
|
||||
isDirectory: sinon.stub().returns(true),
|
||||
}
|
||||
this.fileStats = {
|
||||
isFile: sinon.stub().returns(true),
|
||||
}
|
||||
this.fsPromises = {
|
||||
lstat: sinon.stub(),
|
||||
stat: sinon.stub(),
|
||||
readFile: sinon.stub(),
|
||||
mkdir: sinon.stub().resolves(),
|
||||
rm: sinon.stub().resolves(),
|
||||
unlink: sinon.stub().resolves(),
|
||||
rmdir: sinon.stub().resolves(),
|
||||
}
|
||||
this.fsPromises.lstat.withArgs(this.compileDir).resolves(this.dirStats)
|
||||
this.fsPromises.stat
|
||||
.withArgs(Path.join(this.compileDir, 'output.synctex.gz'))
|
||||
.resolves(this.fileStats)
|
||||
|
||||
this.CompileManager = SandboxedModule.require(MODULE_PATH, {
|
||||
requires: {
|
||||
'./LatexRunner': this.LatexRunner,
|
||||
'./ResourceWriter': this.ResourceWriter,
|
||||
'./OutputFileFinder': this.OutputFileFinder,
|
||||
'./OutputCacheManager': this.OutputCacheManager,
|
||||
'@overleaf/settings': this.Settings,
|
||||
'@overleaf/metrics': {
|
||||
inc: sinon.stub(),
|
||||
timing: sinon.stub(),
|
||||
gauge: sinon.stub(),
|
||||
Timer: sinon.stub().returns({ done: sinon.stub() }),
|
||||
},
|
||||
child_process: this.child_process,
|
||||
'./CommandRunner': this.CommandRunner,
|
||||
'./DraftModeManager': this.DraftModeManager,
|
||||
'./TikzManager': this.TikzManager,
|
||||
'./LockManager': this.LockManager,
|
||||
'./SynctexOutputParser': this.SynctexOutputParser,
|
||||
'fs/promises': this.fsPromises,
|
||||
'./CLSICacheHandler': {
|
||||
notifyCLSICacheAboutBuild: sinon.stub(),
|
||||
downloadLatestCompileCache: sinon.stub().resolves(),
|
||||
downloadOutputDotSynctexFromCompileCache: sinon.stub().resolves(),
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
describe('doCompileWithLock', function () {
|
||||
beforeEach(function () {
|
||||
this.request = {
|
||||
resources: this.resources,
|
||||
rootResourcePath: (this.rootResourcePath = 'main.tex'),
|
||||
project_id: this.projectId,
|
||||
user_id: this.userId,
|
||||
compiler: (this.compiler = 'pdflatex'),
|
||||
timeout: (this.timeout = 42000),
|
||||
imageName: (this.image = 'example.com/image'),
|
||||
flags: (this.flags = ['-file-line-error']),
|
||||
compileGroup: (this.compileGroup = 'compile-group'),
|
||||
stopOnFirstError: false,
|
||||
metricsOpts: {
|
||||
path: 'clsi-perf',
|
||||
method: 'minimal',
|
||||
compile: 'initial',
|
||||
},
|
||||
}
|
||||
this.env = {
|
||||
OVERLEAF_PROJECT_ID: this.projectId,
|
||||
}
|
||||
})
|
||||
|
||||
describe('when the project is locked', function () {
|
||||
beforeEach(async function () {
|
||||
const error = new Error('locked')
|
||||
this.LockManager.acquire.throws(error)
|
||||
await expect(
|
||||
this.CompileManager.promises.doCompileWithLock(this.request, {}, {})
|
||||
).to.be.rejectedWith(error)
|
||||
})
|
||||
|
||||
it('should ensure that the compile directory exists', function () {
|
||||
expect(this.fsPromises.mkdir).to.have.been.calledWith(this.compileDir, {
|
||||
recursive: true,
|
||||
})
|
||||
})
|
||||
|
||||
it('should not run LaTeX', function () {
|
||||
expect(this.LatexRunner.promises.runLatex).not.to.have.been.called
|
||||
})
|
||||
})
|
||||
|
||||
describe('normally', function () {
|
||||
beforeEach(async function () {
|
||||
this.result = await this.CompileManager.promises.doCompileWithLock(
|
||||
this.request,
|
||||
{},
|
||||
{}
|
||||
)
|
||||
})
|
||||
|
||||
it('should ensure that the compile directory exists', function () {
|
||||
expect(this.fsPromises.mkdir).to.have.been.calledWith(this.compileDir, {
|
||||
recursive: true,
|
||||
})
|
||||
})
|
||||
|
||||
it('should write the resources to disk', function () {
|
||||
expect(
|
||||
this.ResourceWriter.promises.syncResourcesToDisk
|
||||
).to.have.been.calledWith(this.request, this.compileDir)
|
||||
})
|
||||
|
||||
it('should run LaTeX', function () {
|
||||
expect(this.LatexRunner.promises.runLatex).to.have.been.calledWith(
|
||||
`${this.projectId}-${this.userId}`,
|
||||
{
|
||||
directory: this.compileDir,
|
||||
mainFile: this.rootResourcePath,
|
||||
compiler: this.compiler,
|
||||
timeout: this.timeout,
|
||||
image: this.image,
|
||||
flags: this.flags,
|
||||
environment: this.env,
|
||||
compileGroup: this.compileGroup,
|
||||
stopOnFirstError: this.request.stopOnFirstError,
|
||||
stats: sinon.match.object,
|
||||
timings: sinon.match.object,
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('should find the output files', function () {
|
||||
expect(
|
||||
this.OutputFileFinder.promises.findOutputFiles
|
||||
).to.have.been.calledWith(this.resources, this.compileDir)
|
||||
})
|
||||
|
||||
it('should return the output files', function () {
|
||||
expect(this.result.outputFiles).to.equal(this.buildFiles)
|
||||
})
|
||||
|
||||
it('should not inject draft mode by default', function () {
|
||||
expect(this.DraftModeManager.promises.injectDraftMode).not.to.have.been
|
||||
.called
|
||||
})
|
||||
})
|
||||
|
||||
describe('with draft mode', function () {
|
||||
beforeEach(async function () {
|
||||
this.request.draft = true
|
||||
await this.CompileManager.promises.doCompileWithLock(
|
||||
this.request,
|
||||
{},
|
||||
{}
|
||||
)
|
||||
})
|
||||
|
||||
it('should inject the draft mode header', function () {
|
||||
expect(
|
||||
this.DraftModeManager.promises.injectDraftMode
|
||||
).to.have.been.calledWith(this.compileDir + '/' + this.rootResourcePath)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a check option', function () {
|
||||
beforeEach(async function () {
|
||||
this.request.check = 'error'
|
||||
await this.CompileManager.promises.doCompileWithLock(
|
||||
this.request,
|
||||
{},
|
||||
{}
|
||||
)
|
||||
})
|
||||
|
||||
it('should run chktex', function () {
|
||||
expect(this.LatexRunner.promises.runLatex).to.have.been.calledWith(
|
||||
`${this.projectId}-${this.userId}`,
|
||||
{
|
||||
directory: this.compileDir,
|
||||
mainFile: this.rootResourcePath,
|
||||
compiler: this.compiler,
|
||||
timeout: this.timeout,
|
||||
image: this.image,
|
||||
flags: this.flags,
|
||||
environment: {
|
||||
CHKTEX_OPTIONS: '-nall -e9 -e10 -w15 -w16',
|
||||
CHKTEX_EXIT_ON_ERROR: 1,
|
||||
CHKTEX_ULIMIT_OPTIONS: '-t 5 -v 64000',
|
||||
OVERLEAF_PROJECT_ID: this.projectId,
|
||||
},
|
||||
compileGroup: this.compileGroup,
|
||||
stopOnFirstError: this.request.stopOnFirstError,
|
||||
stats: sinon.match.object,
|
||||
timings: sinon.match.object,
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a knitr file and check options', function () {
|
||||
beforeEach(async function () {
|
||||
this.request.rootResourcePath = 'main.Rtex'
|
||||
this.request.check = 'error'
|
||||
await this.CompileManager.promises.doCompileWithLock(
|
||||
this.request,
|
||||
{},
|
||||
{}
|
||||
)
|
||||
})
|
||||
|
||||
it('should not run chktex', function () {
|
||||
expect(this.LatexRunner.promises.runLatex).to.have.been.calledWith(
|
||||
`${this.projectId}-${this.userId}`,
|
||||
{
|
||||
directory: this.compileDir,
|
||||
mainFile: 'main.Rtex',
|
||||
compiler: this.compiler,
|
||||
timeout: this.timeout,
|
||||
image: this.image,
|
||||
flags: this.flags,
|
||||
environment: this.env,
|
||||
compileGroup: this.compileGroup,
|
||||
stopOnFirstError: this.request.stopOnFirstError,
|
||||
stats: sinon.match.object,
|
||||
timings: sinon.match.object,
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the compile times out', function () {
|
||||
beforeEach(async function () {
|
||||
const error = new Error('timed out!')
|
||||
error.timedout = true
|
||||
this.LatexRunner.promises.runLatex.rejects(error)
|
||||
await expect(
|
||||
this.CompileManager.promises.doCompileWithLock(this.request, {}, {})
|
||||
).to.be.rejected
|
||||
})
|
||||
|
||||
it('should clear the compile directory', function () {
|
||||
for (const { path } of this.buildFiles) {
|
||||
expect(this.fsPromises.unlink).to.have.been.calledWith(
|
||||
this.compileDir + '/' + path
|
||||
)
|
||||
}
|
||||
expect(this.fsPromises.unlink).to.have.been.calledWith(
|
||||
this.compileDir + '/main.tex'
|
||||
)
|
||||
expect(this.fsPromises.rmdir).to.have.been.calledWith(this.compileDir)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the compile is manually stopped', function () {
|
||||
beforeEach(async function () {
|
||||
const error = new Error('terminated!')
|
||||
error.terminated = true
|
||||
this.LatexRunner.promises.runLatex.rejects(error)
|
||||
await expect(
|
||||
this.CompileManager.promises.doCompileWithLock(this.request, {}, {})
|
||||
).to.be.rejected
|
||||
})
|
||||
|
||||
it('should clear the compile directory', function () {
|
||||
for (const { path } of this.buildFiles) {
|
||||
expect(this.fsPromises.unlink).to.have.been.calledWith(
|
||||
this.compileDir + '/' + path
|
||||
)
|
||||
}
|
||||
expect(this.fsPromises.unlink).to.have.been.calledWith(
|
||||
this.compileDir + '/main.tex'
|
||||
)
|
||||
expect(this.fsPromises.rmdir).to.have.been.calledWith(this.compileDir)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('clearProject', function () {
|
||||
it('should clear the compile directory', async function () {
|
||||
await this.CompileManager.promises.clearProject(
|
||||
this.projectId,
|
||||
this.userId
|
||||
)
|
||||
|
||||
expect(this.fsPromises.rm).to.have.been.calledWith(this.compileDir, {
|
||||
force: true,
|
||||
recursive: true,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('syncing', function () {
|
||||
beforeEach(function () {
|
||||
this.page = 1
|
||||
this.h = 42.23
|
||||
this.v = 87.56
|
||||
this.width = 100.01
|
||||
this.height = 234.56
|
||||
this.line = 5
|
||||
this.column = 3
|
||||
this.filename = 'main.tex'
|
||||
})
|
||||
|
||||
describe('syncFromCode', function () {
|
||||
beforeEach(function () {
|
||||
this.records = [{ page: 1, h: 2, v: 3, width: 4, height: 5 }]
|
||||
this.SynctexOutputParser.parseViewOutput
|
||||
.withArgs(this.commandOutput)
|
||||
.returns(this.records)
|
||||
})
|
||||
|
||||
describe('normal case', function () {
|
||||
beforeEach(async function () {
|
||||
this.result = await this.CompileManager.promises.syncFromCode(
|
||||
this.projectId,
|
||||
this.userId,
|
||||
this.filename,
|
||||
this.line,
|
||||
this.column,
|
||||
''
|
||||
)
|
||||
})
|
||||
|
||||
it('should execute the synctex binary', function () {
|
||||
const outputFilePath = `${this.compileDir}/output.pdf`
|
||||
const inputFilePath = `${this.compileDir}/${this.filename}`
|
||||
expect(this.CommandRunner.promises.run).to.have.been.calledWith(
|
||||
`${this.projectId}-${this.userId}`,
|
||||
[
|
||||
'synctex',
|
||||
'view',
|
||||
'-i',
|
||||
`${this.line}:${this.column}:${inputFilePath}`,
|
||||
'-o',
|
||||
outputFilePath,
|
||||
],
|
||||
this.compileDir,
|
||||
this.Settings.clsi.docker.image,
|
||||
60000,
|
||||
{}
|
||||
)
|
||||
})
|
||||
|
||||
it('should return the parsed output', function () {
|
||||
expect(this.result).to.deep.equal(this.records)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a custom imageName', function () {
|
||||
const customImageName = 'foo/bar:tag-0'
|
||||
beforeEach(async function () {
|
||||
await this.CompileManager.promises.syncFromCode(
|
||||
this.projectId,
|
||||
this.userId,
|
||||
this.filename,
|
||||
this.line,
|
||||
this.column,
|
||||
{ imageName: customImageName }
|
||||
)
|
||||
})
|
||||
|
||||
it('should execute the synctex binary in a custom docker image', function () {
|
||||
const outputFilePath = `${this.compileDir}/output.pdf`
|
||||
const inputFilePath = `${this.compileDir}/${this.filename}`
|
||||
expect(this.CommandRunner.promises.run).to.have.been.calledWith(
|
||||
`${this.projectId}-${this.userId}`,
|
||||
[
|
||||
'synctex',
|
||||
'view',
|
||||
'-i',
|
||||
`${this.line}:${this.column}:${inputFilePath}`,
|
||||
'-o',
|
||||
outputFilePath,
|
||||
],
|
||||
this.compileDir,
|
||||
customImageName,
|
||||
60000,
|
||||
{}
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('syncFromPdf', function () {
|
||||
beforeEach(function () {
|
||||
this.records = [{ file: 'main.tex', line: 1, column: 1 }]
|
||||
this.SynctexOutputParser.parseEditOutput
|
||||
.withArgs(this.commandOutput, this.compileDir)
|
||||
.returns(this.records)
|
||||
})
|
||||
|
||||
describe('normal case', function () {
|
||||
beforeEach(async function () {
|
||||
this.result = await this.CompileManager.promises.syncFromPdf(
|
||||
this.projectId,
|
||||
this.userId,
|
||||
this.page,
|
||||
this.h,
|
||||
this.v,
|
||||
{ imageName: '' }
|
||||
)
|
||||
})
|
||||
|
||||
it('should execute the synctex binary', function () {
|
||||
const outputFilePath = `${this.compileDir}/output.pdf`
|
||||
expect(this.CommandRunner.promises.run).to.have.been.calledWith(
|
||||
`${this.projectId}-${this.userId}`,
|
||||
[
|
||||
'synctex',
|
||||
'edit',
|
||||
'-o',
|
||||
`${this.page}:${this.h}:${this.v}:${outputFilePath}`,
|
||||
],
|
||||
this.compileDir,
|
||||
this.Settings.clsi.docker.image,
|
||||
60000,
|
||||
{}
|
||||
)
|
||||
})
|
||||
|
||||
it('should return the parsed output', function () {
|
||||
expect(this.result).to.deep.equal(this.records)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a custom imageName', function () {
|
||||
const customImageName = 'foo/bar:tag-1'
|
||||
beforeEach(async function () {
|
||||
await this.CompileManager.promises.syncFromPdf(
|
||||
this.projectId,
|
||||
this.userId,
|
||||
this.page,
|
||||
this.h,
|
||||
this.v,
|
||||
{ imageName: customImageName }
|
||||
)
|
||||
})
|
||||
|
||||
it('should execute the synctex binary in a custom docker image', function () {
|
||||
const outputFilePath = `${this.compileDir}/output.pdf`
|
||||
expect(this.CommandRunner.promises.run).to.have.been.calledWith(
|
||||
`${this.projectId}-${this.userId}`,
|
||||
[
|
||||
'synctex',
|
||||
'edit',
|
||||
'-o',
|
||||
`${this.page}:${this.h}:${this.v}:${outputFilePath}`,
|
||||
],
|
||||
this.compileDir,
|
||||
customImageName,
|
||||
60000,
|
||||
{}
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('wordcount', function () {
|
||||
beforeEach(async function () {
|
||||
this.timeout = 60 * 1000
|
||||
this.filename = 'main.tex'
|
||||
this.image = 'example.com/image'
|
||||
|
||||
this.result = await this.CompileManager.promises.wordcount(
|
||||
this.projectId,
|
||||
this.userId,
|
||||
this.filename,
|
||||
this.image
|
||||
)
|
||||
})
|
||||
|
||||
it('should run the texcount command', function () {
|
||||
this.filePath = `$COMPILE_DIR/${this.filename}`
|
||||
this.command = ['texcount', '-nocol', '-inc', this.filePath]
|
||||
|
||||
expect(this.CommandRunner.promises.run).to.have.been.calledWith(
|
||||
`${this.projectId}-${this.userId}`,
|
||||
this.command,
|
||||
this.compileDir,
|
||||
this.image,
|
||||
this.timeout,
|
||||
{}
|
||||
)
|
||||
})
|
||||
|
||||
it('should return the parsed output', function () {
|
||||
expect(this.result).to.deep.equal({
|
||||
encode: 'ascii',
|
||||
textWords: 2,
|
||||
headWords: 0,
|
||||
outside: 0,
|
||||
headers: 0,
|
||||
elements: 0,
|
||||
mathInline: 0,
|
||||
mathDisplay: 0,
|
||||
errors: 0,
|
||||
messages: '',
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
223
services/clsi/test/unit/js/ContentCacheManagerTests.js
Normal file
223
services/clsi/test/unit/js/ContentCacheManagerTests.js
Normal file
@@ -0,0 +1,223 @@
|
||||
const fs = require('node:fs')
|
||||
const Path = require('node:path')
|
||||
const { expect } = require('chai')
|
||||
|
||||
const MODULE_PATH = '../../../app/js/ContentCacheManager'
|
||||
|
||||
describe('ContentCacheManager', function () {
|
||||
let contentDir, pdfPath, xrefPath
|
||||
let ContentCacheManager, files, Settings
|
||||
before(function () {
|
||||
Settings = require('@overleaf/settings')
|
||||
ContentCacheManager = require(MODULE_PATH)
|
||||
})
|
||||
let contentRanges, newContentRanges, reclaimed
|
||||
async function run(filePath, pdfSize, pdfCachingMinChunkSize) {
|
||||
const result = await ContentCacheManager.promises.update({
|
||||
contentDir,
|
||||
filePath,
|
||||
pdfSize,
|
||||
pdfCachingMinChunkSize,
|
||||
compileTime: 1337,
|
||||
})
|
||||
let newlyReclaimed
|
||||
;({
|
||||
contentRanges,
|
||||
newContentRanges,
|
||||
reclaimedSpace: newlyReclaimed,
|
||||
} = result)
|
||||
reclaimed += newlyReclaimed
|
||||
|
||||
const fileNames = await fs.promises.readdir(contentDir)
|
||||
files = {}
|
||||
for (const fileName of fileNames) {
|
||||
const path = Path.join(contentDir, fileName)
|
||||
files[path] = await fs.promises.readFile(path)
|
||||
}
|
||||
}
|
||||
before(function () {
|
||||
contentDir =
|
||||
'/overleaf/services/clsi/output/602cee6f6460fca0ba7921e6/content/1797a7f48f9-5abc1998509dea1f'
|
||||
pdfPath =
|
||||
'/overleaf/services/clsi/output/602cee6f6460fca0ba7921e6/generated-files/1797a7f48ea-8ac6805139f43351/output.pdf'
|
||||
xrefPath =
|
||||
'/overleaf/services/clsi/output/602cee6f6460fca0ba7921e6/generated-files/1797a7f48ea-8ac6805139f43351/output.pdfxref'
|
||||
|
||||
reclaimed = 0
|
||||
Settings.pdfCachingMinChunkSize = 1024
|
||||
})
|
||||
|
||||
before(async function () {
|
||||
await fs.promises.rm(contentDir, { recursive: true, force: true })
|
||||
await fs.promises.mkdir(contentDir, { recursive: true })
|
||||
await fs.promises.mkdir(Path.dirname(pdfPath), { recursive: true })
|
||||
})
|
||||
|
||||
describe('minimal', function () {
|
||||
const PATH_MINIMAL_PDF = 'test/acceptance/fixtures/minimal.pdf'
|
||||
const PATH_MINIMAL_XREF = 'test/acceptance/fixtures/minimal.pdfxref'
|
||||
const OBJECT_ID_1 = '9 0 '
|
||||
const HASH_LARGE =
|
||||
'd7cfc73ad2fba4578a437517923e3714927bbf35e63ea88bd93c7a8076cf1fcd'
|
||||
const OBJECT_ID_2 = '10 0 '
|
||||
const HASH_SMALL =
|
||||
'896749b8343851b0dc385f71616916a7ba0434fcfb56d1fc7e27cd139eaa2f71'
|
||||
function getChunkPath(hash) {
|
||||
return Path.join('test/unit/js/snapshots/minimalCompile/chunks', hash)
|
||||
}
|
||||
let MINIMAL_SIZE, RANGE_1, RANGE_2, h1, h2, START_1, START_2, END_1, END_2
|
||||
before(async function () {
|
||||
await fs.promises.copyFile(PATH_MINIMAL_PDF, pdfPath)
|
||||
await fs.promises.copyFile(PATH_MINIMAL_XREF, xrefPath)
|
||||
const MINIMAL = await fs.promises.readFile(PATH_MINIMAL_PDF)
|
||||
MINIMAL_SIZE = (await fs.promises.stat(PATH_MINIMAL_PDF)).size
|
||||
RANGE_1 = await fs.promises.readFile(getChunkPath(HASH_LARGE))
|
||||
RANGE_2 = await fs.promises.readFile(getChunkPath(HASH_SMALL))
|
||||
h1 = HASH_LARGE
|
||||
h2 = HASH_SMALL
|
||||
START_1 = MINIMAL.indexOf(RANGE_1)
|
||||
END_1 = START_1 + RANGE_1.byteLength
|
||||
START_2 = MINIMAL.indexOf(RANGE_2)
|
||||
END_2 = START_2 + RANGE_2.byteLength
|
||||
})
|
||||
async function runWithMinimal(pdfCachingMinChunkSize) {
|
||||
await run(pdfPath, MINIMAL_SIZE, pdfCachingMinChunkSize)
|
||||
}
|
||||
|
||||
describe('with two ranges qualifying', function () {
|
||||
before(async function () {
|
||||
await runWithMinimal(500)
|
||||
})
|
||||
it('should produce two ranges', function () {
|
||||
expect(contentRanges).to.have.length(2)
|
||||
})
|
||||
|
||||
it('should find the correct offsets', function () {
|
||||
expect(contentRanges).to.deep.equal([
|
||||
{
|
||||
objectId: OBJECT_ID_1,
|
||||
start: START_1,
|
||||
end: END_1,
|
||||
hash: h1,
|
||||
},
|
||||
{
|
||||
objectId: OBJECT_ID_2,
|
||||
start: START_2,
|
||||
end: END_2,
|
||||
hash: h2,
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('should store the contents', function () {
|
||||
expect(files).to.deep.equal({
|
||||
[Path.join(contentDir, h1)]: RANGE_1,
|
||||
[Path.join(contentDir, h2)]: RANGE_2,
|
||||
[Path.join(contentDir, '.state.v0.json')]: Buffer.from(
|
||||
JSON.stringify({
|
||||
hashAge: [
|
||||
[h1, 0],
|
||||
[h2, 0],
|
||||
],
|
||||
hashSize: [
|
||||
[h1, RANGE_1.byteLength],
|
||||
[h2, RANGE_2.byteLength],
|
||||
],
|
||||
})
|
||||
),
|
||||
})
|
||||
})
|
||||
|
||||
it('should mark all ranges as new', function () {
|
||||
expect(contentRanges).to.deep.equal(newContentRanges)
|
||||
})
|
||||
|
||||
describe('when re-running with one range too small', function () {
|
||||
before(async function () {
|
||||
await runWithMinimal(1024)
|
||||
})
|
||||
|
||||
it('should produce one range', function () {
|
||||
expect(contentRanges).to.have.length(1)
|
||||
})
|
||||
|
||||
it('should find the correct offsets', function () {
|
||||
expect(contentRanges).to.deep.equal([
|
||||
{
|
||||
objectId: OBJECT_ID_1,
|
||||
start: START_1,
|
||||
end: END_1,
|
||||
hash: h1,
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('should update the age of the 2nd range', function () {
|
||||
expect(files).to.deep.equal({
|
||||
[Path.join(contentDir, h1)]: RANGE_1,
|
||||
[Path.join(contentDir, h2)]: RANGE_2,
|
||||
[Path.join(contentDir, '.state.v0.json')]: Buffer.from(
|
||||
JSON.stringify({
|
||||
hashAge: [
|
||||
[h1, 0],
|
||||
[h2, 1],
|
||||
],
|
||||
hashSize: [
|
||||
[h1, RANGE_1.byteLength],
|
||||
[h2, RANGE_2.byteLength],
|
||||
],
|
||||
})
|
||||
),
|
||||
})
|
||||
})
|
||||
|
||||
it('should find no new ranges', function () {
|
||||
expect(newContentRanges).to.deep.equal([])
|
||||
})
|
||||
|
||||
describe('when re-running 5 more times', function () {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
before(async function () {
|
||||
await runWithMinimal(1024)
|
||||
})
|
||||
}
|
||||
|
||||
it('should still produce one range', function () {
|
||||
expect(contentRanges).to.have.length(1)
|
||||
})
|
||||
|
||||
it('should still find the correct offsets', function () {
|
||||
expect(contentRanges).to.deep.equal([
|
||||
{
|
||||
objectId: OBJECT_ID_1,
|
||||
start: START_1,
|
||||
end: END_1,
|
||||
hash: h1,
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('should delete the 2nd range', function () {
|
||||
expect(files).to.deep.equal({
|
||||
[Path.join(contentDir, h1)]: RANGE_1,
|
||||
[Path.join(contentDir, '.state.v0.json')]: Buffer.from(
|
||||
JSON.stringify({
|
||||
hashAge: [[h1, 0]],
|
||||
hashSize: [[h1, RANGE_1.byteLength]],
|
||||
})
|
||||
),
|
||||
})
|
||||
})
|
||||
|
||||
it('should find no new ranges', function () {
|
||||
expect(newContentRanges).to.deep.equal([])
|
||||
})
|
||||
|
||||
it('should yield the reclaimed space', function () {
|
||||
expect(reclaimed).to.equal(RANGE_2.byteLength)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
79
services/clsi/test/unit/js/ContentTypeMapperTests.js
Normal file
79
services/clsi/test/unit/js/ContentTypeMapperTests.js
Normal file
@@ -0,0 +1,79 @@
|
||||
/* 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:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
const SandboxedModule = require('sandboxed-module')
|
||||
const sinon = require('sinon')
|
||||
const modulePath = require('node:path').join(
|
||||
__dirname,
|
||||
'../../../app/js/ContentTypeMapper'
|
||||
)
|
||||
|
||||
describe('ContentTypeMapper', function () {
|
||||
beforeEach(function () {
|
||||
return (this.ContentTypeMapper = SandboxedModule.require(modulePath))
|
||||
})
|
||||
|
||||
return describe('map', function () {
|
||||
it('should map .txt to text/plain', function () {
|
||||
const contentType = this.ContentTypeMapper.map('example.txt')
|
||||
return contentType.should.equal('text/plain')
|
||||
})
|
||||
|
||||
it('should map .csv to text/csv', function () {
|
||||
const contentType = this.ContentTypeMapper.map('example.csv')
|
||||
return contentType.should.equal('text/csv')
|
||||
})
|
||||
|
||||
it('should map .pdf to application/pdf', function () {
|
||||
const contentType = this.ContentTypeMapper.map('example.pdf')
|
||||
return contentType.should.equal('application/pdf')
|
||||
})
|
||||
|
||||
it('should fall back to octet-stream', function () {
|
||||
const contentType = this.ContentTypeMapper.map('example.unknown')
|
||||
return contentType.should.equal('application/octet-stream')
|
||||
})
|
||||
|
||||
describe('coercing web files to plain text', function () {
|
||||
it('should map .js to plain text', function () {
|
||||
const contentType = this.ContentTypeMapper.map('example.js')
|
||||
return contentType.should.equal('text/plain')
|
||||
})
|
||||
|
||||
it('should map .html to plain text', function () {
|
||||
const contentType = this.ContentTypeMapper.map('example.html')
|
||||
return contentType.should.equal('text/plain')
|
||||
})
|
||||
|
||||
return it('should map .css to plain text', function () {
|
||||
const contentType = this.ContentTypeMapper.map('example.css')
|
||||
return contentType.should.equal('text/plain')
|
||||
})
|
||||
})
|
||||
|
||||
return describe('image files', function () {
|
||||
it('should map .png to image/png', function () {
|
||||
const contentType = this.ContentTypeMapper.map('example.png')
|
||||
return contentType.should.equal('image/png')
|
||||
})
|
||||
|
||||
it('should map .jpeg to image/jpeg', function () {
|
||||
const contentType = this.ContentTypeMapper.map('example.jpeg')
|
||||
return contentType.should.equal('image/jpeg')
|
||||
})
|
||||
|
||||
return it('should map .svg to text/plain to protect against XSS (SVG can execute JS)', function () {
|
||||
const contentType = this.ContentTypeMapper.map('example.svg')
|
||||
return contentType.should.equal('text/plain')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
246
services/clsi/test/unit/js/DockerLockManagerTests.js
Normal file
246
services/clsi/test/unit/js/DockerLockManagerTests.js
Normal file
@@ -0,0 +1,246 @@
|
||||
/* eslint-disable
|
||||
no-return-assign,
|
||||
*/
|
||||
// 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
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
const SandboxedModule = require('sandboxed-module')
|
||||
const sinon = require('sinon')
|
||||
const modulePath = require('node:path').join(
|
||||
__dirname,
|
||||
'../../../app/js/DockerLockManager'
|
||||
)
|
||||
|
||||
describe('DockerLockManager', function () {
|
||||
beforeEach(function () {
|
||||
return (this.LockManager = SandboxedModule.require(modulePath, {
|
||||
requires: {
|
||||
'@overleaf/settings': (this.Settings = { clsi: { docker: {} } }),
|
||||
},
|
||||
}))
|
||||
})
|
||||
|
||||
return describe('runWithLock', function () {
|
||||
describe('with a single lock', function () {
|
||||
beforeEach(function (done) {
|
||||
this.callback = sinon.stub()
|
||||
return this.LockManager.runWithLock(
|
||||
'lock-one',
|
||||
releaseLock =>
|
||||
setTimeout(() => releaseLock(null, 'hello', 'world'), 100),
|
||||
|
||||
(err, ...args) => {
|
||||
this.callback(err, ...Array.from(args))
|
||||
return done()
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
return it('should call the callback', function () {
|
||||
return this.callback
|
||||
.calledWith(null, 'hello', 'world')
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with two locks', function () {
|
||||
beforeEach(function (done) {
|
||||
this.callback1 = sinon.stub()
|
||||
this.callback2 = sinon.stub()
|
||||
this.LockManager.runWithLock(
|
||||
'lock-one',
|
||||
releaseLock =>
|
||||
setTimeout(() => releaseLock(null, 'hello', 'world', 'one'), 100),
|
||||
|
||||
(err, ...args) => {
|
||||
return this.callback1(err, ...Array.from(args))
|
||||
}
|
||||
)
|
||||
return this.LockManager.runWithLock(
|
||||
'lock-two',
|
||||
releaseLock =>
|
||||
setTimeout(() => releaseLock(null, 'hello', 'world', 'two'), 200),
|
||||
|
||||
(err, ...args) => {
|
||||
this.callback2(err, ...Array.from(args))
|
||||
return done()
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('should call the first callback', function () {
|
||||
return this.callback1
|
||||
.calledWith(null, 'hello', 'world', 'one')
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
return it('should call the second callback', function () {
|
||||
return this.callback2
|
||||
.calledWith(null, 'hello', 'world', 'two')
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
return describe('with lock contention', function () {
|
||||
describe('where the first lock is released quickly', function () {
|
||||
beforeEach(function (done) {
|
||||
this.LockManager.MAX_LOCK_WAIT_TIME = 1000
|
||||
this.LockManager.LOCK_TEST_INTERVAL = 100
|
||||
this.callback1 = sinon.stub()
|
||||
this.callback2 = sinon.stub()
|
||||
this.LockManager.runWithLock(
|
||||
'lock',
|
||||
releaseLock =>
|
||||
setTimeout(() => releaseLock(null, 'hello', 'world', 'one'), 100),
|
||||
|
||||
(err, ...args) => {
|
||||
return this.callback1(err, ...Array.from(args))
|
||||
}
|
||||
)
|
||||
return this.LockManager.runWithLock(
|
||||
'lock',
|
||||
releaseLock =>
|
||||
setTimeout(() => releaseLock(null, 'hello', 'world', 'two'), 200),
|
||||
|
||||
(err, ...args) => {
|
||||
this.callback2(err, ...Array.from(args))
|
||||
return done()
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('should call the first callback', function () {
|
||||
return this.callback1
|
||||
.calledWith(null, 'hello', 'world', 'one')
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
return it('should call the second callback', function () {
|
||||
return this.callback2
|
||||
.calledWith(null, 'hello', 'world', 'two')
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('where the first lock is held longer than the waiting time', function () {
|
||||
beforeEach(function (done) {
|
||||
let doneTwo
|
||||
this.LockManager.MAX_LOCK_HOLD_TIME = 10000
|
||||
this.LockManager.MAX_LOCK_WAIT_TIME = 1000
|
||||
this.LockManager.LOCK_TEST_INTERVAL = 100
|
||||
this.callback1 = sinon.stub()
|
||||
this.callback2 = sinon.stub()
|
||||
let doneOne = (doneTwo = false)
|
||||
const finish = function (key) {
|
||||
if (key === 1) {
|
||||
doneOne = true
|
||||
}
|
||||
if (key === 2) {
|
||||
doneTwo = true
|
||||
}
|
||||
if (doneOne && doneTwo) {
|
||||
return done()
|
||||
}
|
||||
}
|
||||
this.LockManager.runWithLock(
|
||||
'lock',
|
||||
releaseLock =>
|
||||
setTimeout(
|
||||
() => releaseLock(null, 'hello', 'world', 'one'),
|
||||
1100
|
||||
),
|
||||
|
||||
(err, ...args) => {
|
||||
this.callback1(err, ...Array.from(args))
|
||||
return finish(1)
|
||||
}
|
||||
)
|
||||
return this.LockManager.runWithLock(
|
||||
'lock',
|
||||
releaseLock =>
|
||||
setTimeout(() => releaseLock(null, 'hello', 'world', 'two'), 100),
|
||||
|
||||
(err, ...args) => {
|
||||
this.callback2(err, ...Array.from(args))
|
||||
return finish(2)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('should call the first callback', function () {
|
||||
return this.callback1
|
||||
.calledWith(null, 'hello', 'world', 'one')
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
return it('should call the second callback with an error', function () {
|
||||
const error = sinon.match.instanceOf(Error)
|
||||
return this.callback2.calledWith(error).should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
return describe('where the first lock is held longer than the max holding time', function () {
|
||||
beforeEach(function (done) {
|
||||
let doneTwo
|
||||
this.LockManager.MAX_LOCK_HOLD_TIME = 1000
|
||||
this.LockManager.MAX_LOCK_WAIT_TIME = 2000
|
||||
this.LockManager.LOCK_TEST_INTERVAL = 100
|
||||
this.callback1 = sinon.stub()
|
||||
this.callback2 = sinon.stub()
|
||||
let doneOne = (doneTwo = false)
|
||||
const finish = function (key) {
|
||||
if (key === 1) {
|
||||
doneOne = true
|
||||
}
|
||||
if (key === 2) {
|
||||
doneTwo = true
|
||||
}
|
||||
if (doneOne && doneTwo) {
|
||||
return done()
|
||||
}
|
||||
}
|
||||
this.LockManager.runWithLock(
|
||||
'lock',
|
||||
releaseLock =>
|
||||
setTimeout(
|
||||
() => releaseLock(null, 'hello', 'world', 'one'),
|
||||
1500
|
||||
),
|
||||
|
||||
(err, ...args) => {
|
||||
this.callback1(err, ...Array.from(args))
|
||||
return finish(1)
|
||||
}
|
||||
)
|
||||
return this.LockManager.runWithLock(
|
||||
'lock',
|
||||
releaseLock =>
|
||||
setTimeout(() => releaseLock(null, 'hello', 'world', 'two'), 100),
|
||||
|
||||
(err, ...args) => {
|
||||
this.callback2(err, ...Array.from(args))
|
||||
return finish(2)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('should call the first callback', function () {
|
||||
return this.callback1
|
||||
.calledWith(null, 'hello', 'world', 'one')
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
return it('should call the second callback', function () {
|
||||
return this.callback2
|
||||
.calledWith(null, 'hello', 'world', 'two')
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
981
services/clsi/test/unit/js/DockerRunnerTests.js
Normal file
981
services/clsi/test/unit/js/DockerRunnerTests.js
Normal file
@@ -0,0 +1,981 @@
|
||||
/* 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
|
||||
* DS206: Consider reworking classes to avoid initClass
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
const SandboxedModule = require('sandboxed-module')
|
||||
const sinon = require('sinon')
|
||||
const { expect } = require('chai')
|
||||
const modulePath = require('node:path').join(
|
||||
__dirname,
|
||||
'../../../app/js/DockerRunner'
|
||||
)
|
||||
const Path = require('node:path')
|
||||
|
||||
describe('DockerRunner', function () {
|
||||
beforeEach(function () {
|
||||
let container, Docker, Timer
|
||||
this.container = container = {}
|
||||
this.DockerRunner = SandboxedModule.require(modulePath, {
|
||||
requires: {
|
||||
'@overleaf/settings': (this.Settings = {
|
||||
clsi: { docker: {} },
|
||||
path: {},
|
||||
}),
|
||||
dockerode: (Docker = (function () {
|
||||
Docker = class Docker {
|
||||
static initClass() {
|
||||
this.prototype.getContainer = sinon.stub().returns(container)
|
||||
this.prototype.createContainer = sinon
|
||||
.stub()
|
||||
.yields(null, container)
|
||||
this.prototype.listContainers = sinon.stub()
|
||||
}
|
||||
}
|
||||
Docker.initClass()
|
||||
return Docker
|
||||
})()),
|
||||
fs: (this.fs = {
|
||||
stat: sinon.stub().yields(null, {
|
||||
isDirectory() {
|
||||
return true
|
||||
},
|
||||
}),
|
||||
}),
|
||||
'./Metrics': {
|
||||
Timer: (Timer = class Timer {
|
||||
done() {}
|
||||
}),
|
||||
},
|
||||
'./LockManager': {
|
||||
runWithLock(key, runner, callback) {
|
||||
return runner(callback)
|
||||
},
|
||||
},
|
||||
},
|
||||
globals: { Math }, // used by lodash
|
||||
})
|
||||
this.Docker = Docker
|
||||
this.getContainer = Docker.prototype.getContainer
|
||||
this.createContainer = Docker.prototype.createContainer
|
||||
this.listContainers = Docker.prototype.listContainers
|
||||
|
||||
this.directory = '/local/compile/directory'
|
||||
this.mainFile = 'main-file.tex'
|
||||
this.compiler = 'pdflatex'
|
||||
this.image = 'example.com/overleaf/image:2016.2'
|
||||
this.env = {}
|
||||
this.callback = sinon.stub()
|
||||
this.project_id = 'project-id-123'
|
||||
this.volumes = { '/some/host/dir/compiles/directory': '/compile' }
|
||||
this.Settings.clsi.docker.image = this.defaultImage = 'default-image'
|
||||
this.Settings.path.sandboxedCompilesHostDirCompiles =
|
||||
'/some/host/dir/compiles'
|
||||
this.Settings.path.sandboxedCompilesHostDirOutput = '/some/host/dir/output'
|
||||
this.compileGroup = 'compile-group'
|
||||
return (this.Settings.clsi.docker.env = { PATH: 'mock-path' })
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
this.DockerRunner.stopContainerMonitor()
|
||||
})
|
||||
|
||||
describe('run', function () {
|
||||
beforeEach(function (done) {
|
||||
this.DockerRunner._getContainerOptions = sinon
|
||||
.stub()
|
||||
.returns((this.options = { mockoptions: 'foo' }))
|
||||
this.DockerRunner._fingerprintContainer = sinon
|
||||
.stub()
|
||||
.returns((this.fingerprint = 'fingerprint'))
|
||||
|
||||
this.name = `project-${this.project_id}-${this.fingerprint}`
|
||||
|
||||
this.command = ['mock', 'command', '--outdir=$COMPILE_DIR']
|
||||
this.command_with_dir = ['mock', 'command', '--outdir=/compile']
|
||||
this.timeout = 42000
|
||||
return done()
|
||||
})
|
||||
|
||||
describe('successfully', function () {
|
||||
beforeEach(function (done) {
|
||||
this.DockerRunner._runAndWaitForContainer = sinon
|
||||
.stub()
|
||||
.callsArgWith(3, null, (this.output = 'mock-output'))
|
||||
return this.DockerRunner.run(
|
||||
this.project_id,
|
||||
this.command,
|
||||
this.directory,
|
||||
this.image,
|
||||
this.timeout,
|
||||
this.env,
|
||||
this.compileGroup,
|
||||
(err, output) => {
|
||||
this.callback(err, output)
|
||||
return done()
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('should generate the options for the container', function () {
|
||||
return this.DockerRunner._getContainerOptions
|
||||
.calledWith(
|
||||
this.command_with_dir,
|
||||
this.image,
|
||||
this.volumes,
|
||||
this.timeout
|
||||
)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should generate the fingerprint from the returned options', function () {
|
||||
return this.DockerRunner._fingerprintContainer
|
||||
.calledWith(this.options)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should do the run', function () {
|
||||
return this.DockerRunner._runAndWaitForContainer
|
||||
.calledWith(this.options, this.volumes, this.timeout)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
return it('should call the callback', function () {
|
||||
return this.callback.calledWith(null, this.output).should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('standard compile', function () {
|
||||
beforeEach(function () {
|
||||
this.directory = '/var/lib/overleaf/data/compiles/xyz'
|
||||
this.DockerRunner._runAndWaitForContainer = sinon
|
||||
.stub()
|
||||
.callsArgWith(3, null, (this.output = 'mock-output'))
|
||||
return this.DockerRunner.run(
|
||||
this.project_id,
|
||||
this.command,
|
||||
this.directory,
|
||||
this.image,
|
||||
this.timeout,
|
||||
this.env,
|
||||
this.compileGroup,
|
||||
this.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should re-write the bind directory', function () {
|
||||
const volumes =
|
||||
this.DockerRunner._runAndWaitForContainer.lastCall.args[1]
|
||||
return expect(volumes).to.deep.equal({
|
||||
'/some/host/dir/compiles/xyz': '/compile',
|
||||
})
|
||||
})
|
||||
|
||||
return it('should call the callback', function () {
|
||||
return this.callback.calledWith(null, this.output).should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('synctex-output', function () {
|
||||
beforeEach(function () {
|
||||
this.directory = '/var/lib/overleaf/data/output/xyz/generated-files/id'
|
||||
this.DockerRunner._runAndWaitForContainer = sinon
|
||||
.stub()
|
||||
.callsArgWith(3, null, (this.output = 'mock-output'))
|
||||
this.DockerRunner.run(
|
||||
this.project_id,
|
||||
this.command,
|
||||
this.directory,
|
||||
this.image,
|
||||
this.timeout,
|
||||
this.env,
|
||||
'synctex-output',
|
||||
this.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should re-write the bind directory and set ro flag', function () {
|
||||
const volumes =
|
||||
this.DockerRunner._runAndWaitForContainer.lastCall.args[1]
|
||||
expect(volumes).to.deep.equal({
|
||||
'/some/host/dir/output/xyz/generated-files/id': '/compile:ro',
|
||||
})
|
||||
})
|
||||
|
||||
it('should call the callback', function () {
|
||||
this.callback.calledWith(null, this.output).should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('synctex', function () {
|
||||
beforeEach(function () {
|
||||
this.directory = '/var/lib/overleaf/data/compile/xyz'
|
||||
this.DockerRunner._runAndWaitForContainer = sinon
|
||||
.stub()
|
||||
.callsArgWith(3, null, (this.output = 'mock-output'))
|
||||
this.DockerRunner.run(
|
||||
this.project_id,
|
||||
this.command,
|
||||
this.directory,
|
||||
this.image,
|
||||
this.timeout,
|
||||
this.env,
|
||||
'synctex',
|
||||
this.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should re-write the bind directory', function () {
|
||||
const volumes =
|
||||
this.DockerRunner._runAndWaitForContainer.lastCall.args[1]
|
||||
expect(volumes).to.deep.equal({
|
||||
'/some/host/dir/compiles/xyz': '/compile:ro',
|
||||
})
|
||||
})
|
||||
|
||||
it('should call the callback', function () {
|
||||
this.callback.calledWith(null, this.output).should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('wordcount', function () {
|
||||
beforeEach(function () {
|
||||
this.directory = '/var/lib/overleaf/data/compile/xyz'
|
||||
this.DockerRunner._runAndWaitForContainer = sinon
|
||||
.stub()
|
||||
.callsArgWith(3, null, (this.output = 'mock-output'))
|
||||
this.DockerRunner.run(
|
||||
this.project_id,
|
||||
this.command,
|
||||
this.directory,
|
||||
this.image,
|
||||
this.timeout,
|
||||
this.env,
|
||||
'wordcount',
|
||||
this.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should re-write the bind directory', function () {
|
||||
const volumes =
|
||||
this.DockerRunner._runAndWaitForContainer.lastCall.args[1]
|
||||
expect(volumes).to.deep.equal({
|
||||
'/some/host/dir/compiles/xyz': '/compile:ro',
|
||||
})
|
||||
})
|
||||
|
||||
it('should call the callback', function () {
|
||||
this.callback.calledWith(null, this.output).should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the run throws an error', function () {
|
||||
beforeEach(function () {
|
||||
let firstTime = true
|
||||
this.output = 'mock-output'
|
||||
this.DockerRunner._runAndWaitForContainer = (
|
||||
options,
|
||||
volumes,
|
||||
timeout,
|
||||
callback
|
||||
) => {
|
||||
if (callback == null) {
|
||||
callback = function () {}
|
||||
}
|
||||
if (firstTime) {
|
||||
firstTime = false
|
||||
const error = new Error('(HTTP code 500) server error - ...')
|
||||
error.statusCode = 500
|
||||
return callback(error)
|
||||
} else {
|
||||
return callback(null, this.output)
|
||||
}
|
||||
}
|
||||
sinon.spy(this.DockerRunner, '_runAndWaitForContainer')
|
||||
this.DockerRunner.destroyContainer = sinon.stub().callsArg(3)
|
||||
return this.DockerRunner.run(
|
||||
this.project_id,
|
||||
this.command,
|
||||
this.directory,
|
||||
this.image,
|
||||
this.timeout,
|
||||
this.env,
|
||||
this.compileGroup,
|
||||
this.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should do the run twice', function () {
|
||||
return this.DockerRunner._runAndWaitForContainer.calledTwice.should.equal(
|
||||
true
|
||||
)
|
||||
})
|
||||
|
||||
it('should destroy the container in between', function () {
|
||||
return this.DockerRunner.destroyContainer
|
||||
.calledWith(this.name, null)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
return it('should call the callback', function () {
|
||||
return this.callback.calledWith(null, this.output).should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with no image', function () {
|
||||
beforeEach(function () {
|
||||
this.DockerRunner._runAndWaitForContainer = sinon
|
||||
.stub()
|
||||
.callsArgWith(3, null, (this.output = 'mock-output'))
|
||||
return this.DockerRunner.run(
|
||||
this.project_id,
|
||||
this.command,
|
||||
this.directory,
|
||||
null,
|
||||
this.timeout,
|
||||
this.env,
|
||||
this.compileGroup,
|
||||
this.callback
|
||||
)
|
||||
})
|
||||
|
||||
return it('should use the default image', function () {
|
||||
return this.DockerRunner._getContainerOptions
|
||||
.calledWith(
|
||||
this.command_with_dir,
|
||||
this.defaultImage,
|
||||
this.volumes,
|
||||
this.timeout
|
||||
)
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with image override', function () {
|
||||
beforeEach(function () {
|
||||
this.Settings.texliveImageNameOveride = 'overrideimage.com/something'
|
||||
this.DockerRunner._runAndWaitForContainer = sinon
|
||||
.stub()
|
||||
.callsArgWith(3, null, (this.output = 'mock-output'))
|
||||
return this.DockerRunner.run(
|
||||
this.project_id,
|
||||
this.command,
|
||||
this.directory,
|
||||
this.image,
|
||||
this.timeout,
|
||||
this.env,
|
||||
this.compileGroup,
|
||||
this.callback
|
||||
)
|
||||
})
|
||||
|
||||
return it('should use the override and keep the tag', function () {
|
||||
const image = this.DockerRunner._getContainerOptions.args[0][1]
|
||||
return image.should.equal('overrideimage.com/something/image:2016.2')
|
||||
})
|
||||
})
|
||||
|
||||
describe('with image restriction', function () {
|
||||
beforeEach(function () {
|
||||
this.Settings.clsi.docker.allowedImages = [
|
||||
'repo/image:tag1',
|
||||
'repo/image:tag2',
|
||||
]
|
||||
this.DockerRunner._runAndWaitForContainer = sinon
|
||||
.stub()
|
||||
.callsArgWith(3, null, (this.output = 'mock-output'))
|
||||
})
|
||||
|
||||
describe('with a valid image', function () {
|
||||
beforeEach(function () {
|
||||
this.DockerRunner.run(
|
||||
this.project_id,
|
||||
this.command,
|
||||
this.directory,
|
||||
'repo/image:tag1',
|
||||
this.timeout,
|
||||
this.env,
|
||||
this.compileGroup,
|
||||
this.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should setup the container', function () {
|
||||
this.DockerRunner._getContainerOptions.called.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a invalid image', function () {
|
||||
beforeEach(function () {
|
||||
this.DockerRunner.run(
|
||||
this.project_id,
|
||||
this.command,
|
||||
this.directory,
|
||||
'something/different:evil',
|
||||
this.timeout,
|
||||
this.env,
|
||||
this.compileGroup,
|
||||
this.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should call the callback with an error', function () {
|
||||
const err = new Error('image not allowed')
|
||||
this.callback.called.should.equal(true)
|
||||
this.callback.args[0][0].message.should.equal(err.message)
|
||||
})
|
||||
|
||||
it('should not setup the container', function () {
|
||||
this.DockerRunner._getContainerOptions.called.should.equal(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('run with _getOptions', function () {
|
||||
beforeEach(function (done) {
|
||||
// this.DockerRunner._getContainerOptions = sinon
|
||||
// .stub()
|
||||
// .returns((this.options = { mockoptions: 'foo' }))
|
||||
this.DockerRunner._fingerprintContainer = sinon
|
||||
.stub()
|
||||
.returns((this.fingerprint = 'fingerprint'))
|
||||
|
||||
this.name = `project-${this.project_id}-${this.fingerprint}`
|
||||
|
||||
this.command = ['mock', 'command', '--outdir=$COMPILE_DIR']
|
||||
this.command_with_dir = ['mock', 'command', '--outdir=/compile']
|
||||
this.timeout = 42000
|
||||
return done()
|
||||
})
|
||||
|
||||
describe('when a compile group config is set', function () {
|
||||
beforeEach(function () {
|
||||
this.Settings.clsi.docker.compileGroupConfig = {
|
||||
'compile-group': {
|
||||
'HostConfig.newProperty': 'new-property',
|
||||
},
|
||||
'other-group': { otherProperty: 'other-property' },
|
||||
}
|
||||
this.DockerRunner._runAndWaitForContainer = sinon
|
||||
.stub()
|
||||
.callsArgWith(3, null, (this.output = 'mock-output'))
|
||||
return this.DockerRunner.run(
|
||||
this.project_id,
|
||||
this.command,
|
||||
this.directory,
|
||||
this.image,
|
||||
this.timeout,
|
||||
this.env,
|
||||
this.compileGroup,
|
||||
this.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should set the docker options for the compile group', function () {
|
||||
const options =
|
||||
this.DockerRunner._runAndWaitForContainer.lastCall.args[0]
|
||||
return expect(options.HostConfig).to.deep.include({
|
||||
Binds: ['/some/host/dir/compiles/directory:/compile:rw'],
|
||||
LogConfig: { Type: 'none', Config: {} },
|
||||
CapDrop: 'ALL',
|
||||
SecurityOpt: ['no-new-privileges'],
|
||||
newProperty: 'new-property',
|
||||
})
|
||||
})
|
||||
|
||||
return it('should call the callback', function () {
|
||||
return this.callback.calledWith(null, this.output).should.equal(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('_runAndWaitForContainer', function () {
|
||||
beforeEach(function () {
|
||||
this.options = { mockoptions: 'foo', name: (this.name = 'mock-name') }
|
||||
this.DockerRunner.startContainer = (
|
||||
options,
|
||||
volumes,
|
||||
attachStreamHandler,
|
||||
callback
|
||||
) => {
|
||||
attachStreamHandler(null, (this.output = 'mock-output'))
|
||||
return callback(null, (this.containerId = 'container-id'))
|
||||
}
|
||||
sinon.spy(this.DockerRunner, 'startContainer')
|
||||
this.DockerRunner.waitForContainer = sinon
|
||||
.stub()
|
||||
.callsArgWith(2, null, (this.exitCode = 42))
|
||||
return this.DockerRunner._runAndWaitForContainer(
|
||||
this.options,
|
||||
this.volumes,
|
||||
this.timeout,
|
||||
this.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should create/start the container', function () {
|
||||
return this.DockerRunner.startContainer
|
||||
.calledWith(this.options, this.volumes)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should wait for the container to finish', function () {
|
||||
return this.DockerRunner.waitForContainer
|
||||
.calledWith(this.name, this.timeout)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
return it('should call the callback with the output', function () {
|
||||
return this.callback.calledWith(null, this.output).should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('startContainer', function () {
|
||||
beforeEach(function () {
|
||||
this.attachStreamHandler = sinon.stub()
|
||||
this.attachStreamHandler.cock = true
|
||||
this.options = { mockoptions: 'foo', name: 'mock-name' }
|
||||
this.container.inspect = sinon.stub().callsArgWith(0)
|
||||
this.DockerRunner.attachToContainer = (
|
||||
containerId,
|
||||
attachStreamHandler,
|
||||
cb
|
||||
) => {
|
||||
attachStreamHandler()
|
||||
return cb()
|
||||
}
|
||||
return sinon.spy(this.DockerRunner, 'attachToContainer')
|
||||
})
|
||||
|
||||
describe('when the container exists', function () {
|
||||
beforeEach(function () {
|
||||
this.container.inspect = sinon.stub().callsArgWith(0)
|
||||
this.container.start = sinon.stub().yields()
|
||||
|
||||
return this.DockerRunner.startContainer(
|
||||
this.options,
|
||||
this.volumes,
|
||||
() => {},
|
||||
this.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should start the container with the given name', function () {
|
||||
this.getContainer.calledWith(this.options.name).should.equal(true)
|
||||
return this.container.start.called.should.equal(true)
|
||||
})
|
||||
|
||||
it('should not try to create the container', function () {
|
||||
return this.createContainer.called.should.equal(false)
|
||||
})
|
||||
|
||||
it('should attach to the container', function () {
|
||||
return this.DockerRunner.attachToContainer.called.should.equal(true)
|
||||
})
|
||||
|
||||
it('should call the callback', function () {
|
||||
return this.callback.called.should.equal(true)
|
||||
})
|
||||
|
||||
return it('should attach before the container starts', function () {
|
||||
return sinon.assert.callOrder(
|
||||
this.DockerRunner.attachToContainer,
|
||||
this.container.start
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the container does not exist', function () {
|
||||
beforeEach(function () {
|
||||
const exists = false
|
||||
this.container.start = sinon.stub().yields()
|
||||
this.container.inspect = sinon
|
||||
.stub()
|
||||
.callsArgWith(0, { statusCode: 404 })
|
||||
return this.DockerRunner.startContainer(
|
||||
this.options,
|
||||
this.volumes,
|
||||
this.attachStreamHandler,
|
||||
this.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should create the container', function () {
|
||||
return this.createContainer.calledWith(this.options).should.equal(true)
|
||||
})
|
||||
|
||||
it('should call the callback and stream handler', function () {
|
||||
this.attachStreamHandler.called.should.equal(true)
|
||||
return this.callback.called.should.equal(true)
|
||||
})
|
||||
|
||||
it('should attach to the container', function () {
|
||||
return this.DockerRunner.attachToContainer.called.should.equal(true)
|
||||
})
|
||||
|
||||
return it('should attach before the container starts', function () {
|
||||
return sinon.assert.callOrder(
|
||||
this.DockerRunner.attachToContainer,
|
||||
this.container.start
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the container is already running', function () {
|
||||
beforeEach(function () {
|
||||
const error = new Error(
|
||||
`HTTP code is 304 which indicates error: server error - start: Cannot start container ${this.name}: The container MOCKID is already running.`
|
||||
)
|
||||
error.statusCode = 304
|
||||
this.container.start = sinon.stub().yields(error)
|
||||
this.container.inspect = sinon.stub().callsArgWith(0)
|
||||
return this.DockerRunner.startContainer(
|
||||
this.options,
|
||||
this.volumes,
|
||||
this.attachStreamHandler,
|
||||
this.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should not try to create the container', function () {
|
||||
return this.createContainer.called.should.equal(false)
|
||||
})
|
||||
|
||||
return it('should call the callback and stream handler without an error', function () {
|
||||
this.attachStreamHandler.called.should.equal(true)
|
||||
return this.callback.called.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
return describe('when the container tries to be created, but already has been (race condition)', function () {})
|
||||
})
|
||||
|
||||
describe('waitForContainer', function () {
|
||||
beforeEach(function () {
|
||||
this.containerId = 'container-id'
|
||||
this.timeout = 5000
|
||||
this.container.wait = sinon
|
||||
.stub()
|
||||
.yields(null, { StatusCode: (this.statusCode = 42) })
|
||||
return (this.container.kill = sinon.stub().yields())
|
||||
})
|
||||
|
||||
describe('when the container returns in time', function () {
|
||||
beforeEach(function () {
|
||||
return this.DockerRunner.waitForContainer(
|
||||
this.containerId,
|
||||
this.timeout,
|
||||
this.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should wait for the container', function () {
|
||||
this.getContainer.calledWith(this.containerId).should.equal(true)
|
||||
return this.container.wait.called.should.equal(true)
|
||||
})
|
||||
|
||||
return it('should call the callback with the exit', function () {
|
||||
return this.callback
|
||||
.calledWith(null, this.statusCode)
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
return describe('when the container does not return before the timeout', function () {
|
||||
beforeEach(function (done) {
|
||||
this.container.wait = function (callback) {
|
||||
if (callback == null) {
|
||||
callback = function () {}
|
||||
}
|
||||
return setTimeout(() => callback(null, { StatusCode: 42 }), 100)
|
||||
}
|
||||
this.timeout = 5
|
||||
return this.DockerRunner.waitForContainer(
|
||||
this.containerId,
|
||||
this.timeout,
|
||||
(...args) => {
|
||||
this.callback(...Array.from(args || []))
|
||||
return done()
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('should call kill on the container', function () {
|
||||
this.getContainer.calledWith(this.containerId).should.equal(true)
|
||||
return this.container.kill.called.should.equal(true)
|
||||
})
|
||||
|
||||
it('should call the callback with an error', function () {
|
||||
this.callback.calledWith(sinon.match(Error)).should.equal(true)
|
||||
|
||||
const errorObj = this.callback.args[0][0]
|
||||
expect(errorObj.message).to.include('container timed out')
|
||||
expect(errorObj.timedout).equal(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('destroyOldContainers', function () {
|
||||
beforeEach(function (done) {
|
||||
const oneHourInSeconds = 60 * 60
|
||||
const oneHourInMilliseconds = oneHourInSeconds * 1000
|
||||
const nowInSeconds = Date.now() / 1000
|
||||
this.containers = [
|
||||
{
|
||||
Name: '/project-old-container-name',
|
||||
Id: 'old-container-id',
|
||||
Created: nowInSeconds - oneHourInSeconds - 100,
|
||||
},
|
||||
{
|
||||
Name: '/project-new-container-name',
|
||||
Id: 'new-container-id',
|
||||
Created: nowInSeconds - oneHourInSeconds + 100,
|
||||
},
|
||||
{
|
||||
Name: '/totally-not-a-project-container',
|
||||
Id: 'some-random-id',
|
||||
Created: nowInSeconds - 2 * oneHourInSeconds,
|
||||
},
|
||||
]
|
||||
this.DockerRunner.MAX_CONTAINER_AGE = oneHourInMilliseconds
|
||||
this.listContainers.callsArgWith(1, null, this.containers)
|
||||
this.DockerRunner.destroyContainer = sinon.stub().callsArg(3)
|
||||
return this.DockerRunner.destroyOldContainers(error => {
|
||||
this.callback(error)
|
||||
return done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should list all containers', function () {
|
||||
return this.listContainers.calledWith({ all: true }).should.equal(true)
|
||||
})
|
||||
|
||||
it('should destroy old containers', function () {
|
||||
this.DockerRunner.destroyContainer.callCount.should.equal(1)
|
||||
return this.DockerRunner.destroyContainer
|
||||
.calledWith('project-old-container-name', 'old-container-id')
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should not destroy new containers', function () {
|
||||
return this.DockerRunner.destroyContainer
|
||||
.calledWith('project-new-container-name', 'new-container-id')
|
||||
.should.equal(false)
|
||||
})
|
||||
|
||||
it('should not destroy non-project containers', function () {
|
||||
return this.DockerRunner.destroyContainer
|
||||
.calledWith('totally-not-a-project-container', 'some-random-id')
|
||||
.should.equal(false)
|
||||
})
|
||||
|
||||
return it('should callback the callback', function () {
|
||||
return this.callback.called.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('_destroyContainer', function () {
|
||||
beforeEach(function () {
|
||||
this.containerId = 'some_id'
|
||||
this.fakeContainer = { remove: sinon.stub().callsArgWith(1, null) }
|
||||
return (this.Docker.prototype.getContainer = sinon
|
||||
.stub()
|
||||
.returns(this.fakeContainer))
|
||||
})
|
||||
|
||||
it('should get the container', function (done) {
|
||||
return this.DockerRunner._destroyContainer(
|
||||
this.containerId,
|
||||
false,
|
||||
err => {
|
||||
if (err) return done(err)
|
||||
this.Docker.prototype.getContainer.callCount.should.equal(1)
|
||||
this.Docker.prototype.getContainer
|
||||
.calledWith(this.containerId)
|
||||
.should.equal(true)
|
||||
return done()
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('should try to force-destroy the container when shouldForce=true', function (done) {
|
||||
return this.DockerRunner._destroyContainer(
|
||||
this.containerId,
|
||||
true,
|
||||
err => {
|
||||
if (err) return done(err)
|
||||
this.fakeContainer.remove.callCount.should.equal(1)
|
||||
this.fakeContainer.remove
|
||||
.calledWith({ force: true, v: true })
|
||||
.should.equal(true)
|
||||
return done()
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('should not try to force-destroy the container when shouldForce=false', function (done) {
|
||||
return this.DockerRunner._destroyContainer(
|
||||
this.containerId,
|
||||
false,
|
||||
err => {
|
||||
if (err) return done(err)
|
||||
this.fakeContainer.remove.callCount.should.equal(1)
|
||||
this.fakeContainer.remove
|
||||
.calledWith({ force: false, v: true })
|
||||
.should.equal(true)
|
||||
return done()
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('should not produce an error', function (done) {
|
||||
return this.DockerRunner._destroyContainer(
|
||||
this.containerId,
|
||||
false,
|
||||
err => {
|
||||
expect(err).to.equal(null)
|
||||
return done()
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
describe('when the container is already gone', function () {
|
||||
beforeEach(function () {
|
||||
this.fakeError = new Error('woops')
|
||||
this.fakeError.statusCode = 404
|
||||
this.fakeContainer = {
|
||||
remove: sinon.stub().callsArgWith(1, this.fakeError),
|
||||
}
|
||||
return (this.Docker.prototype.getContainer = sinon
|
||||
.stub()
|
||||
.returns(this.fakeContainer))
|
||||
})
|
||||
|
||||
return it('should not produce an error', function (done) {
|
||||
return this.DockerRunner._destroyContainer(
|
||||
this.containerId,
|
||||
false,
|
||||
err => {
|
||||
expect(err).to.equal(null)
|
||||
return done()
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
return describe('when container.destroy produces an error', function (done) {
|
||||
beforeEach(function () {
|
||||
this.fakeError = new Error('woops')
|
||||
this.fakeError.statusCode = 500
|
||||
this.fakeContainer = {
|
||||
remove: sinon.stub().callsArgWith(1, this.fakeError),
|
||||
}
|
||||
return (this.Docker.prototype.getContainer = sinon
|
||||
.stub()
|
||||
.returns(this.fakeContainer))
|
||||
})
|
||||
|
||||
return it('should produce an error', function (done) {
|
||||
return this.DockerRunner._destroyContainer(
|
||||
this.containerId,
|
||||
false,
|
||||
err => {
|
||||
expect(err).to.not.equal(null)
|
||||
expect(err).to.equal(this.fakeError)
|
||||
return done()
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
return describe('kill', function () {
|
||||
beforeEach(function () {
|
||||
this.containerId = 'some_id'
|
||||
this.fakeContainer = { kill: sinon.stub().callsArgWith(0, null) }
|
||||
return (this.Docker.prototype.getContainer = sinon
|
||||
.stub()
|
||||
.returns(this.fakeContainer))
|
||||
})
|
||||
|
||||
it('should get the container', function (done) {
|
||||
return this.DockerRunner.kill(this.containerId, err => {
|
||||
if (err) return done(err)
|
||||
this.Docker.prototype.getContainer.callCount.should.equal(1)
|
||||
this.Docker.prototype.getContainer
|
||||
.calledWith(this.containerId)
|
||||
.should.equal(true)
|
||||
return done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should try to force-destroy the container', function (done) {
|
||||
return this.DockerRunner.kill(this.containerId, err => {
|
||||
if (err) return done(err)
|
||||
this.fakeContainer.kill.callCount.should.equal(1)
|
||||
return done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should not produce an error', function (done) {
|
||||
return this.DockerRunner.kill(this.containerId, err => {
|
||||
expect(err).to.equal(undefined)
|
||||
return done()
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the container is not actually running', function () {
|
||||
beforeEach(function () {
|
||||
this.fakeError = new Error('woops')
|
||||
this.fakeError.statusCode = 500
|
||||
this.fakeError.message =
|
||||
'Cannot kill container <whatever> is not running'
|
||||
this.fakeContainer = {
|
||||
kill: sinon.stub().callsArgWith(0, this.fakeError),
|
||||
}
|
||||
return (this.Docker.prototype.getContainer = sinon
|
||||
.stub()
|
||||
.returns(this.fakeContainer))
|
||||
})
|
||||
|
||||
return it('should not produce an error', function (done) {
|
||||
return this.DockerRunner.kill(this.containerId, err => {
|
||||
expect(err).to.equal(undefined)
|
||||
return done()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
return describe('when container.kill produces a legitimate error', function (done) {
|
||||
beforeEach(function () {
|
||||
this.fakeError = new Error('woops')
|
||||
this.fakeError.statusCode = 500
|
||||
this.fakeError.message = 'Totally legitimate reason to throw an error'
|
||||
this.fakeContainer = {
|
||||
kill: sinon.stub().callsArgWith(0, this.fakeError),
|
||||
}
|
||||
return (this.Docker.prototype.getContainer = sinon
|
||||
.stub()
|
||||
.returns(this.fakeContainer))
|
||||
})
|
||||
|
||||
return it('should produce an error', function (done) {
|
||||
return this.DockerRunner.kill(this.containerId, err => {
|
||||
expect(err).to.not.equal(undefined)
|
||||
expect(err).to.equal(this.fakeError)
|
||||
return done()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
44
services/clsi/test/unit/js/DraftModeManagerTests.js
Normal file
44
services/clsi/test/unit/js/DraftModeManagerTests.js
Normal file
@@ -0,0 +1,44 @@
|
||||
const Path = require('node:path')
|
||||
const fsPromises = require('node:fs/promises')
|
||||
const { expect } = require('chai')
|
||||
const mockFs = require('mock-fs')
|
||||
const SandboxedModule = require('sandboxed-module')
|
||||
|
||||
const MODULE_PATH = Path.join(__dirname, '../../../app/js/DraftModeManager')
|
||||
|
||||
describe('DraftModeManager', function () {
|
||||
beforeEach(function () {
|
||||
this.DraftModeManager = SandboxedModule.require(MODULE_PATH, {
|
||||
requires: {
|
||||
'fs/promises': fsPromises,
|
||||
},
|
||||
})
|
||||
this.filename = '/mock/filename.tex'
|
||||
this.contents = `\
|
||||
\\documentclass{article}
|
||||
\\begin{document}
|
||||
Hello world
|
||||
\\end{document}\
|
||||
`
|
||||
mockFs({
|
||||
[this.filename]: this.contents,
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
mockFs.restore()
|
||||
})
|
||||
|
||||
describe('injectDraftMode', function () {
|
||||
it('prepends a special command to the beginning of the file', async function () {
|
||||
await this.DraftModeManager.promises.injectDraftMode(this.filename)
|
||||
const contents = await fsPromises.readFile(this.filename, {
|
||||
encoding: 'utf8',
|
||||
})
|
||||
expect(contents).to.equal(
|
||||
'\\PassOptionsToPackage{draft}{graphicx}\\PassOptionsToPackage{draft}{graphics}' +
|
||||
this.contents
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
219
services/clsi/test/unit/js/LatexRunnerTests.js
Normal file
219
services/clsi/test/unit/js/LatexRunnerTests.js
Normal file
@@ -0,0 +1,219 @@
|
||||
const SandboxedModule = require('sandboxed-module')
|
||||
const sinon = require('sinon')
|
||||
const { expect } = require('chai')
|
||||
|
||||
const MODULE_PATH = require('node:path').join(
|
||||
__dirname,
|
||||
'../../../app/js/LatexRunner'
|
||||
)
|
||||
|
||||
describe('LatexRunner', function () {
|
||||
beforeEach(function () {
|
||||
this.Settings = {
|
||||
docker: {
|
||||
socketPath: '/var/run/docker.sock',
|
||||
},
|
||||
}
|
||||
this.commandRunnerOutput = {
|
||||
stdout: 'this is stdout',
|
||||
stderr: 'this is stderr',
|
||||
}
|
||||
this.CommandRunner = {
|
||||
run: sinon.stub().yields(null, this.commandRunnerOutput),
|
||||
}
|
||||
this.fs = {
|
||||
writeFile: sinon.stub().yields(),
|
||||
unlink: sinon
|
||||
.stub()
|
||||
.yields(new Error('ENOENT: no such file or directory, unlink ...')),
|
||||
}
|
||||
this.LatexRunner = SandboxedModule.require(MODULE_PATH, {
|
||||
requires: {
|
||||
'@overleaf/settings': this.Settings,
|
||||
'./CommandRunner': this.CommandRunner,
|
||||
fs: this.fs,
|
||||
},
|
||||
})
|
||||
|
||||
this.directory = '/local/compile/directory'
|
||||
this.mainFile = 'main-file.tex'
|
||||
this.compiler = 'pdflatex'
|
||||
this.image = 'example.com/image'
|
||||
this.compileGroup = 'compile-group'
|
||||
this.callback = sinon.stub()
|
||||
this.project_id = 'project-id-123'
|
||||
this.env = { foo: '123' }
|
||||
this.timeout = 42000
|
||||
this.flags = []
|
||||
this.stopOnFirstError = false
|
||||
this.stats = {}
|
||||
this.timings = {}
|
||||
|
||||
this.call = function (callback) {
|
||||
this.LatexRunner.runLatex(
|
||||
this.project_id,
|
||||
{
|
||||
directory: this.directory,
|
||||
mainFile: this.mainFile,
|
||||
compiler: this.compiler,
|
||||
timeout: this.timeout,
|
||||
image: this.image,
|
||||
environment: this.env,
|
||||
compileGroup: this.compileGroup,
|
||||
flags: this.flags,
|
||||
stopOnFirstError: this.stopOnFirstError,
|
||||
timings: this.timings,
|
||||
stats: this.stats,
|
||||
},
|
||||
callback
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
describe('runLatex', function () {
|
||||
describe('normally', function () {
|
||||
beforeEach(function (done) {
|
||||
this.call(done)
|
||||
})
|
||||
|
||||
it('should run the latex command', function () {
|
||||
this.CommandRunner.run.should.have.been.calledWith(
|
||||
this.project_id,
|
||||
[
|
||||
'latexmk',
|
||||
'-cd',
|
||||
'-jobname=output',
|
||||
'-auxdir=$COMPILE_DIR',
|
||||
'-outdir=$COMPILE_DIR',
|
||||
'-synctex=1',
|
||||
'-interaction=batchmode',
|
||||
'-f',
|
||||
'-pdf',
|
||||
'$COMPILE_DIR/main-file.tex',
|
||||
],
|
||||
this.directory,
|
||||
this.image,
|
||||
this.timeout,
|
||||
this.env,
|
||||
this.compileGroup
|
||||
)
|
||||
})
|
||||
|
||||
it('should record the stdout and stderr', function () {
|
||||
this.fs.writeFile.should.have.been.calledWith(
|
||||
this.directory + '/' + 'output.stdout',
|
||||
'this is stdout',
|
||||
{ flag: 'wx' }
|
||||
)
|
||||
this.fs.writeFile.should.have.been.calledWith(
|
||||
this.directory + '/' + 'output.stderr',
|
||||
'this is stderr',
|
||||
{ flag: 'wx' }
|
||||
)
|
||||
this.fs.unlink.should.have.been.calledWith(
|
||||
this.directory + '/' + 'output.stdout'
|
||||
)
|
||||
this.fs.unlink.should.have.been.calledWith(
|
||||
this.directory + '/' + 'output.stderr'
|
||||
)
|
||||
})
|
||||
|
||||
it('should not record cpu metrics', function () {
|
||||
expect(this.timings['cpu-percent']).to.not.exist
|
||||
expect(this.timings['cpu-time']).to.not.exist
|
||||
expect(this.timings['sys-time']).to.not.exist
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a different compiler', function () {
|
||||
beforeEach(function (done) {
|
||||
this.compiler = 'lualatex'
|
||||
this.call(done)
|
||||
})
|
||||
|
||||
it('should set the appropriate latexmk flag', function () {
|
||||
this.CommandRunner.run.should.have.been.calledWith(this.project_id, [
|
||||
'latexmk',
|
||||
'-cd',
|
||||
'-jobname=output',
|
||||
'-auxdir=$COMPILE_DIR',
|
||||
'-outdir=$COMPILE_DIR',
|
||||
'-synctex=1',
|
||||
'-interaction=batchmode',
|
||||
'-f',
|
||||
'-lualatex',
|
||||
'$COMPILE_DIR/main-file.tex',
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('with time -v', function () {
|
||||
beforeEach(function (done) {
|
||||
this.commandRunnerOutput.stderr =
|
||||
'\tCommand being timed: "sh -c timeout 1 yes > /dev/null"\n' +
|
||||
'\tUser time (seconds): 0.28\n' +
|
||||
'\tSystem time (seconds): 0.70\n' +
|
||||
'\tPercent of CPU this job got: 98%\n'
|
||||
this.call(done)
|
||||
})
|
||||
|
||||
it('should record cpu metrics', function () {
|
||||
expect(this.timings['cpu-percent']).to.equal(98)
|
||||
expect(this.timings['cpu-time']).to.equal(0.28)
|
||||
expect(this.timings['sys-time']).to.equal(0.7)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with an .Rtex main file', function () {
|
||||
beforeEach(function (done) {
|
||||
this.mainFile = 'main-file.Rtex'
|
||||
this.call(done)
|
||||
})
|
||||
|
||||
it('should run the latex command on the equivalent .tex file', function () {
|
||||
const command = this.CommandRunner.run.args[0][1]
|
||||
const mainFile = command.slice(-1)[0]
|
||||
mainFile.should.equal('$COMPILE_DIR/main-file.tex')
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a flags option', function () {
|
||||
beforeEach(function (done) {
|
||||
this.flags = ['-shell-restricted', '-halt-on-error']
|
||||
this.call(done)
|
||||
})
|
||||
|
||||
it('should include the flags in the command', function () {
|
||||
const command = this.CommandRunner.run.args[0][1]
|
||||
const flags = command.filter(
|
||||
arg => arg === '-shell-restricted' || arg === '-halt-on-error'
|
||||
)
|
||||
flags.length.should.equal(2)
|
||||
flags[0].should.equal('-shell-restricted')
|
||||
flags[1].should.equal('-halt-on-error')
|
||||
})
|
||||
})
|
||||
|
||||
describe('with the stopOnFirstError option', function () {
|
||||
beforeEach(function (done) {
|
||||
this.stopOnFirstError = true
|
||||
this.call(done)
|
||||
})
|
||||
|
||||
it('should set the appropriate flags', function () {
|
||||
this.CommandRunner.run.should.have.been.calledWith(this.project_id, [
|
||||
'latexmk',
|
||||
'-cd',
|
||||
'-jobname=output',
|
||||
'-auxdir=$COMPILE_DIR',
|
||||
'-outdir=$COMPILE_DIR',
|
||||
'-synctex=1',
|
||||
'-interaction=batchmode',
|
||||
'-halt-on-error',
|
||||
'-pdf',
|
||||
'$COMPILE_DIR/main-file.tex',
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
116
services/clsi/test/unit/js/LockManagerTests.js
Normal file
116
services/clsi/test/unit/js/LockManagerTests.js
Normal file
@@ -0,0 +1,116 @@
|
||||
const { expect } = require('chai')
|
||||
const sinon = require('sinon')
|
||||
const SandboxedModule = require('sandboxed-module')
|
||||
const modulePath = require('node:path').join(
|
||||
__dirname,
|
||||
'../../../app/js/LockManager'
|
||||
)
|
||||
const Errors = require('../../../app/js/Errors')
|
||||
|
||||
describe('LockManager', function () {
|
||||
beforeEach(function () {
|
||||
this.key = '/local/compile/directory'
|
||||
this.clock = sinon.useFakeTimers()
|
||||
this.LockManager = SandboxedModule.require(modulePath, {
|
||||
requires: {
|
||||
'@overleaf/metrics': (this.Metrics = {
|
||||
inc: sinon.stub(),
|
||||
gauge: sinon.stub(),
|
||||
}),
|
||||
'@overleaf/settings': (this.Settings = {
|
||||
compileConcurrencyLimit: 5,
|
||||
}),
|
||||
'./Errors': (this.Erros = Errors),
|
||||
'./RequestParser': { MAX_TIMEOUT: 600 },
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
this.clock.restore()
|
||||
})
|
||||
|
||||
describe('when the lock is available', function () {
|
||||
it('the lock can be acquired', function () {
|
||||
const lock = this.LockManager.acquire(this.key)
|
||||
expect(lock).to.exist
|
||||
lock.release()
|
||||
})
|
||||
})
|
||||
|
||||
describe('after the lock is acquired', function () {
|
||||
beforeEach(function () {
|
||||
this.lock = this.LockManager.acquire(this.key)
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
if (this.lock != null) {
|
||||
this.lock.release()
|
||||
}
|
||||
})
|
||||
|
||||
it("the lock can't be acquired again", function () {
|
||||
expect(() => this.LockManager.acquire(this.key)).to.throw(
|
||||
Errors.AlreadyCompilingError
|
||||
)
|
||||
})
|
||||
|
||||
it('another lock can be acquired', function () {
|
||||
const lock = this.LockManager.acquire('another key')
|
||||
expect(lock).to.exist
|
||||
lock.release()
|
||||
})
|
||||
|
||||
it('the lock can be acquired again after an expiry period', function () {
|
||||
// The expiry time is a little bit over 10 minutes. Let's wait 15 minutes.
|
||||
this.clock.tick(15 * 60 * 1000)
|
||||
this.lock = this.LockManager.acquire(this.key)
|
||||
expect(this.lock).to.exist
|
||||
})
|
||||
|
||||
it('the lock can be acquired again after it was released', function () {
|
||||
this.lock.release()
|
||||
this.lock = this.LockManager.acquire(this.key)
|
||||
expect(this.lock).to.exist
|
||||
})
|
||||
})
|
||||
|
||||
describe('concurrency limit', function () {
|
||||
it('exceeding the limit', function () {
|
||||
for (let i = 0; i <= this.Settings.compileConcurrencyLimit; i++) {
|
||||
this.LockManager.acquire('test_key' + i)
|
||||
}
|
||||
this.Metrics.inc
|
||||
.calledWith('exceeded-compilier-concurrency-limit')
|
||||
.should.equal(false)
|
||||
expect(() =>
|
||||
this.LockManager.acquire(
|
||||
'test_key_' + (this.Settings.compileConcurrencyLimit + 1),
|
||||
false
|
||||
)
|
||||
).to.throw(Errors.TooManyCompileRequestsError)
|
||||
|
||||
this.Metrics.inc
|
||||
.calledWith('exceeded-compilier-concurrency-limit')
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('within the limit', function () {
|
||||
for (let i = 0; i <= this.Settings.compileConcurrencyLimit - 1; i++) {
|
||||
this.LockManager.acquire('test_key' + i)
|
||||
}
|
||||
this.Metrics.inc
|
||||
.calledWith('exceeded-compilier-concurrency-limit')
|
||||
.should.equal(false)
|
||||
|
||||
const lock = this.LockManager.acquire(
|
||||
'test_key_' + this.Settings.compileConcurrencyLimit,
|
||||
false
|
||||
)
|
||||
|
||||
expect(lock.key).to.equal(
|
||||
'test_key_' + this.Settings.compileConcurrencyLimit
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
105
services/clsi/test/unit/js/OutputControllerTests.js
Normal file
105
services/clsi/test/unit/js/OutputControllerTests.js
Normal file
@@ -0,0 +1,105 @@
|
||||
const SandboxedModule = require('sandboxed-module')
|
||||
const sinon = require('sinon')
|
||||
const MODULE_PATH = require('node:path').join(
|
||||
__dirname,
|
||||
'../../../app/js/OutputController'
|
||||
)
|
||||
|
||||
describe('OutputController', function () {
|
||||
describe('createOutputZip', function () {
|
||||
beforeEach(function () {
|
||||
this.archive = {}
|
||||
|
||||
this.pipeline = sinon.stub().resolves()
|
||||
|
||||
this.archiveFilesForBuild = sinon.stub().resolves(this.archive)
|
||||
|
||||
this.OutputController = SandboxedModule.require(MODULE_PATH, {
|
||||
requires: {
|
||||
'./OutputFileArchiveManager': {
|
||||
archiveFilesForBuild: this.archiveFilesForBuild,
|
||||
},
|
||||
'stream/promises': {
|
||||
pipeline: this.pipeline,
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
describe('when OutputFileArchiveManager creates an archive', function () {
|
||||
beforeEach(function (done) {
|
||||
this.res = {
|
||||
attachment: sinon.stub(),
|
||||
setHeader: sinon.stub(),
|
||||
}
|
||||
this.req = {
|
||||
params: {
|
||||
project_id: 'project-id-123',
|
||||
user_id: 'user-id-123',
|
||||
build_id: 'build-id-123',
|
||||
},
|
||||
query: {
|
||||
files: ['output.tex'],
|
||||
},
|
||||
}
|
||||
this.pipeline.callsFake(() => {
|
||||
done()
|
||||
return Promise.resolve()
|
||||
})
|
||||
this.OutputController.createOutputZip(this.req, this.res)
|
||||
})
|
||||
|
||||
it('creates a pipeline from the archive to the response', function () {
|
||||
sinon.assert.calledWith(this.pipeline, this.archive, this.res)
|
||||
})
|
||||
|
||||
it('calls the express convenience method to set attachment headers', function () {
|
||||
sinon.assert.calledWith(this.res.attachment, 'output.zip')
|
||||
})
|
||||
|
||||
it('sets the X-Content-Type-Options header to nosniff', function () {
|
||||
sinon.assert.calledWith(
|
||||
this.res.setHeader,
|
||||
'X-Content-Type-Options',
|
||||
'nosniff'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when OutputFileArchiveManager throws an error', function () {
|
||||
let error
|
||||
|
||||
beforeEach(function (done) {
|
||||
error = new Error('error message')
|
||||
|
||||
this.archiveFilesForBuild.rejects(error)
|
||||
|
||||
this.res = {
|
||||
status: sinon.stub().returnsThis(),
|
||||
send: sinon.stub(),
|
||||
}
|
||||
this.req = {
|
||||
params: {
|
||||
project_id: 'project-id-123',
|
||||
user_id: 'user-id-123',
|
||||
build_id: 'build-id-123',
|
||||
},
|
||||
query: {
|
||||
files: ['output.tex'],
|
||||
},
|
||||
}
|
||||
this.OutputController.createOutputZip(
|
||||
this.req,
|
||||
this.res,
|
||||
(this.next = sinon.stub().callsFake(() => {
|
||||
done()
|
||||
}))
|
||||
)
|
||||
})
|
||||
|
||||
it('calls next with the error', function () {
|
||||
sinon.assert.calledWith(this.next, error)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
229
services/clsi/test/unit/js/OutputFileArchiveManagerTests.js
Normal file
229
services/clsi/test/unit/js/OutputFileArchiveManagerTests.js
Normal file
@@ -0,0 +1,229 @@
|
||||
const SandboxedModule = require('sandboxed-module')
|
||||
const sinon = require('sinon')
|
||||
const { assert, expect } = require('chai')
|
||||
|
||||
const MODULE_PATH = require('node:path').join(
|
||||
__dirname,
|
||||
'../../../app/js/OutputFileArchiveManager'
|
||||
)
|
||||
|
||||
describe('OutputFileArchiveManager', function () {
|
||||
const userId = 'user-id-123'
|
||||
const projectId = 'project-id-123'
|
||||
const buildId = 'build-id-123'
|
||||
|
||||
afterEach(function () {
|
||||
sinon.restore()
|
||||
})
|
||||
|
||||
beforeEach(function () {
|
||||
this.OutputFileFinder = {
|
||||
promises: {
|
||||
findOutputFiles: sinon.stub().resolves({ outputFiles: [] }),
|
||||
},
|
||||
}
|
||||
|
||||
this.OutputCacheManger = {
|
||||
path: sinon.stub().callsFake((build, path) => {
|
||||
return `${build}/${path}`
|
||||
}),
|
||||
}
|
||||
|
||||
this.archive = {
|
||||
append: sinon.stub(),
|
||||
finalize: sinon.stub().resolves(),
|
||||
on: sinon.stub(),
|
||||
}
|
||||
|
||||
this.archiver = sinon.stub().returns(this.archive)
|
||||
|
||||
this.outputDir = '/output/dir'
|
||||
|
||||
this.fs = {
|
||||
open: sinon.stub().callsFake(file => ({
|
||||
createReadStream: sinon.stub().returns(`handle: ${file}`),
|
||||
})),
|
||||
}
|
||||
|
||||
this.OutputFileArchiveManager = SandboxedModule.require(MODULE_PATH, {
|
||||
requires: {
|
||||
'./OutputFileFinder': this.OutputFileFinder,
|
||||
'./OutputCacheManager': this.OutputCacheManger,
|
||||
archiver: this.archiver,
|
||||
'fs/promises': this.fs,
|
||||
'@overleaf/settings': {
|
||||
path: {
|
||||
outputDir: this.outputDir,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the output cache directory contains only exportable files', function () {
|
||||
beforeEach(async function () {
|
||||
this.OutputFileFinder.promises.findOutputFiles.resolves({
|
||||
outputFiles: [
|
||||
{ path: 'file_1' },
|
||||
{ path: 'file_2' },
|
||||
{ path: 'file_3' },
|
||||
{ path: 'file_4' },
|
||||
],
|
||||
})
|
||||
await this.OutputFileArchiveManager.archiveFilesForBuild(
|
||||
projectId,
|
||||
userId,
|
||||
buildId
|
||||
)
|
||||
})
|
||||
|
||||
it('creates a zip archive', function () {
|
||||
sinon.assert.calledWith(this.archiver, 'zip')
|
||||
})
|
||||
|
||||
it('listens to errors from the archive', function () {
|
||||
sinon.assert.calledWith(this.archive.on, 'error', sinon.match.func)
|
||||
})
|
||||
|
||||
it('adds all the output files to the archive', function () {
|
||||
expect(this.archive.append.callCount).to.equal(4)
|
||||
sinon.assert.calledWith(
|
||||
this.archive.append,
|
||||
`handle: ${this.outputDir}/${projectId}-${userId}/${buildId}/file_1`,
|
||||
sinon.match({ name: 'file_1' })
|
||||
)
|
||||
sinon.assert.calledWith(
|
||||
this.archive.append,
|
||||
`handle: ${this.outputDir}/${projectId}-${userId}/${buildId}/file_2`,
|
||||
sinon.match({ name: 'file_2' })
|
||||
)
|
||||
sinon.assert.calledWith(
|
||||
this.archive.append,
|
||||
`handle: ${this.outputDir}/${projectId}-${userId}/${buildId}/file_3`,
|
||||
sinon.match({ name: 'file_3' })
|
||||
)
|
||||
sinon.assert.calledWith(
|
||||
this.archive.append,
|
||||
`handle: ${this.outputDir}/${projectId}-${userId}/${buildId}/file_4`,
|
||||
sinon.match({ name: 'file_4' })
|
||||
)
|
||||
})
|
||||
|
||||
it('finalizes the archive after all files are appended', function () {
|
||||
sinon.assert.called(this.archive.finalize)
|
||||
expect(this.archive.finalize.calledBefore(this.archive.append)).to.be
|
||||
.false
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the directory includes files ignored by web', function () {
|
||||
beforeEach(async function () {
|
||||
this.OutputFileFinder.promises.findOutputFiles.resolves({
|
||||
outputFiles: [
|
||||
{ path: 'file_1' },
|
||||
{ path: 'file_2' },
|
||||
{ path: 'file_3' },
|
||||
{ path: 'file_4' },
|
||||
{ path: 'output.pdf' },
|
||||
],
|
||||
})
|
||||
await this.OutputFileArchiveManager.archiveFilesForBuild(
|
||||
projectId,
|
||||
userId,
|
||||
buildId
|
||||
)
|
||||
})
|
||||
|
||||
it('only includes the non-ignored files in the archive', function () {
|
||||
expect(this.archive.append.callCount).to.equal(4)
|
||||
sinon.assert.calledWith(
|
||||
this.archive.append,
|
||||
`handle: ${this.outputDir}/${projectId}-${userId}/${buildId}/file_1`,
|
||||
sinon.match({ name: 'file_1' })
|
||||
)
|
||||
sinon.assert.calledWith(
|
||||
this.archive.append,
|
||||
`handle: ${this.outputDir}/${projectId}-${userId}/${buildId}/file_2`,
|
||||
sinon.match({ name: 'file_2' })
|
||||
)
|
||||
sinon.assert.calledWith(
|
||||
this.archive.append,
|
||||
`handle: ${this.outputDir}/${projectId}-${userId}/${buildId}/file_3`,
|
||||
sinon.match({ name: 'file_3' })
|
||||
)
|
||||
sinon.assert.calledWith(
|
||||
this.archive.append,
|
||||
`handle: ${this.outputDir}/${projectId}-${userId}/${buildId}/file_4`,
|
||||
sinon.match({ name: 'file_4' })
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when one of the files is called output.pdf', function () {
|
||||
beforeEach(async function () {
|
||||
this.OutputFileFinder.promises.findOutputFiles.resolves({
|
||||
outputFiles: [
|
||||
{ path: 'file_1' },
|
||||
{ path: 'file_2' },
|
||||
{ path: 'file_3' },
|
||||
{ path: 'file_4' },
|
||||
{ path: 'output.pdf' },
|
||||
],
|
||||
})
|
||||
await this.OutputFileArchiveManager.archiveFilesForBuild(
|
||||
projectId,
|
||||
userId,
|
||||
buildId
|
||||
)
|
||||
})
|
||||
|
||||
it('does not include that file in the archive', function () {
|
||||
expect(this.archive.append.callCount).to.equal(4)
|
||||
sinon.assert.calledWith(
|
||||
this.archive.append,
|
||||
`handle: ${this.outputDir}/${projectId}-${userId}/${buildId}/file_1`,
|
||||
sinon.match({ name: 'file_1' })
|
||||
)
|
||||
sinon.assert.calledWith(
|
||||
this.archive.append,
|
||||
`handle: ${this.outputDir}/${projectId}-${userId}/${buildId}/file_2`,
|
||||
sinon.match({ name: 'file_2' })
|
||||
)
|
||||
sinon.assert.calledWith(
|
||||
this.archive.append,
|
||||
`handle: ${this.outputDir}/${projectId}-${userId}/${buildId}/file_3`,
|
||||
sinon.match({ name: 'file_3' })
|
||||
)
|
||||
sinon.assert.calledWith(
|
||||
this.archive.append,
|
||||
`handle: ${this.outputDir}/${projectId}-${userId}/${buildId}/file_4`,
|
||||
sinon.match({ name: 'file_4' })
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the output directory cannot be accessed', function () {
|
||||
beforeEach(async function () {
|
||||
this.OutputFileFinder.promises.findOutputFiles.rejects({
|
||||
code: 'ENOENT',
|
||||
})
|
||||
})
|
||||
|
||||
it('rejects with a NotFoundError', async function () {
|
||||
try {
|
||||
await this.OutputFileArchiveManager.archiveFilesForBuild(
|
||||
projectId,
|
||||
userId,
|
||||
buildId
|
||||
)
|
||||
assert.fail('should have thrown a NotFoundError')
|
||||
} catch (err) {
|
||||
expect(err).to.haveOwnProperty('name', 'NotFoundError')
|
||||
}
|
||||
})
|
||||
|
||||
it('does not create an archive', function () {
|
||||
expect(this.archiver.called).to.be.false
|
||||
})
|
||||
})
|
||||
})
|
72
services/clsi/test/unit/js/OutputFileFinderTests.js
Normal file
72
services/clsi/test/unit/js/OutputFileFinderTests.js
Normal file
@@ -0,0 +1,72 @@
|
||||
const SandboxedModule = require('sandboxed-module')
|
||||
const sinon = require('sinon')
|
||||
const modulePath = require('node:path').join(
|
||||
__dirname,
|
||||
'../../../app/js/OutputFileFinder'
|
||||
)
|
||||
const { expect } = require('chai')
|
||||
const mockFs = require('mock-fs')
|
||||
|
||||
describe('OutputFileFinder', function () {
|
||||
beforeEach(function () {
|
||||
this.OutputFileFinder = SandboxedModule.require(modulePath, {})
|
||||
this.directory = '/test/dir'
|
||||
this.callback = sinon.stub()
|
||||
|
||||
mockFs({
|
||||
[this.directory]: {
|
||||
resource: {
|
||||
'path.tex': 'a source file',
|
||||
},
|
||||
'output.pdf': 'a generated pdf file',
|
||||
extra: {
|
||||
'file.tex': 'a generated tex file',
|
||||
},
|
||||
'sneaky-file': mockFs.symlink({
|
||||
path: '../foo',
|
||||
}),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
mockFs.restore()
|
||||
})
|
||||
|
||||
describe('findOutputFiles', function () {
|
||||
beforeEach(async function () {
|
||||
this.resource_path = 'resource/path.tex'
|
||||
this.output_paths = ['output.pdf', 'extra/file.tex']
|
||||
this.all_paths = this.output_paths.concat([this.resource_path])
|
||||
this.resources = [{ path: (this.resource_path = 'resource/path.tex') }]
|
||||
const { outputFiles, allEntries } =
|
||||
await this.OutputFileFinder.promises.findOutputFiles(
|
||||
this.resources,
|
||||
this.directory
|
||||
)
|
||||
this.outputFiles = outputFiles
|
||||
this.allEntries = allEntries
|
||||
})
|
||||
|
||||
it('should only return the output files, not directories or resource paths', function () {
|
||||
expect(this.outputFiles).to.have.deep.members([
|
||||
{
|
||||
path: 'output.pdf',
|
||||
type: 'pdf',
|
||||
},
|
||||
{
|
||||
path: 'extra/file.tex',
|
||||
type: 'tex',
|
||||
},
|
||||
])
|
||||
expect(this.allEntries).to.deep.equal([
|
||||
'extra/file.tex',
|
||||
'extra/',
|
||||
'output.pdf',
|
||||
'resource/path.tex',
|
||||
'resource/',
|
||||
'sneaky-file',
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
192
services/clsi/test/unit/js/OutputFileOptimiserTests.js
Normal file
192
services/clsi/test/unit/js/OutputFileOptimiserTests.js
Normal file
@@ -0,0 +1,192 @@
|
||||
/* eslint-disable
|
||||
no-return-assign,
|
||||
no-unused-vars,
|
||||
n/no-deprecated-api,
|
||||
*/
|
||||
// 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 SandboxedModule = require('sandboxed-module')
|
||||
const sinon = require('sinon')
|
||||
const modulePath = require('node:path').join(
|
||||
__dirname,
|
||||
'../../../app/js/OutputFileOptimiser'
|
||||
)
|
||||
const path = require('node:path')
|
||||
const { expect } = require('chai')
|
||||
const { EventEmitter } = require('node:events')
|
||||
|
||||
describe('OutputFileOptimiser', function () {
|
||||
beforeEach(function () {
|
||||
this.OutputFileOptimiser = SandboxedModule.require(modulePath, {
|
||||
requires: {
|
||||
fs: (this.fs = {}),
|
||||
path: (this.Path = {}),
|
||||
child_process: { spawn: (this.spawn = sinon.stub()) },
|
||||
'./Metrics': {},
|
||||
},
|
||||
globals: { Math }, // used by lodash
|
||||
})
|
||||
this.directory = '/test/dir'
|
||||
return (this.callback = sinon.stub())
|
||||
})
|
||||
|
||||
describe('optimiseFile', function () {
|
||||
beforeEach(function () {
|
||||
this.src = './output.pdf'
|
||||
return (this.dst = './output.pdf')
|
||||
})
|
||||
|
||||
describe('when the file is not a pdf file', function () {
|
||||
beforeEach(function (done) {
|
||||
this.src = './output.log'
|
||||
this.OutputFileOptimiser.checkIfPDFIsOptimised = sinon
|
||||
.stub()
|
||||
.callsArgWith(1, null, false)
|
||||
this.OutputFileOptimiser.optimisePDF = sinon
|
||||
.stub()
|
||||
.callsArgWith(2, null)
|
||||
return this.OutputFileOptimiser.optimiseFile(this.src, this.dst, done)
|
||||
})
|
||||
|
||||
it('should not check if the file is optimised', function () {
|
||||
return this.OutputFileOptimiser.checkIfPDFIsOptimised
|
||||
.calledWith(this.src)
|
||||
.should.equal(false)
|
||||
})
|
||||
|
||||
return it('should not optimise the file', function () {
|
||||
return this.OutputFileOptimiser.optimisePDF
|
||||
.calledWith(this.src, this.dst)
|
||||
.should.equal(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the pdf file is not optimised', function () {
|
||||
beforeEach(function (done) {
|
||||
this.OutputFileOptimiser.checkIfPDFIsOptimised = sinon
|
||||
.stub()
|
||||
.callsArgWith(1, null, false)
|
||||
this.OutputFileOptimiser.optimisePDF = sinon
|
||||
.stub()
|
||||
.callsArgWith(2, null)
|
||||
return this.OutputFileOptimiser.optimiseFile(this.src, this.dst, done)
|
||||
})
|
||||
|
||||
it('should check if the pdf is optimised', function () {
|
||||
return this.OutputFileOptimiser.checkIfPDFIsOptimised
|
||||
.calledWith(this.src)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
return it('should optimise the pdf', function () {
|
||||
return this.OutputFileOptimiser.optimisePDF
|
||||
.calledWith(this.src, this.dst)
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
return describe('when the pdf file is optimised', function () {
|
||||
beforeEach(function (done) {
|
||||
this.OutputFileOptimiser.checkIfPDFIsOptimised = sinon
|
||||
.stub()
|
||||
.callsArgWith(1, null, true)
|
||||
this.OutputFileOptimiser.optimisePDF = sinon
|
||||
.stub()
|
||||
.callsArgWith(2, null)
|
||||
return this.OutputFileOptimiser.optimiseFile(this.src, this.dst, done)
|
||||
})
|
||||
|
||||
it('should check if the pdf is optimised', function () {
|
||||
return this.OutputFileOptimiser.checkIfPDFIsOptimised
|
||||
.calledWith(this.src)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
return it('should not optimise the pdf', function () {
|
||||
return this.OutputFileOptimiser.optimisePDF
|
||||
.calledWith(this.src, this.dst)
|
||||
.should.equal(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
return describe('checkIfPDFISOptimised', function () {
|
||||
beforeEach(function () {
|
||||
this.callback = sinon.stub()
|
||||
this.fd = 1234
|
||||
this.fs.open = sinon.stub().yields(null, this.fd)
|
||||
this.fs.read = sinon
|
||||
.stub()
|
||||
.withArgs(this.fd)
|
||||
.yields(null, 100, Buffer.from('hello /Linearized 1'))
|
||||
this.fs.close = sinon.stub().withArgs(this.fd).yields(null)
|
||||
return this.OutputFileOptimiser.checkIfPDFIsOptimised(
|
||||
this.src,
|
||||
this.callback
|
||||
)
|
||||
})
|
||||
|
||||
describe('for a linearised file', function () {
|
||||
beforeEach(function () {
|
||||
this.fs.read = sinon
|
||||
.stub()
|
||||
.withArgs(this.fd)
|
||||
.yields(null, 100, Buffer.from('hello /Linearized 1'))
|
||||
return this.OutputFileOptimiser.checkIfPDFIsOptimised(
|
||||
this.src,
|
||||
this.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should open the file', function () {
|
||||
return this.fs.open.calledWith(this.src, 'r').should.equal(true)
|
||||
})
|
||||
|
||||
it('should read the header', function () {
|
||||
return this.fs.read.calledWith(this.fd).should.equal(true)
|
||||
})
|
||||
|
||||
it('should close the file', function () {
|
||||
return this.fs.close.calledWith(this.fd).should.equal(true)
|
||||
})
|
||||
|
||||
return it('should call the callback with a true result', function () {
|
||||
return this.callback.calledWith(null, true).should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
return describe('for an unlinearised file', function () {
|
||||
beforeEach(function () {
|
||||
this.fs.read = sinon
|
||||
.stub()
|
||||
.withArgs(this.fd)
|
||||
.yields(null, 100, Buffer.from('hello not linearized 1'))
|
||||
return this.OutputFileOptimiser.checkIfPDFIsOptimised(
|
||||
this.src,
|
||||
this.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should open the file', function () {
|
||||
return this.fs.open.calledWith(this.src, 'r').should.equal(true)
|
||||
})
|
||||
|
||||
it('should read the header', function () {
|
||||
return this.fs.read.calledWith(this.fd).should.equal(true)
|
||||
})
|
||||
|
||||
it('should close the file', function () {
|
||||
return this.fs.close.calledWith(this.fd).should.equal(true)
|
||||
})
|
||||
|
||||
return it('should call the callback with a false result', function () {
|
||||
return this.callback.calledWith(null, false).should.equal(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
174
services/clsi/test/unit/js/ProjectPersistenceManagerTests.js
Normal file
174
services/clsi/test/unit/js/ProjectPersistenceManagerTests.js
Normal file
@@ -0,0 +1,174 @@
|
||||
/* 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
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
const SandboxedModule = require('sandboxed-module')
|
||||
const sinon = require('sinon')
|
||||
const assert = require('chai').assert
|
||||
const modulePath = require('node:path').join(
|
||||
__dirname,
|
||||
'../../../app/js/ProjectPersistenceManager'
|
||||
)
|
||||
const tk = require('timekeeper')
|
||||
|
||||
describe('ProjectPersistenceManager', function () {
|
||||
beforeEach(function () {
|
||||
this.fsPromises = {
|
||||
statfs: sinon.stub(),
|
||||
}
|
||||
|
||||
this.ProjectPersistenceManager = SandboxedModule.require(modulePath, {
|
||||
requires: {
|
||||
'@overleaf/metrics': (this.Metrics = { gauge: sinon.stub() }),
|
||||
'./UrlCache': (this.UrlCache = {}),
|
||||
'./CompileManager': (this.CompileManager = {}),
|
||||
fs: { promises: this.fsPromises },
|
||||
'@overleaf/settings': (this.settings = {
|
||||
project_cache_length_ms: 1000,
|
||||
path: {
|
||||
compilesDir: '/compiles',
|
||||
outputDir: '/output',
|
||||
clsiCacheDir: '/cache',
|
||||
},
|
||||
}),
|
||||
},
|
||||
})
|
||||
this.callback = sinon.stub()
|
||||
this.project_id = 'project-id-123'
|
||||
return (this.user_id = '1234')
|
||||
})
|
||||
|
||||
describe('refreshExpiryTimeout', function () {
|
||||
it('should leave expiry alone if plenty of disk', function (done) {
|
||||
this.fsPromises.statfs.resolves({
|
||||
blocks: 100,
|
||||
bsize: 1,
|
||||
bavail: 40,
|
||||
})
|
||||
|
||||
this.ProjectPersistenceManager.refreshExpiryTimeout(() => {
|
||||
this.Metrics.gauge.should.have.been.calledWith(
|
||||
'disk_available_percent',
|
||||
40
|
||||
)
|
||||
this.ProjectPersistenceManager.EXPIRY_TIMEOUT.should.equal(
|
||||
this.settings.project_cache_length_ms
|
||||
)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should drop EXPIRY_TIMEOUT 10% if low disk usage', function (done) {
|
||||
this.fsPromises.statfs.resolves({
|
||||
blocks: 100,
|
||||
bsize: 1,
|
||||
bavail: 5,
|
||||
})
|
||||
|
||||
this.ProjectPersistenceManager.refreshExpiryTimeout(() => {
|
||||
this.Metrics.gauge.should.have.been.calledWith(
|
||||
'disk_available_percent',
|
||||
5
|
||||
)
|
||||
this.ProjectPersistenceManager.EXPIRY_TIMEOUT.should.equal(900)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should not drop EXPIRY_TIMEOUT to below 50% of project_cache_length_ms', function (done) {
|
||||
this.fsPromises.statfs.resolves({
|
||||
blocks: 100,
|
||||
bsize: 1,
|
||||
bavail: 5,
|
||||
})
|
||||
this.ProjectPersistenceManager.EXPIRY_TIMEOUT = 500
|
||||
this.ProjectPersistenceManager.refreshExpiryTimeout(() => {
|
||||
this.Metrics.gauge.should.have.been.calledWith(
|
||||
'disk_available_percent',
|
||||
5
|
||||
)
|
||||
this.ProjectPersistenceManager.EXPIRY_TIMEOUT.should.equal(500)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should not modify EXPIRY_TIMEOUT if there is an error getting disk values', function (done) {
|
||||
this.fsPromises.statfs.rejects(new Error())
|
||||
this.ProjectPersistenceManager.refreshExpiryTimeout(() => {
|
||||
this.ProjectPersistenceManager.EXPIRY_TIMEOUT.should.equal(1000)
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('clearExpiredProjects', function () {
|
||||
beforeEach(function () {
|
||||
this.project_ids = ['project-id-1', 'project-id-2']
|
||||
this.ProjectPersistenceManager._findExpiredProjectIds = sinon
|
||||
.stub()
|
||||
.callsArgWith(0, null, this.project_ids)
|
||||
this.ProjectPersistenceManager.clearProjectFromCache = sinon
|
||||
.stub()
|
||||
.callsArg(2)
|
||||
this.CompileManager.clearExpiredProjects = sinon.stub().callsArg(1)
|
||||
return this.ProjectPersistenceManager.clearExpiredProjects(this.callback)
|
||||
})
|
||||
|
||||
it('should clear each expired project', function () {
|
||||
return Array.from(this.project_ids).map(projectId =>
|
||||
this.ProjectPersistenceManager.clearProjectFromCache
|
||||
.calledWith(projectId)
|
||||
.should.equal(true)
|
||||
)
|
||||
})
|
||||
|
||||
return it('should call the callback', function () {
|
||||
return this.callback.called.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
return describe('clearProject', function () {
|
||||
beforeEach(function () {
|
||||
this.ProjectPersistenceManager._clearProjectFromDatabase = sinon
|
||||
.stub()
|
||||
.callsArg(1)
|
||||
this.UrlCache.clearProject = sinon.stub().callsArg(2)
|
||||
this.CompileManager.clearProject = sinon.stub().callsArg(2)
|
||||
return this.ProjectPersistenceManager.clearProject(
|
||||
this.project_id,
|
||||
this.user_id,
|
||||
this.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should clear the project from the database', function () {
|
||||
return this.ProjectPersistenceManager._clearProjectFromDatabase
|
||||
.calledWith(this.project_id)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should clear all the cached Urls for the project', function () {
|
||||
return this.UrlCache.clearProject
|
||||
.calledWith(this.project_id)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should clear the project compile folder', function () {
|
||||
return this.CompileManager.clearProject
|
||||
.calledWith(this.project_id, this.user_id)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
return it('should call the callback', function () {
|
||||
return this.callback.called.should.equal(true)
|
||||
})
|
||||
})
|
||||
})
|
480
services/clsi/test/unit/js/RequestParserTests.js
Normal file
480
services/clsi/test/unit/js/RequestParserTests.js
Normal file
@@ -0,0 +1,480 @@
|
||||
const SandboxedModule = require('sandboxed-module')
|
||||
const sinon = require('sinon')
|
||||
const { expect } = require('chai')
|
||||
const modulePath = require('node:path').join(
|
||||
__dirname,
|
||||
'../../../app/js/RequestParser'
|
||||
)
|
||||
const tk = require('timekeeper')
|
||||
|
||||
describe('RequestParser', function () {
|
||||
beforeEach(function () {
|
||||
tk.freeze()
|
||||
this.callback = sinon.stub()
|
||||
this.validResource = {
|
||||
path: 'main.tex',
|
||||
date: '12:00 01/02/03',
|
||||
content: 'Hello world',
|
||||
}
|
||||
this.validRequest = {
|
||||
compile: {
|
||||
token: 'token-123',
|
||||
options: {
|
||||
imageName: 'basicImageName/here:2017-1',
|
||||
compiler: 'pdflatex',
|
||||
timeout: 42,
|
||||
},
|
||||
resources: [],
|
||||
},
|
||||
}
|
||||
this.RequestParser = SandboxedModule.require(modulePath, {
|
||||
requires: {
|
||||
'@overleaf/settings': (this.settings = {}),
|
||||
'./OutputCacheManager': { BUILD_REGEX: /^[0-9a-f]+-[0-9a-f]+$/ },
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
tk.reset()
|
||||
})
|
||||
|
||||
describe('without a top level object', function () {
|
||||
beforeEach(function () {
|
||||
this.RequestParser.parse([], this.callback)
|
||||
})
|
||||
|
||||
it('should return an error', function () {
|
||||
expect(this.callback).to.have.been.called
|
||||
expect(this.callback.args[0][0].message).to.equal(
|
||||
'top level object should have a compile attribute'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('without a compile attribute', function () {
|
||||
beforeEach(function () {
|
||||
this.RequestParser.parse({}, this.callback)
|
||||
})
|
||||
|
||||
it('should return an error', function () {
|
||||
expect(this.callback).to.have.been.called
|
||||
expect(this.callback.args[0][0].message).to.equal(
|
||||
'top level object should have a compile attribute'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('without a valid compiler', function () {
|
||||
beforeEach(function () {
|
||||
this.validRequest.compile.options.compiler = 'not-a-compiler'
|
||||
this.RequestParser.parse(this.validRequest, this.callback)
|
||||
})
|
||||
|
||||
it('should return an error', function () {
|
||||
this.callback
|
||||
.calledWithMatch({
|
||||
message:
|
||||
'compiler attribute should be one of: pdflatex, latex, xelatex, lualatex',
|
||||
})
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('without a compiler specified', function () {
|
||||
beforeEach(function (done) {
|
||||
delete this.validRequest.compile.options.compiler
|
||||
this.RequestParser.parse(this.validRequest, (error, data) => {
|
||||
if (error) return done(error)
|
||||
this.data = data
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should set the compiler to pdflatex by default', function () {
|
||||
this.data.compiler.should.equal('pdflatex')
|
||||
})
|
||||
})
|
||||
|
||||
describe('with imageName set', function () {
|
||||
beforeEach(function (done) {
|
||||
this.RequestParser.parse(this.validRequest, (error, data) => {
|
||||
if (error) return done(error)
|
||||
this.data = data
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should set the imageName', function () {
|
||||
this.data.imageName.should.equal('basicImageName/here:2017-1')
|
||||
})
|
||||
})
|
||||
|
||||
describe('when image restrictions are present', function () {
|
||||
beforeEach(function () {
|
||||
this.settings.clsi = { docker: {} }
|
||||
this.settings.clsi.docker.allowedImages = [
|
||||
'repo/name:tag1',
|
||||
'repo/name:tag2',
|
||||
]
|
||||
})
|
||||
|
||||
describe('with imageName set to something invalid', function () {
|
||||
beforeEach(function () {
|
||||
const request = this.validRequest
|
||||
request.compile.options.imageName = 'something/different:latest'
|
||||
this.RequestParser.parse(request, (error, data) => {
|
||||
this.error = error
|
||||
this.data = data
|
||||
})
|
||||
})
|
||||
|
||||
it('should throw an error for imageName', function () {
|
||||
expect(String(this.error)).to.include(
|
||||
'imageName attribute should be one of'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with imageName set to something valid', function () {
|
||||
beforeEach(function () {
|
||||
const request = this.validRequest
|
||||
request.compile.options.imageName = 'repo/name:tag1'
|
||||
this.RequestParser.parse(request, (error, data) => {
|
||||
this.error = error
|
||||
this.data = data
|
||||
})
|
||||
})
|
||||
|
||||
it('should set the imageName', function () {
|
||||
this.data.imageName.should.equal('repo/name:tag1')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('with flags set', function () {
|
||||
beforeEach(function (done) {
|
||||
this.validRequest.compile.options.flags = ['-file-line-error']
|
||||
this.RequestParser.parse(this.validRequest, (error, data) => {
|
||||
if (error) return done(error)
|
||||
this.data = data
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should set the flags attribute', function () {
|
||||
expect(this.data.flags).to.deep.equal(['-file-line-error'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('with flags not specified', function () {
|
||||
beforeEach(function (done) {
|
||||
this.RequestParser.parse(this.validRequest, (error, data) => {
|
||||
if (error) return done(error)
|
||||
this.data = data
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('it should have an empty flags list', function () {
|
||||
expect(this.data.flags).to.deep.equal([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('without a timeout specified', function () {
|
||||
beforeEach(function (done) {
|
||||
delete this.validRequest.compile.options.timeout
|
||||
this.RequestParser.parse(this.validRequest, (error, data) => {
|
||||
if (error) return done(error)
|
||||
this.data = data
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should set the timeout to MAX_TIMEOUT', function () {
|
||||
this.data.timeout.should.equal(this.RequestParser.MAX_TIMEOUT * 1000)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a timeout larger than the maximum', function () {
|
||||
beforeEach(function (done) {
|
||||
this.validRequest.compile.options.timeout =
|
||||
this.RequestParser.MAX_TIMEOUT + 1
|
||||
this.RequestParser.parse(this.validRequest, (error, data) => {
|
||||
if (error) return done(error)
|
||||
this.data = data
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should set the timeout to MAX_TIMEOUT', function () {
|
||||
this.data.timeout.should.equal(this.RequestParser.MAX_TIMEOUT * 1000)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a timeout', function () {
|
||||
beforeEach(function (done) {
|
||||
this.RequestParser.parse(this.validRequest, (error, data) => {
|
||||
if (error) return done(error)
|
||||
this.data = data
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should set the timeout (in milliseconds)', function () {
|
||||
this.data.timeout.should.equal(
|
||||
this.validRequest.compile.options.timeout * 1000
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a resource without a path', function () {
|
||||
beforeEach(function () {
|
||||
delete this.validResource.path
|
||||
this.validRequest.compile.resources.push(this.validResource)
|
||||
this.RequestParser.parse(this.validRequest, this.callback)
|
||||
})
|
||||
|
||||
it('should return an error', function () {
|
||||
this.callback
|
||||
.calledWithMatch({
|
||||
message: 'all resources should have a path attribute',
|
||||
})
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a resource with a path', function () {
|
||||
beforeEach(function () {
|
||||
this.validResource.path = this.path = 'test.tex'
|
||||
this.validRequest.compile.resources.push(this.validResource)
|
||||
this.RequestParser.parse(this.validRequest, this.callback)
|
||||
this.data = this.callback.args[0][1]
|
||||
})
|
||||
|
||||
it('should return the path in the parsed response', function () {
|
||||
this.data.resources[0].path.should.equal(this.path)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a resource with a malformed modified date', function () {
|
||||
beforeEach(function () {
|
||||
this.validResource.modified = 'not-a-date'
|
||||
this.validRequest.compile.resources.push(this.validResource)
|
||||
this.RequestParser.parse(this.validRequest, this.callback)
|
||||
})
|
||||
|
||||
it('should return an error', function () {
|
||||
this.callback
|
||||
.calledWithMatch({
|
||||
message:
|
||||
'resource modified date could not be understood: ' +
|
||||
this.validResource.modified,
|
||||
})
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a valid buildId', function () {
|
||||
beforeEach(function (done) {
|
||||
this.validRequest.compile.options.buildId = '195a4869176-a4ad60bee7bf35e4'
|
||||
this.RequestParser.parse(this.validRequest, (error, data) => {
|
||||
if (error) return done(error)
|
||||
this.data = data
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should return an error', function () {
|
||||
this.data.buildId.should.equal('195a4869176-a4ad60bee7bf35e4')
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a bad buildId', function () {
|
||||
beforeEach(function () {
|
||||
this.validRequest.compile.options.buildId = 'foo/bar'
|
||||
this.RequestParser.parse(this.validRequest, this.callback)
|
||||
})
|
||||
|
||||
it('should return an error', function () {
|
||||
this.callback
|
||||
.calledWithMatch({
|
||||
message:
|
||||
'buildId attribute does not match regex /^[0-9a-f]+-[0-9a-f]+$/',
|
||||
})
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a resource with a valid date', function () {
|
||||
beforeEach(function () {
|
||||
this.date = '12:00 01/02/03'
|
||||
this.validResource.modified = this.date
|
||||
this.validRequest.compile.resources.push(this.validResource)
|
||||
this.RequestParser.parse(this.validRequest, this.callback)
|
||||
this.data = this.callback.args[0][1]
|
||||
})
|
||||
|
||||
it('should return the date as a Javascript Date object', function () {
|
||||
;(this.data.resources[0].modified instanceof Date).should.equal(true)
|
||||
this.data.resources[0].modified
|
||||
.getTime()
|
||||
.should.equal(Date.parse(this.date))
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a resource without either a content or URL attribute', function () {
|
||||
beforeEach(function () {
|
||||
delete this.validResource.url
|
||||
delete this.validResource.content
|
||||
this.validRequest.compile.resources.push(this.validResource)
|
||||
this.RequestParser.parse(this.validRequest, this.callback)
|
||||
})
|
||||
|
||||
it('should return an error', function () {
|
||||
this.callback
|
||||
.calledWithMatch({
|
||||
message:
|
||||
'all resources should have either a url or content attribute',
|
||||
})
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a resource where the content is not a string', function () {
|
||||
beforeEach(function () {
|
||||
this.validResource.content = []
|
||||
this.validRequest.compile.resources.push(this.validResource)
|
||||
this.RequestParser.parse(this.validRequest, this.callback)
|
||||
})
|
||||
|
||||
it('should return an error', function () {
|
||||
this.callback
|
||||
.calledWithMatch({ message: 'content attribute should be a string' })
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a resource where the url is not a string', function () {
|
||||
beforeEach(function () {
|
||||
this.validResource.url = []
|
||||
this.validRequest.compile.resources.push(this.validResource)
|
||||
this.RequestParser.parse(this.validRequest, this.callback)
|
||||
})
|
||||
|
||||
it('should return an error', function () {
|
||||
this.callback
|
||||
.calledWithMatch({ message: 'url attribute should be a string' })
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a resource with a url', function () {
|
||||
beforeEach(function () {
|
||||
this.validResource.url = this.url = 'www.example.com'
|
||||
this.validRequest.compile.resources.push(this.validResource)
|
||||
this.RequestParser.parse(this.validRequest, this.callback)
|
||||
this.data = this.callback.args[0][1]
|
||||
})
|
||||
|
||||
it('should return the url in the parsed response', function () {
|
||||
this.data.resources[0].url.should.equal(this.url)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a resource with a content attribute', function () {
|
||||
beforeEach(function () {
|
||||
this.validResource.content = this.content = 'Hello world'
|
||||
this.validRequest.compile.resources.push(this.validResource)
|
||||
this.RequestParser.parse(this.validRequest, this.callback)
|
||||
this.data = this.callback.args[0][1]
|
||||
})
|
||||
|
||||
it('should return the content in the parsed response', function () {
|
||||
this.data.resources[0].content.should.equal(this.content)
|
||||
})
|
||||
})
|
||||
|
||||
describe('without a root resource path', function () {
|
||||
beforeEach(function () {
|
||||
delete this.validRequest.compile.rootResourcePath
|
||||
this.RequestParser.parse(this.validRequest, this.callback)
|
||||
this.data = this.callback.args[0][1]
|
||||
})
|
||||
|
||||
it("should set the root resource path to 'main.tex' by default", function () {
|
||||
this.data.rootResourcePath.should.equal('main.tex')
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a root resource path', function () {
|
||||
beforeEach(function () {
|
||||
this.validRequest.compile.rootResourcePath = this.path = 'test.tex'
|
||||
this.RequestParser.parse(this.validRequest, this.callback)
|
||||
this.data = this.callback.args[0][1]
|
||||
})
|
||||
|
||||
it('should return the root resource path in the parsed response', function () {
|
||||
this.data.rootResourcePath.should.equal(this.path)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a root resource path that is not a string', function () {
|
||||
beforeEach(function () {
|
||||
this.validRequest.compile.rootResourcePath = []
|
||||
this.RequestParser.parse(this.validRequest, this.callback)
|
||||
})
|
||||
|
||||
it('should return an error', function () {
|
||||
this.callback
|
||||
.calledWithMatch({
|
||||
message: 'rootResourcePath attribute should be a string',
|
||||
})
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a root resource path that has a relative path', function () {
|
||||
beforeEach(function () {
|
||||
this.validRequest.compile.rootResourcePath = 'foo/../../bar.tex'
|
||||
this.RequestParser.parse(this.validRequest, this.callback)
|
||||
this.data = this.callback.args[0][1]
|
||||
})
|
||||
|
||||
it('should return an error', function () {
|
||||
this.callback
|
||||
.calledWithMatch({ message: 'relative path in root resource' })
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a root resource path that has unescaped + relative path', function () {
|
||||
beforeEach(function () {
|
||||
this.validRequest.compile.rootResourcePath = 'foo/../bar.tex'
|
||||
this.RequestParser.parse(this.validRequest, this.callback)
|
||||
this.data = this.callback.args[0][1]
|
||||
})
|
||||
|
||||
it('should return an error', function () {
|
||||
this.callback
|
||||
.calledWithMatch({ message: 'relative path in root resource' })
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with an unknown syncType', function () {
|
||||
beforeEach(function () {
|
||||
this.validRequest.compile.options.syncType = 'unexpected'
|
||||
this.RequestParser.parse(this.validRequest, this.callback)
|
||||
this.data = this.callback.args[0][1]
|
||||
})
|
||||
|
||||
it('should return an error', function () {
|
||||
this.callback
|
||||
.calledWithMatch({
|
||||
message: 'syncType attribute should be one of: full, incremental',
|
||||
})
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
})
|
241
services/clsi/test/unit/js/ResourceStateManagerTests.js
Normal file
241
services/clsi/test/unit/js/ResourceStateManagerTests.js
Normal file
@@ -0,0 +1,241 @@
|
||||
/* 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:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
const SandboxedModule = require('sandboxed-module')
|
||||
const sinon = require('sinon')
|
||||
const { expect } = require('chai')
|
||||
const modulePath = require('node:path').join(
|
||||
__dirname,
|
||||
'../../../app/js/ResourceStateManager'
|
||||
)
|
||||
const Path = require('node:path')
|
||||
const Errors = require('../../../app/js/Errors')
|
||||
|
||||
describe('ResourceStateManager', function () {
|
||||
beforeEach(function () {
|
||||
this.ResourceStateManager = SandboxedModule.require(modulePath, {
|
||||
singleOnly: true,
|
||||
requires: {
|
||||
fs: (this.fs = {}),
|
||||
'./SafeReader': (this.SafeReader = {}),
|
||||
},
|
||||
})
|
||||
this.basePath = '/path/to/write/files/to'
|
||||
this.resources = [
|
||||
{ path: 'resource-1-mock' },
|
||||
{ path: 'resource-2-mock' },
|
||||
{ path: 'resource-3-mock' },
|
||||
]
|
||||
this.state = '1234567890'
|
||||
this.resourceFileName = `${this.basePath}/.project-sync-state`
|
||||
this.resourceFileContents = `${this.resources[0].path}\n${this.resources[1].path}\n${this.resources[2].path}\nstateHash:${this.state}`
|
||||
return (this.callback = sinon.stub())
|
||||
})
|
||||
|
||||
describe('saveProjectState', function () {
|
||||
beforeEach(function () {
|
||||
return (this.fs.writeFile = sinon.stub().callsArg(2))
|
||||
})
|
||||
|
||||
describe('when the state is specified', function () {
|
||||
beforeEach(function () {
|
||||
return this.ResourceStateManager.saveProjectState(
|
||||
this.state,
|
||||
this.resources,
|
||||
this.basePath,
|
||||
this.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should write the resource list to disk', function () {
|
||||
return this.fs.writeFile
|
||||
.calledWith(this.resourceFileName, this.resourceFileContents)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
return it('should call the callback', function () {
|
||||
return this.callback.called.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
return describe('when the state is undefined', function () {
|
||||
beforeEach(function () {
|
||||
this.state = undefined
|
||||
this.fs.unlink = sinon.stub().callsArg(1)
|
||||
return this.ResourceStateManager.saveProjectState(
|
||||
this.state,
|
||||
this.resources,
|
||||
this.basePath,
|
||||
this.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should unlink the resource file', function () {
|
||||
return this.fs.unlink
|
||||
.calledWith(this.resourceFileName)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should not write the resource list to disk', function () {
|
||||
return this.fs.writeFile.called.should.equal(false)
|
||||
})
|
||||
|
||||
return it('should call the callback', function () {
|
||||
return this.callback.called.should.equal(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('checkProjectStateMatches', function () {
|
||||
describe('when the state matches', function () {
|
||||
beforeEach(function () {
|
||||
this.SafeReader.readFile = sinon
|
||||
.stub()
|
||||
.callsArgWith(3, null, this.resourceFileContents)
|
||||
return this.ResourceStateManager.checkProjectStateMatches(
|
||||
this.state,
|
||||
this.basePath,
|
||||
this.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should read the resource file', function () {
|
||||
return this.SafeReader.readFile
|
||||
.calledWith(this.resourceFileName)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
return it('should call the callback with the results', function () {
|
||||
return this.callback
|
||||
.calledWithMatch(null, this.resources)
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the state file is not present', function () {
|
||||
beforeEach(function () {
|
||||
this.SafeReader.readFile = sinon.stub().callsArg(3)
|
||||
return this.ResourceStateManager.checkProjectStateMatches(
|
||||
this.state,
|
||||
this.basePath,
|
||||
this.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should read the resource file', function () {
|
||||
return this.SafeReader.readFile
|
||||
.calledWith(this.resourceFileName)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should call the callback with an error', function () {
|
||||
this.callback
|
||||
.calledWith(sinon.match(Errors.FilesOutOfSyncError))
|
||||
.should.equal(true)
|
||||
|
||||
const message = this.callback.args[0][0].message
|
||||
expect(message).to.include('invalid state for incremental update')
|
||||
})
|
||||
})
|
||||
|
||||
return describe('when the state does not match', function () {
|
||||
beforeEach(function () {
|
||||
this.SafeReader.readFile = sinon
|
||||
.stub()
|
||||
.callsArgWith(3, null, this.resourceFileContents)
|
||||
return this.ResourceStateManager.checkProjectStateMatches(
|
||||
'not-the-original-state',
|
||||
this.basePath,
|
||||
this.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should call the callback with an error', function () {
|
||||
this.callback
|
||||
.calledWith(sinon.match(Errors.FilesOutOfSyncError))
|
||||
.should.equal(true)
|
||||
|
||||
const message = this.callback.args[0][0].message
|
||||
expect(message).to.include('invalid state for incremental update')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
return describe('checkResourceFiles', function () {
|
||||
describe('when all the files are present', function () {
|
||||
beforeEach(function () {
|
||||
this.allFiles = [
|
||||
this.resources[0].path,
|
||||
this.resources[1].path,
|
||||
this.resources[2].path,
|
||||
]
|
||||
return this.ResourceStateManager.checkResourceFiles(
|
||||
this.resources,
|
||||
this.allFiles,
|
||||
this.basePath,
|
||||
this.callback
|
||||
)
|
||||
})
|
||||
|
||||
return it('should call the callback', function () {
|
||||
return this.callback.calledWithExactly().should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when there is a missing file', function () {
|
||||
beforeEach(function () {
|
||||
this.allFiles = [this.resources[0].path, this.resources[1].path]
|
||||
this.fs.stat = sinon.stub().callsArgWith(1, new Error())
|
||||
return this.ResourceStateManager.checkResourceFiles(
|
||||
this.resources,
|
||||
this.allFiles,
|
||||
this.basePath,
|
||||
this.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should call the callback with an error', function () {
|
||||
this.callback
|
||||
.calledWith(sinon.match(Errors.FilesOutOfSyncError))
|
||||
.should.equal(true)
|
||||
|
||||
const message = this.callback.args[0][0].message
|
||||
expect(message).to.include(
|
||||
'resource files missing in incremental update'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
return describe('when a resource contains a relative path', function () {
|
||||
beforeEach(function () {
|
||||
this.resources[0].path = '../foo/bar.tex'
|
||||
this.allFiles = [
|
||||
this.resources[0].path,
|
||||
this.resources[1].path,
|
||||
this.resources[2].path,
|
||||
]
|
||||
return this.ResourceStateManager.checkResourceFiles(
|
||||
this.resources,
|
||||
this.allFiles,
|
||||
this.basePath,
|
||||
this.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should call the callback with an error', function () {
|
||||
this.callback.calledWith(sinon.match(Error)).should.equal(true)
|
||||
|
||||
const message = this.callback.args[0][0].message
|
||||
expect(message).to.include('relative path in resource file list')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
532
services/clsi/test/unit/js/ResourceWriterTests.js
Normal file
532
services/clsi/test/unit/js/ResourceWriterTests.js
Normal file
@@ -0,0 +1,532 @@
|
||||
/* eslint-disable
|
||||
no-return-assign,
|
||||
*/
|
||||
// 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
|
||||
* DS206: Consider reworking classes to avoid initClass
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
const SandboxedModule = require('sandboxed-module')
|
||||
const sinon = require('sinon')
|
||||
const { expect } = require('chai')
|
||||
const modulePath = require('node:path').join(
|
||||
__dirname,
|
||||
'../../../app/js/ResourceWriter'
|
||||
)
|
||||
const path = require('node:path')
|
||||
|
||||
describe('ResourceWriter', function () {
|
||||
beforeEach(function () {
|
||||
let Timer
|
||||
this.ResourceWriter = SandboxedModule.require(modulePath, {
|
||||
singleOnly: true,
|
||||
requires: {
|
||||
fs: (this.fs = {
|
||||
mkdir: sinon.stub().callsArg(1),
|
||||
unlink: sinon.stub().callsArg(1),
|
||||
}),
|
||||
'./ResourceStateManager': (this.ResourceStateManager = {}),
|
||||
'./UrlCache': (this.UrlCache = {
|
||||
createProjectDir: sinon.stub().yields(),
|
||||
}),
|
||||
'./OutputFileFinder': (this.OutputFileFinder = {}),
|
||||
'./Metrics': (this.Metrics = {
|
||||
inc: sinon.stub(),
|
||||
Timer: (Timer = (function () {
|
||||
Timer = class Timer {
|
||||
static initClass() {
|
||||
this.prototype.done = sinon.stub()
|
||||
}
|
||||
}
|
||||
Timer.initClass()
|
||||
return Timer
|
||||
})()),
|
||||
}),
|
||||
},
|
||||
})
|
||||
this.project_id = 'project-id-123'
|
||||
this.basePath = '/path/to/write/files/to'
|
||||
return (this.callback = sinon.stub())
|
||||
})
|
||||
|
||||
describe('syncResourcesToDisk on a full request', function () {
|
||||
beforeEach(function () {
|
||||
this.resources = ['resource-1-mock', 'resource-2-mock', 'resource-3-mock']
|
||||
this.request = {
|
||||
project_id: this.project_id,
|
||||
syncState: (this.syncState = '0123456789abcdef'),
|
||||
resources: this.resources,
|
||||
}
|
||||
this.ResourceWriter._writeResourceToDisk = sinon.stub().callsArg(3)
|
||||
this.ResourceWriter._removeExtraneousFiles = sinon.stub().yields(null)
|
||||
this.ResourceStateManager.saveProjectState = sinon.stub().callsArg(3)
|
||||
return this.ResourceWriter.syncResourcesToDisk(
|
||||
this.request,
|
||||
this.basePath,
|
||||
this.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should remove old files', function () {
|
||||
return this.ResourceWriter._removeExtraneousFiles
|
||||
.calledWith(this.request, this.resources, this.basePath)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should write each resource to disk', function () {
|
||||
return Array.from(this.resources).map(resource =>
|
||||
this.ResourceWriter._writeResourceToDisk
|
||||
.calledWith(this.project_id, resource, this.basePath)
|
||||
.should.equal(true)
|
||||
)
|
||||
})
|
||||
|
||||
it('should store the sync state and resource list', function () {
|
||||
return this.ResourceStateManager.saveProjectState
|
||||
.calledWith(this.syncState, this.resources, this.basePath)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
return it('should call the callback', function () {
|
||||
return this.callback.called.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('syncResourcesToDisk on an incremental update', function () {
|
||||
beforeEach(function () {
|
||||
this.resources = ['resource-1-mock']
|
||||
this.request = {
|
||||
project_id: this.project_id,
|
||||
syncType: 'incremental',
|
||||
syncState: (this.syncState = '1234567890abcdef'),
|
||||
resources: this.resources,
|
||||
}
|
||||
this.fullResources = this.resources.concat(['file-1'])
|
||||
this.ResourceWriter._writeResourceToDisk = sinon.stub().callsArg(3)
|
||||
this.ResourceWriter._removeExtraneousFiles = sinon
|
||||
.stub()
|
||||
.yields(null, (this.outputFiles = []), (this.allFiles = []))
|
||||
this.ResourceStateManager.checkProjectStateMatches = sinon
|
||||
.stub()
|
||||
.callsArgWith(2, null, this.fullResources)
|
||||
this.ResourceStateManager.saveProjectState = sinon.stub().callsArg(3)
|
||||
this.ResourceStateManager.checkResourceFiles = sinon.stub().callsArg(3)
|
||||
return this.ResourceWriter.syncResourcesToDisk(
|
||||
this.request,
|
||||
this.basePath,
|
||||
this.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should check the sync state matches', function () {
|
||||
return this.ResourceStateManager.checkProjectStateMatches
|
||||
.calledWith(this.syncState, this.basePath)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should remove old files', function () {
|
||||
return this.ResourceWriter._removeExtraneousFiles
|
||||
.calledWith(this.request, this.fullResources, this.basePath)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should check each resource exists', function () {
|
||||
return this.ResourceStateManager.checkResourceFiles
|
||||
.calledWith(this.fullResources, this.allFiles, this.basePath)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should write each resource to disk', function () {
|
||||
return Array.from(this.resources).map(resource =>
|
||||
this.ResourceWriter._writeResourceToDisk
|
||||
.calledWith(this.project_id, resource, this.basePath)
|
||||
.should.equal(true)
|
||||
)
|
||||
})
|
||||
|
||||
return it('should call the callback', function () {
|
||||
return this.callback.called.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('syncResourcesToDisk on an incremental update when the state does not match', function () {
|
||||
beforeEach(function () {
|
||||
this.resources = ['resource-1-mock']
|
||||
this.request = {
|
||||
project_id: this.project_id,
|
||||
syncType: 'incremental',
|
||||
syncState: (this.syncState = '1234567890abcdef'),
|
||||
resources: this.resources,
|
||||
}
|
||||
this.ResourceStateManager.checkProjectStateMatches = sinon
|
||||
.stub()
|
||||
.callsArgWith(2, (this.error = new Error()))
|
||||
return this.ResourceWriter.syncResourcesToDisk(
|
||||
this.request,
|
||||
this.basePath,
|
||||
this.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should check whether the sync state matches', function () {
|
||||
return this.ResourceStateManager.checkProjectStateMatches
|
||||
.calledWith(this.syncState, this.basePath)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
return it('should call the callback with an error', function () {
|
||||
return this.callback.calledWith(this.error).should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('_removeExtraneousFiles', function () {
|
||||
beforeEach(function () {
|
||||
this.output_files = [
|
||||
{
|
||||
path: 'output.pdf',
|
||||
type: 'pdf',
|
||||
},
|
||||
{
|
||||
path: 'extra/file.tex',
|
||||
type: 'tex',
|
||||
},
|
||||
{
|
||||
path: 'extra.aux',
|
||||
type: 'aux',
|
||||
},
|
||||
{
|
||||
path: 'cache/_chunk1',
|
||||
},
|
||||
{
|
||||
path: 'figures/image-eps-converted-to.pdf',
|
||||
type: 'pdf',
|
||||
},
|
||||
{
|
||||
path: 'foo/main-figure0.md5',
|
||||
type: 'md5',
|
||||
},
|
||||
{
|
||||
path: 'foo/main-figure0.dpth',
|
||||
type: 'dpth',
|
||||
},
|
||||
{
|
||||
path: 'foo/main-figure0.pdf',
|
||||
type: 'pdf',
|
||||
},
|
||||
{
|
||||
path: '_minted-main/default-pyg-prefix.pygstyle',
|
||||
type: 'pygstyle',
|
||||
},
|
||||
{
|
||||
path: '_minted-main/default.pygstyle',
|
||||
type: 'pygstyle',
|
||||
},
|
||||
{
|
||||
path: '_minted-main/35E248B60965545BD232AE9F0FE9750D504A7AF0CD3BAA7542030FC560DFCC45.pygtex',
|
||||
type: 'pygtex',
|
||||
},
|
||||
{
|
||||
path: '_markdown_main/30893013dec5d869a415610079774c2f.md.tex',
|
||||
type: 'tex',
|
||||
},
|
||||
{
|
||||
path: 'output.stdout',
|
||||
},
|
||||
{
|
||||
path: 'output.stderr',
|
||||
},
|
||||
]
|
||||
this.resources = 'mock-resources'
|
||||
this.request = {
|
||||
project_id: this.project_id,
|
||||
syncType: 'incremental',
|
||||
syncState: (this.syncState = '1234567890abcdef'),
|
||||
resources: this.resources,
|
||||
}
|
||||
this.OutputFileFinder.findOutputFiles = sinon
|
||||
.stub()
|
||||
.callsArgWith(2, null, this.output_files)
|
||||
this.ResourceWriter._deleteFileIfNotDirectory = sinon.stub().callsArg(1)
|
||||
return this.ResourceWriter._removeExtraneousFiles(
|
||||
this.request,
|
||||
this.resources,
|
||||
this.basePath,
|
||||
this.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should find the existing output files', function () {
|
||||
return this.OutputFileFinder.findOutputFiles
|
||||
.calledWith(this.resources, this.basePath)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should delete the output files', function () {
|
||||
return this.ResourceWriter._deleteFileIfNotDirectory
|
||||
.calledWith(path.join(this.basePath, 'output.pdf'))
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should delete the stdout log file', function () {
|
||||
return this.ResourceWriter._deleteFileIfNotDirectory
|
||||
.calledWith(path.join(this.basePath, 'output.stdout'))
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should delete the stderr log file', function () {
|
||||
return this.ResourceWriter._deleteFileIfNotDirectory
|
||||
.calledWith(path.join(this.basePath, 'output.stderr'))
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should delete the extra files', function () {
|
||||
return this.ResourceWriter._deleteFileIfNotDirectory
|
||||
.calledWith(path.join(this.basePath, 'extra/file.tex'))
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should not delete the extra aux files', function () {
|
||||
return this.ResourceWriter._deleteFileIfNotDirectory
|
||||
.calledWith(path.join(this.basePath, 'extra.aux'))
|
||||
.should.equal(false)
|
||||
})
|
||||
|
||||
it('should not delete the knitr cache file', function () {
|
||||
return this.ResourceWriter._deleteFileIfNotDirectory
|
||||
.calledWith(path.join(this.basePath, 'cache/_chunk1'))
|
||||
.should.equal(false)
|
||||
})
|
||||
|
||||
it('should not delete the epstopdf converted files', function () {
|
||||
return this.ResourceWriter._deleteFileIfNotDirectory
|
||||
.calledWith(
|
||||
path.join(this.basePath, 'figures/image-eps-converted-to.pdf')
|
||||
)
|
||||
.should.equal(false)
|
||||
})
|
||||
|
||||
it('should not delete the tikz md5 files', function () {
|
||||
return this.ResourceWriter._deleteFileIfNotDirectory
|
||||
.calledWith(path.join(this.basePath, 'foo/main-figure0.md5'))
|
||||
.should.equal(false)
|
||||
})
|
||||
|
||||
it('should not delete the tikz dpth files', function () {
|
||||
return this.ResourceWriter._deleteFileIfNotDirectory
|
||||
.calledWith(path.join(this.basePath, 'foo/main-figure0.dpth'))
|
||||
.should.equal(false)
|
||||
})
|
||||
|
||||
it('should not delete the tikz pdf files', function () {
|
||||
return this.ResourceWriter._deleteFileIfNotDirectory
|
||||
.calledWith(path.join(this.basePath, 'foo/main-figure0.pdf'))
|
||||
.should.equal(false)
|
||||
})
|
||||
|
||||
it('should not delete the minted pygstyle files', function () {
|
||||
return this.ResourceWriter._deleteFileIfNotDirectory
|
||||
.calledWith(
|
||||
path.join(this.basePath, '_minted-main/default-pyg-prefix.pygstyle')
|
||||
)
|
||||
.should.equal(false)
|
||||
})
|
||||
|
||||
it('should not delete the minted default pygstyle files', function () {
|
||||
return this.ResourceWriter._deleteFileIfNotDirectory
|
||||
.calledWith(path.join(this.basePath, '_minted-main/default.pygstyle'))
|
||||
.should.equal(false)
|
||||
})
|
||||
|
||||
it('should not delete the minted default pygtex files', function () {
|
||||
return this.ResourceWriter._deleteFileIfNotDirectory
|
||||
.calledWith(
|
||||
path.join(
|
||||
this.basePath,
|
||||
'_minted-main/35E248B60965545BD232AE9F0FE9750D504A7AF0CD3BAA7542030FC560DFCC45.pygtex'
|
||||
)
|
||||
)
|
||||
.should.equal(false)
|
||||
})
|
||||
|
||||
it('should not delete the markdown md.tex files', function () {
|
||||
return this.ResourceWriter._deleteFileIfNotDirectory
|
||||
.calledWith(
|
||||
path.join(
|
||||
this.basePath,
|
||||
'_markdown_main/30893013dec5d869a415610079774c2f.md.tex'
|
||||
)
|
||||
)
|
||||
.should.equal(false)
|
||||
})
|
||||
|
||||
it('should call the callback', function () {
|
||||
return this.callback.called.should.equal(true)
|
||||
})
|
||||
|
||||
return it('should time the request', function () {
|
||||
return this.Metrics.Timer.prototype.done.called.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('_writeResourceToDisk', function () {
|
||||
describe('with a url based resource', function () {
|
||||
beforeEach(function () {
|
||||
this.fs.mkdir = sinon.stub().callsArg(2)
|
||||
this.resource = {
|
||||
path: 'main.tex',
|
||||
url: 'http://www.example.com/primary/main.tex',
|
||||
fallbackURL: 'http://fallback.example.com/fallback/main.tex',
|
||||
modified: Date.now(),
|
||||
}
|
||||
this.UrlCache.downloadUrlToFile = sinon
|
||||
.stub()
|
||||
.callsArgWith(5, 'fake error downloading file')
|
||||
return this.ResourceWriter._writeResourceToDisk(
|
||||
this.project_id,
|
||||
this.resource,
|
||||
this.basePath,
|
||||
this.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should ensure the directory exists', function () {
|
||||
this.fs.mkdir
|
||||
.calledWith(
|
||||
path.dirname(path.join(this.basePath, this.resource.path))
|
||||
)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should write the URL from the cache', function () {
|
||||
return this.UrlCache.downloadUrlToFile
|
||||
.calledWith(
|
||||
this.project_id,
|
||||
this.resource.url,
|
||||
this.resource.fallbackURL,
|
||||
path.join(this.basePath, this.resource.path),
|
||||
this.resource.modified
|
||||
)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should call the callback', function () {
|
||||
return this.callback.called.should.equal(true)
|
||||
})
|
||||
|
||||
return it('should not return an error if the resource writer errored', function () {
|
||||
return expect(this.callback.args[0][0]).not.to.exist
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a content based resource', function () {
|
||||
beforeEach(function () {
|
||||
this.resource = {
|
||||
path: 'main.tex',
|
||||
content: 'Hello world',
|
||||
}
|
||||
this.fs.writeFile = sinon.stub().callsArg(2)
|
||||
this.fs.mkdir = sinon.stub().callsArg(2)
|
||||
return this.ResourceWriter._writeResourceToDisk(
|
||||
this.project_id,
|
||||
this.resource,
|
||||
this.basePath,
|
||||
this.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should ensure the directory exists', function () {
|
||||
return this.fs.mkdir
|
||||
.calledWith(
|
||||
path.dirname(path.join(this.basePath, this.resource.path))
|
||||
)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should write the contents to disk', function () {
|
||||
return this.fs.writeFile
|
||||
.calledWith(
|
||||
path.join(this.basePath, this.resource.path),
|
||||
this.resource.content
|
||||
)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
return it('should call the callback', function () {
|
||||
return this.callback.called.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
return describe('with a file path that breaks out of the root folder', function () {
|
||||
beforeEach(function () {
|
||||
this.resource = {
|
||||
path: '../../main.tex',
|
||||
content: 'Hello world',
|
||||
}
|
||||
this.fs.writeFile = sinon.stub().callsArg(2)
|
||||
return this.ResourceWriter._writeResourceToDisk(
|
||||
this.project_id,
|
||||
this.resource,
|
||||
this.basePath,
|
||||
this.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should not write to disk', function () {
|
||||
return this.fs.writeFile.called.should.equal(false)
|
||||
})
|
||||
|
||||
it('should return an error', function () {
|
||||
this.callback.calledWith(sinon.match(Error)).should.equal(true)
|
||||
|
||||
const message = this.callback.args[0][0].message
|
||||
expect(message).to.include('resource path is outside root directory')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
return describe('checkPath', function () {
|
||||
describe('with a valid path', function () {
|
||||
beforeEach(function () {
|
||||
return this.ResourceWriter.checkPath('foo', 'bar', this.callback)
|
||||
})
|
||||
|
||||
return it('should return the joined path', function () {
|
||||
return this.callback.calledWith(null, 'foo/bar').should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with an invalid path', function () {
|
||||
beforeEach(function () {
|
||||
this.ResourceWriter.checkPath('foo', 'baz/../../bar', this.callback)
|
||||
})
|
||||
|
||||
it('should return an error', function () {
|
||||
this.callback.calledWith(sinon.match(Error)).should.equal(true)
|
||||
|
||||
const message = this.callback.args[0][0].message
|
||||
expect(message).to.include('resource path is outside root directory')
|
||||
})
|
||||
})
|
||||
|
||||
describe('with another invalid path matching on a prefix', function () {
|
||||
beforeEach(function () {
|
||||
return this.ResourceWriter.checkPath(
|
||||
'foo',
|
||||
'../foobar/baz',
|
||||
this.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should return an error', function () {
|
||||
this.callback.calledWith(sinon.match(Error)).should.equal(true)
|
||||
|
||||
const message = this.callback.args[0][0].message
|
||||
expect(message).to.include('resource path is outside root directory')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
248
services/clsi/test/unit/js/StaticServerForbidSymlinksTests.js
Normal file
248
services/clsi/test/unit/js/StaticServerForbidSymlinksTests.js
Normal file
@@ -0,0 +1,248 @@
|
||||
/* 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:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
const SandboxedModule = require('sandboxed-module')
|
||||
const assert = require('node:assert')
|
||||
const path = require('node:path')
|
||||
const sinon = require('sinon')
|
||||
const modulePath = path.join(
|
||||
__dirname,
|
||||
'../../../app/js/StaticServerForbidSymlinks'
|
||||
)
|
||||
const { expect } = require('chai')
|
||||
|
||||
describe('StaticServerForbidSymlinks', function () {
|
||||
beforeEach(function () {
|
||||
this.settings = {
|
||||
path: {
|
||||
compilesDir: '/compiles/here',
|
||||
},
|
||||
}
|
||||
|
||||
this.fs = {}
|
||||
this.ForbidSymlinks = SandboxedModule.require(modulePath, {
|
||||
requires: {
|
||||
'@overleaf/settings': this.settings,
|
||||
fs: this.fs,
|
||||
},
|
||||
})
|
||||
|
||||
this.dummyStatic = (rootDir, options) => (req, res, next) =>
|
||||
// console.log "dummyStatic serving file", rootDir, "called with", req.url
|
||||
// serve it
|
||||
next()
|
||||
|
||||
this.StaticServerForbidSymlinks = this.ForbidSymlinks(
|
||||
this.dummyStatic,
|
||||
this.settings.path.compilesDir
|
||||
)
|
||||
this.req = {
|
||||
params: {
|
||||
project_id: '12345',
|
||||
},
|
||||
}
|
||||
|
||||
this.res = {}
|
||||
return (this.req.url = '/12345/output.pdf')
|
||||
})
|
||||
|
||||
describe('sending a normal file through', function () {
|
||||
beforeEach(function () {
|
||||
return (this.fs.realpath = sinon
|
||||
.stub()
|
||||
.callsArgWith(
|
||||
1,
|
||||
null,
|
||||
`${this.settings.path.compilesDir}/${this.req.params.project_id}/output.pdf`
|
||||
))
|
||||
})
|
||||
|
||||
return it('should call next', function (done) {
|
||||
this.res.sendStatus = function (resCode) {
|
||||
resCode.should.equal(200)
|
||||
return done()
|
||||
}
|
||||
return this.StaticServerForbidSymlinks(this.req, this.res, done)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a missing file', function () {
|
||||
beforeEach(function () {
|
||||
return (this.fs.realpath = sinon
|
||||
.stub()
|
||||
.callsArgWith(
|
||||
1,
|
||||
{ code: 'ENOENT' },
|
||||
`${this.settings.path.compilesDir}/${this.req.params.project_id}/unknown.pdf`
|
||||
))
|
||||
})
|
||||
|
||||
return it('should send a 404', function (done) {
|
||||
this.res.sendStatus = function (resCode) {
|
||||
resCode.should.equal(404)
|
||||
return done()
|
||||
}
|
||||
return this.StaticServerForbidSymlinks(this.req, this.res)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a new line', function () {
|
||||
beforeEach(function () {
|
||||
this.req.url = '/12345/output.pdf\nother file'
|
||||
this.fs.realpath = sinon.stub().yields()
|
||||
})
|
||||
|
||||
it('should process the correct file', function (done) {
|
||||
this.res.sendStatus = () => {
|
||||
this.fs.realpath.should.have.been.calledWith(
|
||||
`${this.settings.path.compilesDir}/12345/output.pdf\nother file`
|
||||
)
|
||||
done()
|
||||
}
|
||||
this.StaticServerForbidSymlinks(this.req, this.res)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a symlink file', function () {
|
||||
beforeEach(function () {
|
||||
return (this.fs.realpath = sinon
|
||||
.stub()
|
||||
.callsArgWith(1, null, `/etc/${this.req.params.project_id}/output.pdf`))
|
||||
})
|
||||
|
||||
return it('should send a 404', function (done) {
|
||||
this.res.sendStatus = function (resCode) {
|
||||
resCode.should.equal(404)
|
||||
return done()
|
||||
}
|
||||
return this.StaticServerForbidSymlinks(this.req, this.res)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a relative file', function () {
|
||||
beforeEach(function () {
|
||||
return (this.req.url = '/12345/../67890/output.pdf')
|
||||
})
|
||||
|
||||
return it('should send a 404', function (done) {
|
||||
this.res.sendStatus = function (resCode) {
|
||||
resCode.should.equal(404)
|
||||
return done()
|
||||
}
|
||||
return this.StaticServerForbidSymlinks(this.req, this.res)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a unnormalized file containing .', function () {
|
||||
beforeEach(function () {
|
||||
return (this.req.url = '/12345/foo/./output.pdf')
|
||||
})
|
||||
|
||||
return it('should send a 404', function (done) {
|
||||
this.res.sendStatus = function (resCode) {
|
||||
resCode.should.equal(404)
|
||||
return done()
|
||||
}
|
||||
return this.StaticServerForbidSymlinks(this.req, this.res)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a file containing an empty path', function () {
|
||||
beforeEach(function () {
|
||||
return (this.req.url = '/12345/foo//output.pdf')
|
||||
})
|
||||
|
||||
return it('should send a 404', function (done) {
|
||||
this.res.sendStatus = function (resCode) {
|
||||
resCode.should.equal(404)
|
||||
return done()
|
||||
}
|
||||
return this.StaticServerForbidSymlinks(this.req, this.res)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a non-project file', function () {
|
||||
beforeEach(function () {
|
||||
return (this.req.url = '/.foo/output.pdf')
|
||||
})
|
||||
|
||||
return it('should send a 404', function (done) {
|
||||
this.res.sendStatus = function (resCode) {
|
||||
resCode.should.equal(404)
|
||||
return done()
|
||||
}
|
||||
return this.StaticServerForbidSymlinks(this.req, this.res)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a file outside the compiledir', function () {
|
||||
beforeEach(function () {
|
||||
return (this.req.url = '/../bar/output.pdf')
|
||||
})
|
||||
|
||||
return it('should send a 404', function (done) {
|
||||
this.res.sendStatus = function (resCode) {
|
||||
resCode.should.equal(404)
|
||||
return done()
|
||||
}
|
||||
return this.StaticServerForbidSymlinks(this.req, this.res)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a file with no leading /', function () {
|
||||
beforeEach(function () {
|
||||
return (this.req.url = './../bar/output.pdf')
|
||||
})
|
||||
|
||||
return it('should send a 404', function (done) {
|
||||
this.res.sendStatus = function (resCode) {
|
||||
resCode.should.equal(404)
|
||||
return done()
|
||||
}
|
||||
return this.StaticServerForbidSymlinks(this.req, this.res)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a github style path', function () {
|
||||
beforeEach(function () {
|
||||
this.req.url = '/henryoswald-latex_example/output/output.log'
|
||||
return (this.fs.realpath = sinon
|
||||
.stub()
|
||||
.callsArgWith(
|
||||
1,
|
||||
null,
|
||||
`${this.settings.path.compilesDir}/henryoswald-latex_example/output/output.log`
|
||||
))
|
||||
})
|
||||
|
||||
return it('should call next', function (done) {
|
||||
this.res.sendStatus = function (resCode) {
|
||||
resCode.should.equal(200)
|
||||
return done()
|
||||
}
|
||||
return this.StaticServerForbidSymlinks(this.req, this.res, done)
|
||||
})
|
||||
})
|
||||
|
||||
return describe('with an error from fs.realpath', function () {
|
||||
beforeEach(function () {
|
||||
return (this.fs.realpath = sinon.stub().callsArgWith(1, 'error'))
|
||||
})
|
||||
|
||||
return it('should send a 500', function (done) {
|
||||
this.res.sendStatus = function (resCode) {
|
||||
resCode.should.equal(500)
|
||||
return done()
|
||||
}
|
||||
return this.StaticServerForbidSymlinks(this.req, this.res)
|
||||
})
|
||||
})
|
||||
})
|
116
services/clsi/test/unit/js/SynctexOutputParserTests.js
Normal file
116
services/clsi/test/unit/js/SynctexOutputParserTests.js
Normal file
@@ -0,0 +1,116 @@
|
||||
const Path = require('node:path')
|
||||
const SandboxedModule = require('sandboxed-module')
|
||||
const { expect } = require('chai')
|
||||
|
||||
const MODULE_PATH = Path.join(__dirname, '../../../app/js/SynctexOutputParser')
|
||||
|
||||
describe('SynctexOutputParser', function () {
|
||||
beforeEach(function () {
|
||||
this.SynctexOutputParser = SandboxedModule.require(MODULE_PATH)
|
||||
})
|
||||
|
||||
describe('parseViewOutput', function () {
|
||||
it('parses valid output', function () {
|
||||
const output = `This is SyncTeX command line utility, version 1.5
|
||||
SyncTeX result begin
|
||||
Output:/compile/output.pdf
|
||||
Page:1
|
||||
x:136.537964
|
||||
y:661.437561
|
||||
h:133.768356
|
||||
v:663.928223
|
||||
W:343.711060
|
||||
H:9.962640
|
||||
before:
|
||||
offset:-1
|
||||
middle:
|
||||
after:
|
||||
Output:/compile/output.pdf
|
||||
Page:2
|
||||
x:178.769592
|
||||
y:649.482361
|
||||
h:134.768356
|
||||
v:651.973022
|
||||
W:342.711060
|
||||
H:19.962640
|
||||
before:
|
||||
offset:-1
|
||||
middle:
|
||||
after:
|
||||
SyncTeX result end
|
||||
`
|
||||
const records = this.SynctexOutputParser.parseViewOutput(output)
|
||||
expect(records).to.deep.equal([
|
||||
{
|
||||
page: 1,
|
||||
h: 133.768356,
|
||||
v: 663.928223,
|
||||
width: 343.71106,
|
||||
height: 9.96264,
|
||||
},
|
||||
{
|
||||
page: 2,
|
||||
h: 134.768356,
|
||||
v: 651.973022,
|
||||
width: 342.71106,
|
||||
height: 19.96264,
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('handles garbage', function () {
|
||||
const output = 'This computer is on strike!'
|
||||
const records = this.SynctexOutputParser.parseViewOutput(output)
|
||||
expect(records).to.deep.equal([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseEditOutput', function () {
|
||||
it('parses valid output', function () {
|
||||
const output = `This is SyncTeX command line utility, version 1.5
|
||||
SyncTeX result begin
|
||||
Output:/compile/output.pdf
|
||||
Input:/compile/main.tex
|
||||
Line:17
|
||||
Column:-1
|
||||
Offset:0
|
||||
Context:
|
||||
SyncTeX result end
|
||||
`
|
||||
const records = this.SynctexOutputParser.parseEditOutput(
|
||||
output,
|
||||
'/compile'
|
||||
)
|
||||
expect(records).to.deep.equal([
|
||||
{ file: 'main.tex', line: 17, column: -1 },
|
||||
])
|
||||
})
|
||||
|
||||
it('handles values that contain colons', function () {
|
||||
const output = `This is SyncTeX command line utility, version 1.5
|
||||
SyncTeX result begin
|
||||
Output:/compile/output.pdf
|
||||
Input:/compile/this-file:has-a-weird-name.tex
|
||||
Line:17
|
||||
Column:-1
|
||||
Offset:0
|
||||
Context:
|
||||
SyncTeX result end
|
||||
`
|
||||
|
||||
const records = this.SynctexOutputParser.parseEditOutput(
|
||||
output,
|
||||
'/compile'
|
||||
)
|
||||
expect(records).to.deep.equal([
|
||||
{ file: 'this-file:has-a-weird-name.tex', line: 17, column: -1 },
|
||||
])
|
||||
})
|
||||
|
||||
it('handles garbage', function () {
|
||||
const output = '2 + 2 = 4'
|
||||
const records = this.SynctexOutputParser.parseEditOutput(output)
|
||||
expect(records).to.deep.equal([])
|
||||
})
|
||||
})
|
||||
})
|
185
services/clsi/test/unit/js/TikzManager.js
Normal file
185
services/clsi/test/unit/js/TikzManager.js
Normal file
@@ -0,0 +1,185 @@
|
||||
/* eslint-disable
|
||||
no-return-assign,
|
||||
*/
|
||||
// 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 SandboxedModule = require('sandboxed-module')
|
||||
const sinon = require('sinon')
|
||||
const modulePath = require('node:path').join(
|
||||
__dirname,
|
||||
'../../../app/js/TikzManager'
|
||||
)
|
||||
|
||||
describe('TikzManager', function () {
|
||||
beforeEach(function () {
|
||||
return (this.TikzManager = SandboxedModule.require(modulePath, {
|
||||
requires: {
|
||||
'./ResourceWriter': (this.ResourceWriter = {}),
|
||||
'./SafeReader': (this.SafeReader = {}),
|
||||
fs: (this.fs = {}),
|
||||
},
|
||||
}))
|
||||
})
|
||||
|
||||
describe('checkMainFile', function () {
|
||||
beforeEach(function () {
|
||||
this.compileDir = 'compile-dir'
|
||||
this.mainFile = 'main.tex'
|
||||
return (this.callback = sinon.stub())
|
||||
})
|
||||
|
||||
describe('if there is already an output.tex file in the resources', function () {
|
||||
beforeEach(function () {
|
||||
this.resources = [{ path: 'main.tex' }, { path: 'output.tex' }]
|
||||
return this.TikzManager.checkMainFile(
|
||||
this.compileDir,
|
||||
this.mainFile,
|
||||
this.resources,
|
||||
this.callback
|
||||
)
|
||||
})
|
||||
|
||||
return it('should call the callback with false ', function () {
|
||||
return this.callback.calledWithExactly(null, false).should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
return describe('if there is no output.tex file in the resources', function () {
|
||||
beforeEach(function () {
|
||||
this.resources = [{ path: 'main.tex' }]
|
||||
return (this.ResourceWriter.checkPath = sinon
|
||||
.stub()
|
||||
.withArgs(this.compileDir, this.mainFile)
|
||||
.callsArgWith(2, null, `${this.compileDir}/${this.mainFile}`))
|
||||
})
|
||||
|
||||
describe('and the main file contains tikzexternalize', function () {
|
||||
beforeEach(function () {
|
||||
this.SafeReader.readFile = sinon
|
||||
.stub()
|
||||
.withArgs(`${this.compileDir}/${this.mainFile}`)
|
||||
.callsArgWith(3, null, 'hello \\tikzexternalize')
|
||||
return this.TikzManager.checkMainFile(
|
||||
this.compileDir,
|
||||
this.mainFile,
|
||||
this.resources,
|
||||
this.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should look at the file on disk', function () {
|
||||
return this.SafeReader.readFile
|
||||
.calledWith(`${this.compileDir}/${this.mainFile}`)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
return it('should call the callback with true ', function () {
|
||||
return this.callback.calledWithExactly(null, true).should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('and the main file does not contain tikzexternalize', function () {
|
||||
beforeEach(function () {
|
||||
this.SafeReader.readFile = sinon
|
||||
.stub()
|
||||
.withArgs(`${this.compileDir}/${this.mainFile}`)
|
||||
.callsArgWith(3, null, 'hello')
|
||||
return this.TikzManager.checkMainFile(
|
||||
this.compileDir,
|
||||
this.mainFile,
|
||||
this.resources,
|
||||
this.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should look at the file on disk', function () {
|
||||
return this.SafeReader.readFile
|
||||
.calledWith(`${this.compileDir}/${this.mainFile}`)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
return it('should call the callback with false', function () {
|
||||
return this.callback.calledWithExactly(null, false).should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
return describe('and the main file contains \\usepackage{pstool}', function () {
|
||||
beforeEach(function () {
|
||||
this.SafeReader.readFile = sinon
|
||||
.stub()
|
||||
.withArgs(`${this.compileDir}/${this.mainFile}`)
|
||||
.callsArgWith(3, null, 'hello \\usepackage[random-options]{pstool}')
|
||||
return this.TikzManager.checkMainFile(
|
||||
this.compileDir,
|
||||
this.mainFile,
|
||||
this.resources,
|
||||
this.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should look at the file on disk', function () {
|
||||
return this.SafeReader.readFile
|
||||
.calledWith(`${this.compileDir}/${this.mainFile}`)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
return it('should call the callback with true ', function () {
|
||||
return this.callback.calledWithExactly(null, true).should.equal(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
return describe('injectOutputFile', function () {
|
||||
beforeEach(function () {
|
||||
this.rootDir = '/mock'
|
||||
this.filename = 'filename.tex'
|
||||
this.callback = sinon.stub()
|
||||
this.content = `\
|
||||
\\documentclass{article}
|
||||
\\usepackage{tikz}
|
||||
\\tikzexternalize
|
||||
\\begin{document}
|
||||
Hello world
|
||||
\\end{document}\
|
||||
`
|
||||
this.fs.readFile = sinon.stub().callsArgWith(2, null, this.content)
|
||||
this.fs.writeFile = sinon.stub().callsArg(3)
|
||||
this.ResourceWriter.checkPath = sinon
|
||||
.stub()
|
||||
.callsArgWith(2, null, `${this.rootDir}/${this.filename}`)
|
||||
return this.TikzManager.injectOutputFile(
|
||||
this.rootDir,
|
||||
this.filename,
|
||||
this.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('sould check the path', function () {
|
||||
return this.ResourceWriter.checkPath
|
||||
.calledWith(this.rootDir, this.filename)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should read the file', function () {
|
||||
return this.fs.readFile
|
||||
.calledWith(`${this.rootDir}/${this.filename}`, 'utf8')
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should write out the same file as output.tex', function () {
|
||||
return this.fs.writeFile
|
||||
.calledWith(`${this.rootDir}/output.tex`, this.content, { flag: 'wx' })
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
return it('should call the callback', function () {
|
||||
return this.callback.called.should.equal(true)
|
||||
})
|
||||
})
|
||||
})
|
148
services/clsi/test/unit/js/UrlCacheTests.js
Normal file
148
services/clsi/test/unit/js/UrlCacheTests.js
Normal file
@@ -0,0 +1,148 @@
|
||||
/* 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
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
const SandboxedModule = require('sandboxed-module')
|
||||
const sinon = require('sinon')
|
||||
const { expect } = require('chai')
|
||||
const modulePath = require('node:path').join(
|
||||
__dirname,
|
||||
'../../../app/js/UrlCache'
|
||||
)
|
||||
|
||||
describe('UrlCache', function () {
|
||||
beforeEach(function () {
|
||||
this.callback = sinon.stub()
|
||||
this.url =
|
||||
'http://filestore/project/60b0dd39c418bc00598a0d22/file/60ae721ffb1d920027d3201f'
|
||||
this.fallbackURL = 'http://filestore/bucket/project-blobs/key/ab/cd/ef'
|
||||
this.project_id = '60b0dd39c418bc00598a0d22'
|
||||
return (this.UrlCache = SandboxedModule.require(modulePath, {
|
||||
requires: {
|
||||
'./UrlFetcher': (this.UrlFetcher = {
|
||||
promises: { pipeUrlToFileWithRetry: sinon.stub().resolves() },
|
||||
}),
|
||||
'@overleaf/settings': (this.Settings = {
|
||||
path: { clsiCacheDir: '/cache/dir' },
|
||||
}),
|
||||
'@overleaf/metrics': {
|
||||
Timer: sinon.stub().returns({ done: sinon.stub() }),
|
||||
},
|
||||
fs: (this.fs = {
|
||||
promises: {
|
||||
rm: sinon.stub().resolves(),
|
||||
copyFile: sinon.stub().resolves(),
|
||||
},
|
||||
}),
|
||||
},
|
||||
}))
|
||||
})
|
||||
|
||||
describe('downloadUrlToFile', function () {
|
||||
beforeEach(function () {
|
||||
this.destPath = 'path/to/destination'
|
||||
})
|
||||
|
||||
it('should not download on the happy path', function (done) {
|
||||
this.UrlCache.downloadUrlToFile(
|
||||
this.project_id,
|
||||
this.url,
|
||||
this.fallbackURL,
|
||||
this.destPath,
|
||||
this.lastModified,
|
||||
error => {
|
||||
expect(error).to.not.exist
|
||||
expect(
|
||||
this.UrlFetcher.promises.pipeUrlToFileWithRetry.called
|
||||
).to.equal(false)
|
||||
done()
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('should not download on the semi-happy path', function (done) {
|
||||
const codedError = new Error()
|
||||
codedError.code = 'ENOENT'
|
||||
this.fs.promises.copyFile.onCall(0).rejects(codedError)
|
||||
this.fs.promises.copyFile.onCall(1).resolves()
|
||||
|
||||
this.UrlCache.downloadUrlToFile(
|
||||
this.project_id,
|
||||
this.url,
|
||||
this.fallbackURL,
|
||||
this.destPath,
|
||||
this.lastModified,
|
||||
error => {
|
||||
expect(error).to.not.exist
|
||||
expect(
|
||||
this.UrlFetcher.promises.pipeUrlToFileWithRetry.called
|
||||
).to.equal(false)
|
||||
done()
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('should download on cache miss', function (done) {
|
||||
const codedError = new Error()
|
||||
codedError.code = 'ENOENT'
|
||||
this.fs.promises.copyFile.onCall(0).rejects(codedError)
|
||||
this.fs.promises.copyFile.onCall(1).rejects(codedError)
|
||||
this.fs.promises.copyFile.onCall(2).resolves()
|
||||
|
||||
this.UrlCache.downloadUrlToFile(
|
||||
this.project_id,
|
||||
this.url,
|
||||
this.fallbackURL,
|
||||
this.destPath,
|
||||
this.lastModified,
|
||||
error => {
|
||||
expect(error).to.not.exist
|
||||
expect(
|
||||
this.UrlFetcher.promises.pipeUrlToFileWithRetry.called
|
||||
).to.equal(true)
|
||||
done()
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('should raise non cache-miss errors', function (done) {
|
||||
const codedError = new Error()
|
||||
codedError.code = 'FOO'
|
||||
this.fs.promises.copyFile.rejects(codedError)
|
||||
this.UrlCache.downloadUrlToFile(
|
||||
this.project_id,
|
||||
this.url,
|
||||
this.fallbackURL,
|
||||
this.destPath,
|
||||
this.lastModified,
|
||||
error => {
|
||||
expect(error).to.equal(codedError)
|
||||
done()
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('clearProject', function () {
|
||||
beforeEach(function (done) {
|
||||
this.UrlCache.clearProject(this.project_id, done)
|
||||
})
|
||||
|
||||
it('should clear the cache in bulk', function () {
|
||||
expect(
|
||||
this.fs.promises.rm.calledWith('/cache/dir/' + this.project_id, {
|
||||
force: true,
|
||||
recursive: true,
|
||||
})
|
||||
).to.equal(true)
|
||||
})
|
||||
})
|
||||
})
|
93
services/clsi/test/unit/js/pdfjsTests.js
Normal file
93
services/clsi/test/unit/js/pdfjsTests.js
Normal file
@@ -0,0 +1,93 @@
|
||||
const fs = require('node:fs')
|
||||
const Path = require('node:path')
|
||||
const { expect } = require('chai')
|
||||
const { parseXrefTable } = require('../../../app/js/XrefParser')
|
||||
const { NoXrefTableError } = require('../../../app/js/Errors')
|
||||
const PATH_EXAMPLES = 'test/acceptance/fixtures/examples/'
|
||||
const PATH_SNAPSHOTS = 'test/unit/js/snapshots/pdfjs/'
|
||||
const EXAMPLES = fs.readdirSync(PATH_EXAMPLES)
|
||||
|
||||
function snapshotPath(example) {
|
||||
return Path.join(PATH_SNAPSHOTS, example, 'XrefTable.json')
|
||||
}
|
||||
|
||||
function pdfPath(example) {
|
||||
return Path.join(PATH_EXAMPLES, example, 'output.pdf')
|
||||
}
|
||||
|
||||
async function loadContext(example) {
|
||||
const size = (await fs.promises.stat(pdfPath(example))).size
|
||||
|
||||
let blob
|
||||
try {
|
||||
blob = await fs.promises.readFile(snapshotPath(example))
|
||||
} catch (e) {
|
||||
if (e.code !== 'ENOENT') {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
const snapshot = blob ? JSON.parse(blob) : null
|
||||
return {
|
||||
size,
|
||||
snapshot,
|
||||
}
|
||||
}
|
||||
|
||||
async function backFillSnapshot(example, size) {
|
||||
const table = await parseXrefTable(pdfPath(example), size, () => {})
|
||||
await fs.promises.mkdir(Path.dirname(snapshotPath(example)), {
|
||||
recursive: true,
|
||||
})
|
||||
await fs.promises.writeFile(
|
||||
snapshotPath(example),
|
||||
JSON.stringify(table, null, 2)
|
||||
)
|
||||
return table
|
||||
}
|
||||
|
||||
describe('pdfjs', function () {
|
||||
describe('when the pdf is an empty file', function () {
|
||||
it('should yield no entries', async function () {
|
||||
const path = 'does/not/matter.pdf'
|
||||
let table
|
||||
try {
|
||||
table = await parseXrefTable(path, 0)
|
||||
} catch (e) {
|
||||
expect(e).to.be.an.instanceof(NoXrefTableError)
|
||||
}
|
||||
expect(table).to.not.exist
|
||||
})
|
||||
})
|
||||
|
||||
for (const example of EXAMPLES) {
|
||||
describe(example, function () {
|
||||
let size, snapshot
|
||||
before('load snapshot', async function () {
|
||||
const ctx = await loadContext(example)
|
||||
size = ctx.size
|
||||
snapshot = ctx.snapshot
|
||||
})
|
||||
|
||||
before('back fill new snapshot', async function () {
|
||||
if (snapshot === null) {
|
||||
console.error('back filling snapshot for', example)
|
||||
snapshot = await backFillSnapshot(example, size)
|
||||
}
|
||||
})
|
||||
|
||||
it('should produce the expected xRef table', async function () {
|
||||
const table = await parseXrefTable(pdfPath(example), size, () => {})
|
||||
// compare the essential parts of the xref table only
|
||||
expect(table.xRefEntries[0]).to.include({ offset: 0 })
|
||||
expect(table.xRefEntries.slice(1)).to.deep.equal(
|
||||
snapshot.xRefEntries
|
||||
.slice(1)
|
||||
.filter(xref => xref.uncompressed) // we only use the uncompressed fields
|
||||
.map(xref => {
|
||||
return { offset: xref.offset, uncompressed: xref.uncompressed } // ignore unused gen field
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
@@ -0,0 +1,7 @@
|
||||
obj
|
||||
<< /Type /ObjStm /Length 447 /Filter /FlateDecode /N 5 /First 32 >>
|
||||
stream
|
||||
x<EFBFBD><EFBFBD>RQk<EFBFBD>0~߯<>ǍK'ɒ<0C><>$<24>vkGIJ[(y<><12><18>8<EFBFBD>*<2A><><EFBFBD><EFBFBD>$<24>I<EFBFBD>R<EFBFBD>d<EFBFBD><64><EFBFBD>><3E>I"H@<05><06><>9@J!` V<>/gg f<>>BZx<5A>J<EFBFBD><4A><EFBFBD>9<EFBFBD>ۮ]-B<>'ZNg <20><>k<EFBFBD>%<25>i\<5C>!<11><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>f<EFBFBD><66>4<EFBFBD><34><EFBFBD><EFBFBD>m݁<6D><DD81><EFBFBD>4<EFBFBD><34><EFBFBD>9Ķ<39>CY]\<5C><06>@<40><><EFBFBD><EFBFBD><EFBFBD>!<21> 4c<34>
|
||||
U<EFBFBD><EFBFBD>f<1D>=<17><>JgO<67><17><11>g<EFBFBD><67><EFBFBD>><3E><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>zz<>>A)<29><>C9W<39>w<EFBFBD>K<EFBFBD>q<EFBFBD>PÜ#<23><><EFBFBD><EFBFBD>/48<34><38>VX/<0B>T<EFBFBD><54><EFBFBD>p <09>-%2"<22><>*<2A>B;<3B><>X<EFBFBD><0E><>2<EFBFBD>,9<><39>G<03>z;<3B><>E<EFBFBD>Υ<1B><>Jj/<2F>c
|
||||
<EFBFBD>n<EFBFBD>%<25><><EFBFBD><EFBFBD><18><><EFBFBD>ᵦf3<66><33><EFBFBD>]!<21><><EFBFBD><EFBFBD><EFBFBD>=<3D>y<EFBFBD><79>
|
||||
,<2C>s]<5D><>@<40>e+COW.C<><43><EFBFBD><EFBFBD>kڒ <09>c_ťX<C5A5> v<><15>><3E>N<EFBFBD><4E><06><><EFBFBD><EFBFBD>2u<32><75>7=<3D><><EFBFBD><EFBFBD><EFBFBD>}<7D><> #<23><>HV<48>r<EFBFBD>9?kv<01><>6<EFBFBD><36><EFBFBD><EFBFBD><EFBFBD>G<EFBFBD>^<5E>z.<2E><1B><>v<11><>=Uyj<79><13><>ǡp<C7A1><70><EFBFBD><EFBFBD>z<><7A>2
|
Binary file not shown.
@@ -0,0 +1,359 @@
|
||||
{
|
||||
"xRefEntries": [
|
||||
{
|
||||
"offset": 0,
|
||||
"gen": 0,
|
||||
"free": true
|
||||
},
|
||||
{
|
||||
"offset": 123103,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 123422,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 15,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 216,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 1084,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 1244,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 4001,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 4155,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 4297,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 4933,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 5309,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 5498,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 30250,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 31471,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 38404,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 39046,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 40166,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 40906,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 65560,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 74702,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 81705,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 97182,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 104117,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 111195,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 118571,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 6,
|
||||
"gen": 0
|
||||
},
|
||||
{
|
||||
"offset": 6,
|
||||
"gen": 1
|
||||
},
|
||||
{
|
||||
"offset": 6,
|
||||
"gen": 2
|
||||
},
|
||||
{
|
||||
"offset": 6,
|
||||
"gen": 3
|
||||
},
|
||||
{
|
||||
"offset": 6,
|
||||
"gen": 4
|
||||
},
|
||||
{
|
||||
"offset": 6,
|
||||
"gen": 5
|
||||
},
|
||||
{
|
||||
"offset": 6,
|
||||
"gen": 6
|
||||
},
|
||||
{
|
||||
"offset": 6,
|
||||
"gen": 7
|
||||
},
|
||||
{
|
||||
"offset": 6,
|
||||
"gen": 8
|
||||
},
|
||||
{
|
||||
"offset": 6,
|
||||
"gen": 9
|
||||
},
|
||||
{
|
||||
"offset": 6,
|
||||
"gen": 10
|
||||
},
|
||||
{
|
||||
"offset": 6,
|
||||
"gen": 11
|
||||
},
|
||||
{
|
||||
"offset": 6,
|
||||
"gen": 12
|
||||
},
|
||||
{
|
||||
"offset": 6,
|
||||
"gen": 13
|
||||
},
|
||||
{
|
||||
"offset": 6,
|
||||
"gen": 14
|
||||
},
|
||||
{
|
||||
"offset": 6,
|
||||
"gen": 15
|
||||
},
|
||||
{
|
||||
"offset": 6,
|
||||
"gen": 16
|
||||
},
|
||||
{
|
||||
"offset": 6,
|
||||
"gen": 17
|
||||
},
|
||||
{
|
||||
"offset": 6,
|
||||
"gen": 18
|
||||
},
|
||||
{
|
||||
"offset": 6,
|
||||
"gen": 19
|
||||
},
|
||||
{
|
||||
"offset": 6,
|
||||
"gen": 20
|
||||
},
|
||||
{
|
||||
"offset": 6,
|
||||
"gen": 21
|
||||
},
|
||||
{
|
||||
"offset": 6,
|
||||
"gen": 22
|
||||
},
|
||||
{
|
||||
"offset": 6,
|
||||
"gen": 23
|
||||
},
|
||||
{
|
||||
"offset": 6,
|
||||
"gen": 24
|
||||
},
|
||||
{
|
||||
"offset": 6,
|
||||
"gen": 25
|
||||
},
|
||||
{
|
||||
"offset": 6,
|
||||
"gen": 26
|
||||
},
|
||||
{
|
||||
"offset": 6,
|
||||
"gen": 27
|
||||
},
|
||||
{
|
||||
"offset": 6,
|
||||
"gen": 28
|
||||
},
|
||||
{
|
||||
"offset": 6,
|
||||
"gen": 29
|
||||
},
|
||||
{
|
||||
"offset": 6,
|
||||
"gen": 30
|
||||
},
|
||||
{
|
||||
"offset": 6,
|
||||
"gen": 31
|
||||
},
|
||||
{
|
||||
"offset": 6,
|
||||
"gen": 32
|
||||
},
|
||||
{
|
||||
"offset": 6,
|
||||
"gen": 33
|
||||
},
|
||||
{
|
||||
"offset": 6,
|
||||
"gen": 34
|
||||
},
|
||||
{
|
||||
"offset": 6,
|
||||
"gen": 35
|
||||
},
|
||||
{
|
||||
"offset": 6,
|
||||
"gen": 36
|
||||
},
|
||||
{
|
||||
"offset": 6,
|
||||
"gen": 37
|
||||
},
|
||||
{
|
||||
"offset": 6,
|
||||
"gen": 38
|
||||
},
|
||||
{
|
||||
"offset": 6,
|
||||
"gen": 39
|
||||
},
|
||||
{
|
||||
"offset": 6,
|
||||
"gen": 40
|
||||
},
|
||||
{
|
||||
"offset": 6,
|
||||
"gen": 41
|
||||
},
|
||||
{
|
||||
"offset": 6,
|
||||
"gen": 42
|
||||
},
|
||||
{
|
||||
"offset": 6,
|
||||
"gen": 43
|
||||
},
|
||||
{
|
||||
"offset": 6,
|
||||
"gen": 44
|
||||
},
|
||||
{
|
||||
"offset": 6,
|
||||
"gen": 45
|
||||
},
|
||||
{
|
||||
"offset": 6,
|
||||
"gen": 46
|
||||
},
|
||||
{
|
||||
"offset": 6,
|
||||
"gen": 47
|
||||
},
|
||||
{
|
||||
"offset": 6,
|
||||
"gen": 48
|
||||
},
|
||||
{
|
||||
"offset": 6,
|
||||
"gen": 49
|
||||
},
|
||||
{
|
||||
"offset": 6,
|
||||
"gen": 50
|
||||
},
|
||||
{
|
||||
"offset": 6,
|
||||
"gen": 51
|
||||
},
|
||||
{
|
||||
"offset": 6,
|
||||
"gen": 52
|
||||
},
|
||||
{
|
||||
"offset": 6,
|
||||
"gen": 53
|
||||
},
|
||||
{
|
||||
"offset": 6,
|
||||
"gen": 54
|
||||
},
|
||||
{
|
||||
"offset": 6,
|
||||
"gen": 55
|
||||
}
|
||||
],
|
||||
"startXRefTable": 123422
|
||||
}
|
@@ -0,0 +1,148 @@
|
||||
{
|
||||
"xRefEntries": [
|
||||
{
|
||||
"offset": 0,
|
||||
"gen": 0,
|
||||
"free": true
|
||||
},
|
||||
{
|
||||
"offset": 59313,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 59561,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 15,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 216,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 734,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 784,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 913,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 1028,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 1528,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 9787,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 18282,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 33607,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 45579,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 58005,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 14,
|
||||
"gen": 0
|
||||
},
|
||||
{
|
||||
"offset": 14,
|
||||
"gen": 1
|
||||
},
|
||||
{
|
||||
"offset": 14,
|
||||
"gen": 2
|
||||
},
|
||||
{
|
||||
"offset": 14,
|
||||
"gen": 3
|
||||
},
|
||||
{
|
||||
"offset": 14,
|
||||
"gen": 4
|
||||
},
|
||||
{
|
||||
"offset": 14,
|
||||
"gen": 5
|
||||
},
|
||||
{
|
||||
"offset": 14,
|
||||
"gen": 6
|
||||
},
|
||||
{
|
||||
"offset": 14,
|
||||
"gen": 7
|
||||
},
|
||||
{
|
||||
"offset": 14,
|
||||
"gen": 8
|
||||
},
|
||||
{
|
||||
"offset": 14,
|
||||
"gen": 9
|
||||
},
|
||||
{
|
||||
"offset": 14,
|
||||
"gen": 10
|
||||
},
|
||||
{
|
||||
"offset": 14,
|
||||
"gen": 11
|
||||
},
|
||||
{
|
||||
"offset": 14,
|
||||
"gen": 12
|
||||
},
|
||||
{
|
||||
"offset": 14,
|
||||
"gen": 13
|
||||
},
|
||||
{
|
||||
"offset": 14,
|
||||
"gen": 14
|
||||
},
|
||||
{
|
||||
"offset": 14,
|
||||
"gen": 15
|
||||
},
|
||||
{
|
||||
"offset": 14,
|
||||
"gen": 16
|
||||
}
|
||||
],
|
||||
"startXRefTable": 59561
|
||||
}
|
@@ -0,0 +1,182 @@
|
||||
{
|
||||
"xRefEntries": [
|
||||
{
|
||||
"offset": 0,
|
||||
"gen": 0,
|
||||
"free": true
|
||||
},
|
||||
{
|
||||
"offset": 67338,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 67606,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 15,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 216,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 790,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 840,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 975,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 1083,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 1578,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 9881,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 17868,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 29906,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 38400,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 46656,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 56198,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 65682,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 0
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 1
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 2
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 3
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 4
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 5
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 6
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 7
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 8
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 9
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 10
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 11
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 12
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 13
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 14
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 15
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 16
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 17
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 18
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 19
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 20
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 21
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 22
|
||||
}
|
||||
],
|
||||
"startXRefTable": 67606
|
||||
}
|
@@ -0,0 +1,226 @@
|
||||
{
|
||||
"xRefEntries": [
|
||||
{
|
||||
"offset": 0,
|
||||
"gen": 0,
|
||||
"free": true
|
||||
},
|
||||
{
|
||||
"offset": 69708,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 70038,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 15,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 216,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 867,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 990,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 1143,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 1251,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 1834,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 10137,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 18124,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 31939,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 40433,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 48689,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 58231,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 67715,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 0
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 1
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 2
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 3
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 4
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 5
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 6
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 7
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 8
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 9
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 10
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 11
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 12
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 13
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 14
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 15
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 16
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 17
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 18
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 19
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 20
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 21
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 22
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 23
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 24
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 25
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 26
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 27
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 28
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 29
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 30
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 31
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 32
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 33
|
||||
}
|
||||
],
|
||||
"startXRefTable": 70038
|
||||
}
|
@@ -0,0 +1,145 @@
|
||||
{
|
||||
"xRefEntries": [
|
||||
{
|
||||
"offset": 0,
|
||||
"gen": 0,
|
||||
"free": true
|
||||
},
|
||||
{
|
||||
"offset": 31354,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 31614,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 15,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 216,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 727,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 777,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 909,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 1017,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 19127,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 19313,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 19557,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 19948,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 20677,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 23321,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 30318,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 15,
|
||||
"gen": 0
|
||||
},
|
||||
{
|
||||
"offset": 15,
|
||||
"gen": 1
|
||||
},
|
||||
{
|
||||
"offset": 15,
|
||||
"gen": 2
|
||||
},
|
||||
{
|
||||
"offset": 15,
|
||||
"gen": 3
|
||||
},
|
||||
{
|
||||
"offset": 15,
|
||||
"gen": 4
|
||||
},
|
||||
{
|
||||
"offset": 15,
|
||||
"gen": 5
|
||||
},
|
||||
{
|
||||
"offset": 15,
|
||||
"gen": 6
|
||||
},
|
||||
{
|
||||
"offset": 15,
|
||||
"gen": 7
|
||||
},
|
||||
{
|
||||
"offset": 15,
|
||||
"gen": 8
|
||||
},
|
||||
{
|
||||
"offset": 15,
|
||||
"gen": 9
|
||||
},
|
||||
{
|
||||
"offset": 15,
|
||||
"gen": 10
|
||||
},
|
||||
{
|
||||
"offset": 15,
|
||||
"gen": 11
|
||||
},
|
||||
{
|
||||
"offset": 15,
|
||||
"gen": 12
|
||||
},
|
||||
{
|
||||
"offset": 15,
|
||||
"gen": 13
|
||||
},
|
||||
{
|
||||
"offset": 15,
|
||||
"gen": 14
|
||||
}
|
||||
],
|
||||
"startXRefTable": 31614
|
||||
}
|
135
services/clsi/test/unit/js/snapshots/pdfjs/feynmf/XrefTable.json
Normal file
135
services/clsi/test/unit/js/snapshots/pdfjs/feynmf/XrefTable.json
Normal file
@@ -0,0 +1,135 @@
|
||||
{
|
||||
"xRefEntries": [
|
||||
{
|
||||
"offset": 0,
|
||||
"gen": 0,
|
||||
"free": true
|
||||
},
|
||||
{
|
||||
"offset": 27064,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 27312,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 15,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 216,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 713,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 763,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 892,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 1007,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 1235,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 4832,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 12199,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 19196,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 26341,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 13,
|
||||
"gen": 0
|
||||
},
|
||||
{
|
||||
"offset": 13,
|
||||
"gen": 1
|
||||
},
|
||||
{
|
||||
"offset": 13,
|
||||
"gen": 2
|
||||
},
|
||||
{
|
||||
"offset": 13,
|
||||
"gen": 3
|
||||
},
|
||||
{
|
||||
"offset": 13,
|
||||
"gen": 4
|
||||
},
|
||||
{
|
||||
"offset": 13,
|
||||
"gen": 5
|
||||
},
|
||||
{
|
||||
"offset": 13,
|
||||
"gen": 6
|
||||
},
|
||||
{
|
||||
"offset": 13,
|
||||
"gen": 7
|
||||
},
|
||||
{
|
||||
"offset": 13,
|
||||
"gen": 8
|
||||
},
|
||||
{
|
||||
"offset": 13,
|
||||
"gen": 9
|
||||
},
|
||||
{
|
||||
"offset": 13,
|
||||
"gen": 10
|
||||
},
|
||||
{
|
||||
"offset": 13,
|
||||
"gen": 11
|
||||
},
|
||||
{
|
||||
"offset": 13,
|
||||
"gen": 12
|
||||
},
|
||||
{
|
||||
"offset": 13,
|
||||
"gen": 13
|
||||
},
|
||||
{
|
||||
"offset": 13,
|
||||
"gen": 14
|
||||
}
|
||||
],
|
||||
"startXRefTable": 27312
|
||||
}
|
110
services/clsi/test/unit/js/snapshots/pdfjs/feynmp/XrefTable.json
Normal file
110
services/clsi/test/unit/js/snapshots/pdfjs/feynmp/XrefTable.json
Normal file
@@ -0,0 +1,110 @@
|
||||
{
|
||||
"xRefEntries": [
|
||||
{
|
||||
"offset": 0,
|
||||
"gen": 65535,
|
||||
"free": true
|
||||
},
|
||||
{
|
||||
"offset": 4964,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 5023,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 5234,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 15,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 734,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 799,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 933,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 1104,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 1947,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 1992,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 2182,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 2427,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 2597,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 2822,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 2989,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 3239,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 3271,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 3328,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 3740,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 4270,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
}
|
||||
],
|
||||
"startXRefTable": 6682
|
||||
}
|
@@ -0,0 +1,113 @@
|
||||
{
|
||||
"xRefEntries": [
|
||||
{
|
||||
"offset": 0,
|
||||
"gen": 0,
|
||||
"free": true
|
||||
},
|
||||
{
|
||||
"offset": 31058,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 31307,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 15,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 216,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 678,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 728,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 855,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 970,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 1203,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 18852,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 30165,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 11,
|
||||
"gen": 0
|
||||
},
|
||||
{
|
||||
"offset": 11,
|
||||
"gen": 1
|
||||
},
|
||||
{
|
||||
"offset": 11,
|
||||
"gen": 2
|
||||
},
|
||||
{
|
||||
"offset": 11,
|
||||
"gen": 3
|
||||
},
|
||||
{
|
||||
"offset": 11,
|
||||
"gen": 4
|
||||
},
|
||||
{
|
||||
"offset": 11,
|
||||
"gen": 5
|
||||
},
|
||||
{
|
||||
"offset": 11,
|
||||
"gen": 6
|
||||
},
|
||||
{
|
||||
"offset": 11,
|
||||
"gen": 7
|
||||
},
|
||||
{
|
||||
"offset": 11,
|
||||
"gen": 8
|
||||
},
|
||||
{
|
||||
"offset": 11,
|
||||
"gen": 9
|
||||
},
|
||||
{
|
||||
"offset": 11,
|
||||
"gen": 10
|
||||
},
|
||||
{
|
||||
"offset": 11,
|
||||
"gen": 11
|
||||
}
|
||||
],
|
||||
"startXRefTable": 31307
|
||||
}
|
@@ -0,0 +1,129 @@
|
||||
{
|
||||
"xRefEntries": [
|
||||
{
|
||||
"offset": 0,
|
||||
"gen": 0,
|
||||
"free": true
|
||||
},
|
||||
{
|
||||
"offset": 6344,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 15,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 216,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 707,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 757,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 887,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 990,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 1257,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 1679,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 2052,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 4249,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 4343,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 5387,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 5481,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 5519,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 15,
|
||||
"gen": 0
|
||||
},
|
||||
{
|
||||
"offset": 15,
|
||||
"gen": 1
|
||||
},
|
||||
{
|
||||
"offset": 15,
|
||||
"gen": 2
|
||||
},
|
||||
{
|
||||
"offset": 15,
|
||||
"gen": 3
|
||||
},
|
||||
{
|
||||
"offset": 15,
|
||||
"gen": 4
|
||||
},
|
||||
{
|
||||
"offset": 15,
|
||||
"gen": 5
|
||||
},
|
||||
{
|
||||
"offset": 15,
|
||||
"gen": 6
|
||||
},
|
||||
{
|
||||
"offset": 15,
|
||||
"gen": 7
|
||||
},
|
||||
{
|
||||
"offset": 15,
|
||||
"gen": 8
|
||||
},
|
||||
{
|
||||
"offset": 15,
|
||||
"gen": 9
|
||||
},
|
||||
{
|
||||
"offset": 15,
|
||||
"gen": 10
|
||||
}
|
||||
],
|
||||
"startXRefTable": 6344
|
||||
}
|
@@ -0,0 +1,114 @@
|
||||
{
|
||||
"xRefEntries": [
|
||||
{
|
||||
"offset": 0,
|
||||
"gen": 0,
|
||||
"free": true
|
||||
},
|
||||
{
|
||||
"offset": 34767,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 35015,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 15,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 216,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 678,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 728,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 856,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 971,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 1394,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 10990,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 19087,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 33769,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 12,
|
||||
"gen": 0
|
||||
},
|
||||
{
|
||||
"offset": 12,
|
||||
"gen": 1
|
||||
},
|
||||
{
|
||||
"offset": 12,
|
||||
"gen": 2
|
||||
},
|
||||
{
|
||||
"offset": 12,
|
||||
"gen": 3
|
||||
},
|
||||
{
|
||||
"offset": 12,
|
||||
"gen": 4
|
||||
},
|
||||
{
|
||||
"offset": 12,
|
||||
"gen": 5
|
||||
},
|
||||
{
|
||||
"offset": 12,
|
||||
"gen": 6
|
||||
},
|
||||
{
|
||||
"offset": 12,
|
||||
"gen": 7
|
||||
},
|
||||
{
|
||||
"offset": 12,
|
||||
"gen": 8
|
||||
},
|
||||
{
|
||||
"offset": 12,
|
||||
"gen": 9
|
||||
},
|
||||
{
|
||||
"offset": 12,
|
||||
"gen": 10
|
||||
}
|
||||
],
|
||||
"startXRefTable": 35015
|
||||
}
|
@@ -0,0 +1,109 @@
|
||||
{
|
||||
"xRefEntries": [
|
||||
{
|
||||
"offset": 0,
|
||||
"gen": 0,
|
||||
"free": true
|
||||
},
|
||||
{
|
||||
"offset": 23295,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 23543,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 15,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 216,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 671,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 721,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 847,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 955,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 7385,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 15752,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 22721,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 11,
|
||||
"gen": 0
|
||||
},
|
||||
{
|
||||
"offset": 11,
|
||||
"gen": 1
|
||||
},
|
||||
{
|
||||
"offset": 11,
|
||||
"gen": 2
|
||||
},
|
||||
{
|
||||
"offset": 11,
|
||||
"gen": 3
|
||||
},
|
||||
{
|
||||
"offset": 11,
|
||||
"gen": 4
|
||||
},
|
||||
{
|
||||
"offset": 11,
|
||||
"gen": 5
|
||||
},
|
||||
{
|
||||
"offset": 11,
|
||||
"gen": 6
|
||||
},
|
||||
{
|
||||
"offset": 11,
|
||||
"gen": 7
|
||||
},
|
||||
{
|
||||
"offset": 11,
|
||||
"gen": 8
|
||||
},
|
||||
{
|
||||
"offset": 11,
|
||||
"gen": 9
|
||||
},
|
||||
{
|
||||
"offset": 11,
|
||||
"gen": 10
|
||||
}
|
||||
],
|
||||
"startXRefTable": 23543
|
||||
}
|
101
services/clsi/test/unit/js/snapshots/pdfjs/hebrew/XrefTable.json
Normal file
101
services/clsi/test/unit/js/snapshots/pdfjs/hebrew/XrefTable.json
Normal file
@@ -0,0 +1,101 @@
|
||||
{
|
||||
"xRefEntries": [
|
||||
{
|
||||
"offset": 0,
|
||||
"gen": 0,
|
||||
"free": true
|
||||
},
|
||||
{
|
||||
"offset": 24490,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 24739,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 15,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 216,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 657,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 707,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 833,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 948,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 1290,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 13083,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 23411,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 11,
|
||||
"gen": 0
|
||||
},
|
||||
{
|
||||
"offset": 11,
|
||||
"gen": 1
|
||||
},
|
||||
{
|
||||
"offset": 11,
|
||||
"gen": 2
|
||||
},
|
||||
{
|
||||
"offset": 11,
|
||||
"gen": 3
|
||||
},
|
||||
{
|
||||
"offset": 11,
|
||||
"gen": 4
|
||||
},
|
||||
{
|
||||
"offset": 11,
|
||||
"gen": 5
|
||||
},
|
||||
{
|
||||
"offset": 11,
|
||||
"gen": 6
|
||||
},
|
||||
{
|
||||
"offset": 11,
|
||||
"gen": 7
|
||||
},
|
||||
{
|
||||
"offset": 11,
|
||||
"gen": 8
|
||||
}
|
||||
],
|
||||
"startXRefTable": 24739
|
||||
}
|
148
services/clsi/test/unit/js/snapshots/pdfjs/knitr/XrefTable.json
Normal file
148
services/clsi/test/unit/js/snapshots/pdfjs/knitr/XrefTable.json
Normal file
@@ -0,0 +1,148 @@
|
||||
{
|
||||
"xRefEntries": [
|
||||
{
|
||||
"offset": 0,
|
||||
"gen": 0,
|
||||
"free": true
|
||||
},
|
||||
{
|
||||
"offset": 43550,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 43799,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 15,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 216,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 734,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 784,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 913,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 1021,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 1546,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 5794,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 12915,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 23660,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 30657,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 42604,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 14,
|
||||
"gen": 0
|
||||
},
|
||||
{
|
||||
"offset": 14,
|
||||
"gen": 1
|
||||
},
|
||||
{
|
||||
"offset": 14,
|
||||
"gen": 2
|
||||
},
|
||||
{
|
||||
"offset": 14,
|
||||
"gen": 3
|
||||
},
|
||||
{
|
||||
"offset": 14,
|
||||
"gen": 4
|
||||
},
|
||||
{
|
||||
"offset": 14,
|
||||
"gen": 5
|
||||
},
|
||||
{
|
||||
"offset": 14,
|
||||
"gen": 6
|
||||
},
|
||||
{
|
||||
"offset": 14,
|
||||
"gen": 7
|
||||
},
|
||||
{
|
||||
"offset": 14,
|
||||
"gen": 8
|
||||
},
|
||||
{
|
||||
"offset": 14,
|
||||
"gen": 9
|
||||
},
|
||||
{
|
||||
"offset": 14,
|
||||
"gen": 10
|
||||
},
|
||||
{
|
||||
"offset": 14,
|
||||
"gen": 11
|
||||
},
|
||||
{
|
||||
"offset": 14,
|
||||
"gen": 12
|
||||
},
|
||||
{
|
||||
"offset": 14,
|
||||
"gen": 13
|
||||
},
|
||||
{
|
||||
"offset": 14,
|
||||
"gen": 14
|
||||
},
|
||||
{
|
||||
"offset": 14,
|
||||
"gen": 15
|
||||
},
|
||||
{
|
||||
"offset": 14,
|
||||
"gen": 16
|
||||
}
|
||||
],
|
||||
"startXRefTable": 43799
|
||||
}
|
@@ -0,0 +1,182 @@
|
||||
{
|
||||
"xRefEntries": [
|
||||
{
|
||||
"offset": 0,
|
||||
"gen": 0,
|
||||
"free": true
|
||||
},
|
||||
{
|
||||
"offset": 75299,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 75548,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 15,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 216,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 790,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 840,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 975,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 1083,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 2128,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 13799,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 23682,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 31867,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 36116,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 50352,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 61569,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 73516,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 0
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 1
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 2
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 3
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 4
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 5
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 6
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 7
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 8
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 9
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 10
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 11
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 12
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 13
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 14
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 15
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 16
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 17
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 18
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 19
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 20
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 21
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 22
|
||||
}
|
||||
],
|
||||
"startXRefTable": 75548
|
||||
}
|
@@ -0,0 +1,140 @@
|
||||
{
|
||||
"xRefEntries": [
|
||||
{
|
||||
"offset": 0,
|
||||
"gen": 65535,
|
||||
"free": true
|
||||
},
|
||||
{
|
||||
"offset": 25097,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 25156,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 25367,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 15,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 854,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 919,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 1074,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 1245,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 18343,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 18388,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 18752,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 19071,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 19360,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 19604,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 19770,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 20007,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 20174,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 20424,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 20456,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 20525,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 23109,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 23500,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 24229,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 24641,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 24741,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 24985,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
}
|
||||
],
|
||||
"startXRefTable": 26815
|
||||
}
|
@@ -0,0 +1,94 @@
|
||||
{
|
||||
"xRefEntries": [
|
||||
{
|
||||
"offset": 0,
|
||||
"gen": 0,
|
||||
"free": true
|
||||
},
|
||||
{
|
||||
"offset": 3568,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 3777,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 15,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 216,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 643,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 693,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 819,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 934,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 1118,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 1210,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 2555,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 3030,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 12,
|
||||
"gen": 0
|
||||
},
|
||||
{
|
||||
"offset": 12,
|
||||
"gen": 1
|
||||
},
|
||||
{
|
||||
"offset": 12,
|
||||
"gen": 2
|
||||
},
|
||||
{
|
||||
"offset": 12,
|
||||
"gen": 3
|
||||
},
|
||||
{
|
||||
"offset": 12,
|
||||
"gen": 4
|
||||
},
|
||||
{
|
||||
"offset": 12,
|
||||
"gen": 5
|
||||
}
|
||||
],
|
||||
"startXRefTable": 3777
|
||||
}
|
@@ -0,0 +1,128 @@
|
||||
{
|
||||
"xRefEntries": [
|
||||
{
|
||||
"offset": 0,
|
||||
"gen": 0,
|
||||
"free": true
|
||||
},
|
||||
{
|
||||
"offset": 16762,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 16877,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 17142,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 24335,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 32164,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 32412,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 15,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 216,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 671,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 721,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 856,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 973,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 1318,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 2218,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 13,
|
||||
"gen": 0
|
||||
},
|
||||
{
|
||||
"offset": 13,
|
||||
"gen": 1
|
||||
},
|
||||
{
|
||||
"offset": 13,
|
||||
"gen": 2
|
||||
},
|
||||
{
|
||||
"offset": 13,
|
||||
"gen": 3
|
||||
},
|
||||
{
|
||||
"offset": 13,
|
||||
"gen": 4
|
||||
},
|
||||
{
|
||||
"offset": 13,
|
||||
"gen": 5
|
||||
},
|
||||
{
|
||||
"offset": 13,
|
||||
"gen": 6
|
||||
},
|
||||
{
|
||||
"offset": 13,
|
||||
"gen": 7
|
||||
},
|
||||
{
|
||||
"offset": 13,
|
||||
"gen": 8
|
||||
},
|
||||
{
|
||||
"offset": 13,
|
||||
"gen": 9
|
||||
},
|
||||
{
|
||||
"offset": 13,
|
||||
"gen": 10
|
||||
},
|
||||
{
|
||||
"offset": 13,
|
||||
"gen": 11
|
||||
}
|
||||
],
|
||||
"startXRefTable": 32412
|
||||
}
|
@@ -0,0 +1,111 @@
|
||||
{
|
||||
"xRefEntries": [
|
||||
{
|
||||
"offset": 0,
|
||||
"gen": 0,
|
||||
"free": true
|
||||
},
|
||||
{
|
||||
"offset": 16778,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 16893,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 17109,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 24938,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 25186,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 15,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 216,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 650,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 700,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 836,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 953,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 1298,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 2103,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 12,
|
||||
"gen": 0
|
||||
},
|
||||
{
|
||||
"offset": 12,
|
||||
"gen": 1
|
||||
},
|
||||
{
|
||||
"offset": 12,
|
||||
"gen": 2
|
||||
},
|
||||
{
|
||||
"offset": 12,
|
||||
"gen": 3
|
||||
},
|
||||
{
|
||||
"offset": 12,
|
||||
"gen": 4
|
||||
},
|
||||
{
|
||||
"offset": 12,
|
||||
"gen": 5
|
||||
},
|
||||
{
|
||||
"offset": 12,
|
||||
"gen": 6
|
||||
},
|
||||
{
|
||||
"offset": 12,
|
||||
"gen": 7
|
||||
},
|
||||
{
|
||||
"offset": 12,
|
||||
"gen": 8
|
||||
}
|
||||
],
|
||||
"startXRefTable": 25186
|
||||
}
|
@@ -0,0 +1,97 @@
|
||||
{
|
||||
"xRefEntries": [
|
||||
{
|
||||
"offset": 0,
|
||||
"gen": 0,
|
||||
"free": true
|
||||
},
|
||||
{
|
||||
"offset": 20679,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 20927,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 15,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 216,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 650,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 700,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 826,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 934,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 1252,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 8248,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 20115,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 11,
|
||||
"gen": 0
|
||||
},
|
||||
{
|
||||
"offset": 11,
|
||||
"gen": 1
|
||||
},
|
||||
{
|
||||
"offset": 11,
|
||||
"gen": 2
|
||||
},
|
||||
{
|
||||
"offset": 11,
|
||||
"gen": 3
|
||||
},
|
||||
{
|
||||
"offset": 11,
|
||||
"gen": 4
|
||||
},
|
||||
{
|
||||
"offset": 11,
|
||||
"gen": 5
|
||||
},
|
||||
{
|
||||
"offset": 11,
|
||||
"gen": 6
|
||||
},
|
||||
{
|
||||
"offset": 11,
|
||||
"gen": 7
|
||||
}
|
||||
],
|
||||
"startXRefTable": 20927
|
||||
}
|
@@ -0,0 +1,156 @@
|
||||
{
|
||||
"xRefEntries": [
|
||||
{
|
||||
"offset": 0,
|
||||
"gen": 0,
|
||||
"free": true
|
||||
},
|
||||
{
|
||||
"offset": 29506,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 29621,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 29918,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 30033,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 30274,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 30389,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 30644,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 42802,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 43050,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 15,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 216,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 695,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 746,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 900,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 1017,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 1286,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 2443,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 13147,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 0
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 1
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 2
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 3
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 4
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 5
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 6
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 7
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 8
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 9
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 10
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 11
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 12
|
||||
},
|
||||
{
|
||||
"offset": 16,
|
||||
"gen": 13
|
||||
}
|
||||
],
|
||||
"startXRefTable": 43050
|
||||
}
|
@@ -0,0 +1,114 @@
|
||||
{
|
||||
"xRefEntries": [
|
||||
{
|
||||
"offset": 0,
|
||||
"gen": 0,
|
||||
"free": true
|
||||
},
|
||||
{
|
||||
"offset": 34102,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 34350,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 15,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 216,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 678,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 728,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 856,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 971,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 1514,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 10973,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 19139,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 33047,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 12,
|
||||
"gen": 0
|
||||
},
|
||||
{
|
||||
"offset": 12,
|
||||
"gen": 1
|
||||
},
|
||||
{
|
||||
"offset": 12,
|
||||
"gen": 2
|
||||
},
|
||||
{
|
||||
"offset": 12,
|
||||
"gen": 3
|
||||
},
|
||||
{
|
||||
"offset": 12,
|
||||
"gen": 4
|
||||
},
|
||||
{
|
||||
"offset": 12,
|
||||
"gen": 5
|
||||
},
|
||||
{
|
||||
"offset": 12,
|
||||
"gen": 6
|
||||
},
|
||||
{
|
||||
"offset": 12,
|
||||
"gen": 7
|
||||
},
|
||||
{
|
||||
"offset": 12,
|
||||
"gen": 8
|
||||
},
|
||||
{
|
||||
"offset": 12,
|
||||
"gen": 9
|
||||
},
|
||||
{
|
||||
"offset": 12,
|
||||
"gen": 10
|
||||
}
|
||||
],
|
||||
"startXRefTable": 34350
|
||||
}
|
@@ -0,0 +1,111 @@
|
||||
{
|
||||
"xRefEntries": [
|
||||
{
|
||||
"offset": 0,
|
||||
"gen": 0,
|
||||
"free": true
|
||||
},
|
||||
{
|
||||
"offset": 9449,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 9564,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 9730,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 17293,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 17541,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 15,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 216,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 650,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 700,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 835,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 952,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 1097,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 1758,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 12,
|
||||
"gen": 0
|
||||
},
|
||||
{
|
||||
"offset": 12,
|
||||
"gen": 1
|
||||
},
|
||||
{
|
||||
"offset": 12,
|
||||
"gen": 2
|
||||
},
|
||||
{
|
||||
"offset": 12,
|
||||
"gen": 3
|
||||
},
|
||||
{
|
||||
"offset": 12,
|
||||
"gen": 4
|
||||
},
|
||||
{
|
||||
"offset": 12,
|
||||
"gen": 5
|
||||
},
|
||||
{
|
||||
"offset": 12,
|
||||
"gen": 6
|
||||
},
|
||||
{
|
||||
"offset": 12,
|
||||
"gen": 7
|
||||
},
|
||||
{
|
||||
"offset": 12,
|
||||
"gen": 8
|
||||
}
|
||||
],
|
||||
"startXRefTable": 17541
|
||||
}
|
@@ -0,0 +1,114 @@
|
||||
{
|
||||
"xRefEntries": [
|
||||
{
|
||||
"offset": 0,
|
||||
"gen": 0,
|
||||
"free": true
|
||||
},
|
||||
{
|
||||
"offset": 37282,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 37530,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 15,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 216,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 678,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 728,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 856,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 971,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 1322,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 9581,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 24286,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 36258,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 12,
|
||||
"gen": 0
|
||||
},
|
||||
{
|
||||
"offset": 12,
|
||||
"gen": 1
|
||||
},
|
||||
{
|
||||
"offset": 12,
|
||||
"gen": 2
|
||||
},
|
||||
{
|
||||
"offset": 12,
|
||||
"gen": 3
|
||||
},
|
||||
{
|
||||
"offset": 12,
|
||||
"gen": 4
|
||||
},
|
||||
{
|
||||
"offset": 12,
|
||||
"gen": 5
|
||||
},
|
||||
{
|
||||
"offset": 12,
|
||||
"gen": 6
|
||||
},
|
||||
{
|
||||
"offset": 12,
|
||||
"gen": 7
|
||||
},
|
||||
{
|
||||
"offset": 12,
|
||||
"gen": 8
|
||||
},
|
||||
{
|
||||
"offset": 12,
|
||||
"gen": 9
|
||||
},
|
||||
{
|
||||
"offset": 12,
|
||||
"gen": 10
|
||||
}
|
||||
],
|
||||
"startXRefTable": 37530
|
||||
}
|
@@ -0,0 +1,128 @@
|
||||
{
|
||||
"xRefEntries": [
|
||||
{
|
||||
"offset": 0,
|
||||
"gen": 0,
|
||||
"free": true
|
||||
},
|
||||
{
|
||||
"offset": 48194,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 48442,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 15,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 216,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 699,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 749,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 878,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 1000,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 8546,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 9072,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 10659,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 18919,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 35129,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 47101,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 14,
|
||||
"gen": 0
|
||||
},
|
||||
{
|
||||
"offset": 14,
|
||||
"gen": 1
|
||||
},
|
||||
{
|
||||
"offset": 14,
|
||||
"gen": 2
|
||||
},
|
||||
{
|
||||
"offset": 14,
|
||||
"gen": 3
|
||||
},
|
||||
{
|
||||
"offset": 14,
|
||||
"gen": 4
|
||||
},
|
||||
{
|
||||
"offset": 14,
|
||||
"gen": 5
|
||||
},
|
||||
{
|
||||
"offset": 14,
|
||||
"gen": 6
|
||||
},
|
||||
{
|
||||
"offset": 14,
|
||||
"gen": 7
|
||||
},
|
||||
{
|
||||
"offset": 14,
|
||||
"gen": 8
|
||||
},
|
||||
{
|
||||
"offset": 14,
|
||||
"gen": 9
|
||||
},
|
||||
{
|
||||
"offset": 14,
|
||||
"gen": 10
|
||||
},
|
||||
{
|
||||
"offset": 14,
|
||||
"gen": 11
|
||||
}
|
||||
],
|
||||
"startXRefTable": 48442
|
||||
}
|
@@ -0,0 +1,168 @@
|
||||
{
|
||||
"xRefEntries": [
|
||||
{
|
||||
"offset": 0,
|
||||
"gen": 0,
|
||||
"free": true
|
||||
},
|
||||
{
|
||||
"offset": 2924,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 3039,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 4606,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 4721,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 7754,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 7870,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 11668,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 21077,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 28498,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 35464,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 35699,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 15,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 216,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 703,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 754,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 909,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 1026,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 2161,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 18,
|
||||
"gen": 0
|
||||
},
|
||||
{
|
||||
"offset": 18,
|
||||
"gen": 1
|
||||
},
|
||||
{
|
||||
"offset": 18,
|
||||
"gen": 2
|
||||
},
|
||||
{
|
||||
"offset": 18,
|
||||
"gen": 3
|
||||
},
|
||||
{
|
||||
"offset": 18,
|
||||
"gen": 4
|
||||
},
|
||||
{
|
||||
"offset": 18,
|
||||
"gen": 5
|
||||
},
|
||||
{
|
||||
"offset": 18,
|
||||
"gen": 6
|
||||
},
|
||||
{
|
||||
"offset": 18,
|
||||
"gen": 7
|
||||
},
|
||||
{
|
||||
"offset": 18,
|
||||
"gen": 8
|
||||
},
|
||||
{
|
||||
"offset": 18,
|
||||
"gen": 9
|
||||
},
|
||||
{
|
||||
"offset": 18,
|
||||
"gen": 10
|
||||
},
|
||||
{
|
||||
"offset": 18,
|
||||
"gen": 11
|
||||
},
|
||||
{
|
||||
"offset": 18,
|
||||
"gen": 12
|
||||
},
|
||||
{
|
||||
"offset": 18,
|
||||
"gen": 13
|
||||
},
|
||||
{
|
||||
"offset": 18,
|
||||
"gen": 14
|
||||
},
|
||||
{
|
||||
"offset": 18,
|
||||
"gen": 15
|
||||
},
|
||||
{
|
||||
"offset": 18,
|
||||
"gen": 16
|
||||
}
|
||||
],
|
||||
"startXRefTable": 35699
|
||||
}
|
@@ -0,0 +1,98 @@
|
||||
{
|
||||
"xRefEntries": [
|
||||
{
|
||||
"offset": 0,
|
||||
"gen": 0,
|
||||
"free": true
|
||||
},
|
||||
{
|
||||
"offset": 8578,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 15,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 216,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 658,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 708,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 837,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 940,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 1191,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 1627,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 7676,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 7784,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 7822,
|
||||
"gen": 0,
|
||||
"uncompressed": true
|
||||
},
|
||||
{
|
||||
"offset": 12,
|
||||
"gen": 0
|
||||
},
|
||||
{
|
||||
"offset": 12,
|
||||
"gen": 1
|
||||
},
|
||||
{
|
||||
"offset": 12,
|
||||
"gen": 2
|
||||
},
|
||||
{
|
||||
"offset": 12,
|
||||
"gen": 3
|
||||
},
|
||||
{
|
||||
"offset": 12,
|
||||
"gen": 4
|
||||
},
|
||||
{
|
||||
"offset": 12,
|
||||
"gen": 5
|
||||
},
|
||||
{
|
||||
"offset": 12,
|
||||
"gen": 6
|
||||
}
|
||||
],
|
||||
"startXRefTable": 8578
|
||||
}
|
Reference in New Issue
Block a user