Skip to content

Commit e40a499

Browse files
committed
chore: cleanup
Assisted-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent b22857b commit e40a499

8 files changed

Lines changed: 57 additions & 12 deletions

File tree

.gitattributes

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@
88
*.http binary
99
*.gpg binary
1010

11+
# Version manifest: each package bumps its own line — take both sides on overlap
12+
# instead of raising a conflict. A trailing sentinel key keeps every real entry
13+
# comma-terminated so the union stays valid JSON.
14+
/resources/version.json merge=union
15+
1116
# Default: nothing is included in the `composer archive` of testo/testo.
1217
# Re-include only the files that actually belong to the root metapackage below.
1318
* export-ignore

.github/release-please/sync-deps.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,14 @@
5151
const MANIFEST = 'resources/version.json';
5252
const SECTIONS = ['require', 'require-dev'];
5353

54+
/**
55+
* Trailing sentinel key in the manifest. It carries no package: its only job is
56+
* to keep every real entry comma-terminated so a `merge=union` of concurrent
57+
* per-package version bumps (see .gitattributes) stays valid JSON. Skipped
58+
* everywhere a manifest key is treated as a package path.
59+
*/
60+
const SENTINEL = '_';
61+
5462
/**
5563
* The framework meta-package that plugins/bridges depend on. Unlike siblings
5664
* (pinned with a caret), it is refreshed with an open upper bound
@@ -74,6 +82,9 @@
7482
// changed, so we fall back to refreshing every package.
7583
$released = [];
7684
foreach ($manifest as $path => $version) {
85+
if ($path === SENTINEL) {
86+
continue;
87+
}
7788
if (!$hasPrevious || !\array_key_exists($path, $previous) || $previous[$path] !== $version) {
7889
$released[$path] = true;
7990
}
@@ -86,7 +97,7 @@
8697
// The root (testo/testo) is intentionally excluded — siblings only.
8798
$versions = [];
8899
foreach ($manifest as $path => $version) {
89-
if ($path === '.') {
100+
if ($path === '.' || $path === SENTINEL) {
90101
continue;
91102
}
92103
$composer = readJson($root . "/$path/composer.json");

plugin/assert/src/Api/Builtin/ArrayType.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,13 @@ public function isList(string $message = ''): static;
4646
/**
4747
* Asserts that the array holds the same elements as the expected iterable, regardless of order.
4848
*
49-
* Both sides are canonicalized (recursively sorted, keys discarded) and then compared loosely,
50-
* matching PHPUnit's {@see \PHPUnit\Framework\Assert::assertEqualsCanonicalizing()} semantics.
49+
* Both sides are canonicalized (recursively sorted, keys discarded) and then compared loosely:
50+
*
51+
* ```php
52+
* Assert::array([3, 1, 2])->sameElementsAs([1, 2, 3]); // passes — order ignored
53+
* Assert::array(['a' => 1, 'b' => 2])->sameElementsAs([2, 1]); // passes — keys ignored
54+
* Assert::array([1, 2])->sameElementsAs([1, 2, 3]); // fails — different elements
55+
* ```
5156
*
5257
* @param iterable $expected The expected elements, in any order.
5358
* @param string $message Optional message for the assertion.

plugin/assert/src/Internal/Assertion/AssertArray.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,8 +143,7 @@ public function sameElementsAs(iterable $expected, string $message = ''): static
143143

144144
/**
145145
* Recursively sorts an array by value and discards keys, so two arrays holding the same
146-
* elements in any order (and under any keys) canonicalize to an identical shape. Mirrors the
147-
* canonicalization PHPUnit applies for `assertEqualsCanonicalizing`.
146+
* elements in any order (and under any keys) canonicalize to an identical shape.
148147
*/
149148
private static function canonicalize(array $value): array
150149
{

resources/version.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,6 @@
1818
"bridge/mockery": "0.1.2",
1919
"bridge/rector": "0.2.4",
2020
"bridge/revolt": "0.1.1",
21-
"bridge/vcr": "0.1.0"
21+
"bridge/vcr": "0.1.0",
22+
"_": ""
2223
}

skills/testo-flaky-tests/SKILL.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ public function __construct(
6767
) {}
6868
```
6969

70-
- `times` is the **total** number of runs. `#[Repeat(times: 3)]` runs the test three times (mirrors Kotlin's `repeat(n)` and JUnit's `@RepeatedTest(n)`). It is **not** "additional repetitions on top of one run".
70+
- `times` is the **total** number of runs. `#[Repeat(times: 3)]` runs the test three times. It is **not** "additional repetitions on top of one run".
7171
- `maxFailures` defaults to `0` — any single failure fails the whole loop.
7272
- Combining with `#[Retry]`: Repeat runs *inside* Retry — each retry attempt re-runs the full repeat cycle. Possible, but the semantics are subtle; surface it to the user before suggesting both.
7373

@@ -91,7 +91,7 @@ Don't ship `#[Repeat(times: 50)]` long-term on a fast suite — CI cost adds up.
9191
## Pitfalls
9292

9393
- **Don't disable `markFlaky`** on `#[Retry]` / `#[Repeat]` (it defaults to `true`). Setting `markFlaky: false` is **silent rot** — a flaky test that retries to green hides the underlying defect. Only flip it off when the user explicitly asks.
94-
- **`Repeat(times: N)` is total runs, not extra runs.** `Repeat(times: 1)` runs the test once. People coming from older PHPUnit `@Repeat` semantics expect "additional" — they're wrong here.
94+
- **`Repeat(times: N)` is total runs, not extra runs.** `Repeat(times: 1)` runs the test once, `Repeat(times: 3)` runs it three times total — `N` is the run count, not a number of *additional* runs on top of the first.
9595
- Combining `#[Retry]` with `#[Repeat]` is allowed: Repeat runs **inside** Retry (each retry attempt re-runs the full repeat cycle). Only suggest both when the user genuinely wants that nesting.
9696
- A test with `Expect::exception(...)` and `#[Retry]` is almost always wrong — expected exceptions are deterministic by design.
9797
- Don't use retries to paper over network calls in unit tests — replace the dependency with a fake instead.

skills/testo-write-tests/SKILL.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ description: Write or modify tests in a project that uses the Testo PHP testing
55

66
# Writing tests with Testo
77

8-
Testo is **not PHPUnit**. The attribute set, assertion facade, exception expectations, and lifecycle
9-
hooks are Testo's own — do not transliterate PHPUnit idioms.
8+
The attribute set, assertion facade, exception expectations, and lifecycle hooks are Testo's own.
9+
Write them the Testo way described below — don't transliterate idioms from other test frameworks.
1010

1111
## Before you write code
1212

@@ -81,7 +81,7 @@ Assert::string($s)->contains('foo')->notContains('bar');
8181
Assert::int($n)->greaterThan(0)->lessThanOrEqual(100);
8282
Assert::numeric($n)->between(1, 100); // int, float, or numeric string
8383
Assert::array($a)->hasKeys('id', 'name')->isList()->hasCount(3)->contains('x')->notContains('y');
84-
Assert::array($a)->sameElementsAs([3, 2, 1]); // order-insensitive, like assertEqualsCanonicalizing
84+
Assert::array($a)->sameElementsAs([3, 2, 1]); // order-insensitive, keys ignored
8585
Assert::object($o)->instanceOf(Foo::class)->hasProperty('id');
8686
Assert::json($s)->isObject()->hasKeys(['data', 'meta'])->assertPath('$.data.id', 42);
8787
```
@@ -241,7 +241,7 @@ Always pass `--json` (as above) for a compact, machine-readable report that's ch
241241
summary plus the failed tests. Use `--log-json=build/report.json` to write it to a file while keeping
242242
the terminal output.
243243

244-
Use the Testo CLI, **never** `phpunit`.
244+
Always run tests through the Testo CLI (`vendor/bin/testo`).
245245

246246
## Pitfalls
247247

tests/Testo/Acceptance/SyncDepsTest.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,30 @@ public function leavesUnmanagedTestoPackagesUntouched(): void
206206
}
207207
}
208208

209+
/**
210+
* The manifest carries a trailing sentinel key (`_`) so that a `merge=union`
211+
* of concurrent per-package bumps stays valid JSON. It names no package, so
212+
* the synchroniser must skip it rather than try to read `_/composer.json`.
213+
*/
214+
public function ignoresTheSentinelManifestKey(): void
215+
{
216+
$dir = $this->fixture(
217+
['.' => '1.0.0', 'plugin/a' => '1.2.3', '_' => ''],
218+
[
219+
'composer.json' => $this->composer('testo/testo', ['testo/a' => '0.1 - 1']),
220+
'plugin/a/composer.json' => $this->composer('testo/a'),
221+
],
222+
);
223+
224+
try {
225+
$this->run($dir);
226+
227+
Assert::same($this->requireOf($dir, 'composer.json')['testo/a'], '^1.2.3');
228+
} finally {
229+
$this->cleanup($dir);
230+
}
231+
}
232+
209233
public function isIdempotent(): void
210234
{
211235
$dir = $this->fixture(

0 commit comments

Comments
 (0)