Просмотр исходного кода

[web] fetch file-tree in fallback to old compile mode (#32403)

* [web] fetch file-tree in fallback to old compile mode

* [web] tweak unit test

GitOrigin-RevId: 1c5f8cb1f0cb15c91efc46babec343d228631019
Jakob Ackermann 4 месяцев назад
Родитель
Сommit
205573c01a

+ 8 - 0
services/web/app/src/Features/Compile/ClsiManager.mjs

@@ -768,6 +768,10 @@ async function _buildRequest(projectId, userId, options) {
         'failed to compose history-full request'
         'failed to compose history-full request'
       )
       )
       // fall back to old compile mode
       // fall back to old compile mode
+      return await _buildRequest(projectId, userId, {
+        ...options,
+        compileFromHistory: false,
+      })
     }
     }
   } else if (options.compileFromHistory) {
   } else if (options.compileFromHistory) {
     // incremental sync
     // incremental sync
@@ -785,6 +789,10 @@ async function _buildRequest(projectId, userId, options) {
         'failed to compose history-incremental request'
         'failed to compose history-incremental request'
       )
       )
       // fall back to old compile mode
       // fall back to old compile mode
+      return await _buildRequest(projectId, userId, {
+        ...options,
+        compileFromHistory: false,
+      })
     }
     }
   }
   }
 
 

+ 184 - 8
services/web/test/unit/src/Compile/ClsiManager.test.mjs

@@ -3,6 +3,7 @@ import { setTimeout } from 'node:timers/promises'
 import sinon from 'sinon'
 import sinon from 'sinon'
 import tk from 'timekeeper'
 import tk from 'timekeeper'
 import { RequestFailedError } from '@overleaf/fetch-utils'
 import { RequestFailedError } from '@overleaf/fetch-utils'
+import _ from 'lodash'
 
 
 const FILESTORE_URL = 'http://filestore.example.com'
 const FILESTORE_URL = 'http://filestore.example.com'
 const CLSI_HOST = 'clsi.example.com'
 const CLSI_HOST = 'clsi.example.com'
@@ -15,13 +16,6 @@ describe('ClsiManager', function () {
     tk.freeze(Date.now())
     tk.freeze(Date.now())
 
 
     ctx.user_id = 'user-id'
     ctx.user_id = 'user-id'
-    ctx.project = {
-      _id: 'project-id',
-      compiler: 'latex',
-      rootDoc_id: 'mock-doc-id-1',
-      imageName: 'mock-image-name',
-      overleaf: { history: { id: 42 } },
-    }
     ctx.docs = {
     ctx.docs = {
       '/main.tex': {
       '/main.tex': {
         name: 'main.tex',
         name: 'main.tex',
@@ -53,6 +47,20 @@ describe('ClsiManager', function () {
         created: new Date(),
         created: new Date(),
       },
       },
     }
     }
+    ctx.project = {
+      _id: 'project-id',
+      compiler: 'latex',
+      rootDoc_id: 'mock-doc-id-1',
+      imageName: 'mock-image-name',
+      overleaf: { history: { id: 42 } },
+      rootFolder: [
+        {
+          docs: [],
+          files: [],
+          folders: [],
+        },
+      ],
+    }
     ctx.clsiCookieKey = 'clsiserver'
     ctx.clsiCookieKey = 'clsiserver'
     ctx.clsiServerId = 'clsi-server-id'
     ctx.clsiServerId = 'clsi-server-id'
     ctx.newClsiServerId = 'newserver'
     ctx.newClsiServerId = 'newserver'
@@ -121,7 +129,17 @@ describe('ClsiManager', function () {
     ctx.ProjectGetter = {
     ctx.ProjectGetter = {
       promises: {
       promises: {
         findById: sinon.stub().resolves(ctx.project),
         findById: sinon.stub().resolves(ctx.project),
-        getProject: sinon.stub().resolves(ctx.project),
+        getProject: sinon.stub().callsFake((projectId, projection) => {
+          const result = { _id: ctx.project._id }
+          for (const [field, v] of Object.entries(projection)) {
+            if (v) {
+              _.set(result, field, _.get(ctx.project, field))
+            } else {
+              _.unset(result, field)
+            }
+          }
+          return result
+        }),
       },
       },
     }
     }
     ctx.DocumentUpdaterHandler = {
     ctx.DocumentUpdaterHandler = {
@@ -162,6 +180,9 @@ describe('ClsiManager', function () {
       clearCache: sinon.stub().resolves(),
       clearCache: sinon.stub().resolves(),
     }
     }
     ctx.HistoryManager = {
     ctx.HistoryManager = {
+      promises: {
+        flushProject: sinon.stub().resolves(),
+      },
       getFilestoreBlobURL: sinon.stub().callsFake((historyId, hash) => {
       getFilestoreBlobURL: sinon.stub().callsFake((historyId, hash) => {
         if (hash === GLOBAL_BLOB_HASH) {
         if (hash === GLOBAL_BLOB_HASH) {
           return `${FILESTORE_URL}/history/global/hash/${hash}`
           return `${FILESTORE_URL}/history/global/hash/${hash}`
@@ -392,6 +413,155 @@ describe('ClsiManager', function () {
       })
       })
     })
     })
 
 
+    describe('with compile from history fallback to incremental', function () {
+      const buildId = '18fbe9e7564-30dcb2f71250c690'
+
+      beforeEach(async function (ctx) {
+        ctx.outputFiles = [
+          {
+            url: `/project/${ctx.project_id}/user/${ctx.user_id}/build/1234/output/output.pdf`,
+            path: 'output.pdf',
+            type: 'pdf',
+            build: buildId,
+          },
+          {
+            url: `/project/${ctx.project_id}/user/${ctx.user_id}/build/1234/output/output.log`,
+            path: 'output.log',
+            type: 'log',
+            build: buildId,
+          },
+        ]
+        ctx.responseBody.compile.outputFiles = ctx.outputFiles.map(
+          outputFile => ({
+            ...outputFile,
+            url: `http://${CLSI_HOST}${outputFile.url}`,
+          })
+        )
+        ctx.responseBody.compile.buildId = buildId
+        ctx.timeout = 100
+        ctx.HistoryManager.promises.flushProject.rejects()
+        const doc = ctx.docs['/main.tex']
+        ctx.DocumentUpdaterHandler.promises.getProjectDocsIfMatch.resolves([
+          { _id: doc._id, lines: doc.lines, v: 123 },
+        ])
+        ctx.ProjectEntityHandler.getAllDocPathsFromProject.returns({
+          'mock-doc-id-1': 'main.tex',
+        })
+        ctx.result = await ctx.ClsiManager.promises.sendRequest(
+          ctx.project._id,
+          ctx.user_id,
+          {
+            compileBackendClass: 'c3d',
+            compileGroup: 'standard',
+            timeout: ctx.timeout,
+            compileFromHistory: true,
+            incrementalCompilesEnabled: true,
+          }
+        )
+      })
+
+      it('should send the request to the CLSI', function (ctx) {
+        const doc = ctx.docs['/main.tex']
+        ctx.FetchUtils.fetchStringWithResponse.should.have.been.calledWith(
+          sinon.match(
+            url =>
+              url.host === CLSI_HOST &&
+              url.pathname ===
+                `/project/${ctx.project._id}/user/${ctx.user_id}/compile` &&
+              url.searchParams.get('compileBackendClass') === 'c3d' &&
+              url.searchParams.get('compileGroup') === 'standard'
+          ),
+          {
+            method: 'POST',
+            json: sinon.match({
+              compile: {
+                options: {
+                  compiler: ctx.project.compiler,
+                  imageName: ctx.project.imageName,
+                  timeout: ctx.timeout,
+                  draft: false,
+                  compileGroup: 'standard',
+                  metricsMethod: 'standard',
+                  stopOnFirstError: false,
+                  syncType: 'incremental',
+                  syncState: '01234567890abcdef',
+                },
+                rootResourcePath: 'main.tex',
+                resources: [
+                  { path: 'main.tex', content: doc.lines.join('\n') },
+                ],
+              },
+            }),
+            headers: {
+              Accept: 'application/json',
+              'Content-Type': 'application/json',
+              Cookie: `${ctx.clsiCookieKey}=${ctx.clsiServerId}`,
+            },
+            signal: sinon.match.instanceOf(AbortSignal),
+          }
+        )
+      })
+
+      it('should compute the hash with the full project', function (ctx) {
+        ctx.ClsiStateManager.computeHash.should.have.been.calledWithMatch({
+          rootFolder: ctx.project.rootFolder,
+        })
+      })
+
+      it('should get the project with the required fields', function (ctx) {
+        ctx.ProjectGetter.promises.getProject.should.have.been.calledWith(
+          ctx.project._id,
+          {
+            compiler: 1,
+            rootDoc_id: 1,
+            imageName: 1,
+            rootFolder: 1,
+            'overleaf.history.id': 1,
+          }
+        )
+      })
+
+      it('should get only the live docs from the docupdater with a background flush in docupdater', function (ctx) {
+        ctx.DocumentUpdaterHandler.promises.getProjectDocsIfMatch.should.have.been.calledWith(
+          ctx.project._id
+        )
+      })
+
+      it('should not get any docs from mongo', function (ctx) {
+        ctx.ProjectEntityHandler.promises.getAllDocs.should.not.have.been.calledWith(
+          ctx.project._id
+        )
+      })
+
+      it('should not get any of the files', function (ctx) {
+        ctx.ProjectEntityHandler.promises.getAllFiles.should.not.have.been
+          .called
+      })
+
+      it('should return the status and output files', function (ctx) {
+        expect(ctx.result.status).to.equal('success')
+        expect(ctx.result.outputFiles.map(f => f.path)).to.have.members(
+          ctx.outputFiles.map(f => f.path)
+        )
+      })
+
+      it('should return the buildId', function (ctx) {
+        expect(ctx.result.buildId).to.equal(buildId)
+      })
+
+      it('should persist the cookie from the response', function (ctx) {
+        expect(
+          ctx.ClsiCookieManager.promises.setServerId
+        ).to.have.been.calledWith(
+          ctx.project._id,
+          ctx.user_id,
+          'standard',
+          'c3d',
+          ctx.newClsiServerId
+        )
+      })
+    })
+
     describe('with ranges on the pdf and stats/timings details', function () {
     describe('with ranges on the pdf and stats/timings details', function () {
       beforeEach(async function (ctx) {
       beforeEach(async function (ctx) {
         ctx.ranges = [{ start: 1, end: 42, hash: 'foo' }]
         ctx.ranges = [{ start: 1, end: 42, hash: 'foo' }]
@@ -499,6 +669,12 @@ describe('ClsiManager', function () {
         )
         )
       })
       })
 
 
+      it('should not get any docs from mongo', function (ctx) {
+        ctx.ProjectEntityHandler.promises.getAllDocs.should.not.have.been.calledWith(
+          ctx.project._id
+        )
+      })
+
       it('should not get any of the files', function (ctx) {
       it('should not get any of the files', function (ctx) {
         ctx.ProjectEntityHandler.promises.getAllFiles.should.not.have.been
         ctx.ProjectEntityHandler.promises.getAllFiles.should.not.have.been
           .called
           .called