├── .github ├── codecov.yml └── workflows │ ├── codeql.yml │ ├── integration.yml │ ├── publish.yml │ └── unittest.yml ├── .gitignore ├── .readthedocs.yaml ├── HISTORY.md ├── LICENSE ├── README.md ├── api4jenkins ├── __init__.py ├── __version__.py ├── artifact.py ├── build.py ├── credential.py ├── exceptions.py ├── http.py ├── input.py ├── item.py ├── job.py ├── mix.py ├── node.py ├── plugin.py ├── queue.py ├── report.py ├── system.py ├── user.py └── view.py ├── docs ├── Makefile ├── make.bat ├── requirements.txt └── source │ ├── conf.py │ ├── index.rst │ └── user │ ├── api.rst │ ├── example.rst │ └── install.rst ├── setup.py ├── tests ├── __init__.py ├── integration │ ├── __init__.py │ ├── conftest.py │ ├── test_01_jenkins.py │ ├── test_02_job.py │ ├── test_03_credential.py │ ├── test_04_view.py │ ├── test_05_build.py │ ├── test_06_system.py │ ├── test_07_node.py │ ├── test_08_plugin.py │ └── tests_data │ │ ├── args_job.xml │ │ ├── credential.xml │ │ ├── domain.xml │ │ ├── folder.xml │ │ ├── job.xml │ │ ├── servererror.xml │ │ └── view.xml └── unit │ ├── __init__.py │ ├── conftest.py │ ├── test_artifact.py │ ├── test_build.py │ ├── test_credential.py │ ├── test_input.py │ ├── test_jenkins.py │ ├── test_job.py │ ├── test_node.py │ ├── test_plugin.py │ ├── test_queue.py │ ├── test_report.py │ ├── test_system.py │ ├── test_view.py │ └── tests_data │ ├── credential │ ├── UserPasswordCredential.xml │ ├── credentials.json │ ├── domains.json │ └── user_psw.json │ ├── jenkins │ ├── crumb.json │ └── jenkins.json │ ├── job │ ├── folder.json │ ├── mbranch.json │ └── pipeline.json │ ├── node │ ├── node.json │ └── nodes.json │ ├── plugin │ ├── installStatus.json │ ├── installStatus_done.json │ └── plugin.json │ ├── queue │ ├── blockeditem.json │ ├── buildableitem.json │ ├── leftitem.json │ ├── queue.json │ └── waitingitem.json │ ├── report │ ├── coverage_report.json │ ├── coverage_report_trend.json │ ├── coverage_result.json │ ├── coverage_trends.json │ └── test_report.json │ ├── run │ ├── freestylebuild.json │ └── workflowrun.json │ └── view │ └── allview.json └── tox.ini /.github/codecov.yml: -------------------------------------------------------------------------------- 1 | flag_management: 2 | individual_flags: 3 | - name: unittest 4 | paths: 5 | - api4jenkins 6 | - tests/unit/ 7 | statuses: 8 | - type: project 9 | target: 80% 10 | threshold: 1% 11 | - name: integration 12 | paths: 13 | - api4jenkins 14 | - tests/integration/ 15 | statuses: 16 | - type: project 17 | target: auto 18 | threshold: 1% 19 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [master, main, 1.x] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [master, main, 1.x] 20 | schedule: 21 | - cron: '0 23 * * 0' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | language: ['python'] 32 | # CodeQL supports [ $supported-codeql-languages ] 33 | # Learn more: 34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v2 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v1 43 | with: 44 | languages: ${{ matrix.language }} 45 | # If you wish to specify custom queries, you can do so here or in a config file. 46 | # By default, queries listed here will override any specified in a config file. 47 | # Prefix the list here with "+" to use these queries and those in the config file. 48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 49 | 50 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 51 | # If this step fails, then you should remove it and run the build manually (see below) 52 | - name: Autobuild 53 | uses: github/codeql-action/autobuild@v1 54 | 55 | # ℹ️ Command-line programs to run using the OS shell. 56 | # 📚 https://git.io/JvXDl 57 | 58 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 59 | # and modify them (or add more) to build your code if your project 60 | # uses a compiled language 61 | 62 | #- run: | 63 | # make bootstrap 64 | # make release 65 | 66 | - name: Perform CodeQL Analysis 67 | uses: github/codeql-action/analyze@v1 68 | -------------------------------------------------------------------------------- /.github/workflows/integration.yml: -------------------------------------------------------------------------------- 1 | name: Integration Tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | integration-tests: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Check out code 10 | uses: actions/checkout@v3 11 | 12 | - name: Set up Python 13 | uses: actions/setup-python@v4 14 | with: 15 | python-version: "3.x" 16 | 17 | - name: Install dependencies 18 | run: pip install --upgrade keyring==21.4.0 setuptools wheel twine coveralls tox 19 | 20 | - name: Start Jenkins 21 | id: start-jenkins 22 | shell: bash 23 | run: | 24 | set -e 25 | sudo docker run -dt --rm --name jenkins-master joelee2012/standalone-jenkins:latest 26 | echo 'Waiting for Jenkins to start...' 27 | until sudo docker logs jenkins-master | grep -q 'Jenkins is fully up and running'; do 28 | sleep 1 29 | done 30 | ip=$(sudo docker inspect --format='{{.NetworkSettings.IPAddress}}' jenkins-master) 31 | password=$(sudo docker exec jenkins-master cat /var/jenkins_home/secrets/initialAdminPassword) 32 | version=$(sudo docker exec jenkins-master sh -c 'echo "$JENKINS_VERSION"') 33 | port=8080 34 | echo "url=http://${ip}:${port}/" >> $GITHUB_OUTPUT 35 | echo "user=admin" >> $GITHUB_OUTPUT 36 | echo "password=${password}" >> $GITHUB_OUTPUT 37 | echo "version=${version}" >> $GITHUB_OUTPUT 38 | 39 | - name: Run integration tests 40 | env: 41 | JENKINS_URL: ${{ steps.start-jenkins.outputs.url }} 42 | JENKINS_USER: ${{ steps.start-jenkins.outputs.user }} 43 | JENKINS_PASSWORD: ${{ steps.start-jenkins.outputs.password }} 44 | JENKINS_VERSION: ${{ steps.start-jenkins.outputs.version }} 45 | run: tox -e integration 46 | 47 | - name: Upload coverage to Codecov 48 | uses: codecov/codecov-action@v3 49 | with: 50 | directory: tests/integration/ 51 | files: coverage.xml 52 | flags: integration 53 | verbose: true 54 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Release Python Package 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | deploy: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Check out code 12 | uses: actions/checkout@v3 13 | 14 | - name: Set up Python 15 | uses: actions/setup-python@v4 16 | with: 17 | python-version: '3.x' 18 | 19 | - name: Install dependencies 20 | run: | 21 | python -m pip install --upgrade pip 22 | pip install setuptools wheel twine 23 | 24 | - name: Build and publish 25 | env: 26 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 27 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 28 | run: | 29 | python setup.py sdist bdist_wheel 30 | twine upload dist/* 31 | -------------------------------------------------------------------------------- /.github/workflows/unittest.yml: -------------------------------------------------------------------------------- 1 | name: Unit Test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | unit-tests: 7 | runs-on: ${{ matrix.os }} 8 | 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] 13 | # os: [ubuntu-latest, macOS-latest, windows-latest] 14 | os: [ubuntu-latest] 15 | 16 | steps: 17 | - name: Check out code 18 | uses: actions/checkout@v3 19 | 20 | - name: Set up Python ${{ matrix.python-version }} 21 | uses: actions/setup-python@v4 22 | with: 23 | python-version: ${{ matrix.python-version }} 24 | 25 | - name: Install dependencies 26 | run: pip install --upgrade keyring==21.4.0 setuptools wheel twine tox 27 | 28 | - name: Run pylint & codestyle & unit tests 29 | run: tox 30 | 31 | - name: Upload coverage to Codecov 32 | if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12' 33 | uses: codecov/codecov-action@v3 34 | with: 35 | directory: tests/unit/ 36 | files: coverage.xml 37 | flags: unittest 38 | verbose: true 39 | 40 | - name: Run twine check 41 | run: | 42 | python setup.py sdist bdist_wheel 43 | twine check dist/* 44 | -------------------------------------------------------------------------------- /.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 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | docs/build/ 28 | docs/source/_build/ 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | .hypothesis/ 50 | .pytest_cache/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | db.sqlite3 60 | 61 | # Flask stuff: 62 | instance/ 63 | .webassets-cache 64 | 65 | # Scrapy stuff: 66 | .scrapy 67 | 68 | # Sphinx documentation 69 | docs/_build/ 70 | 71 | # PyBuilder 72 | target/ 73 | 74 | # Jupyter Notebook 75 | .ipynb_checkpoints 76 | 77 | # pyenv 78 | .python-version 79 | 80 | # celery beat schedule file 81 | celerybeat-schedule 82 | 83 | # SageMath parsed files 84 | *.sage.py 85 | 86 | # Environments 87 | .env 88 | .venv 89 | env/ 90 | venv/ 91 | ENV/ 92 | env.bak/ 93 | venv.bak/ 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 | # mac os related 109 | .DS_Store -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: "ubuntu-22.04" 5 | tools: 6 | python: "3.11" 7 | 8 | formats: 9 | - pdf 10 | - epub 11 | 12 | python: 13 | install: 14 | - requirements: docs/requirements.txt 15 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | Release History 2 | =============== 3 | 2.0.2 (2023-11-06) 4 | ------------------ 5 | - Fix certificates verification after migration from `requtests` to `httpx` 6 | 7 | 2.0.1 (2023-10-31) 8 | ------------------ 9 | - Update doc and requires python 3.8 (#94) 10 | 11 | 2.0.0 (2023-10-19) 12 | ------------------- 13 | - Add Async Client 14 | 15 | 1.15.0 (2023-08-30) 16 | ------------------- 17 | - Support to manage domains for credential 18 | 19 | 1.14.1 (2023-07-24) 20 | ------------------- 21 | - Support additional session headers 22 | 23 | 1.14 (2022-11-28) 24 | ----------------- 25 | - Add AnkaCloudComputer (#70) 26 | - Remove 3.6 support (#71) 27 | 28 | 1.13 (2022-10-01) 29 | ----------------- 30 | - fix for approving the pending input when jenkins runs on subpath (#66) 31 | - support get build by display name for job (#63) 32 | 33 | 1.12 (2022-08-04) 34 | ----------------- 35 | - Add EC2Computer as Node child class (#58) 36 | 37 | 1.11 (2022-07-08) 38 | ----------------- 39 | - encode request data with utf-8 40 | - add parameter "recursive" for `Job.duplicate` 41 | - fix issue for `Jenkins.crate_job` 42 | 43 | 1.10 (2022-05-17) 44 | ----------------- 45 | - recursively create parent of job if it does not exist 46 | - add buildable property for multibranchproject 47 | - set dependency version 48 | 49 | 1.9.1 (2022-03-29) 50 | ------------------- 51 | - change OrganizationFolder to inherit from WorkflowMultiBranchProject 52 | - `Jenkins.get_job` return consistent result 53 | 54 | 1.9 (2022-03-12) 55 | ----------------- 56 | - Resolve `name`, `full_name`, `full_display_name` from url for `Job` 57 | - Add new methods for `Nodes`, `Project` 58 | - Support `SectionedView` and `OrganizationFolder` 59 | 60 | 1.8 (2021-12-27) 61 | ----------------- 62 | - Rename built-in node as https://www.jenkins.io/doc/book/managing/built-in-node-migration/ 63 | - Get parameters for job 64 | 65 | 1.7 (2021-10-09) 66 | ----------------- 67 | - `Jenkins.get_job` can accept job url now 68 | - Support to retrieve coverage which was generated by [JaCoCo](https://plugins.jenkins.io/jacoco/) and [Code Coverage API](https://plugins.jenkins.io/code-coverage-api/) 69 | 70 | 1.6 (2021-08-01) 71 | ----------------- 72 | - Support NestedView 73 | - Support decrypt credential in Jenkins 74 | - Update doc 75 | - bugfix for `queue.get_build` 76 | 77 | 1.5.1 (2021-05-11) 78 | ------------------- 79 | - Bugfix for nodes.iter_builds 80 | 81 | 1.5 (2021-04-29) 82 | ----------------- 83 | - Add methods to get parameters and causes for `Build` and `QueueItem` 84 | - Add methods to manage jcasc 85 | - Add help functions 86 | 87 | 1.4 (2021-03-31) 88 | ----------------- 89 | - Support to retrieve test report for build 90 | - Support to validate Jenkinsfile 91 | 92 | 1.3 (2021-02-28) 93 | ----------------- 94 | - Add capability to get/save artifacts for `WorkflowRun`. 95 | - Make `Jenkins` and `Folder` is subscribed and can be iterated with depth. 96 | - Refactor some code. 97 | 98 | 1.2 (2021-01-31) 99 | ---------------- 100 | - Support to enable, disable, scan, get_scan_log for `WorkflowMultiBranchProject` 101 | - Call `Jenkins.get_job` for getting parent of `Job` 102 | - Support process input step for `WorkflowRun`, see `WorkflowRun.get_pending_input()` 103 | - Support user management 104 | 105 | 1.1 (2020-12-31) 106 | ----------------- 107 | - Rewrite documentation and add more examples 108 | - `Jenkins.build_job()` and `Project.build()` accept key word argments instead of dict 109 | - Support to access attribute with None type value in json 110 | - Fix typo in `Folder.__iter__()` 111 | 112 | 1.0 (2020-11-15) 113 | ------------------ 114 | - First release 115 | 116 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Unit Test](https://github.com/joelee2012/api4jenkins/actions/workflows/unittest.yml/badge.svg?branch=main)](https://github.com/joelee2012/api4jenkins/actions/workflows/unittest.yml) 2 | [![Integration Test](https://github.com/joelee2012/api4jenkins/actions/workflows/integration.yml/badge.svg?branch=main)](https://github.com/joelee2012/api4jenkins/actions/workflows/integration.yml) 3 | ![CodeQL](https://github.com/joelee2012/api4jenkins/workflows/CodeQL/badge.svg?branch=main) 4 | [![codecov](https://codecov.io/gh/joelee2012/api4jenkins/branch/main/graph/badge.svg?token=YGM4CIB149)](https://codecov.io/gh/joelee2012/api4jenkins) 5 | ![PyPI](https://img.shields.io/pypi/v/api4jenkins) 6 | ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/api4jenkins) 7 | ![PyPI - Wheel](https://img.shields.io/pypi/wheel/api4jenkins) 8 | [![Documentation Status](https://readthedocs.org/projects/api4jenkins/badge/?version=latest)](https://api4jenkins.readthedocs.io/en/latest/?badge=latest) 9 | ![GitHub](https://img.shields.io/github/license/joelee2012/api4jenkins) 10 | 11 | 12 | # Jenkins Python Client 13 | 14 | [Python3](https://www.python.org/) client library for [Jenkins API](https://www.jenkins.io/doc/book/using/remote-access-api/) which provides sync and async APIs. 15 | 16 | # Features 17 | 18 | - Provides sync and async APIs 19 | - Object oriented, each Jenkins item has corresponding class, easy to use and extend 20 | - Base on `api/json`, easy to query/filter attribute of item 21 | - Setup relationship between class just like Jenkins item 22 | - Support api for almost every Jenkins item 23 | - Pythonic 24 | - Test with latest Jenkins LTS 25 | 26 | # Installation 27 | 28 | ```bash 29 | python3 -m pip install api4jenkins 30 | ``` 31 | 32 | # Quick start 33 | 34 | Sync example: 35 | 36 | ```python 37 | >>> from api4jenkins import Jenkins 38 | >>> client = Jenkins('http://127.0.0.1:8080/', auth=('admin', 'admin')) 39 | >>> client.version 40 | '2.176.2' 41 | >>> xml = """ 42 | ... 43 | ... 44 | ... 45 | ... echo $JENKINS_VERSION 46 | ... 47 | ... 48 | ... """ 49 | >>> client.create_job('path/to/job', xml) 50 | >>> import time 51 | >>> item = client.build_job('path/to/job') 52 | >>> while not item.get_build(): 53 | ... time.sleep(1) 54 | >>> build = item.get_build() 55 | >>> for line in build.progressive_output(): 56 | ... print(line) 57 | ... 58 | Started by user admin 59 | Running as SYSTEM 60 | Building in workspace /var/jenkins_home/workspace/freestylejob 61 | [freestylejob] $ /bin/sh -xe /tmp/jenkins2989549474028065940.sh 62 | + echo $JENKINS_VERSION 63 | 2.176.2 64 | Finished: SUCCESS 65 | >>> build.building 66 | False 67 | >>> build.result 68 | 'SUCCESS' 69 | ``` 70 | 71 | Async example 72 | 73 | ```python 74 | import asyncio 75 | import time 76 | from api4jenkins import AsyncJenkins 77 | 78 | async main(): 79 | client = AsyncJenkins('http://127.0.0.1:8080/', auth=('admin', 'admin')) 80 | print(await client.version) 81 | xml = """ 82 | 83 | 84 | 85 | echo $JENKINS_VERSION 86 | 87 | 88 | """ 89 | await client.create_job('job', xml) 90 | item = await client.build_job('job') 91 | while not await item.get_build(): 92 | time.sleep(1) 93 | build = await item.get_build() 94 | async for line in build.progressive_output(): 95 | print(line) 96 | 97 | print(await build.building) 98 | print(await build.result) 99 | 100 | asyncio.run(main()) 101 | ``` 102 | 103 | # Documentation 104 | User Guide and API Reference is available on [Read the Docs](https://api4jenkins.readthedocs.io/) 105 | 106 | -------------------------------------------------------------------------------- /api4jenkins/__version__.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | __version__ = '2.0.4' 3 | __title__ = 'api4jenkins' 4 | __description__ = 'Jenkins Python Client' 5 | __url__ = 'https://github.com/joelee2012/api4jenkins' 6 | __author__ = 'Joe Lee' 7 | __author_email__ = 'lj_2005@163.com' 8 | __license__ = 'Apache 2.0' 9 | __copyright__ = 'Copyright 2023 Joe Lee' 10 | __documentation__ = 'https://api4jenkins.readthedocs.io' 11 | -------------------------------------------------------------------------------- /api4jenkins/artifact.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import anyio 3 | 4 | from .item import AsyncItem, Item 5 | from .mix import RawJsonMixIn 6 | 7 | 8 | class Artifact(RawJsonMixIn, Item): 9 | 10 | def __init__(self, jenkins, raw): 11 | super().__init__( 12 | jenkins, f"{jenkins.url}{raw['url'][1:]}") 13 | # remove trailing slash 14 | self.url = self.url[:-1] 15 | self.raw = raw 16 | self.raw['_class'] = 'Artifact' 17 | 18 | def save(self, filename=None): 19 | if not filename: 20 | filename = self.name 21 | with self.handle_stream('GET', '') as resp: 22 | save_response_to(resp, filename) 23 | 24 | 25 | def save_response_to(response, filename): 26 | with open(filename, 'wb') as fd: 27 | for chunk in response.iter_bytes(chunk_size=128): 28 | fd.write(chunk) 29 | 30 | 31 | class AsyncArtifact(RawJsonMixIn, AsyncItem): 32 | 33 | def __init__(self, jenkins, raw): 34 | super().__init__( 35 | jenkins, f"{jenkins.url}{raw['url'][1:]}") 36 | # remove trailing slash 37 | self.url = self.url[:-1] 38 | self.raw = raw 39 | self.raw['_class'] = 'Artifact' 40 | 41 | async def save(self, filename=None): 42 | if not filename: 43 | filename = self.name 44 | async with self.handle_stream('GET', '') as resp: 45 | await save_response_to(resp, filename) 46 | 47 | 48 | async def async_save_response_to(response, filename): 49 | async with anyio.wrap_file(open(filename, 'wb')) as fd: 50 | async for chunk in response.aiter_bytes(chunk_size=128): 51 | await fd.write(chunk) 52 | -------------------------------------------------------------------------------- /api4jenkins/build.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | import asyncio 4 | import re 5 | import time 6 | 7 | from .artifact import Artifact, async_save_response_to, save_response_to 8 | from .input import PendingInputAction 9 | from .item import AsyncItem, Item 10 | from .mix import (ActionsMixIn, AsyncActionsMixIn, AsyncDeletionMixIn, 11 | AsyncDescriptionMixIn, DeletionMixIn, DescriptionMixIn) 12 | from .report import (AsyncCoverageReport, AsyncCoverageResult, 13 | AsyncCoverageTrends, AsyncTestReport, CoverageReport, 14 | CoverageResult, CoverageTrends, TestReport) 15 | 16 | 17 | class Build(Item, DescriptionMixIn, DeletionMixIn, ActionsMixIn): 18 | 19 | def console_text(self): 20 | with self.handle_stream('GET', 'consoleText') as resp: 21 | yield from resp.iter_lines() 22 | 23 | def progressive_output(self, html=False): 24 | url = 'logText/progressiveHtml' if html else 'logText/progressiveText' 25 | start = 0 26 | while True: 27 | resp = self.handle_req('GET', url, params={'start': start}) 28 | time.sleep(1) 29 | if start == resp.headers.get('X-Text-Size'): 30 | continue 31 | yield from resp.iter_lines() 32 | if not resp.headers.get('X-More-Data'): 33 | break 34 | start = resp.headers['X-Text-Size'] 35 | 36 | def stop(self): 37 | return self.handle_req('POST', 'stop') 38 | 39 | def term(self): 40 | return self.handle_req('POST', 'term') 41 | 42 | def kill(self): 43 | return self.handle_req('POST', 'kill') 44 | 45 | def get_next_build(self): 46 | if item := self.api_json(tree='nextBuild[url]')['nextBuild']: 47 | return self.__class__(self.jenkins, item['url']) 48 | return None 49 | 50 | def get_previous_build(self): 51 | if item := self.api_json(tree='previousBuild[url]')['previousBuild']: 52 | return self.__class__(self.jenkins, item['url']) 53 | return None 54 | 55 | @property 56 | def project(self): 57 | job_name = self.jenkins._url2name(re.sub(r'\w+[/]?$', '', self.url)) 58 | return self.jenkins.get_job(job_name) 59 | 60 | def get_test_report(self): 61 | tr = TestReport(self.jenkins, f'{self.url}testReport/') 62 | return tr if tr.exists() else None 63 | 64 | def get_coverage_report(self): 65 | '''Access coverage report generated by `JaCoCo `_''' 66 | cr = CoverageReport(self.jenkins, f'{self.url}jacoco/') 67 | return cr if cr.exists() else None 68 | 69 | def get_coverage_result(self): 70 | '''Access coverage result generated by `Code Coverage API `_''' 71 | cr = CoverageResult(self.jenkins, f'{self.url}coverage/result/') 72 | return cr if cr.exists() else None 73 | 74 | def get_coverage_trends(self): 75 | ct = CoverageTrends(self.jenkins, f'{self.url}coverage/trend/') 76 | return ct if ct.exists() else None 77 | 78 | 79 | class WorkflowRun(Build): 80 | 81 | def get_pending_input(self): 82 | '''get current pending input step''' 83 | data = self.handle_req('GET', 'wfapi/describe').json() 84 | if not data['_links'].get('pendingInputActions'): 85 | return None 86 | action = self.handle_req('GET', 'wfapi/pendingInputActions').json()[0] 87 | action["abortUrl"] = action["abortUrl"][action["abortUrl"].index( 88 | "/job/"):] 89 | return PendingInputAction(self.jenkins, action) 90 | 91 | @property 92 | def artifacts(self): 93 | artifacts = self.handle_req('GET', 'wfapi/artifacts').json() 94 | return [Artifact(self.jenkins, art) for art in artifacts] 95 | 96 | def save_artifacts(self, filename='archive.zip'): 97 | with self.handle_stream('GET', 'artifact/*zip*/archive.zip') as resp: 98 | save_response_to(resp, filename) 99 | 100 | 101 | class FreeStyleBuild(Build): 102 | pass 103 | 104 | 105 | class MatrixBuild(Build): 106 | pass 107 | 108 | # async class 109 | 110 | 111 | class AsyncBuild(AsyncItem, AsyncDescriptionMixIn, AsyncDeletionMixIn, AsyncActionsMixIn): 112 | 113 | async def console_text(self): 114 | async with self.handle_stream('GET', 'consoleText') as resp: 115 | async for line in resp.aiter_lines(): 116 | yield line 117 | 118 | async def progressive_output(self, html=False): 119 | url = 'logText/progressiveHtml' if html else 'logText/progressiveText' 120 | start = 0 121 | while True: 122 | resp = await self.handle_req('GET', url, params={'start': start}) 123 | await asyncio.sleep(1) 124 | if start == resp.headers.get('X-Text-Size'): 125 | continue 126 | async for line in resp.aiter_lines(): 127 | yield line 128 | if not resp.headers.get('X-More-Data'): 129 | break 130 | start = resp.headers['X-Text-Size'] 131 | 132 | async def stop(self): 133 | return await self.handle_req('POST', 'stop') 134 | 135 | async def term(self): 136 | return await self.handle_req('POST', 'term') 137 | 138 | async def kill(self): 139 | return await self.handle_req('POST', 'kill') 140 | 141 | async def get_next_build(self): 142 | data = await self.api_json(tree='nextBuild[url]') 143 | return self.__class__(self.jenkins, data['nextBuild']['url']) if data['nextBuild'] else None 144 | 145 | async def get_previous_build(self): 146 | data = await self.api_json(tree='previousBuild[url]') 147 | return self.__class__(self.jenkins, data['previousBuild']['url']) if data['previousBuild'] else None 148 | 149 | @property 150 | async def project(self): 151 | job_name = self.jenkins._url2name(re.sub(r'\w+[/]?$', '', self.url)) 152 | return await self.jenkins.get_job(job_name) 153 | 154 | async def get_test_report(self): 155 | tr = AsyncTestReport(self.jenkins, f'{self.url}testReport/') 156 | return tr if await tr.exists() else None 157 | 158 | async def get_coverage_report(self): 159 | '''Access coverage report generated by `JaCoCo `_''' 160 | cr = AsyncCoverageReport(self.jenkins, f'{self.url}jacoco/') 161 | return cr if await cr.exists() else None 162 | 163 | async def get_coverage_result(self): 164 | '''Access coverage result generated by `Code Coverage API `_''' 165 | cr = AsyncCoverageResult(self.jenkins, f'{self.url}coverage/result/') 166 | return cr if await cr.exists() else None 167 | 168 | async def get_coverage_trends(self): 169 | ct = AsyncCoverageTrends(self.jenkins, f'{self.url}coverage/trend/') 170 | return ct if await ct.exists() else None 171 | 172 | 173 | class AsyncWorkflowRun(AsyncBuild): 174 | 175 | async def get_pending_input(self): 176 | '''get current pending input step''' 177 | data = (await self.handle_req('GET', 'wfapi/describe')).json() 178 | if not data['_links'].get('pendingInputActions'): 179 | return None 180 | action = (await self.handle_req('GET', 'wfapi/pendingInputActions')).json()[0] 181 | action["abortUrl"] = action["abortUrl"][action["abortUrl"].index( 182 | "/job/"):] 183 | return PendingInputAction(self.jenkins, action) 184 | 185 | @property 186 | async def artifacts(self): 187 | artifacts = (await self.handle_req('GET', 'wfapi/artifacts')).json() 188 | return [Artifact(self.jenkins, art) for art in artifacts] 189 | 190 | async def save_artifacts(self, filename='archive.zip'): 191 | async with self.handle_stream('GET', 'artifact/*zip*/archive.zip') as resp: 192 | await async_save_response_to(resp, filename) 193 | 194 | 195 | class AsyncFreeStyleBuild(AsyncBuild): 196 | pass 197 | 198 | 199 | class AsyncMatrixBuild(AsyncBuild): 200 | pass 201 | -------------------------------------------------------------------------------- /api4jenkins/credential.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | 4 | from .item import AsyncItem, Item 5 | from .mix import (AsyncConfigurationMixIn, AsyncDeletionMixIn, 6 | ConfigurationMixIn, DeletionMixIn) 7 | 8 | 9 | class Credentials(Item): 10 | 11 | def get(self, name): 12 | for key in self.api_json(tree='domains[urlName]')['domains'].keys(): 13 | if key == name: 14 | return Domain(self.jenkins, f'{self.url}domain/{key}/') 15 | return None 16 | 17 | def create(self, xml): 18 | self.handle_req('POST', 'createDomain', 19 | headers=self.headers, content=xml) 20 | 21 | def iter(self): 22 | for key in self.api_json(tree='domains[urlName]')['domains'].keys(): 23 | yield Domain(self.jenkins, f'{self.url}domain/{key}/') 24 | 25 | @property 26 | def global_domain(self): 27 | return self['_'] 28 | 29 | 30 | class Domain(Item, ConfigurationMixIn, DeletionMixIn): 31 | 32 | def get(self, id): 33 | for item in self.api_json(tree='credentials[id]')['credentials']: 34 | if item['id'] == id: 35 | return Credential(self.jenkins, f'{self.url}credential/{id}/') 36 | return None 37 | 38 | def create(self, xml): 39 | self.handle_req('POST', 'createCredentials', 40 | headers=self.headers, content=xml) 41 | 42 | def iter(self): 43 | for item in self.api_json(tree='credentials[id]')['credentials']: 44 | yield Credential(self.jenkins, f'{self.url}credential/{item["id"]}/') 45 | 46 | 47 | class Credential(Item, ConfigurationMixIn, DeletionMixIn): 48 | pass 49 | 50 | 51 | # async class 52 | class AsyncCredentials(AsyncItem): 53 | 54 | async def get(self, name): 55 | data = await self.api_json(tree='domains[urlName]') 56 | for key in data['domains'].keys(): 57 | if key == name: 58 | return AsyncDomain(self.jenkins, f'{self.url}domain/{key}/') 59 | return None 60 | 61 | async def create(self, xml): 62 | await self.handle_req('POST', 'createDomain', 63 | headers=self.headers, content=xml) 64 | 65 | async def aiter(self): 66 | data = await self.api_json(tree='domains[urlName]') 67 | for key in data['domains'].keys(): 68 | yield Domain(self.jenkins, f'{self.url}domain/{key}/') 69 | 70 | @property 71 | async def global_domain(self): 72 | return await self['_'] 73 | 74 | 75 | class AsyncDomain(AsyncItem, AsyncConfigurationMixIn, AsyncDeletionMixIn): 76 | 77 | async def get(self, id): 78 | data = await self.api_json(tree='credentials[id]') 79 | for item in data['credentials']: 80 | if item['id'] == id: 81 | return AsyncCredential(self.jenkins, f'{self.url}credential/{id}/') 82 | return None 83 | 84 | async def create(self, xml): 85 | await self.handle_req('POST', 'createCredentials', 86 | headers=self.headers, content=xml) 87 | 88 | async def aiter(self): 89 | data = await self.api_json(tree='credentials[id]') 90 | for item in data['credentials']: 91 | yield AsyncCredential(self.jenkins, f'{self.url}credential/{item["id"]}/') 92 | 93 | 94 | class AsyncCredential(AsyncItem, AsyncConfigurationMixIn, AsyncDeletionMixIn): 95 | pass 96 | -------------------------------------------------------------------------------- /api4jenkins/exceptions.py: -------------------------------------------------------------------------------- 1 | 2 | class JenkinsAPIException(Exception): 3 | pass 4 | 5 | 6 | class ItemNotFoundError(JenkinsAPIException): 7 | pass 8 | 9 | 10 | class AuthenticationError(JenkinsAPIException): 11 | pass 12 | 13 | 14 | class ItemExistsError(JenkinsAPIException): 15 | pass 16 | 17 | 18 | class UnsafeCharacterError(JenkinsAPIException): 19 | pass 20 | 21 | 22 | class BadRequestError(JenkinsAPIException): 23 | pass 24 | 25 | 26 | class ServerError(JenkinsAPIException): 27 | pass 28 | -------------------------------------------------------------------------------- /api4jenkins/http.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import inspect 3 | import logging 4 | 5 | from httpx import (AsyncClient, AsyncHTTPTransport, Client, HTTPTransport, 6 | Request, Response) 7 | 8 | from .exceptions import AuthenticationError, BadRequestError, ItemNotFoundError 9 | 10 | logger = logging.getLogger(__name__) 11 | 12 | 13 | def log_request(request: Request) -> None: 14 | logger.debug( 15 | f"Send Request: {request.method} {request.url} - Waiting for response") 16 | 17 | 18 | def check_response(response: Response) -> None: 19 | if response.is_success or response.is_redirect: 20 | return 21 | if response.status_code == 404: 22 | raise ItemNotFoundError(f'404 Not found {response.url}') 23 | if response.status_code == 401: 24 | raise AuthenticationError( 25 | f'401 Invalid authorization for {response.url}') 26 | if response.status_code == 403: 27 | raise PermissionError(f'403 No permission to access {response.url}') 28 | if response.status_code == 400: 29 | raise BadRequestError(f'400 {response.headers["X-Error"]}') 30 | response.raise_for_status() 31 | 32 | 33 | def _new_transport(obj, kwargs): 34 | init_args = { 35 | arg: kwargs.pop(arg) 36 | for arg in inspect.getfullargspec(obj).args 37 | if arg in kwargs 38 | } 39 | return obj(**init_args) 40 | 41 | 42 | def new_http_client(**kwargs) -> Client: 43 | trans = _new_transport(HTTPTransport, kwargs) 44 | return Client( 45 | transport=trans, 46 | **kwargs, 47 | event_hooks={'request': [log_request], 'response': [check_response]} 48 | ) 49 | 50 | 51 | async def async_log_request(request: Request) -> None: 52 | logger.debug( 53 | f"Send Request: {request.method} {request.url} - Waiting for response") 54 | 55 | 56 | async def async_check_response(response: Response) -> None: 57 | check_response(response) 58 | 59 | 60 | def new_async_http_client(**kwargs) -> AsyncClient: 61 | trans = _new_transport(AsyncHTTPTransport, kwargs) 62 | return AsyncClient( 63 | transport=trans, 64 | **kwargs, 65 | event_hooks={'request': [async_log_request], 66 | 'response': [async_check_response]} 67 | ) 68 | -------------------------------------------------------------------------------- /api4jenkins/input.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import json 3 | 4 | from .item import AsyncItem, Item 5 | from .mix import AsyncRawJsonMixIn, RawJsonMixIn 6 | 7 | 8 | class PendingInputAction(RawJsonMixIn, Item): 9 | ''' this class implement functionality to process 10 | `input step `_ 11 | ''' 12 | 13 | def __init__(self, jenkins, raw): 14 | super().__init__( 15 | jenkins, f"{jenkins.url}{raw['abortUrl'].rstrip('abort').lstrip('/')}") 16 | self.raw = raw 17 | self.raw['_class'] = 'PendingInputAction' 18 | 19 | def abort(self): 20 | '''submit `input step `_''' 21 | return self.handle_req('POST', 'abort') 22 | 23 | def submit(self, **params): 24 | '''submit `input step `_ 25 | 26 | - if input requires parameters: 27 | - if submit without parameters, it will use default value of parameters 28 | - if submit with wrong parameters, exception raised 29 | - if input does not requires parameters, but submit with parameters, exception raised 30 | ''' 31 | if params: 32 | data = _make_input_params(self.raw, **params) 33 | return self.handle_req('POST', 'submit', data=data) 34 | return self.handle_req('POST', 'proceedEmpty') 35 | 36 | 37 | def _make_input_params(api_json, **params): 38 | input_args = [input['name'] for input in api_json['inputs']] 39 | params_keys = list(params.keys()) 40 | if not input_args: 41 | raise TypeError(f'input takes 0 argument, but got {params_keys}') 42 | if any(k not in input_args for k in params_keys): 43 | raise TypeError( 44 | f'input takes arguments: {input_args}, but got {params_keys}') 45 | params = [{'name': k, 'value': v} for k, v in params.items()] 46 | return {'proceed': api_json['proceedText'], 47 | 'json': json.dumps({'parameter': params})} 48 | 49 | 50 | class AsyncPendingInputAction(AsyncRawJsonMixIn, AsyncItem): 51 | def __init__(self, jenkins, raw): 52 | super().__init__( 53 | jenkins, f"{jenkins.url}{raw['abortUrl'].rstrip('abort').lstrip('/')}") 54 | self.raw = raw 55 | self.raw['_class'] = 'AsyncPendingInputAction' 56 | 57 | async def abort(self): 58 | return await self.handle_req('POST', 'abort') 59 | 60 | async def submit(self, **params): 61 | if params: 62 | data = _make_input_params(self.raw, **params) 63 | return await self.handle_req('POST', 'submit', data=data) 64 | return await self.handle_req('POST', 'proceedEmpty') 65 | -------------------------------------------------------------------------------- /api4jenkins/item.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | import contextlib 4 | import re 5 | from importlib import import_module 6 | 7 | import api4jenkins 8 | 9 | from .exceptions import ItemNotFoundError 10 | 11 | 12 | def camel(s): 13 | if s[0] == '_': 14 | return s 15 | first, *other = s.split('_') 16 | return first.lower() + ''.join(x.title() for x in other) 17 | 18 | 19 | def _snake(): 20 | pattern = re.compile(r'(?' 74 | 75 | def _add_crumb(self, crumb, kwargs): 76 | if crumb: 77 | headers = kwargs.get('headers', {}) 78 | headers.update(crumb) 79 | kwargs['headers'] = headers 80 | 81 | @classmethod 82 | def _get_attr_names(cls, api_json): 83 | types = (int, str, bool, type(None)) 84 | cls._attr_names = [snake(k) 85 | for k in api_json if isinstance(api_json[k], types)] 86 | 87 | 88 | class Item(BaseItem): 89 | 90 | def api_json(self, tree='', depth=0): 91 | params = {'depth': depth} 92 | if tree: 93 | params['tree'] = tree 94 | return self.handle_req('GET', 'api/json', params=params).json() 95 | 96 | def handle_req(self, method, entry, **kwargs): 97 | self._add_crumb(self.jenkins.crumb, kwargs) 98 | return self._request(method, self.url + entry, **kwargs) 99 | 100 | @contextlib.contextmanager 101 | def handle_stream(self, method, entry, **kwargs): 102 | self._add_crumb(self.jenkins.crumb, kwargs) 103 | with self._stream(method, self.url + entry, **kwargs) as response: 104 | yield response 105 | 106 | def exists(self): 107 | try: 108 | self.api_json(tree='_class') 109 | return True 110 | except ItemNotFoundError: 111 | return False 112 | 113 | @property 114 | def dynamic_attrs(self): 115 | if not self._attr_names: 116 | self._get_attr_names(self.api_json()) 117 | return self._attr_names 118 | 119 | def __getattr__(self, name): 120 | if name in self.dynamic_attrs: 121 | attr = camel(name) 122 | return self.api_json(tree=attr)[attr] 123 | return super().__getattribute__(name) 124 | 125 | def __getitem__(self, name): 126 | if hasattr(self, 'get'): 127 | return self.get(name) 128 | raise TypeError(f"'{type(self).__name__}' object is not subscriptable") 129 | 130 | def iter(self): 131 | raise TypeError(f"'{type(self).__name__}' object is not iterable") 132 | 133 | def __iter__(self): 134 | yield from self.iter() 135 | 136 | 137 | class AsyncItem(BaseItem): 138 | 139 | async def api_json(self, tree='', depth=0): 140 | params = {'depth': depth} 141 | if tree: 142 | params['tree'] = tree 143 | return (await self.handle_req('GET', 'api/json', params=params)).json() 144 | 145 | async def handle_req(self, method, entry, **kwargs): 146 | self._add_crumb(await self.jenkins.crumb, kwargs) 147 | return await self._request(method, self.url + entry, **kwargs) 148 | 149 | @contextlib.asynccontextmanager 150 | async def handle_stream(self, method, entry, **kwargs): 151 | self._add_crumb(await self.jenkins.crumb, kwargs) 152 | async with self._stream(method, self.url + entry, **kwargs) as response: 153 | yield response 154 | 155 | async def exists(self): 156 | try: 157 | await self.api_json(tree='_class') 158 | return True 159 | except ItemNotFoundError: 160 | return False 161 | 162 | @property 163 | async def dynamic_attrs(self): 164 | if not self._attr_names: 165 | self._get_attr_names(await self.api_json()) 166 | return self._attr_names 167 | 168 | async def __getattr__(self, name): 169 | if name in (await self.dynamic_attrs): 170 | attr = camel(name) 171 | return (await self.api_json(tree=attr))[attr] 172 | return super().__getattribute__(name) 173 | 174 | async def __getitem__(self, name): 175 | if hasattr(self, 'get'): 176 | return await self.get(name) 177 | raise TypeError(f"'{type(self).__name__}' object is not subscriptable") 178 | 179 | async def aiter(self): 180 | raise TypeError(f"'{type(self).__name__}' object is not iterable") 181 | 182 | async def __aiter__(self): 183 | async for item in self.aiter(): 184 | yield item 185 | -------------------------------------------------------------------------------- /api4jenkins/mix.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | # pylint: disable=no-member 4 | # type: ignore 5 | from collections import namedtuple 6 | from pathlib import PurePosixPath 7 | 8 | 9 | class UrlMixIn: 10 | __slots__ = () 11 | 12 | def _url2name(self, url): 13 | if not url.startswith(self.url): 14 | raise ValueError(f'{url} is not in {self.url}') 15 | return url.replace(self.url, '/').replace('/job/', '/').strip('/') 16 | 17 | def _name2url(self, full_name): 18 | if not full_name: 19 | return self.url 20 | full_name = full_name.strip('/').replace('/', '/job/') 21 | return f'{self.url}job/{full_name}/' 22 | 23 | def _parse_name(self, full_name): 24 | if full_name.startswith(('http://', 'https://')): 25 | full_name = self._url2name(full_name) 26 | path = PurePosixPath(full_name) 27 | parent = str(path.parent) if path.parent.name else '' 28 | return parent, path.name 29 | 30 | 31 | class DeletionMixIn: 32 | __slots__ = () 33 | 34 | def delete(self): 35 | self.handle_req('POST', 'doDelete') 36 | 37 | 38 | class ConfigurationMixIn: 39 | __slots__ = () 40 | 41 | def configure(self, xml=None): 42 | if not xml: 43 | return self.handle_req('GET', 'config.xml').text 44 | return self.handle_req('POST', 'config.xml', 45 | headers=self.headers, content=xml) 46 | 47 | @property 48 | def name(self): 49 | return self.url.split('/')[-2] 50 | 51 | 52 | class DescriptionMixIn: 53 | __slots__ = () 54 | 55 | def set_description(self, text): 56 | self.handle_req('POST', 'submitDescription', 57 | params={'description': text}) 58 | 59 | 60 | class RunScriptMixIn: 61 | __slots__ = () 62 | 63 | def run_script(self, script): 64 | return self.handle_req('POST', 'scriptText', 65 | data={'script': script}).text 66 | 67 | 68 | class EnableMixIn: 69 | __slots__ = () 70 | 71 | def enable(self): 72 | return self.handle_req('POST', 'enable') 73 | 74 | def disable(self): 75 | return self.handle_req('POST', 'disable') 76 | 77 | 78 | class RawJsonMixIn: 79 | __slots__ = () 80 | 81 | def api_json(self, tree='', depth=0): 82 | return self.raw 83 | 84 | 85 | Parameter = namedtuple('Parameter', ['class_name', 'name', 'value']) 86 | 87 | 88 | class ActionsMixIn: 89 | __slots__ = () 90 | 91 | def get_parameters(self): 92 | parameters = [] 93 | for action in self.api_json()['actions']: 94 | if 'parameters' in action: 95 | parameters.extend(Parameter(raw['_class'], raw['name'], raw.get( 96 | 'value', '')) for raw in action['parameters']) 97 | break 98 | return parameters 99 | 100 | def get_causes(self): 101 | return next((action['causes'] for action in self.api_json()['actions'] if 'causes' in action), []) 102 | 103 | 104 | # async classes 105 | 106 | 107 | class AsyncDeletionMixIn: 108 | __slots__ = () 109 | 110 | async def delete(self): 111 | await self.handle_req('POST', 'doDelete') 112 | 113 | 114 | class AsyncConfigurationMixIn: 115 | __slots__ = () 116 | 117 | async def configure(self, xml=None): 118 | if xml: 119 | return await self.handle_req('POST', 'config.xml', 120 | headers=self.headers, content=xml) 121 | return (await self.handle_req('GET', 'config.xml')).text 122 | 123 | @property 124 | def name(self): 125 | return self.url.split('/')[-2] 126 | 127 | 128 | class AsyncDescriptionMixIn: 129 | __slots__ = () 130 | 131 | async def set_description(self, text): 132 | await self.handle_req('POST', 'submitDescription', 133 | params={'description': text}) 134 | 135 | 136 | class AsyncRunScriptMixIn: 137 | __slots__ = () 138 | 139 | async def run_script(self, script): 140 | return (await self.handle_req('POST', 'scriptText', data={'script': script})).text 141 | 142 | 143 | class AsyncEnableMixIn: 144 | __slots__ = () 145 | 146 | async def enable(self): 147 | return await self.handle_req('POST', 'enable') 148 | 149 | async def disable(self): 150 | return await self.handle_req('POST', 'disable') 151 | 152 | 153 | class AsyncRawJsonMixIn: 154 | __slots__ = () 155 | 156 | async def api_json(self, tree='', depth=0): 157 | return self.raw 158 | 159 | 160 | class AsyncActionsMixIn: 161 | __slots__ = () 162 | 163 | async def get_parameters(self): 164 | parameters = [] 165 | data = await self.api_json() 166 | for action in data['actions']: 167 | if 'parameters' in action: 168 | parameters.extend(Parameter(raw['_class'], raw['name'], raw.get( 169 | 'value', '')) for raw in action['parameters']) 170 | break 171 | return parameters 172 | 173 | async def get_causes(self): 174 | data = await self.api_json() 175 | return next((action['causes'] for action in data['actions'] if 'causes' in action), []) 176 | -------------------------------------------------------------------------------- /api4jenkins/node.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | import json 4 | 5 | from .exceptions import ItemNotFoundError 6 | from .item import AsyncItem, Item, new_item 7 | from .mix import (AsyncConfigurationMixIn, AsyncDeletionMixIn, 8 | AsyncRunScriptMixIn, ConfigurationMixIn, DeletionMixIn, 9 | RunScriptMixIn) 10 | 11 | # query builds from 'executors', 'oneOffExecutors' in computer(s), 12 | # cause freestylebuild is in executors, and workflowbuild has different _class in 13 | # executors(..PlaceholderTask$PlaceholderExecutable) and oneOffExecutors(org.jenkinsci.plugins.workflow.job.WorkflowRun) 14 | _nodes_tree = ('computer[executors[currentExecutable[url]],' 15 | 'oneOffExecutors[currentExecutable[url]]]') 16 | 17 | _node_tree = ('executors[currentExecutable[url]],' 18 | 'oneOffExecutors[currentExecutable[url]]') 19 | 20 | 21 | def _make_node_setting(name, **kwargs): 22 | node_setting = { 23 | 'nodeDescription': '', 24 | 'numExecutors': 1, 25 | 'remoteFS': '/home/jenkins', 26 | 'labelString': '', 27 | 'mode': 'NORMAL', 28 | 'retentionStrategy': { 29 | 'stapler-class': 'hudson.slaves.RetentionStrategy$Always' 30 | }, 31 | 'nodeProperties': {'stapler-class-bag': 'true'}, 32 | 'launcher': {'stapler-class': 'hudson.slaves.JNLPLauncher'} 33 | } 34 | node_setting.update(kwargs) 35 | return { 36 | 'name': name, 37 | 'type': 'hudson.slaves.DumbSlave$DescriptorImpl', 38 | 'json': json.dumps(node_setting) 39 | } 40 | 41 | 42 | def _new_builds(jenkins, api_json): 43 | for computer in api_json['computer']: 44 | for item in _parse_builds(computer): 45 | yield new_item(jenkins, 'api4jenkins.build', item) 46 | 47 | 48 | def _parse_builds(data): 49 | for kind in ['executors', 'oneOffExecutors']: 50 | for executor in data.get(kind): 51 | # in case of issue: 52 | # https://github.com/joelee2012/api4jenkins/issues/16 53 | execable = executor['currentExecutable'] 54 | if execable and not execable['_class'].endswith('PlaceholderExecutable'): 55 | yield {'url': execable['url'], '_class': execable['_class']} 56 | 57 | 58 | def _iter_node(jenkins, api_json): 59 | for item in api_json['computer']: 60 | item['url'] = f"{jenkins.url}computer/{item['displayName']}/" 61 | yield new_item(jenkins, __name__, item) 62 | 63 | 64 | def _get_node(jenkins, api_json, name): 65 | for item in api_json['computer']: 66 | if name == item['displayName']: 67 | item['url'] = f"{jenkins.url}computer/{item['displayName']}/" 68 | return new_item(jenkins, __name__, item) 69 | return None 70 | 71 | 72 | class IterBuildingBuildsMixIn: 73 | # pylint: disable=no-member 74 | def iter_building_builds(self): 75 | yield from filter(lambda build: build.building, self.iter_builds()) 76 | 77 | 78 | class Nodes(Item, IterBuildingBuildsMixIn): 79 | ''' 80 | classdocs 81 | ''' 82 | 83 | def create(self, name, **kwargs): 84 | self.handle_req('POST', 'doCreateItem', 85 | data=_make_node_setting(name, **kwargs)) 86 | 87 | def get(self, name): 88 | return _get_node(self.jenkins, self.api_json(tree='computer[displayName]'), name) 89 | 90 | def iter_builds(self): 91 | yield from _new_builds(self.jenkins, self.api_json(_nodes_tree, 2)) 92 | 93 | def iter(self): 94 | yield from _iter_node(self.jenkins, self.api_json(tree='computer[displayName]')) 95 | 96 | def filter_node_by_label(self, *labels): 97 | for node in self: 98 | for label in node.api_json()['assignedLabels']: 99 | if label['name'] in labels: 100 | yield node 101 | 102 | def filter_node_by_status(self, *, online): 103 | yield from filter(lambda node: online != node.offline, self) 104 | 105 | # following two functions should be used in this module only 106 | 107 | 108 | class Node(Item, ConfigurationMixIn, DeletionMixIn, RunScriptMixIn, IterBuildingBuildsMixIn): 109 | 110 | def enable(self): 111 | if self.offline: 112 | self.handle_req('POST', 'toggleOffline', 113 | params={'offlineMessage': ''}) 114 | 115 | def disable(self, msg=''): 116 | if not self.offline: 117 | self.handle_req('POST', 'toggleOffline', 118 | params={'offlineMessage': msg}) 119 | 120 | def iter_builds(self): 121 | for item in _parse_builds(self.api_json(_node_tree, 2)): 122 | yield new_item(self.jenkins, 'api4jenkins.build', item) 123 | 124 | def __iter__(self): 125 | yield from self.iter_builds() 126 | 127 | 128 | class MasterComputerMixIn: 129 | def __init__(self, jenkins, url): 130 | # rename built-in node: https://www.jenkins.io/doc/upgrade-guide/2.319/ 131 | name = 'master' if url.endswith('/master/') else 'built-in' 132 | super().__init__(jenkins, f'{jenkins.url}computer/({name})/') 133 | 134 | 135 | class MasterComputer(MasterComputerMixIn, Node): 136 | pass 137 | 138 | 139 | class SlaveComputer(Node): 140 | pass 141 | 142 | 143 | class KubernetesComputer(Node): 144 | pass 145 | 146 | 147 | class DockerComputer(Node): 148 | pass 149 | 150 | 151 | class EC2Computer(Node): 152 | pass 153 | 154 | 155 | class AsyncIterBuildingBuildsMixIn: 156 | # pylint: disable=no-member 157 | async def iter_building_builds(self): 158 | async for build in self.iter_builds(): 159 | if await build.building: 160 | yield build 161 | 162 | 163 | class AsyncNodes(AsyncItem, AsyncIterBuildingBuildsMixIn): 164 | async def create(self, name, **kwargs): 165 | await self.handle_req('POST', 'doCreateItem', data=_make_node_setting(name, **kwargs)) 166 | 167 | async def get(self, name): 168 | return _get_node(self.jenkins, await self.api_json(tree='computer[displayName]'), name) 169 | 170 | async def iter_builds(self): 171 | for build in _new_builds(self.jenkins, await self.api_json(_nodes_tree, 2)): 172 | yield build 173 | 174 | async def aiter(self): 175 | data = await self.api_json(tree='computer[displayName]') 176 | for node in _iter_node(self.jenkins, data): 177 | yield node 178 | 179 | async def filter_node_by_label(self, *labels): 180 | async for node in self: 181 | data = await node.api_json() 182 | for label in data['assignedLabels']: 183 | if label['name'] in labels: 184 | yield node 185 | 186 | async def filter_node_by_status(self, *, online): 187 | async for node in self: 188 | if online != await node.offline: 189 | yield node 190 | 191 | 192 | class AsyncNode(AsyncItem, AsyncConfigurationMixIn, AsyncDeletionMixIn, AsyncRunScriptMixIn, AsyncIterBuildingBuildsMixIn): 193 | 194 | async def enable(self): 195 | if await self.offline: 196 | await self.handle_req('POST', 'toggleOffline', 197 | params={'offlineMessage': ''}) 198 | 199 | async def disable(self, msg=''): 200 | if not await self.offline: 201 | await self.handle_req('POST', 'toggleOffline', 202 | params={'offlineMessage': msg}) 203 | 204 | async def iter_builds(self): 205 | for item in _parse_builds(await self.api_json(_node_tree, 2)): 206 | yield new_item(self.jenkins, 'api4jenkins.build', item) 207 | 208 | async def __aiter__(self): 209 | async for build in self.iter_builds(): 210 | yield build 211 | 212 | 213 | class AsyncMasterComputer(MasterComputerMixIn, AsyncNode): 214 | pass 215 | 216 | 217 | class AsyncSlaveComputer(AsyncNode): 218 | pass 219 | 220 | 221 | class AsyncKubernetesComputer(AsyncNode): 222 | pass 223 | 224 | 225 | class AsyncDockerComputer(AsyncNode): 226 | pass 227 | 228 | 229 | class AsyncEC2Computer(AsyncNode): 230 | pass 231 | -------------------------------------------------------------------------------- /api4jenkins/plugin.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import json 3 | import time 4 | import xml.etree.ElementTree as ET 5 | 6 | from .item import AsyncItem, Item 7 | 8 | 9 | class PluginsManager(Item): 10 | 11 | def get(self, name): 12 | for plugin in self.api_json(tree='plugins[shortName]')['plugins']: 13 | if plugin['shortName'] == name: 14 | return Plugin(self.jenkins, f'{self.url}plugin/{name}/') 15 | return None 16 | 17 | def install(self, *names, block=False): 18 | plugin_xml = ET.Element('jenkins') 19 | for name in names: 20 | if '@' not in name: 21 | name += '@latest' 22 | ET.SubElement(plugin_xml, 'install', {'plugin': name}) 23 | self.handle_req('POST', 'installNecessaryPlugins', 24 | headers=self.headers, 25 | content=ET.tostring(plugin_xml)) 26 | 27 | while block and not self.installation_done: 28 | time.sleep(2) 29 | 30 | def uninstall(self, *names): 31 | for name in names: 32 | self.handle_req('POST', f'plugin/{name}/doUninstall') 33 | 34 | def set_site(self, url): 35 | self.handle_req('POST', 'siteConfigure', params={'site': url}) 36 | self.check_updates_server() 37 | 38 | def check_updates_server(self): 39 | self.handle_req('POST', 'checkUpdatesServer') 40 | 41 | @property 42 | def update_center(self): 43 | return UpdateCenter(self.jenkins, f'{self.jenkins.url}updateCenter/') 44 | 45 | @property 46 | def site(self): 47 | return self.update_center.site 48 | 49 | @property 50 | def restart_required(self): 51 | return self.update_center.restart_required 52 | 53 | @property 54 | def installation_done(self): 55 | return self.update_center.installation_done 56 | 57 | def set_proxy(self, name, port, *, username='', 58 | password='', no_proxy='', test_url=''): 59 | data = {'name': name, 'port': port, 'userName': username, 60 | 'password': password, 'noProxyHost': no_proxy, 61 | 'testUrl': test_url} 62 | self.handle_req('POST', 'proxyConfigure', data={ 63 | 'json': json.dumps(data)}) 64 | 65 | def __iter__(self): 66 | for plugin in self.api_json(tree='plugins[shortName]')['plugins']: 67 | yield Plugin(self.jenkins, 68 | f'{self.url}plugin/{plugin["shortName"]}/') 69 | 70 | 71 | class Plugin(Item): 72 | def uninstall(self): 73 | self.handle_req('POST', 'doUninstall') 74 | 75 | 76 | class UpdateCenter(Item): 77 | 78 | @property 79 | def installation_done(self): 80 | resp = self.handle_req('GET', 'installStatus') 81 | return all(job['installStatus'] != 'Pending' 82 | for job in resp.json()['data']['jobs']) 83 | 84 | @property 85 | def restart_required(self): 86 | return self.api_json(tree='restartRequiredForCompletion').get( 87 | 'restartRequiredForCompletion') 88 | 89 | @property 90 | def site(self): 91 | return self.api_json(tree='sites[url]')['sites'][0].get('url') 92 | 93 | 94 | # async class 95 | 96 | class AsyncPluginsManager(AsyncItem): 97 | 98 | async def get(self, name): 99 | data = await self.api_json(tree='plugins[shortName]') 100 | for plugin in data['plugins']: 101 | if plugin['shortName'] == name: 102 | return AsyncPlugin(self.jenkins, f'{self.url}plugin/{name}/') 103 | return None 104 | 105 | async def install(self, *names, block=False): 106 | plugin_xml = ET.Element('jenkins') 107 | for name in names: 108 | if '@' not in name: 109 | name += '@latest' 110 | ET.SubElement(plugin_xml, 'install', {'plugin': name}) 111 | await self.handle_req('POST', 'installNecessaryPlugins', 112 | headers=self.headers, 113 | content=ET.tostring(plugin_xml)) 114 | 115 | while block and not await self.installation_done: 116 | time.sleep(2) 117 | 118 | async def uninstall(self, *names): 119 | for name in names: 120 | await self.handle_req('POST', f'plugin/{name}/doUninstall') 121 | 122 | async def set_site(self, url): 123 | await self.handle_req('POST', 'siteConfigure', params={'site': url}) 124 | await self.check_updates_server() 125 | 126 | async def check_updates_server(self): 127 | await self.handle_req('POST', 'checkUpdatesServer') 128 | 129 | @property 130 | def update_center(self): 131 | return AsyncUpdateCenter(self.jenkins, f'{self.jenkins.url}updateCenter/') 132 | 133 | @property 134 | def site(self): 135 | return self.update_center.site 136 | 137 | @property 138 | def restart_required(self): 139 | return self.update_center.restart_required 140 | 141 | @property 142 | def installation_done(self): 143 | return self.update_center.installation_done 144 | 145 | async def set_proxy(self, name, port, *, username='', 146 | password='', no_proxy='', test_url=''): 147 | data = {'name': name, 'port': port, 'userName': username, 148 | 'password': password, 'noProxyHost': no_proxy, 149 | 'testUrl': test_url} 150 | await self.handle_req('POST', 'proxyConfigure', data={ 151 | 'json': json.dumps(data)}) 152 | 153 | async def __aiter__(self): 154 | data = await self.api_json(tree='plugins[shortName]') 155 | for plugin in data['plugins']: 156 | yield AsyncPlugin(self.jenkins, 157 | f'{self.url}plugin/{plugin["shortName"]}/') 158 | 159 | 160 | class AsyncPlugin(AsyncItem): 161 | async def uninstall(self): 162 | await self.handle_req('POST', 'doUninstall') 163 | 164 | 165 | class AsyncUpdateCenter(AsyncItem): 166 | 167 | @property 168 | async def installation_done(self): 169 | resp = await self.handle_req('GET', 'installStatus') 170 | return all(job['installStatus'] != 'Pending' 171 | for job in resp.json()['data']['jobs']) 172 | 173 | @property 174 | async def restart_required(self): 175 | data = await self.api_json(tree='restartRequiredForCompletion') 176 | return data.get('restartRequiredForCompletion') 177 | 178 | @property 179 | async def site(self): 180 | data = await self.api_json(tree='sites[url]') 181 | return data['sites'][0].get('url') 182 | -------------------------------------------------------------------------------- /api4jenkins/queue.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import re 3 | 4 | from .item import AsyncItem, Item 5 | from .mix import (ActionsMixIn, AsyncActionsMixIn) 6 | 7 | 8 | class Queue(Item): 9 | def get(self, id): 10 | for item in self.api_json(tree='items[id,url]')['items']: 11 | if item['id'] == int(id): 12 | return QueueItem(self.jenkins, 13 | f"{self.jenkins.url}{item['url']}") 14 | return None 15 | 16 | def cancel(self, id): 17 | self.handle_req('POST', 'cancelItem', params={'id': id}) 18 | 19 | def __iter__(self): 20 | for item in self.api_json(tree='items[url]')['items']: 21 | yield QueueItem(self.jenkins, f"{self.jenkins.url}{item['url']}") 22 | 23 | # https://javadoc.jenkins.io/hudson/model/Queue.html#buildables 24 | # (enter) --> waitingList --+--> blockedProjects 25 | # | ^ 26 | # | | 27 | # | v 28 | # +--> buildables ---> pending ---> left 29 | # ^ | 30 | # | | 31 | # +---(rarely)---+ 32 | 33 | 34 | class QueueItem(Item, ActionsMixIn): 35 | 36 | def __init__(self, jenkins, url): 37 | if not url.startswith('https') and jenkins.url.startswith('https'): 38 | url = re.sub(r'^http[s]', 'https', url) 39 | super().__init__(jenkins, url) 40 | self.id = int(self.url.split('/')[-2]) 41 | self._build = None 42 | 43 | def get_job(self): 44 | if self._class.endswith('$BuildableItem'): 45 | return self.get_build().project 46 | task = self.api_json(tree='task[url]')['task'] 47 | return self._new_item('api4jenkins.job', task) 48 | 49 | def get_build(self): 50 | if not self._build: 51 | _class = self._class 52 | # BlockedItem does not have build 53 | if _class.endswith('$LeftItem'): 54 | executable = self.api_json('executable[url]')['executable'] 55 | self._build = self._new_item( 56 | 'api4jenkins.build', executable) 57 | elif _class.endswith(('$BuildableItem', '$WaitingItem')): 58 | for build in self.jenkins.nodes.iter_builds(): 59 | # https://javadoc.jenkins.io/hudson/model/Run.html#getQueueId-- 60 | # https://javadoc.jenkins.io/hudson/model/Queue.Item.html#getId-- 61 | # ensure build exists, see https://github.com/joelee2012/api4jenkins/issues/49 62 | if build.exists() and int(build.queue_id) == self.id: 63 | self._build = build 64 | break 65 | return self._build 66 | 67 | def cancel(self): 68 | self.jenkins.queue.cancel(self.id) 69 | 70 | # due to item type is dynamic 71 | 72 | 73 | class BuildableItem(QueueItem): 74 | pass 75 | 76 | 77 | class BlockedItem(QueueItem): 78 | pass 79 | 80 | 81 | class LeftItem(QueueItem): 82 | pass 83 | 84 | 85 | class WaitingItem(QueueItem): 86 | pass 87 | 88 | # async class 89 | 90 | 91 | class AsyncQueue(AsyncItem): 92 | 93 | async def get(self, id): 94 | for item in (await self.api_json(tree='items[id,url]'))['items']: 95 | if item['id'] == int(id): 96 | return AsyncQueueItem(self.jenkins, 97 | f"{self.jenkins.url}{item['url']}") 98 | return None 99 | 100 | async def cancel(self, id): 101 | await self.handle_req('POST', 'cancelItem', params={'id': id}) 102 | 103 | async def __aiter__(self): 104 | for item in (await self.api_json(tree='items[url]'))['items']: 105 | yield AsyncQueueItem(self.jenkins, f"{self.jenkins.url}{item['url']}") 106 | 107 | 108 | class AsyncQueueItem(AsyncItem, AsyncActionsMixIn): 109 | 110 | def __init__(self, jenkins, url): 111 | if not url.startswith('https') and jenkins.url.startswith('https'): 112 | url = re.sub(r'^http[s]', 'https', url) 113 | super().__init__(jenkins, url) 114 | self.id = int(self.url.split('/')[-2]) 115 | self._build = None 116 | 117 | async def get_job(self): 118 | _class = await self._class 119 | if _class.endswith('$BuildableItem'): 120 | build = await self.get_build() 121 | return await build.project 122 | data = await self.api_json(tree='task[url]') 123 | return self._new_item('api4jenkins.job', data['task']) 124 | 125 | async def get_build(self): 126 | if not self._build: 127 | _class = await self._class 128 | # BlockedItem does not have build 129 | if _class.endswith('$LeftItem'): 130 | data = await self.api_json('executable[url]') 131 | self._build = self._new_item( 132 | 'api4jenkins.build', data['executable']) 133 | elif _class.endswith(('$BuildableItem', '$WaitingItem')): 134 | async for build in self.jenkins.nodes.iter_builds(): 135 | # https://javadoc.jenkins.io/hudson/model/Run.html#getQueueId-- 136 | # https://javadoc.jenkins.io/hudson/model/Queue.Item.html#getId-- 137 | # ensure build exists, see https://github.com/joelee2012/api4jenkins/issues/49 138 | if await build.exists() and int(await build.queue_id) == self.id: 139 | self._build = build 140 | break 141 | return self._build 142 | 143 | async def cancel(self): 144 | await self.jenkins.queue.cancel(self.id) 145 | 146 | 147 | class AsyncBuildableItem(AsyncQueueItem): 148 | pass 149 | 150 | 151 | class AsyncBlockedItem(AsyncQueueItem): 152 | pass 153 | 154 | 155 | class AsyncLeftItem(AsyncQueueItem): 156 | pass 157 | 158 | 159 | class AsyncWaitingItem(AsyncQueueItem): 160 | pass 161 | -------------------------------------------------------------------------------- /api4jenkins/report.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | from .item import AsyncItem, Item, camel, snake 3 | 4 | 5 | class GetMixIn: 6 | 7 | def get(self, name): 8 | return next((item for item in self if item.name == name), None) 9 | 10 | 11 | class ResultBase: 12 | 13 | def __init__(self, raw): 14 | self.raw = raw 15 | 16 | def __getattr__(self, name): 17 | camel_name = camel(name) 18 | if camel_name in self.raw: 19 | return self.raw[camel_name] 20 | return super().__getattribute__(name) 21 | 22 | def __str__(self): 23 | return f'<{type(self).__name__}: {self.name}>' 24 | 25 | def __dir__(self): 26 | return super().__dir__() + [snake(k) for k in self.raw] 27 | 28 | 29 | class TestReport(Item, GetMixIn): 30 | 31 | def __iter__(self): 32 | for suite in self.api_json()['suites']: 33 | yield TestSuite(suite) 34 | 35 | @property 36 | def suites(self): 37 | yield from self 38 | 39 | 40 | class TestSuite(ResultBase, GetMixIn): 41 | 42 | def __iter__(self): 43 | for case in self.raw['cases']: 44 | yield TestCase(case) 45 | 46 | @property 47 | def cases(self): 48 | yield from self 49 | 50 | 51 | class TestCase(ResultBase): 52 | pass 53 | 54 | 55 | class CoverageReport(Item, GetMixIn): 56 | '''Access coverage report generated by `JaCoCo `_''' 57 | 58 | report_types = ['branchCoverage', 'classCoverage', 'complexityScore', 59 | 'instructionCoverage', 'lineCoverage', 'methodCoverage'] 60 | 61 | def __getattr__(self, name): 62 | attr = camel(name) 63 | if attr not in self.report_types: 64 | raise AttributeError( 65 | f"'CoverageReport' object has no attribute '{name}'") 66 | return self.get(attr) 67 | 68 | def __iter__(self): 69 | for k, v in self.api_json().items(): 70 | if k not in ['_class', 'previousResult']: 71 | v['name'] = k 72 | yield Coverage(v) 73 | 74 | def trends(self, count=2): 75 | def _resolve(data): 76 | if data['previousResult']: 77 | yield from _resolve(data['previousResult']) 78 | for k, v in data.items(): 79 | if k not in ['_class', 'previousResult']: 80 | v['name'] = k 81 | yield Coverage(v) 82 | 83 | yield from _resolve(self.api_json(depth=count)) 84 | 85 | 86 | class Coverage(ResultBase): 87 | pass 88 | 89 | 90 | class CoverageResult(Item, GetMixIn): 91 | '''Access coverage result generated by `Code Coverage API `_''' 92 | 93 | def __iter__(self): 94 | for element in self.api_json(depth=1)['results']['elements']: 95 | yield CoverageElement(element) 96 | 97 | 98 | class CoverageElement(ResultBase): 99 | pass 100 | 101 | 102 | class CoverageTrends(Item, GetMixIn): 103 | def __iter__(self): 104 | for trend in self.api_json(depth=1)['trends']: 105 | trend['name'] = trend['buildName'] 106 | yield CoverageTrend(trend) 107 | 108 | 109 | class CoverageTrend(ResultBase): 110 | 111 | def __iter__(self): 112 | for element in self.raw['elements']: 113 | yield CoverageElement(element) 114 | 115 | 116 | # async class 117 | 118 | class AsyncGetMixIn: 119 | 120 | async def get(self, name): 121 | async for item in self: 122 | if item.name == name: 123 | return item 124 | 125 | 126 | class AsyncTestReport(AsyncItem, AsyncGetMixIn): 127 | 128 | async def __aiter__(self): 129 | data = await self.api_json() 130 | for suite in data['suites']: 131 | yield TestSuite(suite) 132 | 133 | @property 134 | async def suites(self): 135 | async for suite in self: 136 | yield suite 137 | 138 | 139 | class AsyncCoverageReport(AsyncItem, AsyncGetMixIn): 140 | '''Access coverage report generated by `JaCoCo `_''' 141 | 142 | report_types = ['branchCoverage', 'classCoverage', 'complexityScore', 143 | 'instructionCoverage', 'lineCoverage', 'methodCoverage'] 144 | 145 | async def __getattr__(self, name): 146 | attr = camel(name) 147 | if attr not in self.report_types: 148 | raise AttributeError( 149 | f"'CoverageReport' object has no attribute '{name}'") 150 | return await self.get(attr) 151 | 152 | async def __aiter__(self): 153 | data = await self.api_json() 154 | for k, v in data.items(): 155 | if k not in ['_class', 'previousResult']: 156 | v['name'] = k 157 | yield Coverage(v) 158 | 159 | async def trends(self, count=2): 160 | def _resolve(data): 161 | if data['previousResult']: 162 | yield from _resolve(data['previousResult']) 163 | for k, v in data.items(): 164 | if k not in ['_class', 'previousResult']: 165 | v['name'] = k 166 | yield Coverage(v) 167 | data = await self.api_json(depth=count) 168 | for c in _resolve(data): 169 | yield c 170 | 171 | 172 | class AsyncCoverageResult(AsyncItem, AsyncGetMixIn): 173 | '''Access coverage result generated by `Code Coverage API `_''' 174 | 175 | async def __aiter__(self): 176 | data = await self.api_json(depth=1) 177 | for element in data['results']['elements']: 178 | yield CoverageElement(element) 179 | 180 | 181 | class AsyncCoverageTrends(AsyncItem, AsyncGetMixIn): 182 | async def __aiter__(self): 183 | data = await self.api_json(depth=1) 184 | for trend in data['trends']: 185 | trend['name'] = trend['buildName'] 186 | yield CoverageTrend(trend) 187 | -------------------------------------------------------------------------------- /api4jenkins/system.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import json 3 | from functools import partial 4 | 5 | from .item import AsyncItem, Item, snake 6 | from .mix import AsyncRunScriptMixIn, RunScriptMixIn 7 | 8 | 9 | class System(Item, RunScriptMixIn): 10 | 11 | def __init__(self, jenkins, url): 12 | ''' 13 | see: https://support.cloudbees.com/hc/en-us/articles/216118748-How-to-Start-Stop-or-Restart-your-Instance- 14 | ''' 15 | super().__init__(jenkins, url) 16 | 17 | def _post(entry): 18 | return self.handle_req('POST', entry) 19 | 20 | for entry in ['restart', 'safeRestart', 'exit', 21 | 'safeExit', 'quietDown', 'cancelQuietDown']: 22 | setattr(self, snake(entry), partial(_post, entry)) 23 | 24 | def reload_jcasc(self): 25 | return self.handle_req('POST', 'configuration-as-code/reload') 26 | 27 | def export_jcasc(self): 28 | return self.handle_req('POST', 'configuration-as-code/export').text 29 | 30 | def apply_jcasc(self, content): 31 | params = {"newSource": content} 32 | resp = self.handle_req( 33 | 'POST', 'configuration-as-code/checkNewSource', params=params) 34 | if resp.text.startswith('
'): 35 | raise ValueError(resp.text) 36 | data = {'json': json.dumps(params), 37 | 'replace': 'Apply new configuration'} 38 | return self.handle_req('POST', 'configuration-as-code/replace', data=data) 39 | 40 | def decrypt_secret(self, text): 41 | cmd = f'println(hudson.util.Secret.decrypt("{text}"))' 42 | return self.run_script(cmd) 43 | 44 | # async class 45 | 46 | 47 | class AsyncSystem(AsyncItem, AsyncRunScriptMixIn): 48 | 49 | def __init__(self, jenkins, url): 50 | ''' 51 | see: https://support.cloudbees.com/hc/en-us/articles/216118748-How-to-Start-Stop-or-Restart-your-Instance- 52 | ''' 53 | super().__init__(jenkins, url) 54 | 55 | async def _post(entry): 56 | return await self.handle_req('POST', entry) 57 | 58 | for entry in ['restart', 'safeRestart', 'exit', 59 | 'safeExit', 'quietDown', 'cancelQuietDown']: 60 | setattr(self, snake(entry), partial(_post, entry)) 61 | 62 | async def reload_jcasc(self): 63 | return await self.handle_req('POST', 'configuration-as-code/reload') 64 | 65 | async def export_jcasc(self): 66 | data = await self.handle_req('POST', 'configuration-as-code/export') 67 | return data.text 68 | 69 | async def apply_jcasc(self, content): 70 | params = {"newSource": content} 71 | resp = await self.handle_req( 72 | 'POST', 'configuration-as-code/checkNewSource', params=params) 73 | if resp.text.startswith('
'): 74 | raise ValueError(resp.text) 75 | data = {'json': json.dumps(params), 76 | 'replace': 'Apply new configuration'} 77 | return await self.handle_req('POST', 'configuration-as-code/replace', data=data) 78 | 79 | async def decrypt_secret(self, text): 80 | cmd = f'println(hudson.util.Secret.decrypt("{text}"))' 81 | return await self.run_script(cmd) 82 | -------------------------------------------------------------------------------- /api4jenkins/user.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | from collections import namedtuple 3 | 4 | from .item import AsyncItem, Item 5 | from .mix import (AsyncDeletionMixIn, AsyncDescriptionMixIn, 6 | DeletionMixIn, DescriptionMixIn) 7 | 8 | user_tree = 'users[user[id,absoluteUrl,fullName]]' 9 | new_token_url = 'descriptorByName/jenkins.security.ApiTokenProperty/generateNewToken' 10 | revoke_token_url = 'descriptorByName/jenkins.security.ApiTokenProperty/revoke' 11 | 12 | 13 | class Users(Item): 14 | 15 | def __iter__(self): 16 | for user in self.api_json(tree=user_tree)['users']: 17 | yield User(self.jenkins, user['user']['absoluteUrl']) 18 | 19 | def get(self, name): 20 | for user in self.api_json(tree=user_tree)['users']: 21 | if name in [user['user']['id'], user['user']['fullName']]: 22 | return User(self.jenkins, user['user']['absoluteUrl']) 23 | return None 24 | 25 | 26 | ApiToken = namedtuple('ApiToken', ['name', 'uuid', 'value']) 27 | 28 | 29 | class User(Item, DeletionMixIn, DescriptionMixIn): 30 | 31 | def generate_token(self, name=''): 32 | data = self.handle_req('POST', new_token_url, 33 | params={'newTokenName': name}).json()['data'] 34 | return ApiToken(data['tokenName'], data['tokenUuid'], data['tokenValue']) 35 | 36 | def revoke_token(self, uuid): 37 | return self.handle_req('POST', revoke_token_url, params={'tokenUuid': uuid}) 38 | 39 | # async class 40 | 41 | 42 | class AsyncUsers(AsyncItem): 43 | 44 | async def __aiter__(self): 45 | for user in (await self.api_json(tree=user_tree))['users']: 46 | yield AsyncUser(self.jenkins, user['user']['absoluteUrl']) 47 | 48 | async def get(self, name): 49 | for user in (await self.api_json(tree=user_tree))['users']: 50 | if name in [user['user']['id'], user['user']['fullName']]: 51 | return AsyncUser(self.jenkins, user['user']['absoluteUrl']) 52 | return None 53 | 54 | 55 | class AsyncUser(AsyncItem, AsyncDeletionMixIn, AsyncDescriptionMixIn): 56 | 57 | async def generate_token(self, name=''): 58 | data = (await self.handle_req('POST', new_token_url, 59 | params={'newTokenName': name})).json()['data'] 60 | return ApiToken(data['tokenName'], data['tokenUuid'], data['tokenValue']) 61 | 62 | async def revoke_token(self, uuid): 63 | return await self.handle_req('POST', revoke_token_url, params={'tokenUuid': uuid}) 64 | -------------------------------------------------------------------------------- /api4jenkins/view.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | from .item import AsyncItem, Item 4 | from .mix import (AsyncConfigurationMixIn, AsyncDeletionMixIn, 5 | AsyncDescriptionMixIn, ConfigurationMixIn, 6 | DeletionMixIn, DescriptionMixIn) 7 | 8 | 9 | class Views(Item): 10 | ''' 11 | classdocs 12 | ''' 13 | 14 | def __init__(self, owner): 15 | ''' 16 | Constructor 17 | ''' 18 | self.owner = owner 19 | super().__init__(owner.jenkins, owner.url) 20 | 21 | def get(self, name): 22 | for item in self.api_json(tree='views[name,url]')['views']: 23 | if name == item['name']: 24 | return self._new_item(__name__, item) 25 | return None 26 | 27 | def create(self, name, xml): 28 | self.handle_req('POST', 'createView', params={'name': name}, 29 | headers=self.headers, content=xml) 30 | 31 | def __iter__(self): 32 | for item in self.api_json(tree='views[name,url]')['views']: 33 | yield self._new_item(__name__, item) 34 | 35 | 36 | class View(Item, ConfigurationMixIn, DescriptionMixIn, DeletionMixIn): 37 | 38 | def get(self, name): 39 | for item in self.api_json(tree='jobs[name,url]')['jobs']: 40 | if name == item['name']: 41 | return self._new_item('api4jenkins.job', item) 42 | return None 43 | 44 | def __iter__(self): 45 | for item in self.api_json(tree='jobs[name,url]')['jobs']: 46 | yield self._new_item('api4jenkins.job', item) 47 | 48 | def include(self, name): 49 | self.handle_req('POST', 'addJobToView', params={'name': name}) 50 | 51 | def exclude(self, name): 52 | self.handle_req('POST', 'removeJobFromView', params={'name': name}) 53 | 54 | 55 | class AllView(View): 56 | def __init__(self, jenkins, url): 57 | # name of all view for jenkins is 'all', but for folder is 'All' 58 | name = 'view/all' if jenkins.url == url else 'view/All' 59 | super().__init__(jenkins, url + name) 60 | 61 | 62 | class MyView(View): 63 | pass 64 | 65 | 66 | class ListView(View): 67 | pass 68 | 69 | 70 | class Dashboard(View): 71 | pass 72 | 73 | 74 | class NestedView(View): 75 | 76 | @property 77 | def views(self): 78 | return Views(self) 79 | 80 | 81 | class SectionedView(View): 82 | pass 83 | 84 | 85 | class AsyncViews(AsyncItem): 86 | ''' 87 | classdocs 88 | ''' 89 | 90 | def __init__(self, owner): 91 | ''' 92 | Constructor 93 | ''' 94 | self.owner = owner 95 | super().__init__(owner.jenkins, owner.url) 96 | 97 | async def get(self, name): 98 | data = await self.api_json(tree='views[name,url]') 99 | for item in data['views']: 100 | if name == item['name']: 101 | return self._new_item(__name__, item) 102 | return None 103 | 104 | async def create(self, name, xml): 105 | await self.handle_req('POST', 'createView', params={'name': name}, 106 | headers=self.headers, content=xml) 107 | 108 | async def __aiter__(self): 109 | data = await self.api_json(tree='views[name,url]') 110 | for item in data['views']: 111 | yield self._new_item(__name__, item) 112 | 113 | 114 | class AsyncView(AsyncItem, AsyncConfigurationMixIn, AsyncDescriptionMixIn, AsyncDeletionMixIn): 115 | 116 | async def get(self, name): 117 | data = await self.api_json(tree='jobs[name,url]') 118 | for item in data['jobs']: 119 | if name == item['name']: 120 | return self._new_item('api4jenkins.job', item) 121 | return None 122 | 123 | async def __aiter__(self): 124 | data = await self.api_json(tree='jobs[name,url]') 125 | for item in data['jobs']: 126 | yield self._new_item('api4jenkins.job', item) 127 | 128 | async def include(self, name): 129 | await self.handle_req('POST', 'addJobToView', params={'name': name}) 130 | 131 | async def exclude(self, name): 132 | await self.handle_req('POST', 'removeJobFromView', params={'name': name}) 133 | 134 | 135 | class AsyncAllView(AsyncView): 136 | def __init__(self, jenkins, url): 137 | # name of all view for jenkins is 'all', but for folder is 'All' 138 | name = 'view/all' if jenkins.url == url else 'view/All' 139 | super().__init__(jenkins, url + name) 140 | 141 | 142 | class AsyncMyView(AsyncView): 143 | pass 144 | 145 | 146 | class AsyncListView(AsyncView): 147 | pass 148 | 149 | 150 | class AsyncDashboard(AsyncView): 151 | pass 152 | 153 | 154 | class AsyncNestedView(AsyncView): 155 | 156 | @property 157 | def views(self): 158 | return AsyncViews(self) 159 | 160 | 161 | class AsyncSectionedView(AsyncView): 162 | pass 163 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | httpx 2 | sphinx -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | 14 | import sys 15 | from pathlib import Path 16 | 17 | repo_root = Path(__file__).parent.parent.parent 18 | sys.path.insert(0, str(repo_root)) 19 | # -- Project information ----------------------------------------------------- 20 | 21 | about = {} 22 | with open(repo_root.joinpath('api4jenkins', '__version__.py')) as f: 23 | exec(f.read(), about) 24 | 25 | project = about['__title__'] 26 | copyright = about['__copyright__'] 27 | author = about['__author__'] 28 | 29 | # The full version, including alpha/beta/rc tags 30 | release = about['__version__'] 31 | 32 | version = about['__version__'] 33 | # -- General configuration --------------------------------------------------- 34 | 35 | # Add any Sphinx extension module names here, as strings. They can be 36 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 37 | # ones. 38 | extensions = ["sphinx.ext.autodoc", 39 | "sphinx.ext.intersphinx", 40 | "sphinx.ext.todo", 41 | "sphinx.ext.viewcode",] 42 | 43 | # Add any paths that contain templates here, relative to this directory. 44 | templates_path = ['_templates'] 45 | 46 | # List of patterns, relative to source directory, that match files and 47 | # directories to ignore when looking for source files. 48 | # This pattern also affects html_static_path and html_extra_path. 49 | exclude_patterns = [] 50 | 51 | 52 | # -- Options for HTML output ------------------------------------------------- 53 | 54 | # The theme to use for HTML and HTML Help pages. See the documentation for 55 | # a list of builtin themes. 56 | # 57 | html_theme = 'alabaster' 58 | 59 | # Add any paths that contain custom static files (such as style sheets) here, 60 | # relative to this directory. They are copied after the builtin static files, 61 | # so a file named "default.css" will overwrite the builtin "default.css". 62 | # html_static_path = ['_static'] 63 | 64 | master_doc = 'index' 65 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. api4jenkins documentation master file, created by 2 | sphinx-quickstart on Mon Dec 16 20:16:12 2019. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Jenkins Python Client 7 | ================================================= 8 | .. image:: https://img.shields.io/github/license/joelee2012/api4jenkins 9 | :target: https://pypi.org/project/api4jenkins/ 10 | 11 | .. image:: https://img.shields.io/pypi/wheel/api4jenkins 12 | :target: https://pypi.org/project/api4jenkins/ 13 | 14 | .. image:: https://img.shields.io/pypi/v/api4jenkins 15 | :target: https://pypi.org/project/api4jenkins/ 16 | 17 | .. image:: https://img.shields.io/pypi/pyversions/api4jenkins 18 | :target: https://pypi.org/project/api4jenkins/ 19 | 20 | 21 | 22 | `Python3 `_ client library for 23 | `Jenkins API `_. 24 | 25 | 26 | Features 27 | -------- 28 | 29 | - Provides ``sync`` and ``async`` APIs 30 | - Object oriented, each Jenkins item has corresponding class, easy to use and extend 31 | - Base on ``api/json``, easy to query/filter attribute of item 32 | - Setup relationship between class just like Jenkins item 33 | - Support api for almost every Jenkins item 34 | - Pythonic 35 | - Test with latest `Jenkins LTS `_ 36 | 37 | 38 | Quick start 39 | ---------------------------------------- 40 | 41 | Here is an example to create and build job, then monitor progressive output 42 | until it's done. 43 | 44 | Sync example:: 45 | 46 | >>> from api4jenkins import Jenkins 47 | >>> client = Jenkins('http://127.0.0.1:8080/', auth=('admin', 'admin')) 48 | >>> client.version 49 | '2.176.2' 50 | >>> xml = """ 51 | ... 52 | ... 53 | ... 54 | ... echo $JENKINS_VERSION 55 | ... 56 | ... 57 | ... """ 58 | >>> client.create_job('path/to/job', xml) 59 | >>> import time 60 | >>> item = client.build_job('path/to/job') 61 | >>> while not item.get_build(): 62 | ... time.sleep(1) 63 | >>> build = item.get_build() 64 | >>> for line in build.progressive_output(): 65 | ... print(line) 66 | ... 67 | Started by user admin 68 | Running as SYSTEM 69 | Building in workspace /var/jenkins_home/workspace/freestylejob 70 | [freestylejob] $ /bin/sh -xe /tmp/jenkins2989549474028065940.sh 71 | + echo $JENKINS_VERSION 72 | 2.176.2 73 | Finished: SUCCESS 74 | >>> build.building 75 | False 76 | >>> build.result 77 | 'SUCCESS' 78 | 79 | 80 | Async example:: 81 | 82 | import asyncio 83 | import time 84 | from api4jenkins import AsyncJenkins 85 | 86 | async main(): 87 | client = AsyncJenkins('http://127.0.0.1:8080/', auth=('admin', 'admin')) 88 | print(await client.version) 89 | xml = """ 90 | 91 | 92 | 93 | echo $JENKINS_VERSION 94 | 95 | 96 | """ 97 | await client.create_job('job', xml) 98 | item = await client.build_job('job') 99 | while not await item.get_build(): 100 | time.sleep(1) 101 | build = await item.get_build() 102 | async for line in build.progressive_output(): 103 | print(line) 104 | 105 | print(await build.building) 106 | print(await build.result) 107 | 108 | asyncio.run(main()) 109 | 110 | 111 | .. toctree:: 112 | :maxdepth: 2 113 | :caption: Contents: 114 | 115 | user/install 116 | user/example 117 | user/api 118 | 119 | 120 | Indices and tables 121 | ================== 122 | 123 | * :ref:`genindex` 124 | * :ref:`modindex` 125 | * :ref:`search` 126 | -------------------------------------------------------------------------------- /docs/source/user/api.rst: -------------------------------------------------------------------------------- 1 | :title: API reference 2 | 3 | API reference 4 | ============= 5 | 6 | .. automodule:: api4jenkins 7 | :members: 8 | :undoc-members: 9 | :inherited-members: 10 | 11 | .. automodule:: api4jenkins.job 12 | :members: 13 | :undoc-members: 14 | :inherited-members: 15 | 16 | .. automodule:: api4jenkins.build 17 | :members: 18 | :undoc-members: 19 | :inherited-members: 20 | 21 | .. automodule:: api4jenkins.input 22 | :members: 23 | :undoc-members: 24 | :inherited-members: 25 | 26 | .. automodule:: api4jenkins.queue 27 | :members: 28 | :undoc-members: 29 | :inherited-members: 30 | 31 | .. automodule:: api4jenkins.credential 32 | :members: 33 | :undoc-members: 34 | :inherited-members: 35 | 36 | .. automodule:: api4jenkins.node 37 | :members: 38 | :undoc-members: 39 | :inherited-members: 40 | 41 | .. automodule:: api4jenkins.plugin 42 | :members: 43 | :undoc-members: 44 | :inherited-members: 45 | 46 | .. automodule:: api4jenkins.view 47 | :members: 48 | :undoc-members: 49 | :inherited-members: 50 | 51 | .. automodule:: api4jenkins.system 52 | :members: 53 | :undoc-members: 54 | :inherited-members: 55 | 56 | .. automodule:: api4jenkins.user 57 | :members: 58 | :undoc-members: 59 | :inherited-members: 60 | 61 | .. automodule:: api4jenkins.report 62 | :members: 63 | :undoc-members: 64 | :inherited-members: 65 | 66 | .. automodule:: api4jenkins.item 67 | :members: 68 | :undoc-members: 69 | :inherited-members: 70 | 71 | .. automodule:: api4jenkins.mix 72 | :members: 73 | :undoc-members: 74 | :inherited-members: 75 | 76 | .. automodule:: api4jenkins.http 77 | :members: 78 | :undoc-members: 79 | :inherited-members: 80 | 81 | .. automodule:: api4jenkins.exceptions 82 | :members: 83 | :undoc-members: 84 | :inherited-members: -------------------------------------------------------------------------------- /docs/source/user/install.rst: -------------------------------------------------------------------------------- 1 | .. _install: 2 | 3 | Installation 4 | ============= 5 | 6 | From pypi 7 | ---------- 8 | 9 | The easiest (and best) way to install is through pip:: 10 | 11 | $ python -m pip install api4jenkins 12 | 13 | From source 14 | ------------------- 15 | 16 | Optional you can clone the public repository to install:: 17 | 18 | $ git clone https://github.com/joelee2012/api4jenkins 19 | $ cd api4jenkins 20 | $ python -m pip install . 21 | 22 | Prerequisites 23 | ------------------ 24 | Install following plugins for Jenkins to enable full functionality for api4jenkins: 25 | - `folder `_ 26 | - `pipeline `_ 27 | - `credentials `_ 28 | - `next-build-number `_ 29 | 30 | 31 | .. include:: ../../../HISTORY.md -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import os 4 | 5 | from setuptools import setup 6 | 7 | here = os.path.abspath(os.path.dirname(__file__)) 8 | about = {} 9 | with open(os.path.join(here, 'api4jenkins', '__version__.py')) as f: 10 | exec(f.read(), about) 11 | 12 | with open('README.md') as f: 13 | readme = f.read() 14 | 15 | requires = [ 16 | 'httpx >= 0.24.1' 17 | ] 18 | 19 | setup( 20 | name=about['__title__'], 21 | version=about['__version__'], 22 | description=about['__description__'], 23 | long_description=readme, 24 | long_description_content_type='text/markdown', 25 | url=about['__url__'], 26 | author=about['__author__'], 27 | author_email=about['__author_email__'], 28 | packages=['api4jenkins'], 29 | package_data={'': ['LICENSE']}, 30 | python_requires='>=3.8', 31 | install_requires=requires, 32 | license=about['__license__'], 33 | keywords=["RESTAPI", "Jenkins"], 34 | classifiers=[ 35 | 'Development Status :: 5 - Production/Stable', 36 | 'Intended Audience :: Developers', 37 | 'Intended Audience :: System Administrators', 38 | 'License :: OSI Approved :: Apache Software License', 39 | 'Operating System :: OS Independent', 40 | 'Programming Language :: Python', 41 | 'Programming Language :: Python :: 3', 42 | 'Programming Language :: Python :: 3.8', 43 | 'Programming Language :: Python :: 3.9', 44 | 'Programming Language :: Python :: 3.10', 45 | 'Programming Language :: Python :: 3.11', 46 | 'Programming Language :: Python :: 3.12', 47 | 'Topic :: Utilities', 48 | ], 49 | project_urls={ 50 | 'Documentation': about['__documentation__'], 51 | 'Source': about['__url__'], 52 | }, 53 | ) 54 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joelee2012/api4jenkins/8a19088c37860583b9f733c9f6ba9a8b86e75e5f/tests/__init__.py -------------------------------------------------------------------------------- /tests/integration/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joelee2012/api4jenkins/8a19088c37860583b9f733c9f6ba9a8b86e75e5f/tests/integration/__init__.py -------------------------------------------------------------------------------- /tests/integration/conftest.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import contextlib 3 | import os 4 | import sys 5 | import time 6 | from pathlib import Path 7 | 8 | import pytest 9 | 10 | from api4jenkins import (EMPTY_FOLDER_XML, AsyncFolder, AsyncJenkins, Folder, 11 | Jenkins) 12 | from api4jenkins.job import AsyncWorkflowJob, WorkflowJob 13 | 14 | TEST_DATA_DIR = Path(__file__).with_name('tests_data') 15 | 16 | 17 | def load_xml(name): 18 | with open(TEST_DATA_DIR.joinpath(name)) as f: 19 | return f.read() 20 | 21 | 22 | @pytest.fixture(scope='session') 23 | def jenkins(): 24 | yield Jenkins(os.environ['JENKINS_URL'], auth=( 25 | os.environ['JENKINS_USER'], os.environ['JENKINS_PASSWORD'])) 26 | 27 | 28 | @pytest.fixture(scope='session') 29 | def async_jenkins(): 30 | yield AsyncJenkins(os.environ['JENKINS_URL'], auth=( 31 | os.environ['JENKINS_USER'], os.environ['JENKINS_PASSWORD'])) 32 | 33 | 34 | @pytest.fixture(scope='session') 35 | def folder_xml(): 36 | return EMPTY_FOLDER_XML 37 | 38 | 39 | # @pytest.fixture(scope='session') 40 | # def job_xml(): 41 | # return load_xml('job.xml') 42 | 43 | 44 | # @pytest.fixture(scope='session') 45 | # def job_with_args_xml(): 46 | # return load_xml('job_params.xml') 47 | 48 | 49 | @pytest.fixture(scope='session') 50 | def credential_xml(): 51 | return load_xml('credential.xml') 52 | 53 | 54 | @pytest.fixture(scope='session') 55 | def view_xml(): 56 | return load_xml('view.xml') 57 | 58 | 59 | @pytest.fixture(scope='session') 60 | def folder(jenkins: Jenkins): 61 | return Folder(jenkins, jenkins._name2url('folder')) 62 | 63 | 64 | @pytest.fixture(scope='session') 65 | def async_folder(async_jenkins: AsyncJenkins): 66 | return AsyncFolder(async_jenkins, async_jenkins._name2url('async_folder')) 67 | 68 | 69 | @pytest.fixture(scope='session') 70 | def job(jenkins: Jenkins): 71 | return WorkflowJob(jenkins, jenkins._name2url('folder/job')) 72 | 73 | 74 | @pytest.fixture(scope='session') 75 | def async_job(async_jenkins: AsyncJenkins): 76 | return AsyncWorkflowJob(async_jenkins, async_jenkins._name2url('async_folder/job')) 77 | 78 | 79 | @pytest.fixture(scope='session') 80 | def args_job(jenkins: Jenkins): 81 | return WorkflowJob(jenkins, jenkins._name2url('folder/args_job')) 82 | 83 | 84 | @pytest.fixture(scope='session') 85 | def async_args_job(async_jenkins: AsyncJenkins): 86 | return AsyncWorkflowJob(async_jenkins, async_jenkins._name2url('async_folder/args_job')) 87 | 88 | 89 | @pytest.fixture(scope='session', autouse=True) 90 | def setup(jenkins, credential_xml, view_xml): 91 | try: 92 | for name in ['folder/folder', 'folder/for_rename', 'folder/for_move', 'async_folder/folder', 'async_folder/for_rename', 'async_folder/for_move']: 93 | jenkins.create_job(name, EMPTY_FOLDER_XML, True) 94 | 95 | for name in ['folder/job', 'async_folder/job']: 96 | jenkins.create_job(name, load_xml('job.xml')) 97 | 98 | for name in ['folder/args_job', 'async_folder/args_job']: 99 | jenkins.create_job(name, load_xml('args_job.xml')) 100 | 101 | jenkins.credentials.global_domain.create(credential_xml) 102 | jenkins.credentials.create(load_xml('domain.xml')) 103 | jenkins.views.create('global-view', view_xml) 104 | jenkins['folder'].credentials.global_domain.create(credential_xml) 105 | jenkins['async_folder'].credentials.global_domain.create( 106 | credential_xml) 107 | jenkins['folder'].views.create('folder-view', view_xml) 108 | jenkins['async_folder'].views.create('folder-view', view_xml) 109 | 110 | yield 111 | finally: 112 | jenkins.delete_job('folder') 113 | jenkins.delete_job('async_folder') 114 | jenkins.credentials.global_domain.get('user-id').delete() 115 | jenkins.credentials.get('testing').delete() 116 | jenkins.views.get('global-view').delete() 117 | 118 | 119 | @pytest.fixture(scope='session') 120 | def retrive_build_and_output(): 121 | def _retrive(item): 122 | for _ in range(10): 123 | if item.get_build(): 124 | break 125 | time.sleep(1) 126 | else: 127 | raise TimeoutError('unable to get build in 10 seconds!!') 128 | build = item.get_build() 129 | output = [] 130 | for line in build.progressive_output(): 131 | output.append(str(line)) 132 | return build, output 133 | return _retrive 134 | 135 | 136 | @pytest.fixture(scope='session') 137 | async def async_retrive_build_and_output(): 138 | async def _retrive(item): 139 | for _ in range(10): 140 | if await item.get_build(): 141 | break 142 | await asyncio.sleep(1) 143 | else: 144 | raise TimeoutError('unable to get build in 10 seconds!!') 145 | build = await item.get_build() 146 | output = [] 147 | async for line in build.progressive_output(): 148 | output.append(str(line)) 149 | return build, output 150 | return _retrive 151 | 152 | # workaround for https://github.com/pytest-dev/pytest-asyncio/issues/371 153 | 154 | 155 | @pytest.fixture(scope="session") 156 | def event_loop(): 157 | policy = asyncio.get_event_loop_policy() 158 | loop = policy.new_event_loop() 159 | yield loop 160 | if loop.is_running(): 161 | time.sleep(2) 162 | loop.close() 163 | -------------------------------------------------------------------------------- /tests/integration/test_02_job.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | import pytest 4 | 5 | 6 | class TestFolder: 7 | 8 | def test_parent(self, jenkins, folder, job): 9 | assert folder == job.parent 10 | assert jenkins == folder.parent 11 | 12 | def test_iter_jobs(self, folder): 13 | assert len(list(folder.iter(2))) == 5 14 | assert len(list(folder(2))) == 5 15 | assert len(list(folder)) == 4 16 | assert folder['job'] 17 | 18 | 19 | class TestProject: 20 | def test_name(self, job): 21 | assert job.name == 'job' 22 | assert job.full_name == 'folder/job' 23 | assert job.full_display_name == 'folder » job' 24 | 25 | def test_get_parameters(self, args_job): 26 | assert args_job.get_parameters()[0]['name'] == 'ARG1' 27 | 28 | def test_get_build(self, job): 29 | assert job[0] is None 30 | assert job[1] 31 | 32 | def test_get_special_build(self, job): 33 | assert job.get_first_build() 34 | assert job.get_last_failed_build() is None 35 | 36 | def test_iter_build(self, job): 37 | assert len(list(job)) == 2 38 | assert len(list(job.iter())) == 2 39 | 40 | def test_iter_all_builds(self, job): 41 | assert len(list(job.iter_all_builds())) == 2 42 | 43 | def test_building(self, job): 44 | assert job.building == False 45 | 46 | def test_set_next_build_number(self, job): 47 | job.set_next_build_number(10) 48 | assert job.next_build_number == 10 49 | 50 | def test_filter_builds_by_result(self, job): 51 | assert len(list(job.filter_builds_by_result(result='SUCCESS'))) == 2 52 | assert not list(job.filter_builds_by_result(result='ABORTED')) 53 | with pytest.raises(ValueError): 54 | assert list(job.filter_builds_by_result( 55 | result='not a status')) == 'x' 56 | 57 | 58 | class TestAsyncFolder: 59 | async def test_parent(self, async_jenkins, async_folder, async_job): 60 | assert async_folder == await async_job.parent 61 | assert async_jenkins == await async_folder.parent 62 | 63 | async def test_iter_jobs(self, async_folder): 64 | assert len([j async for j in async_folder(2)]) == 5 65 | assert len([j async for j in async_folder]) == 4 66 | assert await async_folder['job'] 67 | 68 | 69 | class TestAsyncProject: 70 | async def test_name(self, async_job): 71 | assert async_job.name == 'job' 72 | assert async_job.full_name == 'async_folder/job' 73 | assert async_job.full_display_name == 'async_folder » job' 74 | 75 | async def test_get_parameters(self, async_args_job): 76 | assert (await async_args_job.get_parameters())[0]['name'] == 'ARG1' 77 | 78 | async def test_get_build(self, async_job): 79 | assert await async_job.get(0) is None 80 | assert await async_job[1] 81 | 82 | async def test_get_special_build(self, async_job): 83 | assert await async_job.get_first_build() 84 | assert await async_job.get_last_failed_build() is None 85 | 86 | async def test_iter_build(self, async_job): 87 | assert len([b async for b in async_job]) == 2 88 | assert len([b async for b in async_job.aiter()]) == 2 89 | 90 | async def test_iter_all_builds(self, async_job): 91 | assert len([b async for b in async_job.iter_all_builds()]) == 2 92 | 93 | async def test_building(self, async_job): 94 | assert await async_job.building == False 95 | 96 | async def test_set_next_build_number(self, async_job): 97 | await async_job.set_next_build_number(10) 98 | assert await async_job.next_build_number == 10 99 | 100 | async def test_filter_builds_by_result(self, async_job): 101 | assert len([b async for b in async_job.filter_builds_by_result(result='SUCCESS')]) == 2 102 | assert not [b async for b in async_job.filter_builds_by_result(result='ABORTED')] 103 | with pytest.raises(ValueError): 104 | assert [b async for b in async_job.filter_builds_by_result(result='not a status')] == 'x' 105 | -------------------------------------------------------------------------------- /tests/integration/test_03_credential.py: -------------------------------------------------------------------------------- 1 | class TestCredentials: 2 | def test_credentials_iter(self, jenkins): 3 | assert len(list(jenkins.credentials)) == 2 4 | 5 | def test_credential_get(self, jenkins): 6 | assert jenkins.credentials.global_domain is not None 7 | assert jenkins.credentials.get('not exists') is None 8 | 9 | def test_domain_get(self, jenkins): 10 | c = jenkins.credentials.global_domain['user-id'] 11 | assert c.id == 'user-id' 12 | assert jenkins.credentials.global_domain['not exists'] is None 13 | 14 | def test_domain_iter(self, folder): 15 | assert len(list(folder.credentials.global_domain)) == 1 16 | 17 | 18 | class TestAsyncCredential: 19 | async def test_credentials_iter(self, async_jenkins): 20 | assert len([c async for c in async_jenkins.credentials]) == 2 21 | 22 | async def test_credential_get(self, async_jenkins): 23 | domain = await async_jenkins.credentials.global_domain 24 | assert domain is not None 25 | domain = await async_jenkins.credentials.get('not exists') 26 | assert domain is None 27 | 28 | async def test_domain_get(self, async_jenkins): 29 | domain = await async_jenkins.credentials.global_domain 30 | c = await domain['user-id'] 31 | assert await c.id == 'user-id' 32 | c = await domain['not exists'] 33 | assert c is None 34 | 35 | async def test_domain_iter(self, async_folder): 36 | assert len([c async for c in await async_folder.credentials.global_domain]) == 1 37 | -------------------------------------------------------------------------------- /tests/integration/test_04_view.py: -------------------------------------------------------------------------------- 1 | class TestGlobalView: 2 | def test_get(self, jenkins): 3 | v = jenkins.views.get('global-view') 4 | assert v.name == 'global-view' 5 | assert jenkins.views.get('not exist') is None 6 | 7 | def test_iter(self, jenkins): 8 | assert len(list(jenkins.views)) == 2 9 | 10 | def test_in_exclude(self, jenkins): 11 | v = jenkins.views.get('global-view') 12 | assert v['folder'] is None 13 | v.include('folder') 14 | assert v['folder'] 15 | v.exclude('folder') 16 | assert v['folder'] is None 17 | 18 | 19 | class TestFolderView: 20 | def test_get(self, folder): 21 | v = folder.views.get('folder-view') 22 | assert v.name == 'folder-view' 23 | assert folder.views.get('not exist') is None 24 | -------------------------------------------------------------------------------- /tests/integration/test_05_build.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | 4 | class TestBuild: 5 | def test_get_job(self, job): 6 | assert job == job[1].project 7 | 8 | def test_console_text(self, job): 9 | assert 'true' in ''.join(job[1].console_text()) 10 | 11 | def test_get_next_build(self, job): 12 | assert job[1].get_next_build().number == 2 13 | assert job[2].get_next_build() is None 14 | 15 | def test_get_previous_build(self, job): 16 | assert job[2].get_previous_build().number == 1 17 | assert job[1].get_previous_build() is None 18 | 19 | def test_coverage_report(self, job): 20 | pass 21 | -------------------------------------------------------------------------------- /tests/integration/test_06_system.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import yaml 3 | 4 | conf = '''unclassified: 5 | timestamper: 6 | allPipelines: true 7 | ''' 8 | 9 | 10 | class TestSystem: 11 | def test_export_jcasc(self, jenkins): 12 | data = yaml.safe_load(jenkins.system.export_jcasc()) 13 | assert data['unclassified']['timestamper']['allPipelines'] == False 14 | 15 | @pytest.mark.xfail 16 | def test_apply_jcasc(self, jenkins): 17 | jenkins.system.apply_jcasc(conf) 18 | # jenkins.system.reload_jcasc() 19 | data = yaml.safe_load(jenkins.system.export_jcasc()) 20 | assert data['unclassified']['timestamper']['allPipelines'] 21 | 22 | 23 | class TestAsyncSystem: 24 | async def test_export_jcasc(self, async_jenkins): 25 | data = yaml.safe_load(await async_jenkins.system.export_jcasc()) 26 | assert data['unclassified']['timestamper']['allPipelines'] == False 27 | 28 | @pytest.mark.xfail 29 | async def test_apply_jcasc(self, async_jenkins): 30 | await async_jenkins.system.apply_jcasc(conf) 31 | await async_jenkins.system.reload_jcasc() 32 | data = yaml.safe_load(await async_jenkins.system.export_jcasc()) 33 | assert data['unclassified']['timestamper']['allPipelines'] 34 | -------------------------------------------------------------------------------- /tests/integration/test_07_node.py: -------------------------------------------------------------------------------- 1 | 2 | import pytest 3 | 4 | 5 | @pytest.fixture 6 | def node(jenkins): 7 | return jenkins.nodes.get('Built-In Node') 8 | 9 | 10 | @pytest.fixture 11 | async def anode(async_jenkins): 12 | return await async_jenkins.nodes.get('Built-In Node') 13 | 14 | 15 | class TestNode: 16 | def test_get(self, node): 17 | assert node.display_name == 'Built-In Node' 18 | 19 | def test_iter(self, jenkins): 20 | assert len(list(jenkins.nodes)) == 1 21 | 22 | def test_filter_node_by_label(self, jenkins): 23 | assert len(list(jenkins.nodes.filter_node_by_label('built-in'))) == 1 24 | 25 | def test_filter_node_by_status(self, jenkins): 26 | assert len(list(jenkins.nodes.filter_node_by_status(online=True))) == 1 27 | 28 | def test_enable_disable(self, node): 29 | assert node.offline == False 30 | node.disable() 31 | assert node.offline 32 | node.enable() 33 | 34 | def test_iter_build_on_node(self, node): 35 | assert not list(node) 36 | 37 | 38 | class TestAsyncNode: 39 | async def test_get(self, anode): 40 | assert await anode.display_name == 'Built-In Node' 41 | 42 | async def test_iter(self, async_jenkins): 43 | assert len([n async for n in async_jenkins.nodes]) == 1 44 | 45 | async def test_filter_node_by_label(self, async_jenkins): 46 | assert len([n async for n in async_jenkins.nodes.filter_node_by_label('built-in')]) == 1 47 | 48 | async def test_filter_node_by_status(self, async_jenkins): 49 | assert len([n async for n in async_jenkins.nodes.filter_node_by_status(online=True)]) == 1 50 | 51 | async def test_enable_disable(self, anode): 52 | assert await anode.offline == False 53 | await anode.disable() 54 | assert await anode.offline 55 | await anode.enable() 56 | 57 | async def test_iter_build_on_node(self, anode): 58 | assert not [b async for b in anode] 59 | -------------------------------------------------------------------------------- /tests/integration/test_08_plugin.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.fixture 5 | def plugin(jenkins): 6 | jenkins.plugins.install('nodejs', block=True) 7 | yield jenkins.plugins.get('nodejs') 8 | jenkins.plugins.uninstall('nodejs') 9 | 10 | 11 | @pytest.fixture 12 | async def async_plugin(async_jenkins): 13 | await async_jenkins.plugins.install('nodejs', block=True) 14 | yield await async_jenkins.plugins.get('nodejs') 15 | await async_jenkins.plugins.uninstall('nodejs') 16 | 17 | 18 | class TestPlugin: 19 | def test_get(self, jenkins): 20 | assert jenkins.plugins.get('git') 21 | assert jenkins.plugins.get('notxist') is None 22 | 23 | def test_install(self, plugin): 24 | assert plugin.short_name == 'nodejs' 25 | 26 | 27 | class TestAsyncPlugin: 28 | async def test_get(self, async_jenkins): 29 | assert await async_jenkins.plugins.get('git') 30 | assert await async_jenkins.plugins.get('notxist') is None 31 | 32 | async def test_install(self, async_plugin): 33 | assert await async_plugin.short_name == 'nodejs' 34 | -------------------------------------------------------------------------------- /tests/integration/tests_data/args_job.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | false 5 | 6 | 7 | 8 | 9 | ARG1 10 | false 11 | 12 | 13 | 14 | 15 | 16 | 17 | true 18 | 19 | 20 | false 21 | -------------------------------------------------------------------------------- /tests/integration/tests_data/credential.xml: -------------------------------------------------------------------------------- 1 | 2 | GLOBAL 3 | user-id 4 | user-name 5 | user-password 6 | user id for testing 7 | -------------------------------------------------------------------------------- /tests/integration/tests_data/domain.xml: -------------------------------------------------------------------------------- 1 | 2 | testing 3 | Credentials for use against the *.test.example.com hosts 4 | 5 | 6 | *.test.example.com 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /tests/integration/tests_data/folder.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /tests/integration/tests_data/job.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | true 6 | 7 | false 8 | -------------------------------------------------------------------------------- /tests/integration/tests_data/view.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | new text 4 | false 5 | false 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | false 22 | -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joelee2012/api4jenkins/8a19088c37860583b9f733c9f6ba9a8b86e75e5f/tests/unit/__init__.py -------------------------------------------------------------------------------- /tests/unit/conftest.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pathlib import Path 3 | 4 | import pytest 5 | 6 | from api4jenkins import AsyncJenkins, Jenkins 7 | from api4jenkins.build import (AsyncFreeStyleBuild, AsyncWorkflowRun, 8 | FreeStyleBuild, WorkflowRun) 9 | from api4jenkins.credential import (AsyncCredential, AsyncCredentials, 10 | AsyncDomain, Credential, Credentials, 11 | Domain) 12 | from api4jenkins.item import AsyncItem, Item 13 | from api4jenkins.job import (AsyncFolder, AsyncOrganizationFolder, 14 | AsyncWorkflowJob, AsyncWorkflowMultiBranchProject, 15 | Folder, OrganizationFolder, WorkflowJob, 16 | WorkflowMultiBranchProject) 17 | from api4jenkins.node import AsyncNode, AsyncNodes, Node, Nodes 18 | from api4jenkins.plugin import AsyncPluginsManager, PluginsManager 19 | from api4jenkins.queue import AsyncQueue, AsyncQueueItem, Queue, QueueItem 20 | from api4jenkins.report import (AsyncCoverageReport, AsyncCoverageResult, 21 | AsyncTestReport, CoverageReport, 22 | CoverageResult, TestReport) 23 | from api4jenkins.view import AllView, AsyncAllView 24 | 25 | DATA = Path(__file__).with_name('tests_data') 26 | 27 | 28 | def _api_json(self, tree='', depth=0): 29 | if self.url == self.jenkins.url: 30 | return load_json('jenkins/jenkins.json') 31 | elif isinstance(self, (Folder, AsyncFolder)): 32 | return load_json('job/folder.json') 33 | elif isinstance(self, (WorkflowJob, AsyncWorkflowJob)): 34 | return load_json('job/pipeline.json') 35 | elif isinstance(self, (WorkflowRun, AsyncWorkflowRun)): 36 | return load_json('run/workflowrun.json') 37 | elif isinstance(self, (FreeStyleBuild, AsyncFreeStyleBuild)): 38 | return load_json('run/freestylebuild.json') 39 | elif isinstance(self, (Credentials, AsyncCredentials)): 40 | return load_json('credential/domains.json') 41 | elif isinstance(self, (Domain, AsyncDomain)): 42 | return load_json('credential/credentials.json') 43 | elif isinstance(self, (Credential, AsyncCredential)): 44 | return load_json('credential/user_psw.json') 45 | elif isinstance(self, (PluginsManager, AsyncPluginsManager)): 46 | return load_json('plugin/plugin.json') 47 | elif isinstance(self, (Queue, AsyncQueue)): 48 | return load_json('queue/queue.json') 49 | elif isinstance(self, (Nodes, AsyncNodes)): 50 | return load_json('node/nodes.json') 51 | elif isinstance(self, (Node, AsyncNode)): 52 | return load_json('node/node.json') 53 | elif isinstance(self, (AllView, AsyncAllView)): 54 | return load_json('view/allview.json') 55 | elif isinstance(self, (TestReport, AsyncTestReport)): 56 | return load_json('report/test_report.json') 57 | elif isinstance(self, (CoverageReport, AsyncCoverageReport)): 58 | return load_json('report/coverage_report.json') 59 | elif isinstance(self, (CoverageResult, AsyncCoverageResult)): 60 | return load_json('report/coverage_result.json') 61 | elif isinstance(self, (QueueItem, AsyncQueueItem)): 62 | return load_json('queue/waitingitem.json') 63 | raise TypeError(f'unknown item: {type(self)}') 64 | 65 | 66 | async def _async_api_json(self, tree='', depth=0): 67 | return _api_json(self, tree, depth) 68 | 69 | 70 | @pytest.fixture(autouse=True) 71 | def mock_api_json(monkeypatch): 72 | monkeypatch.setattr(Item, 'api_json', _api_json) 73 | monkeypatch.setattr(AsyncItem, 'api_json', _async_api_json) 74 | 75 | 76 | def load_json(file_): 77 | with open(DATA.joinpath(file_), 'rb') as f: 78 | return json.load(f) 79 | 80 | 81 | @pytest.fixture(scope='module') 82 | def url(): 83 | return 'http://0.0.0.0:8080/' 84 | 85 | 86 | @pytest.fixture(scope='module') 87 | def jenkins(url): 88 | j = Jenkins(url, auth=('admin', 'password')) 89 | j._crumb = load_json('jenkins/crumb.json') 90 | return j 91 | 92 | 93 | @pytest.fixture(scope='module') 94 | def async_jenkins(url): 95 | j = AsyncJenkins(url, auth=('admin', 'password')) 96 | j._crumb = load_json('jenkins/crumb.json') 97 | return j 98 | 99 | 100 | @pytest.fixture() 101 | def folder(jenkins): 102 | return Folder(jenkins, f'{jenkins.url}job/folder/') 103 | 104 | 105 | @pytest.fixture() 106 | def async_folder(async_jenkins): 107 | return AsyncFolder(async_jenkins, f'{async_jenkins.url}job/folder/') 108 | 109 | 110 | @pytest.fixture(scope='module') 111 | def job(jenkins): 112 | return WorkflowJob(jenkins, f'{jenkins.url}job/folder/job/pipeline/') 113 | 114 | 115 | @pytest.fixture(scope='module') 116 | def async_job(async_jenkins): 117 | return AsyncWorkflowJob(async_jenkins, f'{async_jenkins.url}job/folder/job/pipeline/') 118 | 119 | 120 | @pytest.fixture(scope='module') 121 | def build(jenkins): 122 | return WorkflowRun(jenkins, f'{jenkins.url}job/folder/job/pipeline/2/') 123 | 124 | 125 | @pytest.fixture(scope='module') 126 | def async_build(async_jenkins): 127 | return AsyncWorkflowRun(async_jenkins, f'{async_jenkins.url}job/folder/job/pipeline/2/') 128 | 129 | 130 | @pytest.fixture(scope='module') 131 | def multi_job(jenkins): 132 | return WorkflowMultiBranchProject(jenkins, f'{jenkins.url}job/folder/multi-pipe/') 133 | 134 | 135 | @pytest.fixture(scope='module') 136 | def org_job(jenkins): 137 | return OrganizationFolder(jenkins, f'{jenkins.url}job/folder/org-pipe/') 138 | 139 | 140 | @pytest.fixture(scope='module') 141 | def async_multi_job(async_jenkins): 142 | return AsyncWorkflowMultiBranchProject(async_jenkins, f'{async_jenkins.url}job/folder/multi-pipe/') 143 | 144 | 145 | @pytest.fixture(scope='module') 146 | def async_org_job(async_jenkins): 147 | return AsyncOrganizationFolder(async_jenkins, f'{async_jenkins.url}job/folder/org-pipe/') 148 | 149 | 150 | @pytest.fixture(scope='module') 151 | def credential(jenkins): 152 | return Credential(jenkins, f'{jenkins.url}credentials/store/system/domain/_/test-user/') 153 | 154 | 155 | @pytest.fixture(scope='module') 156 | def async_credential(async_jenkins): 157 | return AsyncCredential(async_jenkins, f'{async_jenkins.url}credentials/store/system/domain/_/test-user/') 158 | 159 | 160 | @pytest.fixture(scope='module') 161 | def view(jenkins): 162 | return AllView(jenkins, jenkins.url) 163 | 164 | 165 | @pytest.fixture(scope='module') 166 | def async_view(async_jenkins): 167 | return AsyncAllView(async_jenkins, async_jenkins.url) 168 | 169 | # @pytest.fixture 170 | # def mock_resp(): 171 | # with respx.mock() as respx_mock: 172 | # yield respx_mock 173 | 174 | 175 | @pytest.fixture 176 | def test_report(jenkins, build): 177 | return TestReport(jenkins, f'{build.url}testReport') 178 | 179 | # @pytest.fixture 180 | # def coverage_report(jenkins, workflow): 181 | # return workflow.get 182 | -------------------------------------------------------------------------------- /tests/unit/test_artifact.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from api4jenkins.artifact import Artifact 3 | 4 | 5 | @pytest.fixture 6 | def artifact(jenkins): 7 | raw = { 8 | "id": "n3", 9 | "name": "hello2.jar", 10 | "path": "target/hello2.jar", 11 | "url": "/job/test/1/artifact/target/hello2.jar", 12 | "size": 6 13 | } 14 | return Artifact(jenkins, raw) 15 | 16 | 17 | class TestArtifact: 18 | 19 | def test_attrs(self, artifact, jenkins): 20 | assert artifact.id == "n3" 21 | assert artifact.name == "hello2.jar" 22 | assert artifact.path == "target/hello2.jar" 23 | assert artifact.url == f'{jenkins.url}job/test/1/artifact/target/hello2.jar' 24 | assert artifact.size == 6 25 | 26 | def test_save(self, artifact, respx_mock, tmp_path): 27 | respx_mock.get(artifact.url).respond(content='abcd') 28 | filename = tmp_path / artifact.name 29 | artifact.save(filename) 30 | assert filename.exists() 31 | -------------------------------------------------------------------------------- /tests/unit/test_build.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import pytest 3 | from respx import MockResponse 4 | 5 | from api4jenkins.build import AsyncWorkflowRun, WorkflowRun 6 | from api4jenkins.input import PendingInputAction 7 | 8 | 9 | class TestBuild: 10 | def test_console_text(self, build, respx_mock): 11 | body = 'a\nb' 12 | respx_mock.get(f'{build.url}consoleText').respond(content=body) 13 | assert list(build.console_text()) == body.split('\n') 14 | 15 | def test_progressive_output(self, build, respx_mock): 16 | body = ['a', 'b'] 17 | headers = {'X-More-Data': 'True', 'X-Text-Size': '1'} 18 | req_url = f'{build.url}logText/progressiveText' 19 | respx_mock.get(req_url).mock( 20 | side_effect=[MockResponse(headers=headers, content=body[0], status_code=200), MockResponse(content=body[1], status_code=200)]) 21 | assert list(build.progressive_output()) == body 22 | 23 | def test_get_next_build(self, build): 24 | assert build.get_next_build() is None 25 | 26 | def test_get_previous_build(self, build): 27 | assert isinstance(build.get_previous_build(), WorkflowRun) 28 | 29 | def test_get_job(self, build, job): 30 | assert job == build.project 31 | 32 | @pytest.mark.parametrize('action', ['stop', 'term', 'kill']) 33 | def test_stop_term_kill(self, build, respx_mock, action): 34 | req_url = f'{build.url}{action}' 35 | respx_mock.post(req_url) 36 | getattr(build, action)() 37 | assert respx_mock.calls[0].request.url == req_url 38 | 39 | def test_get_parameters(self, build): 40 | params = build.get_parameters() 41 | assert params[0].name == 'parameter1' 42 | assert params[0].value == 'value1' 43 | assert params[1].name == 'parameter2' 44 | assert params[1].value == 'value2' 45 | 46 | def test_get_causes(self, build): 47 | causes = build.get_causes() 48 | assert causes[0]['shortDescription'] == 'Started by user admin' 49 | assert causes[1]['shortDescription'] == 'Replayed #1' 50 | 51 | 52 | class TestWorkflowRun: 53 | 54 | @pytest.mark.parametrize('data, obj', [({"_links": {}}, type(None)), 55 | ({"_links": {"pendingInputActions": 't1'}}, 56 | PendingInputAction), 57 | ({"_links": {"pendingInputActions": 't2'}}, PendingInputAction)]) 58 | def test_get_pending_input(self, build, respx_mock, data, obj): 59 | respx_mock.get(f'{build.url}wfapi/describe').respond(json=data) 60 | if data['_links'] and 'pendingInputActions' in data['_links']: 61 | if data['_links']['pendingInputActions'] == "t1": 62 | respx_mock.get(f'{build.url}wfapi/pendingInputActions').respond( 63 | json=[{'abortUrl': '/job/Test%20Workflow/11/input/Ef95dd500ae6ed3b27b89fb852296d12/abort'}]) 64 | elif data['_links']['pendingInputActions'] == "t2": 65 | respx_mock.get(f'{build.url}wfapi/pendingInputActions').respond( 66 | json=[{'abortUrl': '/jenkins/job/Test%20Workflow/11/input/Ef95dd500ae6ed3b27b89fb852296d12/abort'}]) 67 | 68 | assert isinstance(build.get_pending_input(), obj) 69 | 70 | @pytest.mark.parametrize('data, count', [([], 0), 71 | ([{"url": 'abcd'}], 1)], 72 | ids=["empty", "no empty"]) 73 | def test_get_artifacts(self, build, respx_mock, data, count): 74 | respx_mock.get(f'{build.url}wfapi/artifacts').respond(json=data) 75 | assert len(build.artifacts) == count 76 | 77 | def test_save_artifacts(self, build, respx_mock, tmp_path): 78 | respx_mock.get( 79 | f'{build.url}artifact/*zip*/archive.zip').respond(content='abc') 80 | filename = tmp_path / 'my_archive.zip' 81 | build.save_artifacts(filename) 82 | assert filename.exists() 83 | 84 | 85 | class TestAsyncBuild: 86 | async def test_console_text(self, async_build, respx_mock): 87 | body = 'a\nb' 88 | respx_mock.get(f'{async_build.url}consoleText').respond(content=body) 89 | output = [line async for line in async_build.console_text()] 90 | assert output == body.split('\n') 91 | 92 | async def test_progressive_output(self, async_build, respx_mock): 93 | body = ['a', 'b'] 94 | headers = {'X-More-Data': 'True', 'X-Text-Size': '1'} 95 | req_url = f'{async_build.url}logText/progressiveText' 96 | respx_mock.get(req_url).mock( 97 | side_effect=[MockResponse(headers=headers, content=body[0], status_code=200), MockResponse(content=body[1], status_code=200)]) 98 | 99 | assert [line async for line in async_build.progressive_output()] == body 100 | 101 | async def test_get_next_build(self, async_build): 102 | assert await async_build.get_next_build() is None 103 | 104 | async def test_get_previous_build(self, async_build): 105 | assert isinstance(await async_build.get_previous_build(), AsyncWorkflowRun) 106 | 107 | async def test_get_job(self, async_build, async_job): 108 | assert async_job == await async_build.project 109 | 110 | @pytest.mark.parametrize('action', ['stop', 'term', 'kill']) 111 | async def test_stop_term_kill(self, async_build, respx_mock, action): 112 | req_url = f'{async_build.url}{action}' 113 | respx_mock.post(req_url) 114 | await getattr(async_build, action)() 115 | assert respx_mock.calls[0].request.url == req_url 116 | 117 | async def test_get_parameters(self, async_build): 118 | params = await async_build.get_parameters() 119 | assert params[0].name == 'parameter1' 120 | assert params[0].value == 'value1' 121 | assert params[1].name == 'parameter2' 122 | assert params[1].value == 'value2' 123 | 124 | async def test_get_causes(self, async_build): 125 | causes = await async_build.get_causes() 126 | assert causes[0]['shortDescription'] == 'Started by user admin' 127 | assert causes[1]['shortDescription'] == 'Replayed #1' 128 | 129 | 130 | class TestAsyncWorkflowRun: 131 | 132 | @pytest.mark.parametrize('data, obj', [({"_links": {}}, type(None)), 133 | ({"_links": {"pendingInputActions": 't1'}}, 134 | PendingInputAction), 135 | ({"_links": {"pendingInputActions": 't2'}}, PendingInputAction)]) 136 | async def test_get_pending_input(self, async_build, respx_mock, data, obj): 137 | respx_mock.get(f'{async_build.url}wfapi/describe').respond(json=data) 138 | if data['_links'] and 'pendingInputActions' in data['_links']: 139 | if data['_links']['pendingInputActions'] == "t1": 140 | respx_mock.get(f'{async_build.url}wfapi/pendingInputActions').respond( 141 | json=[{'abortUrl': '/job/Test%20Workflow/11/input/Ef95dd500ae6ed3b27b89fb852296d12/abort'}]) 142 | elif data['_links']['pendingInputActions'] == "t2": 143 | respx_mock.get(f'{async_build.url}wfapi/pendingInputActions').respond( 144 | json=[{'abortUrl': '/jenkins/job/Test%20Workflow/11/input/Ef95dd500ae6ed3b27b89fb852296d12/abort'}]) 145 | 146 | assert isinstance(await async_build.get_pending_input(), obj) 147 | 148 | @pytest.mark.parametrize('data, count', [([], 0), 149 | ([{"url": 'abcd'}], 1)], 150 | ids=["empty", "no empty"]) 151 | async def test_get_artifacts(self, async_build, respx_mock, data, count): 152 | respx_mock.get(f'{async_build.url}wfapi/artifacts').respond(json=data) 153 | artifacts = await async_build.artifacts 154 | assert len(artifacts) == count 155 | 156 | async def test_save_artifacts(self, async_build, respx_mock, tmp_path): 157 | respx_mock.get( 158 | f'{async_build.url}artifact/*zip*/archive.zip').respond(content='abc') 159 | filename = tmp_path / 'my_archive.zip' 160 | await async_build.save_artifacts(filename) 161 | assert filename.exists() 162 | -------------------------------------------------------------------------------- /tests/unit/test_credential.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | import pytest 4 | from api4jenkins.credential import AsyncCredential, AsyncDomain, Credential, Domain 5 | 6 | 7 | @pytest.fixture 8 | def global_domain(jenkins): 9 | return jenkins.credentials.global_domain 10 | 11 | 12 | @pytest.fixture 13 | async def async_global_domain(async_jenkins): 14 | return await async_jenkins.credentials.global_domain 15 | 16 | 17 | class TestCredentials: 18 | 19 | def test_get(self, jenkins): 20 | assert isinstance(jenkins.credentials['_'], Domain) 21 | assert jenkins.credentials['x'] is None 22 | 23 | def test_create(self, jenkins, respx_mock): 24 | req_url = f'{jenkins.credentials.url}createDomain' 25 | respx_mock.post(req_url) 26 | jenkins.credentials.create('xml') 27 | assert respx_mock.calls[0].request.url == req_url 28 | 29 | def test_iter(self, jenkins): 30 | assert len(list(jenkins.credentials)) == 3 31 | 32 | 33 | class TestDomain: 34 | 35 | @pytest.mark.parametrize('id_, obj', [('not exist', type(None)), ('test-user', Credential)]) 36 | def test_get(self, global_domain, id_, obj): 37 | assert isinstance(global_domain[id_], obj) 38 | 39 | def test_create(self, global_domain, respx_mock): 40 | req_url = f'{global_domain.url}createCredentials' 41 | respx_mock.post(req_url) 42 | global_domain.create('xml') 43 | assert respx_mock.calls[0].request.url == req_url 44 | 45 | def test_iter(self, global_domain): 46 | creds = list(global_domain) 47 | assert len(creds) == 2 48 | assert all(isinstance(c, Credential) for c in creds) 49 | 50 | 51 | class TestCredential: 52 | 53 | def test_delete(self, credential, respx_mock): 54 | req_url = f'{credential.url}doDelete' 55 | respx_mock.post(req_url) 56 | credential.delete() 57 | assert respx_mock.calls[0].response.status_code == 200 58 | assert respx_mock.calls[0].request.url == req_url 59 | 60 | @pytest.mark.parametrize('req, xml, body', 61 | [('GET', None, ''), ('POST', '', '')], 62 | ids=['get', 'set']) 63 | def test_configure(self, credential, respx_mock, req, xml, body): 64 | req_url = f'{credential.url}config.xml' 65 | respx_mock.route(method=req, url=req_url).respond(content=body) 66 | text = credential.configure(xml) 67 | assert respx_mock.calls[0].request.url == req_url 68 | 69 | 70 | class TestAsyncCredentials: 71 | 72 | async def test_get(self, async_jenkins): 73 | assert isinstance(await async_jenkins.credentials['_'], AsyncDomain) 74 | assert await async_jenkins.credentials['x'] is None 75 | 76 | async def test_create(self, async_jenkins, respx_mock): 77 | req_url = f'{async_jenkins.credentials.url}createDomain' 78 | respx_mock.post(req_url) 79 | await async_jenkins.credentials.create('xml') 80 | assert respx_mock.calls[0].request.url == req_url 81 | 82 | async def test_iter(self, async_jenkins): 83 | assert len([c async for c in async_jenkins.credentials]) == 3 84 | 85 | 86 | class TestAsyncDomain: 87 | 88 | @pytest.mark.parametrize('id_, obj', [('not exist', type(None)), ('test-user', AsyncCredential)]) 89 | async def test_get(self, async_global_domain, id_, obj): 90 | assert isinstance(await async_global_domain[id_], obj) 91 | 92 | async def test_create(self, async_global_domain, respx_mock): 93 | req_url = f'{async_global_domain.url}createCredentials' 94 | respx_mock.post(req_url) 95 | await async_global_domain.create('xml') 96 | assert respx_mock.calls[0].request.url == req_url 97 | 98 | async def test_iter(self, async_global_domain): 99 | creds = [c async for c in async_global_domain] 100 | assert len(creds) == 2 101 | assert all(isinstance(c, AsyncCredential) for c in creds) 102 | -------------------------------------------------------------------------------- /tests/unit/test_input.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from api4jenkins.input import AsyncPendingInputAction, PendingInputAction, _make_input_params 3 | 4 | 5 | raw = { 6 | "id": "3eaa25d43fac6e39a12c3936942b72c8", 7 | "proceedText": "Proceed", 8 | "message": "Is the build okay?", 9 | "inputs": [], 10 | "proceedUrl": "/job/input-pipeline/47/wfapi/inputSubmit?inputId=3eaa25d43fac6e39a12c3936942b72c8", 11 | "abortUrl": "/job/input-pipeline/47/input/3eaa25d43fac6e39a12c3936942b72c8/abort", 12 | "redirectApprovalUrl": "/job/input-pipeline/47/input/" 13 | } 14 | 15 | 16 | @pytest.fixture 17 | def pending_input(jenkins): 18 | return PendingInputAction(jenkins, raw) 19 | 20 | 21 | @pytest.fixture 22 | def async_pending_input(async_jenkins): 23 | return AsyncPendingInputAction(async_jenkins, raw) 24 | # @pytest.mark.skip 25 | 26 | 27 | class TestPendingInput: 28 | 29 | def test_access_attrs(self, pending_input): 30 | assert isinstance(pending_input, PendingInputAction) 31 | assert pending_input.message == "Is the build okay?" 32 | assert pending_input.id == "3eaa25d43fac6e39a12c3936942b72c8" 33 | assert pending_input.proceed_url == "/job/input-pipeline/47/wfapi/inputSubmit?inputId=3eaa25d43fac6e39a12c3936942b72c8" 34 | assert pending_input.abort_url == "/job/input-pipeline/47/input/3eaa25d43fac6e39a12c3936942b72c8/abort" 35 | 36 | def test_abort(self, pending_input, respx_mock): 37 | respx_mock.post(f'{pending_input.url}abort') 38 | pending_input.abort() 39 | assert respx_mock.calls[0].request.url == f'{pending_input.url}abort' 40 | 41 | def test_submit_empty(self, pending_input, respx_mock): 42 | respx_mock.post(f'{pending_input.url}proceedEmpty') 43 | pending_input.submit() 44 | assert respx_mock.calls[0].request.url == f'{pending_input.url}proceedEmpty' 45 | 46 | def test_submit_arg(self, pending_input, respx_mock): 47 | pending_input.raw['inputs'] = [{'name': 'arg1'}] 48 | respx_mock.post(f'{pending_input.url}submit').respond( 49 | json={'arg1': 'x'}) 50 | pending_input.submit(arg1='x') 51 | assert respx_mock.calls[0].request.url == f'{pending_input.url}submit' 52 | 53 | def test_submit_empty_with_arg(self, pending_input): 54 | pending_input.raw['inputs'] = [] 55 | with pytest.raises(TypeError): 56 | pending_input.submit(arg1='x') 57 | 58 | def test_submit_wrong_arg(self, pending_input): 59 | pending_input.raw['inputs'] = [{'name': 'arg1'}] 60 | with pytest.raises(TypeError): 61 | pending_input.submit(arg2='x') 62 | 63 | 64 | class TestAsyncPendingInput: 65 | 66 | async def test_access_attrs(self, async_pending_input): 67 | assert isinstance(async_pending_input, AsyncPendingInputAction) 68 | assert await async_pending_input.message == "Is the build okay?" 69 | assert await async_pending_input.id == "3eaa25d43fac6e39a12c3936942b72c8" 70 | assert await async_pending_input.proceed_url == "/job/input-pipeline/47/wfapi/inputSubmit?inputId=3eaa25d43fac6e39a12c3936942b72c8" 71 | assert await async_pending_input.abort_url == "/job/input-pipeline/47/input/3eaa25d43fac6e39a12c3936942b72c8/abort" 72 | 73 | async def test_abort(self, async_pending_input, respx_mock): 74 | url = f'{async_pending_input.url}abort' 75 | respx_mock.post(url) 76 | await async_pending_input.abort() 77 | assert respx_mock.calls[0].request.url == url 78 | 79 | async def test_submit_empty(self, async_pending_input, respx_mock): 80 | url = f'{async_pending_input.url}proceedEmpty' 81 | respx_mock.post(url) 82 | await async_pending_input.submit() 83 | assert respx_mock.calls[0].request.url == url 84 | 85 | async def test_submit_arg(self, async_pending_input, respx_mock): 86 | async_pending_input.raw['inputs'] = [{'name': 'arg1'}] 87 | url = f'{async_pending_input.url}submit' 88 | respx_mock.post(url) 89 | await async_pending_input.submit(arg1='x') 90 | assert respx_mock.calls[0].request.url == url 91 | 92 | async def test_submit_empty_with_arg(self, async_pending_input): 93 | async_pending_input.raw['inputs'] = [] 94 | with pytest.raises(TypeError): 95 | await async_pending_input.submit(arg1='x') 96 | 97 | async def test_submit_wrong_arg(self, async_pending_input): 98 | async_pending_input.raw['inputs'] = [{'name': 'arg1'}] 99 | with pytest.raises(TypeError): 100 | await async_pending_input.submit(arg2='x') 101 | -------------------------------------------------------------------------------- /tests/unit/test_job.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import pytest 3 | 4 | from api4jenkins.build import AsyncWorkflowRun, WorkflowRun 5 | from api4jenkins.item import snake 6 | 7 | 8 | class TestFolder: 9 | 10 | def test_parent(self, folder, jenkins, job): 11 | assert job.parent == folder 12 | assert folder.parent == jenkins 13 | 14 | @pytest.mark.parametrize('req, xml, body', 15 | [('GET', None, ''), ('POST', '', '')], 16 | ids=['get', 'set']) 17 | def test_configure(self, folder, respx_mock, req, xml, body): 18 | req_url = f'{folder.url}config.xml' 19 | respx_mock.route(method=req, url=req_url).respond(content=body) 20 | text = folder.configure(xml) 21 | assert respx_mock.calls[0].request.url == req_url 22 | 23 | 24 | class TestWorkflowMultiBranchProject: 25 | 26 | def test_scan(self, multi_job, respx_mock): 27 | req_url = f'{multi_job.url}build?delay=0' 28 | respx_mock.post(req_url) 29 | multi_job.scan() 30 | assert respx_mock.calls[0].request.url == req_url 31 | 32 | def test_get_scan_log(self, multi_job, respx_mock): 33 | body = 'a\nb' 34 | respx_mock.get( 35 | f'{multi_job.url}indexing/consoleText').respond(content=body) 36 | assert list(multi_job.get_scan_log()) == body.split('\n') 37 | 38 | 39 | class TestOrganizationFolder: 40 | def test_get_scan_log(self, org_job, respx_mock): 41 | body = 'a\nb' 42 | respx_mock.get( 43 | f'{org_job.url}computation/consoleText').respond(content=body) 44 | assert list(org_job.get_scan_log()) == body.split('\n') 45 | 46 | 47 | class TestProject: 48 | 49 | @pytest.mark.parametrize('number, obj', 50 | [(52, WorkflowRun), (100, type(None))], 51 | ids=['exist', 'not exist']) 52 | def test_get_build(self, job, number, obj): 53 | build = job[number] 54 | assert isinstance(build, obj) 55 | build = job[f'#{number}'] 56 | assert isinstance(build, obj) 57 | 58 | @pytest.mark.parametrize('key', ['firstBuild', 'lastBuild', 'lastCompletedBuild', 59 | 'lastFailedBuild', 'lastStableBuild', 'lastUnstableBuild', 60 | 'lastSuccessfulBuild', 'lastUnsuccessfulBuild']) 61 | def test_get_(self, job, key): 62 | build = getattr(job, snake(f'get_{key}'))() 63 | assert isinstance(build, WorkflowRun) 64 | assert build.url == job.api_json()[key]['url'] 65 | 66 | def test_iter_builds(self, job): 67 | builds = list(job) 68 | assert len(builds) == 8 69 | 70 | @pytest.mark.parametrize('action', ['enable', 'disable']) 71 | def test_enable_disable(self, job, respx_mock, action): 72 | req_url = f'{job.url}{action}' 73 | respx_mock.post(req_url) 74 | getattr(job, action)() 75 | assert respx_mock.calls[0].request.url == req_url 76 | 77 | def test_building(self, job): 78 | assert job.building is False 79 | 80 | 81 | class TestAsyncFolder: 82 | 83 | async def test_parent(self, async_folder, async_jenkins, async_job): 84 | assert await async_folder.parent == async_jenkins 85 | assert await async_job.parent == async_folder 86 | 87 | @pytest.mark.parametrize('req, xml, body', 88 | [('GET', None, ''), ('POST', '', '')], 89 | ids=['get', 'set']) 90 | async def test_configure(self, async_folder, req, xml, body, respx_mock): 91 | req_url = f'{async_folder.url}config.xml' 92 | respx_mock.route(method=req, url=req_url).respond(content=body) 93 | text = await async_folder.configure(xml) 94 | assert respx_mock.calls[0].request.url == req_url 95 | 96 | 97 | class TestAsyncWorkflowMultiBranchProject: 98 | 99 | async def test_scan(self, async_multi_job, respx_mock): 100 | req_url = f'{async_multi_job.url}build?delay=0' 101 | respx_mock.post(req_url) 102 | await async_multi_job.scan() 103 | assert respx_mock.calls[0].request.url == req_url 104 | 105 | async def test_get_scan_log(self, async_multi_job, respx_mock): 106 | body = 'a\nb' 107 | respx_mock.get( 108 | f'{async_multi_job.url}indexing/consoleText').respond(content=body) 109 | assert [line async for line in async_multi_job.get_scan_log()] == body.split('\n') 110 | 111 | 112 | class TestAsyncOrganizationFolder: 113 | async def test_get_scan_log(self, async_org_job, respx_mock): 114 | body = 'a\nb' 115 | respx_mock.get( 116 | f'{async_org_job.url}computation/consoleText').respond(content=body) 117 | assert [line async for line in async_org_job.get_scan_log()] == body.split('\n') 118 | 119 | 120 | class TestAsyncProject: 121 | 122 | @pytest.mark.parametrize('number, obj', 123 | [(52, AsyncWorkflowRun), (100, type(None))], 124 | ids=['exist', 'not exist']) 125 | async def test_get_build(self, async_job, number, obj): 126 | build = await async_job[number] 127 | assert isinstance(build, obj) 128 | build = await async_job[f'#{number}'] 129 | assert isinstance(build, obj) 130 | 131 | @pytest.mark.parametrize('key', ['firstBuild', 'lastBuild', 'lastCompletedBuild', 132 | 'lastFailedBuild', 'lastStableBuild', 'lastUnstableBuild', 133 | 'lastSuccessfulBuild', 'lastUnsuccessfulBuild']) 134 | async def test_get_(self, async_job, key): 135 | build = await getattr(async_job, snake(f'get_{key}'))() 136 | job_json = await async_job.api_json() 137 | assert isinstance(build, AsyncWorkflowRun) 138 | assert build.url == job_json[key]['url'] 139 | 140 | async def test_iter_builds(self, async_job): 141 | builds = [b async for b in async_job] 142 | assert len(builds) == 8 143 | 144 | @pytest.mark.parametrize('action', ['enable', 'disable']) 145 | async def test_enable_disable(self, async_job, respx_mock, action): 146 | req_url = f'{async_job.url}{action}' 147 | respx_mock.post(req_url) 148 | await getattr(async_job, action)() 149 | assert respx_mock.calls[0].request.url == req_url 150 | 151 | async def test_building(self, async_job): 152 | assert await async_job.building is False 153 | -------------------------------------------------------------------------------- /tests/unit/test_node.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | 4 | class TestNodes: 5 | 6 | def test_get(self, jenkins): 7 | node = jenkins.nodes.get('master') 8 | assert node.url == 'http://0.0.0.0:8080/computer/(master)/' 9 | assert jenkins.nodes.get('notexist') is None 10 | 11 | def test_iter(self, jenkins): 12 | nodes = list(jenkins.nodes) 13 | assert len(nodes) == 2 14 | assert nodes[0].display_name == 'master' 15 | 16 | def test_iter_builds(self, jenkins): 17 | builds = list(jenkins.nodes.iter_builds()) 18 | assert len(builds) == 2 19 | assert builds[0].url == 'http://0.0.0.0:8080/job/freestylejob/14/' 20 | assert builds[1].url == 'http://0.0.0.0:8080/job/folder/job/pipeline/53/' 21 | 22 | 23 | class TestNode: 24 | 25 | def test_iter_builds(self, jenkins): 26 | node = jenkins.nodes.get('master') 27 | builds = list(node) 28 | assert len(builds) == 2 29 | assert builds[0].url == 'http://0.0.0.0:8080/job/freestylejob/14/' 30 | assert builds[1].url == 'http://0.0.0.0:8080/job/folder/job/pipeline/53/' 31 | -------------------------------------------------------------------------------- /tests/unit/test_plugin.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | import pytest 4 | from api4jenkins.plugin import Plugin 5 | 6 | 7 | class TestPlugins: 8 | 9 | @pytest.mark.parametrize('name, obj', 10 | [('not exist', type(None)), ('git', Plugin)]) 11 | def test_get(self, jenkins, name, obj): 12 | assert isinstance(jenkins.plugins.get(name), obj) 13 | -------------------------------------------------------------------------------- /tests/unit/test_queue.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | import pytest 4 | 5 | from api4jenkins.build import AsyncWorkflowRun, WorkflowRun 6 | from api4jenkins.job import AsyncWorkflowJob, WorkflowJob 7 | from api4jenkins.queue import AsyncQueueItem, QueueItem 8 | 9 | from .conftest import load_json 10 | 11 | 12 | class TestQueue: 13 | 14 | @pytest.mark.parametrize('id_, obj', 15 | [(1, type(None)), (669, QueueItem)]) 16 | def test_get(self, jenkins, id_, obj): 17 | assert isinstance(jenkins.queue.get(id_), obj) 18 | 19 | def test_iter(self, jenkins): 20 | items = list(jenkins.queue) 21 | assert len(items) == 2 22 | 23 | def test_cancel(self, jenkins, respx_mock): 24 | req_url = f'{jenkins.queue.url}cancelItem?id=0' 25 | respx_mock.post(req_url) 26 | jenkins.queue.cancel(0) 27 | assert respx_mock.calls[0].request.url == req_url 28 | 29 | 30 | class TestQueueItem: 31 | 32 | @pytest.mark.parametrize('id_, type_', 33 | [(669, 'blockeditem'), 34 | (599, 'leftitem'), 35 | (700, 'waitingitem'), 36 | (668, 'buildableitem')]) 37 | def test_get_job(self, jenkins, job, id_, type_, monkeypatch): 38 | item = QueueItem(jenkins, f'{jenkins.url}queue/item/{id_}/') 39 | 40 | def _api_json(tree='', depth=0): 41 | return load_json(f'queue/{type_}.json') 42 | 43 | monkeypatch.setattr(item, 'api_json', _api_json) 44 | assert job == item.get_job() 45 | 46 | @pytest.mark.parametrize('id_, type_, obj', 47 | [(669, 'blockeditem', type(None)), 48 | (599, 'leftitem', WorkflowRun), 49 | (668, 'waitingitem', WorkflowRun), 50 | (668, 'buildableitem', WorkflowRun)]) 51 | def test_get_build(self, jenkins, id_, type_, obj, monkeypatch): 52 | item = QueueItem(jenkins, f'{jenkins.url}queue/item/{id_}/') 53 | 54 | def _api_json(tree='', depth=0): 55 | return load_json(f'queue/{type_}.json') 56 | 57 | monkeypatch.setattr(item, 'api_json', _api_json) 58 | build = item.get_build() 59 | assert isinstance(build, obj) 60 | 61 | def test_get_parameters(self, jenkins): 62 | item = QueueItem(jenkins, f'{jenkins.url}queue/item/668/') 63 | params = item.get_parameters() 64 | assert len(params) == 0 65 | 66 | def test_get_causes(self, jenkins): 67 | item = QueueItem(jenkins, f'{jenkins.url}queue/item/668/') 68 | causes = item.get_causes() 69 | assert causes[0]['shortDescription'] == 'Triggered by' 70 | 71 | 72 | class TestAsyncQueue: 73 | 74 | @pytest.mark.parametrize('id_, obj', 75 | [(1, type(None)), (669, AsyncQueueItem)]) 76 | async def test_get(self, async_jenkins, id_, obj): 77 | assert isinstance(await async_jenkins.queue.get(id_), obj) 78 | 79 | async def test_iter(self, async_jenkins): 80 | items = [i async for i in async_jenkins.queue] 81 | assert len(items) == 2 82 | 83 | async def test_cancel(self, async_jenkins, respx_mock): 84 | req_url = f'{async_jenkins.queue.url}cancelItem?id=0' 85 | respx_mock.post(req_url) 86 | await async_jenkins.queue.cancel(0) 87 | assert respx_mock.calls[0].request.url == req_url 88 | 89 | 90 | class TestAsyncQueueItem: 91 | 92 | @pytest.mark.parametrize('id_, type_', 93 | [(669, 'blockeditem'), 94 | (599, 'leftitem'), 95 | (700, 'waitingitem'), 96 | (668, 'buildableitem')]) 97 | async def test_get_job(self, async_jenkins, async_job, id_, type_, monkeypatch): 98 | item = AsyncQueueItem( 99 | async_jenkins, f'{async_jenkins.url}queue/item/{id_}/') 100 | 101 | async def _api_json(tree='', depth=0): 102 | return load_json(f'queue/{type_}.json') 103 | 104 | monkeypatch.setattr(item, 'api_json', _api_json) 105 | job = await item.get_job() 106 | assert isinstance(job, AsyncWorkflowJob) 107 | assert job == job 108 | 109 | @pytest.mark.parametrize('id_, type_, obj', 110 | [(669, 'blockeditem', type(None)), 111 | (599, 'leftitem', AsyncWorkflowRun), 112 | (668, 'waitingitem', AsyncWorkflowRun), 113 | (668, 'buildableitem', AsyncWorkflowRun)]) 114 | async def test_get_build(self, async_jenkins, id_, type_, obj, monkeypatch): 115 | item = AsyncQueueItem( 116 | async_jenkins, f'{async_jenkins.url}queue/item/{id_}/') 117 | 118 | async def _api_json(tree='', depth=0): 119 | return load_json(f'queue/{type_}.json') 120 | 121 | monkeypatch.setattr(item, 'api_json', _api_json) 122 | assert isinstance(await item.get_build(), obj) 123 | 124 | async def test_get_parameters(self, async_jenkins): 125 | item = AsyncQueueItem( 126 | async_jenkins, f'{async_jenkins.url}queue/item/668/') 127 | params = await item.get_parameters() 128 | assert len(params) == 0 129 | 130 | async def test_get_causes(self, async_jenkins): 131 | item = AsyncQueueItem( 132 | async_jenkins, f'{async_jenkins.url}queue/item/668/') 133 | causes = await item.get_causes() 134 | assert causes[0]['shortDescription'] == 'Triggered by' 135 | -------------------------------------------------------------------------------- /tests/unit/test_report.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.fixture 5 | def suite(build): 6 | return build.get_test_report().get('pytest') 7 | 8 | 9 | @pytest.fixture 10 | def case(suite): 11 | return suite.get('test_exists') 12 | 13 | 14 | @pytest.fixture 15 | def coverage_report(build): 16 | return build.get_coverage_report() 17 | 18 | 19 | class TestTestReport: 20 | 21 | def test_attributes(self, test_report): 22 | assert test_report.fail_count == 2 23 | assert test_report.pass_count == 37 24 | 25 | def test_iterate(self, test_report): 26 | assert len(list(test_report)) == 1 27 | assert len(list(test_report.suites)) == 1 28 | 29 | 30 | class TestTestSuite: 31 | 32 | def test_attributes(self, suite): 33 | assert suite.name == 'pytest' 34 | assert suite.duration == 74.678 35 | 36 | def test_get_case(self, suite): 37 | case = suite.get('test_exists') 38 | assert case.name == 'test_exists' 39 | assert case.status == 'PASSED' 40 | assert suite.get('notexist') is None 41 | 42 | def test_iterate(self, suite): 43 | assert len(list(suite)) == 1 44 | assert len(list(suite.cases)) == 1 45 | 46 | 47 | class TestTestCase: 48 | 49 | def test_attributes(self, case): 50 | assert case.name == 'test_exists' 51 | assert case.status == 'PASSED' 52 | 53 | 54 | class TestCoverageReport: 55 | 56 | def test_attributes(self, coverage_report): 57 | assert coverage_report.branch_coverage.covered == 244 58 | assert coverage_report.method_coverage.covered == 320 59 | 60 | def test_get(self, coverage_report): 61 | assert coverage_report.get('branchCoverage').covered == 244 62 | assert coverage_report.get('methodCoverage').covered == 320 63 | 64 | def test_wrong_attribute(self, coverage_report): 65 | with pytest.raises(AttributeError): 66 | coverage_report.xxxxx 67 | 68 | 69 | class TestCoverageResult: 70 | 71 | def test_get(self, build): 72 | cr = build.get_coverage_result() 73 | assert cr.get('Report').ratio == 100 74 | assert cr.get('Line').ratio == 83.83372 75 | 76 | 77 | @pytest.fixture 78 | async def async_suite(async_test_report): 79 | return await async_test_report.get('pytest') 80 | 81 | 82 | @pytest.fixture 83 | async def async_case(async_suite): 84 | return await async_suite.get('test_exists') 85 | 86 | 87 | @pytest.fixture 88 | async def async_coverage_report(async_build): 89 | return await async_build.get_coverage_report() 90 | 91 | 92 | @pytest.fixture 93 | async def async_test_report(async_build): 94 | return await async_build.get_test_report() 95 | 96 | 97 | class TestAsyncTestReport: 98 | 99 | async def test_attributes(self, async_test_report): 100 | assert await async_test_report.fail_count == 2 101 | assert await async_test_report.pass_count == 37 102 | 103 | async def test_iterate(self, async_test_report): 104 | assert len([s async for s in async_test_report]) == 1 105 | assert len([s async for s in async_test_report.suites]) == 1 106 | 107 | 108 | class TestAsyncCoverageReport: 109 | 110 | async def test_attributes(self, async_coverage_report): 111 | assert (await async_coverage_report.branch_coverage).covered == 244 112 | assert (await async_coverage_report.method_coverage).covered == 320 113 | 114 | async def test_get(self, async_coverage_report): 115 | assert (await async_coverage_report.get('branchCoverage')).covered == 244 116 | assert (await async_coverage_report.get('methodCoverage')).covered == 320 117 | 118 | async def test_wrong_attribute(self, async_coverage_report): 119 | with pytest.raises(AttributeError): 120 | await async_coverage_report.xxxxx 121 | -------------------------------------------------------------------------------- /tests/unit/test_system.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from api4jenkins.item import snake 3 | 4 | 5 | class TestSystem: 6 | 7 | @pytest.mark.parametrize('action', ['restart', 'safeRestart', 'quietDown', 8 | 'cancelQuietDown', 'exit', 'safeExit']) 9 | def test_enable_disable(self, jenkins, respx_mock, action): 10 | req_url = f'{jenkins.system.url}{action}' 11 | respx_mock.post(req_url) 12 | getattr(jenkins.system, snake(action))() 13 | assert respx_mock.calls[0].request.url == req_url 14 | 15 | def test_reload_jcasc(self, jenkins, respx_mock): 16 | req_url = f'{jenkins.system.url}configuration-as-code/reload' 17 | respx_mock.post(req_url) 18 | jenkins.system.reload_jcasc() 19 | assert respx_mock.calls[0].request.url == req_url 20 | 21 | def test_export_jcasc(self, jenkins, respx_mock): 22 | req_url = f'{jenkins.system.url}configuration-as-code/export' 23 | respx_mock.post(req_url) 24 | jenkins.system.export_jcasc() 25 | assert respx_mock.calls[0].request.url == req_url 26 | 27 | def test_apply_jcasc(self, jenkins, respx_mock): 28 | check_url = f'{jenkins.system.url}configuration-as-code/checkNewSource?newSource=yaml' 29 | respx_mock.post(check_url) 30 | req_url = f'{jenkins.system.url}configuration-as-code/replace' 31 | respx_mock.post(req_url) 32 | jenkins.system.apply_jcasc('yaml') 33 | assert respx_mock.calls[0].request.url == check_url 34 | assert respx_mock.calls[1].request.url == req_url 35 | 36 | def test_apply_jcasc_fail(self, jenkins, respx_mock): 37 | check_url = f'{jenkins.system.url}configuration-as-code/checkNewSource?newSource=yaml' 38 | respx_mock.post(check_url).respond(text='
') 39 | with pytest.raises(ValueError): 40 | jenkins.system.apply_jcasc('yaml') 41 | assert respx_mock.calls[0].request.url == check_url 42 | 43 | 44 | class TestAsyncSystem: 45 | 46 | @pytest.mark.parametrize('action', ['restart', 'safeRestart', 'quietDown', 47 | 'cancelQuietDown', 'exit', 'safeExit']) 48 | async def test_enable_disable(self, async_jenkins, respx_mock, action): 49 | req_url = f'{async_jenkins.system.url}{action}' 50 | respx_mock.post(req_url) 51 | await getattr(async_jenkins.system, snake(action))() 52 | assert respx_mock.calls[0].request.url == req_url 53 | 54 | async def test_reload_jcasc(self, async_jenkins, respx_mock): 55 | req_url = f'{async_jenkins.system.url}configuration-as-code/reload' 56 | respx_mock.post(req_url) 57 | await async_jenkins.system.reload_jcasc() 58 | assert respx_mock.calls[0].request.url == req_url 59 | 60 | async def test_export_jcasc(self, async_jenkins, respx_mock): 61 | req_url = f'{async_jenkins.system.url}configuration-as-code/export' 62 | respx_mock.post(req_url) 63 | await async_jenkins.system.export_jcasc() 64 | assert respx_mock.calls[0].request.url == req_url 65 | 66 | async def test_apply_jcasc(self, async_jenkins, respx_mock): 67 | check_url = f'{async_jenkins.system.url}configuration-as-code/checkNewSource?newSource=yaml' 68 | respx_mock.post(check_url) 69 | req_url = f'{async_jenkins.system.url}configuration-as-code/replace' 70 | respx_mock.post(req_url) 71 | await async_jenkins.system.apply_jcasc('yaml') 72 | assert respx_mock.calls[0].request.url == check_url 73 | assert respx_mock.calls[1].request.url == req_url 74 | 75 | async def test_apply_jcasc_fail(self, async_jenkins, respx_mock): 76 | check_url = f'{async_jenkins.system.url}configuration-as-code/checkNewSource?newSource=yaml' 77 | respx_mock.post(check_url).respond(text='
') 78 | with pytest.raises(ValueError): 79 | await async_jenkins.system.apply_jcasc('yaml') 80 | assert respx_mock.calls[0].request.url == check_url 81 | -------------------------------------------------------------------------------- /tests/unit/test_view.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import pytest 3 | from api4jenkins.view import AllView, AsyncAllView 4 | 5 | 6 | class TestViews: 7 | 8 | def test_get(self, jenkins): 9 | view = jenkins.views.get('all') 10 | assert isinstance(view, AllView) 11 | assert view.url == jenkins.url + 'view/all/' 12 | assert jenkins.views.get('not exist') is None 13 | 14 | def test_views(self, jenkins): 15 | assert len(list(jenkins.views)) == 1 16 | 17 | 18 | class TestView: 19 | 20 | def test_get_job(self, view): 21 | job_in_view = view['folder'] 22 | job = view.jenkins.get_job('folder') 23 | assert job_in_view == job 24 | assert view['not exist'] is None 25 | 26 | def test_iter(self, view): 27 | assert len(list(view)) == 2 28 | 29 | @pytest.mark.parametrize('action, entry', [('include', 'addJobToView'), 30 | ('exclude', 'removeJobFromView')]) 31 | def test_include_exclude(self, view, respx_mock, action, entry): 32 | req_url = f'{view.url}{entry}?name=folder1' 33 | respx_mock.post(req_url).respond(json={'name': 'folder1'}) 34 | getattr(view, action)('folder1') 35 | assert respx_mock.calls[0].request.url == req_url 36 | 37 | 38 | class TestAsyncViews: 39 | 40 | async def test_get(self, async_jenkins): 41 | view = await async_jenkins.views['all'] 42 | assert isinstance(view, AsyncAllView) 43 | assert view.url == f'{async_jenkins.url}view/all/' 44 | assert await async_jenkins.views['not exist'] is None 45 | 46 | async def test_views(self, async_jenkins): 47 | assert len([view async for view in async_jenkins.views]) == 1 48 | 49 | 50 | class TestAsyncView: 51 | 52 | async def test_get_job(self, async_view, async_folder): 53 | job_in_view = await async_view['folder'] 54 | assert job_in_view == async_folder 55 | assert await async_view['not exist'] is None 56 | 57 | async def test_iter(self, async_view): 58 | assert len([job async for job in async_view]) == 2 59 | 60 | @pytest.mark.parametrize('action, entry', [('include', 'addJobToView'), 61 | ('exclude', 'removeJobFromView')]) 62 | async def test_include_exclude(self, async_view, respx_mock, action, entry): 63 | req_url = f'{async_view.url}{entry}?name=folder1' 64 | respx_mock.post(req_url).respond(json={'name': 'folder1'}) 65 | await getattr(async_view, action)('folder1') 66 | assert respx_mock.calls[0].request.url == req_url 67 | -------------------------------------------------------------------------------- /tests/unit/tests_data/credential/UserPasswordCredential.xml: -------------------------------------------------------------------------------- 1 | 2 | GLOBAL 3 | user-id 4 | user-name 5 | user-password 6 | user id for testing 7 | -------------------------------------------------------------------------------- /tests/unit/tests_data/credential/credentials.json: -------------------------------------------------------------------------------- 1 | { 2 | "_class": "com.cloudbees.plugins.credentials.CredentialsStoreAction$DomainWrapper", 3 | "credentials": [ 4 | { 5 | "description": "ssh key for connect slave", 6 | "displayName": "root (ssh key for connect slave)", 7 | "fingerprint": null, 8 | "fullName": "system/_/79aa8598-912d-4475-a962-748365cc81d6", 9 | "id": "79aa8598-912d-4475-a962-748365cc81d6", 10 | "typeName": "SSH Username with private key" 11 | }, 12 | { 13 | "description": "for testing", 14 | "displayName": "user1/****** (for testing)", 15 | "fingerprint": null, 16 | "fullName": "system/_/test-user", 17 | "id": "test-user", 18 | "typeName": "Username with password" 19 | } 20 | ], 21 | "description": "Credentials that should be available irrespective of domain specification to requirements matching.", 22 | "displayName": "Global credentials (unrestricted)", 23 | "fullDisplayName": "System » Global credentials (unrestricted)", 24 | "fullName": "system/_", 25 | "global": true, 26 | "urlName": "_" 27 | } -------------------------------------------------------------------------------- /tests/unit/tests_data/credential/domains.json: -------------------------------------------------------------------------------- 1 | { 2 | "_class": "com.cloudbees.hudson.plugins.folder.properties.FolderCredentialsProvider$FolderCredentialsProperty$CredentialsStoreActionImpl", 3 | "domains": { 4 | "_": { 5 | "_class": "com.cloudbees.plugins.credentials.CredentialsStoreAction$DomainWrapper", 6 | "urlName": "_" 7 | }, 8 | "production": { 9 | "_class": "com.cloudbees.plugins.credentials.CredentialsStoreAction$DomainWrapper", 10 | "urlName": "production" 11 | }, 12 | "testing": { 13 | "_class": "com.cloudbees.plugins.credentials.CredentialsStoreAction$DomainWrapper", 14 | "urlName": "testing" 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /tests/unit/tests_data/credential/user_psw.json: -------------------------------------------------------------------------------- 1 | { 2 | "_class": "com.cloudbees.plugins.credentials.CredentialsStoreAction$CredentialsWrapper", 3 | "description": "for testing", 4 | "displayName": "user1/****** (for testing)", 5 | "fingerprint": null, 6 | "fullName": "system/_/test-user", 7 | "id": "test-user", 8 | "typeName": "Username with password" 9 | } -------------------------------------------------------------------------------- /tests/unit/tests_data/jenkins/crumb.json: -------------------------------------------------------------------------------- 1 | { 2 | "crumbRequestField": "mock-crumb-id", 3 | "crumb": "mock-crumb" 4 | } -------------------------------------------------------------------------------- /tests/unit/tests_data/jenkins/jenkins.json: -------------------------------------------------------------------------------- 1 | { 2 | "_class": "hudson.model.Hudson", 3 | "assignedLabels": [ 4 | { 5 | "name": "master" 6 | } 7 | ], 8 | "mode": "EXCLUSIVE", 9 | "nodeDescription": "the master Jenkins node", 10 | "nodeName": "", 11 | "numExecutors": 1, 12 | "description": "Jenkins configured automatically by Jenkins Configuration as Code Plugin\n\n", 13 | "jobs": [ 14 | { 15 | "_class": "com.cloudbees.hudson.plugins.folder.Folder", 16 | "name": "folder", 17 | "url": "http://0.0.0.0:8080/job/folder/", 18 | "jobs": [ 19 | { 20 | "_class": "hudson.model.FreeStyleProject", 21 | "name": "freestylejob", 22 | "url": "http://0.0.0.0:8080/job/folder/job/freestylejob/", 23 | "color": "blue" 24 | }, 25 | { 26 | "_class": "org.jenkinsci.plugins.workflow.job.WorkflowJob", 27 | "name": "pipeline", 28 | "url": "http://0.0.0.0:8080/job/folder/job/pipeline/", 29 | "color": "red" 30 | }, 31 | { 32 | "_class": "hudson.matrix.MatrixProject", 33 | "name": "maxtrixjob", 34 | "url": "http://0.0.0.0:8080/job/folder/job/maxtrixjob/", 35 | "color": "blue" 36 | }, 37 | { 38 | "_class": "com.cloudbees.hudson.plugins.folder.Folder", 39 | "name": "folder2", 40 | "url": "http://0.0.0.0:8080/job/folder/job/folder2/" 41 | } 42 | ] 43 | } 44 | ], 45 | "overallLoad": {}, 46 | "primaryView": { 47 | "_class": "hudson.model.AllView", 48 | "name": "all", 49 | "url": "http://0.0.0.0:8080/" 50 | }, 51 | "quietingDown": false, 52 | "slaveAgentPort": 50000, 53 | "unlabeledLoad": { 54 | "_class": "jenkins.model.UnlabeledLoadStatistics" 55 | }, 56 | "useCrumbs": false, 57 | "useSecurity": true, 58 | "views": [ 59 | { 60 | "_class": "hudson.model.AllView", 61 | "name": "all", 62 | "url": "http://0.0.0.0:8080/" 63 | } 64 | ] 65 | } -------------------------------------------------------------------------------- /tests/unit/tests_data/job/folder.json: -------------------------------------------------------------------------------- 1 | { 2 | "_class": "com.cloudbees.hudson.plugins.folder.Folder", 3 | "actions": [ 4 | {}, 5 | {}, 6 | { 7 | "_class": "com.cloudbees.plugins.credentials.ViewCredentialsAction" 8 | } 9 | ], 10 | "description": "", 11 | "displayName": "folder", 12 | "displayNameOrNull": null, 13 | "fullDisplayName": "folder", 14 | "fullName": "folder", 15 | "name": "folder", 16 | "url": "http://0.0.0.0:8080/job/folder/", 17 | "jobs": [ 18 | { 19 | "_class": "hudson.model.FreeStyleProject", 20 | "name": "Level2_FreeStyleProject", 21 | "url": "http://0.0.0.0:8080/job/folder/job/Level2_FreeStyleProject/", 22 | "color": "blue" 23 | }, 24 | { 25 | "_class": "org.jenkinsci.plugins.workflow.job.WorkflowJob", 26 | "name": "pipeline", 27 | "url": "http://0.0.0.0:8080/job/folder/job/pipeline/", 28 | "color": "red" 29 | }, 30 | { 31 | "_class": "hudson.matrix.MatrixProject", 32 | "name": "Level2_MatrixProject", 33 | "url": "http://0.0.0.0:8080/job/folder/job/Level2_MatrixProject/", 34 | "color": "blue" 35 | }, 36 | { 37 | "_class": "com.cloudbees.hudson.plugins.folder.Folder", 38 | "name": "folder2", 39 | "url": "http://0.0.0.0:8080/job/folder/job/folder2/" 40 | } 41 | ], 42 | "primaryView": { 43 | "_class": "hudson.model.AllView", 44 | "name": "All", 45 | "url": "http://0.0.0.0:8080/job/folder/" 46 | }, 47 | "views": [ 48 | { 49 | "_class": "hudson.model.AllView", 50 | "name": "All", 51 | "url": "http://0.0.0.0:8080/job/folder/" 52 | } 53 | ] 54 | } -------------------------------------------------------------------------------- /tests/unit/tests_data/job/mbranch.json: -------------------------------------------------------------------------------- 1 | { 2 | "_class": "org.jenkinsci.plugins.workflow.multibranch.WorkflowMultiBranchProject", 3 | "actions": [ 4 | {}, 5 | {}, 6 | {}, 7 | { 8 | "_class": "org.jenkinsci.plugins.github_branch_source.GitHubRepoMetadataAction" 9 | }, 10 | { 11 | "_class": "jenkins.scm.api.metadata.ObjectMetadataAction" 12 | }, 13 | {}, 14 | {}, 15 | {}, 16 | {}, 17 | {}, 18 | {}, 19 | { 20 | "_class": "com.cloudbees.plugins.credentials.ViewCredentialsAction" 21 | } 22 | ], 23 | "description": null, 24 | "displayName": "Level1_multibranchjob", 25 | "displayNameOrNull": null, 26 | "fullDisplayName": "Level1_multibranchjob", 27 | "fullName": "Level1_multibranchjob", 28 | "name": "Level1_multibranchjob", 29 | "url": "http://0.0.0.0:8080/job/Level1_multibranchjob/", 30 | "healthReport": [ 31 | { 32 | "description": "Worst health: Level1_multibranchjob » feature/github-commands: Build stability: All recent builds failed.", 33 | "iconClassName": "icon-health-00to19", 34 | "iconUrl": "health-00to19.png", 35 | "score": 0 36 | } 37 | ], 38 | "jobs": [ 39 | { 40 | "_class": "org.jenkinsci.plugins.workflow.job.WorkflowJob", 41 | "name": "feature%2Fgithub-commands", 42 | "url": "http://0.0.0.0:8080/job/Level1_multibranchjob/job/branch1/", 43 | "color": "red" 44 | }, 45 | { 46 | "_class": "org.jenkinsci.plugins.workflow.job.WorkflowJob", 47 | "name": "master", 48 | "url": "http://0.0.0.0:8080/job/Level1_multibranchjob/job/master/", 49 | "color": "red" 50 | } 51 | ], 52 | "primaryView": { 53 | "_class": "jenkins.branch.MultiBranchProjectViewHolder$ViewImpl", 54 | "name": "default", 55 | "url": "http://0.0.0.0:8080/job/Level1_multibranchjob/" 56 | }, 57 | "views": [ 58 | { 59 | "_class": "jenkins.branch.MultiBranchProjectViewHolder$ViewImpl", 60 | "name": "change-requests", 61 | "url": "http://0.0.0.0:8080/job/Level1_multibranchjob/view/change-requests/" 62 | }, 63 | { 64 | "_class": "jenkins.branch.MultiBranchProjectViewHolder$ViewImpl", 65 | "name": "default", 66 | "url": "http://0.0.0.0:8080/job/Level1_multibranchjob/" 67 | } 68 | ], 69 | "sources": [ 70 | {} 71 | ] 72 | } -------------------------------------------------------------------------------- /tests/unit/tests_data/job/pipeline.json: -------------------------------------------------------------------------------- 1 | { 2 | "_class": "org.jenkinsci.plugins.workflow.job.WorkflowJob", 3 | "actions": [ 4 | {}, 5 | {}, 6 | {}, 7 | {}, 8 | {}, 9 | {}, 10 | { 11 | "_class": "hudson.plugins.jobConfigHistory.JobConfigHistoryProjectAction" 12 | }, 13 | {}, 14 | {}, 15 | {}, 16 | {}, 17 | {}, 18 | { 19 | "_class": "org.jenkinsci.plugins.testresultsanalyzer.TestResultsAnalyzerAction" 20 | }, 21 | {}, 22 | {}, 23 | { 24 | "_class": "com.cloudbees.plugins.credentials.ViewCredentialsAction" 25 | } 26 | ], 27 | "description": "", 28 | "displayName": "Level1_WorkflowJob", 29 | "displayNameOrNull": null, 30 | "fullDisplayName": "Level1_WorkflowJob", 31 | "fullName": "Level1_WorkflowJob", 32 | "name": "Level1_WorkflowJob", 33 | "url": "http://0.0.0.0:8080/job/Level1_WorkflowJob/", 34 | "buildable": true, 35 | "builds": [ 36 | { 37 | "_class": "org.jenkinsci.plugins.workflow.job.WorkflowRun", 38 | "number": 52, 39 | "displayName": "#52", 40 | "url": "http://0.0.0.0:8080/job/Level1_WorkflowJob/52/", 41 | "building": false 42 | }, 43 | { 44 | "_class": "org.jenkinsci.plugins.workflow.job.WorkflowRun", 45 | "number": 51, 46 | "displayName": "#51", 47 | "url": "http://0.0.0.0:8080/job/Level1_WorkflowJob/51/", 48 | "building": false 49 | }, 50 | { 51 | "_class": "org.jenkinsci.plugins.workflow.job.WorkflowRun", 52 | "number": 50, 53 | "displayName": "#50", 54 | "url": "http://0.0.0.0:8080/job/Level1_WorkflowJob/50/", 55 | "building": false 56 | }, 57 | { 58 | "_class": "org.jenkinsci.plugins.workflow.job.WorkflowRun", 59 | "number": 49, 60 | "displayName": "#49", 61 | "url": "http://0.0.0.0:8080/job/Level1_WorkflowJob/49/", 62 | "building": false 63 | }, 64 | { 65 | "_class": "org.jenkinsci.plugins.workflow.job.WorkflowRun", 66 | "number": 48, 67 | "displayName": "#48", 68 | "url": "http://0.0.0.0:8080/job/Level1_WorkflowJob/48/", 69 | "building": false 70 | }, 71 | { 72 | "_class": "org.jenkinsci.plugins.workflow.job.WorkflowRun", 73 | "number": 47, 74 | "displayName": "#47", 75 | "url": "http://0.0.0.0:8080/job/Level1_WorkflowJob/47/", 76 | "building": false 77 | }, 78 | { 79 | "_class": "org.jenkinsci.plugins.workflow.job.WorkflowRun", 80 | "number": 46, 81 | "displayName": "#46", 82 | "url": "http://0.0.0.0:8080/job/Level1_WorkflowJob/46/", 83 | "building": false 84 | }, 85 | { 86 | "_class": "org.jenkinsci.plugins.workflow.job.WorkflowRun", 87 | "number": 45, 88 | "displayName": "#45", 89 | "url": "http://0.0.0.0:8080/job/Level1_WorkflowJob/45/", 90 | "building": false 91 | } 92 | ], 93 | "color": "red", 94 | "firstBuild": { 95 | "_class": "org.jenkinsci.plugins.workflow.job.WorkflowRun", 96 | "number": 45, 97 | "displayName": "#45", 98 | "url": "http://0.0.0.0:8080/job/Level1_WorkflowJob/45/" 99 | }, 100 | "healthReport": [ 101 | { 102 | "description": "Build stability: 4 out of the last 5 builds failed.", 103 | "iconClassName": "icon-health-00to19", 104 | "iconUrl": "health-00to19.png", 105 | "score": 20 106 | } 107 | ], 108 | "inQueue": false, 109 | "keepDependencies": false, 110 | "lastBuild": { 111 | "_class": "org.jenkinsci.plugins.workflow.job.WorkflowRun", 112 | "number": 52, 113 | "displayName": "#52", 114 | "url": "http://0.0.0.0:8080/job/Level1_WorkflowJob/52/" 115 | }, 116 | "lastCompletedBuild": { 117 | "_class": "org.jenkinsci.plugins.workflow.job.WorkflowRun", 118 | "number": 52, 119 | "displayName": "#52", 120 | "url": "http://0.0.0.0:8080/job/Level1_WorkflowJob/52/" 121 | }, 122 | "lastFailedBuild": { 123 | "_class": "org.jenkinsci.plugins.workflow.job.WorkflowRun", 124 | "number": 52, 125 | "displayName": "#52", 126 | "url": "http://0.0.0.0:8080/job/Level1_WorkflowJob/52/" 127 | }, 128 | "lastStableBuild": { 129 | "_class": "org.jenkinsci.plugins.workflow.job.WorkflowRun", 130 | "number": 49, 131 | "displayName": "#49", 132 | "url": "http://0.0.0.0:8080/job/Level1_WorkflowJob/49/" 133 | }, 134 | "lastSuccessfulBuild": { 135 | "_class": "org.jenkinsci.plugins.workflow.job.WorkflowRun", 136 | "number": 49, 137 | "displayName": "#49", 138 | "url": "http://0.0.0.0:8080/job/Level1_WorkflowJob/49/" 139 | }, 140 | "lastUnstableBuild": { 141 | "_class": "org.jenkinsci.plugins.workflow.job.WorkflowRun", 142 | "number": 52, 143 | "displayName": "#52", 144 | "url": "http://0.0.0.0:8080/job/Level1_WorkflowJob/52/" 145 | }, 146 | "lastUnsuccessfulBuild": { 147 | "_class": "org.jenkinsci.plugins.workflow.job.WorkflowRun", 148 | "number": 52, 149 | "displayName": "#52", 150 | "url": "http://0.0.0.0:8080/job/Level1_WorkflowJob/52/" 151 | }, 152 | "nextBuildNumber": 53, 153 | "property": [], 154 | "queueItem": null, 155 | "concurrentBuild": true, 156 | "resumeBlocked": false 157 | } -------------------------------------------------------------------------------- /tests/unit/tests_data/plugin/installStatus.json: -------------------------------------------------------------------------------- 1 | { 2 | "status": "ok", 3 | "data": { 4 | "jobs": [{ 5 | "name": "nant", 6 | "version": "1.4.3", 7 | "title": "NAnt", 8 | "installStatus": "Success", 9 | "requiresRestart": "false" 10 | }, { 11 | "name": "cloudbees-folder", 12 | "version": "6.9", 13 | "title": "Folders", 14 | "installStatus": "SuccessButRequiresRestart", 15 | "requiresRestart": "true" 16 | }, { 17 | "name": "scm-api", 18 | "version": "2.6.3", 19 | "title": "SCM API", 20 | "installStatus": "SuccessButRequiresRestart", 21 | "requiresRestart": "true" 22 | }, { 23 | "name": "structs", 24 | "version": "1.19", 25 | "title": "Structs", 26 | "installStatus": "SuccessButRequiresRestart", 27 | "requiresRestart": "true" 28 | }, { 29 | "name": "script-security", 30 | "version": "1.61", 31 | "title": "Script Security", 32 | "installStatus": "SuccessButRequiresRestart", 33 | "requiresRestart": "true" 34 | }, { 35 | "name": "workflow-step-api", 36 | "version": "2.20", 37 | "title": "Pipeline: Step API", 38 | "installStatus": "SuccessButRequiresRestart", 39 | "requiresRestart": "true" 40 | }, { 41 | "name": "workflow-api", 42 | "version": "2.35", 43 | "title": "Pipeline: API", 44 | "installStatus": "SuccessButRequiresRestart", 45 | "requiresRestart": "true" 46 | }, { 47 | "name": "workflow-support", 48 | "version": "3.3", 49 | "title": "Pipeline: Supporting APIs", 50 | "installStatus": "SuccessButRequiresRestart", 51 | "requiresRestart": "true" 52 | }, { 53 | "name": "workflow-cps", 54 | "version": "2.72", 55 | "title": "Pipeline: Groovy", 56 | "installStatus": "SuccessButRequiresRestart", 57 | "requiresRestart": "true" 58 | }, { 59 | "name": "config-file-provider", 60 | "version": "3.6.2", 61 | "title": "Config File Provider", 62 | "installStatus": "SuccessButRequiresRestart", 63 | "requiresRestart": "true" 64 | }, { 65 | "name": "workflow-job", 66 | "version": "2.33", 67 | "title": "Pipeline: Job", 68 | "installStatus": "SuccessButRequiresRestart", 69 | "requiresRestart": "true" 70 | }, { 71 | "name": "branch-api", 72 | "version": "2.5.3", 73 | "title": "Branch API", 74 | "installStatus": "SuccessButRequiresRestart", 75 | "requiresRestart": "true" 76 | }, { 77 | "name": "workflow-multibranch", 78 | "version": "2.21", 79 | "title": "Pipeline: Multibranch", 80 | "installStatus": "SuccessButRequiresRestart", 81 | "requiresRestart": "true" 82 | }, { 83 | "name": "pipeline-multibranch-defaults", 84 | "version": "2.0", 85 | "title": "Pipeline: Multibranch with defaults", 86 | "installStatus": "Failure", 87 | "requiresRestart": "false" 88 | }, { 89 | "name": "ownership", 90 | "version": "0.12.1", 91 | "title": "Job and Node ownership", 92 | "installStatus": "Success", 93 | "requiresRestart": "false" 94 | }, { 95 | "name": "nomad", 96 | "version": "0.6.5", 97 | "title": "Nomad", 98 | "installStatus": "Success", 99 | "requiresRestart": "false" 100 | }, { 101 | "name": "pipeline-multibranch-defaults", 102 | "version": "2.0", 103 | "title": "Pipeline: Multibranch with defaults", 104 | "installStatus": "Failure", 105 | "requiresRestart": "false" 106 | }, { 107 | "name": "multi-slave-config-plugin", 108 | "version": "1.2.0", 109 | "title": "Multi slave config", 110 | "installStatus": "Success", 111 | "requiresRestart": "false" 112 | }, { 113 | "name": "mock-slave", 114 | "version": "1.13", 115 | "title": "Mock Agent", 116 | "installStatus": "Success", 117 | "requiresRestart": "false" 118 | }, { 119 | "name": "jquery", 120 | "version": "1.12.4-0", 121 | "title": "jQuery", 122 | "installStatus": "Success", 123 | "requiresRestart": "false" 124 | }, { 125 | "name": "nodelabelparameter", 126 | "version": "1.7.2", 127 | "title": "Node and Label parameter", 128 | "installStatus": "Success", 129 | "requiresRestart": "false" 130 | }, { 131 | "name": "cloud-stats", 132 | "version": "0.24", 133 | "title": "Cloud Statistics", 134 | "installStatus": "Success", 135 | "requiresRestart": "false" 136 | }, { 137 | "name": "credentials", 138 | "version": "2.2.0", 139 | "title": "Credentials", 140 | "installStatus": "Pending", 141 | "requiresRestart": "false" 142 | }, { 143 | "name": "ssh-credentials", 144 | "version": "1.17.1", 145 | "title": "SSH Credentials", 146 | "installStatus": "Pending", 147 | "requiresRestart": "false" 148 | }, { 149 | "name": "openstack-cloud", 150 | "version": "2.47", 151 | "title": "OpenStack Cloud", 152 | "installStatus": "Pending", 153 | "requiresRestart": "false" 154 | } 155 | ], 156 | "state": "RESTART" 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /tests/unit/tests_data/plugin/installStatus_done.json: -------------------------------------------------------------------------------- 1 | { 2 | "status": "ok", 3 | "data": { 4 | "jobs": [{ 5 | "name": "nant", 6 | "version": "1.4.3", 7 | "title": "NAnt", 8 | "installStatus": "Success", 9 | "requiresRestart": "false" 10 | }, { 11 | "name": "cloudbees-folder", 12 | "version": "6.9", 13 | "title": "Folders", 14 | "installStatus": "SuccessButRequiresRestart", 15 | "requiresRestart": "true" 16 | }, { 17 | "name": "scm-api", 18 | "version": "2.6.3", 19 | "title": "SCM API", 20 | "installStatus": "SuccessButRequiresRestart", 21 | "requiresRestart": "true" 22 | }, { 23 | "name": "structs", 24 | "version": "1.19", 25 | "title": "Structs", 26 | "installStatus": "SuccessButRequiresRestart", 27 | "requiresRestart": "true" 28 | }, { 29 | "name": "script-security", 30 | "version": "1.61", 31 | "title": "Script Security", 32 | "installStatus": "SuccessButRequiresRestart", 33 | "requiresRestart": "true" 34 | }, { 35 | "name": "workflow-step-api", 36 | "version": "2.20", 37 | "title": "Pipeline: Step API", 38 | "installStatus": "SuccessButRequiresRestart", 39 | "requiresRestart": "true" 40 | }, { 41 | "name": "workflow-api", 42 | "version": "2.35", 43 | "title": "Pipeline: API", 44 | "installStatus": "SuccessButRequiresRestart", 45 | "requiresRestart": "true" 46 | }, { 47 | "name": "workflow-support", 48 | "version": "3.3", 49 | "title": "Pipeline: Supporting APIs", 50 | "installStatus": "SuccessButRequiresRestart", 51 | "requiresRestart": "true" 52 | }, { 53 | "name": "workflow-cps", 54 | "version": "2.72", 55 | "title": "Pipeline: Groovy", 56 | "installStatus": "SuccessButRequiresRestart", 57 | "requiresRestart": "true" 58 | }, { 59 | "name": "config-file-provider", 60 | "version": "3.6.2", 61 | "title": "Config File Provider", 62 | "installStatus": "SuccessButRequiresRestart", 63 | "requiresRestart": "true" 64 | }, { 65 | "name": "workflow-job", 66 | "version": "2.33", 67 | "title": "Pipeline: Job", 68 | "installStatus": "SuccessButRequiresRestart", 69 | "requiresRestart": "true" 70 | }, { 71 | "name": "branch-api", 72 | "version": "2.5.3", 73 | "title": "Branch API", 74 | "installStatus": "SuccessButRequiresRestart", 75 | "requiresRestart": "true" 76 | }, { 77 | "name": "workflow-multibranch", 78 | "version": "2.21", 79 | "title": "Pipeline: Multibranch", 80 | "installStatus": "SuccessButRequiresRestart", 81 | "requiresRestart": "true" 82 | }, { 83 | "name": "pipeline-multibranch-defaults", 84 | "version": "2.0", 85 | "title": "Pipeline: Multibranch with defaults", 86 | "installStatus": "Failure", 87 | "requiresRestart": "false" 88 | }, { 89 | "name": "ownership", 90 | "version": "0.12.1", 91 | "title": "Job and Node ownership", 92 | "installStatus": "Success", 93 | "requiresRestart": "false" 94 | }, { 95 | "name": "nomad", 96 | "version": "0.6.5", 97 | "title": "Nomad", 98 | "installStatus": "Success", 99 | "requiresRestart": "false" 100 | }, { 101 | "name": "pipeline-multibranch-defaults", 102 | "version": "2.0", 103 | "title": "Pipeline: Multibranch with defaults", 104 | "installStatus": "Failure", 105 | "requiresRestart": "false" 106 | }, { 107 | "name": "multi-slave-config-plugin", 108 | "version": "1.2.0", 109 | "title": "Multi slave config", 110 | "installStatus": "Success", 111 | "requiresRestart": "false" 112 | }, { 113 | "name": "mock-slave", 114 | "version": "1.13", 115 | "title": "Mock Agent", 116 | "installStatus": "Success", 117 | "requiresRestart": "false" 118 | }, { 119 | "name": "jquery", 120 | "version": "1.12.4-0", 121 | "title": "jQuery", 122 | "installStatus": "Success", 123 | "requiresRestart": "false" 124 | }, { 125 | "name": "nodelabelparameter", 126 | "version": "1.7.2", 127 | "title": "Node and Label parameter", 128 | "installStatus": "Success", 129 | "requiresRestart": "false" 130 | }, { 131 | "name": "cloud-stats", 132 | "version": "0.24", 133 | "title": "Cloud Statistics", 134 | "installStatus": "Success", 135 | "requiresRestart": "false" 136 | }, { 137 | "name": "credentials", 138 | "version": "2.2.0", 139 | "title": "Credentials", 140 | "installStatus": "SuccessButRequiresRestart", 141 | "requiresRestart": "true" 142 | }, { 143 | "name": "ssh-credentials", 144 | "version": "1.17.1", 145 | "title": "SSH Credentials", 146 | "installStatus": "SuccessButRequiresRestart", 147 | "requiresRestart": "true" 148 | }, { 149 | "name": "openstack-cloud", 150 | "version": "2.47", 151 | "title": "OpenStack Cloud", 152 | "installStatus": "Failure", 153 | "requiresRestart": "false" 154 | }, { 155 | "name": "vs-code-metrics", 156 | "version": "1.7", 157 | "title": "Visual Studio Code Metrics", 158 | "installStatus": "Success", 159 | "requiresRestart": "false" 160 | }, { 161 | "name": "vs-code-metrics", 162 | "version": "1.7", 163 | "title": "Visual Studio Code Metrics", 164 | "installStatus": "Success", 165 | "requiresRestart": "false" 166 | }, { 167 | "name": "vstestrunner", 168 | "version": "1.0.8", 169 | "title": "VSTest Runner", 170 | "installStatus": "Success", 171 | "requiresRestart": "false" 172 | }, { 173 | "name": "mstest", 174 | "version": "1.0.0", 175 | "title": "MSTest", 176 | "installStatus": "Success", 177 | "requiresRestart": "false" 178 | } 179 | ], 180 | "state": "RESTART" 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /tests/unit/tests_data/plugin/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "_class": "hudson.LocalPluginManager", 3 | "plugins": [ 4 | { 5 | "shortName": "jquery-detached" 6 | }, 7 | { 8 | "shortName": "bouncycastle-api" 9 | }, 10 | { 11 | "shortName": "htmlpublisher" 12 | }, 13 | { 14 | "shortName": "plain-credentials" 15 | }, 16 | { 17 | "shortName": "pam-auth" 18 | }, 19 | { 20 | "shortName": "resource-disposer" 21 | }, 22 | { 23 | "shortName": "timestamper" 24 | }, 25 | { 26 | "shortName": "docker-plugin" 27 | }, 28 | { 29 | "shortName": "pipeline-model-extensions" 30 | }, 31 | { 32 | "shortName": "cvs" 33 | }, 34 | { 35 | "shortName": "pipeline-build-step" 36 | }, 37 | { 38 | "shortName": "pipeline-model-api" 39 | }, 40 | { 41 | "shortName": "workflow-cps-global-lib" 42 | }, 43 | { 44 | "shortName": "workflow-job" 45 | }, 46 | { 47 | "shortName": "analysis-core" 48 | }, 49 | { 50 | "shortName": "ws-cleanup" 51 | }, 52 | { 53 | "shortName": "branch-api" 54 | }, 55 | { 56 | "shortName": "maven-plugin" 57 | }, 58 | { 59 | "shortName": "matrix-auth" 60 | }, 61 | { 62 | "shortName": "git-server" 63 | }, 64 | { 65 | "shortName": "github-api" 66 | }, 67 | { 68 | "shortName": "antisamy-markup-formatter" 69 | }, 70 | { 71 | "shortName": "ownership" 72 | }, 73 | { 74 | "shortName": "build-view-column" 75 | }, 76 | { 77 | "shortName": "command-launcher" 78 | }, 79 | { 80 | "shortName": "handlebars" 81 | }, 82 | { 83 | "shortName": "git" 84 | }, 85 | { 86 | "shortName": "gradle" 87 | }, 88 | { 89 | "shortName": "jsch" 90 | }, 91 | { 92 | "shortName": "workflow-scm-step" 93 | }, 94 | { 95 | "shortName": "vstestrunner" 96 | }, 97 | { 98 | "shortName": "apache-httpcomponents-client-4-api" 99 | }, 100 | { 101 | "shortName": "pipeline-input-step" 102 | }, 103 | { 104 | "shortName": "pipeline-multibranch-defaults" 105 | }, 106 | { 107 | "shortName": "jdk-tool" 108 | }, 109 | { 110 | "shortName": "external-monitor-job" 111 | }, 112 | { 113 | "shortName": "mailer" 114 | }, 115 | { 116 | "shortName": "ant" 117 | }, 118 | { 119 | "shortName": "nant" 120 | }, 121 | { 122 | "shortName": "workflow-api" 123 | }, 124 | { 125 | "shortName": "pipeline-model-declarative-agent" 126 | }, 127 | { 128 | "shortName": "github-organization-folder" 129 | }, 130 | { 131 | "shortName": "pipeline-github-lib" 132 | }, 133 | { 134 | "shortName": "token-macro" 135 | }, 136 | { 137 | "shortName": "git-client" 138 | }, 139 | { 140 | "shortName": "jackson2-api" 141 | }, 142 | { 143 | "shortName": "ssh-credentials" 144 | }, 145 | { 146 | "shortName": "cloudbees-folder" 147 | }, 148 | { 149 | "shortName": "job-restrictions" 150 | }, 151 | { 152 | "shortName": "gerrit-trigger" 153 | }, 154 | { 155 | "shortName": "build-timeout" 156 | }, 157 | { 158 | "shortName": "email-ext" 159 | }, 160 | { 161 | "shortName": "workflow-basic-steps" 162 | }, 163 | { 164 | "shortName": "mapdb-api" 165 | }, 166 | { 167 | "shortName": "mock-slave" 168 | }, 169 | { 170 | "shortName": "matrix-project" 171 | }, 172 | { 173 | "shortName": "durable-task" 174 | }, 175 | { 176 | "shortName": "subversion" 177 | }, 178 | { 179 | "shortName": "mstest" 180 | }, 181 | { 182 | "shortName": "javadoc" 183 | }, 184 | { 185 | "shortName": "pipeline-stage-step" 186 | }, 187 | { 188 | "shortName": "display-url-api" 189 | }, 190 | { 191 | "shortName": "junit" 192 | }, 193 | { 194 | "shortName": "cloud-stats" 195 | }, 196 | { 197 | "shortName": "pipeline-milestone-step" 198 | }, 199 | { 200 | "shortName": "synopsys-coverity" 201 | }, 202 | { 203 | "shortName": "workflow-aggregator" 204 | }, 205 | { 206 | "shortName": "ldap" 207 | }, 208 | { 209 | "shortName": "momentjs" 210 | }, 211 | { 212 | "shortName": "nodelabelparameter" 213 | }, 214 | { 215 | "shortName": "jquery" 216 | }, 217 | { 218 | "shortName": "authentication-tokens" 219 | }, 220 | { 221 | "shortName": "workflow-multibranch" 222 | }, 223 | { 224 | "shortName": "workflow-durable-task-step" 225 | }, 226 | { 227 | "shortName": "script-security" 228 | }, 229 | { 230 | "shortName": "ace-editor" 231 | }, 232 | { 233 | "shortName": "docker-java-api" 234 | }, 235 | { 236 | "shortName": "groovy-label-assignment" 237 | }, 238 | { 239 | "shortName": "github-branch-source" 240 | }, 241 | { 242 | "shortName": "pipeline-stage-tags-metadata" 243 | }, 244 | { 245 | "shortName": "workflow-cps" 246 | }, 247 | { 248 | "shortName": "config-file-provider" 249 | }, 250 | { 251 | "shortName": "pipeline-graph-analysis" 252 | }, 253 | { 254 | "shortName": "lockable-resources" 255 | }, 256 | { 257 | "shortName": "workflow-support" 258 | }, 259 | { 260 | "shortName": "vs-code-metrics" 261 | }, 262 | { 263 | "shortName": "pipeline-rest-api" 264 | }, 265 | { 266 | "shortName": "docker-commons" 267 | }, 268 | { 269 | "shortName": "ssh-slaves" 270 | }, 271 | { 272 | "shortName": "pipeline-model-definition" 273 | }, 274 | { 275 | "shortName": "github" 276 | }, 277 | { 278 | "shortName": "scm-api" 279 | }, 280 | { 281 | "shortName": "docker-workflow" 282 | }, 283 | { 284 | "shortName": "credentials" 285 | }, 286 | { 287 | "shortName": "windows-slaves" 288 | }, 289 | { 290 | "shortName": "pipeline-stage-view" 291 | }, 292 | { 293 | "shortName": "credentials-binding" 294 | }, 295 | { 296 | "shortName": "workflow-step-api" 297 | }, 298 | { 299 | "shortName": "structs" 300 | }, 301 | { 302 | "shortName": "openstack-cloud" 303 | }, 304 | { 305 | "shortName": "multi-slave-config-plugin" 306 | } 307 | ] 308 | } -------------------------------------------------------------------------------- /tests/unit/tests_data/queue/blockeditem.json: -------------------------------------------------------------------------------- 1 | { 2 | "_class": "hudson.model.Queue$BlockedItem", 3 | "actions": [{ 4 | "_class": "hudson.model.CauseAction", 5 | "causes": [{ 6 | "_class": "hudson.model.Cause$UserIdCause", 7 | "shortDescription": "Started by user joelee", 8 | "userId": "admin", 9 | "userName": "joelee" 10 | } 11 | ] 12 | } 13 | ], 14 | "blocked": true, 15 | "buildable": false, 16 | "id": 669, 17 | "inQueueSince": 1561364267921, 18 | "params": "", 19 | "stuck": false, 20 | "task": { 21 | "_class": "org.jenkinsci.plugins.workflow.job.WorkflowJob", 22 | "name": "folder/job/pipeline", 23 | "url": "http://0.0.0.0:8080/job/folder/job/pipeline/", 24 | "color": "red_anime" 25 | }, 26 | "url": "queue/item/669/", 27 | "why": "Build #168 is already in progress (ETA:N/A)", 28 | "buildableStartMilliseconds": 1561364277513 29 | } -------------------------------------------------------------------------------- /tests/unit/tests_data/queue/buildableitem.json: -------------------------------------------------------------------------------- 1 | { 2 | "_class": "hudson.model.Queue$BuildableItem", 3 | "actions": [], 4 | "blocked": false, 5 | "buildable": true, 6 | "id": 668, 7 | "inQueueSince": 1561364099799, 8 | "params": "", 9 | "stuck": true, 10 | "task": { 11 | "_class": "org.jenkinsci.plugins.workflow.support.steps.ExecutorStepExecution$PlaceholderTask" 12 | }, 13 | "url": "queue/item/668/", 14 | "why": "There are no nodes with the label ‘xxxx’", 15 | "buildableStartMilliseconds": 1561364099799, 16 | "pending": false 17 | } -------------------------------------------------------------------------------- /tests/unit/tests_data/queue/leftitem.json: -------------------------------------------------------------------------------- 1 | { 2 | "_class": "hudson.model.Queue$LeftItem", 3 | "actions": [{ 4 | "_class": "hudson.model.CauseAction", 5 | "causes": [{ 6 | "_class": "hudson.model.Cause$UserIdCause", 7 | "shortDescription": "Started by user joelee", 8 | "userId": "admin", 9 | "userName": "joelee" 10 | } 11 | ] 12 | } 13 | ], 14 | "blocked": false, 15 | "buildable": false, 16 | "id": 599, 17 | "inQueueSince": 1559112231788, 18 | "params": "", 19 | "stuck": false, 20 | "task": { 21 | "_class": "org.jenkinsci.plugins.workflow.job.WorkflowJob", 22 | "name": "folder/job/pipeline", 23 | "url": "http://0.0.0.0:8080/job/folder/job/pipeline/", 24 | "color": "aborted_anime" 25 | }, 26 | "url": "queue/item/599/", 27 | "why": null, 28 | "cancelled": false, 29 | "executable": { 30 | "_class": "org.jenkinsci.plugins.workflow.job.WorkflowRun", 31 | "number": 54, 32 | "url": "http://0.0.0.0:8080/job/folder/job/pipeline/54/" 33 | } 34 | } -------------------------------------------------------------------------------- /tests/unit/tests_data/queue/queue.json: -------------------------------------------------------------------------------- 1 | { 2 | "_class": "hudson.model.Queue", 3 | "discoverableItems": [], 4 | "items": [{ 5 | "_class": "hudson.model.Queue$BlockedItem", 6 | "actions": [{ 7 | "_class": "hudson.model.CauseAction", 8 | "causes": [{ 9 | "_class": "hudson.model.Cause$UserIdCause", 10 | "shortDescription": "Started by user joelee", 11 | "userId": "admin", 12 | "userName": "joelee" 13 | } 14 | ] 15 | } 16 | ], 17 | "blocked": true, 18 | "buildable": false, 19 | "id": 669, 20 | "inQueueSince": 1561364267921, 21 | "params": "", 22 | "stuck": false, 23 | "task": { 24 | "_class": "org.jenkinsci.plugins.workflow.job.WorkflowJob", 25 | "name": "folder/job/pipeline", 26 | "url": "http://0.0.0.0:8080/job/folder/job/pipeline/", 27 | "color": "red_anime" 28 | }, 29 | "url": "queue/item/669/", 30 | "why": "Build #168 is already in progress (ETA:N/A)", 31 | "buildableStartMilliseconds": 1561364277513 32 | }, { 33 | "_class": "hudson.model.Queue$BuildableItem", 34 | "actions": [], 35 | "blocked": false, 36 | "buildable": true, 37 | "id": 668, 38 | "inQueueSince": 1561364099799, 39 | "params": "", 40 | "stuck": true, 41 | "task": { 42 | "_class": "org.jenkinsci.plugins.workflow.support.steps.ExecutorStepExecution$PlaceholderTask" 43 | }, 44 | "url": "queue/item/668/", 45 | "why": "There are no nodes with the label ‘xxxx’", 46 | "buildableStartMilliseconds": 1561364099799, 47 | "pending": false 48 | } 49 | ] 50 | } -------------------------------------------------------------------------------- /tests/unit/tests_data/queue/waitingitem.json: -------------------------------------------------------------------------------- 1 | { 2 | "_class": "hudson.model.Queue$WaitingItem", 3 | "actions": [ 4 | { 5 | "_class": "hudson.model.CauseAction", 6 | "causes": [ 7 | { 8 | "_class": "com.sonyericsson.hudson.plugins.gerrit.trigger.hudsontrigger.GerritCause", 9 | "shortDescription": "Triggered by" 10 | } 11 | ] 12 | }, 13 | {}, 14 | {}, 15 | {}, 16 | { 17 | "_class": "hudson.model.ParametersAction", 18 | "parameters": [] 19 | } 20 | ], 21 | "blocked": false, 22 | "buildable": false, 23 | "id": 668, 24 | "inQueueSince": 1566553108796, 25 | "params": "", 26 | "stuck": false, 27 | "task": { 28 | "_class": "org.jenkinsci.plugins.workflow.job.WorkflowJob", 29 | "name": "folder/job/pipeline", 30 | "url": "http://0.0.0.0:8080/job/folder/job/pipeline/", 31 | "color": "red" 32 | }, 33 | "url": "queue/item/700/", 34 | "why": "In the quiet period. Expires in 4.6 sec", 35 | "timestamp": 1566553113796 36 | } -------------------------------------------------------------------------------- /tests/unit/tests_data/report/coverage_report.json: -------------------------------------------------------------------------------- 1 | { 2 | "_class": "hudson.plugins.jacoco.report.CoverageReport", 3 | "branchCoverage": { 4 | "covered": 244, 5 | "missed": 1588, 6 | "percentage": 13, 7 | "percentageFloat": 13.318777, 8 | "total": 1832 9 | }, 10 | "classCoverage": { 11 | "covered": 38, 12 | "missed": 21, 13 | "percentage": 64, 14 | "percentageFloat": 64.406784, 15 | "total": 59 16 | }, 17 | "complexityScore": { 18 | "covered": 398, 19 | "missed": 1218, 20 | "percentage": 25, 21 | "percentageFloat": 24.628712, 22 | "total": 1616 23 | }, 24 | "instructionCoverage": { 25 | "covered": 4205, 26 | "missed": 9220, 27 | "percentage": 31, 28 | "percentageFloat": 31.32216, 29 | "total": 13425 30 | }, 31 | "lineCoverage": { 32 | "covered": 712, 33 | "missed": 562, 34 | "percentage": 56, 35 | "percentageFloat": 55.88697, 36 | "total": 1274 37 | }, 38 | "methodCoverage": { 39 | "covered": 320, 40 | "missed": 380, 41 | "percentage": 46, 42 | "percentageFloat": 45.714287, 43 | "total": 700 44 | }, 45 | "previousResult": { 46 | "_class": "hudson.plugins.jacoco.report.CoverageReport" 47 | } 48 | } -------------------------------------------------------------------------------- /tests/unit/tests_data/report/coverage_report_trend.json: -------------------------------------------------------------------------------- 1 | { 2 | "_class": "hudson.plugins.jacoco.report.CoverageReport", 3 | "branchCoverage": { 4 | "covered": 244, 5 | "missed": 1588, 6 | "percentage": 13, 7 | "percentageFloat": 13.318777, 8 | "total": 1832 9 | }, 10 | "classCoverage": { 11 | "covered": 38, 12 | "missed": 21, 13 | "percentage": 64, 14 | "percentageFloat": 64.406784, 15 | "total": 59 16 | }, 17 | "complexityScore": { 18 | "covered": 398, 19 | "missed": 1218, 20 | "percentage": 25, 21 | "percentageFloat": 24.628712, 22 | "total": 1616 23 | }, 24 | "instructionCoverage": { 25 | "covered": 4205, 26 | "missed": 9220, 27 | "percentage": 31, 28 | "percentageFloat": 31.32216, 29 | "total": 13425 30 | }, 31 | "lineCoverage": { 32 | "covered": 712, 33 | "missed": 562, 34 | "percentage": 56, 35 | "percentageFloat": 55.88697, 36 | "total": 1274 37 | }, 38 | "methodCoverage": { 39 | "covered": 320, 40 | "missed": 380, 41 | "percentage": 46, 42 | "percentageFloat": 45.714287, 43 | "total": 700 44 | }, 45 | "previousResult": { 46 | "_class": "hudson.plugins.jacoco.report.CoverageReport", 47 | "branchCoverage": { 48 | "covered": 244, 49 | "missed": 1588, 50 | "percentage": 13, 51 | "percentageFloat": 13.318777, 52 | "total": 1832 53 | }, 54 | "classCoverage": { 55 | "covered": 38, 56 | "missed": 21, 57 | "percentage": 64, 58 | "percentageFloat": 64.406784, 59 | "total": 59 60 | }, 61 | "complexityScore": { 62 | "covered": 398, 63 | "missed": 1218, 64 | "percentage": 25, 65 | "percentageFloat": 24.628712, 66 | "total": 1616 67 | }, 68 | "instructionCoverage": { 69 | "covered": 4205, 70 | "missed": 9220, 71 | "percentage": 31, 72 | "percentageFloat": 31.32216, 73 | "total": 13425 74 | }, 75 | "lineCoverage": { 76 | "covered": 712, 77 | "missed": 562, 78 | "percentage": 56, 79 | "percentageFloat": 55.88697, 80 | "total": 1274 81 | }, 82 | "methodCoverage": { 83 | "covered": 320, 84 | "missed": 380, 85 | "percentage": 46, 86 | "percentageFloat": 45.714287, 87 | "total": 700 88 | }, 89 | "previousResult": { 90 | "_class": "hudson.plugins.jacoco.report.CoverageReport", 91 | "branchCoverage": { 92 | "covered": 0, 93 | "missed": 1820, 94 | "percentage": 0, 95 | "percentageFloat": 0, 96 | "total": 1820 97 | }, 98 | "classCoverage": { 99 | "covered": 0, 100 | "missed": 59, 101 | "percentage": 0, 102 | "percentageFloat": 0, 103 | "total": 59 104 | }, 105 | "complexityScore": { 106 | "covered": 0, 107 | "missed": 1605, 108 | "percentage": 0, 109 | "percentageFloat": 0, 110 | "total": 1605 111 | }, 112 | "instructionCoverage": { 113 | "covered": 0, 114 | "missed": 13339, 115 | "percentage": 0, 116 | "percentageFloat": 0, 117 | "total": 13339 118 | }, 119 | "lineCoverage": { 120 | "covered": 0, 121 | "missed": 1256, 122 | "percentage": 0, 123 | "percentageFloat": 0, 124 | "total": 1256 125 | }, 126 | "methodCoverage": { 127 | "covered": 0, 128 | "missed": 695, 129 | "percentage": 0, 130 | "percentageFloat": 0, 131 | "total": 695 132 | }, 133 | "previousResult": null 134 | } 135 | } 136 | } -------------------------------------------------------------------------------- /tests/unit/tests_data/report/coverage_result.json: -------------------------------------------------------------------------------- 1 | { 2 | "_class": "io.jenkins.plugins.coverage.targets.CoverageResult", 3 | "results": { 4 | "children": [ 5 | {} 6 | ], 7 | "elements": [ 8 | { 9 | "denominator": 1, 10 | "name": "Report", 11 | "numerator": 1, 12 | "ratio": 100 13 | }, 14 | { 15 | "denominator": 1, 16 | "name": "Directory", 17 | "numerator": 1, 18 | "ratio": 100 19 | }, 20 | { 21 | "denominator": 19, 22 | "name": "File", 23 | "numerator": 18, 24 | "ratio": 94.73684 25 | }, 26 | { 27 | "denominator": 866, 28 | "name": "Line", 29 | "numerator": 726, 30 | "ratio": 83.83372 31 | } 32 | ], 33 | "name": "All reports" 34 | } 35 | } -------------------------------------------------------------------------------- /tests/unit/tests_data/report/coverage_trends.json: -------------------------------------------------------------------------------- 1 | { 2 | "_class": "io.jenkins.plugins.coverage.targets.CoverageTrendTree", 3 | "children": [ 4 | { 5 | "children": [ 6 | {} 7 | ], 8 | "name": "istanbul: cov.xml", 9 | "trends": [ 10 | {}, 11 | {} 12 | ] 13 | } 14 | ], 15 | "name": "All reports", 16 | "trends": [ 17 | { 18 | "buildName": "#5", 19 | "elements": [ 20 | { 21 | "denominator": 1, 22 | "name": "Report", 23 | "numerator": 1, 24 | "ratio": 100 25 | }, 26 | { 27 | "denominator": 1, 28 | "name": "Directory", 29 | "numerator": 1, 30 | "ratio": 100 31 | }, 32 | { 33 | "denominator": 19, 34 | "name": "File", 35 | "numerator": 18, 36 | "ratio": 94.73684 37 | }, 38 | { 39 | "denominator": 866, 40 | "name": "Line", 41 | "numerator": 726, 42 | "ratio": 83.83372 43 | } 44 | ] 45 | }, 46 | { 47 | "buildName": "#3", 48 | "elements": [ 49 | { 50 | "denominator": 1, 51 | "name": "Report", 52 | "numerator": 1, 53 | "ratio": 100 54 | }, 55 | { 56 | "denominator": 1, 57 | "name": "Directory", 58 | "numerator": 1, 59 | "ratio": 100 60 | }, 61 | { 62 | "denominator": 19, 63 | "name": "File", 64 | "numerator": 18, 65 | "ratio": 94.73684 66 | }, 67 | { 68 | "denominator": 866, 69 | "name": "Line", 70 | "numerator": 726, 71 | "ratio": 83.83372 72 | } 73 | ] 74 | } 75 | ] 76 | } -------------------------------------------------------------------------------- /tests/unit/tests_data/report/test_report.json: -------------------------------------------------------------------------------- 1 | { 2 | "_class": "hudson.tasks.junit.TestResult", 3 | "testActions": [], 4 | "duration": 74.678, 5 | "empty": false, 6 | "failCount": 2, 7 | "passCount": 37, 8 | "skipCount": 0, 9 | "suites": [ 10 | { 11 | "cases": [ 12 | { 13 | "testActions": [], 14 | "age": 0, 15 | "className": "tests.integration.test_jenkins.TestJenkins", 16 | "duration": 0.645, 17 | "errorDetails": null, 18 | "errorStackTrace": null, 19 | "failedSince": 0, 20 | "name": "test_exists", 21 | "skipped": false, 22 | "skippedMessage": null, 23 | "status": "PASSED", 24 | "stderr": null, 25 | "stdout": null 26 | } 27 | ], 28 | "duration": 74.678, 29 | "enclosingBlockNames": [], 30 | "enclosingBlocks": [], 31 | "id": null, 32 | "name": "pytest", 33 | "nodeId": null, 34 | "stderr": null, 35 | "stdout": null, 36 | "timestamp": "2021-02-22T17:33:40.266004" 37 | } 38 | ] 39 | } -------------------------------------------------------------------------------- /tests/unit/tests_data/run/freestylebuild.json: -------------------------------------------------------------------------------- 1 | { 2 | "_class": "hudson.model.FreeStyleBuild", 3 | "actions": [ 4 | { 5 | "_class": "hudson.model.ParametersAction", 6 | "parameters": [ 7 | { 8 | "_class": "hudson.model.StringParameterValue", 9 | "name": "arg", 10 | "value": "value1" 11 | } 12 | ] 13 | }, 14 | { 15 | "_class": "hudson.model.CauseAction", 16 | "causes": [ 17 | { 18 | "_class": "hudson.model.Cause$UserIdCause", 19 | "shortDescription": "Started by user admin", 20 | "userId": "admin", 21 | "userName": "admin" 22 | } 23 | ] 24 | }, 25 | {}, 26 | {}, 27 | {} 28 | ], 29 | "artifacts": [], 30 | "building": false, 31 | "description": null, 32 | "displayName": "#14", 33 | "duration": 599334, 34 | "estimatedDuration": 599335, 35 | "executor": null, 36 | "fullDisplayName": "freestylejob #14", 37 | "id": "14", 38 | "keepLog": false, 39 | "number": 14, 40 | "queueId": 151, 41 | "result": "SUCCESS", 42 | "timestamp": 1619167515987, 43 | "url": "http://0.0.0.0:8080/job/freestylejob/14/", 44 | "builtOn": "", 45 | "changeSet": { 46 | "_class": "hudson.scm.EmptyChangeLogSet", 47 | "items": [], 48 | "kind": null 49 | }, 50 | "culprits": [] 51 | } -------------------------------------------------------------------------------- /tests/unit/tests_data/run/workflowrun.json: -------------------------------------------------------------------------------- 1 | { 2 | "_class": "org.jenkinsci.plugins.workflow.job.WorkflowRun", 3 | "actions": [ 4 | { 5 | "_class": "hudson.model.CauseAction", 6 | "causes": [ 7 | { 8 | "_class": "hudson.model.Cause$UserIdCause", 9 | "shortDescription": "Started by user admin", 10 | "userId": "admin", 11 | "userName": "admin" 12 | }, 13 | { 14 | "_class": "org.jenkinsci.plugins.workflow.cps.replay.ReplayCause", 15 | "shortDescription": "Replayed #1" 16 | } 17 | ] 18 | }, 19 | { 20 | "_class": "hudson.model.ParametersAction", 21 | "parameters": [ 22 | { 23 | "_class": "hudson.model.StringParameterValue", 24 | "name": "parameter1", 25 | "value": "value1" 26 | }, 27 | { 28 | "_class": "hudson.model.StringParameterValue", 29 | "name": "parameter2", 30 | "value": "value2" 31 | } 32 | ] 33 | } 34 | ], 35 | "artifacts": [], 36 | "building": false, 37 | "description": null, 38 | "displayName": "#52", 39 | "duration": 324447, 40 | "estimatedDuration": 197743, 41 | "executor": null, 42 | "fullDisplayName": "folder/job/pipeline #52", 43 | "id": "52", 44 | "keepLog": false, 45 | "number": 52, 46 | "queueId": 668, 47 | "result": "FAILURE", 48 | "timestamp": 1559281453240, 49 | "url": "http://0.0.0.0:8080/job/folder/job/pipeline/52/", 50 | "changeSets": [], 51 | "culprits": [], 52 | "nextBuild": null, 53 | "previousBuild": { 54 | "number": 51, 55 | "url": "http://0.0.0.0:8080/job/folder/job/pipeline/51/" 56 | } 57 | } -------------------------------------------------------------------------------- /tests/unit/tests_data/view/allview.json: -------------------------------------------------------------------------------- 1 | { 2 | "_class": "hudson.model.AllView", 3 | "description": null, 4 | "jobs": [ 5 | { 6 | "_class": "org.jenkinsci.plugins.workflow.job.WorkflowJob", 7 | "name": "folder/job/pipeline", 8 | "url": "http://0.0.0.0:8080/job/folder/job/pipeline/", 9 | "color": "red" 10 | }, 11 | { 12 | "_class": "com.cloudbees.hudson.plugins.folder.Folder", 13 | "name": "folder", 14 | "url": "http://0.0.0.0:8080/job/folder/" 15 | } 16 | ], 17 | "name": "all", 18 | "property": [], 19 | "url": "http://http://0.0.0.0:8080/" 20 | } -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | skip_missing_interpreters = True 3 | envlist = style, pylint, py3 4 | 5 | 6 | [testenv] 7 | deps = respx 8 | pytest-cov 9 | pytest-asyncio 10 | commands = 11 | pytest -v --cov=api4jenkins tests/unit \ 12 | -o junit_family=xunit2 \ 13 | --asyncio-mode=auto \ 14 | --cov-report term \ 15 | --cov-report html:tests/unit/htmlcov \ 16 | --cov-report xml:tests/unit/coverage.xml 17 | 18 | [testenv:pylint] 19 | deps = pylint 20 | commands = 21 | pylint -E api4jenkins 22 | 23 | [testenv:style] 24 | deps = pycodestyle 25 | commands = 26 | pycodestyle --show-source --show-pep8 \ 27 | --ignore=E501,F401,E128,E402,E731,F821 api4jenkins 28 | 29 | [testenv:integration] 30 | deps = pytest-cov 31 | pytest-asyncio 32 | pyyaml 33 | passenv = JENKINS_* 34 | commands = 35 | pytest -v --cov=api4jenkins tests/integration \ 36 | --asyncio-mode=auto \ 37 | --cov-report term \ 38 | --cov-report html:tests/integration/htmlcov \ 39 | --cov-report xml:tests/integration/coverage.xml 40 | --------------------------------------------------------------------------------