├── doc ├── locust.md ├── sql_write.md ├── Recording.md └── CaseWrite.md ├── config ├── __init__.py ├── __pycache__ │ ├── setting.cpython-36.pyc │ ├── __init__.cpython-36.pyc │ └── xml_handler.cpython-36.pyc ├── wechat.yaml ├── config.xml ├── path.yaml ├── xml_handler.py └── setting.py ├── lib ├── __init__.py ├── public │ ├── __init__.py │ ├── __pycache__ │ │ ├── logger.cpython-36.pyc │ │ ├── wraps.cpython-36.pyc │ │ ├── __init__.cpython-36.pyc │ │ ├── Recursion.cpython-36.pyc │ │ ├── load_cases.cpython-36.pyc │ │ ├── case_manager.cpython-36.pyc │ │ └── http_keywords.cpython-36.pyc │ ├── Recursion.py │ ├── text_similarity_comparison.py │ ├── relevance.py │ ├── http_keywords.py │ ├── logger.py │ ├── case_manager.py │ ├── load_cases.py │ ├── wraps.py │ └── HtmlReport.py ├── utils │ ├── __init__.py │ ├── __pycache__ │ │ ├── fp.cpython-36.pyc │ │ ├── email.cpython-36.pyc │ │ ├── __init__.cpython-36.pyc │ │ ├── security.cpython-36.pyc │ │ ├── time_util.cpython-36.pyc │ │ ├── analyze_log.cpython-36.pyc │ │ ├── exceptions.cpython-36.pyc │ │ ├── create_workFlow_obj.cpython-36.pyc │ │ └── load_fiddler_files.cpython-36.pyc │ ├── exceptions.py │ ├── test_api_server.py │ ├── time_util.py │ ├── fp.py │ ├── random_data.py │ ├── security.py │ ├── excel_handler.py │ ├── analyze_log.py │ ├── use_MySql.py │ ├── email.py │ ├── resvalues.py │ └── recording.py ├── templates │ ├── locust_func │ ├── locust_load_attr │ ├── locust_header │ ├── header │ ├── content │ └── email ├── static │ └── meteor.ico ├── __pycache__ │ └── __init__.cpython-36.pyc └── __about__.py ├── .coveralls.yaml ├── variables ├── random_params │ ├── RandomName.yaml │ ├── RandomWord.yaml │ ├── RandomDate.yaml │ ├── RandomCompany.yaml │ ├── RandomPostcode.yaml │ ├── RandomSsn.yaml │ ├── RandomEmail.yaml │ ├── RandomPhoneNum.yaml │ ├── RandomSentence.yaml │ ├── RandomParagraph.yaml │ └── RandomText.yaml ├── config_params │ └── Host.yaml ├── extract_params │ ├── token.yaml │ └── RsaPublicKey.yaml └── interface_params │ ├── favorites.yaml │ └── login.yaml ├── .idea ├── sonarlint │ └── issuestore │ │ ├── 0 │ │ └── 3 │ │ │ └── 0398ccd0f49298b10a3d76a47800d2ebecd49859 │ │ ├── 2 │ │ └── 4 │ │ │ └── 245d8ec80d138d8104c8f8338a3e2066fd7680d1 │ │ ├── 8 │ │ ├── 0 │ │ │ └── 806d6ceb4e60342798038124ccf7dc1a423a4433 │ │ └── e │ │ │ └── 8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d │ │ ├── 9 │ │ └── 4 │ │ │ └── 948af7a24852e9101a97a505cc5d2c7f7215c1f4 │ │ ├── b │ │ ├── 6 │ │ │ └── b68caa7969296d0b3c2aacfc5c318c77ad9909b4 │ │ └── e │ │ │ └── bef34275ebe3082d1d4188ad1239d4bdb072d867 │ │ ├── e │ │ └── 5 │ │ │ └── e563262209b7c2e62d1869582c8134583baa2204 │ │ └── index.pb ├── encodings.xml ├── libraries │ └── R_User_Library.xml ├── vcs.xml ├── modules.xml ├── misc.xml ├── MeteorTears.iml ├── codeStyles │ └── Project.xml ├── inspectionProfiles │ └── Project_Default.xml └── sonarIssues.xml ├── EnvironClean.py ├── data ├── case_data │ ├── bot_profile.yaml │ └── bot_prs.yaml └── env_data │ └── stopKeywords ├── .travis.yml ├── requirements.txt ├── Pipfile ├── LICENSE ├── cases ├── User_Home │ └── Home.yaml ├── User_Member │ └── Member.yaml ├── User_Coupon │ └── Coupon.yaml ├── User_Favorites │ └── Favorites.yaml └── ApiLogin.yaml ├── .coverage ├── run.py ├── setup.py ├── loader.py ├── README.md ├── report └── email ├── update_log.md └── pylint.conf /doc/locust.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /config/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /lib/public/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/templates/locust_func: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/templates/locust_load_attr: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.coveralls.yaml: -------------------------------------------------------------------------------- 1 | service_name: travis-pro 2 | -------------------------------------------------------------------------------- /variables/random_params/RandomName.yaml: -------------------------------------------------------------------------------- 1 | RandomName: 王杰 -------------------------------------------------------------------------------- /variables/random_params/RandomWord.yaml: -------------------------------------------------------------------------------- 1 | RandomWord: 项目 -------------------------------------------------------------------------------- /variables/random_params/RandomDate.yaml: -------------------------------------------------------------------------------- 1 | RandomDate: 1993-11-20 -------------------------------------------------------------------------------- /variables/random_params/RandomCompany.yaml: -------------------------------------------------------------------------------- 1 | RandomCompany: 创汇传媒有限公司 -------------------------------------------------------------------------------- /variables/random_params/RandomPostcode.yaml: -------------------------------------------------------------------------------- 1 | RandomPostcode: 354179 -------------------------------------------------------------------------------- /variables/random_params/RandomSsn.yaml: -------------------------------------------------------------------------------- 1 | RandomSsn: 371103197010242523 -------------------------------------------------------------------------------- /.idea/sonarlint/issuestore/0/3/0398ccd0f49298b10a3d76a47800d2ebecd49859: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.idea/sonarlint/issuestore/2/4/245d8ec80d138d8104c8f8338a3e2066fd7680d1: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.idea/sonarlint/issuestore/8/e/8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.idea/sonarlint/issuestore/b/6/b68caa7969296d0b3c2aacfc5c318c77ad9909b4: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.idea/sonarlint/issuestore/b/e/bef34275ebe3082d1d4188ad1239d4bdb072d867: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.idea/sonarlint/issuestore/e/5/e563262209b7c2e62d1869582c8134583baa2204: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /variables/random_params/RandomEmail.yaml: -------------------------------------------------------------------------------- 1 | RandomEmail: bailei@yahoo.com -------------------------------------------------------------------------------- /variables/random_params/RandomPhoneNum.yaml: -------------------------------------------------------------------------------- 1 | RandomPhoneNum: 13563595308 -------------------------------------------------------------------------------- /variables/random_params/RandomSentence.yaml: -------------------------------------------------------------------------------- 1 | RandomSentence: 历史特别这个介绍出现. -------------------------------------------------------------------------------- /variables/config_params/Host.yaml: -------------------------------------------------------------------------------- 1 | Host: https://test2-appserver.atzc.com:7065 -------------------------------------------------------------------------------- /variables/extract_params/token.yaml: -------------------------------------------------------------------------------- 1 | token: 069d7e59a83c4c2dadd7f332619dcaf4 -------------------------------------------------------------------------------- /lib/static/meteor.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoxiaolulu/MeteorTears/HEAD/lib/static/meteor.ico -------------------------------------------------------------------------------- /variables/random_params/RandomParagraph.yaml: -------------------------------------------------------------------------------- 1 | RandomParagraph: 根据学习如果.经营不要注意进行不断所以他们.应该状态经营用户音乐科技这个.特别最大大学工程什么更新人员. -------------------------------------------------------------------------------- /lib/__pycache__/__init__.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoxiaolulu/MeteorTears/HEAD/lib/__pycache__/__init__.cpython-36.pyc -------------------------------------------------------------------------------- /lib/utils/__pycache__/fp.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoxiaolulu/MeteorTears/HEAD/lib/utils/__pycache__/fp.cpython-36.pyc -------------------------------------------------------------------------------- /config/__pycache__/setting.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoxiaolulu/MeteorTears/HEAD/config/__pycache__/setting.cpython-36.pyc -------------------------------------------------------------------------------- /config/__pycache__/__init__.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoxiaolulu/MeteorTears/HEAD/config/__pycache__/__init__.cpython-36.pyc -------------------------------------------------------------------------------- /config/wechat.yaml: -------------------------------------------------------------------------------- 1 | ### 企业微信配置参数 ### 2 | wechat: 3 | corp: 'wwc970d773f2883409' 4 | secret: 'nkiM5L0pP438RDjO-oiX-MMzzWg2gaQI96t9NxtTQNs' 5 | -------------------------------------------------------------------------------- /lib/public/__pycache__/logger.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoxiaolulu/MeteorTears/HEAD/lib/public/__pycache__/logger.cpython-36.pyc -------------------------------------------------------------------------------- /lib/public/__pycache__/wraps.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoxiaolulu/MeteorTears/HEAD/lib/public/__pycache__/wraps.cpython-36.pyc -------------------------------------------------------------------------------- /lib/utils/__pycache__/email.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoxiaolulu/MeteorTears/HEAD/lib/utils/__pycache__/email.cpython-36.pyc -------------------------------------------------------------------------------- /EnvironClean.py: -------------------------------------------------------------------------------- 1 | """ 2 | Setup/Teardown 测试数据 3 | .yaml用例文件调用示例: 4 | setUp: EnvironClean.{func} 5 | tearDown: EnvironClean.{func} 6 | """ 7 | 8 | -------------------------------------------------------------------------------- /config/__pycache__/xml_handler.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoxiaolulu/MeteorTears/HEAD/config/__pycache__/xml_handler.cpython-36.pyc -------------------------------------------------------------------------------- /lib/public/__pycache__/__init__.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoxiaolulu/MeteorTears/HEAD/lib/public/__pycache__/__init__.cpython-36.pyc -------------------------------------------------------------------------------- /lib/utils/__pycache__/__init__.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoxiaolulu/MeteorTears/HEAD/lib/utils/__pycache__/__init__.cpython-36.pyc -------------------------------------------------------------------------------- /lib/utils/__pycache__/security.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoxiaolulu/MeteorTears/HEAD/lib/utils/__pycache__/security.cpython-36.pyc -------------------------------------------------------------------------------- /lib/utils/__pycache__/time_util.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoxiaolulu/MeteorTears/HEAD/lib/utils/__pycache__/time_util.cpython-36.pyc -------------------------------------------------------------------------------- /lib/public/__pycache__/Recursion.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoxiaolulu/MeteorTears/HEAD/lib/public/__pycache__/Recursion.cpython-36.pyc -------------------------------------------------------------------------------- /lib/public/__pycache__/load_cases.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoxiaolulu/MeteorTears/HEAD/lib/public/__pycache__/load_cases.cpython-36.pyc -------------------------------------------------------------------------------- /lib/utils/__pycache__/analyze_log.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoxiaolulu/MeteorTears/HEAD/lib/utils/__pycache__/analyze_log.cpython-36.pyc -------------------------------------------------------------------------------- /lib/utils/__pycache__/exceptions.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoxiaolulu/MeteorTears/HEAD/lib/utils/__pycache__/exceptions.cpython-36.pyc -------------------------------------------------------------------------------- /lib/public/__pycache__/case_manager.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoxiaolulu/MeteorTears/HEAD/lib/public/__pycache__/case_manager.cpython-36.pyc -------------------------------------------------------------------------------- /lib/public/__pycache__/http_keywords.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoxiaolulu/MeteorTears/HEAD/lib/public/__pycache__/http_keywords.cpython-36.pyc -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /lib/utils/__pycache__/create_workFlow_obj.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoxiaolulu/MeteorTears/HEAD/lib/utils/__pycache__/create_workFlow_obj.cpython-36.pyc -------------------------------------------------------------------------------- /lib/utils/__pycache__/load_fiddler_files.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoxiaolulu/MeteorTears/HEAD/lib/utils/__pycache__/load_fiddler_files.cpython-36.pyc -------------------------------------------------------------------------------- /.idea/libraries/R_User_Library.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/sonarlint/issuestore/8/0/806d6ceb4e60342798038124ccf7dc1a423a4433: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoxiaolulu/MeteorTears/HEAD/.idea/sonarlint/issuestore/8/0/806d6ceb4e60342798038124ccf7dc1a423a4433 -------------------------------------------------------------------------------- /.idea/sonarlint/issuestore/9/4/948af7a24852e9101a97a505cc5d2c7f7215c1f4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoxiaolulu/MeteorTears/HEAD/.idea/sonarlint/issuestore/9/4/948af7a24852e9101a97a505cc5d2c7f7215c1f4 -------------------------------------------------------------------------------- /data/case_data/bot_profile.yaml: -------------------------------------------------------------------------------- 1 | bot_profile: 2 | action: SELECT 3 | execSQL: 4 | table: "[dbo].[Tenant_Bot_Profile]" 5 | columns: Bot_Name 6 | params: WHERE Bot_Name = 'test' 7 | desc: "" -------------------------------------------------------------------------------- /data/case_data/bot_prs.yaml: -------------------------------------------------------------------------------- 1 | bot_prs: 2 | action: SELECT 3 | execSQL: 4 | table: "[dbo].[Tenant_Bot_Profile]" 5 | columns: Bot_Constellation 6 | params: WHERE Bot_Name = 'test' 7 | desc: "" -------------------------------------------------------------------------------- /lib/templates/locust_header: -------------------------------------------------------------------------------- 1 | import urllib3 2 | from locust import HttpLocust, TaskSet, task 3 | 4 | urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) 5 | 6 | 7 | class {}(TaskSet): 8 | 9 | -------------------------------------------------------------------------------- /variables/random_params/RandomText.yaml: -------------------------------------------------------------------------------- 1 | RandomText: 以后介绍由于的话可是等级显示.安全登录制作数据责任标准女人.其中来源应该这种结果不能有关. 2 | 继续说明根据正在结果.会员一点出来方法一定.帖子文章就是公司我的. 3 | 是一作为其中次数.只要电话图片网站. 4 | 程序学习操作大家商品知道目前.很多继续提高位置起来.上海留言密码今天查看因为认为. 5 | 品牌软件一定留言设备这么设计.搜索行业这么教育.只有然后发生任何方面但是因此选择. -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.6" 4 | install: 5 | - pip install pipenv --upgrade-strategy=only-if-needed 6 | - pipenv install --dev --skip-lock 7 | script: 8 | - pipenv run coverage run run.py 9 | after_success: 10 | - pipenv run coveralls 11 | -------------------------------------------------------------------------------- /lib/templates/header: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | import unittest 3 | from lib.public import wraps 4 | import EnvironClean 5 | 6 | 7 | class {}(unittest.TestCase): 8 | 9 | """{}接口测试脚本""" 10 | 11 | def setUp(self): 12 | {} 13 | 14 | def tearDown(self): 15 | {} 16 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /lib/__about__.py: -------------------------------------------------------------------------------- 1 | __title__ = 'MeteorTears' 2 | __description__ = 'Even the most boring times in life are limited.' 3 | __url__ = 'https://github.com/xiaoxiaolulu/MeteorTears' 4 | __version__ = '1.0.1' 5 | __author__ = 'Null' 6 | __author_email__ = '546464268@qq.com' 7 | __license__ = 'MIT' 8 | __copyright__ = 'Copyright 2018 Null' 9 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /variables/extract_params/RsaPublicKey.yaml: -------------------------------------------------------------------------------- 1 | RsaPublicKey: -----BEGIN PUBLIC KEY----- 2 | MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCVvsPPGKEz84Devsriyto7txCH 3 | r+glAVW6k+TvrdXTG8NEeg6ummlrjQ+iOGxWMv2hMTgvyyF2aLrgoAnb+LCqw0aT 4 | x57eYj1afcJLyGYYlSfzgf7BByPSgUYObxBDVCoITbSnp1Z0z0tIpXBg9zE+Povj 5 | OyZOYBGrfWu1btBkFwIDAQAB 6 | -----END PUBLIC KEY----- 7 | -------------------------------------------------------------------------------- /lib/utils/exceptions.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | 4 | class JsonLoadingError(Exception): 5 | pass 6 | 7 | 8 | class TestApiMethodError(Exception): 9 | pass 10 | 11 | 12 | class CaseYamlFileNotFound(FileNotFoundError, FileExistsError): 13 | pass 14 | 15 | 16 | class SubInheritCaseParamsKwargs(TypeError, KeyError): 17 | pass 18 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | xlwt==1.3.0 2 | six==1.11.0 3 | requests_html==0.10.0 4 | defusedxml==0.5.0 5 | PySnooper==0.0.37 6 | xlrd==1.1.0 7 | numpy==1.15.0 8 | simplejson==3.16.0 9 | colorama==0.3.9 10 | click==6.7 11 | setuptools==40.5.0 12 | urllib3==1.23 13 | requests_file==1.4.3 14 | Flask_HTTPAuth==3.2.4 15 | Flask==1.0.2 16 | Faker==1.0.7 17 | PyMySQL==0.9.2 18 | requests==2.20.0 19 | jieba==0.39 20 | PyYAML==5.1.1 21 | pylint==2.3.1 22 | coverage==4.5.2 23 | coveralls==1.5.1 24 | -------------------------------------------------------------------------------- /lib/templates/content: -------------------------------------------------------------------------------- 1 | 2 | @unittest.skipIf({}, '条件为True ,用例跳过') 3 | @wraps.cases_runner 4 | @wraps.result_assert 5 | def {}(self, *args, **kwargs): 6 | """{}""" 7 | response = kwargs.get('response') 8 | self.assertEqual(kwargs.get('expect_assert_value'), kwargs.get('kwassert_value')) 9 | self.assertEqual(kwargs.get('expect_kwassert_same'), kwargs.get('response_kwassert_content')) 10 | self.assertEqual(kwargs.get('database_check'), kwargs.get('execute_res')) 11 | 12 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | xlrd = "==1.1.0" 10 | click = "==6.7" 11 | colorama = "==0.3.9" 12 | urllib3 = "==1.23" 13 | pymssql = "==2.1.4" 14 | six = "==1.11.0" 15 | requests = "==2.21.0" 16 | xlwt = "==1.3.0" 17 | coverage = "==4.5.2" 18 | coveralls = "==1.5.1" 19 | pylint = "==2.3.1" 20 | Flask = "==1.0.2" 21 | PyYAML = "==5.1" 22 | defusedxml = "==0.5.0" 23 | PySnooper = "==0.0.37" 24 | requests-html = "==0.10.0" 25 | requests-file = "==1.4.3" 26 | Faker = "==1.0.7" 27 | 28 | [requires] 29 | python_version = "3.6" 30 | -------------------------------------------------------------------------------- /config/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | EmailReceivers 5 | 13564957378@163.com 6 | 7 | 8 | EmailSender 9 | 546464268@qq.com 10 | whpttvwcobmubbcd 11 | 12 | 13 | MySQLTest9 14 | 47.96.104.13 15 | 3306 16 | autoTest 17 | auto2016 18 | atzuchedb 19 | utf8 20 | 21 | 22 | -------------------------------------------------------------------------------- /lib/utils/test_api_server.py: -------------------------------------------------------------------------------- 1 | from flask import abort, jsonify, Flask, request, Response, make_response 2 | from flask_httpauth import HTTPBasicAuth 3 | 4 | auth = HTTPBasicAuth() 5 | app = Flask(__name__) 6 | 7 | 8 | tasks = { 9 | "data": { 10 | "loginName": "admin", 11 | "roles": 1, 12 | "permissions": 1, 13 | "active": 1 14 | }, 15 | "stateCode": { 16 | "code": 0, 17 | "desc": "成功" 18 | }, 19 | "statusText": "成功", 20 | "timestamp": "1500531770453", 21 | "success": 1 22 | } 23 | 24 | 25 | @app.route("/task", methods=['GET']) 26 | def get_all_task(): 27 | return jsonify(tasks) 28 | 29 | 30 | if __name__ == "__main__": 31 | app.run( 32 | host="127.0.0.1", 33 | port=8989, 34 | debug=True 35 | ) 36 | -------------------------------------------------------------------------------- /lib/utils/time_util.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | import time 3 | 4 | 5 | def timestamp(format_key: str) -> str: 6 | r"""格式化时间 7 | 8 | :Args: 9 | - format_key: 转化格式方式, str object. 10 | 11 | :Usage: 12 | timestamp('format_day') 13 | """ 14 | format_time = { 15 | 'default': 16 | { 17 | 'format_day': '%Y-%m-%d', 18 | 'format_now': '%Y-%m-%d-%H_%M_%S', 19 | 'unix_now': '%Y-%m-%d %H:%M:%S', 20 | } 21 | } 22 | return time.strftime(format_time['default'][format_key], time.localtime(time.time())) 23 | 24 | 25 | def time_unix() -> int: 26 | r"""转化为时间蹉 27 | 28 | :Usage: 29 | time_unix() 30 | """ 31 | return int(time.mktime(time.strptime(timestamp('unix_now'), "%Y-%m-%d %H:%M:%S"))) 32 | -------------------------------------------------------------------------------- /config/path.yaml: -------------------------------------------------------------------------------- 1 | ### 项目个目录路径 ### 2 | 3 | # Unittest文件模板-用例函数部分(test case) 4 | CONTENT: lib/templates/content 5 | # Unittest文件模板-类组织结构(test suite) 6 | HEADER: lib/templates/header 7 | # 邮件模板 8 | EMAIL: lib/templates/email 9 | # 项目配置文件 10 | XML_CONFIG: config/config.xml 11 | # 自动生成.py用例文件 12 | TEST_CASES: lib/test_cases/ 13 | # 企业微信相关参数文件 14 | WECHAT: config/wechat.yaml 15 | # 日志存放目录 16 | LOG: report/log/ 17 | # 测试报告存放目录 18 | REPORT: report/ 19 | # 编写Yaml格式测试用例目录 20 | CASES: cases/ 21 | 22 | API_PATH: cases/test_api/ 23 | 24 | TEST_CASE: cases/test_cases/ 25 | 26 | TEST_SUITE: cases/test_suites/ 27 | # 测试数据 28 | CASE_DATA: data/case_data/ 29 | # 项目依赖的公共数据 30 | ENV_DATA: data/env_data/ 31 | # 临时变量 & 常用变量 & 随机数据 文件目录 32 | RES: variables/ 33 | # 接口提取参数存放路径 34 | EXTRACT_PARAMS: variables/extract_params/ 35 | # 接口Host等常量配置参数存放路径 36 | CONFIG_PARAMS: variables/config_params/ 37 | # 接口Data/Json等参数存放路径 38 | INTERFACE_PARAMS: variables/interface_params/ 39 | # 随机变量存放路径 40 | RANDOM_PARAMS: variables/random_params/ 41 | -------------------------------------------------------------------------------- /variables/interface_params/favorites.yaml: -------------------------------------------------------------------------------- 1 | favorites: 2 | skip: False 3 | relevant_parameter: [token, RandomPostcode] 4 | method: post 5 | url: https://test2-appserver.atzc.com:7065/v57/favorites 6 | json: 7 | "AppChannelId": "testmarket" 8 | "schema": "A" 9 | "OS": "ANDROID" 10 | "appName": "atzucheApp" 11 | "OsVersion": "27" 12 | "mem_no": "819209698" 13 | "IMEI": "861438046958534" 14 | "AndroidId": "7ccde31ec3ec4990" 15 | "deviceName": "V1816A" 16 | "mac": "B40FB38790F3" 17 | "token": ${token}$ 18 | "AppVersion": ${RandomPostcode}$ 19 | "publicToken": ${token}$ 20 | "publicCityCode": "021" 21 | "requestId": "B40FB38790F31560755232507" 22 | "PublicLongitude": "121.409265" 23 | "androidID": "7ccde31ec3ec4990" 24 | "PublicLatitude": "31.172216" 25 | timeout: 8 26 | headers: 27 | Content-Type: application/json; charset=utf-8 28 | User-Agent: Autoyol_98:Android_27|36722B4DB3C7E55D77375E490D3B5796D30A340002F1349E3B9F1CA3BF 29 | Accept: application/json;version=3.0;compress=false 30 | -------------------------------------------------------------------------------- /variables/interface_params/login.yaml: -------------------------------------------------------------------------------- 1 | login: 2 | # 是否跳过 3 | skip: False 4 | # 请求方式 5 | method: post 6 | # 请求路由 7 | url: https://test2-appserver.atzc.com:7065/v31/mem/action/login 8 | timeout: 8 9 | # 请求参数 10 | json: 11 | "AppChannelId": "testmarket" 12 | "OS": "ANDROID" 13 | "validCode": "111111" 14 | "loginType": "validCode" 15 | "appName": "atzucheApp" 16 | "OsVersion": "27" 17 | "mobile": "13564957378" 18 | "mem_no": "718160237" 19 | "IMEI": "" 20 | "AndroidId": "7ccde31ec3ec4990" 21 | "deviceName": "V1816A" 22 | "mac": "B40FB38790F3" 23 | "AppVersion": 98 24 | "publicToken": "0" 25 | "publicCityCode": "021" 26 | "requestId": "B40FB38790F31560751297632" 27 | "PublicLongitude": "0" 28 | "androidID": "7ccde31ec3ec4990" 29 | "PublicLatitude": "0" 30 | # 请求头部 31 | headers: 32 | Content-Type: application/json; charset=utf-8 33 | User-Agent: Autoyol_80:Android_21|CF336E2B0D0A361E679A6699A38A1576D3013004042034B17B72C7B188 34 | Accept: application/json;version=3.0;compress=false 35 | -------------------------------------------------------------------------------- /doc/sql_write.md: -------------------------------------------------------------------------------- 1 | # Mysql执行语句编写讲解 2 | 3 | ### 示例如下: 4 | ```yaml 5 | - ChannelBudget: 6 | action: SELECT 7 | execSQL: 8 | - table: shopping 9 | - columns: ['id'] 10 | - params: id='1' 11 | - desc: ORDER BY id DESC LIMIT 1 12 | except: 13 | - is_table: 0 14 | - message: You have an error in your SQL syntax 15 | ``` 16 | 17 | 18 | key | value | Sample 19 | ------------ | -------------| ---------------- 20 | action| sql执行操作类 | SELECT/DELETE/INSERT/UPDATE等 21 | table| 数据库表 | channel_budget 22 | columns| 列名 | ['channel_id'] 列表类型,支持多个值 23 | params| 检索条件 | id='1' 24 | desc| 排序 | ORDER BY ID DESC LIMIT 1 25 | 26 | 27 | ### 执行 28 | 29 | ```python 30 | @test_data_runner 31 | def channel_budget(): 32 | pass 33 | ``` 34 | 35 | #### 返回结果如下 36 | ```python 37 | [DEBUG] [2018-12-10 18:12:25,227] logger.py [line:136] : 操作的数据库表为 ====> Shopping 38 | [DEBUG] [2018-12-10 18:12:25,233] logger.py [line:136] : 执行的SQL语句为 ===> SELECT id FROM shopping WHERE id='1' ORDER BY id DESC LIMIT 1 39 | [DEBUG] [2018-12-10 18:12:25,234] logger.py [line:136] : 执行结果为 ===> 1 40 | ``` 41 | -------------------------------------------------------------------------------- /lib/utils/fp.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | import os 3 | 4 | 5 | def iter_files(path: str, otype='path') -> list: 6 | r"""返回文件目录路径中所有文件路径,以列表的形式返回. 7 | 8 | :Args: 9 | - param path: 文件路径, str object. 10 | - param otype: 输出返回的数据类型、默认文件路径, str object default path. 11 | 12 | :Usage: 13 | iter_files('./cases/') 14 | """ 15 | 16 | filename = [] 17 | 18 | def iterate_files(path): 19 | 20 | path_rest = path if not isinstance(path, bytes) else path.decode() 21 | abspath = os.path.abspath(path_rest) 22 | 23 | try: 24 | all_files = os.listdir(abspath) 25 | for items in all_files: 26 | files = os.path.join(path, items) 27 | if os.path.isfile(files): 28 | filename.append(files) if otype == 'path' else filename.append(items) 29 | else: 30 | iterate_files(files) 31 | except (FileNotFoundError, AttributeError, BytesWarning, IOError, FileExistsError): 32 | pass 33 | 34 | iterate_files(path) 35 | 36 | return filename 37 | -------------------------------------------------------------------------------- /.idea/MeteorTears.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 17 | 18 | 19 | 21 | 22 | 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Null 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /lib/utils/random_data.py: -------------------------------------------------------------------------------- 1 | import ast 2 | from config import setting 3 | from faker import Faker 4 | from lib.public import logger 5 | 6 | 7 | class RandomData(object): 8 | 9 | fake = Faker(locale='zh_CN') 10 | random_data = { 11 | 'RandomName': fake.name(), 12 | 'RandomPhoneNum': fake.phone_number(), 13 | 'RandomWord': fake.word(ext_word_list=None), 14 | 'RandomSentence': fake.sentence(nb_words=6, variable_nb_words=True, ext_word_list=None), 15 | 'RandomParagraph': fake.paragraph(nb_sentences=3, variable_nb_sentences=True, ext_word_list=None), 16 | 'RandomPostcode': fake.postcode(), 17 | 'RandomCompany': fake.company(), 18 | 'RandomDate': fake.date(), 19 | 'RandomEmail': fake.ascii_email(), 20 | 'RandomText': fake.text(), 21 | 'RandomSsn': fake.ssn() 22 | } 23 | 24 | @classmethod 25 | def create_random_test_data(cls) -> None: 26 | r"""创建常用的随机数据,并生成.yaml文件存在临时文件目录中 27 | """ 28 | for key, value in cls.random_data.items(): 29 | filepath = setting.RANDOM_PARAMS + key + '.yaml' 30 | with open(filepath, 'w', encoding='utf-8') as file: 31 | file.write('{}: {}'.format(key, value)) 32 | logger.log_info('生成随机测试数据 => {} 成功.'.format(filepath)) 33 | -------------------------------------------------------------------------------- /cases/User_Home/Home.yaml: -------------------------------------------------------------------------------- 1 | ##################################### 2 | # Created By: Null 3 | # Created date: 2019-06-11 4 | # Desc: 首页 5 | ##################################### 6 | test_api_setup: 7 | relevant_parameter: [login] 8 | description: "member_extend_case" 9 | cases: ${login}$ 10 | # Response返回体提取的参数 11 | res_index: [token] 12 | 13 | test_home_success: 14 | # 关联的临时变量文件 15 | relevant_parameter: [token, Host] 16 | # 关联的Sql数据 17 | relevant_sql: [] 18 | # 是否跳过 19 | skip: False 20 | # 用例描述 21 | description: "首页-/v59/homepage/homeData" 22 | # 请求方式 23 | method: get 24 | # 请求路由 25 | url: ${Host}$/v59/homepage/homeData 26 | params: cityCode=${token}$&OS=ANDROID&OsVersion=27&AppVersion=98&IMEI=861438046958534&mac=B40FB38790F3&androidID=7ccde31ec3ec4990&PublicLongitude=121.409265&PublicLatitude=31.172216&publicCityCode=021&appName=atzucheApp&deviceName=V1816A&publicToken=${token}$&AppChannelId=testmarket&AndroidId=7ccde31ec3ec4990&requestId=B40FB38790F31560752685029&mem_no=819209698&schema=A 27 | timeout: 8 28 | # 请求头部 29 | headers: 30 | Content-Type: application/json; charset=utf-8 31 | User-Agent: Autoyol_98:Android_27|36722B4DB3C7E55D77375E490D3B5796D30A340002F1349E3B9F1CA3BF 32 | Accept: application/json;version=3.0;compress=false 33 | # 断言 34 | assert: 35 | status_code: 200 36 | resCode: "000000" 37 | resMsg: "success" 38 | # 落库校验 39 | check_db: -------------------------------------------------------------------------------- /cases/User_Member/Member.yaml: -------------------------------------------------------------------------------- 1 | ##################################### 2 | # Created By: Null 3 | # Created date: 2019-06-11 4 | # Desc: 我的 5 | ##################################### 6 | test_api_setup: 7 | relevant_parameter: [login] 8 | description: "member_extend_case" 9 | cases: ${login}$ 10 | # Response返回体提取的参数 11 | res_index: [token] 12 | 13 | test_member_success: 14 | # 关联的临时变量文件 15 | relevant_parameter: [token, Host] 16 | # 关联的Sql数据 17 | relevant_sql: [] 18 | # 是否跳过 19 | skip: False 20 | # 用例描述 21 | description: "我的页面-/v47/member/info" 22 | # 请求方式 23 | method: get 24 | # 请求路由 25 | url: ${Host}$/v47/member/info 26 | params: machineCode=B40FB38790F38614380469585347ccde31ec3ec4990&token=${token}$&OS=ANDROID&OsVersion=27&AppVersion=98&IMEI=861438046958534&mac=B40FB38790F3&androidID=7ccde31ec3ec4990&PublicLongitude=121.409265&PublicLatitude=31.172216&publicCityCode=021&appName=atzucheApp&deviceName=V1816A&publicToken=${token}$&AppChannelId=testmarket&AndroidId=7ccde31ec3ec4990&requestId=B40FB38790F31560757258156&mem_no=819209698&schema=A 27 | timeout: 8 28 | # 请求头部 29 | headers: 30 | Content-Type: application/json; charset=utf-8 31 | User-Agent: Autoyol_98:Android_27|36722B4DB3C7E55D77375E490D3B5796D30A340002F1349E3B9F1CA3BF 32 | Accept: application/json;version=3.0;compress=false 33 | # 断言 34 | assert: 35 | status_code: 200 36 | resCode: "000000" 37 | resMsg: "success" 38 | nickName: "135***7378" 39 | # 落库校验 40 | check_db: -------------------------------------------------------------------------------- /cases/User_Coupon/Coupon.yaml: -------------------------------------------------------------------------------- 1 | ##################################### 2 | # Created By: Null 3 | # Created date: 2019-06-11 4 | # Desc: 优惠券 5 | ##################################### 6 | test_api_setup: 7 | relevant_parameter: [login] 8 | description: "member_extend_case" 9 | cases: ${login}$ 10 | # Response返回体提取的参数 11 | res_index: [token] 12 | 13 | test_coupon_success: 14 | # 关联的临时变量文件 15 | relevant_parameter: [token, Host] 16 | # 关联的Sql数据 17 | relevant_sql: [] 18 | # 是否跳过 19 | skip: False 20 | # 用例描述 21 | description: "优惠券-/v40/disCoupon/own" 22 | # 请求方式 23 | method: get 24 | # 请求路由 25 | url: ${Host}$/v40/disCoupon/own 26 | params: pageSize=10&pageNum=1&token=${token}$&status=1&OS=ANDROID&OsVersion=28&AppVersion=98&IMEI=866957038129841&mac=54B12180B787&androidID=79f05a5ad238e896&PublicLongitude=121.409303&PublicLatitude=31.172189&publicCityCode=021&appName=atzucheApp&deviceName=STF-AL00&publicToken=${token}$&AppChannelId=testmarket&AndroidId=79f05a5ad238e896&requestId=54B12180B7871560241379724&mem_no=201158709&schema=A 27 | timeout: 8 28 | # 请求头部 29 | headers: 30 | Content-Type: application/json; charset=utf-8 31 | User-Agent: Autoyol_98:Android_27|36722B4DB3C7E55D77375E490D3B5796D30A340002F1349E3B9F1CA3BF 32 | Accept: application/json;version=3.0;compress=false 33 | # 断言 34 | assert: 35 | status_code: 200 36 | resCode: "000000" 37 | resMsg: "success" 38 | # 断言Json中存在多个相同的key,独立出来进行断言以.分割 39 | assert_same_key: 40 | disCouponList.0.showPreferential: "¥200" 41 | disCouponList.1.showPreferential: "¥300" 42 | # 落库校验 43 | check_db: 44 | -------------------------------------------------------------------------------- /lib/utils/security.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | import base64 3 | 4 | 5 | def encrypt(key: bytes)-> bytes: 6 | r"""加密 7 | 8 | :Arg: 9 | - key: 加密的字符串, bytes object. 10 | 11 | :Usage: 12 | encrypt(b'hello') 13 | """ 14 | if isinstance(key, (bytearray, bytes)): 15 | return base64.b64encode(key) 16 | else: 17 | try: 18 | bytes_obj = bytes(key, encoding='utf-8') 19 | return base64.b64encode(bytes_obj) 20 | except TypeError: 21 | raise TypeError("argument should be a bytes-like object" 22 | "not %r" % key.__class__.__name__) from None 23 | 24 | 25 | def decryption(key: bytes) -> str: 26 | r"""解密 27 | 28 | :Args: 29 | - key: 解密的字符串, bytes object. 30 | 31 | :Usage: 32 | decryption(b'NTQ2NDY0MjY4QHFxLmNvbQ==') 33 | """ 34 | if isinstance(key, (bytearray, bytes)): 35 | return str(base64.b64decode(key), encoding='utf-8') 36 | else: 37 | try: 38 | bytes_obj = bytes(key, encoding='utf-8') 39 | return str(base64.b64decode(bytes_obj), encoding='utf-8') 40 | except TypeError: 41 | raise TypeError("argument should be a bytes-like object" 42 | "not %r" % key.__class__.__name__) from None 43 | 44 | 45 | def batch_decryption(keys): 46 | r"""批量解密 47 | 48 | :Args: 49 | - keys: 解密的数据, list object or tuple object. 50 | 51 | :Usage: 52 | batch_decryption(['MTkyLjE2OC4xNzAuMjQ=', 'MzMwNg==']) 53 | """ 54 | return dict(zip(keys, map(lambda content: decryption(bytes(content, encoding='utf-8')), keys.values())))\ 55 | if isinstance(keys, dict) else list(map(lambda item: decryption(bytes(item, encoding='utf-8')), keys)) 56 | -------------------------------------------------------------------------------- /config/xml_handler.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | import os 3 | from defusedxml.ElementTree import parse 4 | 5 | 6 | class MetaSingleton(type): 7 | 8 | __instances = {} 9 | 10 | def __call__(cls, *args, **kwargs): 11 | if cls not in cls.__instances: 12 | cls.__instances[cls] = super(MetaSingleton, cls).__call__(*args, **kwargs) 13 | return cls.__instances[cls] 14 | 15 | 16 | class XmlHandler(metaclass=MetaSingleton): 17 | 18 | def __init__(self, filename: str): 19 | self.filename = os.fspath(filename) 20 | self.baseFilename = os.path.abspath(filename) 21 | 22 | @property 23 | def _getroot(self) -> str: 24 | r"""获取Xml节点 25 | 26 | :Usage: 27 | _getroot() 28 | """ 29 | return parse(self.filename).getroot() 30 | 31 | def get_child(self, tag: str) -> dict: 32 | r"""获取Xml节点属性 33 | 34 | :Args: 35 | - tag: 根据tag取一个tag下对应的子属性, str object. 36 | 37 | :Usage: 38 | get_child('emailReceivers') 39 | """ 40 | elements = [] 41 | try: 42 | for child in self._getroot: 43 | if child.tag == tag: 44 | for grandchildren in child: 45 | elements.append(dict(**{grandchildren.tag: grandchildren.text})) 46 | return {elements[0]['type']: elements[1:]} 47 | except (IndexError, KeyError): 48 | raise IndexError("The element child key is exist ") 49 | 50 | @property 51 | def get_all_receivers(self) -> list: 52 | r"""以列表的方式返回接收人列表 53 | 54 | :Usage: 55 | get_all_receivers() 56 | """ 57 | receivers = [] 58 | for value in self.get_child('emailReceivers')['EmailReceivers']: 59 | for child in value.values(): 60 | receivers.append(child) 61 | return receivers 62 | -------------------------------------------------------------------------------- /.coverage: -------------------------------------------------------------------------------- 1 | !coverage.py: This is a private format, don't read it directly!{"lines":{"F:\\MeteorTears\\run.py":[2,3,4,5,6,7,8,9,12,13,14,15,16,17,18,44,45,20,21,22,25,26,27,28,29],"F:\\MeteorTears\\config\\__init__.py":[1],"F:\\MeteorTears\\config\\setting.py":[2,3,4,8,9,10,11,12,13,14,15,16,17,18,19,20,21,25,26,27,28,32,33,34,35,36,37,38,39,40,41,42,43,44,45,49,50,54,55,56,57],"F:\\MeteorTears\\config\\xml_handler.py":[2,3,6,8,10,16,18,22,23,32,52,67,68,11,12,19,20,13,42,43,44,30,45,46,47,48,60,61,62,63,64],"F:\\MeteorTears\\lib\\__init__.py":[1],"F:\\MeteorTears\\lib\\utils\\__init__.py":[1],"F:\\MeteorTears\\lib\\utils\\email.py":[2,3,4,5,6,7,8,11,13,28,29,41,61,82,83],"F:\\MeteorTears\\lib\\utils\\fp.py":[2,5,13,15,31,17,18,20,21,22,33],"F:\\MeteorTears\\lib\\public\\__init__.py":[1],"F:\\MeteorTears\\lib\\public\\logger.py":[2,3,4,5,6,7,8,10,12,13,14,15,16,17,18,19,22,23,25,29,33,38,40,41,42,43,46,47,48,49,52,53,55,64,68,72,77,79,92,107,117,123,128,137,130,134,138,139,140],"F:\\MeteorTears\\lib\\utils\\time_util.py":[2,5,26],"F:\\MeteorTears\\lib\\public\\case_manager.py":[2,3,4,5,6,7,9,12,14,18,27,28,29,49,79,86,88,89,15,16,19,20,21,22,23,90,56,57,80,81,82,83,103,106,109],"F:\\MeteorTears\\lib\\public\\load_cases.py":[2,3,4,5,6,7,10,12,15,24,25,34,54,56,59,66,67,13,41,42,22,51],"F:\\MeteorTears\\lib\\utils\\exceptions.py":[2,3,6,7],"F:\\MeteorTears\\lib\\public\\Recursion.py":[4,6,7,35,36,54,55],"F:\\MeteorTears\\lib\\public\\BeautifulReport.py":[1,2,3,4,5,6,7,8,9,10,11,13,20,23,24,26,29,32,35,39,27,40,42,45,46,48,49,50,51,52,53,57,58,59,62,63,65,73,82,101,102,104,131,132,136,137,145,160,169,181,207,216,217,225,245,267,289,309,319,320,332,333,335,342,363,386,387,400,336,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,337,338,339,340,350,351,353,354,356,357,358,187,134,188,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,359,368,370,371,373,374,375,376,377,384,378,379,380,381,382,383,360,361],"F:\\MeteorTears\\lib\\utils\\analyze_log.py":[2,3,4,5,6,7,8,11,13,14,15,17,18,29,44,56,57,71,72,94,95]}} -------------------------------------------------------------------------------- /cases/User_Favorites/Favorites.yaml: -------------------------------------------------------------------------------- 1 | ##################################### 2 | # Created By: Null 3 | # Created date: 2019-06-11 4 | # Desc: 收藏 5 | ##################################### 6 | test_api_setup001: 7 | relevant_parameter: [login] 8 | description: "member_extend_case001" 9 | cases: ${login}$ 10 | # Response返回体提取的参数 11 | res_index: [token] 12 | 13 | test_api_setup002: 14 | relevant_parameter: [favorites] 15 | description: "member_extend_case002" 16 | cases: ${favorites}$ 17 | # 断言 18 | assert: 19 | status_code: 200 20 | resCode: "000000" 21 | resMsg: "success" 22 | assert_same_key: 23 | carList.0.brandInfo: "奔驰B级(进口)" 24 | carList.1.brandInfo: "别克凯越" 25 | # 落库校验 26 | check_db: 27 | 28 | test_favorites_success: 29 | # 关联的临时变量文件 30 | relevant_parameter: [token, Host] 31 | # 关联的Sql数据 32 | relevant_sql: [] 33 | # 是否跳过 34 | skip: False 35 | # 用例描述 36 | description: "收藏-/v57/favorites" 37 | # 请求方式 38 | method: post 39 | # 请求路由 40 | url: ${Host}$/v57/favorites 41 | json: 42 | "AppChannelId": "testmarket" 43 | "schema": "A" 44 | "OS": "ANDROID" 45 | "appName": "atzucheApp" 46 | "OsVersion": "27" 47 | "mem_no": "819209698" 48 | "IMEI": "861438046958534" 49 | "AndroidId": "7ccde31ec3ec4990" 50 | "deviceName": "V1816A" 51 | "mac": "B40FB38790F3" 52 | "token": ${token}$ 53 | "AppVersion": 98 54 | "publicToken": ${token}$ 55 | "publicCityCode": "021" 56 | "requestId": "B40FB38790F31560755232507" 57 | "PublicLongitude": "121.409265" 58 | "androidID": "7ccde31ec3ec4990" 59 | "PublicLatitude": "31.172216" 60 | timeout: 8 61 | # 请求头部 62 | headers: 63 | Content-Type: application/json; charset=utf-8 64 | User-Agent: Autoyol_98:Android_27|36722B4DB3C7E55D77375E490D3B5796D30A340002F1349E3B9F1CA3BF 65 | Accept: application/json;version=3.0;compress=false 66 | # 断言 67 | assert: 68 | status_code: 200 69 | resCode: "000000" 70 | resMsg: "success" 71 | assert_same_key: 72 | carList.0.brandInfo: "奔驰B级(进口)" 73 | carList.1.brandInfo: "别克凯越" 74 | # 落库校验 75 | check_db: -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | import os 3 | import click 4 | import unittest 5 | import pysnooper 6 | from config import setting 7 | from lib.utils import time_util 8 | from lib.utils import email 9 | from lib.public.case_manager import TestContainer 10 | from lib.utils.analyze_log import WeChatAlarm 11 | from lib.utils.random_data import RandomData 12 | from lib.public.HtmlReport import Report 13 | 14 | 15 | @click.command() 16 | @click.option('--cases', default=setting.TEST_CASES, help="case file path") 17 | @click.option('--pattern', default='*.py', help="get cases file pattern") 18 | @click.option( 19 | '--report', 20 | default=setting.REPORT, 21 | help="generator report in path") 22 | @pysnooper.snoop() 23 | def run(cases=setting.TEST_CASES, pattern='*.py', report=setting.REPORT) -> None: 24 | r"""运行测试用例主入口 25 | 26 | :param cases: 运行的测试用例文件路径, str object. 27 | :param pattern: 匹配运行的测试用例文件, str object. 28 | :param report: 生成的测试报告路径, str object. 29 | """ 30 | 31 | # 生成随机测试数据 32 | RandomData.create_random_test_data() 33 | 34 | # 加载&运行测试用例 35 | test_suite = unittest.defaultTestLoader.discover(cases, pattern) 36 | result = Report(test_suite) 37 | result.report(filename='Report', description='Report', log_path=report) 38 | 39 | # 临时文件回溯 40 | def files_backtrack(filepath: list) -> None: 41 | if isinstance(filepath, list): 42 | for path in filepath: 43 | try: 44 | for files in os.listdir(path): 45 | filename, pattern_back_date = path + files, time_util.timestamp('format_day') 46 | if os.path.isfile(filename) and pattern_back_date not in files.split('.'): 47 | os.remove(filename) 48 | except PermissionError: 49 | pass 50 | 51 | back_track_files_path = [cases, setting.LOG] 52 | files_backtrack(back_track_files_path) 53 | 54 | # 发送邮件 55 | # send_mail = email.SendMail() 56 | # send_mail.send_mail() 57 | 58 | # 日志告警 59 | # push_msg = WeChatAlarm() 60 | # push_msg.send_message(push_msg.error_log_message()) 61 | 62 | 63 | if __name__ == '__main__': 64 | run() 65 | -------------------------------------------------------------------------------- /doc/Recording.md: -------------------------------------------------------------------------------- 1 | #### 接口录制V1.1.0 2 | 1. 通过抓包工具保存.har文件 3 | 2. 执行 python recording.py -r {录制文件路径} -n {生成用例命名} -p {生成用例保存路径} 4 | 3. V1.0.0 逻辑回溯全部废除 5 | 6 | 7 | #### 接口录制V1.0.0 8 | ```text 9 | File -》Save -》 (a) All sessions 以saz格式文件保存所有会话 10 | (b) Selected Sessions 保存选择的会话 11 | 1. in ArchiveZIP :保存为saz文件 12 | 2. in ArchiveZIP :保存为saz文件 13 | 3. as Text (Headers only) :仅保存头部 14 | (c) Request 保存请求 15 | 1. Entir Request:保存整个请求信息(headers和body) 16 | 2. Request Body:只保存请求body部分 17 | (d) Response 保存请求返回 18 | 1. Request Body:只保存请求body部分 19 | 2. Response Body:只保存返回body部分 20 | 3. Response Body:只保存返回body部分 21 | 22 | 返回Response结构体乱码 23 | 点击decode 24 | ``` 25 | 26 | #### 接口回放 27 | 1. File -》Load Archive 导入saz文件 28 | 2. Ctr + A 选择全部接口 29 | 3. 点击Replay按钮, 批量请求 30 | 31 | 32 | #### 修改CustomRules文件 33 | 1. 找到OnBeforeResponse方法 34 | 2. 添加如下代码 35 | ```javascript 36 | oSession.utilDecodeResponse(); 37 | var now = new Date(); 38 | var ts = now.getTime(); 39 | var filename = record + ts + '_' + oSession.id + '.yaml'; 40 | var curDate = new Date(); 41 | var logContent = "Request url: " + oSession.url + "\r\nRequest header: " + oSession.oRequest.headers + "\r\nRequest body: " + oSession.GetRequestBodyAsString() + "\r\nResponse code: " + oSession.responseCode + "\r\nResponse body: " + oSession.GetResponseBodyAsString() + "\r\n"; 42 | var sw : System.IO.StreamWriter; 43 | if (System.IO.File.Exists(filename)){ 44 | sw = System.IO.File.AppendText(filename); 45 | sw.Write(logContent); 46 | } 47 | else{ 48 | sw = System.IO.File.CreateText(filename); 49 | sw.Write(logContent); 50 | } 51 | sw.Close(); 52 | sw.Dispose(); 53 | ``` 54 | 3. C:\Users\56464\Documents\Fiddler2\Scripts\目录下最好先备份原文件,并命名CustomRulesBack.js 55 | 4. 录制的原始接口信息会保存在/WorkFlow/目录下 56 | 5. 录制完的接口为JSON格式文件, load_fiddler_files.py分析并生成新的迭代对象, create_workFlow_obj.py将生成新的Json格式用例文件, 57 | 58 | 59 | ### 注意事项 60 | 录制的接口采用不同的文件管理用例, 同时在不同的case层进行遍历, 但运行逻辑与报告生成方式与手动编写用例同 61 | -------------------------------------------------------------------------------- /config/setting.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | import os 3 | import yaml 4 | from config import xml_handler 5 | 6 | 7 | __all__ = [ 8 | 'CONTENT', 9 | 'HEADER', 10 | 'EMAIL', 11 | 'XML_CONFIG', 12 | 'TEST_CASES', 13 | 'EMAIL_CONF', 14 | 'DATA_BASE_CONF', 15 | 'WECHAT', 16 | 'CASES', 17 | 'API_PATH', 18 | 'TEST_SUITE', 19 | 'TEST_CASE', 20 | 'LOG', 21 | 'REPORT', 22 | 'CASES', 23 | 'CASE_DATA', 24 | 'ENV_DATA', 25 | 'RES', 26 | 'EXTRACT_PARAMS', 27 | 'CONFIG_PARAMS', 28 | 'INTERFACE_PARAMS', 29 | 'RANDOM_PARAMS' 30 | ] 31 | 32 | 33 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 34 | PRO_PATH = {} 35 | with open(BASE_DIR + '/config/path.yaml', 'r', encoding='utf-8') as file: 36 | PRO_PATH.update(yaml.safe_load(file)) 37 | 38 | 39 | # project PATH 40 | CONTENT = PRO_PATH['CONTENT'] 41 | HEADER = PRO_PATH['HEADER'] 42 | EMAIL = PRO_PATH['EMAIL'] 43 | XML_CONFIG = PRO_PATH['XML_CONFIG'] 44 | TEST_CASES = PRO_PATH['TEST_CASES'] 45 | WECHAT = PRO_PATH['WECHAT'] 46 | LOG = PRO_PATH['LOG'] 47 | REPORT = PRO_PATH['REPORT'] 48 | CASES = PRO_PATH['CASES'] 49 | API_PATH = PRO_PATH['API_PATH'] 50 | CASE_DATA = PRO_PATH['CASE_DATA'] 51 | ENV_DATA = PRO_PATH['ENV_DATA'] 52 | RES = PRO_PATH['RES'] 53 | EXTRACT_PARAMS = PRO_PATH['EXTRACT_PARAMS'] 54 | CONFIG_PARAMS = PRO_PATH['CONFIG_PARAMS'] 55 | INTERFACE_PARAMS = PRO_PATH['INTERFACE_PARAMS'] 56 | RANDOM_PARAMS = PRO_PATH['RANDOM_PARAMS'] 57 | TEST_CASE = PRO_PATH['TEST_CASE'] 58 | TEST_SUITE = PRO_PATH['TEST_SUITE'] 59 | 60 | 61 | # READ CONF 62 | BASE_CONF = xml_handler.XmlHandler(XML_CONFIG) 63 | BASE_EMAIL_CONF = BASE_CONF.get_child('emailSender')['EmailSender'] 64 | BASE_DATA_BASE_CONF = BASE_CONF.get_child('MySqlSetting')['MySQLTest9'] 65 | 66 | 67 | # EMAIL SETTING 68 | EMAIL_CONF = dict({ 69 | 'sendaddr_name': BASE_EMAIL_CONF[0]['sendaddr_name'], 70 | 'sendaddr_pswd': BASE_EMAIL_CONF[1]['sendaddr_pswd'] 71 | }, **{'receivers': BASE_CONF.get_all_receivers}) 72 | 73 | 74 | # DATA SETTING 75 | DATA_BASE_CONF = { 76 | 'host': BASE_DATA_BASE_CONF[0]['host'], 77 | 'port': int(BASE_DATA_BASE_CONF[1]['port']), 78 | 'user': BASE_DATA_BASE_CONF[2]['user'], 79 | 'passwd': BASE_DATA_BASE_CONF[3]['passwd'], 80 | 'db': BASE_DATA_BASE_CONF[4]['db'], 81 | 'charset': BASE_DATA_BASE_CONF[5]['charset'] 82 | } -------------------------------------------------------------------------------- /lib/public/Recursion.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | 4 | class GetJsonParams(object): 5 | 6 | @classmethod 7 | def get_value(cls, my_dict: dict, key: str) -> str: 8 | r"""解析一个嵌套字典,并获取指定key的值 9 | 10 | :Args: 11 | - my_dict: 解析的字典, dict object. 12 | - key: 指定解析的键, str object. 13 | 14 | :Usage: 15 | get_value({'hello': 'world'}, 'hello') 16 | """ 17 | 18 | if isinstance(my_dict, dict): 19 | if my_dict.get(key) or my_dict.get(key) == 0 or my_dict.get(key) == '' \ 20 | and my_dict.get(key) is False or my_dict.get(key) == []: 21 | return my_dict.get(key) 22 | 23 | for my_dict_key in my_dict: 24 | if cls.get_value(my_dict.get(my_dict_key), key) or \ 25 | cls.get_value(my_dict.get(my_dict_key), key) is False: 26 | return cls.get_value(my_dict.get(my_dict_key), key) 27 | 28 | if isinstance(my_dict, list): 29 | for my_dict_arr in my_dict: 30 | if cls.get_value(my_dict_arr, key) \ 31 | or cls.get_value(my_dict_arr, key) is False: 32 | return cls.get_value(my_dict_arr, key) 33 | 34 | @classmethod 35 | def get_same_content(cls, my_dict: dict, list_key: str, list_index: int, same_key: str) -> str: 36 | r"""解析一个嵌套字典中存在相同key的情况 37 | 38 | :Arg: 39 | - my_dict: 需要解析的字典, dict object. 40 | - list_key: 相同key存在的数组, str object. 41 | - list_index: 取数组中第几个个字典, int object. 42 | - same_key: 需要取值的KEY值, str object. 43 | 44 | :Usage: 45 | get_same_content(my_dict=my_dict, list_key='datalist', list_index=0, same_key='botName') 46 | """ 47 | return dict(cls.get_value(my_dict=my_dict, key=list_key)[list_index])[same_key] 48 | 49 | @classmethod 50 | def for_keys_to_dict(cls, *args: tuple, my_dict: dict) -> dict: 51 | r"""指定多个key,并获取一个字典的多个对应的key,组成一个新的字典 52 | 53 | :Arg: 54 | - args: 指定的key值, tuple object. 55 | - my_dict: 解析的字典, dict object. 56 | 57 | :Usage: 58 | for_keys_to_do_dict('hello', {'hello': 'hello'}) 59 | """ 60 | result = {} 61 | if len(args) > 0: 62 | for key in args: 63 | result.update({key: cls.get_value(my_dict, str(key))}) 64 | return result 65 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 11 | 12 | 13 | 14 | 15 | 21 | 22 | 26 | 27 | 28 | 29 | 35 | 36 | 37 | 38 | 39 | 45 | 46 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /doc/CaseWrite.md: -------------------------------------------------------------------------------- 1 | #### 用例编写(Yaml文件管理) 2 | ```yaml 3 | test_get_public_key: 4 | # 上下游关联的参数文件名 5 | relevant_parameter: [Host] 6 | # 此接口落库的sql语句 7 | relevant_sql: search_all_tenant_conf 8 | # 测试用例名称 9 | description: "获取公钥" 10 | # 请求方式 11 | method: get 12 | # 请求路由 13 | url: ${Host}$/api/auth/getpublickey 14 | # 接口断言 15 | assert: 16 | Code: 1 17 | # 提取测试接口Response返回参数 18 | res_index: [RsaPublicKey, Key] 19 | # 落库校验 20 | check_db: 21 | TenantName: TESTRLBC 22 | ``` 23 | key | value | example 24 | ------------------- | ------------------- | ---------------- 25 | url | 请求接口路由 | /admin/compaign/export 26 | method | 请求方式 | GET 27 | params | url地址参数 | channelId=123importId=456 28 | data | 请求数据 | {"name": "SEMAUTO", "categoryId": $arguments, "enabled": 1} 29 | file | 上传文件数据 | {file=operate_excel.save_excel(file=os.path.join(parameters.make_directory('Data', 0), 'excel\compaign_template.xlsx'),data_index=0,excel_key='落地页编号',excel_name='compaign_template_副本.xlsx')} 30 | json | Json类型请求 | {"name": "SEMAUTO", "categoryId": $arguments, "enabled": 1} 31 | headers | 请求头 | {'Authorization': 'eyJ0eXAiOiJK', 'Content-Type': 'application/json'} 32 | timeout | 超时时间 | timeout: 8 33 | setUp | 前置条件 | setUp: print('前置条件') 34 | tearDown | 后置条件 | tearDown: print('后置条件') 35 | skip | 用例跳过 | 布尔值False或者True 36 | assert | 结果断言 | {"username": "NULL", "password": "123456", "auth_code": ['len', 4]} 37 | responseType | 验证断言结果的数据类型 | {'Response': ['type', 'dict']} 38 | description | 用例描述 | "新增渠道" 39 | res_index | 提取变量 | res_index: [RsaPublicKey, Key] 40 | check_db | 落库检查 | check_db: {TenantName: TESTRLBC} 41 | relevant_parameter | 上下游接口关联参数 | relevant_parameter: [Host] 42 | relevant_sql | 需要检查的sql语句 | relevant_sql: search_all_tenant_conf 43 | jsonDiff | 接口自动对比 | jsonDiff: {Code:1, message: 成功} 44 | 45 | 46 | ##### 关于断言 47 | 1. 多层结果断言, 以键值对的方式写入, 断言的Key: 预期的Value 48 | 2. 返回体数据类型断言,整体返回提ResponseType:[type, dict], 断言某个Key的类型 Key: [type, str] 49 | 3. 返回结果长度断言, Key: [len, 36] 50 | ```text 51 | assert: 52 | code: 1 53 | username: Null 54 | password: 123456 55 | ResponseType: [ 56 | type, 57 | dict] 58 | username: [ 59 | type, 60 | str] 61 | password: [ 62 | len, 63 | 8] 64 | ``` 65 | -------------------------------------------------------------------------------- /lib/utils/excel_handler.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | import ast 3 | import os 4 | import xlrd 5 | import xlwt 6 | from config import setting 7 | 8 | 9 | def read_excel(file: str) -> list: 10 | r"""读取excel 11 | 12 | :Args: 13 | - file: 文件路径, str object. 14 | 15 | :Usage: 16 | read_excel(../data/test.j.xlsx) 17 | """ 18 | workbook = xlrd.open_workbook(file) 19 | table = workbook.sheet_by_index(0) 20 | rows, cols_name, list__ = table.nrows, table.row_values(0), [] 21 | 22 | for row in range(1, rows): 23 | data = table.row_values(row) 24 | 25 | if data: 26 | content = {} 27 | for index in range(len(cols_name)): 28 | content[cols_name[index]] = data[index] 29 | list__.append(content) 30 | 31 | return list__ 32 | 33 | 34 | def analyze_excel(file: str, data_index: int, excel_key: str) -> list: 35 | r"""分析表格中的事件, 并使用eval方法执行py代码 36 | 37 | :Args: 38 | - file: 文件路径, str object. 39 | - data_index: 指定数据的索引, int object. 40 | - excel_key: python代码数据对应的键, str object. 41 | 42 | :Usage: 43 | analyze_excel('..data/test.j.xlsx', 1, 'landing_page') 44 | """ 45 | 46 | data = read_excel(file)[data_index] 47 | function_obj = ast.literal_eval(data[excel_key]) 48 | 49 | dic = {} 50 | for key, value in dict(data).items(): 51 | dic[key] = value 52 | if key == excel_key: 53 | dic[key] = function_obj 54 | return [dic] 55 | 56 | 57 | def save_excel( 58 | file: str, 59 | data_index: int, 60 | excel_key: str, 61 | excel_name: str = 'copy_excel'): 62 | r"""分析表格后得到新的数据,并写入一份副本文件 63 | 64 | :Args: 65 | - file: 文件路径, str object. 66 | - data_index: 指定数据的索引, int object. 67 | - excel_key: python代码数据对应的键, str object 68 | - excel_name: 复制的文件名, 默认为copy_excel, str object. 69 | 70 | :Usage: 71 | save_excel('../data/test.j.xlsx', 2, 'landing_page', 'test.j-副本.xlsx') 72 | """ 73 | excel_copy = os.path.join(setting.CASE_DATA, 'copy_excel') 74 | workbook = xlwt.Workbook(encoding='utf-8') 75 | table, data = workbook.add_sheet(u"sheet1", cell_overwrite_ok=True), analyze_excel(file, data_index, excel_key) 76 | 77 | copy_data, k_data, v_data = [], [], [] 78 | for index, value in enumerate(data): 79 | for k, v in value.items(): 80 | k_data.append(k), v_data.append(v) 81 | copy_data.append(k_data) 82 | copy_data.append(v_data) 83 | 84 | for i, j in enumerate(copy_data): 85 | for q, p in enumerate(j): 86 | table.write(i, q, str(p)) 87 | workbook.save(os.path.join(excel_copy, excel_name)) 88 | -------------------------------------------------------------------------------- /cases/ApiLogin.yaml: -------------------------------------------------------------------------------- 1 | ##################################### 2 | # Created By: Null 3 | # Created date: 2019-06-05 4 | # Desc: 登录 5 | ##################################### 6 | test_api_login_success: 7 | # 关联的临时变量文件 8 | relevant_parameter: [Host] 9 | # 关联的Sql数据 10 | relevant_sql: [] 11 | # 是否跳过 12 | skip: False 13 | # 用例描述 14 | description: "登录成功-/v31/mem/action/login" 15 | # 请求方式 16 | method: post 17 | # 请求路由 18 | url: ${Host}$/v31/mem/action/login 19 | timeout: 8 20 | # 请求参数 21 | json: 22 | "AppChannelId": "testmarket" 23 | "OS": "ANDROID" 24 | "validCode": "111111" 25 | "loginType": "validCode" 26 | "appName": "atzucheApp" 27 | "OsVersion": "27" 28 | "mobile": "13564957378" 29 | "mem_no": "718160237" 30 | "IMEI": "" 31 | "AndroidId": "7ccde31ec3ec4990" 32 | "deviceName": "V1816A" 33 | "mac": "B40FB38790F3" 34 | "AppVersion": 98 35 | "publicToken": "0" 36 | "publicCityCode": "021" 37 | "requestId": "B40FB38790F31560751297632" 38 | "PublicLongitude": "0" 39 | "androidID": "7ccde31ec3ec4990" 40 | "PublicLatitude": "0" 41 | # 请求头部 42 | headers: 43 | Content-Type: application/json; charset=utf-8 44 | User-Agent: Autoyol_80:Android_21|CF336E2B0D0A361E679A6699A38A1576D3013004042034B17B72C7B188 45 | Accept: application/json;version=3.0;compress=false 46 | # Response返回体提取的参数 47 | res_index: [token] 48 | # 断言 49 | assert: 50 | status_code: 200 51 | resCode: "000000" 52 | resMsg: "success" 53 | # 落库校验 54 | check_db: 55 | 56 | 57 | test_login_fail: 58 | relevant_parameter: [Host] 59 | relevant_sql: [] 60 | skip: False 61 | description: "登录失败-/v31/mem/action/login" 62 | method: post 63 | url: ${Host}$/v31/mem/action/login 64 | timeout: 8 65 | json: 66 | AppChannelId: "testmarket" 67 | schema: "A" 68 | OS: "ANDROID" 69 | validCode: "111111" 70 | loginType: "validCode" 71 | appName: "atzucheApp" 72 | OsVersion: "28" 73 | mobile: "158008738061111111111111111111111111111111111111111111111" 74 | mem_no: null 75 | IMEI: "866957038129841" 76 | AndroidId: "79f05a5ad238e896" 77 | deviceName: "STF-AL00" 78 | mac: "54B12180B787" 79 | AppVersion: 98 80 | publicToken: "0" 81 | publicCityCode: "021" 82 | requestId: "54B12180B7871559714482129" 83 | PublicLongitude: "121.409095" 84 | androidID: "79f05a5ad238e896" 85 | PublicLatitude: "31.172282" 86 | headers: 87 | Content-Type: application/json; charset=utf-8 88 | Accept-Encoding: gzip 89 | User-Agent: Autoyol_80:Android_21|CF336E2B0D0A361E679A6699A38A1576D3013004042034B17B72C7B188 90 | Accept: application/json;version=3.0;compress=false 91 | assert: 92 | status_code: 200 93 | resCode: "000000" 94 | resMsg: "success" 95 | check_db: 96 | -------------------------------------------------------------------------------- /lib/public/text_similarity_comparison.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | import jieba 3 | from collections import Counter 4 | import numpy as np 5 | import math 6 | from lib.public import logger 7 | from config import setting 8 | from os import path 9 | 10 | 11 | def stopwords(seg_list: list) -> list: 12 | r"""过滤掉文本中的停用词, 停用词路径:/data/env_data/stopKeywords/ 13 | 14 | :Args: 15 | - seg_list: 使用JieBa分词后对待测试文本进行分词后得到的一个列表对象, list object. 16 | """ 17 | stayed_word = [] 18 | 19 | filter_keywords = open(path.join(setting.ENV_DATA, 'stopKeywords'), 'r', encoding='utf-8') 20 | stop_key = [line.strip() for line in filter_keywords.readlines()] 21 | for word in seg_list: 22 | if word not in stop_key: 23 | stayed_word.append(word) 24 | return stayed_word 25 | 26 | 27 | def count(res: str): 28 | r"""待测试文本进行分词后,统计得出词频。 29 | 30 | :Args: 31 | - res: 待测试文本关键字, str object. 32 | """ 33 | seg_list = list(jieba.cut(res)) 34 | seg_list = stopwords(seg_list) 35 | dic = Counter(seg_list) 36 | 37 | return (dic) 38 | 39 | 40 | def merge_word(expect: dict, res: dict) -> list: 41 | r"""关键词合并单词. 42 | 43 | :Args: 44 | - expect: 预期待测文本值, dict object. 45 | - res: Response返回预对比值, dict object. 46 | """ 47 | return list(set(list(expect.keys())).union(set(list(res.keys())))) 48 | 49 | 50 | def cal_vector(expect: dict, merge_word: list) -> list: 51 | r"""获取文本向量值 52 | 53 | :Args: 54 | - expect: 预期待测文本值,, dict object. 55 | - merge_word: 关键词合并单词., list object. 56 | """ 57 | vector = [] 58 | for ch in merge_word: 59 | if ch in expect: 60 | vector.append(expect[ch]) 61 | else: 62 | vector.append(0) 63 | return vector 64 | 65 | 66 | def cal_con_dis(v1: list, v2: list, length_vector): 67 | r"""计算余弦距离. 68 | 69 | :Args: 70 | - v1: 预期的向量, list object. 71 | - v2: 实际的向量, list object. 72 | - length_vector: 向量的长度, list object. 73 | """ 74 | a1 = np.asarray(v1) 75 | a2 = np.asarray(v2) 76 | A = math.sqrt(np.sum(a1**2)) * math.sqrt(np.sum(a1**2)) 77 | B = np.sum(a1 * a2) 78 | 79 | try: 80 | return format(float(B) / A, ".3f") 81 | except ZeroDivisionError: 82 | return 0 83 | 84 | 85 | def contrast_num(expected_knowledge, actual_knowledge) -> float: 86 | r"""得出计算对比度 87 | 88 | :Args: 89 | - expected_knowledge: 预期的待测试文本, str object. 90 | - actual_knowledge: 实际Response返回文本, str object. 91 | """ 92 | expect, res = count(expected_knowledge), count(actual_knowledge) 93 | merge = merge_word(expect, res) 94 | v1, v2 = cal_vector(expect, merge), cal_vector(res, merge) 95 | diff = round(float(cal_con_dis(v2, v1, len(merge))), 4) 96 | logger.log_info('{}{}{}'.format(expect, res, diff)) 97 | return diff 98 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | import io 3 | import os 4 | import sys 5 | from shutil import rmtree 6 | from setuptools import Command, find_packages, setup 7 | 8 | 9 | about = {} 10 | here = os.path.abspath(os.path.dirname(__file__)) 11 | with io.open(os.path.join(here, 'lib', '__about__.py'), encoding='utf-8') as f: 12 | exec(f.read(), about) 13 | 14 | with io.open("README.md", encoding='utf-8') as f: 15 | long_description = f.read() 16 | 17 | install_requires = [ 18 | 'requests', 19 | 'contextlib2', 20 | 'colorama', 21 | 'xlrd', 22 | 'xlwt', 23 | 'pyyaml', 24 | 'pymysql' 25 | ] 26 | 27 | 28 | class UploadCommand(Command): 29 | 30 | user_options = [] 31 | 32 | @staticmethod 33 | def status(s): 34 | print("\033[0;32m{0}\033[0m".format(s)) 35 | 36 | def initialize_options(self): 37 | pass 38 | 39 | def finalize_options(self): 40 | pass 41 | 42 | def run(self): 43 | try: 44 | self.status('Removing previous builds…') 45 | rmtree(os.path.join(here, 'dist')) 46 | except OSError: 47 | pass 48 | 49 | self.status('Building Source and Wheel (universal) distribution…') 50 | os.system('{0} setup.py sdist bdist_wheel --universal'.format(sys.executable)) 51 | 52 | self.status('Uploading the package to PyPi via Twine…') 53 | os.system('twine upload dist/*') 54 | 55 | self.status('Publishing git tags…') 56 | os.system('git tag v{0}'.format(about['__version__'])) 57 | os.system('git push --tags') 58 | 59 | sys.exit() 60 | 61 | 62 | setup( 63 | name=about['__title__'], 64 | version=about['__version__'], 65 | description=about['__description__'], 66 | long_description=long_description, 67 | long_description_content_type='text/markdown', 68 | author=about['__author__'], 69 | author_email=about['__author_email__'], 70 | url=about['__url__'], 71 | license=about['__license__'], 72 | python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4', 73 | packages=find_packages(exclude=["testhtml", "record", 'work_flow']), 74 | package_data={ 75 | '': ["README.md"], 76 | 'lib': ["templates/*"], 77 | }, 78 | keywords='HTTP api test.j requests', 79 | install_requires=install_requires, 80 | extras_require={}, 81 | classifiers=[ 82 | "Development Status :: 3 - Alpha", 83 | 'Programming Language :: Python :: 3.4', 84 | 'Programming Language :: Python :: 3.5', 85 | 'Programming Language :: Python :: 3.6', 86 | 'Programming Language :: Python :: 3.7' 87 | ], 88 | entry_points={ 89 | 'console_scripts': [ 90 | 'runner=run:run', 91 | ] 92 | }, 93 | cmdclass={ 94 | 'upload': UploadCommand 95 | } 96 | ) 97 | -------------------------------------------------------------------------------- /lib/utils/analyze_log.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | import yaml 3 | import json 4 | import re 5 | import os 6 | import requests 7 | from lib.public import logger 8 | from config import setting 9 | 10 | 11 | class WeChatAlarm(object): 12 | 13 | WeChat = {} 14 | with open(setting.WECHAT, 'r', encoding='utf-8') as file: 15 | WeChat.update(yaml.safe_load(file)['wechat']) 16 | 17 | @property 18 | def log_file(self) -> str: 19 | r"""获取最新的日志文件 20 | 21 | :Usage: 22 | log_file() 23 | """ 24 | log_list = os.listdir(setting.LOG) 25 | log_list.sort() 26 | return log_list[-1] 27 | 28 | def analyze_files(self) -> list: 29 | r"""分析日志信息,提取存在错误的信息 30 | 31 | :Usage: 32 | analyze_files() 33 | """ 34 | error_message = [] 35 | with open(setting.LOG + self.log_file, 'r', encoding='utf-8') as file: 36 | for line in file.readlines(): 37 | er, wr = re.compile(r'ERROR.*'), re.compile(r'WARN.*') 38 | if er.findall(line) or wr.findall(line): 39 | error_message.append(line) 40 | return error_message 41 | 42 | def error_log_message(self) -> str: 43 | r"""拼接错误信息 44 | 45 | :Usage: 46 | error_log_message() 47 | """ 48 | msg = '' + '\n' 49 | for value in self.analyze_files(): 50 | msg += value 51 | return msg 52 | 53 | @classmethod 54 | def get_token(cls) -> str: 55 | r"""获取微信公众号token信息 56 | 57 | :Usage: 58 | get_token() 59 | """ 60 | token_url = 'https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=%s&corpsecret=%s' \ 61 | % (cls.WeChat['corp'], cls.WeChat['secret']) 62 | response = requests.get(token_url, verify=True).text 63 | response_dict = json.loads(response) 64 | token = response_dict['access_token'] 65 | return token 66 | 67 | @classmethod 68 | def send_message(cls, message: str) -> None: 69 | r"""发送错误的日志信息给企业微信 70 | 71 | :Args: 72 | - message: 错误的日志信息, str object. 73 | """ 74 | send_url = (' https://qyapi.weixin.qq.com/cgi-bin/message/send?' 75 | 'access_token={}'.format(cls.get_token())) 76 | post_data = { 77 | "touser": "@all", 78 | "msgtype": "text", 79 | "agentid": 1000002, 80 | "text": { 81 | "content": message 82 | }, 83 | "safe": 0 84 | } 85 | res = requests.post(send_url, data=json.dumps(post_data), verify=True) 86 | logger.log_info("The Push WeChat message result is {}".format( 87 | json.dumps(res.json(), indent=4, ensure_ascii=False)) 88 | ) 89 | 90 | 91 | if __name__ == '__main__': 92 | pass 93 | -------------------------------------------------------------------------------- /lib/utils/use_MySql.py: -------------------------------------------------------------------------------- 1 | # # -*- coding:utf-8 -*- 2 | import yaml 3 | import types 4 | import pymysql 5 | from config import setting 6 | from lib.public.Recursion import GetJsonParams 7 | 8 | 9 | class ExecuteSQL(GetJsonParams): 10 | 11 | def __init__(self, connect_setting: dict = None): 12 | self.connect_setting = connect_setting 13 | self.conn = None 14 | self.cursor = None 15 | 16 | def __enter__(self): 17 | self.conn = pymysql.connect(**self.connect_setting) 18 | self.cursor = self.conn.cursor() 19 | return self 20 | 21 | def execute(self, *args, **kwargs) -> dict: 22 | r"""执行SQL语句 23 | 24 | :Args: 25 | - query: 查询执行语句 str object. 26 | - args: 用于执行的查询参数, tuple/list/dict object. 27 | 28 | :Usage: 29 | execute("SELECT count(*) FROM sqlite_master WHERE type='table' AND name='test_results'") 30 | """ 31 | self.conn = pymysql.connect(**self.connect_setting) 32 | self.cursor = self.conn.cursor(cursor=pymysql.cursors.DictCursor) 33 | self.cursor.execute(*args, **kwargs) 34 | return self.cursor.fetchall() 35 | 36 | @classmethod 37 | def loads_sql_data(cls, filename) -> types.GeneratorType: 38 | r"""加载SQL数据,并以字典的形式返回 39 | 40 | :Usage: 41 | loads_sql_data() 42 | """ 43 | with open(setting.CASE_DATA + filename, encoding='utf-8') as file: 44 | for class_name, body in yaml.safe_load(file).items(): 45 | if len(body) > 1: 46 | query_action = body['action'] 47 | table = cls.get_value(body['execSQL'], 'table') 48 | columns = cls.get_value(body['execSQL'], 'columns') 49 | params = cls.get_value(body['execSQL'], 'params') 50 | desc = cls.get_value(body['execSQL'], 'desc') 51 | yield { 52 | 'classname': class_name, 53 | 'action': query_action, 54 | 'table': table, 55 | 'columns': columns, 56 | 'params': params, 57 | 'desc': desc 58 | } 59 | 60 | def specify_file_execution_data(self, filename: str): 61 | for sql_content in iter(self.loads_sql_data(filename)): 62 | class_name = sql_content['class_name'] 63 | action = sql_content['action'] 64 | table = sql_content['sql'] 65 | columns = sql_content['columns'] 66 | params = sql_content['params'] 67 | desc = sql_content['desc'] 68 | 69 | def __exit__(self, exc_type, exc_val, exc_tb): 70 | if not exc_tb: 71 | self.conn.commit() 72 | self.cursor.close() 73 | del self.cursor 74 | self.conn.close() 75 | del self.conn 76 | else: 77 | self.conn.rollback() 78 | -------------------------------------------------------------------------------- /lib/public/relevance.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import re 3 | import ast 4 | 5 | 6 | def custom_manage(custom: str, relevance: dict) -> dict: 7 | r"""上下游接口文件管理配置 8 | 9 | :Args: 10 | - custom: 需要关联的接口用例数据, str object. 11 | - relevance: 以${key}$的形式进行匹配内容替换, dict object. 12 | 13 | FIXME:替换参数为INT类型时不支持,若该INT类型的参数为中间位置替换时,\n 14 | 会影响后面参数的正常替换 15 | """ 16 | if isinstance(custom, str): 17 | _re_custom = {} 18 | 19 | try: 20 | 21 | relevance_list = re.findall("\${(.*?)}\$", custom) 22 | for n in relevance_list: 23 | if isinstance(relevance[n], str): 24 | pattern = re.compile('\${' + n + '}\$') 25 | custom = re.sub(pattern, relevance[n], custom, count=1) 26 | _re_custom.update(ast.literal_eval(custom)) 27 | if isinstance(relevance[n], dict): 28 | _custom = ast.literal_eval(custom) 29 | for _custom_key, _custom_value in _custom.items(): 30 | if _custom[_custom_key] == '${' + n + '}$': 31 | _custom[_custom_key] = relevance[n] 32 | else: 33 | _custom[_custom_key] = _custom_value 34 | _re_custom.update(_custom) 35 | except TypeError: 36 | pass 37 | return _re_custom 38 | 39 | 40 | def extend_cases_manage(custom: str, relevance: dict) -> dict: 41 | r"""testsuite中入参变量替换数据 42 | 43 | :Args: 44 | - custom: 用例数据中存在需要入参的数据, str object. 45 | - relevance: 以${key}$的形式进行匹配内容替换, dict object. 46 | 47 | FIXME:替换变量不要添加int类型字符 48 | """ 49 | if isinstance(custom, str): 50 | _re_custom = {} 51 | 52 | try: 53 | 54 | relevance_list = re.findall("\$<(.*?)>\$", custom) 55 | for n in relevance_list: 56 | if isinstance(relevance[n], str): 57 | pattern = re.compile('\$<' + n + '>\$') 58 | custom = re.sub(pattern, relevance[n], custom, count=1) 59 | _re_custom.update(ast.literal_eval(custom)) 60 | if isinstance(relevance[n], dict): 61 | _custom = ast.literal_eval(custom) 62 | for _custom_key, _custom_value in _custom.items(): 63 | if _custom[_custom_key] == '$<' + n + '>$': 64 | _custom[_custom_key] = relevance[n] 65 | else: 66 | _custom[_custom_key] = _custom_value 67 | _re_custom.update(_custom) 68 | except TypeError: 69 | pass 70 | return _re_custom 71 | d = {'func_params': ['username'], 'method': 'post', 'url': '${Host}$/v31/mem/action/login', 'json': {'AppChannelId': 'testmarket', 'username': '$$'}, 'headers': {'Content-Type': 'application/json; charset=utf-8'}} 72 | c = {'username': '123213'} 73 | print(extend_cases_manage(str(c), d)) -------------------------------------------------------------------------------- /.idea/sonarlint/issuestore/index.pb: -------------------------------------------------------------------------------- 1 | 2 | = 3 | update_log.md,b\e\bef34275ebe3082d1d4188ad1239d4bdb072d867 4 | 9 5 | README.md,8\e\8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d 6 | C 7 | lib/public/wraps.py,9\4\948af7a24852e9101a97a505cc5d2c7f7215c1f4 8 | < 9 | Pipfile.lock,e\5\e563262209b7c2e62d1869582c8134583baa2204 10 | 7 11 | LICENSE,0\3\0398ccd0f49298b10a3d76a47800d2ebecd49859 12 | ? 13 | .coveralls.yaml,b\6\b68caa7969296d0b3c2aacfc5c318c77ad9909b4 14 | 9 15 | .coverage,2\4\245d8ec80d138d8104c8f8338a3e2066fd7680d1 16 | 6 17 | run.py,8\0\806d6ceb4e60342798038124ccf7dc1a423a4433 18 | F 19 | lib/utils/time_util.py,1\6\164c678e6d378cf12dd8e3cb6a30c3185297c7b9 20 | A 21 | config/setting.py,8\a\8a7407d4db016a33919dea5539250bf08d1984c1 22 | L 23 | lib/utils/test_api_server.py,9\7\971ffe2cfe5de59dedcc75bd7dd3d2c8ac428733 24 | B 25 | lib/utils/email.py,c\1\c1dcc7af870c6eb5f9f4f4f0df5f5fbfd6473c3a 26 | @ 27 | config/path.yaml,f\f\ff67f5e26d5537df4991daaba91b11ac175d99a6 28 | @ 29 | doc/Recording.md,5\1\513ce1337df4721f10b31ea9d8a835107c5e09dd 30 | @ 31 | doc/CaseWrite.md,3\b\3bded7c68bec1963a749dc0fe10b7bfcf4308d05 32 | @ 33 | doc/sql_write.md,8\2\82f79891e3bf44345b067a75df0a8be518db1707 34 | G 35 | lib/utils/exceptions.py,a\3\a30d2ea00a0b0fb916e646d0a0348241b916147a 36 | F 37 | lib/utils/recording.py,9\e\9ec347c8a29abfcd24a80ba7f2210db7587e7d0d 38 | ? 39 | EnvironClean.py,7\7\7724f7468e912ff2dd1a80fec9c2b08cb81dc151 40 | D 41 | lib/public/logger.py,5\4\546d86439a1e6b0422f4668c945760d64655294c 42 | K 43 | lib/public/http_keywords.py,5\2\52b47af5d579b450ae858594aa5a5dd852c9774b 44 | H 45 | lib/public/HtmlReport.py,7\7\77110284543a45bd51793617d602b8319f3a617f 46 | G 47 | lib/public/Recursion.py,8\a\8ad16cd4c99150fb0380181915587e15131fabab 48 | 7 49 | test.py,a\3\a36e355ddb203d7d4133221f339dc406cb9f480f 50 | H 51 | lib/public/load_cases.py,2\a\2abf93b307d81be8152f36d6cc58a419acf9cd8a 52 | G 53 | lib/public/relevance.py,9\f\9f34d8abf16d30cbcd3fb4f5b9be860e81f4f8c9 54 | 8 55 | setup.py,8\e\8e2edce0d507e1297474f25c00cae94258db38d8 56 | J 57 | lib/public/case_manager.py,9\e\9eaf2d352104ebeb622181d77c6cc66b358a6f3f 58 | G 59 | cases/test_api/api.yaml,6\8\680145bf822d7a7c3c60167b0194805d98fe167f 60 | O 61 | data/case_data/bot_profile.yaml,f\0\f0e55907ba3fb05286cf564d5b2f086c74165c69 62 | 9 63 | 1231.json,3\9\394b1b340616bc50a0344bde884e88e0fec06f5c 64 | I 65 | cases/test_cases/003.yaml,3\9\39b516f5c66ff881571ac0415f72cbc1d718d0fc 66 | G 67 | cases/test_api/002.yaml,6\1\615971ee12c261c813ff0a979ebdc70bab820df6 68 | J 69 | cases/test_cases/case.yaml,2\b\2b478aab3d84f444fc9377207f4fcd6c03c6164e 70 | < 71 | 3123213.json,2\3\23b3894140d1cb3b39352bcae9d05343e8801842 72 | D 73 | config/ApiLogin.yaml,6\f\6f91f90323ca52a713abfeafe21b2dcd3da6c5fa 74 | 9 75 | loader.py,e\6\e68c7038f2b85fbb069640297b92d55ed2d4c60a 76 | N 77 | config/User_Member/Member.yaml,7\d\7d8269534c714f45c6d62b96f014c7297a4d3366 78 | J 79 | config/User_Home/Home.yaml,f\2\f24dd59fa254b1ef278b97824870441ca2a16279 80 | N 81 | config/User_Coupon/Coupon.yaml,d\c\dcf91960d4da64d259a001c11904a44452cc9593 82 | T 83 | $config/User_Favorites/Favorites.yaml,4\a\4a81bd248b7ff07ca5f0947d2d826a41c454d35b -------------------------------------------------------------------------------- /lib/public/http_keywords.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | import ast 3 | import json 4 | import urllib3 5 | import requests 6 | import simplejson 7 | from urllib import parse 8 | from lib.public import logger 9 | from lib.utils import exceptions 10 | from requests import exceptions 11 | from lib.public.Recursion import GetJsonParams 12 | 13 | urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) 14 | 15 | 16 | class BaseKeyWords(GetJsonParams): 17 | 18 | def __init__(self, request_body: dict): 19 | self.request_body = request_body 20 | 21 | def post(self, **kwargs: dict) -> requests.Response: 22 | r"""发送POST请求。返回:class:`Response` object。 23 | 24 | :Args: 25 | - \*\*kwargs:: “session.post”接受的可选参数。 26 | 27 | :Usage: 28 | post(url='/admin/category/add', data={"name": "AUTO", "enabled": 1}) 29 | """ 30 | return requests.post(verify=False, **kwargs) 31 | 32 | def get(self, **kwargs: dict) -> requests.Response: 33 | r"""发送GET请求。返回:class:`Response` object。 34 | 35 | :Args: 36 | - \*\*kwargs:: “session.get”接受的可选参数 37 | 38 | :Usage: 39 | get(url='/admin/category/getNames') 40 | """ 41 | return requests.get(**kwargs, verify=False) 42 | 43 | def make_test_templates(self) -> dict: 44 | r"""创建测试用例的基础数据 45 | 46 | :Usage: 47 | make_test_templates() 48 | """ 49 | 50 | logger.log_debug(self.request_body) 51 | method = GetJsonParams.get_value(self.request_body, 'method') 52 | 53 | if method in ['get', 'GET']: 54 | temp = ('url', 'params', 'headers', 'timeout') 55 | request_body = GetJsonParams.for_keys_to_dict(*temp, my_dict=self.request_body) 56 | if request_body['params']: 57 | if '=' in request_body.get('params') or '&' in request_body.get('params'): 58 | request_body['params'] = dict(parse.parse_qsl(request_body['params'])) 59 | 60 | logger.log_info("接受GET的请求参数为{}".format( 61 | json.dumps(request_body, indent=4, ensure_ascii=False)) 62 | ) 63 | 64 | try: 65 | response = self.get(**request_body) 66 | try: 67 | response_body = response.json() 68 | except simplejson.JSONDecodeError: 69 | response_body = response.text 70 | return { 71 | "status_code": response.status_code, 72 | "response_body": response_body 73 | } 74 | except exceptions.Timeout as error: 75 | raise error 76 | 77 | if method in ['post', 'POST']: 78 | temp = ('url', 'headers', 'json', 'data', 'files', 'timeout') 79 | request_body = GetJsonParams.for_keys_to_dict(*temp, my_dict=self.request_body) 80 | 81 | logger.log_info("接受POST的请求参数为{}".format( 82 | json.dumps(request_body, indent=4, ensure_ascii=False)) 83 | ) 84 | 85 | try: 86 | response = self.post(**request_body) 87 | try: 88 | response_body = response.json() 89 | except simplejson.JSONDecodeError: 90 | response_body = response.text 91 | return { 92 | "status_code": response.status_code, 93 | "response_body": response_body 94 | } 95 | except exceptions.Timeout as error: 96 | raise error 97 | else: 98 | raise exceptions.TestApiMethodError("接口测试请求类型错误, 请检查相关用例!") 99 | -------------------------------------------------------------------------------- /lib/utils/email.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | import smtplib 3 | from lib.utils import fp 4 | from config import setting 5 | from lib.public import logger 6 | from email.header import Header 7 | from email.mime.text import MIMEText 8 | from email.mime.multipart import MIMEMultipart 9 | from lib.utils import resvalues 10 | 11 | 12 | class SendMail(object): 13 | 14 | def __init__(self, receiver=None, mode='rb'): 15 | # The recipient, list type, can be reassigned, 16 | # and if the recipient list is empty, the default sender is the 17 | # receiver. 18 | if receiver is None: 19 | if isinstance(setting.EMAIL_CONF['receivers'], list): 20 | if len(setting.EMAIL_CONF['receivers']) < 1: 21 | self.receiver = setting.EMAIL_CONF['sendaddr_name'] 22 | else: 23 | self.receiver = setting.EMAIL_CONF['receivers'] 24 | else: 25 | self.receiver = receiver 26 | self.mode = mode 27 | self.msg = MIMEMultipart() 28 | self.cons = open(setting.EMAIL, encoding='utf-8') 29 | self.con = self.cons.read() 30 | 31 | def make_email_template(self): 32 | r"""将各测试数据写入测试邮件模板 33 | """ 34 | content, cases_table = resvalues.get_report_values(), resvalues.write_cases_result() 35 | with open(setting.REPORT + 'email', 'w', encoding='utf-8') as file: 36 | file.write(self.con.format(content[0], content[1], content[2], content[3], cases_table)) 37 | 38 | @property 39 | def get_html_report(self) -> str: 40 | r"""获取测试报告路径 41 | 42 | :Usage: 43 | get_html_report() 44 | """ 45 | try: 46 | return setting.REPORT + 'Report.html' 47 | except FileNotFoundError: 48 | pass 49 | 50 | def email_content(self) -> None: 51 | r"""定义发送邮件的内容 52 | 53 | :Usage: 54 | email_content() 55 | """ 56 | self.msg['Subject'] = Header('SEM AUTO TEST REPORT', 'utf-8') 57 | self.make_email_template() 58 | with open(setting.REPORT + 'email', self.mode) as file: 59 | mail_body = file.read() 60 | self.msg.attach(MIMEText(mail_body, _subtype='html', _charset='utf-8')) 61 | att1 = MIMEText(open(self.get_html_report, self.mode).read(), 'base64', 'utf-8') 62 | att1["Content-Type"] = 'application/octet-stream' 63 | att1["Content-Disposition"] = 'attachment; filename="report.html"' 64 | self.msg.attach(att1) 65 | att2 = MIMEText(open(fp.iter_files(setting.LOG)[-1], self.mode).read(), 'base64', 'utf-8') 66 | att2["Content-Type"] = 'application/octet-stream' 67 | att2["Content-Disposition"] = 'attachment; filename="MyApiTest.log"' 68 | self.msg.attach(att2) 69 | 70 | def send_mail(self): 71 | r"""发送测试报告邮件 72 | """ 73 | self.email_content() 74 | self.msg['From'] = setting.EMAIL_CONF['sendaddr_name'] 75 | self.msg['To'] = ','.join(self.receiver) 76 | server = smtplib.SMTP_SSL('smtp.qq.com', 465) 77 | try: 78 | server.login( 79 | setting.EMAIL_CONF['sendaddr_name'], 80 | setting.EMAIL_CONF['sendaddr_pswd']) 81 | server.sendmail( 82 | setting.EMAIL_CONF['sendaddr_name'], 83 | self.receiver, 84 | self.msg.as_string()) 85 | logger.log_debug( 86 | 'Please check if the email has been sent successfully.') 87 | except smtplib.SMTPException as error: 88 | logger.log_warn(error) 89 | finally: 90 | server.quit() 91 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 63 | -------------------------------------------------------------------------------- /lib/utils/resvalues.py: -------------------------------------------------------------------------------- 1 | import re 2 | import os 3 | import types 4 | from requests_html import HTMLSession 5 | from requests_file import FileAdapter 6 | from lib.public.load_cases import LoadCase 7 | from lib.public.Recursion import GetJsonParams 8 | from config import setting 9 | 10 | 11 | def get_report_values() -> tuple: 12 | r"""获取测试报告中的数据值,用于传参与email中的数据统计 13 | """ 14 | session = HTMLSession() 15 | session.mount('file://', FileAdapter()) 16 | filepath = (os.path.join(setting.BASE_DIR, setting.REPORT, 'Report.html')).replace("\\", "/") 17 | html_obj = session.get(f'file:///{filepath}') 18 | test_pass_pattern = re.findall('"testPass": \d+,', html_obj.html.text)[0].split(':')[1].replace(',', '') 19 | test_all_pattern = re.findall('"testAll": \d+,', html_obj.html.text)[0].split(':')[1].replace(',', '') 20 | test_fail_pattern = re.findall('"testFail": \d+,', html_obj.html.text)[0].split(':')[1].replace(',', '') 21 | test_skip_pattern = re.findall('"testSkip": \d+,', html_obj.html.text)[0].split(':')[1].replace(',', '') 22 | return test_all_pattern, test_pass_pattern, test_fail_pattern, test_skip_pattern 23 | 24 | 25 | def case_values() -> types.GeneratorType: 26 | r"""获取测试用例中的基准数据,用于数据统计 27 | """ 28 | LD = LoadCase(setting.CASES) 29 | tags = LD.load_files() 30 | for items in tags: 31 | for class_name, body in items.items(): 32 | for key, value in body.items(): 33 | func_name = key 34 | description = GetJsonParams.get_value(value, 'description') 35 | method = GetJsonParams.get_value(value, 'method') 36 | url = GetJsonParams.get_value(value, 'url') 37 | yield { 38 | 'class_name': class_name, 39 | 'func_name': func_name, 40 | 'description': description, 41 | 'url': url, 42 | 'method': method 43 | } 44 | 45 | 46 | def get_cases_content_list() -> list: 47 | r"""返回测试用例数据,以list的形式返回 48 | :rtype: list object. 49 | """ 50 | cases_content_list = [] 51 | for case_content in iter(case_values()): 52 | cases_content_list.append(case_content) 53 | return cases_content_list 54 | 55 | 56 | HTML_BASE_TEMPLATE = """ 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | {} 69 |
IdTestSuiteTestCaseDescriptionUrlMethod
70 | 71 | 72 | """ 73 | 74 | 75 | HTML_TABLE_TEMPLATE = """ 76 | 77 | {} 78 | {} 79 | {} 80 | {} 81 | {} 82 | {} 83 | 84 | """ 85 | 86 | 87 | def write_cases_result() -> str: 88 | r"""将测试用例中基准数据生成表格HTML 89 | """ 90 | 91 | cases_result_template = '' 92 | for index, value in enumerate(get_cases_content_list()): 93 | class_name = value['class_name'] 94 | func_name = value['func_name'] 95 | description = value['description'] 96 | url = value['url'].split('$')[-1] 97 | method = value['method'] 98 | cases_result_template += HTML_TABLE_TEMPLATE.format( 99 | (index + 1), class_name, func_name, description, url, method 100 | ) 101 | return HTML_BASE_TEMPLATE.format(cases_result_template) 102 | -------------------------------------------------------------------------------- /loader.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | from os import path 3 | from config import setting 4 | from lib.utils import fp 5 | from lib.public import logger 6 | from lib.utils import exceptions 7 | from collections import Iterable 8 | from lib.public import relevance 9 | from lib.public.Recursion import GetJsonParams 10 | 11 | 12 | class Loader(object): 13 | 14 | def __init__(self, filepath: str = None): 15 | self.filepath = filepath 16 | 17 | @staticmethod 18 | def get_filename(pwd: str) -> list: 19 | r"""以列表的形式获取文件路径 20 | 21 | :Args: 22 | - pwd: 文件根目录, str object. 23 | :return: 以list的数据类型返回文件路径 24 | :rtype: list object. 25 | """ 26 | try: 27 | filename = fp.iter_files(pwd) 28 | return filename 29 | except exceptions.CaseYamlFileNotFound: 30 | logger.log_warn('目录文件不存在.') 31 | 32 | @staticmethod 33 | def get_yaml_content(filepath: str) -> list: 34 | r"""读取yaml文件中的内容,以列表的形式返回 35 | 36 | :Args: 37 | - filepath: yaml 文件路径, str object. 38 | :return: 以list的数据类型返回yaml文件中的数据 39 | :rtype: list object. 40 | """ 41 | try: 42 | with open(filepath, encoding='utf-8') as stream: 43 | yaml_content = yaml.safe_load(stream) 44 | return yaml_content 45 | except exceptions.CaseYamlFileNotFound: 46 | logger.log_warn('读取的yaml用例文件 -> {} 不存在,请查看'.format(filepath)) 47 | 48 | def classification_cases(self) -> dict: 49 | r"""对测试数据进行类别分类, 并对yaml文件中存在继承关系的文件路径\n 50 | 替换成实际的数据。 51 | 52 | :return: 返回处理完成的测试用例集 53 | :rtype: dict object. 54 | """ 55 | 56 | def get_case_content(filepath: str) -> tuple: 57 | r"""获取遍历文件内容,以tuple类型数据返回. 58 | 59 | :Args: 60 | - filepath: 遍历文件路径 61 | """ 62 | collection = {} 63 | path = self.get_filename(filepath) 64 | if isinstance(path, Iterable): 65 | for yaml_path in path: 66 | classname = yaml_path.split('/')[-1].split('.')[0] 67 | yaml_content = self.get_yaml_content(yaml_path) 68 | collection.update({classname: yaml_content}) 69 | return classname, collection 70 | 71 | """加载api文件数据""" 72 | api_name, api_content = get_case_content(setting.API_PATH) 73 | 74 | """加载case文件数据""" 75 | case_name, case_content = get_case_content(setting.TEST_CASE) 76 | 77 | # 对case中存在依赖关系的文件路径,替换成实际用例数据. 78 | for case_filename, cases in case_content.items(): 79 | for index, case in enumerate(cases): 80 | for func_name, func_value in case.items(): 81 | for body_key, body_value in func_value.items(): 82 | if body_key == 'testapi': 83 | be_related = body_value.split('/')[-1].split('.')[0] 84 | func_value[body_key] = api_content[be_related] 85 | else: 86 | func_value[body_key] = body_value 87 | return case_content 88 | 89 | def sub_case_func_params(self): 90 | cases = self.classification_cases() 91 | for classname, case_content in cases.items(): 92 | for func in case_content: 93 | for func_name, func_content in func.items(): 94 | 95 | request_body = GetJsonParams.get_value(func_content['testapi'][0], 'request_body') 96 | func_params = dict(request_body).get('func_params') 97 | 98 | if func_params: 99 | if isinstance(func_params, list): 100 | 101 | _relevance = {} 102 | for func_param in func_params: 103 | _relevance.update({func_param: GetJsonParams.get_value(func, 'params_kwargs')}) 104 | 105 | relevance_body = relevance.extend_cases_manage(str(request_body), _relevance) 106 | print(request_body) 107 | 108 | 109 | if __name__ == '__main__': 110 | l = Loader() 111 | l.sub_case_func_params() 112 | -------------------------------------------------------------------------------- /lib/public/logger.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | import six 3 | import abc 4 | import logging 5 | from config import setting 6 | from colorama import Fore, init 7 | from lib.utils import time_util 8 | from logging.handlers import TimedRotatingFileHandler 9 | 10 | init(autoreset=True) 11 | 12 | CRITICAL = 50 13 | FATAL = CRITICAL 14 | ERROR = 40 15 | WARNING = 30 16 | WARN = WARNING 17 | INFO = 20 18 | DEBUG = 10 19 | NOTSET = 0 20 | 21 | 22 | @six.add_metaclass(abc.ABCMeta) 23 | class Color(object): 24 | 25 | @abc.abstractmethod 26 | def get_color_by_str(self, color_str): 27 | pass 28 | 29 | @abc.abstractmethod 30 | def get_all_colors(self): 31 | pass 32 | 33 | @abc.abstractmethod 34 | def get_color_set(self): 35 | pass 36 | 37 | 38 | class ColorConfig(Color): 39 | 40 | FORE_GROUND_DEBUG = 'yellow' 41 | FORE_GROUND_INFO = 'green' 42 | FORE_GROUND_ERROR = 'red' 43 | FORE_GROUND_WARN = 'cyan' 44 | 45 | __COLOR = { 46 | 'DEBUG': FORE_GROUND_DEBUG, 47 | 'INFO': FORE_GROUND_INFO, 48 | 'ERROR': FORE_GROUND_ERROR, 49 | 'WARN': FORE_GROUND_WARN, 50 | } 51 | 52 | __COLORS = __COLOR.keys() 53 | __COLOR_SET = set(__COLOR) 54 | 55 | @classmethod 56 | def get_color_by_str(cls, color_str): 57 | if not isinstance(color_str, str): 58 | raise TypeError('the color type is not str {type}'.format(type=type(color_str))) 59 | color = str(color_str).upper() 60 | if color not in cls.__COLOR: 61 | raise KeyError('the color key {key} is not in color dic'.format(key=color)) 62 | return cls.__COLOR[color] 63 | 64 | @classmethod 65 | def get_all_colors(cls): 66 | return cls.__COLORS 67 | 68 | @classmethod 69 | def get_color_set(cls): 70 | return cls.__COLOR_SET 71 | 72 | @classmethod 73 | def get_colors_dic(cls): 74 | return cls.__COLOR 75 | 76 | 77 | class Logger(logging.Logger): 78 | 79 | def __init__(self, filemode='a', encoding='utf-8', level=DEBUG, stream=True, file=True): 80 | self.filename = setting.LOG + '{}.log'.format(time_util.timestamp('format_day')) 81 | self.mode = filemode 82 | self.encoding = encoding 83 | self.now = time_util.timestamp('format_now') 84 | self.level = level 85 | super(Logger, self).__init__(self.now, level=level) 86 | self.file_handler = None 87 | if stream: 88 | self.set_stream_handler() 89 | if file: 90 | self.set_file_handler() 91 | 92 | def set_stream_handler(self, level=None): 93 | 94 | file_handler = TimedRotatingFileHandler(filename=self.filename, when='D', interval=1, 95 | backupCount=15, encoding=self.encoding) 96 | file_handler.suffix = '%Y-%m-%d-%H_%M_%S.log' 97 | if not level: 98 | file_handler.setLevel(self.level) 99 | else: 100 | file_handler.setLevel(level) 101 | formatter = logging.Formatter("[%(levelname)s] [%(asctime)s] %(filename)s [line:%(lineno)d] : %(message)s") 102 | 103 | file_handler.setFormatter(formatter) 104 | self.file_handler = file_handler 105 | self.addHandler(file_handler) 106 | 107 | def set_file_handler(self, level=None): 108 | stream_handler = logging.StreamHandler() 109 | formatter = logging.Formatter("[%(levelname)s] [%(asctime)s] %(filename)s [line:%(lineno)d] : %(message)s") 110 | stream_handler.setFormatter(formatter) 111 | if not level: 112 | stream_handler.setLevel(self.level) 113 | else: 114 | stream_handler.setLevel(level) 115 | self.addHandler(stream_handler) 116 | 117 | def reset_name(self, name): 118 | self.name = name 119 | self.removeHandler(self.file_handler) 120 | self.set_file_handler() 121 | 122 | 123 | def coloring(msg, color="GREEN"): 124 | fore_color = getattr(Fore, color.upper()) 125 | return fore_color + msg 126 | 127 | 128 | def log_with_color(level, flag=False): 129 | 130 | def wrapper(msg): 131 | color = ColorConfig.get_colors_dic()[str(level).upper()] 132 | getattr(Logger(), level)(coloring(msg, color)) if flag else getattr(Logger(), level)(msg) 133 | 134 | return wrapper 135 | 136 | 137 | log_info = log_with_color('info') 138 | log_debug = log_with_color('debug') 139 | log_warn = log_with_color('warn') 140 | log_error = log_with_color('error') 141 | -------------------------------------------------------------------------------- /lib/utils/recording.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | import yaml 3 | import json 4 | import click 5 | from os import path 6 | from urllib.parse import unquote 7 | 8 | 9 | def loads_recording_file_content(filepath: str): 10 | r"""加载抓包工具保存的文件并获取request数据 11 | 12 | :Args: 13 | - filepath: 录制文件路径地址, str object. 14 | """ 15 | if path.exists(filepath): 16 | with open(filepath, "r+", encoding="utf-8") as f: 17 | try: 18 | content_json = json.loads(f.read()) 19 | return content_json["log"]["entries"] 20 | except (KeyError, TypeError): 21 | pass 22 | 23 | 24 | RECORDING_CONTENT = lambda path: loads_recording_file_content(path) 25 | 26 | 27 | def _method(filepath: str) -> str: 28 | r"""抓包保存的录制文件中的request-method元素 29 | 30 | :Args: 31 | - filepath: 录制文件路径地址, str object. 32 | """ 33 | return RECORDING_CONTENT(filepath)[0]['request']['method'] 34 | 35 | 36 | def _url(filepath: str) -> str: 37 | r"""抓包保存的录制文件中的request-url元素 38 | 39 | :Args: 40 | - filepath: 录制文件路径地址, str object. 41 | """ 42 | return RECORDING_CONTENT(filepath)[0]['request']['url'] 43 | 44 | 45 | def _params(filepath: str) -> dict: 46 | r"""抓包保存的录制文件中的request-params元素 47 | 48 | :Args: 49 | - filepath: 录制文件路径地址, str object. 50 | """ 51 | request_data = RECORDING_CONTENT(filepath)[0]['request'].get("postData", {}) 52 | if _method(filepath) in ['POST']: 53 | return { 54 | 'json': json.loads(request_data.get("text")) 55 | } 56 | else: 57 | if _url(filepath).find('?'): 58 | return {} 59 | else: 60 | params = RECORDING_CONTENT(filepath)[0]['request'].get('queryString', []) 61 | post_data = { 62 | item["name"]: item.get("value") 63 | for item in params 64 | } 65 | if isinstance(post_data, dict): 66 | converted = [] 67 | for key, value in post_data.items(): 68 | converted.append('{}={}'.format(key, value)) 69 | return { 70 | 'params': '&'.join(converted) 71 | } 72 | 73 | 74 | def _headers(filepath: str) -> dict: 75 | r"""抓包保存的录制文件中的request-headers元素 76 | 77 | :Args: 78 | - filepath: 录制文件路径地址, str object. 79 | """ 80 | origin_list = RECORDING_CONTENT(filepath)[0]['request']['headers'] 81 | return { 82 | item["name"]: item.get("value") 83 | for item in origin_list 84 | } 85 | 86 | 87 | def _status_code(filepath: str) -> int: 88 | r"""抓包保存的录制文件中的response-status_code元素 89 | 90 | :Args: 91 | - filepath: 录制文件路径地址, str object. 92 | """ 93 | return RECORDING_CONTENT(filepath)[0]['response']['status'] 94 | 95 | 96 | def generate_case_data(filepath: str, func_name: str, func_path: str) -> None: 97 | r"""生成.yaml的测试用例. 98 | 99 | :Args: 100 | - filepath: 录制文件路径地址, str object. 101 | - func_name: 用例名称&用例描述, str object. 102 | - func_path: 用例保存路径, str object. 103 | """ 104 | request_body = dict( 105 | { 106 | 'relevant_parameter': [], 107 | 'relevant_sql': [], 108 | 'skip': False, 109 | 'description': func_name, 110 | 'method': _method(filepath), 111 | 'url': _url(filepath), 112 | 'timeout': 8, 113 | 'headers': _headers(filepath), 114 | 'res_index': [], 115 | 'assert': { 116 | 'status_code': _status_code(filepath) 117 | } 118 | }, 119 | **_params(filepath) 120 | ) 121 | func_body = { 122 | func_name: request_body 123 | } 124 | with open(path.join(func_path, func_name + '.yaml'), 'w', encoding='utf-8') as file: 125 | yaml.dump(func_body, file, allow_unicode=True, default_flow_style=False, indent=4) 126 | print('录制的接口文件生成用例成功 -> {}'.format(path.join(func_path, func_name + '.yaml'))) 127 | 128 | 129 | @click.command() 130 | @click.option('--r', default=None, help="recording file path") 131 | @click.option('--n', default=None, help="test case func name") 132 | @click.option( 133 | '--p', 134 | default=None, 135 | help="generate test case in path") 136 | def main(r: str, n: str, p: str) -> None: 137 | r"""接口录制模块命令行运行主入口 138 | 139 | :Arg: 140 | - r: 抓包工具保存下来的接口录制源文件, str object. 141 | - n: 生成用例的名称以及用例描述, str object. 142 | - p: 生成接口用例存放路径, str object. 143 | """ 144 | generate_case_data(r, n, p) 145 | 146 | 147 | if __name__ == '__main__': 148 | main() 149 | -------------------------------------------------------------------------------- /lib/public/case_manager.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | import os 3 | import types 4 | from config import setting 5 | from lib.public import logger 6 | from lib.public import load_cases 7 | from lib.public.Recursion import GetJsonParams 8 | 9 | LD = load_cases.LoadCase(setting.CASES) 10 | 11 | 12 | class CreateCase(GetJsonParams): 13 | 14 | def __init__(self): 15 | self.headers = None 16 | self.content = None 17 | 18 | def __enter__(self): 19 | self.headers = open(setting.HEADER, encoding='utf-8') 20 | self.header = self.headers.read() 21 | self.content = open(setting.CONTENT, encoding='utf-8') 22 | self.cont = self.content.read() 23 | return self 24 | 25 | def make_headers_and_contents( 26 | self, 27 | class_name: str, 28 | setup: str, 29 | teardown: str, 30 | skip: str, 31 | func_name: str, 32 | description: str) -> None: 33 | r"""创建用例文件 34 | 35 | :Args: 36 | - classname: 测试用例文件名、等同于转化成py文件的类名(test suite), str object. 37 | - setup: 测试用例前置条件, str object. 38 | - teardown: 测试用例后置条件, str object. 39 | - func_name: 具体的测试用例、等同于转化成py文件的函数名(test case), str object. 40 | - skip: 测试用例是否跳过, str object. 41 | - description: 测试用例的用例描述, str object. 42 | 43 | :Usage: 44 | make_headers_and_contents('Channel', 'add_channel', '新增渠道') 45 | """ 46 | filename = setting.TEST_CASES + class_name + '.py' 47 | if not os.path.exists(filename): 48 | with open(filename, 'w', encoding='utf-8') as file: 49 | file.write(self.header.format(class_name, class_name, setup, teardown)) 50 | 51 | with open(filename, 'a', encoding='utf-8') as file: 52 | file.write(self.cont.format(skip, func_name, description)) 53 | 54 | def create_template(self) -> types.GeneratorType: 55 | r"""通过上下文管理器读取yaml测试用例加载测试模板,自动生成unittest .py形式的case 56 | 57 | :Usage: 58 | create_template() 59 | """ 60 | # tags = LD.sub_case_func_params() 61 | tags = LD.load_files() 62 | for items in tags: 63 | for class_name, body in items.items(): 64 | if len(body): 65 | for key, value in body.items(): 66 | func_name = key 67 | description = self.get_value(value, 'description') 68 | skip = self.get_value(value, 'skip') 69 | setup = self.get_value(value, 'setUp') 70 | teardown = self.get_value(value, 'tearDown') 71 | body = value 72 | yield load_cases.Containers({ 73 | 'class_name': class_name, 74 | 'func_name': func_name, 75 | 'description': description, 76 | 'skip': bool(skip), 77 | 'setup': setup, 78 | 'teardown': teardown, 79 | 'body': body 80 | }), self.make_headers_and_contents 81 | else: 82 | for func, _body in body.items: 83 | yield load_cases.Containers({ 84 | 'class_name': class_name, 85 | 'func_name': func, 86 | 'description': self.get_value(_body, 'description'), 87 | 'skip': bool(self.get_value(_body, 'skip')), 88 | 'setup': self.get_value(_body, 'setUp'), 89 | 'teardown': self.get_value(_body, 'tearDown'), 90 | 'body': _body 91 | }) 92 | 93 | def __exit__(self, exc_type, exc_val, exc_tb): 94 | self.headers.close() 95 | del self.header 96 | self.content.close() 97 | del self.cont 98 | 99 | 100 | class TestContainer: 101 | 102 | cases = [] 103 | with CreateCase() as file: 104 | for items in file.create_template(): 105 | obj, func = items 106 | func( 107 | obj.crop['class_name'], 108 | obj.crop['setup'], 109 | obj.crop['teardown'], 110 | obj.crop['skip'], 111 | obj.crop['func_name'], 112 | obj.crop['description'] 113 | ) 114 | cases.append(obj.crop) 115 | logger.log_info( 116 | "测试用例已自动生成完毕, 文件: {}.py -> 具体测试用例:{}".format( 117 | obj.crop['class_name'], 118 | obj.crop['func_name'])) 119 | 120 | def __iter__(self): 121 | return iter(self.cases) 122 | 123 | def __next__(self): 124 | return next(self.cases) 125 | 126 | def __repr__(self): 127 | return self.cases 128 | -------------------------------------------------------------------------------- /lib/templates/email: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 12 | 52 | 53 | 54 | 61 | 62 | 63 | 83 | 84 | {} 85 | 86 | 107 | 108 | 109 | 112 | 113 |
4 | 5 | 6 | 7 | 8 |
9 |
13 | 14 | 15 | 16 | 17 | 18 | 21 | 22 | 23 | 26 | 27 | 28 | 29 | 30 | 31 | 34 | 35 | 36 | 39 | 40 | 41 | 44 | 45 | 46 | 49 | 50 |
Hello All:
19 | 本次为考勤系统项目接口测试邮件,已经测试完毕(附件为本次测试报告) 20 |
24 | 如有问题,及时反馈!谢谢! 25 |
注意事项:
32 | 1. 邮件为每日定点发送, 如有打扰抱歉! 33 |
37 | 2. 如发现存在失败的用例,请及时检查! 38 |
42 | 3. 运行用例皆为框架自动生成, 如有纰漏及时检查。 43 |
47 | 4. 测试框架在不断升级ing, 如有使用上的问题请及时联系Null. 48 |
51 |
55 | 56 | 57 | 58 | 59 |
结果数据统计如下:
60 |
64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 |
序号用例名称用例总数用例通过用例失败用例跳过
1Report{}{}{}{}
82 |
87 | 88 | 89 | 90 | 93 | 94 | 95 | 98 | 99 | 100 | 103 | 104 | 105 |
91 | 如有任何问题或需要帮助,欢迎随时与我联系。 92 |
96 | 联系人:Null 97 |
101 | QQ:546464268 102 |
106 |
110 | Copyright© MeteorTearsTeam 111 |
114 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Meteor tears 2 | 3 | [![LICENSE](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/xiaoxiaolulu/MeteorTears/blob/master/LICENSE) [![python version](https://img.shields.io/badge/python-3.4%7C3.5%7C3.6%7C3.7-blue.svg)](https://pypi.org/project/MeteorTears/) [![Build Status](https://travis-ci.org/xiaoxiaolulu/MeteorTears.svg?branch=master)](https://travis-ci.org/xiaoxiaolulu/MeteorTears) [![Coverage Status](https://coveralls.io/repos/github/xiaoxiaolulu/MeteorTears/badge.svg?branch=master)](https://coveralls.io/github/xiaoxiaolulu/MeteorTears?branch=master) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/bfd93c4e2362409da23ee48826d1ad39)](https://www.codacy.com?utm_source=github.com&utm_medium=referral&utm_content=xiaoxiaolulu/MeteorTears&utm_campaign=Badge_Grade) 4 | 5 | 6 | Meteor tears 一款基于python-request通过Yaml格式文件管理用例的接口测试工具 7 | 8 | 9 | #### 项目特点如下 10 | 1. 数据管理使用Yaml文件 11 | 2. 用例编写使用Yaml文件 12 | 3. 支持上下游接口参数关联,提取 13 | 4. 接口Response返回体多字段, Type断言, len断言, 对比 14 | 5. 落库校验,支持多个字段 15 | 6. 接口录制功能(待完善) 16 | 7. 微信,邮件告警 17 | 18 | 19 | #### 用例(示例) 20 | ```yaml 21 | test_update_bot_baseinfo: 22 | relevant_parameter: [Host, Token] 23 | relevant_sql: [bot_profile, bot_prs] 24 | description: "更新机器人" 25 | method: post 26 | url: ${Host}$/api/admin/bot/botprofile/updatebotbaseinfo 27 | json: 28 | BotConfigId: 8e0b6707-bcc6-4c4c-b072-80b169003804 29 | Bot_Name: Null 30 | Bot_Gender: 女 31 | Bot_DayOfBirth: "2019-03-08" 32 | Bot_Constellation: 双鱼座 33 | Bot_BloodType: AB 34 | Bot_Birthplace: 上海-上海 35 | Bot_Height: 165 36 | Bot_Weight: 50 37 | Bot_Company: 骨灰级 38 | Bot_School: 上海有限公司 39 | ID: 273d8a2a-9b0e-4582-b13b-0a60f103f621 40 | CreateDate: "" 41 | UpdateDate: "" 42 | CreateUserId: "" 43 | CreateUserName: "" 44 | UpdateUserId": "" 45 | UpdateUserName: "" 46 | headers: 47 | Content-Type: application/json; charset=utf-8 48 | Access-Token: ${Token}$ 49 | assert: 50 | Status: 1 51 | Data: true 52 | check_db: 53 | Bot_Name: test 54 | Bot_Constellation: 水瓶座 55 | ``` 56 | key | value | example 57 | ------------------- | ------------------- | ---------------- 58 | url | 请求接口路由 | /admin/compaign/export 59 | method | 请求方式 | GET 60 | params | url地址参数 | channelId=123importId=456 61 | data | 请求数据 | {"name": "SEMAUTO", "categoryId": $arguments, "enabled": 1} 62 | file | 上传文件数据 | {file=operate_excel.save_excel(file=os.path.join(parameters.make_directory('Data', 0), 'excel\compaign_template.xlsx'),data_index=0,excel_key='落地页编号',excel_name='compaign_template_副本.xlsx')} 63 | json | Json类型请求 | {"name": "SEMAUTO", "categoryId": $arguments, "enabled": 1} 64 | headers | 请求头 | {'Authorization': 'eyJ0eXAiOiJK', 'Content-Type': 'application/json'} 65 | timeout | 超时时间 | timeout: 8 66 | setUp | 前置条件 | setUp: print('前置条件') 67 | tearDown | 后置条件 | tearDown: print('后置条件') 68 | skip | 用例跳过 | 布尔值False或者True 69 | assert | 结果断言 | {"username": "NULL", "password": "123456", "auth_code": ['len', 4]} 70 | responseType | 验证断言结果的数据类型 | {'Response': ['type', 'dict']} 71 | description | 用例描述 | "新增渠道" 72 | res_index | 提取变量 | res_index: [RsaPublicKey, Key] 73 | check_db | 落库检查 | check_db: {TenantName: TESTRLBC} 74 | relevant_parameter | 上下游接口关联参数 | relevant_parameter: [Host] 75 | relevant_sql | 需要检查的sql语句 | relevant_sql: search_all_tenant_conf 76 | jsonDiff | 接口自动对比 | jsonDiff: {Code:1, message: 成功} 77 | 78 | ##### 用例解耦 79 | 1. 继承临时Api文件进行用例场景组合 80 | ```text 81 | test_api_setup001: 82 | relevant_parameter: [login] 83 | description: "member_extend_case001" 84 | cases: ${login}$ 85 | # Response返回体提取的参数 86 | res_index: [token] 87 | ``` 88 | 89 | ##### 关于断言 90 | 1. 多层结果断言, 以键值对的方式写入, 断言的Key: 预期的Value 91 | 2. 返回体数据类型断言,整体返回提ResponseType:[type, dict], 断言某个Key的类型 Key: [type, str] 92 | 3. 返回结果长度断言, Key: [len, 36] 93 | 4. 断言中存在多层嵌套情况使用.进行分割取值,如:数组.索引.需要取值的key,若超出取值索引报IndexError 94 | ```text 95 | assert: 96 | code: 1 97 | username: Null 98 | password: 123456 99 | ResponseType: [ 100 | type, 101 | dict] 102 | username: [ 103 | type, 104 | str] 105 | password: [ 106 | len, 107 | 8] 108 | assert_same_key: 109 | disCouponList.0.showPreferential: "¥200" 110 | ``` 111 | 112 | ##### 落库校验 113 | 1. 用例头写入关联的sql文件 relevant_sql: [bot_profile, bot_prs] 114 | ```text 115 | relevant_sql: [bot_profile, bot_prs] 116 | check_db: 117 | Bot_Name: test 118 | Bot_Constellation: 水瓶座 119 | ``` 120 | 121 | 122 | #### Mysql执行语句编写 123 | ```yaml 124 | - ChannelBudget: 125 | action: SELECT 126 | execSQL: 127 | - table: shopping 128 | - columns: ['id'] 129 | - params: id='1' 130 | - desc: ORDER BY id DESC LIMIT 1 131 | ``` 132 | 133 | key | value | Sample 134 | ------------ | -------------| ---------------- 135 | action | sql执行操作类 | SELECT/DELETE/INSERT/UPDATE等 136 | table | 数据库表 | channel_budget 137 | columns | 列名 | ['channel_id'] 列表类型,支持多个值 138 | params | 检索条件 | id='1' 139 | desc | 排序 | ORDER BY ID DESC LIMIT 1 140 | 141 | 142 | 欢迎交流 QQ: 546464268(Null) 143 | -------------------------------------------------------------------------------- /report/email: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 12 | 52 | 53 | 54 | 61 | 62 | 63 | 83 | 84 | 85 | 86 | 126 | 127 | 128 | 129 | 150 | 151 | 152 | 155 | 156 |
4 | 5 | 6 | 7 | 8 |
9 |
13 | 14 | 15 | 16 | 17 | 18 | 21 | 22 | 23 | 26 | 27 | 28 | 29 | 30 | 31 | 34 | 35 | 36 | 39 | 40 | 41 | 44 | 45 | 46 | 49 | 50 |
Hello All:
19 | 本次为考勤系统项目接口测试邮件,已经测试完毕(附件为本次测试报告) 20 |
24 | 如有问题,及时反馈!谢谢! 25 |
注意事项:
32 | 1. 邮件为每日定点发送, 如有打扰抱歉! 33 |
37 | 2. 如发现存在失败的用例,请及时检查! 38 |
42 | 3. 运行用例皆为框架自动生成, 如有纰漏及时检查。 43 |
47 | 4. 测试框架在不断升级ing, 如有使用上的问题请及时联系Null. 48 |
51 |
55 | 56 | 57 | 58 | 59 |
结果数据统计如下:
60 |
64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 |
序号用例名称用例总数用例通过用例失败用例跳过
1Report 5 2 3 0
82 |
87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 |
IdTestSuiteTestCaseDescriptionUrlMethod
1Apilogintest_api_login_success登录成功-/v31/mem/action/login/v31/mem/action/loginpost
2Apilogintest_login_fail登录失败-/v31/mem/action/login/v31/mem/action/loginpost
3Coupontest_coupon_success优惠券-/v40/disCoupon/own/v40/disCoupon/ownget
125 |
130 | 131 | 132 | 133 | 136 | 137 | 138 | 141 | 142 | 143 | 146 | 147 | 148 |
134 | 如有任何问题或需要帮助,欢迎随时与我联系。 135 |
139 | 联系人:Null 140 |
144 | QQ:546464268 145 |
149 |
153 | Copyright© MeteorTearsTeam 154 |
157 | -------------------------------------------------------------------------------- /lib/public/load_cases.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | import yaml 3 | from os import path 4 | from lib.utils import fp 5 | from lib.public import logger 6 | from lib.utils import exceptions 7 | from config import setting 8 | from collections import Iterable 9 | from lib.public.Recursion import GetJsonParams 10 | from lib.public import relevance 11 | 12 | 13 | TestMap = { 14 | "api": [], 15 | "cases": [], 16 | 'PWD': setting.BASE_DIR 17 | } 18 | 19 | ExtendCaseMap = {} 20 | 21 | get_value = lambda seq, key: GetJsonParams.get_value(seq, key) 22 | 23 | 24 | class LoadCase(object): 25 | 26 | def __init__(self, path: str = None): 27 | self.path = path 28 | 29 | def get_all_files(self) -> list: 30 | r"""返回文件目录路径下全部文件列表 31 | 32 | :Usage: 33 | get_all_files() 34 | """ 35 | return fp.iter_files(self.path) 36 | 37 | @property 38 | def __get_files_name(self) -> list: 39 | r"""返回文件目录下的文件名 40 | 41 | :Usage: 42 | __get_files_name 43 | """ 44 | return fp.iter_files(self.path, otype='name') 45 | 46 | @staticmethod 47 | def load_yaml_file(filepath) -> dict: 48 | r"""加载并读取.yaml格式文件 49 | 50 | :Args: 51 | - filepath: yaml文件路径, str object. 52 | """ 53 | with open(filepath, encoding='utf-8') as stream: 54 | file_content = yaml.safe_load(stream) 55 | return file_content 56 | 57 | def load_file(self, file_path: str) -> dict: 58 | r"""加载单个.yaml测试用例文件 59 | 60 | :Args: 61 | - file_path: yaml文件路径, str object. 62 | """ 63 | if not path.isfile(file_path): 64 | raise exceptions.CaseYamlFileNotFound("{} does not exist.".format(file_path)) 65 | 66 | file_suffix = path.splitext(file_path)[1].lower() 67 | if file_suffix in ['.yaml', '.yml']: 68 | return self.load_yaml_file(file_path) 69 | else: 70 | err_msg = u"Unsupported file format: {}".format(file_path) 71 | logger.log_error(err_msg) 72 | return {} 73 | 74 | def load_files(self) -> list: 75 | r"""加载cases目录下的用例文件 76 | 77 | :Usage: 78 | load_files() 79 | """ 80 | files_list = [] 81 | for index, file in enumerate(self.get_all_files()): 82 | class_name = self.__get_files_name[index].split('.')[0].title().replace('_', '') 83 | try: 84 | with open(file, encoding='utf-8') as f: 85 | files_list.append({class_name: yaml.safe_load(f)}) 86 | except exceptions.JsonLoadingError as err: 87 | logger.log_error( 88 | "Json file parsing error, error file: {0}, error message: {1}".format( 89 | file, err)) 90 | return files_list 91 | 92 | def classification_cases(self) -> tuple: 93 | r"""加载cases文件夹下的用例数据,并对数据进行分类 94 | """ 95 | 96 | for yaml_content in self.load_files(): 97 | _extend_cases_path = get_value(yaml_content, 'testcases') 98 | _extend_api_path = get_value(yaml_content, 'apipath') 99 | 100 | # if _extend_cases_path: 101 | # TestMap['suites'].append(yaml_content) 102 | # testcase_path = path.join( 103 | # TestMap['PWD'], 104 | # *_extend_cases_path.split('/') 105 | # ) 106 | # test_dict = self.load_file(testcase_path) 107 | # ExtendCaseMap[_extend_cases_path] = test_dict 108 | 109 | if _extend_api_path: 110 | TestMap['cases'].append(yaml_content) 111 | testapi_path = path.join( 112 | TestMap['PWD'], 113 | *_extend_api_path.split('/') 114 | ) 115 | test_dict = self.load_file(testapi_path) 116 | ExtendCaseMap[_extend_api_path] = test_dict 117 | if not _extend_cases_path and not _extend_api_path: 118 | TestMap['api'].append(yaml_content) 119 | 120 | return TestMap, ExtendCaseMap 121 | 122 | def _case_loads_inherit_case(self) -> tuple: 123 | r"""对于测试用例中调用或有继承关系的yaml文件,将testapi字段的文件路径替换成具体的用例数据. 124 | """ 125 | cases = {} 126 | cases_set, extend_cases = self.classification_cases() 127 | for tags in cases_set['cases']: 128 | 129 | for class_name, content in tags.items(): 130 | for case_name, value in content.items(): 131 | extend_case_flag = get_value(tags, 'extend_api') 132 | if extend_case_flag: 133 | 134 | _relevance = {} 135 | for extend_case in extend_case_flag: 136 | extend_case_path = setting.API_PATH + extend_case + '.yaml' 137 | with open(extend_case_path, encoding='utf-8') as file: 138 | _relevance.update(yaml.safe_load(file)) 139 | relevance_body = relevance.custom_manage(str(value), _relevance, '2') 140 | cases.update({class_name: {case_name: relevance_body}}) 141 | return cases, extend_cases 142 | # return cases, extend_cases 143 | # inks, ouks, sub_key, sub_dict = [], [], [], {} 144 | # for case in dict(cases_set)['cases']: 145 | # if isinstance(case, Iterable): 146 | # for keys in dict(case).keys(): 147 | # ouks.append(keys)+++ 148 | # for cases_keys in dict(case)[keys]: 149 | # inks.append(cases_keys) 150 | # 151 | # for ink in inks: 152 | # sub_key.append(dict(GetJsonParams.get_value(cases_set, ink))['testapi']) 153 | # 154 | # for key, value in dict(extend_cases).items(): 155 | # if key in sub_key: 156 | # sub_dict.update(value) 157 | # 158 | # try: 159 | # for sui_index in range(len(cases_set['cases'])): 160 | # for index, ink in enumerate(inks): 161 | # cases_set['cases'][sui_index][ouks[index]][ink]['testapi'] = sub_dict 162 | # except (KeyError, ValueError): 163 | # pass 164 | 165 | # return cases_set, extend_cases 166 | 167 | def sub_case_func_params(self): 168 | r""" 将继承case中需要入参的参数替换params_keys中的各个值 169 | 入参: 170 | params_kwargs: 171 | - username: tracy.liu 172 | - password: 12345678 173 | 传参: 174 | func_params: [username, password] 175 | json: 176 | ${username}$ 177 | ${password}$ 178 | """ 179 | ncases = { 180 | 'cases': [ 181 | ] 182 | } 183 | api_case_set, extend_cases = self._case_loads_inherit_case() 184 | func_params = GetJsonParams.get_value(extend_cases, 'func_params') 185 | params_kwargs = GetJsonParams.get_value(api_case_set['cases'], 'params_kwargs') 186 | if isinstance(func_params, Iterable) and isinstance(params_kwargs, Iterable): 187 | 188 | for case in api_case_set['cases']: 189 | _relevance = {} 190 | for func_param in func_params: 191 | _relevance.update({func_param: dict(params_kwargs)[func_param]}) 192 | 193 | relevance_body = relevance.custom_manage(str(case), _relevance, '1') 194 | ncases['cases'].append(relevance_body) 195 | 196 | return ncases['cases'] 197 | 198 | 199 | class Containers(object): 200 | 201 | def __init__(self, crop: dict): 202 | self.crop = crop 203 | 204 | def __repr__(self): 205 | return "Containers <{}->{}>".format( 206 | self.crop.get('class_name'), 207 | self.crop.get('func_name') 208 | ) 209 | -------------------------------------------------------------------------------- /update_log.md: -------------------------------------------------------------------------------- 1 | ### 2019-06-28 2 | 1. 对测试用例数据执行前进行预处理 3 | ```示例 4 | 测试用例分层结构: 5 | 1.其中testapi目录不会执行 6 | 2.api文件和csase文件调用 7 | a.testapi 8 | 以$$方式代表入参方式 9 | testapi: cases\testapi\api.yaml -> [一个yaml文件对应一个api] 10 | 入参方式: parmas_kwargs: 11 | user: tary.liu 12 | psw: 123456 13 | b.testsuite 14 | testcase: cases\testcaes\case.yaml -> [一个yaml文件对应一个case文件] 15 | suite一般调用正向的测试cases,通过组装不同的case形成不同的测试场景 16 | 3. 最后处理完数据后无序加载测试容器,执行逻辑运行逻辑不变 17 | 加载数据以[{filename: {casename: casebody}}, {filename: {casename: casebody}}] 18 | 转化成py文件对应的数据: 19 | filename => 文件名&类名 20 | casename => 用例函数名 21 | casebody => 用例运行装饰器处理的测试数据,包含requestbody&断言等 22 | 23 | cases----------------testapi 24 | | 25 | |------------testcases 26 | | 27 | |------------testsuites 28 | ``` 29 | 30 | ### 2019-06-27 31 | 1. 优化lib.public.load_cases,对测试用例数据进行分类 32 | ```示例 33 | a.用例之间的调用以testcases: cases\testcaes\login.yaml进行调用 34 | b.被调用的测试用例以.py函数入参形式调用 status -> TODO 35 | parmas_kwargs: 36 | - username: tracy.liu 37 | - password: 546464628 38 | 39 | 传统形式:<封装固定方法进行调用> 40 | def login(username, password): 41 | res = request.post(host, {user: username, psw: password}, headers) 42 | self.assertEqual(res.status_code: 200) 43 | self.assertEqual(res.json()[user], username) 44 | 45 | cases---------testcases 46 | | 47 | |------testsuites 48 | ``` 49 | 50 | 51 | ### 2019-06-26 52 | 1. 修复lib.utils.recording._params(filepath)录制Get类型接口 53 | ```示例 54 | coupon: 55 | assert: 56 | status_code: 200 57 | description: coupon 58 | headers: 59 | Accept: application/json;version=3.0;compress=false 60 | Accept-Encoding: gzip 61 | Connection: Keep-Alive 62 | Content-Type: application/json; charset=utf-8 63 | Host: test2-appserver.atzc.com:7065 64 | User-Agent: Autoyol_99:Android_27|91AB99D0EDA542DCC966AABD2A0C5BB8D30A34D40031FF9E3B9F1CA3BF 65 | X-Tingyun-Id: YfYbInNBhKA;c=2;r=1295516226;u=24e923be2321c04dc7b49754eae43b7f::839528A9D7D98C34 66 | method: GET 67 | relevant_parameter: [] 68 | relevant_sql: [] 69 | res_index: [] 70 | skip: false 71 | timeout: 8 72 | url: https://test2-appserver.atzc.com:7065/v40/disCoupon/own?pageSize=10&pageNum=1&token=61527c61c92946d58fe1b0934e84613f&status=1&OS=ANDROID&OsVersion=27&AppVersion=99&IMEI=861438046958534&mac=B40FB38790F3&androidID=7ccde31ec3ec4990&PublicLongitude=121.409244&PublicLatitude=31.172197&publicCityCode=021&appName=atzucheApp&deviceName=V1816A&publicToken=61527c61c92946d58fe1b0934e84613f&AppChannelId=testmarket&AndroidId=7ccde31ec3ec4990&requestId=B40FB38790F31561518520965&mem_no=819209698 73 | ``` 74 | 75 | 76 | ### 2019-06-25 77 | 1. 接口录制模块完善, del老版code, 新增Recording module 78 | ```示例(注意事项逻辑未必稳定,待进一步测试) 79 | python recording.py --r=C:\Users\56464\Desktop\Untitled.har --n=test --p=C:\Users\56464\Desktop\MeteorTears 80 | 81 | 参数: 82 | --r 抓包工具保存下来的录制文件路径 83 | --n 生成测试用例名称命名 84 | --p 生成测试用例保存文件路径 85 | ``` 86 | 87 | 88 | ### 2019-06-24 89 | 1. 优化临时文件目录结构 90 | ```示例 91 | 现在分四层 92 | variables---------config_params (Host等常量配置文件目录) 93 | | 94 | |-----extract_params (接口提取参数保存文件目录) 95 | | 96 | |-----interface_params (Data/Json等接口参数保存文件目录) 97 | | 98 | |-----random_params (随机参数变量保存文件目录) 99 | ``` 100 | 2. setUp & tearDown涉及负责多条数据处理时,使用EnvironClean模块进行函数编程,再在yamlCase中调用数据处理函数 101 | ```示例 102 | EnvironPreparation: 103 | setUp: EnvironClean.marketing_partner_operation() 104 | ``` 105 | 106 | 107 | ### 2019-06-20 108 | 1. 优化wraps.cases_runner逻辑,现支持临时变量文件中的变量替换 109 | 2. 针对用例存在耦合的情况,以临时变量文件的方式继承api请求文件,以确保无论用例执行顺序如何,都能单独运行 110 | ```示例 111 | test_api_setup001: 112 | relevant_parameter: [login] 113 | description: "member_extend_case001" 114 | cases: ${login}$ 115 | # Response返回体提取的参数 116 | res_index: [token] 117 | ``` 118 | 119 | 120 | ### 2019-06-13 121 | 1. 加入Response文本对比功能,关键词json_diff 122 | 2. 优化邮件模板,新增现有测试用例统计元素 123 | 3. 优化部分文件注释 124 | ```示例 125 | json_diff: 126 | { 127 | "status_code": 200, 128 | "response_body": { 129 | "data": { 130 | "disCouponList": [ 131 | { 132 | "id": "466667", 133 | "disName": "我不知道", 134 | "endDate": "2019-06-30 00:00", 135 | "description": "1. 满100000000减2000\n2. 有效期:2019.06.12-2019.06.30\n3. 仅抵扣租金", 136 | "overlaidType": "0", 137 | "status": "1", 138 | "isFirstLimit": "0", 139 | "showPreferential": "¥2000" 140 | }, 141 | { 142 | "id": "466665", 143 | "disName": "12", 144 | "endDate": "2019-06-30 00:00", 145 | "description": "1. 满1000减200\n2. 有效期:2019.06.11-2019.06.30\n3. 仅抵扣租金", 146 | "overlaidType": "0", 147 | "status": "1", 148 | "isFirstLimit": "0", 149 | "showPreferential": "¥200" 150 | } 151 | ], 152 | "count": "2", 153 | "totalPage": "1" 154 | }, 155 | "resCode": "000000", 156 | "resMsg": "success" 157 | } 158 | } 159 | ``` 160 | 161 | 162 | ### 2019-06-12 163 | 1. 报告失败重跑htmlReport, 暂时不稳定,请暂时使用HtmlReport_back 164 | 2. 优化多层嵌套字典中存在相同Key取值的逻辑断言, 以.分割 165 | ```示例 166 | assert_same_key: 167 | disCouponList.0.showPreferential: "¥2000" 168 | ``` 169 | 170 | 171 | ### 2019-06-06 172 | 1. 优化报告逻辑,新增失败重跑功能 173 | 174 | 175 | ### 2019-06-02 176 | 1. 项目结构目录调整 177 | 2. 优化Response返回结构体 178 | 179 | 180 | ### 2019-05-17 181 | 1. 优化email模板,新增测试数据统计功能 182 | 2. 优化用例格式,新增前后置条件编写 183 | 184 | 185 | ### 2019-05-16 186 | 1. 新增自动生成随机测试数据功能 187 | 188 | 189 | ### 2019-05-14 190 | 1. 新增用例跳过功能 191 | 192 | 193 | ### 2019-05-13 194 | 1. 更新断言逻辑,新增断言Response指定字段长度 195 | 2. 更新落库校验逻辑,支持多字段断言 196 | 3. 更新部分文档 197 | 198 | 199 | ### 2019-05-09 200 | 1. 修复部分参数文件&配置文件错误 201 | 2. 重构部分代码 202 | 3. 数据库操作由Mysql改为Sqlserver 203 | 4. 增加数据落库校验 204 | 5. SQL语句使用Yaml文件编写 205 | 6. TODO 代码与校验逻辑待优化,暂只支持一个字段的落库校验 206 | 207 | 208 | ### 2019-03-27 209 | 1.完成关联参数配置 210 | 211 | 212 | ### 2019-03-22 213 | 1.用例管理文件格式变更为Yaml json.load =》yaml.load,方便contextor更优雅的实现传参方式 214 | 2.添加res_index方便动态传参,保存入临时变量文件(用例执行完自动回溯) 215 | ```text 216 | 用例中的变量名以{临时变量文件名}的方式书写 217 | 1. 用例编写文件目录cases,生成临时yaml用例文件目录caseAll, test_cases根据caseAll自动生成py用例文件,并执行 218 | ``` 219 | 220 | ### 2018-12-26 221 | 1. 搭建travis-ci + coveralls 222 | 2. 使用 coverage 223 | 3. setup.py done 224 | 225 | 226 | ### 2018-12-24 227 | 1. 修改CustomRules文件代码,使Fiddler能自动保存会话进指定的目录 228 | 2. 分析录制接口文件并生成新的request的对象 229 | 3. 生成录制接口的用例数据对象, 运行逻辑采用之前的手动编写用例规则 230 | 4. 完成V1.3.0 TODO, 接口性能与用例模型待补充 231 | 232 | 233 | ### 2018-12-14 234 | 1. 完成Email模板,样式已调试完毕 235 | 2. 用例基本可跑批 236 | 3. 查看项目代码, 使用FIXME对待完善代码就行标签注释(共计14个TODO) status -> 代码风格保持一致性,部分代码待优化美观 237 | 238 | 239 | ### V1.3.0 TODO 240 | 1. 对Fiddler进行改造使之能够完成接口录制功能,python对录制接口分析并自动运行 status -> done 逻辑变更,老版代码回溯删除 241 | 242 | 243 | ### V1.2.0 TODO 244 | 1. 使用locust库二次开发,完成接口性能测试 status -> 待研发 245 | 246 | 247 | ### V1.1.0 TODO 248 | 1. 测试模型设计并编写完毕 status -> done 249 | 2. 服务器部署, 接入Jenkins, 持续集成 status -> done 250 | 251 | 252 | ### V1.0.0 TODO 253 | 1. 项目配置层代码优化, 当前代码过于繁琐复杂 status -> done 254 | 2. 测试用例的校验器设计[Response结构体分析->返回数据类型 code等多重断言] status -> done 255 | 3. 测试模板的重新定义, 使之功能更为全面 status -> done 256 | 4. 测试阶段,编写用例测试,框架逻辑是否有所遗漏 status -> done 257 | 5. 第一版本基本用例编写功能完成 status -> done 258 | 6. 后期完善之前老代码,重构告警机制代码 status ->done 259 | 260 | 261 | ### 2018-12-13 262 | 1. 完成用例的基本运行编写 263 | 2. 生成测试报告 264 | 3. 检查当前所有代码是否符合PEP8代码规范标准 265 | 266 | 267 | ### 2018-12-12 268 | 1. 测试模板设计与编写 269 | 2. 用例生成主体逻辑 270 | 3. Json文件用例编写设计 271 | 4. Https/Http关键字模块编写 272 | 5. 代码PEP8编码规范检查并修改 273 | 274 | 275 | ### 2018-12-11 以前的日志忘写了,写一下现阶段完成的Modules 276 | 1. Xml配置文件类, 配置层基本完成 277 | 2. Mysql操作类(本次使用Yaml文件管理数据操作), 包含一个装饰器, 业务数据层基本完成 278 | 3. 数据安全模式, 一个简单加密解密类 279 | 4. fp文件操作采用以前的代码 280 | 5. Excel操作类, 支持动态参数, 沿用先前的代码 281 | 6. 多层嵌套Json解析操作类 282 | 7. 日志操作类, 依旧沿用以前的代码 283 | 8. 时间日期方法, 沿用先前代码 284 | 9. 完善了代码风格与注释 285 | 10. 明确了代码的基本分层与风格 286 | -------------------------------------------------------------------------------- /lib/public/wraps.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | import ast 3 | import yaml 4 | import json 5 | import builtins 6 | from os import path 7 | from os.path import exists 8 | from functools import wraps 9 | from collections import Iterable 10 | from config import setting 11 | from lib.public import logger, relevance 12 | from lib.public import http_keywords 13 | from lib.utils.use_MySql import ExecuteSQL 14 | from lib.public.Recursion import GetJsonParams 15 | from lib.public.case_manager import TestContainer 16 | from lib.public import text_similarity_comparison as diff_lib 17 | 18 | 19 | DataBaseSetting = setting.DATA_BASE_CONF 20 | VariablesPathList = [setting.CONFIG_PARAMS, setting.EXTRACT_PARAMS, setting.INTERFACE_PARAMS, setting.RANDOM_PARAMS] 21 | 22 | 23 | def cases_runner(func): 24 | 25 | @wraps(func) 26 | def wrap(*args): 27 | 28 | for items in iter(TestContainer()): 29 | 30 | for key, value in dict(items).items(): 31 | if value == func.__name__: 32 | 33 | body = {} 34 | 35 | # 用例文件与临时变量文件相互关联 36 | # relevant_params = items.get('body').get('relevant_parameter') 37 | relevant_params = GetJsonParams.get_value(items, 'relevant_parameter') 38 | if relevant_params: 39 | 40 | if isinstance(relevant_params, list): 41 | _relevance = {} 42 | 43 | for relevant_param in relevant_params: 44 | relevant_files = relevant_param + '.yaml' 45 | for base_path in VariablesPathList: 46 | if exists(path.join(base_path, relevant_files)): 47 | with open(base_path + relevant_files, encoding='utf-8') as file: 48 | _relevance.update(yaml.safe_load(file)) 49 | 50 | # 判断关联文件中是否存在替换的变量,将其替换 51 | _relevance_params = GetJsonParams.get_value(_relevance, 'relevant_parameter') 52 | if _relevance_params: 53 | 54 | if isinstance(_relevance_params, list): 55 | _next_relevance = {} 56 | 57 | for _relevance_param in _relevance_params: 58 | _relevant_files = _relevance_param + '.yaml' 59 | for base_path in VariablesPathList: 60 | if exists(path.join(base_path, _relevant_files)): 61 | with open(base_path + _relevant_files, encoding='utf-8') as file: 62 | _next_relevance.update(yaml.safe_load(file)) 63 | 64 | _relevance_body = relevance.custom_manage(str(_relevance), _next_relevance) 65 | _relevance.update(_relevance_body) 66 | 67 | relevance_body = relevance.custom_manage(str(items['body']), _relevance) 68 | body.update(relevance_body) 69 | 70 | # 运行用例,暂支持Post与Get请求接口 71 | handler = http_keywords.BaseKeyWords(body) 72 | result = handler.make_test_templates() 73 | 74 | logger.log_info("本次用例执行的测试结果为{}".format( 75 | json.dumps(result, indent=4, ensure_ascii=False)) 76 | ) 77 | 78 | # 将临时变量写入yaml文件 79 | res_index = items.get('body').get('res_index') 80 | if res_index: 81 | if isinstance(res_index, list): 82 | for res_key in res_index: 83 | return_res = GetJsonParams.get_value(result, res_key) 84 | file_name = setting.EXTRACT_PARAMS + res_key 85 | logger.log_debug('保存的变量值为 => {} '.format(return_res)) 86 | 87 | with open(file_name + '.yaml', 'w', encoding='utf-8') as file: 88 | file.write('{}: {}'.format(res_key, return_res)) 89 | 90 | if isinstance(res_index, str): 91 | return_res = GetJsonParams.get_value(result, res_index) 92 | file_name = setting.EXTRACT_PARAMS + res_index 93 | logger.log_debug('保存的变量值为 {}'.format(return_res)) 94 | 95 | with open(file_name, 'w', encoding='utf-8') as file: 96 | file.write('{}: {}'.format(res_index, return_res)) 97 | 98 | # 验证接口请求数据是否落库 99 | excep_columns, res_sql = {}, {} 100 | relevant_database = items.get('body').get('relevant_sql') 101 | if relevant_database: 102 | 103 | if isinstance(relevant_database, list): 104 | for relevant_db in relevant_database: 105 | filename = relevant_db + '.yaml' 106 | 107 | relevant_sql = {} 108 | with open(setting.CASE_DATA + filename, 'rb') as file: 109 | relevant_sql.update(yaml.safe_load(file)) 110 | 111 | action = relevant_sql[relevant_db]['action'] 112 | columns = relevant_sql[relevant_db]['execSQL']['columns'] 113 | table = relevant_sql[relevant_db]['execSQL']['table'] 114 | params = relevant_sql[relevant_db]['execSQL']['params'] 115 | desc = relevant_sql[relevant_db]['execSQL']['desc'] 116 | execute_sql = '{} {} FROM {} {} {}'.format(action, columns, table, params, desc) 117 | execute_res = ExecuteSQL(DataBaseSetting).execute(execute_sql)[0][0] 118 | 119 | res_sql.update({columns: execute_res}) 120 | logger.log_debug('执行sql结果为{}'.format(execute_res)) 121 | 122 | return func( 123 | *args, 124 | response=result, 125 | kwassert=items.get('body').get('assert'), 126 | kwassert_same=items.get('body').get('assert_same_key'), 127 | json_diff=items.get('body').get('json_diff'), 128 | execute_res=res_sql, 129 | db_check=items.get('body').get('check_db') 130 | ) 131 | 132 | return wrap 133 | 134 | 135 | def result_assert(func): 136 | 137 | @wraps(func) 138 | def wrap(*args, **kwargs): 139 | 140 | response = kwargs.get('response') 141 | kwassert = kwargs.get('kwassert') if kwargs.get('kwassert') else {} 142 | kwassert_same = kwargs.get('kwassert_same') 143 | json_diff = kwargs.get('json_diff') 144 | database_check = kwargs.get('db_check') if kwargs.get('db_check') else {} 145 | execute_res = kwargs.get('execute_res') 146 | 147 | tmp = tuple(kwassert.keys()) 148 | result = GetJsonParams.for_keys_to_dict(*tmp, my_dict=response) 149 | 150 | for key, value in kwassert.items(): 151 | 152 | if isinstance(value, list): 153 | tp, _value = value 154 | if tp == "type" and key == "ResponseType": 155 | result[key] = [tp, repr(getattr(builtins, tp)(response)).split("'")[1]] 156 | elif tp == "type": 157 | result[key] = [tp, repr(getattr(builtins, tp)(result.get(key))).split("'")[1]] 158 | elif tp == "len": 159 | result[key] = [tp, repr(getattr(builtins, tp)(result.get(key)))] 160 | else: 161 | result[key] = [tp, getattr(builtins, tp)(result.get(key))] 162 | 163 | expect_kwassert_same, response_kwassert_content = '', '' 164 | if isinstance(kwassert_same, Iterable): 165 | for key, value in dict(kwassert_same).items(): 166 | flag = key.split('.') 167 | response_kwassert_content += GetJsonParams.get_same_content(response, flag[0], int(flag[1]), flag[2]) 168 | expect_kwassert_same += value 169 | 170 | expect_assert_value = json.dumps( 171 | result, 172 | sort_keys=True, 173 | ensure_ascii=False 174 | ) 175 | 176 | kwassert_value = json.dumps( 177 | kwassert, 178 | sort_keys=True, 179 | ensure_ascii=False 180 | ) 181 | 182 | expect_json_diff = json.dumps( 183 | json_diff, 184 | sort_keys=True, 185 | ensure_ascii=False 186 | ) 187 | 188 | json_diff_content = diff_lib.contrast_num(str(expect_json_diff), str(response)) 189 | logger.log_info("本次Response自动对比结果为{}".format( 190 | json.dumps(json_diff_content, indent=4, ensure_ascii=False)) 191 | ) 192 | 193 | return func( 194 | *args, 195 | response=result, 196 | expect_assert_value=expect_assert_value, 197 | kwassert_value=kwassert_value, 198 | expect_kwassert_same=expect_kwassert_same, 199 | response_kwassert_content=response_kwassert_content, 200 | database_check=database_check, 201 | execute_res=execute_res 202 | ) 203 | 204 | return wrap 205 | -------------------------------------------------------------------------------- /.idea/sonarIssues.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 428 | 429 | -------------------------------------------------------------------------------- /lib/public/HtmlReport.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from io import StringIO as StringIO 4 | import time 5 | import json 6 | import unittest 7 | import platform 8 | import base64 9 | import traceback 10 | from functools import wraps 11 | 12 | __all__ = ['Report'] 13 | 14 | HTML_IMG_TEMPLATE = """ 15 | 16 | 17 | 18 |

19 | """ 20 | 21 | 22 | class OutputRedirector(object): 23 | """ Wrapper to redirect stdout or stderr """ 24 | 25 | def __init__(self, fp): 26 | self.fp = fp 27 | 28 | def write(self, s): 29 | self.fp.write(s) 30 | 31 | def writelines(self, lines): 32 | self.fp.writelines(lines) 33 | 34 | def flush(self): 35 | self.fp.flush() 36 | 37 | 38 | stdout_redirector = OutputRedirector(sys.stdout) 39 | stderr_redirector = OutputRedirector(sys.stderr) 40 | 41 | SYSSTR = platform.system() 42 | 43 | FIELDS = { 44 | "testPass": 0, 45 | "testResult": [ 46 | ], 47 | "testName": "", 48 | "testAll": 0, 49 | "testFail": 0, 50 | "beginTime": "", 51 | "totalTime": "", 52 | "testSkip": 0 53 | } 54 | 55 | 56 | class PATH: 57 | """ all file PATH meta """ 58 | config_tmp_path = './lib/templates/report' 59 | 60 | 61 | class MakeResultJson: 62 | """ make html table tags """ 63 | 64 | def __init__(self, datas: tuple): 65 | """ 66 | init self object 67 | :param datas: 拿到所有返回数据结构 68 | """ 69 | self.datas = datas 70 | print(datas) 71 | self.result_schema = {} 72 | 73 | def __setitem__(self, key, value): 74 | """ 75 | :param key: self[key] 76 | :param value: value 77 | :return: 78 | """ 79 | self[key] = value 80 | 81 | def __repr__(self) -> str: 82 | """ 83 | 返回对象的html结构体 84 | :rtype: dict 85 | :return: self的repr对象, 返回一个构造完成的tr表单 86 | """ 87 | keys = ( 88 | 'className', 89 | 'methodName', 90 | 'description', 91 | 'spendTime', 92 | 'status', 93 | 'log', 94 | ) 95 | obj_type = (int, tuple, bool, str, dict, list, bytes, float) 96 | for key, data in zip(keys, self.datas): 97 | try: 98 | if isinstance(data, obj_type): 99 | self.result_schema.setdefault(key, data) 100 | except TypeError: 101 | continue 102 | return json.dumps(self.result_schema) 103 | 104 | 105 | TestResult = unittest.TestResult 106 | 107 | 108 | class ReportTestResult(TestResult): 109 | """ override""" 110 | 111 | def __init__(self, suite, stream=sys.stdout, retry=1, save_last_try=False): 112 | super(ReportTestResult, self).__init__() 113 | self.begin_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) 114 | self.start_time = 0 115 | self.stream = stream 116 | self.end_time = 0 117 | self.failure_count = 0 118 | self.error_count = 0 119 | self.success_count = 0 120 | self.skipped = 0 121 | self.verbosity = 1 122 | self.success_case_info = [] 123 | self.skipped_case_info = [] 124 | self.failures_case_info = [] 125 | self.errors_case_info = [] 126 | self.all_case_counter = 0 127 | self.suite = suite 128 | self.status = '' 129 | self.result_list = [] 130 | self.fail_result = [] 131 | self.retry = retry 132 | self.save_last_try = save_last_try 133 | self.case_status = 0 134 | self.trys = 0 135 | self.trys = 1 136 | self.case_log = '' 137 | self.default_report_name = '自动化测试报告' 138 | self.FIELDS = None 139 | self.sys_stdout = None 140 | self.sys_stderr = None 141 | self.outputBuffer = None 142 | 143 | @property 144 | def success_counter(self) -> int: 145 | """ set success counter """ 146 | return self.success_count 147 | 148 | @success_counter.setter 149 | def success_counter(self, value) -> None: 150 | """ 151 | success_counter函数的setter方法, 用于改变成功的case数量 152 | :param value: 当前传递进来的成功次数的int数值 153 | :return: 154 | """ 155 | self.success_count = value 156 | 157 | def startTest(self, test) -> None: 158 | """ 159 | 当测试用例测试即将运行时调用 160 | :return: 161 | """ 162 | unittest.TestResult.startTest(self, test) 163 | self.outputBuffer = StringIO() 164 | stdout_redirector.fp = self.outputBuffer 165 | stderr_redirector.fp = self.outputBuffer 166 | self.sys_stdout = sys.stdout 167 | self.sys_stdout = sys.stderr 168 | sys.stdout = stdout_redirector 169 | sys.stderr = stderr_redirector 170 | self.start_time = time.time() 171 | 172 | def stopTest(self, test) -> None: 173 | """ 174 | 当测试用力执行完成后进行调用 175 | :return: 176 | """ 177 | # FIXME: 重跑方法存在问题, 暂时影响不是很大 178 | 179 | if self.retry and self.retry >= 1: 180 | self.trys += 1 181 | if self.case_status == 1: 182 | if self.trys <= self.retry: 183 | # if self.save_last_try: 184 | # t = self.fail_result.pop(-1) 185 | # if t[0] == 1: 186 | # self.failure_count -= 1 187 | # else: 188 | # self.error_count -= 1 189 | import copy 190 | test = copy.copy(test) 191 | sys.stderr.write("Retesting... ") 192 | sys.stderr.write(str(test)) 193 | sys.stderr.write('..%d \n' % self.trys) 194 | doc = getattr(test, '_testMethodDoc', u"") or u'' 195 | if doc.find('_retry') != -1: 196 | doc = doc[:doc.find('_retry')] 197 | desc = "%s__retry:%s" % (doc, '重跑用例') 198 | test._testMethodDoc = desc 199 | test(self) 200 | else: 201 | self.case_status = 0 202 | self.trys = 0 203 | self.end_time = '{0:.3} s'.format((time.time() - self.start_time)) 204 | self.result_list.append(self.get_all_result_info_tuple(test)) 205 | self.complete_output() 206 | 207 | def complete_output(self): 208 | """ 209 | Disconnect output redirection and return buffer. 210 | Safe to call multiple times. 211 | """ 212 | if self.sys_stdout: 213 | sys.stdout = self.sys_stdout 214 | sys.stderr = self.sys_stdout 215 | self.sys_stdout = None 216 | self.sys_stdout = None 217 | return self.outputBuffer.getvalue() 218 | 219 | def stopTestRun(self, title=None) -> dict: 220 | """ 221 | 所有测试执行完成后, 执行该方法 222 | :param title: 223 | :return: 224 | """ 225 | FIELDS['testPass'] = self.success_counter 226 | for item in self.result_list: 227 | item = json.loads(str(MakeResultJson(item))) 228 | FIELDS.get('testResult').append(item) 229 | FIELDS['testAll'] = len(self.result_list) 230 | FIELDS['testName'] = title if title else self.default_report_name 231 | FIELDS['testFail'] = self.failure_count 232 | FIELDS['beginTime'] = self.begin_time 233 | end_time = int(time.time()) 234 | start_time = int( 235 | time.mktime( 236 | time.strptime( 237 | self.begin_time, 238 | '%Y-%m-%d %H:%M:%S'))) 239 | FIELDS['totalTime'] = str(end_time - start_time) + 's' 240 | FIELDS['testError'] = self.error_count 241 | FIELDS['testSkip'] = self.skipped 242 | self.FIELDS = FIELDS 243 | return FIELDS 244 | 245 | def get_all_result_info_tuple(self, test) -> tuple: 246 | """ 247 | 接受test 相关信息, 并拼接成一个完成的tuple结构返回 248 | :param test: 249 | :return: 250 | """ 251 | return tuple([*self.get_testcase_property(test), 252 | self.end_time, self.status, self.case_log]) 253 | 254 | @staticmethod 255 | def error_or_failure_text(err) -> str: 256 | """ 257 | 获取sys.exc_info()的参数并返回字符串类型的数据, 去掉t6 error 258 | :param err: 259 | :return: 260 | """ 261 | return traceback.format_exception(*err) 262 | 263 | def addSuccess(self, test) -> None: 264 | """ 265 | pass 266 | :param test: 267 | :return: 268 | """ 269 | logs = [] 270 | output = self.complete_output() 271 | logs.append(output) 272 | if self.verbosity > 1: 273 | sys.stderr.write('ok ') 274 | sys.stderr.write(str(test)) 275 | sys.stderr.write('\n') 276 | else: 277 | sys.stderr.write('.') 278 | self.success_counter += 1 279 | self.status = '成功' 280 | self.case_status = 0 281 | self.case_log = output.split('\n') 282 | self._mirrorOutput = True # print(class_name, method_name, method_doc) 283 | 284 | def addError(self, test, err): 285 | """ 286 | add Some Error report and infos 287 | :param test: 288 | :param err: 289 | :return: 290 | """ 291 | logs = [] 292 | output = self.complete_output() 293 | logs.append(output) 294 | logs.extend(self.error_or_failure_text(err)) 295 | self.failure_count += 1 296 | self.case_status = 1 297 | self.add_test_type('失败', logs) 298 | if self.verbosity > 1: 299 | sys.stderr.write('F ') 300 | sys.stderr.write(str(test)) 301 | sys.stderr.write('\n') 302 | else: 303 | sys.stderr.write('F') 304 | 305 | self._mirrorOutput = True 306 | 307 | def addFailure(self, test, err): 308 | """ 309 | add Some Failures report and infos 310 | :param test: 311 | :param err: 312 | :return: 313 | """ 314 | logs = [] 315 | output = self.complete_output() 316 | logs.append(output) 317 | logs.extend(self.error_or_failure_text(err)) 318 | self.failure_count += 1 319 | self.case_status = 1 320 | TestResult.addFailure(self, test, err) 321 | _, _exc_str = self.failures[-1] 322 | self.fail_result.append((1, test)) 323 | # self.result_list.append((1, test, output, _exc_str)) 324 | self.add_test_type('失败', logs) 325 | if self.verbosity > 1: 326 | sys.stderr.write('F ') 327 | sys.stderr.write(str(test)) 328 | sys.stderr.write('\n') 329 | else: 330 | sys.stderr.write('F') 331 | 332 | self._mirrorOutput = True 333 | 334 | def addSkip(self, test, reason) -> None: 335 | """ 336 | 获取全部的跳过的case信息 337 | :param test: 338 | :param reason: 339 | :return: None 340 | """ 341 | logs = [reason] 342 | self.complete_output() 343 | self.skipped += 1 344 | self.case_status = 0 345 | self.add_test_type('跳过', logs) 346 | 347 | if self.verbosity > 1: 348 | sys.stderr.write('S ') 349 | sys.stderr.write(str(test)) 350 | sys.stderr.write('\n') 351 | else: 352 | sys.stderr.write('S') 353 | self._mirrorOutput = True 354 | 355 | def add_test_type(self, status: str, case_log: list) -> None: 356 | """ 357 | abstruct add test.j type and return tuple 358 | :param status: 359 | :param case_log: 360 | :return: 361 | """ 362 | self.status = status 363 | self.case_log = case_log 364 | 365 | @staticmethod 366 | def get_testcase_property(test) -> tuple: 367 | """ 368 | 接受一个test, 并返回一个test的class_name, method_name, method_doc属性 369 | :param test: 370 | :return: (class_name, method_name, method_doc) -> tuple 371 | """ 372 | class_name = test.__class__.__qualname__ 373 | method_name = test.__dict__['_testMethodName'] 374 | method_doc = test.__dict__['_testMethodDoc'] 375 | return class_name, method_name, method_doc 376 | 377 | 378 | class Report(ReportTestResult, PATH): 379 | img_path = 'img/' if platform.system() != 'Windows' else 'img\\' 380 | 381 | def __init__(self, suites, retry=0, save_last_try=True): 382 | super(Report, self).__init__(suites) 383 | self.suites = suites 384 | self.log_path = None 385 | self.title = '自动化测试报告' 386 | self.filename = 'report.html' 387 | self.retry = retry 388 | self.save_last_try = save_last_try 389 | 390 | def report(self, description, filename: str = None, log_path='.'): 391 | """ 392 | 生成测试报告,并放在当前运行路径下 393 | :param log_path: 生成report的文件存储路径 394 | :param filename: 生成文件的filename 395 | :param description: 生成文件的注释 396 | :return: 397 | """ 398 | if filename: 399 | self.filename = filename if filename.endswith('.html') else filename + '.html' 400 | 401 | if description: 402 | self.title = description 403 | 404 | self.log_path = os.path.abspath(log_path) 405 | self.suites.run(result=self) 406 | self.stopTestRun(self.title) 407 | self.output_report() 408 | text = '\n测试已全部完成, 可前往{}查询测试报告'.format(self.log_path) 409 | print(text) 410 | 411 | def output_report(self): 412 | """ 413 | 生成测试报告到指定路径下 414 | :return: 415 | """ 416 | template_path = self.config_tmp_path 417 | override_path = os.path.abspath(self.log_path) if \ 418 | os.path.abspath(self.log_path).endswith('/') else \ 419 | os.path.abspath(self.log_path) + '/' 420 | 421 | with open(template_path, 'rb') as file: 422 | body = file.readlines() 423 | with open(override_path + self.filename, 'wb') as write_file: 424 | for item in body: 425 | if item.strip().startswith(b'var resultData'): 426 | head = ' var resultData = ' 427 | item = item.decode().split(head) 428 | item[1] = head + \ 429 | json.dumps(self.FIELDS, ensure_ascii=False, indent=4) 430 | item = ''.join(item).encode() 431 | item = bytes(item) + b';\n' 432 | write_file.write(item) 433 | 434 | @staticmethod 435 | def img2base(img_path: str, file_name: str) -> str: 436 | """ 437 | 接受传递进函数的filename 并找到文件转换为base64格式 438 | :param img_path: 通过文件名及默认路径找到的img绝对路径 439 | :param file_name: 用户在装饰器中传递进来的问价匿名 440 | :return: 441 | """ 442 | pattern = '/' if platform != 'Windows' else '\\' 443 | 444 | with open(img_path + pattern + file_name, 'rb') as file: 445 | data = file.read() 446 | return base64.b64encode(data).decode() 447 | 448 | def add_test_img(*pargs): 449 | """ 450 | 接受若干个图片元素, 并展示在测试报告中 451 | :param pargs: 452 | :return: 453 | """ 454 | 455 | def _wrap(func): 456 | @wraps(func) 457 | def __wrap(*args, **kwargs): 458 | img_path = os.path.abspath( 459 | '{}'.format(Report.img_path)) 460 | try: 461 | result = func(*args, **kwargs) 462 | except Exception: 463 | if 'save_img' in dir(args[0]): 464 | save_img = getattr(args[0], 'save_img') 465 | save_img(func.__name__) 466 | data = Report.img2base(img_path, pargs[0] + '.png') 467 | print(HTML_IMG_TEMPLATE.format(data, data)) 468 | sys.exit(0) 469 | print('

') 470 | 471 | if len(pargs) > 1: 472 | for parg in pargs: 473 | print(parg + ':') 474 | data = Report.img2base(img_path, parg + '.png') 475 | print(HTML_IMG_TEMPLATE.format(data, data)) 476 | return result 477 | if not os.path.exists(img_path + pargs[0] + '.png'): 478 | return result 479 | data = Report.img2base(img_path, pargs[0] + '.png') 480 | print(HTML_IMG_TEMPLATE.format(data, data)) 481 | return result 482 | return __wrap 483 | return _wrap -------------------------------------------------------------------------------- /data/env_data/stopKeywords: -------------------------------------------------------------------------------- 1 | ! 2 | " 3 | # 4 | $ 5 | % 6 | & 7 | ' 8 | ( 9 | ) 10 | * 11 | + 12 | , 13 | - 14 | -- 15 | . 16 | .. 17 | ... 18 | ...... 19 | ................... 20 | ./ 21 | .一 22 | .数 23 | .日 24 | / 25 | // 26 | 0 27 | 1 28 | 2 29 | 3 30 | 4 31 | 5 32 | 6 33 | 7 34 | 8 35 | 9 36 | : 37 | :// 38 | :: 39 | ; 40 | < 41 | = 42 | > 43 | >> 44 | ? 45 | @ 46 | A 47 | Lex 48 | [ 49 | \ 50 | ] 51 | ^ 52 | _ 53 | ` 54 | exp 55 | sub 56 | sup 57 | | 58 | } 59 | ~ 60 | ~~~~ 61 | · 62 | × 63 | ××× 64 | Δ 65 | Ψ 66 | γ 67 | μ 68 | φ 69 | φ. 70 | В 71 | — 72 | —— 73 | ——— 74 | ‘ 75 | ’ 76 | ’‘ 77 | “ 78 | ” 79 | ”, 80 | … 81 | …… 82 | …………………………………………………③ 83 | ′∈ 84 | ′| 85 | ℃ 86 | Ⅲ 87 | ↑ 88 | → 89 | ∈[ 90 | ∪φ∈ 91 | ≈ 92 | ① 93 | ② 94 | ②c 95 | ③ 96 | ③] 97 | ④ 98 | ⑤ 99 | ⑥ 100 | ⑦ 101 | ⑧ 102 | ⑨ 103 | ⑩ 104 | ── 105 | ■ 106 | ▲ 107 |   108 | 、 109 | 。 110 | 〈 111 | 〉 112 | 《 113 | 》 114 | 》), 115 | 」 116 | 『 117 | 』 118 | 【 119 | 】 120 | 〔 121 | 〕 122 | 〕〔 123 | ㈧ 124 | 一 125 | 一. 126 | 一一 127 | 一下 128 | 一个 129 | 一些 130 | 一何 131 | 一切 132 | 一则 133 | 一则通过 134 | 一天 135 | 一定 136 | 一方面 137 | 一旦 138 | 一时 139 | 一来 140 | 一样 141 | 一次 142 | 一片 143 | 一番 144 | 一直 145 | 一致 146 | 一般 147 | 一起 148 | 一转眼 149 | 一边 150 | 一面 151 | 七 152 | 万一 153 | 三 154 | 三天两头 155 | 三番两次 156 | 三番五次 157 | 上 158 | 上下 159 | 上升 160 | 上去 161 | 上来 162 | 上述 163 | 上面 164 | 下 165 | 下列 166 | 下去 167 | 下来 168 | 下面 169 | 不 170 | 不一 171 | 不下 172 | 不久 173 | 不了 174 | 不亦乐乎 175 | 不仅 176 | 不仅...而且 177 | 不仅仅 178 | 不仅仅是 179 | 不会 180 | 不但 181 | 不但...而且 182 | 不光 183 | 不免 184 | 不再 185 | 不力 186 | 不单 187 | 不变 188 | 不只 189 | 不可 190 | 不可开交 191 | 不可抗拒 192 | 不同 193 | 不外 194 | 不外乎 195 | 不够 196 | 不大 197 | 不如 198 | 不妨 199 | 不定 200 | 不对 201 | 不少 202 | 不尽 203 | 不尽然 204 | 不巧 205 | 不已 206 | 不常 207 | 不得 208 | 不得不 209 | 不得了 210 | 不得已 211 | 不必 212 | 不怎么 213 | 不怕 214 | 不惟 215 | 不成 216 | 不拘 217 | 不择手段 218 | 不敢 219 | 不料 220 | 不断 221 | 不日 222 | 不时 223 | 不是 224 | 不曾 225 | 不止 226 | 不止一次 227 | 不比 228 | 不消 229 | 不满 230 | 不然 231 | 不然的话 232 | 不特 233 | 不独 234 | 不由得 235 | 不知不觉 236 | 不管 237 | 不管怎样 238 | 不经意 239 | 不胜 240 | 不能 241 | 不能不 242 | 不至于 243 | 不若 244 | 不要 245 | 不论 246 | 不起 247 | 不足 248 | 不过 249 | 不迭 250 | 不问 251 | 不限 252 | 与 253 | 与其 254 | 与其说 255 | 与否 256 | 与此同时 257 | 专门 258 | 且 259 | 且不说 260 | 且说 261 | 两者 262 | 严格 263 | 严重 264 | 个 265 | 个人 266 | 个别 267 | 中小 268 | 中间 269 | 丰富 270 | 串行 271 | 临 272 | 临到 273 | 为 274 | 为主 275 | 为了 276 | 为什么 277 | 为什麽 278 | 为何 279 | 为止 280 | 为此 281 | 为着 282 | 主张 283 | 主要 284 | 举凡 285 | 举行 286 | 乃 287 | 乃至 288 | 乃至于 289 | 么 290 | 之 291 | 之一 292 | 之前 293 | 之后 294 | 之後 295 | 之所以 296 | 之类 297 | 乌乎 298 | 乎 299 | 乒 300 | 乘 301 | 乘势 302 | 乘机 303 | 乘胜 304 | 乘虚 305 | 乘隙 306 | 九 307 | 也 308 | 也好 309 | 也就是说 310 | 也是 311 | 也罢 312 | 了 313 | 了解 314 | 争取 315 | 二 316 | 二来 317 | 二话不说 318 | 二话没说 319 | 于 320 | 于是 321 | 于是乎 322 | 云云 323 | 云尔 324 | 互 325 | 互相 326 | 五 327 | 些 328 | 交口 329 | 亦 330 | 产生 331 | 亲口 332 | 亲手 333 | 亲眼 334 | 亲自 335 | 亲身 336 | 人 337 | 人人 338 | 人们 339 | 人家 340 | 人民 341 | 什么 342 | 什么样 343 | 什麽 344 | 仅 345 | 仅仅 346 | 今 347 | 今后 348 | 今天 349 | 今年 350 | 今後 351 | 介于 352 | 仍 353 | 仍旧 354 | 仍然 355 | 从 356 | 从不 357 | 从严 358 | 从中 359 | 从事 360 | 从今以后 361 | 从优 362 | 从古到今 363 | 从古至今 364 | 从头 365 | 从宽 366 | 从小 367 | 从新 368 | 从无到有 369 | 从早到晚 370 | 从未 371 | 从来 372 | 从此 373 | 从此以后 374 | 从而 375 | 从轻 376 | 从速 377 | 从重 378 | 他 379 | 他人 380 | 他们 381 | 他是 382 | 他的 383 | 代替 384 | 以 385 | 以上 386 | 以下 387 | 以为 388 | 以便 389 | 以免 390 | 以前 391 | 以及 392 | 以后 393 | 以外 394 | 以後 395 | 以故 396 | 以期 397 | 以来 398 | 以至 399 | 以至于 400 | 以致 401 | 们 402 | 任 403 | 任何 404 | 任凭 405 | 任务 406 | 企图 407 | 伙同 408 | 会 409 | 伟大 410 | 传 411 | 传说 412 | 传闻 413 | 似乎 414 | 似的 415 | 但 416 | 但凡 417 | 但愿 418 | 但是 419 | 何 420 | 何乐而不为 421 | 何以 422 | 何况 423 | 何处 424 | 何妨 425 | 何尝 426 | 何必 427 | 何时 428 | 何止 429 | 何苦 430 | 何须 431 | 余外 432 | 作为 433 | 你 434 | 你们 435 | 你是 436 | 你的 437 | 使 438 | 使得 439 | 使用 440 | 例如 441 | 依 442 | 依据 443 | 依照 444 | 依靠 445 | 便 446 | 便于 447 | 促进 448 | 保持 449 | 保管 450 | 保险 451 | 俺 452 | 俺们 453 | 倍加 454 | 倍感 455 | 倒不如 456 | 倒不如说 457 | 倒是 458 | 倘 459 | 倘使 460 | 倘或 461 | 倘然 462 | 倘若 463 | 借 464 | 借以 465 | 借此 466 | 假使 467 | 假如 468 | 假若 469 | 偏偏 470 | 做到 471 | 偶尔 472 | 偶而 473 | 傥然 474 | 像 475 | 儿 476 | 允许 477 | 元/吨 478 | 充其极 479 | 充其量 480 | 充分 481 | 先不先 482 | 先后 483 | 先後 484 | 先生 485 | 光 486 | 光是 487 | 全体 488 | 全力 489 | 全年 490 | 全然 491 | 全身心 492 | 全部 493 | 全都 494 | 全面 495 | 八 496 | 八成 497 | 公然 498 | 六 499 | 兮 500 | 共 501 | 共同 502 | 共总 503 | 关于 504 | 其 505 | 其一 506 | 其中 507 | 其二 508 | 其他 509 | 其余 510 | 其后 511 | 其它 512 | 其实 513 | 其次 514 | 具体 515 | 具体地说 516 | 具体来说 517 | 具体说来 518 | 具有 519 | 兼之 520 | 内 521 | 再 522 | 再其次 523 | 再则 524 | 再有 525 | 再次 526 | 再者 527 | 再者说 528 | 再说 529 | 冒 530 | 冲 531 | 决不 532 | 决定 533 | 决非 534 | 况且 535 | 准备 536 | 凑巧 537 | 凝神 538 | 几 539 | 几乎 540 | 几度 541 | 几时 542 | 几番 543 | 几经 544 | 凡 545 | 凡是 546 | 凭 547 | 凭借 548 | 出 549 | 出于 550 | 出去 551 | 出来 552 | 出现 553 | 分别 554 | 分头 555 | 分期 556 | 分期分批 557 | 切 558 | 切不可 559 | 切切 560 | 切勿 561 | 切莫 562 | 则 563 | 则甚 564 | 刚 565 | 刚好 566 | 刚巧 567 | 刚才 568 | 初 569 | 别 570 | 别人 571 | 别处 572 | 别是 573 | 别的 574 | 别管 575 | 别说 576 | 到 577 | 到了儿 578 | 到处 579 | 到头 580 | 到头来 581 | 到底 582 | 到目前为止 583 | 前后 584 | 前此 585 | 前者 586 | 前进 587 | 前面 588 | 加上 589 | 加之 590 | 加以 591 | 加入 592 | 加强 593 | 动不动 594 | 动辄 595 | 勃然 596 | 匆匆 597 | 十分 598 | 千 599 | 千万 600 | 千万千万 601 | 半 602 | 单 603 | 单单 604 | 单纯 605 | 即 606 | 即令 607 | 即使 608 | 即便 609 | 即刻 610 | 即如 611 | 即将 612 | 即或 613 | 即是说 614 | 即若 615 | 却 616 | 却不 617 | 历 618 | 原来 619 | 去 620 | 又 621 | 又及 622 | 及 623 | 及其 624 | 及时 625 | 及至 626 | 双方 627 | 反之 628 | 反之亦然 629 | 反之则 630 | 反倒 631 | 反倒是 632 | 反应 633 | 反手 634 | 反映 635 | 反而 636 | 反过来 637 | 反过来说 638 | 取得 639 | 取道 640 | 受到 641 | 变成 642 | 古来 643 | 另 644 | 另一个 645 | 另一方面 646 | 另外 647 | 另悉 648 | 另方面 649 | 另行 650 | 只 651 | 只当 652 | 只怕 653 | 只是 654 | 只有 655 | 只消 656 | 只要 657 | 只限 658 | 叫 659 | 叫做 660 | 召开 661 | 叮咚 662 | 叮当 663 | 可 664 | 可以 665 | 可好 666 | 可是 667 | 可能 668 | 可见 669 | 各 670 | 各个 671 | 各人 672 | 各位 673 | 各地 674 | 各式 675 | 各种 676 | 各级 677 | 各自 678 | 合理 679 | 同 680 | 同一 681 | 同时 682 | 同样 683 | 后 684 | 后来 685 | 后者 686 | 后面 687 | 向 688 | 向使 689 | 向着 690 | 吓 691 | 吗 692 | 否则 693 | 吧 694 | 吧哒 695 | 吱 696 | 呀 697 | 呃 698 | 呆呆地 699 | 呐 700 | 呕 701 | 呗 702 | 呜 703 | 呜呼 704 | 呢 705 | 周围 706 | 呵 707 | 呵呵 708 | 呸 709 | 呼哧 710 | 呼啦 711 | 咋 712 | 和 713 | 咚 714 | 咦 715 | 咧 716 | 咱 717 | 咱们 718 | 咳 719 | 哇 720 | 哈 721 | 哈哈 722 | 哉 723 | 哎 724 | 哎呀 725 | 哎哟 726 | 哗 727 | 哗啦 728 | 哟 729 | 哦 730 | 哩 731 | 哪 732 | 哪个 733 | 哪些 734 | 哪儿 735 | 哪天 736 | 哪年 737 | 哪怕 738 | 哪样 739 | 哪边 740 | 哪里 741 | 哼 742 | 哼唷 743 | 唉 744 | 唯有 745 | 啊 746 | 啊呀 747 | 啊哈 748 | 啊哟 749 | 啐 750 | 啥 751 | 啦 752 | 啪达 753 | 啷当 754 | 喀 755 | 喂 756 | 喏 757 | 喔唷 758 | 喽 759 | 嗡 760 | 嗡嗡 761 | 嗬 762 | 嗯 763 | 嗳 764 | 嘎 765 | 嘎嘎 766 | 嘎登 767 | 嘘 768 | 嘛 769 | 嘻 770 | 嘿 771 | 嘿嘿 772 | 四 773 | 因 774 | 因为 775 | 因了 776 | 因此 777 | 因着 778 | 因而 779 | 固 780 | 固然 781 | 在 782 | 在下 783 | 在于 784 | 地 785 | 均 786 | 坚决 787 | 坚持 788 | 基于 789 | 基本 790 | 基本上 791 | 处在 792 | 处处 793 | 处理 794 | 复杂 795 | 多 796 | 多么 797 | 多亏 798 | 多多 799 | 多多少少 800 | 多多益善 801 | 多少 802 | 多年前 803 | 多年来 804 | 多数 805 | 多次 806 | 够瞧的 807 | 大 808 | 大不了 809 | 大举 810 | 大事 811 | 大体 812 | 大体上 813 | 大凡 814 | 大力 815 | 大多 816 | 大多数 817 | 大大 818 | 大家 819 | 大张旗鼓 820 | 大批 821 | 大抵 822 | 大概 823 | 大略 824 | 大约 825 | 大致 826 | 大都 827 | 大量 828 | 大面儿上 829 | 失去 830 | 奇 831 | 奈 832 | 奋勇 833 | 她 834 | 她们 835 | 她是 836 | 她的 837 | 好 838 | 好在 839 | 好的 840 | 好象 841 | 如 842 | 如上 843 | 如上所述 844 | 如下 845 | 如今 846 | 如何 847 | 如其 848 | 如前所述 849 | 如同 850 | 如常 851 | 如是 852 | 如期 853 | 如果 854 | 如次 855 | 如此 856 | 如此等等 857 | 如若 858 | 始而 859 | 姑且 860 | 存在 861 | 存心 862 | 孰料 863 | 孰知 864 | 宁 865 | 宁可 866 | 宁愿 867 | 宁肯 868 | 它 869 | 它们 870 | 它们的 871 | 它是 872 | 它的 873 | 安全 874 | 完全 875 | 完成 876 | 定 877 | 实现 878 | 实际 879 | 宣布 880 | 容易 881 | 密切 882 | 对 883 | 对于 884 | 对应 885 | 对待 886 | 对方 887 | 对比 888 | 将 889 | 将才 890 | 将要 891 | 将近 892 | 小 893 | 少数 894 | 尔 895 | 尔后 896 | 尔尔 897 | 尔等 898 | 尚且 899 | 尤其 900 | 就 901 | 就地 902 | 就是 903 | 就是了 904 | 就是说 905 | 就此 906 | 就算 907 | 就要 908 | 尽 909 | 尽可能 910 | 尽如人意 911 | 尽心尽力 912 | 尽心竭力 913 | 尽快 914 | 尽早 915 | 尽然 916 | 尽管 917 | 尽管如此 918 | 尽量 919 | 局外 920 | 居然 921 | 届时 922 | 属于 923 | 屡 924 | 屡屡 925 | 屡次 926 | 屡次三番 927 | 岂 928 | 岂但 929 | 岂止 930 | 岂非 931 | 川流不息 932 | 左右 933 | 巨大 934 | 巩固 935 | 差一点 936 | 差不多 937 | 己 938 | 已 939 | 已矣 940 | 已经 941 | 巴 942 | 巴巴 943 | 带 944 | 帮助 945 | 常 946 | 常常 947 | 常言说 948 | 常言说得好 949 | 常言道 950 | 平素 951 | 年复一年 952 | 并 953 | 并不 954 | 并不是 955 | 并且 956 | 并排 957 | 并无 958 | 并没 959 | 并没有 960 | 并肩 961 | 并非 962 | 广大 963 | 广泛 964 | 应当 965 | 应用 966 | 应该 967 | 庶乎 968 | 庶几 969 | 开外 970 | 开始 971 | 开展 972 | 引起 973 | 弗 974 | 弹指之间 975 | 强烈 976 | 强调 977 | 归 978 | 归根到底 979 | 归根结底 980 | 归齐 981 | 当 982 | 当下 983 | 当中 984 | 当儿 985 | 当前 986 | 当即 987 | 当口儿 988 | 当地 989 | 当场 990 | 当头 991 | 当庭 992 | 当时 993 | 当然 994 | 当真 995 | 当着 996 | 形成 997 | 彻夜 998 | 彻底 999 | 彼 1000 | 彼时 1001 | 彼此 1002 | 往 1003 | 往往 1004 | 待 1005 | 待到 1006 | 很 1007 | 很多 1008 | 很少 1009 | 後来 1010 | 後面 1011 | 得 1012 | 得了 1013 | 得出 1014 | 得到 1015 | 得天独厚 1016 | 得起 1017 | 心里 1018 | 必 1019 | 必定 1020 | 必将 1021 | 必然 1022 | 必要 1023 | 必须 1024 | 快 1025 | 快要 1026 | 忽地 1027 | 忽然 1028 | 怎 1029 | 怎么 1030 | 怎么办 1031 | 怎么样 1032 | 怎奈 1033 | 怎样 1034 | 怎麽 1035 | 怕 1036 | 急匆匆 1037 | 怪 1038 | 怪不得 1039 | 总之 1040 | 总是 1041 | 总的来看 1042 | 总的来说 1043 | 总的说来 1044 | 总结 1045 | 总而言之 1046 | 恍然 1047 | 恐怕 1048 | 恰似 1049 | 恰好 1050 | 恰如 1051 | 恰巧 1052 | 恰恰 1053 | 恰恰相反 1054 | 恰逢 1055 | 您 1056 | 您们 1057 | 您是 1058 | 惟其 1059 | 惯常 1060 | 意思 1061 | 愤然 1062 | 愿意 1063 | 慢说 1064 | 成为 1065 | 成年 1066 | 成年累月 1067 | 成心 1068 | 我 1069 | 我们 1070 | 我是 1071 | 我的 1072 | 或 1073 | 或则 1074 | 或多或少 1075 | 或是 1076 | 或曰 1077 | 或者 1078 | 或许 1079 | 战斗 1080 | 截然 1081 | 截至 1082 | 所 1083 | 所以 1084 | 所在 1085 | 所幸 1086 | 所有 1087 | 所谓 1088 | 才 1089 | 才能 1090 | 扑通 1091 | 打 1092 | 打从 1093 | 打开天窗说亮话 1094 | 扩大 1095 | 把 1096 | 抑或 1097 | 抽冷子 1098 | 拦腰 1099 | 拿 1100 | 按 1101 | 按时 1102 | 按期 1103 | 按照 1104 | 按理 1105 | 按说 1106 | 挨个 1107 | 挨家挨户 1108 | 挨次 1109 | 挨着 1110 | 挨门挨户 1111 | 挨门逐户 1112 | 换句话说 1113 | 换言之 1114 | 据 1115 | 据实 1116 | 据悉 1117 | 据我所知 1118 | 据此 1119 | 据称 1120 | 据说 1121 | 掌握 1122 | 接下来 1123 | 接着 1124 | 接著 1125 | 接连不断 1126 | 放量 1127 | 故 1128 | 故意 1129 | 故此 1130 | 故而 1131 | 敞开儿 1132 | 敢 1133 | 敢于 1134 | 敢情 1135 | 数/ 1136 | 整个 1137 | 断然 1138 | 方 1139 | 方便 1140 | 方才 1141 | 方能 1142 | 方面 1143 | 旁人 1144 | 无 1145 | 无宁 1146 | 无法 1147 | 无论 1148 | 既 1149 | 既...又 1150 | 既往 1151 | 既是 1152 | 既然 1153 | 日复一日 1154 | 日渐 1155 | 日益 1156 | 日臻 1157 | 日见 1158 | 时候 1159 | 昂然 1160 | 明显 1161 | 明确 1162 | 是 1163 | 是不是 1164 | 是以 1165 | 是否 1166 | 是的 1167 | 显然 1168 | 显著 1169 | 普通 1170 | 普遍 1171 | 暗中 1172 | 暗地里 1173 | 暗自 1174 | 更 1175 | 更为 1176 | 更加 1177 | 更进一步 1178 | 曾 1179 | 曾经 1180 | 替 1181 | 替代 1182 | 最 1183 | 最后 1184 | 最大 1185 | 最好 1186 | 最後 1187 | 最近 1188 | 最高 1189 | 有 1190 | 有些 1191 | 有关 1192 | 有利 1193 | 有力 1194 | 有及 1195 | 有所 1196 | 有效 1197 | 有时 1198 | 有点 1199 | 有的 1200 | 有的是 1201 | 有着 1202 | 有著 1203 | 望 1204 | 朝 1205 | 朝着 1206 | 末##末 1207 | 本 1208 | 本人 1209 | 本地 1210 | 本着 1211 | 本身 1212 | 权时 1213 | 来 1214 | 来不及 1215 | 来得及 1216 | 来看 1217 | 来着 1218 | 来自 1219 | 来讲 1220 | 来说 1221 | 极 1222 | 极为 1223 | 极了 1224 | 极其 1225 | 极力 1226 | 极大 1227 | 极度 1228 | 极端 1229 | 构成 1230 | 果然 1231 | 果真 1232 | 某 1233 | 某个 1234 | 某些 1235 | 某某 1236 | 根据 1237 | 根本 1238 | 格外 1239 | 梆 1240 | 概 1241 | 次第 1242 | 欢迎 1243 | 欤 1244 | 正值 1245 | 正在 1246 | 正如 1247 | 正巧 1248 | 正常 1249 | 正是 1250 | 此 1251 | 此中 1252 | 此后 1253 | 此地 1254 | 此处 1255 | 此外 1256 | 此时 1257 | 此次 1258 | 此间 1259 | 殆 1260 | 毋宁 1261 | 每 1262 | 每个 1263 | 每天 1264 | 每年 1265 | 每当 1266 | 每时每刻 1267 | 每每 1268 | 每逢 1269 | 比 1270 | 比及 1271 | 比如 1272 | 比如说 1273 | 比方 1274 | 比照 1275 | 比起 1276 | 比较 1277 | 毕竟 1278 | 毫不 1279 | 毫无 1280 | 毫无例外 1281 | 毫无保留地 1282 | 汝 1283 | 沙沙 1284 | 没 1285 | 没奈何 1286 | 没有 1287 | 沿 1288 | 沿着 1289 | 注意 1290 | 活 1291 | 深入 1292 | 清楚 1293 | 满 1294 | 满足 1295 | 漫说 1296 | 焉 1297 | 然 1298 | 然则 1299 | 然后 1300 | 然後 1301 | 然而 1302 | 照 1303 | 照着 1304 | 牢牢 1305 | 特别是 1306 | 特殊 1307 | 特点 1308 | 犹且 1309 | 犹自 1310 | 独 1311 | 独自 1312 | 猛然 1313 | 猛然间 1314 | 率尔 1315 | 率然 1316 | 现代 1317 | 现在 1318 | 理应 1319 | 理当 1320 | 理该 1321 | 瑟瑟 1322 | 甚且 1323 | 甚么 1324 | 甚或 1325 | 甚而 1326 | 甚至 1327 | 甚至于 1328 | 用 1329 | 用来 1330 | 甫 1331 | 甭 1332 | 由 1333 | 由于 1334 | 由是 1335 | 由此 1336 | 由此可见 1337 | 略 1338 | 略为 1339 | 略加 1340 | 略微 1341 | 白 1342 | 白白 1343 | 的 1344 | 的确 1345 | 的话 1346 | 皆可 1347 | 目前 1348 | 直到 1349 | 直接 1350 | 相似 1351 | 相信 1352 | 相反 1353 | 相同 1354 | 相对 1355 | 相对而言 1356 | 相应 1357 | 相当 1358 | 相等 1359 | 省得 1360 | 看 1361 | 看上去 1362 | 看出 1363 | 看到 1364 | 看来 1365 | 看样子 1366 | 看看 1367 | 看见 1368 | 看起来 1369 | 真是 1370 | 真正 1371 | 眨眼 1372 | 着 1373 | 着呢 1374 | 矣 1375 | 矣乎 1376 | 矣哉 1377 | 知道 1378 | 砰 1379 | 确定 1380 | 碰巧 1381 | 社会主义 1382 | 离 1383 | 种 1384 | 积极 1385 | 移动 1386 | 究竟 1387 | 穷年累月 1388 | 突出 1389 | 突然 1390 | 窃 1391 | 立 1392 | 立刻 1393 | 立即 1394 | 立地 1395 | 立时 1396 | 立马 1397 | 竟 1398 | 竟然 1399 | 竟而 1400 | 第 1401 | 第二 1402 | 等 1403 | 等到 1404 | 等等 1405 | 策略地 1406 | 简直 1407 | 简而言之 1408 | 简言之 1409 | 管 1410 | 类如 1411 | 粗 1412 | 精光 1413 | 紧接着 1414 | 累年 1415 | 累次 1416 | 纯 1417 | 纯粹 1418 | 纵 1419 | 纵令 1420 | 纵使 1421 | 纵然 1422 | 练习 1423 | 组成 1424 | 经 1425 | 经常 1426 | 经过 1427 | 结合 1428 | 结果 1429 | 给 1430 | 绝 1431 | 绝不 1432 | 绝对 1433 | 绝非 1434 | 绝顶 1435 | 继之 1436 | 继后 1437 | 继续 1438 | 继而 1439 | 维持 1440 | 综上所述 1441 | 缕缕 1442 | 罢了 1443 | 老 1444 | 老大 1445 | 老是 1446 | 老老实实 1447 | 考虑 1448 | 者 1449 | 而 1450 | 而且 1451 | 而况 1452 | 而又 1453 | 而后 1454 | 而外 1455 | 而已 1456 | 而是 1457 | 而言 1458 | 而论 1459 | 联系 1460 | 联袂 1461 | 背地里 1462 | 背靠背 1463 | 能 1464 | 能否 1465 | 能够 1466 | 腾 1467 | 自 1468 | 自个儿 1469 | 自从 1470 | 自各儿 1471 | 自后 1472 | 自家 1473 | 自己 1474 | 自打 1475 | 自身 1476 | 臭 1477 | 至 1478 | 至于 1479 | 至今 1480 | 至若 1481 | 致 1482 | 般的 1483 | 良好 1484 | 若 1485 | 若夫 1486 | 若是 1487 | 若果 1488 | 若非 1489 | 范围 1490 | 莫 1491 | 莫不 1492 | 莫不然 1493 | 莫如 1494 | 莫若 1495 | 莫非 1496 | 获得 1497 | 藉以 1498 | 虽 1499 | 虽则 1500 | 虽然 1501 | 虽说 1502 | 蛮 1503 | 行为 1504 | 行动 1505 | 表明 1506 | 表示 1507 | 被 1508 | 要 1509 | 要不 1510 | 要不是 1511 | 要不然 1512 | 要么 1513 | 要是 1514 | 要求 1515 | 见 1516 | 规定 1517 | 觉得 1518 | 譬喻 1519 | 譬如 1520 | 认为 1521 | 认真 1522 | 认识 1523 | 让 1524 | 许多 1525 | 论 1526 | 论说 1527 | 设使 1528 | 设或 1529 | 设若 1530 | 诚如 1531 | 诚然 1532 | 话说 1533 | 该 1534 | 该当 1535 | 说明 1536 | 说来 1537 | 说说 1538 | 请勿 1539 | 诸 1540 | 诸位 1541 | 诸如 1542 | 谁 1543 | 谁人 1544 | 谁料 1545 | 谁知 1546 | 谨 1547 | 豁然 1548 | 贼死 1549 | 赖以 1550 | 赶 1551 | 赶快 1552 | 赶早不赶晚 1553 | 起 1554 | 起先 1555 | 起初 1556 | 起头 1557 | 起来 1558 | 起见 1559 | 起首 1560 | 趁 1561 | 趁便 1562 | 趁势 1563 | 趁早 1564 | 趁机 1565 | 趁热 1566 | 趁着 1567 | 越是 1568 | 距 1569 | 跟 1570 | 路经 1571 | 转动 1572 | 转变 1573 | 转贴 1574 | 轰然 1575 | 较 1576 | 较为 1577 | 较之 1578 | 较比 1579 | 边 1580 | 达到 1581 | 达旦 1582 | 迄 1583 | 迅速 1584 | 过 1585 | 过于 1586 | 过去 1587 | 过来 1588 | 运用 1589 | 近 1590 | 近几年来 1591 | 近年来 1592 | 近来 1593 | 还 1594 | 还是 1595 | 还有 1596 | 还要 1597 | 这 1598 | 这一来 1599 | 这个 1600 | 这么 1601 | 这么些 1602 | 这么样 1603 | 这么点儿 1604 | 这些 1605 | 这会儿 1606 | 这儿 1607 | 这就是说 1608 | 这时 1609 | 这样 1610 | 这次 1611 | 这点 1612 | 这种 1613 | 这般 1614 | 这边 1615 | 这里 1616 | 这麽 1617 | 进入 1618 | 进去 1619 | 进来 1620 | 进步 1621 | 进而 1622 | 进行 1623 | 连 1624 | 连同 1625 | 连声 1626 | 连日 1627 | 连日来 1628 | 连袂 1629 | 连连 1630 | 迟早 1631 | 迫于 1632 | 适应 1633 | 适当 1634 | 适用 1635 | 逐步 1636 | 逐渐 1637 | 通常 1638 | 通过 1639 | 造成 1640 | 逢 1641 | 遇到 1642 | 遭到 1643 | 遵循 1644 | 遵照 1645 | 避免 1646 | 那 1647 | 那个 1648 | 那么 1649 | 那么些 1650 | 那么样 1651 | 那些 1652 | 那会儿 1653 | 那儿 1654 | 那时 1655 | 那末 1656 | 那样 1657 | 那般 1658 | 那边 1659 | 那里 1660 | 那麽 1661 | 部分 1662 | 都 1663 | 鄙人 1664 | 采取 1665 | 里面 1666 | 重大 1667 | 重新 1668 | 重要 1669 | 鉴于 1670 | 针对 1671 | 长期以来 1672 | 长此下去 1673 | 长线 1674 | 长话短说 1675 | 问题 1676 | 间或 1677 | 防止 1678 | 阿 1679 | 附近 1680 | 陈年 1681 | 限制 1682 | 陡然 1683 | 除 1684 | 除了 1685 | 除却 1686 | 除去 1687 | 除外 1688 | 除开 1689 | 除此 1690 | 除此之外 1691 | 除此以外 1692 | 除此而外 1693 | 除非 1694 | 随 1695 | 随后 1696 | 随时 1697 | 随着 1698 | 随著 1699 | 隔夜 1700 | 隔日 1701 | 难得 1702 | 难怪 1703 | 难说 1704 | 难道 1705 | 难道说 1706 | 集中 1707 | 零 1708 | 需要 1709 | 非但 1710 | 非常 1711 | 非徒 1712 | 非得 1713 | 非特 1714 | 非独 1715 | 靠 1716 | 顶多 1717 | 顷 1718 | 顷刻 1719 | 顷刻之间 1720 | 顷刻间 1721 | 顺 1722 | 顺着 1723 | 顿时 1724 | 颇 1725 | 风雨无阻 1726 | 饱 1727 | 首先 1728 | 马上 1729 | 高低 1730 | 高兴 1731 | 默然 1732 | 默默地 1733 | 齐 1734 | ︿ 1735 | ! 1736 | # 1737 | $ 1738 | % 1739 | & 1740 | ' 1741 | ( 1742 | ) 1743 | )÷(1- 1744 | )、 1745 | * 1746 | + 1747 | +ξ 1748 | ++ 1749 | , 1750 | ,也 1751 | - 1752 | -β 1753 | -- 1754 | -[*]- 1755 | . 1756 | / 1757 | 0 1758 | 0:2 1759 | 1 1760 | 1. 1761 | 12% 1762 | 2 1763 | 2.3% 1764 | 3 1765 | 4 1766 | 5 1767 | 5:0 1768 | 6 1769 | 7 1770 | 8 1771 | 9 1772 | : 1773 | ; 1774 | < 1775 | <± 1776 | <Δ 1777 | <λ 1778 | <φ 1779 | << 1780 | = 1781 | =″ 1782 | =☆ 1783 | =( 1784 | =- 1785 | =[ 1786 | ={ 1787 | > 1788 | >λ 1789 | ? 1790 | @ 1791 | A 1792 | LI 1793 | R.L. 1794 | ZXFITL 1795 | [ 1796 | [①①] 1797 | [①②] 1798 | [①③] 1799 | [①④] 1800 | [①⑤] 1801 | [①⑥] 1802 | [①⑦] 1803 | [①⑧] 1804 | [①⑨] 1805 | [①A] 1806 | [①B] 1807 | [①C] 1808 | [①D] 1809 | [①E] 1810 | [①] 1811 | [①a] 1812 | [①c] 1813 | [①d] 1814 | [①e] 1815 | [①f] 1816 | [①g] 1817 | [①h] 1818 | [①i] 1819 | [①o] 1820 | [② 1821 | [②①] 1822 | [②②] 1823 | [②③] 1824 | [②④ 1825 | [②⑤] 1826 | [②⑥] 1827 | [②⑦] 1828 | [②⑧] 1829 | [②⑩] 1830 | [②B] 1831 | [②G] 1832 | [②] 1833 | [②a] 1834 | [②b] 1835 | [②c] 1836 | [②d] 1837 | [②e] 1838 | [②f] 1839 | [②g] 1840 | [②h] 1841 | [②i] 1842 | [②j] 1843 | [③①] 1844 | [③⑩] 1845 | [③F] 1846 | [③] 1847 | [③a] 1848 | [③b] 1849 | [③c] 1850 | [③d] 1851 | [③e] 1852 | [③g] 1853 | [③h] 1854 | [④] 1855 | [④a] 1856 | [④b] 1857 | [④c] 1858 | [④d] 1859 | [④e] 1860 | [⑤] 1861 | [⑤]] 1862 | [⑤a] 1863 | [⑤b] 1864 | [⑤d] 1865 | [⑤e] 1866 | [⑤f] 1867 | [⑥] 1868 | [⑦] 1869 | [⑧] 1870 | [⑨] 1871 | [⑩] 1872 | [*] 1873 | [- 1874 | [] 1875 | ] 1876 | ]∧′=[ 1877 | ][ 1878 | _ 1879 | a] 1880 | b] 1881 | c] 1882 | e] 1883 | f] 1884 | ng昉 1885 | { 1886 | {- 1887 | | 1888 | } 1889 | }> 1890 | ~ 1891 | ~± 1892 | ~+ 1893 | ¥ -------------------------------------------------------------------------------- /pylint.conf: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # A comma-separated list of package or module names from where C extensions may 4 | # be loaded. Extensions are loading into the active Python interpreter and may 5 | # run arbitrary code. 6 | extension-pkg-whitelist= 7 | 8 | # Add files or directories to the blacklist. They should be base names, not 9 | # paths. 10 | ignore=CVS 11 | 12 | # Add files or directories matching the regex patterns to the blacklist. The 13 | # regex matches against base names, not paths. 14 | ignore-patterns= 15 | 16 | # Python code to execute, usually for sys.path manipulation such as 17 | # pygtk.require(). 18 | #init-hook= 19 | 20 | # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the 21 | # number of processors available to use. 22 | jobs=1 23 | 24 | # Control the amount of potential inferred values when inferring a single 25 | # object. This can help the performance when dealing with large functions or 26 | # complex, nested conditions. 27 | limit-inference-results=100 28 | 29 | # List of plugins (as comma separated values of python modules names) to load, 30 | # usually to register additional checkers. 31 | load-plugins= 32 | 33 | # Pickle collected data for later comparisons. 34 | persistent=no 35 | 36 | # Specify a configuration file. 37 | #rcfile= 38 | 39 | # When enabled, pylint would attempt to guess common misconfiguration and emit 40 | # user-friendly hints instead of false-positive error messages. 41 | suggestion-mode=yes 42 | 43 | # Allow loading of arbitrary C extensions. Extensions are imported into the 44 | # active Python interpreter and may run arbitrary code. 45 | unsafe-load-any-extension=no 46 | 47 | 48 | [MESSAGES CONTROL] 49 | 50 | # Only show warnings with the listed confidence levels. Leave empty to show 51 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. 52 | confidence= 53 | 54 | # Disable the message, report, category or checker with the given id(s). You 55 | # can either give multiple identifiers separated by comma (,) or put this 56 | # option multiple times (only on the command line, not in the configuration 57 | # file where it should appear only once). You can also use "--disable=all" to 58 | # disable everything first and then reenable specific checks. For example, if 59 | # you want to run only the similarities checker, you can use "--disable=all 60 | # --enable=similarities". If you want to run only the classes checker, but have 61 | # no Warning level messages displayed, use "--disable=all --enable=classes 62 | # --disable=W". 63 | disable=print-statement, 64 | parameter-unpacking, 65 | unpacking-in-except, 66 | old-raise-syntax, 67 | backtick, 68 | long-suffix, 69 | old-ne-operator, 70 | old-octal-literal, 71 | import-star-module-level, 72 | non-ascii-bytes-literal, 73 | raw-checker-failed, 74 | bad-inline-option, 75 | locally-disabled, 76 | file-ignored, 77 | suppressed-message, 78 | useless-suppression, 79 | deprecated-pragma, 80 | use-symbolic-message-instead, 81 | apply-builtin, 82 | basestring-builtin, 83 | buffer-builtin, 84 | cmp-builtin, 85 | coerce-builtin, 86 | execfile-builtin, 87 | file-builtin, 88 | long-builtin, 89 | raw_input-builtin, 90 | reduce-builtin, 91 | standarderror-builtin, 92 | unicode-builtin, 93 | xrange-builtin, 94 | coerce-method, 95 | delslice-method, 96 | getslice-method, 97 | setslice-method, 98 | no-absolute-import, 99 | old-division, 100 | dict-iter-method, 101 | dict-view-method, 102 | next-method-called, 103 | metaclass-assignment, 104 | indexing-exception, 105 | raising-string, 106 | reload-builtin, 107 | oct-method, 108 | hex-method, 109 | nonzero-method, 110 | cmp-method, 111 | input-builtin, 112 | round-builtin, 113 | intern-builtin, 114 | unichr-builtin, 115 | map-builtin-not-iterating, 116 | zip-builtin-not-iterating, 117 | range-builtin-not-iterating, 118 | filter-builtin-not-iterating, 119 | using-cmp-argument, 120 | eq-without-hash, 121 | div-method, 122 | idiv-method, 123 | rdiv-method, 124 | exception-message-attribute, 125 | invalid-str-codec, 126 | sys-max-int, 127 | bad-python3-import, 128 | deprecated-string-function, 129 | deprecated-str-translate-call, 130 | deprecated-itertools-function, 131 | deprecated-types-field, 132 | next-method-defined, 133 | dict-items-not-iterating, 134 | dict-keys-not-iterating, 135 | dict-values-not-iterating, 136 | deprecated-operator-function, 137 | deprecated-urllib-function, 138 | xreadlines-attribute, 139 | deprecated-sys-function, 140 | exception-escape, 141 | comprehension-escape 142 | 143 | # Enable the message, report, category or checker with the given id(s). You can 144 | # either give multiple identifier separated by comma (,) or put this option 145 | # multiple time (only on the command line, not in the configuration file where 146 | # it should appear only once). See also the "--disable" option for examples. 147 | enable=c-extension-no-member 148 | 149 | 150 | [REPORTS] 151 | 152 | # Python expression which should return a note less than 10 (10 is the highest 153 | # note). You have access to the variables errors warning, statement which 154 | # respectively contain the number of errors / warnings messages and the total 155 | # number of statements analyzed. This is used by the global evaluation report 156 | # (RP0004). 157 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 158 | 159 | # Template used to display messages. This is a python new-style format string 160 | # used to format the message information. See doc for all details. 161 | #msg-template= 162 | 163 | # Set the output format. Available formats are text, parseable, colorized, json 164 | # and msvs (visual studio). You can also give a reporter class, e.g. 165 | # mypackage.mymodule.MyReporterClass. 166 | output-format=text 167 | 168 | # Tells whether to display a full report or only the messages. 169 | reports=no 170 | 171 | # Activate the evaluation score. 172 | score=yes 173 | 174 | 175 | [REFACTORING] 176 | 177 | # Maximum number of nested blocks for function / method body 178 | max-nested-blocks=5 179 | 180 | # Complete name of functions that never returns. When checking for 181 | # inconsistent-return-statements if a never returning function is called then 182 | # it will be considered as an explicit return statement and no message will be 183 | # printed. 184 | never-returning-functions=sys.exit 185 | 186 | 187 | [BASIC] 188 | 189 | # Naming style matching correct argument names. 190 | argument-naming-style=snake_case 191 | 192 | # Regular expression matching correct argument names. Overrides argument- 193 | # naming-style. 194 | #argument-rgx= 195 | 196 | # Naming style matching correct attribute names. 197 | attr-naming-style=snake_case 198 | 199 | # Regular expression matching correct attribute names. Overrides attr-naming- 200 | # style. 201 | #attr-rgx= 202 | 203 | # Bad variable names which should always be refused, separated by a comma. 204 | bad-names=foo, 205 | bar, 206 | baz, 207 | toto, 208 | tutu, 209 | tata 210 | 211 | # Naming style matching correct class attribute names. 212 | class-attribute-naming-style=any 213 | 214 | # Regular expression matching correct class attribute names. Overrides class- 215 | # attribute-naming-style. 216 | #class-attribute-rgx= 217 | 218 | # Naming style matching correct class names. 219 | class-naming-style=PascalCase 220 | 221 | # Regular expression matching correct class names. Overrides class-naming- 222 | # style. 223 | #class-rgx= 224 | 225 | # Naming style matching correct constant names. 226 | const-naming-style=UPPER_CASE 227 | 228 | # Regular expression matching correct constant names. Overrides const-naming- 229 | # style. 230 | #const-rgx= 231 | 232 | # Minimum line length for functions/classes that require docstrings, shorter 233 | # ones are exempt. 234 | docstring-min-length=-1 235 | 236 | # Naming style matching correct function names. 237 | function-naming-style=snake_case 238 | 239 | # Regular expression matching correct function names. Overrides function- 240 | # naming-style. 241 | #function-rgx= 242 | 243 | # Good variable names which should always be accepted, separated by a comma. 244 | good-names=i, 245 | j, 246 | k, 247 | ex, 248 | Run, 249 | _ 250 | 251 | # Include a hint for the correct naming format with invalid-name. 252 | include-naming-hint=no 253 | 254 | # Naming style matching correct inline iteration names. 255 | inlinevar-naming-style=any 256 | 257 | # Regular expression matching correct inline iteration names. Overrides 258 | # inlinevar-naming-style. 259 | #inlinevar-rgx= 260 | 261 | # Naming style matching correct method names. 262 | method-naming-style=snake_case 263 | 264 | # Regular expression matching correct method names. Overrides method-naming- 265 | # style. 266 | #method-rgx= 267 | 268 | # Naming style matching correct module names. 269 | module-naming-style=snake_case 270 | 271 | # Regular expression matching correct module names. Overrides module-naming- 272 | # style. 273 | #module-rgx= 274 | 275 | # Colon-delimited sets of names that determine each other's naming style when 276 | # the name regexes allow several styles. 277 | name-group= 278 | 279 | # Regular expression which should only match function or class names that do 280 | # not require a docstring. 281 | no-docstring-rgx=^_ 282 | 283 | # List of decorators that produce properties, such as abc.abstractproperty. Add 284 | # to this list to register other decorators that produce valid properties. 285 | # These decorators are taken in consideration only for invalid-name. 286 | property-classes=abc.abstractproperty 287 | 288 | # Naming style matching correct variable names. 289 | variable-naming-style=snake_case 290 | 291 | # Regular expression matching correct variable names. Overrides variable- 292 | # naming-style. 293 | #variable-rgx= 294 | 295 | 296 | [FORMAT] 297 | 298 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 299 | expected-line-ending-format= 300 | 301 | # Regexp for a line that is allowed to be longer than the limit. 302 | ignore-long-lines=^\s*(# )??$ 303 | 304 | # Number of spaces of indent required inside a hanging or continued line. 305 | indent-after-paren=4 306 | 307 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 308 | # tab). 309 | indent-string=' ' 310 | 311 | # Maximum number of characters on a single line. 312 | max-line-length=100 313 | 314 | # Maximum number of lines in a module. 315 | max-module-lines=1000 316 | 317 | # List of optional constructs for which whitespace checking is disabled. `dict- 318 | # separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. 319 | # `trailing-comma` allows a space between comma and closing bracket: (a, ). 320 | # `empty-line` allows space-only lines. 321 | no-space-check=trailing-comma, 322 | dict-separator 323 | 324 | # Allow the body of a class to be on the same line as the declaration if body 325 | # contains single statement. 326 | single-line-class-stmt=no 327 | 328 | # Allow the body of an if to be on the same line as the test if there is no 329 | # else. 330 | single-line-if-stmt=no 331 | 332 | 333 | [LOGGING] 334 | 335 | # Format style used to check logging format string. `old` means using % 336 | # formatting, while `new` is for `{}` formatting. 337 | logging-format-style=old 338 | 339 | # Logging modules to check that the string format arguments are in logging 340 | # function parameter format. 341 | logging-modules=logging 342 | 343 | 344 | [MISCELLANEOUS] 345 | 346 | # List of note tags to take in consideration, separated by a comma. 347 | notes=FIXME, 348 | XXX, 349 | TODO 350 | 351 | 352 | [SIMILARITIES] 353 | 354 | # Ignore comments when computing similarities. 355 | ignore-comments=yes 356 | 357 | # Ignore docstrings when computing similarities. 358 | ignore-docstrings=yes 359 | 360 | # Ignore imports when computing similarities. 361 | ignore-imports=no 362 | 363 | # Minimum lines number of a similarity. 364 | min-similarity-lines=4 365 | 366 | 367 | [SPELLING] 368 | 369 | # Limits count of emitted suggestions for spelling mistakes. 370 | max-spelling-suggestions=4 371 | 372 | # Spelling dictionary name. Available dictionaries: none. To make it working 373 | # install python-enchant package.. 374 | spelling-dict= 375 | 376 | # List of comma separated words that should not be checked. 377 | spelling-ignore-words= 378 | 379 | # A path to a file that contains private dictionary; one word per line. 380 | spelling-private-dict-file= 381 | 382 | # Tells whether to store unknown words to indicated private dictionary in 383 | # --spelling-private-dict-file option instead of raising a message. 384 | spelling-store-unknown-words=no 385 | 386 | 387 | [STRING] 388 | 389 | # This flag controls whether the implicit-str-concat-in-sequence should 390 | # generate a warning on implicit string concatenation in sequences defined over 391 | # several lines. 392 | check-str-concat-over-line-jumps=no 393 | 394 | 395 | [TYPECHECK] 396 | 397 | # List of decorators that produce context managers, such as 398 | # contextlib.contextmanager. Add to this list to register other decorators that 399 | # produce valid context managers. 400 | contextmanager-decorators=contextlib.contextmanager 401 | 402 | # List of members which are set dynamically and missed by pylint inference 403 | # system, and so shouldn't trigger E1101 when accessed. Python regular 404 | # expressions are accepted. 405 | generated-members= 406 | 407 | # Tells whether missing members accessed in mixin class should be ignored. A 408 | # mixin class is detected if its name ends with "mixin" (case insensitive). 409 | ignore-mixin-members=yes 410 | 411 | # Tells whether to warn about missing members when the owner of the attribute 412 | # is inferred to be None. 413 | ignore-none=yes 414 | 415 | # This flag controls whether pylint should warn about no-member and similar 416 | # checks whenever an opaque object is returned when inferring. The inference 417 | # can return multiple potential results while evaluating a Python object, but 418 | # some branches might not be evaluated, which results in partial inference. In 419 | # that case, it might be useful to still emit no-member and other checks for 420 | # the rest of the inferred objects. 421 | ignore-on-opaque-inference=yes 422 | 423 | # List of class names for which member attributes should not be checked (useful 424 | # for classes with dynamically set attributes). This supports the use of 425 | # qualified names. 426 | ignored-classes=optparse.Values,thread._local,_thread._local 427 | 428 | # List of module names for which member attributes should not be checked 429 | # (useful for modules/projects where namespaces are manipulated during runtime 430 | # and thus existing member attributes cannot be deduced by static analysis. It 431 | # supports qualified module names, as well as Unix pattern matching. 432 | ignored-modules= 433 | 434 | # Show a hint with possible names when a member name was not found. The aspect 435 | # of finding the hint is based on edit distance. 436 | missing-member-hint=yes 437 | 438 | # The minimum edit distance a name should have in order to be considered a 439 | # similar match for a missing member name. 440 | missing-member-hint-distance=1 441 | 442 | # The total number of similar names that should be taken in consideration when 443 | # showing a hint for a missing member. 444 | missing-member-max-choices=1 445 | 446 | 447 | [VARIABLES] 448 | 449 | # List of additional names supposed to be defined in builtins. Remember that 450 | # you should avoid defining new builtins when possible. 451 | additional-builtins= 452 | 453 | # Tells whether unused global variables should be treated as a violation. 454 | allow-global-unused-variables=yes 455 | 456 | # List of strings which can identify a callback function by name. A callback 457 | # name must start or end with one of those strings. 458 | callbacks=cb_, 459 | _cb 460 | 461 | # A regular expression matching the name of dummy variables (i.e. expected to 462 | # not be used). 463 | dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ 464 | 465 | # Argument names that match this expression will be ignored. Default to name 466 | # with leading underscore. 467 | ignored-argument-names=_.*|^ignored_|^unused_ 468 | 469 | # Tells whether we should check for unused import in __init__ files. 470 | init-import=no 471 | 472 | # List of qualified module names which can have objects that can redefine 473 | # builtins. 474 | redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io 475 | 476 | 477 | [CLASSES] 478 | 479 | # List of method names used to declare (i.e. assign) instance attributes. 480 | defining-attr-methods=__init__, 481 | __new__, 482 | setUp 483 | 484 | # List of member names, which should be excluded from the protected access 485 | # warning. 486 | exclude-protected=_asdict, 487 | _fields, 488 | _replace, 489 | _source, 490 | _make 491 | 492 | # List of valid names for the first argument in a class method. 493 | valid-classmethod-first-arg=cls 494 | 495 | # List of valid names for the first argument in a metaclass class method. 496 | valid-metaclass-classmethod-first-arg=cls 497 | 498 | 499 | [DESIGN] 500 | 501 | # Maximum number of arguments for function / method. 502 | max-args=5 503 | 504 | # Maximum number of attributes for a class (see R0902). 505 | max-attributes=7 506 | 507 | # Maximum number of boolean expressions in an if statement. 508 | max-bool-expr=5 509 | 510 | # Maximum number of branch for function / method body. 511 | max-branches=12 512 | 513 | # Maximum number of locals for function / method body. 514 | max-locals=15 515 | 516 | # Maximum number of parents for a class (see R0901). 517 | max-parents=7 518 | 519 | # Maximum number of public methods for a class (see R0904). 520 | max-public-methods=20 521 | 522 | # Maximum number of return / yield for function / method body. 523 | max-returns=6 524 | 525 | # Maximum number of statements in function / method body. 526 | max-statements=50 527 | 528 | # Minimum number of public methods for a class (see R0903). 529 | min-public-methods=2 530 | 531 | 532 | [IMPORTS] 533 | 534 | # Allow wildcard imports from modules that define __all__. 535 | allow-wildcard-with-all=no 536 | 537 | # Analyse import fallback blocks. This can be used to support both Python 2 and 538 | # 3 compatible code, which means that the block might have code that exists 539 | # only in one or another interpreter, leading to false positives when analysed. 540 | analyse-fallback-blocks=no 541 | 542 | # Deprecated modules which should not be used, separated by a comma. 543 | deprecated-modules=optparse,tkinter.tix 544 | 545 | # Create a graph of external dependencies in the given file (report RP0402 must 546 | # not be disabled). 547 | ext-import-graph= 548 | 549 | # Create a graph of every (i.e. internal and external) dependencies in the 550 | # given file (report RP0402 must not be disabled). 551 | import-graph= 552 | 553 | # Create a graph of internal dependencies in the given file (report RP0402 must 554 | # not be disabled). 555 | int-import-graph= 556 | 557 | # Force import order to recognize a module as part of the standard 558 | # compatibility libraries. 559 | known-standard-library= 560 | 561 | # Force import order to recognize a module as part of a third party library. 562 | known-third-party=enchant 563 | 564 | 565 | [EXCEPTIONS] 566 | 567 | # Exceptions that will emit a warning when being caught. Defaults to 568 | # "BaseException, Exception". 569 | overgeneral-exceptions=BaseException, 570 | Exception 571 | --------------------------------------------------------------------------------