test: add comprehensive tests and CI for export feature
Add extensive test coverage, demo program, and CI integration for //export with different names feature: Unit Tests (cl/builtin_test.go): - TestHandleExportDiffName: core functionality with 4 scenarios * Different names with enableExportRename * Same names with enableExportRename * Different names with spaces in export directive * Matching names without enableExportRename - TestInitLinknameByDocExportDiffNames: flag behavior verification * Export with different names when enabled * Export with same name when enabled * Normal linkname directives - TestInitLinkExportDiffNames: edge case handling * Symbol not found in decl packages (silent handling) Demo (_demo/embed/export/): - Example program demonstrating various export patterns - Verification script testing both embedded and non-embedded targets - Documents expected behavior and error cases CI Integration (.github/workflows/llgo.yml): - Add export demo to embedded target tests - Ensure feature works correctly across platforms - Catch regressions in future changes The tests verify: ✓ Different names work with -target flag ✓ Same names work in all cases ✓ Different names fail without -target flag ✓ Proper error messages for invalid exports ✓ Silent handling for decl packages 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2
.github/actions/setup-deps/action.yml
vendored
2
.github/actions/setup-deps/action.yml
vendored
@@ -16,6 +16,8 @@ runs:
|
|||||||
- name: Install macOS dependencies
|
- name: Install macOS dependencies
|
||||||
if: runner.os == 'macOS'
|
if: runner.os == 'macOS'
|
||||||
shell: bash
|
shell: bash
|
||||||
|
env:
|
||||||
|
HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK: 1
|
||||||
run: |
|
run: |
|
||||||
brew update
|
brew update
|
||||||
|
|
||||||
|
|||||||
7
.github/workflows/llgo.yml
vendored
7
.github/workflows/llgo.yml
vendored
@@ -133,6 +133,13 @@ jobs:
|
|||||||
chmod +x test.sh
|
chmod +x test.sh
|
||||||
./test.sh
|
./test.sh
|
||||||
|
|
||||||
|
- name: Test export with different symbol names on embedded targets
|
||||||
|
run: |
|
||||||
|
echo "Testing //export with different symbol names on embedded targets..."
|
||||||
|
cd _demo/embed/export
|
||||||
|
chmod +x verify_export.sh
|
||||||
|
./verify_export.sh
|
||||||
|
|
||||||
- name: _xtool build tests
|
- name: _xtool build tests
|
||||||
run: |
|
run: |
|
||||||
cd _xtool
|
cd _xtool
|
||||||
|
|||||||
67
_demo/embed/export/main.go
Normal file
67
_demo/embed/export/main.go
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/goplus/lib/c"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This demo shows how to use //export with different symbol names on embedded targets.
|
||||||
|
//
|
||||||
|
// On embedded targets, you can export Go functions with different C symbol names.
|
||||||
|
// This is useful for hardware interrupt handlers that require specific names.
|
||||||
|
|
||||||
|
// Standard Go export - same name
|
||||||
|
//
|
||||||
|
//export HelloWorld
|
||||||
|
func HelloWorld() {
|
||||||
|
c.Printf(c.Str("Hello from "))
|
||||||
|
c.Printf(c.Str("HelloWorld\n"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Embedded target export - different name
|
||||||
|
// Go function name: interruptLPSPI2
|
||||||
|
// Exported C symbol: LPSPI2_IRQHandler
|
||||||
|
//
|
||||||
|
//export LPSPI2_IRQHandler
|
||||||
|
func interruptLPSPI2() {
|
||||||
|
c.Printf(c.Str("LPSPI2 interrupt "))
|
||||||
|
c.Printf(c.Str("handler called\n"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Embedded target export - different name
|
||||||
|
// Go function name: systemTickHandler
|
||||||
|
// Exported C symbol: SysTick_Handler
|
||||||
|
//
|
||||||
|
//export SysTick_Handler
|
||||||
|
func systemTickHandler() {
|
||||||
|
c.Printf(c.Str("SysTick "))
|
||||||
|
c.Printf(c.Str("handler called\n"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Embedded target export - different name
|
||||||
|
// Go function name: Add
|
||||||
|
// Exported C symbol: AddFunc
|
||||||
|
//
|
||||||
|
//export AddFunc
|
||||||
|
func Add(a, b int) int {
|
||||||
|
result := a + b
|
||||||
|
c.Printf(c.Str("AddFunc(%d, %d) = %d\n"), a, b, result)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
c.Printf(c.Str("=== Export Demo ===\n\n"))
|
||||||
|
|
||||||
|
// Call exported functions directly from Go
|
||||||
|
c.Printf(c.Str("Calling HelloWorld:\n"))
|
||||||
|
HelloWorld()
|
||||||
|
|
||||||
|
c.Printf(c.Str("\nSimulating hardware interrupts:\n"))
|
||||||
|
interruptLPSPI2()
|
||||||
|
systemTickHandler()
|
||||||
|
|
||||||
|
c.Printf(c.Str("\nTesting function with return value:\n"))
|
||||||
|
result := Add(10, 20)
|
||||||
|
c.Printf(c.Str("Result: %d\n"), result)
|
||||||
|
|
||||||
|
c.Printf(c.Str("\n=== Demo Complete ===\n"))
|
||||||
|
}
|
||||||
54
_demo/embed/export/verify_export.sh
Executable file
54
_demo/embed/export/verify_export.sh
Executable file
@@ -0,0 +1,54 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "Building for embedded target..."
|
||||||
|
|
||||||
|
# Build for embedded target as executable
|
||||||
|
# Use llgo directly instead of llgo.sh to avoid go.mod version check
|
||||||
|
llgo build -o test-verify --target=esp32 .
|
||||||
|
|
||||||
|
echo "Checking exported symbols..."
|
||||||
|
|
||||||
|
# Get exported symbols
|
||||||
|
exported_symbols=$(nm -gU ./test-verify.elf | grep -E "(HelloWorld|LPSPI2_IRQHandler|SysTick_Handler|AddFunc)" | awk '{print $NF}')
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Exported symbols:"
|
||||||
|
echo "$exported_symbols" | awk '{print " " $0}'
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check expected symbols
|
||||||
|
expected=("HelloWorld" "LPSPI2_IRQHandler" "SysTick_Handler" "AddFunc")
|
||||||
|
missing=""
|
||||||
|
|
||||||
|
for symbol in "${expected[@]}"; do
|
||||||
|
if ! echo "$exported_symbols" | grep -q "^$symbol$"; then
|
||||||
|
missing="$missing $symbol"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -n "$missing" ]; then
|
||||||
|
echo "❌ Missing symbols:$missing"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✅ Symbol name mapping verification:"
|
||||||
|
echo " HelloWorld -> HelloWorld"
|
||||||
|
echo " interruptLPSPI2 -> LPSPI2_IRQHandler"
|
||||||
|
echo " systemTickHandler -> SysTick_Handler"
|
||||||
|
echo " Add -> AddFunc"
|
||||||
|
echo ""
|
||||||
|
echo "🎉 All export symbols verified successfully!"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "Testing that non-embedded target rejects different export names..."
|
||||||
|
# Build without --target should fail with panic
|
||||||
|
if llgo build -o test-notarget . 2>&1 | grep -q 'export comment has wrong name "LPSPI2_IRQHandler"'; then
|
||||||
|
echo "✅ Correctly rejected different export name on non-embedded target"
|
||||||
|
else
|
||||||
|
echo "❌ Should have panicked with 'export comment has wrong name' error"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
echo "Note: Different symbol names are only supported on embedded targets."
|
||||||
@@ -393,10 +393,10 @@ func TestErrImport(t *testing.T) {
|
|||||||
|
|
||||||
func TestErrInitLinkname(t *testing.T) {
|
func TestErrInitLinkname(t *testing.T) {
|
||||||
var ctx context
|
var ctx context
|
||||||
ctx.initLinkname("//llgo:link abc", func(name string) (string, bool, bool) {
|
ctx.initLinkname("//llgo:link abc", func(name string, isExport bool) (string, bool, bool) {
|
||||||
return "", false, false
|
return "", false, false
|
||||||
})
|
})
|
||||||
ctx.initLinkname("//go:linkname Printf printf", func(name string) (string, bool, bool) {
|
ctx.initLinkname("//go:linkname Printf printf", func(name string, isExport bool) (string, bool, bool) {
|
||||||
return "", false, false
|
return "", false, false
|
||||||
})
|
})
|
||||||
defer func() {
|
defer func() {
|
||||||
@@ -404,7 +404,7 @@ func TestErrInitLinkname(t *testing.T) {
|
|||||||
t.Fatal("initLinkname: no error?")
|
t.Fatal("initLinkname: no error?")
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
ctx.initLinkname("//go:linkname Printf printf", func(name string) (string, bool, bool) {
|
ctx.initLinkname("//go:linkname Printf printf", func(name string, isExport bool) (string, bool, bool) {
|
||||||
return "foo.Printf", false, name == "Printf"
|
return "foo.Printf", false, name == "Printf"
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -506,3 +506,238 @@ func TestInstantiate(t *testing.T) {
|
|||||||
t.Fatal("error")
|
t.Fatal("error")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHandleExportDiffName(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
enableExportRename bool
|
||||||
|
line string
|
||||||
|
fullName string
|
||||||
|
inPkgName string
|
||||||
|
wantHasLinkname bool
|
||||||
|
wantLinkname string
|
||||||
|
wantExport string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "ExportDiffNames_DifferentName",
|
||||||
|
enableExportRename: true,
|
||||||
|
line: "//export IRQ_Handler",
|
||||||
|
fullName: "pkg.HandleInterrupt",
|
||||||
|
inPkgName: "HandleInterrupt",
|
||||||
|
wantHasLinkname: true,
|
||||||
|
wantLinkname: "IRQ_Handler",
|
||||||
|
wantExport: "IRQ_Handler",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ExportDiffNames_SameName",
|
||||||
|
enableExportRename: true,
|
||||||
|
line: "//export SameName",
|
||||||
|
fullName: "pkg.SameName",
|
||||||
|
inPkgName: "SameName",
|
||||||
|
wantHasLinkname: true,
|
||||||
|
wantLinkname: "SameName",
|
||||||
|
wantExport: "SameName",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ExportDiffNames_WithSpaces",
|
||||||
|
enableExportRename: true,
|
||||||
|
line: "//export Timer_Callback ",
|
||||||
|
fullName: "pkg.OnTimerTick",
|
||||||
|
inPkgName: "OnTimerTick",
|
||||||
|
wantHasLinkname: true,
|
||||||
|
wantLinkname: "Timer_Callback",
|
||||||
|
wantExport: "Timer_Callback",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ExportDiffNames_Disabled_MatchingName",
|
||||||
|
enableExportRename: false,
|
||||||
|
line: "//export Func",
|
||||||
|
fullName: "pkg.Func",
|
||||||
|
inPkgName: "Func",
|
||||||
|
wantHasLinkname: true,
|
||||||
|
wantLinkname: "Func",
|
||||||
|
wantExport: "Func",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// Save and restore global state
|
||||||
|
oldEnableExportRename := enableExportRename
|
||||||
|
defer func() {
|
||||||
|
EnableExportRename(oldEnableExportRename)
|
||||||
|
}()
|
||||||
|
EnableExportRename(tt.enableExportRename)
|
||||||
|
|
||||||
|
// Setup context
|
||||||
|
prog := llssa.NewProgram(nil)
|
||||||
|
pkg := prog.NewPackage("test", "test")
|
||||||
|
ctx := &context{
|
||||||
|
prog: prog,
|
||||||
|
pkg: pkg,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call initLinkname with closure that mimics initLinknameByDoc behavior
|
||||||
|
ret := ctx.initLinkname(tt.line, func(name string, isExport bool) (string, bool, bool) {
|
||||||
|
return tt.fullName, false, name == tt.inPkgName || (isExport && enableExportRename)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Verify result
|
||||||
|
hasLinkname := (ret == hasLinkname)
|
||||||
|
if hasLinkname != tt.wantHasLinkname {
|
||||||
|
t.Errorf("hasLinkname = %v, want %v", hasLinkname, tt.wantHasLinkname)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tt.wantHasLinkname {
|
||||||
|
// Check linkname was set
|
||||||
|
if link, ok := prog.Linkname(tt.fullName); !ok || link != tt.wantLinkname {
|
||||||
|
t.Errorf("linkname = %q (ok=%v), want %q", link, ok, tt.wantLinkname)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check export was set
|
||||||
|
exports := pkg.ExportFuncs()
|
||||||
|
if export, ok := exports[tt.fullName]; !ok || export != tt.wantExport {
|
||||||
|
t.Errorf("export = %q (ok=%v), want %q", export, ok, tt.wantExport)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInitLinknameByDocExportDiffNames(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
enableExportRename bool
|
||||||
|
doc *ast.CommentGroup
|
||||||
|
fullName string
|
||||||
|
inPkgName string
|
||||||
|
wantExported bool // Whether the symbol should be exported with different name
|
||||||
|
wantLinkname string
|
||||||
|
wantExport string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "WithExportDiffNames_DifferentNameExported",
|
||||||
|
enableExportRename: true,
|
||||||
|
doc: &ast.CommentGroup{
|
||||||
|
List: []*ast.Comment{
|
||||||
|
{Text: "//export IRQ_Handler"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fullName: "pkg.HandleInterrupt",
|
||||||
|
inPkgName: "HandleInterrupt",
|
||||||
|
wantExported: true,
|
||||||
|
wantLinkname: "IRQ_Handler",
|
||||||
|
wantExport: "IRQ_Handler",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "WithoutExportDiffNames_NotExported",
|
||||||
|
enableExportRename: false,
|
||||||
|
doc: &ast.CommentGroup{
|
||||||
|
List: []*ast.Comment{
|
||||||
|
{Text: "//export DifferentName"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fullName: "pkg.HandleInterrupt",
|
||||||
|
inPkgName: "HandleInterrupt",
|
||||||
|
wantExported: false,
|
||||||
|
// Without enableExportRename, it goes through normal flow which expects same name
|
||||||
|
// The symbol "DifferentName" won't be found, so no export happens
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// Without enableExportRename, export with different name will panic
|
||||||
|
if !tt.wantExported && !tt.enableExportRename {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r == nil {
|
||||||
|
t.Error("expected panic for export with different name when enableExportRename=false")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save and restore global state
|
||||||
|
oldEnableExportRename := enableExportRename
|
||||||
|
defer func() {
|
||||||
|
EnableExportRename(oldEnableExportRename)
|
||||||
|
}()
|
||||||
|
EnableExportRename(tt.enableExportRename)
|
||||||
|
|
||||||
|
// Setup context
|
||||||
|
prog := llssa.NewProgram(nil)
|
||||||
|
pkg := prog.NewPackage("test", "test")
|
||||||
|
ctx := &context{
|
||||||
|
prog: prog,
|
||||||
|
pkg: pkg,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call initLinknameByDoc
|
||||||
|
ctx.initLinknameByDoc(tt.doc, tt.fullName, tt.inPkgName, false)
|
||||||
|
|
||||||
|
// Verify export behavior
|
||||||
|
exports := pkg.ExportFuncs()
|
||||||
|
if tt.wantExported {
|
||||||
|
// Should have exported the symbol with different name
|
||||||
|
if export, ok := exports[tt.fullName]; !ok || export != tt.wantExport {
|
||||||
|
t.Errorf("export = %q (ok=%v), want %q", export, ok, tt.wantExport)
|
||||||
|
}
|
||||||
|
// Check linkname was also set
|
||||||
|
if link, ok := prog.Linkname(tt.fullName); !ok || link != tt.wantLinkname {
|
||||||
|
t.Errorf("linkname = %q (ok=%v), want %q", link, ok, tt.wantLinkname)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInitLinkExportDiffNames(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
enableExportRename bool
|
||||||
|
line string
|
||||||
|
wantPanic bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "ExportDiffNames_Enabled_NoError",
|
||||||
|
enableExportRename: true,
|
||||||
|
line: "//export IRQ_Handler",
|
||||||
|
wantPanic: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ExportDiffNames_Disabled_Panic",
|
||||||
|
enableExportRename: false,
|
||||||
|
line: "//export IRQ_Handler",
|
||||||
|
wantPanic: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if tt.wantPanic {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r == nil {
|
||||||
|
t.Error("expected panic but didn't panic")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
oldEnableExportRename := enableExportRename
|
||||||
|
defer func() {
|
||||||
|
EnableExportRename(oldEnableExportRename)
|
||||||
|
}()
|
||||||
|
EnableExportRename(tt.enableExportRename)
|
||||||
|
|
||||||
|
prog := llssa.NewProgram(nil)
|
||||||
|
pkg := prog.NewPackage("test", "test")
|
||||||
|
ctx := &context{
|
||||||
|
prog: prog,
|
||||||
|
pkg: pkg,
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.initLinkname(tt.line, func(inPkgName string, isExport bool) (fullName string, isVar, ok bool) {
|
||||||
|
// Simulate initLinknames scenario: symbol not found (like in decl packages)
|
||||||
|
return "", false, false
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user