├── 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 |
5 |
--------------------------------------------------------------------------------
/icons/buildnotify.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/icons/buildnotify-inactive.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/icons/buildnotify-success.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/icons/buildnotify-failure-building.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/icons/buildnotify-success-building.svg:
--------------------------------------------------------------------------------
1 |
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 | []()
13 |
14 | Customize notifications that you'd like to see
15 | []()
16 |
17 | Tweak configuration
18 | []()
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 | []()
4 | []()
5 | []()
6 | []()
7 | []()
8 | []()
9 | []()
10 | []()
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 | []()
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 |
--------------------------------------------------------------------------------