CompileManagerTests.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521
  1. const { expect } = require('chai')
  2. const sinon = require('sinon')
  3. const modulePath = '../../../../app/src/Features/Compile/CompileManager.js'
  4. const SandboxedModule = require('sandboxed-module')
  5. describe('CompileManager', function () {
  6. beforeEach(function () {
  7. this.rateLimitGetStub = sinon.stub()
  8. this.ratelimiter = { addCount: sinon.stub() }
  9. this.timer = {
  10. done: sinon.stub(),
  11. }
  12. this.Metrics = {
  13. Timer: sinon.stub().returns(this.timer),
  14. inc: sinon.stub(),
  15. }
  16. this.CompileManager = SandboxedModule.require(modulePath, {
  17. requires: {
  18. '@overleaf/settings': (this.settings = {
  19. apis: { clsi: { defaultBackendClass: 'e2' } },
  20. redis: { web: { host: 'localhost', port: 42 } },
  21. rateLimit: { autoCompile: {} },
  22. }),
  23. '../../infrastructure/RedisWrapper': {
  24. client: () => (this.rclient = { auth() {} }),
  25. },
  26. '../Project/ProjectRootDocManager': (this.ProjectRootDocManager = {}),
  27. '../Project/ProjectGetter': (this.ProjectGetter = {}),
  28. '../User/UserGetter': (this.UserGetter = {}),
  29. './ClsiManager': (this.ClsiManager = {}),
  30. '../../infrastructure/RateLimiter': this.ratelimiter,
  31. '@overleaf/metrics': this.Metrics,
  32. '../SplitTests/SplitTestHandler': {
  33. getAssignmentForMongoUser: (this.getAssignmentForMongoUser = sinon
  34. .stub()
  35. .yields(null, {
  36. variant: 'default',
  37. })),
  38. },
  39. },
  40. })
  41. this.project_id = 'mock-project-id-123'
  42. this.user_id = 'mock-user-id-123'
  43. this.callback = sinon.stub()
  44. this.limits = {
  45. timeout: 42,
  46. compileGroup: 'standard',
  47. }
  48. })
  49. describe('compile', function () {
  50. beforeEach(function () {
  51. this.CompileManager._checkIfRecentlyCompiled = sinon
  52. .stub()
  53. .callsArgWith(2, null, false)
  54. this.ProjectRootDocManager.ensureRootDocumentIsSet = sinon
  55. .stub()
  56. .callsArgWith(1, null)
  57. this.CompileManager.getProjectCompileLimits = sinon
  58. .stub()
  59. .callsArgWith(1, null, this.limits)
  60. this.ClsiManager.sendRequest = sinon
  61. .stub()
  62. .callsArgWith(
  63. 3,
  64. null,
  65. (this.status = 'mock-status'),
  66. (this.outputFiles = 'mock output files'),
  67. (this.output = 'mock output')
  68. )
  69. })
  70. describe('succesfully', function () {
  71. beforeEach(function () {
  72. this.CompileManager._checkIfAutoCompileLimitHasBeenHit = (
  73. isAutoCompile,
  74. compileGroup,
  75. cb
  76. ) => cb(null, true)
  77. this.CompileManager.compile(
  78. this.project_id,
  79. this.user_id,
  80. {},
  81. this.callback
  82. )
  83. })
  84. it('should check the project has not been recently compiled', function () {
  85. this.CompileManager._checkIfRecentlyCompiled
  86. .calledWith(this.project_id, this.user_id)
  87. .should.equal(true)
  88. })
  89. it('should ensure that the root document is set', function () {
  90. this.ProjectRootDocManager.ensureRootDocumentIsSet
  91. .calledWith(this.project_id)
  92. .should.equal(true)
  93. })
  94. it('should get the project compile limits', function () {
  95. this.CompileManager.getProjectCompileLimits
  96. .calledWith(this.project_id)
  97. .should.equal(true)
  98. })
  99. it('should run the compile with the compile limits', function () {
  100. this.ClsiManager.sendRequest
  101. .calledWith(this.project_id, this.user_id, {
  102. timeout: this.limits.timeout,
  103. compileGroup: 'standard',
  104. })
  105. .should.equal(true)
  106. })
  107. it('should call the callback with the output', function () {
  108. this.callback
  109. .calledWith(null, this.status, this.outputFiles, this.output)
  110. .should.equal(true)
  111. })
  112. it('should time the compile', function () {
  113. this.timer.done.called.should.equal(true)
  114. })
  115. })
  116. describe('when the project has been recently compiled', function () {
  117. it('should return', function (done) {
  118. this.CompileManager._checkIfAutoCompileLimitHasBeenHit = (
  119. isAutoCompile,
  120. compileGroup,
  121. cb
  122. ) => cb(null, true)
  123. this.CompileManager._checkIfRecentlyCompiled = sinon
  124. .stub()
  125. .callsArgWith(2, null, true)
  126. this.CompileManager.compile(
  127. this.project_id,
  128. this.user_id,
  129. {},
  130. (err, status) => {
  131. if (err) {
  132. return done(err)
  133. }
  134. status.should.equal('too-recently-compiled')
  135. done()
  136. }
  137. )
  138. })
  139. })
  140. describe('should check the rate limit', function () {
  141. it('should return', function (done) {
  142. this.CompileManager._checkIfAutoCompileLimitHasBeenHit = sinon
  143. .stub()
  144. .callsArgWith(2, null, false)
  145. this.CompileManager.compile(
  146. this.project_id,
  147. this.user_id,
  148. {},
  149. (err, status) => {
  150. if (err) {
  151. return done(err)
  152. }
  153. status.should.equal('autocompile-backoff')
  154. done()
  155. }
  156. )
  157. })
  158. })
  159. })
  160. describe('getProjectCompileLimits', function () {
  161. beforeEach(function () {
  162. this.features = {
  163. compileTimeout: (this.timeout = 42),
  164. compileGroup: (this.group = 'priority'),
  165. }
  166. this.ProjectGetter.getProject = sinon
  167. .stub()
  168. .callsArgWith(
  169. 2,
  170. null,
  171. (this.project = { owner_ref: (this.owner_id = 'owner-id-123') })
  172. )
  173. this.UserGetter.getUser = sinon
  174. .stub()
  175. .callsArgWith(
  176. 2,
  177. null,
  178. (this.user = { features: this.features, analyticsId: 'abc' })
  179. )
  180. this.CompileManager.getProjectCompileLimits(
  181. this.project_id,
  182. this.callback
  183. )
  184. })
  185. it('should look up the owner of the project', function () {
  186. this.ProjectGetter.getProject
  187. .calledWith(this.project_id, { owner_ref: 1 })
  188. .should.equal(true)
  189. })
  190. it("should look up the owner's features", function () {
  191. this.UserGetter.getUser
  192. .calledWith(this.project.owner_ref, {
  193. _id: 1,
  194. alphaProgram: 1,
  195. analyticsId: 1,
  196. betaProgram: 1,
  197. features: 1,
  198. splitTests: 1,
  199. })
  200. .should.equal(true)
  201. })
  202. it('should return the limits', function () {
  203. this.callback
  204. .calledWith(null, {
  205. timeout: this.timeout,
  206. compileGroup: this.group,
  207. compileBackendClass: 'e2',
  208. ownerAnalyticsId: 'abc',
  209. showFasterCompilesFeedbackUI: false,
  210. })
  211. .should.equal(true)
  212. })
  213. })
  214. describe('compileBackendClass', function () {
  215. beforeEach(function () {
  216. this.features = {
  217. compileTimeout: 42,
  218. compileGroup: 'standard',
  219. }
  220. this.ProjectGetter.getProject = sinon
  221. .stub()
  222. .yields(null, { owner_ref: 'owner-id-123' })
  223. this.UserGetter.getUser = sinon
  224. .stub()
  225. .yields(null, { features: this.features, analyticsId: 'abc' })
  226. })
  227. describe('with standard compile', function () {
  228. beforeEach(function () {
  229. this.features.compileGroup = 'standard'
  230. })
  231. it('should return the default class and disable ui', function (done) {
  232. this.CompileManager.getProjectCompileLimits(
  233. this.project_id,
  234. (err, { compileBackendClass, showFasterCompilesFeedbackUI }) => {
  235. if (err) return done(err)
  236. expect(compileBackendClass).to.equal('e2')
  237. expect(showFasterCompilesFeedbackUI).to.equal(false)
  238. done()
  239. }
  240. )
  241. })
  242. })
  243. describe('with priority compile', function () {
  244. beforeEach(function () {
  245. this.features.compileGroup = 'priority'
  246. })
  247. describe('split test not active', function () {
  248. beforeEach(function () {
  249. this.getAssignmentForMongoUser.yields(null, {
  250. analytics: { segmentation: {} },
  251. variant: 'default',
  252. })
  253. })
  254. it('should return the default class and disable ui', function (done) {
  255. this.CompileManager.getProjectCompileLimits(
  256. this.project_id,
  257. (err, { compileBackendClass, showFasterCompilesFeedbackUI }) => {
  258. if (err) return done(err)
  259. expect(compileBackendClass).to.equal('e2')
  260. expect(showFasterCompilesFeedbackUI).to.equal(false)
  261. done()
  262. }
  263. )
  264. })
  265. })
  266. describe('split test active', function () {
  267. describe('default variant', function () {
  268. beforeEach(function () {
  269. this.getAssignmentForMongoUser.yields(null, {
  270. analytics: { segmentation: { splitTest: 'foo' } },
  271. variant: 'default',
  272. })
  273. })
  274. it('should return the default class and enable ui', function (done) {
  275. this.CompileManager.getProjectCompileLimits(
  276. this.project_id,
  277. (err, { compileBackendClass, showFasterCompilesFeedbackUI }) => {
  278. if (err) return done(err)
  279. expect(compileBackendClass).to.equal('e2')
  280. expect(showFasterCompilesFeedbackUI).to.equal(true)
  281. done()
  282. }
  283. )
  284. })
  285. })
  286. describe('c2d variant', function () {
  287. beforeEach(function () {
  288. this.getAssignmentForMongoUser.yields(null, {
  289. analytics: { segmentation: { splitTest: 'foo' } },
  290. variant: 'c2d',
  291. })
  292. })
  293. it('should return the c2d class and enable ui', function (done) {
  294. this.CompileManager.getProjectCompileLimits(
  295. this.project_id,
  296. (err, { compileBackendClass, showFasterCompilesFeedbackUI }) => {
  297. if (err) return done(err)
  298. expect(compileBackendClass).to.equal('c2d')
  299. expect(showFasterCompilesFeedbackUI).to.equal(true)
  300. done()
  301. }
  302. )
  303. })
  304. })
  305. })
  306. })
  307. })
  308. describe('deleteAuxFiles', function () {
  309. beforeEach(function () {
  310. this.CompileManager.getProjectCompileLimits = sinon
  311. .stub()
  312. .callsArgWith(
  313. 1,
  314. null,
  315. (this.limits = { compileGroup: 'mock-compile-group' })
  316. )
  317. this.ClsiManager.deleteAuxFiles = sinon.stub().callsArg(3)
  318. this.CompileManager.deleteAuxFiles(
  319. this.project_id,
  320. this.user_id,
  321. this.callback
  322. )
  323. })
  324. it('should look up the compile group to use', function () {
  325. this.CompileManager.getProjectCompileLimits
  326. .calledWith(this.project_id)
  327. .should.equal(true)
  328. })
  329. it('should delete the aux files', function () {
  330. this.ClsiManager.deleteAuxFiles
  331. .calledWith(this.project_id, this.user_id, this.limits)
  332. .should.equal(true)
  333. })
  334. it('should call the callback', function () {
  335. this.callback.called.should.equal(true)
  336. })
  337. })
  338. describe('_checkIfRecentlyCompiled', function () {
  339. describe('when the key exists in redis', function () {
  340. beforeEach(function () {
  341. this.rclient.set = sinon.stub().callsArgWith(5, null, null)
  342. this.CompileManager._checkIfRecentlyCompiled(
  343. this.project_id,
  344. this.user_id,
  345. this.callback
  346. )
  347. })
  348. it('should try to set the key', function () {
  349. this.rclient.set
  350. .calledWith(
  351. `compile:${this.project_id}:${this.user_id}`,
  352. true,
  353. 'EX',
  354. this.CompileManager.COMPILE_DELAY,
  355. 'NX'
  356. )
  357. .should.equal(true)
  358. })
  359. it('should call the callback with true', function () {
  360. this.callback.calledWith(null, true).should.equal(true)
  361. })
  362. })
  363. describe('when the key does not exist in redis', function () {
  364. beforeEach(function () {
  365. this.rclient.set = sinon.stub().callsArgWith(5, null, 'OK')
  366. this.CompileManager._checkIfRecentlyCompiled(
  367. this.project_id,
  368. this.user_id,
  369. this.callback
  370. )
  371. })
  372. it('should try to set the key', function () {
  373. this.rclient.set
  374. .calledWith(
  375. `compile:${this.project_id}:${this.user_id}`,
  376. true,
  377. 'EX',
  378. this.CompileManager.COMPILE_DELAY,
  379. 'NX'
  380. )
  381. .should.equal(true)
  382. })
  383. it('should call the callback with false', function () {
  384. this.callback.calledWith(null, false).should.equal(true)
  385. })
  386. })
  387. })
  388. describe('_checkIfAutoCompileLimitHasBeenHit', function () {
  389. it('should be able to compile if it is not an autocompile', function (done) {
  390. this.ratelimiter.addCount.callsArgWith(2, null, true)
  391. this.CompileManager._checkIfAutoCompileLimitHasBeenHit(
  392. false,
  393. 'everyone',
  394. (err, canCompile) => {
  395. if (err) {
  396. return done(err)
  397. }
  398. canCompile.should.equal(true)
  399. done()
  400. }
  401. )
  402. })
  403. it('should be able to compile if rate limit has remianing', function (done) {
  404. this.ratelimiter.addCount.callsArgWith(1, null, true)
  405. this.CompileManager._checkIfAutoCompileLimitHasBeenHit(
  406. true,
  407. 'everyone',
  408. (err, canCompile) => {
  409. if (err) {
  410. return done(err)
  411. }
  412. const args = this.ratelimiter.addCount.args[0][0]
  413. args.throttle.should.equal(12.5)
  414. args.subjectName.should.be.oneOf(['everyone-b-one', 'everyone-b-two'])
  415. args.timeInterval.should.equal(20)
  416. args.endpointName.should.equal('auto_compile')
  417. canCompile.should.equal(true)
  418. done()
  419. }
  420. )
  421. })
  422. it('should be not able to compile if rate limit has no remianing', function (done) {
  423. this.ratelimiter.addCount.callsArgWith(1, null, false)
  424. this.CompileManager._checkIfAutoCompileLimitHasBeenHit(
  425. true,
  426. 'everyone',
  427. (err, canCompile) => {
  428. if (err) {
  429. return done(err)
  430. }
  431. canCompile.should.equal(false)
  432. done()
  433. }
  434. )
  435. })
  436. it('should return false if there is an error in the rate limit', function (done) {
  437. this.ratelimiter.addCount.callsArgWith(1, 'error')
  438. this.CompileManager._checkIfAutoCompileLimitHasBeenHit(
  439. true,
  440. 'everyone',
  441. (err, canCompile) => {
  442. if (err) {
  443. return done(err)
  444. }
  445. canCompile.should.equal(false)
  446. done()
  447. }
  448. )
  449. })
  450. })
  451. describe('wordCount', function () {
  452. beforeEach(function () {
  453. this.CompileManager.getProjectCompileLimits = sinon
  454. .stub()
  455. .callsArgWith(
  456. 1,
  457. null,
  458. (this.limits = { compileGroup: 'mock-compile-group' })
  459. )
  460. this.ClsiManager.wordCount = sinon.stub().callsArg(4)
  461. this.CompileManager.wordCount(
  462. this.project_id,
  463. this.user_id,
  464. false,
  465. this.callback
  466. )
  467. })
  468. it('should look up the compile group to use', function () {
  469. this.CompileManager.getProjectCompileLimits
  470. .calledWith(this.project_id)
  471. .should.equal(true)
  472. })
  473. it('should call wordCount for project', function () {
  474. this.ClsiManager.wordCount
  475. .calledWith(this.project_id, this.user_id, false, this.limits)
  476. .should.equal(true)
  477. })
  478. it('should call the callback', function () {
  479. this.callback.called.should.equal(true)
  480. })
  481. })
  482. })