From 57e414a932a9a75501d020f2f0bcd81da6d01318 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Wed, 26 Aug 2026 00:14:23 +0400 Subject: [PATCH 1/7] test(sandbox): showcase testMetadata reporting via a middleware plugin Add a sandbox playground that emits ##teamcity[testMetadata] messages for every TeamCity metadata type. A #[TestMetadata] attribute declares the values; a TestMetadataInterceptor (registered by TestMetadataPlugin on the sandbox suite) writes them straight to the stream after the test body runs, nested between testStarted and testFinished like the built-in bench metrics. Covers numbers (a chartable table), text, links, and images/artifacts sourced from both local files and URLs; relative file references resolve against the test directory. Assisted-By: Claude Opus 4.8 (1M context) --- testo.php | 2 + .../Sandbox/Metadata/MetadataShowcaseTest.php | 126 ++++++++++++++++++ tests/Sandbox/Metadata/MetadataType.php | 22 +++ tests/Sandbox/Metadata/TestMetadata.php | 23 ++++ .../Metadata/TestMetadataInterceptor.php | 87 ++++++++++++ tests/Sandbox/Metadata/TestMetadataPlugin.php | 23 ++++ tests/Sandbox/Metadata/resources/chart.png | Bin 0 -> 722 bytes tests/Sandbox/Metadata/resources/report.csv | 6 + 8 files changed, 289 insertions(+) create mode 100644 tests/Sandbox/Metadata/MetadataShowcaseTest.php create mode 100644 tests/Sandbox/Metadata/MetadataType.php create mode 100644 tests/Sandbox/Metadata/TestMetadata.php create mode 100644 tests/Sandbox/Metadata/TestMetadataInterceptor.php create mode 100644 tests/Sandbox/Metadata/TestMetadataPlugin.php create mode 100644 tests/Sandbox/Metadata/resources/chart.png create mode 100644 tests/Sandbox/Metadata/resources/report.csv diff --git a/testo.php b/testo.php index e4ceb9eb..6226897f 100644 --- a/testo.php +++ b/testo.php @@ -6,6 +6,7 @@ use Testo\Application\Config\FinderConfig; use Testo\Application\Config\SuiteConfig; use Testo\Testing\InjectPlugin; +use Tests\Sandbox\Metadata\TestMetadataPlugin; return new ApplicationConfig( src: new FinderConfig( @@ -48,6 +49,7 @@ location: new FinderConfig( include: ['tests/Sandbox'], ), + plugins: [new TestMetadataPlugin()], ), ], require 'internal/container/tests/suites.php', diff --git a/tests/Sandbox/Metadata/MetadataShowcaseTest.php b/tests/Sandbox/Metadata/MetadataShowcaseTest.php new file mode 100644 index 00000000..42eec474 --- /dev/null +++ b/tests/Sandbox/Metadata/MetadataShowcaseTest.php @@ -0,0 +1,126 @@ +publish($info); + } + } + + private function publish(TestInfo $info): void + { + $reflection = $info->testDefinition->reflection; + $baseDir = \dirname($reflection->getFileName() ?: '.'); + + foreach ($reflection->getAttributes(TestMetadata::class) as $attribute) { + $meta = $attribute->newInstance(); + $value = $this->resolveValue($meta, $baseDir); + + $value === '' or \fwrite($this->output, Formatter::testMetadata( + testName: $info->name, + name: $meta->name, + value: $value, + type: $meta->type->value, + identity: $info->identity, + ) . "\n"); + } + } + + /** + * Resolves a local file reference against the test's directory; a URL or an absolute path passes + * through untouched, as do the non-file types. + */ + private function resolveValue(TestMetadata $meta, string $baseDir): string + { + $isFileType = $meta->type === MetadataType::Image || $meta->type === MetadataType::Artifact; + + return match (true) { + !$isFileType, + $this->isUrl($meta->value), + $this->isAbsolute($meta->value) => $meta->value, + default => $baseDir . \DIRECTORY_SEPARATOR . \strtr($meta->value, '/', \DIRECTORY_SEPARATOR), + }; + } + + private function isUrl(string $value): bool + { + return \preg_match('#^[a-z][a-z0-9+.-]*://#i', $value) === 1; + } + + private function isAbsolute(string $value): bool + { + return $value !== '' && ($value[0] === '/' || \preg_match('#^[a-z]:[\\\\/]#i', $value) === 1); + } +} diff --git a/tests/Sandbox/Metadata/TestMetadataPlugin.php b/tests/Sandbox/Metadata/TestMetadataPlugin.php new file mode 100644 index 00000000..278b30f7 --- /dev/null +++ b/tests/Sandbox/Metadata/TestMetadataPlugin.php @@ -0,0 +1,23 @@ +get(InterceptorCollector::class)->addInterceptor(new TestMetadataInterceptor()); + } +} diff --git a/tests/Sandbox/Metadata/resources/chart.png b/tests/Sandbox/Metadata/resources/chart.png new file mode 100644 index 0000000000000000000000000000000000000000..a2448c6b9cb5ce40cdcb7355545c246a1f660fee GIT binary patch literal 722 zcmeAS@N?(olHy`uVBq!ia0vp^AAq=mg9%7FUwKggq&N#aB8wRq_>O=u<5X=vX$A(S z7Ec$)kcv5PZ#V`$3J`G!eDliHqCiKwh;grp*ahiVcfT}fE%Eupnc=vj^qj}Vnz$3D zbrqGh)%P8bu6O5Wu=l*MgkzS7<7B0bAQs=Q1)hQ?noVl110s0py*vMg+i%P2|NWa? z`{%>P{hw8Cm(d9?|Cqev?fMt<+-(;>=cv-W{PNk89XI!$4p{7|C-wf& z_JS*uZ~dB<{7v@!$J;O8RzCQ0I$rG0zq}%YhY39|AA582ohz*Ql|66ubIyAr@6@g* zd`oW8x1aUz&{2NXYdqV&I3BNfUi{H`!ojA^`3}?jcD>!{wN2Ed=JK`eG6kZ8BFZ2M b=09UfjOoqZ8<(qriIl<9)z4*}Q$iB}<=Q&$ literal 0 HcmV?d00001 diff --git a/tests/Sandbox/Metadata/resources/report.csv b/tests/Sandbox/Metadata/resources/report.csv new file mode 100644 index 00000000..26e29c3a --- /dev/null +++ b/tests/Sandbox/Metadata/resources/report.csv @@ -0,0 +1,6 @@ +iteration,throughput_ops,latency_ms +1,1180,0.85 +2,1240,0.81 +3,1305,0.77 +4,1288,0.78 +5,1342,0.74 From 6f541817c4e390d2e356e56d48190eeb1ed63bb8 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Wed, 26 Aug 2026 10:03:20 +0400 Subject: [PATCH 2/7] feat(bench): report metrics as a grouped, dimensioned metadata table MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bench metrics now spell `bench...` instead of the flat `bench..`, so a consumer that understands the four-segment `testMetadata` convention rebuilds them as a sortable table with spanning column-group bands — the same grouping the terminal's ASCII table prints: Benchmark setup, Time results, Filtered results, Summary. Every case emits every column so the grid stays rectangular, and the standalone `bench.iterations` counter folds into each row's Benchmark setup group to keep the key depth uniform — a mixed depth would drop a consumer back to a flat list. The unit no longer hides in the leaf name. A value is a `Testo\Core\Metric\Metric` — a generic `Metric` carrying its number in the unit it was measured in. Units are split into family enums (Time, Memory, Percent, Scalar) under a shared `Unit` interface, so a `Metric` cannot be handed to a consumer keyed to `Metric