├── 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 | [](https://badge.fury.io/py/jenkins-cli)
3 | [](https://travis-ci.org/LD250/jenkins-cli-python)
4 | [](https://landscape.io/github/LD250/jenkins-cli-python/master)
5 | [](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 |
--------------------------------------------------------------------------------