#!/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('' => 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, '', [ '_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 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 or the F 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 =item * L =item * L =item * L =item * L =item * L =item * For further examples, take a look at the framework self test collection (t::tlib::AllTests). =back =cut