7 ##############################################################################
11 tprove_gtk - Simple proof of concept GUI for proving tests
15 tprove_gtk [ list of test files ]
19 I've included this in the distribution. It's a gtk interface by Torsten
20 Schoenfeld. I've not run it myself.
22 C<tprove_gtk> is not installed on your system unless you explicitly copy it
23 somewhere in your path. The current incarnation B<must> be run in a directory
24 with both C<t/> and C<lib/> (i.e., the standard "root" level directory in
25 which CPAN style modules are developed). This will probably change in the
26 future. As noted, this is a proof of concept.
30 This is alpha code. You've been warned.
40 sub { -f && /\.t$/ && push @tests => $File::Find::name },
45 pipe( my $reader, my $writer );
47 # Unfortunately, autoflush-ing seems to be a big performance problem. If you
48 # don't care about "real-time" progress bars, turn this off.
49 $writer->autoflush(1);
51 if ( my $pid = fork ) {
54 my $gui = Gui->new( $pid, $reader );
55 $gui->add_tests(@tests);
60 die "Cannot fork: $!" unless defined $pid;
63 my $runner = TestRunner->new($writer);
64 $runner->add_tests(@tests);
70 ###############################################################################
71 # --------------------------------------------------------------------------- #
72 ###############################################################################
76 use Glib qw(TRUE FALSE);
90 if ( !Gtk2->CHECK_VERSION( 2, 6, 0 ) ) {
91 die("$0 needs gtk+ >= 2.6");
98 if ( defined $self->{reader_source} ) {
99 Glib::Source->remove( $self->{reader_source} );
104 my ( $class, $child_pid, $reader ) = @_;
106 my $self = bless {}, $class;
108 $self->create_window();
109 $self->create_menu();
110 $self->create_view();
112 $self->{child_pid} = $child_pid;
113 $self->{child_running} = TRUE;
115 $self->{reader_source} = Glib::IO->add_watch(
116 fileno $reader, [qw(in pri hup)],
117 \&_callback_reader, $self
124 my ( $self, @tests ) = @_;
126 my $model = $self->{_model};
128 $self->{_path_cache} = {};
130 foreach my $test (@tests) {
131 my $iter = $model->append();
132 $model->set( $iter, COLUMN_FILENAME, $test );
133 $self->{_path_cache}->{$test} = $model->get_path($iter);
140 my $window = Gtk2::Window->new();
141 my $vbox = Gtk2::VBox->new( FALSE, 5 );
144 $window->set_title("Test Runner");
145 $window->set_default_size( 300, 600 );
146 $window->signal_connect( delete_event => \&_callback_quit, $self );
148 $self->{_window} = $window;
149 $self->{_vbox} = $vbox;
155 my $window = $self->{_window};
156 my $vbox = $self->{_vbox};
161 <menu action="test_menu">
162 <menuitem action="quit_item" />
169 [ "test_menu", undef, "_Tests" ],
174 "Quit the test runner",
175 sub { _callback_quit( undef, undef, $self ) },
179 my $action_group = Gtk2::ActionGroup->new("main");
180 $action_group->add_actions($actions);
182 my $manager = Gtk2::UIManager->new();
183 $manager->insert_action_group( $action_group, 0 );
184 $manager->add_ui_from_string($ui);
186 my $menu_box = Gtk2::VBox->new( FALSE, 0 );
187 $manager->signal_connect(
189 my ( $manager, $widget ) = @_;
190 $menu_box->pack_start( $widget, FALSE, FALSE, 0 );
194 $vbox->pack_start( $menu_box, FALSE, FALSE, 0 );
195 $window->add_accel_group( $manager->get_accel_group() );
197 $self->{_manager} = $manager;
203 my $window = $self->{_window};
204 my $vbox = $self->{_vbox};
206 my $scroller = Gtk2::ScrolledWindow->new();
207 $scroller->set_policy( "never", "automatic" );
209 my $model = Gtk2::ListStore->new(
211 # filename total run pass fail skip todo
212 qw(Glib::String Glib::Int Glib::Int Glib::Int Glib::Int Glib::Int Glib::Int)
214 my $view = Gtk2::TreeView->new($model);
216 # ------------------------------------------------------------------------- #
218 my $column_filename = Gtk2::TreeViewColumn->new_with_attributes(
220 Gtk2::CellRendererText->new(),
221 text => COLUMN_FILENAME
223 $column_filename->set_sizing("autosize");
224 $column_filename->set_expand(TRUE);
225 $view->append_column($column_filename);
227 # ------------------------------------------------------------------------- #
229 my $renderer_progress = Gtk2::CellRendererProgress->new();
230 my $column_progress = Gtk2::TreeViewColumn->new_with_attributes(
234 $column_progress->set_cell_data_func(
237 my ( $column, $renderer, $model, $iter ) = @_;
240 = $model->get( $iter, COLUMN_TOTAL, COLUMN_RUN );
252 text => "$run/$total",
253 value => $run / $total * 100
264 $view->append_column($column_progress);
266 # ------------------------------------------------------------------------- #
268 my @count_columns = (
269 [ "Pass", COLUMN_PASS ],
270 [ "Fail", COLUMN_FAIL ],
271 [ "Skip", COLUMN_SKIP ],
272 [ "Todo", COLUMN_TODO ],
275 foreach (@count_columns) {
276 my ( $heading, $column_number ) = @{$_};
278 my $renderer = Gtk2::CellRendererText->new();
279 $renderer->set( xalign => 1.0 );
281 my $column = Gtk2::TreeViewColumn->new_with_attributes(
284 text => $column_number
287 $view->append_column($column);
290 # ------------------------------------------------------------------------- #
292 $scroller->add($view);
293 $vbox->pack_start( $scroller, TRUE, TRUE, 0 );
295 $self->{_view} = $view;
296 $self->{_model} = $model;
302 $self->{_window}->show_all();
307 # --------------------------------------------------------------------------- #
309 sub _callback_reader {
310 my ( $fileno, $condition, $self ) = @_;
312 if ( $condition & "in" || $condition & "pri" ) {
313 my $data = <$reader>;
315 if ( $data !~ /^[^\t]+ \t \d+ \t \d+ \t \d+ \t \d+ \t \d+ \t \d+$/x )
320 my ( $filename, $total, $run, $pass, $fail, $skip, $todo )
323 my $view = $self->{_view};
324 my $model = $self->{_model};
325 my $path_cache = $self->{_path_cache};
327 if ( $path_cache->{$filename} ) {
328 my $iter = $model->get_iter( $path_cache->{$filename} );
331 COLUMN_TOTAL, $total,
338 $view->scroll_to_cell( $path_cache->{$filename} );
342 elsif ( $condition & "hup" ) {
343 $self->{child_running} = FALSE;
348 warn "got unknown condition: $condition";
356 my ( $window, $event, $self ) = @_;
358 if ( $self->{child_running} ) {
359 kill "TERM", $self->{child_pid};
365 ###############################################################################
366 # --------------------------------------------------------------------------- #
367 ###############################################################################
372 use TAP::Parser::Source::Perl;
384 my ( $class, $writer ) = @_;
386 my $self = bless {}, $class;
388 $self->{_writer} = $writer;
394 my ( $self, @tests ) = @_;
396 $self->{_tests} = [@tests];
398 $self->{_results} = {};
399 foreach my $test ( @{ $self->{_tests} } ) {
400 $self->{_results}->{$test} = [ 0, 0, 0, 0, 0, 0 ];
407 my $source = TAP::Parser::Source::Perl->new();
409 foreach my $test ( @{ $self->{_tests} } ) {
410 my $stream = $source->source($test)->get_stream();
412 my $parser = $self->analyze( $test, $stream );
414 # $aggregate -> add($test, $parser);
417 warn "Could not run `$test´: " . $source->error();
422 my $writer = $self->{_writer};
424 $writer->print("\n");
428 my ( $self, $test, $stream ) = @_;
430 my $writer = $self->{_writer};
431 my $result = $self->{_results}->{$test};
433 my $parser = TAP::Parser->new( { stream => $stream } );
434 while ( my $line = $parser->next() ) {
435 if ( $line->is_plan() ) {
436 $result->[INDEX_TOTAL] = $line->tests_planned();
439 elsif ( $line->is_test() ) {
440 $result->[INDEX_RUN]++;
442 if ( $line->has_skip() ) {
443 $result->[INDEX_SKIP]++;
447 if ( $line->has_todo() ) {
448 $result->[INDEX_TODO]++;
451 if ( $line->is_ok() ) {
452 $result->[INDEX_PASS]++;
455 $result->[INDEX_FAIL]++;
459 elsif ( $line->is_comment() ) {
465 warn "Unknown result type `"
466 . $line->type() . "´: "
467 . $line->as_string();
470 my $string = join "\t", $test, @{$result};
471 $writer->print("$string\n");