package hrp import ( "bufio" _ "embed" "fmt" "html/template" "os" "path/filepath" "regexp" "strings" "github.com/pkg/errors" "github.com/rs/zerolog/log" "github.com/httprunner/funplugin/fungo" "github.com/httprunner/funplugin/myexec" "github.com/httprunner/httprunner/v5/code" "github.com/httprunner/httprunner/v5/internal/builtin" "github.com/httprunner/httprunner/v5/internal/config" "github.com/httprunner/httprunner/v5/internal/sdk" "github.com/httprunner/httprunner/v5/internal/version" ) //go:embed internal/scaffold/templates/plugin/debugtalkPythonTemplate var pyTemplate string //go:embed internal/scaffold/templates/plugin/debugtalkGoTemplate var goTemplate string // regex for finding all function names type regexFunctions struct { *regexp.Regexp } var ( regexPyFunctionName = regexFunctions{regexp.MustCompile(`(?m)^def ([a-zA-Z_]\w*)\(.*\)`)} regexGoFunctionName = regexFunctions{regexp.MustCompile(`(?m)^func ([a-zA-Z_]\w*)\(.*\)`)} ) func (r *regexFunctions) findAllFunctionNames(content string) ([]string, error) { var functionNames []string // find all function names functionNameSlice := r.FindAllStringSubmatch(content, -1) for _, elem := range functionNameSlice { name := strings.Trim(elem[1], " ") functionNames = append(functionNames, name) } var filteredFunctionNames []string if r == ®exPyFunctionName { // filter private functions for _, name := range functionNames { if strings.HasPrefix(name, "__") { continue } filteredFunctionNames = append(filteredFunctionNames, name) } } else if r == ®exGoFunctionName { // filter main and init function for _, name := range functionNames { if name == "main" { log.Warn().Msg("plugin debugtalk.go should not define main() function !!!") return nil, errors.New("debugtalk.go should not contain main() function") } if name == "init" { continue } filteredFunctionNames = append(filteredFunctionNames, name) } } log.Info().Strs("functionNames", filteredFunctionNames).Msg("find all function names") return filteredFunctionNames, nil } type pluginTemplate struct { path string // file path Version string // hrp version FunctionNames []string // function names } func (pt *pluginTemplate) generate(tmpl, output string) error { file, err := os.Create(output) if err != nil { log.Error().Err(err).Msg("open output file failed") return err } defer file.Close() writer := bufio.NewWriter(file) err = template.Must(template.New("debugtalk").Parse(tmpl)).Execute(writer, pt) if err != nil { log.Error().Err(err).Msg("execute template parsing failed") return err } err = writer.Flush() if err == nil { log.Info().Str("output", output).Msg("generate debugtalk success") } else { log.Error().Str("output", output).Msg("generate debugtalk failed") } return err } func (pt *pluginTemplate) generatePy(output string) error { // specify output file path if output == "" { output = filepath.Join(config.GetConfig().RootDir, PluginPySourceGenFile) } else if builtin.IsFolderPathExists(output) { output = filepath.Join(output, PluginPySourceGenFile) } // generate .debugtalk_gen.py err := pt.generate(pyTemplate, output) if err != nil { return err } log.Info().Str("output", output).Str("plugin", pt.path).Msg("build python plugin successfully") return nil } func (pt *pluginTemplate) generateGo(output string) error { pluginDir := filepath.Dir(pt.path) err := pt.generate(goTemplate, filepath.Join(pluginDir, PluginGoSourceGenFile)) if err != nil { return errors.Wrap(err, "generate hashicorp plugin failed") } // check go sdk in tempDir if err := myexec.RunCommand("go", "version"); err != nil { return errors.Wrap(err, "go sdk not installed") } if !builtin.IsFilePathExists(filepath.Join(pluginDir, "go.mod")) { // create go mod if err := myexec.ExecCommandInDir(myexec.Command("go", "mod", "init", "main"), pluginDir); err != nil { return err } // download plugin dependency // funplugin version should be locked funplugin := fmt.Sprintf("github.com/httprunner/funplugin@%s", fungo.Version) if err := myexec.ExecCommandInDir(myexec.Command("go", "get", funplugin), pluginDir); err != nil { return errors.Wrap(err, "go get funplugin failed") } } // add missing and remove unused modules if err := myexec.ExecCommandInDir(myexec.Command("go", "mod", "tidy"), pluginDir); err != nil { return errors.Wrap(err, "go mod tidy failed") } // specify output file path if output == "" { output = filepath.Join(config.GetConfig().RootDir, PluginHashicorpGoBuiltFile) } else if builtin.IsFolderPathExists(output) { output = filepath.Join(output, PluginHashicorpGoBuiltFile) } outputPath, _ := filepath.Abs(output) // build go plugin to debugtalk.bin cmd := myexec.Command("go", "build", "-o", outputPath, PluginGoSourceGenFile, filepath.Base(pt.path)) if err := myexec.ExecCommandInDir(cmd, pluginDir); err != nil { return errors.Wrap(err, "go build plugin failed") } log.Info().Str("output", outputPath).Str("plugin", pt.path).Msg("build go plugin successfully") return nil } // buildGo builds debugtalk.go to debugtalk.bin func buildGo(path string, output string) error { log.Info().Str("path", path).Str("output", output).Msg("start to build go plugin") // report GA event sdk.SendGA4Event("hrp_build_plugin", map[string]interface{}{ "pluginType": "go", }) content, err := os.ReadFile(path) if err != nil { log.Error().Err(err).Msg("failed to read file") return errors.Wrap(code.LoadFileError, err.Error()) } functionNames, err := regexGoFunctionName.findAllFunctionNames(string(content)) if err != nil { return errors.Wrap(code.InvalidPluginFile, err.Error()) } templateContent := &pluginTemplate{ path: path, Version: version.VERSION, FunctionNames: functionNames, } err = templateContent.generateGo(output) if err != nil { return errors.Wrap(code.BuildGoPluginFailed, err.Error()) } return nil } // buildPy completes funppy information in debugtalk.py func buildPy(path string, output string) error { log.Info().Str("path", path).Str("output", output).Msg("start to prepare python plugin") // report GA event sdk.SendGA4Event("hrp_build_plugin", map[string]interface{}{ "pluginType": "python", }) // check the syntax of debugtalk.py err := myexec.ExecPython3Command("py_compile", path) if err != nil { return errors.Wrap(code.InvalidPluginFile, fmt.Sprintf("python plugin syntax invalid: %s", err.Error())) } content, err := os.ReadFile(path) if err != nil { log.Error().Err(err).Msg("failed to read file") return errors.Wrap(code.LoadFileError, err.Error()) } functionNames, err := regexPyFunctionName.findAllFunctionNames(string(content)) if err != nil { return errors.Wrap(code.InvalidPluginFile, err.Error()) } templateContent := &pluginTemplate{ path: path, Version: version.VERSION, FunctionNames: functionNames, } err = templateContent.generatePy(output) if err != nil { return errors.Wrap(code.BuildPyPluginFailed, err.Error()) } return nil } func BuildPlugin(path string, output string) (err error) { ext := filepath.Ext(path) switch ext { case ".py": err = buildPy(path, output) case ".go": err = buildGo(path, output) default: return errors.Wrap(code.UnsupportedFileExtension, "type error, expected .py or .go") } if err != nil { log.Error().Err(err).Str("path", path).Msg("build plugin failed") return err } return nil }