refactor: build plugin

This commit is contained in:
debugtalk
2022-06-13 23:40:45 +08:00
parent 182d2fd5d8
commit 9de49a1d29
11 changed files with 93 additions and 300 deletions
+2
View File
@@ -5,6 +5,8 @@
**go version** **go version**
- feat #1342: support specify custom python3 venv - feat #1342: support specify custom python3 venv
- feat: support python3 venv priority, specified > projectDir/.venv > $HOME/.hrp/venv
- refactor: build plugin mechanism
- fix: pip upgrade httprunner when installing hrp - fix: pip upgrade httprunner when installing hrp
## v4.1.2 (2022-06-09) ## v4.1.2 (2022-06-09)
+1 -1
View File
@@ -1,5 +1,5 @@
{ {
"project_name": "demo-with-go-plugin", "project_name": "demo-with-go-plugin",
"create_time": "2022-06-09T23:39:14.781583+08:00", "create_time": "2022-06-13T23:28:24.920066+08:00",
"hrp_version": "v4.1.2" "hrp_version": "v4.1.2"
} }
@@ -173,4 +173,4 @@
] ]
} }
] ]
} }
+5 -57
View File
@@ -1,67 +1,15 @@
# NOTE: Generated By hrp v4.1.2, DO NOT EDIT! # NOTE: Generated By hrp v4.1.2, DO NOT EDIT!
import logging import sys
import time import os
import funppy
from typing import List sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from debugtalk import *
def get_user_agent():
return "hrp/funppy"
def sleep(n_secs):
time.sleep(n_secs)
def sum(*args):
result = 0
for arg in args:
result += arg
return result
def sum_ints(*args: List[int]) -> int:
result = 0
for arg in args:
result += arg
return result
def sum_two_int(a: int, b: int) -> int:
return a + b
def sum_two_string(a: str, b: str) -> str:
return a + b
def sum_strings(*args: List[str]) -> str:
result = ""
for arg in args:
result += arg
return result
def concatenate(*args: List[str]) -> str:
result = ""
for arg in args:
result += str(arg)
return result
def setup_hook_example(name):
logging.warning("setup_hook_example")
return f"setup_hook_example: {name}"
def teardown_hook_example(name):
logging.warning("teardown_hook_example")
return f"teardown_hook_example: {name}"
if __name__ == "__main__": if __name__ == "__main__":
import funppy
funppy.register("get_user_agent", get_user_agent) funppy.register("get_user_agent", get_user_agent)
funppy.register("sleep", sleep) funppy.register("sleep", sleep)
funppy.register("sum", sum) funppy.register("sum", sum)
+1 -1
View File
@@ -1,5 +1,5 @@
{ {
"project_name": "demo-with-py-plugin", "project_name": "demo-with-py-plugin",
"create_time": "2022-06-09T23:39:14.922843+08:00", "create_time": "2022-06-13T23:28:25.254595+08:00",
"hrp_version": "v4.1.2" "hrp_version": "v4.1.2"
} }
@@ -173,4 +173,4 @@
] ]
} }
] ]
} }
+70 -168
View File
@@ -4,13 +4,12 @@ import (
"bufio" "bufio"
_ "embed" _ "embed"
"fmt" "fmt"
"io" "html/template"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"regexp" "regexp"
"strings" "strings"
"text/template"
"github.com/httprunner/funplugin/shared" "github.com/httprunner/funplugin/shared"
"github.com/pkg/errors" "github.com/pkg/errors"
@@ -20,156 +19,57 @@ import (
"github.com/httprunner/httprunner/v4/hrp/internal/version" "github.com/httprunner/httprunner/v4/hrp/internal/version"
) )
const (
fungo = `"github.com/httprunner/funplugin/fungo"`
regexPythonFunctionName = `def ([a-zA-Z_]\w*)\(.*\)`
regexGoImports = `import \(([\s\S]*?)\)`
regexGoImport = `import (\"[\s\S]*\")`
regexGoFunctionName = `func ([A-Z][a-zA-Z_]\w*)\(.*\)`
regexGoFunctionContent = `func [\s\S]*?\n}`
)
//go:embed internal/scaffold/templates/plugin/debugtalkPythonTemplate //go:embed internal/scaffold/templates/plugin/debugtalkPythonTemplate
var pyTemplate string var pyTemplate string
//go:embed internal/scaffold/templates/plugin/debugtalkGoTemplate //go:embed internal/scaffold/templates/plugin/debugtalkGoTemplate
var goTemplate string var goTemplate string
type TemplateContent struct { // regex for finding all function names
var (
regexPyFunctionName = regexp.MustCompile(`def ([a-zA-Z_]\w*)\(.*\)`)
regexGoFunctionName = regexp.MustCompile(`func ([A-Z][a-zA-Z_]\w*)\(.*\)`)
)
type pluginTemplateContent struct {
Version string // hrp version Version string // hrp version
Regexps *Regexps // match import/function FunctionNames []string // function names
Imports []string // python/go import
FromImports []string // python from...import...
Functions []string // python/go function
FunctionNames []string // function name set by user
Packages []string // python packages
} }
type Regexps struct { func findAllFunctionNames(t *regexp.Regexp, path string) (functionNames []string, err error) {
Import *regexp.Regexp log.Info().Str("path", path).Msg("find all function names from plugin file")
Imports *regexp.Regexp
FunctionName *regexp.Regexp
FunctionContent *regexp.Regexp // including function define and body
}
func (t *TemplateContent) parseGoContent(path string) error {
log.Info().Str("path", path).Msg("start to parse debugtalk.go")
content, err := os.ReadFile(path) content, err := os.ReadFile(path)
if err != nil { if err != nil {
log.Error().Err(err).Msg("failed to read file") log.Error().Err(err).Msg("failed to read file")
return err return nil, errors.Wrap(err, "read file failed")
}
originalContent := string(content)
// parse imports
importSlice := t.Regexps.Imports.FindAllStringSubmatch(originalContent, -1)
if len(importSlice) != 0 {
imports := strings.Replace(importSlice[0][1], "\t", "", -1)
for _, elem := range strings.Split(imports, "\n") {
t.Imports = append(t.Imports, strings.TrimSpace(elem))
}
}
// parse import
importSlice = t.Regexps.Import.FindAllStringSubmatch(originalContent, -1)
if len(importSlice) != 0 {
for _, elem := range importSlice {
t.Imports = append(t.Imports, strings.TrimSpace(elem[1]))
}
}
// import fungo package
if !builtin.Contains(t.Imports, fungo) {
t.Imports = append(t.Imports, fungo)
} }
// parse function name // find all function names
functionNameSlice := t.Regexps.FunctionName.FindAllStringSubmatch(originalContent, -1) functionNameSlice := t.FindAllStringSubmatch(string(content), -1)
for _, elem := range functionNameSlice { for _, elem := range functionNameSlice {
name := strings.Trim(elem[1], " ") name := strings.Trim(elem[1], " ")
if name == "main" { functionNames = append(functionNames, name)
continue
}
t.FunctionNames = append(t.FunctionNames, name)
} }
// parse function content return
functionContentSlice := t.Regexps.FunctionContent.FindAllStringSubmatch(originalContent, -1)
for _, f := range functionContentSlice {
if strings.Contains(f[0], "func main") {
continue
}
t.Functions = append(t.Functions, strings.Trim(f[0], "\n"))
}
return nil
} }
func (t *TemplateContent) parsePyContent(path string) error { func generate(data *pluginTemplateContent, tmpl, output string) error {
file, err := os.Open(path)
if err != nil {
log.Error().Err(err).Str("path", path).Msg("failed to open file")
return err
}
defer file.Close()
r := bufio.NewReader(file)
// record content excluding import and main
content := ""
// parse python content line by line
for {
l, _, err := r.ReadLine()
if err == io.EOF {
break
}
line := string(l)
if strings.HasPrefix(line, "import") {
t.Imports = append(t.Imports, strings.Trim(line, " "))
// e.g. import module as md
// import package.module
t.Packages = append(t.Packages, strings.Split(strings.Split(line, " ")[1], ".")[0])
} else if strings.HasPrefix(line, "from") {
t.FromImports = append(t.FromImports, strings.Trim(line, " "))
// e.g. from package.module import function
// from module import function
// from package import module
t.Packages = append(t.Packages, strings.Split(strings.Split(line, " ")[1], ".")[0])
} else {
// no parse content at under of `if __name__ == "__main__"`
if strings.HasPrefix(line, "if __name__") {
break
}
if strings.HasPrefix(line, "def") {
functionNameSlice := t.Regexps.FunctionName.FindAllStringSubmatch(line, -1)
if len(functionNameSlice) == 0 {
continue
}
t.FunctionNames = append(t.FunctionNames, functionNameSlice[0][1])
}
content += line + "\n"
}
}
// function content
t.Functions = append(t.Functions, strings.Trim(content, "\n"))
return nil
}
func (t *TemplateContent) genDebugTalk(output string, templ string) error {
file, err := os.Create(output) file, err := os.Create(output)
if err != nil { if err != nil {
log.Error().Err(err).Msg("open file failed") log.Error().Err(err).Msg("open output file failed")
return err return err
} }
defer file.Close() defer file.Close()
writer := bufio.NewWriter(file) writer := bufio.NewWriter(file)
tmpl := template.Must(template.New("debugtalk").Parse(templ)) err = template.Must(template.New("debugtalk").Parse(tmpl)).Execute(writer, data)
err = tmpl.Execute(writer, t)
if err != nil { if err != nil {
log.Error().Err(err).Msg("execute applies a parsed template to the specified data object failed") log.Error().Err(err).Msg("execute template parsing failed")
return err return err
} }
err = writer.Flush() err = writer.Flush()
if err == nil { if err == nil {
log.Info().Str("output", output).Msg("generate debugtalk success") log.Info().Str("output", output).Msg("generate debugtalk success")
@@ -181,35 +81,35 @@ func (t *TemplateContent) genDebugTalk(output string, templ string) error {
// buildGo builds debugtalk.go to debugtalk.bin // buildGo builds debugtalk.go to debugtalk.bin
func buildGo(path string, output string) error { func buildGo(path string, output string) error {
templateContent := &TemplateContent{ functionNames, err := findAllFunctionNames(regexGoFunctionName, path)
Version: version.VERSION, if err != nil {
Regexps: &Regexps{ return errors.Wrap(err, "find all function names failed")
Import: regexp.MustCompile(regexGoImport), }
Imports: regexp.MustCompile(regexGoImports), // filter main and init function
FunctionName: regexp.MustCompile(regexGoFunctionName), var filteredFunctionNames []string
FunctionContent: regexp.MustCompile(regexGoFunctionContent), for _, name := range functionNames {
}, if name == "main" || name == "init" {
continue
}
filteredFunctionNames = append(filteredFunctionNames, name)
}
templateContent := &pluginTemplateContent{
Version: version.VERSION,
FunctionNames: filteredFunctionNames,
} }
pluginDir := filepath.Dir(path) pluginDir := filepath.Dir(path)
err = generate(templateContent, goTemplate, filepath.Join(pluginDir, PluginGoSourceGenFile))
if err != nil {
return errors.Wrap(err, "generate hashicorp plugin failed")
}
// check go sdk in tempDir // check go sdk in tempDir
if err := builtin.ExecCommandInDir(exec.Command("go", "version"), pluginDir); err != nil { if err := builtin.ExecCommandInDir(exec.Command("go", "version"), pluginDir); err != nil {
return errors.Wrap(err, "go sdk not installed") return errors.Wrap(err, "go sdk not installed")
} }
// parse debugtalk.go in pluginDir
err := templateContent.parseGoContent(path)
if err != nil {
return err
}
// generate debugtalk.go in pluginDir
err = templateContent.genDebugTalk(filepath.Join(pluginDir, PluginGoSourceGenFile), goTemplate)
if err != nil {
return err
}
if !builtin.IsFilePathExists(filepath.Join(pluginDir, "go.mod")) { if !builtin.IsFilePathExists(filepath.Join(pluginDir, "go.mod")) {
// create go mod // create go mod
if err := builtin.ExecCommandInDir(exec.Command("go", "mod", "init", "main"), pluginDir); err != nil { if err := builtin.ExecCommandInDir(exec.Command("go", "mod", "init", "main"), pluginDir); err != nil {
@@ -220,64 +120,66 @@ func buildGo(path string, output string) error {
// funplugin version should be locked // funplugin version should be locked
funplugin := fmt.Sprintf("github.com/httprunner/funplugin@%s", shared.Version) funplugin := fmt.Sprintf("github.com/httprunner/funplugin@%s", shared.Version)
if err := builtin.ExecCommandInDir(exec.Command("go", "get", funplugin), pluginDir); err != nil { if err := builtin.ExecCommandInDir(exec.Command("go", "get", funplugin), pluginDir); err != nil {
return err return errors.Wrap(err, "go get funplugin failed")
} }
} }
// add missing and remove unused modules // add missing and remove unused modules
if err := builtin.ExecCommandInDir(exec.Command("go", "mod", "tidy"), pluginDir); err != nil { if err := builtin.ExecCommandInDir(exec.Command("go", "mod", "tidy"), pluginDir); err != nil {
return err return errors.Wrap(err, "go mod tidy failed")
} }
// specify output file path
if output == "" { if output == "" {
dir, _ := os.Getwd() dir, _ := os.Getwd()
output = filepath.Join(dir, PluginHashicorpGoBuiltFile) output = filepath.Join(dir, PluginHashicorpGoBuiltFile)
} else if builtin.IsFolderPathExists(output) { } else if builtin.IsFolderPathExists(output) {
output = filepath.Join(output, PluginHashicorpGoBuiltFile) output = filepath.Join(output, PluginHashicorpGoBuiltFile)
} }
outputPath, err := filepath.Abs(output) outputPath, _ := filepath.Abs(output)
if err != nil {
return err
}
// build plugin debugtalk.bin // build go plugin to debugtalk.bin
cmd := exec.Command("go", "build", "-o", outputPath, PluginGoSourceGenFile, filepath.Base(path)) cmd := exec.Command("go", "build", "-o", outputPath, PluginGoSourceGenFile, filepath.Base(path))
if err := builtin.ExecCommandInDir(cmd, pluginDir); err != nil { if err := builtin.ExecCommandInDir(cmd, pluginDir); err != nil {
return err return errors.Wrap(err, "go build plugin failed")
} }
log.Info().Str("output", outputPath).Str("plugin", path).Msg("build plugin successfully") log.Info().Str("output", outputPath).Str("plugin", path).Msg("build go plugin successfully")
return nil return nil
} }
// buildPy completes funppy information in debugtalk.py // buildPy completes funppy information in debugtalk.py
func buildPy(path string, output string) error { func buildPy(path string, output string) error {
templateContent := &TemplateContent{
Version: version.VERSION,
Regexps: &Regexps{
FunctionName: regexp.MustCompile(regexPythonFunctionName),
},
}
err := templateContent.parsePyContent(path)
if err != nil {
return err
}
// check the syntax of debugtalk.py // check the syntax of debugtalk.py
err = builtin.CheckPythonScriptSyntax(path) err := builtin.ExecCommand("python3", "-m", "py_compile", path)
if err != nil {
return errors.Wrap(err, "python plugin syntax invalid")
}
functionNames, err := findAllFunctionNames(regexPyFunctionName, path)
if err != nil { if err != nil {
return err return err
} }
templateContent := &pluginTemplateContent{
Version: version.VERSION,
FunctionNames: functionNames,
}
// generate .debugtalk_gen.py // specify output file path
if output == "" { if output == "" {
dir, _ := os.Getwd() dir, _ := os.Getwd()
output = filepath.Join(dir, PluginPySourceGenFile) output = filepath.Join(dir, PluginPySourceGenFile)
} else if builtin.IsFolderPathExists(output) { } else if builtin.IsFolderPathExists(output) {
output = filepath.Join(output, PluginPySourceGenFile) output = filepath.Join(output, PluginPySourceGenFile)
} }
err = templateContent.genDebugTalk(output, pyTemplate)
return err // generate .debugtalk_gen.py
err = generate(templateContent, pyTemplate, output)
if err != nil {
return err
}
log.Info().Str("output", output).Str("plugin", path).Msg("build python plugin successfully")
return nil
} }
func BuildPlugin(path string, output string) (err error) { func BuildPlugin(path string, output string) (err error) {
@@ -291,7 +193,7 @@ func BuildPlugin(path string, output string) (err error) {
return errors.New("type error, expected .py or .go") return errors.New("type error, expected .py or .go")
} }
if err != nil { if err != nil {
log.Error().Err(err).Str("arg", path).Msg("build plugin failed") log.Error().Err(err).Str("path", path).Msg("build plugin failed")
os.Exit(1) os.Exit(1)
} }
return nil return nil
-4
View File
@@ -185,10 +185,6 @@ func InstallPythonPackage(python3 string, pkg string) (err error) {
return nil return nil
} }
func CheckPythonScriptSyntax(path string) error {
return ExecCommand("python3", "-m", "py_compile", path)
}
func ExecCommandInDir(cmd *exec.Cmd, dir string) error { func ExecCommandInDir(cmd *exec.Cmd, dir string) error {
log.Info().Str("cmd", cmd.String()).Str("dir", dir).Msg("exec command") log.Info().Str("cmd", cmd.String()).Str("dir", dir).Msg("exec command")
cmd.Dir = dir cmd.Dir = dir
@@ -1,67 +1,15 @@
# NOTE: Generated By hrp v4.1.2, DO NOT EDIT! # NOTE: Generated By hrp v4.1.2, DO NOT EDIT!
import logging import sys
import time import os
import funppy
from typing import List sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from debugtalk import *
def get_user_agent():
return "hrp/funppy"
def sleep(n_secs):
time.sleep(n_secs)
def sum(*args):
result = 0
for arg in args:
result += arg
return result
def sum_ints(*args: List[int]) -> int:
result = 0
for arg in args:
result += arg
return result
def sum_two_int(a: int, b: int) -> int:
return a + b
def sum_two_string(a: str, b: str) -> str:
return a + b
def sum_strings(*args: List[str]) -> str:
result = ""
for arg in args:
result += arg
return result
def concatenate(*args: List[str]) -> str:
result = ""
for arg in args:
result += str(arg)
return result
def setup_hook_example(name):
logging.warning("setup_hook_example")
return f"setup_hook_example: {name}"
def teardown_hook_example(name):
logging.warning("teardown_hook_example")
return f"teardown_hook_example: {name}"
if __name__ == "__main__": if __name__ == "__main__":
import funppy
funppy.register("get_user_agent", get_user_agent) funppy.register("get_user_agent", get_user_agent)
funppy.register("sleep", sleep) funppy.register("sleep", sleep)
funppy.register("sum", sum) funppy.register("sum", sum)
@@ -1,15 +1,12 @@
# NOTE: Generated By hrp {{ .Version }}, DO NOT EDIT! # NOTE: Generated By hrp {{ .Version }}, DO NOT EDIT!
{{ range $import := .Imports }} import sys
{{- $import}} import os
{{ end }}
{{ range $fromImport := .FromImports }} sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
{{- $fromImport}}
{{ end }} from debugtalk import *
{{ range $function := .Functions }}
{{- $function }}
{{ end }}
if __name__ == "__main__": if __name__ == "__main__":
import funppy import funppy
@@ -1,4 +1,4 @@
// NOTE: Generated By hrp v4.1.3, DO NOT EDIT! // NOTE: Generated By hrp v4.1.2, DO NOT EDIT!
package main package main
import ( import (