├── tests ├── __init__.py └── test_cli.py ├── jenkins_cli ├── version.py ├── __init__.py ├── cli_arguments.py └── cli.py ├── MANIFEST.in ├── jenkins-cli.png ├── requirements.txt ├── .travis.yml ├── tox.ini ├── .gitignore ├── README.rst ├── LICENSE ├── contrib └── bash-completion │ └── jenkins ├── setup.py └── README.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /jenkins_cli/version.py: -------------------------------------------------------------------------------- 1 | version = '0.2.0' 2 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md LICENSE README.rst LICENSE 2 | recursive-include contrib * 3 | -------------------------------------------------------------------------------- /jenkins-cli.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LD250/jenkins-cli-python/HEAD/jenkins-cli.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | python-jenkins==0.4.14 2 | tox==2.3.1 3 | pyfakefs==2.7.0 4 | mock==2.0.0 5 | unittest2==1.1.0 6 | flake8==2.5.4 7 | pyxdg==0.25 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: "python" 2 | python: 3 | - "2.7" 4 | - "3.4" 5 | - "3.5" 6 | - "3.6" 7 | install: 8 | - pip install flake8 mock==2.0.0 9 | script: 10 | - flake8 11 | - python setup.py test 12 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # Tox (http://tox.testrun.org/) is a tool for running tests 2 | # in multiple virtualenvs. This configuration file will run the 3 | # test suite on all supported python versions. To use it, "pip install tox" 4 | # and then run "tox" from this directory. 5 | 6 | [tox] 7 | envlist = py27, py35 8 | skipsdist = True 9 | 10 | [testenv] 11 | commands = 12 | flake8 13 | {envpython} setup.py test 14 | deps = 15 | flake8 16 | [flake8] 17 | ignore=E501, E121, E123, E126, E133, E226, E241, E242, E704 18 | exclude = .venv,.tox,dist,doc,build,*.egg 19 | 20 | -------------------------------------------------------------------------------- /jenkins_cli/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | from jenkins import JenkinsException 4 | 5 | from jenkins_cli.cli import JenkinsCli, CliException 6 | from jenkins_cli.cli_arguments import load_parser 7 | 8 | 9 | def main(): 10 | parser = load_parser() 11 | args = parser.parse_args() 12 | 13 | try: 14 | if args.jenkins_command is None: 15 | parser.print_help() 16 | else: 17 | JenkinsCli(args).run_command(args) 18 | except JenkinsException as e: 19 | print("Jenkins server response: %s:" % e) 20 | except KeyboardInterrupt: 21 | print("Aborted") 22 | except CliException as e: 23 | print(e) 24 | print("Read jenkins --help") 25 | 26 | 27 | if __name__ == "__main__": 28 | main() 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ 58 | 59 | # Program settings 60 | .jenkins-cli 61 | .jenkins-cli.old 62 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Jenkins command line interface 2 | ============================== 3 | **Based on** 4 | 5 | python-jenkins: https://github.com/openstack/python-jenkins 6 | 7 | **Tested on** 8 | 9 | Jenkins ver: 1.565, 1.655 10 | 11 | Python ver: 2.7, 3.4, 3.5 12 | 13 | Documentation and examples: 14 | --------------------------- 15 | https://github.com/LD250/jenkins-cli-python 16 | 17 | 18 | Installation: 19 | ------------------ 20 | Please check the documentation for installation options 21 | 22 | 23 | Commands overview: 24 | ------------------ 25 | :jobs: Show all jobs and their statuses 26 | :queue: Show builds queue 27 | :building: Build executor status 28 | :start: Start job 29 | :info: Job info 30 | :setbranch: Set VCS branch (Mercurial or Git) 31 | :stop: Stop job 32 | :configxml: Get the xml of job configuration 33 | :console: Show console for the build 34 | :builds: Show builds for the job 35 | :changes: Show build's changes 36 | 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /contrib/bash-completion/jenkins: -------------------------------------------------------------------------------- 1 | # jenkins(1) completion -*- shell-script -*- 2 | 3 | _jenkins() 4 | { 5 | local cur prev words cword 6 | _init_completion || return 7 | 8 | opts="jobs \ 9 | queue \ 10 | building \ 11 | builds \ 12 | start \ 13 | info \ 14 | configxml \ 15 | setbranch \ 16 | stop \ 17 | console \ 18 | changes \ 19 | -h \ 20 | --help \ 21 | --host \ 22 | --username \ 23 | --password \ 24 | --version" 25 | 26 | case $prev in 27 | jobs) 28 | opts="-h --help -a -p" 29 | ;; 30 | builds|start|info|configxml|setbranch|stop|console|changes) 31 | opts="-h --help" 32 | 33 | # if the cached-jobs file exists suggest also job names 34 | CACHE_DIR=${XDG_CACHE_HOME:-~/.cache}"/python-jenkins-cli" 35 | if [ -r $CACHE_DIR/job_cache ]; then 36 | opts="$opts $(cat $CACHE_DIR/job_cache)" 37 | fi 38 | ;; 39 | queue|building) 40 | opts="-h --help" 41 | ;; 42 | esac 43 | 44 | COMPREPLY=($(compgen -W "${opts}" -- ${cur})) 45 | return 0 46 | } 47 | 48 | complete -F _jenkins jenkins 49 | 50 | # ex: ts=4 sw=4 et filetype=sh 51 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import ctypes 2 | import os 3 | import sys 4 | import re 5 | from setuptools import setup, find_packages 6 | 7 | 8 | try: 9 | is_root_user = os.geteuid() == 0 10 | except AttributeError: 11 | is_root_user = sys.platform == 'win32' and ctypes.windll.shell32.IsUserAnAdmin() != 0 12 | 13 | 14 | here = os.path.abspath(os.path.dirname(__file__)) 15 | with open(os.path.join(here, 'README.rst')) as f: 16 | README = f.read() 17 | 18 | version_file_content = open(os.path.join(here, 'jenkins_cli/version.py')).read() 19 | version_match = re.search(r"^version = ['\"]([^'\"]*)['\"]", 20 | version_file_content, re.M) 21 | if version_match: 22 | version = version_match.group(1) 23 | else: 24 | raise RuntimeError('Unable to find version string.') 25 | 26 | requires = ['python-jenkins==0.4.14', 27 | 'six>=1.9.0', 28 | 'pyxdg>=0.25'] 29 | 30 | tests_require = ['unittest2==1.1.0', 31 | 'mock==2.0.0', 32 | 'pyfakefs==2.7.0'] 33 | 34 | data_files = [] 35 | completion_dirs = ['/usr/share/bash-completion/completions', 36 | '/usr/local/opt/bash-completion/etc/bash_completion.d'] 37 | 38 | if is_root_user: 39 | for d in completion_dirs: 40 | if os.path.isdir(d): 41 | data_files.append((d, ['contrib/bash-completion/jenkins'])) 42 | else: 43 | print("Non-root user detected. Bash completion won't be installed.") 44 | 45 | setup( 46 | name='jenkins-cli', 47 | version=version, 48 | description='Commandline interface for Jenkins', 49 | long_description=README, 50 | author='Denys Levchenko', 51 | author_email='denis.levtchenko@gmail.com', 52 | url='https://github.com/LD250/jenkins-cli-python', 53 | keywords='jenkins, commandline, cli', 54 | license='http://opensource.org/licenses/MIT', 55 | classifiers=( 56 | 'Natural Language :: English', 57 | 'Environment :: Console', 58 | 'Intended Audience :: Developers', 59 | 'Programming Language :: Python', 60 | 'Programming Language :: Python :: 2.7', 61 | 'Programming Language :: Python :: 3.4', 62 | 'Programming Language :: Python :: 3.5', 63 | 'License :: OSI Approved :: MIT License', 64 | ), 65 | packages=find_packages(), 66 | install_requires=requires, 67 | tests_require=tests_require, 68 | test_suite="tests", 69 | data_files=data_files, 70 | entry_points={ 71 | 'console_scripts': ['jenkins = jenkins_cli:main'] 72 | } 73 | ) 74 | -------------------------------------------------------------------------------- /jenkins_cli/cli_arguments.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | 3 | from jenkins_cli.cli import get_jobs_legend 4 | from jenkins_cli.version import version 5 | 6 | 7 | def load_parser(): 8 | """ 9 | Create a parser and load it with CLI arguments 10 | 11 | Returns: ArgumentParser instance 12 | """ 13 | parser = argparse.ArgumentParser(prog='jenkins', 14 | description='Host, username and password may be specified either by the command line arguments ' 15 | 'or in the configuration file (.jenkins-cli). Command line arguments have the highest priority, ' 16 | 'after that the .jenkins-cli file from current folder is used. If there is no' 17 | '.jenkins-cli file in the current folder, settings will be read from .jenkins-cli located in the home' 18 | 'folder') 19 | parser.add_argument('--host', metavar='jenkins-url', help='Jenkins Host', default=None) 20 | parser.add_argument('--username', metavar='username', help='Jenkins Username', default=None) 21 | parser.add_argument('--password', metavar='password', help='Jenkins Password', default=None) 22 | parser.add_argument('--version', '-v', action='version', version='jenkins-cli %s' % version) 23 | parser.add_argument('-e', '--environment', 24 | help='Which config section to use') 25 | 26 | subparsers = parser.add_subparsers(title='Available commands', dest='jenkins_command') 27 | 28 | jobs_parser = subparsers.add_parser('jobs', 29 | help='Show all jobs and their statuses', 30 | formatter_class=argparse.RawTextHelpFormatter, 31 | description="Status description:\n\n" + "\n".join(get_jobs_legend())) 32 | jobs_parser.add_argument('-a', help='show only active jobs', default=False, action='store_true') 33 | jobs_parser.add_argument('-p', help='show only jobs in build progress', default=False, action='store_true') 34 | 35 | subparsers.add_parser('queue', help='Show builds queue') 36 | 37 | subparsers.add_parser('building', help='Build executor status') 38 | 39 | builds_parser = subparsers.add_parser('builds', help='Show builds for the job') 40 | builds_parser.add_argument('job_name', help='Job name of the builds') 41 | 42 | start_parser = subparsers.add_parser('start', help='Start job') 43 | start_parser.add_argument('job_name', help='Job to start', nargs='*') 44 | 45 | start_parser = subparsers.add_parser('info', help='Job info') 46 | start_parser.add_argument('job_name', help='Job to get info for') 47 | 48 | start_parser = subparsers.add_parser('configxml', help='Job config in xml format') 49 | start_parser.add_argument('job_name', help='Job to get config for') 50 | 51 | set_branch = subparsers.add_parser('setbranch', help='Set VCS branch (Mercurial or Git)') 52 | set_branch.add_argument('job_name', help='Job to set branch for') 53 | set_branch.add_argument('branch_name', help='Name of the VCS branch') 54 | 55 | stop_parser = subparsers.add_parser('stop', help='Stop job') 56 | stop_parser.add_argument('job_name', help='Job to stop') 57 | 58 | wait_parser = subparsers.add_parser('wait', 59 | help='Wait for the next building job') 60 | wait_parser.add_argument('job_name', help='Job to wait') 61 | wait_parser.add_argument('-t', '--interval', 62 | help='refresh interval in seconds', default=3, 63 | type=check_nonnegative) 64 | 65 | console_parser = subparsers.add_parser('console', help='Show console for the build') 66 | console_parser.add_argument('job_name', help='Job to show console for') 67 | console_parser.add_argument('-b', '--build', help='job build number to show console for (if omitted, last build number is used)', default='') 68 | console_parser.add_argument('-n', help='show first n lines only(if n is negative, show last n lines)', type=int) 69 | console_parser.add_argument('-i', help='interactive console', default=False, action='store_true') 70 | console_parser.add_argument('-t', '--interval', help='refresh interval in seconds (in case of interactive console -i)', default=3, type=check_nonnegative) 71 | 72 | changes_parser = subparsers.add_parser('changes', help="Show build's changes") 73 | changes_parser.add_argument('job_name', help='Job to show changes for') 74 | changes_parser.add_argument('-b', '--build', help='job build number to show changes for (if omitted, last build number is used)', default='') 75 | 76 | return parser 77 | 78 | 79 | def check_nonnegative(value): 80 | """ 81 | Checks if (possibly string) value is non-negative integer and returns it. 82 | 83 | Raise: 84 | ArgumentTypeError: if value is not a non-negative integer 85 | """ 86 | try: 87 | ivalue = int(value) 88 | if ivalue < 0: 89 | raise ValueError() 90 | except: 91 | raise argparse.ArgumentTypeError("Value must be a non-negative integer: %s" % value) 92 | return ivalue 93 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jenkins command line interface 2 | [![PyPI version](https://badge.fury.io/py/jenkins-cli.svg)](https://badge.fury.io/py/jenkins-cli) 3 | [![Build Status](https://travis-ci.org/LD250/jenkins-cli-python.svg?branch=master)](https://travis-ci.org/LD250/jenkins-cli-python) 4 | [![Code Health](https://landscape.io/github/LD250/jenkins-cli-python/master/landscape.svg?style=flat)](https://landscape.io/github/LD250/jenkins-cli-python/master) 5 | [![Requirements Status](https://requires.io/github/LD250/jenkins-cli-python/requirements.svg?branch=master)](https://requires.io/github/LD250/jenkins-cli-python/requirements/?branch=master) 6 | 7 | **Based on** 8 | [python-jenkins](https://github.com/openstack/python-jenkins) 9 | 10 | # Tested on 11 | Jenkins ver: 1.565, 1.655 12 | 13 | Python ver: 2.7, 3.4, 3.5 14 | 15 | # Table of contents 16 | * [Installation](#installation) 17 | * [Commands overview](#commands-overview) 18 | * [Usage example](#usage-example) 19 | * [Tests](#tests) 20 | 21 | # Installation: 22 | There are more ways to install jenkins-cli-python: 23 | 24 | 1. The easiest way is to install the package using **pip**: 25 | 26 | ```bash 27 | pip install jenkins-cli 28 | ``` 29 | if you want autocompletions for commands and job names, you need to install package with root privileges 30 | 31 | ```bash 32 | sudo pip install jenkins-cli 33 | ``` 34 | 35 | 2. If you want the lastest features, you can install the package directly from the github **repo**: 36 | 37 | ```bash 38 | git clone https://github.com/LD250/jenkins-cli-python.git 39 | cd jenkins-cli-python 40 | python setup.py install 41 | ``` 42 | 43 | 3. jenkins-cli-python has also been packaged for **Fedora 25** as a copr repo: 44 | 45 | ```bash 46 | dnf copr enable radomirbosak/python-jenkins-cli 47 | dnf install python3-jenkins-cli 48 | ``` 49 | 50 | ## Configuration file (.jenkins-cli) 51 | 52 | Host, username and password may be specified either by the command line arguments or in the configuration file **(.jenkins-cli)**. Command line arguments have the highest priority, after that the **.jenkins-cli** file from current folder is used. If there is no.jenkins-cli file in the current folder, settings will be read from **.jenkins-cli** located in the home folder 53 | 54 | **.jenkins-cli** example 55 | ```txt 56 | [DEFAULT] 57 | host=http://localhost:8082/ 58 | username=username 59 | password=****** 60 | 61 | [prod] 62 | host=https://production-jenkins.example.com/ 63 | username=username 64 | password=xxxxxx 65 | ``` 66 | 67 | # Commands overview: 68 | jobs Show all jobs and their statuses 69 | queue Show builds queue 70 | building Build executor status 71 | start Start job 72 | info Job info 73 | setbranch Set VCS branch (Mercurial or Git) 74 | configxml Get the xml of job configuration 75 | stop Stop job 76 | console Show console for the build 77 | builds Show builds for the job 78 | changes Show build's changes 79 | Run `jenkins --help` for detailed help. To view optional parameters, run `--help` for the specific command. For example `jenkins jobs --help` will show job status description and optional arguments. 80 | 81 | 82 | # Usage example: 83 | 84 | Show status descriptions 85 | ```bash 86 | $ jenkins jobs --help 87 | usage: jenkins jobs [-h] [-a] [-p] 88 | 89 | Status description: 90 | 91 | ... -> Unknown 92 | F.. -> Failed 93 | D.. -> Disabled 94 | U.. -> Unstable 95 | N.. -> Not built 96 | S.. -> Stable 97 | A.. -> Aborted 98 | .>> -> Build in progress 99 | 100 | optional arguments: 101 | -h, --help show this help message and exit 102 | -a show only active jobs 103 | -p show only jobs in build progress 104 | ``` 105 | Show jobs 106 | ```bash 107 | $ jenkins jobs 108 | D.. hudson 109 | S.. jenkins-cli 110 | U>> new-project 111 | ``` 112 | Show job info 113 | ```bash 114 | $ jenkins info jenkins-cli 115 | Last build name: jenkins-cli #18 (result: SUCCESS) 116 | Last success build name: jenkins-cli #18 117 | Build started: 2016-03-31 00:22:38.326999 118 | Building now: No 119 | Git branch set to: master 120 | ``` 121 | Update VCS branch 122 | ```bash 123 | $ jenkins setbranch new-feature 124 | Done 125 | ``` 126 | Run job 127 | ```bash 128 | $ jenkins start jenkins-cli 129 | jenkins-cli: started 130 | ``` 131 | View job builds 132 | ```bash 133 | $ jenkins builds jenkins-cli 134 | S.. #18 0:00:07 (2 commits) 135 | S.. #17 0:00:08 (2 commits) 136 | F.. #16 0:00:00 (4 commits) 137 | S.. #15 0:00:08 (2 commits) 138 | ``` 139 | Show previous build changes 140 | ```bash 141 | $ jenkins changes jenkins-cli -b 17 142 | 1. add .travis.yml from add-travis branch by Denys Levchenko affected 1 files 143 | 2. scaffolding for tests by Denys Levchenko affected 5 files 144 | ``` 145 | Show current job console output (last 13 lines) 146 | ```bash 147 | $ jenkins console jenkins-cli -n 13 148 | test_jobs (tests.test_cli.TestCliCommands) ... ok 149 | test_queue (tests.test_cli.TestCliCommands) ... ok 150 | test_set_branch (tests.test_cli.TestCliCommands) ... ok 151 | test_start (tests.test_cli.TestCliCommands) ... ok 152 | test_stop (tests.test_cli.TestCliCommands) ... ok 153 | test_read_settings_from_file (tests.test_cli.TestCliFileUsing) ... ok 154 | 155 | ---------------------------------------------------------------------- 156 | Ran 12 tests in 0.015s 157 | 158 | OK 159 | Finished: SUCCESS 160 | ``` 161 | 162 | # Tests 163 | 164 | To perform flake8 checks and run tests similar to Travis, do the following 165 | 166 | ```bash 167 | pip install -r requirements.txt 168 | tox 169 | ``` 170 | -------------------------------------------------------------------------------- /jenkins_cli/cli.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import os 3 | import sys 4 | import datetime 5 | from time import time, sleep 6 | import jenkins 7 | import socket 8 | from xml.etree import ElementTree 9 | from xdg.BaseDirectory import save_cache_path 10 | 11 | try: 12 | from ConfigParser import ConfigParser, NoSectionError 13 | except ImportError: 14 | from configparser import ConfigParser, NoSectionError 15 | 16 | 17 | STATUSES_COLOR = {'blue': {'symbol': 'S', 18 | 'color': '\033[94m', 19 | 'descr': 'Stable'}, 20 | 'red': {'symbol': 'F', 21 | 'color': '\033[91m', 22 | 'descr': 'Failed'}, 23 | 'yellow': {'symbol': 'U', 24 | 'color': '\033[93m', 25 | 'descr': 'Unstable'}, 26 | 'disabled': {'symbol': 'D', 27 | 'color': '\033[97m', 28 | 'descr': 'Disabled'}, 29 | 'notbuilt': {'symbol': 'N', 30 | 'color': '\033[97m', 31 | 'descr': 'Not built'}, 32 | 'unknown': {'symbol': '.', 33 | 'color': '\033[97m', 34 | 'descr': 'Unknown'}, 35 | 'aborted': {'symbol': 'A', 36 | 'color': '\033[97m', 37 | 'descr': 'Aborted'} 38 | } 39 | 40 | 41 | ENDCOLLOR = '\033[0m' 42 | ANIME_SYMBOL = ['..', '>>'] 43 | AUTHOR_COLLOR = '\033[94m' 44 | MSG_COLLOR = '\033[93m' 45 | 46 | RESULT_TO_COLOR = {"FAILURE": 'red', 47 | "SUCCESS": 'blue', 48 | "UNSTABLE": 'yellow', 49 | "ABORTED": 'aborted', 50 | "DISABLED": 'aborted' 51 | } 52 | 53 | 54 | def get_formated_status(job_color, format_pattern="%(color)s%(symbol)s%(run_status)s%(endcollor)s", extra_params=None): 55 | if not extra_params: 56 | extra_params = {} 57 | color_status = job_color.split('_') 58 | color = color_status[0] 59 | run_status = color_status[1] if len(color_status) == 2 else None 60 | status = STATUSES_COLOR[color] 61 | params = {'color': status['color'], 62 | 'symbol': status['symbol'], 63 | 'descr': status['descr'], 64 | 'run_status': ANIME_SYMBOL[run_status == 'anime'], 65 | 'endcollor': ENDCOLLOR} 66 | params.update(extra_params) 67 | return format_pattern % params 68 | 69 | 70 | def get_jobs_legend(): 71 | pattern = "%(color)s%(symbol)s..%(endcollor)s -> %(descr)s" 72 | legend = [get_formated_status(job_color, pattern) for job_color in STATUSES_COLOR.keys()] 73 | legend.append(".>> -> Build in progress") 74 | return legend 75 | 76 | 77 | def xml_to_string(root): 78 | return ElementTree.tostring(root, encoding=('unicode' if sys.version_info[0] == 3 else None)) 79 | 80 | 81 | class CliException(Exception): 82 | pass 83 | 84 | 85 | class JenkinsCli(object): 86 | SETTINGS_FILE_NAME = '.jenkins-cli' 87 | JOB_CACHE_FILE_NAME = 'job_cache' 88 | 89 | QUEUE_EMPTY_TEXT = "Building queue is empty" 90 | 91 | INFO_TEMPLATE = ("Last build name: %s (result: %s)\n" 92 | "Last successful build name: %s\n" 93 | "Build started: %s\n" 94 | "Building now: %s\n" 95 | "%s branch set to: %s") 96 | 97 | def __init__(self, args, timeout=socket._GLOBAL_DEFAULT_TIMEOUT): 98 | self.jenkins = self.auth(args.host, args.username, args.password, 99 | args.environment, timeout) 100 | 101 | @classmethod 102 | def auth(cls, host=None, username=None, password=None, environment=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT): 103 | if host is None or username is None or password is None: 104 | settings_dict = cls.read_settings_from_file(environment) 105 | try: 106 | host = host or settings_dict['host'] 107 | username = username or settings_dict.get('username', None) 108 | password = password or settings_dict.get('password', None) 109 | except KeyError: 110 | raise CliException('Jenkins "host" should be specified by the command-line option or in the .jenkins-cli file') 111 | return jenkins.Jenkins(host, username, password, timeout) 112 | 113 | @classmethod 114 | def read_settings_from_file(cls, environment): 115 | # get config filename 116 | current_folder = os.getcwd() 117 | filename = os.path.join(current_folder, cls.SETTINGS_FILE_NAME) 118 | if not os.path.exists(filename): 119 | home_folder = os.path.expanduser("~") 120 | filename = os.path.join(home_folder, cls.SETTINGS_FILE_NAME) 121 | if not os.path.exists(filename): 122 | return {} 123 | 124 | # use the DEFAULT section if no env is specified 125 | if not environment: 126 | environment = 'DEFAULT' 127 | 128 | # read the config file 129 | config = ConfigParser() 130 | try: 131 | with open(filename, 'r') as f: 132 | config.readfp(f) 133 | except Exception as e: 134 | raise CliException('Error reading %s: %s' % (filename, e)) 135 | 136 | # return the variables as dict 137 | try: 138 | return dict(config.items(environment)) 139 | except NoSectionError: 140 | raise CliException('%s section not found in .jenkins-cli config' 141 | ' file' % environment) 142 | 143 | def run_command(self, args): 144 | command = args.jenkins_command 145 | getattr(self, command)(args) 146 | 147 | def jobs(self, args): 148 | jobs = self._get_jobs(args) 149 | 150 | # print jobs 151 | for job in jobs: 152 | formated_status = get_formated_status(job['color']) 153 | print(formated_status + " " + job['name']) 154 | 155 | # save job names to cache file 156 | our_cache_dir = save_cache_path('python-jenkins-cli') 157 | job_cache_file = os.path.join(our_cache_dir, self.JOB_CACHE_FILE_NAME) 158 | with open(job_cache_file, 'w') as f: 159 | f.write(' '.join(job['name'] for job in jobs)) 160 | 161 | def _get_jobs(self, args): 162 | jobs = self.jenkins.get_jobs() 163 | if args.a: 164 | jobs = [j for j in jobs if j.get('color') != 'disabled'] 165 | if hasattr(args, 'p') and args.p: 166 | jobs = [j for j in jobs if 'anime' in j.get('color')] 167 | return jobs 168 | 169 | def queue(self, args): 170 | jobs = self.jenkins.get_queue_info() 171 | if jobs: 172 | for job in jobs: 173 | print("%s %s" % (job['task']['name'], job['why'])) 174 | else: 175 | print(self.QUEUE_EMPTY_TEXT) 176 | 177 | def _check_job(self, job_name): 178 | job_name = self.jenkins.get_job_name(job_name) 179 | if not job_name: 180 | raise CliException('Job name does not exist') 181 | return job_name 182 | 183 | def _get_scm_name_and_node(self, xml_root): 184 | scm_name = 'UnknownVCS' 185 | branch_node = None 186 | try: 187 | scm = xml_root.find('scm') 188 | if scm.attrib['class'] == 'hudson.plugins.mercurial.MercurialSCM': 189 | scm_name = 'Mercurial' 190 | branch_node = scm.find('revision') 191 | elif scm.attrib['class'] == 'hudson.plugins.git.GitSCM': 192 | scm_name = 'Git' 193 | branch_node = scm.find('branches').find('hudson.plugins.git.BranchSpec').find('name') 194 | except AttributeError: 195 | pass 196 | return (scm_name, branch_node) 197 | 198 | def info(self, args): 199 | job_name = self._check_job(args.job_name) 200 | job_info = self.jenkins.get_job_info(job_name, 1) 201 | if not job_info: 202 | job_info = {} 203 | last_build = job_info.get('lastBuild') or {} 204 | last_success_build = job_info.get('lastSuccessfulBuild') or {} 205 | xml = self.jenkins.get_job_config(job_name) 206 | root = ElementTree.fromstring(xml.encode('utf-8')) 207 | scm_name, branch_node = self._get_scm_name_and_node(root) 208 | if branch_node is not None: 209 | branch_name = branch_node.text 210 | else: 211 | branch_name = 'Unknown branch' 212 | print(self.INFO_TEMPLATE % (last_build.get('fullDisplayName', 'Not Built'), 213 | last_build.get('result', 'Not Built'), 214 | last_success_build.get('fullDisplayName', 'Not Built'), 215 | datetime.datetime.fromtimestamp(last_build['timestamp'] / 1000) if last_build else 'Not Built', 216 | 'Yes' if last_build.get('building') else 'No', 217 | scm_name, 218 | branch_name)) 219 | 220 | def configxml(self, args): 221 | job_name = self._check_job(args.job_name) 222 | job_config = self.jenkins.get_job_config(job_name) 223 | print(job_config) 224 | 225 | def setbranch(self, args): 226 | job_name = self._check_job(args.job_name) 227 | xml = self.jenkins.get_job_config(job_name) 228 | root = ElementTree.fromstring(xml.encode('utf-8')) 229 | _, branch_node = self._get_scm_name_and_node(root) 230 | if branch_node is not None: 231 | branch_node.text = args.branch_name 232 | new_xml = xml_to_string(root) 233 | self.jenkins.reconfig_job(job_name, new_xml) 234 | print('Done') 235 | else: 236 | print("Cannot set branch name") 237 | 238 | def start(self, args): 239 | for job in args.job_name: 240 | job_name = self._check_job(job) 241 | start_status = self.jenkins.build_job(job_name) 242 | print("%s: %s" % (job_name, 'started' if not start_status else start_status)) 243 | 244 | def _get_build_changesets(self, build): 245 | if 'changeSet' in build and 'items' in build['changeSet']: 246 | return build['changeSet']['items'] 247 | else: 248 | return [] 249 | 250 | def _get_build_duration(self, build): 251 | return datetime.timedelta(milliseconds=build["duration"]) 252 | 253 | def builds(self, args): 254 | job_name = self._check_job(args.job_name) 255 | job_info = self.jenkins.get_job_info(job_name, 1) 256 | if not job_info['builds']: 257 | print("%(job_name)s has no builds" % {'job_name': job_name}) 258 | else: 259 | for build in job_info['builds'][:10]: 260 | color = RESULT_TO_COLOR.get(build['result'], 'unknown') 261 | if build['building']: 262 | color = color + "_anime" 263 | pattern = "%(color)s%(symbol)s%(run_status)s #%(number)s%(endcollor)s %(duration)s (%(changeset_count)s commits)" 264 | changeset_count = len(self._get_build_changesets(build)) 265 | status = get_formated_status(color, 266 | format_pattern=pattern, 267 | extra_params={'number': build['number'], 268 | 'duration': str(self._get_build_duration(build)).split('.')[0], 269 | 'changeset_count': changeset_count}) 270 | print(status) 271 | 272 | def stop(self, args): 273 | job_name = self._check_job(args.job_name) 274 | info = self.jenkins.get_job_info(job_name, 1) 275 | last_build = info.get('lastBuild') or {} 276 | build_number = last_build.get('number') 277 | if build_number and last_build.get('building'): 278 | stop_status = self.jenkins.stop_build(job_name, build_number) 279 | print("%s: %s" % (job_name, 'stopped' if not stop_status else stop_status)) 280 | else: 281 | print("%s job is not running" % job_name) 282 | 283 | def _get_build_number(self, job_name, build_number=None): 284 | info = self.jenkins.get_job_info(job_name) 285 | if not info['lastBuild']: 286 | return None 287 | if build_number: 288 | if build_number[0] == "#": 289 | build_number = build_number[1:] 290 | if build_number.isdigit(): 291 | build_number = int(build_number) 292 | else: 293 | raise CliException('Build number must include digits only') 294 | else: 295 | build_number = info['lastBuild'].get('number') 296 | return build_number 297 | 298 | def changes(self, args): 299 | job_name = self._check_job(args.job_name) 300 | build_number = self._get_build_number(job_name, args.build) 301 | if build_number is None: 302 | print("Cannot show changes. %(job_name)s has no builds" % {'job_name': job_name}) 303 | else: 304 | build = self.jenkins.get_build_info(job_name, build_number) 305 | if 'changeSet' in build: 306 | changesets = build['changeSet'].get('items') 307 | if changesets: 308 | for index, change in enumerate(changesets): 309 | params = {'num': index + 1, 310 | 'msg': change['msg'], 311 | 'author': change['author'].get('fullName', 'Unknown'), 312 | 'is_merge': "MERGE" if change.get('merge') else '', 313 | 'affected_files': len(change['affectedPaths']), 314 | 'endcollor': ENDCOLLOR, 315 | 'author_collor': AUTHOR_COLLOR, 316 | 'msg_collor': MSG_COLLOR} 317 | print("%(num)s. %(msg_collor)s%(msg)s%(endcollor)s by %(author_collor)s%(author)s%(endcollor)s affected %(affected_files)s files %(is_merge)s" % params) 318 | else: 319 | print("%(job_name)s %(build_number)s has no changes" % {'job_name': job_name, 'build_number': build_number}) 320 | 321 | def console(self, args): 322 | job_name = self._check_job(args.job_name) 323 | build_number = self._get_build_number(job_name, args.build) 324 | if build_number is None: 325 | print("Cannot show console output. %(job_name)s has no builds" % {'job_name': job_name}) 326 | else: 327 | console_out = self.jenkins.get_build_console_output(job_name, build_number) 328 | console_out = console_out.splitlines() 329 | last_line_num = len(console_out) 330 | if args.n: 331 | console_out = console_out[args.n:] if args.n < 0 else console_out[:args.n] 332 | print("\n".join(console_out)) 333 | if args.i: 334 | build_info = self.jenkins.get_build_info(job_name, build_number) 335 | while build_info['building']: 336 | sleep(args.interval) 337 | console_out = self.jenkins.get_build_console_output(job_name, build_number) 338 | console_out = console_out.splitlines() 339 | new_line_num = len(console_out) 340 | if new_line_num > last_line_num: 341 | print("\n".join(console_out[last_line_num:])) 342 | last_line_num = new_line_num 343 | build_info = self.jenkins.get_build_info(job_name, build_number) 344 | 345 | def building(self, args): 346 | args.a = True 347 | jobs = [j for j in self._get_jobs(args) if 'anime' in j['color']] 348 | if jobs: 349 | for job in jobs: 350 | info = self.jenkins.get_job_info(job['name']) 351 | build_number = info['lastBuild'].get('number') 352 | eta = "unknown" 353 | display_name = job['name'] 354 | if build_number: 355 | build_info = self.jenkins.get_build_info(job['name'], build_number) 356 | eta = (build_info['timestamp'] + build_info['estimatedDuration']) / 1000 - time() 357 | eta = datetime.timedelta(seconds=eta) 358 | display_name = build_info['fullDisplayName'] 359 | print("%s estimated time left %s" % (display_name, eta)) 360 | else: 361 | print("Nothing is being built now") 362 | 363 | def wait(self, args): 364 | """ Wait for the next building job, if there is one currently running, 365 | it will return immediately""" 366 | job_name = self._check_job(args.job_name) 367 | job_info = self.jenkins.get_job_info(args.job_name, 1) 368 | build_number = self._get_build_number(job_name) 369 | 370 | if not job_info: 371 | job_info = {} 372 | old_build_number = build_number 373 | while build_number == old_build_number: 374 | if job_info.get('lastBuild', {}).get('building'): 375 | break 376 | build_number = self._get_build_number(job_name) 377 | sleep(args.interval) 378 | -------------------------------------------------------------------------------- /tests/test_cli.py: -------------------------------------------------------------------------------- 1 | import unittest2 as unittest 2 | import os 3 | from argparse import Namespace 4 | from xml.etree import ElementTree 5 | from datetime import datetime, timedelta 6 | 7 | from pyfakefs import fake_filesystem_unittest 8 | import mock 9 | 10 | import socket 11 | 12 | import jenkins 13 | 14 | from jenkins_cli.cli import JenkinsCli, CliException, STATUSES_COLOR, ENDCOLLOR 15 | 16 | GIT_SCM_XML = """\n\n \n \n false\n \n \n 2\n \n \n https://github.com/LD250/jenkins-cli-python/\n \n \n \n \n cli-tests\n \n \n false\n \n \n \n true\n false\n false\n false\n \n false\n \n \n \n \n \n System-CPython-2.7\n \n true\n false\n shell\n pip install -U pip\npip install -U setuptools\npip install -U wheel\npip install -r requirements.txt\npip list -o\n\nflake8 jenkins_cli\npython setup.py test\n false\n \n \n \n \n 17 | """ 18 | 19 | HG_SCM_XML = """\n\n \n \n false\n \n \n \n BRANCH\n v123\n false\n \n false\n \n true\n false\n false\n false\n \n false\n \n \n \n 20 | """ 21 | 22 | EMPTY_SCM_XML = """\n\n \n \n false\n \n \n true\n false\n false\n false\n \n false\n \n \n \n 23 | """ 24 | 25 | TS = 1456870299 26 | 27 | 28 | class TestCliAuth(unittest.TestCase): 29 | 30 | @mock.patch.object(JenkinsCli, 'read_settings_from_file', return_value={}) 31 | @mock.patch.object(jenkins.Jenkins, '__init__', return_value=None) 32 | def test_auth_no_file_settings(self, patched_init, read_settings_from_file): 33 | exep = None 34 | try: 35 | JenkinsCli.auth() 36 | except Exception as e: 37 | exep = e 38 | self.assertEqual(type(exep), CliException) 39 | self.assertEqual(patched_init.called, False) 40 | 41 | host = 'http://localhost:5055' 42 | JenkinsCli.auth(host=host) 43 | patched_init.assert_called_once_with(host, None, None, socket._GLOBAL_DEFAULT_TIMEOUT) 44 | patched_init.reset_mock() 45 | 46 | username = 'username' 47 | password = 'password' 48 | JenkinsCli.auth(host=host, username=username, password=password) 49 | patched_init.assert_called_once_with(host, username, password, socket._GLOBAL_DEFAULT_TIMEOUT) 50 | 51 | @mock.patch.object(JenkinsCli, 'read_settings_from_file') 52 | @mock.patch.object(jenkins.Jenkins, '__init__', return_value=None) 53 | def test_auth_has_file_settings(self, patched_init, read_settings_from_file): 54 | read_settings_from_file.return_value = {'username': 'username'} 55 | exep = None 56 | try: 57 | JenkinsCli.auth() 58 | except Exception as e: 59 | exep = e 60 | self.assertEqual(type(exep), CliException) 61 | self.assertEqual(patched_init.called, False) 62 | 63 | host_from_file = 'http://low.priority.com' 64 | username = 'username' 65 | password = 'password' 66 | read_settings_from_file.return_value = {'host': host_from_file, 'username': username, 'password': password} 67 | JenkinsCli.auth() 68 | patched_init.assert_called_once_with(host_from_file, username, password, socket._GLOBAL_DEFAULT_TIMEOUT) 69 | patched_init.reset_mock() 70 | 71 | host = 'http://localhost:5055' 72 | JenkinsCli.auth(host=host) 73 | patched_init.assert_called_once_with(host, username, password, socket._GLOBAL_DEFAULT_TIMEOUT) 74 | patched_init.reset_mock() 75 | 76 | 77 | class TestCliFileUsing(fake_filesystem_unittest.TestCase): 78 | HOME_FILE_CONTENT = ("[DEFAULT]\n" 79 | "host =https://jenkins.host.com\n" 80 | "username= username\n" 81 | "some weird settings = value = value") 82 | 83 | LOCAL_FILE_CONTENT = ("[DEFAULT]\n" 84 | "host=http://jenkins.localhosthost.ua\n" 85 | "username=Denys\n" 86 | "password=myPassword\n" 87 | "other_setting=some_value") 88 | 89 | MULTIENV_FILE_CONTENT = ("[DEFAULT]\n" 90 | "host =https://jenkins.host.com\n" 91 | "username= username\n" 92 | "some default settings = value = value\n" 93 | "\n" 94 | "[alternative]\n" 95 | "host=http://jenkins.localhosthost.ua\n" 96 | "username=Denys\n" 97 | "password=myPassword\n" 98 | "other_setting=some_value" 99 | ) 100 | 101 | def setUp(self): 102 | self.setUpPyfakefs() 103 | 104 | def test_read_settings_from_file(self): 105 | current_folder = os.getcwd() 106 | local_folder_filename = os.path.join(current_folder, JenkinsCli.SETTINGS_FILE_NAME) 107 | home_folder_filename = os.path.join(os.path.expanduser("~"), JenkinsCli.SETTINGS_FILE_NAME) 108 | self.assertFalse(os.path.exists(local_folder_filename)) 109 | self.assertFalse(os.path.exists(home_folder_filename)) 110 | 111 | self.fs.CreateFile(home_folder_filename, 112 | contents=self.HOME_FILE_CONTENT) 113 | self.assertTrue(os.path.exists(home_folder_filename)) 114 | settings_dict = JenkinsCli.read_settings_from_file(environment=None) 115 | self.assertEqual(settings_dict, 116 | {"host": 'https://jenkins.host.com', 117 | "username": "username", 118 | "some weird settings": "value = value" 119 | }) 120 | 121 | self.fs.CreateFile(local_folder_filename, 122 | contents=self.LOCAL_FILE_CONTENT) 123 | self.assertTrue(os.path.exists(local_folder_filename)) 124 | settings_dict = JenkinsCli.read_settings_from_file(environment=None) 125 | self.assertEqual(settings_dict, 126 | {"host": 'http://jenkins.localhosthost.ua', 127 | "username": "Denys", 128 | "password": "myPassword", 129 | "other_setting": "some_value" 130 | }) 131 | 132 | def test_read_settings_from_file_alt_environment(self): 133 | # make sure we are in the fake fs 134 | current_folder = os.getcwd() 135 | local_folder_filename = os.path.join(current_folder, JenkinsCli.SETTINGS_FILE_NAME) 136 | self.assertFalse(os.path.exists(local_folder_filename)) 137 | 138 | # create the fake config file 139 | self.fs.CreateFile(local_folder_filename, 140 | contents=self.MULTIENV_FILE_CONTENT) 141 | self.assertTrue(os.path.exists(local_folder_filename)) 142 | 143 | # read the config from the file 144 | settings_dict = JenkinsCli.read_settings_from_file(environment='alternative') 145 | 146 | # test and that the alternative environment is used, with the missing 147 | # values being provided from the DEFAULT environmtne 148 | self.assertEqual(settings_dict, 149 | {"host": 'http://jenkins.localhosthost.ua', 150 | "username": "Denys", 151 | "password": "myPassword", 152 | "other_setting": "some_value", 153 | 'some default settings': 'value = value' 154 | }) 155 | 156 | 157 | class TestCliCommands(unittest.TestCase): 158 | 159 | def setUp(self): 160 | self.args = Namespace(host='http://jenkins.host.com', username=None, password=None, environment=None) 161 | self.print_patcher = mock.patch('jenkins_cli.cli.print') 162 | self.patched_print = self.print_patcher.start() 163 | 164 | def tearDown(self): 165 | self.print_patcher.stop() 166 | 167 | @mock.patch.object(jenkins.Jenkins, 'get_jobs') 168 | def test_jobs(self, patched_get_jobs): 169 | jobs = [{'name': 'Job1', 170 | 'color': 'blue'}, 171 | {'name': 'Job2', 172 | 'color': 'disabled'}] 173 | patched_get_jobs.return_value = jobs 174 | self.args.a = False 175 | JenkinsCli(self.args).jobs(self.args) 176 | arg1 = "%sS..%s Job1" % (STATUSES_COLOR[jobs[0]['color']]['color'], ENDCOLLOR) 177 | arg2 = "%sD..%s Job2" % (STATUSES_COLOR[jobs[1]['color']]['color'], ENDCOLLOR) 178 | self.patched_print.assert_has_calls([mock.call(arg1)], [mock.call(arg2)]) 179 | self.patched_print.reset_mock() 180 | self.args.a = True 181 | JenkinsCli(self.args).jobs(self.args) 182 | self.patched_print.assert_called_once_with(arg1) 183 | 184 | @mock.patch.object(jenkins.Jenkins, 'get_queue_info') 185 | def test_queue(self, patched_get_queue_info): 186 | queue_list = [ 187 | {u'task': {u'url': u'http://your_url/job/my_job/', u'color': u'aborted_anime', u'name': u'my_job'}, 188 | u'stuck': False, 189 | u'actions': [{u'causes': [{u'shortDescription': u'Started by timer'}]}], 190 | u'why': u'Build #2,532 is already in progress (ETA:10 min)'}, 191 | {u'task': {u'url': u'http://your_url/job/my_job/', u'color': u'aborted_anime', u'name': u'my_job2'}, 192 | u'stuck': False, 193 | u'actions': [{u'causes': [{u'shortDescription': u'Started by timer'}]}], 194 | u'why': u'Build #234 is already in progress (ETA:10 min)'} 195 | ] 196 | patched_get_queue_info.return_value = [] 197 | JenkinsCli(self.args).queue(self.args) 198 | self.patched_print.assert_called_once_with(JenkinsCli.QUEUE_EMPTY_TEXT) 199 | patched_get_queue_info.reset_mock() 200 | patched_get_queue_info.return_value = queue_list 201 | JenkinsCli(self.args).queue(self.args) 202 | args = ["%s %s" % (job['task']['name'], job['why']) for job in queue_list] 203 | self.patched_print.assert_has_calls([mock.call(args[0])], [mock.call(args[1])]) 204 | 205 | @mock.patch.object(jenkins.Jenkins, 'get_job_name') 206 | def test_check_job(self, patched_get_job_name): 207 | patched_get_job_name.return_value = None 208 | exep = None 209 | try: 210 | JenkinsCli(self.args)._check_job('Job1') 211 | except Exception as e: 212 | exep = e 213 | self.assertEqual(type(exep), CliException) 214 | 215 | patched_get_job_name.return_value = 'Job1' 216 | job_name = JenkinsCli(self.args)._check_job('Job1') 217 | self.assertEqual(job_name, 'Job1') 218 | 219 | def test_get_scm_name_and_node(self): 220 | root = ElementTree.fromstring(GIT_SCM_XML.encode('utf-8')) 221 | name, branch_node = JenkinsCli(self.args)._get_scm_name_and_node(root) 222 | self.assertEqual(name, 'Git') 223 | self.assertEqual(branch_node.text, 'cli-tests') 224 | 225 | root = ElementTree.fromstring(HG_SCM_XML.encode('utf-8')) 226 | name, branch_node = JenkinsCli(self.args)._get_scm_name_and_node(root) 227 | self.assertEqual(name, 'Mercurial') 228 | self.assertEqual(branch_node.text, 'v123') 229 | 230 | root = ElementTree.fromstring(EMPTY_SCM_XML.encode('utf-8')) 231 | name, branch_node = JenkinsCli(self.args)._get_scm_name_and_node(root) 232 | self.assertEqual(name, 'UnknownVCS') 233 | self.assertEqual(branch_node, None) 234 | 235 | @mock.patch.object(jenkins.Jenkins, 'get_job_config') 236 | @mock.patch.object(jenkins.Jenkins, 'get_job_info') 237 | @mock.patch.object(jenkins.Jenkins, 'get_job_name', return_value='Job1') 238 | def test_info(self, patched_get_job_name, patched_get_job_info, patched_get_job_config): 239 | self.args.job_name = "Job1" 240 | patched_get_job_info.return_value = {} 241 | patched_get_job_config.return_value = EMPTY_SCM_XML 242 | JenkinsCli(self.args).info(self.args) 243 | arg = JenkinsCli.INFO_TEMPLATE % ('Not Built', 'Not Built', 'Not Built', 'Not Built', 'No', 'UnknownVCS', 'Unknown branch') 244 | self.patched_print.assert_called_once_with(arg) 245 | self.patched_print.reset_mock() 246 | 247 | job_info = {'lastBuild': {'fullDisplayName': 'FDN (cur)', 248 | 'result': 'Done', 249 | 'timestamp': TS * 1000, 250 | 'building': True}, 251 | 'lastSuccessfulBuild': {'fullDisplayName': 'FDN (last)'}} 252 | patched_get_job_info.return_value = job_info 253 | patched_get_job_config.return_value = GIT_SCM_XML 254 | JenkinsCli(self.args).info(self.args) 255 | arg = JenkinsCli.INFO_TEMPLATE % ('FDN (cur)', 'Done', 'FDN (last)', datetime.fromtimestamp(TS), 'Yes', 'Git', 'cli-tests') 256 | self.patched_print.assert_called_once_with(arg) 257 | self.patched_print.reset_mock() 258 | 259 | job_info['building'] = False 260 | patched_get_job_info.return_value = job_info 261 | patched_get_job_config.return_value = HG_SCM_XML 262 | JenkinsCli(self.args).info(self.args) 263 | arg = JenkinsCli.INFO_TEMPLATE % ('FDN (cur)', 'Done', 'FDN (last)', datetime.fromtimestamp(TS), 'Yes', 'Mercurial', 'v123') 264 | self.patched_print.assert_called_once_with(arg) 265 | 266 | @mock.patch.object(jenkins.Jenkins, 'reconfig_job') 267 | @mock.patch.object(jenkins.Jenkins, 'get_job_config') 268 | @mock.patch.object(jenkins.Jenkins, 'get_job_name', return_value='Job1') 269 | def test_setbranch(self, patched_get_job_name, patched_get_job_config, patched_reconfig_job): 270 | patched_get_job_config.return_value = EMPTY_SCM_XML 271 | self.args.job_name = 'Job1' 272 | self.args.branch_name = 'b1' 273 | JenkinsCli(self.args).setbranch(self.args) 274 | self.assertFalse(patched_reconfig_job.called) 275 | self.patched_print.assert_called_once_with("Cannot set branch name") 276 | self.patched_print.reset_mock() 277 | 278 | patched_get_job_config.return_value = GIT_SCM_XML 279 | JenkinsCli(self.args).setbranch(self.args) 280 | self.assertEqual(patched_reconfig_job.call_args[0][0], 'Job1') 281 | self.assertIn('b1', str(patched_reconfig_job.call_args[0][1])) 282 | self.assertNotIn('cli-tests', patched_reconfig_job.call_args[1]) 283 | self.patched_print.assert_called_once_with("Done") 284 | patched_reconfig_job.reset_mock() 285 | self.patched_print.reset_mock() 286 | 287 | patched_get_job_config.return_value = HG_SCM_XML 288 | JenkinsCli(self.args).setbranch(self.args) 289 | self.assertEqual(patched_reconfig_job.call_args[0][0], 'Job1') 290 | self.assertIn('b1', str(patched_reconfig_job.call_args[0][1])) 291 | self.assertNotIn('v123', patched_reconfig_job.call_args[1]) 292 | self.patched_print.assert_called_once_with("Done") 293 | patched_reconfig_job.reset_mock() 294 | self.patched_print.reset_mock() 295 | 296 | @mock.patch.object(jenkins.Jenkins, 'build_job', return_value=None) 297 | @mock.patch.object(jenkins.Jenkins, 'get_job_name', return_value='Job1') 298 | def test_start(self, patched_job_name, patched_build_job): 299 | self.args.job_name = ['Job1'] 300 | JenkinsCli(self.args).start(self.args) 301 | patched_build_job.assert_called_once_with('Job1') 302 | self.patched_print.assert_called_once_with("%s: %s" % ('Job1', 'started')) 303 | 304 | @mock.patch.object(jenkins.Jenkins, 'stop_build', return_value=None) 305 | @mock.patch.object(jenkins.Jenkins, 'get_job_info') 306 | @mock.patch.object(jenkins.Jenkins, 'get_job_name', return_value='Job1') 307 | def test_stop(self, patched_job_name, patched_job_info, patched_stop_build): 308 | self.args.job_name = 'Job1' 309 | patched_job_info.return_value = {'lastBuild': {}} 310 | JenkinsCli(self.args).stop(self.args) 311 | self.assertFalse(patched_stop_build.called) 312 | self.patched_print.assert_called_once_with("%s job is not running" % 'Job1') 313 | self.patched_print.reset_mock() 314 | 315 | patched_job_info.return_value = {'lastBuild': {'building': True, 'number': 22}} 316 | JenkinsCli(self.args).stop(self.args) 317 | patched_stop_build.assert_called_once_with('Job1', 22) 318 | self.patched_print.assert_called_once_with("Job1: stopped") 319 | 320 | @mock.patch('jenkins_cli.cli.time', return_value=0) 321 | @mock.patch.object(jenkins.Jenkins, 'get_jobs') 322 | @mock.patch.object(jenkins.Jenkins, 'get_build_info') 323 | @mock.patch.object(jenkins.Jenkins, 'get_job_info') 324 | @mock.patch.object(jenkins.Jenkins, 'get_job_name', side_effect=lambda j: j) 325 | def test_building(self, patched_job_name, patched_job_info, patched_build_info, get_jobs_patched, patched_time): 326 | get_jobs_patched.return_value = [{'name': 'Job1', 'color': 'blue'}] 327 | JenkinsCli(self.args).building(self.args) 328 | self.assertFalse(patched_job_info.called) 329 | self.assertFalse(patched_build_info.called) 330 | self.patched_print.assert_called_once_with("Nothing is being built now") 331 | self.patched_print.reset_mock() 332 | 333 | get_jobs_patched.return_value = [{'name': 'Job1', 'color': 'blue_anime'}, 334 | {'name': 'Job5', 'color': 'red_anime'}] 335 | patched_job_info.return_value = {'lastBuild': {}} 336 | JenkinsCli(self.args).building(self.args) 337 | self.assertFalse(patched_build_info.called) 338 | self.patched_print.assert_has_calls([mock.call("Job1 estimated time left unknown")], 339 | [mock.call("Job5 estimated time left unknown")]) 340 | self.patched_print.reset_mock() 341 | 342 | patched_job_info.return_value = {'lastBuild': {'number': 2}} 343 | 344 | def info_side_effect(name, number): 345 | return {'timestamp': TS * 1000, 'estimatedDuration': 0, 'fullDisplayName': 'FDN ' + name} 346 | 347 | patched_build_info.side_effect = info_side_effect 348 | JenkinsCli(self.args).building(self.args) 349 | self.patched_print.assert_has_calls([mock.call("FDN Job1 estimated time left %s" % timedelta(seconds=TS))], 350 | [mock.call("FDN Job5 estimated time left %s" % timedelta(seconds=TS))]) 351 | 352 | 353 | if __name__ == '__main__': 354 | unittest.main() 355 | --------------------------------------------------------------------------------