From 26c69beeb9323fba84786ef534bb552949ea10eb Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Fri, 14 Nov 2025 12:18:15 +0100 Subject: [PATCH 1/4] fix the types thing --- testdata/integration/README.md | 208 +++++++++++++++++++++++++++++++++ 1 file changed, 208 insertions(+) create mode 100644 testdata/integration/README.md diff --git a/testdata/integration/README.md b/testdata/integration/README.md new file mode 100644 index 000000000..c034dcddf --- /dev/null +++ b/testdata/integration/README.md @@ -0,0 +1,208 @@ +# Integration Test Fixtures + +This directory contains Go source files used as test fixtures for the FrankenPHP extension-init integration tests. + +## Overview + +These fixtures test the full end-to-end workflow of the extension-init command: +1. Generating extension files from Go source code +2. Compiling FrankenPHP with the generated extension +3. Executing PHP code that uses the extension +4. Verifying the output + +## Test Fixtures + +### Happy Path Tests + +#### `basic_function.go` +Tests basic function generation with primitive types: +- `test_uppercase(string): string` - String parameter and return +- `test_add_numbers(int, int): int` - Integer parameters +- `test_multiply(float, float): float` - Float parameters +- `test_is_enabled(bool): bool` - Boolean parameter + +**What it tests:** +- Function parsing and generation +- Type conversion for all primitive types +- C/Go bridge code generation +- PHP stub file generation + +#### `class_methods.go` +Tests opaque class generation with methods: +- `Counter` class - Integer counter with increment/decrement operations +- `StringHolder` class - String storage and manipulation + +**What it tests:** +- Class declaration with `//export_php:class` +- Method declaration with `//export_php:method` +- Object lifecycle (creation and destruction) +- Method calls with various parameter and return types +- Nullable parameters (`?int`) +- Opaque object encapsulation (no direct property access) + +#### `constants.go` +Tests constant generation and usage: +- Global constants (int, string, bool, float) +- Iota sequences for enumerations +- Class constants +- Functions using constants + +**What it tests:** +- `//export_php:const` directive +- `//export_php:classconstant` directive +- Constant type detection and conversion +- Iota sequence handling +- Integration of constants with functions and classes + +#### `namespace.go` +Tests namespace support: +- Functions in namespace `TestIntegration\Extension` +- Classes in namespace +- Constants in namespace + +**What it tests:** +- `//export_php:namespace` directive +- Namespace declaration in stub files +- C name mangling for namespaces +- Proper scoping of functions, classes, and constants + +### Error Case Tests + +#### `invalid_signature.go` +Tests error handling for invalid function signatures: +- Function with unsupported return type + +**What it tests:** +- Validation of return types +- Clear error messages for unsupported types +- Graceful failure during generation + +#### `type_mismatch.go` +Tests error handling for type mismatches: +- PHP signature declares `int` but Go function expects `string` +- Method return type mismatch + +**What it tests:** +- Parameter type validation +- Return type validation +- Type compatibility checking between PHP and Go + +## Running Integration Tests Locally + +Integration tests are tagged with `//go:build integration` and are skipped by default because they require: +1. PHP development headers (`php-config`) +2. PHP sources (for `gen_stub.php` script) +3. xcaddy (for building FrankenPHP) + +### Prerequisites + +1. **Install PHP development headers:** + ```bash + # Ubuntu/Debian + sudo apt-get install php-dev + + # macOS + brew install php + ``` + +2. **Download PHP sources:** + ```bash + wget https://www.php.net/distributions/php-8.4.0.tar.gz + tar xzf php-8.4.0.tar.gz + export GEN_STUB_SCRIPT=$PWD/php-8.4.0/build/gen_stub.php + ``` + +3. **Install xcaddy:** + ```bash + go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest + ``` + +### Running the Tests + +```bash +cd internal/extgen +go test -tags integration -v -timeout 30m +``` + +The timeout is set to 30 minutes because: +- Each test compiles a full FrankenPHP binary with xcaddy +- Multiple test scenarios are run sequentially +- Compilation can be slow on CI runners + +### Skipping Tests + +If any of the prerequisites are not met, the tests will be skipped automatically with a clear message: +- Missing `GEN_STUB_SCRIPT`: "Integration tests require PHP sources" +- Missing `xcaddy`: "Integration tests require xcaddy to build FrankenPHP" +- Missing `php-config`: "Integration tests require PHP development headers" + +## CI Integration + +Integration tests run automatically in CI on: +- Pull requests to `main` branch +- Pushes to `main` branch +- PHP versions: 8.3, 8.4 +- Platform: Linux (Ubuntu) + +The CI workflow (`.github/workflows/tests.yaml`) automatically: +1. Sets up Go and PHP +2. Installs xcaddy +3. Downloads PHP sources +4. Sets `GEN_STUB_SCRIPT` environment variable +5. Runs integration tests with 30-minute timeout + +## Adding New Test Fixtures + +To add a new integration test fixture: + +1. **Create a new Go file** in this directory with your test code +2. **Use export_php directives** to declare functions, classes, or constants +3. **Add a new test function** in `internal/extgen/integration_test.go`: + ```go + func TestYourFeature(t *testing.T) { + suite := setupTest(t) + + sourceFile := filepath.Join("..", "..", "testdata", "integration", "your_file.go") + sourceFile, err := filepath.Abs(sourceFile) + require.NoError(t, err) + + targetFile, err := suite.createGoModule(sourceFile) + require.NoError(t, err) + + err = suite.runExtensionInit(targetFile) + require.NoError(t, err) + + _, err = suite.compileFrankenPHP(filepath.Dir(targetFile)) + require.NoError(t, err) + + phpCode := ` Date: Mon, 15 Dec 2025 09:41:48 +0100 Subject: [PATCH 2/4] add integration test --- testdata/integration/README.md | 208 --------------------------------- 1 file changed, 208 deletions(-) delete mode 100644 testdata/integration/README.md diff --git a/testdata/integration/README.md b/testdata/integration/README.md deleted file mode 100644 index c034dcddf..000000000 --- a/testdata/integration/README.md +++ /dev/null @@ -1,208 +0,0 @@ -# Integration Test Fixtures - -This directory contains Go source files used as test fixtures for the FrankenPHP extension-init integration tests. - -## Overview - -These fixtures test the full end-to-end workflow of the extension-init command: -1. Generating extension files from Go source code -2. Compiling FrankenPHP with the generated extension -3. Executing PHP code that uses the extension -4. Verifying the output - -## Test Fixtures - -### Happy Path Tests - -#### `basic_function.go` -Tests basic function generation with primitive types: -- `test_uppercase(string): string` - String parameter and return -- `test_add_numbers(int, int): int` - Integer parameters -- `test_multiply(float, float): float` - Float parameters -- `test_is_enabled(bool): bool` - Boolean parameter - -**What it tests:** -- Function parsing and generation -- Type conversion for all primitive types -- C/Go bridge code generation -- PHP stub file generation - -#### `class_methods.go` -Tests opaque class generation with methods: -- `Counter` class - Integer counter with increment/decrement operations -- `StringHolder` class - String storage and manipulation - -**What it tests:** -- Class declaration with `//export_php:class` -- Method declaration with `//export_php:method` -- Object lifecycle (creation and destruction) -- Method calls with various parameter and return types -- Nullable parameters (`?int`) -- Opaque object encapsulation (no direct property access) - -#### `constants.go` -Tests constant generation and usage: -- Global constants (int, string, bool, float) -- Iota sequences for enumerations -- Class constants -- Functions using constants - -**What it tests:** -- `//export_php:const` directive -- `//export_php:classconstant` directive -- Constant type detection and conversion -- Iota sequence handling -- Integration of constants with functions and classes - -#### `namespace.go` -Tests namespace support: -- Functions in namespace `TestIntegration\Extension` -- Classes in namespace -- Constants in namespace - -**What it tests:** -- `//export_php:namespace` directive -- Namespace declaration in stub files -- C name mangling for namespaces -- Proper scoping of functions, classes, and constants - -### Error Case Tests - -#### `invalid_signature.go` -Tests error handling for invalid function signatures: -- Function with unsupported return type - -**What it tests:** -- Validation of return types -- Clear error messages for unsupported types -- Graceful failure during generation - -#### `type_mismatch.go` -Tests error handling for type mismatches: -- PHP signature declares `int` but Go function expects `string` -- Method return type mismatch - -**What it tests:** -- Parameter type validation -- Return type validation -- Type compatibility checking between PHP and Go - -## Running Integration Tests Locally - -Integration tests are tagged with `//go:build integration` and are skipped by default because they require: -1. PHP development headers (`php-config`) -2. PHP sources (for `gen_stub.php` script) -3. xcaddy (for building FrankenPHP) - -### Prerequisites - -1. **Install PHP development headers:** - ```bash - # Ubuntu/Debian - sudo apt-get install php-dev - - # macOS - brew install php - ``` - -2. **Download PHP sources:** - ```bash - wget https://www.php.net/distributions/php-8.4.0.tar.gz - tar xzf php-8.4.0.tar.gz - export GEN_STUB_SCRIPT=$PWD/php-8.4.0/build/gen_stub.php - ``` - -3. **Install xcaddy:** - ```bash - go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest - ``` - -### Running the Tests - -```bash -cd internal/extgen -go test -tags integration -v -timeout 30m -``` - -The timeout is set to 30 minutes because: -- Each test compiles a full FrankenPHP binary with xcaddy -- Multiple test scenarios are run sequentially -- Compilation can be slow on CI runners - -### Skipping Tests - -If any of the prerequisites are not met, the tests will be skipped automatically with a clear message: -- Missing `GEN_STUB_SCRIPT`: "Integration tests require PHP sources" -- Missing `xcaddy`: "Integration tests require xcaddy to build FrankenPHP" -- Missing `php-config`: "Integration tests require PHP development headers" - -## CI Integration - -Integration tests run automatically in CI on: -- Pull requests to `main` branch -- Pushes to `main` branch -- PHP versions: 8.3, 8.4 -- Platform: Linux (Ubuntu) - -The CI workflow (`.github/workflows/tests.yaml`) automatically: -1. Sets up Go and PHP -2. Installs xcaddy -3. Downloads PHP sources -4. Sets `GEN_STUB_SCRIPT` environment variable -5. Runs integration tests with 30-minute timeout - -## Adding New Test Fixtures - -To add a new integration test fixture: - -1. **Create a new Go file** in this directory with your test code -2. **Use export_php directives** to declare functions, classes, or constants -3. **Add a new test function** in `internal/extgen/integration_test.go`: - ```go - func TestYourFeature(t *testing.T) { - suite := setupTest(t) - - sourceFile := filepath.Join("..", "..", "testdata", "integration", "your_file.go") - sourceFile, err := filepath.Abs(sourceFile) - require.NoError(t, err) - - targetFile, err := suite.createGoModule(sourceFile) - require.NoError(t, err) - - err = suite.runExtensionInit(targetFile) - require.NoError(t, err) - - _, err = suite.compileFrankenPHP(filepath.Dir(targetFile)) - require.NoError(t, err) - - phpCode := ` Date: Fri, 14 Nov 2025 12:27:55 +0100 Subject: [PATCH 3/4] feat(types): expose `IsPacked` to help dealing with hashmaps and lists in Go code --- docs/extensions.md | 1 + types.go | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/docs/extensions.md b/docs/extensions.md index 595b0424d..fc188d59d 100644 --- a/docs/extensions.md +++ b/docs/extensions.md @@ -212,6 +212,7 @@ func process_data_packed(arr *C.zend_array) unsafe.Pointer { - `frankenphp.GoAssociativeArray(arr unsafe.Pointer, ordered bool) frankenphp.AssociativeArray` - Convert a PHP array to an ordered Go `AssociativeArray` (map with order) - `frankenphp.GoMap(arr unsafe.Pointer) map[string]any` - Convert a PHP array to an unordered Go map - `frankenphp.GoPackedArray(arr unsafe.Pointer) []any` - Convert a PHP array to a Go slice +- `frankenphp.IsPacked(zval unsafe.Pointer) bool` - Check if a PHP array is packed (indexed only) or associative (key-value pairs) ### Working with Callables diff --git a/types.go b/types.go index 3128a7c50..2322496f2 100644 --- a/types.go +++ b/types.go @@ -416,6 +416,26 @@ func createNewArray(size uint32) *C.zend_array { return (*C.zend_array)(unsafe.Pointer(arr)) } +// IsPacked determines if the given zval pointer represents a packed array (list). +// Returns false if the zval is nil, not an array, or not packed. +func IsPacked(zval unsafe.Pointer) bool { + if zval == nil { + return false + } + + v, err := extractZvalValue((*C.zval)(zval), C.IS_ARRAY) + if err != nil { + return false + } + + ht := (*C.HashTable)(v) + if ht == nil { + return false + } + + return htIsPacked(ht) +} + // htIsPacked checks if a zend_array is a list (packed) or hashmap (not packed). func htIsPacked(ht *C.zend_array) bool { flags := *(*C.uint32_t)(unsafe.Pointer(&ht.u[0])) From 5d9bcde598d4788d6845fdb869c654aec69db6de Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Mon, 15 Dec 2025 09:46:39 +0100 Subject: [PATCH 4/4] Simplify IsPacked --- docs/extensions.md | 2 +- docs/fr/extensions.md | 1 + types.go | 20 +++++--------------- 3 files changed, 7 insertions(+), 16 deletions(-) diff --git a/docs/extensions.md b/docs/extensions.md index fc188d59d..30f434eb5 100644 --- a/docs/extensions.md +++ b/docs/extensions.md @@ -212,7 +212,7 @@ func process_data_packed(arr *C.zend_array) unsafe.Pointer { - `frankenphp.GoAssociativeArray(arr unsafe.Pointer, ordered bool) frankenphp.AssociativeArray` - Convert a PHP array to an ordered Go `AssociativeArray` (map with order) - `frankenphp.GoMap(arr unsafe.Pointer) map[string]any` - Convert a PHP array to an unordered Go map - `frankenphp.GoPackedArray(arr unsafe.Pointer) []any` - Convert a PHP array to a Go slice -- `frankenphp.IsPacked(zval unsafe.Pointer) bool` - Check if a PHP array is packed (indexed only) or associative (key-value pairs) +- `frankenphp.IsPacked(zval *C.zend_array) bool` - Check if a PHP array is packed (indexed only) or associative (key-value pairs) ### Working with Callables diff --git a/docs/fr/extensions.md b/docs/fr/extensions.md index 1e03fbc54..caf50e9ca 100644 --- a/docs/fr/extensions.md +++ b/docs/fr/extensions.md @@ -209,6 +209,7 @@ func process_data_packed(arr *C.zval) unsafe.Pointer { - `frankenphp.GoAssociativeArray(arr unsafe.Pointer, ordered bool) frankenphp.AssociativeArray` - Convertir un tableau PHP vers un `AssociativeArray` Go ordonné (map avec ordre) - `frankenphp.GoMap(arr unsafe.Pointer) map[string]any` - Convertir un tableau PHP vers une map Go non ordonnée - `frankenphp.GoPackedArray(arr unsafe.Pointer) []any` - Convertir un tableau PHP vers un slice Go +- `frankenphp.IsPacked(zval *C.zend_array) bool` - Vérifie si le tableau PHP est une liste ou un tableau associatif ### Travailler avec des Callables diff --git a/types.go b/types.go index 2322496f2..17a2d5506 100644 --- a/types.go +++ b/types.go @@ -416,24 +416,14 @@ func createNewArray(size uint32) *C.zend_array { return (*C.zend_array)(unsafe.Pointer(arr)) } -// IsPacked determines if the given zval pointer represents a packed array (list). -// Returns false if the zval is nil, not an array, or not packed. -func IsPacked(zval unsafe.Pointer) bool { - if zval == nil { - return false - } - - v, err := extractZvalValue((*C.zval)(zval), C.IS_ARRAY) - if err != nil { - return false - } - - ht := (*C.HashTable)(v) - if ht == nil { +// IsPacked determines if the given zend_array is a packed array (list). +// Returns false if the array is nil or not packed. +func IsPacked(arr unsafe.Pointer) bool { + if arr == nil { return false } - return htIsPacked(ht) + return htIsPacked((*C.zend_array)(arr)) } // htIsPacked checks if a zend_array is a list (packed) or hashmap (not packed).