summaryrefslogblamecommitdiffstats
path: root/release-notes/reports.py
blob: f45f370a33bbe64755cb22fd10ebff35f60ac30d (plain) (tree)




























































































































































































































































































































































































                                                                                       
#
# RTEMS Tools Project (http://www.rtems.org/)
# Copyright 2018 Danxue Huang (danxue.huang@gmail.com)
# Copyright 2022 Chris Johns (chris@contemporary.software)
# All rights reserved.
#
# This file is part of the RTEMS Tools package in 'rtems-tools'.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#

import datetime
import os
import re
import time
import threading
import sys

from markdown_generator import MarkdownGenerator
import reraise

heading_base = 2


class ticket(object):

    def __init__(self, fmt, ticket):
        self.generator = MarkdownGenerator()
        self.format = fmt
        self.ticket = ticket
        self.thread = None
        self.result = None

    def _format_contents(self):
        ticket_meta = self.ticket['meta']
        ticket_link = self.ticket.get('comment_attachment',
                                      {}).get('link', None)
        summary = ticket_meta.get('summary', None)
        ticket_meta.pop('description', None)
        ticket_meta.pop('summary', None)
        if ticket_link is not None:
            ticket_id_link = \
                self.generator.gen_hyperlink(self.ticket_id(), '#t' + self.ticket_id())
            tlink = self.generator.gen_bold(ticket_id_link) + \
                ' - ' + self.generator.gen_bold(summary)
            self.generator.gen_heading(tlink,
                                       heading_base + 1,
                                       anchor='t' + self.ticket_id())
        for k in ['Created', 'Modified', 'Blocked By']:
            ticket_meta[k] = self.ticket['ticket'][k]
        meta_keys = [k.capitalize() for k in ticket_meta.keys()]
        meta_vals = [v for v in ticket_meta.values()]
        order = [
            'Id', 'Reporter', 'Created', 'Modified', 'Owner', 'Type',
            'Component', 'Status', 'Resolution', 'Version', 'Milestone',
            'Priority', 'Severity', 'Keywords', 'Cc', 'Blocking', 'Blocked by'
        ]
        meta_table = []
        for c in range(0, len(order)):
            i = meta_keys.index(order[c])
            if meta_keys[i] in ['Created', 'Modified']:
                dt = datetime.datetime.strptime(meta_vals[i],
                                                '%m/%d/%y %H:%M:%S')
                ds = dt.strftime('%d %B %Y %H:%M:%S')
                if ds[0] == '0':
                    ds = ds[1:]
                meta_vals[i] = ds
            meta_table += [[
                self.generator.gen_bold(meta_keys[i]), meta_vals[i]
            ]]
        meta_table = [[
            self.generator.gen_bold('Link'),
            self.generator.gen_hyperlink(ticket_link, ticket_link)
        ]] + meta_table
        self.generator.gen_table(None, meta_table, align=['right', 'left'])

    def _description(self, description):
        description = description.replace('\r\n', '\n')

        #
        # The code blocks needs to be reviewed
        #
        if self.ticket_id() == '3384':
            description = re.sub('%s}}}', '%s}\n}\n}', description)

        if self.format == 'markdown':
            description = re.sub(r'{{{(.*)}}}', r'`\1`', description)
        else:
            description = re.sub(r'{{{(.*)}}}', r':code:`\1`', description)

        if self.format == 'rst':
            description = re.sub(r'(>+) ---', r'\1 \-\-\-', description)

        description = re.sub(r'{{{!(.*)\n', '{{{\n', description)
        description = re.sub(r'}}}}', '}\n}}}', description)
        description = re.sub(r'{{{[ \t]+\n', '{{{\n', description)
        description = re.sub(r'{{{([#$])', '{{{\n#', description)
        description = description.replace('{{{\n', '```\n')
        description = description.replace('\n}}}', '\n```')
        description = re.sub(
            r"^[ \t]*#([ \t]*define|[ \t]*include|[ \t]*endif|[ \t]*ifdef|" \
            "[ \t]*ifndef|[ \t]*if|[ \t]*else)(.*)$",
            r"`#\1\2`",
            description,
            flags=re.MULTILINE)

        if self.format == 'markdown':
            description = re.sub(r'{{{(?!\n)', '`', description)
            description = re.sub(r'(?!\n)}}}', '`', description)
        else:
            description = re.sub(r'{{{(?!\n)', ':code:`', description)
            description = re.sub(r'(?!\n)}}}', '`', description)

        # Two lines after the opening (and after the ending)
        # back-ticks misses up with the text area rendering.
        description = re.sub('```\n\n', '```\n', description)
        description = re.sub('\n\n```', '\n```', description)

        # For ticket 2624 where the opening three curly braces are not
        # on a separate line.
        description = re.sub(r'```(?!\n)', '```\n', description)
        description = re.sub(r'(?!\n)```', '\n```', description)

        # For ticket 2993 where the defective closing curly brackets
        # miss up with text area rendering.
        description = re.sub('}}:', '```\n', description)

        # Ticket 3771 has code that's not written in a code block,
        # which is interpretted by the Markdown generator as headers
        # (#define)... Hence, we fix that manually.

        if self.ticket_id() == '3771':
            description = re.sub('`#define',
                                 '```\n#define',
                                 description,
                                 count=1)
            description = re.sub('Problem facing on writing',
                                 '```\nProblem facing on writing',
                                 description,
                                 count=1)
            description = re.sub(r'[ ]{8,}', ' ', description)

        if self.format == 'rst':
            description = description.replace('=', '\\=')
            description = description.replace('\n', '\n\n')
            description = re.sub(r'^(#+)', '', description, flags=re.MULTILINE)

        return description

    def _format_description(self):
        if 'description' not in self.ticket['comment_attachment']:
            return
        description = self.ticket['comment_attachment']['description']
        self.generator.gen_raw_text(self.generator.gen_bold('Description'))
        self.generator.gen_line('')
        self.generator.gen_line_block(self._description(description))
        self.generator.gen_line('')

    def _meta_label(self, label):
        if label == 'attachment':
            label = 'attach'
        return label

    def _format_comments(self):
        if 'comments' not in self.ticket['comment_attachment']:
            return
        comments = self.ticket['comment_attachment']['comments']
        if len(comments) == 0:
            return
        self.generator.gen_line('')
        cnt = 0
        bold = self.generator.gen_bold
        for comment in comments:
            cnt += 1
            self.generator.gen_line(
                self.generator.gen_topic('Comment ' + str(cnt)))
            self.generator.gen_line('')
            if not comment['creator']:
                creator = 'none'
            else:
                creator = comment['creator']
            ul = [bold(creator) + ', ' + comment['published']]
            for m in comment['meta']:
                ul += [bold(self._meta_label(m[0]) + ':') + ' ' + m[1]]
            self.generator.gen_raw(self.generator.gen_ordered_lists(ul))
            self.generator.gen_line('')
            self.generator.gen_line_block(
                self._description(comment['description']))
            self.generator.gen_line('')

    def _format_attachments(self):
        if 'attachments' not in self.ticket['comment_attachment']:
            return
        attachments = self.ticket['comment_attachment']['attachments']
        if len(attachments) == 0:
            return
        self.generator.gen_heading('Attachments:', heading_base + 2)
        cnt = 0
        tab = []
        bold = self.generator.gen_bold
        for attachment in attachments:
            cnt += 1
            tab += [[
                bold(str(cnt)),
                bold('%s, %s' %
                     (attachment['creator'], attachment['published']))
            ]]
            for m in attachment['meta']:
                tab += [['', bold(self._meta_label(m[0])) + ': ' + m[1]]]
            if len(attachment['description']) != 0:
                tab += [['', attachment['description']]]
        if len(tab) != 0:
            self.generator.gen_line('')
            self.generator.gen_table(None, tab)
            self.generator.gen_line('')

    def _runner(self):
        try:
            self.formatter()
        except KeyboardInterrupt:
            pass
        except:
            self.result = sys.exc_info()

    def formatter(self):
        self._format_contents()
        self._format_description()
        self._format_attachments()
        self._format_comments()

    def ticket_id(self):
        return self.ticket['ticket']['id']

    def run(self):
        self.thread = threading.Thread(target=self._runner,
                                       name='format-ticket-%s' %
                                       (self.ticket_id()))
        self.thread.start()

    def is_alive(self):
        return self.thread and self.thread.is_alive()

    def reraise(self):
        if self.result is not None:
            reraise.reraise(*self.result)


class generator:

    def __init__(self, release, fmt='markdown'):
        if fmt != 'markdown' and fmt != 'trac':
            raise RuntimeError('invalid format: ' + fmt)
        self.release = release
        self.milestone = None
        self.format = fmt
        self.generator = MarkdownGenerator()

    def set_milestone(self, milestone):
        self.milestone = milestone

    def gen_toc(self, notes):
        headings = [line for line in notes
                    if line.startswith('##')] if notes is not None else []
        self.generator.gen_raw(self.md_toc(headings))

    def gen_start(self, notes):
        self.generator.gen_raw('# RTEMS Release ' + self.milestone +
                               os.linesep)
        if notes is not None:
            self.generator.gen_raw(os.linesep.join(notes))
        self.generator.gen_page_break()

    def gen_overall_progress(self, overall_progress):
        self.generator.gen_heading(
            'RTEMS ' + self.milestone + ' Ticket Overview', heading_base)
        self.generator.gen_table(
            [k.capitalize() for k in overall_progress.keys()],
            [overall_progress.values()],
            align='left')

    def gen_tickets_summary(self, tickets):
        self.generator.gen_line_break()
        self.generator.gen_heading(
            'RTEMS ' + self.milestone + ' Ticket Summary', heading_base)
        keys = tickets.keys()
        id_summary_mapping = [
            ('[%s](#t%s)' % (k, k), tickets[k]['meta']['status'],
             tickets[k]['meta']['summary']) for k in keys
        ]
        cols = ['ID', 'Status', 'Summary']
        self.generator.gen_table(cols, id_summary_mapping, sort_by='ID')
        self.generator.gen_line_break()

    @staticmethod
    def _convert_to_bulleted_link(name: str, generator):
        level = name.count('#')
        stripped_name = name.replace('#', '').strip()
        linked_name = name.lower().replace(' ',
                                           '-').replace('-', '', 1).replace(
                                               '#', '', level - 1)
        if not isinstance(generator, MarkdownGenerator):
            linked_name = linked_name.replace('.', '-')

        return f"{('    ' * (level - 1)) + '* '}[{stripped_name}]({linked_name})"

    def md_toc(self, headings):
        tmp_gen = MarkdownGenerator()
        toc_headers = [h[1:] for h in headings]
        toc_headers.extend([
            '# RTEMS ' + self.milestone + ' Ticket Overview',
            '# RTEMS ' + self.milestone + ' Ticket Summary',
            '# RTEMS ' + self.milestone + ' Tickets By Category'
        ])
        toc_headers.append('# RTEMS ' + self.milestone + ' Tickets')
        bulleted_links = []
        for c in toc_headers:
            bulleted_links.append(self._convert_to_bulleted_link(c, tmp_gen))
        for b in bulleted_links:
            tmp_gen.gen_unwrapped_line(b)
        return tmp_gen.content

    def gen_tickets_stats_by_category(self, by_category):
        self.generator.gen_heading('RTEMS ' + self.milestone + \
                                   ' Tickets By Category', heading_base)
        self.generator.gen_line('')

        for category in by_category:
            self.generator.gen_heading(category.capitalize(), heading_base + 1)

            # Get header and all rows to generate table, set category as first col
            header = [category.capitalize()]
            rows = []
            ticket_stats_list = list(by_category[category].values())
            if len(ticket_stats_list) > 0:
                header += [k.capitalize() for k in ticket_stats_list[0].keys()]

            for category_value in by_category[category]:
                ticket_stats = by_category[category][category_value]
                rows.append([category_value] + list(ticket_stats.values()))

            self.generator.gen_table(header, rows)
            self.generator.gen_line('')

    def gen_individual_tickets_info(self, tickets):
        self.generator.gen_line_break()
        self.generator.gen_heading('RTEMS ' + self.milestone + ' Tickets',
                                   heading_base)
        num_jobs = 1
        job_count = 0
        job_total = len(tickets)
        job_len = len(str(job_total))
        for ticket_id in sorted(list(tickets.keys())):
            job = ticket(self.format, tickets[ticket_id])
            job_count += 1
            print('\r %*d of %d - %s ticket %s ' %
                  (job_len, job_count, job_total, self.milestone, ticket_id),
                  end='')
            job.formatter()
            self.generator.gen_horizontal_line()
            self.generator.content += job.generator.content
        print()