-
Notifications
You must be signed in to change notification settings - Fork 184
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Bug 913911 - add a webservice method to expose a bug graphs (includin…
…g dependency and duplicates) * Bug 913911 - add a webservice method to expose a bug graphs (including dependency and duplicates) * - No longer require Graph::D3 - Updated query to use 'WITH RECURSIVE' for better DB performance - Added simple rest API test that needs to expand coverage * Removed unneeded comment from test case * - Return both sides of the relationship such as blocked and dependson in the result - Improved test script * Extended test case to cover regressions and duplicates. Also check for private bug handling * Updated docs and also using to_hash provided by Bug.pm instead of rolling our own * Add stricter value checking for SQL values
- Loading branch information
Showing
11 changed files
with
949 additions
and
254 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
# This Source Code Form is subject to the terms of the Mozilla Public | ||
# License, v. 2.0. If a copy of the MPL was not distributed with this | ||
# file, You can obtain one at http://mozilla.org/MPL/2.0/. | ||
# | ||
# This Source Code Form is "Incompatible With Secondary Licenses", as | ||
# defined by the Mozilla Public License, v. 2.0. | ||
|
||
package Bugzilla::API::V1::BugGraph; | ||
|
||
use 5.10.1; | ||
use Mojo::Base qw( Mojolicious::Controller ); | ||
|
||
use List::Util qw(any none); | ||
use PerlX::Maybe; | ||
use Try::Tiny; | ||
|
||
use Bugzilla; | ||
use Bugzilla::Constants; | ||
use Bugzilla::Error; | ||
use Bugzilla::Logging; | ||
use Bugzilla::Report::Graph; | ||
|
||
sub setup_routes { | ||
my ($class, $r) = @_; | ||
$r->get('/bug/:id/graph')->to('V1::BugGraph#graph'); | ||
} | ||
|
||
sub graph { | ||
my ($self, $params) = @_; | ||
my $user = $self->bugzilla->login; | ||
|
||
Bugzilla->usage_mode(USAGE_MODE_MOJO_REST); | ||
|
||
my $bug_id = $self->param('id'); | ||
my $relationship = $self->param('relationship') || 'dependencies'; | ||
my $depth = $self->param('depth') || 3; | ||
my $ids_only = $self->param('ids_only') ? 1 : 0; | ||
my $show_resolved = $self->param('show_resolved') ? 1 : 0; | ||
|
||
if ($bug_id !~ /^\d+$/) { | ||
ThrowCodeError('param_invalid', | ||
{function => 'bug/<id>/graph', param => 'bug_id'}); | ||
} | ||
|
||
if ($depth !~ /^\d+$/ || ($depth > 9 || $depth < 1)) { | ||
ThrowCodeError('param_invalid', | ||
{function => 'bug/<id>/graph', param => 'depth'}); | ||
} | ||
|
||
my %relationships = ( | ||
dependencies => ['dependson,blocked', 'blocked,dependson'], | ||
duplicates => ['dupe,dupe_of', 'dupe_of,dupe'], | ||
regressions => ['regresses,regressed_by', 'regressed_by,regresses'], | ||
); | ||
|
||
if (none { $relationship eq $_ } keys %relationships) { | ||
ThrowCodeError('param_invalid', | ||
{function => 'bug/<id>/graph ', param => 'relationship'}); | ||
} | ||
|
||
my $result = {}; | ||
try { | ||
foreach my $fields (@{$relationships{$relationship}}) { | ||
Bugzilla->switch_to_shadow_db(); | ||
|
||
my ($source, $sink) = split /,/, $fields; | ||
|
||
my $report = Bugzilla::Report::Graph->new( | ||
bug_id => $bug_id, | ||
table => $relationship, | ||
source => $source, | ||
sink => $sink, | ||
depth => $depth, | ||
); | ||
|
||
# Remove any secure bugs that user cannot see | ||
$report->prune_graph(sub { $user->visible_bugs($_[0]) }); | ||
|
||
# If we do not want resolved bugs (default) then filter those | ||
# by passing in reference to the subroutine for filtering out | ||
# resolved bugs | ||
if (!$show_resolved) { | ||
$report->prune_graph(sub { $self->_prune_resolved($_[0]) }); | ||
} | ||
|
||
if (!$ids_only) { | ||
my $bugs = Bugzilla::Bug->new_from_list([$report->graph->vertices]); | ||
foreach my $bug (@$bugs) { | ||
$report->graph->set_vertex_attributes($bug->id, $bug->to_hash); | ||
} | ||
} | ||
|
||
$result->{$source} = $report->tree; | ||
} | ||
} | ||
catch { | ||
FATAL($_); | ||
$result = {exception => 'Internal Error', request_id => $self->req->request_id}; | ||
}; | ||
|
||
return $self->render(json => $result); | ||
} | ||
|
||
# This method takes a set of bugs and using a single SQL statement, | ||
# removes any bugs from the list which have a non-empty resolution (unresolved) | ||
sub _prune_resolved { | ||
my ($self, $bugs) = @_; | ||
my $dbh = Bugzilla->dbh; | ||
|
||
return $bugs if !$bugs->size; | ||
|
||
my $placeholders = join ',', split //, '?' x $bugs->size; | ||
my $query | ||
= "SELECT bug_id FROM bugs WHERE (resolution IS NULL OR resolution = '') AND bug_id IN ($placeholders)"; | ||
my $filtered_bugs | ||
= Bugzilla->dbh->selectcol_arrayref($query, undef, $bugs->elements); | ||
|
||
return $filtered_bugs; | ||
} | ||
|
||
1; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
# This Source Code Form is subject to the terms of the Mozilla Public | ||
# License, v. 2.0. If a copy of the MPL was not distributed with this | ||
# file, You can obtain one at http://mozilla.org/MPL/2.0/. | ||
# | ||
# This Source Code Form is "Incompatible With Secondary Licenses", as | ||
# defined by the Mozilla Public License, v. 2.0. | ||
|
||
package Bugzilla::Report::Graph; | ||
use 5.10.1; | ||
use Moo; | ||
|
||
use Graph::Directed; | ||
use Graph::Traversal::DFS; | ||
use PerlX::Maybe 'maybe'; | ||
use Type::Utils qw(class_type); | ||
use Types::Standard qw(Bool Enum Int Str ArrayRef); | ||
use Set::Object qw(set); | ||
|
||
use Bugzilla; | ||
use Bugzilla::Logging; | ||
use Bugzilla::Types qw(DB); | ||
|
||
our $valid_tables = [qw(dependencies duplicates regressions)]; | ||
our $valid_fields = [qw(blocked dependson dupe dupe_of regresses regressed_by)]; | ||
|
||
has bug_id => (is => 'ro', isa => Int, required => 1); | ||
has table => | ||
(is => 'ro', isa => Enum $valid_tables, default => 'dependencies',); | ||
has depth => (is => 'ro', isa => Int, default => 3); | ||
has source => (is => 'ro', isa => Enum $valid_fields, default => 'dependson',); | ||
has sink => (is => 'ro', isa => Enum $valid_fields, default => 'blocked',); | ||
has limit => (is => 'ro', isa => Int, default => 10_000); | ||
has paths => (is => 'lazy', isa => ArrayRef [ArrayRef]); | ||
has graph => (is => 'lazy', isa => class_type({class => 'Graph'})); | ||
has query => (is => 'lazy', isa => Str); | ||
|
||
# Run the query that will list of the paths from the parent bug | ||
# down to the last child in the tree | ||
sub _build_paths { | ||
my ($self) = @_; | ||
return Bugzilla->dbh->selectall_arrayref($self->query, undef, $self->bug_id); | ||
} | ||
|
||
# Builds a new directed graph | ||
sub _build_graph { | ||
my ($self) = @_; | ||
my $paths = $self->paths; | ||
my $graph = Graph::Directed->new; | ||
|
||
foreach my $path (@$paths) { | ||
pop @$path until defined $path->[-2]; | ||
$graph->add_path(@$path); | ||
} | ||
|
||
return $graph; | ||
} | ||
|
||
sub _build_query { | ||
my ($self) = @_; | ||
my $table = $self->table; | ||
my $alias = substr $table, 0, 1; | ||
my $depth = $self->depth; | ||
my $source = $self->source; | ||
my $sink = $self->sink; | ||
my $limit = $self->limit; | ||
|
||
# WITH RECURSIVE is available in MySQL 8.x and newer as | ||
# well as recent versions of PostgreSQL and SQLite. | ||
my $query = "WITH RECURSIVE RelationshipTree AS ( | ||
SELECT t.$source, t.$sink, 1 AS depth FROM $table t WHERE t.$source = ? | ||
UNION ALL | ||
SELECT t.$source, t.$sink, rt.depth + 1 AS depth FROM $table t | ||
JOIN RelationshipTree rt ON t.$source = rt.$sink WHERE rt.depth <= $depth LIMIT $limit) | ||
SELECT rt.$source, rt.$sink FROM RelationshipTree rt"; | ||
|
||
return $query; | ||
} | ||
|
||
# Using a callback filter being passed in, remove any unwanted vertices | ||
# in the graph such as secure bugs if the user cannot see them. Then | ||
# remove any unreachable vertices as well. | ||
sub prune_graph { | ||
my ($self, $filter) = @_; | ||
|
||
my $all_vertices = set($self->graph->vertices); | ||
my $filtered_vertices = set(@{$filter->($all_vertices)}); | ||
my $pruned_vertices = $all_vertices - $filtered_vertices; | ||
$self->graph->delete_vertices($pruned_vertices->members); | ||
|
||
# Finally remove any vertices that are now unreachable | ||
my $reachable_vertices | ||
= set($self->bug_id, $self->graph->all_reachable($self->bug_id)); | ||
my $unreachable_vertices = $filtered_vertices - $reachable_vertices; | ||
$self->graph->delete_vertices($unreachable_vertices->members); | ||
|
||
return $pruned_vertices + $unreachable_vertices; | ||
} | ||
|
||
# Generates the final tree stucture based on the directed graph | ||
sub tree { | ||
my ($self) = @_; | ||
my $graph = $self->graph; | ||
|
||
my %nodes = map { $_ => {maybe bug => $graph->get_vertex_attributes($_)} } | ||
$graph->vertices; | ||
|
||
my $search = Graph::Traversal::DFS->new( | ||
$graph, | ||
start => $self->bug_id, | ||
tree_edge => sub { | ||
my ($u, $v) = @_; | ||
$nodes{$u}{$v} = $nodes{$v}; | ||
} | ||
); | ||
$search->dfs; | ||
|
||
return $nodes{$self->bug_id} || {}; | ||
} | ||
|
||
|
||
1; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.