[3.0][Testing] Cover last week's fixes that the unit suite can reach - #9511
Open
albertlast wants to merge 19 commits into
Open
[3.0][Testing] Cover last week's fixes that the unit suite can reach#9511albertlast wants to merge 19 commits into
albertlast wants to merge 19 commits into
Conversation
ActionTrait declares $obj as a static property, and a static property is
shared with every descendant class that does not redeclare it. None of
the eleven action classes that extend another action redeclare it, so
they all share one slot with their parent.
Once the parent has been loaded, load() finds that slot occupied and
returns the parent's instance, which does not satisfy the "static"
return type:
SMF\Actions\Login2::load(): Return value must be of type
SMF\Actions\Logout, SMF\Actions\Login2 returned
This is reachable during login: User::enforceBans() calls Logout::call()
to kick a banned member, by which point Login2 has already been loaded,
so a banned member gets a fatal error instead of being logged out.
Checks that the cached instance is of the class being loaded, rather than
merely present.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
SMF has no automated tests. CI proves that the code parses and that it is formatted; it never executes anything. Every one of the bugs fixed in SimpleMachines#9319 through SimpleMachines#9322 parsed cleanly and passed every check. Quite a lot of 3.0 is reachable without a forum behind it. The bootstrap here defines the constants index.php would define and points the autoloader at Sources/, and that is enough: no Settings.php, no database, no request. Anything that reaches Config::$modSettings, User::$me or Db::$db is out of scope and belongs in an integration suite. The first tests cover ground that recently broke: - ActionTrait::load() returning an instance of the class it was called on, in both orders and in two separate class hierarchies. - CreatePost_Notify::getTimeOffset(), including the half-hour and quarter-hour zones that an int cast used to truncate. - Utils::buildRegex(), including the trailing quoted character from SimpleMachines#9318. tests/ is already excluded from the license header check in BuildTools, and the directory index.php files keep check-smf-index happy. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds a PHPUnit workflow across the same 8.4 and 8.5 matrix the syntax check already uses, and a composer test script. Two adjustments fall out of running the suite next to the existing checks. The PHPUnit cache lives in .phpunit.cache rather than under cache/, because check-smf-index walks every directory that is not hidden and would otherwise report a missing index file the moment anyone runs the tests locally. And AGENTS.md no longer says there is no test suite; it now says what the suite does and does not cover, so an agent does not mistake a green run for proof that a change works. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The first pass covered only the three things that had recently broken. Quite a lot more is reachable without a database once the bootstrap sets the paths and default language that the Unicode and entity helpers use to find their data files, which is six lines and still reads nothing from Settings.php. Adds coverage for Utils' entity-aware string handling and Unicode case conversion, IP, Url, Uuid, Sapi, Security's password hashing, Punycode and TimeInterval. 99 tests, 144 assertions, on 8.4 and 8.5. Two behaviours are deliberately described rather than asserted, because pinning them down would preserve something that looks wrong: - Sapi::memoryReturnBytes() strips the last character before parsing, so a unit-less value such as '128' reads as 12 and the '-1' that means "no limit" reads as 0. Only suffixed values are asserted. - Url::isScheme() compares the scheme without normalising case, so an uppercase scheme fails to match its own name. Only exact-case matching is asserted. IP's constructor accepts the packed binary form, which it cannot tell apart from any other 4 or 16 byte string, so 'nope' becomes 110.111.112.101. That one is genuine ambiguity rather than a defect, so it is pinned down as a test in its own right. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
memoryReturnBytes() removed the last character of the value before
parsing the number, on the assumption that it is always a designator.
PHP's shorthand notation is optional, so a plain byte count loses its
last digit: '128' reads as 12, and '2097152' reads as 209715.
Graphics\Image does exactly that, passing a computed byte count with no
designator, so resizing an image asks for a tenth of the memory it just
worked out that it needs.
The other value with no designator is '-1', which means there is no
limit. It read as 0, because intval('-') is 0, so setMemoryLimit() found
the current limit to be smaller than anything and set one. On a server
with no memory limit, asking for 128M capped it at 128M.
Only strips the last character when it is one of the designators PHP
accepts, and reports "no limit" as PHP_INT_MAX so that the callers
comparing it against an amount they need do not each have to special
case it.
The dead is_integer() check went with it; the parameter is typed string.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
RFC 3986, section 3.1, makes scheme names case insensitive, and this class keeps the scheme exactly as it was written rather than normalizing it. isScheme() compared the two with in_array(), so a URL written with an uppercase scheme did not match its own name. That reaches two callers. isWebsite() stops recognising HTTP:// and HTTPS:// as websites, and the avatar handling in User treats a DATA: URI as though it were a remote address. Folds both sides before comparing, and makes the comparison strict while it is there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The memoryReturnBytes() and isScheme() cases were described in comments rather than asserted, because pinning down the behaviour would have preserved it. Now that both are fixed, they become tests. Verified to fail against the unfixed code: reverting the two source files alone fails exactly these six tests and nothing else. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CoversClass on a trait is not a valid coverage target, and PHPUnit only says so when coverage is actually collected. The suite passed on its own and failed all five ActionTrait cases the moment anyone ran it with --coverage. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: albertlast <mathiaspapealbert@hotmail.com> # Conflicts: # AGENTS.md
The testing notes described the suite mainly as a limitation, which left agents with no way to tell whether the code in front of them was reachable from it. Sets out the expectation that a reachable change carries a test, and lists the cases that work with the examples already in tests/Unit/: pure helpers, value objects, class-level behaviour, protected helpers through reflection, and modSettings keys the test sets itself. Also names the strict-mode traps and the two ways the style fixer rearranges a test file. Corrects the CI claim as well; phpunit.yml only runs on pull requests and on pushes to release-3.0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: albertlast <mathiaspapealbert@hotmail.com>
Signed-off-by: albertlast <mathiaspapealbert@hotmail.com>
Three of the bug fixes merged into release-3.0 over the past week touch code that needs no database, so they can be pinned down here rather than only in a running forum. SimpleMachines#9506 stopped SMF\IP::ip2range() validating the two ends of a range as IPv4 only, which meant an IPv6 range was not recognised as a range at all and came back as 255.255.255.255. SimpleMachines#9507 made SMF\Url::proxied() strip the brackets off an IPv6 host before asking filter_var() about it. Without that, every literal IPv6 host read as a name rather than an address, so the private and reserved ranges were not excluded and the proxy would fetch them. SimpleMachines#9499 stopped SMF\TimeInterval holding a \DateInterval of its own and started being one, which is what \DateTime::add() and ::sub() read. The two cases here that asserted the old canonical string form are updated: a duration naming no years or months no longer carries a zero day count through stringification. Also covers SMF\IP::matchToCIDR(), which SimpleMachines#9507 restructured without meaning to change it. Those cases pass either side of that commit, which is the point. Three notes on what is deliberately absent. TimeInterval::localize() goes through Lang::getTxt(), which loads a language file and so wants Theme and Db::$db. MarkdownParser, for SimpleMachines#9509, builds its parent, which calls User::setMe(). Neither is reachable from this bootstrap. The third is a defect rather than a boundary. matchToCIDR() builds its IPv6 mask with str_repeat('f', (int) $cidr_subnetmask / 4), where the cast binds to the subnet mask rather than to the division, so any prefix length that is not a multiple of four hands str_repeat() a float and throws a TypeError. The switch immediately below it, which exists to add the odd nibble for exactly those prefixes, has therefore never run. The data provider says so in a comment and stays on multiples of four: asserting the TypeError would only preserve it. Signed-off-by: Mathias Alberts <mathiaspapealbert@hotmail.com> Signed-off-by: albertlast <mathiaspapealbert@hotmail.com>
Three files are out of step with the SMF/section_comments fixer: Alert.php is missing the blank line before a banner, and Autolinker.php and Help.php each carry an extra one. Nothing has been checking. The workflow asks tj-actions/changed-files what the pull request touched and, unless that list contains .php-cs-fixer.dist.php or composer.lock, passes it to the fixer as --path-mode=intersection. A pull request that touches neither of those and does not touch these three files never looks at them, and that has been every pull request for some time. Found by a branch that does change composer.lock, which is what turns the check back into a whole-tree scan. Signed-off-by: Mathias Alberts <mathiaspapealbert@hotmail.com> Signed-off-by: albertlast <mathiaspapealbert@hotmail.com>
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.
Note
This change was produced by an LLM. The tests, the commit message and this
description were all written by Claude (Anthropic), driven by @albertlast. It has
not yet had human code review.
The suite was run rather than only reasoned about, and every test below was also
run against the unfixed code it covers. Please review it as untrusted work.
Description
This is the first instalment of what #9326 was for: a sweep of the bug fixes merged
into
release-3.0over the past week, asking of each one whether the unit suite canreach it, and writing a test where it can.
Twenty-five or so fixes landed in that window. Most of them are templates, JavaScript,
or SQL, and most of the PHP ones need
Db::$dborUser::$me. Three do not.IP::ip2range()on IPv6 ranges, written both elided and in full, plus IPv4 and the wildcard forms as the controlUrl::proxied()refusing a private, loopback or documentation IPv6 host written in brackets, and still proxying a global oneTimeIntervalbeing a\DateIntervalrather than holding one:\DateTime::add()and::sub(), the%afallback, fractional seconds, and the string formIP::matchToCIDR()is covered too. #9507 restructured it without meaning to change it,and those cases pass either side of that commit — which is the point of having them.
140 tests, 206 assertions, still well under a second.
These are regression tests, not tests written to fit
Each set was run against the code as it was before its fix, by checking out the single
file at the commit before the merge.
Sources/IP.phpbefore [3.0] Makes SMF\IP::ip2range() work correctly with IPv6 addresses #9506: both IPv6 range cases fail, and the failure is worthreading.
2001:db8::1-2001:db8::ffcame back as255.255.255.255for the low end,because neither end validated as IPv4, so the range was not recognised as a range and
the address was walked as though it were a fragment. A ban on an IPv6 range was
landing somewhere else entirely.
Sources/Url.phpbefore [3.0] Improves handling of IPv6 hosts in SMF\IP and SMF\Url #9507: the three private IPv6 cases fail, each by producing aproxy.phpURL.http://[::1]/pic.pngwas being handed to the proxy, which is theforum fetching its own loopback on behalf of whoever pasted the address. The IPv4
cases pass either side, as they should.
Sources/TimeInterval.phpbefore [3.0] Restores full compatibility of SMF\TimeInterval with \DateInterval #9499: the two date-arithmetic cases do not fail somuch as stop, with
DateObjectError: Object of type SMF\TimeInterval (inheriting DateInterval) has not been correctly initialized by calling parent::__construct().Four more fail on the string form.
Two existing tests changed
testTimeOnlyDurationsKeepTheirTimeDesignatorandtestStringifyingIsStableAcrossARoundTripasserted
P0DT30M. #9499 removed the behaviour behind that — the class no longerpopulates
daysfor a duration naming no years or months, and no longer writes a zeroday count out — so both now assert
PT30M, which is the shorter and more conventionalform. A third case,
testAZeroUnitIsDroppedOnTheWayBackOut, keepsP0DT30Mas inputso the normalisation itself stays watched.
This is the suite doing its job on the first change to reach it: #9499 was a deliberate
behaviour change, and the two failures said so.
What is deliberately not here
Two of the week's fixes looked reachable and are not:
MarkdownParser. The constructor callsParser::setStaticVars(), which callsUser::setMe(), which wantsDb::$db.TimeInterval::localize(), the other half of [3.0] Restores full compatibility of SMF\TimeInterval with \DateInterval #9499. Every branch of it goesthrough
Lang::getTxt(), which loads a language file, which wantsTheme::$currentand
Db::$db.toParsable()walks the same units with the strings hard coded, so theordering is not entirely unwatched.
Both belong to an integration suite. Neither is a reason to contort the production code.
The rest of the week was out of reach for the ordinary reasons: #9433, #9459, #9463,
#9455, #9466, #9482 and #9476 need a database; #9453 reaches
Db::$dbthroughPageIndex's constructor; #9449, #9444 and #9454 are templates; #9479, #9494 and #9495are JavaScript; #9505 needs a socket; #9502 sits inside a protected method on a loaded
Profile.One defect found while writing these
IP::matchToCIDR()builds its IPv6 mask like this:The cast binds to
$cidr_subnetmask, not to the division, so the argument is a floatwhenever the prefix length is not a multiple of four, and
str_repeat()throws aTypeError.matchToCIDR('2001:db8::/33')is a fatal error, not afalse.The
switch ($cidr_subnetmask % 4)immediately below it exists to add the odd nibblefor exactly those prefix lengths, so all three of its cases are unreachable and always
have been. The line predates #9507, which renamed the variable on it; it came in with
9cc402c99, moving the method over fromQueryString::matchIPtoCIDR().Nothing here fixes it. The data provider keeps to multiples of four and says why in a
comment, because asserting the
TypeErrorwould preserve it. Worth its own PR.How this was verified
vendor/bin/phpunit: OK (140 tests, 206 assertions) on PHP 8.4.24.php-cs-fixer check --allow-risky=yes tests/:Found 0 of 13 files that can be fixed.check-signed-off.phppasses locally.A word on the three lines of
Sources/in this diffThey are #9512, merged in, and they are not this branch's doing.
.github/workflows/php-cs-fixer.ymlrestricts the check to the files a pull requesttouched, unless that pull request changes
.php-cs-fixer.dist.phporcomposer.lock,in which case it scans everything. This branch changes
composer.lock, because #9326adds PHPUnit, so it got the whole-tree scan and reported three pre-existing violations
of the
SMF/section_commentsfixer inAlert.php,Autolinker.phpandHelp.php—none of which this branch goes anywhere near. They are one blank line added and two
removed, and they have been sitting in
release-3.0unnoticed because no pull requestsince has touched those files or triggered a full scan.
#9512 fixes them against
release-3.0and is worth merging on its own. It is mergedhere so this branch is green now rather than green later; merge #9512 first and it
disappears from this diff.
Relationship to other PRs
This is stacked on #9326 and contains it, so the diff shown here is that branch's as
well as its own. It also therefore carries everything #9326 carries: #9317, #9321,
#9324 and #9325, plus #9512 as described above. Merge those and rebase this on
release-3.0, and onlytests/is left.The three fixes it covers — #9499, #9506, #9507 — are already in
release-3.0, which ismerged into this branch.
Issues References (Fixes|Related|Closes)