diff --git a/converter.py b/converter.py new file mode 100644 index 00000000..a54bc377 --- /dev/null +++ b/converter.py @@ -0,0 +1,176 @@ +import io +import json +import pprint +import re +import textwrap +from typing import Any + +from mitmproxy import http +from mitmproxy.utils import strutils + + +def curl_command(flow: http.HTTPFlow) -> str: + data = "curl " + + request = flow.request.copy() + request.decode(strict=False) + + for k, v in request.headers.items(multi=True): + data += "-H '%s:%s' " % (k, v) + + if request.method != "GET": + data += "-X %s " % request.method + + data += "'%s'" % request.url + + if request.content: + data += " --data-binary '%s'" % strutils.bytes_to_escaped_str( + request.content, escape_single_quotes=True + ) + + return data + + +def python_arg(arg: str, val: Any) -> str: + if not val: + return "" + if arg: + arg += "=" + arg_str = "{}{},\n".format(arg, pprint.pformat(val, 79 - len(arg))) + return textwrap.indent(arg_str, " " * 4) + + +def python_code(flow: http.HTTPFlow): + code = io.StringIO() + + def writearg(arg, val): + code.write(python_arg(arg, val)) + + code.write("import requests\n") + code.write("\n") + if flow.request.method.lower() in ("get", "post", "put", "head", "delete", "patch"): + code.write("response = requests.{}(\n".format(flow.request.method.lower())) + else: + code.write("response = requests.request(\n") + writearg("", flow.request.method) + url_without_query = flow.request.url.split("?", 1)[0] + writearg("", url_without_query) + + writearg("params", list(flow.request.query.fields)) + + headers = flow.request.headers.copy() + # requests adds those by default. + for x in (":authority", "host", "content-length"): + headers.pop(x, None) + writearg("headers", dict(headers)) + try: + if "json" not in flow.request.headers.get("content-type", ""): + raise ValueError() + writearg("json", json.loads(flow.request.text)) + except ValueError: + writearg("data", flow.request.content) + + code.seek(code.tell() - 2) # remove last comma + code.write("\n)\n") + code.write("\n") + code.write("print(response.text)") + + return code.getvalue() + + +def locust_code(flow): + code = textwrap.dedent( + """ + from locust import HttpLocust, TaskSet, task + class UserBehavior(TaskSet): + def on_start(self): + ''' on_start is called when a Locust start before any task is scheduled ''' + self.{name}() + @task() + def {name}(self): + url = self.locust.host + '{path}' + {headers}{params}{data} + self.response = self.client.request( + method='{method}', + url=url,{args} + ) + ### Additional tasks can go here ### + class WebsiteUser(HttpLocust): + task_set = UserBehavior + min_wait = 1000 + max_wait = 3000 +""" + ).strip() + + name = re.sub("\W|^(?=\d)", "_", flow.request.path.strip("/").split("?", 1)[0]) + if not name: + new_name = "_".join([str(flow.request.host), str(flow.request.timestamp_start)]) + name = re.sub("\W|^(?=\d)", "_", new_name) + + path_without_query = flow.request.path.split("?")[0] + + args = "" + headers = "" + + def conv(x): + return strutils.bytes_to_escaped_str(x, escape_single_quotes=True) + + if flow.request.headers: + lines = [ + (conv(k), conv(v)) + for k, v in flow.request.headers.fields + if conv(k).lower() not in [":authority", "host", "cookie"] + ] + lines = [" '%s': '%s',\n" % (k, v) for k, v in lines] + headers += "\n headers = {\n%s }\n" % "".join(lines) + args += "\n headers=headers," + + params = "" + if flow.request.query: + lines = [ + " %s: %s,\n" % (repr(k), repr(v)) + for k, v in flow.request.query.collect() + ] + params = "\n params = {\n%s }\n" % "".join(lines) + args += "\n params=params," + + data = "" + if flow.request.content: + data = "\n data = '''%s'''\n" % conv(flow.request.content) + args += "\n data=data," + + code = code.format( + name=name, + path=path_without_query, + headers=headers, + params=params, + data=data, + method=flow.request.method, + args=args, + ) + + return code + + +def locust_task(flow): + code = locust_code(flow) + start_task = len(code.split("@task")[0]) - 4 + end_task = -19 - len(code.split("### Additional")[1]) + task_code = code[start_task:end_task] + + return task_code + + +def url(flow): + return flow.request.url + + +EXPORTERS = [ + ("content", "c", None), + ("headers+content", "h", None), + ("url", "u", url), + ("as curl command", "r", curl_command), + ("as python code", "p", python_code), + ("as locust code", "l", locust_code), + ("as locust task", "t", locust_task), +] diff --git a/demo/.debugtalk_gen.py b/demo/.debugtalk_gen.py new file mode 100644 index 00000000..a57fef72 --- /dev/null +++ b/demo/.debugtalk_gen.py @@ -0,0 +1,23 @@ +# NOTE: Generated By hrp v4.1.4, DO NOT EDIT! + +import sys +import os + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from debugtalk import * + + +if __name__ == "__main__": + import funppy + funppy.register("get_user_agent", get_user_agent) + funppy.register("sleep", sleep) + funppy.register("sum", sum) + funppy.register("sum_ints", sum_ints) + funppy.register("sum_two_int", sum_two_int) + funppy.register("sum_two_string", sum_two_string) + funppy.register("sum_strings", sum_strings) + funppy.register("concatenate", concatenate) + funppy.register("setup_hook_example", setup_hook_example) + funppy.register("teardown_hook_example", teardown_hook_example) + funppy.serve() diff --git a/demo/.env b/demo/.env new file mode 100644 index 00000000..59ecc742 --- /dev/null +++ b/demo/.env @@ -0,0 +1,3 @@ +base_url=https://postman-echo.com +USERNAME=debugtalk +PASSWORD=123456 \ No newline at end of file diff --git a/demo/.gitignore b/demo/.gitignore new file mode 100644 index 00000000..4c8cb60c --- /dev/null +++ b/demo/.gitignore @@ -0,0 +1,14 @@ +reports/ +*.so +.vscode/ +.idea/ +.DS_Store +output/ +__pycache__/ +*.pyc +.python-version +logs/ + +# plugin +debugtalk.bin +debugtalk.so diff --git a/demo/debugtalk.py b/demo/debugtalk.py new file mode 100644 index 00000000..334a46c7 --- /dev/null +++ b/demo/debugtalk.py @@ -0,0 +1,62 @@ +import logging +import time +from typing import List + + +# commented out function will be filtered +# def get_headers(): +# return {"User-Agent": "hrp"} + + +def get_user_agent(): + return "hrp/funppy" + + +def sleep(n_secs): + time.sleep(n_secs) + + +def sum(*args): + result = 0 + for arg in args: + result += arg + return result + + +def sum_ints(*args: List[int]) -> int: + result = 0 + for arg in args: + result += arg + return result + + +def sum_two_int(a: int, b: int) -> int: + return a + b + + +def sum_two_string(a: str, b: str) -> str: + return a + b + + +def sum_strings(*args: List[str]) -> str: + result = "" + for arg in args: + result += arg + return result + + +def concatenate(*args: List[str]) -> str: + result = "" + for arg in args: + result += str(arg) + return result + + +def setup_hook_example(name): + logging.warning("setup_hook_example") + return f"setup_hook_example: {name}" + + +def teardown_hook_example(name): + logging.warning("teardown_hook_example") + return f"teardown_hook_example: {name}" diff --git a/demo/har/.keep b/demo/har/.keep new file mode 100644 index 00000000..e69de29b diff --git a/demo/proj.json b/demo/proj.json new file mode 100644 index 00000000..08277e5f --- /dev/null +++ b/demo/proj.json @@ -0,0 +1,5 @@ +{ + "project_name": "demo", + "create_time": "2022-06-23T11:15:39.635136+08:00", + "hrp_version": "v4.1.4" +} diff --git a/demo/testcases/demo.json b/demo/testcases/demo.json new file mode 100644 index 00000000..8e50e2aa --- /dev/null +++ b/demo/testcases/demo.json @@ -0,0 +1,176 @@ +{ + "config": { + "name": "demo with complex mechanisms", + "base_url": "https://postman-echo.com", + "variables": { + "a": "${sum(10, 2.3)}", + "b": 3.45, + "n": "${sum_ints(1, 2, 2)}", + "varFoo1": "${gen_random_string($n)}", + "varFoo2": "${max($a, $b)}" + } + }, + "teststeps": [ + { + "name": "transaction 1 start", + "transaction": { + "name": "tran1", + "type": "start" + } + }, + { + "name": "get with params", + "request": { + "method": "GET", + "url": "/get", + "params": { + "foo1": "$varFoo1", + "foo2": "$varFoo2" + }, + "headers": { + "User-Agent": "HttpRunnerPlus" + } + }, + "variables": { + "b": 34.5, + "n": 3, + "name": "get with params", + "varFoo2": "${max($a, $b)}" + }, + "setup_hooks": [ + "${setup_hook_example($name)}" + ], + "teardown_hooks": [ + "${teardown_hook_example($name)}" + ], + "extract": { + "varFoo1": "body.args.foo1" + }, + "validate": [ + { + "check": "status_code", + "assert": "equals", + "expect": 200, + "msg": "check response status code" + }, + { + "check": "headers.\"Content-Type\"", + "assert": "startswith", + "expect": "application/json" + }, + { + "check": "body.args.foo1", + "assert": "length_equals", + "expect": 5, + "msg": "check args foo1" + }, + { + "check": "$varFoo1", + "assert": "length_equals", + "expect": 5, + "msg": "check args foo1" + }, + { + "check": "body.args.foo2", + "assert": "equals", + "expect": "34.5", + "msg": "check args foo2" + } + ] + }, + { + "name": "transaction 1 end", + "transaction": { + "name": "tran1", + "type": "end" + } + }, + { + "name": "post json data", + "request": { + "method": "POST", + "url": "/post", + "body": { + "foo1": "$varFoo1", + "foo2": "${max($a, $b)}" + } + }, + "validate": [ + { + "check": "status_code", + "assert": "equals", + "expect": 200, + "msg": "check status code" + }, + { + "check": "body.json.foo1", + "assert": "length_equals", + "expect": 5, + "msg": "check args foo1" + }, + { + "check": "body.json.foo2", + "assert": "equals", + "expect": 12.3, + "msg": "check args foo2" + } + ] + }, + { + "name": "post form data", + "request": { + "method": "POST", + "url": "/post", + "headers": { + "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8" + }, + "body": { + "foo1": "$varFoo1", + "foo2": "${max($a, $b)}", + "time": "${get_timestamp()}" + } + }, + "extract": { + "varTime": "body.form.time" + }, + "validate": [ + { + "check": "status_code", + "assert": "equals", + "expect": 200, + "msg": "check status code" + }, + { + "check": "body.form.foo1", + "assert": "length_equals", + "expect": 5, + "msg": "check args foo1" + }, + { + "check": "body.form.foo2", + "assert": "equals", + "expect": "12.3", + "msg": "check args foo2" + } + ] + }, + { + "name": "get with timestamp", + "request": { + "method": "GET", + "url": "/get", + "params": { + "time": "$varTime" + } + }, + "validate": [ + { + "check": "body.args.time", + "assert": "length_equals", + "expect": 13, + "msg": "check extracted var timestamp" + } + ] + } + ] +} diff --git a/demo/testcases/ref_testcase.yml b/demo/testcases/ref_testcase.yml new file mode 100644 index 00000000..c0932124 --- /dev/null +++ b/demo/testcases/ref_testcase.yml @@ -0,0 +1,33 @@ +config: + name: "request methods testcase: reference testcase" + variables: + foo1: testsuite_config_bar1 + expect_foo1: testsuite_config_bar1 + expect_foo2: config_bar2 + base_url: "https://postman-echo.com" + verify: False + +teststeps: +- + name: request with functions + variables: + foo1: testcase_ref_bar1 + expect_foo1: testcase_ref_bar1 + testcase: testcases/requests.yml + export: + - foo3 +- + name: post form data + variables: + foo1: bar1 + request: + method: POST + url: /post + headers: + User-Agent: ${get_user_agent()} + Content-Type: "application/x-www-form-urlencoded" + body: "foo1=$foo1&foo2=$foo3" + validate: + - eq: ["status_code", 200] + - eq: ["body.form.foo1", "bar1"] + - eq: ["body.form.foo2", "bar21"] \ No newline at end of file diff --git a/demo/testcases/requests.json b/demo/testcases/requests.json new file mode 100644 index 00000000..4c01ec74 --- /dev/null +++ b/demo/testcases/requests.json @@ -0,0 +1,136 @@ +{ + "config": { + "name": "request methods testcase with functions", + "variables": { + "foo1": "config_bar1", + "foo2": "config_bar2", + "expect_foo1": "config_bar1", + "expect_foo2": "config_bar2" + }, + "headers": { + "User-Agent": "${get_user_agent()}" + }, + "base_url": "https://postman-echo.com", + "verify": false, + "export": [ + "foo3" + ] + }, + "teststeps": [ + { + "name": "get with params", + "variables": { + "foo1": "${ENV(USERNAME)}", + "foo2": "bar21", + "sum_v": "${sum_two_int(10000000, 20000000)}" + }, + "request": { + "method": "GET", + "url": "/get", + "params": { + "foo1": "$foo1", + "foo2": "$foo2", + "sum_v": "$sum_v" + } + }, + "extract": { + "foo3": "body.args.foo2" + }, + "validate": [ + { + "check": "status_code", + "assert": "equal", + "expect": 200, + "msg": "check status_code" + }, + { + "check": "body.args.foo1", + "assert": "equal", + "expect": "debugtalk", + "msg": "check body.args.foo1" + }, + { + "check": "body.args.sum_v", + "assert": "equal", + "expect": "30000000", + "msg": "check body.args.sum_v" + }, + { + "check": "body.args.foo2", + "assert": "equal", + "expect": "bar21", + "msg": "check body.args.foo2" + } + ] + }, + { + "name": "post raw text", + "variables": { + "foo1": "bar12", + "foo3": "bar32" + }, + "request": { + "method": "POST", + "url": "/post", + "headers": { + "Content-Type": "text/plain" + }, + "body": "This is expected to be sent back as part of response body: $foo1-$foo2-$foo3." + }, + "validate": [ + { + "check": "status_code", + "assert": "equal", + "expect": 200, + "msg": "check status_code" + }, + { + "check": "body.data", + "assert": "equal", + "expect": "This is expected to be sent back as part of response body: bar12-$expect_foo2-bar32.", + "msg": "check body.data" + } + ] + }, + { + "name": "post form data", + "variables": { + "foo2": "bar23" + }, + "request": { + "method": "POST", + "url": "/post", + "headers": { + "Content-Type": "application/x-www-form-urlencoded" + }, + "body": "foo1=$foo1&foo2=$foo2&foo3=$foo3" + }, + "validate": [ + { + "check": "status_code", + "assert": "equal", + "expect": 200, + "msg": "check status_code" + }, + { + "check": "body.form.foo1", + "assert": "equal", + "expect": "$expect_foo1", + "msg": "check body.form.foo1" + }, + { + "check": "body.form.foo2", + "assert": "equal", + "expect": "bar23", + "msg": "check body.form.foo2" + }, + { + "check": "body.form.foo3", + "assert": "equal", + "expect": "bar21", + "msg": "check body.form.foo3" + } + ] + } + ] +} \ No newline at end of file diff --git a/demo/testcases/requests.yml b/demo/testcases/requests.yml new file mode 100644 index 00000000..5922ab12 --- /dev/null +++ b/demo/testcases/requests.yml @@ -0,0 +1,62 @@ +config: + name: "request methods testcase with functions" + variables: + foo1: config_bar1 + foo2: config_bar2 + expect_foo1: config_bar1 + expect_foo2: config_bar2 + headers: + User-Agent: ${get_user_agent()} + verify: False + export: ["foo3"] + +teststeps: +- + name: get with params + variables: + foo1: ${ENV(USERNAME)} + foo2: bar21 + sum_v: "${sum_two_int(10000000, 20000000)}" + request: + method: GET + url: $base_url/get + params: + foo1: $foo1 + foo2: $foo2 + sum_v: $sum_v + extract: + foo3: "body.args.foo2" + validate: + - eq: ["status_code", 200] + - eq: ["body.args.foo1", "debugtalk"] + - eq: ["body.args.sum_v", "30000000"] + - eq: ["body.args.foo2", "bar21"] +- + name: post raw text + variables: + foo1: "bar12" + foo3: "bar32" + request: + method: POST + url: $base_url/post + headers: + Content-Type: "text/plain" + body: "This is expected to be sent back as part of response body: $foo1-$foo2-$foo3." + validate: + - eq: ["status_code", 200] + - eq: ["body.data", "This is expected to be sent back as part of response body: bar12-$expect_foo2-bar32."] +- + name: post form data + variables: + foo2: bar23 + request: + method: POST + url: $base_url/post + headers: + Content-Type: "application/x-www-form-urlencoded" + body: "foo1=$foo1&foo2=$foo2&foo3=$foo3" + validate: + - eq: ["status_code", 200] + - eq: ["body.form.foo1", "$expect_foo1"] + - eq: ["body.form.foo2", "bar23"] + - eq: ["body.form.foo3", "bar21"] diff --git a/examples/data/postman/__init__.py b/examples/data/postman/__init__.py new file mode 100644 index 00000000..70cfba53 --- /dev/null +++ b/examples/data/postman/__init__.py @@ -0,0 +1 @@ +# NOTICE: Generated By HttpRunner. DO NOT EDIT! diff --git a/examples/data/postman/postman_collection_test.json b/examples/data/postman/postman_collection_test.json new file mode 100644 index 00000000..8592d93b --- /dev/null +++ b/examples/data/postman/postman_collection_test.json @@ -0,0 +1,81 @@ +{ + "config": { + "name": "postman collection demo" + }, + "teststeps": [ + { + "name": "folder1 - folder2 - Get with params", + "request": { + "method": "GET", + "url": "https://postman-echo.com/get", + "params": { + "k1": "v1", + "k2": "v2" + } + } + }, + { + "name": "folder3 - Post form-data", + "request": { + "method": "POST", + "url": "https://postman-echo.com/post", + "upload": { + "intro_key": "intro.txt", + "k1": "v1", + "k2": "v2", + "logo_key": "logo.jpeg" + } + } + }, + { + "name": "folder3 - Post x-www-form-urlencoded", + "request": { + "method": "POST", + "url": "https://postman-echo.com/post", + "headers": { + "Content-Type": "application/x-www-form-urlencoded" + }, + "body": { + "k1": "v1", + "k2": "v2" + } + } + }, + { + "name": "folder3 - Post raw json", + "request": { + "method": "POST", + "url": "https://postman-echo.com/post", + "headers": { + "Content-Type": "application/json" + }, + "body": { + "k1": "v1", + "k2": "v2" + } + } + }, + { + "name": "folder3 - Post raw text", + "request": { + "method": "POST", + "url": "https://postman-echo.com/post", + "headers": { + "Content-Type": "text/plain" + }, + "body": "have a nice day" + } + }, + { + "name": "Get request headers", + "request": { + "method": "GET", + "url": "https://postman-echo.com/headers", + "headers": { + "Connection": "close", + "User-Agent": "HttpRunner" + } + } + } + ] +} diff --git a/examples/data/postman/postman_collection_test_test.py b/examples/data/postman/postman_collection_test_test.py new file mode 100644 index 00000000..4a813aa8 --- /dev/null +++ b/examples/data/postman/postman_collection_test_test.py @@ -0,0 +1,55 @@ +# NOTE: Generated By HttpRunner v4.1.4 +# FROM: postman/postman_collection_test.json +from httprunner import HttpRunner, Config, Step, RunRequest + + +class TestCasePostmanCollectionTest(HttpRunner): + + config = Config("postman collection demo") + + teststeps = [ + Step( + RunRequest("folder1 - folder2 - Get with params") + .get("https://postman-echo.com/get") + .with_params(**{"k1": "v1", "k2": "v2"}) + ), + Step( + RunRequest("folder3 - Post form-data") + .post("https://postman-echo.com/post") + .upload( + **{ + "intro_key": "intro.txt", + "k1": "v1", + "k2": "v2", + "logo_key": "logo.jpeg", + } + ) + ), + Step( + RunRequest("folder3 - Post x-www-form-urlencoded") + .post("https://postman-echo.com/post") + .with_headers(**{"Content-Type": "application/x-www-form-urlencoded"}) + .with_data({"k1": "v1", "k2": "v2"}) + ), + Step( + RunRequest("folder3 - Post raw json") + .post("https://postman-echo.com/post") + .with_headers(**{"Content-Type": "application/json"}) + .with_json({"k1": "v1", "k2": "v2"}) + ), + Step( + RunRequest("folder3 - Post raw text") + .post("https://postman-echo.com/post") + .with_headers(**{"Content-Type": "text/plain"}) + .with_data("have a nice day") + ), + Step( + RunRequest("Get request headers") + .get("https://postman-echo.com/headers") + .with_headers(**{"Connection": "close", "User-Agent": "HttpRunner"}) + ), + ] + + +if __name__ == "__main__": + TestCasePostmanCollectionTest().test_start() diff --git a/examples/postman_echo/.debugtalk_gen.py b/examples/postman_echo/.debugtalk_gen.py new file mode 100644 index 00000000..f3e58877 --- /dev/null +++ b/examples/postman_echo/.debugtalk_gen.py @@ -0,0 +1,20 @@ +# NOTE: Generated By hrp v4.2.0, DO NOT EDIT! + +import sys +import os + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from debugtalk import * + + +if __name__ == "__main__": + import funppy + funppy.register("get_httprunner_version", get_httprunner_version) + funppy.register("sum_two", sum_two) + funppy.register("get_testcase_config_variables", get_testcase_config_variables) + funppy.register("get_testsuite_config_variables", get_testsuite_config_variables) + funppy.register("get_app_version", get_app_version) + funppy.register("calculate_two_nums", calculate_two_nums) + funppy.register("fake_rand_count", fake_rand_count) + funppy.serve() diff --git a/go.mod b/go.mod index 3b02532a..8fa0c5ce 100644 --- a/go.mod +++ b/go.mod @@ -88,3 +88,4 @@ require ( ) // replace github.com/httprunner/funplugin => ../funplugin +replace github.com/electricbubble/gidevice => github.com/debugtalk/gidevice v0.6.3-0.20221008035433-d79086a752a7 diff --git a/go.sum b/go.sum index 1f3a0ba3..8b05a05a 100644 --- a/go.sum +++ b/go.sum @@ -96,12 +96,12 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/debugtalk/gidevice v0.6.3-0.20221008035433-d79086a752a7 h1:rxaa937c9aj3Yu4M2UZb5CLAgmPu5XXpXQEfKKSWkEw= +github.com/debugtalk/gidevice v0.6.3-0.20221008035433-d79086a752a7/go.mod h1:bRHL2M9qgeEKju8KRvKMZUVEg7t5zMnTiG3SJ3QDH5o= github.com/denisbrodbeck/machineid v1.0.1 h1:geKr9qtkB876mXguW2X6TU4ZynleN6ezuMSRhl4D7AQ= github.com/denisbrodbeck/machineid v1.0.1/go.mod h1:dJUwb7PTidGDeYyUBmXZ2GphQBbjJCrnectwCyxcUSI= github.com/electricbubble/gadb v0.0.7 h1:fxvVLVNs3IFKuYAEXDF2tDZUjT9jNCltoTSirjM5dgo= github.com/electricbubble/gadb v0.0.7/go.mod h1:3293YJ6OWHv/Q6NA5dwSbK43MbmYm8+Vz2d7h5J3IA8= -github.com/electricbubble/gidevice v0.6.2 h1:eIeCHH7Xn5fTwnUv3qL8c7L4anKIHtjlTBkgr1LDVTc= -github.com/electricbubble/gidevice v0.6.2/go.mod h1:bRHL2M9qgeEKju8KRvKMZUVEg7t5zMnTiG3SJ3QDH5o= github.com/electricbubble/opencv-helper v0.0.3 h1:p0sHTUPPPm8GqzVUtYH+wQbJoguzotUXVRAS7Ibk7nI= github.com/electricbubble/opencv-helper v0.0.3/go.mod h1:VHB21p5xsIjXUsUleWSaKGJosRsRAO7cuJoZKf7uCcc= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= diff --git a/google_style.py b/google_style.py new file mode 100644 index 00000000..9ba4349f --- /dev/null +++ b/google_style.py @@ -0,0 +1,297 @@ +# -*- coding: utf-8 -*- +"""Example Google style docstrings. + +This module demonstrates documentation as specified by the `Google Python +Style Guide`_. Docstrings may extend over multiple lines. Sections are created +with a section header and a colon followed by a block of indented text. + +Example: + Examples can be given using either the ``Example`` or ``Examples`` + sections. Sections support any reStructuredText formatting, including + literal blocks:: + + $ python example_google.py + +Section breaks are created by resuming unindented text. Section breaks +are also implicitly created anytime a new section starts. + +Attributes: + module_level_variable1 (int): Module level variables may be documented in + either the ``Attributes`` section of the module docstring, or in an + inline docstring immediately following the variable. + + Either form is acceptable, but the two should not be mixed. Choose + one convention to document module level variables and be consistent + with it. + +Todo: + * For module TODOs + * You have to also use ``sphinx.ext.todo`` extension + +.. _Google Python Style Guide: + http://google.github.io/styleguide/pyguide.html + +""" + +module_level_variable1 = 12345 + +module_level_variable2 = 98765 +"""int: Module level variable documented inline. + +The docstring may span multiple lines. The type may optionally be specified +on the first line, separated by a colon. +""" + + +def function_with_types_in_docstring(param1, param2): + """Example function with types documented in the docstring. + + `PEP 484`_ type annotations are supported. If attribute, parameter, and + return types are annotated according to `PEP 484`_, they do not need to be + included in the docstring: + + Args: + param1 (int): The first parameter. + param2 (str): The second parameter. + + Returns: + bool: The return value. True for success, False otherwise. + + .. _PEP 484: + https://www.python.org/dev/peps/pep-0484/ + + """ + + +def function_with_pep484_type_annotations(param1: int, param2: str) -> bool: + """Example function with PEP 484 type annotations. + + Args: + param1: The first parameter. + param2: The second parameter. + + Returns: + The return value. True for success, False otherwise. + + """ + + +def module_level_function(param1, param2=None, *args, **kwargs): + """This is an example of a module level function. + + Function parameters should be documented in the ``Args`` section. The name + of each parameter is required. The type and description of each parameter + is optional, but should be included if not obvious. + + If \*args or \*\*kwargs are accepted, + they should be listed as ``*args`` and ``**kwargs``. + + The format for a parameter is:: + + name (type): description + The description may span multiple lines. Following + lines should be indented. The "(type)" is optional. + + Multiple paragraphs are supported in parameter + descriptions. + + Args: + param1 (int): The first parameter. + param2 (:obj:`str`, optional): The second parameter. Defaults to None. + Second line of description should be indented. + *args: Variable length argument list. + **kwargs: Arbitrary keyword arguments. + + Returns: + bool: True if successful, False otherwise. + + The return type is optional and may be specified at the beginning of + the ``Returns`` section followed by a colon. + + The ``Returns`` section may span multiple lines and paragraphs. + Following lines should be indented to match the first line. + + The ``Returns`` section supports any reStructuredText formatting, + including literal blocks:: + + { + 'param1': param1, + 'param2': param2 + } + + Raises: + AttributeError: The ``Raises`` section is a list of all exceptions + that are relevant to the interface. + ValueError: If `param2` is equal to `param1`. + + """ + if param1 == param2: + raise ValueError("param1 may not be equal to param2") + return True + + +def example_generator(n): + """Generators have a ``Yields`` section instead of a ``Returns`` section. + + Args: + n (int): The upper limit of the range to generate, from 0 to `n` - 1. + + Yields: + int: The next number in the range of 0 to `n` - 1. + + Examples: + Examples should be written in doctest format, and should illustrate how + to use the function. + + >>> print([i for i in example_generator(4)]) + [0, 1, 2, 3] + + """ + for i in range(n): + yield i + + +class ExampleError(Exception): + """Exceptions are documented in the same way as classes. + + The __init__ method may be documented in either the class level + docstring, or as a docstring on the __init__ method itself. + + Either form is acceptable, but the two should not be mixed. Choose one + convention to document the __init__ method and be consistent with it. + + Note: + Do not include the `self` parameter in the ``Args`` section. + + Args: + msg (str): Human readable string describing the exception. + code (:obj:`int`, optional): Error code. + + Attributes: + msg (str): Human readable string describing the exception. + code (int): Exception error code. + + """ + + def __init__(self, msg, code): + self.msg = msg + self.code = code + + +class ExampleClass(object): + """The summary line for a class docstring should fit on one line. + + If the class has public attributes, they may be documented here + in an ``Attributes`` section and follow the same formatting as a + function's ``Args`` section. Alternatively, attributes may be documented + inline with the attribute's declaration (see __init__ method below). + + Properties created with the ``@property`` decorator should be documented + in the property's getter method. + + Attributes: + attr1 (str): Description of `attr1`. + attr2 (:obj:`int`, optional): Description of `attr2`. + + """ + + def __init__(self, param1, param2, param3): + """Example of docstring on the __init__ method. + + The __init__ method may be documented in either the class level + docstring, or as a docstring on the __init__ method itself. + + Either form is acceptable, but the two should not be mixed. Choose one + convention to document the __init__ method and be consistent with it. + + Note: + Do not include the `self` parameter in the ``Args`` section. + + Args: + param1 (str): Description of `param1`. + param2 (:obj:`int`, optional): Description of `param2`. Multiple + lines are supported. + param3 (:obj:`list` of :obj:`str`): Description of `param3`. + + """ + self.attr1 = param1 + self.attr2 = param2 + self.attr3 = param3 #: Doc comment *inline* with attribute + + #: list of str: Doc comment *before* attribute, with type specified + self.attr4 = ["attr4"] + + self.attr5 = None + """str: Docstring *after* attribute, with type specified.""" + + @property + def readonly_property(self): + """str: Properties should be documented in their getter method.""" + return "readonly_property" + + @property + def readwrite_property(self): + """:obj:`list` of :obj:`str`: Properties with both a getter and setter + should only be documented in their getter method. + + If the setter method contains notable behavior, it should be + mentioned here. + """ + return ["readwrite_property"] + + @readwrite_property.setter + def readwrite_property(self, value): + value + + def example_method(self, param1, param2): + """Class methods are similar to regular functions. + + Note: + Do not include the `self` parameter in the ``Args`` section. + + Args: + param1: The first parameter. + param2: The second parameter. + + Returns: + True if successful, False otherwise. + + """ + return True + + def __special__(self): + """By default special members with docstrings are not included. + + Special members are any methods or attributes that start with and + end with a double underscore. Any special member with a docstring + will be included in the output, if + ``napoleon_include_special_with_doc`` is set to True. + + This behavior can be enabled by changing the following setting in + Sphinx's conf.py:: + + napoleon_include_special_with_doc = True + + """ + pass + + def __special_without_docstring__(self): + pass + + def _private(self): + """By default private members are not included. + + Private members are any methods or attributes that start with an + underscore and are *not* special. By default they are not included + in the output. + + This behavior can be changed such that private members *are* included + by changing the following setting in Sphinx's conf.py:: + + napoleon_include_private_with_doc = True + + """ + pass + + def _private_without_docstring(self): + pass diff --git a/hrp/internal/version/VERSION b/hrp/internal/version/VERSION index 2bdbc37a..3d9f5b5f 100644 --- a/hrp/internal/version/VERSION +++ b/hrp/internal/version/VERSION @@ -1 +1 @@ -v4.3.0-beta-09302036 \ No newline at end of file +v4.3.0-beta-10081235 \ No newline at end of file diff --git a/httprunner/__init__.py b/httprunner/__init__.py index 08477dd7..f3da2174 100644 --- a/httprunner/__init__.py +++ b/httprunner/__init__.py @@ -1,4 +1,4 @@ -__version__ = "v4.3.0-beta-09302036" +__version__ = "v4.3.0-beta-10081235" __description__ = "One-stop solution for HTTP(S) testing." diff --git a/httprunner/step_android.py b/httprunner/step_android.py new file mode 100644 index 00000000..6a485cc8 --- /dev/null +++ b/httprunner/step_android.py @@ -0,0 +1,136 @@ +from typing import Text + +from loguru import logger +import uiautomator2 as u2 + +from httprunner.models import IStep, StepResult, TStep, TStepAndroidUI +from httprunner.runner import HttpRunner + + +def run_android_ui(runner: HttpRunner, step: TStep) -> StepResult: + step_result = StepResult( + name=step.name, + step_type="android_ui", + success=False, + ) + logger.info(f"run android ui action: {step.android.method}, param: {step.android.param}") + + return step_result + + +class StepAndroidControl(IStep): + + def __init__(self, step: TStep): + self.__step = step + + def start_app(self, package_name: Text) -> "StepAndroidControl": + return self + + def stop_app(self, package_name: Text) -> "StepAndroidControl": + return self + + def start_watcher(self) -> "StepAndroidControl": + return self + + def stop_watcher(self) -> "StepAndroidControl": + return self + + def start_camera(self) -> "StepAndroidControl": + return self + + def stop_camera(self) -> "StepAndroidControl": + return self + + def start_record(self) -> "StepAndroidControl": + return self + + def stop_record(self) -> "StepAndroidControl": + return self + + def struct(self) -> TStep: + return self.__step + + def name(self) -> Text: + return self.__step.name + + def type(self) -> Text: + return "android-control" + + def run(self, runner: HttpRunner): + return run_android_ui(runner, self.__step) + + +class StepAndroidUI(IStep): + + def __init__(self, step: TStep): + self.__step = step + + def press_back(self) -> "StepAndroidUI": + self.__step.android.method = "press" + self.__step.android.param = "back" + return self + + def press_home(self) -> "StepAndroidUI": + self.__step.android.method = "press" + self.__step.android.param = "home" + return self + + def sleep(self, time: int) -> "StepAndroidUI": + self.__step.android.method = "sleep" + self.__step.android.param = time + return self + + def swipe_up(self) -> "StepAndroidUI": + self.__step.android.method = "swipe" + self.__step.android.param = [0.25, 0.5, 0.75, 0.5] + return self + + def swipe_down(self) -> "StepAndroidUI": + self.__step.android.method = "swipe" + self.__step.android.param = [0.75, 0.5, 0.25, 0.5] + return self + + def swipe_left(self) -> "StepAndroidUI": + self.__step.android.method = "swipe" + self.__step.android.param = [0.5, 0.75, 0.5, 0.25] + return self + + def swipe_right(self) -> "StepAndroidUI": + self.__step.android.method = "swipe" + self.__step.android.param = [0.5, 0.25, 0.5, 0.75] + return self + + def swipe(self, from_x: float, from_y: float, to_x: float, to_y: float) -> "StepAndroidUI": + self.__step.android.method = "swipe" + self.__step.android.param = [from_x, from_y, to_x, to_y] + return self + + def click(self, text: Text) -> "StepAndroidUI": + self.__step.android.method = "click" + self.__step.android.param = text + return self + + def struct(self) -> TStep: + return self.__step + + def name(self) -> Text: + return self.__step.name + + def type(self) -> Text: + return "android-ui" + + def run(self, runner: HttpRunner): + return run_android_ui(runner, self.__step) + + +class RunAndroidUI(object): + + def __init__(self, name: Text): + self.__step = TStep(name=name) + self.__step.android = TStepAndroidUI() + + def control(self) -> StepAndroidControl: + return StepAndroidControl(self.__step) + + def ui(self) -> StepAndroidUI: + return StepAndroidUI(self.__step) diff --git a/httprunner/step_android_test.py b/httprunner/step_android_test.py new file mode 100644 index 00000000..fe484a89 --- /dev/null +++ b/httprunner/step_android_test.py @@ -0,0 +1,42 @@ +from httprunner import HttpRunner, Config, Step, RunAndroidUI + + +class TestCaseAndroidDemo(HttpRunner): + + config = ( + Config("demo for android UI test") + .variables( + **{ + "foo1": "config_bar1", + "foo2": "config_bar2", + "expect_foo1": "config_bar1", + "expect_foo2": "config_bar2", + } + ) + .android() + .serial("xxx") + .package_name("xxx") + .install_apk("xxx") + ) + + teststeps = [ + # Step( + # RunAndroidUI("start app").control().start_app("com.ss.android.ugc.aweme") + # ), + Step( + RunAndroidUI("back home").ui().press_home() + ), + Step( + RunAndroidUI("back home").control().start_app() + ), + Step( + RunAndroidUI("swipe up").ui().swipe_up() + ), + Step( + RunAndroidUI("swipe up").ui().swipe_up() + ), + ] + + +if __name__ == "__main__": + TestCaseAndroidDemo().test_start() diff --git a/main.go b/main.go new file mode 100644 index 00000000..5d81a823 --- /dev/null +++ b/main.go @@ -0,0 +1,59 @@ +package main + +import ( + "log" + "os" + "strings" + + "github.com/electricbubble/gadb" +) + +func main() { + adbClient, err := gadb.NewClient() + checkErr(err, "fail to connect adb server") + + devices, err := adbClient.DeviceList() + checkErr(err) + + if len(devices) == 0 { + log.Fatalln("list of devices is empty") + } + + dev := devices[0] + + userHomeDir, _ := os.UserHomeDir() + apk, err := os.Open(userHomeDir + "/Desktop/xuexi_android_10002068.apk") + checkErr(err) + + log.Println("starting to push apk") + + remotePath := "/data/local/tmp/xuexi_android_10002068.apk" + err = dev.PushFile(apk, remotePath) + checkErr(err, "adb push") + + log.Println("push completed") + + log.Println("starting to install apk") + + shellOutput, err := dev.RunShellCommand("pm install", remotePath) + checkErr(err, "pm install") + if !strings.Contains(shellOutput, "Success") { + log.Fatalln("fail to install: ", shellOutput) + } + + log.Println("install completed") + +} + +func checkErr(err error, msg ...string) { + if err == nil { + return + } + + var output string + if len(msg) != 0 { + output = msg[0] + " " + } + output += err.Error() + log.Fatalln(output) +} diff --git a/pyproject.toml b/pyproject.toml index db1b5924..6e10cef3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "httprunner" -version = "v4.3.0-beta-09302036" +version = "v4.3.0-beta-10081235" description = "One-stop solution for HTTP(S) testing." license = "Apache-2.0" readme = "README.md"