[3.0][Testing] Run the integration tests in CI, with or without Docker - #9659
Open
albertlast wants to merge 8 commits into
Open
[3.0][Testing] Run the integration tests in CI, with or without Docker#9659albertlast wants to merge 8 commits into
albertlast wants to merge 8 commits into
Conversation
The integration suite reaches the database, but not a page. Everything
between a request arriving and HTML coming back - the session, the
cookies, the theme, the templates, the permission checks - had no
automated coverage at all, and that is where the failures people actually
report live.
Requests have to be real ones: obExit(), redirectexit() and fatal*() all
end in exit, and Db::$db, ActionTrait::$obj and Theme::$loaded cannot be
reset, so a test process can carry out one request in itself and no more.
tests/Support/HttpClient.php is a small browser built on the curl
extension the forum already requires, so this costs no new dependency.
Three files to start: a sweep of the pages a guest can reach, the login
journey, and starting a topic and replying to it. Every one of them ends
in assertNoErrorsLogged(), which is the point - SMF records most of what
goes wrong in log_errors rather than showing it, so a page can return a
flawless 200 while logging an undefined index on every hit.
Four things about SMF made these harder to write than expected, and each
is commented where it bites rather than worked around silently:
- The first request of a new session regenerates it, so a security
token minted on the very first page a visitor sees can never be
validated. It looks like a broken token, not a replaced session.
- Only the button that was clicked gets submitted. The posting form
offers "preview" and "post"; sending both means preview wins and the
post is never made, with an ordinary 200 to show for it.
- Security::spamProtection() allows one login or post every two seconds
per IP, and tests are much faster than people, so submitForm() waits
it out once instead of failing at random.
- curl only writes cookies with an expiry to its jar file, so a handle
opened per request loses the session every time.
HTTP tests cannot be wrapped in a transaction - the request runs in the
web server's process on its own connection, and on MySQL's REPEATABLE
READ an open transaction here would never see what it wrote, quietly
making assertNoErrorsLogged() incapable of failing. IntegrationTestCase
gains usesTransaction() so they can opt out, and PostingTest removes what
it creates through Topic::remove().
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: albertlast <mathiaspapealbert@hotmail.com>
The suite ships the machinery but not the instructions for using it, and three things are not discoverable from reading it: which of the three suites a new test belongs in, who the request is actually made as, and where the endpoint and field names come from. The second is the one that misleads. HttpTestCase inherits actingAs() from IntegrationTestCase, where it repoints User::$me in the PHPUnit process - but the request is handled by Apache in another process, which knows only the cookie. Calling it in an HTTP test changes nothing and leaves the assertions describing a guest, confidently. The identity of a request here is the cookie jar and nothing else. Field names are the opposite problem: they look like something to look up, and are not. submit() scrapes the form the way a browser does, which is what carries the session check and the security token - both named differently for every session, so a hand built POST body gets a 403 it cannot fix. The worked example prints them to make that concrete. Also notes that install.php left in the board root puts an errorbox on every page an administrator sees, which fails assertLooksLikeAForumPage() and crowds out whatever the test was looking at. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: albertlast <mathiaspapealbert@hotmail.com>
The tests skip rather than fail when they cannot sign in, which says what is wrong but not what to do about it. user.sh answers both halves: check says whether the password the suite is using is the right one, and reset puts a forum installed some other way back on the credentials it expects. Signed-off-by: albertlast <mathiaspapealbert@hotmail.com>
The SMF/section_comments fixer puts the Public methods banner at the top of the group, and the DataProvider attribute belongs to the method under it, not above the banner. That one file was all the style check was failing on. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: albertlast <mathiaspapealbert@hotmail.com>
The scripts that build a forum and run the integration suite against it all drove `docker compose exec`, so the suite was out of reach for anyone not running the stack. The unit job matrixes windows-latest deliberately, because people develop SMF on Windows; the integration suite excluded exactly those people, and CI could not run it either. SMF_RUNNER now picks where the work happens, and defaults to local: the php on this machine and a database it can already reach. --docker, or SMF_RUNNER=docker in .env, asks for the stack instead, and nothing about that path changes. The tooling moves to .dev/, since neither runner is the second-class one now. The name keeps its leading dot on purpose: check-smf-index.php wants an index.php stub in every directory but the dot ones, other/ and vendor/, and the repository root is the forum's webroot, so a plain tools/ would be served by a real install. .dev/db.php is how the local runner reaches a database. Under Docker the mysql and psql clients are already in the container; locally they would be a dependency nothing else here needs, while mysqli and pgsql are ones SMF has anyway. So any machine that can serve the forum can run these scripts. Locally the forum is served by PHP's own server, which is enough for the tests: SMF routes on the query string and on PATH_INFO, and there is no .htaccess at the root, so nothing here wants mod_rewrite. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: albertlast <mathiaspapealbert@hotmail.com>
The suite skips itself when there is no forum to talk to, so that `composer test` stays useful on a machine with nothing installed. In CI that same kindness is a lie: a job wired to the wrong database reports a green run having tested nothing. PHPUnit has --fail-on-skipped for exactly this, but it does not reach a skip raised in setUpBeforeClass(): that marks the class as skipped rather than its tests, and only skipped tests count towards the exit code. The check moves to setUp(), where it does count. Installation memoises its answer, so asking once per test costs a function call. tearDown() now checks that there is a connection before rolling back. PHPUnit runs it even for a test setUp() skipped, and without the guard a run that should read as "43 skipped, no forum" reads as 18 errors instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: albertlast <mathiaspapealbert@hotmail.com>
The 43 tests in tests/Integration/ have never run in CI. The PHPUnit job is a runner with no database and no Settings.php, so every one of them skipped: 20 of them on the pull request that added the suite, 43 on the one that added the HTTP tests. A green check there proved the unit suite passed and nothing else. A second job installs a forum on the runner and runs them, on MySQL and on PostgreSQL. Both, because the two disagree: the counter regression in ModSettingsTest passes on MySQL with the bug still in place and fails only on PostgreSQL, so one engine would have proved nothing. It uses service containers and the same .dev scripts a person would, rather than a second install path that could drift from the one people actually run. --fail-on-skipped is the point of the job. Both the missing-forum skip and the wrong-password one exist so the suite stays usable on a machine with no forum; in CI they would turn a job that tested nothing into a green tick. The unit job narrows to the unit suite, so it reports what it ran instead of counting those skips as part of a passing run. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: albertlast <mathiaspapealbert@hotmail.com>
It sources lib.sh and refers to the directory the scripts live in, both of which the move took with it, so leaving it behind breaks it. Unlike the rest, these four scripts and schema-tool.php cannot mean anything but the compose stack: they drive upgrade.php inside the web container, kill it part way through, and read databases the stack owns. So they set SMF_RUNNER=docker for themselves and call require_docker, and nobody running them has to remember a flag. They pass --docker on to the reset.sh and install-forum.sh they invoke, since a shell variable does not cross into a child process. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: albertlast <mathiaspapealbert@hotmail.com>
albertlast
force-pushed
the
tests/ci-integration
branch
from
September 7, 2026 06:29
5b7bc9a to
948a2a2
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
The suites from #9345 and #9347 have never run in CI. The PHPUnit job is a runner
with no database and no
Settings.php, soIntegrationTestCaseskips all of them:A green check on either proves the unit suite passed and nothing else.
Underneath that is a second problem. Everything that can produce a forum
(
install-forum.sh,reset.sh,test.sh) drovedocker compose exec, so theintegration suite was unreachable for anyone not running the stack. The unit job
matrixes
windows-latestdeliberately, because people develop SMF on Windows;the integration suite excluded exactly those people, and CI could not run it
either.
So this does both: the tooling learns to work without Docker, and CI uses that
same path.
Local is the default
SMF_RUNNERpicks where the work happens and defaults tolocal— the PHP onthis machine and a database it can already reach.
--docker, orSMF_RUNNER=dockerin.env, asks for the stack, and nothing about that pathchanges.
Local is the default because it is the one that needs nothing installed beyond
what SMF itself requires, and the only one available on a machine without Docker.
This does change what an existing command means, so three things guard it:
require_local_depsnames--dockerin its failure message, the README leadswith it, and the failure when there is no local database is loud rather than
silent.
Why
.dev/and nottools/The tooling moves out of
.docker/, since neither runner is the second-class onenow. The leading dot is deliberate rather than cosmetic:
check-smf-index.php— whichphp.ymlruns on every PR — requires an exactindex.phpstub in every directory except the dot ones,./otherand./vendor. The repository root is the forum's webroot, so a plaintools/wouldbe served by a real install and would need a stub in every subdirectory. It is
the same reason
.docker/and.github/are spelled the way they are..docker/keeps what builds and configures containers:php/,mysql/,postgres/,env.example.compose.yamlis untouched..dev/db.phpThe two runners reach a database differently. Under Docker the
mysqlandpsqlclients are already inside the container; locally they would be a dependency
nothing else here needs, while
mysqliandpgsqlare ones SMF has anyway. Sothe local runner goes through PHP, and any machine that can serve the forum can
run these scripts with no database client installed.
The web server
Locally the forum is served by PHP's own built-in server. That is enough for
these tests: SMF routes on the query string and on
PATH_INFO(
Sources/QueryString.php:153), there is no.htaccessat the root, andSapi::isSoftware()returns false for an unrecognisedSERVER_SOFTWAREratherthan misbehaving. It is not Apache, though, so the compose stack stays the closer
thing to production and the README says so.
--fail-on-skipped, and whyIntegrationTestCasechangesThe skip is a convenience locally and a lie in CI: a job wired to the wrong
database would report a green run having tested nothing. PHPUnit has
--fail-on-skippedfor exactly this.It does not reach a skip raised in
setUpBeforeClass(). That marks the classas skipped rather than its tests, and only skipped tests count towards the exit
code — so with the check where #9345 put it, the flag was inert in the one case
that matters most. Measured, on a tree with no
Settings.php:So the check moves to
setUp().Installationmemoises its answer, so askingonce per test costs a function call.
tearDown()gains anisset(Db::$db)guardto go with it: PHPUnit runs it even for a test
setUp()skipped, and without ita run that should read as "43 skipped, no forum" reads as 18 errors instead.
The workflow
A second job installs a forum on the runner and runs the suite, on MySQL and on
PostgreSQL. Both, for the reason #9345 gives: the counter regression in
ModSettingsTestpasses on MySQL with the bug still in place and fails only onPostgreSQL, so one engine would have proved nothing. It uses service containers
and the same
.devscripts a person would, rather than a second install paththat could drift from the one people actually run.
It is
ubuntu-latestonly —windows-latestrunners cannot run Linuxcontainers. Windows coverage stays with the unit job, and the local runner is
what lets a Windows developer run these by hand.
The unit job narrows to
composer test-unit, so it reports what it ran insteadof counting 20-43 skips as part of a passing run.
Verified
Every command was run. Two of the three defects below are ones the verification
found in this branch's own code, which seemed worth saying rather than quietly
fixing:
db_emptywas a silent no-op on MySQL.GROUP_CONCATtruncates atgroup_concat_max_len, 1024 bytes by default, which SMF's 72 tables passcomfortably. The reset dropped nothing and reported success. The names are now
collected first and the
DROPbuilt from them.db.phpswallowed errors after the first statement. Foldingnext_result()into a loop condition means a batch that failed halfway exits0 — which is how the bug above stayed hidden.
--fail-on-skippedwas inert, as above.Settings.php, with--fail-on-skippedSettings.php, without itSMF_ADMIN_PASS, with--fail-on-skippedcomposer test-unitcomposer testAlso: a full local-mode install from scratch on both engines (
reset.sh→db_empty→stage_installer→ both installer passes), the same through--docker,user.sh checkanduse-engine.shboth ways, and.dev/ci.shgreen — including
check-signed-off.php,check-smf-license.php,check-smf-languages.php,check-smf-index.phpandcheck-version.php, whichis what confirms the
.dev/naming above.shellcheckis clean on all sevenscripts, at the same level the originals were.
And the workflow itself, on this pull request:
0 skipped on both, which is the number that matters — the whole point is that
a green run here can no longer mean an empty one. The logs show each job
installing a forum, serving it and running the suite against it.
Not verified on Windows yet
The local runner exists so that a developer on Windows can run this suite, and it
has been exercised on Linux only. Nothing in it is platform-specific by design —
the database goes through
mysqli/pgsqlrather than a CLI client precisely sothat Windows needs nothing extra, and
serve_startprobes through PHP ratherthan
curlfor the same reason — but that is an argument, not a test. Happy tohold this until someone has run
.dev/test.sh --engine mysqlon Windows.Merge order
Merge #9345 and #9347 before this one, and the PRs they name before that.
This branch contains the whole chain, so the diff shown here is mostly theirs;
once they land and this is rebased on
release-3.0, what is left is.dev/,.github/workflows/phpunit.ymland theIntegrationTestCasechange.The upgrade tooling that landed while this was being written
(
compare-upgrade.sh,interrupt-upgrade.sh,rerun-upgrade.sh,upgrade-readings.sh,schema-tool.php) moves to.dev/with the rest, in itsown commit: it sources
lib.shand uses$DEV_DIR, so leaving it behind wouldbreak it. Those five pin
SMF_RUNNER=dockerfor themselves and pass--dockeron to the scripts they invoke, since orchestrating containers is all they can
mean and a shell variable does not cross into a child process.
One thing still to do: #9330 and #9657 also touch
.docker/, so both want arebase on top of this. Only
.docker/README.mdgenuinely collides — #9330's.docker/baseline/subtree has its own self-containedlib.shand does notsource the top-level one.
Issues References (Fixes|Related|Closes)
IntegrationTestCase.🤖 Generated with Claude Code