Repository: defnngj/pyse
Branch: master
Commit: f94c55006802
Files: 204
Total size: 675.4 KB
Directory structure:
gitextract_p6uqsu94/
├── .gitignore
├── CHANGES.md
├── LICENSE
├── MANIFEST.in
├── README.md
├── api_case/
│ ├── api_case.xlsx
│ └── confrun.py
├── demo/
│ ├── README.md
│ ├── __init__.py
│ ├── confrun.py
│ ├── reports/
│ │ └── .keep
│ ├── run.py
│ ├── test_data/
│ │ ├── csv_data.csv
│ │ ├── excel_data.xlsx
│ │ ├── json_data.json
│ │ └── yaml_data.yaml
│ └── test_dir/
│ ├── __init__.py
│ ├── api_case/
│ │ ├── __init__.py
│ │ └── test_http_demo.py
│ ├── app_case/
│ │ ├── __init__.py
│ │ ├── test_first_demo.py
│ │ ├── test_po_demo.py
│ │ └── test_u2_demo.py
│ └── web_case/
│ ├── __init__.py
│ ├── test_data_demo.py
│ ├── test_ddt_demo.py
│ ├── test_first_demo.py
│ ├── test_fixture_demo.py
│ ├── test_playwright_demo.py
│ └── test_po_demo.py
├── description.rst
├── docs/
│ ├── .gitignore
│ ├── README.md
│ ├── compare.md
│ ├── deploy.sh
│ ├── package.json
│ └── vpdocs/
│ ├── .vuepress/
│ │ └── config.js
│ ├── README.md
│ ├── api-testing/
│ │ ├── api_case.md
│ │ ├── api_object.md
│ │ ├── assert.md
│ │ ├── more.md
│ │ ├── start.md
│ │ └── webscocket.md
│ ├── app-testing/
│ │ ├── adb_lib.md
│ │ ├── appium_lab.md
│ │ ├── extensions.md
│ │ ├── page_object.md
│ │ └── start.md
│ ├── develop.md
│ ├── getting-started/
│ │ ├── advanced.md
│ │ ├── create_project.md
│ │ ├── data_driver.md
│ │ ├── dependent_func.md
│ │ ├── installation.md
│ │ ├── quick_start.md
│ │ └── seldom_cli.md
│ ├── introduce.md
│ ├── more-ability/
│ │ ├── benchmark.md
│ │ ├── db_operation.md
│ │ └── test_library.md
│ ├── platform/
│ │ └── platform.md
│ ├── version/
│ │ └── CHANGES.md
│ └── web-testing/
│ ├── browser_driver.md
│ ├── chaining.md
│ ├── other.md
│ ├── page_object.md
│ └── seldom_api.md
├── pyproject.toml
├── requirements.txt
├── seldom/
│ ├── __init__.py
│ ├── appdriver.py
│ ├── appium_lab/
│ │ ├── __init__.py
│ │ ├── action.py
│ │ ├── android.py
│ │ ├── appium_service.py
│ │ ├── find.py
│ │ ├── keyboard.py
│ │ ├── ocr_plugin.py
│ │ └── switch.py
│ ├── case.py
│ ├── cli.py
│ ├── db_operation/
│ │ ├── __init__.py
│ │ ├── base_db.py
│ │ ├── mongo_db.py
│ │ ├── mssql_db.py
│ │ ├── mysql_db.py
│ │ ├── postgres_db.py
│ │ └── sqlite_db.py
│ ├── driver.py
│ ├── extend_lib/
│ │ ├── __init__.py
│ │ ├── base_assert.py
│ │ ├── curlify.py
│ │ ├── jsonpath.py
│ │ ├── parameterized.py
│ │ └── tomorrow.py
│ ├── file_runner/
│ │ ├── __init__.py
│ │ └── api_excel.py
│ ├── har2case/
│ │ ├── __init__.py
│ │ ├── core.py
│ │ ├── demo.har
│ │ └── utils.py
│ ├── logging/
│ │ ├── __init__.py
│ │ ├── exceptions.py
│ │ └── log.py
│ ├── project_temp/
│ │ ├── __init__.py
│ │ ├── api/
│ │ │ ├── confrun.py
│ │ │ ├── run.py
│ │ │ └── test_sample.py
│ │ ├── app/
│ │ │ ├── confrun.py
│ │ │ ├── run.py
│ │ │ └── test_sample.py
│ │ ├── data.json
│ │ └── web/
│ │ ├── confrun.py
│ │ ├── run.py
│ │ └── test_sample.py
│ ├── request.py
│ ├── running/
│ │ ├── DebugTestRunner.py
│ │ ├── __init__.py
│ │ ├── config.py
│ │ ├── loader_extend.py
│ │ ├── loader_hook.py
│ │ └── runner.py
│ ├── skip.py
│ ├── swagger2case/
│ │ ├── __init__.py
│ │ ├── core.py
│ │ └── swagger.json
│ ├── testdata/
│ │ ├── __init__.py
│ │ ├── conversion.py
│ │ ├── parameterization.py
│ │ ├── random_data.py
│ │ └── random_func.py
│ ├── utils/
│ │ ├── __init__.py
│ │ ├── adbutils.py
│ │ ├── benchmark.py
│ │ ├── cache.py
│ │ ├── cache_data.json
│ │ ├── dependence.py
│ │ ├── diff.py
│ │ ├── encrypt.py
│ │ ├── file_extend.py
│ │ ├── genson.py
│ │ ├── jmespath.py
│ │ ├── match_image.py
│ │ ├── resource_loader.py
│ │ ├── send_extend.py
│ │ ├── thread_lab.py
│ │ └── timer.py
│ ├── webcommon/
│ │ ├── __init__.py
│ │ ├── find_elems.py
│ │ ├── keyboard.py
│ │ ├── locators.py
│ │ └── selector.py
│ ├── webdriver.py
│ ├── webdriver_chaining.py
│ └── websocket_client.py
└── tests/
├── data/
│ ├── country.graphql
│ ├── db.sqlite3
│ └── hello.txt
├── test_adb.py
├── test_api_object.py
├── test_autowing.py
├── test_base_assert.py
├── test_benchmark.py
├── test_browser.py
├── test_browser_new.py
├── test_cache/
│ ├── __init__.py
│ ├── test_cache.py
│ ├── test_cache_thread.py
│ └── test_memory_cache.py
├── test_db/
│ ├── __init__.py
│ ├── test_db_mssql.py
│ ├── test_db_mysql.py
│ ├── test_db_postgresdb.py
│ └── test_db_sqlite3.py
├── test_dependent_func.py
├── test_encrypt.py
├── test_fixture.py
├── test_graphql.py
├── test_http_assert.py
├── test_jsonpath.py
├── test_locators.py
├── test_log.py
├── test_other_lib/
│ ├── __init__.py
│ ├── test_playwright.py
│ ├── test_pyautogui.py
│ └── test_uiautomator.py
├── test_playwright_sample.py
├── test_random/
│ ├── __init__.py
│ └── test_testdata.py
├── test_request_extend.py
├── test_skip.py
├── test_steps_chaining.py
├── test_steps_chaining_browser.py
├── test_thread/
│ ├── __init__.py
│ ├── test_thread.py
│ ├── test_thread_browser.py
│ ├── test_thread_case.py
│ └── test_thread_path.py
├── test_utils/
│ ├── __init__.py
│ └── test_file.py
└── test_websocket/
├── __init__.py
├── test_websocket.py
└── webscoket_server.py
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
.idea
*.pyc
*.log
report
build
dist
seldom.egg-info
log/
.history
================================================
FILE: CHANGES.md
================================================
### 3.14.2
* 合并提交:
* `send_extend.py`: 规范参数多类型,避免警告提示。
* 优化`loadert('start run')`和`loader("end run")` 的调用。
* 升级依赖库的版本。
### 3.14.1
* 修复:脚手架:web示例错误。
* 修复:App API `drag_from_to()` 参数顺序错误。#262
* 修复:Web API `is_visible()` 执行报错。 #264
* 优化:`seldom.main()` 参数类型警告。
* 优化:`seldom` 命令,提升性能。
* 支持:python 3.14 测试通过。
* 新增:`HttpRequest` 增加`__init__()` 支持初始化`base_url`参数。
* 新增:提供`resource_file()` 加载 GraphQL 和 JSON 文件。
### 3.14.0
* App测试
* 功能:增加`ADBUtils`类,支持更多`adb`操作。
* Web测试:
* 增加`self.assert_screenshot()`断言图片。
* API测试:
* 增加`slef.save_response()`保存响应结果到文件中。
* 增加`self.ip_address()`获取请求的IP地址。
* 扩展模块重新实现`File`类。
* 修复`seldom.main()`方法`browser`参数类型警告。
* `typer`重写`seldom`命令行工具。
* 增加`PostgresDB`使用文档。
* 升级`Appium-Python-Client==5.1.0`。
### 3.13.0
* 功能:unittest所有基础断言增加日志, 例如:`assertEqual()`、`assertIn()`...。
* 功能:增加`adb`操作:`get_devices`、`launch_app`、`close_app`。
* 功能:`seldom.main()`增加`device`参数,用于存储设备ID。感谢@ThickBull
* 修复bug: appium提供的`query_app_state()`执行报错。
* 文档:增加`auto-wing` AI库使用示例。
* 文档:修复官方文档左侧菜单无法显示的问题。
* 升级`Appium-Python-Client==4.5.0`。
* 支持 `python 3.9 ~ 3.13` 版本。
### 3.12.0
* 功能:支持基准测试功能。
* 功能:提供加密模块,支持 `MD5`,`SHA1`,`AES`,`Base64`等各种`编码&解码`,`加密&解密`等算法。
* 功能:`seldom.main()`增加`env`参数,用于设置静态变量。
* 功能:HTML测试报告根据网络判断是否生成本地本地静态文件,无网络情况也可正常显示报告。
* 修复:`dependent_func()`装饰器调用静态方法报错。感谢@Catking233
### 3.11.0
* 功能:平台化用例执行,`seldom.main()`支持加载`confrun.py`中的 `start_run()/end_run()`。
* 功能:平台化用例解析,识别用例标签`label`。
* HTTP测试:通过`confrun.py`支持`proxies()`配置全局的请求代理。
* App测试:
* appium_lab 增加 `drag_from_to()`方法,支持坐标位滑动。感谢@guweifan
* appium_lab 增加`AppiumService`类,支持启动appium server。感谢@guweifan
* 优化:`jsonpath.py`的代码。
### 3.10.0
* 重要:所有app/web元素定位支持`selector`模式,详细查看文档。
* 更新:`sleep()`增加默认值1s,也支持随机休眠范围:`self.seep((1, 3))`。
* 更新: `appium_lab`模块的 `Action()` 类下面的方法支持自定义休眠时间、间隔时间等。
* 修复:`Steps()`类的 `open()` 方法默认传url报错 [#241](https://github.com/SeldomQA/seldom/issues/241)。
* 告警:`type_enter()`添加移除警告,推荐使用`type()`。
* 文档:
* 修改playwright使用示例。
* 增加pyAutoGUI使用示例。
### 3.9.1
* 更新:脚手架项目模板,增加`run.py`文件。
* 修复:生成随机数,获取在线时间接口错误。
* 修复:`datetime.utcnow()`在Python 3.12 告警。
* App测试:
* 修复`back()`、`home()`方法报错。
* 增加`long_press_key()`方法。
* API测试:
* 增加`assertStatusOk()`断言方法,断言接口返回状态码`200`。
* `@check_response()`装饰器重命名`@api()`,更简洁。
* Web测试:
* 增加`prompt_value()`方法,支持弹窗输入 [#166](https://github.com/SeldomQA/seldom/issues/166)。
* 增加`action_chains()`方法,返回Selenium的`ActionChains()`
类对象 [#119](https://github.com/SeldomQA/seldom/issues/119)。
* 增加`is_visible()`方法,检查页面元素是否可见 [#62](https://github.com/SeldomQA/seldom/issues/62)。
* `Pycharm`右键运行Web UI用例,抛异常提示。
* 文档更新:
* 增加浏览器代理设置示例 [#31](https://github.com/SeldomQA/seldom/issues/31)。
* 操作已打开浏览器示例 [#174](https://github.com/SeldomQA/seldom/issues/174)。
* 升级:`XTestRunner==1.8.0`。
### 3.9.0
* App测试。
* 升级`Appium-Python-Client==4.1.0`
* 提供`UiAutomator2Options`和`EspressoOptions`类,替换appium提供的这个两个类。
* 移除不再支持的API: `launch_app()`、`close_app()`、`reset()`。
* 增加App相关操作时的日志。
* Web测试浏览启动重构。
* 支持`start/end`启动和关闭浏览器。
* 支持`start_class/end_class`启动和关闭浏览器。
* 支持`new_browser()`重新打开一个浏览器。
* `self.open()` 检测到没有指定浏览器,不再默认启动一个`Chrome()`浏览器。
* 链式API `Steps()`类添加`browser`参数。
* `Seldom.driver`对象支持多线程。
* `log`日志显示当前运行的线程。
* `Cache`缓存类支持多线程。
* 其他:移除直接依赖库:`requests`和`websocket-client`, 使用间接依赖。
* `XTestRunner` -> `requests`
* `Appium-Python-Client` -> `selenium` -> `websocket-client`
### 3.8.1
* App测试。
* 支持`Appium-Python-Client==4.0.1`,修复`4.0.0` 引起的问题。
* `seldom` 命令,创建项目命令区分`web/app/api`项目。
* 修复`seldom-platform`平台运行错误。
### 3.8.0
* API测试:支持执行Excel测试用例, `seldom --api-excel api_case.xlsx` 具体用法查看文档。
* App:增加 `self.keyboard_search()`模拟键盘上的搜索按键。
* 优化: `@file_data()`参数化装饰器代码。
### 3.7.1
* 优化:`main()` 中的`path`参数支持列表,可以指定多个目录或文件。
* 新增:提供`from seldom.utils.send_extend import RunResult` 获取用例的执行数据。
* App测试。
* 增加`swipe_right()`左滑 和 `swipe_left()`右滑支持。
* `AppiumLab()` 默认允许不传`driver`参数。
* 其他:
* `Python 3.12` 测试通过。
### 3.7.0
* `@data()`数据驱动装饰器增加`cartesian=True`参数,支持笛卡尔积。
* 新增`WebSocket`接口测试支持。
* App测试。
* 支持`Appium-Python-Client==4.0.0`,修复`4.0.0` 引起的问题。
* Web测试
* 重新支持指定浏览器驱动,使用`executable_path`参数。
* 其他:
* 基于`selenium`依赖库,移除 `Python 3.7` 支持。
### 3.6.0
* `seldom.main()`方法增加`failfast`参数,debug模式,允许第一条用例失败,停止执行。
* 增加`@retry()`装饰器,用于函数&方法错误重试。
* HTTP测试
* 支持`swagger`文档转seldom用例,使用命令 `seldom -s2c swagger.json` 。
* 文档:增加 API Object model 概念的介绍,以及seldom中的应用。
### 3.5.0
* 新增:支持 Postgre SQL 数据库操作。
* web测试
* `pause()` 用于暂停操作。
* 移除`webdriver_manager_extend.py`文件(之前漏移除文件)。
* App测试
* 支持`appium 2.0` 正式版。
* 支持appium-OCR-plugin插件。
* 增加`click_image()`方法,支持图片点击定位。
* `press_key()` 支持`ENTER`参数,模拟键盘回车。
### 3.4.1
* 修复:`diff_json()` 对比特殊数据的异常没有捕捉到。
* web测试
* `screenshots()` 增加`images`参数,支持传入截图对象 [#202](https://github.com/SeldomQA/seldom/issues/202)。
* `open_electron()` 增加`chromedriver_path`参数,支持手动指定驱动地址。
* `setUpClass()`/`tearDownClass()` 增加异常捕捉,避免报错之后,用例无法统计的问题。
### 3.4.0
* 新增:`dependent_func()`装饰器,支持用例方法依赖调用,具体使用参考文档。
* api测试
* 修复:har2case 请求头参数类型判断不准的问题。
* web测试
* 增加`open_electron()` 方法,支持启动桌面electron应用。
* 键盘操作`Key()`支持链式调用,例如: `self.Keys(id_="kw").select_all().cut()` 全选并删除。
* cache操作日志增加 emoji。
* 修复:`diff_json()` 优化,支持dict深度排序。 [#197](https://github.com/SeldomQA/seldom/issues/197)
### 3.3.0
* web测试
* 浏览器驱动`webdriver-manager` 替换为`selenium-manager`。
* 增加`execute_cdp_cmd()` 方法。
* 随机数据
* `online_timestamp()` 在线获取时间戳。
* `online_now_datetime()` 在线获取当前时间,格式为:`%Y-%m-%d %H:%M:%S`。
* 增加运行时内嵌(built-in)方法:`base_url()`、`driver()` - 无需导入,可以在自动化程序任意位置使用这两个方法。
* 移除`parameterized` 库的依赖,改为内置。
* 修复:`diff_json()` 对比 `[{}]` 数据时报错。 [#197](https://github.com/SeldomQA/seldom/issues/197)
### 3.2.3
* HTTP自动化
* `confrun.py` 支持 `mock_url` hook 钩子函数。
* 增加 `self.base_url` 获取 `base_url`。
* Web自动化
* 更新:`get_elements()` 增加`empty`参数,设置为`True`, 允许返回空列表 `[]`
* 更新: `debug=True` 模式,移除操作元素边框高亮,提高用例执行速度。
* App测试
* 修复:`key_text()` 无法输入点号`.`的问题。
* 优化:`seldom_log.log` 文件只记录一次运行结果,减少文件大小。
* 升级:`webdriver_manager==4.0.0` [#189](https://github.com/SeldomQA/seldom/issues/189)
* 其他: 添加 `pyproject.toml` 支持。
* 文档:增加其他库的使用例子。
### 3.2.2
* 功能:增加`@threads()`支持多线程运行用例。
* 功能:增加`@rerun()` 重复执行某个测试方法。
* 功能:数据库操作
* `MySQLDB()`、`MSSQLDB()` 支持`charset` 参数设置字符集。
* `init_table()` 批量插入数据库增加`clear` 参数,可以选择是否删除表再插入。
* 功能:Web自动化
* 新增`save_screenshot()` 截图保存本地。
* 修改`screenshots()` 自动截图保存到HTML报告,移除`file_path` 参数。
* 修改`element_screenshot()` 元素截图保存到HTML报告,移除`file_path` 参数。
* `type()` 方法增加 `click` 参数,针对app元素优化,app的输入框往往需要点击以下锁定光标再输入。
* 修复:浏览器配置参数 `option` 更名为 `options`。
* 其他:增加 python3.11 支持。
### 3.2.1
* 功能:增加`@disk_cache()`、`@memory_cache()` 缓存装饰器。
* 功能:app测试,seldom支持本身API支持appium定位。
* 功能:db操作,增加`insert_get_last_id()` 方法,插入数据并返回id。
* 修复:`@data_class()` 必传`input_values` 参数问题。
* 修复:设置log等级,HTML报告无法根据等级打印日志问题。
### 3.2.0
* Web UI测试,增加一组新的警告框 alert 操作。
* `self.alert.text`
* `self.alert.accept()`
* `self.alert.dismiss()`
* `self.alert.send_keys("text")`
* App UI测试。
* `AppiumLab()` 类增加 `context()` 方法获取当前上下文。
* `AppiumLab()` 类增加 `size()` 当前窗口尺寸。
* API 测试。
* 增加`self.patch()` 请求方法。
* 增加`self.json_to_dict()` 支持单引号JSON格式转字典。
* cache 增加文件锁,防止多线程读写错误(Windows不支持 fcntl)
* 支持 `XTestRunner=>1.6.2` 版本
* XML格式的报告支持 rerun 重跑参数。
* HTML 报告skip用例样式微调。
* HTML 重跑只显示最后一次结果。
* SMTP 发送报告增加 `ssl` 参数。
* `seldom.main()` 方法 ⚠ 不兼容更新
* 移除 `save_last_run` 参数。
* `browser` 参数支持`dict` 格式, 所有和浏览器配置相关的有发生修改。 包括
* 设置浏览器驱动地址。
* 设置 headless 模式。
* 设置 options 参数。
* 设置 selenium grid 地址。
### 3.1.3
* 功能:`file_data()` 增加`end_line`
参数,对于csv/excel文件支持读取到第几行结束。[#163](https://github.com/SeldomQA/seldom/issues/163)
* 优化:`self.assertElement()` 断言元素时间过长的问题。
* 优化:`self.assertJSON()` 断言日志,区分告警和错误。
* 移除:`self.jresponse()` 方法。
### 3.1.2(internal)
> 内部版本:移除了日志打印的 emoji 表情。
* 功能:`seldom.main()` 方法 path 参数支持斜杠路径`\`(windows系统用`\` 表示路径)。
### 3.1.1
* 功能:`confrun.py` 增加`start_run()/end_run()` 钩子函数,用于运行前/后相关配置。
* 优化:`@api_data()` 装饰器增加 `headers` 参数。
* 优化:`assertJSON()` 断言增加 `exclude` 参数,屏蔽检查的字段,例如 `["start_time", "token"]`。
* 修复:`rediscover()` 查找用例bug。
* 依赖:升级`XTestRunner==1.5.0` 支持飞书/微信发送消息。
### 3.1.0
* 功能:提供 `confrun.py` 运行配置文件,配合 `seldom` 命令使用。
* 功能:Web测试,增加 `self.get_log()` 方法。
* 升级:`webdriver_manager==3.8.5` ,支持Mac M1芯片的浏览器驱动。[#159](https://github.com/SeldomQA/seldom/issues/159)
* 修复:seldom-platform平台同步多个项目引起的Bug。[#158](https://github.com/SeldomQA/seldom/issues/158)
* 修复:Web测试, `self.close()` 关闭浏览器Bug。
### 3.0.1
* 功能:支持 `SQL Server` 数据库支持,需要单独安装`pymssql`库。
* 功能:http接口测试增加`curl()`方法,支持请求转 `cURL`。
* 功能:`seldom` 命令增加`--log-level` 参数,log类型:`TRACE`, `DEBUG`, `INFO`, `SUCCESS`, `WARNING`, `ERROR` 等。
### 3.0.0
* `seldom 3.0` 的核心是支持app测试,并且相关API已稳定,目的已达到,接下来将会在`3.0`基础上继续开发。
* 功能:`collect_cases()` 支持 `warning` 参数。
### 3.0.0beta2
* 修复:
* 接口测试: 接口返回文本`r.text` 中文乱码问题。[#146](https://github.com/SeldomQA/seldom/issues/146)
* app测试:感谢 @986379041
* `install_app()` 错误
* `close_app()` 错误
* 功能:
* `TestMainExtend` 类增加 `tester`参数。 [#149](https://github.com/SeldomQA/seldom/issues/149)
* 生成随机数,增加`get_month()` 和 `get_year()`方法。 [#152](https://github.com/SeldomQA/seldom/issues/152)
* seldom命令增加清除所有缓存。`> seldom --clear-cache true`。 [#153](https://github.com/SeldomQA/seldom/issues/153)
* 其他:
* seldom 运行用例,优化内存使用。
### 3.0.0beta1
* 支持App测试
* 依赖`Appium-Python-Client`库。
* `main()` 增加 `app_info`, `app_server` 参数。
* 增加`appium_lab` 模块。
* 增加`AppDriver` 类。
* 优化:基于pylint检查分析工具 优化代码。
* 其他:
* 生成随机数,增加`get_timestamp()` 获取当前时间戳。
* 数据库查询,增加`query_one()` 查询一条数据。
### 2.10.6 ~ 2.10.7
* 功能:`seldom`命令重大更新,支持更多参数和功能。
* 功能:`@file_data()` 当设置`Seldom.env`时支持更深一级遍历。
* 修复:`diff_json()` 对比数据错误。
### 2.10.4 ~ 2.10.5
* 重构log日志打印。 @Yongchin
* 彻底修复日志重复打印的问题。
* 移除`log.printf()` 非标准日志类型。
* 修复:
* `sender()` 发送完邮件,`seldom_log.log` 文件无法删除的问题。
* `TestMainExtend` 类`run_cases()`按照用例的顺序执行。@luna-CY
* 修复`request` 带上`url=` 参数时异常。 @986379041@qq.com
* 依赖:`webdriver_manager`依赖升级到`3.8.2`
* 移除:`Opera` 浏览器的支持,selenium 4 已经移除了对opera的单独驱动支持。
### 2.10.3
* 数据驱动:`@data()` 和 `@file_data()` 优化用例名称和描述。
* 增加`Seldom.env`环境配置变量,`@file_data()` 数据驱动装饰器支持环境变量。
* 修复:`Edge`浏览器启动错误。
* 修复:HTTP接口测试`self.post()`方法 `data`参数不是dict类型错误。
* 平台化支持:优化用例收集,具体查看文档。
### 2.10.2
*
更新:移动模式列表更新,去掉旧设备,增加新设备 [link](https://github.com/SeldomQA/seldom/blob/master/docs/vpdocs/other/other.md)
* 功能:测试报告显示断言信息。
* 功能:`main()` 通过`open=False`可以控制运行完测试 不自动化打开测试报告。
* Web 测试:
* 增加`self.new_browser()` 可以打开新的浏览器,但只能使用`selenium` 的 API
* 增加`switch_to_frame_parent` 切换到上一级表单,[#118](https://github.com/SeldomQA/seldom/issues/118)。
* 优化`assertNotElement` 执行慢的情况 [#120](https://github.com/SeldomQA/seldom/issues/120)
* HTTP 测试:
* 优化:JSON日志进行格式化打印。
### 2.10.1
* 修复:seldom log 问题引起,错误信息无法在控制台打印。
> 2.10.0 为了解决[107](https://github.com/SeldomQA/seldom/issues/107)
> 问题,我们经过反复的讨论和优化,甚至对相关库XTestRunner做了修改;以为完美解决了这个问题,没想到还是引起了一些严重的错误。为此,我们感到非常沮丧,退回到2.9.0的实现方案。请升级到2.10.1版本。
### 2.10.0
* seldom log功能:
* 修复打印日志显示固定文件的问题 [107](https://github.com/SeldomQA/seldom/issues/107)。
* log方法变更:`log.warn()` -> `log.warning()`。
* 功能:提供了`cache` 类来模拟缓存。
* 功能:`@data()` 装饰器支持 `dict` 格式。
* 功能:`self.jresponse()` 方法设计不合理,给以废弃提示;可以使用`self.jsonpath()`/`self.jmespath()` 替代。
* 优化:断言方法`assertSchema()`、`assertJSON()`支持`response`传参。
* 优化:`@check_response()` check检查失败打印`response`。
* 修复:`webdriver_manager` 没有设置上限版本,导致`webdriver_manager>=3.6.x` 报错; 如果使用的 `seldom<=2.9`
请重新安装`webdriver_manager==3.5.2`。
### 2.9.0
* seldom log功能:
* 开放seldom 的`log`能力,可以配置`颜色(colorlog)`、`格式(format)`、`等级(level)` 等。
* 重新定义了seldom打印日志的格式。
* 所有log统一记录到`/reports/seldom_log.log`文件,不再每次生成单独文件。
* 功能:提供了`@check_response()` 装饰器,为接口封装提供强大的支持。
* 功能:集成`genson`库,生成JsonSchema模板 [100](https://github.com/SeldomQA/seldom/issues/100) 。
* 功能:增加`assertInPath()` 断言方法。
* 功能:增加`jmespath()`方法,方便提取测试数据。
* 优化:`jresponse()` 增加对`jmespath` 语法的支持。
* 优化:支持`self.get()/self.post()/self.put()/self.delete()` 返回response对象。
### 2.8.0
* 功能:增加MongoDB 数据库操作 [93](https://github.com/SeldomQA/seldom/issues/93) 。
* 功能:支持单个用例执行 [94](https://github.com/SeldomQA/seldom/issues/94) 。
* 功能:`sendmail()` 增加`delete`参数,发送完邮件删除`reports/`
目录下面的报告和日志文件 [95](https://github.com/SeldomQA/seldom/issues/95) 。
* 功能:增加`jsonpath` 和 `jresponse()` ,更容易查找json数据 [96](https://github.com/SeldomQA/seldom/issues/96) 。
* 功能:创建项目脚手架增加api测试例子:`seldom -project mypro` 。
* 其他: 全新的seldom在线文档:https://seldomqa.github.io/ ,感谢 @nickliya
### 2.7.0
* 功能:引入`loguru` 库用于打印日志(之前使用python默认logging总有一些重复打印或不打印的问题)。
* 功能:web自动化增加一套方法链(method chaining)的API。
* 功能:支持手动指定浏览器驱动路径。
### 2.6.0
* 移除:自带的`HTMLTestRunner`,HTML报告采用`XTestRunner`。
* 移除:对`unittest-xml-reporting`库的依赖,XML报告使用`XTestRunner`。
* 修改:`SMTP`类发送邮件方法 `sender()` -> `sendmail()`, 发送邮件样式采用`XTestRunner`。
* 增加:`seldom.main()`方法增加`tester` 参数,用于设置测试人员名字,默认`Anonymous`。
* 增加:`seldom.main()`方法增加`language` 参数,用于设置报告中英文`en/zh-CN`,默认`en`。
* 增加:发送钉钉功能。
* 修改:接口测试 `self.session` -> `self.Session()`。
* 移除:接口测试 `self.request()` 方法移除(注:该方法原本不可用)。
### 2.5.1
* 功能:Http接口测试使用日志打印接口信息
* 功能:Http接口测试打印`json`参数 [83](https://github.com/SeldomQA/seldom/issues/83)
* 修复:Web UI测试`self.Key()` 无法定位元素的问题
### 2.5.0
* 功能:支持测试平台化。
* 功能:utils 增加`file`类,获取当前文件目录更方便。
* 修复:`self.select()` 操作下拉选择错误。
* 修复:`diff_json()` 对比json文件错误。
### 2.4.2
* 功能:增强`@file_data`使用方式,json/yaml支持内嵌`dict`数据。
### 2.4.1
* 优化:HTTP接口测试增加`cookies`信息打印。
* 优化:`@file_data()` 使用,支持指定目录。
* 修复:`visit()` 方法默认浏览器没有自动安装浏览器驱动的问题。
* 修复:`query_sql()` 执行SQL没有提交的问题。
### 2.4.0
* 适配selenium 4.0+ ,适配相关依赖库新版本。
* 测试用例支持`label`标签分类。
* 接口测试增加打印入参信息 [79](https://github.com/SeldomQA/seldom/issues/79) 。
* EdgeChromium浏览器支持`headless`模式。
* Web自动化测试增加元素截图`self.element_screenshot()`
* 优化HTML测试报告样式。
* 优化邮件模板样式。
### 2.3.3
* 增加 `assertNotText()` 断言方法 [75](https://github.com/SeldomQA/seldom/issues/75) 。
* 修复`main()`设置`rerun` 和 `save_last_run`参数,导致用例统计错误 [76](https://github.com/SeldomQA/seldom/issues/76) 。
### 2.3.2
* 接口调用如果是图片类型,不在打印内容。
* 增加`screenshot` 针对定位的元素截图, 用法`self.screenshot(id="xx")`。
* 测试报告:优化截图的样式。
* 发邮件功能,默认增加附件为测试报告。
### 2.3.1
* 修复`assertUrl()`、`assertInUrl()` 断言中文编码错误。
* 增加文件路径操作。
* `file_path()` 获取当前文件路径。
* `file_dir()` 获取当前文件目录。
* `file_dir_dir()` 获取当前文件目录的目录。
* `file_dir_dir_dir()` 获取当前文件目录的目录的目录。
* `init_env_path()` 添加路径到环境变量。
* 优化`main()` 方法中代码的执行顺序。
### 2.3.0
* 集成 `webdriver-manager`,不需要再单独安装浏览器驱动。
* seldom logo 显示版本号。
* 固定`selenium`版本号,暂没做`4.0.0`适配。
### 2.2.4
* 修复HTTP接口测试,指定`url`参数错误的问题。[71](https://github.com/SeldomQA/seldom/issues/71)
* 支持发送多人邮件。[72](https://github.com/SeldomQA/seldom/issues/72)
* 优化HTMLTestRunner, 重跑次数不记录为用例数。
* 修复pip安装缺少`description.rst` 问题。
### 2.2.3
* 支持控制台操作步骤显示在HTML报告中。[42](https://github.com/SeldomQA/seldom/issues/42)
* 修改`get_elements()`返回空列表。[69](https://github.com/SeldomQA/seldom/issues/69)
* 修复因为`colorama`/`emoji`导致的编码错误。[70](https://github.com/SeldomQA/seldom/issues/70)
### 2.2.2
* 优化db操作方法。
* 打印`logs`合并到 `reports` 目录。
### 2.2.1
* webdriver文件增加类型。
* 删除utils 错误代码。
* 修复:`diff_json()` 函数处理复杂数据报错 #66
* 修复:运行接口测试用例报 driver 错误 #68
* 修复:测试报告`popper.min.js` CDN 太慢的问题
### 2.2.0
* 增加接口测试方法`session`、`request`。
* 增加`seldom -h2c`参数,用于将har文件转成测试用例。
### 2.1.1
* 增加随机生成时间方法`get_past_time()`、`get_future_time()`
* 优化:截图方法`screenshots()`,可以在任意位置使用该方法生成截图,并显示在HTML测试报告中。
* 修复:接口测试`main()`中base_url 和 方法中的 url 同时存在的问题。
* 修复:优化MySQL数据库连接的问题。
* 修复:发送邮件时的错误。
* 修复:当`main()`中的timeout设置为1时,断言失败的问题。
### 2.1.0
* 增加数据库操作,同时支持`sqlite3`、`mysql`。
* 优化`file_data()`,兼容2.0.0用法。
### 2.0.1
* 优化 `file_data()`, 自动查找数据文件。
* 优化脚手架,创建项目例子更新。
### 2.0.0
* webdriver API 修改
* 移除 `self.get()`
* 增加 `self.visit()`
* 移除 `self.open_new_window()`
* 移除 `self.current_window_handle()`
* 移除 `self.new_window_handle()`
* 移除 `self.window_handles()`
* 修改 `self.switch_to_window()` 用法
* 优化打印日志,为每种操作加上 emoji
* 增加`expected_failure`用例装饰器,用于标记一条用例失败
* 增加 `file_dir()`, 返回当前文件所在目录的绝对路径。
* 运行完成自动通过浏览器打开HTML报告
* `main()`方法修改
* 修复`debug`参数类型错误异常提示
* 控制台更换字符logo*
* 整合 webdriver/request
* 上线 readthedocs 文档
### 2.0.0.beta
* 支持 HTTP接口测试
### 1.10.3
...
### 1.10.2
* HTMLTestRunner代码优化
* 修复bug
### 1.10.1
* webdriver代码重构
* 修复严重bug
### 1.10.0
* 增加断言元素方法:`assertElement`、`assertNotElement`
* 增加单个测试类、用例执行的方法
* 修复报告样式bug
* 命令行工具优化
### 1.9.0
* 测试报告重构
* 用例描述单独一列
* 增加单个用例运行时间
* 新的报告样式
* 脚手架工具创建项目更新
* 增加随机生成手机号方法
### 1.8.0
* 增加用例依赖装饰器
### 1.7.2
* bug修复版本
### 1.7.0
* 重构浏览器驱动,开放浏览器可配置能力。
### 1.6.0
* 浏览器增加简写
* 支持 logs 日志
* 支持 XML 测试报告
* 增加 file_data 方法实现参数化。
* 修复一些bug
### 1.5.6
* 封装test fixture方法
### 1.5.5
* 修改HTMLTestRunner 错误日志的展示
* 增加mobile web的支持
### 1.5.4
* 增加keys键盘操作
* 元素操作增加聚焦
* debug 模式增加慢操作
### 1.5.3
* 修复bug
* 增加 yaml_to_list()方法
### 1.5.2
* 修复bug
### 1.5.1
* 修复日志重复打印问题
* 修复测试报告不截图问题
* 日志增加emoji表情
### 1.5.0
* 自动化运行过程中,对操作的元素加边框,使其更醒目。
* 去掉对 `setUpClass()`方法的占用,代码做了较大重构。
* 在使用poium时,驱动的获取方式改变,这一点不向下兼容。
### 1.2.6
* 完善自动化发邮件功能
* 增加 type_enter() 方法
* 优化项目的代码的调用
* 修复 seldom + poium 日志问题
### 1.2.5
* 重新定制测试报告样式
* seldom.main()增加timeout参数
### 1.2.4
* 增加数据解析相关操作方法
* 增加跳过测试相关方法
* 增加发邮件功能
* 修复bug, 优化代码
### 1.2.3
* 增加 slow_click() 方法。
* seldom.main() 默认运行当前文件不需要传参。
* seldom.main(report="report-name.html") 允许自定义报告名称。
### 1.2.2
* fix bug
* add function: csv_to_list()/ excel_to_list()
### 1.2.0
Global launch browser
### 1.1.0
selenium grid support
Added safari support
### 1.0.0
The framework function has been basically improved. I'm glad to release version 1.0
### 0.3.6
Add cookie manipulation APIs
Optimized element wait
### 0.3.5
Added chrome/firefox browser driver download command
Driver file path Settings are supported
### 0.3.3
add skip case
### 0.3.2
Added a switch to display the last rerun result
Optimized assertion method
### 0.3.0
Update element positioning
### 0.2.0
Change the project name to seldom
Introducing the poium test library,
### 0.1.5
* Increased test case failure rerun
* Add use case failure screenshots
### 0.1.2
new framework
#### 0.0.9
Simplifying API calls
#### 0.0.8
add parameterized
Beautification test report
#### 0.0.7
Re based on unittest.
#### 0.0.6
add setup.py file, Specification of the installation process, a time to install all dependencies.
Delete unnecessary files
#### 0.0.5
Increase the support of multiple positioning methods
#### 0.0.4
Method to add default to wait.
Modify the realization of the individual methods
#### 0.0.3.1 version update:
* Repair part bug.
#### 0.0.3 version update:
* With the nose instead of unittest.
* Discard HTMLTestRunner,Integrated nose-html-reporting.
* modify the examples under demo.
#### 0.0.2 version update:
* all the elements of the operation selector xpath replaced by css, css syntax because more concise.
* when you run the test case no longer need to specify the directory, the default directory for the current test.
* modify the examples under demo.
================================================
FILE: LICENSE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2019 Software Freedom Conservancy (SFC)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
FILE: MANIFEST.in
================================================
recursive-include seldom/running/html *.html
================================================
FILE: README.md
================================================
[GitHub](https://github.com/SeldomQA/seldom) | [Gitee](https://gitee.com/fnngj/seldom) |

[](https://badge.fury.io/py/seldom) 
Seldom is an automation testing framework based on unittest.
> seldom 是基于unittest 的自动化测试框架。
### Features
⭐ web/app/api全功能测试框架
⭐ 提供脚手架快速创建自动化项目
⭐ 集成`XTestRunner`测试报告,现代美观
⭐ 提供丰富的断言
⭐ 提供强大的`数据驱动`
⭐ 平台化支持
### Install
```shell
pip install seldom
```
If you want to keep up with the latest version, you can install with GitHub/Gitee repository url:
```shell
> pip install -U git+https://github.com/SeldomQA/seldom.git@master
> pip install -U git+https://gitee.com/fnngj/seldom.git@master
```
### 🤖 Quick Start
1、查看帮助:
```shell
seldom --help
Usage: seldom [OPTIONS]
seldom CLI.
╭─ Options ────────────────────────────────────────────────────────────────────────────────────────╮
│ --version -v Show version. │
│ --project-api -api TEXT Create a project of API type. [default: None] │
│ --project-app -app TEXT Create a project of App type [default: None] │
│ --project-web -web TEXT Create a project of Web type [default: None] │
│ --clear-cache -cc Clear all caches of seldom. │
│ --log-level -ll TEXT Set the log level [TRACE |DEBUG | INFO | SUCCESS | │
│ WARNING | ERROR]. │
│ [default: None] │
│ --mod -m TEXT Run tests modules, classes or even individual test │
│ methods from the command line. │
│ [default: None] │
│ --path -p TEXT Run test case file path. [default: None] │
│ --env -e TEXT Set the Seldom run environment `Seldom.env`. │
│ [default: None] │
│ --browser -b TEXT The browser that runs the Web UI automation tests │
│ [chrome | edge | firefox | chromium]. Need the --path. │
│ [default: None] │
│ --base-url -u TEXT The base-url that runs the HTTP automation tests. Need │
│ the --path. │
│ [default: None] │
│ --debug -d Debug mode. Need the --path/--mod. │
│ --rerun -rr INTEGER The number of times a use case failed to run again. │
│ Need the --path. │
│ [default: 0] │
│ --report -r TEXT Set the test report for output. Need the --path. │
│ [default: None] │
│ --collect -c Collect project test cases. Need the --path. │
│ --level -l TEXT Parse the level of use cases [data | case]. Need the │
│ --path. │
│ [default: data] │
│ --case-json -j TEXT Test case files. Need the --path. [default: None] │
│ --har2case -h2c TEXT HAR file converts an seldom test case. [default: None] │
│ --swagger2case -s2c TEXT Swagger file converts an seldom test case. │
│ [default: None] │
│ --api-excel TEXT Run the api test cases in the excel file. │
│ [default: None] │
│ --install-completion Install completion for the current shell. │
│ --show-completion Show completion for the current shell, to copy it or │
│ customize the installation. │
│ --help Show this message and exit. │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
```
2、创建项目:
```shell
> seldom -api myapi # API automation test project.
> seldom -app myapp # or App automation test project.
> seldom -web myweb # or Web automation test project.
```
目录结构如下:
```shell
myweb/
├── test_dir/
│ ├── __init__.py
│ └── test_sample.py
├── test_data/
│ └── data.json
├── reports/
└── confrun.py
```
* `test_dir/` 测试用例目录。
* `test_data/` 测试数据文件目录。
* `reports/` 测试报告目录。
* `confrun.py` 运行配置文件。
3、运行项目:
* ❌️ 在`PyCharm`中右键执行。
* ✔️ 通过命令行工具执行。
```shell
> seldom -p test_dir # 运行 test_dir 测试目录
__ __
________ / /___/ /___ ____ ____
/ ___/ _ \/ / __ / __ \/ __ ` ___/
(__ ) __/ / /_/ / /_/ / / / / / /
/____/\___/_/\__,_/\____/_/ /_/ /_/ v3.x.x
-----------------------------------------
@itest.info
...
2022-04-30 18:37:36 log.py | INFO | ✅ Find 1 element: id=sb_form_q -> input 'seldom'.
2022-04-30 18:37:39 log.py | INFO | 👀 assertIn title: seldom - 搜索.
.52022-04-30 18:37:39 log.py | INFO | 📖 https://cn.bing.com
2022-04-30 18:37:41 log.py | INFO | ✅ Find 1 element: id=sb_form_q -> input 'poium'.
2022-04-30 18:37:42 log.py | INFO | 👀 assertIn title: poium - 搜索.
.62022-04-30 18:37:42 log.py | INFO | 📖 https://cn.bing.com
2022-04-30 18:37:43 log.py | INFO | ✅ Find 1 element: id=sb_form_q -> input 'XTestRunner'.
2022-04-30 18:37:44 log.py | INFO | 👀 assertIn title: XTestRunner - 搜索.
.72022-04-30 18:37:44 log.py | INFO | 📖 http://www.itest.info
2022-04-30 18:37:52 log.py | INFO | 👀 assertIn url: http://www.itest.info/.
.82022-04-30 18:37:52 log.py | SUCCESS | generated html file: file:///D:\mypro\reports\2022_04_30_18_37_29_result.html
2022-04-30 18:37:52 log.py | SUCCESS | generated log file: file:///D:\mypro\reports\seldom_log.log
```
4、查看报告
你可以到 `mypro\reports\` 目录查看测试报告。

## 🔬 Demo
> seldom继承unittest单元测试框架,完全遵循unittest编写用例规范。
[demo](/demo) 提供了丰富实例,帮你快速了解seldom的用法。
### Web UI 测试
```python
import seldom
from seldom import Steps
class BaiduTest(seldom.TestCase):
def test_case_one(self):
"""a simple test case """
self.open("https://www.baidu.com")
self.type(id_="kw", text="seldom")
self.click(css="#su")
self.assertTitle("seldom_百度搜索")
def test_case_two(self):
"""method chaining """
Steps().open("https://www.baidu.com").find("#kw").type("seldom").find("#su").click()
self.assertTitle("seldom_百度搜索")
if __name__ == '__main__':
seldom.main(browser="chrome")
```
__说明:__
* `seldom.main()` 通过 `browser` 指定运行的浏览器。
### HTTP 测试
seldom 2.0 支持HTTP测试
```python
import seldom
class TestRequest(seldom.TestCase):
def test_put_method(self):
self.put('/put', data={'key': 'value'})
self.assertStatusCode(200)
def test_post_method(self):
self.post('/post', data={'key': 'value'})
self.assertStatusCode(200)
def test_get_method(self):
payload = {'key1': 'value1', 'key2': 'value2'}
self.get("/get", params=payload)
self.assertStatusCode(200)
def test_delete_method(self):
self.delete('/delete')
self.assertStatusCode(200)
if __name__ == '__main__':
seldom.main(base_url="http://httpbin.org")
```
__说明:__
* `seldom.main()` 通过 `base_url` 指定接口项目基本URL地址。
### App 测试
seldom 3.0 支持App测试
```python
import seldom
from seldom.appium_lab.keyboard import KeyEvent
from seldom.appium_lab.android import UiAutomator2Options
class TestBingApp(seldom.TestCase):
def start(self):
self.ke = KeyEvent(self.driver)
def test_bing_search(self):
"""
test bing App search
"""
self.sleep(2)
self.click(id_="com.microsoft.bing:id/sa_hp_header_search_box")
self.type(id_="com.microsoft.bing:id/sapphire_search_header_input", text="seldomQA")
self.ke.press_key("ENTER")
self.sleep(1)
elem = self.get_elements(xpath='//android.widget.TextView')
self.assertIn("seldom", elem[0].text.lower())
if __name__ == '__main__':
capabilities = {
'deviceName': 'ELS-AN00',
'automationName': 'UiAutomator2',
'platformName': 'Android',
'appPackage': 'com.microsoft.bing',
'appActivity': 'com.microsoft.sapphire.app.main.MainSapphireActivity',
'noReset': True,
}
options = UiAutomator2Options().load_capabilities(capabilities)
seldom.main(app_server="http://127.0.0.1:4723", app_info=options, debug=True)
```
__说明:__
* `seldom.main()` 通过 `app_info` 指定App信息; `app_server` 指定appium server 地址。
## 📖 Document
[中文文档](https://seldomqa.github.io/)
### 项目实例
B站实战视频:
https://www.bilibili.com/video/BV1QHQVYoEHC
基于seldom的web UI自动化项目:
https://github.com/SeldomQA/seldom-web-testing
基于seldom的接口自动化项目:
https://github.com/defnngj/seldom-api-testing
## 微信(WeChat)
> 相关书籍推荐, 基于 SeldomQA 相关开源项目,虫师 编著。
> 欢迎添加微信,交流和反馈问题。
### Star History

### 感谢
感谢从以下项目中得到思路和帮助。
* [parameterized](https://github.com/wolever/parameterized)
* [utx](https://github.com/jianbing/utx)
### 贡献者
### 交流
QQ群:948994709
================================================
FILE: api_case/confrun.py
================================================
"""
seldom confrun.py hooks function
"""
def base_url():
"""
http test
api base url
"""
return "http://www.httpbin.org"
def debug():
"""
debug mod
"""
return False
def rerun():
"""
error/failure rerun times
"""
return 0
def report():
"""
setting report path
Used:
return "d://mypro/result.html" or "d://mypro/result.xml"
"""
return None
def timeout():
"""
setting timeout
"""
return 10
def title():
"""
setting report title
"""
return "seldom 执行 excel 接口用例"
def tester():
"""
setting report tester
"""
return "bugmaster"
def description():
"""
setting report description
"""
return ["windows", "api"]
def language():
"""
setting report language
return "en" or "zh-CN"
"""
return "en"
def failfast():
"""
fail fast
:return:
"""
return False
================================================
FILE: demo/README.md
================================================
## seldom demo
通过 demo 帮助你快速了解seldom的使用。
### 准备工作
* 目录树:
```shell
./demo
├── README.md
├── __init__.py
├── confrun.py # 运行配置钩子函数
├── reports # 测试报告
├── test_data # 测试数据
└── test_dir # 测试用例
├── __init__.py
├── api_case # http接口用例
├── app_case # app UI 自动化用例
└── web_case # web UI 自动化用例
```
* 请安装 seldom 最新版本。
```shell
> pip install -U seldom
```
* 确保 seldom 命令可用。
```shell
> seldom --version
seldom, version 3.1.1
```
### 使用方法
seldom 从 3.1 开始支持 `confrun.py` 配置运行钩子函数,并推荐你使用这种方式。
| 函数 | 类型 | 说明 |
|-------------|---------|-----------------------------------------------------------|
| start_run() | fixture | 运行测试之前执行 |
| end_run() | fixture | 运行测试之后执行 |
| browser() | web | 设置浏览器类型:gc(google chrome)/ff(firefox)/edge/ie/safari |
| base_url() | api | 设置http接口基本url: 例如 http://httpbin.org |
| app_info() | app | 基于appium,启动app信息 |
| app_server() | app | 基于appium,设置appium 服务地址+端口 |
| debug() | general | 是否开启debug模式:True/False |
| rerun() | general | 用例失败/错误重跑,默认:0 |
| report() | general | 指定报告生成地址,例如: `/User/tech/xxx.html`、 `/User/tech/xxx.xml` |
| timeout() | general | 全局运行超时时间,默认:10 |
| title() | general | 测试报告标题:html报告 |
| tester() | general | 测试人员名字:html报告 |
| description() | general | 测试报告描述:html报告 |
| language() | general | 测试报告语言:html报告,类型: `en`、`zh-CN` |
| whitelist() | general | 运行用例白名单 |
| blacklist() | general | 运行用例黑名单 |
__特殊配置__
特殊配置是针对不同的测试类型设置的配置。
* web UI 自动化配置
```python
# confrun.py
def browser():
"""
Web UI test:
browser: gc(google chrome)/ff(firefox)/edge/ie/safari
"""
return "gc"
```
运行测试:
```shell
> seldom --path test_dir/web_case/
> seldom --path test_dir/web_case/test_playwright_demo.py
```
* http 接口测试
```python
# confrun.py
def base_url():
"""
http test
api base url
"""
return "http://httpbin.org"
```
运行测试:
```shell
> seldom --path test_dir/web_case/
```
* app UI 自动化测试
```python
# confrun.py
def app_info():
"""
app UI test
appium app config
"""
desired_caps = {
'deviceName': 'JEF_AN20',
'automationName': 'UiAutomator2',
'platformName': 'Android',
'platformVersion': '10.0',
'appPackage': 'com.meizu.flyme.flymebbs',
'appActivity': '.ui.LoadingActivity',
'noReset': True,
}
return desired_caps
def app_server():
"""
app UI test
appium server/desktop address
"""
return "http://127.0.0.1:4723"
```
运行测试:
```shell
> seldom --path test_dir/app_case/
```
__通用配置__
通用配置是不管运行什么类型的测试都适用的配置。
```python
# confrun.py
def debug():
"""
debug mod
"""
return False
def rerun():
"""
error/failure rerun times
"""
return 0
def report():
"""
setting report path
Used:
return "d://mypro/result.html"
return "d://mypro/result.xml"
"""
return None
def timeout():
"""
setting timeout
"""
return 10
def title():
"""
setting report title
"""
return "seldom test report"
def tester():
"""
setting report tester
"""
return "bugmaster"
def description():
"""
setting report description
"""
return ["windows", "jenkins"]
def language():
"""
setting report language
return "en"
return "zh-CN"
"""
return "en"
def whitelist():
"""test label white list"""
return []
def blacklist():
"""test label black list"""
return []
```
================================================
FILE: demo/__init__.py
================================================
================================================
FILE: demo/confrun.py
================================================
"""
seldom confrun.py hooks function
"""
def start_run():
"""
Test the hook function before running
"""
...
def end_run():
"""
Test the hook function after running
"""
...
def debug():
"""
debug mod
"""
return False
def rerun():
"""
error/failure rerun times
"""
return 0
def report():
"""
setting report path
Used:
return "d://mypro/result.html"
return "d://mypro/result.xml"
"""
return None
def timeout():
"""
setting timeout
"""
return 10
def title():
"""
setting report title
"""
return "seldom test report"
def tester():
"""
setting report tester
"""
return "bugmaster"
def description():
"""
setting report description
"""
return ["windows", "jenkins"]
def language():
"""
setting report language
return "en"
return "zh-CN"
"""
return "en"
def whitelist():
"""test label white list"""
return []
def blacklist():
"""test label black list"""
return []
================================================
FILE: demo/reports/.keep
================================================
================================================
FILE: demo/run.py
================================================
import seldom
"""
说明:
path: 指定测试目录。
browser: Web测试,指定浏览器,默认chrome - web专用
base_url: Http测试,指定接口地址。 - api专用
app_info: 启动app配置。 - app专用
app_server: appium server 地址。 - app专用
title: 指定测试项目标题。
tester: 指定测试人员。
description: 指定测试环境描述。
debug: debug模式,设置为True不生成测试用例。
rerun: 测试失败重跑
"""
if __name__ == '__main__':
# web case 配置
seldom.main(path="./test_dir/web_case",
browser="chrome",
title="seldom Web demo",
tester="虫师",
description=["Browser: Chrome"],
rerun=2)
# api case 配置
# seldom.main(path="./test_dir/api_case",
# base_url="http://httpbin.org",
# title="seldom API demo",
# tester="虫师",
# rerun=2)
# app case 配置
# from seldom.appium_lab.android import UiAutomator2Options
# capabilities = {
# 'deviceName': 'ELS-AN00',
# 'automationName': 'UiAutomator2',
# 'platformName': 'Android',
# 'appPackage': 'com.microsoft.bing',
# 'appActivity': 'com.microsoft.sapphire.app.main.MainSapphireActivity',
# 'noReset': True,
# }
# options = UiAutomator2Options().load_capabilities(capabilities)
# seldom.main(path="./test_dir/app_case",
# app_server="http://127.0.0.1:4723",
# app_info=options,
# title="seldom App demo",
# tester="虫师",
# rerun=2)
================================================
FILE: demo/test_data/csv_data.csv
================================================
firstname,lastname
Forest,Hobbs
Ferdinand,Lozano
================================================
FILE: demo/test_data/json_data.json
================================================
{
"name": [
["Wayne", "Burch"],
["Jamie-louise", "Wong"]
],
"login":[
{
"scene": "tom用户登录",
"username": "Tom",
"password": "tom123"
},
{
"scene": "jerry用户登录",
"username": "Jerry",
"password": "jerry123"
}
]
}
================================================
FILE: demo/test_data/yaml_data.yaml
================================================
---
name:
- - Elnora
- West
- - Leon
- Richard
login:
- username: Tom
password: tom123
- username: Jerry
password: jerry123
================================================
FILE: demo/test_dir/__init__.py
================================================
================================================
FILE: demo/test_dir/api_case/__init__.py
================================================
================================================
FILE: demo/test_dir/api_case/test_http_demo.py
================================================
import seldom
from seldom import data
class TestRequest(seldom.TestCase):
"""
http api test demo
doc: https://requests.readthedocs.io/en/master/
"""
def test_put_method(self):
"""
test put request
"""
self.put('/put', data={'key': 'value'})
self.assertStatusCode(200)
def test_post_method(self):
"""
test post request
"""
self.post('/post', data={'key':'value'})
self.assertStatusCode(200)
def test_get_method(self):
"""
test get request
"""
payload = {'key1': 'value1', 'key2': 'value2'}
self.get("/get", params=payload)
self.assertStatusCode(200)
def test_delete_method(self):
"""
test delete request
"""
self.delete('/delete')
self.assertStatusCode(200)
class TestAssert(seldom.TestCase):
"""
Test Assert
"""
def test_data_assert(self):
"""
The JSON data returned by the assertion
:return:
"""
self.get("/get")
self.assertStatusCode(200)
assert_data = {"headers": {"Host": "httpbin.org", "User-Agent": "python-requests/2.26.0"}}
self.assertJSON(assert_data, exclude=["headers", "user-agent"]) # exclude 过滤掉 json中的部分字段。
def test_format_assert(self):
"""
Assert json-schema
help doc: https://json-schema.org/
"""
self.get("/get")
self.assertStatusCode(200)
# 数据校验
schema = {
"type": "object",
"properties": {
"headers": {
"Host": "httpbin.org",
"User-Agent": "python-requests/2.22.0"
},
"origin": {"type": "string"},
"url": {
"type": "string",
"minLength": 20
}
},
}
self.assertSchema(schema)
def test_path_assert(self):
"""
assert jmesPath
help doc: https://jmespath.org/
"""
payload = {"foot": "bread"}
self.get('/get', params=payload)
self.assertPath("args.foot", "bread")
class TestRespData(seldom.TestCase):
"""
Test response data
"""
def test_resp_data(self):
"""
Get the returned data
"""
payload = {'key1': 'value1', 'key2': 'value2'}
self.post("/post", data=payload)
self.assertStatusCode(200)
self.assertEqual(self.response["form"]["key1"], "value1")
self.assertEqual(self.response["form"]["key2"], "value2")
def test_data_dependency(self):
"""
Test for interface data dependencies
"""
headers = {"X-Account-Fullname": "bugmaster"}
self.get("/get", headers=headers)
self.assertStatusCode(200)
username = self.response["headers"]["X-Account-Fullname"]
self.post("/post", data={'username': username})
self.assertStatusCode(200)
class TestDDT(seldom.TestCase):
"""
Test Data Driver
"""
@data([
("key1", 'value1'),
("key2", 'value2'),
("key3", 'value3')
])
def test_data(self, key, value):
"""
Data-Driver Tests
"""
payload = {key: value}
self.post("/post", data=payload)
self.assertStatusCode(200)
self.assertEqual(self.response["form"][key], value)
if __name__ == '__main__':
seldom.main(base_url="http://httpbin.org", debug=True)
================================================
FILE: demo/test_dir/app_case/__init__.py
================================================
================================================
FILE: demo/test_dir/app_case/test_first_demo.py
================================================
from appium.options.android import UiAutomator2Options
import seldom
from seldom.appium_lab.keyboard import KeyEvent
class TestBingApp(seldom.TestCase):
"""
Test Bing APP
"""
def start(self):
self.ke = KeyEvent(self.driver)
def test_bing_search(self):
"""
test bing App search
"""
self.sleep(2)
self.click(id_="com.microsoft.bing:id/sa_hp_header_search_box")
self.type(id_="com.microsoft.bing:id/sapphire_search_header_input", text="seldomQA")
self.ke.press_key("ENTER")
self.sleep(1)
elem = self.get_elements(xpath='//android.widget.TextView')
self.assertIn("seldom", elem[0].text.lower())
if __name__ == '__main__':
capabilities = {
'deviceName': 'ELS-AN00',
'automationName': 'UiAutomator2',
'platformName': 'Android',
'appPackage': 'com.microsoft.bing',
'appActivity': 'com.microsoft.sapphire.app.main.MainSapphireActivity',
'noReset': True,
}
options = UiAutomator2Options().load_capabilities(capabilities)
seldom.main(app_server="http://127.0.0.1:4723", app_info=options, debug=True)
================================================
FILE: demo/test_dir/app_case/test_po_demo.py
================================================
from poium import Page, Element
import seldom
from seldom.appium_lab.keyboard import KeyEvent
from seldom.appium_lab.android import UiAutomator2Options
class BingPage(Page):
"""BBS Page"""
search_button = Element("id=com.microsoft.bing:id/sa_hp_header_search_box")
search_input = Element("id=com.microsoft.bing:id/sapphire_search_header_input")
search_count = Element('//android.widget.TextView[@resource-id="count"]')
class TestBingApp(seldom.TestCase):
"""
Test Bing App
"""
def start(self):
self.bing_page = BingPage(self.driver)
self.ke = KeyEvent(self.driver)
def test_bbs(self):
"""
test bbs search
"""
self.sleep(2)
self.bing_page.search_button.click()
self.sleep(1)
self.bing_page.search_input.send_keys("seldom")
self.ke.press_key("ENTER")
self.sleep(1)
counts = self.bing_page.search_count
self.assertIn("个结果", counts.text.lower())
if __name__ == '__main__':
capabilities = {
'deviceName': 'ELS-AN00',
'automationName': 'UiAutomator2',
'platformName': 'Android',
'appPackage': 'com.microsoft.bing',
'appActivity': 'com.microsoft.sapphire.app.main.MainSapphireActivity',
'noReset': True,
}
options = UiAutomator2Options().load_capabilities(capabilities)
seldom.main(app_server="http://127.0.0.1:4723", app_info=options, debug=True)
================================================
FILE: demo/test_dir/app_case/test_u2_demo.py
================================================
"""
需要安装 uiautomator2: https://github.com/openatx/uiautomator2
> pip install uiautomator2
"""
import seldom
import uiautomator2 as u2
class MyAppTest(seldom.TestCase):
def start(self):
# 链接设备
self.d = u2.connect('192.168.31.234')
# 启动App
self.d.app_start("com.meizu.mzbbs")
def end(self):
# 停止app
self.d.app_stop("com.meizu.mzbbs")
def test_app(self, user):
""" 使用 uiautomator2 """
# 搜索
self.d(resourceId="com.meizu.flyme.flymebbs:id/nw").click()
# 输入关键字
self.d(resourceId="com.meizu.flyme.flymebbs:id/nw").set_text("flyme")
# 搜索按钮
self.d(resourceId="com.meizu.flyme.flymebbs:id/o1").click()
self.sleep(2)
if __name__ == '__main__':
seldom.main(debug=True)
================================================
FILE: demo/test_dir/web_case/__init__.py
================================================
================================================
FILE: demo/test_dir/web_case/test_data_demo.py
================================================
import seldom
from seldom import testdata
class RandomDataTest(seldom.TestCase):
"""
Randomly generate test data
"""
def test_case(self):
"""
used testdata test
"""
self.open("https://www.runoob.com/try/try.php?filename=tryhtml_input")
self.switch_to_frame(id_="iframeResult")
self.type(name="firstname", text=testdata.first_name())
self.type(name="lastname", text=testdata.last_name())
self.sleep(1)
if __name__ == '__main__':
seldom.main(browser="gc", debug=True)
================================================
FILE: demo/test_dir/web_case/test_ddt_demo.py
================================================
import seldom
from seldom import data, file_data
class BingTest(seldom.TestCase):
"""Bing search test case"""
@data([
("First case description", "seldom"),
("Second case description", "selenium"),
("Third case description", "unittest"),
])
def test_bing_tuple(self, desc, search_key):
"""
used tuple test data
:param desc: case desc
:param search_key: search keyword
"""
self.open("https://cn.bing.com")
self.type(id_="sb_form_q", text=search_key, enter=True)
self.assertInTitle(search_key)
@data([
["First case description", "seldom"],
["Second case description", "selenium"],
["Third case description", "unittest"],
])
def test_bing_list(self, desc, search_key):
"""
used list test data
:param desc: case desc
:param search_key: search keyword
"""
self.open("https://cn.bing.com")
self.type(id_="sb_form_q", text=search_key, enter=True)
self.assertInTitle(search_key)
@data([
{"scene": "First case description", "search_key": "seldom"},
{"scene": "Second case description", "search_key": "selenium"},
{"scene": "Third case description", "search_key": "unittest"},
])
def test_bing_dict(self, scene, search_key):
"""
used dict test data
:param scene: case desc
:param search_key: search keyword
"""
self.open("https://cn.bing.com")
self.type(id_="sb_form_q", text=search_key, enter=True)
self.assertInTitle(search_key)
class FileDataTest(seldom.TestCase):
"""form input test case"""
def start(self):
self.test_url = "https://www.w3school.com.cn/tiy/t.asp?f=eg_html_form_submit"
@file_data("json_data.json", key="name")
def test_json_list(self, firstname, lastname):
"""
used file_data test
"""
self.open(self.test_url)
self.switch_to_frame(id_="iframeResult")
self.type(name="firstname", text=firstname, clear=True)
self.type(name="lastname", text=lastname, clear=True)
self.sleep(1)
@file_data("json_data.json", key="login")
def test_json_dict(self, _, username, password):
"""
used file_data test
"""
self.open(self.test_url)
self.switch_to_frame(id_="iframeResult")
self.type(name="firstname", text=username, clear=True)
self.type(name="lastname", text=password, clear=True)
self.sleep(1)
@file_data("yaml_data.yaml", key="name")
def test_yaml_list(self, firstname, lastname):
"""
used file_data test
"""
self.open(self.test_url)
self.switch_to_frame(id_="iframeResult")
self.type(name="firstname", text=firstname, clear=True)
self.type(name="lastname", text=lastname, clear=True)
self.sleep(1)
@file_data("yaml_data.yaml", key="login")
def test_yaml_dict(self, username, password):
"""
used file_data test
"""
self.open(self.test_url)
self.switch_to_frame(id_="iframeResult")
self.type(name="firstname", text=username, clear=True)
self.type(name="lastname", text=password, clear=True)
self.sleep(1)
@file_data("csv_data.csv", line=2)
def test_csv(self, firstname, lastname):
"""
used file_data test
"""
self.open(self.test_url)
self.switch_to_frame(id_="iframeResult")
self.type(name="firstname", text=firstname, clear=True)
self.type(name="lastname", text=lastname, clear=True)
self.sleep(1)
@file_data(file="excel_data.xlsx", sheet="Sheet1", line=2)
def test_excel(self, firstname, lastname):
"""
used file_data test
"""
self.open(self.test_url)
self.switch_to_frame(id_="iframeResult")
self.type(name="firstname", text=firstname, clear=True)
self.type(name="lastname", text=lastname, clear=True)
self.sleep(1)
if __name__ == '__main__':
seldom.main(browser="gc", debug=False)
================================================
FILE: demo/test_dir/web_case/test_first_demo.py
================================================
import seldom
from seldom import Steps
class BingTest(seldom.TestCase):
"""Bing search test case"""
def test_case(self):
"""a simple test case """
self.open("https://cn.bing.com")
self.type(id_="sb_form_q", text="seldom", enter=True)
self.sleep(2)
self.assertInTitle("seldom")
def test_case_two(self):
"""method chaining """
Steps(url="https://cn.bing.com").open().find("#sb_form_q").type("seldom").submit().sleep(2)
self.assertInTitle("seldom")
if __name__ == '__main__':
seldom.main(browser="gc")
================================================
FILE: demo/test_dir/web_case/test_fixture_demo.py
================================================
import seldom
class BaiduTest(seldom.TestCase):
"""
* start_class/end_class
* start/end
"""
@classmethod
def start_class(cls):
"""start class"""
print("test class start")
cls.max_window(cls)
@classmethod
def end_class(cls):
"""end class"""
...
def start(self):
"""start"""
print("test case start")
self.index_page = "https://www.baidu.com/"
self.news_page = "https://news.baidu.com/"
def end(self):
"""end"""
...
def test_open_index(self):
"""open baidu index page"""
self.open(self.index_page)
def test_open_news(self):
"""open baidu news page"""
self.open(self.news_page)
if __name__ == '__main__':
seldom.main(debug=True)
================================================
FILE: demo/test_dir/web_case/test_playwright_demo.py
================================================
"""
需要安装 playwright: https://playwright.dev/
> pip install playwright
"""
import seldom
from playwright.sync_api import sync_playwright
from playwright.sync_api import expect
class Playwright(seldom.TestCase):
def start(self):
p = sync_playwright().start()
self.browser = p.chromium.launch()
self.page = self.browser.new_page()
def end(self):
self.browser.close()
def test_start(self):
page = self.page
page.goto("https://playwright.dev")
print(page.title())
expect(page).to_have_title("Fast and reliable end-to-end testing for modern web apps | Playwright")
get_started = page.locator('text=Get Started')
expect(get_started).to_have_attribute('href', '/docs/intro')
get_started.click()
expect(page).to_have_url('https://playwright.dev/docs/intro')
if __name__ == '__main__':
seldom.main()
================================================
FILE: demo/test_dir/web_case/test_po_demo.py
================================================
"""
page object model
Using the poium Library
https://github.com/SeldomQA/poium
```
> pip install poium>=1.6.1
```
"""
import seldom
from poium import Page, Element, Elements
class BaiduPage(Page):
"""baidu page"""
input = Element("#kw", describe="搜索输入框")
button = Element("id=su", describe="搜索按钮")
result = Elements("//div/h3/a", describe="搜索结果")
class BaiduTest(seldom.TestCase):
"""Baidu search test case"""
def test_baidu_page(self):
"""
A simple test
"""
page = BaiduPage()
page.open("https://www.baidu.com")
self.sleep(3)
# assert element is exist
self.assertTrue(page.input.is_exist())
self.assertTrue(page.button.is_exist())
# operation element
page.input.send_keys("(title:seldom)")
page.button.click()
self.sleep(3)
# assert title
self.assertTitle("(title:seldom)_百度搜索")
# Loop assertion result
for r in page.result:
# assert text in
self.assertIn("seldom", r.text.lower())
if __name__ == '__main__':
seldom.main(browser='chrome')
================================================
FILE: description.rst
================================================
seldom
---------------
Seldom is an automation testing framework based on unittest.
Installation
------------
$ pip install seldom
Documentation
++++++++++++++++++
https://seldomqa.github.io
================================================
FILE: docs/.gitignore
================================================
node_modules
.temp
.cache
.DS_Store
vpdocs/.vuepress/dist
================================================
FILE: docs/README.md
================================================
## ☘️Introduction
此目录用于**存放 & 编辑** seldom 相关文档
## 📖 Document
[中文文档](https://seldomqa.github.io/)
[English document(readthedocs)](https://seldomqa.readthedocs.io/en/latest/index.html)
## 结构
```shell
docs/
├── README.md
├── conf.py # rst文档配置文件
├── deploy.sh # vuepress文档部署脚本
├── index.rst
├── markdown2rst.py # md转rst脚本
├── package.json
├── requirements.txt # python模块依赖
├── rst_docs # 用于存放rst文档
├── vpdocs # 用于vuepress文档
│ ├── README.md
│ ├── advanced
│ ├── db
│ ├── getting-started
│ ├── http
│ ├── introduce.md
│ ├── other
│ └── platform
└── yarn.lock
```
## 如何贡献文档
1. clone 本项目
```bash
git clone https://github.com/SeldomQA/seldom.git
```
2. 进入到文档目录&启动项目
```bash
cd docs
npm install
npm run dev
npm run build
```
3. 编辑相关文档(编辑 vpdocs 目录下的文档)
================================================
FILE: docs/compare.md
================================================
## seldom vs pytest
| 功能 | seldom | pytest |
|---------------|--------------------------------------------|---------------------------------------|
| web UI测试 | 支持 ✅ | 支持(需安装 selenium) ⚠️ |
| web UI断言 | 支持(assertText、assertTitle、assertElement) ✅ | 不支持 ❌ |
| playwright | 支持(需安装playwright) ⚠️ | 支持(playwright提供playwright-pytest插件) ✅ |
| 失败截图 | 支持(自动实现) ✅ | 支持(需要设置) ✅ |
| http接口测试 | 支持 ✅ | 支持(需安装 requests) ⚠️ |
| http接口断言 | 支持(assertJSON、assertPath、assertSchema) ✅ | 不支持 ❌ |
| app UI测试 | 支持 ✅ | 支持(需安装 appium) ⚠️ |
| Page Object模式 | 支持(推荐poium) ✅ | 支持(推荐poium) ✅ |
| 脚手架 | 支持(快速创建项目) ✅ | 不支持 ❌ |
| 生成随机测试数据 | 支持`testdata` ✅ | 不支持 ❌ |
| 发送消息 | 支持(email、钉钉、飞书、微信)✅ | 不支持 ❌ |
| log日志 | 支持 ✅ | 不支持 ❌ |
| 数据库操作 | 支持(sqlite3、MySQL、SQL Server) ✅ | 不支持 ❌ |
| 用例依赖 | 支持`@depend()` ✅ | `@pytest.mark.dependency()`支持 ✅ |
| 失败重跑 | 支持`rerun` ✅ | pytest-rerunfailures 支持 ✅ |
| 用例分类标签 | 支持`@label()` ✅ | `@pytest.mark.xxx`支持 ✅ |
| HTML测试报告 | 支持 ✅ | pytest-html、allure ✅ |
| XML测试报告 | 支持 ✅ | 自带 `--junit-xml` ✅ |
| 数据驱动方法 | `@data()` ✅ | `@pytest.mark.parametrize()` ✅ |
| 数据驱动文件 | `@file_data()`(JSON\YAML\CSV\Excel) ✅ | 不支持 ❌ |
| 钩子函数 | `confrun.py`用例运行钩子 ⚠️ | `conftest.py` 功能更强大 ✅ |
| 命令行工具CLI | 支持`seldom` ✅ | 支持`pytest` ✅ |
| 并发执行 | 不支持 ❌ | pytest-xdist、pytest-parallel ✅ |
| 平台化 | 支持(seldom-platform)✅ | 不支持 ❌ |
| 第三方插件 | seldom(unittest)的生态比较糟糕 ⚠️ | pytest有丰富插件生态 ✅ |
__说明__
* ✅ : 表示支持。
* ⚠️: 支持,但支持的不好,或没有对方好。
* ❌ : 不支持,表示框架没有该功能,第三方插件也没有。
================================================
FILE: docs/deploy.sh
================================================
# 确保脚本抛出遇到的错误
set -e
# 生成静态文件
npm run build
# 进入生成的文件夹
cd vpdocs/.vuepress/dist
git init
git add -A
git commit -m 'deploy'
# 如果发布到 https://SeldomQA.github.io
git push -f https://github.com/SeldomQA/SeldomQA.github.io.git master
cd -
================================================
FILE: docs/package.json
================================================
{
"name": "vuepress-docs",
"version": "1.0.0",
"description": "docs by vuepress",
"main": "index.js",
"author": "Yongchin",
"license": "MIT",
"scripts": {
"dev": "vuepress dev vpdocs",
"build": "vuepress build vpdocs"
},
"type": "module",
"devDependencies": {
"@vuepress/bundler-vite": "^2.0.0-rc.20",
"@vuepress/plugin-search": "^2.0.0-rc.83",
"@vuepress/theme-default": "^2.0.0-rc.84",
"sass-embedded": "^1.85.1",
"vuepress": "^2.0.0-rc.20"
}
}
================================================
FILE: docs/vpdocs/.vuepress/config.js
================================================
import { defineUserConfig } from 'vuepress'
import { viteBundler } from '@vuepress/bundler-vite'
import { defaultTheme } from '@vuepress/theme-default'
import { searchPlugin } from '@vuepress/plugin-search'
export default defineUserConfig({
title: "seldom文档",
description: "seldom 是基于unittest 的自动化测试框架。",
base: "/",
head: [
['link', { rel: 'icon', href: '/logo.jpeg' }]
],
bundler: viteBundler({
viteOptions: {},
}),
plugins: [
searchPlugin({
// 配置项
}),
],
theme: defaultTheme({
repo: "SeldomQA/seldom",
docsBranch: "vuepress-docs/docs/vpdocs",
logo: "/logo.jpeg",
navbar: [
{ text: "介绍", link: "/introduce" },
{ text: "安装", link: "/getting-started/installation" },
],
sidebar: [
"/introduce",
{
text: "开始",
children: [
"/getting-started/installation",
"/getting-started/create_project",
"/getting-started/quick_start",
"/getting-started/advanced",
"/getting-started/data_driver",
"/getting-started/dependent_func",
"/getting-started/seldom_cli",
],
},
{
text: "web UI 测试",
children: [
"/web-testing/browser_driver",
"/web-testing/seldom_api",
"/web-testing/chaining",
"/web-testing/page_object",
"/web-testing/other",
],
},
{
text: "App UI 测试",
children: [
"/app-testing/start",
"/app-testing/appium_lab",
"/app-testing/page_object",
"/app-testing/extensions",
"/app-testing/adb_lib",
],
},
{
text: "HTTP接口测试",
children: [
"/api-testing/start",
"/api-testing/assert",
"/api-testing/api_object",
"/api-testing/more",
"/api-testing/api_case",
"/api-testing/webscocket",
],
},
{
text: "更多能力",
children: [
"/more-ability/db_operation",
"/more-ability/test_library",
"/more-ability/benchmark",
],
},
"/platform/platform",
"/version/CHANGES",
],
editLinks: true,
editLinkText: "在 GitHub 上编辑此页",
lastUpdated: "上次更新",
}),
})
================================================
FILE: docs/vpdocs/README.md
================================================
---
home: true
heroText: Seldom
heroImage: /image/book.jpg
actions:
- text: 快速上手→
link: /getting-started/quick_start.html
type: primary
- text: 项目简介
link: /introduce.html
type: secondary
features:
- title: 全能
details: seldom支持web/app/api等类型的自动化测试。
- title: 快速
details: 提供脚手架快速创建自动化项目。
- title: 报告
details: 集成XTestRunner测试报告,现代美观。
- title: 断言
details: 提供丰富的断言,方便验证测试结果。
- title: 数据驱动
details: 支持Excel/CSV/JSON/YAML数据文件。
- title: 平台化
details: seldom提供了平台化支持。
footer: MIT Licensed | Copyright © 2025-重定向科技
---
================================================
FILE: docs/vpdocs/api-testing/api_case.md
================================================
# 支持Excel测试用例
> seldom > 3.8.0
在编写接口测试用例的时候,有时候测试用例非常简单,比如单接口的测试,不需要登录token,不存在用例数据依赖,也不需要参数加密,此时,使用`Excel`
文件编写用例更为高效。
seldom支持了这种用例的编写。
### 编写Excel用例
[查看例子](https://github.com/SeldomQA/seldom/tree/master/api_case)
首先,创建一个Excel文件,格式如下。
| name | api | method | headers | param_type | params | assert | exclude |
|-----------------|-------|--------|---------|------------|--------|--------|---------|
| 简单GET接口 | /get | GET | {} | data | {} | {} | [] |
| 简单POST接口-json参数 | /post | POST | {} | json | {} | {} | [] |
| ... | | | | | | | |
__参数说明__
| 字段 | 说明 | 列子 |
|--------------|-------------------------------------------------------|------------------------------------------------------|
| `name` | 用例的名称,会在测试报告中展示。 | |
| `api` | 接口的地址,可以写完整的URL地址, 也可以只定义路径,`base_url` 在 `confrun.py` | 例如:`http://www.httpbin.org/get` or `/get` |
| `method` | 接口的请求方法,必须大写,不允许为空 | 支持:`GET`、`POST`、`PUT`、`DELETE` |
| `headers` | 请求头,不允许为空,默认为 `{}`,字段必须双引号`"`。 | 例如:`{"user-agent": "my-app/0.0.1"}` |
| `param_type` | 接口参数类型,必须小写,不允许为空。 | 例如:`data`、 `json` |
| `params` | 接口参数,不允许为空,默认为 `{}`,字段必须双引号`"`。 | 例如:`{"id": 1, "name": "jack"}` |
| `assert` | 断言接口返回,允许为空 或 `{}`, | 例如:`{"status": 200, "success": True, "data": [...]}` |
| `exclude` | 断言过滤字段,一些特殊的字段会导致断言失败,需要过滤掉。 | 例如:`["X-Amzn-Trace-Id", "timestamp"]` |
__confrun.py配置__
```python
def base_url():
"""
http test
api base url
"""
return "http://www.httpbin.org"
def debug():
"""
debug mod
"""
return False
def rerun():
"""
error/failure rerun times
"""
return 0
def report():
"""
setting report path
Used:
return "d://mypro/result.html" or "d://mypro/result.xml"
"""
return None
def timeout():
"""
setting timeout
"""
return 10
def title():
"""
setting report title
"""
return "seldom 执行 excel 接口用例"
def tester():
"""
setting report tester
"""
return "bugmaster"
def description():
"""
setting report description
"""
return ["windows", "api"]
def language():
"""
setting report language
return "en" or "zh-CN"
"""
return "zh-CN"
def failfast():
"""
fail fast
:return:
"""
return False
```
### 运行测试用例
* 目录结构
```shell
mypro/
├── api_case.xlsx
└── confrun.py
```
* 运行测试
```shell
> cd mypro
> seldom --api-excel api_case.xlsx
```
* 运行日志
```shell
seldom --api-excel .\api_case.xlsx
run .\api_case.xlsx file.
__ __
________ / /___/ /___ ____ ____
/ ___/ _ \/ / __ / __ \/ __ ` ___/
(__ ) __/ / /_/ / /_/ / / / / / /
/____/\___/_/\__,_/\____/_/ /_/ /_/ v3.x.x
-----------------------------------------
@itest.info
2024-07-06 21:00:35 | INFO | runner.py | TestLoader: ...\Lib\site-packages\seldom\file_runner\api_excel.py
2024-07-06 21:00:35 | INFO | parameterization.py | find data file: .\api_case.xlsx
XTestRunner Running tests...
----------------------------------------------------------------------
2024-07-06 21:00:35 | INFO | api_excel.py | execute api case: [简单GET接口]
2024-07-06 21:00:35 | INFO | request.py | -------------- Request -----------------[🚀]
2024-07-06 21:00:35 | INFO | request.py | [method]: GET [url]: http://www.httpbin.org/get
2024-07-06 21:00:35 | DEBUG | request.py | [headers]:
{
"user-agent": "my-app/0.0.1"
}
2024-07-06 21:00:35 | DEBUG | request.py | [params]:
{
"key": "value"
}
2024-07-06 21:00:35 | INFO | request.py | -------------- Response ----------------[🛬️]
2024-07-06 21:00:35 | INFO | request.py | successful with status 200
2024-07-06 21:00:35 | DEBUG | request.py | [type]: json [time]: 0.481752
2024-07-06 21:00:35 | DEBUG | request.py | [response]:
{
"args": {
"key": "value"
},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate",
"Host": "www.httpbin.org",
"User-Agent": "my-app/0.0.1",
"X-Amzn-Trace-Id": "Root=1-66893ff2-60ed7c5378ca01452917ea0c"
},
"origin": "14.155.89.115",
"url": "http://www.httpbin.org/get?key=value"
}
2024-07-06 21:00:35 | INFO | case.py | 👀 assertJSON -> {'args': {'key': 'value'}, 'headers': {'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate', 'Host': 'www.httpbin.org', 'User-Agent': 'my-app/0.0.1', 'X-Amzn-Trace-Id': 'Root=1-668906ef-2e2d8c4c3f36a228264da1ab'}, 'origin': '14.155.89.115', 'url': 'http://www.httpbin.org/get?key=value'}.
...
```
* 生成测试报告

================================================
FILE: docs/vpdocs/api-testing/api_object.md
================================================
# API Object
API Object Models,简称AOM,AOM是一种设计模式,它围绕着将API、路由或功能交互及其相关行为封装在结构良好的对象中。AOM旨在增强API测试和集成的直观性和弹性。在实践中,AOM需要精心设计专门的API对象,以有效地保护用户免受与API 请求、响应、端点交互和身份验证过程相关的复杂性的影响。
seldom 支持AOM, 并且提供了一些好用的功能,辅助你使用AOM.
* 目录结构如下
```shell
mypro/
├── api/
│ ├── __init__.py
│ ├── auth_object.py
│ └── xxx_object.py
├── test_dir/
│ ├── test_auth.py
│ └── test_xxx.py
│ ...
```
* 创建 API Object
```python
# api/auth_object.py
from seldom.testdata import get_int
from seldom.request import HttpRequest
from seldom.request import check_response
class AuthAPIObject(HttpRequest):
def __init__(self, api_key):
self.api_key = api_key
@check_response(ret="form.token")
def get_token(self, user_id:str) -> str:
"""
模拟:根据用户ID生成登录token
:param user_id:
:return:
"""
data = {"user_id": user_id, "token": "t" + str(get_int(10000, 99999))}
r = self.post("/post?key=" + self.api_key, data=data)
return r
```
定义API接口,根据`get_token()`用于生成token,这里我们通过随机数模拟的生成的token。`check_response()`装饰器用于装饰接口,`form.token` 用于提取API的返回值。
* 创建测试用例
```python
# test_dir/test_auth.py
import seldom
from api.auth_object import AuthAPIObject
class TestAPI(seldom.TestCase):
def test_case(self):
auth_object = AuthAPIObject(api_key="abc123")
token = auth_object.get_token(user_id="123")
print("token", token)
if __name__ == '__main__':
seldom.main(debug=True, base_url="https://httpbin.org")
```
在用例层调用`AuthAPIObject`类下面的对象,测试API。
* AOM 原则
首先,API只允许通过的APIObject进行封装,那么在封装之前可以检索一下是否有封装了,如果有,进一步确认是否满足自己的调用需求,我们一般在测试API的时候一般各种参数验证,当API作为依赖接口调用的时候,一般参数比较少且固定,所以,API在封装的时候要兼顾到这两种情况。
其次,用例层只能通过APIObject的封装调用API,像登录token这种大部分API会用到的信息,可以通过类初始化时传入,后续调用类下面方法的时候就不需要关心的。如果是多个API组成一个场景,也可以再进行一层业务层的封装。
================================================
FILE: docs/vpdocs/api-testing/assert.md
================================================
# 接口断言
断言接口返回的数据是HTTP接口自动化测试非常重要的工作,提供强大的断言方法可以提高用例的编写效率。
## assertJSON
`assertJSON()` 断言接口返回的某部分数据。
* 请求参数
```json
{
"name": "tom",
"hobby": [
"basketball",
"swim"
]
}
```
* 返回结果
```json
{
"args": {
"hobby": [
"basketball",
"swim"
],
"name": "tom"
},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate",
"Host": "httpbin.org",
"User-Agent": "python-requests/2.25.0",
"X-Amzn-Trace-Id": "Root=1-62851614-1ca9fdb276238c60406c118f"
},
"origin": "113.87.15.99",
"url": "http://httpbin.org/get?name=tom&hobby=basketball&hobby=swim"
}
```
我的目标是断言`name` 和 `hobby` 部分的内容。
```python
import seldom
class TestAPI(seldom.TestCase):
def test_assert_json(self):
# 接口参数
payload = {"name": "tom", "hobby": ["basketball", "swim"]}
# 接口调用
self.get("http://httpbin.org/get", params=payload)
# 断言数据
assert_data = {
"hobby": ["swim", "basketball"],
"name": "tom"
}
self.assertJSON(assert_data, self.response["args"], exclude=["xxx"])
```
* exclude 用于设置跳过的检查字段,例如一些 时间、随机数 等,每次调用都不一样,但并不影响结果的正确性。通过 exclude
来设置屏蔽这些字段的检查。
## assertPath
`assertPath` 是基于 `jmespath` 实现的断言,功能非常强大。
* jmespath: https://jmespath.org/specification.html
接口返回数据如下:
```json
{
"args": {
"hobby": [
"basketball",
"swim"
],
"name": "tom"
}
}
```
seldom中可以通过path进行断言:
```python
import seldom
class TestAPI(seldom.TestCase):
def test_assert_path(self):
payload = {'name': 'tom', 'hobby': ['basketball', 'swim']}
self.get("http://httpbin.org/get", params=payload)
self.assertPath("args.name", "tom")
self.assertPath("args.hobby[0]", "basketball")
self.assertInPath("args.hobby[0]", "ball")
```
* `args.hobby[0]` 提取接口返回的数据。
* `assertPath()` 判断提取的数据是否等于`basketball`;
* `assertInPath()` 判断提取的数据是否包含`ball`。
## assertSchema
当你不关心数据本身是什么,而是关心数据的结构和类型时,可以使用 `assertSchema` 断言方法。 `assertSchema` 是基于 `jsonschema`
实现的断言方法。
* jsonschema: https://json-schema.org/learn/
```python
import seldom
from seldom.utils import genson
class TestAPI(seldom.TestCase):
def test_assert_schema(self):
# 接口参数
payload = {"hobby": ["basketball", "swim"], "name": "tom", "age": "18"}
# 调用接口
self.get("/get", params=payload)
# 生成数据结构和类型
schema = genson(self.response["args"])
print("json Schema: \n", schema)
# 断言数据结构和类型
assert_data = {
"$schema": "http://json-schema.org/schema#",
"type": "object",
"properties": {
"age": {
"type": "string"
},
"hobby": {
"type": "array", "items": {"type": "string"}
},
"name": {
"type": "string"
}
},
"required":
["age", "hobby", "name"]
}
self.assertSchema(assert_data, self.response["args"])
```
* `genson`: 可以生成`jsonschema`数据结构和类型(`seldom 2.9` 新增)。
================================================
FILE: docs/vpdocs/api-testing/more.md
================================================
# 更多功能
### har to case
对于不熟悉 Requests 库的人来说,通过Seldom来写接口测试用例还是会有一点难度。于是,seldom 提供了`har` 文件转 `case` 的命令。
首先,打开fiddler 工具进行抓包,选中某一个请求。
然后,选择菜单栏:`file` -> `Export Sessions` -> `Selected Sessions...`

选择导出的文件格式。

点击`next` 保存为`demo.har` 文件。
最后,通过`seldom -h2c` 转为`demo.py` 脚本文件。
```shell
> seldom -h2c demo.har
2021-06-14 18:05:50 [INFO] Start to generate testcase.
2021-06-14 18:05:50 [INFO] created file: ...\demo.py
```
`demo.py` 文件。
```python
import seldom
class TestRequest(seldom.TestCase):
def start(self):
self.url = "http://httpbin.org/post"
def test_case(self):
headers = {"User-Agent": "python-requests/2.25.0", "Accept-Encoding": "gzip, deflate",
"Accept": "application/json", "Connection": "keep-alive", "Host": "httpbin.org",
"Content-Length": "36", "Origin": "http://httpbin.org", "Content-Type": "application/json",
"Cookie": "lang=zh"}
cookies = {"lang": "zh"}
self.post(self.url, json={"key1": "value1", "key2": "value2"}, headers=headers, cookies=cookies)
self.assertStatusCode(200)
if __name__ == '__main__':
seldom.main()
```
### swagger to case
> seldom 3.6 版本支持。
seldom 提供了`swagger` 转 `case` 的命令。 使用 `seldom -s2c` 命令。
```shell
> seldom -s2c swagger.json
2024-03-04 00:02:22 | INFO | core.py | Start to generate testcase.
2024-03-04 00:02:22 | INFO | core.py | created file: ...\swagger.py
```
将swagger文档转为 seldom 自动化测试用例。
```python
import seldom
class TestRequest(seldom.TestCase):
def test_pet_petId_uploadImage_api_post(self):
url = f"https://petstore.swagger.io/pet/{petId}/uploadImage"
params = {}
headers = {}
headers["Content-Type"] = "multipart/form-data"
data = {"additionalMetadata": additionalMetadata, "file": file}
r = self.post(url, headers=headers, params=params, data=data)
print(r.status_code)
def test_pet_api_post(self):
url = f"https://petstore.swagger.io/pet"
params = {}
headers = {}
headers["Content-Type"] = "application/json"
data = {}
r = self.post(url, headers=headers, params=params, data=data)
print(r.status_code)
```
需要注意的是,转换的seldom自动化测试用例有一些`变量`,需要用户根据实际情况进行定义。
### 请求转 cURL
seldom 支持将请求转成`cCURL`命令, 你可以方便的通过`cURL`命令执行,或者导入到其他接口工具,例如,postman 支持`cURL`命令导入。
```python
# test_http.py
import seldom
class TestRequest(seldom.TestCase):
"""
http api test demo
doc: https://requests.readthedocs.io/en/master/
"""
def test_get_curl(self):
"""
test get curl
"""
self.get('http://httpbin.org/get', params={'key': 'value'})
curl = self.curl()
print(curl)
self.post('http://httpbin.org/post', data={'key': 'value'})
curl = self.curl()
print(curl)
# or
r = self.delete('http://httpbin.org/delete', params={'key': 'value'})
curl = self.curl(r.request)
print(curl)
r = self.put('http://httpbin.org/put', json={'key': 'value'}, headers={"token": "123"})
curl = self.curl(r.request)
print(curl)
if __name__ == '__main__':
seldom.main(debug=True)
```
* 日志结果
```shell
> python test_http.py
...
curl -X GET 'Content-Type: application/json' -H 'token: 123' -d '{"key": "value"}' http://httpbin.org/get
curl -X POST 'Content-Type: application/x-www-form-urlencoded' -H -d key=value http://httpbin.org/post
curl -X DELETE 'http://httpbin.org/delete?key=value'
curl -X PUT -H 'Content-Type: application/json' -H 'token: 123' -d '{"key": "value"}' http://httpbin.org/put
```
### 接口数据依赖
在场景测试中,我们需要利用上一个接口的数据,调用下一个接口。
* 简单的接口依赖
```python
import seldom
class TestRespData(seldom.TestCase):
def test_data_dependency(self):
"""
Test for interface data dependencies
"""
headers = {"X-Account-Fullname": "bugmaster"}
self.get("/get", headers=headers)
self.assertStatusCode(200)
username = self.response["headers"]["X-Account-Fullname"]
self.post("/post", data={'username': username})
self.assertStatusCode(200)
```
seldom提供了`self.response`用于记录上个接口返回的结果,直接拿来用即可。
* 封装接口依赖
1. 创建公共模块
```python
# common.py
from seldom.request import check_response
from seldom.request import HttpRequest
class Common(HttpRequest):
@check_response(
describe="获取登录用户名",
status_code=200,
ret="headers.Account",
check={"headers.Host": "httpbin.org"},
debug=True
)
def get_login_user(self):
"""
调用接口获得用户名
"""
headers = {"Account": "bugmaster"}
r = self.get("http://httpbin.org/get", headers=headers)
return r
if __name__ == '__main__':
c = Common()
c.get_login_user()
```
* 运行日志
```shell
2023-02-14 23:51:48 request.py | DEBUG | Execute get_login_user - args: (<__main__.Common object at 0x0000023263075100>,)
2023-02-14 23:51:48 request.py | DEBUG | Execute get_login_user - kwargs: {}
2023-02-14 23:51:48 request.py | INFO | -------------- Request -----------------[🚀]
2023-02-14 23:51:48 request.py | INFO | [method]: GET [url]: http://httpbin.org/get
2023-02-14 23:51:48 request.py | DEBUG | [headers]:
{
"Account": "bugmaster"
}
2023-02-14 23:51:49 request.py | INFO | -------------- Response ----------------[🛬️]
2023-02-14 23:51:49 request.py | INFO | successful with status 200
2023-02-14 23:51:49 request.py | DEBUG | [type]: json [time]: 0.601097
2023-02-14 23:51:49 request.py | DEBUG | [response]:
{
"args": {},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate",
"Account": "bugmaster",
"Host": "httpbin.org",
"User-Agent": "python-requests/2.28.1",
"X-Amzn-Trace-Id": "Root=1-63ebae14-1e629b132c21f68e23ffeb33"
},
"origin": "173.248.248.88",
"url": "http://httpbin.org/get"
}
2023-02-14 23:51:49 request.py | DEBUG | Execute get_login_user - response:
{'args': {}, 'headers': {'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate', 'Account': 'bugmaster', 'Host': 'httpbin.org', 'User-Agent': 'python-requests/2.28.1', 'X-Amzn-Trace-Id': 'Root=1-63ebae14-1e629b132c21f68e23ffeb33'}, 'origin': '173.248.248.88', 'url': 'http://httpbin.org/get'}
2023-02-14 23:51:49 request.py | INFO | Execute get_login_user - 获取登录用户名 success!
```
`@check_response` 专门用于处理封装的方法。
__参数说明:__
* `describe`: 封装方法描述。
* `status_code`: 判断接口返回的 HTTP 状态码,默认`200`。
* `ret`: 提取接口返回的字段,参考`jmespath` 提取规则。
* `check`: 检查接口返回的字段。参考`jmespath` 提取规则。
* `debug`: 开启`debug`,打印更多信息。
2. 引用公共模块
```python
import seldom
from common import Common
class TestRequest(seldom.TestCase):
def start(self):
self.c = Common()
def test_case(self):
# 调用 get_login_user() 获取
user = self.c.get_login_user()
self.post("http://httpbin.org/post", data={'username': user})
self.assertStatusCode(200)
if __name__ == '__main__':
seldom.main(debug=True)
```
### Session使用
在实际测试过程中,大部分接口需要登录,`Session` 是一种非常简单记录登录状态的方式。
```python
import seldom
class TestCase(seldom.TestCase):
def start(self):
self.s = self.Session()
self.s.get('/cookies/set/sessioncookie/123456789')
def test_get_cookie1(self):
self.s.get('/cookies')
def test_get_cookie2(self):
self.s.get('/cookies')
if __name__ == '__main__':
seldom.main(debug=True, base_url="https://httpbin.org")
```
用法非常简单,你只需要在每个接口之前调用一次`登录`, `self.s`对象就记录下了登录状态,通过`self.s` 再去调用其他接口就不需要登录。
### 提取接口返回数据
当接口返回的数据比较复杂时,我们需要有更方便方式去提取数据,seldom提供 `jmespath`、`jsonpath` 来简化数据提取。
* 接口返回数据
```json
{
"args": {
"hobby": [
"basketball",
"swim"
],
"name": "tom"
},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate",
"Host": "httpbin.org",
"User-Agent": "python-requests/2.25.0",
"X-Amzn-Trace-Id": "Root=1-62851614-1ca9fdb276238c60406c118f"
},
"origin": "113.87.15.99",
"url": "http://httpbin.org/get?name=tom&hobby=basketball&hobby=swim"
}
```
* 常规提取
```python
import seldom
class TestAPI(seldom.TestCase):
def test_extract_responses(self):
"""
提取 response 数据
"""
payload = {"hobby": ["basketball", "swim"], "name": "tom", "age": "18"}
self.get("http://httpbin.org/get", params=payload)
# response
response1 = self.response["args"]["name"]
response2 = self.response["args"]["hobby"]
response3 = self.response["args"]["hobby"][0]
print(f"response1 --> {response1}")
print(f"response2 --> {response2}")
print(f"response3 --> {response3}")
# jmespath
jmespath1 = self.jmespath("args.name")
jmespath2 = self.jmespath("args.hobby")
jmespath3 = self.jmespath("args.hobby[0]")
jmespath4 = self.jmespath("hobby[0]", response=self.response["args"])
print(f"\njmespath1 --> {jmespath1}")
print(f"jmespath2 --> {jmespath2}")
print(f"jmespath3 --> {jmespath3}")
print(f"jmespath4 --> {jmespath4}")
# jsonpath
jsonpath1 = self.jsonpath("$..name")
jsonpath2 = self.jsonpath("$..hobby")
jsonpath3 = self.jsonpath("$..hobby[0]")
jsonpath4 = self.jsonpath("$..hobby[0]", index=0)
jsonpath5 = self.jsonpath("$..hobby[0]", index=0, response=self.response["args"])
print(f"\njsonpath1 --> {jsonpath1}")
print(f"jsonpath2 --> {jsonpath2}")
print(f"jsonpath3 --> {jsonpath3}")
print(f"jsonpath4 --> {jsonpath4}")
print(f"jsonpath5 --> {jsonpath5}")
...
```
说明:
* `response`: 保存接口返回的数据,可以直接以,字典列表的方式提取。
* `jmespath()`: 根据 JMESPath 语法规则,默认提取接口返回的数据,也可指定`resposne`数据提取。
* `jsonpath()`: 根据 JsonPath 语法规则,默认提取接口返回的数据, `index`指定下标,也可指定`resposne`数据提取。
运行结果:
```shell
2022-05-19 00:57:08 log.py | DEBUG | [response]:
{'args': {'age': '18', 'hobby': ['basketball', 'swim'], 'name': 'tom'}, 'headers': {'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate', 'Host': 'httpbin.org', 'User-Agent': 'python-requests/2.25.0', 'X-Amzn-Trace-Id': 'Root=1-62852563-2fe77d4b1ce544696af60f10'}, 'origin': '113.87.15.99', 'url': 'http://httpbin.org/get?hobby=basketball&hobby=swim&name=tom&age=18'}
response1 --> tom
response2 --> ['basketball', 'swim']
response3 --> basketball
jmespath1 --> tom
jmespath2 --> ['basketball', 'swim']
jmespath3 --> basketball
jmespath4 --> basketball
jsonpath1 --> ['tom']
jsonpath2 --> [['basketball', 'swim']]
jsonpath3 --> ['basketball']
jsonpath4 --> basketball
jsonpath5 --> basketball
```
运行结果
```shell
...
2022-04-10 21:05:17.683 | DEBUG | seldom.logging.log:debug:34 - [response]:
{'args': {'age': '18', 'hobby': ['basketball', 'swim'], 'name': 'tom'}, 'headers': {'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate', 'Host': 'httpbin.org', 'User-Agent': 'python-requests/2.25.0', 'X-Amzn-Trace-Id': 'Root=1-6252d60c-551433d744b6869e5d1944d7'}, 'origin': '113.87.12.14', 'url': 'http://httpbin.org/get?hobby=basketball&hobby=swim&name=tom&age=18'}
2022-04-10 21:05:17.686 | DEBUG | seldom.logging.log:debug:34 - [jresponse]:
['basketball']
2022-04-10 21:05:17.689 | DEBUG | seldom.logging.log:debug:34 - [jresponse]:
['18']
```
### genson
通过 `assertSchema()` 断言时需要写JSON
Schema,但是这个写起来需要学习成本,seldom集成了[GenSON](https://github.com/wolverdude/GenSON) ,可以帮你自动生成。
* 例子
```python
import seldom
from seldom.utils import genson
class TestAPI(seldom.TestCase):
def test_assert_schema(self):
payload = {"hobby": ["basketball", "swim"], "name": "tom", "age": "18"}
self.get("/get", params=payload)
print("response \n", self.response)
schema = genson(self.response)
print("json Schema \n", schema)
self.assertSchema(schema)
```
* 运行日志
```shell
...
response
{'args': {'age': '18', 'hobby': ['basketball', 'swim'], 'name': 'tom'}, 'headers': {'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate', 'Host': 'httpbin.org', 'User-Agent': 'python-requests/2.25.0', 'X-Amzn-Trace-Id': 'Root=1-626574d0-4c04bb7e76a53e8042c9d856'}, 'origin': '173.248.248.88', 'url': 'http://httpbin.org/get?hobby=basketball&hobby=swim&name=tom&age=18'}
json Schema
{'$schema': 'http://json-schema.org/schema#', 'type': 'object', 'properties': {'args': {'type': 'object', 'properties': {'age': {'type': 'string'}, 'hobby': {'type': 'array', 'items': {'type': 'string'}}, 'name': {'type': 'string'}}, 'required': ['age', 'hobby', 'name']}, 'headers': {'type': 'object', 'properties': {'Accept': {'type': 'string'}, 'Accept-Encoding': {'type': 'string'}, 'Host': {'type': 'string'}, 'User-Agent': {'type': 'string'}, 'X-Amzn-Trace-Id': {'type': 'string'}}, 'required': ['Accept', 'Accept-Encoding', 'Host', 'User-Agent', 'X-Amzn-Trace-Id']}, 'origin': {'type': 'string'}, 'url': {'type': 'string'}}, 'required': ['args', 'headers', 'origin', 'url']}
```
### mock URL
> seldom 3.2.3 支持
seldom 运行允许通过`confrun.py`文件中`mock_url()` 配置mock URL映射。
* `confrun.py` 配置要映射的mock URL。
```python
def mock_url():
"""
mock url
:return:
"""
config = {
"http://httpbin.org/get": "http://127.0.0.1:8000/api/data",
}
return config
```
* test_api.py
```python
import seldom
class TestRequest(seldom.TestCase):
"""
http api test demo
"""
def test_get_method(self):
payload = {'key1': 'value1', 'key2': 'value2'}
self.get("/get", params=payload)
self.assertStatusCode(200)
if __name__ == '__main__':
seldom.main(base_url="http://httpbin.org")
```
* 运行
```shell
> python test_api.py
2023-07-30 14:47:08 | INFO | request.py | -------------- Request -----------------[🚀]
2023-07-30 14:47:08 | INFO | request.py | [method]: GET [url]: http://httpbin.org/get
2023-07-30 14:47:08 | DEBUG | request.py | [params]:
{
"key1": "value1",
"key2": "value2"
}
2023-07-30 14:47:08 | DEBUG | request.py | mock url: http://127.0.0.1:8000/api/data
2023-07-30 14:47:08 | INFO | request.py | -------------- Response ----------------[🛬️]
2023-07-30 14:47:08 | INFO | request.py | successful with status 200
2023-07-30 14:47:08 | DEBUG | request.py | [type]: json [time]: 0.002738
2023-07-30 14:47:08 | DEBUG | request.py | [response]:
[{'item_name': 'apple'}, {'item_name': 'banana'}, {'item_name': 'orange'}, {'item_name': 'watermelon'}, {'item_name': 'grape'}]
2023-07-30 14:47:08 | INFO | case.py | 👀 assertStatusCode -> 200.
```
通过日志可以看到 `http://httpbin.org/get` 替换成为 `http://127.0.0.1:8000/api/data` 执行。 当你不想mock的时候只需要修改
mock_url() 即可,对于用例来说无影响。
### 配置`proxies`代理
> seldom 3.11.0
__单个方法设置代理__
seldom 支持在每个请求方法中设置代理。
```shell
import seldom
class TestHttpAssert(seldom.TestCase):
def test_req_proxy(self):
"""
test request proxy
"""
payload = {"name": "tom", "hobby": ["basketball", "swim"]}
proxies = {
"https": "http://localhost:1080",
"http": "http://localhost:1080",
}
self.get("/get", params=payload, proxies=proxies)
```
__全局设置代理__
当我们要所有用例都使用代理时,每个方法都单独设置就很麻烦了,可以使用`confrun.py`全局设置。
* 目录结构
```shell
├───reports
├───test_data
├───test_dir
│ ├───...
├───confrun.py # 配置文件
└───run.py
```
* `confrun.py` 配置要映射的mock URL。
```python
def proxies():
"""
http proxies
"""
proxies_conf = {
"https": "http://localhost:1080",
"http": "http://localhost:1080",
}
return proxies_conf
```
通过`run.py`文件全局运行测试,这里的代理配置将作用于所有请求方法。
### 保存响应结果
> seldom > 3.13
有时候接口的response非常的长,终端显示不完整,那我们就可以使用`save_response()`将结果保存到文件中。
```python
import seldom
class TestSaveResp(seldom.TestCase):
def test_save_response(self):
"""将response保存到文件中"""
resp = self.get("/get")
self.save_response(resp)
if __name__ == '__main__':
seldom.main(base_url="https://httpbin.org")
```
### 保存响应结果
> seldom > 3.13
当我们本地使用了host切换,需要知道当前的请求是否指向了host地址,可以使用`ip_address()`方法。
```python
import seldom
class TestReqIP(seldom.TestCase):
def test_get_ip_address(self):
"""检查当前请求的IP地址"""
self.get("/get")
self.ip_address()
if __name__ == '__main__':
seldom.main(base_url="https://httpbin.org")
```
### @retry装饰器
`@retry()` 装饰器用于用法失败充实,例如封装的登录方法,允许API调用失败后再次尝试。
示例如下:
```python
from seldom.request import HttpRequest
from seldom.request import check_response, retry
class LoginAPIObject(HttpRequest):
@retry(times=2, wait=3)
@check_response(ret="form.token")
def user_login(self, username: str, password: str) -> str:
"""
模拟:登录API
"""
params = {"username": username, "token": password}
r = self.post("/error", json=params)
return r
if __name__ == '__main__':
login = LoginAPIObject()
login.user_login("tom", "abc123")
```
* `@retry()`装饰器,`times`参数指定重试次数,默认`3`次,`wait`参数指定重试间隔,默认`1s`。
* `@retry()`装饰器可以单独使用,也可以和 `@check_response()`装饰器一起使用,如果一起使用的话,需要在上方。
运行结果:
```shell
2024-03-04 22:36:09 | INFO | request.py | -------------- Request -----------------[🚀]
2024-03-04 22:36:09 | INFO | request.py | [method]: POST [url]: /error
2024-03-04 22:36:09 | DEBUG | request.py | [json]:
{
"username": "tom",
"token": "abc123"
}
2024-03-04 22:36:09 | WARNING | request.py | Attempt to execute failed with error: 'Invalid URL '/error': No scheme supplied. Perhaps you meant https:///error?'. Attempting retry number 1...
2024-03-04 22:36:12 | INFO | request.py | -------------- Request -----------------[🚀]
2024-03-04 22:36:12 | INFO | request.py | [method]: POST [url]: /error
2024-03-04 22:36:12 | DEBUG | request.py | [json]:
{
"username": "tom",
"token": "abc123"
}
2024-03-04 22:36:12 | WARNING | request.py | Attempt to execute failed with error: 'Invalid URL '/error': No scheme supplied. Perhaps you meant https:///error?'. Attempting retry number 2...
2024-03-04 22:36:15 | INFO | request.py | -------------- Request -----------------[🚀]
2024-03-04 22:36:15 | INFO | request.py | [method]: POST [url]: /error
2024-03-04 22:36:15 | DEBUG | request.py | [json]:
{
"username": "tom",
"token": "abc123"
}
Traceback (most recent call last):
File "D:\github\seldom\api\auth_object.py", line 20, in
login.user_login("tom", "abc123")
....
File "C:\Users\fnngj\.virtualenvs\seldom-wKum2rzm\Lib\site-packages\requests\models.py", line 439, in prepare_url
raise MissingSchema(
requests.exceptions.MissingSchema: Invalid URL '/error': No scheme supplied. Perhaps you meant https:///error?
```
从运行结果可以看到,调用接口重试了2次,如果仍然错误,抛出异常。
## 加密工具
> seldom > 3.11.0
在进行接口测试的时候,经常设计参数的加密,例如:`MD5`、`AES`等。Seldom 框架提供完整的加密解密功能,支持以下功能:
* 哈希算法
* MD5
* SHA1/SHA224/SHA256/SHA384/SHA512
* HMAC
* 对称加密
* AES (CBC/ECB/CFB/OFB/CTR)
* DES
* 3DES
* 非对称加密
* RSA
* 编码转换
* Base16/Base32/Base64/Base85
* URL编码
* HTML编码
__示例__
```python
import unittest
# 导入待测试的模块
from seldom.utils.encrypt import (
CipherMode,
HashUtil,
AESUtil,
EncodeUtil,
)
class TestHashUtil(unittest.TestCase):
"""测试 HashUtil 类"""
def test_md5(self):
text = "hello world"
expected = "5eb63bbbe01eeed093cb22bb8f5acdc3"
self.assertEqual(HashUtil.md5(text), expected)
def test_sha256(self):
text = "hello world"
expected = "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"
self.assertEqual(HashUtil.sha256(text), expected)
class TestAESUtil(unittest.TestCase):
"""测试 AESUtil 类"""
def test_encrypt_decrypt_cbc(self):
key = "mysecretkey"
text = "hello world"
encrypted = AESUtil.encrypt(key, text, mode=CipherMode.CBC)
decrypted = AESUtil.decrypt(key, encrypted, mode=CipherMode.CBC)
self.assertEqual(decrypted, text)
class TestEncodeUtil(unittest.TestCase):
"""测试 EncodeUtil 类"""
def test_base64_encode_decode(self):
text = "hello world"
encoded = EncodeUtil.base64_encode(text)
decoded = EncodeUtil.base64_decode(encoded)
self.assertEqual(decoded, text)
def test_url_encode_decode(self):
text = "hello world"
encoded = EncodeUtil.url_encode(text)
decoded = EncodeUtil.url_decode(encoded)
self.assertEqual(decoded, text)
def test_html_encode_decode(self):
text = "hello world"
encoded = EncodeUtil.html_encode(text)
decoded = EncodeUtil.html_decode(encoded)
self.assertEqual(decoded, text)
if __name__ == '__main__':
unittest.main()
```
同时`示例`看到,我们可以非常低成本的使用各种加解密算法。
__运行结果__
```shell
> python .\test_encrypt.py
2025-01-07 18:20:12 | INFO | encrypt.py | MainThread | ✅ [encrypt] method, generated data: jUTwE9UV8c/00d9Kl9UOhdTOoOwWYSVOJ7io72MtWeE=
2025-01-07 18:20:12 | INFO | encrypt.py | MainThread | ✅ [decrypt] method, generated data: hello world
.2025-01-07 18:20:12 | INFO | encrypt.py | MainThread | ✅ [base64_encode] method, generated data: aGVsbG8gd29ybGQ=
2025-01-07 18:20:12 | INFO | encrypt.py | MainThread | ✅ [base64_decode] method, generated data: hello world
.2025-01-07 18:20:12 | INFO | encrypt.py | MainThread | ✅ [html_encode] method, generated data: <html>hello world</html>
2025-01-07 18:20:12 | INFO | encrypt.py | MainThread | ✅ [html_decode] method, generated data: hello world
.2025-01-07 18:20:12 | INFO | encrypt.py | MainThread | ✅ [url_encode] method, generated data: hello%20world
2025-01-07 18:20:12 | INFO | encrypt.py | MainThread | ✅ [url_decode] method, generated data: hello world
.2025-01-07 18:20:12 | INFO | encrypt.py | MainThread | ✅ [md5] method, generated data: 5eb63bbbe01eeed093cb22bb8f5acdc3
.2025-01-07 18:20:12 | INFO | encrypt.py | MainThread | ✅ [sha256] method, generated data: b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9
.
----------------------------------------------------------------------
Ran 6 tests in 0.005s
OK
```
================================================
FILE: docs/vpdocs/api-testing/start.md
================================================
# HTTP测试
## 优势
seldom 非常适合个人接口自动化项目,它有以下优势。
* 可以写更少的代码
* 提供详细的运行日志
* 提供专门为接口设计的断言
* 强大的数据驱动
* 自动生成HTML/XML测试报告
* 支持生成随机数据
* 支持`har`/`swagger`文件转case
* 支持数据库操作
这些是seldom支持的功能,我们只需要集成HTTP接口库,并提供强大的断言即可。`seldom 2.0` 加入了HTTP接口自动化测试支持。
Seldom 完全兼容 [Requests](https://docs.python-requests.org/en/master/) API 如下:
| seldom | requests |
|----------------|--------------------|
| self.get() | requests.get() |
| self.post() | requests.post() |
| self.put() | requests.put() |
| self.delete() | requests.delete() |
| self.patch() | requests.patch() |
| self.session() | requests.session() |
## Seldom VS Request+unittest
* unittest + requests 接口自动化示例:
```python
import unittest
import requests
class TestAPI(unittest.TestCase):
def test_get_method(self):
payload = {'key1': 'value1', 'key2': 'value2'}
r = requests.get("http://httpbin.org/get", params=payload)
self.assertEqual(r.status_code, 200)
if __name__ == '__main__':
unittest.main()
```
* seldom 接口自动化测试示例:
```python
# test_req.py
import seldom
class TestAPI(seldom.TestCase):
def test_get_method(self):
payload = {'key1': 'value1', 'key2': 'value2'}
self.get("http://httpbin.org/get", params=payload)
self.assertStatusCode(200)
if __name__ == '__main__':
seldom.main(debug=True)
```
主要简化点在,接口的返回数据的处理。当然,seldom真正的优势在断言、日志和报告。
* 运行日志
打开debug模式`seldom.run(debug=True)` 运行上面的用例。
```shell
> python test_req.py
__ __
________ / /___/ /___ ____ ____
/ ___/ _ \/ / __ / __ \/ __ ` ___/
(__ ) __/ / /_/ / /_/ / / / / / /
/____/\___/_/\__,_/\____/_/ /_/ /_/ v3.x.x
-----------------------------------------
@itest.info
test_get_method (test_req.TestAPI) ... 2023-02-14 23:37:07 request.py | INFO |
-------------- Request -----------------[🚀]
2023-02-14 23:37:07 request.py | INFO | [method]: GET [url]: http://httpbin.org/get
2023-02-14 23:37:07 request.py | DEBUG | [params]:
{
"key1": "value1",
"key2": "value2"
}
2023-02-14 23:37:08 request.py | INFO | -------------- Response ----------------[🛬️]
2023-02-14 23:37:08 request.py | INFO | successful with status 200
2023-02-14 23:37:08 request.py | DEBUG | [type]: json [time]: 0.785683
2023-02-14 23:37:08 request.py | DEBUG | [response]:
{
"args": {
"key1": "value1",
"key2": "value2"
},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate",
"Host": "httpbin.org",
"User-Agent": "python-requests/2.28.1",
"X-Amzn-Trace-Id": "Root=1-63ebaaa4-325e25be64b104e770c25f8f"
},
"origin": "173.248.248.88",
"url": "http://httpbin.org/get?key1=value1&key2=value2"
}
2023-02-14 23:37:08 case.py | INFO | 👀 assertStatusCode -> 200.
ok
----------------------------------------------------------------------
Ran 1 test in 0.795s
OK
2023-02-14 23:37:08 runner.py | SUCCESS | A run the test in debug mode without generating HTML report!
```
通过日志/报告都可以看到详细的HTTP接口调用信息。
================================================
FILE: docs/vpdocs/api-testing/webscocket.md
================================================
# WebSocket
> seldom > 3.6.0 支持该功能
有些时间我们需要通过`WebSocket`实现长连接,很高兴的告诉告诉你seldom支持`WebSocket`测试了。
### WebSocket 生命周期
WebSocket 生命周期中包含几个关键的事件,这些事件允许开发人员在连接的不同阶段执行代码。以下是WebSocket API中定义的主要事件:
* `open`: 当WebSocket连接成功建立时触发。这个事件表明客户端与服务器之间的连接已经打开,可以开始数据传输。
* `message`: 当客户端接收到服务器发送的消息时触发。这个事件用于处理从服务器接收到的所有消息。
* `error`: 当发生错误,导致WebSocket连接关闭之前或连接无法成功建立时触发。这个事件可以用来处理和响应WebSocket过程中出现的任何异常或错误情况。
* `close`: 当连接被关闭时触发,无论是客户端还是服务器端主动关闭连接,或是因为某种原因连接被迫关闭。这个事件表明WebSocket连接已经彻底关闭,可以进行清理和后续处理。
### seldom测试WebSocket
在seldom中测试WebSocket非常简单。
* 首先,需要一个WebSocket服务。
通过`aiohttp`实现`websocket_server.py`。
```shell
# websocket_server.py
from aiohttp import web
import aiohttp
async def websocket_handler(request):
ws = web.WebSocketResponse()
await ws.prepare(request)
async for msg in ws:
if msg.type == aiohttp.WSMsgType.TEXT:
print("message", msg.data)
if msg.data == 'close':
await ws.close()
else:
await ws.send_str(f"Message text was: {msg.data}")
elif msg.type == aiohttp.WSMsgType.ERROR:
print('ws connection closed with exception %s' %
ws.exception())
print('websocket connection closed')
return ws
app = web.Application()
app.router.add_get('/ws', websocket_handler)
web.run_app(app, port=8765)
```
* 然后,通过seldom编写WebSocket测试用例。
```shell
import seldom
from seldom.logging import log
from seldom.websocket_client import WebSocketClient
class WebSocketTest(seldom.TestCase):
def start(self):
# 创建WebSocket客户端线程
self.client = WebSocketClient("ws://0.0.0.0:8765/ws")
self.client.start()
# 等待客户端连接建立
self.sleep(1) # 这里假设服务器可以在1秒内响应连接
def tearDown(self):
# 发送关闭消息
self.client.send_message("close")
# 停止WebSocket客户端线程
self.client.stop()
self.client.join()
def test_send_and_receive_message(self):
# 发送消息
self.client.send_message("Hello, WebSocket!")
self.client.join(1) # 等待接收消息
self.client.send_message("How are you?")
self.client.join(1) # 等待接收消息
# 验证是否收到消息
log.info(self.client.received_messages)
self.assertEqual(len(self.client.received_messages), 2)
self.assertIn("Hello, WebSocket!", self.client.received_messages[0])
self.assertIn("How are you?", self.client.received_messages[1])
if __name__ == '__main__':
seldom.main(debug=True)
```
* 运行日志
```shell
> python test_websocket.py
test_send_and_receive_message (test_websocket.WebSocketTest.test_send_and_receive_message) ...
2024-04-05 23:36:33 | INFO | case.py | 💤️ sleep: 1s.
2024-04-05 23:36:33 | INFO | websocket_client.py | WebSocket connection opened.
2024-04-05 23:36:36 | INFO | test_websocket.py | ['Message text was: Hello, WebSocket!', 'Message text was: How are you?']
ok
----------------------------------------------------------------------
Ran 1 test in 3.006s
OK
2024-04-05 23:36:36 | SUCCESS | runner.py | A run the test in debug mode without generating HTML report!
```
================================================
FILE: docs/vpdocs/app-testing/adb_lib.md
================================================
# ADB 操作
App(Android)测试必然需要用到adb命令, seldom根据需要封装了几个常用的操作。
* 获取设备信息
```python
from seldom.utils.adbutils import ADBUtils
adb = ADBUtils()
devices = adb.refresh_devices()
print("当前连接设备:", devices)
# 设置默认设备 - 多设备的情况下,后续操作需要设置设备ID
if devices:
adb.set_default_device(devices[0][0])
```
打印信息:
```shell
当前连接设备: [('MDX0220413011925', 'ELS-AN00')]
```
* 获取当前启动的app信息
```shell
from seldom.utils.adbutils import ADBUtils
adb = ADBUtils()
app_info = adb.get_app_info()
for info in app_info:
print(info['package'], info["activity"])
```
打印信息
```shell
com.huawei.android.launcher com.huawei.android.launcher.unihome.UniHomeLauncher
com.hpbr.bosszhipin com.hpbr.bosszhipin.module.main.activity.MainActivity
com.android.mms com.android.mms.ui.ConversationList
com.tencent.mm com.tencent.mm.ui.LauncherUI
com.delivery.aggregator com.delivery.aggregator.activity.QYMainActivity
com.huawei.browser com.huawei.browser.BrowserMainActivity
com.huawei.android.launcher .unihome.UniHomeLauncher
com.hpbr.bosszhipin .module.main.activity.MainActivity
com.android.mms .ui.ConversationList
com.tencent.mm .ui.LauncherUI
com.delivery.aggregator .activity.QYMainActivity
com.huawei.browser .BrowserMainActivity
```
* 启动&关闭app
```python
import time
from seldom.utils.adbutils import ADBUtils
adb = ADBUtils()
package = "com.microsoft.bing"
if adb.launch_app(package):
print(f"成功启动 {package}")
time.sleep(5)
if adb.close_app(package):
print(f"成功关闭 {package}")
```
打印信息:
```shell
成功启动 com.microsoft.bing
成功关闭 com.microsoft.bing
```
================================================
FILE: docs/vpdocs/app-testing/appium_lab.md
================================================
# appium API
appium API继承 selenium API,所以,操作方法是通用的。在seldom 中,请参考web UI 中的seldom API。
## appium 定位
* 支持定位类型
seldom 支持定位如下,包括selenium/appium。
| 类型 | 定位 | **kwargs |
|-----------------|----------------------|-----------------------------|
| selenium/appium | id | id_="id" |
| selenium | mame | name="name" |
| selenium/appium | class | class_name="class" |
| selenium | tag | tag="input" |
| selenium | link_text | link_text="文字链接" |
| selenium | partial_link_text | partial_link_text="文字链" |
| selenium/appium | xpath | xpath="//*[@id='11']" |
| selenium | css | cass="input#id" |
| appium | ios_uiautomation | ios_uiautomation = "xx" |
| appium | ios_predicate | ios_predicate = "xx" |
| appium | ios_class_chain | ios_class_chain = "xx" |
| appium | android_uiautomator | android_uiautomator = "xx" |
| appium | android_viewtag | android_viewtag = "xx" |
| appium | android_data_matcher | android_data_matcher = "xx" |
| appium | android_view_matcher | android_view_matcher = "xx" |
| appium | windows_uiautomation | windows_uiautomation = "xx" |
| appium | accessibility_id | accessibility_id = "xx" |
| appium | image | image = "xx" |
| appium | custom | custom = "xx" |
* 定位用法
```python
import seldom
from seldom.appium_lab.android import UiAutomator2Options
class TestBBS(seldom.TestCase):
def test_bbs(self):
"""定位方法用法"""
self.click(id_="com.meizu.flyme.flymebbs:id/nw")
self.sleep(2)
self.type(android_uiautomator='new UiSelector().resourceId("com.meizu.flyme.flymebbs:id/nw")', text="flyme")
...
if __name__ == '__main__':
capabilities = {
"automationName": "UiAutomator2",
"platformName": "Android",
"appPackage": "com.meizu.flyme.flymebbs",
"appActivity": "com.meizu.myplus.ui.splash.SplashActivity",
"noReset": True,
}
options = UiAutomator2Options().load_capabilities(capabilities)
seldom.main(app_server="http://127.0.0.1:4723", app_info=options, debug=True)
```
## appium lab
`appium_lab` 封装了常用App操作
* 基本用法
```python
import seldom
from seldom.appium_lab import AppiumLab
class TestBBS(seldom.TestCase):
def start(self):
# 导入 AppiumLab
self.appium_lab = AppiumLab(self.driver)
def test_bbs(self):
# 点击输入框
self.click(id_="com.meizu.flyme.flymebbs:id/nw")
self.sleep(2)
# 判断当前虚拟键盘是否显示
keyboard = self.appium_lab.is_keyboard_shown()
print(keyboard)
# 收起当前键盘
self.appium_lab.hide_keyboard()
self.sleep(3)
if __name__ == '__main__':
...
```
* 启动appium server
```python
from seldom.appium_lab.appium_service import AppiumService
if __name__ == '__main__':
# 启动 Appium Server
app_ser = AppiumService(
addr="127.0.0.1",
port="4723",
use_plugins="images",
args=["--allow-cors", "--tmp", "C:\Windows\Temp"])
app_ser.start_service()
```
参数说明:
* `addr`: appium server 地址, 默认: `127.0.0.1`
* `port`: appium server 端口, 默认:`4723`
* `log`: 设置 appium server 日志, 默认:`appium_server_1734493548.log`
* `use_plugins`: 设置使用的插件,默认None,不使用。
* `args`: 支持添加更多的参数,例如 `args=["--allow-cors", "--tmp", "C:\Windows\Temp"]`
启动日志:
```shell
2024-12-18 11:52:54 | INFO | appium_service.py | MainThread | 🚀 launch appium server: ['--address', '127.0.0.1', '--port', '4723', '--log', 'D:\\github\\seldomQA\\seldom\\seldom\\appium_lab\\appium_server_1734493974.log', '--use-plugins', 'iamges,ocr', '--allow-cors']
```
`AppiumLab` 类中分以下几类操作:
__Action__
`Action`中提供基本滑动/触摸操作。
```python
from seldom.appium_lab import AppiumLab
appium_lab = AppiumLab()
# 触摸坐标位
appium_lab.tap(x=100, y=200)
# 上划
appium_lab.swipe_up()
# 下划
appium_lab.swipe_down()
# 左划
appium_lab.swipe_left()
# 右划
appium_lab.swipe_right()
# 从x坐标滑动到y坐标
appium_lab.drag_from_to()
```
__Switch__
`Switch`中提供基本上下文切换操作。
```python
from seldom.appium_lab import AppiumLab
appium_lab = AppiumLab()
# 返回当前上下文
context = appium_lab.context()
# 切换原生app
appium_lab.switch_to_app()
# 切换webview
appium_lab.switch_to_web()
# 切换flutter
appium_lab.switch_to_flutter()
# 切换OCR
appium_lab.switch_to_ocr()
```
__Find__
`Find`中提供基于文本的查找,一个元素可以没有ID、name,但一定有显示的文本,这里提供了一组基于文本的查找。
```python
from seldom.appium_lab import AppiumLab
appium_lab = AppiumLab()
# Android
appium_lab.find_view(text="xxx标题").click()
appium_lab.find_view(content_desc="xxx标题").click()
appium_lab.find_edit_text(text="xxx标题").click()
appium_lab.find_button(text="xxx标题").click()
appium_lab.find_button(content_desc="xxx标题").click()
appium_lab.find_text_view(text="xxx标题").click()
appium_lab.find_image_view(text="xxx标题").click()
appium_lab.find_check_box(text="xxx标题").click()
# iOS
appium_lab.find_static_text(text="xxx标题").click()
appium_lab.find_other(text="xxx标题").click()
appium_lab.find_text_field(text="xxx标题").click()
appium_lab.find_image(text="xxx标题").click()
appium_lab.find_ios_button(text="xxx标题").click()
```
__keyboard__
`keyboard`中提供基于键盘的输入和操作。
```python
from seldom.appium_lab import AppiumLab
appium_lab = AppiumLab()
# 基于键盘输入(支持大小写)
appium_lab.key_text("Hello123")
# 手机home键
appium_lab.home()
# 手机返回键
appium_lab.back()
# 判断当前虚拟键盘是否显示(True/False)
ret = appium_lab.is_keyboard_shown()
print(ret)
# 收起虚拟键盘
appium_lab.hide_keyboard()
# 返回当前窗口尺寸
size = appium_lab.size()
```
## appium driver
`AppDriver` 封装了App相关的操作。
```python
import seldom
class TestApp(seldom.TestCase):
"""
Test App
"""
def test_bbs_search(self):
"""
appium api
"""
# app置于后台10s
self.background_app(10)
# 检查设备上是否安装了应用程序
self.is_app_installed("bundle_id")
# 安装app
self.install_app("/app/path/xxx.apk")
# 删除app
self.remove_app("app_id")
# 如果app正在运行,终止运行
self.terminate_app("app_id")
# 如果app未运行,则激活它或者在后台运行
self.activate_app("app_id")
# 查询app 状态
state = self.query_app_state("app_id")
print(state)
# 从指定的设备返回应用程序字符串语言
language, string = self.app_strings()
print(language, string)
# 点击图片
self.click_image("/you/path/xxx.png")
```
> 目前 seldom 集成的 appium API
> 并不完整,在使用过程中如有问题,欢迎提 [issues](https://github.com/SeldomQA/seldom/issues)。
================================================
FILE: docs/vpdocs/app-testing/extensions.md
================================================
# appium 扩展
appium支持扩展,通过扩展来增强appium定位元素的能力。
## appium images-plugin
使用此插件支持的`-image`定位器策略,可以通过Appium指定想要定位的元素的图片文件。
* 安装Appium images-plugin插件。
```shell
> appium plugin install images
```
* 查看已安装的Appium插件。
```shell
> appium plugin list --installed
✔ Listing installed plugins
- images@2.1.8 [installed (npm)]
```
* 启动Appium server时指定使用OCR插件。
```shell
> appium server --address '127.0.0.1' -p 4723 --use-plugins=images
```
* 目录结构
```tree
├───test_appium_images.py
└───phone.jpg
```
* 编写App自动化测试脚本
```python
# test_appium_images.py
import seldom
from seldom.utils.file_extend import file
from seldom.appium_lab.android import UiAutomator2Options
class TestApp(seldom.TestCase):
def test_app_images(self):
self.wait(10)
file_path = file.join(file.dir, "phone.jpg")
self.click_image(file_path)
if __name__ == '__main__':
capabilities = {
"automationName": "UiAutomator2",
"platformName": "Android",
"appPackage": "com.meizu.flyme.flymebbs",
"appActivity": "com.meizu.myplus.ui.splash.SplashActivity",
"noReset": True,
}
options = UiAutomator2Options().load_capabilities(capabilities)
seldom.main(app_server="http://127.0.0.1:4723", app_info=options)
```
通过`click_image()` 来点击图片匹配到整个页面上的元素的坐标位。
## Appium OCR plugin
* 安装Appium OCR plugin插件。
```shell
> appium plugin install images--source=npm appium-ocr-plugin
```
* 查看已安装的Appium插件。
```shell
> appium plugin list --installed
✔ Listing installed plugins
- ocr@0.2.0 [installed (npm)]
```
* 启动Appium server时指定使用OCR插件。
```shell
> appium server --address '127.0.0.1' -p 4723 --use-plugins=ocr
```
* 编写App自动测试脚本。
```python
# test_appium_orc.py
import seldom
from seldom.appium_lab.switch import Switch
from seldom.appium_lab.ocr_plugin import OCRCommand
from seldom.appium_lab.android import UiAutomator2Options
class TestApp(seldom.TestCase):
def start(self):
self.switch = Switch(self.driver)
def test_orc_case(self):
ocr = self.driver.ocr_command({})
print(ocr)
self.switch.switch_to_ocr()
self.click(xpath='//words/item[text() = "Flyme"]')
if __name__ == '__main__':
capabilities = {
"automationName": "UiAutomator2",
"platformName": "Android",
"appPackage": "com.meizu.flyme.flymebbs",
"appActivity": "com.meizu.myplus.ui.splash.SplashActivity",
"noReset": True,
}
options = UiAutomator2Options().load_capabilities(capabilities)
seldom.main(app_server="http://127.0.0.1:4723", app_info=options, extensions=[OCRCommand])
```
根据上面代码示例,打印ocr变量得到一个JSON结构体。
```json
{
"words": [
{
"text": "mEngine",
"confidence": 88.47775268554688,
"bbox": {
"x0": 86,
"y0": 509,
"x1": 308,
"y1": 560
}
},
{
"text": "Flyme",
"confidence": 91.3454818725586,
"bbox": {
"x0": 316,
"y0": 1132,
"x1": 420,
"y1": 1172
}
},
{
"text": "A9",
"confidence": 34.86248779296875,
"bbox": {
"x0": 1017,
"y0": 2565,
"x1": 1078,
"y1": 2595
}
}
],
"lines": [
{
"text": "mEngine BY Ni0vEh 1 Bl\n\n",
"confidence": 21.003677368164062,
"bbox": {
"x0": 86,
"y0": 500,
"x1": 674,
"y1": 560
}
},
{
"text": "Flyme\n\n",
"confidence": 91.3454818725586,
"bbox": {
"x0": 316,
"y0": 1132,
"x1": 420,
"y1": 1172
}
},
{
"text": "A9\n",
"confidence": 34.86248779296875,
"bbox": {
"x0": 1017,
"y0": 2565,
"x1": 1078,
"y1": 2595
}
}
],
"blocks": [
{
"text": "mEngine BY Ni0vEh 1 Bl\n\n",
"confidence": 21.003677368164062,
"bbox": {
"x0": 86,
"y0": 500,
"x1": 674,
"y1": 560
}
},
{
"text": "Flyme\n\n",
"confidence": 91.3454818725586,
"bbox": {
"x0": 316,
"y0": 1132,
"x1": 420,
"y1": 1172
}
},
{
"text": "A9\n",
"confidence": 34.86248779296875,
"bbox": {
"x0": 1017,
"y0": 2565,
"x1": 1078,
"y1": 2595
}
}
]
}
```
JSON结构体说明:
* wrods - Tesseract识别的单个单词的列表。
* lines - Tesseract识别的文本行的列表。
* blocks - Tesseract识别连续文本块的列表。
每项都引用一个OCR对象,它们本身包含3个数据:
- text:识别的文本。
- confidence:Tesseract对于给定文本的OCR处理结果的置信度(范围在0到100之间)。
- bbox:发现文本的边界框,`边界框`标记为x0、x1、y0和y1的值的对象。分别表文本的上下左右坐标位置,其中。这里,x0表示发现文本的左边x坐标,x1表示右边x坐标,y0表示上部y坐标,y1表示下部y坐标。
================================================
FILE: docs/vpdocs/app-testing/page_object.md
================================================
# Page Object
在编写App自动化测试时,推荐使用`page object models`(简称 PO设计模式)。你可以看到seldom并没有完全封装appium的API,我们可以借助
poium 来实现基于元素的定位。
github: https://github.com/SeldomQA/poium
__pip 安装__
```shell
> pip install poium
```
__使用poium__
在seldom中基于poium实现元素的定位和操作。
```python
import seldom
from seldom.appium_lab.android import UiAutomator2Options
from poium import Page, Element, Elements
class BBSPage(Page):
search_input = Element(id_="com.meizu.flyme.flymebbs:id/nw")
search_button = Element(id_="com.meizu.flyme.flymebbs:id/o1")
search_result = Elements(id_="com.meizu.flyme.flymebbs:id/a29")
class TestBBS(seldom.TestCase):
def start(self):
self.bbs_page = BBSPage(self.driver)
def test_bbs(self):
self.sleep(5)
self.bbs_page.search_input.click()
self.bbs_page.search_input.send_keys("flyme")
self.bbs_page.search_button.click()
elems = self.bbs_page.search_result
for title in elems:
self.assertIn("flyme", title.text.lower())
if __name__ == '__main__':
# 定义运行App
capabilities = {
"automationName": "UiAutomator2",
"platformName": "Android",
"appPackage": "com.meizu.flyme.flymebbs",
"appActivity": "com.meizu.myplus.ui.splash.SplashActivity",
"noReset": True,
}
options = UiAutomator2Options().load_capabilities(capabilities)
seldom.main(app_server="http://127.0.0.1:4723", app_info=options, debug=True)
```
__定位方法__
poium 支持的定位方法。
```shell
# selenium
css = "xx"
id_ = "xx"
name = "xx"
xpath = "xx"
link_text = "xx"
partial_link_text = "xx"
tag = "xx"
class_name = "xx"
# appium
ios_uiautomation = "xx"
ios_predicate = "xx"
ios_class_chain = "xx"
android_uiautomator = "xx"
android_viewtag = "xx"
android_data_matcher = "xx"
android_view_matcher = "xx"
windows_uiautomation = "xx"
accessibility_id = "xx"
image = "xx"
custom = "xx"
```
__`Element`类参数__
* timeout: 设置超时检查次数,默认为5。
* index: 设置元素索引,当你的定位方式默认匹配到多个元素时,默认返回第1个,即为0.
* describe: 设置元素描述,默认为undefined, 建议为每个元素增加描述。
================================================
FILE: docs/vpdocs/app-testing/start.md
================================================
# app 测试
`seldom 3.0` 基于appium支持APP测试。
appium 官方网站:https://appium.io/
## 环境安装
app 的自动化测试环境相比较 web 要复杂一些,请参考appium官方。
1. 安装node
https://nodejs.org/en/
```shell
> node --version
v16.17.0
```
2. 安装appium
```shell
> npm i --location=global appium # appium 2.x
```
3. 启动appium
```shell
> appium server --address '127.0.0.1' -p 4723
[Appium] Welcome to Appium v2.2.2
[Appium] Non-default server args:
[Appium] {
[Appium] address: '127.0.0.1'
[Appium] }
...
```
4. 移动设备
准备一台设备(Android/iOS手机)通过USB数据线连接电脑。通过以下工具确认手机与电脑是否连接。
* adb
```shell
> adb devices
List of devices attached
UMXDU000000000000 device
```
* taobao-iphone-device
```shell
> tidevice list
List of apple devices attached
00008030-00000000000000 xxx的iPhoneSE
```
## 编写测试
基于seldom编写app自动化测试, 由于appium 继承自selenium,所以,部分API共用。
```python
import seldom
from seldom.appium_lab.android import UiAutomator2Options
class TestBBS(seldom.TestCase):
def test_bbs_search(self):
self.click(id_="com.meizu.flyme.flymebbs:id/nw")
self.type(id_="com.meizu.flyme.flymebbs:id/nw", text="flyme")
self.click(id_="com.meizu.flyme.flymebbs:id/o1")
self.sleep(2)
elems = self.get_elements(id_="com.meizu.flyme.flymebbs:id/a29")
for title in elems:
print(title.text)
self.assertIn("lyme", title.text)
if __name__ == '__main__':
capabilities = {
"automationName": "UiAutomator2",
"platformName": "Android",
"appPackage": "com.meizu.flyme.flymebbs",
"appActivity": "com.meizu.myplus.ui.splash.SplashActivity",
"noReset": True,
}
options = UiAutomator2Options().load_capabilities(capabilities)
seldom.main(app_server="http://127.0.0.1:4723", app_info=options)
```
> 注:上面的测试用例隐含了appium的一些知识点,你需要对appium有足够的了解。
* 运行日志
```shell
python test_app.py
__ __
________ / /___/ /___ ____ ____
/ ___/ _ \/ / __ / __ \/ __ ` ___/
(__ ) __/ / /_/ / /_/ / / / / / /
/____/\___/_/\__,_/\____/_/ /_/ /_/ v3.0.0
-----------------------------------------
@itest.info
XTestRunner Running tests...
----------------------------------------------------------------------
2022-10-03 00:01:30 webdriver.py | INFO | 💤️ sleep: 5s.
2022-10-03 00:01:35 webdriver.py | INFO | ✅ Find 1 element: id=com.meizu.flyme.flymebbs:id/nw -> click.
2022-10-03 00:01:36 webdriver.py | INFO | ✅ Find 1 element: id=com.meizu.flyme.flymebbs:id/nw -> input 'flyme'.
2022-10-03 00:01:37 webdriver.py | INFO | ✅ Find 1 element: id=com.meizu.flyme.flymebbs:id/o1 -> click.
2022-10-03 00:01:37 webdriver.py | INFO | 💤️ sleep: 2s.
2022-10-03 00:01:39 webdriver.py | INFO | ✅ Find 5 element: id=com.meizu.flyme.flymebbs:id/a29 .
flyme的屏幕色彩显示应该是比较差的
魅族17的Flyme9状态栏下拉问题。
flyme9.3连上耳机来电话还是会外放
flyme自带录屏功能吗?
关于Flyme 8.18.0A稳定版
Generating HTML reports...
.12022-10-03 00:01:40 runner.py | SUCCESS | generated html file: file:///D:\github\seldom\reports\2022_10_03_00_01_23_result.html
2022-10-03 00:01:40 runner.py | SUCCESS | generated log file: file:///D:\github\seldom\reports\seldom_log.log
```
================================================
FILE: docs/vpdocs/develop.md
================================================
## ☘️Introduction
基于 vuepress2.0+ 的 **seldom [操作文档](https://seldomqa.github.io/)**
你可以使用 Markdown 书写文档,并通过 VuePress 部署为可预览的页面。
## 📖使用说明
### 1. 安装
1. clone本项目并安装依赖
```bash
git clone https://github.com/SeldomQA/seldom.git
cd docs
yarn install
```
### 2. 开发
正式开发前,可以先阅读 [VuePress官方文档](https://v2.vuepress.vuejs.org/zh/)。
在`docs/vpdocs`文件夹内,修改你想修改的`.md`文档并保存。
然后执行以下命令进行预览或打包
```bash
yarn run dev # 预览
yarn run build # 生成静态页面
```
## 部署
**Github-Pages手动本地部署部署说明:**
本地进入项目中执行`deploy.sh`即可自动部署到github pages。
deploy.sh 的详情如下(**请自行判断启用注释掉的命令**):
```shell
#!/usr/bin/env sh
# 确保脚本抛出遇到的错误
set -e
# 生成静态文件
npm run build
# 进入生成的文件夹
cd vpdocs/.vuepress/dist
git init
git add -A
git commit -m 'deploy'
# 如果发布到 https://SeldomQA.github.io
git push -f git@github.com:SeldomQA/SeldomQA.github.io.git master
cd -
```
更多部署方式可以参阅 [VuePress文档|部署](https://v1.vuepress.vuejs.org/guide/deploy.html)。
---
Author:[@Yongchin](https://github.com/nickliya)
================================================
FILE: docs/vpdocs/getting-started/advanced.md
================================================
# 高级用法
### fixture
有时自动化测试用例的运行需要一些前置&后置步骤,seldom提供了相应的方法。
seldom重写了unittest的`fixture`,所以,请使用seldom的`fixture`,对应表格。
| unittest | seldom | 说明 |
|--------------------|------------------|-----------------------------|
| setUpClass(cls) | start_class(cls) | 测试类开始执行。 |
| tearDownClass(cls) | end_class(cls) | 测试类结束执行。 |
| setUp(self) | start(self) | 测试方法(用例)开始执行。 |
| tearDown(self) | end(self) | 测试方法(用例)结束执行。 |
| - | start_run() | `confrun.py`文件配置,整个用例开始前运行。 |
| - | end_run() | `confrun.py`文件配置,整个用例结束后运行。 |
__示例1__
针对每条测试类/测试用例的fixture使用示例。
```python
# test_fixture.py
import seldom
class TestCase(seldom.TestCase):
@classmethod
def start_class(cls):
print("测试类开始执行")
@classmethod
def end_class(cls):
print("测试类结束执行")
def start(self):
print("一条测试用例开始")
def end(self):
print("一条测试结果")
def test_case_one(self):
...
def test_case_two(self):
...
if __name__ == '__main__':
seldom.main(debug=True)
```
> 警告:不要把用例的操作步骤写到`start_class/end_class`中! 因为它不属于某条用例的一部分,一旦里面的操作步骤运行失败,会影响用例的执行。
__运行结果__
```shell
> python test_fixture.py
...
测试类开始执行
test_case_one (zzz_case.TestCase.test_case_one) ... 一条测试用例开始
一条测试结果
ok
test_case_two (zzz_case.TestCase.test_case_two) ... 一条测试用例开始
一条测试结果
ok
测试类结束执行
...
```
__示例2__
有时候我们需要整个测试`开始前`或`结束后`完成一些工作,可以通过下面的方式配置。
* 目录结构
```
mypro/
├── test_dir/
│ ├── __init__.py
│ ├── test_sample.py
├── confrun.py
└── run.py
```
* `confrun.py` 配置前后置动作
```python
from seldom.logging import log
from seldom.utils import cache
def start_run():
"""
Test the hook function before running
"""
log.info("start_run")
cache.set({"token": "token123"})
def end_run():
"""
Test the hook function after running
"""
log.info("end_run")
cache.clear("token")
```
> 示例中用于添加和清除 cache, 根据实际需求你可以加上任何动作。
* `run.py` 执行用例
```python
import seldom
if __name__ == '__main__':
seldom.main(path="./test_dir")
```
* 运行结果
```shell
> python run.py
...
2024-12-06 17:55:04 | INFO | confrun.py | MainThread | start_run # confrun.py 所有用例前的动作
2024-12-06 17:55:04 | INFO | cache.py | MainThread | 💾 Set cache data: token = token123
2024-12-06 17:55:04 | INFO | runner.py | MainThread | TestLoader: ./test_dir
XTestRunner Running tests...
----------------------------------------------------------------------
2024-12-06 17:55:04 | INFO | cache.py | MainThread | 💾 Get cache data: token = token123
Generating HTML reports...
.12024-12-06 17:55:04 | SUCCESS | runner.py | MainThread | generated html file: file:///D:\github\seldomQA\seldom\reports\2024_12_06_17_55_03_result.html
2024-12-06 17:55:04 | SUCCESS | runner.py | MainThread | generated log file: file:///D:\github\seldomQA\seldom\reports\seldom_log.log
2024-12-06 17:55:04 | INFO | confrun.py | MainThread | end_run # confrun.py 所有用例后的动作
2024-12-06 17:55:04 | INFO | cache.py | MainThread | 💾 Clear cache data: token
```
### 跳过测试
seldom 提供了跳过用例的装饰用于跳过暂时不执行的用例。
__装饰器__
* `seldom.skip()`:无条件地跳过一个测试。
* `seldom.skip_if()`: 如果条件为真,则跳过测试。
* `seldom.skip_unless()`: 跳过一个测试,除非条件为真。
* `seldom.expected_failure()`: 预期测试用例会失败。
* `self.skipTest()`: 根据条件跳过测试。
__使用方法__
```python
# test_skip.py
import seldom
@seldom.skip(reason="跳过类")
class SkipTest(seldom.TestCase):
def test_case(self):
...
class YouTest(seldom.TestCase):
@seldom.skip(reason="跳过用例")
def test_skip_case(self):
...
def test_if_skip(self):
login = False
if login is False:
self.skipTest(reason="登录失败,跳过后续执行")
if __name__ == '__main__':
seldom.main(debug=True)
```
### 重复执行
当然某一段测试需要重复执行,使用`for`循环是常规的操作,seldom提供了`rerun()` 方法可以更优雅的完成这个工作。
```python
import seldom
from seldom import rerun
class TestCase(seldom.TestCase):
@rerun(100)
def test_search_seldom(self):
self.open("https://www.baidu.com")
self.type_enter(id_="kw", text="seldom")
```
通过`@rerun()` 装饰 `test_searchseldom()` 可以执行 100 次,统计结果仍为1条用例,如果想统计为 100 条用例,请使用`@data()`
装饰器。
### 随机测试数据
测试数据是测试用例的重要部分,有时不能把测试数据写死在测试用例中,比如注册新用户,一旦执行过用例那么测试数据就已经存在了,所以每次执行注册新用户的数据不能是一样的,这就需要随机生成一些测试数据。
seldom 提供了随机获取测试数据的方法。
```python
import seldom
from seldom import testdata
class YouTest(seldom.TestCase):
def test_case(self):
"""a simple test case """
word = testdata.get_word()
print(word)
if __name__ == '__main__':
seldom.main()
```
通过`get_word()` 随机获取一个单词,然后对这个单词进行搜索。
**更多的方法**
```python
from seldom.testdata import *
# 随机一个名字
print("名字:", first_name())
print("名字(男):", first_name(gender="male"))
print("名字(女):", first_name(gender="female"))
print("名字(中文男):", first_name(gender="male", language="zh"))
print("名字(中文女):", first_name(gender="female", language="zh"))
# 随机一个姓
print("姓:", last_name())
print("姓(中文):", last_name(language="zh"))
# 随机一个姓名
print("姓名:", username())
print("姓名(中文):", username(language="zh"))
# 随机一个生日
print("生日:", get_birthday())
print("生日字符串:", get_birthday(as_str=True))
print("生日年龄范围:", get_birthday(start_age=20, stop_age=30))
# 日期
print("日期(当前):", get_date())
print("日期(昨天):", get_date(-1))
print("日期(明天):", get_date(1))
print("当月:", get_month())
print("上个月:", get_month(-1))
print("下个月:", get_month(1))
print("今年:", get_year())
print("去年:", get_year(-1))
print("明年:", get_year(1))
# 数字
print("数字(8位):", get_digits(8))
# 邮箱
print("邮箱:", get_email())
# 浮点数
print("浮点数:", get_float())
print("浮点数范围:", get_float(min_size=1.0, max_size=2.0))
# 随机时间
print("当前时间:", get_now_datetime())
print("当前时间(格式化字符串):", get_now_datetime(strftime=True))
print("未来时间:", get_future_datetime())
print("未来时间(格式化字符串):", get_future_datetime(strftime=True))
print("过去时间:", get_past_datetime())
print("过去时间(格式化字符串):", get_past_datetime(strftime=True))
# 随机数据
print("整型:", get_int())
print("整型32位:", get_int32())
print("整型64位:", get_int64())
print("MD5:", get_md5())
print("UUID:", get_uuid())
print("单词:", get_word())
print("单词组(3个):", get_words(3))
print("手机号:", get_phone())
print("手机号(移动):", get_phone(operator="mobile"))
print("手机号(联通):", get_phone(operator="unicom"))
print("手机号(电信):", get_phone(operator="telecom"))
# 在线时间
print("当前时间戳:", online_timestamp())
print("当前日期时间:", online_now_datetime())
```
* 运行结果
```
名字: Hayden
名字(男): Brantley
名字(女): Julia
名字(中文男): 觅儿
名字(中文女): 若星
姓: Lee
姓(中文): 白
姓名: Genesis
姓名(中文): 廉高义
生日: 2000-03-11
生日字符串: 1994-11-12
生日年龄范围: 1996-01-12
日期(当前): 2022-09-17
日期(昨天): 2022-09-16
日期(明天): 2022-09-18
数字(8位): 48285099
邮箱: melanie@yahoo.com
浮点数: 1.5315717275531858e+308
浮点数范围: 1.6682402084146244
当前时间: 2022-09-17 23:33:22.736031
当前时间(格式化字符串): 2022-09-17 23:33:22
未来时间: 2054-05-02 11:33:47.736031
未来时间(格式化字符串): 2070-08-28 16:38:45
过去时间: 2004-09-03 12:56:23.737031
过去时间(格式化字符串): 2006-12-06 07:58:37
整型: 7831034423589443450
整型32位: 1119927937
整型64位: 3509365234787490389
MD5: d0f6c6abbfe1cfeea60ecfdd1ef2f4b9
UUID: 5fd50475-2723-4a36-a769-1d4c9784223a
单词: habitasse
单词组(3个): уж pede. metus.
手机号: 13171039843
手机号(移动): 15165746029
手机号(联通): 16672812525
手机号(电信): 17345142737
当前时间戳 1695137988672
当前日期时间 2023-09-19 23:39:48
```
### 用例的依赖
> 在 seldom 1.8.0 版本实现了该功能。
在编写用例的时候不推荐你编写依赖的用例,但是,有些时候我们并不能完全消除这些依赖。seldom 增加了用例依赖的方法。
**depend**
`depend` 装饰器用来设置依赖的用例。
```python
import seldom
from seldom import depend
class TestDepend(seldom.TestCase):
def test_001(self):
print("test_001")
@depend("test_001")
def test_002(self):
print("test_002")
@depend("test_002")
def test_003(self):
print("test_003")
if __name__ == '__main__':
seldom.main(debug=True)
```
`test_002` 依赖于 `test_001` , `test_003`又依赖于`test_002`。当被依赖的用例,错误、失败、跳过,那么依赖的用例自动跳过。
**if_depend**
`if_depend` 装饰器不会依赖用例的执行状态,可以自己定义是否要跳过依赖的用例。
```python
import seldom
from seldom import if_depend
class TestIfDepend(seldom.TestCase):
Test001 = True
def test_001(self):
TestIfDepend.Test001 = False # 修改Test001为 False
@if_depend("Test001")
def test_002(self):
...
if __name__ == '__main__':
seldom.main(debug=True)
```
1. 首先,定义变量 `Test001`,默认值为`True`。
2. 在`test_001`用例中,可以根据一些条件来选择是否修改`Test001`的值,如果改为`False`, 那么依赖的用例将被跳过。
3. 在`test_002`用例中,通过`if_depend`装饰器来判断`Test001`的值,如果为为`False`, 那么装饰的用例跳过,否则执行。
**@depend 和 @data()**
`@depend()` 装饰器可以和 `@data()` 装饰器混合使用。
```python
import seldom
from seldom import data, depend
class DataDriverTest(seldom.TestCase):
def test_001(self):
self.assertEqual(1, 2)
@data([
("First", "seldom"),
("Second", "selenium"),
("Third", "unittest"),
])
@depend("test_001") # 依赖 test_001 的结果
def test_002(self, name, keyword):
"""
Used tuple test data
:param name: case desc
:param keyword: case data
"""
print(f"{name} - test data: {keyword}")
if __name__ == '__main__':
seldom.main(debug=True)
```
使用要求:
1. 被依赖的用例不能用 @data() 装饰器,否则就是一组用例了,只能指定单个用例。
2. `@depend()` 要放到 `@data()` 下面使用。
### 用例分类标签
> 在 seldom 2.4.0 版本实现了该功能。
**使用方式**
```python
# test_label.py
import seldom
from seldom import label
class MyTest(seldom.TestCase):
@label("base")
def test_label_base(self):
self.assertEqual(1 + 1, 2)
@label("slow")
def test_label_slow(self):
self.assertEqual(1, 2)
def test_no_label(self):
self.assertEqual(2 + 3, 5)
if __name__ == '__main__':
# seldom.main(debug=True, whitelist=["base"]) # whitelist
seldom.main(debug=True, blacklist=["slow"]) # blacklist
```
如果只运行标签为`base`的用例,设置白名单(whitelist)。
```shell
> python test_label.py
test_label_base (btest_label.MyTest) ... ok
test_label_slow (btest_label.MyTest) ... skipped "label whitelist {'base'}"
test_no_label (btest_label.MyTest) ... skipped "label whitelist {'base'}"
----------------------------------------------------------------------
Ran 3 tests in 0.001s
OK (skipped=2)
```
如果只想屏蔽标签为`slow`的用例,设置黑名单(blacklist)。
```shell
> python test_label.py
test_label_base (btest_label.MyTest) ... ok
test_label_slow (btest_label.MyTest) ... skipped "label blacklist {'slow'}"
test_no_label (btest_label.MyTest) ... ok
----------------------------------------------------------------------
Ran 3 tests in 0.001s
OK (skipped=1)
```
### 发送邮件
> 在 seldom 1.2.4 版本实现了该功能。
如果你想将测试完成的报告发送到指定邮箱,那么可以调用发邮件的方法实现。
```python
import seldom
from seldom import SMTP
# ...
if __name__ == '__main__':
report_path = "/you/path/report.html"
seldom.main(report=report_path)
smtp = SMTP(user="send@126.com", password="abc123", host="smtp.126.com", ssl=True)
smtp.sendmail(to="receive@mail.com", subject="Email title", attachments=report_path, delete=False)
```
__SMTP()类__
- `user`: 邮箱用户名。
- `password`: 邮箱密码。
- `host`: 邮箱服务地址。
- `ssl`: `True` 使用 `SMTP_SSL()`,`False` 使用 `SMTP()`,两种方式应对不同的邮箱服务。
__sendmail()方法__
- `subject`: 邮件标题,默认:`Seldom Test Report`。
- `to`: 添加收件人,支持多个收件人: `["aa@mail.com", "bb@mail.com"]`。
- `attachments`: 设置附件,默认发送 HTML 测试报告。
- `delete`: 是否删除报告&日志。(在服务器上运行自动化,每次都会产生一份报告和日志,手动删除比较麻烦。)
> `debug`模式不会生成测试报告, 自动化发邮件不支持`debug` 模式,自然也无法将报告发送到指定邮箱了。
### 发送钉钉
> 在 seldom 2.6.0 版本实现了该功能。
seldom 还提供了发送钉钉的 API。
帮助文档:
https://open.dingtalk.com/document/group/enterprise-created-chatbot
```python
import seldom
from seldom import DingTalk
# ...
if __name__ == '__main__':
seldom.main()
ding = DingTalk(
access_token="690900b5ce6d5d10bb1218b8e64a4e2b55f96a6d116aaf50",
key="xxxx",
app_secret="xxxxx",
at_mobiles=[13700000000, 13800000000],
is_at_all=False,
)
ding.sender()
```
__参数说明:__
- `access_token`: 钉钉机器人的 access_token
- `key`: 如果钉钉机器人安全设置了关键字,则需要传入对应的关键字。
- `app_secret`: 如果钉钉机器人安全设置了签名,则需要传入对应的密钥。
- `at_mobiles`: 发送通知钉钉中要@人的手机号列表,如:[137xxx, 188xxx]。
- `is_at_all`: 是否@所有人,默认为 False, 设为 True 则会@所有人。
### seldom日志
> 在 seldom 2.9.0 版本提供了日志的配置能力。
在项目中你可以使用seldom提供的`log` 打印日志。
* 使用log
```python
from seldom.logging import log
log.trace("this is trace info.")
log.info("this is info.")
log.error("this error info.")
log.debug("this debug info.")
log.success("this success info.")
log.warning("this warning info.")
```
* 运行日志
```shell
2022-04-30 16:31:49 test_log.py | TRACE | this is trace info.
2022-04-30 16:31:49 test_log.py | INFO | this is info.
2022-04-30 16:31:49 test_log.py | ERROR | this error info.
2022-04-30 16:31:49 test_log.py | DEBUG | this debug info.
2022-04-30 16:31:49 test_log.py | SUCCESS | this success info.
2022-04-30 16:31:49 test_log.py | WARNING | this warning info.
```
* 关闭日志颜色
```python
from seldom.logging import log_cfg
from seldom.logging import log
log_cfg.set_level(colorlog=False) # 关闭日志颜色
log.trace("this is trace info.")
# ...
```
* 自定义日志格式
```python
from seldom.logging import log_cfg
from seldom.logging import log
# 定义日志格式
format = "{time:YYYY-MM-DD HH:mm:ss}> {file} | {level} | {message} "
log_cfg.set_level(format=format)
log.trace("this is trace info.")
```
* 日志级别
```python
from seldom.logging import log_cfg
from seldom.logging import log
# 设置日志级别
log_cfg.set_level(level="DEBUG")
log.trace("this is trace info.")
log.error("this error info.")
```
> log level: TRACE < DEBUG < INFO < SUCCESS < WARNING < ERROR
### 缓存 cache
> 在 seldom 2.10.0 版本实现了该功能。
实际测试过程中,往往需要需要通过cache去记录一些数据,从而减少不必要的操作。例如
登录token,很多条用例都会用到登录token,那么就可以借助缓存来暂存登录token,从而减少重复动作。
* cache
```python
from seldom.utils import cache
# 清除指定缓存
cache.clear()
# 获取指定缓存
token = cache.get("token")
print(f"token: {token}")
# 判断为空写入缓存
if token is None:
cache.set({"token": "123"})
# 设置存在的数据(相当于更新)
cache.set({"token": "456"})
# value复杂格式设置存在的数据
cache.set({"user": [{"name": "tom", "age": 11}]})
# 获取所有缓存
all_token = cache.get()
print(f"all: {all_token}")
# 清除指定缓存
cache.clear("token")
```
> 注:seldom 提供的 `cache` 本质上是通过json文件来临时记录数据,没有失效时间。你需要在适当的位置做清除操作。例如,整个用例开始时清除。
* memery_cache
使用内存的实现的cache 装饰器。
```python
import time
import seldom
from seldom.utils import memory_cache
@memory_cache()
def add(x, y):
print("calculating: %s + %s" % (x, y))
time.sleep(2)
c = x + y
return c
class MyTest(seldom.TestCase):
def test_case(self):
"""test cache 1"""
r = add(1, 2)
self.assertEqual(r, 3)
def test_case2(self):
"""test cache 2"""
r = add(1, 2)
self.assertEqual(r, 3)
def test_case3(self):
"""test cache 3"""
r = add(1, 2)
self.assertEqual(r, 3)
if __name__ == '__main__':
seldom.main(debug=True)
```
* disk_cache
使用磁盘实现的cache 装饰器。
```python
import time
import seldom
from seldom.utils import disk_cache
@disk_cache()
def add(x, y):
print("calculating: %s + %s" % (x, y))
time.sleep(2)
c = x + y
return c
class MyTest(seldom.TestCase):
def test_case(self):
"""test cache 1"""
r = add(1, 2)
self.assertEqual(r, 3)
def test_case2(self):
"""test cache 2"""
r = add(1, 2)
self.assertEqual(r, 3)
def test_case3(self):
"""test cache 3"""
r = add(1, 2)
self.assertEqual(r, 3)
if __name__ == '__main__':
dc = disk_cache()
# 清除所有函数缓存
# dc.clear()
# 清除 `add()` 函数缓存
dc.clear("add")
seldom.main(debug=True)
```
================================================
FILE: docs/vpdocs/getting-started/create_project.md
================================================
# 创建项目
seldom已经安装完成,那么现在已经迫不及待的想体验seldom的使用。
### 自动生成项目
seldom 通过`seldom`命令提供了脚手架,可以快速的帮我们创建自动化测试项目。
1. 查看帮助:
```shell
> seldom --help
Usage: seldom [OPTIONS]
seldom CLI.
Options:
--version Show version.
--project-api TEXT Create an API automation test project.
--project-app TEXT Create an App automation test project.
--project-web TEXT Create an Web automation test project.
-cc, --clear-cache BOOLEAN Clear all caches of seldom.
-p, --path TEXT Run test case file path.
-c, --collect / -nc, --no-collect
Collect project test cases. Need the
`--path`.
-l, --level [data|method] Parse the level of use cases. Need the
--path.
-j, --case-json TEXT Test case files. Need the `--path`.
-e, --env TEXT Set the Seldom run environment `Seldom.env`.
-b, --browser [chrome|firefox|ie|edge|safari]
The browser that runs the Web UI automation
tests. Need the `--path`.
-u, --base-url TEXT The base-url that runs the HTTP automation
tests. Need the `--path`.
-d, --debug / -nd, --no-debug Debug mode. Need the `--path`.
-rr, --rerun INTEGER The number of times a use case failed to run
again. Need the `--path`.
-r, --report TEXT Set the test report for output. Need the
`--path`.
-m, --mod TEXT Run tests modules, classes or even
individual test methods from the command
line.
-ll, --log-level [TRACE|DEBUG|INFO|SUCCESS|WARNING|ERROR]
Set the log level.
-h2c, --har2case TEXT HAR file converts an seldom test case.
-s2c, --swagger2case TEXT Swagger file converts an seldom test case.
--api-excel TEXT Run the api test cases in the excel file.
--help Show this message and exit.
```
2. 创建项目:
```shell
> seldom -P mypro
```
目录结构如下:
```shell
mypro/
├── test_dir/
│ ├── __init__.py
│ ├── test_web_sample.py
│ ├── test_api_sample.py
├── test_data/
│ ├── data.json
├── reports/
└── confrun.py
```
* `test_dir/` 测试用例目录。
* `test_data/` 测试数据文件目录。
* `reports/` 测试报告目录。
* `confrun.py` 运行测试用例配置文件。
3. 克隆项目
如果无法使用`seldom`命令,可以通过git克隆相关项目进行学习。
* seldom-web-testing
```shell
> git clone https://github.com/SeldomQA/seldom-web-testing
```
* seldom-api-testing
```shell
> git clone https://github.com/defnngj/seldom-api-testing
```
### 创建测试用例
根据上面的创建的项目,可以在`test_dir`目录下继续创建测试用例:`test_sample.py`。
```py
import seldom
class YouTest(seldom.TestCase):
def test_case(self):
"""a simple test case """
...
if __name__ == '__main__':
seldom.main()
```
根据自己的需求编写`Web UI`、`App UI`或`HTTP接口`自动化测试。
================================================
FILE: docs/vpdocs/getting-started/data_driver.md
================================================
# 数据驱动
数据驱动是测试框架非常重要的功能之一,它可以有效的节约大量重复的测试代码。seldom针对该功能做强大的支持。
### @class_data() 方法
`class_data()` 装饰测试类,测试类下面的任何方法可以共用 `class_data()` 中定义的变量。
* 用法一
```python
import seldom
from seldom import data_class
@data_class([
{"username": "user_1", "password": "abc123"},
{"username": "user_2", "password": "abc456"},
])
class DDTTest(seldom.TestCase):
def test_data_func(self):
""" data driver case """
print("username->", self.username)
print("password->", self.password)
if __name__ == '__main__':
seldom.main(debug=True)
```
* 用法二
```python
import seldom
from seldom import data_class
@data_class(("username", "password"), [
("user_1", "abc123"),
("user_1", "abc456"),
])
class DDTTest(seldom.TestCase):
def test_data_func(self):
""" data driver case """
print("username->", self.username)
print("password->", self.password)
if __name__ == '__main__':
seldom.main(debug=True)
```
### @data()方法
当测试数据量比较少的情况下,可以通过`@data()`管理测试数据。
**参数化测试用例**
```python
import seldom
from seldom import data
class DataDriverTest(seldom.TestCase):
@data([
("First case", "seldom"),
("Second case", "selenium"),
("Third case", "unittest"),
])
def test_tuple_data(self, name, keyword):
"""
Used tuple test data
:param name: case desc
:param keyword: case data
"""
print(f"test data: {keyword}")
@data([
["First case", "seldom"],
["Second case", "selenium"],
["Third case", "unittest"],
])
def test_list_data(self, name, keyword):
"""
Used list test data
"""
print(f"test data: {keyword}")
@data([
{"scene": 'First case', 'keyword': 'seldom'},
{"scene": 'Second case', 'keyword': 'selenium'},
{"scene": 'Third case', 'keyword': 'unittest'},
])
def test_dict_data(self, scene, keyword):
"""
used dict test data
"""
print(f"case desc: {scene}")
print(f"test data: {keyword}")
@data([
[1, 2], [3, 4], [5, 6]
],
cartesian=True)
def test_cartesian_product(self, one, two, three):
"""
cartesian product
"""
print(f"test data: {one}, {two}, {three}")
```
通过`@data()` 装饰器来参数化测试用例。
**动态生成测试数据**
除了使用固定的数据外,也可以动态生成一些测试数据用于自动化测试。
```python
import seldom
from seldom import data
from seldom import testdata
def test_data() -> list:
"""
自动生成测试数据
return [{},{}]
"""
login_data = []
for i in range(5):
login_data.append({
"scene": f"login{i}",
"username": testdata.get_email(),
"password": testdata.get_int(100000, 999999)
})
return login_data
class MyTest(seldom.TestCase):
@data(test_data())
def test_login(self, _, username, password):
"""test login"""
print(f"test username: {username}")
print(f"test password: {password}")
```
### @file_data() 方法
当测试数据量比较大的情况下,可以通过`@file_data()`管理测试数据。
__CSV 文件参数化__
seldom 支持将`csv`文件的参数化。
表格内容如下(data.csv):
| username | password |
|----------|----------|
| admin | admin123 |
| guest | guest123 |
```python
import seldom
from seldom import file_data
class YouTest(seldom.TestCase):
@file_data("data.csv", line=2, end_line=10)
def test_login(self, username, password):
"""a simple test case """
print(username)
print(password)
# ...
```
- file: 指定 csv 文件的路径。
- line: 指定从第几行开始读取,默认第 1 行。
- end_line: 指定读取到第几行的数据,默认None, 最后一行。
**excel 文件参数化**
seldom 支持将`excel`文件的参数化。
```python
import seldom
from seldom import file_data
class YouTest(seldom.TestCase):
@file_data("data.xlsx", sheet="Sheet1", line=2, end_line=10)
def test_login(self, username, password):
"""a simple test case """
print(username)
print(password)
# ...
```
- file : 指定 excel 文件的路径。
- sheet: 指定 excel 的标签页,默认名称为 Sheet1。
- line : 指定从第几行开始读取,默认第 1 行。
- end_line: 指定读取到第几行的数据,默认None, 最后一行。
**JSON 文件参数化**
seldom 支持将`JSON`文件的参数化。
json 文件:
```json
{
"login1": [
[
"admin",
"admin123"
],
[
"guest",
"guest123"
]
],
"login2": [
{
"username": "Tom",
"password": "tom123"
},
{
"username": "Jerry",
"password": "jerry123"
}
]
}
```
> 注:`login1` 和 `login2` 的调用方法一样。 区别是前者更简洁,后者更易读。
```python
import seldom
from seldom import file_data
class YouTest(seldom.TestCase):
@file_data("data.json", key="login1")
def test_login(self, username, password):
"""a simple test case """
print(username)
print(password)
# ...
```
- file : 指定 JSON 文件的路径。
- key: 指定字典的 key,默认不指定解析整个 JSON 文件。
**YAML 文件参数化**
seldom 支持`YAML`文件的参数化。
data.yaml 文件:
```yaml
login1:
- - admin
- admin123
- - guest
- guest123
login2:
- username: Tom
password: tom123
- username: Jerry
password: jerry123
```
同`JSON`用法一样,`YAML`书写更加简洁。
```python
import seldom
from seldom import file_data
class YouTest(seldom.TestCase):
@file_data("data.yaml", key="login1")
def test_login(self, username, password):
"""a simple test case """
print(username)
print(password)
# ...
```
- file : 指定 YAML 文件的路径。
- key: 指定字典的 key,默认不指定解析整个 YAML 文件。
__解释: `@file_data()`是如何查找测试数据文件的?__
```shell
mypro/
├── test_dir/
│ ├── module/
│ │ ├── case/
│ │ │ ├── test_sample.py (使用@file_data)
├── test_data/
│ ├── module_data/
│ │ ├── data.csv (测试数据文件所以位置)
...
```
在 `test_sample.py` 中使用`@file_data("data.csv")`默认只能向上查找两级目录,即到`module`目录下遍历查找`data.csv`
文件。显然这中情况下是无法找到`data.csv` 文件的。
如果用例层级比较深,只需要指定文件目录的`“相对路径”`即可,使用方式:`@file_data("test_data/module_data/data.csv")`
,不要加`./`的前缀。
**支持配置测试环境**
在自动化测试过程中,我们往往需要一套代码在不同的环境下运行,seldom支持根据环境使用不同的数据文件。
* 数据文件目录结构(一)
```shell
.
└── test_data
├── develop
│ └── test_data.json
├── product
│ └── test_data.json
└── test
└── test_data.json
```
* 数据文件目录结构(二)
```shell
.
├── develop
│ └── test_data
│ └── test_data.json
├── product
│ └── test_data
│ └── test_data.json
└── test
└── test_data
└── test_data.json
```
* 配置测试环境
```python
import seldom
from seldom import file_data
class MyTest(seldom.TestCase):
# 数据文件目录结构(一)
@file_data("test_data.json")
def test_case(self, req, resp):
f"""a simple test case"""
...
# 数据文件目录结构(二)
@file_data("test_data/test_data.json")
def test_case(self, req, resp):
f"""a simple test case"""
...
if __name__ == '__main__':
# test/develop/product 设置当前环境
seldom.main(debug=True, env="product")
```
`env` 默认为`None`,当设置了`环境变量`,`@file_data()`会带上`环境变量`的目录名,例如:
* `test_data.json` 查找的文件为 `product/test_data.json`
* `test_data/test_data.json` 查找的文件为 `product/test_data/test_data.json`
> * `env` 可以随意命名,但最好遵循一定的规范,例如`test/develop/product`用于区分不同的环境。
> * 我们还可以利用`env`环境变量实现更多的配置,下面的示例。
```python
import seldom
from seldom import Seldom
class MyTest(seldom.TestCase):
def test_env(self):
if Seldom.env == "product":
username = "admin"
elif Seldom.env == "develop":
username = "guest"
else:
username = "tom"
...
if __name__ == '__main__':
seldom.main(debug=True, env="product")
```
### @api_data()方法
越来越多的公司落地 数据工厂,通过造数平台/数据工厂 来创建管理测试数据;`@api_data()` 装饰器支持通过URL获取驱动数据。
* 接口数据
`http://127.0.0.1:8080/v1/public/data_service/get_case_data?data_id=1`
```json
{
"success": true,
"error": {
"code": "",
"message": ""
},
"result": [
{
"scene": "测试1",
"email": "li123@126.com",
"password": "abc123"
},
{
"scene": "测试2",
"email": "li456@126.com",
"password": "abc456"
}
]
}
```
* 调用接口数据
```python
import seldom
from seldom import api_data
class TestApi(seldom.TestCase):
@api_data(url="http://127.0.0.1:8080/v1/public/data_service/get_case_data",
params={"data_id": 1},
headers={"X-Account-Email": "li.li@gmail.com"},
ret="result")
def test_case(self, scene, email, password):
"""
test get request
"""
print("name:", scene)
print("email:", email)
print("password:", password)
if __name__ == '__main__':
seldom.main(debug=True)
```
__`api_data()`参数说明__
* url: 返回数据的接口url地址;默认仅支持`GET` 接口。
* params: 请求参数。
* header: 请求头。
* ret: 提取接口返回的数据,默认仅支持 list 类型。
### 使用函数构造数据
如果数据驱动使用的数据比较简单其有规律,可以通过自定义函数生成,并且把函数传给 `@data()` 装饰器即可。
```python
import seldom
from seldom import data
def register():
"""生成注册账号信息"""
users = []
for i in range(10):
users.append({
"username": f"user{i}",
"password": f"abc123{i}",
"password2": f"abc123{i}"}
)
return users
class DDTTest(seldom.TestCase):
@data(register())
def test_data_func(self, username, password, password2):
""" data driver case """
print("username->", username)
print("password->", password)
print("password2->", password2)
if __name__ == '__main__':
seldom.main(debug=True)
```
### 支持第三方 ddt 库
seldom 仍然允许你使用第三方参数化库,例如:[ddt](https://github.com/datadriventests/ddt)。
安装:
```shell
> pip install ddt
```
创建测试文件`test_data.json`:
```json
{
"test_data_1": {
"word": "seldom"
},
"test_data_2": {
"word": "unittest"
},
"test_data_3": {
"word": "selenium"
}
}
```
在 seldom 使用`ddt`。
```python
import seldom
from ddt import ddt, file_data
@ddt
class YouTest(seldom.TestCase):
@file_data("test_data.json")
def test_case(self, word):
"""a simple test case """
self.open("https://www.baidu.com")
self.type(id_="kw", text=word)
self.click(css="#su")
self.assertTitle(word + "_百度搜索")
if __name__ == '__main__':
seldom.main()
```
更多的用法请查看 ddt 文档:https://ddt.readthedocs.io/en/latest/example.html
================================================
FILE: docs/vpdocs/getting-started/dependent_func.md
================================================
# 方法的依赖
> 在 seldom 3.4.0 版本实现了该功能。
在复杂的测试场景中,常常会存在用例依赖,以一个接口自动化平台为例,依赖关系:
`创建用例` --> `创建模块` --> `创建项目` --> `登录`。
__用例依赖的问题__
* 用例的依赖对于的执行顺序有严格的要求,比如让被依赖的方法先执行。
* 一旦使用用例依赖,依赖的用例就无法单独执行了,按照用例的设计原则,每条用例都应该独立执行。
__正确的做法__
`我们应该将依赖的操作封装成方法调用`。如果能通过装饰器实现调用,那就很有趣了。
[aomaker](https://github.com/ae86sen/aomaker) 提供了这种装饰器的实现,seldom 进行了复刻,只是的定位和用法用有所不同。
### 类内部方法调用
我们可以在测试类下面,创建普通的方法。然后通过`@dependent_func()`装饰器调用他。
```python
import seldom
from seldom.testdata import get_md5
from seldom.utils import cache, dependent_func
class DependentTest(seldom.TestCase):
@staticmethod
def user_login(username, password):
"""
模拟用户登录,获取登录token
"""
return get_md5(username+password)
@dependent_func(user_login, username="tom", password="t123")
def test_case(self,):
"""
sample test case
"""
token = cache.get("user_login")
print("token", token)
if __name__ == '__main__':
seldom.main(debug=True)
cache.clear()
```
__说明__
这个例子涉及到不少知识点。
1. `test_case()` 用例依赖 `user_login()` 方法,通过 `@dependent_func()` 装饰器调用 `user_login` 方法。
2. `user_login()` 方法运行的时候需要参数(username、password),可以直接在 `@dependent_func()` 装饰器中设置参数:`username="tom"`、 `password="t123"`。
3. `user_login()` 方法运行运行完会生成 token, 保存于 cache中,通过 ` cache.get()` 可以获取到token, 默认通过方法名`user_login` 作为key获取。
4. 为了简化代码,生成token 是通过 `get_md5()` 根据传入的参数生成的一个 md5 值。
5. `cache.clear()` 用于清空缓存, 再次调用 `user_login()` 方法直接不执行,应为cache已经有上次的执行结果了。
__执行日志__
```shell
python zzz_demo.py
...
test_case (zzz_demo.DependentTest.test_case)
sample test case ... 2023-11-15 23:26:36 | INFO | dependence.py | 🔗 depends on , execute.
2023-11-15 23:26:36 | INFO | cache.py | 💾 Set cache data: user_login = 35e0ff9c4cba89998dda8255d0eb5408
2023-11-15 23:26:36 | INFO | cache.py | 💾 Get cache data: user_login = 35e0ff9c4cba89998dda8255d0eb5408
token 35e0ff9c4cba89998dda8255d0eb5408
ok
----------------------------------------------------------------------
Ran 1 test in 0.005s
OK
2023-11-15 23:26:36 | SUCCESS | runner.py | A run the test in debug mode without generating HTML report!
2023-11-15 23:26:36 | INFO | cache.py | 💾 Clear all cache data
```
### 外部类方法依赖
* 创建依赖方法
```python
# common.py
from seldom.testdata import get_md5
class Login:
@staticmethod
def account_login(username, password):
"""
模拟用户&密码登录,获取登录token
"""
return get_md5(username+password)
login=Login()
```
* 用例引用依赖方法
```python
import seldom
from seldom.utils import cache, dependent_func
from common import Login # 方式1:引用依赖类
# from common import login # 方式2:引用初始化好的类对象
class DependentTest(seldom.TestCase):
@dependent_func(Login().account_login, key_name="token1", username="tom", password="t123")
# @dependent_func(login.account_login, key_name="token1", username="tom", password="t123")
def test_case(self):
"""
Used tuple test data
"""
token = cache.get("token1")
print("token", token)
if __name__ == '__main__':
seldom.main(debug=True)
```
__说明__
1. `Common` 类的`account_login()`方法可以不设置为静态方法,导入时需要类需要加括号:`Common().user_login`。 或者先初始化类对象`login=Login()` 再调用。
2. `key_name` 指定缓存的 `key`,如果指定为`token1`, 从缓存读取也使用这个`cache.get("token1")`。
### 多重方法依赖
复杂的场景当然是需要多重依赖的。
1. 被依赖的方法可以进一步使用 `dependent_func()`装饰器进行多重复依赖。
`查询模块` --> `查询项目` --> `登录`
```python
# common.py
from seldom.testdata import get_md5, get_int
from seldom.utils import cache, dependent_func
class Login:
@staticmethod
def account_login(username, password):
"""
模拟用户&密码登录,获取登录token
"""
return get_md5(username+password)
class DepFunc:
@staticmethod
@dependent_func(Login.account_login, key_name="token", username="jack", password="456")
def get_project_id():
token = cache.get("token")
print(f"使用token:{token} 查询项目, 返回项目ID")
return get_int(1, 1000)
@staticmethod
@dependent_func(get_project_id, key_name="pid")
def get_module_id():
pid = cache.get("pid")
print(f"使用项目ID:{pid} 查询模块, 返回模块ID")
return get_int(1, 1000)
```
在用例中直接调用 `DepFunc.get_module_id` 方法即可。
```python
import seldom
from seldom.utils import cache, dependent_func
from common import DepFunc
class DependentTest(seldom.TestCase):
@dependent_func(DepFunc.get_module_id, key_name="mid")
def test_case(self):
"""
sample test case
"""
mid = cache.get("mid")
print(f"模块ID: {mid}")
if __name__ == '__main__':
seldom.main(debug=True)
cache.clear()
```
2. 测试用例也可以同时使用多个 `@dependent_func()` 装饰器依赖多个方法,顺序由上到下执行,这种方式主要用于被依赖的方法之间没有依赖关系。
```python
# common.py
from seldom.testdata import get_int, username
class DataFunc:
@staticmethod
def get_name():
return username(language="zh")
@staticmethod
def get_age():
return get_int(1, 99)
```
在用例中使用多个`@dependent_func()`依赖装饰器。
```python
import seldom
from seldom.utils import cache, dependent_func
from common import DataFunc
class DependentTest(seldom.TestCase):
@dependent_func(DataFunc.get_name, key_name="name")
@dependent_func(DataFunc.get_age, key_name="age")
def test_case(self):
"""
sample test case
"""
name = cache.get("name")
age = cache.get("age")
print(f"名字: {name}, 年龄: {age}")
if __name__ == '__main__':
seldom.main(debug=True)
cache.clear()
```
### 参数化使用
参数化 `@data()`、 `@file_data()` 是seldom最重要的功能之一,能否和 `@dependent_func()` 一起使用? 当然可以。
```python
import seldom
from seldom import data
from seldom.testdata import get_md5
from seldom.utils import cache, dependent_func
class DependentTest(seldom.TestCase):
@staticmethod
def user_login(username, password):
"""
模拟用户登录,获取登录token
"""
return get_md5(username+password)
@data([
("case1", "foo"),
("case2", "bar"),
])
@dependent_func(user_login, username="tom", password="t123")
def test_case(self, _, keyword):
"""
Used tuple test data
"""
token = cache.get("user_login")
print(keyword, "token", token)
if __name__ == '__main__':
seldom.main(debug=True)
cache.clear()
```
__说明__
1. `@data()` 装饰器必须写在 `@dependent_func()` 的上面。
2. 运行两条用例,`user_login()` 被执行过一次后,第二次则不需要重复执行,直接返回结果。
================================================
FILE: docs/vpdocs/getting-started/installation.md
================================================
# Installation
seldom的安装非常简单。
* 快速安装
目前已经上传 pypi.org ,可以使用pip命令安装。
```shell
> pip install seldom
```
* 体验最新代码
如果你想随时体验最新的代码,可以使用下面的命令。
```shell
> pip install -U git+https://github.com/defnngj/seldom.git@master
```
* 安装依赖
随着seldom 加入更多的功能,seldom不得不依赖其他的开源库。你可以在 requirements.txt 文件里面看到这些依赖。
```shell
Appium-Python-Client>=4.1.0
XTestRunner>=1.7.2
loguru>=0.7.0
openpyxl>=3.0.3
pyyaml>=6.0
jsonschema>=4.10.0
jmespath>=0.10.0
pymysql>=1.0.0
genson==1.2.2
click~=8.1.3
python-dateutil==2.8.2
```
先通过 `pip` 命令安装这些依赖库,可以加快seldom的安装。
```shell
> pip install -r requirements.txt
```
* 检查安装
最后,我们可以通过`pip show seldom`命令检查安装。
```shell
> pip show seldom
Name: seldom
Version: 3.x.x
Summary: Seldom automation testing framework based on unittest.
Home-page: https://seldomqa.github.io
Author: bugmaster
Author-email: fnngj@126.com
License: Apache-2.0
Location: C:\Python311\Lib\site-packages
Requires: Appium-Python-Client, click, genson, jmespath, jsonschema, loguru, openpyxl, pymysql, python-dateutil, pyyaml, requests, websocket-client, XTestRunner
Required-by:
```
================================================
FILE: docs/vpdocs/getting-started/quick_start.md
================================================
# 快速开始
### 基本规范
`seldom`继承`unittest`单元测试框架,所以他的编写规范与[unittest](https://docs.python.org/3/library/unittest.html)基本保持一致。
```shell
# test_sample.py
import seldom
class YouTest(seldom.TestCase):
def test_case(self):
"""a simple test case """
self.assertEqual(1+1, 2)
if __name__ == '__main__':
seldom.main()
```
基本规范:
1. 创建测试类`YouTest`并继承`seldom.TestCase`类。
2. 创建测试方法`test_case`, 必须以`test`开头。
3. `seldom.main()`是框架运行的入口方法,接下来详细介绍。
### `main()` 方法
`main()`方法是seldom运行测试的入口, 它提供了一些最基本也是最重要的配置。
```python
import seldom
# ...
if __name__ == '__main__':
seldom.main(path="./",
case="test_file.MyClassTest.test_case",
browser="chrome",
base_url=None,
debug=False,
timeout=10,
app_server=None,
app_info=None,
report=None,
title="百度测试用例",
tester="虫师",
description="测试环境:chrome",
rerun=0,
language="en",
whitelist=[],
blacklist=[],
open=True,
extensions=None,
failfast=False,
env="test",
benchmark=False
)
```
__参数说明__
* `path` : 指定测试目录或文件, 与`case`参数互斥。`seldom > 3.7.0 支持 list 传多个目录或文件`。
* `case` : 指定测试用例, 与`path`参数互斥。
* `browser` : 指定浏览器("chrome"、"firefox" 等), Web测试。
* `base_url` : 设置全局的基本URL, HTTP测试。
* `app_info` : app 启动信息,参考`desired_capabilities`配置, app测试。
* `app_server` : appium server 启动地址(默认 http://127.0.0.1:4723), app测试。
* `report` : 自定义测试报告的名称,默认格式为`2020_04_04_11_55_20_result.html`。
* `title` : 指定测试报告标题。
* `tester` : 指定测试人员, 默认`Anonymous`。
* `description` : 指定测试报告描述。
* `debug` : debug模式,设置为True不生成测试HTML测试,默认为`False`。
* `rerun` : 设置失败重新运行次数,默认为 `0`。
* `language` : 设置HTML报告中英文,默认`en`, 中文`zh-CN`。
* `timeout` : 设置超时时间,默认`10`秒。
* `whitelist` : 用例标签(label)设置白名单。
* `blacklist` : 用例标签(label)设置黑名单。
* `open` : 是否使用浏览器自动打开测试报告,默认`True`。
* `extensions`: 加载扩展,appium使用。
* `failfast`: 当执行到失败的用例时,停止执行,仅在 `debug=True`时有效。
* `env`: 设置运行环境变量。
* `benchmark`: 是否进行基准测试。
* `device`: 设置移动设备ID(例如:`MDX0220413010000`)
### `confrun.py` 配置文件
> seldom 3.1.0 提供过了 `confrun.py` 用于配置运行环境。 配置函数与 `seldom.main()` 的参数一致。
在这个文件中可以定义运行相关的钩子函数。
```py
"""
seldom confrun.py hooks function
"""
from seldom.appium_lab.android import UiAutomator2Options
def start_run():
"""
Test the hook function before running
"""
...
def end_run():
"""
Test the hook function after running
"""
...
def browser():
"""
Web UI test:
browser: gc(google chrome)/ff(firefox)/edge/ie/safari
"""
return "gc"
def base_url():
"""
http test
api base url
"""
return "http://httpbin.org"
def app_info():
"""
app UI test
appium app config
"""
capabilities = {
"automationName": "UiAutomator2",
"platformName": "Android",
"appPackage": "com.meizu.flyme.flymebbs",
"appActivity": "com.meizu.myplus.ui.splash.SplashActivity",
"noReset": True,
}
options = UiAutomator2Options().load_capabilities(capabilities)
return options
def app_server():
"""
app UI test
appium server/desktop address
"""
return "http://127.0.0.1:4723"
def debug():
"""
debug mod
"""
return False
def rerun():
"""
error/failure rerun times
"""
return 0
def report():
"""
setting report path
Used:
return "d://mypro/result.html"
return "d://mypro/result.xml"
"""
return None
def timeout():
"""
setting timeout
"""
return 10
def title():
"""
setting report title
"""
return "seldom test report"
def tester():
"""
setting report tester
"""
return "bugmaster"
def description():
"""
setting report description
"""
return ["windows", "jenkins"]
def language():
"""
setting report language
return "en"
return "zh-CN"
"""
return "en"
def whitelist():
"""test label white list"""
return []
def blacklist():
"""test label black list"""
return []
def mock_url():
"""
Replace the fixed url with the mock url
:return:
"""
config = {
"http://httpbin.org/get": "http://127.0.0.1:8000/api/data",
}
return config
def failfast():
"""Use case exe failed to stop, only support debug=True"""
return False
```
以上配置根据需求自动化项目类型配置,相互可能冲突的钩子函数:
* Web UI测试: `browser()`
* http 接口测试: `base_url()`
* app UI测试: `app_info()`, `app_server()`
参数表格:
| seldom.main() (参数) | confrun.py (函数) | 类型 | 说明 |
|--------------------|------------------|------|------------------------------------------------------|
| path | - | 通用 | 指定测试目录或文件, 与`case`参数互斥。 |
| case | - | 通用 | 指定测试用例, 与`path`参数互斥。 |
| browser | browser() | Web | 指定web测试运行的浏览器。 |
| base_url | base_url() | HTTP | 指定HTTP接口测试的基本URL。 |
| - | mock_url() | HTTP | 配置HTTP接口 mock URL。 |
| - | proxies() | HTTP | 配置HTTP接口proxies代理。 |
| app_info | app_info() | App | app 启动信息,参考appium `desired_capabilities`配置, app测试。 |
| app_server | app_server() | App | appium server 启动地址(默认 http://127.0.0.1:4723), app测试。 |
| report | report() | 通用 | 自定义测试报告的名称,例如`result.html/result.xml`。 |
| title | title() | 通用 | 指定HTML报告标题。 |
| tester | tester() | 通用 | 指定HTML报告测试人员。 |
| description | description() | 通用 | 指定HTML报告描述。 |
| language | language() | 通用 | 设置HTML报告中英文,默认`en`, 中文`zh-CN`。 |
| debug | debug() | 通用 | debug模式,设置为True不生成测试HTML测试,默认为`False`。 |
| rerun | rerun() | 通用 | 设置失败重新运行次数。 |
| timeout | timeout() | 通用 | 设置自动化全局超时时间,默认`10`秒。作用于元素定位、断言等。 |
| whitelist | whitelist() | 通用 | 用例标签(label)设置白名单。 |
| blacklist | blacklist() | 通用 | 用例标签(label)设置黑名单。 |
| open | - | 通用 | 是否使用浏览器自动打开测试报告,默认`True`。 |
| extensions | - | App | 加载扩展,appium使用。 |
| failfast | - | 通用 | 当执行到失败的用例时,停止执行,仅在 `debug=True`时有效。 |
| env | - | 通用 | 设置运行环境变量。 |
| benchmark | - | 通用 | 是否进行基准测试。 |
### 运行测试
seldom 的运行有三种方式:
* `main()` 方法:在`.py` 文件中使用`seldom.main()` 方法。
* `seldom` 命令:通过`sedom` 命令指定要运行的目录&文件&类&方法。
* ~~`pycharm`右键执行:这种方式无法读取到配置,有严重缺陷。~~
> 强烈建议使用前两种!!
__1. `main()`方法运行测试__
* 目录结构
```
mypro/
├── test_dir/
│ ├── __init__.py
│ ├── test_sample.py
└── run.py # 运行配置文件
```
创建 `test_sample.py` 文件,在测试文件中使用`main()`方法,如下:
```py
# test_sample.py
import seldom
from seldom import data
class YouTest(seldom.TestCase):
def test_case(self):
"""a simple test case """
self.assertEqual(1 + 1, 2)
@data([
("case1", "seldom"),
("case2", "XTestRunner"),
])
def test_ddt(self, name, search):
""" ddt case """
print(f"name: {name}, search_key: {search}")
if __name__ == '__main__':
# 运行当前文件中的用例
seldom.main() # 默认运行当前文件中所有用例
seldom.main(case="test_sample") # 指定当前文件
seldom.main(case="test_sample.YouTest") # 指定测试类
seldom.main(case="test_sample.YouTest.test_case") # 指定测试用例
# 使用参数化的用例
seldom.main(case="test_sample.YouTest.test_ddt") # 错误用法
seldom.main(case="test_sample.YouTest.test_ddt_0") # 正确用法,0表示第一条数据用例
```
创建 `run.py` 文件,用于全局的指定要运行的用例。
```python
import seldom
if __name__ == '__main__':
# 指定运行其他目录&文件
seldom.main(path="./") # 指定当前文件所在目录下面的用例。
seldom.main(path="./test_dir/") # 指定当前目录下面的test_dir/ 目录下面的用例。
seldom.main(path="./test_dir/test_sample.py") # 指定测试文件中的用例。
seldom.main(path="D:/seldom_sample/test_dir/test_sample.py") # 指定文件的绝对路径。
```
`seldom.main()` 提供哪些参数,请参考前面的文档。
* 运行测试文件
```shell
> cd mypro/ # 进入项目根目录
> python ./test_dir/test_sample.py # 运行指定测试文件
> python run.py # 运行run.py文件
```
__2. seldom命令执行__
* 目录结构
```
mypro/
├── test_dir/
│ ├── __init__.py
│ ├── test_sample.py
└── confrun.py # 运行配置文件
```
`seldom -p`命令指定目录和文件。
`seldom -m`命令可以提供更细粒度的运行。
```shell
> cd mypro/ # 进入项目根目录
> seldom -p test_dir # 运行目录
> seldom -p test_dir/test_sample.py # 运行文件
> seldom -m test_dir.test_sample # 运行文件
> seldom -m test_dir.test_sample.YouTest # 运行 SampleTest 测试类
> seldom -m test_dir.test_sample.YouTest.test_case # 运行 test_case 测试方法
```
运行相关的配置,可以在`confrun.py` 文件中配置。
__3. 在pyCharm中运行测试__
> 强烈不建议这种方式,除非你的测试用例没有任何依赖。
步骤一:配置测试用例通过 unittest 运行。

步骤二:在文件中选择测试类或用例执行。

> 警告:运行用例打开的浏览器,需要手动关闭, seldom不做用例关闭操作。
### 失败重跑
Seldom支持`错误`&`失败`重跑。
```python
# test_sample.py
import seldom
class YouTest(seldom.TestCase):
def test_error(self):
"""error case"""
self.assertEqual(a, 2)
def test_fail(self):
"""fail case """
self.assertEqual(1 + 1, 3)
if __name__ == '__main__':
seldom.main(rerun=3)
```
参数说明:
* rerun: 指定重跑的次数,默认为 `0`。
运行日志:
```shell
> python test_sample.py
__ __
________ / /___/ /___ ____ ____
/ ___/ _ \/ / __ / __ \/ __ ` ___/
(__ ) __/ / /_/ / /_/ / / / / / /
/____/\___/_/\__,_/\____/_/ /_/ /_/ v3.x.x
-----------------------------------------
@itest.info
XTestRunner Running tests...
----------------------------------------------------------------------
ERetesting... test_error (test_sample.YouTest)..1
ERetesting... test_error (test_sample.YouTest)..2
ERetesting... test_error (test_sample.YouTest)..3
EFRetesting... test_fail (test_sample.YouTest)..1
FRetesting... test_fail (test_sample.YouTest)..2
FRetesting... test_fail (test_sample.YouTest)..3
Generating HTML reports...
F2022-07-12 00:22:52 log.py | SUCCESS | generated html file: file:///D:\github\seldom\reports\2022_07_12_00_22_51_result.html
2022-07-12 00:22:52 log.py | SUCCESS | generated log file: file:///D:\github\seldom\reports\seldom_log.log
```
### 测试报告
seldom 默认生成HTML测试报告,在运行测试文件下自动创建`reports`目录。
* 运行测试用例前
```shell
mypro/
└── test_sample.py
```
* 运行测试用例后
```shell
mypro/
├── reports/
│ ├── 2020_01_01_11_20_33_result.html
│ ├── seldom_log.log
└── test_sample.py
```
通过浏览器打开 `2020_01_01_11_20_33_result.html` 测试报告,查看测试结果。

__debug模式__
如果不想每次运行都生成HTML报告,可以打开`debug`模式。
```py
import seldom
seldom.main(debug=True)
```
__定义测试报告__
```py
import seldom
seldom.main(report="./report.html",
title="百度测试用例",
tester="虫师",
description="测试环境:windows 10/ chrome")
```
* report: 配置报告名称和路径。
* title: 自定义报告的标题。
* tester: 指定自动化测试工程师名字。
* description: 添加报告信息,支持列表, 例如:["OS: windows","Browser: chrome"]。
__XML测试报告__
如果需要生成XML格式的报告,只需要修改报告的后缀名为`.xml`即可。
```py
import seldom
seldom.main(report="report.xml")
```
### 多线程运行
多线程无疑可以缩短用例的运行时间,一般由两种方式实现。
1. 设置线程数,交由框架去分配用例,或按照测试用例、测试类、测试模块分配给线程执行。
* 优点:简单,例如 pytest-xdist ,只需要指定 `线程数` 即可。
* 缺点:无法控制用例的拆分粒度,如果在设计用例时,不同的用例有依赖,刚好被分到的不同的线程,那么必定导致用例失败。
2. 自己分好线程,分别调用框架执行。
* 优点:手动划分线程,可以按照目录、文件、甚至测试类或方法 拆分线程。
* 缺点:首先会比较麻烦,而且多个线程的执行结果无法很好的合并到一起。
seldom 推荐第二种方法,把线程的划分方式交给用户,无疑是更灵活的方法。至于报告的合并统计就每有什么好办法了。
* 用例维度使用多线程。
```python
import seldom
from seldom.extend_lib import threads
class MyTest(seldom.TestCase):
def test_baidu(self):
self.open("https://www.baidu.com")
self.sleep(3)
def test_bing(self):
self.open("https://www.bing.com")
self.sleep(4)
@threads(2) # !!!核心!!!! 设置线程数
def run_case(case: str, browser: str):
"""
根据传入的case执行用例
"""
seldom.main(case=case, browser=browser, debug=True)
if __name__ == "__main__":
# 将两条用例拆分,分别用不同的浏览器执行
cases = {
"test_thread_case.MyTest.test_baidu": "chrome",
"test_thread_case.MyTest.test_bing": "edge"
}
for key, value in cases.items():
run_case(key, value)
```
* 目录或文件维度使用多线程。
```python
import seldom
from seldom.extend_lib import threads
@threads(3) # !!!核心!!!! 设置线程数
def run_case(path: str):
"""
根据传入的path执行用例
"""
seldom.main(path=path, debug=True)
if __name__ == "__main__":
# 定义3个测试文件,分别丢给3个线程执行。
paths = [
"./test_dir/more_case/test_case1.py",
"./test_dir/more_case/test_case2.py",
"./test_dir/more_case/test_case3.py"
]
for p in paths:
run_case(p)
```
================================================
FILE: docs/vpdocs/getting-started/seldom_cli.md
================================================
# seldom CLI
`seldom 2.10.7` 对命令行工具做了增强,可以使用命令行的方式运行用例。
## seldom 帮助
* `seldom --help` 查看帮助使用
```shell
> seldom --help
Usage: seldom [OPTIONS]
seldom CLI.
╭─ Options ────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ --version -v Show version. │
│ --project-api -api TEXT Create a project of API type. [default: None] │
│ --project-app -app TEXT Create a project of App type [default: None] │
│ --project-web -web TEXT Create a project of Web type [default: None] │
│ --clear-cache -cc Clear all caches of seldom. │
│ --log-level -ll TEXT Set the log level [TRACE |DEBUG | INFO | SUCCESS | WARNING | ERROR]. │
│ [default: None] │
│ --mod -m TEXT Run tests modules, classes or even individual test methods from the command │
│ line. │
│ [default: None] │
│ --path -p TEXT Run test case file path. [default: None] │
│ --env -e TEXT Set the Seldom run environment `Seldom.env`. [default: None] │
│ --browser -b TEXT The browser that runs the Web UI automation tests [chrome | edge | firefox | │
│ chromium]. Need the --path. │
│ [default: None] │
│ --base-url -u TEXT The base-url that runs the HTTP automation tests. Need the --path. │
│ [default: None] │
│ --debug -d Debug mode. Need the --path/--mod. │
│ --rerun -rr INTEGER The number of times a use case failed to run again. Need the --path. │
│ [default: 0] │
│ --report -r TEXT Set the test report for output. Need the --path. [default: None] │
│ --collect -c Collect project test cases. Need the --path. │
│ --level -l TEXT Parse the level of use cases [data | case]. Need the --path. [default: data] │
│ --case-json -j TEXT Test case files. Need the --path. [default: None] │
│ --har2case -h2c TEXT HAR file converts an seldom test case. [default: None] │
│ --swagger2case -s2c TEXT Swagger file converts an seldom test case. [default: None] │
│ --api-excel TEXT Run the api test cases in the excel file. [default: None] │
│ --install-completion Install completion for the current shell. │
│ --show-completion Show completion for the current shell, to copy it or customize the │
│ installation. │
│ --help Show this message and exit. │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
```
如果无法使用`seldom` 命令。
1. 请确保你已经安装了seldom
```shell
> pip install seldom
```
2. 如果仍然无法使用`seldom`命令,请用`where`检查安装位置。
```shell
> where seldom
C:\Python311\Scripts\seldom.exe
```
## seldom 使用
### 创建项目
- `-api/-app/-web/`
```shell
> seldom -api myapi # API automation test project.
> seldom -app myapp # or App automation test project.
> seldom -web myweb # or Web automation test project.
```
注:`har` 是fiddler 抓包工具导出的一种格式,即 `HTTPArchive`。
### 运行测试目录&文件
* `-p\--path`
```shell
> seldom -p ./test_dir/ # 指定运行目录
> seldom -p ./test_dir/test_first_demo.py # 指定运行文件
```
不支持斜杠`\`表示路径
### 运行文件&类&方法
* `-m\--mod`
```shell
> seldom -m test_dir # 目录名
> seldom -m test_dir.test_sample # 目录名.文件名,不要.py后缀
> seldom -m test_dir.test_sample.SampleTest # 目录名.文件名.类名
> seldom -m test_dir.test_sample.SampleTest.test_case # 目录名.文件名.类名.方法名
```
### 调试模式
* ` -d, --debug`
```shell
> seldom -p test_sample.py -d # 开启debug模式(默认不指定-d关闭)
```
### 运行浏览器
* `-b/--browser`
```shell
> seldom -p test_sample.py -b firefox # firefox浏览器
```
> 支持`[chrome|chrimium|firefox|edge]` 浏览器。
### 运行URL
* `-u/--base-url`
```shell
> seldom -p test_http_demo.py -u http://httpbin.org # base-url
```
### 测试报告
* `-r/--report`
```shell
> seldom -p test_first_demo.py -r result.html # HTML报告
> seldom -p test_first_demo.py -r result.xml # XML报告
```
### 失败/错误重跑次数
* `-rr/--rerun`
```shell
> seldom -p test_first_demo.py -rr 2 # rerun重跑次数
```
### 数据驱动运行环境
* `-e/--env`
```shell
> seldom -p test_ddt_demo.py -e production # 运行环境
```
> 注:参考`数据驱动` 一章 `Seldom.env` 的用法。
### 收集测试用例
```shell
> seldom -p test_dir -c -l method -j case.json
Collect use cases for the test_dir directory.
add env Path: .
__ __
________ / /___/ /___ ____ ____
/ ___/ _ \/ / __ / __ \/ __ ` ___/
(__ ) __/ / /_/ / /_/ / / / / / /
/____/\___/_/\__,_/\____/_/ /_/ /_/ v{x}.{x}.{x}
-----------------------------------------
@itest.info
save them to D:\github\seldom\demo\case.json
```
* 说明:
- `-p/--path`: 指定收集用例的目录:`test_dir`。
- `-c, --collect`: 指定收集用例, 默认`False`。
- `-l/--level`: 指定收集用例级别: `data/method`。
- `-j/--case-json`: 收集用例保存文件: `case.json`。
### 运行收集测试用例
```shell
> seldom -p test_dir -j case.json -r result.html
```
* 说明:
- `-p/--path`: 指定运行用例的根目录:`test_dir`。
- `-j/--case-json`: 运行收集用例文件: `case.json`。
- `-r/--report`: 运行收集用例生成报告: `result.html`。
### 清除所有缓存
```shell
> seldom --clear-cache
```
* 说明:默认清空seldom所有缓存,即`cache.clear()`
### har转接口测试用例
* `-h2c/--har2case`
```shell
> seldom -h2c demo.har
2022-09-03 11:29:29 core.py | INFO | demo.py
2022-09-03 11:29:29 core.py | INFO | Start to generate testcase.
2022-09-03 11:29:29 core.py | INFO | created file: D:\github\seldom\seldom\har2case\demo.py
```
### swagger转接口测试用例
* `-s2c/--swagger2case`
```shell
> seldom -s2c swagger.json
2025-07-08 23:24:04 | INFO | core.py | MainThread | Start to generate testcase.
2025-07-08 23:24:04 | INFO | core.py | MainThread | created file: D:\github\seldomQA\seldom\seldom\swagger2case\swagger.py
```
### 执行 API(excel文件)测试用例
```shell
> seldom --api-excel api_case.xlsx
```
* 说明:简单的HTTP接口测试可以使用excel编写,seldom支持运行excel文件。excel的具体定义可以参考`HTTP接口测试`章节。
================================================
FILE: docs/vpdocs/introduce.md
================================================
# 介绍
## 新书推荐
京东 [购买链接](https://item.jd.com/14859108.html)
天猫 [购买链接](https://detail.tmall.com/item.htm?id=852715481274&skuId=5817727406269)
当当 [购买链接](https://product.dangdang.com/29809610.html)
依托于 SeldomQA 相关项目的开发和维护,在 `自动化测试框架设计`、 `定制化测试报告设计`、 `设计模式`,以及`测试平台开发`
方面有着深厚技术积累和独特的设计理念。
一本真正介绍 __自动化测试框架设计__ 的书终于出版了,书中浅显易懂的介绍了 SeldomQA
相关项目中的诸多设计和封装技术。并且,介绍了`一个开源自动化测试框架从设计到发布的整个流程`。
如果你正在使用SeldomQA相关项目之余,想了解他们背后的设计,那么这本书非常值得购买。
## seldom框架
### 特点
> seldom 是基于 unittest 的全功能自动化测试框架;针对自动化测试达到开箱即用。
__seldom特点__
* 支持测试类型(web/app/api)
* 丰富的断言
* 生成随机测试数据
* 用例依赖
* 用例分类标签
* 支持发送(邮件、钉钉、飞书、企微)消息等
* 日志打印
* 缓存cache
* 命令行工具
* 强大的数据驱动(JSON/YAML/CSV/EXCEL)
* HTML/XML报告
* 失败重跑&截图
* 数据库操作(MySQL/sqlite3/Mongodb)
* 支持平台化
### 设计理念
简单一句话就是回到最初写代码的样子。
自动化测试框架很多,只有在测试领域有一个比较奇怪的现象,如何用不写代码的方式解决自动化问题。为此,我们发明了用特定领域语言写用例,发明了用
excel 写用例,发明了用 YAML/JSON 写用例。这些方案看似简化了用例的编写,但是,会让解决复杂的问题变得更复杂。比如实现个分支判断/循环,传递参数,调用封装的步骤,编程语言中用
if/for 、变量、函数就实现了,但是用非编程语言的方式写用例处理起来就很麻烦。最终,并不能完全脱离编程,那么为什么不一开始就选择一个编程框架呢?
然而,seldom的定位是尽量用简单的设计去解决复杂问题,例如
Flask、requests、yagmail...等,这些框架/库都有一个共同的特点,用简单的方式去解决复杂的问题,在编程语言这个层面,并不会给你太多限制,你可以完全使用它,也可以只用一部分,也可以平滑的实现它不支持的功能。
seldom的目标以就让你用最少的代码编写自动化测试用例,当遇到seldom没有的功能,你可以方便的进行扩展。-- 这就是seldom的设计理念。
### 发展历史
2015年7月15号我在github上提交一个自动化项目,命名为:`pyse`, 即各取了`python` 和 `selenium`前两个字符。项目非常简单核心就三个文件。
* `pyse.py`:针对 selenium API做了简单封装。
* `HTMLTestRunner.py`: 修改的HTMLTestRunner报告。
* `TestRunner.py`: 一个简单的 unittest运行器。
之后项目断断续续的在维护,直到2019年,也许是太闲了,加上对UI自动化有了更深入的理解,重新投入主要精力维护pyse项目。
后来就需要将提交到pypi,这样更方便通过pip安装,发现 `pyse` 早已经被占用了,后来更名为`seldom`
,其实命名没有太多寓意,就是看他长得和`selenium`比较接近。
2020年1月发布1.0版本,之所以发布1.0
是因为自认为框架的功能比较成熟了,并且花费时间补充了文档。大家都不重视文档,其实文档非常重要,也需要花大量的时间更新和维护。有时候你加个功能很简单,编写说明文档和使用示例就要花费等同的时间。
1.0 版本之后,项目核心围绕着 selenium API的封装 和 unittest框架扩展(seldom基于unittest)等。
2021年4月正式发布 2.0,集成requests, 正式支持http接口测试。起因是发现cypress支持http调用,哦,原来UI测试工具也可以去做接口,格局一下子打开了!如何在不影响现有selenium
API的情况下集成requests是2.0考虑的重点。
2022年1月seldom项目正式在公司内部推广使用,当时我们做了几版的接口测试平台,平台的开发维护成本比较高,对于复杂的场景用例,编写成本比框架还要复杂简单;功能也依赖于平台所提供的,相比较而言,框架却有最大的灵活性,可以很好的基于业务做各种设计和封装。
因为在公司得到推广使用,seldom明显进入了更加快速的迭代开发阶段,并且稳定性、可用性会得到了很大的提升。
seldom 3.0 背景
seldom集成App测试是顺理成章的事情,早在几个月前我已经在公司项目中尝试 seldom + appium
进行App自动化测试。App自动化的维护成本确实比接口要高许多,这是由App本身的特点决定的,框架很难做到实质上的改变。
2022年10月seldom 3.0 beta发布,之所以选择appium有几个原因:
* appium 是由商业工具在维护,历史比较长,不会随意停止维护。
* appium 应用更加广泛,使用得人更多,支持得平台多(android/ios/flutter)
* appium 继承selenium,对于seldom来说对原有API改动最小。
目前,seldom 3.0 正式版已经发布,欢迎使用。
### seldom vs pytest
seldom 是建立在 unittest 的基础上的自动化测试框架。与 pytest进行对比,无疑相当于像拿一台`电脑`与一颗 intel `CPU` 进行比较,虽然
intel `CPU` 很强大,但我们无法直接拿一个`CPU`打游戏,对吧? pytest 就像一个 `CPU`
,虽然很强大,但无法直接拿来做自动化测试,比如配合各种测试库。而seldom不需要额外安装测试库,即可开始编写自动化测试用例。
* seldom vs pytest 对比差异
| 功能 | seldom | pytest |
|---------------|--------------------------------------------|---------------------------------------|
| web UI测试 | 支持 ✅ | 支持(需安装 selenium) ⚠️ |
| web UI断言 | 支持(assertText、assertTitle、assertElement) ✅ | 不支持 ❌ |
| playwright | 支持(需安装playwright) ⚠️ | 支持(playwright提供playwright-pytest插件) ✅ |
| 失败截图 | 支持(自动实现) ✅ | 支持(需要设置) ✅ |
| http接口测试 | 支持 ✅ | 支持(需安装 requests) ⚠️ |
| http接口断言 | 支持(assertJSON、assertPath、assertSchema) ✅ | 不支持 ❌ |
| app UI测试 | 支持 ✅ | 支持(需安装 appium) ⚠️ |
| Page Object模式 | 支持(推荐poium) ✅ | 支持(推荐poium) ✅ |
| 脚手架 | 支持(快速创建项目) ✅ | 不支持 ❌ |
| 生成随机测试数据 | 支持`testdata` ✅ | 不支持 ❌ |
| 发送消息 | 支持(email、钉钉、飞书、微信)✅ | 不支持 ❌ |
| log日志 | 支持 ✅ | 不支持 ❌ |
| 数据库操作 | 支持(sqlite3、MySQL、SQL Server) ✅ | 不支持 ❌ |
| 用例依赖 | 支持`@depend()` ✅ | `@pytest.mark.dependency()`支持 ✅ |
| 失败重跑 | 支持`rerun` ✅ | pytest-rerunfailures 支持 ✅ |
| 用例分类标签 | 支持`@label()` ✅ | `@pytest.mark.xxx`支持 ✅ |
| HTML测试报告 | 支持 ✅ | pytest-html、allure ✅ |
| XML测试报告 | 支持 ✅ | 自带 `--junit-xml` ✅ |
| 数据驱动方法 | `@data()` ✅ | `@pytest.mark.parametrize()` ✅ |
| 数据驱动文件 | `@file_data()`(JSON\YAML\CSV\Excel) ✅ | 不支持 ❌ |
| 钩子函数 | `confrun.py`用例运行钩子 ⚠️ | `conftest.py` 功能更强大 ✅ |
| 命令行工具CLI | 支持`seldom` ✅ | 支持`pytest` ✅ |
| 并发执行 | 不支持 ❌ | pytest-xdist、pytest-parallel ✅ |
| 平台化 | 支持(seldom-platform)✅ | 不支持 ❌ |
| 第三方插件 | seldom(unittest)的生态比较糟糕 ⚠️ | pytest有丰富插件生态 ✅ |
__说明__
* ✅ : 表示支持。
* ⚠️: 支持,但支持的不好,或没有对方好。
* ❌ : 不支持,表示框架没有该功能,第三方插件也没有。
## 框架学习
B站实战视频:https://www.bilibili.com/video/BV1QHQVYoEHC
================================================
FILE: docs/vpdocs/more-ability/benchmark.md
================================================
# 基准测试
基准测试属于性能测试的一种,用于评估和衡量软件的性能指标。我们可以在软件开发的某个阶段通过基准测试建立一个已知的性能水平,称为"
基准线"。当系统的软硬件环境发生变化之后再进行一次基准测试以确定那些变化对性能的影响。__这是基准测试最常见的用途。
Donald Knuth在1974年出版的《Structured Programming with go to Statements》提到:
> 毫无疑问,对效率的片面追求会导致各种滥用。程序员会浪费大量的时间在非关键程序的速度上,实际上这些尝试提升效率的行为反倒可能产生很大的负面影响,特别是当调试和维护的时候。我们不应该过度纠结于细节的优化,应该说约97%的场景:过早的优化是万恶之源。当然我们也不应该放弃对那关键3%的优化。一个好的程序员不会因为这个比例小就裹足不前,他们会明智地观察和识别哪些是关键的代码;但是仅当关键代码已经被确认的前提下才会进行优化。对于很多程序员来说,判断哪部分是关键的性能瓶颈,是很容易犯经验上的错误的,因此一般应该借助测量工具来证明。
虽然经常被解读为不需要关心性能,但是的少部分情况下(3%)应该观察和识别关键代码并进行优化。
## 进行基准测试
在某些情况下,需要进行,seldom 提供了基准测试的功能。
__示例__
```python
import time
import seldom
from seldom.testdata import get_int
from seldom.utils.benchmark import benchmark_test
class MyTests(seldom.TestCase):
@benchmark_test()
def test_something_performance_1(self):
"""
something code performance
"""
num = get_int(1, 2000) / 1000
time.sleep(num)
@benchmark_test(rounds=10, iterations=2)
def test_something_performance_2(self):
"""
something code performance
"""
num = get_int(1, 2000) / 1000
time.sleep(num)
@benchmark_test(rounds=10)
def test_http_performance(self):
"""
test http benchmark
"""
self.get("https://httpbin.org/get")
self.assertStatusOk()
if __name__ == "__main__":
seldom.main(benchmark=True)
```
运行结果:
```shell
> python test_benchmark.py
...
=============================================== benchmark: 3 tests ===========================================
Name (time in s) Min Max Mean StdDev Median IQR Outliers OPS Rounds Iterations
--------------------------------------------------------------------------------------------------------------
test_http_performance 0.9126 3.0273 1.3924 0.5931 1.2281 0.5033 1;9 0.7182 10 1
test_something_performance_1 0.1354 1.8026 0.7440 0.6109 0.5856 0.7842 1;4 1.3441 5 1
test_something_performance_2 0.6498 1.8315 1.1404 0.4289 1.0993 0.7592 5;5 0.8769 10 2
--------------------------------------------------------------------------------------------------------------
Legend:
Outliers: 1 Standard Deviation from Mean; 1.5 IQR (InterQuartile Range) from 1st Quartile and 3rd Quartile.
OPS: Operations Per Second, computed as 1 / Mean
```
__说明__
* `@benchmark_test()`: 基准测试装饰器:
* `rounds`: 运行基准测试的轮数,默认参数为`5`。
* `iterations`: 每轮调用函数的次数,默认参数为 `1`。
* `main()`运行方法中,`branch`参数必须设置为`True`,且不支持生成HTML报告。
* 基准测试结果说明
* `Min`: 最小执行时间,单位为秒s。
* `Max`: 最大执行时间,单位为秒s。
* `Minean`: 该测试运行过程中的平均执行时间,单位为秒s。
* `StdDev`: 标准差,表示测试执行时间的离散程度或波动性。较大的标准差意味着测试结果波动较大。。
* `Median`: 中位数,表示排序后所有执行时间的中间值。它用于衡量数据的集中趋势,避免极端值(如最大或最小值)影响结果。
* `IQR`: 四分位距,表示第 75 百分位数和第 25 百分位数之间的差距。它反映了数据的分布范围。
* `Outliers`: 异常值的数量,以及非异常值的数量。异常值的定义通常是距离均值超过 1 个标准差,或者距离第一四分位数和第三四分位数超过
1.5 倍 IQR。
* `OPS`: 每秒操作数,计算公式是 1 / Mean。它衡量每秒能够执行的操作次数,越高代表性能越好。
* `Rounds`: 运行基准测试的轮数,默认参数为`5`。
* `Iterations`: 每轮调用函数的次数,默认参数为 `1`。
================================================
FILE: docs/vpdocs/more-ability/db_operation.md
================================================
# 数据库操作
seldom 支持sqlite3、MySQL、SQL Server、MongoDB、PostgreSQL等数据库操作。
| sqlite3 | MySQL | SQL Server | PostgreSQL |
|----------------------|----------------------|----------------------|----------------------|
| execute_sql() | execute_sql() | execute_sql() | execute_sql() |
| query_sql() | query_sql() | query_sql() | query_sql() |
| query_one() | query_one() | query_one() | query_one() |
| insert_get_last_id() | insert_get_last_id() | insert_get_last_id() | insert_get_last_id() |
| delete() | delete() | delete() | delete() |
| insert() | insert() | insert() | insert() |
| select() | select() | select() | select() |
| update() | update() | update() | update() |
| init_table() | init_table() | init_table() | init_table() |
| close() | close() | close() | close() |
为了减少seldom框架的依赖库,默认只安装了`MySQL`数据库依赖,其他数据库你可以根据自己的需求进行单独安装。
```shell
> pip show pymongo[optional]
> pip install pymssql[optional]
> pip install psycopg2[optional]
```
MongoDB: https://github.com/mongodb/mongo-python-driver
SQL Server: https://github.com/pymssql/pymssql
PostgreSQL: https://github.com/psycopg/psycopg2
### 连接数据库
__连接sqlit3数据库__
```py
from seldom.db_operation import SQLiteDB
db = SQLiteDB(r"D:\learnAPI\db.sqlite3")
```
__连接MySQL数据库__
```py
from seldom.db_operation import MySQLDB
db = MySQLDB(host="127.0.0.1",
port=3306,
user="root",
password="123",
database="db_name")
```
__连接SQL Server数据库__
```py
from seldom.db_operation.mssql_db import MSSQLDB
db = MSSQLDB(server="127.0.0.1",
user="SA",
password="tc@123",
database="TestDB")
```
__连接PostgresSQL数据库__
```py
from seldom.db_operation.postgres_db import PostgresDB
db = PostgresDB(host="localhost",
port=3306,
user="dev",
password="808801",
database="db_user")
```
### 操作方法
* execute_sql
执行sql语句,无返回结果。
```python
db.execute_sql("INSERT INTO user (id, name) VALUES (1, 'tom') ")
db.execute_sql("UPDATE user SET name = 'jack' WHERE id=1")
db.execute_sql("DELETE FROM user WHERE id = 1")
```
* query_sql
执行查询sql语句,返回查询结果。
```python
ret = db.query_sql("select * from user")
print(ret)
```
* query_one
执行查询sql语句,返回一条结果。
```python
ret = db.query_one("select * from user")
print(ret)
```
* insert_get_last_id
插入数据并返回最新的ID。
```python
last_id = db.insert_get_last_id("INSERT INTO user (id, name) VALUES (1, 'tom') ")
print(last_id)
```
* delete
删除表数据。
```py
db.delete(table="user", where={"id": 1})
```
* insert
插入一条数据。
```py
data = {"id": 10, "name": "jean"}
db.insert(table="user", data=data)
```
* select
查询表数据。
```py
result = db.select(table="user", where={"id": 1, "name": "tom"})
print(result)
result = db.select(table="user", one=True) # one=True 返回一条结果
print(result)
```
* update
更新表数据。
```py
db.update(table="user", where={"name": "tom", }, data={"name": "jack"})
```
* init_table
批量插入数据,在插入之前先清空表数据。
```py
# more table data
table_data = {
"group": [
{"id": 1, "name": "test"},
{"id": 2, "name": "product"},
{"id": 3, "name": "develop"},
],
"user": [
{"id": 1, "name": "jeannie"},
{"id": 2, "name": "joye"},
{"id": 3, "name": "blue"},
],
}
db.init_table(table_data)
```
* close
关闭数据库连接。
```py
db.close()
```
## MongoDB
MongoDB 是一个基于分布式文件存储的数据库,属于非关系型数据库,与关系型数据库得操作有着较大得差异,它本身支持字典传参,所以,seldom
只简单封装了数据库连接。
* 连接MongoDB
```python
from seldom.db_operation.mongo_db import MongoDB
db = MongoDB(host="localhost", port=27017, db="yapi")
```
__参数说明:__
* host: 连接地址。
* port: 端口号。
* db: 数据库名字。
__pymongo__
以下操作seldom没有做任何封装,请参考[pymongo](https://github.com/mongodb/mongo-python-driver)
* 获取集合信息
```python
col = db.list_collection_names()
print(col)
```
结果:
```shell
collection list: ['project', 'log', ...]
```
* 获取表一条数据
```python
data = db.project.find_one()
print("table one data:", data)
```
结果:
```shell
table data: {'_id': 11, 'switch_notice': True, 'is_mock_open': False, 'strice': False, 'is_json5': False, 'name': '发布会签到系统'}
```
* [添加数据](https://www.runoob.com/python3/python-mongodb-insert-document.html)
* [查询数据](https://www.runoob.com/python3/python-mongodb-query-document.html)
* [修改数据](https://www.runoob.com/python3/python-mongodb-update-document.html)
* [删除数据](https://www.runoob.com/python3/python-mongodb-delete-document.html)
================================================
FILE: docs/vpdocs/more-ability/test_library.md
================================================
# 支持更多测试库
seldom 集成了`selenium`、`appium`、`requests`,他们都是非常优秀且成熟的库,这并不是说,你不能在seldom使用其他的测试库。
seldom 作为一个测试框架,理论上可以与任何测试库一起使用。seldom提供的基础能力(数据驱动、随机数、测试报告、缓存...等)同样可以提升这些测试库的使用效率。
### 使用playwright
playwright就微软推出的优秀的 web UI 自动化测试库。
官方地址: https://playwright.dev/
* pip安装Playwright
```shell
> pip install playwright
```
* playwright 安装浏览器以及驱动
```shell
> playwright install
```
* 使用例子
```python
import seldom
from playwright.sync_api import sync_playwright
from playwright.sync_api import expect
class Playwright(seldom.TestCase):
def start(self):
self.p = sync_playwright().start()
self.chromium = self.p.chromium.launch()
self.page = self.chromium.new_page()
def end(self):
self.chromium.close()
self.p.stop()
def test_playwright_start(self):
"""
test playwright index page
"""
self.page.goto("http://playwright.dev")
expect(self.page).to_have_title("Fast and reliable end-to-end testing for modern web apps | Playwright")
get_started = self.page.locator('text=Get Started')
expect(get_started).to_have_attribute('href', '/docs/intro')
get_started.click()
expect(self.page).to_have_url('http://playwright.dev/docs/intro')
# 截图
screenshot_bytes = self.page.screenshot()
self.screenshots(image=screenshot_bytes)
def test_playwright_todo(self):
"""
test playwright todoMVC
"""
self.page.goto("https://demo.playwright.dev/todomvc/#/")
new_todo = self.page.locator(".new-todo")
new_todo.fill("sleep")
new_todo.press("Enter")
new_todo.fill("code")
new_todo.press("Enter")
new_todo.fill("eat")
new_todo.press("Enter")
self.page.locator('li').filter(has_text='code').get_by_label('Toggle Todo').check()
self.page.locator('li').filter(has_text='sleep').get_by_label('Toggle Todo').check()
self.page.locator('li').filter(has_text='eat').get_by_label('Toggle Todo').check()
# 截图
screenshot_bytes = self.page.screenshot()
self.screenshots(image=screenshot_bytes)
if __name__ == '__main__':
seldom.main()
```
### 使用uiautomator2
uiautomator2是openatx推出的优秀的Android自动化测试工具,Api简单,同样得到广泛应用。
github地址: https://github.com/openatx/uiautomator2
* pip安装
```shell
pip install uiautomator2
```
* 使用例子
```python
import seldom
from seldom.utils.adbutils import ADBUtils
import uiautomator2 as u2
class MyAppTest(seldom.TestCase):
def start(self):
# 链接设备
self.d = u2.connect(self.device)
# 启动App
self.d.app_start("com.microsoft.bing")
def end(self):
# 停止app
self.d.app_stop("com.microsoft.bing")
def test_bing_app(self):
""" 使用 uiautomator2 """
self.d(resourceId="com.microsoft.bing:id/sa_hp_header_search_box").click()
self.d(resourceId="com.microsoft.bing:id/input_container").set_text("seldomQA")
self.d(resourceId="com.microsoft.bing:id/input_container").center()
# ....
if __name__ == '__main__':
adb = ADBUtils()
devices = adb.refresh_devices()
seldom.main(debug=True, device=devices[0][0])
```
### 使用pyAutoGUI
pyAutoGUI专注于模拟鼠标和键盘操作,实现GUI的自动化。
适用于需要在多个操作系统(Windows、macOS、Linux)上模拟用户输入(如点击、拖动、输入文本等)的场景,如自动化测试、数据录入、游戏辅助等。
官方地址: https://github.com/asweigart/pyautogui
* pip安装pyAutoGUI
```shell
> pip install pyautogui
```
* 使用例子
```python
import os
import pyautogui
import seldom
from seldom.testdata import get_int
class TestPyAutoGUINote(seldom.TestCase):
def start(self):
# 打开记事本(这里使用运行命令来打开,确保路径正确)
os.system('notepad.exe')
self.sleep()
def end(self):
# 模拟按下 Alt+F4 关闭记事本
pyautogui.hotkey('alt', 'f4')
def test_write_and_save(self):
"""
打开一个新的标签页写入内容并保存
"""
# 模拟按下 Ctrl+t 创建一个新的标签页
pyautogui.hotkey('ctrl', 't')
self.sleep()
pyautogui.press('shift') # 切换英文输入法
# 写入字符串到记事本
pyautogui.write('Hello, this is a test string written by pyautogui.', interval=0.1) # interval 参数设置每个字符之间的延迟时间
# 模拟按下 Ctrl+S 保存文件
pyautogui.hotkey('ctrl', 's')
self.sleep()
# 切换英文输入法
pyautogui.press('shift')
self.sleep()
# 输入文件名 + 回车确定
pyautogui.write(f'test_file{get_int()}.txt')
self.sleep()
pyautogui.press('enter')
self.sleep()
if __name__ == '__main__':
seldom.main()
```
### 使用auto-wing
AI在自动化领域已经得到相关的应用,出现了不少项目(`browser-use`、`Midscene.js`
等)。auto-wing是一款基于LLM的自动化工具。可以很好的整合到seldom框架中使用。
GitHub地址: https://github.com/SeldomQA/auto-wing
* pip安装auto-wing
```shell
> pip install autowing
```
* 配置大模型 `API_key`
在脚本目录下创建`.env`文件,配置LLM的`API_key`, 支持多模型:`openai`、`deepseek`、`qwen` 和 `doubao`。这里以 `deepseek`为例。
```env
.env
AUTOWING_MODEL_PROVIDER=deepseek
DEEPSEEK_API_KEY=sk-abdefghijklmnopqrstwvwxyz0123456789
```
* 使用例子
```python
import seldom
from seldom import Seldom
from autowing.selenium.fixture import create_fixture
from dotenv import load_dotenv
class TestBingSearch(seldom.TestCase):
@classmethod
def start_class(cls):
# load .env file
load_dotenv()
# Create AI fixture
ai_fixture = create_fixture()
cls.ai = ai_fixture(Seldom.driver)
def test_bing_search(self):
"""
Test Bing search functionality using AI-driven automation.
"""
self.open("https://cn.bing.com")
self.ai.ai_action('搜索输入框输入"playwright"关键字,并回车')
self.sleep(3)
items = self.ai.ai_query('string[], 搜索结果列表中包含"playwright"相关的标题')
self.assertGreater(len(items), 1)
self.assertTrue(
self.ai.ai_assert('检查搜索结果列表第一条标题是否包含"playwright"字符串')
)
if __name__ == '__main__':
seldom.main(browser="edge", debug=True)
```
================================================
FILE: docs/vpdocs/platform/platform.md
================================================
# 平台化支持
为了更好的支持测试用例平台化,Seldom 提供了API用于获取用例列表,以及根据传入的用例信息运行测试用例。
## 接入平台方式
seldom-platform项目: https://github.com/SeldomQA/seldom-platform
目录结构如下:
```shell
mypro/
├── test_dir/
│ ├── module_api/
│ │ ├── test_http_demo.py
│ ├── module_web/
│ │ ├── test_first_demo.py
│ │ ├── test_ddt_demo.py
└── run.py
```
### 获取用例信息
```py
# run.py
from seldom import SeldomTestLoader
from seldom import TestMainExtend
if __name__ == '__main__':
SeldomTestLoader.collectCaseInfo = True
main_extend = TestMainExtend(path="./test_dir/")
case_info = main_extend.collect_cases(json=True)
print(case_info)
```
__说明__
返回的用例信息列表:
* `collectCaseInfo` :`collectCaseInfo`设置为`True` 说明需要收集用例信息。
* `TestMainExtend(path="./test_dir/")` : `TestMainExtend`类是`TestMain`类的扩展,`path`设置收集用例的目录,不能为空。
* `collect_cases(json=False, level="data", warning=False)`:返回收集的用例信息。
* `json=False`:默认为`list`格式;设置为`True`返回`json`格式。
* `level="data"` :默认为`data`,数据驱动的每条数据被解析为一条用例。如果设置为 `method` 数据驱动的方法被解析为一条用例。
* `warning=False`: 默认为`False`,
在收集用例的过程中,因为缺少依赖库,或导包错误会导致部分用例收集报错,是否要将这些错误保存下来。开启(True)后,默认保存在`reports/collect_warning.log`
文件中。
```json
[
{
"file": "module_api.test_http_demo",
"class": {
"name": "TestRequest",
"doc": "\n http api test demo\n doc: https://requests.readthedocs.io/en/master/\n "
},
"method": {
"name": "test_get_method",
"doc": "\n test get request\n ",
"label": null
}
},
{
"file": "module_api.test_http_demo",
"class": {
"name": "TestRequest",
"doc": "\n http api test demo\n doc: https://requests.readthedocs.io/en/master/\n "
},
"method": {
"name": "test_post_method",
"doc": "\n test post request\n ",
"label": null
}
},
{
"file": "module_web.test_ddt_demo",
"class": {
"name": "BaiduTest",
"doc": "Baidu search test case"
},
"method": {
"name": "test_baidu_0",
"doc": "used parameterized test [with name=1, search_key='seldom']\n :param name: case name\n :param search_key: search keyword\n ",
"label": null
}
},
{
"file": "module_web.test_ddt_demo",
"class": {
"name": "BaiduTest",
"doc": "Baidu search test case"
},
"method": {
"name": "test_baidu_1",
"doc": "used parameterized test [with name=2, search_key='selenium']\n :param name: case name\n :param search_key: search keyword\n ",
"label": null
}
},
{
"file": "module_web.test_ddt_demo",
"class": {
"name": "BaiduTest",
"doc": "Baidu search test case"
},
"method": {
"name": "test_baidu_2",
"doc": "used parameterized test [with name=3, search_key='unittest']\n :param name: case name\n :param search_key: search keyword\n ",
"label": null
}
},
{
"file": "module_web.test_first_demo",
"class": {
"name": "BaiduTest",
"doc": "Baidu search test case"
},
"method": {
"name": "test_case",
"doc": "a simple test case ",
"label": null
}
}
]
```
数据结构说明:
* file: 获取类的文件名,包含目录名。
* class: 测试类的名字`name` 和 描述`doc`。
* method: 测试方法的名字`name` 和 描述`doc`, `label`。
> 注明:seldom==3.11.0 版本测试方法增加`label`字段。
### 执行用例信息
当获取用例信息之后,可以进行自定义,例如 挑选出需要执行的用例,重新传给Seldom 执行。
```python
# run.py
from seldom import TestMainExtend
if __name__ == '__main__':
# 自定义要执行的用例
cases = [
{
"file": "module_web.test_first_demo",
"class": {
"name": "BaiduTest",
"doc": "Baidu search test case"
},
"method": {
"name": "test_case",
"doc": "a simple test case ",
"label": ""
}
}
]
main_extend = TestMainExtend(path="./test_dir")
main_extend.run_cases(cases)
```
说明:
* `cases` 定义要执行的用例信息, `doc` 非必填字段。
* `TestMainExtend(path="./test_dir")` : 其中`path`指定从哪个目录查找用例集合。
* `run_cases(cases)`: 运行用例。
## 接入平台必读
如果你只是使用seldom框架编写用例,那么代码只要框架能运行即可,如果要接入seldom-platform平台,那么需要注意一下几点。
#### 🚧 测试每个子目录必须包含`__init__.py`文件。
* 目录结构
```shell
├───reports
├───test_data
├───test_dir
│ ├───api_case
│ │ └───__init__.py
│ ├───app_case
│ │ └───__init__.py
│ ├───web_case
│ │ └───__init__.py
│ └───__init__.py
└───run.py
```
> 如果子目录不添加 __init__.py 文件会导致目录下面的用例无法解析。
#### 🚧 用例的前置动作
在用 seldom框架写用例的时候需要执行一些`前置/后置`动作。
```python
import seldom
from seldom.utils import cache
if __name__ == '__main__':
# 前置动作
cache.set({"token": "token123"})
# 执行用例
seldom.main("./test_dir")
# 后置动作
cache.clear("token")
```
但是,平台执行的时候,不会执行 `前置/后置`动作。 那么,为了使平台可以执行前置动作,需要使用`confrun.py`文件进行配置。
* 目录结构
```shell
├───reports
├───test_data
├───test_dir
│ ├───...
├───confrun.py
└───run.py
```
* confrun.py配置
```python
"""
seldom confrun.py hooks function
"""
from seldom.utils import cache
def start_run():
"""前置动作"""
cache.set({"token": "token123"})
def end_run():
"""后置动作"""
cache.clear("token")
```
* run.py文件
```python
import seldom
if __name__ == '__main__':
# 执行用例
seldom.main("./test_dir")
```
通过上面的配置,`前置、后置`动作就可以在平台上运行,当然,这样设置本地也可正常运行。
================================================
FILE: docs/vpdocs/version/CHANGES.md
================================================
# 版本更新
### seldom 3.x
__3.13.0(2025-03-16)__
* 功能:unittest所有基础断言增加日志, 例如:`assertEqual()`、`assertIn()`...。
* 功能:增加`adb`操作:`get_devices`、`launch_app`、`close_app`。
* 功能:`seldom.main()`增加`device`参数,用于存储设备ID。感谢@ThickBull
* 修复bug: appium提供的`query_app_state()`执行报错。
* 文档:增加`auto-wing` AI库使用示例。
* 文档:修复官方文档左侧菜单无法显示的问题。
* 升级`Appium-Python-Client==4.5.0`。
* 支持 `python 3.9 ~ 3.13` 版本。
__3.12.0(2025-01-18)__
* 功能:支持基准测试功能。
* 功能:提供加密模块,支持 `MD5`,`SHA1`,`AES`,`Base64`等各种`编码&解码`,`加密&解密`等算法。
* 功能:`seldom.main()`增加`env`参数,用于设置静态变量。
* 功能:HTML测试报告根据网络判断是否生成本地本地静态文件,无网络情况也可正常显示报告。
* 修复:`dependent_func()`装饰器调用静态方法报错。感谢@Catking233
__3.11.0(2024-12-19)__
* 功能:平台化用例执行,`seldom.main()`支持加载`confrun.py`中的 `start_run()/end_run()`。
* 功能:平台化用例解析,识别用例标签`label`。
* HTTP测试:通过`confrun.py`支持`proxies()`配置全局的请求代理。
* App测试:
* appium_lab 增加 `drag_from_to()`方法,支持坐标位滑动。感谢@guweifan
* appium_lab 增加`AppiumService`类,支持启动appium server。感谢@guweifan
* 优化:`jsonpath.py`的代码。
__3.10.0(2024-11-11)__
* 重要:所有app/web元素定位支持`selector`模式,详细查看文档。
* 更新:`sleep()`增加默认值1s,也支持随机休眠范围:`self.seep((1, 3))`。
* 更新: `appium_lab`模块的 `Action()` 类下面的方法支持自定义休眠时间、间隔时间等。
* 修复:`Steps()`类的 `open()` 方法默认传url报错 [#241](https://github.com/SeldomQA/seldom/issues/241)。
* 告警:`type_enter()`添加移除警告,推荐使用`type()`。
* 文档:
* 修改playwright使用示例。
* 增加pyAutoGUI使用示例。
__3.9.1(2024-10-10)__
* 更新:脚手架项目模板,增加`run.py`文件。
* 修复:生成随机数,获取在线时间接口错误。
* 修复:`datetime.utcnow()`在Python 3.12 告警。
* App测试:
* 修复`back()`、`home()`方法报错。
* 增加`long_press_key()`方法。
* API测试:
* 增加`assertStatusOk()`断言方法,断言接口返回状态码`200`。
* `@check_response()`装饰器重命名`@api()`,更简洁。
* Web测试:
* 增加`prompt_value()`方法,支持弹窗输入 [#166](https://github.com/SeldomQA/seldom/issues/166)。
* 增加`action_chains()`方法,返回Selenium的`ActionChains()`
类对象 [#119](https://github.com/SeldomQA/seldom/issues/119)。
* 增加`is_visible()`方法,检查页面元素是否可见 [#62](https://github.com/SeldomQA/seldom/issues/62)。
* `Pycharm`右键运行Web UI用例,抛异常提示。
* 文档更新:
* 增加浏览器代理设置示例 [#31](https://github.com/SeldomQA/seldom/issues/31)。
* 操作已打开浏览器示例 [#174](https://github.com/SeldomQA/seldom/issues/174)。
* 升级:`XTestRunner==1.8.0`。
__3.9.0(2024-09-09)__
* App测试。
* 升级`Appium-Python-Client==4.1.0`
* 提供`UiAutomator2Options`和`EspressoOptions`类,替换appium提供的这个两个类。
* 移除不再支持的API: `launch_app()`、`close_app()`、`reset()`。
* 增加App相关操作时的日志。
* Web测试浏览启动重构。
* 支持`start/end`启动和关闭浏览器。
* 支持`start_class/end_class`启动和关闭浏览器。
* 支持`new_browser()`重新打开一个浏览器。
* `self.open()` 检测到没有指定浏览器,不再默认启动一个`Chrome()`浏览器。
* 链式API `Steps()`类添加`browser`参数。
* `Seldom.driver`对象支持多线程。
* `log`日志显示当前运行的线程。
* `Cache`缓存类支持多线程。
* 其他:移除直接依赖库:`requests`和`websocket-client`, 使用间接依赖。
* `XTestRunner` -> `requests`
* `Appium-Python-Client` -> `selenium` -> `websocket-client`
__3.8.1(2024-08-20)__
* App测试。
* 支持`Appium-Python-Client==4.0.1`,修复`4.0.0` 引起的问题。
* `seldom` 命令,创建项目命令区分`web/app/api`项目。
* 修复`seldom-platform`平台运行错误。
__3.8.0(2024-07-06)__
* API测试:
* 支持执行Excel测试用例, `seldom --api-excel api_case.xlsx` 具体用法查看文档。
* App:
* 增加 `self.keyboard_search()`模拟键盘上的搜索按键。
* 优化: `@file_data()`参数化装饰器代码。
__3.7.1(2024-06-01)__
* 优化:`main()` 中的`path`参数支持列表,可以指定多个目录或文件。
* 新增:提供`from seldom.utils.send_extend import RunResult` 获取用例的执行数据。
* App测试。
* 增加`swipe_right()`左滑 和 `swipe_left()`右滑支持。
* `AppiumLab()` 默认允许不传`driver`参数。
* 其他:
* `Python 3.12` 测试通过。
__3.7.0(2024-05-06)__
* `@data()`数据驱动装饰器增加`cartesian=True`参数,支持笛卡尔积。
* 新增`WebSocket`接口测试支持。
* App测试。
* 支持`Appium-Python-Client==4.0.0`,修复`4.0.0` 引起的问题。
* Web测试
* 重新支持指定浏览器驱动,使用`executable_path`参数。
* 其他:
* 基于`selenium`依赖库,移除 `Python 3.7` 支持。
__3.6.0(2024-03-04)__
* `seldom.main()`方法增加`failfast`参数,debug模式,允许第一条用例失败,停止执行。
* 增加`@retry()`装饰器,用于函数&方法错误重试。
* HTTP测试
* 支持`swagger`文档转seldom用例,使用命令 `seldom -s2c swagger.json` 。
* 文档:增加 `API Object model` 概念的介绍,以及在seldom中的应用。
__3.5.0(2024-01-14)__
* 新增:支持 Postgre SQL 数据库操作。
* web测试
* `pause()` 用于暂停操作。
* 移除`webdriver_manager_extend.py`文件(之前漏移除文件)。
* App测试
* 支持`appium 2.0` 正式版。
* 支持appium-OCR-plugin插件。
* 增加`click_image()`方法,支持图片点击定位。
* `press_key()` 支持`ENTER`参数,模拟键盘回车。
__3.4.1(2023-11-26)__
* 修复:`diff_json()` 对比特殊数据的异常没有捕捉到。
* `setUpClass()`/`tearDownClass()` 增加异常捕捉,避免报错之后,用例无法统计的问题。
* web测试
* `screenshots()` 增加images参数,支持传入截图对象 [#202](https://github.com/SeldomQA/seldom/issues/201)。
* `open_electron()` 增加chromedriver_path参数,支持手动指定驱动地址。
__3.4.0(2023-11-18)__
* 新增:`dependent_func()`装饰器,支持用例方法依赖调用,具体使用参考文档。
* api测试
* 修复:har2case 请求头参数类型判断不准的问题。
* web测试
* 增加`open_electron()` 方法,支持启动桌面electron应用。
* 键盘操作`Key()`支持链式调用,例如: `self.Keys(id_="kw").select_all().cut()` 全选并删除。
* cache操作日志增加 emoji。
* 修复:`diff_json()` 优化,支持dict深度排序。 [#197](https://github.com/SeldomQA/seldom/issues/197)
__3.3.0(2023-09-26)__
* web测试
* 浏览器驱动`webdriver-manager` 替换为`selenium-manager`。
* 增加`execute_cdp_cmd()` 方法。
* 随机数据
* `online_timestamp()` 在线获取时间戳。
* `online_now_datetime()` 在线获取当前时间,格式为:`%Y-%m-%d %H:%M:%S`。
* 增加运行时内嵌(built-in)方法:`base_url()`、`driver()` - 无需导入,可以在自动化程序任意位置使用这两个方法。
* 移除`parameterized` 库的依赖,改为内置。
* 修复:`diff_json()` 对比 `[{}]` 数据时报错。 [#197](https://github.com/SeldomQA/seldom/issues/197)
__3.2.3(2023-07-30)__
* HTTP自动化
* `confrun.py` 支持 `mock_url` hook 钩子函数。
* 增加 `self.base_url` 获取 `base_url`。
* Web自动化
* 更新:`get_elements()` 增加`empty`参数,设置为`True`, 允许返回空列表 `[]`
* 更新: `debug=True` 模式,移除操作元素边框高亮,提高用例执行速度。
* App测试
* 修复:`key_text()` 无法输入点号`.`的问题。
* 优化:`seldom_log.log` 文件只记录一次运行结果,减少文件大小。
* 升级:`webdriver_manager==4.0.0` [#189](https://github.com/SeldomQA/seldom/issues/189)
* 其他: 添加 `pyproject.toml` 支持。
* 文档:增加其他库的使用例子。
__3.2.2(2023-05-10)__
* 功能:增加`@threads()`支持多线程运行用例。
* 功能:增加`@rerun()` 重复执行某个测试方法。
* 功能:数据库操作
* `MySQLDB()`、`MSSQLDB()` 支持`charset` 参数设置字符集。
* `init_table()` 批量插入数据库增加`clear` 参数,可以选择是否删除表再插入。
* 功能:Web自动化
* 新增`save_screenshot()` 截图保存本地。
* 修改`screenshots()` 自动截图保存到HTML报告,移除`file_path` 参数。
* 修改`element_screenshot()` 元素截图保存到HTML报告,移除`file_path` 参数。
* `type()` 方法增加 `click` 参数,针对app元素优化,app的输入框往往需要点击以下锁定光标再输入。
* 修复:浏览器配置参数 `option` 更名为 `options`。
* 其他:增加 python3.11 支持。
__3.2.1(2023-04-14)__
* 功能:增加`@disk_cache()`、`@memory_cache()` 缓存装饰器。
* 功能:app测试,seldom支持本身API支持appium定位。
* 功能:db操作,增加`insert_get_last_id()` 方法,插入数据并返回id。
* 修复:`@data_class()` 必传`input_values` 参数问题。
* 修复:设置log等级,HTML报告无法根据等级打印日志问题。
__3.2.0(2023-03-14)__
* Web UI测试,增加一组新的警告框 alert 操作。
* `self.alert.text`
* `self.alert.accept()`
* `self.alert.dismiss()`
* `self.alert.send_keys("text")`
* App UI测试。
* `AppiumLab()` 类增加 `context()` 方法获取当前上下文。
* `AppiumLab()` 类增加 `size()` 当前窗口尺寸。
* API 测试。
* 增加`self.patch()` 请求方法。
* 增加`self.json_to_dict()` 支持单引号JSON格式转字典。
* cache 增加文件锁,防止多线程读写错误(Windows不支持 fcntl)
* 支持 `XTestRunner=>1.6.2` 版本
* XML格式的报告支持 rerun 重跑参数。
* HTML 报告skip用例样式微调。
* HTML 重跑只显示最后一次结果。
* SMTP 发送报告增加 `ssl` 参数。
* `seldom.main()` 方法 ⚠ 不兼容更新
* 移除 `save_last_run` 参数。
* `browser` 参数支持`dict` 格式, 所有和浏览器配置相关的有发生修改。 包括
* 设置浏览器驱动地址。
* 设置 headless 模式。
* 设置 options 参数。
* 设置 selenium grid 地址。
__3.1.3(2023-02-15)__
* 功能:`file_data()` 增加`end_line`
参数,对于csv/excel文件支持读取到第几行结束。[#163](https://github.com/SeldomQA/seldom/issues/163)
* 优化:`self.assertElement()` 断言元素时间过长的问题。
* 优化:`self.assertJSON()` 断言日志,区分告警和错误。
* 移除:`self.jresponse()` 方法。
__3.1.2(internal)__
> 内部版本:移除了日志打印的 emoji 表情。
* 功能:`seldom.main()` 方法 path 参数支持斜杠路径`\`(windows系统用`\` 表示路径)。
__3.1.1(2023-01-03)__
* 功能:`confrun.py` 增加`start_run()/end_run()` 钩子函数,用于运行前/后相关配置。
* 优化:`@api_data()` 装饰器增加 `headers` 参数。
* 优化:`assertJSON()` 断言增加 `exclude` 参数,屏蔽检查的字段,例如 `["start_time", "token"]`。
* 修复:`rediscover()` 查找用例bug。
* 依赖:升级`XTestRunner==1.5.0` 支持飞书/微信发送消息。
__3.1.0(2022-12-15)__
* 功能:提供 `confrun.py` 运行配置文件,配合 `seldom` 命令使用。
* 功能:Web测试,增加 `self.get_log()` 方法。
* 升级:`webdriver_manager==3.8.5` ,支持Mac M1芯片的浏览器驱动。[#159](https://github.com/SeldomQA/seldom/issues/159)
* 修复:seldom-platform平台同步多个项目引起的Bug。[#158](https://github.com/SeldomQA/seldom/issues/158)
* 修复:Web测试, `self.close()` 关闭浏览器Bug。
__3.0.1(2022-11-5)__
* 功能:支持 `SQL Server` 数据库支持,需要单独安装`pymssql`库。
* 功能:http接口测试增加`curl()`方法,支持请求转 `cURL`。
* 功能:`seldom` 命令增加`--log-level` 参数,log类型:`TRACE`, `DEBUG`, `INFO`, `SUCCESS`, `WARNING`, `ERROR` 等。
__3.0.0(2022-10-31)__
* `seldom 3.0` 的核心是支持app测试,并且相关API已稳定,目的已达到,接下来将会在`3.0`基础上继续开发。
* 功能:`collect_cases()` 支持 `warning` 参数。
__3.0.0beta2(2022-10-26)__
* 修复:
* 接口测试: 接口返回文本`r.text` 中文乱码问题。[#146](https://github.com/SeldomQA/seldom/issues/146)
* app测试:感谢 @986379041
* `install_app()` 错误
* `close_app()` 错误
* 功能:
* `TestMainExtend` 类增加 `tester`参数。 [#149](https://github.com/SeldomQA/seldom/issues/149)
* 生成随机数,增加`get_month()` 和 `get_year()`方法。 [#152](https://github.com/SeldomQA/seldom/issues/152)
* seldom命令增加清除所有缓存。`> seldom --clear-cache true`。 [#153](https://github.com/SeldomQA/seldom/issues/153)
* 其他:
* seldom 运行用例,优化内存使用。
__3.0.0beta1(2022-10-03)__
* 支持App测试
* 依赖`Appium-Python-Client`库。
* `main()` 增加 `app_info`, `app_server` 参数。
* 增加`appium_lab` 模块。
* 增加`AppDriver` 类。
* 优化:基于pylint检查分析工具 优化代码。
* 其他:
* 生成随机数,增加`get_timestamp()` 获取当前时间戳。
* 数据库查询,增加`query_one()` 查询一条数据。
### seldom 2.x
__2.10.6/7(2022-09-07)__
* 功能:`seldom`命令重大更新,支持更多参数和功能。
* 功能:`@file_data()` 当设置`Seldom.env`时支持更深一级遍历。
* 修复:`diff_json()` 对比数据错误。
__2.10.4/5(2022-08-17)__
* 重构log日志打印。 @Yongchin
* 彻底修复日志重复打印的问题。
* 移除`log.printf()` 非标准日志类型。
* 修复:
* `sender()` 发送完邮件,`seldom_log.log` 文件无法删除的问题。
* `TestMainExtend` 类`run_cases()`按照用例的顺序执行。@luna-CY
* 修复`request` 带上`url=` 参数时异常。 @986379041@qq.com
* 依赖:`webdriver_manager`依赖升级到`3.8.2`
* 移除:`Opera` 浏览器的支持,selenium 4 已经移除了对opera的单独驱动支持。
__2.10.3(2022-07-17)__
* 数据驱动:`@data()` 和 `@file_data()` 优化用例名称和描述。
* 增加`Seldom.env`环境配置变量,`@file_data()` 数据驱动装饰器支持环境变量。
* 修复:`Edge`浏览器启动错误。
* 修复:HTTP接口测试`self.post()`方法 `data`参数不是dict类型错误。
* 平台化支持:优化用例收集,具体查看文档。
__2.10.2(2022-06-25)__
*
更新:移动模式列表更新,去掉旧设备,增加新设备 [link](https://github.com/SeldomQA/seldom/blob/master/docs/vpdocs/other/other.md)
* 功能:测试报告显示断言信息。
* 功能:`main()` 通过`open=False`可以控制运行完测试 不自动化打开测试报告。
* Web 测试:
* 增加`self.new_browser()` 可以打开新的浏览器,但只能使用`selenium` 的 API
* 增加`switch_to_frame_parent` 切换到上一级表单,[#118](https://github.com/SeldomQA/seldom/issues/118)。
* 优化`assertNotElement` 执行慢的情况 [#120](https://github.com/SeldomQA/seldom/issues/120)
* HTTP 测试:
* 优化:JSON日志进行格式化打印。
__2.10.1(2022-05-30)__
* 修复:seldom log 问题引起,错误信息无法在控制台打印。
> 2.10.0 为了解决[107](https://github.com/SeldomQA/seldom/issues/107)
> 问题,我们经过反复的讨论和优化,甚至对相关库XTestRunner做了修改;以为完美解决了这个问题,没想到还是引起了一些严重的错误。为此,我们感到非常沮丧,退回到2.9.0的实现方案。请升级到2.10.1版本。
__2.10.0(2022-05-25)__
* seldom log功能:
* 修复打印日志显示固定文件的问题 [107](https://github.com/SeldomQA/seldom/issues/107)。
* log方法变更:`log.warn()` -> `log.warning()`。
* 功能:提供了`cache` 类来模拟缓存。
* 功能:`@data()` 装饰器支持 `dict` 格式。
* 功能:`self.jresponse()` 方法设计不合理,给以废弃提示;可以使用`self.jsonpath()`/`self.jmespath()` 替代。
* 优化:断言方法`assertSchema()`、`assertJSON()`支持`response`传参。
* 优化:`@check_response()` check检查失败打印`response`。
* 修复:`webdriver_manager` 没有设置上限版本,导致`webdriver_manager>=3.6.x` 报错; 如果使用的 `seldom<=2.9`
请重新安装`webdriver_manager==3.5.2`。
__2.9.0(2022-04-30)__
* seldom log功能:
* 开放seldom 的`log`能力,可以配置`颜色(colorlog)`、`格式(format)`、`等级(level)` 等。
* 重新定义了seldom打印日志的格式。
* 所有log统一记录到`/reports/seldom_log.log`文件,不再每次生成单独文件。
* 功能:提供了`@check_response()` 装饰器,为接口封装提供强大的支持。
* 功能:集成`genson`库,生成JsonSchema模板 [100](https://github.com/SeldomQA/seldom/issues/100) 。
* 功能:增加`assertInPath()` 断言方法。
* 功能:增加`jmespath()`方法,方便提取测试数据。
* 优化:`jresponse()` 增加对`jmespath` 语法的支持。
* 优化:支持`self.get()/self.post()/self.put()/self.delete()` 返回response对象。
__2.8.0(2022-04-16)__
* 功能:增加MongoDB 数据库操作 [93](https://github.com/SeldomQA/seldom/issues/93) 。
* 功能:支持单个用例执行 [94](https://github.com/SeldomQA/seldom/issues/94) 。
* 功能:`sendmail()` 增加`delete`参数,发送完邮件删除`reports/`
目录下面的报告和日志文件 [95](https://github.com/SeldomQA/seldom/issues/95) 。
* 功能:增加`jsonpath` 和 `jresponse()` ,更容易查找json数据 [96](https://github.com/SeldomQA/seldom/issues/96) 。
* 功能:创建项目脚手架增加api测试例子:`seldom -project mypro` 。
* 其他: 全新的seldom在线文档:https://seldomqa.github.io/ ,感谢 @nickliya
__2.7.0(2022-03-26)__
* 功能:引入`loguru` 库用于打印日志(之前使用python默认logging总有一些重复打印或不打印的问题)。
* 功能:web自动化增加一套方法链(method chaining)的API。
* 功能:支持手动指定浏览器驱动路径。
__2.6.0(2022-03-18)__
* 移除:自带的`HTMLTestRunner`,HTML报告采用`XTestRunner`。
* 移除:对`unittest-xml-reporting`库的依赖,XML报告使用`XTestRunner`。
* 修改:`SMTP`类发送邮件方法 `sender()` -> `sendmail()`, 发送邮件样式采用`XTestRunner`。
* 增加:`seldom.main()`方法增加`tester` 参数,用于设置测试人员名字,默认`Anonymous`。
* 增加:`seldom.main()`方法增加`language` 参数,用于设置报告中英文`en/zh-CN`,默认`en`。
* 增加:发送钉钉功能。
* 修改:接口测试 `self.session` -> `self.Session()`。
* 移除:接口测试 `self.request()` 方法移除(注:该方法原本不可用)。
__2.5.1(2022-02-19)__
* 功能:Http接口测试使用日志打印接口信息
* 功能:Http接口测试打印`json`参数 [83](https://github.com/SeldomQA/seldom/issues/83)
* 修复:Web UI测试`self.Key()` 无法定位元素的问题
__2.5.0(2022-01-30)__
* 功能:支持测试平台化。
* 功能:utils 增加`file`类,获取当前文件目录更方便。
* 修复:`self.select()` 操作下拉选择错误。
* 修复:`diff_json()` 对比json文件错误。
__2.4.2(2022-01-18)__
* 功能:增强`@file_data`使用方式,json/yaml支持内嵌`dict`数据。
__2.4.1(2022-01-17)__
* 优化:HTTP接口测试增加`cookies`信息打印。
* 优化:`@file_data()` 使用,支持指定目录。
* 修复:`visit()` 方法默认浏览器没有自动安装浏览器驱动的问题。
* 修复:`query_sql()` 执行SQL没有提交的问题。
__2.4.0(2022-01-02)__
* 适配selenium 4.0+ ,适配相关依赖库新版本。
* 测试用例支持`label`标签分类。
* 接口测试增加打印入参信息 [79](https://github.com/SeldomQA/seldom/issues/79) 。
* EdgeChromium浏览器支持`headless`模式。
* Web自动化测试增加元素截图`self.element_screenshot()`
* 优化HTML测试报告样式。
* 优化邮件模板样式。
__2.3.3(2021-11-12)__
* 增加 `assertNotText()` 断言方法 [75](https://github.com/SeldomQA/seldom/issues/75) 。
* 修复`main()`设置`rerun` 和 `save_last_run`参数,导致用例统计错误 [76](https://github.com/SeldomQA/seldom/issues/76) 。
__2.3.2(2021-11-08)__
* 接口调用如果是图片类型,不在打印内容。
* 增加`screenshot` 针对定位的元素截图, 用法`self.screenshot(id="xx")`。
* 测试报告:优化截图的样式。
* 发邮件功能,默认增加附件为测试报告。
__2.3.1(2021-11-02)__
* 修复`assertUrl()`、`assertInUrl()` 断言中文编码错误。
* 增加文件路径操作。
* `file_path()` 获取当前文件路径。
* `file_dir()` 获取当前文件目录。
* `file_dir_dir()` 获取当前文件目录的目录。
* `file_dir_dir_dir()` 获取当前文件目录的目录的目录。
* `init_env_path()` 添加路径到环境变量。
* 优化`main()` 方法中代码的执行顺序。
__2.3.0(2021-10-18)__
* 集成 `webdriver-manager`,不需要再单独安装浏览器驱动。
* seldom logo 显示版本号。
* 固定`selenium`版本号,暂没做`4.0.0`适配。
__2.2.4(2021-09-21)__
* 修复HTTP接口测试,指定`url`参数错误的问题。[71](https://github.com/SeldomQA/seldom/issues/71)
* 支持发送多人邮件。[72](https://github.com/SeldomQA/seldom/issues/72)
* 优化HTMLTestRunner, 重跑次数不记录为用例数。
* 修复pip安装缺少`description.rst` 问题。
__2.2.3(2021-08-27)__
* 支持控制台操作步骤显示在HTML报告中。[42](https://github.com/SeldomQA/seldom/issues/42)
* 修改`get_elements()`返回空列表。[69](https://github.com/SeldomQA/seldom/issues/69)
* 修复因为`colorama`/`emoji`导致的编码错误。[70](https://github.com/SeldomQA/seldom/issues/70)
__2.2.2(2021-08-13)__
* 优化db操作方法。
* 打印`logs`合并到 `reports` 目录。
__2.2.1(2021-06-30)__
* webdriver文件增加类型。
* 删除utils 错误代码。
* 修复:`diff_json()` 函数处理复杂数据报错 #66
* 修复:运行接口测试用例报 driver 错误 #68
* 修复:测试报告`popper.min.js` CDN 太慢的问题
__2.2.0(2021-06-15)__
* 增加接口测试方法`session`、`request`。
* 增加`seldom -h2c`参数,用于将har文件转成测试用例。
__2.1.1(2021-05-28)__
* 增加随机生成时间方法`get_past_time()`、`get_future_time()`
* 优化:截图方法`screenshots()`,可以在任意位置使用该方法生成截图,并显示在HTML测试报告中。
* 修复:接口测试`main()`中base_url 和 方法中的 url 同时存在的问题。
* 修复:优化MySQL数据库连接的问题。
* 修复:发送邮件时的错误。
* 修复:当`main()`中的timeout设置为1时,断言失败的问题。
__2.1.0(2021-05-19)__
* 增加数据库操作,同时支持`sqlite3`、`mysql`。
* 优化`file_data()`,兼容2.0.0用法。
__2.0.1(2021-05-07)__
* 优化 `file_data()`, 自动查找数据文件。
* 优化脚手架,创建项目例子更新。
__2.0.0(2021-04-24)__
* webdriver API 修改
* 移除 `self.get()`
* 增加 `self.visit()`
* 移除 `self.open_new_window()`
* 移除 `self.current_window_handle()`
* 移除 `self.new_window_handle()`
* 移除 `self.window_handles()`
* 修改 `self.switch_to_window()` 用法
* 优化打印日志,为每种操作加上 emoji
* 增加`expected_failure`用例装饰器,用于标记一条用例失败
* 增加 `file_dir()`, 返回当前文件所在目录的绝对路径。
* 运行完成自动通过浏览器打开HTML报告
* `main()`方法修改
* 修复`debug`参数类型错误异常提示
* 控制台更换字符logo*
* 整合 webdriver/request
* 上线 readthedocs 文档
__2.0.0.beta(2021-03-24)__
* 支持 HTTP接口测试
### seldom 1.x
__1.10.3(2021-03-23)__
...
__1.10.2(2021-03-13)__
* HTMLTestRunner代码优化
* 修复bug
__1.10.1(2021-03-04)__
* webdriver代码重构
* 修复严重bug
__1.10.0(2021-01-29)__
* 增加断言元素方法:`assertElement`、`assertNotElement`
* 增加单个测试类、用例执行的方法
* 修复报告样式bug
* 命令行工具优化
__1.9.0(2020-12-19)__
* 测试报告重构
* 用例描述单独一列
* 增加单个用例运行时间
* 新的报告样式
* 脚手架工具创建项目更新
* 增加随机生成手机号方法
__1.8.0(2020-11-17)__
* 增加用例依赖装饰器
__1.7.2(2020-10-10)__
* bug修复版本
__1.7.0(2020-09-21)__
* 重构浏览器驱动,开放浏览器可配置能力。
__1.6.0(2020-08-24)__
* 浏览器增加简写
* 支持 logs 日志
* 支持 XML 测试报告
* 增加 file_data 方法实现参数化。
* 修复一些bug
__1.5.6(2020-07-24)__
* 封装test fixture方法
__1.5.5(2020-06-29)__
* 修改HTMLTestRunner 错误日志的展示
* 增加mobile web的支持
__1.5.4(2020-06-04)__
* 增加keys键盘操作
* 元素操作增加聚焦
* debug 模式增加慢操作
__1.5.3(2020-05-31)__
* 修复bug
* 增加 yaml_to_list()方法
__1.5.2(202x-05-16)__
* 修复bug
__1.5.1 (2020-05-14)__
* 修复日志重复打印问题
* 修复测试报告不截图问题
* 日志增加emoji表情
__1.5.0(2020-04-29)__
* 自动化运行过程中,对操作的元素加边框,使其更醒目。
* 去掉对 `setUpClass()`方法的占用,代码做了较大重构。
* 在使用poium时,驱动的获取方式改变,这一点不向下兼容。
__1.2.6 (2020-04-22)__
* 完善自动化发邮件功能
* 增加 type_enter() 方法
* 优化项目的代码的调用
* 修复 seldom + poium 日志问题
__1.2.5(2020-04-13)__
* 重新定制测试报告样式
* seldom.main()增加timeout参数
__1.2.4(2020-03-19)__
* 增加数据解析相关操作方法
* 增加跳过测试相关方法
* 增加发邮件功能
* 修复bug, 优化代码
__1.2.3(2020-03-11)__
* 增加 slow_click() 方法。
* seldom.main() 默认运行当前文件不需要传参。
* seldom.main(report="report-name.html") 允许自定义报告名称。
__1.2.2(2020-03-03)__
* fix bug
* add function: csv_to_list()/ excel_to_list()
__1.2.0(2020-02-01)__
Global launch browser
__1.1.0(2020-01-19)__
selenium grid support
Added safari support
__1.0.0(2020-01-04)__
The framework function has been basically improved. I'm glad to release version 1.0
### seldom 0.x
__0.3.6(2019-12-23)__
Add cookie manipulation APIs
Optimized element wait
__0.3.5(2019-12-06)__
Added chrome/firefox browser driver download command
Driver file path Settings are supported
__0.3.3(2019-11-30)__
add skip case
__0.3.2(2019-11-27)__
Added a switch to display the last rerun result
Optimized assertion method
__0.3.0(2019-11-21)__
Update element positioning
__0.2.0(2019-11-17)__
Change the project name to seldom
Introducing the poium test library,
### pyse
__0.1.5(2019-11-15)__
* Increased test case failure rerun
* Add use case failure screenshots
__0.0.9(2018-03-29)__
Simplifying API calls
__0.0.8(2017-11-23)__
add parameterized
Beautification test report
__0.0.7(2016-11-09)__
Re based on unittest.
__0.0.6(2016-04-29)__
add setup.py file, Specification of the installation process, a time to install all dependencies.
Delete unnecessary files
__0.0.5__
Increase the support of multiple positioning methods
__0.0.4__
Method to add default to wait.
Modify the realization of the individual methods
__0.0.3.1 version update__
* Repair part bug.
__0.0.3 version update(2015-09-08)__
* With the nose instead of unittest.
* Discard HTMLTestRunner,Integrated nose-html-reporting.
* modify the examples under demo.
__0.0.2 version update(2015-09-08)__
* all the elements of the operation selector xpath replaced by css, css syntax because more concise.
* when you run the test case no longer need to specify the directory, the default directory for the current test.
* modify the examples under demo.
================================================
FILE: docs/vpdocs/web-testing/browser_driver.md
================================================
# 浏览器与驱动
### 管理浏览器驱动
> seldom 2.3.0 版本集成webdriver_manager管理浏览器驱动。
>
> seldom 3.3.0 版本移除了webdriver_manager,selenium 4.6 之后内置了 selenium-manager 可以自动管理浏览器驱动。
#### 自动下载
如果你不配置浏览器驱动也没关系,seldom(selenium)会根据你使用的浏览器版本,自动化下载对应的驱动文件。
* 编写简单的用例
```python
import seldom
class BingTest(seldom.TestCase):
def test_bing_search(self):
"""selenium api"""
self.open("http://www.bing.com")
self.type(id_="sb_form_q", text="seldom", enter=True)
self.sleep(2)
self.assertTitle("seldom - 搜索")
if __name__ == '__main__':
seldom.main(browser="edge", debug=True)
```
selenium驱动检查逻辑:
1. 首先判断 环境变量 `PATH` 是否配置了浏览器驱动。 通过`where` 查找命令位置,如果可以找到说明,已配置了,环境变量`PATH`。
```shell
> where msedgedriver
D:\webdriver\msedgedriver.exe
```
2. 如果没有找到浏览器驱动,会根据当前浏览器版本,查找对应驱动文件下载。 `selenium-manager` 可以查看浏览器驱动的默认安装路径。
```shell
> selenium-manager --driver msedgedriver
INFO Driver path: C:\Users\xxx\.cache\selenium\msedgedriver\win64\116.0.1938.76\msedgedriver.exe
INFO Browser path: C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe
```
#### 手动下载
通过 `selenium-manager` 命令下载浏览器驱动,需要知道每个浏览器驱动的名字。
```shell
> selenium-manager --driver chromedriver # chrome
> selenium-manager --driver msedgedriver # edge
> selenium-manager --driver geckodriver # firefox
```
### 指定浏览器驱动
虽然,`selenium-manager`可以方便的管理浏览器驱动,但`selenium-manager`自动下载浏览器驱动很慢,有些环境也不是方便。
> seldom 3.7 版本重新支持 `executable_path` 参数,指定浏览器驱动。
```python
import seldom
# ……
if __name__ == '__main__':
browser = {
"browser": "chrome",
"executable_path": "D:\webdriver\chromedriver.exe", # 设置chrome浏览器驱动位置,其他浏览器类似。
}
seldom.main(browser=browser)
```
### 指定不同的浏览器
我们运行的自动化测试不可能只在一个浏览器下运行,我们分别需要在chrome、firefox浏览器下运行。在seldom中需要只需要修改一个配置即可。
```python
import seldom
# ……
if __name__ == '__main__':
seldom.main(browser="chrome") # chrome浏览器,默认值
seldom.main(browser="gc") # google chrome简写
seldom.main(browser="firefox") # firefox浏览器
seldom.main(browser="ff") # firefox简写
seldom.main(browser="edge") # edge浏览器
seldom.main(browser="safari") # safari浏览器
seldom.main(browser="ie") # internet explore浏览器
```
在`main()`方法中通过`browser`参数设置不同的浏览器,默认为`Chrome`浏览器。
### 控制浏览器启动和关闭
seldom 默认通过`seldom.main(browser="edge")`全局设置浏览器的启动和关闭,一般我们不需要关心浏览器的启动和关闭。
> seldom 3.9.0 支持手动控制浏览器的驱动和关闭。
* 每个用例启动和关闭浏览器。
```python
import seldom
class WebTestOne(seldom.TestCase):
"""case lunch browser"""
def start(self):
self.browser("edge")
def end(self):
self.quit()
def test_baidu(self):
self.open("http://www.baidu.com")
...
def test_bing(self):
self.open("http://cn.bing.com")
...
if __name__ == '__main__':
seldom.main()
```
* 每个类启动和关闭浏览器。
```python
import seldom
class WebTestTwo(seldom.TestCase):
"""class lunch browser"""
@classmethod
def start_class(cls):
cls.browser(cls, "gc")
@classmethod
def end_class(cls):
cls.quit(cls)
def test_baidu(self):
self.open("http://www.baidu.com")
...
def test_bing(self):
self.open("http://cn.bing.com")
...
if __name__ == '__main__':
seldom.main()
```
* 打开一个新的浏览器
seldom 默认会启动一个浏览器,在运行的过程中需要打开一个新的浏览器执行其他操作,可以使用`new_browser()`方法。
```python
import seldom
class WebTestNew(seldom.TestCase):
"""Web search test case"""
def test_new_browser(self):
# default browser
self.open("http://www.baidu.com")
self.Keys(css="#kw").input("seldom").enter()
self.sleep(2)
self.screenshots()
self.assertInTitle("seldom")
# open new browser
browser = self.new_browser()
browser.open("http://cn.bing.com")
browser.type(id_="sb_form_q", text="seldom", enter=True)
self.sleep(2)
browser.screenshots()
if __name__ == '__main__':
seldom.main(browser="edge")
```
================================================
FILE: docs/vpdocs/web-testing/chaining.md
================================================
# 链式调用
方法链接是一种技术,用于对同一个对象进行多个方法调用,只使用一次对象引用。
### 基本例子
先来看一下如何通过seldom使用链式调用编写Web测试用例。
```python
import seldom
from seldom import Steps
class BaiduTest(seldom.TestCase):
def test_search_one(self):
"""
百度搜索
"""
Steps(desc="百度搜索").open("http://www.baidu.com").find("#kw").type("seldom").find("#su").click()
self.assertInTitle("seldom")
def test_search_two(self):
"""
百度搜索
"""
s = Steps(desc="百度搜索")
s.open("http://www.baidu.com")
s.find("#kw").type("seldom").enter()
self.assertInTitle("seldom")
if __name__ == '__main__':
seldom.main(browser="gc", tester="虫师")
```
用例像链条一样将整个测试过程串联起来,当然,如果你讨厌换行符`\`,也可以将用例分成多次调用,总之,只要你愿意,可以将所有步骤都串联起来。
```python
import seldom
from seldom import Steps
class BaiduTest(seldom.TestCase):
def test_search_setting(self):
"""百度搜索设置"""
Steps(url="http://www.baidu.com", desc="百度搜索设置")
.open()
.find("#s-usersetting-top").click()
.find("#s-user-setting-menu > div > a.setpref").click().sleep(2)
.find('[data-tabid="advanced"]').click().sleep(2)
.find("#q5_1").click().sleep(2)
.find('[data-tabid="general"]').click().sleep(2)
.find_text("保存设置").click()
.alert().accept()
if __name__ == '__main__':
seldom.main(browser="gc", tester="虫师")
```
### Steps 类
`Steps` 类所提供的API 大部分和`Webidrver` 类保持一致,但考虑掉到链式的特点,命名上更体现`动作`。
__查找元素__
```python
from seldom import Steps
c = Steps()
c.find("#id")
c.find(".class")
c.find("[name=password]")
c.find("div > tr > td")
c.find("div", 1)
c.find("text=hao123")
c.find("text*=hao1")
c.find_text("新闻")
```
* find(): 只支持CSS定位,这几乎是最强大的定位方法了。 新的测试库`cypress`、`playwright` 默认也都是CSS定位。
* `text=` 用来定位文本,相当于`find_text()`。
* `test*=` 用例模糊定位文本。
* find_text(): 用于定位文本。
__操作方法__
```python
import seldom
from seldom import Steps
class TestCase(seldom.TestCase):
def test_chaining_api(self):
Steps(desc="chaining api")
.open("https://www.baidu.com")
.max_window()
.set_window(800, 600)
.find("css").clear()
.find("css").type("seldom")
.find("css").enter()
.find("css").submit()
.find("css").click()
.find("css").double_click()
.find("css").move_to_click()
.find("css").click_and_hold()
.find("css").switch_to_frame()
.find("css").select(value="")
.find("css").select(text="每页显示20条")
.find("css").select(index=2)
.switch_to_frame_out()
.switch_to_window(1)
.refresh()
.alert().accept()
.alert().dismiss()
.screenshots()
.element_screenshot()
.sleep(1)
.close()
.quit()
```
1. 基于元素定位的操作先调用`find()/find_text()`, 例如`type()`, `click()` 等。
2. `accept()/dismiss()` 是基于alert的操作。
### 控制浏览器启动和关闭
seldom 默认通过`seldom.main(browser="edge")`全局设置浏览器的启动和关闭,一般我们不需要关心浏览器的启动和关闭。
> seldom 3.9.0 支持手动控制浏览器的驱动和关闭。
* 每个用例启动和关闭浏览器。
```python
import seldom
from seldom import Steps
class WebTestChaining(seldom.TestCase):
"""test chaining API"""
def start(self):
self.step = Steps(browser="edge")
def end(self):
self.step.quit()
def test_baidu(self):
"""test baidu search"""
self.step.open("https://www.baidu.com").find("#kw").type("seldom").find("#su").click().sleep(2)
self.assertInTitle("seldom")
def test_bing(self):
"""test bing search"""
self.step.open("https://www.bing.com").find("#sb_form_q").type("seldomqa").submit().sleep(2)
self.assertInTitle("seldomqa")
if __name__ == '__main__':
seldom.main()
```
================================================
FILE: docs/vpdocs/web-testing/other.md
================================================
# 浏览器启动配置
selenium 在启动浏览器的时候可以做很多配置,seldom 试图简化这些配置,但是总有很多情况兼顾不到。
> `seldom 3.2` 版本开放了这些配置,你只需要将配置传给 seldom 即可。
### 使用headless模式
Firefox和Chrome浏览器支持`headless`模式,将浏览器置于后台运行,这样不会影响到我们在测试机上完成其他工作。
* chrome
```python
import seldom
from selenium.webdriver import ChromeOptions
# ...
if __name__ == '__main__':
chrome_options = ChromeOptions()
chrome_options.add_argument("--headless=new") # 开启 headless 模式
browser = {
"browser": "chrome",
"options": chrome_options
}
seldom.main(browser=browser)
```
* firefox
```python
import seldom
from selenium.webdriver import FirefoxOptions
# ...
if __name__ == '__main__':
firefox_options = FirefoxOptions()
firefox_options.add_argument("-headless") # 开启 headless 模式
browser = {
"browser": "firefox",
"options": firefox_options
}
seldom.main(browser=browser)
```
* edge
```python
import seldom
from selenium.webdriver import EdgeOptions
# ...
if __name__ == '__main__':
edge_option = EdgeOptions()
edge_option.add_argument("--headless=new")
browser = {
"browser": "edge",
"options": edge_option
}
seldom.main(browser=browser)
```
### Selenium Grid
首先,安装Java环境,然后下载 `selenium-server`。
__Standalone__
独立运行,只需要启动一个服务,默认端口`4444`。
```shell
> java -jar selenium-server-4.31.0.jar standalone
```
__Hub和Node__
Hub和Node是一种分布式模式,由Hub管理Node执行。
* 启动Hub主节点
```shell
> java -jar selenium-server-4.31.0.jar hub
```
* 启动Node分支节点
```shell
> java -jar selenium-server-4.31.0.jar node
```
* 启动远程Node节点
```shell
java -jar selenium-server-4.31.0.jar node --hub http://:4444
```
注:由于hub和远程node不同的主机,所以远程node需要指定Hub的IP地址(即``)
__Seldom使用__
下面是Seldom框架中如何指定 Selenium Server 地址来运行测试用例。
```python
import seldom
from selenium.webdriver import ChromeOptions
# ……
if __name__ == '__main__':
chrome_options = ChromeOptions()
browser = {
"options": chrome_options, # chrome浏览器配置,其他类似
"command_executor": "http://192.168.0.202:4444", # selenium server 地址
}
seldom.main(browser=browser)
```
* 设置远程节点,[selenium Grid doc](https://www.selenium.dev/documentation/grid/getting_started/)。
### Mobile Web 模式
seldom 还支持 Mobile web 模式:
```python
import seldom
from selenium.webdriver import ChromeOptions
# ...
if __name__ == '__main__':
chrome_options = ChromeOptions()
chrome_options.add_experimental_option("mobileEmulation", {"deviceName": "iPhone 8"})
browser = {
"browser": "chrome",
"options": chrome_options,
}
seldom.main(debug=True, browser=browser)
```
* deviceName: 指定移动设备的型号。
> 移动设备通过通过 浏览器开发者工具 查看,参考型号:
> 'iPhone 8', 'iPhone 8 Plus', 'iPhone SE', 'iPhone X', 'iPhone XR', 'iPhone 12 Pro',
'Pixel 2', 'Pixel XL', 'Pixel 5', 'Samsung Galaxy S8+', 'Samsung Galaxy S20 Ultra',
'iPad Air', 'iPad Pro', 'iPad Mini'。
### 浏览器忽略无效证书
```python
import seldom
from selenium.webdriver import ChromeOptions
# ...
if __name__ == '__main__':
chrome_options = ChromeOptions()
chrome_options.add_argument('--ignore-certificate-errors') # 忽略无效证书的问题
browser = {
"browser": "chrome",
"options": chrome_options,
}
seldom.main(browser=browser)
```
### 浏览器关闭沙盒模式
```python
import seldom
from selenium.webdriver import ChromeOptions
# ...
if __name__ == '__main__':
chrome_options = ChromeOptions()
chrome_options.add_argument('--no-sandbox') # 关闭沙盒模式
browser = {
"browser": "chrome",
"options": chrome_options,
}
seldom.main(browser=browser)
```
### 开启实验性功能
chrome开启实验性功能参数 `excludeSwitches`。
```python
import seldom
from selenium.webdriver import ChromeOptions
# ...
if __name__ == '__main__':
chrome_options = ChromeOptions()
chrome_options.add_experimental_option("excludeSwitches", ['enable-automation', 'enable-logging'])
browser = {
"browser": "chrome",
"options": chrome_options,
}
seldom.main(browser=browser)
```
### 设置浏览器代理
```python
import seldom
from selenium.webdriver import ChromeOptions
# ...
if __name__ == '__main__':
proxy = "127.0.0.1:1080" # 示例代理地址和端口
chrome_options = ChromeOptions()
chrome_options.add_argument(f"--proxy-server={proxy}")
browser = {
"browser": "chrome",
"options": chrome_options,
}
seldom.main(browser=browser)
```
### 连接已打开浏览器
* 查看浏览器安装位置
```shell
> selenium-manager.exe --browser edge
[2024-10-08T03:50:40.000Z INFO ] Driver path: C:\Users\xx\.cache\selenium\msedgedriver\win64\130.0.2849.13\msedgedriver.exe
[2024-10-08T03:50:40.000Z INFO ] Browser path: C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe
```
* 启动浏览器
```shell
msedge.exe --remote-debugging-port=9527 --user-data-dir="D:\webdriver\edge"
```
`--remote-debugging-port`: 浏览器远程调试端口。
`--user-data-dir`: 用户数据目录,创建一个空目录用于保存浏览器用户数据数据。
* 启动浏览器指定端口
```python
import seldom
from selenium.webdriver import EdgeOptions
# ...
if __name__ == '__main__':
option = EdgeOptions()
# 设置连接已打开浏览器
option.add_experimental_option("debuggerAddress", "127.0.0.1:9527")
browser = {
"browser": "edge",
"options": option
}
seldom.main(browser=browser)
```
================================================
FILE: docs/vpdocs/web-testing/page_object.md
================================================
# Page Object
seldom API 的设计理念是将元素操作和元素定位放到起,本身不太适合实现`Page object`设计模式。
[poium](https://github.com/SeldomQA/poium) 是`Page objects`设计模式最佳实践。
* pip 安装
```shell
> pip install poium
```
将 seldom 与 poium 结合使用。
```python
import seldom
from poium import Page, Element
class BaiduPage(Page):
"""baidu page"""
search_input = Element(id_="kw")
search_button = Element(id_="su")
class BaiduTest(seldom.TestCase):
"""Baidu search test case"""
def test_case(self):
"""
A simple test
"""
page = BaiduPage(self.driver, print_log=True)
page.open("https://www.baidu.com")
page.search_input.send_keys("seldom")
page.search_button.click()
self.assertTitle("seldom_百度搜索")
if __name__ == '__main__':
seldom.main(browser="chrome")
```
================================================
FILE: docs/vpdocs/web-testing/seldom_api.md
================================================
# Seldom API
### 查找元素
seldom 提供了8中定位方式,与Selenium保持一致。
* id_
* name
* class_name
* tag
* link_text
* partial_link_text
* css
* xpath
__使用方式__
```python
# **kwargs
self.type(id_="kw", text="seldom")
self.type(name="wd", text="seldom")
self.type(class_name="s_ipt", text="seldom")
self.type(tag="input", text="seldom")
self.type(xpath="//input[@id='kw']", text="seldom")
self.type(css="#kw", text="seldom")
self.click(link_text="hao123")
self.click(partial_link_text="hao")
# selector
self.type("id=kw", text="seldom")
self.type("name=wd", text="seldom")
self.type("class=s_ipt", text="seldom")
self.type("tag=input", text="seldom")
self.type("//input[@id='kw']", text="seldom") # xpath
self.type("#kw", text="seldom") # css
self.click("text=hao123")
self.click("text*=hao")
```
> seldom 3.10.0 引入新的 selector 定位,弱化了selenium/appium 的定位类型方式。
* `**kwargs` 和 `selector` 定位对比。
| 类型 | 定位 | **kwargs | selector |
|-----------------|----------------------|-----------------------------|---------------------------|
| selenium/appium | id | id_="id" | "id=id" |
| selenium | mame | name="name" | "name=name" |
| selenium/appium | class | class_name="class" | "class=class" |
| selenium | tag | tag="input" | "tag=input" |
| selenium | link_text | link_text="文字链接" | "text=文字链接" |
| selenium | partial_link_text | partial_link_text="文字链" | "text~=文字链" |
| selenium/appium | xpath | xpath="//*[@id='11']" | "//*[@id='11']" |
| selenium | css | css="input#id" | "input#id" |
| appium | ios_predicate | ios_predicate = "xx" | "ios_predicate=xx" |
| appium | ios_class_chain | ios_class_chain = "xx" | "ios_predicate=xx" |
| appium | android_uiautomator | android_uiautomator = "xx" | "android_uiautomator=xx" |
| appium | android_viewtag | android_viewtag = "xx" | "android_viewtag=xx" |
| appium | android_data_matcher | android_data_matcher = "xx" | "android_data_matcher=xx" |
| appium | android_view_matcher | android_view_matcher = "xx" | "android_view_matcher=xx" |
| appium | accessibility_id | accessibility_id = "xx" | "accessibility_id=xx" |
| appium | image | image = "xx" | "image=xx" |
| appium | custom | custom = "xx" | "custom=xx" |
__帮助信息__
* [CSS选择器](https://www.w3school.com.cn/cssref/css_selectors.asp)
* [xpath语法](https://www.w3school.com.cn/xpath/xpath_syntax.asp)
__使用下标__
有时候无法通过一种定位找到单个元素,那么可以通过`index`指定一组元素中的第几个。
```py
self.type(tag="input", index=7, text="seldom")
```
通过`tag="input"`匹配出一组元素, `index=7` 指定这一组元素中的第8个,`index`默认下标为`0`。
### 断言
seldom 提供了一组针对Web页面的断言方法。
__使用方法__
```python
# 断言标题是否等于"title"
self.assertTitle("title")
# 断言标题是否包含"title"
self.assertInTitle("title")
# 断言URL是否等于
self.assertUrl("url")
# 断言URL是否包含
self.assertInUrl("url")
# 断言页面包含“text”
self.assertText("text")
# 断言页面不包含“text”
self.assertNotText("text")
# 断言警告是否存在"text" 提示信息
self.assertAlertText("text")
# 断言元素是否存在
self.assertElement(css="#kw")
# 断言元素是否不存在
self.assertNotElement(css="#kwasdfasdfa")
```
__断言截图__
* 安装 pillow
```shell
pip install pillow
```
```python
# 断言截图
self.assertScreenshot(tolerance=0)
```
> 说明:
> tolerance: 允许比较的像素差,默认是0
> 第一次运行在 `reports\screenshots\`目录下生成`xx_base.png`基础图片,第二次运行生成`xx_diff.png` 图片进行对比。如果
`xx_base.png` 基础图片错误,请手动删除并重新运行。
### WebDriver API
seldom简化了selenium中的API,使操作Web页面更加简单。
大部分API都由`WebDriver`类提供:
```python
import seldom
class TestCase(seldom.TestCase):
def test_seldom_api(self):
# Accept warning box. -> Be removed in the future
self.accept_alert()
# Adds a cookie to your current session.
self.add_cookie({'name': 'foo', 'value': 'bar'})
# Adds a cookie to your current session.
cookie_list = [
{'name': 'foo', 'value': 'bar'},
{'name': 'foo', 'value': 'bar'}
]
self.add_cookies(cookie_list)
# Clear the contents of the input box.
self.clear(css="#el")
# It can click any text / image can be clicked
# Connection, check box, radio buttons, and even drop-down box etc..
self.click(css="#el")
# Mouse over the element.
self.move_to_element(css="#el")
# Click the element by the link text
self.click_text("新闻")
# Simulates the user clicking the "close" button in the titlebar of a popup window or tab.
self.close()
# Delete all cookies in the scope of the session.
self.delete_all_cookies()
# Deletes a single cookie with the given name.
self.delete_cookie('my_cookie')
# Dismisses the alert available. -> Be removed in the future
self.dismiss_alert()
# Double click element.
self.double_click(css="#el")
# Execute JavaScript scripts.
self.execute_script("window.scrollTo(200,1000);")
# Setting width and height of window scroll bar.
self.window_scroll(width=300, height=500)
# Setting width and height of element scroll bar.
self.element_scroll(css=".class", width=300, height=500)
# open url.
self.open("https://www.baidu.com")
# Gets the text of the Alert. -> Be removed in the future
alert_title = self.get_alert_text
# Execute Chrome Devtools Protocol command and get returned result
self.execute_cdp_cmd('Runtime.evaluate', {'expression': "alert('hello world')"})
# Gets the value of an element attribute.
self.get_attribute(css="#el", attribute="type")
# Returns information of cookie with ``name`` as an object.
self.get_cookie(name="kkk")
# Returns a set of dictionaries, corresponding to cookies visible in the current session.
self.get_cookies()
# Gets the element to display,The return result is true or false.
self.get_display(css="#el")
# Get a set of elements
self.get_element(css="#el", index=0)
# Get element text information.
self.get_text(css="#el")
# Get window title.
title = self.get_title
# Get the URL address of the current page.
url = self.get_url
# Gets the log for a given log type
logs = self.get_log("browser")
# Set browser window maximized.
self.max_window()
# open url.
self.open("https://www.baidu.com")
# Quit the driver and close all the windows.
self.quit()
# Refresh the current page.
self.refresh()
# Right click element.
self.right_click(css="#el")
# Saves a screenshots of the current window to a PNG image file.
self.screenshots() # Save to HTML report
self.screenshots('/Screenshots/foo.png') # Save to the specified directory
# Saves a element screenshot of the element to a PNG image file.
self.element_screenshot(css="#id") # Save to HTML report
self.element_screenshot(css="#id", file_path='/Screenshots/foo.png') # Save to the specified directory
"""
Constructor. A check is made that the given element is, indeed, a SELECT tag. If it is not,
then an UnexpectedTagNameException is thrown.
每页显示10条
每页显示20条
每页显示50条
"""
self.select(css="#nr", value='20')
self.select(css="#nr", text='每页显示20条')
self.select(css="#nr", index=2)
# Set browser window wide and high.
self.set_window(100, 200)
# Submit the specified form.
self.submit(css="#el")
# Switch to the specified frame.
self.switch_to_frame(css="#el")
# Switches focus to the parent context. If the current context is the top
# level browsing context, the context remains unchanged.
self.switch_to_frame_parent()
# Returns the current form machine form at the next higher level.
# Corresponding relationship with switch_to_frame () method.
self.switch_to_frame_out()
# Switches focus to the specified window.
# This switches to the new windows/tab (0 is the first one)
self.switch_to_window(1)
# Operation input box.
self.type(css="#el", text="selenium")
# Implicitly wait.All elements on the page.
self.wait(10)
# Setting width and height of window scroll bar.
self.window_scroll(width=300, height=500)
# alert operation. (seldom>=3.2.0)
text = self.alert.text
self.alert.accept()
self.alert.dismiss()
self.alert.send_keys("text")
```
### 键盘操作
有时候我们需要用到键盘操作,比如`Enter`,`Backspace`,`TAB`,或者`ctrl/command + a`、`ctrl/command + c`组合键操作,seldom提供了一组键盘操作。
__使用方法__
```py
import seldom
class Test(seldom.TestCase):
def test_key(self):
self.open("https://www.baidu.com")
# 输入 seldomm
self.Keys(css="#kw").input("seldomm")
# 删除多输入的一个m
self.Keys(id_="kw").backspace()
# 输入“教程”
self.Keys(id_="kw").input("教程")
# ctrl+a 全选输入框内容
self.Keys(id_="kw").select_all()
# ctrl+x 剪切输入框内容
self.Keys(id_="kw").cut()
# ctrl+v 粘贴内容到输入框
self.Keys(id_="kw").paste()
# 通过回车键来代替单击操作
self.Keys(id_="kw").enter()
# 支持组合操作
self.Keys(id_="kw").select_all().cut() # 全选剪切
self.Keys(id_="kw").select_all().delete() # 全选删除
if __name__ == '__main__':
seldom.main(browser="firefox", debug=True)
```
### 测试electron应用
Electron是一个使用 JavaScript、HTML 和 CSS 构建桌面应用程序的框架。 嵌入 Chromium 和 Node.js 到 二进制的 Electron 允许您保持一个
JavaScript代码代码库并创建 在Windows、macOS和Linux上运行的跨平台应用。
https://www.electronjs.org/
```python
import seldom
class DataDriverTest(seldom.TestCase):
def start(self):
# appium-desktop基于Electron开发的桌面应用
self.app_path = f"C:\Program Files\Appium Server GUI\Appium Server GUI.exe"
def test_case(self):
"""
Used tuple test data
"""
self.open_electron(app_path=self.app_path)
self.sleep(10)
self.switch_to_window(0)
self.Keys(css="#simpleHostInput").select_all().delete()
self.type(css="#simpleHostInput", text="127.0.0.1")
self.Keys(css="#simplePortInput").select_all().delete()
self.type(css="#simplePortInput", text="4724")
self.click(css="#startServerBtn")
self.sleep(5)
if __name__ == '__main__':
seldom.main(debug=True)
```
================================================
FILE: pyproject.toml
================================================
[project]
name = "seldom"
version = "3.14.2"
description = "Seldom automation testing framework based on unittest."
authors = [{ name = "bugmaster", email = "defnngj@gmail.com" }]
license = { file = "LICENSE" }
readme = "README.md"
requires-python = ">=3.9,<4.0"
keywords = ["seldom", "selenium", "appium", "requests", "unittest"]
classifiers = [
"Intended Audience :: Developers",
"Operating System :: MacOS :: MacOS X",
"Operating System :: Microsoft :: Windows",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
"Topic :: Software Development :: Testing",
]
dependencies = [
"appium-python-client>=5.2.0,<6.0.0",
"xtestrunner>=1.8.6",
"loguru~=0.7.0",
"openpyxl>=3.0.3,<4.0.0",
"pyyaml>=6.0,<7.0",
"jsonschema>=4.10.0,<5.0.0",
"jmespath>=0.10.0,<1.0.0",
"pymysql>=1.0.0,<2.0.0",
"genson~=1.2.2",
"typer>=0.24.0,<1.0.0",
"python-dateutil~=2.8.2",
"pycryptodome>=3.21.0,<4.0.0"
]
[project.urls]
Homepage = "https://seldomqa.github.io"
Repository = "https://github.com/SeldomQA/seldom"
Documentation = "https://seldomqa.github.io"
[project.scripts]
seldom = "seldom.cli:app"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
[[tool.poetry.source]]
name = "douban"
url = "https://pypi.doubanio.com/simple/"
================================================
FILE: requirements.txt
================================================
Appium-Python-Client>=5.2.0
XTestRunner>=1.8.6
loguru~=0.7.0
openpyxl>=3.0.3
pyyaml>=6.0
jsonschema>=4.10.0
jmespath>=0.10.0
pymysql>=1.0.0
genson~=1.2.2
typer>=0.24.0
python-dateutil~=2.8.2
pycryptodome>=3.21.0
================================================
FILE: seldom/__init__.py
================================================
#!/usr/bin/python
#
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from .case import TestCase
from .running.config import Seldom
from .running.loader_extend import SeldomTestLoader
from .running.runner import main, TestMainExtend
from .utils.send_extend import SMTP, DingTalk
from .webdriver_chaining import Steps
from .skip import *
from .driver import *
from .testdata.parameterization import *
__author__ = "bugmaster"
__version__ = "3.14.2"
__description__ = "Seldom is an automation testing framework based on unittest."
================================================
FILE: seldom/appdriver.py
================================================
"""
appium API
"""
import base64
from pathlib import Path
from typing import Any, Dict
from appium.webdriver.common.appiumby import AppiumBy
from appium.webdriver import Remote as AppiumRemote
from seldom.logging import log
from seldom.running.config import Seldom
from seldom.webdriver import WebDriver
class AppDriver(WebDriver):
"""
appium base API
"""
def __init__(self):
self.browser = AppiumRemote(command_executor=Seldom.app_server, options=Seldom.app_info,
extensions=Seldom.extensions)
Seldom.driver = self.browser
def background_app(self, seconds: int):
"""
Puts the application in the background on the device for a certain duration.
Args:
seconds: the duration for the application to remain in the background
"""
log.info(f"📱 background app {seconds}s")
self.browser.background_app(seconds=seconds)
return self
def is_app_installed(self, bundle_id: str) -> bool:
"""Checks whether the application specified by `bundle_id` is installed on the device.
Args:
bundle_id: the id of the application to query
Returns:
`True` if app is installed
"""
log.info(f"📱 is app installed: {bundle_id}")
return self.browser.is_app_installed(bundle_id=bundle_id)
def install_app(self, app_path: str, **options: Any):
"""Install the application found at `app_path` on the device.
Args:
app_path: the local or remote path to the application to install
Keyword Args:
replace (bool): [Android only] whether to reinstall/upgrade the package if it is
already present on the device under test. True by default
timeout (int): [Android only] how much time to wait for the installation to complete.
60000ms by default.
allowTestPackages (bool): [Android only] whether to allow installation of packages marked
as test in the manifest. False by default
useSdcard (bool): [Android only] whether to use the SD card to install the app. False by default
grantPermissions (bool): [Android only] whether to automatically grant application permissions
on Android 6+ after the installation completes. False by default
Returns:
Union['WebDriver', 'Applications']: Self instance
"""
log.info(f"📱 install app: {app_path}")
self.browser.install_app(app_path=app_path, **options)
return self
def remove_app(self, app_id: str, **options: Any):
"""Remove the specified application from the device.
Args:
app_id: the application id to be removed
Keyword Args:
keepData (bool): [Android only] whether to keep application data and caches after it is uninstalled.
False by default
timeout (int): [Android only] how much time to wait for the uninstall to complete.
20000ms by default.
Returns:
Union['WebDriver', 'Applications']: Self instance
"""
log.info(f"📱 remove app: {app_id}")
self.browser.remove_app(app_id=app_id, **options)
return self
def terminate_app(self, app_id: str, **options: Any) -> bool:
"""Terminates the application if it is running.
Args:
app_id: the application id to be terminates
Keyword Args:
`timeout` (int): [Android only] how much time to wait for the uninstall to complete.
500ms by default.
Returns:
True if the app has been successfully terminated
"""
log.info(f"📱 terminate app: {app_id}")
return self.browser.terminate_app(app_id=app_id, **options)
def activate_app(self, app_id: str):
"""Activates the application if it is not running
or is running in the background.
Args:
app_id: the application id to be activated
Returns:
Union['WebDriver', 'Applications']: Self instance
"""
self.browser.activate_app(app_id=app_id)
return self
def query_app_state(self, app_id: str) -> int:
"""Queries the state of the application.
Args:
app_id: the application id to be queried
Returns:
One of possible application state constants. See ApplicationState
class for more details.
"""
log.info(f"📱 query app state: {app_id}")
return self.browser.query_app_state(app_id=app_id)
def app_strings(self, language: str = None, string_file: str = None) -> Dict[str, str]:
"""Returns the application strings from the device for the specified
language.
Args:
language: strings language code
string_file: the name of the string file to query
Returns:
The key is string id and the value is the content.
"""
log.info(f"📱 app strings")
return self.browser.app_strings(language=language, string_file=string_file)
@staticmethod
def base64_image(image_path: str):
"""
jpg/png file to base64
:param image_path:
:return:
"""
file_path = Path(image_path)
if file_path.is_file() is False:
log.error("The file path does not exist.")
return
with open(image_path, 'rb') as png_file:
b64_data = base64.b64encode(png_file.read()).decode('UTF-8')
return b64_data
def click_image(self, image_path: str) -> None:
"""
click image
:param image_path:
:return:
"""
log.info(f"✅ image -> click.")
self.browser.update_settings({"getMatchedImageResult": True})
self.browser.update_settings({"fixImageTemplatescale": True})
b64 = self.base64_image(image_path)
self.browser.find_element(AppiumBy.IMAGE, b64).click()
def keyboard_search(self) -> None:
"""
appium API
App keyboard search key.
"""
log.info("🔍 keyboard search key.")
self.browser.execute_script('mobile: performEditorAction', {'action': 'search'})
================================================
FILE: seldom/appium_lab/__init__.py
================================================
"""
appium laboratory
"""
from seldom.logging import log
from seldom.appium_lab.action import Action
from seldom.appium_lab.find import FindByText
from seldom.appium_lab.keyboard import KeyEvent
class AppiumLab(Action, FindByText, KeyEvent):
"""
app state:
【0】 is not installed.
【1】 is not running.
【2】 is running in background or suspended.
【3】 is running in background.
【4】 is running in foreground. (number)
"""
def check_state(self, app_id: str) -> int:
"""
check app state
:param app_id:
:return:
"""
state = self.driver.query_app_state(app_id)
if state == 0:
log.info(f"{app_id} is not installed.")
elif state == 1:
log.info(f"{app_id} is not running.")
elif state == 2:
log.info(f"{app_id} is running in background or suspended.")
elif state == 3:
log.info(f"{app_id} is running in background.")
elif state == 4:
log.info(f"{app_id} is running in foreground.")
else:
log.info(f"{app_id} state of the unknown.")
return state
def launch_app(self, app_id: str) -> None:
"""launch app"""
log.info(f"launch App {app_id}")
self.switch_to_app()
state = self.check_state(app_id)
if state != 4:
self.driver.launch_app()
def close_app(self, app_id: str) -> None:
"""close app"""
log.info(f"close App {app_id}")
self.switch_to_app()
self.driver.close_app()
self.check_state(app_id)
================================================
FILE: seldom/appium_lab/action.py
================================================
"""
appium action
"""
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.actions import interaction
from selenium.webdriver.common.actions.action_builder import ActionBuilder
from selenium.webdriver.common.actions.pointer_input import PointerInput
from seldom.appium_lab.switch import Switch
from seldom.logging import log
class Action(Switch):
"""
Encapsulate basic actions: swipe, tap, etc
"""
def __init__(self, driver=None):
Switch.__init__(self, driver)
self.switch_to_app()
self._size = self.driver.get_window_size()
self.width = self._size.get("width") # {'width': 1080, 'height': 2028}
self.height = self._size.get("height") # {'width': 1080, 'height': 2028}
def size(self) -> dict:
"""
return screen resolution.
"""
log.info(f"screen resolution: {self._size}")
return self._size
def _perform_action(self, x_start: int, y_start: int, x_end: int, y_end: int):
"""
General method to perform actions.
"""
actions = ActionChains(self.driver)
actions.w3c_actions = ActionBuilder(self.driver, mouse=PointerInput(interaction.POINTER_TOUCH, "touch"))
actions.w3c_actions.pointer_action.move_to_location(x_start, y_start)
actions.w3c_actions.pointer_action.pointer_down()
actions.w3c_actions.pointer_action.move_to_location(x_end, y_end)
actions.w3c_actions.pointer_action.release()
actions.perform()
def tap(self, x: int, y: int, pause: float = 0.1, sleep: float = 2) -> None:
"""
Tap on the coordinates
:param x: x coordinates
:param y: y coordinates
:param pause: pause time
:param sleep: sleep time
:return:
"""
self.switch_to_app()
log.info(f"top x={x},y={y}.")
actions = ActionChains(self.driver)
actions.w3c_actions = ActionBuilder(self.driver, mouse=PointerInput(interaction.POINTER_TOUCH, "touch"))
actions.w3c_actions.pointer_action.move_to_location(x, y)
actions.w3c_actions.pointer_action.pointer_down()
actions.w3c_actions.pointer_action.pause(pause)
actions.w3c_actions.pointer_action.release()
actions.perform()
self.sleep(sleep)
def swipe_up(self, times: int = 1, upper: bool = False, interval_time: float = 1):
"""
swipe up
:param times: swipe times
:param upper: Keyboard screen occlusion, swipe only the upper half of the area.
:param interval_time: interval time
:return:
"""
self.switch_to_app()
log.info(f"⬆️ swipe up {times} times")
x_start = int(self.width / 2)
x_end = int(self.width / 2)
if upper is True:
self.height = (self.height / 2)
y_start = int((self.height / 3) * 2)
y_end = int((self.height / 3) * 1)
for _ in range(times):
self._perform_action(x_start, x_end, y_start, y_end)
self.sleep(interval_time)
def swipe_down(self, times: int = 1, upper: bool = False, interval_time: float = 1) -> None:
"""
swipe down
:param times: swipe times
:param upper: Keyboard screen occlusion, swipe only the upper half of the area.
:param interval_time: interval time
:return:
"""
self.switch_to_app()
log.info(f"⬇️ swipe down {times} times")
x_start = int(self.width / 2)
x_end = int(self.width / 2)
if upper is True:
self.height = (self.height / 2)
y_start = int((self.height / 3) * 1)
y_end = int((self.height / 3) * 2)
for _ in range(times):
self._perform_action(x_start, x_end, y_start, y_end)
self.sleep(interval_time)
def swipe_left(self, times: int = 1, width_percentage: float = 0.8, interval_time: float = 1):
"""
Swipe left
:param times: swipe times
:param width_percentage: Percentage of the screen width to swipe (default 80%)
:param interval_time: interval time
:return:
"""
self.switch_to_app()
log.info(f"⬅️ swipe left {times} times")
x_start = int(self.width * (1 - width_percentage / 2))
x_end = int(self.width * width_percentage / 2)
y_start = int(self.height / 2)
y_end = int(self.height / 2)
for _ in range(times):
self._perform_action(x_start, x_end, y_start, y_end)
self.sleep(interval_time)
def swipe_right(self, times: int = 1, width_percentage: float = 0.8, interval_time: float = 1):
"""
Swipe right
:param times: swipe times
:param width_percentage: Percentage of the screen width to swipe (default 80%)
:param interval_time: interval time
:return:
"""
self.switch_to_app()
log.info(f"➡️ swipe right {times} times")
x_start = int(self.width * width_percentage / 2)
x_end = int(self.width * (1 - width_percentage / 2))
y_start = int(self.height / 2)
y_end = int(self.height / 2)
for _ in range(times):
self._perform_action(x_start, x_end, y_start, y_end)
self.sleep(interval_time)
def drag_from_to(self, x_start: int, x_end: int, y_start: int, y_end: int, interval_time: float = 1):
"""
The x coordinates slide to the y coordinates
:param x_start:
:param x_end:
:param y_start:
:param y_end:
:param interval_time:
:return:
"""
self._perform_action(x_start, x_end, y_start, y_end)
self.sleep(interval_time)
================================================
FILE: seldom/appium_lab/android.py
================================================
from appium.options.android import UiAutomator2Options
from appium.options.android import EspressoOptions
from appium.options.common.base import T
from typing import Any, Dict
from seldom.running.config import Seldom
from seldom.logging import log
class UiAutomator2Options(UiAutomator2Options):
"""
Override UiAutomator2Options class methods
"""
def load_capabilities(self: T, caps: Dict[str, Any]) -> T:
"""Sets multiple capabilities"""
log.info(f"app info: {caps}")
for name, value in caps.items():
if name == "appPackage":
Seldom.app_package = value
self.set_capability(name, value)
return self
class EspressoOptions(EspressoOptions):
"""
Override EspressoOptions class methods
"""
def load_capabilities(self: T, caps: Dict[str, Any]) -> T:
"""Sets multiple capabilities"""
log.info(f"app info: {caps}")
for name, value in caps.items():
Seldom.app_package = value
self.set_capability(name, value)
return self
================================================
FILE: seldom/appium_lab/appium_service.py
================================================
import time
from seldom.logging import log
from seldom.utils import file
from appium.webdriver.appium_service import AppiumService as OriginalServer
class AppiumService(OriginalServer):
"""
appium service
"""
def __init__(self,
addr: str = "127.0.0.1",
port: str = "4723",
log: str = None,
use_plugins: str = None,
args: list = []):
super().__init__()
self.addr = addr
self.port = port
self.log = log
self.use_plugins = use_plugins
self.args = args
def start_service(self, **kwargs) -> None:
"""
start service
:param kwargs:
args = [
f'-p {self.port}',
f'-g {self.log_file_path}',
'--session-override',
'--log-timestamp',
'--session-override',
'--local-timezone',
'--allow-insecure chromedriver_autodownload',
]
:return:
"""
start_args = ['--address', self.addr, '--port', self.port]
if self.log is None:
now_time = str(time.time()).split(".")[0]
log_file = file.join(file.dir, f"appium_server_{now_time}.log")
for param in ["--log", log_file]:
start_args.append(param)
if self.use_plugins is not None:
start_args.append("--use-plugins")
start_args.append(self.use_plugins)
for param in self.args:
start_args.append(param)
log.info(f"🚀 launch appium server: {start_args}")
self.start(args=start_args, **kwargs)
if __name__ == '__main__':
# service = AppiumService()
service = AppiumService(use_plugins="iamges,ocr", args=["--allow-cors"])
service.start_service()
================================================
FILE: seldom/appium_lab/find.py
================================================
"""
find element by text
"""
from appium.webdriver.common.appiumby import AppiumBy
from seldom.appium_lab.switch import Switch
from seldom.logging import log
class FindByText(Switch):
"""
Find elements based on text
"""
@staticmethod
def __remove_unprintable_chars(string: str) -> str:
"""
remove unprintable chars
:param string: string
"""
return ''.join(x for x in string if x.isprintable())
def __find(self, class_name: str, attribute: str, text: str):
"""
find element
:param class_name: class name
:param attribute: attribute
:param text: text
:return:
"""
elems = self.driver.find_elements(AppiumBy.CLASS_NAME, class_name)
for _ in range(3):
if len(elems) > 0:
break
self.sleep(1)
for elem in elems:
if elem.get_attribute(attribute) is None:
continue
attribute_text = self.__remove_unprintable_chars(elem.get_attribute(attribute))
if text in attribute_text:
log.info(f'find -> {attribute_text}')
return elem
return None
def find_view(self, text: str = None, content_desc: str = None):
"""
Android: find view
:param text:
:param content_desc:
:return:
"""
self.switch_to_app()
if text is not None:
attribute = "text"
_text = text
elif content_desc is not None:
attribute = "content-desc"
_text = content_desc
else:
raise ValueError("parameter error, setting text/content_desc")
for _ in range(3):
elem = self.__find(class_name="android.view.View", attribute=attribute, text=_text)
if elem is not None:
break
self.sleep(1)
else:
raise ValueError(f"Unable to find -> {text}")
return elem
def find_edit_text(self, text: str):
"""
Android: find editText
:param text:
:return:
"""
self.switch_to_app()
for _ in range(3):
elem = self.__find(class_name="android.widget.EditText", attribute="text", text=text)
if elem is not None:
break
self.sleep(1)
else:
raise ValueError(f"Unable to find -> {text}")
return elem
def find_button(self, text: str = None, content_desc: str = None):
"""
Android: find button
:param text:
:param content_desc:
:return:
"""
self.switch_to_app()
if text is not None:
attribute = "text"
_text = text
elif content_desc is not None:
attribute = "content-desc"
_text = content_desc
else:
raise ValueError("parameter error, setting text/content_desc")
for _ in range(3):
elem = self.__find(class_name="android.widget.Button", attribute=attribute, text=_text)
if elem is not None:
break
self.sleep(1)
else:
raise ValueError(f"Unable to find -> {text}")
return elem
def find_text_view(self, text: str):
"""
Android: find TextView
:param text:
:return:
"""
self.switch_to_app()
for _ in range(3):
elem = self.__find(class_name="android.widget.TextView", attribute="text", text=text)
if elem is not None:
break
self.sleep(1)
else:
raise ValueError(f"Unable to find -> {text}")
return elem
def find_image_view(self, text: str):
"""
Android: find ImageView
:param text:
:return:
"""
self.switch_to_app()
for _ in range(3):
elem = self.__find(class_name="android.widget.ImageView", attribute="content-desc", text=text)
if elem is not None:
break
self.sleep(1)
else:
raise ValueError(f"Unable to find -> {text}")
return elem
def find_check_box(self, text: str):
"""
Android: find CheckBox
:param text:
:return:
"""
self.switch_to_app()
for _ in range(3):
elem = self.__find(class_name="android.widget.CheckBox", attribute="text", text=text)
if elem is not None:
break
self.sleep(1)
else:
raise ValueError(f"Unable to find -> {text}")
return elem
def find_static_text(self, text: str):
"""
iOS: find XCUIElementTypeStaticText
:param text:
:return:
"""
self.switch_to_app()
for _ in range(3):
elem = self.__find(class_name="XCUIElementTypeStaticText", attribute="name", text=text)
if elem is not None:
break
self.sleep(1)
else:
raise ValueError(f"Unable to find -> {text}")
return elem
def find_other(self, text: str):
"""
iOS: find XCUIElementTypeOther
:param text:
:return:
"""
self.switch_to_app()
for _ in range(3):
elem = self.__find(class_name="XCUIElementTypeOther", attribute="name", text=text)
if elem is not None:
break
self.sleep(1)
else:
raise ValueError(f"Unable to find -> {text}")
return elem
def find_text_field(self, text: str):
"""
iOS: find XCUIElementTypeTextField
:param text:
:return:
"""
self.switch_to_app()
for _ in range(3):
elem = self.__find(class_name="XCUIElementTypeTextField", attribute="name", text=text)
if elem is not None:
break
self.sleep(1)
else:
raise ValueError(f"Unable to find -> {text}")
return elem
def find_image(self, text: str):
"""
iOS: find XCUIElementTypeImage
:param text:
:return:
"""
self.switch_to_app()
for _ in range(3):
elem = self.__find(class_name="XCUIElementTypeImage", attribute="name", text=text)
if elem is not None:
break
self.sleep(1)
else:
raise ValueError(f"Unable to find -> {text}")
return elem
def find_ios_button(self, text: str):
"""
iOS: find XCUIElementTypeButton
:param text:
:return:
"""
self.switch_to_app()
for _ in range(3):
elem = self.__find(class_name="XCUIElementTypeButton", attribute="name", text=text)
if elem is not None:
break
self.sleep(1)
else:
raise ValueError(f"Unable to find -> {text}")
return elem
================================================
FILE: seldom/appium_lab/keyboard.py
================================================
"""
App keyboard operation
"""
from seldom.logging import log
keycodes = {
'0': 7,
'1': 8,
'2': 9,
'3': 10,
'4': 11,
'5': 12,
'6': 13,
'7': 14,
'8': 15,
'9': 16,
'A': 29,
'B': 30,
'C': 31,
'D': 32,
'E': 33,
'F': 34,
'G': 35,
'H': 36,
'I': 37,
'J': 38,
'K': 39,
'L': 40,
'M': 41,
'N': 42,
'O': 43,
'P': 44,
'Q': 45,
'R': 46,
'S': 47,
'T': 48,
'U': 49,
'V': 50,
'W': 51,
'X': 52,
'Y': 53,
'Z': 54,
',': 55,
'.': 56,
' ': 62,
'*': 17,
'#': 18,
'`': 68,
'-': 69,
'[': 71,
']': 72,
'\\': 73,
';': 74,
'/': 76,
'@': 77,
'=': 161,
'+': 157,
'NUM_LOCK': 143,
'CAPS_LOCK': 115,
'HOME': 4,
'BACK': 3,
'ENTER': 66,
}
class KeyEvent:
"""
KeyEvent:
https://developer.android.com/reference/android/view/KeyEvent
"""
def __init__(self, driver):
self.driver = driver
def key_text(self, text: str = ""):
"""
keyword input text.
:param text: input text
Usage:
key_text("Hello")
"""
if text == "":
return
log.info(f'input "{text}"')
for string in text:
keycode = keycodes.get(string.upper(), 0)
if keycode == 0:
raise KeyError(f"The '{string}' character is not supported")
if string.isupper():
self.driver.press_keycode(keycode, 64, 59)
else:
self.driver.keyevent(keycode)
def press_key(self, key: str):
"""
keyboard
:param key: keyword name
press_key("HOME")
"""
log.info(f'press key "{key}"')
keycode = keycodes.get(key.upper(), 0)
if keycode == 0:
raise KeyError(f"The '{key}' character is not supported")
self.driver.press_keycode(keycode)
def long_press_key(self, key: str):
"""
keyboard
:param key: keyword name
press_key("HOME")
"""
log.info(f'long press key "{key}"')
keycode = keycodes.get(key.upper(), 0)
if keycode == 0:
raise KeyError(f"The '{key}' character is not supported")
self.driver.long_press_keycode(keycode)
def back(self):
"""go back"""
log.info("go back")
self.driver.press_key("BACK")
def home(self):
"""press home"""
log.info("press home")
self.driver.press_key("HOME")
def hide_keyboard(self, key_name=None, key=None, strategy=None):
"""
Hides the software keyboard on the device.
In iOS, use `key_name` to press
a particular key, or `strategy`. In Android, no parameters are used.
Args:
key_name: key to press
key:
strategy: strategy for closing the keyboard (e.g., `tapOutside`)
"""
log.info("hide keyboard")
self.driver.hide_keyboard(key_name=key_name, key=key, strategy=strategy)
def is_keyboard_shown(self) -> bool:
"""Attempts to detect whether a software keyboard is present
Returns:
`True` if keyboard is shown
"""
ret = self.driver.is_keyboard_shown()
log.info(f"is keyboard shown: {ret}")
return ret
================================================
FILE: seldom/appium_lab/ocr_plugin.py
================================================
"""
Appium OCR plugin
help: https://github.com/jlipps/appium-ocr-plugin
"""
from appium.webdriver.webdriver import ExtensionBase
class OCRCommand(ExtensionBase):
def method_name(self):
return 'ocr_command'
def ocr_command(self, argument):
return self.execute(argument)['value']
def add_command(self):
add = ('post', '/session/$sessionId/appium/ocr')
return add
================================================
FILE: seldom/appium_lab/switch.py
================================================
"""
switch app context
"""
import time
from seldom.logging import log
from seldom.running.config import Seldom
class Switch:
"""
switch context by appium
"""
def __init__(self, driver=None):
self.driver = Seldom.driver
if driver is not None:
self.driver = driver
def context(self):
"""
Returns the current context of the current session.
"""
current_context = self.driver.current_context
all_context = self.driver.contexts
log.info(f"current context: {current_context}.")
log.info(f"all context: {all_context}.")
return current_context
def switch_to_app(self) -> None:
"""
Switch to native app.
"""
current_context = self.driver.current_context
if current_context != "NATIVE_APP":
log.info("🔀 switch to native app.")
self.driver.switch_to.context('NATIVE_APP')
def switch_to_web(self, context_name: str = None) -> None:
"""
Switch to web view.
"""
log.info("🔀 switch to webview.")
if context_name is not None:
self.driver.switch_to.context(context_name)
else:
all_context = self.driver.contexts
for context in all_context:
if "WEBVIEW" in context:
self.driver.switch_to.context(context)
break
else:
raise NameError("No WebView found.")
def switch_to_flutter(self) -> None:
"""
Switch to flutter app.
"""
current_context = self.driver.current_context
if current_context != "FLUTTER":
log.info("🔀 switch to flutter.")
self.driver.switch_to.context('FLUTTER')
def switch_to_ocr(self) -> None:
"""
Switch to OCR app.
help: https://github.com/jlipps/appium-ocr-plugin
"""
log.info("🔀 switch to OCR.")
self.driver.switch_to.context('OCR')
@staticmethod
def sleep(sec):
"""
python time.sleep()
:param sec:
"""
time.sleep(sec)
================================================
FILE: seldom/case.py
================================================
"""
seldom test case
"""
import pdb
import random
import inspect
import unittest
from time import sleep
from urllib.parse import unquote
from jsonschema import validate
from jsonschema.exceptions import ValidationError
from selenium.webdriver.common.by import By
from selenium.webdriver.remote.webdriver import WebDriver as SeleniumWebDriver
from appium.webdriver.webdriver import WebDriver as AppiumWebdriver
from seldom.webdriver import WebDriver
from seldom.appdriver import AppDriver
from seldom.running.config import Seldom
from seldom.logging import log
from seldom.logging.exceptions import NotFindElementError
from seldom.utils import diff_json, AssertInfo, jmespath
from seldom.request import HttpRequest, ResponseResult, formatting
from seldom.extend_lib.base_assert import log_assertions
class TestCase(unittest.TestCase, AppDriver, HttpRequest):
"""seldom TestCase class"""
def start_class(self):
"""
Hook method for setting up class fixture before running tests in the class.
"""
pass
def end_class(self):
"""
Hook method for deconstructing the class fixture after running all tests in the class.
"""
pass
@classmethod
def setUpClass(cls):
try:
if (Seldom.app_server is not None) and (Seldom.app_info is not None):
# lunch appium driver
AppDriver.__init__(cls)
elif isinstance(Seldom.driver, SeleniumWebDriver):
# init selenium driver
WebDriver.__init__(cls)
cls().start_class()
except BaseException as e:
log.error(f"start_class Exception: {e}")
@classmethod
def tearDownClass(cls):
try:
# close appium
if all([
Seldom.app_server is not None,
Seldom.app_info is not None,
isinstance(Seldom.driver, AppiumWebdriver),
Seldom.app_package is not None]
):
Seldom.driver.terminate_app(Seldom.app_package)
cls().end_class()
except BaseException as e:
log.error(f"end_class Exception: {e}")
def start(self):
"""
Hook method for setting up the test fixture before exercising it.
"""
pass
def end(self):
"""
Hook method for deconstructing the test fixture after testing it.
"""
pass
def setUp(self):
self.images = []
self.start()
def tearDown(self):
self.end()
@property
def driver(self):
"""
return browser driver (web)
"""
return Seldom.driver
@property
def device(self):
"""
return uiautomator2 driver (app)
"""
return Seldom.device
def browser(self, name: str) -> None:
"""
launch browser.
:param: browser name.
Usage:
self.browser()
self.browser(cls)
"""
try:
self.images
except AttributeError:
self.images = []
WebDriver.__init__(self, browser_name=name, images=self.images)
def quit(self) -> None:
"""
Quit browser
- quit the driver and close all the windows.
Usage:
self.quit()
self.quit(cls)
"""
if isinstance(self.browser, SeleniumWebDriver) is True:
self.browser.quit()
Seldom.driver = None
def new_browser(self) -> WebDriver:
"""
launch new browser
"""
wd = WebDriver(is_new=True, images=self.images)
return wd
def assertTitle(self, title: str = None, msg: str = None) -> None:
"""
Asserts whether the current title is in line with expectations.
Usage:
self.assertTitle("title")
"""
if title is None:
raise AssertionError("The assertion title cannot be empty.")
log.info(f"👀 assertTitle -> {title}.")
for _ in range(Seldom.timeout + 1):
try:
self.assertEqual(title, Seldom.driver.title)
break
except AssertionError:
sleep(1)
else:
self.assertEqual(title, Seldom.driver.title, msg=msg)
def assertInTitle(self, title: str = None, msg: str = None) -> None:
"""
Asserts whether the current title is in line with expectations.
Usage:
self.assertInTitle("title")
"""
if title is None:
raise AssertionError("The assertion title cannot be empty.")
log.info(f"👀 assertInTitle -> {title}.")
for _ in range(Seldom.timeout + 1):
try:
self.assertIn(title, Seldom.driver.title)
break
except AssertionError:
sleep(1)
else:
self.assertIn(title, Seldom.driver.title, msg=msg)
def assertUrl(self, url: str = None, msg: str = None) -> None:
"""
Asserts whether the current URL is in line with expectations.
Usage:
self.assertUrl("url")
"""
if url is None:
raise AssertionError("The assertion URL cannot be empty.")
log.info(f"👀 assertUrl -> {url}.")
current_url = unquote(Seldom.driver.current_url)
for _ in range(Seldom.timeout + 1):
try:
self.assertEqual(url, current_url)
break
except AssertionError:
sleep(1)
else:
self.assertEqual(url, current_url, msg=msg)
def assertInUrl(self, url: str = None, msg: str = None) -> None:
"""
Asserts whether the current URL is in line with expectations.
Usage:
self.assertInUrl("url")
"""
if url is None:
raise AssertionError("The assertion URL cannot be empty.")
log.info(f"👀 assertInUrl -> {url}.")
for _ in range(Seldom.timeout + 1):
current_url = unquote(Seldom.driver.current_url)
try:
self.assertIn(url, current_url)
break
except AssertionError:
sleep(1)
else:
self.assertIn(url, Seldom.driver.current_url, msg=msg)
def assertText(self, text: str = None, msg: str = None) -> None:
"""
Asserts whether the text of the current page conforms to expectations.
Usage:
self.assertText("text")
"""
if text is None:
raise AssertionError("The assertion text cannot be empty.")
elem = Seldom.driver.find_element(By.TAG_NAME, "html")
log.info(f"👀 assertText -> {text}.")
for _ in range(Seldom.timeout + 1):
if elem.is_displayed():
try:
self.assertIn(text, elem.text)
break
except AssertionError:
sleep(1)
else:
self.assertIn(text, elem.text, msg=msg)
def assertNotText(self, text: str = None, msg: str = None) -> None:
"""
Asserts that the current page does not contain the specified text.
Usage:
self.assertNotText("text")
"""
if text is None:
raise AssertionError("The assertion text cannot be empty.")
elem = Seldom.driver.find_element(By.TAG_NAME, "html")
log.info(f"👀 assertNotText -> {text}.")
for _ in range(Seldom.timeout + 1):
if elem.is_displayed():
try:
self.assertNotIn(text, elem.text)
break
except AssertionError:
sleep(1)
else:
self.assertNotIn(text, elem.text, msg=msg)
def assertAlertText(self, text: str = None, msg: str = None) -> None:
"""
Asserts whether the text of the current page conforms to expectations.
Usage:
self.assertAlertText("text")
"""
if text is None:
raise NameError("Alert text cannot be empty.")
log.info(f"👀 assertAlertText -> {text}.")
alert_text = Seldom.driver.switch_to.alert.text
for _ in range(Seldom.timeout + 1):
try:
self.assertEqual(alert_text, text, msg=msg)
break
except AssertionError:
sleep(1)
else:
self.assertEqual(alert_text, text, msg=msg)
def assertElement(self, index: int = 0, msg: str = None, **kwargs) -> None:
"""
Asserts whether the element exists.
Usage:
self.assertElement(css="#id")
"""
log.info("👀 assertElement.")
if msg is None:
msg = "No element found"
try:
self.get_element(index=index, **kwargs)
elem = True
except NotFindElementError:
elem = False
self.assertTrue(elem, msg=msg)
def assertNotElement(self, index: int = 0, msg: str = None, **kwargs) -> None:
"""
Asserts if the element does not exist.
Usage:
self.assertNotElement(css="#id")
"""
log.info("👀 assertNotElement.")
if msg is None:
msg = "Find the element"
timeout_backups = Seldom.timeout
Seldom.timeout = 2
try:
self.get_element(index=index, **kwargs)
elem = True
except NotFindElementError:
elem = False
Seldom.timeout = timeout_backups
self.assertFalse(elem, msg=msg)
def assertScreenshot(self, tolerance: int = 0, msg: str = None):
"""
Asserts if the element does not exist.
Usage:
self.assertScreenshot()
"""
from seldom.utils.match_image import assert_screenshot
log.info(f"👀 assertScreenshot.")
stack_t = inspect.stack()
ret = assert_screenshot(Seldom.driver, tolerance, stack_t)
if isinstance(ret, bool):
self.assertTrue(ret, msg=msg)
def assertStatusCode(self, status_code: int, msg: str = None) -> None:
"""
Asserts the HTTP status code
"""
log.info(f"👀 assertStatusCode -> {status_code}.")
self.assertEqual(ResponseResult.status_code, status_code, msg=msg)
def assertStatusOk(self, msg: str = None) -> None:
"""
Asserts the HTTP status code is 200
"""
log.info(f"👀 assertStatusOK -> 200.")
self.assertEqual(ResponseResult.status_code, 200, msg=msg)
def assertSchema(self, schema, response=None) -> None:
"""
Assert JSON Schema
doc: https://json-schema.org/
"""
log.info(f"👀 assertSchema -> {formatting(schema)}.")
if response is None:
response = ResponseResult.response
try:
validate(instance=response, schema=schema)
except ValidationError as msg:
self.assertEqual("Response data", "Schema data", msg)
def assertJSON(self, assert_json, response=None, exclude=None) -> None:
"""
Assert JSON data
"""
log.info(f"👀 assertJSON -> {assert_json}.")
if response is None:
response = ResponseResult.response
AssertInfo.warning = []
AssertInfo.error = []
diff_json(response, assert_json, exclude)
if len(AssertInfo.warning) != 0:
log.warning(AssertInfo.warning)
if len(AssertInfo.error) != 0:
self.assertEqual("Response data", "Assert data", msg=AssertInfo.error)
def assertPath(self, path, value) -> None:
"""
Assert path data
doc: https://jmespath.org/
"""
log.info(f"👀 assertPath -> {path} >> {value}.")
search_value = jmespath(ResponseResult.response, path)
self.assertEqual(search_value, value)
def assertInPath(self, path, value) -> None:
"""
Assert path data
doc: https://jmespath.org/
"""
log.info(f"👀 assertInPath -> {path} >> {value}.")
search_value = jmespath(ResponseResult.response, path)
self.assertIn(value, search_value)
def xSkip(self, reason):
"""
Skip this test.
:param reason:
Usage:
if data is None:
self.xSkip("data is None.")
"""
self.skipTest(reason)
def xFail(self, msg):
"""
Fail immediately, with the given message
:param msg:
Usage:
if data is None:
self.xFail("This case fails.")
"""
self.fail(msg)
@staticmethod
def sleep(sec: int | tuple = 1) -> None:
"""
Usage:
self.sleep(seconds)
"""
if isinstance(sec, tuple):
sec = random.randint(sec[0], sec[1])
log.info(f"💤️ sleep: {sec}s.")
sleep(sec)
@staticmethod
def pause() -> None:
"""
pause. type 'c', and press [Enter] continue run
Usage:
self.pause()
"""
log.info(f"⏸️ pause. type 'c', and press [Enter] continue run.")
pdb.set_trace()
@log_assertions
def assertEqual(self, first, second, msg=None):
super().assertEqual(first, second, msg)
@log_assertions
def assertNotEqual(self, first, second, msg=None):
super().assertNotEqual(first, second, msg)
@log_assertions
def assertTrue(self, expr, msg=None):
super().assertTrue(expr, msg)
@log_assertions
def assertFalse(self, expr, msg=None):
super().assertFalse(expr, msg)
@log_assertions
def assertIn(self, member, container, msg=None):
super().assertIn(member, container, msg)
@log_assertions
def assertNotIn(self, member, container, msg=None):
super().assertNotIn(member, container, msg)
@log_assertions
def assertIsInstance(self, obj, cls, msg=None):
super().assertIsInstance(obj, cls, msg)
@log_assertions
def assertNotIsInstance(self, obj, cls, msg=None):
super().assertNotIsInstance(obj, cls, msg)
@log_assertions
def assertRegex(self, text, regex, msg=None):
super().assertRegex(text, regex, msg)
@log_assertions
def assertNotRegex(self, text, regex, msg=None):
super().assertNotRegex(text, regex, msg)
@log_assertions
def assertAlmostEqual(self, first, second, places=None, msg=None, delta=None):
super().assertAlmostEqual(first, second, places, msg, delta)
@log_assertions
def assertNotAlmostEqual(self, first, second, places=None, msg=None, delta=None):
super().assertNotAlmostEqual(first, second, places, msg, delta)
@log_assertions
def assertGreater(self, a, b, msg=None):
super().assertGreater(a, b, msg)
@log_assertions
def assertGreaterEqual(self, a, b, msg=None):
super().assertGreaterEqual(a, b, msg)
@log_assertions
def assertLess(self, a, b, msg=None):
super().assertLess(a, b, msg)
@log_assertions
def assertLessEqual(self, a, b, msg=None):
super().assertLessEqual(a, b, msg)
@log_assertions
def assertCountEqual(self, a, b, msg=None):
super().assertCountEqual(a, b, msg)
================================================
FILE: seldom/cli.py
================================================
"""
seldom CLI
"""
import json
import os
import sys
from pathlib import Path
import typer
# Import only the absolute minimum at module level
app = typer.Typer(help="seldom CLI.")
# Current file and directory
current_file = Path(__file__).resolve()
current_dir = current_file.parent
# Functions for lazy importing common modules
def _import_seldom_core():
"""Lazy import of seldom core modules"""
import ssl
# Set ssl context
ssl._create_default_https_context = ssl._create_unverified_context
import seldom
from seldom.running.config import Seldom
from seldom import SeldomTestLoader, TestMainExtend
from seldom.logging import log, log_cfg
from seldom.utils import file, cache
from seldom.running.loader_hook import loader
return {
'seldom': seldom,
'Seldom': Seldom,
'SeldomTestLoader': SeldomTestLoader,
'TestMainExtend': TestMainExtend,
'log': log,
'log_cfg': log_cfg,
'file': file,
'cache': cache,
'loader': loader
}
def _import_har_parser():
"""Lazy import of HarParser"""
from seldom.har2case.core import HarParser
return HarParser
def _import_swagger_parser():
"""Lazy import of SwaggerParser"""
from seldom.swagger2case.core import SwaggerParser
return SwaggerParser
def _import_file_running_config():
"""Lazy import of FileRunningConfig"""
from seldom.running.config import FileRunningConfig
return FileRunningConfig
@app.command()
def main(
version: bool = typer.Option(None, "-v", "--version", help="Show version."),
project_api: str = typer.Option(None, "-api", "--project-api", help="Create a project of API type."),
project_app: str = typer.Option(None, "-app", "--project-app", help="Create a project of App type"),
project_web: str = typer.Option(None, "-web", "--project-web", help="Create a project of Web type"),
clear_cache: bool = typer.Option(False, "-cc", "--clear-cache", help="Clear all caches of seldom."),
log_level: str = typer.Option(None, "-ll", "--log-level",
help="Set the log level [TRACE |DEBUG | INFO | SUCCESS | WARNING | ERROR].",
case_sensitive=False, show_choices=True),
mod: str = typer.Option(None, "-m", "--mod",
help="Run tests modules, classes or even individual test methods from the command line."),
path: str = typer.Option(None, "-p", "--path", help="Run test case file path."),
env: str = typer.Option(None, "-e", "--env", help="Set the Seldom run environment `Seldom.env`."),
browser: str = typer.Option(None, "-b", "--browser",
help="The browser that runs the Web UI automation tests [chrome | edge | firefox | chromium]. Need the --path."),
base_url: str = typer.Option(None, "-u", "--base-url",
help="The base-url that runs the HTTP automation tests. Need the --path."),
debug: bool = typer.Option(False, "-d", "--debug", help="Debug mode. Need the --path/--mod."),
rerun: int = typer.Option(0, "-rr", "--rerun",
help="The number of times a use case failed to run again. Need the --path."),
report: str = typer.Option(None, "-r", "--report", help="Set the test report for output. Need the --path."),
collect: bool = typer.Option(False, "-c", "--collect", help="Collect project test cases. Need the --path."),
level: str = typer.Option("data", "-l", "--level",
help="Parse the level of use cases [data | case]. Need the --path."),
case_json: str = typer.Option(None, "-j", "--case-json", help="Test case files. Need the --path."),
har2case: str = typer.Option(None, "-h2c", "--har2case", help="HAR file converts an seldom test case."),
swagger2case: str = typer.Option(None, "-s2c", "--swagger2case",
help="Swagger file converts an seldom test case."),
api_excel: str = typer.Option(None, help="Run the api test cases in the excel file."),
):
"""
seldom CLI.
"""
# For simple commands (like --help and --version), return directly without importing any modules
if version:
from seldom import __version__
typer.echo(f"seldom version: {__version__}")
return typer.Exit()
# Check if this is a --help call or no arguments provided (will trigger help)
if len(sys.argv) <= 2 or (len(sys.argv) == 3 and (sys.argv[2] in ['--help', '-h'])):
# Return 0 to let Typer display help message without importing anything
return 0
# Import modules only when core functionality commands are needed
core_commands = [project_api, project_app, project_web, clear_cache, log_level, path, mod, har2case, swagger2case,
api_excel]
if any(core_commands):
# Import core modules
core = _import_seldom_core()
log = core['log']
log_cfg = core['log_cfg']
cache = core['cache']
file = core['file']
loader = core['loader']
seldom = core['seldom']
Seldom = core['Seldom']
SeldomTestLoader = core['SeldomTestLoader']
TestMainExtend = core['TestMainExtend']
else:
# If no commands are specified, Typer will automatically display help information
return 0
if project_api:
create_scaffold(project_api, "api", log)
return 0
if project_app:
create_scaffold(project_app, "app", log)
return 0
if project_web:
create_scaffold(project_web, "web", log)
return 0
if clear_cache:
cache.clear()
if log_level:
allowed_levels = ["TRACE", "DEBUG", "INFO", "SUCCESS", "WARNING", "ERROR"]
if log_level.upper() not in allowed_levels:
typer.echo(f"Invalid log level: {log_level}. Choose from {allowed_levels}")
raise typer.Exit(code=1)
log_cfg.set_level(level=log_level.upper())
# check hook function(confrun.py)
if browser is None:
browser = loader("browser") if loader("browser") is not None else browser
if base_url is None:
base_url = loader("base_url") if loader("base_url") is not None else base_url
if debug is False:
debug = loader("debug") if loader("debug") is not None else debug
if rerun is None:
rerun = loader("rerun") if loader("rerun") is not None else rerun
if report is None:
report = loader("report") if loader("report") is not None else report
timeout = loader("timeout") if loader("timeout") is not None else 10
app_server = loader("app_server") if loader("app_server") is not None else None
app_info = loader("app_info") if loader("app_info") is not None else None
title = loader("title") if loader("title") is not None else "Seldom Test Report"
tester = loader("tester") if loader("tester") is not None else "Anonymous"
description = loader("description") if loader("description") is not None else "Test case execution"
language = loader("language") if loader("language") is not None else "en"
whitelist = loader("whitelist") if loader("whitelist") is not None else []
blacklist = loader("blacklist") if loader("blacklist") is not None else []
extensions = loader("extensions") if loader("extensions") is not None else None
failfast = loader("failfast") if loader("failfast") is not None else False
if path:
Seldom.env = env
if collect is True and case_json is not None:
typer.echo(f"Collect use cases for the {path} directory.")
if os.path.isdir(path) is True:
typer.echo(f"Add Env Path: {os.path.dirname(path)}.")
file.add_to_path(os.path.dirname(path))
SeldomTestLoader.collectCaseInfo = True
loader("start_run")
main_extend = TestMainExtend(path=path)
case_info = main_extend.collect_cases(json=True, level=level, warning=True)
case_path = os.path.join(os.getcwd(), case_json)
with open(case_path, "w", encoding="utf-8") as json_file:
json_file.write(case_info)
loader("end_run")
typer.echo(f"Save them to {case_path}")
return 0
if collect is False and case_json is not None:
typer.echo(f"Read the {case_json} case file for execution in the {path} directory")
if os.path.exists(case_json) is False:
typer.echo(f"The run case file {case_json} does not exist.")
return 0
if os.path.isdir(path) is False:
typer.echo(f"The run case path {path} does not exist.")
return 0
typer.echo(f"Add Env Path: {os.path.dirname(path)}.")
file.add_to_path(os.path.dirname(path))
with open(case_json, encoding="utf-8") as json_file:
case = json.load(json_file)
path, case = reset_case(path, case)
main_extend = TestMainExtend(
path=path, browser=browser, base_url=base_url, debug=debug, timeout=timeout,
app_server=app_server, app_info=app_info, report=report, title=title, tester=tester,
description=description, rerun=rerun, language=language,
whitelist=whitelist, blacklist=blacklist, extensions=extensions)
main_extend.run_cases(case)
return 0
seldom.main(
path=path, browser=browser, base_url=base_url, debug=debug, timeout=timeout,
app_server=app_server, app_info=app_info, report=report, title=title, tester=tester,
description=description, rerun=rerun, language=language,
whitelist=whitelist, blacklist=blacklist, extensions=extensions, failfast=failfast)
return 0
if mod:
file_dir = os.getcwd()
sys.path.insert(0, file_dir)
seldom.main(
case=mod, browser=browser, base_url=base_url, debug=debug, timeout=timeout,
app_server=app_server, app_info=app_info, report=report, title=title, tester=tester,
description=description, rerun=rerun, language=language,
whitelist=whitelist, blacklist=blacklist, extensions=extensions, failfast=failfast)
return 0
if har2case:
HarParser = _import_har_parser()
har_parser = HarParser(har2case)
har_parser.gen_testcase()
return 0
if swagger2case:
SwaggerParser = _import_swagger_parser()
sp = SwaggerParser(swagger=swagger2case)
sp.gen_testcase()
return 0
if api_excel:
typer.echo(f"Run {api_excel} file.")
if Path(api_excel).exists() is False:
raise FileNotFoundError(f"{api_excel} file does not exist")
FileRunningConfig = _import_file_running_config()
FileRunningConfig.api_excel_file_name = api_excel
script_path = file.join(file.dir, "file_runner", "api_excel.py")
seldom.main(
path=script_path, base_url=base_url, debug=debug, timeout=timeout,
report=report, title=title, tester=tester,
description=description, rerun=rerun, language=language, failfast=failfast)
return 0
return 0
def create_scaffold(project_name: str, project_type: str, log) -> None:
"""
create scaffold with specified project name.
"""
if os.path.isdir(project_name):
log.info(f"Folder {project_name} exists, please specify a new folder name.")
return
log.info(f"Start to create new test project: {project_name}")
log.info(f"CWD: {os.getcwd()}\n")
def create_folder(path):
"""Create folder"""
os.makedirs(path)
log.info(f"📁 Created folder: {path}")
def create_file(path, file_content=""):
"""Create file"""
with open(path, 'w', encoding="utf-8") as py_file:
py_file.write(file_content)
log.info(f"📄 Created file: {path}")
data_path = current_dir / "project_temp" / "data.json"
confrun_path = current_dir / "project_temp" / project_type / "confrun.py"
run_path = current_dir / "project_temp" / project_type / "run.py"
sample_path = current_dir / "project_temp" / project_type / "test_sample.py"
test_data = data_path.read_text(encoding='utf-8')
confrun_test = confrun_path.read_text(encoding='utf-8')
run_test = run_path.read_text(encoding='utf-8')
test_web_sample = sample_path.read_text(encoding='utf-8')
create_folder(project_name)
create_folder(os.path.join(project_name, "test_dir"))
create_folder(os.path.join(project_name, "reports"))
create_folder(os.path.join(project_name, "test_data"))
create_file(os.path.join(project_name, "test_data", "data.json"), test_data)
create_file(os.path.join(project_name, "test_dir", "__init__.py"))
create_file(os.path.join(project_name, "test_dir", "test_sample.py"), test_web_sample)
create_file(os.path.join(project_name, "confrun.py"), confrun_test)
create_file(os.path.join(project_name, "run.py"), run_test)
log.info(f"🎉 Project '{project_name}' created successfully.")
log.info(f"👉 Go to the project folder and run 'python run.py' to start testing.")
def reset_case(path: str, cases: list) -> tuple[str, list]:
"""
Reset the use case data
:param path: case base path
:param cases: case data
"""
if len(cases) == 0:
return path, cases
for case in cases:
if "." not in case["file"]:
return path, cases
case_start = cases[0]["file"].split(".")[0]
for case in cases:
if case["file"].startswith(f"{case_start}.") is False:
break
else:
path = os.path.join(path, case_start)
for case in cases:
case["file"] = case["file"][len(case_start) + 1:]
return path, cases
return path, cases
if __name__ == "__main__":
app()
================================================
FILE: seldom/db_operation/__init__.py
================================================
from .sqlite_db import SQLiteDB
from .mysql_db import MySQLDB
================================================
FILE: seldom/db_operation/base_db.py
================================================
"""
SQL API
"""
class SQLBase:
"""SQL base API"""
@staticmethod
def dict_to_str(data: dict) -> str:
"""
dict to set str
"""
tmp_list = []
for key, value in data.items():
if value is None:
tmp = f"{key}=null"
elif isinstance(value, int):
tmp = f"{key}={value}"
else:
tmp = f"{key}='{value}'"
tmp_list.append(tmp)
return ','.join(tmp_list)
@staticmethod
def dict_to_str_and(conditions: dict) -> str:
"""
dict to where and str
"""
tmp_list = []
for key, value in conditions.items():
if value is None:
tmp = f"{key}=null"
elif isinstance(value, int):
tmp = f"{key}={value}"
else:
tmp = f"{key}='{value}'"
tmp_list.append(tmp)
return ' and '.join(tmp_list)
def delete(self, table: str, where: dict = None) -> None:
"""
delete table data
"""
return self.delete_data(table, where)
def insert(self, table: str, data: dict) -> None:
"""
insert sql statement
"""
return self.insert_data(table, data)
def select(self, table: str, where: dict = None, one: bool = False) -> list:
"""
select sql statement
"""
return self.select_data(table, where, one)
def update(self, table: str, data: dict, where: dict) -> None:
"""
update sql statement
"""
return self.update_data(table, data, where)
================================================
FILE: seldom/db_operation/mongo_db.py
================================================
"""
Mongo DB API
"""
try:
from pymongo import MongoClient
except ModuleNotFoundError as e:
raise ModuleNotFoundError("Please install the library. https://github.com/mongodb/mongo-python-driver")
class MongoDB:
"""Mongo DB table API"""
def __new__(cls, host: str, port: int, db: str):
"""
Connect the mongodb database
"""
client = MongoClient(host, port)
db_obj = client[db]
return db_obj
if __name__ == '__main__':
mongo_db = MongoDB("localhost", 27017, "yapi")
col = mongo_db.list_collection_names()
print("collection list: ", col)
data = mongo_db.project.find_one()
print("table one data:", data)
================================================
FILE: seldom/db_operation/mssql_db.py
================================================
"""
MS SQL Server DB API
"""
from typing import Any
try:
import pymssql
except ModuleNotFoundError as e:
raise ModuleNotFoundError("Please install the library. https://github.com/pymssql/pymssql")
from seldom.db_operation.base_db import SQLBase
class MSSQLDB(SQLBase):
"""SQL Server DB table API"""
def __init__(self, server: str, user: str, password: str, database: str, charset="utf8mb4"):
"""
Connect to the SQL Server database
:param server:
:param user:
:param password:
:param database:
"""
self.connection = pymssql.connect(server=server,
user=user,
password=password,
database=database,
charset=charset)
# self.cursor = self.connection.cursor(as_dict=True)
def close(self) -> None:
"""
Close the database connection
"""
self.connection.close()
def execute_sql(self, sql: str) -> None:
"""
Execute SQL
"""
print("running sql ", sql)
with self.connection.cursor() as cursor:
cursor.execute(sql)
self.connection.commit()
def query_sql(self, sql: str) -> list:
"""
Query SQL
return: query data
"""
data_list = []
with self.connection.cursor() as cursor:
cursor.execute(sql)
rows = cursor.fetchall()
for row in rows:
data_list.append(row)
self.connection.commit()
return data_list
def query_one(self, sql: str) -> Any:
"""
Query one data SQL
:return:
"""
with self.connection.cursor() as cursor:
cursor.execute(sql)
row = cursor.fetchone()
self.connection.commit()
return row
def insert_get_last_id(self, sql: str) -> int:
"""
insert sql and get last row id
:param sql:
:return:
"""
with self.connection.cursor() as cursor:
cursor.execute(sql)
last_id = cursor.lastrowid
self.connection.commit()
return last_id
def insert_data(self, table: str, data: dict) -> None:
"""
insert sql statement
"""
for key in data:
data[key] = "'" + str(data[key]) + "'"
key = ','.join(data.keys())
value = ','.join(data.values())
sql = f"""insert into {table} ({key}) values ({value})"""
self.execute_sql(sql)
def select_data(self, table: str, where: dict = None, one: bool = False) -> Any:
"""
select sql statement
"""
sql = f"""select * from {table} """
if where is not None:
sql += f""" where {self.dict_to_str_and(where)}"""
if one is True:
return self.query_one(sql)
return self.query_sql(sql)
def update_data(self, table: str, data: dict, where: dict) -> None:
"""
update sql statement
"""
sql = f"""update {table} set """
sql += self.dict_to_str(data)
if where:
sql += f""" where {self.dict_to_str_and(where)};"""
self.execute_sql(sql)
def delete_data(self, table: str, where: dict = None) -> None:
"""
delete table data
"""
sql = f"""delete from {table}"""
if where is not None:
sql += f""" where {self.dict_to_str_and(where)};"""
self.execute_sql(sql)
def init_table(self, table_data: dict, clear: bool = True) -> None:
"""
init table data
"""
for table, data_list in table_data.items():
if clear:
self.delete_data(table)
for data in data_list:
self.insert_data(table, data)
================================================
FILE: seldom/db_operation/mysql_db.py
================================================
"""
MySQL DB API
"""
from typing import Any
import pymysql.cursors
from seldom.db_operation.base_db import SQLBase
class MySQLDB(SQLBase):
"""MySQL DB table API"""
def __init__(self, host: str, port: int, user: str, password: str, database: str, charset='utf8mb4'):
"""
Connect to the MySQL database
:param host:
:param port:
:param user:
:param password:
:param database:
"""
self.connection = pymysql.connect(host=host,
port=int(port),
user=user,
password=password,
database=database,
charset=charset,
cursorclass=pymysql.cursors.DictCursor)
def close(self) -> None:
"""
Close the database connection
"""
self.connection.close()
def execute_sql(self, sql: str) -> None:
"""
Execute SQL
"""
with self.connection.cursor() as cursor:
self.connection.ping(reconnect=True)
if "delete" in sql.lower()[0:6]:
cursor.execute("SET FOREIGN_KEY_CHECKS=0;")
cursor.execute(sql)
self.connection.commit()
def query_sql(self, sql: str) -> list:
"""
Query SQL
return: query data
"""
data_list = []
with self.connection.cursor() as cursor:
self.connection.ping(reconnect=True)
cursor.execute(sql)
rows = cursor.fetchall()
for row in rows:
data_list.append(row)
self.connection.commit()
return data_list
def query_one(self, sql: str) -> Any:
"""
Query one data SQL
:return:
"""
with self.connection.cursor() as cursor:
self.connection.ping(reconnect=True)
cursor.execute(sql)
row = cursor.fetchone()
self.connection.commit()
return row
def insert_get_last_id(self, sql: str) -> int:
"""
insert sql and get last row id
:param sql:
:return:
"""
with self.connection.cursor() as cursor:
self.connection.ping(reconnect=True)
cursor.execute(sql)
last_id = cursor.lastrowid
self.connection.commit()
return last_id
def insert_data(self, table: str, data: dict) -> None:
"""
insert sql statement
"""
for key in data:
data[key] = "'" + str(data[key]) + "'"
key = ','.join(data.keys())
value = ','.join(data.values())
sql = f"""insert into {table} ({key}) values ({value})"""
self.execute_sql(sql)
def select_data(self, table: str, where: dict = None, one: bool = False) -> Any:
"""
select sql statement
"""
sql = f"""select * from {table} """
if where is not None:
sql += f""" where {self.dict_to_str_and(where)}"""
if one is True:
return self.query_one(sql)
return self.query_sql(sql)
def update_data(self, table: str, data: dict, where: dict) -> None:
"""
update sql statement
"""
sql = f"""update {table} set """
sql += self.dict_to_str(data)
if where:
sql += f""" where {self.dict_to_str_and(where)};"""
self.execute_sql(sql)
def delete_data(self, table: str, where: dict = None) -> None:
"""
delete table data
"""
sql = f"""delete from {table}"""
if where is not None:
sql += f""" where {self.dict_to_str_and(where)};"""
self.execute_sql(sql)
def init_table(self, table_data: dict, clear: bool = True) -> None:
"""
init table data
"""
for table, data_list in table_data.items():
if clear:
self.delete_data(table)
for data in data_list:
self.insert_data(table, data)
self.close()
================================================
FILE: seldom/db_operation/postgres_db.py
================================================
from typing import Any
try:
import psycopg2
import psycopg2.extras
except ModuleNotFoundError as e:
raise ModuleNotFoundError("Please install the library. https://github.com/psycopg/psycopg2")
from seldom.db_operation.base_db import SQLBase
class PostgresDB(SQLBase):
def __init__(self, host: str, port: int, database: str, user: str, password: str, charset: str = 'utf8'):
"""
Connect to Postgres database
:param host:
:param port:
:param database:
:param user:
:param password:
:param charset: (default 'utf8')
"""
self.connection = psycopg2.connect(host=host, port=port, database=database, user=user, password=password)
self.connection.set_client_encoding(charset)
self.connection.autocommit = True
def close(self):
"""
Close the database connection
"""
self.connection.close()
def execute_sql(self, sql: str) -> None:
"""
Execute SQL
"""
print("runner SQL ", sql)
with self.connection.cursor() as cursor:
cursor.execute(sql)
def query_sql(self, sql: str) -> list:
"""
Query SQL
:return: query data
"""
data_list = []
with self.connection.cursor(cursor_factory=psycopg2.extras.DictCursor) as cursor:
cursor.execute(sql)
rows = cursor.fetchall()
for row in rows:
data_list.append(row)
return data_list
def query_one(self, sql: str) -> Any:
"""
Query one row
"""
with self.connection.cursor(cursor_factory=psycopg2.extras.DictCursor) as cursor:
cursor.execute(sql)
row = cursor.fetchone()
return row
def insert_get_last_id(self, sql: str) -> int:
"""
insert sql and get last row id
:param sql:
:return:
"""
with self.connection.cursor() as cursor:
cursor.execute(sql)
last_id = cursor.lastrowid
return last_id
def insert_data(self, table: str, data: dict) -> None:
"""
insert sql statement
"""
for key in data:
data[key] = "'" + str(data[key]) + "'"
key = ','.join(data.keys())
value = ','.join(data.values())
sql = f"INSERT INTO {table} ({key}) VALUES ({value})"
self.execute_sql(sql)
def select_data(self, table: str, where: dict = None, one: bool = False) -> Any:
"""
Select SQL statement
"""
sql = f"SELECT * FROM {table}"
if where:
where_clause = self.dict_to_str_and(where)
sql += f" WHERE {where_clause}"
return self.query_one(sql) if one else self.query_sql(sql)
def update_data(self, table: str, data: dict, where: dict) -> None:
"""
Update SQL statement
"""
set_clause = self.dict_to_str(data)
where_clause = self.dict_to_str_and(where)
sql = f"UPDATE {table} SET {set_clause} WHERE {where_clause}"
self.execute_sql(sql)
def delete_data(self, table: str, where: dict = None) -> None:
"""
Delete SQL statement
"""
sql = f"DELETE FROM {table}"
if where:
where_clause = self.dict_to_str_and(where)
sql += f" WHERE {where_clause}"
self.execute_sql(sql)
def init_table(self, table_data: dict, clear: bool = True) -> None:
"""
Initialize table data
"""
for table, data_list in table_data.items():
if clear:
self.delete_data(table)
for data in data_list:
self.insert_data(table, data)
================================================
FILE: seldom/db_operation/sqlite_db.py
================================================
"""
SQLite3 DB API
"""
from typing import Any
import sqlite3
from seldom.db_operation.base_db import SQLBase
class SQLiteDB(SQLBase):
"""SQLite3 DB table API"""
def __init__(self, db_path: str):
"""
Connect to the sqlite database
"""
self.connection = sqlite3.connect(db_path)
self.cursor = self.connection.cursor()
def close(self) -> None:
"""
Close the database connection
"""
self.connection.close()
def execute_sql(self, sql: str) -> None:
"""
Execute SQL
"""
self.cursor.execute(sql)
self.connection.commit()
def insert_data(self, table: str, data: dict) -> None:
"""
insert sql statement
"""
for key in data:
data[key] = "'" + str(data[key]) + "'"
key = ','.join(data.keys())
value = ','.join(data.values())
sql = f"""insert into {table} ({key}) values ({value})"""
self.execute_sql(sql)
def query_sql(self, sql: str) -> list:
"""
Query SQL
return: query data
"""
data_list = []
self.cursor.execute(sql)
rows = self.cursor.fetchall()
for row in rows:
data_list.append(row)
return data_list
def query_one(self, sql: str) -> Any:
"""
Query one data SQL
return: query data
"""
self.cursor.execute(sql)
row = self.cursor.fetchone()
return row
def insert_get_last_id(self, sql: str) -> int:
"""
insert sql and get last row id
:param sql:
:return: query data
"""
self.cursor.execute(sql)
last_id = self.cursor.lastrowid
self.connection.commit()
return last_id
def select_data(self, table: str, where: dict = None, one: bool = False) -> Any:
"""
select sql statement
"""
sql = f"""select * from {table} """
if where is not None:
sql += f""" where {self.dict_to_str_and(where)}"""
if one is True:
return self.query_one(sql)
return self.query_sql(sql)
def update_data(self, table: str, data: dict, where: dict) -> None:
"""
update sql statement
"""
sql = f"""update {table} set """
sql += self.dict_to_str(data)
if where:
sql += f""" where {self.dict_to_str_and(where)};"""
self.execute_sql(sql)
def delete_data(self, table: str, where: dict = None) -> None:
"""
delete table data
"""
sql = f"""delete from {table}"""
if where is not None:
sql += f""" where {self.dict_to_str_and(where)};"""
self.execute_sql(sql)
def init_table(self, table_data: dict, clear: bool = True) -> None:
"""
init table data
"""
for table, data_list in table_data.items():
if clear:
self.delete_data(table)
for data in data_list:
self.insert_data(table, data)
================================================
FILE: seldom/driver.py
================================================
"""
browser driver
"""
from selenium import webdriver
from selenium.webdriver.chrome.service import Service as ChromeService
from selenium.webdriver.edge.service import Service as EdgeService
from selenium.webdriver.firefox.service import Service as FireFoxService
from selenium.webdriver.ie.service import Service as IEService
from selenium.webdriver.safari.service import Service as SafariService
from seldom.logging.exceptions import BrowserTypeError
__all__ = ["Browser"]
class Browser:
"""
Run class initialization method, the default is proper
to drive the Firefox browser. Of course, you can also
pass parameter for other browser, Chrome browser for the "Chrome",
the Internet Explorer browser for "Internet Explorer" or "ie".
"""
def __new__(cls, name: str = None, executable_path=None, options=None, command_executor=""):
"""
new browser driver
:param name: browser name.
:param executable_path: browser driver path.
:param options:
:param command_executor:
"""
if (name is None) or (name in ["chrome", "google chrome", "gc"]):
return cls.chrome(executable_path, options, command_executor)
if name in ["firefox", "ff"]:
return cls.firefox(executable_path, options, command_executor)
if name in ["internet explorer", "ie", "IE"]:
return cls.ie(executable_path, options, command_executor)
if name == "edge":
return cls.edge(executable_path, options, command_executor)
if name == "chromium":
return cls.edge(executable_path, options, command_executor)
if name == "safari":
return cls.safari(executable_path, options, command_executor)
raise BrowserTypeError(
f"Not found `{name}` browser, See the help doc: https://seldomqa.github.io/web-testing/browser_driver.html.")
@staticmethod
def chrome(executable_path, options, command_executor):
"""
Chrome browser driver
"""
is_grid = False
if command_executor != "":
is_grid = True
driver = webdriver.Remote(options=options, command_executor=command_executor)
else:
driver = webdriver.Chrome(options=options, service=ChromeService(executable_path=executable_path))
if is_grid is False:
driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
"source": """
Object.defineProperty(navigator, 'webdriver', {
get: () => undefined
})"""
})
return driver
@staticmethod
def firefox(executable_path, options, command_executor):
"""
firefox browser driver
"""
if command_executor != "":
driver = webdriver.Remote(options=options, command_executor=command_executor)
else:
driver = webdriver.Firefox(options=options, service=FireFoxService(executable_path=executable_path))
return driver
@staticmethod
def ie(executable_path, options, command_executor):
"""
internet explorer browser driver
"""
if command_executor != "":
driver = webdriver.Remote(options=options, command_executor=command_executor)
else:
driver = webdriver.Ie(options=options, service=IEService(executable_path=executable_path))
return driver
@staticmethod
def edge(executable_path, options, command_executor):
"""
edge browser driver
"""
if command_executor != "":
driver = webdriver.Remote(options=options, command_executor=command_executor)
else:
driver = webdriver.Edge(options=options, service=EdgeService(executable_path=executable_path))
return driver
@staticmethod
def safari(executable_path, options, command_executor):
"""
safari browser driver
"""
if command_executor != "":
return webdriver.Remote(options=options, command_executor=command_executor)
else:
return webdriver.Safari(options=options, service=SafariService(executable_path=executable_path))
================================================
FILE: seldom/extend_lib/__init__.py
================================================
"""
In order to reduce dependencies,
some simple third-party libraries are directly migrated over.
"""
from .jsonpath import jsonpath
from .curlify import to_curl
from .tomorrow import threads
from .parameterized import parameterized, param, parameterized_class
================================================
FILE: seldom/extend_lib/base_assert.py
================================================
from functools import wraps
from seldom.logging import log
def log_assertions(func):
"""
Decorator: Adds logging capabilities to assertion methods
:param func:
:return:
"""
@wraps(func)
def wrapper(*args, **kwargs):
"""
wrapper
:param args:
:param kwargs:
:return:
"""
args_to_log = []
if args:
args_to_log = args[1:]
log.info(f"👀 {func.__name__} -> {args_to_log}")
result = func(*args, **kwargs)
return result
return wrapper
================================================
FILE: seldom/extend_lib/curlify.py
================================================
"""
A library to convert python requests object to curl command.
GitHub: https://github.com/ofw/curlify
"""
from shlex import quote
def to_curl(request, compressed: bool = False, verify: bool = True) -> str:
"""
Returns string with curl command by provided request object
:param request: request object
:param compressed:
If `True` then `--compressed` argument will be added to result
:param verify:
"""
parts = [
('curl', None),
('-X', request.method),
]
for key, value in sorted(request.headers.items()):
parts += [('-H', f'{key}: {value}')]
if request.body:
body = request.body
if isinstance(body, bytes):
body = body.decode('utf-8')
parts += [('-d', body)]
if compressed:
parts += [('--compressed', None)]
if not verify:
parts += [('--insecure', None)]
parts += [(None, request.url)]
flat_parts = []
for key, value in parts:
if key:
flat_parts.append(quote(key))
if value:
flat_parts.append(quote(value))
return ' '.join(flat_parts)
================================================
FILE: seldom/extend_lib/jsonpath.py
================================================
"""
An XPath for JSON
help: https://goessner.net/articles/JsonPath/
GitHub: https://gist.github.com/drewr/783585
"""
from __future__ import annotations
import re
import sys
from typing import Any, List, Union, Dict, Callable
from seldom.logging import log
__all__ = ['jsonpath']
def normalize(x: str) -> str:
"""normalize the path expression; outside jsonpath to allow testing"""
subx: List[str] = []
def f1(m: re.Match) -> str:
n = len(subx)
g1 = m.group(1)
subx.append(g1)
return f"[#{n}]"
x = re.sub(r"[\['](\??\(.*?\))[\]']", f1, x)
x = re.sub(r"'?(? str:
return subx[int(m.group(1))]
x = re.sub(r"#([0-9]+)", f2, x)
return x
def jsonpath(
obj: Union[Dict, List],
expr: str,
result_type: str = 'VALUE',
debug: int = 0,
use_eval: bool = True
) -> Union[List[Any], bool]:
"""
traverse JSON object using jsonpath expr, returning values or paths
"""
result: List[Any] = []
def s(x: Union[str, int], y: str) -> str:
"""concatenate path elements"""
return f"{x};{y}"
def isint(x: str) -> bool:
"""check if argument represents a decimal integer"""
return x.isdigit()
def as_path(path: str) -> str:
"""convert internal path representation to
"full bracket notation" for PATH output"""
p = '$'
for piece in path.split(';')[1:]:
if isint(piece):
p += f"[{piece}]"
else:
p += f"['{piece}']"
return p
def store(path: str, object: Any) -> str:
if result_type == 'VALUE':
result.append(object)
elif result_type == 'IPATH':
result.append(path.split(';')[1:])
else: # PATH
result.append(as_path(path))
return path
def trace(expr: str, obj: Any, path: str) -> None:
if debug:
log.debug(f"trace {expr} / {path}")
if expr:
x = expr.split(';')
loc = x[0]
x = ';'.join(x[1:])
if debug:
log.debug(f"\t {loc} {type(obj)}")
if loc == "*":
def f03(key: str, loc: str, expr: str, obj: Any, path: str) -> None:
if debug > 1:
log.debug(f"\tf03 {key} {loc} {expr} {path}")
trace(s(key, expr), obj, path)
walk(loc, x, obj, path, f03)
elif loc == "..":
trace(x, obj, path)
def f04(key: str, loc: str, expr: str, obj: Any, path: str) -> None:
if debug > 1:
log.debug(f"\tf04 {key} {loc} {expr} {path}")
if isinstance(obj, dict):
if key in obj:
trace(s('..', expr), obj[key], s(path, key))
else:
if key < len(obj):
trace(s('..', expr), obj[key], s(path, key))
walk(loc, x, obj, path, f04)
elif loc == "!":
# Perl jsonpath extension: return keys
def f06(key: str, loc: str, expr: str, obj: Any, path: str) -> None:
if isinstance(obj, dict):
trace(expr, key, path)
walk(loc, x, obj, path, f06)
elif isinstance(obj, dict) and loc in obj:
trace(x, obj[loc], s(path, loc))
elif isinstance(obj, list) and isint(loc):
iloc = int(loc)
if debug:
log.debug(f"-----> {iloc} {len(obj)}")
if len(obj) > iloc:
trace(x, obj[iloc], s(path, loc))
else:
# [(index_expression)]
if loc.startswith("(") and loc.endswith(")"):
if debug > 1:
log.debug(f"index {loc}")
e = evalx(loc, obj)
trace(s(e, x), obj, path)
return
# ?(filter_expression)
if loc.startswith("?(") and loc.endswith(")"):
if debug > 1:
log.debug(f"filter {loc}")
def f05(key: str, loc: str, expr: str, obj: Any, path: str) -> None:
if debug > 1:
log.debug(f"f05 {key} {loc} {expr} {path}")
if isinstance(obj, dict):
eval_result = evalx(loc, obj[key])
else:
eval_result = evalx(loc, obj[int(key)])
if eval_result:
trace(s(key, expr), obj, path)
loc = loc[2:-1]
walk(loc, x, obj, path, f05)
return
m = re.match(r'(-?[0-9]*):(-?[0-9]*):?(-?[0-9]*)$', loc)
if m:
if isinstance(obj, (dict, list)):
def max(x: int, y: int) -> int:
if x > y:
return x
return y
def min(x: int, y: int) -> int:
if x < y:
return x
return y
objlen = len(obj)
s0 = m.group(1)
s1 = m.group(2)
s2 = m.group(3)
# XXX int("badstr") raises exception
start = int(s0) if s0 else 0
end = int(s1) if s1 else objlen
step = int(s2) if s2 else 1
if start < 0:
start = max(0, start + objlen)
else:
start = min(objlen, start)
if end < 0:
end = max(0, end + objlen)
else:
end = min(objlen, end)
for i in range(start, end, step):
trace(s(i, x), obj, path)
return
# after (expr) & ?(expr)
if loc.find(",") >= 0:
# [index,index....]
for piece in re.split(r"'?,'?", loc):
if debug > 1:
log.debug(f"piece {piece}")
trace(s(piece, x), obj, path)
else:
store(path, obj)
def walk(loc: str, expr: str, obj: Any, path: str, funct: Callable) -> None:
if isinstance(obj, list):
for i in range(0, len(obj)):
funct(i, loc, expr, obj, path)
elif isinstance(obj, dict):
for key in obj:
funct(key, loc, expr, obj, path)
def evalx(loc: str, obj: Any) -> Any:
"""eval expression"""
if debug:
log.debug(f"evalx {loc}")
# a nod to JavaScript. doesn't work for @.name.name.length
# Write len(@.name.name) instead!!!
loc = loc.replace("@.length", "len(__obj)")
loc = loc.replace("&&", " and ").replace("||", " or ")
# replace !@.name with 'name' not in obj
# XXX handle !@.name.name.name....
def notvar(m: re.Match) -> str:
return f"'{m.group(1)}' not in __obj"
loc = re.sub(r"!@\.([a-zA-Z@_0-9-]*)", notvar, loc)
# replace @.name.... with __obj['name']....
# handle @.name[.name...].length
def varmatch(m: re.Match) -> str:
def brackets(elts: List[str]) -> str:
ret = "__obj"
for e in elts:
if isint(e):
ret += f"[{e}]"
else:
ret += f"['{e}']"
return ret
g1 = m.group(1)
elts = g1.split('.')
if elts[-1] == "length":
return f"len({brackets(elts[1:-1])})"
return brackets(elts[1:])
loc = re.sub(r'(? == translation
# causes problems if a string contains =
# replace @ w/ "__obj", but \@ means a literal @
loc = re.sub(r'(? {v}")
return v
# body of jsonpath()
caller_globals = sys._getframe(1).f_globals
if not (expr and obj):
return False
cleaned_expr = normalize(expr)
if cleaned_expr.startswith("$;"):
cleaned_expr = cleaned_expr[2:]
trace(cleaned_expr, obj, '$')
return result if result else False
if __name__ == '__main__':
try:
import json
except ImportError:
import simplejson as json
if len(sys.argv) not in (3, 4):
sys.stdout.write("Usage: jsonpath.py FILE PATH [OUTPUT_TYPE]\n")
sys.exit(1)
try:
with open(sys.argv[1]) as f:
object = json.load(f)
except Exception as e:
log.error(f"Error loading JSON file: {e}")
sys.exit(1)
path = sys.argv[2]
format = sys.argv[3] if len(sys.argv) > 3 else 'VALUE'
value = jsonpath(object, path, format, debug=2)
if not value:
sys.exit(1)
with sys.stdout as f:
json.dump(value, f, sort_keys=True, indent=1)
f.write("\n")
sys.exit(0)
================================================
FILE: seldom/extend_lib/parameterized.py
================================================
"""
Parameterized testing with any Python test framework.
GitHub:https://github.com/wolever/parameterized
"""
import inspect
import re
import sys
import warnings
from collections import namedtuple
from functools import wraps
from types import MethodType as MethodType
from typing import Iterable
try:
from unittest import mock
except ImportError:
try:
import mock
except ImportError:
mock = None
try:
from collections import OrderedDict as MaybeOrderedDict
except ImportError:
MaybeOrderedDict = dict
from unittest import TestCase
try:
from unittest import SkipTest
except ImportError:
class SkipTest(Exception):
pass
# NOTE: even though Python 2 support has been dropped, these checks have been
# left in place to avoid merge conflicts. They can be removed in the future, and
# future code can be written to assume Python 3.
PY3 = sys.version_info[0] == 3
PY2 = sys.version_info[0] == 2
if PY3:
# Python 3 doesn't have an InstanceType, so just use a dummy type.
class InstanceType:
pass
lzip = lambda *a: list(zip(*a))
text_type = str
string_types = str,
bytes_type = bytes
def make_method(func, instance, type):
if instance is None:
return func
return MethodType(func, instance)
else:
raise ValueError("Python 2 is not supported!!")
def to_text(x):
if isinstance(x, text_type):
return x
try:
return text_type(x, "utf-8")
except UnicodeDecodeError:
return text_type(x, "latin1")
CompatArgSpec = namedtuple("CompatArgSpec", "args varargs keywords defaults")
def getargspec(func):
if PY2:
return CompatArgSpec(*inspect.getargspec(func))
args = inspect.getfullargspec(func)
if args.kwonlyargs:
raise TypeError((
"parameterized does not (yet) support functions with keyword "
"only arguments, but %r has keyword only arguments. "
"Please open an issue with your usecase if this affects you: "
"https://github.com/wolever/parameterized/issues/new"
) % (func,))
return CompatArgSpec(*args[:4])
def skip_on_empty_helper(*a, **kw):
raise SkipTest("parameterized input is empty")
def reapply_patches_if_need(func):
def dummy_wrapper(orgfunc):
@wraps(orgfunc)
def dummy_func(*args, **kwargs):
return orgfunc(*args, **kwargs)
return dummy_func
if hasattr(func, 'patchings'):
is_original_async = inspect.iscoroutinefunction(func)
func = dummy_wrapper(func)
tmp_patchings = func.patchings
delattr(func, 'patchings')
for patch_obj in tmp_patchings:
if is_original_async:
func = patch_obj.decorate_async_callable(func)
else:
func = patch_obj.decorate_callable(func)
return func
# `parameterized.expand` strips out `mock` patches from the source method in favor of re-applying them over the
# generated methods instead. Sadly, this can cause problems with old versions of the `mock` package, as shown in
# https://bugs.python.org/issue40126 (bpo-40126).
#
# Long story short, bpo-40126 arises whenever the `patchings` list of a `mock`-decorated method is left fully empty.
#
# The bug has been fixed in the `mock` code itself since:
# - Python 3.7.8-rc1, 3.8.3-rc1 and later (for the `unittest.mock` package) [0][1].
# - Version 4 of the `mock` backport package (https://pypi.org/project/mock/) [2].
#
# To work around the problem when running old `mock` versions, we avoid fully stripping out patches from the source
# method in favor of replacing them with a "dummy" no-op patch instead.
#
# [0] https://docs.python.org/release/3.7.10/whatsnew/changelog.html#python-3-7-8-release-candidate-1
# [1] https://docs.python.org/release/3.8.10/whatsnew/changelog.html#python-3-8-3-release-candidate-1
# [2] https://mock.readthedocs.io/en/stable/changelog.html#b1
PYTHON_DOESNT_HAVE_FIX_FOR_BPO_40126 = (
sys.version_info[:3] < (3, 7, 8) or (sys.version_info[:2] >= (3, 8) and sys.version_info[:3] < (3, 8, 3))
)
try:
import mock as _mock_backport
except ImportError:
_mock_backport = None
MOCK_BACKPORT_DOESNT_HAVE_FIX_FOR_BPO_40126 = _mock_backport is not None and _mock_backport.version_info[0] < 4
AVOID_CLEARING_MOCK_PATCHES = PYTHON_DOESNT_HAVE_FIX_FOR_BPO_40126 or MOCK_BACKPORT_DOESNT_HAVE_FIX_FOR_BPO_40126
class DummyPatchTarget(object):
dummy_attribute = None
@staticmethod
def create_dummy_patch():
if mock is not None:
return mock.patch.object(DummyPatchTarget(), "dummy_attribute", new=None)
else:
raise ImportError("Missing mock package")
def delete_patches_if_need(func):
if hasattr(func, 'patchings'):
if AVOID_CLEARING_MOCK_PATCHES:
func.patchings[:] = [DummyPatchTarget.create_dummy_patch()]
else:
func.patchings[:] = []
_param = namedtuple("param", "args kwargs")
class param(_param):
""" Represents a single parameter to a test case.
For example::
>>> p = param("foo", bar=16)
>>> p
param("foo", bar=16)
>>> p.args
('foo', )
>>> p.kwargs
{'bar': 16}
Intended to be used as an argument to ``@parameterized``::
@parameterized([
param("foo", bar=16),
])
def test_stuff(foo, bar=16):
pass
"""
def __new__(cls, *args, **kwargs):
return _param.__new__(cls, args, kwargs)
@classmethod
def explicit(cls, args=None, kwargs=None):
""" Creates a ``param`` by explicitly specifying ``args`` and
``kwargs``::
>>> param.explicit([1,2,3])
param(*(1, 2, 3))
>>> param.explicit(kwargs={"foo": 42})
param(*(), **{"foo": "42"})
"""
args = args or ()
kwargs = kwargs or {}
return cls(*args, **kwargs)
@classmethod
def from_decorator(cls, args):
""" Returns an instance of ``param()`` for ``@parameterized`` argument
``args``::
>>> param.from_decorator((42, ))
param(args=(42, ), kwargs={})
>>> param.from_decorator("foo")
param(args=("foo", ), kwargs={})
"""
if isinstance(args, param):
return args
elif isinstance(args, (str, bytes)) or not isinstance(args, Iterable):
args = (args,)
try:
return cls(*args)
except TypeError as e:
if "after * must be" not in str(e):
raise
raise TypeError(
"Parameters must be tuples, but %r is not (hint: use '(%r, )')"
% (args, args),
)
def __repr__(self):
return "param(*%r, **%r)" % self
class QuietOrderedDict(MaybeOrderedDict):
""" When OrderedDict is available, use it to make sure that the kwargs in
doc strings are consistently ordered. """
__str__ = dict.__str__
__repr__ = dict.__repr__
def parameterized_argument_value_pairs(func, p):
"""Return tuples of parameterized arguments and their values.
This is useful if you are writing your own doc_func
function and need to know the values for each parameter name::
>>> def func(a, foo=None, bar=42, **kwargs): pass
>>> p = param(1, foo=7, extra=99)
>>> parameterized_argument_value_pairs(func, p)
[("a", 1), ("foo", 7), ("bar", 42), ("**kwargs", {"extra": 99})]
If the function's first argument is named ``self`` then it will be
ignored::
>>> def func(self, a): pass
>>> p = param(1)
>>> parameterized_argument_value_pairs(func, p)
[("a", 1)]
Additionally, empty ``*args`` or ``**kwargs`` will be ignored::
>>> def func(foo, *args): pass
>>> p = param(1)
>>> parameterized_argument_value_pairs(func, p)
[("foo", 1)]
>>> p = param(1, 16)
>>> parameterized_argument_value_pairs(func, p)
[("foo", 1), ("*args", (16, ))]
"""
argspec = getargspec(func)
arg_offset = 1 if argspec.args[:1] == ["self"] else 0
named_args = argspec.args[arg_offset:]
result = lzip(named_args, p.args)
named_args = argspec.args[len(result) + arg_offset:]
varargs = p.args[len(result):]
result.extend([
(name, p.kwargs.get(name, default))
for (name, default)
in zip(named_args, argspec.defaults or [])
])
seen_arg_names = set([n for (n, _) in result])
keywords = QuietOrderedDict(sorted([
(name, p.kwargs[name])
for name in p.kwargs
if name not in seen_arg_names
]))
if varargs:
result.append(("*%s" % (argspec.varargs,), tuple(varargs)))
if keywords:
result.append(("**%s" % (argspec.keywords,), keywords))
return result
def short_repr(x, n=64):
""" A shortened repr of ``x`` which is guaranteed to be ``unicode``::
>>> short_repr("foo")
u"foo"
>>> short_repr("123456789", n=4)
u"12...89"
"""
x_repr = to_text(repr(x))
if len(x_repr) > n:
x_repr = x_repr[:n // 2] + "..." + x_repr[len(x_repr) - n // 2:]
return x_repr
def default_doc_func(func, num, p):
if func.__doc__ is None:
return None
all_args_with_values = parameterized_argument_value_pairs(func, p)
# Assumes that the function passed is a bound method.
descs = ["%s=%s" % (n, short_repr(v)) for n, v in all_args_with_values]
# The documentation might be a multiline string, so split it
# and just work with the first string, ignoring the period
# at the end if there is one.
first, nl, rest = func.__doc__.lstrip().partition("\n")
suffix = ""
if first.endswith("."):
suffix = "."
first = first[:-1]
args = "%s[with %s]" % (len(first) and " " or "", ", ".join(descs))
return "".join(
to_text(x)
for x in [first.rstrip(), args, suffix, nl, rest]
)
def default_name_func(func, num, p):
base_name = func.__name__
name_suffix = "_%s" % (num,)
if len(p.args) > 0 and isinstance(p.args[0], string_types):
name_suffix += "_" + parameterized.to_safe_name(p.args[0])
return base_name + name_suffix
_test_runner_override = None
_test_runner_guess = False
_test_runners = set(["unittest", "unittest2", "nose", "nose2", "pytest"])
_test_runner_aliases = {
"_pytest": "pytest",
}
def set_test_runner(name):
global _test_runner_override
if name not in _test_runners:
raise TypeError(
"Invalid test runner: %r (must be one of: %s)"
% (name, ", ".join(_test_runners)),
)
_test_runner_override = name
def detect_runner():
""" Guess which test runner we're using by traversing the stack and looking
for the first matching module. This *should* be reasonably safe, as
it's done during test discovery where the test runner should be the
stack frame immediately outside. """
if _test_runner_override is not None:
return _test_runner_override
global _test_runner_guess
if _test_runner_guess is False:
stack = inspect.stack()
for record in reversed(stack):
frame = record[0]
module = frame.f_globals.get("__name__").partition(".")[0]
if module in _test_runner_aliases:
module = _test_runner_aliases[module]
if module in _test_runners:
_test_runner_guess = module
break
if record[1].endswith("python2.6/unittest.py"):
_test_runner_guess = "unittest"
break
else:
_test_runner_guess = None
return _test_runner_guess
class parameterized(object):
""" Parameterize a test case::
class TestInt(object):
@parameterized([
("A", 10),
("F", 15),
param("10", 42, base=42)
])
def test_int(self, input, expected, base=16):
actual = int(input, base=base)
assert_equal(actual, expected)
@parameterized([
(2, 3, 5)
(3, 5, 8),
])
def test_add(a, b, expected):
assert_equal(a + b, expected)
"""
def __init__(self, input, doc_func=None, skip_on_empty=False):
self.get_input = self.input_as_callable(input)
self.doc_func = doc_func or default_doc_func
self.skip_on_empty = skip_on_empty
def __call__(self, test_func):
self.assert_not_in_testcase_subclass()
@wraps(test_func)
def wrapper(test_self=None):
test_cls = test_self and type(test_self)
if test_self is not None:
if issubclass(test_cls, InstanceType):
raise TypeError((
"@parameterized can't be used with old-style classes, but "
"%r has an old-style class. Consider using a new-style "
"class, or '@parameterized.expand' "
"(see http://stackoverflow.com/q/54867/71522 for more "
"information on old-style classes)."
) % (test_self,))
original_doc = wrapper.__doc__
for num, args in enumerate(wrapper.parameterized_input):
p = param.from_decorator(args)
unbound_func, nose_tuple = self.param_as_nose_tuple(test_self, test_func, num, p)
try:
wrapper.__doc__ = nose_tuple[0].__doc__
# Nose uses `getattr(instance, test_func.__name__)` to get
# a method bound to the test instance (as opposed to a
# method bound to the instance of the class created when
# tests were being enumerated). Set a value here to make
# sure nose can get the correct test method.
if test_self is not None:
setattr(test_cls, test_func.__name__, unbound_func)
yield nose_tuple
finally:
if test_self is not None:
delattr(test_cls, test_func.__name__)
wrapper.__doc__ = original_doc
input = self.get_input()
if not input:
if not self.skip_on_empty:
raise ValueError(
"Parameters iterable is empty (hint: use "
"`parameterized([], skip_on_empty=True)` to skip "
"this test when the input is empty)"
)
wrapper = wraps(test_func)(skip_on_empty_helper)
wrapper.parameterized_input = input
wrapper.parameterized_func = test_func
test_func.__name__ = "_parameterized_original_%s" % (test_func.__name__,)
return wrapper
def param_as_nose_tuple(self, test_self, func, num, p):
nose_func = wraps(func)(lambda *args: func(*args[:-1], **args[-1]))
nose_func.__doc__ = self.doc_func(func, num, p)
# Track the unbound function because we need to setattr the unbound
# function onto the class for nose to work (see comments above), and
# Python 3 doesn't let us pull the function out of a bound method.
unbound_func = nose_func
if test_self is not None:
# Under nose on Py2 we need to return an unbound method to make
# sure that the `self` in the method is properly shared with the
# `self` used in `setUp` and `tearDown`. But only there. Everyone
# else needs a bound method.
func_self = (
None if PY2 and detect_runner() == "nose" else
test_self
)
nose_func = make_method(nose_func, func_self, type(test_self))
return unbound_func, (nose_func,) + p.args + (p.kwargs or {},)
def assert_not_in_testcase_subclass(self):
parent_classes = self._terrible_magic_get_defining_classes()
if any(issubclass(cls, TestCase) for cls in parent_classes):
raise Exception("Warning: '@parameterized' tests won't work "
"inside subclasses of 'TestCase' - use "
"'@parameterized.expand' instead.")
def _terrible_magic_get_defining_classes(self):
""" Returns the set of parent classes of the class currently being defined.
Will likely only work if called from the ``parameterized`` decorator.
This function is entirely @brandon_rhodes's fault, as he suggested
the implementation: http://stackoverflow.com/a/8793684/71522
"""
stack = inspect.stack()
if len(stack) <= 4:
return []
frame = stack[4]
code_context = frame[4] and frame[4][0].strip()
if not (code_context and code_context.startswith("class ")):
return []
_, _, parents = code_context.partition("(")
parents, _, _ = parents.partition(")")
return eval("[" + parents + "]", frame[0].f_globals, frame[0].f_locals)
@classmethod
def input_as_callable(cls, input):
if callable(input):
return lambda: cls.check_input_values(input())
input_values = cls.check_input_values(input)
return lambda: input_values
@classmethod
def check_input_values(cls, input_values):
# Explicitly convery non-list inputs to a list so that:
# 1. A helpful exception will be raised if they aren't iterable, and
# 2. Generators are unwrapped exactly once (otherwise `nosetests
# --processes=n` has issues; see:
# https://github.com/wolever/nose-parameterized/pull/31)
if not isinstance(input_values, list):
input_values = list(input_values)
return [param.from_decorator(p) for p in input_values]
@classmethod
def expand(cls, input, name_func=None, doc_func=None, skip_on_empty=False,
namespace=None, **legacy):
""" A "brute force" method of parameterizing test cases. Creates new
test cases and injects them into the namespace that the wrapped
function is being defined in. Useful for parameterizing tests in
subclasses of 'UnitTest', where Nose test generators don't work.
:param input: An iterable of values to pass to the test function.
:param name_func: A function that takes a single argument (the
value from the input iterable) and returns a string to use as
the name of the test case. If not provided, the name of the
test case will be the name of the test function with the
parameter value appended.
:param doc_func: A function that takes a single argument (the
value from the input iterable) and returns a string to use as
the docstring of the test case. If not provided, the docstring
of the test case will be the docstring of the test function.
:param skip_on_empty: If True, the test will be skipped if the
input iterable is empty. If False, a ValueError will be raised
if the input iterable is empty.
:param namespace: The namespace (dict-like) to inject the test cases
into. If not provided, the namespace of the test function will
be used.
>>> @parameterized.expand([("foo", 1, 2)])
... def test_add1(name, input, expected):
... actual = add1(input)
... assert_equal(actual, expected)
...
>>>
"""
if "testcase_func_name" in legacy:
warnings.warn("testcase_func_name= is deprecated; use name_func=",
DeprecationWarning, stacklevel=2)
if not name_func:
name_func = legacy["testcase_func_name"]
if "testcase_func_doc" in legacy:
warnings.warn("testcase_func_doc= is deprecated; use doc_func=",
DeprecationWarning, stacklevel=2)
if not doc_func:
doc_func = legacy["testcase_func_doc"]
doc_func = doc_func or default_doc_func
name_func = name_func or default_name_func
def parameterized_expand_wrapper(f, instance=None):
frame_locals = namespace
if frame_locals is None:
frame_locals = inspect.currentframe().f_back.f_locals
parameters = cls.input_as_callable(input)()
if not parameters:
if not skip_on_empty:
raise ValueError(
"Parameters iterable is empty (hint: use "
"`parameterized.expand([], skip_on_empty=True)` to skip "
"this test when the input is empty)"
)
return wraps(f)(skip_on_empty_helper)
digits = len(str(len(parameters) - 1))
for num, p in enumerate(parameters):
name = name_func(f, "{num:0>{digits}}".format(digits=digits, num=num), p)
# If the original function has patches applied by 'mock.patch',
# re-construct all patches on the just former decoration layer
# of param_as_standalone_func so as not to share
# patch objects between new functions
nf = reapply_patches_if_need(f)
frame_locals[name] = cls.param_as_standalone_func(p, nf, name)
frame_locals[name].__doc__ = doc_func(f, num, p)
# Delete original patches to prevent new function from evaluating
# original patching object as well as re-constructed patches.
delete_patches_if_need(f)
f.__test__ = False
return parameterized_expand_wrapper
@classmethod
def param_as_standalone_func(cls, p, func, name):
if inspect.iscoroutinefunction(func):
@wraps(func)
async def standalone_func(*a, **kw):
return await func(*(a + p.args), **p.kwargs, **kw)
else:
@wraps(func)
def standalone_func(*a, **kw):
return func(*(a + p.args), **p.kwargs, **kw)
standalone_func.__name__ = name
# place_as is used by py.test to determine what source file should be
# used for this test.
standalone_func.place_as = func
# Remove __wrapped__ because py.test will try to look at __wrapped__
# to determine which parameters should be used with this test case,
# and obviously we don't need it to do any parameterization.
try:
del standalone_func.__wrapped__
except AttributeError:
pass
return standalone_func
@classmethod
def to_safe_name(cls, s):
if not isinstance(s, str):
s = str(s)
return str(re.sub("[^a-zA-Z0-9_]+", "_", s))
def parameterized_class(attrs, input_values=None, class_name_func=None):
""" Parameterizes a test class by setting attributes on the class.
Can be used in two ways:
1) With a list of dictionaries containing attributes to override::
@parameterized_class([
{ "username": "foo" },
{ "username": "bar", "access_level": 2 },
])
class TestUserAccessLevel(TestCase):
...
2) With a tuple of attributes, then a list of tuples of values:
@parameterized_class(("username", "access_level"), [
("foo", 1),
("bar", 2)
])
class TestUserAccessLevel(TestCase):
...
"""
if isinstance(attrs, string_types):
attrs = [attrs]
input_dicts = (
attrs if input_values is None else
[dict(zip(attrs, vals)) for vals in input_values]
)
class_name_func = class_name_func or default_class_name_func
def decorator(base_class):
test_class_module = sys.modules[base_class.__module__].__dict__
for idx, input_dict in enumerate(input_dicts):
test_class_dict = dict(base_class.__dict__)
test_class_dict.update(input_dict)
name = class_name_func(base_class, idx, input_dict)
test_class_module[name] = type(name, (base_class,), test_class_dict)
# We need to leave the base class in place (see issue #73), but if we
# leave the test_ methods in place, the test runner will try to pick
# them up and run them... which doesn't make sense, since no parameters
# will have been applied.
# Address this by iterating over the base class and remove all test
# methods.
for method_name in list(base_class.__dict__):
if method_name.startswith("test"):
delattr(base_class, method_name)
return base_class
return decorator
def get_class_name_suffix(params_dict):
if "name" in params_dict:
return parameterized.to_safe_name(params_dict["name"])
params_vals = (
params_dict.values() if PY3 else
(v for (_, v) in sorted(params_dict.items()))
)
return parameterized.to_safe_name(next((
v for v in params_vals
if isinstance(v, string_types)
), ""))
def default_class_name_func(cls, num, params_dict):
suffix = get_class_name_suffix(params_dict)
return "%s_%s%s" % (
cls.__name__,
num,
suffix and "_" + suffix,
)
================================================
FILE: seldom/extend_lib/tomorrow.py
================================================
"""
this is tomorrow3 library,Easier way to use thread pool executor
GitHub: https://github.com/dflupu/tomorrow3
"""
from functools import wraps
from threading import Semaphore
from concurrent.futures import ThreadPoolExecutor
def threads(n, queue_max=None):
def decorator(f):
pool = ThreadPoolExecutor(n)
sem_max = queue_max
if sem_max is None:
sem_max = n
sem = Semaphore(sem_max)
thrown_exception = None
def wait():
# everything has already been added to the pool when .wait is called
# if we can acquire the semaphore sem_max times, it means nothing is left in the pool
for _ in range(sem_max):
sem.acquire()
# release in case the function will be called again after .wait
for _ in range(sem_max):
sem.release()
if thrown_exception:
raise thrown_exception
f.wait = wait
def on_done(f):
nonlocal thrown_exception
try:
f.result()
except Exception as ex:
thrown_exception = ex
finally:
sem.release()
@wraps(f)
def wrapped(*args, **kwargs):
if thrown_exception:
raise thrown_exception
sem.acquire(blocking=True)
future = pool.submit(f, *args, **kwargs)
future.add_done_callback(on_done)
return future
return wrapped
return decorator
================================================
FILE: seldom/file_runner/__init__.py
================================================
================================================
FILE: seldom/file_runner/api_excel.py
================================================
import seldom
from seldom import file_data
import json
from seldom.logging import log
from seldom.running.config import FileRunningConfig
class APITest(seldom.TestCase):
@file_data(FileRunningConfig.api_excel_file_name, line=2)
def test_api_excel(self, name, url, method, headers, param_type, param, assert_resp, exclude):
"""
case name
"""
log.info(f"execute api case: [{name}]")
param_dict = json.loads(param)
headers_dict = json.loads(headers)
if method == "GET":
self.get(url=url, params=param_dict, headers=headers_dict)
elif method == "POST":
if param_type == "data":
self.post(url=url, data=param_dict, headers=headers_dict)
elif param_type == "json":
self.post(url=url, json=param_dict, headers=headers_dict)
else:
raise ValueError("param_typ error")
elif method == "PUT":
if param_type == "data":
self.put(url=url, data=param_dict, headers=headers_dict)
elif param_type == "json":
self.put(url=url, json=param_dict, headers=headers_dict)
else:
raise ValueError("param_typ error")
elif method == "DELETE":
if param_type == "data":
self.delete(url=url, data=param_dict, headers=headers_dict)
elif param_type == "json":
self.delete(url=url, json=param_dict, headers=headers_dict)
else:
raise ValueError("param_typ error")
else:
raise ValueError("method error")
if assert_resp != "" or assert_resp != "{}":
assert_resp = json.loads(assert_resp)
self.assertJSON(assert_resp, exclude=exclude)
================================================
FILE: seldom/har2case/__init__.py
================================================
================================================
FILE: seldom/har2case/core.py
================================================
"""
har to case core
"""
import os
from seldom.logging import log
from seldom.har2case import utils
class HarParser:
"""HarParser class"""
def __init__(self, har_file_path: str):
self.har_file_path = har_file_path
self.case_template = """import seldom
class TestRequest(seldom.TestCase):
def start(self):
self.url = "{url}"
def test_case(self):
headers = {header}
cookies = {cookie}
self.{method}(self.url, {params}, headers=headers, cookies=cookies)
self.assertStatusCode({resp_status})
if __name__ == '__main__':
seldom.main()
"""
def _make_testcase(self) -> str:
"""
make test case.
test case are parsed from HAR log entries list.
"""
testcase = ""
log_entries = utils.load_har_log_entries(self.har_file_path)
for entry_json in log_entries:
url = entry_json["request"].get("url")
method = entry_json["request"].get("method").lower()
headers = entry_json["request"].get("headers")
cookies = entry_json["request"].get("cookies")
response_status = entry_json["response"].get("status")
headers_str = utils.list_to_dict_str(headers)
cookies_str = utils.list_to_dict_str(cookies)
if "?" in url:
url = url.split("?")[0]
if method in ["post", "put", "delete"]:
# from-data
params = entry_json["request"]["postData"].get("params")
if params is not None:
params_dict = utils.list_to_dict_str(params)
data_str = "data=" + params_dict
else:
data_str = "data={}"
# json
text = entry_json["request"]["postData"].get("text")
mime_type = entry_json["request"]["postData"].get("mimeType")
if mime_type is not None:
if "application/json" in mime_type:
data_str = "json=" + text
else:
data_str = "json={}"
elif method == "get":
# params
query_string = entry_json["request"].get("queryString")
if query_string is not None:
query_string_str = utils.list_to_dict_str(query_string)
data_str = "params=" + query_string_str
else:
data_str = "params={}"
else:
raise TypeError("Only POST/GET/PUT/DELETE methods are supported。")
testcase = self.case_template.format(header=headers_str,
cookie=cookies_str,
method=method,
url=url,
params=data_str,
resp_status=response_status)
return testcase
@staticmethod
def create_file(save_path: str, file_content: str = "") -> None:
"""
create test case file
"""
with open(save_path, 'w', encoding="utf8") as file:
file.write(file_content)
log.info(f"created file: {save_path}")
def gen_testcase(self) -> None:
"""
generate test case
"""
har_file = os.path.splitext(self.har_file_path)[0]
output_testcase_file = f"{har_file}.py"
log.info("Start to generate testcase.")
testcase = self._make_testcase()
har_path = os.path.dirname(os.path.abspath(har_file))
self.create_file(os.path.join(har_path, output_testcase_file), testcase)
if __name__ == '__main__':
hp = HarParser(os.path.join(os.path.dirname(os.path.abspath(__file__)), "demo.har"))
hp.gen_testcase()
================================================
FILE: seldom/har2case/demo.har
================================================
{
"log": {
"pages": [],
"entries": [
{
"time": 537,
"request": {
"headersSize": 277,
"postData": {
"text": "{\"key1\": \"value1\", \"key2\": \"value2\"}",
"mimeType": "application/json"
},
"queryString": [],
"headers": [
{
"name": "User-Agent",
"value": "python-requests/2.25.0"
},
{
"name": "Accept-Encoding",
"value": "gzip, deflate"
},
{
"name": "Accept",
"value": "application/json"
},
{
"name": "Connection",
"value": "keep-alive"
},
{
"name": "Host",
"value": "httpbin.org"
},
{
"name": "Content-Length",
"value": "36"
},
{
"name": "Origin",
"value": "http://httpbin.org"
},
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Cookie",
"value": "lang=zh"
}
],
"bodySize": 36,
"url": "http://httpbin.org/post",
"cookies": [
{
"name": "lang",
"value": "zh"
}
],
"method": "POST",
"httpVersion": "HTTP/1.1"
},
"timings": {
"blocked": -1,
"receive": 0,
"wait": 232,
"dns": 9,
"send": 0,
"connect": 296
},
"response": {
"headersSize": 249,
"bodySize": 606,
"statusText": "OK",
"redirectURL": "",
"status": 200,
"httpVersion": "HTTP/1.1",
"cookies": [],
"content": {
"compression": 0,
"text": "{\n \"args\": {}, \n \"data\": \"{\\\"key1\\\": \\\"value1\\\", \\\"key2\\\": \\\"value2\\\"}\", \n \"files\": {}, \n \"form\": {}, \n \"headers\": {\n \"Accept\": \"application/json\", \n \"Accept-Encoding\": \"gzip, deflate\", \n \"Content-Length\": \"36\", \n \"Content-Type\": \"application/json\", \n \"Cookie\": \"lang=zh\", \n \"Host\": \"httpbin.org\", \n \"Origin\": \"http://httpbin.org\", \n \"User-Agent\": \"python-requests/2.25.0\", \n \"X-Amzn-Trace-Id\": \"Root=1-60c729cb-3dc8394d3247ee142de982e4\"\n }, \n \"json\": {\n \"key1\": \"value1\", \n \"key2\": \"value2\"\n }, \n \"origin\": \"116.25.147.137\", \n \"url\": \"http://httpbin.org/post\"\n}\n",
"size": 606,
"mimeType": "application/json"
},
"headers": [
{
"name": "Date",
"value": "Mon, 14 Jun 2021 10:04:59 GMT"
},
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Content-Length",
"value": "606"
},
{
"name": "Connection",
"value": "keep-alive"
},
{
"name": "Server",
"value": "gunicorn/19.9.0"
},
{
"name": "Access-Control-Allow-Origin",
"value": "http://httpbin.org"
},
{
"name": "Access-Control-Allow-Credentials",
"value": "true"
}
]
},
"startedDateTime": "2021-06-14T18:04:59.9444171+08:00",
"cache": {}
}
],
"creator": {
"version": "5.0.20204.45441",
"name": "Fiddler"
},
"version": "1.1"
}
}
================================================
FILE: seldom/har2case/utils.py
================================================
"""
har to case utils
"""
import io
import sys
import json
from seldom.logging import log
def load_har_log_entries(file_path):
""" load HAR file and return log entries list
Args:
file_path (str)
Returns:
list: entries
[
{
"request": {},
"response": {}
},
{
"request": {},
"response": {}
}
]
"""
with io.open(file_path, "r+", encoding="utf-8-sig") as file:
try:
content_json = json.loads(file.read())
return content_json["log"]["entries"]
except (KeyError, TypeError):
log.error(f"HAR file content error: {file_path}")
sys.exit(1)
def list_to_dict_str(data: list) -> str:
"""
list -> dict -> string
"""
data_dict = {}
for param in data:
data_dict[param["name"]] = param["value"]
if len(data_dict) == 0:
data_dict_str = "{}"
else:
data_dict_str = json.dumps(data_dict)
return data_dict_str
================================================
FILE: seldom/logging/__init__.py
================================================
from .log import log
from .log import log_cfg
================================================
FILE: seldom/logging/exceptions.py
================================================
"""
Exceptions that may happen in all the seldom code.
"""
class SeldomException(Exception):
"""
Base seldom exception.
"""
def __init__(self, msg: str = None, screen: str = None, stacktrace: str = None):
self.msg = msg
self.screen = screen
self.stacktrace = stacktrace
def __str__(self):
exception_msg = f"Message: {self.msg}\n"
if self.screen is not None:
exception_msg += "Screenshot: available via screen\n"
if self.stacktrace is not None:
stacktrace = "\n".join(self.stacktrace)
exception_msg += f"Stacktrace:\n{stacktrace}"
return exception_msg
class BrowserTypeError(SeldomException):
"""
Browser type error
"""
pass
class NotFindElementError(SeldomException):
"""
No element errors were found
"""
pass
class TestFixtureRunError(SeldomException):
"""
Test fixture run error
"""
pass
class FileTypeError(SeldomException):
"""
Data file type error
"""
pass
class RunParamError(SeldomException):
"""
seldom run param error.
"""
pass
class RunningError(SeldomException):
"""
seldom running error
"""
pass
================================================
FILE: seldom/logging/log.py
================================================
"""
Seldom log
"""
import os
import sys
import time
import inspect
from loguru import logger
from seldom.running.config import BrowserConfig
stack_t = inspect.stack()
ins = inspect.getframeinfo(stack_t[1][0])
exec_dir = os.path.dirname(os.path.abspath(ins.filename))
report_dir = os.path.join(exec_dir, "reports")
if os.path.exists(report_dir) is False:
os.mkdir(report_dir)
with open(os.path.join(report_dir, "seldom_log.log"), "w") as log:
log.truncate(0)
if BrowserConfig.LOG_PATH is None:
BrowserConfig.LOG_PATH = os.path.join(report_dir, "seldom_log.log")
if BrowserConfig.REPORT_PATH is None:
now_time = time.strftime("%Y_%m_%d_%H_%M_%S")
BrowserConfig.REPORT_PATH = os.path.join(report_dir, now_time + "_result.html")
class LogConfig:
"""log config"""
def __init__(self, level: str = "DEBUG", colorlog: bool = True):
self.logger = logger
self._colorlog = colorlog
self._console_format = "{time:YYYY-MM-DD HH:mm:ss}> | {level: <8} | {file} | {thread.name} | {message} "
self._log_format = "{time: YYYY-MM-DD HH:mm:ss} | {level: <8} | {file: <10} | {thread.name} | {message}"
self._level = level
self.logfile = BrowserConfig.LOG_PATH
self.set_level(self._colorlog, self._console_format, self._level)
def set_level(self, colorlog: bool = True, format: str = None, level: str = "DEBUG"):
"""
setting level
:param colorlog:
:param format:
:param level:
:return:
"""
if format is None:
format = self._console_format
self.logger.remove()
self._level = level
self.logger.add(sys.stderr, level=level, colorize=colorlog, format=format)
self.logger.add(self.logfile, level=level, colorize=False, format=self._log_format, encoding="utf-8")
# log level: TRACE < DEBUG < INFO < SUCCESS < WARNING < ERROR
log_cfg = LogConfig(level="TRACE")
log = logger
================================================
FILE: seldom/project_temp/__init__.py
================================================
================================================
FILE: seldom/project_temp/api/confrun.py
================================================
"""
seldom confrun.py hooks function - api auto test project
Run:
> seldom -p test_dir
"""
def base_url():
"""
http test
api base url
"""
return "http://httpbin.org"
def debug():
"""
debug mod
"""
return False
def rerun():
"""
error/failure rerun times
"""
return 0
def report():
"""
setting report path
Used:
return "d://mypro/result.html"
return "d://mypro/result.xml"
"""
return None
def timeout():
"""
setting timeout
"""
return 10
def title():
"""
setting report title
"""
return "seldom test report"
def tester():
"""
setting report tester
"""
return "bugmaster"
def description():
"""
setting report description
"""
return ["windows", "jenkins"]
def language():
"""
setting report language
return "en"
return "zh-CN"
"""
return "en"
def whitelist():
"""test label white list"""
return []
def blacklist():
"""test label black list"""
return []
================================================
FILE: seldom/project_temp/api/run.py
================================================
"""
seldom.main() - Run seldom main method.
Run:
> python run.py
"""
import seldom
"""
参数说明:
path: 指定测试目录。
base_url: Http测试,指定接口地址。
title: 指定测试项目标题。
tester: 指定测试人员。
description: 指定测试环境描述。
debug: debug模式,设置为True不生成测试用例。
rerun: 测试失败重跑
language: 测试报告语言:en/zh-CN。
"""
if __name__ == '__main__':
seldom.main(path="./test_dir",
base_url="http://httpbin.org",
title="seldom API test demo",
tester="虫师",
rerun=2,
language="zh-CN")
================================================
FILE: seldom/project_temp/api/test_sample.py
================================================
import seldom
from seldom import file_data
class TestRequest(seldom.TestCase):
def test_put_method(self):
"""test put case"""
self.put('/put', data={'key': 'value'})
self.assertStatusCode(200)
def test_post_method(self):
"""test post case"""
self.post('/post', data={'key': 'value'})
self.assertStatusCode(200)
def test_get_method(self):
"""test get case"""
payload = {'key1': 'value1', 'key2': 'value2'}
self.get('/get', params=payload)
self.assertStatusCode(200)
def test_delete_method(self):
"""test delete case"""
self.delete('/delete')
self.assertStatusCode(200)
class TestDDT(seldom.TestCase):
@file_data(file="data.json", key="api")
def test_get_method(self, _, id_, name):
"""test ddt case"""
payload = {'key1': id_, 'key2': name}
self.get('/get', params=payload)
self.assertStatusCode(200)
if __name__ == '__main__':
seldom.main(base_url="http://httpbin.org")
================================================
FILE: seldom/project_temp/app/confrun.py
================================================
"""
seldom confrun.py hooks function - app auto test project
Run:
> seldom -p test_dir
"""
from seldom.appium_lab.android import UiAutomator2Options
def app_info():
"""
app UI test
appium app config
"""
capabilities = {
'deviceName': 'ELS-AN00',
'automationName': 'UiAutomator2',
'platformName': 'Android',
'appPackage': 'com.microsoft.bing',
'appActivity': 'com.microsoft.sapphire.app.main.MainSapphireActivity',
'noReset': True,
}
options = UiAutomator2Options().load_capabilities(capabilities)
return options
def app_server():
"""
app UI test
appium server address
"""
return "http://127.0.0.1:4723"
def debug():
"""
debug mod
"""
return False
def rerun():
"""
error/failure rerun times
"""
return 0
def report():
"""
setting report path
Used:
return "d://mypro/result.html"
return "d://mypro/result.xml"
"""
return None
def timeout():
"""
setting timeout
"""
return 10
def title():
"""
setting report title
"""
return "seldom test report"
def tester():
"""
setting report tester
"""
return "bugmaster"
def description():
"""
setting report description
"""
return ["windows", "jenkins"]
def language():
"""
setting report language
return "en"
return "zh-CN"
"""
return "en"
def whitelist():
"""test label white list"""
return []
def blacklist():
"""test label black list"""
return []
================================================
FILE: seldom/project_temp/app/run.py
================================================
"""
seldom.main() - Run seldom main method.
Run:
> python run.py
"""
import seldom
from seldom.appium_lab.android import UiAutomator2Options
"""
参数说明:
path: 指定测试目录。
app_info: 启动app配置。
app_server: appium server 地址。
title: 指定测试项目标题。
tester: 指定测试人员。
description: 指定测试环境描述。
debug: debug模式,设置为True不生成测试用例。
rerun: 测试失败重跑。
language: 测试报告语言:en/zh-CN。
"""
if __name__ == '__main__':
capabilities = {
'deviceName': 'ELS-AN00',
'automationName': 'UiAutomator2',
'platformName': 'Android',
'appPackage': 'com.microsoft.bing',
'appActivity': 'com.microsoft.sapphire.app.main.MainSapphireActivity',
'noReset': True,
}
options = UiAutomator2Options().load_capabilities(capabilities)
seldom.main(path="./test_dir",
app_info=options,
app_server="http://127.0.0.1:4723",
title="Seldom App test demo",
tester="虫师",
rerun=2,
language="zh-CN")
================================================
FILE: seldom/project_temp/app/test_sample.py
================================================
from appium.options.android import UiAutomator2Options
import seldom
from seldom.appium_lab.keyboard import KeyEvent
class TestBingApp(seldom.TestCase):
"""
Test Bing APP
"""
def start(self):
self.ke = KeyEvent(self.driver)
def test_bing_search(self):
"""
test bing App search
"""
self.sleep(2)
self.click(id_="com.microsoft.bing:id/sa_hp_header_search_box")
self.type(id_="com.microsoft.bing:id/sapphire_search_header_input", text="seldomQA")
self.ke.press_key("ENTER")
self.sleep(1)
elem = self.get_elements(xpath='//android.widget.TextView')
self.assertIn("seldom", elem[0].text.lower())
if __name__ == '__main__':
capabilities = {
'deviceName': 'ELS-AN00',
'automationName': 'UiAutomator2',
'platformName': 'Android',
'appPackage': 'com.microsoft.bing',
'appActivity': 'com.microsoft.sapphire.app.main.MainSapphireActivity',
'noReset': True,
}
options = UiAutomator2Options().load_capabilities(capabilities)
seldom.main(app_server="http://127.0.0.1:4723", app_info=options, debug=True)
================================================
FILE: seldom/project_temp/data.json
================================================
{
"bing": [
[
"case1",
"seldom"
],
[
"case2",
"poium"
],
[
"case3",
"XTestRunner"
]
],
"api": [
{
"case": "case1",
"id": 1,
"name": "tom"
},
{
"case": "case2",
"id": 2,
"name": "jack"
}
]
}
================================================
FILE: seldom/project_temp/web/confrun.py
================================================
"""
seldom confrun.py hooks function - web auto test project
Run:
> seldom -p test_dir
"""
def browser():
"""
web UI test:
browser: gc(google chrome)/ff(firefox)/edge/ie/safari
"""
return "gc"
def debug():
"""
debug mod
"""
return False
def rerun():
"""
error/failure rerun times
"""
return 0
def report():
"""
setting report path
Used:
return "d://mypro/result.html"
return "d://mypro/result.xml"
"""
return None
def timeout():
"""
setting timeout
"""
return 10
def title():
"""
setting report title
"""
return "seldom test report"
def tester():
"""
setting report tester
"""
return "bugmaster"
def description():
"""
setting report description
"""
return ["windows", "jenkins"]
def language():
"""
setting report language
return "en"
return "zh-CN"
"""
return "en"
def whitelist():
"""test label white list"""
return []
def blacklist():
"""test label black list"""
return []
================================================
FILE: seldom/project_temp/web/run.py
================================================
"""
seldom.main() - Run seldom main method.
Run:
> python run.py
"""
import seldom
"""
参数说明:
path: 指定测试目录。
browser: Web测试,指定浏览器,默认chrome。
title: 指定测试项目标题。
tester: 指定测试人员。
description: 指定测试环境描述。
debug: debug模式,设置为True不生成测试用例。
rerun: 测试失败重跑。
language: 测试报告语言:en/zh-CN。
"""
if __name__ == '__main__':
seldom.main(path="./test_dir",
browser="chrome",
title="Seldom Web test demo",
tester="虫师",
description=["Browser: Chrome"],
rerun=2,
language="zh-CN")
================================================
FILE: seldom/project_temp/web/test_sample.py
================================================
import seldom
from seldom import file_data
class SampleTest(seldom.TestCase):
def test_case(self):
"""a simple test case """
self.open("https://www.selenium.dev")
self.assertTitle("Selenium")
self.assertInUrl("selenium.dev")
class DDTTest(seldom.TestCase):
@file_data(file="data.json", key="bing")
def test_data_driver(self, _, keyword):
""" data driver case """
self.open("https://cn.bing.com")
self.type(id_="sb_form_q", text=keyword, enter=True)
self.assertInTitle(keyword)
if __name__ == '__main__':
seldom.main(debug=True)
================================================
FILE: seldom/request.py
================================================
"""
seldom requests
"""
import os
import ast
import time
import json
import socket
from typing import Any
from functools import wraps
from urllib.parse import urlparse
import requests
from seldom.running.config import Seldom
from seldom.running.loader_hook import loader
from seldom.logging import log
from seldom.utils import jmespath as utils_jmespath
from seldom.extend_lib import jsonpath as lib_jsonpath
from seldom.extend_lib import to_curl
IMG = ["jpg", "jpeg", "gif", "bmp", "webp"]
class ResponseResult:
status_code = 200
response = None
request = None
def formatting(msg):
"""formatted message"""
if isinstance(msg, dict):
return json.dumps(msg, indent=2, ensure_ascii=False)
return msg
def request(func):
def wrapper(*args, **kwargs):
func_name = func.__name__
log.info('-------------- [📤] Request -----------------')
try:
url = list(args)[1]
except IndexError:
url = kwargs.get("url", "")
if (Seldom.base_url is not None) and (url.startswith("http") is False):
url = Seldom.base_url + url
img_file = False
file_type = url.split(".")[-1]
if file_type in IMG:
img_file = True
log.info(f"[method]: {func_name.upper()} [url]: {url} ")
auth = kwargs.get("auth", None)
headers = kwargs.get("headers", None)
cookies = kwargs.get("cookies", None)
params = kwargs.get("params", None)
data = kwargs.get("data", None)
json_ = kwargs.get("json", None)
files = kwargs.get("files", None)
if auth is not None:
log.debug(f"[auth]:\n{auth}")
if headers is not None:
log.debug(f"[headers]:\n{formatting(headers)}")
if cookies is not None:
log.debug(f"[cookies]:\n{formatting(cookies)}")
if params is not None:
log.debug(f"[params]:\n{formatting(params)}")
if data is not None:
log.debug(f"[data]:\n{formatting(data)}")
if json_ is not None:
log.debug(f"[json]:\n{formatting(json_)}")
if files is not None:
log.debug(f"[files]:\n{files}")
# running function
r = func(*args, **kwargs)
ResponseResult.request = r.request
ResponseResult.status_code = r.status_code
log.info("-------------- [📨] Response ----------------")
if ResponseResult.status_code == 200 or ResponseResult.status_code == 304:
log.info(f"successful with status {ResponseResult.status_code}")
else:
log.warning(f"unsuccessful with status {ResponseResult.status_code}")
resp_time = r.elapsed.total_seconds()
try:
resp = r.json()
log.debug(f"[type]: json [time]: {resp_time}")
log.debug(f"[response]:\n {formatting(resp)}")
ResponseResult.response = resp
except BaseException as msg:
log.debug("[warning]: failed to convert res to json, try to convert to text")
log.trace(f"[warning]: {msg}")
if img_file is True:
log.debug(f"[type]: {file_type} [time]: {resp_time}")
ResponseResult.response = r.content
else:
r.encoding = 'utf-8'
log.debug(f"[type]: text [time]: {resp_time}")
log.debug(f"[response]:\n {r.text}")
ResponseResult.response = r.text
return r
return wrapper
def mock_url(url: str) -> str:
"""
If the mock hook is set, replace it with the mock url
:param url:
"""
configs = loader("mock_url") if loader("mock_url") is not None else None
if configs is None:
return url
replace_url = configs.get(url, "")
if replace_url == "":
return url
log.debug(f"mock url: {replace_url}")
return replace_url
def check_proxies() -> Any | None:
"""
check http proxies
"""
configs = loader("proxies") if loader("proxies") is not None else None
return configs
class HttpRequest:
"""seldom http request class"""
def __init__(self, base_url=None, *args, **kwargs):
self.base_url = base_url
if self.base_url is not None:
Seldom.base_url = self.base_url
self.args = args
self.kwargs = kwargs
@request
def get(self, url, params=None, **kwargs):
if (Seldom.base_url is not None) and (url.startswith("http") is False):
url = Seldom.base_url + url
url = mock_url(url)
if kwargs.get('proxies', None) is None:
kwargs["proxies"] = check_proxies()
return requests.get(url, params=params, timeout=Seldom.timeout, **kwargs)
@request
def post(self, url, data=None, json=None, **kwargs):
if (Seldom.base_url is not None) and (url.startswith("http") is False):
url = Seldom.base_url + url
url = mock_url(url)
if kwargs.get("proxies", None) is None:
kwargs["proxies"] = check_proxies()
return requests.post(url, data=data, json=json, timeout=Seldom.timeout, **kwargs)
@request
def put(self, url, data=None, **kwargs):
if (Seldom.base_url is not None) and (url.startswith("http") is False):
url = Seldom.base_url + url
url = mock_url(url)
if kwargs.get("proxies", None) is None:
kwargs["proxies"] = check_proxies()
return requests.put(url, data=data, timeout=Seldom.timeout, **kwargs)
@request
def delete(self, url, **kwargs):
if (Seldom.base_url is not None) and (url.startswith("http") is False):
url = Seldom.base_url + url
url = mock_url(url)
if kwargs.get("proxies", None) is None:
kwargs["proxies"] = check_proxies()
return requests.delete(url, timeout=Seldom.timeout, **kwargs)
@request
def patch(self, url, data=None, **kwargs):
if (Seldom.base_url is not None) and (url.startswith("http") is False):
url = Seldom.base_url + url
url = mock_url(url)
if kwargs.get("proxies", None) is None:
kwargs["proxies"] = check_proxies()
return requests.patch(url, data=data, timeout=Seldom.timeout, **kwargs)
@property
def response(self) -> Any:
"""
Returns the result of the response.
:return:
"""
return ResponseResult.response
@staticmethod
def jsonpath(expr, index: int = None, response=None) -> Any:
"""
Extract the data in response
mode:
* jsonpath: https://goessner.net/articles/JsonPath/
* jmespath: https://jmespath.org/
"""
if response is None:
response = ResponseResult.response
ret = lib_jsonpath(response, expr)
if index is not None:
ret = ret[index]
return ret
@staticmethod
def jmespath(expr, response=None) -> Any:
"""
Extract the data in response
* jmespath: https://jmespath.org/
"""
if response is None:
response = ResponseResult.response
ret = utils_jmespath(response, expr)
return ret
@property
def status_code(self) -> int:
"""
Returns the result of the status code
:return: status_code
"""
return ResponseResult.status_code
@staticmethod
def curl(request=None, compressed: bool = False, verify: bool = True) -> str:
"""
requests to cURL command
:param request: request object
:param compressed:
:param verify:
:return:
"""
if request is None:
return to_curl(ResponseResult.request, compressed, verify)
return to_curl(request, compressed, verify)
class Session(requests.Session):
@request
def get(self, url, **kwargs):
r"""Sends a GET request. Returns :class:`Response` object.
:param url: URL for the new :class:`Request` object.
:param \*\*kwargs: Optional arguments that ``request`` takes.
:rtype: requests.Response
"""
if (Seldom.base_url is not None) and (url.startswith("http") is False):
url = Seldom.base_url + url
url = mock_url(url)
if kwargs.get("proxies", None) is None:
kwargs["proxies"] = check_proxies()
kwargs.setdefault('allow_redirects', True)
return self.request('GET', url, **kwargs)
@request
def post(self, url, data=None, json=None, **kwargs):
r"""Sends a POST request. Returns :class:`Response` object.
:param url: URL for the new :class:`Request` object.
:param data: (optional) Dictionary, list of tuples, bytes, or file-like
object to send in the body of the :class:`Request`.
:param json: (optional) json to send in the body of the :class:`Request`.
:param \*\*kwargs: Optional arguments that ``request`` takes.
:rtype: requests.Response
"""
if (Seldom.base_url is not None) and (url.startswith("http") is False):
url = Seldom.base_url + url
url = mock_url(url)
if kwargs.get("proxies", None) is None:
kwargs["proxies"] = check_proxies()
return self.request('POST', url, data=data, json=json, **kwargs)
@request
def put(self, url, data=None, **kwargs):
r"""Sends a PUT request. Returns :class:`Response` object.
:param url: URL for the new :class:`Request` object.
:param data: (optional) Dictionary, list of tuples, bytes, or file-like
object to send in the body of the :class:`Request`.
:param \*\*kwargs: Optional arguments that ``request`` takes.
:rtype: requests.Response
"""
if (Seldom.base_url is not None) and (url.startswith("http") is False):
url = Seldom.base_url + url
url = mock_url(url)
if kwargs.get("proxies", None) is None:
kwargs["proxies"] = check_proxies()
return self.request('PUT', url, data=data, **kwargs)
@request
def delete(self, url, **kwargs):
r"""Sends a DELETE request. Returns :class:`Response` object.
:param url: URL for the new :class:`Request` object.
:param \*\*kwargs: Optional arguments that ``request`` takes.
:rtype: requests.Response
"""
if (Seldom.base_url is not None) and (url.startswith("http") is False):
url = Seldom.base_url + url
url = mock_url(url)
if kwargs.get("proxies", None) is None:
kwargs["proxies"] = check_proxies()
return self.request('DELETE', url, **kwargs)
@staticmethod
def json_to_dict(data: str, replace_quotes: bool = True) -> dict:
"""
json to dict
:param data: json data.
:param replace_quotes: whether to replace single quotes.
"""
if isinstance(data, dict):
return data
elif isinstance(data, str):
try:
data_dict = ast.literal_eval(data)
except ValueError:
try:
if replace_quotes:
data = data.replace('\'', '\"')
data_dict = json.loads(data)
except json.decoder.JSONDecodeError:
log.error(f"json to dict error. --> {data}")
return {}
else:
return data_dict
else:
return data_dict
else:
log.error(f"type error --> {data}")
return {}
@property
def base_url(self):
"""
return base url (http)
"""
return Seldom.base_url
@staticmethod
def save_response(response: requests.Response, filename: str = None):
"""
save response.
:param response:
:param filename:
:return:
"""
# Determine content type
content_type = response.headers.get('Content-Type', '').lower()
data = response.text
ext = '.txt'
if 'application/json' in content_type or response.text.strip().startswith('{'):
try:
data = response.json()
ext = '.json'
except requests.exceptions.JSONDecodeError:
pass
if filename is None:
timestamp = int(time.time() * 1000)
filename = f"response_{timestamp}{ext}"
else:
root, _ = os.path.splitext(filename)
filename = f"{root}{ext}"
# Save file
with open(filename, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=4) if ext == '.json' else f.write(data)
return filename
def ip_address(self, url: str = None) -> str:
"""
request ip ip_address
:param url:
:return:
"""
if url is None:
url = self.base_url
parsed_url = urlparse(url)
domain = parsed_url.netloc
ip_address = socket.gethostbyname(domain)
log.info(f"🌐 IP address: {ip_address}")
return ip_address
@base_url.setter
def base_url(self, value):
self._base_url = value
def check_response(describe: str = "", status_code: int = 200, ret: str = None, check: dict = None,
debug: bool = False):
"""
checkout response data
:param describe: interface describe
:param status_code: http status code
:param ret: return data
:param check: check data
:param debug: debug Ture/False
:return:
"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
func_name = func.__name__
if debug is True:
log.debug(f"Execute {func_name} - args: {args}")
log.debug(f"Execute {func_name} - kwargs: {kwargs}")
r = func(*args, **kwargs)
flat = True
if r.status_code != status_code:
log.error(f"Execute {func_name} - {describe} failed: {r.status_code}")
flat = False
try:
r.json()
except json.decoder.JSONDecodeError:
log.error(f"Execute {func_name} - {describe} failed:Not in JSON format")
flat = False
if debug is True:
log.debug(f"Execute {func_name} - response:\n {r.json()}")
if flat is True:
log.info(f"Execute {func_name} - {describe} success!")
if check is not None:
for expr, value in check.items():
data = utils_jmespath(r.json(), expr)
if data != value:
log.error(f"Execute {func_name} - check data failed:{expr} = {value}")
log.error(f"Execute {func_name} - response:{r.json()}")
raise ValueError(f"{data} != {value}")
if ret is not None:
data = utils_jmespath(r.json(), ret)
if data is None:
log.error(f"Execute {func_name} - return {ret} is None")
return data
return r.json()
return wrapper
return decorator
# @check_response as @api
api = check_response
def retry(times: int = 3, wait: int = 1):
"""
retry the decorator
:param: times: times of retries
:wait: retry interval, Default(s)
"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
attempts = 0
while attempts < times:
try:
return func(*args, **kwargs)
except Exception as e:
log.warning(
f"""Attempt to execute <{func.__name__}> failed with error: '{e}'. Attempting retry number {attempts + 1}...""")
time.sleep(wait)
attempts += 1
return func(*args, **kwargs)
return wrapper
return decorator
================================================
FILE: seldom/running/DebugTestRunner.py
================================================
"""
Run tests in debug mode
"""
import unittest
import functools
from seldom.utils.benchmark import benchmark
from seldom.running.config import Seldom
class DebugTestRunner(unittest.TextTestRunner):
"""Debug test runner"""
def __init__(self, *args, **kwargs):
"""
Append blacklist & whitelist attributes to TestRunner instance
"""
self.whitelist = set(kwargs.pop('whitelist', []))
self.blacklist = set(kwargs.pop('blacklist', []))
super(DebugTestRunner, self).__init__(*args, **kwargs)
@classmethod
def test_iter(cls, suite):
"""
Iterate through test suites, and yield individual tests
"""
for test in suite:
if isinstance(test, unittest.TestSuite):
for t in cls.test_iter(test):
yield t
else:
yield test
def run(self, testlist):
"""
Run the given test case or test suite.
"""
# Change given testlist into a TestSuite
suite = unittest.TestSuite()
# Add each test in testlist, apply skip mechanism if necessary
for test in self.test_iter(testlist):
# Determine if test should be skipped
skip = bool(self.whitelist)
test_method = getattr(test, test._testMethodName)
test_labels = getattr(test, '_labels', set()) | getattr(test_method, '_labels', set())
if test_labels & self.whitelist:
skip = False
if test_labels & self.blacklist:
skip = True
if skip:
# Test should be skipped.
# replace original method with function "skip"
# Create a "skip test" wrapper for the actual test method
@functools.wraps(test_method)
def skip_wrapper(*args, **kwargs):
raise unittest.SkipTest('label exclusion')
skip_wrapper.__unittest_skip__ = True
if len(self.whitelist) >= 1:
skip_wrapper.__unittest_skip_why__ = f'label whitelist {self.whitelist}'
if len(self.blacklist) >= 1:
skip_wrapper.__unittest_skip_why__ = f'label blacklist {self.blacklist}'
setattr(test, test._testMethodName, skip_wrapper)
suite.addTest(test)
# Resume normal TextTestRunner function with the new test suite
if Seldom.benchmark is True:
result = super(DebugTestRunner, self).run(suite)
benchmark.report()
return result
super(DebugTestRunner, self).run(suite)
================================================
FILE: seldom/running/__init__.py
================================================
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
================================================
FILE: seldom/running/config.py
================================================
"""
Seldom configuration file
"""
import logging
import threading
import requests
class Seldom:
"""
Seldom browser driver
"""
_thread_local = threading.local()
@property
def driver(self):
"""
Browser or App driver
"""
return getattr(self._thread_local, 'driver', None)
@driver.setter
def driver(self, value):
self._thread_local.driver = value
@property
def base_url(self):
"""
API base url
"""
return getattr(self._thread_local, 'base_url', None)
@base_url.setter
def base_url(self, value):
self._thread_local.base_url = value
@property
def device(self):
"""
Android base device
"""
return getattr(self._thread_local, 'device', None)
@device.setter
def device(self, value):
self._thread_local.device = value
timeout = 10
debug = False
compare_url = None
app_server = None
app_info = None
app_package = None
extensions = None
env = None
api_data_url = None
benchmark = False
Seldom = Seldom()
class BrowserConfig:
"""
Define run browser config
"""
NAME = None
REPORT_PATH = None
REPORT_TITLE = "Seldom Test Report"
LOG_PATH = None
# driver config
options = None
command_executor = ""
executable_path = None
def base_url():
"""return base url"""
return Seldom.base_url
def driver():
"""return driver"""
return Seldom.driver
def env():
"""return env"""
return Seldom.env
class FileRunningConfig:
"""
file runner config
"""
api_excel_file_name = None
def report_local_style() -> bool:
"""
Check report with local style
:return:
"""
try:
resp = requests.get("https://telegraph-image-cq2.pages.dev")
if resp.status_code != 200:
return True
else:
return False
except BaseException as msg:
logging.debug(msg)
return True
================================================
FILE: seldom/running/loader_extend.py
================================================
"""seldom Loading unittests."""
import os
import sys
import functools
from fnmatch import fnmatchcase
from unittest.loader import TestLoader
class SeldomTestLoader(TestLoader):
"""
This class is responsible for loading tests according to various criteria
and returning them wrapped in a TestSuite
"""
testNamePatterns = None
collectCaseInfo = False # Switch of collecting use case information
collectCaseList = [] # List of use case information
def getTestCaseNames(self, testCaseClass):
"""Return a sorted sequence of method names found within testCaseClass
"""
def shouldIncludeMethod(attrname):
"""
should Include Method
:param attrname:
:return:
"""
if not attrname.startswith(self.testMethodPrefix):
return False
testFunc = getattr(testCaseClass, attrname)
testLabels = getattr(testFunc, '_labels', set())
if testLabels:
label_str = next(iter(testLabels))
else:
label_str = None
if not callable(testFunc):
return False
fullName = f"""{testCaseClass.__module__}.{testCaseClass.__qualname__}.{attrname}"""
if self.collectCaseInfo is True:
case_info = {
"file": testCaseClass.__module__,
"class": {
"name": testCaseClass.__name__,
"doc": testCaseClass.__doc__
},
"method": {
"name": attrname,
"doc": testFunc.__doc__,
"label": label_str
}
}
self.collectCaseList.append(case_info)
return self.testNamePatterns is None or \
any(fnmatchcase(fullName, pattern) for pattern in self.testNamePatterns)
testFnNames = list(filter(shouldIncludeMethod, dir(testCaseClass)))
if self.sortTestMethodsUsing:
testFnNames.sort(key=functools.cmp_to_key(self.sortTestMethodsUsing))
return testFnNames
def rediscover(self, start_dir, pattern='test*.py', top_level_dir=None):
"""Find and return all test modules from the specified start
directory, recursing into subdirectories to find them and return all
tests found within them. Only test files that match the pattern will
be loaded. (Using shell style pattern matching.)
All test modules must be importable from the top level of the project.
If the start directory is not the top level directory then the top
level directory must be specified separately.
If a test package name (directory with '__init__.py') matches the
pattern then the package will be checked for a 'load_tests' function. If
this exists then it will be called with (loader, tests, pattern) unless
the package has already had load_tests called from the same discovery
invocation, in which case the package module object is not scanned for
tests - this ensures that when a package uses discover to further
discover child tests that infinite recursion does not happen.
If load_tests exists then discovery does *not* recurse into the package,
load_tests is responsible for loading all tests in the package.
The pattern is deliberately not stored as a loader attribute so that
packages can continue discovery themselves. top_level_dir is stored so
load_tests does not need to pass this argument in to loader.discover().
Paths are sorted before being imported to ensure reproducible execution
order even on filesystems with non-alphabetical ordering like ext3/4.
*** discover() vs rediscover() ***
rediscover(): Remove self._top_level_dir
"""
set_implicit_top = False
if top_level_dir is None:
set_implicit_top = True
top_level_dir = start_dir
top_level_dir = os.path.abspath(top_level_dir)
if not top_level_dir in sys.path:
# all test modules must be importable from the top level directory
# should we *unconditionally* put the start directory in first
# in sys.path to minimise likelihood of conflicts between installed
# modules and development versions?
sys.path.insert(0, top_level_dir)
self._top_level_dir = top_level_dir
is_not_importable = False
is_namespace = False
tests = []
if os.path.isdir(os.path.abspath(start_dir)):
start_dir = os.path.abspath(start_dir)
if start_dir != top_level_dir:
is_not_importable = not os.path.isfile(os.path.join(start_dir, '__init__.py'))
else:
# support for discovery from dotted module names
try:
__import__(start_dir)
except ImportError:
is_not_importable = True
else:
the_module = sys.modules[start_dir]
top_part = start_dir.split('.')[0]
try:
start_dir = os.path.abspath(
os.path.dirname((the_module.__file__)))
except AttributeError:
# look for namespace packages
try:
spec = the_module.__spec__
except AttributeError:
spec = None
if spec and spec.loader is None:
if spec.submodule_search_locations is not None:
is_namespace = True
for path in the_module.__path__:
if (not set_implicit_top and
not path.startswith(top_level_dir)):
continue
self._top_level_dir = \
(path.split(the_module.__name__
.replace(".", os.path.sep))[0])
tests.extend(self._find_tests(path,
pattern,
namespace=True))
elif the_module.__name__ in sys.builtin_module_names:
# builtin module
raise TypeError('Can not use builtin modules '
'as dotted module names') from None
else:
raise TypeError(
'don\'t know how to discover from {!r}'
.format(the_module)) from None
if set_implicit_top:
if not is_namespace:
self._top_level_dir = \
self._get_directory_containing_module(top_part)
sys.path.remove(top_level_dir)
else:
sys.path.remove(top_level_dir)
if is_not_importable:
raise ImportError('Start directory is not importable: %r' % start_dir)
if not is_namespace:
tests = list(self._find_tests(start_dir, pattern))
return self.suiteClass(tests)
seldomTestLoader = SeldomTestLoader()
================================================
FILE: seldom/running/loader_hook.py
================================================
import os
import sys
import importlib
def loader(func_name: str, file_name: str = "confrun.py", *args, **kwargs):
"""
Execute the hook function dynamically.
:param func_name: function name
:param file_name: hook file name
"""
# By default, confrun.py files are searched in the current directory
file_dir = os.getcwd()
sys.path.insert(0, file_dir)
all_hook_files = list(filter(lambda x: x.endswith(file_name), os.listdir(file_dir)))
all_hook_module = list(map(lambda x: x.replace(".py", ""), all_hook_files))
hooks = []
for module_name in all_hook_module:
hooks.append(importlib.import_module(module_name))
# Execute the function according to the name
for per_hook in hooks:
try:
func = getattr(per_hook, func_name)
return func(*args, **kwargs)
except AttributeError:
return None
if __name__ == '__main__':
browser = loader("browser")
print(f"get browser name: {browser}")
================================================
FILE: seldom/running/runner.py
================================================
"""
seldom main
"""
import ast
import builtins
import inspect
import json as sys_json
import os
import re
import unittest
import webbrowser
from typing import Dict, List, Any
from XTestRunner import HTMLTestRunner
from XTestRunner import XMLTestRunner
from selenium.common.exceptions import InvalidSessionIdException
from selenium.webdriver.remote.webdriver import WebDriver as SeleniumWebDriver
from seldom.driver import Browser
from seldom.logging import log
from seldom.logging import log_cfg
from seldom.logging.exceptions import SeldomException, RunParamError
from seldom.running.DebugTestRunner import DebugTestRunner
from seldom.running.config import Seldom, BrowserConfig
from seldom.running.config import base_url as base_url_func
from seldom.running.config import driver as driver_func
from seldom.running.config import env as env_func
from seldom.running.config import report_local_style
from seldom.running.loader_extend import seldomTestLoader
from seldom.running.loader_hook import loader
INIT_FILE = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "__init__.py")
_version_re = re.compile(r'__version__\s+=\s+(.*)')
with open(INIT_FILE, 'rb') as f:
VERSION = str(ast.literal_eval(_version_re.search(
f.read().decode('utf-8')).group(1)))
SELDOM_STR = r"""
__ __
________ / /___/ /___ ____ ____
/ ___/ _ \/ / __ / __ \/ __ ` ___/
(__ ) __/ / /_/ / /_/ / / / / / /
/____/\___/_/\__,_/\____/_/ /_/ /_/ v""" + VERSION + """
-----------------------------------------
@itest.info
"""
class TestMain:
"""
Reimplemented Seldom Runner, Support for Web and API
"""
TestSuits = []
def __init__(
self,
path: str | list[str] | None = None,
case: str | None = None,
browser: str | dict[str, Any] | None = None,
base_url: str | None = None,
debug: bool = False,
timeout: int = 10,
app_server: str | None = None,
app_info: dict[str, Any] | None = None,
report: str | None = None,
title: str = "Seldom Test Report",
tester: str = "Anonymous",
description: str | list[str] = "Test case execution",
rerun: int = 0,
language: str = "en",
whitelist: list[str] | None = None,
blacklist: list[str] | None = None,
open_report: bool = True,
auto: bool = True,
extensions: list[str] | dict[str, Any] | None = None,
failfast: bool = False,
env: str | None = None,
benchmark: bool = False,
device: str | None = None,
):
"""
runner test case
:param path:
:param case:
:param browser:
:param base_url:
:param title:
:param tester:
:param description:
:param debug:
:param timeout:
:param app_server:
:param app_info:
:param report:
:param rerun:
:param language:
:param whitelist:
:param blacklist:
:param open_report:
:param auto:
:param extensions:
:parma failfast: only support debug=True
:parma env:
:parma benchmark:
:parma device:
:return:
"""
print(SELDOM_STR)
self.path = path
self.case = case
self.browser = browser
self.report = report
self.title = BrowserConfig.REPORT_TITLE = title
self.tester = tester
self.description = description
self.debug = debug
self.rerun = rerun
self.language = language
self.whitelist = whitelist if whitelist is not None else []
self.blacklist = blacklist if blacklist is not None else []
self.open_report = open_report
self.auto = auto
self.failfast = failfast
self.device = device
Seldom.app_server = app_server
Seldom.app_info = app_info
Seldom.extensions = extensions
Seldom.env = env
Seldom.device = device
if failfast is True and debug is False:
raise RunParamError("failfast cannot be true, setting `debug=True`")
if isinstance(timeout, int) is False:
raise TypeError(f"Timeout {timeout} is not integer.")
if isinstance(debug, bool) is False:
raise TypeError(f"Debug {debug} is not Boolean type.")
Seldom.timeout = timeout
Seldom.debug = debug
Seldom.base_url = base_url
setattr(builtins, 'base_url', base_url_func)
setattr(builtins, 'driver', driver_func)
setattr(builtins, 'env', env_func)
if benchmark is True:
self.debug = True
Seldom.benchmark = True
# ----- Global open browser -----
loader("start_run")
self.open_browser()
if self.case is not None:
self.TestSuits = seldomTestLoader.loadTestsFromName(self.case)
elif self.path is None:
stack_t = inspect.stack()
ins = inspect.getframeinfo(stack_t[1][0])
file_dir = os.path.dirname(os.path.abspath(ins.filename))
file_path = ins.filename
if "\\" in file_path:
this_file = file_path.split("\\")[-1]
elif "/" in file_path:
this_file = file_path.split("/")[-1]
else:
this_file = file_path
self.TestSuits = seldomTestLoader.discover(file_dir, this_file)
else:
paths = []
if isinstance(self.path, str):
paths.append(self.path)
elif isinstance(self.path, list):
paths = self.path
else:
raise TypeError("The `path` type is incorrect. Only list or string is supported.")
self.TestSuits = unittest.TestSuite()
for path in paths:
log.info(f"TestLoader: {path}")
if len(path) > 3:
if path[-3:] == ".py":
if "/" in path:
path_list = path.split("/")
path_dir = path.replace(path_list[-1], "")
test_suits = seldomTestLoader.discover(path_dir, pattern=path_list[-1])
elif "\\" in path:
path_list = path.split("\\")
path_dir = path.replace(path_list[-1], "")
test_suits = seldomTestLoader.discover(path_dir, pattern=path_list[-1])
else:
test_suits = seldomTestLoader.discover(os.getcwd(), pattern=path)
else:
test_suits = seldomTestLoader.rediscover(path)
else:
test_suits = seldomTestLoader.discover(path)
if isinstance(self.path, str):
self.TestSuits = test_suits
break
self.TestSuits.addTest(test_suits)
if self.auto is True:
self.run(self.TestSuits)
# ----- Close browser globally -----
self.close_browser()
loader("end_run")
def run(self, suits) -> None:
"""
run test case
"""
if self.debug is False:
for filename in os.listdir(os.getcwd()):
if filename == "reports":
break
else:
os.mkdir(os.path.join(os.getcwd(), "reports"))
if (self.report is None) and (BrowserConfig.REPORT_PATH is not None):
report_path = BrowserConfig.REPORT_PATH
else:
report_path = BrowserConfig.REPORT_PATH = os.path.join(os.getcwd(), "reports", self.report)
with open(report_path, 'wb') as fp:
if report_path.split(".")[-1] == "xml":
runner = XMLTestRunner(output=fp, logger=log_cfg, rerun=self.rerun,
blacklist=self.blacklist, whitelist=self.whitelist)
runner.run(suits)
else:
is_local = report_local_style()
if self.device is not None:
self.title = f"{self.title} For {self.device}"
runner = HTMLTestRunner(stream=fp, title=self.title, tester=self.tester,
description=self.description, local_style=is_local,
rerun=self.rerun, logger=log_cfg,
language=self.language, blacklist=self.blacklist, whitelist=self.whitelist)
runner.run(suits)
log.success(f"generated html file: file:///{report_path}")
log.success(f"generated log file: file:///{BrowserConfig.LOG_PATH}")
if self.open_report is True:
webbrowser.open_new(f"file:///{report_path}")
else:
runner = DebugTestRunner(
blacklist=self.blacklist,
whitelist=self.whitelist,
verbosity=2,
failfast=self.failfast
)
runner.run(suits)
log.success("A run the test in debug mode without generating HTML report!")
def open_browser(self) -> None:
"""
If you set up a browser, open the browser
"""
if self.browser is not None:
if isinstance(self.browser, str):
BrowserConfig.NAME = self.browser
elif isinstance(self.browser, dict):
BrowserConfig.NAME = self.browser.get("browser", None)
BrowserConfig.executable_path = self.browser.get("executable_path", None)
BrowserConfig.options = self.browser.get("options", None)
BrowserConfig.command_executor = self.browser.get("command_executor", "")
else:
raise TypeError("browser type error, str or dict.")
Seldom.driver = Browser(BrowserConfig.NAME, BrowserConfig.executable_path, BrowserConfig.options,
BrowserConfig.command_executor)
@staticmethod
def close_browser() -> None:
"""
How to open the browser, close the browser
"""
if all([
isinstance(Seldom.driver, SeleniumWebDriver),
Seldom.app_server is None,
Seldom.app_info is None]
):
try:
Seldom.driver.quit()
except InvalidSessionIdException:
...
Seldom.driver = None
class TestMainExtend(TestMain):
"""
TestMain tests class extensions.
1. Collect use case information and return to the list
2. Execute the use cases based on the use case list
"""
def __init__(
self,
path: str | list[str] | None = None,
browser: str | dict[str, Any] | None = None,
base_url: str = None,
debug: bool = False,
timeout: int = 10,
app_server=None,
app_info=None,
report: str = None,
title: str = "Seldom Test Report",
tester: str = "Anonymous",
description: str = "Test case execution",
rerun: int = 0,
language: str = "en",
whitelist: list = None,
blacklist: list = None,
extensions=None,
):
if path is None:
raise FileNotFoundError("Specify a file path")
super().__init__(path=path, browser=browser, base_url=base_url, debug=debug, timeout=timeout,
app_server=app_server, app_info=app_info, report=report, title=title, tester=tester,
description=description, rerun=rerun, language=language,
whitelist=whitelist, blacklist=blacklist, open_report=False, auto=False, extensions=extensions)
def collect_cases(self, json: bool = False, level: str = "data", warning: bool = False) -> Any:
"""
Return the collected case information.
SeldomTestLoader.collectCaseInfo = True
:param json: Return JSON format
:param level: Parse the level of use cases:
* data: Each piece of test data is parsed into a use case.
* method: Each method is resolved into a use case
:param warning: Whether to collect warning information
"""
if level not in ["data", "method"]:
raise ValueError("level value error.")
cases = seldomTestLoader.collectCaseList
if level == "method":
# Remove the data-driven use case end number
cases_backup_one = []
for case in cases:
case_name = case["method"]["name"]
if "_" not in case_name:
cases_backup_one.append(case)
else:
try:
int(case_name.split("_")[-1])
except ValueError:
cases_backup_one.append(case)
else:
case_name_end = case_name.split("_")[-1]
case["method"]["name"] = case_name[:-(len(case_name_end) + 1)]
cases_backup_one.append(case)
# Remove duplicate use cases
cases_backup_two = []
case_full_list = []
for case in cases_backup_one:
case_full = f'{case["file"]}.{case["class"]["name"]}.{case["method"]["name"]}'
if case_full not in case_full_list:
case_full_list.append(case_full)
cases_backup_two.append(case)
cases = cases_backup_two
if warning is True:
self._load_testsuite(warning=True)
if json is True:
return sys_json.dumps(cases, indent=2, ensure_ascii=False)
return cases
def _load_testsuite(self, warning: bool = False) -> Dict[str, List[Any]]:
"""
load test suite and convert to mapping
:param warning: Whether to collect warning information
"""
mapping = {}
exception_info = ""
for suits in self.TestSuits:
for cases in suits:
if isinstance(cases, unittest.suite.TestSuite) is False:
if warning is True:
exception_info = exception_info + str(cases._exception) + "\n"
continue
for case in cases:
file_name = case.__module__
class_name = case.__class__.__name__
key = f"{file_name}.{class_name}"
if mapping.get(key, None) is None:
mapping[key] = []
mapping[key].append(case)
if warning is True:
collect_file = os.path.join(os.path.dirname(BrowserConfig.REPORT_PATH), "collect_warning.log")
with open(collect_file, "w", encoding="utf-8") as file:
file.write(exception_info)
return mapping
def run_cases(self, data: list) -> None:
"""
run list case
:param data: test case list
:return:
"""
loader("start_run")
if isinstance(data, list) is False:
raise TypeError("Use cases must be lists.")
if len(data) == 0:
log.error("There are no use cases to execute")
return
suit = unittest.TestSuite()
case_mapping = self._load_testsuite()
for d in data:
d_file = d.get("file", None)
d_class = d.get("class").get("name", None)
d_method = d.get("method").get("name", None)
if (d_file is None) or (d_class is None) or (d_method is None):
raise SeldomException(
"""Use case format error, please refer to:
https://seldomqa.github.io/platform/platform.html""")
cases = case_mapping.get(f"{d_file}.{d_class}", None)
if cases is None:
continue
for case in cases:
method_name = str(case).split(" ")[0]
if "_" not in method_name:
if method_name == d_method:
suit.addTest(case)
else:
try:
int(method_name.split("_")[-1])
except ValueError:
if method_name == d_method:
suit.addTest(case)
else:
if method_name.startswith(d_method):
suit.addTest(case)
self.run(suit)
self.close_browser()
loader("end_run")
main = TestMain
if __name__ == '__main__':
main()
================================================
FILE: seldom/skip.py
================================================
"""
unittest decorator
"""
import unittest
import functools
__all__ = [
"skip", "skip_if", "skip_unless", "expected_failure", "depend", "if_depend", "label", "rerun"
]
def skip(reason=None):
"""
Unconditionally skip a test.
:param reason:
:return:
"""
if reason is None:
reason = "Skip the use case."
return unittest.skip(reason)
def skip_if(condition, reason):
"""
Skip a test if the condition is true.
:param condition:
:param reason:
:return:
"""
return unittest.skipIf(condition, reason)
def skip_unless(condition, reason):
"""
Skip a test unless the condition is true.
:param condition:
:param reason:
:return:
"""
return unittest.skipUnless(condition, reason)
def expected_failure(test_item):
"""
Expect the test case to failure
:param test_item:
:return:
"""
return unittest.expectedFailure(test_item)
def depend(case=None):
"""
Use case dependency
:param case
:return:
"""
def wrapper_func(test_func):
@functools.wraps(test_func)
def inner_func(self, *args):
if case == test_func.__name__:
raise ValueError(f"{case} cannot depend on itself")
failures = str([fail_[0] for fail_ in self._outcome.result.failures])
errors = str([error_[0] for error_ in self._outcome.result.errors])
skipped = str([skip_[0] for skip_ in self._outcome.result.skipped])
flag = (case in failures) or (case in errors) or (case in skipped)
test = skip_if(flag, f'{case} failed or error or skipped')(test_func)
try:
return test(self)
except TypeError:
return None
return inner_func
return wrapper_func
def if_depend(value):
"""
Custom skip condition
:param value
:return:
"""
def wrapper_func(function):
def inner_func(self, *args, **kwargs):
if not getattr(self, value):
self.skipTest('Dependent use case not passed')
else:
function(self, *args, **kwargs)
return inner_func
return wrapper_func
def label(*labels):
"""
Test case classification label
Usage:
@label('quick')
class MyTest(unittest.TestCase):
def test_foo(self):
pass
"""
def inner(cls):
# append labels to class
cls._labels = set(labels) | getattr(cls, '_labels', set())
return cls
return inner
def rerun(times: int = 2):
"""
Repeat a function multiple times.
Note: The change method cannot count the number of test cases.
:param times: Number of runs, default 2
return
"""
def wrapper(func):
@functools.wraps(func)
def decorator(*args, **kwargs):
for i in range(times):
func(*args, **kwargs)
return decorator
return wrapper
================================================
FILE: seldom/swagger2case/__init__.py
================================================
================================================
FILE: seldom/swagger2case/core.py
================================================
import os
import json
import requests
from seldom.logging import log
from seldom.utils.file_extend import file
class SwaggerParser:
def __init__(self, swagger: str, online=False):
"""
:param swagger: file path or http address
:param online: is online
"""
self.swagger = swagger
if online:
self.doc = self.online_swagger_doc(self.swagger)
else:
self.doc = self.local_swagger_file(self.swagger)
@staticmethod
def local_swagger_file(file_path: str) -> dict:
"""
Read swagger local files
:param file_path:
:return:
"""
with open(file_path, "r+", encoding="utf-8") as json_file:
doc = json.load(json_file)
return doc
@staticmethod
def online_swagger_doc(url: str) -> dict:
"""
Read swagger doc online
:param url:
:return:
"""
doc = requests.get(url).json()
return doc
@staticmethod
def create_file(save_path: str, code: str = "") -> None:
"""
create test case file
"""
with open(save_path, 'w', encoding="utf8") as file:
file.write(code)
log.info(f"created file: {save_path}")
def swagger_to_seldom_code(self, swagger_doc: dict) -> str:
"""
swagger to seldom code
:param swagger_doc: swagger doc
:return:
"""
paths = swagger_doc['paths']
seldom_code = '''import seldom
class TestRequest(seldom.TestCase):
'''
for path, methods in paths.items():
api_name = path.replace('/', '_').replace('{', '').replace('}', '') + "_api"
for method, opts in methods.items():
func_code = f"\n def test{api_name}_{method}(self):"
# 参数处理
parameters = opts.get('parameters', [])
form_params = []
path_params = []
query_params = []
header_params = []
for param in parameters:
name = param['name']
if param['in'] == 'path':
path_params.append(name)
elif param['in'] == 'query':
query_params.append(name)
elif param['in'] == 'header':
header_params.append(name)
elif param['in'] == 'formData':
form_params.append(name)
# request params
func_code += f'\n url = f"{swagger_doc["schemes"][0]}://{swagger_doc["host"]}{path}"'
query_dict = ", ".join([f"\"{p}\": {p}" for p in query_params])
if query_dict:
func_code += f'\n params = {{{query_dict}}}'
else:
func_code += f'\n params = {{}}'
if header_params:
headers_dict = ", ".join([f"\"{p}\": {p}" for p in header_params])
func_code += f'\n headers = {{{headers_dict}}}'
else:
func_code += f'\n headers = {{}}'
consumes = opts.get('consumes', ['application/json'])
func_code += f'\n headers["Content-Type"] = "{consumes[0]}"'
if form_params:
form_dict = ", ".join([f"\"{p}\": {p}" for p in form_params])
func_code += f'\n data = {{{form_dict}}}'
else:
func_code += f'\n data = {{}}'
# request send
req_method = method.lower()
func_code += f'\n r = self.{req_method}(url, headers=headers, params=params, data=data)'
func_code += f"\n print(r.status_code)"
# add test method
seldom_code += func_code + "\n\n"
seldom_code += '''
if __name__ == '__main__':
seldom.main()
'''
return seldom_code
def gen_testcase(self) -> None:
"""
generate test case
"""
if "\\" in self.swagger:
swagger_file = self.swagger.split("\\")[-1]
elif "/" in self.swagger:
swagger_file = self.swagger.split("/")[-1]
else:
swagger_file = self.swagger
if "." in swagger_file:
swagger_file = swagger_file.split(".")[0]
output_testcase_file = f"{swagger_file}.py"
log.info("Start to generate testcase.")
testcase = self.swagger_to_seldom_code(self.doc)
swagger_path = os.path.dirname(os.path.abspath(self.swagger))
self.create_file(file.join(swagger_path, output_testcase_file), testcase)
if __name__ == '__main__':
# online swagger doc
# sp = SwaggerParser(swagger="https://petstore.swagger.io/v2/swagger.json", online=True)
# sp.gen_testcase()
# local swagger doc
swagger_file_path = file.join(file.dir, "swagger.json")
sp = SwaggerParser(swagger=swagger_file_path)
sp.gen_testcase()
================================================
FILE: seldom/swagger2case/swagger.json
================================================
{
"swagger": "2.0",
"info": {
"description": "This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters.",
"version": "1.0.6",
"title": "Swagger Petstore",
"termsOfService": "http://swagger.io/terms/",
"contact": {
"email": "apiteam@swagger.io"
},
"license": {
"name": "Apache 2.0",
"url": "http://www.apache.org/licenses/LICENSE-2.0.html"
}
},
"host": "petstore.swagger.io",
"basePath": "/v2",
"tags": [
{
"name": "pet",
"description": "Everything about your Pets",
"externalDocs": {
"description": "Find out more",
"url": "http://swagger.io"
}
},
{
"name": "store",
"description": "Access to Petstore orders"
},
{
"name": "user",
"description": "Operations about user",
"externalDocs": {
"description": "Find out more about our store",
"url": "http://swagger.io"
}
}
],
"schemes": [
"https",
"http"
],
"paths": {
"/pet/{petId}/uploadImage": {
"post": {
"tags": [
"pet"
],
"summary": "uploads an image",
"description": "",
"operationId": "uploadFile",
"consumes": [
"multipart/form-data"
],
"produces": [
"application/json"
],
"parameters": [
{
"name": "petId",
"in": "path",
"description": "ID of pet to update",
"required": true,
"type": "integer",
"format": "int64"
},
{
"name": "additionalMetadata",
"in": "formData",
"description": "Additional data to pass to server",
"required": false,
"type": "string"
},
{
"name": "file",
"in": "formData",
"description": "file to upload",
"required": false,
"type": "file"
}
],
"responses": {
"200": {
"description": "successful operation",
"schema": {
"$ref": "#/definitions/ApiResponse"
}
}
},
"security": [
{
"petstore_auth": [
"write:pets",
"read:pets"
]
}
]
}
},
"/pet": {
"post": {
"tags": [
"pet"
],
"summary": "Add a new pet to the store",
"description": "",
"operationId": "addPet",
"consumes": [
"application/json",
"application/xml"
],
"produces": [
"application/json",
"application/xml"
],
"parameters": [
{
"in": "body",
"name": "body",
"description": "Pet object that needs to be added to the store",
"required": true,
"schema": {
"$ref": "#/definitions/Pet"
}
}
],
"responses": {
"405": {
"description": "Invalid input"
}
},
"security": [
{
"petstore_auth": [
"write:pets",
"read:pets"
]
}
]
},
"put": {
"tags": [
"pet"
],
"summary": "Update an existing pet",
"description": "",
"operationId": "updatePet",
"consumes": [
"application/json",
"application/xml"
],
"produces": [
"application/json",
"application/xml"
],
"parameters": [
{
"in": "body",
"name": "body",
"description": "Pet object that needs to be added to the store",
"required": true,
"schema": {
"$ref": "#/definitions/Pet"
}
}
],
"responses": {
"400": {
"description": "Invalid ID supplied"
},
"404": {
"description": "Pet not found"
},
"405": {
"description": "Validation exception"
}
},
"security": [
{
"petstore_auth": [
"write:pets",
"read:pets"
]
}
]
}
},
"/pet/findByStatus": {
"get": {
"tags": [
"pet"
],
"summary": "Finds Pets by status",
"description": "Multiple status values can be provided with comma separated strings",
"operationId": "findPetsByStatus",
"produces": [
"application/json",
"application/xml"
],
"parameters": [
{
"name": "status",
"in": "query",
"description": "Status values that need to be considered for filter",
"required": true,
"type": "array",
"items": {
"type": "string",
"enum": [
"available",
"pending",
"sold"
],
"default": "available"
},
"collectionFormat": "multi"
}
],
"responses": {
"200": {
"description": "successful operation",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/Pet"
}
}
},
"400": {
"description": "Invalid status value"
}
},
"security": [
{
"petstore_auth": [
"write:pets",
"read:pets"
]
}
]
}
},
"/pet/findByTags": {
"get": {
"tags": [
"pet"
],
"summary": "Finds Pets by tags",
"description": "Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.",
"operationId": "findPetsByTags",
"produces": [
"application/json",
"application/xml"
],
"parameters": [
{
"name": "tags",
"in": "query",
"description": "Tags to filter by",
"required": true,
"type": "array",
"items": {
"type": "string"
},
"collectionFormat": "multi"
}
],
"responses": {
"200": {
"description": "successful operation",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/Pet"
}
}
},
"400": {
"description": "Invalid tag value"
}
},
"security": [
{
"petstore_auth": [
"write:pets",
"read:pets"
]
}
],
"deprecated": true
}
},
"/pet/{petId}": {
"get": {
"tags": [
"pet"
],
"summary": "Find pet by ID",
"description": "Returns a single pet",
"operationId": "getPetById",
"produces": [
"application/json",
"application/xml"
],
"parameters": [
{
"name": "petId",
"in": "path",
"description": "ID of pet to return",
"required": true,
"type": "integer",
"format": "int64"
}
],
"responses": {
"200": {
"description": "successful operation",
"schema": {
"$ref": "#/definitions/Pet"
}
},
"400": {
"description": "Invalid ID supplied"
},
"404": {
"description": "Pet not found"
}
},
"security": [
{
"api_key": []
}
]
},
"post": {
"tags": [
"pet"
],
"summary": "Updates a pet in the store with form data",
"description": "",
"operationId": "updatePetWithForm",
"consumes": [
"application/x-www-form-urlencoded"
],
"produces": [
"application/json",
"application/xml"
],
"parameters": [
{
"name": "petId",
"in": "path",
"description": "ID of pet that needs to be updated",
"required": true,
"type": "integer",
"format": "int64"
},
{
"name": "name",
"in": "formData",
"description": "Updated name of the pet",
"required": false,
"type": "string"
},
{
"name": "status",
"in": "formData",
"description": "Updated status of the pet",
"required": false,
"type": "string"
}
],
"responses": {
"405": {
"description": "Invalid input"
}
},
"security": [
{
"petstore_auth": [
"write:pets",
"read:pets"
]
}
]
},
"delete": {
"tags": [
"pet"
],
"summary": "Deletes a pet",
"description": "",
"operationId": "deletePet",
"produces": [
"application/json",
"application/xml"
],
"parameters": [
{
"name": "api_key",
"in": "header",
"required": false,
"type": "string"
},
{
"name": "petId",
"in": "path",
"description": "Pet id to delete",
"required": true,
"type": "integer",
"format": "int64"
}
],
"responses": {
"400": {
"description": "Invalid ID supplied"
},
"404": {
"description": "Pet not found"
}
},
"security": [
{
"petstore_auth": [
"write:pets",
"read:pets"
]
}
]
}
},
"/store/order": {
"post": {
"tags": [
"store"
],
"summary": "Place an order for a pet",
"description": "",
"operationId": "placeOrder",
"consumes": [
"application/json"
],
"produces": [
"application/json",
"application/xml"
],
"parameters": [
{
"in": "body",
"name": "body",
"description": "order placed for purchasing the pet",
"required": true,
"schema": {
"$ref": "#/definitions/Order"
}
}
],
"responses": {
"200": {
"description": "successful operation",
"schema": {
"$ref": "#/definitions/Order"
}
},
"400": {
"description": "Invalid Order"
}
}
}
},
"/store/order/{orderId}": {
"get": {
"tags": [
"store"
],
"summary": "Find purchase order by ID",
"description": "For valid response try integer IDs with value >= 1 and <= 10. Other values will generated exceptions",
"operationId": "getOrderById",
"produces": [
"application/json",
"application/xml"
],
"parameters": [
{
"name": "orderId",
"in": "path",
"description": "ID of pet that needs to be fetched",
"required": true,
"type": "integer",
"maximum": 10,
"minimum": 1,
"format": "int64"
}
],
"responses": {
"200": {
"description": "successful operation",
"schema": {
"$ref": "#/definitions/Order"
}
},
"400": {
"description": "Invalid ID supplied"
},
"404": {
"description": "Order not found"
}
}
},
"delete": {
"tags": [
"store"
],
"summary": "Delete purchase order by ID",
"description": "For valid response try integer IDs with positive integer value. Negative or non-integer values will generate API errors",
"operationId": "deleteOrder",
"produces": [
"application/json",
"application/xml"
],
"parameters": [
{
"name": "orderId",
"in": "path",
"description": "ID of the order that needs to be deleted",
"required": true,
"type": "integer",
"minimum": 1,
"format": "int64"
}
],
"responses": {
"400": {
"description": "Invalid ID supplied"
},
"404": {
"description": "Order not found"
}
}
}
},
"/store/inventory": {
"get": {
"tags": [
"store"
],
"summary": "Returns pet inventories by status",
"description": "Returns a map of status codes to quantities",
"operationId": "getInventory",
"produces": [
"application/json"
],
"parameters": [],
"responses": {
"200": {
"description": "successful operation",
"schema": {
"type": "object",
"additionalProperties": {
"type": "integer",
"format": "int32"
}
}
}
},
"security": [
{
"api_key": []
}
]
}
},
"/user/createWithArray": {
"post": {
"tags": [
"user"
],
"summary": "Creates list of users with given input array",
"description": "",
"operationId": "createUsersWithArrayInput",
"consumes": [
"application/json"
],
"produces": [
"application/json",
"application/xml"
],
"parameters": [
{
"in": "body",
"name": "body",
"description": "List of user object",
"required": true,
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/User"
}
}
}
],
"responses": {
"default": {
"description": "successful operation"
}
}
}
},
"/user/createWithList": {
"post": {
"tags": [
"user"
],
"summary": "Creates list of users with given input array",
"description": "",
"operationId": "createUsersWithListInput",
"consumes": [
"application/json"
],
"produces": [
"application/json",
"application/xml"
],
"parameters": [
{
"in": "body",
"name": "body",
"description": "List of user object",
"required": true,
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/User"
}
}
}
],
"responses": {
"default": {
"description": "successful operation"
}
}
}
},
"/user/{username}": {
"get": {
"tags": [
"user"
],
"summary": "Get user by user name",
"description": "",
"operationId": "getUserByName",
"produces": [
"application/json",
"application/xml"
],
"parameters": [
{
"name": "username",
"in": "path",
"description": "The name that needs to be fetched. Use user1 for testing. ",
"required": true,
"type": "string"
}
],
"responses": {
"200": {
"description": "successful operation",
"schema": {
"$ref": "#/definitions/User"
}
},
"400": {
"description": "Invalid username supplied"
},
"404": {
"description": "User not found"
}
}
},
"put": {
"tags": [
"user"
],
"summary": "Updated user",
"description": "This can only be done by the logged in user.",
"operationId": "updateUser",
"consumes": [
"application/json"
],
"produces": [
"application/json",
"application/xml"
],
"parameters": [
{
"name": "username",
"in": "path",
"description": "name that need to be updated",
"required": true,
"type": "string"
},
{
"in": "body",
"name": "body",
"description": "Updated user object",
"required": true,
"schema": {
"$ref": "#/definitions/User"
}
}
],
"responses": {
"400": {
"description": "Invalid user supplied"
},
"404": {
"description": "User not found"
}
}
},
"delete": {
"tags": [
"user"
],
"summary": "Delete user",
"description": "This can only be done by the logged in user.",
"operationId": "deleteUser",
"produces": [
"application/json",
"application/xml"
],
"parameters": [
{
"name": "username",
"in": "path",
"description": "The name that needs to be deleted",
"required": true,
"type": "string"
}
],
"responses": {
"400": {
"description": "Invalid username supplied"
},
"404": {
"description": "User not found"
}
}
}
},
"/user/login": {
"get": {
"tags": [
"user"
],
"summary": "Logs user into the system",
"description": "",
"operationId": "loginUser",
"produces": [
"application/json",
"application/xml"
],
"parameters": [
{
"name": "username",
"in": "query",
"description": "The user name for login",
"required": true,
"type": "string"
},
{
"name": "password",
"in": "query",
"description": "The password for login in clear text",
"required": true,
"type": "string"
}
],
"responses": {
"200": {
"description": "successful operation",
"headers": {
"X-Expires-After": {
"type": "string",
"format": "date-time",
"description": "date in UTC when token expires"
},
"X-Rate-Limit": {
"type": "integer",
"format": "int32",
"description": "calls per hour allowed by the user"
}
},
"schema": {
"type": "string"
}
},
"400": {
"description": "Invalid username/password supplied"
}
}
}
},
"/user/logout": {
"get": {
"tags": [
"user"
],
"summary": "Logs out current logged in user session",
"description": "",
"operationId": "logoutUser",
"produces": [
"application/json",
"application/xml"
],
"parameters": [],
"responses": {
"default": {
"description": "successful operation"
}
}
}
},
"/user": {
"post": {
"tags": [
"user"
],
"summary": "Create user",
"description": "This can only be done by the logged in user.",
"operationId": "createUser",
"consumes": [
"application/json"
],
"produces": [
"application/json",
"application/xml"
],
"parameters": [
{
"in": "body",
"name": "body",
"description": "Created user object",
"required": true,
"schema": {
"$ref": "#/definitions/User"
}
}
],
"responses": {
"default": {
"description": "successful operation"
}
}
}
}
},
"securityDefinitions": {
"api_key": {
"type": "apiKey",
"name": "api_key",
"in": "header"
},
"petstore_auth": {
"type": "oauth2",
"authorizationUrl": "https://petstore.swagger.io/oauth/authorize",
"flow": "implicit",
"scopes": {
"read:pets": "read your pets",
"write:pets": "modify pets in your account"
}
}
},
"definitions": {
"ApiResponse": {
"type": "object",
"properties": {
"code": {
"type": "integer",
"format": "int32"
},
"type": {
"type": "string"
},
"message": {
"type": "string"
}
}
},
"Category": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"format": "int64"
},
"name": {
"type": "string"
}
},
"xml": {
"name": "Category"
}
},
"Pet": {
"type": "object",
"required": [
"name",
"photoUrls"
],
"properties": {
"id": {
"type": "integer",
"format": "int64"
},
"category": {
"$ref": "#/definitions/Category"
},
"name": {
"type": "string",
"example": "doggie"
},
"photoUrls": {
"type": "array",
"xml": {
"wrapped": true
},
"items": {
"type": "string",
"xml": {
"name": "photoUrl"
}
}
},
"tags": {
"type": "array",
"xml": {
"wrapped": true
},
"items": {
"xml": {
"name": "tag"
},
"$ref": "#/definitions/Tag"
}
},
"status": {
"type": "string",
"description": "pet status in the store",
"enum": [
"available",
"pending",
"sold"
]
}
},
"xml": {
"name": "Pet"
}
},
"Tag": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"format": "int64"
},
"name": {
"type": "string"
}
},
"xml": {
"name": "Tag"
}
},
"Order": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"format": "int64"
},
"petId": {
"type": "integer",
"format": "int64"
},
"quantity": {
"type": "integer",
"format": "int32"
},
"shipDate": {
"type": "string",
"format": "date-time"
},
"status": {
"type": "string",
"description": "Order Status",
"enum": [
"placed",
"approved",
"delivered"
]
},
"complete": {
"type": "boolean"
}
},
"xml": {
"name": "Order"
}
},
"User": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"format": "int64"
},
"username": {
"type": "string"
},
"firstName": {
"type": "string"
},
"lastName": {
"type": "string"
},
"email": {
"type": "string"
},
"password": {
"type": "string"
},
"phone": {
"type": "string"
},
"userStatus": {
"type": "integer",
"format": "int32",
"description": "User Status"
}
},
"xml": {
"name": "User"
}
}
},
"externalDocs": {
"description": "Find out more about Swagger",
"url": "http://swagger.io"
}
}
================================================
FILE: seldom/testdata/__init__.py
================================================
from .random_func import *
================================================
FILE: seldom/testdata/conversion.py
================================================
"""
Data type conversion of different files
"""
import csv
import json
from itertools import islice
import codecs
import yaml
from openpyxl import load_workbook
def check_data(list_data: list) -> list:
"""
Checking test data format.
:param list_data:
:return:
"""
if isinstance(list_data, list) is False:
raise TypeError("The data format is not `list`.")
if len(list_data) == 0:
raise ValueError("The data format cannot be `[]`.")
if isinstance(list_data[0], dict):
test_data = []
for data in list_data:
line = []
for d in data.values():
line.append(d)
test_data.append(line)
return test_data
return list_data
def csv_to_list(file: str = None, line: int = 1, end_line: int = None) -> list:
"""
Convert CSV file data to list
:param file: Path to file
:param line: Start line of read data
:param end_line: End line of read data
:return: list data
Usage:
csv_to_list("data.csv", line=1)
"""
if file is None:
raise FileExistsError("Please specify the CSV file to convert.")
table_data = []
with codecs.open(file, 'r', encoding='utf_8_sig') as csv_file:
csv_data = csv.reader(csv_file)
for i in islice(csv_data, line - 1, end_line):
table_data.append(i)
return table_data
def excel_to_list(file: str = None, sheet: str = "Sheet1", line: int = 1, end_line: int = None) -> list:
"""
Convert Excel file data to list
:param file: Path to file
:param sheet: Excel sheet, default name is Sheet1
:param line: Start line of read data
:param end_line: Start line of read data
:return: list data
Usage:
excel_to_list("data.xlsx", sheet="Sheet1", line=1)
"""
if file is None:
raise FileExistsError("Please specify the Excel file to convert.")
excel_table = load_workbook(file)
sheet = excel_table[sheet]
if end_line is None:
end_line = sheet.max_row
table_data = []
for i in sheet.iter_rows(line, end_line):
line_data = []
for field in i:
line_data.append(field.value)
table_data.append(line_data)
return table_data
def json_to_list(file: str = None, key: str = None) -> list:
"""
Convert JSON file data to list
:param file: Path to file
:param key: Specifies the key for the dictionary
:return: list data
Usage:
json_to_list("data.yaml", key="login")
"""
if file is None:
raise FileExistsError("Please specify the JSON file to convert.")
if key is None:
with open(file, "r", encoding="utf-8") as json_file:
data = json.load(json_file)
list_data = check_data(data)
else:
with open(file, "r", encoding="utf-8") as json_file:
try:
data = json.load(json_file)[key]
list_data = check_data(data)
except KeyError as exc:
raise ValueError(f"Check the test data, no '{key}'.") from exc
return list_data
def yaml_to_list(file: str = None, key: str = None) -> list:
"""
Convert YAML file data to list
:param file: Path to file
:param key: Specifies the key for the dictionary
:return: list data
Usage:
yaml_to_list("data.yaml", key="login")
"""
if file is None:
raise FileExistsError("Please specify the YAML file to convert.")
if key is None:
with open(file, "r", encoding="utf-8") as yaml_file:
data = yaml.load(yaml_file, Loader=yaml.FullLoader)
list_data = check_data(data)
else:
with open(file, "r", encoding="utf-8") as yaml_file:
try:
data = yaml.load(yaml_file, Loader=yaml.FullLoader)[key]
list_data = check_data(data)
except KeyError as exc:
raise ValueError(f"Check the YAML test data, no '{key}'") from exc
return list_data
================================================
FILE: seldom/testdata/parameterization.py
================================================
"""
test data driver function
"""
import inspect as sys_inspect
import itertools
import os
import warnings
from functools import wraps
from pathlib import Path
import requests
from seldom.running.config import Seldom
from seldom.extend_lib import parameterized_class
from seldom.extend_lib.parameterized import delete_patches_if_need
from seldom.extend_lib.parameterized import inspect
from seldom.extend_lib.parameterized import parameterized
from seldom.extend_lib.parameterized import parameterized_argument_value_pairs
from seldom.extend_lib.parameterized import reapply_patches_if_need
from seldom.extend_lib.parameterized import short_repr
from seldom.extend_lib.parameterized import skip_on_empty_helper
from seldom.extend_lib.parameterized import to_text
from seldom.logging import log
from seldom.logging.exceptions import FileTypeError
from seldom.testdata import conversion
from seldom.utils import jmespath as utils_jmespath
__all__ = [
"file_data", "api_data", "data", "data_class", "find_file"
]
def _search_file_path(file_name: str, file_dir: Path) -> str:
"""
find file path
:param file_name:
:param file_dir:
"""
file_path = ""
find_root_dir = file_dir.parent.parent
for root, _, files in os.walk(find_root_dir, topdown=False):
for file in files:
if Seldom.env is not None:
if root.endswith(Seldom.env) and file == file_name:
file_path = os.path.join(root, file_name)
break
else:
if file == file_name:
file_path = os.path.join(root, file_name)
break
else:
continue
break
return file_path
def _search_env_file_path(file_dir: Path, file_part_path: str) -> str:
"""
find environment file path, Seldom.env != None
:param file_dir:
:param file_part_path:
"""
file_path = ""
find_root_dir = file_dir.parent
file_name = file_part_path.split("/")[-1]
file_part = os.path.join(Seldom.env, file_part_path[:-len(file_name) - 1])
for root, _, files in os.walk(find_root_dir, topdown=False):
for file in files:
if root.endswith(file_part) and file == file_name:
file_path = os.path.join(root, file_name)
break
else:
continue
break
return file_path
def find_file(file: str, file_dir: Path) -> str:
"""
find file
:param file:
:param file_dir:
"""
if os.path.isfile(file) is True:
return file
if "/" in file or "\\" in file:
file = file.replace("\\", "/")
if Seldom.env is not None:
file_path = _search_env_file_path(file_dir=file_dir, file_part_path=file)
return file_path
else:
# Starting at file_dir, search up the 5 levels of parent directories
for _ in range(5):
current_dir = os.path.join(file_dir, file)
if os.path.isfile(current_dir):
return current_dir
file_dir = file_dir.parent # Move up to the parent directory
else:
return ""
else:
file_path = _search_file_path(file_dir=file_dir, file_name=file)
return file_path
def file_data(file: str, line: int = 1, sheet: str = "Sheet1", key: str = None, end_line: int = None):
"""
Support file parameterization.
:param file: file name
:param line: Start line number of an Excel/CSV file
:param end_line: End line number of an Excel/CSV file
:param sheet: Excel sheet name
:param key: Key name of an YAML/JSON file
Usage:
d.json
```json
{
"login": [
["admin", "admin123"],
["guest", "guest123"]
]
}
```
>> @file_data(file="d.json", key="login")
... def test_case(self, username, password):
... print(username)
... print(password)
"""
if file is None:
raise FileExistsError("File name does not exist.")
stack_t = sys_inspect.stack()
ins = sys_inspect.getframeinfo(stack_t[1][0])
file_dir = Path(ins.filename).resolve().parent
if Seldom.env is not None:
log.info(f"env: '{Seldom.env}', find data file: '{file}'")
else:
log.info(f"find data file: {file}")
file_path = find_file(file, file_dir)
if file_path == "":
if Seldom.env is not None:
raise FileExistsError(f"No '{Seldom.env}/{file}' data file found.")
raise FileExistsError(f"No '{file}' data file found.")
suffix = file.split(".")[-1]
if suffix == "csv":
data_list = conversion.csv_to_list(file_path, line=line, end_line=end_line)
elif suffix == "xlsx":
data_list = conversion.excel_to_list(file_path, sheet=sheet, line=line, end_line=end_line)
elif suffix == "json":
data_list = conversion.json_to_list(file_path, key=key)
elif suffix == "yaml":
data_list = conversion.yaml_to_list(file_path, key=key)
else:
raise FileTypeError(f"Your file is not supported: {file}")
return data(data_list)
def api_data(url: str = None, params: dict = None, headers: dict = None, ret: str = None):
"""
Support api data parameterization.
:param url:
:param params:
:param headers:
:param ret:
:return:
"""
if url is None and Seldom.api_data_url is None:
raise ValueError("url is not None")
url = url if url is not None else Seldom.api_data_url
resp = requests.get(url, params=params, headers=headers).json()
if ret is not None:
data_ = utils_jmespath(resp, ret)
if data_ is None:
raise ValueError(f"Error - return {ret} is None in {resp}")
if isinstance(data_, list) is False:
raise TypeError(f"Error - {data_} is not list")
return data(data_)
if isinstance(resp, list) is False:
raise TypeError(f"Error - {resp} is not list")
return data(resp)
def data(input, name_func=None, doc_func=None, skip_on_empty=False, cartesian=False, **legacy):
""" A "brute force" method of parameterizing test cases. Creates new
test cases and injects them into the namespace that the wrapped
function is being defined in. Useful for parameterizing tests in
subclasses of 'UnitTest', where Nose test generators don't work.
>> @data([("foo", 1, 2)])
... def test_add1(name, input, expected):
... actual = add1(input)
... assert_equal(actual, expected)
...
>> locals()
... 'test_add1_foo_0': ...
>>
"""
if cartesian is True:
input = cartesian_product(input)
input = conversion.check_data(input)
if "testcase_func_name" in legacy:
warnings.warn("testcase_func_name= is deprecated; use name_func=",
DeprecationWarning, stacklevel=2)
if not name_func:
name_func = legacy["testcase_func_name"]
if "testcase_func_doc" in legacy:
warnings.warn("testcase_func_doc= is deprecated; use doc_func=",
DeprecationWarning, stacklevel=2)
if not doc_func:
doc_func = legacy["testcase_func_doc"]
doc_func = doc_func or default_doc_func
name_func = name_func or default_name_func
def parameterized_expand_wrapper(f, instance=None):
frame_locals = inspect.currentframe().f_back.f_locals
parameters = parameterized.input_as_callable(input)()
if not parameters:
if not skip_on_empty:
raise ValueError(
"Parameters iterable is empty (hint: use "
"`parameterized.expand([], skip_on_empty=True)` to skip "
"this test when the input is empty)"
)
return wraps(f)(skip_on_empty_helper)
digits = len(str(len(parameters) - 1))
for num, p in enumerate(parameters):
name = name_func(f, "{num:0>{digits}}".format(digits=digits, num=num), p)
# If the original function has patches applied by 'mock.patch',
# re-construct all patches on the just former decoration layer
# of param_as_standalone_func so as not to share
# patch objects between new functions
nf = reapply_patches_if_need(f)
frame_locals[name] = parameterized.param_as_standalone_func(p, nf, name)
frame_locals[name].__doc__ = doc_func(f, num, p)
# Delete original patches to prevent new function from evaluating
# original patching object as well as re-constructed patches.
delete_patches_if_need(f)
f.__test__ = False
return parameterized_expand_wrapper
def data_class(attrs, input_values=None, class_name_func=None):
"""
Parameterizes a test class by setting attributes on the class.
"""
return parameterized_class(attrs, input_values=input_values, class_name_func=class_name_func)
def default_name_func(func, num, p):
"""
return test function name
"""
base_name = func.__name__
name_suffix = f"_{num}"
if len(p.args) > 0 and isinstance(p.args[0], str):
# name_suffix += "_" + parameterized.to_safe_name(p.args[0])
name_suffix += ""
return base_name + name_suffix
def default_doc_func(func, num, p):
"""
return test function doc
"""
if func.__doc__ is None:
return None
all_args_with_values = parameterized_argument_value_pairs(func, p)
first_args_with_values = [all_args_with_values[0]]
# Assumes that the function passed is a bound method.
descs = ["%s=%s" % (n, short_repr(v)) for n, v in first_args_with_values]
# The documentation might be a multiline string, so split it
# and just work with the first string, ignoring the period
# at the end if there is one.
first, nl, rest = func.__doc__.lstrip().partition("\n")
suffix = ""
if first.endswith("."):
suffix = "."
first = first[:-1]
args = "%s[%s]" % (len(first) and " " or "", ", ".join(descs))
return "".join(
to_text(x)
for x in [first.rstrip(), args, suffix, nl, rest]
)
def cartesian_product(arr) -> list:
"""
Cartesian product
:param arr: Two-dimensional list
return:
"""
cp = list(itertools.product(*arr))
return cp
================================================
FILE: seldom/testdata/random_data.py
================================================
"""
random data file
"""
import re
import sys
# https://www.ssa.gov/oact/babynames/decades/names2010s.html
en_first_names_male = list(set(re.split(r"\s+", """
Noah Liam Jacob William Mason Ethan Michael Alexander James Elijah Benjamin Daniel Aiden Logan Jayden
Matthew Lucas David Jackson Joseph Anthony Samuel Joshua Gabriel Andrew John Christopher Oliver Dylan
Carter Isaac Luke Henry Owen Ryan Nathan Wyatt Caleb Sebastian Jack Christian Jonathan Julian Landon
Levi Isaiah Hunter Aaron Charles Thomas Eli Jaxon Connor Nicholas Jeremiah Grayson Cameron Brayden Adrian
Evan Jordan Josiah Angel Robert Gavin Tyler Austin Colton Jose Dominic Brandon Ian Lincoln Hudson Kevin
Zachary Adam Mateo Jason Chase Nolan Ayden Cooper Parker Xavier Asher Carson Jace Easton Justin Leo
Bentley Jaxson Nathaniel Blake Elias Theodore Kayden Luis Tristan Bryson Ezra Juan Brody Vincent Micah
Miles Santiago Cole Ryder Carlos Damian Leonardo Roman Max Sawyer Jesus Diego Greyson Alex Maxwell Axel
Eric Wesley Declan Giovanni Ezekiel Braxton Ashton Ivan Hayden Camden Silas Bryce Weston Harrison Jameson
George Antonio Timothy Kaiden Jonah Everett Miguel Steven Richard Emmett Victor Kaleb Kai Maverick Joel
Bryan Maddox Kingston Aidan Patrick Edward Emmanuel Jude Alejandro Preston Luca Bennett Jesse Colin Jaden
Malachi Kaden Jayce Alan Kyle Marcus Brian Ryker Grant Jeremy Abel Riley Calvin Brantley Caden Oscar Abraham
Brady Sean Jake Tucker Nicolas Mark Amir Avery King Gael Kenneth Bradley Cayden Xander Graham Rowan
""".strip())))
en_first_names_female = list(set(re.split(r"\s+", """
Emma Olivia Sophia Isabella Ava Mia Abigail Emily Charlotte Madison Elizabeth Amelia Evelyn Ella Chloe
Harper Avery Sofia Grace Addison Victoria Lily Natalie Aubrey Lillian Zoey Hannah Layla Brooklyn Scarlett
Zoe Camila Samantha Riley Leah Aria Savannah Audrey Anna Allison Gabriella Claire Hailey Penelope Aaliyah
Sarah Nevaeh Kaylee Stella Mila Nora Ellie Bella Lucy Alexa Arianna Violet Ariana Genesis Alexis Eleanor
Maya Caroline Peyton Skylar Madelyn Serenity Kennedy Taylor Alyssa Autumn Paisley Ashley Brianna Sadie
Naomi Kylie Julia Sophie Mackenzie Eva Gianna Luna Katherine Hazel Khloe Ruby Melanie Piper Lydia Aubree
Madeline Aurora Faith Alexandra Alice Kayla Jasmine Maria Annabelle Lauren Reagan Elena Rylee Isabelle
Bailey Eliana Sydney Makayla Cora Morgan Natalia Kimberly Vivian Quinn Valentina Andrea Willow Clara London
Jade Liliana Jocelyn Trinity Kinsley Brielle Mary Molly Hadley Delilah Emilia Josephine Brooke Ivy Lilly
Adeline Payton Lyla Isla Jordyn Paige Isabel Mariah Mya Nicole Valeria Destiny Rachel Ximena Emery Everly
Sara Angelina Adalynn Kendall Reese Aliyah Margaret Juliana Melody Amy Eden Mckenzie Laila Vanessa Ariel
Gracie Valerie Adalyn Brooklynn Gabrielle Kaitlyn Athena Elise Jessica Adriana Leilani Ryleigh Daisy Nova
Norah Eliza Rose Rebecca Michelle Alaina Catherine Londyn Summer Lila Jayla Katelyn Daniela Harmony Alana
Amaya Emerson Julianna Cecilia Izabella""".strip())))
en_last_names = list(set(re.split(r"\s+", """
Smith Johnson Williams Jones Brown Davis Miller Wilson Moore Taylor Anderson Thomas Jackson White Harris
Martin Thompson Garcia Martinez Robinson Clark Rodriguez Lewis Lee Walker Hall Allen Young Hernandez King
Wright Lopez Hill Scott Green Adams Baker Gonzalez Nelson Carter Mitchell Perez Roberts Turner Phillips
Campbell Parker Evans Edwards Collins Stewart Sanchez Morris Rogers Reed Cook Morgan Bell Murphy Bailey
Rivera Cooper Richardson Cox Howard Ward Torres Peterson Gray Ramirez James Watson Brooks Kelly Sanders
Price Bennett Wood Barnes Ross Henderson Coleman Jenkins Perry Powell Long Patterson Hughes Flores Washington
Butler Simmons Foster Gonzales Bryant Alexander Russell Griffin Diaz Hayes Myers Ford Hamilton Graham Sullivan
Wallace Woods Cole West Jordan Owens Reynolds Fisher Ellis Harrison Gibson Mcdonald Cruz Marshall Ortiz Gomez
Murray Freeman Wells Webb Simpson Stevens Tucker Porter Hunter Hicks Crawford Henry Boyd Mason Morales Kennedy
Warren Dixon Ramos Reyes Burns Gordon Shaw Holmes Rice Robertson Hunt Black Daniels Palmer Mills Nichols Grant
Knight Ferguson Rose Stone Hawkins Dunn Perkins Hudson Spencer Gardner Stephens Payne Pierce Berry Matthews
Arnold Wagner Willis Ray Watkins Olson Carroll Duncan Snyder Hart Cunningham Bradley Lane Andrews Ruiz Harper
Fox Riley Armstrong Carpenter Weaver Greene Lawrence Elliott Chavez Sims Austin Peters Kelley Franklin
""".strip())))
zh_names_male = list(set(re.split(r"\s+", """德义 苍 鹏云 炎 和志 新霁 澜 星泽 驰轩 楚 宏深 全 波涛 飞文 波 振国 凯 光启 经略 乐天
志强 作人 英叡 英华 星阑 景龙 鹏鲸 采 浩然 举 芬 鸿才 卫 嘉纳 旭东 玉泽 祺瑞 荫 茂德 博 鸿羲 彦 涵衍 开诚 鸿远 凯歌 星华 玉宇 潍 德华 甲
梓 正阳 文乐 高杰 骄 腾逸 鸿畅 修平 飞扬 宏爽 乐康 和风 洁 令羽 承载 礼 昊硕 天瑞 安宁 高义 兴朝 颖 浩波 洲 颉 良骏 颜 锋 承业 彭泽 骏年
坚白 运 承悦 钧 涵蓄 修雅 濮 天翰 经纬 天工 国源 奇迈 海逸 郁 俊侠 烨伟 昆峰 高旻 高超 鹏运 安 浩初 英逸 英豪 元甲 弘雅 温瑜 高雅 德明 慈
良畴 良骥 康德 皓轩 涵忍 振锐 嘉石 浦泽 飞飙 子 安澜 奇略 英 学海 悦 明喆 兴昌 凯安 乐生 仕 凯泽 和煦 鸿才 伟懋 华美 怀 景行 鸿远 宜人 顺
彭魄 宇文 景中 浩壤 奕 乐水 俊捷 高阳 云天 豪 驰翰 鑫鹏 鸾 伟诚 彤 华皓 安翔 正德 兴运 鸿达 硕 光济 博明 彭勃 阳曦 熠彤 泽语 睿德 原 令璟
璞玉 明珠 许 宸 景明 春 子明 和昶 远航 枫 宜民 修然 巍然 卿 睿思 兴思 朔 高芬 容 长 鸿信 晨濡 俊哲 和悦 俊发 文曜 伟兆 昊天 宾""".strip())))
zh_names_female = list(set(re.split(r"\s+", """海莹 曼珠 虹影 凝安 淳美 清润 旋 馨香 骊霞 水丹 长文 怀薇 平卉 向露 秀敏 青柏 尔阳 奥婷
智美 雅可 骊燕 燕珺 白曼 春枫 谷之 暖姝 易绿 娅欣 欢 半梅 忆彤 宇 茗 芳洁 双文 艳芳 珍丽 杨 若星 松 葳 晓畅 菱华 新荣 觅露 冰夏 初柳 迎蕾
海宁 香 妙颜 靖之 冰莹 天菱 诗丹 思思 玄素 安波 依秋 香巧 蕙 朝旭 怡 赞悦 梓彤 婉静 庄静 冬卉 冷雪 冰海 吟怀 月灵 优瑗 清嘉 悦爱 迎荷 旎旎
秋柳 巧春 美偲 忆灵 谷兰 雅娴 靖易 曼冬 溶溶 冷之 幼 芳蔼 妃 惜蕊 曼彤 傲之 愫 雪儿 以轩 丹秋 格 若云 骏 雅洁 朝雨 合美 馥 绿柳 慕凝 静曼
品韵 易容 娅芳 月怡 琲 如云 格格 溪 儿 璠瑜 丁辰 秀媚 岚岚 筱 听 元冬 白山 乃 茉 寄蕾 倚 傲儿 谷 淑君 帅 凡灵 语林 叶 幻珊 山雁 涵涵 灿
绮梅 平宁 天真 迎丝 水蓉 灵韵 甜恬 盼盼 韵宁 渟 安荷 智 虹英 访烟 玲玲 正思 霏 寄灵 友琴 绿柏 觅儿 娅玟 书琴 天蓉 宝 巧 寒雁 云韶 青旋 凝安
清懿 依云 以松 妙珍 灵阳 韶美 梓珊 未 初雪 姝艳 采梦 苒 若山 绚 彤彤 家 春娇 梦云 溪蓝 以松 半烟 孤云 玑 元绿 如曼 痴瑶 灵雨 梦寒 湘云 涵畅
玲琳 素华 """.strip())))
zh_last_name = list(set(re.split(r"\s+", """赵 钱 孙 李 周 吴 郑 王 冯 陈 褚 卫 蒋 沈 韩 杨 朱 秦 尤 许 何 吕 施 张 孔 曹 严 华 金
魏 陶 姜 戚 谢 邹 喻 柏 水 窦 章 云 苏 潘 葛 奚 范 彭 郎 鲁 韦 昌 马 苗 凤 花 方 俞 任 袁 柳 酆 鲍 史 唐 费 廉 岑 薛 雷 贺 倪 汤
滕 殷 罗 毕 郝 邬 安 常 乐 于 时 傅 皮 卞 齐 康 伍 余 元 卜 顾 孟 平 黄 和 萧 尹 湛 汪 祁 毛 禹 狄 米 贝 成 戴 谈 宋 茅 庞 熊 纪
舒 屈 项 祝 董 梁 杜 阮 蓝 闵 席 季 麻 强 贾 路 娄 危 江 童 颜 郭 梅 盛 林 刁 钟 徐 邱 骆 高 夏 蔡 田 樊 胡 凌 霍 虞 万 支 柯 昝
管 卢 莫 白 房 裘 缪 干 解 应 宗 丁 宣 贲 邓 郁 单 杭 洪 包 诸 左 石 崔 吉 钮 龚 程 嵇 邢 滑 裴 陆 荣 翁 荀 羊 宇文 尉迟 延陵 羊舌
羊角 乐正 诸葛 颛孙 仲孙 仲长 长孙 钟离 宗政 左丘 主父 宰父 子书 子车 子桑 百里 北堂 北野 哥舒 谷梁 闻人 王孙 王官 王叔 巫马 微生 淳于
单于 成公 叱干 叱利 褚师 端木 东方 东郭 东宫 东野 东里 东门 第二 第五 公祖 公玉 公西 公孟 公伯 公仲 公孙 公广 公上 公冶 公羊 公良 公户
公仪 公山 公门 公坚 公乘 欧阳 濮阳 青阳 漆雕 壤驷 上官 司徒 司马 司空 司寇 士孙 申屠 叔孙 叔仲 侍其 令狐 梁丘 闾丘 刘傅 慕容 万俟 谷利
高堂 南宫 南门 南荣 南野 女娲 纳兰 澹台 拓跋 太史 太叔 太公 秃发 夏侯 西门 鲜于 轩辕 相里 皇甫 赫连 呼延 胡母 亓官 夹谷 即墨 独孤 段干
达奚""".strip())))
# via: http://www.lipsum.com/feed/html
# russian is from: http://masterrussian.com/vocabulary/most_common_words.htm
# japanese (4bytes) are from: http://www.i18nguy.com/unicode/supplementary-test.html
ascii_paragraphs = '''
Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Phasellus
pharetra urna sit amet magna. Donec posuere porta velit. Vestibulum sed libero.
Ut vestibulum sodales arcu. Proin vulputate, mi quis luctus ornare, elit ligula fringilla nisi,
eu tempor purus felis a enim. Phasellus in justo et nisi rhoncus porttitor. Donec ligula felis,
sagittis at, vestibulum eu, vehicula sed, nisl. Aenean convallis pharetra nisl. Mauris imperdiet
libero eu urna ultrices vulputate. Donec semper nunc et nibh. In hac habitasse platea dictumst.
Fusce et ipsum semper velit tempor pharetra. Donec pretium sollicitudin purus. Cras mi velit,
egestas id, ultrices vitae, viverra sit amet, justo.
Quisque cursus tristique nunc. Fusce varius, orci et pellentesque aliquet,
nibh ipsum sodales lorem, iaculis tincidunt massa metus ut erat. Fusce dictum,
dolor ut laoreet aliquam, massa urna placerat nibh, vitae tristique nisl neque posuere mi.
Aliquam at orci. Nulla sem. Nullam risus. Nullam pharetra dapibus mauris. Mauris mollis pretium arcu.
Vestibulum sem massa, tempor a, dictum id, rutrum eu, ligula. Class aptent taciti sociosqu ad
litora torquent per conubia nostra, per inceptos himenaeos. Curabitur ultrices dignissim nibh.
Aenean nisl.
Integer bibendum pharetra orci. Suspendisse commodo, lorem elementum egestas hendrerit,
metus elit rutrum sapien, quis aliquam nibh nisi at ligula. Nam lobortis commodo mauris.
Vivamus semper, leo vel accumsan mattis, nulla elit vestibulum augue, vitae pharetra dolor nibh
sit amet odio. Pellentesque scelerisque ipsum id elit. Nulla aliquet semper dolor. Praesent ut lorem.
Curabitur dictum, magna eu porttitor rutrum, ipsum justo porttitor erat, sit amet tristique est ante
ut elit. Mauris vel est. In cursus, velit quis pharetra adipiscing, purus quam sagittis mi,
eget molestie leo lectus ac lacus. Curabitur ante massa, aliquam ut, scelerisque a, condimentum at,
eros. Nunc vitae neque. Nam sagittis scelerisque magna. Class aptent taciti sociosqu ad litora
torquent per conubia nostra, per inceptos himenaeos. Donec cursus pede. Quisque a mauris nec
turpis convallis scelerisque. Donec quam lorem, mollis vestibulum, euismod in, hendrerit et, sapien.
Curabitur felis.
Morbi pretium lorem imperdiet dui. Maecenas quis ligula. Morbi tempor velit sit amet felis.
Donec at dui. Donec neque. Quisque quis mauris a libero ultrices iaculis. Integer congue feugiat justo.
Quisque imperdiet lectus eu orci. Class aptent taciti sociosqu ad litora torquent per conubia nostra,
per inceptos himenaeos. Vivamus id lectus. Phasellus odio nisi, auctor eu, hendrerit quis,
iaculis sit amet, felis. Sed blandit mollis nunc. Sed velit magna, tristique tristique, porttitor ut,
dictum a, arcu. In hac habitasse platea dictumst. Cras semper bibendum tortor. Cum sociis natoque
penatibus et magnis dis parturient montes, nascetur ridiculus mus. Suspendisse potenti.
In hac habitasse platea dictumst. Fusce mi sem, varius vitae, molestie ut, gravida venenatis, nibh.
Nam risus lectus, interdum at, condimentum eu, aliquet et, ipsum.
Mauris mi tortor, elementum ut, mattis eget, aliquam a, tellus.
Suspendisse porttitor orci. Donec rutrum diam non est. Duis ac nunc. Cras sollicitudin aliquet mi.
Cras in pede. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae;
Nam vehicula est at metus. Suspendisse sapien. Nunc lobortis tortor sed purus hendrerit pellentesque.
Nunc laoreet. Morbi pharetra. Integer cursus molestie turpis. Nam cursus sodales sem.
Maecenas non lacus. Pellentesque habitant morbi tristique senectus et netus et malesuada fames
ac turpis egestas. Nam vel nibh eu nulla blandit facilisis. Sed varius turpis ac neque.
Curabitur vel erat. Morbi sed purus id erat tincidunt ullamcorper.
'''
unicode_paragraphs = '''
\u0437\u043d\u0430\u0442\u044c \u043c\u043e\u0439 \u0434\u043e \u0438\u043b\u0438 \u0435\u0441\u043b\u0438
\u0432\u0440\u0435\u043c\u044f \u0440\u0443\u043a\u0430 \u043d\u0435\u0442 \u0441\u0430\u043c\u044b\u0439
\u043d\u0438 \u0441\u0442\u0430\u0442\u044c \u0431\u043e\u043b\u044c\u0448\u043e\u0439 \u0434\u0430\u0436\u0435
\u0434\u0440\u0443\u0433\u043e\u0439 \u043d\u0430\u0448 \u0441\u0432\u043e\u0439 \u043d\u0443 \u043f\u043e\u0434
\u0433\u0434\u0435 \u0434\u0435\u043b\u043e \u0435\u0441\u0442\u044c \u0441\u0430\u043c \u0440\u0430\u0437
\u0447\u0442\u043e\u0431\u044b \u0434\u0432\u0430 \u0442\u0430\u043c \u0447\u0435\u043c \u0433\u043b\u0430\u0437
\u0436\u0438\u0437\u043d\u044c \u043f\u0435\u0440\u0432\u044b\u0439 \u0434\u0435\u043d\u044c \u0442\u0443\u0442
\u0432\u043e \u043d\u0438\u0447\u0442\u043e \u043f\u043e\u0442\u043e\u043c \u043e\u0447\u0435\u043d\u044c
\u0441\u043e \u0445\u043e\u0442\u0435\u0442\u044c \u043b\u0438 \u043f\u0440\u0438 \u0433\u043e\u043b\u043e\u0432\u0430
\u043d\u0430\u0434\u043e \u0431\u0435\u0437 \u0432\u0438\u0434\u0435\u0442\u044c \u0438\u0434\u0442\u0438
\u0442\u0435\u043f\u0435\u0440\u044c \u0442\u043e\u0436\u0435 \u0441\u0442\u043e\u044f\u0442\u044c
\u0434\u0440\u0443\u0433 \u0434\u043e\u043c \u0441\u0435\u0439\u0447\u0430\u0441 \u043c\u043e\u0436\u043d\u043e
\u043f\u043e\u0441\u043b\u0435 \u0441\u043b\u043e\u0432\u043e \u0437\u0434\u0435\u0441\u044c
\u0434\u0443\u043c\u0430\u0442\u044c \u043c\u0435\u0441\u0442\u043e \u0441\u043f\u0440\u043e\u0441\u0438\u0442\u044c
\u0447\u0435\u0440\u0435\u0437 \u043b\u0438\u0446\u043e \u0447\u0442\u043e \u0442\u043e\u0433\u0434\u0430
\u0432\u0435\u0434\u044c \u0445\u043e\u0440\u043e\u0448\u0438\u0439 \u043a\u0430\u0436\u0434\u044b\u0439
\u043d\u043e\u0432\u044b\u0439 \u0436\u0438\u0442\u044c \u0434\u043e\u043b\u0436\u043d\u044b\u0439
\u0441\u043c\u043e\u0442\u0440\u0435\u0442\u044c \u043f\u043e\u0447\u0435\u043c\u0443
\u043f\u043e\u0442\u043e\u043c\u0443 \u0441\u0442\u043e\u0440\u043e\u043d\u0430 \u043f\u0440\u043e\u0441\u0442\u043e
\u043d\u043e\u0433\u0430 \u0441\u0438\u0434\u0435\u0442\u044c \u043f\u043e\u043d\u044f\u0442\u044c
\u0438\u043c\u0435\u0442\u044c \u043a\u043e\u043d\u0435\u0447\u043d\u044b\u0439 \u0434\u0435\u043b\u0430\u0442\u044c
\u0432\u0434\u0440\u0443\u0433 \u043d\u0430\u0434 \u0432\u0437\u044f\u0442\u044c \u043d\u0438\u043a\u0442\u043e
\u0441\u0434\u0435\u043b\u0430\u0442\u044c \u0434\u0432\u0435\u0440\u044c \u043f\u0435\u0440\u0435\u0434
\u043d\u0443\u0436\u043d\u044b\u0439 \u043f\u043e\u043d\u0438\u043c\u0430\u0442\u044c
\u043a\u0430\u0437\u0430\u0442\u044c\u0441\u044f \u0440\u0430\u0431\u043e\u0442\u0430 \u0442\u0440\u0438
\u0432\u0430\u0448 \u0443\u0436 \u0437\u0435\u043c\u043b\u044f \u043a\u043e\u043d\u0435\u0446
\u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0447\u0430\u0441 \u0433\u043e\u043b\u043e\u0441
\u0433\u043e\u0440\u043e\u0434 \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0438\u0439 \u043f\u043e\u043a\u0430
\u0445\u043e\u0440\u043e\u0448\u043e \u0434\u0430\u0432\u0430\u0442\u044c \u0432\u043e\u0434\u0430
\u0431\u043e\u043b\u0435\u0435 \u0445\u043e\u0442\u044f \u0432\u0441\u0435\u0433\u0434\u0430
\u0432\u0442\u043e\u0440\u043e\u0439 \u043a\u0443\u0434\u0430 \u043f\u043e\u0439\u0442\u0438
\u0441\u0442\u043e\u043b \u0440\u0435\u0431\u0451\u043d\u043e\u043a \u0443\u0432\u0438\u0434\u0435\u0442\u044c
\u0441\u0438\u043b\u0430 \u043e\u0442\u0435\u0446 \u0436\u0435\u043d\u0449\u0438\u043d\u0430
\u043c\u0430\u0448\u0438\u043d\u0430 \u0441\u043b\u0443\u0447\u0430\u0439 \u043d\u043e\u0447\u044c
\u0441\u0440\u0430\u0437\u0443 \u043c\u0438\u0440 \u0441\u043e\u0432\u0441\u0435\u043c
\u043e\u0441\u0442\u0430\u0442\u044c\u0441\u044f \u043e\u0431 \u0432\u0438\u0434 \u0432\u044b\u0439\u0442\u0438
\u0434\u0430\u0442\u044c \u0440\u0430\u0431\u043e\u0442\u0430\u0442\u044c \u043b\u044e\u0431\u0438\u0442\u044c
\u0441\u0442\u0430\u0440\u044b\u0439 \u043f\u043e\u0447\u0442\u0438 \u0440\u044f\u0434
\u043e\u043a\u0430\u0437\u0430\u0442\u044c\u0441\u044f \u043d\u0430\u0447\u0430\u043b\u043e
\u0442\u0432\u043e\u0439 \u0432\u043e\u043f\u0440\u043e\u0441 \u043c\u043d\u043e\u0433\u043e
\u0432\u043e\u0439\u043d\u0430 \u0441\u043d\u043e\u0432\u0430 \u043e\u0442\u0432\u0435\u0442\u0438\u0442\u044c
\u043c\u0435\u0436\u0434\u0443 \u043f\u043e\u0434\u0443\u043c\u0430\u0442\u044c \u043e\u043f\u044f\u0442\u044c
\u0431\u0435\u043b\u044b\u0439 \u0434\u0435\u043d\u044c\u0433\u0438 \u0437\u043d\u0430\u0447\u0438\u0442\u044c
\u043f\u0440\u043e \u043b\u0438\u0448\u044c \u043c\u0438\u043d\u0443\u0442\u0430 \u0436\u0435\u043d\u0430
'''
# only add 4-byte unicode if 4-byte unicode is supported
if sys.maxunicode > 65535:
unicode_paragraphs += '''
\U0002070e \U00020731 \U00020779 \U00020c53 \U00020c78 \U00020c96 \U00020ccf \U00020cd5 \U00020d15 \U00020d7c
\U00020d7f \U00020e0e \U00020e0f \U00020e77 \U00020e9d \U00020ea2 \U00020ed7 \U00020ef9 \U00020efa \U00020f2d
\U00020f2e \U00020f4c \U00020fb4 \U00020fbc \U00020fea \U0002105c \U0002106f \U00021075 \U00021076 \U0002107b
\U000210c1 \U000210c9 \U000211d9 \U000220c7 \U000227b5 \U00022ad5 \U00022b43 \U00022bca \U00022c51 \U00022c55
\U00022cc2 \U00022d08 \U00022d4c \U00022d67 \U00022eb3 \U00023cb7 \U000244d3 \U00024db8 \U00024dea \U0002512b
\U00026258 \U000267cc \U000269f2 \U000269fa \U00027a3e \U0002815d \U00028207 \U000282e2 \U00028cca \U00028ccd
\U00028cd2 \U00029d98
'''
ascii_words = re.split(r'\s+', ascii_paragraphs.strip())
unicode_words = re.split(r'\s+', unicode_paragraphs.strip())
words_str = ascii_words + unicode_words
mobile = [134, 135, 136, 137, 138, 139, 147, 150, 151, 152, 157, 158, 159, 172, 178, 182, 183, 184, 187, 188, 195, 197,
198]
unicom = [130, 131, 132, 145, 155, 156, 166, 175, 176, 185, 186, 196]
telecom = [133, 149, 153, 180, 181, 189, 173, 177, 190, 191, 193, 199]
================================================
FILE: seldom/testdata/random_func.py
================================================
"""
A function that generates random data
"""
import re
import sys
import time
import uuid
import random
import hashlib
import datetime
import requests
import string
from dateutil.relativedelta import relativedelta
from seldom.testdata.random_data import (
en_first_names_male,
en_first_names_female,
en_last_names,
zh_names_male,
zh_names_female,
zh_last_name,
words_str,
mobile,
unicom,
telecom,
)
TAOBAO_TIME = "https://acs.m.taobao.com/gw/mtop.common.getTimestamp/"
def first_name(gender: str = "", language: str = "en") -> str:
"""
get first name
:param gender:
:param language:
:return:
"""
genders = ["", "m", "f", "male", "female"]
if gender not in genders:
raise ValueError("Unsupported gender, try [m, f, male, female] instead")
if language == "en":
if gender == "":
name = random.choice(en_first_names_female + en_first_names_male)
elif gender in ["m", "male"]:
name = random.choice(en_first_names_male)
else:
name = random.choice(en_first_names_female)
return name.capitalize()
if language == "zh":
if gender == "":
name = random.choice(zh_names_female + zh_names_male)
elif gender == "m":
name = random.choice(zh_names_male)
else:
name = random.choice(zh_names_female)
return name
return ""
def last_name(language: str = "en") -> str:
"""
get last name
:return:
"""
if language == "en":
name = random.choice(en_last_names)
return name.capitalize()
if language == "zh":
name = random.choice(zh_last_name)
return name
raise ValueError(f"{language} language is not supported")
def username(name: str = "", language: str = "en") -> str:
"""
this is a very basic username generator
"""
if language == "en":
if not name:
name = first_name() if yes() else last_name()
name = re.sub(r"['-]", "", name)
return name
if language == "zh":
name = f"{last_name(language=language)}{first_name(language=language)}"
return name
raise ValueError(f"{language} language is not supported")
def password(length: int = 12) -> str:
"""
Generate a random password containing uppercase, lowercase, digits, and special characters.
:param length: Length of the password (minimum 4)
:return: Random password string
"""
if length < 4:
raise ValueError("Password length must be at least 4 to include all character types.")
chars = [
random.choice(string.ascii_lowercase),
random.choice(string.ascii_uppercase),
random.choice(string.digits),
random.choice('!@#$%^&*()-_=+[]{}|;:,.<>?')
]
if length > 4:
all_chars = string.ascii_letters + string.digits + '!@#$%^&*()-_=+[]{}|;:,.<>?'
chars += random.choices(all_chars, k=length - 4)
random.shuffle(chars)
return ''.join(chars)
def get_email(name: str = "") -> str:
"""
return a random email address
"""
name = username(name)
email_domains = [
"126.com",
"163.com",
"qq.com",
"sina.com",
"sohu.com",
"yahoo.com",
"hotmail.com",
"outlook.com",
"gmail.com",
"msn.com",
"mail.com",
]
return f"{name.lower()}@{random.choice(email_domains)}"
def get_md5(val: str = "") -> str:
"""Return an md5 hash of val, if no val then return a random md5 hash
:param val: string, the value you want to md5 hash
:returns: string, the md5 hash as a 32 char hex string
"""
if not val:
val = get_uuid()
if getattr(val, "encode", None):
ret = hashlib.md5(val.encode("utf-8")).hexdigest()
else:
ret = hashlib.md5(val).hexdigest()
return ret
def get_uuid() -> str:
"""
Generate a random UUID
"""
return str(uuid.uuid4())
def get_int(min_size: int = 1, max_size=sys.maxsize) -> int:
"""
return integer style data
:param min_size:
:param max_size:
"""
return random.randint(min_size, max_size)
def get_int32(min_size: int = 1) -> int:
"""
returns a 32-bit positive integer
"""
return random.randint(min_size, 2 ** 31 - 1)
def get_int64(min_size=1):
"""returns up to a 64-bit positive integer"""
return random.randint(min_size, 2 ** 63 - 1)
def get_float(min_size: float = None, max_size: float = None) -> float:
"""
return a random float
sames as the random method but automatically sets min and max
:param min_size: float, the minimum float size you want
:param max_size: float, the maximum float size you want
:returns: float, a random value between min_size and max_size
"""
float_info = sys.float_info
if min_size is None:
min_size = float_info.min
if max_size is None:
max_size = float_info.max
return random.uniform(min_size, max_size)
def get_digits(count: int) -> str:
"""
return a string value that contains count digits
:param count: int, how many digits you want, so if you pass in 4, you would get 4 digits
:returns: string, this returns a string because the digits might start with
zero
"""
max_size = int("9" * count)
ret = "{{:0>{}}}".format(count).format(get_int(0, max_size))
return ret
def get_string(length: int = 8) -> str:
"""
Generate a random string of specified length.
:param length: Length of the string
:return: Random string
"""
chars = string.ascii_letters + string.digits
return ''.join(random.choices(chars, k=length))
def get_number(length: int = 8) -> int:
"""
Generate a random number of specified length.
:param length: Length of the number
:returns: random number
"""
if length < 1:
raise ValueError("count must be >= 1")
min_value = 10 ** (length - 1) if length > 1 else 0
max_value = 10 ** length - 1
return random.randint(min_value, max_value)
def yes(specifier=0) -> int:
"""
Decide if we should perform this action, this is just a simple way to do something
I do in tests every now and again
:Example:
# EXAMPLE -- simple yes or no question
if testdata.yes():
# do this
else:
# don't do it
# EXAMPLE -- multiple choice
choice = testdata.yes(3)
if choice == 1:
# do the first thing
elif choice == 2:
# do the second thing
else:
# do the third thing
# EXAMPLE -- do something 75% of the time
if testdata.yes(0.75):
# do it the majority of the time
else:
# but every once in a while don't do it
:param specifier: int|float, if int, return a value between 1 and specifier.
if float, return 1 approximately specifier percent of the time, return 0
100% - specifier percent of the time
:returns: integer, usually 1 (True) or 0 (False)
"""
if specifier:
if isinstance(specifier, int):
choice = random.randint(1, specifier)
else:
if specifier < 1.0:
specifier *= 100.0
specifier = int(specifier)
num = random.randint(0, 100)
choice = 1 if num <= specifier else 0
else:
choice = random.choice([0, 1])
return choice
def get_words(count: int = 0, as_str: bool = True, words=None) -> str:
"""get some amount of random words
:param count: integer, how many words you want, 0 means a random amount (at most 20)
:param as_str: boolean, True to return as string, false to return as list of words
:param words: list, a list of words to choose from, defaults to unicode + ascii words
:returns: unicode|list, your requested words
"""
# since we specified we didn't care, randomly choose how many words there should be
if count == 0:
count = random.randint(1, 20)
if not words:
words = words_str
ret_words = random.sample(words, count)
return ret_words if not as_str else ' '.join(ret_words)
def get_word(words=None) -> str:
"""get word"""
return get_words(1, as_str=True, words=words)
def get_birthday(as_str: bool = False, start_age: int = 18, stop_age: int = 100) -> [str, datetime]:
"""
return a random YYYY-MM-DD
:param as_str: boolean, true to return the bday as a YYYY-MM-DD string
:param start_age: int, minimum age of the birthday date
:param stop_age: int, maximum age of the birthday date
:returns: datetime.date|string
"""
age = random.randint(start_age, stop_age)
year = (datetime.datetime.now(tz=datetime.UTC) - datetime.timedelta(weeks=(age * 52))).year
month = random.randint(1, 12)
if month == 2:
day = random.randint(1, 28)
elif month in [9, 4, 6, 11]:
day = random.randint(1, 30)
else:
day = random.randint(1, 31)
birthday = datetime.date(year, month, day)
if as_str:
birthday = "{:%Y-%m-%d}".format(birthday)
return birthday
def get_past_datetime(now=None, strftime=False) -> datetime:
"""
a datetime guaranteed to be in the past from now.
return: 2001-06-13 00:11:33.168502
"""
if not now:
now = datetime.datetime.now()
if isinstance(now, datetime.timedelta):
now = datetime.datetime.now() - now
td = now - datetime.datetime(year=2000, month=1, day=1)
data_time = now - datetime.timedelta(
days=random.randint(1, max(td.days, 1)),
seconds=random.randint(1, max(td.seconds, 1))
)
if strftime is True:
return data_time.strftime("%Y-%m-%d %H:%M:%S")
return data_time
def get_future_datetime(now=None, strftime=False) -> datetime:
"""
a datetime guaranteed to be in the future from now
return: 2034-02-23 04:59:41.168502
"""
if not now:
now = datetime.datetime.now()
if isinstance(now, datetime.timedelta):
now = datetime.datetime.now() + now
data_time = now + datetime.timedelta(
weeks=random.randint(1, 52 * 50),
hours=random.randint(0, 24),
days=random.randint(0, 365),
seconds=random.randint(0, 86400)
)
if strftime is True:
return data_time.strftime("%Y-%m-%d %H:%M:%S")
return data_time
def get_now_datetime(strftime=False) -> [str, datetime]:
"""
Get date time, default to current day.
:return:
"""
date_time = datetime.datetime.now()
if strftime is True:
return date_time.strftime("%Y-%m-%d %H:%M:%S")
return date_time
def get_past_time() -> str:
"""
Gets the past date time
:return: 2019-05-16 00:34:30
"""
number = random.randint(100000, 99999999)
date_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time() - number))
return date_time
def get_future_time() -> datetime:
"""
Gets the future date time.
:return: 2022-10-24 19:52:21
"""
number = random.randint(100000, 99999999)
date_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time() + number))
return date_time
def get_date(day=None) -> str:
"""
Get date, default to current day.
:return:
"""
if day is None:
date = datetime.datetime.now().strftime("%Y-%m-%d")
else:
date = (datetime.datetime.now() + datetime.timedelta(days=day)).strftime("%Y-%m-%d")
return date
def get_month(month: int = None) -> str:
"""
Get month, default to current month.
:param month:
:return:
"""
if month is None:
date = datetime.datetime.now().strftime("%Y-%m")
else:
date = str(datetime.date.today() + relativedelta(months=+month))[0:7]
return date
def get_year(year: int = None) -> str:
"""
Get year, default to current month.
:param year:
:return:
"""
if year is None:
date = datetime.datetime.now().strftime("%Y")
else:
date = str(datetime.date.today() + relativedelta(years=+year))[0:4]
return date
def get_phone(operator: str = None) -> str:
"""
get phone number
:return:
"""
if operator is None:
all_operator = mobile + unicom + telecom
top_third = random.choice(all_operator)
elif operator == "mobile":
top_third = random.choice(mobile)
elif operator == "unicom":
top_third = random.choice(unicom)
elif operator == "telecom":
top_third = random.choice(telecom)
else:
raise TypeError("Please select the right operator:'mobile','unicom','telecom' ")
suffix = random.randint(9999999, 100000000)
return f"{top_third}{suffix}"
def get_timestamp(level="second") -> str:
"""
get now timestamp
:return:
"""
time_list = str(time.time()).split('.', maxsplit=1)
if level == "second":
return time_list[0]
if level == "millisecond":
return time_list[0] + time_list[1]
return ""
def online_timestamp() -> str:
"""
get now timestamp
:return:
"""
r = requests.get(TAOBAO_TIME)
data = r.json()
ts = data["data"]["t"]
return ts
def online_now_datetime() -> [str, datetime]:
"""
Get online date time, default to current day.
:return:
"""
ts = online_timestamp()
date_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(int(ts[:10])))
return date_time
================================================
FILE: seldom/utils/__init__.py
================================================
from .file_extend import file, find_file_path
from .diff import diff_json, AssertInfo
from .jmespath import jmespath
from .genson import genson
from .cache import cache, memory_cache, disk_cache
from .dependence import dependent_func
from .timer import timer
================================================
FILE: seldom/utils/adbutils.py
================================================
import os
import re
import subprocess
import time
from typing import List, Tuple, Optional
from contextlib import contextmanager
from seldom.logging import log
class ADBUtils:
"""
Enhanced ADB utility class with proper resource management
Features: Device management, app control, and information retrieval
"""
def __init__(self, default_device: str = None):
"""
Initialize ADB controller with resource-safe implementation
:param default_device: Default device serial number (optional)
"""
self.default_device = default_device
self._devices_cache = []
self._last_refresh_time = 0
self.CACHE_EXPIRE = 60
@contextmanager
def _safe_popen(self, command: str):
"""Context manager for safe subprocess handling"""
process = None
try:
process = subprocess.Popen(
command,
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
yield process
finally:
if process is not None:
process.terminate()
try:
process.wait(timeout=1)
except subprocess.TimeoutExpired:
process.kill()
def refresh_devices(self, force: bool = False) -> List[Tuple[str, str]]:
"""
Safely refresh device list with proper resource cleanup
:param force: Whether to force refresh
:return: List of (serial, device_name) tuples
"""
current_time = time.time()
if not force and current_time - self._last_refresh_time < self.CACHE_EXPIRE:
return self._devices_cache
self._devices_cache = []
try:
with self._safe_popen("adb devices") as process:
stdout, _ = process.communicate(timeout=5)
raw_devices = stdout.splitlines()[1:]
for line in raw_devices:
parts = line.split()
if len(parts) > 1 and parts[1] == "device":
serial = parts[0]
device_name = self._get_device_name(serial)
if device_name:
self._devices_cache.append((serial, device_name))
self._last_refresh_time = current_time
except Exception as e:
log.error(f"Device refresh failed: {str(e)}")
return self._devices_cache
def _get_device_name(self, device_serial: str) -> str:
"""
Safely get device name with proper subprocess handling
:param device_serial: Device serial number
:return: Device model name or empty string on failure
"""
try:
with self._safe_popen(f"adb -s {device_serial} shell getprop ro.product.model") as process:
stdout, _ = process.communicate(timeout=3)
return stdout.strip().replace(' ', '')
except Exception as e:
log.error(f"Failed to get device info: {str(e)}")
return ""
def set_default_device(self, device_serial: str) -> bool:
"""
Set default device
:param device_serial: Device serial number
:return: Whether the operation succeeded
"""
devices = [d[0] for d in self.refresh_devices()]
if device_serial in devices:
self.default_device = device_serial
return True
log.info(f"Device {device_serial} does not exist or is not connected")
return False
def launch_app(self, package_name: str, device_id: Optional[str] = None) -> bool:
"""
Launch app (with error handling and result return)
:param package_name: App package name
:param device_id: Device serial number (optional)
:return: Whether the operation succeeded
"""
device = device_id or self.default_device
try:
cmd = f"adb{' -s ' + device if device else ''} shell monkey -p {package_name} -c android.intent.category.LAUNCHER 1"
exit_code = os.system(cmd)
return exit_code == 0
except Exception as e:
log.error(f"Failed to launch app {package_name}: {e}")
return False
def close_app(self, package_name: str, device_id: Optional[str] = None) -> bool:
"""
Close app (optimized implementation)
:param package_name: App package name
:param device_id: Device serial number (optional)
:return: Whether the operation succeeded
"""
device = device_id or self.default_device
try:
cmd = f"adb{' -s ' + device if device else ''} shell am force-stop {package_name}"
exit_code = os.system(cmd)
return exit_code == 0
except Exception as e:
log.error(f"Failed to close app {package_name}: {e}")
return False
def get_app_info(self, device_id: Optional[str] = None) -> dict:
"""
Enhanced foreground app detection with precise window dump parsing
Returns: {
'package': str,
'activity': str,
'window_state': str,
'orientation': int,
'stack_id': int,
'bounds': Tuple[int,int,int,int] # (x1,y1,x2,y2)
}
Example Output for Bing:
{
'package': 'com.microsoft.bing',
'activity': 'com.microsoft.sapphire.app.main.MainSapphireActivity',
'window_state': 'mCurrentFocus',
'orientation': 0, # portrait
'stack_id': 18153,
'bounds': (0, 0, 1200, 2640)
}
"""
device = device_id or self.default_device
try:
# Execute adb command to get window info
cmd = f"adb{' -s ' + device if device else ''} shell dumpsys window windows"
result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=5)
if result.returncode != 0:
raise subprocess.CalledProcessError(result.returncode, cmd)
return self._parse_window_dump(result.stdout)
except subprocess.TimeoutExpired:
print("Window dump timed out after 5 seconds")
except Exception as e:
print(f"Foreground detection failed: {type(e).__name__}: {str(e)}")
return None
def _parse_window_dump(self, dump_content: str) -> List[dict]:
"""
Enhanced window dump parser with precise pattern matching
Returns filtered list of valid application windows only
"""
window_info_list = []
# Primary pattern for focused application windows
app_window_pattern = re.compile(
r"Window\{[\w]+\s[\w]+\s(?P[^\s/]+)/(?P[^\s\}]+).*?"
r"mDisplayId=(?P\d+).*?"
r"(rootTaskId|mTaskId)=(?P\d+).*?"
r"mBounds=Rect\((?P\d+,\s*\d+\s*-\s*\d+,\s*\d+)\)",
re.DOTALL
)
# Secondary pattern for ActivityRecord entries
activity_record_pattern = re.compile(
r"ActivityRecord\{[\w]+\s[\w]+\s(?P[^\s/]+)/(?P[^\s\}]+).*?"
r"t(?P\d+)",
re.DOTALL
)
# System/overlay window filter (exclude navigation bars, popups etc.)
system_window_indicators = {
'pip-dismiss-overlay',
'GestureSildeOut',
'GestureNavRight',
'NavigationBar',
'NotificationShade',
'ShellDropTarget',
'PopupWindow'
}
# Extract orientation
orientation = 0
orient_match = re.search(r"mRotation=ROTATION_(\d+)", dump_content)
if orient_match:
orientation = int(orient_match.group(1)) * 90
# Process window matches
for match in app_window_pattern.finditer(dump_content):
info = match.groupdict()
package = info['package']
activity = info['activity']
# Skip system/overlay windows
if any(indicator in package for indicator in system_window_indicators):
continue
# Process bounds
bounds = (0, 0, 0, 0)
if info['bounds']:
bounds = tuple(map(int, re.split(r"\s*,\s* |\s*-\s*", info['bounds'])))
window_info = {
'package': package,
'activity': activity,
'window_state': 'focused',
'orientation': orientation,
'stack_id': int(info.get('stack_id', 0)),
'bounds': bounds,
'window_id': match.group(0)[7:15], # Extract window ID
'source': 'window_dump'
}
window_info_list.append(window_info)
# Process ActivityRecord matches
for match in activity_record_pattern.finditer(dump_content):
info = match.groupdict()
if not any(w['package'] == info['package'] and
w['activity'] == info['activity']
for w in window_info_list):
window_info_list.append({
'package': info['package'],
'activity': info['activity'],
'window_state': 'activity_record',
'stack_id': int(info.get('stack_id', 0)),
'source': 'activity_record'
})
# Filter and prioritize valid application windows
return [
info for info in window_info_list
if not info['package'].startswith(('u0 ', 'pip-'))
and '.' in info['activity']
]
================================================
FILE: seldom/utils/benchmark.py
================================================
import time
from typing import Callable, List, Dict, Any
class Benchmark:
"""
Benchmark Class
"""
def __init__(self):
self.results = {}
@staticmethod
def measure(func: Callable, rounds: int, iterations: int, *args: Any, **kwargs: Any) -> List[float]:
"""
Measures the time of 'func' execution and supports rounds and iterations.
Call 'iterations' func each time and count the total time.
:param func:
:param rounds:
:param iterations:
:param args:
:param kwargs:
:return:
"""
durations = []
for _ in range(rounds):
start_time = time.time()
for _ in range(iterations): # exe `iterations` times
func(*args, **kwargs)
end_time = time.time()
durations.append(float((end_time - start_time) / iterations))
return durations
def add_result(self, test_name: str, durations: List[float], iterations: int) -> None:
"""
Save test results, including test name, durations, and number of iterations.
:param test_name:
:param durations:
:param iterations:
:return:
"""
if test_name not in self.results:
self.results[test_name] = {'durations': [], 'iterations': 0}
self.results[test_name]['durations'].extend(durations)
self.results[test_name]['iterations'] = iterations # 存储迭代次数
def get_stats(self, test_name: str) -> Dict[str, Any]:
"""
Get statistics for a test, including minimum, maximum, mean, standard difference, and so on.
:param test_name:
:return:
"""
durations = self.results[test_name]['durations']
iterations = self.results[test_name]['iterations']
min_time = min(durations)
max_time = max(durations)
mean_time = self.mean(durations)
stddev_time = self.stddev(durations, mean_time)
median_time = self.median(durations)
iqr = self.iqr(durations)
ops = 1 / mean_time if mean_time > 0 else 0
return {
"min": min_time,
"max": max_time,
"mean": mean_time,
"stddev": stddev_time,
"median": median_time,
"iqr": iqr,
"ops": ops,
"rounds": len(durations),
"iterations": iterations
}
def report(self) -> None:
"""
Output the benchmark test report.
"""
print(
f"=============================================== benchmark: {len(self.results)} tests ===========================================")
print(
f"Name (time in s) Min Max Mean StdDev Median IQR Outliers OPS Rounds Iterations")
print(
f"--------------------------------------------------------------------------------------------------------------")
for test_name, durations in self.results.items():
stats = self.get_stats(test_name)
outliers = self.get_outliers(durations['durations'], stats['mean'], stats['stddev'], stats['iqr'])
print(
f"{test_name:<20} {stats['min']:7.4f} {stats['max']:7.4f} {stats['mean']:7.4f} {stats['stddev']:7.4f} "
f"{stats['median']:7.4f} {stats['iqr']:7.4f} {outliers:<10} {stats['ops']:7.4f} {stats['rounds']:7} {stats['iterations']:10}")
print(
f"--------------------------------------------------------------------------------------------------------------")
print("Legend: ")
print(
" Outliers: 1 Standard Deviation from Mean; 1.5 IQR (InterQuartile Range) from 1st Quartile and 3rd Quartile.")
print(" OPS: Operations Per Second, computed as 1 / Mean")
def get_outliers(self, durations: List[float], mean: float, stddev: float, iqr: float) -> str:
"""
Identifies outliers in the benchmark results based on the mean, standard deviation, and IQR.
:param durations:
:param mean:
:param stddev:
:param iqr:
:return:
"""
outliers = []
if not durations or not isinstance(durations, list):
return "0;0" # 没有数据或数据格式不正确
# 确保每个 duration 都是 float 类型
for duration in durations:
try:
duration = float(duration)
except (ValueError, TypeError):
continue # 如果无法转换为浮动数值,则跳过
if abs(duration - mean) > stddev or \
duration < self.percentile(durations, 25) - 1.5 * iqr or \
duration > self.percentile(durations, 75) + 1.5 * iqr:
outliers.append(duration)
return f"{len(outliers)};{len(durations) - len(outliers)}"
def mean(self, data: List[float]) -> float:
"""Calculates the mean (average) of a list of numbers."""
return sum(data) / len(data) if data else 0
def stddev(self, data: List[float], mean: float) -> float:
"""Calculates the standard deviation of a list of numbers."""
variance = sum((x - mean) ** 2 for x in data) / len(data)
return variance ** 0.5 if data else 0
def median(self, data: List[float]) -> float:
"""Calculates the median of a list of numbers."""
data_sorted = sorted(data)
n = len(data_sorted)
if n % 2 == 0:
return (data_sorted[n // 2 - 1] + data_sorted[n // 2]) / 2
else:
return data_sorted[n // 2]
def percentile(self, data: List[float], p: float) -> float:
"""Calculates the p-th percentile of a list of numbers."""
data_sorted = sorted(data)
k = (len(data_sorted) - 1) * p / 100
f = int(k)
c = k - f
if f + 1 < len(data_sorted):
return data_sorted[f] + c * (data_sorted[f + 1] - data_sorted[f])
else:
return data_sorted[f]
def iqr(self, data: List[float]) -> float:
"""Calculates the interquartile range (IQR) of a list of numbers."""
return self.percentile(data, 75) - self.percentile(data, 25)
benchmark = Benchmark()
def benchmark_test(rounds: int = 5, iterations: int = 1):
"""
benchmark test decorator
:param rounds:
:param iterations:
:return:
"""
def decorator(func):
def wrapper(self, *args, **kwargs):
durations = benchmark.measure(func, rounds, iterations, self, *args, **kwargs)
benchmark.add_result(func.__name__, durations, iterations)
return wrapper
return decorator
================================================
FILE: seldom/utils/cache.py
================================================
"""
seldom cache
"""
import os
import json
import uuid
import pickle
import shutil
import tempfile
import threading
from seldom.logging import log
from functools import lru_cache
from functools import wraps as func_wraps
DATA_PATH = os.path.join(tempfile.gettempdir(), "cache_data.json")
class Cache:
"""
Disk Cache through JSON files
"""
_lock = threading.Lock() # 创建类级别的锁
def __init__(self):
is_exist = os.path.isfile(DATA_PATH)
if is_exist is False:
with open(DATA_PATH, "w", encoding="utf-8") as json_file:
json.dump({}, json_file)
@staticmethod
def clear(name: str = None) -> None:
"""
Clearing cached
:param name: key
"""
with Cache._lock:
if name is None:
with open(DATA_PATH, "w+", encoding="utf-8") as json_file:
json.dump({}, json_file)
log.info("💾 Clear all cache data.")
else:
with open(DATA_PATH, "r+", encoding="utf-8") as json_file:
save_data = json.load(json_file)
if name in set(save_data.keys()):
del save_data[name]
log.info(f"💾 Clear cache data: {name}")
json_file.seek(0)
json_file.truncate()
json.dump(save_data, json_file)
@staticmethod
def set(data: dict) -> None:
"""
Setting cached
:param data:
"""
with Cache._lock:
with open(DATA_PATH, "r+", encoding="utf-8") as json_file:
save_data = json.load(json_file)
for key, value in data.items():
data = save_data.get(key, None)
if data is None:
log.info(f"💾 Set cache data: {key} = {value}")
else:
log.info(f"💾 Update cache data: {key} = {value}")
save_data[key] = value
json_file.seek(0)
json_file.truncate()
json.dump(save_data, json_file)
@staticmethod
def get(name=None):
"""
Getting cached
:param name: key
:return:
"""
with Cache._lock:
with open(DATA_PATH, "r+", encoding="utf-8") as json_file:
save_data = json.load(json_file)
if name is None:
return save_data
value = save_data.get(name, None)
if value is not None:
log.info(f"💾 Get cache data: {name} = {value}")
return value
cache = Cache()
def memory_cache(maxsize=None, typed=False):
""" memory (Least-recently-used) cache decorator
"""
return lru_cache(maxsize=maxsize, typed=typed)
class DiskCache:
"""
Cache data to disk decorator
"""
_NAMESPACE = uuid.UUID("c875fb30-a8a8-402d-a796-225a6b065cad")
def __init__(self, cache_path=None):
if cache_path:
self.cache_path = os.path.abspath(cache_path)
else:
self.cache_path = os.path.join(tempfile.gettempdir(), ".diskcache")
def __call__(self, func):
"""
Returns a wrapped function.
If there is no cache on disk, the function is called to get the result, cached and returned
If there is a cache on the disk, the cached result is returned directly
:param func:
"""
@func_wraps(func)
def wrapper(*args, **kw):
params_uuid = uuid.uuid5(self._NAMESPACE, "-".join(map(str, (args, kw))))
key = '{}-{}.cache'.format(func.__name__, str(params_uuid))
cache_file = os.path.join(self.cache_path, key)
if not os.path.exists(self.cache_path):
os.makedirs(self.cache_path)
try:
with open(cache_file, 'rb') as f:
val = pickle.load(f)
except Exception:
val = func(*args, **kw)
try:
with open(cache_file, 'wb') as f:
pickle.dump(val, f)
except Exception:
pass
return val
return wrapper
def clear(self, func_name: str = None) -> None:
"""
clear function cache
:param func_name:
:return:
"""
if func_name is None:
log.info("💾 Clear all function cache")
if os.path.exists(self.cache_path):
shutil.rmtree(self.cache_path)
else:
log.info(f"💾 Clear function cache: {func_name}")
for cache_file in os.listdir(self.cache_path):
if cache_file.startswith(func_name + "-"):
os.remove(os.path.join(self.cache_path, cache_file))
disk_cache = DiskCache
================================================
FILE: seldom/utils/cache_data.json
================================================
{}
================================================
FILE: seldom/utils/dependence.py
================================================
from typing import Callable, Text, Tuple
from functools import wraps
from seldom.utils import cache
from seldom.logging import log
def dependent_func(func_obj: Callable, key_name: Text = None, *out_args, **out_kwargs):
"""
Dependent function decorator.
:param func_obj: function object.
:param key_name:
:param out_args:
:param out_kwargs:
:return:
"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
func_name = func.__name__
if isinstance(func_obj, staticmethod):
depend_func_name = func_obj.__func__.__name__
else:
depend_func_name = func_obj.__name__
key = key_name
if key_name is None:
key = depend_func_name
if not cache.get(key):
dependence_res = _call_dependence(func_obj, func_name, *out_args, **out_kwargs)
cache.set({key: dependence_res})
else:
log.info(f"🔗 <{depend_func_name}> a has been executed, obtain it in cache through `{key}`.")
r = func(*args, **kwargs)
return r
return wrapper
return decorator
def _call_dependence(dependent_api: Callable or Text, func_name: Text, *args, **kwargs) -> Tuple:
"""
Execution dependent method.
:param dependent_api:
:param func_name:
:param args:
:param kwargs:
:return:
"""
if isinstance(dependent_api, staticmethod):
dependent_api = dependent_api.__func__
depend_func_name = dependent_api.__name__
log.info(f"🔗 <{func_name}> depends on <{depend_func_name}>, execute.")
res = dependent_api(*args, **kwargs)
return res
================================================
FILE: seldom/utils/diff.py
================================================
"""
diff file
"""
from typing import Any
from seldom.logging import log
class AssertInfo:
"""
Save assert warning/error info.
"""
warning = []
error = []
def _all_values_are_same(input_list) -> bool:
"""
Check whether all values in the list are the same amount
"""
return input_list.count(input_list[0]) == len(input_list)
def _list_sorted(data):
"""
list sorted
"""
if isinstance(data[0], dict):
if len(data[0]) == 0:
log.info("data is [{}]")
try:
# Judgment sort item
number = 0
for i in range(len(data[0].keys())):
all_value = []
for d in data:
v = list(d.values())[i]
all_value.append(v)
if _all_values_are_same(all_value) is False:
number = i
break
data = sorted(data, key=lambda x: x[list(data[0].keys())[number]])
except (TypeError, AttributeError, IndexError, KeyError):
data = data
else:
data = sorted(data)
return data
def diff_json(response_data: Any, assert_data: Any, exclude: list = None) -> None:
"""
Compare the JSON data format
"""
if exclude is None:
exclude = []
if isinstance(response_data, dict) and isinstance(assert_data, dict):
# dict format
for key in assert_data:
# skip check
if key in exclude:
continue
if key not in response_data:
AssertInfo.error.append(f"❌ Response data has no key: {key}")
for key in response_data:
# skip check
if key in exclude:
continue
if key in assert_data:
# recursion
diff_json(response_data[key], assert_data[key], exclude)
else:
AssertInfo.warning.append(f"💡 Assert data has not key: {key}")
elif isinstance(response_data, list) and isinstance(assert_data, list):
# list format
if len(response_data) == 0:
log.info("response is []")
else:
response_data = _list_sorted(response_data)
if len(response_data) != len(assert_data):
log.info(f"list len: '{len(response_data)}' != '{len(assert_data)}'")
if len(assert_data) > 0:
assert_data = _list_sorted(assert_data)
for src_list, dst_list in zip(response_data, assert_data):
# recursion
diff_json(src_list, dst_list, exclude)
else:
# different format
if str(response_data) != str(assert_data):
AssertInfo.error.append(f"❌ Value are not equal: {assert_data} != {response_data}")
================================================
FILE: seldom/utils/encrypt.py
================================================
"""
Encryption Utility Module
Provides comprehensive encryption and decryption functionalities, supporting the following features:
Features:
- Hash Algorithms
* MD5
* SHA1/SHA224/SHA256/SHA384/SHA512
* HMAC
- Symmetric Encryption
* AES (CBC/ECB/CFB/OFB/CTR)
* DES
* 3DES
- Asymmetric Encryption
* RSA
- Encoding Conversion
* Base16/Base32/Base64/Base85
* URL Encoding
* HTML Encoding
- Random Number Generation
* Secure Random Numbers
* UUID Generation
"""
import base64
import hashlib
import hmac
import html
import urllib.parse
import uuid
from enum import Enum, auto
from functools import wraps
from typing import Optional, Type
from Crypto.Cipher import AES, DES, DES3, PKCS1_OAEP
from Crypto.PublicKey import RSA
from Crypto.Random import get_random_bytes
from Crypto.Util.Padding import pad, unpad
from seldom.logging import log
class CipherMode(Enum):
"""Cipher Mode Enumeration"""
ECB = auto()
CBC = auto()
CFB = auto()
OFB = auto()
CTR = auto()
def encrypt_handler(func):
"""Decorator for encryption operations"""
@wraps(func)
def wrapper(*args, **kwargs):
try:
result = func(*args, **kwargs)
log.info(f"✅ [{func.__name__}] method, generated data: {result}")
return result
except Exception as e:
log.error(f"❌ [{func.__name__}] operation failed: {str(e)}")
raise
return wrapper
class HashUtil:
"""Hash Algorithm Utility Class"""
@staticmethod
@encrypt_handler
def md5(text: str, encoding: str = 'utf-8') -> str:
"""MD5 Hash"""
return hashlib.md5(str(text).encode(encoding)).hexdigest()
@staticmethod
@encrypt_handler
def sha1(text: str, encoding: str = 'utf-8') -> str:
"""SHA1 Hash"""
return hashlib.sha1(str(text).encode(encoding)).hexdigest()
@staticmethod
@encrypt_handler
def sha256(text: str, encoding: str = 'utf-8') -> str:
"""SHA256 Hash"""
return hashlib.sha256(str(text).encode(encoding)).hexdigest()
@staticmethod
@encrypt_handler
def sha512(text: str, encoding: str = 'utf-8') -> str:
"""SHA512 Hash"""
return hashlib.sha512(str(text).encode(encoding)).hexdigest()
@staticmethod
@encrypt_handler
def hmac_sha256(key: str, text: str, encoding: str = 'utf-8') -> str:
"""HMAC-SHA256"""
return hmac.new(
key.encode(encoding),
text.encode(encoding),
hashlib.sha256
).hexdigest()
class AESUtil:
"""AES Encryption Utility Class"""
@staticmethod
def _pad_key(key: bytes) -> bytes:
"""Adjust key length to 16/24/32 bytes"""
key_length = len(key)
if key_length <= 16:
return key.ljust(16, b'\0')
elif key_length <= 24:
return key.ljust(24, b'\0')
else:
return key.ljust(32, b'\0')
@staticmethod
@encrypt_handler
def encrypt(key: str, text: str, mode: CipherMode = CipherMode.CBC,
encoding: str = 'utf-8') -> str:
"""
AES Encryption
Args:
key: Key (automatically adjusted to 16/24/32 bytes)
text: Text to encrypt
mode: Encryption mode
encoding: Character encoding
Returns:
str: Base64-encoded encrypted result
"""
# Process key
padded_key = AESUtil._pad_key(key.encode(encoding))
# Generate random IV
iv = get_random_bytes(16)
# Create cipher
if mode == CipherMode.ECB:
cipher = AES.new(padded_key, AES.MODE_ECB)
else:
cipher = AES.new(padded_key, getattr(AES, f'MODE_{mode.name}'), iv)
# Encrypt
padded_data = pad(text.encode(encoding), AES.block_size)
encrypted_data = cipher.encrypt(padded_data)
# Combine IV and encrypted data
result = iv + encrypted_data if mode != CipherMode.ECB else encrypted_data
return base64.b64encode(result).decode(encoding)
@staticmethod
@encrypt_handler
def decrypt(key: str, encrypted_text: str, mode: CipherMode = CipherMode.CBC,
encoding: str = 'utf-8') -> str:
"""
AES Decryption
Args:
key: Key (automatically adjusted to 16/24/32 bytes)
encrypted_text: Base64-encoded encrypted text
mode: Encryption mode
encoding: Character encoding
Returns:
str: Decrypted original text
"""
# Process key
padded_key = AESUtil._pad_key(key.encode(encoding))
# Decode Base64
encrypted_data = base64.b64decode(encrypted_text)
# Extract IV and encrypted data
if mode == CipherMode.ECB:
iv = b''
cipher_text = encrypted_data
else:
iv = encrypted_data[:16]
cipher_text = encrypted_data[16:]
# Create cipher
if mode == CipherMode.ECB:
cipher = AES.new(padded_key, AES.MODE_ECB)
else:
cipher = AES.new(padded_key, getattr(AES, f'MODE_{mode.name}'), iv)
# Decrypt
decrypted_data = cipher.decrypt(cipher_text)
unpadded_data = unpad(decrypted_data, AES.block_size)
return unpadded_data.decode(encoding)
class DESUtil:
"""DES Encryption Utility Class"""
@staticmethod
def _pad_key(key: bytes) -> bytes:
"""Adjust key length to 8 bytes"""
return key[:8].ljust(8, b'\0')
@staticmethod
@encrypt_handler
def encrypt(key: str, text: str, mode: CipherMode = CipherMode.CBC,
encoding: str = 'utf-8') -> str:
"""
DES Encryption
Args:
key: Key (automatically adjusted to 8 bytes)
text: Text to encrypt
mode: Encryption mode
encoding: Character encoding
Returns:
str: Base64-encoded encrypted result
"""
# Process key
padded_key = DESUtil._pad_key(key.encode(encoding))
# Generate random IV
iv = get_random_bytes(8)
# Create cipher
if mode == CipherMode.ECB:
cipher = DES.new(padded_key, DES.MODE_ECB)
else:
cipher = DES.new(padded_key, getattr(DES, f'MODE_{mode.name}'), iv)
# Encrypt
padded_data = pad(text.encode(encoding), DES.block_size)
encrypted_data = cipher.encrypt(padded_data)
# Combine IV and encrypted data
result = iv + encrypted_data if mode != CipherMode.ECB else encrypted_data
return base64.b64encode(result).decode(encoding)
@staticmethod
@encrypt_handler
def decrypt(key: str, encrypted_text: str, mode: CipherMode = CipherMode.CBC,
encoding: str = 'utf-8') -> str:
"""
DES Decryption
Args:
key: Key (automatically adjusted to 8 bytes)
encrypted_text: Base64-encoded encrypted text
mode: Encryption mode
encoding: Character encoding
Returns:
str: Decrypted original text
"""
# Process key
padded_key = DESUtil._pad_key(key.encode(encoding))
# Decode Base64
encrypted_data = base64.b64decode(encrypted_text)
# Extract IV and encrypted data
if mode == CipherMode.ECB:
iv = b''
cipher_text = encrypted_data
else:
iv = encrypted_data[:8]
cipher_text = encrypted_data[8:]
# Create cipher
if mode == CipherMode.ECB:
cipher = DES.new(padded_key, DES.MODE_ECB)
else:
cipher = DES.new(padded_key, getattr(DES, f'MODE_{mode.name}'), iv)
# Decrypt
decrypted_data = cipher.decrypt(cipher_text)
unpadded_data = unpad(decrypted_data, DES.block_size)
return unpadded_data.decode(encoding)
class TripleDESUtil:
"""3DES Encryption Utility Class"""
@staticmethod
def _pad_key(key: bytes) -> bytes:
"""Adjust key length to 24 bytes"""
return key[:24].ljust(24, b'\0')
@staticmethod
@encrypt_handler
def encrypt(key: str, text: str, mode: CipherMode = CipherMode.CBC,
encoding: str = 'utf-8') -> str:
"""
3DES Encryption
Args:
key: Key (automatically adjusted to 24 bytes)
text: Text to encrypt
mode: Encryption mode
encoding: Character encoding
Returns:
str: Base64-encoded encrypted result
"""
# Process key
padded_key = TripleDESUtil._pad_key(key.encode(encoding))
# Generate random IV
iv = get_random_bytes(8)
# Create cipher
if mode == CipherMode.ECB:
cipher = DES3.new(padded_key, DES3.MODE_ECB)
else:
cipher = DES3.new(padded_key, getattr(DES3, f'MODE_{mode.name}'), iv)
# Encrypt
padded_data = pad(text.encode(encoding), DES3.block_size)
encrypted_data = cipher.encrypt(padded_data)
# Combine IV and encrypted data
result = iv + encrypted_data if mode != CipherMode.ECB else encrypted_data
return base64.b64encode(result).decode(encoding)
@staticmethod
@encrypt_handler
def decrypt(key: str, encrypted_text: str, mode: CipherMode = CipherMode.CBC,
encoding: str = 'utf-8') -> str:
"""
3DES Decryption
Args:
key: Key (automatically adjusted to 24 bytes)
encrypted_text: Base64-encoded encrypted text
mode: Encryption mode
encoding: Character encoding
Returns:
str: Decrypted original text
"""
# Process key
padded_key = TripleDESUtil._pad_key(key.encode(encoding))
# Decode Base64
encrypted_data = base64.b64decode(encrypted_text)
# Extract IV and encrypted data
if mode == CipherMode.ECB:
iv = b''
cipher_text = encrypted_data
else:
iv = encrypted_data[:8]
cipher_text = encrypted_data[8:]
# Create cipher
if mode == CipherMode.ECB:
cipher = DES3.new(padded_key, DES3.MODE_ECB)
else:
cipher = DES3.new(padded_key, getattr(DES3, f'MODE_{mode.name}'), iv)
# Decrypt
decrypted_data = cipher.decrypt(cipher_text)
unpadded_data = unpad(decrypted_data, DES3.block_size)
return unpadded_data.decode(encoding)
class RSAUtil:
"""RSA Encryption Utility Class"""
def __init__(self, public_key: Optional[str] = None,
private_key: Optional[str] = None):
"""
Initialize RSA Utility
Args:
public_key: PEM-format public key
private_key: PEM-format private key
"""
self.public_key = RSA.import_key(public_key) if public_key else None
self.private_key = RSA.import_key(private_key) if private_key else None
@staticmethod
@encrypt_handler
def generate_key_pair(bits: int = 2048) -> tuple[str, str]:
"""
Generate RSA Key Pair
Args:
bits: Key length
Returns:
tuple: (Public key, Private key) in PEM format
"""
key = RSA.generate(bits)
private_key = key.export_key().decode()
public_key = key.publickey().export_key().decode()
return public_key, private_key
@encrypt_handler
def encrypt(self, text: str, encoding: str = 'utf-8') -> str:
"""
RSA Encryption
Args:
text: Text to encrypt
encoding: Character encoding
Returns:
str: Base64-encoded encrypted result
"""
if not self.public_key:
raise ValueError("Public key not set")
cipher = PKCS1_OAEP.new(self.public_key)
encrypted_data = cipher.encrypt(text.encode(encoding))
return base64.b64encode(encrypted_data).decode(encoding)
@encrypt_handler
def decrypt(self, encrypted_text: str, encoding: str = 'utf-8') -> str:
"""
RSA Decryption
Args:
encrypted_text: Base64-encoded encrypted text
encoding: Character encoding
Returns:
str: Decrypted original text
"""
if not self.private_key:
raise ValueError("Private key not set")
cipher = PKCS1_OAEP.new(self.private_key)
encrypted_data = base64.b64decode(encrypted_text)
decrypted_data = cipher.decrypt(encrypted_data)
return decrypted_data.decode(encoding)
class EncodeUtil:
"""Encoding Utility Class"""
@staticmethod
@encrypt_handler
def base64_encode(text: str, encoding: str = 'utf-8') -> str:
"""Base64 Encoding"""
return base64.b64encode(text.encode(encoding)).decode(encoding)
@staticmethod
@encrypt_handler
def base64_decode(text: str, encoding: str = 'utf-8') -> str:
"""Base64 Decoding"""
return base64.b64decode(text).decode(encoding)
@staticmethod
@encrypt_handler
def url_encode(text: str) -> str:
"""URL Encoding"""
return urllib.parse.quote(text)
@staticmethod
@encrypt_handler
def url_decode(text: str) -> str:
"""URL Decoding"""
return urllib.parse.unquote(text)
@staticmethod
@encrypt_handler
def html_encode(text: str) -> str:
"""HTML Encoding"""
return html.escape(text)
@staticmethod
@encrypt_handler
def html_decode(text: str) -> str:
"""HTML Decoding"""
return html.unescape(text)
@staticmethod
@encrypt_handler
def base16_encode(text: str, encoding: str = 'utf-8') -> str:
"""Base16 Encoding"""
return base64.b16encode(text.encode(encoding)).decode(encoding)
@staticmethod
@encrypt_handler
def base16_decode(text: str, encoding: str = 'utf-8') -> str:
"""Base16 Decoding"""
return base64.b16decode(text).decode(encoding)
@staticmethod
@encrypt_handler
def base32_encode(text: str, encoding: str = 'utf-8') -> str:
"""Base32 Encoding"""
return base64.b32encode(text.encode(encoding)).decode(encoding)
@staticmethod
@encrypt_handler
def base32_decode(text: str, encoding: str = 'utf-8') -> str:
"""Base32 Decoding"""
return base64.b32decode(text).decode(encoding)
@staticmethod
@encrypt_handler
def base85_encode(text: str, encoding: str = 'utf-8') -> str:
"""Base85 Encoding"""
return base64.b85encode(text.encode(encoding)).decode(encoding)
@staticmethod
@encrypt_handler
def base85_decode(text: str, encoding: str = 'utf-8') -> str:
"""Base85 Decoding"""
return base64.b85decode(text).decode(encoding)
class EncryptUtil:
"""Encryption Utility Class"""
# Create static instances
hash = HashUtil()
encode = EncodeUtil()
@staticmethod
def aes() -> Type[AESUtil]:
"""Return AES Utility Class"""
return AESUtil
@staticmethod
def des() -> Type[DESUtil]:
"""Return DES Utility Class"""
return DESUtil
@staticmethod
def des3() -> Type[TripleDESUtil]:
"""Return 3DES Utility Class"""
return TripleDESUtil
@staticmethod
def rsa(public_key: Optional[str] = None,
private_key: Optional[str] = None) -> RSAUtil:
"""Create RSA Utility Instance"""
return RSAUtil(public_key, private_key)
# Create default encryption utility instance
encrypt_util = EncryptUtil()
================================================
FILE: seldom/utils/file_extend.py
================================================
"""
file extend
"""
import os
import sys
import inspect
from pathlib import Path
class FindFilePath:
"""find file path"""
def __new__(cls, name: str = None) -> str:
if name is None:
raise NameError("Please specify filename")
stack_t = inspect.stack()
ins = inspect.getframeinfo(stack_t[1][0])
file_path = Path(ins.filename).resolve()
this_file_dir = file_path.parent.parent
_file_path = None
for root, _, files in os.walk(this_file_dir, topdown=False):
for _file in files:
if _file == name:
_file_path = os.path.join(root, _file)
break
else:
continue
break
return _file_path
find_file_path = FindFilePath
class File:
"""file class"""
@staticmethod
def _get_caller_path(min_stack_level: int = 2) -> str:
"""
Automatically find and return the correct caller's file path as string.
"""
# 遍历调用栈,找到最终调用者的文件路径
for level in range(min_stack_level, len(inspect.stack())):
frame = inspect.stack()[level].frame
filename = inspect.getframeinfo(frame).filename
if not filename.endswith("file_extend.py"):
return str(Path(filename).resolve())
raise RuntimeError("Unable to determine caller path.")
@property
def path(self) -> str:
"""
Returns the absolute path to the current file.
e.g., /User/tech/you/test_dir/test_sample.py
"""
return str(self._get_caller_path())
@property
def dir(self) -> str:
"""
Returns the absolute path to the directory where the current file resides
For example:
"/User/tech/you/test_dir/test_sample.py"
return "/User/tech/you/test_dir/"
"""
return str(Path(self.path).parent)
def parent_dir(self, level: int = 1) -> str:
"""
Generic method to get N-th parent directory.
:param level: How many levels up from current file.
:return: Parent directory path.
"""
return str(Path(self.path).parents[level - 1])
@property
def dir_dir(self) -> str:
"""
Returns the absolute directory path of the current file directory.
For example:
"/User/tech/you/test_dir/test_sample.py"
return "/User/tech/you/"
"""
return self.parent_dir(2)
@property
def dir_dir_dir(self) -> str:
"""
Returns the absolute directory path of the current file directory
For example:
/User/tech/you/test_dir/test_sample.py
return "/User/tech/"
"""
return self.parent_dir(3)
@staticmethod
def add_to_path(path: str = None) -> None:
"""
add path to environment variable path.
"""
if path is None:
raise FileNotFoundError("Please setting the File Path")
sys.path.insert(1, path)
@staticmethod
def join(a, *paths):
"""
Connect two or more path names
"""
return os.path.join(a, *paths)
@staticmethod
def remove(path) -> None:
"""
del file
:param path:
:return:
"""
if Path(path).exists():
os.remove(path)
else:
raise FileNotFoundError("file does not exist")
file = File()
================================================
FILE: seldom/utils/genson.py
================================================
"""
genson:
https://github.com/wolverdude/GenSON
"""
from genson import SchemaBuilder
from seldom.request import ResponseResult
def genson(data: dict = None):
"""
return schema data
"""
if (data is None) and ResponseResult.response is not None:
data = ResponseResult.response
builder = SchemaBuilder()
builder.add_object(data)
to_schema = builder.to_schema()
return to_schema
================================================
FILE: seldom/utils/jmespath.py
================================================
"""
jmespath search data
https://github.com/jmespath/jmespath.py
"""
from jmespath import search
def jmespath(data, expression, options=None):
"""
search jmespath data
"""
return search(expression, data, options)
================================================
FILE: seldom/utils/match_image.py
================================================
import inspect
import os
from pathlib import Path
try:
from PIL import Image, ImageChops
except ModuleNotFoundError as e:
raise ModuleNotFoundError("Please install the library. https://python-pillow.github.io")
from seldom.logging import log
def save_screenshot(page, file_path: str) -> None:
"""
Capture and save a screenshot.
:param page: Playwright or Selenium page object
:param file_path: Path where the screenshot will be saved
"""
if hasattr(page, 'screenshot'):
page.screenshot(path=file_path)
else:
page.save_screenshot(file_path)
def compare_images(img1_path: str, img2_path: str, tolerance: int = 0) -> bool:
"""
Compare two images and return True if they are the same, False otherwise.
:param img1_path: Path of the first image
:param img2_path: Path of the second image
:param tolerance: Allowed pixel difference (default is 0)
:return: True if images are the same, False if they are different
"""
img1 = Image.open(img1_path)
img2 = Image.open(img2_path)
if img1.size != img2.size:
return False
diff = ImageChops.difference(img1, img2)
bbox = diff.getbbox()
if bbox:
if tolerance > 0:
diff_data = diff.getdata()
diff_count = sum(1 for pixel in diff_data if any(i > tolerance for i in pixel))
if diff_count / len(diff_data) < tolerance:
return True
return False
return True
def assert_screenshot(page, tolerance: int = 0, stack_t=None) -> bool | None:
"""
Automatically capture a screenshot and compare it with the baseline image.
:param page: Playwright or Selenium page object
:param tolerance: Allowed pixel difference for comparison (default is 0)
:param stack_t: Time to wait before comparing again
:raises AssertionError: If images do not match
"""
ins = inspect.getframeinfo(stack_t[1][0])
file_path = Path(ins.filename).resolve()
scr_dir = os.path.join(file_path.parent, "reports", "screenshots")
if os.path.exists(scr_dir) is False:
os.mkdir(scr_dir)
file_name = file_path.stem
class_name = stack_t[1][0].f_locals.get('self').__class__.__name__
method_name = stack_t[1][3]
base_img_name = f"{file_name}.py_{class_name}_{method_name}_base.png"
diff_img_name = f"{file_name}.py_{class_name}_{method_name}_diff.png"
base_img_path = os.path.join(scr_dir, base_img_name)
diff_img_path = os.path.join(scr_dir, diff_img_name)
if not os.path.exists(base_img_path):
save_screenshot(page, base_img_path)
log.info(f"Generate the base image file: {base_img_path}")
return None
else:
if os.path.exists(diff_img_path):
os.remove(diff_img_path)
save_screenshot(page, diff_img_path)
log.info(f"Generate the diff image file: {diff_img_path}")
if not compare_images(base_img_path, diff_img_path, tolerance):
log.error(f"Image assertion failed: {base_img_name} and {diff_img_name} are different")
return False
else:
log.info(f"Image assertion passed: {base_img_name} and {diff_img_name} match")
return True
================================================
FILE: seldom/utils/resource_loader.py
================================================
import json
from functools import lru_cache
from pathlib import Path
from typing import Any, Dict, Literal
from seldom.logging import log
from seldom.testdata.parameterization import find_file
def resource_file(
file: str,
key: str | None = None,
base_dir: Path | None = None,
*,
return_type: Literal["gql", "json"] | None = None,
) -> str | Dict[str, Any]:
"""
Load GraphQL or JSON resource file for test cases.
:param file: Filename (e.g., 'query.gql', 'data.json')
:param key: If JSON, extract nested value by key (dot-separated, e.g., 'user.profile')
:param base_dir: Base directory to search from. If None, uses caller's directory.
:param return_type: Explicitly specify expected type ('gql' or 'json') for better typing.
:return: str (for GraphQL) or dict (for JSON)
"""
if not file:
raise ValueError("File name must not be empty.")
# Determine base directory
if base_dir is None:
import inspect
caller_frame = inspect.currentframe().f_back
if caller_frame is None:
raise RuntimeError("Unable to determine caller directory.")
base_dir = Path(caller_frame.f_code.co_filename).parent.resolve()
# Locate file
file_path = Path(find_file(file, base_dir))
if not file_path or not file_path.exists():
raise FileNotFoundError(f"Resource file not found: {file}")
# Determine format by suffix (robust)
suffix = file_path.suffix.lower()[1:] # e.g., 'gql', 'graphql', 'json'
if return_type is None:
if suffix in ("graphql", "gql"):
return_type = "gql"
elif suffix == "json":
return_type = "json"
else:
raise ValueError(f"Unsupported file extension: {suffix}. Use .gql, .graphql, or .json.")
# Load content
content = _load_cached_file(file_path, is_json=(return_type == "json"))
# Extract by key if needed (for JSON)
if return_type == "json" and key:
content = _get_nested_value(content, key)
return content
@lru_cache(maxsize=128)
def _load_cached_file(file_path: Path, is_json: bool) -> str | Dict[str, Any]:
"""Cached file loader to avoid repeated I/O in parametrized tests."""
try:
with open(file_path, 'r', encoding='utf-8') as f:
if is_json:
return json.load(f)
else:
return f.read().strip()
except Exception as e:
log.error(f"Failed to load resource file {file_path}: {e}")
raise
def _get_nested_value(data: Dict[str, Any], key_path: str) -> Any:
"""Support dot-notation key access: e.g., 'user.profile.name'."""
keys = key_path.split('.')
value = data
for k in keys:
if not isinstance(value, dict):
raise KeyError(f"Cannot traverse into non-dict at key: {k}")
if k not in value:
raise KeyError(f"Key '{k}' not found in nested data. Full path: {key_path}")
value = value[k]
return value
================================================
FILE: seldom/utils/send_extend.py
================================================
"""
send message file
"""
import os
from XTestRunner import DingTalk as XDingTalk
from XTestRunner import FeiShu as XFeiShu
from XTestRunner import SMTP as XSMTP
from XTestRunner import Weinxin as XWeinxin
from XTestRunner.config import RunResult as XRunResult
from seldom.running.config import BrowserConfig
from seldom.utils import file
class SMTP(XSMTP):
"""send email class"""
def sendmail(self, to: str | list[str], subject: str = None, attachments: str = None, delete: bool = False) -> None:
"""
seldom send email
:param to:
:param subject:
:param attachments:
:param delete: delete report&log file
:return
"""
if attachments is None:
attachments = BrowserConfig.REPORT_PATH
if subject is None:
subject = BrowserConfig.REPORT_TITLE
self.sender(to=to, subject=subject, attachments=attachments)
if delete is True:
file.remove(BrowserConfig.REPORT_PATH)
is_exist = os.path.isfile(BrowserConfig.LOG_PATH)
if is_exist is True:
with open(BrowserConfig.LOG_PATH, "r+", encoding="utf-8") as log_file:
log_file.truncate(0)
class DingTalk(XDingTalk):
"""
send dingtalk, Inherit XTestRunner DingTalk Class
"""
class FeiShu(XFeiShu):
"""
send FeiShu, Inherit XTestRunner FeiShu Class
"""
class Weinxin(XWeinxin):
"""
send weixin, Inherit XTestRunner weixin Class
"""
class RunResult(XRunResult):
"""
XTestRunner Test run results
"""
================================================
FILE: seldom/utils/thread_lab.py
================================================
"""
case more threading
"""
from threading import Thread
from seldom.logging import log
class ThreadWait:
"""
Function thread decorator
get_result()
ThreadWait.get_all_result()
"""
result_dict = {}
thread_dict = {}
class SeldomThread(Thread):
"""seldom thread"""
def __init__(self, func, name='', *args, **kwargs):
Thread.__init__(self)
self.func = func
self.name = name
self.args = args
self.kwargs = kwargs
self.result = None
def run(self):
log.info(f"{self} Start with SeldomThread...")
if isinstance(self.args, tuple) and len(self.args) > 1:
name_key = self.args[0]
else:
name_key = self.ident
self.result = self.func(*self.args, **self.kwargs)
ThreadWait.result_dict[name_key] = self.result
def get_result(self):
"""Return run result"""
self.join()
return self.result
def __init__(self, func):
self.func = func
def __call__(self, *args, **kwargs):
_my_thread = self.SeldomThread(self.func, self.func.__name__, *args, **kwargs)
_my_thread.start()
self.thread_dict[_my_thread.ident] = _my_thread
return _my_thread
@classmethod
def get_all_result(cls):
"""Return all run result"""
for k, thr in cls.thread_dict.items():
thr.join()
return cls.result_dict
================================================
FILE: seldom/utils/timer.py
================================================
import time
from seldom.logging import log
def timer(func):
"""
timer decorator
:param func:
:return:
"""
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
log.info(f"{func.__name__} executed in {end_time - start_time:.3f}s")
return result
return wrapper
================================================
FILE: seldom/webcommon/__init__.py
================================================
================================================
FILE: seldom/webcommon/find_elems.py
================================================
import time
from selenium.common.exceptions import TimeoutException
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
from seldom.logging.exceptions import NotFindElementError
from seldom.running.config import Seldom
from seldom.webcommon.locators import LOCATOR_LIST
from seldom.webcommon.selector import selection_checker
class WebElement:
"""Web Element API"""
def __init__(self, browser, selector: str = None, **kwargs) -> None:
self.browser = browser
if selector is not None:
self.by, self.value = selection_checker(selector)
else:
if not kwargs:
raise ValueError("Please specify a locator.")
if len(kwargs) > 1:
raise ValueError("Please specify only one locator.")
by, self.value = next(iter(kwargs.items()))
self.by = LOCATOR_LIST.get(by)
if not self.by:
raise ValueError(f"The find element locator is not supported: {by}. ")
self.find_elem_info = None
self.find_elem_warn = None
def find(self, index: int = None, empty: bool = False, highlight: bool = False):
"""
Return the element(s) found by the locator.
:param index:
:param empty:
:param highlight:
:return:
"""
# Use WebDriverWait instead of time.sleep for smarter waiting
try:
elems = WebDriverWait(self.browser, Seldom.timeout).until(
EC.presence_of_all_elements_located((self.by, self.value))
)
self.find_elem_info = f"Found {len(elems)} element(s): {self.by}={self.value}"
except TimeoutException:
self.find_elem_warn = f"❌ No elements found for: {self.by}={self.value}"
if not empty:
raise NotFindElementError(self.find_elem_warn)
return []
if index is None:
return elems
# Return specific element by index
if index < len(elems):
# element highlight
if highlight is True:
self._highlight_element(elems[index])
return elems[index]
raise IndexError(f"Index {index} out of bounds for the found elements.")
def _highlight_element(self, elem=None) -> None:
"""
Highlight the element on the page for debugging purposes.
:param elem: Web element to be highlighted
"""
if Seldom.app_server is not None and Seldom.app_info is not None:
return None # Skip highlighting if app info is unavailable
border_styles = []
if Seldom.debug:
border_styles = [
'arguments[0].style.border="3px solid #FF0000"',
'arguments[0].style.border="3px solid #00FF00"',
'arguments[0].style.border=""'
]
for style in border_styles:
self.browser.execute_script(style, elem)
time.sleep(0.2)
return None
@property
def info(self):
"""Return element info"""
return self.find_elem_info
@property
def warn(self):
"""Return element warning"""
return self.find_elem_warn
================================================
FILE: seldom/webcommon/keyboard.py
================================================
import platform
from selenium.webdriver.common.keys import Keys
from seldom.logging import log
from seldom.webcommon.find_elems import WebElement
class KeysClass:
"""
Achieve keyboard shortcuts
Usage:
self.Keys(id_="kw").enter()
"""
def __init__(self, browser, selector: str = None, index: int = 0, **kwargs) -> None:
self.browser = browser
self.web_elem = WebElement(self.browser, selector=selector, **kwargs)
self.elem = self.web_elem.find(index, highlight=True)
def input(self, text=""):
"""
input text
:param text:
:return:
"""
log.info(f"✅ {self.web_elem.info}, input '{text}'.")
self.elem.send_keys(text)
return self
def enter(self):
"""
enter.
:return:
"""
log.info(f"✅ {self.web_elem.info}, enter.")
self.elem.send_keys(Keys.ENTER)
return self
def select_all(self):
"""
select all.
:return:
"""
log.info(f"✅ {self.web_elem.info}, ctrl+a.")
if platform.system().lower() == "darwin":
self.elem.send_keys(Keys.COMMAND, "a")
else:
self.elem.send_keys(Keys.CONTROL, "a")
return self
def cut(self):
"""
cut.
:return:
"""
log.info(f"✅ {self.web_elem.info}, ctrl+x.")
if platform.system().lower() == "darwin":
self.elem.send_keys(Keys.COMMAND, "x")
else:
self.elem.send_keys(Keys.CONTROL, "x")
return self
def copy(self):
"""
copy.
:return:
"""
log.info(f"✅ {self.web_elem.info}, ctrl+c.")
if platform.system().lower() == "darwin":
self.elem.send_keys(Keys.COMMAND, "c")
else:
self.elem.send_keys(Keys.CONTROL, "c")
return self
def paste(self):
"""
paste.
:return:
"""
log.info(f"✅ {self.web_elem.info}, ctrl+v.")
if platform.system().lower() == "darwin":
self.elem.send_keys(Keys.COMMAND, "v")
else:
self.elem.send_keys(Keys.CONTROL, "v")
return self
def backspace(self):
"""
Backspace key.
:return:
"""
log.info(f"✅ {self.web_elem.info}, backspace.")
self.elem.send_keys(Keys.BACKSPACE)
return self
def delete(self):
"""
Delete key.
:return:
"""
log.info(f"✅ {self.web_elem.info}, delete.")
self.elem.send_keys(Keys.DELETE)
return self
def tab(self):
"""
Tab key.
"""
log.info(f"✅ {self.web_elem.info}, tab.")
self.elem.send_keys(Keys.TAB)
def space(self):
"""
Space key.
:return:
"""
log.info(f"✅ {self.web_elem.info}, space.")
self.elem.send_keys(Keys.SPACE)
return self
================================================
FILE: seldom/webcommon/locators.py
================================================
"""
appium & selenium locator
"""
from appium.webdriver.common.appiumby import AppiumBy as By
LOCATOR_LIST = {
'css': By.CSS_SELECTOR,
'id_': By.ID,
'name': By.NAME,
'xpath': By.XPATH,
'link_text': By.LINK_TEXT,
'partial_link_text': By.PARTIAL_LINK_TEXT,
'tag': By.TAG_NAME,
'class_name': By.CLASS_NAME,
'ios_predicate': By.IOS_PREDICATE,
'ios_class_chain': By.IOS_CLASS_CHAIN,
'android_uiautomator': By.ANDROID_UIAUTOMATOR,
'android_viewtag': By.ANDROID_VIEWTAG,
'android_data_matcher': By.ANDROID_DATA_MATCHER,
'android_view_matcher': By.ANDROID_VIEW_MATCHER,
'accessibility_id': By.ACCESSIBILITY_ID,
'image': By.IMAGE,
'custom': By.CUSTOM,
}
SELECTOR_LIST = {
"text=": (By.LINK_TEXT, 5),
"text~=": (By.PARTIAL_LINK_TEXT, 6),
"text*=": (By.PARTIAL_LINK_TEXT, 6),
"id=": (By.ID, 3),
"name=": (By.NAME, 5),
"class=": (By.CLASS_NAME, 6),
"tag=": (By.TAG_NAME, 4),
"ios_predicate=": (By.IOS_PREDICATE, 14),
"ios_class_chain=": (By.IOS_CLASS_CHAIN, 16),
"android_uiautomator=": (By.ANDROID_UIAUTOMATOR, 20),
"android_viewtag=": (By.ANDROID_VIEWTAG, 16),
"android_datamatcher=": (By.ANDROID_DATA_MATCHER, 20),
"android_viewmatcher=": (By.ANDROID_VIEW_MATCHER, 20),
"accessibility_id=": (By.ACCESSIBILITY_ID, 17),
"image=": (By.IMAGE, 6),
"xpath=": (By.XPATH, 6),
"css=": (By.CSS_SELECTOR, 4),
}
================================================
FILE: seldom/webcommon/selector.py
================================================
from appium.webdriver.common.appiumby import AppiumBy as By
from seldom.webcommon.locators import SELECTOR_LIST
def selection_checker(selector: str) -> (str, str):
"""
Check the location method and return the corresponding locator strategy and value.
:param selector: Selector string, which includes a prefix indicating the type of locator.
:return: Tuple (locator strategy, value)
"""
if len(selector) == 0:
raise ValueError(f"The selector cannot have length 0")
# Check for prefix match in the locator dictionary
for prefix, (locator, length) in SELECTOR_LIST.items():
if selector.startswith(prefix) and len(selector) > length:
return locator, selector[length:]
# Handle xpath and css selectors
if selector.startswith("/"):
return By.XPATH, selector
else:
return By.CSS_SELECTOR, selector
================================================
FILE: seldom/webdriver.py
================================================
"""
selenium WebDriver API
"""
import base64
import os
import time
import warnings
from selenium.common.exceptions import TimeoutException
from selenium.webdriver import Chrome
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.remote.webdriver import WebDriver as SeleniumWebDriver
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.select import Select
from selenium.webdriver.support.wait import WebDriverWait
from seldom.driver import Browser
from seldom.logging import log
from seldom.logging.exceptions import RunningError
from seldom.running.config import Seldom, BrowserConfig
from seldom.testdata import get_timestamp
from seldom.webcommon.find_elems import WebElement
from seldom.webcommon.keyboard import KeysClass
from seldom.webcommon.locators import LOCATOR_LIST
__all__ = ["WebDriver"]
class WebDriver:
"""
Seldom framework for the main class, the original
selenium provided by the method of the two packaging,
making it easier to use.
"""
def __init__(self, browser_name: str = None, is_new: bool = False, images: list = []):
self.images = images
if browser_name is not None:
self.browser = Browser(browser_name, BrowserConfig.executable_path, BrowserConfig.options,
BrowserConfig.command_executor)
Seldom.driver = self.browser
elif is_new is True:
self.browser = Browser(BrowserConfig.NAME, BrowserConfig.executable_path, BrowserConfig.options,
BrowserConfig.command_executor)
else:
self.browser = Seldom.driver
def Keys(self, selector: str = None, index: int = 0, **kwargs) -> KeysClass:
"""return KeysClass class"""
keys = KeysClass(self.browser, selector=selector, index=index, **kwargs)
return keys
class Alert:
"""
Alert operation.
"""
def __init__(self, browser):
self.browser = browser
@property
def text(self) -> str:
"""
Gets the text of the Alert.
"""
log.info(f"✅ alert text: {self.browser.switch_to.alert.text}.")
return self.browser.switch_to.alert.text
def dismiss(self) -> None:
"""
Dismisses the alert available.
"""
log.info("✅ dismiss alert.")
return self.browser.switch_to.alert.dismiss()
def accept(self):
"""
Accepts the alert available.
Usage::
Alert(driver).accept() # Confirm the alert dialog.
"""
log.info("✅ accept alert.")
return self.browser.switch_to.alert.accept()
def send_keys(self, text: str) -> None:
"""
Send Keys to the Alert.
:Args:
- text: The text to be sent to Alert.
"""
log.info(f"✅ input alert '{text}'.")
return self.browser.switch_to.alert.send_keys(text)
def prompt_value(self, text: str):
"""
set prompt value
:param text:
:return:
"""
log.info(f"✅ Set prompt input '{text}'.")
return self.browser.execute_script('window.prompt = function() { return "' + text + '"; }')
@property
def alert(self) -> Alert:
"""return Alert class"""
alert = self.Alert(self.browser)
return alert
def visit(self, url: str) -> None:
"""
visit url.
Usage:
self.visit("https://www.baidu.com")
"""
log.info(f"📖 {url}")
try:
self.browser.get(url)
except BaseException:
raise RunningError("""❌️Muggle! Seldom running on Pycharm is not supported.
You go See See: https://seldomqa.github.io/getting-started/quick_start.html""")
def open_electron(self, app_path: str, disable_gpu: bool = False, chromedriver_path=None) -> None:
"""
open electron application, default(chrome)
:param app_path: App executable file path.
:param disable_gpu: disable GPU.
:param chromedriver_path: chromedriver local path.
Usage:
self.open_electron('/User/app/xx.exe')
"""
options = Options()
if disable_gpu is True:
options.add_argument('--disable-gpu')
options.binary_location = app_path
log.info(f"💻 open electron app {app_path}")
self.browser = Chrome(options=options, service=Service(chromedriver_path))
def open(self, url: str) -> None:
"""
open url.
Usage:
self.open("https://www.baidu.com")
"""
self.visit(url)
@property
def page_source(self) -> str:
"""
Gets the source of the current page
:param self:
:return:
"""
return self.browser.page_source
def execute_cdp_cmd(self, cmd: str, cmd_args: dict):
"""
Execute Chrome Devtools Protocol command and get returned result The
command and command args should follow chrome devtools protocol
domains/commands, refer to link
https://chromedevtools.github.io/devtools-protocol/
"""
return self.browser.execute_cdp_cmd(cmd, cmd_args)
def get_log(self, log_type: str):
"""
Gets the log for a given log type
:Usage:
self.get_log('browser')
self.get_log('driver')
self.get_log('client')
self.get_log('server')
"""
return self.browser.get_log(log_type)
def max_window(self) -> None:
"""
Set browser window maximized.
Usage:
self.max_window()
"""
self.browser.maximize_window()
def set_window(self, wide: int = 0, high: int = 0) -> None:
"""
Set browser window wide and high.
Usage:
self.set_window(wide,high)
"""
self.browser.set_window_size(wide, high)
def get_windows(self) -> dict:
"""
Gets the width and height of the current window.
:Usage:
driver.get_windows()
"""
return self.browser.get_window_size()
def type(self, selector: str = None, text: str = "", clear: bool = False, enter: bool = False, click: bool = False,
index: int = 0,
**kwargs) -> None:
"""
Operation input box.
Usage:
self.type(css="#el", text="selenium")
"""
if clear is True:
self.clear(selector, index, **kwargs)
if click is True:
self.click(selector, index, **kwargs)
time.sleep(0.5)
web_elem = WebElement(self.browser, selector=selector, **kwargs)
elem = web_elem.find(index, highlight=True)
log.info(f"✅ {web_elem.info} -> input '{text}'.")
elem.send_keys(text)
if enter is True:
elem.send_keys(Keys.ENTER)
def type_enter(self, selector: str = None, text: str = "", clear: bool = False, index: int = 0, **kwargs) -> None:
"""
Enter text and enter directly.
Usage:
self.type_enter(css="#el", text="selenium")
"""
warnings.warn('''use self.type(css="#el", text="selenium", enter=True)''', DeprecationWarning, stacklevel=2)
if clear is True:
self.clear(selector, index, **kwargs)
web_elem = WebElement(self.browser, selector=selector, **kwargs)
elem = web_elem.find(index, highlight=True)
log.info(f"✅ {web_elem.info} -> input '{text}' and enter.")
elem.send_keys(text)
elem.send_keys(Keys.ENTER)
def clear(self, selector: str = None, index: int = 0, **kwargs) -> None:
"""
Clear the contents of the input box.
Usage:
self.clear(css="#el")
"""
web_elem = WebElement(self.browser, selector=selector, **kwargs)
elem = web_elem.find(index, highlight=True)
log.info(f"✅ {web_elem.info} -> clear input.")
elem.clear()
def click(self, selector: str = None, index: int = 0, **kwargs) -> None:
"""
It can click any text / image can be clicked
Connection, check box, radio buttons, and even drop-down box etc.
Usage:
self.click(css="#el")
"""
web_elem = WebElement(self.browser, selector=selector, **kwargs)
elem = web_elem.find(index, highlight=True)
log.info(f"✅ {web_elem.info} -> click.")
elem.click()
def slow_click(self, selector: str = None, index: int = 0, **kwargs) -> None:
"""
Moving the mouse to the middle of an element. and click element.
Usage:
self.slow_click(css="#el")
"""
web_elem = WebElement(self.browser, selector=selector, **kwargs)
elem = web_elem.find(index, highlight=True)
log.info(f"✅ {web_elem.info} -> slow click.")
ActionChains(self.browser).move_to_element(elem).click(elem).perform()
def right_click(self, selector: str = None, index: int = 0, **kwargs) -> None:
"""
Right click element.
Usage:
self.right_click(css="#el")
"""
web_elem = WebElement(self.browser, selector=selector, **kwargs)
elem = web_elem.find(index)
log.info(f"✅ {web_elem.info} -> right click.")
ActionChains(self.browser).context_click(elem).perform()
def move_to_element(self, selector: str = None, index: int = 0, **kwargs) -> None:
"""
Mouse over the element.
Usage:
self.move_to_element(css="#el")
"""
web_elem = WebElement(self.browser, selector=selector, **kwargs)
elem = web_elem.find(index)
log.info(f"✅ {web_elem.info} -> move to element.")
ActionChains(self.browser).move_to_element(elem).perform()
def click_and_hold(self, selector: str = None, index: int = 0, **kwargs) -> None:
"""
Mouse over the element.
Usage:
self.move_to_element(css="#el")
"""
web_elem = WebElement(self.browser, selector=selector, **kwargs)
elem = web_elem.find(index)
log.info(f"✅ {web_elem.info} -> click and hold.")
ActionChains(self.browser).click_and_hold(elem).perform()
def drag_and_drop_by_offset(self, selector: str = None, index: int = 0, x: int = 0, y: int = 0, **kwargs) -> None:
"""
Holds down the left mouse button on the source element,
then moves to the target offset and releases the mouse button.
:Args:
- source: The element to mouse down.
- x: X offset to move to.
- y: Y offset to move to.
"""
web_elem = WebElement(self.browser, selector=selector, **kwargs)
elem = web_elem.find(index, highlight=True)
action = ActionChains(self.browser)
log.info(f"✅ {web_elem.info} -> drag and drop by offset.")
action.drag_and_drop_by_offset(elem, x, y).perform()
def double_click(self, selector: str = None, index: int = 0, **kwargs) -> None:
"""
Double click element.
Usage:
self.double_click(css="#el")
"""
web_elem = WebElement(self.browser, selector=selector, **kwargs)
elem = web_elem.find(index, highlight=True)
log.info(f"✅ {web_elem.info} -> double click.")
ActionChains(self.browser).double_click(elem).perform()
def action_chains(self) -> ActionChains:
"""
return ActionChains class
:return:
"""
return ActionChains(self.browser)
def click_text(self, text: str, index: int = 0) -> None:
"""
Click the element by the link text
Usage:
self.click_text("新闻")
"""
web_elem = WebElement(self.browser, link_text=text)
elem = web_elem.find(index, highlight=True)
log.info(f"✅ {web_elem.info} -> click link.")
elem.click()
def close(self) -> None:
"""
Closes the current window.
Usage:
self.close()
"""
if isinstance(self.browser, SeleniumWebDriver) is True:
self.browser.close()
def submit(self, selector: str = None, index: int = 0, **kwargs) -> None:
"""
Submit the specified form.
Usage:
driver.submit(css="#el")
"""
web_elem = WebElement(self.browser, selector=selector, **kwargs)
elem = web_elem.find(index, highlight=True)
log.info(f"✅ {web_elem.info} -> submit.")
elem.submit()
def refresh(self) -> None:
"""
Refresh the current page.
Usage:
self.refresh()
"""
log.info("🔄️ refresh page.")
self.browser.refresh()
def execute_script(self, script: str, *args):
"""
Execute JavaScript scripts.
Usage:
self.execute_script("window.scrollTo(200,1000);")
"""
return self.browser.execute_script(script, *args)
def window_scroll(self, width: int = 0, height: int = 0) -> None:
"""
Setting width and height of window scroll bar.
Usage:
self.window_scroll(width=300, height=500)
"""
js = f"window.scrollTo({width},{height});"
self.execute_script(js)
def element_scroll(self, css: str, width: int = 0, height: int = 0) -> None:
"""
Setting width and height of element scroll bar.
Usage:
self.element_scroll(css=".class", width=300, height=500)
"""
scroll_life = f'document.querySelector("{css}").scrollLeft = {width};'
scroll_top = f'document.querySelector("{css}").scrollTop = {height};'
self.execute_script(scroll_life)
self.execute_script(scroll_top)
def get_attribute(self, selector: str = None, attribute=None, index: int = 0, **kwargs) -> str:
"""
Gets the value of an element attribute.
Usage:
self.get_attribute(css="#el", attribute="type")
"""
if attribute is None:
raise ValueError("attribute is not None")
web_elem = WebElement(self.browser, selector=selector, **kwargs)
elem = web_elem.find(index, highlight=True)
log.info(f"✅ {web_elem.info} -> get attribute:{attribute}.")
return elem.get_attribute(attribute)
def get_text(self, selector: str = None, index: int = 0, **kwargs) -> str:
"""
Get element text information.
Usage:
self.get_text(css="#el")
"""
web_elem = WebElement(self.browser, selector=selector, **kwargs)
elem = web_elem.find(index, highlight=True)
log.info(f"✅ {web_elem.info} -> get text: {elem.text}.")
return elem.text
def get_display(self, selector: str = None, index: int = 0, **kwargs) -> bool:
"""
Gets the element to display,The return result is true or false.
Usage:
self.get_display(css="#el")
"""
web_elem = WebElement(self.browser, selector=selector, **kwargs)
elem = web_elem.find(index, highlight=True)
result = elem.is_displayed()
log.info(f"✅ {web_elem.info} -> element is display: {result}.")
return result
@property
def get_title(self) -> str:
"""
Get window title.
Usage:
self.get_title()
"""
log.info(f"✅ get title: {self.browser.title}.")
return self.browser.title
@property
def get_url(self) -> str:
"""
Get the URL address of the current page.
Usage:
self.get_url()
"""
log.info(f"✅ get current url: {self.browser.current_url}.")
return self.browser.current_url
@property
def get_alert_text(self) -> str:
"""
Gets the text of the Alert.
Usage:
self.get_alert_text()
"""
warnings.warn("use self.alert.text instead", DeprecationWarning, stacklevel=2)
log.info(f"✅ alert text: {self.browser.switch_to.alert.text}.")
return self.browser.switch_to.alert.text
def wait(self, secs: int = 10) -> None:
"""
Implicitly wait.All elements on the page.
Usage:
self.wait(10)
"""
log.info(f"⌛️ implicitly wait: {secs}s.")
self.browser.implicitly_wait(secs)
def is_visible(self, timeout: float = 5, **kwargs) -> bool:
"""
Determine if the element is visible
:param timeout:
:param kwargs:
:return:
"""
log.info("✅ element is visible.")
key, value = next(iter(kwargs.items()))
locator = (LOCATOR_LIST[key], value)
try:
WebDriverWait(driver=self.browser, timeout=timeout).until(
EC.visibility_of_element_located(locator)
)
return True
except TimeoutException:
return False
def accept_alert(self) -> None:
"""
Accept warning box.
Usage:
self.accept_alert()
"""
warnings.warn("use self.alert.accept() instead", DeprecationWarning, stacklevel=2)
log.info("✅ accept alert.")
self.browser.switch_to.alert.accept()
def dismiss_alert(self) -> None:
"""
Dismisses the alert available.
Usage:
self.dismiss_alert()
"""
warnings.warn("use self.alert.dismiss() instead", DeprecationWarning, stacklevel=2)
log.info("✅ dismiss alert.")
self.browser.switch_to.alert.dismiss()
def switch_to_frame(self, selector: str = None, index: int = 0, **kwargs) -> None:
"""
Switch to the specified frame.
Usage:
self.switch_to_frame(css="#el")
"""
web_elem = WebElement(self.browser, selector=selector, **kwargs)
elem = web_elem.find(index, highlight=True)
log.info(f"✅ {web_elem.info} -> switch to frame.")
self.browser.switch_to.frame(elem)
def switch_to_frame_parent(self) -> None:
"""
Switches focus to the parent context. If the current context is the top
level browsing context, the context remains unchanged.
Usage:
self.switch_to_frame_parent()
"""
log.info("✅ switch to parent frame.")
self.browser.switch_to.parent_frame()
def switch_to_frame_out(self) -> None:
"""
Returns the current form machine form at the next higher level.
Corresponding relationship with switch_to_frame () method.
Usage:
self.switch_to_frame_out()
"""
log.info("✅ switch to frame out.")
self.browser.switch_to.default_content()
def switch_to_window(self, window: int) -> None:
"""
Switches focus to the specified window.
:Args:
- window: window index. 1 represents a newly opened window (0 is the first one)
:Usage:
self.switch_to_window(1)
"""
log.info(f"✅ switch to the {window} window.")
all_handles = self.browser.window_handles
self.browser.switch_to.window(all_handles[window])
def switch_to_new_window(self, type_hint=None) -> None:
"""
Switches to a new top-level browsing context.
The type hint can be one of "tab" or "window". If not specified the
browser will automatically select it.
:Usage:
self.switch_to_new_window('tab')
"""
log.info("✅ switch to new window.")
self.browser.switch_to.new_window(type_hint=type_hint)
def save_screenshot(self, file_path: str = None, selector: str = None, index: int = 0, **kwargs) -> None:
"""
Saves a screenshots of the current window to a PNG image file.
Usage:
self.save_screenshot()
self.save_screenshot('/Screenshots/foo.png')
self.save_screenshot(id_="bLogo", index=0)
"""
if file_path is None:
img_dir = os.path.join(os.getcwd(), "reports", "images")
if os.path.exists(img_dir) is False:
os.mkdir(img_dir)
file_path = os.path.join(img_dir, get_timestamp() + ".png")
if len(kwargs) == 0:
log.info(f"📷️ screenshot -> ({file_path}).")
self.browser.save_screenshot(file_path)
else:
log.info(f"📷️ element screenshot -> ({file_path}).")
web_elem = WebElement(self.browser, selector=selector, **kwargs)
elem = web_elem.find(index)
elem.screenshot(file_path)
def screenshots(self, image=None) -> None:
"""
Saves a screenshots of the current window to HTML report.
Usage:
self.screenshots()
"""
if image is not None:
log.info("📷️ screenshot -> HTML report.")
self.images.append(base64.b64encode(image).decode())
return None
if Seldom.debug is True:
img_dir = os.path.join(os.getcwd(), "reports", "images")
if os.path.exists(img_dir) is False:
os.mkdir(img_dir)
file_path = os.path.join(img_dir, get_timestamp() + ".png")
log.info(f"📷️ screenshot -> ({file_path}).")
self.browser.save_screenshot(file_path)
else:
log.info("📷️ screenshot -> HTML report.")
self.images.append(self.browser.get_screenshot_as_base64())
def element_screenshot(self, selector: str = None, index: int = 0, **kwargs) -> None:
"""
Saves an element screenshot of the element to HTML report.
Usage:
self.element_screenshot(css="#id")
self.element_screenshot(css="#id", index=0)
"""
web_elem = WebElement(self.browser, selector=selector, **kwargs)
elem = web_elem.find(index)
if Seldom.debug is True:
img_dir = os.path.join(os.getcwd(), "reports", "images")
if os.path.exists(img_dir) is False:
os.mkdir(img_dir)
file_path = os.path.join(img_dir, get_timestamp() + ".png")
log.info(f"📷️ element screenshot -> ({file_path}).")
elem.screenshot(file_path)
else:
log.info("📷️ element screenshot -> HTML Report.")
self.images.append(elem.screenshot_as_base64)
def select(self, selector: str = None, value: str = None, text: str = None, index: int = None, **kwargs) -> None:
"""
Constructor. A check is made that the given element is, indeed, a SELECT tag. If it is not,
then an UnexpectedTagNameException is thrown.
:Args:
- css - element SELECT element to wrap
- value - The value to match against
Usage:
每页显示10条
每页显示20条
每页显示50条
self.select(css="#nr", value='20')
self.select(css="#nr", text='每页显示20条')
self.select(css="#nr", index=2)
"""
web_elem = WebElement(self.browser, selector=selector, **kwargs)
elem = web_elem.find(0, highlight=True)
log.info(f"✅ {web_elem.info} -> select option.")
if value is not None:
Select(elem).select_by_value(value)
elif text is not None:
Select(elem).select_by_visible_text(text)
elif index is not None:
Select(elem).select_by_index(index)
else:
raise ValueError(
'"value" or "text" or "index" options can not be all empty.')
def get_cookies(self) -> list:
"""
Returns a set of dictionaries, corresponding to cookies visible in the current session.
Usage:
self.get_cookies()
"""
return self.browser.get_cookies()
def get_cookie(self, name: str) -> dict:
"""
Returns information of cookie with ``name`` as an object.
Usage:
self.get_cookie("name")
"""
return self.browser.get_cookie(name)
def add_cookie(self, cookie_dict: dict) -> None:
"""
Adds a cookie to your current session.
Usage:
self.add_cookie({'name' : 'foo', 'value' : 'bar'})
"""
if isinstance(cookie_dict, dict):
self.browser.add_cookie(cookie_dict)
else:
raise TypeError("Wrong cookie type.")
def add_cookies(self, cookie_list: list) -> None:
"""
Adds a cookie to your current session.
Usage:
cookie_list = [
{'name' : 'foo', 'value' : 'bar'},
{'name' : 'foo', 'value' : 'bar'}
]
self.add_cookies(cookie_list)
"""
if isinstance(cookie_list, list):
for cookie in cookie_list:
if isinstance(cookie, dict):
self.browser.add_cookie(cookie)
else:
raise TypeError("Wrong cookie type.")
else:
raise TypeError("Wrong cookie type.")
def delete_cookie(self, name: str) -> None:
"""
Deletes a single cookie with the given name.
Usage:
self.delete_cookie('my_cookie')
"""
self.browser.delete_cookie(name)
def delete_all_cookies(self) -> None:
"""
Delete all cookies in the scope of the session.
Usage:
self.delete_all_cookies()
"""
self.browser.delete_all_cookies()
def check_element(self, css: str = None) -> None:
"""
Check that the element exists
Usage:
self.check_element(css="#el")
"""
if css is None:
raise NameError("Please enter a CSS selector")
log.info("👀 check element.")
js = f'return document.querySelectorAll("{css}")'
ret = self.browser.execute_script(js)
if len(ret) > 0:
for i in range(len(ret)):
js = f'return document.querySelectorAll("{css}")[{i}].outerHTML;'
ret = self.browser.execute_script(js)
log.info(f"{i} -> {ret}")
else:
log.warning("No elements were found.")
def get_elements(self, selector: str = None, **kwargs):
"""
Get a set of elements
Usage:
ret = self.get_elements(css="#el")
print(len(ret))
"""
web_elem = WebElement(self.browser, selector=selector, **kwargs)
elems = web_elem.find(empty=True)
if len(elems) == 0:
log.warning(f"{web_elem.warn}.")
else:
log.info(f"✅ {web_elem.info}.")
return elems
def get_element(self, selector: str = None, index: int = 0, **kwargs):
"""
Get a set of elements
Usage:
elem = self.get_element(index=1, css="#el")
elem.click()
"""
web_elem = WebElement(self.browser, selector=selector, **kwargs)
elem = web_elem.find(index)
log.info(f"✅ {web_elem.info}.")
return elem
def switch_to_app(self) -> None:
"""
appium API
Switch to native app.
"""
log.info("🔀 switch to native app.")
current_context = self.browser.current_context
if current_context != "NATIVE_APP":
self.browser.switch_to.context('NATIVE_APP')
def switch_to_web(self, context=None) -> None:
"""
appium API
Switch to web view.
"""
log.info("🔀 switch to webview.")
current_context = self.browser.current_context
if context is not None:
self.browser.switch_to.context(context)
elif "WEBVIEW" in current_context:
return
else:
all_context = self.browser.contexts
for context in all_context:
if "WEBVIEW" in context:
self.browser.switch_to.context(context)
break
else:
raise NameError("No WebView found.")
def switch_to_flutter(self) -> None:
"""
appium API
Switch to flutter app.
"""
log.info("🔀 switch to flutter.")
current_context = self.browser.current_context
if current_context != "NATIVE_APP":
self.browser.switch_to.context('FLUTTER')
================================================
FILE: seldom/webdriver_chaining.py
================================================
"""
WebDriver chaining API
"""
import os
import random
import time
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.select import Select
from seldom.driver import Browser
from seldom.logging import log
from seldom.logging.exceptions import RunningError
from seldom.running.config import Seldom, BrowserConfig
from seldom.testdata import get_timestamp
from seldom.webcommon.find_elems import WebElement
__all__ = ["Steps"]
class Steps:
"""
WebDriver Basic method chaining
Write test cases quickly.
"""
def __init__(self, browser=None, url: str = None, desc: str = None, images: list = []):
if browser is not None:
self.browser = Browser(browser, BrowserConfig.executable_path, BrowserConfig.options,
BrowserConfig.command_executor)
Seldom.driver = self.browser
else:
self.browser = Seldom.driver
self.url = url
self.elem = None
self.alert_obj = None
self.desc = desc
log.info(f"🔖 Test Case: {self.desc}")
self.images = images
def open(self, url: str = None):
"""
open url.
Usage:
open("https://www.baidu.com")
"""
if self.url is not None:
url = self.url
log.info(f"📖 {url}")
try:
self.browser.get(url)
except AttributeError:
raise RunningError(
"Muggle! Seldom running on Pycharm is not supported. You go See See: https://seldomqa.github.io/getting-started/quick_start.html")
return self
def max_window(self):
"""
Set browser window maximized.
Usage:
max_window()
"""
self.browser.maximize_window()
return self
def set_window(self, wide: int = 0, high: int = 0):
"""
Set browser window wide and high.
Usage:
.set_window(wide,high)
"""
self.browser.set_window_size(wide, high)
return self
def find(self, selector: str, index: int = 0):
"""
find element
"""
web_elem = WebElement(self.browser, selector=selector)
self.elem = web_elem.find(index, highlight=True)
log.info(f"🔍 {web_elem.info}.")
return self
def find_text(self, text: str, index: int = 0):
"""
find link text
Usage:
find_text("新闻")
"""
web_elem = WebElement(self.browser, link_text=text)
self.elem = web_elem.find(index, highlight=True)
log.info(f"🔍 find {web_elem.info} text.")
return self
def type(self, text):
"""
type text.
"""
log.info(f"✅ input '{text}'.")
self.elem.send_keys(text)
return self
def click(self):
"""
click.
"""
log.info("✅ click.")
self.elem.click()
return self
def clear(self):
"""
clear input.
Usage:
clear()
"""
log.info("✅ clear.")
self.elem.clear()
return self
def submit(self):
"""
submit input
Usage:
submit()
"""
log.info("✅ submit.")
self.elem.submit()
return self
def enter(self):
"""
enter.
Usage:
enter()
"""
log.info("✅ enter.")
self.elem.send_keys(Keys.ENTER)
return self
def move_to_click(self):
"""
Moving the mouse to the middle of an element. and click element.
Usage:
move_to_click()
"""
log.info("✅ Move to the element and click.")
ActionChains(self.browser).move_to_element(self.elem).click(self.elem).perform()
return self
def right_click(self):
"""
Right click element.
Usage:
right_click()
"""
log.info("✅ right click.")
ActionChains(self.browser).context_click(self.elem).perform()
return self
def move_to_element(self):
"""
Mouse over the element.
Usage:
move_to_element()
"""
log.info("✅ move to element.")
ActionChains(self.browser).move_to_element(self.elem).perform()
return self
def click_and_hold(self):
"""
Mouse over the element.
Usage:
move_to_element()
"""
log.info("✅ click and hold.")
ActionChains(self.browser).click_and_hold(self.elem).perform()
return self
def double_click(self):
"""
Double click element.
Usage:
double_click()
"""
log.info("✅ double click.")
ActionChains(self.browser).double_click(self.elem).perform()
return self
def close(self):
"""
Closes the current window.
Usage:
close()
"""
self.browser.close()
return self
def quit(self):
"""
Quit the driver and close all the windows.
Usage:
quit()
"""
self.browser.quit()
return self
def refresh(self):
"""
Refresh the current page.
Usage:
refresh()
"""
log.info("🔄️ refresh page.")
self.browser.refresh()
return self
def alert(self):
"""
get alert.
Usage:
alert()
"""
log.info("🔍 alert.")
self.alert_obj = self.browser.switch_to.alert
return self
def accept(self):
"""
Accept warning box.
Usage:
alert().accept()
"""
log.info("✅ accept alert.")
self.alert_obj.accept()
return self
def dismiss(self):
"""
Dismisses the alert available.
Usage:
alert().dismiss()
"""
log.info("✅ dismiss alert.")
self.browser.switch_to.alert.dismiss()
return self
def switch_to_frame(self):
"""
Switch to the specified frame.
Usage:
switch_to_frame()
"""
log.info("✅ switch to frame.")
self.browser.switch_to.frame(self.elem)
return self
def switch_to_frame_out(self):
"""
Returns the current form machine form at the next higher level.
Corresponding relationship with switch_to_frame () method.
Usage:
switch_to_frame_out()
"""
log.info("✅ switch to frame out.")
self.browser.switch_to.default_content()
return self
def switch_to_window(self, window: int):
"""
Switches focus to the specified window.
:Args:
- window: window index. 1 represents a newly opened window (0 is the first one)
:Usage:
switch_to_window(1)
"""
log.info(f"✅ switch to the {window} window.")
all_handles = self.browser.window_handles
self.browser.switch_to.window(all_handles[window])
return self
def screenshots(self, file_path: str = None):
"""
Saves a screenshots of the current window to a PNG image file.
Usage:
screenshots()
screenshots('/Screenshots/foo.png')
"""
if file_path is None:
img_dir = os.path.join(os.getcwd(), "reports", "images")
if os.path.exists(img_dir) is False:
os.mkdir(img_dir)
file_path = os.path.join(img_dir, get_timestamp() + ".png")
log.info(f"📷️ screenshot -> ({file_path}).")
self.browser.save_screenshot(file_path)
return self
def element_screenshot(self, file_path: str = None):
"""
Saves the element screenshot of the element to a PNG image file.
Usage:
element_screenshot()
element_screenshot(file_path='/Screenshots/foo.png')
"""
if file_path is None:
img_dir = os.path.join(os.getcwd(), "reports", "images")
if os.path.exists(img_dir) is False:
os.mkdir(img_dir)
file_path = os.path.join(img_dir, get_timestamp() + ".png")
log.info(f"📷️ element screenshot -> ({file_path}).")
self.elem.screenshot(file_path)
return self
def select(self, value: str = None, text: str = None, index: int = None):
"""
Constructor. A check is made that the given element is, indeed, a SELECT tag. If it is not,
then an UnexpectedTagNameException is thrown.
:Args:
- css - element SELECT element to wrap
- value - The value to match against
Usage:
每页显示10条
每页显示20条
每页显示50条
select(value='20')
select(text='每页显示20条')
select(index=2)
"""
log.info("✅ select option.")
if value is not None:
Select(self.elem).select_by_value(value)
elif text is not None:
Select(self.elem).select_by_visible_text(text)
elif index is not None:
Select(self.elem).select_by_index(index)
else:
raise ValueError(
'"value" or "text" or "index" options can not be all empty.')
return self
def sleep(self, sec: [int, tuple] = 1):
"""
Usage:
self.sleep(seconds)
"""
if isinstance(sec, tuple):
sec = random.randint(sec[0], sec[1])
log.info(f"💤️ sleep: {sec}s.")
time.sleep(sec)
return self
================================================
FILE: seldom/websocket_client.py
================================================
from threading import Thread
import websocket
from seldom.logging import log
class WebSocketClient(Thread):
"""
WebSocket Client class
"""
def __init__(self, url):
Thread.__init__(self)
self.url = url
self.ws = None
self.running = False
self.received_messages = []
def run(self):
"""
Run WebSocket.
Returns:
"""
self.running = True
try:
self.ws = websocket.create_connection(self.url)
self.on_open()
except Exception as e:
self.on_error(e)
self.running = False
return
while self.running:
try:
message = self.ws.recv()
if message is None:
self.on_close()
break
self.received_messages.append(message)
except websocket.WebSocketConnectionClosedException as e:
log.error(e)
self.on_close()
break
except Exception as e:
self.on_error(e)
break
def send_message(self, message):
"""
send message.
:param message:
:return:
"""
try:
if self.ws:
self.ws.send(message)
except Exception as e:
self.on_error(e)
def stop(self):
"""
stop and close WebSocket.
"""
self.running = False
if self.ws:
self.ws.close()
@staticmethod
def on_open():
"""
Open WebSocket connection.
:return:
"""
log.info("WebSocket connection opened.")
@staticmethod
def on_error(error):
"""
WebSocket error info.
:param error:
:return:
"""
log.error(f"WebSocket error: {error}")
def on_close(self):
"""
Close WebSocket connection.
:return:
"""
log.info("WebSocket connection closed.")
self.running = False
================================================
FILE: tests/data/country.graphql
================================================
query GetCountry($code: ID!) {
country(code: $code) {
name
capital
currency
languages {
name
}
}
}
================================================
FILE: tests/data/hello.txt
================================================
hello world!
================================================
FILE: tests/test_adb.py
================================================
import time
from seldom.utils.adbutils import ADBUtils
import unittest
class ADBUtilsTest(unittest.TestCase):
def setUp(self):
self.adb = ADBUtils()
def test_devices(self):
# 自动刷新并获取所有设备
devices = self.adb.refresh_devices()
print("当前连接设备:", devices)
# 设置默认设备
if devices:
self.adb.set_default_device(devices[0][0])
def test_app_lunch_and_close(self):
# 应用操作
package = "com.microsoft.bing"
if self.adb.launch_app(package):
print(f"成功启动 {package}")
time.sleep(5)
if self.adb.close_app(package):
print(f"成功关闭 {package}")
def test_app_info(self):
# 获取信息
app_info = self.adb.get_app_info()
for info in app_info:
print(info['package'], info["activity"])
if __name__ == '__main__':
unittest.main()
================================================
FILE: tests/test_api_object.py
================================================
import seldom
from seldom.request import api
from seldom.request import HttpRequest
class LoginApiObject(HttpRequest):
"""
Login API objects
"""
@api(
describe="获取登录用户名",
status_code=200,
ret="headers.Account",
check={"headers.Host": "httpbin.org"},
debug=True
)
def get_login_user(self):
"""
调用接口获得用户名
"""
headers = {"Account": "bugmaster"}
r = self.get(f"{self.base_url}/get", headers=headers)
return r
class LoginTest(seldom.TestCase):
def test_user_login(self):
"""test user login case"""
lao = LoginApiObject()
username = lao.get_login_user()
self.assertEqual(username, "bugmaster")
if __name__ == '__main__':
seldom.main(base_url="https://httpbin.org", debug=True)
================================================
FILE: tests/test_autowing.py
================================================
"""
> pip install autowing
"""
import seldom
from seldom import Seldom
from autowing.selenium.fixture import create_fixture
from dotenv import load_dotenv
class TestBingSearch(seldom.TestCase):
@classmethod
def start_class(cls):
# load .env file
load_dotenv()
# Create AI fixture
ai_fixture = create_fixture()
cls.ai = ai_fixture(Seldom.driver)
def test_bing_search(self):
"""
Test Bing search functionality using AI-driven automation.
"""
self.open("https://cn.bing.com")
self.ai.ai_action('搜索输入框输入"playwright"关键字,并回车')
self.sleep(3)
items = self.ai.ai_query('string[], 搜索结果列表中包含"playwright"相关的标题')
self.assertGreater(len(items), 1)
self.assertTrue(
self.ai.ai_assert('检查搜索结果列表第一条标题是否包含"playwright"字符串')
)
if __name__ == '__main__':
seldom.main(browser="edge", debug=True)
================================================
FILE: tests/test_base_assert.py
================================================
import seldom
class TestAssertions(seldom.TestCase):
def test_assertEqual(self):
self.assertEqual(10, 10) # 正确
with self.assertRaises(AssertionError):
self.assertEqual(10, 20) # 错误
self.assertNotEqual(10, 20) # 正确
with self.assertRaises(AssertionError):
self.assertNotEqual(10, 10) # 错误
def test_assertTrue(self):
self.assertTrue(True) # 正确
with self.assertRaises(AssertionError):
self.assertTrue(False) # 错误
self.assertFalse(False) # 正确
with self.assertRaises(AssertionError):
self.assertFalse(True) # 错误
def test_assertIn(self):
self.assertIn(1, [1, 2, 3]) # 正确
with self.assertRaises(AssertionError):
self.assertIn(4, [1, 2, 3]) # 错误
self.assertNotIn(4, [1, 2, 3]) # 正确
with self.assertRaises(AssertionError):
self.assertNotIn(1, [1, 2, 3]) # 错误
def test_assertIsInstance(self):
self.assertIsInstance(10, int) # 正确
with self.assertRaises(AssertionError):
self.assertIsInstance(10, str) # 错误
self.assertNotIsInstance(10, str) # 正确
with self.assertRaises(AssertionError):
self.assertNotIsInstance(10, int) # 错误
def test_assertRegex(self):
self.assertRegex("hello world", r"hello") # 正确
with self.assertRaises(AssertionError):
self.assertRegex("hello world", r"goodbye") # 错误
self.assertNotRegex("hello world", r"goodbye") # 正确
with self.assertRaises(AssertionError):
self.assertNotRegex("hello world", r"hello") # 错误
def test_assertAlmostEqual(self):
self.assertAlmostEqual(1.0, 1.00001, places=4) # 正确
with self.assertRaises(AssertionError):
self.assertAlmostEqual(1.0, 1.1, places=4) # 错误
self.assertNotAlmostEqual(1.0, 1.1, places=4) # 正确
with self.assertRaises(AssertionError):
self.assertNotAlmostEqual(1.0, 1.00001, places=4) # 错误
def test_assertGreater(self):
self.assertGreater(10, 5) # 正确
with self.assertRaises(AssertionError):
self.assertGreater(5, 10) # 错误
self.assertGreaterEqual(10, 10) # 正确
with self.assertRaises(AssertionError):
self.assertGreaterEqual(5, 10) # 错误
def test_assertLess(self):
self.assertLess(5, 10) # 正确
with self.assertRaises(AssertionError):
self.assertLess(10, 5) # 错误
self.assertLessEqual(10, 10) # 正确
with self.assertRaises(AssertionError):
self.assertLessEqual(10, 5) # 错误
def test_assertCountEqual(self):
self.assertCountEqual([1, 2, 3], [3, 2, 1]) # 正确
with self.assertRaises(AssertionError):
self.assertCountEqual([1, 2, 3], [1, 2]) # 错误
if __name__ == "__main__":
seldom.main(debug=True)
================================================
FILE: tests/test_benchmark.py
================================================
import time
import seldom
from seldom.testdata import get_int
from seldom.utils.benchmark import benchmark_test
class MyTests(seldom.TestCase):
@benchmark_test()
def test_something_performance_1(self):
"""
something code performance
"""
num = get_int(1, 2000) / 1000
time.sleep(num)
@benchmark_test(rounds=10, iterations=2)
def test_something_performance_2(self):
"""
something code performance
"""
num = get_int(1, 2000) / 1000
time.sleep(num)
@benchmark_test(rounds=10)
def test_http_performance(self):
"""
test http benchmark
"""
self.get("https://httpbin.org/get")
self.assertStatusOk()
if __name__ == "__main__":
seldom.main(browser=True)
================================================
FILE: tests/test_browser.py
================================================
import seldom
class WebTestOne(seldom.TestCase):
"""case lunch browser"""
def start(self):
self.browser("edge")
def end(self):
self.quit()
def test_baidu(self):
self.open("http://www.baidu.com")
self.Keys(css="#chat-textarea").input("seldom").enter()
self.sleep(2)
self.screenshots()
self.assertInTitle("seldom")
def test_bing(self):
self.open("http://cn.bing.com")
self.type(id_="sb_form_q", text="seldom", enter=True)
self.sleep(2)
self.screenshots()
self.assertInTitle("seldom")
class WebTestTwo(seldom.TestCase):
"""class lunch browser"""
@classmethod
def start_class(cls):
cls.browser(cls, "edge")
@classmethod
def end_class(cls):
cls.quit(cls)
def test_baidu(self):
self.open("http://www.baidu.com")
self.Keys(css="#chat-textarea").input("seldom").enter()
self.sleep(2)
self.screenshots()
self.assertInTitle("seldom")
def test_bing(self):
self.open("http://cn.bing.com")
self.type(id_="sb_form_q", text="seldom", enter=True)
self.sleep(2)
self.screenshots()
self.assertInTitle("seldom")
if __name__ == '__main__':
seldom.main()
================================================
FILE: tests/test_browser_new.py
================================================
import seldom
class WebTestNew(seldom.TestCase):
"""Web search test case"""
def test_new_browser(self):
# default browser
self.open("http://www.baidu.com")
self.Keys(css="#chat-textarea").input("seldom").enter()
self.sleep(2)
self.screenshots()
self.assertInTitle("seldom")
# open new browser
browser = self.new_browser()
browser.open("http://cn.bing.com")
browser.type(id_="sb_form_q", text="seldom", enter=True)
self.sleep(2)
browser.screenshots()
if __name__ == '__main__':
seldom.main(browser="edge")
================================================
FILE: tests/test_cache/__init__.py
================================================
================================================
FILE: tests/test_cache/test_cache.py
================================================
"""
author: bugmaster
data: 2022/5/22
desc: 缓存用法
"""
from seldom.utils import cache
# 清除指定缓存
cache.clear()
# 获取指定缓存
token = cache.get("token")
print(f"token: {token}")
# 判断为空写入缓存
if token is None:
cache.set({"token": "123"})
# 设置存在的数据(相当于更新)
cache.set({"token": "456"})
# value复杂格式设置存在的数据
cache.set({"user": [{"name": "tom", "age": 11}]})
# 获取所有缓存
all_token = cache.get()
print(f"all: {all_token}")
# 清除指定缓存
cache.clear("token")
================================================
FILE: tests/test_cache/test_cache_thread.py
================================================
"""
author: bugmaster
data: 2024/8/27
desc: 多线程缓存用法
"""
from seldom.utils import cache
from seldom.extend_lib import threads
if __name__ == "__main__":
@threads(3)
def operating_token(tk: str):
"""
根据传入的token操作
"""
cache.clear("token")
# 获取指定缓存
token = cache.get("token")
# 判断为空写入缓存
if token is None:
cache.set({"token": tk})
# 将两条用例拆分,分别用不同的浏览器执行
token = ["t123", "t456", "t789"]
for t in token:
operating_token(t)
================================================
FILE: tests/test_cache/test_memory_cache.py
================================================
import seldom
import time
from seldom.logging import log
from seldom.utils import memory_cache
@memory_cache()
def add(x, y):
log.info(f"calculating: {x} + {y}")
time.sleep(2)
return x + y
class MyTest(seldom.TestCase):
def test_case(self):
"""test cache 1"""
r = add(1, 2)
self.assertEqual(r, 3)
def test_case2(self):
"""test cache 2"""
r = add(2, 3)
self.assertEqual(r, 5)
def test_case3(self):
"""test cache 3"""
r = add(1, 2)
self.assertEqual(r, 3)
def test_case4(self):
"""test cache 4"""
r = add(2, 3)
self.assertEqual(r, 5)
if __name__ == '__main__':
seldom.main(debug=False)
================================================
FILE: tests/test_db/__init__.py
================================================
================================================
FILE: tests/test_db/test_db_mssql.py
================================================
"""
author: bugmaster
data: 2022/10/01
desc: 数据库操作
Please install the library. https://github.com/pymssql/pymssql
"""
import unittest
from seldom.db_operation.mssql_db import MSSQLDB
class MSSQLTest(unittest.TestCase):
"""测试操作MS SQL Server数据库API"""
def setUp(self) -> None:
""""初始化DB连接"""
self.db = MSSQLDB(server="127.0.0.1", user="SA", password="tc@123", database="TestDB")
self.db.execute_sql("INSERT INTO users (email, password) VALUES ('test@gmail.com', 'test123') ")
def tearDown(self) -> None:
self.db.delete("users", {"email": "test@gmail.com"})
def test_query_sql(self):
"""测试查询SQL"""
result = self.db.query_sql("select * from users")
self.assertIsInstance(result, list)
def test_query_one(self):
"""测试查询SQL一条数据"""
result = self.db.query_one("select * from users")
self.assertIsInstance(result, tuple)
def test_execute_sql(self):
"""测试执行SQL"""
db = self.db
db.execute_sql("INSERT INTO users (email, password) VALUES ('tom@gmail.com', 'tom22') ")
db.execute_sql("UPDATE users SET password='tom33' WHERE email='tom@gmail.com'")
db.execute_sql("DELETE FROM users WHERE email = 'tom@gmail.com' ")
result = db.query_sql("select * from users WHERE email='tom@gmail.com'")
self.assertEqual(len(result), 0)
def test_select_sql(self):
"""测试查询SQL"""
result1 = self.db.select(table="users", where={"email": "test@gmail.com"})
self.assertEqual(result1[0][1], "test@gmail.com")
result2 = self.db.select(table="users", one=True)
self.assertIsInstance(result2, tuple)
def test_delete_sql(self):
"""测试删除SQL"""
self.db.delete(table="users", where={"email": "test@gmail.com"})
result = self.db.query_sql("select * from users WHERE email='test@gmail.com'")
self.assertEqual(len(result), 0)
def test_update_sql(self):
"""测试更新SQL"""
self.db.update(table="users", where={"email": "test@gmail.com", }, data={"password": "test22"})
result = self.db.query_sql("select * from users WHERE email='test@gmail.com'")
self.assertEqual(result[0][2], 'test22')
def test_insert_sql(self):
"""测试插入SQL"""
data = {"email": "jean@gmail.com", "password": "jean11"}
self.db.insert(table="users", data=data)
result = self.db.query_sql("select * from users WHERE email='jean@gmail.com'")
self.assertTrue(len(result[0]) > 1)
def test_init_table(self):
"""测试批量插入数据"""
# more table data
table_data = {
"users": [
{"email": "jeannie@gmail.com", "password": "jeannie25"},
{"email": "joye@gmail.com", "password": "joye26"},
{"email": "blue@gmail.com", "password": "blue27"},
],
}
self.db.init_table(table_data)
if __name__ == "__main__":
unittest.main()
================================================
FILE: tests/test_db/test_db_mysql.py
================================================
"""
author: bugmaster
data: 2022/10/01
desc: 数据库操作
"""
import unittest
from seldom.db_operation import MySQLDB
class MySQLTest(unittest.TestCase):
"""测试操作MySQL数据库API"""
def setUp(self) -> None:
""""初始化DB连接"""
self.db = MySQLDB(host="localhost", port=3306, user="root", password="198876", database="guest3")
self.db.execute_sql("INSERT INTO api_user (name, age) VALUES ('test', 11) ")
def tearDown(self) -> None:
self.db.delete("api_user", {"name": "test"})
def test_query_sql(self):
"""测试查询SQL"""
result = self.db.query_sql("select * from api_user")
self.assertIsInstance(result, list)
def test_query_one(self):
"""测试查询SQL一条数据"""
result = self.db.query_one("select * from api_user")
self.assertIsInstance(result, dict)
def test_execute_sql(self):
"""测试执行SQL"""
db = self.db
db.execute_sql("INSERT INTO api_user (name, age) VALUES ('tom', 22) ")
db.execute_sql("UPDATE api_user SET age=23 WHERE name='tom'")
db.execute_sql("DELETE FROM api_user WHERE name = 'tom' ")
result = db.query_sql("select * from api_user WHERE name='tom'")
self.assertEqual(len(result), 0)
def test_select_sql(self):
"""测试查询SQL"""
result1 = self.db.select(table="api_user", where={"name": "test"})
self.assertEqual(result1[0]["name"], "test")
result2 = self.db.select(table="api_user", one=True)
self.assertIsInstance(result2, dict)
def test_delete_sql(self):
"""测试删除SQL"""
# delete sql
self.db.delete(table="api_user", where={"name": "test"})
result = self.db.query_sql("select * from api_user WHERE name='test'")
self.assertEqual(len(result), 0)
def test_update_sql(self):
"""测试更新SQL"""
self.db.update(table="api_user", where={"name": "test", }, data={"age": "22"})
result = self.db.query_sql("select * from api_user WHERE name='test'")
self.assertEqual(result[0]["age"], 22)
def test_insert_sql(self):
"""测试插入SQL"""
data = {"name": "jean", "age": 11}
self.db.insert(table="api_user", data=data)
result = self.db.query_sql("select * from api_user WHERE name='jean'")
self.assertTrue(len(result[0]) > 1)
def test_init_table(self):
"""测试批量插入数据"""
# more table data
table_data = {
"api_group": [
{"name": "test"},
{"name": "product"},
{"name": "develop"},
],
"api_user": [
{"name": "jeannie", "age": 25},
{"name": "joye", "age": 26},
{"name": "blue", "age": 27},
],
}
self.db.init_table(table_data)
if __name__ == "__main__":
unittest.main()
================================================
FILE: tests/test_db/test_db_postgresdb.py
================================================
"""
author: bugmaster
data: 2022/10/01
desc: 数据库操作
Please install the library. https://github.com/psycopg/psycopg2
"""
import unittest
from seldom.db_operation.postgres_db import PostgresDB
class PostgresDBTest(unittest.TestCase):
def setUp(self) -> None:
""""初始化DB连接"""
self.db = PostgresDB(host="localhost", port=3306, user="dev", password="808801", database="db_user")
sql = """INSERT INTO
public.cusm_account (id, name, cn_name, mobile_phone_region, mobile_phone, email, password, status)
VALUES (DEFAULT, 'test', 'ces', '+86', '13122221111', null, '123456Aq!', 1) """
self.db.execute_sql(sql)
def tearDown(self) -> None:
self.db.delete("public.cusm_account", {"name": "test"})
def test_query_sql(self):
"""测试查询SQL"""
result = self.db.query_sql("select * from public.cusm_account")
self.assertIsInstance(result, list)
def test_query_one(self):
"""测试查询SQL一条数据"""
result = self.db.query_one("select * from public.cusm_account")
self.assertIsInstance(result, dict)
def test_execute_sql(self):
"""测试执行SQL"""
db = self.db
db.execute_sql("INSERT INTO public.cusm_account (name, cn_name) VALUES ('tom', 22) ")
db.execute_sql("UPDATE public.cusm_account SET cn_name=23 WHERE name='tom'")
db.execute_sql("DELETE FROM public.cusm_account WHERE name = 'tom' ")
result = db.query_sql("select * from public.cusm_account WHERE name='tom'")
self.assertEqual(len(result), 0)
def test_select_sql(self):
"""测试查询SQL"""
result1 = self.db.select(table="public.cusm_account", where={"name": "test"})
self.assertEqual(result1[0]["name"], "test")
result2 = self.db.select(table="public.cusm_account", one=True)
self.assertIsInstance(result2, list)
def test_delete_sql(self):
"""测试删除SQL"""
# delete sql
self.db.delete(table="public.cusm_account", where={"name": "test"})
result = self.db.query_sql("select * from public.cusm_account WHERE name='test'")
self.assertEqual(len(result), 0)
def test_update_sql(self):
"""测试更新SQL"""
self.db.update(table="public.cusm_account", where={"name": "test", }, data={"cn_name": "22"})
result = self.db.query_sql("select * from public.cusm_account WHERE name='test'")
self.assertEqual(result[0]["cn_name"], 22)
def test_insert_sql(self):
"""测试插入SQL"""
data = {"name": "jean", "cn_name": 11}
self.db.insert(table="public.cusm_account", data=data)
result = self.db.query_sql("select * from public.cusm_account WHERE name='jean'")
self.assertTrue(len(result[0]) > 1)
if __name__ == "__main__":
unittest.main()
================================================
FILE: tests/test_db/test_db_sqlite3.py
================================================
"""
author: bugmaster
data: 2022/10/01
desc: 数据库操作
"""
import unittest
from seldom.db_operation import SQLiteDB
from seldom.utils import file
class SQLite3Test(unittest.TestCase):
"""测试SQLite数据库API"""
def setUp(self) -> None:
""""初始化DB连接"""
db_path = file.join(file.dir_dir, "data", "db.sqlite3")
self.db = SQLiteDB(db_path)
self.db.insert(table="api_user", data={"name": "test", "age": 11})
def tearDown(self) -> None:
self.db.delete("api_user", {"name": "test"})
def test_query_sql(self):
"""测试查询SQL"""
result = self.db.query_sql("select * from api_user")
self.assertIsInstance(result, list)
def test_query_one(self):
"""测试查询SQL"""
result = self.db.query_one("select * from api_user")
self.assertIsInstance(result, tuple)
def test_execute_sql(self):
"""测试执行SQL"""
db = self.db
db.execute_sql("INSERT INTO api_user (name, age) VALUES ('tom', 22) ")
db.execute_sql("UPDATE api_user SET age=23 WHERE name='tom'")
db.execute_sql("DELETE FROM api_user WHERE name = 'tom' ")
result = db.query_sql("select * from api_user WHERE name='tom'")
self.assertEqual(len(result), 0)
def test_select_sql(self):
"""测试查询SQL"""
result = self.db.select(table="api_user", where={"name": "test"})
self.assertEqual(result[0][1], "test")
result = self.db.select(table="api_user", one=True)
self.assertIsInstance(result, tuple)
def test_delete_sql(self):
"""测试删除SQL"""
# delete sql
self.db.delete(table="api_user", where={"name": "test"})
result = self.db.query_sql("select * from api_user WHERE name='test'")
self.assertEqual(len(result), 0)
def test_update_sql(self):
"""测试更新SQL"""
self.db.update(table="api_user", where={"name": "test", }, data={"age": "22"})
result = self.db.query_sql("select * from api_user WHERE name='test'")
self.assertEqual(result[0][2], 22)
def test_insert_sql(self):
"""测试插入SQL"""
data = {"name": "jean", "age": 11}
self.db.insert(table="api_user", data=data)
result = self.db.query_sql("select * from api_user WHERE name='jean'")
self.assertTrue(len(result[0]) > 1)
def test_init_table(self):
"""测试批量插入数据"""
# more table data
table_data = {
"api_user": [ # 表名
{"name": "jeannie", "age": 25},
{"name": "joye", "age": 26},
{"name": "blue", "age": 27},
],
}
self.db.init_table(table_data)
if __name__ == "__main__":
unittest.main()
================================================
FILE: tests/test_dependent_func.py
================================================
import seldom
from seldom.testdata import get_md5
from seldom.utils import cache, dependent_func
class DependentTest(seldom.TestCase):
@staticmethod
def user_login(username, password):
"""
模拟用户登录,获取登录token
"""
return get_md5(username + password)
@dependent_func(user_login, username="tom", password="t123")
def test_case(self):
"""
sample test case
"""
token = cache.get("user_login")
print("token", token)
if __name__ == '__main__':
seldom.main()
cache.clear()
================================================
FILE: tests/test_encrypt.py
================================================
import unittest
# 导入待测试的模块
from seldom.utils.encrypt import (
CipherMode,
HashUtil,
AESUtil,
DESUtil,
TripleDESUtil,
RSAUtil,
EncodeUtil,
encrypt_util
)
class TestHashUtil(unittest.TestCase):
"""测试 HashUtil 类"""
def test_md5(self):
text = "hello world"
expected = "5eb63bbbe01eeed093cb22bb8f5acdc3"
self.assertEqual(HashUtil.md5(text), expected)
def test_sha1(self):
text = "hello world"
expected = "2aae6c35c94fcfb415dbe95f408b9ce91ee846ed"
self.assertEqual(HashUtil.sha1(text), expected)
def test_sha256(self):
text = "hello world"
expected = "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"
self.assertEqual(HashUtil.sha256(text), expected)
def test_sha512(self):
text = "hello world"
expected = "309ecc489c12d6eb4cc40f50c902f2b4d0ed77ee511a7c7a9bcd3ca86d4cd86f" \
"989dd35bc5ff499670da34255b45b0cfd830e81f605dcf7dc5542e93ae9cd76f"
self.assertEqual(HashUtil.sha512(text), expected)
def test_hmac_sha256(self):
key = "secret"
text = "hello world"
expected = "734cc62f32841568f45715aeb9f4d7891324e6d948e4c6c60c0621cdac48623a"
self.assertEqual(HashUtil.hmac_sha256(key, text), expected)
class TestAESUtil(unittest.TestCase):
"""测试 AESUtil 类"""
def test_encrypt_decrypt_cbc(self):
key = "mysecretkey"
text = "hello world"
encrypted = AESUtil.encrypt(key, text, mode=CipherMode.CBC)
decrypted = AESUtil.decrypt(key, encrypted, mode=CipherMode.CBC)
self.assertEqual(decrypted, text)
def test_encrypt_decrypt_ecb(self):
key = "mysecretkey"
text = "hello world"
encrypted = AESUtil.encrypt(key, text, mode=CipherMode.ECB)
decrypted = AESUtil.decrypt(key, encrypted, mode=CipherMode.ECB)
self.assertEqual(decrypted, text)
class TestDESUtil(unittest.TestCase):
"""测试 DESUtil 类"""
def test_encrypt_decrypt_cbc(self):
key = "mysecretkey"
text = "hello world"
encrypted = DESUtil.encrypt(key, text, mode=CipherMode.CBC)
decrypted = DESUtil.decrypt(key, encrypted, mode=CipherMode.CBC)
self.assertEqual(decrypted, text)
def test_encrypt_decrypt_ecb(self):
key = "mysecretkey"
text = "hello world"
encrypted = DESUtil.encrypt(key, text, mode=CipherMode.ECB)
decrypted = DESUtil.decrypt(key, encrypted, mode=CipherMode.ECB)
self.assertEqual(decrypted, text)
class TestTripleDESUtil(unittest.TestCase):
"""测试 TripleDESUtil 类"""
def test_encrypt_decrypt_cbc(self):
key = "mysecretkey123456789012"
text = "hello world"
encrypted = TripleDESUtil.encrypt(key, text, mode=CipherMode.CBC)
decrypted = TripleDESUtil.decrypt(key, encrypted, mode=CipherMode.CBC)
self.assertEqual(decrypted, text)
def test_encrypt_decrypt_ecb(self):
key = "mysecretkey123456789012"
text = "hello world"
encrypted = TripleDESUtil.encrypt(key, text, mode=CipherMode.ECB)
decrypted = TripleDESUtil.decrypt(key, encrypted, mode=CipherMode.ECB)
self.assertEqual(decrypted, text)
class TestRSAUtil(unittest.TestCase):
"""测试 RSAUtil 类"""
def setUp(self):
self.public_key, self.private_key = RSAUtil.generate_key_pair()
def test_encrypt_decrypt(self):
rsa = RSAUtil(self.public_key, self.private_key)
text = "hello world"
encrypted = rsa.encrypt(text)
decrypted = rsa.decrypt(encrypted)
self.assertEqual(decrypted, text)
class TestEncodeUtil(unittest.TestCase):
"""测试 EncodeUtil 类"""
def test_base64_encode_decode(self):
text = "hello world"
encoded = EncodeUtil.base64_encode(text)
decoded = EncodeUtil.base64_decode(encoded)
self.assertEqual(decoded, text)
def test_url_encode_decode(self):
text = "hello world"
encoded = EncodeUtil.url_encode(text)
decoded = EncodeUtil.url_decode(encoded)
self.assertEqual(decoded, text)
def test_html_encode_decode(self):
text = "hello world"
encoded = EncodeUtil.html_encode(text)
decoded = EncodeUtil.html_decode(encoded)
self.assertEqual(decoded, text)
class TestEncryptUtil(unittest.TestCase):
"""测试 EncryptUtil 类"""
def test_hash_util(self):
self.assertIsInstance(encrypt_util.hash, HashUtil)
def test_encode_util(self):
self.assertIsInstance(encrypt_util.encode, EncodeUtil)
def test_aes_util(self):
self.assertEqual(encrypt_util.aes(), AESUtil)
def test_des_util(self):
self.assertEqual(encrypt_util.des(), DESUtil)
def test_des3_util(self):
self.assertEqual(encrypt_util.des3(), TripleDESUtil)
def test_rsa_util(self):
rsa = encrypt_util.rsa()
self.assertIsInstance(rsa, RSAUtil)
if __name__ == "__main__":
unittest.main()
================================================
FILE: tests/test_fixture.py
================================================
import seldom
class TestCase(seldom.TestCase):
@staticmethod
def start_class(cls):
print("测试类开始执行")
@staticmethod
def end_class(cls):
print("测试类结束执行")
def start(self):
print("一条测试用例开始")
def end(self):
print("一条测试结果")
def test_case_one(self):
...
def test_case_two(self):
...
if __name__ == '__main__':
seldom.main(debug=True)
================================================
FILE: tests/test_graphql.py
================================================
import seldom
from seldom import TestCase
from seldom.utils.resource_loader import resource_file
class TestGraphQL(TestCase):
def test_graphql_query(self):
"""
查询中国(code: "CN")的名称、首都、货币和官方语言
"""
params = {
"query": resource_file("country.graphql"),
"variables": {
"code": "CN" # 可以改成 "US", "FR", "JP" 等
}
}
self.post("/", json=params)
self.assertStatusOk()
self.assertPath("data.country.name", "China")
if __name__ == '__main__':
seldom.main(base_url="https://countries.trevorblades.com", debug=True)
================================================
FILE: tests/test_http_assert.py
================================================
import seldom
from seldom.utils import genson
class TestHttpAssert(seldom.TestCase):
def test_assert_json(self):
"""
test assertJSON
"""
payload = {"name": "tom", "hobby": ["basketball", "swim"]}
self.get("/get", params=payload)
assert_data = {
"hobby": ["swim", "basketball"],
"name": "tom"
}
self.assertJSON(assert_data, self.response["args"])
def test_assert_path(self):
"""
test assertJSON
"""
payload = {'name': 'tom', 'hobby': ['basketball', 'swim']}
self.get("/get", params=payload)
self.assertPath("args.name", "tom")
self.assertPath("args.hobby[0]", "basketball")
self.assertInPath("args.hobby[0]", "ball")
def test_assert_schema(self):
"""
test assertJSON
"""
payload = {"hobby": ["basketball", "swim"], "name": "tom", "age": "18"}
self.get("/get", params=payload)
schema = genson(self.response["args"])
print("json Schema: \n", schema)
# 断言数据结构和类型
assert_data = {
"$schema": "http://json-schema.org/schema#",
"type": "object",
"properties": {
"age": {
"type": "string"
},
"hobby": {
"type": "array", "items": {"type": "string"}
},
"name": {
"type": "string"
}
},
"required":
["age", "hobby", "name"]
}
self.assertSchema(assert_data, self.response["args"])
if __name__ == '__main__':
seldom.main(debug=True, base_url="http://httpbin.org")
================================================
FILE: tests/test_jsonpath.py
================================================
import unittest
from seldom.extend_lib.jsonpath2 import jsonpath
json_data = {
"args": {
"id": "1",
"name": "tom",
"hobby": ["code", "sleep", "eat"],
"child": [
{
"id": "2",
"name": "jack",
"hobby": ["play"],
},
{
"id": "3",
"name": "blue",
"hobby": ["sleep"],
}
]
},
"name": "user-list"
}
class JSONPathTest(unittest.TestCase):
def test_case(self):
ret = jsonpath(json_data, "name")
self.assertEqual(ret, ['user-list'])
def test_case1(self):
ret = jsonpath(json_data, "args.id")
self.assertEqual(ret, ['1'])
def test_case2(self):
ret = jsonpath(json_data, "args.hobby[0]")
self.assertEqual(ret, ['code'])
def test_case3(self):
ret = jsonpath(json_data, "args.child[1].id")
self.assertEqual(ret, ['3'])
# self.assertEqual(ret, "1")
def test_case4(self):
ret = jsonpath(json_data, "args.child[1].hobby[0]")
self.assertEqual(ret, ['sleep'])
if __name__ == '__main__':
unittest.main()
================================================
FILE: tests/test_locators.py
================================================
"""
selenium locators
"""
import seldom
from seldom import Steps
class TestForm(seldom.TestCase):
def start(self):
self.test_url = "https://seleniumbase.io/demo_page"
self.open(self.test_url)
def test_locator(self):
"""locator"""
self.type(id_="myTextarea", text="id", clear=True)
self.type(name="textareaName", text="name", clear=True)
self.type(class_name="textareaClass", text="class name", clear=True)
self.type(css="#myTextarea", text="css", clear=True)
self.type(xpath="//textarea[@id='myTextarea']", text="xpath", clear=True)
self.click(link_text="seleniumbase.com")
self.open(self.test_url)
self.click(partial_link_text="se.com")
self.open(self.test_url)
def test_selector(self):
"""selector"""
self.type("id=myTextarea", text="id", clear=True)
self.type("name=textareaName", text="name", clear=True)
self.type("class=textareaClass", text="class name", clear=True)
self.type("#myTextarea", text="css", clear=True)
self.type("//textarea[@id='myTextarea']", text="xpath", clear=True)
self.click("text=seleniumbase.com")
self.open(self.test_url)
self.click("text*=se.com")
self.open(self.test_url)
def test_step(self):
"""chaining find"""
s = Steps()
s.open(self.test_url).find("id=myTextarea").clear().type("id") \
.find("name=textareaName").clear().type("name") \
.find("name=textareaName").clear().type("name") \
.find("class=textareaClass").clear().type("class name") \
.find("#myTextarea").clear().type("css") \
.find("//textarea[@id='myTextarea']").clear().type("xpath") \
.find("text=seleniumbase.com").click() \
.open(self.test_url) \
.find("text*=se.com").click() \
.open(self.test_url)
if __name__ == '__main__':
seldom.main(browser="edge", debug=True)
================================================
FILE: tests/test_log.py
================================================
import sys
import seldom
from seldom.logging import log
from seldom import data
class TestCase(seldom.TestCase):
def test_case(self):
""" sample case """
sys.stderr.write("3. 进入了test_case1了\n")
print("4. print msg")
log.debug("5. log msg")
def test_case2(self):
""" sample case """
log.warning("6. log warning msg")
@data([
(1, 'seldom'),
(2, 'selenium'),
(3, 'unittest'),
])
def test_ddt(self, _, keyword):
""" ddt case """
print("7. this is print msg")
log.debug(f"test data: {keyword}")
def test_failed(self):
""" ddt case """
assert 0
def test_error(self):
""" ddt case """
raise IOError("ddd")
if __name__ == '__main__':
print("1. 逻辑顺序测试开始!🚀")
log.debug("2. logger的内容不会被吃掉,但是没有进入seldom.main(),所以不会出现在报告中")
# seldom.main()
seldom.main(report="report.xml")
================================================
FILE: tests/test_other_lib/__init__.py
================================================
================================================
FILE: tests/test_other_lib/test_playwright.py
================================================
"""
playwright demo
doc: https://playwright.dev
"""
import seldom
from playwright.sync_api import sync_playwright
from playwright.sync_api import expect
class Playwright(seldom.TestCase):
def start(self):
self.p = sync_playwright().start()
self.chromium = self.p.chromium.launch(headless=False)
self.page = self.chromium.new_page()
def end(self):
self.chromium.close()
self.p.stop()
def test_playwright_start(self):
"""
test playwright index page
"""
self.page.goto("http://playwright.dev")
expect(self.page).to_have_title("Fast and reliable end-to-end testing for modern web apps | Playwright")
get_started = self.page.locator('text=Get Started')
expect(get_started).to_have_attribute('href', '/docs/intro')
get_started.click()
expect(self.page).to_have_url('http://playwright.dev/docs/intro')
# playwright 实现截图
screenshot_bytes = self.page.screenshot()
self.screenshots(image=screenshot_bytes)
def test_playwright_todo(self):
"""
test playwright todoMVC
"""
self.page.goto("https://demo.playwright.dev/todomvc/#/")
new_todo = self.page.locator(".new-todo")
new_todo.fill("sleep")
new_todo.press("Enter")
new_todo.fill("code")
new_todo.press("Enter")
new_todo.fill("eat")
new_todo.press("Enter")
self.page.locator('li').filter(has_text='code').get_by_label('Toggle Todo').check()
self.page.locator('li').filter(has_text='sleep').get_by_label('Toggle Todo').check()
self.page.locator('li').filter(has_text='eat').get_by_label('Toggle Todo').check()
# 截图
screenshot_bytes = self.page.screenshot()
self.screenshots(image=screenshot_bytes)
if __name__ == '__main__':
seldom.main()
================================================
FILE: tests/test_other_lib/test_pyautogui.py
================================================
import os
import pyautogui
import seldom
from seldom.testdata import get_int
class TestPyAutoGUINote(seldom.TestCase):
def start(self):
# 打开记事本(这里使用运行命令来打开,确保路径正确)
os.system('notepad.exe')
self.sleep()
def end(self):
# 模拟按下 Alt+F4 关闭记事本
pyautogui.hotkey('alt', 'f4')
def test_write_and_save(self):
"""
打开一个新的标签页写入内容并保存
"""
# 模拟按下 Ctrl+t 创建一个新的标签页
pyautogui.hotkey('ctrl', 't')
self.sleep()
pyautogui.press('shift') # 切换英文输入法
# 写入字符串到记事本
pyautogui.write('Hello, this is a test string written by pyautogui.', interval=0.1) # interval 参数设置每个字符之间的延迟时间
# 模拟按下 Ctrl+S 保存文件
pyautogui.hotkey('ctrl', 's')
self.sleep()
# 切换英文输入法
pyautogui.press('shift')
self.sleep()
# 输入文件名 + 回车确定
pyautogui.write(f'test_file{get_int()}.txt')
self.sleep()
pyautogui.press('enter')
self.sleep()
if __name__ == '__main__':
seldom.main()
================================================
FILE: tests/test_other_lib/test_uiautomator.py
================================================
import seldom
import uiautomator2 as u2
class MyAppTest(seldom.TestCase):
def start(self):
# 链接设备
self.d = u2.connect('192.168.31.234')
# 启动App
self.d.app_start("com.meizu.mzbbs")
def end(self):
# 停止app
self.d.app_stop("com.meizu.mzbbs")
def test_app(self, user):
""" 使用 uiautomator2 """
# 搜索
self.d(resourceId="com.meizu.flyme.flymebbs:id/nw").click()
# 输入关键字
self.d(resourceId="com.meizu.flyme.flymebbs:id/nw").set_text("flyme")
# 搜索按钮
self.d(resourceId="com.meizu.flyme.flymebbs:id/o1").click()
self.sleep(2)
if __name__ == '__main__':
seldom.main()
================================================
FILE: tests/test_playwright_sample.py
================================================
"""
需要安装 playwright: https://playwright.dev/
> pip install playwright
"""
import seldom
import base64
from playwright.sync_api import sync_playwright
from playwright.sync_api import expect
class Playwright(seldom.TestCase):
def start(self):
p = sync_playwright().start()
self.browser = p.chromium.launch()
self.page = self.browser.new_page()
def end(self):
self.browser.close()
def test_start(self):
page = self.page
page.goto("https://playwright.dev")
expect(page).to_have_title("Fast and reliable end-to-end testing for modern web apps | Playwright")
get_started = page.locator('text=Get Started')
expect(get_started).to_have_attribute('href', '/docs/intro')
# 截图保存到报告
screenshot_bytes = page.screenshot()
screenshot_b64 = base64.b64encode(screenshot_bytes).decode('utf-8')
self.images.append(screenshot_b64)
get_started.click()
expect(page).to_have_url('https://playwright.dev/docs/intro')
if __name__ == '__main__':
seldom.main()
================================================
FILE: tests/test_random/__init__.py
================================================
================================================
FILE: tests/test_random/test_testdata.py
================================================
"""
author: bugmaster
data: 2022/9/17
desc: 生成随机数用法
"""
import seldom
from seldom.testdata import *
class TestRandomData(seldom.TestCase):
def test_print_data(self):
# 随机一个名字
print("名字:", first_name())
print("名字(男):", first_name(gender="male"))
print("名字(女):", first_name(gender="female"))
print("名字(中文男):", first_name(gender="male", language="zh"))
print("名字(中文女):", first_name(gender="female", language="zh"))
# 随机一个姓
print("姓:", last_name())
print("姓(中文):", last_name(language="zh"))
# 随机一个姓名
print("姓名:", username())
print("姓名(中文):", username(language="zh"))
# 随机一个生日
print("生日:", get_birthday())
print("生日字符串:", get_birthday(as_str=True))
print("生日年龄范围:", get_birthday(start_age=20, stop_age=30))
# 日期
print("日期(当前):", get_date())
print("日期(昨天):", get_date(-1))
print("日期(明天):", get_date(1))
print("当月:", get_month())
print("上个月:", get_month(-1))
print("下个月:", get_month(1))
print("今年:", get_year())
print("去年:", get_year(-1))
print("明年:", get_year(1))
# 数字
print("数字(8位):", get_digits(8))
# 邮箱
print("邮箱:", get_email())
# 浮点数
print("浮点数:", get_float())
print("浮点数范围:", get_float(min_size=1.0, max_size=2.0))
# 随机时间
print("当前时间:", get_now_datetime())
print("当前时间(格式化字符串):", get_now_datetime(strftime=True))
print("未来时间:", get_future_datetime())
print("未来时间(格式化字符串):", get_future_datetime(strftime=True))
print("过去时间:", get_past_datetime())
print("过去时间(格式化字符串):", get_past_datetime(strftime=True))
# 随机数据
print("整型:", get_int())
print("整型32位:", get_int32())
print("整型64位:", get_int64())
print("MD5:", get_md5())
print("UUID:", get_uuid())
print("单词:", get_word())
print("单词组(3个):", get_words(3))
print("手机号:", get_phone())
print("手机号(移动):", get_phone(operator="mobile"))
print("手机号(联通):", get_phone(operator="unicom"))
print("手机号(电信):", get_phone(operator="telecom"))
if __name__ == '__main__':
seldom.main()
================================================
FILE: tests/test_request_extend.py
================================================
import seldom
class TestSaveResp(seldom.TestCase):
def test_save_response(self):
"""将response保存到文件中"""
resp = self.get("/get")
self.save_response(resp)
class TestReqIP(seldom.TestCase):
def test_get_ip_address(self):
"""检查当前请求的IP地址"""
self.get("/get")
self.ip_address()
if __name__ == '__main__':
seldom.main(base_url="https://httpbin.org")
================================================
FILE: tests/test_skip.py
================================================
import seldom
@seldom.skip(reason="跳过类")
class SkipTest(seldom.TestCase):
def test_case(self):
...
class YouTest(seldom.TestCase):
@seldom.skip(reason="跳过用例")
def test_skip_case(self):
...
def test_if_skip(self):
login = False
if login is False:
self.skipTest(reason="登录失败,跳过后续执行")
if __name__ == '__main__':
seldom.main(debug=True)
================================================
FILE: tests/test_steps_chaining.py
================================================
import seldom
from seldom import Steps
class WebTestChaining(seldom.TestCase):
"""test chaining API"""
def test_baidu(self):
"""test baidu search"""
Steps().open("https://www.baidu.com").find("#kw").type("seldom").find("#su").click().sleep(2)
self.assertInTitle("seldom")
def test_bing(self):
"""test bing search"""
Steps().open("https://www.bing.com").find("#sb_form_q").type("seldomqa").submit().sleep(2)
self.assertInTitle("seldomqa")
if __name__ == '__main__':
seldom.main(browser="edge")
================================================
FILE: tests/test_steps_chaining_browser.py
================================================
import seldom
from seldom import Steps
class WebTestChaining(seldom.TestCase):
"""test chaining API"""
def start(self):
self.step = Steps(browser="edge")
def end(self):
self.step.quit()
def test_baidu(self):
"""test baidu search"""
self.step.open("https://www.baidu.com").find("#kw").type("seldom").find("#su").click().sleep(2)
self.assertInTitle("seldom")
def test_bing(self):
"""test bing search"""
self.step.open("https://www.bing.com").find("#sb_form_q").type("seldomqa").submit().sleep(2)
self.assertInTitle("seldomqa")
if __name__ == '__main__':
seldom.main()
================================================
FILE: tests/test_thread/__init__.py
================================================
================================================
FILE: tests/test_thread/test_thread.py
================================================
"""
author: bugmaster
data: 2022/6/18
desc: 线程用法
"""
import time
import seldom
from seldom import data
from seldom.utils.thread_lab import ThreadWait
@ThreadWait
def slow_event(case_name, s):
"""
Take care of things that are slow!
:param case_name: case name
:param s: Some of the parameters
:return:
"""
print(f"{case_name} running sleep {s}s")
time.sleep(s)
return s + 1
class MyTest(seldom.TestCase):
@classmethod
def start_class(cls):
# 存放用例和结果
cls.assertDict = {}
def end_class(self):
"""
** 所有用例运行完成,搜集结果并断言
"""
all_result = ThreadWait.get_all_result()
for case, value in all_result.items():
self.assertEqual(self.assertDict[case], value)
def test_case_success(self):
self.sleep(1)
# 调用slow_event
slow_event("test_case_success", 2)
self.assertDict['test_case_success'] = 3
def test_case_fail(self):
self.sleep(1)
# 调用slow_event
slow_event("test_case_fail", 3)
self.assertDict['test_case_fail'] = 3
@data([
("case", "0_case", 4, 5),
("case", "1_case", 5, 6),
("case", "2_case", 6, 7),
])
def test_ddt(self, _, name, sec, ret):
self.sleep(1)
# 调用slow_event
slow_event(f"test_case3_{name}", sec)
self.assertDict[f"test_case3_{name}"] = ret
if __name__ == '__main__':
seldom.main(debug=False)
================================================
FILE: tests/test_thread/test_thread_browser.py
================================================
import time
import seldom
from seldom.extend_lib import threads
class BingTest(seldom.TestCase):
"""Bing search test case"""
def test_case(self):
"""a simple test case """
self.open("https://cn.bing.com")
self.type(id_="sb_form_q", text="seldom", enter=True)
self.assertInTitle("seldom")
if __name__ == '__main__':
@threads(3)
def run_case(browser):
seldom.main(browser=browser)
browser = ["gc", "ff", "edge"]
for b in browser:
run_case(b)
time.sleep(1)
================================================
FILE: tests/test_thread/test_thread_case.py
================================================
import time
import seldom
from seldom.extend_lib import threads
class MyTest(seldom.TestCase):
def test_baidu(self):
self.open("https://www.baidu.com")
self.sleep(3)
def test_bing(self):
self.open("https://www.bing.com")
self.sleep(4)
if __name__ == "__main__":
@threads(2) # !!!核心!!!! 设置线程数
def run_case(case: str):
"""
根据传入的case执行用例
"""
seldom.main(case=case, browser="gc", debug=True)
# 将两条用例拆分,分别用不同的浏览器执行
cases = [
"test_thread_case.MyTest.test_baidu",
"test_thread_case.MyTest.test_bing"
]
for c in cases:
run_case(c)
time.sleep(1)
================================================
FILE: tests/test_thread/test_thread_path.py
================================================
import seldom
from seldom.extend_lib import threads
@threads(3) # !!!核心!!!! 设置线程数
def run_case(path: str):
"""
根据传入的path执行用例
"""
seldom.main(path=path, debug=True)
if __name__ == "__main__":
# 定义3个测试文件,分别丢给3个线程执行。
paths = [
"./test_dir/more_case/test_case1.py",
"./test_dir/more_case/test_case2.py",
"./test_dir/more_case/test_case3.py"
]
for p in paths:
run_case(p)
================================================
FILE: tests/test_utils/__init__.py
================================================
================================================
FILE: tests/test_utils/test_file.py
================================================
import unittest
from pathlib import Path
from seldom.utils import file
class TestFilePathUtils(unittest.TestCase):
def test_path_is_string_and_correct(self):
"""测试 path 属性是否是字符串且路径正确"""
self.assertIsInstance(file.path, str)
expected_path = str(Path(__file__).resolve())
self.assertEqual(file.path, expected_path)
def test_dir_is_string_and_correct(self):
"""测试 dir 属性是否是字符串且路径正确"""
self.assertIsInstance(file.dir, str)
expected_dir = str(Path(__file__).resolve().parent)
self.assertEqual(file.dir, expected_dir)
def test_dir_dir_is_string_and_correct(self):
"""测试 dir_dir 属性是否是字符串且路径正确"""
self.assertIsInstance(file.dir_dir, str)
expected_dir_dir = str(Path(__file__).resolve().parent.parent)
self.assertEqual(file.dir_dir, expected_dir_dir)
def test_dir_dir_dir_is_string_and_correct(self):
"""测试 dir_dir_dir 属性是否是字符串且路径正确"""
self.assertIsInstance(file.dir_dir_dir, str)
expected_dir_dir_dir = str(Path(__file__).resolve().parent.parent.parent)
self.assertEqual(file.dir_dir_dir, expected_dir_dir_dir)
def test_parent_dir_is_string_and_correct(self):
"""测试 parent_dir 方法是否是字符串且路径正确(level=4)"""
self.assertIsInstance(file.parent_dir(4), str)
expected_parent_4 = str(Path(__file__).resolve().parents[3])
self.assertEqual(file.parent_dir(4), expected_parent_4)
if __name__ == '__main__':
unittest.main()
================================================
FILE: tests/test_websocket/__init__.py
================================================
================================================
FILE: tests/test_websocket/test_websocket.py
================================================
import seldom
from seldom.logging import log
from seldom.websocket_client import WebSocketClient
class WebSocketTest(seldom.TestCase):
def start(self):
"""创建WebSocket客户端线程"""
self.client = WebSocketClient("ws://127.0.0.1:8765/echo")
self.client.start()
# 等待客户端连接建立
self.sleep(1)
def end(self):
"""发送关闭消息"""
self.client.send_message("close")
# 停止WebSocket客户端线程
self.client.stop()
self.client.join()
def test_send_and_receive_message(self):
"""
测试发送送消息用例
"""
self.client.send_message("Hello, WebSocket!")
self.client.join(1)
self.client.send_message("How are you?")
self.client.join(1)
# 验证是否收到消息
log.info(self.client.received_messages)
self.assertEqual(len(self.client.received_messages), 2)
self.assertIn("Hello, WebSocket!", self.client.received_messages[0])
self.assertIn("How are you?", self.client.received_messages[1])
if __name__ == '__main__':
seldom.main(debug=True)
================================================
FILE: tests/test_websocket/webscoket_server.py
================================================
import aiohttp
from aiohttp import web
async def websocket_handler(request):
"""
WebSocket handler
:param request:
:return:
"""
ws = web.WebSocketResponse()
await ws.prepare(request)
async for msg in ws:
if msg.type == aiohttp.WSMsgType.TEXT:
if msg.data == 'close':
await ws.close()
else:
await ws.send_str(f"Message text was: {msg.data}")
elif msg.type == aiohttp.WSMsgType.ERROR:
print(f'ws connection closed with exception {ws.exception()}')
print('websocket connection closed')
return ws
app = web.Application()
app.router.add_get('/echo', websocket_handler)
if __name__ == '__main__':
web.run_app(app, port=8765)