diff options
author | Max Kanat-Alexander <mkanat@bugzilla.org> | 2010-07-07 23:34:25 +0200 |
---|---|---|
committer | Max Kanat-Alexander <mkanat@bugzilla.org> | 2010-07-07 23:34:25 +0200 |
commit | 87ea46f7fa2b269f065181f7765352184bb59717 (patch) | |
tree | 20e37379d319535c954480e86765a580342118bd /xt/lib/Bugzilla/Test/Search/FieldTest.pm | |
parent | 814b24fdc9407a741967322041ff817665f8e00b (diff) | |
download | bugzilla-87ea46f7fa2b269f065181f7765352184bb59717.tar.gz bugzilla-87ea46f7fa2b269f065181f7765352184bb59717.tar.xz |
Bug 574879: Create a test that assures the correctness of Search.pm's
boolean charts
r=glob, a=mkanat
Diffstat (limited to 'xt/lib/Bugzilla/Test/Search/FieldTest.pm')
-rw-r--r-- | xt/lib/Bugzilla/Test/Search/FieldTest.pm | 564 |
1 files changed, 564 insertions, 0 deletions
diff --git a/xt/lib/Bugzilla/Test/Search/FieldTest.pm b/xt/lib/Bugzilla/Test/Search/FieldTest.pm new file mode 100644 index 000000000..4c43e34ed --- /dev/null +++ b/xt/lib/Bugzilla/Test/Search/FieldTest.pm @@ -0,0 +1,564 @@ +# -*- 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 |