Răsfoiți Sursa

Merge pull request #19270 from overleaf/jpa-faster-e2e-in-ci

[server-pro] faster e2e test CI wall time

GitOrigin-RevId: eeb6d3044d888acd4d52919507c0bc566d7e0b46
Jakob Ackermann 2 ani în urmă
părinte
comite
eb32d3c8be

+ 20 - 2
server-ce/test/Makefile

@@ -9,6 +9,8 @@ export PWD = $(shell pwd)
 export TEX_LIVE_DOCKER_IMAGE ?= quay.io/sharelatex/texlive-full:2023.1
 export ALL_TEX_LIVE_DOCKER_IMAGES ?= quay.io/sharelatex/texlive-full:2023.1,quay.io/sharelatex/texlive-full:2022.1
 export IMAGE_TAG_PRO ?= quay.io/sharelatex/sharelatex-pro:latest
+export CYPRESS_SHARD ?=
+export COMPOSE_PROJECT_NAME ?= test
 
 test-e2e-native:
 	docker compose -f docker-compose.yml -f docker-compose.native.yml up --build --no-log-prefix sharelatex host-admin -d
@@ -23,11 +25,27 @@ test-e2e-open:
 clean:
 	docker compose down --volumes --timeout 0
 
-prefetch:
-	docker compose pull e2e mongo redis saml ldap
+prefetch: prefetch_default
+prefetch_default: prefetch_default_compose
+prefetch_default_compose:
+	docker compose pull e2e mongo redis
+
+prefetch_default: prefetch_default_compose_build
+prefetch_default_compose_build:
 	docker compose build
+
+prefetch: prefetch_custom
+prefetch_custom: prefetch_custom_compose_pull
+prefetch_custom_compose_pull:
+	docker compose pull saml ldap
+
+prefetch_custom: prefetch_custom_texlive
+prefetch_custom_texlive:
 	echo -n "$$ALL_TEX_LIVE_DOCKER_IMAGES" | xargs -d, -I% \
 	sh -exc 'tag=%; re_tag=quay.io/sharelatex/$${tag#*/}; docker pull $$tag; docker tag $$tag $$re_tag'
+
+prefetch_custom: prefetch_old
+prefetch_old:
 	docker pull $(IMAGE_TAG_PRO:latest=4.2)
 	docker pull $(IMAGE_TAG_PRO:latest=5.0.1-RC1)
 	docker pull $(IMAGE_TAG_PRO:latest=5.0)

+ 2 - 1
server-ce/test/accounts.spec.ts

@@ -1,7 +1,8 @@
 import { createMongoUser, ensureUserExists, login } from './helpers/login'
-import { startWith } from './helpers/config'
+import { isExcludedBySharding, startWith } from './helpers/config'
 
 describe('Accounts', function () {
+  if (isExcludedBySharding('CE_DEFAULT')) return
   startWith({})
   ensureUserExists({ email: 'user@example.com' })
 

+ 2 - 1
server-ce/test/admin.spec.ts

@@ -1,4 +1,4 @@
-import { startWith } from './helpers/config'
+import { isExcludedBySharding, startWith } from './helpers/config'
 import { activateUser, ensureUserExists, login } from './helpers/login'
 import { v4 as uuid } from 'uuid'
 import { createProject } from './helpers/project'
@@ -20,6 +20,7 @@ describe('admin panel', function () {
       return cy.findByText(projectName).parent().parent()
     }
 
+    if (isExcludedBySharding('PRO_DEFAULT_2')) return
     startWith({
       pro: true,
     })

+ 2 - 1
server-ce/test/create-and-compile-project.spec.ts

@@ -1,9 +1,10 @@
 import { ensureUserExists, login } from './helpers/login'
 import { createProject } from './helpers/project'
-import { startWith } from './helpers/config'
+import { isExcludedBySharding, startWith } from './helpers/config'
 import { throttledRecompile } from './helpers/compile'
 
 describe('Project creation and compilation', function () {
+  if (isExcludedBySharding('CE_DEFAULT')) return
   startWith({})
   ensureUserExists({ email: 'user@example.com' })
   ensureUserExists({ email: 'collaborator@example.com' })

+ 2 - 1
server-ce/test/customization.spec.ts

@@ -1,6 +1,7 @@
-import { startWith } from './helpers/config'
+import { isExcludedBySharding, startWith } from './helpers/config'
 
 describe('Customization', () => {
+  if (isExcludedBySharding('CE_CUSTOM_1')) return
   startWith({
     vars: {
       OVERLEAF_APP_NAME: 'CUSTOM APP NAME',

+ 1 - 1
server-ce/test/cypress.config.js

@@ -23,6 +23,6 @@ module.exports = defineConfig({
     specPattern,
   },
   retries: {
-    runMode: 1,
+    runMode: 3,
   },
 })

+ 4 - 0
server-ce/test/docker-compose.yml

@@ -57,6 +57,7 @@ services:
     volumes:
       - ./:/e2e
     environment:
+      CYPRESS_SHARD:
       CYPRESS_BASE_URL: http://sharelatex
       SPEC_PATTERN: '**/*.spec.{js,jsx,ts,tsx}'
     depends_on:
@@ -76,6 +77,7 @@ services:
       - /tmp/.X11-unix:/tmp/.X11-unix
     user: "${DOCKER_USER:-1000:1000}"
     environment:
+      CYPRESS_SHARD:
       CYPRESS_BASE_URL: http://sharelatex
       SPEC_PATTERN: '**/*.spec.{js,jsx,ts,tsx}'
       DISPLAY: ${DISPLAY:-:0}
@@ -96,6 +98,8 @@ services:
     stop_grace_period: 0s
     environment:
       PWD:
+      CYPRESS_SHARD:
+      COMPOSE_PROJECT_NAME:
       TEX_LIVE_DOCKER_IMAGE:
       ALL_TEX_LIVE_DOCKER_IMAGES:
       IMAGE_TAG_CE: ${IMAGE_TAG_CE:-sharelatex/sharelatex:latest}

+ 2 - 1
server-ce/test/editor.spec.ts

@@ -1,10 +1,11 @@
 import { createProject } from './helpers/project'
-import { startWith } from './helpers/config'
+import { isExcludedBySharding, startWith } from './helpers/config'
 import { ensureUserExists, login } from './helpers/login'
 import { v4 as uuid } from 'uuid'
 import { beforeWithReRunOnTestRetry } from './helpers/beforeWithReRunOnTestRetry'
 
 describe('editor', () => {
+  if (isExcludedBySharding('PRO_DEFAULT_1')) return
   startWith({ pro: true })
   ensureUserExists({ email: 'user@example.com' })
   ensureUserExists({ email: 'collaborator@example.com' })

+ 3 - 1
server-ce/test/external-auth.spec.ts

@@ -1,6 +1,7 @@
-import { startWith } from './helpers/config'
+import { isExcludedBySharding, startWith } from './helpers/config'
 
 describe('SAML', () => {
+  if (isExcludedBySharding('PRO_CUSTOM_1')) return
   const overleafPublicHost = Cypress.env('OVERLEAF_PUBLIC_HOST') || 'sharelatex'
   const samlPublicHost = Cypress.env('SAML_PUBLIC_HOST') || 'saml'
 
@@ -36,6 +37,7 @@ describe('SAML', () => {
 })
 
 describe('LDAP', () => {
+  if (isExcludedBySharding('PRO_CUSTOM_1')) return
   startWith({
     pro: true,
     vars: {

+ 4 - 1
server-ce/test/git-bridge.spec.ts

@@ -1,4 +1,4 @@
-import { startWith } from './helpers/config'
+import { isExcludedBySharding, startWith } from './helpers/config'
 import { ensureUserExists, login } from './helpers/login'
 import { createProject } from './helpers/project'
 
@@ -19,6 +19,7 @@ describe('git-bridge', function () {
     Cypress.env('GIT_BRIDGE_PUBLIC_HOST') || 'sharelatex'
 
   describe('enabled in Server Pro', function () {
+    if (isExcludedBySharding('PRO_CUSTOM_1')) return
     startWith({
       pro: true,
       vars: ENABLED_VARS,
@@ -272,6 +273,7 @@ Hello world
   }
 
   describe('disabled in Server Pro', () => {
+    if (isExcludedBySharding('PRO_DEFAULT_1')) return
     startWith({
       pro: true,
     })
@@ -279,6 +281,7 @@ Hello world
   })
 
   describe('unavailable in CE', () => {
+    if (isExcludedBySharding('CE_CUSTOM_1')) return
     startWith({
       pro: false,
       vars: ENABLED_VARS,

+ 8 - 14
server-ce/test/graceful-shutdown.spec.ts

@@ -1,14 +1,10 @@
+import { ensureUserExists, login } from './helpers/login'
 import {
-  ensureUserExists,
-  login,
-  resetCreatedUsersCache,
-} from './helpers/login'
-import { STARTUP_TIMEOUT, startWith } from './helpers/config'
-import {
-  dockerCompose,
-  getRedisKeys,
-  resetData,
-} from './helpers/hostAdminClient'
+  isExcludedBySharding,
+  STARTUP_TIMEOUT,
+  startWith,
+} from './helpers/config'
+import { dockerCompose, getRedisKeys } from './helpers/hostAdminClient'
 import { createProject } from './helpers/project'
 import { throttledRecompile } from './helpers/compile'
 
@@ -23,13 +19,11 @@ function bringServerProBackUp() {
 }
 
 describe('GracefulShutdown', function () {
-  before(async () => {
-    resetCreatedUsersCache()
-    await resetData()
-  })
+  if (isExcludedBySharding('PRO_CUSTOM_1')) return
   startWith({
     pro: true,
     withDataDir: true,
+    resetData: true,
   })
   ensureUserExists({ email: USER })
 

+ 24 - 2
server-ce/test/helpers/config.ts

@@ -1,8 +1,24 @@
 import { reconfigure } from './hostAdminClient'
+import { resetCreatedUsersCache } from './login'
 
 export const STARTUP_TIMEOUT =
   parseInt(Cypress.env('STARTUP_TIMEOUT'), 10) || 120_000
 
+export function isExcludedBySharding(
+  shard:
+    | 'CE_DEFAULT'
+    | 'CE_CUSTOM_1'
+    | 'CE_CUSTOM_2'
+    | 'PRO_DEFAULT_1'
+    | 'PRO_DEFAULT_2'
+    | 'PRO_CUSTOM_1'
+    | 'PRO_CUSTOM_2'
+    | 'PRO_CUSTOM_3'
+) {
+  const SHARD = Cypress.env('SHARD')
+  return SHARD && shard !== SHARD
+}
+
 let lastConfig: string
 
 export function startWith({
@@ -11,6 +27,7 @@ export function startWith({
   vars = {},
   varsFn = () => ({}),
   withDataDir = false,
+  resetData = false,
 }) {
   before(async function () {
     Object.assign(vars, varsFn())
@@ -20,10 +37,15 @@ export function startWith({
       vars,
       withDataDir,
     })
-    if (lastConfig === cfg) return
+    if (resetData) {
+      resetCreatedUsersCache()
+      // no return here, always reconfigure when resetting data
+    } else if (lastConfig === cfg) {
+      return
+    }
 
     this.timeout(STARTUP_TIMEOUT)
-    await reconfigure({ pro, version, vars, withDataDir })
+    await reconfigure({ pro, version, vars, withDataDir, resetData })
     lastConfig = cfg
   })
 }

+ 2 - 12
server-ce/test/helpers/hostAdminClient.ts

@@ -9,23 +9,12 @@ export async function dockerCompose(cmd: string, ...args: string[]) {
   })
 }
 
-export async function mongoInit() {
-  return await fetchJSON(`${hostAdminUrl}/mongo/init`, {
-    method: 'POST',
-  })
-}
-
-export async function resetData() {
-  return await fetchJSON(`${hostAdminUrl}/reset/data`, {
-    method: 'POST',
-  })
-}
-
 export async function reconfigure({
   pro = false,
   version = 'latest',
   vars = {},
   withDataDir = false,
+  resetData = false,
 }) {
   return await fetchJSON(`${hostAdminUrl}/reconfigure`, {
     method: 'POST',
@@ -34,6 +23,7 @@ export async function reconfigure({
       version,
       vars,
       withDataDir,
+      resetData,
     }),
   })
 }

+ 2 - 1
server-ce/test/history.spec.ts

@@ -1,9 +1,10 @@
 import { createProject } from './helpers/project'
 import { throttledRecompile } from './helpers/compile'
 import { ensureUserExists, login } from './helpers/login'
-import { startWith } from './helpers/config'
+import { isExcludedBySharding, startWith } from './helpers/config'
 
 describe('History', function () {
+  if (isExcludedBySharding('CE_DEFAULT')) return
   startWith({})
   ensureUserExists({ email: 'user@example.com' })
   beforeEach(function () {

+ 54 - 48
server-ce/test/host-admin.js

@@ -10,12 +10,19 @@ const {
 } = require('celebrate')
 const YAML = require('js-yaml')
 
+const DATA_DIR = Path.join(
+  __dirname,
+  'data',
+  // Give each shard their own data dir.
+  process.env.CYPRESS_SHARD || 'default'
+)
 const PATHS = {
   DOCKER_COMPOSE_FILE: 'docker-compose.yml',
-  DOCKER_COMPOSE_OVERRIDE: 'docker-compose.override.yml',
+  // Give each shard their own override file.
+  DOCKER_COMPOSE_OVERRIDE: `docker-compose.${process.env.CYPRESS_SHARD || 'override'}.yml`,
   DOCKER_COMPOSE_NATIVE: 'docker-compose.native.yml',
-  DATA_DIR: Path.join(__dirname, 'data'),
-  SANDBOXED_COMPILES_HOST_DIR: Path.join(__dirname, 'data/compiles'),
+  DATA_DIR,
+  SANDBOXED_COMPILES_HOST_DIR: Path.join(DATA_DIR, 'compiles'),
 }
 const IMAGES = {
   CE: process.env.IMAGE_TAG_CE.replace(/:.+/, ''),
@@ -245,7 +252,8 @@ app.post(
   }
 )
 
-function mongoInit(callback) {
+function maybeMongoInit(mongoInit, callback) {
+  if (!mongoInit) return callback()
   runDockerCompose(
     'up',
     ['--detach', '--wait', 'mongo'],
@@ -271,11 +279,30 @@ function mongoInit(callback) {
   )
 }
 
-app.post('/mongo/init', (req, res) => {
-  mongoInit((error, stdout, stderr) => {
-    res.json({ error, stdout, stderr })
-  })
-})
+function maybeResetData(resetData, callback) {
+  if (!resetData) return callback()
+
+  runDockerCompose(
+    'stop',
+    ['--timeout=0', 'sharelatex'],
+    (error, stdout, stderr) => {
+      if (error) return callback(error, stdout, stderr)
+
+      try {
+        purgeDataDir()
+      } catch (error) {
+        return callback(error)
+      }
+
+      mongoIsInitialized = false
+      runDockerCompose(
+        'down',
+        ['--timeout=0', '--volumes', 'mongo', 'redis'],
+        callback
+      )
+    }
+  )
+}
 
 app.post(
   '/reconfigure',
@@ -286,56 +313,35 @@ app.post(
         version: Joi.string().required(),
         vars: allowedVars,
         withDataDir: Joi.boolean().optional(),
+        resetData: Joi.boolean().optional(),
       },
     },
     { allowUnknown: false }
   ),
   (req, res) => {
-    const { pro, version, vars, withDataDir } = req.body
-    try {
-      setVarsDockerCompose({ pro, version, vars, withDataDir })
-    } catch (error) {
-      return res.json({ error })
-    }
-
-    const doMongoInit = mongoIsInitialized ? cb => cb() : mongoInit
-    doMongoInit((error, stdout, stderr) => {
-      if (error) return res.json({ error, stdout, stderr })
-      runDockerCompose(
-        'up',
-        ['--detach', '--wait', 'sharelatex'],
-        (error, stdout, stderr) => {
-          res.json({ error, stdout, stderr })
-        }
-      )
-    })
-  }
-)
-
-app.post('/reset/data', (req, res) => {
-  runDockerCompose(
-    'stop',
-    ['--timeout=0', 'sharelatex'],
-    (error, stdout, stderr) => {
+    const { pro, version, vars, withDataDir, resetData } = req.body
+    maybeResetData(resetData, (error, stdout, stderr) => {
       if (error) return res.json({ error, stdout, stderr })
 
       try {
-        purgeDataDir()
+        setVarsDockerCompose({ pro, version, vars, withDataDir })
       } catch (error) {
         return res.json({ error })
       }
 
-      mongoIsInitialized = false
-      runDockerCompose(
-        'down',
-        ['--timeout=0', '--volumes', 'mongo', 'redis'],
-        (error, stdout, stderr) => {
-          res.json({ error, stdout, stderr })
-        }
-      )
-    }
-  )
-})
+      maybeMongoInit(!mongoIsInitialized, (error, stdout, stderr) => {
+        if (error) return res.json({ error, stdout, stderr })
+        runDockerCompose(
+          'up',
+          ['--detach', '--wait', 'sharelatex'],
+          (error, stdout, stderr) => {
+            res.json({ error, stdout, stderr })
+          }
+        )
+      })
+    })
+  }
+)
 
 app.get('/redis/keys', (req, res) => {
   runDockerCompose(
@@ -352,7 +358,7 @@ app.use(handleValidationErrors())
 purgeDataDir()
 
 // Init on startup
-mongoInit(err => {
+maybeMongoInit(true, err => {
   if (err) {
     console.error('mongo init failed', err)
     process.exit(1)

+ 4 - 1
server-ce/test/learn-wiki.spec.ts

@@ -1,4 +1,4 @@
-import { startWith } from './helpers/config'
+import { isExcludedBySharding, startWith } from './helpers/config'
 import { ensureUserExists, login } from './helpers/login'
 import { v4 as uuid } from 'uuid'
 
@@ -16,6 +16,7 @@ describe('LearnWiki', function () {
   ensureUserExists({ email: REGULAR_USER })
 
   describe('enabled in Pro', () => {
+    if (isExcludedBySharding('PRO_CUSTOM_2')) return
     startWith({
       pro: true,
       vars: {
@@ -64,11 +65,13 @@ describe('LearnWiki', function () {
   })
 
   describe('disabled in Pro', () => {
+    if (isExcludedBySharding('PRO_DEFAULT_1')) return
     startWith({ pro: true })
     checkDisabled()
   })
 
   describe('unavailable in CE', () => {
+    if (isExcludedBySharding('CE_CUSTOM_1')) return
     startWith({
       pro: false,
       vars: {

+ 2 - 1
server-ce/test/project-list.spec.ts

@@ -1,12 +1,13 @@
 import { ensureUserExists, login } from './helpers/login'
 import { createProject } from './helpers/project'
-import { startWith } from './helpers/config'
+import { isExcludedBySharding, startWith } from './helpers/config'
 import { v4 as uuid } from 'uuid'
 
 const WITHOUT_PROJECTS_USER = 'user-without-projects@example.com'
 const REGULAR_USER = 'user@example.com'
 
 describe('Project List', () => {
+  if (isExcludedBySharding('PRO_DEFAULT_2')) return
   startWith({ pro: true })
 
   const findProjectRow = (projectName: string) => {

+ 2 - 1
server-ce/test/project-sharing.spec.ts

@@ -1,11 +1,12 @@
 import { v4 as uuid } from 'uuid'
-import { startWith } from './helpers/config'
+import { isExcludedBySharding, startWith } from './helpers/config'
 import { ensureUserExists, login } from './helpers/login'
 import { createProject } from './helpers/project'
 import { throttledRecompile } from './helpers/compile'
 import { beforeWithReRunOnTestRetry } from './helpers/beforeWithReRunOnTestRetry'
 
 describe('Project Sharing', function () {
+  if (isExcludedBySharding('CE_CUSTOM_2')) return
   ensureUserExists({ email: 'user@example.com' })
   startWith({ withDataDir: true })
 

+ 4 - 1
server-ce/test/sandboxed-compiles.spec.ts

@@ -1,6 +1,6 @@
 import { ensureUserExists, login } from './helpers/login'
 import { createProject } from './helpers/project'
-import { startWith } from './helpers/config'
+import { isExcludedBySharding, startWith } from './helpers/config'
 import { throttledRecompile } from './helpers/compile'
 import { v4 as uuid } from 'uuid'
 import { waitUntilScrollingFinished } from './helpers/waitUntilScrollingFinished'
@@ -19,6 +19,7 @@ describe('SandboxedCompiles', function () {
   }
 
   describe('enabled in Server Pro', () => {
+    if (isExcludedBySharding('PRO_CUSTOM_2')) return
     startWith({
       pro: true,
       vars: enabledVars,
@@ -215,6 +216,7 @@ describe('SandboxedCompiles', function () {
   }
 
   describe('disabled in Server Pro', () => {
+    if (isExcludedBySharding('PRO_DEFAULT_2')) return
     startWith({ pro: true })
 
     checkUsesDefaultCompiler()
@@ -223,6 +225,7 @@ describe('SandboxedCompiles', function () {
   })
 
   describe.skip('unavailable in CE', () => {
+    if (isExcludedBySharding('CE_CUSTOM_1')) return
     startWith({ pro: false, vars: enabledVars })
 
     checkUsesDefaultCompiler()

+ 4 - 1
server-ce/test/templates.spec.ts

@@ -1,4 +1,4 @@
-import { startWith } from './helpers/config'
+import { isExcludedBySharding, startWith } from './helpers/config'
 import { ensureUserExists, login } from './helpers/login'
 import { createProject } from './helpers/project'
 
@@ -32,6 +32,7 @@ describe('Templates', () => {
   }
 
   describe('enabled in Server Pro', () => {
+    if (isExcludedBySharding('PRO_CUSTOM_2')) return
     startWith({
       pro: true,
       varsFn,
@@ -238,11 +239,13 @@ describe('Templates', () => {
   }
 
   describe('disabled Server Pro', () => {
+    if (isExcludedBySharding('PRO_DEFAULT_2')) return
     startWith({ pro: true })
     checkDisabled()
   })
 
   describe('unavailable in CE', () => {
+    if (isExcludedBySharding('CE_CUSTOM_1')) return
     startWith({
       pro: false,
       varsFn,

+ 6 - 11
server-ce/test/upgrading.spec.ts

@@ -1,10 +1,6 @@
-import {
-  ensureUserExists,
-  login,
-  resetCreatedUsersCache,
-} from './helpers/login'
-import { startWith } from './helpers/config'
-import { dockerCompose, resetData, runScript } from './helpers/hostAdminClient'
+import { ensureUserExists, login } from './helpers/login'
+import { isExcludedBySharding, startWith } from './helpers/config'
+import { dockerCompose, runScript } from './helpers/hostAdminClient'
 import { createProject } from './helpers/project'
 import { throttledRecompile } from './helpers/compile'
 import { v4 as uuid } from 'uuid'
@@ -13,6 +9,8 @@ const USER = 'user@example.com'
 const PROJECT_NAME = 'Old Project'
 
 describe('Upgrading', function () {
+  if (isExcludedBySharding('PRO_CUSTOM_3')) return
+
   function testUpgrade(
     steps: {
       version: string
@@ -24,16 +22,13 @@ describe('Upgrading', function () {
     const startOptions = steps.shift()!
 
     before(async () => {
-      cy.log('Reset mongo/redis/on-disk data')
-      resetCreatedUsersCache()
-      await resetData()
-
       cy.log('Create old instance')
     })
     startWith({
       pro: true,
       version: startOptions.version,
       withDataDir: true,
+      resetData: true,
       vars: startOptions.vars,
     })
     before(function () {