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 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>
# 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 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