Skip to content

Commit 57d4422

Browse files
fix the types thing
1 parent c3a2571 commit 57d4422

File tree

2 files changed

+237
-19
lines changed

2 files changed

+237
-19
lines changed

testdata/integration/README.md

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
# Integration Test Fixtures
2+
3+
This directory contains Go source files used as test fixtures for the FrankenPHP extension-init integration tests.
4+
5+
## Overview
6+
7+
These fixtures test the full end-to-end workflow of the extension-init command:
8+
1. Generating extension files from Go source code
9+
2. Compiling FrankenPHP with the generated extension
10+
3. Executing PHP code that uses the extension
11+
4. Verifying the output
12+
13+
## Test Fixtures
14+
15+
### Happy Path Tests
16+
17+
#### `basic_function.go`
18+
Tests basic function generation with primitive types:
19+
- `test_uppercase(string): string` - String parameter and return
20+
- `test_add_numbers(int, int): int` - Integer parameters
21+
- `test_multiply(float, float): float` - Float parameters
22+
- `test_is_enabled(bool): bool` - Boolean parameter
23+
24+
**What it tests:**
25+
- Function parsing and generation
26+
- Type conversion for all primitive types
27+
- C/Go bridge code generation
28+
- PHP stub file generation
29+
30+
#### `class_methods.go`
31+
Tests opaque class generation with methods:
32+
- `Counter` class - Integer counter with increment/decrement operations
33+
- `StringHolder` class - String storage and manipulation
34+
35+
**What it tests:**
36+
- Class declaration with `//export_php:class`
37+
- Method declaration with `//export_php:method`
38+
- Object lifecycle (creation and destruction)
39+
- Method calls with various parameter and return types
40+
- Nullable parameters (`?int`)
41+
- Opaque object encapsulation (no direct property access)
42+
43+
#### `constants.go`
44+
Tests constant generation and usage:
45+
- Global constants (int, string, bool, float)
46+
- Iota sequences for enumerations
47+
- Class constants
48+
- Functions using constants
49+
50+
**What it tests:**
51+
- `//export_php:const` directive
52+
- `//export_php:classconstant` directive
53+
- Constant type detection and conversion
54+
- Iota sequence handling
55+
- Integration of constants with functions and classes
56+
57+
#### `namespace.go`
58+
Tests namespace support:
59+
- Functions in namespace `TestIntegration\Extension`
60+
- Classes in namespace
61+
- Constants in namespace
62+
63+
**What it tests:**
64+
- `//export_php:namespace` directive
65+
- Namespace declaration in stub files
66+
- C name mangling for namespaces
67+
- Proper scoping of functions, classes, and constants
68+
69+
### Error Case Tests
70+
71+
#### `invalid_signature.go`
72+
Tests error handling for invalid function signatures:
73+
- Function with unsupported return type
74+
75+
**What it tests:**
76+
- Validation of return types
77+
- Clear error messages for unsupported types
78+
- Graceful failure during generation
79+
80+
#### `type_mismatch.go`
81+
Tests error handling for type mismatches:
82+
- PHP signature declares `int` but Go function expects `string`
83+
- Method return type mismatch
84+
85+
**What it tests:**
86+
- Parameter type validation
87+
- Return type validation
88+
- Type compatibility checking between PHP and Go
89+
90+
## Running Integration Tests Locally
91+
92+
Integration tests are tagged with `//go:build integration` and are skipped by default because they require:
93+
1. PHP development headers (`php-config`)
94+
2. PHP sources (for `gen_stub.php` script)
95+
3. xcaddy (for building FrankenPHP)
96+
97+
### Prerequisites
98+
99+
1. **Install PHP development headers:**
100+
```bash
101+
# Ubuntu/Debian
102+
sudo apt-get install php-dev
103+
104+
# macOS
105+
brew install php
106+
```
107+
108+
2. **Download PHP sources:**
109+
```bash
110+
wget https://www.php.net/distributions/php-8.4.0.tar.gz
111+
tar xzf php-8.4.0.tar.gz
112+
export GEN_STUB_SCRIPT=$PWD/php-8.4.0/build/gen_stub.php
113+
```
114+
115+
3. **Install xcaddy:**
116+
```bash
117+
go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest
118+
```
119+
120+
### Running the Tests
121+
122+
```bash
123+
cd internal/extgen
124+
go test -tags integration -v -timeout 30m
125+
```
126+
127+
The timeout is set to 30 minutes because:
128+
- Each test compiles a full FrankenPHP binary with xcaddy
129+
- Multiple test scenarios are run sequentially
130+
- Compilation can be slow on CI runners
131+
132+
### Skipping Tests
133+
134+
If any of the prerequisites are not met, the tests will be skipped automatically with a clear message:
135+
- Missing `GEN_STUB_SCRIPT`: "Integration tests require PHP sources"
136+
- Missing `xcaddy`: "Integration tests require xcaddy to build FrankenPHP"
137+
- Missing `php-config`: "Integration tests require PHP development headers"
138+
139+
## CI Integration
140+
141+
Integration tests run automatically in CI on:
142+
- Pull requests to `main` branch
143+
- Pushes to `main` branch
144+
- PHP versions: 8.3, 8.4
145+
- Platform: Linux (Ubuntu)
146+
147+
The CI workflow (`.github/workflows/tests.yaml`) automatically:
148+
1. Sets up Go and PHP
149+
2. Installs xcaddy
150+
3. Downloads PHP sources
151+
4. Sets `GEN_STUB_SCRIPT` environment variable
152+
5. Runs integration tests with 30-minute timeout
153+
154+
## Adding New Test Fixtures
155+
156+
To add a new integration test fixture:
157+
158+
1. **Create a new Go file** in this directory with your test code
159+
2. **Use export_php directives** to declare functions, classes, or constants
160+
3. **Add a new test function** in `internal/extgen/integration_test.go`:
161+
```go
162+
func TestYourFeature(t *testing.T) {
163+
suite := setupTest(t)
164+
165+
sourceFile := filepath.Join("..", "..", "testdata", "integration", "your_file.go")
166+
sourceFile, err := filepath.Abs(sourceFile)
167+
require.NoError(t, err)
168+
169+
targetFile, err := suite.createGoModule(sourceFile)
170+
require.NoError(t, err)
171+
172+
err = suite.runExtensionInit(targetFile)
173+
require.NoError(t, err)
174+
175+
_, err = suite.compileFrankenPHP(filepath.Dir(targetFile))
176+
require.NoError(t, err)
177+
178+
phpCode := `<?php /* your test PHP code */ `
179+
180+
output, err := suite.runPHPCode(phpCode)
181+
require.NoError(t, err)
182+
183+
// Add assertions
184+
assert.Contains(t, output, "expected output")
185+
}
186+
```
187+
188+
4. **Document your fixture** in this README
189+
190+
## Test Coverage
191+
192+
Current integration test coverage:
193+
- ✅ Basic functions with primitive types
194+
- ✅ Classes with methods
195+
- ✅ Nullable parameters
196+
- ✅ Global and class constants
197+
- ✅ Iota sequences
198+
- ✅ Namespaces
199+
- ✅ Invalid signatures (error case)
200+
- ✅ Type mismatches (error case)
201+
- ✅ Missing gen_stub.php (error case)
202+
203+
Future coverage goals:
204+
- Array handling (packed, associative, maps)
205+
- Default parameter values
206+
- Multiple namespaces (error case)
207+
- Complex nested types
208+
- Performance benchmarks

types.go

Lines changed: 29 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -218,19 +218,18 @@ func phpArray[T any](entries map[string]T, order []string) unsafe.Pointer {
218218
val := entries[key]
219219
zval := phpValue(val)
220220
C.zend_hash_str_update(zendArray, toUnsafeChar(key), C.size_t(len(key)), zval)
221+
C.__efree__(unsafe.Pointer(zval))
221222
}
222223
} else {
223224
zendArray = createNewArray((uint32)(len(entries)))
224225
for key, val := range entries {
225226
zval := phpValue(val)
226227
C.zend_hash_str_update(zendArray, toUnsafeChar(key), C.size_t(len(key)), zval)
228+
C.__efree__(unsafe.Pointer(zval))
227229
}
228230
}
229231

230-
var zval C.zval
231-
C.__zval_arr__(&zval, zendArray)
232-
233-
return unsafe.Pointer(&zval)
232+
return unsafe.Pointer(zendArray)
234233
}
235234

236235
// EXPERIMENTAL: PHPPackedArray converts a Go slice to a PHP zval with a zend_array value.
@@ -239,12 +238,10 @@ func PHPPackedArray[T any](slice []T) unsafe.Pointer {
239238
for _, val := range slice {
240239
zval := phpValue(val)
241240
C.zend_hash_next_index_insert(zendArray, zval)
241+
C.__efree__(unsafe.Pointer(zval))
242242
}
243243

244-
var zval C.zval
245-
C.__zval_arr__(&zval, zendArray)
246-
247-
return unsafe.Pointer(&zval)
244+
return unsafe.Pointer(zendArray)
248245
}
249246

250247
// EXPERIMENTAL: GoValue converts a PHP zval to a Go value
@@ -364,35 +361,39 @@ func PHPValue(value any) unsafe.Pointer {
364361
}
365362

366363
func phpValue(value any) *C.zval {
367-
var zval C.zval
364+
zval := (*C.zval)(C.__emalloc__(C.size_t(unsafe.Sizeof(C.zval{}))))
368365

369366
if toZvalObj, ok := value.(toZval); ok {
367+
C.__efree__(unsafe.Pointer(zval))
370368
return toZvalObj.toZval()
371369
}
372370

373371
switch v := value.(type) {
374372
case nil:
375-
C.__zval_null__(&zval)
373+
C.__zval_null__(zval)
376374
case bool:
377-
C.__zval_bool__(&zval, C._Bool(v))
375+
C.__zval_bool__(zval, C._Bool(v))
378376
case int:
379-
C.__zval_long__(&zval, C.zend_long(v))
377+
C.__zval_long__(zval, C.zend_long(v))
380378
case int64:
381-
C.__zval_long__(&zval, C.zend_long(v))
379+
C.__zval_long__(zval, C.zend_long(v))
382380
case float64:
383-
C.__zval_double__(&zval, C.double(v))
381+
C.__zval_double__(zval, C.double(v))
384382
case string:
385383
str := (*C.zend_string)(PHPString(v, false))
386-
C.__zval_string__(&zval, str)
384+
C.__zval_string__(zval, str)
387385
case map[string]any:
388-
return (*C.zval)(PHPAssociativeArray[any](AssociativeArray[any]{Map: v}))
386+
zendArray := (*C.HashTable)(PHPAssociativeArray[any](AssociativeArray[any]{Map: v}))
387+
C.__zval_arr__(zval, zendArray)
389388
case []any:
390-
return (*C.zval)(PHPPackedArray(v))
389+
zendArray := (*C.HashTable)(PHPPackedArray(v))
390+
C.__zval_arr__(zval, zendArray)
391391
default:
392+
C.__efree__(unsafe.Pointer(zval))
392393
panic(fmt.Sprintf("unsupported Go type %T", v))
393394
}
394395

395-
return &zval
396+
return zval
396397
}
397398

398399
// createNewArray creates a new zend_array with the specified size.
@@ -456,12 +457,19 @@ func CallPHPCallable(cb unsafe.Pointer, params []interface{}) interface{} {
456457
var paramStorage *C.zval
457458
if paramCount > 0 {
458459
paramStorage = (*C.zval)(C.__emalloc__(C.size_t(paramCount) * C.size_t(unsafe.Sizeof(C.zval{}))))
459-
defer C.__efree__(unsafe.Pointer(paramStorage))
460+
defer func() {
461+
for i := 0; i < paramCount; i++ {
462+
targetZval := (*C.zval)(unsafe.Pointer(uintptr(unsafe.Pointer(paramStorage)) + uintptr(i)*unsafe.Sizeof(C.zval{})))
463+
C.zval_ptr_dtor(targetZval)
464+
}
465+
C.__efree__(unsafe.Pointer(paramStorage))
466+
}()
460467

461468
for i, param := range params {
462469
targetZval := (*C.zval)(unsafe.Pointer(uintptr(unsafe.Pointer(paramStorage)) + uintptr(i)*unsafe.Sizeof(C.zval{})))
463470
sourceZval := phpValue(param)
464471
*targetZval = *sourceZval
472+
C.__efree__(unsafe.Pointer(sourceZval))
465473
}
466474
}
467475

@@ -473,6 +481,8 @@ func CallPHPCallable(cb unsafe.Pointer, params []interface{}) interface{} {
473481
}
474482

475483
goResult, err := goValue[any](&retval)
484+
C.zval_ptr_dtor(&retval)
485+
476486
if err != nil {
477487
return nil
478488
}

0 commit comments

Comments
 (0)