File: //usr/local/share/perl5/Test/Unit/TkTestRunner.pm
#!/usr/bin/perl -w
package Test::Unit::TkTestRunner;
use strict;
use base qw(Test::Unit::Runner);
use Tk;
use Tk::BrowseEntry;
use Benchmark;
use Test::Unit; # for copyright & version number
use Test::Unit::Result;
use Test::Unit::Loader;
sub new {
my $self = bless {}, shift;
return $self;
}
sub about {
my $self = shift;
my $dialog = $self->{frame}->DialogBox(
-title => 'About PerlUnit',
-buttons => [ 'OK' ]
);
my $text = $dialog->add("ROText"); #, -width => 80, -height => 20);
$text->insert("end", Test::Unit::COPYRIGHT_NOTICE);
$text->pack();
$dialog->Show();
}
sub add_error {
my $self = shift;
$self->{number_of_errors} = $self->{result}->error_count();
$self->append_failure("Error", @_);
$self->update();
}
sub add_failure {
my $self = shift;
$self->{number_of_failures} = $self->{result}->failure_count();
$self->append_failure("Failure", @_);
$self->update();
}
sub append_failure {
my ($self, $kind, $test, $exception)=@_;
my $message = $test->name(); #bad juju!!
if ($message) {
$kind .= ":".substr($message, 0, 100);
}
$self->{failure_list}->insert("end", $message);
push @{$self->{failed_tests}}, $test;
push @{$self->{exceptions}}, $exception;
}
sub plan{
my $self = shift;
$self->{planned} = shift;
}
sub choose_file {
my $self = shift;
my $name = $self->{suite_name};
my @types = ([ 'All Files', '*' ]);
my $dir = undef;
if (defined $name) {
require File::Basename;
my $sfx;
($name, $dir, $sfx) = File::Basename::fileparse($name, '\..*');
if (defined($sfx) && length($sfx)) {
unshift(@types, [ 'Similar Files', [$sfx]]);
$name .= $sfx;
}
}
my $file = $self->{frame}->getOpenFile(
-title => "Select test case",
-initialdir => $dir,
-initialfile => $name,
-filetypes => \@types
);
if (defined $file) {
$file=~s/\/+/\//g;
}
$self->{suite_name} = $file;
}
sub create_punit_menu {
my $self = shift;
my $main_menu = $self->{frame}->Menu(
-type => 'menubar',
-menuitems => [
[
cascade => 'F~ile',
-menuitems => [
[
command => 'O~pen',
-command => sub { $self->choose_file() }
],
[
command => 'Ex~it',
-command => sub { $self->{frame}->destroy() }
],
],
],
[
cascade => 'H~elp',
-menuitems => [
[
command => 'A~bout PerlUnit',
-command => sub { $self->about() }
],
],
],
],
);
return $main_menu;
}
sub create_menus {
my $self = shift;
$self->{frame}->configure(-menu => $self->create_punit_menu());
}
sub create_ui {
my $self = shift;
# Lay the window out....
my $mw = $self->{frame} = MainWindow->new(
-title => 'Run Test Suite',
-width => 200
);
# I need stretchy labels, Tk doesnt have them
my $mklabel = sub {
my (@args)=@_;
$self->{$args[0]} = $args[2];
$mw->Entry(
-textvariable => \$self->{$args[0]},
-justify => $args[1],
-relief => 'flat',
-state => 'disabled'
);
};
$self->create_menus();
$self->{suite_label} = $mw->Label(
-text => 'Enter the name of the TestCase:'
);
$self->{suite_name} = "x";
$self->{suite_field} = $mw->BrowseEntry(
-textvariable => \$self->{suite_name},
-choices => [],
);
$self->{add_text_listener} = sub { $self->run_suite() };
$self->{run} = $mw->Button(
-text => 'Run',
-state => 'normal',
-command => sub { $self->run_suite() }
);
my $lab1 = $mw->Label(-text => "Runs:");
my $lab2 = &{$mklabel}('number_of_runs', 'right', 0);
my $lab3 = $mw->Label(-text => "Errors:");
my $lab4 = &{$mklabel}('number_of_errors', 'right', 0);
my $lab5 = $mw->Label(-text => "Failures:");
my $lab6 = &{$mklabel}('number_of_failures', 'right', 0);
$self->{progress_bar} = $mw->ArrayBar(
-width => 20,
-length => 400,
-colors => [ 'green', 'red', 'grey' ]
);
$self->{failure_label} = $mw->Label(
-text => 'Errors and Failures:',
-justify => 'left'
);
$self->{failure_list} = $mw->Scrolled('Listbox', -scrollbars => 'e');
$self->{failure_list}->insert("end", "", "", "", "", "", "");
$self->{quit_button} = $mw->Button(
-text => 'Quit',
-command => sub { $mw->destroy() }
);
$self->{rerun_button} = $mw->Button(
-text => 'ReRun',
-state => 'normal',
-command => sub { $self->rerun() }
);
$self->{show_error_button} = $mw->Button(
-text => 'Show...',
-state => 'normal',
-command => sub { $self->show_error_trace() }
);
$self->{status_line_box}= &{$mklabel}('status_line', 'left', 'Status line');
$self->{status_line_box}->configure(-relief => 'sunken', -bg => 'grey');
# Bindings go here, so objects are already defined.
$self->{failure_list}->bind('<Double-1>' => sub { $self->show_error_trace() });
# all geometry management BELOW this point. Otherwise bindings
# wont work.
$self->{suite_label}->form(
-left => [ '%0' ],
-top => [ '%0' ],
-fill => 'x'
);
$self->{run}->form(
-right => [ '%100' ],
-top => [ $self->{suite_label} ],
);
$self->{suite_field}->form(
-left => [ '%0' ],
-right => [$self->{run}],
-top => [$self->{suite_label}], -fill => 'x'
);
$lab1->form(-left => ['%0'], -top => [$self->{suite_field}, 10]);
$lab2->form(-left => [$lab1], -top => [$self->{suite_field}, 10], -fill => 'x');
$lab3->form(-left => [$lab2], -top => [$self->{suite_field}, 10]);
$lab4->form(-left => [$lab3], -top => [$self->{suite_field}, 10], -fill => 'x');
$lab5->form(-left => [$lab4], -top => [$self->{suite_field}, 10]);
$lab6->form(-left => [$lab5], -top => [$self->{suite_field}, 10], -fill => 'x');
$self->{progress_bar}->form(-left => [ '%0' ], -top => [$lab6, 10]);
$self->{failure_label}->form(
-left => [ '%0' ],
-top => [$self->{progress_bar}, 10],
-right => [ '%100' ]
);
$self->{failure_list}->form(
-left => [ '%0' ],
-top => [$self->{failure_label}],
-right => [ '%100' ],
-fill => 'both'
);
# this is in a wierd order 'cos Quit keeps trying to resize.
$self->{quit_button}->form(
-right => [ '%100' ],
-bottom => [ '%100' ],
-fill => 'none'
);
$self->{show_error_button}->form(
-right => [ '%100' ],
-bottom => [$self->{quit_button}],
-top => [$self->{failure_list}]
);
# Rerun doesn't work yet.
# $self->{rerun_button}->form(
# -right => [$self->{show_error_button}],
# -top => [$self->{failure_list}]
# );
$self->{status_line_box}->form(
-left => [ '%0' ],
-right => [$self->{quit_button}],
-bottom => [ '%100' ],
-top => [$self->{show_error_button}],
-fill => 'x'
);
$self->reset();
return $mw;
}
sub end_test {
my $self = shift;
$self->{runs} = $self->{result}->run_count();
$self->update();
}
sub get_test {
my $self = shift;
my $suite = Test::Unit::Loader->obj_load(shift);
$self->{status_line}="";
return $suite;
}
sub is_error_selected {
my $self = shift;
($self->{listbox}->curselection>=0)?1:0;
}
sub load_frame_icon {
# not implemented
}
sub main {
my $main = new Test::Unit::TkTestRunner()->start(@_);
}
sub rerun {
# not implemented and not going to!
my $self = shift;
my $index = $self->{failure_list}->curselection;
return if $index < 0;
my $test = $self->{failed_tests}->[$index];
#if (! $test->isa("Test::Unit::TestCase")) {
$self->show_status("Could not reload test.");
#}
# Not sure how to do this...
}
sub reset {
my $self = shift;
$self->{number_of_errors} = 0;
$self->{number_of_failures} = 0;
$self->{number_of_runs} = 0;
$self->{planned} = 0;
$self->{failure_list}->delete(0, "end");
$self->{exceptions} = [];
$self->{failed_tests} = [];
$self->{progress_bar}->value(0, 0, 1);
}
sub run {
my $self = shift;
$self->run_suite();
}
sub run_failed {
my $self = shift;
# not implemented
}
sub run_suite {
my $self = shift;
my $suite;
if (defined($self->{runner})) {
$self->{result}->stop();
}
else {
$self->add_to_history();
$self->{run}->configure(-text => "Stop");
$self->show_info("Initializing...");
$self->reset();
$self->show_info("Load Test Case...");
eval {
$suite = $self->get_test($self->{suite_name});
};
if ($@ or !$suite) {
$suite = undef;
$self->show_status("Could not load test!");
}
if ($suite) {
$self->{runner} = 1;
$self->{planned} = $suite->count_test_cases();
$self->{result} = $self->create_test_result();
$self->{result}->add_listener($self);
$self->show_info("Running...");
$self->{start_time} = new Benchmark();
$suite->run($self->{result});
if ($self->{result}->should_stop()) {
$self->show_status("Stopped");
}
else {
$self->{finish_time} = new Benchmark();
$self->{run_time} = timediff($self->{finish_time},
$self->{start_time});
$self->show_info("Finished: ".timestr($self->{run_time}, 'nop'));
}
}
$self->{runner} = undef;
$self->{result} = undef;
$self->{run}->configure(-text => "Run");
}
}
sub show_error_trace {
# pop up a text dialog containing the details.
my $self = shift;
my $dialog = $self->{frame}->DialogBox(
-title => 'Details',
-buttons => [ 'OK' ]
);
my $selected = $self->{failure_list}->curselection;
return unless defined($selected) && $self->{exceptions}[$selected];
my $text = $dialog->add("Scrolled", "ROText", -width => 80, -height => 20)
->pack(-expand => 1, -fill => 'both');
$text->insert("end", $self->{exceptions}[$selected]->to_string());
my $e = $self->{exceptions}[$selected];
if ($e->object->annotations()) {
foreach my $data ("\n\nAnnotations:\n", $e->object->annotations()) {
$text->insert("end", $data); # third arg would be a tag
}
}
$dialog->Show();
}
sub show_info {
my $self = shift;
$self->{status_line} = shift;
$self->{status_line_box}->configure(-bg => 'grey');
}
sub show_status {
my $self = shift;
$self->{status_line} = shift;
$self->{status_line_box}->configure(-bg => 'red');
}
sub start {
my $self = shift;
my (@args)=@_;
my $mw = $self->create_ui();
if (@args) {
$self->{suite_name} = shift @args;
}
MainLoop;
}
sub start_test {
my $self = shift;
my $test = shift;
$self->{number_of_runs} = $self->{result}->run_count();
$self->show_info("Running: " . $test->name());
}
sub add_pass {
my $self = shift;
my ($test, $exception)=@_;
$self->update();
}
sub update {
my $self = shift;
my $result = $self->{result};
my $total = $result->run_count();
my $failures = $result->failure_count();
my $errors = $result->error_count();
my $passes = $total-$failures-$errors;
my $bad = $failures+$errors;
#$passes = $result->run_count();
my $todo = ($total>$self->{planned})?0:$self->{planned}-$total;
$self->{progress_bar}->value($passes, $bad, $todo);
# force entry into the event loop.
# this makes it nearly like its threaded...
#sleep 1;
$self->{frame}->update();
}
sub add_to_history {
my $self = shift;
my $new_item = $self->{suite_name};
my $h = $self->{suite_field};
my $choices = $h->cget('-choices');
my @choices = ();
if (ref($choices)) {
@choices=@{$h->cget('-choices')};
}
elsif ($choices) {
# extraordinarily bad - choices is a scalar if theres
# only one, and undefined if there are none!
@choices = ($h->cget('-choices'));
}
@choices = ($new_item, grep {$_ ne $new_item} @choices);
if (@choices>10) {
@choices=@choices[0..9];
}
$h->configure(-choices => \@choices);
}
package Tk::ArrayBar;
# progressbar doesnt cut it.
# This expects a variable which is an array ref, and
# a matching list of colours. Sortof like stacked progress bars.
# Heavily - ie almost totally - based on the code in ProgressBar.
use Tk;
use Tk::Canvas;
use Tk::ROText;
use Tk::DialogBox;
use Carp;
use strict;
use base qw(Tk::Derived Tk::Canvas);
Construct Tk::Widget 'ArrayBar';
sub ClassInit {
my ($class, $mw) = @_;
$class->SUPER::ClassInit($mw);
$mw->bind($class, '<Configure>', [ '_layoutRequest', 1 ]);
}
sub Populate {
my($c, $args) = @_;
$c->ConfigSpecs(
-width => [ PASSIVE => undef, undef, 0 ],
'-length' => [ PASSIVE => undef, undef, 0 ],
-padx => [ PASSIVE => 'padX', 'Pad', 0 ],
-pady => [ PASSIVE => 'padY', 'Pad', 0 ],
-colors => [ PASSIVE => undef, undef, undef ],
-relief => [ SELF => 'relief', 'Relief', 'sunken' ],
-value => [ METHOD => undef, undef, undef ],
-variable => [ PASSIVE => undef, undef, [ 0 ] ],
-anchor => [ METHOD => 'anchor', 'Anchor', 'w' ],
-resolution => [ PASSIVE => undef, undef, 1.0 ],
-highlightthickness => [
SELF => 'highlightThickness', 'HighlightThickness', 0
],
-troughcolor => [
PASSIVE => 'troughColor', 'Background', 'grey55'
],
);
_layoutRequest($c, 1);
$c->OnDestroy([ Destroyed => $c ]);
}
sub anchor {
my $c = shift;
my $var = \$c->{Configure}{'-anchor'};
my $old = $$var;
if (@_) {
my $new = shift;
croak "bad anchor position \"$new\": must be n, s, w or e"
unless $new =~ /^[news]$/;
$$var = $new;
}
$old;
}
sub _layoutRequest {
my $c = shift;
my $why = shift;
$c->afterIdle([ '_arrange', $c ]) unless $c->{layout_pending};
$c->{layout_pending} |= $why;
}
sub _arrange {
my $c = shift;
my $why = $c->{layout_pending};
$c->{layout_pending} = 0;
my $w = $c->Width;
my $h = $c->Height;
my $bw = $c->cget('-borderwidth') + $c->cget('-highlightthickness');
my $x = abs(int($c->{Configure}{'-padx'})) + $bw;
my $y = abs(int($c->{Configure}{'-pady'})) + $bw;
my $value = $c->cget('-variable');
my $horz = $c->{Configure}{'-anchor'} =~ /[ew]/i ? 1 : 0;
my $dir = $c->{Configure}{'-anchor'} =~ /[ne]/i ? -1 : 1;
if ($w == 1 && $h == 1) {
my $bw = $c->cget('-borderwidth');
$h = $c->pixels($c->cget('-length')) || 40;
$w = $c->pixels($c->cget('-width')) || 20;
($w, $h) = ($h, $w) if $horz;
$c->GeometryRequest($w, $h);
$c->parent->update;
$c->update;
$w = $c->Width;
$h = $c->Height;
}
$w -= $x*2;
$h -= $y*2;
my $length = $horz ? $w : $h;
my $width = $horz ? $h : $w;
# at this point we have the length and width of the
# bar independent of orientation and padding.
# blocks and gaps are not used.
# unlike progressbar I need to redraw these each time.
# actually resizing them might be better...
my $colors = $c->{Configure}{'-colors'} || [ 'green', 'red', 'grey55' ];
$c->delete($c->find('all'));
$c->createRectangle(
0, 0, $w+$x*2, $h+$y*2,
-fill => $c->{Configure}{'-troughcolor'},
-width => 0,
-outline => undef
);
my $total;
my $count_value = scalar(@$value)-1;
foreach my $val (@$value) {
$total += $val > 0 ? $val : 0;
}
# prevent div by zero and give a nice initial appearance.
$total = $total ? $total : 1;
my $curx = $x;
my $cury = $y;
foreach my $index (0..$count_value) {
my $size = ($length*$value->[$index])/$total;
my $ud = $horz?$width:$size;
my $lr = $horz?$size:$width;
$c->{cover}->[$index] = $c->createRectangle(
$curx, $cury, $curx+$lr-1, $cury+$ud-1,
-fill => $colors->[$index],
-width => 1,
-outline => 'black'
);
$curx+=$horz?$lr:0;
$cury+=$horz?0:$ud;
}
}
sub value {
my $c = shift;
my $val = $c->cget('-variable');
if (@_) {
$c->configure(-variable => [@_]);
_layoutRequest($c, 2);
}
}
sub Destroyed {
my $c = shift;
my $var = delete $c->{'-variable'};
untie $$var if defined($var) && ref($var);
}
1;
__END__
=head1 NAME
Test::Unit::TkTestRunner - unit testing framework helper class
=head1 SYNOPSIS
use Test::Unit::TkTestRunner;
Test::Unit::TkTestRunner::main($my_testcase_class);
=head1 DESCRIPTION
This class is the test runner for the GUI style use of the testing
framework.
It is used by simple command line tools like the F<TkTestRunner.pl>
script provided.
The class needs as arguments the names of the classes encapsulating
the tests to be run.
=head1 AUTHOR
Copyright (c) 2000-2002, 2005 the PerlUnit Development Team
(see L<Test::Unit> or the F<AUTHORS> file included in this
distribution).
All rights reserved. This program is free software; you can
redistribute it and/or modify it under the same terms as Perl itself.
=head1 SEE ALSO
=over 4
=item *
L<Test::Unit::Loader>
=item *
L<Test::Unit::Listener>
=item *
L<Test::Unit::Result>
=item *
L<Test::Unit::TestRunner>
=item *
L<Test::Unit::TestCase>
=item *
L<Test::Unit::TestSuite>
=item *
For further examples, take a look at the framework self test
collection (t::tlib::AllTests).
=back
=cut