diff --git a/.github/workflows/hrp-scaffold.yml b/.github/workflows/hrp-scaffold.yml index 81116817..62b66f1c 100644 --- a/.github/workflows/hrp-scaffold.yml +++ b/.github/workflows/hrp-scaffold.yml @@ -26,7 +26,7 @@ jobs: - name: Run start project run: ./output/hrp startproject demo - name: Run demo tests - run: ./output/hrp run demo/testcases/demo.json demo/testcases/demo.yaml + run: ./output/hrp run demo/testcases/demo_with_funplugin.json demo/testcases/demo_requests.yml demo/testcases/demo_ref_testcase.yml scaffold-with-go-plugin: strategy: @@ -48,7 +48,7 @@ jobs: - name: Run start project run: ./output/hrp startproject demo --go - name: Run demo tests - run: ./output/hrp run demo/testcases/demo.json demo/testcases/demo.yaml + run: ./output/hrp run demo/testcases/demo_with_funplugin.json demo/testcases/demo_requests.yml demo/testcases/demo_ref_testcase.yml scaffold-without-custom-plugin: strategy: @@ -70,4 +70,4 @@ jobs: - name: Run start project run: ./output/hrp startproject demo --ignore-plugin - name: Run demo tests - run: ./output/hrp run demo/testcases/demo.json demo/testcases/demo.yaml + run: ./output/hrp run demo/testcases/demo_without_plugin.json diff --git a/.github/workflows/smoketest.yml b/.github/workflows/smoketest.yml index a11870b6..a638edfd 100644 --- a/.github/workflows/smoketest.yml +++ b/.github/workflows/smoketest.yml @@ -37,7 +37,6 @@ jobs: run: | poetry run hrun -V poetry run httprunner run -h - poetry run httprunner startproject -h - name: Run smoketest - postman echo run: | poetry run hrun examples/postman_echo/request_methods diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 37ac2752..e38b7a3c 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -12,6 +12,7 @@ **python version** +- change: remove startproject, move all features to go version, replace with `hrp startproject` - change: remove har2case, move all features to go version, replace with `hrp run` - change: remove locust, you should run load tests with go version, replace with `hrp boom` - change: remove fastapi and uvicorn dependencies diff --git a/hrp/boomer_test.go b/hrp/boomer_test.go index 79ffd2fe..4edefa38 100644 --- a/hrp/boomer_test.go +++ b/hrp/boomer_test.go @@ -6,8 +6,8 @@ import ( ) func TestBoomerStandaloneRun(t *testing.T) { - buildHashicorpPlugin() - defer removeHashicorpPlugin() + buildHashicorpGoPlugin() + defer removeHashicorpGoPlugin() testcase1 := &TestCase{ Config: NewConfig("TestCase1").SetBaseURL("http://httpbin.org"), @@ -25,7 +25,7 @@ func TestBoomerStandaloneRun(t *testing.T) { NewStep("TestCase3").CallRefCase(&TestCase{Config: NewConfig("TestCase3")}), }, } - testcase2 := &demoTestCaseJSONPath + testcase2 := &demoTestCaseWithPluginJSONPath b := NewBoomer(2, 1) go b.Run(testcase1, testcase2) diff --git a/hrp/cmd/scaffold.go b/hrp/cmd/scaffold.go index cc7f18a0..0d065839 100644 --- a/hrp/cmd/scaffold.go +++ b/hrp/cmd/scaffold.go @@ -30,6 +30,7 @@ var scaffoldCmd = &cobra.Command{ } else { pluginType = scaffold.Py // default } + err := scaffold.CreateScaffold(args[0], pluginType) if err != nil { log.Error().Err(err).Msg("create scaffold project failed") diff --git a/hrp/convert_test.go b/hrp/convert_test.go index 7c922d7e..351aedf0 100644 --- a/hrp/convert_test.go +++ b/hrp/convert_test.go @@ -8,23 +8,198 @@ import ( "github.com/httprunner/httprunner/hrp/internal/builtin" ) +const templatesDir = "internal/scaffold/templates/" + +var ( + demoTestCaseWithPluginJSONPath TestCasePath = templatesDir + "testcases/demo_with_funplugin.json" + demoTestCaseWithPluginYAMLPath TestCasePath = templatesDir + "testcases/demo_with_funplugin.yaml" + demoTestCaseWithoutPluginJSONPath TestCasePath = templatesDir + "testcases/demo_without_funplugin.json" + demoTestCaseWithoutPluginYAMLPath TestCasePath = templatesDir + "testcases/demo_without_funplugin.yaml" +) + var ( - demoTestCaseJSONPath TestCasePath = "../examples/hrp/demo.json" - demoTestCaseYAMLPath TestCasePath = "../examples/hrp/demo.yaml" demoRefAPIYAMLPath TestCasePath = "../examples/hrp/ref_api_test.yaml" demoRefTestCaseJSONPath TestCasePath = "../examples/hrp/ref_testcase_test.json" demoThinkTimeJsonPath TestCasePath = "../examples/hrp/think_time_test.json" demoAPIYAMLPath APIPath = "../examples/hrp/api/put.yml" ) +var demoTestCaseWithPlugin = &TestCase{ + Config: NewConfig("demo with complex mechanisms"). + SetBaseURL("https://postman-echo.com"). + WithVariables(map[string]interface{}{ // global level variables + "n": "${sum_ints(1, 2, 2)}", + "a": "${sum(10, 2.3)}", + "b": 3.45, + "varFoo1": "${gen_random_string($n)}", + "varFoo2": "${max($a, $b)}", // 12.3; eval with built-in function + }), + TestSteps: []IStep{ + NewStep("transaction 1 start").StartTransaction("tran1"), // start transaction + NewStep("get with params"). + WithVariables(map[string]interface{}{ // step level variables + "n": 3, // inherit config level variables if not set in step level, a/varFoo1 + "b": 34.5, // override config level variable if existed, n/b/varFoo2 + "varFoo2": "${max($a, $b)}", // 34.5; override variable b and eval again + "name": "get with params", + }). + SetupHook("${setup_hook_example($name)}"). + GET("/get"). + TeardownHook("${teardown_hook_example($name)}"). + WithParams(map[string]interface{}{"foo1": "$varFoo1", "foo2": "$varFoo2"}). // request with params + WithHeaders(map[string]string{"User-Agent": "HttpRunnerPlus"}). // request with headers + Extract(). + WithJmesPath("body.args.foo1", "varFoo1"). // extract variable with jmespath + Validate(). + AssertEqual("status_code", 200, "check response status code"). // validate response status code + AssertStartsWith("headers.\"Content-Type\"", "application/json", ""). // validate response header + AssertLengthEqual("body.args.foo1", 5, "check args foo1"). // validate response body with jmespath + AssertLengthEqual("$varFoo1", 5, "check args foo1"). // assert with extracted variable from current step + AssertEqual("body.args.foo2", "34.5", "check args foo2"), // notice: request params value will be converted to string + NewStep("transaction 1 end").EndTransaction("tran1"), // end transaction + NewStep("post json data"). + POST("/post"). + WithBody(map[string]interface{}{ + "foo1": "$varFoo1", // reference former extracted variable + "foo2": "${max($a, $b)}", // 12.3; step level variables are independent, variable b is 3.45 here + }). + Validate(). + AssertEqual("status_code", 200, "check status code"). + AssertLengthEqual("body.json.foo1", 5, "check args foo1"). + AssertEqual("body.json.foo2", 12.3, "check args foo2"), + NewStep("post form data"). + POST("/post"). + WithHeaders(map[string]string{"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"}). + WithBody(map[string]interface{}{ + "foo1": "$varFoo1", // reference former extracted variable + "foo2": "${max($a, $b)}", // 12.3; step level variables are independent, variable b is 3.45 here + "time": "${get_timestamp()}", + }). + Extract(). + WithJmesPath("body.form.time", "varTime"). + Validate(). + AssertEqual("status_code", 200, "check status code"). + AssertLengthEqual("body.form.foo1", 5, "check args foo1"). + AssertEqual("body.form.foo2", "12.3", "check args foo2"), // form data will be converted to string + NewStep("get with timestamp"). + GET("/get").WithParams(map[string]interface{}{"time": "$varTime"}). + Validate(). + AssertLengthEqual("body.args.time", 13, "check extracted var timestamp"), + }, +} + +var demoTestCaseWithoutPlugin = &TestCase{ + Config: NewConfig("demo without custom function plugin"). + SetBaseURL("https://postman-echo.com"). + WithVariables(map[string]interface{}{ // global level variables + "n": 5, + "a": 12.3, + "b": 3.45, + "varFoo1": "${gen_random_string($n)}", + "varFoo2": "${max($a, $b)}", // 12.3; eval with built-in function + }), + TestSteps: []IStep{ + NewStep("transaction 1 start").StartTransaction("tran1"), // start transaction + NewStep("get with params"). + WithVariables(map[string]interface{}{ // step level variables + "n": 3, // inherit config level variables if not set in step level, a/varFoo1 + "b": 34.5, // override config level variable if existed, n/b/varFoo2 + "varFoo2": "${max($a, $b)}", // 34.5; override variable b and eval again + "name": "get with params", + }). + GET("/get"). + WithParams(map[string]interface{}{"foo1": "$varFoo1", "foo2": "$varFoo2"}). // request with params + WithHeaders(map[string]string{"User-Agent": "HttpRunnerPlus"}). // request with headers + Extract(). + WithJmesPath("body.args.foo1", "varFoo1"). // extract variable with jmespath + Validate(). + AssertEqual("status_code", 200, "check response status code"). // validate response status code + AssertStartsWith("headers.\"Content-Type\"", "application/json", ""). // validate response header + AssertLengthEqual("body.args.foo1", 5, "check args foo1"). // validate response body with jmespath + AssertLengthEqual("$varFoo1", 5, "check args foo1"). // assert with extracted variable from current step + AssertEqual("body.args.foo2", "34.5", "check args foo2"), // notice: request params value will be converted to string + NewStep("transaction 1 end").EndTransaction("tran1"), // end transaction + NewStep("post json data"). + POST("/post"). + WithBody(map[string]interface{}{ + "foo1": "$varFoo1", // reference former extracted variable + "foo2": "${max($a, $b)}", // 12.3; step level variables are independent, variable b is 3.45 here + }). + Validate(). + AssertEqual("status_code", 200, "check status code"). + AssertLengthEqual("body.json.foo1", 5, "check args foo1"). + AssertEqual("body.json.foo2", 12.3, "check args foo2"), + NewStep("post form data"). + POST("/post"). + WithHeaders(map[string]string{"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"}). + WithBody(map[string]interface{}{ + "foo1": "$varFoo1", // reference former extracted variable + "foo2": "${max($a, $b)}", // 12.3; step level variables are independent, variable b is 3.45 here + "time": "${get_timestamp()}", + }). + Extract(). + WithJmesPath("body.form.time", "varTime"). + Validate(). + AssertEqual("status_code", 200, "check status code"). + AssertLengthEqual("body.form.foo1", 5, "check args foo1"). + AssertEqual("body.form.foo2", "12.3", "check args foo2"), // form data will be converted to string + NewStep("get with timestamp"). + GET("/get").WithParams(map[string]interface{}{"time": "$varTime"}). + Validate(). + AssertLengthEqual("body.args.time", 13, "check extracted var timestamp"), + }, +} + +func TestGenDemoTestCase(t *testing.T) { + tCase, _ := demoTestCaseWithPlugin.ToTCase() + err := builtin.Dump2JSON(tCase, demoTestCaseWithPluginJSONPath.ToString()) + if err != nil { + t.Fail() + } + err = builtin.Dump2YAML(tCase, demoTestCaseWithPluginYAMLPath.ToString()) + if err != nil { + t.Fail() + } + + tCase, _ = demoTestCaseWithoutPlugin.ToTCase() + err = builtin.Dump2JSON(tCase, demoTestCaseWithoutPluginJSONPath.ToString()) + if err != nil { + t.Fail() + } + err = builtin.Dump2YAML(tCase, demoTestCaseWithoutPluginYAMLPath.ToString()) + if err != nil { + t.Fail() + } +} + +func TestJsonDemoWithPlugin(t *testing.T) { + buildHashicorpGoPlugin() + defer removeHashicorpGoPlugin() + + err := NewRunner(nil).Run(&demoTestCaseWithPluginJSONPath) // hrp.Run(testCase) + if err != nil { + t.Fail() + } +} + +func TestYamlDemoWithPlugin(t *testing.T) { + buildHashicorpGoPlugin() + defer removeHashicorpGoPlugin() + + err := NewRunner(nil).Run(&demoTestCaseWithPluginYAMLPath) // hrp.Run(testCase) + if err != nil { + t.Fail() + } +} + func TestLoadCase(t *testing.T) { tcJSON := &TCase{} tcYAML := &TCase{} - err := builtin.LoadFile(demoTestCaseJSONPath.ToString(), tcJSON) + err := builtin.LoadFile(demoTestCaseWithPluginJSONPath.ToString(), tcJSON) if !assert.NoError(t, err) { t.Fail() } - err = builtin.LoadFile(demoTestCaseYAMLPath.ToString(), tcYAML) + err = builtin.LoadFile(demoTestCaseWithPluginYAMLPath.ToString(), tcYAML) if !assert.NoError(t, err) { t.Fail() } diff --git a/hrp/internal/builtin/function.go b/hrp/internal/builtin/function.go index e271b447..7ce36eb7 100644 --- a/hrp/internal/builtin/function.go +++ b/hrp/internal/builtin/function.go @@ -1,26 +1,11 @@ package builtin import ( - "bytes" "crypto/md5" - "encoding/csv" "encoding/hex" - builtinJSON "encoding/json" - "fmt" "math" "math/rand" - "os" - "os/exec" - "path/filepath" - "strconv" - "strings" "time" - - "github.com/pkg/errors" - "github.com/rs/zerolog/log" - "gopkg.in/yaml.v3" - - "github.com/httprunner/httprunner/hrp/internal/json" ) var Functions = map[string]interface{}{ @@ -61,235 +46,3 @@ func MD5(str string) string { hasher.Write([]byte(str)) return hex.EncodeToString(hasher.Sum(nil)) } - -func Dump2JSON(data interface{}, path string) error { - path, err := filepath.Abs(path) - if err != nil { - log.Error().Err(err).Msg("convert absolute path failed") - return err - } - log.Info().Str("path", path).Msg("dump data to json") - file, _ := json.MarshalIndent(data, "", " ") - err = os.WriteFile(path, file, 0644) - if err != nil { - log.Error().Err(err).Msg("dump json path failed") - return err - } - return nil -} - -func Dump2YAML(data interface{}, path string) error { - path, err := filepath.Abs(path) - if err != nil { - log.Error().Err(err).Msg("convert absolute path failed") - return err - } - log.Info().Str("path", path).Msg("dump data to yaml") - - // init yaml encoder - buffer := new(bytes.Buffer) - encoder := yaml.NewEncoder(buffer) - encoder.SetIndent(4) - - // encode - err = encoder.Encode(data) - if err != nil { - return err - } - - err = os.WriteFile(path, buffer.Bytes(), 0644) - if err != nil { - log.Error().Err(err).Msg("dump yaml path failed") - return err - } - return nil -} - -func FormatResponse(raw interface{}) interface{} { - formattedResponse := make(map[string]interface{}) - for key, value := range raw.(map[string]interface{}) { - // convert value to json - if key == "body" { - b, _ := json.MarshalIndent(&value, "", " ") - value = string(b) - } - formattedResponse[key] = value - } - return formattedResponse -} - -func ExecCommand(cmd *exec.Cmd, cwd string) error { - log.Info().Str("cmd", cmd.String()).Str("cwd", cwd).Msg("exec command") - cmd.Dir = cwd - output, err := cmd.CombinedOutput() - out := strings.TrimSpace(string(output)) - if err != nil { - log.Error().Err(err).Str("output", out).Msg("exec command failed") - } else if len(out) != 0 { - log.Info().Str("output", out).Msg("exec command success") - } - return err -} - -func CreateFolder(folderPath string) error { - log.Info().Str("path", folderPath).Msg("create folder") - err := os.MkdirAll(folderPath, os.ModePerm) - if err != nil { - log.Error().Err(err).Msg("create folder failed") - return err - } - return nil -} - -func CreateFile(filePath string, data string) error { - log.Info().Str("path", filePath).Msg("create file") - err := os.WriteFile(filePath, []byte(data), 0o644) - if err != nil { - log.Error().Err(err).Msg("create file failed") - return err - } - return nil -} - -// isFilePathExists returns true if path exists, whether path is file or dir -func isPathExists(path string) bool { - if _, err := os.Stat(path); os.IsNotExist(err) { - return false - } - return true -} - -// isFilePathExists returns true if path exists and path is file -func isFilePathExists(path string) bool { - info, err := os.Stat(path) - if err != nil { - // path not exists - return false - } - - // path exists - if info.IsDir() { - // path is dir, not file - return false - } - return true -} - -func EnsureFolderExists(folderPath string) error { - if !isPathExists(folderPath) { - err := CreateFolder(folderPath) - return err - } else if isFilePathExists(folderPath) { - return fmt.Errorf("path %v should be directory", folderPath) - } - return nil -} - -func Contains(s []string, e string) bool { - for _, a := range s { - if a == e { - return true - } - } - return false -} - -func GetRandomNumber(min, max int) int { - if min > max { - return 0 - } - r := rand.Intn(max - min + 1) - return min + r -} - -func Interface2Float64(i interface{}) (float64, error) { - switch i.(type) { - case int: - return float64(i.(int)), nil - case int32: - return float64(i.(int32)), nil - case int64: - return float64(i.(int64)), nil - case float32: - return float64(i.(float32)), nil - case float64: - return i.(float64), nil - case string: - intVar, err := strconv.Atoi(i.(string)) - if err != nil { - return 0, err - } - return float64(intVar), err - } - // json.Number - value, ok := i.(builtinJSON.Number) - if ok { - return value.Float64() - } - return 0, errors.New("failed to convert interface to float64") -} - -var ErrUnsupportedFileExt = fmt.Errorf("unsupported file extension") - -// LoadFile loads file content with file extension and assigns to structObj -func LoadFile(path string, structObj interface{}) (err error) { - log.Info().Str("path", path).Msg("load file") - file, err := readFile(path) - if err != nil { - return errors.Wrap(err, "read file failed") - } - - ext := filepath.Ext(path) - switch ext { - case ".json", ".har": - decoder := json.NewDecoder(bytes.NewReader(file)) - decoder.UseNumber() - err = decoder.Decode(structObj) - case ".yaml", ".yml": - err = yaml.Unmarshal(file, structObj) - default: - err = ErrUnsupportedFileExt - } - return err -} - -func loadFromCSV(path string) []map[string]interface{} { - log.Info().Str("path", path).Msg("load csv file") - file, err := readFile(path) - if err != nil { - log.Error().Err(err).Msg("read csv file failed") - panic(err) - } - - r := csv.NewReader(strings.NewReader(string(file))) - content, err := r.ReadAll() - if err != nil { - log.Error().Err(err).Msg("parse csv file failed") - panic(err) - } - var result []map[string]interface{} - for i := 1; i < len(content); i++ { - row := make(map[string]interface{}) - for j := 0; j < len(content[i]); j++ { - row[content[0][j]] = content[i][j] - } - result = append(result, row) - } - return result -} - -func readFile(path string) ([]byte, error) { - var err error - path, err = filepath.Abs(path) - if err != nil { - log.Error().Err(err).Str("path", path).Msg("convert absolute path failed") - return nil, err - } - - file, err := os.ReadFile(path) - if err != nil { - log.Error().Err(err).Msg("read file failed") - return nil, err - } - return file, nil -} diff --git a/hrp/internal/builtin/utils.go b/hrp/internal/builtin/utils.go new file mode 100644 index 00000000..7d9bd4a8 --- /dev/null +++ b/hrp/internal/builtin/utils.go @@ -0,0 +1,252 @@ +package builtin + +import ( + "bytes" + "encoding/csv" + builtinJSON "encoding/json" + "fmt" + "math/rand" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + "gopkg.in/yaml.v3" + + "github.com/httprunner/httprunner/hrp/internal/json" +) + +func Dump2JSON(data interface{}, path string) error { + path, err := filepath.Abs(path) + if err != nil { + log.Error().Err(err).Msg("convert absolute path failed") + return err + } + log.Info().Str("path", path).Msg("dump data to json") + file, _ := json.MarshalIndent(data, "", " ") + err = os.WriteFile(path, file, 0644) + if err != nil { + log.Error().Err(err).Msg("dump json path failed") + return err + } + return nil +} + +func Dump2YAML(data interface{}, path string) error { + path, err := filepath.Abs(path) + if err != nil { + log.Error().Err(err).Msg("convert absolute path failed") + return err + } + log.Info().Str("path", path).Msg("dump data to yaml") + + // init yaml encoder + buffer := new(bytes.Buffer) + encoder := yaml.NewEncoder(buffer) + encoder.SetIndent(4) + + // encode + err = encoder.Encode(data) + if err != nil { + return err + } + + err = os.WriteFile(path, buffer.Bytes(), 0644) + if err != nil { + log.Error().Err(err).Msg("dump yaml path failed") + return err + } + return nil +} + +func FormatResponse(raw interface{}) interface{} { + formattedResponse := make(map[string]interface{}) + for key, value := range raw.(map[string]interface{}) { + // convert value to json + if key == "body" { + b, _ := json.MarshalIndent(&value, "", " ") + value = string(b) + } + formattedResponse[key] = value + } + return formattedResponse +} + +func ExecCommand(cmd *exec.Cmd, cwd string) error { + log.Info().Str("cmd", cmd.String()).Str("cwd", cwd).Msg("exec command") + cmd.Dir = cwd + output, err := cmd.CombinedOutput() + out := strings.TrimSpace(string(output)) + if err != nil { + log.Error().Err(err).Str("output", out).Msg("exec command failed") + } else if len(out) != 0 { + log.Info().Str("output", out).Msg("exec command success") + } + return err +} + +func CreateFolder(folderPath string) error { + log.Info().Str("path", folderPath).Msg("create folder") + err := os.MkdirAll(folderPath, os.ModePerm) + if err != nil { + log.Error().Err(err).Msg("create folder failed") + return err + } + return nil +} + +func CreateFile(filePath string, data string) error { + log.Info().Str("path", filePath).Msg("create file") + err := os.WriteFile(filePath, []byte(data), 0o644) + if err != nil { + log.Error().Err(err).Msg("create file failed") + return err + } + return nil +} + +// isFilePathExists returns true if path exists, whether path is file or dir +func isPathExists(path string) bool { + if _, err := os.Stat(path); os.IsNotExist(err) { + return false + } + return true +} + +// isFilePathExists returns true if path exists and path is file +func isFilePathExists(path string) bool { + info, err := os.Stat(path) + if err != nil { + // path not exists + return false + } + + // path exists + if info.IsDir() { + // path is dir, not file + return false + } + return true +} + +func EnsureFolderExists(folderPath string) error { + if !isPathExists(folderPath) { + err := CreateFolder(folderPath) + return err + } else if isFilePathExists(folderPath) { + return fmt.Errorf("path %v should be directory", folderPath) + } + return nil +} + +func Contains(s []string, e string) bool { + for _, a := range s { + if a == e { + return true + } + } + return false +} + +func GetRandomNumber(min, max int) int { + if min > max { + return 0 + } + r := rand.Intn(max - min + 1) + return min + r +} + +func Interface2Float64(i interface{}) (float64, error) { + switch i.(type) { + case int: + return float64(i.(int)), nil + case int32: + return float64(i.(int32)), nil + case int64: + return float64(i.(int64)), nil + case float32: + return float64(i.(float32)), nil + case float64: + return i.(float64), nil + case string: + intVar, err := strconv.Atoi(i.(string)) + if err != nil { + return 0, err + } + return float64(intVar), err + } + // json.Number + value, ok := i.(builtinJSON.Number) + if ok { + return value.Float64() + } + return 0, errors.New("failed to convert interface to float64") +} + +var ErrUnsupportedFileExt = fmt.Errorf("unsupported file extension") + +// LoadFile loads file content with file extension and assigns to structObj +func LoadFile(path string, structObj interface{}) (err error) { + log.Info().Str("path", path).Msg("load file") + file, err := readFile(path) + if err != nil { + return errors.Wrap(err, "read file failed") + } + + ext := filepath.Ext(path) + switch ext { + case ".json", ".har": + decoder := json.NewDecoder(bytes.NewReader(file)) + decoder.UseNumber() + err = decoder.Decode(structObj) + case ".yaml", ".yml": + err = yaml.Unmarshal(file, structObj) + default: + err = ErrUnsupportedFileExt + } + return err +} + +func loadFromCSV(path string) []map[string]interface{} { + log.Info().Str("path", path).Msg("load csv file") + file, err := readFile(path) + if err != nil { + log.Error().Err(err).Msg("read csv file failed") + panic(err) + } + + r := csv.NewReader(strings.NewReader(string(file))) + content, err := r.ReadAll() + if err != nil { + log.Error().Err(err).Msg("parse csv file failed") + panic(err) + } + var result []map[string]interface{} + for i := 1; i < len(content); i++ { + row := make(map[string]interface{}) + for j := 0; j < len(content[i]); j++ { + row[content[0][j]] = content[i][j] + } + result = append(result, row) + } + return result +} + +func readFile(path string) ([]byte, error) { + var err error + path, err = filepath.Abs(path) + if err != nil { + log.Error().Err(err).Str("path", path).Msg("convert absolute path failed") + return nil, err + } + + file, err := os.ReadFile(path) + if err != nil { + log.Error().Err(err).Msg("read file failed") + return nil, err + } + return file, nil +} diff --git a/hrp/internal/scaffold/demo.go b/hrp/internal/scaffold/demo.go deleted file mode 100644 index 83338830..00000000 --- a/hrp/internal/scaffold/demo.go +++ /dev/null @@ -1,260 +0,0 @@ -package scaffold - -import "github.com/httprunner/httprunner/hrp" - -var demoTestCase = &hrp.TestCase{ - Config: hrp.NewConfig("demo with complex mechanisms"). - SetBaseURL("https://postman-echo.com"). - WithVariables(map[string]interface{}{ // global level variables - "n": "${sum_ints(1, 2, 2)}", - "a": "${sum(10, 2.3)}", - "b": 3.45, - "varFoo1": "${gen_random_string($n)}", - "varFoo2": "${max($a, $b)}", // 12.3; eval with built-in function - }), - TestSteps: []hrp.IStep{ - hrp.NewStep("transaction 1 start").StartTransaction("tran1"), // start transaction - hrp.NewStep("get with params"). - WithVariables(map[string]interface{}{ // step level variables - "n": 3, // inherit config level variables if not set in step level, a/varFoo1 - "b": 34.5, // override config level variable if existed, n/b/varFoo2 - "varFoo2": "${max($a, $b)}", // 34.5; override variable b and eval again - "name": "get with params", - }). - SetupHook("${setup_hook_example($name)}"). - GET("/get"). - TeardownHook("${teardown_hook_example($name)}"). - WithParams(map[string]interface{}{"foo1": "$varFoo1", "foo2": "$varFoo2"}). // request with params - WithHeaders(map[string]string{"User-Agent": "HttpRunnerPlus"}). // request with headers - Extract(). - WithJmesPath("body.args.foo1", "varFoo1"). // extract variable with jmespath - Validate(). - AssertEqual("status_code", 200, "check response status code"). // validate response status code - AssertStartsWith("headers.\"Content-Type\"", "application/json", ""). // validate response header - AssertLengthEqual("body.args.foo1", 5, "check args foo1"). // validate response body with jmespath - AssertLengthEqual("$varFoo1", 5, "check args foo1"). // assert with extracted variable from current step - AssertEqual("body.args.foo2", "34.5", "check args foo2"), // notice: request params value will be converted to string - hrp.NewStep("transaction 1 end").EndTransaction("tran1"), // end transaction - hrp.NewStep("post json data"). - POST("/post"). - WithBody(map[string]interface{}{ - "foo1": "$varFoo1", // reference former extracted variable - "foo2": "${max($a, $b)}", // 12.3; step level variables are independent, variable b is 3.45 here - }). - Validate(). - AssertEqual("status_code", 200, "check status code"). - AssertLengthEqual("body.json.foo1", 5, "check args foo1"). - AssertEqual("body.json.foo2", 12.3, "check args foo2"), - hrp.NewStep("post form data"). - POST("/post"). - WithHeaders(map[string]string{"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"}). - WithBody(map[string]interface{}{ - "foo1": "$varFoo1", // reference former extracted variable - "foo2": "${max($a, $b)}", // 12.3; step level variables are independent, variable b is 3.45 here - "time": "${get_timestamp()}", - }). - Extract(). - WithJmesPath("body.form.time", "varTime"). - Validate(). - AssertEqual("status_code", 200, "check status code"). - AssertLengthEqual("body.form.foo1", 5, "check args foo1"). - AssertEqual("body.form.foo2", "12.3", "check args foo2"), // form data will be converted to string - hrp.NewStep("get with timestamp"). - GET("/get").WithParams(map[string]interface{}{"time": "$varTime"}). - Validate(). - AssertLengthEqual("body.args.time", 13, "check extracted var timestamp"), - }, -} - -var demoTestCaseWithoutPlugin = &hrp.TestCase{ - Config: hrp.NewConfig("demo without custom function plugin"). - SetBaseURL("https://postman-echo.com"). - WithVariables(map[string]interface{}{ // global level variables - "n": 5, - "a": 12.3, - "b": 3.45, - "varFoo1": "${gen_random_string($n)}", - "varFoo2": "${max($a, $b)}", // 12.3; eval with built-in function - }), - TestSteps: []hrp.IStep{ - hrp.NewStep("transaction 1 start").StartTransaction("tran1"), // start transaction - hrp.NewStep("get with params"). - WithVariables(map[string]interface{}{ // step level variables - "n": 3, // inherit config level variables if not set in step level, a/varFoo1 - "b": 34.5, // override config level variable if existed, n/b/varFoo2 - "varFoo2": "${max($a, $b)}", // 34.5; override variable b and eval again - "name": "get with params", - }). - GET("/get"). - WithParams(map[string]interface{}{"foo1": "$varFoo1", "foo2": "$varFoo2"}). // request with params - WithHeaders(map[string]string{"User-Agent": "HttpRunnerPlus"}). // request with headers - Extract(). - WithJmesPath("body.args.foo1", "varFoo1"). // extract variable with jmespath - Validate(). - AssertEqual("status_code", 200, "check response status code"). // validate response status code - AssertStartsWith("headers.\"Content-Type\"", "application/json", ""). // validate response header - AssertLengthEqual("body.args.foo1", 5, "check args foo1"). // validate response body with jmespath - AssertLengthEqual("$varFoo1", 5, "check args foo1"). // assert with extracted variable from current step - AssertEqual("body.args.foo2", "34.5", "check args foo2"), // notice: request params value will be converted to string - hrp.NewStep("transaction 1 end").EndTransaction("tran1"), // end transaction - hrp.NewStep("post json data"). - POST("/post"). - WithBody(map[string]interface{}{ - "foo1": "$varFoo1", // reference former extracted variable - "foo2": "${max($a, $b)}", // 12.3; step level variables are independent, variable b is 3.45 here - }). - Validate(). - AssertEqual("status_code", 200, "check status code"). - AssertLengthEqual("body.json.foo1", 5, "check args foo1"). - AssertEqual("body.json.foo2", 12.3, "check args foo2"), - hrp.NewStep("post form data"). - POST("/post"). - WithHeaders(map[string]string{"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"}). - WithBody(map[string]interface{}{ - "foo1": "$varFoo1", // reference former extracted variable - "foo2": "${max($a, $b)}", // 12.3; step level variables are independent, variable b is 3.45 here - "time": "${get_timestamp()}", - }). - Extract(). - WithJmesPath("body.form.time", "varTime"). - Validate(). - AssertEqual("status_code", 200, "check status code"). - AssertLengthEqual("body.form.foo1", 5, "check args foo1"). - AssertEqual("body.form.foo2", "12.3", "check args foo2"), // form data will be converted to string - hrp.NewStep("get with timestamp"). - GET("/get").WithParams(map[string]interface{}{"time": "$varTime"}). - Validate(). - AssertLengthEqual("body.args.time", 13, "check extracted var timestamp"), - }, -} - -// debugtalk.go -var demoGoPlugin = `package main - -import ( - "fmt" - - "github.com/httprunner/funplugin/fungo" -) - -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) -} - -func main() { - fungo.Register("sum_ints", SumInts) - fungo.Register("sum_two_int", SumTwoInt) - fungo.Register("sum", Sum) - fungo.Register("setup_hook_example", SetupHookExample) - fungo.Register("teardown_hook_example", TeardownHookExample) - fungo.Serve() -} -` - -// debugtalk.py -var demoPyPlugin = `import logging -from typing import List - -import funppy - - -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__': - funppy.register("sum", sum) - funppy.register("sum_ints", sum_ints) - funppy.register("concatenate", concatenate) - funppy.register("sum_two_int", sum_two_int) - funppy.register("sum_two_string", sum_two_string) - funppy.register("sum_strings", sum_strings) - funppy.register("setup_hook_example", setup_hook_example) - funppy.register("teardown_hook_example", teardown_hook_example) - funppy.serve() -` - -// .gitignore -var demoIgnoreContent = `.env -reports/ -*.so -.vscode/ -.idea/ -.DS_Store -output/ - -# plugin -debugtalk.bin -debugtalk.so -` - -// .env -var demoEnvContent = `USERNAME=debugtalk -PASSWORD=123456 -` diff --git a/hrp/internal/scaffold/demo_test.go b/hrp/internal/scaffold/demo_test.go deleted file mode 100644 index 4a88ead3..00000000 --- a/hrp/internal/scaffold/demo_test.go +++ /dev/null @@ -1,75 +0,0 @@ -package scaffold - -import ( - "os" - "os/exec" - "testing" - - "github.com/rs/zerolog/log" - - "github.com/httprunner/httprunner/hrp" - "github.com/httprunner/httprunner/hrp/internal/builtin" -) - -var ( - demoTestCaseJSONPath hrp.TestCasePath = "../../../examples/hrp/demo.json" - demoTestCaseYAMLPath hrp.TestCasePath = "../../../examples/hrp/demo.yaml" -) - -func buildHashicorpPlugin() { - log.Info().Msg("[init] build hashicorp go plugin") - cmd := exec.Command("go", "build", - "-o", "../../../examples/hrp/debugtalk.bin", - "../../../examples/hrp/plugin/hashicorp.go", "../../../examples/hrp/plugin/debugtalk.go") - if err := cmd.Run(); err != nil { - panic(err) - } -} - -func removeHashicorpPlugin() { - log.Info().Msg("[teardown] remove hashicorp plugin") - os.Remove("../../../examples/hrp/debugtalk.bin") -} - -func TestGenDemoTestCase(t *testing.T) { - tCase, _ := demoTestCase.ToTCase() - err := builtin.Dump2JSON(tCase, demoTestCaseJSONPath.ToString()) - if err != nil { - t.Fail() - } - err = builtin.Dump2YAML(tCase, demoTestCaseYAMLPath.ToString()) - if err != nil { - t.Fail() - } -} - -func TestExampleDemo(t *testing.T) { - buildHashicorpPlugin() - defer removeHashicorpPlugin() - - demoTestCase.Config.Path = "../../../examples/hrp/debugtalk.bin" - err := hrp.NewRunner(nil).Run(demoTestCase) // hrp.Run(demoTestCase) - if err != nil { - t.Fail() - } -} - -func TestJsonDemo(t *testing.T) { - buildHashicorpPlugin() - defer removeHashicorpPlugin() - - err := hrp.NewRunner(nil).Run(&demoTestCaseJSONPath) // hrp.Run(testCase) - if err != nil { - t.Fail() - } -} - -func TestYamlDemo(t *testing.T) { - buildHashicorpPlugin() - defer removeHashicorpPlugin() - - err := hrp.NewRunner(nil).Run(&demoTestCaseYAMLPath) // hrp.Run(testCase) - if err != nil { - t.Fail() - } -} diff --git a/hrp/internal/scaffold/main.go b/hrp/internal/scaffold/main.go index c567f28e..37f819f1 100644 --- a/hrp/internal/scaffold/main.go +++ b/hrp/internal/scaffold/main.go @@ -1,16 +1,16 @@ package scaffold import ( + "embed" "fmt" "os" "os/exec" - "path" + "path/filepath" "github.com/pkg/errors" "github.com/rs/zerolog/log" "github.com/httprunner/funplugin/shared" - "github.com/httprunner/httprunner/hrp" "github.com/httprunner/httprunner/hrp/internal/builtin" "github.com/httprunner/httprunner/hrp/internal/sdk" ) @@ -23,6 +23,25 @@ const ( Go PluginType = "go" ) +//go:embed templates/* +var templatesDir embed.FS + +// CopyFile copies a file from templates dir to scaffold project +func CopyFile(templateFile, targetFile string) error { + log.Info().Str("path", targetFile).Msg("create file") + content, err := templatesDir.ReadFile(templateFile) + if err != nil { + return errors.Wrap(err, "template file not found") + } + + err = os.WriteFile(targetFile, content, 0o644) + if err != nil { + log.Error().Err(err).Msg("create file failed") + return err + } + return nil +} + func CreateScaffold(projectName string, pluginType PluginType) error { // report event sdk.SendEvent(sdk.EventTracking{ @@ -46,48 +65,56 @@ func CreateScaffold(projectName string, pluginType PluginType) error { if err := builtin.CreateFolder(projectName); err != nil { return err } - if err := builtin.CreateFolder(path.Join(projectName, "har")); err != nil { + if err := builtin.CreateFolder(filepath.Join(projectName, "har")); err != nil { return err } - if err := builtin.CreateFolder(path.Join(projectName, "testcases")); err != nil { + if err := builtin.CreateFolder(filepath.Join(projectName, "testcases")); err != nil { return err } - if err := builtin.CreateFolder(path.Join(projectName, "reports")); err != nil { - return err - } - - // create demo testcases - var tCase *hrp.TCase - if pluginType == Ignore { - tCase, _ = demoTestCaseWithoutPlugin.ToTCase() - } else { - tCase, _ = demoTestCase.ToTCase() - } - err := builtin.Dump2JSON(tCase, path.Join(projectName, "testcases", "demo.json")) - if err != nil { - log.Error().Err(err).Msg("create demo.json testcase failed") - return err - } - err = builtin.Dump2YAML(tCase, path.Join(projectName, "testcases", "demo.yaml")) - if err != nil { - log.Error().Err(err).Msg("create demo.yml testcase failed") + if err := builtin.CreateFolder(filepath.Join(projectName, "reports")); err != nil { return err } // create .gitignore - if err := builtin.CreateFile(path.Join(projectName, ".gitignore"), demoIgnoreContent); err != nil { + err := CopyFile("templates/gitignore", filepath.Join(projectName, ".gitignore")) + if err != nil { return err } // create .env - if err := builtin.CreateFile(path.Join(projectName, ".env"), demoEnvContent); err != nil { + err = CopyFile("templates/env", filepath.Join(projectName, ".env")) + if err != nil { + return err + } + + // create demo testcases + if pluginType == Ignore { + err := CopyFile("templates/testcases/demo_without_plugin.json", + filepath.Join(projectName, "testcases", "demo_without_plugin.json")) + if err != nil { + return err + } + log.Info().Msg("skip creating function plugin") + return nil + } + + err = CopyFile("templates/testcases/demo_with_funplugin.json", + filepath.Join(projectName, "testcases", "demo_with_funplugin.json")) + if err != nil { + return err + } + err = CopyFile("templates/testcases/demo_requests.yml", + filepath.Join(projectName, "testcases", "demo_requests.yml")) + if err != nil { + return err + } + err = CopyFile("templates/testcases/demo_ref_testcase.yml", + filepath.Join(projectName, "testcases", "demo_ref_testcase.yml")) + if err != nil { return err } // create debugtalk function plugin switch pluginType { - case Ignore: - log.Info().Msg("skip creating function plugin") - return nil case Py: return createPythonPlugin(projectName) case Go: @@ -105,12 +132,13 @@ func createGoPlugin(projectName string) error { } // create debugtalk.go - pluginDir := path.Join(projectName, "plugin") + pluginDir := filepath.Join(projectName, "plugin") if err := builtin.CreateFolder(pluginDir); err != nil { return err } - pluginFile := path.Join(pluginDir, "debugtalk.go") - if err := builtin.CreateFile(pluginFile, demoGoPlugin); err != nil { + err := CopyFile("templates/plugin/debugtalk.go", + filepath.Join(projectName, "plugin", "debugtalk.go")) + if err != nil { return err } @@ -125,7 +153,7 @@ func createGoPlugin(projectName string) error { } // build plugin debugtalk.bin - if err := builtin.ExecCommand(exec.Command("go", "build", "-o", path.Join("..", "debugtalk.bin"), "debugtalk.go"), pluginDir); err != nil { + if err := builtin.ExecCommand(exec.Command("go", "build", "-o", filepath.Join("..", "debugtalk.bin"), "debugtalk.go"), pluginDir); err != nil { return err } @@ -136,8 +164,9 @@ func createPythonPlugin(projectName string) error { log.Info().Msg("start to create hashicorp python plugin") // create debugtalk.py - pluginFile := path.Join(projectName, "debugtalk.py") - if err := builtin.CreateFile(pluginFile, demoPyPlugin); err != nil { + pluginFile := filepath.Join(projectName, "debugtalk.py") + err := CopyFile("templates/plugin/debugtalk.py", pluginFile) + if err != nil { return err } diff --git a/hrp/internal/scaffold/templates/env b/hrp/internal/scaffold/templates/env new file mode 100644 index 00000000..9b5dc360 --- /dev/null +++ b/hrp/internal/scaffold/templates/env @@ -0,0 +1,2 @@ +USERNAME=debugtalk +PASSWORD=123456 \ No newline at end of file diff --git a/hrp/internal/scaffold/templates/gitignore b/hrp/internal/scaffold/templates/gitignore new file mode 100644 index 00000000..33401380 --- /dev/null +++ b/hrp/internal/scaffold/templates/gitignore @@ -0,0 +1,15 @@ +.env +reports/ +*.so +.vscode/ +.idea/ +.DS_Store +output/ +__pycache__/ +*.pyc +.python-version +logs/ + +# plugin +debugtalk.bin +debugtalk.so diff --git a/hrp/internal/scaffold/templates/plugin/debugtalk.go b/hrp/internal/scaffold/templates/plugin/debugtalk.go new file mode 100644 index 00000000..f99a1321 --- /dev/null +++ b/hrp/internal/scaffold/templates/plugin/debugtalk.go @@ -0,0 +1,57 @@ +package main + +import ( + "fmt" + + "github.com/httprunner/funplugin/fungo" +) + +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) +} + +func GetVersion() string { + return "v4.0.0-alpha" +} + +func main() { + fungo.Register("get_httprunner_version", GetVersion) + fungo.Register("sum_ints", SumInts) + fungo.Register("sum_two_int", SumTwoInt) + fungo.Register("sum_two", SumTwoInt) + fungo.Register("sum", Sum) + fungo.Register("setup_hook_example", SetupHookExample) + fungo.Register("teardown_hook_example", TeardownHookExample) + fungo.Serve() +} diff --git a/examples/hrp/debugtalk.py b/hrp/internal/scaffold/templates/plugin/debugtalk.py similarity index 84% rename from examples/hrp/debugtalk.py rename to hrp/internal/scaffold/templates/plugin/debugtalk.py index 3d2bb5ff..743e7f62 100644 --- a/examples/hrp/debugtalk.py +++ b/hrp/internal/scaffold/templates/plugin/debugtalk.py @@ -1,53 +1,71 @@ import logging +import time from typing import List import funppy +def get_httprunner_version(): + return "v4.0.0-alpha" + + +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__': + funppy.register("get_httprunner_version", get_httprunner_version) funppy.register("sum", sum) funppy.register("sum_ints", sum_ints) funppy.register("concatenate", concatenate) funppy.register("sum_two_int", sum_two_int) + funppy.register("sum_two", sum_two_int) funppy.register("sum_two_string", sum_two_string) funppy.register("sum_strings", sum_strings) funppy.register("setup_hook_example", setup_hook_example) diff --git a/hrp/internal/scaffold/templates/testcases/__init__.py b/hrp/internal/scaffold/templates/testcases/__init__.py new file mode 100644 index 00000000..70cfba53 --- /dev/null +++ b/hrp/internal/scaffold/templates/testcases/__init__.py @@ -0,0 +1 @@ +# NOTICE: Generated By HttpRunner. DO NOT EDIT! diff --git a/hrp/internal/scaffold/templates/testcases/demo_ref_testcase.yml b/hrp/internal/scaffold/templates/testcases/demo_ref_testcase.yml new file mode 100644 index 00000000..7c9bcd19 --- /dev/null +++ b/hrp/internal/scaffold/templates/testcases/demo_ref_testcase.yml @@ -0,0 +1,33 @@ +config: + name: "request methods testcase: reference testcase" + variables: + foo1: testsuite_config_bar1 + expect_foo1: testsuite_config_bar1 + expect_foo2: config_bar2 + base_url: "https://postman-echo.com" + verify: False + +teststeps: +- + name: request with functions + variables: + foo1: testcase_ref_bar1 + expect_foo1: testcase_ref_bar1 + testcase: testcases/demo_requests.yml + export: + - foo3 +- + name: post form data + variables: + foo1: bar1 + request: + method: POST + url: /post + headers: + User-Agent: HttpRunner/${get_httprunner_version()} + Content-Type: "application/x-www-form-urlencoded" + data: "foo1=$foo1&foo2=$foo3" + validate: + - eq: ["status_code", 200] + - eq: ["body.form.foo1", "bar1"] + - eq: ["body.form.foo2", "bar21"] diff --git a/hrp/internal/scaffold/templates/testcases/demo_ref_testcase_test.py b/hrp/internal/scaffold/templates/testcases/demo_ref_testcase_test.py new file mode 100644 index 00000000..e8707a57 --- /dev/null +++ b/hrp/internal/scaffold/templates/testcases/demo_ref_testcase_test.py @@ -0,0 +1,60 @@ +# NOTE: Generated By HttpRunner v4.0.0-alpha +# FROM: testcases/demo_ref_testcase.yml + + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent.parent)) + + +from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase + +from testcases.demo_requests_test import TestCaseDemoRequests as DemoRequests + + +class TestCaseDemoRefTestcase(HttpRunner): + + config = ( + Config("request methods testcase: reference testcase") + .variables( + **{ + "foo1": "testsuite_config_bar1", + "expect_foo1": "testsuite_config_bar1", + "expect_foo2": "config_bar2", + } + ) + .base_url("https://postman-echo.com") + .verify(False) + ) + + teststeps = [ + Step( + RunTestCase("request with functions") + .with_variables( + **{"foo1": "testcase_ref_bar1", "expect_foo1": "testcase_ref_bar1"} + ) + .call(DemoRequests) + .export(*["foo3"]) + ), + Step( + RunRequest("post form data") + .with_variables(**{"foo1": "bar1"}) + .post("/post") + .with_headers( + **{ + "User-Agent": "HttpRunner/${get_httprunner_version()}", + "Content-Type": "application/x-www-form-urlencoded", + } + ) + .with_data("foo1=$foo1&foo2=$foo3") + .validate() + .assert_equal("status_code", 200) + .assert_equal("body.form.foo1", "bar1") + .assert_equal("body.form.foo2", "bar21") + ), + ] + + +if __name__ == "__main__": + TestCaseDemoRefTestcase().test_start() diff --git a/hrp/internal/scaffold/templates/testcases/demo_requests.yml b/hrp/internal/scaffold/templates/testcases/demo_requests.yml new file mode 100644 index 00000000..7c8be928 --- /dev/null +++ b/hrp/internal/scaffold/templates/testcases/demo_requests.yml @@ -0,0 +1,65 @@ +config: + name: "request methods testcase with functions" + variables: + foo1: config_bar1 + foo2: config_bar2 + expect_foo1: config_bar1 + expect_foo2: config_bar2 + base_url: "https://postman-echo.com" + verify: False + export: ["foo3"] + +teststeps: +- + name: get with params + variables: + foo1: bar11 + foo2: bar21 + sum_v: "${sum_two(1, 2)}" + request: + method: GET + url: /get + params: + foo1: $foo1 + foo2: $foo2 + sum_v: $sum_v + headers: + User-Agent: HttpRunner/${get_httprunner_version()} + extract: + foo3: "body.args.foo2" + validate: + - eq: ["status_code", 200] + - eq: ["body.args.foo1", "bar11"] + - eq: ["body.args.sum_v", "3"] + - eq: ["body.args.foo2", "bar21"] +- + name: post raw text + variables: + foo1: "bar12" + foo3: "bar32" + request: + method: POST + url: /post + headers: + User-Agent: HttpRunner/${get_httprunner_version()} + Content-Type: "text/plain" + data: "This is expected to be sent back as part of response body: $foo1-$foo2-$foo3." + validate: + - eq: ["status_code", 200] + - eq: ["body.data", "This is expected to be sent back as part of response body: bar12-$expect_foo2-bar32."] +- + name: post form data + variables: + foo2: bar23 + request: + method: POST + url: /post + headers: + User-Agent: HttpRunner/${get_httprunner_version()} + Content-Type: "application/x-www-form-urlencoded" + data: "foo1=$foo1&foo2=$foo2&foo3=$foo3" + validate: + - eq: ["status_code", 200] + - eq: ["body.form.foo1", "$expect_foo1"] + - eq: ["body.form.foo2", "bar23"] + - eq: ["body.form.foo3", "bar21"] diff --git a/hrp/internal/scaffold/templates/testcases/demo_requests_test.py b/hrp/internal/scaffold/templates/testcases/demo_requests_test.py new file mode 100644 index 00000000..526961e3 --- /dev/null +++ b/hrp/internal/scaffold/templates/testcases/demo_requests_test.py @@ -0,0 +1,83 @@ +# NOTE: Generated By HttpRunner v4.0.0-alpha +# FROM: testcases/demo_requests.yml + + +from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase + + +class TestCaseDemoRequests(HttpRunner): + + config = ( + Config("request methods testcase with functions") + .variables( + **{ + "foo1": "config_bar1", + "foo2": "config_bar2", + "expect_foo1": "config_bar1", + "expect_foo2": "config_bar2", + } + ) + .base_url("https://postman-echo.com") + .verify(False) + .export(*["foo3"]) + ) + + teststeps = [ + Step( + RunRequest("get with params") + .with_variables( + **{"foo1": "bar11", "foo2": "bar21", "sum_v": "${sum_two(1, 2)}"} + ) + .get("/get") + .with_params(**{"foo1": "$foo1", "foo2": "$foo2", "sum_v": "$sum_v"}) + .with_headers(**{"User-Agent": "HttpRunner/${get_httprunner_version()}"}) + .extract() + .with_jmespath("body.args.foo2", "foo3") + .validate() + .assert_equal("status_code", 200) + .assert_equal("body.args.foo1", "bar11") + .assert_equal("body.args.sum_v", "3") + .assert_equal("body.args.foo2", "bar21") + ), + Step( + RunRequest("post raw text") + .with_variables(**{"foo1": "bar12", "foo3": "bar32"}) + .post("/post") + .with_headers( + **{ + "User-Agent": "HttpRunner/${get_httprunner_version()}", + "Content-Type": "text/plain", + } + ) + .with_data( + "This is expected to be sent back as part of response body: $foo1-$foo2-$foo3." + ) + .validate() + .assert_equal("status_code", 200) + .assert_equal( + "body.data", + "This is expected to be sent back as part of response body: bar12-$expect_foo2-bar32.", + ) + ), + Step( + RunRequest("post form data") + .with_variables(**{"foo2": "bar23"}) + .post("/post") + .with_headers( + **{ + "User-Agent": "HttpRunner/${get_httprunner_version()}", + "Content-Type": "application/x-www-form-urlencoded", + } + ) + .with_data("foo1=$foo1&foo2=$foo2&foo3=$foo3") + .validate() + .assert_equal("status_code", 200) + .assert_equal("body.form.foo1", "$expect_foo1") + .assert_equal("body.form.foo2", "bar23") + .assert_equal("body.form.foo3", "bar21") + ), + ] + + +if __name__ == "__main__": + TestCaseDemoRequests().test_start() diff --git a/examples/hrp/demo.json b/hrp/internal/scaffold/templates/testcases/demo_with_funplugin.json similarity index 100% rename from examples/hrp/demo.json rename to hrp/internal/scaffold/templates/testcases/demo_with_funplugin.json diff --git a/examples/hrp/demo.yaml b/hrp/internal/scaffold/templates/testcases/demo_with_funplugin.yaml similarity index 100% rename from examples/hrp/demo.yaml rename to hrp/internal/scaffold/templates/testcases/demo_with_funplugin.yaml diff --git a/hrp/internal/scaffold/templates/testcases/demo_without_funplugin.json b/hrp/internal/scaffold/templates/testcases/demo_without_funplugin.json new file mode 100644 index 00000000..29f311bc --- /dev/null +++ b/hrp/internal/scaffold/templates/testcases/demo_without_funplugin.json @@ -0,0 +1,170 @@ +{ + "config": { + "name": "demo without custom function plugin", + "base_url": "https://postman-echo.com", + "variables": { + "a": 12.3, + "b": 3.45, + "n": 5, + "varFoo1": "${gen_random_string($n)}", + "varFoo2": "${max($a, $b)}" + } + }, + "teststeps": [ + { + "name": "transaction 1 start", + "transaction": { + "name": "tran1", + "type": "start" + } + }, + { + "name": "get with params", + "request": { + "method": "GET", + "url": "/get", + "params": { + "foo1": "$varFoo1", + "foo2": "$varFoo2" + }, + "headers": { + "User-Agent": "HttpRunnerPlus" + } + }, + "variables": { + "b": 34.5, + "n": 3, + "name": "get with params", + "varFoo2": "${max($a, $b)}" + }, + "extract": { + "varFoo1": "body.args.foo1" + }, + "validate": [ + { + "check": "status_code", + "assert": "equals", + "expect": 200, + "msg": "check response status code" + }, + { + "check": "headers.\"Content-Type\"", + "assert": "startswith", + "expect": "application/json" + }, + { + "check": "body.args.foo1", + "assert": "length_equals", + "expect": 5, + "msg": "check args foo1" + }, + { + "check": "$varFoo1", + "assert": "length_equals", + "expect": 5, + "msg": "check args foo1" + }, + { + "check": "body.args.foo2", + "assert": "equals", + "expect": "34.5", + "msg": "check args foo2" + } + ] + }, + { + "name": "transaction 1 end", + "transaction": { + "name": "tran1", + "type": "end" + } + }, + { + "name": "post json data", + "request": { + "method": "POST", + "url": "/post", + "body": { + "foo1": "$varFoo1", + "foo2": "${max($a, $b)}" + } + }, + "validate": [ + { + "check": "status_code", + "assert": "equals", + "expect": 200, + "msg": "check status code" + }, + { + "check": "body.json.foo1", + "assert": "length_equals", + "expect": 5, + "msg": "check args foo1" + }, + { + "check": "body.json.foo2", + "assert": "equals", + "expect": 12.3, + "msg": "check args foo2" + } + ] + }, + { + "name": "post form data", + "request": { + "method": "POST", + "url": "/post", + "headers": { + "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8" + }, + "body": { + "foo1": "$varFoo1", + "foo2": "${max($a, $b)}", + "time": "${get_timestamp()}" + } + }, + "extract": { + "varTime": "body.form.time" + }, + "validate": [ + { + "check": "status_code", + "assert": "equals", + "expect": 200, + "msg": "check status code" + }, + { + "check": "body.form.foo1", + "assert": "length_equals", + "expect": 5, + "msg": "check args foo1" + }, + { + "check": "body.form.foo2", + "assert": "equals", + "expect": "12.3", + "msg": "check args foo2" + } + ] + }, + { + "name": "get with timestamp", + "request": { + "method": "GET", + "url": "/get", + "params": { + "time": "$varTime" + } + }, + "validate": [ + { + "check": "body.args.time", + "assert": "length_equals", + "expect": 13, + "msg": "check extracted var timestamp" + } + ] + } + ] +} \ No newline at end of file diff --git a/hrp/internal/scaffold/templates/testcases/demo_without_funplugin.yaml b/hrp/internal/scaffold/templates/testcases/demo_without_funplugin.yaml new file mode 100644 index 00000000..b276c271 --- /dev/null +++ b/hrp/internal/scaffold/templates/testcases/demo_without_funplugin.yaml @@ -0,0 +1,110 @@ +config: + name: demo without custom function plugin + base_url: https://postman-echo.com + variables: + a: 12.3 + b: 3.45 + "n": 5 + varFoo1: ${gen_random_string($n)} + varFoo2: ${max($a, $b)} +teststeps: + - name: transaction 1 start + transaction: + name: tran1 + type: start + - name: get with params + request: + method: GET + url: /get + params: + foo1: $varFoo1 + foo2: $varFoo2 + headers: + User-Agent: HttpRunnerPlus + variables: + b: 34.5 + "n": 3 + name: get with params + varFoo2: ${max($a, $b)} + extract: + varFoo1: body.args.foo1 + validate: + - check: status_code + assert: equals + expect: 200 + msg: check response status code + - check: headers."Content-Type" + assert: startswith + expect: application/json + - check: body.args.foo1 + assert: length_equals + expect: 5 + msg: check args foo1 + - check: $varFoo1 + assert: length_equals + expect: 5 + msg: check args foo1 + - check: body.args.foo2 + assert: equals + expect: "34.5" + msg: check args foo2 + - name: transaction 1 end + transaction: + name: tran1 + type: end + - name: post json data + request: + method: POST + url: /post + body: + foo1: $varFoo1 + foo2: ${max($a, $b)} + validate: + - check: status_code + assert: equals + expect: 200 + msg: check status code + - check: body.json.foo1 + assert: length_equals + expect: 5 + msg: check args foo1 + - check: body.json.foo2 + assert: equals + expect: 12.3 + msg: check args foo2 + - name: post form data + request: + method: POST + url: /post + headers: + Content-Type: application/x-www-form-urlencoded; charset=UTF-8 + body: + foo1: $varFoo1 + foo2: ${max($a, $b)} + time: ${get_timestamp()} + extract: + varTime: body.form.time + validate: + - check: status_code + assert: equals + expect: 200 + msg: check status code + - check: body.form.foo1 + assert: length_equals + expect: 5 + msg: check args foo1 + - check: body.form.foo2 + assert: equals + expect: "12.3" + msg: check args foo2 + - name: get with timestamp + request: + method: GET + url: /get + params: + time: $varTime + validate: + - check: body.args.time + assert: length_equals + expect: 13 + msg: check extracted var timestamp diff --git a/hrp/plugin_test.go b/hrp/plugin_test.go index 2016ef5b..612e40ae 100644 --- a/hrp/plugin_test.go +++ b/hrp/plugin_test.go @@ -8,32 +8,24 @@ import ( func TestLocateFile(t *testing.T) { // specify target file path - _, err := locateFile("../examples/hrp/plugin/debugtalk.go", "debugtalk.go") + _, err := locateFile(templatesDir+"plugin/debugtalk.go", "debugtalk.go") if !assert.Nil(t, err) { t.Fail() } // specify path with the same dir - _, err = locateFile("../examples/hrp/plugin/hashicorp.go", "debugtalk.go") + _, err = locateFile(templatesDir+"plugin/debugtalk.py", "debugtalk.go") if !assert.Nil(t, err) { t.Fail() } // specify target file path dir - _, err = locateFile("../examples/hrp/plugin/", "debugtalk.go") + _, err = locateFile(templatesDir+"plugin/", "debugtalk.go") if !assert.Nil(t, err) { t.Fail() } // specify wrong path - _, err = locateFile("../examples/hrp", "debugtalk.go") - if !assert.Error(t, err) { - t.Fail() - } - _, err = locateFile("../examples/hrp/demo.json", "debugtalk.go") - if !assert.Error(t, err) { - t.Fail() - } _, err = locateFile(".", "debugtalk.go") if !assert.Error(t, err) { t.Fail() @@ -45,17 +37,17 @@ func TestLocateFile(t *testing.T) { } func TestLocatePythonPlugin(t *testing.T) { - _, err := locatePlugin("../examples/hrp/debugtalk.py") + _, err := locatePlugin(templatesDir + "plugin/debugtalk.py") if !assert.Nil(t, err) { t.Fail() } } func TestLocateGoPlugin(t *testing.T) { - buildHashicorpPlugin() - defer removeHashicorpPlugin() + buildHashicorpGoPlugin() + defer removeHashicorpGoPlugin() - _, err := locatePlugin("../examples/hrp/debugtalk.bin") + _, err := locatePlugin(templatesDir + "debugtalk.bin") if !assert.Nil(t, err) { t.Fail() } diff --git a/hrp/runner_test.go b/hrp/runner_test.go index d6945425..b753ebf9 100644 --- a/hrp/runner_test.go +++ b/hrp/runner_test.go @@ -8,31 +8,49 @@ import ( "time" "github.com/rs/zerolog/log" + + "github.com/httprunner/httprunner/hrp/internal/scaffold" ) -func buildHashicorpPlugin() { +func buildHashicorpGoPlugin() { log.Info().Msg("[init] build hashicorp go plugin") cmd := exec.Command("go", "build", - "-o", "../examples/hrp/debugtalk.bin", - "../examples/hrp/plugin/hashicorp.go", "../examples/hrp/plugin/debugtalk.go") + "-o", templatesDir+"debugtalk.bin", templatesDir+"plugin/debugtalk.go") if err := cmd.Run(); err != nil { panic(err) } } -func removeHashicorpPlugin() { - log.Info().Msg("[teardown] remove hashicorp plugin") - os.Remove("../examples/hrp/debugtalk.bin") +func removeHashicorpGoPlugin() { + log.Info().Msg("[teardown] remove hashicorp go plugin") + os.Remove(templatesDir + "debugtalk.bin") +} + +func buildHashicorpPyPlugin() { + log.Info().Msg("[init] prepare hashicorp python plugin") + pluginFile := templatesDir + "debugtalk.py" + err := scaffold.CopyFile("templates/plugin/debugtalk.py", pluginFile) + if err != nil { + panic(err) + } +} + +func removeHashicorpPyPlugin() { + log.Info().Msg("[teardown] remove hashicorp python plugin") + os.Remove(templatesDir + "debugtalk.py") } func TestHttpRunnerWithGoPlugin(t *testing.T) { - buildHashicorpPlugin() - defer removeHashicorpPlugin() + buildHashicorpGoPlugin() + defer removeHashicorpGoPlugin() assertRunTestCases(t) } func TestHttpRunnerWithPythonPlugin(t *testing.T) { + buildHashicorpPyPlugin() + defer removeHashicorpPyPlugin() + assertRunTestCases(t) } @@ -64,7 +82,7 @@ func assertRunTestCases(t *testing.T) { }, ), NewStep("TestCase4").CallRefCase(&demoRefAPIYAMLPath), - NewStep("TestCase5").CallRefCase(&demoTestCaseJSONPath), + NewStep("TestCase5").CallRefCase(&demoTestCaseWithPluginJSONPath), }, } testcase2 := &TestCase{ @@ -153,8 +171,8 @@ func TestInitRendezvous(t *testing.T) { } func TestThinkTime(t *testing.T) { - buildHashicorpPlugin() - defer removeHashicorpPlugin() + buildHashicorpGoPlugin() + defer removeHashicorpGoPlugin() testcases := []*TestCase{ { diff --git a/httprunner/cli.py b/httprunner/cli.py index 033312c3..d015313e 100644 --- a/httprunner/cli.py +++ b/httprunner/cli.py @@ -9,7 +9,6 @@ from loguru import logger from httprunner import __description__, __version__ from httprunner.compat import ensure_cli_args from httprunner.make import init_make_parser, main_make -from httprunner.scaffold import init_parser_scaffold, main_scaffold from httprunner.utils import ga_client, init_sentry_sdk init_sentry_sdk() @@ -65,7 +64,6 @@ def main(): subparsers = parser.add_subparsers(help="sub-command help") sub_parser_run = init_parser_run(subparsers) - sub_parser_scaffold = init_parser_scaffold(subparsers) sub_parser_make = init_make_parser(subparsers) if len(sys.argv) == 1: @@ -80,9 +78,6 @@ def main(): elif sys.argv[1] in ["-h", "--help"]: # httprunner -h parser.print_help() - elif sys.argv[1] == "startproject": - # httprunner startproject - sub_parser_scaffold.print_help() elif sys.argv[1] == "run": # httprunner run pytest.main(["-h"]) @@ -109,8 +104,6 @@ def main(): if sys.argv[1] == "run": sys.exit(main_run(extra_args)) - elif sys.argv[1] == "startproject": - main_scaffold(args) elif sys.argv[1] == "make": main_make(args.testcase_path) diff --git a/httprunner/scaffold.py b/httprunner/scaffold.py deleted file mode 100644 index 282bfb08..00000000 --- a/httprunner/scaffold.py +++ /dev/null @@ -1,204 +0,0 @@ -import os.path -import subprocess -import sys - -from loguru import logger - -from httprunner.utils import ga_client - - -def init_parser_scaffold(subparsers): - sub_parser_scaffold = subparsers.add_parser( - "startproject", help="Create a new project with template structure." - ) - sub_parser_scaffold.add_argument( - "project_name", type=str, nargs="?", help="Specify new project name." - ) - return sub_parser_scaffold - - -def create_scaffold(project_name): - """ create scaffold with specified project name. - """ - - def show_tree(prj_name): - try: - print(f"\n$ tree {prj_name} -a") - subprocess.run(["tree", prj_name, "-a"]) - print("") - except FileNotFoundError: - logger.warning("tree command not exists, ignore.") - - if os.path.isdir(project_name): - logger.warning( - f"Project folder {project_name} exists, please specify a new project name." - ) - show_tree(project_name) - return 1 - elif os.path.isfile(project_name): - logger.warning( - f"Project name {project_name} conflicts with existed file, please specify a new one." - ) - return 1 - - logger.info(f"Create new project: {project_name}") - print(f"Project Root Dir: {os.path.join(os.getcwd(), project_name)}\n") - - def create_folder(path): - os.makedirs(path) - msg = f"created folder: {path}" - print(msg) - - def create_file(path, file_content=""): - with open(path, "w", encoding="utf-8") as f: - f.write(file_content) - msg = f"created file: {path}" - print(msg) - - demo_testcase_request_content = """ -config: - name: "request methods testcase with functions" - variables: - foo1: config_bar1 - foo2: config_bar2 - expect_foo1: config_bar1 - expect_foo2: config_bar2 - base_url: "https://postman-echo.com" - verify: False - export: ["foo3"] - -teststeps: -- - name: get with params - variables: - foo1: bar11 - foo2: bar21 - sum_v: "${sum_two(1, 2)}" - request: - method: GET - url: /get - params: - foo1: $foo1 - foo2: $foo2 - sum_v: $sum_v - headers: - User-Agent: HttpRunner/${get_httprunner_version()} - extract: - foo3: "body.args.foo2" - validate: - - eq: ["status_code", 200] - - eq: ["body.args.foo1", "bar11"] - - eq: ["body.args.sum_v", "3"] - - eq: ["body.args.foo2", "bar21"] -- - name: post raw text - variables: - foo1: "bar12" - foo3: "bar32" - request: - method: POST - url: /post - headers: - User-Agent: HttpRunner/${get_httprunner_version()} - Content-Type: "text/plain" - data: "This is expected to be sent back as part of response body: $foo1-$foo2-$foo3." - validate: - - eq: ["status_code", 200] - - eq: ["body.data", "This is expected to be sent back as part of response body: bar12-$expect_foo2-bar32."] -- - name: post form data - variables: - foo2: bar23 - request: - method: POST - url: /post - headers: - User-Agent: HttpRunner/${get_httprunner_version()} - Content-Type: "application/x-www-form-urlencoded" - data: "foo1=$foo1&foo2=$foo2&foo3=$foo3" - validate: - - eq: ["status_code", 200] - - eq: ["body.form.foo1", "$expect_foo1"] - - eq: ["body.form.foo2", "bar23"] - - eq: ["body.form.foo3", "bar21"] -""" - demo_testcase_with_ref_content = """ -config: - name: "request methods testcase: reference testcase" - variables: - foo1: testsuite_config_bar1 - expect_foo1: testsuite_config_bar1 - expect_foo2: config_bar2 - base_url: "https://postman-echo.com" - verify: False - -teststeps: -- - name: request with functions - variables: - foo1: testcase_ref_bar1 - expect_foo1: testcase_ref_bar1 - testcase: testcases/demo_testcase_request.yml - export: - - foo3 -- - name: post form data - variables: - foo1: bar1 - request: - method: POST - url: /post - headers: - User-Agent: HttpRunner/${get_httprunner_version()} - Content-Type: "application/x-www-form-urlencoded" - data: "foo1=$foo1&foo2=$foo3" - validate: - - eq: ["status_code", 200] - - eq: ["body.form.foo1", "bar1"] - - eq: ["body.form.foo2", "bar21"] -""" - ignore_content = "\n".join( - [".env", "reports/*", "__pycache__/*", "*.pyc", ".python-version", "logs/*"] - ) - demo_debugtalk_content = """import time - -from httprunner import __version__ - - -def get_httprunner_version(): - return __version__ - - -def sum_two(m, n): - return m + n - - -def sleep(n_secs): - time.sleep(n_secs) -""" - demo_env_content = "\n".join(["USERNAME=leolee", "PASSWORD=123456"]) - - create_folder(project_name) - create_folder(os.path.join(project_name, "har")) - create_folder(os.path.join(project_name, "testcases")) - create_folder(os.path.join(project_name, "reports")) - - create_file( - os.path.join(project_name, "testcases", "demo_testcase_request.yml"), - demo_testcase_request_content, - ) - create_file( - os.path.join(project_name, "testcases", "demo_testcase_ref.yml"), - demo_testcase_with_ref_content, - ) - create_file(os.path.join(project_name, "debugtalk.py"), demo_debugtalk_content) - create_file(os.path.join(project_name, ".env"), demo_env_content) - create_file(os.path.join(project_name, ".gitignore"), ignore_content) - - show_tree(project_name) - return 0 - - -def main_scaffold(args): - ga_client.track_event("Scaffold", "startproject") - sys.exit(create_scaffold(args.project_name)) diff --git a/httprunner/scaffold_test.py b/httprunner/scaffold_test.py deleted file mode 100644 index fbb2ed64..00000000 --- a/httprunner/scaffold_test.py +++ /dev/null @@ -1,29 +0,0 @@ -import os -import shutil -import subprocess -import unittest -import platform - -from httprunner.scaffold import create_scaffold - - -class TestScaffold(unittest.TestCase): - def test_create_scaffold(self): - project_name = "projectABC" - create_scaffold(project_name) - self.assertTrue(os.path.isdir(os.path.join(project_name, "har"))) - self.assertTrue(os.path.isdir(os.path.join(project_name, "testcases"))) - self.assertTrue(os.path.isdir(os.path.join(project_name, "reports"))) - self.assertTrue(os.path.isfile(os.path.join(project_name, "debugtalk.py"))) - self.assertTrue(os.path.isfile(os.path.join(project_name, ".env"))) - - # run demo testcases - try: - if platform.system() == "Windows": - subprocess.check_call(["hrun", project_name], shell=True) - else: - subprocess.check_call(["hrun", project_name]) - except subprocess.SubprocessError: - raise - finally: - shutil.rmtree(project_name)