Merge pull request #569 from sharelatex/bg-compile-from-redis

compile from redis
This commit is contained in:
Brian Gough 2017-08-25 09:09:52 +01:00 committed by GitHub
commit f9d1650c6a
13 changed files with 535 additions and 69 deletions

View file

@ -3,17 +3,31 @@ async = require "async"
Settings = require "settings-sharelatex" Settings = require "settings-sharelatex"
request = require('request') request = require('request')
Project = require("../../models/Project").Project Project = require("../../models/Project").Project
ProjectGetter = require("../Project/ProjectGetter")
ProjectEntityHandler = require("../Project/ProjectEntityHandler") ProjectEntityHandler = require("../Project/ProjectEntityHandler")
logger = require "logger-sharelatex" logger = require "logger-sharelatex"
Url = require("url") Url = require("url")
ClsiCookieManager = require("./ClsiCookieManager") ClsiCookieManager = require("./ClsiCookieManager")
ClsiStateManager = require("./ClsiStateManager")
_ = require("underscore") _ = require("underscore")
async = require("async") async = require("async")
ClsiFormatChecker = require("./ClsiFormatChecker") ClsiFormatChecker = require("./ClsiFormatChecker")
DocumentUpdaterHandler = require "../DocumentUpdater/DocumentUpdaterHandler"
Metrics = require('metrics-sharelatex')
module.exports = ClsiManager = module.exports = ClsiManager =
sendRequest: (project_id, user_id, options = {}, callback = (error, status, outputFiles, clsiServerId, validationProblems) ->) -> sendRequest: (project_id, user_id, options = {}, callback) ->
ClsiManager.sendRequestOnce project_id, user_id, options, (error, status, result...) ->
return callback(error) if error?
if status is 'conflict'
options = _.clone(options)
options.syncType = "full" # force full compile
ClsiManager.sendRequestOnce project_id, user_id, options, callback # try again
else
callback(error, status, result...)
sendRequestOnce: (project_id, user_id, options = {}, callback = (error, status, outputFiles, clsiServerId, validationProblems) ->) ->
ClsiManager._buildRequest project_id, options, (error, req) -> ClsiManager._buildRequest project_id, options, (error, req) ->
return callback(error) if error? return callback(error) if error?
logger.log project_id: project_id, "sending compile to CLSI" logger.log project_id: project_id, "sending compile to CLSI"
@ -28,7 +42,7 @@ module.exports = ClsiManager =
if error? if error?
logger.err err:error, project_id:project_id, "error sending request to clsi" logger.err err:error, project_id:project_id, "error sending request to clsi"
return callback(error) return callback(error)
logger.log project_id: project_id, outputFilesLength: response?.outputFiles?.length, status: response?.status, "received compile response from CLSI" logger.log project_id: project_id, outputFilesLength: response?.outputFiles?.length, status: response?.status, compile_status: response?.compile?.status, "received compile response from CLSI"
ClsiCookieManager._getServerId project_id, (err, clsiServerId)-> ClsiCookieManager._getServerId project_id, (err, clsiServerId)->
if err? if err?
logger.err err:err, project_id:project_id, "error getting server id" logger.err err:err, project_id:project_id, "error getting server id"
@ -85,6 +99,8 @@ module.exports = ClsiManager =
callback null, body callback null, body
else if response.statusCode == 413 else if response.statusCode == 413
callback null, compile:status:"project-too-large" callback null, compile:status:"project-too-large"
else if response.statusCode == 409
callback null, compile:status:"conflict"
else else
error = new Error("CLSI returned non-success code: #{response.statusCode}") error = new Error("CLSI returned non-success code: #{response.statusCode}")
logger.error err: error, project_id: project_id, "CLSI returned failure code" logger.error err: error, project_id: project_id, "CLSI returned failure code"
@ -101,19 +117,77 @@ module.exports = ClsiManager =
return outputFiles return outputFiles
VALID_COMPILERS: ["pdflatex", "latex", "xelatex", "lualatex"] VALID_COMPILERS: ["pdflatex", "latex", "xelatex", "lualatex"]
_buildRequest: (project_id, options={}, callback = (error, request) ->) -> _buildRequest: (project_id, options={}, callback = (error, request) ->) ->
Project.findById project_id, {compiler: 1, rootDoc_id: 1, imageName: 1}, (error, project) -> ProjectGetter.getProject project_id, {compiler: 1, rootDoc_id: 1, imageName: 1, rootFolder:1}, (error, project) ->
return callback(error) if error? return callback(error) if error?
return callback(new Errors.NotFoundError("project does not exist: #{project_id}")) if !project? return callback(new Errors.NotFoundError("project does not exist: #{project_id}")) if !project?
if project.compiler not in ClsiManager.VALID_COMPILERS if project.compiler not in ClsiManager.VALID_COMPILERS
project.compiler = "pdflatex" project.compiler = "pdflatex"
if options.incrementalCompilesEnabled or options.syncType? # new way, either incremental or full
timer = new Metrics.Timer("editor.compile-getdocs-redis")
ClsiManager.getContentFromDocUpdaterIfMatch project_id, project, (error, projectStateHash, docUpdaterDocs) ->
timer.done()
return callback(error) if error?
logger.log project_id: project_id, projectStateHash: projectStateHash, docs: docUpdaterDocs?, "checked project state"
# see if we can send an incremental update to the CLSI
if docUpdaterDocs? and options.syncType isnt "full"
# Workaround: for now, always flush project to mongo on compile
# until we have automatic periodic flushing on the docupdater
# side, to prevent documents staying in redis too long.
DocumentUpdaterHandler.flushProjectToMongo project_id, (error) ->
return callback(error) if error?
Metrics.inc "compile-from-redis"
ClsiManager._buildRequestFromDocupdater project_id, options, project, projectStateHash, docUpdaterDocs, callback
else
Metrics.inc "compile-from-mongo"
ClsiManager._buildRequestFromMongo project_id, options, project, projectStateHash, callback
else # old way, always from mongo
timer = new Metrics.Timer("editor.compile-getdocs-mongo")
ClsiManager._getContentFromMongo project_id, (error, docs, files) ->
timer.done()
return callback(error) if error?
ClsiManager._finaliseRequest project_id, options, project, docs, files, callback
getContentFromDocUpdaterIfMatch: (project_id, project, callback = (error, projectStateHash, docs) ->) ->
ClsiStateManager.computeHash project, (error, projectStateHash) ->
return callback(error) if error?
DocumentUpdaterHandler.getProjectDocsIfMatch project_id, projectStateHash, (error, docs) ->
return callback(error) if error?
callback(null, projectStateHash, docs)
_buildRequestFromDocupdater: (project_id, options, project, projectStateHash, docUpdaterDocs, callback = (error, request) ->) ->
ProjectEntityHandler.getAllDocPathsFromProject project, (error, docPath) ->
return callback(error) if error?
docs = {}
for doc in docUpdaterDocs or []
path = docPath[doc._id]
docs[path] = doc
# send new docs but not files as those are already on the clsi
options = _.clone(options)
options.syncType = "incremental"
options.syncState = projectStateHash
ClsiManager._finaliseRequest project_id, options, project, docs, [], callback
_buildRequestFromMongo: (project_id, options, project, projectStateHash, callback = (error, request) ->) ->
ClsiManager._getContentFromMongo project_id, (error, docs, files) ->
return callback(error) if error?
options = _.clone(options)
options.syncType = "full"
options.syncState = projectStateHash
ClsiManager._finaliseRequest project_id, options, project, docs, files, callback
_getContentFromMongo: (project_id, callback = (error, docs, files) ->) ->
DocumentUpdaterHandler.flushProjectToMongo project_id, (error) ->
return callback(error) if error?
ProjectEntityHandler.getAllDocs project_id, (error, docs = {}) -> ProjectEntityHandler.getAllDocs project_id, (error, docs = {}) ->
return callback(error) if error? return callback(error) if error?
ProjectEntityHandler.getAllFiles project_id, (error, files = {}) -> ProjectEntityHandler.getAllFiles project_id, (error, files = {}) ->
return callback(error) if error? return callback(error) if error?
callback(null, docs, files)
_finaliseRequest: (project_id, options, project, docs, files, callback = (error, params) -> ) ->
resources = [] resources = []
rootResourcePath = null rootResourcePath = null
rootResourcePathOverride = null rootResourcePathOverride = null
@ -148,6 +222,8 @@ module.exports = ClsiManager =
imageName: project.imageName imageName: project.imageName
draft: !!options.draft draft: !!options.draft
check: options.check check: options.check
syncType: options.syncType
syncState: options.syncState
rootResourcePath: rootResourcePath rootResourcePath: rootResourcePath
resources: resources resources: resources
} }

View file

@ -0,0 +1,31 @@
Settings = require "settings-sharelatex"
logger = require "logger-sharelatex"
crypto = require "crypto"
ProjectEntityHandler = require "../Project/ProjectEntityHandler"
# The "state" of a project is a hash of the relevant attributes in the
# project object in this case we only need the rootFolder.
#
# The idea is that it will change if any doc or file is
# created/renamed/deleted, and also if the content of any file (not
# doc) changes.
#
# When the hash changes the full set of files on the CLSI will need to
# be updated. If it doesn't change then we can overwrite changed docs
# in place on the clsi, getting them from the docupdater.
#
# The docupdater is responsible for setting the key in redis, and
# unsetting it if it removes any documents from the doc updater.
buildState = (s) ->
return crypto.createHash('sha1').update(s, 'utf8').digest('hex')
module.exports = ClsiStateManager =
computeHash: (project, callback = (err, hash) ->) ->
ProjectEntityHandler.getAllEntitiesFromProject project, (err, docs, files) ->
fileList = ("#{f.file._id}:#{f.file.rev}:#{f.file.created}:#{f.path}" for f in files or [])
docList = ("#{d.doc._id}:#{d.path}" for d in docs or [])
sortedEntityList = [docList..., fileList...].sort()
hash = buildState(sortedEntityList.join("\n"))
callback(null, hash)

View file

@ -30,6 +30,8 @@ module.exports = CompileController =
options.draft = req.body.draft options.draft = req.body.draft
if req.body?.check in ['validate', 'error', 'silent'] if req.body?.check in ['validate', 'error', 'silent']
options.check = req.body.check options.check = req.body.check
if req.body?.incrementalCompilesEnabled
options.incrementalCompilesEnabled = true
logger.log {options:options, project_id:project_id, user_id:user_id}, "got compile request" logger.log {options:options, project_id:project_id, user_id:user_id}, "got compile request"
CompileManager.compile project_id, user_id, options, (error, status, outputFiles, clsiServerId, limits, validationProblems) -> CompileManager.compile project_id, user_id, options, (error, status, outputFiles, clsiServerId, limits, validationProblems) ->
return next(error) if error? return next(error) if error?

View file

@ -1,7 +1,6 @@
Settings = require('settings-sharelatex') Settings = require('settings-sharelatex')
RedisWrapper = require("../../infrastructure/RedisWrapper") RedisWrapper = require("../../infrastructure/RedisWrapper")
rclient = RedisWrapper.client("clsi_recently_compiled") rclient = RedisWrapper.client("clsi_recently_compiled")
DocumentUpdaterHandler = require "../DocumentUpdater/DocumentUpdaterHandler"
Project = require("../../models/Project").Project Project = require("../../models/Project").Project
ProjectRootDocManager = require "../Project/ProjectRootDocManager" ProjectRootDocManager = require "../Project/ProjectRootDocManager"
UserGetter = require "../User/UserGetter" UserGetter = require "../User/UserGetter"
@ -30,8 +29,6 @@ module.exports = CompileManager =
return callback null, "too-recently-compiled", [] return callback null, "too-recently-compiled", []
CompileManager._ensureRootDocumentIsSet project_id, (error) -> CompileManager._ensureRootDocumentIsSet project_id, (error) ->
return callback(error) if error?
DocumentUpdaterHandler.flushProjectToMongo project_id, (error) ->
return callback(error) if error? return callback(error) if error?
CompileManager.getProjectCompileLimits project_id, (error, limits) -> CompileManager.getProjectCompileLimits project_id, (error, limits) ->
return callback(error) if error? return callback(error) if error?

View file

@ -123,6 +123,36 @@ module.exports = DocumentUpdaterHandler =
logger.error project_id:project_id, doc_id:doc_id, url: url, "doc updater returned a non-success status code: #{res.statusCode}" logger.error project_id:project_id, doc_id:doc_id, url: url, "doc updater returned a non-success status code: #{res.statusCode}"
callback new Error("doc updater returned a non-success status code: #{res.statusCode}") callback new Error("doc updater returned a non-success status code: #{res.statusCode}")
getProjectDocsIfMatch: (project_id, projectStateHash, callback = (error, docs) ->) ->
# If the project state hasn't changed, we can get all the latest
# docs from redis via the docupdater. Otherwise we will need to
# fall back to getting them from mongo.
timer = new metrics.Timer("get-project-docs")
url = "#{settings.apis.documentupdater.url}/project/#{project_id}/doc?state=#{projectStateHash}"
logger.log project_id:project_id, "getting project docs from document updater"
request.get url, (error, res, body)->
timer.done()
if error?
logger.error err:error, url:url, project_id:project_id, "error getting project docs from doc updater"
return callback(error)
if res.statusCode is 409 # HTTP response code "409 Conflict"
# Docupdater has checked the projectStateHash and found that
# it has changed. This means that the docs currently in redis
# aren't the only change to the project and the full set of
# docs/files should be retreived from docstore/filestore
# instead.
return callback()
else if res.statusCode >= 200 and res.statusCode < 300
logger.log project_id:project_id, "got project docs from document document updater"
try
docs = JSON.parse(body)
catch error
return callback(error)
callback null, docs
else
logger.error project_id:project_id, url: url, "doc updater returned a non-success status code: #{res.statusCode}"
callback new Error("doc updater returned a non-success status code: #{res.statusCode}")
acceptChanges: (project_id, doc_id, change_ids = [], callback = (error) ->) -> acceptChanges: (project_id, doc_id, change_ids = [], callback = (error) ->) ->
timer = new metrics.Timer("accept-changes") timer = new metrics.Timer("accept-changes")
reqSettings = reqSettings =

View file

@ -21,18 +21,10 @@ CooldownManager = require '../Cooldown/CooldownManager'
module.exports = ProjectEntityHandler = module.exports = ProjectEntityHandler =
getAllFolders: (project_id, callback) -> getAllFolders: (project_id, callback) ->
logger.log project_id:project_id, "getting all folders for project" logger.log project_id:project_id, "getting all folders for project"
folders = {}
processFolder = (basePath, folder) ->
folders[basePath] = folder
for childFolder in (folder.folders or [])
if childFolder.name?
processFolder path.join(basePath, childFolder.name), childFolder
ProjectGetter.getProjectWithoutDocLines project_id, (err, project) -> ProjectGetter.getProjectWithoutDocLines project_id, (err, project) ->
return callback(err) if err? return callback(err) if err?
return callback("no project") if !project? return callback("no project") if !project?
processFolder "/", project.rootFolder[0] ProjectEntityHandler.getAllFoldersFromProject project, callback
callback null, folders
getAllDocs: (project_id, callback) -> getAllDocs: (project_id, callback) ->
logger.log project_id:project_id, "getting all docs for project" logger.log project_id:project_id, "getting all docs for project"
@ -74,6 +66,43 @@ module.exports = ProjectEntityHandler =
files[path.join(folderPath, file.name)] = file files[path.join(folderPath, file.name)] = file
callback null, files callback null, files
getAllFoldersFromProject: (project, callback) ->
folders = {}
processFolder = (basePath, folder) ->
folders[basePath] = folder
for childFolder in (folder.folders or [])
if childFolder.name?
processFolder path.join(basePath, childFolder.name), childFolder
processFolder "/", project.rootFolder[0]
callback null, folders
getAllEntitiesFromProject: (project, callback) ->
logger.log project:project, "getting all files for project"
@getAllFoldersFromProject project, (err, folders = {}) ->
return callback(err) if err?
docs = []
files = []
for folderPath, folder of folders
for doc in (folder.docs or [])
if doc?
docs.push({path: path.join(folderPath, doc.name), doc:doc})
for file in (folder.fileRefs or [])
if file?
files.push({path: path.join(folderPath, file.name), file:file})
callback null, docs, files
getAllDocPathsFromProject: (project, callback) ->
logger.log project:project, "getting all docs for project"
@getAllFoldersFromProject project, (err, folders = {}) ->
return callback(err) if err?
docPath = {}
for folderPath, folder of folders
for doc in (folder.docs or [])
docPath[doc._id] = path.join(folderPath, doc.name)
logger.log count:_.keys(docPath).length, project_id:project._id, "returning docPaths for project"
callback null, docPath
flushProjectToThirdPartyDataStore: (project_id, callback) -> flushProjectToThirdPartyDataStore: (project_id, callback) ->
self = @ self = @
logger.log project_id:project_id, "flushing project to tpds" logger.log project_id:project_id, "flushing project to tpds"

View file

@ -18,7 +18,7 @@ block content
| #{translate("beta_program_badge_description")} | #{translate("beta_program_badge_description")}
span.beta-feature-badge span.beta-feature-badge
p.text-centered p.text-centered
strong We're not currently testing anything in beta, but keep checking back! strong We're currently testing lower latency compilation features in beta.
.row.text-centered .row.text-centered
.col-md-12 .col-md-12
if user.betaProgram if user.betaProgram

View file

@ -105,6 +105,7 @@ define [
rootDoc_id: options.rootDocOverride_id or null rootDoc_id: options.rootDocOverride_id or null
draft: $scope.draft draft: $scope.draft
check: checkType check: checkType
incrementalCompilesEnabled: window.user?.betaProgram
_csrf: window.csrfToken _csrf: window.csrfToken
}, {params: params} }, {params: params}

View file

@ -12,6 +12,8 @@ describe "ClsiManager", ->
getCookieJar: sinon.stub().callsArgWith(1, null, @jar) getCookieJar: sinon.stub().callsArgWith(1, null, @jar)
setServerId: sinon.stub().callsArgWith(2) setServerId: sinon.stub().callsArgWith(2)
_getServerId:sinon.stub() _getServerId:sinon.stub()
@ClsiStateManager =
computeHash: sinon.stub().callsArgWith(1, null, "01234567890abcdef")
@ClsiFormatChecker = @ClsiFormatChecker =
checkRecoursesForProblems:sinon.stub().callsArgWith(1) checkRecoursesForProblems:sinon.stub().callsArgWith(1)
@ClsiManager = SandboxedModule.require modulePath, requires: @ClsiManager = SandboxedModule.require modulePath, requires:
@ -26,10 +28,18 @@ describe "ClsiManager", ->
url: "https://clsipremium.example.com" url: "https://clsipremium.example.com"
"../../models/Project": Project: @Project = {} "../../models/Project": Project: @Project = {}
"../Project/ProjectEntityHandler": @ProjectEntityHandler = {} "../Project/ProjectEntityHandler": @ProjectEntityHandler = {}
"../Project/ProjectGetter": @ProjectGetter = {}
"../DocumentUpdater/DocumentUpdaterHandler": @DocumentUpdaterHandler =
getProjectDocsIfMatch: sinon.stub().callsArgWith(2,null,null)
"./ClsiCookieManager": @ClsiCookieManager "./ClsiCookieManager": @ClsiCookieManager
"./ClsiStateManager": @ClsiStateManager
"logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub(), warn: sinon.stub() } "logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub(), warn: sinon.stub() }
"request": @request = sinon.stub() "request": @request = sinon.stub()
"./ClsiFormatChecker": @ClsiFormatChecker "./ClsiFormatChecker": @ClsiFormatChecker
"metrics-sharelatex": @Metrics =
Timer: class Timer
done: sinon.stub()
inc: sinon.stub()
@project_id = "project-id" @project_id = "project-id"
@user_id = "user-id" @user_id = "user-id"
@callback = sinon.stub() @callback = sinon.stub()
@ -93,6 +103,25 @@ describe "ClsiManager", ->
it "should call the callback with a failure statue", -> it "should call the callback with a failure statue", ->
@callback.calledWith(null, @status).should.equal true @callback.calledWith(null, @status).should.equal true
describe "with a sync conflict", ->
beforeEach ->
@ClsiManager.sendRequestOnce = sinon.stub()
@ClsiManager.sendRequestOnce.withArgs(@project_id, @user_id, {syncType:"full"}).callsArgWith(3, null, @status = "success")
@ClsiManager.sendRequestOnce.withArgs(@project_id, @user_id, {}).callsArgWith(3, null, "conflict")
@ClsiManager.sendRequest @project_id, @user_id, {}, @callback
it "should call the sendRequestOnce method twice", ->
@ClsiManager.sendRequestOnce.calledTwice.should.equal true
it "should call the sendRequestOnce method with syncType:full", ->
@ClsiManager.sendRequestOnce.calledWith(@project_id, @user_id, {syncType:"full"}).should.equal true
it "should call the sendRequestOnce method without syncType:full", ->
@ClsiManager.sendRequestOnce.calledWith(@project_id, @user_id, {}).should.equal true
it "should call the callback with a success status", ->
@callback.calledWith(null, @status, ).should.equal true
describe "deleteAuxFiles", -> describe "deleteAuxFiles", ->
beforeEach -> beforeEach ->
@ClsiManager._makeRequest = sinon.stub().callsArg(2) @ClsiManager._makeRequest = sinon.stub().callsArg(2)
@ -144,6 +173,8 @@ describe "ClsiManager", ->
@Project.findById = sinon.stub().callsArgWith(2, null, @project) @Project.findById = sinon.stub().callsArgWith(2, null, @project)
@ProjectEntityHandler.getAllDocs = sinon.stub().callsArgWith(1, null, @docs) @ProjectEntityHandler.getAllDocs = sinon.stub().callsArgWith(1, null, @docs)
@ProjectEntityHandler.getAllFiles = sinon.stub().callsArgWith(1, null, @files) @ProjectEntityHandler.getAllFiles = sinon.stub().callsArgWith(1, null, @files)
@ProjectGetter.getProject = sinon.stub().callsArgWith(2, null, @project)
@DocumentUpdaterHandler.flushProjectToMongo = sinon.stub().callsArgWith(1, null)
describe "with a valid project", -> describe "with a valid project", ->
beforeEach (done) -> beforeEach (done) ->
@ -152,8 +183,13 @@ describe "ClsiManager", ->
done() done()
it "should get the project with the required fields", -> it "should get the project with the required fields", ->
@Project.findById @ProjectGetter.getProject
.calledWith(@project_id, {compiler:1, rootDoc_id: 1, imageName: 1}) .calledWith(@project_id, {compiler:1, rootDoc_id: 1, imageName: 1, rootFolder: 1})
.should.equal true
it "should flush the project to the database", ->
@DocumentUpdaterHandler.flushProjectToMongo
.calledWith(@project_id)
.should.equal true .should.equal true
it "should get all the docs", -> it "should get all the docs", ->
@ -175,6 +211,8 @@ describe "ClsiManager", ->
imageName: @image imageName: @image
draft: false draft: false
check: undefined check: undefined
syncType: undefined # "full"
syncState: undefined # "01234567890abcdef"
rootResourcePath: "main.tex" rootResourcePath: "main.tex"
resources: [{ resources: [{
path: "main.tex" path: "main.tex"
@ -189,6 +227,51 @@ describe "ClsiManager", ->
}] }]
) )
describe "with the incremental compile option", ->
beforeEach (done) ->
@ClsiStateManager.computeHash = sinon.stub().callsArgWith(1, null, @project_state_hash = "01234567890abcdef")
@DocumentUpdaterHandler.getProjectDocsIfMatch = sinon.stub().callsArgWith(2, null, [{_id:@doc_1._id, lines: @doc_1.lines, v: 123}])
@ProjectEntityHandler.getAllDocPathsFromProject = sinon.stub().callsArgWith(1, null, {"mock-doc-id-1":"main.tex"})
@ClsiManager._buildRequest @project_id, {timeout:100, incrementalCompilesEnabled:true}, (error, request) =>
@request = request
done()
it "should get the project with the required fields", ->
@ProjectGetter.getProject
.calledWith(@project_id, {compiler:1, rootDoc_id: 1, imageName: 1, rootFolder: 1})
.should.equal true
it "should flush the project to the database", ->
@DocumentUpdaterHandler.flushProjectToMongo
.calledWith(@project_id)
.should.equal true
it "should get only the live docs from the docupdater", ->
@DocumentUpdaterHandler.getProjectDocsIfMatch
.calledWith(@project_id)
.should.equal true
it "should not get any of the files", ->
@ProjectEntityHandler.getAllFiles
.called.should.equal false
it "should build up the CLSI request", ->
expect(@request).to.deep.equal(
compile:
options:
compiler: @compiler
timeout : 100
imageName: @image
draft: false
check: undefined
syncType: "incremental"
syncState: "01234567890abcdef"
rootResourcePath: "main.tex"
resources: [{
path: "main.tex"
content: @doc_1.lines.join("\n")
}]
)
describe "when root doc override is valid", -> describe "when root doc override is valid", ->
beforeEach (done) -> beforeEach (done) ->

View file

@ -0,0 +1,148 @@
sinon = require('sinon')
chai = require('chai')
should = chai.should()
expect = chai.expect
modulePath = "../../../../app/js/Features/Compile/ClsiStateManager.js"
SandboxedModule = require('sandboxed-module')
describe "ClsiStateManager", ->
beforeEach ->
@ClsiStateManager = SandboxedModule.require modulePath, requires:
"settings-sharelatex": @settings = {}
"../Project/ProjectEntityHandler": @ProjectEntityHandler = {}
"logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub(), warn: sinon.stub() }
@project = "project"
@callback = sinon.stub()
describe "computeHash", ->
beforeEach (done) ->
@docs = [
{path: "/main.tex", doc: {_id: "doc-id-1"}}
{path: "/folder/sub.tex", doc: {_id: "doc-id-2"}}
]
@files = [
{path: "/figure.pdf", file: {_id: "file-id-1", rev: 123, created: "aaaaaa"}}
{path: "/folder/fig2.pdf", file: {_id: "file-id-2", rev: 456, created: "bbbbbb"}}
]
@ProjectEntityHandler.getAllEntitiesFromProject = sinon.stub().callsArgWith(1, null, @docs, @files)
@ClsiStateManager.computeHash @project, (err, hash) =>
@hash0 = hash
done()
describe "with a sample project", ->
beforeEach ->
@ClsiStateManager.computeHash @project, @callback
it "should call the callback with a hash value", ->
@callback
.calledWith(null, "9c2c2428e4147db63cacabf6f357af483af6551d")
.should.equal true
describe "when the files and docs are in a different order", ->
beforeEach ->
[@docs[0], @docs[1]] = [@docs[1], @docs[0]]
[@files[0], @files[1]] = [@files[1], @files[0]]
@ClsiStateManager.computeHash @project, @callback
it "should call the callback with the same hash value", ->
@callback
.calledWith(null, @hash0)
.should.equal true
describe "when a doc is renamed", ->
beforeEach (done) ->
@docs[0].path = "/new.tex"
@ClsiStateManager.computeHash @project, (err, hash) =>
@hash1 = hash
done()
it "should call the callback with a different hash value", ->
@callback
.neverCalledWith(null, @hash0)
.should.equal true
describe "when a file is renamed", ->
beforeEach (done) ->
@files[0].path = "/newfigure.pdf"
@ClsiStateManager.computeHash @project, (err, hash) =>
@hash1 = hash
done()
it "should call the callback with a different hash value", ->
@callback
.neverCalledWith(null, @hash0)
.should.equal true
describe "when a doc is added", ->
beforeEach (done) ->
@docs.push { path: "/newdoc.tex", doc: {_id: "newdoc-id"}}
@ClsiStateManager.computeHash @project, (err, hash) =>
@hash1 = hash
done()
it "should call the callback with a different hash value", ->
@callback
.neverCalledWith(null, @hash0)
.should.equal true
describe "when a file is added", ->
beforeEach (done) ->
@files.push { path: "/newfile.tex", file: {_id: "newfile-id", rev: 123}}
@ClsiStateManager.computeHash @project, (err, hash) =>
@hash1 = hash
done()
it "should call the callback with a different hash value", ->
@callback
.neverCalledWith(null, @hash0)
.should.equal true
describe "when a doc is removed", ->
beforeEach (done) ->
@docs.pop()
@ClsiStateManager.computeHash @project, (err, hash) =>
@hash1 = hash
done()
it "should call the callback with a different hash value", ->
@callback
.neverCalledWith(null, @hash0)
.should.equal true
describe "when a file is removed", ->
beforeEach (done) ->
@files.pop()
@ClsiStateManager.computeHash @project, (err, hash) =>
@hash1 = hash
done()
it "should call the callback with a different hash value", ->
@callback
.neverCalledWith(null, @hash0)
.should.equal true
describe "when a file's revision is updated", ->
beforeEach (done) ->
@files[0].file.rev++
@ClsiStateManager.computeHash @project, (err, hash) =>
@hash1 = hash
done()
it "should call the callback with a different hash value", ->
@callback
.neverCalledWith(null, @hash0)
.should.equal true
describe "when a file's date is updated", ->
beforeEach (done) ->
@files[0].file.created = "zzzzzz"
@ClsiStateManager.computeHash @project, (err, hash) =>
@hash1 = hash
done()
it "should call the callback with a different hash value", ->
@callback
.neverCalledWith(null, @hash0)
.should.equal true

View file

@ -17,7 +17,6 @@ describe "CompileManager", ->
redis: web: {host: "localhost", port: 42} redis: web: {host: "localhost", port: 42}
"../../infrastructure/RedisWrapper": "../../infrastructure/RedisWrapper":
client: () => @rclient = { auth: () -> } client: () => @rclient = { auth: () -> }
"../DocumentUpdater/DocumentUpdaterHandler": @DocumentUpdaterHandler = {}
"../Project/ProjectRootDocManager": @ProjectRootDocManager = {} "../Project/ProjectRootDocManager": @ProjectRootDocManager = {}
"../../models/Project": Project: @Project = {} "../../models/Project": Project: @Project = {}
"../User/UserGetter": @UserGetter = {} "../User/UserGetter": @UserGetter = {}
@ -40,7 +39,6 @@ describe "CompileManager", ->
beforeEach -> beforeEach ->
@CompileManager._checkIfRecentlyCompiled = sinon.stub().callsArgWith(2, null, false) @CompileManager._checkIfRecentlyCompiled = sinon.stub().callsArgWith(2, null, false)
@CompileManager._ensureRootDocumentIsSet = sinon.stub().callsArgWith(1, null) @CompileManager._ensureRootDocumentIsSet = sinon.stub().callsArgWith(1, null)
@DocumentUpdaterHandler.flushProjectToMongo = sinon.stub().callsArgWith(1, null)
@CompileManager.getProjectCompileLimits = sinon.stub().callsArgWith(1, null, @limits) @CompileManager.getProjectCompileLimits = sinon.stub().callsArgWith(1, null, @limits)
@ClsiManager.sendRequest = sinon.stub().callsArgWith(3, null, @status = "mock-status", @outputFiles = "mock output files", @output = "mock output") @ClsiManager.sendRequest = sinon.stub().callsArgWith(3, null, @status = "mock-status", @outputFiles = "mock output files", @output = "mock output")
@ -54,11 +52,6 @@ describe "CompileManager", ->
.calledWith(@project_id, @user_id) .calledWith(@project_id, @user_id)
.should.equal true .should.equal true
it "should flush the project to the database", ->
@DocumentUpdaterHandler.flushProjectToMongo
.calledWith(@project_id)
.should.equal true
it "should ensure that the root document is set", -> it "should ensure that the root document is set", ->
@CompileManager._ensureRootDocumentIsSet @CompileManager._ensureRootDocumentIsSet
.calledWith(@project_id) .calledWith(@project_id)

View file

@ -252,6 +252,47 @@ describe 'DocumentUpdaterHandler', ->
.calledWith(new Error("doc updater returned failure status code: 500")) .calledWith(new Error("doc updater returned failure status code: 500"))
.should.equal true .should.equal true
describe "getProjectDocsIfMatch", ->
beforeEach ->
@callback = sinon.stub()
@project_state_hash = "1234567890abcdef"
describe "successfully", ->
beforeEach ->
@doc0 =
_id: @doc_id
lines: @lines
v: @version
@docs = [ @doc0, @doc0, @doc0 ]
@body = JSON.stringify @docs
@request.get = sinon.stub().callsArgWith(1, null, {statusCode: 200}, @body)
@handler.getProjectDocsIfMatch @project_id, @project_state_hash, @callback
it 'should get the documenst from the document updater', ->
url = "#{@settings.apis.documentupdater.url}/project/#{@project_id}/doc?state=#{@project_state_hash}"
@request.get.calledWith(url).should.equal true
it "should call the callback with the documents", ->
@callback.calledWithExactly(null, @docs).should.equal true
describe "when the document updater API returns an error", ->
beforeEach ->
@request.get = sinon.stub().callsArgWith(1, @error = new Error("something went wrong"), null, null)
@handler.getProjectDocsIfMatch @project_id, @project_state_hash, @callback
it "should return an error to the callback", ->
@callback.calledWith(@error).should.equal true
describe "when the document updater returns a conflict error code", ->
beforeEach ->
@request.get = sinon.stub().callsArgWith(1, null, { statusCode: 409 }, "Conflict")
@handler.getProjectDocsIfMatch @project_id, @project_state_hash, @callback
it "should return the callback with no documents", ->
@callback
.alwaysCalledWithExactly()
.should.equal true
describe "acceptChanges", -> describe "acceptChanges", ->
beforeEach -> beforeEach ->
@change_id = "mock-change-id-1" @change_id = "mock-change-id-1"

View file

@ -781,6 +781,41 @@ describe 'ProjectEntityHandler', ->
}) })
.should.equal true .should.equal true
describe "getAllFoldersFromProject", ->
beforeEach ->
@callback = sinon.stub()
@ProjectEntityHandler.getAllFoldersFromProject @project, @callback
it "should call the callback with the folders", ->
@callback
.calledWith(null, {
"/": @project.rootFolder[0]
"/folder1": @folder1
})
.should.equal true
describe "getAllDocPathsFromProject", ->
beforeEach ->
@docs = [{
_id: @doc1._id
lines: @lines1 = ["one"]
rev: @rev1 = 1
}, {
_id: @doc2._id
lines: @lines2 = ["two"]
rev: @rev2 = 2
}]
@callback = sinon.stub()
@ProjectEntityHandler.getAllDocPathsFromProject @project, @callback
it "should call the callback with the path for each doc_id", ->
@expected = {}
@expected[@doc1._id] = "/#{@doc1.name}"
@expected[@doc2._id] = "/folder1/#{@doc2.name}"
@callback
.calledWith(null, @expected)
.should.equal true
describe "flushProjectToThirdPartyDataStore", -> describe "flushProjectToThirdPartyDataStore", ->
beforeEach (done) -> beforeEach (done) ->
@project = { @project = {