diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 67a35ebd..a50a37a9 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,12 +1,14 @@ # Release History -## v4.1.0 (2022-05-23) +## v4.1.0 (2022-05-25) - feat: add `wiki` sub-command to open httprunner website **go version** +- fix #1308: load `.env` file as environment variables - fix #1309: locate plugin file upward recursively until system root dir +- refactor: move base_url to config env - feat: support converting Postman collection to HttpRunner testcase - refactor: improve the extensibility of `hrp convert` using interface `ICaseConverter` diff --git a/docs/cmd/hrp.md b/docs/cmd/hrp.md index 9e7fe395..74a6c8b2 100644 --- a/docs/cmd/hrp.md +++ b/docs/cmd/hrp.md @@ -35,5 +35,6 @@ Copyright 2017 debugtalk * [hrp pytest](hrp_pytest.md) - run API test with pytest * [hrp run](hrp_run.md) - run API test with go engine * [hrp startproject](hrp_startproject.md) - create a scaffold project +* [hrp wiki](hrp_wiki.md) - visit https://httprunner.com -###### Auto generated by spf13/cobra on 23-May-2022 +###### Auto generated by spf13/cobra on 25-May-2022 diff --git a/docs/cmd/hrp_boom.md b/docs/cmd/hrp_boom.md index 2d384e99..f6eb3a55 100644 --- a/docs/cmd/hrp_boom.md +++ b/docs/cmd/hrp_boom.md @@ -42,4 +42,4 @@ hrp boom [flags] * [hrp](hrp.md) - Next-Generation API Testing Solution. -###### Auto generated by spf13/cobra on 23-May-2022 +###### Auto generated by spf13/cobra on 25-May-2022 diff --git a/docs/cmd/hrp_convert.md b/docs/cmd/hrp_convert.md index 80fcaf2f..c3b1b5cc 100644 --- a/docs/cmd/hrp_convert.md +++ b/docs/cmd/hrp_convert.md @@ -22,4 +22,4 @@ hrp convert $path... [flags] * [hrp](hrp.md) - Next-Generation API Testing Solution. -###### Auto generated by spf13/cobra on 23-May-2022 +###### Auto generated by spf13/cobra on 25-May-2022 diff --git a/docs/cmd/hrp_har2case.md b/docs/cmd/hrp_har2case.md index 0ef151a3..a37bbf1e 100644 --- a/docs/cmd/hrp_har2case.md +++ b/docs/cmd/hrp_har2case.md @@ -24,4 +24,4 @@ hrp har2case $har_path... [flags] * [hrp](hrp.md) - Next-Generation API Testing Solution. -###### Auto generated by spf13/cobra on 23-May-2022 +###### Auto generated by spf13/cobra on 25-May-2022 diff --git a/docs/cmd/hrp_pytest.md b/docs/cmd/hrp_pytest.md index 2ed3b104..30682a20 100644 --- a/docs/cmd/hrp_pytest.md +++ b/docs/cmd/hrp_pytest.md @@ -16,4 +16,4 @@ hrp pytest $path ... [flags] * [hrp](hrp.md) - Next-Generation API Testing Solution. -###### Auto generated by spf13/cobra on 23-May-2022 +###### Auto generated by spf13/cobra on 25-May-2022 diff --git a/docs/cmd/hrp_run.md b/docs/cmd/hrp_run.md index ff4ba4a7..46725b8a 100644 --- a/docs/cmd/hrp_run.md +++ b/docs/cmd/hrp_run.md @@ -35,4 +35,4 @@ hrp run $path... [flags] * [hrp](hrp.md) - Next-Generation API Testing Solution. -###### Auto generated by spf13/cobra on 23-May-2022 +###### Auto generated by spf13/cobra on 25-May-2022 diff --git a/docs/cmd/hrp_startproject.md b/docs/cmd/hrp_startproject.md index d598c7aa..eb0b6bb9 100644 --- a/docs/cmd/hrp_startproject.md +++ b/docs/cmd/hrp_startproject.md @@ -20,4 +20,4 @@ hrp startproject $project_name [flags] * [hrp](hrp.md) - Next-Generation API Testing Solution. -###### Auto generated by spf13/cobra on 23-May-2022 +###### Auto generated by spf13/cobra on 25-May-2022 diff --git a/docs/cmd/hrp_wiki.md b/docs/cmd/hrp_wiki.md new file mode 100644 index 00000000..1219555f --- /dev/null +++ b/docs/cmd/hrp_wiki.md @@ -0,0 +1,19 @@ +## hrp wiki + +visit https://httprunner.com + +``` +hrp wiki [flags] +``` + +### Options + +``` + -h, --help help for wiki +``` + +### SEE ALSO + +* [hrp](hrp.md) - Next-Generation API Testing Solution. + +###### Auto generated by spf13/cobra on 25-May-2022 diff --git a/examples/demo-with-go-plugin/.env b/examples/demo-with-go-plugin/.env new file mode 100644 index 00000000..59ecc742 --- /dev/null +++ b/examples/demo-with-go-plugin/.env @@ -0,0 +1,3 @@ +base_url=https://postman-echo.com +USERNAME=debugtalk +PASSWORD=123456 \ No newline at end of file diff --git a/examples/demo-with-go-plugin/.gitignore b/examples/demo-with-go-plugin/.gitignore index 33401380..4c8cb60c 100644 --- a/examples/demo-with-go-plugin/.gitignore +++ b/examples/demo-with-go-plugin/.gitignore @@ -1,4 +1,3 @@ -.env reports/ *.so .vscode/ diff --git a/examples/demo-with-go-plugin/proj.json b/examples/demo-with-go-plugin/proj.json new file mode 100644 index 00000000..4dea84bc --- /dev/null +++ b/examples/demo-with-go-plugin/proj.json @@ -0,0 +1,6 @@ +{ + "project_name": "demo-with-go-plugin", + "project_path": "/Users/debugtalk/MyProjects/HttpRunner-dev/httprunner/examples/demo-with-go-plugin", + "create_time": "2022-05-25T11:14:42.750876+08:00", + "hrp_version": "v4.1.0-beta" +} \ No newline at end of file diff --git a/examples/demo-with-go-plugin/testcases/requests.json b/examples/demo-with-go-plugin/testcases/requests.json index b13f3837..9b4214d7 100644 --- a/examples/demo-with-go-plugin/testcases/requests.json +++ b/examples/demo-with-go-plugin/testcases/requests.json @@ -17,7 +17,7 @@ { "name": "get with params", "variables": { - "foo1": "bar11", + "foo1": "${ENV(USERNAME)}", "foo2": "bar21", "sum_v": "${sum_two_int(1, 2)}" }, @@ -46,7 +46,7 @@ { "eq": [ "body.args.foo1", - "bar11" + "debugtalk" ] }, { diff --git a/examples/demo-with-go-plugin/testcases/requests.yml b/examples/demo-with-go-plugin/testcases/requests.yml index 86d1b9cc..bc9aa108 100644 --- a/examples/demo-with-go-plugin/testcases/requests.yml +++ b/examples/demo-with-go-plugin/testcases/requests.yml @@ -5,7 +5,6 @@ config: foo2: config_bar2 expect_foo1: config_bar1 expect_foo2: config_bar2 - base_url: "https://postman-echo.com" verify: False export: ["foo3"] @@ -13,12 +12,12 @@ teststeps: - name: get with params variables: - foo1: bar11 + foo1: ${ENV(USERNAME)} foo2: bar21 sum_v: "${sum_two_int(1, 2)}" request: method: GET - url: /get + url: $base_url/get params: foo1: $foo1 foo2: $foo2 @@ -29,7 +28,7 @@ teststeps: foo3: "body.args.foo2" validate: - eq: ["status_code", 200] - - eq: ["body.args.foo1", "bar11"] + - eq: ["body.args.foo1", "debugtalk"] - eq: ["body.args.sum_v", "3"] - eq: ["body.args.foo2", "bar21"] - @@ -39,7 +38,7 @@ teststeps: foo3: "bar32" request: method: POST - url: /post + url: $base_url/post headers: User-Agent: funplugin/${get_version()} Content-Type: "text/plain" @@ -53,7 +52,7 @@ teststeps: foo2: bar23 request: method: POST - url: /post + url: $base_url/post headers: User-Agent: funplugin/${get_version()} Content-Type: "application/x-www-form-urlencoded" diff --git a/examples/demo-with-py-plugin/.env b/examples/demo-with-py-plugin/.env new file mode 100644 index 00000000..59ecc742 --- /dev/null +++ b/examples/demo-with-py-plugin/.env @@ -0,0 +1,3 @@ +base_url=https://postman-echo.com +USERNAME=debugtalk +PASSWORD=123456 \ No newline at end of file diff --git a/examples/demo-with-py-plugin/.gitignore b/examples/demo-with-py-plugin/.gitignore index 33401380..4c8cb60c 100644 --- a/examples/demo-with-py-plugin/.gitignore +++ b/examples/demo-with-py-plugin/.gitignore @@ -1,4 +1,3 @@ -.env reports/ *.so .vscode/ diff --git a/examples/demo-with-py-plugin/proj.json b/examples/demo-with-py-plugin/proj.json new file mode 100644 index 00000000..cca69211 --- /dev/null +++ b/examples/demo-with-py-plugin/proj.json @@ -0,0 +1,6 @@ +{ + "project_name": "demo-with-py-plugin", + "project_path": "/Users/debugtalk/MyProjects/HttpRunner-dev/httprunner/examples/demo-with-py-plugin", + "create_time": "2022-05-25T11:14:52.333942+08:00", + "hrp_version": "v4.1.0-beta" +} \ No newline at end of file diff --git a/examples/demo-with-py-plugin/testcases/requests.json b/examples/demo-with-py-plugin/testcases/requests.json index b13f3837..9b4214d7 100644 --- a/examples/demo-with-py-plugin/testcases/requests.json +++ b/examples/demo-with-py-plugin/testcases/requests.json @@ -17,7 +17,7 @@ { "name": "get with params", "variables": { - "foo1": "bar11", + "foo1": "${ENV(USERNAME)}", "foo2": "bar21", "sum_v": "${sum_two_int(1, 2)}" }, @@ -46,7 +46,7 @@ { "eq": [ "body.args.foo1", - "bar11" + "debugtalk" ] }, { diff --git a/examples/demo-with-py-plugin/testcases/requests.yml b/examples/demo-with-py-plugin/testcases/requests.yml index 86d1b9cc..bc9aa108 100644 --- a/examples/demo-with-py-plugin/testcases/requests.yml +++ b/examples/demo-with-py-plugin/testcases/requests.yml @@ -5,7 +5,6 @@ config: foo2: config_bar2 expect_foo1: config_bar1 expect_foo2: config_bar2 - base_url: "https://postman-echo.com" verify: False export: ["foo3"] @@ -13,12 +12,12 @@ teststeps: - name: get with params variables: - foo1: bar11 + foo1: ${ENV(USERNAME)} foo2: bar21 sum_v: "${sum_two_int(1, 2)}" request: method: GET - url: /get + url: $base_url/get params: foo1: $foo1 foo2: $foo2 @@ -29,7 +28,7 @@ teststeps: foo3: "body.args.foo2" validate: - eq: ["status_code", 200] - - eq: ["body.args.foo1", "bar11"] + - eq: ["body.args.foo1", "debugtalk"] - eq: ["body.args.sum_v", "3"] - eq: ["body.args.foo2", "bar21"] - @@ -39,7 +38,7 @@ teststeps: foo3: "bar32" request: method: POST - url: /post + url: $base_url/post headers: User-Agent: funplugin/${get_version()} Content-Type: "text/plain" @@ -53,7 +52,7 @@ teststeps: foo2: bar23 request: method: POST - url: /post + url: $base_url/post headers: User-Agent: funplugin/${get_version()} Content-Type: "application/x-www-form-urlencoded" diff --git a/examples/demo-without-plugin/.env b/examples/demo-without-plugin/.env new file mode 100644 index 00000000..59ecc742 --- /dev/null +++ b/examples/demo-without-plugin/.env @@ -0,0 +1,3 @@ +base_url=https://postman-echo.com +USERNAME=debugtalk +PASSWORD=123456 \ No newline at end of file diff --git a/examples/demo-without-plugin/.gitignore b/examples/demo-without-plugin/.gitignore index 33401380..4c8cb60c 100644 --- a/examples/demo-without-plugin/.gitignore +++ b/examples/demo-without-plugin/.gitignore @@ -1,4 +1,3 @@ -.env reports/ *.so .vscode/ diff --git a/examples/demo-without-plugin/proj.json b/examples/demo-without-plugin/proj.json new file mode 100644 index 00000000..3fabec85 --- /dev/null +++ b/examples/demo-without-plugin/proj.json @@ -0,0 +1,6 @@ +{ + "project_name": "demo-without-plugin", + "project_path": "/Users/debugtalk/MyProjects/HttpRunner-dev/httprunner/examples/demo-without-plugin", + "create_time": "2022-05-25T11:14:53.862348+08:00", + "hrp_version": "v4.1.0-beta" +} \ No newline at end of file diff --git a/hrp/config.go b/hrp/config.go index af903bc5..404e51c9 100644 --- a/hrp/config.go +++ b/hrp/config.go @@ -10,6 +10,7 @@ import ( func NewConfig(name string) *TConfig { return &TConfig{ Name: name, + Environs: make(map[string]string), Variables: make(map[string]interface{}), } } @@ -19,9 +20,10 @@ func NewConfig(name string) *TConfig { type TConfig struct { Name string `json:"name" yaml:"name"` // required Verify bool `json:"verify,omitempty" yaml:"verify,omitempty"` - BaseURL string `json:"base_url,omitempty" yaml:"base_url,omitempty"` - Headers map[string]string `json:"headers,omitempty" yaml:"headers,omitempty"` - Variables map[string]interface{} `json:"variables,omitempty" yaml:"variables,omitempty"` + BaseURL string `json:"base_url,omitempty" yaml:"base_url,omitempty"` // deprecated in v4.1, moved to env + Headers map[string]string `json:"headers,omitempty" yaml:"headers,omitempty"` // public request headers + Environs map[string]string `json:"environs,omitempty" yaml:"environs,omitempty"` // environment variables + Variables map[string]interface{} `json:"variables,omitempty" yaml:"variables,omitempty"` // global variables Parameters map[string]interface{} `json:"parameters,omitempty" yaml:"parameters,omitempty"` ParametersSetting *TParamsConfig `json:"parameters_setting,omitempty" yaml:"parameters_setting,omitempty"` ThinkTimeSetting *ThinkTimeConfig `json:"think_time,omitempty" yaml:"think_time,omitempty"` diff --git a/hrp/internal/builtin/utils.go b/hrp/internal/builtin/utils.go index 27098249..d4920084 100644 --- a/hrp/internal/builtin/utils.go +++ b/hrp/internal/builtin/utils.go @@ -295,12 +295,44 @@ func LoadFile(path string, structObj interface{}) (err error) { err = decoder.Decode(structObj) case ".yaml", ".yml": err = yaml.Unmarshal(file, structObj) + case ".env": + err = parseEnvContent(file, structObj) default: err = ErrUnsupportedFileExt } return err } +func parseEnvContent(file []byte, obj interface{}) error { + envMap := obj.(map[string]string) + lines := strings.Split(string(file), "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" || strings.HasPrefix(line, "#") { + // empty line or comment line + continue + } + var kv []string + if strings.Contains(line, "=") { + kv = strings.SplitN(line, "=", 2) + } else if strings.Contains(line, ":") { + kv = strings.SplitN(line, ":", 2) + } + if len(kv) != 2 { + return errors.New(".env format error") + } + + key := strings.TrimSpace(kv[0]) + value := strings.TrimSpace(kv[1]) + envMap[key] = value + + // set env + log.Info().Str("key", key).Msg("set env") + os.Setenv(key, value) + } + return nil +} + func loadFromCSV(path string) []map[string]interface{} { log.Info().Str("path", path).Msg("load csv file") file, err := readFile(path) diff --git a/hrp/internal/scaffold/main.go b/hrp/internal/scaffold/main.go index a0fe5efa..3392d960 100644 --- a/hrp/internal/scaffold/main.go +++ b/hrp/internal/scaffold/main.go @@ -104,7 +104,7 @@ func CreateScaffold(projectName string, pluginType PluginType, force bool) error } projectInfo := &ProjectInfo{ - ProjectName: projectName, + ProjectName: filepath.Base(projectName), ProjectPath: projectPath, CreateTime: time.Now(), Version: version.VERSION, diff --git a/hrp/internal/scaffold/templates/env b/hrp/internal/scaffold/templates/env index 9b5dc360..59ecc742 100644 --- a/hrp/internal/scaffold/templates/env +++ b/hrp/internal/scaffold/templates/env @@ -1,2 +1,3 @@ +base_url=https://postman-echo.com USERNAME=debugtalk PASSWORD=123456 \ No newline at end of file diff --git a/hrp/internal/scaffold/templates/gitignore b/hrp/internal/scaffold/templates/gitignore index 33401380..4c8cb60c 100644 --- a/hrp/internal/scaffold/templates/gitignore +++ b/hrp/internal/scaffold/templates/gitignore @@ -1,4 +1,3 @@ -.env reports/ *.so .vscode/ diff --git a/hrp/internal/scaffold/templates/testcases/demo_requests.json b/hrp/internal/scaffold/templates/testcases/demo_requests.json index b13f3837..9b4214d7 100644 --- a/hrp/internal/scaffold/templates/testcases/demo_requests.json +++ b/hrp/internal/scaffold/templates/testcases/demo_requests.json @@ -17,7 +17,7 @@ { "name": "get with params", "variables": { - "foo1": "bar11", + "foo1": "${ENV(USERNAME)}", "foo2": "bar21", "sum_v": "${sum_two_int(1, 2)}" }, @@ -46,7 +46,7 @@ { "eq": [ "body.args.foo1", - "bar11" + "debugtalk" ] }, { diff --git a/hrp/internal/scaffold/templates/testcases/demo_requests.yml b/hrp/internal/scaffold/templates/testcases/demo_requests.yml index 86d1b9cc..bc9aa108 100644 --- a/hrp/internal/scaffold/templates/testcases/demo_requests.yml +++ b/hrp/internal/scaffold/templates/testcases/demo_requests.yml @@ -5,7 +5,6 @@ config: foo2: config_bar2 expect_foo1: config_bar1 expect_foo2: config_bar2 - base_url: "https://postman-echo.com" verify: False export: ["foo3"] @@ -13,12 +12,12 @@ teststeps: - name: get with params variables: - foo1: bar11 + foo1: ${ENV(USERNAME)} foo2: bar21 sum_v: "${sum_two_int(1, 2)}" request: method: GET - url: /get + url: $base_url/get params: foo1: $foo1 foo2: $foo2 @@ -29,7 +28,7 @@ teststeps: foo3: "body.args.foo2" validate: - eq: ["status_code", 200] - - eq: ["body.args.foo1", "bar11"] + - eq: ["body.args.foo1", "debugtalk"] - eq: ["body.args.sum_v", "3"] - eq: ["body.args.foo2", "bar21"] - @@ -39,7 +38,7 @@ teststeps: foo3: "bar32" request: method: POST - url: /post + url: $base_url/post headers: User-Agent: funplugin/${get_version()} Content-Type: "text/plain" @@ -53,7 +52,7 @@ teststeps: foo2: bar23 request: method: POST - url: /post + url: $base_url/post headers: User-Agent: funplugin/${get_version()} Content-Type: "application/x-www-form-urlencoded" diff --git a/hrp/runner.go b/hrp/runner.go index 62a9a6b7..ca4c9db6 100644 --- a/hrp/runner.go +++ b/hrp/runner.go @@ -287,6 +287,25 @@ func (r *testCaseRunner) parseConfig() error { } r.parsedConfig.BaseURL = convertString(parsedBaseURL) + // merge config environment variables with base_url + // priority: env base_url > base_url + if cfg.Environs != nil { + r.parsedConfig.Environs = cfg.Environs + } else { + r.parsedConfig.Environs = make(map[string]string) + } + if value, ok := r.parsedConfig.Environs["base_url"]; !ok || value == "" { + if r.parsedConfig.BaseURL != "" { + r.parsedConfig.Environs["base_url"] = r.parsedConfig.BaseURL + } + } + + // merge config variables with environment variables + // priority: env > config variables + for k, v := range r.parsedConfig.Environs { + r.parsedConfig.Variables[k] = v + } + // ensure correction of think time config r.parsedConfig.ThinkTimeSetting.checkThinkTime() diff --git a/hrp/step_request.go b/hrp/step_request.go index d4e17e99..39e15d74 100644 --- a/hrp/step_request.go +++ b/hrp/step_request.go @@ -145,7 +145,8 @@ func (r *requestBuilder) prepareUrlParams(stepVariables map[string]interface{}) log.Error().Err(err).Msg("parse request url failed") return err } - rawUrl := buildURL(r.config.BaseURL, convertString(requestUrl)) + baseURL := stepVariables["base_url"].(string) + rawUrl := buildURL(baseURL, convertString(requestUrl)) // prepare request params var queryParams url.Values diff --git a/hrp/testcase.go b/hrp/testcase.go index 66690340..6548ab55 100644 --- a/hrp/testcase.go +++ b/hrp/testcase.go @@ -80,6 +80,25 @@ func (path *TestCasePath) ToTestCase() (*TestCase, error) { return nil, errors.Wrap(err, "failed to get project root dir") } + // load .env file + dotEnvPath := filepath.Join(projectRootDir, ".env") + if builtin.IsFilePathExists(dotEnvPath) { + envVars := make(map[string]string) + err = builtin.LoadFile(dotEnvPath, envVars) + if err != nil { + return nil, errors.Wrap(err, "failed to load .env file") + } + + // override testcase config env with variables loaded from .env file + // priority: .env file > testcase config env + if testCase.Config.Environs == nil { + testCase.Config.Environs = make(map[string]string) + } + for key, value := range envVars { + testCase.Config.Environs[key] = value + } + } + for _, step := range tc.TestSteps { if step.API != nil { apiPath, ok := step.API.(string) diff --git a/httprunner/thrift/data_convertor.py b/httprunner/thrift/data_convertor.py index b25af390..0561ef4a 100644 --- a/httprunner/thrift/data_convertor.py +++ b/httprunner/thrift/data_convertor.py @@ -307,7 +307,7 @@ class MyJSONEncoder(json.JSONEncoder): chunks = self.iterencode(o, _one_shot=True) if not isinstance(chunks, (list, tuple)): chunks = list(chunks) - # add by braver(braver@bytedance.com) + # add by braver # todo: fix 'utf8' codec can't decode byte 0x91 in position 3: invalid start byte" if self.skip_nonutf8_value: # 缺省为false tmp_chunks = [] @@ -324,7 +324,7 @@ class MyJSONEncoder(json.JSONEncoder): class ThriftJSONEncoder(json.JSONEncoder): """ - add by braver(Braver@bytedance.com) + add by braver """ def __init__( @@ -377,7 +377,7 @@ class ThriftJSONEncoder(json.JSONEncoder): chunks = self.iterencode(o, _one_shot=True) if not isinstance(chunks, (list, tuple)): chunks = list(chunks) - # add by braver(braver@bytedance.com) + # add by braver # todo: fix 'utf8' codec can't decode byte 0x91 in position 3: invalid start byte" if self.skip_nonutf8_value: # 缺省为false tmp_chunks = []