Skip to content

Commit 1c8ffd0

Browse files
committed
+ Support ob_start with coroutines and tests
1 parent 819c461 commit 1c8ffd0

File tree

7 files changed

+316
-0
lines changed

7 files changed

+316
-0
lines changed
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
--TEST--
2+
Output Buffer: Basic ob_start isolation with suspend
3+
--FILE--
4+
<?php
5+
6+
use function Async\spawn;
7+
use function Async\suspend;
8+
9+
echo "Main starts\n";
10+
11+
ob_start();
12+
echo "Main buffer before";
13+
14+
spawn(function() {
15+
echo "Coroutine starts\n";
16+
17+
ob_start();
18+
echo "Before suspend";
19+
20+
suspend(); // Coroutine loses control
21+
22+
echo " After suspend";
23+
$content = ob_get_contents();
24+
ob_end_clean();
25+
26+
echo "Got: '$content'\n";
27+
});
28+
29+
suspend(); // Main also loses control
30+
31+
echo " Main buffer after";
32+
$main_content = ob_get_contents();
33+
ob_end_clean();
34+
35+
echo "Main got: '$main_content'\n";
36+
37+
?>
38+
--EXPECT--
39+
Main starts
40+
Coroutine starts
41+
Main got: 'Main buffer before Main buffer after'
42+
Got: 'Before suspend After suspend'
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
--TEST--
2+
Output Buffer: Multiple coroutines with independent buffers
3+
--FILE--
4+
<?php
5+
6+
use function Async\spawn;
7+
use function Async\suspend;
8+
9+
echo "Start\n";
10+
11+
spawn(function() {
12+
ob_start();
13+
echo "Coroutine A: part1";
14+
15+
suspend(); // Switch context
16+
17+
echo " part2";
18+
$content = ob_get_contents();
19+
ob_end_clean();
20+
echo "A got: '$content'\n";
21+
});
22+
23+
spawn(function() {
24+
ob_start();
25+
echo "Coroutine B: part1";
26+
27+
suspend(); // Switch context
28+
29+
echo " part2";
30+
$content = ob_get_contents();
31+
ob_end_clean();
32+
echo "B got: '$content'\n";
33+
});
34+
35+
spawn(function() {
36+
ob_start();
37+
echo "Coroutine C: part1";
38+
39+
suspend(); // Switch context
40+
41+
echo " part2";
42+
$content = ob_get_contents();
43+
ob_end_clean();
44+
echo "C got: '$content'\n";
45+
});
46+
47+
echo "End\n";
48+
49+
?>
50+
--EXPECT--
51+
Start
52+
End
53+
A got: 'Coroutine A: part1 part2'
54+
B got: 'Coroutine B: part1 part2'
55+
C got: 'Coroutine C: part1 part2'
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
--TEST--
2+
Output Buffer: Nested ob_start in coroutines
3+
--FILE--
4+
<?php
5+
6+
use function Async\spawn;
7+
use function Async\suspend;
8+
9+
echo "Start\n";
10+
11+
spawn(function() {
12+
ob_start();
13+
echo "Level 1: ";
14+
15+
ob_start();
16+
echo "Level 2: ";
17+
18+
suspend(); // Context switch with nested buffers
19+
20+
echo "content";
21+
$level2 = ob_get_contents();
22+
ob_end_clean();
23+
24+
echo "Got level2: '$level2' ";
25+
$level1 = ob_get_contents();
26+
ob_end_clean();
27+
28+
echo "Got level1: '$level1'\n";
29+
});
30+
31+
echo "End\n";
32+
33+
?>
34+
--EXPECT--
35+
Start
36+
End
37+
Got level1: 'Level 1: Got level2: 'Level 2: content' '
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
--TEST--
2+
Output Buffer: ob_flush and ob_clean isolation
3+
--FILE--
4+
<?php
5+
6+
use function Async\spawn;
7+
use function Async\suspend;
8+
9+
echo "Start\n";
10+
11+
spawn(function() {
12+
ob_start();
13+
echo "Before flush\n";
14+
15+
suspend(); // Context switch
16+
17+
ob_flush(); // Should flush only this coroutine's buffer
18+
echo " After flush";
19+
20+
suspend(); // Another context switch
21+
22+
$content = ob_get_contents();
23+
ob_end_clean();
24+
echo "Remaining: '$content'\n";
25+
});
26+
27+
spawn(function() {
28+
ob_start();
29+
echo "Other coroutine";
30+
31+
suspend(); // Context switch
32+
33+
ob_clean(); // Should clean only this coroutine's buffer
34+
echo "After clean";
35+
36+
$content = ob_get_contents();
37+
ob_end_clean();
38+
echo "Got: '$content'\n";
39+
});
40+
41+
echo "End\n";
42+
43+
?>
44+
--EXPECT--
45+
Start
46+
End
47+
Before flush
48+
Got: 'After clean'
49+
Remaining: ' After flush'
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
--TEST--
2+
Output Buffer: Mixed buffered and non-buffered output
3+
--FILE--
4+
<?php
5+
6+
use function Async\spawn;
7+
use function Async\suspend;
8+
9+
echo "Main without buffer\n";
10+
11+
spawn(function() {
12+
echo "Coroutine 1 without buffer\n";
13+
14+
ob_start();
15+
echo "Now with buffer";
16+
17+
suspend(); // Context switch
18+
19+
echo " continued";
20+
$content = ob_get_contents();
21+
ob_end_clean();
22+
23+
echo "Buffered was: '$content'\n";
24+
});
25+
26+
spawn(function() {
27+
ob_start();
28+
echo "Coroutine 2 always buffered";
29+
30+
suspend(); // Context switch
31+
32+
echo " until end";
33+
$content = ob_get_contents();
34+
ob_end_clean();
35+
36+
echo "Got: '$content'\n";
37+
});
38+
39+
echo "Main still without buffer\n";
40+
41+
?>
42+
--EXPECT--
43+
Main without buffer
44+
Main still without buffer
45+
Coroutine 1 without buffer
46+
Buffered was: 'Now with buffer continued'
47+
Got: 'Coroutine 2 always buffered until end'
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
--TEST--
2+
Output Buffer: Exception handling with ob_start
3+
--FILE--
4+
<?php
5+
6+
use function Async\spawn;
7+
use function Async\suspend;
8+
9+
echo "Start\n";
10+
11+
spawn(function() {
12+
ob_start();
13+
echo "Before exception";
14+
15+
suspend(); // Context switch
16+
17+
try {
18+
throw new Exception("Test exception");
19+
} catch (Exception $e) {
20+
echo " Caught: " . $e->getMessage();
21+
}
22+
23+
$content = ob_get_contents();
24+
ob_end_clean();
25+
echo "Got: '$content'\n";
26+
});
27+
28+
spawn(function() {
29+
ob_start();
30+
echo "Normal coroutine";
31+
32+
suspend(); // Context switch
33+
34+
echo " continues";
35+
$content = ob_get_contents();
36+
ob_end_clean();
37+
echo "Got: '$content'\n";
38+
});
39+
40+
echo "End\n";
41+
42+
?>
43+
--EXPECT--
44+
Start
45+
End
46+
Got: 'Before exception Caught: Test exception'
47+
Got: 'Normal coroutine continues'

tests/output_buffer/README.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Output Buffer Isolation Tests
2+
3+
This directory contains tests for output buffer isolation between coroutines using `ob_start()` and related functions.
4+
5+
## Test Cases
6+
7+
### 001-ob_start_basic_isolation.phpt
8+
Tests basic isolation of `ob_start()` buffers between main coroutine and spawned coroutines with context switching via `suspend()`.
9+
10+
### 002-multiple_coroutines_isolation.phpt
11+
Tests that multiple coroutines can each have their own independent output buffers that don't interfere with each other.
12+
13+
### 003-nested_ob_start.phpt
14+
Tests nested `ob_start()` calls within a single coroutine to ensure the buffer stack is properly maintained across context switches.
15+
16+
### 004-ob_flush_isolation.phpt
17+
Tests `ob_flush()` and `ob_clean()` operations work correctly within each coroutine's isolated buffer context.
18+
19+
### 005-mixed_buffering.phpt
20+
Tests scenarios where some coroutines use output buffering and others don't, ensuring proper isolation.
21+
22+
### 006-exception_handling.phpt
23+
Tests that output buffer isolation works correctly even when exceptions are thrown within coroutines.
24+
25+
## Key Testing Scenarios
26+
27+
- **Context Switching**: All tests use `suspend()` to force context switches and verify buffer state is preserved
28+
- **Buffer Isolation**: Each coroutine should have completely independent output buffers
29+
- **State Preservation**: Buffer content should be maintained across context switches
30+
- **Nested Buffers**: Buffer stacks should be properly isolated per coroutine
31+
- **Exception Safety**: Buffer isolation should work even when exceptions occur
32+
33+
## Expected Behavior
34+
35+
When `ob_start()` is called within a coroutine, it should:
36+
1. Create an isolated output buffer for that coroutine
37+
2. Automatically register a context switch handler for the coroutine
38+
3. Save/restore buffer state when the coroutine loses/regains control
39+
4. Not interfere with output buffers in other coroutines or the main thread

0 commit comments

Comments
 (0)