feat: add build command for plugin

This commit is contained in:
xucong053
2022-05-20 16:53:09 +08:00
parent c847511c96
commit 67c0cb8640
7 changed files with 499 additions and 0 deletions

26
hrp/cmd/build.go Normal file
View File

@@ -0,0 +1,26 @@
package cmd
import (
"github.com/spf13/cobra"
"github.com/httprunner/httprunner/v4/hrp/internal/build"
)
var buildCmd = &cobra.Command{
Use: "build $path ...",
Short: "build plugin for testing",
Long: `build python/go plugin for testing`,
Example: ` $ hrp build plugin/debugtalk.go
$ hrp build plugin/debugtalk.py`,
Args: cobra.MinimumNArgs(1),
PreRun: func(cmd *cobra.Command, args []string) {
setLogLevel(logLevel)
},
RunE: func(cmd *cobra.Command, args []string) error {
return build.Run(args)
},
}
func init() {
rootCmd.AddCommand(buildCmd)
}

View File

@@ -0,0 +1,40 @@
package examples
import (
"fmt"
)
func SumTwoInt(a, b int) int {
return a + b
}
func SumInts(args ...int) int {
var sum int
for _, arg := range args {
sum += arg
}
return sum
}
func Sum(args ...interface{}) (interface{}, error) {
var sum float64
for _, arg := range args {
switch v := arg.(type) {
case int:
sum += float64(v)
case float64:
sum += v
default:
return nil, fmt.Errorf("unexpected type: %T", arg)
}
}
return sum, nil
}
func SetupHookExample(args string) string {
return fmt.Sprintf("step name: %v, setup...", args)
}
func TeardownHookExample(args string) string {
return fmt.Sprintf("step name: %v, teardown...", args)
}

View File

@@ -0,0 +1,53 @@
import logging
import time
from typing import List
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}"

304
hrp/internal/build/main.go Normal file
View File

@@ -0,0 +1,304 @@
package build
import (
"bufio"
_ "embed"
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"text/template"
"github.com/httprunner/funplugin/shared"
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
)
const (
funppy = `import funppy`
fungo = `"github.com/httprunner/funplugin/fungo"`
regexPythonFunctionName = `def ([a-zA-Z_]\w*)\(.*\)`
regexGoImport = `import\s*\(\n([\s\S]*)\n\)`
regexGoFunctionName = `func ([a-zA-Z_]\w*)\(.*\)`
regexGoFunctionContent = `func [\s\S]*?\n}\n`
)
//go:embed templates/debugtalkPythonTemplate
var pyTemplate string
//go:embed templates/debugtalkGoTemplate
var goTemplate string
type TemplateContent struct {
Fun string // funplugin package
Regexps *Regexps // match import/function
Imports []string // python/go import
FromImports []string // python from...import...
Functions []string // python/go function
FunctionNames []string // function name set by user
FunctionSnakeNames []string // function snake name converts by function name for registering plugin
}
type Regexps struct {
Import *regexp.Regexp
FunctionName *regexp.Regexp
FunctionContent *regexp.Regexp // including function define and body
}
func (t *TemplateContent) parseGoContent(path string) error {
log.Info().Msg(fmt.Sprintf("start to parse %v", path))
content, err := os.ReadFile(path)
if err != nil {
log.Error().Err(err).Msg("failed to read file")
return err
}
originalContent := string(content)
// parse imports
importSlice := t.Regexps.Import.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, elem)
}
} else {
if strings.Contains(originalContent, "\nimport ") {
return errors.New(`import style error, expected import ( ... )`)
}
}
// import fungo package
if !builtin.Contains(t.Imports, fungo) {
t.Imports = append(t.Imports, t.Fun)
}
// parse function name
functionNameSlice := t.Regexps.FunctionName.FindAllStringSubmatch(originalContent, -1)
for _, elem := range functionNameSlice {
name := strings.Trim(elem[1], " ")
if name == "main" {
continue
}
t.FunctionNames = append(t.FunctionNames, name)
t.FunctionSnakeNames = append(t.FunctionSnakeNames, convertSnakeName(name))
}
// parse function content
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 {
file, err := os.Open(path)
if err != nil {
fmt.Printf("Error: %s\n", err)
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, " "))
} else if strings.HasPrefix(line, "from") {
t.FromImports = append(t.FromImports, strings.Trim(line, " "))
} 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])
t.FunctionSnakeNames = append(t.FunctionSnakeNames, convertSnakeName(functionNameSlice[0][1]))
}
content += line + "\n"
}
}
// function content
t.Functions = append(t.Functions, strings.Trim(content, "\n"))
// import funppy
if !builtin.Contains(t.Imports, t.Fun) {
t.Imports = append(t.Imports, t.Fun)
}
return nil
}
func (t *TemplateContent) genDebugTalk(path string, templ string) error {
file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0o666)
if err != nil {
log.Error().Err(err).Msg("open file failed")
return err
}
defer file.Close()
writer := bufio.NewWriter(file)
tmpl := template.Must(template.New("debugtalk").Parse(templ))
err = tmpl.Execute(writer, t)
if err != nil {
log.Error().Err(err).Msg("execute applies a parsed template to the specified data object failed")
return err
}
err = writer.Flush()
if err == nil {
log.Info().Str("path", path).Msg("generate debugtalk success")
} else {
log.Error().Str("path", path).Msg("generate debugtalk failed")
}
return err
}
// buildGo builds debugtalk.go to debugtalk.bin
func buildGo(path string) error {
templateContent := &TemplateContent{
Fun: fungo,
Regexps: &Regexps{
Import: regexp.MustCompile(regexGoImport),
FunctionName: regexp.MustCompile(regexGoFunctionName),
FunctionContent: regexp.MustCompile(regexGoFunctionContent),
},
}
dir, _ := filepath.Split(path)
// create temp dir for building
tempDir, err := ioutil.TempDir("", "hrp_build")
if err != nil {
return err
}
// check go sdk in tempDir
if err := builtin.ExecCommandInDir(exec.Command("go", "version"), tempDir); err != nil {
return errors.Wrap(err, "go sdk not installed")
}
// create pluginDir
pluginDir := filepath.Join(tempDir, "plugin")
if err := builtin.CreateFolder(pluginDir); err != nil {
return err
}
// 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, "debugtalk.go"), goTemplate)
if err != nil {
return err
}
// create go mod
if err := builtin.ExecCommandInDir(exec.Command("go", "mod", "init", "plugin"), pluginDir); err != nil {
return err
}
// download plugin dependency
// funplugin version should be locked
funplugin := fmt.Sprintf("github.com/httprunner/funplugin@%s", shared.Version)
if err := builtin.ExecCommandInDir(exec.Command("go", "get", funplugin), pluginDir); err != nil {
return err
}
outputPath, err := filepath.Abs(filepath.Join(dir, "../debugtalk.bin"))
if err != nil {
return err
}
// build plugin debugtalk.bin
if err := builtin.ExecCommandInDir(exec.Command("go", "build", "-o", outputPath, "debugtalk.go"), pluginDir); err != nil {
return err
}
log.Info().Msg(fmt.Sprintf("build %s to %s successfully", path, outputPath))
return nil
}
// buildPy completes funppy information in debugtalk.py
func buildPy(path string) error {
templateContent := &TemplateContent{
Fun: funppy,
Regexps: &Regexps{
FunctionName: regexp.MustCompile(regexPythonFunctionName),
},
}
err := templateContent.parsePyContent(path)
if err != nil {
return err
}
// generate debugtalk.py
dir, _ := filepath.Split(path)
err = templateContent.genDebugTalk(filepath.Join(dir, "../debugtalk.py"), pyTemplate)
if err != nil {
return err
}
// ensure funppy in .env
_, err = builtin.EnsurePython3Venv("funppy")
if err != nil {
return err
}
return nil
}
func Run(args []string) (err error) {
for _, arg := range args {
ext := filepath.Ext(arg)
switch ext {
case ".py":
err = buildPy(arg)
case ".go":
err = buildGo(arg)
default:
return errors.New("type error, expected .py or .go")
}
if err != nil {
log.Error().Err(err).Msg(fmt.Sprintf("failed to build %s", arg))
os.Exit(1)
}
}
return nil
}
// convertSnakeName converts name to snake name
func convertSnakeName(originalName string) string {
snakeName := make([]byte, 0, len(originalName)*2)
flag := false
num := len(originalName)
for i := 0; i < num; i++ {
ch := originalName[i]
if i > 0 && ch >= 'A' && ch <= 'Z' && flag {
snakeName = append(snakeName, '_')
}
if ch != '_' {
flag = true
}
snakeName = append(snakeName, ch)
}
return strings.ToLower(string(snakeName))
}

View File

@@ -0,0 +1,44 @@
package build
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestRun(t *testing.T) {
err := Run([]string{"examples/debugtalk_no_funppy.py", "examples/debugtalk_no_fungo.go"})
if !assert.Nil(t, err) {
t.Fatal()
}
}
func TestConvertSnakeName(t *testing.T) {
testData := []struct {
expectedValue string
originalValue string
}{
{
expectedValue: "test_name",
originalValue: "testName",
},
{
expectedValue: "test",
originalValue: "test",
},
{
expectedValue: "test_name",
originalValue: "TestName",
},
{
expectedValue: "test_name",
originalValue: "test_name",
},
}
for _, data := range testData {
name := convertSnakeName(data.originalValue)
if !assert.Equal(t, data.expectedValue, name) {
t.Fatal()
}
}
}

View File

@@ -0,0 +1,17 @@
package main
import (
{{- range $import := .Imports }}
{{ $import -}}
{{ end }}
)
{{ range $function := .Functions }}
{{ $function }}
{{ end }}
func main() {
{{- range $idx, $name := .FunctionNames }}
fungo.Register("{{ index $.FunctionSnakeNames $idx }}", {{ $name }})
{{- end }}
fungo.Serve()
}

View File

@@ -0,0 +1,15 @@
{{- range $import := .Imports }}
{{- $import}}
{{ end }}
{{ range $fromImport := .FromImports }}
{{- $fromImport}}
{{ end }}
{{ range $function := .Functions }}
{{ $function }}
{{ end }}
if __name__ == "__main__":
{{- range $mainRegSnake := .FunctionSnakeNames }}
funppy.register("{{ $mainRegSnake }}", {{ $mainRegSnake }})
{{- end }}
funppy.serve()