This commit is contained in:
nuno maduro
2026-04-27 13:16:05 +01:00
parent f45cbf43c5
commit 81bfdbf8fe
5 changed files with 82 additions and 21 deletions

View File

@ -367,6 +367,15 @@ YAML;
return $m[1];
}
// SSH URL form: ssh://[user@]github.com[:port]/org/repo(.git).
// Some teams configure this explicitly to pin the SSH port; the
// colon-separated form above doesn't match. Mirrors the parser
// in `Storage::originIdentity` so the same remote produces the
// same project key for both storage and remote-fetch.
if (preg_match('#^ssh://(?:[^@/]+@)?github\.com(?::\d+)?/([\w.-]+/[\w.-]+?)(?:\.git)?/?$#i', $url, $m) === 1) {
return $m[1];
}
return null;
}

View File

@ -6,6 +6,7 @@ namespace Pest\Plugins\Tia;
use Pest\Support\Container;
use PHPUnit\Framework\TestStatus\TestStatus;
use Symfony\Component\Console\Output\OutputInterface;
/**
* File-level Test Impact Analysis graph.
@ -319,7 +320,21 @@ final class Graph
if ($newJsFiles !== []) {
$freshMap = JsModuleGraph::buildStrict($this->projectRoot);
if ($freshMap !== null) {
if ($freshMap === null) {
// Vite resolver was unavailable (Node missing, cold-start
// timeout, vite.config refused to load). Falling back to
// the broad watch pattern is the correct call, but
// doing so silently can make a slow replay feel
// inexplicable — surface a single line so the user
// knows precision was downgraded for these files.
$output = Container::getInstance()->get(OutputInterface::class);
if ($output instanceof OutputInterface) {
$output->writeln(sprintf(
' <fg=yellow>TIA</> Vite resolver unavailable — falling back to watch pattern for %d new JS file(s).',
count($newJsFiles),
));
}
} else {
foreach ($newJsFiles as $rel) {
$pages = $freshMap[$rel] ?? [];

View File

@ -100,21 +100,19 @@ final class Recorder
if (function_exists('pcov\\start')) {
$this->driver = 'pcov';
$this->driverAvailable = true;
} elseif (function_exists('xdebug_start_code_coverage')) {
// Xdebug is loaded. Probe whether coverage mode is active by
// attempting a start — it emits E_WARNING when the mode is off.
// We capture the warning via a temporary error handler.
$probeOk = true;
set_error_handler(static function () use (&$probeOk): bool {
$probeOk = false;
} elseif (function_exists('xdebug_start_code_coverage') && function_exists('xdebug_info')) {
// Xdebug 3+ exposes the active mode set via `xdebug_info`,
// so we can ask directly instead of probing with a
// start/stop pair. The probe approach used to emit
// E_WARNING when coverage mode was off; with monitoring
// agents (Sentry, Bugsnag) hooked into the error
// handler stack that warning could be reported as a
// real error. `xdebug_info('mode')` is silent and
// returns the active modes as a list, so a presence
// check is enough.
$modes = \xdebug_info('mode');
return true;
});
\xdebug_start_code_coverage();
restore_error_handler();
if ($probeOk) {
\xdebug_stop_code_coverage(false);
if (is_array($modes) && in_array('coverage', $modes, true)) {
$this->driver = 'xdebug';
$this->driverAvailable = true;
}

View File

@ -105,21 +105,27 @@ final class TableExtractor
}
/**
* @return list<string> Table names referenced by `Schema::` calls
* OR raw DDL statements in the given migration
* @return list<string> Table names referenced by `Schema::` calls,
* raw DDL, or DML inside the given migration
* file contents. Empty when nothing matches —
* callers treat that as "fall back to the
* broad watch pattern".
*
* Two passes:
* Three passes:
* 1. `Schema::create|table|drop|dropIfExists|dropColumn[s]|rename`
* captures the conventional Laravel migration shape.
* 2. Raw DDL fallback: scans for `CREATE / ALTER / DROP /
* TRUNCATE / RENAME TABLE <name>` patterns inside string
* literals (i.e. `DB::statement('CREATE TABLE …')`,
* `DB::unprepared('ALTER TABLE …')`). False positives possible
* if the same syntax appears in a comment or unrelated string,
* but over-attribution is correctness-safe.
* `DB::unprepared('ALTER TABLE …')`).
* 3. DML inside migration bodies — `INSERT INTO`, `UPDATE … SET`,
* `DELETE FROM`, and Laravel's fluent `DB::table('foo')`.
* Catches the seeded-lookup-table case where a migration
* populates rows that tests later read.
*
* False positives possible when the same syntax appears in a
* comment or unrelated string, but over-attribution is
* correctness-safe.
*/
public static function fromMigrationSource(string $php): array
{
@ -157,6 +163,33 @@ final class TableExtractor
}
}
// Pass 3: DML inside migration bodies. Migrations that seed
// lookup tables via `DB::statement('INSERT INTO roles …')`,
// `DB::table('statuses')->insert(…)`, `UPDATE foo SET …`, or
// `DELETE FROM bar` are common in Laravel. Without picking
// these up, an edit to the seed payload would route through
// only the schema'd tables and silently skip every test that
// reads from the populated table. Fluent-builder calls
// (`DB::table('x')`) and raw SQL strings are both covered.
$dmlPatterns = [
'/INSERT\s+(?:IGNORE\s+)?INTO\s+["`\[]?(\w+)["`\]]?/i',
'/UPDATE\s+["`\[]?(\w+)["`\]]?\s+SET\b/i',
'/DELETE\s+FROM\s+["`\[]?(\w+)["`\]]?/i',
'/DB::table\(\s*[\'"]([^\'"]+)[\'"]\s*\)/',
];
foreach ($dmlPatterns as $pattern) {
if (preg_match_all($pattern, $php, $matches) === false) {
continue;
}
foreach ($matches[1] as $name) {
$lower = strtolower($name);
if (! self::isSchemaMeta($lower)) {
$tables[$lower] = true;
}
}
}
$out = array_keys($tables);
sort($out);

View File

@ -43,12 +43,18 @@ final readonly class Inertia implements WatchDefault
'resources/js/Pages/**/*.tsx' => [$browserDir],
'resources/js/Pages/**/*.jsx' => [$browserDir],
'resources/js/Pages/**/*.svelte' => [$browserDir],
'resources/js/Pages/**/*.ts' => [$browserDir],
'resources/js/Pages/**/*.js' => [$browserDir],
// Shared layouts / components consumed by pages.
'resources/js/Layouts/**/*.vue' => [$browserDir],
'resources/js/Layouts/**/*.tsx' => [$browserDir],
'resources/js/Layouts/**/*.ts' => [$browserDir],
'resources/js/Layouts/**/*.js' => [$browserDir],
'resources/js/Components/**/*.vue' => [$browserDir],
'resources/js/Components/**/*.tsx' => [$browserDir],
'resources/js/Components/**/*.ts' => [$browserDir],
'resources/js/Components/**/*.js' => [$browserDir],
// SSR entry point.
'resources/js/ssr.js' => [$browserDir],