├── THANKS ├── test ├── __init__.py ├── integration │ ├── __init__.py │ ├── projects_test.py │ ├── build_icons_test.py │ ├── appui_test.py │ ├── timed_event_test.py │ ├── buildnotify_test.py │ ├── config_test.py │ ├── preferences_dialog_test.py │ ├── server_configuration_dialog_test.py │ └── app_menu_test.py ├── whitelist.py ├── utils.py ├── version_test.py ├── response_test.py ├── fake_keyring.py ├── unit │ └── distance_of_time_test.py ├── project_builder.py ├── serverconfig_test.py ├── http_connection_test.py ├── server_test.py ├── fake_conf.py ├── projects_test.py ├── project_test.py └── project_status_notification_test.py ├── pytest.ini ├── INSTALL ├── AUTHORS ├── MANIFEST.in ├── mypy.ini ├── docs ├── images │ ├── misc.png │ ├── servers.png │ ├── projectlist.png │ └── notifications.png ├── _config.yml ├── faq.md ├── installation.md ├── usage.md └── index.md ├── .github ├── dependabot.yml ├── stale.yml └── workflows │ ├── package.yml │ └── main.yml ├── stdeb.cfg ├── .gitignore ├── icons ├── buildnotify-failure.svg ├── buildnotify.svg ├── buildnotify-inactive.svg ├── buildnotify-success.svg ├── buildnotify-failure-building.svg ├── buildnotify-success-building.svg └── icons.qrc ├── test-requirements.txt ├── setup.sh ├── Vagrantfile ├── tox.ini ├── DEVELOPMENT.md ├── LICENSE ├── README.rst ├── snap └── snapcraft.yaml ├── data ├── cctray.xml ├── server_configuration.ui └── preferences.ui ├── setup.py ├── CHANGELOG ├── pavement.py ├── CODE_OF_CONDUCT.md └── .pylintrc /THANKS: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/integration/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/whitelist.py: -------------------------------------------------------------------------------- 1 | _.encoding # type: ignore 2 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | markers = 3 | requireshead 4 | functional -------------------------------------------------------------------------------- /INSTALL: -------------------------------------------------------------------------------- 1 | * LINUX 2 | Use setup.sh to install the required dependencies 3 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Anay Nayak 2 | anayak007@gmail.com 3 | https://github.com/anaynayak 4 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include buildnotifylib *.py 2 | include *.desktop 3 | include *.svg -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | ignore_missing_imports = True 3 | warn_unused_ignores = True 4 | -------------------------------------------------------------------------------- /docs/images/misc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anaynayak/buildnotify/HEAD/docs/images/misc.png -------------------------------------------------------------------------------- /docs/images/servers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anaynayak/buildnotify/HEAD/docs/images/servers.png -------------------------------------------------------------------------------- /docs/images/projectlist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anaynayak/buildnotify/HEAD/docs/images/projectlist.png -------------------------------------------------------------------------------- /docs/images/notifications.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anaynayak/buildnotify/HEAD/docs/images/notifications.png -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "pip" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /stdeb.cfg: -------------------------------------------------------------------------------- 1 | [DEFAULT] 2 | Depends: python-pyqt5, python-dateutil, python-tz, python-keyring 3 | XS-Python-Version: >= 3.7 4 | MIME-Desktop-Files: buildnotify.desktop 5 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman 2 | google_analytics: UA-106316944-1 3 | title: BuildNotify 4 | description: Buildnotify is a system tray build status notification app. 5 | show_downloads: true 6 | -------------------------------------------------------------------------------- /test/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | def fake_content(): 5 | path = os.path.realpath(os.path.dirname(os.path.abspath(__file__)) + "/../data/cctray.xml") 6 | return open(path, encoding='utf-8').read() 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .DS_Store 3 | venv 4 | .eggs 5 | *~ 6 | .idea 7 | .vagrant 8 | *.egg-info 9 | .cache 10 | *.diff 11 | dist 12 | report.txt 13 | .tox 14 | .coverage 15 | coverage.xml 16 | parts 17 | .vscode 18 | reports 19 | -------------------------------------------------------------------------------- /icons/buildnotify-failure.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /icons/buildnotify.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /icons/buildnotify-inactive.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /icons/buildnotify-success.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /icons/buildnotify-failure-building.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /icons/buildnotify-success-building.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /test-requirements.txt: -------------------------------------------------------------------------------- 1 | pytest==7.3.1 2 | pytest-qt==4.0.2 3 | pytest-cov==4.1.0 4 | pytest-mock==3.7.0 5 | pytest-pylint==0.18.0 6 | requests_mock==1.8.0 7 | pytest-xvfb==2.0.0 8 | keyring==23.5.0 9 | mock==5.0.2 10 | flake8==7.0.0 11 | PyQt5-stubs==5.14.2.2 12 | pytest-mypy==0.9.0 -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | sudo apt-get update 4 | sudo apt-get install -y ubuntu-desktop xinit unity 5 | sudo apt-get install -y python-pyqt5 pyqt5-dev-tools python-tz python-dateutil qtdeclarative5-dev qttools5-dev-tools python-pip python-keyring 6 | sudo pip install paver 7 | sudo pip install -r test-requirements.txt 8 | 9 | -------------------------------------------------------------------------------- /test/version_test.py: -------------------------------------------------------------------------------- 1 | from os import environ 2 | from re import match 3 | 4 | from buildnotifylib import version 5 | 6 | 7 | def test_should_cleanup_url(): 8 | assert match('^\d*.\d*.\d*$', version.version()) 9 | environ['BUILD_LABEL'] = '23' 10 | assert match('^\d*.\d*.\d*.dev23$', version.version()) 11 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | Vagrant.configure(2) do |config| 5 | config.vm.box = "ubuntu/trusty64" 6 | config.vm.box_check_update = false 7 | 8 | config.vm.provider "virtualbox" do |vb| 9 | vb.gui = true 10 | vb.memory = "2048" 11 | end 12 | 13 | config.vm.provision "shell", path: "setup.sh" 14 | end 15 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | daysUntilStale: 60 2 | daysUntilClose: 7 3 | exemptLabels: 4 | - pinned 5 | - security 6 | staleLabel: wontfix 7 | markComment: > 8 | This issue has been automatically marked as stale because it has not had 9 | recent activity. It will be closed if no further activity occurs. Thank you 10 | for your contributions. 11 | closeComment: false 12 | -------------------------------------------------------------------------------- /icons/icons.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | ../icons/buildnotify-success.svg 4 | ../icons/buildnotify-failure-building.svg 5 | ../icons/buildnotify-inactive.svg 6 | ../icons/buildnotify-success-building.svg 7 | ../icons/buildnotify-failure.svg 8 | 9 | 10 | -------------------------------------------------------------------------------- /test/response_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from buildnotifylib.core.response import Response 3 | from requests.exceptions import SSLError 4 | 5 | class ResponseTest(unittest.TestCase): 6 | def test_should_return_ssl_error(self): 7 | response = Response({}, SSLError()) 8 | 9 | self.assertTrue(response.failed()) 10 | self.assertTrue(response.ssl_error()) 11 | -------------------------------------------------------------------------------- /test/fake_keyring.py: -------------------------------------------------------------------------------- 1 | import keyring 2 | 3 | 4 | class FakeKeyring(keyring.backend.KeyringBackend): 5 | priority = 1 6 | 7 | def set_password(self, servicename, username, password): 8 | pass 9 | 10 | def get_password(self, servicename, username): 11 | return "pass" 12 | 13 | def delete_password(self, servicename, username, password): 14 | pass 15 | -------------------------------------------------------------------------------- /test/integration/projects_test.py: -------------------------------------------------------------------------------- 1 | from test.fake_conf import ConfigBuilder 2 | from buildnotifylib.core.projects import ProjectsPopulator 3 | import pytest 4 | 5 | 6 | @pytest.mark.functional 7 | def test_should_fetch_projects(qtbot): 8 | conf = ConfigBuilder().build() 9 | populator = ProjectsPopulator(conf) 10 | with qtbot.waitSignal(populator.updated_projects, timeout=1000): 11 | populator.process() 12 | -------------------------------------------------------------------------------- /test/unit/distance_of_time_test.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from pytz import timezone 3 | 4 | from buildnotifylib.core.distance_of_time import DistanceOfTime 5 | 6 | 7 | def test_should_get_relative_distance(): 8 | assert "1 minute" == DistanceOfTime(datetime.now(timezone('US/Eastern'))).age() 9 | 10 | 11 | def test_should_get_relative_distance_for_tz_unaware(): 12 | assert "1 minute" == DistanceOfTime(datetime.now()).age() 13 | -------------------------------------------------------------------------------- /test/integration/build_icons_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from buildnotifylib.build_icons import BuildIcons 3 | 4 | 5 | @pytest.mark.functional 6 | def test_should_consolidate_build_status(qtbot): 7 | aggregate_status = BuildIcons().for_aggregate_status('Success.Sleeping', "0") 8 | assert aggregate_status is not None 9 | 10 | 11 | @pytest.mark.functional 12 | def test_should_consolidate_build_status_with_failure_count(qtbot): 13 | aggregate_status = BuildIcons().for_aggregate_status('Success.Building', "1") 14 | assert aggregate_status is not None 15 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py37-pyqt5 3 | 4 | [testenv] 5 | deps=-rtest-requirements.txt 6 | commands= 7 | pytest -vv -s -l {posargs} 8 | flake8 /buildnotify 9 | setenv= 10 | pyqt5: PYTEST_QT_API=pyqt5 11 | passenv=DISPLAY XAUTHORITY USER USERNAME 12 | 13 | [testenv:coverage] 14 | changedir=. 15 | deps=-rtest-requirements.txt 16 | commands= 17 | pytest -vv -s -l --cov-report=xml --cov=buildnotifylib --junitxml=reports/pytest/junit.xml {posargs} 18 | flake8 /buildnotify 19 | setenv= 20 | pyqt5: PYTEST_QT_API=pyqt5 21 | passenv=DISPLAY XAUTHORITY USER USERNAME 22 | 23 | [flake8] 24 | ignore = E501, W291 25 | max-complexity = 5 26 | -------------------------------------------------------------------------------- /test/project_builder.py: -------------------------------------------------------------------------------- 1 | from buildnotifylib.core.project import Project 2 | from buildnotifylib.config import Config 3 | 4 | 5 | class ProjectBuilder(object): 6 | def __init__(self, attrs, url='someurl', prefix=None): 7 | self.attrs = attrs 8 | self.url = url 9 | self._prefix = prefix 10 | self._timezone = Config.NONE_TIMEZONE 11 | 12 | def server(self, url): 13 | self.url = url 14 | return self 15 | 16 | def prefix(self, prefix): 17 | self._prefix = prefix 18 | return self 19 | 20 | def timezone(self, timezone): 21 | self._timezone = timezone 22 | return self 23 | 24 | def build(self): 25 | return Project(self.url, self._prefix, self._timezone, Attrs(self.attrs)) 26 | 27 | 28 | class Attrs(dict): 29 | def __missing__(self, key): 30 | return key 31 | -------------------------------------------------------------------------------- /test/serverconfig_test.py: -------------------------------------------------------------------------------- 1 | from buildnotifylib.serverconfig import ServerConfig 2 | 3 | 4 | def test_should_cleanup_url(): 5 | assert ServerConfig('http://url.com:9800/cc.xml', [], '', '', '', '').url == 'http://url.com:9800/cc.xml' 6 | assert ServerConfig('url.com:9800/cc.xml', [], '', '', '', '').url == 'http://url.com:9800/cc.xml' 7 | assert ServerConfig('url.com/cc.xml', [], '', '', '', '').url == 'http://url.com/cc.xml' 8 | assert ServerConfig('localhost:8080/cc.xml', [], '', '', '', '').url == 'http://localhost:8080/cc.xml' 9 | assert ServerConfig('http://localhost:8080/cc.xml', [], '', '', '', '').url == 'http://localhost:8080/cc.xml' 10 | assert ServerConfig('https://localhost:8080/cc.xml', [], '', '', '', '').url == 'https://localhost:8080/cc.xml' 11 | assert ServerConfig('file://localhost:8080/cc.xml', [], '', '', '', '').url == 'file://localhost:8080/cc.xml' 12 | -------------------------------------------------------------------------------- /docs/faq.md: -------------------------------------------------------------------------------- 1 | # FAQ 2 | 3 | ## What all do I need to run this on my machine? 4 | 5 | The steps specified in the [Installation](installation.md) page should help you get started. 6 | 7 | ## Why is BuildNotify not a gnome-applet anymore? 8 | 9 | Well, it turns out that there are a lot more desktop environments other than Gnome which are being actively used by users and writing an application which works on everything is a real pain. 10 | 11 | ## Does this work on Windows or Mac? 12 | 13 | Yes. The application has been rewritten in PyQt so that it can work across different environments. If it worked for your XYZ configuration, do let me know. 14 | 15 | ## Why PyQt? Why not Xyz? 16 | 17 | PyQt saved me from the pain of writing environment specific code as currently there is no uniform way of providing system tray applications which would work on KDE/Gnome/MyOwnDesktopEnvironment. Besides, it works for Windows/Mac for free. -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | # Installation instructions 2 | 3 | ## Ubuntu installation (Utopic Unicorn 14.10 and beyond) 4 | 5 | ```commandline 6 | sudo apt-get install buildnotify 7 | ``` 8 | 9 | Thanks to Daniel Lintott for getting BuildNotify integrated into the main debian archive. 10 | 11 | ## Ubuntu installation (pre-14.10 Utopic Unicorn) 12 | * Run the following commands in a terminal and then launch it from Menu > Internet > BuildNotify 13 | 14 | ```commandline 15 | sudo add-apt-repository ppa:anay/ppa 16 | sudo apt-get update 17 | sudo apt-get install python-buildnotify 18 | ``` 19 | 20 | The PPA is currently setup at [https://launchpad.net/~anay/+archive/ppa](https://launchpad.net/~anay/+archive/ppa) 21 | 22 | ## Alternate/Manual installation 23 | 24 | * Install missing dependencies from setup.sh 25 | * pip install buildnotify 26 | 27 | Once you have installed the application, [you can configure it to monitor CI servers](usage.md) -------------------------------------------------------------------------------- /test/http_connection_test.py: -------------------------------------------------------------------------------- 1 | import requests_mock 2 | 3 | from buildnotifylib.core.http_connection import HttpConnection 4 | from buildnotifylib.serverconfig import ServerConfig 5 | 6 | 7 | def test_should_pass_auth_if_provided(): 8 | with requests_mock.Mocker() as m: 9 | m.get('http://localhost:8080/cc.xml', text='content') 10 | response = HttpConnection().connect(ServerConfig('http://localhost:8080/cc.xml', [], '', '', 'user', 'pass'), 3) 11 | assert str(response) == 'content' 12 | assert m.last_request.headers.get("Authorization") 13 | 14 | 15 | def test_should_fetch_data_without_auth(): 16 | with requests_mock.Mocker() as m: 17 | m.get('http://localhost:8080/cc.xml', text='content') 18 | response = HttpConnection().connect(ServerConfig('localhost:8080/cc.xml', [], '', '', None, None), 3) 19 | assert str(response) == 'content' 20 | assert not m.last_request.headers.get("Authorization") 21 | -------------------------------------------------------------------------------- /.github/workflows/package.yml: -------------------------------------------------------------------------------- 1 | name: package 2 | on: 3 | push: 4 | branches: [master] 5 | 6 | env: 7 | BUILD_LABEL: ${{ github.run_id }} 8 | jobs: 9 | pypi: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-python@v2 14 | with: 15 | python-version: "3.8" 16 | - name: Build dist 17 | run: | 18 | pip3 install paver 19 | export BUILD_VERSION=`python3 buildnotifylib/version.py` 20 | paver dist_pypi 21 | - name: Publish to Test PyPI 22 | uses: pypa/gh-action-pypi-publish@master 23 | with: 24 | user: __token__ 25 | password: ${{ secrets.pypi_password }} 26 | repository_url: https://test.pypi.org/legacy/ 27 | - name: Publish to PyPI 28 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') 29 | uses: pypa/gh-action-pypi-publish@master 30 | with: 31 | user: __token__ 32 | password: ${{ secrets.pypi_password }} 33 | -------------------------------------------------------------------------------- /DEVELOPMENT.md: -------------------------------------------------------------------------------- 1 | # Local setup 2 | 3 | * `pyenv virtualenv 3.8.5 buildnotify` 4 | * `pyenv activate buildnotify` 5 | * `pip install -r test-requirements.txt` 6 | * `tox` for running tests 7 | * `pip install -e .` for installing locally. Use `python buildnotifyapplet.py` to launch 8 | * `paver mk_resources` is used to regenerate source files corresponding to icons/dialogs. 9 | 10 | Paver commands can be viewed by running `paver -h`. Run `pip install paver` if not installed. 11 | 12 | ## Editing the UI 13 | 14 | To edit the dialog windows, you should use [Qt Designer](https://doc.qt.io/qt-5/qtdesigner-manual.html). 15 | Once you're done, invoke the following command to regenerate its Python implementation: 16 | 17 | ```shell 18 | pyuic5 -x data/.ui -o buildnotifylib/generated/_ui.py 19 | ``` 20 | 21 | ## Packaging 22 | 23 | Dependencies for creating a pip/deb package (use virtualenv) 24 | 25 | ```shell 26 | pip install paver 27 | pip install stdeb 28 | pip install twine 29 | pip install keyrings.alt 30 | apt-get install debhelper dput 31 | ``` 32 | -------------------------------------------------------------------------------- /docs/usage.md: -------------------------------------------------------------------------------- 1 | # How to use 2 | 3 | Once installed, you should be able to launch Buildnotify using the launcher. You should see a new icon in the notification tray. 4 | 5 | Right click and configure as per the instructions below. 6 | 7 | ## Configuration 8 | 9 | Given a url pointing to cctray.xml, BuildNotify notifies you of any changes in the project status for selected projects in the CI server. 10 | 11 | Add a new server by clicking the `+` sign 12 | [![Servers](images/servers.png)]() 13 | 14 | Customize notifications that you'd like to see 15 | [![Notifications](images/notifications.png)]() 16 | 17 | Tweak configuration 18 | [![Misc configuration](images/misc.png)]() 19 | 20 | 21 | ## Tray Menu 22 | * Each project is represented with an icon indicating the last build status. 23 | * If the build is still in progress, an activity indicator icon is used to indicate the server activity. 24 | * All projects in the configured CI servers contribute to the overall build status which is displayed in the tray. 25 | * Clicking on any project in the tray menu would take you to the project page on the CI server. 26 | -------------------------------------------------------------------------------- /test/integration/appui_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import re 3 | from PyQt5 import QtWidgets 4 | 5 | from buildnotifylib.app_ui import AppUi 6 | from buildnotifylib.build_icons import BuildIcons 7 | from buildnotifylib.core.projects import OverallIntegrationStatus 8 | from buildnotifylib.core.continous_integration_server import ContinuousIntegrationServer 9 | from test.fake_conf import ConfigBuilder 10 | from test.project_builder import ProjectBuilder 11 | 12 | 13 | @pytest.mark.functional 14 | def test_should_update_tooltip_on_poll(qtbot): 15 | conf = ConfigBuilder().build() 16 | widget = AppUi(QtWidgets.QWidget(), conf, BuildIcons()) 17 | qtbot.addWidget(widget) 18 | project1 = ProjectBuilder({ 19 | 'name': 'a', 20 | 'lastBuildStatus': 'Success', 21 | 'activity': 'Sleeping', 22 | 'lastBuildTime': '2016-09-17 11:31:12' 23 | }).build() 24 | servers = [ContinuousIntegrationServer('someurl', [project1])] 25 | 26 | widget.update_projects(OverallIntegrationStatus(servers)) 27 | 28 | assert re.compile("Last checked: \d{4}-\d\d-\d\d \d\d:\d\d:\d\d").match(str(widget.tray.toolTip())) is not None 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | 3 | GPL v3 License 4 | 5 | Copyright (c) 2009 Anay Nayak 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | 25 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | schedule: 9 | - cron: "0 0 * * *" 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: actions/setup-python@v2 16 | with: 17 | python-version: "3.8" 18 | - name: Python Vulture Action 19 | uses: anaynayak/python-vulture-action@v1.0 20 | with: 21 | vulture-args: buildnotifylib/ test/whitelist.py --exclude generated 22 | - name: Install dependencies 23 | run: | 24 | sudo apt-get install -y -qq xvfb curl git libxkbcommon-x11-0 herbstluftwm 25 | pip3 install tox 26 | pip3 install paver 27 | - name: Test & publish code coverage 28 | uses: paambaati/codeclimate-action@v2.6.0 29 | env: 30 | CC_TEST_REPORTER_ID: ${{secrets.CC_TEST_REPORTER_ID}} 31 | with: 32 | coverageCommand: tox -e coverage -- -m "not functional" 33 | - name: Test install 34 | run: | 35 | paver dist_pypi 36 | pip3 install dist/*.tar.gz 37 | -------------------------------------------------------------------------------- /test/server_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from buildnotifylib.core.continous_integration_server import ContinuousIntegrationServer 4 | from buildnotifylib.core.filtered_continuous_integration_server import FilteredContinuousIntegrationServer 5 | from test.project_builder import ProjectBuilder 6 | 7 | 8 | class FilteredContinuousIntegrationServerTest(unittest.TestCase): 9 | def test_should_remove_excluded_projects(self): 10 | project1 = ProjectBuilder({'name': 'a'}).build() 11 | project2 = ProjectBuilder({'name': 'b'}).build() 12 | 13 | server = FilteredContinuousIntegrationServer(ContinuousIntegrationServer("someurl", [project1, project2]), ['a']) 14 | 15 | self.assertEqual([project2], server.get_projects()) 16 | 17 | def test_prefix_shouldnt_affect_exclusion(self): 18 | project1 = ProjectBuilder({'name': 'a'}).prefix('s1').build() 19 | project2 = ProjectBuilder({'name': 'b'}).prefix('s1').build() 20 | 21 | server = FilteredContinuousIntegrationServer(ContinuousIntegrationServer("someurl", [project1, project2]), ['a']) 22 | 23 | self.assertEqual([project2], server.get_projects()) 24 | 25 | 26 | if __name__ == '__main__': 27 | unittest.main() 28 | -------------------------------------------------------------------------------- /test/integration/timed_event_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from PyQt5.QtWidgets import QWidget 3 | 4 | from buildnotifylib.core.repeat_timed_event import RepeatTimedEvent 5 | from buildnotifylib.core.timed_event import TimedEvent 6 | 7 | 8 | class TargetTimedEvent(object): 9 | def __init__(self): 10 | pass 11 | 12 | def method(self, **kwargs): 13 | pass 14 | 15 | 16 | @pytest.mark.functional 17 | def test_should_trigger_event_on_timeout(qtbot, mocker): 18 | m = mocker.patch.object(TargetTimedEvent, 'method') 19 | widget = QWidget() 20 | qtbot.addWidget(widget) 21 | event = TimedEvent(widget, TargetTimedEvent().method, 20) 22 | event.start() 23 | 24 | qtbot.waitUntil(lambda: m.assert_any_call()) 25 | 26 | 27 | @pytest.mark.functional 28 | def test_should_repeat_trigger_event(qtbot, mocker): 29 | m = mocker.patch.object(TargetTimedEvent, 'method') 30 | widget = QWidget() 31 | qtbot.addWidget(widget) 32 | target = TargetTimedEvent() 33 | event = RepeatTimedEvent(widget, target.method, 3, 10) 34 | event.start() 35 | 36 | qtbot.waitUntil(lambda: m.assert_any_call(0)) 37 | qtbot.waitUntil(lambda: m.assert_any_call(1)) 38 | qtbot.waitUntil(lambda: m.assert_any_call(2)) 39 | -------------------------------------------------------------------------------- /test/integration/buildnotify_test.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | 4 | import keyring 5 | import keyring.backend 6 | import pytest 7 | import requests_mock 8 | from PyQt5.QtWidgets import QWidget, QSystemTrayIcon 9 | 10 | from buildnotifylib import BuildNotify 11 | from test.fake_conf import ConfigBuilder 12 | from test.fake_keyring import FakeKeyring 13 | from test.utils import fake_content 14 | 15 | 16 | @pytest.mark.functional 17 | def test_should_consolidate_build_status(qtbot): 18 | with requests_mock.Mocker() as m: 19 | url = 'http://localhost:8080/cc.xml' 20 | m.get(url, text=fake_content()) 21 | keyring.set_keyring(FakeKeyring()) 22 | parent = QWidget() 23 | conf = ConfigBuilder().server(url).build() 24 | b = BuildNotify(parent, conf, 10) 25 | qtbot.addWidget(b.app) 26 | parent.show() 27 | 28 | qtbot.waitUntil(lambda: hasattr(b, 'app_ui')) 29 | 30 | def projects_loaded(): 31 | assert len([str(a.text()) for a in b.app_ui.app_menu.menu.actions()]) == 11 32 | 33 | if QSystemTrayIcon.isSystemTrayAvailable(): 34 | qtbot.waitUntil(lambda: re.compile("Last checked.*").match(b.app_ui.tray.toolTip()) is not None, timeout=5000) 35 | qtbot.waitUntil(projects_loaded) 36 | -------------------------------------------------------------------------------- /test/fake_conf.py: -------------------------------------------------------------------------------- 1 | from buildnotifylib.config import Config 2 | 3 | 4 | class FakeSettings(object): 5 | def __init__(self, settings=None): 6 | if settings is None: 7 | settings = {} 8 | self.settings = settings 9 | 10 | def setValue(self, key, val): 11 | self.settings[key] = val 12 | 13 | def value(self, key, fallback=None, type=None): 14 | return self.settings.get(key, fallback) 15 | 16 | 17 | class ConfigBuilder(object): 18 | def __init__(self, overrides=None): 19 | if overrides is None: 20 | overrides = {} 21 | self.conf = { 22 | 'sort_by_name': True, 23 | 'values/lastBuildTimeForProject': False 24 | } 25 | self._merge(overrides) 26 | 27 | def server(self, url, overrides=None): 28 | if overrides is None: 29 | overrides = {} 30 | urls = self.conf.get('connection/urls', []) 31 | urls.append(url) 32 | self._merge({'connection/urls': urls}) 33 | self._merge({'display_prefix/%s' % url: ""}) 34 | self._merge(overrides) 35 | return self 36 | 37 | def build(self): 38 | return Config(FakeSettings(self.conf)) 39 | 40 | def _merge(self, overrides): 41 | self.conf = dict(self.conf, **overrides) 42 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | BuildNotify 2 | =========== 3 | 4 | BuildNotify is a CCMenu/CCTray equivalent for Ubuntu. It resides in your system tray and notifies you of the build status for different projects on your continuous integration servers. BuildNotify is largely inspired from the awesome CCMenu available for Mac. 5 | 6 | Features 7 | ======== 8 | 9 | * Monitor projects on multiple CruiseControl continuous integration servers. 10 | * Access to overall continuous integration status from the system tray. 11 | * Access individual project pages through the tray menu. 12 | * Receive notifications for fixed/broken/still failing builds. 13 | * Easy access to the last build time for each project 14 | * Customize build notifications. 15 | 16 | .. image:: https://anaynayak.github.io/buildnotify/images/projectlist.png 17 | 18 | Building from source 19 | ==================== 20 | 21 | The ubuntu package is pretty old! You can use the pypi package which is in sync with latest releases. 22 | 23 | To do so do the following:: 24 | 25 | pipx run --spec=buildnotify buildnotifyapplet.py 26 | 27 | this will launch buildnotifyapplet.py and show a icon in the menubar. 28 | 29 | 30 | Installing from PyPI 31 | ==================== 32 | 33 | ``pip install buildnotify --pre`` 34 | 35 | Launch using ``.local/bin/buildnotifyapplet.py`` 36 | 37 | 38 | Supported continuous integration systems 39 | ======================================== 40 | - See https://cctray.org/servers/ 41 | 42 | -------------------------------------------------------------------------------- /snap/snapcraft.yaml: -------------------------------------------------------------------------------- 1 | name: buildnotify-test 2 | version: "1.0+git" 3 | summary: A system tray based build status notification app for cctray.xml feed. 4 | description: | 5 | BuildNotify is a system tray notification app for continuous integration build status changes. 6 | grade: devel 7 | confinement: devmode 8 | base: core18 9 | icon: icons/buildnotify.svg 10 | architectures: [amd64] 11 | apps: 12 | buildnotify: 13 | command: desktop-launch $SNAP/bin/buildnotifyapplet.py 14 | extensions: 15 | - kde-neon 16 | plugs: 17 | - desktop 18 | - desktop-legacy 19 | - wayland 20 | - x11 21 | - unity7 22 | - network 23 | - home 24 | - opengl 25 | desktop: share/applications/buildnotify.desktop 26 | 27 | environment: 28 | LC_ALL: C.UTF-8 29 | LANG: C.UTF-8 30 | 31 | parts: 32 | buildnotify: 33 | plugin: python 34 | python-version: python3 35 | source: . 36 | build-packages: 37 | - python3 38 | - python3-pyqt5 39 | - python-keyring 40 | - execstack 41 | stage-packages: 42 | - libqt53dextras5 43 | - libqt53dquick5 44 | - libqt53danimation5 45 | - libqt53dquickscene2d5 46 | - libqt5webview5 47 | - libatk1.0-0 48 | - libgtk-3-0 49 | - libspeechd2 50 | - qtwebengine5-dev 51 | override-prime: | 52 | snapcraftctl prime 53 | sed -i 's|Icon=/usr/share/pixmaps/buildnotify.svg|Icon=${SNAP}/share/pixmaps/buildnotify.svg|g' share/applications/buildnotify.desktop 54 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # BuildNotify 2 | 3 | [![PyPI](https://img.shields.io/pypi/v/buildnotify.svg?maxAge=86400)]() 4 | [![python-buildnotify](https://img.shields.io/badge/deb-0.3.5-green.svg?style=flat)]() 5 | [![PyPI](https://img.shields.io/pypi/l/Buildnotify.svg)]() 6 | [![PyPI status](https://img.shields.io/pypi/status/buildnotify.svg)]() 7 | [![GitHub issues](https://img.shields.io/github/issues/anaynayak/buildnotify.svg)]() 8 | [![CI](https://github.com/anaynayak/buildnotify/workflows/ci/badge.svg)]() 9 | [![Coverage Status](https://img.shields.io/codeclimate/c/anaynayak/buildnotify.svg?maxAge=3600)]() 10 | [![Maintainability](https://img.shields.io/codeclimate/maintainability/anaynayak/buildnotify.svg?maxAge=3600)]() 11 | 12 | BuildNotify is a CCMenu/CCTray equivalent for Ubuntu. It resides in your system tray and notifies you of the build status for different projects on your continuous integration servers. BuildNotify is largely inspired from the awesome CCMenu available for Mac. 13 | 14 | * [Installation](installation.md) 15 | * [Frequently asked questions](faq.md) 16 | * [Configuration and usage](usage.md) 17 | 18 | # Features 19 | 20 | * Monitor projects on multiple CruiseControl continuous integration servers. 21 | * Access to overall continuous integration status from the system tray. 22 | * Access individual project pages through the tray menu. 23 | * Receive notifications for fixed/broken/still failing builds. 24 | * Easy access to the last build time for each project 25 | * Customize build notifications. 26 | 27 | [![Project List](images/projectlist.png)]() 28 | -------------------------------------------------------------------------------- /data/cctray.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from os import getenv 4 | 5 | from setuptools import setup 6 | 7 | 8 | def readme(): 9 | with open('README.rst') as f: 10 | return f.read() 11 | 12 | 13 | def version(): 14 | return getenv('BUILD_VERSION', "2.1.0") 15 | 16 | 17 | setup(name='BuildNotify', 18 | version=version(), 19 | description='Cruise Control build monitor for Windows/Linux/Mac', 20 | keywords='cctray ccmenu buildnotify ubuntu linux cruisecontrol continuous integration ci', 21 | author='Anay Nayak', 22 | install_requires=['pytz', 'PyQt5==5.15.9', 'python-dateutil', 'requests'], 23 | author_email='anayak007@gmail.com', 24 | url="https://anaynayak.github.io/buildnotify/", 25 | license='GPL v3', 26 | long_description=readme(), 27 | packages=['buildnotifylib', 'buildnotifylib.core', 28 | 'buildnotifylib.generated'], 29 | package_dir={'buildnotifylib': 'buildnotifylib'}, 30 | data_files=[ 31 | ('share/applications', ['buildnotify.desktop']), 32 | ('share/pixmaps', ['buildnotify.svg']), 33 | ], 34 | classifiers=[ 35 | 'Intended Audience :: Developers', 36 | 'Development Status :: 5 - Production/Stable', 37 | 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', 38 | 'Topic :: Software Development', 39 | 'Programming Language :: Python', 40 | 'Intended Audience :: Developers', 41 | 'Intended Audience :: End Users/Desktop', 42 | 'Environment :: X11 Applications :: Qt', 43 | 'Topic :: Software Development :: Build Tools', 44 | 'Topic :: Software Development :: User Interfaces', 45 | 'Topic :: Software Development :: Widget Sets' 46 | ], 47 | scripts=['buildnotifyapplet.py']) 48 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | ## Unreleased 2 | 3 | ### Added 4 | - Option to show last build label by @Deuchnord 5 | 6 | ## [2.1.0] - 2021-01-03 7 | 8 | ### Tech 9 | - Type declarations, mypy for static type checker 10 | - Use github actions for running builds 11 | - Bump dependencies 12 | 13 | ### Added 14 | - Show context menu on left click for non-darwin platforms 15 | - Bearer token authentication by @Deuchnord 16 | - Enhance config windows UX by @Deuchnord 17 | - Snaps are automatically pushed to https://snapcraft.io/buildnotify-test 18 | 19 | ## [2.0.0] - 2019-08-16 20 | 21 | ### Added 22 | - Bump to Python3 23 | 24 | ## [1.0.4] - 2017-12-14 25 | 26 | ### Added 27 | - Codeclimate for test coverage 28 | - Deployment to testpypi 29 | 30 | ### Fixed 31 | - Add http scheme in cctray.xml feed if missing 32 | - Preferences dialog uses available space on resize 33 | - Server configuration without exclusion no longer causes failure 34 | 35 | ## [1.0.3] - 2017-12-14 36 | 37 | ### Fixed 38 | - Fixed bug with url scheme detection. 39 | 40 | ## [1.0.2] - 2017-12-13 41 | 42 | ### Fixed 43 | - Fixed blocker bug with .desktop file leading to installation failure 44 | 45 | 46 | ## [1.0.1] - 2017-12-09 47 | 48 | ### Fixed 49 | - Fixed bug with missing icon 50 | - App crash on network connectivity issue 51 | 52 | 53 | ## [1.0.0] - 2017-08-17 54 | 55 | ### Added 56 | - Sort projects by last build time or name 57 | - Allow selection/deselection of all projects in configuration screen 58 | - Using keychain to manage passwords 59 | - Support for self-signed certificates 60 | - Per CI server build label prefix 61 | - Support for projects without URL scheme specified in the cctray.xml feed 62 | - Upgrade to PyQt5 63 | ### Changed 64 | - Allow polling unit to seconds from minutes. 65 | - List of projects can now be dragged to extend the visible list 66 | ### Fixed 67 | - Encoding of username/password for basic authentication 68 | - Projects with same name across CI servers are handled correctly 69 | -------------------------------------------------------------------------------- /pavement.py: -------------------------------------------------------------------------------- 1 | from paver.easy import task, needs, path, sh 2 | 3 | 4 | @task 5 | def clean(): 6 | for fl in ['BuildNotify.egg-info', 'build', 'dist', 'deb_dist']: 7 | p = path(fl) 8 | p.rmtree() 9 | 10 | 11 | @task 12 | def mk_resources(): 13 | """Regenerate source corresponding to icons/Qt designer files""" 14 | sh('pyuic5 -o buildnotifylib/generated/preferences_ui.py data/preferences.ui') 15 | sh('pyuic5 -o buildnotifylib/generated/server_configuration_ui.py data/server_configuration.ui') 16 | sh('pyrcc5 icons/icons.qrc -o buildnotifylib/generated/icons_rc.py') 17 | 18 | 19 | @task 20 | @needs('dist_pypi', 'dist_ppa') 21 | def dist(): 22 | """Triggers dist_pypi and dist_ppa""" 23 | pass 24 | 25 | 26 | @task 27 | @needs('clean') 28 | def dist_pypi(): 29 | sh('python3 setup.py sdist') 30 | 31 | 32 | @task 33 | @needs('dist_pypi') 34 | def release_pypi(): 35 | """Upload package to https://pypi.python.org/pypi/BuildNotify/""" 36 | # Set TWINE_USERNAME, TWINE_PASSWORD 37 | pkg = path('dist').files('*.tar.gz')[0].name 38 | # sh('twine upload --sign dist/' + pkg) 39 | sh('twine upload dist/' + pkg) 40 | 41 | 42 | @task 43 | @needs('clean', 'mk_deb') 44 | def dist_ppa(): 45 | """Upload package to https://launchpad.net/~anay/+archive/ppa""" 46 | sh('python3 setup.py --command-packages=stdeb.command sdist_dsc --force-buildsystem=False') 47 | dist_package = path('deb_dist').dirs('buildnotify-*')[0] 48 | sh('sed -i s/unstable/precise/ %s/debian/changelog' % dist_package) 49 | sh('cd %s;dpkg-buildpackage -i -S -I -rfakeroot' % dist_package) 50 | changes_file = path('deb_dist').files('*.changes')[0] 51 | sh('dput ppa:anay/ppa %s' % changes_file) 52 | 53 | 54 | @task 55 | @needs('clean') 56 | def mk_deb(): 57 | """Build deb package""" 58 | sh('python3 setup.py --command-packages=stdeb.command bdist_deb') 59 | 60 | 61 | @task 62 | @needs('clean') 63 | def mk_osc(): 64 | """Upload package to https://build.opensuse.org/package/repositories/home:Anay/BuildNotifyTest""" 65 | sh('python3 setup.py sdist') 66 | sh('python3 setup.py --command-packages=stdeb.command sdist_dsc --force-buildsystem=False') 67 | dist_package = path('deb_dist').dirs('buildnotify-*')[0] 68 | sh('rm %s' % path('../BuildNotifyTest').files('*.tar.gz')[0]) 69 | sh('rm %s' % path('../BuildNotifyTest').files('*.dsc')[0]) 70 | sh('cp %s/debian/changelog ../BuildNotifyTest/debian.changelog' % dist_package) 71 | sh('cp %s/debian/control ../BuildNotifyTest/debian.control' % dist_package) 72 | sh('cp %s/debian/rules ../BuildNotifyTest/debian.rules' % dist_package) 73 | sh('cp %s ../BuildNotifyTest/' % path('dist').files('BuildNotify-*')[0]) 74 | sh('cp %s ../BuildNotifyTest/' % path('deb_dist').files('buildnotify*.dsc')[0]) 75 | sh('osc addremove ../BuildNotifyTest/') 76 | sh('osc commit ../BuildNotifyTest/ -m"Updated package"') 77 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at anayak007+buildnotify@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /test/integration/config_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from PyQt5 import QtCore 4 | 5 | from buildnotifylib.config import Config, Preferences 6 | from buildnotifylib.serverconfig import ServerConfig 7 | 8 | 9 | class ConfigTest(unittest.TestCase): 10 | def setUp(self): 11 | q_settings = QtCore.QSettings("BuildNotifyTest", "BuildNotifyTest") 12 | q_settings.clear() 13 | self.config = Config(q_settings) 14 | 15 | def test_should_persist_user_project_excludes(self): 16 | self.config.set_project_excludes('https://github.com/anaynayak/buildnotify/cctray.xml', 17 | ['buildnotify::test-server']) 18 | self.assertEqual(['buildnotify::test-server'], 19 | self.config.get_project_excludes('https://github.com/anaynayak/buildnotify/cctray.xml')) 20 | 21 | def test_should_persist_empty_user_project_excludes(self): 22 | self.config.set_project_excludes('https://github.com/anaynayak/buildnotify/cctray.xml', []) 23 | self.assertEqual([], self.config.get_project_excludes('https://github.com/anaynayak/buildnotify/cctray.xml')) 24 | 25 | def test_should_return_an_empty_list_if_unmapped(self): 26 | self.assertEqual([], 27 | self.config.get_project_excludes('https://github.com/anaynayak/buildnotify/buildnotify.xml')) 28 | 29 | def test_should_store_server_preferences(self): 30 | self.config.save_server_config( 31 | ServerConfig('https://github.com/anaynayak/buildnotify/cctray.xml', ['excludedproject'], 'EDT', 'prefix', 32 | 'user', 'pass')) 33 | server = self.config.get_server_config("https://github.com/anaynayak/buildnotify/cctray.xml") 34 | self.assertEqual('user', server.username) 35 | self.assertEqual('pass', server.password) 36 | self.assertEqual("https://github.com/anaynayak/buildnotify/cctray.xml", server.url) 37 | self.assertEqual('EDT', server.timezone) 38 | self.assertEqual(['excludedproject'], server.excluded_projects) 39 | self.assertEqual('prefix', server.prefix) 40 | self.assertEqual(["https://github.com/anaynayak/buildnotify/cctray.xml"], self.config.get_urls()) 41 | 42 | def test_should_get_empty_if_missing(self): 43 | server = self.config.get_server_config('someurl') 44 | self.assertEqual('', server.username) 45 | 46 | def test_should_return_all_servers(self): 47 | self.config.save_server_config(ServerConfig('url1', [], 'EDT', 'prefix', 'user', 'pass')) 48 | self.config.save_server_config(ServerConfig('url2', [], 'EDT', 'prefix', 'user', 'pass')) 49 | servers = self.config.get_server_configs() 50 | self.assertEqual(2, len(servers)) 51 | self.assertEqual('http://url1', servers[0].url) 52 | self.assertEqual('http://url2', servers[1].url) 53 | 54 | def test_should_update_preferences(self): 55 | self.config.update_preferences(Preferences(['url1'], 300, '/bin/sh', True, False, True, [], False)) 56 | 57 | self.assertEqual(self.config.get_urls(), ['url1']) 58 | self.assertEqual(self.config.get_interval_in_seconds(), 300) 59 | self.assertEqual(self.config.get_custom_script(), '/bin/sh') 60 | self.assertEqual(self.config.get_custom_script_enabled(), True) 61 | self.assertEqual(self.config.get_sort_by_last_build_time(), False) 62 | self.assertEqual(self.config.get_sort_by_name(), True) 63 | self.assertEqual(self.config.get_show_last_build_label(), False) 64 | 65 | 66 | if __name__ == '__main__': 67 | unittest.main() 68 | -------------------------------------------------------------------------------- /test/integration/preferences_dialog_test.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | from PyQt5 import QtCore 5 | from PyQt5.QtCore import QItemSelectionModel, Qt 6 | from PyQt5.QtWidgets import QDialogButtonBox 7 | 8 | from buildnotifylib.preferences import PreferencesDialog 9 | from buildnotifylib.server_configuration_dialog import ServerConfigurationDialog 10 | from test.fake_conf import ConfigBuilder 11 | 12 | 13 | @pytest.mark.functional 14 | @pytest.mark.requireshead 15 | def test_should_show_configured_urls(qtbot): 16 | file_path = os.path.realpath(os.path.dirname(os.path.abspath(__file__)) + "../../../data/cctray.xml") 17 | conf = ConfigBuilder().server("file://" + file_path).build() 18 | dialog = PreferencesDialog(conf) 19 | qtbot.addWidget(dialog) 20 | assert [str(s) for s in dialog.ui.cctrayPathList.model().stringList()] == ["file://" + file_path] 21 | 22 | 23 | @pytest.mark.functional 24 | @pytest.mark.requireshead 25 | def test_should_show_configure_notifications(qtbot): 26 | file_path = os.path.realpath(os.path.dirname(os.path.abspath(__file__)) + "../../../data/cctray.xml") 27 | conf = ConfigBuilder().server("file://" + file_path).build() 28 | dialog = PreferencesDialog(conf) 29 | qtbot.addWidget(dialog) 30 | dialog.show() 31 | dialog.ui.tabWidget.setCurrentIndex(1) 32 | assert dialog.ui.connectivityIssuesCheckbox.isChecked() 33 | assert dialog.ui.fixedBuildsCheckbox.isChecked() 34 | assert dialog.ui.brokenBuildsCheckbox.isChecked() 35 | assert not dialog.ui.successfulBuildsCheckbox.isChecked() 36 | assert not dialog.ui.scriptCheckbox.isChecked() 37 | assert dialog.ui.scriptLineEdit.text() == 'echo #status# #projects# >> /tmp/buildnotify.log' 38 | 39 | 40 | @pytest.mark.functional 41 | @pytest.mark.requireshead 42 | def test_should_return_preferences_on_accept(qtbot): 43 | conf = ConfigBuilder().build() 44 | dialog = PreferencesDialog(conf) 45 | qtbot.addWidget(dialog) 46 | 47 | def close_dialog(): 48 | button = dialog.ui.buttonBox.button(QDialogButtonBox.Ok) 49 | qtbot.mouseClick(button, QtCore.Qt.LeftButton) 50 | 51 | QtCore.QTimer.singleShot(100, close_dialog) 52 | preferences = dialog.open() 53 | 54 | qtbot.waitUntil(lambda: preferences is not None) 55 | 56 | 57 | @pytest.mark.functional 58 | def test_should_prefill_server_config(qtbot, mocker): 59 | file_path = os.path.realpath(os.path.dirname(os.path.abspath(__file__)) + "../../../data/cctray.xml") 60 | conf = ConfigBuilder().server("file://" + file_path).build() 61 | dialog = PreferencesDialog(conf) 62 | qtbot.addWidget(dialog) 63 | dialog.show() 64 | 65 | index = dialog.ui.cctrayPathList.model().index(0, 0) 66 | dialog.ui.cctrayPathList.selectionModel().select(index, QItemSelectionModel.Select) 67 | dialog.ui.cctrayPathList.setCurrentIndex(index) 68 | dialog.item_selection_changed(True) 69 | 70 | m = mocker.patch.object(ServerConfigurationDialog, 'open') 71 | 72 | qtbot.mouseClick(dialog.ui.configureProjectButton, Qt.LeftButton) 73 | 74 | qtbot.waitUntil(lambda: m.assert_any_call()) 75 | 76 | 77 | @pytest.mark.functional 78 | @pytest.mark.requireshead 79 | def test_should_remove_configured_servers(qtbot): 80 | file_path = os.path.realpath(os.path.dirname(os.path.abspath(__file__)) + "../../../data/cctray.xml") 81 | conf = ConfigBuilder().server("file://" + file_path).build() 82 | dialog = PreferencesDialog(conf) 83 | qtbot.addWidget(dialog) 84 | dialog.show() 85 | 86 | index = dialog.ui.cctrayPathList.model().index(0, 0) 87 | dialog.ui.cctrayPathList.selectionModel().select(index, QItemSelectionModel.Select) 88 | dialog.ui.cctrayPathList.setCurrentIndex(index) 89 | dialog.item_selection_changed(True) 90 | 91 | qtbot.mouseClick(dialog.ui.removeButton, Qt.LeftButton) 92 | 93 | assert [str(s) for s in dialog.ui.cctrayPathList.model().stringList()] == [] 94 | -------------------------------------------------------------------------------- /test/projects_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from buildnotifylib.core.continous_integration_server import ContinuousIntegrationServer 4 | from buildnotifylib.core.projects import OverallIntegrationStatus, ProjectLoader 5 | from buildnotifylib.serverconfig import ServerConfig 6 | from .project_builder import ProjectBuilder 7 | from io import StringIO 8 | 9 | 10 | class OverallIntegrationStatusTest(unittest.TestCase): 11 | def test_should_consolidate_build_status(self): 12 | project1 = ProjectBuilder({'name': 'a', 'lastBuildStatus': 'Success', 'activity': 'Sleeping'}).build() 13 | project2 = ProjectBuilder({'name': 'a', 'lastBuildStatus': 'Success', 'activity': 'Sleeping'}).build() 14 | status = OverallIntegrationStatus([ContinuousIntegrationServer("someurl", [project1, project2])]) 15 | self.assertEqual('Success.Sleeping', status.get_build_status()) 16 | 17 | def test_should_mark_failed_if_even_one_failed(self): 18 | project1 = ProjectBuilder({'name': 'a', 'lastBuildStatus': 'Success', 'activity': 'Sleeping'}).build() 19 | project2 = ProjectBuilder({'name': 'a', 'lastBuildStatus': 'Failure', 'activity': 'Sleeping'}).build() 20 | status = OverallIntegrationStatus([ContinuousIntegrationServer("someurl", [project1, project2])]) 21 | self.assertEqual('Failure.Sleeping', status.get_build_status()) 22 | 23 | def test_should_mark_failed_if_even_one_failed_across_servers(self): 24 | project1 = ProjectBuilder({'name': 'a', 'lastBuildStatus': 'Success', 'activity': 'Sleeping'}).build() 25 | project2 = ProjectBuilder({'name': 'a', 'lastBuildStatus': 'Failure', 'activity': 'Sleeping'}).build() 26 | status = OverallIntegrationStatus([ 27 | ContinuousIntegrationServer("url1", [project1]), 28 | ContinuousIntegrationServer('url2', [project2]) 29 | ]) 30 | self.assertEqual('Failure.Sleeping', status.get_build_status()) 31 | 32 | def test_should_identify_failing_builds(self): 33 | project1 = ProjectBuilder({'name': 'a', 'lastBuildStatus': 'Success', 'activity': 'Sleeping'}).build() 34 | project2 = ProjectBuilder({'name': 'a', 'lastBuildStatus': 'Failure', 'activity': 'Sleeping'}).build() 35 | status = OverallIntegrationStatus([ContinuousIntegrationServer("someurl", [project1, project2])]) 36 | self.assertEqual([project2], status.get_failing_builds()) 37 | 38 | 39 | class MockConnection(object): 40 | def __init__(self, data): 41 | self.data = data 42 | 43 | def connect(self, server, timeout, additional_headers=None): 44 | return self.data 45 | 46 | 47 | class ProjectLoaderTest(unittest.TestCase): 48 | def test_should_load_feed(self): 49 | connection = MockConnection(""" 50 | 51 | 56 | """) 57 | response = ProjectLoader(ServerConfig('url', [], '', '', '', ''), 10, connection).get_data() 58 | projects = response.server.get_projects() 59 | self.assertEqual(1, len(projects)) 60 | self.assertEqual("project", projects[0].name) 61 | self.assertEqual("Sleeping", projects[0].activity) 62 | self.assertEqual("Success", projects[0].status) 63 | self.assertEqual(False, response.server.unavailable) 64 | 65 | def test_should_respond_even_if_things_fail(self): 66 | class MockConnection(object): 67 | def __init__(self): 68 | pass 69 | 70 | def connect(self, server, timeout): 71 | raise Exception("something went wrong") 72 | 73 | response = ProjectLoader(ServerConfig('url', [], '', '', '', ''), 10, MockConnection()).get_data() 74 | projects = response.server.get_projects() 75 | self.assertEqual(0, len(projects)) 76 | self.assertEqual(True, response.server.unavailable) 77 | 78 | def test_should_set_display_prefix(self): 79 | connection = MockConnection(""" 80 | 81 | 86 | """) 87 | response = ProjectLoader(ServerConfig('url', [], '', 'RELEASE', '', ''), 10, connection).get_data() 88 | projects = response.server.get_projects() 89 | self.assertEqual(1, len(projects)) 90 | self.assertEqual("[RELEASE] project", projects[0].label()) 91 | 92 | 93 | if __name__ == '__main__': 94 | unittest.main() 95 | -------------------------------------------------------------------------------- /test/project_test.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import unittest 3 | from datetime import timedelta 4 | 5 | from dateutil.tz import tzlocal 6 | 7 | from buildnotifylib.core.project import Project 8 | 9 | 10 | class ProjectTest(unittest.TestCase): 11 | def test_should_ignore_empty_last_build_time(self): 12 | project = Project('i', None, 'None', { 13 | 'lastBuildTime': '', 14 | 'name': 'g', 15 | 'lastBuildStatus': 'n', 16 | 'activity': 'o', 17 | 'url': 'r'}) 18 | self.assertEqual(datetime.datetime.now().date(), project.get_last_build_time().date()) 19 | self.assertEqual(tzlocal(), project.get_last_build_time().tzinfo) 20 | 21 | def test_should_correctly_parse_project(self): 22 | project = Project('url', None, 'None', { 23 | 'name': 'proj1', 24 | 'lastBuildStatus': 'Success', 25 | 'activity': 'Sleeping', 26 | 'url': '1.2.3.4:8080/cc.xml', 27 | 'lastBuildLabel': '120', 28 | 'lastBuildTime': '2009-05-29T13:54:07' 29 | }) 30 | 31 | self.assertEqual('url', project.server_url) 32 | self.assertEqual('proj1', project.name) 33 | self.assertEqual('Success', project.status) 34 | self.assertEqual('http://1.2.3.4:8080/cc.xml', project.url) 35 | self.assertEqual('Sleeping', project.activity) 36 | self.assertEqual('2009-05-29T13:54:07', project.last_build_time) 37 | self.assertEqual('120', project.last_build_label) 38 | self.assertEqual(datetime.datetime(2009, 5, 29, 13, 54, 7, 0, tzlocal()), project.get_last_build_time()) 39 | self.assertEqual("Success.Sleeping", project.get_build_status()) 40 | 41 | def test_should_display_last_build_label_on_demand(self): 42 | project = Project('url', None, 'None', { 43 | 'name': 'proj1', 44 | 'lastBuildStatus': 'Success', 45 | 'activity': 'Sleeping', 46 | 'url': '1.2.3.4:8080/cc.xml', 47 | 'lastBuildLabel': 'master', 48 | 'lastBuildTime': '2009-05-29T13:54:07' 49 | }) 50 | 51 | self.assertEqual('proj1', project.label()) 52 | self.assertEqual('proj1 (master)', project.label(True)) 53 | 54 | def test_should_not_override_existing_url_scheme(self): 55 | project = Project('url', '', 'tz', { 56 | 'name': 'proj1', 57 | 'lastBuildStatus': 'Success', 58 | 'activity': 'Sleeping', 59 | 'url': 'https://10.0.0.1/project1', 60 | 'lastBuildLabel': '120', 61 | 'lastBuildTime': '2009-05-29T13:54:07' 62 | }) 63 | self.assertEqual('https://10.0.0.1/project1', project.url) 64 | 65 | 66 | class ProjectTimezoneTest(unittest.TestCase): 67 | @classmethod 68 | def tzproj(cls, time, timezone='None'): 69 | return Project('url', None, timezone, { 70 | 'name': 'proj1', 71 | 'lastBuildStatus': 'Success', 72 | 'activity': 'Sleeping', 73 | 'url': 'someurl', 74 | 'lastBuildLabel': '120', 75 | 'lastBuildTime': time 76 | }) 77 | 78 | def test_should_retain_original_tz_offset(self): 79 | project = ProjectTimezoneTest.tzproj('2015-02-14T13:23:20+05:30') 80 | build_time = project.get_last_build_time() 81 | self.assertEqual(datetime.datetime(2015, 2, 14, 13, 23, 20, 0, None), 82 | build_time.replace(tzinfo=None)) 83 | self.assertEqual(build_time.utcoffset(), timedelta(hours=5, minutes=30)) 84 | 85 | def test_should_consider_other_variants1(self): 86 | project = ProjectTimezoneTest.tzproj('2015-02-14T13:25:53Z') 87 | build_time = project.get_last_build_time() 88 | self.assertEqual(datetime.datetime(2015, 2, 14, 13, 25, 53, 0, None), 89 | build_time.replace(tzinfo=None)) 90 | self.assertEqual(build_time.utcoffset(), timedelta(hours=0, minutes=0)) 91 | 92 | def test_should_consider_other_variants2(self): 93 | project = ProjectTimezoneTest.tzproj('2015-02-14T13:27:20.000+0000') 94 | build_time = project.get_last_build_time() 95 | self.assertEqual(datetime.datetime(2015, 2, 14, 13, 27, 20, 0, None), 96 | build_time.replace(tzinfo=None)) 97 | self.assertEqual(build_time.utcoffset(), timedelta(hours=0, minutes=0)) 98 | 99 | def test_should_consider_other_variants3(self): 100 | project = ProjectTimezoneTest.tzproj('2015-02-14T13:23:20+00:00', 'None') 101 | build_time = project.get_last_build_time() 102 | self.assertEqual(datetime.datetime(2015, 2, 14, 13, 23, 20, 0, None), 103 | build_time.replace(tzinfo=None)) 104 | self.assertEqual(build_time.utcoffset(), timedelta(hours=0, minutes=0)) 105 | 106 | def test_should_take_local_timezone_if_unspecified(self): 107 | project = ProjectTimezoneTest.tzproj('2015-02-14T13:23:20', 'None') 108 | build_time = project.get_last_build_time() 109 | self.assertEqual(datetime.datetime(2015, 2, 14, 13, 23, 20, 0, None), 110 | build_time.replace(tzinfo=None)) 111 | self.assertEqual(build_time.tzinfo, tzlocal()) 112 | 113 | def test_should_override_timezone(self): 114 | project = ProjectTimezoneTest.tzproj('2015-02-14T13:23:20+05:30', 'Etc/GMT-5') 115 | build_time = project.get_last_build_time() 116 | self.assertEqual(datetime.datetime(2015, 2, 14, 13, 23, 20, 0, None), 117 | build_time.replace(tzinfo=None)) 118 | self.assertEqual(build_time.utcoffset(), timedelta(hours=5, minutes=0)) 119 | 120 | 121 | if __name__ == '__main__': 122 | unittest.main() 123 | -------------------------------------------------------------------------------- /test/integration/server_configuration_dialog_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import requests_mock 3 | from PyQt5 import QtCore 4 | from PyQt5.QtCore import Qt 5 | from PyQt5.QtWidgets import QMessageBox 6 | from mock import ANY 7 | 8 | from buildnotifylib.server_configuration_dialog import ServerConfigurationDialog 9 | from buildnotifylib.core.keystore import Keystore 10 | from test.fake_conf import ConfigBuilder 11 | from test.utils import fake_content 12 | 13 | 14 | @pytest.mark.functional 15 | @pytest.mark.requireshead 16 | def test_should_show_configured_urls(qtbot): 17 | with requests_mock.Mocker() as m: 18 | url = 'http://localhost:8080/cc.xml' 19 | m.get(url, text=fake_content()) 20 | conf = ConfigBuilder().server(url).build() 21 | dialog = ServerConfigurationDialog(url, conf) 22 | dialog.show() 23 | qtbot.addWidget(dialog) 24 | qtbot.mouseClick(dialog.ui.loadUrlButton, QtCore.Qt.LeftButton) 25 | 26 | qtbot.waitUntil(lambda: dialog.ui.projectsList.model() is not None) 27 | model = dialog.ui.projectsList.model() 28 | assert model.item(0, 0).hasChildren() 29 | assert model.item(0, 0).child(0, 0).isCheckable() 30 | assert model.item(0, 0).child(0, 0).data(Qt.CheckStateRole) == Qt.Checked 31 | assert model.item(0, 0).child(0, 0).text() == "cleanup-artifacts-B" 32 | 33 | assert dialog.ui.timezoneList.currentText() == 'None' 34 | 35 | 36 | @pytest.mark.functional 37 | @pytest.mark.requireshead 38 | def test_should_save_restore_config(qtbot): 39 | with requests_mock.Mocker() as m: 40 | url = 'http://localhost:8080/cc.xml' 41 | m.get(url, text=fake_content()) 42 | conf = ConfigBuilder().server(url).build() 43 | dialog = ServerConfigurationDialog(url, conf) 44 | dialog.show() 45 | qtbot.addWidget(dialog) 46 | qtbot.mouseClick(dialog.ui.loadUrlButton, QtCore.Qt.LeftButton) 47 | 48 | qtbot.waitUntil(lambda: dialog.ui.projectsList.model() is not None) 49 | server_config = dialog.get_server_config() 50 | 51 | conf.save_server_config(server_config) 52 | dialog = ServerConfigurationDialog(url, conf) 53 | dialog.show() 54 | qtbot.addWidget(dialog) 55 | 56 | 57 | @pytest.mark.functional 58 | @pytest.mark.requireshead 59 | def test_should_exclude_projects(qtbot): 60 | with requests_mock.Mocker() as m: 61 | url = 'http://localhost:8080/cc.xml' 62 | m.get(url, text=fake_content()) 63 | conf = ConfigBuilder().server(url).build() 64 | dialog = ServerConfigurationDialog(url, conf) 65 | dialog.show() 66 | qtbot.addWidget(dialog) 67 | qtbot.mouseClick(dialog.ui.loadUrlButton, QtCore.Qt.LeftButton) 68 | 69 | qtbot.waitUntil(lambda: dialog.ui.projectsList.model() is not None) 70 | model = dialog.ui.projectsList.model() 71 | 72 | model.item(0, 0).child(0, 0).setCheckState(QtCore.Qt.Unchecked) 73 | 74 | server_config = dialog.get_server_config() 75 | assert [str(s) for s in server_config.excluded_projects] == ['cleanup-artifacts-B'] 76 | 77 | 78 | @pytest.mark.functional 79 | @pytest.mark.requireshead 80 | def test_should_preload_info(qtbot): 81 | with requests_mock.Mocker() as m: 82 | url = 'http://localhost:8080/cc.xml' 83 | m.get(url, text=fake_content()) 84 | conf = ConfigBuilder().server(url, { 85 | "excludes/%s" % url: ['cleanup-artifacts-B'], 86 | "timezone/%s" % url: 'US/Eastern', 87 | }).build() 88 | 89 | dialog = ServerConfigurationDialog(url, conf) 90 | dialog.show() 91 | qtbot.addWidget(dialog) 92 | qtbot.mouseClick(dialog.ui.loadUrlButton, QtCore.Qt.LeftButton) 93 | 94 | qtbot.waitUntil(lambda: dialog.ui.projectsList.model() is not None) 95 | model = dialog.ui.projectsList.model() 96 | 97 | assert model.item(0, 0).hasChildren() 98 | assert model.item(0, 0).child(0, 0).isCheckable() 99 | assert model.item(0, 0).child(0, 0).text() == "cleanup-artifacts-B" 100 | assert model.item(0, 0).child(0, 0).data(Qt.CheckStateRole) == Qt.Unchecked 101 | 102 | def timezone(): 103 | assert dialog.ui.timezoneList.count() > 100 104 | assert dialog.ui.timezoneList.currentText() == 'US/Eastern' 105 | 106 | qtbot.waitUntil(timezone) 107 | 108 | 109 | @pytest.mark.functional 110 | @pytest.mark.requireshead 111 | def test_should_fail_for_bad_url(qtbot, mocker): 112 | url = "file:///badpath" 113 | conf = ConfigBuilder().server(url).build() 114 | dialog = ServerConfigurationDialog(url, conf) 115 | dialog.show() 116 | qtbot.addWidget(dialog) 117 | m = mocker.patch.object(QMessageBox, 'critical', 118 | return_value=QMessageBox.No) 119 | 120 | qtbot.mouseClick(dialog.ui.loadUrlButton, QtCore.Qt.LeftButton) 121 | 122 | def alert_shown(): 123 | m.assert_called_once_with(dialog, ANY, ANY) 124 | 125 | qtbot.wait_until(alert_shown) 126 | 127 | 128 | @pytest.mark.functional 129 | @pytest.mark.requireshead 130 | def test_should_disable_authentication_if_keystore_is_unavailable(qtbot, mocker): 131 | m = mocker.patch.object(Keystore, 'is_available', 132 | return_value=False) 133 | with requests_mock.Mocker() as r: 134 | url = 'http://localhost:8080/cc.xml' 135 | r.get(url, text=fake_content()) 136 | 137 | conf = ConfigBuilder().server(url).build() 138 | dialog = ServerConfigurationDialog(url, conf) 139 | dialog.show() 140 | qtbot.addWidget(dialog) 141 | 142 | def alert_shown(): 143 | assert not dialog.ui.username.isEnabled() 144 | assert not dialog.ui.password.isEnabled() 145 | assert dialog.ui.authenticationSettings.title() == 'Authentication (keyring dependency missing)' 146 | 147 | qtbot.wait_until(alert_shown) 148 | -------------------------------------------------------------------------------- /test/integration/app_menu_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from PyQt5.QtWidgets import QWidget 3 | from datetime import datetime 4 | from dateutil.relativedelta import relativedelta 5 | 6 | from buildnotifylib.app_menu import AppMenu 7 | from buildnotifylib.preferences import PreferencesDialog 8 | from buildnotifylib.build_icons import BuildIcons 9 | from test.fake_conf import ConfigBuilder 10 | from test.project_builder import ProjectBuilder 11 | 12 | 13 | @pytest.mark.functional 14 | def test_should_set_menu_items_for_projects(qtbot): 15 | conf = ConfigBuilder().server('someurl').build() 16 | parent = QWidget() 17 | app_menu = AppMenu(parent, conf, BuildIcons()) 18 | qtbot.addWidget(parent) 19 | project1 = ProjectBuilder({ 20 | 'name': 'Project 1', 21 | 'url': 'dummyurl', 22 | 'lastBuildStatus': 'Success', 23 | 'activity': 'Sleeping', 24 | 'lastBuildTime': '2016-09-17 11:31:12' 25 | }).server('someurl').build() 26 | app_menu.update([project1]) 27 | app_menu.menu.show() 28 | 29 | assert [str(a.text()) for a in app_menu.menu.actions()] == ['Project 1', 30 | "", 31 | "About", 32 | "Preferences", 33 | "Exit"] 34 | 35 | 36 | @pytest.mark.functional 37 | def test_should_suffix_build_time(qtbot): 38 | conf = ConfigBuilder({'values/lastBuildTimeForProject': True}).build() 39 | parent = QWidget() 40 | app_menu = AppMenu(parent, conf, BuildIcons()) 41 | qtbot.addWidget(parent) 42 | one_year_ago = (datetime.now() - relativedelta(years=1, days=1)).strftime("%Y-%m-%d %H:%M:%S") 43 | project1 = ProjectBuilder({ 44 | 'name': 'Project 1', 45 | 'url': 'dummyurl', 46 | 'lastBuildStatus': 'Success', 47 | 'activity': 'Sleeping', 48 | 'lastBuildTime': one_year_ago 49 | }).timezone('US/Central').build() 50 | 51 | app_menu.update([project1]) 52 | app_menu.menu.show() 53 | 54 | assert [str(a.text()) for a in app_menu.menu.actions()] == ['Project 1, 1 year ago', 55 | "", 56 | "About", 57 | "Preferences", 58 | "Exit"] 59 | 60 | 61 | @pytest.mark.functional 62 | def test_should_sort_by_name(qtbot): 63 | conf = ConfigBuilder({'values/lastBuildTimeForProject': False, 'sort_key': 'sort_name'}).build() 64 | parent = QWidget() 65 | app_menu = AppMenu(parent, conf, BuildIcons()) 66 | qtbot.addWidget(parent) 67 | time = (datetime.now() - relativedelta(years=1)).strftime("%Y-%m-%d %H:%M:%S") 68 | project1 = ProjectBuilder({ 69 | 'name': 'BProject', 70 | 'url': 'dummyurl', 71 | 'lastBuildStatus': 'Success', 72 | 'activity': 'Sleeping', 73 | 'lastBuildTime': time 74 | }).build() 75 | 76 | project2 = ProjectBuilder({ 77 | 'name': 'AProject', 78 | 'url': 'dummyurl', 79 | 'lastBuildStatus': 'Success', 80 | 'activity': 'Sleeping', 81 | 'lastBuildTime': time 82 | }).build() 83 | 84 | app_menu.update([project1, project2]) 85 | app_menu.menu.show() 86 | 87 | assert [str(a.text()) for a in app_menu.menu.actions()] == ['AProject', 88 | 'BProject', 89 | "", 90 | "About", 91 | "Preferences", 92 | "Exit"] 93 | 94 | 95 | @pytest.mark.functional 96 | def test_should_add_display_prefix(qtbot): 97 | conf = ConfigBuilder({'values/lastBuildTimeForProject': False, 'sort_key': 'sort_name'}).server("Server1").server( 98 | "Server2").build() 99 | parent = QWidget() 100 | app_menu = AppMenu(parent, conf, BuildIcons()) 101 | qtbot.addWidget(parent) 102 | time = (datetime.now() - relativedelta(years=1)).strftime("%Y-%m-%d %H:%M:%S") 103 | project1 = ProjectBuilder({ 104 | 'name': 'BProject', 105 | 'url': 'dummyurl', 106 | 'lastBuildStatus': 'Success', 107 | 'activity': 'Sleeping', 108 | 'lastBuildTime': time 109 | }).server('Server2').prefix('R1').build() 110 | 111 | project2 = ProjectBuilder({ 112 | 'name': 'AProject', 113 | 'url': 'dummyurl', 114 | 'lastBuildStatus': 'Success', 115 | 'activity': 'Sleeping', 116 | 'lastBuildTime': time 117 | }).server('Server1').build() 118 | 119 | app_menu.update([project1, project2]) 120 | app_menu.menu.show() 121 | 122 | assert [str(a.text()) for a in app_menu.menu.actions()] == ['AProject', 123 | '[R1] BProject', 124 | "", 125 | "About", 126 | "Preferences", 127 | "Exit"] 128 | 129 | 130 | @pytest.mark.functional 131 | def test_should_consider_prefix_for_sorting(qtbot): 132 | conf = ConfigBuilder({'values/lastBuildTimeForProject': False, 'sort_key': 'sort_name'}).server("Server1").server( 133 | "Server2", { 134 | 'display_prefix/Server2': 'R1'}).build() 135 | parent = QWidget() 136 | app_menu = AppMenu(parent, conf, BuildIcons()) 137 | qtbot.addWidget(parent) 138 | time = (datetime.now() - relativedelta(years=1)).strftime("%Y-%m-%d %H:%M:%S") 139 | project1 = ProjectBuilder({ 140 | 'name': 'BProject', 'url': 'dummyurl', 'lastBuildStatus': 'Success', 141 | 'activity': 'Sleeping', 'lastBuildTime': time 142 | }).server('Server2').prefix('R1').build() 143 | 144 | project2 = ProjectBuilder({ 145 | 'name': 'AProject', 'url': 'dummyurl', 'lastBuildStatus': 'Success', 146 | 'activity': 'Sleeping', 'lastBuildTime': time 147 | }).server('Server1').prefix('R2').build() 148 | 149 | project3 = ProjectBuilder({ 150 | 'name': 'CProject', 'url': 'dummyurl', 'lastBuildStatus': 'Success', 151 | 'activity': 'Sleeping', 'lastBuildTime': time 152 | }).server('Server1').prefix('R2').build() 153 | 154 | app_menu.update([project1, project2, project3]) 155 | app_menu.menu.show() 156 | 157 | assert [str(a.text()) for a in app_menu.menu.actions()] == ['[R1] BProject', 158 | '[R2] AProject', 159 | '[R2] CProject', 160 | "", 161 | "About", 162 | "Preferences", 163 | "Exit"] 164 | 165 | 166 | @pytest.mark.functional 167 | def test_should_show_recent_build_first(qtbot): 168 | conf = ConfigBuilder({'values/lastBuildTimeForProject': False, 'sort_key': 'sort_build_time'}).build() 169 | parent = QWidget() 170 | app_menu = AppMenu(parent, conf, BuildIcons()) 171 | qtbot.addWidget(parent) 172 | project1 = ProjectBuilder({ 173 | 'name': 'BProject', 174 | 'url': 'dummyurl', 175 | 'lastBuildStatus': 'Success', 176 | 'activity': 'Sleeping', 177 | 'lastBuildTime': ((datetime.now() - relativedelta(years=1)).strftime("%Y-%m-%d %H:%M:%S")) 178 | }).build() 179 | 180 | project2 = ProjectBuilder({ 181 | 'name': 'AProject', 182 | 'url': 'dummyurl', 183 | 'lastBuildStatus': 'Success', 184 | 'activity': 'Sleeping', 185 | 'lastBuildTime': ((datetime.now() - relativedelta(years=0)).strftime("%Y-%m-%d %H:%M:%S")) 186 | }).build() 187 | 188 | app_menu.update([project1, project2]) 189 | app_menu.menu.show() 190 | 191 | assert [str(a.text()) for a in app_menu.menu.actions()] == ['AProject', 192 | 'BProject', 193 | "", 194 | "About", 195 | "Preferences", 196 | "Exit"] 197 | 198 | 199 | @pytest.mark.functional 200 | def test_should_show_preferences(qtbot, mocker): 201 | conf = ConfigBuilder().build() 202 | parent = QWidget() 203 | app_menu = AppMenu(parent, conf, BuildIcons()) 204 | qtbot.addWidget(parent) 205 | 206 | mocker.patch.object(PreferencesDialog, 'open', return_value="some preferences") 207 | mocker.patch.object(conf, 'update_preferences') 208 | with qtbot.waitSignal(app_menu.reload_data, timeout=1000): 209 | app_menu.preferences_clicked(None) 210 | -------------------------------------------------------------------------------- /test/project_status_notification_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from buildnotifylib.core.continous_integration_server import ContinuousIntegrationServer 4 | from buildnotifylib.core.projects import OverallIntegrationStatus 5 | from buildnotifylib.project_status_notification import ProjectStatus, ProjectStatusNotification 6 | from test.fake_conf import ConfigBuilder 7 | from test.project_builder import ProjectBuilder 8 | 9 | 10 | class ProjectStatusTest(unittest.TestCase): 11 | def test_should_identify_failing_builds(self): 12 | old_projects = [ 13 | ProjectBuilder({'name': 'proj1', 'lastBuildStatus': 'Success', 'activity': 'Sleeping', 'url': 'someurl', 14 | 'lastBuildTime': '2009-05-29T13:54:07'}).build(), 15 | ProjectBuilder({'name': 'proj2', 'lastBuildStatus': 'Success', 'activity': 'Sleeping', 'url': 'someurl', 16 | 'lastBuildTime': '2009-05-29T13:54:37'}).build()] 17 | new_projects = [ 18 | ProjectBuilder({'name': 'proj1', 'lastBuildStatus': 'Success', 'activity': 'Sleeping', 'url': 'someurl', 19 | 'lastBuildTime': '2009-05-29T13:54:07'}).build(), 20 | ProjectBuilder({'name': 'proj2', 'lastBuildStatus': 'Failure', 'activity': 'Sleeping', 'url': 'someurl', 21 | 'lastBuildTime': '2009-05-29T13:54:37'}).build()] 22 | failing_builds = ProjectStatusTest.build(old_projects, new_projects).failing_builds() 23 | self.assertEqual(1, len(failing_builds)) 24 | self.assertEqual("proj2", failing_builds[0]) 25 | 26 | def test_should_identify_fixed_builds(self): 27 | old_projects = [ 28 | ProjectBuilder({'name': 'proj1', 'lastBuildStatus': 'Failure', 'activity': 'Sleeping', 'url': 'someurl', 29 | 'lastBuildTime': '2009-05-29T13:54:07'}).build(), 30 | ProjectBuilder({'name': 'proj2', 'lastBuildStatus': 'Failure', 'activity': 'Sleeping', 'url': 'someurl', 31 | 'lastBuildTime': '2009-05-29T13:54:37'}).build()] 32 | new_projects = [ 33 | ProjectBuilder({'name': 'proj1', 'lastBuildStatus': 'Success', 'activity': 'Sleeping', 'url': 'someurl', 34 | 'lastBuildTime': '2009-05-29T13:54:07'}).build(), 35 | ProjectBuilder({'name': 'proj2', 'lastBuildStatus': 'Failure', 'activity': 'Sleeping', 'url': 'someurl', 36 | 'lastBuildTime': '2009-05-29T13:54:37'}).build()] 37 | successful_builds = ProjectStatusTest.build(old_projects, new_projects).successful_builds() 38 | self.assertEqual(1, len(successful_builds)) 39 | self.assertEqual("proj1", successful_builds[0]) 40 | 41 | def test_should_identify_still_failing_builds(self): 42 | old_projects = [ 43 | ProjectBuilder( 44 | {'name': 'proj1', 'lastBuildStatus': 'Success', 'activity': 'Sleeping', 'url': 'someurl', 45 | 'lastBuildLabel': '1', 46 | 'lastBuildTime': '2009-05-29T13:54:07'}).build(), 47 | ProjectBuilder( 48 | {'name': 'stillfailingbuild', 'lastBuildStatus': 'Failure', 'activity': 'Sleeping', 'url': 'someurl', 49 | 'lastBuildLabel': '10', 'lastBuildTime': '2009-05-29T13:54:37'}).build()] 50 | new_projects = [ 51 | ProjectBuilder( 52 | {'name': 'proj1', 'lastBuildStatus': 'Success', 'activity': 'Sleeping', 'url': 'someurl', 53 | 'lastBuildLabel': '1', 54 | 'lastBuildTime': '2009-05-29T13:54:07'}).build(), 55 | ProjectBuilder( 56 | {'name': 'stillfailingbuild', 'lastBuildStatus': 'Failure', 'activity': 'Sleeping', 'url': 'someurl', 57 | 'lastBuildLabel': '11', 'lastBuildTime': '2009-05-29T13:54:47'}).build()] 58 | still_failing_builds = ProjectStatusTest.build(old_projects, new_projects).still_failing_builds() 59 | self.assertEqual(1, len(still_failing_builds)) 60 | self.assertEqual("stillfailingbuild", still_failing_builds[0]) 61 | 62 | def test_should_identify_still_successful_builds(self): 63 | old_projects = [ 64 | ProjectBuilder( 65 | {'name': 'proj1', 'lastBuildStatus': 'Success', 'activity': 'Sleeping', 66 | 'url': 'someurl', 'lastBuildLabel': '1', 67 | 'lastBuildTime': '2009-05-29T13:54:07'}).build(), 68 | ProjectBuilder( 69 | {'name': 'Successbuild', 'lastBuildStatus': 'Success', 'activity': 'Sleeping', 70 | 'url': 'someurl', 71 | 'lastBuildLabel': '10', 'lastBuildTime': '2009-05-29T13:54:37'}).build()] 72 | new_projects = [ 73 | ProjectBuilder( 74 | {'name': 'proj1', 'lastBuildStatus': 'Success', 'activity': 'Sleeping', 75 | 'url': 'someurl', 'lastBuildLabel': '1', 76 | 'lastBuildTime': '2009-05-29T13:54:07'}).build(), 77 | ProjectBuilder( 78 | {'name': 'Successbuild', 'lastBuildStatus': 'Success', 'activity': 'Sleeping', 79 | 'url': 'someurl', 80 | 'lastBuildLabel': '11', 'lastBuildTime': '2009-05-29T13:54:47'}).build()] 81 | still_successful_builds = ProjectStatusTest.build(old_projects, new_projects).still_successful_builds() 82 | self.assertEqual(1, len(still_successful_builds)) 83 | self.assertEqual("Successbuild", still_successful_builds[0]) 84 | 85 | def test_should_build_tuples_by_server_url_and_name(self): 86 | project_s1 = ProjectBuilder({'name': 'proj1', 'lastBuildStatus': 'Success', 'activity': 'Sleeping', 87 | 'url': 'someurl', 88 | 'lastBuildTime': '2009-05-29T13:54:07'}).server('s1').build() 89 | project_s2 = ProjectBuilder({'name': 'proj1', 'lastBuildStatus': 'Success', 'activity': 'Sleeping', 90 | 'url': 'someurl', 91 | 'lastBuildTime': '2009-05-29T13:54:07'}).server('s2').build() 92 | old_projects = [project_s1, project_s2] 93 | new_projects = [project_s2, project_s1] 94 | tuple = ProjectStatusTest.build(old_projects, new_projects).tuple_for(project_s2) 95 | self.assertEqual('s2', tuple.current_project.server_url) 96 | self.assertEqual('s2', tuple.old_project.server_url) 97 | 98 | def test_should_identify_new_builds(self): 99 | old_projects = [ 100 | ProjectBuilder( 101 | {'name': 'proj1', 'lastBuildStatus': 'Success', 'activity': 'Sleeping', 102 | 'url': 'someurl', 103 | 'lastBuildTime': '2009-05-29T13:54:07'}).build()] 104 | new_projects = [ 105 | ProjectBuilder( 106 | {'name': 'proj1', 'lastBuildStatus': 'Success', 'activity': 'Sleeping', 107 | 'url': 'someurl', 108 | 'lastBuildTime': '2009-05-29T13:54:07'}).build(), 109 | ProjectBuilder( 110 | {'name': 'Successbuild', 'lastBuildStatus': 'Success', 111 | 'activity': 'Sleeping', 'url': 'someurl', 112 | 'lastBuildTime': '2009-05-29T13:54:47'}).build()] 113 | still_successful_builds = ProjectStatusTest.build(old_projects, new_projects).still_successful_builds() 114 | self.assertEqual(1, len(still_successful_builds)) 115 | self.assertEqual("Successbuild", still_successful_builds[0]) 116 | 117 | def test_should_include_prefix_in_notification(self): 118 | old_projects = [ 119 | ProjectBuilder( 120 | {'name': 'proj1', 'lastBuildStatus': 'Success', 'activity': 'Sleeping', 121 | 'url': 'someurl', 122 | 'lastBuildTime': '2009-05-29T13:54:07'}).prefix('R1').build()] 123 | new_projects = [ 124 | ProjectBuilder( 125 | {'name': 'proj1', 'lastBuildStatus': 'Success', 'activity': 'Sleeping', 126 | 'url': 'someurl', 127 | 'lastBuildTime': '2009-05-29T13:54:07'}).prefix('R1').build(), 128 | ProjectBuilder( 129 | {'name': 'Successbuild', 'lastBuildStatus': 'Success', 130 | 'activity': 'Sleeping', 'url': 'someurl', 131 | 'lastBuildTime': '2009-05-29T13:54:47'}).prefix('R1').build()] 132 | still_successful_builds = ProjectStatusTest.build(old_projects, new_projects).still_successful_builds() 133 | self.assertEqual(1, len(still_successful_builds)) 134 | self.assertEqual("[R1] Successbuild", still_successful_builds[0]) 135 | 136 | @classmethod 137 | def build(cls, old_projects, new_projects): 138 | return ProjectStatus(old_projects, new_projects) 139 | 140 | 141 | def test_should_return_notifications(mocker): 142 | old_projects = [ProjectBuilder({'name': 'proj1', 143 | 'lastBuildStatus': 'Success', 144 | 'activity': 'Sleeping', 145 | 'url': 'someurl', 146 | 'lastBuildLabel': '1', 147 | 'lastBuildTime': '2009-05-29T13:54:07'}).build(), 148 | ProjectBuilder({'name': 'Successbuild', 149 | 'lastBuildStatus': 'Failure', 150 | 'activity': 'Sleeping', 151 | 'url': 'someurl', 152 | 'lastBuildLabel': '10', 153 | 'lastBuildTime': '2009-05-29T13:54:37'}).build()] 154 | new_projects = [ProjectBuilder({'name': 'proj1', 155 | 'lastBuildStatus': 'Failure', 156 | 'activity': 'Sleeping', 157 | 'url': 'someurl', 158 | 'lastBuildLabel': '2', 159 | 'lastBuildTime': '2009-05-29T13:54:07'}).build(), 160 | ProjectBuilder({'name': 'Successbuild', 161 | 'lastBuildStatus': 'Success', 162 | 'activity': 'Sleeping', 163 | 'url': 'someurl', 164 | 'lastBuildLabel': '11', 165 | 'lastBuildTime': '2009-05-29T13:54:47'}).build()] 166 | old = OverallIntegrationStatus([ContinuousIntegrationServer('url', old_projects)]) 167 | new = OverallIntegrationStatus([ContinuousIntegrationServer('url', new_projects)]) 168 | 169 | class NotificationFake(object): 170 | def __init__(self): 171 | pass 172 | 173 | def show_message(self, **kwargs): 174 | print(kwargs) 175 | 176 | m = mocker.patch.object(NotificationFake, 'show_message') 177 | 178 | notification = ProjectStatusNotification(ConfigBuilder().build(), old, new, NotificationFake()) 179 | notification.show_notifications() 180 | 181 | m.assert_any_call('Broken builds', 'proj1') 182 | m.assert_any_call('Fixed builds', 'Successbuild') 183 | 184 | 185 | if __name__ == '__main__': 186 | unittest.main() 187 | -------------------------------------------------------------------------------- /data/server_configuration.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | serverConfigurationDialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 458 10 | 384 11 | 12 | 13 | 14 | 15 | 0 16 | 0 17 | 18 | 19 | 20 | 21 | 458 22 | 384 23 | 24 | 25 | 26 | Add Server 27 | 28 | 29 | 30 | 31 | 32 | 0 33 | 34 | 35 | 36 | 37 | 38 | 39 | Path to cctray.xml 40 | 41 | 42 | addServerUrl 43 | 44 | 45 | 46 | 47 | 48 | 49 | http://[host]:[port]/dashboard/cctray.xml 50 | 51 | 52 | 53 | 54 | 55 | 56 | Authentication 57 | 58 | 59 | 60 | 61 | 62 | QFormLayout::AllNonFixedFieldsGrow 63 | 64 | 65 | 66 | 67 | Username 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | Password 78 | 79 | 80 | 81 | 82 | 83 | 84 | QLineEdit::Password 85 | 86 | 87 | 88 | 89 | 90 | 91 | Authentication type 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | Username/password 100 | 101 | 102 | 103 | 104 | Authentication Bearer token 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | Misc 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 0 127 | 0 128 | 129 | 130 | 131 | 132 | 180 133 | 0 134 | 135 | 136 | 137 | 138 | 180 139 | 16777215 140 | 141 | 142 | 143 | Server timezone 144 | 145 | 146 | timezoneList 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 0 155 | 0 156 | 157 | 158 | 159 | 160 | 210 161 | 0 162 | 163 | 164 | 165 | 166 | 200 167 | 16777215 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 0 177 | 0 178 | 179 | 180 | 181 | 182 | 180 183 | 0 184 | 185 | 186 | 187 | 188 | 180 189 | 16777215 190 | 191 | 192 | 193 | Display prefix 194 | 195 | 196 | timezoneList 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 210 205 | 0 206 | 207 | 208 | 209 | e.g. branch/release 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | Qt::Vertical 222 | 223 | 224 | 225 | 20 226 | 40 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | Qt::Horizontal 237 | 238 | 239 | 240 | 40 241 | 20 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | Load 250 | 251 | 252 | false 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | Choose projects 266 | 267 | 268 | addServerUrl 269 | 270 | 271 | 272 | 273 | 274 | 275 | QAbstractItemView::NoEditTriggers 276 | 277 | 278 | QAbstractItemView::NoSelection 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | Qt::Horizontal 288 | 289 | 290 | 291 | 40 292 | 20 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | Back 301 | 302 | 303 | false 304 | 305 | 306 | 307 | 308 | 309 | 310 | OK 311 | 312 | 313 | false 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | addServerUrl 327 | projectsList 328 | 329 | 330 | 331 | 332 | addServerUrl 333 | returnPressed() 334 | loadUrlButton 335 | click() 336 | 337 | 338 | 283 339 | 27 340 | 341 | 342 | 365 343 | 30 344 | 345 | 346 | 347 | 348 | submitButton 349 | clicked() 350 | serverConfigurationDialog 351 | accept() 352 | 353 | 354 | 372 355 | 293 356 | 357 | 358 | 55 359 | 292 360 | 361 | 362 | 363 | 364 | 365 | -------------------------------------------------------------------------------- /data/preferences.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Preferences 4 | 5 | 6 | 7 | 0 8 | 0 9 | 462 10 | 357 11 | 12 | 13 | 14 | Preferences 15 | 16 | 17 | false 18 | 19 | 20 | 21 | 22 | 23 | QDialogButtonBox::Ok 24 | 25 | 26 | 27 | 28 | 29 | 30 | 2 31 | 32 | 33 | 34 | 35 | 0 36 | 0 37 | 38 | 39 | 40 | Servers 41 | 42 | 43 | 44 | 45 | 46 | 47 | 0 48 | 0 49 | 50 | 51 | 52 | Monitored servers 53 | 54 | 55 | 56 | 57 | 58 | QAbstractItemView::NoEditTriggers 59 | 60 | 61 | 62 | 63 | 64 | 65 | false 66 | 67 | 68 | Configure 69 | 70 | 71 | 72 | 73 | 74 | 75 | Qt::Horizontal 76 | 77 | 78 | 79 | 40 80 | 20 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 0 90 | 0 91 | 92 | 93 | 94 | Add 95 | 96 | 97 | + 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 0 106 | 0 107 | 108 | 109 | 110 | Remove 111 | 112 | 113 | - 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | Notifications 125 | 126 | 127 | 128 | 129 | 130 | Notification settings 131 | 132 | 133 | false 134 | 135 | 136 | false 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | successful builds 145 | 146 | 147 | 148 | 149 | 150 | 151 | broken builds 152 | 153 | 154 | 155 | 156 | 157 | 158 | fixed builds 159 | 160 | 161 | 162 | 163 | 164 | 165 | still failing builds 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 200 174 | 16777215 175 | 176 | 177 | 178 | connectivity issues 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | Custom notifications 191 | 192 | 193 | 194 | 195 | 196 | Execute script for notifications 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | Script 206 | 207 | 208 | scriptLineEdit 209 | 210 | 211 | 212 | 213 | 214 | 215 | false 216 | 217 | 218 | #status# and #projects# would be replaced by the build status and projects respectively 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | Qt::Vertical 234 | 235 | 236 | 237 | 20 238 | 40 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | Misc 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | Server polling interval 256 | 257 | 258 | pollingIntervalSpinBox 259 | 260 | 261 | 262 | 263 | 264 | 265 | show last build label for each project 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 130 274 | 0 275 | 276 | 277 | 278 | 279 | 130 280 | 16777215 281 | 282 | 283 | 284 | false 285 | 286 | 287 | seconds 288 | 289 | 290 | 1 291 | 292 | 293 | 60 294 | 295 | 296 | 1 297 | 298 | 299 | 300 | 301 | 302 | 303 | show last build time for each project 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | Build Sort order 313 | 314 | 315 | 316 | 317 | 318 | Sort builds by name 319 | 320 | 321 | 322 | 323 | 324 | 325 | Sort builds by last build time 326 | 327 | 328 | true 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | Qt::Vertical 339 | 340 | 341 | 342 | 20 343 | 40 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | scriptCheckbox 360 | toggled(bool) 361 | scriptLineEdit 362 | setEnabled(bool) 363 | 364 | 365 | 147 366 | 237 367 | 368 | 369 | 192 370 | 277 371 | 372 | 373 | 374 | 375 | 376 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # A comma-separated list of package or module names from where C extensions may 4 | # be loaded. Extensions are loading into the active Python interpreter and may 5 | # run arbitrary code 6 | extension-pkg-whitelist=PyQt5 7 | 8 | # Add files or directories to the blacklist. They should be base names, not 9 | # paths. 10 | ignore=CVS 11 | 12 | # Add files or directories matching the regex patterns to the blacklist. The 13 | # regex matches against base names, not paths. 14 | ignore-patterns=preferences_ui.py,server_configuration_ui.py,icons_rc.py 15 | 16 | # Python code to execute, usually for sys.path manipulation such as 17 | # pygtk.require(). 18 | #init-hook= 19 | 20 | # Use multiple processes to speed up Pylint. 21 | jobs=4 22 | 23 | # List of plugins (as comma separated values of python modules names) to load, 24 | # usually to register additional checkers. 25 | load-plugins= 26 | 27 | # Pickle collected data for later comparisons. 28 | persistent=yes 29 | 30 | # Specify a configuration file. 31 | #rcfile= 32 | 33 | # Allow loading of arbitrary C extensions. Extensions are imported into the 34 | # active Python interpreter and may run arbitrary code. 35 | unsafe-load-any-extension=no 36 | 37 | 38 | [MESSAGES CONTROL] 39 | 40 | # Only show warnings with the listed confidence levels. Leave empty to show 41 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED 42 | confidence= 43 | 44 | # Disable the message, report, category or checker with the given id(s). You 45 | # can either give multiple identifiers separated by comma (,) or put this 46 | # option multiple times (only on the command line, not in the configuration 47 | # file where it should appear only once).You can also use "--disable=all" to 48 | # disable everything first and then reenable specific checks. For example, if 49 | # you want to run only the similarities checker, you can use "--disable=all 50 | # --enable=similarities". If you want to run only the classes checker, but have 51 | # no Warning level messages displayed, use"--disable=all --enable=classes 52 | # --disable=W" 53 | disable=print-statement,parameter-unpacking,unpacking-in-except,old-raise-syntax,backtick,long-suffix,old-ne-operator,old-octal-literal,import-star-module-level,raw-checker-failed,bad-inline-option,locally-disabled,locally-enabled,file-ignored,suppressed-message,useless-suppression,deprecated-pragma,apply-builtin,basestring-builtin,buffer-builtin,cmp-builtin,coerce-builtin,execfile-builtin,file-builtin,long-builtin,raw_input-builtin,reduce-builtin,standarderror-builtin,unicode-builtin,xrange-builtin,coerce-method,delslice-method,getslice-method,setslice-method,no-absolute-import,old-division,dict-iter-method,dict-view-method,next-method-called,metaclass-assignment,indexing-exception,raising-string,reload-builtin,oct-method,hex-method,nonzero-method,cmp-method,input-builtin,round-builtin,intern-builtin,unichr-builtin,map-builtin-not-iterating,zip-builtin-not-iterating,range-builtin-not-iterating,filter-builtin-not-iterating,using-cmp-argument,eq-without-hash,div-method,idiv-method,rdiv-method,exception-message-attribute,invalid-str-codec,sys-max-int,bad-python3-import,deprecated-string-function,deprecated-str-translate-call 54 | 55 | # Enable the message, report, category or checker with the given id(s). You can 56 | # either give multiple identifier separated by comma (,) or put this option 57 | # multiple time (only on the command line, not in the configuration file where 58 | # it should appear only once). See also the "--disable" option for examples. 59 | enable= 60 | 61 | 62 | [REPORTS] 63 | 64 | # Python expression which should return a note less than 10 (10 is the highest 65 | # note). You have access to the variables errors warning, statement which 66 | # respectively contain the number of errors / warnings messages and the total 67 | # number of statements analyzed. This is used by the global evaluation report 68 | # (RP0004). 69 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 70 | 71 | # Template used to display messages. This is a python new-style format string 72 | # used to format the message information. See doc for all details 73 | #msg-template= 74 | 75 | # Set the output format. Available formats are text, parseable, colorized, json 76 | # and msvs (visual studio).You can also give a reporter class, eg 77 | # mypackage.mymodule.MyReporterClass. 78 | output-format=text 79 | 80 | # Tells whether to display a full report or only the messages 81 | reports=no 82 | 83 | # Activate the evaluation score. 84 | score=yes 85 | 86 | 87 | [REFACTORING] 88 | 89 | # Maximum number of nested blocks for function / method body 90 | max-nested-blocks=5 91 | 92 | 93 | [SIMILARITIES] 94 | 95 | # Ignore comments when computing similarities. 96 | ignore-comments=yes 97 | 98 | # Ignore docstrings when computing similarities. 99 | ignore-docstrings=yes 100 | 101 | # Ignore imports when computing similarities. 102 | ignore-imports=no 103 | 104 | # Minimum lines number of a similarity. 105 | min-similarity-lines=4 106 | 107 | 108 | [FORMAT] 109 | 110 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 111 | expected-line-ending-format= 112 | 113 | # Regexp for a line that is allowed to be longer than the limit. 114 | ignore-long-lines=^\s*(# )??$ 115 | 116 | # Number of spaces of indent required inside a hanging or continued line. 117 | indent-after-paren=4 118 | 119 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 120 | # tab). 121 | indent-string=' ' 122 | 123 | # Maximum number of characters on a single line. 124 | max-line-length=200 125 | 126 | # Maximum number of lines in a module 127 | max-module-lines=1000 128 | 129 | # List of optional constructs for which whitespace checking is disabled. `dict- 130 | # separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. 131 | # `trailing-comma` allows a space between comma and closing bracket: (a, ). 132 | # `empty-line` allows space-only lines. 133 | no-space-check=trailing-comma,dict-separator 134 | 135 | # Allow the body of a class to be on the same line as the declaration if body 136 | # contains single statement. 137 | single-line-class-stmt=no 138 | 139 | # Allow the body of an if to be on the same line as the test if there is no 140 | # else. 141 | single-line-if-stmt=no 142 | 143 | 144 | [BASIC] 145 | 146 | # Naming hint for argument names 147 | argument-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ 148 | 149 | # Regular expression matching correct argument names 150 | argument-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ 151 | 152 | # Naming hint for attribute names 153 | attr-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ 154 | 155 | # Regular expression matching correct attribute names 156 | attr-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ 157 | 158 | # Bad variable names which should always be refused, separated by a comma 159 | bad-names=foo,bar,baz,toto,tutu,tata 160 | 161 | # Naming hint for class attribute names 162 | class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ 163 | 164 | # Regular expression matching correct class attribute names 165 | class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ 166 | 167 | # Naming hint for class names 168 | class-name-hint=[A-Z_][a-zA-Z0-9]+$ 169 | 170 | # Regular expression matching correct class names 171 | class-rgx=[A-Z_][a-zA-Z0-9]+$ 172 | 173 | # Naming hint for constant names 174 | const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ 175 | 176 | # Regular expression matching correct constant names 177 | const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ 178 | 179 | # Minimum line length for functions/classes that require docstrings, shorter 180 | # ones are exempt. 181 | docstring-min-length=1000 182 | 183 | # Naming hint for function names 184 | function-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ 185 | 186 | # Regular expression matching correct function names 187 | function-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ 188 | 189 | # Good variable names which should always be accepted, separated by a comma 190 | good-names=i,j,k,ex,Run,_ 191 | 192 | # Include a hint for the correct naming format with invalid-name 193 | include-naming-hint=no 194 | 195 | # Naming hint for inline iteration names 196 | inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ 197 | 198 | # Regular expression matching correct inline iteration names 199 | inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ 200 | 201 | # Naming hint for method names 202 | method-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ 203 | 204 | # Regular expression matching correct method names 205 | method-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ 206 | 207 | # Naming hint for module names 208 | module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 209 | 210 | # Regular expression matching correct module names 211 | module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 212 | 213 | # Colon-delimited sets of names that determine each other's naming style when 214 | # the name regexes allow several styles. 215 | name-group= 216 | 217 | # Regular expression which should only match function or class names that do 218 | # not require a docstring. 219 | no-docstring-rgx=.* 220 | 221 | # List of decorators that produce properties, such as abc.abstractproperty. Add 222 | # to this list to register other decorators that produce valid properties. 223 | property-classes=abc.abstractproperty 224 | 225 | # Naming hint for variable names 226 | variable-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ 227 | 228 | # Regular expression matching correct variable names 229 | variable-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ 230 | 231 | 232 | [MISCELLANEOUS] 233 | 234 | # List of note tags to take in consideration, separated by a comma. 235 | notes=FIXME,XXX,TODO 236 | 237 | 238 | [VARIABLES] 239 | 240 | # List of additional names supposed to be defined in builtins. Remember that 241 | # you should avoid to define new builtins when possible. 242 | additional-builtins= 243 | 244 | # Tells whether unused global variables should be treated as a violation. 245 | allow-global-unused-variables=yes 246 | 247 | # List of strings which can identify a callback function by name. A callback 248 | # name must start or end with one of those strings. 249 | callbacks=cb_,_cb 250 | 251 | # A regular expression matching the name of dummy variables (i.e. expectedly 252 | # not used). 253 | dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ 254 | 255 | # Argument names that match this expression will be ignored. Default to name 256 | # with leading underscore 257 | ignored-argument-names=_.*|^ignored_|^unused_ 258 | 259 | # Tells whether we should check for unused import in __init__ files. 260 | init-import=no 261 | 262 | # List of qualified module names which can have objects that can redefine 263 | # builtins. 264 | redefining-builtins-modules=six.moves,future.builtins 265 | 266 | 267 | [SPELLING] 268 | 269 | # Spelling dictionary name. Available dictionaries: none. To make it working 270 | # install python-enchant package. 271 | spelling-dict= 272 | 273 | # List of comma separated words that should not be checked. 274 | spelling-ignore-words= 275 | 276 | # A path to a file that contains private dictionary; one word per line. 277 | spelling-private-dict-file= 278 | 279 | # Tells whether to store unknown words to indicated private dictionary in 280 | # --spelling-private-dict-file option instead of raising a message. 281 | spelling-store-unknown-words=no 282 | 283 | 284 | [TYPECHECK] 285 | 286 | # List of decorators that produce context managers, such as 287 | # contextlib.contextmanager. Add to this list to register other decorators that 288 | # produce valid context managers. 289 | contextmanager-decorators=contextlib.contextmanager 290 | 291 | # List of members which are set dynamically and missed by pylint inference 292 | # system, and so shouldn't trigger E1101 when accessed. Python regular 293 | # expressions are accepted. 294 | generated-members= 295 | 296 | # Tells whether missing members accessed in mixin class should be ignored. A 297 | # mixin class is detected if its name ends with "mixin" (case insensitive). 298 | ignore-mixin-members=yes 299 | 300 | # This flag controls whether pylint should warn about no-member and similar 301 | # checks whenever an opaque object is returned when inferring. The inference 302 | # can return multiple potential results while evaluating a Python object, but 303 | # some branches might not be evaluated, which results in partial inference. In 304 | # that case, it might be useful to still emit no-member and other checks for 305 | # the rest of the inferred objects. 306 | ignore-on-opaque-inference=yes 307 | 308 | # List of class names for which member attributes should not be checked (useful 309 | # for classes with dynamically set attributes). This supports the use of 310 | # qualified names. 311 | ignored-classes=optparse.Values,thread._local,_thread._local 312 | 313 | # List of module names for which member attributes should not be checked 314 | # (useful for modules/projects where namespaces are manipulated during runtime 315 | # and thus existing member attributes cannot be deduced by static analysis. It 316 | # supports qualified module names, as well as Unix pattern matching. 317 | ignored-modules= 318 | 319 | # Show a hint with possible names when a member name was not found. The aspect 320 | # of finding the hint is based on edit distance. 321 | missing-member-hint=yes 322 | 323 | # The minimum edit distance a name should have in order to be considered a 324 | # similar match for a missing member name. 325 | missing-member-hint-distance=1 326 | 327 | # The total number of similar names that should be taken in consideration when 328 | # showing a hint for a missing member. 329 | missing-member-max-choices=1 330 | 331 | 332 | [LOGGING] 333 | 334 | # Logging modules to check that the string format arguments are in logging 335 | # function parameter format 336 | logging-modules=logging 337 | 338 | 339 | [CLASSES] 340 | 341 | # List of method names used to declare (i.e. assign) instance attributes. 342 | defining-attr-methods=__init__,__new__,setUp 343 | 344 | # List of member names, which should be excluded from the protected access 345 | # warning. 346 | exclude-protected=_asdict,_fields,_replace,_source,_make 347 | 348 | # List of valid names for the first argument in a class method. 349 | valid-classmethod-first-arg=cls 350 | 351 | # List of valid names for the first argument in a metaclass class method. 352 | valid-metaclass-classmethod-first-arg=mcs 353 | 354 | 355 | [DESIGN] 356 | 357 | # Maximum number of arguments for function / method 358 | max-args=5 359 | 360 | # Maximum number of attributes for a class (see R0902). 361 | max-attributes=7 362 | 363 | # Maximum number of boolean expressions in a if statement 364 | max-bool-expr=5 365 | 366 | # Maximum number of branch for function / method body 367 | max-branches=12 368 | 369 | # Maximum number of locals for function / method body 370 | max-locals=15 371 | 372 | # Maximum number of parents for a class (see R0901). 373 | max-parents=7 374 | 375 | # Maximum number of public methods for a class (see R0904). 376 | max-public-methods=20 377 | 378 | # Maximum number of return / yield for function / method body 379 | max-returns=6 380 | 381 | # Maximum number of statements in function / method body 382 | max-statements=50 383 | 384 | # Minimum number of public methods for a class (see R0903). 385 | min-public-methods=2 386 | 387 | 388 | [IMPORTS] 389 | 390 | # Allow wildcard imports from modules that define __all__. 391 | allow-wildcard-with-all=no 392 | 393 | # Analyse import fallback blocks. This can be used to support both Python 2 and 394 | # 3 compatible code, which means that the block might have code that exists 395 | # only in one or another interpreter, leading to false positives when analysed. 396 | analyse-fallback-blocks=no 397 | 398 | # Deprecated modules which should not be used, separated by a comma 399 | deprecated-modules=regsub,TERMIOS,Bastion,rexec 400 | 401 | # Create a graph of external dependencies in the given file (report RP0402 must 402 | # not be disabled) 403 | ext-import-graph= 404 | 405 | # Create a graph of every (i.e. internal and external) dependencies in the 406 | # given file (report RP0402 must not be disabled) 407 | import-graph= 408 | 409 | # Create a graph of internal dependencies in the given file (report RP0402 must 410 | # not be disabled) 411 | int-import-graph= 412 | 413 | # Force import order to recognize a module as part of the standard 414 | # compatibility libraries. 415 | known-standard-library= 416 | 417 | # Force import order to recognize a module as part of a third party library. 418 | known-third-party=enchant 419 | 420 | 421 | [EXCEPTIONS] 422 | 423 | # Exceptions that will emit a warning when being caught. Defaults to 424 | # "Exception" 425 | overgeneral-exceptions=Exception 426 | --------------------------------------------------------------------------------