1#!/usr/bin/perl -w
2
3use strict;
4use warnings;
5use utf8;
6use Test::More tests => 248;
7#use Test::More 'no_plan';
8use App::Sqitch;
9use Locale::TextDomain qw(App-Sqitch);
10use Test::NoWarnings;
11use Test::Exception;
12use Test::MockModule;
13use Path::Class;
14use Term::ANSIColor qw(color);
15use Encode;
16use lib 't/lib';
17use MockOutput;
18use LC;
19
20$ENV{SQITCH_CONFIG}        = 'nonexistent.conf';
21$ENV{SQITCH_USER_CONFIG}   = 'nonexistent.user';
22$ENV{SQITCH_SYSTEM_CONFIG} = 'nonexistent.sys';
23
24my $CLASS = 'App::Sqitch::Command::log';
25require_ok $CLASS;
26
27ok my $sqitch = App::Sqitch->new(
28    options => {
29        engine    => 'sqlite',
30        top_dir   => Path::Class::Dir->new('test-log')->stringify,
31        plan_file => Path::Class::File->new('t/sql/sqitch.plan')->stringify,
32    },
33), 'Load a sqitch sqitch object';
34my $config = $sqitch->config;
35isa_ok my $log = App::Sqitch::Command->load({
36    sqitch  => $sqitch,
37    command => 'log',
38    config  => $config,
39}), $CLASS, 'log command';
40
41can_ok $log, qw(
42    target
43    change_pattern
44    project_pattern
45    committer_pattern
46    max_count
47    skip
48    reverse
49    format
50    options
51    execute
52    configure
53);
54
55is_deeply [$CLASS->options], [qw(
56    event=s@
57    target|t=s
58    change-pattern|change=s
59    project-pattern|project=s
60    committer-pattern|committer=s
61    format|f=s
62    date-format|date=s
63    max-count|n=i
64    skip=i
65    reverse!
66    color=s
67    no-color
68    abbrev=i
69    oneline
70)], 'Options should be correct';
71
72##############################################################################
73# Test database.
74is $log->target, undef, 'Default target should be undef';
75isa_ok $log = $CLASS->new(
76    sqitch   => $sqitch,
77    target => 'foo',
78), $CLASS, 'new status with target';
79is $log->target, 'foo', 'Should have target "foo"';
80
81##############################################################################
82# Test configure().
83my $cmock = Test::MockModule->new('App::Sqitch::Config');
84
85# Test date_format validation.
86my $configured = $CLASS->configure($config, {});
87isa_ok delete $configured->{formatter}, 'App::Sqitch::ItemFormatter', 'Formatter';
88is_deeply $configured, {},
89    'Should get empty hash for no config or options';
90$cmock->mock( get => 'nonesuch' );
91throws_ok { $CLASS->configure($config, {}), {} } 'App::Sqitch::X',
92    'Should get error for invalid date format in config';
93is $@->ident, 'datetime',
94    'Invalid date format error ident should be "datetime"';
95is $@->message, __x(
96    'Unknown date format "{format}"',
97    format => 'nonesuch',
98), 'Invalid date format error message should be correct';
99$cmock->unmock_all;
100
101throws_ok { $CLASS->configure($config, { date_format => 'non'}), {} }
102    'App::Sqitch::X',
103    'Should get error for invalid date format in optsions';
104is $@->ident, 'datetime',
105    'Invalid date format error ident should be "log"';
106is $@->message, __x(
107    'Unknown date format "{format}"',
108    format => 'non',
109), 'Invalid date format error message should be correct';
110
111# Test format validation.
112$cmock->mock( get => sub {
113    my ($self, %p) = @_;
114    return 'nonesuch' if $p{key} eq 'log.format';
115    return undef;
116});
117throws_ok { $CLASS->configure($config, {}), {} } 'App::Sqitch::X',
118    'Should get error for invalid format in config';
119is $@->ident, 'log',
120    'Invalid format error ident should be "log"';
121is $@->message, __x(
122    'Unknown log format "{format}"',
123    format => 'nonesuch',
124), 'Invalid format error message should be correct';
125$cmock->unmock_all;
126
127throws_ok { $CLASS->configure($config, { format => 'non'}), {} }
128    'App::Sqitch::X',
129    'Should get error for invalid format in optsions';
130is $@->ident, 'log',
131    'Invalid format error ident should be "log"';
132is $@->message, __x(
133    'Unknown log format "{format}"',
134    format => 'non',
135), 'Invalid format error message should be correct';
136
137# Test color configuration.
138$configured = $CLASS->configure( $config, { no_color => 1 } );
139is $configured->{formatter}->color, 'never',
140    'Configuration should respect --no-color, setting "never"';
141
142# Test oneline configuration.
143$configured = $CLASS->configure( $config, { oneline => 1 });
144is $configured->{format}, '%{:event}C%h %l%{reset}C %o:%n %s',
145    '--oneline should set format';
146is $configured->{formatter}{abbrev}, 6, '--oneline should set abbrev to 6';
147
148$configured = $CLASS->configure( $config, { oneline => 1, format => 'format:foo', abbrev => 5 });
149is $configured->{format}, 'foo', '--oneline should not override --format';
150is $configured->{formatter}{abbrev}, 5, '--oneline should not overrride --abbrev';
151
152my $config_color = 'auto';
153$cmock->mock( get => sub {
154    my ($self, %p) = @_;
155    return $config_color if $p{key} eq 'log.color';
156    return undef;
157});
158
159my $log_config = {};
160$cmock->mock( get_section => sub { $log_config } );
161
162$configured = $CLASS->configure( $config, { no_color => 1 } );
163
164is $configured->{formatter}->color, 'never',
165    'Configuration should respect --no-color even when configure is set';
166
167NEVER: {
168    $config_color = 'never';
169    $log_config = { color => $config_color };
170    my $configured = $CLASS->configure( $config, $log_config );
171    is $configured->{formatter}->color, 'never',
172        'Configuration should respect color option';
173
174    # Try it with config.
175    $log_config = { color => $config_color };
176    $configured = $CLASS->configure( $config, {} );
177    is $configured->{formatter}->color, 'never',
178        'Configuration should respect color config';
179}
180
181ALWAYS: {
182    $config_color = 'always';
183    $log_config = { color => $config_color };
184    my $configured = $CLASS->configure( $config, $log_config );
185    is_deeply $configured->{formatter}->color, 'always',
186        'Configuration should respect color option';
187
188    # Try it with config.
189    $log_config = { color => $config_color };
190    $configured = $CLASS->configure( $config, {} );
191    is_deeply $configured->{formatter}->color, 'always',
192        'Configuration should respect color config';
193}
194
195AUTO: {
196    $config_color = 'auto';
197    $log_config = { color => $config_color };
198    for my $enabled (0, 1) {
199        my $configured = $CLASS->configure( $config, $log_config );
200        is_deeply $configured->{formatter}->color, 'auto',
201            'Configuration should respect color option';
202
203        # Try it with config.
204        $log_config = { color => $config_color };
205        $configured = $CLASS->configure( $config, {} );
206        is_deeply $configured->{formatter}->color, 'auto',
207            'Configuration should respect color config';
208    }
209}
210
211$cmock->unmock_all;
212
213###############################################################################
214# Test named formats.
215my $cdt = App::Sqitch::DateTime->now;
216my $pdt = $cdt->clone->subtract(days => 1);
217my $event = {
218    event           => 'deploy',
219    project         => 'logit',
220    change_id       => '000011112222333444',
221    change          => 'lolz',
222    tags            => [ '@beta', '@gamma' ],
223    committer_name  => 'larry',
224    committer_email => 'larry@example.com',
225    committed_at    => $cdt,
226    planner_name    => 'damian',
227    planner_email   => 'damian@example.com',
228    planned_at      => $pdt,
229    note            => "For the LOLZ.\n\nYou know, funny stuff and cute kittens, right?",
230    requires        => [qw(foo bar)],
231    conflicts       => []
232};
233
234my $ciso = $cdt->as_string( format => 'iso' );
235my $craw = $cdt->as_string( format => 'raw' );
236my $piso = $pdt->as_string( format => 'iso' );
237my $praw = $pdt->as_string( format => 'raw' );
238for my $spec (
239    [ raw => "deploy 000011112222333444 (\@beta, \@gamma)\n"
240        . "name      lolz\n"
241        . "project   logit\n"
242        . "requires  foo, bar\n"
243        . "planner   damian <damian\@example.com>\n"
244        . "planned   $praw\n"
245        . "committer larry <larry\@example.com>\n"
246        . "committed $craw\n\n"
247        . "    For the LOLZ.\n    \n    You know, funny stuff and cute kittens, right?\n"
248    ],
249    [ full =>  __('Deploy') . " 000011112222333444 (\@beta, \@gamma)\n"
250        . __('Name:     ') . " lolz\n"
251        . __('Project:  ') . " logit\n"
252        . __('Requires: ') . " foo, bar\n"
253        . __('Planner:  ') . " damian <damian\@example.com>\n"
254        . __('Planned:  ') . " __PDATE__\n"
255        . __('Committer:') . " larry <larry\@example.com>\n"
256        . __('Committed:') . " __CDATE__\n\n"
257        . "    For the LOLZ.\n    \n    You know, funny stuff and cute kittens, right?\n"
258    ],
259    [ long =>  __('Deploy') . " 000011112222333444 (\@beta, \@gamma)\n"
260        . __('Name:     ') . " lolz\n"
261        . __('Project:  ') . " logit\n"
262        . __('Planner:  ') . " damian <damian\@example.com>\n"
263        . __('Committer:') . " larry <larry\@example.com>\n\n"
264        . "    For the LOLZ.\n    \n    You know, funny stuff and cute kittens, right?\n"
265    ],
266    [ medium =>  __('Deploy') . " 000011112222333444\n"
267        . __('Name:     ') . " lolz\n"
268        . __('Committer:') . " larry <larry\@example.com>\n"
269        . __('Date:     ') . " __CDATE__\n\n"
270        . "    For the LOLZ.\n    \n    You know, funny stuff and cute kittens, right?\n"
271    ],
272    [ short =>  __('Deploy') . " 000011112222333444\n"
273        . __('Name:     ') . " lolz\n"
274        . __('Committer:') . " larry <larry\@example.com>\n\n"
275        . "    For the LOLZ.\n",
276    ],
277    [ oneline => '000011112222333444 ' . __('deploy') . ' logit:lolz For the LOLZ.' ],
278) {
279    local $ENV{ANSI_COLORS_DISABLED} = 1;
280    my $configured = $CLASS->configure( $config, { format => $spec->[0] } );
281    my $format = $configured->{format};
282    ok my $log = $CLASS->new( sqitch => $sqitch, %{ $configured } ),
283        qq{Instantiate with format "$spec->[0]"};
284    (my $exp = $spec->[1]) =~ s/__CDATE__/$ciso/;
285    $exp =~ s/__PDATE__/$piso/;
286    is $log->formatter->format( $log->format, $event ), $exp,
287        qq{Format "$spec->[0]" should output correctly};
288
289    if ($spec->[1] =~ /__CDATE__/) {
290        # Test different date formats.
291        for my $date_format (qw(rfc long medium)) {
292            ok my $log = $CLASS->new(
293                sqitch => $sqitch,
294                format => $format,
295                formatter => App::Sqitch::ItemFormatter->new(date_format => $date_format),
296            ), qq{Instantiate with format "$spec->[0]" and date format "$date_format"};
297            my $date = $cdt->as_string( format => $date_format );
298            (my $exp = $spec->[1]) =~ s/__CDATE__/$date/;
299            $date = $pdt->as_string( format => $date_format );
300            $exp =~ s/__PDATE__/$date/;
301            is $log->formatter->format( $log->format, $event ), $exp,
302                qq{Format "$spec->[0]" and date format "$date_format" should output correctly};
303        }
304    }
305
306    if ($spec->[1] =~ s/\s+[(]?[@]beta,\s+[@]gamma[)]?//) {
307        # Test without tags.
308        local $event->{tags} = [];
309        (my $exp = $spec->[1]) =~ s/__CDATE__/$ciso/;
310        $exp =~ s/__PDATE__/$piso/;
311        is $log->formatter->format( $log->format, $event ), $exp,
312            qq{Format "$spec->[0]" should output correctly without tags};
313    }
314}
315
316###############################################################################
317# Test all formatting characters.
318my $local_cdt = $cdt->clone;
319$local_cdt->set_time_zone('local');
320$local_cdt->set( locale => $LC::TIME );
321my $local_pdt = $pdt->clone;
322$local_pdt->set_time_zone('local');
323$local_pdt->set( locale => $LC::TIME );
324
325my $formatter = $log->formatter;
326for my $spec (
327    ['%e', { event => 'deploy' }, 'deploy' ],
328    ['%e', { event => 'revert' }, 'revert' ],
329    ['%e', { event => 'fail' },   'fail' ],
330
331    ['%L', { event => 'deploy' }, __ 'Deploy' ],
332    ['%L', { event => 'revert' }, __ 'Revert' ],
333    ['%L', { event => 'fail' },   __ 'Fail' ],
334
335    ['%l', { event => 'deploy' }, __ 'deploy' ],
336    ['%l', { event => 'revert' }, __ 'revert' ],
337    ['%l', { event => 'fail' },   __ 'fail' ],
338
339    ['%{event}_',     {}, __ 'Event:    ' ],
340    ['%{change}_',    {}, __ 'Change:   ' ],
341    ['%{committer}_', {}, __ 'Committer:' ],
342    ['%{planner}_',   {}, __ 'Planner:  ' ],
343    ['%{by}_',        {}, __ 'By:       ' ],
344    ['%{date}_',      {}, __ 'Date:     ' ],
345    ['%{committed}_', {}, __ 'Committed:' ],
346    ['%{planned}_',   {}, __ 'Planned:  ' ],
347    ['%{name}_',      {}, __ 'Name:     ' ],
348    ['%{email}_',     {}, __ 'Email:    ' ],
349    ['%{requires}_',  {}, __ 'Requires: ' ],
350    ['%{conflicts}_', {}, __ 'Conflicts:' ],
351
352    ['%H', { change_id => '123456789' }, '123456789' ],
353    ['%h', { change_id => '123456789' }, '123456789' ],
354    ['%{5}h', { change_id => '123456789' }, '12345' ],
355    ['%{7}h', { change_id => '123456789' }, '1234567' ],
356
357    ['%n', { change => 'foo' }, 'foo'],
358    ['%n', { change => 'bar' }, 'bar'],
359    ['%o', { project => 'foo' }, 'foo'],
360    ['%o', { project => 'bar' }, 'bar'],
361
362    ['%c', { committer_name => 'larry', committer_email => 'larry@example.com'  }, 'larry <larry@example.com>'],
363    ['%{n}c', { committer_name => 'damian' }, 'damian'],
364    ['%{name}c', { committer_name => 'chip' }, 'chip'],
365    ['%{e}c', { committer_email => 'larry@example.com'  }, 'larry@example.com'],
366    ['%{email}c', { committer_email => 'damian@example.com' }, 'damian@example.com'],
367
368    ['%{date}c', { committed_at => $cdt }, $cdt->as_string( format => 'iso' ) ],
369    ['%{date:rfc}c', { committed_at => $cdt }, $cdt->as_string( format => 'rfc' ) ],
370    ['%{d:long}c', { committed_at => $cdt }, $cdt->as_string( format => 'long' ) ],
371    ["%{d:cldr:HH'h' mm'm'}c", { committed_at => $cdt }, $local_cdt->format_cldr( q{HH'h' mm'm'} ) ],
372    ["%{d:strftime:%a at %H:%M:%S}c", { committed_at => $cdt }, $local_cdt->strftime('%a at %H:%M:%S') ],
373
374    ['%p', { planner_name => 'larry', planner_email => 'larry@example.com'  }, 'larry <larry@example.com>'],
375    ['%{n}p', { planner_name => 'damian' }, 'damian'],
376    ['%{name}p', { planner_name => 'chip' }, 'chip'],
377    ['%{e}p', { planner_email => 'larry@example.com'  }, 'larry@example.com'],
378    ['%{email}p', { planner_email => 'damian@example.com' }, 'damian@example.com'],
379
380    ['%{date}p', { planned_at => $pdt }, $pdt->as_string( format => 'iso' ) ],
381    ['%{date:rfc}p', { planned_at => $pdt }, $pdt->as_string( format => 'rfc' ) ],
382    ['%{d:long}p', { planned_at => $pdt }, $pdt->as_string( format => 'long' ) ],
383    ["%{d:cldr:HH'h' mm'm'}p", { planned_at => $pdt }, $local_pdt->format_cldr( q{HH'h' mm'm'} ) ],
384    ["%{d:strftime:%a at %H:%M:%S}p", { planned_at => $pdt }, $local_pdt->strftime('%a at %H:%M:%S') ],
385
386    ['%t', { tags => [] }, '' ],
387    ['%t', { tags => ['@foo'] }, ' @foo' ],
388    ['%t', { tags => ['@foo', '@bar'] }, ' @foo, @bar' ],
389    ['%{|}t', { tags => [] }, '' ],
390    ['%{|}t', { tags => ['@foo'] }, ' @foo' ],
391    ['%{|}t', { tags => ['@foo', '@bar'] }, ' @foo|@bar' ],
392
393    ['%T', { tags => [] }, '' ],
394    ['%T', { tags => ['@foo'] }, ' (@foo)' ],
395    ['%T', { tags => ['@foo', '@bar'] }, ' (@foo, @bar)' ],
396    ['%{|}T', { tags => [] }, '' ],
397    ['%{|}T', { tags => ['@foo'] }, ' (@foo)' ],
398    ['%{|}T', { tags => ['@foo', '@bar'] }, ' (@foo|@bar)' ],
399
400    ['%r', { requires => [] }, '' ],
401    ['%r', { requires => ['foo'] }, ' foo' ],
402    ['%r', { requires => ['foo', 'bar'] }, ' foo, bar' ],
403    ['%{|}r', { requires => [] }, '' ],
404    ['%{|}r', { requires => ['foo'] }, ' foo' ],
405    ['%{|}r', { requires => ['foo', 'bar'] }, ' foo|bar' ],
406
407    ['%R', { requires => [] }, '' ],
408    ['%R', { requires => ['foo'] }, __('Requires: ') . " foo\n" ],
409    ['%R', { requires => ['foo', 'bar'] }, __('Requires: ') . " foo, bar\n" ],
410    ['%{|}R', { requires => [] }, '' ],
411    ['%{|}R', { requires => ['foo'] }, __('Requires: ') . " foo\n" ],
412    ['%{|}R', { requires => ['foo', 'bar'] }, __('Requires: ') . " foo|bar\n" ],
413
414    ['%x', { conflicts => [] }, '' ],
415    ['%x', { conflicts => ['foo'] }, ' foo' ],
416    ['%x', { conflicts => ['foo', 'bax'] }, ' foo, bax' ],
417    ['%{|}x', { conflicts => [] }, '' ],
418    ['%{|}x', { conflicts => ['foo'] }, ' foo' ],
419    ['%{|}x', { conflicts => ['foo', 'bax'] }, ' foo|bax' ],
420
421    ['%X', { conflicts => [] }, '' ],
422    ['%X', { conflicts => ['foo'] }, __('Conflicts:') . " foo\n" ],
423    ['%X', { conflicts => ['foo', 'bar'] }, __('Conflicts:') . " foo, bar\n" ],
424    ['%{|}X', { conflicts => [] }, '' ],
425    ['%{|}X', { conflicts => ['foo'] }, __('Conflicts:') . " foo\n" ],
426    ['%{|}X', { conflicts => ['foo', 'bar'] }, __('Conflicts:') . " foo|bar\n" ],
427
428    ['%{yellow}C', {}, '' ],
429    ['%{:event}C', { event => 'deploy' }, '' ],
430    ['%v', {}, "\n" ],
431    ['%%', {}, '%' ],
432
433    ['%s', { note => 'hi there' }, 'hi there' ],
434    ['%s', { note => "hi there\nyo" }, 'hi there' ],
435    ['%s', { note => "subject line\n\nfirst graph\n\nsecond graph\n\n" }, 'subject line' ],
436    ['%{  }s', { note => 'hi there' }, '  hi there' ],
437    ['%{xx}s', { note => 'hi there' }, 'xxhi there' ],
438
439    ['%b', { note => 'hi there' }, '' ],
440    ['%b', { note => "hi there\nyo" }, 'yo' ],
441    ['%b', { note => "subject line\n\nfirst graph\n\nsecond graph\n\n" }, "first graph\n\nsecond graph\n\n" ],
442    ['%{  }b', { note => 'hi there' }, '' ],
443    ['%{xxx }b', { note => "hi there\nyo" }, "xxx yo" ],
444    ['%{x}b', { note => "subject line\n\nfirst graph\n\nsecond graph\n\n" }, "xfirst graph\nx\nxsecond graph\nx\n" ],
445    ['%{ }b', { note => "hi there\r\nyo" }, " yo" ],
446
447    ['%B', { note => 'hi there' }, 'hi there' ],
448    ['%B', { note => "hi there\nyo" }, "hi there\nyo" ],
449    ['%B', { note => "subject line\n\nfirst graph\n\nsecond graph\n\n" }, "subject line\n\nfirst graph\n\nsecond graph\n\n" ],
450    ['%{  }B', { note => 'hi there' }, '  hi there' ],
451    ['%{xxx }B', { note => "hi there\nyo" }, "xxx hi there\nxxx yo" ],
452    ['%{x}B', { note => "subject line\n\nfirst graph\n\nsecond graph\n\n" }, "xsubject line\nx\nxfirst graph\nx\nxsecond graph\nx\n" ],
453    ['%{ }B', { note => "hi there\r\nyo" }, " hi there\r\n yo" ],
454
455    ['%{change}a',    $event, "change    $event->{change}\n" ],
456    ['%{change_id}a', $event, "change_id $event->{change_id}\n" ],
457    ['%{event}a',     $event, "event     $event->{event}\n" ],
458    ['%{tags}a',      $event, 'tags      ' . join(', ', @{ $event->{tags} }) . "\n" ],
459    ['%{requires}a',  $event, 'requires  ' . join(', ', @{ $event->{requires} }) . "\n" ],
460    ['%{conflicts}a', $event, '' ],
461    ['%{committer_name}a', $event, "committer_name $event->{committer_name}\n" ],
462    ['%{committed_at}a',   $event, "committed_at $craw\n" ],
463) {
464    local $ENV{ANSI_COLORS_DISABLED} = 1;
465    (my $desc = encode_utf8 $spec->[2]) =~ s/\n/[newline]/g;
466    is $formatter->format( $spec->[0], $spec->[1] ), $spec->[2],
467        qq{Format "$spec->[0]" should output "$desc"};
468}
469
470throws_ok { $formatter->format( '%_', {} ) } 'App::Sqitch::X',
471    'Should get exception for format "%_"';
472is $@->ident, 'format', '%_ error ident should be "format"';
473is $@->message, __ 'No label passed to the _ format',
474    '%_ error message should be correct';
475throws_ok { $formatter->format( '%{foo}_', {} ) } 'App::Sqitch::X',
476    'Should get exception for unknown label in format "%_"';
477is $@->ident, 'format', 'Invalid %_ label error ident should be "format"';
478is $@->message, __x(
479    'Unknown label "{label}" passed to the _ format',
480    label => 'foo'
481), 'Invalid %_ label error message should be correct';
482
483ok $log = $CLASS->new(
484    sqitch    => $sqitch,
485    formatter => App::Sqitch::ItemFormatter->new(abbrev => 4)
486), 'Instantiate with abbrev => 4';
487is $log->formatter->format( '%h', { change_id => '123456789' } ),
488    '1234', '%h should respect abbrev';
489is $log->formatter->format( '%H', { change_id => '123456789' } ),
490    '123456789', '%H should not respect abbrev';
491
492ok $log = $CLASS->new(
493    sqitch    => $sqitch,
494    formatter => App::Sqitch::ItemFormatter->new(date_format => 'rfc')
495), 'Instantiate with date_format => "rfc"';
496is $log->formatter->format( '%{date}c', { committed_at => $cdt } ),
497    $cdt->as_string( format => 'rfc' ),
498    '%{date}c should respect the date_format attribute';
499is $log->formatter->format( '%{d:iso}c', { committed_at => $cdt } ),
500    $cdt->as_string( format => 'iso' ),
501    '%{iso}c should override the date_format attribute';
502
503throws_ok { $formatter->format( '%{foo}a', {}) } 'App::Sqitch::X',
504    'Should get exception for unknown attribute passed to %a';
505is $@->ident, 'format', '%a error ident should be "format"';
506is $@->message, __x(
507    '{attr} is not a valid change attribute', attr => 'foo'
508), '%a error message should be correct';
509
510
511delete $ENV{ANSI_COLORS_DISABLED};
512for my $color (qw(yellow red blue cyan magenta)) {
513    is $formatter->format( "%{$color}C", {} ), color($color),
514        qq{Format "%{$color}C" should output }
515        . color($color) . $color . color('reset');
516}
517
518for my $spec (
519    [ ':event', { event => 'deploy' }, 'green', 'deploy' ],
520    [ ':event', { event => 'revert' }, 'blue',  'revert' ],
521    [ ':event', { event => 'fail'   }, 'red',   'fail'   ],
522) {
523    is $formatter->format( "%{$spec->[0]}C", $spec->[1] ), color($spec->[2]),
524        qq{Format "%{$spec->[0]}C" on "$spec->[3]" should output }
525        . color($spec->[2]) . $spec->[2] . color('reset');
526}
527
528# Make sure other colors work.
529my $yellow = color('yellow') . '%s' . color('reset');
530my $green  = color('green')  . '%s' . color('reset');
531$event->{conflicts} = [qw(dr_evil)];
532for my $spec (
533    [ full => sprintf($green, __ ('Deploy') . ' 000011112222333444')
534        . " (\@beta, \@gamma)\n"
535        . __ ('Name:     ') . " lolz\n"
536        . __ ('Project:  ') . " logit\n"
537        . __ ('Requires: ') . " foo, bar\n"
538        . __ ('Conflicts:') . " dr_evil\n"
539        . __ ('Planner:  ') . " damian <damian\@example.com>\n"
540        . __ ('Planned:  ') . " __PDATE__\n"
541        . __ ('Committer:') . " larry <larry\@example.com>\n"
542        . __ ('Committed:') . " __CDATE__\n\n"
543        . "    For the LOLZ.\n    \n    You know, funny stuff and cute kittens, right?\n"
544    ],
545    [ long => sprintf($green, __ ('Deploy') . ' 000011112222333444')
546        . " (\@beta, \@gamma)\n"
547        . __ ('Name:     ') . " lolz\n"
548        . __ ('Project:  ') . " logit\n"
549        . __ ('Planner:  ') . " damian <damian\@example.com>\n"
550        . __ ('Committer:') . " larry <larry\@example.com>\n\n"
551        . "    For the LOLZ.\n    \n    You know, funny stuff and cute kittens, right?\n"
552    ],
553    [ medium => sprintf($green, __ ('Deploy') . ' 000011112222333444') . "\n"
554        . __ ('Name:     ') . " lolz\n"
555        . __ ('Committer:') . " larry <larry\@example.com>\n"
556        . __ ('Date:     ') . " __CDATE__\n\n"
557        . "    For the LOLZ.\n    \n    You know, funny stuff and cute kittens, right?\n"
558    ],
559    [ short => sprintf($green, __ ('Deploy') . ' 000011112222333444') . "\n"
560        . __ ('Name:     ') . " lolz\n"
561        . __ ('Committer:') . " larry <larry\@example.com>\n\n"
562        . "    For the LOLZ.\n",
563    ],
564    [ oneline => sprintf "$green %s %s", '000011112222333444' . ' '
565        . __('deploy'), 'logit:lolz', 'For the LOLZ.',
566    ],
567) {
568    my $format = $CLASS->configure( $config, { format => $spec->[0] } )->{format};
569    ok my $log = $CLASS->new( sqitch => $sqitch, format => $format ),
570        qq{Instantiate with format "$spec->[0]" again};
571    (my $exp = $spec->[1]) =~ s/__CDATE__/$ciso/;
572    $exp =~ s/__PDATE__/$piso/;
573    is $log->formatter->format( $log->format, $event ), $exp,
574        qq{Format "$spec->[0]" should output correctly with color};
575}
576
577throws_ok { $formatter->format( '%{BLUELOLZ}C', {} ) } 'App::Sqitch::X',
578    'Should get an error for an invalid color';
579is $@->ident, 'format', 'Invalid color error ident should be "format"';
580is $@->message, __x(
581    '{color} is not a valid ANSI color', color => 'BLUELOLZ'
582), 'Invalid color error message should be correct';
583
584##############################################################################
585# Test execute().
586my $emock = Test::MockModule->new('App::Sqitch::Engine::sqlite');
587$emock->mock(destination => 'flipr');
588
589my $mock_target = Test::MockModule->new('App::Sqitch::Target');
590my ($target_name_arg, $orig_meth);
591$target_name_arg = '_blah';
592$mock_target->mock(new => sub {
593    my $self = shift;
594    my %p = @_;
595    $target_name_arg = $p{name};
596    $self->$orig_meth(@_);
597});
598$orig_meth = $mock_target->original('new');
599
600# First test for uninitialized DB.
601my $init = 0;
602$emock->mock(initialized => sub { $init });
603throws_ok { $log->execute } 'App::Sqitch::X',
604    'Should get exception for unititialied db';
605is $@->ident, 'log', 'Uninit db error ident should be "log"';
606is $@->exitval, 1, 'Uninit db exit val should be 1';
607is $@->message, __x(
608    'Database {db} has not been initialized for Sqitch',
609    db => 'db:sqlite:',
610), 'Uninit db error message should be correct';
611is $target_name_arg, undef, 'Should have passed undef to Target';
612
613# Next, test for no events.
614$init = 1;
615$target_name_arg = '_blah';
616my @events;
617my $iter = sub { shift @events };
618my $search_args;
619$emock->mock(search_events => sub {
620    shift;
621    $search_args = [@_];
622    return $iter;
623});
624$log = $CLASS->new(sqitch => $sqitch);
625throws_ok { $log->execute } 'App::Sqitch::X',
626    'Should get error for empty event table';
627is $@->ident, 'log', 'no events error ident should be "log"';
628is $@->exitval, 1, 'no events exit val should be 1';
629is $@->message, __x(
630    'No events logged for {db}',
631    db => 'flipr',
632), 'no events error message should be correct';
633is_deeply $search_args, [limit => 1],
634    'Search should have been limited to one row';
635is $target_name_arg, undef, 'Should have passed undef to Target again';
636
637# Okay, let's add some events.
638push @events => {}, $event;
639$target_name_arg = '_blah';
640$log = $CLASS->new(sqitch => $sqitch);
641ok $log->execute, 'Execute log';
642is $target_name_arg, undef, 'Should have passed undef to Target once more';
643is_deeply $search_args, [
644    event     => undef,
645    change    => undef,
646    project   => undef,
647    committer => undef,
648    limit     => undef,
649    offset    => undef,
650    direction => 'DESC'
651], 'The proper args should have been passed to search_events';
652
653is_deeply +MockOutput->get_page, [
654    [__x 'On database {db}', db => 'flipr'],
655    [ $log->formatter->format( $log->format, $event ) ],
656], 'The change should have been paged';
657
658# Make sure a passed target is processed.
659push @events => {}, $event;
660$target_name_arg = '_blah';
661ok $log->execute('db:sqlite:whatever.db'), 'Execute with target arg';
662is $target_name_arg, 'db:sqlite:whatever.db',
663    'Target name should have been passed to Target';
664is_deeply $search_args, [
665    event     => undef,
666    change    => undef,
667    project   => undef,
668    committer => undef,
669    limit     => undef,
670    offset    => undef,
671    direction => 'DESC'
672], 'The proper args should have been passed to search_events';
673
674is_deeply +MockOutput->get_page, [
675    [__x 'On database {db}', db => 'flipr'],
676    [ $log->formatter->format( $log->format, $event ) ],
677], 'The change should have been paged';
678
679# Set attributes and add more events.
680my $event2 = {
681    event           => 'revert',
682    change_id       => '84584584359345',
683    change          => 'barf',
684    tags            => [],
685    committer_name  => 'theory',
686    committer_email => 'theory@example.com',
687    committed_at    => $cdt,
688    note            => 'Oh man this was a bad idea',
689};
690push @events => {}, $event, $event2;
691isa_ok $log = $CLASS->new(
692    sqitch            => $sqitch,
693    target            => 'db:sqlite:foo.db',
694    event             => [qw(revert fail)],
695    change_pattern    => '.+',
696    project_pattern   => '.+',
697    committer_pattern => '.+',
698    max_count         => 10,
699    skip              => 5,
700    reverse           => 1,
701), $CLASS, 'log with attributes';
702
703$target_name_arg = '_blah';
704ok $log->execute, 'Execute log with attributes';
705is $target_name_arg, $log->target, 'Should have passed target name to Target';
706is_deeply $search_args, [
707    event     => [qw(revert fail)],
708    change    => '.+',
709    project   => '.+',
710    committer => '.+',
711    limit     => 10,
712    offset    => 5,
713    direction => 'ASC'
714], 'All params should have been passed to search_events';
715
716is_deeply +MockOutput->get_page, [
717    [__x 'On database {db}', db => 'flipr'],
718    [ $log->formatter->format( $log->format, $event ) ],
719    [ $log->formatter->format( $log->format, $event2 ) ],
720], 'Both changes should have been paged';
721
722# Make sure we get a warning when both the option and the arg are specified.
723push @events => {}, $event;
724ok $log->execute('foo'), 'Execute log with attributes';
725is $target_name_arg, $log->target, 'Should have passed target name to Target';
726is_deeply +MockOutput->get_warn, [[__x(
727    'Both the --target option and the target argument passed; using {option}',
728    option => $log->target,
729)]], 'Should have got warning for two targets';
730
731# Make sure we catch bad format codes.
732isa_ok $log = $CLASS->new(
733    sqitch => $sqitch,
734    format => '%Z',
735), $CLASS, 'log with bad format';
736
737push @events, {}, $event;
738$target_name_arg = '_blah';
739throws_ok { $log->execute } 'App::Sqitch::X',
740    'Should get an exception for a bad format code';
741is $@->ident, 'format',
742    'bad format code format error ident should be "format"';
743is $@->message, __x(
744    'Unknown format code "{code}"', code => 'Z',
745), 'bad format code format error message should be correct';
746is $target_name_arg, $log->target, 'Should have passed target name to Target';
747