Files
archived-frankenphp/internal/extgen/utils_test.go
Kévin Dunglas 25ed020036 feat: Windows support (#2119)
Closes #83 #880 #1286.

Working patch for Windows support.

Supports linking to the [official PHP release (TS
version)](https://www.php.net/downloads.php).
Includes some work from #1286 (thanks @TenHian!!)

This patch allows using Visual Studio to compile the cgo code. To do so,
it must be compiled with Go 1.26 (RC) with the following setup:

```powershell
winget install -e --id Microsoft.VisualStudio.2022.Community --override "--passive --wait --add Microsoft.VisualStudio.Workload.NativeDesktop --add Microsoft.VisualStudio.Component.VC.Llvm.Clang --includeRecommended"
winget install -e --id GoLang.Go

$env:PATH += ';C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\Llvm\bin'

cd c:\
gh repo clone microsoft/vcpkg
.\vcpkg\bootstrap-vcpkg.bat
.\vcpkg\vcpkg install pthreads brotli

# build watcher
Invoke-WebRequest -Uri "https://github.com/e-dant/watcher/releases/download/0.14.3/x86_64-pc-windows-msvc.tar" -OutFile "$env:TEMP\watcher.tar"
tar -xf "$env:TEMP\watcher.tar" -C C:\
Rename-Item -Path "C:\x86_64-pc-windows-msvc" -NewName "watcher-x86_64-pc-windows-msvc"
Remove-Item "$env:TEMP\watcher.tar"

# download php
Invoke-WebRequest -Uri "https://downloads.php.net/~windows/releases/archives/php-8.5.1-Win32-vs17-x64.zip" -OutFile "$env:TEMP\php.zip"
Expand-Archive -Path "$env:TEMP\php.zip" -DestinationPath "C:\"
Remove-Item "$env:TEMP\php.zip"

# download php development package
Invoke-WebRequest -Uri "https://downloads.php.net/~windows/releases/archives/php-devel-pack-8.5.1-Win32-vs17-x64.zip" -OutFile "$env:TEMP\php-devel.zip"
Expand-Archive -Path "$env:TEMP\php-devel.zip" -DestinationPath "C:\"
Remove-Item "$env:TEMP\php-devel.zip"

$env:GOTOOLCHAIN = 'go1.26rc1'
$env:CC = 'clang'
$env:CXX = 'clang++'
$env:CGO_CFLAGS = "-I$env:C:\vcpkg\installed\x64-windows\include -IC:\watcher-x86_64-pc-windows-msvc -IC:\php-8.5.1-devel-vs17-x64\include -IC:\php-8.5.1-devel-vs17-x64\include\main -IC:\php-8.5.1-devel-vs17-x64\include\TSRM -IC:\php-8.5.1-devel-vs17-x64\include\Zend -IC:\php-8.5.1-devel-vs17-x64\include\ext"
$env:CGO_LDFLAGS = '-LC:\vcpkg\installed\x64-windows\lib -lbrotlienc -LC:\watcher-x86_64-pc-windows-msvc -llibwatcher-c -LC:\php-8.5.1-Win32-vs17-x64 -LC:\php-8.5.1-devel-vs17-x64\lib -lphp8ts -lphp8embed'

# clone frankenphp and build
git clone -b windows https://github.com/php/frankenphp.git
cd frankenphp\caddy\frankenphp
go build -ldflags '-extldflags="-fuse-ld=lld"' -tags nowatcher,nobadger,nomysql,nopgx

# Tests

$env:PATH += ";$env:VCPKG_ROOT\installed\x64-windows\bin;C:\watcher-x86_64-pc-windows-msvc";C:\php-8.5.1-Win32-vs17-x64"
"opcache.enable=0`r`nopcache.enable_cli=0" | Out-File -Encoding ascii php.ini
$env:PHPRC = Get-Location
go test -ldflags '-extldflags="-fuse-ld=lld"' -tags nowatcher,nobadger,nomysql,nopgx .
```

TODO:

- [x] Fix remaining skipped tests (scaling and watcher)
- [x] Test if the watcher mode works as expected
- [x] Automate the build with GitHub Actions

---------

Signed-off-by: Marc <m@pyc.ac>
Co-authored-by: Kévin Dunglas <kevin@dunglas.dev>
Co-authored-by: DubbleClick <m@pyc.ac>
2026-02-26 12:38:14 +01:00

248 lines
5.2 KiB
Go

package extgen
import (
"os"
"path/filepath"
"runtime"
"testing"
"github.com/stretchr/testify/assert"
)
func TestWriteFile(t *testing.T) {
tests := []struct {
name string
filename string
content string
expectError bool
}{
{
name: "write simple file",
filename: "test.txt",
content: "hello world",
expectError: false,
},
{
name: "write empty file",
filename: "empty.txt",
content: "",
expectError: false,
},
{
name: "write file with special characters",
filename: "special.txt",
content: "hello\nworld\t!@#$%^&*()",
expectError: false,
},
{
name: "write to invalid directory",
filename: "/nonexistent/directory/file.txt",
content: "test",
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var filename string
if !tt.expectError {
tempDir := t.TempDir()
filename = filepath.Join(tempDir, tt.filename)
} else {
filename = tt.filename
}
err := writeFile(filename, tt.content)
if tt.expectError {
assert.Error(t, err, "writeFile() should return an error")
return
}
assert.NoError(t, err, "writeFile() should not return an error")
content, err := os.ReadFile(filename)
assert.NoError(t, err, "Failed to read written file")
assert.Equal(t, tt.content, string(content), "writeFile() content mismatch")
info, err := os.Stat(filename)
assert.NoError(t, err, "Failed to stat file")
expectedMode := os.FileMode(0644)
if runtime.GOOS == "windows" {
expectedMode = os.FileMode(0666)
}
assert.Equal(t, expectedMode, info.Mode().Perm(), "writeFile() wrong permissions")
})
}
}
func TestReadFile(t *testing.T) {
tests := []struct {
name string
content string
expectError bool
}{
{
name: "read simple file",
content: "hello world",
expectError: false,
},
{
name: "read empty file",
content: "",
expectError: false,
},
{
name: "read file with special characters",
content: "hello\nworld\t!@#$%^&*()",
expectError: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tempDir := t.TempDir()
filename := filepath.Join(tempDir, "test.txt")
err := os.WriteFile(filename, []byte(tt.content), 0644)
assert.NoError(t, err, "Failed to create test file")
content, err := readFile(filename)
if tt.expectError {
assert.Error(t, err, "readFile() should return an error")
return
}
assert.NoError(t, err, "readFile() should not return an error")
assert.Equal(t, tt.content, content, "readFile() content mismatch")
})
}
t.Run("read nonexistent file", func(t *testing.T) {
_, err := readFile("/nonexistent/file.txt")
assert.Error(t, err, "readFile() should return an error for nonexistent file")
})
}
func TestSanitizePackageName(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{
name: "simple valid name",
input: "mypackage",
expected: "mypackage",
},
{
name: "name with hyphens",
input: "my-package",
expected: "my_package",
},
{
name: "name with dots",
input: "my.package",
expected: "my_package",
},
{
name: "name with both hyphens and dots",
input: "my-package.name",
expected: "my_package_name",
},
{
name: "name starting with number",
input: "123package",
expected: "_123package",
},
{
name: "name starting with underscore",
input: "_package",
expected: "_package",
},
{
name: "name starting with letter",
input: "Package",
expected: "Package",
},
{
name: "name starting with special character",
input: "@package",
expected: "_@package",
},
{
name: "complex name",
input: "123my-complex.package@name",
expected: "_123my_complex_package@name",
},
{
name: "empty string",
input: "",
expected: "",
},
{
name: "single character letter",
input: "a",
expected: "a",
},
{
name: "single character number",
input: "1",
expected: "_1",
},
{
name: "single character underscore",
input: "_",
expected: "_",
},
{
name: "single character special",
input: "@",
expected: "_@",
},
{
name: "multiple consecutive hyphens",
input: "my--package",
expected: "my__package",
},
{
name: "multiple consecutive dots",
input: "my..package",
expected: "my__package",
},
{
name: "mixed case with special chars",
input: "MyPackage-name.version",
expected: "MyPackage_name_version",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := SanitizePackageName(tt.input)
assert.Equal(t, tt.expected, result, "SanitizePackageName(%q)", tt.input)
})
}
}
func BenchmarkSanitizePackageName(b *testing.B) {
testCases := []string{
"simple",
"my-package",
"my.package.name",
"123complex-package.name@version",
"very-long-package-name-with-many-special-characters.and.dots",
}
for _, tc := range testCases {
b.Run(tc, func(b *testing.B) {
for b.Loop() {
SanitizePackageName(tc)
}
})
}
}