# -*- Mode: perl; indent-tabs-mode: nil -*- # # The contents of this file are subject to the Mozilla Public # License Version 1.1 (the "License"); you may not use this file # except in compliance with the License. You may obtain a copy of # the License at http://www.mozilla.org/MPL/ # # Software distributed under the License is distributed on an "AS # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or # implied. See the License for the specific language governing # rights and limitations under the License. # # The Original Code is the Bugzilla Test Runner System. # # The Initial Developer of the Original Code is Maciej Maczynski. # Portions created by Maciej Maczynski are Copyright (C) 2001 # Maciej Maczynski. All Rights Reserved. # # Portions taken from processbug.cgi and Bug.pm # which are copyrighted by # Terry Weissman # Dan Mosedale # Dave Miller # Christopher Aillon # Myk Melez # Jeff Hedlund # Frederic Buclin # # Contributor(s): Greg Hendricks # Jeff Dayley # M-A Parent package Bugzilla::Extension::Testopia::TestCase; use strict; use Bugzilla::Util; use Bugzilla::Bug; use Bugzilla::User; use Bugzilla::Config; use Bugzilla::Error; use Bugzilla::Constants; use Bugzilla::Extension::Testopia::Constants; use Bugzilla::Extension::Testopia::Util; use Bugzilla::Extension::Testopia::TestPlan; use Bugzilla::Extension::Testopia::TestRun; use Bugzilla::Extension::Testopia::TestCaseRun; use Bugzilla::Extension::Testopia::Category; use Bugzilla::Extension::Testopia::Attachment; use JSON; use Text::Diff; use base qw(Exporter Bugzilla::Object); our @EXPORT = qw(lookup_status lookup_status_by_name lookup_category lookup_category_by_name lookup_priority lookup_priority_by_value lookup_default_tester); ############################### #### Initialization #### ############################### use constant DB_TABLE => "test_cases"; use constant NAME_FIELD => "alias"; use constant ID_FIELD => "case_id"; use constant DB_COLUMNS => qw( case_id case_status_id category_id priority_id author_id default_tester_id creation_date estimated_time isautomated sortkey script arguments summary requirement alias ); use constant REQUIRED_CREATE_FIELDS => qw(case_status_id category_id priority_id author_id summary plans); use constant UPDATE_COLUMNS => qw(case_status_id category_id priority_id default_tester_id isautomated sortkey script arguments summary requirement alias estimated_time dependson blocks runs tags components); use constant VALIDATORS => { case_status_id => \&_check_status, priority_id => \&_check_priority, default_tester_id => \&_check_tester, author_id => \&_check_author, isautomated => \&_check_automated, sortkey => \&_check_sortkey, script => \&_check_script, arguments => \&_check_arguments, summary => \&_check_summary, requirement => \&_check_requirements, alias => \&_check_alias, estimated_time => \&_check_time, dependson => \&_check_dependency, blocked => \&_check_dependency, plans => \&_check_plans, runs => \&_check_runs, tags => \&_check_tags, components => \&_check_components, bugs => \&_check_bugs, }; use constant ALIAS_MAX_LENGTH => 255; use constant REQUIREMENT_MAX_LENGTH => 255; use constant SUMMARY_MAX_LENGTH => 255; use constant TAG_MAX_LENGTH => 255; sub display_columns { my $self = shift; my @columns = [{column => 'case_id', desc => 'ID' }, {column => 'case_status_id', desc => 'Status' }, {column => 'category_id', desc => 'Category' }, {column => 'priority_id', desc => 'Priority' }, {column => 'summary', desc => 'Summary' }, {column => 'requirement', desc => 'Requirement' }, {column => 'alias', desc => 'Alias' }]; $self->{'display_columns'} = \@columns; return $self->{'display_columns'}; } sub report_columns { my $self = shift; my %columns; # Changes here need to match Report.pm $columns{'Status'} = "case_status"; $columns{'Priority'} = "priority"; $columns{'Product'} = "product"; $columns{'Component'} = "component"; $columns{'Category'} = "category"; $columns{'Automated'} = "isautomated"; $columns{'Tags'} = "tags"; $columns{'Requirement'} = "requirement"; $columns{'Author'} = "author"; $columns{'Default tester'} = "default_tester"; my @result; push @result, {'name' => $_, 'id' => $columns{$_}} foreach (sort(keys %columns)); unshift @result, {'name' => '', 'id'=> ''}; return \@result; } ############################### #### Validators #### ############################### sub _check_status{ my ($invocant, $status) = @_; $status = trim($status); my $status_id; if ($status =~ /^\d+$/){ $status_id = Bugzilla::Extension::Testopia::Util::validate_selection($status, 'case_status_id', 'test_case_status'); } else { trick_taint($status); $status_id = lookup_status_by_name($status); } ThrowUserError('invalid_status') unless $status_id; return $status_id; } sub _check_category{ my ($invocant, $category, $product) = @_; $category = trim($category); my $category_id; if (ref $category){ $product = Bugzilla::Product->check($category->{'product'}); $category_id = Bugzilla::Extension::Testopia::Category::check_case_category($category->{'category'}, $product); } elsif ($category =~ /^\d+$/){ $category_id = Bugzilla::Extension::Testopia::Util::validate_selection($category, 'category_id', 'test_case_categories'); } else { $category_id = Bugzilla::Extension::Testopia::Category::check_case_category($category, $product); } return $category_id; } sub _check_priority{ my ($invocant, $priority) = @_; $priority = trim($priority); trick_taint($priority); my $priority_id; if ($priority =~ /^\d+$/){ $priority_id = Bugzilla::Extension::Testopia::Util::validate_selection($priority, 'id', 'priority'); } else { $priority_id = lookup_priority_by_value($priority); } ThrowCodeError('bad_arg', {argument => 'priority', function => 'set_priority'}) unless $priority_id; return $priority_id; } sub _check_tester{ my ($invocant, $tester) = @_; $tester = trim($tester); return unless $tester; if ($tester =~ /^\d+$/){ $tester = Bugzilla::User->new($tester); return $tester->id; } else { my $id = login_to_id($tester, THROW_ERROR); return $id; } } sub _check_author{ my ($invocant, $tester) = @_; $tester = trim($tester); return unless $tester; if ($tester =~ /^\d+$/){ $tester = Bugzilla::User->new($tester); return $tester->id; } else { my $id = login_to_id($tester, THROW_ERROR); return $id; } } sub _check_automated{ my ($invocant, $isactive) = @_; $isactive = trim($isactive); ThrowCodeError('bad_arg', {argument => 'isautomated', function => 'set_automated'}) unless ($isactive =~ /(1|0)/); return $isactive; } sub _check_sortkey{ my ($invocant, $sortkey) = @_; $sortkey = trim($sortkey); return unless $sortkey; ThrowCodeError('bad_arg', {argument => 'sortkey', function => 'set_sortkey'}) unless ($sortkey =~ /^\d+$/); return $sortkey; } sub _check_script{ my ($invocant, $value) = @_; return $value; } sub _check_arguments{ my ($invocant, $value) = @_; return $value; } sub _check_summary{ my ($invocant, $summary) = @_; $summary = clean_text($summary) if $summary; if (!defined $summary || $summary eq '') { ThrowUserError('testopia-missing-required-field', {'field' => 'summary'}); } return $summary; } sub _check_requirements{ my ($invocant, $value) = @_; return $value; } sub _check_alias { my ($invocant, $alias) = @_; $alias = trim($alias); return unless $alias; trick_taint($alias); my $id; my $dbh = Bugzilla->dbh; my $query = "SELECT case_id FROM test_cases WHERE alias = ?"; # If this is a new test case then we only need to check if another test case # has this alias already. If we are updating the test case though, we know # there is already at least one that has this alias - this case. $query .= " AND case_id != ?" if ref $invocant; if (ref $invocant){ ($id) = $dbh->selectrow_array($query, undef, ($alias, $invocant->id)); } else { ($id) = $dbh->selectrow_array($query, undef, $alias); } ThrowUserError('testiopia-alias-exists', {'alias' => $alias}) if $id; return $alias; } sub _check_time{ my ($invocant, $time) = @_; $time = trim($time); return '0:0:0' unless $time; $time =~ m/^(\d+)[:\s](\d+)[:\s](\d+)$/; ThrowUserError('testopia-format-error', {'field' => 'Estimated Time' }) unless (defined $1 && defined $2 && $2 < 60 && defined $3 && $3 < 60); $time = "$1:$2:$3"; return $time; } sub _check_dependency{ my ($invocant, $value) = @_; $value = trim($value); if ($value) { my @validvalues; foreach my $id (split(/[\s,]+/, $value)) { next unless $id; Bugzilla::Extension::Testopia::Util::validate_test_id($id, 'case'); push(@validvalues, $id); } $value = join(",", @validvalues); return $value; } return; } sub _check_plans { my ($invocant, $plans) = @_; # $plans is a reference to an array of Bugzilla::Extension::Testopia::TestPlan objects. ThrowUserError('plan_needed') unless scalar @$plans > 0; return $plans; } sub _check_cases { my ($invocant, $caseids) = @_; my @cases; foreach my $caseid (split(/[\s,]+/, $caseids)){ Bugzilla::Extension::Testopia::Util::validate_test_id($caseid, 'case'); push @cases, Bugzilla::Extension::Testopia::TestCase->new($caseid); } return \@cases; } sub _check_runs { my ($invocant, $runids) = @_; my @runs; if (ref $runids eq 'ARRAY'){ $runids = join(',' ,@$runids); } foreach my $runid (split(/[\s,]+/, $runids)){ Bugzilla::Extension::Testopia::Util::validate_test_id($runid, 'run'); push @runs, Bugzilla::Extension::Testopia::TestRun->new($runid); } return \@runs; } sub _check_tags { my ($invocant, $tags) = @_; return $tags; } sub _check_components { my ($invocant, $components) = @_; my @components; my @comp_ids; my $dbh = Bugzilla->dbh; ThrowUserError('testopia-missing-parameter', {param => 'components'}) unless $components; if (ref $components eq 'HASH'){ my $prod = Bugzilla::Product->check($components->{'product'}); my $comp = Bugzilla::Component->check({product=> $prod, name => $components->{'component'}}); push @comp_ids, $comp->id; } elsif (ref $components eq 'ARRAY'){ foreach my $c (@$components){ if (ref $c){ my $prod = Bugzilla::Product->check($c->{'product'}); my $comp = Bugzilla::Component->check({product => $prod, name => $c->{'component'}}); push @comp_ids, $comp->id; } else{ validate_selection($c,'id','components'); push @comp_ids, $c; } } } else { @comp_ids = split(/[\s,]+/, $components); } foreach my $id (@comp_ids){ Bugzilla::Extension::Testopia::Util::validate_selection($id, 'id', 'components'); trick_taint($id); if (ref $invocant){ my ($is) = $dbh->selectrow_array( "SELECT case_id FROM test_case_components WHERE case_id = ? AND component_id = ?", undef, ($invocant->id, $id)); ThrowUserError('testopia_component_attached') if $is; } push @components, $id; } return \@components; } sub _check_bugs { my ($invocant, $bugids, $attach) = @_; my @bugids; my @ids; my $dbh = Bugzilla->dbh; if (ref $bugids eq 'ARRAY'){ push @ids, @$bugids; } else { push @ids, split(/[\s,]+/, $bugids); } foreach my $bug (@ids){ trick_taint($bug); Bugzilla::Bug->check($bug); if (ref $invocant && $attach){ my ($exists) = $dbh->selectrow_array( "SELECT bug_id FROM test_case_bugs WHERE case_id=? AND bug_id=?", undef, ($invocant->id, $bug)); next if ($exists); } push @bugids, $bug; } return \@bugids; } ############################### #### Mutators #### ############################### sub set_case_status { $_[0]->set('case_status_id', $_[1]); } sub set_priority { $_[0]->set('priority_id', $_[1]); } sub set_default_tester { $_[0]->set('default_tester_id', $_[1]); } sub set_sortkey { $_[0]->set('sortkey', $_[1]); } sub set_requirement { $_[0]->set('requirement', $_[1]); } sub set_isautomated { $_[0]->set('isautomated', $_[1]); } sub set_script { $_[0]->set('script', $_[1]); } sub set_arguments { $_[0]->set('arguments', $_[1]); } sub set_summary { $_[0]->set('summary', $_[1]); } sub set_alias { $_[0]->set('alias', $_[1]); } sub set_estimated_time { $_[0]->set('estimated_time', $_[1]); } sub set_dependson { $_[0]->set('dependson', $_[1]); } sub set_blocks { $_[0]->set('blocks', $_[1]); } sub set_category { my ($self, $value) = @_; $value = $self->_check_category($value, $self->plans->[0]->product); $self->set('category_id', $value); } sub new { my $invocant = shift; my $class = ref($invocant) || $invocant; my $param = shift; # We want to be able to supply an empty object to the templates for numerous # lists etc. This is much cleaner than exporting a bunch of subroutines and # adding them to $vars one by one. Probably just Laziness shining through. if (ref $param eq 'HASH' && !keys %$param){ bless($param, $class); return $param; } if (!defined $param || (!ref($param) && $param !~ /^\d+$/)) { $param = { name => $param }; } unshift @_, $param; my $self = $class->SUPER::new(@_); return $self; } sub run_create_validators { my $class = shift; my $params = $class->SUPER::run_create_validators(@_); my $product = $params->{plans}->[0]->product; $params->{category_id} = $class->_check_category($params->{category_id}, $product); return $params; } sub run_import_validators { my $class = shift; my $params = $class->SUPER::run_create_validators(@_); return $params; } sub create { my ($class, $params) = @_; $class->SUPER::check_required_create_fields($params); my $field_values = $class->run_create_validators($params); $field_values->{creation_date} = Bugzilla::Extension::Testopia::Util::get_time_stamp(); # We have to handle these fields a bit differently since they have their own tables. my $action = $field_values->{action}; my $effect = $field_values->{effect}; my $setup = $field_values->{setup}; my $breakdown = $field_values->{breakdown}; my $dependson = $field_values->{dependson}; my $blocks = $field_values->{blocks}; my $plans = $field_values->{plans}; my $runs = $field_values->{runs}; my $component = $field_values->{components}; my $tags = $field_values->{tags}; my $bugs = $field_values->{bugs}; # Since these are not part of a case strictly speaking, we remove them before # calling insert_create_data. foreach my $field (qw(action effect setup breakdown dependson blocks plans runs components tags bugs)){ delete $field_values->{$field}; } my $self = $class->SUPER::insert_create_data($field_values); $self->store_text($self->id, $field_values->{'author_id'}, $action, $effect, $setup, $breakdown,0); $self->update_deps($dependson, $blocks, $self->id); foreach my $p (@$plans){ $self->link_plan($p->id, $self->id); } delete $self->{'plans'}; foreach my $run (@$runs){ $run->add_case_run($self->id, $self->sortkey); } $self->add_component($component, "VALIDATED"); $self->add_tag($tags); $self->attach_bug($bugs); return $self; } sub update { my $self = shift; my $dbh = Bugzilla->dbh; my $timestamp = Bugzilla::Extension::Testopia::Util::get_time_stamp(); $self->update_deps($self->{'dependson'}, $self->{'blocks'}); $dbh->bz_start_transaction(); my $changed = $self->SUPER::update(); delete $changed->{'sortkey'}; foreach my $field (keys %$changed){ Bugzilla::Extension::Testopia::Util::log_activity('case', $self->id, $field, $timestamp, $changed->{$field}->[0], $changed->{$field}->[1]); } $dbh->bz_commit_transaction(); } ############################### #### Functions #### ############################### sub lookup_status { my ($id) = @_; my $dbh = Bugzilla->dbh; detaint_natural($id); my ($value) = $dbh->selectrow_array( "SELECT name FROM test_case_status WHERE case_status_id = ?", undef, $id); return $value; } sub lookup_status_by_name { my ($name) = @_; trick_taint($name); my $dbh = Bugzilla->dbh; my ($value) = $dbh->selectrow_array( "SELECT case_status_id FROM test_case_status WHERE name = ?", undef, $name); return $value; } sub lookup_category { my ($id) = @_; my $dbh = Bugzilla->dbh; detaint_natural($id); my ($value) = $dbh->selectrow_array( "SELECT name FROM test_case_categories WHERE category_id = ?", undef, $id); return $value; } sub lookup_category_by_name { my ($name) = @_; trick_taint($name); my $dbh = Bugzilla->dbh; my ($value) = $dbh->selectrow_array( "SELECT category_id FROM test_case_categories WHERE name = ?", undef, $name); return $value; } sub lookup_priority { my ($id) = @_; my $dbh = Bugzilla->dbh; detaint_natural($id); my ($value) = $dbh->selectrow_array( "SELECT value FROM priority WHERE id = ?", undef, $id); return $value; } sub lookup_priority_by_value { my ($value) = @_; my $dbh = Bugzilla->dbh; trick_taint($value); my ($id) = $dbh->selectrow_array( "SELECT id FROM priority WHERE value = ?", undef, $value); return $id; } sub lookup_default_tester { my ($id) = @_; detaint_natural($id) or return ''; my $dbh = Bugzilla->dbh; my ($value) = $dbh->selectrow_array( "SELECT login_name FROM profiles WHERE userid = ?", undef, $id); return $value; } ############################### #### Methods #### ############################### sub get_selectable_components { my $self = shift; my ($byid) = @_; my $dbh = Bugzilla->dbh; my @exclusions; unless ($byid) { foreach my $e (@{$self->components}){ push @exclusions, $e->{'id'}; } } my $query = "SELECT id FROM components WHERE product_id IN (" . join(",", @{$self->get_product_ids}) . ") "; if (@exclusions){ $query .= "AND id NOT IN(". join(",", @exclusions) .") "; } $query .= "ORDER BY name"; my $ref = $dbh->selectcol_arrayref($query); my @comps; push @comps, {'id' => '0', 'name' => '--Please Select--'} unless $byid; foreach my $id (@$ref){ push @comps, Bugzilla::Component->new($id); } return \@comps; } =head2 get_category_list Returns a list of categories associated with products in all plans referenced by this case. =cut sub get_category_list{ my $self = shift; my $dbh = Bugzilla->dbh; my $ids = $dbh->selectcol_arrayref( "SELECT category_id FROM test_case_categories WHERE product_id IN (". join(",", @{$self->get_product_ids}) .")"); my @categories; foreach my $c (@$ids){ push @categories, Bugzilla::Extension::Testopia::Category->new($c); } return \@categories; } =head2 get_product_ids Returns the list of product ids that this case is associated with =cut sub get_product_ids { my $self = shift; if ($self->id == 0){ my @ids; foreach my $plan (@{$self->plans}){ push @ids, $plan->product_id if Bugzilla->user->can_see_product($plan->product->name); } return \@ids; } my $dbh = Bugzilla->dbh; my $ref = $dbh->selectcol_arrayref( "SELECT DISTINCT products.id FROM products JOIN test_plans AS plans ON plans.product_id = products.id JOIN test_case_plans ON plans.plan_id = test_case_plans.plan_id JOIN test_cases AS cases ON cases.case_id = test_case_plans.case_id WHERE cases.case_id = ? ORDER BY products.id", undef, $self->{'case_id'}); return $ref; } =head2 get_status_list Returns the list of legal statuses for a test case =cut sub get_status_list { my $self = shift; my $dbh = Bugzilla->dbh; my $ref = $dbh->selectall_arrayref(" SELECT case_status_id AS id, name FROM test_case_status", {"Slice"=>{}}); return $ref } =head2 get_text_versions Returns the list of versions of the plan document. =cut sub get_text_versions { my $self = shift; my $dbh = Bugzilla->dbh; my $versions = $dbh->selectall_arrayref( "SELECT case_text_version AS id, case_text_version AS name FROM test_case_texts WHERE case_id = ? ORDER BY case_text_version", {'Slice' =>{}}, $self->id); return $versions; } =head2 get_priority_list Returns a list of legal priorities =cut sub get_priority_list { my $self = shift; my $dbh = Bugzilla->dbh; my $ref = $dbh->selectall_arrayref(" SELECT id, value AS name FROM priority ORDER BY sortkey", {"Slice"=>{}}); return $ref } =head2 get_caserun_count Takes a status and returns the count of that status =cut sub get_caserun_count { my $self = shift; my ($status) = @_; my $dbh = Bugzilla->dbh; my $query = "SELECT COUNT(*) FROM test_case_runs WHERE case_id = ? "; $query .= "AND case_run_status_id = ?" if $status; my $count; if ($status){ ($count) = $dbh->selectrow_array($query,undef,($self->id,$status)); } else { ($count) = $dbh->selectrow_array($query,undef,$self->id); } return $count; } =head2 add_tag Associates a tag with this test case =cut sub add_tag { my $self = shift; my $dbh = Bugzilla->dbh; my @tags; foreach my $t (@_){ if (ref $t eq 'ARRAY'){ push @tags, $_ foreach @$t; } else { push @tags, split(/,+/, $t); } } foreach my $name (@tags){ my $tag = Bugzilla::Extension::Testopia::TestTag->create({'tag_name' => $name}); $tag->attach($self); } } =head2 remove_tag Disassociates a tag from this test case =cut sub remove_tag { my $self = shift; my ($tag_name) = @_; my $tag = Bugzilla::Extension::Testopia::TestTag->check_tag($tag_name); ThrowUserError('testopia-unknown-tag', {'name' => $tag}) unless $tag; my $dbh = Bugzilla->dbh; $dbh->do("DELETE FROM test_case_tags WHERE tag_id=? AND case_id=?", undef, $tag->id, $self->{'case_id'}); return; } =head2 attach_bug Attaches the specified bug to this test case =cut sub attach_bug { my $self = shift; my ($bugids, $caserun_id) = @_; my $dbh = Bugzilla->dbh; trick_taint($caserun_id) if $caserun_id; $bugids = $self->_check_bugs($bugids, "ATTACH"); $dbh->bz_start_transaction(); foreach my $bug (@$bugids){ $dbh->do("INSERT INTO test_case_bugs (bug_id, case_run_id, case_id) VALUES(?,?,?)", undef, ($bug, $caserun_id, $self->id)); } $dbh->bz_commit_transaction(); } =head2 detach_bug Removes the association of the specified bug from this test case-run =cut sub detach_bug { my $self = shift; my ($bugids) = @_; my $dbh = Bugzilla->dbh; $bugids = $self->_check_bugs($bugids); foreach my $bug (@$bugids){ $dbh->do("DELETE FROM test_case_bugs WHERE bug_id = ? AND case_id = ?", undef, ($bug, $self->{'case_id'})); } } =head2 add_component Associates a component with this test case =cut sub add_component { my $self = shift; my ($compids, $validated) = @_; my $dbh = Bugzilla->dbh; my $comps = $validated ? $compids : $self->_check_components($compids); foreach my $id (@$comps){ $dbh->do("INSERT INTO test_case_components (case_id, component_id) VALUES (?,?)",undef, $self->{'case_id'}, $id); } delete $self->{'components'}; } sub add_to_run { my $self = shift; my ($runids) = @_; my $runs = $self->_check_runs($runids); foreach my $run (@$runs){ $run->add_case_run($self->id, $self->sortkey); } } =head2 add_blocks Adds a list of test cases to the current test cases being blocked by the testcase =cut sub add_blocks { my $self = shift; my $case_ids = shift; my $cases = $self->_check_cases($case_ids); my @blocks; my $dbh = Bugzilla->dbh(); # Get a list of the cases this test case currently blocks my $current_blocks = $dbh->selectcol_arrayref("SELECT blocked FROM test_case_dependencies WHERE dependson = ?", undef, $self->id()); # update the list of items foreach (@$cases) { push @blocks, $_->id(); } push @blocks, @$current_blocks; $cases = join ",", @blocks; $self->set_blocks($cases); $self->update(); } =head2 remove_blocks Removes a list of test cases from being blocked by the testcase =cut sub remove_blocks { my $self = shift; my $case_ids = shift; return 0 if ($case_ids eq '' or !defined $case_ids); my $dbh = Bugzilla->dbh(); my @cases; foreach my $case (split /[\s,]+/, $case_ids){ detaint_natural($case); push @cases, $case; } my $query ="DELETE FROM test_case_dependencies WHERE dependson = ?"; $query .= "AND ("; $query .= join(" or ", map {"blocked = ?"} @cases); $query .= ")"; $dbh->do($query, undef, $self->id, @cases); } =head2 add_dependson Adds a list of test cases to the current test cases depended on by the testcase =cut sub add_dependson { my $self = shift; my $case_ids = shift; my $cases = $self->_check_cases($case_ids); my @dependson; my $dbh = Bugzilla->dbh(); # Get a list of the cases this test case currently blocks my $current_dependson = $dbh->selectcol_arrayref("SELECT dependson FROM test_case_dependencies WHERE blocked = ?", undef, $self->id()); # update the list of items foreach (@$cases) { push @dependson, $_->id(); } push @dependson, @$current_dependson; $cases = join ",", @dependson; $self->set_dependson($cases); $self->update(); } =head2 remove_dependson Adds a list of test cases to the current test cases depended on by the testcase =cut sub remove_dependson { my $self = shift; my $case_ids = shift; return 0 if ($case_ids eq '' or !defined $case_ids); my $dbh = Bugzilla->dbh(); my @cases; foreach my $case (split /[\s,]+/, $case_ids) { detaint_natural($case); push @cases, $case; } my $query = " DELETE FROM test_case_dependencies WHERE blocked = ?"; $query .= "AND ("; $query .= join(" or ", map {"dependson = ?"} @cases); $query .= ")"; $dbh->do($query, undef, $self->id, @cases); } =head2 remove_component Disassociates a component with this test case =cut sub remove_component { my $self = shift; my ($comp_id) = @_; trick_taint($comp_id); my $dbh = Bugzilla->dbh; $dbh->do("DELETE FROM test_case_components WHERE case_id = ? AND component_id = ?", undef, $self->{'case_id'}, $comp_id); } =head2 compare_doc_versions Returns a unified diff of two versions of a case document (action and effect). It takes two arguments both integers representing the first and second versions to compare. =cut sub compare_doc_versions { my $self = shift; my ($newversion, $oldversion) = @_; detaint_natural($newversion); detaint_natural($oldversion); my $dbh = Bugzilla->dbh; my %diff; my ($newaction, $neweffect, $newsetup, $newbreakdown) = $dbh->selectrow_array( "SELECT action, effect, setup, breakdown FROM test_case_texts WHERE case_id = ? AND case_text_version = ?", undef, $self->{'case_id'}, $newversion); my ($oldaction, $oldeffect, $oldsetup, $oldbreakdown) = $dbh->selectrow_array( "SELECT action, effect, setup, breakdown FROM test_case_texts WHERE case_id = ? AND case_text_version = ?", undef, $self->{'case_id'}, $oldversion); $diff{'action'} = diff(\$newaction, \$oldaction); $diff{'effect'} = diff(\$neweffect, \$oldeffect); $diff{'setup'} = diff(\$newsetup, \$oldsetup); $diff{'breakdown'} = diff(\$newbreakdown, \$oldbreakdown); return \%diff; } =head2 diff_case_doc Returns the diff of the latest case document (action and effect) with some text passed as an argument. Used to determine if the text has changed and thus requiring a new version be created. =cut sub diff_case_doc { my $self = shift; my ($newaction, $neweffect, $newsetup, $newbreakdown) = @_; my $dbh = Bugzilla->dbh; my ($oldaction, $oldeffect, $oldsetup, $oldbreakdown) = $dbh->selectrow_array( "SELECT action, effect, setup, breakdown FROM test_case_texts WHERE case_id = ? AND case_text_version = ?", undef, ($self->{'case_id'}, $self->version)); my $diff = diff(\$newaction, \$oldaction); $diff .= diff(\$neweffect, \$oldeffect); $diff .= diff(\$newsetup, \$oldsetup); $diff .= diff(\$newbreakdown, \$oldbreakdown); return $diff } =head2 get_fields Returns a reference to a list of test case field descriptions from the test_fielddefs table. =cut sub get_fields { my $self = shift; my $dbh = Bugzilla->dbh; my $types = $dbh->selectall_arrayref( "SELECT fieldid AS id, description AS name FROM test_fielddefs WHERE table_name=?", {"Slice"=>{}}, "test_cases"); unshift @$types, {id => 'text', name => 'Text (Action, Setup, etc.)'}; unshift @$types, {id => '[Creation]', name => '[Created]'}; return $types; } =head2 store Stores a test case object in the database. This method is used to store a newly created test case. It returns the new ID. =cut sub store { my $self = shift; my $dbh = Bugzilla->dbh; # Exclude the auto-incremented field from the column list. my $columns = join(", ", grep {$_ ne 'case_id'} DB_COLUMNS); my ($timestamp) = Bugzilla::Extension::Testopia::Util::get_time_stamp(); $dbh->do("INSERT INTO test_cases ($columns) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)", undef, ## Database Column ## ($self->{'case_status_id'}, # case_status_id $self->{'category_id'}, # category_id $self->{'priority_id'}, # priority_id $self->{'author_id'}, # author_id $self->{'default_tester_id'}, # default_tester_id $timestamp, # creation_date $self->{'estimated_time'}, # estimated_time $self->{'isautomated'}, # isautomated $self->sortkey, # sortkey $self->{'script'}, # script $self->{'arguments'}, # arguments $self->{'summary'}, # summary $self->{'requirement'}, # requirement $self->{'alias'}, # alias )); my $key = $dbh->bz_last_key( 'test_cases', 'case_id' ); $self->store_text($key, $self->{'author_id'}, $self->{'action'}, $self->{'effect'}, $self->{'setup'}, $self->{'breakdown'},0 ,$timestamp); $self->update_deps($self->{'dependson'}, $self->{'blocks'}, $key); foreach my $p (@{$self->{'plans'}}){ $self->link_plan($p->id, $key); } return $key; } =head2 store_text Stores the test case document (action and effect) in the test_case_texts table. Used by both store and copy. Accepts the the test case id, author id, action text, effect text, and an optional timestamp. =cut sub store_text { my $self = shift; my $dbh = Bugzilla->dbh; my ($key, $author, $action, $effect, $setup, $breakdown, $reset_version, $timestamp) = @_; if (!defined $timestamp){ ($timestamp) = Bugzilla::Extension::Testopia::Util::get_time_stamp(); } trick_taint($action) if $action; trick_taint($effect) if $effect; trick_taint($breakdown) if $breakdown; trick_taint($setup) if $setup; detaint_natural($key); my $version = $reset_version ? 0 : $self->version || 0; $dbh->do("INSERT INTO test_case_texts (case_id, case_text_version, who, creation_ts, action, effect, setup, breakdown) VALUES(?,?,?,?,?,?,?,?)", undef, $key, ++$version, $author, $timestamp, $action, $effect, $setup, $breakdown); $self->{'version'} = $version; return $self->{'version'}; } =head2 link_plan Creates a link to the specified plan id. Optionally can create a link for an arbitrary test case, not just this one. =cut sub link_plan { my $self = shift; my $dbh = Bugzilla->dbh; my ($plan_id, $case_id) = @_; $case_id = $self->{'case_id'} unless defined $case_id; #Check that it isn't linked already $dbh->bz_start_transaction(); my ($is) = $dbh->selectrow_array( "SELECT 1 FROM test_case_plans WHERE case_id = ? AND plan_id = ?", undef, ($case_id, $plan_id)); if ($is) { $dbh->bz_commit_transaction(); return; } $dbh->do("INSERT INTO test_case_plans (plan_id, case_id) VALUES (?,?)", undef, $plan_id, $case_id); $dbh->bz_commit_transaction(); # Update the plans array to include new plan added. push @{$self->{'plans'}}, Bugzilla::Extension::Testopia::TestPlan->new($plan_id); } =head2 unlink_plan Removes the link to the specified plan id. =cut sub unlink_plan { my $self = shift; my $dbh = Bugzilla->dbh; my ($plan_id) = @_; detaint_natural($plan_id); my $plan = Bugzilla::Extension::Testopia::TestPlan->new($plan_id); if (scalar @{$self->plans} == 1){ $self->obliterate; } else { $dbh->bz_start_transaction(); foreach my $run (@{$plan->test_runs}){ $dbh->do("DELETE FROM test_case_runs WHERE case_id = ? AND run_id = ?", undef, $self->id, $run->id); } $dbh->do("DELETE FROM test_case_plans WHERE plan_id = ? AND case_id = ?", undef, $plan_id, $self->{'case_id'}); $dbh->bz_commit_transaction(); } # Update the plans array. delete $self->{'plans'}; return 1; } =head2 copy Creates a copy of this test case. Accepts the plan id to link to and a boolean representing whether to copy the case document as well. =cut sub copy { my $self = shift; my $dbh = Bugzilla->dbh; my ($author, $tester, $copydoc, $category_id) = @_; # Exclude the auto-incremented field from the column list. my $columns = join(", ", grep {$_ ne 'case_id'} DB_COLUMNS); my ($timestamp) = Bugzilla::Extension::Testopia::Util::get_time_stamp(); $category_id ||= $self->{'category_id'}; $dbh->do("INSERT INTO test_cases ($columns) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)", undef, ($self->{'case_status_id'}, # case_status_id $category_id, # category_id $self->{'priority_id'}, # priority_id $author, # author_id $tester, # default_tester_id $timestamp, # creation_date $self->{'estimated_time'}, # estimated_time $self->{'isautomated'}, # isautomated $self->sortkey, # sortkey $self->{'script'}, # script $self->{'arguments'}, # arguments $self->{'summary'}, # summary $self->{'requirement'}, # requirement undef # alias )); my $key = $dbh->bz_last_key( 'test_cases', 'case_id' ); if ($copydoc){ $self->store_text($key, Bugzilla->user->id, $self->text->{'action'}, $self->text->{'effect'}, $self->text->{'setup'}, $self->text->{'breakdown'},'VRESET' , $timestamp); } else{ $self->store_text($key, Bugzilla->user->id, '', '', '', '', 'VRESET', $timestamp); } return $key; } =head2 check_alias Checks if the given alias exists already. Returns the case_id of the matching case if it does. =cut =head2 class_check_alias Checks if the given alias exists already. Returns the case_id of the matching test case if it does. =cut sub class_check_alias { my ($alias) = @_; return unless $alias; my $dbh = Bugzilla->dbh; my ($id) = $dbh->selectrow_array( "SELECT case_id FROM test_cases WHERE alias = ?", undef, ($alias)); return $id; } =head2 update Updates this test case with new values supplied by the user. Accepts a reference to a hash with keys identical to a test cases fields and values representing the new values entered. Validation tests should be performed on the values before calling this method. If a field is changed, a history of that change is logged in the test_case_activity table. =cut =head2 history Returns a reference to a list of history entries from the test_case_activity table. =cut sub history { my $self = shift; my $dbh = Bugzilla->dbh; my $ref = $dbh->selectall_arrayref( "SELECT defs.description AS what, p.login_name AS who, a.changed, a.oldvalue, a.newvalue FROM test_case_activity AS a JOIN test_fielddefs AS defs ON a.fieldid = defs.fieldid JOIN profiles AS p ON a.who = p.userid WHERE a.case_id = ?", {'Slice'=>{}}, $self->{'case_id'}); foreach my $row (@$ref){ if ($row->{'what'} eq 'Case Status'){ $row->{'oldvalue'} = lookup_status($row->{'oldvalue'}); $row->{'newvalue'} = lookup_status($row->{'newvalue'}); } elsif ($row->{'what'} eq 'Category'){ $row->{'oldvalue'} = lookup_category($row->{'oldvalue'}); $row->{'newvalue'} = lookup_category($row->{'newvalue'}); } elsif ($row->{'what'} eq 'Priority'){ $row->{'oldvalue'} = lookup_priority($row->{'oldvalue'}); $row->{'newvalue'} = lookup_priority($row->{'newvalue'}); } elsif ($row->{'what'} eq 'Default Tester'){ $row->{'oldvalue'} = lookup_default_tester($row->{'oldvalue'}); $row->{'newvalue'} = lookup_default_tester($row->{'newvalue'}); } $row->{'changed'} = format_time($row->{'changed'}, TIME_FORMAT); } return $ref; } sub last_changed { my $self = shift; my $dbh = Bugzilla->dbh; my ($date) = $dbh->selectrow_array( "SELECT MAX(changed) FROM test_case_activity WHERE case_id = ?", undef, $self->id); return $self->{'creation_date'} unless $date; return $date; } # From process bug sub _snap_shot_deps { my ($i, $target, $me) = (@_); my $dbh = Bugzilla->dbh; my $ref = $dbh->selectcol_arrayref( "SELECT $target FROM test_case_dependencies WHERE $me = ? ORDER BY $target", undef, $i); return join(',', @{$ref}); } # Taken from Bugzilla::Bug sub _get_dep_lists { my ($myfield, $targetfield, $case_id) = (@_); my $dbh = Bugzilla->dbh; my $list_ref = $dbh->selectcol_arrayref( "SELECT test_case_dependencies.$targetfield FROM test_case_dependencies JOIN test_cases ON test_cases.case_id = test_case_dependencies.$targetfield WHERE test_case_dependencies.$myfield = ? ORDER BY test_case_dependencies.$targetfield", undef, ($case_id)); return $list_ref; } sub update_deps { my $self = shift; my ($dependson, $blocks, $case_id) = @_; $case_id = $self->{'case_id'} unless $case_id; my $dbh = Bugzilla->dbh; my $fields = {}; $fields->{'dependson'} = $dependson; $fields->{'blocked'} = $blocks; # From process bug foreach my $field ("dependson", "blocked") { if (exists $fields->{$field}) { my @validvalues; foreach my $id (split(/[\s,]+/, $fields->{$field})) { next unless $id; Bugzilla::Extension::Testopia::Util::validate_test_id($id, 'case'); push(@validvalues, $id); } $fields->{$field} = join(",", @validvalues); } } #From Bug.pm sub ValidateDependencies($$$) my $id = $case_id || 0; unless (defined($fields->{'dependson'}) || defined($fields->{'blocked'})) { return; } my %deps; my %deptree; foreach my $pair (["blocked", "dependson"], ["dependson", "blocked"]) { my ($me, $target) = @{$pair}; $deptree{$target} = []; $deps{$target} = []; next unless $fields->{$target}; my %seen; foreach my $i (split('[\s,]+', $fields->{$target})) { if ($id == $i) { ThrowUserError("dependency_loop_single"); } if (!exists $seen{$i}) { push(@{$deptree{$target}}, $i); $seen{$i} = 1; } } # populate $deps{$target} as first-level deps only. # and find remainder of dependency tree in $deptree{$target} @{$deps{$target}} = @{$deptree{$target}}; my @stack = @{$deps{$target}}; while (@stack) { my $i = shift @stack; my $dep_list = $dbh->selectcol_arrayref("SELECT $target FROM test_case_dependencies WHERE $me = ?", undef, $i); foreach my $t (@$dep_list) { # ignore any _current_ dependencies involving this test_case, # as they will be overwritten with data from the form. if ($t != $id && !exists $seen{$t}) { push(@{$deptree{$target}}, $t); push @stack, $t; $seen{$t} = 1; } } } } my @deps = @{$deptree{'dependson'}}; my @blocks = @{$deptree{'blocked'}}; my @union = (); my @isect = (); my %union = (); my %isect = (); foreach my $block (@deps, @blocks) { $union{$block}++ && $isect{$block}++ } @union = keys %union; @isect = keys %isect; if (scalar(@isect) > 0) { my $both = ""; foreach my $i (@isect) { $both .= "$i " ; } ThrowUserError("dependency_loop_multi", { both => $both }); } #from process_bug foreach my $pair ("blocked/dependson", "dependson/blocked") { my ($me, $target) = split("/", $pair); my @oldlist = @{$dbh->selectcol_arrayref("SELECT $target FROM test_case_dependencies WHERE $me = ? ORDER BY $target", undef, $id)}; if (defined $fields->{$target}) { my %snapshot; my @newlist = sort {$a <=> $b} @{$deps{$target}}; while (0 < @oldlist || 0 < @newlist) { if (@oldlist == 0 || (@newlist > 0 && $oldlist[0] > $newlist[0])) { $snapshot{$newlist[0]} = _snap_shot_deps($newlist[0], $me, $target); shift @newlist; } elsif (@newlist == 0 || (@oldlist > 0 && $newlist[0] > $oldlist[0])) { $snapshot{$oldlist[0]} = _snap_shot_deps($oldlist[0], $me, $target); shift @oldlist; } else { if ($oldlist[0] != $newlist[0]) { die "Error in list comparing code"; } shift @oldlist; shift @newlist; } } my @keys = keys(%snapshot); if (@keys) { my $oldsnap = _snap_shot_deps($id, $target, $me); $dbh->do("DELETE FROM test_case_dependencies WHERE $me = ?", undef, $id); foreach my $i (@{$deps{$target}}) { $dbh->do("INSERT INTO test_case_dependencies ($me, $target) VALUES (?,?)", undef, $id, $i); } } } } delete $self->{'blocked'}; delete $self->{'blocks'}; delete $self->{'dependson'}; delete $self->{'blocked_list'}; delete $self->{'dependson_list'}; } =head2 get_dep_tree Returns a list of test case dependencies =cut sub get_dep_tree { my $self = shift; $self->{'dep_list'} = (); $self->_generate_dep_tree($self->{'case_id'}); return $self->{'dep_list'}; } =head2 _generate_dep_tree Private method that recursivly gets a list of the test cases this blocks =cut sub _generate_dep_tree { my $self = shift; my ($case_id) = @_; my $deps = _get_dep_lists("dependson", "blocked", $case_id); return unless scalar @$deps; foreach my $id (@$deps){ $self->_generate_dep_tree($id); push @{$self->{'dep_list'}}, $id } } =head2 obliterate Removes this case and all things that reference it. =cut sub obliterate { my $self = shift; my $dbh = Bugzilla->dbh; foreach my $obj (@{$self->attachments}){ $obj->unlink_case($self->id); } foreach my $obj (@{$self->caseruns}){ $obj->obliterate; } $dbh->do("DELETE FROM test_case_texts WHERE case_id = ?", undef, $self->id); $dbh->do("DELETE FROM test_case_plans WHERE case_id = ?", undef, $self->id); $dbh->do("DELETE FROM test_case_components WHERE case_id = ?", undef, $self->id); $dbh->do("DELETE FROM test_case_tags WHERE case_id = ?", undef, $self->id); $dbh->do("DELETE FROM test_case_bugs WHERE case_id = ?", undef, $self->id); $dbh->do("DELETE FROM test_case_activity WHERE case_id = ?", undef, $self->id); $dbh->do("DELETE FROM test_case_dependencies WHERE dependson = ? OR blocked = ?", undef, ($self->id, $self->id)); $dbh->do("DELETE FROM test_cases WHERE case_id = ?", undef, $self->id); return 1; } sub fields { my @fields = qw( product plans summary author_id default_tester_id case_status_id priority_id category_id components requirement estimated_time isautomated script arguments alias tags bugs depends_on blocks runs set_up break_down action expected_results creation_date version case_id ); return \@fields; } sub TO_JSON { my $self = shift; my $obj; my $json = new JSON; my @plan_ids; my @comps; foreach my $p (@{$self->plans}){ push @plan_ids, $p->id; } foreach my $field ($self->DB_COLUMNS){ $obj->{$field} = $self->{$field}; } foreach my $c (@{$self->components}){ push @comps, $c->name; } $obj->{'plan_name'} = ${$self->plans}[0]->name; $obj->{'run_count'} = $self->run_count; $obj->{'author_name'} = $self->author->name if $self->author; $obj->{'default_tester'} = $self->default_tester->name if $self->default_tester; $obj->{'status'} = $self->status; $obj->{'priority'} = $self->priority; $obj->{'plan_id'} = $plan_ids[0]; $obj->{'plan_ids'} = \@plan_ids; $obj->{'type'} = $self->type; $obj->{'id'} = $self->id; $obj->{'canedit'} = $self->canedit; $obj->{'canview'} = $self->canview; $obj->{'candelete'} = $self->candelete; $obj->{'category_name'} = $self->category->name if $self->category; $obj->{'product_id'} = $self->plans->[0]->product_id if scalar @{$self->plans}; $obj->{'blocked'} = $self->blocked_list; $obj->{'dependson'} = $self->dependson_list; $obj->{'component'} = join(',',@comps); $obj->{'modified'} = format_time($self->last_changed, TIME_FORMAT); $obj->{'creation_date'} = format_time($self->{'creation_date'}, TIME_FORMAT); $obj->{'average_time'} = $self->calculate_average_time; return $json->encode($obj); } =head2 canview Returns true if the logged in user has rights to view this test case. =cut sub canview { my $self = shift; return 1 if Bugzilla->user->in_group('Testers'); return 1 if $self->get_user_rights(Bugzilla->user->id) & TR_READ; return 0; } =head2 canedit Returns true if the logged in user has rights to edit this test case. =cut sub canedit { my $self = shift; return 1 if Bugzilla->user->in_group('Testers'); return 1 if $self->get_user_rights(Bugzilla->user->id) & TR_WRITE; return 0; } =head2 candelete Returns true if the logged in user has rights to delete this test case. =cut sub candelete { my $self = shift; return 1 if Bugzilla->user->in_group('admin'); return 0 unless Bugzilla->params->{"allow-test-deletion"}; return 1 if Bugzilla->user->in_group('Testers') && Bugzilla->params->{"testopia-allow-group-member-deletes"}; # Otherwise, check for delete rights on all the plans this is linked to my $own_all = 1; foreach my $plan (@{$self->plans}){ if (!($plan->get_user_rights(Bugzilla->user->id) & TR_DELETE)) { $own_all = 0; last; } } return 1 if $own_all; return 0; } =head2 can_unlink_plan Returns true if this test case can be unlinked from the given plan =cut sub can_unlink_plan { my $self = shift; my ($plan_id) = @_; my $plan = Bugzilla::Extension::Testopia::TestPlan->new($plan_id); return 1 if Bugzilla->user->in_group('admin'); return 1 if Bugzilla->user->in_group('Testers') && Bugzilla->params->{"testopia-allow-group-member-deletes"}; return 1 if $plan->get_user_rights(Bugzilla->user->id) & TR_DELETE; return 0; } sub get_user_rights { my $self = shift; my ($userid) = @_; my $dbh = Bugzilla->dbh; my $plan_ids = $dbh->selectcol_arrayref( "SELECT plan_id FROM test_case_plans WHERE case_id = ?", undef, $self->id); return 0 unless $plan_ids; $plan_ids = join(',',@$plan_ids); my ($perms) = $dbh->selectrow_array( "SELECT MAX(permissions) FROM test_plan_permissions LEFT JOIN test_case_plans ON test_plan_permissions.plan_id = test_case_plans.plan_id INNER JOIN test_cases ON test_case_plans.case_id = test_cases.case_id WHERE userid = ? AND test_plan_permissions.plan_id IN ($plan_ids)", undef, $userid); return $perms; } ############################### #### Accessors #### ############################### =head1 ACCESSOR METHODS =head2 id Returns the ID of this object =head2 author Returns a Bugzilla::User object representing the Author of this case =head2 default_tester Returns a Bugzilla::User object representing the run's default tester =head2 creation_date Returns the creation time stamp of this object =head2 isautomated Returns true if this is an automatic test case =head2 script Returns the script of this object =head2 status_id Returns the status_id of this object =head2 arguments Returns the arguments for the script of this object =head2 summary Returns the summary of this object =head2 requirements Returns the requirements of this object =head2 alias Returns the alias of this object =cut sub id { return $_[0]->{'case_id'}; } sub author { return Bugzilla::User->new($_[0]->{'author_id'}); } sub default_tester { return Bugzilla::User->new($_[0]->{'default_tester_id'}); } sub creation_date { return $_[0]->{'creation_date'}; } sub estimated_time { return $_[0]->{'estimated_time'}; } sub isautomated { return $_[0]->{'isautomated'}; } sub script { return $_[0]->{'script'}; } sub status_id { return $_[0]->{'case_status_id'};} sub arguments { return $_[0]->{'arguments'}; } sub summary { return $_[0]->{'summary'}; } sub requirement { return $_[0]->{'requirement'}; } sub alias { return $_[0]->{'alias'}; } =head2 type Returns 'case' =cut sub type { my $self = shift; $self->{'type'} = 'case'; return $self->{'type'}; } =head2 attachments Returns a reference to a list of attachments associated with this case. =cut sub attachments { my ($self) = @_; my $dbh = Bugzilla->dbh; return $self->{'attachments'} if exists $self->{'attachments'}; my $attachments = $dbh->selectcol_arrayref( "SELECT attachment_id FROM test_case_attachments WHERE case_id = ?", undef, $self->{'case_id'}); my @attachments; foreach my $attach (@{$attachments}){ push @attachments, Bugzilla::Extension::Testopia::Attachment->new($attach); } $self->{'attachments'} = \@attachments; return $self->{'attachments'}; } =head2 version Returns the case text version. This number is incremented any time changes are made to the case docs (action and effect). =cut sub version { my $self = shift; my $dbh = Bugzilla->dbh; return $self->{'version'} if exists $self->{'version'}; my ($ver) = $dbh->selectrow_array("SELECT MAX(case_text_version) FROM test_case_texts WHERE case_id = ?", undef, $self->{'case_id'}); $self->{'version'} = $ver; return $self->{'version'}; } =head2 status Looks up the case status based on the case_status_id of this case. =cut sub status { my $self = shift; my $dbh = Bugzilla->dbh; return $self->{'status'} if exists $self->{'status'}; my ($res) = $dbh->selectrow_array("SELECT name FROM test_case_status WHERE case_status_id = ?", undef, $self->{'case_status_id'}); $self->{'status'} = $res; return $self->{'status'}; } =head2 priority Looks up the Bugzilla priority value based on the priority_id of this case. =cut sub priority { my $self = shift; my $dbh = Bugzilla->dbh; return $self->{'priority'} if exists $self->{'priority'}; my ($res) = $dbh->selectrow_array("SELECT value FROM priority WHERE id = ?", undef, $self->{'priority_id'}); $self->{'priority'} = $res; return $self->{'priority'}; } =head2 category Returns the category name based on the category_id of this case =cut sub category { my $self = shift; return $self->{'category'} if exists $self->{'category'}; $self->{'category'} = Bugzilla::Extension::Testopia::Category->new($self->{'category_id'}); return $self->{'category'}; } =head2 components Returns a reference to a list of bugzilla components assoicated with this test case. =cut sub components { my $self = shift; my $dbh = Bugzilla->dbh; return $self->{'components'} if exists $self->{'components'}; my $comps = $dbh->selectcol_arrayref( "SELECT comp.id FROM components AS comp JOIN test_case_components AS tcc ON tcc.component_id = comp.id JOIN test_cases ON tcc.case_id = test_cases.case_id WHERE test_cases.case_id = ?", {'Slice' => {}}, $self->{'case_id'}); my @comps; foreach my $id (@$comps){ my $comp = Bugzilla::Component->new($id); my $prod = Bugzilla::Product->new($comp->product_id); $comp->{'product_name'} = $prod->name; push @comps, $comp; } $self->{'components'} = \@comps; return $self->{'components'}; } =head2 tags Returns a reference to a list of Bugzilla::Extension::Testopia::TestTag objects associated with this case. =cut sub tags { my $self = shift; my $dbh = Bugzilla->dbh; return $self->{'tags'} if exists $self->{'tags'}; my $tagids = $dbh->selectcol_arrayref("SELECT test_case_tags.tag_id FROM test_case_tags INNER JOIN test_tags ON test_case_tags.tag_id = test_tags.tag_id WHERE case_id = ? ORDER BY test_tags.tag_name", undef, $self->{'case_id'}); my @tags; foreach my $id (@{$tagids}){ push @tags, Bugzilla::Extension::Testopia::TestTag->new($id); } $self->{'tags'} = \@tags; return $self->{'tags'}; } =head2 plans Returns a reference to a list of Bugzilla::Extension::Testopia::TestPlan objects associated with this case. =cut sub plans { my $self = shift; my $dbh = Bugzilla->dbh; return $self->{'plans'} if exists $self->{'plans'}; my $ref = $dbh->selectcol_arrayref("SELECT plan_id FROM test_case_plans WHERE case_id = ? ORDER BY plan_id", undef, $self->{'case_id'}); my @plans; foreach my $id (@{$ref}){ push @plans, Bugzilla::Extension::Testopia::TestPlan->new($id); } $self->{'plans'} = \@plans; return $self->{'plans'}; } sub plan_list { my $self = shift; my $dbh = Bugzilla->dbh; return $self->{'plan_list'} if exists $self->{'plan_list'}; my $ref = $dbh->selectcol_arrayref("SELECT plan_id FROM test_case_plans WHERE case_id = ? ORDER BY plan_id", undef, $self->{'case_id'}); $self->{'plan_list'} = join(',', @$ref); return $self->{'plan_list'}; } =head2 bugs Returns a reference to a list of Bugzilla::Bug objects associated with this case. =cut sub bugs { my $self = shift; my $dbh = Bugzilla->dbh; return $self->{'bugs'} if exists $self->{'bugs'}; my $ref = $dbh->selectall_arrayref( "SELECT bug_id, case_run_id FROM test_case_bugs WHERE case_id = ?", {'Slice' => {}}, $self->{'case_id'}); my @bugs; foreach my $row (@{$ref}){ next unless Bugzilla->user->can_see_bug($row->{'bug_id'}); my $bug = Bugzilla::Bug->new($row->{'bug_id'}, Bugzilla->user->id); if ($row->{'case_run_id'}){ my $cr = Bugzilla::Extension::Testopia::TestCaseRun->new($row->{'case_run_id'}); next unless $cr; $bug->{'build'} = $cr->build->name; $bug->{'env'} = $cr->environment->name; $bug->{'run_id'} = $cr->run_id; $bug->{'case_run_id'} = $cr->id; } push @bugs, $bug; } $self->{'bugs'} = \@bugs; return $self->{'bugs'}; } =head2 bug_list Returns a comma separated list of bug ids associated with this case =cut sub bug_list { my $self = shift; return $self->{'bug_list'} if exists $self->{'bug_list'}; my $dbh = Bugzilla->dbh; my @bugs; my $bugids = $dbh->selectcol_arrayref("SELECT bug_id FROM test_case_bugs WHERE case_id=?", undef, $self->id); my @visible; foreach my $bugid (@{$bugids}){ push @visible, $bugid if Bugzilla->user->can_see_bug($bugid); } $self->{'bug_list'} = join(",", @$bugids); return $self->{'bug_list'}; } =head2 text Returns a hash reference representing the action and effect of this case. =cut sub text { my $self = shift; my $dbh = Bugzilla->dbh; my ($version) = @_; trick_taint($version) if $version; return $self->{'text'} if exists $self->{'text'} && !$version; $version = $version || $self->version; my $text = $dbh->selectrow_hashref( "SELECT action, effect, setup, breakdown, profiles.realname AS author, case_text_version AS version FROM test_case_texts INNER JOIN profiles on profiles.userid = test_case_texts.who WHERE case_id = ? AND case_text_version = ?", undef, ($self->{'case_id'}, $version)); return $text if scalar @_; $self->{'text'} = $text; return $self->{'text'}; } =head2 runs Returns a reference to a list of Bugzilla::Extension::Testopia::TestRun objects associated with this case. =cut sub runs { my $self = shift; my $dbh = Bugzilla->dbh; return $self->{'runs'} if exists $self->{'runs'}; my $ref = $dbh->selectcol_arrayref( "SELECT DISTINCT t.run_id FROM test_runs t INNER JOIN test_case_runs r ON r.run_id = t.run_id WHERE case_id = ?", undef, $self->{'case_id'}); my @runs; foreach my $id (@{$ref}){ push @runs, Bugzilla::Extension::Testopia::TestRun->new($id); } $self->{'runs'} = \@runs; return $self->{'runs'}; } sub run_count { my $self = shift; my $dbh = Bugzilla->dbh; my ($runcount) = $dbh->selectrow_array( "SELECT DISTINCT count(run_id) FROM test_case_runs WHERE case_id = ?", undef, $self->id); return $runcount; } =head2 caseruns Returns a reference to a list of Bugzilla::Extension::Testopia::TestCaseRun objects associated with this case. =cut sub caseruns { my $self = shift; my $dbh = Bugzilla->dbh; return $self->{'caseruns'} if exists $self->{'caseruns'}; my $ref = $dbh->selectcol_arrayref("SELECT case_run_id FROM test_case_runs WHERE case_id = ?", undef, $self->{'case_id'}); my @runs; foreach my $id (@{$ref}){ push @runs, Bugzilla::Extension::Testopia::TestCaseRun->new($id); } $self->{'caseruns'} = \@runs; return $self->{'caseruns'}; } sub sortkey { my $self = shift; return $self->{'sortkey'} if exists $self->{'sortkey'}; my $dbh = Bugzilla->dbh; my ($sortkey) = $dbh->selectrow_array("SELECT MAX(sortkey) FROM test_cases"); $self->{'sortkey'} = ++$sortkey; return $self->{'sortkey'}; } =head2 blocked Returns a reference to a list of Bugzilla::Extension::Testopia::TestCase objects which are blocked by this test case. =cut sub blocked { my ($self) = @_; return $self->{'blocked'} if exists $self->{'blocked'}; my @deps; my $ref = _get_dep_lists("dependson", "blocked", $self->{'case_id'}); foreach my $id (@{$ref}){ push @deps, Bugzilla::Extension::Testopia::TestCase->new($id); } $self->{'blocked'} = \@deps; return $self->{'blocked'}; } sub blocked_list { my ($self) = @_; return $self->{'blocked_list'} if exists $self->{'blocked_list'}; my @deps; my $ref = _get_dep_lists("dependson", "blocked", $self->{'case_id'}); $self->{'blocked_list'} = join(",", @$ref); return $self->{'blocked_list'}; } =head2 blocked_list_uncached Returns a space separated list of test cases that are blocked by this test case. This method does not cache the blocked test cases so each call will result in a database read. =cut sub blocked_list_uncached { my ($self) = @_; my $ref = _get_dep_lists("dependson", "blocked", $self->{'case_id'}); return join(" ", @$ref) } =head2 dependson Returns a reference to a list of Bugzilla::Extension::Testopia::TestCase objects which depend on this test case. =cut sub dependson { my ($self) = @_; return $self->{'dependson'} if exists $self->{'dependson'}; my @deps; my $ref = _get_dep_lists("blocked", "dependson", $self->{'case_id'}); foreach my $id (@{$ref}){ push @deps, Bugzilla::Extension::Testopia::TestCase->new($id); } $self->{'dependson'} = \@deps; return $self->{'dependson'}; } sub dependson_list { my ($self) = @_; return $self->{'dependson_list'} if exists $self->{'dependson_list'}; my @deps; my $ref = _get_dep_lists("blocked", "dependson", $self->{'case_id'}); $self->{'dependson_list'} = join(",", @$ref); return $self->{'dependson_list'}; } =head2 dependson_list_uncached Returns a space separated list of test cases that depend on this test case. This method does not cache the dependent test cases so each call will result in a database read. =cut sub dependson_list_uncached { my ($self) = @_; my $ref = _get_dep_lists("blocked", "dependson", $self->{'case_id'}); return join(" ", @$ref) } sub calculate_average_time { my $self = shift; my $dbh = Bugzilla->dbh; my $totalseconds; my $i = 0; foreach my $cr (@{$self->caseruns}){ if ($cr->completion_time){ $totalseconds += $cr->completion_time; $i++; } } my $average = $i ? int($totalseconds / $i) : 0; my @time = gmtime($average); my %time; $time{hr} = $time[2]; $time{min} = $time[1]; $time{sec} = $time[0]; return $time{hr}.":".$time{min}.":".$time{sec}; } =head1 TODO =head1 SEE ALSO TestPlan, TestRun, TestCaseRun =head1 AUTHOR Greg Hendricks =cut 1; __END__ =head1 NAME Bugzilla::Extension::Testopia::TestCase - Testopia Test Case object =head1 DESCRIPTION This module represents a test case in Testopia. Each test case must be linked to one or more test plans. =head1 SYNOPSIS use Bugzilla::Extension::Testopia::TestCase; $case = Bugzilla::Extension::Testopia::TestCase->new($case_id); $case = Bugzilla::Extension::Testopia::TestCase->new(\%case_hash); =cut =head1 FIELDS case_id case_status_id category_id priority_id author_id default_tester_id creation_date estimated_time isautomated sortkey script arguments summary requirement alias =cut =head1 METHODS =cut =head2 lookup_status Takes an ID of the status field and returns the value =cut =head2 lookup_status_by_name Returns the id of the status name passed. =cut =head2 lookup_category Takes an ID of the category field and returns the value =cut =head2 lookup_category_by_name Returns the id of the category name passed. =cut =head2 lookup_priority Takes an ID of the priority field and returns the value =cut =head2 lookup_priority_by_name Returns the id of the priority name passed. =cut =head2 lookup_default_tester Takes an ID of the default_tester field and returns the value =cut