mirror of
https://github.com/discourse/discourse.git
synced 2026-08-12 03:37:13 +08:00
This PR adds a `CAPYBARA_PLAYWRIGHT_SOFT_RESET` env flag which reuses the Playwright browser context between system specs so each example no longer pays the full bundle load cost. `capybara-playwright-driver` already keeps the browser process alive, but its reset closes the context after every example. That throws away the context-level HTTP cache and other browser work cached with the context, so the next example pays the bundle load cost again. Key changes: * Add `PlaywrightSoftReset` in `spec/support/system/capybara_patches.rb` so `Capybara::Playwright::Driver#reset!` clears storage, cookies, and permissions instead of closing the context. This keeps per-example isolation while leaving the browser caches warm. * Detect installed fake clocks by probing `setTimeout.toString()` on the fresh page during the soft reset. Playwright's clock is context-scoped and cannot be uninstalled, so any example that installed one (via `BrowserTime.freeze` or `pw_page.clock.install` directly) automatically gets a full context reset without having to declare anything. * Detect downloads by listening for the context's `download` event. Contexts that produced downloads fall back to a full reset so Playwright's own artifact lifecycle handles cleanup, avoiding races with reused contexts. * Clear `ExtraLocalesController`'s bundle digest cache in `TestSetup` whenever leftover translation overrides are detected (the same branch that already calls `I18n.reload!`). Overrides change locale bundle contents but specs that create them with `create!`/`update_columns` bypass the digest invalidation that `TranslationOverride.upsert!` performs, so a warm HTTP cache could serve stale bundles. A fresh digest changes the bundle URL, which bypasses every cache naturally. * Store downloads under a worker-local `Downloads::FOLDER` so per-worker cleanup cannot delete another parallel worker's artifacts. * Block service workers in system specs (`serviceWorkers: "block"`). Letting each example register a service worker that the soft reset then force-clears caused mass spec timeouts on CI. The exact cause requires further investigation, but system specs currently do not rely on service workers so blocking them is an acceptable trade-off. * Measure the impact on a 16-vCPU DO droplet over three rounds, where the core system suite dropped from ~843s to ~721s (~14.5%). The optimization is opt-in via `CAPYBARA_PLAYWRIGHT_SOFT_RESET=1`, which CI sets only for the core system and chat plugin system jobs while the optimization proves itself.
401 lines
16 KiB
YAML
Vendored
401 lines
16 KiB
YAML
Vendored
name: Tests
|
|
|
|
on:
|
|
pull_request:
|
|
paths-ignore:
|
|
- ".github/workflows/migration-tests.yml"
|
|
- ".github/dependabot.yml"
|
|
- ".github/labeler.yml"
|
|
- "migrations/**"
|
|
push:
|
|
branches:
|
|
- main
|
|
- beta
|
|
- stable
|
|
- release/*
|
|
|
|
concurrency:
|
|
group: tests-${{ format('{0}-{1}', github.head_ref || github.run_number, github.job) }}
|
|
cancel-in-progress: true
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
jobs:
|
|
build:
|
|
if: github.event_name == 'pull_request' || github.repository != 'discourse/discourse-private-mirror'
|
|
name: ${{ (matrix.target == 'core-plugins' && 'plugins (core)') || (matrix.target == 'official-plugins' && 'plugins (official)') || (matrix.target == 'chat' && 'plugins (chat)') || matrix.target }} ${{ matrix.build_type }}
|
|
runs-on: >-
|
|
${{
|
|
github.repository_owner == 'discourse' && (
|
|
matrix.build_type == 'system' && (matrix.target == 'core' || matrix.target == 'core-plugins' || matrix.target == 'official-plugins')
|
|
&& 'cdck-linux-16-core'
|
|
|| 'cdck-linux-8-core'
|
|
)
|
|
|| 'ubuntu-latest'
|
|
}}
|
|
container: discourse/discourse_test:release
|
|
timeout-minutes: >-
|
|
${{
|
|
matrix.build_type == 'system' && (matrix.target == 'core-plugins' || matrix.target == 'official-plugins' || matrix.target == 'core')
|
|
&& 30
|
|
|| 20
|
|
}}
|
|
|
|
defaults:
|
|
run:
|
|
shell: bash
|
|
|
|
env:
|
|
RAILS_ENV: test
|
|
PGUSER: discourse
|
|
PGPASSWORD: discourse
|
|
USES_PARALLEL_DATABASES: ${{ matrix.build_type == 'backend' || matrix.build_type == 'system' }}
|
|
CAPYBARA_DEFAULT_MAX_WAIT_TIME: 20
|
|
MINIO_RUNNER_LOG_LEVEL: WARN
|
|
DISCOURSE_TURBO_RSPEC_RETRY_AND_LOG_FLAKY_TESTS: ${{ (matrix.build_type == 'system' || matrix.build_type == 'backend') && '1' }}
|
|
CAPYBARA_PLAYWRIGHT_SOFT_RESET: ${{ matrix.build_type == 'system' && (matrix.target == 'core' || matrix.target == 'chat') && '1' }}
|
|
CHEAP_SOURCE_MAPS: "1"
|
|
DBUS_SESSION_BUS_ADDRESS: "unix:path=/dev/null"
|
|
MINIO_RUNNER_INSTALL_DIR: /home/discourse/.minio_runner
|
|
EMBER_ENV: development
|
|
LOAD_PLUGINS: ${{ (matrix.target == 'core-plugins' || matrix.target == 'official-plugins' || matrix.target == 'plugins' || matrix.target == 'chat') && '1' || '0' }}
|
|
QUNIT_REUSE_BUILD: 1
|
|
PLAYWRIGHT_BROWSERS_PATH: /home/discourse/.cache/ms-playwright
|
|
|
|
strategy:
|
|
fail-fast: false
|
|
|
|
matrix:
|
|
include:
|
|
- target: core
|
|
build_type: backend
|
|
- target: core
|
|
build_type: frontend
|
|
- target: core
|
|
build_type: system
|
|
- target: plugins
|
|
build_type: backend
|
|
- target: plugins
|
|
build_type: frontend
|
|
- target: core-plugins
|
|
build_type: system
|
|
- target: chat
|
|
build_type: system
|
|
- target: official-plugins
|
|
build_type: system
|
|
- target: themes
|
|
build_type: frontend
|
|
- target: themes
|
|
build_type: system
|
|
|
|
steps:
|
|
- name: Enable jemalloc
|
|
run: echo /usr/lib/libjemalloc.so.1 > /etc/ld.so.preload
|
|
|
|
- name: Verify jemalloc is loaded into Ruby
|
|
run: |
|
|
ruby -e 'abort "jemalloc not loaded" unless File.read("/proc/self/maps").include?("libjemalloc")'
|
|
|
|
- name: Set working directory owner
|
|
run: chown root:root .
|
|
|
|
- name: Set PARALLEL_TEST_PROCESSORS for system tests
|
|
if: matrix.build_type == 'system'
|
|
run: |
|
|
echo "PARALLEL_TEST_PROCESSORS=$(($(nproc) / 2 + 2))" >> $GITHUB_ENV
|
|
|
|
- name: Set QUNIT_PARALLEL for QUnit tests
|
|
if: matrix.build_type == 'frontend'
|
|
run: |
|
|
if [ "${{ matrix.target }}" = "themes" ]; then
|
|
echo "QUNIT_PARALLEL=2" >> $GITHUB_ENV
|
|
else
|
|
echo "QUNIT_PARALLEL=$(($(nproc) / 2))" >> $GITHUB_ENV
|
|
fi
|
|
|
|
- uses: actions/checkout@v7
|
|
with:
|
|
fetch-depth: 1
|
|
|
|
- name: Setup Git
|
|
run: |
|
|
git config --global user.email "ci@ci.invalid"
|
|
git config --global user.name "Discourse CI"
|
|
|
|
- name: Start redis
|
|
run: redis-server /etc/redis/redis.conf &
|
|
|
|
- name: Start Postgres
|
|
run: |
|
|
chown -R postgres /var/run/postgresql
|
|
sudo -E -u postgres script/start_test_db.rb
|
|
sudo -u postgres psql -c "CREATE ROLE $PGUSER LOGIN SUPERUSER PASSWORD '$PGPASSWORD';"
|
|
|
|
- name: Symlink vendor/bundle from image
|
|
run: |
|
|
ln -s /var/www/discourse/vendor/bundle vendor/bundle
|
|
|
|
- name: Setup gems
|
|
run: |
|
|
bundle config --local path vendor/bundle
|
|
bundle config --local deployment true
|
|
bundle config --local without development
|
|
bundle install --jobs $(($(nproc) - 1))
|
|
bundle clean
|
|
|
|
- name: pnpm install
|
|
run: pnpm install --frozen-lockfile
|
|
|
|
- name: Checkout official plugins
|
|
if: matrix.target == 'official-plugins' || matrix.target == 'plugins'
|
|
run: bin/rake plugin:install_all_official
|
|
|
|
- name: Symlinking plugin gems from image
|
|
if: matrix.target == 'plugins' || matrix.target == 'core-plugins' || matrix.target == 'official-plugins'
|
|
run: |
|
|
for dir in /var/www/discourse/plugins/*/gems; do
|
|
plugin_name=$(basename "$(dirname "$dir")")
|
|
plugin_dir="plugins/$plugin_name"
|
|
gem_dir="$plugin_dir/gems"
|
|
|
|
if [ ! -d "$plugin_dir" ]; then
|
|
echo "Skipping $plugin_name: Plugin directory does not exist"
|
|
elif [ -d "$gem_dir" ]; then
|
|
echo "Skipping $plugin_name: Source gems directory exists"
|
|
else
|
|
echo "Symlinking $dir to $gem_dir"
|
|
ln -s "$dir" "$gem_dir"
|
|
fi
|
|
done
|
|
|
|
- name: Pull compatible versions of plugins
|
|
if: (matrix.target == 'official-plugins' || matrix.target == 'plugins') && (github.ref_name != 'main' && github.base_ref != 'main')
|
|
run: LOAD_PLUGINS=0 bin/rake plugin:pull_compatible_all
|
|
|
|
- name: Checkout official themes
|
|
if: matrix.target == 'themes'
|
|
run: bin/rake themes:clone_all_official themes:pull_compatible_all
|
|
|
|
- name: Add hosts to /etc/hosts, otherwise Chrome cannot reach minio
|
|
if: matrix.build_type == 'system' && matrix.target == 'core'
|
|
run: |
|
|
echo "127.0.0.1 minio.local" | sudo tee -a /etc/hosts
|
|
echo "127.0.0.1 discoursetest.minio.local" | sudo tee -a /etc/hosts
|
|
|
|
- name: Create and migrate databases
|
|
run: |
|
|
if [ "$USES_PARALLEL_DATABASES" = "true" ]; then
|
|
parallel --halt now,fail=1 --line-buffer ::: \
|
|
'bin/rake db:create db:migrate' \
|
|
'bin/rake parallel:create parallel:migrate'
|
|
else
|
|
bin/rake db:create db:migrate
|
|
fi
|
|
|
|
- name: Fetch turbo_rspec_runtime.log cache
|
|
uses: actions/cache@v6
|
|
id: test-runtime-cache
|
|
if: matrix.build_type == 'backend' || matrix.build_type == 'system'
|
|
with:
|
|
path: tmp/turbo_rspec_runtime.log
|
|
key: rspec-runtime-${{ matrix.build_type }}-${{ matrix.target }}-${{ github.run_id }}
|
|
restore-keys: rspec-runtime-${{ matrix.build_type }}-${{ matrix.target }}-
|
|
|
|
- name: Check Annotations
|
|
if: matrix.build_type == 'backend' && (matrix.target == 'core' || matrix.target == 'plugins')
|
|
env:
|
|
LOAD_PLUGINS: ""
|
|
run: |
|
|
directories=""
|
|
if [ "${{ matrix.target }}" = "core" ]; then
|
|
directories="app/models"
|
|
else
|
|
directories="$(script/list_bundled_plugins | xargs -I{} find {} -type d -path '*/app/models' -maxdepth 3)"
|
|
fi
|
|
|
|
# annotate:clean runs the seed + annotaterb in a temporary postgres so it
|
|
# doesn't pollute the rspec test DB.
|
|
MODEL_DIR="$(echo "${directories}" | tr '\n' ',')" bin/rake annotate:clean
|
|
|
|
if [ ! -z "$(git status --porcelain ${directories})" ]; then
|
|
echo "Core annotations are not up to date. To resolve, run:"
|
|
echo " bin/rake annotate:clean"
|
|
echo
|
|
echo "Or manually apply the diff printed below:"
|
|
echo "---------------------------------------------"
|
|
git -c color.ui=always diff ${directories}
|
|
exit 1
|
|
fi
|
|
|
|
- name: Check structure dump
|
|
if: matrix.build_type == 'backend' && matrix.target == 'core'
|
|
run: bin/rake db:check_structure_dump
|
|
|
|
- name: Check Zeitwerk reloading
|
|
if: matrix.build_type == 'backend'
|
|
run: |
|
|
if ! bin/rails runner 'Rails.application.reloader.reload!'; then
|
|
echo
|
|
echo "---------------------------------------------"
|
|
echo
|
|
echo "::error::Zeitwerk reload failed - the app will not be able to reload properly in development."
|
|
echo "To reproduce locally, run \`bin/rails runner 'Rails.application.reloader.reload!'\`."
|
|
echo
|
|
exit 1
|
|
fi
|
|
|
|
- name: Check SKIP_DB_AND_REDIS bootability
|
|
if: matrix.build_type == 'backend'
|
|
run: |
|
|
if ! (SKIP_DB_AND_REDIS=1 RAILS_DB="nonexistent" bin/rails runner "puts 'booted successfully'"); then
|
|
echo
|
|
echo "---------------------------------------------"
|
|
echo
|
|
echo "::error::SKIP_DB_AND_REDIS boot failed. Make sure the database is not being accessed during the Rails boot process."
|
|
echo "To reproduce locally, run \`SKIP_DB_AND_REDIS=1 RAILS_DB='nonexistent' bin/rails runner \"puts 'booted successfully'\"\`."
|
|
echo
|
|
exit 1
|
|
fi
|
|
|
|
- name: Core RSpec
|
|
if: matrix.build_type == 'backend' && matrix.target == 'core'
|
|
run: bin/turbo_rspec --use-runtime-info --format=progress
|
|
|
|
- name: Ember Build
|
|
if: matrix.build_type == 'system' || matrix.build_type == 'frontend'
|
|
run: script/assemble_ember_build.rb
|
|
|
|
- name: Prepare Chrome environment for QUnit
|
|
if: matrix.build_type == 'frontend'
|
|
run: |
|
|
mkdir -p "$HOME/.pki/nssdb" "$HOME/.local/share/pki/nssdb"
|
|
dbus-daemon --system --fork 2>/dev/null || true
|
|
bus_addr=$(dbus-daemon --session --fork --print-address 2>/dev/null)
|
|
echo "DBUS_SESSION_BUS_ADDRESS=$bus_addr" >> "$GITHUB_ENV"
|
|
|
|
- name: Core QUnit
|
|
if: matrix.build_type == 'frontend' && matrix.target == 'core'
|
|
run: QUNIT_WRITE_EXECUTION_FILE=1 bin/rake qunit:test
|
|
|
|
- name: Asset Processor Tests
|
|
if: matrix.build_type == 'frontend' && matrix.target == 'core'
|
|
working-directory: frontend/asset-processor
|
|
run: pnpm test
|
|
|
|
- name: Plugins RSpec
|
|
if: matrix.build_type == 'backend' && matrix.target == 'plugins'
|
|
run: |
|
|
bin/rake plugin:turbo_spec['*','--format=progress --use-runtime-info']
|
|
|
|
- name: Plugins QUnit
|
|
if: matrix.build_type == 'frontend' && matrix.target == 'plugins'
|
|
run: bin/rake 'plugin:qunit[*]'
|
|
|
|
- name: Theme QUnit
|
|
if: matrix.build_type == 'frontend' && matrix.target == 'themes'
|
|
run: DISCOURSE_DEV_DB=discourse_test bin/rake themes:qunit_all_official
|
|
|
|
- uses: actions/upload-artifact@v7
|
|
if: always() && matrix.build_type == 'frontend' && matrix.target == 'core'
|
|
with:
|
|
name: ember-exam-execution-${{ matrix.target }}-${{ matrix.browser }}-frontend-${{ hashFiles('./frontend/discourse/test-execution-*.json') }}
|
|
path: ./frontend/discourse/test-execution-*.json
|
|
|
|
- name: Core System Tests
|
|
if: matrix.build_type == 'system' && matrix.target == 'core'
|
|
env:
|
|
CHECKOUT_TIMEOUT: 10
|
|
run: RAILS_ENABLE_TEST_LOG=1 RAILS_TEST_LOG_LEVEL=error bin/turbo_rspec --use-runtime-info --format documentation spec/system
|
|
|
|
- name: Core Plugins System Tests
|
|
if: matrix.build_type == 'system' && matrix.target == 'core-plugins'
|
|
env:
|
|
CHECKOUT_TIMEOUT: 10
|
|
run: |
|
|
RAILS_ENABLE_TEST_LOG=1 RAILS_TEST_LOG_LEVEL=error bin/turbo_rspec --exclude-pattern="plugins/chat/*" --use-runtime-info --format documentation plugins/*/spec/system
|
|
|
|
- name: Official Plugins System Tests
|
|
if: matrix.build_type == 'system' && matrix.target == 'official-plugins'
|
|
env:
|
|
CHECKOUT_TIMEOUT: 10
|
|
run: |
|
|
RAILS_ENABLE_TEST_LOG=1 RAILS_TEST_LOG_LEVEL=error bin/turbo_rspec --exclude-pattern="$(script/list_bundled_plugins '/*' | paste -sd, -)" --use-runtime-info --format documentation plugins/*/spec/system
|
|
|
|
- name: Chat System Tests
|
|
if: matrix.build_type == 'system' && matrix.target == 'chat'
|
|
env:
|
|
CHECKOUT_TIMEOUT: 10
|
|
run: RAILS_ENABLE_TEST_LOG=1 RAILS_TEST_LOG_LEVEL=error bin/turbo_rspec --use-runtime-info --format documentation plugins/chat/spec/system
|
|
|
|
- name: Theme System Tests
|
|
if: matrix.build_type == 'system' && matrix.target == 'themes'
|
|
env:
|
|
CHECKOUT_TIMEOUT: 10
|
|
run: RAILS_ENABLE_TEST_LOG=1 RAILS_TEST_LOG_LEVEL=error bin/turbo_rspec --format documentation tmp/themes/*/spec/system themes/*/spec/system
|
|
|
|
- name: Upload failed system test artifacts
|
|
uses: actions/upload-artifact@v7
|
|
if: always() && matrix.build_type == 'system'
|
|
with:
|
|
name: failed-system-test-artifacts-${{ matrix.build_type }}-${{ matrix.target }}
|
|
if-no-files-found: ignore
|
|
path: |
|
|
tmp/capybara/*.png
|
|
tmp/capybara/*.zip
|
|
tmp/capybara/*.webm
|
|
|
|
- name: Check for flaky tests report
|
|
id: check-flaky-spec-report
|
|
if: github.repository == 'discourse/discourse' && github.ref_name == 'main' && env.DISCOURSE_TURBO_RSPEC_RETRY_AND_LOG_FLAKY_TESTS == '1'
|
|
run: |
|
|
if [ -f tmp/turbo_rspec_flaky_tests.json ]; then
|
|
echo "exists=true" >> $GITHUB_OUTPUT
|
|
else
|
|
echo "exists=false" >> $GITHUB_OUTPUT
|
|
fi
|
|
|
|
- name: Create flaky tests report artifact
|
|
if: steps.check-flaky-spec-report.outputs.exists == 'true'
|
|
run: cp tmp/turbo_rspec_flaky_tests.json tmp/turbo_rspec_flaky_tests-${{ matrix.build_type }}-${{ matrix.target }}-${{ job.check_run_id }}.json
|
|
|
|
- name: Upload flaky tests report
|
|
uses: actions/upload-artifact@v7
|
|
if: steps.check-flaky-spec-report.outputs.exists == 'true'
|
|
with:
|
|
name: flaky-test-reports-${{ matrix.build_type }}-${{ matrix.target }}
|
|
path: tmp/turbo_rspec_flaky_tests-${{ matrix.build_type }}-${{ matrix.target }}-${{ job.check_run_id }}.json
|
|
|
|
merge:
|
|
if: github.repository == 'discourse/discourse' && github.ref == 'refs/heads/main'
|
|
runs-on: ubuntu-latest
|
|
needs: build
|
|
steps:
|
|
- name: Check for artifacts to merge
|
|
id: check-artifacts
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
run: |
|
|
names=$(gh api "repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts?per_page=100" --jq '.artifacts[].name')
|
|
if echo "$names" | grep -q '^failed-system-test-artifacts-'; then
|
|
echo "has_failed_system=true" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
if echo "$names" | grep -q '^flaky-test-reports-'; then
|
|
echo "has_flaky=true" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
|
|
- name: Merge failed system test artifacts
|
|
if: steps.check-artifacts.outputs.has_failed_system == 'true'
|
|
uses: actions/upload-artifact/merge@v7
|
|
with:
|
|
name: failed-system-test-artifacts
|
|
pattern: failed-system-test-artifacts-*
|
|
delete-merged: true
|
|
|
|
- name: Merge flaky test reports
|
|
if: steps.check-artifacts.outputs.has_flaky == 'true'
|
|
uses: actions/upload-artifact/merge@v7
|
|
with:
|
|
name: flaky-test-reports
|
|
pattern: flaky-test-reports-*
|
|
delete-merged: true
|