tests(extgen): add integration tests (#1984)

Fix #1975
This commit is contained in:
Alexandre Daubois
2025-12-12 14:32:00 +01:00
committed by GitHub
parent 225ca409d3
commit 599c92b15d
9 changed files with 980 additions and 2 deletions

View File

@@ -93,6 +93,49 @@ jobs:
if: matrix.php-versions == '8.5'
run: go mod tidy -diff
working-directory: caddy/
integration-tests:
name: Integration Tests (Linux, PHP ${{ matrix.php-versions }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php-versions: ["8.3", "8.4", "8.5"]
steps:
- uses: actions/checkout@v5
with:
persist-credentials: false
- uses: actions/setup-go@v6
with:
go-version: "1.25"
cache-dependency-path: |
go.sum
caddy/go.sum
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-versions }}
ini-file: development
coverage: none
tools: none
env:
phpts: ts
debug: true
- name: Install PHP development libraries
run: sudo apt-get update && sudo apt-get install -y libkrb5-dev libsodium-dev libargon2-dev
- name: Install xcaddy
run: go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest
- name: Download PHP sources
run: |
PHP_VERSION=$(php -r "echo PHP_VERSION;")
wget -q "https://www.php.net/distributions/php-${PHP_VERSION}.tar.gz"
tar xzf "php-${PHP_VERSION}.tar.gz"
echo "GEN_STUB_SCRIPT=${PWD}/php-${PHP_VERSION}/build/gen_stub.php" >> "${GITHUB_ENV}"
- name: Set CGO flags
run: |
echo "CGO_CFLAGS=$(php-config --includes)" >> "${GITHUB_ENV}"
echo "CGO_LDFLAGS=$(php-config --ldflags) $(php-config --libs)" >> "${GITHUB_ENV}"
- name: Run integration tests
working-directory: internal/extgen/
run: go test -tags integration -v -timeout 30m
tests-mac:
name: Tests (macOS, PHP 8.5)
runs-on: macos-latest
@@ -106,7 +149,7 @@ jobs:
with:
go-version: "1.25"
cache-dependency-path: |
go.sum
go.sum
caddy/go.sum
- uses: shivammathur/setup-php@v2
with:

View File

@@ -29,7 +29,7 @@ func (ag *arginfoGenerator) generate() error {
output, err := cmd.CombinedOutput()
if err != nil {
log.Print("gen_stub.php output:\n", string(output))
return fmt.Errorf("running gen_stub script: %w", err)
return fmt.Errorf("running gen_stub script: %w\nOutput: %s", err, string(output))
}
return ag.fixArginfoFile(stubFile)

View File

@@ -0,0 +1,683 @@
//go:build integration
package extgen
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const (
testModuleName = "github.com/frankenphp/test-extension"
)
type IntegrationTestSuite struct {
tempDir string
genStubScript string
xcaddyPath string
frankenphpPath string
phpConfigPath string
t *testing.T
}
func setupTest(t *testing.T) *IntegrationTestSuite {
t.Helper()
suite := &IntegrationTestSuite{t: t}
suite.genStubScript = os.Getenv("GEN_STUB_SCRIPT")
if suite.genStubScript == "" {
suite.genStubScript = "/usr/local/src/php/build/gen_stub.php"
}
if _, err := os.Stat(suite.genStubScript); os.IsNotExist(err) {
t.Error("GEN_STUB_SCRIPT not found. Integration tests require PHP sources. Set GEN_STUB_SCRIPT environment variable.")
}
xcaddyPath, err := exec.LookPath("xcaddy")
if err != nil {
t.Error("xcaddy not found in PATH. Integration tests require xcaddy to build FrankenPHP.")
}
suite.xcaddyPath = xcaddyPath
phpConfigPath, err := exec.LookPath("php-config")
if err != nil {
t.Error("php-config not found in PATH. Integration tests require PHP development headers.")
}
suite.phpConfigPath = phpConfigPath
tempDir := t.TempDir()
suite.tempDir = tempDir
return suite
}
func (s *IntegrationTestSuite) createGoModule(sourceFile string) (string, error) {
s.t.Helper()
moduleDir := filepath.Join(s.tempDir, "module")
if err := os.MkdirAll(moduleDir, 0o755); err != nil {
return "", fmt.Errorf("failed to create module directory: %w", err)
}
// Get project root for replace directive
projectRoot, err := filepath.Abs(filepath.Join("..", ".."))
if err != nil {
return "", fmt.Errorf("failed to get project root: %w", err)
}
goModContent := fmt.Sprintf(`module %s
go 1.25
require github.com/dunglas/frankenphp v0.0.0
replace github.com/dunglas/frankenphp => %s
`, testModuleName, projectRoot)
if err := os.WriteFile(filepath.Join(moduleDir, "go.mod"), []byte(goModContent), 0o644); err != nil {
return "", fmt.Errorf("failed to create go.mod: %w", err)
}
sourceContent, err := os.ReadFile(sourceFile)
if err != nil {
return "", fmt.Errorf("failed to read source file: %w", err)
}
targetFile := filepath.Join(moduleDir, filepath.Base(sourceFile))
if err := os.WriteFile(targetFile, sourceContent, 0o644); err != nil {
return "", fmt.Errorf("failed to write source file: %w", err)
}
return targetFile, nil
}
func (s *IntegrationTestSuite) runExtensionInit(sourceFile string) error {
s.t.Helper()
os.Setenv("GEN_STUB_SCRIPT", s.genStubScript)
baseName := SanitizePackageName(strings.TrimSuffix(filepath.Base(sourceFile), ".go"))
generator := Generator{
BaseName: baseName,
SourceFile: sourceFile,
BuildDir: filepath.Dir(sourceFile),
}
if err := generator.Generate(); err != nil {
return fmt.Errorf("generation failed: %w", err)
}
return nil
}
// compileFrankenPHP compiles FrankenPHP with the generated extension
func (s *IntegrationTestSuite) compileFrankenPHP(moduleDir string) (string, error) {
s.t.Helper()
projectRoot, err := filepath.Abs(filepath.Join("..", ".."))
if err != nil {
return "", fmt.Errorf("failed to get project root: %w", err)
}
cflags, err := exec.Command(s.phpConfigPath, "--includes").Output()
if err != nil {
return "", fmt.Errorf("failed to get PHP includes: %w", err)
}
ldflags, err := exec.Command(s.phpConfigPath, "--ldflags").Output()
if err != nil {
return "", fmt.Errorf("failed to get PHP ldflags: %w", err)
}
libs, err := exec.Command(s.phpConfigPath, "--libs").Output()
if err != nil {
return "", fmt.Errorf("failed to get PHP libs: %w", err)
}
cgoCflags := strings.TrimSpace(string(cflags))
cgoLdflags := strings.TrimSpace(string(ldflags)) + " " + strings.TrimSpace(string(libs))
outputBinary := filepath.Join(s.tempDir, "frankenphp")
cmd := exec.Command(
s.xcaddyPath,
"build",
"--output", outputBinary,
"--with", "github.com/dunglas/frankenphp="+projectRoot,
"--with", "github.com/dunglas/frankenphp/caddy="+projectRoot+"/caddy",
"--with", testModuleName+"="+moduleDir,
)
cmd.Env = append(os.Environ(),
"CGO_ENABLED=1",
"CGO_CFLAGS="+cgoCflags,
"CGO_LDFLAGS="+cgoLdflags,
fmt.Sprintf("XCADDY_GO_BUILD_FLAGS=-ldflags='-w -s' -tags=nobadger,nomysql,nopgx,nowatcher"),
)
cmd.Dir = s.tempDir
output, err := cmd.CombinedOutput()
if err != nil {
return "", fmt.Errorf("xcaddy build failed: %w\nOutput: %s", err, string(output))
}
s.frankenphpPath = outputBinary
return outputBinary, nil
}
func (s *IntegrationTestSuite) runPHPCode(phpCode string) (string, error) {
s.t.Helper()
if s.frankenphpPath == "" {
return "", fmt.Errorf("FrankenPHP not compiled yet")
}
phpFile := filepath.Join(s.tempDir, "test.php")
if err := os.WriteFile(phpFile, []byte(phpCode), 0o644); err != nil {
return "", fmt.Errorf("failed to create PHP file: %w", err)
}
cmd := exec.Command(s.frankenphpPath, "php-cli", phpFile)
output, err := cmd.CombinedOutput()
if err != nil {
return "", fmt.Errorf("PHP execution failed: %w\nOutput: %s", err, string(output))
}
return string(output), nil
}
// verifyPHPSymbols checks if PHP can find the exposed functions, classes, and constants
func (s *IntegrationTestSuite) verifyPHPSymbols(functions []string, classes []string, constants []string) error {
s.t.Helper()
var checks []string
for _, fn := range functions {
checks = append(checks, fmt.Sprintf("if (!function_exists('%s')) { echo 'MISSING_FUNCTION: %s'; exit(1); }", fn, fn))
}
for _, cls := range classes {
checks = append(checks, fmt.Sprintf("if (!class_exists('%s')) { echo 'MISSING_CLASS: %s'; exit(1); }", cls, cls))
}
for _, cnst := range constants {
checks = append(checks, fmt.Sprintf("if (!defined('%s')) { echo 'MISSING_CONSTANT: %s'; exit(1); }", cnst, cnst))
}
checks = append(checks, "echo 'OK';")
phpCode := "<?php\n" + strings.Join(checks, "\n")
output, err := s.runPHPCode(phpCode)
if err != nil {
return err
}
if !strings.Contains(output, "OK") {
return fmt.Errorf("symbol verification failed: %s", output)
}
return nil
}
func (s *IntegrationTestSuite) verifyFunctionBehavior(phpCode string, expectedOutput string) error {
s.t.Helper()
output, err := s.runPHPCode(phpCode)
if err != nil {
return err
}
if !strings.Contains(output, expectedOutput) {
return fmt.Errorf("unexpected output.\nExpected to contain: %q\nGot: %q", expectedOutput, output)
}
return nil
}
func TestBasicFunction(t *testing.T) {
suite := setupTest(t)
sourceFile := filepath.Join("..", "..", "testdata", "integration", "basic_function.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, "extension-init should succeed")
baseDir := filepath.Dir(targetFile)
baseName := strings.TrimSuffix(filepath.Base(targetFile), ".go")
expectedFiles := []string{
baseName + ".stub.php",
baseName + "_arginfo.h",
baseName + ".h",
baseName + ".c",
baseName + ".go",
"README.md",
}
for _, file := range expectedFiles {
fullPath := filepath.Join(baseDir, file)
assert.FileExists(t, fullPath, "Generated file should exist: %s", file)
}
_, err = suite.compileFrankenPHP(filepath.Dir(targetFile))
require.NoError(t, err, "FrankenPHP compilation should succeed")
err = suite.verifyPHPSymbols(
[]string{"test_uppercase", "test_add_numbers", "test_multiply", "test_is_enabled"},
[]string{},
[]string{},
)
require.NoError(t, err, "all functions should be accessible from PHP")
err = suite.verifyFunctionBehavior(`<?php
$result = test_uppercase("hello world");
if ($result !== "HELLO WORLD") {
echo "FAIL: test_uppercase expected 'HELLO WORLD', got '$result'";
exit(1);
}
$result = test_uppercase("");
if ($result !== "") {
echo "FAIL: test_uppercase with empty string expected '', got '$result'";
exit(1);
}
$sum = test_add_numbers(5, 7);
if ($sum !== 12) {
echo "FAIL: test_add_numbers(5, 7) expected 12, got $sum";
exit(1);
}
$result = test_is_enabled(true);
if ($result !== false) {
echo "FAIL: test_is_enabled(true) expected false, got " . ($result ? "true" : "false");
exit(1);
}
$result = test_is_enabled(false);
if ($result !== true) {
echo "FAIL: test_is_enabled(false) expected true, got " . ($result ? "true" : "false");
exit(1);
}
echo "OK";
`, "OK")
require.NoError(t, err, "all function calls should work correctly")
}
func TestClassMethodsIntegration(t *testing.T) {
suite := setupTest(t)
sourceFile := filepath.Join("..", "..", "testdata", "integration", "class_methods.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)
err = suite.verifyPHPSymbols(
[]string{},
[]string{"Counter", "StringHolder"},
[]string{},
)
require.NoError(t, err, "all classes should be accessible from PHP")
err = suite.verifyFunctionBehavior(`<?php
$counter = new Counter();
if ($counter->getValue() !== 0) {
echo "FAIL: Counter initial value expected 0, got " . $counter->getValue();
exit(1);
}
$counter->increment();
if ($counter->getValue() !== 1) {
echo "FAIL: Counter after increment expected 1, got " . $counter->getValue();
exit(1);
}
$counter->decrement();
if ($counter->getValue() !== 0) {
echo "FAIL: Counter after decrement expected 0, got " . $counter->getValue();
exit(1);
}
$counter->setValue(10);
if ($counter->getValue() !== 10) {
echo "FAIL: Counter after setValue(10) expected 10, got " . $counter->getValue();
exit(1);
}
$newValue = $counter->addValue(5);
if ($newValue !== 15) {
echo "FAIL: Counter addValue(5) expected to return 15, got $newValue";
exit(1);
}
if ($counter->getValue() !== 15) {
echo "FAIL: Counter value after addValue(5) expected 15, got " . $counter->getValue();
exit(1);
}
$counter->updateWithNullable(50);
if ($counter->getValue() !== 50) {
echo "FAIL: Counter after updateWithNullable(50) expected 50, got " . $counter->getValue();
exit(1);
}
$counter->updateWithNullable(null);
if ($counter->getValue() !== 50) {
echo "FAIL: Counter after updateWithNullable(null) expected 50 (unchanged), got " . $counter->getValue();
exit(1);
}
$counter->reset();
if ($counter->getValue() !== 0) {
echo "FAIL: Counter after reset expected 0, got " . $counter->getValue();
exit(1);
}
$counter1 = new Counter();
$counter2 = new Counter();
$counter1->setValue(100);
$counter2->setValue(200);
if ($counter1->getValue() !== 100 || $counter2->getValue() !== 200) {
echo "FAIL: Multiple Counter instances should be independent";
exit(1);
}
$holder = new StringHolder();
$holder->setData("test string");
if ($holder->getData() !== "test string") {
echo "FAIL: StringHolder getData expected 'test string', got '" . $holder->getData() . "'";
exit(1);
}
if ($holder->getLength() !== 11) {
echo "FAIL: StringHolder getLength expected 11, got " . $holder->getLength();
exit(1);
}
$holder->setData("");
if ($holder->getData() !== "") {
echo "FAIL: StringHolder empty string expected '', got '" . $holder->getData() . "'";
exit(1);
}
if ($holder->getLength() !== 0) {
echo "FAIL: StringHolder empty string length expected 0, got " . $holder->getLength();
exit(1);
}
echo "OK";
`, "OK")
require.NoError(t, err, "all class methods should work correctly")
}
func TestConstants(t *testing.T) {
suite := setupTest(t)
sourceFile := filepath.Join("..", "..", "testdata", "integration", "constants.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)
err = suite.verifyPHPSymbols(
[]string{"test_with_constants"},
[]string{"Config"},
[]string{
"TEST_MAX_RETRIES", "TEST_API_VERSION", "TEST_ENABLED", "TEST_PI",
"STATUS_PENDING", "STATUS_PROCESSING", "STATUS_COMPLETED",
},
)
require.NoError(t, err, "all constants, functions, and classes should be accessible from PHP")
err = suite.verifyFunctionBehavior(`<?php
if (TEST_MAX_RETRIES !== 100) {
echo "FAIL: TEST_MAX_RETRIES expected 100, got " . TEST_MAX_RETRIES;
exit(1);
}
if (TEST_API_VERSION !== "2.0.0") {
echo "FAIL: TEST_API_VERSION expected '2.0.0', got '" . TEST_API_VERSION . "'";
exit(1);
}
if (TEST_ENABLED !== true) {
var_dump(TEST_ENABLED);
echo "FAIL: TEST_ENABLED expected true, got " . (TEST_ENABLED ? "true" : "false");
exit(1);
}
if (abs(TEST_PI - 3.14159) > 0.00001) {
echo "FAIL: TEST_PI expected 3.14159, got " . TEST_PI;
exit(1);
}
if (Config::MODE_DEBUG !== 1) {
echo "FAIL: Config::MODE_DEBUG expected 1, got " . Config::MODE_DEBUG;
exit(1);
}
if (Config::MODE_PRODUCTION !== 2) {
echo "FAIL: Config::MODE_PRODUCTION expected 2, got " . Config::MODE_PRODUCTION;
exit(1);
}
if (Config::DEFAULT_TIMEOUT !== 30) {
echo "FAIL: Config::DEFAULT_TIMEOUT expected 30, got " . Config::DEFAULT_TIMEOUT;
exit(1);
}
$config = new Config();
$config->setMode(Config::MODE_DEBUG);
if ($config->getMode() !== Config::MODE_DEBUG) {
echo "FAIL: Config getMode expected MODE_DEBUG, got " . $config->getMode();
exit(1);
}
$result = test_with_constants(STATUS_PENDING);
if ($result !== "pending") {
echo "FAIL: test_with_constants(STATUS_PENDING) expected 'pending', got '$result'";
exit(1);
}
$result = test_with_constants(STATUS_PROCESSING);
if ($result !== "processing") {
echo "FAIL: test_with_constants(STATUS_PROCESSING) expected 'processing', got '$result'";
exit(1);
}
$result = test_with_constants(STATUS_COMPLETED);
if ($result !== "completed") {
echo "FAIL: test_with_constants(STATUS_COMPLETED) expected 'completed', got '$result'";
exit(1);
}
$result = test_with_constants(999);
if ($result !== "unknown") {
echo "FAIL: test_with_constants(999) expected 'unknown', got '$result'";
exit(1);
}
echo "OK";
`, "OK")
require.NoError(t, err, "all constants should have correct values and functions should work")
}
func TestNamespace(t *testing.T) {
suite := setupTest(t)
sourceFile := filepath.Join("..", "..", "testdata", "integration", "namespace.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)
err = suite.verifyPHPSymbols(
[]string{`\\TestIntegration\\Extension\\greet`},
[]string{`\\TestIntegration\\Extension\\Person`},
[]string{`\\TestIntegration\\Extension\\NAMESPACE_VERSION`},
)
require.NoError(t, err, "all namespaced symbols should be accessible from PHP")
err = suite.verifyFunctionBehavior(`<?php
use TestIntegration\Extension;
if (Extension\NAMESPACE_VERSION !== "1.0.0") {
echo "FAIL: NAMESPACE_VERSION expected '1.0.0', got '" . Extension\NAMESPACE_VERSION . "'";
exit(1);
}
$greeting = Extension\greet("Alice");
if ($greeting !== "Hello, Alice!") {
echo "FAIL: greet('Alice') expected 'Hello, Alice!', got '$greeting'";
exit(1);
}
$greeting = Extension\greet("");
if ($greeting !== "Hello, !") {
echo "FAIL: greet('') expected 'Hello, !', got '$greeting'";
exit(1);
}
if (Extension\Person::DEFAULT_AGE !== 18) {
echo "FAIL: Person::DEFAULT_AGE expected 18, got " . Extension\Person::DEFAULT_AGE;
exit(1);
}
$person = new Extension\Person();
$person->setName("Bob");
$person->setAge(25);
if ($person->getName() !== "Bob") {
echo "FAIL: Person getName expected 'Bob', got '" . $person->getName() . "'";
exit(1);
}
if ($person->getAge() !== 25) {
echo "FAIL: Person getAge expected 25, got " . $person->getAge();
exit(1);
}
$person->setAge(Extension\Person::DEFAULT_AGE);
if ($person->getAge() !== 18) {
echo "FAIL: Person setAge(DEFAULT_AGE) expected 18, got " . $person->getAge();
exit(1);
}
$person1 = new Extension\Person();
$person2 = new Extension\Person();
$person1->setName("Alice");
$person1->setAge(30);
$person2->setName("Charlie");
$person2->setAge(40);
if ($person1->getName() !== "Alice" || $person1->getAge() !== 30) {
echo "FAIL: person1 should have independent state";
exit(1);
}
if ($person2->getName() !== "Charlie" || $person2->getAge() !== 40) {
echo "FAIL: person2 should have independent state";
exit(1);
}
echo "OK";
`, "OK")
require.NoError(t, err, "all namespaced symbols should work correctly")
}
func TestInvalidSignature(t *testing.T) {
suite := setupTest(t)
sourceFile := filepath.Join("..", "..", "testdata", "integration", "invalid_signature.go")
sourceFile, err := filepath.Abs(sourceFile)
require.NoError(t, err)
targetFile, err := suite.createGoModule(sourceFile)
require.NoError(t, err)
err = suite.runExtensionInit(targetFile)
assert.Error(t, err, "extension-init should fail for invalid return type")
assert.Contains(t, err.Error(), "no PHP functions, classes, or constants found", "invalid functions should be ignored, resulting in no valid exports")
}
func TestTypeMismatch(t *testing.T) {
suite := setupTest(t)
sourceFile := filepath.Join("..", "..", "testdata", "integration", "type_mismatch.go")
sourceFile, err := filepath.Abs(sourceFile)
require.NoError(t, err)
targetFile, err := suite.createGoModule(sourceFile)
require.NoError(t, err)
err = suite.runExtensionInit(targetFile)
assert.NoError(t, err, "generation should succeed - class is valid even though function/method have type mismatches")
baseDir := filepath.Dir(targetFile)
baseName := strings.TrimSuffix(filepath.Base(targetFile), ".go")
stubFile := filepath.Join(baseDir, baseName+".stub.php")
assert.FileExists(t, stubFile, "stub file should be generated for valid class")
}
func TestMissingGenStub(t *testing.T) {
// temp override of GEN_STUB_SCRIPT
originalValue := os.Getenv("GEN_STUB_SCRIPT")
defer os.Setenv("GEN_STUB_SCRIPT", originalValue)
os.Setenv("GEN_STUB_SCRIPT", "/nonexistent/gen_stub.php")
tempDir := t.TempDir()
sourceFile := filepath.Join(tempDir, "test.go")
err := os.WriteFile(sourceFile, []byte(`package test
//export_php:function dummy(): void
func dummy() {}
`), 0o644)
require.NoError(t, err)
baseName := SanitizePackageName(strings.TrimSuffix(filepath.Base(sourceFile), ".go"))
gen := Generator{
BaseName: baseName,
SourceFile: sourceFile,
BuildDir: filepath.Dir(sourceFile),
}
err = gen.Generate()
assert.Error(t, err, "should fail when gen_stub.php is missing")
assert.Contains(t, err.Error(), "gen_stub.php", "error should mention missing script")
}

32
testdata/integration/basic_function.go vendored Normal file
View File

@@ -0,0 +1,32 @@
package testintegration
// #include <Zend/zend_types.h>
import "C"
import (
"strings"
"unsafe"
"github.com/dunglas/frankenphp"
)
// export_php:function test_uppercase(string $str): string
func test_uppercase(s *C.zend_string) unsafe.Pointer {
str := frankenphp.GoString(unsafe.Pointer(s))
upper := strings.ToUpper(str)
return frankenphp.PHPString(upper, false)
}
// export_php:function test_add_numbers(int $a, int $b): int
func test_add_numbers(a int64, b int64) int64 {
return a + b
}
// export_php:function test_multiply(float $a, float $b): float
func test_multiply(a float64, b float64) float64 {
return a * b
}
// export_php:function test_is_enabled(bool $flag): bool
func test_is_enabled(flag bool) bool {
return !flag
}

72
testdata/integration/class_methods.go vendored Normal file
View File

@@ -0,0 +1,72 @@
package testintegration
// #include <Zend/zend_types.h>
import "C"
import (
"unsafe"
"github.com/dunglas/frankenphp"
)
// export_php:class Counter
type CounterStruct struct {
Value int
}
// export_php:method Counter::increment(): void
func (c *CounterStruct) Increment() {
c.Value++
}
// export_php:method Counter::decrement(): void
func (c *CounterStruct) Decrement() {
c.Value--
}
// export_php:method Counter::getValue(): int
func (c *CounterStruct) GetValue() int64 {
return int64(c.Value)
}
// export_php:method Counter::setValue(int $value): void
func (c *CounterStruct) SetValue(value int64) {
c.Value = int(value)
}
// export_php:method Counter::reset(): void
func (c *CounterStruct) Reset() {
c.Value = 0
}
// export_php:method Counter::addValue(int $amount): int
func (c *CounterStruct) AddValue(amount int64) int64 {
c.Value += int(amount)
return int64(c.Value)
}
// export_php:method Counter::updateWithNullable(?int $newValue): void
func (c *CounterStruct) UpdateWithNullable(newValue *int64) {
if newValue != nil {
c.Value = int(*newValue)
}
}
// export_php:class StringHolder
type StringHolderStruct struct {
Data string
}
// export_php:method StringHolder::setData(string $data): void
func (sh *StringHolderStruct) SetData(data *C.zend_string) {
sh.Data = frankenphp.GoString(unsafe.Pointer(data))
}
// export_php:method StringHolder::getData(): string
func (sh *StringHolderStruct) GetData() unsafe.Pointer {
return frankenphp.PHPString(sh.Data, false)
}
// export_php:method StringHolder::getLength(): int
func (sh *StringHolderStruct) GetLength() int64 {
return int64(len(sh.Data))
}

70
testdata/integration/constants.go vendored Normal file
View File

@@ -0,0 +1,70 @@
package testintegration
// #include <Zend/zend_types.h>
import "C"
import (
"unsafe"
"github.com/dunglas/frankenphp"
)
// export_php:const
const TEST_MAX_RETRIES = 100
// export_php:const
const TEST_API_VERSION = "2.0.0"
// export_php:const
const TEST_ENABLED = true
// export_php:const
const TEST_PI = 3.14159
// export_php:const
const STATUS_PENDING = iota
// export_php:const
const STATUS_PROCESSING = iota
// export_php:const
const STATUS_COMPLETED = iota
// export_php:class Config
type ConfigStruct struct {
Mode int
}
// export_php:classconst Config
const MODE_DEBUG = 1
// export_php:classconst Config
const MODE_PRODUCTION = 2
// export_php:classconst Config
const DEFAULT_TIMEOUT = 30
// export_php:method Config::setMode(int $mode): void
func (c *ConfigStruct) SetMode(mode int64) {
c.Mode = int(mode)
}
// export_php:method Config::getMode(): int
func (c *ConfigStruct) GetMode() int64 {
return int64(c.Mode)
}
// export_php:function test_with_constants(int $status): string
func test_with_constants(status int64) unsafe.Pointer {
var result string
switch status {
case STATUS_PENDING:
result = "pending"
case STATUS_PROCESSING:
result = "processing"
case STATUS_COMPLETED:
result = "completed"
default:
result = "unknown"
}
return frankenphp.PHPString(result, false)
}

View File

@@ -0,0 +1,9 @@
package testintegration
// #include <Zend/zend_types.h>
import "C"
// export_php:function invalid_return_type(string $str): unsupported_type
func invalid_return_type(s *C.zend_string) int {
return 42
}

50
testdata/integration/namespace.go vendored Normal file
View File

@@ -0,0 +1,50 @@
package testintegration
// export_php:namespace TestIntegration\Extension
// #include <Zend/zend_types.h>
import "C"
import (
"unsafe"
"github.com/dunglas/frankenphp"
)
// export_php:const
const NAMESPACE_VERSION = "1.0.0"
// export_php:function greet(string $name): string
func greet(name *C.zend_string) unsafe.Pointer {
str := frankenphp.GoString(unsafe.Pointer(name))
result := "Hello, " + str + "!"
return frankenphp.PHPString(result, false)
}
// export_php:class Person
type PersonStruct struct {
Name string
Age int
}
// export_php:method Person::setName(string $name): void
func (p *PersonStruct) SetName(name *C.zend_string) {
p.Name = frankenphp.GoString(unsafe.Pointer(name))
}
// export_php:method Person::getName(): string
func (p *PersonStruct) GetName() unsafe.Pointer {
return frankenphp.PHPString(p.Name, false)
}
// export_php:method Person::setAge(int $age): void
func (p *PersonStruct) SetAge(age int64) {
p.Age = int(age)
}
// export_php:method Person::getAge(): int
func (p *PersonStruct) GetAge() int64 {
return int64(p.Age)
}
// export_php:classconst Person
const DEFAULT_AGE = 18

19
testdata/integration/type_mismatch.go vendored Normal file
View File

@@ -0,0 +1,19 @@
package testintegration
// #include <Zend/zend_types.h>
import "C"
// export_php:function mismatched_param_type(int $value): int
func mismatched_param_type(value string) int64 {
return 0
}
// export_php:class BadClass
type BadClassStruct struct {
Value int
}
// export_php:method BadClass::wrongReturnType(): string
func (bc *BadClassStruct) WrongReturnType() int {
return bc.Value
}