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