Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions php_binary_child_test.go
Original file line number Diff line number Diff line change
@@ -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 := `<?php $out = fopen('php://stdout', 'w'); fwrite($out, 'first'); usleep(150000); fwrite($out, 'second'); fclose($out);`
if err := os.WriteFile(child, []byte(script), 0644); err != nil {
t.Fatal(err)
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
proc := exec.CommandContext(ctx, executable, child)
var out bytes.Buffer
proc.Stdout = &out
proc.Stderr = &out
if err := proc.Run(); err != nil {
t.Fatalf("child proc: %v\n%s", err, out.Bytes())
}
if got := out.String(); got != "firstsecond" {
t.Fatalf("child stdout %q, want firstsecond", got)
}
}

func TestPHPFixtureProcess_streamPipeLength(t *testing.T) {
fixture := filepath.Join("tests", "php", "stream_get_contents_pipe_length_test.php")
t.Setenv("ORIGAMI_SUITE_SCRIPT", fixture)
TestPHPFixtureProcess(t)
}

func TestDirectScriptArgDetection(t *testing.T) {
if _, ok := directScriptArgFromArgv(); ok {
t.Skip("running under direct-script invocation")
}
path := filepath.Join(t.TempDir(), "probe.php")
if err := os.WriteFile(path, []byte(`<?php echo "ok\n";`), 0644); err != nil {
t.Fatal(err)
}
executable, err := os.Executable()
if err != nil {
t.Fatal(err)
}
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
proc := exec.CommandContext(ctx, executable, path)
out, err := proc.CombinedOutput()
if err != nil {
t.Fatalf("%v\n%s", err, out)
}
if !strings.Contains(string(out), "ok") {
t.Fatalf("unexpected output: %q", out)
}
}
58 changes: 58 additions & 0 deletions run_tests_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,70 @@ import (
"time"

"github.com/php-any/origami/cmd"
"github.com/php-any/origami/data"
"github.com/php-any/origami/std/php/mongodb"
"github.com/php-any/origami/std/php/redis"
)

var suiteOptInExtensions = map[string]func(data.VM){
"mongodb_bson_driver_test.php": mongodb.Load,
"redis_driver_contract_test.php": redis.Load,
"redis_driver_integration_test.php": redis.Load,
"redis_phpredis_features_test.php": redis.Load,
}

var redisIntegrationScripts = map[string]bool{
"redis_driver_integration_test.php": true,
"redis_phpredis_features_test.php": true,
}

func directScriptArgFromArgv() (string, bool) {
for _, arg := range os.Args[1:] {
if strings.HasPrefix(arg, "-") {
continue
}
if cmd.IsDirectScriptArg(arg) {
return arg, true
}
break
}
return "", false
}

func runtimeLoaderForSuiteScript(scriptPath string) func(data.VM) {
base := filepath.Base(scriptPath)
optIn := suiteOptInExtensions[base]
return func(vm data.VM) {
loadDefaultRuntime(vm)
if optIn != nil {
optIn(vm)
}
}
}

func TestMain(m *testing.M) {
// proc_open([PHP_BINARY, script.php]) uses the test binary as PHP_BINARY; run the script directly.
if script, ok := directScriptArgFromArgv(); ok {
if err := cmd.RunScriptFile(script); err != nil {
os.Exit(1)
}
os.Exit(0)
}
os.Exit(m.Run())
}

// Each script is a separate CLI request. Sharing one VM changes declarations,
// handlers, argv and globals, and cannot test an uncaught-exception handler.
func TestPHPFixtureProcess(t *testing.T) {
file := os.Getenv("ORIGAMI_SUITE_SCRIPT")
if file == "" {
return
}
base := filepath.Base(file)
if redisIntegrationScripts[base] && os.Getenv("ORIGAMI_REDIS_TEST_PORT") == "" {
t.Skip("set ORIGAMI_REDIS_TEST_PORT for real Redis integration")
}
cmd.SetRuntimeLoader(runtimeLoaderForSuiteScript(file))
os.Args = []string{os.Args[0], file}
if err := cmd.RunScriptFile(file); err != nil {
t.Fatal(err)
Expand Down Expand Up @@ -56,6 +111,9 @@ func TestRunTests(t *testing.T) {
if runtime.GOOS == "windows" && unix[name] {
t.Skip("requires Unix /bin/echo and stty; run suite on Linux")
}
if redisIntegrationScripts[name] && os.Getenv("ORIGAMI_REDIS_TEST_PORT") == "" {
t.Skip("set ORIGAMI_REDIS_TEST_PORT for real Redis integration")
}
request, cancel := context.WithTimeout(context.Background(), 90*time.Second)
defer cancel()
process := exec.CommandContext(request, executable, "-test.run=^TestPHPFixtureProcess$")
Expand Down
44 changes: 39 additions & 5 deletions std/net/tcp/tcp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"io"
"net"
"sync"
"testing"
"time"
)
Expand All @@ -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)
Expand All @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion std/php/core/mkdir.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}

Expand Down
5 changes: 4 additions & 1 deletion tests/php/halt_compiler_uppercase_test.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
<?php
$source = file_get_contents(__FILE__);
$tail = substr($source, __COMPILER_HALT_OFFSET__);
if ($tail !== "ARCHIVE_PAYLOAD\n<?php declare(strict_types=1);\n") {
// Worktree checkout may use CRLF on Windows; the halt offset is byte-accurate either way.
$normalized = str_replace("\r\n", "\n", $tail);
$expected = "ARCHIVE_PAYLOAD\n<?php declare(strict_types=1);\n";
if ($normalized !== $expected) {
throw new \Exception('Incorrect compiler halt byte offset: ' . bin2hex($tail));
}
echo "halt compiler uppercase passed\n";
Expand Down
24 changes: 13 additions & 11 deletions zy.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,20 @@ import (
"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(func(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)
})
cmd.SetRuntimeLoader(loadDefaultRuntime)
}

func main() {
Expand Down
Loading