CompileManager.test.mjs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457
  1. import { vi, expect } from 'vitest'
  2. import sinon from 'sinon'
  3. const MODULE_PATH = '../../../../app/src/Features/Compile/CompileManager.mjs'
  4. describe('CompileManager', function () {
  5. beforeEach(async function (ctx) {
  6. ctx.rateLimiter = {
  7. consume: sinon.stub().resolves(),
  8. }
  9. ctx.timer = {
  10. done: sinon.stub(),
  11. }
  12. ctx.Metrics = {
  13. Timer: sinon.stub().returns(ctx.timer),
  14. inc: sinon.stub(),
  15. }
  16. vi.doMock('@overleaf/settings', () => ({
  17. default: (ctx.settings = {
  18. apis: {
  19. clsi: { submissionBackendClass: 'c3d' },
  20. },
  21. redis: { web: { host: '127.0.0.1', port: 42 } },
  22. rateLimit: { autoCompile: {} },
  23. }),
  24. }))
  25. vi.doMock('../../../../app/src/infrastructure/RedisWrapper', () => ({
  26. default: {
  27. client: () =>
  28. (ctx.rclient = {
  29. auth() {},
  30. }),
  31. },
  32. }))
  33. vi.doMock(
  34. '../../../../app/src/Features/Project/ProjectRootDocManager',
  35. () => ({
  36. default: (ctx.ProjectRootDocManager = {
  37. promises: {},
  38. }),
  39. })
  40. )
  41. vi.doMock('../../../../app/src/Features/Project/ProjectGetter', () => ({
  42. default: (ctx.ProjectGetter = { promises: {} }),
  43. }))
  44. vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({
  45. default: (ctx.UserGetter = { promises: {} }),
  46. }))
  47. vi.doMock('../../../../app/src/Features/Compile/ClsiManager', () => ({
  48. default: (ctx.ClsiManager = { promises: {} }),
  49. }))
  50. vi.doMock('../../../../app/src/infrastructure/RateLimiter.mjs', () => ({
  51. RateLimiter: sinon.stub().returns(ctx.rateLimiter),
  52. }))
  53. vi.doMock('@overleaf/metrics', () => ({
  54. default: ctx.Metrics,
  55. }))
  56. vi.doMock(
  57. '../../../../app/src/Features/Analytics/UserAnalyticsIdCache',
  58. () => ({
  59. default: (ctx.UserAnalyticsIdCache = {
  60. getWithMetrics: sinon.stub().resolves('abc'),
  61. }),
  62. })
  63. )
  64. vi.doMock(
  65. '../../../../app/src/Features/SplitTests/SplitTestHandler',
  66. () => ({
  67. default: (ctx.SplitTestHandler = {
  68. promises: {},
  69. }),
  70. })
  71. )
  72. ctx.CompileManager = (await import(MODULE_PATH)).default
  73. ctx.project_id = 'mock-project-id-123'
  74. ctx.user_id = 'mock-user-id-123'
  75. ctx.callback = sinon.stub()
  76. ctx.limits = {
  77. timeout: 42,
  78. compileGroup: 'standard',
  79. }
  80. })
  81. describe('compile', function () {
  82. beforeEach(function (ctx) {
  83. ctx.CompileManager._checkIfRecentlyCompiled = sinon.stub().resolves(false)
  84. ctx.ProjectRootDocManager.promises.ensureRootDocumentIsSet = sinon
  85. .stub()
  86. .resolves()
  87. ctx.CompileManager.promises.getProjectCompileLimits = sinon
  88. .stub()
  89. .resolves(ctx.limits)
  90. ctx.ClsiManager.promises.sendRequest = sinon.stub().resolves({
  91. status: (ctx.status = 'mock-status'),
  92. outputFiles: (ctx.outputFiles = []),
  93. clsiServerId: (ctx.output = 'mock output'),
  94. })
  95. })
  96. describe('succesfully', function () {
  97. let result
  98. beforeEach(async function (ctx) {
  99. ctx.CompileManager._checkIfAutoCompileLimitHasBeenHit = async (
  100. isAutoCompile,
  101. compileGroup
  102. ) => true
  103. ctx.ProjectGetter.promises.getProject = sinon
  104. .stub()
  105. .resolves(
  106. (ctx.project = { owner_ref: (ctx.owner_id = 'owner-id-123') })
  107. )
  108. ctx.UserGetter.promises.getUser = sinon.stub().resolves(
  109. (ctx.user = {
  110. features: { compileTimeout: '20s', compileGroup: 'standard' },
  111. analyticsId: 'abc',
  112. })
  113. )
  114. result = await ctx.CompileManager.promises.compile(
  115. ctx.project_id,
  116. ctx.user_id,
  117. {}
  118. )
  119. })
  120. it('should check the project has not been recently compiled', function (ctx) {
  121. ctx.CompileManager._checkIfRecentlyCompiled
  122. .calledWith(ctx.project_id, ctx.user_id)
  123. .should.equal(true)
  124. })
  125. it('should ensure that the root document is set', function (ctx) {
  126. ctx.ProjectRootDocManager.promises.ensureRootDocumentIsSet
  127. .calledWith(ctx.project_id)
  128. .should.equal(true)
  129. })
  130. it('should get the project compile limits', function (ctx) {
  131. ctx.CompileManager.promises.getProjectCompileLimits
  132. .calledWith(ctx.project_id)
  133. .should.equal(true)
  134. })
  135. it('should run the compile with the compile limits', function (ctx) {
  136. ctx.ClsiManager.promises.sendRequest
  137. .calledWith(ctx.project_id, ctx.user_id, {
  138. timeout: ctx.limits.timeout,
  139. compileGroup: 'standard',
  140. buildId: sinon.match(/[a-f0-9]+-[a-f0-9]+/),
  141. })
  142. .should.equal(true)
  143. })
  144. it('should resolve with the output', function (ctx) {
  145. expect(result).to.haveOwnProperty('status', ctx.status)
  146. expect(result).to.haveOwnProperty('clsiServerId', ctx.output)
  147. expect(result).to.haveOwnProperty('outputFiles', ctx.outputFiles)
  148. })
  149. it('should time the compile', function (ctx) {
  150. ctx.timer.done.called.should.equal(true)
  151. })
  152. })
  153. describe('when the project has been recently compiled', function () {
  154. it('should return', async function (ctx) {
  155. ctx.CompileManager._checkIfAutoCompileLimitHasBeenHit = async (
  156. isAutoCompile,
  157. compileGroup
  158. ) => true
  159. ctx.CompileManager._checkIfRecentlyCompiled = sinon
  160. .stub()
  161. .resolves(true)
  162. const { status } = await ctx.CompileManager.promises.compile(
  163. ctx.project_id,
  164. ctx.user_id,
  165. {}
  166. )
  167. status.should.equal('too-recently-compiled')
  168. })
  169. })
  170. describe('should check the rate limit', function () {
  171. it('should return', async function (ctx) {
  172. ctx.CompileManager._checkIfAutoCompileLimitHasBeenHit = sinon
  173. .stub()
  174. .resolves(false)
  175. const { status } = await ctx.CompileManager.promises.compile(
  176. ctx.project_id,
  177. ctx.user_id,
  178. {}
  179. )
  180. expect(status).to.equal('autocompile-backoff')
  181. })
  182. })
  183. })
  184. describe('getProjectCompileLimits', function () {
  185. beforeEach(async function (ctx) {
  186. ctx.features = {
  187. compileTimeout: (ctx.timeout = 42),
  188. compileGroup: (ctx.group = 'priority'),
  189. }
  190. ctx.ProjectGetter.promises.getProject = sinon
  191. .stub()
  192. .resolves(
  193. (ctx.project = { owner_ref: (ctx.owner_id = 'owner-id-123') })
  194. )
  195. ctx.UserGetter.promises.getUser = sinon
  196. .stub()
  197. .resolves((ctx.user = { features: ctx.features, analyticsId: 'abc' }))
  198. try {
  199. const result =
  200. await ctx.CompileManager.promises.getProjectCompileLimits(
  201. ctx.project_id
  202. )
  203. ctx.callback(null, result)
  204. } catch (error) {
  205. ctx.callback(error)
  206. }
  207. })
  208. it('should look up the owner of the project', function (ctx) {
  209. ctx.ProjectGetter.promises.getProject
  210. .calledWith(ctx.project_id, { owner_ref: 1, fromV1TemplateId: 1 })
  211. .should.equal(true)
  212. })
  213. it("should look up the owner's features", function (ctx) {
  214. ctx.UserGetter.promises.getUser
  215. .calledWith(ctx.project.owner_ref, {
  216. _id: 1,
  217. alphaProgram: 1,
  218. analyticsId: 1,
  219. betaProgram: 1,
  220. features: 1,
  221. })
  222. .should.equal(true)
  223. })
  224. it('should return the limits', function (ctx) {
  225. ctx.callback
  226. .calledWith(null, {
  227. timeout: ctx.timeout,
  228. compileGroup: ctx.group,
  229. compileBackendClass: 'c4d',
  230. ownerAnalyticsId: 'abc',
  231. })
  232. .should.equal(true)
  233. })
  234. })
  235. describe('compileBackendClass', function () {
  236. beforeEach(function (ctx) {
  237. ctx.features = {
  238. compileTimeout: 42,
  239. compileGroup: 'standard',
  240. }
  241. ctx.ProjectGetter.promises.getProject = sinon
  242. .stub()
  243. .resolves({ owner_ref: 'owner-id-123' })
  244. ctx.UserGetter.promises.getUser = sinon
  245. .stub()
  246. .resolves({ features: ctx.features, analyticsId: 'abc' })
  247. })
  248. describe('with priority compile', function () {
  249. beforeEach(function (ctx) {
  250. ctx.features.compileGroup = 'priority'
  251. })
  252. it('should return the default class', async function (ctx) {
  253. const { compileBackendClass } =
  254. await ctx.CompileManager.promises.getProjectCompileLimits(
  255. ctx.project_id
  256. )
  257. expect(compileBackendClass).to.equal('c4d')
  258. })
  259. })
  260. })
  261. describe('deleteAuxFiles', function () {
  262. let result
  263. beforeEach(async function (ctx) {
  264. ctx.CompileManager.promises.getProjectCompileLimits = sinon
  265. .stub()
  266. .resolves((ctx.limits = { compileGroup: 'mock-compile-group' }))
  267. ctx.ClsiManager.promises.deleteAuxFiles = sinon.stub().resolves('test')
  268. result = await ctx.CompileManager.promises.deleteAuxFiles(
  269. ctx.project_id,
  270. ctx.user_id
  271. )
  272. })
  273. it('should look up the compile group to use', function (ctx) {
  274. ctx.CompileManager.promises.getProjectCompileLimits
  275. .calledWith(ctx.project_id)
  276. .should.equal(true)
  277. })
  278. it('should delete the aux files', function (ctx) {
  279. ctx.ClsiManager.promises.deleteAuxFiles
  280. .calledWith(ctx.project_id, ctx.user_id, ctx.limits)
  281. .should.equal(true)
  282. })
  283. it('should resolve', function () {
  284. expect(result).not.to.be.undefined
  285. })
  286. })
  287. describe('_checkIfRecentlyCompiled', function () {
  288. describe('when the key exists in redis', function () {
  289. let result
  290. beforeEach(async function (ctx) {
  291. ctx.rclient.set = sinon.stub().resolves(null)
  292. result = await ctx.CompileManager._checkIfRecentlyCompiled(
  293. ctx.project_id,
  294. ctx.user_id
  295. )
  296. })
  297. it('should try to set the key', function (ctx) {
  298. ctx.rclient.set
  299. .calledWith(
  300. `compile:${ctx.project_id}:${ctx.user_id}`,
  301. true,
  302. 'EX',
  303. ctx.CompileManager.COMPILE_DELAY,
  304. 'NX'
  305. )
  306. .should.equal(true)
  307. })
  308. it('should resolve with true', function () {
  309. result.should.equal(true)
  310. })
  311. })
  312. describe('when the key does not exist in redis', function () {
  313. let result
  314. beforeEach(async function (ctx) {
  315. ctx.rclient.set = sinon.stub().resolves('OK')
  316. result = await ctx.CompileManager._checkIfRecentlyCompiled(
  317. ctx.project_id,
  318. ctx.user_id
  319. )
  320. })
  321. it('should try to set the key', function (ctx) {
  322. ctx.rclient.set
  323. .calledWith(
  324. `compile:${ctx.project_id}:${ctx.user_id}`,
  325. true,
  326. 'EX',
  327. ctx.CompileManager.COMPILE_DELAY,
  328. 'NX'
  329. )
  330. .should.equal(true)
  331. })
  332. it('should resolve with false', function () {
  333. result.should.equal(false)
  334. })
  335. })
  336. })
  337. describe('_checkIfAutoCompileLimitHasBeenHit', function () {
  338. it('should be able to compile if it is not an autocompile', async function (ctx) {
  339. const canCompile =
  340. await ctx.CompileManager._checkIfAutoCompileLimitHasBeenHit(
  341. false,
  342. 'everyone'
  343. )
  344. expect(canCompile).to.equal(true)
  345. })
  346. it('should be able to compile if rate limit has remaining', async function (ctx) {
  347. const canCompile =
  348. await ctx.CompileManager._checkIfAutoCompileLimitHasBeenHit(
  349. true,
  350. 'everyone'
  351. )
  352. expect(ctx.rateLimiter.consume).to.have.been.calledWith('global')
  353. expect(canCompile).to.equal(true)
  354. })
  355. it('should be not able to compile if rate limit has no remianing', async function (ctx) {
  356. ctx.rateLimiter.consume.rejects({ remainingPoints: 0 })
  357. const canCompile =
  358. await ctx.CompileManager._checkIfAutoCompileLimitHasBeenHit(
  359. true,
  360. 'everyone'
  361. )
  362. expect(canCompile).to.equal(false)
  363. })
  364. it('should return false if there is an error in the rate limit', async function (ctx) {
  365. ctx.rateLimiter.consume.rejects(new Error('BOOM!'))
  366. const canCompile =
  367. await ctx.CompileManager._checkIfAutoCompileLimitHasBeenHit(
  368. true,
  369. 'everyone'
  370. )
  371. expect(canCompile).to.equal(false)
  372. })
  373. })
  374. describe('wordCount', function () {
  375. let result
  376. const wordCount = 1
  377. beforeEach(async function (ctx) {
  378. ctx.CompileManager.promises.getProjectCompileLimits = sinon
  379. .stub()
  380. .resolves((ctx.limits = { compileGroup: 'mock-compile-group' }))
  381. ctx.ClsiManager.promises.wordCount = sinon.stub().resolves(wordCount)
  382. result = await ctx.CompileManager.promises.wordCount(
  383. ctx.project_id,
  384. ctx.user_id,
  385. false
  386. )
  387. })
  388. it('should look up the compile group to use', function (ctx) {
  389. ctx.CompileManager.promises.getProjectCompileLimits
  390. .calledWith(ctx.project_id)
  391. .should.equal(true)
  392. })
  393. it('should call wordCount for project', function (ctx) {
  394. ctx.ClsiManager.promises.wordCount
  395. .calledWith(ctx.project_id, ctx.user_id, false, ctx.limits)
  396. .should.equal(true)
  397. })
  398. it('should resolve with the wordCount from the ClsiManager', function () {
  399. expect(result).to.equal(wordCount)
  400. })
  401. })
  402. })