Add Bug status table in the detailed report.
authorVincent Tondellier <tonton+hg@team1664.org>
Sun, 04 Dec 2016 01:16:39 +0100
changeset 120 1a0fa98037fa
parent 119 0a9171619fd3
child 121 5a99941ed0ca
Add Bug status table in the detailed report. Initial support for Bugzilla and Redmine
extras/crash_test.conf
lib/CrashTest.pm
lib/CrashTest/Controller/CrashReports.pm
lib/CrashTest/Helper/BugLinks.pm
lib/CrashTest/Model/BugLink.pm
lib/CrashTest/Model/CrashReport.pm
lib/CrashTest/Plugin/BugLink/Bugzilla.pm
lib/CrashTest/Plugin/BugLink/Redmine.pm
lib/CrashTest/Plugin/Storage/Sql/Model/CrashReport.pm
lib/CrashTest/files/templates/report/bugs.html.ep
lib/CrashTest/files/templates/report/crash.html.ep
--- a/extras/crash_test.conf	Sun Nov 27 17:17:43 2016 +0100
+++ b/extras/crash_test.conf	Sun Dec 04 01:16:39 2016 +0100
@@ -54,10 +54,15 @@
     },
     BugTrackerLinks => {
       MyRedmine => {
-        type => 'Redmine',
-        match => qr/\d{1,8}/,
+        Type => 'Redmine',
+        API => { URL => 'https://redmine.example.org/' },
         url_pattern => 'http://redmine.example.org/issues/<%= $bug_key =%>',
       },
+      MyBugzilla => {
+        Type => 'Bugzilla',
+        API => { URL => 'https://bugzilla.example.org/' },
+        url_pattern => 'https://bugzilla.example.org/show_bug.cgi?id=<%= $bug_key =%>',
+      },
     },
     ExtraColumns => {
       Index => [
--- a/lib/CrashTest.pm	Sun Nov 27 17:17:43 2016 +0100
+++ b/lib/CrashTest.pm	Sun Dec 04 01:16:39 2016 +0100
@@ -6,6 +6,7 @@
 use CrashTest::Model::CrashReport;
 use CrashTest::Model::CrashGroup;
 use CrashTest::Model::CrashProcessor;
+use CrashTest::Model::BugLink;
 
 # This method will run once at server start
 sub startup {
@@ -37,13 +38,15 @@
     $self->helper(crash_groups      => sub { state $crash_groups    = CrashTest::Model::CrashGroup->new     (app => $self); });
 
     $self->helper(crash_processor   => sub { state $crash_processor = CrashTest::Model::CrashProcessor->new (app => $self, config => $self->config); });
-    $self->helper(stackfilter       => sub { state $crash_reports   = CrashTest::Model::StackFilter->new    (app => $self, config => $self->config); });
+    $self->helper(stackfilter       => sub { state $stackfilter     = CrashTest::Model::StackFilter->new    (app => $self, config => $self->config); });
     $self->helper(storage           => sub { state $storage         = CrashTest::Model::Storage->new        (app => $self, config => $self->config); });
+    $self->helper(bug_link          => sub { state $bug_link        = CrashTest::Model::BugLink->new        (app => $self, config => $self->config->{WebInterface}->{BugTrackerLinks}); });
 
     $self->plugin('Minion', $self->config->{Processor}->{JobQueue}->{Backend}->{Minion});
 
     $self->storage->load_plugins();
     $self->crash_processor->load_plugins();
+    $self->bug_link->load_plugins();
 
     # Router
     my $r = $self->routes;
--- a/lib/CrashTest/Controller/CrashReports.pm	Sun Nov 27 17:17:43 2016 +0100
+++ b/lib/CrashTest/Controller/CrashReports.pm	Sun Dec 04 01:16:39 2016 +0100
@@ -61,6 +61,14 @@
     }
     $self->stash(threads => \@threads);
 
+    my $report = $self->app->crash_reports->get($uuid);
+    $self->stash(crash => $report);
+
+    my $bugs_status = {};
+    if(defined($report) && defined($report->{bug_links})) {
+        $bugs_status = $self->app->bug_link->get_statuses($report->{bug_links});
+    }
+    $self->stash(bugs_status => $bugs_status);
     my $group = $self->app->crash_groups->get($uuid);
     $self->stash(crash_group => $group);
     $self->stash(extra_columns => $self->app->config->{WebInterface}->{ExtraColumns}->{Index});
--- a/lib/CrashTest/Helper/BugLinks.pm	Sun Nov 27 17:17:43 2016 +0100
+++ b/lib/CrashTest/Helper/BugLinks.pm	Sun Dec 04 01:16:39 2016 +0100
@@ -23,6 +23,7 @@
     $self->app($app);
 
     $app->helper(bug_links => sub { $self->_bug_links(@_) } );
+    $app->helper(bug_status => sub { $self->_bug_status(@_) } );
 }
 
 sub _link_template {
@@ -74,4 +75,34 @@
     return Mojo::ByteStream->new(join ", ", @links);
 }
 
+sub _bug_status {
+    my ($self, $c, $bug, $bugs_status) = @_;
+
+    my $bug_status = $bugs_status->{$bug->{bugtracker_key}}->{$bug->{bug_key}};
+    my $link;
+    my $template = $self->_link_template($bug->{bugtracker_key});
+    if(defined($template)) {
+        # build url using the configured template
+        my $url = $template->process({ bug_key => $bug->{bug_key} });
+        # and create a link to it
+        $link = $self->app->t(tr => sub { join "", (
+            $self->app->t(td => sub { $self->app->link_to($bug->{bug_key} => $url) }),
+            $self->app->t(td => sub { $bug_status->{status} }),
+            $self->app->t(td => sub { $bug_status->{resolution} }),
+            $self->app->t(td => sub { $bug_status->{summary} }),
+            )}
+        );
+    } else {
+        $link = $self->app->t(tr => sub { join "", (
+            $self->app->t(td => $bug->{bug_key}),
+            $self->app->t(td => ""),
+            $self->app->t(td => ""),
+            $self->app->t(td => ""),
+            )}
+        );
+    }
+
+    return Mojo::ByteStream->new($link);
+}
+
 1;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/CrashTest/Model/BugLink.pm	Sun Dec 04 01:16:39 2016 +0100
@@ -0,0 +1,57 @@
+# 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 3 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, see <http://www.gnu.org/licenses/>.
+
+package CrashTest::Model::BugLink;
+use Mojo::Base -base;
+use Mojo::Util qw/dumper/;
+
+has [ qw/app config/ ];
+has bugtrackers => sub { return {} };
+
+sub load_plugins {
+    my $self = shift;
+
+    foreach my $bt_key (keys %{$self->config}) {
+        my $bt_config = $self->config->{$bt_key};
+
+        if(defined($bt_config->{Type})) {
+            $self->app->plugin("CrashTest::Plugin::BugLink::" . $bt_config->{Type}, $bt_config);
+        }
+        $self->bugtrackers->{$bt_key} = "CrashTest::Plugin::BugLink::$bt_config->{Type}"->new(app => $self->app, config => $bt_config);
+    }
+}
+
+sub get_status {
+    my ($self, $bugtracker_key, $bug_key) = @_;
+
+    return $self->bugtrackers->{$bugtracker_key}->get_status($bug_key);
+}
+
+sub get_statuses {
+    my ($self, $bugs) = @_;
+
+    my %bugs_by_tracker;
+    foreach my $bug(@$bugs) {
+        my $tracker = $bug->{bugtracker_key};
+        push @{$bugs_by_tracker{$tracker}}, $bug->{bug_key};
+    }
+
+    my $statuses = {};
+    foreach my $tracker(keys %bugs_by_tracker) {
+        $statuses->{$tracker} = $self->bugtrackers->{$tracker}->get_statuses($bugs_by_tracker{$tracker});
+    }
+
+    return $statuses;
+}
+
+1;
--- a/lib/CrashTest/Model/CrashReport.pm	Sun Nov 27 17:17:43 2016 +0100
+++ b/lib/CrashTest/Model/CrashReport.pm	Sun Dec 04 01:16:39 2016 +0100
@@ -34,6 +34,12 @@
     return $self->app->storage->each("CrashReport::update", $uuid, $pjson, $client_info, $dump);
 }
 
+sub get {
+    my ($self, $uuid) = @_;
+
+    return $self->app->storage->first("CrashReport::get", $uuid);
+}
+
 sub get_processed_data {
     my ($self, $uuid) = @_;
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/CrashTest/Plugin/BugLink/Bugzilla.pm	Sun Dec 04 01:16:39 2016 +0100
@@ -0,0 +1,76 @@
+# 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 3 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, see <http://www.gnu.org/licenses/>.
+
+package CrashTest::Plugin::BugLink::Bugzilla;
+use Mojo::Base 'Mojolicious::Plugin';
+use Mojo::Util qw/dumper/;
+
+use Mojo::UserAgent;
+
+has [ qw/app config/ ];
+has [ qw/_bz_url _ua/ ];
+
+sub register {
+    my ($self, $app, $args) = @_;
+
+    $self->app($app);
+    $self->config($args->{config});
+
+    $app->log->debug("Registered BugLink::Bugzilla");
+
+}
+
+sub get_statuses {
+    my ($self, $bug_keys) = @_;
+
+    $self->_connect() if(!defined($self->_ua));
+
+    my $res = $self->_ua->get($self->_bz_url->path('/rest/bug'), { Accept => 'application/json' },
+      form => {
+        id => join(",", @$bug_keys),
+        include_fields => 'id,summary,status,resolution',
+        api_key => $self->config->{API}->{api_key},
+      }
+    )->res;
+
+    my $statuses = {};
+    foreach my $bug(@{$res->json->{bugs}}) {
+        my $closed = ($bug->{status} =~ /^(?:RESOLVED|VERIFIED)$/);
+
+        $statuses->{$bug->{id}} = {
+            closed => $closed,
+            status => $bug->{status},
+            resolution => $bug->{resolution},
+            summary => $bug->{summary},
+        };
+    }
+    return $statuses;
+}
+
+sub get_status {
+    my ($self, $bug_key) = @_;
+
+    my $statuses = $self->get_statuses([$bug_key]);
+
+    return $statuses->{$bug_key};
+}
+
+sub _connect {
+    my $self = shift;
+
+    $self->_ua(Mojo::UserAgent->new);
+
+    $self->_bz_url(Mojo::URL->new($self->config->{API}->{URL}));
+}
+
+1;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/CrashTest/Plugin/BugLink/Redmine.pm	Sun Dec 04 01:16:39 2016 +0100
@@ -0,0 +1,82 @@
+# 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 3 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, see <http://www.gnu.org/licenses/>.
+
+package CrashTest::Plugin::BugLink::Redmine;
+use Mojo::Base 'Mojolicious::Plugin';
+use Mojo::Util qw/dumper/;
+
+use Mojo::UserAgent;
+
+has [ qw/app config/ ];
+has [ qw/_redmine_url _ua/ ];
+
+sub register {
+    my ($self, $app, $args) = @_;
+
+    $self->app($app);
+    $self->config($args->{config});
+
+    $app->log->debug("Registered BugLink::Redmine");
+
+}
+
+sub get_statuses {
+    my ($self, $bug_keys) = @_;
+
+    my $statuses = {};
+
+    # Seems there is not bulk API in redmine ...
+    # /issues.json does not allow filters on issue #id
+    for my $bug_key(@$bug_keys) {
+        $statuses->{$bug_key} = $self->get_status($bug_key);
+    }
+
+    return $statuses;
+}
+
+sub get_status {
+    my ($self, $bug_key) = @_;
+
+    $self->_connect() if(!defined($self->_ua));
+
+    # Redmine requires the .json extension. We also provide Accept header for consistency
+    my $res = $self->_ua->get($self->_redmine_url->path('/issues/')->path($bug_key . ".json"), { Accept => 'application/json' })->res;
+    my $j = $res->json;
+    my $issue = $j->{issue};
+
+    my $resolution;
+    foreach my $cf(@{$issue->{custom_fields}}) {
+        if($cf->{name} =~ /resolution/i) {
+            $resolution = $cf->{value};
+        }
+    }
+    my $status = $issue->{status}->{name};
+    my $closed = ($status =~ /^(?:Resolved|Closed)$/i);
+
+    return {
+      closed => $closed,
+      status => $status,
+      resolution => $resolution,
+      summary => $issue->{subject},
+    };
+}
+
+sub _connect {
+    my $self = shift;
+
+    $self->_ua(Mojo::UserAgent->new);
+
+    $self->_redmine_url(Mojo::URL->new($self->config->{API}->{URL}));
+}
+
+1;
--- a/lib/CrashTest/Plugin/Storage/Sql/Model/CrashReport.pm	Sun Nov 27 17:17:43 2016 +0100
+++ b/lib/CrashTest/Plugin/Storage/Sql/Model/CrashReport.pm	Sun Dec 04 01:16:39 2016 +0100
@@ -104,6 +104,33 @@
     return ($results, $pager);
 }
 
+sub get {
+    my ($self, $uuid) = @_;
+
+    my @extra_cols;
+    foreach my $extra_col(@{$self->app->config->{WebInterface}->{ExtraColumns}->{Index}}) {
+        push @extra_cols, $extra_col->{db_column} . " AS " . $extra_col->{id};
+    }
+    my $extra_columns = join(",", @extra_cols);
+
+    my $result = $self->db->query("
+        SELECT  crash_reports.*,
+                product.distributor AS p_distributor, product.name AS p_name, product.version AS p_version, product.release_channel AS p_release_channel,
+                crash_user.os AS u_os, crash_user.cpu_arch AS u_cpu_arch, crash_user.cpu_count AS u_cpu_count, crash_user.extra_info AS u_extra_info,
+                (SELECT json_agg(to_json(bug_links)) FROM bug_links WHERE bug_links.crash_group_id = crash_reports.crash_group_id) AS bug_links,
+                $extra_columns
+        FROM crash_reports
+        LEFT JOIN crash_users AS crash_user ON crash_reports.crash_user_id = crash_user.id
+        LEFT JOIN products AS product ON crash_reports.product_id = product.id
+        LEFT JOIN crash_groups AS crash_group ON crash_reports.crash_group_id = crash_group.id
+        WHERE crash_reports.uuid = ?
+        ",
+        $uuid
+    )->expand->hash;
+
+    return $result;
+}
+
 sub get_processed_data {
     my ($self, $uuid) = @_;
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/CrashTest/files/templates/report/bugs.html.ep	Sun Dec 04 01:16:39 2016 +0100
@@ -0,0 +1,14 @@
+% if(defined($bug_links) && scalar(@$bug_links) > 0) {
+  %= t table => (class => 'table table-hover table-bordered table-condensed bug-links') => begin
+    %= t tr => begin
+      %= t th => "Bug"
+      %= t th => "Status"
+      %= t th => "Resolution"
+      %= t th => "Summary"
+    % end
+    % for my $bug(@$bug_links) {
+      %= bug_status($bug, $bugs_status)
+    % }
+  % end
+% }
+
--- a/lib/CrashTest/files/templates/report/crash.html.ep	Sun Nov 27 17:17:43 2016 +0100
+++ b/lib/CrashTest/files/templates/report/crash.html.ep	Sun Dec 04 01:16:39 2016 +0100
@@ -11,6 +11,7 @@
     % if(defined($crash_group)) {
       %= link_to "In crash group \"" . $crash_group->{title} . "\"" => url_for('group', uuid => $crash_group->{uuid})
     % }
+    %= include("report/bugs", bug_links => $crash->{bug_links}, bugs_status => $bugs_status);
    % end
 % end