# -*- 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. # # Contributor(s): Greg Hendricks =head1 NAME Bugzilla::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::Testopia::TestCaseRun; $caserun = Bugzilla::Testopia::TestCaseRun->new($caserun_id); $caserun = Bugzilla::Testopia::TestCaseRun->new(\%caserun_hash); =cut package Bugzilla::Testopia::TestCaseRun; use strict; use Bugzilla::Util; use Bugzilla::Error; use Bugzilla::User; use Bugzilla::Config; use Bugzilla::Testopia::Util; use Bugzilla::Testopia::Constants; use Date::Format; ############################### #### Initialization #### ############################### =head1 FIELDS case_run_id run_id case_id assignee testedby case_run_status_id case_text_version build_id notes close_date iscurrent sortkey =cut use constant DB_COLUMNS => qw( test_case_runs.case_run_id test_case_runs.run_id test_case_runs.case_id test_case_runs.assignee test_case_runs.testedby test_case_runs.case_run_status_id test_case_runs.case_text_version test_case_runs.build_id test_case_runs.environment_id test_case_runs.notes test_case_runs.close_date test_case_runs.iscurrent test_case_runs.sortkey ); our $columns = join(", ", DB_COLUMNS); sub report_columns { my $self = shift; my %columns; # Changes here need to match Report.pm $columns{'Build'} = "build"; $columns{'Status'} = "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; } ############################### #### Methods #### ############################### =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 sub new { my $invocant = shift; my $class = ref($invocant) || $invocant; my $self = {}; bless($self, $class); return $self->_init(@_); } =head2 _init Private constructor for this object =cut sub _init { my $self = shift; my ($param) = (@_); my $dbh = Bugzilla->dbh; my $id = $param unless (ref $param eq 'HASH'); my $obj; if (defined $id && detaint_natural($id)) { $obj = $dbh->selectrow_hashref(qq{ SELECT $columns FROM test_case_runs WHERE case_run_id = ?}, undef, $id); } elsif (ref $param eq 'HASH'){ $obj = $param; } else { Bugzilla::Error::ThrowCodeError('bad_arg', {argument => 'param', function => 'Testopia::TestCaseRun::_init'}); } return undef unless (defined $obj); foreach my $field (keys %$obj) { $self->{$field} = $obj->{$field}; } return $self; } =head2 store Stores a test case run object in the database. This method is used to store a newly created test case run. It returns the new id. =cut sub store { my $self = shift; my $dbh = Bugzilla->dbh; $dbh->do("INSERT INTO test_case_runs ($columns) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)", undef, (undef, $self->{'run_id'}, $self->{'case_id'}, $self->{'assignee'}, undef, IDLE, $self->{'case_text_version'}, $self->{'build_id'}, $self->{'environment_id'}, undef, undef, 1, 0)); my $key = $dbh->bz_last_key( 'test_case_runs', 'case_run_id' ); return $key; } =head2 clone Creates a copy of this caserun and sets it as the current record =cut sub clone { my $self = shift; my ($fields) = @_; my $dbh = Bugzilla->dbh; my $note = "Build or Environment changed. Resetting to IDLE."; $self->append_note($note); $self->{'build_id'} = $fields->{'build_id'} if $fields->{'build_id'}; $self->{'environment_id'} = $fields->{'environment_id'} if $fields->{'environment_id'}; my $entry = $self->store; $self->set_as_current($entry); return $entry; } =head2 check_exists Checks for an existing entry with the same build and environment for this case and run and returns the id if it is found =cut sub check_exists { my $self = shift; my ($run_id, $case_id, $build_id, $env_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)); return $is; } =head2 lookup_case_run_id Checks for an existing entry based on run, case, and build, and returns the id if it is found =cut sub lookup_case_run_id { my ($run_id, $case_id, $build_id) = @_; my $dbh = Bugzilla->dbh; my ($case_run_id) = $dbh->selectrow_array( "SELECT case_run_id FROM test_case_runs WHERE run_id = ? AND case_id = ? AND build_id = ?", undef, ($run_id, $case_id, $build_id)); return $case_run_id; } =head2 update Update this case-run in the database. This method checks which fields have been changed and either creates a clone of the case-run or updates the existing one. =cut sub update { my $self = shift; my ($fields) = @_; my $dbh = Bugzilla->dbh; if ($self->is_closed_status($fields->{'case_run_status_id'})){ $fields->{'close_date'} = Bugzilla::Testopia::Util::get_time_stamp(); } my ($is) = $self->check_exists($self->run_id, $self->case_id, $fields->{'build_id'}, $fields->{'environment_id'}); if ($fields->{'build_id'} != $self->{'build_id'} || $fields->{'environment_id'} != $self->{'environment_id'}){ if ($is){ return $is; } if ($self->{'case_run_status_id'} != IDLE){ return $self->clone($fields); } } return $self->_update_fields($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_lock_tables('test_case_runs WRITE'); foreach my $field (keys %{$newvalues}){ $dbh->do("UPDATE test_case_runs SET $field = ? WHERE case_run_id = ?", undef, $newvalues->{$field}, $self->{'case_run_id'}); } $dbh->bz_unlock_tables(); 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; my $list = $self->get_case_run_list; $dbh->bz_lock_tables('test_case_runs WRITE'); foreach my $c (@{$list}){ $dbh->do("UPDATE test_case_runs SET iscurrent = 0 WHERE case_run_id = ?", undef, $c); } $dbh->do("UPDATE test_case_runs SET iscurrent = 1 WHERE case_run_id = ?", undef, $caserun); $dbh->bz_unlock_tables(); } =head2 set_build Sets the build on a case-run =cut sub set_build { my $self = shift; my ($build_id) = @_; $self->_update_fields({'build_id' => $build_id}); $self->{'build_id'} = $build_id; } =head2 set_environment Sets the environment on a case-run =cut sub set_environment { my $self = shift; my ($env_id) = @_; $self->_update_fields({'environment_id' => $env_id}); $self->{'environment_id'} = $env_id; } =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) = @_; $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){ $self->_update_fields({'close_date' => undef}); $self->{'close_date'} = undef; } else { my $timestamp = Bugzilla::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->{'case_run_status_id'} = $status_id; $self->{'status'} = undef; } =head2 set_assignee Sets the assigned tester for the case-run =cut sub set_assignee { my $self = shift; my ($user_id) = @_; $self->_update_fields({'assignee' => $user_id}); } =head2 lookup_status Returns the status name of the given case_run_status_id =cut sub lookup_status { my $self = shift; my ($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; my ($value) = $dbh->selectrow_array( "SELECT case_run_status_id FROM test_case_run_status WHERE name = ?", undef, $name); return $value; } =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_lock_tables("test_case_runs WRITE"); 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_unlock_tables; $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; } =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 ($bug, $caserun_id) = @_; $caserun_id ||= $self->{'case_run_id'}; my $dbh = Bugzilla->dbh; $dbh->bz_lock_tables('test_case_bugs WRITE'); 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_unlock_tables(); 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_unlock_tables(); } =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; $dbh->do("DELETE FROM test_case_bugs WHERE bug_id = ? AND case_run_id = ?", undef, ($bug, $self->{'case_run_id'})); } =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}); } ############################### #### 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 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'}; } =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'}; $self->{'run'} = Bugzilla::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'}; $self->{'case'} = Bugzilla::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::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::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 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 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 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); 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 $self->{'canview'} if exists $self->{'canview'}; # my ($case_log_id, $run_id, $plan_id, $current_user_id) = @_; # # my $dbh = Bugzilla->dbh; # my $canview = 0; # my $current_user_id = Bugzilla->user->id; # my ($plan_id) = $dbh->selectrow_array("SELECT plan_id FROM test_runs # WHERE run_id=?", # undef, $self->{'test_run_id'}); # # if (0 == &Bugzilla::Param('private-cases-log')) { # $canview = 1; # } else { # # if (0 == $self->{'isprivate'}) { # # if !isprivate, then everybody can run it and should be able to see # # the current status # $canview = 1; # } else { # # check is the current user is a tester: # if (defined $current_user_id) { # # SendSQL("select 1 from test_case_run_testers ". # "where case_log_id=". $self->{'id'} ." and userid=$current_user_id"); # # if (FetchOneColumn()) { # # current user is a tester # $canview = 1; # } else { # # check editors # SendSQL("select 1 from test_plans where plan_id=$plan_id and editor=$current_user_id"); # # if (FetchOneColumn()) { # $canview = 1; # } else { # # check watchers # SendSQL("select 1 from test_plans_watchers where plan_id=$plan_id and userid=$current_user_id"); # if (FetchOneColumn()) { # $canview = 1; # } else { # #check test run manager # SendSQL("select 1 from test_runs where test_run_id=". $self->{'test_run_id'} ." and manager=$current_user_id"); # $canview = FetchOneColumn()? 1 : 0; # # if (0 == $canview) { # if (UserInGroup('admin')) { # $canview = 1; # } # } # } # } # } # } # } # } $self->{'canview'} = 1; return $self->{'canview'}; } =head2 canedit Returns true if the logged in user has rights to edit this case-run. =cut sub canedit { my $self = shift; return !$self->run->stop_date && $self->canview && (UserInGroup('managetestplans') || UserInGroup('edittestcases') || UserInGroup('runtests')); } =head2 candelete Returns true if the logged in user has rights to delete this case-run. =cut sub candelete { my $self = shift; return 0 unless $self->canedit && Param("allow-test-deletion"); return 1 if Bugzilla->user->in_group("admin"); return 1 if Bugzilla->user->id == $self->run->manager->id; return 0; } =head2 obliterate Removes this caserun, it's history, and all things that reference it. =cut sub obliterate { my $self = shift; my $dbh = Bugzilla->dbh; $dbh->do("DELETE FROM test_case_bugs WHERE case_run_id IN (" . join(",", @{$self->get_case_run_list}) . ")", undef, $self->id); $dbh->do("DELETE FROM test_case_runs WHERE case_id = ? AND run_id = ?", undef, ($self->case_id, $self->run_id)); return 1; } =head1 SEE ALSO TestCase TestRun =head1 AUTHOR Greg Hendricks =cut 1;