Skip to content

Commit db12fa7

Browse files
authored
ci: speed up the E2E tests and fix the flaky ones (#1296)
From push to the last E2E shard: 15.7 min on main (median of 12 runs) → 10.6 min on the last run of this branch, with 1 flaky test (0.7 per run on main). - run Playwright with one worker per core on each shard runner (was a single worker) - serve udata with uWSGI (one process per core, with HTTP keepalive) instead of the dev server - init the udata database once in a job running alongside the build, each shard restores the dump - serve the metrics API and matomo.js from a local fake server in CI, a Playwright route turns the browser cache off - the CADA tests replay recorded Tabular API responses instead of calling the real API - a unit test checks the real Tabular API still answers the recorded requests with the same structure (not the same data, so a CADA update doesn't break it) - upgrade Playwright to 1.64.1, its Firefox no longer stalls on every 10th context close (microsoft/playwright#42641) - `gotoHydrated` waits for Nuxt to report the end of hydration, `networkidle` alone isn't enough on a busy CPU - wait for hydration before interacting in the flaky search, discussion, reuse edition and HVD tests - longer timeouts for the map preview and the spreadsheet import, slow to start on a busy CPU - rewrite the edito blocs test without sleeps, picking each object by its title, with new screenshots - one test per URL for the malformed page test, leaving a page aborted the modules it was prefetching
1 parent e466315 commit db12fa7

59 files changed

Lines changed: 865 additions & 566 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/e2e/udata.cfg‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import os
2+
3+
from udata.settings import Defaults
4+
5+
DEBUG = True
6+
TESTING = True
7+
SECRET_KEY = 'test-secret-key-for-ci'
8+
API_TOKEN_SECRET = 'test-api-token-secret-for-ci'
9+
SEND_MAIL = False
10+
SERVER_NAME = 'localhost:7000'
11+
CDATA_BASE_URL = "http://localhost:3000"
12+
13+
# The job preparing the database caches in memory, so that importing the fixtures fetches the
14+
# schema catalog once rather than for every resource.
15+
CACHE_TYPE = os.environ.get('E2E_CACHE_TYPE', 'flask_caching.backends.NullCache')
16+
DEFAULT_LANGUAGE = 'fr'
17+
18+
SCHEMA_CATALOG_URL = 'https://schema.data.gouv.fr/schemas/schemas.json'
19+
20+
URLS_ALLOW_PRIVATE = True
21+
URLS_ALLOW_LOCAL = True
22+
URLS_ALLOWED_TLDS = Defaults.URLS_ALLOWED_TLDS | set(['local'])
23+
URLS_RESOLVE_HOSTNAME = False
24+
25+
PLUGINS = []
26+
# Once https://github.com/etalab/flask-storage/pull/30 is merged, revert to FS_ROOT = 'fs'.
27+
FS_ROOT = os.path.join(os.getcwd(), 'fs')
28+
29+
HARVESTER_BACKENDS = ['dcat', 'csw*']
30+
31+
SESSION_COOKIE_SECURE = False
32+
33+
SECURITY_EMAIL_VALIDATOR_ARGS = {
34+
"check_deliverability": False
35+
}
36+
SECURITY_TWO_FACTOR = True
37+
38+
# The E2E shards reach the services by their container name, the job preparing the
39+
# database runs on the runner itself and reaches them on localhost.
40+
MONGODB_HOST = os.environ.get('E2E_MONGODB_HOST', 'mongodb://mongodb:27017/udata')
41+
CELERY_BROKER_URL = os.environ.get('E2E_REDIS_URL', 'redis://redis:6379')
42+
CELERY_RESULT_BACKEND = CELERY_BROKER_URL
43+
# No worker runs here: tasks run within the request that triggers them, so that
44+
# what they do (the notifications of a discussion, of a badge) can be tested.
45+
CELERY_TASK_ALWAYS_EAGER = True

‎.github/workflows/ci.yml‎

Lines changed: 132 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ env:
1212
CONTAINER_REGISTRY_NAMESPACE: ${{ vars.CONTAINER_REGISTRY_NAMESPACE }}
1313
CONTAINER_NAME: ${{ vars.CONTAINER_NAME }}
1414
UDATA_WORKING_DIR: udata
15+
MONGODB_TOOLS: mongodb-database-tools-ubuntu2404-x86_64-100.13.0
16+
# Serves the metrics API, matomo.js and the Tabular API to the E2E tests, see tests/fake-services.ts
17+
FAKE_SERVICES_URL: http://localhost:3100
18+
# Set to record the Tabular API fixtures from the real API instead of replaying them: each
19+
# E2E shard then uploads what it recorded
20+
FAKE_SERVICES_RECORD: ''
1521

1622
permissions:
1723
id-token: write # Required for OIDC for npm publication
@@ -137,19 +143,95 @@ jobs:
137143
echo "Using udata $ref"
138144
echo "ref=$ref" >> "$GITHUB_OUTPUT"
139145
146+
# ============================================
147+
# JOB 2c: udata database the E2E tests start from (runs in parallel with build)
148+
# ============================================
149+
# Loading the zones and the fixtures takes minutes: done once here, each E2E shard restores
150+
# the dump in seconds instead.
151+
udata_fixtures:
152+
needs: udata_ref
153+
# The init downloads the zones and the fixtures without a timeout: a stalled download
154+
# must fail the run rather than hold the E2E shards waiting for hours
155+
timeout-minutes: 10
156+
runs-on: ubuntu-latest
157+
env:
158+
E2E_MONGODB_HOST: mongodb://localhost:27017/udata
159+
E2E_REDIS_URL: redis://localhost:6379
160+
E2E_CACHE_TYPE: flask_caching.backends.SimpleCache
161+
162+
services:
163+
mongodb:
164+
image: mongo:6.0.4
165+
ports:
166+
- 27017:27017
167+
redis:
168+
image: redis
169+
ports:
170+
- 6379:6379
171+
172+
steps:
173+
- name: Checkout cdata
174+
uses: actions/checkout@v6
175+
176+
- name: Checkout udata
177+
uses: actions/checkout@v6
178+
with:
179+
repository: opendatateam/udata
180+
path: ${{ env.UDATA_WORKING_DIR }}
181+
ref: ${{ needs.udata_ref.outputs.ref }}
182+
183+
- name: Set up uv
184+
uses: astral-sh/setup-uv@v6
185+
with:
186+
python-version: "3.11"
187+
working-directory: ${{ env.UDATA_WORKING_DIR }}
188+
189+
- name: Install Python dependencies for udata
190+
working-directory: ${{ env.UDATA_WORKING_DIR }}
191+
run: uv sync --frozen
192+
193+
- name: Configure udata
194+
run: cp .github/e2e/udata.cfg ${{ env.UDATA_WORKING_DIR }}/udata.cfg
195+
196+
- name: Initialize udata
197+
working-directory: ${{ env.UDATA_WORKING_DIR }}
198+
run: |
199+
uv run udata init
200+
201+
uv run udata licenses
202+
uv run udata spatial load
203+
uv run udata import-fixtures
204+
205+
uv run udata user create --first-name "Admin" --last-name "User" --email "admin@example.com" --password "@1337Password42" --admin
206+
uv run udata user create --first-name "Normal" --last-name "User" --email "normal@example.com" --password "@1337Password42"
207+
uv run udata user create --first-name "2FA" --last-name "User" --email "2fa@example.com" --password "@1337Password42"
208+
209+
# mongodump comes with the service image: nothing to install. TESTING makes udata
210+
# suffix its database name with `-test`.
211+
- name: Dump udata database
212+
run: docker exec ${{ job.services.mongodb.id }} mongodump --db udata-test --archive --gzip > udata.archive
213+
214+
- name: Upload udata database
215+
uses: actions/upload-artifact@v7
216+
with:
217+
name: udata-database
218+
path: udata.archive
219+
retention-days: 1
220+
if-no-files-found: error
221+
140222
# ============================================
141223
# JOB 3: E2E Tests (runs in parallel with quality)
142224
# ============================================
143225
e2e:
144-
needs: [build, udata_ref]
226+
needs: [build, udata_ref, udata_fixtures]
145227
timeout-minutes: 30
146228
runs-on: ubuntu-latest
147229
strategy:
148230
fail-fast: false
149231
matrix:
150232
browser: [chromium, firefox]
151233
# Each (browser, shard) pair runs in its own container with its own MongoDB/Redis/udata,
152-
# so Playwright can keep workers=1 (shared backend state) while shards run in parallel.
234+
# and Playwright runs one worker per core of that runner against it.
153235
# Keep the shard count in sync with the `--shard=.../N` value in the "Run E2E tests" step.
154236
shard: [1, 2, 3, 4]
155237
include:
@@ -158,7 +240,7 @@ jobs:
158240
- browser: firefox
159241
projects: "--project=setup --project=setup-normal-user --project=firefox"
160242
container:
161-
image: mcr.microsoft.com/playwright:v1.63.0-noble
243+
image: mcr.microsoft.com/playwright:v1.64.1-noble
162244
options: --user 1001
163245

164246
services:
@@ -209,76 +291,47 @@ jobs:
209291

210292
- name: Install Python dependencies for udata
211293
working-directory: ${{ env.UDATA_WORKING_DIR }}
212-
run: |
213-
uv sync --frozen
294+
run: uv sync --frozen
214295

215296
- name: Configure udata
216-
working-directory: ${{ env.UDATA_WORKING_DIR }}
217-
run: |
218-
cat << EOF > udata.cfg
219-
from udata.settings import Defaults
220-
221-
DEBUG = True
222-
TESTING = True
223-
SECRET_KEY = 'test-secret-key-for-ci'
224-
API_TOKEN_SECRET = 'test-api-token-secret-for-ci'
225-
SEND_MAIL = False
226-
SERVER_NAME = 'localhost:7000'
227-
CDATA_BASE_URL="http://localhost:3000"
228-
229-
CACHE_TYPE = 'flask_caching.backends.NullCache'
230-
DEFAULT_LANGUAGE = 'fr'
231-
232-
SCHEMA_CATALOG_URL='https://schema.data.gouv.fr/schemas/schemas.json'
233-
234-
URLS_ALLOW_PRIVATE = True
235-
URLS_ALLOW_LOCAL = True
236-
URLS_ALLOWED_TLDS = Defaults.URLS_ALLOWED_TLDS | set(['local'])
237-
URLS_RESOLVE_HOSTNAME = False
297+
run: cp .github/e2e/udata.cfg ${{ env.UDATA_WORKING_DIR }}/udata.cfg
238298

239-
PLUGINS = []
240-
# Once https://github.com/etalab/flask-storage/pull/30 is merged, revert to
241-
# FS_ROOT = 'fs' and quote the heredoc delimiter again (cat << 'EOF').
242-
FS_ROOT = '$PWD/fs'
243-
244-
HARVESTER_BACKENDS = ['dcat', 'csw*']
299+
- name: Download udata database
300+
uses: actions/download-artifact@v8
301+
with:
302+
name: udata-database
245303

246-
SESSION_COOKIE_SECURE = False
304+
- name: Cache MongoDB database tools
305+
id: mongodb-tools
306+
uses: actions/cache@v6
307+
with:
308+
path: mongodb-tools
309+
key: ${{ env.MONGODB_TOOLS }}
247310

248-
SECURITY_EMAIL_VALIDATOR_ARGS = {
249-
"check_deliverability": False
250-
}
251-
SECURITY_TWO_FACTOR = True
311+
- name: Install MongoDB database tools
312+
if: steps.mongodb-tools.outputs.cache-hit != 'true'
313+
run: |
314+
mkdir mongodb-tools
315+
curl -fsSL "https://fastdl.mongodb.org/tools/db/$MONGODB_TOOLS.tgz" | tar -xz --strip-components=1 -C mongodb-tools
252316
253-
MONGODB_HOST = 'mongodb://mongodb:27017/udata'
254-
CELERY_BROKER_URL = 'redis://redis:6379'
255-
CELERY_RESULT_BACKEND = 'redis://redis:6379'
256-
# No worker runs here: tasks run within the request that triggers them, so that
257-
# what they do (the notifications of a discussion, of a badge) can be tested.
258-
CELERY_TASK_ALWAYS_EAGER = True
259-
EOF
317+
- name: Restore udata database
318+
run: mongodb-tools/bin/mongorestore --uri mongodb://mongodb:27017 --archive=udata.archive --gzip
260319

261-
- name: Initialize udata
320+
- name: Start udata server
262321
working-directory: ${{ env.UDATA_WORKING_DIR }}
263322
run: |
264-
# Create fs directory
265323
mkdir -p fs
266324
267-
# Initialize udata
268-
uv run udata init
269-
270-
uv run udata licenses
271-
uv run udata spatial load
272-
uv run udata import-fixtures
273-
274-
uv run udata user create --first-name "Admin" --last-name "User" --email "admin@example.com" --password "@1337Password42" --admin
275-
uv run udata user create --first-name "Normal" --last-name "User" --email "normal@example.com" --password "@1337Password42"
276-
uv run udata user create --first-name "2FA" --last-name "User" --email "2fa@example.com" --password "@1337Password42"
277-
278325
uv run inv i18nc
279326
280-
# No reloader: storage writes in fs/ would restart the server mid-test.
281-
uv run inv serve --port=7000 > udata.log 2>&1 &
327+
# uWSGI as in production, with one process per core: the Playwright workers hit udata
328+
# concurrently, and the single-process development server would serialize them.
329+
# lazy-apps loads the app after the fork, so that no PyMongo client is shared across
330+
# processes. die-on-term makes SIGTERM stop uWSGI, which reloads on it otherwise.
331+
# http-keepalive stands for the nginx of production: the HTTP router closes every
332+
# connection otherwise, and the Playwright API client hangs up on the sockets it reuses.
333+
uv run --with pyuwsgi uwsgi --master --http localhost:7000 --http-keepalive \
334+
--module udata.wsgi:app --processes "$(nproc)" --lazy-apps --die-on-term > udata.log 2>&1 &
282335
283336
# Wait for udata to be ready
284337
timeout 90 bash -c 'until curl -f http://localhost:7000/api/1/site/; do sleep 2; done'
@@ -288,15 +341,19 @@ jobs:
288341
env:
289342
NUXT_PUBLIC_API_BASE: http://localhost:7000
290343
NUXT_PUBLIC_HOMEPAGE_HERO_IMAGES: "hero_15.png"
291-
# Lets the matomo plugin load (tests stub matomo.js at the context
292-
# level, the tracking spec asserts what the app sends). CI pre-starts
293-
# this server and Playwright reuses it (reuseExistingServer), so the
294-
# webServer.env in playwright.config.ts does not apply here.
295-
NUXT_PUBLIC_MATOMO_HOST: http://localhost:3000
344+
# Lets the matomo plugin load (tests/fake-services.ts serves an empty matomo.js,
345+
# the tracking spec asserts what the app sends). CI pre-starts this server and
346+
# Playwright reuses it (reuseExistingServer), so the webServer.env in
347+
# playwright.config.ts does not apply here.
348+
NUXT_PUBLIC_MATOMO_HOST: ${{ env.FAKE_SERVICES_URL }}
349+
# Under a path of its own: the metrics outage tests intercept everything below it,
350+
# and must not take matomo.js down with it
351+
NUXT_PUBLIC_METRICS_API: ${{ env.FAKE_SERVICES_URL }}/metrics
296352
NUXT_SITE_URL: http://localhost:3000
297353
NUXT_PUBLIC_CHARTS_API_BASE: http://localhost:7000
298354
NUXT_PUBLIC_CADA_RESOURCE_ID: 3a6d6fe3-8548-43ed-ad09-72052771447c
299355
NUXT_PUBLIC_GEOPF_ENABLED: "true"
356+
NUXT_PUBLIC_TABULAR_API_URL: ${{ env.FAKE_SERVICES_URL }}/tabular
300357
NUXT_PUBLIC_TABULAR_API_PREVIEW_RESOURCES_ID: 982d9dd0-365a-4c4b-8a83-75dec40c36bb
301358
NUXT_PUBLIC_TABULAR_API_PREVIEW_STATS_ID: 33cf9a65-3f77-4d88-acd1-bca420d83e60
302359
NUXT_PUBLIC_EXPLORER_FEEDBACK_URL: https://example.com/feedback
@@ -312,6 +369,7 @@ jobs:
312369
env:
313370
BASE_URL: http://localhost:3000
314371
NUXT_PUBLIC_API_BASE: http://localhost:7000
372+
NUXT_PUBLIC_METRICS_API: ${{ env.FAKE_SERVICES_URL }}/metrics
315373
UDATA_WORKING_DIR: ${{ env.UDATA_WORKING_DIR }}
316374
CI: true
317375
# Unique blob name per (browser, shard): the default is report-<shard>.zip, which would
@@ -325,7 +383,7 @@ jobs:
325383
working-directory: ${{ env.UDATA_WORKING_DIR }}
326384
run: |
327385
# Kill udata server to release the lock so that post setup-uv can prune the cache
328-
pkill -f "manage.py serve" || true
386+
pkill -f uwsgi || true
329387
330388
- name: Show server logs
331389
if: always()
@@ -336,6 +394,14 @@ jobs:
336394
echo "=== CDATA SERVER LOGS ==="
337395
cat cdata.log || echo "No cdata logs found"
338396
397+
- name: Upload recorded Tabular API fixtures
398+
uses: actions/upload-artifact@v7
399+
if: always() && env.FAKE_SERVICES_RECORD
400+
with:
401+
name: tabular-fixtures-${{ matrix.browser }}-${{ matrix.shard }}
402+
path: tests/fixtures/tabular
403+
retention-days: 1
404+
339405
- name: Upload screenshots
340406
uses: actions/upload-artifact@v7
341407
if: always()

‎components/Pages/EditoBlocs.vue‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
:key="bloc.id"
5555
class="relative"
5656
:class="bloc.class !== 'HeroBloc' && (striped ? 'py-24 odd:bg-gray-some even:bg-white' : 'py-12')"
57+
data-testid="edito-bloc"
5758
>
5859
<!-- Add button above the bloc (absolute positioned) -->
5960
<div

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@
9191
"@nuxt/fonts": "^0.14.0",
9292
"@nuxt/scripts": "^1.3.9",
9393
"@parcel/watcher": "^2.6.0",
94-
"@playwright/test": "^1.63.0",
94+
"@playwright/test": "^1.64.1",
9595
"@tailwindcss/oxide": "^4.3.3",
9696
"@tailwindcss/typography": "^0.5.20",
9797
"@types/culori": "^4.0.1",

‎playwright.config.ts‎

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ export default defineConfig({
2525
timeout: 60_000,
2626
/* Retry on CI to absorb transient failures (flaky externals, network blips); flakes still surface in the report. */
2727
retries: process.env.CI ? 2 : 0,
28-
/* Opt out of parallel tests on CI. */
29-
workers: process.env.CI ? 1 : undefined,
28+
/* Each CI shard has its own backend and runner: use all of its cores. */
29+
workers: process.env.CI ? '100%' : undefined,
3030
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
3131
/* On CI we use the blob reporter so the sharded jobs can be merged into a single HTML report afterwards. */
3232
reporter: process.env.CI ? [['blob'], ['github']] : 'html',
@@ -99,10 +99,20 @@ export default defineConfig({
9999
},
100100
],
101101

102-
/* Run your local dev server before starting the tests */
103-
webServer: {
104-
command: 'pnpm run dev',
105-
url: process.env.BASE_URL || 'http://localhost:3000',
106-
reuseExistingServer: true,
107-
},
102+
webServer: [
103+
/* Run your local dev server before starting the tests */
104+
{
105+
command: 'pnpm run dev',
106+
url: process.env.BASE_URL || 'http://localhost:3000',
107+
reuseExistingServer: true,
108+
},
109+
// Only when the server under test is configured to reach its third-party services there
110+
...(process.env.FAKE_SERVICES_URL
111+
? [{
112+
command: 'node tests/fake-services.ts',
113+
url: `${process.env.FAKE_SERVICES_URL}/matomo.js`,
114+
reuseExistingServer: true,
115+
}]
116+
: []),
117+
],
108118
})

0 commit comments

Comments
 (0)