完善各类通知,企业微信、钉钉、邮件
This commit is contained in:
parent
ffe6fb2553
commit
744b26c5d8
|
@ -42,32 +42,42 @@
|
||||||
<option name="hideEmptyMiddlePackages" value="true" />
|
<option name="hideEmptyMiddlePackages" value="true" />
|
||||||
<option name="showLibraryContents" value="true" />
|
<option name="showLibraryContents" value="true" />
|
||||||
</component>
|
</component>
|
||||||
<component name="PropertiesComponent">{
|
<component name="PropertiesComponent"><![CDATA[{
|
||||||
"keyToString": {
|
"keyToString": {
|
||||||
"RunOnceActivity.OpenProjectViewOnStart": "true",
|
"RunOnceActivity.OpenProjectViewOnStart": "true",
|
||||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||||
"WebServerToolWindowFactoryState": "false",
|
"WebServerToolWindowFactoryState": "false",
|
||||||
"git-widget-placeholder": "master",
|
"git-widget-placeholder": "master",
|
||||||
"last_opened_file_path": "D:/app/apitest",
|
"last_opened_file_path": "D:/app/apitest/image",
|
||||||
"node.js.detected.package.eslint": "true",
|
"node.js.detected.package.eslint": "true",
|
||||||
"node.js.detected.package.tslint": "true",
|
"node.js.detected.package.tslint": "true",
|
||||||
"node.js.selected.package.eslint": "(autodetect)",
|
"node.js.selected.package.eslint": "(autodetect)",
|
||||||
"node.js.selected.package.tslint": "(autodetect)",
|
"node.js.selected.package.tslint": "(autodetect)",
|
||||||
"nodejs_package_manager_path": "npm",
|
"nodejs_package_manager_path": "npm",
|
||||||
"settings.editor.selected.configurable": "com.jetbrains.python.configuration.PyActiveSdkModuleConfigurable",
|
"settings.editor.selected.configurable": "com.jetbrains.python.configuration.PyActiveSdkModuleConfigurable",
|
||||||
"vue.rearranger.settings.migration": "true"
|
"vue.rearranger.settings.migration": "true"
|
||||||
|
},
|
||||||
|
"keyToStringList": {
|
||||||
|
"DatabaseDriversLRU": [
|
||||||
|
"mysql"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}</component>
|
}]]></component>
|
||||||
<component name="RecentsManager">
|
<component name="RecentsManager">
|
||||||
|
<key name="CopyFile.RECENT_KEYS">
|
||||||
|
<recent name="D:\app\apitest\image" />
|
||||||
|
<recent name="D:\app\apitest" />
|
||||||
|
</key>
|
||||||
<key name="MoveFile.RECENT_KEYS">
|
<key name="MoveFile.RECENT_KEYS">
|
||||||
|
<recent name="D:\app\apitest" />
|
||||||
<recent name="D:\app\apitest\encryption_rules" />
|
<recent name="D:\app\apitest\encryption_rules" />
|
||||||
<recent name="D:\app\apitest\common\validation" />
|
<recent name="D:\app\apitest\common\validation" />
|
||||||
<recent name="D:\app\apitest\common\log_utils" />
|
<recent name="D:\app\apitest\common\log_utils" />
|
||||||
<recent name="D:\app\apitest\common\parsing" />
|
<recent name="D:\app\apitest\common\parsing" />
|
||||||
</key>
|
</key>
|
||||||
</component>
|
</component>
|
||||||
<component name="RunManager" selected="Python 测试.Python 测试 (test_executor.py 内)">
|
<component name="RunManager" selected="Python.run">
|
||||||
<configuration name="assert_dict" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true">
|
<configuration name="dingding" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true">
|
||||||
<module name="api_project" />
|
<module name="api_project" />
|
||||||
<option name="INTERPRETER_OPTIONS" value="" />
|
<option name="INTERPRETER_OPTIONS" value="" />
|
||||||
<option name="PARENT_ENVS" value="true" />
|
<option name="PARENT_ENVS" value="true" />
|
||||||
|
@ -75,12 +85,12 @@
|
||||||
<env name="PYTHONUNBUFFERED" value="1" />
|
<env name="PYTHONUNBUFFERED" value="1" />
|
||||||
</envs>
|
</envs>
|
||||||
<option name="SDK_HOME" value="" />
|
<option name="SDK_HOME" value="" />
|
||||||
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/common/data_extraction" />
|
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/common/notiction" />
|
||||||
<option name="IS_MODULE_SDK" value="true" />
|
<option name="IS_MODULE_SDK" value="true" />
|
||||||
<option name="ADD_CONTENT_ROOTS" value="true" />
|
<option name="ADD_CONTENT_ROOTS" value="true" />
|
||||||
<option name="ADD_SOURCE_ROOTS" value="true" />
|
<option name="ADD_SOURCE_ROOTS" value="true" />
|
||||||
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
|
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
|
||||||
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/common/data_extraction/assert_dict.py" />
|
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/common/notiction/dingding.py" />
|
||||||
<option name="PARAMETERS" value="" />
|
<option name="PARAMETERS" value="" />
|
||||||
<option name="SHOW_COMMAND_LINE" value="false" />
|
<option name="SHOW_COMMAND_LINE" value="false" />
|
||||||
<option name="EMULATE_TERMINAL" value="false" />
|
<option name="EMULATE_TERMINAL" value="false" />
|
||||||
|
@ -89,7 +99,7 @@
|
||||||
<option name="INPUT_FILE" value="" />
|
<option name="INPUT_FILE" value="" />
|
||||||
<method v="2" />
|
<method v="2" />
|
||||||
</configuration>
|
</configuration>
|
||||||
<configuration name="dependent_parameter" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true">
|
<configuration name="email_client (1)" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true">
|
||||||
<module name="api_project" />
|
<module name="api_project" />
|
||||||
<option name="INTERPRETER_OPTIONS" value="" />
|
<option name="INTERPRETER_OPTIONS" value="" />
|
||||||
<option name="PARENT_ENVS" value="true" />
|
<option name="PARENT_ENVS" value="true" />
|
||||||
|
@ -97,12 +107,12 @@
|
||||||
<env name="PYTHONUNBUFFERED" value="1" />
|
<env name="PYTHONUNBUFFERED" value="1" />
|
||||||
</envs>
|
</envs>
|
||||||
<option name="SDK_HOME" value="" />
|
<option name="SDK_HOME" value="" />
|
||||||
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/common/data_extraction" />
|
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/common/notiction" />
|
||||||
<option name="IS_MODULE_SDK" value="true" />
|
<option name="IS_MODULE_SDK" value="true" />
|
||||||
<option name="ADD_CONTENT_ROOTS" value="true" />
|
<option name="ADD_CONTENT_ROOTS" value="true" />
|
||||||
<option name="ADD_SOURCE_ROOTS" value="true" />
|
<option name="ADD_SOURCE_ROOTS" value="true" />
|
||||||
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
|
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
|
||||||
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/common/data_extraction/dependent_parameter.py" />
|
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/common/notiction/email_client.py" />
|
||||||
<option name="PARAMETERS" value="" />
|
<option name="PARAMETERS" value="" />
|
||||||
<option name="SHOW_COMMAND_LINE" value="false" />
|
<option name="SHOW_COMMAND_LINE" value="false" />
|
||||||
<option name="EMULATE_TERMINAL" value="false" />
|
<option name="EMULATE_TERMINAL" value="false" />
|
||||||
|
@ -111,7 +121,7 @@
|
||||||
<option name="INPUT_FILE" value="" />
|
<option name="INPUT_FILE" value="" />
|
||||||
<method v="2" />
|
<method v="2" />
|
||||||
</configuration>
|
</configuration>
|
||||||
<configuration name="extractor" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true">
|
<configuration name="email_client" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true">
|
||||||
<module name="api_project" />
|
<module name="api_project" />
|
||||||
<option name="INTERPRETER_OPTIONS" value="" />
|
<option name="INTERPRETER_OPTIONS" value="" />
|
||||||
<option name="PARENT_ENVS" value="true" />
|
<option name="PARENT_ENVS" value="true" />
|
||||||
|
@ -119,12 +129,12 @@
|
||||||
<env name="PYTHONUNBUFFERED" value="1" />
|
<env name="PYTHONUNBUFFERED" value="1" />
|
||||||
</envs>
|
</envs>
|
||||||
<option name="SDK_HOME" value="" />
|
<option name="SDK_HOME" value="" />
|
||||||
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/common/validation" />
|
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/common/notiction" />
|
||||||
<option name="IS_MODULE_SDK" value="true" />
|
<option name="IS_MODULE_SDK" value="true" />
|
||||||
<option name="ADD_CONTENT_ROOTS" value="true" />
|
<option name="ADD_CONTENT_ROOTS" value="true" />
|
||||||
<option name="ADD_SOURCE_ROOTS" value="true" />
|
<option name="ADD_SOURCE_ROOTS" value="true" />
|
||||||
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
|
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
|
||||||
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/common/validation/extractor.py" />
|
<option name="SCRIPT_NAME" value="D:\app\apitest\common\notiction\email_client.py" />
|
||||||
<option name="PARAMETERS" value="" />
|
<option name="PARAMETERS" value="" />
|
||||||
<option name="SHOW_COMMAND_LINE" value="false" />
|
<option name="SHOW_COMMAND_LINE" value="false" />
|
||||||
<option name="EMULATE_TERMINAL" value="false" />
|
<option name="EMULATE_TERMINAL" value="false" />
|
||||||
|
@ -133,7 +143,7 @@
|
||||||
<option name="INPUT_FILE" value="" />
|
<option name="INPUT_FILE" value="" />
|
||||||
<method v="2" />
|
<method v="2" />
|
||||||
</configuration>
|
</configuration>
|
||||||
<configuration name="validator" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true">
|
<configuration name="resultPush" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true">
|
||||||
<module name="api_project" />
|
<module name="api_project" />
|
||||||
<option name="INTERPRETER_OPTIONS" value="" />
|
<option name="INTERPRETER_OPTIONS" value="" />
|
||||||
<option name="PARENT_ENVS" value="true" />
|
<option name="PARENT_ENVS" value="true" />
|
||||||
|
@ -141,12 +151,12 @@
|
||||||
<env name="PYTHONUNBUFFERED" value="1" />
|
<env name="PYTHONUNBUFFERED" value="1" />
|
||||||
</envs>
|
</envs>
|
||||||
<option name="SDK_HOME" value="" />
|
<option name="SDK_HOME" value="" />
|
||||||
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/common/validation" />
|
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/unittestreportnew/core" />
|
||||||
<option name="IS_MODULE_SDK" value="true" />
|
<option name="IS_MODULE_SDK" value="true" />
|
||||||
<option name="ADD_CONTENT_ROOTS" value="true" />
|
<option name="ADD_CONTENT_ROOTS" value="true" />
|
||||||
<option name="ADD_SOURCE_ROOTS" value="true" />
|
<option name="ADD_SOURCE_ROOTS" value="true" />
|
||||||
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
|
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
|
||||||
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/common/validation/validator.py" />
|
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/unittestreportnew/core/resultPush.py" />
|
||||||
<option name="PARAMETERS" value="" />
|
<option name="PARAMETERS" value="" />
|
||||||
<option name="SHOW_COMMAND_LINE" value="false" />
|
<option name="SHOW_COMMAND_LINE" value="false" />
|
||||||
<option name="EMULATE_TERMINAL" value="false" />
|
<option name="EMULATE_TERMINAL" value="false" />
|
||||||
|
@ -155,28 +165,35 @@
|
||||||
<option name="INPUT_FILE" value="" />
|
<option name="INPUT_FILE" value="" />
|
||||||
<method v="2" />
|
<method v="2" />
|
||||||
</configuration>
|
</configuration>
|
||||||
<configuration name="Python 测试 (test_executor.py 内)" type="tests" factoryName="Autodetect" temporary="true" nameIsGenerated="true">
|
<configuration name="run" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true">
|
||||||
<module name="api_project" />
|
<module name="api_project" />
|
||||||
<option name="INTERPRETER_OPTIONS" value="" />
|
<option name="INTERPRETER_OPTIONS" value="" />
|
||||||
<option name="PARENT_ENVS" value="true" />
|
<option name="PARENT_ENVS" value="true" />
|
||||||
|
<envs>
|
||||||
|
<env name="PYTHONUNBUFFERED" value="1" />
|
||||||
|
</envs>
|
||||||
<option name="SDK_HOME" value="" />
|
<option name="SDK_HOME" value="" />
|
||||||
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/test_script" />
|
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
|
||||||
<option name="IS_MODULE_SDK" value="true" />
|
<option name="IS_MODULE_SDK" value="true" />
|
||||||
<option name="ADD_CONTENT_ROOTS" value="true" />
|
<option name="ADD_CONTENT_ROOTS" value="true" />
|
||||||
<option name="ADD_SOURCE_ROOTS" value="true" />
|
<option name="ADD_SOURCE_ROOTS" value="true" />
|
||||||
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
|
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
|
||||||
<option name="_new_additionalArguments" value="""" />
|
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/run.py" />
|
||||||
<option name="_new_target" value=""$PROJECT_DIR$/test_script/test_executor.py"" />
|
<option name="PARAMETERS" value="" />
|
||||||
<option name="_new_targetType" value=""PATH"" />
|
<option name="SHOW_COMMAND_LINE" value="false" />
|
||||||
|
<option name="EMULATE_TERMINAL" value="false" />
|
||||||
|
<option name="MODULE_MODE" value="false" />
|
||||||
|
<option name="REDIRECT_INPUT" value="false" />
|
||||||
|
<option name="INPUT_FILE" value="" />
|
||||||
<method v="2" />
|
<method v="2" />
|
||||||
</configuration>
|
</configuration>
|
||||||
<recent_temporary>
|
<recent_temporary>
|
||||||
<list>
|
<list>
|
||||||
<item itemvalue="Python 测试.Python 测试 (test_executor.py 内)" />
|
<item itemvalue="Python.run" />
|
||||||
<item itemvalue="Python.assert_dict" />
|
<item itemvalue="Python.dingding" />
|
||||||
<item itemvalue="Python.validator" />
|
<item itemvalue="Python.email_client (1)" />
|
||||||
<item itemvalue="Python.extractor" />
|
<item itemvalue="Python.email_client" />
|
||||||
<item itemvalue="Python.dependent_parameter" />
|
<item itemvalue="Python.resultPush" />
|
||||||
</list>
|
</list>
|
||||||
</recent_temporary>
|
</recent_temporary>
|
||||||
</component>
|
</component>
|
||||||
|
@ -209,7 +226,8 @@
|
||||||
<workItem from="1690855882311" duration="1981000" />
|
<workItem from="1690855882311" duration="1981000" />
|
||||||
<workItem from="1690873614157" duration="4828000" />
|
<workItem from="1690873614157" duration="4828000" />
|
||||||
<workItem from="1690937027137" duration="37912000" />
|
<workItem from="1690937027137" duration="37912000" />
|
||||||
<workItem from="1691034024300" duration="1062000" />
|
<workItem from="1691034024300" duration="10612000" />
|
||||||
|
<workItem from="1691370512935" duration="50653000" />
|
||||||
</task>
|
</task>
|
||||||
<task id="LOCAL-00001" summary="优化代码">
|
<task id="LOCAL-00001" summary="优化代码">
|
||||||
<option name="closed" value="true" />
|
<option name="closed" value="true" />
|
||||||
|
@ -333,17 +351,6 @@
|
||||||
<MESSAGE value="增加处理函数调用链变量以及修复动态函数中传参失效的问题" />
|
<MESSAGE value="增加处理函数调用链变量以及修复动态函数中传参失效的问题" />
|
||||||
<option name="LAST_COMMIT_MESSAGE" value="增加处理函数调用链变量以及修复动态函数中传参失效的问题" />
|
<option name="LAST_COMMIT_MESSAGE" value="增加处理函数调用链变量以及修复动态函数中传参失效的问题" />
|
||||||
</component>
|
</component>
|
||||||
<component name="XDebuggerManager">
|
|
||||||
<breakpoint-manager>
|
|
||||||
<breakpoints>
|
|
||||||
<line-breakpoint enabled="true" suspend="THREAD" type="python-line">
|
|
||||||
<url>file://$PROJECT_DIR$/common/validation/extractor.py</url>
|
|
||||||
<line>10</line>
|
|
||||||
<option name="timeStamp" value="12" />
|
|
||||||
</line-breakpoint>
|
|
||||||
</breakpoints>
|
|
||||||
</breakpoint-manager>
|
|
||||||
</component>
|
|
||||||
<component name="com.github.evgenys91.machinet.common.dslhistory.DslHistoryState">
|
<component name="com.github.evgenys91.machinet.common.dslhistory.DslHistoryState">
|
||||||
<option name="historyDtoById">
|
<option name="historyDtoById">
|
||||||
<map>
|
<map>
|
||||||
|
@ -362,23 +369,30 @@
|
||||||
</option>
|
</option>
|
||||||
</component>
|
</component>
|
||||||
<component name="com.intellij.coverage.CoverageDataManagerImpl">
|
<component name="com.intellij.coverage.CoverageDataManagerImpl">
|
||||||
<SUITE FILE_PATH="coverage/apitest$load_modules_from_folder.coverage" NAME="load_modules_from_folder 覆盖结果" MODIFIED="1690882059703" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="true" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/common/validation" />
|
<SUITE FILE_PATH="coverage/apitest$load_modules_from_folder.coverage" NAME="load_modules_from_folder 覆盖结果" MODIFIED="1691398471049" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="true" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/common/validation" />
|
||||||
<SUITE FILE_PATH="coverage/apitest$Unittest__test_executor_py__.coverage" NAME="Unittest (test_executor.py 内) 覆盖结果" MODIFIED="1690850721335" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="true" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/test_script" />
|
<SUITE FILE_PATH="coverage/apitest$run.coverage" NAME="run 覆盖结果" MODIFIED="1691489293524" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="true" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
|
||||||
<SUITE FILE_PATH="coverage/apitest$http_client.coverage" NAME="http_client 覆盖结果" MODIFIED="1690850772526" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="true" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/common/http_client" />
|
|
||||||
<SUITE FILE_PATH="coverage/apitest$Unittest__test_api_py__.coverage" NAME="Unittest (test_api.py 内) 覆盖结果" MODIFIED="1689907531802" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="true" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/test_script" />
|
<SUITE FILE_PATH="coverage/apitest$Unittest__test_api_py__.coverage" NAME="Unittest (test_api.py 内) 覆盖结果" MODIFIED="1689907531802" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="true" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/test_script" />
|
||||||
<SUITE FILE_PATH="coverage/apitest$extractor.coverage" NAME="extractor 覆盖结果" MODIFIED="1691031544934" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="true" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/common/validation" />
|
<SUITE FILE_PATH="coverage/apitest$dingding.coverage" NAME="dingding 覆盖结果" MODIFIED="1691483158998" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="true" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/common/notiction" />
|
||||||
<SUITE FILE_PATH="coverage/apitest$dependent_parameter.coverage" NAME="dependent_parameter 覆盖结果" MODIFIED="1691028535712" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="true" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/common/data_extraction" />
|
|
||||||
<SUITE FILE_PATH="coverage/apitest$assert_dict.coverage" NAME="assert_dict 覆盖结果" MODIFIED="1691034548959" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="true" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/common/data_extraction" />
|
<SUITE FILE_PATH="coverage/apitest$assert_dict.coverage" NAME="assert_dict 覆盖结果" MODIFIED="1691034548959" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="true" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/common/data_extraction" />
|
||||||
|
<SUITE FILE_PATH="coverage/apitest$dependent_parameter.coverage" NAME="dependent_parameter 覆盖结果" MODIFIED="1691398478714" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="true" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/common/data_extraction" />
|
||||||
<SUITE FILE_PATH="coverage/apitest$re_chain.coverage" NAME="re_chain 覆盖结果" MODIFIED="1690962595251" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="true" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/debug" />
|
<SUITE FILE_PATH="coverage/apitest$re_chain.coverage" NAME="re_chain 覆盖结果" MODIFIED="1690962595251" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="true" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/debug" />
|
||||||
<SUITE FILE_PATH="coverage/apitest$validator.coverage" NAME="validator 覆盖结果" MODIFIED="1691032283545" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="true" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/common/validation" />
|
<SUITE FILE_PATH="coverage/apitest$validator.coverage" NAME="validator 覆盖结果" MODIFIED="1691395818973" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="true" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/common/validation" />
|
||||||
<SUITE FILE_PATH="coverage/apitest$loaders.coverage" NAME="loaders 覆盖结果" MODIFIED="1690942567666" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="true" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/common/validation" />
|
<SUITE FILE_PATH="coverage/apitest$exceptions.coverage" NAME="exceptions 覆盖结果" MODIFIED="1691395709236" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="true" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/common/utils" />
|
||||||
<SUITE FILE_PATH="coverage/apitest$.coverage" NAME=" 覆盖结果" MODIFIED="1691035043685" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="true" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/test_script" />
|
|
||||||
<SUITE FILE_PATH="coverage/apitest$requestRecord__1_.coverage" NAME="requestRecord (1) 覆盖结果" MODIFIED="1690531988570" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="true" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/debug" />
|
<SUITE FILE_PATH="coverage/apitest$requestRecord__1_.coverage" NAME="requestRecord (1) 覆盖结果" MODIFIED="1690531988570" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="true" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/debug" />
|
||||||
<SUITE FILE_PATH="coverage/apitest$__init__.coverage" NAME="__init__ 覆盖结果" MODIFIED="1691028135541" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="true" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/debug" />
|
<SUITE FILE_PATH="coverage/apitest$__init__.coverage" NAME="__init__ 覆盖结果" MODIFIED="1691028135541" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="true" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/debug" />
|
||||||
<SUITE FILE_PATH="coverage/apitest$method_chain.coverage" NAME="method_chain 覆盖结果" MODIFIED="1690967090148" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="true" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/debug" />
|
|
||||||
<SUITE FILE_PATH="coverage/apitest$action.coverage" NAME="action 覆盖结果" MODIFIED="1689907783681" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="true" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/common" />
|
<SUITE FILE_PATH="coverage/apitest$action.coverage" NAME="action 覆盖结果" MODIFIED="1689907783681" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="true" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/common" />
|
||||||
|
<SUITE FILE_PATH="coverage/apitest$mysql_client.coverage" NAME="mysql_client 覆盖结果" MODIFIED="1691399388558" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="true" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/common/database" />
|
||||||
|
<SUITE FILE_PATH="coverage/apitest$Unittest__test_executor_py__.coverage" NAME="Unittest (test_executor.py 内) 覆盖结果" MODIFIED="1690850721335" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="true" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/test_script" />
|
||||||
|
<SUITE FILE_PATH="coverage/apitest$http_client.coverage" NAME="http_client 覆盖结果" MODIFIED="1690850772526" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="true" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/common/http_client" />
|
||||||
|
<SUITE FILE_PATH="coverage/apitest$email.coverage" NAME="email_client 覆盖结果" MODIFIED="1691465734077" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="true" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/common/notiction" />
|
||||||
|
<SUITE FILE_PATH="coverage/apitest$extractor.coverage" NAME="extractor 覆盖结果" MODIFIED="1691031544934" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="true" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/common/validation" />
|
||||||
|
<SUITE FILE_PATH="coverage/apitest$email_client__1_.coverage" NAME="email_client (1) 覆盖结果" MODIFIED="1691481071975" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="true" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/common/notiction" />
|
||||||
|
<SUITE FILE_PATH="coverage/apitest$loaders.coverage" NAME="loaders 覆盖结果" MODIFIED="1690942567666" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="true" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/common/validation" />
|
||||||
|
<SUITE FILE_PATH="coverage/apitest$.coverage" NAME=" 覆盖结果" MODIFIED="1691371445339" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="true" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/test_script" />
|
||||||
|
<SUITE FILE_PATH="coverage/apitest$resultPush.coverage" NAME="resultPush 覆盖结果" MODIFIED="1691465454364" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="true" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/unittestreportnew/core" />
|
||||||
|
<SUITE FILE_PATH="coverage/apitest$method_chain.coverage" NAME="method_chain 覆盖结果" MODIFIED="1690967090148" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="true" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/debug" />
|
||||||
<SUITE FILE_PATH="coverage/apitest$encryption_str.coverage" NAME="encryption_str 覆盖结果" MODIFIED="1690796164466" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="true" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/common/crypto" />
|
<SUITE FILE_PATH="coverage/apitest$encryption_str.coverage" NAME="encryption_str 覆盖结果" MODIFIED="1690796164466" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="true" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/common/crypto" />
|
||||||
<SUITE FILE_PATH="coverage/apitest$mysql_client.coverage" NAME="mysql_client 覆盖结果" MODIFIED="1690797332029" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="true" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/common/database" />
|
<SUITE FILE_PATH="coverage/apitest$data_extractor.coverage" NAME="data_extractor 覆盖结果" MODIFIED="1691398486016" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="true" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/common/data_extraction" />
|
||||||
<SUITE FILE_PATH="coverage/apitest$get_set.coverage" NAME="get_set 覆盖结果" MODIFIED="1689930576115" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="true" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/debug" />
|
<SUITE FILE_PATH="coverage/apitest$get_set.coverage" NAME="get_set 覆盖结果" MODIFIED="1689930576115" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="true" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/debug" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
|
@ -7,13 +7,11 @@
|
||||||
@time: 2023/6/16 15:43
|
@time: 2023/6/16 15:43
|
||||||
@desc:
|
@desc:
|
||||||
"""
|
"""
|
||||||
from common.crypto import logger
|
|
||||||
from common.crypto.encryption_rsa import Rsa
|
from common.crypto.encryption_rsa import Rsa
|
||||||
# from common.crypto.encryption_aes import DoAES
|
from common.utils.exceptions import EncryptionError
|
||||||
from encryption_rules import rules
|
from encryption_rules import rules
|
||||||
|
|
||||||
|
|
||||||
@logger.log_decorator()
|
|
||||||
class EncryptData:
|
class EncryptData:
|
||||||
"""
|
"""
|
||||||
数据加密入口
|
数据加密入口
|
||||||
|
@ -32,7 +30,7 @@ class EncryptData:
|
||||||
try:
|
try:
|
||||||
headers = encrypt_func(headers)
|
headers = encrypt_func(headers)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"{headers_crypto} 加密失败:{e}")
|
EncryptionError(headers_crypto, e)
|
||||||
|
|
||||||
if request_data_crypto:
|
if request_data_crypto:
|
||||||
encrypt_func = encryption_methods.get(request_data_crypto)
|
encrypt_func = encryption_methods.get(request_data_crypto)
|
||||||
|
@ -40,6 +38,6 @@ class EncryptData:
|
||||||
try:
|
try:
|
||||||
request_data = encrypt_func(request_data)
|
request_data = encrypt_func(request_data)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"{request_data_crypto} 加密失败:{e}")
|
EncryptionError(request_data_crypto, e)
|
||||||
|
|
||||||
return headers, request_data
|
return headers, request_data
|
||||||
|
|
|
@ -7,6 +7,3 @@
|
||||||
@time: 2023/3/14 14:21
|
@time: 2023/3/14 14:21
|
||||||
@desc:
|
@desc:
|
||||||
"""
|
"""
|
||||||
from common.log_utils.mylogger import MyLogger
|
|
||||||
|
|
||||||
logger = MyLogger()
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -13,7 +13,7 @@ sys.path.append("../")
|
||||||
sys.path.append("./common")
|
sys.path.append("./common")
|
||||||
from jsonpath_ng import parse
|
from jsonpath_ng import parse
|
||||||
from common.utils.environments import Environments
|
from common.utils.environments import Environments
|
||||||
from common.data_extraction import logger
|
from common.utils.exceptions import logger, InvalidParameterFormatError, ParameterExtractionError
|
||||||
|
|
||||||
REPLACE_DICT = {
|
REPLACE_DICT = {
|
||||||
"null": None,
|
"null": None,
|
||||||
|
@ -42,7 +42,7 @@ class DataExtractor(Environments):
|
||||||
|
|
||||||
response = response
|
response = response
|
||||||
if not isinstance(response, (dict, str, list)):
|
if not isinstance(response, (dict, str, list)):
|
||||||
logger.error(f"| 被提取对象非字典、非字符串、非列表,不执行jsonpath提取,被提取对象: {response}")
|
InvalidParameterFormatError(response, "| 被提取对象非字典、非字符串、非列表,不执行jsonpath提取")
|
||||||
return {}
|
return {}
|
||||||
if regex and keys:
|
if regex and keys:
|
||||||
self.substitute_regex(response, regex, keys)
|
self.substitute_regex(response, regex, keys)
|
||||||
|
@ -133,7 +133,7 @@ class DataExtractor(Environments):
|
||||||
self.update_environments(key, result[0]) if len(result) == 1 else self.update_environments(key,
|
self.update_environments(key, result[0]) if len(result) == 1 else self.update_environments(key,
|
||||||
result)
|
result)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"| jsonpath表达式错误'{expression}': {e}")
|
ParameterExtractionError(expression, e)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
|
@ -9,8 +9,11 @@
|
||||||
"""
|
"""
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from common.data_extraction import logger
|
|
||||||
from common.data_extraction.data_extractor import DataExtractor
|
from common.data_extraction.data_extractor import DataExtractor
|
||||||
|
from common.utils.exceptions import ParameterExtractionError, ResponseJsonConversionError
|
||||||
|
|
||||||
|
|
||||||
|
# from common.utils.exceptions import logger
|
||||||
|
|
||||||
|
|
||||||
class DependentParameter(DataExtractor):
|
class DependentParameter(DataExtractor):
|
||||||
|
@ -19,7 +22,6 @@ class DependentParameter(DataExtractor):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
@logger.log_decorator()
|
|
||||||
def replace_dependent_parameter(self, json_string):
|
def replace_dependent_parameter(self, json_string):
|
||||||
"""
|
"""
|
||||||
替换字符串中的关联参数,并返回转化后的字典格式。
|
替换字符串中的关联参数,并返回转化后的字典格式。
|
||||||
|
@ -45,7 +47,9 @@ class DependentParameter(DataExtractor):
|
||||||
args_string = self.ARGS_MATCHER.search(first_method_call_match.group())
|
args_string = self.ARGS_MATCHER.search(first_method_call_match.group())
|
||||||
args_list = args_string.group(1).split(',') if args_string else []
|
args_list = args_string.group(1).split(',') if args_string else []
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"函数写法错误:无法匹配函数调用格式,字符串为:{strings}")
|
raise ParameterExtractionError(key, "在关联参数表中查询不到,请检查关联参数字段提取及填写是否正常")
|
||||||
|
|
||||||
|
# raise ValueError(f"函数写法错误:无法匹配函数调用格式,字符串为:{strings}")
|
||||||
remaining_method_names = self.METHOD_NAME_MATCHER.findall(strings)
|
remaining_method_names = self.METHOD_NAME_MATCHER.findall(strings)
|
||||||
return first_fun, first_method_call, remaining_method_names, args_list
|
return first_fun, first_method_call, remaining_method_names, args_list
|
||||||
|
|
||||||
|
@ -61,7 +65,8 @@ class DependentParameter(DataExtractor):
|
||||||
obj = execute_method_chain(obj, remaining_methods, args=args)
|
obj = execute_method_chain(obj, remaining_methods, args=args)
|
||||||
json_string = json_string.replace(function_pattern, str(obj))
|
json_string = json_string.replace(function_pattern, str(obj))
|
||||||
else:
|
else:
|
||||||
logger.error(f"函数key:{key},在关联参数表中查询不到,请检查关联参数字段提取及填写是否正常\n")
|
ParameterExtractionError(key, "在关联参数表中查询不到,请检查关联参数字段提取及填写是否正常")
|
||||||
|
# logger.error(f"函数key:{key},在关联参数表中查询不到,请检查关联参数字段提取及填写是否正常\n")
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
key = self.PARAMETER_MATCHER.search(json_string)
|
key = self.PARAMETER_MATCHER.search(json_string)
|
||||||
|
@ -75,14 +80,16 @@ class DependentParameter(DataExtractor):
|
||||||
obj = self.get_environments(k)[index] if isinstance(index, int) else self.get_environments(k)
|
obj = self.get_environments(k)[index] if isinstance(index, int) else self.get_environments(k)
|
||||||
json_string = json_string.replace(key.group(), str(obj))
|
json_string = json_string.replace(key.group(), str(obj))
|
||||||
else:
|
else:
|
||||||
logger.error(f"字符串key:{key},字符串在关联参数表中查询不到,请检查关联参数字段提取及填写是否正常\n")
|
ParameterExtractionError(key, "在关联参数表中查询不到,请检查关联参数字段提取及填写是否正常")
|
||||||
|
# logger.error(f"字符串key:{key},字符串在关联参数表中查询不到,请检查关联参数字段提取及填写是否正常\n")
|
||||||
break
|
break
|
||||||
json_string = json_string.replace("True", "true").replace("False", "false")
|
json_string = json_string.replace("True", "true").replace("False", "false")
|
||||||
if self.BRACE_MATCHER.search(json_string) and not self.FUNCTION_CHAIN_MATCHER.search(json_string):
|
if self.BRACE_MATCHER.search(json_string) and not self.FUNCTION_CHAIN_MATCHER.search(json_string):
|
||||||
try:
|
try:
|
||||||
json_string = json.loads(json_string)
|
json_string = json.loads(json_string)
|
||||||
except json.JSONDecodeError as e:
|
except json.JSONDecodeError as e:
|
||||||
logger.error(f"JSONDecodeError:{json_string}:{e}")
|
ResponseJsonConversionError(json_string,e)
|
||||||
|
# logger.error(f"JSONDecodeError:{json_string}:{e}")
|
||||||
return json_string
|
return json_string
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,3 @@
|
||||||
@time: 2023/3/13 14:48
|
@time: 2023/3/13 14:48
|
||||||
@desc:
|
@desc:
|
||||||
"""
|
"""
|
||||||
from common.log_utils.mylogger import MyLogger
|
|
||||||
|
|
||||||
logger = MyLogger()
|
|
Binary file not shown.
Binary file not shown.
|
@ -14,10 +14,11 @@ import pymysql
|
||||||
from dbutils.pooled_db import PooledDB
|
from dbutils.pooled_db import PooledDB
|
||||||
from pymysql.cursors import DictCursor
|
from pymysql.cursors import DictCursor
|
||||||
|
|
||||||
from common.database import logger
|
from common.utils.decorators import singleton
|
||||||
|
from common.utils.exceptions import DatabaseExceptionError, InvalidParameterFormatError
|
||||||
|
|
||||||
|
|
||||||
# @singleton
|
@singleton
|
||||||
class MysqlClient:
|
class MysqlClient:
|
||||||
def __init__(self, db_config):
|
def __init__(self, db_config):
|
||||||
"""
|
"""
|
||||||
|
@ -30,15 +31,13 @@ class MysqlClient:
|
||||||
self.result = {}
|
self.result = {}
|
||||||
try:
|
try:
|
||||||
self.db_base = db_config if isinstance(db_config, dict) else json.loads(db_config)
|
self.db_base = db_config if isinstance(db_config, dict) else json.loads(db_config)
|
||||||
print(self.db_base)
|
|
||||||
self.pool = PooledDB(creator=pymysql, maxconnections=10, **self.db_base)
|
self.pool = PooledDB(creator=pymysql, maxconnections=10, **self.db_base)
|
||||||
self.conn = self.pool.connection()
|
self.conn = self.pool.connection()
|
||||||
self.cursor = self.conn.cursor(DictCursor)
|
self.cursor = self.conn.cursor(DictCursor)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"| 数据库链接失败: {e}")
|
DatabaseExceptionError(self.db_base, e)
|
||||||
# raise
|
# raise
|
||||||
|
|
||||||
# @logger.log_decorator()
|
|
||||||
def execute_sql(self, sql):
|
def execute_sql(self, sql):
|
||||||
"""
|
"""
|
||||||
执行 SQL 语句
|
执行 SQL 语句
|
||||||
|
@ -72,18 +71,15 @@ class MysqlClient:
|
||||||
for method, sql_data in sql.items():
|
for method, sql_data in sql.items():
|
||||||
execute_method = getattr(self, f"_execute_{method}", None)
|
execute_method = getattr(self, f"_execute_{method}", None)
|
||||||
if not execute_method:
|
if not execute_method:
|
||||||
logger.error("| sql字典集编写格式不符合规范")
|
InvalidParameterFormatError(sql, "sql字典集编写格式不符合规范")
|
||||||
raise ValueError("| Invalid SQL method")
|
raise ValueError("| Invalid SQL method")
|
||||||
logger.info(f"| 执行 sql 语句集: {sql_data}")
|
|
||||||
execute_method(sql_data)
|
execute_method(sql_data)
|
||||||
|
|
||||||
self.cursor.close()
|
self.cursor.close()
|
||||||
self.conn.close()
|
self.conn.close()
|
||||||
|
|
||||||
return self.result
|
return self.result
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"| 数据库操作异常: {e}")
|
DatabaseExceptionError(sql, e)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def _execute_write(self, sql_data):
|
def _execute_write(self, sql_data):
|
||||||
|
@ -94,7 +90,7 @@ class MysqlClient:
|
||||||
try:
|
try:
|
||||||
self.cursor.execute(str(sql_))
|
self.cursor.execute(str(sql_))
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
logger.error(f"| 执行 SQL 异常: {sql_}")
|
DatabaseExceptionError(sql_, err)
|
||||||
raise err
|
raise err
|
||||||
self.cursor.connection.commit()
|
self.cursor.connection.commit()
|
||||||
|
|
||||||
|
@ -117,8 +113,9 @@ class MysqlClient:
|
||||||
try:
|
try:
|
||||||
self.cursor.execute(sql_)
|
self.cursor.execute(sql_)
|
||||||
self.result[sql_name] = self.cursor.fetchall()
|
self.result[sql_name] = self.cursor.fetchall()
|
||||||
|
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
logger.error(f"| 查询异常 sql: {sql_}")
|
DatabaseExceptionError(sql_, err)
|
||||||
raise err
|
raise err
|
||||||
|
|
||||||
|
|
||||||
|
|
Binary file not shown.
|
@ -7,6 +7,8 @@ import sys
|
||||||
import requests
|
import requests
|
||||||
import urllib3
|
import urllib3
|
||||||
|
|
||||||
|
from common.utils.exceptions import ResponseJsonConversionError
|
||||||
|
|
||||||
sys.path.append("../")
|
sys.path.append("../")
|
||||||
sys.path.append("./common")
|
sys.path.append("./common")
|
||||||
|
|
||||||
|
@ -15,7 +17,7 @@ from common.file_handling.file_utils import FileUtils
|
||||||
from common.utils.decorators import request_retry_on_exception
|
from common.utils.decorators import request_retry_on_exception
|
||||||
|
|
||||||
|
|
||||||
class Pyt(LoadModulesFromFolder):
|
class HttpClient(LoadModulesFromFolder):
|
||||||
session = requests.Session()
|
session = requests.Session()
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
@ -76,6 +78,7 @@ class Pyt(LoadModulesFromFolder):
|
||||||
try:
|
try:
|
||||||
self.response_json = self.response.json()
|
self.response_json = self.response.json()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
ResponseJsonConversionError(self.response.text, str(e))
|
||||||
self.response_json = None
|
self.response_json = None
|
||||||
return self.response
|
return self.response
|
||||||
|
|
||||||
|
@ -89,5 +92,5 @@ if __name__ == '__main__':
|
||||||
'data': {},
|
'data': {},
|
||||||
'files': ['test.txt']
|
'files': ['test.txt']
|
||||||
}
|
}
|
||||||
pyt = Pyt()
|
pyt = HttpClient()
|
||||||
pyt.http_client(hst, url, method, **kwargs)
|
pyt.http_client(hst, url, method, **kwargs)
|
||||||
|
|
Binary file not shown.
|
@ -11,13 +11,11 @@ from time import perf_counter
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from common.utils.decorators import singleton
|
|
||||||
from config import Config
|
from config import Config
|
||||||
|
|
||||||
LOG_DIR = Config.log_path
|
LOG_DIR = Config.log_path
|
||||||
|
|
||||||
|
|
||||||
@singleton
|
|
||||||
class MyLogger:
|
class MyLogger:
|
||||||
"""
|
"""
|
||||||
根据时间、文件大小切割日志
|
根据时间、文件大小切割日志
|
||||||
|
|
|
@ -0,0 +1,75 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# encoding: utf-8
|
||||||
|
"""
|
||||||
|
@author: kira
|
||||||
|
@contact: 262667641@qq.com
|
||||||
|
@file: dingding.py
|
||||||
|
@time: 2023/8/8 10:59
|
||||||
|
@desc:
|
||||||
|
"""
|
||||||
|
import base64
|
||||||
|
import hashlib
|
||||||
|
import hmac
|
||||||
|
import time
|
||||||
|
import urllib.parse
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from config import Config
|
||||||
|
|
||||||
|
|
||||||
|
class DingTalk:
|
||||||
|
"""顶顶通知"""
|
||||||
|
|
||||||
|
def __init__(self, title, notice_content, except_info):
|
||||||
|
""""""
|
||||||
|
self.url = Config.dingtalk_notice.get("url")
|
||||||
|
self.notice_content = notice_content
|
||||||
|
self.title = title
|
||||||
|
self.except_info = except_info
|
||||||
|
self.secret = Config.dingtalk_notice.get("secret")
|
||||||
|
|
||||||
|
def sign(self):
|
||||||
|
"""加签"""
|
||||||
|
timestamp = str(round(time.time() * 1000))
|
||||||
|
secret_enc = self.secret.encode('utf-8')
|
||||||
|
string_to_sign = '{}\n{}'.format(timestamp, self.secret)
|
||||||
|
string_to_sign_enc = string_to_sign.encode('utf-8')
|
||||||
|
hmac_code = hmac.new(secret_enc, string_to_sign_enc, digestmod=hashlib.sha256).digest()
|
||||||
|
sign = urllib.parse.quote_plus(base64.b64encode(hmac_code))
|
||||||
|
return {"sign": sign, "timestamp": timestamp}
|
||||||
|
|
||||||
|
def content(self):
|
||||||
|
"""markdown 内容"""
|
||||||
|
if Config.dingtalk_notice.get("except_info"):
|
||||||
|
self.notice_content += '\n ### 未通过用例详情:\n'
|
||||||
|
self.notice_content += self.except_info
|
||||||
|
data = {
|
||||||
|
"msgtype": "markdown",
|
||||||
|
"markdown": {
|
||||||
|
"title": '{}({})'.format(self.title, Config.dingtalk_notice.get("key")),
|
||||||
|
"text": self.notice_content
|
||||||
|
},
|
||||||
|
"at": {
|
||||||
|
"atMobiles": Config.dingtalk_notice.get("atMobiles"),
|
||||||
|
"isAtAll": Config.dingtalk_notice.get("isatall")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
|
||||||
|
def send_info(self):
|
||||||
|
"""发送钉钉消息"""
|
||||||
|
notice_content = self.content()
|
||||||
|
if self.secret:
|
||||||
|
sign = self.sign()
|
||||||
|
else:
|
||||||
|
sign = None
|
||||||
|
try:
|
||||||
|
requests.post(url=self.url, json=notice_content, params=sign)
|
||||||
|
except Exception as e:
|
||||||
|
print("发送钉钉异常", e)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
texts = "#### 杭州天气 @150XXXXXXXX \n > 9度,西北风1级,空气良89,相对温度73%\n > ![screenshot](https://img.alicdn.com/tfs/TB1NwmBEL9TBuNjy1zbXXXpepXa-2400-1218.png)\n > ###### 10点20分发布 [天气](https://www.dingtalk.com) \n"
|
||||||
|
print(DingTalk("杭州天气", texts).send_info())
|
|
@ -0,0 +1,74 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# encoding: utf-8
|
||||||
|
"""
|
||||||
|
@author: kira
|
||||||
|
@contact: 262667641@qq.com
|
||||||
|
@file: email.py
|
||||||
|
@time: 2023/8/8 10:58
|
||||||
|
@desc:
|
||||||
|
"""
|
||||||
|
import smtplib
|
||||||
|
from email.header import Header # 将各类信息定义成对象,比如标题等。
|
||||||
|
from email.mime.application import MIMEApplication
|
||||||
|
from email.mime.multipart import MIMEMultipart # 定义带有附件的邮件对象
|
||||||
|
from email.mime.text import MIMEText
|
||||||
|
|
||||||
|
from config import Config
|
||||||
|
|
||||||
|
|
||||||
|
class SendEmail:
|
||||||
|
"""Send mail"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""
|
||||||
|
:param host: smtp server address
|
||||||
|
:param port: smtp server report
|
||||||
|
:param user: Email account number
|
||||||
|
:param password: SMTP service authorization code of mailbox
|
||||||
|
"""
|
||||||
|
# 邮箱服务器地址
|
||||||
|
self.host = Config.mail_data.get("host")
|
||||||
|
# 用户名
|
||||||
|
self.user = Config.mail_data.get("user")
|
||||||
|
# 密码(部分邮箱为授权码)
|
||||||
|
self.password = Config.mail_data.get("password")
|
||||||
|
# 邮件发送方邮箱地址
|
||||||
|
self.sender = Config.mail_data.get("sender")
|
||||||
|
# 25 为 SMTP 端口号
|
||||||
|
self.port = Config.mail_data.get("port")
|
||||||
|
# 定义接收放的邮箱(可以是多个)
|
||||||
|
self.receivers = Config.mail_data.get("receivers")
|
||||||
|
|
||||||
|
def content(self, content=None, file_path=None):
|
||||||
|
"""内容"""
|
||||||
|
msg = MIMEMultipart() # 如果一份邮件含有附件,则必须定义 multipart/mixed 类型
|
||||||
|
msg['Form'] = Header(self.sender)
|
||||||
|
msg['Subject'] = Header('接口自动化测试报告', 'utf-8') # 主题
|
||||||
|
msg['To'] = ','.join(self.receivers)
|
||||||
|
|
||||||
|
# 编辑邮件
|
||||||
|
# ---邮件正文内容---
|
||||||
|
message = MIMEText(content, 'html', 'utf-8')
|
||||||
|
msg.attach(message)
|
||||||
|
# 测试用例与测试报告一起
|
||||||
|
if file_path:
|
||||||
|
for ph in file_path:
|
||||||
|
filename = ph.split('\\')[-1]
|
||||||
|
with open(ph, 'rb') as f:
|
||||||
|
ctx = f.read()
|
||||||
|
part = MIMEApplication(ctx)
|
||||||
|
part.add_header('Content-Disposition', 'attachment', filename=filename)
|
||||||
|
msg.attach(part) # 附件模块添加到 MIMEMultipart
|
||||||
|
|
||||||
|
return msg.as_string()
|
||||||
|
|
||||||
|
def send_mail(self, content, file_path=None):
|
||||||
|
"""发送邮件"""
|
||||||
|
try:
|
||||||
|
s = smtplib.SMTP_SSL(self.host, self.port)
|
||||||
|
# s.starttls()
|
||||||
|
s.login(self.user, self.password)
|
||||||
|
s.sendmail(self.sender, self.receivers, self.content(content, file_path))
|
||||||
|
s.quit()
|
||||||
|
except Exception as e:
|
||||||
|
print("发送邮件异常", e)
|
|
@ -0,0 +1,109 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# encoding: utf-8
|
||||||
|
"""
|
||||||
|
@author: kira
|
||||||
|
@contact: 262667641@qq.com
|
||||||
|
@file: weChat.py
|
||||||
|
@time: 2023/8/8 10:59
|
||||||
|
@desc:
|
||||||
|
"""
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import urllib3
|
||||||
|
|
||||||
|
urllib3.disable_warnings()
|
||||||
|
from config import Config
|
||||||
|
|
||||||
|
|
||||||
|
class WeChat:
|
||||||
|
headers = {"Content-Type": "application/json"}
|
||||||
|
|
||||||
|
def __init__(self, notice_content):
|
||||||
|
self.send_url = Config.weixin_notice.get('send_url')
|
||||||
|
self.up_url = Config.weixin_notice.get("upload_url")
|
||||||
|
self.notice_content = notice_content
|
||||||
|
self.file_lists = Config.weixin_notice.get("file_lists")
|
||||||
|
|
||||||
|
def send_markdown(self):
|
||||||
|
"""
|
||||||
|
发送markdown 请求
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
notice_content = self.notice_content
|
||||||
|
send_markdown_data = {
|
||||||
|
"msgtype": "markdown", # 消息类型,此时固定为markdown
|
||||||
|
"markdown": {
|
||||||
|
"content": notice_content
|
||||||
|
}}
|
||||||
|
# "markdown": {
|
||||||
|
# "content": f"# **提醒!自动化测试反馈**\n#### **请相关同事注意,及时跟进!**\n"
|
||||||
|
# f"> 项目名称:<font color=\"info\">{project_name}</font> \n"
|
||||||
|
# f"> 项目指定端:<font color=\"info\">{project_port}</font> \n"
|
||||||
|
# f"> 测试用例总数:<font color=\"info\">{total_cases}条</font>;测试用例通过率:<font color=\"info\">{pass_rate}</font>\n"
|
||||||
|
# "> **--------------------运行详情--------------------**\n"
|
||||||
|
# f"> **成功数:**<font color=\"info\">{success_cases}</font>\n**失败数:**<font color=\"warning\">{fail_cases}</font>\n "
|
||||||
|
# f"> **跳过数:**<font color=\"info\">{skip_cases}</font>\n**错误数:**<font color=\"comment\">{error_cases}</font>\n"
|
||||||
|
# f"> ##### **报告链接:** [jenkins报告,请点击后进入查看]{report_url}"
|
||||||
|
# # 加粗:**需要加粗的字**
|
||||||
|
# 引用:> 需要引用的文字
|
||||||
|
# 字体颜色(只支持3种内置颜色)
|
||||||
|
# 标题 (支持1至6级标题,注意#与文字中间要有空格)
|
||||||
|
# 绿色:info、灰色:comment、橙红:warning
|
||||||
|
# }
|
||||||
|
# }
|
||||||
|
requests.post(url=self.send_url, headers=self.headers, json=send_markdown_data, verify=False).json()
|
||||||
|
|
||||||
|
def send_file(self, file_path):
|
||||||
|
"""
|
||||||
|
文件路径
|
||||||
|
Args:
|
||||||
|
file_path:
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
media_id = self.upload_media(file_path)
|
||||||
|
for i in media_id:
|
||||||
|
send_data = {"msgtype": "file", "file": {"media_id": i}}
|
||||||
|
requests.post(self.send_url, headers=self.headers, json=send_data, verify=False)
|
||||||
|
import time
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
def upload_media(self, file_lists) -> list:
|
||||||
|
"""
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_lists: 文件路径
|
||||||
|
|
||||||
|
Returns:上传文件后返回的每一个文件id
|
||||||
|
|
||||||
|
"""
|
||||||
|
# print(f"file path: {file_path}")
|
||||||
|
media_ids = []
|
||||||
|
if file_lists:
|
||||||
|
for fp in file_lists:
|
||||||
|
with open(fp, "rb") as f:
|
||||||
|
send_data = {"media": f}
|
||||||
|
res_html = requests.post(self.up_url, files=send_data, verify=False).json()
|
||||||
|
media_id = res_html.get("media_id")
|
||||||
|
media_ids.append(media_id)
|
||||||
|
return media_ids
|
||||||
|
|
||||||
|
def send_main(self):
|
||||||
|
"""
|
||||||
|
发送markdown及上传文件
|
||||||
|
Args:
|
||||||
|
dirs:文件夹路径或文件路径
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self.send_markdown()
|
||||||
|
except Exception as e:
|
||||||
|
print("发送markdown信息异常", e)
|
||||||
|
try:
|
||||||
|
self.send_file(self.file_lists)
|
||||||
|
except Exception as e:
|
||||||
|
print("发送企业微信附件异常", e)
|
|
@ -1,109 +0,0 @@
|
||||||
# -*- coding:utf-8 -*-
|
|
||||||
"""
|
|
||||||
Time: 2020/6/17/017 10:52
|
|
||||||
Author: 陈勇志
|
|
||||||
Email:262667641@qq.com
|
|
||||||
Project:api_project
|
|
||||||
"""
|
|
||||||
import time
|
|
||||||
|
|
||||||
import requests
|
|
||||||
import urllib3
|
|
||||||
|
|
||||||
from common.file_handling.file_utils import FileUtils
|
|
||||||
|
|
||||||
urllib3.disable_warnings()
|
|
||||||
|
|
||||||
|
|
||||||
class WxWorkSms:
|
|
||||||
# header = {"Content-Type": "multipart/form-data"}
|
|
||||||
headers = {"Content-Type": "application/json"}
|
|
||||||
|
|
||||||
def __init__(self, key):
|
|
||||||
self.send_url = f"https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key={key}"
|
|
||||||
self.up_url = f"https://qyapi.weixin.qq.com/cgi-bin/webhook/upload_media?key={key}&type=file"
|
|
||||||
|
|
||||||
def send_markdown(self, project_name, project_port, total_cases, pass_rate, success_cases, fail_cases, skip_cases,
|
|
||||||
error_cases, report_url):
|
|
||||||
"""
|
|
||||||
发送markdown 请求
|
|
||||||
Returns:
|
|
||||||
|
|
||||||
"""
|
|
||||||
send_markdown_data = {
|
|
||||||
"msgtype": "markdown", # 消息类型,此时固定为markdown
|
|
||||||
"markdown": {
|
|
||||||
"content": f"# **提醒!自动化测试反馈**\n#### **请相关同事注意,及时跟进!**\n"
|
|
||||||
f"> 项目名称:<font color=\"info\">{project_name}</font> \n"
|
|
||||||
f"> 项目指定端:<font color=\"info\">{project_port}</font> \n"
|
|
||||||
f"> 测试用例总数:<font color=\"info\">{total_cases}条</font>;测试用例通过率:<font color=\"info\">{pass_rate}</font>\n"
|
|
||||||
"> **--------------------运行详情--------------------**\n"
|
|
||||||
f"> **成功数:**<font color=\"info\">{success_cases}</font>\n**失败数:**<font color=\"warning\">{fail_cases}</font>\n "
|
|
||||||
f"> **跳过数:**<font color=\"info\">{skip_cases}</font>\n**错误数:**<font color=\"comment\">{error_cases}</font>\n"
|
|
||||||
f"> ##### **报告链接:** [jenkins报告,请点击后进入查看]{report_url}"
|
|
||||||
# 加粗:**需要加粗的字**
|
|
||||||
# 引用:> 需要引用的文字
|
|
||||||
# 字体颜色(只支持3种内置颜色)
|
|
||||||
# 标题 (支持1至6级标题,注意#与文字中间要有空格)
|
|
||||||
# 绿色:info、灰色:comment、橙红:warning
|
|
||||||
}
|
|
||||||
}
|
|
||||||
requests.post(url=self.send_url, headers=self.headers, json=send_markdown_data, verify=False).json()
|
|
||||||
|
|
||||||
def send_file(self, file_path):
|
|
||||||
"""
|
|
||||||
文件路径
|
|
||||||
Args:
|
|
||||||
file_path:
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
|
|
||||||
"""
|
|
||||||
media_id = self.upload_media(file_path)
|
|
||||||
for i in media_id:
|
|
||||||
send_data = {"msgtype": "file", "file": {"media_id": i}}
|
|
||||||
requests.post(self.send_url, headers=self.headers, json=send_data, verify=False)
|
|
||||||
time.sleep(2)
|
|
||||||
|
|
||||||
def upload_media(self, file_path) -> list:
|
|
||||||
"""
|
|
||||||
|
|
||||||
Args:
|
|
||||||
file_path: 文件路径
|
|
||||||
|
|
||||||
Returns:上传文件后返回的每一个文件id
|
|
||||||
|
|
||||||
"""
|
|
||||||
# print(f"file path: {file_path}")
|
|
||||||
media_ids = []
|
|
||||||
for fp in file_path:
|
|
||||||
with open(fp, "rb") as f:
|
|
||||||
send_data = {"media": f}
|
|
||||||
res_html = requests.post(self.up_url, files=send_data, verify=False).json()
|
|
||||||
media_id = res_html.get("media_id")
|
|
||||||
media_ids.append(media_id)
|
|
||||||
return media_ids
|
|
||||||
|
|
||||||
def send_main(self, folder_path, project_name, project_port, total_cases, pass_rate, success_cases, fail_cases,
|
|
||||||
skip_cases,
|
|
||||||
error_cases, report_url):
|
|
||||||
"""
|
|
||||||
发送markdown及上传文件
|
|
||||||
Args:
|
|
||||||
dirs:文件夹路径
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
|
|
||||||
"""
|
|
||||||
self.send_markdown(
|
|
||||||
project_name, project_port, total_cases, pass_rate, success_cases, fail_cases, skip_cases,
|
|
||||||
error_cases, report_url
|
|
||||||
)
|
|
||||||
file_path = FileUtils.get_all_path(folder_path)
|
|
||||||
self.send_file(file_path)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
dirs = r'D:\apk_api\api-test-project\OutPut\Reports'
|
|
||||||
WxWorkSms('8b1647d4-dc32-447c-b524-548acf18a938').send_main(dirs, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
|
||||||
# WxWorkSms('8b1647d4-dc32-447c-b524-548acf18a938').send_markdown(1, 2, 3, 4, 5, 6, 7, 8, 9)
|
|
Binary file not shown.
Binary file not shown.
|
@ -13,7 +13,6 @@ import time
|
||||||
from common import bif_functions
|
from common import bif_functions
|
||||||
from common.crypto.encrypt_data import EncryptData
|
from common.crypto.encrypt_data import EncryptData
|
||||||
from common.database.mysql_client import MysqlClient
|
from common.database.mysql_client import MysqlClient
|
||||||
from common.log_utils.mylogger import MyLogger
|
|
||||||
from common.utils.decorators import singleton
|
from common.utils.decorators import singleton
|
||||||
from common.utils.exceptions import *
|
from common.utils.exceptions import *
|
||||||
from common.validation.extractor import Extractor
|
from common.validation.extractor import Extractor
|
||||||
|
@ -22,15 +21,14 @@ from common.validation.validator import Validator
|
||||||
|
|
||||||
|
|
||||||
@singleton
|
@singleton
|
||||||
class Action(Extractor, LoadScript, Validator, MysqlClient):
|
class Action(Extractor, LoadScript, Validator):
|
||||||
def __init__(self, initialize_data=None, db_config=None):
|
def __init__(self, initialize_data=None, db_config=None):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
MysqlClient.__init__(self, db_config)
|
self.db_config = db_config
|
||||||
self.encrypt = EncryptData()
|
self.encrypt = EncryptData()
|
||||||
self.__variables = {}
|
self.__variables = {}
|
||||||
self.set_environments(initialize_data)
|
self.set_environments(initialize_data)
|
||||||
self.set_bif_fun(bif_functions)
|
self.set_bif_fun(bif_functions)
|
||||||
self.logger = MyLogger()
|
|
||||||
|
|
||||||
def execute_dynamic_code(self, item, code):
|
def execute_dynamic_code(self, item, code):
|
||||||
self.variables = item
|
self.variables = item
|
||||||
|
@ -72,12 +70,13 @@ class Action(Extractor, LoadScript, Validator, MysqlClient):
|
||||||
try:
|
try:
|
||||||
self.substitute_data(self.response_json, regex=regex, keys=keys, deps=deps, jp_dict=jp_dict)
|
self.substitute_data(self.response_json, regex=regex, keys=keys, deps=deps, jp_dict=jp_dict)
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
self.logger.error(f"| 分析响应失败:{sheet}_{iid}_{name}_{desc}"
|
msg = f"| 分析响应失败:{sheet}_{iid}_{name}_{desc}"
|
||||||
f"\nregex={regex};"
|
f"\nregex={regex};"
|
||||||
f" \nkeys={keys};"
|
f" \nkeys={keys};"
|
||||||
f"\ndeps={deps};"
|
f"\ndeps={deps};"
|
||||||
f"\njp_dict={jp_dict}"
|
f"\njp_dict={jp_dict}"
|
||||||
f"\n{err}")
|
f"\n{err}"
|
||||||
|
ParameterExtractionError(msg, err)
|
||||||
|
|
||||||
def execute_validation(self, excel, sheet, iid, name, desc, expected):
|
def execute_validation(self, excel, sheet, iid, name, desc, expected):
|
||||||
try:
|
try:
|
||||||
|
@ -85,13 +84,14 @@ class Action(Extractor, LoadScript, Validator, MysqlClient):
|
||||||
result = "PASS"
|
result = "PASS"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
result = "FAIL"
|
result = "FAIL"
|
||||||
self.logger.error(f"| exception case:**{sheet}_{iid}_{name}_{desc}**\n{e}")
|
error_info = f"| exception case:**{sheet}_{iid}_{name}_{desc},{self.assertions}"
|
||||||
raise AssertionFailedError(self.assertions)
|
AssertionFailedError(error_info, e)
|
||||||
|
raise e
|
||||||
finally:
|
finally:
|
||||||
print(f'| Assertion Result-->{self.assertions}')
|
print(f'| 断言结果-->{self.assertions}\n')
|
||||||
response = self.response.text if self.response is not None else str(self.response)
|
response = self.response.text if self.response is not None else str(self.response)
|
||||||
# excel.write_back(sheet_name=sheet, i=iid, response=response, test_result=result,
|
excel.write_back(sheet_name=sheet, i=iid, response=response, test_result=result,
|
||||||
# assert_log=str(self.assertions))
|
assert_log=str(self.assertions))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def base_info(item):
|
def base_info(item):
|
||||||
|
@ -158,27 +158,23 @@ class Action(Extractor, LoadScript, Validator, MysqlClient):
|
||||||
if not is_run or is_run.upper() != "YES":
|
if not is_run or is_run.upper() != "YES":
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def pause_execution(self, sleep_time):
|
@staticmethod
|
||||||
|
def pause_execution(sleep_time):
|
||||||
if sleep_time:
|
if sleep_time:
|
||||||
try:
|
try:
|
||||||
time.sleep(sleep_time)
|
time.sleep(sleep_time)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise MyBaseException(f"暂停时间必须是数字!")
|
raise InvalidSleepTimeError(f"{sleep_time}", e)
|
||||||
|
|
||||||
def exc_sql(self, item):
|
def exc_sql(self, item):
|
||||||
sql, sql_params_dict = self.sql_info(item)
|
sql, sql_params_dict = self.sql_info(item)
|
||||||
sql = self.replace_dependent_parameter(sql)
|
sql = self.replace_dependent_parameter(sql)
|
||||||
if sql:
|
if sql:
|
||||||
try:
|
client = MysqlClient(self.db_config)
|
||||||
execute_sql_results = self.execute_sql(sql)
|
execute_sql_results = client.execute_sql(sql)
|
||||||
except DatabaseExceptionError as e:
|
print(f"| 执行 sql 成功--> {execute_sql_results}")
|
||||||
raise DatabaseExceptionError(sql, str(e))
|
|
||||||
else:
|
|
||||||
if execute_sql_results and sql_params_dict:
|
if execute_sql_results and sql_params_dict:
|
||||||
try:
|
|
||||||
self.substitute_data(execute_sql_results, jp_dict=sql_params_dict)
|
self.substitute_data(execute_sql_results, jp_dict=sql_params_dict)
|
||||||
except Exception as e:
|
|
||||||
ParameterExtractionError(sql_params_dict, str(e))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
|
@ -7,9 +7,13 @@
|
||||||
@time: 2023/3/21 17:41
|
@time: 2023/3/21 17:41
|
||||||
@desc:
|
@desc:
|
||||||
"""
|
"""
|
||||||
import time
|
import json
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
from common.utils.exceptions import RequestSendingError
|
||||||
|
|
||||||
|
|
||||||
def singleton(cls):
|
def singleton(cls):
|
||||||
"""
|
"""
|
||||||
|
@ -29,6 +33,8 @@ def singleton(cls):
|
||||||
|
|
||||||
|
|
||||||
def request_retry_on_exception(retries=2, delay=1.5):
|
def request_retry_on_exception(retries=2, delay=1.5):
|
||||||
|
"""失败请求重发"""
|
||||||
|
|
||||||
def request_decorator(func):
|
def request_decorator(func):
|
||||||
e = None
|
e = None
|
||||||
|
|
||||||
|
@ -51,8 +57,97 @@ def request_retry_on_exception(retries=2, delay=1.5):
|
||||||
time.sleep(delay)
|
time.sleep(delay)
|
||||||
else:
|
else:
|
||||||
return response
|
return response
|
||||||
raise Exception(f"| 请求重试**{retries}**次失败,请检查!!{e}")
|
raise RequestSendingError(kwargs, e)
|
||||||
|
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
return request_decorator
|
return request_decorator
|
||||||
|
|
||||||
|
|
||||||
|
def list_data(datas):
|
||||||
|
"""
|
||||||
|
:param datas: 测试数据
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
|
||||||
|
def wrapper(func):
|
||||||
|
setattr(func, "PARAMS", datas)
|
||||||
|
return func
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
def yaml_data(file_path):
|
||||||
|
"""
|
||||||
|
:param file_path: yaml文件路径
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
|
||||||
|
def wrapper(func):
|
||||||
|
try:
|
||||||
|
with open(file_path, "r", encoding="utf-8") as f:
|
||||||
|
datas = yaml.load(f, Loader=yaml.FullLoader)
|
||||||
|
except:
|
||||||
|
with open(file_path, "r", encoding="gbk") as f:
|
||||||
|
datas = yaml.load(f, Loader=yaml.FullLoader)
|
||||||
|
setattr(func, "PARAMS", datas)
|
||||||
|
return func
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
def json_data(file_path):
|
||||||
|
"""
|
||||||
|
:param file_path: json文件路径
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
|
||||||
|
def wrapper(func):
|
||||||
|
try:
|
||||||
|
with open(file_path, "r", encoding="utf-8") as f:
|
||||||
|
datas = json.load(f)
|
||||||
|
except:
|
||||||
|
with open(file_path, "r", encoding="gbk") as f:
|
||||||
|
datas = json.load(f)
|
||||||
|
setattr(func, "PARAMS", datas)
|
||||||
|
return func
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
import time
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
|
||||||
|
def run_count(count, interval, func, *args, **kwargs):
|
||||||
|
"""运行计数"""
|
||||||
|
for i in range(count):
|
||||||
|
try:
|
||||||
|
func(*args, **kwargs)
|
||||||
|
except Exception as e:
|
||||||
|
print("====用例执行失败===")
|
||||||
|
traceback.print_exc()
|
||||||
|
if i + 1 == count:
|
||||||
|
raise e
|
||||||
|
else:
|
||||||
|
print("==============开始第{}次重运行=============".format(i))
|
||||||
|
time.sleep(interval)
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
def rerun(count, interval=2):
|
||||||
|
"""
|
||||||
|
单个测试用例重运行的装饰器,注意点,如果使用了ddt,那么该方法要在用在ddt之前
|
||||||
|
:param count: 失败重运行次数
|
||||||
|
:param interval: 每次重运行间隔时间,默认三秒钟
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
|
||||||
|
def wrapper(func):
|
||||||
|
def decorator(*args, **kwargs):
|
||||||
|
run_count(count, interval, func, *args, **kwargs)
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
|
@ -7,11 +7,15 @@
|
||||||
@time: 2023/8/1 9:12
|
@time: 2023/8/1 9:12
|
||||||
@desc:
|
@desc:
|
||||||
"""
|
"""
|
||||||
|
from common.log_utils.mylogger import MyLogger
|
||||||
|
|
||||||
|
logger = MyLogger()
|
||||||
|
|
||||||
|
|
||||||
class MyBaseException(Exception):
|
class MyBaseException(Exception):
|
||||||
def __init__(self, msg):
|
def __init__(self, msg):
|
||||||
self.msg = msg
|
self.msg = msg
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.msg
|
return self.msg
|
||||||
|
@ -21,53 +25,118 @@ class RequestSendingError(MyBaseException):
|
||||||
"""请求异常"""
|
"""请求异常"""
|
||||||
ERROR_CODE = 1001
|
ERROR_CODE = 1001
|
||||||
|
|
||||||
def __init__(self, url, reason):
|
def __init__(self, request_info, reason):
|
||||||
msg = f"请求异常:URL={url}, 原因={reason}"
|
msg = f"请求异常:request_info={request_info}, 原因={reason}"
|
||||||
super().__init__(msg)
|
super().__init__(msg)
|
||||||
|
self.logger.error(msg)
|
||||||
|
|
||||||
|
|
||||||
class DatabaseExceptionError(MyBaseException):
|
class DatabaseExceptionError(MyBaseException):
|
||||||
"""数据库异常"""
|
"""数据库异常"""
|
||||||
ERROR_CODE = 1002
|
ERROR_CODE = 1002
|
||||||
|
|
||||||
def __init__(self, operation, reason):
|
def __init__(self, operation_info, reason):
|
||||||
msg = f"数据库异常:操作={operation}, 原因={reason}"
|
msg = f"数据库异常:操作信息={operation_info}, 原因={reason}"
|
||||||
super().__init__(msg)
|
super().__init__(msg)
|
||||||
|
self.logger.error(msg)
|
||||||
|
|
||||||
|
|
||||||
class ParameterExtractionError(MyBaseException):
|
class ParameterExtractionError(MyBaseException):
|
||||||
"""参数提取异常"""
|
"""参数提取异常"""
|
||||||
ERROR_CODE = 1003
|
ERROR_CODE = 1003
|
||||||
|
|
||||||
def __init__(self, parameter_path, reason):
|
def __init__(self, parameter_info, reason):
|
||||||
msg = f"参数提取异常:参数路径={parameter_path}, 原因={reason}"
|
msg = f"参数提取异常:参数信息={parameter_info}, 原因={reason}"
|
||||||
super().__init__(msg)
|
super().__init__(msg)
|
||||||
|
self.logger.error(msg)
|
||||||
|
|
||||||
|
|
||||||
class ParameterReplacementError(MyBaseException):
|
class ParameterReplacementError(MyBaseException):
|
||||||
"""参数替换异常"""
|
"""参数替换异常"""
|
||||||
ERROR_CODE = 1004
|
ERROR_CODE = 1004
|
||||||
|
|
||||||
def __init__(self, parameter_name, reason):
|
def __init__(self, parameter_info, reason):
|
||||||
msg = f"参数替换异常:参数名称={parameter_name}, 原因={reason}"
|
msg = f"参数替换异常:参数名称={parameter_info}, 原因={reason}"
|
||||||
super().__init__(msg)
|
super().__init__(msg)
|
||||||
|
self.logger.error(msg)
|
||||||
|
|
||||||
|
|
||||||
class AssertionFailedError(MyBaseException):
|
class AssertionFailedError(MyBaseException):
|
||||||
"""断言异常"""
|
"""断言异常"""
|
||||||
ERROR_CODE = 1005
|
ERROR_CODE = 1005
|
||||||
|
|
||||||
def __init__(self, assertion):
|
def __init__(self, assertion, reason):
|
||||||
msg = f"断言失败:{assertion}"
|
msg = f"执行断言失败:断言信息={assertion}, 原因={reason}"
|
||||||
print(msg)
|
|
||||||
super().__init__(msg)
|
super().__init__(msg)
|
||||||
|
self.logger.error(msg)
|
||||||
|
|
||||||
|
|
||||||
class ExecuteDynamiCodeError(MyBaseException):
|
class ExecuteDynamiCodeError(MyBaseException):
|
||||||
"""执行动态代码异常"""
|
"""执行动态代码异常"""
|
||||||
ERROR_CODE = 1006
|
ERROR_CODE = 1006
|
||||||
|
|
||||||
def __init__(self, code, reason):
|
def __init__(self, code_info, reason):
|
||||||
msg = f"执行动态代码异常:动态代码={code}, 原因={reason}"
|
msg = f"执行动态代码异常:动态代码信息={code_info}, 原因={reason}"
|
||||||
print(msg)
|
print(msg)
|
||||||
super().__init__(msg)
|
super().__init__(msg)
|
||||||
|
self.logger.error(msg)
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidSleepTimeError(MyBaseException):
|
||||||
|
"""无效的暂停时间异常"""
|
||||||
|
ERROR_CODE = 1007
|
||||||
|
|
||||||
|
def __init__(self, sleep_time, reason):
|
||||||
|
msg = f"无效的暂停时间:sleep_time={sleep_time},原因={reason}"
|
||||||
|
super().__init__(msg)
|
||||||
|
|
||||||
|
self.logger.error(msg)
|
||||||
|
|
||||||
|
|
||||||
|
class ScriptNotFoundError(MyBaseException):
|
||||||
|
"""脚本不存在异常"""
|
||||||
|
ERROR_CODE = 1008
|
||||||
|
|
||||||
|
def __init__(self, script_info, reason):
|
||||||
|
msg = f"脚本不存在异常:script_info={script_info},原因={reason}"
|
||||||
|
super().__init__(msg)
|
||||||
|
self.logger.error(msg)
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidParameterFormatError(MyBaseException):
|
||||||
|
"""无效的参数格式异常"""
|
||||||
|
ERROR_CODE = 1009
|
||||||
|
|
||||||
|
def __init__(self, parameter_info, reason):
|
||||||
|
msg = f"无效的参数格式异常:parameter_info={parameter_info},原因={reason}"
|
||||||
|
super().__init__(msg)
|
||||||
|
self.logger.error(msg)
|
||||||
|
|
||||||
|
|
||||||
|
class ResponseJsonConversionError(MyBaseException):
|
||||||
|
"""响应内容转换为 JSON 格式异常"""
|
||||||
|
ERROR_CODE = 1010
|
||||||
|
|
||||||
|
def __init__(self, response_text, reason):
|
||||||
|
msg = f"响应内容转换为 JSON 格式异常:响应内容={response_text}, 原因={reason}"
|
||||||
|
super().__init__(msg)
|
||||||
|
self.logger.error(msg)
|
||||||
|
|
||||||
|
|
||||||
|
class DynamicLoadingError(MyBaseException):
|
||||||
|
"""动态加载模块或文件异常"""
|
||||||
|
ERROR_CODE = 1011
|
||||||
|
|
||||||
|
def __init__(self, code_info, reason):
|
||||||
|
msg = f"动态加载模块或文件发生异常:动态模块或文件={code_info}, 原因={reason}"
|
||||||
|
super().__init__(msg)
|
||||||
|
self.logger.error(msg)
|
||||||
|
|
||||||
|
|
||||||
|
class EncryptionError(MyBaseException):
|
||||||
|
"""加密失败异常"""
|
||||||
|
ERROR_CODE = 1012
|
||||||
|
|
||||||
|
def __init__(self, method_name, error_message):
|
||||||
|
msg = f"加密失败异常: 加密方法={method_name} 原因={error_message}"
|
||||||
|
super().__init__(msg)
|
||||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -12,6 +12,7 @@ import json
|
||||||
|
|
||||||
import jsonpath
|
import jsonpath
|
||||||
|
|
||||||
|
from common.utils.exceptions import ParameterExtractionError
|
||||||
from common.validation import logger
|
from common.validation import logger
|
||||||
|
|
||||||
|
|
||||||
|
@ -78,6 +79,7 @@ class Extractor:
|
||||||
try:
|
try:
|
||||||
result = jsonpath.jsonpath(resp_obj if isinstance(resp_obj, (dict, list)) else json.dumps(resp_obj), expr)
|
result = jsonpath.jsonpath(resp_obj if isinstance(resp_obj, (dict, list)) else json.dumps(resp_obj), expr)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
ParameterExtractionError(expr, e)
|
||||||
return expr
|
return expr
|
||||||
else:
|
else:
|
||||||
if result is False:
|
if result is False:
|
||||||
|
|
|
@ -2,20 +2,13 @@ import importlib.util
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
from common.utils.exceptions import ScriptNotFoundError
|
||||||
|
|
||||||
sys.path.append('..')
|
sys.path.append('..')
|
||||||
sys.path.append('../utils')
|
sys.path.append('../utils')
|
||||||
|
|
||||||
from common.log_utils.mylogger import MyLogger
|
|
||||||
|
|
||||||
logger = MyLogger()
|
|
||||||
|
|
||||||
|
|
||||||
class ScriptNotFoundError(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class LoadScript:
|
class LoadScript:
|
||||||
# @logger.log_decorator()
|
|
||||||
def load_script(self, script_path):
|
def load_script(self, script_path):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
@ -32,14 +25,12 @@ class LoadScript:
|
||||||
script_module = importlib.util.module_from_spec(spec)
|
script_module = importlib.util.module_from_spec(spec)
|
||||||
spec.loader.exec_module(script_module)
|
spec.loader.exec_module(script_module)
|
||||||
return script_module
|
return script_module
|
||||||
except FileNotFoundError:
|
except Exception as e:
|
||||||
raise ScriptNotFoundError(script_path)
|
raise ScriptNotFoundError(script_path, e)
|
||||||
|
|
||||||
@logger.log_decorator()
|
|
||||||
def load_and_execute_script(self, script_directory, script_name, method_name, request):
|
def load_and_execute_script(self, script_directory, script_name, method_name, request):
|
||||||
"""
|
"""
|
||||||
加载并执行脚本文件中的指定方法
|
加载并执行脚本文件中的指定方法
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
request: 请求数据
|
request: 请求数据
|
||||||
script_directory (str): 脚本文件所在的目录
|
script_directory (str): 脚本文件所在的目录
|
||||||
|
@ -52,7 +43,8 @@ class LoadScript:
|
||||||
if hasattr(script, method_name):
|
if hasattr(script, method_name):
|
||||||
method = getattr(script, method_name)
|
method = getattr(script, method_name)
|
||||||
return method(request)
|
return method(request)
|
||||||
except ScriptNotFoundError:
|
except Exception as e:
|
||||||
|
ScriptNotFoundError(script_path, e)
|
||||||
return request
|
return request
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -12,14 +12,13 @@ import os
|
||||||
import types
|
import types
|
||||||
|
|
||||||
from common.data_extraction.dependent_parameter import DependentParameter
|
from common.data_extraction.dependent_parameter import DependentParameter
|
||||||
from common.validation import logger
|
from common.utils.exceptions import DynamicLoadingError
|
||||||
|
|
||||||
|
|
||||||
class LoadModulesFromFolder(DependentParameter):
|
class LoadModulesFromFolder(DependentParameter):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
@logger.log_decorator()
|
|
||||||
def load_modules_from_folder(self, folder_or_mnodule):
|
def load_modules_from_folder(self, folder_or_mnodule):
|
||||||
"""
|
"""
|
||||||
动态加载文件或模块
|
动态加载文件或模块
|
||||||
|
@ -50,7 +49,9 @@ class LoadModulesFromFolder(DependentParameter):
|
||||||
if callable(o):
|
if callable(o):
|
||||||
self.update_environments(n, o)
|
self.update_environments(n, o)
|
||||||
else:
|
else:
|
||||||
raise TypeError("folder_or_module should be either a folder path (str) or a module (types.ModuleType).")
|
raise DynamicLoadingError(folder_or_mnodule,
|
||||||
|
"older_or_module should be either a folder path (str) or a module ("
|
||||||
|
"types.ModuleType).")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
|
@ -9,12 +9,12 @@
|
||||||
# -------------------------------------------------------------------------------
|
# -------------------------------------------------------------------------------
|
||||||
import types
|
import types
|
||||||
|
|
||||||
from common.http_client.http_client import Pyt
|
from common.http_client.http_client import HttpClient
|
||||||
from common.validation import comparators
|
from common.validation import comparators
|
||||||
from common.validation import logger
|
from common.validation import logger
|
||||||
|
|
||||||
|
|
||||||
class Loaders(Pyt):
|
class Loaders(HttpClient):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
# -------------------------------------------------------------------------------
|
# -------------------------------------------------------------------------------
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from common.validation import logger
|
from common.utils.exceptions import InvalidParameterFormatError
|
||||||
from common.validation.comparator_dict import comparator_dict
|
from common.validation.comparator_dict import comparator_dict
|
||||||
from common.validation.extractor import Extractor
|
from common.validation.extractor import Extractor
|
||||||
from common.validation.loaders import Loaders
|
from common.validation.loaders import Loaders
|
||||||
|
@ -58,7 +58,7 @@ class Validator(Loaders):
|
||||||
"comparator": comparator
|
"comparator": comparator
|
||||||
})
|
})
|
||||||
else:
|
else:
|
||||||
logger.error("参数格式错误!")
|
InvalidParameterFormatError(validate_variables, "参数格式错误!")
|
||||||
|
|
||||||
def validate(self, resp=None):
|
def validate(self, resp=None):
|
||||||
"""
|
"""
|
||||||
|
@ -110,7 +110,8 @@ class Validator(Loaders):
|
||||||
self.assertions.clear()
|
self.assertions.clear()
|
||||||
self.uniform_validate(validate_variables)
|
self.uniform_validate(validate_variables)
|
||||||
if not self.validate_variables_list:
|
if not self.validate_variables_list:
|
||||||
raise "uniform_validate 执行失败,无法进行 validate 校验"
|
raise InvalidParameterFormatError(self.validate_variables_list,
|
||||||
|
"uniform_validate 执行失败,无法进行 validate 校验")
|
||||||
self.validate(resp)
|
self.validate(resp)
|
||||||
|
|
||||||
|
|
||||||
|
|
26
config.py
26
config.py
|
@ -22,9 +22,35 @@ class Config:
|
||||||
# 测试报告及 logger 所在路径
|
# 测试报告及 logger 所在路径
|
||||||
# *****************************************************************
|
# *****************************************************************
|
||||||
test_report = os.path.join(base_path, "output", "reports")
|
test_report = os.path.join(base_path, "output", "reports")
|
||||||
|
test_report_file = os.path.join(base_path, "output", "reports","report.html")
|
||||||
log_path = os.path.join(base_path, "output", "log")
|
log_path = os.path.join(base_path, "output", "log")
|
||||||
SCRIPTS_DIR = os.path.join(base_path, "scripts")
|
SCRIPTS_DIR = os.path.join(base_path, "scripts")
|
||||||
|
|
||||||
|
# 邮件配置信息
|
||||||
|
mail_data = {
|
||||||
|
"host": "smtp.qq.com", # 邮件服务地址
|
||||||
|
"user": "262667641@qq.com", # 用户名
|
||||||
|
"password": "ztvqsnikiupvbghe", # 密码(部分邮箱为授权码)# 密码
|
||||||
|
"sender": "262667641@qq.com", # 发送人
|
||||||
|
"port": 465, # smtp 端口号
|
||||||
|
"receivers": ['262667641@qq.com', '125109524@qq.com'] # 接收方的邮箱
|
||||||
|
}
|
||||||
|
# 企业微信机器人配置信息
|
||||||
|
weixin_notice = {
|
||||||
|
"send_url": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=8b1647d4-dc32-447c-b524-548acf18a938",
|
||||||
|
"upload_url": "https://qyapi.weixin.qq.com/cgi-bin/webhook/upload_media?type=file&key=8b1647d4-dc32-447c-b524-548acf18a938",
|
||||||
|
"file_lists": [test_report_file, test_case] # 需要推送的文件的路径
|
||||||
|
}
|
||||||
|
# 钉钉机器人配置信息
|
||||||
|
dingtalk_notice = {
|
||||||
|
"url": "https://oapi.dingtalk.com/robot/send?access_token=7d1e11079e00a4ca9f11283f526349abd5ba3f792ef7bcb346909ff215af02de",
|
||||||
|
"secret": "SEC441dbbdb8dbe150e5fc3e348bb449d3113b1be1a90be527b898ccd78c51566c1",
|
||||||
|
"key": "", # 安全关键字
|
||||||
|
"atMobiles": "18127813600", # 需要@指定人员的手机号
|
||||||
|
"isAtAll": True, # 是否@ 所有人
|
||||||
|
"except_info": False # 是否发送测试不通过的异常数据
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
test = Config()
|
test = Config()
|
||||||
|
|
|
@ -15,15 +15,13 @@ from natsort import natsorted
|
||||||
__all__ = ["md5_sign", "sha1_sign"]
|
__all__ = ["md5_sign", "sha1_sign"]
|
||||||
|
|
||||||
from common.crypto.encryption_str import sha1_secret_str, md5
|
from common.crypto.encryption_str import sha1_secret_str, md5
|
||||||
from extensions import logger
|
|
||||||
|
|
||||||
|
|
||||||
@logger.log_decorator()
|
|
||||||
def md5_sign(data: dict):
|
def md5_sign(data: dict):
|
||||||
"""
|
"""
|
||||||
数据加签
|
数据加签
|
||||||
Args:
|
Args:
|
||||||
**data:需要加钱的数据
|
**data:需要加钱的数据DD
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
|
@ -42,7 +40,6 @@ def md5_sign(data: dict):
|
||||||
return {**data, **{"sign": sign_value}}
|
return {**data, **{"sign": sign_value}}
|
||||||
|
|
||||||
|
|
||||||
@logger.log_decorator()
|
|
||||||
def sha1_sign(post_data: dict):
|
def sha1_sign(post_data: dict):
|
||||||
timestamp = int(round(time.time() * 1000)) # 毫秒级时间戳
|
timestamp = int(round(time.time() * 1000)) # 毫秒级时间戳
|
||||||
argument = {"secretKey": "", "timestamp": timestamp} # 加密加盐参数
|
argument = {"secretKey": "", "timestamp": timestamp} # 加密加盐参数
|
||||||
|
|
|
@ -7,8 +7,8 @@
|
||||||
@time: 2023/3/14 16:21
|
@time: 2023/3/14 16:21
|
||||||
@desc:
|
@desc:
|
||||||
"""
|
"""
|
||||||
from common.log_utils.mylogger import MyLogger
|
# from common.log_utils.mylogger import MyLogger
|
||||||
|
|
||||||
logger = MyLogger()
|
# logger = MyLogger()
|
||||||
|
|
||||||
from .dynamic_scaling_methods import *
|
from .dynamic_scaling_methods import *
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 530 KiB |
File diff suppressed because one or more lines are too long
10
run.py
10
run.py
|
@ -4,7 +4,6 @@
|
||||||
# @Email : 262667641@qq.com
|
# @Email : 262667641@qq.com
|
||||||
# @File : run_main.py
|
# @File : run_main.py
|
||||||
# @Project : risk_api_project
|
# @Project : risk_api_project
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
|
@ -13,8 +12,8 @@ sys.path.append("./")
|
||||||
sys.path.append('cases')
|
sys.path.append('cases')
|
||||||
|
|
||||||
from config import Config
|
from config import Config
|
||||||
from unittestreport import TestRunner
|
# from unittestreport import TestRunner
|
||||||
from common.utils.WxworkSms import WxWorkSms
|
from unittestreportnew import TestRunner
|
||||||
|
|
||||||
|
|
||||||
def run():
|
def run():
|
||||||
|
@ -23,7 +22,10 @@ def run():
|
||||||
runner = TestRunner(test_case, report_dir=test_report, title="接口自动化测试报告", templates=2, tester="kira",
|
runner = TestRunner(test_case, report_dir=test_report, title="接口自动化测试报告", templates=2, tester="kira",
|
||||||
desc="自动化测试")
|
desc="自动化测试")
|
||||||
runner.run()
|
runner.run()
|
||||||
WxWorkSms('8b1647d4-dc32-447c-b524-548acf18a938').send_main(test_report, 1, 2, 3, 4, 5, 6, 7, 8, 9)
|
# get_failed_test_cases = runner.get_failed_test_cases()
|
||||||
|
# runner.email_notice()
|
||||||
|
# runner.dingtalk_notice()
|
||||||
|
runner.weixin_notice()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
"""
|
||||||
|
============================
|
||||||
|
Author:柠檬班-木森
|
||||||
|
Time:2020/7/16 17:47
|
||||||
|
E-mail:3247119728@qq.com
|
||||||
|
Company:湖南零檬信息技术有限公司
|
||||||
|
============================
|
||||||
|
"""
|
|
@ -0,0 +1,9 @@
|
||||||
|
### 【{{title}}】测试结果
|
||||||
|
##### 测试人员: {{tester}}
|
||||||
|
##### 开始时间: {{begin_time}}
|
||||||
|
##### 执行时间: {{runtime}}
|
||||||
|
##### 用例总数: {{all}}
|
||||||
|
##### 成功用例: {{success}}
|
||||||
|
##### 失败用例: {{fail}}
|
||||||
|
##### 错误用例: {{error}}
|
||||||
|
##### 跳过用例: {{skip}}
|
|
@ -0,0 +1,456 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>测试报告</title>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="https://cdn.staticfile.org/twitter-bootstrap/4.5.0/css/bootstrap.min.css">
|
||||||
|
<script src=" https://cdn.staticfile.org/jquery/2.0.0/jquery.min.js"></script>
|
||||||
|
<script src="https://cdn.staticfile.org/echarts/5.1.2/echarts.min.js"></script>
|
||||||
|
<!-- 页面样式-->
|
||||||
|
<style type="text/css">
|
||||||
|
/*标题样式*/
|
||||||
|
.title {
|
||||||
|
width: auto;
|
||||||
|
height: 60px;
|
||||||
|
text-align: center;
|
||||||
|
font: bolder 38px/60px "Microsoft YaHei UI";
|
||||||
|
}
|
||||||
|
|
||||||
|
/*汇总信息样式*/
|
||||||
|
.summary {
|
||||||
|
width: 90%;
|
||||||
|
position: absolute;
|
||||||
|
top: 120px;
|
||||||
|
margin-left: 5%;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-left {
|
||||||
|
font: bolder 20px/30px "Microsoft YaHei UI";
|
||||||
|
}
|
||||||
|
|
||||||
|
.left {
|
||||||
|
width: 50%;
|
||||||
|
float: left;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.right {
|
||||||
|
width: 50%;
|
||||||
|
float: right;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.desc {
|
||||||
|
float: left;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item span {
|
||||||
|
font: normal 16px/38px "Microsoft YaHei UI";
|
||||||
|
padding: 30px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item {
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
padding: .4rem 1.25rem;
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1px solid rgba(0, 0, 0, .125);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 执行信息样式 */
|
||||||
|
.test_info {
|
||||||
|
width: 90%;
|
||||||
|
position: absolute;
|
||||||
|
top: 900px;
|
||||||
|
margin-left: 5%;
|
||||||
|
|
||||||
|
color: #28a745 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table td, th {
|
||||||
|
border: solid 2px rgba(9, 122, 51, 0.11) !important;
|
||||||
|
padding: 0;
|
||||||
|
line-height: 40px;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
select {
|
||||||
|
border: 0;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
height: 2em;
|
||||||
|
width: 8em;
|
||||||
|
margin-left: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
option {
|
||||||
|
text-align: center;
|
||||||
|
height: 36px;
|
||||||
|
font: none 18px/36px "Microsoft YaHei UI";
|
||||||
|
color: #28a745 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test_log {
|
||||||
|
background: rgba(163, 171, 189, 0.15);
|
||||||
|
width: 100%;
|
||||||
|
height: 50px;
|
||||||
|
border-top: none;
|
||||||
|
border-bottom: none;
|
||||||
|
display: none;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test_log td {
|
||||||
|
text-align: left;
|
||||||
|
height: 30px;
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 3em;
|
||||||
|
padding-right:3em;
|
||||||
|
font: none 18px/24px "Microsoft YaHei UI";
|
||||||
|
color: #9e141a;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
margin: 0;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
white-space: -moz-pre-wrap;
|
||||||
|
white-space: -o-pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
line-height: 22px;
|
||||||
|
font-size: 14px
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 测试图表显示*/
|
||||||
|
.char {
|
||||||
|
width: 90%;
|
||||||
|
position: absolute;
|
||||||
|
top: 450px;
|
||||||
|
margin-left: 5%;
|
||||||
|
color: #28a745 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<!--报告标题-->
|
||||||
|
<div class="title text-success">
|
||||||
|
<div class="shadow-lg p-3 mb-5 bg-white rounded">{{ title }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!--汇总信息-->
|
||||||
|
<div class="summary">
|
||||||
|
<p class="text-left text-success">测试结果汇总</p>
|
||||||
|
<div class="left">
|
||||||
|
<ul class="list-group">
|
||||||
|
<li class="list-group-item">
|
||||||
|
<button type="button" class="btn btn-success">测试人员</button>
|
||||||
|
<span class="text-dark">{{ tester }}</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item">
|
||||||
|
<button type="button" class="btn btn-success">开始时间</button>
|
||||||
|
<span class="text-dark">{{ begin_time }}</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item">
|
||||||
|
<button type="button" class="btn btn-success">执行时间</button>
|
||||||
|
<span class="text-dark">{{ runtime }}</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item">
|
||||||
|
<button type="button" class="btn btn-success">用例总数</button>
|
||||||
|
<span class="text-dark">{{ all }}</span>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="right">
|
||||||
|
<ul class="list-group">
|
||||||
|
|
||||||
|
<li class="list-group-item">
|
||||||
|
<button type="button" class="btn btn-success">成功用例</button>
|
||||||
|
<span class="text-success">{{ success }}</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item">
|
||||||
|
<button type="button" class="btn btn-warning">失败用例</button>
|
||||||
|
<span class="text-warning">{{ fail }}</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item">
|
||||||
|
<button type="button" class="btn btn-danger">错误用例</button>
|
||||||
|
<span class="text-danger">{{ error }}</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item">
|
||||||
|
<button type="button" class="btn btn-secondary">跳过用例</button>
|
||||||
|
<span class="text-secondary">{{ skip }}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="desc">
|
||||||
|
<ul class="list-group">
|
||||||
|
<li class="list-group-item">
|
||||||
|
<button type="button" class="btn btn-success">描述信息</button>
|
||||||
|
<span class="text-secondary">{{ desc }}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!--测试图表-->
|
||||||
|
<div class="char">
|
||||||
|
<p class="text-left text-success">图表展示</p>
|
||||||
|
<div id="char2" style="width: 49%;height: 400px;float: left"></div>
|
||||||
|
<div id="char" style="width: 49%;height: 400px ;float: left"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!--详细信息-->
|
||||||
|
<div class="test_info">
|
||||||
|
|
||||||
|
<p class="text-left text-success">详细信息</p>
|
||||||
|
<div class="table_data">
|
||||||
|
|
||||||
|
<table class="table">
|
||||||
|
<thead class="bg-success text-light">
|
||||||
|
<tr>
|
||||||
|
<th scope="col" style="width: 5%;padding: 0">编号</th>
|
||||||
|
<th scope="col" style="width: 20%;padding: 0">
|
||||||
|
<span>测试类</span>
|
||||||
|
<select id="testClass">
|
||||||
|
<option>所有</option>
|
||||||
|
{% for foo in testClass %}
|
||||||
|
<option>{{ foo }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
</th>
|
||||||
|
|
||||||
|
<th scope="col" style="width: 15%;padding: 0">测试方法</th>
|
||||||
|
<th scope="col" style="width: 20%;padding: 0">用例描述</th>
|
||||||
|
<th scope="col" style="width: 15%;padding: 0">执行时间</th>
|
||||||
|
<th scope="col" style="width: 20%;padding: 0">
|
||||||
|
|
||||||
|
<span>执行结果</span>
|
||||||
|
<select id="testResult">
|
||||||
|
<option>所有</option>
|
||||||
|
<option class="text-success">成功</option>
|
||||||
|
<option class="text-warning">失败</option>
|
||||||
|
<option class="text-danger">错误</option>
|
||||||
|
<option class="text-info">跳过</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
</th>
|
||||||
|
<th scope="col" style="width: 10%;padding: 0">详细信息</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for foo in results %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ loop.index }}</td>
|
||||||
|
<td class="{{ foo.class_name }}">{{ foo.class_name }}</td>
|
||||||
|
<td>{{ foo.method_name }}</td>
|
||||||
|
<td>{{ foo.method_doc }}</td>
|
||||||
|
<td>{{ foo.run_time }}</td>
|
||||||
|
{% if foo.state == '成功' %}
|
||||||
|
<td class="text-success">{{ foo.state }}</td>
|
||||||
|
{% elif foo.state == '失败' %}
|
||||||
|
<td class="text-warning">{{ foo.state }}</td>
|
||||||
|
{% elif foo.state == '错误' %}
|
||||||
|
<td class="text-danger">{{ foo.state }}</td>
|
||||||
|
{% else %}
|
||||||
|
<td class="text-info">{{ foo.state }}</td>
|
||||||
|
{% endif %}
|
||||||
|
<td>
|
||||||
|
<button type="button" class="btn btn-success btn_info">查看详情</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr class="test_log">
|
||||||
|
|
||||||
|
<td colspan="7" class="small text-muted" style=" word-wrap:break-word; word-break:break-all">
|
||||||
|
{% for item in foo.run_info %}
|
||||||
|
|
||||||
|
<pre>{{ item }}</pre>
|
||||||
|
{% endfor %}
|
||||||
|
{% if foo.run_info == [] %}
|
||||||
|
<pre>无内容输出!</pre>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="height: 200px"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
var tbodyTr = $('tbody tr');
|
||||||
|
var testResult = $("#testResult");
|
||||||
|
var testClass = $("#testClass");
|
||||||
|
<!-- 用例执行详细信息显示切换-->
|
||||||
|
$(".btn_info").click(function () {
|
||||||
|
$(this).parent().parent().next().toggle();
|
||||||
|
|
||||||
|
});
|
||||||
|
// 当选择用例类之后触发
|
||||||
|
testClass.change(function () {
|
||||||
|
var cls = $(this).val();
|
||||||
|
var res = testResult.val();
|
||||||
|
elementDisplay(cls, res);
|
||||||
|
sort()
|
||||||
|
});
|
||||||
|
testResult.change(function () {
|
||||||
|
var res = $(this).val();
|
||||||
|
var cls = testClass.val();
|
||||||
|
elementDisplay(cls, res);
|
||||||
|
sort()
|
||||||
|
});
|
||||||
|
|
||||||
|
function elementDisplay(cls, res) {
|
||||||
|
// 用例数据的显示
|
||||||
|
if (cls === "所有") {
|
||||||
|
if (res === "所有") {
|
||||||
|
tbodyTr.has('button').show();
|
||||||
|
} else if (res === '成功') {
|
||||||
|
tbodyTr.hide();
|
||||||
|
tbodyTr.has('button').has('.text-success').show()
|
||||||
|
|
||||||
|
} else if (res === '失败') {
|
||||||
|
tbodyTr.hide();
|
||||||
|
tbodyTr.has('button').has('.text-warning').show()
|
||||||
|
|
||||||
|
} else if (res === '错误') {
|
||||||
|
tbodyTr.hide();
|
||||||
|
tbodyTr.has('button').has('.text-danger').show()
|
||||||
|
|
||||||
|
} else if (res === '跳过') {
|
||||||
|
tbodyTr.hide();
|
||||||
|
tbodyTr.has('button').has('.text-info').show()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (res === "所有") {
|
||||||
|
tbodyTr.hide();
|
||||||
|
tbodyTr.has('button').has('.' + cls + '').show()
|
||||||
|
} else if (res === '成功') {
|
||||||
|
tbodyTr.hide();
|
||||||
|
tbodyTr.has('button').has('.' + cls + '').has('.text-success').show()
|
||||||
|
} else if (res === '失败') {
|
||||||
|
tbodyTr.hide();
|
||||||
|
tbodyTr.has('button').has('.' + cls + '').has('.text-warning').show()
|
||||||
|
} else if (res === '错误') {
|
||||||
|
tbodyTr.hide();
|
||||||
|
tbodyTr.has('button').has('.' + cls + '').has('.text-danger').show()
|
||||||
|
} else if (res === '跳过') {
|
||||||
|
tbodyTr.hide();
|
||||||
|
tbodyTr.has('button').has('.' + cls + '').has('.text-info').show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sort() {
|
||||||
|
//重新排列显示序号
|
||||||
|
// 选择所有可以见的tr
|
||||||
|
var visibleTr = tbodyTr.filter(":visible");
|
||||||
|
|
||||||
|
visibleTr.each(function (index, element) {
|
||||||
|
element.firstElementChild.innerHTML = index + 1;
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
// 基于准备好的dom,初始化echarts实例
|
||||||
|
var myChart = echarts.init(document.getElementById('char'));
|
||||||
|
var myChart2 = echarts.init(document.getElementById('char2'));
|
||||||
|
// 指定图表的配置项和数据
|
||||||
|
option = {
|
||||||
|
color: ['#00a10a', '#ddb518', 'rgba(204,46,41,0.73)', '#85898c'],
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'item',
|
||||||
|
formatter: '{a} <br/>{b}: {c} ({d}%)'
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
orient: 'vertical',
|
||||||
|
left: 10,
|
||||||
|
data: ['通过', '失败', '错误', '跳过']
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: '测试结果',
|
||||||
|
type: 'pie',
|
||||||
|
radius: ['50%', '70%'],
|
||||||
|
avoidLabelOverlap: false,
|
||||||
|
label: {
|
||||||
|
show: false,
|
||||||
|
position: 'center'
|
||||||
|
},
|
||||||
|
emphasis: {
|
||||||
|
label: {
|
||||||
|
show: true,
|
||||||
|
fontSize: '30',
|
||||||
|
fontWeight: 'bold'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
labelLine: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
data: [
|
||||||
|
{value: {{success}}, name: '通过'},
|
||||||
|
{value: {{fail}}, name: '失败'},
|
||||||
|
{value: {{error}}, name: '错误'},
|
||||||
|
{value: {{skip}}, name: '跳过'}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
option2 = {
|
||||||
|
tooltip: {
|
||||||
|
formatter: '{a} <br/>{b} : {c}%'
|
||||||
|
},
|
||||||
|
toolbox: {
|
||||||
|
feature: {
|
||||||
|
restore: {},
|
||||||
|
saveAsImage: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: '测试结果',
|
||||||
|
type: 'gauge',
|
||||||
|
detail: {formatter: '{{pass_rate}}%'},
|
||||||
|
data: [{value: '{{pass_rate}}', name: '用例通过率'}],
|
||||||
|
axisLine: {
|
||||||
|
lineStyle: {
|
||||||
|
color: [
|
||||||
|
[0.2, '#c20000'],
|
||||||
|
[0.8, '#ddb518'],
|
||||||
|
[1, '#00a10a']]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
myChart2.setOption(option2);
|
||||||
|
// 使用刚指定的配置项和数据显示图表。
|
||||||
|
myChart.setOption(option);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,90 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>测试报告</title>
|
||||||
|
<!-- 页面样式-->
|
||||||
|
<style type="text/css">
|
||||||
|
.main {
|
||||||
|
height: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*汇总信息样式*/
|
||||||
|
.summary {
|
||||||
|
width: 90%;
|
||||||
|
position: absolute;
|
||||||
|
margin-left: 5%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
width: auto;
|
||||||
|
height: 60px;
|
||||||
|
text-align: center;
|
||||||
|
font: bolder 38px/60px "Microsoft YaHei UI";
|
||||||
|
color: #02992c;
|
||||||
|
margin-bottom: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-left {
|
||||||
|
text-align: left;
|
||||||
|
font: bolder 20px/30px "Microsoft YaHei UI";
|
||||||
|
color: #02992c;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
td, th {
|
||||||
|
border: solid 1px rgba(133, 137, 140, 0.76);
|
||||||
|
height: 50px;
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
color: #02992c;
|
||||||
|
padding-left: 5%;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
color: #00a10a;
|
||||||
|
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="main">
|
||||||
|
<!--汇总信息-->
|
||||||
|
<div class="summary">
|
||||||
|
<div class="text-left">【{{ title }}】用例执行汇总信息如下:</div>
|
||||||
|
<table width=60% border="1" cellpadding="0" cellspacing="0" bordercolor="#FF0000"
|
||||||
|
style="border-collapse:collapse;">
|
||||||
|
<tr>
|
||||||
|
<th>测试人员</th>
|
||||||
|
<td>{{ tester }}</td>
|
||||||
|
<th>成功用例</th>
|
||||||
|
<td><span>{{ success }}</span></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>开始时间</th>
|
||||||
|
<td><span>{{ begin_time }}</span></td>
|
||||||
|
<th style="color: #cc8b0c">失败用例</th>
|
||||||
|
<td style="color: #cc8b0c">{{ fail }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>执行时间</th>
|
||||||
|
<td><span>{{ runtime }}</span></td>
|
||||||
|
<th style="color: #cc0109">错误用例</th>
|
||||||
|
<td><span style="color: #cc0109">{{ error }}</span></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>用例总数</th>
|
||||||
|
<td><span>{{ all }}</span></td>
|
||||||
|
<th style="color: #575a5c">跳过用例</th>
|
||||||
|
<td><span style="color: #575a5c">{{ skip }}</span></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,799 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>{{ title }}</title>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="https://cdn.staticfile.org/twitter-bootstrap/4.5.0/css/bootstrap.min.css">
|
||||||
|
<script src=" https://cdn.staticfile.org/jquery/2.0.0/jquery.min.js"></script>
|
||||||
|
<script src="https://cdn.staticfile.org/echarts/5.1.2/echarts.min.js"></script>
|
||||||
|
<!-- 页面样式-->
|
||||||
|
<style type="text/css">
|
||||||
|
/*标题样式*/
|
||||||
|
|
||||||
|
.main {
|
||||||
|
background: url("image/test_img.png") no-repeat;
|
||||||
|
background-size: 100%;
|
||||||
|
-webkit-background-size: 100%;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
width: auto;
|
||||||
|
height: 80px;
|
||||||
|
text-align: center;
|
||||||
|
font: bolder 30px/80px "Microsoft YaHei UI";
|
||||||
|
color: #fff;
|
||||||
|
background: rgba(3, 14, 70, 0.5);
|
||||||
|
border-bottom: solid 1px rgb(3, 14, 70);
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-box {
|
||||||
|
height: 700px;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box1 {
|
||||||
|
flex: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box2 {
|
||||||
|
flex: 5;
|
||||||
|
margin: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box3 {
|
||||||
|
flex: 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
position: relative;
|
||||||
|
border: 1px solid rgba(25, 186, 139, 0.17);
|
||||||
|
background: rgba(3, 14, 70, 0.5);
|
||||||
|
padding: 0 10px 50px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel h2, .test_info h2 {
|
||||||
|
height: 40px;
|
||||||
|
line-height: 40px;
|
||||||
|
text-align: center;
|
||||||
|
color: #007bff;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel .chart {
|
||||||
|
height: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel .chart2 {
|
||||||
|
height: 350px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel .desc {
|
||||||
|
height: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel .desc .info {
|
||||||
|
font: normal 18px/25px "Microsoft YaHei UI";
|
||||||
|
color: #fff;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel .desc .info div {
|
||||||
|
padding: 10px;
|
||||||
|
border: solid 1px #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel .desc .info div span {
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* 执行信息样式 */
|
||||||
|
.test_info {
|
||||||
|
width: 100%;
|
||||||
|
padding: 20px 100px;
|
||||||
|
background: rgba(3, 14, 70, 0.5);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.table td, .table th {
|
||||||
|
border: solid 1px #5765a4 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
line-height: 40px;
|
||||||
|
text-align: center;
|
||||||
|
height: 40px;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
border: 0;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
height: 2em;
|
||||||
|
width: 8em;
|
||||||
|
margin-left: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
option {
|
||||||
|
text-align: center;
|
||||||
|
height: 36px;
|
||||||
|
font: none 18px/36px "Microsoft YaHei UI";
|
||||||
|
color: #28a745 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test_log {
|
||||||
|
background: rgba(163, 171, 189, 0.15);
|
||||||
|
width: 100%;
|
||||||
|
height: 50px;
|
||||||
|
border-top: none;
|
||||||
|
border-bottom: none;
|
||||||
|
display: none;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test_log td {
|
||||||
|
text-align: left;
|
||||||
|
height: 30px;
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 3em;
|
||||||
|
padding-right: 3em;
|
||||||
|
font: none 18px/24px "Microsoft YaHei UI";
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
margin: 0;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
white-space: -moz-pre-wrap;
|
||||||
|
white-space: -o-pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
color: #fff;
|
||||||
|
line-height: 22px;
|
||||||
|
font-size: 14px
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart4 {
|
||||||
|
overflow: auto;
|
||||||
|
width: 100%;
|
||||||
|
height: 600px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart4::-webkit-scrollbar {
|
||||||
|
width: 10px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart4::-webkit-scrollbar-thumb {
|
||||||
|
/*滚动条里面小方块*/
|
||||||
|
border-radius: 5px;
|
||||||
|
-webkit-box-shadow: inset 0 0 5px rgba(4, 0, 225, 0.62);
|
||||||
|
background: #272789;
|
||||||
|
height: 10px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart4::-webkit-scrollbar-track {
|
||||||
|
/*滚动条里面轨道*/
|
||||||
|
-webkit-box-shadow: inset 0 0 5px rgba(0, 21, 255, 0.54);
|
||||||
|
border-radius: 10px;
|
||||||
|
background: rgba(214, 214, 214, 0.64);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/*详细内容描述的小标题*/
|
||||||
|
.table_title {
|
||||||
|
text-align: center;
|
||||||
|
background: rgba(1, 2, 37, 0.72);
|
||||||
|
font: bold 18px/30px 'Microsoft YaHei UI';
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 10px
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="main">
|
||||||
|
<div class="title">
|
||||||
|
{{ title }}
|
||||||
|
</div>
|
||||||
|
<div class="content-box">
|
||||||
|
<div class="box1">
|
||||||
|
<div class="panel">
|
||||||
|
<h2>执行结果</h2>
|
||||||
|
<div class="chart" id="char3"></div>
|
||||||
|
<div class="panel-footer"></div>
|
||||||
|
</div>
|
||||||
|
<div class="panel ">
|
||||||
|
<h2>成功占比</h2>
|
||||||
|
<div class="chart" id="char1"></div>
|
||||||
|
<div class="panel-footer"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="box2">
|
||||||
|
<div class="panel">
|
||||||
|
<h2>运行信息</h2>
|
||||||
|
<div class="desc">
|
||||||
|
|
||||||
|
<div class="info">
|
||||||
|
<div style="flex:5">
|
||||||
|
<button type="button" class="btn btn-primary btn-sm">开始时间</button>
|
||||||
|
<span>{{ begin_time }}</span>
|
||||||
|
</div>
|
||||||
|
<div style="flex:5">
|
||||||
|
<button type="button" class="btn btn-primary btn-sm">用例总数</button>
|
||||||
|
<span>{{ all }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="info">
|
||||||
|
<div style="flex:5">
|
||||||
|
<button type="button" class="btn btn-primary btn-sm">运行时长</button>
|
||||||
|
<span>{{ runtime }}</span>
|
||||||
|
</div>
|
||||||
|
<div style="flex:5">
|
||||||
|
<button type="button" class="btn btn-primary btn-sm ">测试人员</button>
|
||||||
|
<span>{{ tester }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="info">
|
||||||
|
<div style="flex:5">
|
||||||
|
<button type="button" class="btn btn-primary btn-sm">成功用例</button>
|
||||||
|
<span>{{ success }}</span>
|
||||||
|
</div>
|
||||||
|
<div style="flex:5">
|
||||||
|
<button type="button" class="btn btn-primary btn-sm ">通过率</button>
|
||||||
|
<span>{{pass_rate}}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel">
|
||||||
|
<h2>通过率趋势图</h2>
|
||||||
|
<div class="chart2" id="char2"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="box3">
|
||||||
|
<div class="panel">
|
||||||
|
<h2>历史构建结果</h2>
|
||||||
|
<div class="chart4">
|
||||||
|
<table class="table" style="color: #d6d6d6;padding: 0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">执行时间</th>
|
||||||
|
<th scope="col">用例总数</th>
|
||||||
|
<th scope="col">成功用例数</th>
|
||||||
|
<th scope="col">通过率</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for hos in history[::-1] %}
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{{hos.begin_time}}</th>
|
||||||
|
<td>{{hos.all}}</td>
|
||||||
|
<td>{{hos.success}}</td>
|
||||||
|
<td>{{hos.pass_rate}}%</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="test_info">
|
||||||
|
<h2>本次运行详情</h2>
|
||||||
|
<div class="table_data">
|
||||||
|
|
||||||
|
<table class="table" style="color: #fff">
|
||||||
|
<thead class="text-light" style="background: rgba(3, 14, 70, 0.5)">
|
||||||
|
<tr>
|
||||||
|
<th scope="col" style="width: 5%;padding: 0">编号</th>
|
||||||
|
<th scope="col" style="width: 20%;padding: 0">
|
||||||
|
<span>测试类</span>
|
||||||
|
<select id="testClass">
|
||||||
|
<option>所有</option>
|
||||||
|
{% for foo in testClass %}
|
||||||
|
<option>{{ foo }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
</th>
|
||||||
|
<th scope="col" style="width: 15%;padding: 0">测试方法</th>
|
||||||
|
<th scope="col" style="width: 20%;padding: 0">用例描述</th>
|
||||||
|
<th scope="col" style="width: 15%;padding: 0">执行时间</th>
|
||||||
|
<th scope="col" style="width: 20%;padding: 0">
|
||||||
|
|
||||||
|
<span>执行结果</span>
|
||||||
|
<select id="testResult">
|
||||||
|
<option>所有</option>
|
||||||
|
<option class="text-success">成功</option>
|
||||||
|
<option class="text-danger">失败</option>
|
||||||
|
<option class="text-warning">错误</option>
|
||||||
|
<option class="text-info">跳过</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
</th>
|
||||||
|
<th scope="col" style="width: 10%;padding: 0">详细信息</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for foo in results %}
|
||||||
|
<tr class="case_">
|
||||||
|
<td>{{ loop.index }}</td>
|
||||||
|
<td class="{{ foo.class_name }}">{{ foo.class_name }}</td>
|
||||||
|
<td>{{ foo.method_name }}</td>
|
||||||
|
<td>{{ foo.method_doc }}</td>
|
||||||
|
<td>{{ foo.run_time }}</td>
|
||||||
|
{% if foo.state == '成功' %}
|
||||||
|
<td class="text-success">{{ foo.state }}</td>
|
||||||
|
{% elif foo.state == '失败' %}
|
||||||
|
<td class="text-warning">{{ foo.state }}</td>
|
||||||
|
{% elif foo.state == '错误' %}
|
||||||
|
<td class="text-danger">{{ foo.state }}</td>
|
||||||
|
{% else %}
|
||||||
|
<td class="text-info">{{ foo.state }}</td>
|
||||||
|
{% endif %}
|
||||||
|
<td>
|
||||||
|
<button type="button" class="btn btn_info btn-primary btn-sm">查看详情</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr class="test_log">
|
||||||
|
|
||||||
|
<td colspan="7" class="small text-muted" style=" word-wrap:break-word; word-break:break-all">
|
||||||
|
{% for item in foo.run_info %}
|
||||||
|
|
||||||
|
<pre>{{ item }}</pre>
|
||||||
|
{% endfor %}
|
||||||
|
{% if foo.run_info == [] %}
|
||||||
|
<pre>无内容输出!</pre>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="height: 200px"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
|
||||||
|
const resulte = {
|
||||||
|
"success": `{{success}}`,
|
||||||
|
"all": `{{all}}`,
|
||||||
|
"fail": `{{fail}}`,
|
||||||
|
"skip": '{{skip}}',
|
||||||
|
"error": `{{error}}`,
|
||||||
|
"runtime": '{{runtime}}',
|
||||||
|
"begin_time": "{{runtime}}",
|
||||||
|
"pass_rate": '{{pass_rate}}',
|
||||||
|
}
|
||||||
|
;
|
||||||
|
const history = {{history}};
|
||||||
|
|
||||||
|
var passRate = [];
|
||||||
|
var dTime = [];
|
||||||
|
history.forEach(function (item, index, array) {
|
||||||
|
passRate.push(item.pass_rate);
|
||||||
|
dTime.push(item.begin_time)
|
||||||
|
});
|
||||||
|
if (passRate.length === 1) {
|
||||||
|
passRate.unshift(0);
|
||||||
|
dTime.unshift(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
function char01() {
|
||||||
|
let myChart = echarts.init(document.getElementById('char1'));
|
||||||
|
let option = {
|
||||||
|
color: ['#28a745', '#ffc107', '#dc3545', '#17a2b8'],
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'item',
|
||||||
|
formatter: '{a} <br/>{b}: {c} ({d}%)',
|
||||||
|
backgroundColor: 'rgba(3, 14, 70, 0.5)',
|
||||||
|
borderColor: '#333',
|
||||||
|
textStyle: {
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: "13"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
bottom: "0%",
|
||||||
|
// 小图标的宽度和高度
|
||||||
|
itemWidth: 10,
|
||||||
|
itemHeight: 10,
|
||||||
|
data: ['通过', '失败', '错误', '跳过'],
|
||||||
|
textStyle: {
|
||||||
|
color: "rgba(255,255,255,.5)",
|
||||||
|
fontSize: "12"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: '测试结果',
|
||||||
|
type: 'pie',
|
||||||
|
radius: ['50%', '70%'],
|
||||||
|
avoidLabelOverlap: false,
|
||||||
|
label: {
|
||||||
|
show: false,
|
||||||
|
position: 'center'
|
||||||
|
},
|
||||||
|
emphasis: {
|
||||||
|
label: {
|
||||||
|
show: true,
|
||||||
|
fontSize: '20',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: '#fff',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
labelLine: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
data: [
|
||||||
|
{value: resulte.success, name: '通过'},
|
||||||
|
{value: resulte.fail, name: '失败'},
|
||||||
|
{value: resulte.error, name: '错误'},
|
||||||
|
{value: resulte.skip, name: '跳过'}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
myChart.setOption(option)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
char01();
|
||||||
|
|
||||||
|
// 大图
|
||||||
|
function char02() {
|
||||||
|
|
||||||
|
// 基于准备好的dom,初始化echarts实例
|
||||||
|
let myChart = echarts.init(document.getElementById("char2"));
|
||||||
|
// 2. 指定配置和数据
|
||||||
|
option = {
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
formatter: '{a} <br/>{b}: ({c}%)',
|
||||||
|
backgroundColor: 'rgba(3, 14, 70, 0.5)',
|
||||||
|
borderColor: '#333',
|
||||||
|
textStyle: {
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: "13"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
grid: {
|
||||||
|
left: "10",
|
||||||
|
top: "30",
|
||||||
|
right: "10",
|
||||||
|
bottom: "0",
|
||||||
|
containLabel: true
|
||||||
|
},
|
||||||
|
|
||||||
|
xAxis: [{
|
||||||
|
type: "category",
|
||||||
|
boundaryGap: false,
|
||||||
|
show: false,
|
||||||
|
axisLabel: {
|
||||||
|
textStyle: {
|
||||||
|
color: "rgba(255,255,255,.6)",
|
||||||
|
fontSize: 12
|
||||||
|
}
|
||||||
|
},
|
||||||
|
axisLine: {
|
||||||
|
lineStyle: {
|
||||||
|
color: "rgba(255,255,255,.2)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data: dTime
|
||||||
|
},
|
||||||
|
{
|
||||||
|
axisPointer: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
axisLine: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
position: "bottom",
|
||||||
|
offset: 20
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
yAxis: [{
|
||||||
|
type: "value",
|
||||||
|
axisTick: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
axisLine: {
|
||||||
|
lineStyle: {
|
||||||
|
color: "rgba(255,255,255,.1)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
axisLabel: {
|
||||||
|
textStyle: {
|
||||||
|
color: "rgba(255,255,255,.6)",
|
||||||
|
fontSize: 12
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
splitLine: {
|
||||||
|
lineStyle: {
|
||||||
|
color: "rgba(255,255,255,.1)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
series: [{
|
||||||
|
name: "通过率",
|
||||||
|
type: "line",
|
||||||
|
smooth: true,
|
||||||
|
symbol: "circle",
|
||||||
|
symbolSize: 5,
|
||||||
|
showSymbol: true,
|
||||||
|
lineStyle: {
|
||||||
|
normal: {
|
||||||
|
color: "#0184d5",
|
||||||
|
width: 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
areaStyle: {
|
||||||
|
normal: {
|
||||||
|
color: new echarts.graphic.LinearGradient(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
[{
|
||||||
|
offset: 0,
|
||||||
|
color: "rgba(1, 132, 213, 0.4)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
offset: 0.8,
|
||||||
|
color: "rgba(1, 132, 213, 0.1)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
false
|
||||||
|
),
|
||||||
|
shadowColor: "rgba(0, 0, 0, 0.1)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
itemStyle: {
|
||||||
|
normal: {
|
||||||
|
color: "#0184d5",
|
||||||
|
borderColor: "rgba(221, 220, 107, .1)",
|
||||||
|
borderWidth: 18
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data: passRate
|
||||||
|
},
|
||||||
|
]
|
||||||
|
};
|
||||||
|
// 重新把配置好的新数据给实例对象
|
||||||
|
myChart.setOption(option);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
char02();
|
||||||
|
|
||||||
|
function char03() {
|
||||||
|
// 基于准备好的dom,初始化echarts实例
|
||||||
|
let myChart = echarts.init(document.getElementById("char3"));
|
||||||
|
var data = [resulte.success, resulte.fail, resulte.error, resulte.skip];
|
||||||
|
var titlename = ["通过用例", "失败用例", "错误用例", "跳过用例",];
|
||||||
|
var valdata = [resulte.all, resulte.all, resulte.all, resulte.all];
|
||||||
|
var myColor = ['#28a745', '#ffc107', '#dc3545', '#17a2b8'];
|
||||||
|
option = {
|
||||||
|
//图标位置
|
||||||
|
grid: {
|
||||||
|
top: "10%",
|
||||||
|
left: "22%",
|
||||||
|
bottom: "10%"
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
yAxis: [{
|
||||||
|
show: true,
|
||||||
|
data: titlename,
|
||||||
|
inverse: true,
|
||||||
|
axisLine: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
splitLine: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
axisTick: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
axisLabel: {
|
||||||
|
color: "#fff",
|
||||||
|
|
||||||
|
rich: {
|
||||||
|
lg: {
|
||||||
|
backgroundColor: "#339911",
|
||||||
|
color: "#fff",
|
||||||
|
borderRadius: 15,
|
||||||
|
align: "center",
|
||||||
|
width: 15,
|
||||||
|
height: 15
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
show: false,
|
||||||
|
inverse: true,
|
||||||
|
data: valdata,
|
||||||
|
axisLabel: {
|
||||||
|
textStyle: {
|
||||||
|
fontSize: 12,
|
||||||
|
color: "#fff"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
axisTick: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
axisLine: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
series: [{
|
||||||
|
type: "bar",
|
||||||
|
yAxisIndex: 0,
|
||||||
|
data: data,
|
||||||
|
barCategoryGap: 50,
|
||||||
|
barWidth: 18,
|
||||||
|
itemStyle: {
|
||||||
|
normal: {
|
||||||
|
barBorderRadius: 20,
|
||||||
|
color: function (params) {
|
||||||
|
var num = myColor.length;
|
||||||
|
return myColor[params.dataIndex % num];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
normal: {
|
||||||
|
show: true,
|
||||||
|
position: "right",
|
||||||
|
formatter: "{c}条",
|
||||||
|
color:"#fff",
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "bar",
|
||||||
|
yAxisIndex: 1,
|
||||||
|
barCategoryGap: 50,
|
||||||
|
data: valdata,
|
||||||
|
barWidth: 20,
|
||||||
|
itemStyle: {
|
||||||
|
normal: {
|
||||||
|
color: "none",
|
||||||
|
borderColor: "#00c1de",
|
||||||
|
borderWidth: 2,
|
||||||
|
barBorderRadius: 15
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
// 使用刚指定的配置项和数据显示图表。
|
||||||
|
myChart.setOption(option);
|
||||||
|
window.addEventListener("resize", function () {
|
||||||
|
myChart.resize();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
char03()
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
var tbodyTr = $('tbody .case_');
|
||||||
|
var testResult = $("#testResult");
|
||||||
|
var testClass = $("#testClass");
|
||||||
|
<!-- 用例执行详细信息显示切换-->
|
||||||
|
$(".btn_info").click(function () {
|
||||||
|
$(this).parent().parent().next().toggle();
|
||||||
|
|
||||||
|
});
|
||||||
|
// 当选择用例类之后触发
|
||||||
|
testClass.change(function () {
|
||||||
|
$('.test_log').hide();
|
||||||
|
var cls = $(this).val();
|
||||||
|
var res = testResult.val();
|
||||||
|
elementDisplay(cls, res);
|
||||||
|
sort()
|
||||||
|
});
|
||||||
|
testResult.change(function () {
|
||||||
|
var res = $(this).val();
|
||||||
|
var cls = testClass.val();
|
||||||
|
elementDisplay(cls, res);
|
||||||
|
sort()
|
||||||
|
});
|
||||||
|
|
||||||
|
function elementDisplay(cls, res) {
|
||||||
|
// 用例数据的显示
|
||||||
|
if (cls === "所有") {
|
||||||
|
if (res === "所有") {
|
||||||
|
tbodyTr.has('button').show();
|
||||||
|
} else if (res === '成功') {
|
||||||
|
tbodyTr.hide();
|
||||||
|
tbodyTr.has('button').has('.text-success').show()
|
||||||
|
|
||||||
|
} else if (res === '错误') {
|
||||||
|
tbodyTr.hide();
|
||||||
|
tbodyTr.has('button').has('.text-danger').show()
|
||||||
|
|
||||||
|
} else if (res === '失败') {
|
||||||
|
tbodyTr.hide();
|
||||||
|
tbodyTr.has('button').has('.text-warning').show()
|
||||||
|
|
||||||
|
} else if (res === '跳过') {
|
||||||
|
tbodyTr.hide();
|
||||||
|
tbodyTr.has('button').has('.text-info').show()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (res === "所有") {
|
||||||
|
tbodyTr.hide();
|
||||||
|
tbodyTr.has('button').has('.' + cls + '').show()
|
||||||
|
} else if (res === '成功') {
|
||||||
|
tbodyTr.hide();
|
||||||
|
tbodyTr.has('button').has('.' + cls + '').has('.text-success').show()
|
||||||
|
} else if (res === '错误') {
|
||||||
|
tbodyTr.hide();
|
||||||
|
tbodyTr.has('button').has('.' + cls + '').has('.text-danger').show()
|
||||||
|
} else if (res === '失败') {
|
||||||
|
tbodyTr.hide();
|
||||||
|
tbodyTr.has('button').has('.' + cls + '').has('.text-warning').show()
|
||||||
|
} else if (res === '跳过') {
|
||||||
|
tbodyTr.hide();
|
||||||
|
tbodyTr.has('button').has('.' + cls + '').has('.text-info').show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sort() {
|
||||||
|
//重新排列显示序号
|
||||||
|
// 选择所有可以见的tr
|
||||||
|
var visibleTr = tbodyTr.filter(":visible");
|
||||||
|
|
||||||
|
visibleTr.each(function (index, element) {
|
||||||
|
element.firstElementChild.innerHTML = index + 1;
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
$('.nav-tabs li').click(function () {
|
||||||
|
$(this).find('a').addClass('active');
|
||||||
|
$(this).siblings().find('a').removeClass('active');
|
||||||
|
$(this).parent().next().children('.tab-content div').eq($(this).index()).addClass('active show').siblings().removeClass('active show')
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,20 @@
|
||||||
|
# **提醒!自动化测试反馈**\n#### **请相关同事注意,及时跟进!**
|
||||||
|
|
||||||
|
> 项目名称:<font color='info'>{{title}}</font>
|
||||||
|
> 测试人员:<font color='info'>{{tester}}</font>
|
||||||
|
> 开始时间:<font color='info'>{{begin_time}}</font>
|
||||||
|
> 运行时间:<font color='info'>{{runtime}}</font>
|
||||||
|
> 测试用例总数:<font color='info'>{{all}}</font>
|
||||||
|
> 测试用例通过率:<font color='info'>{pass_rate}%</font>
|
||||||
|
> 成功数: <font color='info'>{{success}}</font>
|
||||||
|
> 失败数:<font color='warning'>{{fail}}</font>
|
||||||
|
> 跳过数:<font color='info'>{{skip}}, </font>
|
||||||
|
> 错误数:<font color='comment'>{{error}}</font>
|
||||||
|
> **--------------------上一次运行结果--------------------**
|
||||||
|
> 测试用例总数: <font color='info'>{{ history[-1]['all'] }}</font>
|
||||||
|
> 测试用例通过率:<font color='info'>{{history[-1]['pass_rate']}}%</font>
|
||||||
|
> 成功数:<font color='info'>{{history[-1]['success']}}</font>
|
||||||
|
> 失败数:<font color='warning'>{{history[-1]['fail']}}</font>
|
||||||
|
> 跳过数:<font color='info'>{{history[-1]['skip']}}</font>
|
||||||
|
> 错误数:<font color='comment'>{{history[-1]['error']}}</font>
|
||||||
|
> ##### **报告链接:** [jenkins报告,请点击后进入查看](report_url)
|
|
@ -9,12 +9,13 @@
|
||||||
"""
|
"""
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from ddt import ddt, data
|
|
||||||
|
|
||||||
import extensions
|
import extensions
|
||||||
from common.file_handling.do_excel import DoExcel
|
from common.file_handling.do_excel import DoExcel
|
||||||
from common.utils.action import Action
|
from common.utils.action import Action
|
||||||
from config import Config
|
from config import Config
|
||||||
|
from unittestreportnew import list_data
|
||||||
|
# from ddt import ddt, data
|
||||||
|
from unittestreportnew.core.dataDriver import ddt
|
||||||
|
|
||||||
test_file = Config.test_case # 获取 excel 文件路径
|
test_file = Config.test_case # 获取 excel 文件路径
|
||||||
excel = DoExcel(test_file)
|
excel = DoExcel(test_file)
|
||||||
|
@ -34,7 +35,8 @@ class TestProjectApi(unittest.TestCase):
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@data(*test_case)
|
# @data(*test_case)
|
||||||
|
@list_data(test_case)
|
||||||
def test_api(self, item):
|
def test_api(self, item):
|
||||||
sheet, iid, condition, st, name, desc, h_crypto, r_crypto, method, expected = self.action.base_info(item)
|
sheet, iid, condition, st, name, desc, h_crypto, r_crypto, method, expected = self.action.base_info(item)
|
||||||
if self.action.is_run(condition):
|
if self.action.is_run(condition):
|
||||||
|
@ -62,7 +64,6 @@ class TestProjectApi(unittest.TestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def tearDownClass(cls) -> None:
|
def tearDownClass(cls) -> None:
|
||||||
excel.close_excel()
|
excel.close_excel()
|
||||||
cls.action.logger.info(f"所有用例执行完毕")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
|
||||||
|
"""
|
||||||
|
unittestreport基于unittest扩展了5个功能:
|
||||||
|
1、html测试报告的生成(三个风格)
|
||||||
|
2、测试用例失败重运行
|
||||||
|
3、测试报告邮件发送功能
|
||||||
|
4、数据驱动
|
||||||
|
5、多线程执行测试用例
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .core.testRunner import TestRunner,Load
|
||||||
|
from common.utils.decorators import list_data,json_data,yaml_data
|
||||||
|
from common.utils.decorators import rerun,run_count
|
||||||
|
# from .core.reRun import rerun
|
|
@ -0,0 +1,65 @@
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
|
|
||||||
|
def _create_test_name(index, name, title):
|
||||||
|
"""
|
||||||
|
Create a new test name based on index and name.
|
||||||
|
:param index: Index for generating the test name.
|
||||||
|
:param name: Base name for the test.
|
||||||
|
:param title: Base title for the test.
|
||||||
|
:return: Generated test name.
|
||||||
|
"""
|
||||||
|
test_name = f"{name}_{index + 1:03}_{title}"
|
||||||
|
return test_name
|
||||||
|
|
||||||
|
|
||||||
|
def _set_function_attributes(func, original_func, new_name, test_desc):
|
||||||
|
"""
|
||||||
|
Set attributes of a function.
|
||||||
|
:param func: The function to set attributes for.
|
||||||
|
:param original_func: The original function being wrapped.
|
||||||
|
:param new_name: New name for the function.
|
||||||
|
:param test_desc: New documentation for the function.
|
||||||
|
"""
|
||||||
|
func.__wrapped__ = original_func
|
||||||
|
func.__name__ = new_name
|
||||||
|
func.__doc__ = test_desc
|
||||||
|
|
||||||
|
|
||||||
|
def _update_func(new_func_name, params, test_desc, func, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Create a wrapper function with updated attributes.
|
||||||
|
:param new_func_name: New name for the wrapper function.
|
||||||
|
:param params: Test parameters.
|
||||||
|
:param test_desc: Test description.
|
||||||
|
:param func: Original function to be wrapped.
|
||||||
|
:param args: Additional positional arguments for the function.
|
||||||
|
:param kwargs: Additional keyword arguments for the function.
|
||||||
|
:return: Wrapped function.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@wraps(func)
|
||||||
|
def wrapper(self):
|
||||||
|
return func(self, params, *args, **kwargs)
|
||||||
|
|
||||||
|
_set_function_attributes(wrapper, func, new_func_name, test_desc)
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
def ddt(cls):
|
||||||
|
"""
|
||||||
|
:param cls: 测试类
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
for func_name, func in list(cls.__dict__.items()):
|
||||||
|
if hasattr(func, "PARAMS"):
|
||||||
|
for index, case_data in enumerate(getattr(func, "PARAMS")):
|
||||||
|
name = str(case_data.get("Name", "缺少Name"))
|
||||||
|
test_desc = str(case_data.get("Description", "缺少Description"))
|
||||||
|
new_test_name = _create_test_name(index, func_name, name)
|
||||||
|
func2 = _update_func(new_test_name, case_data, test_desc, func)
|
||||||
|
setattr(cls, new_test_name, func2)
|
||||||
|
else:
|
||||||
|
# Avoid name clashes
|
||||||
|
delattr(cls, func_name)
|
||||||
|
return cls
|
|
@ -0,0 +1,635 @@
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import inspect
|
||||||
|
import warnings
|
||||||
|
from functools import wraps
|
||||||
|
from types import MethodType as MethodType
|
||||||
|
from collections import namedtuple
|
||||||
|
|
||||||
|
try:
|
||||||
|
from collections import OrderedDict as MaybeOrderedDict
|
||||||
|
except ImportError:
|
||||||
|
MaybeOrderedDict = dict
|
||||||
|
|
||||||
|
from unittest import TestCase
|
||||||
|
|
||||||
|
try:
|
||||||
|
from unittest import SkipTest
|
||||||
|
except ImportError:
|
||||||
|
class SkipTest(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
PY3 = sys.version_info[0] == 3
|
||||||
|
PY2 = sys.version_info[0] == 2
|
||||||
|
|
||||||
|
if PY3:
|
||||||
|
class InstanceType():
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
lzip = lambda *a: list(zip(*a))
|
||||||
|
text_type = str
|
||||||
|
string_types = str,
|
||||||
|
bytes_type = bytes
|
||||||
|
|
||||||
|
|
||||||
|
def make_method(func, instance, type):
|
||||||
|
if instance is None:
|
||||||
|
return func
|
||||||
|
return MethodType(func, instance)
|
||||||
|
|
||||||
|
CompatArgSpec = namedtuple("CompatArgSpec", "args varargs keywords defaults")
|
||||||
|
|
||||||
|
|
||||||
|
def getargspec(func):
|
||||||
|
if PY2:
|
||||||
|
return CompatArgSpec(*inspect.getargspec(func))
|
||||||
|
args = inspect.getfullargspec(func)
|
||||||
|
if args.kwonlyargs:
|
||||||
|
raise TypeError((
|
||||||
|
"parameterized does not (yet) support functions with keyword "
|
||||||
|
"only arguments, but %r has keyword only arguments. "
|
||||||
|
"Please open an issue with your usecase if this affects you: "
|
||||||
|
"https://github.com/wolever/parameterized/issues/new"
|
||||||
|
) % (func,))
|
||||||
|
return CompatArgSpec(*args[:4])
|
||||||
|
|
||||||
|
|
||||||
|
def skip_on_empty_helper(*a, **kw):
|
||||||
|
raise SkipTest("parameterized input is empty")
|
||||||
|
|
||||||
|
|
||||||
|
def reapply_patches_if_need(func):
|
||||||
|
def dummy_wrapper(orgfunc):
|
||||||
|
@wraps(orgfunc)
|
||||||
|
def dummy_func(*args, **kwargs):
|
||||||
|
return orgfunc(*args, **kwargs)
|
||||||
|
|
||||||
|
return dummy_func
|
||||||
|
|
||||||
|
if hasattr(func, 'patchings'):
|
||||||
|
func = dummy_wrapper(func)
|
||||||
|
tmp_patchings = func.patchings
|
||||||
|
delattr(func, 'patchings')
|
||||||
|
for patch_obj in tmp_patchings:
|
||||||
|
func = patch_obj.decorate_callable(func)
|
||||||
|
return func
|
||||||
|
|
||||||
|
|
||||||
|
def delete_patches_if_need(func):
|
||||||
|
if hasattr(func, 'patchings'):
|
||||||
|
func.patchings[:] = []
|
||||||
|
|
||||||
|
|
||||||
|
_param = namedtuple("param", "args kwargs")
|
||||||
|
|
||||||
|
|
||||||
|
class param(_param):
|
||||||
|
""" Represents a single parameter to a test case.
|
||||||
|
|
||||||
|
For example::
|
||||||
|
|
||||||
|
>>> p = param("foo", bar=16)
|
||||||
|
>>> p
|
||||||
|
param("foo", bar=16)
|
||||||
|
>>> p.args
|
||||||
|
('foo', )
|
||||||
|
>>> p.kwargs
|
||||||
|
{'bar': 16}
|
||||||
|
|
||||||
|
Intended to be used as an argument to ``@parameterized``::
|
||||||
|
|
||||||
|
@parameterized([
|
||||||
|
param("foo", bar=16),
|
||||||
|
])
|
||||||
|
def test_stuff(foo, bar=16):
|
||||||
|
pass
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __new__(cls, *args, **kwargs):
|
||||||
|
return _param.__new__(cls, args, kwargs)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def explicit(cls, args=None, kwargs=None):
|
||||||
|
""" Creates a ``param`` by explicitly specifying ``args`` and
|
||||||
|
``kwargs``::
|
||||||
|
|
||||||
|
>>> param.explicit([1,2,3])
|
||||||
|
param(*(1, 2, 3))
|
||||||
|
>>> param.explicit(kwargs={"foo": 42})
|
||||||
|
param(*(), **{"foo": "42"})
|
||||||
|
"""
|
||||||
|
args = args or ()
|
||||||
|
kwargs = kwargs or {}
|
||||||
|
return cls(*args, **kwargs)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_decorator(cls, args):
|
||||||
|
""" Returns an instance of ``param()`` for ``@parameterized`` argument
|
||||||
|
``args``::
|
||||||
|
|
||||||
|
>>> param.from_decorator((42, ))
|
||||||
|
param(args=(42, ), kwargs={})
|
||||||
|
>>> param.from_decorator("foo")
|
||||||
|
param(args=("foo", ), kwargs={})
|
||||||
|
"""
|
||||||
|
if isinstance(args, param):
|
||||||
|
return args
|
||||||
|
elif isinstance(args, string_types):
|
||||||
|
args = (args,)
|
||||||
|
try:
|
||||||
|
return cls(*args)
|
||||||
|
except TypeError as e:
|
||||||
|
if "after * must be" not in str(e):
|
||||||
|
raise
|
||||||
|
raise TypeError(
|
||||||
|
"Parameters must be tuples, but %r is not (hint: use '(%r, )')"
|
||||||
|
% (args, args),
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "param(*%r, **%r)" % self
|
||||||
|
|
||||||
|
|
||||||
|
class QuietOrderedDict(MaybeOrderedDict):
|
||||||
|
""" When OrderedDict is available, use it to make sure that the kwargs in
|
||||||
|
doc strings are consistently ordered. """
|
||||||
|
__str__ = dict.__str__
|
||||||
|
__repr__ = dict.__repr__
|
||||||
|
|
||||||
|
|
||||||
|
def parameterized_argument_value_pairs(func, p):
|
||||||
|
"""Return tuples of parameterized arguments and their values.
|
||||||
|
|
||||||
|
This is useful if you are writing your own doc_func
|
||||||
|
function and need to know the values for each parameter name::
|
||||||
|
|
||||||
|
>>> def func(a, foo=None, bar=42, **kwargs): pass
|
||||||
|
>>> p = param(1, foo=7, extra=99)
|
||||||
|
>>> parameterized_argument_value_pairs(func, p)
|
||||||
|
[("a", 1), ("foo", 7), ("bar", 42), ("**kwargs", {"extra": 99})]
|
||||||
|
|
||||||
|
If the function's first argument is named ``self`` then it will be
|
||||||
|
ignored::
|
||||||
|
|
||||||
|
>>> def func(self, a): pass
|
||||||
|
>>> p = param(1)
|
||||||
|
>>> parameterized_argument_value_pairs(func, p)
|
||||||
|
[("a", 1)]
|
||||||
|
|
||||||
|
Additionally, empty ``*args`` or ``**kwargs`` will be ignored::
|
||||||
|
|
||||||
|
>>> def func(foo, *args): pass
|
||||||
|
>>> p = param(1)
|
||||||
|
>>> parameterized_argument_value_pairs(func, p)
|
||||||
|
[("foo", 1)]
|
||||||
|
>>> p = param(1, 16)
|
||||||
|
>>> parameterized_argument_value_pairs(func, p)
|
||||||
|
[("foo", 1), ("*args", (16, ))]
|
||||||
|
"""
|
||||||
|
argspec = getargspec(func)
|
||||||
|
arg_offset = 1 if argspec.args[:1] == ["self"] else 0
|
||||||
|
|
||||||
|
named_args = argspec.args[arg_offset:]
|
||||||
|
|
||||||
|
result = lzip(named_args, p.args)
|
||||||
|
named_args = argspec.args[len(result) + arg_offset:]
|
||||||
|
varargs = p.args[len(result):]
|
||||||
|
|
||||||
|
result.extend([
|
||||||
|
(name, p.kwargs.get(name, default))
|
||||||
|
for (name, default)
|
||||||
|
in zip(named_args, argspec.defaults or [])
|
||||||
|
])
|
||||||
|
|
||||||
|
seen_arg_names = set([n for (n, _) in result])
|
||||||
|
keywords = QuietOrderedDict(sorted([
|
||||||
|
(name, p.kwargs[name])
|
||||||
|
for name in p.kwargs
|
||||||
|
if name not in seen_arg_names
|
||||||
|
]))
|
||||||
|
|
||||||
|
if varargs:
|
||||||
|
result.append(("*%s" % (argspec.varargs,), tuple(varargs)))
|
||||||
|
|
||||||
|
if keywords:
|
||||||
|
result.append(("**%s" % (argspec.keywords,), keywords))
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def short_repr(x, n=64):
|
||||||
|
""" A shortened repr of ``x`` which is guaranteed to be ``unicode``::
|
||||||
|
|
||||||
|
>>> short_repr("foo")
|
||||||
|
u"foo"
|
||||||
|
>>> short_repr("123456789", n=4)
|
||||||
|
u"12...89"
|
||||||
|
"""
|
||||||
|
|
||||||
|
x_repr = repr(x)
|
||||||
|
if isinstance(x_repr, bytes_type):
|
||||||
|
try:
|
||||||
|
x_repr = text_type(x_repr, "utf-8")
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
x_repr = text_type(x_repr, "latin1")
|
||||||
|
if len(x_repr) > n:
|
||||||
|
x_repr = x_repr[:n // 2] + "..." + x_repr[len(x_repr) - n // 2:]
|
||||||
|
return x_repr
|
||||||
|
|
||||||
|
|
||||||
|
def default_doc_func(func, num, p):
|
||||||
|
if func.__doc__ is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
all_args_with_values = parameterized_argument_value_pairs(func, p)
|
||||||
|
|
||||||
|
# Assumes that the function passed is a bound method.
|
||||||
|
descs = ["%s=%s" % (n, short_repr(v)) for n, v in all_args_with_values]
|
||||||
|
|
||||||
|
# The documentation might be a multiline string, so split it
|
||||||
|
# and just work with the first string, ignoring the period
|
||||||
|
# at the end if there is one.
|
||||||
|
first, nl, rest = func.__doc__.lstrip().partition("\n")
|
||||||
|
suffix = ""
|
||||||
|
if first.endswith("."):
|
||||||
|
suffix = "."
|
||||||
|
first = first[:-1]
|
||||||
|
args = "%s[with %s]" % (len(first) and " " or "", ", ".join(descs))
|
||||||
|
return "".join([first.rstrip(), args, suffix, nl, rest])
|
||||||
|
|
||||||
|
|
||||||
|
def default_name_func(func, num, p):
|
||||||
|
base_name = func.__name__
|
||||||
|
name_suffix = "_%s" % (num,)
|
||||||
|
|
||||||
|
if len(p.args) > 0 and isinstance(p.args[0], string_types):
|
||||||
|
name_suffix += "_" + parameterized.to_safe_name(p.args[0])
|
||||||
|
return base_name + name_suffix
|
||||||
|
|
||||||
|
|
||||||
|
_test_runner_override = None
|
||||||
|
_test_runner_guess = False
|
||||||
|
_test_runners = set(["unittest", "unittest2", "nose", "nose2", "pytest"])
|
||||||
|
_test_runner_aliases = {
|
||||||
|
"_pytest": "pytest",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def set_test_runner(name):
|
||||||
|
global _test_runner_override
|
||||||
|
if name not in _test_runners:
|
||||||
|
raise TypeError(
|
||||||
|
"Invalid test runner: %r (must be one of: %s)"
|
||||||
|
% (name, ", ".join(_test_runners)),
|
||||||
|
)
|
||||||
|
_test_runner_override = name
|
||||||
|
|
||||||
|
|
||||||
|
def detect_runner():
|
||||||
|
""" Guess which test runner we're using by traversing the stack and looking
|
||||||
|
for the first matching module. This *should* be reasonably safe, as
|
||||||
|
it's done during test disocvery where the test runner should be the
|
||||||
|
stack frame immediately outside. """
|
||||||
|
if _test_runner_override is not None:
|
||||||
|
return _test_runner_override
|
||||||
|
global _test_runner_guess
|
||||||
|
if _test_runner_guess is False:
|
||||||
|
stack = inspect.stack()
|
||||||
|
for record in reversed(stack):
|
||||||
|
frame = record[0]
|
||||||
|
module = frame.f_globals.get("__name__").partition(".")[0]
|
||||||
|
if module in _test_runner_aliases:
|
||||||
|
module = _test_runner_aliases[module]
|
||||||
|
if module in _test_runners:
|
||||||
|
_test_runner_guess = module
|
||||||
|
break
|
||||||
|
if record[1].endswith("python2.6/unittest.py"):
|
||||||
|
_test_runner_guess = "unittest"
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
_test_runner_guess = None
|
||||||
|
return _test_runner_guess
|
||||||
|
|
||||||
|
|
||||||
|
class parameterized(object):
|
||||||
|
""" Parameterize a test case::
|
||||||
|
|
||||||
|
class TestInt(object):
|
||||||
|
@parameterized([
|
||||||
|
("A", 10),
|
||||||
|
("F", 15),
|
||||||
|
param("10", 42, base=42)
|
||||||
|
])
|
||||||
|
def test_int(self, input, expected, base=16):
|
||||||
|
actual = int(input, base=base)
|
||||||
|
assert_equal(actual, expected)
|
||||||
|
|
||||||
|
@parameterized([
|
||||||
|
(2, 3, 5)
|
||||||
|
(3, 5, 8),
|
||||||
|
])
|
||||||
|
def test_add(a, b, expected):
|
||||||
|
assert_equal(a + b, expected)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, input, doc_func=None, skip_on_empty=False):
|
||||||
|
self.get_input = self.input_as_callable(input)
|
||||||
|
self.doc_func = doc_func or default_doc_func
|
||||||
|
self.skip_on_empty = skip_on_empty
|
||||||
|
|
||||||
|
def __call__(self, test_func):
|
||||||
|
self.assert_not_in_testcase_subclass()
|
||||||
|
|
||||||
|
@wraps(test_func)
|
||||||
|
def wrapper(test_self=None):
|
||||||
|
test_cls = test_self and type(test_self)
|
||||||
|
if test_self is not None:
|
||||||
|
if issubclass(test_cls, InstanceType):
|
||||||
|
raise TypeError((
|
||||||
|
"@parameterized can't be used with old-style classes, but "
|
||||||
|
"%r has an old-style class. Consider using a new-style "
|
||||||
|
"class, or '@parameterized.expand' "
|
||||||
|
"(see http://stackoverflow.com/q/54867/71522 for more "
|
||||||
|
"information on old-style classes)."
|
||||||
|
) % (test_self,))
|
||||||
|
|
||||||
|
original_doc = wrapper.__doc__
|
||||||
|
for num, args in enumerate(wrapper.parameterized_input):
|
||||||
|
p = param.from_decorator(args)
|
||||||
|
unbound_func, nose_tuple = self.param_as_nose_tuple(test_self, test_func, num, p)
|
||||||
|
try:
|
||||||
|
wrapper.__doc__ = nose_tuple[0].__doc__
|
||||||
|
# Nose uses `getattr(instance, test_func.__name__)` to get
|
||||||
|
# a method bound to the test instance (as opposed to a
|
||||||
|
# method bound to the instance of the class created when
|
||||||
|
# tests were being enumerated). Set a value here to make
|
||||||
|
# sure nose can get the correct test method.
|
||||||
|
if test_self is not None:
|
||||||
|
setattr(test_cls, test_func.__name__, unbound_func)
|
||||||
|
yield nose_tuple
|
||||||
|
finally:
|
||||||
|
if test_self is not None:
|
||||||
|
delattr(test_cls, test_func.__name__)
|
||||||
|
wrapper.__doc__ = original_doc
|
||||||
|
|
||||||
|
input = self.get_input()
|
||||||
|
if not input:
|
||||||
|
if not self.skip_on_empty:
|
||||||
|
raise ValueError(
|
||||||
|
"Parameters iterable is empty (hint: use "
|
||||||
|
"`parameterized([], skip_on_empty=True)` to skip "
|
||||||
|
"this test when the input is empty)"
|
||||||
|
)
|
||||||
|
wrapper = wraps(test_func)(skip_on_empty_helper)
|
||||||
|
|
||||||
|
wrapper.parameterized_input = input
|
||||||
|
wrapper.parameterized_func = test_func
|
||||||
|
test_func.__name__ = "_parameterized_original_%s" % (test_func.__name__,)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
def param_as_nose_tuple(self, test_self, func, num, p):
|
||||||
|
nose_func = wraps(func)(lambda *args: func(*args[:-1], **args[-1]))
|
||||||
|
nose_func.__doc__ = self.doc_func(func, num, p)
|
||||||
|
# Track the unbound function because we need to setattr the unbound
|
||||||
|
# function onto the class for nose to work (see comments above), and
|
||||||
|
# Python 3 doesn't let us pull the function out of a bound method.
|
||||||
|
unbound_func = nose_func
|
||||||
|
if test_self is not None:
|
||||||
|
# Under nose on Py2 we need to return an unbound method to make
|
||||||
|
# sure that the `self` in the method is properly shared with the
|
||||||
|
# `self` used in `setUp` and `tearDown`. But only there. Everyone
|
||||||
|
# else needs a bound method.
|
||||||
|
func_self = (
|
||||||
|
None if PY2 and detect_runner() == "nose" else
|
||||||
|
test_self
|
||||||
|
)
|
||||||
|
nose_func = make_method(nose_func, func_self, type(test_self))
|
||||||
|
return unbound_func, (nose_func,) + p.args + (p.kwargs or {},)
|
||||||
|
|
||||||
|
def assert_not_in_testcase_subclass(self):
|
||||||
|
parent_classes = self._terrible_magic_get_defining_classes()
|
||||||
|
if any(issubclass(cls, TestCase) for cls in parent_classes):
|
||||||
|
raise Exception("Warning: '@parameterized' tests won't work "
|
||||||
|
"inside subclasses of 'TestCase' - use "
|
||||||
|
"'@parameterized.expand' instead.")
|
||||||
|
|
||||||
|
def _terrible_magic_get_defining_classes(self):
|
||||||
|
""" Returns the set of parent classes of the class currently being defined.
|
||||||
|
Will likely only work if called from the ``parameterized`` decorator.
|
||||||
|
This function is entirely @brandon_rhodes's fault, as he suggested
|
||||||
|
the implementation: http://stackoverflow.com/a/8793684/71522
|
||||||
|
"""
|
||||||
|
stack = inspect.stack()
|
||||||
|
if len(stack) <= 4:
|
||||||
|
return []
|
||||||
|
frame = stack[4]
|
||||||
|
code_context = frame[4] and frame[4][0].strip()
|
||||||
|
if not (code_context and code_context.startswith("class ")):
|
||||||
|
return []
|
||||||
|
_, _, parents = code_context.partition("(")
|
||||||
|
parents, _, _ = parents.partition(")")
|
||||||
|
return eval("[" + parents + "]", frame[0].f_globals, frame[0].f_locals)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def input_as_callable(cls, input):
|
||||||
|
if callable(input):
|
||||||
|
return lambda: cls.check_input_values(input())
|
||||||
|
input_values = cls.check_input_values(input)
|
||||||
|
return lambda: input_values
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def check_input_values(cls, input_values):
|
||||||
|
# Explicitly convery non-list inputs to a list so that:
|
||||||
|
# 1. A helpful exception will be raised if they aren't iterable, and
|
||||||
|
# 2. Generators are unwrapped exactly once (otherwise `nosetests
|
||||||
|
# --processes=n` has issues; see:
|
||||||
|
# https://github.com/wolever/nose-parameterized/pull/31)
|
||||||
|
if not isinstance(input_values, list):
|
||||||
|
input_values = list(input_values)
|
||||||
|
return [param.from_decorator(p) for p in input_values]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def expand(cls, input, name_func=None, doc_func=None, skip_on_empty=False,
|
||||||
|
**legacy):
|
||||||
|
""" A "brute force" method of parameterizing test cases. Creates new
|
||||||
|
test cases and injects them into the namespace that the wrapped
|
||||||
|
function is being defined in. Useful for parameterizing tests in
|
||||||
|
subclasses of 'UnitTest', where Nose test generators don't work.
|
||||||
|
|
||||||
|
>>> @parameterized.expand([("foo", 1, 2)])
|
||||||
|
... def test_add1(name, input, expected):
|
||||||
|
... actual = add1(input)
|
||||||
|
... assert_equal(actual, expected)
|
||||||
|
...
|
||||||
|
>>> locals()
|
||||||
|
... 'test_add1_foo_0': <function ...> ...
|
||||||
|
>>>
|
||||||
|
"""
|
||||||
|
|
||||||
|
if "testcase_func_name" in legacy:
|
||||||
|
warnings.warn("testcase_func_name= is deprecated; use name_func=",
|
||||||
|
DeprecationWarning, stacklevel=2)
|
||||||
|
if not name_func:
|
||||||
|
name_func = legacy["testcase_func_name"]
|
||||||
|
|
||||||
|
if "testcase_func_doc" in legacy:
|
||||||
|
warnings.warn("testcase_func_doc= is deprecated; use doc_func=",
|
||||||
|
DeprecationWarning, stacklevel=2)
|
||||||
|
if not doc_func:
|
||||||
|
doc_func = legacy["testcase_func_doc"]
|
||||||
|
|
||||||
|
doc_func = doc_func or default_doc_func
|
||||||
|
name_func = name_func or default_name_func
|
||||||
|
|
||||||
|
def parameterized_expand_wrapper(f, instance=None):
|
||||||
|
stack = inspect.stack()
|
||||||
|
frame = stack[1]
|
||||||
|
frame_locals = frame[0].f_locals
|
||||||
|
|
||||||
|
parameters = cls.input_as_callable(input)()
|
||||||
|
|
||||||
|
if not parameters:
|
||||||
|
if not skip_on_empty:
|
||||||
|
raise ValueError(
|
||||||
|
"Parameters iterable is empty (hint: use "
|
||||||
|
"`parameterized.expand([], skip_on_empty=True)` to skip "
|
||||||
|
"this test when the input is empty)"
|
||||||
|
)
|
||||||
|
return wraps(f)(lambda: skip_on_empty_helper())
|
||||||
|
|
||||||
|
digits = len(str(len(parameters) - 1))
|
||||||
|
for num, p in enumerate(parameters):
|
||||||
|
name = name_func(f, "{num:0>{digits}}".format(digits=digits, num=num), p)
|
||||||
|
# If the original function has patches applied by 'mock.patch',
|
||||||
|
# re-construct all patches on the just former decoration layer
|
||||||
|
# of param_as_standalone_func so as not to share
|
||||||
|
# patch objects between new functions
|
||||||
|
nf = reapply_patches_if_need(f)
|
||||||
|
frame_locals[name] = cls.param_as_standalone_func(p, nf, name)
|
||||||
|
frame_locals[name].__doc__ = doc_func(f, num, p)
|
||||||
|
|
||||||
|
# Delete original patches to prevent new function from evaluating
|
||||||
|
# original patching object as well as re-constructed patches.
|
||||||
|
delete_patches_if_need(f)
|
||||||
|
|
||||||
|
f.__test__ = False
|
||||||
|
|
||||||
|
return parameterized_expand_wrapper
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def param_as_standalone_func(cls, p, func, name):
|
||||||
|
@wraps(func)
|
||||||
|
def standalone_func(*a):
|
||||||
|
return func(*(a + p.args), **p.kwargs)
|
||||||
|
|
||||||
|
standalone_func.__name__ = name
|
||||||
|
|
||||||
|
# place_as is used by py.test to determine what source file should be
|
||||||
|
# used for this test.
|
||||||
|
standalone_func.place_as = func
|
||||||
|
|
||||||
|
# Remove __wrapped__ because py.test will try to look at __wrapped__
|
||||||
|
# to determine which parameters should be used with this test case,
|
||||||
|
# and obviously we don't need it to do any parameterization.
|
||||||
|
try:
|
||||||
|
del standalone_func.__wrapped__
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
return standalone_func
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def to_safe_name(cls, s):
|
||||||
|
return str(re.sub("[^a-zA-Z0-9_]+", "_", s))
|
||||||
|
|
||||||
|
|
||||||
|
def parameterized_class(attrs, input_values=None, class_name_func=None, classname_func=None):
|
||||||
|
""" Parameterizes a test class by setting attributes on the class.
|
||||||
|
|
||||||
|
Can be used in two ways:
|
||||||
|
|
||||||
|
1) With a list of dictionaries containing attributes to override::
|
||||||
|
|
||||||
|
@parameterized_class([
|
||||||
|
{ "username": "foo" },
|
||||||
|
{ "username": "bar", "access_level": 2 },
|
||||||
|
])
|
||||||
|
class TestUserAccessLevel(TestCase):
|
||||||
|
...
|
||||||
|
|
||||||
|
2) With a tuple of attributes, then a list of tuples of values:
|
||||||
|
|
||||||
|
@parameterized_class(("username", "access_level"), [
|
||||||
|
("foo", 1),
|
||||||
|
("bar", 2)
|
||||||
|
])
|
||||||
|
class TestUserAccessLevel(TestCase):
|
||||||
|
...
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
if isinstance(attrs, string_types):
|
||||||
|
attrs = [attrs]
|
||||||
|
|
||||||
|
input_dicts = (
|
||||||
|
attrs if input_values is None else
|
||||||
|
[dict(zip(attrs, vals)) for vals in input_values]
|
||||||
|
)
|
||||||
|
|
||||||
|
class_name_func = class_name_func or default_class_name_func
|
||||||
|
|
||||||
|
if classname_func:
|
||||||
|
warnings.warn(
|
||||||
|
"classname_func= is deprecated; use class_name_func= instead. "
|
||||||
|
"See: https://github.com/wolever/parameterized/pull/74#issuecomment-613577057",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
class_name_func = lambda cls, idx, input: classname_func(cls, idx, input_dicts)
|
||||||
|
|
||||||
|
def decorator(base_class):
|
||||||
|
test_class_module = sys.modules[base_class.__module__].__dict__
|
||||||
|
for idx, input_dict in enumerate(input_dicts):
|
||||||
|
test_class_dict = dict(base_class.__dict__)
|
||||||
|
test_class_dict.update(input_dict)
|
||||||
|
|
||||||
|
name = class_name_func(base_class, idx, input_dict)
|
||||||
|
|
||||||
|
test_class_module[name] = type(name, (base_class,), test_class_dict)
|
||||||
|
|
||||||
|
# We need to leave the base class in place (see issue #73), but if we
|
||||||
|
# leave the test_ methods in place, the test runner will try to pick
|
||||||
|
# them up and run them... which doesn't make sense, since no parameters
|
||||||
|
# will have been applied.
|
||||||
|
# Address this by iterating over the base class and remove all test
|
||||||
|
# methods.
|
||||||
|
for method_name in list(base_class.__dict__):
|
||||||
|
if method_name.startswith("test_"):
|
||||||
|
delattr(base_class, method_name)
|
||||||
|
return base_class
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def get_class_name_suffix(params_dict):
|
||||||
|
if "name" in params_dict:
|
||||||
|
return parameterized.to_safe_name(params_dict["name"])
|
||||||
|
|
||||||
|
params_vals = (
|
||||||
|
params_dict.values() if PY3 else
|
||||||
|
(v for (_, v) in sorted(params_dict.items()))
|
||||||
|
)
|
||||||
|
return parameterized.to_safe_name(next((
|
||||||
|
v for v in params_vals
|
||||||
|
if isinstance(v, string_types)
|
||||||
|
), ""))
|
||||||
|
|
||||||
|
|
||||||
|
def default_class_name_func(cls, num, params_dict):
|
||||||
|
suffix = get_class_name_suffix(params_dict)
|
||||||
|
return "%s_%s%s" % (
|
||||||
|
cls.__name__,
|
||||||
|
num,
|
||||||
|
suffix and "_" + suffix,
|
||||||
|
)
|
|
@ -0,0 +1,20 @@
|
||||||
|
import base64
|
||||||
|
import hashlib
|
||||||
|
import hmac
|
||||||
|
import smtplib
|
||||||
|
import time
|
||||||
|
import urllib.parse
|
||||||
|
from email.header import Header
|
||||||
|
from email.mime.multipart import MIMEMultipart
|
||||||
|
from email.mime.text import MIMEText
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from config import Config
|
||||||
|
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# if __name__ == '__main__':
|
||||||
|
# # r'D:\app\apitest\cases\cases\test_cases.xlsx',r"D:\app\apitest\OutPut\reports\report.html"
|
||||||
|
# sm = SendEmail()
|
||||||
|
# sm.send_mail("111", [])
|
|
@ -0,0 +1,188 @@
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import traceback
|
||||||
|
import unittest
|
||||||
|
from io import StringIO
|
||||||
|
|
||||||
|
origin_stdout = sys.stdout
|
||||||
|
|
||||||
|
|
||||||
|
def output2console(s):
|
||||||
|
"""Output stdout content to console"""
|
||||||
|
tmp_stdout = sys.stdout
|
||||||
|
sys.stdout = origin_stdout
|
||||||
|
print(s, end='')
|
||||||
|
sys.stdout = tmp_stdout
|
||||||
|
|
||||||
|
|
||||||
|
class OutputRedirector(object):
|
||||||
|
""" Wrapper to redirect stdout or stderr """
|
||||||
|
|
||||||
|
def __init__(self, fp):
|
||||||
|
self.fp = fp
|
||||||
|
|
||||||
|
def write(self, s):
|
||||||
|
self.fp.write(s)
|
||||||
|
origin_stdout.write(str(s))
|
||||||
|
|
||||||
|
def writelines(self, lines):
|
||||||
|
self.fp.writelines(lines)
|
||||||
|
|
||||||
|
def flush(self):
|
||||||
|
self.fp.flush()
|
||||||
|
|
||||||
|
|
||||||
|
stdout_redirector = OutputRedirector(sys.stdout)
|
||||||
|
stderr_redirector = OutputRedirector(sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
class TestResult(unittest.TestResult):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self.fields = {
|
||||||
|
"success": 0,
|
||||||
|
"all": 0,
|
||||||
|
"fail": 0,
|
||||||
|
"skip": 0,
|
||||||
|
"error": 0,
|
||||||
|
"begin_time": "",
|
||||||
|
"results": [],
|
||||||
|
"testClass": set()
|
||||||
|
}
|
||||||
|
self.sys_stdout = None
|
||||||
|
self.sys_stderr = None
|
||||||
|
self.outputBuffer = None
|
||||||
|
|
||||||
|
def startTest(self, test):
|
||||||
|
super().startTest(test)
|
||||||
|
self.start_time = time.time()
|
||||||
|
self.outputBuffer = StringIO()
|
||||||
|
stdout_redirector.fp = self.outputBuffer
|
||||||
|
stderr_redirector.fp = self.outputBuffer
|
||||||
|
self.sys_stdout = sys.stdout
|
||||||
|
self.sys_stderr = sys.stderr
|
||||||
|
sys.stdout = stdout_redirector
|
||||||
|
sys.stderr = stderr_redirector
|
||||||
|
|
||||||
|
def complete_output(self):
|
||||||
|
if self.sys_stdout:
|
||||||
|
sys.stdout = self.sys_stdout
|
||||||
|
sys.stderr = self.sys_stderr
|
||||||
|
self.sys_stdout = None
|
||||||
|
self.sys_stderr = None
|
||||||
|
return self.outputBuffer.getvalue()
|
||||||
|
|
||||||
|
def stopTest(self, test):
|
||||||
|
test.run_time = '{:.3}s'.format((time.time() - self.start_time))
|
||||||
|
test.class_name = test.__class__.__qualname__
|
||||||
|
test.method_name = test.__dict__['_testMethodName']
|
||||||
|
test.method_doc = test.shortDescription()
|
||||||
|
self.fields['results'].append(test)
|
||||||
|
self.fields["testClass"].add(test.class_name)
|
||||||
|
self.complete_output()
|
||||||
|
|
||||||
|
def stopTestRun(self, title=None):
|
||||||
|
self.fields['fail'] = len(self.failures)
|
||||||
|
self.fields['error'] = len(self.errors)
|
||||||
|
self.fields['skip'] = len(self.skipped)
|
||||||
|
self.fields['all'] = sum(
|
||||||
|
[self.fields['fail'], self.fields['error'], self.fields['skip'], self.fields['success']])
|
||||||
|
self.fields['testClass'] = list(self.fields['testClass'])
|
||||||
|
|
||||||
|
def addSuccess(self, test):
|
||||||
|
self.fields["success"] += 1
|
||||||
|
test.state = '成功'
|
||||||
|
sys.stdout.write("{}执行——>【通过】\n".format(test))
|
||||||
|
logs = []
|
||||||
|
output = self.complete_output()
|
||||||
|
logs.append(output)
|
||||||
|
test.run_info = logs
|
||||||
|
|
||||||
|
def addFailure(self, test, err):
|
||||||
|
super().addFailure(test, err)
|
||||||
|
logs = []
|
||||||
|
test.state = '失败'
|
||||||
|
sys.stderr.write("{}执行——>【失败】\n".format(test))
|
||||||
|
output = self.complete_output()
|
||||||
|
logs.append(output)
|
||||||
|
logs.extend(traceback.format_exception(*err))
|
||||||
|
test.run_info = logs
|
||||||
|
|
||||||
|
def addSkip(self, test, reason):
|
||||||
|
super().addSkip(test, reason)
|
||||||
|
test.state = '跳过'
|
||||||
|
sys.stdout.write("{}执行--【跳过Skip】\n".format(test))
|
||||||
|
logs = [reason]
|
||||||
|
test.run_info = logs
|
||||||
|
|
||||||
|
def addError(self, test, err):
|
||||||
|
super().addError(test, err)
|
||||||
|
test.state = '错误'
|
||||||
|
sys.stderr.write("{}执行——>【错误Error】\n".format(test))
|
||||||
|
logs = []
|
||||||
|
logs.extend(traceback.format_exception(*err))
|
||||||
|
test.run_info = logs
|
||||||
|
if test.__class__.__qualname__ == '_ErrorHolder':
|
||||||
|
test.run_time = 0
|
||||||
|
res = re.search(r'(.*)\(.*\.(.*)\)', test.description)
|
||||||
|
test.class_name = res.group(2)
|
||||||
|
test.method_name = res.group(1)
|
||||||
|
test.method_doc = test.shortDescription()
|
||||||
|
self.fields['results'].append(test)
|
||||||
|
self.fields["testClass"].add(test.class_name)
|
||||||
|
else:
|
||||||
|
output = self.complete_output()
|
||||||
|
logs.append(output)
|
||||||
|
|
||||||
|
|
||||||
|
class ReRunResult(TestResult):
|
||||||
|
|
||||||
|
def __init__(self, count, interval):
|
||||||
|
super().__init__()
|
||||||
|
self.count = count
|
||||||
|
self.interval = interval
|
||||||
|
self.run_cases = []
|
||||||
|
|
||||||
|
def startTest(self, test):
|
||||||
|
if not hasattr(test, "count"):
|
||||||
|
super().startTest(test)
|
||||||
|
|
||||||
|
def stopTest(self, test):
|
||||||
|
if test not in self.run_cases:
|
||||||
|
self.run_cases.append(test)
|
||||||
|
super().stopTest(test)
|
||||||
|
|
||||||
|
def addFailure(self, test, err):
|
||||||
|
if not hasattr(test, 'count'):
|
||||||
|
test.count = 0
|
||||||
|
if test.count < self.count:
|
||||||
|
test.count += 1
|
||||||
|
sys.stderr.write("{}执行——>【失败Failure】\n".format(test))
|
||||||
|
for string in traceback.format_exception(*err):
|
||||||
|
sys.stderr.write(string)
|
||||||
|
sys.stderr.write("================{}重运行第{}次================\n".format(test, test.count))
|
||||||
|
|
||||||
|
time.sleep(self.interval)
|
||||||
|
test.run(self)
|
||||||
|
else:
|
||||||
|
super().addFailure(test, err)
|
||||||
|
if test.count != 0:
|
||||||
|
sys.stderr.write("================重运行{}次完毕================\n".format(test.count))
|
||||||
|
|
||||||
|
def addError(self, test, err):
|
||||||
|
if not hasattr(test, 'count'):
|
||||||
|
test.count = 0
|
||||||
|
if test.count < self.count:
|
||||||
|
test.count += 1
|
||||||
|
sys.stderr.write("{}执行——>【错误Error】\n".format(test))
|
||||||
|
for string in traceback.format_exception(*err):
|
||||||
|
sys.stderr.write(string)
|
||||||
|
sys.stderr.write("================{}重运行第{}次================\n".format(test, test.count))
|
||||||
|
time.sleep(self.interval)
|
||||||
|
test.run(self)
|
||||||
|
else:
|
||||||
|
super().addError(test, err)
|
||||||
|
if test.count != 0:
|
||||||
|
sys.stderr.write("================重运行{}次完毕================\n".format(test.count))
|
|
@ -0,0 +1,247 @@
|
||||||
|
import copy
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import unittest
|
||||||
|
from concurrent.futures.thread import ThreadPoolExecutor
|
||||||
|
from json import JSONDecodeError
|
||||||
|
|
||||||
|
from jinja2 import Environment, FileSystemLoader
|
||||||
|
|
||||||
|
from common.notice.dingding import DingTalk
|
||||||
|
from common.notice.email_client import SendEmail
|
||||||
|
from common.notice.weChat import WeChat
|
||||||
|
from ..core.testResult import ReRunResult
|
||||||
|
|
||||||
|
Load = unittest.defaultTestLoader
|
||||||
|
|
||||||
|
|
||||||
|
class TestRunner:
|
||||||
|
|
||||||
|
def __init__(self, suite: unittest.TestSuite,
|
||||||
|
filename="report.html",
|
||||||
|
report_dir="./reports",
|
||||||
|
title='接口测试报告',
|
||||||
|
tester='测试人员',
|
||||||
|
desc="接口自动化测试报告",
|
||||||
|
templates=2
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
测试运行器,用于执行测试套件并生成测试报告
|
||||||
|
:param suite: 测试套件
|
||||||
|
:param filename: 报告文件名
|
||||||
|
:param report_dir: 报告保存路径
|
||||||
|
:param title: 报告标题
|
||||||
|
:param tester: 测试人员
|
||||||
|
:param desc: 报告描述
|
||||||
|
:param templates: 报告模板选择
|
||||||
|
"""
|
||||||
|
# super().__init__()
|
||||||
|
if not isinstance(suite, unittest.TestSuite):
|
||||||
|
raise TypeError("suite 参数不是一个测试套件")
|
||||||
|
if not isinstance(filename, str):
|
||||||
|
raise TypeError("filename 不是字符串类型")
|
||||||
|
if not filename.endswith(".html"):
|
||||||
|
filename = filename + ".html"
|
||||||
|
self.suite = suite
|
||||||
|
self.filename = filename
|
||||||
|
self.title = title
|
||||||
|
self.tester = tester
|
||||||
|
self.desc = desc
|
||||||
|
self.templates = templates
|
||||||
|
self.report_dir = report_dir
|
||||||
|
self.result = []
|
||||||
|
self.starttime = time.time()
|
||||||
|
self.res = None
|
||||||
|
self.env = None
|
||||||
|
self.template_path = os.path.join(os.path.dirname(__file__), '../../templates')
|
||||||
|
self.env = Environment(loader=FileSystemLoader(self.template_path))
|
||||||
|
|
||||||
|
def __classification_suite(self):
|
||||||
|
"""将测试套件按类别划分"""
|
||||||
|
suites_list = []
|
||||||
|
|
||||||
|
def find_test_cases(suite):
|
||||||
|
for item in suite:
|
||||||
|
if isinstance(item, unittest.TestCase):
|
||||||
|
suites_list.append(suite)
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
find_test_cases(item)
|
||||||
|
|
||||||
|
find_test_cases(copy.deepcopy(self.suite))
|
||||||
|
return suites_list
|
||||||
|
|
||||||
|
def __get_reports(self):
|
||||||
|
"""生成测试报告"""
|
||||||
|
print("所有用例执行完毕,正在生成测试报告中......")
|
||||||
|
test_result = self.__calculate_test_result()
|
||||||
|
test_result['runtime'] = '{:.2f} S'.format(time.time() - self.starttime)
|
||||||
|
test_result["begin_time"] = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(self.starttime))
|
||||||
|
test_result["title"] = self.title
|
||||||
|
test_result["tester"] = self.tester
|
||||||
|
test_result['desc'] = self.desc
|
||||||
|
test_result['pass_rate'] = int(test_result['success'] / test_result['all'] * 100) if test_result['all'] else 0
|
||||||
|
|
||||||
|
# 判断是否要生产测试报告
|
||||||
|
os.makedirs(self.report_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# 获取历史执行数据
|
||||||
|
test_result['history'] = self.__handle_history_data(test_result)
|
||||||
|
|
||||||
|
self.template_file = self.__get_template_file()
|
||||||
|
|
||||||
|
report_content = self.__generating_templates_and_report_content(self.template_file, test_result)
|
||||||
|
# 写入报告文件
|
||||||
|
self.file_path = os.path.join(self.report_dir, self.filename)
|
||||||
|
with open(self.file_path, 'wb') as f:
|
||||||
|
f.write(report_content.encode('utf8'))
|
||||||
|
self.test_result = test_result
|
||||||
|
print(f"测试报告已经生成,报告路径为:{self.file_path}")
|
||||||
|
return test_result
|
||||||
|
|
||||||
|
def __calculate_test_result(self):
|
||||||
|
"""计算结果"""
|
||||||
|
test_result = {"success": 0, "all": 0, "fail": 0, "skip": 0, "error": 0, "results": [], "testClass": []}
|
||||||
|
for report_content in self.result:
|
||||||
|
for item in test_result:
|
||||||
|
test_result[item] += report_content.fields[item]
|
||||||
|
return test_result
|
||||||
|
|
||||||
|
def __get_template_file(self):
|
||||||
|
"""获取模板文件"""
|
||||||
|
template_file_mapping = {
|
||||||
|
2: "templates2.html",
|
||||||
|
3: "templates3.html"
|
||||||
|
}
|
||||||
|
|
||||||
|
return template_file_mapping.get(self.templates, "templates.html")
|
||||||
|
|
||||||
|
def __generating_templates_and_report_content(self, template_file, test_result):
|
||||||
|
"""渲染模板并生成报告内容"""
|
||||||
|
return self.env.get_template(template_file).render(test_result)
|
||||||
|
|
||||||
|
def __handle_history_data(self, test_result):
|
||||||
|
"""
|
||||||
|
处理历史数据
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with open(os.path.join(self.report_dir, 'history.json'), 'r', encoding='utf-8') as f:
|
||||||
|
history = json.load(f)
|
||||||
|
except FileNotFoundError as e:
|
||||||
|
history = []
|
||||||
|
except JSONDecodeError as e:
|
||||||
|
history = []
|
||||||
|
history.append({'success': test_result['success'],
|
||||||
|
'all': test_result['all'],
|
||||||
|
'fail': test_result['fail'],
|
||||||
|
'skip': test_result['skip'],
|
||||||
|
'error': test_result['error'],
|
||||||
|
'runtime': test_result['runtime'],
|
||||||
|
'begin_time': test_result['begin_time'],
|
||||||
|
'pass_rate': test_result['pass_rate'],
|
||||||
|
})
|
||||||
|
|
||||||
|
with open(os.path.join(self.report_dir, 'history.json'), 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(history, f, ensure_ascii=True)
|
||||||
|
return history
|
||||||
|
|
||||||
|
def run(self, thread_count=1, count=0, interval=2):
|
||||||
|
"""
|
||||||
|
The entrance to running tests
|
||||||
|
Note: if multiple test classes share a global variable, errors may occur due to resource competition
|
||||||
|
:param thread_count:Number of threads. default 1
|
||||||
|
:param count: Rerun times, default 0
|
||||||
|
:param interval: Rerun interval, default 2
|
||||||
|
:return: Test run results
|
||||||
|
"""
|
||||||
|
suites = self.__classification_suite()
|
||||||
|
|
||||||
|
if thread_count > 1:
|
||||||
|
with ThreadPoolExecutor(max_workers=thread_count) as ts:
|
||||||
|
for i in suites:
|
||||||
|
self.res = ReRunResult(count=count, interval=interval)
|
||||||
|
self.result.append(self.res)
|
||||||
|
ts.submit(i.run, result=self.res).add_done_callback(self.res.stopTestRun)
|
||||||
|
else:
|
||||||
|
self.res = ReRunResult(count=count, interval=interval)
|
||||||
|
self.result.append(self.res)
|
||||||
|
self.suite.run(self.res)
|
||||||
|
self.res.stopTestRun()
|
||||||
|
result = self.__get_reports()
|
||||||
|
return result
|
||||||
|
|
||||||
|
def rerun_run(self, count=0, interval=2):
|
||||||
|
"""
|
||||||
|
重新运行测试用例,包括失败和错误的用例
|
||||||
|
:param count: 重新运行次数,默认为0
|
||||||
|
:param interval: 重新运行间隔,默认为2秒
|
||||||
|
:return: 重新运行间隔,默认为2秒
|
||||||
|
"""
|
||||||
|
self.res = ReRunResult(count=count, interval=interval)
|
||||||
|
self.result.append(self.res)
|
||||||
|
test_case_suites = self.__classification_suite()
|
||||||
|
for test_case_suite in test_case_suites:
|
||||||
|
test_case_suite.run(self.res)
|
||||||
|
self.res.stopTestRun()
|
||||||
|
result = self.__get_reports()
|
||||||
|
return result
|
||||||
|
|
||||||
|
def get_except_info(self):
|
||||||
|
"""Get error reporting information for error cases and failure cases"""
|
||||||
|
except_info = []
|
||||||
|
num = 0
|
||||||
|
for i in self.result:
|
||||||
|
for texts in i.failures:
|
||||||
|
t, content = texts
|
||||||
|
num += 1
|
||||||
|
except_info.append("*{}、用例【{}】执行失败*,\n失败信息如下:".format(num, t._testMethodDoc))
|
||||||
|
except_info.append(content)
|
||||||
|
for texts in i.errors:
|
||||||
|
num += 1
|
||||||
|
t, content = texts
|
||||||
|
except_info.append("*{}、用例【{}】执行错误*,\n错误信息如下:".format(num, t._testMethodDoc))
|
||||||
|
except_info.append(content)
|
||||||
|
except_str = "\n".join(except_info)
|
||||||
|
return except_str
|
||||||
|
|
||||||
|
def get_failed_test_cases(self):
|
||||||
|
"""get error or failed testcase info"""
|
||||||
|
failed_test_cases = []
|
||||||
|
for res in self.result:
|
||||||
|
for failure in res.failures:
|
||||||
|
failed_test_cases.append(failure[0])
|
||||||
|
for error in res.errors:
|
||||||
|
failed_test_cases.append(error[0])
|
||||||
|
return failed_test_cases
|
||||||
|
|
||||||
|
def email_notice(self):
|
||||||
|
"""
|
||||||
|
发送邮件通知
|
||||||
|
"""
|
||||||
|
email_content = {"file": [os.path.abspath(self.file_path)],
|
||||||
|
"content": self.__generating_templates_and_report_content("templates03.html", self.test_result)
|
||||||
|
}
|
||||||
|
sm = SendEmail()
|
||||||
|
file_path = email_content["file"]
|
||||||
|
content = email_content["content"]
|
||||||
|
sm.send_mail(content=content, file_path=file_path)
|
||||||
|
|
||||||
|
def dingtalk_notice(self):
|
||||||
|
"""
|
||||||
|
发送钉钉机器人通知
|
||||||
|
"""
|
||||||
|
|
||||||
|
notice_content = self.__generating_templates_and_report_content('dingtalk.md', self.test_result)
|
||||||
|
except_info = self.get_except_info()
|
||||||
|
ding = DingTalk(self.title, notice_content, except_info)
|
||||||
|
ding.send_info()
|
||||||
|
|
||||||
|
def weixin_notice(self):
|
||||||
|
"""
|
||||||
|
推送企业微信机器人
|
||||||
|
"""
|
||||||
|
notice_content = self.__generating_templates_and_report_content('weChat.md', self.test_result)
|
||||||
|
wx = WeChat(notice_content)
|
||||||
|
wx.send_main()
|
Loading…
Reference in New Issue