mirror of
https://github.com/navidrome/navidrome.git
synced 2026-03-04 06:35:52 +00:00
420 lines
16 KiB
Go
420 lines
16 KiB
Go
package main
|
|
|
|
import (
|
|
"go/format"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
)
|
|
|
|
var testdataDir string
|
|
|
|
func readTestdata(filename string) string {
|
|
content, err := os.ReadFile(filepath.Join(testdataDir, filename))
|
|
Expect(err).ToNot(HaveOccurred(), "Failed to read testdata file: %s", filename)
|
|
return string(content)
|
|
}
|
|
|
|
var _ = Describe("hostgen CLI", Ordered, func() {
|
|
var (
|
|
testDir string
|
|
outputDir string
|
|
hostgenBin string
|
|
)
|
|
|
|
BeforeAll(func() {
|
|
// Set testdata directory
|
|
testdataDir = filepath.Join(mustGetWd(GinkgoT()), "plugins", "cmd", "hostgen", "testdata")
|
|
|
|
// Build the hostgen binary
|
|
hostgenBin = filepath.Join(os.TempDir(), "hostgen-test")
|
|
cmd := exec.Command("go", "build", "-o", hostgenBin, ".")
|
|
cmd.Dir = filepath.Join(mustGetWd(GinkgoT()), "plugins", "cmd", "hostgen")
|
|
output, err := cmd.CombinedOutput()
|
|
Expect(err).ToNot(HaveOccurred(), "Failed to build hostgen: %s", output)
|
|
DeferCleanup(func() {
|
|
os.Remove(hostgenBin)
|
|
})
|
|
})
|
|
|
|
BeforeEach(func() {
|
|
var err error
|
|
testDir, err = os.MkdirTemp("", "hostgen-test-input-*")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
outputDir, err = os.MkdirTemp("", "hostgen-test-output-*")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
})
|
|
|
|
AfterEach(func() {
|
|
os.RemoveAll(testDir)
|
|
os.RemoveAll(outputDir)
|
|
})
|
|
|
|
Describe("CLI flags and behavior", func() {
|
|
BeforeEach(func() {
|
|
serviceCode := `package testpkg
|
|
|
|
import "context"
|
|
|
|
//nd:hostservice name=Test permission=test
|
|
type TestService interface {
|
|
//nd:hostfunc
|
|
DoAction(ctx context.Context, input string) (output string, err error)
|
|
}
|
|
`
|
|
Expect(os.WriteFile(filepath.Join(testDir, "service.go"), []byte(serviceCode), 0600)).To(Succeed())
|
|
})
|
|
|
|
It("supports verbose mode", func() {
|
|
cmd := exec.Command(hostgenBin, "-input", testDir, "-output", outputDir, "-package", "testpkg", "-v")
|
|
output, err := cmd.CombinedOutput()
|
|
Expect(err).ToNot(HaveOccurred(), "Command failed: %s", output)
|
|
|
|
outputStr := string(output)
|
|
Expect(outputStr).To(ContainSubstring("Input directory:"))
|
|
Expect(outputStr).To(ContainSubstring("Output directory:"))
|
|
Expect(outputStr).To(ContainSubstring("Found 1 host service(s)"))
|
|
Expect(outputStr).To(ContainSubstring("Generated"))
|
|
})
|
|
|
|
It("supports dry-run mode", func() {
|
|
cmd := exec.Command(hostgenBin, "-input", testDir, "-output", outputDir, "-package", "testpkg", "-dry-run")
|
|
output, err := cmd.CombinedOutput()
|
|
Expect(err).ToNot(HaveOccurred(), "Command failed: %s", output)
|
|
|
|
Expect(string(output)).To(ContainSubstring("RegisterTestHostFunctions"))
|
|
Expect(filepath.Join(outputDir, "test_gen.go")).ToNot(BeAnExistingFile())
|
|
})
|
|
|
|
It("infers package name from output directory", func() {
|
|
customOutput, err := os.MkdirTemp("", "mypkg")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
defer os.RemoveAll(customOutput)
|
|
|
|
cmd := exec.Command(hostgenBin, "-input", testDir, "-output", customOutput)
|
|
_, err = cmd.CombinedOutput()
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
content, err := os.ReadFile(filepath.Join(customOutput, "test_gen.go"))
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(string(content)).To(ContainSubstring("package mypkg"))
|
|
})
|
|
|
|
It("returns error for invalid input directory", func() {
|
|
cmd := exec.Command(hostgenBin, "-input", "/nonexistent/path")
|
|
output, err := cmd.CombinedOutput()
|
|
Expect(err).To(HaveOccurred())
|
|
Expect(string(output)).To(ContainSubstring("Error parsing source files"))
|
|
})
|
|
|
|
It("handles no annotated services gracefully", func() {
|
|
Expect(os.WriteFile(filepath.Join(testDir, "service.go"), []byte("package testpkg\n"), 0600)).To(Succeed())
|
|
|
|
cmd := exec.Command(hostgenBin, "-input", testDir, "-output", outputDir, "-v")
|
|
output, err := cmd.CombinedOutput()
|
|
Expect(err).ToNot(HaveOccurred(), "Command failed: %s", output)
|
|
Expect(string(output)).To(ContainSubstring("No host services found"))
|
|
})
|
|
|
|
It("generates separate files for multiple services", func() {
|
|
// Remove service.go created by BeforeEach
|
|
Expect(os.Remove(filepath.Join(testDir, "service.go"))).To(Succeed())
|
|
|
|
service1 := `package testpkg
|
|
import "context"
|
|
//nd:hostservice name=ServiceA permission=a
|
|
type ServiceA interface {
|
|
//nd:hostfunc
|
|
MethodA(ctx context.Context) error
|
|
}
|
|
`
|
|
service2 := `package testpkg
|
|
import "context"
|
|
//nd:hostservice name=ServiceB permission=b
|
|
type ServiceB interface {
|
|
//nd:hostfunc
|
|
MethodB(ctx context.Context) error
|
|
}
|
|
`
|
|
Expect(os.WriteFile(filepath.Join(testDir, "a.go"), []byte(service1), 0600)).To(Succeed())
|
|
Expect(os.WriteFile(filepath.Join(testDir, "b.go"), []byte(service2), 0600)).To(Succeed())
|
|
|
|
cmd := exec.Command(hostgenBin, "-input", testDir, "-output", outputDir, "-package", "testpkg", "-v")
|
|
output, err := cmd.CombinedOutput()
|
|
Expect(err).ToNot(HaveOccurred(), "Command failed: %s", output)
|
|
Expect(string(output)).To(ContainSubstring("Found 2 host service(s)"))
|
|
|
|
Expect(filepath.Join(outputDir, "servicea_gen.go")).To(BeAnExistingFile())
|
|
Expect(filepath.Join(outputDir, "serviceb_gen.go")).To(BeAnExistingFile())
|
|
})
|
|
|
|
It("generates only host code with -host-only flag", func() {
|
|
cmd := exec.Command(hostgenBin, "-input", testDir, "-output", outputDir, "-package", "testpkg", "-host-only")
|
|
output, err := cmd.CombinedOutput()
|
|
Expect(err).ToNot(HaveOccurred(), "Command failed: %s", output)
|
|
|
|
Expect(filepath.Join(outputDir, "test_gen.go")).To(BeAnExistingFile())
|
|
Expect(filepath.Join(outputDir, "go")).ToNot(BeADirectory())
|
|
})
|
|
|
|
It("generates only client code with -plugin-only flag", func() {
|
|
cmd := exec.Command(hostgenBin, "-input", testDir, "-output", outputDir, "-package", "main", "-plugin-only")
|
|
output, err := cmd.CombinedOutput()
|
|
Expect(err).ToNot(HaveOccurred(), "Command failed: %s", output)
|
|
|
|
// Host code should not exist in output root
|
|
entries, err := os.ReadDir(outputDir)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
var genFiles []string
|
|
for _, e := range entries {
|
|
if e.Name() != "go" {
|
|
genFiles = append(genFiles, e.Name())
|
|
}
|
|
}
|
|
Expect(genFiles).To(BeEmpty(), "Expected no host code files, found: %v", genFiles)
|
|
|
|
// Client code should exist in go/ subdirectory
|
|
Expect(filepath.Join(outputDir, "go")).To(BeADirectory())
|
|
Expect(filepath.Join(outputDir, "go", "nd_host_test.go")).To(BeAnExistingFile())
|
|
})
|
|
|
|
It("generates both host and client code by default", func() {
|
|
cmd := exec.Command(hostgenBin, "-input", testDir, "-output", outputDir, "-package", "testpkg")
|
|
output, err := cmd.CombinedOutput()
|
|
Expect(err).ToNot(HaveOccurred(), "Command failed: %s", output)
|
|
|
|
// Host code in output root
|
|
Expect(filepath.Join(outputDir, "test_gen.go")).To(BeAnExistingFile())
|
|
|
|
// Client code in go/ subdirectory
|
|
Expect(filepath.Join(outputDir, "go")).To(BeADirectory())
|
|
Expect(filepath.Join(outputDir, "go", "nd_host_test.go")).To(BeAnExistingFile())
|
|
})
|
|
|
|
It("rejects using both -host-only and -plugin-only together", func() {
|
|
cmd := exec.Command(hostgenBin, "-input", testDir, "-output", outputDir, "-package", "testpkg", "-host-only", "-plugin-only")
|
|
output, err := cmd.CombinedOutput()
|
|
Expect(err).To(HaveOccurred())
|
|
Expect(string(output)).To(ContainSubstring("-host-only and -plugin-only cannot be used together"))
|
|
})
|
|
})
|
|
|
|
Describe("code generation", func() {
|
|
DescribeTable("generates correct host and client output",
|
|
func(serviceFile, hostExpectedFile, clientExpectedFile string) {
|
|
serviceCode := readTestdata(serviceFile)
|
|
hostExpected := readTestdata(hostExpectedFile)
|
|
clientExpected := readTestdata(clientExpectedFile)
|
|
|
|
Expect(os.WriteFile(filepath.Join(testDir, "service.go"), []byte(serviceCode), 0600)).To(Succeed())
|
|
|
|
// Generate both host and client code in one run
|
|
cmd := exec.Command(hostgenBin, "-input", testDir, "-output", outputDir, "-package", "testpkg")
|
|
output, err := cmd.CombinedOutput()
|
|
Expect(err).ToNot(HaveOccurred(), "Command failed: %s", output)
|
|
|
|
// Verify host code
|
|
entries, err := os.ReadDir(outputDir)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
var hostFiles []string
|
|
for _, e := range entries {
|
|
if e.Name() != "go" && !e.IsDir() {
|
|
hostFiles = append(hostFiles, e.Name())
|
|
}
|
|
}
|
|
Expect(hostFiles).To(HaveLen(1), "Expected exactly one host file, got: %v", hostFiles)
|
|
|
|
hostActual, err := os.ReadFile(filepath.Join(outputDir, hostFiles[0]))
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
formattedHostActual, err := format.Source(hostActual)
|
|
Expect(err).ToNot(HaveOccurred(), "Generated host code is not valid Go:\n%s", hostActual)
|
|
|
|
formattedHostExpected, err := format.Source([]byte(hostExpected))
|
|
Expect(err).ToNot(HaveOccurred(), "Expected host code is not valid Go")
|
|
|
|
Expect(string(formattedHostActual)).To(Equal(string(formattedHostExpected)), "Host code mismatch")
|
|
|
|
// Verify client code
|
|
goDir := filepath.Join(outputDir, "go")
|
|
clientEntries, err := os.ReadDir(goDir)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(clientEntries).To(HaveLen(1), "Expected exactly one client file")
|
|
|
|
clientActual, err := os.ReadFile(filepath.Join(goDir, clientEntries[0].Name()))
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
formattedClientActual, err := format.Source(clientActual)
|
|
Expect(err).ToNot(HaveOccurred(), "Generated client code is not valid Go:\n%s", clientActual)
|
|
|
|
formattedClientExpected, err := format.Source([]byte(clientExpected))
|
|
Expect(err).ToNot(HaveOccurred(), "Expected client code is not valid Go")
|
|
|
|
Expect(string(formattedClientActual)).To(Equal(string(formattedClientExpected)), "Client code mismatch")
|
|
},
|
|
|
|
Entry("simple string params",
|
|
"echo_service.go", "echo_expected.go", "echo_client_expected.go"),
|
|
|
|
Entry("multiple simple params (int32)",
|
|
"math_service.go", "math_expected.go", "math_client_expected.go"),
|
|
|
|
Entry("struct param with request type",
|
|
"store_service.go", "store_expected.go", "store_client_expected.go"),
|
|
|
|
Entry("mixed simple and complex params",
|
|
"list_service.go", "list_expected.go", "list_client_expected.go"),
|
|
|
|
Entry("method without error",
|
|
"counter_service.go", "counter_expected.go", "counter_client_expected.go"),
|
|
|
|
Entry("no params, error only",
|
|
"ping_service.go", "ping_expected.go", "ping_client_expected.go"),
|
|
|
|
Entry("map and interface types",
|
|
"meta_service.go", "meta_expected.go", "meta_client_expected.go"),
|
|
|
|
Entry("pointer types",
|
|
"users_service.go", "users_expected.go", "users_client_expected.go"),
|
|
|
|
Entry("multiple returns",
|
|
"search_service.go", "search_expected.go", "search_client_expected.go"),
|
|
|
|
Entry("bytes",
|
|
"codec_service.go", "codec_expected.go", "codec_client_expected.go"),
|
|
)
|
|
|
|
It("generates compilable host code for comprehensive service", func() {
|
|
serviceCode := readTestdata("comprehensive_service.go")
|
|
|
|
Expect(os.WriteFile(filepath.Join(testDir, "service.go"), []byte(serviceCode), 0600)).To(Succeed())
|
|
|
|
// Create go.mod
|
|
goMod := "module testpkg\n\ngo 1.23\n\nrequire github.com/extism/go-sdk v1.7.1\n"
|
|
Expect(os.WriteFile(filepath.Join(testDir, "go.mod"), []byte(goMod), 0600)).To(Succeed())
|
|
|
|
// Generate host code only
|
|
cmd := exec.Command(hostgenBin, "-input", testDir, "-output", testDir, "-package", "testpkg", "-host-only")
|
|
output, err := cmd.CombinedOutput()
|
|
Expect(err).ToNot(HaveOccurred(), "Generation failed: %s", output)
|
|
|
|
// Tidy dependencies
|
|
goGetCmd := exec.Command("go", "mod", "tidy")
|
|
goGetCmd.Dir = testDir
|
|
goGetOutput, err := goGetCmd.CombinedOutput()
|
|
Expect(err).ToNot(HaveOccurred(), "go mod tidy failed: %s", goGetOutput)
|
|
|
|
// Build
|
|
buildCmd := exec.Command("go", "build", ".")
|
|
buildCmd.Dir = testDir
|
|
buildOutput, err := buildCmd.CombinedOutput()
|
|
Expect(err).ToNot(HaveOccurred(), "Build failed: %s", buildOutput)
|
|
})
|
|
|
|
It("generates compilable client code for comprehensive service", func() {
|
|
serviceCode := readTestdata("comprehensive_service.go")
|
|
|
|
Expect(os.WriteFile(filepath.Join(testDir, "service.go"), []byte(serviceCode), 0600)).To(Succeed())
|
|
|
|
// Generate client code only to a separate client directory
|
|
clientDir := filepath.Join(outputDir, "client")
|
|
cmd := exec.Command(hostgenBin, "-input", testDir, "-output", clientDir, "-package", "main", "-plugin-only")
|
|
output, err := cmd.CombinedOutput()
|
|
Expect(err).ToNot(HaveOccurred(), "Generation failed: %s", output)
|
|
|
|
// Read generated client code
|
|
goDir := filepath.Join(clientDir, "go")
|
|
entries, err := os.ReadDir(goDir)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(entries).To(HaveLen(1), "Expected exactly one generated client file")
|
|
|
|
content, err := os.ReadFile(filepath.Join(goDir, entries[0].Name()))
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
// Verify key expected content first
|
|
contentStr := string(content)
|
|
// Should have wasmimport declarations for all methods
|
|
Expect(contentStr).To(ContainSubstring("//go:wasmimport extism:host/user comprehensive_simpleparams"))
|
|
Expect(contentStr).To(ContainSubstring("//go:wasmimport extism:host/user comprehensive_structparam"))
|
|
Expect(contentStr).To(ContainSubstring("//go:wasmimport extism:host/user comprehensive_noerror"))
|
|
Expect(contentStr).To(ContainSubstring("//go:wasmimport extism:host/user comprehensive_noparams"))
|
|
Expect(contentStr).To(ContainSubstring("//go:wasmimport extism:host/user comprehensive_noparamsnoreturns"))
|
|
|
|
// Should have response types for methods with complex returns
|
|
Expect(contentStr).To(ContainSubstring("type ComprehensiveSimpleParamsResponse struct"))
|
|
Expect(contentStr).To(ContainSubstring("type ComprehensiveMultipleReturnsResponse struct"))
|
|
|
|
// Should have wrapper functions
|
|
Expect(contentStr).To(ContainSubstring("func ComprehensiveSimpleParams("))
|
|
Expect(contentStr).To(ContainSubstring("func ComprehensiveNoParams()"))
|
|
Expect(contentStr).To(ContainSubstring("func ComprehensiveNoParamsNoReturns()"))
|
|
|
|
// Move generated file to clientDir root for compilation
|
|
Expect(os.Rename(filepath.Join(goDir, entries[0].Name()), filepath.Join(clientDir, "nd_host.go"))).To(Succeed())
|
|
|
|
// Create go.mod for client code
|
|
goMod := "module main\n\ngo 1.23\n\nrequire github.com/extism/go-pdk v1.1.1\n"
|
|
Expect(os.WriteFile(filepath.Join(clientDir, "go.mod"), []byte(goMod), 0600)).To(Succeed())
|
|
|
|
// Add a simple main function for the plugin
|
|
mainGo := `package main
|
|
|
|
func main() {}
|
|
`
|
|
Expect(os.WriteFile(filepath.Join(clientDir, "main.go"), []byte(mainGo), 0600)).To(Succeed())
|
|
|
|
// Add type definitions needed by the generated code
|
|
typesGo := `package main
|
|
|
|
type User2 struct {
|
|
ID string
|
|
Name string
|
|
}
|
|
|
|
type Filter2 struct {
|
|
Active bool
|
|
}
|
|
`
|
|
Expect(os.WriteFile(filepath.Join(clientDir, "types.go"), []byte(typesGo), 0600)).To(Succeed())
|
|
|
|
// Tidy dependencies
|
|
goTidyCmd := exec.Command("go", "mod", "tidy")
|
|
goTidyCmd.Dir = clientDir
|
|
goTidyOutput, err := goTidyCmd.CombinedOutput()
|
|
Expect(err).ToNot(HaveOccurred(), "go mod tidy failed: %s", goTidyOutput)
|
|
|
|
// Build as WASM plugin - this validates the client code compiles correctly
|
|
buildCmd := exec.Command("go", "build", "-buildmode=c-shared", "-o", "plugin.wasm", ".")
|
|
buildCmd.Dir = clientDir
|
|
buildCmd.Env = append(os.Environ(), "GOOS=wasip1", "GOARCH=wasm")
|
|
buildOutput, err := buildCmd.CombinedOutput()
|
|
Expect(err).ToNot(HaveOccurred(), "WASM build failed: %s", buildOutput)
|
|
|
|
// Verify .wasm file was created
|
|
Expect(filepath.Join(clientDir, "plugin.wasm")).To(BeAnExistingFile())
|
|
})
|
|
})
|
|
})
|
|
|
|
func mustGetWd(t FullGinkgoTInterface) string {
|
|
dir, err := os.Getwd()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for {
|
|
if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil {
|
|
return dir
|
|
}
|
|
parent := filepath.Dir(dir)
|
|
if parent == dir {
|
|
t.Fatal("could not find project root")
|
|
}
|
|
dir = parent
|
|
}
|
|
}
|