├── .editorconfig ├── .gitattributes ├── .github └── workflows │ ├── publish.yml │ └── unittest.yml ├── .gitignore ├── .project ├── .pydevproject ├── CONTRIBUTING.md ├── LICENSE.TXT ├── MANIFEST.in ├── README.md ├── __main__.py ├── docs ├── requirements.txt ├── testbase │ ├── Makefile │ ├── api │ │ ├── conf.rst │ │ ├── context.rst │ │ ├── datadrive.rst │ │ ├── dist.rst │ │ ├── exlib.rst │ │ ├── loader.rst │ │ ├── logger.rst │ │ ├── management.rst │ │ ├── plan.rst │ │ ├── project.rst │ │ ├── report.rst │ │ ├── resource.rst │ │ ├── runner.rst │ │ ├── serialization.rst │ │ ├── testcase.rst │ │ ├── testresult.rst │ │ └── util.rst │ ├── apiref.rst │ ├── conf.py │ ├── datadrive.rst │ ├── dist.rst │ ├── environ.rst │ ├── extension.rst │ ├── index.rst │ ├── make.bat │ ├── project.rst │ ├── resource.rst │ ├── settings.rst │ ├── settingslist.rst │ ├── setup.rst │ ├── static │ │ └── assert_fail.png │ ├── testcase.rst │ ├── testcaserun.rst │ ├── testcheck.rst │ ├── testdiscover.rst │ ├── testmgr.rst │ ├── testplan.rst │ ├── testresult.rst │ └── testrun.rst └── tuia │ ├── Makefile │ ├── conf.py │ ├── index.rst │ ├── make.bat │ ├── qpath.rst │ ├── quickstart.rst │ └── tuia.rst ├── qta-manage.py ├── qta_statics ├── TestReport.xsl ├── TestResult.xsl ├── __init__.py └── qta-report.html ├── qtaf_settings.py ├── requirements.txt ├── setup.py ├── testbase ├── __init__.py ├── assertion.py ├── compat │ ├── __init__.py │ └── _imp.py ├── conf.py ├── context.py ├── datadrive.py ├── dist.py ├── exlib.py ├── loader.py ├── logger.py ├── management.py ├── plan.py ├── project.py ├── report.py ├── resource.py ├── retry.py ├── runner.py ├── serialization.py ├── test.py ├── testcase.py ├── testresult.py ├── testsuite.py ├── types.py ├── util.py └── version.py ├── tests ├── __init__.py ├── data │ ├── __init__.py │ └── server.py ├── sampletest │ ├── __init__.py │ ├── datatest.py │ ├── hellotest.py │ ├── loaderr.py │ ├── paramtest.py │ ├── repeattest.py │ ├── runnertest.py │ ├── seqtest.py │ ├── sharedatatest.py │ ├── suitetest.py │ ├── tagtest.py │ ├── test.json │ ├── test_dict.json │ └── test_global_parameters.json ├── sampletestplan │ ├── __init__.py │ └── hello.py ├── test_testbase │ ├── __init__.py │ ├── test_assert.py │ ├── test_conf.py │ ├── test_datadrive.py │ ├── test_loader.py │ ├── test_logger.py │ ├── test_management.py │ ├── test_plan.py │ ├── test_report.py │ ├── test_resource.py │ ├── test_retry.py │ ├── test_runner.py │ ├── test_serialization.py │ ├── test_testcase.py │ ├── test_testsuite.py │ └── test_util.py └── test_tuia │ ├── __init__.py │ └── test_qpath.py └── tuia ├── __init__.py ├── _tif.py ├── accessible.py ├── app.py ├── control.py ├── env.py ├── exceptions.py ├── filedialog.py ├── gfcontrols.py ├── keyboard.py ├── mouse.py ├── qpath.py ├── qpathparser.py ├── remoteprocessing.py ├── testcase.py ├── uiacontrols.py ├── util.py ├── webcontrols.py ├── wincontrols.py └── wintypes.py /.editorconfig: -------------------------------------------------------------------------------- 1 | # https://editorconfig.org/ 2 | # Refer: https://github.com/django/django/blob/master/.editorconfig 3 | 4 | root = true 5 | 6 | [*] 7 | indent_style = space 8 | indent_size = 4 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | end_of_line = lf 12 | charset = utf-8 13 | 14 | # Docstrings and comments use max_line_length = 119 15 | [*.py] 16 | max_line_length = 119 17 | 18 | # Use 2 spaces for the HTML files 19 | [*.html] 20 | indent_size = 2 21 | 22 | # The JSON files contain newlines inconsistently 23 | [*.json] 24 | indent_size = 2 25 | insert_final_newline = ignore 26 | 27 | [**/admin/js/vendor/**] 28 | indent_style = ignore 29 | indent_size = ignore 30 | 31 | # Minified JavaScript files shouldn't be changed 32 | [**.min.js] 33 | indent_style = ignore 34 | insert_final_newline = ignore 35 | 36 | # Makefiles always use tabs for indentation 37 | [Makefile] 38 | indent_style = tab 39 | 40 | # Batch files use tabs for indentation 41 | [*.bat] 42 | indent_style = tab 43 | 44 | [docs/**.txt] 45 | max_line_length = 79 46 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Basic .gitattributes for a python repo. 2 | # Fefer: https://github.com/alexkaratarakis/gitattributes 3 | 4 | # Source files 5 | # ============ 6 | * text=auto eol=lf 7 | *.html linguist-language=Python -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | # This workflows will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | name: Upload Python Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | deploy: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Set up Python 18 | uses: actions/setup-python@v1 19 | with: 20 | python-version: '3.x' 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install setuptools wheel twine 25 | - name: Build and publish 26 | env: 27 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 28 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 29 | run: | 30 | release=${{ github.ref }} 31 | echo ${release: 10} > version.txt 32 | python setup.py sdist bdist_wheel 33 | twine upload dist/* 34 | -------------------------------------------------------------------------------- /.github/workflows/unittest.yml: -------------------------------------------------------------------------------- 1 | name: Unittest 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - release/* 8 | 9 | pull_request: 10 | branches: 11 | - master 12 | 13 | jobs: 14 | test: 15 | name: Test on python ${{ matrix.python-version }} and ${{ matrix.os }} 16 | runs-on: ${{ matrix.os }} 17 | strategy: 18 | max-parallel: 4 19 | matrix: 20 | python-version: ['2.7', '3.7', '3.8', '3.9', '3.10', '3.11', '3.12'] 21 | os: [ubuntu-22.04, windows-2019, macos-13] 22 | exclude: 23 | - os: macos-13 24 | python-version: "2.7" 25 | - os: macos-13 26 | python-version: "3.7" 27 | - os: windows-2019 28 | python-version: "3.7" 29 | 30 | steps: 31 | - uses: actions/checkout@v2 32 | - name: Set up Python ${{ matrix.python-version }} 33 | uses: actions/setup-python@v1 34 | if: matrix.python-version != '2.7' && matrix.python-version != '3.7' 35 | with: 36 | python-version: ${{ matrix.python-version }} 37 | - name: Install Ubuntu Python 2.7 38 | if: matrix.python-version == '2.7' && matrix.os == 'ubuntu-22.04' 39 | run: | 40 | sudo apt-get update 41 | sudo apt-get install -y --no-install-recommends python2 python3-virtualenv 42 | virtualenv -p python2 ${HOME}/cp27 43 | ${HOME}/cp27/bin/python -m pip install -U pip 44 | ${HOME}/cp27/bin/python -m pip install -U setuptools wheel 45 | echo "${HOME}/cp27/bin" >> $GITHUB_PATH 46 | - name: Install Windows Python 2.7 47 | shell: cmd 48 | if: matrix.python-version == '2.7' && matrix.os == 'windows-2019' 49 | run: | 50 | choco install wget --no-progress 51 | wget -nv "https://www.python.org/ftp/python/2.7.18/python-2.7.18.amd64.msi" 52 | start /wait msiexec.exe /passive /i python-2.7.18.amd64.msi /norestart /L*V "python_install.log" ADDLOCAL=ALL ALLUSERS=1 TARGETDIR=c:\python27 53 | type "python_install.log" 54 | - name: Install Ubuntu Python 3.7 55 | if: matrix.python-version == '3.7' && matrix.os == 'ubuntu-22.04' 56 | env: 57 | DEBIAN_FRONTEND: noninteractive 58 | run: | 59 | sudo apt-get update && sudo apt-get install -y software-properties-common 60 | sudo add-apt-repository ppa:deadsnakes/ppa -y && sudo apt-get update 61 | sudo apt-get install -y python3.7 python3.7-distutils 62 | wget https://bootstrap.pypa.io/pip/3.7/get-pip.py && python3.7 get-pip.py 63 | mkdir ${HOME}/.bin 64 | ln -s $(which python3.7) "${HOME}/.bin/python" 65 | echo "${HOME}/.bin" >> $GITHUB_PATH 66 | - name: Install Dependencies 67 | run: | 68 | python -m pip install --upgrade pip pytest pytest-cov codecov mock 69 | pip install -r requirements.txt 70 | - name: Run Tests 71 | run: | 72 | python -m pytest tests/ --cov=testbase --cov-report=xml 73 | - name: Upload coverage to Codecov 74 | uses: codecov/codecov-action@v4 75 | with: 76 | token: ${{ secrets.CODECOV_TOKEN }} 77 | fail_ci_if_error: true 78 | files: ./coverage.xml 79 | flags: unittests 80 | name: codecov-qtaf 81 | path_to_write_report: ./coverage/codecov_report.txt 82 | verbose: true 83 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | _build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *.cover 48 | .hypothesis/ 49 | .pytest_cache/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | local_settings.py 58 | db.sqlite3 59 | 60 | # Flask stuff: 61 | instance/ 62 | .webassets-cache 63 | 64 | # Scrapy stuff: 65 | .scrapy 66 | 67 | # Sphinx documentation 68 | docs/_build/ 69 | 70 | # PyBuilder 71 | target/ 72 | 73 | # Jupyter Notebook 74 | .ipynb_checkpoints 75 | 76 | # pyenv 77 | .python-version 78 | 79 | # celery beat schedule file 80 | celerybeat-schedule 81 | 82 | # SageMath parsed files 83 | *.sage.py 84 | 85 | # Environments 86 | .env 87 | .venv 88 | env/ 89 | venv/ 90 | ENV/ 91 | env.bak/ 92 | venv.bak/ 93 | Pipfile.lock 94 | 95 | # Spyder project settings 96 | .spyderproject 97 | .spyproject 98 | 99 | # Rope project settings 100 | .ropeproject 101 | 102 | # mkdocs documentation 103 | /site 104 | 105 | # mypy 106 | .mypy_cache/ 107 | 108 | # IDE 109 | .vscode 110 | .idea 111 | 112 | # other 113 | .DS_Store 114 | version.txt 115 | *.prefs 116 | -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | QTAF 4 | 5 | 6 | 7 | 8 | 9 | org.python.pydev.PyDevBuilder 10 | 11 | 12 | 13 | 14 | 15 | org.python.pydev.pythonNature 16 | 17 | 18 | -------------------------------------------------------------------------------- /.pydevproject: -------------------------------------------------------------------------------- 1 | 2 | 3 | python 4 | python interpreter 5 | 6 | /${PROJECT_DIR_NAME} 7 | 8 | 9 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tencent/QTAF/7a6510695344ec59cdc51433ee8f1bb24881b13d/CONTRIBUTING.md -------------------------------------------------------------------------------- /LICENSE.TXT: -------------------------------------------------------------------------------- 1 | Tencent is pleased to support the open source community by making QTA available. 2 | Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved. 3 | If you have downloaded a copy of the QTA binary from Tencent, please note that the QTA binary is licensed under the BSD 3-Clause License. 4 | If you have downloaded a copy of the QTA source code from Tencent, please note that QTAsource code is licensed under the BSD 3-Clause License, except for the third-party components listed below which are subject to different license terms. Your integration ofQTA into your own projects may require compliance with the BSD 3-ClauseLicense, as well as the other licenses applicable to the third-party components included within QTA. 5 | A copy of theBSD 3-Clause License is included in this file. 6 | 7 | Terms of the BSD 3-Clause License: 8 | -------------------------------------------------------------------- 9 | 10 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 11 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 12 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 13 | 3. Neither the name of [copyright holder] nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 15 | 16 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | global-include *.txt *.TXT qta_statics/*.* 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # QTAF 2 | 3 | [![Build Status](https://travis-ci.org/Tencent/QTAF.svg?branch=master)](https://travis-ci.org/Tencent/QTAF) 4 | [![PyPi version](https://img.shields.io/pypi/v/qtaf.svg)](https://pypi.python.org/pypi/qtaf/) 5 | [![GitHub tag](https://img.shields.io/github/tag/Tencent/QTAF.svg)](https://GitHub.com/Tencent/QTAF/tags/) 6 | 7 | QTA is a cross-platform test automation tool for servers and native, hybrid and applications. 8 | 9 | ### Supported Platforms 10 | 11 | * iOS (powered by [QT4i](https://github.com/tencent/QT4i) driver) 12 | * Android (powered by [QT4A](https://github.com/tencent/QT4A) driver) 13 | * Web (powered by [QT4W](https://github.com/tencent/QT4W) driver) 14 | * Windows (powered by QT4C driver) 15 | * macOS (powered by QT4Mac driver)) 16 | * Server (powered by QT4S driver) 17 | 18 | QTAF (QTA Framework) is a base framework for QTA, including, 19 | 20 | * testbase 21 | * tuia 22 | 23 | ### Testbase 24 | 25 | Testbase is a test framework providing test execution, reporting and management, and is the common base for each platform-specific QTA driver. 26 | 27 | For more inforamtion about quick startup, usage and API reference, please read [testbase's document](http://qta-testbase.readthedocs.io/zh/latest/). 28 | 29 | 30 | ### TUIA 31 | 32 | TUIA (Tencent UI Automation) is a base framework for UI test automation, which is used by each platform-specific QTA driver for client. 33 | 34 | For more inforamtion about quick startup, usage and API reference, please read [TUIA's document](http://qta-tuia.readthedocs.io/zh/latest/). 35 | 36 | 37 | ------------------------------ 38 | 39 | QTA是一个跨平台的测试自动化工具,适用于后台、原生或混合型客户端应用的测试。 40 | 41 | ### 平台支持 42 | 43 | * iOS (由[QT4i](https://github.com/tencent/QT4i) driver提供) 44 | * Android (由[QT4A](https://github.com/tencent/QT4A) driver提供) 45 | * Web (由[QT4W](https://github.com/tencent/QT4W) driver提供) 46 | * Windows (由QT4C driver提供) 47 | * macOS (由QT4Mac driver提供) 48 | * Server (由QT4S driver提供) 49 | 50 | 51 | QTAF (QTA Framework)是QTA的基础框架,包括以下模块: 52 | 53 | * testbase 54 | * tuia 55 | 56 | ### Testbase 57 | 58 | Testbase是测试框架基础,提供包括测试执行、报告和用例管理等基础功能。Testbase会被各个平台的QTA Driver所使用。 59 | 60 | 快速入门、使用和接口文档请参考《[Testbase文档](http://qta-testbase.readthedocs.io/zh/latest/)》。 61 | 62 | 63 | ### TUIA 64 | 65 | TUIA (Tencent UI Automation)是UI自动化基础库,为QTA各个平台下的客户端UI测试Driver所使用。 66 | 67 | 快速入门、使用和接口文档请参考《[TUIA文档](http://qta-tuia.readthedocs.io/zh/latest/index.html)》。 68 | 69 | ------------------------------ 70 | 71 | 欢迎加入QQ群(432699528)交流使用和反馈 72 | -------------------------------------------------------------------------------- /__main__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Tencent is pleased to support the open source community by making QTA available. 4 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved. 5 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this 6 | # file except in compliance with the License. You may obtain a copy of the License at 7 | # 8 | # https://opensource.org/licenses/BSD-3-Clause 9 | # 10 | # Unless required by applicable law or agreed to in writing, software distributed 11 | # under the License is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS 12 | # OF ANY KIND, either express or implied. See the License for the specific language 13 | # governing permissions and limitations under the License. 14 | # 15 | """QTAF执行入口 16 | """ 17 | 18 | from testbase.management import qta_manage_main 19 | 20 | if __name__ == "__main__": 21 | qta_manage_main() 22 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx==1.8.1 2 | recommonmark 3 | sphinx_rtd_theme 4 | ply 5 | -------------------------------------------------------------------------------- /docs/testbase/api/conf.rst: -------------------------------------------------------------------------------- 1 | :mod:`testbase.conf` Package 2 | ============================ 3 | 4 | .. automodule:: testbase.conf 5 | :members: 6 | :show-inheritance: 7 | 8 | -------------------------------------------------------------------------------- /docs/testbase/api/context.rst: -------------------------------------------------------------------------------- 1 | :mod:`testbase.context` Package 2 | =============================== 3 | 4 | .. automodule:: testbase.context 5 | :members: 6 | :show-inheritance: 7 | 8 | -------------------------------------------------------------------------------- /docs/testbase/api/datadrive.rst: -------------------------------------------------------------------------------- 1 | :mod:`testbase.datadrive` Package 2 | ================================= 3 | 4 | .. automodule:: testbase.datadrive 5 | :members: 6 | :show-inheritance: 7 | 8 | -------------------------------------------------------------------------------- /docs/testbase/api/dist.rst: -------------------------------------------------------------------------------- 1 | :mod:`testbase.dist` Package 2 | ================================ 3 | 4 | .. automodule:: testbase.dist 5 | :members: 6 | :show-inheritance: 7 | 8 | -------------------------------------------------------------------------------- /docs/testbase/api/exlib.rst: -------------------------------------------------------------------------------- 1 | :mod:`testbase.exlib` Package 2 | ============================= 3 | 4 | .. automodule:: testbase.exlib 5 | :members: 6 | :show-inheritance: 7 | 8 | -------------------------------------------------------------------------------- /docs/testbase/api/loader.rst: -------------------------------------------------------------------------------- 1 | :mod:`testbase.loader` Package 2 | ============================== 3 | 4 | .. automodule:: testbase.loader 5 | :members: 6 | :show-inheritance: 7 | 8 | -------------------------------------------------------------------------------- /docs/testbase/api/logger.rst: -------------------------------------------------------------------------------- 1 | :mod:`testbase.logger` Package 2 | ============================== 3 | 4 | .. automodule:: testbase.logger 5 | :members: 6 | :show-inheritance: 7 | 8 | -------------------------------------------------------------------------------- /docs/testbase/api/management.rst: -------------------------------------------------------------------------------- 1 | :mod:`testbase.management` Package 2 | ================================== 3 | 4 | .. automodule:: testbase.management 5 | :members: 6 | :show-inheritance: 7 | 8 | -------------------------------------------------------------------------------- /docs/testbase/api/plan.rst: -------------------------------------------------------------------------------- 1 | :mod:`testbase.plan` Package 2 | ================================ 3 | 4 | .. automodule:: testbase.plan 5 | :members: 6 | :show-inheritance: 7 | 8 | -------------------------------------------------------------------------------- /docs/testbase/api/project.rst: -------------------------------------------------------------------------------- 1 | :mod:`testbase.project` Package 2 | =============================== 3 | 4 | .. automodule:: testbase.project 5 | :members: 6 | :show-inheritance: 7 | 8 | -------------------------------------------------------------------------------- /docs/testbase/api/report.rst: -------------------------------------------------------------------------------- 1 | :mod:`testbase.report` Package 2 | ============================== 3 | 4 | .. automodule:: testbase.report 5 | :members: 6 | :show-inheritance: 7 | 8 | -------------------------------------------------------------------------------- /docs/testbase/api/resource.rst: -------------------------------------------------------------------------------- 1 | :mod:`testbase.resource` Package 2 | ================================ 3 | 4 | .. automodule:: testbase.resource 5 | :members: 6 | :show-inheritance: 7 | 8 | -------------------------------------------------------------------------------- /docs/testbase/api/runner.rst: -------------------------------------------------------------------------------- 1 | :mod:`testbase.runner` Package 2 | ============================== 3 | 4 | .. automodule:: testbase.runner 5 | :members: 6 | :show-inheritance: 7 | 8 | -------------------------------------------------------------------------------- /docs/testbase/api/serialization.rst: -------------------------------------------------------------------------------- 1 | :mod:`testbase.serialization` Package 2 | ===================================== 3 | 4 | .. automodule:: testbase.serialization 5 | :members: 6 | :show-inheritance: 7 | 8 | -------------------------------------------------------------------------------- /docs/testbase/api/testcase.rst: -------------------------------------------------------------------------------- 1 | :mod:`testbase.testcase` Package 2 | ================================ 3 | 4 | .. automodule:: testbase.testcase 5 | :members: 6 | :show-inheritance: 7 | 8 | -------------------------------------------------------------------------------- /docs/testbase/api/testresult.rst: -------------------------------------------------------------------------------- 1 | :mod:`testbase.testresult` Package 2 | ================================== 3 | 4 | .. automodule:: testbase.testresult 5 | :members: 6 | :show-inheritance: 7 | 8 | -------------------------------------------------------------------------------- /docs/testbase/api/util.rst: -------------------------------------------------------------------------------- 1 | :mod:`testbase.util` Package 2 | ============================ 3 | 4 | .. automodule:: testbase.util 5 | :members: 6 | :show-inheritance: 7 | 8 | -------------------------------------------------------------------------------- /docs/testbase/apiref.rst: -------------------------------------------------------------------------------- 1 | 接口文档 2 | ======== 3 | 4 | .. toctree:: 5 | :maxdepth: 1 6 | 7 | api/conf 8 | api/context 9 | api/datadrive 10 | api/exlib 11 | api/loader 12 | api/logger 13 | api/management 14 | api/project 15 | api/report 16 | api/resource 17 | api/runner 18 | api/serialization 19 | api/testcase 20 | api/testresult 21 | api/dist 22 | api/plan 23 | api/util 24 | -------------------------------------------------------------------------------- /docs/testbase/dist.rst: -------------------------------------------------------------------------------- 1 | 测试项目打包 2 | ============ 3 | 4 | QTA内置测试项目打包的功能,方便将测试项目打包并发布给支持QTA测试执行的执行系统 5 | 6 | ======== 7 | 执行打包 8 | ======== 9 | 10 | 调用测试项目的manage.py:: 11 | 12 | $ python manage.py dist --version 1.0.0 13 | 14 | 执行成功后可以看到生成文件在dist目录下:: 15 | 16 | dist/ 17 | foo-1.0.0.tar.gz 18 | 19 | 20 | .. _RunDistPkg: 21 | 22 | ======== 23 | 使用包 24 | ======== 25 | 26 | 对于生成的包,QTA内置了执行测试的工具,可以通过调用qta-manage命令来执行测试:: 27 | 28 | $ qta-manage runtest foo-1.0.0.tar.gz footest 29 | 30 | qta-manage run命令提供丰富的控制参数来控制测试用例的执行范围。比如,只执行特定状态的用例:: 31 | 32 | $ qta-manage runtest foo-1.0.0.tar.gz footest --status Ready --status BVT 33 | 34 | 除了状态外,用例优先级、负责人等都能作为过滤的选项,也能通过--exclude-name来排除特定的包或模块的用例集:: 35 | 36 | $ qta-manage runtest foo-1.0.0.tar.gz footest --exclude-name footest.hello 37 | 38 | QTA打包生成的包是Python的sdist标准格式,而qta-manage runtest命令是通过生成一个virtualenv来执行测试的Python代码,如果需要,用户也能控制使用的virtualenv:: 39 | 40 | $ qta-manage runtest foo-1.0.0.tar.gz footest --venv /path/to/your/venv 41 | 42 | qta-manage run命令,也能控制使用的测试执行器、测试报告类型和测试资源管理后端类型:: 43 | 44 | $ qta-manage runtest foo-1.0.0.tar.gz footest --runner-type multithread --report-type json 45 | 46 | 上面的命令行就指定使用多线程执行,并生成JSON格式的报告;更多的可选的执行器、报告类型可以通过qta-manage run的--help参数查询。 47 | 在指定特定类型的runner和report后,也能传递参数给特定类型的runner和report,例如:: 48 | 49 | $ qta-manage runtest foo-1.0.0.tar.gz footest --runner-type multithread --runner-args "--concurrent 10" 50 | 51 | 比如上面的命令就指定多线程执行时使用10个线程。具体的runner和report类型有哪些可选参数,可以通过这样获取:: 52 | 53 | $ qta-manage foo-1.0.0.tar.gz runtest --runner-args-help multithread 54 | usage: qta-manage [-h] [--retries RETRIES] [--concurrent CONCURRENT] 55 | 56 | optional arguments: 57 | -h, --help show this help message and exit 58 | --retries RETRIES retry count while test case failed 59 | --concurrent CONCURRENT 60 | number of concurrent thread 61 | 62 | 同理,测试报告也能通过“--report-args-help”查询。 63 | 64 | 65 | -------------------------------------------------------------------------------- /docs/testbase/environ.rst: -------------------------------------------------------------------------------- 1 | 环境变量配置 2 | ============== 3 | 4 | 下面环境变量可以用来告诉QTAF,配置文件存放的位置,环境变量指定的优先级是最高的 5 | 6 | ==================== 7 | QTAF_SETTINGS_MODULE 8 | ==================== 9 | 指定用户自定义的配置模块,python在运行时可以找到的模块,支持多级路径,例如:myproject.settings_20160705 -------------------------------------------------------------------------------- /docs/testbase/extension.rst: -------------------------------------------------------------------------------- 1 | 开发新的扩展 2 | ============== 3 | 4 | QTAF的扩展允许用户扩展QTAF命令行工具的功能。通过实现扩展,用户能定制化测试执行和资源管理的方式,也能定制自定义的测试报告的格式,方便第三方的系统或平台开发对QTA测试用例的执行的支持。 5 | 6 | ========= 7 | 扩展点 8 | ========= 9 | 10 | 目前支持扩展的功能有: 11 | 12 | * qta-manage 13 | * runtest命令 14 | * runplan命令 15 | 16 | * 每个项目的manage.py 17 | * runtest命令 18 | * runplan命令 19 | 20 | 21 | 以上的命令都支持用户自定义测试执行器(TestRunner)、测试报告(TestReport)和测试资源管理后端(TestResourceManagerBackend) 22 | 23 | 24 | ======== 25 | 实现扩展 26 | ======== 27 | 28 | QTAF的扩展使用Python setuptools提供的 `Entry point机制`_。QTAF定义了三个Entry points: 29 | 30 | .. _Entry point机制: http://setuptools.readthedocs.io/en/latest/pkg_resources.html#entry-points 31 | 32 | * qtaf.runner:测试执行器类型扩展点,对应接口 “:class:`testbase.runner.BaseTestRunner`”,更多请参考“:ref:`CustomTestRunner`” 33 | * qtaf.report:测试报告类型扩展点,对应接口 “:class:`testbase.report.ITestReport`”,更多请参考“:ref:`CustomTestReport`” 34 | * qtaf.resmgr_backend:资源管理后端扩展点,对应接口 “:class:`testbase.resource.IResourceManagerBackend`”,更多请参考“:ref:`CustomResmgrBackend`” 35 | 36 | 37 | ======================= 38 | 关于扩展包命名的规范 39 | ======================= 40 | 41 | 请按照包格式:: 42 | 43 | qtaf-ext- 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /docs/testbase/index.rst: -------------------------------------------------------------------------------- 1 | .. Testbase documentation master file, created by 2 | sphinx-quickstart on Thu Jan 28 14:06:37 2016. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | QTA Testbase文档 7 | ================== 8 | 9 | Testbase是所有QTA测试项目的基础,主要提供测试用例管理和执行、测试结果和报告、测试项目管理配置等功能。 10 | 11 | ======== 12 | 使用文档 13 | ======== 14 | 15 | .. toctree:: 16 | :maxdepth: 1 17 | 18 | setup 19 | project 20 | testcase 21 | testcheck 22 | testcaserun 23 | testresult 24 | testmgr 25 | datadrive 26 | testrun 27 | testdiscover 28 | testplan 29 | settings 30 | resource 31 | dist 32 | extension 33 | 34 | ======== 35 | 接口文档 36 | ======== 37 | 38 | .. toctree:: 39 | :maxdepth: 1 40 | 41 | apiref 42 | settingslist 43 | environ 44 | 45 | 索引和搜索 46 | ============= 47 | 48 | * :ref:`genindex` 49 | * :ref:`modindex` 50 | * :ref:`search` 51 | 52 | -------------------------------------------------------------------------------- /docs/testbase/project.rst: -------------------------------------------------------------------------------- 1 | 创建和修改测试项目 2 | ===================== 3 | 4 | 对于QTA,一个测试自动化项目指的是针具体产品的特定测试的自动化用例和支持库的集合。在测试项目的支持库中,Testbase是必不可少的基础库,因为Testbase提供了测试项目的基本的管理的支持。 5 | 6 | ============== 7 | 创建测试项目 8 | ============== 9 | 10 | 在安装好QTAF后,可以在终端中执行一下命令:: 11 | 12 | $ qta-manage createproject foo 13 | 14 | 如果执行qta-manage提示找不到对应命令,则也可以通过以下方式执行:: 15 | 16 | $ python -m qta-manage createproject foo 17 | 18 | 执行成功后,可以看到当前目录下生成一下结构的文件:: 19 | 20 | /footestproj/ 21 | /foolib/ 22 | /__init__.py 23 | /testcase.py 24 | /footest/ 25 | /__init__.py 26 | /hello.py 27 | /.project 28 | /.pydevproject 29 | /settings.py 30 | /manage.py 31 | /requirements.txt 32 | 33 | 34 | ====================== 35 | 导入测试项目到Eclipse 36 | ====================== 37 | 38 | 如果在Windows/Mac上,可以使用eclispe导入以上项目: 39 | 40 | * File -> Import... 打开Import对话框 41 | * 选择源类型:General/Existing Projects into Workspace 42 | * 通过Select root directory选择创建的QTA项目的根路径 43 | * 看到Projects窗口显示footestproj,选择并点击Finish完成导入 44 | 45 | ============== 46 | 测试项目结构 47 | ============== 48 | 49 | 对于测试项目,一般包括一下的模块: 50 | 51 | * 测试用例,比如foo项目中的footest包,这里存储所有的测试用例的脚本。 52 | 53 | * 测试业务库,比如foo项目中的foolib包,这里存放所有测试业务lib层的代码。 54 | 55 | * 项目配置文件,即settings.py 56 | 57 | * 项目辅助脚本,即manage.py 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /docs/testbase/settings.rst: -------------------------------------------------------------------------------- 1 | 配置测试项目 2 | ============ 3 | 4 | 本节主要介绍如何修改测试项目的配置文件 settings.py来修改测试框架的行为。如果需要查询QTA框架的全部配置项,请参考《:doc:`./settingslist`》。 5 | 6 | ========== 7 | 配置语法 8 | ========== 9 | 10 | 测试项目的配置文件是一个python模块,所以配置项都是python模块的模块变量,如下所示:: 11 | 12 | DEBUG = True 13 | RUNNER_THREAD_COUNT = 5 14 | LOCAL_REPORT = 'console' 15 | 16 | 由于使用的是python模块表示,因此需要符合以下要求: 17 | 18 | * 需要符合python语法要求 19 | 20 | 除此之外,对于配置项还需要符合以下要求: 21 | 22 | * 配置变量名必须使用大写 23 | * 配置变量名不可以双下划线开头 24 | 25 | 比如下面的变量都是非法的:: 26 | 27 | lower_test = 34 28 | __CONFIG = "XXX" 29 | 30 | ========== 31 | 配置文件 32 | ========== 33 | 34 | QTA配置文件分为三种: 35 | 36 | * 用户配置文件 37 | * 依赖Egg包的配置文件 38 | * Testbase配置文件(即qtaf_settings模块) 39 | 40 | .. note:: 注意依赖Egg包的配置文件只有通过“manage.py installlib”方式安装到测试项目中,其配置文件才会被加载,具体的依赖egg,可以参考exlib下的installed_libs.txt 41 | 42 | 用户配置文件存放在测试项目的顶层位置;而QTAF配置文件打包在QTAF的egg包中,在QTAF egg包的顶层位置上;如下:: 43 | 44 | test_proj/ 45 | qt4a/ 46 | exlib/ 47 | qtaf.egg/ 48 | testbase/ 49 | tuia/ 50 | pyqq/ 51 | qtaf_settings.py # Testbase配置 52 | qt4i.egg/ 53 | qt4i/settings.py # 依赖Egg包的配置文件 54 | mqlib/ 55 | mqtest/ 56 | settings.py # 用户配置 57 | 58 | 59 | 当两个配置文件中的配置项存在冲突时,按照以下优先级从高到低处理: 60 | 61 | * 用户配置文件 62 | * 依赖Egg包的配置文件 63 | * Testbase配置文件 64 | 65 | 也就是说,用户配置文件可以重载QTAF配置中的默认配置。 66 | 67 | ============== 68 | 配置文件定位 69 | ============== 70 | 71 | 上面提到的三种配置文件,对于存在整个工程的情况来说,就可以直接使用,不需要额外处理。 72 | 如果想要独立使用qtaf或其他qta的egg模块,可以采用定义环境变量的方式告诉qtaf配置文件的位置:: 73 | 74 | QTAF_EXLIB_PATH: 指定qta相关egg包存放的路径,qtaf、qt4s、qt4a等egg都会去这里查找,并加载配置 75 | QTAF_INSTALLED_LIBS: 指定已安装并计划使用的第三方模块(即qtaf除外的),多个模块间用分号隔开,例如:qt4s;qt4a;qt4i 76 | QTAF_SETTINGS_MODULE: 指定用户自定义的配置模块,python在运行时可以找到的模块,支持多级路径,例如:myproject.settings_20160705 77 | 78 | .. warning:: 特别注意,如果环境变量存在,仅仅使用环境变量指定的内容,例如存在QTAF_INSTALLED_LIBS环境变量,就不会使用exlib目录下的installed_libs.txt中的内容了 79 | 80 | ============ 81 | 使用测试配置 82 | ============ 83 | 84 | 配置使用的接口统一使用conf接口,如下:: 85 | 86 | from testbase.conf import settings 87 | if settings.DEBUG: 88 | print 'debug mode' 89 | else: 90 | print 'release mode' 91 | 92 | 也可以使用get接口查询配置,比如:: 93 | 94 | from testbase.conf import settings 95 | my_conf = settings.get('MY_SETTING', None) 96 | 97 | .. warning:: settings.py和qtaf_settings.py也是可以直接import使用的,但是不建议这样做,如果这样使用,可能会遇到非预期的结果。 98 | 99 | 注意settings配置不允许动态修改配置的值,如:: 100 | 101 | settings.DEBUG = False 102 | 103 | 会导致异常:: 104 | 105 | Traceback (most recent call last): 106 | File "D:\workspace\qtaftest\test.py", line 17, in 107 | settings.DEBUG = 9 108 | File "build\bdist.win32\egg\testbase\conf.py", line 85, in __setattr__ 109 | RuntimeError: 尝试动态修改配置项"DEBUG" 110 | 111 | =========== 112 | 增加配置项 113 | =========== 114 | 115 | QTA对配置项的新增没有严格的限制,但是为避免冲突,最好按照以下的原则: 116 | 117 | * 测试项目自定义的配置,增加一个统一的前缀,比如QQ的测试项目增加前缀“QQ\_” 118 | * QTA相关组件的配置项目,除了统一增加前缀外,还需要更新到《:doc:`./settingslist`》 119 | 120 | ========================== 121 | 自定义settings所在的文件 122 | ========================== 123 | 124 | QTA默认是通过加载Python模块`settings`来读取所有配置,用户可以通过设置环境变量`QTAF_SETTINGS_MODULE`来指定配置项所在的模块名。 125 | 126 | 如果需要切换多套配置文件,可以在根木目创建一个settings pakcage,定义多个配置文件,然后在__init__.py中根据 127 | 需要定义个配置项用于加载子模块的配置项:: 128 | 129 | test_proj/ 130 | qt4a/ 131 | exlib/ 132 | mqlib/ 133 | mqtest/ 134 | settings/ 135 | __init__.py 136 | prod.py #正式环境 137 | test.py #测试环境 138 | 139 | 比如需要使用正式环境的配置:: 140 | 141 | $ QTAF_SETTINGS_MODULE=settings.prod python manage.py shell 142 | 143 | 比如需要使用测试环境的配置:: 144 | 145 | $ QTAF_SETTINGS_MODULE=settings.test python manage.py shell 146 | 147 | =================== 148 | 使用SettingsMixin 149 | =================== 150 | 151 | SettingsMixin是一个混合类,用于方便地跟用户定义的类进行复合,在定义配置项的时候, 152 | 将定义放到lib层,而不是孤立地放在settings.py或配置模块中,再人工进行关联。 153 | 154 | ------------- 155 | 定义配置项 156 | ------------- 157 | 158 | 一个简单的使用例子如下:: 159 | 160 | from qt4s.service import Channel 161 | from qt4s.conn2 import HttpConn 162 | from testbase.conf import SettingsMixin 163 | 164 | class MyChannel(Channel, SettingsMixin): 165 | """define a pseudo channel 166 | """ 167 | class Settings(object): 168 | MYCHANNEL_URL = "http://www.xxxx.com" 169 | 170 | def __init__(self): 171 | self._conn = HttpConn() 172 | 173 | def get(self, uri, params): 174 | return self._conn.get(self.settings.MYCHANNEL_URL + uri, params) 175 | 176 | MyChannel多重继承了Channel和SettingsMixin,SettingsMixin要求类的内部定义一个Settings类, 177 | 这个类定义配置项的规则如下: 178 | 179 | * 配置项必须以当前类的名字大写+下划线开头,例如这里的"MYCHANNEL\_"; 180 | * 配置项的每个字母都必须大写; 181 | * 访问配置项,使用self.settings访问,例如self.settings.MYCHANNEL_URL 182 | 183 | ------------- 184 | 重载配置项 185 | ------------- 186 | 187 | 重载配置项,分为两种情况 188 | 189 | ~~~~~~~~~~~~~~ 190 | 派生类重载 191 | ~~~~~~~~~~~~~~ 192 | 193 | 如果某个SettingsMixin类被继承,那么子类可以访问父类所拥有的配置项,并且可以重载父类的配置项,但是这里重载的方式比较特殊。 194 | 195 | 因为SettingsMixin要求当前类下必须定义一个嵌套Settings类,并且配置项必须以类名加下划线开头,因此,子类要重载父类的配置项, 196 | 通过定义相同后缀的配置项来实现,如下面的DUMMY_A和DUMMYCHILD_A,它们的后缀名都是"A",这样才会生效。 197 | 198 | 一个具体的例子如下:: 199 | 200 | from testbase.conf import SettingsMixin 201 | 202 | class Dummy(SettingsMixin): 203 | class Settings(object): 204 | DUMMY_A = 0 205 | 206 | def print_a(self): 207 | print("DUMMY_A=%s" % self.settings.DUMMY_A) 208 | 209 | class DummyChild(Dummy): 210 | class Settings(object): 211 | DUMMYCHILD_A = 2 212 | 213 | dummy = Dummy() 214 | assert dummy.settings.DUMMY_A == 0 215 | dummy.print_a() 216 | # DUMMY_A = 0 217 | 218 | child = DummyChild() 219 | assert child.settings.DUMMY_A == 2 220 | assert child.settings.DUMMYCHILD_A == 2 221 | child.print_a() 222 | # DUMMY_A = 2 223 | 224 | 如上,我们看到,在覆盖掉父类的配置项后,在父类的方法中访问的配置项也一样会被重载,这样可以复用父类的一些配置项,并根据需要进行重载。 225 | 226 | ~~~~~~~~~~~~~~~~~~ 227 | 全局配置项重载 228 | ~~~~~~~~~~~~~~~~~~ 229 | 230 | SettingsMixin定义的配置项还可以被全局配置项重载,并且全局配置项的优先级最高。我们仍然用上面的Dummy和DummyChild来说明问题。 231 | 232 | settings.py:: 233 | 234 | DUMMYCHILD_A = 3 235 | 236 | xxxxcase.py:: 237 | 238 | dummy = Dummy() 239 | assert dummy.settings.DUMMY_A == 0 240 | dummy.print_a() 241 | # DUMMY_A = 0 242 | 243 | child = DummyChild() 244 | assert child.settings.DUMMY_A == 3 245 | assert child.settings.DUMMYCHILD_A == 3 246 | child.print_a() 247 | # DUMMY_A = 3 248 | 249 | 可以看到,即使子类重载了DUMMY_A的值为2,但是仍然可以在settings.py中已更高的优先级将其修改。 250 | 251 | .. warning:: **框架在SettingsMixin定义的某个配置项被子类重载后,是不允许再在settings.py中去重载该配置项的**, 252 | 即:如果我们在settings.py中添加DUMMY_A = 5,框架会提示错误,要求用户去重载DUMMYCHILD_A,而不是DUMMY_A。 253 | 这样可以防止使用配置项在派生类之间冲突,并且简化配置项的设置。 254 | 255 | -------------------------------------------------------------------------------- /docs/testbase/settingslist.rst: -------------------------------------------------------------------------------- 1 | 配置项说明文档 2 | =============== 3 | 4 | ===== 5 | DEBUG 6 | ===== 7 | 当值为True时,表示为调试模式下执行,否则为正式环境下执行。 8 | 9 | 适用3.30.26或以上版本。 10 | 11 | ========== 12 | DATA_DRIVE 13 | ========== 14 | 默认为False,当为True时表示使用全局数据驱动,此时需要通过DATA_SOURCE指定数据文件。 15 | 16 | 适用5.0.0或以上版本。 17 | 18 | 19 | =========== 20 | DATA_SROUCE 21 | =========== 22 | 如果是字符串,则用于指定全局数据驱动的数据文件路径,使用相对路径,“/”标识测试项目的根目录;指定的数据文件使用Python代码格式,其中需要定义一个列表或字典兼容类型的“DATASET”变量,表示测试数据驱动的源数据。 23 | 24 | 如果是列表或字典兼容类型,则表示数据驱动的数据源。 25 | 26 | 适用5.0.0或以上版本。 27 | 28 | ============ 29 | PROJECT_MODE 30 | ============ 31 | 指定项目的运行管理模式:独立模式(standalone)和标准模式(standard),默认为独立模式。 32 | 33 | 适用5.0.97或以上版本。 34 | 35 | ============ 36 | PROJECT_ROOT 37 | ============ 38 | 仅标准模式下配置文件中的改设置才有效,用于指定项目的根目录,此目录会加到python path中。在独立模式下PROJECT_ROOT的指会自动推导得到,因此不能修改和设置。 39 | 40 | 适用5.0.97或以上版本。 41 | 42 | ============== 43 | INSTALLED_APPS 44 | ============== 45 | 仅标准模式下配置文件中的改设置才有效,用于指定已安装的QTAF库。在独立模式下INSTALLED_APPS的值通过读取exlib/installed_libs.txt得到,,因此不能修改和设置。 46 | 47 | 适用5.0.97或以上版本。 48 | 49 | ============== 50 | QTAF_ASSERT_CONTINUE 51 | ============== 52 | 指定断言assert_函数的行为,默认为True。当QTAF_ASSERT_CONTINUE为True时,assert_断言失败用例会继续执行;当QTAF_ASSERT_CONTINUE为False时,用例会终止执行。 53 | -------------------------------------------------------------------------------- /docs/testbase/setup.rst: -------------------------------------------------------------------------------- 1 | 使用前准备 2 | ========== 3 | 4 | ================= 5 | Python版本依赖 6 | ================= 7 | 8 | QTAF兼容python2.7以上和python3.6以上版本,windows下推荐使用ActivePython版本。Mac系统通常内置了python2.7。 9 | 10 | ================= 11 | 通过pip工具安装 12 | ================= 13 | 14 | 安装方法如下:: 15 | 16 | pip install qtaf 17 | 18 | .. warning:: 如果是mac os的用户,在使用pip安装的时候最好指定--user选项,否则安装可能失败或者script脚本无法使用。 19 | 20 | =================== 21 | 通过Git拉取最新代码 22 | =================== 23 | 24 | 操作方式如下:: 25 | 26 | git clone https://github.com/tencent/qtaf 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /docs/testbase/static/assert_fail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tencent/QTAF/7a6510695344ec59cdc51433ee8f1bb24881b13d/docs/testbase/static/assert_fail.png -------------------------------------------------------------------------------- /docs/testbase/testcaserun.rst: -------------------------------------------------------------------------------- 1 | 执行测试用例 2 | ============== 3 | 4 | 对于测试用例的执行,QTA也提供了一定的扩展能力。 5 | 6 | ======== 7 | 重复执行 8 | ======== 9 | 10 | 比如需要让一个测试用例重复执行多次:: 11 | 12 | from testbase.testcase import TestCase, RepeatTestCaseRunner 13 | 14 | class RepeatTest(TestCase): 15 | '''测试示例 16 | ''' 17 | owner = "foo" 18 | status = TestCase.EnumStatus.Ready 19 | timeout = 1 20 | priority = TestCase.EnumPriority.Normal 21 | case_runner = RepeatTestCaseRunner() 22 | repeat = 2 23 | 24 | def run_test(self): 25 | self.log_info('第%s次测试执行' % self.iteration) 26 | 27 | 28 | if __name__ == '__main__': 29 | HelloTest().debug_run() 30 | 31 | 这个用例和一般用例的区别是: 32 | 33 | * 增加repeat属性,用于指定要重复执行的次数 34 | 35 | * case_runner属性,指定了一个“:class:`testbase.testcase.RepeatTestCaseRunner`”实例 36 | 37 | 直接执行以上代码,输出为:: 38 | 39 | ============================================================ 40 | 测试用例:RepeatTest 所有者:foo 优先级:Normal 超时:1分钟 41 | ============================================================ 42 | INFO: 第0次测试执行 43 | ============================================================ 44 | 测试用例开始时间: 2015-07-16 20:17:11 45 | 测试用例结束时间: 2015-07-16 20:17:11 46 | 测试用例执行时间: 00:00:0.00 47 | 测试用例步骤结果: 1:通过 48 | 测试用例最终结果: 通过 49 | ============================================================ 50 | ============================================================ 51 | 测试用例:RepeatTest 所有者:foo 优先级:Normal 超时:1分钟 52 | ============================================================ 53 | INFO: 第1次测试执行 54 | ============================================================ 55 | 测试用例开始时间: 2015-07-16 20:17:11 56 | 测试用例结束时间: 2015-07-16 20:17:11 57 | 测试用例执行时间: 00:00:0.00 58 | 测试用例步骤结果: 1:通过 59 | 测试用例最终结果: 通过 60 | ============================================================ 61 | 62 | 可以看到测试用例被执行了两次,而且每次执行的时候,用例成员变量iteration都会增加1。 63 | 64 | ============== 65 | 控制执行顺序 66 | ============== 67 | 68 | 对于一些极端的情况下,需要控制测试用例的执行顺序。比如执行测试用例A、B、C需要按照一定先A、后B、再C的顺序来执行。 69 | 70 | .. warning:: QTA不推荐测试用例之间存在依赖关系,这样对于用例的可读性和后续的维护都会带来麻烦,所以不推荐控制用例按照顺序来执行。 71 | 72 | 73 | 例如下面一个控制执行顺序的例子:: 74 | 75 | from testbase import TestCase 76 | from testbase.testcase import RepeatTestCaseRunner 77 | 78 | class TestA(TestCase): 79 | '''测试示例 80 | ''' 81 | timeout = 1 82 | owner = "foo" 83 | status = TestCase.EnumStatus.Ready 84 | priority = TestCase.EnumPriority.Normal 85 | 86 | def run_test(self): 87 | pass 88 | 89 | class TestB(TestCase): 90 | '''测试示例 91 | ''' 92 | timeout = 1 93 | owner = "foo" 94 | status = TestCase.EnumStatus.Ready 95 | priority = TestCase.EnumPriority.Normal 96 | case_runner = RepeatTestCaseRunner() 97 | repeat = 2 98 | 99 | def run_test(self): 100 | pass 101 | 102 | class TestC(TestCase): 103 | '''测试示例 104 | ''' 105 | timeout = 1 106 | owner = "foo" 107 | status = TestCase.EnumStatus.Ready 108 | priority = TestCase.EnumPriority.Normal 109 | 110 | def run_test(self): 111 | pass 112 | 113 | __qtaf_seq_tests__ = [TestA, TestB, TestC] 114 | 115 | if __name__ == '__main__': 116 | from testbase.testcase import debug_run_all 117 | debug_run_all() 118 | 119 | 120 | 以上用例和普通的用例完全一致,不一样的地方是在模块中定义了变量qtaf_seq_tests ,这个变量就是用来指定测试用例的执行顺序。需要注意的是,如果要指定测试用例按照顺序执行,这些用例的实现都必须放在同一个代码文件中,这样限制的目的是为了提高代码的可读性。 121 | 122 | 以上的例子的执行结果如下:: 123 | 124 | ============================================================ 125 | 测试用例:TestA 所有者:foo 优先级:Normal 超时:1分钟 126 | ============================================================ 127 | ============================================================ 128 | 测试用例开始时间: 2015-07-16 20:24:46 129 | 测试用例结束时间: 2015-07-16 20:24:46 130 | 测试用例执行时间: 00:00:0.00 131 | 测试用例步骤结果: 1:通过 132 | 测试用例最终结果: 通过 133 | ============================================================ 134 | ============================================================ 135 | 测试用例:TestB 所有者:foo 优先级:Normal 超时:1分钟 136 | ============================================================ 137 | ============================================================ 138 | 测试用例开始时间: 2015-07-16 20:24:46 139 | 测试用例结束时间: 2015-07-16 20:24:46 140 | 测试用例执行时间: 00:00:0.00 141 | 测试用例步骤结果: 1:通过 142 | 测试用例最终结果: 通过 143 | ============================================================ 144 | ============================================================ 145 | 测试用例:TestB 所有者:foo 优先级:Normal 超时:1分钟 146 | ============================================================ 147 | ============================================================ 148 | 测试用例开始时间: 2015-07-16 20:24:46 149 | 测试用例结束时间: 2015-07-16 20:24:46 150 | 测试用例执行时间: 00:00:0.00 151 | 测试用例步骤结果: 1:通过 152 | 测试用例最终结果: 通过 153 | ============================================================ 154 | ============================================================ 155 | 测试用例:TestC 所有者:foo 优先级:Normal 超时:1分钟 156 | ============================================================ 157 | ============================================================ 158 | 测试用例开始时间: 2015-07-16 20:24:46 159 | 测试用例结束时间: 2015-07-16 20:24:46 160 | 测试用例执行时间: 00:00:0.00 161 | 测试用例步骤结果: 1:通过 162 | 测试用例最终结果: 通过 163 | ============================================================ 164 | 165 | =============== 166 | 自定义执行方式 167 | =============== 168 | 169 | 对于一般的测试用例的执行,QTA是按照下面的流程处理的: 170 | 171 | 1. 获取尝试测试用例对应的case_runner静态变量,如果不存在,则设置case_runner为一个“:class:`testbase.testcase.TestCaseRunner`”实例 172 | 173 | 2. 使用case_runner去执行对应的用例 174 | 175 | 因此,每个测试用例都可以通过指定这个case_runner来重载用例的执行逻辑。前面的重复执行用例的例子,就是通过“:class:`testbase.testcase.RepeatTestCaseRunner`”来实现的。 176 | 177 | 测试用例指定的case_runner要符合一定的接口规范,这个接口就是“:class:`testbase.testcase.ITestCaseRunner`”,其定义如下:: 178 | 179 | class ITestCaseRunner(object): 180 | 181 | def run(self, testcase, testresult_factory ): 182 | """执行一个用例 183 | 184 | :param testcase: 测试用例 185 | :type testcase: TestCase 186 | :param testresult_factory: 测试结果对象工厂 187 | :type testresult_factory: ITestResultFactory 188 | :rtype: TestResult/TestResultCollection 189 | """ 190 | pass 191 | 192 | 下面以一个例子来示例如果重载case_runner来指定一个测试用例执行的时候重复执行多次,也就是实现一个我们自己的版本的RepeatTestCaseRunner:: 193 | 194 | from testbase.testresult import TestResultCollection 195 | from testbase.testcase import ITestCaseRunner, TestCaseRunner 196 | 197 | class RepeatTestCaseRunner(ITestCaseRunner): 198 | 199 | def run(self, testcase, testresult_factory ): 200 | passed = True 201 | results = [] 202 | for _ in range(testcase.repeat): 203 | result = TestCaseRunner().run(testcase, testresult_factory) 204 | results.append(result) 205 | passed &= result.passed 206 | if not passed: #有一次执行不通过则中止执行 207 | break 208 | return TestResultCollection(results, passed) 209 | -------------------------------------------------------------------------------- /docs/testbase/testdiscover.rst: -------------------------------------------------------------------------------- 1 | 发现测试用例 2 | =============== 3 | 4 | 有时候,我们希望查看指定的条件和用例集下,最终会被执行的用例的列表,可以使用python manage.py discover命令来实现。例如:: 5 | 6 | $ python manage.py discover footest 7 | $ python manage.py discover --tag test --owner foo --priority High --status BVT footest 8 | $ python manage.py discover --excluded-name bar --output-file xxxx.txt footest 9 | 10 | 除了可以指定用例集,也可以指定用例状态、优先级和标签等等,与runtest命令类似。 11 | 12 | =============== 13 | 指定用例集 14 | =============== 15 | 16 | 这里可以参考“:ref:`specify_test`”。 17 | 18 | ===================== 19 | 指定用例优先级 20 | ===================== 21 | 22 | 这里可以参考“:ref:`specify_priority`”。 23 | 24 | **这里需要特别注意的是,discover命令默认放到normal的用例优先级是High和Normal。** 25 | 26 | ==================== 27 | 指定用例状态 28 | ==================== 29 | 30 | 这里可以参考“:ref:`specify_status`”。 31 | 32 | **这里需要特别注意的是,discover命令默认放到normal的用例状态只有Ready。** 33 | 34 | ==================== 35 | 指定用例作者 36 | ==================== 37 | 38 | 这里可以参考“:ref:`specify_owner`”。 39 | 40 | ==================== 41 | 指定用例标签 42 | ==================== 43 | 44 | 这里可以参考“:ref:`specify_tag`”。 45 | 46 | ==================== 47 | 指定显示的用例列表类型 48 | ==================== 49 | 50 | 使用--show命令可以显示用例列表类型,支持三种类型:normal、filtered和error。 51 | 52 | 默认情况下,三种类型都会展示,如果希望指定了--show选项,会以用户指定的类型为准。可以指定多个--show选项。例如:: 53 | 54 | $ python manage.py discover --show normal footest 55 | $ python manage.py discover --show normal --show error footest 56 | 57 | ==================== 58 | 指定输出格式 59 | ==================== 60 | 61 | 使用--output-type可以指定输出用例列表的格式,目前支持格式如下: 62 | 63 | * stream,每个部分一行是一个用例,会详细展示用例相关的属性 64 | * line,每个部分的用例用空格拼接为一整行,用于runtest或者测试计划 65 | 66 | ==================== 67 | 指定输出文件 68 | ==================== 69 | 70 | 使用--output-file可以指定用例列表的输出文件,未指定时,会将内容输出到stdout。 -------------------------------------------------------------------------------- /docs/testbase/testmgr.rst: -------------------------------------------------------------------------------- 1 | 管理测试用例 2 | ============== 3 | 4 | 从《:doc:`./testcase`》部分可以我们知道,一个测试用例,就是一个Python的测试用例类。在实际的测试项目中,会包含成百上千的测试用例,所以需要一种组织形式来管理这些测试用例。 5 | 6 | ======== 7 | 组织形式 8 | ======== 9 | 10 | 一个测试用例,就是一个Python的测试用例类,因此测试用例的组织其实就是Python类的组织。对于Python而言,存在三层结构的代码组织形式:包、模块和类。 11 | 12 | Python模块即对应一个py代码文件,比如前面的hello.py就是定义一个python的模块。Python的包是一个模块的容器,Python要求包中至少定义一个__init__.py的模块,而且Python包是允许包含另一个Python包,因此可以构成一个N层次的树状结构。例如下面的代码组织形式:: 13 | 14 | zootest\ 15 | __init__.py 16 | cat\ 17 | __init__.py 18 | feed.py * 19 | play.py 20 | dog\ 21 | __init__.py 22 | feed.py 23 | play.py 24 | 25 | Python以名字来唯一表示一个模块,也就是说,名字相同的模块就是同一个模块,所以模块名字必须是唯一的。使用“.”间隔的方式来唯一定位一个模块,比如上面的代码树的例子中加“*”的模块的名字如下:: 26 | 27 | zootest.cat.feed 28 | 29 | 因此,对应的在feed.py里面的类的名字的唯一标识为:: 30 | 31 | zootest.cat.feed.FeedFishTest 32 | zootest.cat.feed.FeedMouseTest 33 | zootest.cat.feed.FeedAppleTest 34 | 35 | 由于一个测试用例,就是一个Python的测试用例类,所以测试用例的名字也就和类的名字是一样的(数据驱动用例除外)。 36 | 37 | .. note:: Python初学者容易忘记在包定义中增加__init__.py文件,如果没有__init__.py,则对于Python来说只是一个普通的文件夹,因此定义在里面的测试用例也无法被QTA识别出来。 38 | 39 | ============== 40 | 加载测试用例 41 | ============== 42 | 43 | 对于一个测试项目中大量的测试用例,我们可以使用TestLoader来加载和分析,例如下面的代码:: 44 | 45 | from testbase.loader import TestLoader 46 | 47 | loader = TestLoader() 48 | for it in loader.load("zootest"): 49 | print(it) 50 | 51 | 上面代码是加载zootest包下面的全部测试用例,并展示其对应的测试用例名称,执行的结果如下:: 52 | 53 | zootest.cat.feed.FeedFishTest 54 | zootest.cat.feed.FeedMouseTest 55 | zootest.cat.feed.FeedAppleTest 56 | zootest.cat.play.PlayBallTest 57 | zootest.cat.play.PlayLightTest 58 | zootest.dog.feed.FeedFishTest 59 | zootest.dog.feed.FeedMouseTest 60 | zootest.dog.feed.FeedAppleTest 61 | zootest.dog.play.PlayBallTest 62 | zootest.dog.play.PlayLightTest 63 | 64 | TestLoader的load可以接受非顶层的包名,比如:: 65 | 66 | for it in loader.load("zootest.cat"): 67 | print(it) 68 | 69 | 返回:: 70 | 71 | zootest.cat.feed.FeedFishTest 72 | zootest.cat.feed.FeedMouseTest 73 | zootest.cat.feed.FeedAppleTest 74 | zootest.cat.play.PlayBallTest 75 | zootest.cat.play.PlayLightTest 76 | 77 | 也支持模块名:: 78 | 79 | for it in loader.load("zootest.cat.feed"): 80 | print(it) 81 | 82 | 返回:: 83 | 84 | zootest.cat.feed.FeedFishTest 85 | zootest.cat.feed.FeedMouseTest 86 | zootest.cat.feed.FeedAppleTest 87 | 88 | 甚至可以支持测试用例名:: 89 | 90 | for it in loader.load("zootest.cat.feed.FeedFishTest"): 91 | print(it) 92 | 93 | 返回:: 94 | 95 | zootest.cat.feed.FeedFishTest 96 | 97 | 可以看到通过不同的层次路径,我们可以控制测试用例的范围。如果通过名字控制的方式比较难筛选,也可以通过过滤函数来筛选:: 98 | 99 | def filter( testcase ): 100 | if testcase.status != TestCase.EnumStatus.Ready: 101 | return "status is not ready" 102 | 103 | loader = TestLoader(filter) 104 | for it in loader.load("zootest"): 105 | print(it) 106 | 107 | 以上的代码可以过滤掉全部状态不是为Ready的测试用例。如果需要查询被过滤的全部测试用例,可以调用下面接口:: 108 | 109 | filtered_records = loader.get_filtered_tests_with_reason() 110 | for tc in filtered_records: 111 | print(tc.name, filtered_records[tc]) 112 | 113 | ============== 114 | 处理加载失败 115 | ============== 116 | 117 | 测试用例加载过程中,可能会遇到由于测试脚本设计问题,在加载模块的时候就异常了,比如下面的py脚本:: 118 | 119 | from testbase.testcase import TestCase 120 | 121 | raise RuntimeError("load error") 122 | 123 | class HelloTest(TestCase): 124 | '''测试示例 125 | ''' 126 | owner = "foo" 127 | status = TestCase.EnumStatus.Ready 128 | timeout = 1 129 | priority = TestCase.EnumPriority.Normal 130 | 131 | def runTest(self): 132 | pass 133 | 134 | 135 | 上面的脚本加载必然失败,TestLoader会把这种错误记录下来,通过下面的方式可以查询:: 136 | 137 | err_records = loader.get_last_errors() 138 | for name in err_records: 139 | print 'name:', name 140 | print 'error:', err_records[name] 141 | 142 | 执行的结果:: 143 | 144 | name: hello 145 | error: Traceback (most recent call last): 146 | File "D:\workspace\qtaf5\test\hellotest.py", line 14, in 147 | raise RuntimeError("load error") 148 | RuntimeError: load error 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | -------------------------------------------------------------------------------- /docs/testbase/testplan.rst: -------------------------------------------------------------------------------- 1 | 测试计划 2 | ======== 3 | 4 | 在“:doc:`testrun`”中介绍了一种批量执行测试用例和生成对应测试报告的方法,但是在实际测试执行中,还需要一些前置和后置的动作,以及对测试资源(帐号、设备等)进行初始化和清理,而“测试计划”就是用于解决这个问题。 5 | 6 | 7 | ============== 8 | 定义测试计划 9 | ============== 10 | 11 | 测试计划都以“:class:`testbase.plan.TestPlan`”为基类:: 12 | 13 | from testbase.plan import TestPlan 14 | 15 | class AndroidAppTestPlan(TestPlan): 16 | """Android App test plan 17 | """ 18 | tests = "adtest" 19 | test_target_args = "http://package.com/xx.apk" 20 | 21 | def get_test_target(self): 22 | """获取被测对象详情 23 | """ 24 | return {"apk": self.test_target_args", 25 | "version": tool_get_apk_ver(self.test_target_args)} 26 | 27 | def test_setup(self, report): 28 | """全局初始化 29 | """ 30 | install_tools("adb") 31 | 32 | def resource_setup(self, report, restype, resource): 33 | """测试资源初始化 34 | """ 35 | if res_type == "android": 36 | adb_install(resource["serialno"], self.test_target_args) 37 | 38 | if __name__ == '__main__': 39 | AndroidAppTestPlan().debug_run() 40 | 41 | .. note:: TestPlan不允许子类重载__init__方法,否则会导致对象初始化失败。 42 | 43 | 上面的代码定义了一个测试计划,包括两个必要的类成员变量: 44 | 45 | * tests:要执行的测试用例,接受多种类型参数,可以参考“:ref:`TestRunnerRunParam`” 46 | * test_target: 被测对象的参数,由用户自定义,可以是任意的Python类型,一般来说主要是字符串等 47 | 48 | 这里实现了两个接口 49 | 50 | * :meth:`testbase.plan.TestPlan.get_test_target`:用于解析被测对象参数,并返回对应的被测对象的Key-Value信息,具体的KV结构完全由用户自定义,这个方法返回的结果会提供给测试报告进行记录 51 | * :meth:`testbase.plan.TestPlan.test_setup`:全局初始化,会在测试执行之前处理一次 52 | * :meth:`testbase.plan.TestPlan.resource_setup`:资源初始化,针对每个资源都会有一次操作。这里大部分的资源由资源管理系统提供,资源的注册和新增可以通过资源管理的接口实现,详情可以参考“:ref:`RegisterResType`”;但有一种特殊的资源类型“node”会由测试执行器定义,node类型的资源表示的是当前执行测试用例的主机,因此,如果需要对当前执行测试的主机环境进行预处理,可以针对node类型的资源进行处理即可。 53 | 54 | 和初始化的接口对应的,TestPlan也同时提供的清理接口: 55 | 56 | * :meth:`testbase.plan.TestPlan.test_teardown`:全局清理 57 | * :meth:`testbase.plan.TestPlan.resource_teardown`:资源清理 58 | 59 | 如果需要也可以重载以上两个方法。 60 | 61 | ============== 62 | 调试测试计划 63 | ============== 64 | 65 | 和测试用例类似,测试计划也提供了 :meth:`testbase.plan.TestPlan.debug_run` 的方法用于调试执行。像上面的例子,在__main__分支下调用debug_run后,只要直接执行当前的脚本就可以实现调试。 66 | 67 | 默认情况下,执行测试计划会执行全部用例,且使用 :class:`testbase.report.StreamTestReport` 类型的报告和 :class:`testbase.resource.LocalResourceManagerBackend` 类型的后端,如果用户需要指定对应的后端,可以通过参数传递给debug_run方法:: 68 | 69 | if __name__ == '__main__': 70 | from testbase.report import XMLReport 71 | from testbase.resource import LocalResourceManagerBackend 72 | AndroidAppTestPlan().debug_run( 73 | report=XMLReport(), 74 | resmgr_backend=LocalResourceManagerBackend()) 75 | 76 | 77 | ===================== 78 | 测试计划存放的位置 79 | ===================== 80 | 81 | 测试计划的存放位置框架没有强制的要求,建议一般是存放在“testplan”名字后缀的Python包或模块中,比如下面的项目代码结构:: 82 | 83 | /footestproj/ 84 | footest/ 85 | footestplan/ 86 | func.py <----功能测试计划 87 | perf.py <----性能测试计划 88 | foolib/ 89 | exlib/ 90 | resources/ 91 | settings.py 92 | manage.py 93 | 94 | 95 | ============== 96 | 执行测试计划 97 | ============== 98 | 99 | 正式执行测试计划有两种方式,一种是通过QTAF提供的命令行工具,一种是直接调用QTAF的接口 100 | 101 | ----------- 102 | 命令行接口 103 | ----------- 104 | 105 | qta-manage接口和每个项目的manage.py都有提供“runplan”命令用于执行一个测试计划。 106 | 107 | 如果通过qta-manage调用,可以针对已经打包(参考“:doc:`dist`”)的项目中的测试计划进行执行:: 108 | 109 | $ qta-manage runplan footest-1.0.0.tar.gz footestplan.FooTestPlan 110 | 111 | 如果通过manage.py调用:: 112 | 113 | $ manage.py runplan footestplan.FooTestPlan 114 | 115 | 此外,qta-manage和manage.py的runplan和runtest类似,都提供选择测试类型执行器、测试报告、资源管理类型的参数,详情可以参考“:ref:`RunDistPkg`”。 116 | 117 | ----------- 118 | 类接口 119 | ----------- 120 | 121 | “:class:`testbase.runner.TestRunner.run`”也支持传入“:class:`testbase.plan.TestPlan`”对象:: 122 | 123 | from testbase.runner import TestRunner 124 | from testbase.report import StreamTestReport 125 | from footestplan import FooTestPlan 126 | TestRunner(StreamTestReport()).run(FooTestPlan()) 127 | 128 | TestRunner其他的用法和执行用例的方式一致,详情请参考“:doc:`testrun`”。 -------------------------------------------------------------------------------- /docs/tuia/index.rst: -------------------------------------------------------------------------------- 1 | .. TUIA documentation master file, created by 2 | sphinx-quickstart on Sat Dec 10 17:16:13 2016. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | QTA UI自动化基础文档 7 | ============= 8 | 9 | 内容: 10 | 11 | .. toctree:: 12 | :maxdepth: 1 13 | 14 | quickstart 15 | qpath 16 | tuia 17 | 18 | 19 | 索引和搜索 20 | ===== 21 | 22 | * :ref:`genindex` 23 | * :ref:`modindex` 24 | * :ref:`search` 25 | 26 | -------------------------------------------------------------------------------- /docs/tuia/qpath.rst: -------------------------------------------------------------------------------- 1 | QPath语法和使用 2 | ========== 3 | 4 | QPath是QTA UI自动化框架中最主要的定位UI元素的语言。 5 | 6 | ======== 7 | QPath的语法 8 | ======== 9 | 10 | QPath的完整的语法定义如下:: 11 | 12 | QPath ::= Seperator QPath Seperator UIObjectLocator 13 | UIObjectLocator ::= UIObjectProperty PropertyConnector UIObjectLocator 14 | UIObjectProperty ::= UIProperty 15 | | RelationProperty 16 | | IndexProperty 17 | | UITypeProperty 18 | UIProperty ::= PropertyName Operator Literal 19 | RelationProperty ::= MaxDepthIdentifier EqualOperator Literal 20 | IndexProperty ::= InstanceIdentifier EqualOperator Literal 21 | UITypeProperty ::= UITypeIdentifier EqualOperator StringLiteral 22 | MaxDepthIdentifier := "MaxDepth" 23 | InstanceIdentifier ::= "Instance" 24 | UITypeIdentifier := "UIType" 25 | Operator ::= EqualOperator | MatchOperator 26 | PropertyConnector ::= "&&" | "&" 27 | Seperator ::= "/" 28 | EqualOperator ::= "=" #精确匹配 29 | MatchOperator ::= "~=" #使用正则进行模糊匹配 30 | PropertyName ::= "[a-zA-Z_]*" 31 | Literal := StringLiteral 32 | | IntegerLiteral 33 | | BooleanLiteral 34 | StringLiteral ::= "\"[a-zA-Z_]*\"" 35 | IntegerLiteral ::= "[0-9]*" 36 | BooleanLiteral := "True" | "False" | "true" | "false" 37 | 38 | 需要注意的是: 39 | 40 | * QPath的属性名(PropertyName)都是不区分大小写 41 | 42 | 以下是一个最简单的QPath的例子:: 43 | 44 | / Id='MainPanel' 45 | 46 | QPath通过“/”来连接多个UI对象定位器(UIObjectLocator),比如:: 47 | 48 | / Id='MainPanel' / Id='LogoutButton' 49 | 50 | 一个UI对象定位器也可以存在多个属性,使用“&”或者“&&”连接,比如:: 51 | 52 | / Id='MainPanel' & className='Window' / Id='LogoutButton' && Visiable='True' 53 | 54 | 如果属性值本身是布尔或者数值类型,也可以不使用字符串标识,比如下面的QPath和之前的QPath是等价的:: 55 | 56 | / Id='MainPanel' & className='Window' / Id='LogoutButton' && Visiable=True 57 | 58 | QPath也支持对属性进行正则匹配,可以使用“~=”:: 59 | 60 | / Id~='MainP.*' / Id='LogoutButton' 61 | 62 | 63 | ============ 64 | QPath和UI元素查找 65 | ============ 66 | 67 | QPath的语义是描述如何去定位一个或者多个UI元素。因为在所有的UI框架中,UI元素的组织形式都是树状的, 68 | 因此QPath描述的也就是如何在一个UI树里面去定位一个或者多个UI元素。 69 | QPath在树查找的过程,就是从根节点开始,一层一层增加树的深度去查找UI元素,QPath的间隔符“/”就是为了区分层次,比如下面的QPath:: 70 | 71 | / Id='MainPanel' & className='Window' / Id='LogoutButton' && Visiable='True' 72 | 73 | 有两层的查找。 74 | 在每一层的查找过程中,QPath通过属性匹配的方式来筛选UI元素。在一些情况下,如果UI元素不存在QPath中定义的属性,则认为不匹配。 75 | QPath在一层深度中查找到一个或者多个UI元素,则这些UI元素都会作为根,依次查找下一层的UI元素;如果在一层深度的UI元素查找中未能找到任何匹配的UI元素,则查找失败,即这个QPath不匹配任何的UI元素。 76 | 77 | ========== 78 | QPath的特殊属性 79 | ========== 80 | 81 | 一般来说,QPath的查找都是匹配UI元素的属性,但QPath也定义了一些特殊的属性, 82 | 83 | ---------- 84 | MaxDepth属性 85 | ---------- 86 | MaxDepth属性用于控制一次搜索的最大深度。一般情况下,一个QPath有多少个UIObjectLocator则有会匹配到同样深度的子UI元素,但是对于UI元素层次比较深的UI元素,QPath会变得很长。 87 | 比如下面的例子:: 88 | 89 | / ClassName='TxGuiFoundation' && Caption~='QQ\d+' 90 | / ClassName='TxGuiFoundation' 91 | / ClassName='TxGuiFoundation' 92 | / ClassName='TxGuiFoundation' 93 | / ClassName='TxGuiFoundation' 94 | / ClassName='TxGuiFoundation' 95 | / ClassName='TxGuiFoundation' 96 | / ClassName='GF' && name='mainpanel' 97 | 98 | 为解决这个问题,QPath使用关系属性(RelationProperty)来指定一次搜索的深度,比如上面很冗余的QPath在很多情况下都可以修改为:: 99 | 100 | / ClassName='TxGuiFoundation' && Caption~='QQ\d+' 101 | / name='mainpanel'&& MaxDepth='7' 102 | 103 | 注意MaxDepth是从当前深度算起,也就是说MaxDepth='1'时和没有指定MaxDepth是一样的,也就是说MaxDepth默认为1。 104 | 105 | ---------- 106 | Instance属性 107 | ---------- 108 | 109 | 当一个QPath搜索的结果中包含多个匹配的UI元素时,可以使用索引属性(IndexProperty)来唯一指定一个UI元素,比如假设以上的QPath搜索得到了多个UI元素,需要指定返回第一个匹配的UI元素,则可以写为:: 110 | 111 | / ClassName='TxGuiFoundation' && Caption~='QQ\d+' 112 | / name='mainpanel'&&MaxDepth='7'&&Instance='0' 113 | 114 | 注意索引是从0开始计算的,支持负数索引。 115 | 116 | .. note:: 当QPath找到多个UI元素的时候其排序本身并没有标准的定义,一般来说和树遍历的方式有关,具体的平台可能都存在差异,而且由于指定Instance本身并不是很好维护的方式,所以要尽量避免使用。 117 | 118 | -------- 119 | UIType属性 120 | -------- 121 | 122 | 当UI界面混合使用多种类型的UI控件时,可以通过UIType指定控件的类型,如果不指定UIType,则继承上一层搜索时指定的UIType,对于只有一种控件类型的平台,可以指定一个默认UIType,用户可以不用在QPath显式指定UIType。比如以下一个使用UIType的例子:: 123 | 124 | / ClassName='TxGuiFoundation' && Caption~='QQ\d+' 125 | / UIType='GF'&&name='main' 126 | / name='mainpanel' 127 | 128 | 在这里,UIType默认为“Win32”,因此在第一层搜索的搜索时候,只搜索控件类型为Win32的控件;但在第二层搜索的时候,UIType指定为“GF”,则第二层和第三层的搜索都只搜索控件类型为GF的控件。 129 | 130 | 131 | =========== 132 | QPath简便写法汇总 133 | =========== 134 | 135 | 在一些平台上,可以使用字符串类型来标识一个等价意义的QPath,特汇总如下。 136 | 137 | ------------- 138 | QT4C的字符串类型定位器 139 | ------------- 140 | 141 | 在QT4C中字符串类型的定位器等价于在UI元素树查找对应Name属性值匹配的元素,比如字符串:: 142 | 143 | "Hello" 144 | 145 | 等价于下面的“伪”QPath:: 146 | 147 | QPath('/Name="Hello"' && MaxDepth=∞') 148 | 149 | ------------- 150 | QT4i的字符串类型定位器 151 | ------------- 152 | 153 | 在QT4i中字符串类型的定位器等价于在UI元素树查找对应Name属性值匹配的元素,比如字符串:: 154 | 155 | "Hello" 156 | 157 | 等价于下面的“伪”QPath:: 158 | 159 | QPath('/Name="Hello"' && MaxDepth=∞') 160 | 161 | ============ 162 | QPath兼容性问题汇总 163 | ============ 164 | 165 | 由于历史原因,QTA的各个UI自动化的驱动器在QPath和UI元素查找的实现上略有差异,特汇总如下。 166 | 167 | --------------- 168 | QT4A的首级Id无限深度查找 169 | --------------- 170 | 171 | 对于QT4A的QPath,如果QPath的第一层的Selector只有一个Id的属性,则在搜索UI元素的时候遍历的深度为无限。比如下面的QPath:: 172 | 173 | QPath('/Id="Hello"') 174 | 175 | 在QT4A中等价于下面的“伪”QPath:: 176 | 177 | QPath('/Id="Hello"' && MaxDepth=∞') 178 | 179 | 因为不存在值为“无限”的数值,所以上面的QPath其实是不合法的。 180 | 181 | 182 | --------------- 183 | QT4A的Instance属性 184 | --------------- 185 | 对于QT4A的QPath,如果Instance的取值类型为字符串,则Instance的值表示的是第N个元素(从1开始计算); 186 | 如果Instance的取值类型为数值,则Instance的值表示的是第N-1元素(从0开始计算)。 187 | 比如下面的QPath:: 188 | 189 | QPath('/Id="Hello" && Instance="1" ') 190 | 191 | 和下面的QPath是等价的:: 192 | 193 | QPath('/Id="Hello" && Instance=0 ') 194 | 195 | 一般来说,在QT4A中推荐使用第二种用法;第二种用法和其他平台的驱动器的与语义是一致的。 196 | 197 | 198 | 199 | 200 | 201 | 202 | -------------------------------------------------------------------------------- /docs/tuia/quickstart.rst: -------------------------------------------------------------------------------- 1 | 快速入门 2 | ==== 3 | 4 | 在编写UI自动化测试之前,您需要先准备好以下的工具和开发环境: 5 | 6 | * Python运行环境 7 | * QTAF(`testbase `_ & `tuia `_) 8 | * 一个适用于对应平台QTA测试驱动器,如QT4A 9 | 10 | 在开始之前,您需要先创建一个QTA测试自动化项目,并掌握基本的测试用例组织形式和设计方法,详情请参考《`QTA Testbase文档 `_》。 11 | 12 | .. note:: 为了简单,下文示例代码使用QTA的一个驱动器————QT4X,QT4X是一个示例的QTA测试驱动器,支持对“QT4X SUT UI框架”实现的应用的自动化测试。“QT4X SUT UI框架”其实并不是一个真正的UI应用框架,而只是用于QTA驱动器的测试和学习目的设计,基于Python的XML etree实现的“伪”框架。实际使用时,可以针对测试的平台选择对应的驱动器进行替换。 13 | 14 | 15 | -------- 16 | 准备一个测试项目 17 | -------- 18 | 可以使用QTAF包创建一个空测试项目,比如:: 19 | 20 | $ qta-manage createproject foo 21 | 22 | 创建成功后,安装对应平台的“测试驱动器”:: 23 | 24 | $ pip install https://github.com/qtacore/QT4x/archive/master.zip 25 | 26 | ------- 27 | 封装一个App 28 | ------- 29 | 30 | 对于QTA,一个App表示一个被测的应用。我们在测试项目的foolib目录下创建一个新的Python模块“fooapp.py”:: 31 | 32 | from qt4x.app import App 33 | 34 | class FooApp(App): 35 | 36 | ENTRY = "qt4x_sut.demo" 37 | PORT = 16000 38 | 39 | .. note:: 由于平台的差异,各个QTA的驱动器封装App的方式略不同,具体需要参考对应驱动器的文档。对于QT4X,这里“ENTRY”指定的是一个Python的模块名,标识被测应用代码的入口;而“PORT”指定的是测试桩RPC服务的监听端口。 40 | 41 | 42 | ---------- 43 | 封装一个Window 44 | ---------- 45 | 46 | 对于QTA,一个Window表示被测应用的一个用户界面。一般来说,QTA的Window的概念和被测平台都能找到对应的概念,比如Android的Activity、iOS的Window、Win32平台上的窗口。 47 | QTA的窗口的封装是为了告知QTA框架以下的信息: 48 | * 如果找到一个窗口 49 | * 窗口内的UI元素的布局信息 50 | 51 | 因此,在封装窗口的时候,我们需要获取被测应用界面的窗口和对应的布局信息,这就需要利用对应平台下的UI布局查看工具: 52 | 53 | ======== ====== ======================================== 54 | 平台 驱动器 UI查看工具 55 | ======== ====== ======================================== 56 | Android QT4A AndroidUISpy 57 | iOS QT4i iOSUISpy 58 | Win32 QT4C QTAInspect 59 | Web QT4W 可以在具体平台上的工具内部调起相关工具 60 | ======== ====== ======================================== 61 | 62 | 对于QT4X,我们的UI窗口是由一个XML定义的,具体可以查看“qt4x_sut.demo”模块定义的窗口布局XML,比如主窗口布局如下:: 63 | 64 | MAIN_LAYOUT = """ 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | """ 80 | 81 | 被测应用在创建时注册了主窗口:: 82 | 83 | class TextEditorApp(App): 84 | '''text editor app 85 | ''' 86 | def on_created(self): 87 | self.register_window("Main", MAIN_LAYOUT) 88 | ... 89 | 90 | 可以看到注册的窗口标识符为“Main”,因此我们封装对应的窗口:: 91 | 92 | from qt4x.app import Window 93 | 94 | class MainWindow(Window): 95 | """main window 96 | """ 97 | NAME = "Main" 98 | 99 | .. note:: 由于平台的差异,各个QTA的驱动器封装Window的方式略不同,甚至一些平台下的类名字都不同,具体需要参考对应驱动器的文档。对于QT4X SUT的UI框架,一个窗口可以通过其注册时的标识符唯一确定,所以QT4X要求窗口封装的时候需要指定这个“标识符”,也就是“NAME”属性 100 | 101 | ------ 102 | 指定窗口布局 103 | ------ 104 | 105 | 以上的窗口的封装只是告诉测试框架如何找到这个窗口,还需要指定这个窗口的布局。所谓窗口的布局,即是一个窗口中的UI元素的信息,包括: 106 | 107 | * 名称————便于后面使用这个UI元素 108 | * UI元素类型————指定这个UI元素可以被如何使用 109 | * UI元素的定位器————如何定位此UI元素 110 | 111 | QTA框架通过在窗口类的updateLocator接口来设置窗口的布局信息,比如对应上面的窗口,我们封装一个UI元素的布局:: 112 | 113 | from qt4x.controls import Button, Window 114 | from qt4x.qpath import QPath 115 | 116 | class MainWindow(Window): 117 | """main window 118 | """ 119 | NAME = "Main" 120 | 121 | def __init__(self, app): 122 | super(MainWindow, self).__init__(app) 123 | self.update_locator({ 124 | "About Button": {"type": Button, "root": self, "locator": QPath("/name='menu' /name='about_btn' && MaxDepth=4 ")} 125 | }) 126 | 127 | 这里是重载了窗口的构造函数,并追加调用updateLocator接口来设置窗口布局。 128 | 129 | .. note:: 虽然在各个平台下的都是通过updateLocator来设置窗口布局,但是由于各个平台下的Window类的构造函数的参数可能不同,重装构造函数时需要注意。 130 | 131 | 从updateLocator的调用参数看,窗口布局是一个Python的dict结构: 132 | 133 | * 字典的键就是UI元素的“名称”,主要用于后面使用这个UI元素时所谓索引,一般这个名称建议是一个可读性较好且和被测应用的功能业务相关的名字; 134 | * 字典的值也是一个Python的字典,标识UI元素的属性,字典包括一下内容: 135 | 136 | * type: 标识UI元素元素的类型,对应一个Python类 137 | * locator:UI元素元素的定位器 138 | * root:UI元素元素定位查找时的使用的根节点UI元素 139 | 140 | .. note:: UI元素属性的type和root的属性有可能不是必填属性,具体请参考对应平台的驱动器的接口 141 | 142 | 在上面的简单的例子中,我们定义了一个“About Button”的按钮(Button)UI元素。这里存在两个问题,一是,在实际的UI布局的封装的时候,该如何为一个UI元素指定一个准确的UI元素类型呢? 143 | QTA框架对UI元素的类型并不做任何校验和检查的,也就是说,如果指定了一个UI元素类型为Button,则这个UI元素就可以当作一个Button来使用,即使这个UI元素实际上并不是一个Button,而是一个InputUI元素也是可以在QTA框架中当作Button来使用(当然,最终执行的结果会导致异常)。 144 | 因此,选择一个UI元素的类型就是看需要对这个UI元素进行哪些操作,一般来说,QTA各个平台下的基础UI元素类型在名字上都会尽量和对应的UI框架的UI元素类名一致,但也不排除有例外的情况。在选择UI元素的类型时,可以查看QTA各个平台的驱动器的UI元素类定义对应的Python模块: 145 | 146 | ======== ====== ======================================== 147 | 平台 驱动器 Python模块 148 | ======== ====== ======================================== 149 | Android QT4A qt4a.andrcontrols 150 | iOS QT4i qt4i.icontrols 151 | Win32 QT4C tuia.wincontrols 152 | Web QT4W qt4w.webcontrols 153 | Demo QT4X qt4x.controls 154 | ======== ====== ======================================== 155 | 156 | UI元素定位的第二个问题就是如何设计“UI元素的定位器”。由于在目前已知的所有的UI框架中,UI元素的组织结构都是树状的,包括我们这里使用的QT4X使用的XML结构也是树状的。 157 | 因此,QTA的UI元素的定位的本质就是在树状的结构中准确找到指定的节点,为此,QTA框架需要有两个参数: 158 | 159 | * 起始查找节点,也就是前面U元素属性的“root”参数就是用于指定UI树查找的根节点 160 | * UI元素定位器 161 | 162 | UI树查找的根节点很容易理解,来看看UI元素的定位器。QTA框架使用了多种类型的UI元素的定位器: 163 | 164 | * QPath:QTA最主要的UI元素定位 165 | * XPath:主要用于Web UI元素的定位,XPath有W3C定义(https://www.w3.org/TR/xpath/) 166 | * str:各个平台下意义不同,一般是QPath或XPath的一种简便形式 167 | 168 | 由于XPath W3C是标准定义的语言,这里就不再赘述。 169 | 而QPath是QTA框架定义的一种简单的UI定位语言,以上面的QPath的例子:: 170 | 171 | QPath("/name='menu' /name='about_btn' && MaxDepth=4 ") 172 | 173 | 以上的QPath查找控件的过程如下: 174 | 175 | 1. 从根节点(窗口容器)开始查找其直接孩子节点,如果节点对应的UI元素的属性存在name属性且取值为menu,则执行第二级查找 176 | 2. 开始第二级查找,以第一次查找的结果对象的UI元素为根,查找深度小于或等于4的全部孩子节点对应的UI元素,如果其属性存在name属性且取值为about_btn,则就是要定位的目标UI元素 177 | 178 | 以上只是简单的例子,更多关于QPath的语法和使用方法说明,可以参考《:doc:`./uilocator`》。 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | -------------------------------------------------------------------------------- /docs/tuia/tuia.rst: -------------------------------------------------------------------------------- 1 | 接口文档 2 | ==== 3 | 4 | Submodules 5 | ---------- 6 | 7 | tuia.qpathparser module 8 | ----------------------- 9 | 10 | .. automodule:: tuia.qpathparser 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | tuia.qpathparser module 16 | ----------------------- 17 | 18 | .. automodule:: tuia.exceptions 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | Module contents 24 | --------------- 25 | 26 | .. automodule:: tuia 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | -------------------------------------------------------------------------------- /qta-manage.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Tencent is pleased to support the open source community by making QTA available. 4 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved. 5 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this 6 | # file except in compliance with the License. You may obtain a copy of the License at 7 | # 8 | # https://opensource.org/licenses/BSD-3-Clause 9 | # 10 | # Unless required by applicable law or agreed to in writing, software distributed 11 | # under the License is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS 12 | # OF ANY KIND, either express or implied. See the License for the specific language 13 | # governing permissions and limitations under the License. 14 | # 15 | from testbase.management import qta_manage_main 16 | 17 | if __name__ == "__main__": 18 | qta_manage_main() 19 | -------------------------------------------------------------------------------- /qta_statics/TestReport.xsl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 55 | 56 | 57 |
58 | 测试报告链接: 59 | 点击这里 60 |
61 |
测试运行环境:
62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 |
主机名
操作系统
72 |
测试运行时间:
73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 |
Run开始时间
Run结束时间
Run执行时间
87 |
测试用例汇总:
88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 98 | 101 | 104 | 105 |
用例总数通过用例数失败用例数
96 | 97 | 99 | 100 | 102 | 103 |
106 |
加载失败模块:
107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 121 | 122 | 123 | 124 |
模块名失败Log
117 | 118 | 119 | Log 120 |
125 |
测试用例详细信息:
126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 162 | 163 | 164 | 165 |
测试结果测试用例负责人用例描述用例状态用例Log
失败 144 | 145 | 146 | Log 147 |
通过 158 | 159 | 160 | Log 161 |
166 | 167 | 168 |
169 |
-------------------------------------------------------------------------------- /qta_statics/TestResult.xsl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | ]> 16 | 18 | 19 | 20 | 21 | 22 | 75 | 76 | 77 |
78 | 79 | 80 | 81 | 84 | 85 | 94 | 95 | 96 | 97 | 100 | 101 | 104 | 105 | 106 | 107 | 110 | 111 | 114 | 115 | 116 | 117 | 120 | 121 | 124 | 125 |
用例名字: 82 | 83 | 运行结果: 86 | 87 | 88 | color: #00FF00 89 | color: #FF0000 90 | 91 | 92 | 93 |
开始时间: 98 | 99 | 负责人: 102 | 103 |
结束时间: 108 | 109 | 优先级: 112 | 113 |
运行时间: 118 | 119 | 用例超时: 122 | 分钟 123 |
126 |
127 | 128 | 129 | 130 |
131 | 132 | 133 | 134 | 135 | 136 |
137 | 138 | 142 | 143 |
144 | 145 | 146 | 147 |
148 |
149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | [] 158 | 159 | 160 | 161 | 162 | 163 | 通过 164 | 失败 165 | 166 | 167 |
168 |
169 | 170 | 171 | padding:2px 2px; background-color:#B3E8B8 172 | 173 | 174 | 175 | 176 | padding:2px 2px; background-color:#F5BCBD 177 | 178 | 179 | 180 | 181 | 184 | 189 | 194 | 200 | 201 |
182 | 步骤: 183 | 185 | 186 | 187 | 188 | 190 |   191 | 192 | 193 | 195 |   196 | 197 | 198 | 199 |
202 |
203 |
204 | 205 | 206 |
207 |
208 | 209 | 210 | 211 | DEBUG: 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | INFO: 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | WARNING: 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | ERROR: 236 | 237 | 238 | 239 |
240 | 					
241 | 				
242 | 243 | 244 | 245 |
246 | 247 | 248 |
249 | 250 | 251 |   期望值: 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 |   实际值: 260 | 261 | 262 | 263 | 264 | 265 |
-------------------------------------------------------------------------------- /qta_statics/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tencent/QTAF/7a6510695344ec59cdc51433ee8f1bb24881b13d/qta_statics/__init__.py -------------------------------------------------------------------------------- /qtaf_settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Tencent is pleased to support the open source community by making QTA available. 4 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved. 5 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this 6 | # file except in compliance with the License. You may obtain a copy of the License at 7 | # 8 | # https://opensource.org/licenses/BSD-3-Clause 9 | # 10 | # Unless required by applicable law or agreed to in writing, software distributed 11 | # under the License is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS 12 | # OF ANY KIND, either express or implied. See the License for the specific language 13 | # governing permissions and limitations under the License. 14 | # 15 | """ 16 | QTAF配置文件 17 | """ 18 | # ----------------------------------- 19 | # 调试模式开关 20 | # ----------------------------------- 21 | DEBUG = False 22 | 23 | # ----------------------------------- 24 | # 全局数据驱动配置 25 | # ----------------------------------- 26 | DATA_DRIVE = False 27 | DATA_SOURCE = "test/data/server.py" 28 | 29 | # ----------------------------------- 30 | # 项目配置 31 | # ----------------------------------- 32 | PROJECT_NAME = "qtaf" 33 | PROJECT_MODE = "standalone" # choices: standard/standalone 34 | PROJECT_ROOT = None # os.path.dirname(__file__) 35 | INSTALLED_APPS = [] 36 | 37 | 38 | # ----------------------------------- 39 | # Assert 40 | # ----------------------------------- 41 | QTAF_REWRITE_ASSERT = True 42 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | six >= 1.11.0 2 | ply == 3.11 3 | setuptools 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Tencent is pleased to support the open source community by making QTA available. 4 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved. 5 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this 6 | # file except in compliance with the License. You may obtain a copy of the License at 7 | # 8 | # https://opensource.org/licenses/BSD-3-Clause 9 | # 10 | # Unless required by applicable law or agreed to in writing, software distributed 11 | # under the License is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS 12 | # OF ANY KIND, either express or implied. See the License for the specific language 13 | # governing permissions and limitations under the License. 14 | # 15 | 16 | import codecs 17 | import os 18 | from setuptools import setup, find_packages 19 | 20 | BASE_DIR = os.path.dirname(os.path.abspath(__file__)) 21 | 22 | 23 | def generate_version(): 24 | version = "1.0.0" 25 | if os.path.isfile(os.path.join(BASE_DIR, "version.txt")): 26 | with codecs.open("version.txt", "r", encoding="utf-8") as fd: 27 | content = fd.read().strip() 28 | if content: 29 | version = content 30 | with codecs.open( 31 | os.path.join(BASE_DIR, "testbase", "version.py"), "w", encoding="utf-8" 32 | ) as fd: 33 | fd.write('version = "%s"\n' % version) 34 | return str(version) 35 | 36 | 37 | def parse_requirements(): 38 | reqs = [] 39 | if os.path.isfile(os.path.join(BASE_DIR, "requirements.txt")): 40 | with codecs.open( 41 | os.path.join(BASE_DIR, "requirements.txt"), "r", encoding="utf-8" 42 | ) as fd: 43 | for line in fd.readlines(): 44 | line = line.strip() 45 | if line: 46 | reqs.append(line) 47 | return reqs 48 | 49 | 50 | def get_description(): 51 | with codecs.open(os.path.join(BASE_DIR, "README.md"), "r", encoding="utf-8") as fh: 52 | return fh.read() 53 | 54 | 55 | if __name__ == "__main__": 56 | setup( 57 | version=generate_version(), 58 | name="qtaf", 59 | packages=find_packages( 60 | exclude=( 61 | "tests", 62 | "tests.*", 63 | ) 64 | ), 65 | py_modules=["qtaf_settings", "__main__", "qta-manage"], 66 | include_package_data=True, 67 | package_data={ 68 | "": ["*.txt", "*.TXT"], 69 | }, 70 | description="Basic test automation framework for QTA", 71 | long_description=get_description(), 72 | long_description_content_type="text/markdown", 73 | author="Tencent", 74 | license="Copyright(c)2010-2018 Tencent All Rights Reserved. ", 75 | install_requires=parse_requirements(), 76 | entry_points={ 77 | "console_scripts": ["qta-manage = testbase.management:qta_manage_main"], 78 | }, 79 | classifiers=[ 80 | "Programming Language :: Python :: 2.7", 81 | "Operating System :: OS Independent", 82 | ], 83 | url="https://github.com/Tencent/QTAF", 84 | project_urls={ 85 | "TestBase Documentation": "https://qta-testbase.readthedocs.io/zh/latest/", 86 | "TUIA Documentation": "https://qta-tuia.readthedocs.io/zh/latest/", 87 | }, 88 | ) 89 | -------------------------------------------------------------------------------- /testbase/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Tencent is pleased to support the open source community by making QTA available. 4 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved. 5 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this 6 | # file except in compliance with the License. You may obtain a copy of the License at 7 | # 8 | # https://opensource.org/licenses/BSD-3-Clause 9 | # 10 | # Unless required by applicable law or agreed to in writing, software distributed 11 | # under the License is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS 12 | # OF ANY KIND, either express or implied. See the License for the specific language 13 | # governing permissions and limitations under the License. 14 | # 15 | """测试框架类库 16 | """ 17 | 18 | from .testcase import TestCase, TestCaseStatus, TestCasePriority 19 | from .version import version 20 | -------------------------------------------------------------------------------- /testbase/compat/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Tencent is pleased to support the open source community by making QTA available. 4 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved. 5 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this 6 | # file except in compliance with the License. You may obtain a copy of the License at 7 | # 8 | # https://opensource.org/licenses/BSD-3-Clause 9 | # 10 | # Unless required by applicable law or agreed to in writing, software distributed 11 | # under the License is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS 12 | # OF ANY KIND, either express or implied. See the License for the specific language 13 | # governing permissions and limitations under the License. 14 | # 15 | """兼容库 16 | """ 17 | 18 | import sys 19 | 20 | if sys.version_info[0] == 3 and sys.version_info[1] >= 12: 21 | from . import _imp as imp 22 | else: 23 | import imp 24 | -------------------------------------------------------------------------------- /testbase/compat/_imp.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Tencent is pleased to support the open source community by making QTA available. 4 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved. 5 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this 6 | # file except in compliance with the License. You may obtain a copy of the License at 7 | # 8 | # https://opensource.org/licenses/BSD-3-Clause 9 | # 10 | # Unless required by applicable law or agreed to in writing, software distributed 11 | # under the License is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS 12 | # OF ANY KIND, either express or implied. See the License for the specific language 13 | # governing permissions and limitations under the License. 14 | # 15 | """imp兼容库 16 | """ 17 | 18 | import importlib.util 19 | import itertools 20 | import sys 21 | 22 | 23 | def find_module(module_name, path_list=None): 24 | # 查找模块的规范 25 | spec = None 26 | if not path_list: 27 | spec = importlib.util.find_spec(module_name) 28 | else: 29 | _path_list = [] 30 | for path in path_list: 31 | if isinstance(path, list): 32 | _path_list.extend(path) 33 | else: 34 | _path_list.append(path) 35 | for path in _path_list: 36 | sys.path.insert(0, path) 37 | spec = importlib.util.find_spec(module_name) 38 | sys.path.pop(0) 39 | if spec: 40 | break 41 | 42 | if spec is None: 43 | raise ImportError("Module %s not found" % module_name) 44 | 45 | # 获取模块的文件路径 46 | module_file = spec.origin 47 | module_path = spec.submodule_search_locations if hasattr(spec, 'submodule_search_locations') else None 48 | 49 | # description 是一个元组,包含模块类型和其他信息 50 | description = (spec.loader, module_file, spec) 51 | 52 | return (module_file, module_path, description) 53 | 54 | 55 | def load_module(name, file, path, description): 56 | # load_module传入的name参数是给模块命名用的,不需要实际存在,所以这里没有使用 57 | # 从描述中获取加载器 58 | loader = description[0] 59 | # 使用 loader 加载模块 60 | module = loader.load_module() 61 | return module 62 | -------------------------------------------------------------------------------- /testbase/context.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Tencent is pleased to support the open source community by making QTA available. 4 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved. 5 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this 6 | # file except in compliance with the License. You may obtain a copy of the License at 7 | # 8 | # https://opensource.org/licenses/BSD-3-Clause 9 | # 10 | # Unless required by applicable law or agreed to in writing, software distributed 11 | # under the License is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS 12 | # OF ANY KIND, either express or implied. See the License for the specific language 13 | # governing permissions and limitations under the License. 14 | # 15 | """ 16 | 测试用例执行时上下文 17 | """ 18 | 19 | from testbase.util import ThreadGroupLocal 20 | 21 | 22 | def current_testcase(): 23 | """当前正在执行的用例 24 | 25 | :returns: TestCase 26 | """ 27 | return getattr(ThreadGroupLocal(), "testcase", None) 28 | 29 | 30 | def current_testresult(): 31 | """当前正在执行的用例对应的测试结果 32 | 33 | :returns: TestResult 34 | """ 35 | return getattr(ThreadGroupLocal(), "testresult", None) 36 | 37 | 38 | def current_testcase_local(): 39 | return ThreadGroupLocal() 40 | -------------------------------------------------------------------------------- /testbase/datadrive.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Tencent is pleased to support the open source community by making QTA available. 4 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved. 5 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this 6 | # file except in compliance with the License. You may obtain a copy of the License at 7 | # 8 | # https://opensource.org/licenses/BSD-3-Clause 9 | # 10 | # Unless required by applicable law or agreed to in writing, software distributed 11 | # under the License is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS 12 | # OF ANY KIND, either express or implied. See the License for the specific language 13 | # governing permissions and limitations under the License. 14 | # 15 | '''数据驱动模块 16 | 17 | 使用介绍: 18 | 1、在TestCase类前面加入@DataDrive装饰器,参数为测试数据列表或者字典:: 19 | 20 | @datadrive.DataDrive([1,2,3]) 21 | class Test(tc.TestCase): 22 | def runTest(self): 23 | pass 24 | 25 | @datadrive.DataDrive( 26 | { 27 | 'TEST1':1, 28 | 'TEST2':2, 29 | 'TEST3':3, 30 | } 31 | ) 32 | class Test(tc.TestCase): 33 | def runTest(self): 34 | pass 35 | 36 | 在测试报告中展示用例名字时,若参数为列表的,则在用例后面增加一个索引序号,若参数为字典的,则在用例后面增加上key的名字: 37 | 38 | - 列表显示为:: 39 | 40 | Test#1 41 | Test#2 42 | Test#3 43 | 44 | - 字典显示为:: 45 | 46 | Test#TEST1 47 | Test#TEST2 48 | Test#TEST3 49 | 50 | 2、可以在runTest里通过self.casedata使用测试数据:: 51 | 52 | def runTest(self): 53 | print(self.casedata) 54 | 55 | 3、运行及调试方法和原来一样:: 56 | 57 | MyTest().run() 58 | 59 | 60 | 完整的例子如下:: 61 | 62 | # -*- coding: utf-8 -*- 63 | 64 | import testbase.testcase as tc 65 | import testbase.datadrive as datadrive 66 | 67 | @datadrive.DataDrive([1,2,3]) 68 | class MyTest(tc.TestCase): 69 | """test 70 | """ 71 | 72 | owner = 'foo' 73 | priority = tc.TestCase.EnumPriority.High 74 | status = tc.TestCase.EnumStatus.Ready 75 | timeout = 5 76 | 77 | def runTest(self): 78 | print 'runTest, casedata:', self.casedata 79 | 80 | 81 | if __name__ == '__main__': 82 | MyTest().run() 83 | 84 | ''' 85 | from __future__ import absolute_import 86 | 87 | import types 88 | 89 | import six 90 | 91 | from testbase import logger 92 | from testbase.conf import settings 93 | from testbase.testcase import TestCase 94 | from testbase.util import translate_bad_char, has_bad_char, smart_text 95 | 96 | 97 | def translate_bad_char4exclude_keys(data_key): 98 | if isinstance(data_key, six.string_types): 99 | data_key = smart_text(data_key) 100 | else: 101 | data_key = str(data_key) 102 | data_key = translate_bad_char(data_key) 103 | return data_key 104 | 105 | 106 | class DataDrive(object): 107 | """数据驱动类修饰器,标识一个测试用例类使用数据驱动""" 108 | 109 | def __init__(self, case_data): 110 | """构造函数 111 | 112 | :param case_datas: 数据驱动测试数据集 113 | :type case_datas: list/tuple/dict 114 | """ 115 | self._case_data = case_data 116 | 117 | def __call__(self, testcase_class): 118 | """修饰器 119 | 120 | :param testcase_class: 要修饰的测试用例 121 | :type testcase_class: TestCase 122 | """ 123 | if not issubclass(testcase_class, TestCase): 124 | raise TypeError( 125 | "data driver decorator cannot be applied to non-TestCase type" 126 | ) 127 | testcase_class.__qtaf_datadrive__ = self 128 | return testcase_class 129 | 130 | def __iter__(self): 131 | """遍历全部的数据名""" 132 | if isinstance(self._case_data, types.GeneratorType): 133 | self._case_data = list(self._case_data) 134 | if isinstance(self._case_data, list) or isinstance(self._case_data, tuple): 135 | for it in range(len(self._case_data)): 136 | yield it 137 | else: 138 | for it in self._case_data: 139 | yield it 140 | 141 | def __getitem__(self, name): 142 | """获取对应名称的数据""" 143 | return self._case_data[name] 144 | 145 | def __len__(self): 146 | return len(self._case_data) 147 | 148 | 149 | def is_datadrive(obj): 150 | """是否为数据驱动用例 151 | 152 | :param obj: 测试用例或测试用例类 153 | :type obj: TestCase/type 154 | 155 | :returns boolean 156 | """ 157 | return hasattr(obj, "__qtaf_datadrive__") 158 | 159 | 160 | def get_datadrive(obj): 161 | """获取对应用例的数据驱动 162 | 163 | :param obj: 测试用例或测试用例类 164 | :type obj: TestCase/type 165 | 166 | :returns DataDrive 167 | """ 168 | return obj.__qtaf_datadrive__ 169 | 170 | 171 | def _get_translated_in_datadrive(name, dd): 172 | if isinstance(name, six.string_types): 173 | name = smart_text(name) 174 | else: 175 | name = str(name) 176 | translated_name = translate_bad_char(name) 177 | for item in dd: 178 | if isinstance(item, six.string_types): 179 | item_string = smart_text(item) 180 | else: 181 | item_string = str(item) 182 | if translated_name == translate_bad_char(item_string): 183 | return dd[item] 184 | else: 185 | raise ValueError("data drive name '%s' not found" % name) 186 | 187 | 188 | def _translate_bad_char4exclude_keys(data_key): 189 | if isinstance(data_key, six.string_types): 190 | data_key = smart_text(data_key) 191 | else: 192 | data_key = str(data_key) 193 | data_key = translate_bad_char(data_key) 194 | return data_key 195 | 196 | 197 | def load_datadrive_tests(cls, name=None, exclude_data_key=None, attrs=None): 198 | """加载对应数据驱动测试用例类的数据驱动用例""" 199 | if is_datadrive(cls): 200 | dd = get_datadrive(cls) 201 | else: 202 | if not settings.DATA_DRIVE: 203 | raise RuntimeError("DATA_DRIVE is not set to True") 204 | 205 | from testbase.loader import TestDataLoader 206 | 207 | dd = TestDataLoader().load() 208 | 209 | if name: 210 | if name in dd: 211 | drive_data = {name: dd[name]} 212 | else: 213 | drive_value = _get_translated_in_datadrive(name, dd) 214 | drive_data = {name: drive_value} 215 | else: 216 | drive_data = dd 217 | 218 | if exclude_data_key is None: 219 | exclude_data_key = [] 220 | 221 | exclude_data_key = [ 222 | _translate_bad_char4exclude_keys(item) for item in exclude_data_key 223 | ] 224 | 225 | tests = [] 226 | for item in drive_data: 227 | # 如果有排除标签 228 | exclude_item = _translate_bad_char4exclude_keys(item) 229 | if exclude_data_key is not None and exclude_item in exclude_data_key: 230 | continue 231 | testdata = drive_data[item] 232 | if isinstance(item, six.string_types): 233 | item = smart_text(item) 234 | else: 235 | item = str(item) 236 | casedata_name = item 237 | if has_bad_char(item): 238 | casedata_name = translate_bad_char(item) 239 | warn_msg = ( 240 | '[WARNING]%r\'s drive data key should use "%s" instead of "%s"' 241 | % (cls, casedata_name, item) 242 | ) 243 | logger.warn(warn_msg) 244 | 245 | if isinstance(testdata, dict) and "__attrs__" in testdata: 246 | new_attrs = testdata.get("__attrs__") 247 | else: 248 | new_attrs = None 249 | 250 | if attrs: 251 | if not new_attrs: 252 | new_attrs = attrs 253 | else: 254 | new_attrs.update(attrs) 255 | 256 | tests.append(cls(testdata, casedata_name, new_attrs)) 257 | return tests 258 | -------------------------------------------------------------------------------- /testbase/exlib.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Tencent is pleased to support the open source community by making QTA available. 4 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved. 5 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this 6 | # file except in compliance with the License. You may obtain a copy of the License at 7 | # 8 | # https://opensource.org/licenses/BSD-3-Clause 9 | # 10 | # Unless required by applicable law or agreed to in writing, software distributed 11 | # under the License is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS 12 | # OF ANY KIND, either express or implied. See the License for the specific language 13 | # governing permissions and limitations under the License. 14 | # 15 | """ 16 | 扩展库管理(仅独立模式使用) 17 | """ 18 | 19 | import os 20 | import zipfile 21 | import shutil 22 | import logging 23 | import xml.dom.minidom as dom 24 | 25 | from testbase.util import codecs_open 26 | 27 | 28 | class ExLibManager(object): 29 | """扩展库管理器""" 30 | 31 | def __init__(self, proj_root): 32 | self._top_dir = proj_root 33 | self._installed_libs = os.path.join( 34 | self._top_dir, "exlib", "installed_libs.txt" 35 | ) 36 | 37 | def _get_egg_name(self, egg_path): 38 | """获取egg包名称""" 39 | with zipfile.ZipFile(egg_path, "r") as egg: 40 | with egg.open("EGG-INFO/PKG-INFO", "r") as fd: 41 | for line in fd.readlines(): 42 | if line.startswith("Name:"): 43 | return line.split(":")[1].strip() 44 | 45 | def install(self, egg_path): 46 | """安装""" 47 | # 获取全部的顶级package 48 | toplv_pkgs = [] 49 | with zipfile.ZipFile(egg_path, "r") as egg: 50 | for f in egg.filelist: 51 | items = f.filename.split("/") 52 | if len(items) == 2 and items[1].lower() == "__init__.py": 53 | toplv_pkgs.append(items[0]) 54 | 55 | exlib_dir = os.path.join(self._top_dir, "exlib") 56 | if not os.path.isdir(exlib_dir): 57 | os.makedirs(exlib_dir) 58 | 59 | # 移除老的包 60 | egg_name = self._get_egg_name(egg_path) 61 | egg_paths_remove = [] 62 | if egg_name: 63 | for file_name in os.listdir(exlib_dir): 64 | if not file_name.lower().endswith(".egg"): 65 | continue 66 | file_path = os.path.join(exlib_dir, file_name) 67 | if not os.path.isfile(file_path): 68 | continue 69 | if egg_name == self._get_egg_name(file_path): 70 | os.remove(file_path) 71 | egg_paths_remove.append(file_path) 72 | 73 | # 拷贝包 74 | shutil.copy(egg_path, exlib_dir) 75 | 76 | # 记录到installed_libs.txt 77 | installed_pkgs = self.list_names() 78 | for pkg in toplv_pkgs[:]: 79 | if pkg in installed_pkgs: 80 | toplv_pkgs.remove(pkg) 81 | if toplv_pkgs: 82 | with codecs_open(self._installed_libs, "a+") as fd: 83 | for pkg in toplv_pkgs: 84 | fd.write(pkg + "\n") 85 | 86 | self._update_pydev_conf(egg_path, egg_paths_remove) 87 | 88 | def _update_pydev_conf(self, egg_path, egg_paths_remove): 89 | """修改pydev配置""" 90 | egg_names_remove = [] 91 | for it in egg_paths_remove: 92 | egg_names_remove.append(os.path.basename(it).lower()) 93 | egg_name = os.path.basename(egg_path) 94 | 95 | # if len(egg_names_remove) == 1 and egg_names_remove[0] == egg_name: 96 | # return 97 | 98 | pydev_path = os.path.join(self._top_dir, ".pydevproject") 99 | if not os.path.isfile(pydev_path): 100 | logging.warn("pydev configure file not found") 101 | return 102 | doc = dom.parse(pydev_path) 103 | for propnode in doc.getElementsByTagName("pydev_pathproperty"): 104 | if propnode.getAttribute("name") == "org.python.pydev.PROJECT_SOURCE_PATH": 105 | break 106 | else: 107 | propnode = doc.createElement("pydev_pathproperty") 108 | propnode.setAttribute("name", "org.python.pydev.PROJECT_SOURCE_PATH") 109 | projnodes = doc.getElementsByTagName("pydev_project") 110 | if not projnodes: 111 | logging.warn("pydev configure file corrupted") 112 | return 113 | projnodes[0].AppendChild(propnode) 114 | 115 | for pathnode in propnode.getElementsByTagName("path"): 116 | file_path = pathnode.childNodes[0].data 117 | name = file_path[file_path.rfind("/") + 1 :] 118 | if name.lower() in egg_names_remove: 119 | propnode.removeChild(pathnode) 120 | 121 | pathnode = doc.createElement("path") 122 | print(egg_name) 123 | pathnode.appendChild( 124 | doc.createTextNode("/${PROJECT_DIR_NAME}/exlib/%s" % egg_name) 125 | ) 126 | propnode.appendChild(pathnode) 127 | with codecs_open(pydev_path, "w", "utf8") as fd: 128 | fd.write(doc.toxml(encoding="UTF-8")) 129 | 130 | def list_names(self): 131 | """获取全部的扩展包的名字""" 132 | if not os.path.isfile(self._installed_libs): 133 | return [] 134 | names = [] 135 | with codecs_open(self._installed_libs, "r", encoding="utf-8") as fd: 136 | for libname in fd.readlines(): 137 | libname = libname.strip("\r\n ") 138 | if not libname: 139 | continue 140 | names.append(libname) 141 | return names 142 | -------------------------------------------------------------------------------- /testbase/logger.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Tencent is pleased to support the open source community by making QTA available. 4 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved. 5 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this 6 | # file except in compliance with the License. You may obtain a copy of the License at 7 | # 8 | # https://opensource.org/licenses/BSD-3-Clause 9 | # 10 | # Unless required by applicable law or agreed to in writing, software distributed 11 | # under the License is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS 12 | # OF ANY KIND, either express or implied. See the License for the specific language 13 | # governing permissions and limitations under the License. 14 | # 15 | """log模块 16 | """ 17 | 18 | import logging 19 | import sys 20 | import traceback 21 | import inspect 22 | from testbase import context 23 | from testbase.util import ensure_binary_stream, smart_binary 24 | 25 | _stream, _encoding = ensure_binary_stream(sys.stdout) 26 | 27 | class _Formatter(logging.Formatter): 28 | def format(self, record): 29 | s = super(_Formatter, self).format(record) 30 | return smart_binary(s, encoding=_encoding) 31 | 32 | 33 | _stream_handler = logging.StreamHandler(_stream) 34 | _stream_handler.terminator = b"\n" 35 | _stream_handler.setFormatter(_Formatter()) 36 | 37 | 38 | class TestResultBridge(logging.Handler): 39 | """中转log信息到TestResult""" 40 | 41 | def emit(self, log_record): 42 | """Log Handle 必须实现此函数""" 43 | testresult = context.current_testresult() 44 | formatted_msg = self.format(log_record) 45 | if testresult is None: 46 | _stream_handler.emit(log_record) 47 | return 48 | record = {} 49 | if log_record.exc_info: 50 | record["traceback"] = "".join( 51 | traceback.format_tb(log_record.exc_info[2]) 52 | ) + "%s: %s" % (log_record.exc_info[0].__name__, log_record.exc_info[1]) 53 | testresult.log_record(log_record.levelno, formatted_msg, record) 54 | 55 | 56 | _LOGGER_NAME = "QTA_LOGGER" 57 | _logger = logging.getLogger(_LOGGER_NAME) 58 | _logger.setLevel(logging.DEBUG) 59 | _testresult_bridge = TestResultBridge() 60 | _logger.addHandler(_testresult_bridge) 61 | 62 | 63 | def critical(msg, *args, **kwargs): 64 | _logger.error(msg, *args, **kwargs) 65 | 66 | 67 | fatal = critical 68 | 69 | 70 | def error(msg, *args, **kwargs): 71 | """Log a message with severity 'ERROR' on the root logger.""" 72 | _logger.error(msg, *args, **kwargs) 73 | 74 | 75 | def exception(msg, *args): 76 | """Log a message with severity 'ERROR' on the root logger,with exception information.""" 77 | _logger.exception(msg, *args) 78 | 79 | 80 | def warning(msg, *args, **kwargs): 81 | """Log a message with severity 'WARNING' on the root logger.""" 82 | _logger.warning(msg, *args, **kwargs) 83 | 84 | 85 | warn = warning 86 | 87 | 88 | def info(msg, *args, **kwargs): 89 | """Log a message with severity 'INFO' on the root logger.""" 90 | _logger.info(msg, *args, **kwargs) 91 | 92 | 93 | def debug(msg, *args, **kwargs): 94 | """Log a message with severity 'DEBUG' on the root logger.""" 95 | _logger.debug(msg, *args, **kwargs) 96 | 97 | 98 | def log(level, msg, *args, **kwargs): 99 | """Log 'msg % args' with the integer severity 'level' on the root logger.""" 100 | _logger.log(level, msg, *args, **kwargs) 101 | 102 | 103 | def addHandler(hdlr): # pylint: disable=invalid-name 104 | """Add the specified handler to this logger.""" 105 | _logger.addHandler(hdlr) 106 | 107 | 108 | def removeHandler(hdlr): # pylint: disable=invalid-name 109 | """Remove the specified handler from this logger.""" 110 | _logger.removeHandler(hdlr) 111 | 112 | def set_formatter(fmt): 113 | """Set the specified formatter to this logger. 114 | """ 115 | class __Formatter(_Formatter): 116 | def __init__(self, fmt): 117 | super(_Formatter, self).__init__(fmt) 118 | 119 | class _CustomFormatter(logging.Formatter): 120 | def format(self, record): 121 | # Get the code line number and file name of the call logger function. 122 | logger_module = "logger" 123 | for frame_info in inspect.stack(): 124 | frame = frame_info[0] 125 | module_name = inspect.getmodulename(frame.f_code.co_filename) 126 | if module_name != "__init__" and module_name != logger_module: 127 | caller = inspect.getframeinfo(frame) 128 | break 129 | else: 130 | return super(_CustomFormatter, self).format(record) 131 | record.filename = caller.filename.split('/')[-1] 132 | record.lineno = caller.lineno 133 | return super(_CustomFormatter, self).format(record) 134 | 135 | _stream_handler.setFormatter(__Formatter(fmt)) 136 | _testresult_bridge.setFormatter(_CustomFormatter(fmt)) 137 | 138 | def set_level(level): 139 | """Set the specified log level to this logger. 140 | """ 141 | _logger.setLevel(level) 142 | -------------------------------------------------------------------------------- /testbase/plan.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | # 3 | # Tencent is pleased to support the open source community by making QTA available. 4 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved. 5 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this 6 | # file except in compliance with the License. You may obtain a copy of the License at 7 | # 8 | # https://opensource.org/licenses/BSD-3-Clause 9 | # 10 | # Unless required by applicable law or agreed to in writing, software distributed 11 | # under the License is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS 12 | # OF ANY KIND, either express or implied. See the License for the specific language 13 | # governing permissions and limitations under the License. 14 | # 15 | """Test plan 16 | """ 17 | 18 | 19 | class TestPlan(object): 20 | """测试计划""" 21 | 22 | tests = None 23 | test_target_args = {} 24 | 25 | def get_tests(self): 26 | """获取测试用例定义 27 | 28 | :rtypes: str/list(str)/list(TestCase)/TestCaseSettings 29 | """ 30 | if self.tests is None: 31 | raise ValueError("%s's class member tests is not specified" % type(self)) 32 | return self.tests 33 | 34 | def get_test_target(self): 35 | """获取被测对象 36 | 37 | :return: 被测对象信息 38 | :rtypes: dict 39 | """ 40 | if isinstance(self.test_target_args, dict): 41 | return self.test_target_args 42 | raise ValueError( 43 | "`test_target_args` is not a dictionary, please overwrite `get_test_target` for your custom implenmentation" 44 | ) 45 | 46 | def test_setup(self, report): 47 | """测试初始化步骤""" 48 | pass 49 | 50 | def test_teardown(self, report): 51 | """测试清理步骤""" 52 | pass 53 | 54 | def resource_setup(self, report, restype, resource): 55 | """测试资源初始化""" 56 | pass 57 | 58 | def resource_teardown(self, report, restype, resource): 59 | """测试资源清理""" 60 | pass 61 | 62 | def debug_run(self, report=None, resmgr_backend=None): 63 | """调试执行""" 64 | from testbase.runner import TestRunner 65 | from testbase.report import StreamTestReport 66 | 67 | if report is None: 68 | report = StreamTestReport() 69 | return TestRunner(report, resmgr_backend=resmgr_backend).run(self) 70 | -------------------------------------------------------------------------------- /testbase/retry.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Tencent is pleased to support the open source community by making QTA available. 4 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved. 5 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this 6 | # file except in compliance with the License. You may obtain a copy of the License at 7 | # 8 | # https://opensource.org/licenses/BSD-3-Clause 9 | # 10 | # Unless required by applicable law or agreed to in writing, software distributed 11 | # under the License is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS 12 | # OF ANY KIND, either express or implied. See the License for the specific language 13 | # governing permissions and limitations under the License. 14 | # 15 | """重试机制实现 16 | """ 17 | 18 | import time 19 | 20 | 21 | class _RetryItem(object): 22 | """ """ 23 | 24 | def __init__(self, iteration, timestamps=None): 25 | self.__iteration = iteration 26 | self.__ts = timestamps 27 | 28 | @property 29 | def iteration(self): 30 | return self.__iteration 31 | 32 | @property 33 | def ts(self): 34 | return self.__ts 35 | 36 | def __str__(self): 37 | return "<%s iter=%s, ts=%s>" % ( 38 | self.__class__.__name__, 39 | self.__iteration, 40 | self.__ts, 41 | ) 42 | 43 | 44 | class _RetryWithTimeout(object): 45 | """retry mechanism with timeout""" 46 | 47 | def __init__(self, interval=5, timeout=60, raise_error=True): 48 | self.interval = interval 49 | self.timeout = timeout 50 | self.raise_error = raise_error 51 | self.__start_time = None 52 | self.__count = 0 53 | 54 | def __iter__(self): 55 | return self 56 | 57 | def next(self): 58 | if self.__start_time is None: 59 | self.__count += 1 60 | self.__start_time = time.time() 61 | return _RetryItem(self.__count, self.__start_time) 62 | else: 63 | time.sleep(self.interval) 64 | ts = time.time() 65 | if ts - self.__start_time < self.timeout: 66 | self.__count += 1 67 | return _RetryItem(self.__count, ts) 68 | else: 69 | if self.raise_error: 70 | raise RetryLimitExcceeded( 71 | "Procedure retried %s times in %ss" 72 | % (self.__count, self.timeout) 73 | ) 74 | else: 75 | raise StopIteration 76 | 77 | __next__ = next 78 | 79 | 80 | class _RetryWithCount(object): 81 | """retry mechanism with count""" 82 | 83 | def __init__(self, limit=3, interval=0, raise_error=True): 84 | self.limit = limit 85 | self.raise_error = raise_error 86 | self.interval = interval 87 | self.__count = 0 88 | 89 | def __iter__(self): 90 | return self 91 | 92 | def next(self): 93 | self.__count += 1 94 | if self.__count <= self.limit: 95 | if self.__count != 1 and self.interval: 96 | time.sleep(self.interval) 97 | return _RetryItem(self.__count, time.time()) 98 | if self.raise_error: 99 | raise RetryLimitExcceeded( 100 | "Procedure retried for %s times with interval=%ss" 101 | % (self.limit, self.interval) 102 | ) 103 | else: 104 | raise StopIteration 105 | 106 | __next__ = next 107 | 108 | 109 | class RetryLimitExcceeded(Exception): 110 | """maxmium retry limit excceeded error""" 111 | 112 | pass 113 | 114 | 115 | class Retry(object): 116 | """retry mechanism with timeout or limit""" 117 | 118 | def __init__(self, timeout=10, limit=None, interval=0.5, raise_error=True): 119 | if limit: 120 | self._retry = _RetryWithCount( 121 | limit=limit, interval=interval, raise_error=raise_error 122 | ) 123 | else: 124 | self._retry = _RetryWithTimeout( 125 | timeout=timeout, interval=interval, raise_error=raise_error 126 | ) 127 | 128 | def __iter__(self): 129 | return self._retry.__iter__() 130 | 131 | def call(self, callee, *args, **kwargs): 132 | if not hasattr(callee, "__call__"): 133 | raise ValueError("callee must be a function instance") 134 | for _ in self: 135 | r = callee(*args, **kwargs) 136 | if r: 137 | return r 138 | -------------------------------------------------------------------------------- /testbase/serialization.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Tencent is pleased to support the open source community by making QTA available. 4 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved. 5 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this 6 | # file except in compliance with the License. You may obtain a copy of the License at 7 | # 8 | # https://opensource.org/licenses/BSD-3-Clause 9 | # 10 | # Unless required by applicable law or agreed to in writing, software distributed 11 | # under the License is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS 12 | # OF ANY KIND, either express or implied. See the License for the specific language 13 | # governing permissions and limitations under the License. 14 | # 15 | """ 16 | 测试用例序列化和反序列化 17 | """ 18 | 19 | import sys 20 | import pickle 21 | from testbase.testsuite import TestSuiteBase 22 | 23 | 24 | class _EmptyClass(object): 25 | pass 26 | 27 | 28 | def dumps(testcase): 29 | """序列化测试用例 30 | 31 | :param testcase: 测试用例 32 | :type testcase: TestCase 33 | """ 34 | if isinstance(testcase, TestSuiteBase): 35 | return {"id": testcase.suite_class_name, "data": pickle.dumps(testcase.dumps())} 36 | else: 37 | return { 38 | "id": testcase.test_class_name, 39 | "data": pickle.dumps(testcase.casedata), 40 | "dname": testcase.casedataname, 41 | } 42 | 43 | 44 | def loads(buf): 45 | """反序列化测试用例 46 | 47 | :param buf: 测试用例序列化数据 48 | :type buf: dict 49 | :returns: TestCase 50 | """ 51 | testname = buf["id"] 52 | items = testname.split(".") 53 | classname = items[-1] 54 | modulename = ".".join(items[0:-1]) 55 | if not modulename: # main module 56 | modulename = "__main__" 57 | else: 58 | __import__(modulename) 59 | module = sys.modules[modulename] 60 | testclass = getattr(module, classname) 61 | data = pickle.loads(buf["data"]) 62 | if issubclass(testclass, TestSuiteBase): 63 | obj = _EmptyClass() 64 | obj.__class__ = testclass 65 | obj.loads(data) 66 | return obj 67 | else: 68 | attrs = None 69 | if isinstance(data, dict) and "__attrs__" in data: 70 | attrs = data.get("__attrs__") 71 | return testclass(data, buf["dname"], attrs) 72 | -------------------------------------------------------------------------------- /testbase/test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Tencent is pleased to support the open source community by making QTA available. 4 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved. 5 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this 6 | # file except in compliance with the License. You may obtain a copy of the License at 7 | # 8 | # https://opensource.org/licenses/BSD-3-Clause 9 | # 10 | # Unless required by applicable law or agreed to in writing, software distributed 11 | # under the License is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS 12 | # OF ANY KIND, either express or implied. See the License for the specific language 13 | # governing permissions and limitations under the License. 14 | # 15 | """a module for unittest 16 | """ 17 | import os 18 | import threading 19 | 20 | from testbase.conf import settings 21 | 22 | 23 | class _NotExistedItem(object): 24 | pass 25 | 26 | 27 | class modify_settings(object): # pylint: disable=invalid-name 28 | """temporarily modify settings""" 29 | 30 | def __init__(self, **kwargs): 31 | self.new_conf = kwargs 32 | self.old_conf = {} 33 | 34 | def __enter__(self): 35 | settings._Settings__ensure_loaded() # ensure loaded 36 | settings._Settings__sealed = False 37 | for key in self.new_conf: 38 | self.old_conf[key] = getattr(settings, key, _NotExistedItem()) 39 | if isinstance(self.old_conf[key], _NotExistedItem): 40 | settings._Settings__keys.add(key) 41 | setattr(settings, key, self.new_conf[key]) 42 | 43 | def __exit__(self, *args): 44 | for key in self.old_conf: 45 | old_value = self.old_conf[key] 46 | if isinstance(old_value, _NotExistedItem): 47 | delattr(settings, key) 48 | settings._Settings__keys.remove(key) 49 | else: 50 | setattr(settings, key, old_value) 51 | settings._Settings__sealed = True 52 | 53 | 54 | class modify_environ(object): # pylint: disable=invalid-name 55 | """temporarily modify envrion""" 56 | 57 | def __init__(self, **kwargs): 58 | self.new_conf = kwargs 59 | self.old_conf = {} 60 | 61 | def __enter__(self): 62 | for key in self.new_conf: 63 | self.old_conf[key] = os.environ.get(key, _NotExistedItem()) 64 | os.environ[key] = self.new_conf[key] 65 | 66 | def __exit__(self, *args): 67 | for key in self.old_conf: 68 | old_value = self.old_conf[key] 69 | if isinstance(old_value, _NotExistedItem): 70 | del os.environ[key] 71 | else: 72 | os.environ[key] = old_value 73 | 74 | 75 | class modify_attributes(object): # pylint: disable=invalid-name 76 | """temporarily modify attributes""" 77 | 78 | _lock = threading.Lock() 79 | 80 | def __init__(self, target, key_values): 81 | self._target = target 82 | self._old_values = {} 83 | self._new_values = key_values 84 | for key in key_values.keys(): 85 | self._old_values[key] = getattr(target, key) 86 | 87 | def __enter__(self): 88 | self._lock.acquire() 89 | for key in self._new_values: 90 | setattr(self._target, key, self._new_values[key]) 91 | 92 | def __exit__(self, *args): 93 | try: 94 | for key in self._old_values: 95 | setattr(self._target, key, self._old_values[key]) 96 | finally: 97 | self._lock.release() 98 | -------------------------------------------------------------------------------- /testbase/types.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Tencent is pleased to support the open source community by making QTA available. 4 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved. 5 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this 6 | # file except in compliance with the License. You may obtain a copy of the License at 7 | # 8 | # https://opensource.org/licenses/BSD-3-Clause 9 | # 10 | # Unless required by applicable law or agreed to in writing, software distributed 11 | # under the License is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS 12 | # OF ANY KIND, either express or implied. See the License for the specific language 13 | # governing permissions and limitations under the License. 14 | # 15 | """qtaf related types 16 | """ 17 | 18 | import traceback 19 | 20 | import pkg_resources 21 | 22 | from testbase import runner, report, resource, logger 23 | 24 | RUNNER_ENTRY_POINT = "qtaf.runner" 25 | REPORT_ENTRY_POINT = "qtaf.report" 26 | RESMGR_BACKEND_ENTRY_POINT = "qtaf.resmgr_backend" 27 | 28 | runner_types = {} 29 | report_types = {} 30 | resmgr_backend_types = {} 31 | 32 | 33 | def __init_runner_types(): 34 | global runner_types # pylint: disable=invalid-name 35 | if runner_types: 36 | return 37 | runner_types["basic"] = runner.TestRunner 38 | runner_types["multithread"] = runner.ThreadingTestRunner 39 | runner_types["multiprocess"] = runner.MultiProcessTestRunner 40 | for ep in pkg_resources.iter_entry_points(RUNNER_ENTRY_POINT): 41 | if ep.name not in runner_types: 42 | try: 43 | runner_types[ep.name] = ep.load() 44 | except Exception: # pylint: disable=broad-except 45 | stack = traceback.format_exc() 46 | logger.warn( 47 | "load TestRunner type for %s failed:\n%s" % (ep.name, stack) 48 | ) 49 | 50 | 51 | def __init_report_types(): 52 | global report_types # pylint: disable=invalid-name 53 | if report_types: 54 | return 55 | report_types.update( 56 | { 57 | "empty": report.EmptyTestReport, 58 | "stream": report.StreamTestReport, 59 | "xml": report.XMLTestReport, 60 | "json": report.JSONTestReport, 61 | "html": report.HtmlTestReport, 62 | } 63 | ) 64 | 65 | # Register other `ITestReport` implementations from entry points 66 | for ep in pkg_resources.iter_entry_points(REPORT_ENTRY_POINT): 67 | if ep.name not in report_types: 68 | try: 69 | report_types[ep.name] = ep.load() 70 | except Exception: # pylint: disable=broad-except 71 | stack = traceback.format_exc() 72 | logger.warn( 73 | "load ITestReport entry point for %s failed:\n%s" % (ep.name, stack) 74 | ) 75 | 76 | 77 | def __init_resmgr_backend_types(): 78 | global resmgr_backend_types # pylint: disable=invalid-name 79 | if resmgr_backend_types: 80 | return 81 | resmgr_backend_types["local"] = resource.LocalResourceManagerBackend 82 | for ep in pkg_resources.iter_entry_points(RESMGR_BACKEND_ENTRY_POINT): 83 | if ep.name not in resmgr_backend_types: 84 | resmgr_backend_types[ep.name] = ep.load() 85 | 86 | 87 | __init_runner_types() 88 | del __init_runner_types 89 | 90 | __init_report_types() 91 | del __init_report_types 92 | 93 | __init_resmgr_backend_types() 94 | del __init_resmgr_backend_types 95 | -------------------------------------------------------------------------------- /testbase/version.py: -------------------------------------------------------------------------------- 1 | version = "5.2.4" # pylint: disable=invalid-name 2 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | # 3 | # Tencent is pleased to support the open source community by making QTA available. 4 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved. 5 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this 6 | # file except in compliance with the License. You may obtain a copy of the License at 7 | # 8 | # https://opensource.org/licenses/BSD-3-Clause 9 | # 10 | # Unless required by applicable law or agreed to in writing, software distributed 11 | # under the License is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS 12 | # OF ANY KIND, either express or implied. See the License for the specific language 13 | # governing permissions and limitations under the License. 14 | # 15 | """unit tests entry 16 | """ 17 | import argparse 18 | import importlib 19 | import pkgutil 20 | import os 21 | import re 22 | import sys 23 | import traceback 24 | import unittest 25 | 26 | test_dir = os.path.dirname(os.path.abspath(__file__)) 27 | sys.path.insert(0, os.path.dirname(test_dir)) 28 | sys.path.insert(0, test_dir) 29 | loader = unittest.TestLoader() 30 | 31 | 32 | if not hasattr(unittest.TestCase, "assertRegex"): 33 | unittest.TestCase.assertRegex = unittest.TestCase.assertRegexpMatches 34 | 35 | 36 | def load_case(case_path): 37 | parts = case_path.split(".") 38 | temp_parts = parts[:] 39 | 40 | mod = None 41 | while temp_parts: 42 | try: 43 | mod_name = ".".join(temp_parts) 44 | mod = importlib.import_module(mod_name) 45 | break 46 | except Exception: # pylint: disable=broad-except 47 | temp_parts.pop() 48 | if not mod: 49 | raise RuntimeError("case path=%s cannot be imported." % case_path) 50 | 51 | case_name = ".".join(parts[len(temp_parts) :]) 52 | test_suites = [] 53 | if case_name: 54 | test_suites.append(loader.loadTestsFromName(case_name, mod)) 55 | else: 56 | if hasattr(mod, "__path__"): 57 | for _, mod_name, _ in pkgutil.walk_packages(mod.__path__): 58 | mod_name = mod.__name__ + "." + mod_name 59 | sub_mod = importlib.import_module(mod_name) 60 | test_suites.append(loader.loadTestsFromModule(sub_mod)) 61 | else: 62 | test_suites.append(loader.loadTestsFromModule(mod)) 63 | return test_suites 64 | 65 | 66 | def load_cases(tests): 67 | test_suite = unittest.TestSuite() 68 | for test in tests: 69 | try: 70 | test_suite.addTests(load_case(test)) 71 | except: 72 | print('cannot import test "%s":\n%s' % (test, traceback.format_exc())) 73 | raise 74 | return test_suite 75 | 76 | 77 | def main(verbosity, tests): 78 | if not tests: 79 | tests = ["test_testbase", "test_tuia"] 80 | test_suite = load_cases(tests) 81 | runner = unittest.TextTestRunner(verbosity=10 + verbosity) 82 | raise SystemExit(not runner.run(test_suite).wasSuccessful()) 83 | 84 | 85 | if __name__ == "__main__": 86 | parser = argparse.ArgumentParser() 87 | parser.add_argument( 88 | "-v", 89 | metavar="v", 90 | nargs="*", 91 | dest="verbosity", 92 | help="verbose level for result output", 93 | ) 94 | parser.add_argument( 95 | "tests", 96 | metavar="TEST", 97 | nargs="*", 98 | help="a python style module path for testcase set, eg: hello.MyTestCase", 99 | ) 100 | args = parser.parse_args(sys.argv[1:]) 101 | if args.verbosity: 102 | verbosity = len(args.verbosity) 103 | else: 104 | verbosity = 0 105 | main(verbosity, args.tests) 106 | -------------------------------------------------------------------------------- /tests/data/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /tests/data/server.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 测试数据文件 4 | """ 5 | 6 | WTLOGIN = {"HOST": "wtlogin.qq.com", "PORT": 80} 7 | 8 | SSO = [ 9 | {"HOST": "114.12.12.30", "PORT": 8080}, 10 | {"HOST": "114.12.12.31", "PORT": 8080}, 11 | {"HOST": "114.12.12.32", "PORT": 8080}, 12 | ] 13 | 14 | DATASET = [ 15 | { 16 | "SSO": it, 17 | "WTLOGIN": WTLOGIN, 18 | } 19 | for it in SSO 20 | ] 21 | 22 | 23 | DEFAULT_KEY = 0 24 | -------------------------------------------------------------------------------- /tests/sampletest/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import testbase 4 | 5 | class InitTest(testbase.TestCase): 6 | """QT4i测试用例""" 7 | 8 | owner = "foo" 9 | status = testbase.TestCase.EnumStatus.Ready 10 | timeout = 0.1 11 | priority = testbase.TestCase.EnumPriority.Normal 12 | 13 | def run_test(self): 14 | pass -------------------------------------------------------------------------------- /tests/sampletest/datatest.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 数据驱动测试用例 4 | """ 5 | 6 | import testbase 7 | from testbase import datadrive 8 | from testbase import context 9 | 10 | 11 | @datadrive.DataDrive( 12 | { 13 | "TEST1": 1, 14 | "TEST2": 2, 15 | "TEST3": 3, 16 | } 17 | ) 18 | class DataTest(testbase.TestCase): 19 | """数据驱动测试用例""" 20 | 21 | owner = "foo" 22 | status = testbase.TestCase.EnumStatus.Ready 23 | timeout = 0.1 24 | priority = testbase.TestCase.EnumPriority.Normal 25 | 26 | def runTest(self): # pylint: disable=invalid-name 27 | self.logInfo(str(self.casedata)) 28 | 29 | 30 | @datadrive.DataDrive([0]) 31 | class SingleDataTest(testbase.TestCase): 32 | """数据驱动测试用例""" 33 | 34 | owner = "foo" 35 | status = testbase.TestCase.EnumStatus.Ready 36 | timeout = 0.1 37 | priority = testbase.TestCase.EnumPriority.Normal 38 | 39 | def runTest(self): # pylint: disable=invalid-name 40 | self.logInfo(str(self.casedata)) 41 | 42 | 43 | @datadrive.DataDrive([]) 44 | class EmptyDataTest(testbase.TestCase): 45 | """数据驱动测试用例""" 46 | 47 | owner = "foo" 48 | status = testbase.TestCase.EnumStatus.Ready 49 | timeout = 0.1 50 | priority = testbase.TestCase.EnumPriority.Normal 51 | 52 | def runTest(self): # pylint: disable=invalid-name 53 | self.logInfo(str(self.casedata)) 54 | 55 | 56 | @datadrive.DataDrive(["A", "V", "XX", 0]) 57 | class ArrayDataTest(testbase.TestCase): 58 | """数据驱动测试用例""" 59 | 60 | owner = "foo" 61 | status = testbase.TestCase.EnumStatus.Ready 62 | timeout = 0.1 63 | priority = testbase.TestCase.EnumPriority.Normal 64 | 65 | def runTest(self): # pylint: disable=invalid-name 66 | self.logInfo(str(self.casedata)) 67 | 68 | 69 | class ProjDataTest(testbase.TestCase): 70 | """项目级别数据驱动测试用例""" 71 | 72 | owner = "foo" 73 | status = testbase.TestCase.EnumStatus.Ready 74 | timeout = 0.1 75 | priority = testbase.TestCase.EnumPriority.Normal 76 | 77 | def runTest(self): # pylint: disable=invalid-name 78 | self.logInfo(str(context.current_testcase().casedata)) 79 | self.logInfo(str(self.casedata)) 80 | 81 | 82 | bad_names = [ 83 | "foo test", 84 | "a&b", 85 | "2*2", 86 | "5-1", 87 | "foo|bar", 88 | "ok?", 89 | "about", 90 | "10/2", 91 | "go~", 92 | "a=1", 93 | "a(good)", 94 | "b[bad]", 95 | ] 96 | 97 | bad_drive_data = dict(zip(bad_names, bad_names)) 98 | 99 | 100 | @datadrive.DataDrive(bad_drive_data) 101 | class BadCharCaseTest(testbase.TestCase): 102 | """bad char test""" 103 | 104 | owner = "foo" 105 | timeout = 1 106 | priority = testbase.TestCase.EnumPriority.High 107 | status = testbase.TestCase.EnumStatus.Ready 108 | 109 | def run_test(self): 110 | self.log_info('bad char test\'s case name is "%s"' % self.test_name) 111 | 112 | 113 | if __name__ == "__main__": 114 | # DataTest().run() 115 | # DataTest(3).run() 116 | ProjDataTest().run() 117 | -------------------------------------------------------------------------------- /tests/sampletest/loaderr.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 模块描述 4 | """ 5 | import testbase 6 | 7 | 8 | class HelloTest(testbase.TestCase): 9 | """测试示例""" 10 | 11 | owner = "foo" 12 | status = testbase.TestCase.EnumStatus.Ready 13 | timeout = 1 14 | priority = testbase.TestCase.EnumPriority.Normal 15 | 16 | def __init__(self): 17 | pass 18 | 19 | def runTest(self): # pylint: disable=invalid-name 20 | # ----------------------------- 21 | self.startStep("测试webcontrols.WebElement构造函数") 22 | # ----------------------------- 23 | 24 | 25 | raise RuntimeError("load error on this module") 26 | -------------------------------------------------------------------------------- /tests/sampletest/paramtest.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 参数测试用例 4 | """ 5 | 6 | import testbase 7 | 8 | 9 | class ParamTestWithoutAddParams(testbase.TestCase): 10 | """参数测试用例""" 11 | 12 | owner = "foo" 13 | status = testbase.TestCase.EnumStatus.Ready 14 | timeout = 0.1 15 | priority = testbase.TestCase.EnumPriority.Normal 16 | 17 | def runTest(self): # pylint: disable=invalid-name 18 | self.assert_("不存在test", "test" not in self.__dict__) 19 | self.assert_("不存在test1", "test1" not in self.__dict__) 20 | 21 | 22 | class ParamTest(testbase.TestCase): 23 | """参数测试用例""" 24 | 25 | owner = "foo" 26 | status = testbase.TestCase.EnumStatus.Ready 27 | timeout = 0.1 28 | priority = testbase.TestCase.EnumPriority.Normal 29 | 30 | def add_params(self): 31 | self.add_param("test", int, default=100) 32 | self.add_param("test1", int, default=100) 33 | 34 | def runTest(self): # pylint: disable=invalid-name 35 | self.assert_("存在test", "test" in self.__dict__) 36 | self.assert_("存在test1", "test1" in self.__dict__) 37 | 38 | self.assert_equal("断言test", self.test, 100) 39 | self.assert_equal("断言test1", self.test1, 100) 40 | 41 | 42 | class ParamOverWriteTest(testbase.TestCase): 43 | """参数测试用例""" 44 | 45 | owner = "foo" 46 | status = testbase.TestCase.EnumStatus.Ready 47 | timeout = 0.1 48 | priority = testbase.TestCase.EnumPriority.Normal 49 | 50 | def add_params(self): 51 | self.add_param("test", int, default=100) 52 | self.add_param("test1", int, default=100) 53 | 54 | def runTest(self): # pylint: disable=invalid-name 55 | self.assert_("存在test", "test" in self.__dict__) 56 | self.assert_("存在test1", "test1" in self.__dict__) 57 | 58 | self.assert_equal("断言test", self.test, 200) 59 | self.assert_equal("断言test1", self.test1, 400) 60 | 61 | 62 | if __name__ == "__main__": 63 | ParamTest().run() 64 | -------------------------------------------------------------------------------- /tests/sampletest/repeattest.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 一个用例重复执行 4 | """ 5 | 6 | from testbase import TestCase 7 | from testbase.testcase import RepeatTestCaseRunner 8 | 9 | 10 | class RepeatTest(TestCase): 11 | """测试示例""" 12 | 13 | owner = "foo" 14 | status = TestCase.EnumStatus.Ready 15 | timeout = 1 16 | priority = TestCase.EnumPriority.Normal 17 | case_runner = RepeatTestCaseRunner() 18 | repeat = 5 19 | 20 | def runTest(self): # pylint: disable=invalid-name 21 | self.logInfo("第%s次测试执行" % self.iteration) 22 | 23 | 24 | if __name__ == "__main__": 25 | RepeatTest().run() 26 | -------------------------------------------------------------------------------- /tests/sampletest/runnertest.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """test case for runner test 3 | """ 4 | 5 | from testbase import TestCase 6 | from testbase import testresult 7 | 8 | class SuccTest(TestCase): 9 | """测试示例""" 10 | 11 | timeout = 1 12 | owner = "foo" 13 | status = TestCase.EnumStatus.Ready 14 | priority = TestCase.EnumPriority.Normal 15 | expect_passed = True 16 | 17 | def run_test(self): 18 | pass 19 | 20 | 21 | class ErrLogTest(TestCase): 22 | """测试示例""" 23 | 24 | timeout = 1 25 | owner = "foo" 26 | status = TestCase.EnumStatus.Ready 27 | priority = TestCase.EnumPriority.Normal 28 | expect_passed = False 29 | 30 | def run_test(self): 31 | self.test_result.error("error") 32 | 33 | 34 | class FailedTest(TestCase): 35 | """测试示例""" 36 | 37 | timeout = 1 38 | owner = "foo" 39 | status = TestCase.EnumStatus.Ready 40 | priority = TestCase.EnumPriority.Normal 41 | expect_passed = False 42 | 43 | def run_test(self): 44 | self.fail("error") 45 | 46 | 47 | class ExceptTest(TestCase): 48 | """测试示例""" 49 | 50 | timeout = 1 51 | owner = "foo" 52 | status = TestCase.EnumStatus.Ready 53 | priority = TestCase.EnumPriority.Normal 54 | expect_passed = False 55 | 56 | def run_test(self): 57 | raise RuntimeError("fault") 58 | 59 | class FilterCustomTest(TestCase): 60 | """测试示例""" 61 | 62 | timeout = 1 63 | owner = "foo" 64 | status = TestCase.EnumStatus.Ready 65 | priority = TestCase.EnumPriority.Normal 66 | expect_passed = True 67 | 68 | def pre_test(self): 69 | return testresult.TestResultType.FILTERED, "xxx" 70 | 71 | def run_test(self): 72 | raise RuntimeError 73 | 74 | class ExceptTest2(TestCase): 75 | """测试示例""" 76 | 77 | timeout = 1 78 | owner = "foo" 79 | status = TestCase.EnumStatus.Ready 80 | priority = TestCase.EnumPriority.Normal 81 | expect_passed = False 82 | 83 | def run_test(self): 84 | raise RuntimeError("fault") 85 | 86 | 87 | if __name__ == "__main__": 88 | SuccTest().debug_run() 89 | -------------------------------------------------------------------------------- /tests/sampletest/seqtest.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 按照顺序执行用例(伪实现) 4 | """ 5 | 6 | from testbase import TestCase 7 | from testbase.testcase import RepeatTestCaseRunner 8 | 9 | 10 | class TestA(TestCase): 11 | """测试示例""" 12 | 13 | timeout = 1 14 | owner = "foo" 15 | status = TestCase.EnumStatus.Ready 16 | priority = TestCase.EnumPriority.Normal 17 | 18 | def run_test(self): 19 | pass 20 | 21 | 22 | class TestB(TestCase): 23 | """测试示例""" 24 | 25 | timeout = 1 26 | owner = "foo" 27 | status = TestCase.EnumStatus.Ready 28 | priority = TestCase.EnumPriority.Normal 29 | case_runner = RepeatTestCaseRunner() 30 | repeat = 4 31 | 32 | def run_test(self): 33 | pass 34 | 35 | 36 | class TestC(TestCase): 37 | """测试示例""" 38 | 39 | timeout = 1 40 | owner = "foo" 41 | status = TestCase.EnumStatus.Ready 42 | priority = TestCase.EnumPriority.Normal 43 | 44 | def run_test(self): 45 | pass 46 | 47 | 48 | __qtaf_seq_tests__ = [TestA, TestB, TestC] 49 | 50 | if __name__ == "__main__": 51 | 52 | from testbase.testcase import debug_run_all 53 | 54 | debug_run_all() 55 | -------------------------------------------------------------------------------- /tests/sampletest/sharedatatest.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 参数测试用例 4 | """ 5 | 6 | import time 7 | 8 | import testbase 9 | 10 | 11 | class AddShareDataTest(testbase.TestCase): 12 | """参数测试用例""" 13 | 14 | owner = "foo" 15 | status = testbase.TestCase.EnumStatus.Ready 16 | timeout = 0.1 17 | priority = testbase.TestCase.EnumPriority.Normal 18 | 19 | def runTest(self): # pylint: disable=invalid-name 20 | self.add_share_data("test1", 100), 21 | self.add_share_data("test2", {"a": "b", "b": 123, "c": [1, 2, 3]}) 22 | 23 | 24 | class GetShareDataTest(testbase.TestCase): 25 | """参数测试用例""" 26 | 27 | owner = "foo" 28 | status = testbase.TestCase.EnumStatus.Ready 29 | timeout = 1 30 | priority = testbase.TestCase.EnumPriority.Normal 31 | 32 | def runTest(self): # pylint: disable=invalid-name 33 | time.sleep(0.5) 34 | test1 = self.get_share_data("test1") 35 | test2 = self.get_share_data("test2") 36 | self.assert_equal("assert test1", test1, 100) 37 | self.assert_equal("assert test2", test2, {"a": "b", "b": 123, "c": [1, 2, 3]}) 38 | 39 | 40 | class RemoveShareDataTest(testbase.TestCase): 41 | """参数测试用例""" 42 | 43 | owner = "foo" 44 | status = testbase.TestCase.EnumStatus.Ready 45 | timeout = 0.1 46 | priority = testbase.TestCase.EnumPriority.Normal 47 | 48 | def runTest(self): # pylint: disable=invalid-name 49 | self.remove_share_data("test1") 50 | -------------------------------------------------------------------------------- /tests/sampletest/suitetest.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | import time 5 | 6 | from testbase.testcase import TestCase 7 | from testbase.testsuite import TestSuite 8 | 9 | class HelloTest(TestCase): 10 | """测试示例""" 11 | 12 | owner = "foo" 13 | status = TestCase.EnumStatus.Ready 14 | timeout = 1 15 | priority = TestCase.EnumPriority.Normal 16 | 17 | def run_test(self): 18 | # ----------------------------- 19 | self.startStep("测试") 20 | # ----------------------------- 21 | with open("1.txt") as fp: 22 | self.assert_("Check file content", fp.read() == "123456") 23 | time.sleep(2) 24 | 25 | # ----------------------------- 26 | self.startStep("Check share data") 27 | # ----------------------------- 28 | value = self.get_share_data("suite") 29 | self.assert_equal("Check share data", value, "add_share_data") 30 | 31 | 32 | class HelloTestSuite(TestSuite): 33 | """HelloTestSuite""" 34 | 35 | owner = "root" 36 | timeout = 5 37 | priority = TestSuite.EnumPriority.High 38 | status = TestSuite.EnumStatus.Design 39 | testcases = ["sampletest.hellotest.HelloTest", HelloTest] 40 | exec_mode = TestSuite.EnumExecMode.Sequential 41 | stop_on_failure = True 42 | 43 | def pre_test(self): 44 | print("This is pre_test") 45 | with open("1.txt", "w") as fp: 46 | fp.write("123456") 47 | self.add_share_data("suite", "add_share_data") 48 | 49 | def post_test(self): 50 | print("This is post_test") 51 | value = self.get_share_data("suite") 52 | assert value == "add_share_data" 53 | self.remove_share_data("suite") 54 | os.remove("1.txt") 55 | 56 | 57 | class HelloTestSuitePreTestFail(HelloTestSuite): 58 | 59 | testcases = [HelloTest] 60 | 61 | def pre_test(self): 62 | super(HelloTestSuitePreTestFail, self).pre_test() 63 | raise RuntimeError("This is pre_test fail") 64 | 65 | 66 | class HelloTestSuitePostTestFail(HelloTestSuite): 67 | 68 | testcases = [HelloTest] 69 | 70 | def post_test(self): 71 | super(HelloTestSuitePostTestFail, self).post_test() 72 | raise RuntimeError("This is post_test fail") 73 | 74 | 75 | class HelloTestSuiteParallel(HelloTestSuite): 76 | testcases = [HelloTest, HelloTest, HelloTest, HelloTest] 77 | exec_mode = TestSuite.EnumExecMode.Parallel 78 | concurrency = 2 79 | 80 | 81 | class HelloTestSuiteFilter(HelloTestSuite): 82 | testcases = ["sampletest.hellotest.HelloTest", "sampletest.hellotest.HelloTest2", HelloTest] 83 | testcase_filter = { 84 | "priorities": [TestCase.EnumPriority.Normal], 85 | "statuses": [TestCase.EnumStatus.Ready], 86 | } 87 | -------------------------------------------------------------------------------- /tests/sampletest/tagtest.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | import testbase 5 | 6 | __qtaf_tags__ = "mod" 7 | 8 | 9 | class TagTest(testbase.TestCase): 10 | """测试示例""" 11 | 12 | owner = "foo" 13 | status = testbase.TestCase.EnumStatus.Ready 14 | timeout = 1 15 | priority = testbase.TestCase.EnumPriority.Normal 16 | tags = "test" 17 | 18 | def runTest(self): # pylint: disable=invalid-name 19 | # ----------------------------- 20 | self.startStep("测试") 21 | # ----------------------------- 22 | self.assert_equal("断言失败", False, True) 23 | 24 | 25 | class TagTest2(testbase.TestCase): 26 | """测试示例""" 27 | 28 | owner = "foo" 29 | status = testbase.TestCase.EnumStatus.Ready 30 | timeout = 1 31 | priority = testbase.TestCase.EnumPriority.Normal 32 | tags = "test2" 33 | 34 | def runTest(self): # pylint: disable=invalid-name 35 | # ----------------------------- 36 | self.startStep("测试") 37 | # ----------------------------- 38 | self.assert_equal("断言失败", False, True) 39 | -------------------------------------------------------------------------------- /tests/sampletest/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "tests": ["tests.sampletest.hellotest.PassedCase"], 3 | "excluded_name": [], 4 | "owner": [], 5 | "priority": ["BVT", "High"], 6 | "status": ["Design", "Ready"], 7 | "tag": [], 8 | "excluded_tag": [], 9 | "working_dir": null, 10 | "stop_on_failure": true, 11 | "execute_type": "random", 12 | "report_type": "stream", 13 | "report_args": "", 14 | "resmgr_backend_type": "local", 15 | "runner_type": "basic", 16 | "runner_args": "", 17 | "share_data": {}, 18 | "global_parameters": {} 19 | } 20 | -------------------------------------------------------------------------------- /tests/sampletest/test_dict.json: -------------------------------------------------------------------------------- 1 | { 2 | "tests": [{"name": "tests.sampletest.paramtest.ParamOverWriteTest", "parameters": {"test": 200, "test1": 400}}], 3 | "excluded_name": [], 4 | "owner": [], 5 | "priority": ["BVT", "High", "Normal"], 6 | "status": ["Design", "Ready"], 7 | "tag": [], 8 | "excluded_tag": [] 9 | } 10 | -------------------------------------------------------------------------------- /tests/sampletest/test_global_parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "tests": [{"name": "tests.sampletest.paramtest.ParamOverWriteTest"}], 3 | "excluded_name": [], 4 | "owner": [], 5 | "priority": ["BVT", "High", "Normal"], 6 | "status": ["Design", "Ready"], 7 | "tag": [], 8 | "excluded_tag": [], 9 | "working_dir": null, 10 | "stop_on_failure": true, 11 | "execute_type": "random", 12 | "report_type": "stream", 13 | "report_args": "", 14 | "resmgr_backend_type": "local", 15 | "runner_type": "basic", 16 | "runner_args": "", 17 | "share_data": {}, 18 | "global_parameters": {"test": 200, "test1": 400} 19 | } 20 | -------------------------------------------------------------------------------- /tests/sampletestplan/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tencent/QTAF/7a6510695344ec59cdc51433ee8f1bb24881b13d/tests/sampletestplan/__init__.py -------------------------------------------------------------------------------- /tests/sampletestplan/hello.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from testbase.plan import TestPlan 4 | from testbase.resource import LocalResourceHandler, LocalResourceManagerBackend 5 | 6 | 7 | class HelloResourceHandler(LocalResourceHandler): 8 | def iter_resource(self, res_type, res_group=None, condition=None): 9 | for i in range(2): 10 | yield {"id": i + 1} 11 | 12 | 13 | LocalResourceManagerBackend.register_resource_type("hello", HelloResourceHandler()) 14 | 15 | 16 | class HelloTestPlan(TestPlan): 17 | tests = "tests.sampletest.runnertest" 18 | test_target_args = {} 19 | 20 | def test_setup(self, report): 21 | report.info("plan", "test_setup") 22 | 23 | def test_teardown(self, report): 24 | report.info("plan", "test_teardown") 25 | 26 | def resource_setup(self, report, res_type, resource): 27 | report.info("plan", "resource_setup-%s-%s" % (res_type, resource["id"])) 28 | 29 | def resource_teardown(self, report, res_type, resource): 30 | report.info("plan", "resource_teardown-%s-%s" % (res_type, resource["id"])) 31 | 32 | 33 | if __name__ == "__main__": 34 | HelloTestPlan().debug_run() 35 | -------------------------------------------------------------------------------- /tests/test_testbase/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Tencent is pleased to support the open source community by making QTA available. 3 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved. 4 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this 5 | # file except in compliance with the License. You may obtain a copy of the License at 6 | # 7 | # https://opensource.org/licenses/BSD-3-Clause 8 | # 9 | # Unless required by applicable law or agreed to in writing, software distributed 10 | # under the License is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS 11 | # OF ANY KIND, either express or implied. See the License for the specific language 12 | # governing permissions and limitations under the License. 13 | # 14 | -------------------------------------------------------------------------------- /tests/test_testbase/test_assert.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Tencent is pleased to support the open source community by making QTA available. 4 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved. 5 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this 6 | # file except in compliance with the License. You may obtain a copy of the License at 7 | # 8 | # https://opensource.org/licenses/BSD-3-Clause 9 | # 10 | # Unless required by applicable law or agreed to in writing, software distributed 11 | # under the License is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS 12 | # OF ANY KIND, either express or implied. See the License for the specific language 13 | # governing permissions and limitations under the License. 14 | # 15 | """testcase for testbase.asserts 16 | """ 17 | 18 | import unittest 19 | 20 | from testbase.testcase import TestCase 21 | from testbase.assertion import _AssertHookedCache 22 | from testbase.datadrive import DataDrive 23 | from testbase.test import modify_settings 24 | 25 | 26 | class AssertionFailureTest(TestCase): 27 | """dummy class for asserts test""" 28 | 29 | owner = "dummy" 30 | timeout = 10 31 | priority = TestCase.EnumPriority.High 32 | status = TestCase.EnumStatus.Ready 33 | 34 | def run_test(self): 35 | self.start_step("step 1") 36 | self.assert_("assert1", self.bar(1) in [1, 4]) 37 | self.start_step("step 2") 38 | self.assert_("assert", self.foo(self.bar(1)) in [2, 4]) 39 | 40 | def foo(self, a): 41 | return a + 1 42 | 43 | def bar(self, b): 44 | return b + 1 45 | 46 | 47 | class AssertionInnerInvokeTest(AssertionFailureTest): 48 | """xxxx""" 49 | 50 | def run_test(self): 51 | self.assert_foo_bar() 52 | 53 | def assert_foo_bar(self): 54 | self.assert_("assert", self.foo(self.bar(1)) in [2, 4]) 55 | 56 | def assert_foo_bar_with_for(self): 57 | for _ in range(2): 58 | self.assert_("assert", self.foo(self.bar(1)) in [2, 4]) 59 | 60 | def assert_foo_bar_with_while(self): 61 | i = 1 62 | while i < 3: 63 | self.assert_("assert", self.foo(self.bar(1)) in [2, 4]) 64 | 65 | def assert_foo_bar_with_dict(self): 66 | i = 1 67 | if i == 1: 68 | self.assert_(message="assert", value=self.foo(self.bar(1)) in [2, 4]) 69 | 70 | 71 | class AssertionInnerInvokeForTest(AssertionFailureTest): 72 | """xxxx""" 73 | 74 | def run_test(self): 75 | self.assert_foo_bar() 76 | 77 | def assert_foo_bar_with_for(self): 78 | for _ in range(2): 79 | self.assert_("assert", self.foo(self.bar(1)) in [2, 4]) 80 | 81 | 82 | @DataDrive([1, 2]) 83 | class AssertionDatadriveTest(AssertionFailureTest): 84 | """dummy class for asserts test""" 85 | 86 | def run_test(self): 87 | self.assert_("assert", self.foo(self.bar(1)) in [2, 4]) 88 | 89 | 90 | class AssertionTest(unittest.TestCase): 91 | """unit test for assertion""" 92 | 93 | def setUp(self): 94 | _AssertHookedCache().clear() 95 | self.case = AssertionFailureTest() 96 | self.code = self.case.run_test.__func__.__code__ 97 | 98 | def tearDown(self): 99 | self.case.run_test.__func__.__code__ = self.code 100 | 101 | def is_func_rewritten(self, new_func, old_code): 102 | new_code = new_func.__func__.__code__ 103 | return new_code != old_code 104 | 105 | def test_assert_failure(self): 106 | case = AssertionFailureTest() 107 | old_run_test_code = case.run_test.__func__.__code__ 108 | case.debug_run() 109 | self.assertEqual(case.test_result.passed, False, "断言失败,用例没有失败") 110 | self.assertEqual( 111 | len(case.test_result._step_results), 2, "设置了断言失败继续执行,但是用例没有继续执行" 112 | ) 113 | self.assertEqual( 114 | self.is_func_rewritten(case.run_test, old_run_test_code), 115 | True, 116 | "重写assert失败,code对象没有改变", 117 | ) 118 | 119 | def test_assert_inner_invoke(self): 120 | case = AssertionInnerInvokeTest() 121 | old_assert_foo_bar_code = case.assert_foo_bar.__func__.__code__ 122 | case.debug_run() 123 | 124 | self.assertEqual(case.test_result.passed, False, "断言失败,用例没有失败") 125 | self.assertEqual( 126 | self.is_func_rewritten(case.assert_foo_bar, old_assert_foo_bar_code), 127 | True, 128 | "重写assert失败,code对象没有改变", 129 | ) 130 | 131 | def test_assert_inner_invoke_with_for(self): 132 | case = AssertionInnerInvokeForTest() 133 | old_assert_foo_bar_with_for_code = ( 134 | case.assert_foo_bar_with_for.__func__.__code__ 135 | ) 136 | case.debug_run() 137 | 138 | self.assertEqual(case.test_result.passed, False, "断言失败,用例没有失败") 139 | self.assertEqual( 140 | self.is_func_rewritten( 141 | case.assert_foo_bar_with_for, old_assert_foo_bar_with_for_code 142 | ), 143 | True, 144 | "重写assert失败,code对象没有改变", 145 | ) 146 | 147 | def test_assert_datadrive(self): 148 | case = AssertionDatadriveTest() 149 | old_run_test_code = case.run_test.__func__.__code__ 150 | 151 | report = case.debug_run_one(0) 152 | self.assertEqual(report.is_passed(), False, "数据驱动断言失败,用例没有失败") 153 | 154 | report = case.debug_run_one(1) 155 | self.assertEqual(report.is_passed(), False, "数据驱动断言失败,用例没有失败") 156 | 157 | self.assertEqual( 158 | self.is_func_rewritten(case.run_test, old_run_test_code), 159 | True, 160 | "重写assert失败,code对象没有改变", 161 | ) 162 | 163 | def test_disable_rewrite_assert(self): 164 | with modify_settings(QTAF_REWRITE_ASSERT=False): 165 | case = AssertionFailureTest() 166 | old_run_test_code = case.run_test.__func__.__code__ 167 | case.debug_run() 168 | self.assertEqual(case.test_result.passed, False, "禁用重写assert,用例没有失败") 169 | self.assertEqual( 170 | self.is_func_rewritten(case.run_test, old_run_test_code), 171 | False, 172 | "禁用重写assert,assert_被重写了", 173 | ) 174 | 175 | def test_disable_assert_failed_continue(self): 176 | with modify_settings(QTAF_ASSERT_CONTINUE=False): 177 | case = AssertionFailureTest() 178 | case.debug_run() 179 | self.assertEqual(case.test_result.passed, False, "断言失败退出执行,用例没有失败") 180 | self.assertEqual( 181 | len(case.test_result._step_results), 1, "设置了断言失败退出执行,但是用例仍继续执行" 182 | ) 183 | 184 | 185 | if __name__ == "__main__": 186 | unittest.main() 187 | -------------------------------------------------------------------------------- /tests/test_testbase/test_conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Tencent is pleased to support the open source community by making QTA available. 4 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved. 5 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this 6 | # file except in compliance with the License. You may obtain a copy of the License at 7 | # 8 | # https://opensource.org/licenses/BSD-3-Clause 9 | # 10 | # Unless required by applicable law or agreed to in writing, software distributed 11 | # under the License is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS 12 | # OF ANY KIND, either express or implied. See the License for the specific language 13 | # governing permissions and limitations under the License. 14 | # 15 | """conf test 16 | """ 17 | 18 | import unittest 19 | 20 | from testbase.conf import settings, SettingsMixin 21 | from testbase.test import modify_settings 22 | 23 | 24 | class SettingTest(unittest.TestCase): 25 | def test_get(self): 26 | """get settings""" 27 | self.assertEqual(settings.DEBUG, False) 28 | self.assertEqual(settings.get("DEBUG"), False) 29 | self.assertEqual(settings.get("NOT_EXIST", False), False) 30 | 31 | def test_set(self): 32 | """set settings failed""" 33 | self.assertRaises(RuntimeError, setattr, settings, "DEBUG", False) 34 | 35 | def test_contain(self): 36 | """test settings in op""" 37 | self.assertEqual( 38 | "DEBUG" in settings, True, "DEBUG should have been in settings" 39 | ) 40 | self.assertEqual( 41 | "IMPOSSIBLE" in settings, False, "IMPOSSIBLE is unexpected in settings" 42 | ) 43 | 44 | def test_iteration(self): 45 | self.assertEqual( 46 | "DEBUG" in list(settings), True, "DEBUG should have been in list(settings)" 47 | ) 48 | 49 | 50 | class Dummy(SettingsMixin): 51 | class Settings(object): 52 | DUMMY_A = 0 53 | 54 | def __init__(self): 55 | self.x = self.settings.DUMMY_A 56 | 57 | 58 | class Dummy2(SettingsMixin): 59 | class Settings(object): 60 | B = 1 61 | 62 | 63 | class Dummy3(SettingsMixin): 64 | class Settings(object): 65 | Dummy3_A = 4 66 | 67 | 68 | class DummyChild0(Dummy): 69 | pass 70 | 71 | 72 | class DummyChild1(Dummy): 73 | class Settings(object): 74 | DUMMYCHILD1_A = 2 75 | 76 | 77 | class DummyChild2(Dummy): 78 | class Settings(object): 79 | DUMMY_A = 1 80 | 81 | 82 | class DummyChild3(Dummy): 83 | class Settings(object): 84 | DUMMYCHILD3_B = -1 85 | 86 | 87 | class SettingsMixinTest(unittest.TestCase): 88 | """test case for settings mixin class""" 89 | 90 | def test_get(self): 91 | self.reset_class_settings(Dummy) 92 | dummy = Dummy() 93 | self.assertEqual(dummy.settings.DUMMY_A, 0) 94 | self.assertRaises(AttributeError, getattr, dummy.settings, "B") 95 | 96 | with modify_settings(GLOBAL_X="xxxx", DUMMY_A=100): 97 | self.reset_class_settings(Dummy) 98 | self.assertEqual(dummy.settings.GLOBAL_X, "xxxx") 99 | self.assertEqual(dummy.settings.DUMMY_A, 100) 100 | 101 | def test_set(self): 102 | self.reset_class_settings(Dummy) 103 | dummy = Dummy() 104 | self.assertRaises(RuntimeError, setattr, dummy.settings, "C", 2) 105 | 106 | def test_declare(self): 107 | self.assertRaises(RuntimeError, getattr, Dummy2(), "settings") 108 | self.assertRaises(RuntimeError, getattr, Dummy3(), "settings") 109 | self.assertRaises(RuntimeError, getattr, Dummy3(), "settings") 110 | 111 | def test_deriving(self): 112 | self.reset_class_settings(DummyChild0) 113 | child = DummyChild0() 114 | self.assertEqual(child.settings.DUMMY_A, 0) 115 | 116 | self.reset_class_settings(DummyChild1) 117 | child = DummyChild1() 118 | self.assertEqual(child.settings.DUMMY_A, 2) 119 | self.assertEqual(child.settings.DUMMYCHILD1_A, 2) 120 | 121 | with modify_settings(DUMMY_A=3): 122 | self.reset_class_settings(Dummy) 123 | dummy = Dummy() 124 | self.assertEqual(dummy.settings.DUMMY_A, 3) 125 | 126 | self.reset_class_settings(DummyChild1) 127 | child = DummyChild1() 128 | self.assertEqual(child.settings.DUMMY_A, 2) 129 | 130 | with modify_settings(DUMMYCHILD1_A=4): 131 | self.reset_class_settings(DummyChild1) 132 | child = DummyChild1() 133 | self.assertEqual(child.settings.DUMMY_A, 4) 134 | self.assertEqual(child.settings.DUMMYCHILD1_A, 4) 135 | 136 | self.assertRaises(RuntimeError, DummyChild2) 137 | 138 | child = DummyChild3() 139 | self.assertEqual(child.settings.DUMMYCHILD3_B, -1) 140 | self.assertRaises(AttributeError, getattr, child.settings, "DUMMYCHILD3_A") 141 | 142 | def reset_class_settings(self, cls): 143 | settings_key = "_%s_settings" % cls.__name__ 144 | if hasattr(cls, settings_key): 145 | delattr(cls, settings_key) 146 | 147 | 148 | if __name__ == "__main__": 149 | unittest.main(defaultTest="SettingsMixinTest.test_deriving") 150 | -------------------------------------------------------------------------------- /tests/test_testbase/test_datadrive.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Tencent is pleased to support the open source community by making QTA available. 4 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved. 5 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this 6 | # file except in compliance with the License. You may obtain a copy of the License at 7 | # 8 | # https://opensource.org/licenses/BSD-3-Clause 9 | # 10 | # Unless required by applicable law or agreed to in writing, software distributed 11 | # under the License is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS 12 | # OF ANY KIND, either express or implied. See the License for the specific language 13 | # governing permissions and limitations under the License. 14 | # 15 | """testcase test 16 | """ 17 | 18 | import unittest 19 | 20 | from testbase import datadrive 21 | from testbase.loader import TestDataLoader 22 | from testbase.test import modify_settings 23 | from testbase.testcase import TestCase 24 | 25 | 26 | class ClassDataDriveTest(unittest.TestCase): 27 | def test_load_dict_data(self): 28 | 29 | data = { 30 | "x": {"value": "x", "xxx": {"owner": "foo"}}, 31 | "y": {"value": 2, "xxx": {"priority": TestCase.EnumPriority.Low}}, 32 | } 33 | 34 | @datadrive.DataDrive(data) 35 | class Demo(TestCase): 36 | owner = "xxx" 37 | timeout = 1 38 | priority = TestCase.EnumPriority.BVT 39 | status = TestCase.EnumStatus.Design 40 | 41 | def run_test(self): 42 | pass 43 | 44 | tests = datadrive.load_datadrive_tests(Demo) 45 | self.assertEqual(len(tests), 2) 46 | for test in tests: 47 | self.assertEqual(test.casedata, data[test.casedataname]) 48 | 49 | def test_load_list_data(self): 50 | 51 | data = ["xxx", 1111] 52 | 53 | @datadrive.DataDrive(data) 54 | class Demo(TestCase): 55 | owner = "xxx" 56 | timeout = 1 57 | priority = TestCase.EnumPriority.BVT 58 | status = TestCase.EnumStatus.Design 59 | 60 | def run_test(self): 61 | pass 62 | 63 | tests = datadrive.load_datadrive_tests(Demo) 64 | self.assertEqual(len(tests), 2) 65 | self.assertEqual(tests[0].casedata, data[0]) 66 | self.assertEqual(tests[1].casedata, data[1]) 67 | 68 | def test_set_attrs(self): 69 | data = [ 70 | {"value": "x", "__attrs__": {"owner": "foo"}}, 71 | {"value": 2, "__attrs__": {"priority": TestCase.EnumPriority.Low}}, 72 | ] 73 | 74 | @datadrive.DataDrive(data) 75 | class Demo(TestCase): 76 | owner = "xxx" 77 | timeout = 1 78 | priority = TestCase.EnumPriority.BVT 79 | status = TestCase.EnumStatus.Design 80 | 81 | def run_test(self): 82 | pass 83 | 84 | tests = datadrive.load_datadrive_tests(Demo) 85 | self.assertEqual(len(tests), 2) 86 | self.assertEqual(tests[0].owner, "foo") 87 | self.assertEqual(tests[0].priority, TestCase.EnumPriority.BVT) 88 | self.assertEqual(tests[1].owner, "xxx") 89 | self.assertEqual(tests[1].priority, TestCase.EnumPriority.Low) 90 | 91 | def test_set_attrs_tags(self): 92 | class Base(TestCase): 93 | tags = "base" 94 | 95 | data = [ 96 | {"value": "x", "__attrs__": {"tags": "foo"}}, 97 | {"value": "x", "__attrs__": {"tags": ("fff", "xxx")}}, 98 | ] 99 | 100 | @datadrive.DataDrive(data) 101 | class Demo(Base): 102 | owner = "xxx" 103 | timeout = 1 104 | priority = TestCase.EnumPriority.BVT 105 | status = TestCase.EnumStatus.Design 106 | tags = "base2" 107 | 108 | def run_test(self): 109 | pass 110 | 111 | tests = datadrive.load_datadrive_tests(Demo) 112 | self.assertEqual(len(tests), 2) 113 | self.assertEqual(tests[0].tags, set(["base", "foo"])) 114 | self.assertEqual(tests[1].tags, set(["base", "fff", "xxx"])) 115 | 116 | def test_set_attrs_doc(self): 117 | data = [ 118 | {"value": "x", "__attrs__": {"__doc__": "doc"}}, 119 | {"value": "x", "__attrs__": {"xxxx": ("fff", "xxx")}}, 120 | ] 121 | 122 | @datadrive.DataDrive(data) 123 | class Demo(TestCase): 124 | """base""" 125 | 126 | owner = "xxx" 127 | timeout = 1 128 | priority = TestCase.EnumPriority.BVT 129 | status = TestCase.EnumStatus.Design 130 | tags = "base2" 131 | 132 | def run_test(self): 133 | pass 134 | 135 | tests = datadrive.load_datadrive_tests(Demo) 136 | self.assertEqual(len(tests), 2) 137 | self.assertEqual(tests[0].test_doc, "doc") 138 | self.assertEqual(tests[1].test_doc, "base") 139 | 140 | 141 | class GlobalDataDriveTest(unittest.TestCase): 142 | def test_global_overwrite_attrs(self): 143 | class Demo(TestCase): 144 | owner = "xxx" 145 | timeout = 1 146 | priority = TestCase.EnumPriority.BVT 147 | status = TestCase.EnumStatus.Design 148 | 149 | def run_test(self): 150 | pass 151 | 152 | data_source = [0, 1, 2] 153 | with modify_settings(DATA_DRIVE=True, DATA_SOURCE=data_source): 154 | drive_data = TestDataLoader().load() 155 | self.assertEqual(len(drive_data), len(data_source)) 156 | 157 | tests = datadrive.load_datadrive_tests(Demo) 158 | self.assertEqual(len(tests), len(data_source)) 159 | for index, test in enumerate(tests): 160 | self.assertEqual(test.casedataname, str(index)) 161 | self.assertEqual(test.casedata, data_source[index]) 162 | 163 | tests = datadrive.load_datadrive_tests(Demo, 1) 164 | self.assertEqual(len(tests), 1) 165 | self.assertEqual(tests[0].casedataname, str(1)) 166 | self.assertEqual(tests[0].casedata, 1) 167 | 168 | data_map = [ 169 | ("a", "owner", "a"), 170 | ("b", "timeout", 10), 171 | ("c", "priority", TestCase.EnumPriority.BVT), 172 | ("d", "status", TestCase.EnumStatus.Implement), 173 | ("e", "__doc__", "e"), 174 | ("f", "tags", set(["abc"])), 175 | ("g", "tags", set(["a", "b", "c"])), 176 | ] 177 | data_source = {} 178 | for char, field, value in data_map: 179 | data_source[char] = {"data_%s" % char: char, "__attrs__": {field: value}} 180 | 181 | with modify_settings(DATA_DRIVE=True, DATA_SOURCE=data_source): 182 | tests = datadrive.load_datadrive_tests(Demo) 183 | self.assertEqual(len(tests), len(data_map)) 184 | for char, field, value in data_map: 185 | test = datadrive.load_datadrive_tests(Demo, char)[0] 186 | self.assertEqual(test.casedataname, char) 187 | self.assertEqual(test.casedata["data_%s" % char], char) 188 | if field == "__doc__": 189 | field = "test_doc" 190 | field_value = getattr(test, field) 191 | self.assertEqual(field_value, value) 192 | 193 | 194 | if __name__ == "__main__": 195 | unittest.main(defaultTest="GlobalDataDriveTest.test_global_overwrite_attrs") 196 | -------------------------------------------------------------------------------- /tests/test_testbase/test_logger.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Tencent is pleased to support the open source community by making QTA available. 4 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved. 5 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this 6 | # file except in compliance with the License. You may obtain a copy of the License at 7 | # 8 | # https://opensource.org/licenses/BSD-3-Clause 9 | # 10 | # Unless required by applicable law or agreed to in writing, software distributed 11 | # under the License is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS 12 | # OF ANY KIND, either express or implied. See the License for the specific language 13 | # governing permissions and limitations under the License. 14 | # 15 | """logger test 16 | """ 17 | 18 | import sys 19 | import logging 20 | import unittest 21 | from testbase import logger 22 | if sys.version_info < (3, 3): 23 | from imp import reload 24 | else: 25 | from importlib import reload 26 | 27 | class TestHandler(logging.Handler): 28 | def __init__(self, *args, **kwargs): 29 | super(logging.Handler, self).__init__(*args, **kwargs) 30 | self.messages = [] 31 | 32 | def emit(self, record): 33 | self.messages.append(self.format(record)) 34 | 35 | def format(self, record): 36 | return self.formatter.format(record) 37 | 38 | class LoggerTest(unittest.TestCase): 39 | 40 | 41 | def test_set_formatter(self): 42 | try: 43 | handler = TestHandler() 44 | logger._stream_handler = handler 45 | 46 | logger.set_formatter('[%(message)s]') 47 | logger.info('www') 48 | last = handler.messages[-1] 49 | 50 | self.assertIn(b'[www]', last) 51 | finally: 52 | reload(logger) 53 | 54 | def test_set_log_level(self): 55 | try: 56 | handler = TestHandler() 57 | logger._stream_handler = handler 58 | 59 | logger.set_level(logging.ERROR) 60 | logger.info('www') 61 | 62 | self.assertTrue(len(handler.messages) == 0) 63 | finally: 64 | reload(logger) 65 | 66 | if __name__ == '__main__': 67 | unittest.main() 68 | -------------------------------------------------------------------------------- /tests/test_testbase/test_plan.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import unittest 4 | from testbase.report import StreamTestReport 5 | 6 | 7 | class PlanTest(unittest.TestCase): 8 | def test_debug_run(self): 9 | from tests.sampletestplan.hello import HelloTestPlan 10 | 11 | report = HelloTestPlan().debug_run() 12 | self.assertIsInstance(report, StreamTestReport) 13 | -------------------------------------------------------------------------------- /tests/test_testbase/test_resource.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Tencent is pleased to support the open source community by making QTA available. 4 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved. 5 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this 6 | # file except in compliance with the License. You may obtain a copy of the License at 7 | # 8 | # https://opensource.org/licenses/BSD-3-Clause 9 | # 10 | # Unless required by applicable law or agreed to in writing, software distributed 11 | # under the License is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS 12 | # OF ANY KIND, either express or implied. See the License for the specific language 13 | # governing permissions and limitations under the License. 14 | # 15 | """resource test 16 | """ 17 | 18 | import os 19 | import shutil 20 | import sys 21 | import unittest 22 | 23 | import six 24 | 25 | from testbase import resource 26 | from testbase.conf import settings 27 | from testbase.util import codecs_open 28 | 29 | suffix = "%s%s" % (sys.version_info[0], sys.version_info[1]) 30 | root_dir = os.path.join(settings.PROJECT_ROOT, "resources") 31 | test_dir_name = "test_dir_%s" % suffix 32 | test_file_name = "a_%s.txt" % suffix 33 | 34 | 35 | def _create_local_testfile(): 36 | test_dir = os.path.join(root_dir, test_dir_name) 37 | if not os.path.exists(test_dir): 38 | os.makedirs(test_dir) 39 | with codecs_open(os.path.join(test_dir, "foo.txt"), mode="w") as fd: 40 | fd.write("foo") 41 | local_file = os.path.join(root_dir, "a_%s.txt" % suffix) 42 | with codecs_open(local_file, "w", encoding="utf-8") as fd: 43 | fd.write("abc") 44 | 45 | with codecs_open(os.path.join(test_dir, "bar.md.link"), mode="w") as fd: 46 | fd.write("bar.md") 47 | 48 | return local_file, root_dir 49 | 50 | 51 | dup_test_dir = "base_test_%s" % sys.version_info[0] 52 | 53 | 54 | def _copy_testfile(src): 55 | base_dir = os.path.join(settings.PROJECT_ROOT, dup_test_dir) 56 | if not os.path.isdir(base_dir): 57 | os.mkdir(base_dir) 58 | res_dir = os.path.join(base_dir, "resources") 59 | if os.path.isdir(res_dir): 60 | shutil.rmtree(res_dir) 61 | shutil.copytree(src, res_dir) 62 | return base_dir 63 | 64 | 65 | class TestResManager(unittest.TestCase): 66 | @classmethod 67 | def setUpClass(cls): 68 | if six.PY2: 69 | cls.assertRaisesRegex = cls.assertRaisesRegexp 70 | 71 | cls.local_file, cls.local_dir = _create_local_testfile() 72 | 73 | @classmethod 74 | def tearDownClass(cls): 75 | shutil.rmtree(os.path.join(root_dir, test_dir_name), True) 76 | os.remove(cls.local_file) 77 | 78 | def test_get_local_file(self): 79 | fm = resource.TestResourceManager( 80 | resource.LocalResourceManagerBackend() 81 | ).create_session() 82 | self.assertEqual(self.local_file, fm.get_file(test_file_name)) 83 | self.assertEqual(self.local_file, resource.get_file(test_file_name)) 84 | 85 | paths = os.listdir(os.path.join(self.local_dir, test_dir_name)) 86 | for i, path in enumerate(paths): 87 | if path.endswith(".link"): 88 | paths[i] = path[:-5] 89 | list_result = fm.list_dir(test_dir_name) 90 | self.assertEqual(paths, list_result) 91 | list_result = resource.list_dir(test_dir_name) 92 | self.assertEqual(paths, list_result) 93 | 94 | def test_nofile_raise(self): 95 | fm = resource.TestResourceManager( 96 | resource.LocalResourceManagerBackend() 97 | ).create_session() 98 | self.assertRaisesRegex(Exception, "not found", fm.get_file, "a_xxx.txt") 99 | self.assertRaisesRegex(Exception, "not found", resource.get_file, "a_xxx.txt") 100 | self.assertRaisesRegex(Exception, "not found", fm.list_dir, "xxx_xxx") 101 | self.assertRaisesRegex(Exception, "not found", resource.list_dir, "xxx_xxx") 102 | 103 | def test_duplicated_raise(self): 104 | _, local_dir = _create_local_testfile() 105 | dup_dir = _copy_testfile(local_dir) 106 | self.addCleanup(shutil.rmtree, dup_dir, True) 107 | fm = resource.TestResourceManager( 108 | resource.LocalResourceManagerBackend() 109 | ).create_session() 110 | self.assertRaisesRegex( 111 | Exception, "multiple results", fm.get_file, test_file_name 112 | ) 113 | self.assertRaisesRegex( 114 | Exception, "multiple results", fm.list_dir, test_dir_name 115 | ) 116 | self.assertRaisesRegex( 117 | Exception, "multiple results", resource.get_file, test_file_name 118 | ) 119 | self.assertRaisesRegex( 120 | Exception, "multiple results", resource.list_dir, test_dir_name 121 | ) 122 | 123 | def test_unregisted_restype(self): 124 | rm = resource.TestResourceManager( 125 | resource.LocalResourceManagerBackend() 126 | ).create_session() 127 | with self.assertRaises(ValueError): 128 | rm.acquire_resource("xxx") 129 | with self.assertRaises(ValueError): 130 | rm.release_resource("xxx", 12) 131 | 132 | def test_walk_dir(self): 133 | dir_path_set = set() 134 | dir_names_set = set() 135 | file_names_set = set() 136 | for dir_path, dir_names, file_names in resource.walk("/"): 137 | dir_path_set.add(dir_path) 138 | for dir_name in dir_names: 139 | dir_names_set.add(dir_name) 140 | for file_name in file_names: 141 | file_names_set.add(file_name) 142 | sub_test_dir = "resources" + os.path.sep + test_dir_name 143 | self.assertTrue(sub_test_dir in dir_path_set) 144 | self.assertTrue(test_dir_name in dir_names_set) 145 | self.assertTrue(test_file_name in file_names_set) 146 | 147 | def test_testcase_resources(self): 148 | from tests.sampletest.hellotest import ResmgrTest 149 | 150 | result = ResmgrTest().debug_run() 151 | self.assertTrue(result.is_passed()) 152 | 153 | 154 | if __name__ == "__main__": 155 | unittest.main() 156 | -------------------------------------------------------------------------------- /tests/test_testbase/test_retry.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Tencent is pleased to support the open source community by making QTA available. 4 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved. 5 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this 6 | # file except in compliance with the License. You may obtain a copy of the License at 7 | # 8 | # https://opensource.org/licenses/BSD-3-Clause 9 | # 10 | # Unless required by applicable law or agreed to in writing, software distributed 11 | # under the License is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS 12 | # OF ANY KIND, either express or implied. See the License for the specific language 13 | # governing permissions and limitations under the License. 14 | # 15 | """test cases for retry mechanism 16 | """ 17 | 18 | 19 | import time 20 | import unittest 21 | 22 | from testbase.retry import Retry, RetryLimitExcceeded 23 | 24 | 25 | class TestRetry(unittest.TestCase): 26 | """test retry with invalid calllee""" 27 | 28 | def test_retry_with_timeout(self): 29 | def dummy(toggle_time, start_ts): 30 | if time.time() - start_ts > toggle_time: 31 | return True 32 | 33 | interval = 1 34 | timeout = 5 35 | retry = Retry(interval=interval, timeout=timeout) 36 | self.assertRaises(ValueError, retry.call, None) 37 | 38 | start_time = time.time() 39 | try: 40 | retry.call(dummy, timeout + 1, start_time) 41 | except RetryLimitExcceeded: 42 | time_cost = time.time() - start_time 43 | self.assertGreaterEqual( 44 | time_cost, 45 | 5, 46 | "actual timeout=%s is less than specified timeout=%s" 47 | % (time_cost, timeout), 48 | ) 49 | else: 50 | self.fail("no RetryLimitExcceeded raised") 51 | 52 | start_time = time.time() 53 | count = 0 54 | retry = Retry(interval=interval, timeout=timeout) 55 | for retry_item in retry: 56 | count += 1 57 | self.assertEqual(count, retry_item.iteration, "iteration does not match") 58 | if dummy(2, start_time): 59 | time_cost = time.time() - start_time 60 | self.assertGreaterEqual( 61 | time_cost, 62 | 2, 63 | "actual interval=%s is less than specified interval=%s" 64 | % (time_cost / float(count), interval), 65 | ) 66 | break 67 | else: 68 | self.fail("unexpected timeout") 69 | 70 | def test_retry_with_count(self): 71 | def dummy(param): 72 | param[0] += 1 73 | if param[0] > 2: 74 | return True 75 | 76 | retry = Retry(limit=1) 77 | self.assertRaises(ValueError, retry.call, None) 78 | 79 | x = [0] 80 | try: 81 | retry.call(dummy, x) 82 | except RetryLimitExcceeded: 83 | pass 84 | else: 85 | self.fail("no RetryLimitExcceeded was raised") 86 | 87 | x = [0] 88 | retry = Retry(limit=3) 89 | try: 90 | retry.call(dummy, x) 91 | except RetryLimitExcceeded: 92 | self.fail("RetryLimitExcceeded was raised") 93 | 94 | x = [0] 95 | retry = Retry(limit=3, interval=None) 96 | retry_count = 0 97 | start_time = time.time() 98 | for retry_item in retry: 99 | retry_count += 1 100 | self.assertEqual( 101 | retry_count, retry_item.iteration, "iteration does not match" 102 | ) 103 | if dummy(x): 104 | self.assertEqual(retry_count, 3, "iteration does not match") 105 | break 106 | time_cost = time.time() - start_time 107 | self.assertLess(time_cost, 0.05, "interval is unexpected") 108 | 109 | x = [-5] 110 | limit = 3 111 | retry = Retry(limit=limit, interval=0.5, raise_error=False) 112 | start_time = time.time() 113 | retry.call(dummy, x) 114 | time_cost = time.time() - start_time 115 | self.assertGreaterEqual( 116 | time_cost + 0.1, (limit - 1) * 0.5, "interval has no effect." 117 | ) 118 | -------------------------------------------------------------------------------- /tests/test_testbase/test_serialization.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """test serialization 3 | """ 4 | 5 | import unittest 6 | 7 | from testbase.testcase import TestCase 8 | from testbase.testsuite import SeqTestSuite, TestSuite 9 | from testbase import serialization, datadrive 10 | 11 | drive_data = [ 12 | 0, 13 | { 14 | "data": 1, 15 | "__attrs__": { 16 | "owner": "bar", 17 | "timeout": 5, 18 | "priority": TestCase.EnumPriority.BVT, 19 | "status": TestCase.EnumStatus.Implement, 20 | "tags": ("a", "b", "c"), 21 | "__doc__": "demo", 22 | }, 23 | }, 24 | ] 25 | 26 | 27 | @datadrive.DataDrive(drive_data) 28 | class FooTest(TestCase): 29 | """foo test""" 30 | 31 | owner = "foo" 32 | timeout = 1 33 | priority = TestCase.EnumPriority.High 34 | status = TestCase.EnumStatus.Ready 35 | 36 | def run_test(self): 37 | pass 38 | 39 | 40 | class SerializationTest(unittest.TestCase): 41 | def test_normal_serialization(self): 42 | from tests.sampletest.hellotest import HelloTest 43 | 44 | hello = HelloTest() 45 | data = serialization.dumps(hello) 46 | deserialized_case = serialization.loads(data) 47 | self.assertEqual(type(deserialized_case), HelloTest) 48 | for attr in ["owner", "timeout", "priority", "status", "tags", "test_doc"]: 49 | self.assertEqual(getattr(deserialized_case, attr), getattr(hello, attr)) 50 | 51 | def test_datadrive_serialization(self): 52 | tests = datadrive.load_datadrive_tests(FooTest, 1) 53 | self.assertEqual(len(tests), 1) 54 | test = tests[0] 55 | deserialized_test = serialization.loads(serialization.dumps(test)) 56 | self.assertEqual(deserialized_test.owner, "bar") 57 | self.assertEqual(deserialized_test.timeout, 5) 58 | self.assertEqual(deserialized_test.priority, TestCase.EnumPriority.BVT) 59 | self.assertEqual(deserialized_test.status, TestCase.EnumStatus.Implement) 60 | self.assertEqual(deserialized_test.tags, set(["a", "b", "c"])) 61 | self.assertEqual(deserialized_test.test_doc, "demo") 62 | 63 | def test_serialize_seq_testsuite(self): 64 | from tests.sampletest.hellotest import HelloTest, TimeoutTest 65 | 66 | foo_test = datadrive.load_datadrive_tests(FooTest, 1)[0] 67 | testsuite = SeqTestSuite([HelloTest(), TimeoutTest(), foo_test]) 68 | data = serialization.dumps(testsuite) 69 | deserialized_testsuite = serialization.loads(data) 70 | self.assertEqual(len(deserialized_testsuite), len(testsuite)) 71 | for deserialized_test, test in zip(deserialized_testsuite, testsuite): 72 | self.assertEqual(type(deserialized_test), type(test)) 73 | for attr in ["owner", "timeout", "priority", "status", "tags", "test_doc"]: 74 | self.assertEqual(getattr(deserialized_test, attr), getattr(test, attr)) 75 | 76 | def test_serialize_testsuite(self): 77 | foo_test = datadrive.load_datadrive_tests(FooTest, 1)[0] 78 | testsuite = TestSuite([foo_test]) 79 | data = serialization.dumps(testsuite) 80 | deserialized_testsuite = serialization.loads(data) 81 | self.assertEqual(len(deserialized_testsuite), len(testsuite)) 82 | 83 | 84 | if __name__ == "__main__": 85 | unittest.main(defaultTest="SerializationTest.test_serialize_testsuite") 86 | -------------------------------------------------------------------------------- /tests/test_testbase/test_testsuite.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Tencent is pleased to support the open source community by making QTA available. 4 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved. 5 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this 6 | # file except in compliance with the License. You may obtain a copy of the License at 7 | # 8 | # https://opensource.org/licenses/BSD-3-Clause 9 | # 10 | # Unless required by applicable law or agreed to in writing, software distributed 11 | # under the License is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS 12 | # OF ANY KIND, either express or implied. See the License for the specific language 13 | # governing permissions and limitations under the License. 14 | # 15 | """testsuite test 16 | """ 17 | 18 | import time 19 | import unittest 20 | 21 | from testbase.loader import TestLoader 22 | from testbase.testcase import TestCase 23 | from testbase.testsuite import TestSuite, TestSuiteCaseRunner 24 | 25 | class TestSuiteTest(unittest.TestCase): 26 | 27 | def test_property(self): 28 | test = TestLoader().load("tests.sampletest.suitetest.HelloTestSuite")[0] 29 | self.assertEqual(test.test_name, "tests.sampletest.suitetest.HelloTestSuite") 30 | self.assertEqual(test.owner, "root") 31 | self.assertEqual(len(test.testcases), 2) 32 | 33 | def test_sequential_run(self): 34 | test = TestLoader().load("tests.sampletest.suitetest.HelloTestSuite")[0] 35 | test.debug_run() 36 | self.assertEqual(test.test_result.passed, False) 37 | self.assertEqual(len(test.test_results), 1) 38 | 39 | test.case_runner = TestSuiteCaseRunner( 40 | TestSuite.EnumExecMode.Sequential, 41 | stop_on_failure=False 42 | ) 43 | test.debug_run() 44 | self.assertEqual(test.test_result.passed, False) 45 | self.assertEqual(len(test.test_results), 2) 46 | self.assertEqual(test.test_results[0].passed, False) 47 | self.assertEqual(test.test_results[1].passed, True) 48 | 49 | def test_parallel_run(self): 50 | test = TestLoader().load("tests.sampletest.suitetest.HelloTestSuiteParallel")[0] 51 | time0 = time.time() 52 | test.debug_run() 53 | time_cost = time.time() - time0 54 | self.assertEqual(test.test_result.passed, True) 55 | self.assertEqual(len(test.test_results), 4) 56 | self.assertLess(time_cost, 7) 57 | 58 | def test_run_pre_test_fail(self): 59 | test = TestLoader().load("tests.sampletest.suitetest.HelloTestSuitePreTestFail")[0] 60 | test.debug_run() 61 | self.assertEqual(test.test_result.passed, False) 62 | self.assertEqual(len(test.test_results), 0) 63 | 64 | def test_run_post_test_fail(self): 65 | test = TestLoader().load("tests.sampletest.suitetest.HelloTestSuitePostTestFail")[0] 66 | test.debug_run() 67 | self.assertEqual(test.test_result.passed, False) 68 | self.assertEqual(len(test.test_results), 1) 69 | self.assertEqual(test.test_results[0].passed, True) 70 | 71 | def test_load_from_testsuite(self): 72 | testloader = TestLoader() 73 | testsuite = "tests.sampletest.suitetest.HelloTestSuiteFilter" 74 | testsuite_class = testloader._load(testsuite) 75 | testsuite_class.testcases = ["tests.sampletest.suitetest"] 76 | testsuite_class.testcase_filter = {} 77 | tests = list(testloader.load(testsuite)[0]) 78 | self.assertEqual(len(tests), 1) 79 | self.assertEqual(tests[0].__class__.__name__, "HelloTest") 80 | 81 | def test_filter(self): 82 | testloader = TestLoader() 83 | testsuite = "tests.sampletest.suitetest.HelloTestSuiteFilter" 84 | testsuite_class = testloader._load(testsuite) 85 | test = testloader.load(testsuite)[0] 86 | self.assertEqual(len(test), 2) 87 | 88 | testsuite_class.testcase_filter["priorities"] = [TestCase.EnumPriority.Low] 89 | testsuite_class.testcase_filter["statuses"] = [TestCase.EnumStatus.Design, TestCase.EnumStatus.Ready] 90 | test = testloader.load(testsuite)[0] 91 | self.assertEqual(len(test), 1) 92 | 93 | testsuite_class.testcase_filter["priorities"] = [TestCase.EnumPriority.Low, TestCase.EnumPriority.Normal] 94 | testsuite_class.testcase_filter["statuses"] = [TestCase.EnumStatus.Design, TestCase.EnumStatus.Ready] 95 | test = testloader.load(testsuite)[0] 96 | self.assertEqual(len(test), 3) 97 | 98 | testsuite_class.testcase_filter["owners"] = ["foo"] 99 | test = testloader.load(testsuite)[0] 100 | self.assertEqual(len(test), 2) 101 | 102 | testsuite_class.testcase_filter["owners"] = [] 103 | testsuite_class.testcase_filter["tags"] = ["a", "b", "c"] 104 | test = testloader.load(testsuite)[0] 105 | self.assertEqual(len(test), 2) 106 | 107 | testsuite_class.testcase_filter["tags"] = ["a"] 108 | test = testloader.load(testsuite)[0] 109 | self.assertEqual(len(test), 1) 110 | 111 | testsuite_class.testcase_filter["tags"] = ["b", "c"] 112 | test = testloader.load(testsuite)[0] 113 | self.assertEqual(len(test), 2) 114 | 115 | testsuite_class.testcase_filter["tags"] = [] 116 | testsuite_class.testcase_filter["exclude_tags"] = ["a"] 117 | test = testloader.load(testsuite)[0] 118 | self.assertEqual(len(test), 2) 119 | 120 | testsuite_class.testcase_filter["exclude_tags"] = ["b"] 121 | test = testloader.load(testsuite)[0] 122 | self.assertEqual(len(test), 1) 123 | -------------------------------------------------------------------------------- /tests/test_testbase/test_util.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Tencent is pleased to support the open source community by making QTA available. 4 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved. 5 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this 6 | # file except in compliance with the License. You may obtain a copy of the License at 7 | # 8 | # https://opensource.org/licenses/BSD-3-Clause 9 | # 10 | # Unless required by applicable law or agreed to in writing, software distributed 11 | # under the License is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS 12 | # OF ANY KIND, either express or implied. See the License for the specific language 13 | # governing permissions and limitations under the License. 14 | # 15 | """util test 16 | """ 17 | 18 | import threading 19 | import time 20 | import unittest 21 | import sys 22 | 23 | import six 24 | 25 | from testbase import util 26 | from testbase.test import modify_settings 27 | 28 | 29 | class UtilTest(unittest.TestCase): 30 | def test_smart_binary(self): 31 | s = u"11111\udce444444" 32 | result = util.smart_binary(s) 33 | if six.PY3: 34 | self.assertEqual(result, b"'11111\\udce444444'") 35 | else: 36 | self.assertEqual(result, "11111\xed\xb3\xa444444") 37 | 38 | def test_timeout_lock(self): 39 | if sys.version_info[0] == 2: 40 | return 41 | timeout = 5 42 | lock1 = util.TimeoutLock(timeout) 43 | time0 = time.time() 44 | with lock1: 45 | pass 46 | time_cost = time.time() - time0 47 | self.assertLess(time_cost, 0.1) 48 | def lock_thread(): 49 | with lock1: 50 | print('lock1 acquired in thread') 51 | time.sleep(2) 52 | 53 | t = threading.Thread(target=lock_thread) 54 | t.daemon = True 55 | t.start() 56 | time.sleep(0.1) 57 | time0 = time.time() 58 | with lock1: 59 | print('lock1 acquired') 60 | time_cost = time.time() - time0 61 | self.assertGreater(time_cost, 1.5) 62 | 63 | def test_timeout_lock_deadlock(self): 64 | if sys.version_info[0] == 2: 65 | return 66 | timeout = 5 67 | lock1 = util.TimeoutLock(timeout) 68 | lock2 = threading.RLock() 69 | def lock_thread(): 70 | with lock1: 71 | print('lock1 acquired in thread') 72 | time.sleep(1) 73 | with lock2: 74 | print('lock2 acquired in thread') 75 | t = threading.Thread(target=lock_thread) 76 | t.daemon = True 77 | t.start() 78 | time.sleep(0.1) 79 | time0 = time.time() 80 | with lock2: 81 | print('lock2 acquired') 82 | time.sleep(1) 83 | with lock1: 84 | print('lock1 acquired') 85 | time_cost = time.time() - time0 86 | self.assertGreater(time_cost, timeout + 1) 87 | self.assertLess(time_cost, timeout + 1.5) 88 | 89 | def test_get_last_frame_stack(self): 90 | def func_wrapper(): 91 | return util.get_last_frame_stack(2) 92 | stack = func_wrapper() 93 | assert "func_wrapper" in stack 94 | with modify_settings(QTAF_STACK_FILTERS=["func_wrapper"]): 95 | stack = func_wrapper() 96 | assert "func_wrapper" not in stack 97 | -------------------------------------------------------------------------------- /tests/test_tuia/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Tencent is pleased to support the open source community by making QTA available. 3 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved. 4 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this 5 | # file except in compliance with the License. You may obtain a copy of the License at 6 | # 7 | # https://opensource.org/licenses/BSD-3-Clause 8 | # 9 | # Unless required by applicable law or agreed to in writing, software distributed 10 | # under the License is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS 11 | # OF ANY KIND, either express or implied. See the License for the specific language 12 | # governing permissions and limitations under the License. 13 | # 14 | -------------------------------------------------------------------------------- /tests/test_tuia/test_qpath.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Tencent is pleased to support the open source community by making QTA available. 4 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved. 5 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this 6 | # file except in compliance with the License. You may obtain a copy of the License at 7 | # 8 | # https://opensource.org/licenses/BSD-3-Clause 9 | # 10 | # Unless required by applicable law or agreed to in writing, software distributed 11 | # under the License is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS 12 | # OF ANY KIND, either express or implied. See the License for the specific language 13 | # governing permissions and limitations under the License. 14 | # 15 | """ 16 | QPath测试 17 | """ 18 | import unittest 19 | from tuia.qpathparser import QPathParser, QPathSyntaxError 20 | 21 | 22 | class QPathTest(unittest.TestCase): 23 | def test_invalid_operator_in_instance(self): 24 | qp = "/ ClassName='TxGuiFoundation' & Caption='QQ' / Instance~=2 & name='mainpanel'" 25 | with self.assertRaises(QPathSyntaxError) as cm: 26 | QPathParser().parse(qp) 27 | self.assertEqual(qp.find("~="), cm.exception.lexpos) 28 | 29 | def test_invalid_operator_in_maxdepth(self): 30 | qp = "/ ClassName='TxGuiFoundation' & Caption='QQ' / maxdepth~=2 & name='mainpanel'" 31 | with self.assertRaises(QPathSyntaxError) as cm: 32 | QPathParser().parse(qp) 33 | self.assertEqual(qp.find("~="), cm.exception.lexpos) 34 | 35 | def test_invalid_value_of_instance(self): 36 | qp = "/ ClassName='TxGuiFoundation' & Caption='QQ' / instance='xxx' & name='mainpanel'" 37 | with self.assertRaises(QPathSyntaxError) as cm: 38 | QPathParser().parse(qp) 39 | self.assertEqual(qp.find("'xxx'"), cm.exception.lexpos) 40 | 41 | def test_invalid_value_of_match_operator(self): 42 | qp = "/ ClassName='TxGuiFoundation' & Caption='QQ' / name~=true" 43 | with self.assertRaises(QPathSyntaxError) as cm: 44 | QPathParser().parse(qp) 45 | self.assertEqual(qp.find("~="), cm.exception.lexpos) 46 | 47 | def test_invalid_seperator(self): 48 | qp = "| ClassName='TxGuiFoundation' & Caption='QQ' | name~=true" 49 | with self.assertRaises(QPathSyntaxError) as cm: 50 | QPathParser().parse(qp) 51 | self.assertEqual(0, cm.exception.lexpos) 52 | 53 | # def test_empty_prop_value(self): 54 | # qp = "/ ClassName='TxGuiFoundation' & Caption='' / name~=true" 55 | # with self.assertRaises(QPathSyntaxError) as cm: 56 | # QPathParser().parse(qp) 57 | # self.assertEqual(qp.find("''"), cm.exception.lexpos) 58 | 59 | def test_maxdepth_smaller_than_zero(self): 60 | qp = "/ ClassName='TxGuiFoundation' & Caption='xxx' / Instance=-1 & maxdepth=0 & name='xxx'" 61 | with self.assertRaises(QPathSyntaxError) as cm: 62 | QPathParser().parse(qp) 63 | self.assertEqual(qp.find("0"), cm.exception.lexpos) 64 | 65 | def test_dumps(self): 66 | 67 | qp = "/ ClassName='TxGuiFoundation' & Caption='QQ' / name=true" 68 | data, _ = QPathParser().parse(qp) 69 | self.assertEqual( 70 | [ 71 | {"ClassName": ["=", "TxGuiFoundation"], "Caption": ["=", "QQ"]}, 72 | {"name": ["=", True]}, 73 | ], 74 | data, 75 | ) 76 | 77 | def test_type_trans(self): 78 | qp = "/ A='xxx' & B=-0X2121 & C=True" 79 | data, _ = QPathParser().parse(qp) 80 | data = data[0] 81 | self.assertEqual(data["A"][1], "xxx") 82 | self.assertEqual(data["B"][1], -0x2121) 83 | self.assertEqual(data["C"][1], True) 84 | 85 | def test_escape(self): 86 | qp = "/ A='xxx' & B='\"' & C=True" 87 | data, _ = QPathParser().parse(qp) 88 | self.assertEqual(data[0]["B"][1], '"') 89 | 90 | qp = "/ A='xxx' & B='\\'' & C=True" 91 | data, _ = QPathParser().parse(qp) 92 | self.assertEqual(data[0]["B"][1], "'") 93 | 94 | qp = "/ A='xxx' & B='\d' & C=True" 95 | data, _ = QPathParser().parse(qp) 96 | self.assertEqual(data[0]["B"][1], "\d") 97 | 98 | qp = "/ A='xxx' & B='\\d' & C=True" 99 | data, _ = QPathParser().parse(qp) 100 | self.assertEqual(data[0]["B"][1], "\\d") 101 | 102 | 103 | # class LocatorTest(unittest.TestCase): 104 | # 105 | # def test_as_dict(self): 106 | # qp = "/ ClassName='TxGuiFoundation' & Caption='QQ' / name=true" 107 | # locator = QPath(qp).locators[0] 108 | # self.assertEqual(locator['ClassName'].name.value, 'ClassName') 109 | # self.assertEqual(locator['classname'].name.value, 'ClassName') 110 | # self.assertEqual(locator['Caption'].value.value, 'QQ') 111 | # self.assertTrue('CLASSNAME' in [it for it in locator]) 112 | # self.assertTrue('CAPTION' in [it for it in locator]) 113 | # self.assertTrue('Caption' in locator) 114 | # self.assertTrue('caption' in locator) 115 | -------------------------------------------------------------------------------- /tuia/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Tencent is pleased to support the open source community by making QTA available. 4 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved. 5 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this 6 | # file except in compliance with the License. You may obtain a copy of the License at 7 | # 8 | # https://opensource.org/licenses/BSD-3-Clause 9 | # 10 | # Unless required by applicable law or agreed to in writing, software distributed 11 | # under the License is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS 12 | # OF ANY KIND, either express or implied. See the License for the specific language 13 | # governing permissions and limitations under the License. 14 | # 15 | """Tuia:Tencent UI Automation Framework 16 | """ 17 | -------------------------------------------------------------------------------- /tuia/_tif.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -* 2 | # 3 | # Tencent is pleased to support the open source community by making QTA available. 4 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved. 5 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this 6 | # file except in compliance with the License. You may obtain a copy of the License at 7 | # 8 | # https://opensource.org/licenses/BSD-3-Clause 9 | # 10 | # Unless required by applicable law or agreed to in writing, software distributed 11 | # under the License is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS 12 | # OF ANY KIND, either express or implied. See the License for the specific language 13 | # governing permissions and limitations under the License. 14 | # 15 | 16 | import warnings 17 | 18 | warnings.warn( 19 | "`tuia._tif` is depressed, please use `qt4c._tif` instead", DeprecationWarning 20 | ) 21 | from qt4c._tif import * # pylint: disable=wildcard-import 22 | -------------------------------------------------------------------------------- /tuia/accessible.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF8 -*- 2 | # 3 | # Tencent is pleased to support the open source community by making QTA available. 4 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved. 5 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this 6 | # file except in compliance with the License. You may obtain a copy of the License at 7 | # 8 | # https://opensource.org/licenses/BSD-3-Clause 9 | # 10 | # Unless required by applicable law or agreed to in writing, software distributed 11 | # under the License is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS 12 | # OF ANY KIND, either express or implied. See the License for the specific language 13 | # governing permissions and limitations under the License. 14 | # 15 | 16 | import warnings 17 | 18 | warnings.warn( 19 | "`tuia.accessible` is depressed, please use `qt4c.accessible` instead", 20 | DeprecationWarning, 21 | ) 22 | 23 | from qt4c.accessible import * # pylint: disable=wildcard-import 24 | -------------------------------------------------------------------------------- /tuia/app.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -* 2 | # 3 | # Tencent is pleased to support the open source community by making QTA available. 4 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved. 5 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this 6 | # file except in compliance with the License. You may obtain a copy of the License at 7 | # 8 | # https://opensource.org/licenses/BSD-3-Clause 9 | # 10 | # Unless required by applicable law or agreed to in writing, software distributed 11 | # under the License is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS 12 | # OF ANY KIND, either express or implied. See the License for the specific language 13 | # governing permissions and limitations under the License. 14 | # 15 | 16 | import warnings 17 | 18 | warnings.warn( 19 | "`tuia.app` is depressed, please use `qt4c.app` instead", DeprecationWarning 20 | ) 21 | 22 | from qt4c.app import App # pylint: disable=unused-import 23 | -------------------------------------------------------------------------------- /tuia/control.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -* 2 | # 3 | # Tencent is pleased to support the open source community by making QTA available. 4 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved. 5 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this 6 | # file except in compliance with the License. You may obtain a copy of the License at 7 | # 8 | # https://opensource.org/licenses/BSD-3-Clause 9 | # 10 | # Unless required by applicable law or agreed to in writing, software distributed 11 | # under the License is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS 12 | # OF ANY KIND, either express or implied. See the License for the specific language 13 | # governing permissions and limitations under the License. 14 | # 15 | 16 | import warnings 17 | 18 | warnings.warn( 19 | "`tuia.control` is depressed, please use `qt4c.control` instead", DeprecationWarning 20 | ) 21 | 22 | from qt4c.control import Control, ControlContainer # pylint: disable=unused-import 23 | -------------------------------------------------------------------------------- /tuia/env.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Tencent is pleased to support the open source community by making QTA available. 4 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved. 5 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this 6 | # file except in compliance with the License. You may obtain a copy of the License at 7 | # 8 | # https://opensource.org/licenses/BSD-3-Clause 9 | # 10 | # Unless required by applicable law or agreed to in writing, software distributed 11 | # under the License is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS 12 | # OF ANY KIND, either express or implied. See the License for the specific language 13 | # governing permissions and limitations under the License. 14 | # 15 | """ 16 | 测试环境模块 17 | """ 18 | 19 | import warnings 20 | 21 | warnings.warn("`tuia.env` will be removed in the future", DeprecationWarning) 22 | 23 | 24 | class EnumEnvType(object): 25 | """测试机运行环境的类型 26 | Local代表本机,Lab代表测试任务执行机 27 | """ 28 | 29 | Local, Lab = ("Local", "Lab") 30 | 31 | 32 | run_env = EnumEnvType.Local # pylint: disable=invalid-name 33 | -------------------------------------------------------------------------------- /tuia/exceptions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Tencent is pleased to support the open source community by making QTA available. 4 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved. 5 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this 6 | # file except in compliance with the License. You may obtain a copy of the License at 7 | # 8 | # https://opensource.org/licenses/BSD-3-Clause 9 | # 10 | # Unless required by applicable law or agreed to in writing, software distributed 11 | # under the License is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS 12 | # OF ANY KIND, either express or implied. See the License for the specific language 13 | # governing permissions and limitations under the License. 14 | # 15 | """ 16 | 异常模块定义 17 | """ 18 | 19 | 20 | class ControlNotFoundError(Exception): 21 | """控件没有找到""" 22 | 23 | pass 24 | 25 | 26 | class ControlAmbiguousError(Exception): 27 | """找到多个控件""" 28 | 29 | pass 30 | 31 | 32 | class ControlExpiredError(Exception): 33 | """控件失效错误""" 34 | 35 | pass 36 | 37 | 38 | class TimeoutError(Exception): 39 | """超时异常""" 40 | 41 | pass 42 | -------------------------------------------------------------------------------- /tuia/filedialog.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -* 2 | # 3 | # Tencent is pleased to support the open source community by making QTA available. 4 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved. 5 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this 6 | # file except in compliance with the License. You may obtain a copy of the License at 7 | # 8 | # https://opensource.org/licenses/BSD-3-Clause 9 | # 10 | # Unless required by applicable law or agreed to in writing, software distributed 11 | # under the License is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS 12 | # OF ANY KIND, either express or implied. See the License for the specific language 13 | # governing permissions and limitations under the License. 14 | # 15 | 16 | import warnings 17 | 18 | warnings.warn( 19 | "`tuia.filedialog` is depressed, please use `qt4c.filedialog` instead", 20 | DeprecationWarning, 21 | ) 22 | from qt4c.filedialog import * # pylint: disable=wildcard-import 23 | -------------------------------------------------------------------------------- /tuia/gfcontrols.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -* 2 | # 3 | # Tencent is pleased to support the open source community by making QTA available. 4 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved. 5 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this 6 | # file except in compliance with the License. You may obtain a copy of the License at 7 | # 8 | # https://opensource.org/licenses/BSD-3-Clause 9 | # 10 | # Unless required by applicable law or agreed to in writing, software distributed 11 | # under the License is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS 12 | # OF ANY KIND, either express or implied. See the License for the specific language 13 | # governing permissions and limitations under the License. 14 | # 15 | 16 | import warnings 17 | 18 | warnings.warn( 19 | "`tuia.gfcontrols` is depressed, please use `qt4c.gfcontrols` instead", 20 | DeprecationWarning, 21 | ) 22 | from qt4c.gfcontrols import * # pylint: disable=wildcard-import 23 | -------------------------------------------------------------------------------- /tuia/keyboard.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -* 2 | # 3 | # Tencent is pleased to support the open source community by making QTA available. 4 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved. 5 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this 6 | # file except in compliance with the License. You may obtain a copy of the License at 7 | # 8 | # https://opensource.org/licenses/BSD-3-Clause 9 | # 10 | # Unless required by applicable law or agreed to in writing, software distributed 11 | # under the License is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS 12 | # OF ANY KIND, either express or implied. See the License for the specific language 13 | # governing permissions and limitations under the License. 14 | # 15 | 16 | import warnings 17 | 18 | warnings.warn( 19 | "`tuia.keyboard` is depressed, please use `qt4c.keyboard` instead", 20 | DeprecationWarning, 21 | ) 22 | from qt4c.keyboard import Key, Keyboard, KeyInputError # pylint: disable=unused-import 23 | -------------------------------------------------------------------------------- /tuia/mouse.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -* 2 | # 3 | # Tencent is pleased to support the open source community by making QTA available. 4 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved. 5 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this 6 | # file except in compliance with the License. You may obtain a copy of the License at 7 | # 8 | # https://opensource.org/licenses/BSD-3-Clause 9 | # 10 | # Unless required by applicable law or agreed to in writing, software distributed 11 | # under the License is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS 12 | # OF ANY KIND, either express or implied. See the License for the specific language 13 | # governing permissions and limitations under the License. 14 | # 15 | 16 | import warnings 17 | 18 | warnings.warn( 19 | "`tuia.keyboard` is depressed, please use `qt4c.keyboard` instead", 20 | DeprecationWarning, 21 | ) 22 | 23 | # pylint: disable=unused-import 24 | from qt4c.mouse import ( 25 | Mouse, 26 | MouseClickType, 27 | MouseCursorType, 28 | MouseFlag, 29 | ) 30 | -------------------------------------------------------------------------------- /tuia/qpath.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -* 2 | # 3 | # Tencent is pleased to support the open source community by making QTA available. 4 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved. 5 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this 6 | # file except in compliance with the License. You may obtain a copy of the License at 7 | # 8 | # https://opensource.org/licenses/BSD-3-Clause 9 | # 10 | # Unless required by applicable law or agreed to in writing, software distributed 11 | # under the License is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS 12 | # OF ANY KIND, either express or implied. See the License for the specific language 13 | # governing permissions and limitations under the License. 14 | # 15 | 16 | import warnings 17 | 18 | try: 19 | from qt4c.qpath import QPath, QPathError # pylint: disable=unused-import 20 | 21 | warnings.warn( 22 | "`tuia.qpath` is depressed, please use `qt4c.qpath` instead", DeprecationWarning 23 | ) 24 | except ImportError: 25 | try: 26 | from qt4a.qpath import QPath, QPathError # pylint: disable=unused-import 27 | 28 | warnings.warn( 29 | "`tuia.qpath` is depressed, please use `qt4a.qpath` instead", 30 | DeprecationWarning, 31 | ) 32 | except ImportError: 33 | pass 34 | -------------------------------------------------------------------------------- /tuia/remoteprocessing.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -* 2 | # 3 | # Tencent is pleased to support the open source community by making QTA available. 4 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved. 5 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this 6 | # file except in compliance with the License. You may obtain a copy of the License at 7 | # 8 | # https://opensource.org/licenses/BSD-3-Clause 9 | # 10 | # Unless required by applicable law or agreed to in writing, software distributed 11 | # under the License is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS 12 | # OF ANY KIND, either express or implied. See the License for the specific language 13 | # governing permissions and limitations under the License. 14 | # 15 | 16 | import warnings 17 | 18 | warnings.warn( 19 | "`tuia.remoteprocessing` is depressed, please use `qt4c.remoteprocessing` instead", 20 | DeprecationWarning, 21 | ) 22 | from qt4c.remoteprocessing import * # pylint: disable=wildcard-import 23 | -------------------------------------------------------------------------------- /tuia/testcase.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -* 2 | # 3 | # Tencent is pleased to support the open source community by making QTA available. 4 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved. 5 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this 6 | # file except in compliance with the License. You may obtain a copy of the License at 7 | # 8 | # https://opensource.org/licenses/BSD-3-Clause 9 | # 10 | # Unless required by applicable law or agreed to in writing, software distributed 11 | # under the License is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS 12 | # OF ANY KIND, either express or implied. See the License for the specific language 13 | # governing permissions and limitations under the License. 14 | # 15 | 16 | import warnings 17 | 18 | warnings.warn( 19 | "`tuia.testcase` is depressed, please use `qt4c.testcase` instead", 20 | DeprecationWarning, 21 | ) 22 | from qt4c.testcase import * # pylint: disable=wildcard-import 23 | -------------------------------------------------------------------------------- /tuia/uiacontrols.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -* 2 | # 3 | # Tencent is pleased to support the open source community by making QTA available. 4 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved. 5 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this 6 | # file except in compliance with the License. You may obtain a copy of the License at 7 | # 8 | # https://opensource.org/licenses/BSD-3-Clause 9 | # 10 | # Unless required by applicable law or agreed to in writing, software distributed 11 | # under the License is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS 12 | # OF ANY KIND, either express or implied. See the License for the specific language 13 | # governing permissions and limitations under the License. 14 | # 15 | 16 | import warnings 17 | 18 | warnings.warn( 19 | "`tuia.gfcontrols` is depressed, please use `qt4c.gfcontrols` instead", 20 | DeprecationWarning, 21 | ) 22 | from qt4c.uiacontrols import * # pylint: disable=wildcard-import 23 | -------------------------------------------------------------------------------- /tuia/util.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -* 2 | # 3 | # Tencent is pleased to support the open source community by making QTA available. 4 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved. 5 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this 6 | # file except in compliance with the License. You may obtain a copy of the License at 7 | # 8 | # https://opensource.org/licenses/BSD-3-Clause 9 | # 10 | # Unless required by applicable law or agreed to in writing, software distributed 11 | # under the License is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS 12 | # OF ANY KIND, either express or implied. See the License for the specific language 13 | # governing permissions and limitations under the License. 14 | # 15 | 16 | import warnings 17 | 18 | warnings.warn( 19 | "`tuia.util` is depressed, please use `qt4c.util` instead", DeprecationWarning 20 | ) 21 | try: 22 | from qt4c.util import * # pylint: disable=wildcard-import 23 | except ImportError: 24 | pass 25 | 26 | from testbase.util import Timeout # pylint: disable=unused-import 27 | from tuia.exceptions import TimeoutError # pylint: disable=unused-import 28 | -------------------------------------------------------------------------------- /tuia/webcontrols.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -* 2 | # 3 | # Tencent is pleased to support the open source community by making QTA available. 4 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved. 5 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this 6 | # file except in compliance with the License. You may obtain a copy of the License at 7 | # 8 | # https://opensource.org/licenses/BSD-3-Clause 9 | # 10 | # Unless required by applicable law or agreed to in writing, software distributed 11 | # under the License is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS 12 | # OF ANY KIND, either express or implied. See the License for the specific language 13 | # governing permissions and limitations under the License. 14 | # 15 | 16 | import warnings 17 | 18 | warnings.warn( 19 | "`tuia.webcontrols` is depressed, please use `qt4c.webcontrols` instead", 20 | DeprecationWarning, 21 | ) 22 | from qt4c.webcontrols import * # pylint: disable=wildcard-import 23 | -------------------------------------------------------------------------------- /tuia/wincontrols.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -* 2 | # 3 | # Tencent is pleased to support the open source community by making QTA available. 4 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved. 5 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this 6 | # file except in compliance with the License. You may obtain a copy of the License at 7 | # 8 | # https://opensource.org/licenses/BSD-3-Clause 9 | # 10 | # Unless required by applicable law or agreed to in writing, software distributed 11 | # under the License is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS 12 | # OF ANY KIND, either express or implied. See the License for the specific language 13 | # governing permissions and limitations under the License. 14 | # 15 | 16 | import warnings 17 | 18 | warnings.warn( 19 | "`tuia.wincontrols` is depressed, please use `qt4c.wincontrols` instead", 20 | DeprecationWarning, 21 | ) 22 | from qt4c.wincontrols import * # pylint: disable=wildcard-import 23 | -------------------------------------------------------------------------------- /tuia/wintypes.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -* 2 | # 3 | # Tencent is pleased to support the open source community by making QTA available. 4 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved. 5 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this 6 | # file except in compliance with the License. You may obtain a copy of the License at 7 | # 8 | # https://opensource.org/licenses/BSD-3-Clause 9 | # 10 | # Unless required by applicable law or agreed to in writing, software distributed 11 | # under the License is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS 12 | # OF ANY KIND, either express or implied. See the License for the specific language 13 | # governing permissions and limitations under the License. 14 | # 15 | 16 | import warnings 17 | 18 | warnings.warn( 19 | "`tuia.wintypes` is depressed, please use `qt4c.wintypes` instead", 20 | DeprecationWarning, 21 | ) 22 | from qt4c.wintypes import * # pylint: disable=wildcard-import 23 | --------------------------------------------------------------------------------