r=LpSolit, a=LpSolit git-svn-id: svn://10.0.0.236/trunk@264853 18797224-902f-48f8-a5cc-f745e15eee43
618 lines
19 KiB
Perl
618 lines
19 KiB
Perl
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
#
|
|
# This Source Code Form is "Incompatible With Secondary Licenses", as
|
|
# defined by the Mozilla Public License, v. 2.0.
|
|
|
|
# This module 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::Search;
|
|
use Bugzilla::Test::Search::Constants;
|
|
use Bugzilla::Util qw(trim);
|
|
|
|
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) = @_;
|
|
$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(@_);
|
|
}
|
|
sub number {
|
|
my ($self, $id) = @_;
|
|
foreach my $number (1..NUM_BUGS) {
|
|
return $number if $self->search_test->bug($number)->id == $id;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
# 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_fail {
|
|
my ($self, $number, $results, $sql) = @_;
|
|
my @expected = @{ $self->test->{contains} };
|
|
my @results = sort
|
|
map { $self->number($_) }
|
|
map { $_->[0] }
|
|
@$results;
|
|
return
|
|
" Value: '" . $self->translated_value . "'\n" .
|
|
"Expected: [" . join(',', @expected) . "]\n" .
|
|
" Results: [" . join(',', @results) . "]\n" .
|
|
trim($sql) . "\n";
|
|
}
|
|
|
|
# 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 (@_ > 2) {
|
|
$self->{transformed_value_was_equal}->{$number} = $value;
|
|
$self->search_test->was_equal_cache($self, $number, $value);
|
|
}
|
|
my $cached = $self->search_test->was_equal_cache($self, $number);
|
|
return $cached if defined $cached;
|
|
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)
|
|
and !$self->test->{override}->{$self->field}->{contains})
|
|
{
|
|
$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, $constant, $skip_pg_check) = @_;
|
|
|
|
$constant ||= KNOWN_BROKEN;
|
|
my $field = $self->field;
|
|
my $type = $self->field_object->type;
|
|
my $operator = $self->operator;
|
|
my $value = $self->main_value;
|
|
my $value_name = "$operator-$value";
|
|
if (my $extra_name = $self->test->{extra_name}) {
|
|
$value_name .= "-$extra_name";
|
|
}
|
|
|
|
my $value_broken = $constant->{$value_name}->{$field};
|
|
$value_broken ||= $constant->{$value_name}->{$type};
|
|
return $value_broken if $value_broken;
|
|
my $operator_broken = $constant->{$operator}->{$field};
|
|
$operator_broken ||= $constant->{$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;
|
|
}
|
|
|
|
# Used by subclasses. Checks both bug_is_contained and contains_known_broken
|
|
# to tell you whether or not the bug will *actually* be found by the test.
|
|
sub will_actually_contain_bug {
|
|
my ($self, $number) = @_;
|
|
my $is_contained = $self->bug_is_contained($number) ? 1 : 0;
|
|
my $is_broken = $self->contains_known_broken($number) ? 1 : 0;
|
|
|
|
# If the test is supposed to contain the bug and *isn't* broken,
|
|
# then the test will contain the bug.
|
|
return 1 if ($is_contained and !$is_broken);
|
|
# If this test is *not* supposed to contain the bug, but that test is
|
|
# broken, then this test *will* contain the bug.
|
|
return 1 if (!$is_contained and $is_broken);
|
|
|
|
return 0;
|
|
}
|
|
|
|
# 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 data that will get passed to Bugzilla::Search as its arguments.
|
|
sub search_params {
|
|
my ($self) = @_;
|
|
return $self->{search_params} if $self->{search_params};
|
|
|
|
my %params = (
|
|
"field0-0-0" => $self->field,
|
|
"type0-0-0" => $self->operator,
|
|
"value0-0-0" => $self->translated_value,
|
|
);
|
|
|
|
$self->{search_params} = \%params;
|
|
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 'longdescs.count') {
|
|
@values = scalar(@{ $self->bug($number)->comments });
|
|
}
|
|
elsif ($field eq 'work_time') {
|
|
@values = $self->_values_for($number, 'actual_time');
|
|
}
|
|
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');
|
|
}
|
|
elsif ($field eq 'see_also') {
|
|
@values = $self->_values_for($number, 'see_also', 'name');
|
|
}
|
|
elsif ($field eq 'tag') {
|
|
@values = $self->_values_for($number, 'tags');
|
|
}
|
|
# 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);
|
|
}
|
|
# Sanity check to make sure that none of the <> stuff was left in.
|
|
if ($value =~ /<\d/) {
|
|
die $self->name . ": value untranslated: $value\n";
|
|
}
|
|
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;
|
|
if ($value =~ /<$number-bug_group>/) {
|
|
my @bug_groups = map { $_->name } @{ $bug->groups_in };
|
|
@bug_groups = grep { $_ =~ /^\d+-group-/ } @bug_groups;
|
|
my $group = $bug_groups[0];
|
|
$value =~ s/<$number-bug_group>/$group/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 $type = $self->field_object->type;
|
|
my $substr_size = SUBSTR_SIZE;
|
|
if (exists FIELD_SUBSTR_SIZE->{$field}) {
|
|
$substr_size = FIELD_SUBSTR_SIZE->{$field};
|
|
}
|
|
elsif (exists FIELD_SUBSTR_SIZE->{$type}) {
|
|
$substr_size = FIELD_SUBSTR_SIZE->{$type};
|
|
}
|
|
if ($substr_size > 0) {
|
|
# The field name is included in every field value, and if it's
|
|
# long, it might take up the whole substring, and we don't want that.
|
|
if (!grep { $_ eq $field or $_ eq $type } SUBSTR_NO_FIELD_ADD) {
|
|
$substr_size += length($field);
|
|
}
|
|
my $string = substr($value, 0, $substr_size);
|
|
return $string;
|
|
}
|
|
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 = $self->_test_search_object_creation();
|
|
|
|
my $sql;
|
|
TODO: {
|
|
local $TODO = $search_broken if $search_broken;
|
|
lives_ok { $sql = $search->_sql } "$name: generate SQL";
|
|
}
|
|
|
|
my $results;
|
|
SKIP: {
|
|
skip "Can't run SQL without any SQL", 1 if !defined $sql;
|
|
$results = $self->_test_sql($search);
|
|
}
|
|
|
|
$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, $search) = @_;
|
|
my $name = $self->name;
|
|
my $results;
|
|
lives_ok { $results = $search->data } "$name: Run SQL Query"
|
|
or diag($search->_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 $self->debug_fail($number, $results, $sql);
|
|
}
|
|
else {
|
|
ok(!$result_ids{$bug_id},
|
|
"$name: does not contain bug $number ($bug_id)")
|
|
or diag $self->debug_fail($number, $results, $sql);
|
|
}
|
|
}
|
|
}
|
|
|
|
1;
|