This commit is contained in:
debugtalk
2022-05-27 20:44:39 +08:00
58 changed files with 2680 additions and 990 deletions

View File

@@ -9,6 +9,8 @@
- fix #1308: load `.env` file as environment variables - fix #1308: load `.env` file as environment variables
- fix #1309: locate plugin file upward recursively until system root dir - fix #1309: locate plugin file upward recursively until system root dir
- refactor: move base_url to config env - 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`
## v4.1.0-beta (2022-05-21) ## v4.1.0-beta (2022-05-21)

View File

@@ -30,11 +30,11 @@ Copyright 2017 debugtalk
### SEE ALSO ### SEE ALSO
* [hrp boom](hrp_boom.md) - run load test with boomer * [hrp boom](hrp_boom.md) - run load test with boomer
* [hrp convert](hrp_convert.md) - convert JSON/YAML testcases to pytest/gotest scripts * [hrp convert](hrp_convert.md) - convert to JSON/YAML/gotest/pytest testcases
* [hrp har2case](hrp_har2case.md) - convert HAR to json/yaml testcase files * [hrp har2case](hrp_har2case.md) - convert HAR to json/yaml testcase files
* [hrp pytest](hrp_pytest.md) - run API test with pytest * [hrp pytest](hrp_pytest.md) - run API test with pytest
* [hrp run](hrp_run.md) - run API test with go engine * [hrp run](hrp_run.md) - run API test with go engine
* [hrp startproject](hrp_startproject.md) - create a scaffold project * [hrp startproject](hrp_startproject.md) - create a scaffold project
* [hrp wiki](hrp_wiki.md) - visit https://httprunner.com * [hrp wiki](hrp_wiki.md) - visit https://httprunner.com
###### Auto generated by spf13/cobra on 25-May-2022 ###### Auto generated by spf13/cobra on 27-May-2022

View File

@@ -42,4 +42,4 @@ hrp boom [flags]
* [hrp](hrp.md) - Next-Generation API Testing Solution. * [hrp](hrp.md) - Next-Generation API Testing Solution.
###### Auto generated by spf13/cobra on 25-May-2022 ###### Auto generated by spf13/cobra on 27-May-2022

View File

@@ -1,6 +1,6 @@
## hrp convert ## hrp convert
convert JSON/YAML testcases to pytest/gotest scripts convert to JSON/YAML/gotest/pytest testcases
``` ```
hrp convert $path... [flags] hrp convert $path... [flags]
@@ -9,13 +9,17 @@ hrp convert $path... [flags]
### Options ### Options
``` ```
--gotest convert to gotest scripts (TODO) -h, --help help for convert
-h, --help help for convert -d, --output-dir string specify output directory, default to the same dir with har file
--pytest convert to pytest scripts (default true) -p, --profile string specify profile path to override headers (except for auto-generated headers) and cookies
--to-gotest convert to gotest scripts (TODO)
--to-json convert to JSON scripts (default)
--to-pytest convert to pytest scripts
--to-yaml convert to YAML scripts
``` ```
### SEE ALSO ### SEE ALSO
* [hrp](hrp.md) - Next-Generation API Testing Solution. * [hrp](hrp.md) - Next-Generation API Testing Solution.
###### Auto generated by spf13/cobra on 25-May-2022 ###### Auto generated by spf13/cobra on 27-May-2022

View File

@@ -16,7 +16,7 @@ hrp har2case $har_path... [flags]
-h, --help help for har2case -h, --help help for har2case
-d, --output-dir string specify output directory, default to the same dir with har file -d, --output-dir string specify output directory, default to the same dir with har file
-p, --profile string specify profile path to override headers and cookies -p, --profile string specify profile path to override headers and cookies
-j, --to-json convert to JSON format (default true) -j, --to-json convert to JSON format (default)
-y, --to-yaml convert to YAML format -y, --to-yaml convert to YAML format
``` ```
@@ -24,4 +24,4 @@ hrp har2case $har_path... [flags]
* [hrp](hrp.md) - Next-Generation API Testing Solution. * [hrp](hrp.md) - Next-Generation API Testing Solution.
###### Auto generated by spf13/cobra on 25-May-2022 ###### Auto generated by spf13/cobra on 27-May-2022

View File

@@ -16,4 +16,4 @@ hrp pytest $path ... [flags]
* [hrp](hrp.md) - Next-Generation API Testing Solution. * [hrp](hrp.md) - Next-Generation API Testing Solution.
###### Auto generated by spf13/cobra on 25-May-2022 ###### Auto generated by spf13/cobra on 27-May-2022

View File

@@ -35,4 +35,4 @@ hrp run $path... [flags]
* [hrp](hrp.md) - Next-Generation API Testing Solution. * [hrp](hrp.md) - Next-Generation API Testing Solution.
###### Auto generated by spf13/cobra on 25-May-2022 ###### Auto generated by spf13/cobra on 27-May-2022

View File

@@ -9,6 +9,7 @@ hrp startproject $project_name [flags]
### Options ### Options
``` ```
--empty generate empty project
-f, --force force to overwrite existing project -f, --force force to overwrite existing project
--go generate hashicorp go plugin --go generate hashicorp go plugin
-h, --help help for startproject -h, --help help for startproject
@@ -20,4 +21,4 @@ hrp startproject $project_name [flags]
* [hrp](hrp.md) - Next-Generation API Testing Solution. * [hrp](hrp.md) - Next-Generation API Testing Solution.
###### Auto generated by spf13/cobra on 25-May-2022 ###### Auto generated by spf13/cobra on 27-May-2022

View File

@@ -16,4 +16,4 @@ hrp wiki [flags]
* [hrp](hrp.md) - Next-Generation API Testing Solution. * [hrp](hrp.md) - Next-Generation API Testing Solution.
###### Auto generated by spf13/cobra on 25-May-2022 ###### Auto generated by spf13/cobra on 27-May-2022

View File

@@ -1,128 +0,0 @@
{
"config": {
"name": "testcase description"
},
"teststeps": [
{
"name": "",
"request": {
"method": "GET",
"url": "https://postman-echo.com/get",
"params": {
"foo1": "HDnY8",
"foo2": "34.5"
},
"headers": {
"Accept-Encoding": "gzip",
"Host": "postman-echo.com",
"User-Agent": "HttpRunnerPlus"
}
},
"validate": [
{
"check": "status_code",
"assert": "equals",
"expect": 200,
"msg": "assert response status code"
},
{
"check": "headers.\"Content-Type\"",
"assert": "equals",
"expect": "application/json; charset=utf-8",
"msg": "assert response header Content-Type"
},
{
"check": "body.url",
"assert": "equals",
"expect": "https://postman-echo.com/get?foo1=HDnY8\u0026foo2=34.5",
"msg": "assert response body url"
}
]
},
{
"name": "",
"request": {
"method": "POST",
"url": "https://postman-echo.com/post",
"headers": {
"Accept-Encoding": "gzip",
"Content-Length": "28",
"Content-Type": "application/json; charset=UTF-8",
"Host": "postman-echo.com",
"User-Agent": "Go-http-client/1.1"
},
"cookies": {
"sails.sid": "s%3Az_LpglkKxTvJ_eHVUH6V67drKp0AGWW-.PidabaXOnatLRP47hVyqqepl6BdrpEQzRlJQXtbIiwk"
},
"body": {
"foo1": "HDnY8",
"foo2": 12.3
}
},
"validate": [
{
"check": "status_code",
"assert": "equals",
"expect": 200,
"msg": "assert response status code"
},
{
"check": "headers.\"Content-Type\"",
"assert": "equals",
"expect": "application/json; charset=utf-8",
"msg": "assert response header Content-Type"
},
{
"check": "body.url",
"assert": "equals",
"expect": "https://postman-echo.com/post",
"msg": "assert response body url"
}
]
},
{
"name": "",
"request": {
"method": "POST",
"url": "https://postman-echo.com/post",
"headers": {
"Accept-Encoding": "gzip",
"Content-Length": "20",
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
"Host": "postman-echo.com",
"User-Agent": "Go-http-client/1.1"
},
"cookies": {
"sails.sid": "s%3AS5e7w0zQ0xAsCwh9L8T6R7QLYCO7_gtD.r8%2B2w9IWqEIfuVkrZjnxzm2xADIk34zKAWXRPapr%2FAw"
},
"body": "foo1=HDnY8\u0026foo2=12.3"
},
"validate": [
{
"check": "status_code",
"assert": "equals",
"expect": 200,
"msg": "assert response status code"
},
{
"check": "headers.\"Content-Type\"",
"assert": "equals",
"expect": "application/json; charset=utf-8",
"msg": "assert response header Content-Type"
},
{
"check": "body.data",
"assert": "equals",
"expect": "",
"msg": "assert response body data"
},
{
"check": "body.url",
"assert": "equals",
"expect": "https://postman-echo.com/post",
"msg": "assert response body url"
}
]
}
]
}

View File

@@ -1,3 +1,4 @@
override: true
headers: headers:
Content-Type: "application/x-www-form-urlencoded" Content-Type: "application/x-www-form-urlencoded"
cookies: cookies:

View File

@@ -0,0 +1,488 @@
{
"info": {
"_postman_id": "0417a445-b206-4ea2-b1d2-5441afd6c6b9",
"name": "postman collection demo",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
},
"item": [
{
"name": "folder1",
"item": [
{
"name": "folder2",
"item": [
{
"name": "Get with params",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "https://postman-echo.com/:path?k1=v1&k2=v2",
"protocol": "https",
"host": [
"postman-echo",
"com"
],
"path": [
":path"
],
"query": [
{
"key": "k1",
"value": "v1"
},
{
"key": "k2",
"value": "v2"
},
{
"key": "k3",
"value": "v3",
"disabled": true
}
],
"variable": [
{
"key": "path",
"value": "get"
}
]
}
},
"response": [
{
"name": "Get with params case1",
"originalRequest": {
"method": "GET",
"header": [],
"url": {
"raw": "https://postman-echo.com/:path?k1=v1&k2=v2",
"protocol": "https",
"host": [
"postman-echo",
"com"
],
"path": [
":path"
],
"query": [
{
"key": "k1",
"value": "v1"
},
{
"key": "k2",
"value": "v2"
},
{
"key": "k3",
"value": "v3",
"disabled": true
}
],
"variable": [
{
"key": "path",
"value": "get"
}
]
}
},
"status": "OK",
"code": 200,
"_postman_previewlanguage": "json",
"header": [
{
"key": "Date",
"value": "Mon, 16 May 2022 12:12:28 GMT"
},
{
"key": "Content-Type",
"value": "application/json; charset=utf-8"
},
{
"key": "Content-Length",
"value": "508"
},
{
"key": "Connection",
"value": "keep-alive"
},
{
"key": "ETag",
"value": "W/\"1fc-x4EIPFQzoLX0HenCFPx6HNfG0lc\""
},
{
"key": "Vary",
"value": "Accept-Encoding"
},
{
"key": "set-cookie",
"value": "sails.sid=s%3AX2aa_Z7gbcUqIWAjlBkytBRmQ4WCvc3D.pX9Qxh8aO9Ict0BL4CrRhdDJmz81UVmwFsV5Nx30Ils; Path=/; HttpOnly"
}
],
"cookie": [],
"body": "{\n \"args\": {\n \"k1\": \"v1\",\n \"k2\": \"v2\"\n },\n \"headers\": {\n \"x-forwarded-proto\": \"https\",\n \"x-forwarded-port\": \"443\",\n \"host\": \"postman-echo.com\",\n \"user-agent\": \"PostmanRuntime/7.29.0\",\n \"accept\": \"*/*\",\n \"accept-encoding\": \"gzip, deflate, br\",\n \"cookie\": \"Cookie_1=c1; Cookie_2=c2; sails.sid=s%3AGX6aS9b_phvUSUk66w7ZBgWuOPI7IIKT.ayEGTaW4U35eAWyPz%2Fh6Q74DonNcbqw3H5Q5Zv%2BfKMY\"\n },\n \"url\": \"https://postman-echo.com/get?k1=v1&k2=v2\"\n}"
},
{
"name": "Get with params case2",
"originalRequest": {
"method": "GET",
"header": [],
"url": {
"raw": "https://postman-echo.com/:path?k1=v1&k3=v3",
"protocol": "https",
"host": [
"postman-echo",
"com"
],
"path": [
":path"
],
"query": [
{
"key": "k1",
"value": "v1"
},
{
"key": "k2",
"value": "v2",
"disabled": true
},
{
"key": "k3",
"value": "v3"
}
],
"variable": [
{
"key": "path",
"value": "get"
}
]
}
},
"status": "OK",
"code": 200,
"_postman_previewlanguage": "json",
"header": [
{
"key": "Date",
"value": "Mon, 16 May 2022 12:14:04 GMT"
},
{
"key": "Content-Type",
"value": "application/json; charset=utf-8"
},
{
"key": "Content-Length",
"value": "504"
},
{
"key": "Connection",
"value": "keep-alive"
},
{
"key": "ETag",
"value": "W/\"1f8-tMaKs4xmwr+3su3I8mcgR0p+ucw\""
},
{
"key": "Vary",
"value": "Accept-Encoding"
},
{
"key": "set-cookie",
"value": "sails.sid=s%3AMNuX_i0KgaP_KuuMpYB8RtCNipCGJWVw.4ETfPHxE81Omqb6Yli%2FezUU8CXyYBcN3%2Bxkx5htwh8Y; Path=/; HttpOnly"
}
],
"cookie": [],
"body": "{\n \"args\": {\n \"k1\": \"v1\",\n \"k3\": \"v3\"\n },\n \"headers\": {\n \"x-forwarded-proto\": \"https\",\n \"x-forwarded-port\": \"443\",\n \"host\": \"postman-echo.com\",\n \"user-agent\": \"PostmanRuntime/7.29.0\",\n \"accept\": \"*/*\",\n \"accept-encoding\": \"gzip, deflate, br\",\n \"cookie\": \"Cookie_1=c1; Cookie_2=c2; sails.sid=s%3AX2aa_Z7gbcUqIWAjlBkytBRmQ4WCvc3D.pX9Qxh8aO9Ict0BL4CrRhdDJmz81UVmwFsV5Nx30Ils\"\n },\n \"url\": \"https://postman-echo.com/get?k1=v1&k3=v3\"\n}"
}
]
}
]
}
]
},
{
"name": "folder3",
"item": [
{
"name": "Post form-data",
"request": {
"method": "POST",
"header": [],
"body": {
"mode": "formdata",
"formdata": [
{
"key": "k1",
"value": "v1",
"type": "text"
},
{
"key": "k2",
"value": "v2",
"type": "text"
},
{
"key": "k3",
"value": "v3",
"type": "text",
"disabled": true
}
]
},
"url": {
"raw": "https://postman-echo.com/:path",
"protocol": "https",
"host": [
"postman-echo",
"com"
],
"path": [
":path"
],
"variable": [
{
"key": "path",
"value": "post"
}
]
}
},
"response": []
},
{
"name": "Post x-www-form-urlencoded",
"request": {
"method": "POST",
"header": [],
"body": {
"mode": "urlencoded",
"urlencoded": [
{
"key": "k1",
"value": "v1",
"type": "text"
},
{
"key": "k2",
"value": "v2",
"type": "text"
},
{
"key": "k3",
"value": "v3",
"type": "text",
"disabled": true
}
]
},
"url": {
"raw": "https://postman-echo.com/:path",
"protocol": "https",
"host": [
"postman-echo",
"com"
],
"path": [
":path"
],
"variable": [
{
"key": "path",
"value": "post"
}
]
}
},
"response": []
},
{
"name": "Post raw json",
"request": {
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"k1\": \"v1\",\n \"k2\": \"v2\"\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "https://postman-echo.com/:path",
"protocol": "https",
"host": [
"postman-echo",
"com"
],
"path": [
":path"
],
"variable": [
{
"key": "path",
"value": "post"
}
]
}
},
"response": []
},
{
"name": "Post raw text",
"request": {
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": "have a nice day",
"options": {
"raw": {
"language": "text"
}
}
},
"url": {
"raw": "https://postman-echo.com/:path",
"protocol": "https",
"host": [
"postman-echo",
"com"
],
"path": [
":path"
],
"variable": [
{
"key": "path",
"value": "post"
}
]
}
},
"response": []
}
]
},
{
"name": "Get request headers",
"request": {
"method": "GET",
"header": [
{
"key": "User-Agent",
"value": "HttpRunner",
"type": "text"
},
{
"key": "User-Name",
"value": "bbx",
"type": "text",
"disabled": true
},
{
"key": "Connection",
"value": "close",
"type": "text"
}
],
"url": {
"raw": "https://postman-echo.com/:path",
"protocol": "https",
"host": [
"postman-echo",
"com"
],
"path": [
":path"
],
"variable": [
{
"key": "path",
"value": "headers"
}
]
}
},
"response": [
{
"name": "Get request headers case1",
"originalRequest": {
"method": "GET",
"header": [
{
"key": "User-Agent",
"value": "HttpRunner",
"type": "text"
},
{
"key": "User-Name",
"value": "bbx",
"type": "text",
"disabled": true
},
{
"key": "Cookie",
"value": "Cookie_1=c1; Cookie_2=c2; sails.sid=s%3AGX6aS9b_phvUSUk66w7ZBgWuOPI7IIKT.ayEGTaW4U35eAWyPz%2Fh6Q74DonNcbqw3H5Q5Zv%2BfKMY",
"type": "text"
}
],
"url": {
"raw": "https://postman-echo.com/:path",
"protocol": "https",
"host": [
"postman-echo",
"com"
],
"path": [
":path"
],
"variable": [
{
"key": "path",
"value": "headers"
}
]
}
},
"status": "OK",
"code": 200,
"_postman_previewlanguage": "json",
"header": [
{
"key": "Date",
"value": "Mon, 16 May 2022 12:14:25 GMT"
},
{
"key": "Content-Type",
"value": "application/json; charset=utf-8"
},
{
"key": "Content-Length",
"value": "541"
},
{
"key": "Connection",
"value": "keep-alive"
},
{
"key": "ETag",
"value": "W/\"21d-ld5UvFTaRM6lihVnvCj6mZm5Of0\""
},
{
"key": "Vary",
"value": "Accept-Encoding"
}
],
"cookie": [],
"body": "{\n \"headers\": {\n \"x-forwarded-proto\": \"https\",\n \"x-forwarded-port\": \"443\",\n \"host\": \"postman-echo.com\",\n \"user-agent\": \"HttpRunner\",\n \"cookie\": \"Cookie_1=c1; Cookie_2=c2; sails.sid=s%3AGX6aS9b_phvUSUk66w7ZBgWuOPI7IIKT.ayEGTaW4U35eAWyPz%2Fh6Q74DonNcbqw3H5Q5Zv%2BfKMY\",\n \"accept\": \"*/*\",\n \"accept-encoding\": \"gzip, deflate, br\"\n }\n}"
}
]
}
]
}

View File

@@ -0,0 +1,4 @@
headers:
User-Agent: "this header will be created or updated"
cookies:
Cookie1: "this cookie will be created or updated"

View File

@@ -0,0 +1,5 @@
override: true
headers:
Header1: "all original headers will be overridden"
cookies:
Cookie1: "all original cookies will be overridden"

View File

@@ -1,6 +1,6 @@
{ {
"project_name": "demo-with-go-plugin", "project_name": "demo-with-go-plugin",
"project_path": "/Users/debugtalk/MyProjects/HttpRunner-dev/httprunner/examples/demo-with-go-plugin", "project_path": "/Users/xxxxx/go/src/github.com/httprunner/httprunner/examples/demo-with-go-plugin",
"create_time": "2022-05-25T11:14:42.750876+08:00", "create_time": "2022-05-27T11:34:23.903959+08:00",
"hrp_version": "v4.1.0-beta" "hrp_version": "v4.1.0-beta"
} }

View File

@@ -26,8 +26,8 @@ teststeps:
headers: headers:
User-Agent: funplugin/${get_version()} User-Agent: funplugin/${get_version()}
Content-Type: "application/x-www-form-urlencoded" Content-Type: "application/x-www-form-urlencoded"
data: "foo1=$foo1&foo2=$foo3" body: "foo1=$foo1&foo2=$foo3"
validate: validate:
- eq: ["status_code", 200] - eq: ["status_code", 200]
- eq: ["body.form.foo1", "bar1"] - eq: ["body.form.foo1", "bar1"]
- eq: ["body.form.foo2", "bar21"] - eq: ["body.form.foo2", "bar21"]

View File

@@ -38,28 +38,28 @@
}, },
"validate": [ "validate": [
{ {
"eq": [ "check": "status_code",
"status_code", "assert": "equal",
200 "expect": 200,
] "msg": "check status_code"
}, },
{ {
"eq": [ "check": "body.args.foo1",
"body.args.foo1", "assert": "equal",
"debugtalk" "expect": "debugtalk",
] "msg": "check body.args.foo1"
}, },
{ {
"eq": [ "check": "body.args.sum_v",
"body.args.sum_v", "assert": "equal",
"3" "expect": "3",
] "msg": "check body.args.sum_v"
}, },
{ {
"eq": [ "check": "body.args.foo2",
"body.args.foo2", "assert": "equal",
"bar21" "expect": "bar21",
] "msg": "check body.args.foo2"
} }
] ]
}, },
@@ -76,20 +76,20 @@
"User-Agent": "funplugin/${get_version()}", "User-Agent": "funplugin/${get_version()}",
"Content-Type": "text/plain" "Content-Type": "text/plain"
}, },
"data": "This is expected to be sent back as part of response body: $foo1-$foo2-$foo3." "body": "This is expected to be sent back as part of response body: $foo1-$foo2-$foo3."
}, },
"validate": [ "validate": [
{ {
"eq": [ "check": "status_code",
"status_code", "assert": "equal",
200 "expect": 200,
] "msg": "check status_code"
}, },
{ {
"eq": [ "check": "body.data",
"body.data", "assert": "equal",
"This is expected to be sent back as part of response body: bar12-$expect_foo2-bar32." "expect": "This is expected to be sent back as part of response body: bar12-$expect_foo2-bar32.",
] "msg": "check body.data"
} }
] ]
}, },
@@ -105,32 +105,32 @@
"User-Agent": "funplugin/${get_version()}", "User-Agent": "funplugin/${get_version()}",
"Content-Type": "application/x-www-form-urlencoded" "Content-Type": "application/x-www-form-urlencoded"
}, },
"data": "foo1=$foo1&foo2=$foo2&foo3=$foo3" "body": "foo1=$foo1&foo2=$foo2&foo3=$foo3"
}, },
"validate": [ "validate": [
{ {
"eq": [ "check": "status_code",
"status_code", "assert": "equal",
200 "expect": 200,
] "msg": "check status_code"
}, },
{ {
"eq": [ "check": "body.form.foo1",
"body.form.foo1", "assert": "equal",
"$expect_foo1" "expect": "$expect_foo1",
] "msg": "check body.form.foo1"
}, },
{ {
"eq": [ "check": "body.form.foo2",
"body.form.foo2", "assert": "equal",
"bar23" "expect": "bar23",
] "msg": "check body.form.foo2"
}, },
{ {
"eq": [ "check": "body.form.foo3",
"body.form.foo3", "assert": "equal",
"bar21" "expect": "bar21",
] "msg": "check body.form.foo3"
} }
] ]
} }

View File

@@ -42,7 +42,7 @@ teststeps:
headers: headers:
User-Agent: funplugin/${get_version()} User-Agent: funplugin/${get_version()}
Content-Type: "text/plain" Content-Type: "text/plain"
data: "This is expected to be sent back as part of response body: $foo1-$foo2-$foo3." body: "This is expected to be sent back as part of response body: $foo1-$foo2-$foo3."
validate: validate:
- eq: ["status_code", 200] - eq: ["status_code", 200]
- eq: ["body.data", "This is expected to be sent back as part of response body: bar12-$expect_foo2-bar32."] - eq: ["body.data", "This is expected to be sent back as part of response body: bar12-$expect_foo2-bar32."]
@@ -56,7 +56,7 @@ teststeps:
headers: headers:
User-Agent: funplugin/${get_version()} User-Agent: funplugin/${get_version()}
Content-Type: "application/x-www-form-urlencoded" Content-Type: "application/x-www-form-urlencoded"
data: "foo1=$foo1&foo2=$foo2&foo3=$foo3" body: "foo1=$foo1&foo2=$foo2&foo3=$foo3"
validate: validate:
- eq: ["status_code", 200] - eq: ["status_code", 200]
- eq: ["body.form.foo1", "$expect_foo1"] - eq: ["body.form.foo1", "$expect_foo1"]

View File

@@ -1,6 +1,6 @@
{ {
"project_name": "demo-with-py-plugin", "project_name": "demo-with-py-plugin",
"project_path": "/Users/debugtalk/MyProjects/HttpRunner-dev/httprunner/examples/demo-with-py-plugin", "project_path": "/Users/xxxxx/go/src/github.com/httprunner/httprunner/examples/demo-with-py-plugin",
"create_time": "2022-05-25T11:14:52.333942+08:00", "create_time": "2022-05-27T11:34:31.852589+08:00",
"hrp_version": "v4.1.0-beta" "hrp_version": "v4.1.0-beta"
} }

View File

@@ -26,8 +26,8 @@ teststeps:
headers: headers:
User-Agent: funplugin/${get_version()} User-Agent: funplugin/${get_version()}
Content-Type: "application/x-www-form-urlencoded" Content-Type: "application/x-www-form-urlencoded"
data: "foo1=$foo1&foo2=$foo3" body: "foo1=$foo1&foo2=$foo3"
validate: validate:
- eq: ["status_code", 200] - eq: ["status_code", 200]
- eq: ["body.form.foo1", "bar1"] - eq: ["body.form.foo1", "bar1"]
- eq: ["body.form.foo2", "bar21"] - eq: ["body.form.foo2", "bar21"]

View File

@@ -38,28 +38,28 @@
}, },
"validate": [ "validate": [
{ {
"eq": [ "check": "status_code",
"status_code", "assert": "equal",
200 "expect": 200,
] "msg": "check status_code"
}, },
{ {
"eq": [ "check": "body.args.foo1",
"body.args.foo1", "assert": "equal",
"debugtalk" "expect": "debugtalk",
] "msg": "check body.args.foo1"
}, },
{ {
"eq": [ "check": "body.args.sum_v",
"body.args.sum_v", "assert": "equal",
"3" "expect": "3",
] "msg": "check body.args.sum_v"
}, },
{ {
"eq": [ "check": "body.args.foo2",
"body.args.foo2", "assert": "equal",
"bar21" "expect": "bar21",
] "msg": "check body.args.foo2"
} }
] ]
}, },
@@ -76,20 +76,20 @@
"User-Agent": "funplugin/${get_version()}", "User-Agent": "funplugin/${get_version()}",
"Content-Type": "text/plain" "Content-Type": "text/plain"
}, },
"data": "This is expected to be sent back as part of response body: $foo1-$foo2-$foo3." "body": "This is expected to be sent back as part of response body: $foo1-$foo2-$foo3."
}, },
"validate": [ "validate": [
{ {
"eq": [ "check": "status_code",
"status_code", "assert": "equal",
200 "expect": 200,
] "msg": "check status_code"
}, },
{ {
"eq": [ "check": "body.data",
"body.data", "assert": "equal",
"This is expected to be sent back as part of response body: bar12-$expect_foo2-bar32." "expect": "This is expected to be sent back as part of response body: bar12-$expect_foo2-bar32.",
] "msg": "check body.data"
} }
] ]
}, },
@@ -105,32 +105,32 @@
"User-Agent": "funplugin/${get_version()}", "User-Agent": "funplugin/${get_version()}",
"Content-Type": "application/x-www-form-urlencoded" "Content-Type": "application/x-www-form-urlencoded"
}, },
"data": "foo1=$foo1&foo2=$foo2&foo3=$foo3" "body": "foo1=$foo1&foo2=$foo2&foo3=$foo3"
}, },
"validate": [ "validate": [
{ {
"eq": [ "check": "status_code",
"status_code", "assert": "equal",
200 "expect": 200,
] "msg": "check status_code"
}, },
{ {
"eq": [ "check": "body.form.foo1",
"body.form.foo1", "assert": "equal",
"$expect_foo1" "expect": "$expect_foo1",
] "msg": "check body.form.foo1"
}, },
{ {
"eq": [ "check": "body.form.foo2",
"body.form.foo2", "assert": "equal",
"bar23" "expect": "bar23",
] "msg": "check body.form.foo2"
}, },
{ {
"eq": [ "check": "body.form.foo3",
"body.form.foo3", "assert": "equal",
"bar21" "expect": "bar21",
] "msg": "check body.form.foo3"
} }
] ]
} }

View File

@@ -42,7 +42,7 @@ teststeps:
headers: headers:
User-Agent: funplugin/${get_version()} User-Agent: funplugin/${get_version()}
Content-Type: "text/plain" Content-Type: "text/plain"
data: "This is expected to be sent back as part of response body: $foo1-$foo2-$foo3." body: "This is expected to be sent back as part of response body: $foo1-$foo2-$foo3."
validate: validate:
- eq: ["status_code", 200] - eq: ["status_code", 200]
- eq: ["body.data", "This is expected to be sent back as part of response body: bar12-$expect_foo2-bar32."] - eq: ["body.data", "This is expected to be sent back as part of response body: bar12-$expect_foo2-bar32."]
@@ -56,7 +56,7 @@ teststeps:
headers: headers:
User-Agent: funplugin/${get_version()} User-Agent: funplugin/${get_version()}
Content-Type: "application/x-www-form-urlencoded" Content-Type: "application/x-www-form-urlencoded"
data: "foo1=$foo1&foo2=$foo2&foo3=$foo3" body: "foo1=$foo1&foo2=$foo2&foo3=$foo3"
validate: validate:
- eq: ["status_code", 200] - eq: ["status_code", 200]
- eq: ["body.form.foo1", "$expect_foo1"] - eq: ["body.form.foo1", "$expect_foo1"]

View File

@@ -1,6 +1,6 @@
{ {
"project_name": "demo-without-plugin", "project_name": "demo-without-plugin",
"project_path": "/Users/debugtalk/MyProjects/HttpRunner-dev/httprunner/examples/demo-without-plugin", "project_path": "/Users/xxxxx/go/src/github.com/httprunner/httprunner/examples/demo-without-plugin",
"create_time": "2022-05-25T11:14:53.862348+08:00", "create_time": "2022-05-27T11:34:32.548637+08:00",
"hrp_version": "v4.1.0-beta" "hrp_version": "v4.1.0-beta"
} }

1
go.mod
View File

@@ -7,6 +7,7 @@ require (
github.com/denisbrodbeck/machineid v1.0.1 github.com/denisbrodbeck/machineid v1.0.1
github.com/fatih/color v1.13.0 github.com/fatih/color v1.13.0
github.com/getsentry/sentry-go v0.13.0 github.com/getsentry/sentry-go v0.13.0
github.com/go-openapi/spec v0.20.6
github.com/google/uuid v1.3.0 github.com/google/uuid v1.3.0
github.com/gorilla/websocket v1.4.1 github.com/gorilla/websocket v1.4.1
github.com/httprunner/funplugin v0.4.6 github.com/httprunner/funplugin v0.4.6

27
go.sum
View File

@@ -88,6 +88,7 @@ github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM= github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -131,6 +132,16 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8= github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8=
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA=
github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo=
github.com/go-openapi/spec v0.20.6 h1:ich1RQ3WDbfoeTqTAb+5EIxNmpKVJZWBNah9RAT0jIQ=
github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
@@ -262,6 +273,8 @@ github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9Y
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
@@ -288,16 +301,20 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/labstack/echo/v4 v4.5.0/go.mod h1:czIriw4a0C1dFun+ObrXp7ok03xON0N1awStJ6ArI7Y= github.com/labstack/echo/v4 v4.5.0/go.mod h1:czIriw4a0C1dFun+ObrXp7ok03xON0N1awStJ6ArI7Y=
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/maja42/goval v1.2.1 h1:fyEgzddqPgCZsKcFLk4C6SdCHyEaAHYvtZG4mGzQOHU= github.com/maja42/goval v1.2.1 h1:fyEgzddqPgCZsKcFLk4C6SdCHyEaAHYvtZG4mGzQOHU=
github.com/maja42/goval v1.2.1/go.mod h1:42LU+BQXL/veE9jnTTUOSj38GRmOTSThYSXRVodI5J4= github.com/maja42/goval v1.2.1/go.mod h1:42LU+BQXL/veE9jnTTUOSj38GRmOTSThYSXRVodI5J4=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
@@ -348,6 +365,8 @@ github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5Vgl
github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w=
github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw=
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
@@ -837,8 +856,9 @@ google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
@@ -854,6 +874,7 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA= gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA=
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -2,9 +2,7 @@ package cmd
import ( import (
"errors" "errors"
"os"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/httprunner/httprunner/v4/hrp/internal/convert" "github.com/httprunner/httprunner/v4/hrp/internal/convert"
@@ -12,37 +10,52 @@ import (
var convertCmd = &cobra.Command{ var convertCmd = &cobra.Command{
Use: "convert $path...", Use: "convert $path...",
Short: "convert JSON/YAML testcases to pytest/gotest scripts", Short: "convert to JSON/YAML/gotest/pytest testcases",
Args: cobra.ExactValidArgs(1), Args: cobra.MinimumNArgs(1),
PreRun: func(cmd *cobra.Command, args []string) { PreRun: func(cmd *cobra.Command, args []string) {
setLogLevel(logLevel) setLogLevel(logLevel)
}, },
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
if !pytestFlag && !gotestFlag { var flagCount int
return errors.New("please specify convertion type") var outputType convert.OutputType
if toJSONFlag {
flagCount++
} }
if toYAMLFlag {
var err error flagCount++
if gotestFlag { outputType = convert.OutputTypeYAML
err = convert.Convert2TestScripts("gotest", args...)
} else {
err = convert.Convert2TestScripts("pytest", args...)
} }
if err != nil { if toGoTestFlag {
log.Error().Err(err).Msg("convert test scripts failed") flagCount++
os.Exit(1) outputType = convert.OutputTypeGoTest
} }
if toPyTestFlag {
flagCount++
outputType = convert.OutputTypePyTest
}
if flagCount > 1 {
return errors.New("please specify at most one conversion flag")
}
convert.Run(outputType, outputDir, profilePath, args)
return nil return nil
}, },
} }
var ( var (
pytestFlag bool toJSONFlag bool
gotestFlag bool toYAMLFlag bool
toGoTestFlag bool
toPyTestFlag bool
outputDir string
profilePath string
) )
func init() { func init() {
rootCmd.AddCommand(convertCmd) rootCmd.AddCommand(convertCmd)
convertCmd.Flags().BoolVar(&pytestFlag, "pytest", true, "convert to pytest scripts") convertCmd.Flags().BoolVar(&toPyTestFlag, "to-pytest", false, "convert to pytest scripts")
convertCmd.Flags().BoolVar(&gotestFlag, "gotest", false, "convert to gotest scripts (TODO)") convertCmd.Flags().BoolVar(&toGoTestFlag, "to-gotest", false, "convert to gotest scripts (TODO)")
convertCmd.Flags().BoolVar(&toJSONFlag, "to-json", false, "convert to JSON scripts (default)")
convertCmd.Flags().BoolVar(&toYAMLFlag, "to-yaml", false, "convert to YAML scripts")
convertCmd.Flags().StringVarP(&outputDir, "output-dir", "d", "", "specify output directory, default to the same dir with har file")
convertCmd.Flags().StringVarP(&profilePath, "profile", "p", "", "specify profile path to override headers (except for auto-generated headers) and cookies")
} }

View File

@@ -3,10 +3,9 @@ package cmd
import ( import (
"errors" "errors"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/httprunner/httprunner/v4/hrp/internal/har2case" "github.com/httprunner/httprunner/v4/hrp/internal/convert"
) )
// har2caseCmd represents the har2case command // har2caseCmd represents the har2case command
@@ -19,54 +18,34 @@ var har2caseCmd = &cobra.Command{
setLogLevel(logLevel) setLogLevel(logLevel)
}, },
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
var outputFiles []string var flagCount int
for _, arg := range args { var har2caseOutputType convert.OutputType
// must choose one if har2caseGenJSONFlag {
if !genYAMLFlag && !genJSONFlag { flagCount++
return errors.New("please select convert format type")
}
var outputPath string
var err error
har := har2case.NewHAR(arg)
// specify output dir
if outputDir != "" {
har.SetOutputDir(outputDir)
}
// specify profile
if profilePath != "" {
har.SetProfile(profilePath)
}
// generate json/yaml files
if genYAMLFlag {
outputPath, err = har.GenYAML()
} else {
outputPath, err = har.GenJSON() // default
}
if err != nil {
return err
}
outputFiles = append(outputFiles, outputPath)
} }
log.Info().Strs("output", outputFiles).Msg("convert testcase success") if har2caseGenYAMLFlag {
flagCount++
har2caseOutputType = convert.OutputTypeYAML
}
if flagCount > 1 {
return errors.New("please specify at most one conversion flag")
}
convert.Run(har2caseOutputType, har2caseOutputDir, har2caseProfilePath, args)
return nil return nil
}, },
} }
var ( var (
genJSONFlag bool har2caseGenJSONFlag bool
genYAMLFlag bool har2caseGenYAMLFlag bool
outputDir string har2caseOutputDir string
profilePath string har2caseProfilePath string
) )
func init() { func init() {
rootCmd.AddCommand(har2caseCmd) rootCmd.AddCommand(har2caseCmd)
har2caseCmd.Flags().BoolVarP(&genJSONFlag, "to-json", "j", true, "convert to JSON format") har2caseCmd.Flags().BoolVarP(&har2caseGenJSONFlag, "to-json", "j", false, "convert to JSON format (default)")
har2caseCmd.Flags().BoolVarP(&genYAMLFlag, "to-yaml", "y", false, "convert to YAML format") har2caseCmd.Flags().BoolVarP(&har2caseGenYAMLFlag, "to-yaml", "y", false, "convert to YAML format")
har2caseCmd.Flags().StringVarP(&outputDir, "output-dir", "d", "", "specify output directory, default to the same dir with har file") har2caseCmd.Flags().StringVarP(&har2caseOutputDir, "output-dir", "d", "", "specify output directory, default to the same dir with har file")
har2caseCmd.Flags().StringVarP(&profilePath, "profile", "p", "", "specify profile path to override headers and cookies") har2caseCmd.Flags().StringVarP(&har2caseProfilePath, "profile", "p", "", "specify profile path to override headers and cookies")
} }

View File

@@ -24,7 +24,9 @@ var scaffoldCmd = &cobra.Command{
} }
var pluginType scaffold.PluginType var pluginType scaffold.PluginType
if ignorePlugin { if empty {
pluginType = scaffold.Empty
} else if ignorePlugin {
pluginType = scaffold.Ignore pluginType = scaffold.Ignore
} else if genGoPlugin { } else if genGoPlugin {
pluginType = scaffold.Go pluginType = scaffold.Go
@@ -43,6 +45,7 @@ var scaffoldCmd = &cobra.Command{
} }
var ( var (
empty bool
ignorePlugin bool ignorePlugin bool
genPythonPlugin bool genPythonPlugin bool
genGoPlugin bool genGoPlugin bool
@@ -55,4 +58,5 @@ func init() {
scaffoldCmd.Flags().BoolVar(&genPythonPlugin, "py", true, "generate hashicorp python plugin") scaffoldCmd.Flags().BoolVar(&genPythonPlugin, "py", true, "generate hashicorp python plugin")
scaffoldCmd.Flags().BoolVar(&genGoPlugin, "go", false, "generate hashicorp go plugin") scaffoldCmd.Flags().BoolVar(&genGoPlugin, "go", false, "generate hashicorp go plugin")
scaffoldCmd.Flags().BoolVar(&ignorePlugin, "ignore-plugin", false, "ignore function plugin") scaffoldCmd.Flags().BoolVar(&ignorePlugin, "ignore-plugin", false, "ignore function plugin")
scaffoldCmd.Flags().BoolVar(&empty, "empty", false, "generate empty project")
} }

View File

@@ -23,6 +23,7 @@ func SetUlimit(limit uint64) {
} }
rLimit.Cur = limit rLimit.Cur = limit
rLimit.Max = limit
log.Info().Uint64("limit", rLimit.Cur).Msg("set current ulimit") log.Info().Uint64("limit", rLimit.Cur).Msg("set current ulimit")
err = syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rLimit) err = syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rLimit)
if err != nil { if err != nil {

View File

@@ -285,7 +285,8 @@ func LoadFile(path string, structObj interface{}) (err error) {
if err != nil { if err != nil {
return errors.Wrap(err, "read file failed") return errors.Wrap(err, "read file failed")
} }
// remove BOM at the beginning of file
file = bytes.TrimLeft(file, "\xef\xbb\xbf")
ext := filepath.Ext(path) ext := filepath.Ext(path)
switch ext { switch ext {
case ".json", ".har": case ".json", ".har":
@@ -383,3 +384,9 @@ func readFile(path string) ([]byte, error) {
} }
return file, nil return file, nil
} }
func GetOutputNameWithoutExtension(path string) string {
base := filepath.Base(path)
ext := filepath.Ext(base)
return base[0:len(base)-len(ext)] + "_test"
}

View File

@@ -0,0 +1,77 @@
# hrp convert
## 快速上手
```shell
$ hrp convert -h
convert to JSON/YAML/gotest/pytest testcases
Usage:
hrp convert $path... [flags]
Flags:
-h, --help help for convert
-d, --output-dir string specify output directory, default to the same dir with har file
-p, --profile string specify profile path to override headers (except for auto-generated headers) and cookies
--to-gotest convert to gotest scripts (TODO)
--to-json convert to JSON scripts (default true)
--to-pytest convert to pytest scripts
--to-yaml convert to YAML scripts
Global Flags:
--log-json set log to json format
-l, --log-level string set log level (default "INFO")
```
`hrp convert` 指令用于将 HAR/Postman/JMeter/Swagger 文件或 curl/Apache ab 指令转化为 JSON/YAML/gotest/pytest 形态的测试用例,同时也支持测试用例各个形态之间的相互转化。
该指令所有选项的详细说明如下:
1. `--to-json / --to-yaml / --to-gotest / --to-pytest` 用于将输入转化为对应形态的测试用例,四个选项中最多只能指定一个,如果不指定则默认会将输入转化为 JSON 形态的测试用例
2. `--output-dir` 后接测试用例的期望输出目录的路径,用于将转换生成的测试用例输出到对应的文件夹
3. `--profile` 后接 profile 配置文件的路径,目前支持替换(不存在则会创建)或者覆盖输入的外部脚本/测试用例中的 `Headers``Cookies` 信息profile 文件的后缀可以为 `json/yaml/yml`,下面给出两类 profile 配置文件的示例:
- 根据 profile 替换指定的 `Headers``Cookies` 信息
```yaml
headers:
Header1: "this header will be created or updated"
cookies:
Cookie1: "this cookie will be created or updated"
```
- 根据 profile 覆盖原有的 `Headers``Cookies` 信息
```yaml
override: true
headers:
Header1: "all original headers will be overridden"
cookies:
Cookie1: "all original cookies will be overridden"
```
## 注意事项
1. 输出的测试用例文件名格式为 `Postman 工程文件名称(不带拓展名)` + `_test` + `.json/.yaml/.go/.py 后缀`,如果该文件已经存在则会进行覆盖
2. `hrp convert` 可以自动识别输入类型,因此不需要通过选项来手动制定输入类型,如遇到无法识别、不支持或转换失败的情况,则会输出错误日志并跳过,不会影响其他转换过程的正常进行
3. 在 profile 文件中,指定 `override` 字段为 `false/true` 可以选择修改模式为替换/覆盖。需要注意的是,如果不指定该字段则 profile 的默认修改模式为替换模式
4. 输入为 JSON/YAML 测试用例时,良好兼容 Golang/Python 双引擎的请求体、断言格式细微差异,输出的 JSON/YAML 则统一采用 Golang 引擎的风格
## 转换流程图
`hrp convert` 的转换过程流程图如下:
![flow chart](asset/flowgram.png)
## 开发进度
`hrp convert` 当前的开发进度如下:
| from \ to | JSON | YAML | GoTest | PyTest |
|:---------:|:----:|:----:|:------:|:------:|
| HAR | ✅ | ✅ | ❌ | ✅ |
| Postman | ✅ | ✅ | ❌ | ✅ |
| JMeter | ❌ | ❌ | ❌ | ❌ |
| Swagger | ❌ | ❌ | ❌ | ❌ |
| curl | ❌ | ❌ | ❌ | ❌ |
| Apache ab | ❌ | ❌ | ❌ | ❌ |
| JSON | ✅ | ✅ | ❌ | ✅ |
| YAML | ✅ | ✅ | ❌ | ✅ |
| GoTest | ❌ | ❌ | ❌ | ❌ |
| PyTest | ❌ | ❌ | ❌ | ❌ |

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

View File

@@ -0,0 +1,376 @@
package convert
import (
_ "embed"
"fmt"
"os"
"path/filepath"
"reflect"
"github.com/go-openapi/spec"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
"github.com/httprunner/httprunner/v4/hrp"
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
"github.com/httprunner/httprunner/v4/hrp/internal/sdk"
)
const (
suffixJSON = ".json"
suffixYAML = ".yaml"
suffixGoTest = ".go"
suffixPyTest = ".py"
)
type InputType int
const (
InputTypeUnknown InputType = iota // default input type: unknown
InputTypeHAR
InputTypePostman
InputTypeSwagger
InputTypeJMeter
InputTypeJSON
InputTypeYAML
InputTypeGoTest
InputTypePyTest
)
func (inputType InputType) String() string {
switch inputType {
case InputTypeHAR:
return "har"
case InputTypePostman:
return "postman"
case InputTypeSwagger:
return "swagger"
case InputTypeJMeter:
return "jmeter"
case InputTypeJSON:
return "json testcase"
case InputTypeYAML:
return "yaml testcase"
case InputTypeGoTest:
return "gotest script"
case InputTypePyTest:
return "pytest script"
default:
return "unknown"
}
}
type OutputType int
const (
OutputTypeJSON OutputType = iota // default output type: JSON
OutputTypeYAML
OutputTypeGoTest
OutputTypePyTest
)
func (outputType OutputType) String() string {
switch outputType {
case OutputTypeYAML:
return "yaml"
case OutputTypeGoTest:
return "gotest"
case OutputTypePyTest:
return "pytest"
default:
return "json"
}
}
// TCaseConverter holds the common properties of case converter
type TCaseConverter struct {
InputPath string
OutputDir string
Profile *Profile
InputType InputType
OutputType OutputType
CaseHAR *CaseHar
CasePostman *CasePostman
CaseSwagger *spec.Swagger
TCase *hrp.TCase
}
// Profile is used to override or update(create if not existed) original headers and cookies
type Profile struct {
Override bool `json:"override" yaml:"override"`
Headers map[string]string `json:"headers" yaml:"headers"`
Cookies map[string]string `json:"cookies" yaml:"cookies"`
}
func NewTCaseConverter(path string) (tCaseConverter *TCaseConverter) {
tCaseConverter = &TCaseConverter{
InputPath: path,
InputType: InputTypeUnknown,
}
extName := filepath.Ext(path)
if extName == "" {
log.Warn().Msg("extension name should be specified")
return
}
var err error
switch extName {
case ".har":
caseHAR := new(CaseHar)
err = builtin.LoadFile(path, caseHAR)
if err == nil && !reflect.ValueOf(*caseHAR).IsZero() {
tCaseConverter.InputType = InputTypeHAR
tCaseConverter.CaseHAR = caseHAR
}
case ".json":
tCase := new(hrp.TCase)
err = builtin.LoadFile(path, tCase)
if err == nil && !reflect.ValueOf(*tCase).IsZero() {
tCaseConverter.InputType = InputTypeJSON
tCaseConverter.TCase = tCase
break
}
casePostman := new(CasePostman)
err = builtin.LoadFile(path, casePostman)
// deal with postman field name conflict with swagger
descriptionBackup := casePostman.Info.Description
casePostman.Info.Description = ""
if err == nil && !reflect.ValueOf(*casePostman).IsZero() {
tCaseConverter.InputType = InputTypePostman
casePostman.Info.Description = descriptionBackup
tCaseConverter.CasePostman = casePostman
break
}
caseSwagger := new(spec.Swagger)
err = builtin.LoadFile(path, caseSwagger)
if err == nil && !reflect.ValueOf(*caseSwagger).IsZero() {
tCaseConverter.InputType = InputTypeSwagger
tCaseConverter.CaseSwagger = caseSwagger
}
case ".yaml", ".yml":
tCase := new(hrp.TCase)
err = builtin.LoadFile(path, tCase)
if err == nil && !reflect.ValueOf(*tCase).IsZero() {
tCaseConverter.InputType = InputTypeYAML
tCaseConverter.TCase = tCase
break
}
caseSwagger := new(spec.Swagger)
err = builtin.LoadFile(path, caseSwagger)
if err == nil && !reflect.ValueOf(*caseSwagger).IsZero() {
tCaseConverter.InputType = InputTypeSwagger
tCaseConverter.CaseSwagger = caseSwagger
}
case ".go": // TODO
tCaseConverter.InputType = InputTypeGoTest
case ".py": // TODO
tCaseConverter.InputType = InputTypePyTest
case ".jmx": // TODO
tCaseConverter.InputType = InputTypeJMeter
default:
log.Warn().
Str("input path", tCaseConverter.InputPath).
Msgf("unsupported file type: %v", extName)
}
if tCaseConverter.InputType != InputTypeUnknown {
log.Info().
Str("input path", tCaseConverter.InputPath).
Msgf("load case as: %s", tCaseConverter.InputType.String())
} else {
log.Error().Err(err).
Str("input path", tCaseConverter.InputPath).
Msgf("failed to load case")
}
return
}
func (c *TCaseConverter) SetProfile(path string) {
log.Info().Str("input path", c.InputPath).Str("profile", path).Msg("set profile")
profile := new(Profile)
err := builtin.LoadFile(path, profile)
if err != nil {
log.Warn().Str("path", path).
Msg("failed to load profile, ignore!")
return
}
c.Profile = profile
}
func (c *TCaseConverter) SetOutputDir(dir string) {
log.Info().Str("input path", c.InputPath).Str("output directory", dir).Msg("set output directory")
c.OutputDir = dir
}
func (c *TCaseConverter) genOutputPath(suffix string) string {
outFileFullName := builtin.GetOutputNameWithoutExtension(c.InputPath) + suffix
if c.OutputDir != "" {
return filepath.Join(c.OutputDir, outFileFullName)
} else {
return filepath.Join(filepath.Dir(c.InputPath), outFileFullName)
}
// TODO avoid outFileFullName conflict?
}
func (c *TCaseConverter) ToPyTest() (string, error) {
script := convertConfig(c.TCase.Config)
println(script)
return script, nil
}
func convertConfig(config *hrp.TConfig) string {
script := fmt.Sprintf("Config('%s')", config.Name)
if config.Variables != nil {
script += fmt.Sprintf(".variables(**{%v})", config.Variables)
}
if config.BaseURL != "" {
script += fmt.Sprintf(".base_url('%s')", config.BaseURL)
}
if config.Export != nil {
script += fmt.Sprintf(".export(*%v)", config.Export)
}
script += fmt.Sprintf(".verify(%v)", config.Verify)
return script
}
func (c *TCaseConverter) ToGoTest() (string, error) {
return "", nil
}
// ICaseConverter represents all kinds of case converters which could convert case into JSON/YAML/gotest/pytest format
type ICaseConverter interface {
Struct() *TCaseConverter
ToJSON() (string, error)
ToYAML() (string, error)
ToGoTest() (string, error)
ToPyTest() (string, error)
}
func Run(outputType OutputType, outputDir, profilePath string, args []string) {
// report event
sdk.SendEvent(sdk.EventTracking{
Category: "ConvertTests",
Action: fmt.Sprintf("hrp convert --to-%s", outputType.String()),
})
// identify input and load converters
var iCaseConverters []ICaseConverter
for _, arg := range args {
tCaseConverter := NewTCaseConverter(arg)
tCaseConverter.OutputType = outputType
if outputDir != "" {
tCaseConverter.SetOutputDir(outputDir)
}
if profilePath != "" {
tCaseConverter.SetProfile(profilePath)
}
switch tCaseConverter.InputType {
case InputTypeHAR:
iCaseConverters = append(iCaseConverters, NewConverterHAR(tCaseConverter))
case InputTypePostman:
iCaseConverters = append(iCaseConverters, NewConverterPostman(tCaseConverter))
case InputTypeJSON:
iCaseConverters = append(iCaseConverters, NewConverterJSON(tCaseConverter))
case InputTypeYAML:
iCaseConverters = append(iCaseConverters, NewConverterYAML(tCaseConverter))
case InputTypeSwagger, InputTypeJMeter, InputTypeGoTest, InputTypePyTest:
log.Warn().
Str("input path", tCaseConverter.InputPath).
Msg("case type not supported yet, ignore!")
default:
log.Warn().
Str("input path", tCaseConverter.InputPath).
Msg("unknown case type, ignore!")
}
}
// start converting
var outputFiles []string
var err error
for _, iCaseConverter := range iCaseConverters {
log.Info().Str("input path", iCaseConverter.Struct().InputPath).Msg("start converting")
var outputFile string
switch iCaseConverter.Struct().OutputType {
case OutputTypeYAML:
outputFile, err = iCaseConverter.ToYAML()
case OutputTypeGoTest:
outputFile, err = iCaseConverter.ToGoTest()
case OutputTypePyTest:
outputFile, err = iCaseConverter.ToPyTest()
default:
outputFile, err = iCaseConverter.ToJSON()
}
if err != nil {
log.Error().Err(err).
Str("input path", iCaseConverter.Struct().InputPath).
Msg("error occurs during converting")
continue
}
outputFiles = append(outputFiles, outputFile)
}
log.Info().Strs("output files", outputFiles).Msg("conversion completed")
}
func makeTestCaseFromJSONYAML(iCaseConverter ICaseConverter) (*hrp.TCase, error) {
tCase := iCaseConverter.Struct().TCase
if tCase == nil {
return nil, errors.Errorf("empty json/yaml testcase occurs")
}
profile := iCaseConverter.Struct().Profile
if profile == nil {
return tCase, nil
}
for _, step := range tCase.TestSteps {
// override original headers and cookies
if profile.Override {
step.Request.Headers = make(map[string]string)
step.Request.Cookies = make(map[string]string)
}
// update (create if not existed) original headers and cookies
if step.Request.Headers == nil {
step.Request.Headers = make(map[string]string)
}
if step.Request.Cookies == nil {
step.Request.Cookies = make(map[string]string)
}
for k, v := range profile.Headers {
step.Request.Headers[k] = v
}
for k, v := range profile.Cookies {
step.Request.Cookies[k] = v
}
}
return tCase, nil
}
func convertToPyTest(iCaseConverter ICaseConverter) (string, error) {
// convert to temporary json testcase
jsonPath, err := iCaseConverter.ToJSON()
inputType := iCaseConverter.Struct().InputType
if err != nil {
return "", errors.Wrapf(err, "(%s -> pytest step 1) failed to convert to temporary json testcase", inputType.String())
}
defer func() {
if jsonPath != "" {
if err = os.Remove(jsonPath); err != nil {
log.Error().Err(err).Msgf("(%s -> pytest step defer) failed to clean temporary json testcase", inputType.String())
}
}
}()
// convert from temporary json testcase to pytest
converterJSON := NewConverterJSON(NewTCaseConverter(jsonPath))
pyTestPath, err := converterJSON.MakePyTestScript()
if err != nil {
return "", errors.Wrap(err, "(json -> pytest step 2) failed to convert from temporary json testcase to pytest ")
}
// rename resultant pytest
renamedPyTestPath := iCaseConverter.Struct().genOutputPath(suffixPyTest)
err = os.Rename(pyTestPath, renamedPyTestPath)
if err != nil {
log.Error().Err(err).Msg("(json -> pytest step 3) failed to rename the resultant pytest file")
return pyTestPath, nil
}
return renamedPyTestPath, nil
}

View File

@@ -0,0 +1,60 @@
package convert
import (
_ "embed"
"os"
"github.com/rs/zerolog/log"
"github.com/httprunner/httprunner/v4/hrp"
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
)
func convert2GoTestScripts(paths ...string) error {
log.Warn().Msg("convert to gotest scripts is not supported yet")
os.Exit(1)
// TODO
var testCasePaths []hrp.ITestCase
for _, path := range paths {
testCasePath := hrp.TestCasePath(path)
testCasePaths = append(testCasePaths, &testCasePath)
}
testCases, err := hrp.LoadTestCases(testCasePaths...)
if err != nil {
log.Error().Err(err).Msg("failed to load testcases")
return err
}
var pytestPaths []string
for _, testCase := range testCases {
tc := testCase.ToTCase()
converter := TCaseConverter{
TCase: tc,
}
pytestPath, err := converter.ToPyTest()
if err != nil {
log.Error().Err(err).
Str("originPath", tc.Config.Path).
Msg("convert to pytest failed")
continue
}
log.Info().
Str("pytestPath", pytestPath).
Str("originPath", tc.Config.Path).
Msg("convert to pytest success")
pytestPaths = append(pytestPaths, pytestPath)
}
// format pytest scripts with black
python3, err := builtin.EnsurePython3Venv("black")
if err != nil {
return err
}
args := append([]string{"-m", "black"}, pytestPaths...)
return builtin.ExecCommand(python3, args...)
}
//go:embed testcase.tmpl
var testcaseTemplate string

View File

@@ -1,6 +1,22 @@
package har2case package convert
import "time" import (
"encoding/base64"
"fmt"
"net/url"
"sort"
"strings"
"time"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
"github.com/httprunner/httprunner/v4/hrp"
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
"github.com/httprunner/httprunner/v4/hrp/internal/json"
)
// ==================== model definition starts here ====================
/* /*
HTTP Archive (HAR) format HTTP Archive (HAR) format
@@ -8,8 +24,8 @@ https://w3c.github.io/web-performance/specs/HAR/Overview.html
this file is copied from https://github.com/mrichman/hargo/blob/master/types.go this file is copied from https://github.com/mrichman/hargo/blob/master/types.go
*/ */
// Har is a container type for deserialization // CaseHar is a container type for deserialization
type Har struct { type CaseHar struct {
Log Log `json:"log"` Log Log `json:"log"`
} }
@@ -338,3 +354,333 @@ type TestResult struct {
Method string `json:"method"` Method string `json:"method"`
HarFile string `json:"harfile"` HarFile string `json:"harfile"`
} }
// ==================== model definition ends here ====================
func NewConverterHAR(converter *TCaseConverter) *ConverterHAR {
return &ConverterHAR{
converter: converter,
}
}
type ConverterHAR struct {
converter *TCaseConverter
}
func (c *ConverterHAR) Struct() *TCaseConverter {
return c.converter
}
func (c *ConverterHAR) ToJSON() (string, error) {
tCase, err := c.makeTestCase()
if err != nil {
return "", err
}
jsonPath := c.converter.genOutputPath(suffixJSON)
err = builtin.Dump2JSON(tCase, jsonPath)
if err != nil {
return "", err
}
return jsonPath, nil
}
func (c *ConverterHAR) ToYAML() (string, error) {
tCase, err := c.makeTestCase()
if err != nil {
return "", err
}
yamlPath := c.converter.genOutputPath(suffixYAML)
err = builtin.Dump2YAML(tCase, yamlPath)
if err != nil {
return "", err
}
return yamlPath, nil
}
func (c *ConverterHAR) ToGoTest() (string, error) {
//TODO implement me
return "", errors.New("convert from har to gotest scripts is not supported yet")
}
func (c *ConverterHAR) ToPyTest() (string, error) {
return convertToPyTest(c)
}
func (c *ConverterHAR) makeTestCase() (*hrp.TCase, error) {
teststeps, err := c.prepareTestSteps()
if err != nil {
return nil, err
}
tCase := &hrp.TCase{
Config: c.prepareConfig(),
TestSteps: teststeps,
}
err = tCase.MakeCompat()
if err != nil {
return nil, err
}
return tCase, nil
}
func (c *ConverterHAR) load() (*CaseHar, error) {
har := c.converter.CaseHAR
if har == nil {
return nil, errors.New("empty har case occurs")
}
return har, nil
}
func (c *ConverterHAR) prepareConfig() *hrp.TConfig {
return hrp.NewConfig("testcase description").
SetVerifySSL(false)
}
func (c *ConverterHAR) prepareTestSteps() ([]*hrp.TStep, error) {
har, err := c.load()
if err != nil {
return nil, err
}
var steps []*hrp.TStep
for _, entry := range har.Log.Entries {
step, err := c.prepareTestStep(&entry)
if err != nil {
return nil, err
}
steps = append(steps, step)
}
return steps, nil
}
func (c *ConverterHAR) prepareTestStep(entry *Entry) (*hrp.TStep, error) {
log.Info().
Str("method", entry.Request.Method).
Str("url", entry.Request.URL).
Msg("convert teststep")
step := &stepFromHAR{
TStep: hrp.TStep{
Request: &hrp.Request{},
Validators: make([]interface{}, 0),
},
profile: c.converter.Profile,
}
if err := step.makeRequestMethod(entry); err != nil {
return nil, err
}
if err := step.makeRequestURL(entry); err != nil {
return nil, err
}
if err := step.makeRequestParams(entry); err != nil {
return nil, err
}
if err := step.makeRequestCookies(entry); err != nil {
return nil, err
}
if err := step.makeRequestHeaders(entry); err != nil {
return nil, err
}
if err := step.makeRequestBody(entry); err != nil {
return nil, err
}
if err := step.makeValidate(entry); err != nil {
return nil, err
}
return &step.TStep, nil
}
type stepFromHAR struct {
hrp.TStep
profile *Profile
}
func (s *stepFromHAR) makeRequestMethod(entry *Entry) error {
s.Request.Method = hrp.HTTPMethod(entry.Request.Method)
return nil
}
func (s *stepFromHAR) makeRequestURL(entry *Entry) error {
u, err := url.Parse(entry.Request.URL)
if err != nil {
log.Error().Err(err).Msg("make request url failed")
return err
}
s.Request.URL = fmt.Sprintf("%s://%s", u.Scheme, u.Host+u.Path)
return nil
}
func (s *stepFromHAR) makeRequestParams(entry *Entry) error {
s.Request.Params = make(map[string]interface{})
for _, param := range entry.Request.QueryString {
s.Request.Params[param.Name] = param.Value
}
return nil
}
func (s *stepFromHAR) makeRequestCookies(entry *Entry) error {
// use cookies from har
s.Request.Cookies = make(map[string]string)
for _, cookie := range entry.Request.Cookies {
s.Request.Cookies[cookie.Name] = cookie.Value
}
if s.profile == nil {
return nil
}
// override all cookies according to the profile
if s.profile.Override {
s.Request.Cookies = make(map[string]string)
}
// create or update the cookies according to the profile
for k, v := range s.profile.Cookies {
s.Request.Cookies[k] = v
}
return nil
}
func (s *stepFromHAR) makeRequestHeaders(entry *Entry) error {
// use headers from har
s.Request.Headers = make(map[string]string)
for _, header := range entry.Request.Headers {
if strings.EqualFold(header.Name, "cookie") {
continue
}
s.Request.Headers[header.Name] = header.Value
}
if s.profile == nil {
return nil
}
// override all headers according to the profile
if s.profile.Override {
s.Request.Headers = make(map[string]string)
}
// create or update the headers according to the profile
for k, v := range s.profile.Headers {
s.Request.Headers[k] = v
}
return nil
}
func (s *stepFromHAR) makeRequestBody(entry *Entry) error {
mimeType := entry.Request.PostData.MimeType
if mimeType == "" {
// GET/HEAD/DELETE without body
return nil
}
// POST/PUT with body
if strings.HasPrefix(mimeType, "application/json") {
// post json
var body interface{}
if entry.Request.PostData.Text == "" {
body = nil
} else {
err := json.Unmarshal([]byte(entry.Request.PostData.Text), &body)
if err != nil {
log.Error().Err(err).Msg("make request body failed")
return err
}
}
s.Request.Body = body
} else if strings.HasPrefix(mimeType, "application/x-www-form-urlencoded") {
// post form
var paramsList []string
for _, param := range entry.Request.PostData.Params {
paramsList = append(paramsList, fmt.Sprintf("%s=%s", param.Name, param.Value))
}
s.Request.Body = strings.Join(paramsList, "&")
} else if strings.HasPrefix(mimeType, "text/plain") {
// post raw data
s.Request.Body = entry.Request.PostData.Text
} else {
// TODO
log.Error().Msgf("makeRequestBody: Not implemented for mimeType %s", mimeType)
}
return nil
}
func (s *stepFromHAR) makeValidate(entry *Entry) error {
// make validator for response status code
s.Validators = append(s.Validators, hrp.Validator{
Check: "status_code",
Assert: "equals",
Expect: entry.Response.Status,
Message: "assert response status code",
})
// make validators for response headers
for _, header := range entry.Response.Headers {
// assert Content-Type
if strings.EqualFold(header.Name, "Content-Type") {
s.Validators = append(s.Validators, hrp.Validator{
Check: "headers.\"Content-Type\"",
Assert: "equals",
Expect: header.Value,
Message: "assert response header Content-Type",
})
}
}
// make validators for response body
respBody := entry.Response.Content
if respBody.Text == "" {
// response body is empty
return nil
}
if strings.HasPrefix(respBody.MimeType, "application/json") {
var data []byte
var err error
// response body is json
if respBody.Encoding == "base64" {
// decode base64 text
data, err = base64.StdEncoding.DecodeString(respBody.Text)
if err != nil {
return errors.Wrap(err, "decode base64 error")
}
} else if respBody.Encoding == "" {
// no encoding
data = []byte(respBody.Text)
} else {
// other encoding type
return nil
}
// convert to json
var body interface{}
if err = json.Unmarshal(data, &body); err != nil {
return errors.Wrap(err, "json.Unmarshal body error")
}
jsonBody, ok := body.(map[string]interface{})
if !ok {
return fmt.Errorf("response body is not json, not matched with MimeType")
}
// response body is json
keys := make([]string, 0, len(jsonBody))
for k := range jsonBody {
keys = append(keys, k)
}
// sort map keys to keep validators in stable order
sort.Strings(keys)
for _, key := range keys {
value := jsonBody[key]
switch v := value.(type) {
case map[string]interface{}:
continue
case []interface{}:
continue
default:
s.Validators = append(s.Validators, hrp.Validator{
Check: fmt.Sprintf("body.%s", key),
Assert: "equals",
Expect: v,
Message: fmt.Sprintf("assert response body %s", key),
})
}
}
}
return nil
}

View File

@@ -1,4 +1,4 @@
package har2case package convert
import ( import (
"testing" "testing"
@@ -9,13 +9,16 @@ import (
) )
var ( var (
harPath = "../../../examples/data/har/demo.har" harPath = "../../../examples/data/har/demo.har"
harPath2 = "../../../examples/data/har/postman-echo.har" harPath2 = "../../../examples/data/har/postman-echo.har"
profilePath = "../../../examples/data/har/profile.yml" harProfileOverridePath = "../../../examples/data/har/profile_override.yml"
) )
func TestGenJSON(t *testing.T) { var converterHAR = NewConverterHAR(NewTCaseConverter(harPath))
jsonPath, err := NewHAR(harPath).GenJSON() var converterHAR2 = NewConverterHAR(NewTCaseConverter(harPath2))
func TestHAR2JSON(t *testing.T) {
jsonPath, err := converterHAR.ToJSON()
if !assert.NoError(t, err) { if !assert.NoError(t, err) {
t.Fatal() t.Fatal()
} }
@@ -24,8 +27,8 @@ func TestGenJSON(t *testing.T) {
} }
} }
func TestGenYAML(t *testing.T) { func TestHAR2YAML(t *testing.T) {
yamlPath, err := NewHAR(harPath2).GenYAML() yamlPath, err := converterHAR2.ToYAML()
if !assert.NoError(t, err) { if !assert.NoError(t, err) {
t.Fatal() t.Fatal()
} }
@@ -35,8 +38,7 @@ func TestGenYAML(t *testing.T) {
} }
func TestLoadHAR(t *testing.T) { func TestLoadHAR(t *testing.T) {
har := NewHAR(harPath) h, err := converterHAR.load()
h, err := har.load()
if !assert.NoError(t, err) { if !assert.NoError(t, err) {
t.Fatal() t.Fatal()
} }
@@ -49,28 +51,28 @@ func TestLoadHAR(t *testing.T) {
} }
func TestLoadHARWithProfile(t *testing.T) { func TestLoadHARWithProfile(t *testing.T) {
har := NewHAR(harPath) tCaseConverter := NewTCaseConverter(harPath)
har.SetProfile(profilePath) tCaseConverter.SetProfile(harProfileOverridePath)
_, err := har.load() h := NewConverterHAR(tCaseConverter)
_, err := h.load()
if !assert.NoError(t, err) { if !assert.NoError(t, err) {
t.Fatal() t.Fatal()
} }
if !assert.Equal(t, if !assert.Equal(t,
map[string]interface{}{"Content-Type": "application/x-www-form-urlencoded"}, map[string]string{"Content-Type": "application/x-www-form-urlencoded"},
har.profile["headers"]) { h.converter.Profile.Headers) {
t.Fatal() t.Fatal()
} }
if !assert.Equal(t, if !assert.Equal(t,
map[string]interface{}{"UserName": "debugtalk"}, map[string]string{"UserName": "debugtalk"},
har.profile["cookies"]) { h.converter.Profile.Cookies) {
t.Fatal() t.Fatal()
} }
} }
func TestMakeTestCase(t *testing.T) { func TestMakeTestCaseFromHAR(t *testing.T) {
har := NewHAR(harPath) tCase, err := converterHAR.makeTestCase()
tCase, err := har.makeTestCase()
if !assert.NoError(t, err) { if !assert.NoError(t, err) {
t.Fatal() t.Fatal()
} }
@@ -135,21 +137,13 @@ func TestMakeTestCase(t *testing.T) {
} }
} }
func TestGetFilenameWithoutExtension(t *testing.T) {
filename := getFilenameWithoutExtension(harPath2)
if !assert.Equal(t, "postman-echo", filename) {
t.Fatal()
}
}
func TestMakeRequestURL(t *testing.T) { func TestMakeRequestURL(t *testing.T) {
har := NewHAR("")
entry := &Entry{ entry := &Entry{
Request: Request{ Request: Request{
URL: "http://127.0.0.1:8080/api/login", URL: "http://127.0.0.1:8080/api/login",
}, },
} }
step, err := har.prepareTestStep(entry) step, err := converterHAR.prepareTestStep(entry)
if !assert.NoError(t, err) { if !assert.NoError(t, err) {
t.Fatal() t.Fatal()
} }
@@ -160,7 +154,6 @@ func TestMakeRequestURL(t *testing.T) {
} }
func TestMakeRequestHeaders(t *testing.T) { func TestMakeRequestHeaders(t *testing.T) {
har := NewHAR("")
entry := &Entry{ entry := &Entry{
Request: Request{ Request: Request{
Method: "POST", Method: "POST",
@@ -169,7 +162,7 @@ func TestMakeRequestHeaders(t *testing.T) {
}, },
}, },
} }
step, err := har.prepareTestStep(entry) step, err := converterHAR.prepareTestStep(entry)
if !assert.NoError(t, err) { if !assert.NoError(t, err) {
t.Fatal() t.Fatal()
} }
@@ -181,9 +174,10 @@ func TestMakeRequestHeaders(t *testing.T) {
} }
} }
func TestMakeRequestHeadersWithProfile(t *testing.T) { func TestMakeRequestHeadersWithProfileOverride(t *testing.T) {
har := NewHAR("") tCaseConverter := NewTCaseConverter(harPath)
har.SetProfile(profilePath) tCaseConverter.SetProfile(harProfileOverridePath)
h := NewConverterHAR(tCaseConverter)
entry := &Entry{ entry := &Entry{
Request: Request{ Request: Request{
Method: "POST", Method: "POST",
@@ -192,7 +186,7 @@ func TestMakeRequestHeadersWithProfile(t *testing.T) {
}, },
}, },
} }
step, err := har.prepareTestStep(entry) step, err := h.prepareTestStep(entry)
if !assert.NoError(t, err) { if !assert.NoError(t, err) {
t.Fatal() t.Fatal()
} }
@@ -205,7 +199,6 @@ func TestMakeRequestHeadersWithProfile(t *testing.T) {
} }
func TestMakeRequestCookies(t *testing.T) { func TestMakeRequestCookies(t *testing.T) {
har := NewHAR("")
entry := &Entry{ entry := &Entry{
Request: Request{ Request: Request{
Method: "POST", Method: "POST",
@@ -215,7 +208,7 @@ func TestMakeRequestCookies(t *testing.T) {
}, },
}, },
} }
step, err := har.prepareTestStep(entry) step, err := converterHAR.prepareTestStep(entry)
if !assert.NoError(t, err) { if !assert.NoError(t, err) {
t.Fatal() t.Fatal()
} }
@@ -228,9 +221,10 @@ func TestMakeRequestCookies(t *testing.T) {
} }
} }
func TestMakeRequestCookiesWithProfile(t *testing.T) { func TestMakeRequestCookiesWithProfileOverride(t *testing.T) {
har := NewHAR("") tCaseConverter := NewTCaseConverter(harPath)
har.SetProfile(profilePath) tCaseConverter.SetProfile(harProfileOverridePath)
h := NewConverterHAR(tCaseConverter)
entry := &Entry{ entry := &Entry{
Request: Request{ Request: Request{
Method: "POST", Method: "POST",
@@ -240,7 +234,7 @@ func TestMakeRequestCookiesWithProfile(t *testing.T) {
}, },
}, },
} }
step, err := har.prepareTestStep(entry) step, err := h.prepareTestStep(entry)
if !assert.NoError(t, err) { if !assert.NoError(t, err) {
t.Fatal() t.Fatal()
} }
@@ -253,7 +247,6 @@ func TestMakeRequestCookiesWithProfile(t *testing.T) {
} }
func TestMakeRequestDataParams(t *testing.T) { func TestMakeRequestDataParams(t *testing.T) {
har := NewHAR("")
entry := &Entry{ entry := &Entry{
Request: Request{ Request: Request{
Method: "POST", Method: "POST",
@@ -266,7 +259,7 @@ func TestMakeRequestDataParams(t *testing.T) {
}, },
}, },
} }
step, err := har.prepareTestStep(entry) step, err := converterHAR.prepareTestStep(entry)
if !assert.NoError(t, err) { if !assert.NoError(t, err) {
t.Fatal() t.Fatal()
} }
@@ -277,7 +270,6 @@ func TestMakeRequestDataParams(t *testing.T) {
} }
func TestMakeRequestDataJSON(t *testing.T) { func TestMakeRequestDataJSON(t *testing.T) {
har := NewHAR("")
entry := &Entry{ entry := &Entry{
Request: Request{ Request: Request{
Method: "POST", Method: "POST",
@@ -287,7 +279,7 @@ func TestMakeRequestDataJSON(t *testing.T) {
}, },
}, },
} }
step, err := har.prepareTestStep(entry) step, err := converterHAR.prepareTestStep(entry)
if !assert.NoError(t, err) { if !assert.NoError(t, err) {
t.Fatal() t.Fatal()
} }
@@ -298,7 +290,6 @@ func TestMakeRequestDataJSON(t *testing.T) {
} }
func TestMakeRequestDataTextEmpty(t *testing.T) { func TestMakeRequestDataTextEmpty(t *testing.T) {
har := NewHAR("")
entry := &Entry{ entry := &Entry{
Request: Request{ Request: Request{
Method: "POST", Method: "POST",
@@ -308,7 +299,7 @@ func TestMakeRequestDataTextEmpty(t *testing.T) {
}, },
}, },
} }
step, err := har.prepareTestStep(entry) step, err := converterHAR.prepareTestStep(entry)
if !assert.NoError(t, err) { if !assert.NoError(t, err) {
t.Fatal() t.Fatal()
} }
@@ -319,7 +310,6 @@ func TestMakeRequestDataTextEmpty(t *testing.T) {
} }
func TestMakeValidate(t *testing.T) { func TestMakeValidate(t *testing.T) {
har := NewHAR("")
entry := &Entry{ entry := &Entry{
Response: Response{ Response: Response{
Status: 200, Status: 200,
@@ -335,7 +325,7 @@ func TestMakeValidate(t *testing.T) {
}, },
}, },
} }
step, err := har.prepareTestStep(entry) step, err := converterHAR.prepareTestStep(entry)
if !assert.NoError(t, err) { if !assert.NoError(t, err) {
t.Fatal() t.Fatal()
} }

View File

@@ -0,0 +1,86 @@
package convert
import (
"fmt"
"github.com/pkg/errors"
"github.com/httprunner/httprunner/v4/hrp"
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
"github.com/httprunner/httprunner/v4/hrp/internal/version"
)
func NewConverterJSON(converter *TCaseConverter) *ConverterJSON {
return &ConverterJSON{
converter: converter,
}
}
type ConverterJSON struct {
converter *TCaseConverter
}
func (c *ConverterJSON) Struct() *TCaseConverter {
return c.converter
}
func (c *ConverterJSON) ToJSON() (string, error) {
testCase, err := c.makeTestCase()
if err != nil {
return "", err
}
jsonPath := c.converter.genOutputPath(suffixJSON)
err = builtin.Dump2JSON(testCase, jsonPath)
if err != nil {
return "", err
}
return jsonPath, nil
}
func (c *ConverterJSON) ToYAML() (string, error) {
testCase, err := c.makeTestCase()
if err != nil {
return "", err
}
yamlPath := c.converter.genOutputPath(suffixYAML)
err = builtin.Dump2YAML(testCase, yamlPath)
if err != nil {
return "", err
}
return yamlPath, nil
}
func (c *ConverterJSON) ToGoTest() (string, error) {
//TODO implement me
return "", errors.New("convert from json testcase to gotest scripts is not supported yet")
}
func (c *ConverterJSON) ToPyTest() (string, error) {
return convertToPyTest(c)
}
func (c *ConverterJSON) MakePyTestScript() (string, error) {
httprunner := fmt.Sprintf("httprunner>=%s", version.HttpRunnerMinVersion)
python3, err := builtin.EnsurePython3Venv(httprunner)
if err != nil {
return "", err
}
args := append([]string{"-m", "httprunner", "make"}, c.converter.InputPath)
err = builtin.ExecCommand(python3, args...)
if err != nil {
return "", err
}
return c.converter.genOutputPath(suffixPyTest), nil
}
func (c *ConverterJSON) makeTestCase() (*hrp.TCase, error) {
tCase, err := makeTestCaseFromJSONYAML(c)
if err != nil {
return nil, err
}
err = tCase.MakeCompat()
if err != nil {
return nil, err
}
return tCase, nil
}

View File

@@ -0,0 +1,488 @@
package convert
import (
"bytes"
"fmt"
"io"
"mime/multipart"
"net/url"
"os"
"path/filepath"
"reflect"
"strings"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
"github.com/httprunner/httprunner/v4/hrp"
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
"github.com/httprunner/httprunner/v4/hrp/internal/json"
)
// ==================== model definition starts here ====================
/*
Postman Collection format reference:
https://schema.postman.com/json/collection/v2.0.0/collection.json
https://schema.postman.com/json/collection/v2.1.0/collection.json
*/
// CasePostman represents the postman exported file
type CasePostman struct {
Info TInfo `json:"info"`
Items []TItem `json:"item"`
}
// TInfo gives information about the collection
type TInfo struct {
Name string `json:"name"`
Description string `json:"description"`
Schema string `json:"schema"`
}
// TItem contains the detail information of request and expected responses
// item could be defined recursively
type TItem struct {
Items []TItem `json:"item"`
Name string `json:"name"`
Request TRequest `json:"request"`
Responses []TResponse `json:"response"`
}
type TRequest struct {
Method string `json:"method"`
Headers []TField `json:"header"`
Body TBody `json:"body"`
URL TUrl `json:"url"`
Description string `json:"description"`
}
type TResponse struct {
Name string `json:"name"`
OriginalRequest TRequest `json:"originalRequest"`
Status string `json:"status"`
Code int `json:"code"`
Headers []TField `json:"headers"`
Body string `json:"body"`
}
type TUrl struct {
Raw string `json:"raw"`
Protocol string `json:"protocol"`
Path []string `json:"path"`
Description string `json:"description"`
Query []TField `json:"query"`
Variable []TField `json:"variable"`
}
type TField struct {
Key string `json:"key"`
Value string `json:"value"`
Src string `json:"src"`
Description string `json:"description"`
Type string `json:"type"`
Disabled bool `json:"disabled"`
Enable bool `json:"enable"`
}
type TBody struct {
Mode string `json:"mode"`
FormData []TField `json:"formdata"`
URLEncoded []TField `json:"urlencoded"`
Raw string `json:"raw"`
Disabled bool `json:"disabled"`
Options interface{} `json:"options"`
}
// ==================== model definition ends here ====================
const (
enumBodyRaw = "raw"
enumBodyUrlEncoded = "urlencoded"
enumBodyFormData = "formdata"
enumBodyFile = "file"
enumBodyGraphQL = "graphql"
)
const (
enumFieldTypeText = "text"
enumFieldTypeFile = "file"
)
var contentTypeMap = map[string]string{
"text": "text/plain",
"javascript": "application/javascript",
"json": "application/json",
"html": "text/html",
"xml": "application/xml",
}
func NewConverterPostman(converter *TCaseConverter) *ConverterPostman {
return &ConverterPostman{
converter: converter,
}
}
type ConverterPostman struct {
converter *TCaseConverter
}
func (c *ConverterPostman) Struct() *TCaseConverter {
return c.converter
}
func (c *ConverterPostman) ToJSON() (string, error) {
testCase, err := c.makeTestCase()
if err != nil {
return "", err
}
jsonPath := c.converter.genOutputPath(suffixJSON)
err = builtin.Dump2JSON(testCase, jsonPath)
if err != nil {
return "", err
}
return jsonPath, nil
}
func (c *ConverterPostman) ToYAML() (string, error) {
testCase, err := c.makeTestCase()
if err != nil {
return "", err
}
yamlPath := c.converter.genOutputPath(suffixYAML)
err = builtin.Dump2YAML(testCase, yamlPath)
if err != nil {
return "", err
}
return yamlPath, nil
}
func (c *ConverterPostman) ToGoTest() (string, error) {
//TODO implement me
return "", errors.New("convert from postman to gotest scripts is not supported yet")
}
func (c *ConverterPostman) ToPyTest() (string, error) {
return convertToPyTest(c)
}
func (c *ConverterPostman) makeTestCase() (*hrp.TCase, error) {
casePostman, err := c.load()
if err != nil {
return nil, err
}
teststeps, err := c.prepareTestSteps(casePostman)
if err != nil {
return nil, err
}
tCase := &hrp.TCase{
Config: c.prepareConfig(casePostman),
TestSteps: teststeps,
}
err = tCase.MakeCompat()
if err != nil {
return nil, err
}
return tCase, nil
}
func (c *ConverterPostman) load() (*CasePostman, error) {
casePostman := c.converter.CasePostman
if casePostman == nil {
return nil, errors.New("empty postman case occurs")
}
return casePostman, nil
}
func (c *ConverterPostman) prepareConfig(casePostman *CasePostman) *hrp.TConfig {
return hrp.NewConfig(casePostman.Info.Name).
SetVerifySSL(false)
}
func (c *ConverterPostman) prepareTestSteps(casePostman *CasePostman) ([]*hrp.TStep, error) {
// recursively convert collection items into a list
var itemList []TItem
for _, item := range casePostman.Items {
extractItemList(item, &itemList)
}
var steps []*hrp.TStep
for _, item := range itemList {
step, err := c.prepareTestStep(&item, steps)
if err != nil {
return nil, err
}
steps = append(steps, step)
}
return steps, nil
}
func extractItemList(item TItem, itemList *[]TItem) {
// current item contains no other items and request is not empty
if len(item.Items) == 0 {
if !reflect.DeepEqual(item.Request, TRequest{}) {
*itemList = append(*itemList, item)
}
return
}
// look up all items inside
for _, i := range item.Items {
// append item name
i.Name = fmt.Sprintf("%s - %s", item.Name, i.Name)
extractItemList(i, itemList)
}
}
func (c *ConverterPostman) prepareTestStep(item *TItem, steps []*hrp.TStep) (*hrp.TStep, error) {
log.Info().
Str("method", item.Request.Method).
Str("url", item.Request.URL.Raw).
Msg("convert teststep")
step := &stepFromPostman{
TStep: hrp.TStep{
Request: &hrp.Request{},
Validators: make([]interface{}, 0),
},
profile: c.converter.Profile,
}
if err := step.makeRequestName(item); err != nil {
return nil, err
}
if err := step.makeRequestMethod(item); err != nil {
return nil, err
}
if err := step.makeRequestURL(item); err != nil {
return nil, err
}
if err := step.makeRequestParams(item); err != nil {
return nil, err
}
if err := step.makeRequestHeaders(item); err != nil {
return nil, err
}
if err := step.makeRequestCookies(item); err != nil {
return nil, err
}
if err := step.makeRequestBody(item, steps); err != nil {
return nil, err
}
return &step.TStep, nil
}
type stepFromPostman struct {
hrp.TStep
profile *Profile
}
// makeRequestName indicates the step name the same as item name
func (s *stepFromPostman) makeRequestName(item *TItem) error {
s.Name = item.Name
return nil
}
func (s *stepFromPostman) makeRequestMethod(item *TItem) error {
s.Request.Method = hrp.HTTPMethod(item.Request.Method)
return nil
}
func (s *stepFromPostman) makeRequestURL(item *TItem) error {
rawUrl := item.Request.URL.Raw
// parse path variables like ":path" in https://postman-echo.com/:path?k1=v1&k2=v2
for _, field := range item.Request.URL.Variable {
pathVar := ":" + field.Key
rawUrl = strings.Replace(rawUrl, pathVar, field.Value, -1)
}
u, err := url.Parse(rawUrl)
if err != nil {
return errors.Wrap(err, "parse URL error")
}
s.Request.URL = fmt.Sprintf("%s://%s", u.Scheme, u.Host+u.Path)
return nil
}
func (s *stepFromPostman) makeRequestParams(item *TItem) error {
s.Request.Params = make(map[string]interface{})
for _, field := range item.Request.URL.Query {
if field.Disabled {
continue
}
s.Request.Params[field.Key] = field.Value
}
return nil
}
func (s *stepFromPostman) makeRequestHeaders(item *TItem) error {
// headers defined in postman collection
s.Request.Headers = make(map[string]string)
for _, field := range item.Request.Headers {
if field.Disabled || strings.EqualFold(field.Key, "cookie") {
continue
}
s.Request.Headers[field.Key] = field.Value
}
if s.profile == nil {
return nil
}
// override all headers according to the profile
if s.profile.Override {
s.Request.Headers = make(map[string]string)
}
// create or update the headers according to the profile
for k, v := range s.profile.Headers {
s.Request.Headers[k] = v
}
return nil
}
func (s *stepFromPostman) makeRequestCookies(item *TItem) error {
// cookies defined in postman collection
s.Request.Cookies = make(map[string]string)
for _, field := range item.Request.Headers {
if field.Disabled || !strings.EqualFold(field.Key, "cookie") {
continue
}
s.parseRequestCookiesMap(field.Value)
}
if s.profile == nil {
return nil
}
// override all cookies according to the profile
if s.profile.Override {
s.Request.Cookies = make(map[string]string)
}
// create or update the cookies according to the profile
for k, v := range s.profile.Cookies {
s.Request.Cookies[k] = v
}
return nil
}
func (s *stepFromPostman) parseRequestCookiesMap(cookies string) {
for _, cookie := range strings.Split(cookies, ";") {
cookie = strings.TrimSpace(cookie)
index := strings.Index(cookie, "=")
if index == -1 {
log.Warn().Str("cookie", cookie).Msg("cookie format invalid")
continue
}
s.Request.Cookies[cookie[:index]] = cookie[index+1:]
}
}
func (s *stepFromPostman) makeRequestBody(item *TItem, steps []*hrp.TStep) error {
mode := item.Request.Body.Mode
if mode == "" {
return nil
}
switch mode {
case enumBodyRaw:
return s.makeRequestBodyRaw(item)
case enumBodyFormData:
return s.makeRequestBodyFormData(item, steps)
case enumBodyUrlEncoded:
return s.makeRequestBodyUrlEncoded(item)
case enumBodyFile, enumBodyGraphQL:
return errors.Errorf("unsupported body type: %v", mode)
}
return nil
}
func (s *stepFromPostman) makeRequestBodyRaw(item *TItem) (err error) {
defer func() {
if p := recover(); p != nil {
err = fmt.Errorf("make request body (raw) failed: %v", p)
}
}()
// extract language type, default languageType: text
languageType := "text"
iOptions := item.Request.Body.Options
if iOptions != nil {
iLanguage := iOptions.(map[string]interface{})["raw"]
if iLanguage != nil {
languageType = iLanguage.(map[string]interface{})["language"].(string)
}
}
// make request body and indicate Content-Type
rawBody := item.Request.Body.Raw
if languageType == "json" {
var iBody interface{}
err = json.Unmarshal([]byte(rawBody), &iBody)
if err != nil {
return errors.Wrap(err, "make request body (raw -> json) failed")
}
s.Request.Body = iBody
} else {
s.Request.Body = rawBody
}
s.Request.Headers["Content-Type"] = contentTypeMap[languageType]
return
}
func (s *stepFromPostman) makeRequestBodyFormData(item *TItem, steps []*hrp.TStep) (err error) {
defer func() {
if err != nil {
err = errors.Wrap(err, "make request body form-data failed")
}
}()
payload := &bytes.Buffer{}
writer := multipart.NewWriter(payload)
for _, field := range item.Request.Body.FormData {
if field.Disabled {
continue
}
// form data could be text or file
if field.Type == enumFieldTypeText {
err = writer.WriteField(field.Key, field.Value)
if err != nil {
return
}
} else if field.Type == enumFieldTypeFile {
err = writeFormDataFile(writer, &field)
if err != nil {
return
}
}
}
err = writer.Close()
s.Request.Body = payload.String()
s.Request.Headers["Content-Type"] = writer.FormDataContentType()
return
}
func writeFormDataFile(writer *multipart.Writer, field *TField) error {
file, err := os.Open(field.Src)
if err != nil {
return err
}
defer file.Close()
formFile, err := writer.CreateFormFile(field.Key, filepath.Base(field.Src))
if err != nil {
return err
}
_, err = io.Copy(formFile, file)
return err
}
func (s *stepFromPostman) makeRequestBodyUrlEncoded(item *TItem) error {
payloadMap := make(map[string]string)
for _, field := range item.Request.Body.URLEncoded {
if field.Disabled {
continue
}
payloadMap[field.Key] = field.Value
}
s.Request.Body = payloadMap
s.Request.Headers["Content-Type"] = "application/x-www-form-urlencoded"
return nil
}
// TODO makeValidate from example response
func (s *stepFromPostman) makeValidate(item *TItem) error {
return nil
}

View File

@@ -0,0 +1,159 @@
package convert
import (
"testing"
"github.com/stretchr/testify/assert"
)
var (
collectionPath = "../../../examples/data/postman/postman_collection.json"
collectionProfileOverridePath = "../../../examples/data/postman/profile_override.yml"
collectionProfilePath = "../../../examples/data/postman/profile.yml"
)
var converterPostman = NewConverterPostman(NewTCaseConverter(collectionPath))
func TestPostman2JSON(t *testing.T) {
jsonPath, err := converterPostman.ToJSON()
if !assert.NoError(t, err) {
t.Fatal()
}
if !assert.NotEmpty(t, jsonPath) {
t.Fatal()
}
}
func TestPostman2YAML(t *testing.T) {
yamlPath, err := converterPostman.ToYAML()
if !assert.NoError(t, err) {
t.Fatal()
}
if !assert.NotEmpty(t, yamlPath) {
t.Fatal()
}
}
func TestLoadCollection(t *testing.T) {
casePostman, err := converterPostman.load()
if !assert.NoError(t, err) {
t.Fatal(err)
}
if !assert.Equal(t, "postman collection demo", casePostman.Info.Name) {
t.Fatal()
}
}
func TestMakeTestCaseFromCollection(t *testing.T) {
tCase, err := converterPostman.makeTestCase()
if !assert.NoError(t, err) {
t.Fatal()
}
// check name
if !assert.Equal(t, "postman collection demo", tCase.Config.Name) {
t.Fatal()
}
// check method
if !assert.EqualValues(t, "GET", tCase.TestSteps[0].Request.Method) {
t.Fatal()
}
if !assert.EqualValues(t, "POST", tCase.TestSteps[1].Request.Method) {
t.Fatal()
}
// check url
if !assert.Equal(t, "https://postman-echo.com/get", tCase.TestSteps[0].Request.URL) {
t.Fatal()
}
if !assert.Equal(t, "https://postman-echo.com/post", tCase.TestSteps[1].Request.URL) {
t.Fatal()
}
// check params
if !assert.Equal(t, "v1", tCase.TestSteps[0].Request.Params["k1"]) {
t.Fatal()
}
// check cookies (pass, postman collection doesn't contains cookies)
// check headers
if !assert.Contains(t, tCase.TestSteps[1].Request.Headers["Content-Type"], "multipart/form-data") {
t.Fatal()
}
if !assert.Equal(t, "application/x-www-form-urlencoded", tCase.TestSteps[2].Request.Headers["Content-Type"]) {
t.Fatal()
}
if !assert.Equal(t, "application/json", tCase.TestSteps[3].Request.Headers["Content-Type"]) {
t.Fatal()
}
if !assert.Equal(t, "text/plain", tCase.TestSteps[4].Request.Headers["Content-Type"]) {
t.Fatal()
}
if !assert.Equal(t, "HttpRunner", tCase.TestSteps[5].Request.Headers["User-Agent"]) {
t.Fatal()
}
// check body
if !assert.Equal(t, nil, tCase.TestSteps[0].Request.Body) {
t.Fatal()
}
if !assert.NotEmpty(t, tCase.TestSteps[1].Request.Body) {
t.Fatal()
}
if !assert.Equal(t, map[string]string{"k1": "v1", "k2": "v2"}, tCase.TestSteps[2].Request.Body) {
t.Fatal()
}
if !assert.Equal(t, map[string]interface{}{"k1": "v1", "k2": "v2"}, tCase.TestSteps[3].Request.Body) {
t.Fatal()
}
if !assert.Equal(t, "have a nice day", tCase.TestSteps[4].Request.Body) {
t.Fatal()
}
if !assert.Equal(t, nil, tCase.TestSteps[5].Request.Body) {
t.Fatal()
}
}
func TestMakeTestCaseWithProfileOverride(t *testing.T) {
tCaseConverter := NewTCaseConverter(collectionPath)
tCaseConverter.SetProfile(collectionProfileOverridePath)
c := NewConverterPostman(tCaseConverter)
tCase, err := c.makeTestCase()
if !assert.NoError(t, err) {
t.Fatal()
}
for _, step := range tCase.TestSteps {
if step.Request.Method == "GET" && !assert.Len(t, step.Request.Headers, 1) {
t.Fatal()
}
if step.Request.Method == "POST" && !assert.Len(t, step.Request.Headers, 2) {
t.Fatal()
}
if !assert.Equal(t, "all original headers will be overridden", step.Request.Headers["Header1"]) {
t.Fatal()
}
if !assert.Len(t, step.Request.Cookies, 1) {
t.Fatal()
}
if !assert.Equal(t, "all original cookies will be overridden", step.Request.Cookies["Cookie1"]) {
t.Fatal()
}
}
}
func TestMakeTestCaseWithProfile(t *testing.T) {
tCaseConverter := NewTCaseConverter(collectionPath)
tCaseConverter.SetProfile(collectionProfilePath)
c := NewConverterPostman(tCaseConverter)
tCase, err := c.makeTestCase()
if !assert.NoError(t, err) {
t.Fatal()
}
// create cookies Cookie1 indicated in profile
if !assert.Equal(t, "this cookie will be created or updated", tCase.TestSteps[0].Request.Cookies["Cookie1"]) {
t.Fatal()
}
// update header User-Agent indicated in profile
if !assert.Equal(t, "this header will be created or updated", tCase.TestSteps[5].Request.Headers["User-Agent"]) {
t.Fatal()
}
// pass header Connection which is not indicated in profile
if !assert.Equal(t, "close", tCase.TestSteps[5].Request.Headers["Connection"]) {
t.Fatal()
}
}

View File

@@ -0,0 +1,19 @@
package convert
import (
"fmt"
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
"github.com/httprunner/httprunner/v4/hrp/internal/version"
)
func convert2PyTestScripts(paths ...string) error {
httprunner := fmt.Sprintf("httprunner>=%s", version.HttpRunnerMinVersion)
python3, err := builtin.EnsurePython3Venv(httprunner)
if err != nil {
return err
}
args := append([]string{"-m", "httprunner", "make"}, paths...)
return builtin.ExecCommand(python3, args...)
}

View File

@@ -0,0 +1,69 @@
package convert
import (
"github.com/pkg/errors"
"github.com/httprunner/httprunner/v4/hrp"
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
)
func NewConverterYAML(converter *TCaseConverter) *ConverterYAML {
return &ConverterYAML{
converter: converter,
}
}
type ConverterYAML struct {
converter *TCaseConverter
}
func (c *ConverterYAML) Struct() *TCaseConverter {
return c.converter
}
func (c *ConverterYAML) ToJSON() (string, error) {
testCase, err := c.makeTestCase()
if err != nil {
return "", err
}
jsonPath := c.converter.genOutputPath(suffixJSON)
err = builtin.Dump2JSON(testCase, jsonPath)
if err != nil {
return "", err
}
return jsonPath, nil
}
func (c *ConverterYAML) ToYAML() (string, error) {
testCase, err := c.makeTestCase()
if err != nil {
return "", err
}
yamlPath := c.converter.genOutputPath(suffixYAML)
err = builtin.Dump2YAML(testCase, yamlPath)
if err != nil {
return "", err
}
return yamlPath, nil
}
func (c *ConverterYAML) ToGoTest() (string, error) {
//TODO implement me
return "", errors.New("convert from yaml testcase to gotest scripts is not supported yet")
}
func (c *ConverterYAML) ToPyTest() (string, error) {
return convertToPyTest(c)
}
func (c *ConverterYAML) makeTestCase() (*hrp.TCase, error) {
tCase, err := makeTestCaseFromJSONYAML(c)
if err != nil {
return nil, err
}
err = tCase.MakeCompat()
if err != nil {
return nil, err
}
return tCase, nil
}

View File

@@ -1,120 +0,0 @@
package convert
import (
_ "embed"
"fmt"
"os"
"github.com/rs/zerolog/log"
"github.com/httprunner/httprunner/v4/hrp"
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
"github.com/httprunner/httprunner/v4/hrp/internal/sdk"
"github.com/httprunner/httprunner/v4/hrp/internal/version"
)
func Convert2TestScripts(destType string, paths ...string) error {
// report event
sdk.SendEvent(sdk.EventTracking{
Category: "ConvertTests",
Action: fmt.Sprintf("hrp convert --%s", destType),
})
if destType == "gotest" {
return convert2GoTestScripts(paths...)
} else {
// default to pytest
return convert2PyTestScripts(paths...)
}
}
func convert2PyTestScripts(paths ...string) error {
httprunner := fmt.Sprintf("httprunner>=%s", version.HttpRunnerMinVersion)
python3, err := builtin.EnsurePython3Venv(httprunner)
if err != nil {
return err
}
args := append([]string{"-m", "httprunner", "make"}, paths...)
return builtin.ExecCommand(python3, args...)
}
func convert2GoTestScripts(paths ...string) error {
log.Warn().Msg("convert to gotest scripts is not supported yet")
os.Exit(1)
// TODO
var testCasePaths []hrp.ITestCase
for _, path := range paths {
testCasePath := hrp.TestCasePath(path)
testCasePaths = append(testCasePaths, &testCasePath)
}
testCases, err := hrp.LoadTestCases(testCasePaths...)
if err != nil {
log.Error().Err(err).Msg("failed to load testcases")
return err
}
var pytestPaths []string
for _, testCase := range testCases {
tc := testCase.ToTCase()
converter := CaseConverter{
TCase: tc,
}
pytestPath, err := converter.ToPyTest()
if err != nil {
log.Error().Err(err).
Str("originPath", tc.Config.Path).
Msg("convert to pytest failed")
continue
}
log.Info().
Str("pytestPath", pytestPath).
Str("originPath", tc.Config.Path).
Msg("convert to pytest success")
pytestPaths = append(pytestPaths, pytestPath)
}
// format pytest scripts with black
python3, err := builtin.EnsurePython3Venv("black")
if err != nil {
return err
}
args := append([]string{"-m", "black"}, pytestPaths...)
return builtin.ExecCommand(python3, args...)
}
//go:embed testcase.tmpl
var testcaseTemplate string
type CaseConverter struct {
*hrp.TCase
}
func (c *CaseConverter) ToPyTest() (string, error) {
script := convertConfig(c.TCase.Config)
println(script)
return script, nil
}
func (c *CaseConverter) ToGoTest() (string, error) {
return "", nil
}
func convertConfig(config *hrp.TConfig) string {
script := fmt.Sprintf("Config('%s')", config.Name)
if config.Variables != nil {
script += fmt.Sprintf(".variables(**{%v})", config.Variables)
}
if config.BaseURL != "" {
script += fmt.Sprintf(".base_url('%s')", config.BaseURL)
}
if config.Export != nil {
script += fmt.Sprintf(".export(*%v)", config.Export)
}
script += fmt.Sprintf(".verify(%v)", config.Verify)
return script
}

View File

@@ -1,9 +0,0 @@
# har2case
Convert HAR(HTTP Archive) to YAML/JSON testcases for HttpRunner and HttpRunner+.
## Install
## Quick Start
## Examples

View File

@@ -1,385 +0,0 @@
package har2case
import (
"encoding/base64"
"fmt"
"net/url"
"path/filepath"
"sort"
"strings"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
"github.com/httprunner/httprunner/v4/hrp"
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
"github.com/httprunner/httprunner/v4/hrp/internal/json"
"github.com/httprunner/httprunner/v4/hrp/internal/sdk"
)
const (
suffixJSON = ".json"
suffixYAML = ".yaml"
)
func NewHAR(path string) *har {
return &har{
path: path,
}
}
type har struct {
path string
filterStr string
excludeStr string
profile map[string]interface{}
outputDir string
}
func (h *har) SetProfile(path string) {
log.Info().Str("path", path).Msg("set profile")
h.profile = make(map[string]interface{})
err := builtin.LoadFile(path, h.profile)
if err != nil {
log.Warn().Str("path", path).
Msg("invalid profile format, ignore!")
}
}
func (h *har) SetOutputDir(dir string) {
log.Info().Str("dir", dir).Msg("set output directory")
h.outputDir = dir
}
func (h *har) GenJSON() (jsonPath string, err error) {
event := sdk.EventTracking{
Category: "ConvertTests",
Action: "hrp har2case --to-json",
}
// report start event
go sdk.SendEvent(event)
// report running timing event
defer sdk.SendEvent(event.StartTiming("execution"))
tCase, err := h.makeTestCase()
if err != nil {
return "", err
}
jsonPath = h.genOutputPath(suffixJSON)
err = builtin.Dump2JSON(tCase, jsonPath)
return
}
func (h *har) GenYAML() (yamlPath string, err error) {
event := sdk.EventTracking{
Category: "ConvertTests",
Action: "hrp har2case --to-yaml",
}
// report start event
go sdk.SendEvent(event)
// report running timing event
defer sdk.SendEvent(event.StartTiming("execution"))
tCase, err := h.makeTestCase()
if err != nil {
return "", err
}
yamlPath = h.genOutputPath(suffixYAML)
err = builtin.Dump2YAML(tCase, yamlPath)
return
}
func (h *har) makeTestCase() (*hrp.TCase, error) {
teststeps, err := h.prepareTestSteps()
if err != nil {
return nil, err
}
tCase := &hrp.TCase{
Config: h.prepareConfig(),
TestSteps: teststeps,
}
return tCase, nil
}
func (h *har) load() (*Har, error) {
har := &Har{}
err := builtin.LoadFile(h.path, har)
if err != nil {
return nil, errors.Wrap(err, "load har failed")
}
return har, nil
}
func (h *har) prepareConfig() *hrp.TConfig {
return hrp.NewConfig("testcase description").
SetVerifySSL(false)
}
func (h *har) prepareTestSteps() ([]*hrp.TStep, error) {
har, err := h.load()
if err != nil {
return nil, err
}
var steps []*hrp.TStep
for _, entry := range har.Log.Entries {
step, err := h.prepareTestStep(&entry)
if err != nil {
return nil, err
}
steps = append(steps, step)
}
return steps, nil
}
func (h *har) prepareTestStep(entry *Entry) (*hrp.TStep, error) {
log.Info().
Str("method", entry.Request.Method).
Str("url", entry.Request.URL).
Msg("convert teststep")
step := &tStep{
TStep: hrp.TStep{
Request: &hrp.Request{},
Validators: make([]interface{}, 0),
},
profile: h.profile,
}
if err := step.makeRequestMethod(entry); err != nil {
return nil, err
}
if err := step.makeRequestURL(entry); err != nil {
return nil, err
}
if err := step.makeRequestParams(entry); err != nil {
return nil, err
}
if err := step.makeRequestCookies(entry); err != nil {
return nil, err
}
if err := step.makeRequestHeaders(entry); err != nil {
return nil, err
}
if err := step.makeRequestBody(entry); err != nil {
return nil, err
}
if err := step.makeValidate(entry); err != nil {
return nil, err
}
return &step.TStep, nil
}
type tStep struct {
hrp.TStep
profile map[string]interface{}
}
func (s *tStep) makeRequestMethod(entry *Entry) error {
s.Request.Method = hrp.HTTPMethod(entry.Request.Method)
return nil
}
func (s *tStep) makeRequestURL(entry *Entry) error {
u, err := url.Parse(entry.Request.URL)
if err != nil {
log.Error().Err(err).Msg("make request url failed")
return err
}
s.Request.URL = fmt.Sprintf("%s://%s", u.Scheme, u.Host+u.Path)
return nil
}
func (s *tStep) makeRequestParams(entry *Entry) error {
s.Request.Params = make(map[string]interface{})
for _, param := range entry.Request.QueryString {
s.Request.Params[param.Name] = param.Value
}
return nil
}
func (s *tStep) makeRequestCookies(entry *Entry) error {
s.Request.Cookies = make(map[string]string)
cookies, ok := s.profile["cookies"]
if ok {
// use cookies from profile
cookies, ok := cookies.(map[string]interface{})
if ok {
for k, v := range cookies {
s.Request.Cookies[k] = fmt.Sprintf("%v", v)
}
return nil
}
log.Warn().Interface("cookies", cookies).
Msg("cookies from profile is not a map, ignore!")
}
// use cookies from har
for _, cookie := range entry.Request.Cookies {
s.Request.Cookies[cookie.Name] = cookie.Value
}
return nil
}
func (s *tStep) makeRequestHeaders(entry *Entry) error {
s.Request.Headers = make(map[string]string)
headers, ok := s.profile["headers"]
if ok {
// use headers from profile
cookies, ok := headers.(map[string]interface{})
if ok {
for k, v := range cookies {
s.Request.Headers[k] = fmt.Sprintf("%v", v)
}
return nil
}
log.Warn().Interface("headers", headers).
Msg("headers from profile is not a map, ignore!")
}
// use headers from har
for _, header := range entry.Request.Headers {
if strings.EqualFold(header.Name, "cookie") {
continue
}
s.Request.Headers[header.Name] = header.Value
}
return nil
}
func (s *tStep) makeRequestBody(entry *Entry) error {
mimeType := entry.Request.PostData.MimeType
if mimeType == "" {
// GET/HEAD/DELETE without body
return nil
}
// POST/PUT with body
if strings.HasPrefix(mimeType, "application/json") {
// post json
var body interface{}
if entry.Request.PostData.Text == "" {
body = nil
} else {
err := json.Unmarshal([]byte(entry.Request.PostData.Text), &body)
if err != nil {
log.Error().Err(err).Msg("make request body failed")
return err
}
}
s.Request.Body = body
} else if strings.HasPrefix(mimeType, "application/x-www-form-urlencoded") {
// post form
var paramsList []string
for _, param := range entry.Request.PostData.Params {
paramsList = append(paramsList, fmt.Sprintf("%s=%s", param.Name, param.Value))
}
s.Request.Body = strings.Join(paramsList, "&")
} else if strings.HasPrefix(mimeType, "text/plain") {
// post raw data
s.Request.Body = entry.Request.PostData.Text
} else {
// TODO
log.Error().Msgf("makeRequestBody: Not implemented for mimeType %s", mimeType)
}
return nil
}
func (s *tStep) makeValidate(entry *Entry) error {
// make validator for response status code
s.Validators = append(s.Validators, hrp.Validator{
Check: "status_code",
Assert: "equals",
Expect: entry.Response.Status,
Message: "assert response status code",
})
// make validators for response headers
for _, header := range entry.Response.Headers {
// assert Content-Type
if strings.EqualFold(header.Name, "Content-Type") {
s.Validators = append(s.Validators, hrp.Validator{
Check: "headers.\"Content-Type\"",
Assert: "equals",
Expect: header.Value,
Message: "assert response header Content-Type",
})
}
}
// make validators for response body
respBody := entry.Response.Content
if respBody.Text == "" {
// response body is empty
return nil
}
if strings.HasPrefix(respBody.MimeType, "application/json") {
var data []byte
var err error
// response body is json
if respBody.Encoding == "base64" {
// decode base64 text
data, err = base64.StdEncoding.DecodeString(respBody.Text)
if err != nil {
return errors.Wrap(err, "decode base64 error")
}
} else if respBody.Encoding == "" {
// no encoding
data = []byte(respBody.Text)
} else {
// other encoding type
return nil
}
// convert to json
var body interface{}
if err = json.Unmarshal(data, &body); err != nil {
return errors.Wrap(err, "json.Unmarshal body error")
}
jsonBody, ok := body.(map[string]interface{})
if !ok {
return fmt.Errorf("response body is not json, not matched with MimeType")
}
// response body is json
keys := make([]string, 0, len(jsonBody))
for k := range jsonBody {
keys = append(keys, k)
}
// sort map keys to keep validators in stable order
sort.Strings(keys)
for _, key := range keys {
value := jsonBody[key]
switch v := value.(type) {
case map[string]interface{}:
continue
case []interface{}:
continue
default:
s.Validators = append(s.Validators, hrp.Validator{
Check: fmt.Sprintf("body.%s", key),
Assert: "equals",
Expect: v,
Message: fmt.Sprintf("assert response body %s", key),
})
}
}
}
return nil
}
func (h *har) genOutputPath(suffix string) string {
file := getFilenameWithoutExtension(h.path) + suffix
if h.outputDir != "" {
return filepath.Join(h.outputDir, file)
} else {
return filepath.Join(filepath.Dir(h.path), file)
}
}
func getFilenameWithoutExtension(path string) string {
base := filepath.Base(path)
ext := filepath.Ext(base)
return base[0 : len(base)-len(ext)]
}

View File

@@ -22,4 +22,10 @@ func TestGenDemoExamples(t *testing.T) {
if err != nil { if err != nil {
t.Fatal() t.Fatal()
} }
dir = "../../../examples/empty-demo-without-plugin"
err = CreateScaffold(dir, Empty, true)
if err != nil {
t.Fatal()
}
} }

View File

@@ -20,6 +20,7 @@ import (
type PluginType string type PluginType string
const ( const (
Empty PluginType = "empty"
Ignore PluginType = "ignore" Ignore PluginType = "ignore"
Py PluginType = "py" Py PluginType = "py"
Go PluginType = "go" Go PluginType = "go"
@@ -127,8 +128,17 @@ func CreateScaffold(projectName string, pluginType PluginType, force bool) error
return err return err
} }
// create demo testcases // create project testcases
if pluginType == Ignore { if pluginType == Empty {
// create empty project
err := CopyFile("templates/testcases/demo_empty_request.json",
filepath.Join(projectName, "testcases", "requests.json"))
if err != nil {
return err
}
return nil
} else if pluginType == Ignore {
// create project without funplugin
err := CopyFile("templates/testcases/demo_without_funplugin.json", err := CopyFile("templates/testcases/demo_without_funplugin.json",
filepath.Join(projectName, "testcases", "requests.json")) filepath.Join(projectName, "testcases", "requests.json"))
if err != nil { if err != nil {
@@ -138,6 +148,7 @@ func CreateScaffold(projectName string, pluginType PluginType, force bool) error
return nil return nil
} }
// create project with funplugin
err = CopyFile("templates/testcases/demo_with_funplugin.json", err = CopyFile("templates/testcases/demo_with_funplugin.json",
filepath.Join(projectName, "testcases", "demo.json")) filepath.Join(projectName, "testcases", "demo.json"))
if err != nil { if err != nil {

View File

@@ -0,0 +1,25 @@
{
"config": {
"name": "request methods testcase: empty testcase",
"variables": null,
"verify": false
},
"teststeps": [
{
"name": "",
"variables": null,
"request": {
"method": "GET",
"url": "https://"
},
"validate": [
{
"check": "status_code",
"assert": "equal",
"expect": 200,
"msg": "check status_code"
}
]
}
]
}

View File

@@ -0,0 +1,13 @@
config:
name: "request methods testcase: empty testcase"
variables:
verify: False
teststeps:
- name:
variables:
request:
method: GET
url: "https://"
validate:
- eq: ["status_code", 200]

View File

@@ -8,16 +8,14 @@
"app_version": "2.8.6" "app_version": "2.8.6"
}, },
"base_url": "https://postman-echo.com", "base_url": "https://postman-echo.com",
"herader": [ "headers": {
{ "Accept": "*/*",
"Accept": "*/*", "Accept-Encoding": "gzip, deflate, br",
"Accept-Encoding": "gzip, deflate, br", "Cache-Control": "no-cache",
"Cache-Control": "no-cache", "Connection": "keep-alive",
"Connection": "keep-alive", "Host": "postman-echo.com",
"Host": "postman-echo.com", "User-Agent": "PostmanRuntime/7.28.4"
"User-Agent": "PostmanRuntime/7.28.4" },
}
],
"verify": false, "verify": false,
"export": [ "export": [
"session_token" "session_token"
@@ -48,16 +46,16 @@
}, },
"validate": [ "validate": [
{ {
"eq": [ "check": "status_code",
"status_code", "assert": "equal",
200 "expect": 200,
] "msg": "check status_code"
}, },
{ {
"eq": [ "check": "body.headers.postman-token",
"body.headers.postman-token", "assert": "equal",
"ea19464c-ddd4-4724-abe9-5e2b254c2723" "expect": "ea19464c-ddd4-4724-abe9-5e2b254c2723",
] "msg": "check body.headers.postman-token"
} }
] ]
}, },

View File

@@ -26,8 +26,8 @@ teststeps:
headers: headers:
User-Agent: funplugin/${get_version()} User-Agent: funplugin/${get_version()}
Content-Type: "application/x-www-form-urlencoded" Content-Type: "application/x-www-form-urlencoded"
data: "foo1=$foo1&foo2=$foo3" body: "foo1=$foo1&foo2=$foo3"
validate: validate:
- eq: ["status_code", 200] - eq: ["status_code", 200]
- eq: ["body.form.foo1", "bar1"] - eq: ["body.form.foo1", "bar1"]
- eq: ["body.form.foo2", "bar21"] - eq: ["body.form.foo2", "bar21"]

View File

@@ -38,28 +38,28 @@
}, },
"validate": [ "validate": [
{ {
"eq": [ "check": "status_code",
"status_code", "assert": "equal",
200 "expect": 200,
] "msg": "check status_code"
}, },
{ {
"eq": [ "check": "body.args.foo1",
"body.args.foo1", "assert": "equal",
"debugtalk" "expect": "debugtalk",
] "msg": "check body.args.foo1"
}, },
{ {
"eq": [ "check": "body.args.sum_v",
"body.args.sum_v", "assert": "equal",
"3" "expect": "3",
] "msg": "check body.args.sum_v"
}, },
{ {
"eq": [ "check": "body.args.foo2",
"body.args.foo2", "assert": "equal",
"bar21" "expect": "bar21",
] "msg": "check body.args.foo2"
} }
] ]
}, },
@@ -76,20 +76,20 @@
"User-Agent": "funplugin/${get_version()}", "User-Agent": "funplugin/${get_version()}",
"Content-Type": "text/plain" "Content-Type": "text/plain"
}, },
"data": "This is expected to be sent back as part of response body: $foo1-$foo2-$foo3." "body": "This is expected to be sent back as part of response body: $foo1-$foo2-$foo3."
}, },
"validate": [ "validate": [
{ {
"eq": [ "check": "status_code",
"status_code", "assert": "equal",
200 "expect": 200,
] "msg": "check status_code"
}, },
{ {
"eq": [ "check": "body.data",
"body.data", "assert": "equal",
"This is expected to be sent back as part of response body: bar12-$expect_foo2-bar32." "expect": "This is expected to be sent back as part of response body: bar12-$expect_foo2-bar32.",
] "msg": "check body.data"
} }
] ]
}, },
@@ -105,32 +105,32 @@
"User-Agent": "funplugin/${get_version()}", "User-Agent": "funplugin/${get_version()}",
"Content-Type": "application/x-www-form-urlencoded" "Content-Type": "application/x-www-form-urlencoded"
}, },
"data": "foo1=$foo1&foo2=$foo2&foo3=$foo3" "body": "foo1=$foo1&foo2=$foo2&foo3=$foo3"
}, },
"validate": [ "validate": [
{ {
"eq": [ "check": "status_code",
"status_code", "assert": "equal",
200 "expect": 200,
] "msg": "check status_code"
}, },
{ {
"eq": [ "check": "body.form.foo1",
"body.form.foo1", "assert": "equal",
"$expect_foo1" "expect": "$expect_foo1",
] "msg": "check body.form.foo1"
}, },
{ {
"eq": [ "check": "body.form.foo2",
"body.form.foo2", "assert": "equal",
"bar23" "expect": "bar23",
] "msg": "check body.form.foo2"
}, },
{ {
"eq": [ "check": "body.form.foo3",
"body.form.foo3", "assert": "equal",
"bar21" "expect": "bar21",
] "msg": "check body.form.foo3"
} }
] ]
} }

View File

@@ -42,7 +42,7 @@ teststeps:
headers: headers:
User-Agent: funplugin/${get_version()} User-Agent: funplugin/${get_version()}
Content-Type: "text/plain" Content-Type: "text/plain"
data: "This is expected to be sent back as part of response body: $foo1-$foo2-$foo3." body: "This is expected to be sent back as part of response body: $foo1-$foo2-$foo3."
validate: validate:
- eq: ["status_code", 200] - eq: ["status_code", 200]
- eq: ["body.data", "This is expected to be sent back as part of response body: bar12-$expect_foo2-bar32."] - eq: ["body.data", "This is expected to be sent back as part of response body: bar12-$expect_foo2-bar32."]
@@ -56,7 +56,7 @@ teststeps:
headers: headers:
User-Agent: funplugin/${get_version()} User-Agent: funplugin/${get_version()}
Content-Type: "application/x-www-form-urlencoded" Content-Type: "application/x-www-form-urlencoded"
data: "foo1=$foo1&foo2=$foo2&foo3=$foo3" body: "foo1=$foo1&foo2=$foo2&foo3=$foo3"
validate: validate:
- eq: ["status_code", 200] - eq: ["status_code", 200]
- eq: ["body.form.foo1", "$expect_foo1"] - eq: ["body.form.foo1", "$expect_foo1"]

View File

@@ -17,6 +17,7 @@ const (
goPluginFile = "debugtalk.so" // built from go plugin goPluginFile = "debugtalk.so" // built from go plugin
hashicorpGoPluginFile = "debugtalk.bin" // built from hashicorp go plugin hashicorpGoPluginFile = "debugtalk.bin" // built from hashicorp go plugin
hashicorpPyPluginFile = "debugtalk.py" // used for hashicorp python plugin hashicorpPyPluginFile = "debugtalk.py" // used for hashicorp python plugin
projectInfoFile = "proj.json" // used for ensuring root project
) )
func initPlugin(path string, logOn bool) (plugin funplugin.IPlugin, pluginDir string, err error) { func initPlugin(path string, logOn bool) (plugin funplugin.IPlugin, pluginDir string, err error) {
@@ -118,9 +119,15 @@ func GetProjectRootDirPath(path string) (rootDir string, err error) {
rootDir = filepath.Dir(pluginPath) rootDir = filepath.Dir(pluginPath)
return return
} }
// fix: no debugtalk file in project but having proj.json created by startpeoject
projPath, err := locateFile(path, projectInfoFile)
if err == nil {
rootDir = filepath.Dir(projPath)
return
}
// failed to locate project root dir // failed to locate project root dir
// maybe project plugin debugtalk.xx is not exist // maybe project plugin debugtalk.xx and proj.json are not exist
// use current dir instead // use current dir instead
return os.Getwd() return os.Getwd()
} }

View File

@@ -179,12 +179,13 @@ func (r *HRPRunner) Run(testcases ...ITestCase) error {
}() }()
for it := sessionRunner.parametersIterator; it.HasNext(); { for it := sessionRunner.parametersIterator; it.HasNext(); {
if err = sessionRunner.Start(it.Next()); err != nil { err = sessionRunner.Start(it.Next())
log.Error().Err(err).Msg("[Run] run testcase failed")
return err
}
caseSummary := sessionRunner.GetSummary() caseSummary := sessionRunner.GetSummary()
s.appendCaseSummary(caseSummary) s.appendCaseSummary(caseSummary)
if err != nil {
log.Error().Err(err).Msg("[Run] run testcase failed")
break
}
} }
} }
s.Time.Duration = time.Since(s.Time.StartAt).Seconds() s.Time.Duration = time.Since(s.Time.StartAt).Seconds()

View File

@@ -64,7 +64,7 @@ func (path *TestCasePath) ToTestCase() (*TestCase, error) {
return nil, errors.New("incorrect testcase file format, expected config in file") return nil, errors.New("incorrect testcase file format, expected config in file")
} }
err = tc.makeCompat() err = tc.MakeCompat()
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -173,26 +173,27 @@ type TCase struct {
TestSteps []*TStep `json:"teststeps" yaml:"teststeps"` TestSteps []*TStep `json:"teststeps" yaml:"teststeps"`
} }
// makeCompat converts TCase to compatible testcase // MakeCompat converts TCase compatible with Golang engine style
func (tc *TCase) makeCompat() error { func (tc *TCase) MakeCompat() (err error) {
var err error
defer func() { defer func() {
if p := recover(); p != nil { if p := recover(); p != nil {
err = fmt.Errorf("convert compat testcase error: %v", p) err = fmt.Errorf("[MakeCompat] convert compat testcase error: %v", p)
} }
}() }()
for _, step := range tc.TestSteps { for _, step := range tc.TestSteps {
// 1. deal with request body compatible with HttpRunner // 1. deal with request body compatibility
if step.Request != nil && step.Request.Body == nil { if step.Request != nil && step.Request.Body == nil {
if step.Request.Json != nil { if step.Request.Json != nil {
step.Request.Headers["Content-Type"] = "application/json; charset=utf-8" step.Request.Headers["Content-Type"] = "application/json; charset=utf-8"
step.Request.Body = step.Request.Json step.Request.Body = step.Request.Json
step.Request.Json = nil
} else if step.Request.Data != nil { } else if step.Request.Data != nil {
step.Request.Body = step.Request.Data step.Request.Body = step.Request.Data
step.Request.Data = nil
} }
} }
// 2. deal with validators compatible with HttpRunner // 2. deal with validators compatibility
err = convertCompatValidator(step.Validators) err = convertCompatValidator(step.Validators)
if err != nil { if err != nil {
return err return err
@@ -206,38 +207,46 @@ func (tc *TCase) makeCompat() error {
func convertCompatValidator(Validators []interface{}) (err error) { func convertCompatValidator(Validators []interface{}) (err error) {
for i, iValidator := range Validators { for i, iValidator := range Validators {
if _, ok := iValidator.(Validator); ok {
continue
}
validatorMap := iValidator.(map[string]interface{}) validatorMap := iValidator.(map[string]interface{})
validator := Validator{} validator := Validator{}
_, checkExisted := validatorMap["check"] iCheck, checkExisted := validatorMap["check"]
_, assertExisted := validatorMap["assert"] iAssert, assertExisted := validatorMap["assert"]
_, expectExisted := validatorMap["expect"] iExpect, expectExisted := validatorMap["expect"]
// check priority: HRP > HttpRunner // validator check priority: Golang > Python engine style
if checkExisted && assertExisted && expectExisted { if checkExisted && assertExisted && expectExisted {
// HRP validator format // Golang engine style
validator.Check = validatorMap["check"].(string) validator.Check = iCheck.(string)
validator.Assert = validatorMap["assert"].(string) validator.Assert = iAssert.(string)
validator.Expect = validatorMap["expect"] validator.Expect = iExpect
if msg, existed := validatorMap["msg"]; existed { if iMsg, msgExisted := validatorMap["msg"]; msgExisted {
validator.Message = msg.(string) validator.Message = iMsg.(string)
} }
validator.Check = convertCheckExpr(validator.Check) validator.Check = convertCheckExpr(validator.Check)
Validators[i] = validator Validators[i] = validator
} else if len(validatorMap) == 1 { continue
// HttpRunner validator format }
if len(validatorMap) == 1 {
// Python engine style
for assertMethod, iValidatorContent := range validatorMap { for assertMethod, iValidatorContent := range validatorMap {
checkAndExpect := iValidatorContent.([]interface{}) validatorContent := iValidatorContent.([]interface{})
if len(checkAndExpect) != 2 { if len(validatorContent) > 3 {
return fmt.Errorf("unexpected validator format: %v", validatorMap) return fmt.Errorf("unexpected validator format: %v", validatorMap)
} }
validator.Check = checkAndExpect[0].(string) validator.Check = validatorContent[0].(string)
validator.Assert = assertMethod validator.Assert = assertMethod
validator.Expect = checkAndExpect[1] validator.Expect = validatorContent[1]
if len(validatorContent) == 3 {
validator.Message = validatorContent[2].(string)
}
} }
validator.Check = convertCheckExpr(validator.Check) validator.Check = convertCheckExpr(validator.Check)
Validators[i] = validator Validators[i] = validator
} else { continue
return fmt.Errorf("unexpected validator format: %v", validatorMap)
} }
return fmt.Errorf("unexpected validator format: %v", validatorMap)
} }
return nil return nil
} }

View File

@@ -1,5 +1,5 @@
""" """
This module handles compatibility issues between testcase format v2 and v3. This module handles compatibility issues between testcase format v2, v3 and v4.
""" """
import os import os
import sys import sys
@@ -14,9 +14,8 @@ from httprunner.utils import sort_dict_by_custom_order
def convert_variables( def convert_variables(
raw_variables: Union[Dict, Text], test_path: Text raw_variables: Union[Dict, Text], test_path: Text
) -> Dict[Text, Any]: ) -> Dict[Text, Any]:
if isinstance(raw_variables, Dict): if isinstance(raw_variables, Dict):
return raw_variables return raw_variables
@@ -33,6 +32,18 @@ def convert_variables(
) )
def _convert_request(request: Dict) -> Dict:
if "body" in request:
content_type = ""
if "headers" in request and "Content-Type" in request["headers"]:
content_type = request["headers"]["Content-Type"]
if content_type.startswith("application/json"):
request["json"] = request.pop("body")
else:
request["data"] = request.pop("body")
return _sort_request_by_custom_order(request)
def _convert_jmespath(raw: Text) -> Text: def _convert_jmespath(raw: Text) -> Text:
if not isinstance(raw, Text): if not isinstance(raw, Text):
raise exceptions.TestCaseFormatError(f"Invalid jmespath extractor: {raw}") raise exceptions.TestCaseFormatError(f"Invalid jmespath extractor: {raw}")
@@ -153,6 +164,9 @@ def _ensure_step_attachment(step: Dict) -> Dict:
"name": step["name"], "name": step["name"],
} }
if "request" in step:
test_dict["request"] = _convert_request(step["request"])
if "variables" in step: if "variables" in step:
test_dict["variables"] = step["variables"] test_dict["variables"] = step["variables"]
@@ -181,11 +195,11 @@ def _ensure_step_attachment(step: Dict) -> Dict:
return test_dict return test_dict
def ensure_testcase_v3_api(api_content: Dict) -> Dict: def ensure_testcase_v4_api(api_content: Dict) -> Dict:
logger.info("convert api in v2 to testcase format v3") logger.info("convert api in v2/v3 to testcase format v4")
teststep = { teststep = {
"request": _sort_request_by_custom_order(api_content["request"]), "request": _convert_request(api_content["request"]),
} }
teststep.update(_ensure_step_attachment(api_content)) teststep.update(_ensure_step_attachment(api_content))
@@ -202,8 +216,8 @@ def ensure_testcase_v3_api(api_content: Dict) -> Dict:
} }
def ensure_testcase_v3(test_content: Dict) -> Dict: def ensure_testcase_v4(test_content: Dict) -> Dict:
logger.info("ensure compatibility with testcase format v2") logger.info("ensure compatibility with testcase format v2/v3")
v3_content = {"config": test_content["config"], "teststeps": []} v3_content = {"config": test_content["config"], "teststeps": []}
@@ -221,7 +235,7 @@ def ensure_testcase_v3(test_content: Dict) -> Dict:
teststep = {} teststep = {}
if "request" in step: if "request" in step:
teststep["request"] = _sort_request_by_custom_order(step.pop("request")) pass
elif "api" in step: elif "api" in step:
teststep["testcase"] = step.pop("api") teststep["testcase"] = step.pop("api")
elif "testcase" in step: elif "testcase" in step:

View File

@@ -26,6 +26,53 @@ class TestCompat(unittest.TestCase):
with self.assertRaises(exceptions.TestCaseFormatError): with self.assertRaises(exceptions.TestCaseFormatError):
compat.convert_variables(None, "examples/data/a-b.c/1.yml") compat.convert_variables(None, "examples/data/a-b.c/1.yml")
def test_convert_request(self):
request_with_json_body = {
"method": "POST",
"url": "https://postman-echo.com/post",
"headers": {
"Content-Type": "application/json"
},
"body": {
"k1": "v1",
"k2": "v2"
}
}
self.assertEqual(
compat._convert_request(request_with_json_body),
{
"method": "POST",
"url": "https://postman-echo.com/post",
"headers": {
"Content-Type": "application/json"
},
"json": {
"k1": "v1",
"k2": "v2"
}
}
)
request_with_text_body = {
"method": "POST",
"url": "https://postman-echo.com/post",
"headers": {
"Content-Type": "text/plain"
},
"body": "have a nice day"
}
self.assertEqual(
compat._convert_request(request_with_text_body),
{
"method": "POST",
"url": "https://postman-echo.com/post",
"headers": {
"Content-Type": "text/plain"
},
"data": "have a nice day"
}
)
def test_convert_jmespath(self): def test_convert_jmespath(self):
self.assertEqual(compat._convert_jmespath("content.abc"), "body.abc") self.assertEqual(compat._convert_jmespath("content.abc"), "body.abc")
self.assertEqual(compat._convert_jmespath("json.abc"), "body.abc") self.assertEqual(compat._convert_jmespath("json.abc"), "body.abc")
@@ -85,7 +132,7 @@ class TestCompat(unittest.TestCase):
[{"eq": ["body[0].name", 201]}], [{"eq": ["body[0].name", 201]}],
) )
def test_ensure_testcase_v3_api(self): def test_ensure_testcase_v4_api(self):
api_content = { api_content = {
"name": "get with params", "name": "get with params",
"request": { "request": {
@@ -98,7 +145,7 @@ class TestCompat(unittest.TestCase):
"validate": [{"eq": ["content.varB", 200]}, {"lt": ["json.0.varC", 0]}], "validate": [{"eq": ["content.varB", 200]}, {"lt": ["json.0.varC", 0]}],
} }
self.assertEqual( self.assertEqual(
compat.ensure_testcase_v3_api(api_content), compat.ensure_testcase_v4_api(api_content),
{ {
"config": { "config": {
"name": "get with params", "name": "get with params",
@@ -126,7 +173,7 @@ class TestCompat(unittest.TestCase):
}, },
) )
def test_ensure_testcase_v3(self): def test_ensure_testcase_v4(self):
testcase_content = { testcase_content = {
"config": {"name": "xxx", "base_url": "https://httpbin.org"}, "config": {"name": "xxx", "base_url": "https://httpbin.org"},
"teststeps": [ "teststeps": [
@@ -150,7 +197,7 @@ class TestCompat(unittest.TestCase):
], ],
} }
self.assertEqual( self.assertEqual(
compat.ensure_testcase_v3(testcase_content), compat.ensure_testcase_v4(testcase_content),
{ {
"config": {"name": "xxx", "base_url": "https://httpbin.org"}, "config": {"name": "xxx", "base_url": "https://httpbin.org"},
"teststeps": [ "teststeps": [

View File

@@ -11,8 +11,8 @@ from httprunner import __version__, exceptions
from httprunner.compat import ( from httprunner.compat import (
convert_variables, convert_variables,
ensure_path_sep, ensure_path_sep,
ensure_testcase_v3, ensure_testcase_v4,
ensure_testcase_v3_api, ensure_testcase_v4_api,
) )
from httprunner.loader import ( from httprunner.loader import (
convert_relative_project_root_dir, convert_relative_project_root_dir,
@@ -332,8 +332,8 @@ def make_teststep_chain_style(teststep: Dict) -> Text:
def make_testcase(testcase: Dict, dir_path: Text = None) -> Text: def make_testcase(testcase: Dict, dir_path: Text = None) -> Text:
"""convert valid testcase dict to pytest file path""" """convert valid testcase dict to pytest file path"""
# ensure compatibility with testcase format v2 # ensure compatibility with testcase format v2/v3
testcase = ensure_testcase_v3(testcase) testcase = ensure_testcase_v4(testcase)
# validate testcase format # validate testcase format
load_testcase(testcase) load_testcase(testcase)
@@ -373,9 +373,9 @@ def make_testcase(testcase: Dict, dir_path: Text = None) -> Text:
if not isinstance(test_content, Dict): if not isinstance(test_content, Dict):
raise exceptions.TestCaseFormatError(f"Invalid teststep: {teststep}") raise exceptions.TestCaseFormatError(f"Invalid teststep: {teststep}")
# api in v2 format, convert to v3 testcase # api in v2/v3 format, convert to v4 testcase
if "request" in test_content and "name" in test_content: if "request" in test_content and "name" in test_content:
test_content = ensure_testcase_v3_api(test_content) test_content = ensure_testcase_v4_api(test_content)
test_content.setdefault("config", {})["path"] = ref_testcase_path test_content.setdefault("config", {})["path"] = ref_testcase_path
ref_testcase_python_abs_path = make_testcase(test_content) ref_testcase_python_abs_path = make_testcase(test_content)
@@ -473,9 +473,9 @@ def __make(tests_path: Text):
) )
continue continue
# api in v2 format, convert to v3 testcase # api in v2/v3 format, convert to v4 testcase
if "request" in test_content and "name" in test_content: if "request" in test_content and "name" in test_content:
test_content = ensure_testcase_v3_api(test_content) test_content = ensure_testcase_v4_api(test_content)
if "config" not in test_content: if "config" not in test_content:
logger.warning( logger.warning(