|
1 | 1 | package frankenphp_test |
2 | 2 |
|
3 | 3 | import ( |
| 4 | + "errors" |
4 | 5 | "io" |
5 | 6 | "net/http/httptest" |
6 | 7 | "os" |
7 | | - "path/filepath" |
8 | | - "strings" |
9 | 8 | "testing" |
10 | | - "time" |
11 | 9 |
|
12 | 10 | "github.com/dunglas/frankenphp" |
13 | 11 | "github.com/stretchr/testify/assert" |
14 | 12 | "github.com/stretchr/testify/require" |
15 | 13 | ) |
16 | 14 |
|
17 | | -// serveTestRequest issues an HTTP request through frankenphp against the |
18 | | -// given script under testdata/ and returns the response body. ErrRejected |
19 | | -// is treated as a non-fatal outcome so worker-mode quirks don't fail tests |
20 | | -// that only care about the script's stdout. |
21 | | -func serveTestRequest(t *testing.T, testDataDir, script, query string) string { |
22 | | - t.Helper() |
23 | | - url := "http://example.com/" + script |
24 | | - if query != "" { |
25 | | - url += "?" + query |
26 | | - } |
27 | | - req := httptest.NewRequest("GET", url, nil) |
28 | | - fr, err := frankenphp.NewRequestWithContext(req, frankenphp.WithRequestDocumentRoot(testDataDir, false)) |
29 | | - require.NoError(t, err) |
30 | | - |
31 | | - w := httptest.NewRecorder() |
32 | | - err = frankenphp.ServeHTTP(w, fr) |
33 | | - if err != nil { |
34 | | - require.ErrorAs(t, err, &frankenphp.ErrRejected{}) |
35 | | - } |
36 | | - body, err := io.ReadAll(w.Result().Body) |
37 | | - require.NoError(t, err) |
38 | | - return string(body) |
39 | | -} |
40 | | - |
41 | | -// TestEnsureBackgroundWorkerBatch declares a single catch-all bg worker |
42 | | -// and ensures three distinct names from a single ensure() call. Each |
43 | | -// catch-all instance touches a per-name sentinel; the test asserts that |
44 | | -// all three appear, proving the array form started one worker per name. |
| 15 | +// TestEnsureBackgroundWorkerBatch ensures multiple workers in one call, |
| 16 | +// each publishing its own identity. Verifies the batch path (array arg) |
| 17 | +// shares one deadline across all workers. |
45 | 18 | func TestEnsureBackgroundWorkerBatch(t *testing.T) { |
46 | 19 | cwd, _ := os.Getwd() |
47 | 20 | testDataDir := cwd + "/testdata/" |
48 | | - tmp := t.TempDir() |
49 | 21 |
|
50 | 22 | require.NoError(t, frankenphp.Init( |
51 | | - frankenphp.WithWorkers("", testDataDir+"background-worker-named.php", 0, |
52 | | - frankenphp.WithWorkerBackground(), |
53 | | - frankenphp.WithWorkerMaxThreads(8), |
54 | | - frankenphp.WithWorkerEnv(map[string]string{"BG_SENTINEL_DIR": tmp}), |
55 | | - ), |
56 | | - frankenphp.WithNumThreads(8), |
| 23 | + frankenphp.WithWorkers("worker-a", testDataDir+"background-worker-named.php", 0, |
| 24 | + frankenphp.WithWorkerBackground()), |
| 25 | + frankenphp.WithWorkers("worker-b", testDataDir+"background-worker-named.php", 0, |
| 26 | + frankenphp.WithWorkerBackground()), |
| 27 | + frankenphp.WithWorkers("worker-c", testDataDir+"background-worker-named.php", 0, |
| 28 | + frankenphp.WithWorkerBackground()), |
| 29 | + frankenphp.WithNumThreads(6), |
57 | 30 | )) |
58 | 31 | t.Cleanup(frankenphp.Shutdown) |
59 | 32 |
|
60 | | - body := serveTestRequest(t, testDataDir, "background-worker-batch-ensure.php", "") |
61 | | - assert.Contains(t, body, "ok", "batch ensure script should echo ok, got: %q", body) |
| 33 | + req := httptest.NewRequest("GET", "http://example.com/background-worker-batch-ensure.php", nil) |
| 34 | + fr, err := frankenphp.NewRequestWithContext(req, frankenphp.WithRequestDocumentRoot(testDataDir, false)) |
| 35 | + require.NoError(t, err) |
62 | 36 |
|
63 | | - for _, name := range []string{"batch-a", "batch-b", "batch-c"} { |
64 | | - path := filepath.Join(tmp, name) |
65 | | - assert.Eventually(t, func() bool { |
66 | | - _, err := os.Stat(path) |
67 | | - return err == nil |
68 | | - }, 5*time.Second, 10*time.Millisecond, |
69 | | - "catch-all instance %q should have written its sentinel", name) |
| 37 | + w := httptest.NewRecorder() |
| 38 | + if err := frankenphp.ServeHTTP(w, fr); err != nil && !errors.As(err, &frankenphp.ErrRejected{}) { |
| 39 | + t.Fatalf("serve: %v", err) |
70 | 40 | } |
| 41 | + body, _ := io.ReadAll(w.Result().Body) |
| 42 | + out := string(body) |
| 43 | + |
| 44 | + assert.NotContains(t, out, "MISSING", "batch ensure should have started and published all workers:\n"+out) |
| 45 | + assert.Contains(t, out, "worker-a=worker-a") |
| 46 | + assert.Contains(t, out, "worker-b=worker-b") |
| 47 | + assert.Contains(t, out, "worker-c=worker-c") |
71 | 48 | } |
72 | 49 |
|
73 | | -// TestEnsureBackgroundWorkerBatchEmpty exercises the C-side validation |
74 | | -// that an empty array raises a ValueError before any worker is started. |
75 | | -// The fixture catches the throwable and echoes its class. |
| 50 | +// TestEnsureBackgroundWorkerBatchEmpty verifies that an empty array is |
| 51 | +// rejected with a clear error rather than silently succeeding. |
76 | 52 | func TestEnsureBackgroundWorkerBatchEmpty(t *testing.T) { |
77 | 53 | cwd, _ := os.Getwd() |
78 | 54 | testDataDir := cwd + "/testdata/" |
79 | 55 |
|
80 | 56 | require.NoError(t, frankenphp.Init( |
81 | | - frankenphp.WithWorkers("", testDataDir+"background-worker-named.php", 0, |
82 | | - frankenphp.WithWorkerBackground(), |
83 | | - ), |
84 | | - frankenphp.WithNumThreads(2), |
| 57 | + frankenphp.WithWorkers("bg", testDataDir+"background-worker-named.php", 0, |
| 58 | + frankenphp.WithWorkerBackground()), |
| 59 | + frankenphp.WithNumThreads(3), |
85 | 60 | )) |
86 | 61 | t.Cleanup(frankenphp.Shutdown) |
87 | 62 |
|
88 | | - body := serveTestRequest(t, testDataDir, "background-worker-batch-errors.php", "mode=empty") |
89 | | - assert.True(t, strings.Contains(body, "ValueError"), |
90 | | - "empty array should raise ValueError, got: %q", body) |
91 | | - assert.Contains(t, body, "must not be empty") |
| 63 | + php := `<?php |
| 64 | +try { |
| 65 | + frankenphp_ensure_background_worker([], 1.0); |
| 66 | + echo "FAIL no error"; |
| 67 | +} catch (ValueError $e) { |
| 68 | + echo "OK ", $e->getMessage(); |
| 69 | +}` |
| 70 | + tmp := testDataDir + "bg-batch-empty.php" |
| 71 | + require.NoError(t, os.WriteFile(tmp, []byte(php), 0644)) |
| 72 | + t.Cleanup(func() { _ = os.Remove(tmp) }) |
| 73 | + |
| 74 | + req := httptest.NewRequest("GET", "http://example.com/bg-batch-empty.php", nil) |
| 75 | + fr, _ := frankenphp.NewRequestWithContext(req, frankenphp.WithRequestDocumentRoot(testDataDir, false)) |
| 76 | + w := httptest.NewRecorder() |
| 77 | + _ = frankenphp.ServeHTTP(w, fr) |
| 78 | + body, _ := io.ReadAll(w.Result().Body) |
| 79 | + assert.Contains(t, string(body), "OK ") |
| 80 | + assert.Contains(t, string(body), "must not be empty") |
| 81 | + assert.NotContains(t, string(body), "FAIL") |
92 | 82 | } |
93 | 83 |
|
94 | | -// TestEnsureBackgroundWorkerBatchNonString verifies a non-string element |
95 | | -// raises a TypeError (PHP's standard for argument-type mismatches inside |
96 | | -// our parsed array). |
| 84 | +// TestEnsureBackgroundWorkerBatchNonString verifies array-entry type |
| 85 | +// validation: non-string elements produce a TypeError. |
97 | 86 | func TestEnsureBackgroundWorkerBatchNonString(t *testing.T) { |
98 | 87 | cwd, _ := os.Getwd() |
99 | 88 | testDataDir := cwd + "/testdata/" |
100 | 89 |
|
101 | 90 | require.NoError(t, frankenphp.Init( |
102 | | - frankenphp.WithWorkers("", testDataDir+"background-worker-named.php", 0, |
103 | | - frankenphp.WithWorkerBackground(), |
104 | | - ), |
105 | | - frankenphp.WithNumThreads(2), |
| 91 | + frankenphp.WithWorkers("bg", testDataDir+"background-worker-named.php", 0, |
| 92 | + frankenphp.WithWorkerBackground()), |
| 93 | + frankenphp.WithNumThreads(3), |
106 | 94 | )) |
107 | 95 | t.Cleanup(frankenphp.Shutdown) |
108 | 96 |
|
109 | | - body := serveTestRequest(t, testDataDir, "background-worker-batch-errors.php", "mode=nonstring") |
110 | | - assert.True(t, strings.Contains(body, "TypeError"), |
111 | | - "non-string element should raise TypeError, got: %q", body) |
112 | | -} |
113 | | - |
114 | | -// TestEnsureBackgroundWorkerBatchDuplicate verifies that duplicate names |
115 | | -// in the same batch are rejected as a ValueError, matching the e17577e |
116 | | -// reference behavior (no silent dedup). |
117 | | -func TestEnsureBackgroundWorkerBatchDuplicate(t *testing.T) { |
118 | | - cwd, _ := os.Getwd() |
119 | | - testDataDir := cwd + "/testdata/" |
120 | | - |
121 | | - require.NoError(t, frankenphp.Init( |
122 | | - frankenphp.WithWorkers("", testDataDir+"background-worker-named.php", 0, |
123 | | - frankenphp.WithWorkerBackground(), |
124 | | - ), |
125 | | - frankenphp.WithNumThreads(2), |
126 | | - )) |
127 | | - t.Cleanup(frankenphp.Shutdown) |
128 | | - |
129 | | - body := serveTestRequest(t, testDataDir, "background-worker-batch-errors.php", "mode=duplicate") |
130 | | - assert.True(t, strings.Contains(body, "ValueError"), |
131 | | - "duplicate name should raise ValueError, got: %q", body) |
132 | | - assert.Contains(t, body, "duplicate") |
| 97 | + php := `<?php |
| 98 | +try { |
| 99 | + frankenphp_ensure_background_worker(['bg', 42], 1.0); |
| 100 | + echo "FAIL no error"; |
| 101 | +} catch (TypeError $e) { |
| 102 | + echo "OK ", $e->getMessage(); |
| 103 | +}` |
| 104 | + tmp := testDataDir + "bg-batch-nonstring.php" |
| 105 | + require.NoError(t, os.WriteFile(tmp, []byte(php), 0644)) |
| 106 | + t.Cleanup(func() { _ = os.Remove(tmp) }) |
| 107 | + |
| 108 | + req := httptest.NewRequest("GET", "http://example.com/bg-batch-nonstring.php", nil) |
| 109 | + fr, _ := frankenphp.NewRequestWithContext(req, frankenphp.WithRequestDocumentRoot(testDataDir, false)) |
| 110 | + w := httptest.NewRecorder() |
| 111 | + _ = frankenphp.ServeHTTP(w, fr) |
| 112 | + body, _ := io.ReadAll(w.Result().Body) |
| 113 | + assert.Contains(t, string(body), "OK ") |
| 114 | + assert.Contains(t, string(body), "must contain only strings") |
| 115 | + assert.NotContains(t, string(body), "FAIL") |
133 | 116 | } |
134 | 117 |
|
135 | | -// TestBackgroundWorkerBgFlag asserts that a bg worker script sees |
136 | | -// $_SERVER['FRANKENPHP_WORKER_BACKGROUND'] === true. The fixture writes |
137 | | -// var_export() of the value to a sentinel so the test can read the exact |
138 | | -// PHP-level representation. |
139 | | -func TestBackgroundWorkerBgFlag(t *testing.T) { |
| 118 | +// TestBackgroundWorkerServerFlag confirms that a bg worker sees |
| 119 | +// FRANKENPHP_WORKER_BACKGROUND=true alongside FRANKENPHP_WORKER_NAME in |
| 120 | +// $_SERVER, so scripts can branch without checking every function |
| 121 | +// independently. |
| 122 | +func TestBackgroundWorkerServerFlag(t *testing.T) { |
140 | 123 | cwd, _ := os.Getwd() |
141 | 124 | testDataDir := cwd + "/testdata/" |
142 | 125 |
|
143 | | - tmp := t.TempDir() |
144 | | - sentinel := filepath.Join(tmp, "bg-flag.sentinel") |
145 | | - |
146 | 126 | require.NoError(t, frankenphp.Init( |
147 | | - frankenphp.WithWorkers("bg-flag", testDataDir+"background-worker-bg-flag.php", 1, |
148 | | - frankenphp.WithWorkerBackground(), |
149 | | - frankenphp.WithWorkerEnv(map[string]string{"BG_SENTINEL": sentinel}), |
150 | | - ), |
151 | | - frankenphp.WithNumThreads(2), |
| 127 | + frankenphp.WithWorkers("flag-worker", testDataDir+"background-worker-bg-flag.php", 1, |
| 128 | + frankenphp.WithWorkerBackground()), |
| 129 | + frankenphp.WithNumThreads(3), |
152 | 130 | )) |
153 | 131 | t.Cleanup(frankenphp.Shutdown) |
154 | 132 |
|
155 | | - require.Eventually(t, func() bool { |
156 | | - _, err := os.Stat(sentinel) |
157 | | - return err == nil |
158 | | - }, 5*time.Second, 10*time.Millisecond, |
159 | | - "bg worker should have written the FRANKENPHP_WORKER_BACKGROUND sentinel") |
| 133 | + // ensure() removes the race between Init returning and the eager |
| 134 | + // bg-worker thread reaching its first set_vars. |
| 135 | + php := `<?php |
| 136 | +frankenphp_ensure_background_worker('flag-worker'); |
| 137 | +$vars = frankenphp_get_vars('flag-worker'); |
| 138 | +echo 'name=', $vars['name'] ?? 'MISSING', "\n"; |
| 139 | +echo 'is_background=', var_export($vars['is_background'] ?? 'MISSING', true), "\n"; |
| 140 | +` |
| 141 | + tmp := testDataDir + "bg-flag-reader.php" |
| 142 | + require.NoError(t, os.WriteFile(tmp, []byte(php), 0644)) |
| 143 | + t.Cleanup(func() { _ = os.Remove(tmp) }) |
| 144 | + |
| 145 | + req := httptest.NewRequest("GET", "http://example.com/bg-flag-reader.php", nil) |
| 146 | + fr, _ := frankenphp.NewRequestWithContext(req, frankenphp.WithRequestDocumentRoot(testDataDir, false)) |
| 147 | + w := httptest.NewRecorder() |
| 148 | + _ = frankenphp.ServeHTTP(w, fr) |
| 149 | + body, _ := io.ReadAll(w.Result().Body) |
| 150 | + out := string(body) |
160 | 151 |
|
161 | | - contents, err := os.ReadFile(sentinel) |
162 | | - require.NoError(t, err) |
163 | | - assert.Equal(t, "true", string(contents), |
164 | | - "$_SERVER['FRANKENPHP_WORKER_BACKGROUND'] should be the bool true") |
| 152 | + assert.Contains(t, out, "name=flag-worker") |
| 153 | + assert.Contains(t, out, "is_background=true") |
165 | 154 | } |
0 commit comments