# -*- 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 Testopia System. # # The Initial Developer of the Original Code is Greg Hendricks. # Portions created by Greg Hendricks are Copyright (C) 2006 # Novell. All Rights Reserved. # # Contributor(s): Greg Hendricks package Bugzilla::Extension::Testopia::TestCaseRun; use strict; use Bugzilla::Util; use Bugzilla::Error; use Bugzilla::User; use Bugzilla::Bug; use Bugzilla::Config; use Bugzilla::Constants; use Bugzilla::Extension::Testopia::Util; use Bugzilla::Extension::Testopia::Environment; use Bugzilla::Extension::Testopia::Build; use Bugzilla::Extension::Testopia::Constants; use Bugzilla::Extension::Testopia::Attachment; use Date::Format; use Date::Parse; use base qw(Exporter Bugzilla::Object); #@Testopia::TestCaseRun::EXPORT = qw(lookup_status lookup_status_by_name); ############################### #### Initialization #### ############################### =head1 FIELDS case_run_id run_id case_id assignee testedby case_run_status_id case_text_version build_id environment_id notes running_date close_date iscurrent sortkey =cut use constant DB_TABLE => "test_case_runs"; use constant ID_FIELD => "case_run_id"; use constant NAME_FIELD => ""; use constant DB_COLUMNS => qw( case_run_id run_id case_id assignee testedby case_run_status_id case_text_version build_id environment_id priority_id notes running_date close_date iscurrent sortkey ); use constant REQUIRED_CREATE_FIELDS => qw(case_id run_id build_id environment_id case_run_status_id); use constant UPDATE_COLUMNS => qw(case_run_status_id case_text_version notes sortkey priority_id assignee); use constant VALIDATORS => { case_id => \&_check_case_id, run_id => \&_check_run_id, case_text_version => \&_check_case_text_version, case_run_status_id => \&_check_case_run_status_id, priority_id => \&Bugzilla::Extension::Testopia::TestCase::_check_priority, }; use constant UPDATE_VALIDATORS => { build_id => \&Testopia::TestRun::_check_build, environment_id => \&Testopia::TestRun::_check_env, }; sub report_columns { my $self = shift; my %columns; # Changes here need to match Report.pm $columns{'Build'} = "build"; $columns{'Status'} = "case_run_status"; $columns{'Environment'} = "environment"; $columns{'Assignee'} = "assignee"; $columns{'Tested By'} = "testedby"; $columns{'Milestone'} = "milestone"; $columns{'Case Tags'} = "case_tags"; $columns{'Run Tags'} = "run_tags"; $columns{'Requirement'} = "requirement"; $columns{'Priority'} = "priority"; $columns{'Default tester'} = "default_tester"; $columns{'Category'} = "category"; $columns{'Component'} = "component"; my @result; push @result, {'name' => $_, 'id' => $columns{$_}} foreach (sort(keys %columns)); unshift @result, {'name' => '', 'id'=> ''}; return \@result; } ############################### #### Validators #### ############################### sub _check_case_id { my ($invocant, $id) = @_; return Bugzilla::Extension::Testopia::Util::validate_test_id($id, 'case'); } sub _check_run_id { my ($invocant, $id) = @_; return Bugzilla::Extension::Testopia::Util::validate_test_id($id, 'run'); } sub _check_case_run_status_id { my ($invocant, $status) = @_; $status = trim($status); my $status_id; if ($status =~ /^\d+$/){ $status_id = Bugzilla::Extension::Testopia::Util::validate_selection($status, 'case_run_status_id', 'test_case_run_status'); } else { $status_id = lookup_status_by_name($status); } ThrowUserError('invalid_status') unless $status_id; return $status_id; } sub _check_case_text_version { my ($invocant, $version) = @_; return $version =~ /\d+/ ? $version : 1; } sub _check_assignee{ 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; } } ############################### #### Mutators #### ############################### sub new { my $invocant = shift; my $class = ref($invocant) || $invocant; my ($param, $case_id, $build_id, $env_id) = (@_); my $dbh = Bugzilla->dbh; if ($case_id && detaint_natural($case_id) && $build_id && detaint_natural($build_id) && $env_id && detaint_natural($env_id)){ my $run_id = $param; detaint_natural($case_id) || return undef; detaint_natural($run_id) || return undef; ($param) = $dbh->selectrow_array( "SELECT case_run_id FROM test_case_runs WHERE case_id = ? AND run_id = ? AND build_id = ? AND environment_id = ?", undef, ($case_id, $run_id, $build_id, $env_id)); ThrowUserError('invalid-test-id-non-existent', {type => 'case_run'}) unless $param; } # 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; } unshift @_, $param; my $self = $class->SUPER::new(@_); return $self; } sub run_create_validators { my $class = shift; my $params = $class->SUPER::run_create_validators(@_); my $run = Bugzilla::Extension::Testopia::TestRun->new($params->{run_id}); $params->{build_id} = Bugzilla::Extension::Testopia::TestRun::->_check_build($params->{build_id}, $run->plan->product); $params->{environment_id} = Bugzilla::Extension::Testopia::TestRun::->_check_env($params->{environment_id}, $run->plan->product); $params->{assignee} = $class->_check_assignee($params->{assignee}); $params->{testedby} = $class->_check_assignee($params->{testedby}); 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 $case = Bugzilla::Extension::Testopia::TestCase->new($params->{case_id}); unless ($params->{priority_id}){ $params->{priority_id} = $case->{priority_id}; } my $field_values = $class->run_create_validators($params); $field_values->{case_text_version} ||= $case->version; $field_values->{iscurrent} ||= 1; my $self = $class->SUPER::insert_create_data($field_values); $self->set_as_current; return $self; } ############################### #### Methods #### ############################### =head2 check_exists Checks for an existing entry with the same build and environment for this case and run and switches self to that object. =cut sub switch { my $self = shift; my ($build_id, $env_id ,$run_id, $case_id) = @_; detaint_natural($build_id); detaint_natural($env_id); detaint_natural($run_id); detaint_natural($case_id); $run_id ||= $self->{'run_id'}; $case_id ||= $self->{'case_id'}; $build_id ||= $self->{'build_id'}; $env_id ||= $self->{'environment_id'}; my $dbh = Bugzilla->dbh; my ($is) = $dbh->selectrow_array( "SELECT case_run_id FROM test_case_runs WHERE run_id = ? AND case_id = ? AND build_id = ? AND environment_id = ?", undef, ($run_id, $case_id, $build_id, $env_id)); if ($is){ $self = Bugzilla::Extension::Testopia::TestCaseRun->new($is); } else { my $oldbuild = $self->{'build_id'}; my $oldenv = $self->{'environment_id'}; $self = $self->create({ 'run_id' => $self->{'run_id'}, 'case_id' => $self->{'case_id'}, 'assignee' => $self->{'assignee'}, 'case_text_version' => $self->{'case_text_version'}, 'build_id' => $build_id, 'environment_id' => $env_id, 'case_run_status_id' => IDLE, }); if ($oldbuild != $build_id){ my $build = Bugzilla::Extension::Testopia::Build->new($oldbuild); my $note = "Build Changed by ". Bugzilla->user->login; $note .= ". Old build: '". $build->name; $note .= "' New build: '". $self->build->name; $note .= "'. Resetting to IDLE."; $self->append_note($note); } if ($oldenv != $env_id){ my $environment = Bugzilla::Extension::Testopia::Environment->new($oldenv); my $note = "Environment Changed by ". Bugzilla->user->login; $note .= ". Old environment: '". $environment->name; $note .= "' New environment: '". $self->environment->name; $note .= "'. Resetting to IDLE."; $self->append_note($note); } } $self->set_as_current; return $self; } sub TO_JSON { my $self = shift; my $obj; my $json = new JSON; foreach my $field ($self->DB_COLUMNS){ $obj->{$field} = $self->{$field}; } my @bugs; foreach my $b (@{$self->bugs}){ push @bugs, { bug_id => $b->bug_id, closed => Bugzilla::Bug::is_open_state($b->{'bug_status'}) ? JSON::false : JSON::true}; } my @casebugs; foreach my $b (@{$self->case->bugs}){ push @casebugs, { bug_id => $b->bug_id, closed => Bugzilla::Bug::is_open_state($b->{'bug_status'}) ? JSON::false : JSON::true}; } my $bugs = { bugs => \@bugs }; my $casebugs = { bugs => \@casebugs }; $obj->{'assignee_name'} = $self->assignee->login if $self->assignee; $obj->{'requirement'} = $self->case->requirement if $self->case; $obj->{'testedby'} = $self->testedby->login if $self->testedby; $obj->{'status'} = $self->status; $obj->{'build_name'} = $self->build->name if $self->build; $obj->{'env_name'} = $self->environment->name if $self->environment; $obj->{'env_id'} = $self->environment->id if $self->environment; $obj->{'category'} = $self->case->category->name if $self->case && $self->case->category; $obj->{'priority'} = $self->priority if $self->case; $obj->{'bug_count'} = $self->bug_count; $obj->{'case_summary'} = $self->case->summary if $self->case; $obj->{'component'} = @{$self->case->components}[0]->name if ($self->case && scalar @{$self->case->components}); $obj->{'type'} = $self->type; $obj->{'id'} = $self->id; $obj->{'sortkey'} = $self->sortkey; $obj->{'bug_list'} = $bugs; $obj->{'case_bug_list'} = $casebugs; $obj->{'close_date'} = format_time($self->{close_date}, TIME_FORMAT); $obj->{'running_date'} = format_time($self->{running_date}, TIME_FORMAT); $obj->{'plan_name'} = $self->run->plan->name; return $json->encode($obj); } sub fields { my $self = shift; my @fields = qw( case_id case_summary run_id plan_id build_id environment_id status_id assignee testedby components tags case_text_version priority_id running_date close_date most_current sortkey bugs notes ); return \@fields; } =head2 _update_fields Update this case-run in the database if a change is made to an updatable field. =cut sub _update_fields{ my $self = shift; my ($newvalues) = @_; my $dbh = Bugzilla->dbh; if ($newvalues->{'case_run_status_id'} && $newvalues->{'case_run_status_id'} == FAILED){ $self->_update_deps(BLOCKED); } elsif ($newvalues->{'case_run_status_id'} && $newvalues->{'case_run_status_id'} == PASSED){ $self->_update_deps(IDLE); } $dbh->bz_start_transaction(); foreach my $field (keys %{$newvalues}){ $dbh->do("UPDATE test_case_runs SET $field = ? WHERE case_run_id = ?", undef, $newvalues->{$field}, $self->{'case_run_id'}); $self->{$field} = $newvalues->{$field}; } $dbh->bz_commit_transaction(); return $self->{'case_run_id'}; } =head2 set_as_current Sets this case-run as the current or active one in the history list of case-runs of this build and case_id =cut sub set_as_current { my $self = shift; my ($caserun) = @_; $caserun = $self->{'case_run_id'} unless defined $caserun; my $dbh = Bugzilla->dbh; $dbh->bz_start_transaction(); $dbh->do("UPDATE test_case_runs SET iscurrent = 0 WHERE case_id = ? AND run_id = ?", undef, ($self->case_id, $self->run_id)); $dbh->do("UPDATE test_case_runs SET iscurrent = 1 WHERE case_run_id = ?", undef, $caserun); $dbh->bz_commit_transaction(); } =head2 set_status Sets the status on a case-run and updates the close_date and testedby if the status is a closed status. =cut sub set_status { my $self = shift; my ($status_id, $update_bugs) = @_; $status_id = $self->_check_case_run_status_id($status_id); return if $self->status_id == $status_id; my $oldstatus = $self->status; my $newstatus = lookup_status($status_id); $self->_update_fields({'case_run_status_id' => $status_id}); if ($status_id == IDLE){ $self->_update_fields({'close_date' => undef}); $self->_update_fields({'testedby' => undef}); $self->{'close_date'} = undef; $self->{'testedby'} = undef; } elsif ($status_id == RUNNING || $status_id == PAUSED){ my $timestamp = Bugzilla::Extension::Testopia::Util::get_time_stamp(); $self->_update_fields({'running_date' => $timestamp}) if $status_id == RUNNING; $self->_update_fields({'close_date' => undef}); $self->{'close_date'} = undef; } else { my $timestamp = Bugzilla::Extension::Testopia::Util::get_time_stamp(); $self->_update_fields({'close_date' => $timestamp}); $self->_update_fields({'testedby' => Bugzilla->user->id}); $self->{'close_date'} = $timestamp; $self->{'testedby'} = Bugzilla->user->id; $self->update_bugs('REOPENED') if ($status_id == FAILED && $update_bugs); $self->update_bugs('VERIFIED') if ($status_id == PASSED && $update_bugs); } $self->set_as_current; my $note = "Status changed from $oldstatus to $newstatus by ". Bugzilla->user->login; $note .= " for build '". $self->build->name ."' and environment '". $self->environment->name; $self->append_note($note); $self->{'case_run_status_id'} = $status_id; delete $self->{'status'}; } sub set_sortkey { my $self = shift; my ($sortkey) = @_; my $dbh = Bugzilla->dbh; detaint_natural($sortkey); $dbh->do("UPDATE test_case_runs SET sortkey = ? WHERE case_id = ? AND run_id = ?", undef, ($sortkey, $self->case_id, $self->run_id)); $self->{'sortkey'} = $sortkey; } sub set_priority { $_[0]->set('priority_id', $_[1]); } =head2 set_assignee Sets the assigned tester for the case-run =cut sub set_assignee { my $self = shift; my ($user_id) = @_; my $assignee = $self->_check_assignee($user_id); my $oldassignee = $self->assignee->login || '--unassigned--'; my $newassignee = Bugzilla::User->new($assignee); my $newassignee_name = $newassignee->id ? $newassignee->login : '--unassigned--'; $self->_update_fields({'assignee' => $assignee}); $self->{'assignee'} = $assignee; my $note = "Assignee changed from $oldassignee to ". $newassignee_name; $note .= " by ". Bugzilla->user->login; $note .= " for build '". $self->build->name; $note .= "' and environment '". $self->environment->name; $self->append_note($note); } =head2 append_note Updates the notes field for the case-run =cut sub append_note { my $self = shift; my ($note) = @_; return unless $note; my $timestamp = time2str("%c", time()); $note = "$timestamp: $note"; if ($self->{'notes'}){ $note = $self->{'notes'} . "\n" . $note; } $self->_update_fields({'notes' => $note}); $self->{'notes'} = $note; } =head2 _update_deps Private method for updating blocked test cases. If the pre-requisite case fails, the blocked test cases in a run get a status of BLOCKED if it passes they are set back to IDLE. This only happens to the current case run and only if it doesn't already have a closed status. =cut sub _update_deps { my $self = shift; my ($status) = @_; my $deplist = $self->case->get_dep_tree; return unless $deplist; my $dbh = Bugzilla->dbh; $dbh->bz_start_transaction(); my $caseruns = $dbh->selectcol_arrayref( "SELECT case_run_id FROM test_case_runs WHERE iscurrent = 1 AND run_id = ? AND case_run_status_id IN(". join(',', (IDLE,RUNNING,PAUSED,BLOCKED)) .") AND case_id IN (". join(',', @$deplist) .")", undef, $self->{'run_id'}); my $sth = $dbh->prepare_cached( "UPDATE test_case_runs SET case_run_status_id = ? WHERE case_run_id = ?"); foreach my $id (@$caseruns){ $sth->execute($status, $id); } $dbh->bz_commit_transaction(); $self->{'updated_deps'} = $caseruns; } =head2 get_case_run_list Returns a reference to a list of case-runs for the given case and run =cut sub get_case_run_list { my $self = shift; my $dbh = Bugzilla->dbh; my $ref = $dbh->selectcol_arrayref( "SELECT case_run_id FROM test_case_runs WHERE case_id = ? AND run_id = ?", undef, ($self->{'case_id'}, $self->{'run_id'})); return $ref; } sub get_history { my $self = shift; my $dbh = Bugzilla->dbh; my $ref = $dbh->selectall_arrayref( "SELECT tcr.case_run_id, tcr.close_date, tcr.iscurrent, tb.name AS build_name, te.name as env_name, p.login_name AS testedby, tcrs.name AS status_name FROM test_case_runs tcr INNER JOIN test_builds tb ON tcr.build_id = tb.build_id INNER JOIN test_environments te ON tcr.environment_id = te.environment_id INNER JOIN test_case_run_status tcrs ON tcr.case_run_status_id = tcrs.case_run_status_id LEFT JOIN profiles p ON tcr.testedby = p.userid WHERE case_id = ? AND run_id = ?", {'Slice' =>{}}, ($self->{'case_id'}, $self->{'run_id'})); foreach my $r (@$ref){ my @bugs; my $bugids = $dbh->selectcol_arrayref("SELECT bug_id FROM test_case_bugs WHERE case_run_id=?", undef, $r->{'case_run_id'}); foreach my $bugid (@{$bugids}){ my $b = Bugzilla::Bug->new($bugid, Bugzilla->user->id); push @bugs, { bug_id => $b->bug_id, closed => Bugzilla::Bug::is_open_state($b->{'bug_status'}) ? JSON::false : JSON::true} if Bugzilla->user->can_see_bug($bugid); } my $bugs = { bugs => \@bugs }; $r->{'bug_list'} = $bugs; } return $ref; } =head2 get_status_list Returns a list reference of the legal statuses for a test case-run =cut sub get_status_list { my $self = shift; my $dbh = Bugzilla->dbh; my $ref = $dbh->selectall_arrayref( "SELECT case_run_status_id AS id, name FROM test_case_run_status ORDER BY sortkey", {'Slice' =>{}}); return $ref } =head2 attach_bug Attaches the specified bug to this test case-run =cut sub attach_bug { my $self = shift; my ($bugs, $caserun_id) = @_; $bugs = Bugzilla::Extension::Testopia::TestCase->_check_bugs($bugs, "ATTACH"); $caserun_id ||= $self->{'case_run_id'}; my $dbh = Bugzilla->dbh; $dbh->bz_start_transaction(); foreach my $bug (@$bugs){ trick_taint($bug) if $bug; trick_taint($caserun_id) if $caserun_id; my ($exists) = $dbh->selectrow_array( "SELECT bug_id FROM test_case_bugs WHERE case_run_id=? AND bug_id=?", undef, ($caserun_id, $bug)); if ($exists) { $dbh->bz_commit_transaction(); return; } my ($check) = $dbh->selectrow_array( "SELECT bug_id FROM test_case_bugs WHERE case_id=? AND bug_id=? AND case_run_id=?", undef, ($caserun_id, $bug, undef)); if ($check){ $dbh->do("UPDATE test_case_bugs SET test_case_run_id = ? WHERE case_id = ? AND bug_id = ?", undef, ($bug, $self->{'case_run_id'})); } else{ $dbh->do("INSERT INTO test_case_bugs (bug_id, case_run_id, case_id) VALUES(?,?,?)", undef, ($bug, $self->{'case_run_id'}, $self->{'case_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 ($bug) = @_; my $dbh = Bugzilla->dbh; trick_taint($bug) if $bug; $dbh->do("DELETE FROM test_case_bugs WHERE bug_id = ? AND case_run_id = ?", undef, ($bug, $self->{'case_run_id'})); } =head2 update_bugs Updates bug status depending on whether the case passed or failed. If the case failed it will reopen any attached bugs that are closed. If it passed it will mark RESOLVED bugs VERIFIED. =cut sub update_bugs { my $self = shift; my ($status) = @_; my $resolution; my $dbh = Bugzilla->dbh; my $timestamp = Bugzilla::Extension::Testopia::Util::get_time_stamp(); foreach my $bug (@{$self->case->bugs}){ my $oldstatus = $bug->bug_status; my $oldresolution = $bug->resolution; next if ($status eq 'VERIFIED' && $oldstatus ne 'RESOLVED'); next if ($status eq 'REOPENED' && $oldstatus !~ /(RESOLVED|VERIFIED|CLOSED)/); next if $oldresolution eq 'DUPLICATE'; if ($status eq 'REOPENED'){ $resolution = ''; } else{ $resolution = $oldresolution; } my $comment = "Status updated by Testopia: ". Bugzilla->params->{"urlbase"}; $comment .= "tr_show_case.cgi?case_id=" . $self->case->id; $dbh->bz_start_transaction(); $bug->add_comment($comment); $bug->set_bug_status($status, {resolution => $resolution}); $bug->update(); $dbh->bz_commit_transaction(); } } =head2 obliterate Removes this caserun, its history, and all things that reference it. =cut sub obliterate { my $self = shift; my $single = shift; my $dbh = Bugzilla->dbh; my $sth = $dbh->prepare_cached("DELETE FROM test_case_bugs WHERE case_run_id = ?"); if ($single){ $sth->execute($self->id); $dbh->do("DELETE FROM test_case_runs WHERE case_run_id = ?", undef, ($self->id)); my $ref = $dbh->selectcol_arrayref( "SELECT case_run_id FROM test_case_runs WHERE case_id = ? AND run_id = ? ORDER BY close_date DESC", undef, ($self->{'case_id'}, $self->{'run_id'})); if (scalar @{$ref}){ my $cr = new Bugzilla::Extension::Testopia::TestCaseRun(@{$ref}[0]); $cr->set_as_current; } } else { foreach my $id (@{$self->get_case_run_list}){ $sth->execute($id); } $dbh->do("DELETE FROM test_case_runs WHERE case_id = ? AND run_id = ?", undef, ($self->case_id, $self->run_id)); } return 1; } ############################### #### Accessors #### ############################### =head1 ACCESSOR METHODS =head2 id Returns the ID of the object =head2 testedby Returns a Bugzilla::User object representing the user that closed this case-run. =head2 assignee Returns a Bugzilla::User object representing the user assigned to update this case-run. =head2 case_text_version Returns the version of the test case document that this case-run was run against. =head2 notes Returns the notes of the object =head2 close_date Returns the time stamp of when this case-run was closed =head2 iscurrent Returns true if this is the current case-run in the history list =head2 status_id Returns the status id of the object =head2 sortkey Returns the sortkey of the object =head2 isprivate Returns the true if this case-run is private. =cut =head2 updated_deps Returns a reference to a list of dependent caseruns that were updated =cut sub id { return $_[0]->{'case_run_id'}; } sub case_id { return $_[0]->{'case_id'}; } sub run_id { return $_[0]->{'run_id'}; } sub testedby { return Bugzilla::User->new($_[0]->{'testedby'}); } sub assignee { return Bugzilla::User->new($_[0]->{'assignee'}); } sub case_text_version { return $_[0]->{'case_text_version'}; } sub running_date { return $_[0]->{'running_date'}; } sub close_date { return $_[0]->{'close_date'}; } sub iscurrent { return $_[0]->{'iscurrent'}; } sub status_id { return $_[0]->{'case_run_status_id'}; } sub sortkey { return $_[0]->{'sortkey'}; } sub isprivate { return $_[0]->{'isprivate'}; } sub updated_deps { return $_[0]->{'updated_deps'}; } 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 type Returns 'case' =cut sub type { my $self = shift; $self->{'type'} = 'caserun'; return $self->{'type'}; } =head2 notes Returns the cumulative notes of all caserun records of this case and run. =cut sub notes { my $self = shift; my $dbh = Bugzilla->dbh; my $notes = $dbh->selectcol_arrayref( "SELECT notes FROM test_case_runs WHERE case_id = ? AND run_id = ? ORDER BY case_run_id", undef,($self->case_id, $self->run_id)); return join("\n", @$notes); } =head2 run Returns the TestRun object that this case-run is associated with =cut # The potential exists for creating a circular reference here. sub run { my $self = shift; return $self->{'run'} if exists $self->{'run'}; require Bugzilla::Extension::Testopia::TestRun; $self->{'run'} = Bugzilla::Extension::Testopia::TestRun->new($self->{'run_id'}); return $self->{'run'}; } =head2 case Returns the TestCase object that this case-run is associated with =cut # The potential exists for creating a circular reference here. sub case { my $self = shift; return $self->{'case'} if exists $self->{'case'}; require Bugzilla::Extension::Testopia::TestCase; $self->{'case'} = Bugzilla::Extension::Testopia::TestCase->new($self->{'case_id'}); return $self->{'case'}; } =head2 build Returns the Build object that this case-run is associated with =cut sub build { my $self = shift; return $self->{'build'} if exists $self->{'build'}; $self->{'build'} = Bugzilla::Extension::Testopia::Build->new($self->{'build_id'}); return $self->{'build'}; } =head2 environment Returns the Build object that this case-run is associated with =cut sub environment { my $self = shift; return $self->{'environment'} if exists $self->{'environment'}; $self->{'environment'} = Bugzilla::Extension::Testopia::Environment->new($self->{'environment_id'}); return $self->{'environment'}; } =head2 status Looks up the status name of the associated status_id for this object =cut sub status { my $self = shift; my $dbh = Bugzilla->dbh; ($self->{'status'}) = $dbh->selectrow_array( "SELECT name FROM test_case_run_status WHERE case_run_status_id=?", undef, $self->{'case_run_status_id'}); return $self->{'status'}; } =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->selectall_arrayref( "SELECT attachment_id, case_run_id FROM test_case_attachments WHERE case_id = ?", { Slice=>{} }, $self->case_id); my @attachments; foreach my $attach (@{$attachments}){ my $att = Bugzilla::Extension::Testopia::Attachment->new($attach->{attachment_id}); $att->{'caserun_id'} = $self->id if $attach->{case_run_id}; push @attachments, $att; } $self->{'attachments'} = \@attachments; return $self->{'attachments'}; } =head2 bugs Returns a list of Bugzilla::Bug objects associated with this case-run =cut sub bugs { my $self = shift; #return $self->{'bug'} if exists $self->{'bug'}; my $dbh = Bugzilla->dbh; my @bugs; my $bugids = $dbh->selectcol_arrayref("SELECT bug_id FROM test_case_bugs WHERE case_run_id=?", undef, $self->{'case_run_id'}); foreach my $bugid (@{$bugids}){ push @bugs, Bugzilla::Bug->new($bugid, Bugzilla->user->id) if Bugzilla->user->can_see_bug($bugid); } $self->{'bugs'} = \@bugs; #join(",", @$bugids); return $self->{'bugs'}; } =head2 bug_list Returns a comma separated list of bug ids associated with this case-run =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 DISTINCT bug_id FROM test_case_bugs WHERE case_run_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 bug_count Retuns a count of the bugs associated with this case-run =cut sub bug_count{ my $self = shift; return $self->{'bug_count'} if exists $self->{'bug_count'}; my $dbh = Bugzilla->dbh; $self->{'bug_count'} = $dbh->selectrow_array("SELECT COUNT(bug_id) FROM test_case_bugs WHERE case_run_id=?", undef, $self->{'case_run_id'}); return $self->{'bug_count'}; } =head2 get_buglist Returns a comma separated string off bug ids associated with this case-run =cut sub get_buglist { my $self = shift; my $dbh = Bugzilla->dbh; my $bugids = $dbh->selectcol_arrayref("SELECT bug_id FROM test_case_bugs WHERE case_run_id=?", undef, $self->{'case_run_id'}); return join(',', @{$bugids}); } =head2 is_open_status Returns true if the status of this case-run is an open status =cut sub is_open_status { my $self = shift; my $status = shift; my @open_status_list = (IDLE, RUNNING, PAUSED, ERROR); return 1 if lsearch(\@open_status_list, $status) > -1; } =head2 is_closed_status Returns true if the status of this case-run is a closed status =cut sub is_closed_status { my $self = shift; my $status = shift; my @closed_status_list = (PASSED, FAILED, BLOCKED); return 1 if lsearch(\@closed_status_list, $status) > -1; } =head2 canview Returns true if the logged in user has rights to view this case-run. =cut sub canview { my $self = shift; return 1 if Bugzilla->user->in_group('Testers'); return 1 if $self->run->plan->get_user_rights(Bugzilla->user->id) & TR_READ; return 0; } =head2 canedit Returns true if the logged in user has rights to edit this case-run. =cut sub canedit { my $self = shift; return 1 if Bugzilla->user->in_group('admin'); return 0 if $self->run->stop_date; return 1 if $self->run->plan->get_user_rights(Bugzilla->user->id) & TR_ADMIN; if ($self->status_id == RUNNING){ return 1 if $self->run->manager->id == Bugzilla->user->id; return 0 unless $self->assignee->id && $self->assignee->id == Bugzilla->user->id; } return 1 if Bugzilla->user->in_group('Testers'); return 1 if $self->run->plan->get_user_rights(Bugzilla->user->id) & TR_WRITE; return 0; } =head2 candelete Returns true if the logged in user has rights to delete this case-run. =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"}; return 1 if $self->run->plan->get_user_rights(Bugzilla->user->id) & TR_DELETE; return 0; } sub completion_time { my $self = shift; my $dbh = Bugzilla->dbh; if (str2time($self->running_date) && str2time($self->close_date)){ my $seconds = str2time($self->close_date) - str2time($self->running_date); return $seconds; } return 0; } ############################### #### Functions #### ############################### =head2 lookup_status Returns the status name of the given case_run_status_id =cut sub lookup_status { my ($status_id) = @_; detaint_natural($status_id); my $dbh = Bugzilla->dbh; my ($status) = $dbh->selectrow_array( "SELECT name FROM test_case_run_status WHERE case_run_status_id = ?", undef, $status_id); return $status; } =head2 lookup_status_by_name Returns the id of the status name passed. =cut sub lookup_status_by_name { my ($name) = @_; my $dbh = Bugzilla->dbh; trick_taint($name); my ($value) = $dbh->selectrow_array( "SELECT case_run_status_id FROM test_case_run_status WHERE name = ?", undef, $name); return $value; } 1; __END__ =head1 NAME Bugzilla::Extension::Testopia::TestCaseRun - Testopia Test Case Run object =head1 DESCRIPTION This module represents a test case run in Testopia. A test case run is a record in the test_case_runs table which joins test cases to test runs. Basically, for each test run a selction of test cases is made to be included in that run. As a test run progresses, testers set statuses on each of the cases in the run. If the build is changed on a case-run with a status, a clone of that case-run is made in the table for historical purposes. =head1 SYNOPSIS use Bugzilla::Extension::Testopia::TestCaseRun; $caserun = Bugzilla::Extension::Testopia::TestCaseRun->new($caserun_id); $caserun = Bugzilla::Extension::Testopia::TestCaseRun->new(\%caserun_hash); =cut =head1 METHODS =head2 new Instantiate a new case run. This takes a single argument either a test case ID or a reference to a hash containing keys identical to a test case-run's fields and desired values. =cut =head2 _init Private constructor for this object =cut =head1 SEE ALSO TestCase TestRun =head1 AUTHOR Greg Hendricks =cut