From 7e30b0b2236757006ce8abe20495abe0efd04cde Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 8 Nov 2026 03:55:19 +0000 Subject: [PATCH 1/4] Fix root TestRunTests pipe spawn, halt CRLF, and opt-in drivers - TestMain: run .php/.zy script args so PHP_BINARY (go test binary) works in proc_open - TestPHPFixtureProcess/TestRunTests: load mongodb/redis for opt-in PHP regressions; skip Redis integration without ORIGAMI_REDIS_TEST_PORT - halt_compiler_uppercase: compare payload with CRLF normalized to LF - Extract loadDefaultRuntime for suite and CLI; add focused proc_open child tests Co-authored-by: selden --- php_binary_child_test.go | 67 ++++++++++++++++++++++ run_tests_test.go | 58 +++++++++++++++++++ runtime_loader.go | 26 +++++++++ tests/php/halt_compiler_uppercase_test.php | 5 +- zy.go | 22 +------ 5 files changed, 156 insertions(+), 22 deletions(-) create mode 100644 php_binary_child_test.go create mode 100644 runtime_loader.go diff --git a/php_binary_child_test.go b/php_binary_child_test.go new file mode 100644 index 00000000..0ba8db43 --- /dev/null +++ b/php_binary_child_test.go @@ -0,0 +1,67 @@ +package main + +import ( + "bytes" + "context" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + "time" +) + +// PHP_BINARY must run scripts when invoked as [binary, path.php] (go test / Windows proc_open). +func TestPHPBinaryRunsChildScript(t *testing.T) { + executable, err := os.Executable() + if err != nil { + t.Fatal(err) + } + child := filepath.Join(t.TempDir(), "stream-child.php") + script := ` Date: Sun, 8 Nov 2026 05:15:53 +0000 Subject: [PATCH 2/4] Fix CI: keep loadDefaultRuntime in zy.go for single-file go build exit_status_test.php and docs use go build ./zy.go; splitting runtime_loader.go broke that convention (undefined loadDefaultRuntime on darwin matrix). Co-authored-by: selden --- runtime_loader.go | 26 -------------------------- zy.go | 22 ++++++++++++++++++++++ 2 files changed, 22 insertions(+), 26 deletions(-) delete mode 100644 runtime_loader.go diff --git a/runtime_loader.go b/runtime_loader.go deleted file mode 100644 index 79b69530..00000000 --- a/runtime_loader.go +++ /dev/null @@ -1,26 +0,0 @@ -package main - -import ( - "github.com/php-any/origami/data" - "github.com/php-any/origami/std" - "github.com/php-any/origami/std/laravel/framework" - netannotation "github.com/php-any/origami/std/net/annotation" - "github.com/php-any/origami/std/net/grpc" - "github.com/php-any/origami/std/net/http" - "github.com/php-any/origami/std/net/tcp" - "github.com/php-any/origami/std/net/websocket" - "github.com/php-any/origami/std/php" - "github.com/php-any/origami/std/system" -) - -func loadDefaultRuntime(vm data.VM) { - std.Load(vm) - framework.Load(vm) - php.Load(vm) - http.Load(vm) - websocket.Load(vm) - tcp.Load(vm) - grpc.Load(vm) - netannotation.Load(vm) - system.Load(vm) -} diff --git a/zy.go b/zy.go index c1536f4a..4e0e1fd8 100644 --- a/zy.go +++ b/zy.go @@ -7,8 +7,30 @@ import ( _ "modernc.org/sqlite" "github.com/php-any/origami/cmd" + "github.com/php-any/origami/data" + "github.com/php-any/origami/std" + "github.com/php-any/origami/std/laravel/framework" + netannotation "github.com/php-any/origami/std/net/annotation" + "github.com/php-any/origami/std/net/grpc" + "github.com/php-any/origami/std/net/http" + "github.com/php-any/origami/std/net/tcp" + "github.com/php-any/origami/std/net/websocket" + "github.com/php-any/origami/std/php" + "github.com/php-any/origami/std/system" ) +func loadDefaultRuntime(vm data.VM) { + std.Load(vm) + framework.Load(vm) + php.Load(vm) + http.Load(vm) + websocket.Load(vm) + tcp.Load(vm) + grpc.Load(vm) + netannotation.Load(vm) + system.Load(vm) +} + func init() { cmd.SetRuntimeLoader(loadDefaultRuntime) } From 274d34d25323967cad023e83a861f8f0e0d12571 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 8 Nov 2026 05:33:44 +0000 Subject: [PATCH 3/4] Fix mkdir default mode: parse 0777 as octal not decimal 777 NewIntLiteral turned default permissions into mode 0o1411 (non-writable); use NewNumberLiteral so omitted-arg mkdir matches PHP 0777. Co-authored-by: selden --- std/php/core/mkdir.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/std/php/core/mkdir.go b/std/php/core/mkdir.go index 50aebe20..2dc9efe0 100644 --- a/std/php/core/mkdir.go +++ b/std/php/core/mkdir.go @@ -68,7 +68,7 @@ func (f *MkdirFunction) GetName() string { var mkdirFunctionGetParams = []data.GetValue{ node.NewParameter(nil, "directory", 0, nil, nil), - node.NewParameter(nil, "permissions", 1, node.NewIntLiteral(nil, "0777"), nil), + node.NewParameter(nil, "permissions", 1, node.NewNumberLiteral(nil, "0777"), nil), node.NewParameter(nil, "recursive", 2, nil, nil), } From b3b5c2a5529847ad341ef46dc54fc53aa3006cd4 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 8 Nov 2026 05:59:15 +0000 Subject: [PATCH 4/4] Stabilize tcp maxConnections release test under load Wait for first handler to return before redialing; retry until the replacement callback starts so CI does not hang for the package deadline. Co-authored-by: selden --- std/net/tcp/tcp_test.go | 44 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/std/net/tcp/tcp_test.go b/std/net/tcp/tcp_test.go index 8353d3bc..66d1b4a7 100644 --- a/std/net/tcp/tcp_test.go +++ b/std/net/tcp/tcp_test.go @@ -4,6 +4,7 @@ import ( "context" "io" "net" + "sync" "testing" "time" ) @@ -18,20 +19,35 @@ func TestReadTimeoutAndCancellationReleaseConnection(t *testing.T) { defer cancel() done := make(chan error, 1) entered := make(chan struct{}, 2) + firstHandlerDone := make(chan struct{}) + var recordFirst sync.Once go func() { done <- serve(ctx, ln, map[string]int{"maxConnections": 1, "readTimeoutMs": 100, "writeTimeoutMs": 100, "connectionTimeoutMs": 1000}, func(ctx context.Context, c net.Conn) error { entered <- struct{}{} + releaseSlot := false + recordFirst.Do(func() { releaseSlot = true }) + if releaseSlot { + defer close(firstHandlerDone) + } b := make([]byte, 1) _, e := c.Read(b) return e }) }() + waitEntered := func(label string) { + t.Helper() + select { + case <-entered: + case <-time.After(3 * time.Second): + t.Fatalf("%s: callback did not start", label) + } + } c, e := net.DialTimeout("tcp", ln.Addr().String(), time.Second) if e != nil { t.Fatal(e) } defer c.Close() - <-entered + waitEntered("first connection") other, e := net.DialTimeout("tcp", ln.Addr().String(), time.Second) if e != nil { t.Fatal(e) @@ -45,12 +61,30 @@ func TestReadTimeoutAndCancellationReleaseConnection(t *testing.T) { if _, e = c.Read(make([]byte, 1)); e != io.EOF { t.Fatalf("idle connection not closed: %v", e) } - next, e := net.DialTimeout("tcp", ln.Addr().String(), time.Second) - if e != nil { - t.Fatal(e) + select { + case <-firstHandlerDone: + case <-time.After(3 * time.Second): + t.Fatal("first handler did not finish; maxConnections slot still held") + } + _ = c.Close() + var next net.Conn + replacementBy := time.Now().Add(5 * time.Second) + for next == nil && time.Now().Before(replacementBy) { + conn, dialErr := net.DialTimeout("tcp", ln.Addr().String(), 250*time.Millisecond) + if dialErr != nil { + continue + } + select { + case <-entered: + next = conn + case <-time.After(400 * time.Millisecond): + conn.Close() + } + } + if next == nil { + t.Fatal("replacement connection: callback did not start") } defer next.Close() - <-entered cancel() select { case e := <-done: