From ddc2a11788038a151781846ec080c1fe3171e7ba Mon Sep 17 00:00:00 2001 From: Marcello Duarte Date: Wed, 10 Dec 2025 18:36:29 +0000 Subject: [PATCH 1/4] Fix autoloader race condition for dynamically created IOApp classes --- bin/phunkie | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/bin/phunkie b/bin/phunkie index b1e0b9a..2d411ce 100755 --- a/bin/phunkie +++ b/bin/phunkie @@ -85,6 +85,22 @@ use function Phunkie\Console\Functions\{setColors, printBanner, loadHistory, sav if (isset($args[1]) && !str_starts_with($args[1], '-')) { $className = $args[1]; + + // Try to load the class file directly if it exists (for dynamically created files) + // This handles cases where composer's autoloader cache doesn't see new files + $classFile = str_replace('\\', '/', $className) . '.php'; + $possiblePaths = [ + getcwd() . '/' . $classFile, + __DIR__ . '/../' . $classFile, + __DIR__ . '/../tests/' . str_replace('Tests/', '', $classFile), + ]; + foreach ($possiblePaths as $path) { + if (file_exists($path) && !class_exists($className, false)) { + require_once $path; + break; + } + } + if (class_exists($className) && is_subclass_of($className, IOApp::class)) { $app = new $className(); } From cf088105566b8b6d8ab772adf50bdd59e9e122b5 Mon Sep 17 00:00:00 2001 From: Marcello Duarte Date: Wed, 10 Dec 2025 18:59:54 +0000 Subject: [PATCH 2/4] Fix broken ci --- bin/phunkie | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/bin/phunkie b/bin/phunkie index 2d411ce..c14be6e 100755 --- a/bin/phunkie +++ b/bin/phunkie @@ -88,21 +88,36 @@ use function Phunkie\Console\Functions\{setColors, printBanner, loadHistory, sav // Try to load the class file directly if it exists (for dynamically created files) // This handles cases where composer's autoloader cache doesn't see new files + // Convert namespace to path, handling the Tests -> tests directory mapping $classFile = str_replace('\\', '/', $className) . '.php'; + $testsRelativePath = str_replace('Tests/', '', $classFile); + $possiblePaths = [ + // Most likely: tests directory with lowercase 't' (Linux/CI compatible) + getcwd() . '/tests/' . $testsRelativePath, + __DIR__ . '/../tests/' . $testsRelativePath, + // Also check uppercase Tests for compatibility getcwd() . '/' . $classFile, __DIR__ . '/../' . $classFile, - __DIR__ . '/../tests/' . str_replace('Tests/', '', $classFile), ]; + foreach ($possiblePaths as $path) { if (file_exists($path) && !class_exists($className, false)) { - require_once $path; + try { + require_once $path; + } catch (\Throwable $e) { + fwrite(STDERR, "Error loading $path: " . $e->getMessage() . "\n"); + } break; } } if (class_exists($className) && is_subclass_of($className, IOApp::class)) { - $app = new $className(); + try { + $app = new $className(); + } catch (\Throwable $e) { + fwrite(STDERR, "Error instantiating $className: " . $e->getMessage() . "\n"); + } } } @@ -110,7 +125,13 @@ use function Phunkie\Console\Functions\{setColors, printBanner, loadHistory, sav $app = new PhunkieConsole(); } - $app->run($args)->unsafeRun(); + try { + $app->run($args)->unsafeRun(); + } catch (\Throwable $e) { + fwrite(STDERR, get_class($e) . ": " . $e->getMessage() . "\n"); + fwrite(STDERR, "In " . $e->getFile() . ":" . $e->getLine() . "\n"); + exit(1); + } })(); From c47f811aeb5603c6591a6054e29705743503bc0b Mon Sep 17 00:00:00 2001 From: Marcello Duarte Date: Wed, 10 Dec 2025 19:57:51 +0000 Subject: [PATCH 3/4] Fix broken ci --- bin/phunkie | 29 +++++- tests/Acceptance/ReplSteps.php | 93 ++++++++++++++++--- .../Acceptance/Support/ReplProcessManager.php | 8 ++ 3 files changed, 113 insertions(+), 17 deletions(-) diff --git a/bin/phunkie b/bin/phunkie index c14be6e..8f557c2 100755 --- a/bin/phunkie +++ b/bin/phunkie @@ -101,35 +101,54 @@ use function Phunkie\Console\Functions\{setColors, printBanner, loadHistory, sav __DIR__ . '/../' . $classFile, ]; + $fileLoaded = false; foreach ($possiblePaths as $path) { if (file_exists($path) && !class_exists($className, false)) { try { require_once $path; + $fileLoaded = true; } catch (\Throwable $e) { - fwrite(STDERR, "Error loading $path: " . $e->getMessage() . "\n"); + echo "[DEBUG] Error loading $path: " . get_class($e) . ": " . $e->getMessage() . "\n"; } break; } } - if (class_exists($className) && is_subclass_of($className, IOApp::class)) { + // Debug: check class status after loading attempt + $classExists = class_exists($className, true); + $isIOApp = $classExists && is_subclass_of($className, IOApp::class); + + if (!$classExists) { + echo "[DEBUG] Class $className not found after loading attempt. fileLoaded=$fileLoaded\n"; + echo "[DEBUG] Checked paths:\n"; + foreach ($possiblePaths as $path) { + echo "[DEBUG] $path exists=" . (file_exists($path) ? 'yes' : 'no') . "\n"; + } + } + + if ($classExists && $isIOApp) { try { $app = new $className(); } catch (\Throwable $e) { - fwrite(STDERR, "Error instantiating $className: " . $e->getMessage() . "\n"); + echo "[DEBUG] Error instantiating $className: " . get_class($e) . ": " . $e->getMessage() . "\n"; } + } elseif ($classExists && !$isIOApp) { + echo "[DEBUG] Class $className exists but is not an IOApp\n"; } } if ($app === null) { + if (isset($className)) { + echo "[DEBUG] Falling back to PhunkieConsole (requested class: $className)\n"; + } $app = new PhunkieConsole(); } try { $app->run($args)->unsafeRun(); } catch (\Throwable $e) { - fwrite(STDERR, get_class($e) . ": " . $e->getMessage() . "\n"); - fwrite(STDERR, "In " . $e->getFile() . ":" . $e->getLine() . "\n"); + echo "[DEBUG] Exception during run: " . get_class($e) . ": " . $e->getMessage() . "\n"; + echo "[DEBUG] In " . $e->getFile() . ":" . $e->getLine() . "\n"; exit(1); } })(); diff --git a/tests/Acceptance/ReplSteps.php b/tests/Acceptance/ReplSteps.php index 7a3db2d..15cf9a0 100644 --- a/tests/Acceptance/ReplSteps.php +++ b/tests/Acceptance/ReplSteps.php @@ -44,24 +44,33 @@ public function __construct() private function startRepl(string $command = 'php bin/phunkie'): void { - if ($command !== 'php bin/phunkie' && $command !== 'php bin/phunkie -c') { + $isIOAppCommand = $command !== 'php bin/phunkie' && $command !== 'php bin/phunkie -c'; + + if ($isIOAppCommand) { // Non-standard command means we need to test the actual process $this->useProcessManager = true; } if ($this->useProcessManager) { $this->processManager->start($command); - $stdout = $this->processManager->getStdout(); - if ($stdout !== null) { - $newOutput = ReplOutputReader::readOutput($stdout); - $this->output .= $newOutput; - } - // Also capture stderr for debugging - $stderr = $this->processManager->getStderr(); - if ($stderr !== null) { - $errorOutput = ReplOutputReader::readOutput($stderr); - if ($errorOutput !== '') { - $this->output .= "\n[STDERR]: " . $errorOutput; + + if ($isIOAppCommand) { + // For IOApp commands, wait for the process to complete and capture all output + $this->waitForProcessAndCaptureOutput(); + } else { + // For REPL mode, read available output + $stdout = $this->processManager->getStdout(); + if ($stdout !== null) { + $newOutput = ReplOutputReader::readOutput($stdout); + $this->output .= $newOutput; + } + // Also capture stderr for debugging + $stderr = $this->processManager->getStderr(); + if ($stderr !== null) { + $errorOutput = ReplOutputReader::readOutput($stderr); + if ($errorOutput !== '') { + $this->output .= "\n[STDERR]: " . $errorOutput; + } } } } else { @@ -71,6 +80,66 @@ private function startRepl(string $command = 'php bin/phunkie'): void } } + private function waitForProcessAndCaptureOutput(): void + { + $stdout = $this->processManager->getStdout(); + $stderr = $this->processManager->getStderr(); + + // Wait for the process to complete (up to 5 seconds) + $maxWait = 5.0; + $startTime = microtime(true); + + while ((microtime(true) - $startTime) < $maxWait) { + // Read any available stdout + if ($stdout !== null) { + $read = [$stdout]; + $write = null; + $except = null; + if (stream_select($read, $write, $except, 0, 100000) > 0) { // 100ms timeout + $chunk = stream_get_contents($stdout); + if ($chunk !== false && $chunk !== '') { + $this->output .= $chunk; + } + } + } + + // Read any available stderr + if ($stderr !== null) { + $read = [$stderr]; + $write = null; + $except = null; + if (stream_select($read, $write, $except, 0, 10000) > 0) { // 10ms timeout + $chunk = stream_get_contents($stderr); + if ($chunk !== false && $chunk !== '') { + $this->output .= "\n[STDERR]: " . $chunk; + } + } + } + + // Check if process has exited + $status = $this->processManager->getStatus(); + if ($status !== null && !$status['running']) { + // Process has exited, read any remaining output + usleep(50000); // 50ms grace period for buffered output + if ($stdout !== null) { + $remaining = stream_get_contents($stdout); + if ($remaining !== false && $remaining !== '') { + $this->output .= $remaining; + } + } + if ($stderr !== null) { + $remaining = stream_get_contents($stderr); + if ($remaining !== false && $remaining !== '') { + $this->output .= "\n[STDERR]: " . $remaining; + } + } + break; + } + + usleep(10000); // 10ms sleep between checks + } + } + private function sendInput(string $input): void { // Check if input defines a class/function/trait/interface/enum diff --git a/tests/Acceptance/Support/ReplProcessManager.php b/tests/Acceptance/Support/ReplProcessManager.php index 640faca..024d5c7 100644 --- a/tests/Acceptance/Support/ReplProcessManager.php +++ b/tests/Acceptance/Support/ReplProcessManager.php @@ -80,6 +80,14 @@ public function isRunning(): bool return $this->process !== null; } + public function getStatus(): ?array + { + if ($this->process === null) { + return null; + } + return proc_get_status($this->process); + } + public function terminate(): void { if ($this->process !== null) { From 3983c65ff39fe07c4896cdebc3e35fb7f414f277 Mon Sep 17 00:00:00 2001 From: Marcello Duarte Date: Sat, 13 Dec 2025 17:51:21 +0000 Subject: [PATCH 4/4] Simplify --- bin/phunkie | 61 +++-------------------------------------------------- 1 file changed, 3 insertions(+), 58 deletions(-) diff --git a/bin/phunkie b/bin/phunkie index 8f557c2..21a5784 100755 --- a/bin/phunkie +++ b/bin/phunkie @@ -86,71 +86,16 @@ use function Phunkie\Console\Functions\{setColors, printBanner, loadHistory, sav if (isset($args[1]) && !str_starts_with($args[1], '-')) { $className = $args[1]; - // Try to load the class file directly if it exists (for dynamically created files) - // This handles cases where composer's autoloader cache doesn't see new files - // Convert namespace to path, handling the Tests -> tests directory mapping - $classFile = str_replace('\\', '/', $className) . '.php'; - $testsRelativePath = str_replace('Tests/', '', $classFile); - - $possiblePaths = [ - // Most likely: tests directory with lowercase 't' (Linux/CI compatible) - getcwd() . '/tests/' . $testsRelativePath, - __DIR__ . '/../tests/' . $testsRelativePath, - // Also check uppercase Tests for compatibility - getcwd() . '/' . $classFile, - __DIR__ . '/../' . $classFile, - ]; - - $fileLoaded = false; - foreach ($possiblePaths as $path) { - if (file_exists($path) && !class_exists($className, false)) { - try { - require_once $path; - $fileLoaded = true; - } catch (\Throwable $e) { - echo "[DEBUG] Error loading $path: " . get_class($e) . ": " . $e->getMessage() . "\n"; - } - break; - } - } - - // Debug: check class status after loading attempt - $classExists = class_exists($className, true); - $isIOApp = $classExists && is_subclass_of($className, IOApp::class); - - if (!$classExists) { - echo "[DEBUG] Class $className not found after loading attempt. fileLoaded=$fileLoaded\n"; - echo "[DEBUG] Checked paths:\n"; - foreach ($possiblePaths as $path) { - echo "[DEBUG] $path exists=" . (file_exists($path) ? 'yes' : 'no') . "\n"; - } - } - - if ($classExists && $isIOApp) { - try { - $app = new $className(); - } catch (\Throwable $e) { - echo "[DEBUG] Error instantiating $className: " . get_class($e) . ": " . $e->getMessage() . "\n"; - } - } elseif ($classExists && !$isIOApp) { - echo "[DEBUG] Class $className exists but is not an IOApp\n"; + if (class_exists($className) && is_subclass_of($className, IOApp::class)) { + $app = new $className(); } } if ($app === null) { - if (isset($className)) { - echo "[DEBUG] Falling back to PhunkieConsole (requested class: $className)\n"; - } $app = new PhunkieConsole(); } - try { - $app->run($args)->unsafeRun(); - } catch (\Throwable $e) { - echo "[DEBUG] Exception during run: " . get_class($e) . ": " . $e->getMessage() . "\n"; - echo "[DEBUG] In " . $e->getFile() . ":" . $e->getLine() . "\n"; - exit(1); - } + $app->run($args)->unsafeRun(); })();