Commit 87ea46f7 authored by Max Kanat-Alexander's avatar Max Kanat-Alexander

Bug 574879: Create a test that assures the correctness of Search.pm's

boolean charts r=glob, a=mkanat
parent 814b24fd
......@@ -463,6 +463,9 @@ sub usage_mode {
elsif ($newval == USAGE_MODE_EMAIL) {
$class->error_mode(ERROR_MODE_DIE);
}
elsif ($newval == USAGE_MODE_TEST) {
$class->error_mode(ERROR_MODE_TEST);
}
else {
ThrowCodeError('usage_mode_invalid',
{'invalid_usage_mode', $newval});
......
......@@ -3203,6 +3203,17 @@ sub comments {
return \@comments;
}
# This is needed by xt/search.t.
sub percentage_complete {
my $self = shift;
return undef if $self->{'error'} || !Bugzilla->user->is_timetracker;
my $remaining = $self->remaining_time;
my $actual = $self->actual_time;
my $total = $remaining + $actual;
return undef if $total == 0;
return 100 * ($actual / $total);
}
sub product {
my ($self) = @_;
return $self->{product} if exists $self->{product};
......
......@@ -141,11 +141,13 @@ use File::Basename;
USAGE_MODE_XMLRPC
USAGE_MODE_EMAIL
USAGE_MODE_JSON
USAGE_MODE_TEST
ERROR_MODE_WEBPAGE
ERROR_MODE_DIE
ERROR_MODE_DIE_SOAP_FAULT
ERROR_MODE_JSON_RPC
ERROR_MODE_TEST
COLOR_ERROR
......@@ -457,6 +459,7 @@ use constant USAGE_MODE_CMDLINE => 1;
use constant USAGE_MODE_XMLRPC => 2;
use constant USAGE_MODE_EMAIL => 3;
use constant USAGE_MODE_JSON => 4;
use constant USAGE_MODE_TEST => 5;
# Error modes. Default set by Bugzilla->usage_mode (so ERROR_MODE_WEBPAGE
# usually). Use with Bugzilla->error_mode.
......@@ -464,6 +467,7 @@ use constant ERROR_MODE_WEBPAGE => 0;
use constant ERROR_MODE_DIE => 1;
use constant ERROR_MODE_DIE_SOAP_FAULT => 2;
use constant ERROR_MODE_JSON_RPC => 3;
use constant ERROR_MODE_TEST => 4;
# The ANSI colors of messages that command-line scripts use
use constant COLOR_ERROR => 'red';
......
......@@ -33,6 +33,7 @@ use Bugzilla::WebService::Constants;
use Bugzilla::Util;
use Carp;
use Data::Dumper;
use Date::Format;
# We cannot use $^S to detect if we are in an eval(), because mod_perl
......@@ -102,6 +103,12 @@ sub _throw_error {
$template->process($name, $vars)
|| ThrowTemplateError($template->error());
}
# There are some tests that throw and catch a lot of errors,
# and calling $template->process over and over for those errors
# is too slow. So instead, we just "die" with a dump of the arguments.
elsif (Bugzilla->error_mode == ERROR_MODE_TEST) {
die Dumper($vars);
}
else {
my $message;
$template->process($name, $vars, \$message)
......
......@@ -358,7 +358,9 @@ sub make_admin {
write_params();
}
print "\n", get_text('install_admin_created', { user => $user }), "\n";
if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) {
print "\n", get_text('install_admin_created', { user => $user }), "\n";
}
}
sub _prompt_for_password {
......
......@@ -241,6 +241,8 @@ sub FILESYSTEM {
dirs => DIR_OWNER_WRITE },
t => { files => OWNER_WRITE,
dirs => DIR_OWNER_WRITE },
xt => { files => OWNER_WRITE,
dirs => DIR_OWNER_WRITE },
'docs/lib' => { files => OWNER_WRITE,
dirs => DIR_OWNER_WRITE },
'docs/*/xml' => { files => OWNER_WRITE,
......@@ -333,6 +335,8 @@ EOT
contents => HT_DEFAULT_DENY },
't/.htaccess' => { perms => WS_SERVE,
contents => HT_DEFAULT_DENY },
'xt/.htaccess' => { perms => WS_SERVE,
contents => HT_DEFAULT_DENY },
"$datadir/.htaccess" => { perms => WS_SERVE,
contents => HT_DEFAULT_DENY },
......
......@@ -2159,7 +2159,7 @@ sub _owner_idle_time_greater_less {
my $table = "idle_" . $$chartid;
$$v =~ /^(\d+)\s*([hHdDwWmMyY])?$/;
my $quantity = $1;
my $quantity = $1 || 0;
my $unit = lc $2;
my $unitinterval = 'DAY';
if ($unit eq 'h') {
......
The tests in this directory require a working database, as opposed
to the tests in t/, which simply test the code without a working
installation.
Some of the tests may modify your current working installation, even
if only temporarily. To run the tests that modify your database,
set the environment variable BZ_WRITE_TESTS to 1.
Some tests also take additional, optional arguments. You can pass arguments
to tests like:
prove xt/search.t :: --long --operators=equals,notequals
Note the "::"--that is necessary to note that the arguments are going to
the test, not to "prove".
See the perldoc of the individual tests to see what options they support,
or do "perl xt/search.t --help".
# -*- Mode: perl; indent-tabs-mode: nil -*-
#
# The contents of this file are subject to the Mozilla Public
# License Version 1.1 (the "License"); you may not use this file
# except in compliance with the License. You may obtain a copy of
# the License at http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS
# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
# implied. See the License for the specific language governing
# rights and limitations under the License.
#
# The Original Code is the Bugzilla Bug Tracking System.
#
# The Initial Developer of the Original Code is Everything Solved, Inc.
# Portions created by the Initial Developer are Copyright (C) 2010 the
# Initial Developer. All Rights Reserved.
#
# Contributor(s):
# Max Kanat-Alexander <mkanat@bugzilla.org>
# 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::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;
my $operator_field_tests = ($top_combinations + $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;
return $operator_field_tests + $sql_injection_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->get_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 $value = $self->_bug_create_values->{$number}->{$field};
return $value if defined $value;
return $self->_extra_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(10) . "@" . random(10)
. "." . 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;
}
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-setter");
my $requestee = create_user("$number-requestee");
$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];
}
}
# 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.
$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)};
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 flags percentage_complete
keyword_objects everconfirmed dependson blocked
groups_in)
{
$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;
}
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) VALUES (?,?)',
undef, $bug->id, $see_also);
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);
}
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_add = $update_params{cc};
$cc_add = [$cc_add] if !ref $cc_add;
$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);
# 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};
}
#############
# 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->{'usebugaliases'} = 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 $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;
# -*- Mode: perl; indent-tabs-mode: nil -*-
#
# The contents of this file are subject to the Mozilla Public
# License Version 1.1 (the "License"); you may not use this file
# except in compliance with the License. You may obtain a copy of
# the License at http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS
# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
# implied. See the License for the specific language governing
# rights and limitations under the License.
#
# The Original Code is the Bugzilla Bug Tracking System.
#
# The Initial Developer of the Original Code is Everything Solved, Inc.
# Portions created by the Initial Developer are Copyright (C) 2010 the
# Initial Developer. All Rights Reserved.
#
# Contributor(s):
# Max Kanat-Alexander <mkanat@bugzilla.org>
# This test combines two field/operator combinations using AND in
# a single boolean chart.
package Bugzilla::Test::Search::AndTest;
use base qw(Bugzilla::Test::Search::OrTest);
use Bugzilla::Test::Search::Constants;
use Bugzilla::Test::Search::FakeCGI;
use List::MoreUtils qw(all);
use constant type => 'AND';
#############
# Accessors #
#############
# In an AND test, bugs ARE supposed to be contained only if they are contained
# by ALL tests.
sub bug_is_contained {
my ($self, $number) = @_;
return all { $_->bug_is_contained($number) } $self->field_tests;
}
########################
# SKIP & TODO Messages #
########################
sub _join_skip { () }
sub _join_broken_constant { {} }
##############################
# Bugzilla::Search arguments #
##############################
sub search_params {
my ($self) = @_;
my @all_params = map { $_->search_params } $self->field_tests;
my $params = new Bugzilla::Test::Search::FakeCGI;
my $chart = 0;
foreach my $item (@all_params) {
$params->param("field0-$chart-0", $item->param('field0-0-0'));
$params->param("type0-$chart-0", $item->param('type0-0-0'));
$params->param("value0-$chart-0", $item->param('value0-0-0'));
$chart++;
}
return $params;
}
1;
\ No newline at end of file
# -*- Mode: perl; indent-tabs-mode: nil -*-
#
# The contents of this file are subject to the Mozilla Public
# License Version 1.1 (the "License"); you may not use this file
# except in compliance with the License. You may obtain a copy of
# the License at http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS
# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
# implied. See the License for the specific language governing
# rights and limitations under the License.
#
# The Original Code is the Bugzilla Bug Tracking System.
#
# The Initial Developer of the Original Code is Everything Solved, Inc.
# Portions created by the Initial Developer are Copyright (C) 2010 the
# Initial Developer. All Rights Reserved.
#
# Contributor(s):
# Max Kanat-Alexander <mkanat@bugzilla.org>
# These are constants used by Bugzilla::Test::Search.
# See the comment at the top of that package for a general overview
# of how the search test works, and how the constants are used.
# More detailed information on each constant is available in the comments
# in this file.
package Bugzilla::Test::Search::Constants;
use base qw(Exporter);
use Bugzilla::Constants;
our @EXPORT = qw(
ATTACHMENT_FIELDS
COLUMN_TRANSLATION
COMMENT_FIELDS
CUSTOM_FIELDS
FIELD_SIZE
FIELD_SUBSTR_SIZE
FLAG_FIELDS
INJECTION_BROKEN_FIELD
INJECTION_BROKEN_OPERATOR
INJECTION_TESTS
KNOWN_BROKEN
NUM_BUGS
NUM_SEARCH_TESTS
OR_BROKEN
OR_SKIP
SKIP_FIELDS
SUBSTR_SIZE
TESTS
TESTS_PER_RUN
USER_FIELDS
);
# Bug 1 is designed to be found by all the "equals" tests. It has
# multiple values for several fields where other fields only have
# one value.
#
# Bug 2 and 3 have a dependency relationship with Bug 1,
# but show up in "not equals" tests. We do use bug 2 in multiple-value
# tests.
#
# Bug 4 should never show up in any equals test, and has no relationship
# with any other bug. However, it does have all its fields set.
#
# Bug 5 only has values set for mandatory fields, to expose problems
# that happen with "not equals" tests failing to catch bugs that don't
# have a value set at all.
#
# Bug 6 is a clone of Bug 1, but is in a group that the searcher isn't
# in.
use constant NUM_BUGS => 6;
# How many tests there are for each operator/field combination other
# than the "contains" tests.
use constant NUM_SEARCH_TESTS => 3;
# This is how many tests get run for each field/operator.
use constant TESTS_PER_RUN => NUM_SEARCH_TESTS + NUM_BUGS;
# This is how many random characters we generate for most fields' names.
# (Some fields can't be this long, though, so they have custom lengths
# in Bugzilla::Test::Search).
use constant FIELD_SIZE => 30;
# These are the custom fields that are created if the BZ_MODIFY_DATABASE_TESTS
# environment variable is set.
use constant CUSTOM_FIELDS => {
FIELD_TYPE_FREETEXT, 'cf_freetext',
FIELD_TYPE_SINGLE_SELECT, 'cf_single_select',
FIELD_TYPE_MULTI_SELECT, 'cf_multi_select',
FIELD_TYPE_TEXTAREA, 'cf_textarea',
FIELD_TYPE_DATETIME, 'cf_datetime',
FIELD_TYPE_BUG_ID, 'cf_bugid',
};
# This translates fielddefs names into Search column names.
use constant COLUMN_TRANSLATION => {
creation_ts => 'opendate',
delta_ts => 'changeddate',
work_time => 'actual_time',
};
# Make comment field names to their Bugzilla::Comment accessor.
use constant COMMENT_FIELDS => {
longdesc => 'body',
work_time => 'work_time',
commenter => 'author',
'longdescs.isprivate' => 'is_private',
};
# Same as above, for Bugzilla::Attachment.
use constant ATTACHMENT_FIELDS => {
mimetype => 'contenttype',
submitter => 'attacher',
thedata => 'data',
};
# Same, for Bugzilla::Flag.
use constant FLAG_FIELDS => {
'flagtypes.name' => 'name',
'setters.login_name' => 'setter',
'requestees.login_name' => 'requestee',
};
# These are fields that we don't test. Test::More will mark these
# "TODO & SKIP", and not run tests for them at all.
#
# attachments.isurl can't easily be supported by us, but it's basically
# identical to isprivate and isobsolete for searching, so that's not a big
# loss.
#
# We don't support days_elapsed or owner_idle_time yet.
use constant SKIP_FIELDS => qw(
attachments.isurl
owner_idle_time
days_elapsed
);
# During OR tests, we skip these fields. They basically just don't work
# right in OR tests, and it's too much work to document the exact tests
# that they cause to fail.
use constant OR_SKIP => qw(
percentage_complete
flagtypes.name
);
# All the fields that represent users.
use constant USER_FIELDS => qw(
assigned_to
reporter
qa_contact
commenter
attachments.submitter
setters.login_name
requestees.login_name cc
);
# For the "substr"-type searches, how short of a substring should
# we use?
use constant SUBSTR_SIZE => 20;
# However, for some fields, we use a different size.
use constant FIELD_SUBSTR_SIZE => {
alias => 12,
bug_file_loc => 30,
# Just the month and day.
deadline => -5,
creation_ts => -8,
delta_ts => -8,
work_time => 3,
remaining_time => 3,
see_also => 30,
target_milestone => 12,
};
################
# Known Broken #
################
# See the KNOWN_BROKEN constant for a general description of these
# "_BROKEN" constants.
# Search.pm currently enforces "this must be a 0 or 1" in situations
# where it should not, with two of the attachment booleans.
use constant ATTACHMENT_BOOLEANS_SEARCH_BROKEN => (
'attachments.ispatch' => { search => 1 },
'attachments.isobsolete' => { search => 1 },
);
# Sometimes the search for attachment booleans works, but then contains
# the wrong results, because it does not contain bugs that fully lack
# attachments.
use constant ATTACHMENT_BOOLEANS_CONTAINS_BROKEN => (
'attachments.isobsolete' => { contains => [5] },
'attachments.ispatch' => { contains => [5] },
'attachments.isprivate' => { contains => [5] },
);
# Certain fields fail all the "negative" search tests:
#
# Blocked and Dependson "notequals" only finds bugs that have
# values for the field, but where the dependency list doesn't contain
# the bug you listed. It doesn't find bugs that fully lack values for
# the fields, as it should.
#
# cc "not" matches if any CC'ed user matches, and it fails to match
# if there are no CCs on the bug.
#
# bug_group notequals doesn't find bugs that fully lack groups,
# and matches if there is one group that isn't equal.
#
# bug_file_loc can be NULL, so it gets missed by the normal
# notequals search.
#
# keywords & longdescs "notequals" match if *any* of the values
# are not equal to the string provided. Also, keywords fails to match
# if there are no keywords on the bug.
#
# attachments.* notequals doesn't find bugs that lack attachments.
#
# deadline notequals does not find bugs that lack deadlines
#
# setters notequal doesn't find bugs that fully lack flags.
# (maybe this is OK?)
#
# requestees.login_name doesn't find bugs that fully lack requestees.
use constant NEGATIVE_BROKEN => (
ATTACHMENT_BOOLEANS_CONTAINS_BROKEN,
'attach_data.thedata' => { contains => [5] },
'attachments.description' => { contains => [5] },
'attachments.filename' => { contains => [5] },
'attachments.mimetype' => { contains => [5] },
'attachments.submitter' => { contains => [5] },
blocked => { contains => [3,4,5] },
bug_file_loc => { contains => [5] },
bug_group => { contains => [1,5] },
cc => { contains => [1,5] },
deadline => { contains => [5] },
dependson => { contains => [2,4,5] },
keywords => { contains => [1,5] },
longdesc => { contains => [1] },
'longdescs.isprivate' => { contains => [1] },
percentage_complete => { contains => [1] },
'requestees.login_name' => { contains => [3,4,5] },
'setters.login_name' => { contains => [5] },
work_time => { contains => [1] },
# Custom fields are busted because they can be NULL.
FIELD_TYPE_FREETEXT, { contains => [5] },
FIELD_TYPE_BUG_ID, { contains => [5] },
FIELD_TYPE_DATETIME, { contains => [5] },
FIELD_TYPE_TEXTAREA, { contains => [5] },
);
# Shared between greaterthan and greaterthaneq.
#
# As with other fields, longdescs greaterthan matches if any comment
# matches (which might be OK).
#
# Same for keywords, bug_group, and cc. Logically, all of these might
# be OK, but it makes the operation not the logical reverse of
# lessthaneq. What we're really saying here by marking these broken
# is that there ought to be some way of searching "all ccs" vs "any cc"
# (and same for the other fields).
use constant GREATERTHAN_BROKEN => (
bug_group => { contains => [1] },
cc => { contains => [1] },
keywords => { contains => [1] },
longdesc => { contains => [1] },
FIELD_TYPE_MULTI_SELECT, { contains => [1] },
);
# allwords and allwordssubstr have these broken tests in common.
#
# allwordssubstr work_time only matches against a single comment,
# instead of matching against all comments on a bug. Same is true
# for the other longdesc fields, cc, keywords, and bug_group.
#
# percentage_complete just drops in 0=0 for the term.
use constant ALLWORDS_BROKEN => (
ATTACHMENT_BOOLEANS_SEARCH_BROKEN,
bug_group => { contains => [1] },
cc => { contains => [1] },
keywords => { contains => [1] },
longdesc => { contains => [1] },
work_time => { contains => [1] },
percentage_complete => { contains => [2,3,4,5] },
);
# nowords and nowordssubstr have these broken tests in common.
#
# flagtypes.name doesn't match bugs without flags.
# cc, keywords, longdescs.isprivate, and bug_group actually work properly in
# terms of excluding bug 1 (since we exclude all values in the search,
# on our test), but still fail at including bug 5.
# The longdesc* and work_time fields, coincidentally, work completely
# correctly, possibly because there's only one comment on bug 5.
use constant NOWORDS_BROKEN => (
NEGATIVE_BROKEN,
'flagtypes.name' => { contains => [5] },
bug_group => { contains => [5] },
cc => { contains => [5] },
keywords => { contains => [5] },
longdesc => {},
work_time => {},
'longdescs.isprivate' => {},
);
# Fields that don't generally work at all with changed* searches, but
# probably should.
use constant CHANGED_BROKEN => (
classification => { contains => [1] },
commenter => { contains => [1] },
percentage_complete => { contains => [2,3,4,5] },
'requestees.login_name' => { contains => [1] },
'setters.login_name' => { contains => [1] },
delta_ts => { contains => [1] },
);
# These are additional broken tests that changedfrom and changedto
# have in common.
use constant CHANGED_VALUE_BROKEN => (
bug_group => { contains => [1] },
cc => { contains => [1] },
estimated_time => { contains => [1] },
'flagtypes.name' => { contains => [1] },
keywords => { contains => [1] },
work_time => { contains => [1] },
FIELD_TYPE_MULTI_SELECT, { contains => [1] },
);
# Any test listed in KNOWN_BROKEN gets marked TODO by Test::More
# (using some complex code in Bugzilla::Test::Seach::FieldTest).
# This means that if you run the test under "prove -v", these tests will
# still show up as "not ok", but the test suite results won't show them
# as a failure.
#
# This constant contains operators as keys, which point to hashes. The hashes
# have field names as keys. Each field name points to a hash describing
# how that field/operator combination is broken. The "contains"
# array specifies that that particular "contains" test is expected
# to fail. If "search" is set to 1, then we expect the creation of the
# Bugzilla::Search object to fail.
#
# To allow handling custom fields, you can also use the field type as a key
# instead of the field name. Specifying explicit field names always overrides
# specifying a field type.
#
# Sometimes the operators have multiple tests, and one of them works
# while the other fails. In this case, we have a special override for
# "operator-value", which uniquely identifies tests.
use constant KNOWN_BROKEN => {
notequals => { NEGATIVE_BROKEN },
# percentage_complete substring matches every bug, regardless of
# its percentage_complete value.
substring => {
percentage_complete => { contains => [2,3,4,5] },
},
casesubstring => {
percentage_complete => { contains => [2,3,4,5] },
},
notsubstring => { NEGATIVE_BROKEN },
# Attachment noolean fields don't work with regexes, right now,
# because they throw an error that regexes are not valid booleans.
'regexp-^1-' => { ATTACHMENT_BOOLEANS_SEARCH_BROKEN },
# percentage_complete notregexp fails to match bugs that
# fully lack hours worked.
notregexp => {
NEGATIVE_BROKEN,
percentage_complete => { contains => [5] },
},
'notregexp-^1-' => { ATTACHMENT_BOOLEANS_SEARCH_BROKEN },
# percentage_complete doesn't match bugs with 0 hours worked or remaining.
#
# longdescs.isprivate matches if any comment matches, instead of if all
# comments match. Same for longdescs and work_time. (Commenter is probably
# also broken in this way, but all our comments come from the same user.)
# Also, the attachments ones don't find bugs that have no attachments
# at all (which might be OK?).
#
# attachments.isprivate lessthan doesn't find bugs without attachments.
lessthan => {
ATTACHMENT_BOOLEANS_SEARCH_BROKEN,
'attachments.isprivate' => { contains => [5] },
'longdescs.isprivate' => { contains => [1] },
percentage_complete => { contains => [5] },
work_time => { contains => [1,2,3,4] },
},
# The lessthaneq tests are broken for the same reasons, but they work
# slightly differently so they have a different set of broken tests.
lessthaneq => {
ATTACHMENT_BOOLEANS_CONTAINS_BROKEN,
'longdescs.isprivate' => { contains => [1] },
work_time => { contains => [2,3,4] },
},
greaterthan => { GREATERTHAN_BROKEN },
# percentage_complete is broken -- it won't match equal values.
greaterthaneq => {
GREATERTHAN_BROKEN,
percentage_complete => { contains => [2] },
},
# percentage_complete just throws 0=0 into the search term, returning
# all bugs.
anyexact => {
ATTACHMENT_BOOLEANS_SEARCH_BROKEN,
percentage_complete => { contains => [3,4,5] },
},
# bug_group anywordssubstr returns all our bugs. Not sure why.
anywordssubstr => {
ATTACHMENT_BOOLEANS_SEARCH_BROKEN,
percentage_complete => { contains => [3,4,5] },
bug_group => { contains => [3,4,5] },
},
'allwordssubstr-<1>' => { ALLWORDS_BROKEN },
'allwordssubstr-<1>,<2>' => {
ATTACHMENT_BOOLEANS_SEARCH_BROKEN,
percentage_complete => { contains => [1,2,3,4,5] },
},
# flagtypes.name does not work here, probably because they all try to
# match against a single flag.
# Same for attach_data.thedata.
'allwords-<1>' => {
ALLWORDS_BROKEN,
'attach_data.thedata' => { contains => [1] },
'flagtypes.name' => { contains => [1] },
},
'allwords-<1> <2>' => {
ATTACHMENT_BOOLEANS_SEARCH_BROKEN,
percentage_complete => { contains => [1,2,3,4,5] },
},
nowordssubstr => { NOWORDS_BROKEN },
# attach_data.thedata doesn't match properly with any of the plain
# "words" searches. Also, bug 5 doesn't match because it lacks
# attachments.
nowords => {
NOWORDS_BROKEN,
'attach_data.thedata' => { contains => [1,5] },
},
# anywords searches don't work on decimal values.
# bug_group anywords returns all bugs.
# attach_data doesn't work (perhaps because it's the entire
# data, or some problem with the regex?).
anywords => {
ATTACHMENT_BOOLEANS_SEARCH_BROKEN,
'attach_data.thedata' => { contains => [1] },
bug_group => { contains => [2,3,4,5] },
percentage_complete => { contains => [2,3,4,5] },
work_time => { contains => [1] },
},
'anywords-<1> <2>' => {
bug_group => { contains => [3,4,5] },
percentage_complete => { contains => [3,4,5] },
'attach_data.thedata' => { contains => [1,2] },
work_time => { contains => [1,2] },
},
# setters.login_name and requestees.login name aren't tracked individually
# in bugs_activity, so can't be searched using this method.
#
# percentage_complete isn't tracked in bugs_activity (and it would be
# really hard to track). However, it adds a 0=0 term instead of using
# the changed* charts or simply denying them.
#
# delta_ts changedbefore/after should probably search for bugs based
# on their delta_ts.
#
# creation_ts changedbefore/after should search for bug creation dates.
#
# The commenter field changedbefore/after should search for comment
# creation dates.
#
# classification isn't being tracked properly in bugs_activity, I think.
#
# attach_data.thedata should search when attachments were created and
# who they were created by.
'changedbefore' => {
CHANGED_BROKEN,
'attach_data.thedata' => { contains => [1] },
creation_ts => { contains => [1,5] },
# attachments.* finds values where the date matches exactly.
'attachments.description' => { contains => [2] },
'attachments.filename' => { contains => [2] },
'attachments.isobsolete' => { contains => [2] },
'attachments.ispatch' => { contains => [2] },
'attachments.isprivate' => { contains => [2] },
'attachments.mimetype' => { contains => [2] },
},
'changedafter' => {
'attach_data.thedata' => { contains => [2,3,4] },
classification => { contains => [2,3,4] },
commenter => { contains => [2,3,4] },
creation_ts => { contains => [2,3,4] },
delta_ts => { contains => [2,3,4] },
percentage_complete => { contains => [1,5] },
'requestees.login_name' => { contains => [2,3,4] },
'setters.login_name' => { contains => [2,3,4] },
},
changedfrom => {
CHANGED_BROKEN,
CHANGED_VALUE_BROKEN,
# All fields should have a way to search for "changing
# from a blank value" probably.
blocked => { contains => [1] },
dependson => { contains => [1] },
FIELD_TYPE_BUG_ID, { contains => [1] },
},
# changeto doesn't find work_time changes (probably due to decimal/string
# stuff). Same for remaining_time and estimated_time.
#
# multi-valued fields are stored as comma-separated strings, so you
# can't do changedfrom/to on them.
#
# Perhaps commenter can either tell you who the last commenter was,
# or if somebody commented at a given time (combined with other
# charts).
#
# longdesc changedto/from doesn't do anything; maybe it should.
# Same for attach_data.thedata.
changedto => {
CHANGED_BROKEN,
CHANGED_VALUE_BROKEN,
'attach_data.thedata' => { contains => [1] },
longdesc => { contains => [1] },
remaining_time => { contains => [1] },
},
changedby => {
CHANGED_BROKEN,
# This should probably search the attacher or anybody who changed
# anything about an attachment at all.
'attach_data.thedata' => { contains => [1] },
# This should probably search the reporter.
creation_ts => { contains => [1] },
},
};
#############
# Overrides #
#############
# These overrides are used in the TESTS constant, below.
# Regex tests need unique test values for certain fields.
use constant REGEX_OVERRIDE => {
'attachments.mimetype' => { value => '^text/x-1-' },
bug_file_loc => { value => '^http://1-' },
see_also => { value => '^http://1-' },
blocked => { value => '^<1>$' },
dependson => { value => '^<1>$' },
bug_id => { value => '^<1>$' },
'attachments.isprivate' => { value => '^1' },
cclist_accessible => { value => '^1' },
reporter_accessible => { value => '^1' },
everconfirmed => { value => '^1' },
'longdescs.isprivate' => { value => '^1' },
creation_ts => { value => '^2037-01-01' },
delta_ts => { value => '^2037-01-01' },
deadline => { value => '^2037-02-01' },
estimated_time => { value => '^1.0' },
remaining_time => { value => '^9.0' },
work_time => { value => '^1.0' },
longdesc => { value => '^1-' },
percentage_complete => { value => '^10.0' },
FIELD_TYPE_BUG_ID, { value => '^<1>$' },
FIELD_TYPE_DATETIME, { value => '^2037-03-01' }
};
# Common overrides between lessthan and lessthaneq.
use constant LESSTHAN_OVERRIDE => (
alias => { contains => [1,5] },
estimated_time => { contains => [1,5] },
qa_contact => { contains => [1,5] },
resolution => { contains => [1,5] },
status_whiteboard => { contains => [1,5] },
target_milestone => { contains => [1,5] },
);
# The mandatorily-set fields have values higher than <1>,
# so bug 5 shows up.
use constant GREATERTHAN_OVERRIDE => (
classification => { contains => [2,3,4,5] },
assigned_to => { contains => [2,3,4,5] },
bug_id => { contains => [2,3,4,5] },
bug_severity => { contains => [2,3,4,5] },
bug_status => { contains => [2,3,4,5] },
component => { contains => [2,3,4,5] },
commenter => { contains => [2,3,4,5] },
op_sys => { contains => [2,3,4,5] },
priority => { contains => [2,3,4,5] },
product => { contains => [2,3,4,5] },
reporter => { contains => [2,3,4,5] },
rep_platform => { contains => [2,3,4,5] },
short_desc => { contains => [2,3,4,5] },
version => { contains => [2,3,4,5] },
# Bug 2 is the only bug besides 1 that has a Requestee set.
'requestees.login_name' => { contains => [2] },
FIELD_TYPE_SINGLE_SELECT, { contains => [2,3,4,5] },
# Override SINGLE_SELECT for resolution.
resolution => { contains => [2,3,4] },
);
# For all positive multi-value types.
use constant MULTI_BOOLEAN_OVERRIDE => (
'attachments.ispatch' => { value => '1,1', contains => [1] },
'attachments.isobsolete' => { value => '1,1', contains => [1] },
'attachments.isprivate' => { value => '1,1', contains => [1] },
cclist_accessible => { value => '1,1', contains => [1] },
reporter_accessible => { value => '1,1', contains => [1] },
'longdescs.isprivate' => { value => '1,1', contains => [1] },
everconfirmed => { value => '1,1', contains => [1] },
);
# Same as above, for negative multi-value types.
use constant NEGATIVE_MULTI_BOOLEAN_OVERRIDE => (
'attachments.ispatch' => { value => '1,1', contains => [2,3,4,5] },
'attachments.isobsolete' => { value => '1,1', contains => [2,3,4,5] },
'attachments.isprivate' => { value => '1,1', contains => [2,3,4,5] },
cclist_accessible => { value => '1,1', contains => [2,3,4,5] },
reporter_accessible => { value => '1,1', contains => [2,3,4,5] },
'longdescs.isprivate' => { value => '1,1', contains => [2,3,4,5] },
everconfirmed => { value => '1,1', contains => [2,3,4,5] },
);
# For anyexact and anywordssubstr
use constant ANY_OVERRIDE => (
'work_time' => { value => '1.0,2.0' },
dependson => { value => '<1>,<3>', contains => [1,3] },
MULTI_BOOLEAN_OVERRIDE,
);
# For all the changed* searches. The ones that have empty contains
# are fields that never change in value, or will never be rationally
# tracked in bugs_activity.
use constant CHANGED_OVERRIDE => (
'attachments.submitter' => { contains => [] },
bug_id => { contains => [] },
reporter => { contains => [] },
);
#########
# Tests #
#########
# The basic format of this is a hashref, where the keys are operators,
# and each operator has an arrayref of tests that it runs. The tests
# are hashrefs, with the following possible keys:
#
# contains: This is a list of bug numbers that the search is expected
# to contain. (This is bug numbers, like 1,2,3, not the bug
# ids. For a description of each bug number, see NUM_BUGS.)
# Any bug not listed in "contains" must *not* show up in the
# search result.
# value: The value that you're searching for. There are certain special
# codes that will be replaced with bug values when the tests are
# run. In these examples below, "#" indicates a bug number:
#
# <#> - The field value for this bug.
#
# For any operator that has the string "word" in it, this is
# *all* the values for the current field from the numbered bug,
# joined by a space.
#
# If the operator has the string "substr" in it, then we
# take a substring of the value (for single-value searches)
# or we take a substring of each value and join them (for
# multi-value "word" searches). The length of the substring
# is determined by the SUBSTR_SIZE constants above.)
#
# For other operators, this just becomes the first value from
# the field for the numbered bug.
#
# So, if we were running the "equals" test and checking the
# cc field, <1> would become the login name of the first cc on
# Bug 1. If we did an "anywords" search test, it would become
# a space-separated string of the login names of all the ccs
# on Bug 1. If we did an "anywordssubstr" search test, it would
# become a space-separated string of the first few characters
# of each CC's login name on Bug 1.
#
# <#-id> - The bug id of the numbered bug.
# <#-reporter> - The login name of the numbered bug's reporter.
# <#-delta> - The delta_ts of the numbered bug.
#
# escape: If true, we will call quotemeta() on the value immediately
# before passing it to Search.pm.
#
# transform: A function to call on any field value before inserting
# it for a <#> replacement. The transformation function
# gets all of the bug's values for the field as its arguments.
# if_equal: This allows you to override "contains" for the case where
# the transformed value (from calling the "transform" function)
# is equal to the original value.
#
# override: This allows you to override "contains" and "values" for
# certain fields.
use constant TESTS => {
equals => [
{ contains => [1], value => '<1>' },
],
notequals => [
{ contains => [2,3,4,5], value => '<1>' },
],
substring => [
{ contains => [1], value => '<1>' },
],
casesubstring => [
{ contains => [1], value => '<1>' },
{ contains => [], value => '<1>', transform => sub { lc($_[0]) },
extra_name => 'lc', if_equal => { contains => [1] } },
],
notsubstring => [
{ contains => [2,3,4,5], value => '<1>' },
],
regexp => [
{ contains => [1], value => '<1>', escape => 1 },
{ contains => [1], value => '^1-', override => REGEX_OVERRIDE },
],
notregexp => [
{ contains => [2,3,4,5], value => '<1>', escape => 1 },
{ contains => [2,3,4,5], value => '^1-', override => REGEX_OVERRIDE },
],
lessthan => [
{ contains => [1], value => 2,
override => {
# A lot of these contain bug 5 because an empty value is validly
# less than the specified value.
bug_file_loc => { value => 'http://2-' },
see_also => { value => 'http://2-' },
'attachments.mimetype' => { value => 'text/x-2-' },
blocked => { value => '<4-id>', contains => [1,2] },
dependson => { value => '<3-id>', contains => [1,3] },
bug_id => { value => '<2-id>' },
'attachments.isprivate' => { value => 1, contains => [2,3,4,5] },
cclist_accessible => { value => 1, contains => [2,3,4,5] },
reporter_accessible => { value => 1, contains => [2,3,4,5] },
'longdescs.isprivate' => { value => 1, contains => [2,3,4,5] },
everconfirmed => { value => 1, contains => [2,3,4,5] },
creation_ts => { value => '2037-01-02', contains => [1,5] },
delta_ts => { value => '2037-01-02', contains => [1,5] },
deadline => { value => '2037-02-02' },
remaining_time => { value => 10, contains => [1,5] },
percentage_complete => { value => 11, contains => [1,5] },
longdesc => { value => '2-', contains => [1,5] },
work_time => { value => 1, contains => [5] },
FIELD_TYPE_BUG_ID, { value => '<2>' },
FIELD_TYPE_DATETIME, { value => '2037-03-02' },
LESSTHAN_OVERRIDE,
}
},
],
lessthaneq => [
{ contains => [1], value => '<1>',
override => {
'attachments.ispatch' => { value => 0, contains => [2,3,4,5] },
'attachments.isobsolete' => { value => 0, contains => [2,3,4,5] },
'attachments.isprivate' => { value => 0, contains => [2,3,4,5] },
cclist_accessible => { value => 0, contains => [2,3,4,5] },
reporter_accessible => { value => 0, contains => [2,3,4,5] },
'longdescs.isprivate' => { value => 0, contains => [2,3,4,5] },
everconfirmed => { value => 0, contains => [2,3,4,5] },
blocked => { contains => [1,2] },
dependson => { contains => [1,3] },
creation_ts => { contains => [1,5] },
delta_ts => { contains => [1,5] },
remaining_time => { contains => [1,5] },
longdesc => { contains => [1,5] },
work_time => { value => 1, contains => [1,5] },
LESSTHAN_OVERRIDE,
},
},
],
greaterthan => [
{ contains => [2,3,4], value => '<1>',
override => {
dependson => { contains => [3] },
blocked => { contains => [2] },
'attachments.ispatch' => { value => 0, contains => [1] },
'attachments.isobsolete' => { value => 0, contains => [1] },
'attachments.isprivate' => { value => 0, contains => [1] },
cclist_accessible => { value => 0, contains => [1] },
reporter_accessible => { value => 0, contains => [1] },
'longdescs.isprivate' => { value => 0, contains => [1] },
everconfirmed => { value => 0, contains => [1] },
GREATERTHAN_OVERRIDE,
},
},
],
greaterthaneq => [
{ contains => [2,3,4], value => '<2>',
override => {
'attachments.ispatch' => { value => 1, contains => [1] },
'attachments.isobsolete' => { value => 1, contains => [1] },
'attachments.isprivate' => { value => 1, contains => [1] },
cclist_accessible => { value => 1, contains => [1] },
reporter_accessible => { value => 1, contains => [1] },
'longdescs.isprivate' => { value => 1, contains => [1] },
everconfirmed => { value => 1, contains => [1] },
dependson => { contains => [1,3] },
blocked => { contains => [1,2] },
GREATERTHAN_OVERRIDE,
}
},
],
matches => [
{ contains => [1], value => '<1>' },
],
notmatches => [
{ contains => [2,3,4,5], value => '<1>' },
],
anyexact => [
{ contains => [1,2], value => '<1>,<2>',
override => { ANY_OVERRIDE } },
],
anywordssubstr => [
{ contains => [1,2], value => '<1> <2>',
override => { ANY_OVERRIDE } },
],
allwordssubstr => [
{ contains => [1], value => '<1>',
override => { MULTI_BOOLEAN_OVERRIDE } },
{ contains => [], value => '<1>,<2>' },
],
nowordssubstr => [
{ contains => [2,3,4,5], value => '<1>',
override => {
# longdescs.isprivate translates to "1 0", so no bugs should
# show up.
'longdescs.isprivate' => { contains => [] },
# 1.0 0.0 exludes bug 5.
# XXX However, it also shouldn't match 2, 3, or 4, because
# they contain at least one comment with 0.0 work_time.
work_time => { contains => [2,3,4] },
}
},
],
anywords => [
{ contains => [1], value => '<1>',
override => {
MULTI_BOOLEAN_OVERRIDE,
work_time => { value => '1.0', contains => [1] },
}
},
{ contains => [1,2], value => '<1> <2>',
override => {
MULTI_BOOLEAN_OVERRIDE,
dependson => { value => '<1> <3>', contains => [1,3] },
work_time => { value => '1.0 2.0' },
},
},
],
allwords => [
{ contains => [1], value => '<1>',
override => { MULTI_BOOLEAN_OVERRIDE } },
{ contains => [], value => '<1> <2>' },
],
nowords => [
{ contains => [2,3,4,5], value => '<1>',
override => {
# longdescs.isprivate translates to "1 0", so no bugs should
# show up.
'longdescs.isprivate' => { contains => [] },
# 1.0 0.0 exludes bug 5.
# XXX However, it also shouldn't match 2, 3, or 4, because
# they contain at least one comment with 0.0 work_time.
work_time => { contains => [2,3,4] },
}
},
],
changedbefore => [
{ contains => [1], value => '<2-delta>',
override => {
CHANGED_OVERRIDE,
creation_ts => { contains => [1,5] },
blocked => { contains => [1,2] },
dependson => { contains => [1,3] },
longdesc => { contains => [1,2,5] },
}
},
],
changedafter => [
{ contains => [2,3,4], value => '<1-delta>',
override => {
CHANGED_OVERRIDE,
creation_ts => { contains => [2,3,4] },
# We only change this for one bug, and it doesn't match.
'longdescs.isprivate' => { contains => [] },
# Same for everconfirmed.
'everconfirmed' => { contains => [] },
# For blocked and dependson, they have the delta_ts of bug1
# in the bugs_activity table, so they won't ever match.
blocked => { contains => [] },
dependson => { contains => [] },
}
},
],
changedfrom => [
{ contains => [1], value => '<1>',
override => {
CHANGED_OVERRIDE,
# longdesc changedfrom doesn't make any sense.
longdesc => { contains => [] },
# Nor does creation_ts changedfrom.
creation_ts => { contains => [] },
'attach_data.thedata' => { contains => [] },
},
},
],
changedto => [
{ contains => [1], value => '<1>',
override => {
CHANGED_OVERRIDE,
# I can't imagine any use for creation_ts changedto.
creation_ts => { contains => [] },
}
},
],
changedby => [
{ contains => [1], value => '<1-reporter>',
override => {
CHANGED_OVERRIDE,
blocked => { contains => [1,2] },
dependson => { contains => [1,3] },
},
},
],
};
# Fields that do not behave as we expect, for InjectionTest.
# search => 1 means the Bugzilla::Search creation fails.
# sql_error is a regex that specifies a SQL error that's OK for us to throw.
# operator_ok overrides the "brokenness" of certain operators, so that they
# are always OK for that field/operator combination.
use constant INJECTION_BROKEN_FIELD => {
'attachments.isobsolete' => { search => 1 },
'attachments.ispatch' => { search => 1 },
owner_idle_time => {
sql_error => qr/bugs\.owner_idle_time.+where clause/,
operator_ok => [qw(changedfrom changedto greaterthan greaterthaneq
lessthan lessthaneq)]
},
keywords => {
search => 1,
operator_ok => [qw(allwordssubstr anywordssubstr casesubstring
changedfrom changedto greaterthan greaterthaneq
lessthan lessthaneq notregexp notsubstring
nowordssubstr regexp substring)]
},
};
# Operators that do not behave as we expect, for InjectionTest.
# search => 1 means the Bugzilla::Search creation fails, but
# field_ok contains fields that it does actually succeed for.
use constant INJECTION_BROKEN_OPERATOR => {
changedafter => { search => 1, field_ok => ['percentage_complete'] },
changedbefore => { search => 1, field_ok => ['percentage_complete'] },
changedby => { search => 1, field_ok => ['percentage_complete'] },
};
# Tests run by Bugzilla::Test::Search::InjectionTest.
# We have to make sure the values are all one word or they'll be split
# up by the multi-word tests.
use constant INJECTION_TESTS => (
{ value => ';SEMICOLON_TEST' },
{ value => '--COMMENT_TEST' },
{ value => "'QUOTE_TEST" },
{ value => "';QUOTE_SEMICOLON_TEST" },
{ value => '/*STAR_COMMENT_TEST' }
);
# This overrides KNOWN_BROKEN for OR configurations.
# It indicates that these combinations are broken in some way that they
# aren't broken when alone, because they don't return what they logically
# should when put into an OR.
use constant OR_BROKEN => {
# Multi-value fields search on individual values, so "equals" OR "notequals"
# returns nothing, when it should instead logically return everything.
'blocked-equals' => {
'blocked-notequals' => { contains => [1,2,3,4,5] },
},
'dependson-equals' => {
'dependson-notequals' => { contains => [1,2,3,4,5] },
},
'bug_group-equals' => {
'bug_group-notequals' => { contains => [1,2,3,4,5] },
},
'cc-equals' => {
'cc-notequals' => { contains => [1,2,3,4,5] },
},
'commenter-equals' => {
'commenter-notequals' => { contains => [1,2,3,4,5] },
'longdesc-notequals' => { contains => [2,3,4,5] },
'longdescs.isprivate-notequals' => { contains => [2,3,4,5] },
'work_time-notequals' => { contains => [2,3,4,5] },
},
'commenter-notequals' => {
'commenter-equals' => { contains => [1,2,3,4,5] },
'longdesc-equals' => { contains => [1] },
'longdescs.isprivate-equals' => { contains => [1] },
'work_time-equals' => { contains => [1] },
},
};
1;
# -*- Mode: perl; indent-tabs-mode: nil -*-
#
# The contents of this file are subject to the Mozilla Public
# License Version 1.1 (the "License"); you may not use this file
# except in compliance with the License. You may obtain a copy of
# the License at http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS
# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
# implied. See the License for the specific language governing
# rights and limitations under the License.
#
# The Original Code is the Bugzilla Bug Tracking System.
#
# The Initial Developer of the Original Code is Everything Solved, Inc.
# Portions created by the Initial Developer are Copyright (C) 2010 the
# Initial Developer. All Rights Reserved.
#
# Contributor(s):
# Max Kanat-Alexander <mkanat@bugzilla.org>
# Calling CGI::param over and over turned out to be one of the slowest
# parts of search.t. So we create a simpler thing here that just supports
# "param" in a fast way.
package Bugzilla::Test::Search::FakeCGI;
sub new {
my ($class) = @_;
return bless {}, $class;
}
sub param {
my ($self, $name, @values) = @_;
if (!defined $name) {
return keys %$self;
}
if (@values) {
if (ref $values[0] eq 'ARRAY') {
$self->{$name} = $values[0];
}
else {
$self->{$name} = \@values;
}
}
return () if !exists $self->{$name};
my $item = $self->{$name};
return wantarray ? @{ $item || [] } : $item->[0];
}
sub delete {
my ($self, $name) = @_;
delete $self->{$name};
}
# We don't need to do this, because we don't use old params in search.t.
sub convert_old_params {}
1;
\ No newline at end of file
# -*- Mode: perl; indent-tabs-mode: nil -*-
#
# The contents of this file are subject to the Mozilla Public
# License Version 1.1 (the "License"); you may not use this file
# except in compliance with the License. You may obtain a copy of
# the License at http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS
# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
# implied. See the License for the specific language governing
# rights and limitations under the License.
#
# The Original Code is the Bugzilla Bug Tracking System.
#
# The Initial Developer of the Original Code is Everything Solved, Inc.
# Portions created by the Initial Developer are Copyright (C) 2010 the
# Initial Developer. All Rights Reserved.
#
# Contributor(s):
# Max Kanat-Alexander <mkanat@bugzilla.org>
# This module represents the tests that get run on a single
# operator/field combination for Bugzilla::Test::Search.
# This is where all the actual testing happens.
package Bugzilla::Test::Search::FieldTest;
use strict;
use warnings;
use Bugzilla::Test::Search::FakeCGI;
use Bugzilla::Search;
use Bugzilla::Test::Search::Constants;
use Data::Dumper;
use Scalar::Util qw(blessed);
use Test::More;
use Test::Exception;
###############
# Constructor #
###############
sub new {
my ($class, $operator_test, $field, $test) = @_;
return bless { operator_test => $operator_test,
field_object => $field,
raw_test => $test }, $class;
}
#############
# Accessors #
#############
sub num_tests { return TESTS_PER_RUN }
# The Bugzilla::Test::Search::OperatorTest that this is a child of.
sub operator_test { return $_[0]->{operator_test} }
# The Bugzilla::Field being tested.
sub field_object { return $_[0]->{field_object} }
# The name of the field being tested, which we need much more often
# than we need the object.
sub field {
my ($self) = @_;
return $self->{field_name} ||= $self->field_object->name;
return $self->{field_name};
}
# The Bugzilla::Test::Search object that this is a child of.
sub search_test { return $_[0]->operator_test->search_test }
# The operator being tested
sub operator { return $_[0]->operator_test->operator }
# The bugs currently being tested by Bugzilla::Test::Search.
sub bugs { return $_[0]->search_test->bugs }
sub bug {
my $self = shift;
return $self->search_test->bug(@_);
}
# The name displayed for this test by Test::More. Used in test descriptions.
sub name {
my ($self) = @_;
my $field = $self->field;
my $operator = $self->operator;
my $value = $self->main_value;
my $name = "$field-$operator-$value";
if (my $extra_name = $self->test->{extra_name}) {
$name .= "-$extra_name";
}
return $name;
}
# The appropriate value from the TESTS constant for this test, taking
# into account overrides.
sub test {
my $self = shift;
return $self->{test} if $self->{test};
my %test = %{ $self->{raw_test} };
# We have field name overrides...
my $override = $test{override}->{$self->field};
# And also field type overrides.
if (!$override) {
$override = $test{override}->{$self->field_object->type} || {};
}
foreach my $key (%$override) {
$test{$key} = $override->{$key};
}
$self->{test} = \%test;
return $self->{test};
}
# All the values for all the bugs for this field.
sub _field_values {
my ($self) = @_;
return $self->{field_values} if $self->{field_values};
my %field_values;
foreach my $number (1..NUM_BUGS) {
$field_values{$number} = $self->_field_values_for_bug($number);
}
$self->{field_values} = \%field_values;
return $self->{field_values};
}
# The values for this field for the numbered bug.
sub bug_values {
my ($self, $number) = @_;
return @{ $self->_field_values->{$number} };
}
# The untranslated, non-overriden value--used in the name of the test
# and other places.
sub main_value { return $_[0]->{raw_test}->{value} }
# The untranslated test value, taking into account overrides.
sub test_value { return $_[0]->test->{value} };
# The value translated appropriately for passing to Bugzilla::Search.
sub translated_value {
my $self = shift;
if (!exists $self->{translated_value}) {
my $value = $self->search_test->value_translation_cache($self);
if (!defined $value) {
$value = $self->_translate_value();
$self->search_test->value_translation_cache($self, $value);
}
$self->{translated_value} = $value;
}
return $self->{translated_value};
}
# Used in failure diagnostic messages.
sub debug_value {
my ($self) = @_;
return "Value: '" . $self->translated_value . "'";
}
# True for a bug if we ran the "transform" function on it and the
# result was equal to its first value.
sub transformed_value_was_equal {
my ($self, $number, $value) = @_;
if (defined $value) {
$self->{transformed_value_was_equal}->{$number} = $value;
}
return $self->{transformed_value_was_equal}->{$number};
}
# True if this test is supposed to contain the numbered bug.
sub bug_is_contained {
my ($self, $number) = @_;
my $contains = $self->test->{contains};
if ($self->transformed_value_was_equal($number)) {
$contains = $self->test->{if_equal}->{contains};
}
return grep($_ == $number, @$contains) ? 1 : 0;
}
###################################################
# Accessors: Ways of doing SKIP and TODO on tests #
###################################################
# The tests we know are broken for this operator/field combination.
sub _known_broken {
my $self = shift;
my $field = $self->field;
my $type = $self->field_object->type;
my $operator = $self->operator;
my $value = $self->main_value;
my $value_name = "$operator-$value";
my $value_broken = KNOWN_BROKEN->{$value_name}->{$field};
$value_broken ||= KNOWN_BROKEN->{$value_name}->{$type};
return $value_broken if $value_broken;
my $operator_broken = KNOWN_BROKEN->{$operator}->{$field};
$operator_broken ||= KNOWN_BROKEN->{$operator}->{$type};
return $operator_broken if $operator_broken;
return {};
}
# True if the "contains" search for the numbered bug is broken.
# That is, either the result is supposed to contain it and doesn't,
# or the result is not supposed to contain it and does.
sub contains_known_broken {
my ($self, $number) = @_;
my $field = $self->field;
my $operator = $self->operator;
my $contains_broken = $self->_known_broken->{contains} || [];
if (grep($_ == $number, @$contains_broken)) {
return "$field $operator contains $number is known to be broken";
}
return undef;
}
# Returns a string if creating a Bugzilla::Search object throws an error,
# with this field/operator/value combination.
sub search_known_broken {
my ($self) = @_;
my $field = $self->field;
my $operator = $self->operator;
if ($self->_known_broken->{search}) {
return "Bugzilla::Search for $field $operator is known to be broken";
}
return undef;
}
# Returns a string if we haven't yet implemented the tests for this field,
# but we plan to in the future.
sub field_not_yet_implemented {
my ($self) = @_;
my $skip_this_field = grep { $_ eq $self->field } SKIP_FIELDS;
if ($skip_this_field) {
my $field = $self->field;
return "$field testing not yet implemented";
}
return undef;
}
# Returns a message if this field/operator combination can't ever be run.
# At no time in the future will this field/operator combination ever work.
sub invalid_field_operator_combination {
my ($self) = @_;
my $field = $self->field;
my $operator = $self->operator;
if ($field eq 'content' && $operator !~ /matches/) {
return "content field does not support $operator";
}
elsif ($operator =~ /matches/ && $field ne 'content') {
return "matches operator does not support fields other than content";
}
return undef;
}
# True if this field is broken in an OR combination.
sub join_broken {
my ($self, $or_broken_map) = @_;
my $or_broken = $or_broken_map->{$self->field . '-' . $self->operator};
if (!$or_broken) {
# See if this is a comment field, and in that case, if there's
# a generic entry for all comment fields.
my $is_comment_field = COMMENT_FIELDS->{$self->field};
if ($is_comment_field) {
$or_broken = $or_broken_map->{'longdescs.-' . $self->operator};
}
}
return $or_broken;
}
#########################################
# Accessors: Bugzilla::Search Arguments #
#########################################
# The CGI object that will get passed to Bugzilla::Search as its arguments.
sub search_params {
my $self = shift;
return $self->{search_params} if $self->{search_params};
my $field = $self->field;
my $operator = $self->operator;
my $value = $self->translated_value;
my $cgi = new Bugzilla::Test::Search::FakeCGI;
$cgi->param("field0-0-0", $field);
$cgi->param('type0-0-0', $operator);
$cgi->param('value0-0-0', $value);
$self->{search_params} = $cgi;
return $self->{search_params};
}
sub search_columns {
my ($self) = @_;
my $field = $self->field;
my @search_fields = qw(bug_id);
if ($self->field_object->buglist) {
my $col_name = COLUMN_TRANSLATION->{$field} || $field;
push(@search_fields, $col_name);
}
return \@search_fields;
}
################
# Field Values #
################
sub _field_values_for_bug {
my ($self, $number) = @_;
my $field = $self->field;
my @values;
if ($field =~ /^attach.+\.(.+)$/ ) {
my $attach_field = $1;
$attach_field = ATTACHMENT_FIELDS->{$attach_field} || $attach_field;
@values = $self->_values_for($number, 'attachments', $attach_field);
}
elsif (my $flag_field = FLAG_FIELDS->{$field}) {
@values = $self->_values_for($number, 'flags', $flag_field);
}
elsif (my $translation = COMMENT_FIELDS->{$field}) {
@values = $self->_values_for($number, 'comments', $translation);
# We want the last value to come first, so that single-value
# searches use the last comment.
@values = reverse @values;
}
elsif ($field eq 'bug_group') {
@values = $self->_values_for($number, 'groups_in', 'name');
}
elsif ($field eq 'keywords') {
@values = $self->_values_for($number, 'keyword_objects', 'name');
}
elsif ($field eq 'content') {
@values = $self->_values_for($number, 'short_desc');
}
# Bugzilla::Bug truncates creation_ts, but we need the full value
# from the database. This has no special value for changedfrom,
# because it never changes.
elsif ($field eq 'creation_ts') {
my $bug = $self->bug($number);
my $creation_ts = Bugzilla->dbh->selectrow_array(
'SELECT creation_ts FROM bugs WHERE bug_id = ?',
undef, $bug->id);
@values = ($creation_ts);
}
else {
@values = $self->_values_for($number, $field);
}
# We convert user objects to their login name, here, all in one
# block for simplicity.
if (grep { $_ eq $field } USER_FIELDS) {
# requestees.login_name is empty for most bugs (but checking
# blessed(undef) handles that.
# Values that come from %original_values aren't User objects.
@values = map { blessed($_) ? $_->login : $_ } @values;
@values = grep { defined $_ } @values;
}
return \@values;
}
sub _values_for {
my ($self, $number, $bug_field, $item_field) = @_;
my $item;
if ($self->operator eq 'changedfrom') {
$item = $self->search_test->bug_create_value($number, $bug_field);
}
else {
my $bug = $self->bug($number);
$item = $bug->$bug_field;
}
if ($item_field) {
if ($bug_field eq 'flags' and $item_field eq 'name') {
return (map { $_->name . $_->status } @$item);
}
return (map { $self->_get_item($_, $item_field) } @$item);
}
return @$item if ref($item) eq 'ARRAY';
return $item if defined $item;
return ();
}
sub _get_item {
my ($self, $from, $field) = @_;
if (blessed($from)) {
return $from->$field;
}
return $from->{$field};
}
#####################
# Value Translation #
#####################
# This function translates the "value" specified in TESTS into an actual
# search value to pass to Search.pm. This means that we get the value
# from the current bug (or, in the case of changedfrom, from %original_values)
# and then we insert it as required into the "value" from TESTS. (For example,
# <1> becomes the value for the field from bug 1.)
sub _translate_value {
my $self = shift;
my $value = $self->test_value;
foreach my $number (1..NUM_BUGS) {
$value = $self->_translate_value_for_bug($number, $value);
}
return $value;
}
sub _translate_value_for_bug {
my ($self, $number, $value) = @_;
my $bug = $self->bug($number);
my $bug_id = $bug->id;
$value =~ s/<$number-id>/$bug_id/g;
my $bug_delta = $bug->delta_ts;
$value =~ s/<$number-delta>/$bug_delta/g;
my $reporter = $bug->reporter->login;
$value =~ s/<$number-reporter>/$reporter/g;
my @bug_values = $self->bug_values($number);
return $value if !@bug_values;
if ($self->operator =~ /substr/) {
@bug_values = map { $self->_substr_value($_) } @bug_values;
}
my $string_value = $bug_values[0];
if ($self->operator =~ /word/) {
$string_value = join(' ', @bug_values);
}
if (my $func = $self->test->{transform}) {
my $transformed = $func->(@bug_values);
my $is_equal = $transformed eq $bug_values[0] ? 1 : 0;
$self->transformed_value_was_equal($number, $is_equal);
$string_value = $transformed;
}
if ($self->test->{escape}) {
$string_value = quotemeta($string_value);
}
$value =~ s/<$number>/$string_value/g;
return $value;
}
sub _substr_value {
my ($self, $value) = @_;
my $field = $self->field;
my $substr_size = SUBSTR_SIZE;
if (exists FIELD_SUBSTR_SIZE->{$field}) {
$substr_size = FIELD_SUBSTR_SIZE->{$field};
}
if ($substr_size > 0) {
return substr($value, 0, $substr_size);
}
return substr($value, $substr_size);
}
#####################
# Main Test Methods #
#####################
sub run {
my ($self) = @_;
my $invalid_combination = $self->invalid_field_operator_combination;
my $field_not_implemented = $self->field_not_yet_implemented;
SKIP: {
skip($invalid_combination, $self->num_tests) if $invalid_combination;
TODO: {
todo_skip ($field_not_implemented, $self->num_tests) if $field_not_implemented;
$self->do_tests();
}
}
}
sub do_tests {
my ($self) = @_;
my $name = $self->name;
my $search_broken = $self->search_known_broken;
my $search;
TODO: {
local $TODO = $search_broken if $search_broken;
$search = $self->_test_search_object_creation();
}
my ($results, $sql);
SKIP: {
skip "Can't run SQL without Search object", 2 if !$search;
lives_ok { $sql = $search->getSQL() } "$name: get SQL";
# This prevents warnings from DBD::mysql if we pass undef $sql,
# which happens if "new Bugzilla::Search" fails.
$sql ||= '';
$results = $self->_test_sql($sql);
}
$self->_test_content($results, $sql);
}
sub _test_search_object_creation {
my ($self) = @_;
my $name = $self->name;
my @args = (fields => $self->search_columns, params => $self->search_params);
my $search;
lives_ok { $search = new Bugzilla::Search(@args) }
"$name: create search object";
return $search;
}
sub _test_sql {
my ($self, $sql) = @_;
my $dbh = Bugzilla->dbh;
my $name = $self->name;
my $results;
lives_ok { $results = $dbh->selectall_arrayref($sql) } "$name: Run SQL Query"
or diag($sql);
return $results;
}
sub _test_content {
my ($self, $results, $sql) = @_;
SKIP: {
skip "Without results we can't test them", NUM_BUGS if !$results;
foreach my $number (1..NUM_BUGS) {
$self->_test_content_for_bug($number, $results, $sql);
}
}
}
sub _test_content_for_bug {
my ($self, $number, $results, $sql) = @_;
my $name = $self->name;
my $contains_known_broken = $self->contains_known_broken($number);
my %result_ids = map { $_->[0] => 1 } @$results;
my $bug_id = $self->bug($number)->id;
TODO: {
local $TODO = $contains_known_broken if $contains_known_broken;
if ($self->bug_is_contained($number)) {
ok($result_ids{$bug_id},
"$name: contains bug $number ($bug_id)")
or diag Dumper($results) . $self->debug_value . "\n\nSQL: $sql";
}
else {
ok(!$result_ids{$bug_id},
"$name: does not contain bug $number ($bug_id)")
or diag Dumper($results) . $self->debug_value . "\n\nSQL: $sql";
}
}
}
1;
\ No newline at end of file
# -*- Mode: perl; indent-tabs-mode: nil -*-
#
# The contents of this file are subject to the Mozilla Public
# License Version 1.1 (the "License"); you may not use this file
# except in compliance with the License. You may obtain a copy of
# the License at http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS
# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
# implied. See the License for the specific language governing
# rights and limitations under the License.
#
# The Original Code is the Bugzilla Bug Tracking System.
#
# The Initial Developer of the Original Code is Everything Solved, Inc.
# Portions created by the Initial Developer are Copyright (C) 2010 the
# Initial Developer. All Rights Reserved.
#
# Contributor(s):
# Max Kanat-Alexander <mkanat@bugzilla.org>
# This module represents the SQL Injection tests that get run on a single
# operator/field combination for Bugzilla::Test::Search.
package Bugzilla::Test::Search::InjectionTest;
use base qw(Bugzilla::Test::Search::FieldTest);
use strict;
use warnings;
use Bugzilla::Test::Search::Constants;
use Test::Exception;
sub num_tests { return NUM_SEARCH_TESTS }
sub _known_broken {
my ($self) = @_;
my $operator_broken = INJECTION_BROKEN_OPERATOR->{$self->operator};
# We don't want to auto-vivify $operator_broken and thus make it true.
my @field_ok = $operator_broken ? @{ $operator_broken->{field_ok} || [] }
: ();
return {} if grep { $_ eq $self->field } @field_ok;
my $field_broken = INJECTION_BROKEN_FIELD->{$self->field};
# We don't want to auto-vivify $field_broken and thus make it true.
my @operator_ok = $field_broken ? @{ $field_broken->{operator_ok} || [] }
: ();
return {} if grep { $_ eq $self->operator } @operator_ok;
return $operator_broken || $field_broken || {};
}
sub sql_error_ok { return $_[0]->_known_broken->{sql_error} }
# Injection tests don't have to skip any fields.
sub field_not_yet_implemented { undef }
# Injection tests don't do translation.
sub translated_value { $_[0]->test_value }
sub name { return "injection-" . $_[0]->SUPER::name; }
# Injection tests don't check content.
sub _test_content {}
sub _test_sql {
my $self = shift;
my ($sql) = @_;
my $dbh = Bugzilla->dbh;
my $name = $self->name;
if (my $error_ok = $self->sql_error_ok) {
throws_ok { $dbh->selectall_arrayref($sql) } $error_ok,
"$name: SQL query dies, as we expect";
return;
}
return $self->SUPER::_test_sql(@_);
}
1;
\ No newline at end of file
# -*- Mode: perl; indent-tabs-mode: nil -*-
#
# The contents of this file are subject to the Mozilla Public
# License Version 1.1 (the "License"); you may not use this file
# except in compliance with the License. You may obtain a copy of
# the License at http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS
# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
# implied. See the License for the specific language governing
# rights and limitations under the License.
#
# The Original Code is the Bugzilla Bug Tracking System.
#
# The Initial Developer of the Original Code is Everything Solved, Inc.
# Portions created by the Initial Developer are Copyright (C) 2010 the
# Initial Developer. All Rights Reserved.
#
# Contributor(s):
# Max Kanat-Alexander <mkanat@bugzilla.org>
# This module represents the tests that get run on a single operator
# from the TESTS constant in Bugzilla::Search::Test::Constants.
package Bugzilla::Test::Search::OperatorTest;
use strict;
use warnings;
use Bugzilla::Test::Search::Constants;
use Bugzilla::Test::Search::FieldTest;
use Bugzilla::Test::Search::InjectionTest;
use Bugzilla::Test::Search::OrTest;
use Bugzilla::Test::Search::AndTest;
###############
# Constructor #
###############
sub new {
my ($invocant, $operator, $search_test) = @_;
$search_test ||= $invocant->search_test;
my $class = ref($invocant) || $invocant;
return bless { search_test => $search_test, operator => $operator }, $class;
}
#############
# Accessors #
#############
# The Bugzilla::Test::Search object that this is a child of.
sub search_test { return $_[0]->{search_test} }
# The operator being tested
sub operator { return $_[0]->{operator} }
# The tests that we're going to run on this operator.
sub tests { return @{ TESTS->{$_[0]->operator } } }
# The fields we're going to test for this operator.
sub test_fields { return $_[0]->search_test->all_fields }
sub run {
my ($self) = @_;
foreach my $field ($self->test_fields) {
foreach my $test ($self->tests) {
my $field_test =
new Bugzilla::Test::Search::FieldTest($self, $field, $test);
$field_test->run();
next if !$self->search_test->option('long');
# Run the OR tests. This tests every other operator (including
# this operator itself) in combination with every other field,
# in an OR with this operator and field.
foreach my $other_operator ($self->search_test->all_operators) {
$self->run_join_tests($field_test, $other_operator);
}
}
foreach my $test (INJECTION_TESTS) {
my $injection_test =
new Bugzilla::Test::Search::InjectionTest($self, $field, $test);
$injection_test->run();
}
}
}
sub run_join_tests {
my ($self, $field_test, $other_operator) = @_;
my $other_operator_test = $self->new($other_operator);
foreach my $other_test ($other_operator_test->tests) {
foreach my $other_field ($self->test_fields) {
$self->_run_one_join_test($field_test, $other_operator_test,
$other_field, $other_test);
$self->search_test->clean_test_history();
}
}
}
sub _run_one_join_test {
my ($self, $field_test, $other_operator_test, $other_field, $other_test) = @_;
my $other_field_test =
new Bugzilla::Test::Search::FieldTest($other_operator_test,
$other_field, $other_test);
my $or_test = new Bugzilla::Test::Search::OrTest($field_test,
$other_field_test);
$or_test->run();
my $and_test = new Bugzilla::Test::Search::AndTest($field_test,
$other_field_test);
$and_test->run();
}
1;
\ No newline at end of file
# -*- Mode: perl; indent-tabs-mode: nil -*-
#
# The contents of this file are subject to the Mozilla Public
# License Version 1.1 (the "License"); you may not use this file
# except in compliance with the License. You may obtain a copy of
# the License at http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS
# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
# implied. See the License for the specific language governing
# rights and limitations under the License.
#
# The Original Code is the Bugzilla Bug Tracking System.
#
# The Initial Developer of the Original Code is Everything Solved, Inc.
# Portions created by the Initial Developer are Copyright (C) 2010 the
# Initial Developer. All Rights Reserved.
#
# Contributor(s):
# Max Kanat-Alexander <mkanat@bugzilla.org>
# This test combines two field/operator combinations using OR in
# a single boolean chart.
package Bugzilla::Test::Search::OrTest;
use base qw(Bugzilla::Test::Search::FieldTest);
use Bugzilla::Test::Search::Constants;
use Bugzilla::Test::Search::FakeCGI;
use List::MoreUtils qw(any uniq);
use constant type => 'OR';
###############
# Constructor #
###############
sub new {
my $class = shift;
my $self = { field_tests => [@_] };
return bless $self, $class;
}
#############
# Accessors #
#############
sub field_tests { return @{ $_[0]->{field_tests} } }
sub search_test { ($_[0]->field_tests)[0]->search_test }
sub name {
my ($self) = @_;
my @names = map { $_->name } $self->field_tests;
return join('-' . $self->type . '-', @names);
}
# In an OR test, bugs ARE supposed to be contained if they are contained
# by ANY test.
sub bug_is_contained {
my ($self, $number) = @_;
return any { $_->bug_is_contained($number) } $self->field_tests;
}
# Needed only for failure messages
sub debug_value {
my ($self) = @_;
my @values = map { $_->field . ' ' . $_->debug_value } $self->field_tests;
return join(' ' . $self->type . ' ', @values);
}
########################
# SKIP & TODO Messages #
########################
sub _join_skip { OR_SKIP }
sub _join_broken_constant { OR_BROKEN }
sub field_not_yet_implemented {
my ($self) = @_;
foreach my $test ($self->field_tests) {
if (grep { $_ eq $test->field } $self->_join_skip) {
return $test->field . " is not yet supported in OR tests";
}
}
return $self->_join_messages('field_not_yet_implemented');
}
sub invalid_field_operator_combination {
my ($self) = @_;
return $self->_join_messages('invalid_field_operator_combination');
}
sub search_known_broken {
my ($self) = @_;
return $self->_join_messages('search_known_broken');
}
sub _join_messages {
my ($self, $message_method) = @_;
my @messages = map { $_->$message_method } $self->field_tests;
@messages = grep { $_ } @messages;
return join(' AND ', @messages);
}
sub _bug_will_actually_be_contained {
my ($self, $number) = @_;
my @results;
foreach my $test ($self->field_tests) {
if ($test->bug_is_contained($number)
and !$test->contains_known_broken($number))
{
return 1;
}
elsif (!$test->bug_is_contained($number)
and $test->contains_known_broken($number)) {
return 1;
}
}
return 0;
}
sub contains_known_broken {
my ($self, $number) = @_;
my $join_broken = $self->_join_known_broken;
if (my $contains = $join_broken->{contains}) {
my $contains_is_broken = grep { $_ == $number } @$contains;
if ($contains_is_broken) {
my $name = $self->name;
return "$name contains $number is broken";
}
return undef;
}
return $self->_join_contains_known_broken($number);
}
sub _join_contains_known_broken {
my ($self, $number) = @_;
if ( ( $self->bug_is_contained($number)
and !$self->_bug_will_actually_be_contained($number) )
or ( !$self->bug_is_contained($number)
and $self->_bug_will_actually_be_contained($number) ) )
{
my @messages = map { $_->contains_known_broken($number) } $self->field_tests;
@messages = grep { $_ } @messages;
return join(' AND ', @messages);
}
return undef;
}
sub _join_known_broken {
my ($self) = @_;
my $or_broken = $self->_join_broken_constant;
foreach my $test ($self->field_tests) {
@or_broken_for = map { $_->join_broken($or_broken) } $self->field_tests;
@or_broken_for = grep { defined $_ } @or_broken_for;
last if !@or_broken_for;
$or_broken = $or_broken_for[0];
}
return $or_broken;
}
##############################
# Bugzilla::Search arguments #
##############################
sub search_columns {
my ($self) = @_;
my @columns = map { @{ $_->search_columns } } $self->field_tests;
return [uniq @columns];
}
sub search_params {
my ($self) = @_;
my @all_params = map { $_->search_params } $self->field_tests;
my $params = new Bugzilla::Test::Search::FakeCGI;
my $chart = 0;
foreach my $item (@all_params) {
$params->param("field0-0-$chart", $item->param('field0-0-0'));
$params->param("type0-0-$chart", $item->param('type0-0-0'));
$params->param("value0-0-$chart", $item->param('value0-0-0'));
$chart++;
}
return $params;
}
1;
\ No newline at end of file
#!/usr/bin/perl -w
# -*- Mode: perl; indent-tabs-mode: nil -*-
#
# The contents of this file are subject to the Mozilla Public
# License Version 1.1 (the "License"); you may not use this file
# except in compliance with the License. You may obtain a copy of
# the License at http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS
# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
# implied. See the License for the specific language governing
# rights and limitations under the License.
#
# The Original Code is the Bugzilla Bug Tracking System.
#
# The Initial Developer of the Original Code is Everything Solved, Inc.
# Portions created by the Initial Developer are Copyright (C) 2010 the
# Initial Developer. All Rights Reserved.
#
# Contributor(s):
# Max Kanat-Alexander <mkanat@bugzilla.org>
# For a description of this test, see Bugzilla::Test::Search
# in xt/lib/.
use strict;
use warnings;
use lib qw(. xt/lib lib);
use Bugzilla;
use Bugzilla::Constants;
use Bugzilla::Test::Search;
use Getopt::Long;
use Pod::Usage;
use Test::More;
my %switches;
GetOptions(\%switches, 'operators=s', 'top-operators=s', 'long',
'add-custom-fields', 'help|h') || die $@;
pod2usage(verbose => 1) if $switches{'help'};
plan skip_all => "BZ_WRITE_TESTS environment variable not set"
if !$ENV{BZ_WRITE_TESTS};
Bugzilla->usage_mode(USAGE_MODE_TEST);
my $test = new Bugzilla::Test::Search(\%switches);
plan tests => $test->num_tests;
$test->run();
__END__
=head1 NAME
search.t - Test L<Bugzilla::Search>
=head1 DESCRIPTION
This test tests L<Bugzilla::Search>.
Note that users may be prevented from writing new bugs, products, components,
etc. to your database while this test is running.
=head1 OPTIONS
=over
=item --long
Run AND and OR tests in addition to normal tests. Specifying
--long without also specifying L</--top-operators> is likely to
run your system out of memory.
=item --add-custom-fields
This adds every type of custom field to the database, so that they can
all be tested. Note that this B<CANNOT BE REVERSED>, so do not use this
switch on a production installation.
=item --operators=a,b,c
Limit the test to testing only the listed operators.
=item --top-operators=a,b,c
Limit the top-level tested operators to the following list. This
means that for normal tests, only the listed operators will be tested.
However, for OR and AND tests, all other operators will be tested
along with the operators you listed.
=item --help
Display this help.
=back
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment