feat(testing): add ExpectTestStatus, ExpectAssertionsCount, ExpectTestResultAttribute (#36)#264
Open
rossaddison wants to merge 6 commits into
Open
Conversation
…tResultAttribute (php-testo#36) Three PHP attributes allow a test that uses TestRunner::runTest() to assert properties of the inner stub TestResult without writing explicit assertion code: - #[ExpectTestStatus(Status::X)] — validates stub.status - #[ExpectAssertionsCount(N)] — validates stub.summary.metric('assertions') - #[ExpectTestResultAttribute(K)] — validates stub.getAttribute(K) is not null (repeatable) ExpectInterceptor reads these from the test method's reflection, runs the test, extracts outerResult->result as the stub TestResult, and converts any mismatches into a single Status::Failed with a combined message. Pre-existing outer failures are preserved untouched. Registered automatically via InjectPlugin. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ime return type Style::dim() worked correctly with any string (empty or not) but declared @param non-empty-string, which Psalm flagged at every call site where a plain string was passed. Removed the over-restrictive annotation. ChannelRenderer::formatTime() claimed @return non-empty-string with a /** @var non-empty-string */ inline cast, which Psalm 7 does not accept. Replaced date() with integer arithmetic + sprintf so the implementation is cleaner, and removed the annotation since Psalm 7 does not narrow sprintf to non-empty-string for this version. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ossaddison/testo into feat/36-expect-test-attributes
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
…is mirrored tests/Application/Stub/EmptyRun/ is intentionally empty — it is the test fixture for EmptyRunTest, which asserts that a Testo run over an empty directory yields Status::Risky with zero tests collected. Git does not track empty directories, and bin/build-phpunit.php only copies *.php files when populating the tests/PhpUnit/ mirror, so the mirror never contained tests/PhpUnit/Application/Stub/EmptyRun/. The mirrored EmptyRunTest resolved __DIR__ . '/../../Stub/EmptyRun' to that missing path and threw InvalidArgumentException: File or directory not found — aborting Infection's initial PHPUnit test run on every CI push to 1.x. Add .placeholder.php (no namespace, no classes, no tests) to the source directory. The build script copies it verbatim into the mirror, which creates the required directory. Testo's FinderConfig still discovers zero tests there, so Status::Risky is reported and the assertion holds. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…t coverage ExpectAssertionsCount, ExpectTestStatus, and ExpectTestResultAttribute each had 0% coverage per Codecov, despite ExpectInterceptorTest exercising every constructor via newInstance(). Testo's codecov plugin scopes coverage per test to the classes named in #[Covers(...)] on that test, so lines executed by a test are only credited to files the test explicitly declares — and this test only declared #[Covers(ExpectInterceptor::class)]. Add #[Covers(...)] for the three attribute classes so their already-exercised constructors are credited.
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.
Summary
Closes #36.
Adds three PHP attributes that let a test method assert properties of the inner stub
TestResultreturned byTestRunner::runTest(), without writing explicit assertion code in the test body:#[ExpectTestStatus(Status::X)]— assertsstubResult->status#[ExpectAssertionsCount(N)]— assertsstubResult->summary->metric('assertions')#[ExpectTestResultAttribute('key')]— assertsstubResult->getAttribute('key') !== null(repeatable)How it works
ExpectInterceptor(registered automatically byInjectPlugin) runs atORDER_CLOSE_TO_TEST - 1. It reads the expect attributes from$info->testDefinition->reflection, calls$next()to execute the outer test, extracts$outerResult->resultas the stubTestResult, then validates each declared expectation. Any mismatches are combined into a singleStatus::Failedwith oneRuntimeException. Pre-existing outer failures (or non-terminal statuses) are passed through untouched.Files added
core/Testing/Attribute/ExpectTestStatus.phpcore/Testing/Attribute/ExpectAssertionsCount.phpcore/Testing/Attribute/ExpectTestResultAttribute.phpcore/Testing/Internal/ExpectInterceptor.phpFiles modified
core/Testing/InjectPlugin.phpExpectInterceptorTest plan
tests/Core/Testing/Unit/ExpectInterceptorTest.php— all passChannelRenderer.phpunrelated)Core/Testing/Unitsuite: 15/15 passed🤖 Generated with Claude Code