diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 93fa85b..110da52 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -86,11 +86,30 @@ When reporting issues, please include: If you're adding a new language plugin: 1. Create a new package under `plugins//` -2. Implement the `core.Plugin` interface +2. Implement the `core.Plugin` interface: + ```go + type Plugin interface { + // Name returns the plugin name (e.g., "cgo", "python") + Name() string + + // Generate produces binding code for the given parsed package + Generate(pkg *ParsedPackage) ([]byte, error) + + // Build generates code and compiles/packages it for distribution + Build(pkg *ParsedPackage, inputPath string, opts *BuildOptions) error + } + ``` 3. Register the plugin in `init()` using `factory.Register()` -4. Add comprehensive tests +4. Add comprehensive tests for both `Generate` and `Build` methods 5. Update documentation in `docs/` +### Plugin Implementation Tips + +- The `Generate` method should return the generated source code as bytes +- The `Build` method should handle the full build pipeline (generate code, compile, package) +- Use `core.BuildOptions` to access output directory, library name, build system, and verbose flag +- For languages that need a shared library (like Python), call the CGO plugin's `Build` method first + ## Questions? Feel free to open an issue for questions or discussions about contributing. diff --git a/README.md b/README.md index 738993a..1a8a8e9 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,10 @@ # GoAnywhere +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![CI](https://github.com/riceriley59/goanywhere/actions/workflows/ci.yaml/badge.svg)](https://github.com/riceriley59/goanywhere/actions/workflows/ci.yaml) +[![Go Report Card](https://goreportcard.com/badge/github.com/riceriley59/goanywhere)](https://goreportcard.com/report/github.com/riceriley59/goanywhere) +[![Go Version](https://img.shields.io/badge/go-1.23-blue.svg)](https://golang.org/) [![Coverage Status](https://coveralls.io/repos/github/riceriley59/goanywhere/badge.svg?branch=main)](https://coveralls.io/github/riceriley59/goanywhere?branch=main) -[![Go Version](https://img.shields.io/badge/go-1.25.6-blue.svg)](https://golang.org/) -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) Generate language bindings for your Go libraries to use them from Python, C, and other languages. @@ -40,8 +41,11 @@ goanywhere generate ./mypackage # Generate Python bindings goanywhere generate ./mypackage --plugin python -# Specify output file -goanywhere generate ./mypackage -o ./bindings/main.go +# Build CGO shared library directly +goanywhere build ./mypackage --plugin cgo + +# Build Python package with shared library +goanywhere build ./mypackage --plugin python ``` ## Documentation diff --git a/docs/usage.md b/docs/usage.md index 8b8b444..9bf8ff3 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -1,6 +1,13 @@ # Usage Guide -## Command Overview +## Commands + +GoAnywhere provides two main commands: + +- `generate` - Generate language binding source code +- `build` - Generate and compile bindings into distributable packages + +## Generate Command ```bash goanywhere generate [flags] @@ -15,6 +22,83 @@ goanywhere generate [flags] | `--plugin` | `-p` | Plugin type (`cgo`, `python`) | `cgo` | | `--verbose` | `-v` | Show parsed constructs and skipped items | `false` | +## Build Command + +The `build` command generates binding code and compiles it into ready-to-use packages. + +```bash +goanywhere build [flags] +``` + +### Flags + +| Flag | Short | Description | Default | +|------|-------|-------------|---------| +| `--output` | `-o` | Output directory for built artifacts | `/_build` | +| `--import-path` | `-i` | Import path for the target package | Auto-detected from go.mod | +| `--plugin` | `-p` | Plugin type (`cgo`, `python`) | `cgo` | +| `--build-system` | | Python build system (`setuptools`, `hatch`, `poetry`, `uv`) | `setuptools` | +| `--lib-name` | | Override the default library name | `lib` | +| `--verbose` | `-v` | Show build progress and details | `false` | + +### CGO Build + +Build a shared library from your Go package: + +```bash +goanywhere build ./mypackage --plugin cgo +``` + +This generates CGO bindings and compiles them into a shared library (`.so` on Linux, `.dylib` on macOS, `.dll` on Windows). + +### Python Build + +Build a complete Python package with shared library: + +```bash +goanywhere build ./mypackage --plugin python +``` + +This creates a distributable Python package structure: + +``` +mypackage/python_build/ +├── mypackage/ +│ ├── __init__.py +│ ├── bindings.py +│ └── lib/ +│ └── libmypackage.so +├── cgo_plugin/ +│ └── main.go +├── libmypackage.so +└── pyproject.toml +``` + +### Python Build Systems + +Choose your preferred Python build system: + +```bash +# setuptools (default) +goanywhere build ./mypackage --plugin python --build-system setuptools + +# hatch +goanywhere build ./mypackage --plugin python --build-system hatch + +# poetry +goanywhere build ./mypackage --plugin python --build-system poetry + +# uv +goanywhere build ./mypackage --plugin python --build-system uv +``` + +After building, install the package: + +```bash +cd mypackage/python_build +pip install -e . +``` + ## Examples ### Basic Usage @@ -61,6 +145,8 @@ goanywhere generate ./mypackage -v ## Building the Generated Code +> **Tip:** Use `goanywhere build` to automate these steps. See [Build Command](#build-command). + ### CGO Shared Library After generating CGO bindings, build a shared library: @@ -131,7 +217,16 @@ mypackage/ ## Workflow +### Using Generate (Manual Build) + 1. Write your Go library with exported functions and structs 2. Run `goanywhere generate ./mypackage` 3. Build the shared library with `go build -buildmode=c-shared` 4. Use the library from C, Python, or other languages + +### Using Build (Automated) + +1. Write your Go library with exported functions and structs +2. Run `goanywhere build ./mypackage --plugin python` +3. Install the package with `pip install -e ./mypackage/python_build` +4. Import and use in Python diff --git a/internal/cli/build.go b/internal/cli/build.go new file mode 100644 index 0000000..6df772b --- /dev/null +++ b/internal/cli/build.go @@ -0,0 +1,141 @@ +package cli + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/spf13/cobra" + + "github.com/riceriley59/goanywhere/internal/core" + "github.com/riceriley59/goanywhere/internal/core/factory" +) + +type buildOptions struct { + OutputDir string + ImportPath string + Plugin string + BuildSystem string + LibraryName string + Verbose bool +} + +// NewBuildCmd creates the build subcommand +func NewBuildCmd() *cobra.Command { + opts := &buildOptions{} + + cmd := &cobra.Command{ + Use: "build ", + Short: "Generate and build plugin code for a Go package", + Long: `Generate plugin code and build it as a shared library or package. + +For CGO plugin: + Generates the CGO wrapper code and compiles it to a shared library (.so/.dylib/.dll) + +For Python plugin: + Generates CGO shared library, Python bindings, and creates a Python package + with the specified build system configuration. + +Examples: + goanywhere build ./mypackage --plugin cgo + goanywhere build ./mypackage --plugin cgo -o ./dist + goanywhere build ./mypackage --plugin python --build-system setuptools`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return runBuild(args[0], opts) + }, + } + + cmd.Flags().StringVarP(&opts.OutputDir, "output", "o", "", + "Output directory (default: /_build)") + cmd.Flags().StringVarP(&opts.ImportPath, "import-path", "i", "", + "Import path for the target package (required for proper imports)") + cmd.Flags().StringVarP(&opts.Plugin, "plugin", "p", "cgo", + "Plugin type to build (cgo, python)") + cmd.Flags().StringVar(&opts.BuildSystem, "build-system", "setuptools", + "Python build system (setuptools, hatch, poetry, uv)") + cmd.Flags().StringVar(&opts.LibraryName, "lib-name", "", + "Override the shared library name (default: lib)") + cmd.Flags().BoolVarP(&opts.Verbose, "verbose", "v", false, + "Verbose output") + + return cmd +} + +func runBuild(inputDir string, opts *buildOptions) error { + // Resolve input path + inputPath, err := filepath.Abs(inputDir) + if err != nil { + return fmt.Errorf("invalid input path: %w", err) + } + + // Check if input directory exists + info, err := os.Stat(inputPath) + if err != nil { + return fmt.Errorf("cannot access input directory: %w", err) + } + if !info.IsDir() { + return fmt.Errorf("input path is not a directory: %s", inputPath) + } + + // Parse the Go package + parser := core.NewParser(opts.Verbose) + if opts.Verbose { + fmt.Printf("Parsing package at: %s\n", inputPath) + } + + pkg, err := parser.ParsePackage(inputPath) + if err != nil { + return fmt.Errorf("parse error: %w", err) + } + + // Set import path + if opts.ImportPath != "" { + pkg.ImportPath = opts.ImportPath + } else { + importPath, err := inferImportPath(inputPath) + if err == nil { + pkg.ImportPath = importPath + } else { + return fmt.Errorf("could not determine import path: use --import-path flag") + } + } + + if opts.Verbose { + fmt.Printf("Package: %s\n", pkg.Name) + fmt.Printf("Import path: %s\n", pkg.ImportPath) + fmt.Printf("Functions: %d\n", len(pkg.Functions)) + fmt.Printf("Structs: %d\n", len(pkg.Structs)) + } + + // Determine output directory + outputDir := opts.OutputDir + if outputDir == "" { + outputDir = filepath.Join(inputPath, opts.Plugin+"_build") + } + outputDir, err = filepath.Abs(outputDir) + if err != nil { + return fmt.Errorf("invalid output path: %w", err) + } + + // Create output directory + if err := os.MkdirAll(outputDir, 0755); err != nil { + return fmt.Errorf("cannot create output directory: %w", err) + } + + // Get the plugin + plugin, err := factory.Get(opts.Plugin, opts.Verbose) + if err != nil { + return fmt.Errorf("unsupported plugin for build: %s (supported: cgo, python)", opts.Plugin) + } + + // Build using the plugin + buildOpts := &core.BuildOptions{ + OutputDir: outputDir, + LibraryName: opts.LibraryName, + BuildSystem: opts.BuildSystem, + Verbose: opts.Verbose, + } + + return plugin.Build(pkg, inputPath, buildOpts) +} diff --git a/internal/cli/cli.go b/internal/cli/cli.go index 5d09b6b..d6762c8 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -32,6 +32,7 @@ func NewGoAnywhereCmd() *cobra.Command { // Add subcommands goAnywhereCmd.AddCommand(NewGenerateCmd()) + goAnywhereCmd.AddCommand(NewBuildCmd()) return goAnywhereCmd } diff --git a/internal/cli/cli_test.go b/internal/cli/cli_test.go new file mode 100644 index 0000000..a63ff2a --- /dev/null +++ b/internal/cli/cli_test.go @@ -0,0 +1,344 @@ +package cli + +import ( + "os" + "path/filepath" + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestCli(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "CLI Suite") +} + +var _ = Describe("CLI", func() { + Describe("ExitCode", func() { + It("converts to int correctly", func() { + Expect(ExitCodeSuccess.ToInt()).To(Equal(0)) + Expect(ExitCodeError.ToInt()).To(Equal(1)) + }) + }) + + Describe("NewGoAnywhereCmd", func() { + It("creates root command", func() { + cmd := NewGoAnywhereCmd() + Expect(cmd).NotTo(BeNil()) + Expect(cmd.Use).To(Equal("goanywhere")) + }) + + It("has generate subcommand", func() { + cmd := NewGoAnywhereCmd() + generateCmd, _, err := cmd.Find([]string{"generate"}) + Expect(err).NotTo(HaveOccurred()) + Expect(generateCmd.Use).To(ContainSubstring("generate")) + }) + + It("has build subcommand", func() { + cmd := NewGoAnywhereCmd() + buildCmd, _, err := cmd.Find([]string{"build"}) + Expect(err).NotTo(HaveOccurred()) + Expect(buildCmd.Use).To(ContainSubstring("build")) + }) + + It("has version flag", func() { + cmd := NewGoAnywhereCmd() + Expect(cmd.Version).NotTo(BeEmpty()) + }) + }) + + Describe("NewGenerateCmd", func() { + It("creates generate command with flags", func() { + cmd := NewGenerateCmd() + Expect(cmd).NotTo(BeNil()) + Expect(cmd.Use).To(ContainSubstring("generate")) + + // Check flags exist + Expect(cmd.Flags().Lookup("output")).NotTo(BeNil()) + Expect(cmd.Flags().Lookup("import-path")).NotTo(BeNil()) + Expect(cmd.Flags().Lookup("plugin")).NotTo(BeNil()) + Expect(cmd.Flags().Lookup("verbose")).NotTo(BeNil()) + }) + + It("has correct default plugin value", func() { + cmd := NewGenerateCmd() + pluginFlag := cmd.Flags().Lookup("plugin") + Expect(pluginFlag.DefValue).To(Equal("cgo")) + }) + }) + + Describe("NewBuildCmd", func() { + It("creates build command with flags", func() { + cmd := NewBuildCmd() + Expect(cmd).NotTo(BeNil()) + Expect(cmd.Use).To(ContainSubstring("build")) + + // Check flags exist + Expect(cmd.Flags().Lookup("output")).NotTo(BeNil()) + Expect(cmd.Flags().Lookup("import-path")).NotTo(BeNil()) + Expect(cmd.Flags().Lookup("plugin")).NotTo(BeNil()) + Expect(cmd.Flags().Lookup("verbose")).NotTo(BeNil()) + Expect(cmd.Flags().Lookup("build-system")).NotTo(BeNil()) + Expect(cmd.Flags().Lookup("lib-name")).NotTo(BeNil()) + }) + + It("has correct default build-system value", func() { + cmd := NewBuildCmd() + buildSystemFlag := cmd.Flags().Lookup("build-system") + Expect(buildSystemFlag.DefValue).To(Equal("setuptools")) + }) + }) + + Describe("inferImportPath", func() { + It("returns error for directory without go.mod", func() { + tmpDir, err := os.MkdirTemp("", "no-gomod") + Expect(err).NotTo(HaveOccurred()) + defer func() { _ = os.RemoveAll(tmpDir) }() + + _, err = inferImportPath(tmpDir) + Expect(err).To(HaveOccurred()) + }) + + It("infers import path from go.mod", func() { + tmpDir, err := os.MkdirTemp("", "with-gomod") + Expect(err).NotTo(HaveOccurred()) + defer func() { _ = os.RemoveAll(tmpDir) }() + + goModContent := "module github.com/example/mymodule\n\ngo 1.21\n" + err = os.WriteFile(filepath.Join(tmpDir, "go.mod"), []byte(goModContent), 0644) + Expect(err).NotTo(HaveOccurred()) + + importPath, err := inferImportPath(tmpDir) + Expect(err).NotTo(HaveOccurred()) + Expect(importPath).To(Equal("github.com/example/mymodule")) + }) + + It("infers import path for subdirectory", func() { + tmpDir, err := os.MkdirTemp("", "with-gomod") + Expect(err).NotTo(HaveOccurred()) + defer func() { _ = os.RemoveAll(tmpDir) }() + + goModContent := "module github.com/example/mymodule\n\ngo 1.21\n" + err = os.WriteFile(filepath.Join(tmpDir, "go.mod"), []byte(goModContent), 0644) + Expect(err).NotTo(HaveOccurred()) + + subDir := filepath.Join(tmpDir, "pkg", "mypackage") + err = os.MkdirAll(subDir, 0755) + Expect(err).NotTo(HaveOccurred()) + + importPath, err := inferImportPath(subDir) + Expect(err).NotTo(HaveOccurred()) + Expect(importPath).To(Equal("github.com/example/mymodule/pkg/mypackage")) + }) + }) + + Describe("splitLines", func() { + It("splits string into lines", func() { + lines := splitLines("line1\nline2\nline3") + Expect(lines).To(HaveLen(3)) + Expect(lines[0]).To(Equal("line1")) + Expect(lines[1]).To(Equal("line2")) + Expect(lines[2]).To(Equal("line3")) + }) + + It("handles string without newlines", func() { + lines := splitLines("single line") + Expect(lines).To(HaveLen(1)) + Expect(lines[0]).To(Equal("single line")) + }) + + It("handles empty string", func() { + lines := splitLines("") + Expect(lines).To(HaveLen(0)) + }) + + It("handles trailing newline", func() { + lines := splitLines("line1\nline2\n") + Expect(lines).To(HaveLen(2)) + }) + }) + + Describe("runGenerate", func() { + var fixtureDir string + + BeforeEach(func() { + wd, _ := os.Getwd() + fixtureDir = filepath.Join(wd, "..", "..", "tests", "fixtures", "simple") + }) + + It("returns error for non-existent directory", func() { + opts := &generateOptions{Plugin: "cgo"} + err := runGenerate("/nonexistent/path", opts) + Expect(err).To(HaveOccurred()) + }) + + It("returns error when path is a file", func() { + tmpFile, err := os.CreateTemp("", "testfile") + Expect(err).NotTo(HaveOccurred()) + defer func() { _ = os.Remove(tmpFile.Name()) }() + _ = tmpFile.Close() + + opts := &generateOptions{Plugin: "cgo"} + err = runGenerate(tmpFile.Name(), opts) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("not a directory")) + }) + + It("returns error for invalid plugin", func() { + opts := &generateOptions{Plugin: "invalid"} + err := runGenerate(fixtureDir, opts) + Expect(err).To(HaveOccurred()) + }) + + It("generates CGO code successfully", func() { + tmpDir, err := os.MkdirTemp("", "output") + Expect(err).NotTo(HaveOccurred()) + defer func() { _ = os.RemoveAll(tmpDir) }() + + opts := &generateOptions{ + Plugin: "cgo", + OutputFile: filepath.Join(tmpDir, "main.go"), + ImportPath: "github.com/test/simple", + } + err = runGenerate(fixtureDir, opts) + Expect(err).NotTo(HaveOccurred()) + + // Check output file exists + _, err = os.Stat(filepath.Join(tmpDir, "main.go")) + Expect(err).NotTo(HaveOccurred()) + }) + + It("generates Python code successfully", func() { + tmpDir, err := os.MkdirTemp("", "output") + Expect(err).NotTo(HaveOccurred()) + defer func() { _ = os.RemoveAll(tmpDir) }() + + opts := &generateOptions{ + Plugin: "python", + OutputFile: filepath.Join(tmpDir, "simple.py"), + ImportPath: "github.com/test/simple", + } + err = runGenerate(fixtureDir, opts) + Expect(err).NotTo(HaveOccurred()) + + // Check output file exists + _, err = os.Stat(filepath.Join(tmpDir, "simple.py")) + Expect(err).NotTo(HaveOccurred()) + }) + }) + + Describe("runBuild", func() { + var fixtureDir string + + BeforeEach(func() { + wd, _ := os.Getwd() + fixtureDir = filepath.Join(wd, "..", "..", "tests", "fixtures", "simple") + }) + + It("returns error for non-existent directory", func() { + opts := &buildOptions{Plugin: "cgo"} + err := runBuild("/nonexistent/path", opts) + Expect(err).To(HaveOccurred()) + }) + + It("returns error when path is a file", func() { + tmpFile, err := os.CreateTemp("", "testfile") + Expect(err).NotTo(HaveOccurred()) + defer func() { _ = os.Remove(tmpFile.Name()) }() + _ = tmpFile.Close() + + opts := &buildOptions{Plugin: "cgo"} + err = runBuild(tmpFile.Name(), opts) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("not a directory")) + }) + + It("returns error for unsupported plugin", func() { + opts := &buildOptions{Plugin: "rust"} + err := runBuild(fixtureDir, opts) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("unsupported plugin")) + }) + }) + + Describe("Execute", func() { + It("returns success for help command", func() { + // Save original args + oldArgs := os.Args + defer func() { os.Args = oldArgs }() + + os.Args = []string{"goanywhere", "--help"} + exitCode := Execute() + Expect(exitCode).To(Equal(ExitCodeSuccess)) + }) + + It("returns success for version command", func() { + oldArgs := os.Args + defer func() { os.Args = oldArgs }() + + os.Args = []string{"goanywhere", "--version"} + exitCode := Execute() + Expect(exitCode).To(Equal(ExitCodeSuccess)) + }) + + It("returns error for invalid command", func() { + oldArgs := os.Args + defer func() { os.Args = oldArgs }() + + os.Args = []string{"goanywhere", "invalid-command"} + exitCode := Execute() + Expect(exitCode).To(Equal(ExitCodeError)) + }) + }) + + Describe("runGenerate verbose mode", func() { + var fixtureDir string + + BeforeEach(func() { + wd, _ := os.Getwd() + fixtureDir = filepath.Join(wd, "..", "..", "tests", "fixtures", "simple") + }) + + It("generates with verbose output", func() { + tmpDir, err := os.MkdirTemp("", "output") + Expect(err).NotTo(HaveOccurred()) + defer func() { _ = os.RemoveAll(tmpDir) }() + + opts := &generateOptions{ + Plugin: "cgo", + OutputFile: filepath.Join(tmpDir, "main.go"), + ImportPath: "github.com/test/simple", + Verbose: true, + } + err = runGenerate(fixtureDir, opts) + Expect(err).NotTo(HaveOccurred()) + }) + }) + + Describe("runBuild verbose mode", func() { + var fixtureDir string + + BeforeEach(func() { + wd, _ := os.Getwd() + fixtureDir = filepath.Join(wd, "..", "..", "tests", "fixtures", "simple") + }) + + It("runs with verbose option", func() { + tmpDir, err := os.MkdirTemp("", "build-output") + Expect(err).NotTo(HaveOccurred()) + defer func() { _ = os.RemoveAll(tmpDir) }() + + opts := &buildOptions{ + Plugin: "cgo", + OutputDir: tmpDir, + ImportPath: "github.com/test/simple", + Verbose: true, + } + // This will fail at the CGO compilation step (no C compiler), + // but it will exercise the code path up to that point + _ = runBuild(fixtureDir, opts) + }) + }) +}) diff --git a/internal/core/factory/factory_test.go b/internal/core/factory/factory_test.go index aeb36a1..82f69f8 100644 --- a/internal/core/factory/factory_test.go +++ b/internal/core/factory/factory_test.go @@ -20,6 +20,9 @@ type mockPlugin struct { func (m *mockPlugin) Name() string { return m.name } func (m *mockPlugin) Generate(pkg *core.ParsedPackage) ([]byte, error) { return nil, nil } +func (m *mockPlugin) Build(pkg *core.ParsedPackage, inputPath string, opts *core.BuildOptions) error { + return nil +} var _ = Describe("Plugin factory", func() { Describe("Register and Get", func() { diff --git a/internal/core/parser_test.go b/internal/core/parser_test.go index 1598512..c1958c2 100644 --- a/internal/core/parser_test.go +++ b/internal/core/parser_test.go @@ -183,4 +183,131 @@ var _ = Describe("Parser", func() { Expect(isExported("")).To(BeFalse()) }) }) + + Describe("ParsePackage with complex types", func() { + var complexDir string + + BeforeEach(func() { + wd, _ := os.Getwd() + complexDir = filepath.Join(wd, "..", "..", "tests", "fixtures", "complex") + }) + + It("parses array types", func() { + pkg, err := parser.ParsePackage(complexDir) + Expect(err).NotTo(HaveOccurred()) + + var arrayFn *ParsedFunc + for i := range pkg.Functions { + if pkg.Functions[i].Name == "ProcessArray" { + arrayFn = &pkg.Functions[i] + break + } + } + Expect(arrayFn).NotTo(BeNil()) + Expect(arrayFn.Params).To(HaveLen(1)) + Expect(arrayFn.Params[0].Type.Kind).To(Equal(KindArray)) + Expect(arrayFn.Params[0].Type.Size).To(Equal(10)) + }) + + It("parses slice types", func() { + pkg, err := parser.ParsePackage(complexDir) + Expect(err).NotTo(HaveOccurred()) + + var sliceFn *ParsedFunc + for i := range pkg.Functions { + if pkg.Functions[i].Name == "ProcessSlice" { + sliceFn = &pkg.Functions[i] + break + } + } + Expect(sliceFn).NotTo(BeNil()) + Expect(sliceFn.Params).To(HaveLen(1)) + Expect(sliceFn.Params[0].Type.Kind).To(Equal(KindSlice)) + }) + + It("parses map types", func() { + pkg, err := parser.ParsePackage(complexDir) + Expect(err).NotTo(HaveOccurred()) + + var mapFn *ParsedFunc + for i := range pkg.Functions { + if pkg.Functions[i].Name == "ProcessMap" { + mapFn = &pkg.Functions[i] + break + } + } + Expect(mapFn).NotTo(BeNil()) + Expect(mapFn.Params).To(HaveLen(1)) + Expect(mapFn.Params[0].Type.Kind).To(Equal(KindMap)) + }) + + It("parses pointer types", func() { + pkg, err := parser.ParsePackage(complexDir) + Expect(err).NotTo(HaveOccurred()) + + var ptrFn *ParsedFunc + for i := range pkg.Functions { + if pkg.Functions[i].Name == "ProcessPointer" { + ptrFn = &pkg.Functions[i] + break + } + } + Expect(ptrFn).NotTo(BeNil()) + Expect(ptrFn.Params).To(HaveLen(1)) + Expect(ptrFn.Params[0].Type.Kind).To(Equal(KindPointer)) + }) + + It("parses interface types", func() { + pkg, err := parser.ParsePackage(complexDir) + Expect(err).NotTo(HaveOccurred()) + + var ifaceFn *ParsedFunc + for i := range pkg.Functions { + if pkg.Functions[i].Name == "ProcessInterface" { + ifaceFn = &pkg.Functions[i] + break + } + } + Expect(ifaceFn).NotTo(BeNil()) + Expect(ifaceFn.Params).To(HaveLen(1)) + Expect(ifaceFn.Params[0].Type.Kind).To(Equal(KindInterface)) + }) + + It("parses struct with various field types", func() { + pkg, err := parser.ParsePackage(complexDir) + Expect(err).NotTo(HaveOccurred()) + + var config *ParsedStruct + for i := range pkg.Structs { + if pkg.Structs[i].Name == "Config" { + config = &pkg.Structs[i] + break + } + } + Expect(config).NotTo(BeNil()) + Expect(config.Fields).To(HaveLen(4)) + + // Check field types + fieldTypes := make(map[string]TypeKind) + for _, f := range config.Fields { + fieldTypes[f.Name] = f.Type.Kind + } + Expect(fieldTypes["Name"]).To(Equal(KindString)) + Expect(fieldTypes["Values"]).To(Equal(KindSlice)) + Expect(fieldTypes["Data"]).To(Equal(KindArray)) + Expect(fieldTypes["Options"]).To(Equal(KindMap)) + }) + }) + + Describe("Verbose parser", func() { + It("runs without errors in verbose mode", func() { + wd, _ := os.Getwd() + fixtureDir := filepath.Join(wd, "..", "..", "tests", "fixtures", "simple") + + verboseParser := NewParser(true) + pkg, err := verboseParser.ParsePackage(fixtureDir) + Expect(err).NotTo(HaveOccurred()) + Expect(pkg).NotTo(BeNil()) + }) + }) }) diff --git a/internal/core/plugin.go b/internal/core/plugin.go index 2d6ad9e..799b196 100644 --- a/internal/core/plugin.go +++ b/internal/core/plugin.go @@ -1,5 +1,17 @@ package core +// BuildOptions contains configuration for the Build method +type BuildOptions struct { + // OutputDir is the directory where build artifacts should be placed + OutputDir string + // LibraryName overrides the default library name (default: lib) + LibraryName string + // BuildSystem specifies the build system for language-specific packaging (e.g., setuptools, hatch) + BuildSystem string + // Verbose enables verbose output during build + Verbose bool +} + // Plugin is the interface that all language plugins must implement type Plugin interface { // Name returns the plugin name (e.g., "cgo", "python", "rust") @@ -7,4 +19,8 @@ type Plugin interface { // Generate produces plugin code for the given parsed package Generate(pkg *ParsedPackage) ([]byte, error) + + // Build generates code and compiles/packages it for distribution + // The inputPath is the path to the original Go package source + Build(pkg *ParsedPackage, inputPath string, opts *BuildOptions) error } diff --git a/plugins/cgo/plugin.go b/plugins/cgo/plugin.go index 206da77..2f2f98b 100644 --- a/plugins/cgo/plugin.go +++ b/plugins/cgo/plugin.go @@ -3,6 +3,10 @@ package cgo import ( "bytes" "fmt" + "os" + "os/exec" + "path/filepath" + "runtime" "strings" "text/template" @@ -615,3 +619,71 @@ func capitalize(s string) string { } return strings.ToUpper(s[:1]) + s[1:] } + +// Build generates CGO code and compiles it to a shared library +func (a *Plugin) Build(pkg *core.ParsedPackage, inputPath string, opts *core.BuildOptions) error { + // Generate CGO code + if opts.Verbose { + fmt.Println("Generating CGO wrapper code...") + } + code, err := a.Generate(pkg) + if err != nil { + return fmt.Errorf("generation error: %w", err) + } + + // Write generated code + cgoDir := filepath.Join(opts.OutputDir, "cgo_plugin") + if err := os.MkdirAll(cgoDir, 0755); err != nil { + return fmt.Errorf("cannot create cgo directory: %w", err) + } + + cgoFile := filepath.Join(cgoDir, "main.go") + if err := os.WriteFile(cgoFile, code, 0644); err != nil { + return fmt.Errorf("write error: %w", err) + } + fmt.Printf("Generated CGO wrapper: %s\n", cgoFile) + + // Determine library name and extension + libName := opts.LibraryName + if libName == "" { + libName = "lib" + pkg.Name + } + libExt := getSharedLibExtension() + libFile := filepath.Join(opts.OutputDir, libName+libExt) + + // Build shared library + if opts.Verbose { + fmt.Printf("Building shared library: %s\n", libFile) + } + + cmd := exec.Command("go", "build", "-buildmode=c-shared", "-o", libFile, cgoFile) + cmd.Env = append(os.Environ(), "CGO_ENABLED=1") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + if err := cmd.Run(); err != nil { + return fmt.Errorf("build failed: %w", err) + } + + fmt.Printf("Built shared library: %s\n", libFile) + + // Note about header file + headerFile := filepath.Join(opts.OutputDir, libName+".h") + if _, err := os.Stat(headerFile); err == nil { + fmt.Printf("Generated header file: %s\n", headerFile) + } + + return nil +} + +// getSharedLibExtension returns the platform-specific shared library extension +func getSharedLibExtension() string { + switch runtime.GOOS { + case "darwin": + return ".dylib" + case "windows": + return ".dll" + default: + return ".so" + } +} diff --git a/plugins/cgo/plugin_test.go b/plugins/cgo/plugin_test.go new file mode 100644 index 0000000..a7c5d41 --- /dev/null +++ b/plugins/cgo/plugin_test.go @@ -0,0 +1,543 @@ +package cgo + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/riceriley59/goanywhere/internal/core" +) + +var _ = Describe("Plugin", func() { + var plugin *Plugin + + BeforeEach(func() { + plugin = NewPlugin(false) + }) + + Describe("NewPlugin", func() { + It("creates plugin with verbose off", func() { + p := NewPlugin(false) + Expect(p).NotTo(BeNil()) + Expect(p.verbose).To(BeFalse()) + }) + + It("creates plugin with verbose on", func() { + p := NewPlugin(true) + Expect(p.verbose).To(BeTrue()) + }) + }) + + Describe("Name", func() { + It("returns cgo", func() { + Expect(plugin.Name()).To(Equal("cgo")) + }) + }) + + Describe("Generate", func() { + It("generates valid CGO code for simple package", func() { + pkg := &core.ParsedPackage{ + Name: "test", + ImportPath: "github.com/test/test", + Functions: []core.ParsedFunc{ + { + Name: "Add", + Params: []core.ParsedParam{ + {Name: "a", Type: core.ParsedType{Kind: core.KindPrimitive, Name: "int"}}, + {Name: "b", Type: core.ParsedType{Kind: core.KindPrimitive, Name: "int"}}, + }, + Results: []core.ParsedResult{ + {Type: core.ParsedType{Kind: core.KindPrimitive, Name: "int"}}, + }, + }, + }, + } + + code, err := plugin.Generate(pkg) + Expect(err).NotTo(HaveOccurred()) + Expect(code).NotTo(BeEmpty()) + + codeStr := string(code) + Expect(codeStr).To(ContainSubstring("package main")) + Expect(codeStr).To(ContainSubstring("import \"C\"")) + Expect(codeStr).To(ContainSubstring("//export test_Add")) + Expect(codeStr).To(ContainSubstring("func main()")) + }) + + It("generates struct wrappers", func() { + pkg := &core.ParsedPackage{ + Name: "test", + ImportPath: "github.com/test/test", + Structs: []core.ParsedStruct{ + { + Name: "Point", + Fields: []core.ParsedField{ + {Name: "X", Type: core.ParsedType{Kind: core.KindPrimitive, Name: "int"}, Exported: true}, + {Name: "Y", Type: core.ParsedType{Kind: core.KindPrimitive, Name: "int"}, Exported: true}, + }, + }, + }, + } + + code, err := plugin.Generate(pkg) + Expect(err).NotTo(HaveOccurred()) + + codeStr := string(code) + Expect(codeStr).To(ContainSubstring("Point_New")) + Expect(codeStr).To(ContainSubstring("Point_Free")) + Expect(codeStr).To(ContainSubstring("Point_GetX")) + Expect(codeStr).To(ContainSubstring("Point_SetX")) + }) + + It("generates method wrappers", func() { + pkg := &core.ParsedPackage{ + Name: "test", + ImportPath: "github.com/test/test", + Structs: []core.ParsedStruct{ + { + Name: "Point", + Methods: []core.ParsedMethod{ + { + Name: "Distance", + ReceiverName: "p", + ReceiverType: "Point", + ReceiverIsPtr: true, + Results: []core.ParsedResult{ + {Type: core.ParsedType{Kind: core.KindPrimitive, Name: "float64"}}, + }, + }, + }, + }, + }, + } + + code, err := plugin.Generate(pkg) + Expect(err).NotTo(HaveOccurred()) + + codeStr := string(code) + Expect(codeStr).To(ContainSubstring("Point_Distance")) + }) + + It("handles string parameters and returns", func() { + pkg := &core.ParsedPackage{ + Name: "test", + ImportPath: "github.com/test/test", + Functions: []core.ParsedFunc{ + { + Name: "Greet", + Params: []core.ParsedParam{ + {Name: "name", Type: core.ParsedType{Kind: core.KindString, Name: "string"}}, + }, + Results: []core.ParsedResult{ + {Type: core.ParsedType{Kind: core.KindString, Name: "string"}}, + }, + }, + }, + } + + code, err := plugin.Generate(pkg) + Expect(err).NotTo(HaveOccurred()) + + codeStr := string(code) + Expect(codeStr).To(ContainSubstring("test_Greet")) + Expect(codeStr).To(ContainSubstring("C.GoString")) + Expect(codeStr).To(ContainSubstring("C.CString")) + }) + + It("handles error returns", func() { + pkg := &core.ParsedPackage{ + Name: "test", + ImportPath: "github.com/test/test", + Functions: []core.ParsedFunc{ + { + Name: "Divide", + Params: []core.ParsedParam{ + {Name: "a", Type: core.ParsedType{Kind: core.KindPrimitive, Name: "int"}}, + {Name: "b", Type: core.ParsedType{Kind: core.KindPrimitive, Name: "int"}}, + }, + Results: []core.ParsedResult{ + {Type: core.ParsedType{Kind: core.KindPrimitive, Name: "int"}}, + {Type: core.ParsedType{Kind: core.KindError, Name: "error"}}, + }, + }, + }, + } + + code, err := plugin.Generate(pkg) + Expect(err).NotTo(HaveOccurred()) + + codeStr := string(code) + Expect(codeStr).To(ContainSubstring("test_Divide")) + Expect(codeStr).To(ContainSubstring("outError")) + }) + + It("skips variadic functions", func() { + verbosePlugin := NewPlugin(true) + pkg := &core.ParsedPackage{ + Name: "test", + ImportPath: "github.com/test/test", + Functions: []core.ParsedFunc{ + { + Name: "Sum", + IsVariadic: true, + Params: []core.ParsedParam{ + {Name: "nums", Type: core.ParsedType{Kind: core.KindSlice, Name: "[]int"}}, + }, + Results: []core.ParsedResult{ + {Type: core.ParsedType{Kind: core.KindPrimitive, Name: "int"}}, + }, + }, + }, + } + + code, err := verbosePlugin.Generate(pkg) + Expect(err).NotTo(HaveOccurred()) + + codeStr := string(code) + Expect(codeStr).NotTo(ContainSubstring("test_Sum")) + }) + + It("handles bool parameters", func() { + pkg := &core.ParsedPackage{ + Name: "test", + ImportPath: "github.com/test/test", + Functions: []core.ParsedFunc{ + { + Name: "Toggle", + Params: []core.ParsedParam{ + {Name: "flag", Type: core.ParsedType{Kind: core.KindPrimitive, Name: "bool"}}, + }, + Results: []core.ParsedResult{ + {Type: core.ParsedType{Kind: core.KindPrimitive, Name: "bool"}}, + }, + }, + }, + } + + code, err := plugin.Generate(pkg) + Expect(err).NotTo(HaveOccurred()) + + codeStr := string(code) + Expect(codeStr).To(ContainSubstring("test_Toggle")) + }) + + It("handles float parameters", func() { + pkg := &core.ParsedPackage{ + Name: "test", + ImportPath: "github.com/test/test", + Functions: []core.ParsedFunc{ + { + Name: "Multiply", + Params: []core.ParsedParam{ + {Name: "a", Type: core.ParsedType{Kind: core.KindPrimitive, Name: "float64"}}, + {Name: "b", Type: core.ParsedType{Kind: core.KindPrimitive, Name: "float64"}}, + }, + Results: []core.ParsedResult{ + {Type: core.ParsedType{Kind: core.KindPrimitive, Name: "float64"}}, + }, + }, + }, + } + + code, err := plugin.Generate(pkg) + Expect(err).NotTo(HaveOccurred()) + + codeStr := string(code) + Expect(codeStr).To(ContainSubstring("test_Multiply")) + }) + + It("handles multiple return values", func() { + pkg := &core.ParsedPackage{ + Name: "test", + ImportPath: "github.com/test/test", + Functions: []core.ParsedFunc{ + { + Name: "DivMod", + Params: []core.ParsedParam{ + {Name: "a", Type: core.ParsedType{Kind: core.KindPrimitive, Name: "int"}}, + {Name: "b", Type: core.ParsedType{Kind: core.KindPrimitive, Name: "int"}}, + }, + Results: []core.ParsedResult{ + {Type: core.ParsedType{Kind: core.KindPrimitive, Name: "int"}}, + {Type: core.ParsedType{Kind: core.KindPrimitive, Name: "int"}}, + }, + }, + }, + } + + code, err := plugin.Generate(pkg) + Expect(err).NotTo(HaveOccurred()) + + codeStr := string(code) + Expect(codeStr).To(ContainSubstring("test_DivMod")) + }) + + It("generates handle registry code", func() { + pkg := &core.ParsedPackage{ + Name: "test", + ImportPath: "github.com/test/test", + Structs: []core.ParsedStruct{ + {Name: "Point"}, + }, + } + + code, err := plugin.Generate(pkg) + Expect(err).NotTo(HaveOccurred()) + + codeStr := string(code) + Expect(codeStr).To(ContainSubstring("handleMap")) + Expect(codeStr).To(ContainSubstring("registerHandle")) + Expect(codeStr).To(ContainSubstring("getHandle")) + Expect(codeStr).To(ContainSubstring("freeHandle")) + }) + + It("generates free functions", func() { + pkg := &core.ParsedPackage{ + Name: "test", + ImportPath: "github.com/test/test", + Functions: []core.ParsedFunc{ + { + Name: "GetName", + Results: []core.ParsedResult{ + {Type: core.ParsedType{Kind: core.KindString, Name: "string"}}, + }, + }, + }, + } + + code, err := plugin.Generate(pkg) + Expect(err).NotTo(HaveOccurred()) + + codeStr := string(code) + Expect(codeStr).To(ContainSubstring("Free_String")) + Expect(codeStr).To(ContainSubstring("Free_Bytes")) + }) + + It("handles byte slice parameters", func() { + elemType := core.ParsedType{Kind: core.KindPrimitive, Name: "byte"} + pkg := &core.ParsedPackage{ + Name: "test", + ImportPath: "github.com/test/test", + Functions: []core.ParsedFunc{ + { + Name: "Process", + Params: []core.ParsedParam{ + {Name: "data", Type: core.ParsedType{Kind: core.KindSlice, Name: "[]byte", ElemType: &elemType}}, + }, + Results: []core.ParsedResult{ + {Type: core.ParsedType{Kind: core.KindSlice, Name: "[]byte", ElemType: &elemType}}, + }, + }, + }, + } + + code, err := plugin.Generate(pkg) + Expect(err).NotTo(HaveOccurred()) + + codeStr := string(code) + Expect(codeStr).To(ContainSubstring("test_Process")) + }) + + It("handles methods with parameters", func() { + pkg := &core.ParsedPackage{ + Name: "test", + ImportPath: "github.com/test/test", + Structs: []core.ParsedStruct{ + { + Name: "Calculator", + Methods: []core.ParsedMethod{ + { + Name: "Add", + ReceiverName: "c", + ReceiverType: "Calculator", + ReceiverIsPtr: true, + Params: []core.ParsedParam{ + {Name: "a", Type: core.ParsedType{Kind: core.KindPrimitive, Name: "int"}}, + {Name: "b", Type: core.ParsedType{Kind: core.KindPrimitive, Name: "int"}}, + }, + Results: []core.ParsedResult{ + {Type: core.ParsedType{Kind: core.KindPrimitive, Name: "int"}}, + }, + }, + }, + }, + }, + } + + code, err := plugin.Generate(pkg) + Expect(err).NotTo(HaveOccurred()) + + codeStr := string(code) + Expect(codeStr).To(ContainSubstring("Calculator_Add")) + }) + }) + + Describe("capitalize", func() { + It("capitalizes first letter", func() { + Expect(capitalize("hello")).To(Equal("Hello")) + Expect(capitalize("world")).To(Equal("World")) + }) + + It("handles empty string", func() { + Expect(capitalize("")).To(Equal("")) + }) + + It("handles already capitalized", func() { + Expect(capitalize("Hello")).To(Equal("Hello")) + }) + }) + + Describe("Generate comprehensive", func() { + It("handles empty package", func() { + pkg := &core.ParsedPackage{ + Name: "empty", + ImportPath: "github.com/test/empty", + } + + code, err := plugin.Generate(pkg) + Expect(err).NotTo(HaveOccurred()) + Expect(string(code)).To(ContainSubstring("package main")) + }) + + It("handles function with no params", func() { + pkg := &core.ParsedPackage{ + Name: "test", + ImportPath: "github.com/test/test", + Functions: []core.ParsedFunc{ + { + Name: "GetValue", + Results: []core.ParsedResult{{Type: core.ParsedType{Kind: core.KindPrimitive, Name: "int"}}}, + }, + }, + } + + code, err := plugin.Generate(pkg) + Expect(err).NotTo(HaveOccurred()) + Expect(string(code)).To(ContainSubstring("test_GetValue")) + }) + + It("handles function with no return value", func() { + pkg := &core.ParsedPackage{ + Name: "test", + ImportPath: "github.com/test/test", + Functions: []core.ParsedFunc{ + { + Name: "DoNothing", + }, + }, + } + + code, err := plugin.Generate(pkg) + Expect(err).NotTo(HaveOccurred()) + Expect(string(code)).To(ContainSubstring("test_DoNothing")) + }) + + It("handles struct with unexported fields", func() { + pkg := &core.ParsedPackage{ + Name: "test", + ImportPath: "github.com/test/test", + Structs: []core.ParsedStruct{ + { + Name: "Mixed", + Fields: []core.ParsedField{ + {Name: "Public", Type: core.ParsedType{Kind: core.KindPrimitive, Name: "int"}, Exported: true}, + {Name: "private", Type: core.ParsedType{Kind: core.KindPrimitive, Name: "int"}, Exported: false}, + }, + }, + }, + } + + code, err := plugin.Generate(pkg) + Expect(err).NotTo(HaveOccurred()) + codeStr := string(code) + Expect(codeStr).To(ContainSubstring("Mixed_GetPublic")) + Expect(codeStr).NotTo(ContainSubstring("Mixed_Getprivate")) + }) + + It("handles method with error return", func() { + pkg := &core.ParsedPackage{ + Name: "test", + ImportPath: "github.com/test/test", + Structs: []core.ParsedStruct{ + { + Name: "Service", + Methods: []core.ParsedMethod{ + { + Name: "Call", + ReceiverName: "s", + ReceiverType: "Service", + ReceiverIsPtr: true, + Results: []core.ParsedResult{ + {Type: core.ParsedType{Kind: core.KindString, Name: "string"}}, + {Type: core.ParsedType{Kind: core.KindError, Name: "error"}}, + }, + }, + }, + }, + }, + } + + code, err := plugin.Generate(pkg) + Expect(err).NotTo(HaveOccurred()) + codeStr := string(code) + Expect(codeStr).To(ContainSubstring("Service_Call")) + Expect(codeStr).To(ContainSubstring("outError")) + }) + + It("handles uint types", func() { + pkg := &core.ParsedPackage{ + Name: "test", + ImportPath: "github.com/test/test", + Functions: []core.ParsedFunc{ + { + Name: "ProcessUint", + Params: []core.ParsedParam{ + {Name: "a", Type: core.ParsedType{Kind: core.KindPrimitive, Name: "uint"}}, + {Name: "b", Type: core.ParsedType{Kind: core.KindPrimitive, Name: "uint64"}}, + }, + Results: []core.ParsedResult{ + {Type: core.ParsedType{Kind: core.KindPrimitive, Name: "uint32"}}, + }, + }, + }, + } + + code, err := plugin.Generate(pkg) + Expect(err).NotTo(HaveOccurred()) + Expect(string(code)).To(ContainSubstring("test_ProcessUint")) + }) + + It("handles value receiver method", func() { + pkg := &core.ParsedPackage{ + Name: "test", + ImportPath: "github.com/test/test", + Structs: []core.ParsedStruct{ + { + Name: "Value", + Methods: []core.ParsedMethod{ + { + Name: "Get", + ReceiverName: "v", + ReceiverType: "Value", + ReceiverIsPtr: false, + Results: []core.ParsedResult{ + {Type: core.ParsedType{Kind: core.KindPrimitive, Name: "int"}}, + }, + }, + }, + }, + }, + } + + code, err := plugin.Generate(pkg) + Expect(err).NotTo(HaveOccurred()) + Expect(string(code)).To(ContainSubstring("Value_Get")) + }) + }) + + Describe("getSharedLibExtension", func() { + It("returns platform-specific extension", func() { + ext := getSharedLibExtension() + Expect(ext).To(BeElementOf(".so", ".dylib", ".dll")) + }) + }) +}) diff --git a/plugins/python/plugin.go b/plugins/python/plugin.go index 1a6547e..05a6f8e 100644 --- a/plugins/python/plugin.go +++ b/plugins/python/plugin.go @@ -3,6 +3,8 @@ package python import ( "bytes" "fmt" + "os" + "path/filepath" "strings" "github.com/riceriley59/goanywhere/internal/core" @@ -805,3 +807,203 @@ func toSnakeCase(s string) string { } return string(result) } + +// Build generates Python bindings and creates a distributable Python package +func (a *Plugin) Build(pkg *core.ParsedPackage, inputPath string, opts *core.BuildOptions) error { + // First, build the CGO shared library (Python needs it) + if opts.Verbose { + fmt.Println("Building CGO shared library for Python bindings...") + } + + // Get CGO plugin and build the shared library + cgoPlugin, err := factory.Get("cgo", opts.Verbose) + if err != nil { + return fmt.Errorf("failed to get CGO plugin: %w", err) + } + + if err := cgoPlugin.Build(pkg, inputPath, opts); err != nil { + return fmt.Errorf("failed to build CGO library: %w", err) + } + + // Generate Python bindings + if opts.Verbose { + fmt.Println("Generating Python bindings...") + } + code, err := a.Generate(pkg) + if err != nil { + return fmt.Errorf("generation error: %w", err) + } + + // Create Python package structure + pythonPkgName := strings.ReplaceAll(pkg.Name, "-", "_") + pkgDir := filepath.Join(opts.OutputDir, pythonPkgName) + libDir := filepath.Join(pkgDir, "lib") + + if err := os.MkdirAll(libDir, 0755); err != nil { + return fmt.Errorf("cannot create package directory: %w", err) + } + + // Write Python bindings + bindingsFile := filepath.Join(pkgDir, "bindings.py") + if err := os.WriteFile(bindingsFile, code, 0644); err != nil { + return fmt.Errorf("write error: %w", err) + } + fmt.Printf("Generated Python bindings: %s\n", bindingsFile) + + // Write __init__.py + initContent := fmt.Sprintf(`"""Python bindings for %s""" +from .bindings import * +`, pkg.Name) + initFile := filepath.Join(pkgDir, "__init__.py") + if err := os.WriteFile(initFile, []byte(initContent), 0644); err != nil { + return fmt.Errorf("write error: %w", err) + } + + // Copy shared library to lib directory + libName := opts.LibraryName + if libName == "" { + libName = "lib" + pkg.Name + } + libExt := getSharedLibExtension() + srcLib := filepath.Join(opts.OutputDir, libName+libExt) + dstLib := filepath.Join(libDir, libName+libExt) + + if err := copyFile(srcLib, dstLib); err != nil { + return fmt.Errorf("failed to copy library: %w", err) + } + + // Generate pyproject.toml based on build system + pyprojectContent := generatePyprojectToml(pythonPkgName, opts.BuildSystem) + pyprojectFile := filepath.Join(opts.OutputDir, "pyproject.toml") + if err := os.WriteFile(pyprojectFile, []byte(pyprojectContent), 0644); err != nil { + return fmt.Errorf("write error: %w", err) + } + fmt.Printf("Generated pyproject.toml: %s\n", pyprojectFile) + + // Print instructions + fmt.Printf("\nPython package created at: %s\n", opts.OutputDir) + fmt.Println("\nTo install the package:") + fmt.Printf(" cd %s\n", opts.OutputDir) + fmt.Println(" pip install -e .") + fmt.Println("\nTo build a distributable package:") + switch opts.BuildSystem { + case "hatch": + fmt.Println(" hatch build") + case "poetry": + fmt.Println(" poetry build") + case "uv": + fmt.Println(" uv build") + default: + fmt.Println(" pip install build") + fmt.Println(" python -m build") + } + + return nil +} + +// getSharedLibExtension returns the platform-specific shared library extension +func getSharedLibExtension() string { + switch os.Getenv("GOOS") { + case "darwin": + return ".dylib" + case "windows": + return ".dll" + case "": + // Check runtime if GOOS not set + switch filepath.Separator { + case '/': + // Unix-like, check for macOS + if _, err := os.Stat("/System/Library"); err == nil { + return ".dylib" + } + return ".so" + case '\\': + return ".dll" + } + } + return ".so" +} + +// copyFile copies a file from src to dst +func copyFile(src, dst string) error { + data, err := os.ReadFile(src) + if err != nil { + return err + } + return os.WriteFile(dst, data, 0644) +} + +// generatePyprojectToml generates the pyproject.toml content based on build system +func generatePyprojectToml(pkgName, buildSystem string) string { + switch buildSystem { + case "hatch": + return fmt.Sprintf(`[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "%s" +version = "0.1.0" +description = "Python bindings for %s" +requires-python = ">=3.8" + +[tool.hatch.build.targets.wheel] +packages = ["%s"] + +[tool.hatch.build.targets.wheel.shared-data] +"%s/lib" = "lib" +`, pkgName, pkgName, pkgName, pkgName) + + case "poetry": + return fmt.Sprintf(`[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" + +[tool.poetry] +name = "%s" +version = "0.1.0" +description = "Python bindings for %s" +authors = [] + +[tool.poetry.dependencies] +python = ">=3.8" + +[tool.poetry.packages] +include = "%s" +`, pkgName, pkgName, pkgName) + + case "uv": + // uv is compatible with standard pyproject.toml (setuptools or hatch) + return fmt.Sprintf(`[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "%s" +version = "0.1.0" +description = "Python bindings for %s" +requires-python = ">=3.8" + +[tool.hatch.build.targets.wheel] +packages = ["%s"] +`, pkgName, pkgName, pkgName) + + default: // setuptools + return fmt.Sprintf(`[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "%s" +version = "0.1.0" +description = "Python bindings for %s" +requires-python = ">=3.8" + +[tool.setuptools.packages.find] +where = ["."] + +[tool.setuptools.package-data] +"%s" = ["lib/*"] +`, pkgName, pkgName, pkgName) + } +} diff --git a/plugins/python/plugin_test.go b/plugins/python/plugin_test.go new file mode 100644 index 0000000..3af0ec4 --- /dev/null +++ b/plugins/python/plugin_test.go @@ -0,0 +1,657 @@ +package python + +import ( + "os" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/riceriley59/goanywhere/internal/core" +) + +var _ = Describe("Plugin", func() { + var plugin *Plugin + + BeforeEach(func() { + plugin = NewPlugin(false) + }) + + Describe("NewPlugin", func() { + It("creates plugin with verbose off", func() { + p := NewPlugin(false) + Expect(p).NotTo(BeNil()) + Expect(p.verbose).To(BeFalse()) + }) + + It("creates plugin with verbose on", func() { + p := NewPlugin(true) + Expect(p.verbose).To(BeTrue()) + }) + }) + + Describe("Name", func() { + It("returns python", func() { + Expect(plugin.Name()).To(Equal("python")) + }) + }) + + Describe("Generate", func() { + It("generates valid Python code for simple package", func() { + pkg := &core.ParsedPackage{ + Name: "test", + ImportPath: "github.com/test/test", + Functions: []core.ParsedFunc{ + { + Name: "Add", + Params: []core.ParsedParam{ + {Name: "a", Type: core.ParsedType{Kind: core.KindPrimitive, Name: "int"}}, + {Name: "b", Type: core.ParsedType{Kind: core.KindPrimitive, Name: "int"}}, + }, + Results: []core.ParsedResult{ + {Type: core.ParsedType{Kind: core.KindPrimitive, Name: "int"}}, + }, + }, + }, + } + + code, err := plugin.Generate(pkg) + Expect(err).NotTo(HaveOccurred()) + Expect(code).NotTo(BeEmpty()) + + codeStr := string(code) + Expect(codeStr).To(ContainSubstring("from ctypes import")) + Expect(codeStr).To(ContainSubstring("def add(")) + Expect(codeStr).To(ContainSubstring("test_Add")) + }) + + It("generates class wrappers for structs", func() { + pkg := &core.ParsedPackage{ + Name: "test", + ImportPath: "github.com/test/test", + Structs: []core.ParsedStruct{ + { + Name: "Point", + Fields: []core.ParsedField{ + {Name: "X", Type: core.ParsedType{Kind: core.KindPrimitive, Name: "int"}, Exported: true}, + {Name: "Y", Type: core.ParsedType{Kind: core.KindPrimitive, Name: "int"}, Exported: true}, + }, + }, + }, + } + + code, err := plugin.Generate(pkg) + Expect(err).NotTo(HaveOccurred()) + + codeStr := string(code) + Expect(codeStr).To(ContainSubstring("class Point")) + Expect(codeStr).To(ContainSubstring("def __init__")) + Expect(codeStr).To(ContainSubstring("def __del__")) + Expect(codeStr).To(ContainSubstring("@property")) + Expect(codeStr).To(ContainSubstring("def x(self)")) + }) + + It("generates method wrappers", func() { + pkg := &core.ParsedPackage{ + Name: "test", + ImportPath: "github.com/test/test", + Structs: []core.ParsedStruct{ + { + Name: "Point", + Methods: []core.ParsedMethod{ + { + Name: "Distance", + ReceiverName: "p", + ReceiverType: "Point", + ReceiverIsPtr: true, + Results: []core.ParsedResult{ + {Type: core.ParsedType{Kind: core.KindPrimitive, Name: "float64"}}, + }, + }, + }, + }, + }, + } + + code, err := plugin.Generate(pkg) + Expect(err).NotTo(HaveOccurred()) + + codeStr := string(code) + Expect(codeStr).To(ContainSubstring("def distance(self)")) + }) + + It("handles string parameters and returns", func() { + pkg := &core.ParsedPackage{ + Name: "test", + ImportPath: "github.com/test/test", + Functions: []core.ParsedFunc{ + { + Name: "Greet", + Params: []core.ParsedParam{ + {Name: "name", Type: core.ParsedType{Kind: core.KindString, Name: "string"}}, + }, + Results: []core.ParsedResult{ + {Type: core.ParsedType{Kind: core.KindString, Name: "string"}}, + }, + }, + }, + } + + code, err := plugin.Generate(pkg) + Expect(err).NotTo(HaveOccurred()) + + codeStr := string(code) + Expect(codeStr).To(ContainSubstring("def greet(")) + Expect(codeStr).To(ContainSubstring("_encode_string")) + Expect(codeStr).To(ContainSubstring("_decode_string")) + }) + + It("handles error returns", func() { + pkg := &core.ParsedPackage{ + Name: "test", + ImportPath: "github.com/test/test", + Functions: []core.ParsedFunc{ + { + Name: "Divide", + Params: []core.ParsedParam{ + {Name: "a", Type: core.ParsedType{Kind: core.KindPrimitive, Name: "int"}}, + {Name: "b", Type: core.ParsedType{Kind: core.KindPrimitive, Name: "int"}}, + }, + Results: []core.ParsedResult{ + {Type: core.ParsedType{Kind: core.KindPrimitive, Name: "int"}}, + {Type: core.ParsedType{Kind: core.KindError, Name: "error"}}, + }, + }, + }, + } + + code, err := plugin.Generate(pkg) + Expect(err).NotTo(HaveOccurred()) + + codeStr := string(code) + Expect(codeStr).To(ContainSubstring("def divide(")) + Expect(codeStr).To(ContainSubstring("_check_error")) + }) + + It("skips variadic functions", func() { + verbosePlugin := NewPlugin(true) + pkg := &core.ParsedPackage{ + Name: "test", + ImportPath: "github.com/test/test", + Functions: []core.ParsedFunc{ + { + Name: "Sum", + IsVariadic: true, + Params: []core.ParsedParam{ + {Name: "nums", Type: core.ParsedType{Kind: core.KindSlice, Name: "[]int"}}, + }, + Results: []core.ParsedResult{ + {Type: core.ParsedType{Kind: core.KindPrimitive, Name: "int"}}, + }, + }, + }, + } + + code, err := verbosePlugin.Generate(pkg) + Expect(err).NotTo(HaveOccurred()) + + codeStr := string(code) + Expect(codeStr).NotTo(ContainSubstring("def sum(")) + }) + + It("generates library loader code", func() { + pkg := &core.ParsedPackage{ + Name: "test", + ImportPath: "github.com/test/test", + Functions: []core.ParsedFunc{ + { + Name: "Hello", + Results: []core.ParsedResult{ + {Type: core.ParsedType{Kind: core.KindString, Name: "string"}}, + }, + }, + }, + } + + code, err := plugin.Generate(pkg) + Expect(err).NotTo(HaveOccurred()) + + codeStr := string(code) + Expect(codeStr).To(ContainSubstring("def load_library")) + Expect(codeStr).To(ContainSubstring("libtest")) + Expect(codeStr).To(ContainSubstring(".so")) + Expect(codeStr).To(ContainSubstring(".dylib")) + Expect(codeStr).To(ContainSubstring(".dll")) + }) + + It("generates helper functions", func() { + pkg := &core.ParsedPackage{ + Name: "test", + ImportPath: "github.com/test/test", + Functions: []core.ParsedFunc{ + { + Name: "GetName", + Results: []core.ParsedResult{ + {Type: core.ParsedType{Kind: core.KindString, Name: "string"}}, + }, + }, + }, + } + + code, err := plugin.Generate(pkg) + Expect(err).NotTo(HaveOccurred()) + + codeStr := string(code) + Expect(codeStr).To(ContainSubstring("def _encode_string")) + Expect(codeStr).To(ContainSubstring("def _decode_string")) + Expect(codeStr).To(ContainSubstring("def _check_error")) + }) + + It("generates context manager support", func() { + pkg := &core.ParsedPackage{ + Name: "test", + ImportPath: "github.com/test/test", + Structs: []core.ParsedStruct{ + {Name: "Resource"}, + }, + } + + code, err := plugin.Generate(pkg) + Expect(err).NotTo(HaveOccurred()) + + codeStr := string(code) + Expect(codeStr).To(ContainSubstring("def __enter__")) + Expect(codeStr).To(ContainSubstring("def __exit__")) + }) + + It("handles bool parameters", func() { + pkg := &core.ParsedPackage{ + Name: "test", + ImportPath: "github.com/test/test", + Functions: []core.ParsedFunc{ + { + Name: "Toggle", + Params: []core.ParsedParam{ + {Name: "flag", Type: core.ParsedType{Kind: core.KindPrimitive, Name: "bool"}}, + }, + Results: []core.ParsedResult{ + {Type: core.ParsedType{Kind: core.KindPrimitive, Name: "bool"}}, + }, + }, + }, + } + + code, err := plugin.Generate(pkg) + Expect(err).NotTo(HaveOccurred()) + + codeStr := string(code) + Expect(codeStr).To(ContainSubstring("def toggle(")) + Expect(codeStr).To(ContainSubstring("bool")) + }) + + It("handles float parameters", func() { + pkg := &core.ParsedPackage{ + Name: "test", + ImportPath: "github.com/test/test", + Functions: []core.ParsedFunc{ + { + Name: "Multiply", + Params: []core.ParsedParam{ + {Name: "a", Type: core.ParsedType{Kind: core.KindPrimitive, Name: "float64"}}, + {Name: "b", Type: core.ParsedType{Kind: core.KindPrimitive, Name: "float64"}}, + }, + Results: []core.ParsedResult{ + {Type: core.ParsedType{Kind: core.KindPrimitive, Name: "float64"}}, + }, + }, + }, + } + + code, err := plugin.Generate(pkg) + Expect(err).NotTo(HaveOccurred()) + + codeStr := string(code) + Expect(codeStr).To(ContainSubstring("def multiply(")) + Expect(codeStr).To(ContainSubstring("float")) + }) + + It("handles methods with parameters", func() { + pkg := &core.ParsedPackage{ + Name: "test", + ImportPath: "github.com/test/test", + Structs: []core.ParsedStruct{ + { + Name: "Calculator", + Methods: []core.ParsedMethod{ + { + Name: "Add", + ReceiverName: "c", + ReceiverType: "Calculator", + ReceiverIsPtr: true, + Params: []core.ParsedParam{ + {Name: "a", Type: core.ParsedType{Kind: core.KindPrimitive, Name: "int"}}, + {Name: "b", Type: core.ParsedType{Kind: core.KindPrimitive, Name: "int"}}, + }, + Results: []core.ParsedResult{ + {Type: core.ParsedType{Kind: core.KindPrimitive, Name: "int"}}, + }, + }, + }, + }, + }, + } + + code, err := plugin.Generate(pkg) + Expect(err).NotTo(HaveOccurred()) + + codeStr := string(code) + Expect(codeStr).To(ContainSubstring("def add(self")) + }) + + It("generates type hints", func() { + pkg := &core.ParsedPackage{ + Name: "test", + ImportPath: "github.com/test/test", + Functions: []core.ParsedFunc{ + { + Name: "Add", + Params: []core.ParsedParam{ + {Name: "a", Type: core.ParsedType{Kind: core.KindPrimitive, Name: "int"}}, + {Name: "b", Type: core.ParsedType{Kind: core.KindPrimitive, Name: "int"}}, + }, + Results: []core.ParsedResult{ + {Type: core.ParsedType{Kind: core.KindPrimitive, Name: "int"}}, + }, + }, + }, + } + + code, err := plugin.Generate(pkg) + Expect(err).NotTo(HaveOccurred()) + + codeStr := string(code) + Expect(codeStr).To(ContainSubstring("-> int")) + }) + }) + + Describe("toSnakeCase", func() { + It("converts camelCase to snake_case", func() { + Expect(toSnakeCase("helloWorld")).To(Equal("hello_world")) + Expect(toSnakeCase("MyFunction")).To(Equal("my_function")) + Expect(toSnakeCase("HTTPServer")).To(Equal("h_t_t_p_server")) + }) + + It("handles single word", func() { + Expect(toSnakeCase("hello")).To(Equal("hello")) + Expect(toSnakeCase("Hello")).To(Equal("hello")) + }) + + It("handles empty string", func() { + Expect(toSnakeCase("")).To(Equal("")) + }) + }) + + Describe("Generate comprehensive", func() { + It("handles empty package", func() { + pkg := &core.ParsedPackage{ + Name: "empty", + ImportPath: "github.com/test/empty", + } + + code, err := plugin.Generate(pkg) + Expect(err).NotTo(HaveOccurred()) + Expect(string(code)).To(ContainSubstring("from ctypes import")) + }) + + It("handles function with no params", func() { + pkg := &core.ParsedPackage{ + Name: "test", + ImportPath: "github.com/test/test", + Functions: []core.ParsedFunc{ + { + Name: "GetValue", + Results: []core.ParsedResult{{Type: core.ParsedType{Kind: core.KindPrimitive, Name: "int"}}}, + }, + }, + } + + code, err := plugin.Generate(pkg) + Expect(err).NotTo(HaveOccurred()) + Expect(string(code)).To(ContainSubstring("def get_value()")) + }) + + It("handles function with no return value", func() { + pkg := &core.ParsedPackage{ + Name: "test", + ImportPath: "github.com/test/test", + Functions: []core.ParsedFunc{ + { + Name: "DoNothing", + }, + }, + } + + code, err := plugin.Generate(pkg) + Expect(err).NotTo(HaveOccurred()) + Expect(string(code)).To(ContainSubstring("def do_nothing()")) + }) + + It("handles struct with unexported fields", func() { + pkg := &core.ParsedPackage{ + Name: "test", + ImportPath: "github.com/test/test", + Structs: []core.ParsedStruct{ + { + Name: "Mixed", + Fields: []core.ParsedField{ + {Name: "Public", Type: core.ParsedType{Kind: core.KindPrimitive, Name: "int"}, Exported: true}, + {Name: "private", Type: core.ParsedType{Kind: core.KindPrimitive, Name: "int"}, Exported: false}, + }, + }, + }, + } + + code, err := plugin.Generate(pkg) + Expect(err).NotTo(HaveOccurred()) + codeStr := string(code) + Expect(codeStr).To(ContainSubstring("class Mixed")) + Expect(codeStr).To(ContainSubstring("def public")) + }) + + It("handles method with error return", func() { + pkg := &core.ParsedPackage{ + Name: "test", + ImportPath: "github.com/test/test", + Structs: []core.ParsedStruct{ + { + Name: "Service", + Methods: []core.ParsedMethod{ + { + Name: "Call", + ReceiverName: "s", + ReceiverType: "Service", + ReceiverIsPtr: true, + Results: []core.ParsedResult{ + {Type: core.ParsedType{Kind: core.KindString, Name: "string"}}, + {Type: core.ParsedType{Kind: core.KindError, Name: "error"}}, + }, + }, + }, + }, + }, + } + + code, err := plugin.Generate(pkg) + Expect(err).NotTo(HaveOccurred()) + codeStr := string(code) + Expect(codeStr).To(ContainSubstring("class Service")) + Expect(codeStr).To(ContainSubstring("def call")) + }) + + It("handles uint types", func() { + pkg := &core.ParsedPackage{ + Name: "test", + ImportPath: "github.com/test/test", + Functions: []core.ParsedFunc{ + { + Name: "ProcessUint", + Params: []core.ParsedParam{ + {Name: "a", Type: core.ParsedType{Kind: core.KindPrimitive, Name: "uint"}}, + {Name: "b", Type: core.ParsedType{Kind: core.KindPrimitive, Name: "uint64"}}, + }, + Results: []core.ParsedResult{ + {Type: core.ParsedType{Kind: core.KindPrimitive, Name: "uint32"}}, + }, + }, + }, + } + + code, err := plugin.Generate(pkg) + Expect(err).NotTo(HaveOccurred()) + Expect(string(code)).To(ContainSubstring("def process_uint")) + }) + + It("handles value receiver method", func() { + pkg := &core.ParsedPackage{ + Name: "test", + ImportPath: "github.com/test/test", + Structs: []core.ParsedStruct{ + { + Name: "Value", + Methods: []core.ParsedMethod{ + { + Name: "Get", + ReceiverName: "v", + ReceiverType: "Value", + ReceiverIsPtr: false, + Results: []core.ParsedResult{ + {Type: core.ParsedType{Kind: core.KindPrimitive, Name: "int"}}, + }, + }, + }, + }, + }, + } + + code, err := plugin.Generate(pkg) + Expect(err).NotTo(HaveOccurred()) + Expect(string(code)).To(ContainSubstring("def get(self)")) + }) + + It("handles multiple functions", func() { + pkg := &core.ParsedPackage{ + Name: "test", + ImportPath: "github.com/test/test", + Functions: []core.ParsedFunc{ + {Name: "First", Results: []core.ParsedResult{{Type: core.ParsedType{Kind: core.KindPrimitive, Name: "int"}}}}, + {Name: "Second", Results: []core.ParsedResult{{Type: core.ParsedType{Kind: core.KindPrimitive, Name: "int"}}}}, + {Name: "Third", Results: []core.ParsedResult{{Type: core.ParsedType{Kind: core.KindPrimitive, Name: "int"}}}}, + }, + } + + code, err := plugin.Generate(pkg) + Expect(err).NotTo(HaveOccurred()) + codeStr := string(code) + Expect(codeStr).To(ContainSubstring("def first")) + Expect(codeStr).To(ContainSubstring("def second")) + Expect(codeStr).To(ContainSubstring("def third")) + }) + + It("handles struct with method that returns struct", func() { + pkg := &core.ParsedPackage{ + Name: "test", + ImportPath: "github.com/test/test", + Structs: []core.ParsedStruct{ + { + Name: "Builder", + Methods: []core.ParsedMethod{ + { + Name: "Build", + ReceiverName: "b", + ReceiverType: "Builder", + ReceiverIsPtr: true, + Results: []core.ParsedResult{ + {Type: core.ParsedType{Kind: core.KindPointer, Name: "*Builder", ElemType: &core.ParsedType{Kind: core.KindStruct, Name: "Builder"}}}, + }, + }, + }, + }, + }, + } + + code, err := plugin.Generate(pkg) + Expect(err).NotTo(HaveOccurred()) + Expect(string(code)).To(ContainSubstring("def build")) + }) + }) + + Describe("getSharedLibExtension", func() { + It("returns platform-specific extension", func() { + ext := getSharedLibExtension() + Expect(ext).To(BeElementOf(".so", ".dylib", ".dll")) + }) + }) + + Describe("generatePyprojectToml", func() { + It("generates setuptools config", func() { + content := generatePyprojectToml("mypackage", "setuptools") + Expect(content).To(ContainSubstring("setuptools")) + Expect(content).To(ContainSubstring("mypackage")) + }) + + It("generates hatch config", func() { + content := generatePyprojectToml("mypackage", "hatch") + Expect(content).To(ContainSubstring("hatchling")) + Expect(content).To(ContainSubstring("mypackage")) + }) + + It("generates poetry config", func() { + content := generatePyprojectToml("mypackage", "poetry") + Expect(content).To(ContainSubstring("poetry")) + Expect(content).To(ContainSubstring("mypackage")) + }) + + It("generates uv config", func() { + content := generatePyprojectToml("mypackage", "uv") + Expect(content).To(ContainSubstring("hatchling")) + Expect(content).To(ContainSubstring("mypackage")) + }) + + It("defaults to setuptools for unknown system", func() { + content := generatePyprojectToml("mypackage", "unknown") + Expect(content).To(ContainSubstring("setuptools")) + }) + }) + + Describe("copyFile", func() { + It("copies file contents", func() { + // Create source file + srcFile, err := os.CreateTemp("", "src") + Expect(err).NotTo(HaveOccurred()) + defer func() { _ = os.Remove(srcFile.Name()) }() + + content := []byte("test content") + _, err = srcFile.Write(content) + Expect(err).NotTo(HaveOccurred()) + _ = srcFile.Close() + + // Copy to destination + dstFile, err := os.CreateTemp("", "dst") + Expect(err).NotTo(HaveOccurred()) + defer func() { _ = os.Remove(dstFile.Name()) }() + _ = dstFile.Close() + + err = copyFile(srcFile.Name(), dstFile.Name()) + Expect(err).NotTo(HaveOccurred()) + + // Verify content + data, err := os.ReadFile(dstFile.Name()) + Expect(err).NotTo(HaveOccurred()) + Expect(data).To(Equal(content)) + }) + + It("returns error for non-existent source", func() { + err := copyFile("/nonexistent/file", "/tmp/dst") + Expect(err).To(HaveOccurred()) + }) + }) +}) diff --git a/tests/fixtures/complex/complex.go b/tests/fixtures/complex/complex.go new file mode 100644 index 0000000..90e15ab --- /dev/null +++ b/tests/fixtures/complex/complex.go @@ -0,0 +1,49 @@ +package complex + +// Config represents a configuration with various types +type Config struct { + Name string + Values []int + Data [5]byte + Options map[string]string +} + +// ProcessArray takes a fixed-size array +func ProcessArray(data [10]int) [10]int { + return data +} + +// ProcessSlice takes a slice +func ProcessSlice(data []string) []string { + return data +} + +// ProcessMap takes a map +func ProcessMap(data map[string]int) map[string]int { + return data +} + +// ProcessPointer takes a pointer +func ProcessPointer(p *Config) *Config { + return p +} + +// ProcessInterface takes an interface +func ProcessInterface(data interface{}) interface{} { + return data +} + +// NewConfig creates a new Config +func NewConfig(name string) *Config { + return &Config{Name: name} +} + +// GetName returns the config name +func (c *Config) GetName() string { + return c.Name +} + +// SetValues sets the values slice +func (c *Config) SetValues(values []int) { + c.Values = values +}