| # This Source Code Form is subject to the terms of the Mozilla Public |
| # License, v. 2.0. If a copy of the MPL was not distributed with this |
| # file, You can obtain one at http://mozilla.org/MPL/2.0/. |
| # |
| # This Source Code Form is "Incompatible With Secondary Licenses", as |
| # defined by the Mozilla Public License, v. 2.0. |
| |
| # This module tests Bugzilla/Search.pm. It uses various constants |
| # that are in Bugzilla::Test::Search::Constants, in xt/lib/. |
| # |
| # It does this by: |
| # 1) Creating a bunch of field values. Each field value is |
| # randomly named and fully unique. |
| # 2) Creating a bunch of bugs that use those unique field |
| # values. Each bug has different characteristics--see |
| # the comment above the NUM_BUGS constant for a description |
| # of each bug. |
| # 3) Running searches using the combination of every search operator against |
| # every field. The tests that we run are described by the TESTS constant. |
| # Some of the operator/field combinations are known to be broken-- |
| # these are listed in the KNOWN_BROKEN constant. |
| # 4) For each search, we make sure that certain bugs are contained in |
| # the search, and certain other bugs are not contained in the search. |
| # The code for the operator/field tests is mostly in |
| # Bugzilla::Test::Search::FieldTest. |
| # 5) After testing each operator/field combination's functionality, we |
| # do additional tests to make sure that there are no SQL injections |
| # possible via any operator/field combination. The code for the |
| # SQL Injection tests is in Bugzilla::Test::Search::InjectionTest. |
| # |
| # Generally, the only way that you should modify the behavior of this |
| # script is by modifying the constants. |
| |
| package Bugzilla::Test::Search; |
| |
| use strict; |
| use warnings; |
| use Bugzilla::Attachment; |
| use Bugzilla::Bug (); |
| use Bugzilla::Constants; |
| use Bugzilla::Field; |
| use Bugzilla::Field::Choice; |
| use Bugzilla::FlagType; |
| use Bugzilla::Group; |
| use Bugzilla::Install (); |
| use Bugzilla::Test::Search::Constants; |
| use Bugzilla::Test::Search::CustomTest; |
| use Bugzilla::Test::Search::FieldTestNormal; |
| use Bugzilla::Test::Search::OperatorTest; |
| use Bugzilla::User (); |
| use Bugzilla::Util qw(generate_random_password); |
| |
| use Carp; |
| use DateTime; |
| use Scalar::Util qw(blessed); |
| |
| ############### |
| # Constructor # |
| ############### |
| |
| sub new { |
| my ($class, $options) = @_; |
| return bless { options => $options }, $class; |
| } |
| |
| ############# |
| # Accessors # |
| ############# |
| |
| sub options { return $_[0]->{options} } |
| sub option { return $_[0]->{options}->{$_[1]} } |
| |
| sub num_tests { |
| my ($self) = @_; |
| my @top_operators = $self->top_level_operators; |
| my @all_operators = $self->all_operators; |
| my $top_operator_tests = $self->_total_operator_tests(\@top_operators); |
| my $all_operator_tests = $self->_total_operator_tests(\@all_operators); |
| |
| my @fields = $self->all_fields; |
| |
| # Basically, we run TESTS_PER_RUN tests for each field/operator combination. |
| my $top_combinations = $top_operator_tests * scalar(@fields); |
| my $all_combinations = $all_operator_tests * scalar(@fields); |
| # But we also have ORs, for which we run combinations^2 tests. |
| my $join_tests = $self->option('long') |
| ? ($top_combinations * $all_combinations) : 0; |
| # And AND tests, which means we run 2x $join_tests; |
| $join_tests = $join_tests * 2; |
| # Also, because of NOT tests and Normal tests, we run 3x $top_combinations. |
| my $basic_tests = $top_combinations * 3; |
| my $operator_field_tests = ($basic_tests + $join_tests) * TESTS_PER_RUN; |
| |
| # Then we test each field/operator combination for SQL injection. |
| my @injection_values = INJECTION_TESTS; |
| my $sql_injection_tests = scalar(@fields) * scalar(@top_operators) |
| * scalar(@injection_values) * NUM_SEARCH_TESTS; |
| |
| # This @{ [] } thing is the only reasonable way to get a count out of a |
| # constant array. |
| my $special_tests = scalar(@{ [SPECIAL_PARAM_TESTS, CUSTOM_SEARCH_TESTS] }) |
| * TESTS_PER_RUN; |
| |
| return $operator_field_tests + $sql_injection_tests + $special_tests; |
| } |
| |
| sub _total_operator_tests { |
| my ($self, $operators) = @_; |
| |
| # Some operators have more than one test. Find those ones and add |
| # them to the total operator tests |
| my $extra_operator_tests; |
| foreach my $operator (@$operators) { |
| my $tests = TESTS->{$operator}; |
| next if !$tests; |
| my $extra_num = scalar(@$tests) - 1; |
| $extra_operator_tests += $extra_num; |
| } |
| return scalar(@$operators) + $extra_operator_tests; |
| |
| } |
| |
| sub all_operators { |
| my ($self) = @_; |
| if (not $self->{all_operators}) { |
| |
| my @operators; |
| if (my $limit_operators = $self->option('operators')) { |
| @operators = split(',', $limit_operators); |
| } |
| else { |
| @operators = sort (keys %{ Bugzilla::Search::OPERATORS() }); |
| } |
| # "substr" is just a backwards-compatibility operator, same as "substring". |
| @operators = grep { $_ ne 'substr' } @operators; |
| $self->{all_operators} = \@operators; |
| } |
| return @{ $self->{all_operators} }; |
| } |
| |
| sub all_fields { |
| my $self = shift; |
| if (not $self->{all_fields}) { |
| $self->_create_custom_fields(); |
| my @fields = @{ Bugzilla->fields }; |
| @fields = sort { $a->name cmp $b->name } @fields; |
| $self->{all_fields} = \@fields; |
| } |
| return @{ $self->{all_fields} }; |
| } |
| |
| sub top_level_operators { |
| my ($self) = @_; |
| if (!$self->{top_level_operators}) { |
| my @operators; |
| my $limit_top = $self->option('top-operators'); |
| if ($limit_top) { |
| @operators = split(',', $limit_top); |
| } |
| else { |
| @operators = $self->all_operators; |
| } |
| $self->{top_level_operators} = \@operators; |
| } |
| return @{ $self->{top_level_operators} }; |
| } |
| |
| sub text_fields { |
| my ($self) = @_; |
| my @text_fields = grep { $_->type == FIELD_TYPE_TEXTAREA |
| or $_->type == FIELD_TYPE_FREETEXT } $self->all_fields; |
| @text_fields = map { $_->name } @text_fields; |
| push(@text_fields, qw(short_desc status_whiteboard bug_file_loc see_also)); |
| return @text_fields; |
| } |
| |
| sub bugs { |
| my $self = shift; |
| $self->{bugs} ||= [map { $self->_create_one_bug($_) } (1..NUM_BUGS)]; |
| return @{ $self->{bugs} }; |
| } |
| |
| # Get a numbered bug. |
| sub bug { |
| my ($self, $number) = @_; |
| return ($self->bugs)[$number - 1]; |
| } |
| |
| sub admin { |
| my $self = shift; |
| if (!$self->{admin_user}) { |
| my $admin = create_user("admin"); |
| Bugzilla::Install::make_admin($admin); |
| $self->{admin_user} = $admin; |
| } |
| # We send back a fresh object every time, to make sure that group |
| # memberships are always up-to-date. |
| return new Bugzilla::User($self->{admin_user}->id); |
| } |
| |
| sub nobody { |
| my $self = shift; |
| $self->{nobody} ||= Bugzilla::Group->create({ name => "nobody-" . random(), |
| description => "Nobody", isbuggroup => 1 }); |
| return $self->{nobody}; |
| } |
| sub everybody { |
| my ($self) = @_; |
| $self->{everybody} ||= create_group('To The Limit'); |
| return $self->{everybody}; |
| } |
| |
| sub bug_create_value { |
| my ($self, $number, $field) = @_; |
| $field = $field->name if blessed($field); |
| if ($number == 6 and $field ne 'alias') { |
| $number = 1; |
| } |
| my $extra_values = $self->_extra_bug_create_values->{$number}; |
| if (exists $extra_values->{$field}) { |
| return $extra_values->{$field}; |
| } |
| return $self->_bug_create_values->{$number}->{$field}; |
| } |
| sub bug_update_value { |
| my ($self, $number, $field) = @_; |
| $field = $field->name if blessed($field); |
| if ($number == 6 and $field ne 'alias') { |
| $number = 1; |
| } |
| return $self->_bug_update_values->{$number}->{$field}; |
| } |
| |
| # Values used to create the bugs. |
| sub _bug_create_values { |
| my $self = shift; |
| return $self->{bug_create_values} if $self->{bug_create_values}; |
| my %values; |
| foreach my $number (1..NUM_BUGS) { |
| $values{$number} = $self->_create_field_values($number, 'for create'); |
| } |
| $self->{bug_create_values} = \%values; |
| return $self->{bug_create_values}; |
| } |
| # Values as they existed on the bug, at creation time. Used by the |
| # changedfrom tests. |
| sub _extra_bug_create_values { |
| my $self = shift; |
| $self->{extra_bug_create_values} ||= { map { $_ => {} } (1..NUM_BUGS) }; |
| return $self->{extra_bug_create_values}; |
| } |
| |
| # Values used to update the bugs after they are created. |
| sub _bug_update_values { |
| my $self = shift; |
| return $self->{bug_update_values} if $self->{bug_update_values}; |
| my %values; |
| foreach my $number (1..NUM_BUGS) { |
| $values{$number} = $self->_create_field_values($number); |
| } |
| $self->{bug_update_values} = \%values; |
| return $self->{bug_update_values}; |
| } |
| |
| ############################## |
| # General Helper Subroutines # |
| ############################## |
| |
| sub random { |
| $_[0] ||= FIELD_SIZE; |
| generate_random_password(@_); |
| } |
| |
| # We need to use a custom timestamp for each create() and update(), |
| # because the database returns the same value for LOCALTIMESTAMP(0) |
| # for the entire transaction, and we need each created bug to have |
| # its own creation_ts and delta_ts. |
| sub timestamp { |
| my ($day, $second) = @_; |
| return DateTime->new( |
| year => 2037, |
| month => 1, |
| day => $day, |
| hour => 12, |
| minute => $second, |
| second => 0, |
| # We make it floating because the timezone doesn't matter for our uses, |
| # and we want totally consistent behavior across all possible machines. |
| time_zone => 'floating', |
| ); |
| } |
| |
| sub create_keyword { |
| my ($number) = @_; |
| return Bugzilla::Keyword->create({ |
| name => "$number-keyword-" . random(), |
| description => "Keyword $number" }); |
| } |
| |
| sub create_user { |
| my ($prefix) = @_; |
| my $user_name = $prefix . '-' . random(15) . "@" . random(12) |
| . "." . random(3); |
| my $user_realname = $prefix . '-' . random(); |
| my $user = Bugzilla::User->create({ |
| login_name => $user_name, |
| realname => $user_realname, |
| cryptpassword => '*', |
| }); |
| return $user; |
| } |
| |
| sub create_group { |
| my ($prefix) = @_; |
| return Bugzilla::Group->create({ |
| name => "$prefix-group-" . random(), description => "Everybody $prefix", |
| userregexp => '.*', isbuggroup => 1 }); |
| } |
| |
| sub create_legal_value { |
| my ($field, $number) = @_; |
| my $type = Bugzilla::Field::Choice->type($field); |
| my $field_name = $field->name; |
| return $type->create({ value => "$number-$field_name-" . random(), |
| is_open => 0 }); |
| } |
| |
| ######################### |
| # Custom Field Creation # |
| ######################### |
| |
| sub _create_custom_fields { |
| my ($self) = @_; |
| return if !$self->option('add-custom-fields'); |
| |
| while (my ($type, $name) = each %{ CUSTOM_FIELDS() }) { |
| my $exists = new Bugzilla::Field({ name => $name }); |
| next if $exists; |
| Bugzilla::Field->create({ |
| name => $name, |
| type => $type, |
| description => "Search Test Field $name", |
| enter_bug => 1, |
| custom => 1, |
| buglist => 1, |
| is_mandatory => 0, |
| }); |
| } |
| } |
| |
| ######################## |
| # Field Value Creation # |
| ######################## |
| |
| sub _create_field_values { |
| my ($self, $number, $for_create) = @_; |
| my $dbh = Bugzilla->dbh; |
| |
| Bugzilla->set_user($self->admin); |
| |
| my @selects = grep { $_->is_select } $self->all_fields; |
| my %values; |
| foreach my $field (@selects) { |
| next if $field->is_abnormal; |
| $values{$field->name} = create_legal_value($field, $number)->name; |
| } |
| |
| my $group = create_group($number); |
| $values{groups} = [$group->name]; |
| |
| $values{'keywords'} = create_keyword($number)->name; |
| |
| foreach my $field (qw(assigned_to qa_contact reporter cc)) { |
| $values{$field} = create_user("$number-$field")->login; |
| } |
| |
| my $classification = Bugzilla::Classification->create( |
| { name => "$number-classification-" . random() }); |
| $classification = $classification->name; |
| |
| my $version = "$number-version-" . random(); |
| my $milestone = "$number-tm-" . random(15); |
| my $product = Bugzilla::Product->create({ |
| name => "$number-product-" . random(), |
| description => 'Created by t/search.t', |
| defaultmilestone => $milestone, |
| classification => $classification, |
| version => $version, |
| allows_unconfirmed => 1, |
| }); |
| foreach my $item ($group, $self->nobody) { |
| $product->set_group_controls($item, |
| { membercontrol => CONTROLMAPSHOWN, |
| othercontrol => CONTROLMAPNA }); |
| } |
| # $product->update() is called lower down. |
| my $component = Bugzilla::Component->create({ |
| product => $product, name => "$number-component-" . random(), |
| initialowner => create_user("$number-defaultowner")->login, |
| initialqacontact => create_user("$number-defaultqa")->login, |
| initial_cc => [create_user("$number-initcc")->login], |
| description => "Component $number" }); |
| |
| $values{'product'} = $product->name; |
| $values{'component'} = $component->name; |
| $values{'target_milestone'} = $milestone; |
| $values{'version'} = $version; |
| |
| foreach my $field ($self->text_fields) { |
| # We don't add a - after $field for the text fields, because |
| # if we do, fulltext searching for short_desc pulls out |
| # "short_desc" as a word and matches it in every bug. |
| my $value = "$number-$field" . random(); |
| if ($field eq 'bug_file_loc' or $field eq 'see_also') { |
| $value = "http://$value-" . random(3) |
| . "/show_bug.cgi?id=$number"; |
| } |
| $values{$field} = $value; |
| } |
| $values{'tag'} = ["$number-tag-" . random()]; |
| |
| my @date_fields = grep { $_->type == FIELD_TYPE_DATETIME } $self->all_fields; |
| foreach my $field (@date_fields) { |
| # We use 03 as the month because that differs from our creation_ts, |
| # delta_ts, and deadline. (It's nice to have recognizable values |
| # for each field when debugging.) |
| my $second = $for_create ? $number : $number + 1; |
| $values{$field->name} = "2037-03-0$number 12:34:0$second"; |
| } |
| |
| $values{alias} = "$number-alias-" . random(12); |
| |
| # Prefixing the original comment with "description" makes the |
| # lesserthan and greaterthan tests behave predictably. |
| my $comm_prefix = $for_create ? "description-" : ''; |
| $values{comment} = "$comm_prefix$number-comment-" . random() |
| . ' ' . random(); |
| |
| my @flags; |
| my $setter = create_user("$number-setters.login_name"); |
| my $requestee = create_user("$number-requestees.login_name"); |
| $values{set_flags} = _create_flags($number, $setter, $requestee); |
| |
| my $month = $for_create ? "12" : "02"; |
| $values{'deadline'} = "2037-$month-0$number"; |
| my $estimate_times = $for_create ? 10 : 1; |
| $values{estimated_time} = $estimate_times * $number; |
| |
| $values{attachment} = _get_attach_values($number, $for_create); |
| |
| # Some things only happen on the first bug. |
| if ($number == 1) { |
| # We use 6 as the prefix for the extra values, because bug 6's values |
| # don't otherwise get used (since bug 6 is created as a clone of |
| # bug 1). This also makes sure that our greaterthan/lessthan |
| # tests work properly. |
| my $extra_group = create_group(6); |
| $product->set_group_controls($extra_group, |
| { membercontrol => CONTROLMAPSHOWN, |
| othercontrol => CONTROLMAPNA }); |
| $values{groups} = [$values{groups}->[0], $extra_group->name]; |
| my $extra_keyword = create_keyword(6); |
| $values{keywords} = [$values{keywords}, $extra_keyword->name]; |
| my $extra_cc = create_user("6-cc"); |
| $values{cc} = [$values{cc}, $extra_cc->login]; |
| my @multi_selects = grep { $_->type == FIELD_TYPE_MULTI_SELECT } |
| $self->all_fields; |
| foreach my $field (@multi_selects) { |
| my $new_value = create_legal_value($field, 6); |
| my $name = $field->name; |
| $values{$name} = [$values{$name}, $new_value->name]; |
| } |
| push(@{ $values{'tag'} }, "6-tag-" . random()); |
| } |
| |
| # On bug 5, any field that *can* be left empty, *is* left empty. |
| if ($number == 5) { |
| my @set_fields = grep { $_->type == FIELD_TYPE_SINGLE_SELECT } |
| $self->all_fields; |
| @set_fields = map { $_->name } @set_fields; |
| push(@set_fields, qw(short_desc version reporter)); |
| foreach my $key (keys %values) { |
| delete $values{$key} unless grep { $_ eq $key } @set_fields; |
| } |
| } |
| |
| $product->update(); |
| |
| return \%values; |
| } |
| |
| # Flags |
| sub _create_flags { |
| my ($number, $setter, $requestee) = @_; |
| |
| my $flagtypes = _create_flagtypes($number); |
| |
| my %flags; |
| foreach my $type (qw(a b)) { |
| $flags{$type} = _get_flag_values(@_, $flagtypes->{$type}); |
| } |
| return \%flags; |
| } |
| |
| sub _create_flagtypes { |
| my ($number) = @_; |
| my $dbh = Bugzilla->dbh; |
| my $name = "$number-flag-" . random(); |
| my $desc = "FlagType $number"; |
| |
| my %flagtypes; |
| foreach my $target (qw(a b)) { |
| $dbh->do("INSERT INTO flagtypes |
| (name, description, target_type, is_requestable, |
| is_requesteeble, is_multiplicable, cc_list) |
| VALUES (?,?,?,1,1,1,'')", |
| undef, $name, $desc, $target); |
| my $id = $dbh->bz_last_key('flagtypes', 'id'); |
| $dbh->do('INSERT INTO flaginclusions (type_id) VALUES (?)', |
| undef, $id); |
| my $flagtype = new Bugzilla::FlagType($id); |
| $flagtypes{$target} = $flagtype; |
| } |
| return \%flagtypes; |
| } |
| |
| sub _get_flag_values { |
| my ($number, $setter, $requestee, $flagtype) = @_; |
| |
| my @set_flags; |
| if ($number <= 2) { |
| foreach my $value (qw(? - + ?)) { |
| my $flag = { type_id => $flagtype->id, status => $value, |
| setter => $setter, flagtype => $flagtype }; |
| push(@set_flags, $flag); |
| } |
| $set_flags[0]->{requestee} = $requestee->login; |
| } |
| else { |
| @set_flags = ({ type_id => $flagtype->id, status => '+', |
| setter => $setter, flagtype => $flagtype }); |
| } |
| return \@set_flags; |
| } |
| |
| # Attachments |
| sub _get_attach_values { |
| my ($number, $for_create) = @_; |
| |
| my $boolean = $number == 1 ? 1 : 0; |
| if ($for_create) { |
| $boolean = !$boolean ? 1 : 0; |
| } |
| my $ispatch = $for_create ? 'ispatch' : 'is_patch'; |
| my $isobsolete = $for_create ? 'isobsolete' : 'is_obsolete'; |
| my $isprivate = $for_create ? 'isprivate' : 'is_private'; |
| my $mimetype = $for_create ? 'mimetype' : 'content_type'; |
| |
| my %values = ( |
| description => "$number-attach_desc-" . random(), |
| filename => "$number-filename-" . random(), |
| $ispatch => $boolean, |
| $isobsolete => $boolean, |
| $isprivate => $boolean, |
| $mimetype => "text/x-$number-" . random(), |
| ); |
| if ($for_create) { |
| $values{data} = "$number-data-" . random() . random(); |
| } |
| return \%values; |
| } |
| |
| ################ |
| # Bug Creation # |
| ################ |
| |
| sub _create_one_bug { |
| my ($self, $number) = @_; |
| my $dbh = Bugzilla->dbh; |
| |
| # We need bug 6 to have a unique alias that is not a clone of bug 1's, |
| # so we get the alias separately from the other parameters. |
| my $alias = $self->bug_create_value($number, 'alias'); |
| my $update_alias = $self->bug_update_value($number, 'alias'); |
| |
| # Otherwise, make bug 6 a clone of bug 1. |
| my $real_number = $number; |
| $number = 1 if $number == 6; |
| |
| my $reporter = $self->bug_create_value($number, 'reporter'); |
| Bugzilla->set_user(Bugzilla::User->check($reporter)); |
| |
| # We create the bug with one set of values, and then we change it |
| # to have different values. |
| my %params = %{ $self->_bug_create_values->{$number} }; |
| $params{alias} = $alias; |
| |
| # There are some things in bug_create_values that shouldn't go into |
| # create(). |
| delete @params{qw(attachment set_flags tag)}; |
| |
| my ($status, $resolution, $see_also) = |
| delete @params{qw(bug_status resolution see_also)}; |
| # All the bugs are created with everconfirmed = 0. |
| $params{bug_status} = 'UNCONFIRMED'; |
| my $bug = Bugzilla::Bug->create(\%params); |
| |
| # These are necessary for the changedfrom tests. |
| my $extra_values = $self->_extra_bug_create_values->{$number}; |
| foreach my $field (qw(comments remaining_time percentage_complete |
| keyword_objects everconfirmed dependson blocked |
| groups_in classification actual_time)) |
| { |
| $extra_values->{$field} = $bug->$field; |
| } |
| $extra_values->{reporter_accessible} = $number == 1 ? 0 : 1; |
| $extra_values->{cclist_accessible} = $number == 1 ? 0 : 1; |
| |
| if ($number == 5) { |
| # Bypass Bugzilla::Bug--we don't want any changes in bugs_activity |
| # for bug 5. |
| $dbh->do('UPDATE bugs SET qa_contact = NULL, reporter_accessible = 0, |
| cclist_accessible = 0 WHERE bug_id = ?', |
| undef, $bug->id); |
| $dbh->do('DELETE FROM cc WHERE bug_id = ?', undef, $bug->id); |
| my $ts = '1970-01-01 00:00:00'; |
| $dbh->do('UPDATE bugs SET creation_ts = ?, delta_ts = ? |
| WHERE bug_id = ?', undef, $ts, $ts, $bug->id); |
| $dbh->do('UPDATE longdescs SET bug_when = ? WHERE bug_id = ?', |
| undef, $ts, $bug->id); |
| $bug->{creation_ts} = $ts; |
| $extra_values->{see_also} = []; |
| } |
| else { |
| # Manually set the creation_ts so that each bug has a different one. |
| # |
| # Also, manually update the resolution and bug_status, because |
| # we want to see both of them change in bugs_activity, so we |
| # have to start with values for both (and as of the time when I'm |
| # writing this test, Bug->create doesn't support setting resolution). |
| # |
| # Same for see_also. |
| my $timestamp = timestamp($number, $number - 1); |
| my $creation_ts = $timestamp->ymd . ' ' . $timestamp->hms; |
| $bug->{creation_ts} = $creation_ts; |
| $dbh->do('UPDATE longdescs SET bug_when = ? WHERE bug_id = ?', |
| undef, $creation_ts, $bug->id); |
| $dbh->do('UPDATE bugs SET creation_ts = ?, bug_status = ?, |
| resolution = ? WHERE bug_id = ?', |
| undef, $creation_ts, $status, $resolution, $bug->id); |
| $dbh->do('INSERT INTO bug_see_also (bug_id, value, class) VALUES (?,?,?)', |
| undef, $bug->id, $see_also, 'Bugzilla::BugUrl::Bugzilla'); |
| $extra_values->{see_also} = $bug->see_also; |
| |
| # All the tags must be created as the admin user, so that the |
| # admin user can find them, later. |
| my $original_user = Bugzilla->user; |
| Bugzilla->set_user($self->admin); |
| my $tags = $self->bug_create_value($number, 'tag'); |
| $bug->add_tag($_) foreach @$tags; |
| $extra_values->{tags} = $tags; |
| Bugzilla->set_user($original_user); |
| |
| if ($number == 1) { |
| # Bug 1 needs to start off with reporter_accessible and |
| # cclist_accessible being 0, so that when we change them to 1, |
| # that change shows up in bugs_activity. |
| $dbh->do('UPDATE bugs SET reporter_accessible = 0, |
| cclist_accessible = 0 WHERE bug_id = ?', |
| undef, $bug->id); |
| # Bug 1 gets three comments, so that longdescs.count matches it |
| # uniquely. The third comment is added in the middle, so that the |
| # last comment contains all of the important data, like work_time. |
| $bug->add_comment("1-comment-" . random(100)); |
| } |
| |
| my %update_params = %{ $self->_bug_update_values->{$number} }; |
| my %reverse_map = reverse %{ Bugzilla::Bug->FIELD_MAP }; |
| foreach my $db_name (keys %reverse_map) { |
| next if $db_name eq 'comment'; |
| next if $db_name eq 'status_whiteboard'; |
| if (exists $update_params{$db_name}) { |
| my $update_name = $reverse_map{$db_name}; |
| $update_params{$update_name} = delete $update_params{$db_name}; |
| } |
| } |
| |
| my ($new_status, $new_res) = |
| delete @update_params{qw(status resolution)}; |
| # Bypass the status workflow. |
| $bug->{bug_status} = $new_status; |
| $bug->{resolution} = $new_res; |
| $bug->{everconfirmed} = 1 if $number == 1; |
| |
| # add/remove/set fields. |
| $update_params{keywords} = { set => $update_params{keywords} }; |
| $update_params{groups} = { add => $update_params{groups}, |
| remove => $bug->groups_in }; |
| my @cc_remove = map { $_->login } @{ $bug->cc_users }; |
| my $cc_new = $update_params{cc}; |
| my @cc_add = ref($cc_new) ? @$cc_new : ($cc_new); |
| # We make the admin an explicit CC on bug 1 (but not on bug 6), so |
| # that we can test the %user% pronoun properly. |
| if ($real_number == 1) { |
| push(@cc_add, $self->admin->login); |
| } |
| $update_params{cc} = { add => \@cc_add, remove => \@cc_remove }; |
| my $see_also_remove = $bug->see_also; |
| my $see_also_add = [$update_params{see_also}]; |
| $update_params{see_also} = { add => $see_also_add, |
| remove => $see_also_remove }; |
| $update_params{comment} = { body => $update_params{comment} }; |
| $update_params{work_time} = $number; |
| # Setting work_time kills the remaining_time, so we need to |
| # preserve that. We add 8 because that produces an integer |
| # percentage_complete for bug 1, which is necessary for |
| # accurate "equals"-type searching. |
| $update_params{remaining_time} = $number + 8; |
| $update_params{reporter_accessible} = $number == 1 ? 1 : 0; |
| $update_params{cclist_accessible} = $number == 1 ? 1 : 0; |
| $update_params{alias} = $update_alias; |
| |
| $bug->set_all(\%update_params); |
| my $flags = $self->bug_create_value($number, 'set_flags')->{b}; |
| $bug->set_flags([], $flags); |
| $timestamp->set(second => $number); |
| $bug->update($timestamp->ymd . ' ' . $timestamp->hms); |
| $extra_values->{flags} = $bug->flags; |
| |
| # It's not generally safe to do update() multiple times on |
| # the same Bug object. |
| $bug = new Bugzilla::Bug($bug->id); |
| my $update_flags = $self->bug_update_value($number, 'set_flags')->{b}; |
| $_->{status} = 'X' foreach @{ $bug->flags }; |
| $bug->set_flags($bug->flags, $update_flags); |
| if ($number == 1) { |
| my $comment_id = $bug->comments->[-1]->id; |
| $bug->set_comment_is_private({ $comment_id => 1 }); |
| } |
| $bug->update($bug->delta_ts); |
| |
| my $attach_create = $self->bug_create_value($number, 'attachment'); |
| my $attachment = Bugzilla::Attachment->create({ |
| bug => $bug, |
| creation_ts => $creation_ts, |
| %$attach_create }); |
| # Store for the changedfrom tests. |
| $extra_values->{attachments} = |
| [new Bugzilla::Attachment($attachment->id)]; |
| |
| my $attach_update = $self->bug_update_value($number, 'attachment'); |
| $attachment->set_all($attach_update); |
| # In order to keep the mimetype on the ispatch attachment, |
| # we need to bypass the validator. |
| $attachment->{mimetype} = $attach_update->{content_type}; |
| my $attach_flags = $self->bug_update_value($number, 'set_flags')->{a}; |
| $attachment->set_flags([], $attach_flags); |
| $attachment->update($bug->delta_ts); |
| } |
| |
| # Values for changedfrom. |
| $extra_values->{creation_ts} = $bug->creation_ts; |
| $extra_values->{delta_ts} = $bug->creation_ts; |
| |
| return new Bugzilla::Bug($bug->id); |
| } |
| |
| ################################### |
| # Test::Builder Memory Efficiency # |
| ################################### |
| |
| # Test::Builder stores information for each test run, but Test::Harness |
| # and TAP::Harness don't actually need this information. When we run 60 |
| # million tests, the history eats up all our memory. (After about |
| # 1 million tests, memory usage is around 1 GB.) |
| # |
| # The only part of the history that Test::More actually *uses* is the "ok" |
| # field, which we store more efficiently, in an array, and then we re-populate |
| # the Test_Results in Test::Builder at the end of the test. |
| sub clean_test_history { |
| my ($self) = @_; |
| return if !$self->option('long'); |
| my $builder = Test::More->builder; |
| my $current_test = $builder->current_test; |
| |
| # I don't use details() because I don't want to copy the array. |
| my $results = $builder->{Test_Results}; |
| my $check_test = $current_test - 1; |
| while (my $result = $results->[$check_test]) { |
| last if !$result; |
| $self->test_success($check_test, $result->{ok}); |
| $check_test--; |
| } |
| |
| # Truncate the test history array, but retain the current test number. |
| $builder->{Test_Results} = []; |
| $builder->{Curr_Test} = $current_test; |
| } |
| |
| sub test_success { |
| my ($self, $index, $status) = @_; |
| $self->{test_success}->[$index] = $status; |
| return $self->{test_success}; |
| } |
| |
| sub repopulate_test_results { |
| my ($self) = @_; |
| return if !$self->option('long'); |
| $self->clean_test_history(); |
| # We create only two hashes, for memory efficiency. |
| my %ok = ( ok => 1 ); |
| my %not_ok = ( ok => 0 ); |
| my @results; |
| foreach my $success (@{ $self->{test_success} }) { |
| push(@results, $success ? \%ok : \%not_ok); |
| } |
| my $builder = Test::More->builder; |
| $builder->{Test_Results} = \@results; |
| } |
| |
| ########## |
| # Caches # |
| ########## |
| |
| # When doing AND and OR tests, we essentially test the same field/operator |
| # combinations over and over. So, if we're going to be running those tests, |
| # we cache the translated_value of the FieldTests globally so that we don't |
| # have to re-run the value-translation code every time (which can be pretty |
| # slow). |
| sub value_translation_cache { |
| my ($self, $field_test, $value) = @_; |
| return if !$self->option('long'); |
| my $test_name = $field_test->name; |
| if (@_ == 3) { |
| $self->{value_translation_cache}->{$test_name} = $value; |
| } |
| return $self->{value_translation_cache}->{$test_name}; |
| } |
| |
| # When doing AND/OR tests, the value for transformed_value_was_equal |
| # (see Bugzilla::Test::Search::FieldTest) won't be recalculated |
| # if we pull our values from the value_translation_cache. So we need |
| # to also cache the values for transformed_value_was_equal. |
| sub was_equal_cache { |
| my ($self, $field_test, $number, $value) = @_; |
| return if !$self->option('long'); |
| my $test_name = $field_test->name; |
| if (@_ == 4) { |
| $self->{tvwe_cache}->{$test_name}->{$number} = $value; |
| } |
| return $self->{tvwe_cache}->{$test_name}->{$number}; |
| } |
| |
| ############# |
| # Main Test # |
| ############# |
| |
| sub run { |
| my ($self) = @_; |
| my $dbh = Bugzilla->dbh; |
| |
| # We want backtraces on any "die" message or any warning. |
| # Otherwise it's hard to trace errors inside of Bugzilla::Search from |
| # reading automated test run results. |
| local $SIG{__WARN__} = \&Carp::cluck; |
| local $SIG{__DIE__} = \&Carp::confess; |
| |
| $dbh->bz_start_transaction(); |
| |
| # Some parameters need to be set in order for the tests to function |
| # properly. |
| my $everybody = $self->everybody; |
| my $params = Bugzilla->params; |
| local $params->{'useclassification'} = 1; |
| local $params->{'useqacontact'} = 1; |
| local $params->{'usetargetmilestone'} = 1; |
| local $params->{'mail_delivery_method'} = 'None'; |
| local $params->{'timetrackinggroup'} = $everybody->name; |
| local $params->{'insidergroup'} = $everybody->name; |
| |
| $self->_setup_bugs(); |
| |
| # Even though _setup_bugs set us as an admin, we want to be sure at |
| # this point that we have an admin with refreshed group memberships. |
| Bugzilla->set_user($self->admin); |
| foreach my $test (CUSTOM_SEARCH_TESTS) { |
| my $custom_test = new Bugzilla::Test::Search::CustomTest($test, $self); |
| $custom_test->run(); |
| } |
| foreach my $test (SPECIAL_PARAM_TESTS) { |
| my $operator_test = |
| new Bugzilla::Test::Search::OperatorTest($test->{operator}, $self); |
| my $field = Bugzilla::Field->check($test->{field}); |
| my $special_test = new Bugzilla::Test::Search::FieldTestNormal( |
| $operator_test, $field, $test); |
| $special_test->run(); |
| } |
| foreach my $operator ($self->top_level_operators) { |
| my $operator_test = |
| new Bugzilla::Test::Search::OperatorTest($operator, $self); |
| $operator_test->run(); |
| } |
| |
| # Rollbacks won't get rid of bugs_fulltext entries, so we do that ourselves. |
| my @bug_ids = map { $_->id } $self->bugs; |
| my $bug_id_string = join(',', @bug_ids); |
| $dbh->do("DELETE FROM bugs_fulltext WHERE bug_id IN ($bug_id_string)"); |
| $dbh->bz_rollback_transaction(); |
| $self->repopulate_test_results(); |
| } |
| |
| # This makes a few changes to the bugs after they're created--changes |
| # that can only be done after all the bugs have been created. |
| sub _setup_bugs { |
| my ($self) = @_; |
| $self->_setup_dependencies(); |
| $self->_set_bug_id_fields(); |
| $self->_protect_bug_6(); |
| } |
| sub _setup_dependencies { |
| my ($self) = @_; |
| my $dbh = Bugzilla->dbh; |
| |
| # Set up depedency relationships between the bugs. |
| # Bug 1 + 6 depend on bug 2 and block bug 3. |
| my $bug2 = $self->bug(2); |
| my $bug3 = $self->bug(3); |
| foreach my $number (1,6) { |
| my $bug = $self->bug($number); |
| my @original_delta = ($bug2->delta_ts, $bug3->delta_ts); |
| Bugzilla->set_user($bug->reporter); |
| $bug->set_dependencies([$bug2->id], [$bug3->id]); |
| $bug->update($bug->delta_ts); |
| # Setting dependencies changed the delta_ts on bug2 and bug3, so |
| # re-set them back to what they were before. However, we leave |
| # the correct update times in bugs_activity, so that the changed* |
| # searches still work right. |
| my $set_delta = $dbh->prepare( |
| 'UPDATE bugs SET delta_ts = ? WHERE bug_id = ?'); |
| foreach my $row ([$original_delta[0], $bug2->id], |
| [$original_delta[1], $bug3->id]) |
| { |
| $set_delta->execute(@$row); |
| } |
| } |
| } |
| |
| sub _set_bug_id_fields { |
| my ($self) = @_; |
| # BUG_ID fields couldn't be set before, because before we create bug 1, |
| # we don't necessarily have any valid bug ids.) |
| my @bug_id_fields = grep { $_->type == FIELD_TYPE_BUG_ID } |
| $self->all_fields; |
| foreach my $number (1..NUM_BUGS) { |
| my $bug = $self->bug($number); |
| $number = 1 if $number == 6; |
| next if $number == 5; |
| my $other_bug = $self->bug($number + 1); |
| Bugzilla->set_user($bug->reporter); |
| foreach my $field (@bug_id_fields) { |
| $bug->set_custom_field($field, $other_bug->id); |
| $bug->update($bug->delta_ts); |
| } |
| } |
| } |
| |
| sub _protect_bug_6 { |
| my ($self) = @_; |
| my $dbh = Bugzilla->dbh; |
| |
| Bugzilla->set_user($self->admin); |
| |
| # Put bug6 in the nobody group. |
| my $nobody = $self->nobody; |
| # We pull it newly from the DB to be sure it's safe to call update() |
| # on. |
| my $bug6 = new Bugzilla::Bug($self->bug(6)->id); |
| $bug6->add_group($nobody); |
| $bug6->update($bug6->delta_ts); |
| |
| # Remove the admin (and everybody else) from the $nobody group. |
| $dbh->do('DELETE FROM group_group_map |
| WHERE grantor_id = ? OR member_id = ?', undef, |
| $nobody->id, $nobody->id); |
| } |
| |
| 1; |