File Coverage

blib/lib/Daizu/Test.pm
Criterion Covered Total %
statement 19 21 90.4
branch n/a
condition n/a
subroutine 7 7 100.0
pod n/a
total 26 28 92.8


line stmt bran cond sub pod time code
1             package Daizu::Test;
2 1     1   95269 use warnings;
  1         2  
  1         35  
3 1     1   5 use strict;
  1         1  
  1         34  
4              
5 1     1   5 use base 'Exporter';
  1         7  
  1         211  
6             our @EXPORT_OK = qw(
7             $TEST_DBCONF_FILENAME $DB_SCHEMA_FILENAME
8             $TEST_REPOS_DIR $TEST_REPOS_URL
9             init_tests test_config
10             create_database drop_database
11             create_test_repos
12             get_nav_menu_carefully test_menu_item
13             test_cmp_guids test_cmp_urls
14             );
15              
16 1     1   5 use Path::Class qw( file dir );
  1         2  
  1         60  
17 1     1   19451 use DBI;
  1         26175  
  1         1234  
18 1     1   15 use File::Path qw( rmtree );
  1         2  
  1         71  
19 1     1   7793 use SVN::Core;
  0            
  0            
20             use SVN::Ra;
21             use SVN::Repos;
22             use SVN::Delta;
23             use Carp qw( croak );
24             use Carp::Assert qw( assert );
25             use Test::More;
26             use Daizu::Util qw( db_select );
27              
28             =head1 NAME
29              
30             Daizu::Test - functions for use by the test suite
31              
32             =head1 DESCRIPTION
33              
34             The functions defined in here are only really useful for testing Daizu CMS.
35             This stuff is used by the test suite, in particular C which
36             creates a test database and repository.
37              
38             =head1 CONSTANTS
39              
40             =over
41              
42             =item $TEST_DBCONF_FILENAME
43              
44             Name of configuration file which provides information about how to connect
45             to the databases used for the test suite. The C function
46             parses this.
47              
48             Value: I
49              
50             =item $DB_SCHEMA_FILENAME
51              
52             Name of the SQL file containing the database schema to load into the
53             test database after creating it.
54              
55             Value: db.sql
56              
57             =item $TEST_REPOS_DIR
58              
59             Full path to the directory which should contain the testing repository
60             created at the start of running the tests.
61              
62             Value: I<.test-repos> in the current directory
63              
64             =item $TEST_REPOS_URL
65              
66             A 'file' URL to the test repository.
67              
68             =item $TEST_REPOS_DUMP
69              
70             Full path to the Subversion dump file which is loaded into the
71             test repository.
72              
73             Value: I in the current directory.
74              
75             =item $TEST_OUTPUT_DIR
76              
77             Full path to the directory into which output from publishing test
78             content should be written.
79              
80             Value: I<.test-docroot> in the current directory
81              
82             =item $TEST_CONFIG
83              
84             Filename of config file to use for testing.
85              
86             Value: I (which is created from I
87             by I)
88              
89             =back
90              
91             =cut
92              
93             our $TEST_DBCONF_FILENAME = file('test.conf')->absolute->stringify;
94             our $DB_SCHEMA_FILENAME = 'db.sql';
95             our $TEST_REPOS_DIR = dir('.test-repos')->absolute->stringify;
96             our $TEST_REPOS_URL = "file://$TEST_REPOS_DIR";
97             our $TEST_REPOS_DUMP = file('test-repos.dump')->absolute->stringify;
98             our $TEST_OUTPUT_DIR = dir('.test-output')->absolute->stringify;
99             our $TEST_CONFIG = 'test-config.xml';
100              
101             =head1 FUNCTIONS
102              
103             The following functions are available for export from this module.
104             None of them are exported by default.
105              
106             =over
107              
108             =item init_tests($num_tests, [$show_errors])
109              
110             Load the test configuration file (which will allow you to use
111             the L function later), and check it to make sure
112             the tests are properly configured. If they are then initialize L
113             with the number of tests expected (unless C<$num_tests> is undef).
114             Otherwise tell Test::More to skip all the tests.
115              
116             If C<$show_errors> is present and true, display warnings about any problems
117             with the test configuration file. This should be done in the first test
118             program so that the user knows why the tests aren't being run. The others
119             can just skip the tests.
120              
121             =item test_config()
122              
123             Return a reference to a hash of configuration values from the
124             file specified by L<$TEST_DBCONF_FILENAME|/$TEST_DBCONF_FILENAME>.
125             This will fail if L
126             hasn't been run yet.
127              
128             =cut
129              
130             {
131             my $test_config;
132              
133             sub init_tests
134             {
135             my ($num_tests, $show_errors) = @_;
136             my $errors = '';
137              
138             open my $fh, '<', $TEST_DBCONF_FILENAME
139             or die "$0: error opening 'TEST_DBCONF_FILENAME': $!\n";
140              
141             my %config;
142             while (<$fh>) {
143             next unless /\S/;
144             next if /^\s*#/;
145             chomp;
146             my ($key, $value) = split ' ', $_, 2;
147             $errors .= "$TEST_DBCONF_FILENAME:$.: duplicate value for '$key'\n"
148             if exists $config{$key};
149             $errors .= "$TEST_DBCONF_FILENAME:$.: value missing for '$key'\n"
150             if !defined $value || $value eq '';
151             $config{$key} = $value;
152             }
153              
154             $errors .= "$0: you need to edit the file '$TEST_DBCONF_FILENAME'" .
155             " before you can run the test suite, to configure how the" .
156             " tests should access your PostgreSQL server.\n"
157             if $config{'not-configured'};
158              
159             for (qw( template-dsn test-dsn )) {
160             $errors .= "$0: configuration file '$TEST_DBCONF_FILENAME' must" .
161             " contain a value called '$_' for the test suite to" .
162             " work.\n"
163             unless $config{$_};
164             }
165              
166             if ($errors ne '') {
167             warn "\n\n$errors\n" if $show_errors;
168             plan skip_all => "Tests not configured in '$TEST_DBCONF_FILENAME'";
169             }
170             else {
171             plan tests => $num_tests
172             if defined $num_tests;
173             }
174              
175             $test_config = \%config;
176             }
177              
178             sub test_config
179             {
180             croak "can't call 'test_config' until you've called 'init_tests'"
181             unless defined $test_config;
182             return $test_config;
183             }
184             }
185              
186             =item pg_template_dbh()
187              
188             Returns a L database handle connected to the PostgreSQL C
189             database, which can be used for example to create the test database.
190              
191             =cut
192              
193             sub pg_template_dbh
194             {
195             my $config = test_config();
196             return DBI->connect(
197             $config->{'template-dsn'}, $config->{'template-user'},
198             $config->{'template-password'},
199             { AutoCommit => 1, RaiseError => 1, PrintError => 0 },
200             );
201             }
202              
203             =item create_database()
204              
205             Create the test database, load the database schema into it, and return
206             a L handle for accessing it.
207              
208             =cut
209              
210             sub create_database
211             {
212             # Drop the test DB if it already exists.
213             my $config = test_config();
214             my $db = DBI->connect(
215             $config->{'test-dsn'}, $config->{'test-user'},
216             $config->{'test-password'},
217             { RaiseError => 0, PrintError => 0 },
218             );
219             if (defined $db) {
220             undef $db;
221             drop_database();
222             }
223              
224             $db = pg_template_dbh();
225             my $db_name = _test_db_name();
226             $db->do(qq{
227             create database $db_name
228             });
229              
230             $db->disconnect;
231             $db = DBI->connect(
232             $config->{'test-dsn'}, $config->{'test-user'},
233             $config->{'test-password'},
234             { AutoCommit => 1, RaiseError => 1, PrintError => 0 },
235             );
236              
237             # Turn off warnings while loading the schema. This silences the 'NOTICE'
238             # messages about which indexes PostgreSQL is creating, which aren't
239             # very interesting.
240             local $db->{PrintWarn};
241              
242             open my $schema, '<', $DB_SCHEMA_FILENAME
243             or die "error opening DB schema '$DB_SCHEMA_FILENAME': $!";
244             my $sql = '';
245             while (<$schema>) {
246             next unless /\S/;
247             next if /^\s*--/;
248             $sql .= $_;
249             if (/;$/) {
250             eval { $db->do($sql) };
251             die "Error executing statement:\n$sql:\n$@"
252             if $@;
253             $sql = '';
254             }
255             }
256              
257             croak "error in '$DB_SCHEMA_FILENAME': last statement should end with ';'"
258             if $sql ne '';
259              
260             return $db;
261             }
262              
263             =item drop_database()
264              
265             Delete the test database. Sleeps for a second before doing so, to give
266             the connections a chance to really get cleaned up.
267              
268             =cut
269              
270             sub drop_database
271             {
272             my $db = pg_template_dbh();
273             sleep 1; # Wait until we're properly disconnected.
274              
275             my $db_name = _test_db_name();
276             $db->do(qq{
277             drop database $db_name
278             });
279             }
280              
281             =item create_test_repos()
282              
283             Create an empty Subversion repository for testing, in C<$TEST_REPOS_DIR>.
284              
285             =cut
286              
287             sub create_test_repos
288             {
289             rmtree($TEST_REPOS_DIR)
290             if -e $TEST_REPOS_DIR;
291             SVN::Repos::create($TEST_REPOS_DIR, undef, undef, undef, undef);
292             system("svnadmin load --quiet $TEST_REPOS_DIR <$TEST_REPOS_DUMP");
293             my $ra = SVN::Ra->new(url => $TEST_REPOS_URL);
294             assert($ra->get_latest_revnum > 0); # confirm undump worked
295             return $ra;
296             }
297              
298             =item get_nav_menu_carefully($file)
299              
300             Return the navigation menu for C<$file>, by calling the
301             Lnavigation_menu($file, $url)>
302             method on its generator. The result is returned after some basic
303             checks have been made that it is properly structured. Any problems
304             will cause an assertion to fail (even if C isn't set).
305              
306             =cut
307              
308             sub get_nav_menu_carefully
309             {
310             my ($file) = @_;
311             assert(ref $file);
312              
313             my $gen = $file->generator;
314             my @urls = $gen->urls_info($file);
315             assert(@urls >= 1);
316              
317             my $menu = $gen->navigation_menu($file, $urls[0]);
318              
319             my $num_undef_links = _nav_menu_check_children($menu);
320             assert($num_undef_links == 0 || $num_undef_links == 1);
321              
322             return $menu;
323             }
324            
325             # Check a an array of menu items for structural integrity. The value
326             # should be suitable for being a 'children' item in a navigation menu.
327             sub _nav_menu_check_children
328             {
329             my ($items) = @_;
330             assert(defined $items);
331             assert(ref $items eq 'ARRAY');
332              
333             my $num_undef_links = 0;
334             for my $item (@$items) {
335             assert(defined $item);
336             assert(ref $item eq 'HASH');
337             assert(defined $item->{title});
338             ++$num_undef_links unless defined $item->{link};
339             $num_undef_links += _nav_menu_check_children($item->{children});
340             }
341              
342             return $num_undef_links;
343             }
344              
345             =item test_menu_item($item, $desc, $num_children, $url, $title, [$short_title])
346              
347             Run tests (using L) on the navigation menu item provided
348             in C<$item> (which should be a hash of the type returned for each item
349             by the
350             Lnavigation_menu($file, $url)>
351             method of generator classes).
352              
353             C<$desc> should be a short piece of text to use in the names of the tests.
354             C<$num_children> is the number of children expected to be present in it
355             (although they aren't checked, only the number of them is). C<$url> is
356             a string representation of the expected URL, which is likely to be a
357             relative URL. C<$title> and C<$short_title> are the expected 'title'
358             and 'short_title' values, which may be undef if those values are expected
359             to be missing. If C<$short_title> isn't supplied (the argument is missing
360             rather than undefined) then that won't be tested at all.
361              
362             The tests will be skipped with an appropriate warning if C<$item> is
363             undefined.
364              
365             =cut
366              
367             sub test_menu_item
368             {
369             my ($item, $desc, $num_children, $url, $title, $short_title) = @_;
370              
371             SKIP: {
372             my $num_tests = @_ > 5 ? 4 : 3;
373             skip "expected menu item '$desc' doesn't exist", $num_tests
374             unless defined $item;
375             is($item->{link}, $url, "navigation_menu: $desc: link");
376             is($item->{title}, $title, "navigation_menu: $desc: title");
377             is(scalar @{$item->{children}}, $num_children,
378             "navigation_menu: $desc: num children");
379             is($item->{short_title}, $short_title,
380             "navigation_menu: $desc: short_title")
381             if @_ > 5;
382             }
383             }
384              
385             =item test_cmp_guids($db, $wc_id, $desc, $got, @expected)
386              
387             Compare the array of GUID IDs referenced by C<$got> with the GUID IDs
388             of the filenames listed in C<@expected>. The order doesn't matter.
389             C<$desc> is a string to put in the test descriptions.
390              
391             C<$got> may contain other GUID IDs which aren't expected, so you should
392             check that you've got the right number as well as calling this.
393              
394             =cut
395              
396             sub test_cmp_guids
397             {
398             my ($db, $wc_id, $desc, $got, @expected) = @_;
399             assert(@expected > 0);
400              
401             for my $path (@expected) {
402             my $guid_id = db_select($db, 'wc_file',
403             { wc_id => $wc_id, path => $path },
404             'guid_id',
405             );
406             assert(defined $guid_id);
407              
408             my $found;
409             for (@$got) {
410             next unless $_ == $guid_id;
411             $found = 1;
412             last;
413             }
414             ok($found, "$desc, update $path");
415             }
416             }
417              
418             =item test_cmp_urls($desc, $got, @expected)
419              
420             Compare the URLs in the array referenced by C<$got> with the ones listed
421             in C<@expected>. In both cases they can be plain strings or L objects.
422             The order they are given in doesn't matter.
423              
424             There must be at least one URL expected, and the number of ones in the
425             two arrays is compared in the first test.
426              
427             =cut
428              
429             sub test_cmp_urls
430             {
431             my ($desc, $got, @expected) = @_;
432             is(scalar @$got, scalar @expected, "$desc, num URLs");
433              
434             for my $exp_url (@expected) {
435             $exp_url = URI->new($exp_url);
436              
437             my $found;
438             for (@$got) {
439             next unless $exp_url->eq($_);
440             $found = 1;
441             last;
442             }
443             ok($found, "$desc, pub $exp_url");
444             }
445             }
446              
447             =back
448              
449             =cut
450              
451             sub _test_db_name
452             {
453             my $config = test_config();
454             my $test_dsn = $config->{'test-dsn'};
455             die "$0: can't extract 'dbname' part from test DSN '$test_dsn' in order" .
456             " to drop the test database\n"
457             unless $test_dsn =~ /\bdbname=(\w+)\b/i;
458             return "$1";
459             }
460              
461             =head1 COPYRIGHT
462              
463             This software is copyright 2006 Geoff Richards Egeoff@laxan.comE.
464             For licensing information see this page:
465              
466             L
467              
468             =cut
469              
470             1;
471             # vi:ts=4 sw=4 expandtab