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