Merge pull request #839 from httprunner/leo_dev

2.5.4

**Added**

- doc: add examples in json schema

**Fixed**

- fix #835: UnicodeDecodeError when loading json schema files
- fix: RecursionError caused by checking root dir incorrectly on Windows
This commit is contained in:
debugtalk
2020-01-03 19:08:47 +08:00
committed by GitHub
9 changed files with 332 additions and 82 deletions
+11
View File
@@ -1,5 +1,16 @@
# Release History # Release History
## 2.5.4 (2020-01-03)
**Added**
- doc: add examples in json schema
**Fixed**
- fix #835: UnicodeDecodeError when loading json schema files
- fix: RecursionError caused by checking root dir incorrectly on Windows
## 2.5.3 (2020-01-03) ## 2.5.3 (2020-01-03)
**Fixed** **Fixed**
+7 -6
View File
@@ -1,3 +1,4 @@
import io
import json import json
import os import os
import platform import platform
@@ -14,10 +15,10 @@ testcase_schema_v2_path = os.path.join(schemas_root_dir, "testcase.schema.v2.jso
testsuite_schema_v1_path = os.path.join(schemas_root_dir, "testsuite.schema.v1.json") testsuite_schema_v1_path = os.path.join(schemas_root_dir, "testsuite.schema.v1.json")
testsuite_schema_v2_path = os.path.join(schemas_root_dir, "testsuite.schema.v2.json") testsuite_schema_v2_path = os.path.join(schemas_root_dir, "testsuite.schema.v2.json")
with open(api_schema_path) as f: with io.open(api_schema_path, encoding='utf-8') as f:
api_schema = json.load(f) api_schema = json.load(f)
with open(common_schema_path) as f: with io.open(common_schema_path, encoding='utf-8') as f:
if platform.system() == "Windows": if platform.system() == "Windows":
absolute_base_path = 'file:///' + os.path.abspath(schemas_root_dir).replace("\\", "/") + '/' absolute_base_path = 'file:///' + os.path.abspath(schemas_root_dir).replace("\\", "/") + '/'
else: else:
@@ -27,16 +28,16 @@ with open(common_schema_path) as f:
common_schema = json.load(f) common_schema = json.load(f)
resolver = jsonschema.RefResolver(absolute_base_path, common_schema) resolver = jsonschema.RefResolver(absolute_base_path, common_schema)
with open(testcase_schema_v1_path) as f: with io.open(testcase_schema_v1_path, encoding='utf-8') as f:
testcase_schema_v1 = json.load(f) testcase_schema_v1 = json.load(f)
with open(testcase_schema_v2_path) as f: with io.open(testcase_schema_v2_path, encoding='utf-8') as f:
testcase_schema_v2 = json.load(f) testcase_schema_v2 = json.load(f)
with open(testsuite_schema_v1_path) as f: with io.open(testsuite_schema_v1_path, encoding='utf-8') as f:
testsuite_schema_v1 = json.load(f) testsuite_schema_v1 = json.load(f)
with open(testsuite_schema_v2_path) as f: with io.open(testsuite_schema_v2_path, encoding='utf-8') as f:
testsuite_schema_v2 = json.load(f) testsuite_schema_v2 = json.load(f)
+10 -3
View File
@@ -8,7 +8,7 @@ project_working_directory = None
def locate_file(start_path, file_name): def locate_file(start_path, file_name):
""" locate filename and return absolute file path. """ locate filename and return absolute file path.
searching will be recursive upward until current working directory. searching will be recursive upward until current working directory or system root dir.
Args: Args:
file_name (str): target locate file name file_name (str): target locate file name
@@ -33,11 +33,18 @@ def locate_file(start_path, file_name):
return os.path.abspath(file_path) return os.path.abspath(file_path)
# current working directory # current working directory
if os.path.abspath(start_dir_path) in [os.getcwd(), os.path.abspath(os.sep)]: if os.path.abspath(start_dir_path) == os.getcwd():
raise exceptions.FileNotFound("{} not found in {}".format(file_name, start_path))
# system root dir
# Windows, e.g. 'E:\\'
# Linux/Darwin, '/'
parent_dir = os.path.dirname(start_dir_path)
if parent_dir == start_dir_path:
raise exceptions.FileNotFound("{} not found in {}".format(file_name, start_path)) raise exceptions.FileNotFound("{} not found in {}".format(file_name, start_path))
# locate recursive upward # locate recursive upward
return locate_file(os.path.dirname(start_dir_path), file_name) return locate_file(parent_dir, file_name)
def locate_debugtalk_py(start_path): def locate_debugtalk_py(start_path):
+24
View File
@@ -31,5 +31,29 @@
"required": [ "required": [
"name", "name",
"request" "request"
],
"examples": [
{
"name": "demo api",
"variables": {
"var1": "value1",
"var2": "value2"
},
"request": {
"url": "/api/path/$var1",
"method": "POST",
"headers": {
"Content-Type": "application/json"
},
"json": {
"key": "$var2"
},
"validate": [
{
"eq": ["status_code", 200]
}
]
}
}
] ]
} }
+181 -64
View File
@@ -4,17 +4,29 @@
"definitions": { "definitions": {
"name": { "name": {
"description": "used as api/teststep/testcase/testsuite identification", "description": "used as api/teststep/testcase/testsuite identification",
"type": "string" "type": "string",
"examples": [
"basic test for httpbin"
]
}, },
"base_url": { "base_url": {
"description": "The base_url will be used with relative URI", "description": "The base_url will be used with relative URI",
"type": "string" "type": "string",
"examples": [
"https://httpbin.org"
]
}, },
"variables": { "variables": {
"description": "define variables for api/teststep/testcase/testsuite", "description": "define variables for api/teststep/testcase/testsuite",
"oneOf": [ "oneOf": [
{ {
"type": "object" "type": "object",
"examples": [
{
"var1": "value1",
"var2": "value2"
}
]
}, },
{ {
"type": "array", "type": "array",
@@ -22,18 +34,69 @@
"type": "object", "type": "object",
"maxProperties": 1, "maxProperties": 1,
"minProperties": 1 "minProperties": 1
} },
"examples": [
[
{
"var1": "value1"
},
{
"var2": "value2"
}
]
]
}, },
{ {
"type": "string", "type": "string",
"pattern": "^\\$.*", "pattern": "^\\$.*",
"examples": [ "examples": [
"$prepared_variables",
"${prepare_variables()}", "${prepare_variables()}",
"${prepare_variables($a, $b)}" "${prepare_variables($a, $b)}"
] ]
} }
] ]
}, },
"verify": {
"description": "whether to verify the servers TLS certificate",
"type": "boolean",
"examples": [
true,
false
]
},
"hook": {
"description": "used to define setup_hooks/teardown_hooks for api/teststep/testcase",
"type": "array",
"items": {
"oneOf": [
{
"description": "call setup/teardown hook functions, return nothing",
"type": "string",
"examples": [
[
"${sleep(2)}",
"${hook_print(setup)}",
"${modify_request_json($request, android)}",
"${alter_response($response)}"
]
]
},
{
"description": "call setup/teardown hook functions, return value and assign to variable",
"type": "object",
"examples": [
{
"total": "${sum_two(1, 5)}"
},
{
"filed_name": "get_decoded_response_field($response)"
}
]
}
]
}
},
"config": { "config": {
"description": "used in testcase/testsuite to configure common fields", "description": "used in testcase/testsuite to configure common fields",
"type": "object", "type": "object",
@@ -54,28 +117,11 @@
"$ref": "#/definitions/hook" "$ref": "#/definitions/hook"
}, },
"verify": { "verify": {
"description": "configure verify for current testcase/testsuite", "$ref": "#/definitions/verify"
"oneOf": [
{
"description": "whether we verify the servers TLS certificate",
"type": "boolean"
},
{
"description": "path to a CA bundle to use",
"type": "string"
}
]
} }
}, },
"required": ["name"] "required": ["name"]
}, },
"hook": {
"description": "used to define setup_hooks/teardown_hooks for api/teststep/testcase",
"type": "array",
"items": {
"type": "string"
}
},
"request": { "request": {
"description": "used to define a api request. properties is the same as python package `requests.request`", "description": "used to define a api request. properties is the same as python package `requests.request`",
"type": "object", "type": "object",
@@ -95,25 +141,49 @@
}, },
"url": { "url": {
"description": "request url, may be absolute or relative URI", "description": "request url, may be absolute or relative URI",
"type": "string" "type": "string",
"examples": [
"http://httpbin.org/get?a=1&b=2",
"/get?a=1&b=2",
"get?a=1&b=2"
]
}, },
"params": { "params": {
"description": "query string for request url", "description": "query string for request url",
"type": "object" "type": "object",
"examples": [
{
"a": 1,
"b": 2
}
]
}, },
"data": { "data": {
"anyOf": [ "anyOf": [
{ {
"description": "request body in json format", "description": "request body in json format",
"type": "object" "type": "object",
"examples": [
{
"a": 1,
"b": 2
}
]
}, },
{ {
"description": "request body in application/x-www-form-urlencoded format, e.g. a=1&b=2", "description": "request body in application/x-www-form-urlencoded format",
"type": "string" "type": "string",
"examples": [
"a=1&b=2"
]
}, },
{ {
"description": "request body in string format, e.g. '${prepare_data($a, $b)}'", "description": "request body prepared with function, or reference a variable",
"type": "string" "type": "string",
"examples": [
"$post_data",
"${prepare_data($a, $b)}"
]
} }
] ]
}, },
@@ -124,7 +194,7 @@
"type": "object" "type": "object"
}, },
{ {
"description": "request body in string format, e.g. '${prepare_json($a, $b)}'", "description": "request body prepared with function, or reference a variable",
"type": "string", "type": "string",
"pattern": "^\\$.*", "pattern": "^\\$.*",
"examples": [ "examples": [
@@ -136,7 +206,26 @@
}, },
"headers": { "headers": {
"description": "request headers", "description": "request headers",
"type": "object" "oneOf": [
{
"description": "request headers in json format",
"type": "object",
"examples": [
{
"User-Agent": "python-requests/2.18.4",
"Content-Type": "application/json"
}
]
},
{
"description": "request headers prepared with function, or reference a variable",
"type": "string",
"examples": [
"$prepared_headers",
"${prepare_headers($a, $b)}"
]
}
]
}, },
"cookies": { "cookies": {
"description": "request cookies", "description": "request cookies",
@@ -152,7 +241,10 @@
}, },
"timeout": { "timeout": {
"description": "How many seconds to wait for the server to send data before giving up", "description": "How many seconds to wait for the server to send data before giving up",
"type": "number" "type": "number",
"examples": [
120
]
}, },
"allow_redirects": { "allow_redirects": {
"description": "Enable/disable GET/OPTIONS/POST/PUT/PATCH/DELETE/HEAD redirection. Defaults to True", "description": "Enable/disable GET/OPTIONS/POST/PUT/PATCH/DELETE/HEAD redirection. Defaults to True",
@@ -164,41 +256,21 @@
}, },
"verify": { "verify": {
"description": "configure verify for current api/teststep", "description": "configure verify for current api/teststep",
"oneOf": [ "$ref": "#/definitions/verify"
{
"description": "whether we verify the servers TLS certificate",
"type": "boolean"
},
{
"description": "path to a CA bundle to use",
"type": "string"
}
]
}, },
"stream": { "stream": {
"description": "if False, the response content will be immediately downloaded.", "description": "if False, the response content will be immediately downloaded.",
"type": "boolean" "type": "boolean"
}, },
"cert": {
"oneOf": [
{
"description": "path to ssl client cert file (.pem)",
"type": "string"
},
{
"description": "('cert', 'key') pair",
"type": "array",
"maxItems": 2,
"minItems": 2,
"items": {
"type": "string"
}
}
]
},
"upload": { "upload": {
"description": "upload files", "description": "upload files",
"type": "object" "type": "object",
"examples": [
{
"file": "data/file_to_upload",
"md5": "123"
}
]
} }
}, },
"required": [ "required": [
@@ -216,7 +288,16 @@
"description": "extraction rule for session variable, maybe in jsonpath/regex/jmespath", "description": "extraction rule for session variable, maybe in jsonpath/regex/jmespath",
"type": "string" "type": "string"
} }
} },
"examples": [
{
"code__by_jsonpath": "$.code",
"item_id__by_jsonpath": "$..items.*.id",
"var_name__by_regex": "\"LB[\\d]*(.*)RB[\\d]*\"",
"content_type": "headers.content-type",
"first_name": "content.person.name.first_name"
}
]
}, },
{ {
"type": "array", "type": "array",
@@ -230,7 +311,24 @@
}, },
"minProperties": 1, "minProperties": 1,
"maxProperties": 1 "maxProperties": 1
} },
"examples": [
{
"code__by_jsonpath": "$.code"
},
{
"item_id__by_jsonpath": "$..items.*.id"
},
{
"var_name__by_regex": "\"LB[\\d]*(.*)RB[\\d]*\""
},
{
"content_type": "headers.content-type"
},
{
"first_name": "content.person.name.first_name"
}
]
} }
] ]
}, },
@@ -253,7 +351,18 @@
"description": "expected value" "description": "expected value"
} }
}, },
"required": ["check", "expect"] "required": ["check", "expect"],
"examples": [
{
"check": "body.code",
"comparator": "gt",
"expect": 0
},
{
"check": "status_code",
"expect": 200
}
]
}, },
{ {
"type": "object", "type": "object",
@@ -264,7 +373,15 @@
"minItems": 2, "minItems": 2,
"maxItems": 2 "maxItems": 2
} }
} },
"examples": [
{
"eq": ["status_code", 200]
},
{
"gt": ["body.code", 0]
}
]
} }
] ]
} }
@@ -125,5 +125,60 @@
"required": [ "required": [
"config", "config",
"teststeps" "teststeps"
],
"examples": [
{
"config": {
"name": "testcase name"
},
"teststeps": [
{
"name": "api 1",
"api": "/path/to/api1"
},
{
"name": "api 2",
"api": "/path/to/api2"
}
]
},
{
"config": {
"name": "demo testcase",
"variables": {
"device_sn": "ABC",
"username": "${ENV(USERNAME)}",
"password": "${ENV(PASSWORD)}"
},
"base_url": "http://127.0.0.1:5000"
},
"teststeps": [
{
"name": "demo step 1",
"api": "path/to/api1.yml",
"variables": {
"user_agent": "iOS/10.3",
"device_sn": "$device_sn"
},
"extract": [
{
"token": "content.token"
}
],
"validate": [
{
"eq": ["status_code", 200]
}
]
},
{
"name": "demo step 2",
"api": "path/to/api2.yml",
"variables": {
"token": "$token"
}
}
]
}
] ]
} }
@@ -53,11 +53,11 @@
}, },
"testcases": { "testcases": {
"testcase 1": { "testcase 1": {
"name": "xxx", "name": "testcase 1",
"testcase": "/path/to/testcase1" "testcase": "/path/to/testcase1"
}, },
"testcase 2": { "testcase 2": {
"name": "xxx", "name": "testcase 2",
"testcase": "/path/to/testcase2" "testcase": "/path/to/testcase2"
} }
} }
@@ -42,5 +42,47 @@
"required": [ "required": [
"config", "config",
"testcases" "testcases"
],
"examples": [
{
"config": {
"name": "testsuite name"
},
"testcases": [
{
"name": "testcase 1",
"testcase": "/path/to/testcase1"
},
{
"name": "testcase 2",
"testcase": "/path/to/testcase2"
}
]
},
{
"config": {
"name": "demo testsuite",
"variables": {
"device_sn": "XYZ"
},
"base_url": "http://127.0.0.1:5000"
},
"testcases": [
{
"name": "call demo_testcase with data 1",
"testcase": "path/to/demo_testcase.yml",
"variables": {
"device_sn": "$device_sn"
}
},
{
"name": "call demo_testcase with data 2",
"testcase": "path/to/demo_testcase.yml",
"variables": {
"device_sn": "$device_sn"
}
}
]
}
] ]
} }
-7
View File
@@ -388,13 +388,6 @@ def create_scaffold(project_name):
msg = "created file: {}".format(path) msg = "created file: {}".format(path)
logger.color_print(msg, "BLUE") logger.color_print(msg, "BLUE")
def create_path(path, ptype, file_content=""):
if ptype == "folder":
os.makedirs(path)
elif ptype == "file":
with open(path, 'w') as f:
f.write(file_content)
demo_api_content = """ demo_api_content = """
name: demo api name: demo api
variables: variables: