diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 987f3d4..05ecd98 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,7 +28,7 @@ jobs: run: | # current dothttp prints to terminal, which is not good for pytest. # although, for test cases, we should ask those who prints to return - pytest -s --html=report.html --self-contained-html + pytest - name: Upload test report uses: actions/upload-artifact@v4 with: diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index 6a667bc..1feeec7 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -32,7 +32,7 @@ jobs: # unit tests need python-magic # package in pypi woudn't need python-magic # for integration tests - pytest -s + pytest - name: Build Distribution run: | # install prerequisites diff --git a/dothttp/__version__.py b/dothttp/__version__.py index fe5e312..9117ccd 100644 --- a/dothttp/__version__.py +++ b/dothttp/__version__.py @@ -1 +1 @@ -__version__ = "0.0.43a24" +__version__ = "0.0.43a25" diff --git a/dothttp/exceptions.py b/dothttp/exceptions.py index 1e3345e..b1d286d 100644 --- a/dothttp/exceptions.py +++ b/dothttp/exceptions.py @@ -89,12 +89,12 @@ class PayloadDataNotValidException(PayloadNotValidException): pass -@exception_wrapper("python/js test script compilation failed `{payload}`") +@exception_wrapper("PreRequest Function:`{function}` failed with error: `{payload}`") class PreRequestScriptException(DotHttpException): pass -@exception_wrapper("python/js test script compilation failed `{payload}`") +@exception_wrapper("Python test function: `{function}` execution failed `{payload}`") class ScriptException(DotHttpException): pass diff --git a/dothttp/http.tx b/dothttp/http.tx index 560d74c5..be5e2bb 100644 --- a/dothttp/http.tx +++ b/dothttp/http.tx @@ -177,7 +177,7 @@ PAYLOAD: ; TRIPLE_OR_DOUBLE_STRING: - triple = TRIPLE_QUOTE_STRING | str = STRING + triple = TRIPLE_QUOTE_STRING | str = STRING | str = ID ; @@ -208,7 +208,7 @@ Array: ; Value: - strs += TRIPLE_OR_DOUBLE_STRING | var=VarString | flt=Float | int=Int | bl=Bool | object=Object | array=Array | null="null" | expr=Expression + flt=Float | int=Int | bl=Bool | null="null" |strs += TRIPLE_OR_DOUBLE_STRING | var=VarString | object=Object | array=Array | expr=Expression ; Expression: @@ -249,8 +249,8 @@ Object: ; Member: - (key+=TRIPLE_OR_DOUBLE_STRING ':' value=Value) | - (var=VarString ':' value=Value) + (key+=TRIPLE_OR_DOUBLE_STRING (':'|'=') value=Value) | + (var=VarString (':'|'=') value=Value) ; Comment: UnixComment | CommentLine | CommentBlock diff --git a/dothttp/script/js3py.py b/dothttp/script/js3py.py index 09791fb..f4b59e9 100644 --- a/dothttp/script/js3py.py +++ b/dothttp/script/js3py.py @@ -199,11 +199,13 @@ def init_request_script(self): def pre_request_script(self): try: self._pre_request_script() + except PreRequestScriptException: + raise except Exception as exc: request_logger.error("unknown exception happened", exc_info=True) - raise PreRequestScriptException(payload=str(exc)) + raise PreRequestScriptException(payload=str(exc), function="''") - def execute_test_script(self, resp): + def execute_test_script(self, resp) -> ScriptResult: if not self.client.request.test_script: return ScriptResult(stdout="", error="", properties={}, tests=[]) try: @@ -235,7 +237,7 @@ def _execute_test_script(self, resp) -> None: js_template.replace("JS_CODE_REPLACE", self.client.request.test_script) ) except JsException as exc: - raise ScriptException(payload=str(exc)) + raise ScriptException(payload=str(exc), function="''") content_type = resp.headers.get("content-type", "text/plain") # in some cases mimetype can have charset # like text/plain; charset=utf-8 @@ -279,7 +281,7 @@ def __init__(self, httpdef: HttpDef, prop: PropertyProvider) -> None: ) exec(byte_code, script_gloabal, self.local) except Exception as exc: - raise ScriptException(payload=str(exc)) + raise ScriptException(payload=str(exc), function="test_script.py") def _init_request_script(self) -> None: # just for variables initialization @@ -288,9 +290,13 @@ def _init_request_script(self) -> None: func() def _pre_request_script(self) -> None: - for key, func in self.local.items(): - if (key.startswith("pre")) and isinstance(func, types.FunctionType): - func() + for key, pre_request_func in self.local.items(): + if (key.startswith("pre")) and isinstance(pre_request_func, types.FunctionType): + try: + pre_request_func() + except Exception as exc: + request_logger.error(f"pre request script failed {exc}") + raise PreRequestScriptException(function=key, payload=str(exc)) def _execute_test_script(self, resp: Response) -> ScriptResult: script_result = ScriptResult(stdout="", error="", properties={}, tests=[]) @@ -306,7 +312,7 @@ def _execute_test_script(self, resp: Response) -> ScriptResult: test_result.result = func() test_result.success = True except BaseException as exc: - test_result.error = str(exc) + test_result.error = f"\n\nTest function with name `{key}` failed with error `{exc}`\n\n" test_result.success = False request_logger.error(f"test execution failed {exc}") script_result.tests.append(test_result) diff --git a/dothttp_test/__init__.py b/dothttp_test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dothttp_test/__main__.py b/dothttp_test/__main__.py new file mode 100644 index 0000000..f056203 --- /dev/null +++ b/dothttp_test/__main__.py @@ -0,0 +1,8 @@ +import pytest +from .conftest import * + +if __name__ == "__main__": + # run pytest with the current module + # this will not work because of arguments + # use `pytest dothttp_test --directory test/extensions/commands/names.http --prefix test -v -s --html=report.html` + pytest.main(["dothttp_test", "-v", "-s", "--html=report.html"]) diff --git a/dothttp_test/conftest.py b/dothttp_test/conftest.py new file mode 100644 index 0000000..f84f962 --- /dev/null +++ b/dothttp_test/conftest.py @@ -0,0 +1,26 @@ +def pytest_addoption(parser): + parser.addoption( + "--prefix", action="store", type=str, help="prefix for httpdef name", default="*" + ) + parser.addoption( + "--directory", + action="store", + nargs="+", + help="list of directories to search for httpdef files", + default=["."], + ) + parser.addoption("--property-file", help="property file") + parser.addoption( + "--no-cookie", + help="cookie storage is disabled", + action="store_const", + const=True, + ) + parser.addoption( + "--env", + help="environment to select in property file. properties will be enabled on FIFO", + nargs="+", + default=["*"], + ) + parser.addoption("--property", help="list of property's", + nargs="+", default=[]) diff --git a/dothttp_test/dothttp_test.py b/dothttp_test/dothttp_test.py new file mode 100644 index 0000000..b59af41 --- /dev/null +++ b/dothttp_test/dothttp_test.py @@ -0,0 +1,125 @@ +import os +from collections import namedtuple +import logging + +from dothttp.parse import ( + Config, +) +from dothttp.parse.request_base import ( + RequestCompiler, + dothttp_model, +) +from dotextensions.server.handlers.basic_handlers import RunHttpFileHandler + + +LOGGER = logging.getLogger(__name__) + + +HttpDefTest = namedtuple( + "HttpDef", ["display", "file", "name", "config", "failed"]) + + +def pytest_generate_tests(metafunc): + prefix = metafunc.config.getoption("prefix") + directory = metafunc.config.getoption("directory") + # iterate through all sub directories in the directory + # and get all httpdef files + # and pass it to the test function + tests_to_run = [] + + for dir in directory: + if not os.path.exists(dir): + LOGGER.error(f"{dir} does not exists") + continue + if os.path.isfile(dir): + tests_to_run += extrct_tests_to_run(metafunc, + prefix, dir) + else: + for root, _dirs, files in os.walk(dir): + for filename in files: + tests_to_run += extrct_tests_to_run( + metafunc, prefix, os.path.join(root, filename)) + metafunc.parametrize("httpdeftest", tests_to_run, ids=lambda x: x.display) + + +def extrct_tests_to_run(metafunc, prefix, filename): + tests_to_run = [] + if filename.endswith(".http"): + with open(os.path.join(filename)) as f: + httpdef = f.read() + try: + model = dothttp_model.model_from_str(httpdef) + for http in model.allhttps: + if prefix is not None: + if prefix == "*": + tests_to_run.append( + HttpDefTest( + file=filename, + name=http.namewrap.name, + display=f"{filename} - name={http.namewrap.name}", + config=metafunc.config, + failed=False + ) + ) + elif http.namewrap and http.namewrap.name.startswith(prefix): + tests_to_run.append( + HttpDefTest( + file=filename, + name=http.namewrap.name, + display=f"{filename} - name={http.namewrap.name}", + config=metafunc.config, + failed=False + ) + ) + except: + tests_to_run.append( + HttpDefTest( + file=filename, + name="entire_file", + display=f"{filename} - name='entire_file'", + config=metafunc.config, + failed=True + ) + ) + LOGGER.error(f"error in parsing {filename}") + return tests_to_run + + +def test_httpdef(httpdeftest: HttpDefTest): + if httpdeftest.failed: + assert False, "failed to parse" + # read below arguments from command line + config = Config( + file=httpdeftest.file, + target=httpdeftest.name, + property_file=httpdeftest.config.getoption("property_file"), + env=httpdeftest.config.getoption("env"), + properties=httpdeftest.config.getoption("property"), + debug=True, + info=True, + curl=False, + no_cookie=False, + format=False, + ) + comp = RequestCompiler(config) + + realized_http_def_content = RunHttpFileHandler.get_http_from_req( + comp.httpdef) + + logging.warning( + f"realized_http_def_content {realized_http_def_content}", + ) + + resp = comp.get_response() + + logging.warning(f"resp={resp}") + + script_result = comp.script_execution.execute_test_script(resp) + + for test in script_result.tests: + # at present only one failure is reported in the test. + # if there are multiple failures, only the first one is reported + logging.warning( + f"script_result: test={test}, test_success: {test.success}, error:{test.error}", + ) + assert test.success, test.error diff --git a/examples/example.http b/examples/example.http index 5900187..9998db5 100644 --- a/examples/example.http +++ b/examples/example.http @@ -13,7 +13,9 @@ POST https://httpbin.org/post "lastName": {{lastName='Doe'}}, "age": {{age=25}}, "fullName": "{{firstName}} {{lastName}}", - "ageInDays": (25*365), + ageInDays: (25*365), + // sample without no quotes + description: sampleOneWithNoQuotesInValue } > {% diff --git a/pyproject.toml b/pyproject.toml index 9029ddc..cf7cdc9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "dothttp-req" -version = "0.0.43a24" +version = "0.0.43a25" description = "Dothttp is Simple http client for testing and development" authors = ["Prasanth "] license = "MIT" @@ -10,7 +10,7 @@ classifiers = [ "Operating System :: OS Independent", ] exclude = ["test", "test.core", "test.extensions", "benchmarks"] -packages = [{'include'='dotextensions'}, {'include'='dothttp'}, {'include'='dothttp_req'}] +packages = [{'include'='dotextensions'}, {'include'='dothttp'}, {'include'='dothttp_req'}, {'include'='dothttp_test'}] repository = "https://github.com/cedric05/dothttp" [tool.poetry.urls] diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..8a554c9 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +addopts = --ignore=dothttp_test -s diff --git a/test/core/payload/jsonpayload2.http b/test/core/payload/jsonpayload2.http index 5ec902a..8eb5be3 100644 --- a/test/core/payload/jsonpayload2.http +++ b/test/core/payload/jsonpayload2.http @@ -5,6 +5,11 @@ json({ "null": null, "bool": false, "bool2": true, -"float":1.121212, -"float2":1 +"float" : 1.121212, +"float2":1, +"number":0, +testWithNoQuotes: true, +testNoQuotesAnd_Number1: true, +testWithQuotesAndEqual = true, + }) diff --git a/test/core/test_payload.py b/test/core/test_payload.py index 33ee5a6..5b02c58 100644 --- a/test/core/test_payload.py +++ b/test/core/test_payload.py @@ -27,8 +27,20 @@ def test_json_payload2(self): ) self.assertEqual("POST", req.method, "incorrect method") self.assertEqual( - b'{"string": "simple", "list": ["dothttp", "azure"], "null": null, "bool": false, "bool2": true, "float": 1.121212, "float2": 1}', - req.body, + { + "string": "simple", + "list": ["dothttp", "azure"], + "null": None, + "bool": False, + "bool2": True, + "float": 1.121212, + "float2": 1, + "number": 0, + "testWithNoQuotes": True, + "testNoQuotesAnd_Number1": True, + "testWithQuotesAndEqual": True + }, + json.loads(req.body), "incorrect method", )