Sign Up
Log In
Log In
or
Sign Up
Places
All Projects
Status Monitor
Collapse sidebar
devel:openQA:Tools
osc-plugin-qam
_service:obs_scm:osc-plugin-qam-1.0.3+git0.420b...
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
File _service:obs_scm:osc-plugin-qam-1.0.3+git0.420bf95.obscpio of Package osc-plugin-qam
07070100000000000041ED000000000000000000000003644668C300000000000000000000000000000000000000000000002A00000000osc-plugin-qam-1.0.3+git0.420bf95/.github07070100000001000041ED000000000000000000000002644668C300000000000000000000000000000000000000000000003400000000osc-plugin-qam-1.0.3+git0.420bf95/.github/workflows07070100000002000081A4000000000000000000000001644668C300000259000000000000000000000000000000000000003B00000000osc-plugin-qam-1.0.3+git0.420bf95/.github/workflows/ci.yml--- name: ci # yamllint disable-line rule:truthy on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: python-version: '3.8' cache: 'pip' - name: Setup enviroment run: pip install -r requirements-dev.txt - name: Run tests + cov report run: make test-with-coverage - name: Check style run: make checkstyle - name: Upload coverage to CodeCov uses: codecov/codecov-action@v3 with: files: coverage.xml 07070100000003000081A4000000000000000000000001644668C3000002A5000000000000000000000000000000000000002D00000000osc-plugin-qam-1.0.3+git0.420bf95/.gitignore# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .cache nosetests.xml coverage.xml # Translations *.mo *.pot # Django stuff: *.log # Sphinx documentation Documentation/_build/ # PyBuilder target/ 07070100000004000081A4000000000000000000000001644668C300000338000000000000000000000000000000000000002F00000000osc-plugin-qam-1.0.3+git0.420bf95/.mergify.ymlpull_request_rules: - name: automatic merge conditions: - and: &base_checks - base=master - -label~=^acceptance-tests-needed|not-ready - "#check-failure=0" - "#check-pending=0" - linear-history - and: - "#approved-reviews-by>=1" - "#changes-requested-reviews-by=0" # https://doc.mergify.io/examples.html#require-all-requested-reviews-to-be-approved - "#review-requested=0" actions: &merge merge: method: merge - name: automatic merge on special label conditions: - and: *base_checks - "label=merge-fast" actions: *merge - name: ask to resolve conflict conditions: - conflict actions: comment: message: This pull request is now in conflicts. Could you fix it? 🙏 07070100000005000081A4000000000000000000000001644668C30000176A000000000000000000000000000000000000003000000000osc-plugin-qam-1.0.3+git0.420bf95/ChangeLog.rstChangeLog ######### 0.30.0 ====== - Move to python2/3 compactible code - remove python 2.6 compactibility quirks 0.24.2 ====== - Comliance with new osc 0.163.0. Merge request !6. 0.24.1 ====== - Filter out non-assignable groups from Unassigned Roles. 0.24.0 ====== - Rework of assignment inference. - Document -G flag for approve to warn about (probably) incorrect usage. 0.23.0 ====== - Drop β priority: no longer used - default priority now replaces it completely. 0.22.1 ====== - Make logging configurable and store log-files in $XDG_DATA_HOME/oscqam/. - Fix assignment inference when the expected invariants don't hold. 0.22.0 ====== - Fix naming conflict with newer versions of osc used in tumbleweed. 0.21.0 ====== - Correctly handle new testreports that have no $Author header anymore. 0.20.1 ====== - Report errors when accessing the IBS. - Removed -U flag from approve. Approval for another user is not possible. 0.20.0 ====== - Add new issues field: lists number of issues contained in the request. 0.19.3 ====== - Fix crash when url not retrievable. 0.19.2 ====== - Fix Assignment inference, when history events out of order. 0.19.1 ====== - Fix approval action raising errors if the last qam-group was approved. 0.19.0 ====== - Switch to using HTTPS for testreports, as HTTP gives problem through VPN. 0.18.1 ====== - Fix bnc#998835: better error message and basic return codes. 0.18.0 ====== - Internal refactoring: errors, messages. - Fix bnc#989567. - Can now approve for groups directly. - When approving as a user additional (possible) reviews for the user will be pointed out. 0.17.1 ====== - Fix bnc#676298. 0.17.0 ====== - Add 'creator' field to 'info' and 'assigned' commands. - Correctly handle 'end of transmission'. 0.16.0 ====== - Bugfix: unassign action no longer leaves buildservice in inconsistent state if one of the steps can not be completed. - New alias for quit: 'quit'. - Will output url to testreport in reject / approve actions' comments. 0.15.3 ====== - Fix incorrect formatting of 'Package-Streams' field. - Fix incorrect formatting of 'Bugs' field. 0.15.2 ====== - Provide alternate implementation for SSL connection on python 2.6 versions: utilizes requests library. 0.15.1 ====== - Fix SSL connection to https://maintenance.suse.de 0.15.0 ====== - Use beta-priority to order requests instead of normal priority. - Fallback to normal-priority if beta-priority can not be loaded. - Rejects no longer proceed if no comment is set, even if a message is provided. - Reject comments will now be prefixed by the plugins [oscqam] prefix. 0.14.1 ====== - Fix 'do_my' method: now calls 'do_assigned' with a valid opts. 0.14.0 ====== - Can now limit groups shown in 'open'/'assigned' commands via '-G' flag. 0.13.1 ====== - Fix rejection if a maintenance incident has already (exactly) one reject. 0.13.0 ====== - Allow > 1 group to be assigned at a time. 0.12.2 ====== - Pass correct project to set_attribute call. 0.12.1 ====== - Can specify multiple 'reject_reasons' 0.12.0 ====== - Added 'reject_reasons' to the rejection-command: It is now required to specify *why* a request was rejected. The reason will be stored in the corresponding Maintenance Incident. 0.11.0 ====== - Added 'my' command to list requests assigned to the current user. - Changed 'open' command: will no longer lists requests that the user is already assigned to. 0.10.1 ====== - Fix assign action for OBS. 0.10.0 ====== - Add OBS support to the plugin: - Commands tested & available: open / assigned / info. - Commands untested: assign / unassign / accept / reject. 0.9.0 ===== - Add flag to assign action to not check if a template exists. 0.8.1 ===== - Fix bug when assigning a previously rejected update. 0.8.0 ===== - Add comments features: allow listing and deletion. - Check previous rejects when assigning tester. 0.7.1 ===== - Add missing dependency to spec-file: python-futures 0.7.0 ===== - Use threading to load requests. - Memoize build service requests. - Fix bnc#949745: allow multiline comments. 0.6.0 ===== - Add 'assigned' command to possible commands: list all requests that are assigned (as far as the plugin can infer them). - Add 'info' command to possible commands: list information for one request only. - Inference for assignments now only considers qam-groups and ignore qam-auto. 0.5.2 ===== - Add 'status' and 'Test Plan Reviewer' checks to approve action. - Fix reject outputting complete log. - Fix bnc#943294: match 'Test Plan Reviewers' if 'Test Plan Reviewer' is not found. - Fix bnc#942510: print message after assignment was successful. 0.5.1 ===== - Fix bug in list user-assigned command. 0.5.0 ===== - Assign-check: do not allow assign before the template is generated. - Assign-check: do not allow assign for more than one group. - Add Python 2.6 backport for total_ordering decorator. 0.4.1 ===== - Rewrote assignment inference logic to handle incorrect case. - Workaround for OBS2.7 and osc < 0.152 clients that can not handle acceptinfo-tags. 0.4.0 ===== - Incident priority added to requests and list-sorting. 0.3.2 ===== - Errors occurring during 'assign' will no longer crash the program. - Fixed incorrect log_path in 'decline' action crashing the program. - Fixed unassign action when user passes a group to unassign. - Reworked tests. 0.3.1 ===== - Tabular output will split lists into multiple lines. 0.3.0 ===== - Default list output is less verbose. - To obtain original output use verbose (-v flag). - List output can be generated as a table (-T flag). - Configure data to output in list command (-C parameter). 0.2.0 ===== - With upstream osc-version it is now possible to use the readline shortcuts. - Can use complete request_id in plugin now as well: e.g. ibs qam assign SUSE:Maintenance:123:45678 0.1.0 ===== - Implementation for basic commands: - list, assign, unassign, approve, reject, comment 07070100000006000041ED000000000000000000000003644668C300000000000000000000000000000000000000000000003000000000osc-plugin-qam-1.0.3+git0.420bf95/Documentation070701000000070000A1FF0000000000000000000000016446692300000010000000000000000000000000000000000000003E00000000osc-plugin-qam-1.0.3+git0.420bf95/Documentation/ChangeLog.rst../ChangeLog.rst07070100000008000081A4000000000000000000000001644668C300001A6A000000000000000000000000000000000000003900000000osc-plugin-qam-1.0.3+git0.420bf95/Documentation/Makefile# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make <target>' where <target> is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/oscqam.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/oscqam.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/oscqam" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/oscqam" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 07070100000009000081A4000000000000000000000001644668C3000002AF000000000000000000000000000000000000003900000000osc-plugin-qam-1.0.3+git0.420bf95/Documentation/bugs.rst.. _workarounds: Known bugs & workarounds ======================== This page will list known bugs and (if required) possible workarounds for the problem. 1. No readline support ---------------------- The fact that the interactive mode is currently not using the ``readline``-module is known and (unfortunately), because of some changes that ``osc`` has made. To allow ``readline``-like functionality it is possible to use rlwrap_ with the plugin as a current workaround: After installing rlwrap the following command will restore readline functionality: .. code-block:: bash rlwrap osc --apiurl=https://api.suse.de/ qam .. _rlwrap: https://github.com/hanslub42/rlwrap 0707010000000A000081A4000000000000000000000001644668C30000209C000000000000000000000000000000000000003800000000osc-plugin-qam-1.0.3+git0.420bf95/Documentation/conf.py# -*- coding: utf-8 -*- # # oscqam documentation build configuration file, created by # sphinx-quickstart on Wed Jan 7 15:51:28 2015. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys import os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.abspath("..")) import oscqam # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ "sphinx.ext.autodoc", "sphinx.ext.viewcode", ] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # The suffix of source filenames. source_suffix = ".rst" # The encoding of source files. # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = "index" # General information about the project. project = "oscqam" copyright = "2015, QA Maintenance" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = oscqam.__version__ # The full version, including alpha/beta/rc tags. release = oscqam.__version__ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # today = '' # Else, today_fmt is used as the format for a strftime call. # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ["_build"] # The reST default role (used for this markup: `text`) to use for all # documents. # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. # keep_warnings = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = "sphinx_rtd_theme" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # "<project> v<release> documentation". # html_title = None # A shorter title for the navigation bar. Default is the same as html_title. # html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. # html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. # html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. # html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. # html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. # html_additional_pages = {} # If false, no module index is generated. # html_domain_indices = True # If false, no index is generated. # html_use_index = True # If true, the index is split into individual pages for each letter. # html_split_index = False # If true, links to the reST sources are added to the pages. # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. # html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a <link> tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = "oscqamdoc" # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ ("index", "oscqam.tex", "oscqam Documentation", "Florian Bergmann", "manual"), ] # The name of an image file (relative to this directory) to place at the top of # the title page. # latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # latex_use_parts = False # If true, show page references after internal links. # latex_show_pagerefs = False # If true, show URL addresses after external links. # latex_show_urls = False # Documents to append as an appendix to all manuals. # latex_appendices = [] # If false, no module index is generated. # latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [("index", "oscqam", "oscqam Documentation", ["Florian Bergmann"], 1)] # If true, show URL addresses after external links. # man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ( "index", "oscqam", "oscqam Documentation", "Florian Bergmann", "oscqam", "One line description of project.", "Miscellaneous", ), ] # Documents to append as an appendix to all manuals. # texinfo_appendices = [] # If false, no module index is generated. # texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. # texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. # texinfo_no_detailmenu = False 0707010000000B000081A4000000000000000000000001644668C300000CFD000000000000000000000000000000000000003A00000000osc-plugin-qam-1.0.3+git0.420bf95/Documentation/devel.rstDevelopment =========== Hosting ------- The plugin is hosted on our internal ``gitlab`` instance: https://gitlab.suse.de/qa-maintenance/qam-oscplugin Working from source ------------------- After checking out the source code it is required to setup the plugin, so ``osc`` can find and use it. By default ``osc`` will look in the following paths for plugins: - ``/usr/lib/osc-plugins`` - ``/usr/local/lib/osc-plugins`` - ``/var/lib/osc-plugins`` - ``~/.osc-plugins`` To make ``oscqam`` available to ``osc`` the start-up point needs to be available in one of these paths and the modules from oscqam need to be importable. An easy way is to symlink the ``oscqam`` folder and ``cli.py`` file into e.g. ``~/.osc-plugins`` and set the ``PYTHONPATH`` to include this folder: .. note:: To make usage of the ``development`` version easier, while also having a version from the repository installed, it makes sense to add the ``PYTHONPATH`` change to your ``.{bash,zsh}rc``. To return to the installed version just remove the symbolic links. .. code-block:: bash git clone gitlab@gitlab.suse.de:qa-maintenance/qam-oscplugin.git ln -s "$PWD/oscqam/cli.py" ~/.osc-plugins/oscqam/cli.py ln -s "$PWD/oscqam/oscqam" ~/.osc-plugin/oscqam export PYTHONPATH="~/.osc-plugins:$PYTHONPATH" Testing ------- The oscqam plugin uses pytest_ library to run the test. To setup the project correctly for usage with it, install it using pip: .. code-block:: bash cd <src_directory_oscqam> pip install --user -e . Now running the tests with ``py.test`` should work: .. code-block:: bash cd <src_directory_oscqam> py.test ./tests .. _pytest: http://pytest.org/ Release ------- The current version of the plugin can be installed from the official `QA-Maintenance project`_ in the internal build service: https://build.suse.de/package/show/QA:Maintenance/python-oscqam The plugin should keep building for at least the supported versions. .. _QA-Maintenance project: https://build.suse.de/project/show/QA:Maintenance Procedure ######### 1. Bump version and ChangeLog 2. make release 3. git tag v<version> 4. git push && git push <remote> refs/tags/v<version> Using a virtual environment --------------------------- To process to setup a virtual environment for the plugin is a little more involved than or other projects due to dependencies of `osc`. The process is as follows: - Install development headers for `python`, `openssl` and `libcurl`: .. code:: bash sudo zypper in python-devel openssl-devel libcurl-devel - Create the virtualenvironment and switch to it. - Install the dependencies for `osc`: when installing `pycurl` make sure to set `PYCURL_SSL_LIBRARY=openssl` otherwise the installation of `urlgrabber` will fail. .. code:: bash pip install pycurl urlgrabber - Install the osc version referenced by this repository: .. code:: bash git submodule init git submodule update pip install ./osc - Install this project into the virtualenvironment: .. code:: bash pip install -e . Bug reporting ------------- Bugs can be reported using `bugzilla`_: set the product to ``SUSE Tools`` and choose the component ``oscqam``. .. _bugzilla: https://bugzilla.suse.com 0707010000000C000081A4000000000000000000000001644668C300000291000000000000000000000000000000000000003A00000000osc-plugin-qam-1.0.3+git0.420bf95/Documentation/index.rst.. oscqam documentation master file, created by sphinx-quickstart on Wed Jan 7 15:51:28 2015. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. Welcome to oscqam's documentation! ================================== This package provides the plugin for the osc_ tool that adds additional features to support the QA-Maintenance workflow. Contents: .. toctree:: :maxdepth: 2 install workflows/workflows devel todo bugs ChangeLog Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` .. _osc: https://github.com/openSUSE/osc 0707010000000D0000A1FF000000000000000000000001644669230000000D000000000000000000000000000000000000003C00000000osc-plugin-qam-1.0.3+git0.420bf95/Documentation/install.rst../README.rst0707010000000E000081A4000000000000000000000001644668C3000005B1000000000000000000000000000000000000003900000000osc-plugin-qam-1.0.3+git0.420bf95/Documentation/todo.rstTodo ==== This little section documents known issues or outstanding features requests that should be addressed in the future: 1. Readline support ------------------- Currently not possible for multiple reasons: ``osc`` includes an outdated replacement for the ``cmd`` module that breaks readline functionality (https://github.com/trentm/cmdln/issues/1). .. note:: This has been fixed in the osc-upstream project on github. As soon as a release occurred this part of the problem is solved. Moreover ``osc`` replaces the ``stdout`` and ``stderr`` file-descriptor with a unicode aware wrapper-object. Even when the fixes to the ``cmd`` replacement are made, this wrapper still prevents ``readline`` from working. .. note:: It is possible to just replace the wrapper descriptors with the original ones to allow readline support. However this might lead to problems, as osc probably did not include the wrapper object just for fun. 2. openSUSE support ------------------- The openSUSE build service uses a different template format that needs some adjustments in ``models.py``. 3. Change to server-side unassign --------------------------------- As soon as the ``unassign`` action is implemented server-side this logic should be used. Once this is changed all the code to best-guess the group the user was reviewing for should also be purged from the ``Request`` class. 0707010000000F000041ED000000000000000000000002644668C300000000000000000000000000000000000000000000003A00000000osc-plugin-qam-1.0.3+git0.420bf95/Documentation/workflows07070100000010000081A4000000000000000000000001644668C30000032B000000000000000000000000000000000000004600000000osc-plugin-qam-1.0.3+git0.420bf95/Documentation/workflows/manager.rst================= Manager workflows ================= Manager workflows provide overview over the current update-situation. List assigned updates ===================== List all updates assigned to a member of a QAM group. .. code:: bash ibs qam assigned If you wish to only see updates that are assigned for a specific group you can use the '-G' or '--group' flag with the group as argument. You can also show reviewable updates for multiple groups by passing the flag more than once. .. code:: bash ibs qam assigned -G 'qam-manager' -G 'qam-sle' List unassigned groups for updates ================================== List all unassigned QAM groups for updates: .. code:: bash ibs qam open -T -F ReviewRequestID -F SRCRPMs -F Rating -F Products -F "Incident Priority" -F "Unassigned Roles" 07070100000011000081A4000000000000000000000001644668C300001D41000000000000000000000000000000000000004500000000osc-plugin-qam-1.0.3+git0.420bf95/Documentation/workflows/tester.rst================ Tester workflows ================ There are three workflows: 1. `Find and test an update`_ 2. `List updates you are currently testing`_ 3. `Check comments`_ Find and test an update ======================= 1. Find a matching update to test. 2. Assign the update. 3. Approve or reject the update depending on the test outcome. Finding updates --------------- To find updates you can test, use the ``open`` command: .. code:: bash ibs qam open .. note:: The ``open`` command was called ``list`` before version ``0.6.0``. Instead of adding more options to ``list`` (e.g. ``list open``, ``list assigned``), the command was split into new commands that describe the state of the updates that will be output: - ``open``: the update requires testing. - ``assigned``: the update is already being tested. The ``open`` command will list all updates that fulfil (at least) one of the following properties: 1. You are a member of a group that is not yet being reviewed. 2. You are already reviewing for the request, but the review is not yet finished. If you wish to only see updates for a specific group that are open for review you can use the '-G' or '--group' flag with the group as argument. You can also show reviewable updates for multiple groups by passing the flag more than once. .. code:: bash ibs qam open -G 'qam-manager' -G 'qam-sle' Assigning updates ----------------- To assign an update use the full identifier of a request: .. code:: bash ibs qam assign SUSE:Maintenance:123:12345 Using only the request-id is also possible. .. note:: The request-id is the last numerical part of the full identifier: :: SUSE:Maintenance:<incident_id>:<request_id> .. code:: bash ibs qam assign 12345 By default the ``assign``-command (as well as the ``unassign``-command) will try to automatically find the group you can be assigned (or unassigned) for. This might not work if the plugin finds more than one possible group: in this case pass the group names explicitly via the ``-G`` flag (you can pass more than one group by repeating the flag). .. code:: bash ibs qam assign -G 'qam-atk' -G 'qam-sle' 12345 Unassigning updates ------------------- When you realize that an update can not be tested or finished by you, you can unassign yourself. When you are assigned to multiple groups, using the command without any ``--group`` flag will unassign *all* groups you are assigned for. Passing the ``--group`` flag will only unassign the passed groups: .. code:: bash ibs qam unassign -G 'qam-atk' 12345 This will leave you as a reviewer for the groups you did not unassign for. Finishing updates ----------------- After testing is done either ``approve`` or ``reject`` the update: Approve ~~~~~~~ If you have assigned some reviews to yourself and want to signal them as finished use the following command: .. code:: bash ibs qam approve 12345 Make sure to set the following fields in your test report: .. code:: text status: PASSED Test Plan Reviewer: <some reviewer> .. note:: Reports with an ambiguous status field (``PASSED/FAILED``) or missing the required other fields will be rejected by the plugin. If for some reason you want to approve a group that you did not assign to yourself you can use the following command: .. code:: bash ibs qam approve -G <group_name> 12345 .. warning:: If you assigned yourself to multiple groups and want to approve only a single group, do **not** use the above ``-G`` command. After assigning multiple groups, it is impossible to only approve a single one, as this can not be modeled in the build service: in the build service there is only one review *per user*, even if you assigned yourself to multiple groups. In that case you have to make sure that the reviews for *both* groups are finished before running the normal ``ibs qam approve`` command. Reject ~~~~~~ .. code:: bash ibs qam reject 12345 Make sure to set the following fields in your test report: .. code:: text status: FAILED comment: <reason> You have to either provide a ``reason`` using a ``flag`` (``--reason``) or use the interactive UI when rejecting a request. The possible values of the reason flag can be checked using the help: .. code:: bash ibs qam help reject reject: Reject the request for the user. The command either uses the configured user or the user passed via the `-u` flag. Usage: osc qam reject REQUEST_ID Options: -h, --help show this help message and exit -R REASON, --reason=REASON Reason the request was rejected: admin, retracted, build_problem, not_fixed, regression, false_reject, tracking_issue -M MESSAGE, --message=MESSAGE Message to use for rejection-comment. -U USER, --user=USER User that rejects this request. A more detailed listing of possible reasons for rejection (including examples): 1) Administrative - more fixes - Security overrides Maintenance 2) Retracted request - not needed - not fixed (and reported by other parties) - End of life of the product 3) Build problems - problem with the build/release numbers - wrong channels/products/architectures - missing packages in the build (not in patchinfo!) 4) Tracked issue(s) not fixed - bad upstream fix - bad back-port - incomplete fix 5) Regression - run-time regression - dependency/installation issue 6) False reject - test setup error - manager override to release despite findings 7) Incident tracking issues: - bad bug list - bad CVE list - other issues with patchinfo metadata List updates you are currently testing ====================================== To see which updates are currently being tested by you (or another user), use the ``assigned`` command with the ``-U`` parameter: .. code:: bash ibs qam assigned -U <user> To list the updates currently tested by you, a shortcut command is provided as well: ``my``, which is equivalent to ``ibs qam assigned -U "$your_username"`` .. code:: bash ibs qam my Check comments ============== Apart from working with requests the plugin also allows viewing, adding and removing comments attached to requests. Add a comment ------------- To add a comment to a request use the ``comment`` command: .. code:: bash ibs qam comment <request_id> "<comment_message>" View comments ------------- It is possible to have comments be part of the output of any command that allows the use of the ``--fields`` parameter. Simple add a ``--fields Comments`` field to your desired output. .. code:: bash ibs qam list --fields ReviewRequestID --fields Comments --fields Rating Delete comments --------------- To remove a comment you added to a request use the ``deletecomment`` or ``rmcomment`` command with the ``ReviewRequestID`` you want to remove a comment from. .. code:: bash ibs qam deletecomment <request_id> The plugin will then list all found comments and you have to input the comment_id of the comment you want to remove: .. code:: bash CommentID: Message ------------------ 11946: OK Comment-Id to remove: In the given example input 11946 to remove the comment. .. note:: You can only remove comments that you created yourself. 07070100000012000081A4000000000000000000000001644668C3000000A8000000000000000000000000000000000000004800000000osc-plugin-qam-1.0.3+git0.420bf95/Documentation/workflows/workflows.rst.. _workflows: Workflows ######### The plugin supports workflows for two kind of users: `testers` and `managers`. .. toctree:: :maxdepth: 1 tester manager 07070100000013000081A4000000000000000000000001644668C3000046AC000000000000000000000000000000000000002A00000000osc-plugin-qam-1.0.3+git0.420bf95/LICENSE GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. <one line to give the program's name and a brief idea of what it does.> Copyright (C) <year> <name of author> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. <signature of Ty Coon>, 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. 07070100000014000081A4000000000000000000000001644668C300000026000000000000000000000000000000000000002E00000000osc-plugin-qam-1.0.3+git0.420bf95/MANIFEST.ininclude Documentation/* include *.rst 07070100000015000081A4000000000000000000000001644668C30000013A000000000000000000000000000000000000002B00000000osc-plugin-qam-1.0.3+git0.420bf95/Makefile.PHONY: all all: .PHONY: only-test only-test: python3 -m pytest .PHONY: checkstyle checkstyle: black --check --diff ./ .PHONY: tidy tidy: black ./ .PHONY: test-with-coverage test-with-coverage: python3 -m pytest -v --cov=./oscqam --cov-report=xml --cov-report=term .PHONY: test test: only-test checkstyle 07070100000016000081A4000000000000000000000001644668C300000951000000000000000000000000000000000000002D00000000osc-plugin-qam-1.0.3+git0.420bf95/README.rst.. image:: https://github.com/openSUSE/osc-plugin-qam/actions/workflows/ci.yml/badge.svg :target: https://github.com/openSUSE/osc-plugin-qam/actions/workflows/ci.yml .. image:: https://codecov.io/gh/openSUSE/osc-plugin-qam/branch/master/graph/badge.svg?token=JJRU27WKZ0 :target: https://codecov.io/gh/openSUSE/osc-plugin-qam Getting started =============== Overview -------- The plugin provides the following new features: - a new subshell that can be started via ``osc qam`` that only accepts the new commands of this plugin. - it adds command to help with the update workflow. - to see a list of provided commands use ``osc qam help`` and to see what each command does just use ``osc qam help <command>``. For detailed information about common use cases see the :ref:`workflows`. Installation ------------ To install the plugin add the repository for your distribution from here: http://download.suse.de/ibs/QA:/Maintenance/ .. code:: bash zypper ar -f http://download.suse.de/ibs/QA:/Maintenance/<distribution>/QA:Maintenance.repo zypper in osc-plugin-qam Currently supported distributions are: - Tumbleweed - Leap 15.x - SLE 12-SP4+ - SLE 15.x Usage ----- After the package is installed a new command is now available for osc: ``osc qam``. .. note:: The plugin is currently only useful for the *internal* buildservice. You should actually use your alias that uses ``https://api.suse.de`` or add the flag ``--apiurl=https://api.suse.de``. If you do not want to set an alias, you can configure ``osc`` to automatically default to the internal ibs api. Update your ``.oscrc`` ``[general]`` section: .. code:: bash [general] apiurl = https://api.suse.de Running the command without any further arguments will start an interactive session. .. note:: When you are running a older version of ``osc`` (e.g. 0.148) then the readline-support is not working out-of-the-box. Please see :ref:`workarounds` to see how to still get it working. Instead of running the commands in the interactive session it is also possible to just write out the complete command following the osc qam part: The interactive command sequence to list open requests: .. code-block:: bash osc qam osc-qam> list The single command to list open requests: .. code-block:: bash osc qam list 07070100000017000041ED000000000000000000000005644668C300000000000000000000000000000000000000000000002900000000osc-plugin-qam-1.0.3+git0.420bf95/oscqam07070100000018000081A4000000000000000000000001644668C300000016000000000000000000000000000000000000003500000000osc-plugin-qam-1.0.3+git0.420bf95/oscqam/__init__.py__version__ = "1.0.3" 07070100000019000041ED000000000000000000000002644668C300000000000000000000000000000000000000000000003100000000osc-plugin-qam-1.0.3+git0.420bf95/oscqam/actions0707010000001A000081A4000000000000000000000001644668C3000003CB000000000000000000000000000000000000003D00000000osc-plugin-qam-1.0.3+git0.420bf95/oscqam/actions/__init__.pyfrom .approvegrpoupaction import ApproveGroupAction from .approveuseraction import ApproveUserAction from .assignaction import AssignAction from .commentaction import CommentAction from .deletecommentaction import DeleteCommentAction from .infoaction import InfoAction from .listassignedaction import ListAssignedAction from .listassignedgroupaction import ListAssignedGroupAction from .listassigneduseraction import ListAssignedUserAction from .listgroupaction import ListGroupAction from .listopenaction import ListOpenAction from .rejectaction import RejectAction from .unassignaction import UnassignAction PREFIX = "[oscqam]" __all__ = [ "PREFIX", "AssignAction", "ApproveGroupAction", "ApproveUserAction", "ListOpenAction", "ListGroupAction", "ListAssignedAction", "ListAssignedGroupAction", "ListAssignedUserAction", "UnassignAction", "RejectAction", "CommentAction", "InfoAction", "DeleteCommentAction", ] 0707010000001B000081A4000000000000000000000001644668C3000005B1000000000000000000000000000000000000004200000000osc-plugin-qam-1.0.3+git0.420bf95/oscqam/actions/approveaction.pyfrom .oscaction import OscAction import abc import sys from ..models import Template class ApproveAction(OscAction): """Template class for Approval actions. Subclasses need to overwrite: - get_reviewer: whose review is done. """ def __init__( self, remote, user, request_id, reviewer, template_factory=Template, out=sys.stdout, ): """Approve a review for either a User or a Group. :param remote: Remote interface for build service calls. :type remote: L{oscqam.remote.RemoteFacade} :param user: The user performing this action. :type user: L{string} :param request_id: Id of the request to accept. :type request_id: L{int} :param reviewer: Reviewer to accept this request for. :type reviewer: L{oscqam.models.User} | L{oscqam.models.Group} :param template_factory: Function to get a report-template from. :type template_factory: :param out: File like object to write output messages to. :type out: """ super().__init__(remote, user, out) self.request = remote.requests.by_id(request_id) self.template = self.request.get_template(template_factory) self.reviewer = self.get_reviewer(reviewer) @abc.abstractmethod def get_reviewer(self, reviwer): """Return the object for the given reviewer.""" pass 0707010000001C000081A4000000000000000000000001644668C300000291000000000000000000000000000000000000004800000000osc-plugin-qam-1.0.3+git0.420bf95/oscqam/actions/approvegrpoupaction.pyfrom ..errors import NonMatchingGroupsError from .approveaction import ApproveAction class ApproveGroupAction(ApproveAction): APPROVE_MSG = "Approving {request} for group {group}." def get_reviewer(self, reviewer): return self.remote.groups.for_name(reviewer) def validate(self): if self.reviewer not in self.request.groups: raise NonMatchingGroupsError([self.reviewer], self.request.groups) def action(self): self.validate() msg = self.APPROVE_MSG.format(request=self.request, group=self.reviewer) self.print(msg) self.request.review_accept(group=self.reviewer, comment=msg) 0707010000001D000081A4000000000000000000000001644668C300000747000000000000000000000000000000000000004600000000osc-plugin-qam-1.0.3+git0.420bf95/oscqam/actions/approveuseraction.pyfrom ..errors import NoQamReviewsError, NonMatchingUserGroupsError, NotAssignedError from .approveaction import ApproveAction class ApproveUserAction(ApproveAction): """Approve a review for a user.""" APPROVE_MSG = "Approving {request} for {user} ({groups}). " "Testreport: {url}" MORE_GROUPS_MSG = "The following groups could also be reviewed by you: " "{groups}" def get_reviewer(self, reviewer): return self.remote.users.by_name(reviewer) def reviews_assigned(self): """Ensure that the user was assigned before accepting.""" for review in self.request.assigned_roles: if review.user == self.user: return True else: raise NotAssignedError(self.user) def validate(self): """Check preconditions to be met before a request can be approved. :raises: :class:`oscqam.models.TestResultMismatchError` if conditions are not met. """ self.reviews_assigned() self.template.passed() def additional_reviews(self): """Return groups that could also be reviewed by the user.""" return self.user.reviewable_groups(self.request) def action(self): self.validate() url = self.template.fancy_url groups = ", ".join([str(g) for g in self.user.in_review_groups(self.request)]) msg = self.APPROVE_MSG.format( user=self.reviewer, groups=groups, request=self.request, url=url ) self.print(msg) self.request.review_accept(user=self.reviewer, comment=msg) try: groups = ", ".join(str(g) for g in self.additional_reviews()) msg = self.MORE_GROUPS_MSG.format(groups=groups) self.print(msg) except NonMatchingUserGroupsError: pass except NoQamReviewsError: pass 0707010000001E000081A4000000000000000000000001644668C300000FE6000000000000000000000000000000000000004100000000osc-plugin-qam-1.0.3+git0.420bf95/oscqam/actions/assignaction.pyfrom ..errors import ( NoQamReviewsError, NotPreviousReviewerError, ReportNotYetGeneratedError, TemplateNotFoundError, UninferableError, ) from ..models import Request, Template, UserReview from .oscaction import OscAction class AssignAction(OscAction): ASSIGN_MSG = "Assigning {user} to {group} for {request}." AUTO_INFER_MSG = "Found a possible group: {group}." MULTIPLE_GROUPS_MSG = ( "User could review more than one group: {groups}. " "Specify the group to review using the -G flag." ) def __init__( self, remote, user, request_id, groups=None, template_factory=Template, force=False, template_required=True, **kwargs ): super().__init__(remote, user, **kwargs) self.request = remote.requests.by_id(request_id) self.groups = ( [remote.groups.for_name(group) for group in groups] if groups else None ) self.template_factory = template_factory self.template_required = template_required self.force = force def template_exists(self): """Check that the template associated with the request exists. If the template is not yet generated, assigning a user can lead to the template-generator no longer finding the request and never generating the template. """ try: self.request.get_template(self.template_factory) except TemplateNotFoundError as e: raise ReportNotYetGeneratedError(self.request, str(e)) def check_open_review(self) -> None: if self.request.state.name not in Request.OPEN_STATES: raise NoQamReviewsError([]) def check_previous_rejects(self): """If there were previous rejects for an incident users that have already reviewed this incident should (preferably) review it again. If the user trying to assign himself is not one of the previous reviewers a warning is issued. """ related_requests = self.remote.requests.for_incident(self.request.src_project) if not related_requests: return declined_requests = [ request for request in related_requests if request.state.name == Request.STATE_DECLINED ] if not declined_requests: return reviewers = [ review.reviewer for review in (request.review_list() for request in declined_requests) if isinstance(review, UserReview) ] if self.user not in reviewers: raise NotPreviousReviewerError(reviewers) def validate(self): # if tehere isn't open review all other cheks aren't required and can't be overridden by self.force self.check_open_review() if self.force: return if self.template_required: self.template_exists() self.check_previous_rejects() def action(self): if self.groups: self.assign(self.groups) else: group = self.reviewable_group() # TODO: Ensure that the user actually wants this? self.assign(group) def reviewable_group(self): """Based on the given user and request search for a group that the user could do the review for. """ groups = self.user.reviewable_groups(self.request) if len(groups) > 1: raise UninferableError( AssignAction.MULTIPLE_GROUPS_MSG.format(groups=[str(g) for g in groups]) ) group = groups.pop() self.print(AssignAction.AUTO_INFER_MSG.format(group=group)) return [group] def assign(self, groups): self.validate() for group in groups: msg = AssignAction.ASSIGN_MSG.format( user=self.user, group=group, request=self.request ) self.request.review_assign(reviewer=self.user, group=group, comment=msg) self.print(msg) 0707010000001F000081A4000000000000000000000001644668C30000016A000000000000000000000000000000000000004200000000osc-plugin-qam-1.0.3+git0.420bf95/oscqam/actions/commentaction.pyfrom .oscaction import OscAction class CommentAction(OscAction): """Add a comment to a request.""" def __init__(self, remote, user, request_id, comment): super().__init__(remote, user) self.comment = comment self.request = remote.requests.by_id(request_id) def action(self): self.request.add_comment(self.comment) 07070100000020000081A4000000000000000000000001644668C300000130000000000000000000000000000000000000004800000000osc-plugin-qam-1.0.3+git0.420bf95/oscqam/actions/deletecommentaction.pyfrom .oscaction import OscAction class DeleteCommentAction(OscAction): """Delete a comment.""" def __init__(self, remote, user, comment_id): super().__init__(remote, user) self.comment_id = comment_id def action(self): self.remote.comments.delete(self.comment_id) 07070100000021000081A4000000000000000000000001644668C30000027E000000000000000000000000000000000000003F00000000osc-plugin-qam-1.0.3+git0.420bf95/oscqam/actions/infoaction.pyfrom .listaction import ListAction from ..fields import ReportField class InfoAction(ListAction): default_fields = [ ReportField.review_request_id, ReportField.srcrpms, ReportField.rating, ReportField.products, ReportField.incident_priority, ReportField.assigned_roles, ReportField.unassigned_roles, ReportField.creator, ReportField.issues, ] def __init__(self, remote, user_id, request_id): super().__init__(remote, user_id) self.request = remote.requests.by_id(request_id) def load_requests(self): return [self.request] 07070100000022000081A4000000000000000000000001644668C300000C11000000000000000000000000000000000000003F00000000osc-plugin-qam-1.0.3+git0.420bf95/oscqam/actions/listaction.pyimport abc from concurrent.futures import ThreadPoolExecutor, as_completed import logging from ..errors import TemplateNotFoundError from ..fields import ReportField from ..models import Template from ..utils import multi_level_sort from .oscaction import OscAction from .report import Report class ListAction(OscAction): """Base action for operation that work on a list of requests. Subclasses must overwrite the 'load_requests' method that return the list of requests that should be output according to the formatter and fields. """ default_fields = [ ReportField.review_request_id, ReportField.srcrpms, ReportField.rating, ReportField.products, ReportField.incident_priority, ] def group_sort_reports(self): """Sort reports according to rating and request id. First sort by Priority, then rating and finally request id. """ reports = filter(None, self.reports) self.reports = multi_level_sort( reports, [ lambda l: l.request.reqid, lambda l: l.template.log_entries["Rating"], lambda l: l.request.incident_priority, ], ) def __init__(self, remote, user, template_factory=Template): super().__init__(remote, user) self.template_factory = template_factory def action(self): """Return all reviews that match the parameters of the RequestAction.""" self.reports = self._load_listdata(self.load_requests()) self.group_sort_reports() return self.reports @abc.abstractmethod def load_requests(self): """Load requests this class should operate on. :returns: [:class:`oscqam.models.Request`] """ pass def merge_requests(self, user_requests, group_requests): """Merge the requests together and set a field 'origin' to determine where the request came from. """ all_requests = user_requests.union(group_requests) for request in all_requests: request.origin = [] if request in user_requests: request.origin.append(self.user.login) if request in group_requests: request.origin.extend(request.groups) return all_requests def _load_listdata(self, requests): """Load templates for the given requests. Templates that could not be loaded will print a warning (this can occur and not be a problem: e.g. the template creation script has not yet run). :param requests: [:class:`oscqam.models.Request`] :returns: :class:`oscqam.actions.Report`-generator """ with ThreadPoolExecutor() as executor: results = [ executor.submit(Report, r, self.template_factory) for r in requests ] for promise in as_completed(results): try: yield promise.result() except TemplateNotFoundError as e: logging.warning(str(e)) 07070100000023000081A4000000000000000000000001644668C300000354000000000000000000000000000000000000004700000000osc-plugin-qam-1.0.3+git0.420bf95/oscqam/actions/listassignedaction.pyfrom ..fields import ReportField from .listaction import ListAction class ListAssignedAction(ListAction): """Action to list assigned requests.""" default_fields = [ ReportField.review_request_id, ReportField.srcrpms, ReportField.rating, ReportField.products, ReportField.incident_priority, ReportField.assigned_roles, ReportField.creator, ] def in_review_by_user(self, reviews): for review in reviews: if review.reviewer == self.user and review.open: return True return False def load_requests(self): qam_groups = [ group for group in self.remote.groups.all() if group.is_qam_group() ] return { request for request in self.remote.requests.review_for_groups(qam_groups) } 07070100000024000081A4000000000000000000000001644668C30000035A000000000000000000000000000000000000004C00000000osc-plugin-qam-1.0.3+git0.420bf95/oscqam/actions/listassignedgroupaction.pyfrom ..models import Template from .listassignedaction import ListAssignedAction class ListAssignedGroupAction(ListAssignedAction): def __init__(self, remote, user, groups, template_factory=Template): super().__init__(remote, user, template_factory) if not groups: raise AttributeError("Can not list groups without any groups.") self.groups = [self.remote.groups.for_name(group) for group in groups] def in_review(self, reviews): for review in reviews: if review.reviewer in self.groups: return True return False def load_requests(self): group_requests = set(self.remote.requests.review_for_groups(self.groups)) return { request for request in group_requests if self.in_review(request.review_list()) } 07070100000025000081A4000000000000000000000001644668C3000001A1000000000000000000000000000000000000004B00000000osc-plugin-qam-1.0.3+git0.420bf95/oscqam/actions/listassigneduseraction.pyfrom .listassignedaction import ListAssignedAction class ListAssignedUserAction(ListAssignedAction): """Action to list requests that are assigned to the user.""" def load_requests(self): user_requests = set(self.remote.requests.for_user(self.user)) return { request for request in user_requests if self.in_review_by_user(request.review_list()) } 07070100000026000081A4000000000000000000000001644668C3000001FE000000000000000000000000000000000000004400000000osc-plugin-qam-1.0.3+git0.420bf95/oscqam/actions/listgroupaction.pyfrom ..models import Template from .listaction import ListAction class ListGroupAction(ListAction): def __init__(self, remote, user, groups, template_factory=Template): super().__init__(remote, user, template_factory) if not groups: raise AttributeError("Can not list groups without any groups.") self.groups = [self.remote.groups.for_name(group) for group in groups] def load_requests(self): return set(self.remote.requests.open_for_groups(self.groups)) 07070100000027000081A4000000000000000000000001644668C3000003B8000000000000000000000000000000000000004300000000osc-plugin-qam-1.0.3+git0.420bf95/oscqam/actions/listopenaction.pyfrom ..errors import ReportedError from .listaction import ListAction class ListOpenAction(ListAction): def load_requests(self): def assigned(req): """Check if the request is assigned to the user that requests the listing.""" for review in req.assigned_roles: if review.reviewer == self.user: return True return False def filters(req): return req.active() and assigned(req) user_requests = { req for req in self.remote.requests.for_user(self.user) if filters(req) } qam_groups = self.user.qam_groups if not qam_groups: raise ReportedError( "You are not part of a qam group. " "Can not list requests." ) group_requests = set(self.remote.requests.open_for_groups(qam_groups)) return self.merge_requests(user_requests, group_requests) 07070100000028000081A4000000000000000000000001644668C3000005AD000000000000000000000000000000000000003E00000000osc-plugin-qam-1.0.3+git0.420bf95/oscqam/actions/oscaction.pyimport abc import os import sys from ..remotes import RemoteError class OscAction(metaclass=abc.ABCMeta): """Base class for actions that need to interface with the open build service.""" def __init__(self, remote, user, out=sys.stdout): """ :param remote: Remote endpoint to the buildservice. :type remote: :class:`oscqam.models.RemoteFacade` :param user: Username that performs the action. :type user: str :param out: Filelike to print enduser-messages to. :type out: :class:`file` """ self.remote = remote self.user = remote.users.by_name(user) self.undo_stack = [] self.out = out def __call__(self, *args, **kwargs): """Will attempt the encapsulated action and call the rollback function if an Error is encountered. """ try: return self.action(*args, **kwargs) except RemoteError as e: print(str(e)) self.rollback() @abc.abstractmethod def action(self, *args, **kwargs): pass def rollback(self): for action in self.undo_stack: action() def print(self, msg, end=os.linesep): """Mimick the print-statements behaviour on the out-stream: Print the given message and add a newline. :type msg: str """ self.out.write(msg) self.out.write(end) self.out.flush() 07070100000029000081A4000000000000000000000001644668C300000693000000000000000000000000000000000000004100000000osc-plugin-qam-1.0.3+git0.420bf95/oscqam/actions/rejectaction.pyimport sys from ..errors import NoCommentError from ..models import Template from .oscaction import OscAction class RejectAction(OscAction): """Reject a request for a user and group. Attempts to automatically find the group that the user assigned himself for and will reject that group if possible. """ DECLINE_MSG = "Declining request {request} for {user}. See Testreport: {url}" def __init__( self, remote, user, request_id, reason, force, message=None, out=sys.stdout ): super(RejectAction, self).__init__(remote, user, out=out) self.request = remote.requests.by_id(request_id) self._template = None if not force else "There is no template" self.reason = reason self.message = message self.force: bool = force @property def template(self): if not self._template: self._template = Template(self.request) return self._template def validate(self): """Check preconditions to be met before a request can be approved. :raises: :class:`oscqam.models.TestResultMismatchError` if conditions are not met. """ self.template.failed() if not self.template.log_entries["comment"]: raise NoCommentError() def action(self): if not self.force: self.validate() url = self.template.fancy_url else: url = self.template msg = RejectAction.DECLINE_MSG.format( user=self.user, request=self.request, url=url ) self.print(msg) self.request.review_decline(user=self.user, comment=msg, reasons=self.reason) 0707010000002A000081A4000000000000000000000001644668C300000707000000000000000000000000000000000000003B00000000osc-plugin-qam-1.0.3+git0.420bf95/oscqam/actions/report.pyfrom ..fields import ReportField from ..models import GroupReview class Report: """Composes request with the matching template. Provides a method to output a list of keys from requests/templates and will dispatch to the correct object. """ def __init__(self, request, template_factory): """Associate a request with the correct template.""" self.request = request self.template = request.get_template(template_factory) def value(self, field): """Return the values for fields. :type keys: [:class:`actions.ReportField`] :param keys: Identifiers for the data to be returned from the template or associated request. :returns: [str] """ entries = self.template.log_entries if field == ReportField.unassigned_roles: reviews = ( review for review in self.request.review_list_open() if isinstance(review, GroupReview) and review.reviewer.is_qam_group() ) value = sorted(str(r.reviewer) for r in reviews) elif field == ReportField.package_streams: value = [p for p in self.request.packages] elif field == ReportField.assigned_roles: roles = self.request.assigned_roles value = [str(r) for r in roles] elif field == ReportField.incident_priority: value = self.request.incident_priority elif field == ReportField.comments: value = self.request.comments elif field == ReportField.creator: value = self.request.maker elif field == ReportField.issues: value = str(len(self.request.issues)) else: value = entries[str(field)] return value 0707010000002B000081A4000000000000000000000001644668C30000093C000000000000000000000000000000000000004300000000osc-plugin-qam-1.0.3+git0.420bf95/oscqam/actions/unassignaction.pyimport logging from ..errors import NoReviewError from .oscaction import OscAction class UnassignAction(OscAction): """Will unassign the user from the review and reopen the request for the group the user assign himself for. """ UNASSIGN_MSG = "Unassigning {user} from {request} for group {group}." def __init__(self, remote, user, request_id, groups=None, **kwargs): super().__init__(remote, user, **kwargs) self.request = remote.requests.by_id(request_id) if groups: self._groups = [remote.groups.for_name(group) for group in groups] else: self._groups = None def groups(self): if self._groups: return self._groups return self.review_groups() def action(self): assigned_groups = self.review_groups() self.unassign(self.groups(), assigned_groups) def review_groups(self): """Find the exact group the user is currently reviewing and return it. :return: Group in review by the user. :raise NoReviewError: If the user is not reviewing any group of the request. :raise MultipleReviewsError: If more than one group is assigned to the user. """ groups = self.user.in_review_groups(self.request) if not groups: raise NoReviewError(self.user) return groups def undo_reopen(self, group, comment): def _(): self.print("UNDO: Undoing reopening of group {group}".format(group=group)) self.request.review_accept(group=group, comment=comment) return _ def undo_accept(self, user): def _(): self.print("UNDO: Undoing accepting user {user}".format(user=user)) self.request.review_reopen(user=self.user) return _ # TODO: this action should check and unassign only groups assigned to user.. def unassign(self, groups, user_assigned_groups): for group in groups: msg = UnassignAction.UNASSIGN_MSG.format( user=self.user, group=group, request=self.request ) self.print(msg) logging.debug( "Reverting assignment from %s back to %s" % (group, self.user) ) self.request.review_unassign(reviewer=self.user, group=group, comment=msg) 0707010000002C000081A4000000000000000000000001644668C3000000DF000000000000000000000000000000000000003000000000osc-plugin-qam-1.0.3+git0.420bf95/oscqam/cli.pyimport osc.commandline from oscqam import __version__ as strict_version class QAMCommand(osc.commandline.OscCommand): """QE-Maintenace rewiew workflow helper""" name = "qam" def run(self, _): pass 0707010000002D000081A4000000000000000000000001644668C300000566000000000000000000000000000000000000003800000000osc-plugin-qam-1.0.3+git0.420bf95/oscqam/cli_approve.pyimport osc.commandline from oscqam.actions import ApproveGroupAction, ApproveUserAction from oscqam.common import Common class QAMApproveCommand(osc.commandline.OscCommand, Common): """Approve the request for the user""" name = "approve" parent = "QAMCommand" def init_arguments(self): self.add_argument( "-G", "--group", help="Group to *directly* approve for this request." "Only for groups that do not need reviews", ) self.add_argument("request_id", type=str, help="ID of review request") def run(self, args): self.set_required_params(args) if args.group: if self.yes_no( "This can *NOT* be used to accept a specific group " "you are reviewing. " "It will only accept the group's review. " "This can bring the update into an inconsistent state.\n" "You probably only want to run 'osc qam approve'.\nAbort?", default="yes", ): return action = ApproveGroupAction( self.api, self.affected_user, args.request_id, args.group ) else: action = ApproveUserAction( self.api, self.affected_user, args.request_id, self.affected_user ) action() 0707010000002E000081A4000000000000000000000001644668C3000007B8000000000000000000000000000000000000003700000000osc-plugin-qam-1.0.3+git0.420bf95/oscqam/cli_assign.pyimport osc.commandline from oscqam.actions import AssignAction from oscqam.common import Common from oscqam.errors import NotPreviousReviewerError class QAMAssignCommand(osc.commandline.OscCommand, Common): """Assign the request to the user. The command either uses the user that runs the osc command or the user that was passed as part of the command via the -U flag. It will attempt to automatically find a group that is not currently reviewed, but that the user could review for. If no group can be automatically determined a group must be passed as an argument.""" name = "assign" parent = "QAMCommand" def init_arguments(self): self.add_argument("request_id", type=str, help="ID of review request") self.add_argument("-U", "--user", help="User to assign for this request.") self.add_argument( "-G", "--group", action="append", help="Groups to assign the user for." "Pass multiple groups passing flag multiple times.", ) self.add_argument( "--skip-template", action="store_true", help="Do not check whether a template exists.", ) def run(self, args): self.set_required_params(args) group = args.group if args.group else None template_required = False if args.skip_template else True action = AssignAction( self.api, self.affected_user, args.request_id, group, template_required=template_required, ) try: action() except NotPreviousReviewerError as e: print(str(e)) force = self.yes_no("Do you still want to assign yourself?") if not force: return action = AssignAction( self.api, self.affected_user, args.request_id, group, force=force ) action() 0707010000002F000081A4000000000000000000000001644668C30000098E000000000000000000000000000000000000003900000000osc-plugin-qam-1.0.3+git0.420bf95/oscqam/cli_assigned.pyimport osc.commandline from oscqam.actions import ( ListAssignedAction, ListAssignedGroupAction, ListAssignedUserAction, ) from oscqam.common import Common from oscqam.errors import ConflictingOptions from oscqam.fields import ReportFields class QAMAssignedCommand(osc.commandline.OscCommand, Common): """Show a list of OBS qam-requests that are in review. A request is in review, as soon as a user has been assigned for a group that is required to review a request.""" name = "assigned" parent = "QAMCommand" def init_arguments(self): self.add_argument( "-G", "--group", action="append", default=[], help="Only requests containing assigned reviews for the " "given groups will be output.", ) self.add_argument( "-F", "--fields", action="append", default=[], help="Define the values to output in a cumulative fashion " "(pass flag multiple times). " "Available fields: " + self.all_columns_string + ".", ) self.add_argument( "-U", "--user", default=None, help="List requests assigned to the given USER.", ) self.add_argument( "-T", "--tabular", action="store_true", help="Output the requests in an ASCII-table.", ) self.add_argument( "-V", "--describe-fields", action="store_true", help="Display all available fields for a request: " + self.all_columns_string + ".", ) def run(self, args): if args.verbose and args.fields: raise ConflictingOptions("Only pass '-v' or '-F' not both") if args.user and args.group: raise ConflictingOptions("Only pass '-U' or '-G' not both") self.set_required_params(args) fields = ReportFields.review_fields_by_opts(args) if args.user: action = ListAssignedUserAction(self.api, self.affected_user) elif args.group: action = ListAssignedGroupAction(self.api, self.affected_user, args.group) else: action = ListAssignedAction(self.api, self.affected_user) keys = fields.fields(action) self.list_requests(action, args.tabular, keys) 07070100000030000081A4000000000000000000000001644668C300000337000000000000000000000000000000000000003800000000osc-plugin-qam-1.0.3+git0.420bf95/oscqam/cli_comment.pyimport osc.commandline from oscqam.actions import CommentAction from oscqam.common import Common from oscqam.errors import MissingCommentError class QAMCommentCommand(osc.commandline.OscCommand, Common): """Add a comment to a request. The command will add a comment to the given request.""" name = "comment" parent = "QAMCommand" def init_arguments(self): self.add_argument("request_id", type=str, help="ID of review request") self.add_argument("comment", nargs="*", type=str, help="Text of comment") def run(self, args): self.set_required_params(args) if not args.comment: raise MissingCommentError comment = " ".join(args.comment) action = CommentAction(self.api, self.affected_user, args.request_id, comment) action() 07070100000031000081A4000000000000000000000001644668C300000630000000000000000000000000000000000000003500000000osc-plugin-qam-1.0.3+git0.420bf95/oscqam/cli_info.pyimport osc.commandline from oscqam.actions import InfoAction from oscqam.common import Common from oscqam.errors import ConflictingOptions from oscqam.fields import ReportFields class QAMInfoCommand(osc.commandline.OscCommand, Common): """Show information for the given request.""" name = "info" parent = "QAMCommand" def init_arguments(self): self.add_argument( "-F", "--fields", action="append", default=[], help="Define the values to output in a cumulative fashion " "(pass flag multiple times). " "Available fields: " + self.all_columns_string + ".", ) self.add_argument("request_id", type=str, help="ID of review request") self.add_argument( "-T", "--tabular", action="store_true", help="Output the requests in an ASCII-table.", ) self.add_argument( "-V", "--describe-fields", action="store_true", help="Display all available fields for a request: " + self.all_columns_string + ".", ) def run(self, args): if args.describe_fields and args.fields: raise ConflictingOptions("Only pass '-v' or '-F' not both") self.set_required_params(args) fields = ReportFields.review_fields_by_opts(args) action = InfoAction(self.api, self.affected_user, args.request_id) keys = fields.fields(action) self.list_requests(action, args.tabular, keys) 07070100000032000081A4000000000000000000000001644668C3000008E8000000000000000000000000000000000000003500000000osc-plugin-qam-1.0.3+git0.420bf95/oscqam/cli_list.pyimport osc.commandline from oscqam.actions import ListGroupAction, ListOpenAction from oscqam.common import Common from oscqam.errors import ConflictingOptions from oscqam.fields import ReportFields class QAMListCommand(osc.commandline.OscCommand, Common): """Show a list of OBS qam-requests that are open. By default, open requests assignable to yourself will be shown (currently assigned to a qam-group you are a member of.)""" name = "list" parent = "QAMCommand" aliases = ["open"] def init_arguments(self): self.add_argument( "-F", "--fields", action="append", default=[], help="Define the values to output in a cumulative fashion " "(pass flag multiple times). " "Available fields: " + self.all_columns_string + ".", ) self.add_argument( "-T", "--tabular", action="store_true", help="Output the requests in an ASCII-table.", ) self.add_argument( "-V", "--describe-fields", action="store_true", help="Display all available fields for a request: " + self.all_columns_string + ".", ) self.add_argument( "-G", "--group", action="append", help="Only requests containing open reviews for the given " "groups will be output.", ) self.add_argument( "-U", "--user", default=None, help="List requests assignable to the given USER " "(USER is a member of a qam-group that has an open " "review for the request).", ) def run(self, args): if args.describe_fields and args.fields: raise ConflictingOptions("Only pass '-v' or '-F' not both") self.set_required_params(args) fields = ReportFields.review_fields_by_opts(args) if args.group: action = ListGroupAction(self.api, self.affected_user, args.group) else: action = ListOpenAction(self.api, self.affected_user) keys = fields.fields(action) self.list_requests(action, args.tabular, keys) 07070100000033000081A4000000000000000000000001644668C3000005EC000000000000000000000000000000000000003300000000osc-plugin-qam-1.0.3+git0.420bf95/oscqam/cli_my.pyimport osc.commandline from oscqam.actions import ListAssignedUserAction from oscqam.common import Common from oscqam.errors import ConflictingOptions from oscqam.fields import ReportFields class QAMMyCommand(osc.commandline.OscCommand, Common): """list ....""" name = "my" parent = "QAMCommand" def init_arguments(self): self.add_argument( "-F", "--fields", action="append", default=[], help="Define the values to output in a cumulative fashion " "(pass flag multiple times). " "Available fields: " + self.all_columns_string + ".", ) self.add_argument( "-T", "--tabular", action="store_true", help="Output the requests in an ASCII-table.", ) self.add_argument( "-V", "--describe-fields", action="store_true", help="Display all available fields for a request: " + self.all_columns_string + ".", ) def run(self, args): if args.describe_fields and args.fields: raise ConflictingOptions("Only pass '-v' or '-F' not both") self.set_required_params(args) args.user = self.affected_user fields = ReportFields.review_fields_by_opts(args) action = ListAssignedUserAction(self.api, self.affected_user) keys = fields.fields(action) self.list_requests(action, args.tabular, keys) 07070100000034000081A4000000000000000000000001644668C3000006AE000000000000000000000000000000000000003700000000osc-plugin-qam-1.0.3+git0.420bf95/oscqam/cli_reject.pyimport osc.commandline from oscqam.actions import RejectAction from oscqam.common import Common from oscqam.reject_reasons import RejectReason class QAMRejectCommand(osc.commandline.OscCommand, Common): """Reject the request for the user. The command either uses the configured user or the user passed via the `-U` flag. """ name = "reject" parent = "QAMCommand" def init_arguments(self): self.add_argument("request_id", type=str, help="ID of review request") self.add_argument("-U", "--user", help="User that rejects this request.") self.add_argument( "-M", "--message", help="Message to use for rejection-comment." ) self.add_argument( "-R", "--reason", action="append", help="Reason the request was rejected: " + self.all_reasons_string, ) self.add_argument( "--skip-template", action="store_true", help="Do not check whether a template exists.", ) def run(self, args): message = args.message if args.message else None reasons = ( [RejectReason.from_str(r) for r in args.reason] if args.reason else self.query_enum(RejectReason, lambda r: r.enum_id, lambda r: r.text) ) if reasons == self.SUBQUERY_QUIT: return self.set_required_params(args) template_skip: bool = False if args.skip_template else True action = RejectAction( self.api, self.affected_user, args.request_id, reasons, template_skip, message, ) action() 07070100000035000081A4000000000000000000000001644668C3000004D7000000000000000000000000000000000000003A00000000osc-plugin-qam-1.0.3+git0.420bf95/oscqam/cli_rmcomment.pyimport osc.commandline from oscqam.actions import DeleteCommentAction from oscqam.common import Common from oscqam.errors import InvalidCommentIdError, NoCommentsError class QAMDeleteCommentCommand(osc.commandline.OscCommand, Common): """Remove a comment for the given request. The command will list all available comments of the request to allow choosing the one to remove.""" name = "deletecomment" parent = "QAMCommand" aliases = ["rmcomment"] def init_arguments(self): self.add_argument("request_id", type=str, help="ID of review request") def run(self, args): self.set_required_params(args) request = self.api.requests.by_id(args.request_id) if not request.comments: raise NoCommentsError() print("CommentID: Message") print("------------------") for comment in request.comments: print("{0}: {1}".format(comment.id, comment.text)) comment_id = input("Comment-Id to remove: ") if comment_id not in [c.id for c in request.comments]: raise InvalidCommentIdError(comment_id, request.comments) action = DeleteCommentAction(self.api, self.affected_user, comment_id) action() 07070100000036000081A4000000000000000000000001644668C300000492000000000000000000000000000000000000003900000000osc-plugin-qam-1.0.3+git0.420bf95/oscqam/cli_unassign.pyimport osc.commandline from oscqam.actions import UnassignAction from oscqam.common import Common class QAMUnassignCommand(osc.commandline.OscCommand, Common): """Unassign the request for the user. The command either uses the configured user or the user passed via the `-U` flag. It will attempt to automatically find the group that the user is reviewing for. If the group can not be automatically determined it must be passed as an argument. """ name = "unassign" parent = "QAMCommand" def init_arguments(self): self.add_argument("request_id", type=str, help="ID of review request") self.add_argument("-U", "--user", help="User to assign for this request.") self.add_argument( "-G", "--group", action="append", help="Groups to reassign to this request." "Pass multiple groups passing flag multiple times.", ) def run(self, args): self.set_required_params(args) group = args.group if args.group else None action = UnassignAction(self.api, self.affected_user, args.request_id, group) action() 07070100000037000081A4000000000000000000000001644668C300000101000000000000000000000000000000000000003800000000osc-plugin-qam-1.0.3+git0.420bf95/oscqam/cli_version.pyimport osc.commandline from oscqam import __version__ as version class QAMVersionCommand(osc.commandline.OscCommand): """Print version of osc-plugin-qam""" name = "version" parent = "QAMCommand" def run(self, _): print(version) 07070100000038000081A4000000000000000000000001644668C300000B48000000000000000000000000000000000000003300000000osc-plugin-qam-1.0.3+git0.420bf95/oscqam/common.pyimport osc.conf from oscqam.fields import ReportFields from oscqam.formatters import TabularOutput, VerboseOutput from oscqam.reject_reasons import RejectReason from oscqam.remotes import RemoteFacade class Common: SUBQUERY_QUIT = 4 all_columns_string = ", ".join(str(f) for f in ReportFields.all_fields) all_reasons_string = ", ".join(r.flag for r in RejectReason) def set_required_params(self, args): self.apiurl = args.apiurl self.api = RemoteFacade(self.apiurl) self.affected_user = None if hasattr(args, "user") and args.user: self.affected_user = args.user else: self.affected_user = osc.conf.get_apiurl_usr(self.apiurl) def list_requests(self, action, tabular, keys): listdata = action() formatter = TabularOutput() if tabular else VerboseOutput() if listdata: print(formatter.output(keys, listdata)) @staticmethod def yes_no(question: str, default: str = "no") -> bool: if default not in ("yes", "no"): raise ValueError("Default must be 'yes' or 'no'") valid = {"y": True, "yes": True, "n": False, "no": False} if default == "yes": default = "y" prompt = "[Y/n]" else: default = "n" prompt = "[y/N]" while True: answer = input(" ".join([question, prompt])).lower() if not answer: return valid[default] elif valid.get(answer, None) is not None: return valid[answer] else: print("Invalid choice, please use 'yes' or 'no'") @classmethod def query_enum(cls, enum, tid, desc): """Query the user to specify one specific option from an enum. The enum needs a method 'from_id' that returns the enum for the given id. :param enum: The enum class to query for :param id: Function that returns a unique id for a enum-member. :type id: enum -> object :param desc: Function that returns a descriptive text for a enum-member. :type id: enum -> str :returns: enum selected by the user. """ ids = [tid(member) for member in enum] for member in enum: print("{0}. {1}".format(tid(member), desc(member))) print("q. Quit") user_input = input( "Please specify the options " "(separate multiple values with ,): " ) if user_input.lower() == "q": return cls.SUBQUERY_QUIT numbers = [int(s.strip()) for s in user_input.split(",")] for number in numbers: if number not in ids: print("Invalid number specified: {0}".format(number)) return cls.query_enum(enum, tid, desc) return [enum.from_id(i) for i in numbers] 07070100000039000081A4000000000000000000000001644668C3000004CC000000000000000000000000000000000000003400000000osc-plugin-qam-1.0.3+git0.420bf95/oscqam/domains.py"""Defines domain-objects that encapsulate state without logic. """ from functools import total_ordering @total_ordering class Rating: """Store a template's rating.""" mapping = {"critical": 0, "important": 1, "moderate": 2, "low": 3, "": 4} def __init__(self, rating): self.rating = rating def __lt__(self, other): return self.mapping.get(self.rating, 10) < self.mapping.get(other.rating, 10) def __eq__(self, other): return self.rating == other.rating def __str__(self): return self.rating @total_ordering class Priority: """Store the priority of this request's associated incident.""" def __init__(self, prio): self.priority = int(prio) def __eq__(self, other): return self.priority == other.priority def __lt__(self, other): return self.priority > other.priority def __str__(self): return "{0}".format(self.priority) class UnknownPriority(Priority): def __init__(self): self.priority = None def __eq__(self, other): return isinstance(other, UnknownPriority) def __lt__(self, other): return False def __str__(self): return "{0}".format(self.priority) 0707010000003A000081A4000000000000000000000001644668C3000017D5000000000000000000000000000000000000003300000000osc-plugin-qam-1.0.3+git0.420bf95/oscqam/errors.pyfrom osc.oscerr import OscBaseError class ReportedError(OscBaseError): """Raise on exceptions that can only be reported but not handled.""" return_code = 10 class UninferableError(ReportedError, ValueError): """Error to raise when the program should try to auto-infer some values, but can not do so due to ambiguity. """ class NoQamReviewsError(UninferableError): """Error when no qam groups still need a review.""" def __init__(self, accepted_reviews): """Create new error for accepted reviews.""" message = "No 'qam'-groups need review." from oscqam.models.review import GroupReview accept_reviews = [ review for review in accepted_reviews if isinstance(review, GroupReview) ] message += ( ( " The following groups were already assigned/finished: " "{msg}".format( msg=", ".join( ["{r.reviewer}".format(r=review) for review in accept_reviews] ) ) ) if accept_reviews else "" ) super().__init__(message) class NonMatchingGroupsError(UninferableError): _msg = ( "Expected groups and found groups don't match: " "Expected: {eg}, found-groups: {fg}." ) def __init__(self, expected_groups, found_groups): message = self._msg.format( eg=[g.name for g in expected_groups], fg=[r.name for r in found_groups], ) super().__init__(message) class NonMatchingUserGroupsError(UninferableError): """Error when the user is not a member of a group that still needs to review the request. """ _msg = ( "User groups and required groups don't match: " "User-groups: {ug}, required-groups: {og}." ) def __init__(self, user, user_groups, open_groups): message = self._msg.format( user=user, ug=[g.name for g in user_groups], og=[r.name for r in open_groups], ) super().__init__(message) class InvalidRequestError(ReportedError): """Raise when a request object is missing required information.""" def __init__(self, request): super(InvalidRequestError, self).__init__( "Invalid build service request: {0}".format(request) ) class MissingSourceProjectError(InvalidRequestError): """Raise when a request is missing the source project property.""" def __init__(self, request): super().__init__( "Invalid build service request: " "{0} has no source project.".format(request) ) class TemplateNotFoundError(ReportedError): """Raise when a template could not be found.""" def __init__(self, message): super().__init__("Report could not be loaded: {0}".format(message)) class TestResultMismatchError(ReportedError): _msg = "Request-Status not '{0}': please check report: {1}" def __init__(self, expected, log_path): super().__init__(self._msg.format(expected, log_path)) class ActionError(ReportedError): """General error to raise when an error occurred while performing one of the actions. """ class NoReviewError(UninferableError): """Error to raise when a user attempts an unassign action for a request he did not start a review for. """ def __init__(self, user): super().__init__("User {u} is not assigned for any groups.".format(u=user)) class MultipleReviewsError(UninferableError): """Error to raise when a user attempts an unassign action for a request he is reviewing for multiple groups at once. """ def __init__(self, user, groups): super().__init__( "User {u} is currently reviewing for mulitple groups: {g}." "Please provide which group to unassign via -G parameter.".format( u=user, g=groups ) ) class ReportNotYetGeneratedError(ReportedError): _msg = ( "The report for request '{0}' is not generated yet. " "To prevent bugs in the template parser, assigning " "is not yet possible. " "You can also inspect the server log at {1}" ) def __init__(self, request, log_path): super().__init__( self._msg.format( str(request), log_path.replace("/testreports/", "/reports/") ) ) class OneGroupAssignedError(ReportedError): _msg = ( "User {user} is already assigned for group {group}. " "Assigning for multiple groups at once is currently not allowed " "to prevent inconsistent states in the build service." ) def __init__(self, assignment): super().__init__( self._msg.format(user=str(assignment.user), group=str(assignment.group)) ) class NotPreviousReviewerError(ReportedError): _msg = ( "This request was previously rejected and you were not part " "of the previous set of reviewers: {reviewers}." ) def __init__(self, reviewers): super().__init__(self._msg.format(reviewers=reviewers)) class NoCommentError(ReportedError): _msg = ( "The request you want to reject must have a comment set in the " "testreport." ) def __init__(self): super().__init__(self._msg) class NotAssignedError(ReportedError): _msg = "The user {user} is not assigned to this update." def __init__(self, user): super().__init__(self._msg.format(user=user)) class ConflictingOptions(ReportedError): pass class NoCommentsError(ReportedError): def __init__(self): super().__init__("No comments were found.") class MissingCommentError(ReportedError): def __init__(self): super().__init__("Missing comment") class InvalidCommentIdError(ReportedError): def __init__(self, rid, comments): msg = "Id {0} is not in valid ids: {1}".format( rid, ", ".join([c.id for c in comments]) ) super().__init__(msg) 0707010000003B000081A4000000000000000000000001644668C300000ECF000000000000000000000000000000000000003300000000osc-plugin-qam-1.0.3+git0.420bf95/oscqam/fields.pyfrom enum import Enum from .errors import ReportedError def levenshtein(first, second): """Calculate levenshtein distance between two strings. Deletion, insertion and substitution all have a cost of 1. :type first: str :type second: str :returns: Distance between the strings. """ first = " " + first second = " " + second rows, cols = (len(first), len(second)) matrix = [[0] * cols for _ in range(rows)] # Set the axis for row in range(rows): matrix[row][0] = row for col in range(cols): matrix[0][col] = col for row in range(rows): for col in range(cols): if first[row] == second[col]: matrix[row][col] = matrix[row - 1][col - 1] else: matrix[row][col] = min( matrix[row - 1][col] + 1, matrix[row][col - 1] + 1, matrix[row - 1][col - 1] + 1, ) return matrix[rows - 1][cols - 1] class InvalidFieldsError(ReportedError): """Raise when the user wants to output non-existent fields.""" _msg = "Unknown fields: {0}. " "Did you mean: {1}. " "(Available fields: {2})" def __init__(self, bad_fields): suggestions = self._get_suggestions(bad_fields) super().__init__( self._msg.format( ", ".join(map(repr, bad_fields)), ", ".join(suggestions), ", ".join(map(str, ReportField)), ) ) def _get_suggestions(self, bad_fields): suggestions = set() for bad_field in bad_fields: distances = [ (str(field), levenshtein(str(field), bad_field)) for field in ReportField ] nearest = min(distances, key=lambda d: d[1]) suggestions.add(nearest[0]) return suggestions class ReportField(Enum): """All possible fields that can be displayed for a review.""" review_request_id = (0, "ReviewRequestID") products = (1, "Products") srcrpms = (2, "SRCRPMs") bugs = (3, "Bugs") category = (4, "Category") rating = (5, "Rating") unassigned_roles = (6, "Unassigned Roles") assigned_roles = (7, "Assigned Roles") package_streams = (8, "Package-Streams") incident_priority = (9, "Incident Priority") comments = (10, "Comments") creator = (11, "Creator") issues = (12, "Issues") def __init__(self, enum_id, log_key): self.enum_id = enum_id self.log_key = log_key def __str__(self): return self.log_key @classmethod def from_str(cls, field): for f in cls: if f.value[1] == field: return f raise InvalidFieldsError([field]) class ReportFields: all_fields = [ ReportField.review_request_id, ReportField.products, ReportField.srcrpms, ReportField.bugs, ReportField.category, ReportField.rating, ReportField.unassigned_roles, ReportField.assigned_roles, ReportField.package_streams, ReportField.incident_priority, ReportField.creator, ReportField.issues, ] def fields(self, _): return self.all_fields @staticmethod def review_fields_by_opts(opts): if opts.verbose: return ReportFields() elif opts.fields: return UserFields(opts.fields) else: return DefaultFields() class DefaultFields(ReportFields): def fields(self, action): return action.default_fields class UserFields(ReportFields): def __init__(self, fields): self._fields = [ReportField.from_str(f) for f in fields] def fields(self, _): return self._fields 0707010000003C000081A4000000000000000000000001644668C3000012E2000000000000000000000000000000000000003700000000osc-plugin-qam-1.0.3+git0.420bf95/oscqam/formatters.py"""Formatters used to generate nice looking output. """ import fcntl import os import platform import struct import sys import termios import prettytable from .fields import ReportField def terminal_dimensions(fd=None): """Return dimensions of the terminal. :param fd: filedescriptor of the terminal. :type fd: int. :returns: (int, int) tuple with rows and columns. """ if not fd: if not sys.stdout.isatty(): return (0, 0) fd = sys.stdout.fileno() dim = fcntl.ioctl(fd, termios.TIOCGWINSZ, "0000") rows, columns = struct.unpack("hh", dim) if rows == 0 and columns == 0: try: rows, columns = (int(os.getenv(v)) for v in ["LINES", "COLUMNS"]) except Exception: pass return rows, columns def os_lineseps(value, target=None): """Adjust the lineseperators in value to match the ones used by the current system. :param value: The text to modify :type value: str :param target: The system identifier whose line-endings should be substituted. :type target: str """ def _windows_to_linux(value): return value.replace("\r\n", "\n") def _linux_to_windows(value): if "\r\n" in value: # Seems there are already the correct lines present return value return value.replace("\n", "\r\n") target = target if target else platform.system() if target == "Linux": value = _windows_to_linux(value) elif target == "Windows": value = _linux_to_windows(value) else: return value return value class Formatter: """Base class for specialised formatters.""" def __init__(self, listsep, formatters={}): """ :param listsep: Seperator for lists. :type listsep: str :param formatters: Alternative formatter to use for certain keys. :type formatters: dict(ReportField, formatter) """ self.listsep = listsep self._formatters = { ReportField.bugs: self.list_formatter, ReportField.comments: self.comment_formatter, ReportField.package_streams: self.list_formatter, ReportField.products: self.list_formatter, ReportField.srcrpms: self.list_formatter, ReportField.unassigned_roles: self.list_formatter, ReportField.assigned_roles: self.list_formatter, } for formatter in formatters: self._formatters[formatter] = formatters[formatter] self.default_format = str def output(self, keys, reports): """Format the reports for output based on the keys. :param keys: The fields to output for each report. :type keys: [:class:`oscqam.fields.ReportField`] :param reports: The reports to format for outputting. :type reports: [:class:`oscqam.actions.Report`] :returns: Value that can be passed to print. """ pass def formatter(self, key): return self._formatters.get(key, self.default_format) def comment_formatter(self, value): return self.listsep.join([os_lineseps(str(v)) for v in value]) def list_formatter(self, value): return self.listsep.join(value) class VerboseOutput(Formatter): """Formats reports in a blocks: <key>: <value>+ -------------- """ def __init__(self): super().__init__(",") self.record_sep = "-" * terminal_dimensions()[1] def output(self, keys, reports): output = [] str_template = "{{0:{length}s}}: {{1}}".format( length=max([len(str(k)) for k in keys]) ) for report in reports: values = [] for key in keys: formatter = self.formatter(key) value = formatter(report.value(key)) values.append(str_template.format(str(key), value)) output.append(os.linesep.join(values)) output.append(self.record_sep) return os.linesep.join(output) class TabularOutput(Formatter): """Formats reports in a table +--------+--------+ | <key1> | <key2> | +--------+--------+ | <v1> | <v2> | +--------+--------+ """ def __init__(self): super().__init__(os.linesep, {ReportField.comments: self.comment_formatter}) def output(self, keys, reports): table_formatter = prettytable.PrettyTable(keys) table_formatter.align = "l" table_formatter.border = True for report in reports: values = [] for key in keys: formatter = self.formatter(key) value = formatter(report.value(key)) values.append(value) table_formatter.add_row(values) return table_formatter 0707010000003D000041ED000000000000000000000002644668C300000000000000000000000000000000000000000000003000000000osc-plugin-qam-1.0.3+git0.420bf95/oscqam/models0707010000003E000081A4000000000000000000000001644668C3000004D8000000000000000000000000000000000000003C00000000osc-plugin-qam-1.0.3+git0.420bf95/oscqam/models/__init__.py""" This module contains all models that are required by the QAM plugin to keep everything in a consistent state. """ import osc.core from .attribute import Attribute from .bug import Bug from .comment import Comment from .group import Group from .request import Request from .requestfilters import RequestFilter from .review import UserReview from .review import GroupReview from .template import Template from .user import User from .assignment import Assignment __all__ = [ "Assignment", "Template", "UserReview", "GroupReview", "Request", "Bug", "RequestFilter", "User", "Group", "Comment", "Attribute", ] def monkeypatch(): """Monkey patch retaining of history into the review class.""" def monkey_patched_init(obj, review_node): # logging.debug("Monkeypatched init") original_init(obj, review_node) obj.statehistory = [] for hist_state in review_node.findall("history"): obj.statehistory.append(osc.core.RequestHistory(hist_state)) # logging.warn("Careful - your osc-version requires monkey patching.") original_init = osc.core.ReviewState.__init__ osc.core.ReviewState.__init__ = monkey_patched_init monkeypatch() 0707010000003F000081A4000000000000000000000001644668C300000FA2000000000000000000000000000000000000003E00000000osc-plugin-qam-1.0.3+git0.420bf95/oscqam/models/assignment.pyimport logging from dateutil import parser from .review import GroupReview, UserReview class Assignment: """Associates a user with a group in the relation '<user> performs review for <group>'. This is solely a QAM construct as the buildservice has no concept of these assignments. """ ASSIGNED_DESC = "Review got assigned" ACCEPTED_DESC = "Review got accepted" REOPENED_DESC = "Review got reopened" def __init__(self, user, group): self.user = user self.group = group def __hash__(self): return hash(self.user) + hash(self.group) def __eq__(self, other): return self.user == other.user and self.group == other.group def __repr__(self): return str(self) def __str__(self): return "{1} -> {0}".format(self.user, self.group) @classmethod def infer_group(cls, remote, request, group_review): def get_history(review_state): """Return the history events for the given state that are needed to find assignments in ascending order of occurrence (by date). """ events = review_state.statehistory # TODO: refactor out lambda and filter ... relevant_events = filter( lambda e: e.get_description() in [cls.ASSIGNED_DESC, cls.ACCEPTED_DESC, cls.REOPENED_DESC], events, ) return sorted(relevant_events, key=lambda e: parser.parse(e.when)) group = group_review.reviewer review_state = [r for r in request.reviews if r.by_group == group.name][0] events = get_history(review_state) assignments = set() for event in events: user = remote.users.by_name(event.who) if event.get_description() == cls.ACCEPTED_DESC: logging.debug("Assignment for: %s -> %s" % (group, user)) assignments.add(Assignment(user, group)) elif event.get_description() == cls.REOPENED_DESC: logging.debug("Unassignment for: %s -> %s" % (group, user)) assignments.remove(Assignment(user, group)) else: logging.debug("Unknown event: %s " % event.get_description()) return assignments @classmethod def infer(cls, remote, request): """Create assignments for the given request. First assignments will be found for all groups that are of interest. Once the group assignments (to users) are found, the already finished ones will be removed. :param request: Request to check for a possible assigned roles. :type request: :class:`oscqam.models.Request` :returns: [:class:`oscqam.models.Assignment`] """ assigned_groups = [ g for g in request.review_list() if isinstance(g, GroupReview) and g.state == "accepted" and g.reviewer.is_qam_group() ] unassigned_groups = [ g for g in request.review_list() if isinstance(g, GroupReview) and g.state == "new" and g.reviewer.is_qam_group() ] finished_user = [ u for u in request.review_list() if isinstance(u, UserReview) and u.state == "accepted" ] assignments = set() for group_review in set(assigned_groups) | set(unassigned_groups): assignments.update(cls.infer_group(remote, request, group_review)) for user_review in finished_user: removal = [a for a in assignments if a.user == user_review.reviewer] if removal: logging.debug("Removing assignments %s as they are finished" % removal) for r in removal: assignments.remove(r) if not assignments: logging.debug("No assignments could be found for %s" % request) return list(assignments) 07070100000040000081A4000000000000000000000001644668C300000603000000000000000000000000000000000000003D00000000osc-plugin-qam-1.0.3+git0.420bf95/oscqam/models/attribute.pyfrom xml.etree import ElementTree as ET from .xmlfactorymixin import XmlFactoryMixin class Attribute(XmlFactoryMixin): reject_reason = "MAINT:RejectReason" def __init__(self, remote, attributes, children): super().__init__(remote, attributes, children) # We expect the value to be a sequence type even if there is only # one reasons specified. if not isinstance(self.value, (list, tuple)): self.value = [self.value] @classmethod def parse(cls, remote, xml): return super(Attribute, cls).parse(remote, xml, "attribute") @classmethod def preset(cls, remote, preset, *value): """Create a new attribute from a default attribute. Default attributes are stored as class-variables on this class. """ namespace, name = preset.split(":") return Attribute( remote, {"namespace": namespace, "name": name}, {"value": value} ) def __eq__(self, other): if not isinstance(other, Attribute): return False return ( self.namespace == other.namespace and self.name == other.name and self.value == other.value ) def xml(self): """Turn this attribute into XML.""" root = ET.Element("attribute") root.set("name", self.name) root.set("namespace", self.namespace) for val in self.value: value = ET.SubElement(root, "value") value.text = val return ET.tostring(root) 07070100000041000081A4000000000000000000000001644668C3000000C3000000000000000000000000000000000000003700000000osc-plugin-qam-1.0.3+git0.420bf95/oscqam/models/bug.pyfrom .xmlfactorymixin import XmlFactoryMixin class Bug(XmlFactoryMixin): # TODO: where we get tracker and ID ? def __str__(self): return "{0}:{1}".format(self.tracker, self.id) 07070100000042000081A4000000000000000000000001644668C300000216000000000000000000000000000000000000003B00000000osc-plugin-qam-1.0.3+git0.420bf95/oscqam/models/comment.pyfrom .xmlfactorymixin import XmlFactoryMixin class NullComment: """Null-Object for comments.""" def __init__(self): self.id = None self.text = None def __str__(self): return "" class Comment(XmlFactoryMixin): none = NullComment() def delete(self): self.remote.comments.delete(self) @classmethod def parse(cls, remote, xml): return super(Comment, cls).parse(remote, xml, "comment") def __str__(self): return "{0}: {1}".format(self.id, self.text) 07070100000043000081A4000000000000000000000001644668C300000361000000000000000000000000000000000000003B00000000osc-plugin-qam-1.0.3+git0.420bf95/oscqam/models/filters.pyimport abc class GroupFilter(metaclass=abc.ABCMeta): """Methods that allow filtering on groups.""" @abc.abstractmethod def is_qam_group(self): pass @classmethod def for_remote(cls, remote): """Return the correct Filter for the given remote.""" if "opensuse" in remote.remote: return OBSGroupFilter() else: return IBSGroupFilter() class OBSGroupFilter(GroupFilter): """Methods that allow filtering on groups from OBS.""" def is_qam_group(self, group): return group.name.startswith("qa-opensuse.org") class IBSGroupFilter(GroupFilter): IGNORED_GROUPS = ["qam-auto", "qam-openqa"] """Methods that allow filtering on groups from IBS.""" def is_qam_group(self, group): return group.name.startswith("qam") and group.name not in self.IGNORED_GROUPS 07070100000044000081A4000000000000000000000001644668C3000005C0000000000000000000000000000000000000003900000000osc-plugin-qam-1.0.3+git0.420bf95/oscqam/models/group.pyfrom .filters import GroupFilter from .reviewer import Reviewer from .xmlfactorymixin import XmlFactoryMixin class Group(XmlFactoryMixin, Reviewer): """A group object from the build service.""" def __init__(self, remote, attributes, children): super().__init__(remote, attributes, children) self.remote = remote self.filter = GroupFilter.for_remote(remote) if "title" in children: # We set name to title to ensure equality. This allows us to # prevent having to query *all* groups we need via this method, # which could use very many requests. self.name = children["title"] @classmethod def parse(cls, remote, xml): return super(Group, cls).parse(remote, xml, "group") @classmethod def parse_entry(cls, remote, xml): return super(Group, cls).parse(remote, xml, "entry") def is_qam_group(self): # 'qam-auto' is already used to designate automated reviews: # It is excluded here, as it does not require manual review # by a QAM member. return self.filter.is_qam_group(self) def __hash__(self): # We don't want to hash to the same as only the string. return hash(self.name) + hash(type(self)) def __eq__(self, other): if not isinstance(other, Group): return False return self.name == other.name def __str__(self): return "{0}".format(self.name) 07070100000045000081A4000000000000000000000001644668C3000029D4000000000000000000000000000000000000003B00000000osc-plugin-qam-1.0.3+git0.420bf95/oscqam/models/request.pyimport logging import re from urllib.parse import urlencode from xml.etree import ElementTree as ET import osc.core import osc.oscerr from ..errors import MissingSourceProjectError from .assignment import Assignment from .attribute import Attribute from .comment import Comment from .review import GroupReview from .review import UserReview from .xmlfactorymixin import XmlFactoryMixin class Request(osc.core.Request, XmlFactoryMixin): """Wrapper around osc request object to add logic required by the qam-plugin. """ STATE_NEW = "new" STATE_REVIEW = "review" STATE_DECLINED = "declined" OPEN_STATES = [STATE_NEW, STATE_REVIEW] REVIEW_USER = "BY_USER" REVIEW_GROUP = "BY_GROUP" REVIEW_OTHER = "BY_OTHER" COMPLETE_REQUEST_ID_SRE = re.compile(r"(open)?SUSE:Maintenance:\d+:(?P<req>\d+)") def __init__(self, remote): self.remote = remote super().__init__() self._comments = None self._groups = None self._packages = None self._assigned_roles = None self._priority = None self._reviews = [] self._attributes = {} self._issues = [] self._incident = None def active(self): return self.state == "new" or self.state == "review" @property def incident_priority(self): if not self._priority: self._priority = self.remote.priorities.for_request(self) return self._priority @property def incident(self): if not self._incident: self._incident = self.src_project.split(":")[-1] return self._incident @property def assigned_roles(self): if not self._assigned_roles: self._assigned_roles = Assignment.infer(self.remote, self) return self._assigned_roles @property def comments(self): if not self._comments: self._comments = self.remote.comments.for_request(self) or [Comment.none] return self._comments @property def maker(self): for history in self.statehistory: if history.description == "Request created": self._creator = self.remote.users.by_name(history.who) break else: self._creator = "Unknown" return self._creator @property def issues(self): """Bugs that should be fixed as part of this request""" if not self._issues: self._issues = self.remote.bugs.for_request(self) return self._issues @property def groups(self): # Maybe use a invalidating cache as a trade-off between current # information and slow response. return [ review.reviewer for review in self.review_list() if isinstance(review, GroupReview) ] @property def packages(self): """Collects all packages of the actions that are part of the request.""" if not self._packages: packages = set() for action in self.actions: pkg = action.src_package if pkg != "patchinfo": packages.add(pkg) self._packages = packages return self._packages @property def src_project(self): """Will return the src_project or an empty string if no src_project can be found in the request. """ for action in self.actions: if hasattr(action, "src_project"): prj = action.src_project if prj: return prj else: logging.info("This project has no source project: %s", self.reqid) return "" return "" def attribute(self, attribute): """Load the specified attribute for this request. As requests right now can not contain attributes the attribute will be loaded from the corresponding source-project. """ if attribute not in self._attributes: attributes = self.remote.projects.get_attribute(self.src_project, attribute) if len(attributes) == 1: attributes = attributes[0] self._attributes[attribute] = attributes return self._attributes[attribute] def review_action(self, params, user=None, group=None, comment=None): if not user and not group: raise AttributeError("group or user required for this action.") if user: params["by_user"] = user.login if group: params["by_group"] = group.name url_params = urlencode(params) url = "/".join([self.remote.requests.endpoint, self.reqid]) url += "?" + url_params self.remote.post(url, comment) def review_assign(self, group, reviewer, comment=None): params = {"cmd": "assignreview", "reviewer": reviewer.login} self.review_action(params, group=group, comment=comment) def review_unassign(self, group, reviewer, comment=None): """Will undo the assignment by the group""" params = {"cmd": "assignreview", "revert": 1, "reviewer": reviewer.login} self.review_action(params, group=group, comment=comment) def review_accept(self, user=None, group=None, comment=None): comment = self._format_review_comment(comment) params = {"cmd": "changereviewstate", "newstate": "accepted"} self.review_action(params, user, group, comment) def review_add(self, user=None, group=None, comment=None): """Will add a new reviewrequest for the given user or group.""" comment = self._format_review_comment(comment) params = {"cmd": "addreview"} self.review_action(params, user, group, comment) def review_decline(self, user=None, group=None, comment=None, reasons=None): """Will decline the reviewrequest for the given user or group. :param user: The user declining the request. :param group: The group the request should be declined for. :param comment: A comment that will be added to describe why the request was declined. :param reason: A L{oscqam.reject_reasons.RejectReason} that explains why the request was declined. The reason will be added as an attribute to the Maintenance incident. """ if reasons: reason = self._build_reject_attribute(reasons) self.remote.projects.set_attribute(self.src_project, reason) comment = self._format_review_comment(comment) params = {"cmd": "changereviewstate", "newstate": "declined"} self.review_action(params, user, group, comment) def _build_reject_attribute(self, reasons): reject_reason = self.attribute(Attribute.reject_reason) reason_values = list( map(lambda reason: "{0}:{1}".format(self.reqid, reason.flag), reasons) ) if not reject_reason: reject_reason = Attribute.preset( self.remote, Attribute.reject_reason, *reason_values ) else: for r in reason_values: reject_reason.value.append(r) return reject_reason def review_reopen(self, user=None, group=None, comment=None): """Will reopen a reviewrequest for the given user or group.""" params = {"cmd": "changereviewstate", "newstate": "new"} self.review_action(params, user, group, comment) def _format_review_comment(self, comment): if not comment: return None return "[oscqam] {comment}".format(comment=comment) def review_list(self): """Returns all reviews as a list.""" if not self._reviews: for review in self.reviews: if review.by_group: self._reviews.append(GroupReview(self.remote, review)) elif review.by_user: self._reviews.append(UserReview(self.remote, review)) return self._reviews def review_list_open(self): """Return only open reviews.""" return [r for r in self.review_list() if r.state in Request.OPEN_STATES] def review_list_accepted(self): return [r for r in self.review_list() if r.state.lower() == "accepted"] def add_comment(self, comment): """Adds a comment to this request.""" endpoint = "/comments/request/{id}".format(id=self.reqid) self.remote.post(endpoint, comment) def get_template(self, template_factory): """Return the template associated with this request.""" if not self.src_project: raise MissingSourceProjectError(self) return template_factory(self) @classmethod def filter_by_project(cls, request_substring, requests): return [r for r in requests if request_substring in r.src_project] @classmethod def parse(cls, remote, xml): et = ET.fromstring(xml) requests = [] for request in et.iter(remote.requests.endpoint): try: req = Request(remote) req.read(request) requests.append(req) except osc.oscerr.APIError as e: logging.error(e.msg) pass except osc.oscerr.WrongArgs as e: # Temporary workaround, as OBS >= 2.7 can return requests with # acceptinfo-elements that old osc can not handle. if not (osc.core.get_osc_version() < "0.152"): raise if "acceptinfo" not in str(e): raise else: logging.error("Server version too high for osc-client: %s" % str(e)) pass return requests @classmethod def parse_request_id(cls, request_id): """Extract the request_id from a string if required. The method will extract the request-id of a complete request string (e.g. SUSE:Maintenance:123:45678 has a request id of 45678) if needed. """ reqid = cls.COMPLETE_REQUEST_ID_SRE.match(request_id) if reqid: return reqid.group("req") return request_id def __eq__(self, other): project = self.actions[0].src_project other_project = other.actions[0].src_project return self.reqid == other.reqid and project == other_project def __hash__(self): hash_parts = [self.reqid] if self.src_project: hash_parts.append(self.src_project) hashes = [hash(part) for part in hash_parts] return sum(hashes) def __str__(self): return "{0}".format(self.reqid) 07070100000046000081A4000000000000000000000001644668C300000332000000000000000000000000000000000000004200000000osc-plugin-qam-1.0.3+git0.420bf95/oscqam/models/requestfilters.pyimport abc class RequestFilter(metaclass=abc.ABCMeta): """Methods that allow filtering on requests.""" @abc.abstractmethod def maintenance_requests(self, requests): pass @classmethod def for_remote(cls, remote): """Return the correct Filter for the given remote.""" if "opensuse" in remote.remote: return OBSRequestFilter() else: return IBSRequestFilter() class OBSRequestFilter(RequestFilter): PREFIX = "openSUSE:Maintenance" def maintenance_requests(self, requests): return [r for r in requests if self.PREFIX in r.src_project] class IBSRequestFilter(RequestFilter): PREFIX = "SUSE:Maintenance" def maintenance_requests(self, requests): return [r for r in requests if self.PREFIX in r.src_project] 07070100000047000081A4000000000000000000000001644668C30000036D000000000000000000000000000000000000003A00000000osc-plugin-qam-1.0.3+git0.420bf95/oscqam/models/review.pyclass Review: """Base class for buildservice-review objects.""" OPEN_STATES = ("new", "review") CLOSED_STATES = ("accepted",) def __init__(self, remote, review, reviewer): self._review = review self.remote = remote self.reviewer = reviewer self.state = review.state.lower() self.open = self.state in self.OPEN_STATES self.closed = self.state in self.CLOSED_STATES def __str__(self): return "Review: {0} ({1})".format(self.reviewer, self.state) class GroupReview(Review): def __init__(self, remote, review): reviewer = remote.groups.for_name(review.by_group) super().__init__(remote, review, reviewer) class UserReview(Review): def __init__(self, remote, review): reviewer = remote.users.by_name(review.by_user) super().__init__(remote, review, reviewer) 07070100000048000081A4000000000000000000000001644668C300000138000000000000000000000000000000000000003C00000000osc-plugin-qam-1.0.3+git0.420bf95/oscqam/models/reviewer.pyimport abc class Reviewer(metaclass=abc.ABCMeta): """Superclass for possible reviewer-classes.""" @abc.abstractmethod def is_qam_group(self): """ :returns: True if the group denotes reviews it's associated with to be reviewed by a QAM member. """ pass 07070100000049000081A4000000000000000000000001644668C300000CF0000000000000000000000000000000000000003C00000000osc-plugin-qam-1.0.3+git0.420bf95/oscqam/models/template.pyfrom ..errors import TemplateNotFoundError from ..errors import TestResultMismatchError from ..parsers import TemplateParser from ..utils import https def get_testreport_web(log_path, metadata_path): """Load the template belonging to the request from https://qam.suse.de/testreports/. :param request: The request this template is associated with. :type request: :class:`oscqam.models.Request` :return: Content of the log-file as string. """ report = https(log_path) if not report: raise TemplateNotFoundError(log_path) metadata = https(metadata_path) if not metadata: metadata = None else: metadata = metadata.read() report = report.read() return (report, metadata) class Template: """Facade to web-based templates.""" STATUS_SUCCESS = 0 STATUS_FAILURE = 1 STATUS_UNKNOWN = 2 # Machine readable reports base_url = "https://qam.suse.de/testreports/" # Human readable reports fancy_base_url = "https://qam.suse.de/reports/" def __init__(self, request, tr_getter=get_testreport_web, parser=TemplateParser()): """Create a template from the given request. :param request: The request the template is associated with. :type request: :class:`oscqam.models.Request`. :param tr_getter: Function that can load the template's log file based on the request. Will default to loading testreports from http://qam.suse.de. :type tr_getter: Function: :class:`oscqam.models.Request` -> :class:`str` :param parser: Class that can parse the data returned by tr_getter. :type parser: :class:`oscqam.parsers.TemplateParser` """ self._request = request self.log_entries = parser(*tr_getter(self.url, self.metadata_url)) def failed(self): """Assert that this template is from a failed test. If the template says the test did not fail this will raise an error. """ if self.status != Template.STATUS_FAILURE: raise TestResultMismatchError("FAILED", self.url) def passed(self): """Assert that this template is from a successful test. :raises: :class:`oscqam.models.TestResultMismatchError` if template is not set to PASSED. """ if self.status != Template.STATUS_SUCCESS: raise TestResultMismatchError("PASSED", self.url) @property def status(self): summary = self.log_entries["SUMMARY"] if summary.upper() == "PASSED": return Template.STATUS_SUCCESS elif summary.upper() == "FAILED": return Template.STATUS_FAILURE return Template.STATUS_UNKNOWN @property def url(self): """Return URL to machine readable version of the report.""" return f"{self.base_url}{self._request.src_project}:{self._request.reqid}/log" @property def metadata_url(self): return f"{self.base_url}{self._request.src_project}:{self._request.reqid}/metadata.json" @property def fancy_url(self): """Return URL to human readable version of the report.""" return f"{self.fancy_base_url}{self._request.src_project}:{self._request.reqid}/log" 0707010000004A000081A4000000000000000000000001644668C300000A28000000000000000000000000000000000000003800000000osc-plugin-qam-1.0.3+git0.420bf95/oscqam/models/user.pyfrom ..errors import NoQamReviewsError, NonMatchingUserGroupsError from .review import GroupReview from .reviewer import Reviewer from .xmlfactorymixin import XmlFactoryMixin class User(XmlFactoryMixin, Reviewer): """Wraps a user of the obs in an object.""" def __init__(self, remote, attributes, children): super().__init__(remote, attributes, children) self.remote = remote self._groups = None @property def groups(self): """Read-only property for groups a user is part of.""" # Maybe use a invalidating cache as a trade-off between current # information and slow response. if not self._groups: self._groups = self.remote.groups.for_user(self) return self._groups @property def qam_groups(self): """Return only the groups that are part of the qam-workflow.""" return [group for group in self.groups if group.is_qam_group()] def reviewable_groups(self, request): """Return groups the user could review for the given request. :param request: Request to check for open groups. :type request: :class:`oscqam.models.Request` :returns: set(:class:`oscqam.models.Group`) """ user_groups = set(self.qam_groups) reviews = [ review for review in request.review_list() if ( isinstance(review, GroupReview) and review.open and review.reviewer.is_qam_group() ) ] if not reviews: raise NoQamReviewsError(reviews) review_groups = [review.reviewer for review in reviews] open_groups = set(review_groups) both = user_groups.intersection(open_groups) if not both: raise NonMatchingUserGroupsError(self, user_groups, open_groups) return both def in_review_groups(self, request): reviewing_groups = [] for role in request.assigned_roles: if role.user == self: reviewing_groups.append(role.group) return reviewing_groups def is_qam_group(self): return False def __hash__(self): return hash(self.login) def __eq__(self, other): if not isinstance(other, User): return False return isinstance(other, User) and self.login == other.login def __str__(self): return "{0} ({1})".format(self.realname, self.email) @classmethod def parse(cls, remote, xml): return super(User, cls).parse(remote, xml, remote.users.endpoint) 0707010000004B000081A4000000000000000000000001644668C300000A00000000000000000000000000000000000000004300000000osc-plugin-qam-1.0.3+git0.420bf95/oscqam/models/xmlfactorymixin.pyfrom xml.etree import ElementTree as ET class XmlFactoryMixin: """Can generate an object from xml by recursively parsing the structure. It will set properties to the text-property of a node if there are no children. Otherwise it will parse the children into another node and set the property to a list of these new parsed nodes. """ def __init__(self, remote, attributes, children): """Will set every element in kwargs to a property of the class.""" attributes.update(children) for kwarg in attributes: setattr(self, kwarg, attributes[kwarg]) @staticmethod def listify(dictionary, key): """Will wrap an existing dictionary key in a list.""" if not isinstance(dictionary[key], list): value = dictionary[key] del dictionary[key] dictionary[key] = [value] @classmethod def parse_et(cls, remote, et, tag, wrapper_cls=None): """Recursively parses an element-tree instance. Will iterate over the tag as root-level. """ if not wrapper_cls: wrapper_cls = cls objects = [] for request in et.iter(tag): attribs = {} for attribute in request.attrib: attribs[attribute] = request.attrib[attribute] kwargs = {} for child in request: key = child.tag subchildren = list(child) if subchildren or child.attrib: # Prevent that all children have the same class as the # parent. This might lead to providing methods that make # no sense. value = cls.parse_et(remote, child, key, XmlFactoryMixin) if len(value) == 1: value = value[0] else: if child.text: value = child.text.strip() else: value = None if key in kwargs: XmlFactoryMixin.listify(kwargs, key) kwargs[key].append(value) else: kwargs[key] = value if request.text: kwargs["text"] = request.text kwargs.update(attribs) objects.append(wrapper_cls(remote, attribs, kwargs)) return objects @classmethod def parse(cls, remote, xml, tag): root = ET.fromstring(xml) return cls.parse_et(remote, root, tag, cls) 0707010000004C000081A4000000000000000000000001644668C3000013B6000000000000000000000000000000000000003400000000osc-plugin-qam-1.0.3+git0.420bf95/oscqam/parsers.py"""Parsers to turn (external) data into a more usable formats. """ from collections import defaultdict from itertools import dropwhile, takewhile from json import loads from json.decoder import JSONDecodeError import logging import re from .domains import Rating def until(snippet, lines): """Return lines until the snippet is matched at the beginning of the line. :param snippet: snippet to match at the beginning of a line. :type snippet: str :param lines: lines to return until snippet matches. :type lines: [str] """ def condition(line): return not line.startswith(snippet) return list(takewhile(condition, lines)) def split_comma(line): """Parse a line from a template-log into a list. :type package_line: str :returns: [str] """ return [v.strip() for v in line.split(",")] # TODO: this looks wrong, was valid maybe in beginning of SLE12 def split_products(product_line): """Split products into a list and strip SLE-prefix from each product. :type product_line: str :returns: [str] """ products = ( p if p.endswith(")") else p + ")" for p in (l.strip() for l in product_line.split("),")) ) return [re.sub("^SLE-", "", product, 1) for product in products] def split_srcrpms(srcrpm_line): """Parse 'SRCRPMs' from a template-log into a list. :type srcrpm_line: str :returns: [str] """ return [xs.strip() for xs in srcrpm_line.split(",")] def process_packages(pkgs): ret = set() for key in pkgs.keys(): for pkg in pkgs[key]: ret.add(pkg) return list(ret) class TemplateParser: """Parses a template-logs header-fields.""" end_marker = "#############################" def __call__(self, log, metadata): """Return dictionary of headers from the log-file and values. :returns: {str: object} """ if isinstance(log, bytes): self.log = log.decode() else: self.log = log if isinstance(metadata, bytes): self.metadata = metadata.decode() else: self.metadata = metadata log_entries = self._parse_headers(self._read_headers()) data = None if metadata: try: data = loads(metadata) except JSONDecodeError: data = None pass if data: log_entries.update(self._read_metadata(data)) return log_entries @staticmethod def _read_metadata(data): log_entries = {} log_entries["SRCRPMs"] = data.get("SRCRPMs") log_entries["Products"] = split_products(",".join(data.get("products"))) log_entries["Rating"] = Rating(data.get("rating")) log_entries["Packages"] = process_packages(data.get("packages")) log_entries["Bugs"] = data.get("bugs") log_entries["ReviewRequestID"] = data.get("rrid") return log_entries def _read_comment(self): def condition(line): return not line.startswith(prefix) prefix = "comment:" comment = until( "Products:", dropwhile(condition, self.log.splitlines()), ) return "\n".join(comment) def _read_headers(self): """Reads the template headers into a dictionary. Accumulates comment entry into a list. """ entries = defaultdict(list) comment = self._read_comment() log = self.log.replace(comment, "") entries["comment"] = [comment[len("comment:") :].strip()] lines = [line.strip() for line in log.splitlines() if line.strip()] header_end = len(until(self.end_marker, lines)) lines = lines[:header_end] for line in lines: try: key, value = [l.strip() for l in line.split(":", 1)] entries[key].append(value) except ValueError: logging.debug("Could not parse line: %s", line) continue return entries def _parse_headers(self, entries): """Parses the header-lists into objects or strings. :param entries: Dictionary of headers. :type entries: {str: list} :returns: Dictionary of header-fields: {str: object} """ log_entries = {} for key in entries: value = "\n".join(entries[key]) if key == "Packages": log_entries[key] = split_comma(value) elif key == "Bugs": log_entries[key] = split_comma(value) elif key == "Products": log_entries[key] = split_products(value) elif key == "SRCRPMs": log_entries[key] = split_srcrpms(value) elif key == "Rating": log_entries[key] = Rating(value) elif key == "comment" and value == "NONE": log_entries[key] = None else: log_entries[key] = value return log_entries 0707010000004D000081A4000000000000000000000001644668C300000929000000000000000000000000000000000000003B00000000osc-plugin-qam-1.0.3+git0.420bf95/oscqam/reject_reasons.py"""Implement the possible reject reasons as an enum: """ from enum import Enum from .errors import ReportedError class InvalidRejectError(ReportedError): """Raise when the user wants to output non-existent fields.""" _msg = "Unknown fields: {0}. " "(Available fields: {1})" def __init__(self, bad_fields): super(InvalidRejectError, self).__init__( self._msg.format( ", ".join(map(repr, bad_fields)), ", ".join(r.flag for r in RejectReason), ) ) class RejectReason(Enum): administrative = ( 0, "admin", "Administrative " "(e.g. pack more fixes into the updates)", ) retracted = (1, "retracted", "Retracted " "(e.g. fix not needed)") build_problem = ( 2, "build_problem", "Build problem " "(e.g. wrong rpm $version-$release)", ) not_fixed = ( 3, "not_fixed", "Issues not fixed " "(e.g. incomplete back-port or upstream fix)", ) regression = ( 4, "regression", "Regression " "(e.g. run-time regression or installation issues)", ) false_reject = ( 5, "false_reject", "False reject " "(e.g. spoiled results due to test setup error)", ) tracking_issue = ( 6, "tracking_issue", "Incident tracking issue " "(e.g. bad bug list or issues with patchinfo metadata)", ) def __init__(self, enum_id, flag, text): """ :param enum_id: Id of the enum. :type enum_id: int :param flag: Command line flag to specify the reason. :type enum_id: str :param text: Explanation text for the value. :type text: str """ self.enum_id = enum_id self.flag = flag self.text = text def __str__(self): return self.text @classmethod def from_str(cls, field): for f in cls: if f.value[1] == field: return f raise InvalidRejectError([field]) @classmethod def from_id(cls, id): ids = [e.enum_id for e in RejectReason] for f in cls: if f.value[0] == id: return f raise ValueError( "Enum for id not found {0}. " "Valid ids: {1} ".format(id, ids) ) 0707010000004E000041ED000000000000000000000002644668C300000000000000000000000000000000000000000000003100000000osc-plugin-qam-1.0.3+git0.420bf95/oscqam/remotes0707010000004F000081A4000000000000000000000001644668C300000078000000000000000000000000000000000000003D00000000osc-plugin-qam-1.0.3+git0.420bf95/oscqam/remotes/__init__.pyfrom .remoteerror import RemoteError from .remotefacade import RemoteFacade __all__ = ["RemoteError", "RemoteFacade"] 07070100000050000081A4000000000000000000000001644668C3000001E6000000000000000000000000000000000000003E00000000osc-plugin-qam-1.0.3+git0.420bf95/oscqam/remotes/bugremote.pyfrom ..models import Bug class BugRemote: """Get bug information for a request. This loads the patchinfo-file and parses it.""" endpoint = "/source/{incident}/patchinfo/_patchinfo" def __init__(self, remote): self.remote = remote def for_request(self, request): incident = request.src_project endpoint = self.endpoint.format(incident=incident) xml = self.remote.get(endpoint) return Bug.parse(self.remote, xml, "issue") 07070100000051000081A4000000000000000000000001644668C300000200000000000000000000000000000000000000004200000000osc-plugin-qam-1.0.3+git0.420bf95/oscqam/remotes/commentremote.pyfrom ..models import Comment class CommentRemote: endpoint = "comments" delete_endpoint = "comment" def __init__(self, remote): self.remote = remote def for_request(self, request): endpoint = "{0}/request/{1}".format(self.endpoint, request.reqid) xml = self.remote.get(endpoint) return Comment.parse(self.remote, xml) def delete(self, comment_id): endpoint = "{0}/{1}".format(self.delete_endpoint, comment_id) self.remote.delete(endpoint) 07070100000052000081A4000000000000000000000001644668C300000437000000000000000000000000000000000000004000000000osc-plugin-qam-1.0.3+git0.420bf95/oscqam/remotes/groupremote.pyfrom functools import lru_cache from ..models import Group class GroupRemote: def __init__(self, remote): self.remote = remote self.endpoint = "group" @lru_cache(maxsize=None) def all(self): group_entries = Group.parse_entry(self.remote, self.remote.get(self.endpoint)) return group_entries def for_pattern(self, pattern): return [group for group in self.all() if pattern.match(group.name)] @lru_cache(maxsize=None) def for_name(self, group_name): url = "/".join([self.endpoint, group_name]) group = Group.parse(self.remote, self.remote.get(url)) if group: return group[0] else: raise AttributeError("No group found for name: {0}".format(group_name)) @lru_cache(maxsize=None) def for_user(self, user): params = {"login": user.login} group_entries = Group.parse_entry( self.remote, self.remote.get(self.endpoint, params) ) groups = [self.for_name(g.name) for g in group_entries] return groups 07070100000053000081A4000000000000000000000001644668C300000664000000000000000000000000000000000000004300000000osc-plugin-qam-1.0.3+git0.420bf95/oscqam/remotes/priorityremote.pyfrom urllib.error import HTTPError from xml.etree import ElementTree as ET import requests import urllib3 import urllib3.exceptions from ..domains import Priority, UnknownPriority urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) class PriorityRemote: """Get priority information for a request (if available).""" endpoint = "/source/{0}/_attribute/OBS:IncidentPriority" smelt = "http://smelt.suse.de/graphql" query = "{{ incidents(incidentId: {incident}) {{ edges {{ node {{ priority priorityOverride }} }} }} }}" def __init__(self, remote): self.remote = remote def _priority(self, request): endpoint = self.endpoint.format(request.src_project) try: xml = ET.fromstring(self.remote.get(endpoint)) except HTTPError: return self._smelt_prio(request) else: value = xml.find(".//value") try: return Priority(value.text) except (AttributeError, TypeError): return self._smelt_prio(request) def _smelt_prio(self, request): try: prio = requests.get( self.smelt, params={"query": self.query.format(incident=request.incident)}, verify=False, ).json() prio = prio["data"]["incidents"].get("edges", None) if not prio: return UnknownPriority() return Priority(prio[0]["node"]["priority"]) except Exception: return UnknownPriority() def for_request(self, request): return self._priority(request) 07070100000054000081A4000000000000000000000001644668C3000002C4000000000000000000000000000000000000004200000000osc-plugin-qam-1.0.3+git0.420bf95/oscqam/remotes/projectremote.pyfrom ..models import Attribute class ProjectRemote: create_body = """<attributes> {attribute} </attributes> """ endpoint = "source" def __init__(self, remote): self.remote = remote def get_attribute(self, project, attribute_name): """Return the attribute value for the given project.""" url = f"{self.endpoint}/{project}/_attribute/{attribute_name}" return Attribute.parse(self.remote, self.remote.get(url)) def set_attribute(self, project, attribute): endpoint = f"{self.endpoint}/{project}/_attribute/{attribute.namespace}:{attribute.name}" self.remote.post(endpoint, self.create_body.format(attribute=attribute.xml())) 07070100000055000081A4000000000000000000000001644668C3000001FB000000000000000000000000000000000000004000000000osc-plugin-qam-1.0.3+git0.420bf95/oscqam/remotes/remoteerror.pyfrom ..errors import ReportedError class RemoteError(ReportedError): """Indicates an error while communicating with the remote service.""" _msg = "Error accessing {url} - {ret_code}: {msg}" def __init__(self, url, ret_code, msg, headers, fp): self.url = url self.ret_code = ret_code self.msg = msg self.headers = headers self.fp = fp super().__init__( self._msg.format(url=self.url, ret_code=self.ret_code, msg=self.msg) ) 07070100000056000081A4000000000000000000000001644668C300000994000000000000000000000000000000000000004100000000osc-plugin-qam-1.0.3+git0.420bf95/oscqam/remotes/remotefacade.pyimport logging from urllib.error import HTTPError from urllib.parse import urlencode import osc.core from .bugremote import BugRemote from .commentremote import CommentRemote from .groupremote import GroupRemote from .priorityremote import PriorityRemote from .projectremote import ProjectRemote from .remoteerror import RemoteError from .requestremote import RequestRemote from .userremote import UserRemote class RemoteFacade: def __init__(self, remote): """Initialize a new RemoteOscRemote that points to the given remote.""" self.remote = remote self.comments = CommentRemote(self) self.groups = GroupRemote(self) self.requests = RequestRemote(self) self.users = UserRemote(self) self.projects = ProjectRemote(self) self.priorities = PriorityRemote(self) self.bugs = BugRemote(self) def _check_for_error(self, answer): ret_code = answer.status if ret_code >= 400 and ret_code < 600: raise RemoteError( answer.url, ret_code, answer.msg, answer.headers, answer.fp ) def delete(self, endpoint, params=None): url = "/".join([self.remote, endpoint]) if params: params = urlencode(params) url = url + "?" + params remote = osc.core.http_DELETE(url) self._check_for_error(remote) xml = remote.read() return xml def get(self, endpoint, params=None): """Retrieve information at the given endpoint with the parameters. Call the callback function with the result. """ url = "/".join([self.remote, endpoint]) if params: params = urlencode(params) url = url + "?" + params try: logging.debug("Retrieving: %s" % url) remote = osc.core.http_GET(url) except HTTPError as e: raise RemoteError(e.url, e.status, e.msg, e.headers, e.fp) self._check_for_error(remote) xml = remote.read() return xml def post(self, endpoint, data=None): url = "/".join([self.remote, endpoint]) try: logging.debug("Posting: %s" % url) remote = osc.core.http_POST(url, data=data) self._check_for_error(remote) xml = remote.read() return xml except HTTPError as e: raise RemoteError(e.url, e.status, e.msg, e.headers, e.fp) 07070100000057000081A4000000000000000000000001644668C300000E36000000000000000000000000000000000000004200000000osc-plugin-qam-1.0.3+git0.420bf95/oscqam/remotes/requestremote.pyfrom ..models import Request, RequestFilter class RequestRemote: """Facade for retrieving Request objects from the buildservice API.""" def __init__(self, remote): self.remote = remote self.endpoint = "request" def _group_xpath(self, groups, state): """Search the given groups with the given state.""" def get_group_name(group): if isinstance(group, str): return group return group.name xpaths = [] for group in groups: name = get_group_name(group) xpaths.append( "(review[@by_group='{0}' and @state='{1}'])".format(name, state) ) xpath = " or ".join(xpaths) return "( {0} )".format(xpath) def _get_groups(self, groups, state, **kwargs): if not kwargs: kwargs = {"withfullhistory": "1"} xpaths = ["(state/@name='{0}')".format("review")] xpaths.append(self._group_xpath(groups, state)) xpath = " and ".join(xpaths) params = {"match": xpath, "withfullhistory": "1"} params.update(kwargs) search = "/".join(["search", self.endpoint]) requests = Request.parse(self.remote, self.remote.get(search, params)) return RequestFilter.for_remote(self.remote).maintenance_requests(requests) def open_for_groups(self, groups, **kwargs): """Will return all requests of the given type for the given groups that are still open: the state of the review should be in state 'new'. Args: - remote: The remote facade to use. - groups: The groups that should be used. - **kwargs: additional parameters for the search. """ return self._get_groups(groups, "new", **kwargs) def review_for_groups(self, groups, **kwargs): """Will return all requests for the given groups that are in review. As there is no 'review' state, the state is determined as a group being 'accepted', while a user is in state 'new' for that group. Args: - remote: The remote facade to use. - groups: The groups that should be used. - **kwargs: additional parameters for the search. """ requests = self._get_groups(groups, "accepted", **kwargs) return [request for request in requests if request.assigned_roles] def for_user(self, user): """Will return all requests for the user if they are part of a SUSE:Maintenance project. """ params = { "user": user.login, "view": "collection", "states": "new,review", "withfullhistory": "1", } requests = Request.parse(self.remote, self.remote.get(self.endpoint, params)) return RequestFilter.for_remote(self.remote).maintenance_requests(requests) def for_incident(self, incident): """Return all requests for the given incident that have a qam-group as reviewer. """ params = {"project": incident, "view": "collection", "withfullhistory": "1"} requests = Request.parse(self.remote, self.remote.get(self.endpoint, params)) return [ request for request in requests if any([r.reviewer.is_qam_group() for r in request.review_list()]) ] def by_id(self, req_id): req_id = Request.parse_request_id(req_id) endpoint = "/".join([self.endpoint, req_id]) req = Request.parse( self.remote, self.remote.get(endpoint, {"withfullhistory": 1}) ) return req[0] 07070100000058000081A4000000000000000000000001644668C3000001B2000000000000000000000000000000000000003F00000000osc-plugin-qam-1.0.3+git0.420bf95/oscqam/remotes/userremote.pyfrom functools import lru_cache from ..models import User class UserRemote: def __init__(self, remote): self.remote = remote self.endpoint = "person" @lru_cache(maxsize=None) def by_name(self, name): url = "/".join([self.endpoint, name]) users = User.parse(self.remote, self.remote.get(url)) if users: return users[0] raise AttributeError("User not found.") 07070100000059000081A4000000000000000000000001644668C3000003AD000000000000000000000000000000000000003200000000osc-plugin-qam-1.0.3+git0.420bf95/oscqam/utils.pyfrom itertools import groupby import ssl from urllib.error import HTTPError from urllib.request import urlopen def https(url): try: ctx = ssl.create_default_context() return urlopen(url, context=ctx) except HTTPError: return None def multi_level_sort(xs, criteria): """Sort the given collection based on multiple criteria. The criteria will be sorted by in the given order, whereas each group from the first criteria will be sorted by the second criteria and so forth. :param xs: Iterable of objects. :type xs: [a] :param criteria: Iterable of extractor functions. :type criteria: [a -> b] """ if not criteria: return xs extractor = criteria[-1] xss = sorted(xs, key=extractor) grouped = groupby(xss, extractor) subsorts = (multi_level_sort(list(value), criteria[:-1]) for _, value in grouped) return [s for sub in subsorts for s in sub] 0707010000005A000081A4000000000000000000000001644668C300000054000000000000000000000000000000000000003700000000osc-plugin-qam-1.0.3+git0.420bf95/requirements-dev.txtosc prettytable python-dateutil pytest pytest-cov coverage black requests responses 0707010000005B000081A4000000000000000000000001644668C300000029000000000000000000000000000000000000003300000000osc-plugin-qam-1.0.3+git0.420bf95/requirements.txtprettytable osc python-dateutil requests 0707010000005C000081A4000000000000000000000001644668C300000273000000000000000000000000000000000000002B00000000osc-plugin-qam-1.0.3+git0.420bf95/setup.pyfrom setuptools import find_packages, setup from oscqam import __version__ package = "osc-plugin-qam" version = __version__ setup( name=package, version=version, license="GPL-2.0", license_files=("LICENSE",), description="Plugin for OSC to support the workflow for the QA " + "maintenance department when using the new request / review osc " + "abstractions.", long_description=open("README.rst").read(), url="https://gitlab.suse.de/qa-maintenance/qam-oscplugin", install_requires=["osc", "python-dateutil", "prettytable", "requests"], packages=find_packages(exclude=["tests"]), ) 0707010000005D000041ED000000000000000000000003644668C300000000000000000000000000000000000000000000002800000000osc-plugin-qam-1.0.3+git0.420bf95/tests0707010000005E000081A4000000000000000000000001644668C300000000000000000000000000000000000000000000003400000000osc-plugin-qam-1.0.3+git0.420bf95/tests/__init__.py0707010000005F000081A4000000000000000000000001644668C300000069000000000000000000000000000000000000003400000000osc-plugin-qam-1.0.3+git0.420bf95/tests/conftest.pyimport pytest from .mockremote import MockRemote @pytest.fixture def remote(): return MockRemote() 07070100000060000041ED000000000000000000000002644668C300000000000000000000000000000000000000000000003100000000osc-plugin-qam-1.0.3+git0.420bf95/tests/fixtures07070100000061000081A4000000000000000000000001644668C30000005C000000000000000000000000000000000000004000000000osc-plugin-qam-1.0.3+git0.420bf95/tests/fixtures/attributes.xml<?xml version='1.0' encoding='utf-8'?> <person firstname="John" lastname="Smith"> </person> 07070100000062000081A4000000000000000000000001644668C3000000B0000000000000000000000000000000000000004600000000osc-plugin-qam-1.0.3+git0.420bf95/tests/fixtures/attributes_multi.xml<?xml version='1.0' encoding='utf-8'?> <persons> <person firstname="John" lastname="Smith"> </person> <person firstname="Clara" lastname="Oswald"> </person> </persons> 07070100000063000081A4000000000000000000000001644668C30000047E000000000000000000000000000000000000004300000000osc-plugin-qam-1.0.3+git0.420bf95/tests/fixtures/bug_patchinfo.xml<patchinfo incident="3220"> <issue id="993453" tracker="bnc">VUL-0: CVE-2016-5424 : postgresql: privilege escalation via crafted database and role names</issue> <issue id="993454" tracker="bnc">VUL-0: CVE-2016-5423: postgresql: CASE/WHEN with inlining can cause untrusted pointer dereference</issue> <issue id="2016-5423" tracker="cve" /> <issue id="2016-5424" tracker="cve" /> <category>security</category> <rating>important</rating> <packager>faweiss</packager> <name>postgresql94</name> <description>This update for postgresql94 to version 9.4.9 fixes the several issues. These security issues were fixed: - CVE-2016-5423: CASE/WHEN with inlining can cause untrusted pointer dereference (bsc#993454). - CVE-2016-5424: Fix client programs' handling of special characters in database and role names (bsc#993453). For the non-security issues please refer to - http://www.postgresql.org/docs/9.4/static/release-9-4-9.html - http://www.postgresql.org/docs/9.4/static/release-9-4-8.html - http://www.postgresql.org/docs/9.4/static/release-9-4-7.html </description> <summary>Security update for postgresql94</summary> </patchinfo> 07070100000064000081A4000000000000000000000001644668C300000092000000000000000000000000000000000000004000000000osc-plugin-qam-1.0.3+git0.420bf95/tests/fixtures/comments_1.xml<comments request="58800"> <comment who="anonymous" when="2015-07-01 07:47:43 UTC" id="1322">test comment - please remove</comment> </comments> 07070100000065000081A4000000000000000000000001644668C300000075000000000000000000000000000000000000003A00000000osc-plugin-qam-1.0.3+git0.420bf95/tests/fixtures/flat.xml<?xml version='1.0' encoding='utf-8'?> <person> <firstname>John</firstname> <lastname>Smith</lastname> </person> 07070100000066000081A4000000000000000000000001644668C30000003D000000000000000000000000000000000000003F00000000osc-plugin-qam-1.0.3+git0.420bf95/tests/fixtures/group_all.xml<directory count="1"> <entry name="qam-sle"/> </directory> 07070100000067000081A4000000000000000000000001644668C300000058000000000000000000000000000000000000004500000000osc-plugin-qam-1.0.3+git0.420bf95/tests/fixtures/group_anonymous.xml<directory count="2"> <entry name="qam-test"/> <entry name="qam-sle"/> </directory> 07070100000068000081A4000000000000000000000001644668C300000059000000000000000000000000000000000000004600000000osc-plugin-qam-1.0.3+git0.420bf95/tests/fixtures/group_anonymous2.xml<directory count="2"> <entry name="qam-cloud"/> <entry name="qam-sle"/> </directory> 07070100000069000081A4000000000000000000000001644668C3000000C4000000000000000000000000000000000000004400000000osc-plugin-qam-1.0.3+git0.420bf95/tests/fixtures/group_qam-auto.xml<group> <title>qam-auto</title> <email>testgroup@test.test</email> <maintainer userid="maint"/> <person> <person userid="maint"/> <person userid="anonymous"/> </person> </group> 0707010000006A000081A4000000000000000000000001644668C3000000C5000000000000000000000000000000000000004500000000osc-plugin-qam-1.0.3+git0.420bf95/tests/fixtures/group_qam-cloud.xml<group> <title>qam-cloud</title> <email>testgroup@test.test</email> <maintainer userid="maint"/> <person> <person userid="maint"/> <person userid="anonymous"/> </person> </group> 0707010000006B000081A4000000000000000000000001644668C3000000C6000000000000000000000000000000000000004600000000osc-plugin-qam-1.0.3+git0.420bf95/tests/fixtures/group_qam-openqa.xml<group> <title>qam-openqa</title> <email>testgroup@test.test</email> <maintainer userid="maint"/> <person> <person userid="maint"/> <person userid="anonymous"/> </person> </group> 0707010000006C000081A4000000000000000000000001644668C3000000C3000000000000000000000000000000000000004300000000osc-plugin-qam-1.0.3+git0.420bf95/tests/fixtures/group_qam-sle.xml<group> <title>qam-sle</title> <email>testgroup@test.test</email> <maintainer userid="maint"/> <person> <person userid="maint"/> <person userid="anonymous"/> </person> </group> 0707010000006D000081A4000000000000000000000001644668C3000000C4000000000000000000000000000000000000004400000000osc-plugin-qam-1.0.3+git0.420bf95/tests/fixtures/group_qam-test.xml<group> <title>qam-test</title> <email>testgroup@test.test</email> <maintainer userid="maint"/> <person> <person userid="maint"/> <person userid="anonymous"/> </person> </group> 0707010000006E000081A4000000000000000000000001644668C3000000C7000000000000000000000000000000000000004600000000osc-plugin-qam-1.0.3+git0.420bf95/tests/fixtures/group_some-group.xml<group> <title>some-group</title> <email>some-group@test.test</email> <maintainer userid="maint"/> <person> <person userid="maint"/> <person userid="anonymous"/> </person> </group> 0707010000006F000081A4000000000000000000000001644668C300000077000000000000000000000000000000000000004700000000osc-plugin-qam-1.0.3+git0.420bf95/tests/fixtures/incident_priority.xml<attributes> <attribute name='IncidentPriority' namespace='OBS'> <value>100</value> </attribute> </attributes> 07070100000070000081A4000000000000000000000001644668C3000000F4000000000000000000000000000000000000003C00000000osc-plugin-qam-1.0.3+git0.420bf95/tests/fixtures/nested.xml<?xml version='1.0' encoding='utf-8'?> <person> <firstname>John</firstname> <lastname>Smith</lastname> <address> <streetname> Arcadiaavenue </streetname> <streetnumber> 1 </streetnumber> </address> </person> 07070100000071000081A4000000000000000000000001644668C300000107000000000000000000000000000000000000004700000000osc-plugin-qam-1.0.3+git0.420bf95/tests/fixtures/nested_attributes.xml<?xml version='1.0' encoding='utf-8'?> <person id="1"> <firstname>John</firstname> <lastname>Smith</lastname> <address main="True"> <streetname> Arcadiaavenue </streetname> <streetnumber> 1 </streetnumber> </address> </person> 07070100000072000081A4000000000000000000000001644668C300000172000000000000000000000000000000000000004200000000osc-plugin-qam-1.0.3+git0.420bf95/tests/fixtures/nested_multi.xml<?xml version='1.0' encoding='utf-8'?> <person> <firstname>John</firstname> <lastname>Smith</lastname> <address> <streetname> Arcadiaavenue </streetname> <streetnumber> 1 </streetnumber> </address> <address> <streetname> Rassilonblvd </streetname> <streetnumber> 2 </streetnumber> </address> </person> 07070100000073000081A4000000000000000000000001644668C3000000A4000000000000000000000000000000000000004600000000osc-plugin-qam-1.0.3+git0.420bf95/tests/fixtures/person_anonymous.xml<person> <login>anonymous</login> <email>anonymous@nowhere.none</email> <realname>Unknown User</realname> <state>confirmed</state> <watchlist/> </person> 07070100000074000081A4000000000000000000000001644668C3000000A1000000000000000000000000000000000000004700000000osc-plugin-qam-1.0.3+git0.420bf95/tests/fixtures/person_anonymous2.xml<person> <login>anonymous2</login> <email>anon2@nowhere.none</email> <realname>Unknown User</realname> <state>confirmed</state> <watchlist/> </person> 07070100000075000081A4000000000000000000000001644668C3000000B1000000000000000000000000000000000000004E00000000osc-plugin-qam-1.0.3+git0.420bf95/tests/fixtures/person_maintenance-robot.xml<person> <login>maintenance-robot</login> <email>maintenance-robot@nowhere.none</email> <realname>El roboto</realname> <state>confirmed</state> <watchlist/> </person> 07070100000076000081A4000000000000000000000001644668C3000000A8000000000000000000000000000000000000004B00000000osc-plugin-qam-1.0.3+git0.420bf95/tests/fixtures/person_sle-qam-openqa.xml<person> <login>sle-qam-openqa</login> <email>sle-qam-openqa@nowhere.none</email> <realname>openQA</realname> <state>confirmed</state> <watchlist/> </person> 07070100000077000081A4000000000000000000000001644668C300000098000000000000000000000000000000000000004D00000000osc-plugin-qam-1.0.3+git0.420bf95/tests/fixtures/reject_reason_attribute.xml<attributes> <attribute name="RejectReason" namespace="MAINT"> <value>12345:abc</value> <value>23456:def</value> </attribute> </attributes> 07070100000078000081A4000000000000000000000001644668C30000000F000000000000000000000000000000000000005300000000osc-plugin-qam-1.0.3+git0.420bf95/tests/fixtures/reject_reason_attribute_empty.xml<attributes /> 07070100000079000081A4000000000000000000000001644668C300000086000000000000000000000000000000000000004C00000000osc-plugin-qam-1.0.3+git0.420bf95/tests/fixtures/reject_reason_tracking.xml<attributes> <attribute name="RejectReason" namespace="MAINT"> <value>12345:tracking_issue</value> </attribute> </attributes> 0707010000007A000081A4000000000000000000000001644668C300000347000000000000000000000000000000000000004300000000osc-plugin-qam-1.0.3+git0.420bf95/tests/fixtures/request_12345.xml<request id="12345"> <action type="maintenance_release"> <source project="SUSE:Maintenance:130" package="update-test-trival.SUSE_SLE-12_Update"/> <target project="SUSE:SLE-12:Update" package="update-test-trival.130"/> </action> <review state="review" when="2014-11-14T11:12:53" who="anonymous" by_user="anonymous"> </review> <review state="accepted" when="2014-11-14T11:12:53" who="anonymous" by_group="qam-sle"> <history who="anonymous" when="2017-09-06T08:06:39"> <description>Review got accepted</description> <comment>review for group qam-sle</comment> </history> </review> <review state="new" when="2014-11-14T11:12:53" who="anonymous" by_group="qam-cloud"> </review> <state name="review" who="anonymous" when="2014-12-01T14:46:23"> <comment>In review</comment> </state> </request> 0707010000007B000081A4000000000000000000000001644668C3000002EA000000000000000000000000000000000000004300000000osc-plugin-qam-1.0.3+git0.420bf95/tests/fixtures/request_23456.xml<request id="23456"> <action type="maintenance_release"> <source project="SUSE:Maintenance:130" package="update-test-trival.SUSE_SLE-12_Update"/> <target project="SUSE:SLE-12:Update" package="update-test-trival.130"/> </action> <review state="accepted" when="2014-11-14T11:12:53" who="anonymous" by_group="qam-test"> <history who="anonymous" when="2017-09-06T08:06:39"> <description>Review got accepted</description> <comment>review for group qam-test</comment> </history> </review> <review state="review" by_user="anonymous"> <comment>Do not approve/release.</comment> </review> <state name="review" who="anonymous" when="2014-12-01T14:46:23"> <comment>In review</comment> </state> </request> 0707010000007C000081A4000000000000000000000001644668C300000172000000000000000000000000000000000000004300000000osc-plugin-qam-1.0.3+git0.420bf95/tests/fixtures/request_34567.xml<request id="34567"> <action type="maintenance_release"> <source project="SUSE:Maintenance:130" package="update-test-trival.SUSE_SLE-12_Update"/> <target project="SUSE:SLE-12:Update" package="update-test-trival.130"/> </action> <review state="new" when="2014-11-14T11:12:53" who="anonymous" by_group="qam-sle"> </review> <state name="new"/> </request> 0707010000007D000081A4000000000000000000000001644668C300000178000000000000000000000000000000000000004300000000osc-plugin-qam-1.0.3+git0.420bf95/tests/fixtures/request_45678.xml<request id="45678"> <action type="maintenance_release"> <source project="SUSE:Maintenance:130" package="update-test-trival.SUSE_SLE-12_Update"/> <target project="SUSE:SLE-12:Update" package="update-test-trival.130"/> </action> <review state="review" when="2014-11-14T11:12:53" who="anonymous" by_group="some-group"> </review> <state name="new"/> </request> 0707010000007E000081A4000000000000000000000001644668C300000916000000000000000000000000000000000000004300000000osc-plugin-qam-1.0.3+git0.420bf95/tests/fixtures/request_52542.xml<request id="52542"> <action type="maintenance_release"> <source project="SUSE:Maintenance:453" package="wpa_supplicant.SUSE_SLE-12_Update"/> <target project="SUSE:SLE-12:Update" package="wpa_supplicant.453"/> </action> <action type="maintenance_release"> <source project="SUSE:Maintenance:453" package="patchinfo"/> <target project="SUSE:SLE-12:Update" package="patchinfo.453"/> </action> <action type="maintenance_release"> <source project="SUSE:Maintenance:453" package="patchinfo"/> <target project="SUSE:Updates:SLE-DESKTOP:12:x86_64" package="patchinfo.453"/> </action> <action type="maintenance_release"> <source project="SUSE:Maintenance:453" package="patchinfo"/> <target project="SUSE:Updates:SLE-SERVER:12:ppc64le" package="patchinfo.453"/> </action> <action type="maintenance_release"> <source project="SUSE:Maintenance:453" package="patchinfo"/> <target project="SUSE:Updates:SLE-SERVER:12:s390x" package="patchinfo.453"/> </action> <action type="maintenance_release"> <source project="SUSE:Maintenance:453" package="patchinfo"/> <target project="SUSE:Updates:SLE-SERVER:12:x86_64" package="patchinfo.453"/> </action> <state name="review" who="msmeissn" when="2015-03-02T20:27:30"> <comment/> </state> <review state="accepted" by_group="qam-sle"> <history who="anonymous" when="2015-03-11T16:24:48"> <description>Review got accepted</description> <comment>review assigend to user anonymous</comment> </history> </review> <review state="new" by_user="anonymous"> <history who="anonymous" when="2015-03-11T16:24:48"> <description>Review got assigned</description> <comment>review for group qam-sle</comment> </history> </review> <history who="msmeissn" when="2015-03-02T20:27:32"> <description>Request created</description> <comment>requesting release</comment> </history> <history who="anonymous" when="2015-03-11T16:24:48"> <description>Review got accepted</description> <comment>review assigend to user anonymous</comment> </history> <history who="anonymous" when="2015-03-11T16:24:48"> <description>Review got assigned</description> <comment>review for group qam-sle</comment> </history> <description>requesting release</description> </request> 0707010000007F000081A4000000000000000000000001644668C300000344000000000000000000000000000000000000004300000000osc-plugin-qam-1.0.3+git0.420bf95/tests/fixtures/request_56789.xml<request id="56789"> <action type="maintenance_release"> <source project="SUSE:Maintenance:130" package="update-test-trival.SUSE_SLE-12_Update"/> <target project="SUSE:SLE-12:Update" package="update-test-trival.130"/> </action> <review state="new" when="2014-11-14T11:12:53" who="anonymous" by_user="anonymous"> </review> <review state="accepted" when="2014-11-14T11:12:53" who="anonymous" by_group="qam-sle"> <history who="anonymous" when="2017-09-06T08:06:39"> <description>Review got accepted</description> <comment>review for group qam-sle</comment> </history> </review> <review state="new" when="2014-11-14T11:12:53" who="anonymous" by_group="qam-cloud"> </review> <state name="review" who="anonymous" when="2014-12-01T14:46:23"> <comment>In review</comment> </state> </request> 07070100000080000081A4000000000000000000000001644668C3000003EF000000000000000000000000000000000000004600000000osc-plugin-qam-1.0.3+git0.420bf95/tests/fixtures/request_accepted.xml<request id="12345"> <action type="maintenance_release"> <source project="SUSE:Maintenance:130" package="update-test-trival.SUSE_SLE-12_Update"/> <target project="SUSE:SLE-12:Update" package="update-test-trival.130"/> </action> <review state="review" when="2014-11-14T11:12:53" who="anonymous" by_user="anonymous"> </review> <review state="accepted" when="2014-11-14T11:12:53" who="anonymous" by_group="qam-sle"> </review> <review state="new" when="2014-11-14T11:12:53" who="anonymous" by_group="qam-cloud"> </review> <state name="review" who="anonymous" when="2014-12-01T14:46:23"> <comment>In review</comment> </state> <history who="anonymous" when="2014-12-16T10:54:10"> <description>Review got accepted</description> <comment>review assigend to user anonymous</comment> </history> <history who="anonymous" when="2014-12-16T10:54:10"> <description>Review got assigned</description> <comment>review for group qam-sle</comment> </history> </request> 07070100000081000081A4000000000000000000000001644668C30000099C000000000000000000000000000000000000004F00000000osc-plugin-qam-1.0.3+git0.420bf95/tests/fixtures/request_approval_last_qam.xml<request id="126080" creator="jsegitz"> <action type="maintenance_release"> <source project="SUSE:Maintenance:3919" package="squid.SUSE_SLE-12_Update"/> <target project="SUSE:SLE-12:Update" package="squid.3919"/> <acceptinfo rev="1" srcmd5="6df399f2a5277d8a1dde60e5a5268f77" oproject="SUSE:SLE-12:Update" opackage="squid.3507" osrcmd5="8ff28d8a65a0d449aa3657093e805d79" oxsrcmd5="8ff28d8a65a0d449aa3657093e805d79"/> </action> <state name="review" who="anonymous2" when="2017-01-12T11:32:49"> <comment>release</comment> </state> <review state="accepted" when="2017-01-03T08:04:24" who="maintenance-robot" by_group="qam-auto"> <comment>reviewers added: qam-openqa qam-sle</comment> <history who="maintenance-robot" when="2017-01-03T08:05:15"> <description>Review got accepted</description> <comment>reviewers added: qam-openqa qam-sle</comment> </history> </review> <review state="accepted" by_group="qam-sle"> <comment/> <history who="anonymous" when="2017-01-10T12:08:48"> <description>Review got accepted</description> <comment>review assigend to user anonymous</comment> </history> </review> <review state="review" when="2017-01-10T12:08:48" who="anonymous" by_user="anonymous"> </review> <history who="anonymous2" when="2017-01-03T08:04:24"> <description>Request created</description> <comment>requesting release</comment> </history> <history who="maintenance-robot" when="2017-01-03T08:05:14"> <description>Request got a new review request</description> </history> <history who="maintenance-robot" when="2017-01-03T08:05:15"> <description>Request got a new review request</description> </history> <history who="maintenance-robot" when="2017-01-03T08:05:15"> <description>Review got accepted</description> <comment>reviewers added: qam-openqa qam-sle</comment> </history> <history who="anonymous" when="2017-01-05T15:49:03"> <description>Review got accepted</description> <comment>approving update</comment> </history> <history who="anonymous" when="2017-01-10T12:08:48"> <description>Review got accepted</description> <comment>review assigend to user anonymous</comment> </history> <history who="anonymous" when="2017-01-10T12:08:48"> <description>Review got assigned</description> <comment>review for group qam-sle</comment> </history> <description>requesting release</description> </request> 07070100000082000081A4000000000000000000000001644668C30000049F000000000000000000000000000000000000004400000000osc-plugin-qam-1.0.3+git0.420bf95/tests/fixtures/request_assign.xml<request id="assign"> <action type="maintenance_release"> <source project="SUSE:Maintenance:130" package="update-test-trival.SUSE_SLE-12_Update"/> <target project="SUSE:SLE-12:Update" package="update-test-trival.130"/> </action> <review state="review" when="2014-11-14T11:12:53" who="anonymous" by_user="anonymous"> </review> <review state="accepted" when="2014-11-14T11:12:53" who="anonymous" by_group="qam-sle"> <history who="anonymous" when="2017-09-06T08:06:39"> <description>Review got accepted</description> <comment>review for group qam-sle</comment> </history> </review> <review state="new" when="2014-11-14T11:12:53" who="anonymous" by_group="qam-cloud"> </review> <state name="review" who="anonymous" when="2014-12-01T14:46:23"> <comment>In review</comment> </state> <history who="anonymous" when="2014-12-16T10:54:10"> <description>Review got accepted</description> <comment>review assigend to user anonymous</comment> </history> <history who="anonymous" when="2014-12-16T10:54:10"> <description>Review got assigned</description> <comment>review for group qam-sle</comment> </history> </request> 07070100000083000081A4000000000000000000000001644668C300001258000000000000000000000000000000000000004F00000000osc-plugin-qam-1.0.3+git0.420bf95/tests/fixtures/request_bug_none_assigned.xml<request id="126794" creator="mgerstner"> <action type="maintenance_release"> <source project="SUSE:Maintenance:3551" package="openssh-askpass-gnome.SUSE_SLE-12-SP2_Update"/> <target project="SUSE:SLE-12-SP2:Update" package="openssh-askpass-gnome.3551"/> </action> <action type="maintenance_release"> <source project="SUSE:Maintenance:3551" package="openssh.SUSE_SLE-12-SP2_Update"/> <target project="SUSE:SLE-12-SP2:Update" package="openssh.3551"/> </action> <action type="maintenance_release"> <source project="SUSE:Maintenance:3551" package="patchinfo"/> <target project="SUSE:SLE-12-SP2:Update" package="patchinfo.3551"/> </action> <action type="maintenance_release"> <source project="SUSE:Maintenance:3551" package="patchinfo"/> <target project="SUSE:Updates:SLE-DESKTOP:12-SP2:x86_64" package="patchinfo.3551"/> </action> <action type="maintenance_release"> <source project="SUSE:Maintenance:3551" package="patchinfo"/> <target project="SUSE:Updates:SLE-RPI:12-SP2:aarch64" package="patchinfo.3551"/> </action> <action type="maintenance_release"> <source project="SUSE:Maintenance:3551" package="patchinfo"/> <target project="SUSE:Updates:SLE-SERVER:12-SP2:aarch64" package="patchinfo.3551"/> </action> <action type="maintenance_release"> <source project="SUSE:Maintenance:3551" package="patchinfo"/> <target project="SUSE:Updates:SLE-SERVER:12-SP2:ppc64le" package="patchinfo.3551"/> </action> <action type="maintenance_release"> <source project="SUSE:Maintenance:3551" package="patchinfo"/> <target project="SUSE:Updates:SLE-SERVER:12-SP2:s390x" package="patchinfo.3551"/> </action> <action type="maintenance_release"> <source project="SUSE:Maintenance:3551" package="patchinfo"/> <target project="SUSE:Updates:SLE-SERVER:12-SP2:x86_64" package="patchinfo.3551"/> </action> <state name="review" who="maintenance-robot" when="2017-01-19T07:49:55"> <comment/> </state> <review state="accepted" when="2017-01-17T12:41:23" who="maintenance-robot" by_group="qam-auto"> <comment>reviewers added: qam-openqa qam-sle</comment> <history who="maintenance-robot" when="2017-01-17T12:45:12"> <description>Review got accepted</description> <comment>reviewers added: qam-openqa qam-sle</comment> </history> </review> <review state="accepted" when="2017-01-17T12:45:10" who="sle-qam-openqa" by_group="qam-openqa"> <comment>ok</comment> <history who="sle-qam-openqa" when="2017-01-19T07:49:55"> <description>Review got accepted</description> <comment>ok</comment> </history> </review> <review state="accepted" by_group="qam-sle"> <comment/> <history who="anonymous" when="2017-01-17T14:37:19"> <description>Review got accepted</description> <comment>review assigend to user anonymous</comment> </history> </review> <review state="new" by_user="anonymous"> <history who="anonymous" when="2017-01-17T14:37:19"> <description>Review got assigned</description> <comment>review for group qam-sle</comment> </history> </review> <history who="mgerstner" when="2017-01-17T12:41:21"> <description>Request created</description> <comment>requesting release</comment> </history> <history who="maintenance-robot" when="2017-01-17T12:45:03"> <description>Review got accepted</description> <comment>Unchecked request type (maintenance_release)</comment> </history> <history who="licensedigger" when="2017-01-17T12:45:03"> <description>Review got accepted</description> <comment>Unchecked request type maintenance_release</comment> </history> <history who="maintenance-robot" when="2017-01-17T12:45:10"> <description>Request got a new review request</description> </history> <history who="maintenance-robot" when="2017-01-17T12:45:12"> <description>Request got a new review request</description> </history> <history who="maintenance-robot" when="2017-01-17T12:45:12"> <description>Review got accepted</description> <comment>reviewers added: qam-openqa qam-sle</comment> </history> <history who="anonymous" when="2017-01-17T14:37:19"> <description>Review got assigned</description> <comment>review for group qam-sle</comment> </history> <history who="anonymous" when="2017-01-17T14:37:19"> <description>Review got accepted</description> <comment>review assigend to user anonymous</comment> </history> <history who="sle-qam-openqa" when="2017-01-19T07:49:55"> <description>Review got accepted</description> <comment>ok</comment> </history> <description>requesting release</description> </request> 07070100000084000081A4000000000000000000000001644668C300001258000000000000000000000000000000000000004C00000000osc-plugin-qam-1.0.3+git0.420bf95/tests/fixtures/request_inverse_assign.xml<request id="126794" creator="mgerstner"> <action type="maintenance_release"> <source project="SUSE:Maintenance:3551" package="openssh-askpass-gnome.SUSE_SLE-12-SP2_Update"/> <target project="SUSE:SLE-12-SP2:Update" package="openssh-askpass-gnome.3551"/> </action> <action type="maintenance_release"> <source project="SUSE:Maintenance:3551" package="openssh.SUSE_SLE-12-SP2_Update"/> <target project="SUSE:SLE-12-SP2:Update" package="openssh.3551"/> </action> <action type="maintenance_release"> <source project="SUSE:Maintenance:3551" package="patchinfo"/> <target project="SUSE:SLE-12-SP2:Update" package="patchinfo.3551"/> </action> <action type="maintenance_release"> <source project="SUSE:Maintenance:3551" package="patchinfo"/> <target project="SUSE:Updates:SLE-DESKTOP:12-SP2:x86_64" package="patchinfo.3551"/> </action> <action type="maintenance_release"> <source project="SUSE:Maintenance:3551" package="patchinfo"/> <target project="SUSE:Updates:SLE-RPI:12-SP2:aarch64" package="patchinfo.3551"/> </action> <action type="maintenance_release"> <source project="SUSE:Maintenance:3551" package="patchinfo"/> <target project="SUSE:Updates:SLE-SERVER:12-SP2:aarch64" package="patchinfo.3551"/> </action> <action type="maintenance_release"> <source project="SUSE:Maintenance:3551" package="patchinfo"/> <target project="SUSE:Updates:SLE-SERVER:12-SP2:ppc64le" package="patchinfo.3551"/> </action> <action type="maintenance_release"> <source project="SUSE:Maintenance:3551" package="patchinfo"/> <target project="SUSE:Updates:SLE-SERVER:12-SP2:s390x" package="patchinfo.3551"/> </action> <action type="maintenance_release"> <source project="SUSE:Maintenance:3551" package="patchinfo"/> <target project="SUSE:Updates:SLE-SERVER:12-SP2:x86_64" package="patchinfo.3551"/> </action> <state name="review" who="maintenance-robot" when="2017-01-19T07:49:55"> <comment/> </state> <review state="accepted" when="2017-01-17T12:41:23" who="maintenance-robot" by_group="qam-auto"> <comment>reviewers added: qam-openqa qam-sle</comment> <history who="maintenance-robot" when="2017-01-17T12:45:12"> <description>Review got accepted</description> <comment>reviewers added: qam-openqa qam-sle</comment> </history> </review> <review state="accepted" when="2017-01-17T12:45:10" who="sle-qam-openqa" by_group="qam-openqa"> <comment>ok</comment> <history who="sle-qam-openqa" when="2017-01-19T07:49:55"> <description>Review got accepted</description> <comment>ok</comment> </history> </review> <review state="accepted" by_group="qam-sle"> <comment/> <history who="anonymous" when="2017-01-17T14:37:19"> <description>Review got accepted</description> <comment>review assigend to user anonymous</comment> </history> </review> <review state="new" by_user="anonymous"> <history who="anonymous" when="2017-01-17T14:37:19"> <description>Review got assigned</description> <comment>review for group qam-sle</comment> </history> </review> <history who="mgerstner" when="2017-01-17T12:41:21"> <description>Request created</description> <comment>requesting release</comment> </history> <history who="maintenance-robot" when="2017-01-17T12:45:03"> <description>Review got accepted</description> <comment>Unchecked request type (maintenance_release)</comment> </history> <history who="licensedigger" when="2017-01-17T12:45:03"> <description>Review got accepted</description> <comment>Unchecked request type maintenance_release</comment> </history> <history who="maintenance-robot" when="2017-01-17T12:45:10"> <description>Request got a new review request</description> </history> <history who="maintenance-robot" when="2017-01-17T12:45:12"> <description>Request got a new review request</description> </history> <history who="maintenance-robot" when="2017-01-17T12:45:12"> <description>Review got accepted</description> <comment>reviewers added: qam-openqa qam-sle</comment> </history> <history who="anonymous" when="2017-01-17T14:37:19"> <description>Review got assigned</description> <comment>review for group qam-sle</comment> </history> <history who="anonymous" when="2017-01-17T14:37:19"> <description>Review got accepted</description> <comment>review assigend to user anonymous</comment> </history> <history who="sle-qam-openqa" when="2017-01-19T07:49:55"> <description>Review got accepted</description> <comment>ok</comment> </history> <description>requesting release</description> </request> 07070100000085000081A4000000000000000000000001644668C300000491000000000000000000000000000000000000004900000000osc-plugin-qam-1.0.3+git0.420bf95/tests/fixtures/request_multireview.xml<request id="56789"> <action type="maintenance_release"> <source project="SUSE:Maintenance:130" package="update-test-trival.SUSE_SLE-12_Update"/> <target project="SUSE:SLE-12:Update" package="update-test-trival.130"/> </action> <review state="new" when="2014-11-14T11:12:53" who="anonymous" by_user="anonymous"> </review> <review state="accepted" when="2014-11-14T11:12:53" who="anonymous" by_group="qam-sle"> <history who="anonymous" when="2017-09-06T08:06:39"> <description>Review got accepted</description> <comment>review for group qam-sle</comment> </history> <history who="anonymous2" when="2017-09-06T08:06:39"> <description>Review got accepted</description> <comment>review for group qam-sle</comment> </history> </review> <review state="new" when="2014-11-14T11:12:53" who="anonymous" by_group="qam-cloud"> </review> <review state="new" when="2014-11-14T11:12:53" who="anonymous"> </review> <review state="new" when="2014-11-14T11:12:53" who="anonymous2"> </review> <state name="review" who="anonymous" when="2014-12-01T14:46:23"> <comment>In review</comment> </state> </request> 07070100000086000081A4000000000000000000000001644668C30000023C000000000000000000000000000000000000004400000000osc-plugin-qam-1.0.3+git0.420bf95/tests/fixtures/request_no_qam.xml<request id="12345"> <action type="maintenance_release"> <source project="SUSE:Maintenance:130" package="update-test-trival.SUSE_SLE-12_Update"/> <target project="SUSE:SLE-12:Update" package="update-test-trival.130"/> </action> <review state="accepted" when="2014-11-14T11:12:53" who="anonymous" by_group="test-group"> </review> <review state="review" by_user="anonymous"> <comment>Do not approve/release.</comment> </review> <state name="review" who="anonymous" when="2014-12-01T14:46:23"> <comment>In review</comment> </state> </request> 07070100000087000081A4000000000000000000000001644668C30000027F000000000000000000000000000000000000004400000000osc-plugin-qam-1.0.3+git0.420bf95/tests/fixtures/request_no_src.xml<collection matches="40"> <request id="47159"> <action type="maintenance_release"> <source package="mutt.SUSE_SLE-12_Update"/> <target project="SUSE:SLE-12:Update" package="mutt.209"/> </action> <priority>important</priority> <state name="review" who="anon1" when="2014-12-05T14:53:50"> <comment></comment> </state> <review state="new" by_group="qam-sle"/> <history who="anon1" when="2014-12-05T14:53:52"> <description>Request created</description> <comment>requesting release</comment> </history> <description>requesting release</description> </request> </collection> 07070100000088000081A4000000000000000000000001644668C300000553000000000000000000000000000000000000004E00000000osc-plugin-qam-1.0.3+git0.420bf95/tests/fixtures/request_oneassignoneopen.xml<request id="oneassignoneopen"> <action type="maintenance_release"> <source project="SUSE:Maintenance:130" package="update-test-trival.SUSE_SLE-12_Update"/> <target project="SUSE:SLE-12:Update" package="update-test-trival.130"/> </action> <review state="review" when="2014-11-14T11:12:53" who="anonymous" by_user="anonymous"> </review> <review state="accepted" when="2014-11-14T11:12:53" who="anonymous" by_group="qam-sle"> </review> <review state="new" when="2014-11-14T11:12:53" who="anonymous" by_group="qam-cloud"> </review> <state name="review" who="anonymous" when="2014-12-01T14:46:23"> <comment>In review</comment> </state> <history who="anonymous" when="2014-12-16T10:54:10"> <description>Review got accepted</description> <comment>review assigend to user anonymous</comment> </history> <history who="anonymous" when="2014-12-16T10:54:10"> <description>Review got assigend</description> <comment>review for group qam-sle</comment> </history> <history who="anonymous" when="2014-12-16T10:54:10"> <description>Review got accepted</description> <comment>review assigend to user anonymous</comment> </history> <history who="anonymous" when="2014-12-16T10:54:10"> <description>Review got assigend</description> <comment>review for group qam-cloud</comment> </history> </request> 07070100000089000081A4000000000000000000000001644668C3000002FC000000000000000000000000000000000000004600000000osc-plugin-qam-1.0.3+git0.420bf95/tests/fixtures/request_qam_auto.xml<request id="56789"> <action type="maintenance_release"> <source project="SUSE:Maintenance:130" package="update-test-trival.SUSE_SLE-12_Update"/> <target project="SUSE:SLE-12:Update" package="update-test-trival.130"/> </action> <review state="new" when="2014-11-14T11:12:53" who="anonymous" by_user="anonymous"> </review> <review state="accepted" when="2014-11-14T11:12:53" who="anonymous" by_group="qam-sle"> </review> <review state="accepted" when="2014-11-14T11:12:53" who="anonymous" by_group="qam-auto"> </review> <review state="new" when="2014-11-14T11:12:53" who="anonymous" by_group="qam-cloud"> </review> <state name="review" who="anonymous" when="2014-12-01T14:46:23"> <comment>In review</comment> </state> </request> 0707010000008A000081A4000000000000000000000001644668C300000229000000000000000000000000000000000000004600000000osc-plugin-qam-1.0.3+git0.420bf95/tests/fixtures/request_rejected.xml<request id="56789"> <action type="maintenance_release"> <source project="SUSE:Maintenance:130" package="update-test-trival.SUSE_SLE-12_Update"/> <target project="SUSE:SLE-12:Update" package="update-test-trival.130"/> </action> <review state="accepted" when="2014-11-14T11:12:53" who="anonymous" by_group="qam-sle"> </review> <review state="declined" when="2014-11-14T11:12:53" by_user="anonymous"> </review> <state name="declined" who="anonymous" when="2014-12-01T14:46:23"> <comment>In review</comment> </state> </request> 0707010000008B000081A4000000000000000000000001644668C3000004ED000000000000000000000000000000000000004400000000osc-plugin-qam-1.0.3+git0.420bf95/tests/fixtures/request_search.xml<collection matches="40"> <request id="47159"> <action type="maintenance_release"> <source project="SUSE:Maintenance:209" package="mutt.SUSE_SLE-12_Update"/> <target project="SUSE:SLE-12:Update" package="mutt.209"/> </action> <priority>important</priority> <state name="review" who="anon1" when="2014-12-05T14:53:50"> <comment></comment> </state> <review state="new" by_group="qam-sle"/> <history who="anon1" when="2014-12-05T14:53:52"> <description>Request created</description> <comment>requesting release</comment> </history> <description>requesting release</description> </request> <request id="47251"> <action type="submit"> <source project="Devel:Cloud:5" package="python-pecan" rev="2"/> <target project="SUSE:SLE-12:Update:Products:Cloud5" package="python-pecan"/> </action> <state name="review" who="anon2" when="2014-12-10T16:25:26"> <comment></comment> </state> <review state="new" by_group="legal-auto"/> <review state="new" by_group="cloud-storage-review"/> <history who="anon2" when="2014-12-10T16:25:26"> <description>Request created</description> </history> <description></description> </request> </collection> 0707010000008C000081A4000000000000000000000001644668C30000028A000000000000000000000000000000000000004E00000000osc-plugin-qam-1.0.3+git0.420bf95/tests/fixtures/request_search_none_proj.xml<collection matches="40"> <request id="47159"> <action type="maintenance_release"> <source project="" package="mutt.SUSE_SLE-12_Update"/> <target project="SUSE:SLE-12:Update" package="mutt.209"/> </action> <priority>important</priority> <state name="review" who="anon1" when="2014-12-05T14:53:50"> <comment></comment> </state> <review state="new" by_group="qam-sle"/> <history who="anon1" when="2014-12-05T14:53:52"> <description>Request created</description> <comment>requesting release</comment> </history> <description>requesting release</description> </request> </collection> 0707010000008D000081A4000000000000000000000001644668C300001FC2000000000000000000000000000000000000004600000000osc-plugin-qam-1.0.3+git0.420bf95/tests/fixtures/request_sle11sp4.xml<request id="58800"> <action type="maintenance_release"> <source project="SUSE:Maintenance:610" package="sensors.SUSE_SLE-11-SP4_Update"/> <target project="SUSE:SLE-11-SP4:Update" package="sensors.610"/> </action> <action type="maintenance_release"> <source project="SUSE:Maintenance:610" package="patchinfo"/> <target project="SUSE:SLE-11-SP4:Update" package="patchinfo.610"/> </action> <action type="maintenance_release"> <source project="SUSE:Maintenance:610" package="patchinfo"/> <target project="SUSE:Updates:SLE-SERVER:11-SP4:i586" package="patchinfo.610"/> </action> <action type="maintenance_release"> <source project="SUSE:Maintenance:610" package="patchinfo"/> <target project="SUSE:Updates:SLE-SERVER:11-SP4:ia64" package="patchinfo.610"/> </action> <action type="maintenance_release"> <source project="SUSE:Maintenance:610" package="patchinfo"/> <target project="SUSE:Updates:SLE-SERVER:11-SP4:ppc64" package="patchinfo.610"/> </action> <action type="maintenance_release"> <source project="SUSE:Maintenance:610" package="patchinfo"/> <target project="SUSE:Updates:SLE-SERVER:11-SP4:x86_64" package="patchinfo.610"/> </action> <state name="review" who="anonymous2" when="2015-07-06T12:33:52"> <comment>Test to add qam-cloud to the request.</comment> </state> <review state="accepted" when="2015-06-02T08:43:06" who="anonymous2" by_group="qam-sle"> <comment>[oscqam]::unassign::anonymous2::qam-sle</comment> <history who="anonymous2" when="2015-06-02T09:25:14"> <description>Review got accepted</description> <comment>review assigend to user anonymous</comment> </history> <history who="anonymous2" when="2015-06-26T09:43:52"> <description>Review got reopened</description> <comment>[oscqam]::unassign::anonymous::qam-sle</comment> </history> <history who="anonymous2" when="2015-07-07T09:10:30"> <description>Review got accepted</description> <comment>review assigend to user anonymous2</comment> </history> <history who="anonymous2" when="2015-07-07T09:10:48"> <description>Review got reopened</description> <comment>[oscqam]::unassign::anonymous2::qam-sle</comment> </history> <history who="anonymous" when="2015-07-07T09:12:47"> <description>Review got accepted</description> <comment>review assigend to user anonymous</comment> </history> </review> <review state="new" when="2015-07-06T12:33:52" who="anonymous2" by_group="qam-cloud"> <comment>[oscqam]::unassign::anonymous2::qam-cloud</comment> <history who="anonymous2" when="2015-07-07T09:10:14"> <description>Review got accepted</description> <comment>review assigend to user anonymous2</comment> </history> <history who="anonymous2" when="2015-07-07T09:10:56"> <description>Review got reopened</description> <comment>[oscqam]::unassign::anonymous2::qam-cloud</comment> </history> <history who="anonymous2" when="2015-07-07T09:13:05"> <description>Review got accepted</description> <comment>review assigend to user anonymous2</comment> </history> <history who="anonymous2" when="2015-07-07T09:15:07"> <description>Review got reopened</description> <comment>[oscqam]::unassign::anonymous2::qam-cloud</comment> </history> </review> <review state="accepted" when="2015-07-07T09:10:14" who="anonymous2" by_user="anonymous2"> <comment>[qamosc]::accept::anonymous2 (anon@nowhere.com)::None</comment> <history who="anonymous2" when="2015-07-07T09:10:14"> <description>Review got assigned</description> <comment>review for group qam-cloud</comment> </history> <history who="anonymous2" when="2015-07-07T09:10:30"> <description>Review got assigned</description> <comment>review for group qam-sle</comment> </history> <history who="anonymous2" when="2015-07-07T09:10:48"> <description>Review got accepted</description> <comment>[qamosc]::accept::anonymous2 (anon@nowhere.com)::None</comment> </history> <history who="anonymous2" when="2015-07-07T09:12:47"> <description>Review got assigned</description> <comment>review for group qam-sle</comment> </history> <history who="anonymous2" when="2015-07-07T09:13:05"> <description>Review got assigned</description> <comment>review for group qam-cloud</comment> </history> <history who="anonymous2" when="2015-07-07T09:15:07"> <description>Review got accepted</description> <comment>[qamosc]::accept::anonymous2 (anon@nowhere.com)::None</comment> </history> </review> <history who="BenniBrunner" when="2015-06-01T14:34:32"> <description>Request created</description> <comment>Test-udpate for QA</comment> </history> <history who="BenniBrunner" when="2015-06-02T08:43:06"> <description>Request got a new review request</description> <comment>SLE11-SP4 test-update.</comment> </history> <history who="anonymous" when="2015-06-02T09:25:14"> <description>Review got accepted</description> <comment>review assigend to user anonymous</comment> </history> <history who="anonymous" when="2015-06-02T09:25:14"> <description>Review got assigned</description> <comment>review for group qam-sle</comment> </history> <history who="anonymous2" when="2015-06-26T09:43:52"> <description>Review got reopened</description> <comment>[oscqam]::unassign::anonymous::qam-sle</comment> </history> <history who="anonymous2" when="2015-07-06T12:33:52"> <description>Request got a new review request</description> <comment>Test to add qam-cloud to the request.</comment> </history> <history who="anonymous2" when="2015-07-07T09:10:14"> <description>Review got accepted</description> <comment>review assigend to user anonymous2</comment> </history> <history who="anonymous2" when="2015-07-07T09:10:14"> <description>Review got assigned</description> <comment>review for group qam-cloud</comment> </history> <history who="anonymous2" when="2015-07-07T09:10:30"> <description>Review got accepted</description> <comment>review assigend to user anonymous2</comment> </history> <history who="anonymous2" when="2015-07-07T09:10:30"> <description>Review got assigned</description> <comment>review for group qam-sle</comment> </history> <history who="anonymous2" when="2015-07-07T09:10:48"> <description>Review got reopened</description> <comment>[oscqam]::unassign::anonymous2::qam-sle</comment> </history> <history who="anonymous2" when="2015-07-07T09:10:48"> <description>Review got accepted</description> <comment>[qamosc]::accept::anonymous2 (anon@nowhere.com)::None</comment> </history> <history who="anonymous2" when="2015-07-07T09:10:56"> <description>Review got reopened</description> <comment>[oscqam]::unassign::anonymous2::qam-cloud</comment> </history> <history who="anonymous2" when="2015-07-07T09:12:47"> <description>Review got accepted</description> <comment>review assigend to user anonymous2</comment> </history> <history who="anonymous2" when="2015-07-07T09:12:47"> <description>Review got assigned</description> <comment>review for group qam-sle</comment> </history> <history who="anonymous2" when="2015-07-07T09:13:05"> <description>Review got accepted</description> <comment>review assigend to user anonymous2</comment> </history> <history who="anonymous2" when="2015-07-07T09:13:05"> <description>Review got assigned</description> <comment>review for group qam-cloud</comment> </history> <history who="anonymous2" when="2015-07-07T09:15:07"> <description>Review got reopened</description> <comment>[oscqam]::unassign::anonymous2::qam-cloud</comment> </history> <history who="anonymous2" when="2015-07-07T09:15:07"> <description>Review got accepted</description> <comment>[qamosc]::accept::anonymous2 (anon@nowhere.com)::None</comment> </history> <description>Test-udpate for QA</description> </request> 0707010000008E000081A4000000000000000000000001644668C3000004B1000000000000000000000000000000000000004500000000osc-plugin-qam-1.0.3+git0.420bf95/tests/fixtures/request_sletest.xml<request id="oneassignoneopen"> <action type="maintenance_release"> <source project="SUSE:Maintenance:130" package="update-test-trival.SUSE_SLE-12_Update"/> <target project="SUSE:SLE-12:Update" package="update-test-trival.130"/> </action> <review state="review" when="2014-11-14T11:12:53" who="anonymous" by_user="anonymous"> </review> <review state="accepted" when="2014-11-14T11:12:53" who="anonymous" by_group="qam-sle"> <history who="anonymous" when="2014-12-16T10:54:10"> <description>Review got accepted</description> <comment>review assigend to user anonymous</comment> </history> </review> <review state="new" when="2014-11-14T11:12:53" who="anonymous" by_group="qam-test"> </review> <state name="review" who="anonymous" when="2014-12-01T14:46:23"> <comment>In review</comment> </state> <history who="anonymous" when="2014-12-16T10:54:10"> <description>Review got accepted</description> <comment>review assigend to user anonymous</comment> </history> <history who="anonymous" when="2014-12-16T10:54:10"> <description>Review got assigend</description> <comment>review for group qam-sle</comment> </history> </request> 0707010000008F000081A4000000000000000000000001644668C300000711000000000000000000000000000000000000004700000000osc-plugin-qam-1.0.3+git0.420bf95/tests/fixtures/request_twoassign.xml<request id="twoassign"> <action type="maintenance_release"> <source project="SUSE:Maintenance:130" package="update-test-trival.SUSE_SLE-12_Update"/> <target project="SUSE:SLE-12:Update" package="update-test-trival.130"/> </action> <review state="review" when="2014-11-14T11:12:53" who="anonymous" by_user="anonymous"> </review> <review state="accepted" when="2014-11-14T11:12:53" who="anonymous" by_group="qam-sle"> <history who="anonymous" when="2017-09-06T08:06:39"> <description>Review got accepted</description> <comment>review for group qam-sle</comment> </history> </review> <review state="accepted" when="2014-11-14T11:12:53" who="anonymous" by_group="qam-cloud"> <history who="anonymous" when="2017-09-06T08:06:39"> <description>Review got accepted</description> <comment>review for group qam-sle</comment> </history> </review> <review state="new" when="2014-11-14T11:12:53" who="anonymous" by_user="anonymous"> </review> <state name="review" who="anonymous" when="2014-12-01T14:46:23"> <comment>In review</comment> </state> <history who="anonymous" when="2014-12-16T10:54:10"> <description>Review got accepted</description> <comment>review assigend to user anonymous</comment> </history> <history who="anonymous" when="2014-12-16T10:54:10"> <description>Review got assigned</description> <comment>review for group qam-sle</comment> </history> <history who="anonymous" when="2014-12-16T10:54:10"> <description>Review got accepted</description> <comment>review assigend to user anonymous</comment> </history> <history who="anonymous" when="2014-12-16T10:54:10"> <description>Review got assigned</description> <comment>review for group qam-cloud</comment> </history> </request> 07070100000090000081A4000000000000000000000001644668C3000006C3000000000000000000000000000000000000004900000000osc-plugin-qam-1.0.3+git0.420bf95/tests/fixtures/request_twoassigned.xml<request id="twoassigned"> <action type="maintenance_release"> <source project="SUSE:Maintenance:130" package="update-test-trival.SUSE_SLE-12_Update"/> <target project="SUSE:SLE-12:Update" package="update-test-trival.130"/> </action> <review state="review" when="2014-11-14T11:12:53" who="anonymous" by_user="anonymous"> </review> <review state="accepted" when="2014-11-14T11:12:53" who="anonymous" by_group="qam-sle"> <history who="anonymous" when="2014-12-16T10:54:10"> <description>Review got accepted</description> <comment>review assigend to user anonymous</comment> </history> </review> <review state="accepted" when="2014-11-14T11:12:53" who="anonymous" by_group="qam-cloud"> <history who="anonymous" when="2014-12-16T10:54:10"> <description>Review got accepted</description> <comment>review assigend to user anonymous</comment> </history> </review> <state name="review" who="anonymous" when="2014-12-01T14:46:23"> <comment>In review</comment> </state> <history who="anonymous" when="2014-12-16T10:54:10"> <description>Review got accepted</description> <comment>review assigend to user anonymous</comment> </history> <history who="anonymous" when="2014-12-16T10:54:10"> <description>Review got assigned</description> <comment>review for group qam-sle</comment> </history> <history who="anonymous" when="2014-12-16T10:54:10"> <description>Review got accepted</description> <comment>review assigend to user anonymous</comment> </history> <history who="anonymous" when="2014-12-16T10:54:10"> <description>Review got assigned</description> <comment>review for group qam-cloud</comment> </history> </request> 07070100000091000081A4000000000000000000000001644668C30000022D000000000000000000000000000000000000004400000000osc-plugin-qam-1.0.3+git0.420bf95/tests/fixtures/request_twoqam.xml<request id="56789"> <action type="maintenance_release"> <source project="SUSE:Maintenance:130" package="update-test-trival.SUSE_SLE-12_Update"/> <target project="SUSE:SLE-12:Update" package="update-test-trival.130"/> </action> <review state="new" when="2014-11-14T11:12:53" who="anonymous" by_group="qam-sle"> </review> <review state="new" when="2014-11-14T11:12:53" who="anonymous" by_group="qam-test"> </review> <state name="review" who="anonymous" when="2014-12-01T14:46:23"> <comment>In review</comment> </state> </request> 07070100000092000081A4000000000000000000000001644668C300000559000000000000000000000000000000000000004600000000osc-plugin-qam-1.0.3+git0.420bf95/tests/fixtures/request_unassign.xml<request id="unassign"> <action type="maintenance_release"> <source project="SUSE:Maintenance:130" package="update-test-trival.SUSE_SLE-12_Update"/> <target project="SUSE:SLE-12:Update" package="update-test-trival.130"/> </action> <review state="accepted" when="2014-11-14T11:12:53" who="anonymous" by_user="anonymous"> </review> <review state="new" when="2014-11-14T11:12:53" who="anonymous" by_group="qam-sle"> </review> <review state="new" when="2014-11-14T11:12:53" who="anonymous" by_group="qam-cloud"> </review> <state name="review" who="anonymous" when="2014-12-01T14:46:23"> <comment>In review</comment> </state> <history who="anonymous" when="2014-12-16T10:54:10"> <description>Review got accepted</description> <comment>review assigend to user anonymous</comment> </history> <history who="anonymous" when="2014-12-16T10:54:10"> <description>Review got assigned</description> <comment>review for group qam-sle</comment> </history> <history who="anonymous" when="2014-12-16T12:27:03"> <description>Review got reopened</description> <comment>[oscqam]::unassign::anonymous::qam-sle</comment> </history> <history who="anonymous" when="2014-12-16T12:27:03"> <description>Review got accepted</description> <comment>[oscqam]::unassign::anonymous::qam-sle</comment> </history> </request> 07070100000093000081A4000000000000000000000001644668C300000BB9000000000000000000000000000000000000004800000000osc-plugin-qam-1.0.3+git0.420bf95/tests/fixtures/request_unassigned.xml<request id="60836"> <action type="maintenance_release"> <source project="SUSE:Maintenance:666" package="python-setuptools.SUSE_SLE-12_Update"/> <target project="SUSE:SLE-12:Update" package="python-setuptools.666"/> </action> <action type="maintenance_release"> <source project="SUSE:Maintenance:666" package="patchinfo"/> <target project="SUSE:SLE-12:Update" package="patchinfo.666"/> </action> <action type="maintenance_release"> <source project="SUSE:Maintenance:666" package="patchinfo"/> <target project="SUSE:Updates:12-Cloud-Compute:5:x86_64" package="patchinfo.666"/> </action> <action type="maintenance_release"> <source project="SUSE:Maintenance:666" package="patchinfo"/> <target project="SUSE:Updates:SLE-Module-Containers:12:x86_64" package="patchinfo.666"/> </action> <action type="maintenance_release"> <source project="SUSE:Maintenance:666" package="patchinfo"/> <target project="SUSE:Updates:SLE-Module-Public-Cloud:12:ppc64le" package="patchinfo.666"/> </action> <action type="maintenance_release"> <source project="SUSE:Maintenance:666" package="patchinfo"/> <target project="SUSE:Updates:SLE-Module-Public-Cloud:12:s390x" package="patchinfo.666"/> </action> <action type="maintenance_release"> <source project="SUSE:Maintenance:666" package="patchinfo"/> <target project="SUSE:Updates:SLE-Module-Public-Cloud:12:x86_64" package="patchinfo.666"/> </action> <action type="maintenance_release"> <source project="SUSE:Maintenance:666" package="patchinfo"/> <target project="SUSE:Updates:SLE-SDK:12:ppc64le" package="patchinfo.666"/> </action> <action type="maintenance_release"> <source project="SUSE:Maintenance:666" package="patchinfo"/> <target project="SUSE:Updates:SLE-SDK:12:s390x" package="patchinfo.666"/> </action> <action type="maintenance_release"> <source project="SUSE:Maintenance:666" package="patchinfo"/> <target project="SUSE:Updates:SLE-SDK:12:x86_64" package="patchinfo.666"/> </action> <action type="maintenance_release"> <source project="SUSE:Maintenance:666" package="patchinfo"/> <target project="SUSE:Updates:Storage:1.0:x86_64" package="patchinfo.666"/> </action> <state name="review" who="msmeissn" when="2015-06-22T15:48:44"> <comment/> </state> <review state="new" by_group="qam-cloud"/> <review state="accepted" when="2015-06-22T15:48:49" who="anonymous" by_user="anonymous"> <comment>LGTM</comment> <history who="anonymous" when="2015-06-22T16:09:26"> <description>Review got accepted</description> <comment>LGTM</comment> </history> </review> <review state="new" by_group="qam-sle"/> <review state="accepted" when="2015-06-22T15:48:49" who="LarsMB" by_group="some-group"> <comment/> <history who="LarsMB" when="2015-06-23T09:56:57"> <description>Review got accepted</description> </history> </review> <description>requesting release</description> </request> 07070100000094000081A4000000000000000000000001644668C300000391000000000000000000000000000000000000004400000000osc-plugin-qam-1.0.3+git0.420bf95/tests/fixtures/search_request.xml<collection matches="1"> <request id="56789"> <action type="maintenance_release"> <source project="SUSE:Maintenance:130" package="update-test-trival.SUSE_SLE-12_Update"/> <target project="SUSE:SLE-12:Update" package="update-test-trival.130"/> </action> <review state="new" when="2014-11-14T11:12:53" who="anonymous" by_user="anonymous"> </review> <review state="accepted" when="2014-11-14T11:12:53" who="anonymous" by_group="qam-sle"> <history who="anonymous" when="2017-09-06T08:06:39"> <description>Review got accepted</description> <comment>review for group qam-sle</comment> </history> </review> <review state="new" when="2014-11-14T11:12:53" who="anonymous" by_group="qam-cloud"> </review> <state name="review" who="anonymous" when="2014-12-01T14:46:23"> <comment>In review</comment> </state> </request> </collection> 07070100000095000081A4000000000000000000000001644668C300000467000000000000000000000000000000000000003E00000000osc-plugin-qam-1.0.3+git0.420bf95/tests/fixtures/template.txtSUMMARY: PASSED comment: NONE $Author: oscqam $ Products: SLE-SERVER 11-SP3 (i386, ia64, ppc64, s390x, x86_64), SLE-DESKTOP 11-SP3 (i386, x86_64) Category: security Rating: Important SAT Patch No: 10001 MD5 sum: 10000000000000000000000000000001 SUBSWAMPID: 10001 Packager: oscqam@oscqam.test Bugs: 100001 Repository: None Packages: glibc >= 2.11.3-17.74.13, glibc-32bit >= 2.11.3-17.74.13, glibc-devel >= 2.11.3-17.74.13, glibc-devel-32bit >= 2.11.3-17.74.13, glibc-html >= 2.11.3-17.74.13, glibc-i18ndata >= 2.11.3-17.74.13, glibc-info >= 2.11.3-17.74.13, glibc-locale >= 2.11.3-17.74.13, glibc-locale-32bit >= 2.11.3-17.74.13, glibc-locale-x86 >= 2.11.3-17.74.13, glibc-profile >= 2.11.3-17.74.13, glibc-profile-32bit >= 2.11.3-17.74.13, glibc-profile-x86 >= 2.11.3-17.74.13, glibc-x86 >= 2.11.3-17.74.13, nscd >= 2.11.3-17.74.13 SRCRPMs: glibc, glibc-devel Test Plan Reviewers: oscqam Testplatform: base=sles(major=11,minor=sp3);arch=[i386,x86_64];addon=sdk(major=11,minor=sp3) Testplatform: base=sles(major=11,minor=sp3);arch=[i386,ia64,ppc64,s390x,x86_64] Testplatform: base=sled(major=11,minor=sp3);arch=[i386,x86_64] 07070100000096000081A4000000000000000000000001644668C300000289000000000000000000000000000000000000004100000000osc-plugin-qam-1.0.3+git0.420bf95/tests/fixtures/template_rh.txtSUMMARY: PASSED comment: NONE $Author: oscqam $ Products: RHEL-TEST (i386), SLE-SERVER 11-SP3 (i386, ia64, ppc64, s390x, x86_64) Category: security SAT Patch No: 10001 MD5 sum: 10000000000000000000000000000001 SUBSWAMPID: 10001 Packager: oscqam@oscqam.test Bugs: 100001 Repository: None Packages: glibc >= 2.11.3-17.74.13, glibc-32bit >= 2.11.3-17.74.13 SRCRPMs: glibc, glibc-devel Test Plan Reviewers: oscqam Testplatform: base=sles(major=11,minor=sp3);arch=[i386,x86_64];addon=sdk(major=11,minor=sp3) Testplatform: base=sles(major=11,minor=sp3);arch=[i386,ia64,ppc64,s390x,x86_64] Testplatform: base=sled(major=11,minor=sp3);arch=[i386,x86_64] 07070100000097000081A4000000000000000000000001644668C300000DC0000000000000000000000000000000000000003600000000osc-plugin-qam-1.0.3+git0.420bf95/tests/mockremote.pyfrom collections import defaultdict import logging from oscqam.remotes.commentremote import CommentRemote from oscqam.remotes.groupremote import GroupRemote from oscqam.remotes.priorityremote import PriorityRemote from oscqam.remotes.projectremote import ProjectRemote from oscqam.remotes.requestremote import RequestRemote from oscqam.remotes.userremote import UserRemote from .utils import load_fixture class MockRemote: """Replacement for L{oscqam.models.Remote} that maps HTTP requests to file-paths. The mapping between a request and filepath is determined by the requested URL: the last part of the url is expected to be the identifier, the previous part to the object-type. Files should be named accordingly: {object_type}_{identifier}.xml """ def __init__(self): self.delete_calls = [] self.post_calls = [] self.overrides = defaultdict(dict) self.requests = RequestRemote(self) self.groups = GroupRemote(self) self.users = UserRemote(self) self.comments = CommentRemote(self) self.projects = ProjectRemote(self) self.priorities = PriorityRemote(self) self.remote = "suse-remote" def _load(self, prefix, ids): name = f"{prefix}_{ids}.xml" return load_fixture(name) def _encode_args(self, *args): if args: return repr(args) return "None" def overwrite(self, *args, **kwargs): url = args[0] args = args[1:] if url in self.overrides: # The first arg is the endpoint. enc = self._encode_args(*args) if enc in self.overrides[url]: return self.overrides[url][enc]() return None def get(self, *args, **kwargs): """Replacement for HTTP-get requests. Will first check if the requested URL is registered as an override. If so the override-data will be returned, otherwise the URL will be mapped to the filesystem storage for test-fixtures. """ overwrite = self.overwrite(*args, **kwargs) if overwrite: logging.debug("MOCK::Overwrite: {0}".format(overwrite)) return overwrite url = args[0] args = args[1:] logging.debug("MOCK::URL: {0}".format(url)) try: cls, identifier = url.split("/", 1) except ValueError: if url == "group": cls = "group" identifier = args[0]["login"] else: raise return self._load(cls, identifier) def delete(self, *args, **kwargs): called = "Call-Args: %s. Call-Kwargs: %s" % (args, kwargs) self.delete_calls.append(called) def post(self, *args, **kwargs): called = "Call-Args: %s. Call-Kwargs: %s" % (args, kwargs) overwrite = self.overwrite(*args, **kwargs) if overwrite: return overwrite self.post_calls.append(called) def register_url(self, url, callback, *args): """Allow specifying a override for a given relative url. :param url: Url that should trigger a callback. :type url: str :param callback: Function to call when the url is hit. :type callback: () -> Either(str | Exception) :param *args: Additional arguments that might be passed to in the body of the request. """ enc = self._encode_args(*args) self.overrides[url][enc] = callback 07070100000098000081A4000000000000000000000001644668C300005172000000000000000000000000000000000000003800000000osc-plugin-qam-1.0.3+git0.420bf95/tests/test_actions.pyfrom io import StringIO import pytest from oscqam import actions, errors, fields, models, reject_reasons, remotes from oscqam.actions.oscaction import OscAction from oscqam.actions.report import Report from .utils import FakeTrGetter, create_template_data, load_fixture class UndoAction(OscAction): def __init__(self): # Don't call super to prevent query to model objects. self.undo_stack = [] self.undos = [] def action(self): self.undo_stack.append(lambda: self.undos.append(1)) raise remotes.RemoteError(None, None, None, None, None) user_id = "anonymous" cloud_open = "12345" non_open = "23456" sle_open = "34567" non_qam = "45678" one_assigned = "56789" assigned = "52542" single_assign_single_open = "oneassignoneopen" two_assigned = "twoassigned" multi_available_assign = "twoqam" rejected = "request_rejected.xml" one_open = "sletest" last_qam = "approval_last_qam" inverse_assign_order = "inverse_assign" multireview = "multireview" template_txt = load_fixture("template.txt") def test_undo(): u = UndoAction() u() assert u.undos == [1] def test_infer_no_groups_match(remote): assign_action = actions.AssignAction(remote, user_id, cloud_open) with pytest.raises(errors.NonMatchingUserGroupsError): assign_action() # TODO: Fix this @pytest.mark.skip("Not implemented yet in mock") def test_infer_groups_match(remote): args = { "project": "SUSE:Maintenance:130", "withfullhistory": "1", "view": "collection", } remote.register_url("request", lambda: "<request />", args) assign_action = actions.AssignAction( remote, user_id, sle_open, template_factory=lambda r: True ) assign_action() assert len(remote.post_calls) == 1 def test_infer_groups_no_qam_reviews(remote): assign_action = actions.AssignAction(remote, user_id, non_qam) with pytest.raises(errors.NoQamReviewsError): assign_action() def test_unassign_explicit_group(remote): unassign = actions.UnassignAction(remote, user_id, non_open, ["qam-test"]) unassign() assert len(remote.post_calls) == 1 def test_unassign_multi_reviewer(remote): out = StringIO() unassign = actions.UnassignAction( remote, user_id, multireview, ["qam-sle"], out=out ) unassign() assert ( "Unassigning Unknown User (anonymous@nowhere.none) from 56789 for group qam-sle." in unassign.out.getvalue() ) def test_unassign_inferred_group(remote): unassign = actions.UnassignAction(remote, user_id, assigned) unassign() assert len(remote.post_calls) == 1 def test_unassign_subset_group(remote): out = StringIO() unassign = actions.UnassignAction( remote, user_id, two_assigned, ["qam-sle"], out=out ) unassign() assert len(remote.post_calls) == 1 assert ( "Unassigning Unknown User (anonymous@nowhere.none) from twoassigned for group qam-sle" in unassign.out.getvalue() ) def test_assign_non_matching_groups(remote): assign = actions.AssignAction( remote, user_id, single_assign_single_open, template_factory=lambda r: True ) with pytest.raises(errors.NonMatchingUserGroupsError): assign() def test_assign_multiple_groups(remote): assign = actions.AssignAction( remote, user_id, multi_available_assign, template_factory=lambda r: True ) with pytest.raises(errors.UninferableError): assign() # TODO: Fix this @pytest.mark.skip("Not implemented yet in mock") def test_assign_multiple_groups_explicit(remote): args = { "project": "SUSE:Maintenance:130", "withfullhistory": "1", "view": "collection", } remote.register_url("request", lambda: "<request />", args) out = StringIO() assign = actions.AssignAction( remote, user_id, multi_available_assign, groups=["qam-test"], template_factory=lambda r: True, out=out, ) assign() assert ( assign.out.getvalue() == "Assigning Unknown User (anonymous@nowhere.none) to qam-test for 56789.\n" ) def test_unassign_no_group(remote): unassign = actions.UnassignAction(remote, user_id, non_qam) with pytest.raises(errors.NoReviewError): unassign() def test_unassign_multiple_groups(remote): out = StringIO() unassign = actions.UnassignAction(remote, user_id, two_assigned, out=out) unassign() assert ( "Unassigning Unknown User (anonymous@nowhere.none) from twoassigned for group qam-sle" in unassign.out.getvalue() ) assert ( "Unassigning Unknown User (anonymous@nowhere.none) from twoassigned for group qam-cloud" in unassign.out.getvalue() ) def test_reject_not_failed(remote): """Can not reject a request when the test report is not failed.""" request = remote.requests.by_id(cloud_open) template = models.Template(request, tr_getter=FakeTrGetter(template_txt)) action = actions.RejectAction( remote, user_id, cloud_open, [reject_reasons.RejectReason.administrative], False ) action._template = template with pytest.raises(errors.TestResultMismatchError) as context: action() assert models.Template.base_url in str(context.value) def test_reject_no_comment(remote): """Can not reject a request when the test report is not failed.""" request = remote.requests.by_id(cloud_open) template = models.Template( request, tr_getter=FakeTrGetter( "SUMMARY: FAILED" "\n" "comment: NONE" "\n" "\n" "Products: test" ), ) action = actions.RejectAction( remote, user_id, cloud_open, [reject_reasons.RejectReason.administrative], False ) action._template = template with pytest.raises(errors.NoCommentError): action() def test_reject_no_comment_force(remote): """Reject can be forced without template""" request = remote.requests.by_id(cloud_open) endpoint = "source/{prj}/_attribute/MAINT:RejectReason".format( prj=request.src_project ) remote.register_url(endpoint, lambda: load_fixture("reject_reason_attribute.xml")) action = actions.RejectAction( remote, user_id, cloud_open, [reject_reasons.RejectReason.administrative], True ) action() assert len(remote.post_calls), 2 assert request.src_project in remote.post_calls[0] assert "Testreport: There is no template" in remote.post_calls[1] def test_reject_posts_reason(remote): """Rejecting a request will post a reason attribute.""" request = remote.requests.by_id(cloud_open) template = models.Template( request, tr_getter=FakeTrGetter( """SUMMARY: FAILED comment: Something broke.""", ), ) endpoint = "source/{prj}/_attribute/MAINT:RejectReason".format( prj=request.src_project ) remote.register_url(endpoint, lambda: load_fixture("reject_reason_attribute.xml")) action = actions.RejectAction( remote, user_id, cloud_open, [reject_reasons.RejectReason.administrative], False ) action._template = template action() assert len(remote.post_calls), 2 assert request.src_project in remote.post_calls[0] def test_assign_no_report(remote): def raiser(request): raise errors.TemplateNotFoundError("") assign = actions.AssignAction( remote, user_id, multi_available_assign, groups=["qam-test"], template_factory=raiser, ) with pytest.raises(errors.ReportNotYetGeneratedError): assign() def test_assign_no_review(remote): assign = actions.AssignAction( remote, user_id, "rejected", groups=["qam-test"], template_factory=lambda r: True, ) with pytest.raises(errors.NoQamReviewsError): assign() # TODO: Fix this @pytest.mark.skip("Not implemented yet in mock") def test_list_assigned_user(remote): remote.register_url( "request", lambda: load_fixture("search_request.xml"), { "states": "new,review", "user": "anonymous", "view": "collection", "withfullhistory": "1", }, ) action = actions.ListAssignedUserAction( remote, user_id, template_factory=lambda r: True ) requests = action.load_requests() assert len(requests) == 1 def test_list_assigned(remote): action = actions.ListAssignedAction(remote, "anonymous", fields.DefaultFields()) remote.register_url("group", lambda: load_fixture("group_all.xml")) endpoint = "/source/SUSE:Maintenance:130/_attribute/" "OBS:IncidentPriority" remote.register_url(endpoint, lambda: load_fixture("incident_priority.xml")) requests = action.load_requests() assert len(requests) == 1 def test_approval_requires_status_passed(remote): request = remote.requests.by_id(cloud_open) report = create_template_data( **{ "SUMMARY": "FAILED", } ) template = models.Template(request, tr_getter=FakeTrGetter(report)) approval = actions.ApproveUserAction( remote, user_id, "12345", user_id, template_factory=lambda _: template ) with pytest.raises(errors.TestResultMismatchError): approval() def test_approval(remote): request = remote.requests.by_id(cloud_open) report = create_template_data( **{ "SUMMARY": "PASSED", } ) template = models.Template(request, tr_getter=FakeTrGetter(report)) approval = actions.ApproveUserAction( remote, user_id, "12345", user_id, template_factory=lambda _: template ) approval() assert len(remote.post_calls) == 1 def test_report_field(): assert "Assigned Roles" == str(fields.ReportField.assigned_roles) assert fields.ReportField.assigned_roles == fields.ReportField.from_str( "Assigned Roles" ) def test_load_requests_with_exception(remote): def raise_template_not_found(self): raise errors.TemplateNotFoundError("Test error") request_1 = remote.requests.by_id(cloud_open) request_2 = remote.requests.by_id(non_open) request_2.get_template = raise_template_not_found action = actions.ListOpenAction(remote, "anonymous", template_factory=lambda r: r) requests = list(action._load_listdata([request_1, request_2])) assert len(requests) == 1 def test_remove_comment(remote): action = actions.DeleteCommentAction(remote, user_id, "0") action() assert len(remote.delete_calls) == 1 def test_assign_previous_reject_not_old_reviewer(remote): remote.register_url( "request", lambda: load_fixture(rejected), { "project": "SUSE:Maintenance:130", "view": "collection", "withfullhistory": "1", }, ) assign = actions.AssignAction( remote, "anonymous2", multi_available_assign, groups=["qam-test"], template_factory=lambda r: r, ) with pytest.raises(errors.NotPreviousReviewerError): assign() # TODO: FIX thix @pytest.mark.skip("Broken test - maybe wrong fixture") def test_assign_previous_reject_old_reviewer(remote): out = StringIO() remote.register_url( "request", lambda: load_fixture(rejected), { "project": "SUSE:Maintenance:130", "view": "collection", "withfullhistory": "1", }, ) assign = actions.AssignAction( remote, "anonymous", multi_available_assign, groups=["qam-test"], template_factory=lambda r: r, out=out, ) assign() assert ( assign.out.getvalue() == "Assigning Unknown User (anonymous@nowhere.none) to qam-test for 56789.\n" ) def test_assign_previous_reject_not_old_reviewer_force(remote): out = StringIO() remote.register_url( "request", lambda: load_fixture(rejected), { "project": "SUSE:Maintenance:130", "view": "collection", "withfullhistory": "1", }, ) assign = actions.AssignAction( remote, "anonymous2", multi_available_assign, groups=["qam-test"], template_factory=lambda r: r, force=True, out=out, ) assign() assert ( assign.out.getvalue() == "Assigning Unknown User (anon2@nowhere.none) to qam-test for 56789.\n" ) # TODO: FIX thix @pytest.mark.skip("Broken test - maybe wrong fixture") def test_assign_skip_template(remote): """Assign a request without a testreport template.""" out = StringIO() remote.register_url( "request", lambda: load_fixture(rejected), { "project": "SUSE:Maintenance:130", "view": "collection", "withfullhistory": "1", }, ) def raiser(request): raise errors.TemplateNotFoundError("") assign = actions.AssignAction( remote, user_id, multi_available_assign, groups=["qam-test"], template_factory=raiser, out=out, template_required=False, ) assign() assert ( assign.out.getvalue() == "Assigning Unknown User (anonymous@nowhere.none) to qam-test for 56789.\n" ) def test_report(remote): report = create_template_data( **{ "SUMMARY": "PASSED", } ) request = remote.requests.by_id(cloud_open) template = models.Template(request, tr_getter=FakeTrGetter(report)) report = Report(request=request, template_factory=lambda _: template) assert report.value(fields.ReportField.assigned_roles) == [ "qam-sle -> Unknown User (anonymous@nowhere.none)" ] assert report.value(fields.ReportField.package_streams) == [ "update-test-trival.SUSE_SLE-12_Update" ] assert report.value(fields.ReportField.unassigned_roles) == ["qam-cloud"] def test_unassign_permission_error(remote): def raiser(): raise remotes.RemoteError(None, None, None, None, None) out = StringIO() remote.register_url( "request/twoassigned?newstate=accepted&" "cmd=changereviewstate&by_user=anonymous", raiser, "[oscqam] Unassigning Unknown User (anonymous@nowhere.none) from " "twoassigned for group qam-cloud, qam-sle.", ) unassign = actions.UnassignAction(remote, user_id, two_assigned, out=out) unassign() value = unassign.out.getvalue() assert ( "Unassigning Unknown User (anonymous@nowhere.none) from twoassigned for group qam-sle" in value ) assert ( "Unassigning Unknown User (anonymous@nowhere.none) from twoassigned for group qam-cloud" in value ) def test_decline_output(remote): out = StringIO() request = remote.requests.by_id(cloud_open) template = models.Template( request, tr_getter=FakeTrGetter( """SUMMARY: FAILED comment: Something broke.""", ), ) endpoint = "source/{prj}/_attribute/MAINT:RejectReason".format( prj=request.src_project ) remote.register_url(endpoint, lambda: load_fixture("reject_reason_attribute.xml")) action = actions.RejectAction( remote, user_id, cloud_open, [reject_reasons.RejectReason.administrative], False, out=out, ) action._template = template action() assert ( "Declining request {req} for {user}. See Testreport: {url}".format( req=request, user=action.user, url=action.template.fancy_url ) in action.out.getvalue() ) def test_approve_output(remote): out = StringIO() request = remote.requests.by_id(cloud_open) report = create_template_data(**{"SUMMARY": "PASSED"}) template = models.Template(request, tr_getter=FakeTrGetter(report)) approval = actions.ApproveUserAction( remote, user_id, "12345", user_id, template_factory=lambda _: template, out=out ) approval() assert ( "Approving {req} for {user} ({group}). Testreport: {url}\n".format( req=request, user=approval.reviewer, url=approval.template.fancy_url, group="qam-sle", ) == approval.out.getvalue() ) def test_approve_not_assigned(remote): """A user can not approve an update that is not assigned to him.""" unassigned_request = remote.requests.by_id(multi_available_assign) report = create_template_data(**{"SUMMARY": "PASSED"}) template = models.Template(unassigned_request, tr_getter=FakeTrGetter(report)) approve_action = actions.ApproveUserAction( remote, user_id, multi_available_assign, user_id, template_factory=lambda _: template, ) with pytest.raises(errors.NotAssignedError): approve_action() # TODO: FIX thix @pytest.mark.skip("Broken test - maybe wrong fixture") def test_approve_additional_groups(remote): """If a user can handle more groups after an approval he will be notified about it. """ out = StringIO() request = remote.requests.by_id( one_open, ) report = create_template_data( **{ "SUMMARY": "PASSED", } ) template = models.Template(request, tr_getter=FakeTrGetter(report)) approval = actions.ApproveUserAction( remote, user_id, one_open, user_id, template_factory=lambda _: template, out=out, ) approval() assert ( "Approving {req} for {user} ({group}). Testreport: {url}\n".format( req=request, user=approval.reviewer, url=approval.template.fancy_url, group="qam-sle", ) == approval.out.getvalue() ) assert ( "The following groups could also be reviewed by you: qam-test" in approval.out.getvalue() ) def test_approve_group(remote): out = StringIO() request = remote.requests.by_id( one_open, ) report = create_template_data( **{ "SUMMARY": "PASSED", } ) template = models.Template(request, tr_getter=FakeTrGetter(report)) approval = actions.ApproveGroupAction( remote, user_id, one_open, "qam-test", template_factory=lambda _: template, out=out, ) approval() assert ( "Approving {req} for group {group}.".format(req=request, group="qam-test") in approval.out.getvalue() ) def test_approve_group_not_in_request(remote): out = StringIO() request = remote.requests.by_id( one_open, ) report = create_template_data( **{ "SUMMARY": "PASSED", } ) template = models.Template(request, tr_getter=FakeTrGetter(report)) approval = actions.ApproveGroupAction( remote, user_id, one_open, "qam-cloud", template_factory=lambda _: template, out=out, ) with pytest.raises(errors.NonMatchingGroupsError): approval() def test_approve_last_group_does_not_raise(remote): out = StringIO() request = remote.requests.by_id( last_qam, ) report = create_template_data( **{ "SUMMARY": "PASSED", } ) template = models.Template(request, tr_getter=FakeTrGetter(report)) approval = actions.ApproveUserAction( remote, user_id, last_qam, user_id, template_factory=lambda _: template, out=out, ) approval() assert ( "Approving {req} for {user} ({group}). Testreport: {url}".format( req=request, user=approval.reviewer, url=approval.template.fancy_url, group="qam-sle", ) in approval.out.getvalue() ) def test_approve_misses_assigned_role(remote): out = StringIO() request = remote.requests.by_id( inverse_assign_order, ) report = create_template_data( **{ "SUMMARY": "PASSED", } ) template = models.Template(request, tr_getter=FakeTrGetter(report)) approval = actions.ApproveUserAction( remote, user_id, inverse_assign_order, user_id, template_factory=lambda _: template, out=out, ) approval() assert ( "Approving {req} for {user} ({group}). Testreport: {url}".format( req=request, user=approval.reviewer, url=approval.template.fancy_url, group="qam-sle", ) in approval.out.getvalue() ) 07070100000099000081A4000000000000000000000001644668C300000629000000000000000000000000000000000000003400000000osc-plugin-qam-1.0.3+git0.420bf95/tests/test_cli.pyimport builtins from contextlib import contextmanager from oscqam import formatters from oscqam.utils import multi_level_sort from oscqam.common import Common @contextmanager def wrap_builtin(answer): raw_input = builtins.input builtins.input = lambda x: answer yield builtins.input = raw_input def test_multi_level_sort(): one = {"a": 0, "b": 1} two = {"a": 0, "b": 0} xs = [one, two] criteria = [lambda x: x["b"], lambda x: x["a"]] sortedxs = multi_level_sort(xs, criteria) assert sortedxs[0] == two assert sortedxs[1] == one def test_lineseperators(): line = formatters.os_lineseps("Test\n", target="Windows") assert line == "Test\r\n" line = formatters.os_lineseps("Test\r\n", target="Windows") assert line == "Test\r\n" def test_yes_no_question_true(): interpreter = Common with wrap_builtin("yes"): result = interpreter.yes_no("Sure about that") assert result with wrap_builtin("Y"): result = interpreter.yes_no("Sure about that") assert result with wrap_builtin("yEs"): result = interpreter.yes_no("Sure about that") assert result def test_yes_no_question_false(): interpreter = Common with wrap_builtin("no"): result = interpreter.yes_no("Sure about that") assert result is False with wrap_builtin("n"): result = interpreter.yes_no("Sure about that") assert result is False with wrap_builtin("nO"): result = interpreter.yes_no("Sure about that") assert result is False 0707010000009A000081A4000000000000000000000001644668C3000003A2000000000000000000000000000000000000003700000000osc-plugin-qam-1.0.3+git0.420bf95/tests/test_fields.pyfrom oscqam.fields import InvalidFieldsError, levenshtein def test_insertion_levenshtein(): assert 1 == levenshtein("a", "ab") def test_deletion_levenshtein(): assert 1 == levenshtein("ab", "a") def test_modification_levenshtein(): assert 1 == levenshtein("ab", "ac") def test_equal_levenshtein(): assert 0 == levenshtein("a", "a") def test_mismatched_casing(): assert 2 == levenshtein("RequestReviewId", "Requestreviewid") def test_long_mismatch(): assert 12 == levenshtein("RequestReviewId", "Assigned Roles") def test_suggestions(): fields = ["ReviewRequest"] error = InvalidFieldsError(fields) suggestions = error._get_suggestions(fields) assert suggestions == set(["ReviewRequestID"]) fields = ["ReviewRequest", "Bugz"] error = InvalidFieldsError(fields) suggestions = error._get_suggestions(fields) assert suggestions == set(["ReviewRequestID", "Bugs"]) 0707010000009B000081A4000000000000000000000001644668C300003992000000000000000000000000000000000000003600000000osc-plugin-qam-1.0.3+git0.420bf95/tests/test_model.pyfrom io import StringIO from urllib.error import HTTPError import osc import pytest import responses from oscqam.domains import Priority, UnknownPriority from oscqam.errors import MissingSourceProjectError from oscqam.models import ( Assignment, Attribute, Bug, Comment, Group, Request, Template, User, ) from oscqam.reject_reasons import RejectReason from .mockremote import MockRemote from .utils import FakeTrGetter, create_template_data, load_fixture comment_1_xml = load_fixture("comments_1.xml") req_1_xml = load_fixture("request_12345.xml") req_2_xml = load_fixture("request_23456.xml") req_3_xml = load_fixture("request_52542.xml") req_4_xml = load_fixture("request_56789.xml") req_search = load_fixture("request_search.xml") req_search_none = load_fixture("request_search_none_proj.xml") req_no_src = load_fixture("request_no_src.xml") req_assign = load_fixture("request_assign.xml") req_unassign = load_fixture("request_unassign.xml") req_unassigned = load_fixture("request_unassigned.xml") req_invalid = load_fixture("request_no_src.xml") req_sle11sp4 = load_fixture("request_sle11sp4.xml") req_qam_auto = load_fixture("request_qam_auto.xml") req_two_assign = load_fixture("request_twoassign.xml") template_txt = load_fixture("template.txt") template_rh = load_fixture("template_rh.txt") user_txt = load_fixture("person_anonymous.xml") group_txt = load_fixture("group_qam-sle.xml") bugs_txt = load_fixture("bug_patchinfo.xml") def create_template(request_data=None, template_data=None): if not request_data: request_data = req_1_xml if not template_data: template_data = template_txt request = Request.parse(MockRemote(), request_data)[0] template = Template(request, tr_getter=FakeTrGetter(template_data)) return template def test_merge_requests(remote): request_1 = Request.parse(remote, req_1_xml)[0] request_2 = Request.parse(remote, req_1_xml)[0] requests = set([request_1, request_2]) assert len(requests) == 1 def test_search(remote): """Only requests that are part of SUSE:Maintenance projects should be used. """ requests = Request.parse(remote, req_search) assert len(requests) == 2 requests = Request.filter_by_project("SUSE:Maintenance", requests) assert len(requests) == 1 def test_search_empty_source_project(remote): """Projects with empty source project should be handled gracefully.""" requests = Request.parse(remote, req_search_none) requests = Request.filter_by_project("SUSE:Maintenance", requests) assert len(requests) == 0 def test_project_without_source_project(remote): """When project attribute can be found in a source tag the API should just return an empty string and not fail. """ requests = Request.parse(remote, req_no_src) assert requests[0].src_project == "" requests = Request.filter_by_project("SUSE:Maintenance", requests) assert len(requests) == 0 def test_assigned_roles_request(remote): request = Request.parse(remote, req_assign)[0] assigned = request.assigned_roles assert len(assigned) == 1 assert assigned[0].user.login == "anonymous" assert assigned[0].group.name == "qam-sle" request = Request.parse(remote, req_3_xml)[0] assigned = request.assigned_roles assert len(assigned) == 1 assert assigned[0].user.login == "anonymous" assert assigned[0].group.name == "qam-sle" def test_assigned_multiple_roles(remote): request = Request.parse(remote, req_two_assign)[0] assigned = request.assigned_roles assert len(assigned) == 2 groups = [a.group.name for a in assigned] logins = [a.user.login for a in assigned] assert "anonymous" in logins assert "qam-sle" in groups assert "qam-cloud" in groups def test_assigned_roles_sle11_sp4(remote): request = Request.parse(remote, req_sle11sp4)[0] assigned = request.assigned_roles assert len(assigned) == 1 assert assigned[0].user.login == "anonymous" assert assigned[0].group.name == "qam-sle" def test_unassigned_removes_roles(remote): request = Request.parse(remote, req_unassign)[0] assigned = request.assigned_roles assert len(assigned) == 0 def test_parse_request_id(): test_id = "SUSE:Maintenance:123:45678" req_id = Request.parse_request_id(test_id) assert req_id == "45678" def test_template_splits_srcrpms(): assert create_template().log_entries["SRCRPMs"] == ["glibc", "glibc-devel"] def test_template_splits_bugs(): template_data = create_template_data(Bugs="100001, 100002, 100003") assert create_template(template_data=template_data).log_entries["Bugs"] == [ "100001", "100002", "100003", ] def test_template_splits_products(): assert create_template().log_entries["Products"] == [ "SERVER 11-SP3 (i386, ia64, ppc64, s390x, x86_64)", "DESKTOP 11-SP3 (i386, x86_64)", ] def test_template_splits_non_sle_products(): assert create_template(template_data=template_rh).log_entries["Products"] == [ "RHEL-TEST (i386)", "SERVER 11-SP3 (i386, ia64, ppc64, s390x, x86_64)", ] def test_replacing_sle_prefix(): template_data = create_template_data(Products="SLE-PSLE-SP3 (i386)") assert create_template(template_data=template_data).log_entries["Products"] == [ "PSLE-SP3 (i386)" ] def test_multi_line_comment(): template_data = create_template_data(comment="A comment\nwith multiple lines") assert ( create_template(template_data=template_data).log_entries["comment"] == "A comment\nwith multiple lines" ) def test_template_key_repeats(): template_data = "\n".join( [ "comment: a", "$Author: b", "Products: b", "Testplatform: base=sles", "Testplatform: base=studio", ] ) assert ( create_template(template_data=template_data).log_entries["Testplatform"] == "base=sles\nbase=studio" ) def test_multi_line_comment_first_line_empty(): template_data = create_template_data(comment="\nwith multiple lines") assert ( create_template(template_data=template_data).log_entries["comment"] == "with multiple lines" ) def test_multi_line_comment_with_header_seperator(): template_data = create_template_data(comment="\nwith: multiple lines") assert ( create_template(template_data=template_data).log_entries["comment"] == "with: multiple lines" ) def test_template_for_invalid_request(remote): request = Request.parse(remote, req_invalid)[0] with pytest.raises(MissingSourceProjectError): request.get_template(Template) def test_assignment_equality(remote): user = User.parse(remote, user_txt)[0] group = Group.parse(remote, group_txt)[0] a1 = Assignment(user, group) a2 = Assignment(user, group) assert a1 == a2 def test_assignment_inference_single_group(remote): """Test that assignments can be inferred from a single group even if the comments are not used. """ request = Request.parse(remote, req_4_xml)[0] assignments = Assignment.infer(remote, request) assert len(assignments) == 1 assignment = assignments[0] assert assignment.user.login == "anonymous" assert assignment.group.name == "qam-sle" def test_assignment_inference_ignores_qam_auto(remote): request = Request.parse(remote, req_4_xml)[0] assignments = Assignment.infer(remote, request) assert len(assignments) == 1 assignment = assignments[0] assert assignment.user.login == "anonymous" assert assignment.group.name == "qam-sle" @responses.activate def test_incident_priority(remote): request = Request.parse(remote, req_1_xml)[0] src_project = request.src_project endpoint = "/source/{0}/_attribute/OBS:IncidentPriority".format(src_project) remote.register_url( endpoint, lambda: ( "<attributes>" "<attribute name='IncidentPriority' namespace='OBS'>" "<value>100</value>" "</attribute>" "</attributes>" ), ) incident_priority = request.incident_priority assert incident_priority == Priority(100) @responses.activate def test_incident_priority_empty(remote): request = Request.parse(remote, req_1_xml)[0] src_project = request.src_project endpoint = "/source/{0}/_attribute/OBS:IncidentPriority".format(src_project) remote.register_url(endpoint, lambda: "<attributes/>") incident_priority = request.incident_priority assert incident_priority == UnknownPriority() @responses.activate def test_incident_priority_empty_value(remote): request = Request.parse(remote, req_1_xml)[0] src_project = request.src_project endpoint = "/source/{0}/_attribute/OBS:IncidentPriority".format(src_project) remote.register_url( endpoint, lambda: ( "<attributes>" "<attribute name='IncidentPriority' namespace='OBS'>" "<value />" "</attribute>" "</attributes>" ), ) incident_priority = request.incident_priority assert incident_priority == UnknownPriority() @responses.activate def test_no_incident_priority(remote): def raise_http(): raise HTTPError("test", 500, "test", "", StringIO("")) request = Request.parse(remote, req_1_xml)[0] src_project = request.src_project endpoint = "/source/{0}/_attribute/OBS:IncidentPriority".format(src_project) remote.register_url(endpoint, raise_http) request = Request.parse(remote, req_1_xml)[0] assert request.incident_priority == UnknownPriority() def test_priority_str(): priority = UnknownPriority() assert "None" == str(priority) priority = Priority(100) assert "100" == str(priority) def test_unassigned_roles(remote): request = Request.parse(remote, req_unassigned)[0] open_reviews = request.review_list_open() assert len(open_reviews) == 2 assert open_reviews[0].reviewer.name == "qam-cloud" assert open_reviews[1].reviewer.name == "qam-sle" def test_obs27_workaround_pre_152(remote): def raise_wrong_args(self, request): raise osc.oscerr.WrongArgs("acceptinfo") original_version = osc.core.get_osc_version original_read = Request.read osc.core.get_osc_version = lambda: "0.151" Request.read = raise_wrong_args try: request = Request.parse(remote, req_unassigned) assert request == [] finally: Request.read = original_read osc.core.get_osc_version = original_version def test_obs27_workaround_post_152(remote): def raise_wrong_args(self, request): raise osc.oscerr.WrongArgs("acceptinfo") original_read = Request.read Request.read = raise_wrong_args try: with pytest.raises(osc.oscerr.WrongArgs): Request.parse(remote, req_unassigned) finally: Request.read = original_read def test_request_str(remote): request = Request.parse(remote, req_1_xml)[0] assert str(request) == "12345" def test_parse_comment(remote): comment = Comment.parse(remote, comment_1_xml)[0] assert comment.id == "1322" assert comment.who == "anonymous" assert comment.text == "test comment - please remove" def test_parse_empty_comment(remote): comment_data = '<comments request="0"/>' comments = Comment.parse(remote, comment_data) assert [] == comments def test_attribute_parsing(remote): attribute = Attribute.parse(remote, load_fixture("reject_reason_attribute.xml"))[0] assert attribute.value == ["12345:abc", "23456:def"] def test_attribute_writing(remote): attribute = Attribute.parse(remote, load_fixture("reject_reason_attribute.xml"))[0] assert ( attribute.xml() == b'<attribute name="RejectReason" namespace="MAINT"><value>12345:abc</value><value>23456:def</value></attribute>' ) def test_attribute_get(remote): request = Request.parse(remote, req_1_xml)[0] endpoint = "source/{prj}/_attribute/MAINT:RejectReason".format( prj=request.src_project ) attribute = Attribute.parse(remote, load_fixture("reject_reason_attribute.xml"))[0] remote.register_url(endpoint, lambda: load_fixture("reject_reason_attribute.xml")) assert attribute == request.attribute("MAINT:RejectReason") def test_attribute_post(remote): reject = Attribute.preset(remote, Attribute.reject_reason, "Some_Value") remote.projects.set_attribute("oscqam:test", reject) assert len(remote.post_calls) == 1 def test_build_reject_reason(remote): request = Request.parse(remote, req_1_xml)[0] endpoint = "source/{prj}/_attribute/MAINT:RejectReason".format( prj=request.src_project ) remote.register_url( endpoint, lambda: load_fixture("reject_reason_attribute_empty.xml") ) reject_reasons = [RejectReason.administrative, RejectReason.build_problem] attribute = request._build_reject_attribute(reject_reasons) value1 = "{reqid}:{admin}".format( reqid=request.reqid, admin=RejectReason.administrative.flag ) value2 = "{reqid}:{build}".format( reqid=request.reqid, build=RejectReason.build_problem.flag ) assert attribute.value == (value1, value2) def test_build_reject_reason_existing_reason(remote): request = Request.parse(remote, req_1_xml)[0] endpoint = "source/{prj}/_attribute/MAINT:RejectReason".format( prj=request.src_project ) remote.register_url(endpoint, lambda: load_fixture("reject_reason_tracking.xml")) reject_reasons = [RejectReason.build_problem] attribute = request._build_reject_attribute(reject_reasons) value1 = "{reqid}:{track}".format( reqid=request.reqid, track=RejectReason.tracking_issue.flag ) value2 = "{reqid}:{build}".format( reqid=request.reqid, build=RejectReason.build_problem.flag ) assert attribute.value == [value1, value2] def test_build_reject_reason_existing_reasons(remote): request = Request.parse(remote, req_1_xml)[0] endpoint = "source/{prj}/_attribute/MAINT:RejectReason".format( prj=request.src_project ) remote.register_url(endpoint, lambda: load_fixture("reject_reason_attribute.xml")) reject_reasons = [RejectReason.build_problem] attribute = request._build_reject_attribute(reject_reasons) value2 = "{reqid}:{build}".format( reqid=request.reqid, build=RejectReason.build_problem.flag ) assert attribute.value == ["12345:abc", "23456:def", value2] def test_parse_bugs(remote): bugs = Bug.parse(remote, bugs_txt, "issue") assert len(bugs) == 4 0707010000009C000081A4000000000000000000000001644668C30000080B000000000000000000000000000000000000003E00000000osc-plugin-qam-1.0.3+git0.420bf95/tests/test_remote_facade.pyfrom oscqam.models.xmlfactorymixin import XmlFactoryMixin from .utils import load_fixture def test_parse_flat_xml(): xml = load_fixture("flat.xml") persons = XmlFactoryMixin.parse(None, xml, "person") john = persons[0] assert john.firstname == "John" assert john.lastname == "Smith" def test_parse_nested_xml(): xml = load_fixture("nested.xml") persons = XmlFactoryMixin.parse(None, xml, "person") john = persons[0] assert john.firstname == "John" assert john.lastname == "Smith" assert john.address.streetname == "Arcadiaavenue" assert john.address.streetnumber == "1" def test_parse_nested_xml_multiple(): xml = load_fixture("nested_multi.xml") persons = XmlFactoryMixin.parse(None, xml, "person") john = persons[0] assert john.firstname == "John" assert john.lastname == "Smith" assert len(john.address) == 2 assert john.address[0].streetname == "Arcadiaavenue" assert john.address[0].streetnumber == "1" assert john.address[1].streetname == "Rassilonblvd" assert john.address[1].streetnumber == "2" def test_parse_attributes(): xml = load_fixture("attributes.xml") persons = XmlFactoryMixin.parse(None, xml, "person") john = persons[0] assert john.firstname == "John" assert john.lastname == "Smith" def test_parse_multi_attributes(): xml = load_fixture("attributes_multi.xml") persons = XmlFactoryMixin.parse(None, xml, "person") john = persons[0] assert john.firstname == "John" assert john.lastname == "Smith" clara = persons[1] assert clara.firstname == "Clara" assert clara.lastname == "Oswald" def test_parse_nested_and_attributes(): xml = load_fixture("nested_attributes.xml") persons = XmlFactoryMixin.parse(None, xml, "person") john = persons[0] assert john.id == "1" assert john.firstname == "John" assert john.lastname == "Smith" assert john.address.main == "True" assert john.address.streetname == "Arcadiaavenue" assert john.address.streetnumber == "1" 0707010000009D000081A4000000000000000000000001644668C300000361000000000000000000000000000000000000003100000000osc-plugin-qam-1.0.3+git0.420bf95/tests/utils.pyfrom collections import OrderedDict from pathlib import Path from oscqam.parsers import TemplateParser path = Path(__file__).parent / "fixtures" def load_fixture(name): file = path / name return file.read_text() def create_template_data(**data): """Adds missing keys and values to the template data.""" data = OrderedDict(**data) if "comment" not in data.keys(): data["comment"] = "" if "Products" not in data.keys(): data["Products"] = "none" if TemplateParser.end_marker not in data.keys(): data[TemplateParser.end_marker] = "" return "\n".join(": ".join(v) for v in (zip(data.keys(), data.values()))) class FakeTrGetter: def __init__(self, tmpl, meta=None) -> None: self.tmpl = tmpl self.meta = meta def __call__(self, *args, **kwds): return (self.tmpl, self.meta) 07070100000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000B00000000TRAILER!!!576 blocks
Locations
Projects
Search
Status Monitor
Help
OpenBuildService.org
Documentation
API Documentation
Code of Conduct
Contact
Support
@OBShq
Terms
openSUSE Build Service is sponsored by
The Open Build Service is an
openSUSE project
.
Sign Up
Log In
Places
Places
All Projects
Status Monitor