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

[server-pro] faster e2e test CI wall time

GitOrigin-RevId: eeb6d3044d888acd4d52919507c0bc566d7e0b46
This commit is contained in:
Jakob Ackermann 2024-07-10 16:00:41 +02:00 committed by Copybot
parent c3ed95bc48
commit eb32d3c8be
21 changed files with 154 additions and 103 deletions

View file

@ -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)

View file

@ -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' })

View file

@ -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,
})

View file

@ -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' })

View file

@ -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',

View file

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

View file

@ -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}

View file

@ -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' })

View file

@ -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: {

View file

@ -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,

View file

@ -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 })

View file

@ -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
})
}

View file

@ -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,
}),
})
}

View file

@ -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 () {

View file

@ -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)

View file

@ -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: {

View file

@ -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) => {

View file

@ -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 })

View file

@ -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()

View file

@ -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,

View file

@ -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 () {