1# This Source Code Form is subject to the terms of the Mozilla Public 2# License, v. 2.0. If a copy of the MPL was not distributed with this 3# file, You can obtain one at http://mozilla.org/MPL/2.0/. 4# 5# This Source Code Form is "Incompatible With Secondary Licenses", as 6# defined by the Mozilla Public License, v. 2.0. 7 8# This module tests Bugzilla/Search.pm. It uses various constants 9# that are in Bugzilla::Test::Search::Constants, in xt/lib/. 10# 11# It does this by: 12# 1) Creating a bunch of field values. Each field value is 13# randomly named and fully unique. 14# 2) Creating a bunch of bugs that use those unique field 15# values. Each bug has different characteristics--see 16# the comment above the NUM_BUGS constant for a description 17# of each bug. 18# 3) Running searches using the combination of every search operator against 19# every field. The tests that we run are described by the TESTS constant. 20# Some of the operator/field combinations are known to be broken-- 21# these are listed in the KNOWN_BROKEN constant. 22# 4) For each search, we make sure that certain bugs are contained in 23# the search, and certain other bugs are not contained in the search. 24# The code for the operator/field tests is mostly in 25# Bugzilla::Test::Search::FieldTest. 26# 5) After testing each operator/field combination's functionality, we 27# do additional tests to make sure that there are no SQL injections 28# possible via any operator/field combination. The code for the 29# SQL Injection tests is in Bugzilla::Test::Search::InjectionTest. 30# 31# Generally, the only way that you should modify the behavior of this 32# script is by modifying the constants. 33 34package Bugzilla::Test::Search; 35 36use strict; 37use warnings; 38use Bugzilla::Attachment; 39use Bugzilla::Bug (); 40use Bugzilla::Constants; 41use Bugzilla::Field; 42use Bugzilla::Field::Choice; 43use Bugzilla::FlagType; 44use Bugzilla::Group; 45use Bugzilla::Install (); 46use Bugzilla::Test::Search::Constants; 47use Bugzilla::Test::Search::CustomTest; 48use Bugzilla::Test::Search::FieldTestNormal; 49use Bugzilla::Test::Search::OperatorTest; 50use Bugzilla::User (); 51use Bugzilla::Util qw(generate_random_password); 52 53use Carp; 54use DateTime; 55use Scalar::Util qw(blessed); 56 57############### 58# Constructor # 59############### 60 61sub new { 62 my ($class, $options) = @_; 63 return bless { options => $options }, $class; 64} 65 66############# 67# Accessors # 68############# 69 70sub options { return $_[0]->{options} } 71sub option { return $_[0]->{options}->{$_[1]} } 72 73sub num_tests { 74 my ($self) = @_; 75 my @top_operators = $self->top_level_operators; 76 my @all_operators = $self->all_operators; 77 my $top_operator_tests = $self->_total_operator_tests(\@top_operators); 78 my $all_operator_tests = $self->_total_operator_tests(\@all_operators); 79 80 my @fields = $self->all_fields; 81 82 # Basically, we run TESTS_PER_RUN tests for each field/operator combination. 83 my $top_combinations = $top_operator_tests * scalar(@fields); 84 my $all_combinations = $all_operator_tests * scalar(@fields); 85 # But we also have ORs, for which we run combinations^2 tests. 86 my $join_tests = $self->option('long') 87 ? ($top_combinations * $all_combinations) : 0; 88 # And AND tests, which means we run 2x $join_tests; 89 $join_tests = $join_tests * 2; 90 # Also, because of NOT tests and Normal tests, we run 3x $top_combinations. 91 my $basic_tests = $top_combinations * 3; 92 my $operator_field_tests = ($basic_tests + $join_tests) * TESTS_PER_RUN; 93 94 # Then we test each field/operator combination for SQL injection. 95 my @injection_values = INJECTION_TESTS; 96 my $sql_injection_tests = scalar(@fields) * scalar(@top_operators) 97 * scalar(@injection_values) * NUM_SEARCH_TESTS; 98 99 # This @{ [] } thing is the only reasonable way to get a count out of a 100 # constant array. 101 my $special_tests = scalar(@{ [SPECIAL_PARAM_TESTS, CUSTOM_SEARCH_TESTS] }) 102 * TESTS_PER_RUN; 103 104 return $operator_field_tests + $sql_injection_tests + $special_tests; 105} 106 107sub _total_operator_tests { 108 my ($self, $operators) = @_; 109 110 # Some operators have more than one test. Find those ones and add 111 # them to the total operator tests 112 my $extra_operator_tests; 113 foreach my $operator (@$operators) { 114 my $tests = TESTS->{$operator}; 115 next if !$tests; 116 my $extra_num = scalar(@$tests) - 1; 117 $extra_operator_tests += $extra_num; 118 } 119 return scalar(@$operators) + $extra_operator_tests; 120 121} 122 123sub all_operators { 124 my ($self) = @_; 125 if (not $self->{all_operators}) { 126 127 my @operators; 128 if (my $limit_operators = $self->option('operators')) { 129 @operators = split(',', $limit_operators); 130 } 131 else { 132 @operators = sort (keys %{ Bugzilla::Search::OPERATORS() }); 133 } 134 # "substr" is just a backwards-compatibility operator, same as "substring". 135 @operators = grep { $_ ne 'substr' } @operators; 136 $self->{all_operators} = \@operators; 137 } 138 return @{ $self->{all_operators} }; 139} 140 141sub all_fields { 142 my $self = shift; 143 if (not $self->{all_fields}) { 144 $self->_create_custom_fields(); 145 my @fields = @{ Bugzilla->fields }; 146 @fields = sort { $a->name cmp $b->name } @fields; 147 $self->{all_fields} = \@fields; 148 } 149 return @{ $self->{all_fields} }; 150} 151 152sub top_level_operators { 153 my ($self) = @_; 154 if (!$self->{top_level_operators}) { 155 my @operators; 156 my $limit_top = $self->option('top-operators'); 157 if ($limit_top) { 158 @operators = split(',', $limit_top); 159 } 160 else { 161 @operators = $self->all_operators; 162 } 163 $self->{top_level_operators} = \@operators; 164 } 165 return @{ $self->{top_level_operators} }; 166} 167 168sub text_fields { 169 my ($self) = @_; 170 my @text_fields = grep { $_->type == FIELD_TYPE_TEXTAREA 171 or $_->type == FIELD_TYPE_FREETEXT } $self->all_fields; 172 @text_fields = map { $_->name } @text_fields; 173 push(@text_fields, qw(short_desc status_whiteboard bug_file_loc see_also)); 174 return @text_fields; 175} 176 177sub bugs { 178 my $self = shift; 179 $self->{bugs} ||= [map { $self->_create_one_bug($_) } (1..NUM_BUGS)]; 180 return @{ $self->{bugs} }; 181} 182 183# Get a numbered bug. 184sub bug { 185 my ($self, $number) = @_; 186 return ($self->bugs)[$number - 1]; 187} 188 189sub admin { 190 my $self = shift; 191 if (!$self->{admin_user}) { 192 my $admin = create_user("admin"); 193 Bugzilla::Install::make_admin($admin); 194 $self->{admin_user} = $admin; 195 } 196 # We send back a fresh object every time, to make sure that group 197 # memberships are always up-to-date. 198 return new Bugzilla::User($self->{admin_user}->id); 199} 200 201sub nobody { 202 my $self = shift; 203 $self->{nobody} ||= Bugzilla::Group->create({ name => "nobody-" . random(), 204 description => "Nobody", isbuggroup => 1 }); 205 return $self->{nobody}; 206} 207sub everybody { 208 my ($self) = @_; 209 $self->{everybody} ||= create_group('To The Limit'); 210 return $self->{everybody}; 211} 212 213sub bug_create_value { 214 my ($self, $number, $field) = @_; 215 $field = $field->name if blessed($field); 216 if ($number == 6 and $field ne 'alias') { 217 $number = 1; 218 } 219 my $extra_values = $self->_extra_bug_create_values->{$number}; 220 if (exists $extra_values->{$field}) { 221 return $extra_values->{$field}; 222 } 223 return $self->_bug_create_values->{$number}->{$field}; 224} 225sub bug_update_value { 226 my ($self, $number, $field) = @_; 227 $field = $field->name if blessed($field); 228 if ($number == 6 and $field ne 'alias') { 229 $number = 1; 230 } 231 return $self->_bug_update_values->{$number}->{$field}; 232} 233 234# Values used to create the bugs. 235sub _bug_create_values { 236 my $self = shift; 237 return $self->{bug_create_values} if $self->{bug_create_values}; 238 my %values; 239 foreach my $number (1..NUM_BUGS) { 240 $values{$number} = $self->_create_field_values($number, 'for create'); 241 } 242 $self->{bug_create_values} = \%values; 243 return $self->{bug_create_values}; 244} 245# Values as they existed on the bug, at creation time. Used by the 246# changedfrom tests. 247sub _extra_bug_create_values { 248 my $self = shift; 249 $self->{extra_bug_create_values} ||= { map { $_ => {} } (1..NUM_BUGS) }; 250 return $self->{extra_bug_create_values}; 251} 252 253# Values used to update the bugs after they are created. 254sub _bug_update_values { 255 my $self = shift; 256 return $self->{bug_update_values} if $self->{bug_update_values}; 257 my %values; 258 foreach my $number (1..NUM_BUGS) { 259 $values{$number} = $self->_create_field_values($number); 260 } 261 $self->{bug_update_values} = \%values; 262 return $self->{bug_update_values}; 263} 264 265############################## 266# General Helper Subroutines # 267############################## 268 269sub random { 270 $_[0] ||= FIELD_SIZE; 271 generate_random_password(@_); 272} 273 274# We need to use a custom timestamp for each create() and update(), 275# because the database returns the same value for LOCALTIMESTAMP(0) 276# for the entire transaction, and we need each created bug to have 277# its own creation_ts and delta_ts. 278sub timestamp { 279 my ($day, $second) = @_; 280 return DateTime->new( 281 year => 2037, 282 month => 1, 283 day => $day, 284 hour => 12, 285 minute => $second, 286 second => 0, 287 # We make it floating because the timezone doesn't matter for our uses, 288 # and we want totally consistent behavior across all possible machines. 289 time_zone => 'floating', 290 ); 291} 292 293sub create_keyword { 294 my ($number) = @_; 295 return Bugzilla::Keyword->create({ 296 name => "$number-keyword-" . random(), 297 description => "Keyword $number" }); 298} 299 300sub create_user { 301 my ($prefix) = @_; 302 my $user_name = $prefix . '-' . random(15) . "@" . random(12) 303 . "." . random(3); 304 my $user_realname = $prefix . '-' . random(); 305 my $user = Bugzilla::User->create({ 306 login_name => $user_name, 307 realname => $user_realname, 308 cryptpassword => '*', 309 }); 310 return $user; 311} 312 313sub create_group { 314 my ($prefix) = @_; 315 return Bugzilla::Group->create({ 316 name => "$prefix-group-" . random(), description => "Everybody $prefix", 317 userregexp => '.*', isbuggroup => 1 }); 318} 319 320sub create_legal_value { 321 my ($field, $number) = @_; 322 my $type = Bugzilla::Field::Choice->type($field); 323 my $field_name = $field->name; 324 return $type->create({ value => "$number-$field_name-" . random(), 325 is_open => 0 }); 326} 327 328######################### 329# Custom Field Creation # 330######################### 331 332sub _create_custom_fields { 333 my ($self) = @_; 334 return if !$self->option('add-custom-fields'); 335 336 while (my ($type, $name) = each %{ CUSTOM_FIELDS() }) { 337 my $exists = new Bugzilla::Field({ name => $name }); 338 next if $exists; 339 Bugzilla::Field->create({ 340 name => $name, 341 type => $type, 342 description => "Search Test Field $name", 343 enter_bug => 1, 344 custom => 1, 345 buglist => 1, 346 is_mandatory => 0, 347 }); 348 } 349} 350 351######################## 352# Field Value Creation # 353######################## 354 355sub _create_field_values { 356 my ($self, $number, $for_create) = @_; 357 my $dbh = Bugzilla->dbh; 358 359 Bugzilla->set_user($self->admin); 360 361 my @selects = grep { $_->is_select } $self->all_fields; 362 my %values; 363 foreach my $field (@selects) { 364 next if $field->is_abnormal; 365 $values{$field->name} = create_legal_value($field, $number)->name; 366 } 367 368 my $group = create_group($number); 369 $values{groups} = [$group->name]; 370 371 $values{'keywords'} = create_keyword($number)->name; 372 373 foreach my $field (qw(assigned_to qa_contact reporter cc)) { 374 $values{$field} = create_user("$number-$field")->login; 375 } 376 377 my $classification = Bugzilla::Classification->create( 378 { name => "$number-classification-" . random() }); 379 $classification = $classification->name; 380 381 my $version = "$number-version-" . random(); 382 my $milestone = "$number-tm-" . random(15); 383 my $product = Bugzilla::Product->create({ 384 name => "$number-product-" . random(), 385 description => 'Created by t/search.t', 386 defaultmilestone => $milestone, 387 classification => $classification, 388 version => $version, 389 allows_unconfirmed => 1, 390 }); 391 foreach my $item ($group, $self->nobody) { 392 $product->set_group_controls($item, 393 { membercontrol => CONTROLMAPSHOWN, 394 othercontrol => CONTROLMAPNA }); 395 } 396 # $product->update() is called lower down. 397 my $component = Bugzilla::Component->create({ 398 product => $product, name => "$number-component-" . random(), 399 initialowner => create_user("$number-defaultowner")->login, 400 initialqacontact => create_user("$number-defaultqa")->login, 401 initial_cc => [create_user("$number-initcc")->login], 402 description => "Component $number" }); 403 404 $values{'product'} = $product->name; 405 $values{'component'} = $component->name; 406 $values{'target_milestone'} = $milestone; 407 $values{'version'} = $version; 408 409 foreach my $field ($self->text_fields) { 410 # We don't add a - after $field for the text fields, because 411 # if we do, fulltext searching for short_desc pulls out 412 # "short_desc" as a word and matches it in every bug. 413 my $value = "$number-$field" . random(); 414 if ($field eq 'bug_file_loc' or $field eq 'see_also') { 415 $value = "http://$value-" . random(3) 416 . "/show_bug.cgi?id=$number"; 417 } 418 $values{$field} = $value; 419 } 420 $values{'tag'} = ["$number-tag-" . random()]; 421 422 my @date_fields = grep { $_->type == FIELD_TYPE_DATETIME } $self->all_fields; 423 foreach my $field (@date_fields) { 424 # We use 03 as the month because that differs from our creation_ts, 425 # delta_ts, and deadline. (It's nice to have recognizable values 426 # for each field when debugging.) 427 my $second = $for_create ? $number : $number + 1; 428 $values{$field->name} = "2037-03-0$number 12:34:0$second"; 429 } 430 431 $values{alias} = "$number-alias-" . random(12); 432 433 # Prefixing the original comment with "description" makes the 434 # lesserthan and greaterthan tests behave predictably. 435 my $comm_prefix = $for_create ? "description-" : ''; 436 $values{comment} = "$comm_prefix$number-comment-" . random() 437 . ' ' . random(); 438 439 my @flags; 440 my $setter = create_user("$number-setters.login_name"); 441 my $requestee = create_user("$number-requestees.login_name"); 442 $values{set_flags} = _create_flags($number, $setter, $requestee); 443 444 my $month = $for_create ? "12" : "02"; 445 $values{'deadline'} = "2037-$month-0$number"; 446 my $estimate_times = $for_create ? 10 : 1; 447 $values{estimated_time} = $estimate_times * $number; 448 449 $values{attachment} = _get_attach_values($number, $for_create); 450 451 # Some things only happen on the first bug. 452 if ($number == 1) { 453 # We use 6 as the prefix for the extra values, because bug 6's values 454 # don't otherwise get used (since bug 6 is created as a clone of 455 # bug 1). This also makes sure that our greaterthan/lessthan 456 # tests work properly. 457 my $extra_group = create_group(6); 458 $product->set_group_controls($extra_group, 459 { membercontrol => CONTROLMAPSHOWN, 460 othercontrol => CONTROLMAPNA }); 461 $values{groups} = [$values{groups}->[0], $extra_group->name]; 462 my $extra_keyword = create_keyword(6); 463 $values{keywords} = [$values{keywords}, $extra_keyword->name]; 464 my $extra_cc = create_user("6-cc"); 465 $values{cc} = [$values{cc}, $extra_cc->login]; 466 my @multi_selects = grep { $_->type == FIELD_TYPE_MULTI_SELECT } 467 $self->all_fields; 468 foreach my $field (@multi_selects) { 469 my $new_value = create_legal_value($field, 6); 470 my $name = $field->name; 471 $values{$name} = [$values{$name}, $new_value->name]; 472 } 473 push(@{ $values{'tag'} }, "6-tag-" . random()); 474 } 475 476 # On bug 5, any field that *can* be left empty, *is* left empty. 477 if ($number == 5) { 478 my @set_fields = grep { $_->type == FIELD_TYPE_SINGLE_SELECT } 479 $self->all_fields; 480 @set_fields = map { $_->name } @set_fields; 481 push(@set_fields, qw(short_desc version reporter)); 482 foreach my $key (keys %values) { 483 delete $values{$key} unless grep { $_ eq $key } @set_fields; 484 } 485 } 486 487 $product->update(); 488 489 return \%values; 490} 491 492# Flags 493sub _create_flags { 494 my ($number, $setter, $requestee) = @_; 495 496 my $flagtypes = _create_flagtypes($number); 497 498 my %flags; 499 foreach my $type (qw(a b)) { 500 $flags{$type} = _get_flag_values(@_, $flagtypes->{$type}); 501 } 502 return \%flags; 503} 504 505sub _create_flagtypes { 506 my ($number) = @_; 507 my $dbh = Bugzilla->dbh; 508 my $name = "$number-flag-" . random(); 509 my $desc = "FlagType $number"; 510 511 my %flagtypes; 512 foreach my $target (qw(a b)) { 513 $dbh->do("INSERT INTO flagtypes 514 (name, description, target_type, is_requestable, 515 is_requesteeble, is_multiplicable, cc_list) 516 VALUES (?,?,?,1,1,1,'')", 517 undef, $name, $desc, $target); 518 my $id = $dbh->bz_last_key('flagtypes', 'id'); 519 $dbh->do('INSERT INTO flaginclusions (type_id) VALUES (?)', 520 undef, $id); 521 my $flagtype = new Bugzilla::FlagType($id); 522 $flagtypes{$target} = $flagtype; 523 } 524 return \%flagtypes; 525} 526 527sub _get_flag_values { 528 my ($number, $setter, $requestee, $flagtype) = @_; 529 530 my @set_flags; 531 if ($number <= 2) { 532 foreach my $value (qw(? - + ?)) { 533 my $flag = { type_id => $flagtype->id, status => $value, 534 setter => $setter, flagtype => $flagtype }; 535 push(@set_flags, $flag); 536 } 537 $set_flags[0]->{requestee} = $requestee->login; 538 } 539 else { 540 @set_flags = ({ type_id => $flagtype->id, status => '+', 541 setter => $setter, flagtype => $flagtype }); 542 } 543 return \@set_flags; 544} 545 546# Attachments 547sub _get_attach_values { 548 my ($number, $for_create) = @_; 549 550 my $boolean = $number == 1 ? 1 : 0; 551 if ($for_create) { 552 $boolean = !$boolean ? 1 : 0; 553 } 554 my $ispatch = $for_create ? 'ispatch' : 'is_patch'; 555 my $isobsolete = $for_create ? 'isobsolete' : 'is_obsolete'; 556 my $isprivate = $for_create ? 'isprivate' : 'is_private'; 557 my $mimetype = $for_create ? 'mimetype' : 'content_type'; 558 559 my %values = ( 560 description => "$number-attach_desc-" . random(), 561 filename => "$number-filename-" . random(), 562 $ispatch => $boolean, 563 $isobsolete => $boolean, 564 $isprivate => $boolean, 565 $mimetype => "text/x-$number-" . random(), 566 ); 567 if ($for_create) { 568 $values{data} = "$number-data-" . random() . random(); 569 } 570 return \%values; 571} 572 573################ 574# Bug Creation # 575################ 576 577sub _create_one_bug { 578 my ($self, $number) = @_; 579 my $dbh = Bugzilla->dbh; 580 581 # We need bug 6 to have a unique alias that is not a clone of bug 1's, 582 # so we get the alias separately from the other parameters. 583 my $alias = $self->bug_create_value($number, 'alias'); 584 my $update_alias = $self->bug_update_value($number, 'alias'); 585 586 # Otherwise, make bug 6 a clone of bug 1. 587 my $real_number = $number; 588 $number = 1 if $number == 6; 589 590 my $reporter = $self->bug_create_value($number, 'reporter'); 591 Bugzilla->set_user(Bugzilla::User->check($reporter)); 592 593 # We create the bug with one set of values, and then we change it 594 # to have different values. 595 my %params = %{ $self->_bug_create_values->{$number} }; 596 $params{alias} = $alias; 597 598 # There are some things in bug_create_values that shouldn't go into 599 # create(). 600 delete @params{qw(attachment set_flags tag)}; 601 602 my ($status, $resolution, $see_also) = 603 delete @params{qw(bug_status resolution see_also)}; 604 # All the bugs are created with everconfirmed = 0. 605 $params{bug_status} = 'UNCONFIRMED'; 606 my $bug = Bugzilla::Bug->create(\%params); 607 608 # These are necessary for the changedfrom tests. 609 my $extra_values = $self->_extra_bug_create_values->{$number}; 610 foreach my $field (qw(comments remaining_time percentage_complete 611 keyword_objects everconfirmed dependson blocked 612 groups_in classification actual_time)) 613 { 614 $extra_values->{$field} = $bug->$field; 615 } 616 $extra_values->{reporter_accessible} = $number == 1 ? 0 : 1; 617 $extra_values->{cclist_accessible} = $number == 1 ? 0 : 1; 618 619 if ($number == 5) { 620 # Bypass Bugzilla::Bug--we don't want any changes in bugs_activity 621 # for bug 5. 622 $dbh->do('UPDATE bugs SET qa_contact = NULL, reporter_accessible = 0, 623 cclist_accessible = 0 WHERE bug_id = ?', 624 undef, $bug->id); 625 $dbh->do('DELETE FROM cc WHERE bug_id = ?', undef, $bug->id); 626 my $ts = '1970-01-01 00:00:00'; 627 $dbh->do('UPDATE bugs SET creation_ts = ?, delta_ts = ? 628 WHERE bug_id = ?', undef, $ts, $ts, $bug->id); 629 $dbh->do('UPDATE longdescs SET bug_when = ? WHERE bug_id = ?', 630 undef, $ts, $bug->id); 631 $bug->{creation_ts} = $ts; 632 $extra_values->{see_also} = []; 633 } 634 else { 635 # Manually set the creation_ts so that each bug has a different one. 636 # 637 # Also, manually update the resolution and bug_status, because 638 # we want to see both of them change in bugs_activity, so we 639 # have to start with values for both (and as of the time when I'm 640 # writing this test, Bug->create doesn't support setting resolution). 641 # 642 # Same for see_also. 643 my $timestamp = timestamp($number, $number - 1); 644 my $creation_ts = $timestamp->ymd . ' ' . $timestamp->hms; 645 $bug->{creation_ts} = $creation_ts; 646 $dbh->do('UPDATE longdescs SET bug_when = ? WHERE bug_id = ?', 647 undef, $creation_ts, $bug->id); 648 $dbh->do('UPDATE bugs SET creation_ts = ?, bug_status = ?, 649 resolution = ? WHERE bug_id = ?', 650 undef, $creation_ts, $status, $resolution, $bug->id); 651 $dbh->do('INSERT INTO bug_see_also (bug_id, value, class) VALUES (?,?,?)', 652 undef, $bug->id, $see_also, 'Bugzilla::BugUrl::Bugzilla'); 653 $extra_values->{see_also} = $bug->see_also; 654 655 # All the tags must be created as the admin user, so that the 656 # admin user can find them, later. 657 my $original_user = Bugzilla->user; 658 Bugzilla->set_user($self->admin); 659 my $tags = $self->bug_create_value($number, 'tag'); 660 $bug->add_tag($_) foreach @$tags; 661 $extra_values->{tags} = $tags; 662 Bugzilla->set_user($original_user); 663 664 if ($number == 1) { 665 # Bug 1 needs to start off with reporter_accessible and 666 # cclist_accessible being 0, so that when we change them to 1, 667 # that change shows up in bugs_activity. 668 $dbh->do('UPDATE bugs SET reporter_accessible = 0, 669 cclist_accessible = 0 WHERE bug_id = ?', 670 undef, $bug->id); 671 # Bug 1 gets three comments, so that longdescs.count matches it 672 # uniquely. The third comment is added in the middle, so that the 673 # last comment contains all of the important data, like work_time. 674 $bug->add_comment("1-comment-" . random(100)); 675 } 676 677 my %update_params = %{ $self->_bug_update_values->{$number} }; 678 my %reverse_map = reverse %{ Bugzilla::Bug->FIELD_MAP }; 679 foreach my $db_name (keys %reverse_map) { 680 next if $db_name eq 'comment'; 681 next if $db_name eq 'status_whiteboard'; 682 if (exists $update_params{$db_name}) { 683 my $update_name = $reverse_map{$db_name}; 684 $update_params{$update_name} = delete $update_params{$db_name}; 685 } 686 } 687 688 my ($new_status, $new_res) = 689 delete @update_params{qw(status resolution)}; 690 # Bypass the status workflow. 691 $bug->{bug_status} = $new_status; 692 $bug->{resolution} = $new_res; 693 $bug->{everconfirmed} = 1 if $number == 1; 694 695 # add/remove/set fields. 696 $update_params{keywords} = { set => $update_params{keywords} }; 697 $update_params{groups} = { add => $update_params{groups}, 698 remove => $bug->groups_in }; 699 my @cc_remove = map { $_->login } @{ $bug->cc_users }; 700 my $cc_new = $update_params{cc}; 701 my @cc_add = ref($cc_new) ? @$cc_new : ($cc_new); 702 # We make the admin an explicit CC on bug 1 (but not on bug 6), so 703 # that we can test the %user% pronoun properly. 704 if ($real_number == 1) { 705 push(@cc_add, $self->admin->login); 706 } 707 $update_params{cc} = { add => \@cc_add, remove => \@cc_remove }; 708 my $see_also_remove = $bug->see_also; 709 my $see_also_add = [$update_params{see_also}]; 710 $update_params{see_also} = { add => $see_also_add, 711 remove => $see_also_remove }; 712 $update_params{comment} = { body => $update_params{comment} }; 713 $update_params{work_time} = $number; 714 # Setting work_time kills the remaining_time, so we need to 715 # preserve that. We add 8 because that produces an integer 716 # percentage_complete for bug 1, which is necessary for 717 # accurate "equals"-type searching. 718 $update_params{remaining_time} = $number + 8; 719 $update_params{reporter_accessible} = $number == 1 ? 1 : 0; 720 $update_params{cclist_accessible} = $number == 1 ? 1 : 0; 721 $update_params{alias} = $update_alias; 722 723 $bug->set_all(\%update_params); 724 my $flags = $self->bug_create_value($number, 'set_flags')->{b}; 725 $bug->set_flags([], $flags); 726 $timestamp->set(second => $number); 727 $bug->update($timestamp->ymd . ' ' . $timestamp->hms); 728 $extra_values->{flags} = $bug->flags; 729 730 # It's not generally safe to do update() multiple times on 731 # the same Bug object. 732 $bug = new Bugzilla::Bug($bug->id); 733 my $update_flags = $self->bug_update_value($number, 'set_flags')->{b}; 734 $_->{status} = 'X' foreach @{ $bug->flags }; 735 $bug->set_flags($bug->flags, $update_flags); 736 if ($number == 1) { 737 my $comment_id = $bug->comments->[-1]->id; 738 $bug->set_comment_is_private({ $comment_id => 1 }); 739 } 740 $bug->update($bug->delta_ts); 741 742 my $attach_create = $self->bug_create_value($number, 'attachment'); 743 my $attachment = Bugzilla::Attachment->create({ 744 bug => $bug, 745 creation_ts => $creation_ts, 746 %$attach_create }); 747 # Store for the changedfrom tests. 748 $extra_values->{attachments} = 749 [new Bugzilla::Attachment($attachment->id)]; 750 751 my $attach_update = $self->bug_update_value($number, 'attachment'); 752 $attachment->set_all($attach_update); 753 # In order to keep the mimetype on the ispatch attachment, 754 # we need to bypass the validator. 755 $attachment->{mimetype} = $attach_update->{content_type}; 756 my $attach_flags = $self->bug_update_value($number, 'set_flags')->{a}; 757 $attachment->set_flags([], $attach_flags); 758 $attachment->update($bug->delta_ts); 759 } 760 761 # Values for changedfrom. 762 $extra_values->{creation_ts} = $bug->creation_ts; 763 $extra_values->{delta_ts} = $bug->creation_ts; 764 765 return new Bugzilla::Bug($bug->id); 766} 767 768################################### 769# Test::Builder Memory Efficiency # 770################################### 771 772# Test::Builder stores information for each test run, but Test::Harness 773# and TAP::Harness don't actually need this information. When we run 60 774# million tests, the history eats up all our memory. (After about 775# 1 million tests, memory usage is around 1 GB.) 776# 777# The only part of the history that Test::More actually *uses* is the "ok" 778# field, which we store more efficiently, in an array, and then we re-populate 779# the Test_Results in Test::Builder at the end of the test. 780sub clean_test_history { 781 my ($self) = @_; 782 return if !$self->option('long'); 783 my $builder = Test::More->builder; 784 my $current_test = $builder->current_test; 785 786 # I don't use details() because I don't want to copy the array. 787 my $results = $builder->{Test_Results}; 788 my $check_test = $current_test - 1; 789 while (my $result = $results->[$check_test]) { 790 last if !$result; 791 $self->test_success($check_test, $result->{ok}); 792 $check_test--; 793 } 794 795 # Truncate the test history array, but retain the current test number. 796 $builder->{Test_Results} = []; 797 $builder->{Curr_Test} = $current_test; 798} 799 800sub test_success { 801 my ($self, $index, $status) = @_; 802 $self->{test_success}->[$index] = $status; 803 return $self->{test_success}; 804} 805 806sub repopulate_test_results { 807 my ($self) = @_; 808 return if !$self->option('long'); 809 $self->clean_test_history(); 810 # We create only two hashes, for memory efficiency. 811 my %ok = ( ok => 1 ); 812 my %not_ok = ( ok => 0 ); 813 my @results; 814 foreach my $success (@{ $self->{test_success} }) { 815 push(@results, $success ? \%ok : \%not_ok); 816 } 817 my $builder = Test::More->builder; 818 $builder->{Test_Results} = \@results; 819} 820 821########## 822# Caches # 823########## 824 825# When doing AND and OR tests, we essentially test the same field/operator 826# combinations over and over. So, if we're going to be running those tests, 827# we cache the translated_value of the FieldTests globally so that we don't 828# have to re-run the value-translation code every time (which can be pretty 829# slow). 830sub value_translation_cache { 831 my ($self, $field_test, $value) = @_; 832 return if !$self->option('long'); 833 my $test_name = $field_test->name; 834 if (@_ == 3) { 835 $self->{value_translation_cache}->{$test_name} = $value; 836 } 837 return $self->{value_translation_cache}->{$test_name}; 838} 839 840# When doing AND/OR tests, the value for transformed_value_was_equal 841# (see Bugzilla::Test::Search::FieldTest) won't be recalculated 842# if we pull our values from the value_translation_cache. So we need 843# to also cache the values for transformed_value_was_equal. 844sub was_equal_cache { 845 my ($self, $field_test, $number, $value) = @_; 846 return if !$self->option('long'); 847 my $test_name = $field_test->name; 848 if (@_ == 4) { 849 $self->{tvwe_cache}->{$test_name}->{$number} = $value; 850 } 851 return $self->{tvwe_cache}->{$test_name}->{$number}; 852} 853 854############# 855# Main Test # 856############# 857 858sub run { 859 my ($self) = @_; 860 my $dbh = Bugzilla->dbh; 861 862 # We want backtraces on any "die" message or any warning. 863 # Otherwise it's hard to trace errors inside of Bugzilla::Search from 864 # reading automated test run results. 865 local $SIG{__WARN__} = \&Carp::cluck; 866 local $SIG{__DIE__} = \&Carp::confess; 867 868 $dbh->bz_start_transaction(); 869 870 # Some parameters need to be set in order for the tests to function 871 # properly. 872 my $everybody = $self->everybody; 873 my $params = Bugzilla->params; 874 local $params->{'useclassification'} = 1; 875 local $params->{'useqacontact'} = 1; 876 local $params->{'usetargetmilestone'} = 1; 877 local $params->{'mail_delivery_method'} = 'None'; 878 local $params->{'timetrackinggroup'} = $everybody->name; 879 local $params->{'insidergroup'} = $everybody->name; 880 881 $self->_setup_bugs(); 882 883 # Even though _setup_bugs set us as an admin, we want to be sure at 884 # this point that we have an admin with refreshed group memberships. 885 Bugzilla->set_user($self->admin); 886 foreach my $test (CUSTOM_SEARCH_TESTS) { 887 my $custom_test = new Bugzilla::Test::Search::CustomTest($test, $self); 888 $custom_test->run(); 889 } 890 foreach my $test (SPECIAL_PARAM_TESTS) { 891 my $operator_test = 892 new Bugzilla::Test::Search::OperatorTest($test->{operator}, $self); 893 my $field = Bugzilla::Field->check($test->{field}); 894 my $special_test = new Bugzilla::Test::Search::FieldTestNormal( 895 $operator_test, $field, $test); 896 $special_test->run(); 897 } 898 foreach my $operator ($self->top_level_operators) { 899 my $operator_test = 900 new Bugzilla::Test::Search::OperatorTest($operator, $self); 901 $operator_test->run(); 902 } 903 904 # Rollbacks won't get rid of bugs_fulltext entries, so we do that ourselves. 905 my @bug_ids = map { $_->id } $self->bugs; 906 my $bug_id_string = join(',', @bug_ids); 907 $dbh->do("DELETE FROM bugs_fulltext WHERE bug_id IN ($bug_id_string)"); 908 $dbh->bz_rollback_transaction(); 909 $self->repopulate_test_results(); 910} 911 912# This makes a few changes to the bugs after they're created--changes 913# that can only be done after all the bugs have been created. 914sub _setup_bugs { 915 my ($self) = @_; 916 $self->_setup_dependencies(); 917 $self->_set_bug_id_fields(); 918 $self->_protect_bug_6(); 919} 920sub _setup_dependencies { 921 my ($self) = @_; 922 my $dbh = Bugzilla->dbh; 923 924 # Set up depedency relationships between the bugs. 925 # Bug 1 + 6 depend on bug 2 and block bug 3. 926 my $bug2 = $self->bug(2); 927 my $bug3 = $self->bug(3); 928 foreach my $number (1,6) { 929 my $bug = $self->bug($number); 930 my @original_delta = ($bug2->delta_ts, $bug3->delta_ts); 931 Bugzilla->set_user($bug->reporter); 932 $bug->set_dependencies([$bug2->id], [$bug3->id]); 933 $bug->update($bug->delta_ts); 934 # Setting dependencies changed the delta_ts on bug2 and bug3, so 935 # re-set them back to what they were before. However, we leave 936 # the correct update times in bugs_activity, so that the changed* 937 # searches still work right. 938 my $set_delta = $dbh->prepare( 939 'UPDATE bugs SET delta_ts = ? WHERE bug_id = ?'); 940 foreach my $row ([$original_delta[0], $bug2->id], 941 [$original_delta[1], $bug3->id]) 942 { 943 $set_delta->execute(@$row); 944 } 945 } 946} 947 948sub _set_bug_id_fields { 949 my ($self) = @_; 950 # BUG_ID fields couldn't be set before, because before we create bug 1, 951 # we don't necessarily have any valid bug ids.) 952 my @bug_id_fields = grep { $_->type == FIELD_TYPE_BUG_ID } 953 $self->all_fields; 954 foreach my $number (1..NUM_BUGS) { 955 my $bug = $self->bug($number); 956 $number = 1 if $number == 6; 957 next if $number == 5; 958 my $other_bug = $self->bug($number + 1); 959 Bugzilla->set_user($bug->reporter); 960 foreach my $field (@bug_id_fields) { 961 $bug->set_custom_field($field, $other_bug->id); 962 $bug->update($bug->delta_ts); 963 } 964 } 965} 966 967sub _protect_bug_6 { 968 my ($self) = @_; 969 my $dbh = Bugzilla->dbh; 970 971 Bugzilla->set_user($self->admin); 972 973 # Put bug6 in the nobody group. 974 my $nobody = $self->nobody; 975 # We pull it newly from the DB to be sure it's safe to call update() 976 # on. 977 my $bug6 = new Bugzilla::Bug($self->bug(6)->id); 978 $bug6->add_group($nobody); 979 $bug6->update($bug6->delta_ts); 980 981 # Remove the admin (and everybody else) from the $nobody group. 982 $dbh->do('DELETE FROM group_group_map 983 WHERE grantor_id = ? OR member_id = ?', undef, 984 $nobody->id, $nobody->id); 985} 986 9871; 988