Skip to content

Commit 53b6e8c

Browse files
Merge pull request #4 from phunkie/developing-1.0.0
Developing 1.0.0
2 parents 2ddd79c + 13b8432 commit 53b6e8c

33 files changed

Lines changed: 1324 additions & 630 deletions

.github/workflows/ci.yml

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
name: Tests
2+
3+
on:
4+
push:
5+
branches: [ main, develop ]
6+
pull_request:
7+
branches: [ main, develop ]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
strategy:
13+
fail-fast: false
14+
matrix:
15+
php-version: ['8.2', '8.3', '8.4']
16+
name: PHP ${{ matrix.php-version }}
17+
steps:
18+
- uses: actions/checkout@v4
19+
20+
- name: Setup PHP
21+
uses: shivammathur/setup-php@v2
22+
with:
23+
php-version: ${{ matrix.php-version }}
24+
extensions: mbstring, readline
25+
coverage: none
26+
- name: Validate composer.json and composer.lock
27+
run: composer validate --strict
28+
- name: Unset local path repositories
29+
run: |
30+
composer config --unset repositories
31+
rm composer.lock
32+
- name: Cache Composer packages
33+
id: composer-cache
34+
uses: actions/cache@v3
35+
with:
36+
path: vendor
37+
key: ${{ runner.os }}-php-${{ matrix.php-version }}-${{ hashFiles('**/composer.lock') }}
38+
restore-keys: |
39+
${{ runner.os }}-php-${{ matrix.php-version }}-
40+
- name: Install dependencies
41+
run: composer update --prefer-dist --no-progress
42+
- name: Run PHPUnit tests
43+
run: ./vendor/bin/phpunit --testdox
44+
- name: Run Behat tests (version-aware)
45+
run: ./bin/run-behat-tests.sh
46+
47+
lint:
48+
name: PHP ${{ matrix.php-version }} Code Style (PHP-CS-Fixer)
49+
runs-on: ubuntu-latest
50+
strategy:
51+
fail-fast: false
52+
matrix:
53+
php-version: ['8.2', '8.3', '8.4']
54+
steps:
55+
- uses: actions/checkout@v4
56+
- name: Setup PHP
57+
uses: shivammathur/setup-php@v2
58+
with:
59+
php-version: ${{ matrix.php-version }}
60+
extensions: mbstring, readline
61+
coverage: none
62+
tools: php-cs-fixer
63+
- name: Run PHP-CS-Fixer
64+
run: php-cs-fixer fix --dry-run --diff --verbose --allow-risky=yes
65+
66+
static-analysis:
67+
name: PHP ${{ matrix.php-version }} Static Analysis (PHPStan)
68+
runs-on: ubuntu-latest
69+
strategy:
70+
fail-fast: false
71+
matrix:
72+
php-version: ['8.2', '8.3', '8.4']
73+
steps:
74+
- uses: actions/checkout@v4
75+
- name: Unset local path repositories
76+
run: |
77+
composer config --unset repositories
78+
rm composer.lock
79+
- name: Setup PHP
80+
uses: shivammathur/setup-php@v2
81+
with:
82+
php-version: ${{ matrix.php-version }}
83+
extensions: mbstring, readline
84+
coverage: none
85+
- name: Install dependencies
86+
run: composer update --prefer-dist --no-progress
87+
- name: Run PHPStan
88+
run: vendor/bin/phpstan analyse src

.github/workflows/tests.yml

Lines changed: 0 additions & 49 deletions
This file was deleted.

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ vendor/
44
.phpunit.cache
55
phpunit.xml
66
.claude
7-
.specs
7+
.specs
8+
.php-cs-fixer.cache

.php-cs-fixer.dist.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use PhpCsFixer\Config;
6+
use PhpCsFixer\Finder;
7+
use PhpCsFixer\Runner\Parallel\ParallelConfigFactory;
8+
9+
return (new Config())
10+
->setParallelConfig(ParallelConfigFactory::detect()) // @TODO 4.0 no need to call this manually
11+
->setRiskyAllowed(false)
12+
->setRules([
13+
'@auto' => true
14+
])
15+
// 💡 by default, Fixer looks for `*.php` files excluding `./vendor/` - here, you can groom this config
16+
->setFinder(
17+
(new Finder())
18+
// 💡 root folder to check
19+
->in(__DIR__)
20+
// 💡 additional files, eg bin entry file
21+
// ->append([__DIR__.'/bin-entry-file'])
22+
// 💡 folders to exclude, if any
23+
// ->exclude([/* ... */])
24+
// 💡 path patterns to exclude, if any
25+
// ->notPath([/* ... */])
26+
// 💡 extra configs
27+
// ->ignoreDotFiles(false) // true by default in v3, false in v4 or future mode
28+
// ->ignoreVCSIgnored(true) // true by default
29+
)
30+
;

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Phunkie Console
22

3-
[![Tests](https://github.com/phunkie/console/workflows/Tests/badge.svg)](https://github.com/phunkie/console/actions)
3+
[![CI](https://github.com/phunkie/console/actions/workflows/ci.yml/badge.svg)](https://github.com/phunkie/console/actions)
44
[![PHP Version](https://img.shields.io/packagist/php-v/phunkie/console?color=8892BF)](https://packagist.org/packages/phunkie/console)
55
[![Latest Stable Version](https://img.shields.io/packagist/v/phunkie/console)](https://packagist.org/packages/phunkie/console)
66
[![Total Downloads](https://img.shields.io/packagist/dt/phunkie/console)](https://packagist.org/packages/phunkie/console)

THOUGHTS.md

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
# Stream Reading Improvements for CI Testing
2+
3+
## Problem Statement
4+
Tests fail on CI (GitHub Actions) because `stream_select` behavior differs from local environment. The REPL process outputs to stdout, but tests only capture the banner and miss command output. This is likely due to:
5+
- GitHub Actions TTY limitations
6+
- Stderr writes that unblock select
7+
- Different buffering behavior in CI
8+
9+
## Core Considerations
10+
11+
### 1. ✅ Fix the Loop Logic (DONE)
12+
**Problem**: Current loop exits on first timeout without a prompt, even with longer timeouts.
13+
**Solution**: Continue polling on timeout instead of breaking. Use overall timeout guard.
14+
**Status**: Implemented in `ReplOutputReader::readOutput()`
15+
- Changed `break` to `continue` on timeout without prompt
16+
- Added overall timeout tracking
17+
18+
### 2. ✅ Environment Configuration (DONE)
19+
**Solution**: Use .env for local, .env.test for CI with configurable timeouts
20+
**Status**: Implemented
21+
- `.env` with 1.5s timeout (local, gitignored)
22+
- `.env.test` with 5.0s timeout (CI, tracked)
23+
- ReplOutputReader reads from environment
24+
25+
### 3. ✅ Read from Both stdout AND stderr (DONE)
26+
**Problem**: GitHub Actions might write to stderr, causing stream_select to unblock, but we only read stdout.
27+
**Solution**:
28+
- Pass both `$pipes[1]` (stdout) and `$pipes[2]` (stderr) to stream_select
29+
- Read from whichever is ready
30+
- Only append stdout to output (discard stderr noise, or log it)
31+
**Status**: IMPLEMENTED in `ReplOutputReader::readOutput()`
32+
- Modified signature to accept `$stderrStream` parameter
33+
- Loop reads from both streams
34+
- stderr output logged but not included in return value
35+
- Updated `ReplSteps::readOutput()` to pass stderr
36+
37+
### 4. ✅ Debug Logging for CI (DONE)
38+
**Problem**: Can't see what's happening in CI without visibility, but don't want noise on passing tests
39+
**Solution**: Add conditional debug logging to ReplOutputReader
40+
**Status**: IMPLEMENTED
41+
- Buffers log messages instead of immediately outputting
42+
- Only outputs logs when:
43+
- `REPL_DEBUG=true` in .env (always log)
44+
- Timeout occurs with no output (indicates failure)
45+
- stream_select error occurs
46+
- Keeps local test output clean (.env has `REPL_DEBUG=false`)
47+
- CI gets full logs (.env.test has `REPL_DEBUG=true`)
48+
- Added to `ReplProcessManager::sendInput()` as well
49+
50+
### 5. ✅ Process Management Review (DONE)
51+
**Current**: Using `proc_open` with pipes, streams set to non-blocking
52+
**Considerations**:
53+
- After writing to stdin (`$pipes[0]`), explicitly `fflush($pipes[0])`
54+
- Consider `fclose($pipes[0])` after all input to signal EOF (may not be appropriate for REPL)
55+
- Ensure both stdout and stderr are non-blocking
56+
**Status**: REVIEWED - Already doing `fflush()` after writes
57+
- Added logging to `sendInput()` to verify bytes written
58+
59+
### 6. 🔮 Sentinel/Test Mode Approach (FUTURE)
60+
**Alternative**: Instead of guessing prompts, have REPL print a sentinel in test mode
61+
```php
62+
// In test mode, REPL prints:
63+
echo "COMMAND_DONE_MARKER\n";
64+
```
65+
Then look for sentinel instead of prompt patterns.
66+
**Status**: NOT IMPLEMENTED - Requires REPL changes
67+
**Priority**: LOW - Nice to have, but invasive
68+
69+
### 7. 🔮 Phunkie Streams Solution (FUTURE)
70+
**Idea**: Extract this into a reusable Phunkie Streams component
71+
- `Stream\Process\asyncRead($stream, $predicate, $timeout)`
72+
- Handles non-blocking reads, multiple streams, sentinel detection
73+
**Status**: NOT IMPLEMENTED
74+
**Priority**: LOW - After we solve the immediate problem
75+
76+
## Action Items (Prioritized)
77+
78+
### Immediate (Baby Steps)
79+
1. ✅ Fix loop to continue polling instead of breaking on timeout
80+
2. ✅ Add .env configuration for timeouts
81+
3. ✅ Modify `ReplOutputReader::readOutput()` to accept stderr stream
82+
4. ✅ Update `ReplSteps` to pass stderr to `readOutput()`
83+
5. ✅ Read from both streams in the loop, only append stdout to output
84+
6. ✅ Add debug logging (error_log) to see what's happening in CI
85+
7. ✅ Review `ReplProcessManager` for proper fflush() usage
86+
8.**NEXT**: Test locally to verify changes don't break existing tests
87+
9.**NEXT**: Test on CI with full test suite
88+
89+
### Long Term
90+
10. Review CI logs to see if stderr reading solves the issue
91+
11. Consider reducing timeouts if tests are passing consistently
92+
12. Consider sentinel-based approach for more reliable testing
93+
13. Extract pattern into Phunkie Streams if successful
94+
95+
## Notes
96+
- **Don't just increase timeouts** - that makes CI slow and doesn't address root cause
97+
- **Stderr reading is likely the key** - GitHub Actions might be writing to stderr
98+
- **Keep local tests fast** - use .env for short timeouts locally
99+
- **Baby steps** - implement one thing at a time and test
100+
101+
## Current Status
102+
- Loop logic fixed ✅
103+
- Environment config added ✅
104+
- Stderr reading implemented ✅
105+
- Debug logging implemented with buffering ✅
106+
- **SENTINEL APPROACH IMPLEMENTED**
107+
- **LOCAL TESTS**: All 418 scenarios, 2056 steps PASSING in ~18s ✅
108+
- **NEXT**: Test on CI to verify sentinel approach works in GitHub Actions
109+
110+
## Test Results
111+
- **Local (PHP 8.4)**: 418 scenarios, 2056 steps - ALL PASSED ✅
112+
- **CI**: Pending - need to push and test
113+
114+
## Sentinel Approach + Blocking Read Strategy
115+
116+
### The Problem
117+
GitHub Actions has different TTY/buffering behavior than local environments. The original non-blocking, short-timeout polling approach with `stream_select` would break too aggressively on timeouts, exiting the read loop before all data arrived. This caused tests to only capture the REPL banner, missing actual command output.
118+
119+
### The Solution: Two-Part Fix
120+
121+
#### Part 1: Sentinel Marker (Explicit Ready Signal)
122+
Instead of guessing when the REPL is ready by detecting prompt patterns, the REPL explicitly prints `__PHUNKIE_READY__` when ready for input.
123+
124+
**REPL side** (`src/Repl/ReplLoop.php`):
125+
- Added `isTestMode()` to check `REPL_TEST_MODE` environment variable
126+
- Added `printTestSentinel()` that prints `__PHUNKIE_READY__\n` in test mode
127+
- Called before each prompt in `replLoopTrampoline()`
128+
129+
**Test side**:
130+
- `ReplProcessManager` loads `.env` and passes `REPL_TEST_MODE` to child process
131+
- `ReplOutputReader` detects sentinel instead of prompt patterns in test mode
132+
- Sentinel is stripped from output before returning to tests
133+
- `ReplSteps.waitForPrompt()` uses `ReplOutputReader` for consistent detection
134+
135+
#### Part 2: Blocking Read Strategy (Patience Over Speed)
136+
Changed from aggressive timeout-based polling to blocking reads that wait naturally for data.
137+
138+
**Key changes in `ReplOutputReader::readOutput()`**:
139+
1. **Longer `stream_select` timeout**: Up to 1 second (vs 50ms) to let data arrive naturally
140+
2. **No early exits on timeout**: Only breaks when:
141+
- Prompt/sentinel found ✅
142+
- Overall timeout hit ⏱️
143+
- EOF/error encountered ❌
144+
3. **Calculated remaining timeout**: Uses `min(1.0, remainingTimeout)` for smart waiting
145+
4. **Smaller read chunks**: 4096 bytes for more incremental reading
146+
5. **Proper error handling**: Detects EOF and read errors explicitly
147+
148+
**Before (aggressive)**:
149+
```php
150+
// 50ms timeout on stream_select
151+
$result = @stream_select($read, $write, $except, 0, 50000);
152+
if ($result === 0) {
153+
break; // ❌ Exits too early!
154+
}
155+
```
156+
157+
**After (patient)**:
158+
```php
159+
// Up to 1 second timeout, calculated from remaining time
160+
$selectTimeout = min(1.0, $remainingTimeout);
161+
$result = @stream_select($read, $write, $except, $selectTimeoutSec, $selectTimeoutUsec);
162+
if ($result === 0) {
163+
if (self::endsWithPrompt($output)) {
164+
break; // ✅ Only exit if we have complete response
165+
}
166+
continue; // ✅ Keep waiting for data
167+
}
168+
```
169+
170+
### Benefits
171+
-**Reliable in CI**: Doesn't break early when I/O is slow
172+
-**Explicit ready signal**: Sentinel removes guesswork
173+
-**Fast locally**: ~18 seconds for 418 scenarios
174+
-**Handles slow environments**: Waits patiently up to overall timeout
175+
-**Clean output**: Sentinel stripped automatically
176+
-**Proper error handling**: Detects EOF and errors gracefully
177+
178+
### Configuration
179+
- `.env` (local): `REPL_TEST_MODE=true`, `REPL_OUTPUT_TIMEOUT=1.5`
180+
- `.env.test` (CI): `REPL_TEST_MODE=true`, `REPL_OUTPUT_TIMEOUT=5.0`
181+
182+
### Files Modified
183+
- `src/Repl/ReplLoop.php` - Sentinel printing
184+
- `tests/Acceptance/Support/ReplProcessManager.php` - Environment passing
185+
- `tests/Acceptance/Support/ReplOutputReader.php` - **Blocking read strategy + sentinel detection**
186+
- `tests/Acceptance/ReplSteps.php` - Use ReplOutputReader in test mode
187+
- `.env` and `.env.test` - Configuration

behat.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@
88
return (new Config())
99
->withProfile(
1010
(new Profile('default'))
11-
->withSuite((new Suite('default'))
11+
->withSuite(
12+
(new Suite('default'))
1213
->withContexts(ReplSteps::class)
13-
)
14+
)
1415
)
1516
;

bin/phpstan

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/Users/md/code/phunkie/console/vendor/bin/phpstan

0 commit comments

Comments
 (0)