Skip to content
Browse files

lotta goo

  • Loading branch information...
1 parent e23bb26 commit b25829f36df9c0fd5cf356485bb4da076d177e85 @zzo committed Jul 26, 2011
View
4 README.md
@@ -1,6 +1,6 @@
<!-- The code below creates a default page header by spacing out the topic name -->
-%SPACEOUT{%TOPIC%}%
+JUTE
====================
<!-- %TOC% -->
@@ -23,7 +23,7 @@ Requirements
yapache 1.x (maybe 2.x works - have not tested) for Selenium and Capture modes
-------------------------------------------------------------------------------
-[NodeJS](NodeJS.html) .2 for v8 mode
+[NodeJS](http://nodejs.org) .4 for v8 mode
-------------------------------------
View
878 backend/Yahoo/JUTE/Actions.pm
@@ -0,0 +1,878 @@
+package Yahoo::JUTE::Actions;
+
+my $VERSION = '1.0';
+my $ERROR = q(<?xml version="1.0" encoding="UTF-8"?><testsuites><testsuite name="BROWSER" tests="0" failures="1" time="0">Test Timed Out: Most likely a Javascript parsing error - try loading URL in your browser</testsuite></testsuites>);
+
+use IO::File;
+use File::Temp qw(:POSIX);
+use Fcntl qw(:flock SEEK_END);
+use POSIX ":sys_wait_h";
+use JSON;
+use IPC::ShareLite qw(:lock);
+use URI;
+use URI::Escape;
+use Sys::Hostname;
+use File::Basename;
+use Yahoo::JUTE::Settings;
+
+use Net::hostent;
+use Socket;
+
+use Data::Dumper;
+
+my $BROWSER_TIME_THRESHOLD = $Yahoo::JUTE::Settings::heartbeat_interval || 20;
+my $TEST_TIME_THRESHOLD = $Yahoo::JUTE::Settings::test_wait_threshold || 60;
+my $REMOTE_CHECK_INTERVAL = 10; # seconds between checking for output/if selenium is done
+my $TEST_RUN_CHECK_INTERVAL = 5; # seconds between checking if selenium is done
+my $LOCAL_PORT = 80;
+
+sub get_yslow {
+ my($args) = @_;
+
+ my $params = $args->{params};
+ my $cgi = $args->{cgi};
+ my $output_filename = tmpnam();
+ warn "Writing yslow json output to $output_filename\n";
+
+ my $sel_host = $params->{selenium_host};
+ my $timing = $params->{do_timing};
+ my $url = $params->{url};
+ my $uri = URI->new($url);
+
+ fiddle_share($args,
+ sub {
+ my($obj) = @_;
+ my $save_obj = { output_file => $output_filename };
+ if ($timing) {
+ $save_obj->{wait_for_timing} = 1;
+ }
+ $obj->{yslow}{$uri->host} = $save_obj;
+ }
+ );
+
+ # add timing url if requested
+ if ($timing) {
+ $url .= '?timing_host=' . $cgi->server_name . '/jute/_yslow_timing';
+ warn "Timing url is: $url\n";
+ }
+
+ require WWW::Selenium;
+ my $selenium = WWW::Selenium->new(
+ host => $sel_host,
+ port => 4444,
+ browser => '*firefox',
+ browser_url => $url,
+ );
+
+ $selenium->open($url);
+
+ # Now wait...
+ eval {
+ $selenium->wait_for_page_to_load(2000000);
+ };
+ warn "Error waiting: $@\n" if ($@);
+
+# sleep(10);
+ open(JSON, $output_filename);
+ my $yslow_json = eval { from_json(<JSON>); };
+ close(JSON);
+
+ warn "Responding...!\n";
+ my $response = $args->{response};
+ $response->code(200);
+ $response->content_type('application/x-json');
+ $response->content(to_json({ yslow_data => $yslow_json }));
+}
+
+# YSLOW posting timing back to us
+sub yslow_timing {
+ my($args) = @_;
+
+ my $params = $args->{get_p};
+ my $render_time = $params->{renderTime};
+ my $load_time = $params->{loadTime};
+ my $url = $params->{url};
+ my $uri = URI->new($url);
+
+ my ($obj, $output_obj) = fiddle_share($args,
+ sub {
+ my($obj) = @_;
+ my $ys_obj = $obj->{yslow}{$uri->host};
+ $ys_obj->{render_time} = $render_time;
+ $ys_obj->{load_time} = $load_time;
+ delete $ys_obj->{wait_for_timing};
+ if ($ys_obj->{yslow_object}) {
+ return delete $obj->{yslow}{$uri->host};
+ } else {
+ return;
+ }
+ }
+ );
+
+ if ($output_obj) {
+ dump_yslow_output($args, $output_obj);
+ }
+
+ my $response = $args->{response};
+ $response->code( 200 );
+ $response->content_type('text/plain');
+ $response->content('OK');
+}
+
+# YSLOW posting back to us
+sub yslow {
+ my($args) = @_;
+
+ my $params = $args->{params};
+ my $yslow_var = $params->{POSTDATA};
+ my $yslow_obj = eval { from_json(uri_unescape($yslow_var)); };
+ my $url = $yslow_obj->{u};
+ my $uri = URI->new($url);
+
+ my ($obj, $output_obj) = fiddle_share($args,
+ sub {
+ my ($obj) = @_;
+ my $ys_obj = $obj->{yslow}{$uri->host};
+ if ($ys_obj->{wait_for_timing}) {
+ $ys_obj->{yslow_object} = $yslow_obj;
+ return;
+ } else {
+ $ys_obj->{yslow_object} = $yslow_obj;
+ return delete $obj->{yslow}{$uri->host};
+ }
+ }
+ );
+
+ if ($output_obj) {
+ dump_yslow_output($args, $output_obj);
+ }
+
+ my $response = $args->{response};
+ $response->code( 200 );
+ $response->content_type('text/plain');
+ $response->content('OK');
+}
+
+sub dump_yslow_output {
+ my($args, $output_obj) = @_;
+
+ $output_obj->{yslow_object}{neo_load_time} = $output_obj->{load_time};
+ $output_obj->{yslow_object}{neo_render_time} = $output_obj->{render_time};
+
+ my $dump_me = to_json($output_obj->{yslow_object});
+
+ my $output_file = $output_obj->{output_file};
+ if ($output_file && open(O, ">$output_file")) {
+ print O $dump_me;
+ close(O);
+ } else {
+ _dump_file({ dump_me => $dump_me }, 'dump_me', 'yslow.json', '', $args);
+ }
+}
+
+sub make_sane_names {
+ my($browser) = @_;
+
+ # make a sane filename (especially for hudson)
+ my ($filename, $ip) = split /---/, $browser;
+ $filename =~ s/[\/;]//g;
+ $filename =~ s/[^A-Z0-9a-z-]/_/g;
+
+ # Make a hudson-happy java-like package name
+ my $pkgname = $filename;
+ $pkgname =~ s/\.//g;
+ $pkgname =~ s/_+/./g;
+
+ return ($filename, $pkgname);
+}
+
+sub test_report {
+ my($args) = @_;
+
+ my $params = $args->{params};
+ my $response = $args->{response};
+ my $xml = $params->{results};
+ my $browser = _browser_key($args);
+
+ my($filename, $pkgname) = make_sane_names(_browser_name($args));
+
+ $xml =~ s/testsuite name="/testsuite name="$pkgname./g;
+ $params->{results} = $xml;
+
+ my $name = $params->{name} || 'remote_cover';
+
+ my $succeeded = 1;
+
+ if ($params->{name}) {
+ my ($file) = _dump_file($params, 'results', $filename . '-test.xml', $name, $args);
+ if (failed_tests($file)) {
+ $succeeded = 0;
+ }
+ warn "Test Report for $name - succeeded: $succeeded";
+ }
+
+ if ($params->{coverage} && $params->{coverage} ne 'null') {
+ my $cover_obj = from_json($params->{coverage});
+ foreach my $file (keys %$cover_obj) {
+ $new_file = join '/', 'output', $name, 'lcov-report', $file;
+ $cover_obj->{$new_file} = delete $cover_obj->{$file};
+ }
+ $params->{coverage} = to_json($cover_obj);
+
+ my ($file, $dir) = _dump_file($params, 'coverage', 'cover.json', $name, $args);
+ system($args->{dirs}{java} . ' -jar ' . $args->{dirs}{jar_dir} . "/yuitest-coverage-report.jar -o $dir --format lcov $file");
+ warn "Coverage Report for $name";
+ } else {
+ warn "Empty coverage Report";
+ }
+
+ fiddle_share($args,
+ sub {
+ my $obj = shift;
+ my $total_tests = scalar(@{$obj->{tests_to_run}});
+ for (my $i = 0; $i < @{$obj->{tests_to_run}}; $i++) {
+ my $test = $obj->{tests_to_run}[$i];
+ next unless ($test->{browser} eq $browser);
+ if ($test->{send_output}) {
+ _send_remote_output($test->{send_output}, "$name finished - it ", ($succeeded ? 'SUCCEEDED' : 'FAILED'), ' it took ', (time - $test->{running}), ' seconds');
+ _send_remote_output($test->{send_output}, (scalar(@{$obj->{tests_to_run}}) - 1) . " tests left...");
+ my $total_time = ($total_tests - 1) * (time - $test->{running});
+ $params->{browsers} ||= 1;
+ $total_time /= $params->{browsers};
+ _send_remote_output($test->{send_output}, "At that rate it'll take about $total_time seconds (" . ($total_time/60.) . " minutes) to be done done.");
+ }
+ splice @{$obj->{tests_to_run}}, $i, 1;
+ last;
+ }
+ }
+ );
+
+ $response->code( 200 );
+ $response->content('OK');
+}
+
+sub clear_tests {
+ my($args) = @_;
+
+ my $response = $args->{response};
+ fiddle_share($args,
+ sub {
+ shift->{tests_to_run} = [];
+ }
+ );
+
+ $response->code( 200 );
+ $response->content('OK');
+}
+
+###
+# NEED TO FLOCK!!!
+###
+sub _send_remote_output {
+ my($f) = shift;
+
+ my $line = join '', @_, "\n";
+
+ open(my $file, ">>$f") or (warn "Can't open remote send: $!\n", return);
+ _lock_file($file);
+ $file->print($line);
+ _unlock_file($file);
+
+ warn "send_remote_output: $line";
+}
+
+sub _send_to_client {
+ my($args) = shift;
+
+ my $response = $args->{response};
+ if (!$response->{already_sent_header}) {
+ $response->content_type ? 1 : $response->content_type("text/html");
+ unshift(@_, $response->{_headers}->as_string(), "\n");
+ $response->{already_sent_header}++;
+ }
+
+ my $text = join '', @_;
+
+ warn "To client ($ret): $text\n";
+ my $ret = eval { *STDOUT->syswrite(join '', @_, "\n"); };
+ if ($@) {
+ warn "Client went away: $@\n";
+ }
+
+ *STDOUT->flush();
+ if ($args->{fcgi}) {
+ $args->{fcgi}->Flush;
+ }
+
+ return $ret;
+}
+
+sub clear_results {
+ my($args) = @_;
+
+ my $response = $args->{response};
+ my $output_dir = $args->{dirs}{output_dir};
+
+ system("/bin/rm -rf $output_dir/*");
+ $response->code( 200 );
+ $response->content('OK');
+}
+
+sub startSelenium {
+ my($params, $url, $args, $num_tests) = @_;
+
+ my $response = $args->{response};
+
+ require WWW::Selenium;
+ my $selenium = WWW::Selenium->new(
+ host => $params->{sel_host},
+ port => 4444,
+ browser => $params->{browser},
+ browser_url => $url,
+ );
+
+ $selenium->start;
+ $selenium->open($url);
+
+ $selenium->wait_for_page_to_load(60000); # 60 seconds
+ my $max_wait_time = $TEST_TIME_THRESHOLD * $num_tests; # give test time to run once the page loads - so stick around
+ # While use IE, wait for a few seconds to let the test start
+ if ($params->{browser} =~ /^\*ie/) {
+ $max_wait_time -= sleep($TEST_RUN_CHECK_INTERVAL);
+ }
+
+ eval { $selenium->select_frame('run_tests'); };
+ if ($@) {
+ _send_to_client($args, "Got an exception when try to select the 'run_tests' frame.\n" . to_json({ error => $@ }));
+ return;
+ }
+
+ my $is_test_finished = 0;
+ do {
+ $max_wait_time -= sleep($REMOTE_CHECK_INTERVAL);
+ if ($params->{send_output}) {
+ if (-s $params->{send_output}) {
+ open(my $file, "+<" . $params->{send_output});
+ if ($file) {
+ flock($file, LOCK_EX); ## don't call _lock_file as it seeks to the end...
+ my @out = <$file>;
+ $file->truncate(0);
+ $file->close();
+ _send_to_client($args, @out);
+ }
+ }
+ }
+
+ # Verify if the test finished or not
+ eval {
+ $is_test_finished = $selenium->is_element_present('name=multiple_tests');
+ };
+ if ($@) {
+ # If catch any exception there, continue the work
+ if ($params->{send_output}) {
+ _send_to_client($args, "WARNING: Got an error when try to check if the test finished.\n" . to_json({ error => $@ }));
+ }
+ }
+ } while (!$is_test_finished && ($max_wait_time > 0));
+
+ my $base_dir = $args->{dirs}{output_dir};
+ my $map = {};
+ while (my $component = <$base_dir/*>) {
+ $component = basename($component);
+ my @test_files = <$base_dir/$component/*.xml>;
+ foreach (@test_files) {
+ if (failed_tests($_)) {
+ push @{$map->{FAILED}}, $component;
+ } else {
+ push @{$map->{SUCCEEDED}}, $component;
+ }
+ }
+ }
+ warn "DONE!";
+ warn Dumper($map);
+
+ if ($params->{send_output}) {
+ _send_to_client($args, to_json({ results => $map }));
+ }
+}
+
+sub run_test {
+ my($args) = @_;
+
+ my $response = $args->{response};
+ my $params = $args->{params};
+ my $cgi = $args->{cgi};
+ my $browser_key = _browser_key($args);
+
+ my @tests;
+ if ($params->{test}) {
+ push @tests, $params->{test};
+ } elsif ($params->{tests}) {
+ push @tests, split(/\s+/, $params->{tests});
+ }
+
+ $params->{send_output} = tmpnam() if ($params->{send_output});
+
+ # Tell a browser to fetch
+ my ($obj, $browsers) = fiddle_share($args,
+ sub {
+ my($obj) = @_;
+ my $browsers;
+
+ foreach my $test (@tests) {
+
+ my $url = URI->new($test);
+ if ($params->{sel_host}) {
+ my $base_url = 'http://' . $cgi->server_name;
+ my $url_to_hit = $base_url . '/jute/'; #/capture.html' #?test=' . $url_test; # . '&_one_shot=1';
+
+ # Normalize this so we can hand out tests to it later
+ # (we just gotta assume there's only 1 browser)
+ my $hostent = gethost($params->{sel_host});
+ my $sel_ip = inet_ntoa($hostent->addr);
+
+ push @{$obj->{tests_to_run}},
+ {
+ url => $url->as_string,
+ sel_url => $url_to_hit,
+ sel_host => $params->{sel_host},
+ sel_ip => $sel_ip,
+ sel_browser => $params->{browser},
+ running => 0,
+ send_output => $params->{send_output},
+ };
+
+ $browsers = $url_to_hit;
+ } else {
+ foreach my $browser (keys %{$obj->{browsers}}) {
+ # Don't add tests to any Selenium browsers - they go away...
+ next if ($browser->{is_selenium});
+
+ push @{$obj->{tests_to_run}},
+ {
+ url => $test,
+ browser => $browser,
+ running => 0,
+ send_output => $params->{send_output},
+ };
+ $browsers++;
+ }
+ }
+ }
+
+ return $browsers;
+ }
+ );
+
+ if ($browsers) {
+ $response->code( 200 );
+
+ if ($params->{sel_host}) {
+
+ if ($params->{send_output}) {
+ _send_to_client($args, sprintf("Opening %s on Selenium host %s...", $params->{browser}, $params->{sel_host}));
+ }
+
+ my $num_browsers = $params->{browsers} || 1;
+
+ my @pids;
+ while($num_browsers) {
+ my $pid = Fork();
+ next if (!defined($pid));
+ if (!$pid) {
+ startSelenium($params, $browsers, $args, scalar(@tests));
+ exit();
+ }
+
+ push @pids, $pid;
+ $num_browsers--;
+ }
+
+ do {
+ sleep($TEST_RUN_CHECK_INTERVAL);
+ my $wait = waitpid($pids[-1], &WNOHANG);
+ pop @pids if ($wait);
+ } while(@pids);
+ } else {
+ $response->content('Added ' . ($params->{test} || $params->{tests}) . ' to tests for ' . $browsers . ' browsers.');
+ }
+ } else {
+ warn "No browsers listening!\n";
+ $response->content('No browsers listening!! Test not added!');
+ $response->code( 500 );
+ }
+}
+
+sub get_test {
+ my($args) = @_;
+
+ my $response = $args->{response};
+
+ $response->content(to_json({}));
+
+ my ($obj, $url) = fiddle_share($args,
+ sub {
+ my $obj = shift;
+ my $browser = _browser_key($args);
+ my $browser_name = _browser_name($args);
+ $obj->{browsers}{$browser}{get_test} = time;
+
+# warn "Get test for $browser/$browser_name\n";
+ FIND_TEST: for (my $i = 0; $i < @{$obj->{tests_to_run}}; $i++) {
+ my $test = $obj->{tests_to_run}[$i];
+ if (($test->{browser} eq $browser) && $test->{running}) {
+ # um you're already running this test!
+ # must be something wrong with it - pop it
+ my $error = 'Skipping bad test: ' . $test->{url} . ': we thought it was running!';
+ warn "$error\n";
+ if ($test->{send_output}) {
+ _send_remote_output($test->{send_output}, $error);
+ }
+ splice @{$obj->{tests_to_run}}, $i, 1;
+ redo FIND_TEST;
+ }
+ # So either the browser matches OR it's a Selenium test
+ # so we match on remote IP
+ if (!$test->{browser}) {
+ # A Selenium host
+ $test->{browser} = $browser;
+ $obj->{browsers}{$browser}{is_selenium} = 1;
+ }
+ next unless ($test->{browser} eq $browser);
+# if ($test->{send_output}) {
+# _send_remote_output($test->{send_output}, "Going to run test ", $test->{url}, " in $browser_name/$browser");
+# }
+
+ $test->{running} = time;
+ return $test->{url};
+ }
+
+ return; # nothing for this guy to do...
+ }
+ );
+
+ if ($url) {
+ $response->content(to_json({ testLocation => $url }));
+ warn "Sending test url: $url";
+ } else {
+ # find all local tests
+ my $prefix = $args->{dirs}{test_dir};
+ my $new_prefix = join '/', $args->{dirs}{html_root}, $args->{dirs}{html_test_root};
+ my $local_test_files = $Yahoo::JUTE::Settings::test_files || '*.htm*';
+ my $full_find = $Yahoo::JUTE::Settings::full_find || "-not \\( -path '*/.svn/*' \\) -name '$local_test_files'";
+ my @test_files = `find $prefix $full_find -print`;
+ chomp @test_files;
+ my $data = [];
+ foreach (@test_files) {
+ s#$prefix#/$new_prefix#;
+ my @dirs = split '/', $_;
+ push @$data, { test_url => $_, test_name => join('/', @dirs[-2..-1]) };
+ }
+ use Data::Dumper;
+ warn "Found tests: " . Dumper($data) . "\n";
+ $response->content(to_json({ availableTests => $data }));
+ }
+
+ $response->content_type('application/x-json');
+ $response->code( 200 );
+}
+
+sub capture {
+ my($args) = @_;
+
+ my $add = $args->{get_p}->{_one_shot} ? '_one_shot=1' : '';
+ if ($args->{get_p}{test}) {
+ $args->{params}{test} = $args->{get_p}{test};
+ $add = '_one_shot=1';
+ run_test($args);
+ }
+ $args->{response}->code(302);
+ $args->{response}->content("/jute_docs/capture.html?$add");
+}
+
+sub status {
+ my($args) = @_;
+
+ _dump_obj($args);
+}
+
+sub prune_tests {
+ my ($args) = @_;
+
+ return fiddle_share($args,
+ sub {
+ my ($obj, $now) = (shift, time);
+ my $browser = _browser_key($args);
+ for (my $i = 0; $i < @{$obj->{tests_to_run}}; $i++) {
+ my $test = $obj->{tests_to_run}[$i];
+ my $time_started = $test->{running};
+ if ($time_started) {
+ if ($now - $time_started > $TEST_TIME_THRESHOLD) {
+ # take it out of ay tests it's supposed to be running
+ warn "Test running for longer than $TEST_TIME_THRESHOLD seconds! Killing it...";
+ warn "$now - $time_started = " . ($now - $time_started) . " > $TEST_TIME_THRESHOLD\n";
+ my $failed_test = splice @{$obj->{tests_to_run}}, $i, 1;
+
+ my $url = $failed_test->{url};
+ if ($failed_test->{send_output}) {
+ _send_remote_output($failed_test->{send_output}, "$url finished - it timed out - javascript error?");
+ }
+
+ # Use test file name as the NAME of this test (vs. component name from test itself)
+ my @parts = split m#/#, $url;
+ my $name = pop @parts;
+ $name =~ s/\..*$//;
+
+ my($filename, $pkgname) = make_sane_names(_browser_name($args));
+ my $err = $ERROR;
+ $err =~ s/BROWSER/$pkgname/;
+ $err =~ s/URL/$url/;
+ my $params = { results => $err, name => $name };
+ warn "Dumped error unit test file $name / $filename (from $url)\n";
+ _dump_file($params, 'results', $filename . '-test.xml', $name, $args);
+
+ if ($obj->{browsers}{$browser}) {
+ $obj->{browsers}{$browser}{heart_beat} = $now;
+ $obj->{browsers}{$browser}{get_test} = $now;
+ return 1;
+ }
+ }
+ } else {
+ # make sure browser is still requesting tests
+ if ($obj->{browsers}{$browser}) {
+ my $last_got_test = $obj->{browsers}{$browser}{get_test};
+ if ($args->{action} ne 'get_test' && ($now - $last_got_test > $TEST_TIME_THRESHOLD)) {
+ warn "Been too long since you've requested a test: $browser\nKicking iframe...";
+ return 1;
+ }
+ }
+ }
+ }
+
+ return;
+ }
+ );
+}
+
+sub prune_browsers {
+ my ($args) = @_;
+
+ fiddle_share($args,
+ sub {
+ my ($obj, $now) = (shift, time);
+ my $me = _browser_key($args);
+ if ($obj->{browsers} && ref $obj->{browsers} eq 'HASH') {
+ foreach my $browser (keys %{$obj->{browsers}}) {
+ next if ($browser eq $me);
+ warn "ME: $me\n";
+ warn "browser: $browser\n";
+ my $b_time = $obj->{browsers}{$browser}{heart_beat};
+ if ($now - $b_time > $BROWSER_TIME_THRESHOLD) {
+ warn "We lost $browser!\n";
+ warn "Time since we last saw him: " . ($now - $b_time) . "\n";
+ delete $obj->{browsers}{$browser};
+ # take it out of ay tests it's supposed to be running
+ for (my $i = 0; $i < @{$obj->{tests_to_run}}; $i++) {
+ my $test = $obj->{tests_to_run}[$i];
+ next unless ($test->{browser} eq $browser);
+ splice @{$obj->{tests_to_run}}, $i, 1;
+ redo;
+ }
+ }
+ }
+ }
+ }
+ );
+}
+
+sub prune {
+ my($args) = shift;
+
+ return if ($args->{action} eq 'status');
+
+ prune_browsers($args);
+ return prune_tests($args);
+}
+
+sub _browser_name {
+ my($args) = @_;
+ return join '---', $args->{env}{USER_AGENT}, $args->{env}{REMOTE_ADDR};
+}
+
+sub _browser_key {
+ my($args) = @_;
+
+ return $args->{session};
+}
+
+sub failed_tests {
+ my($file) = @_;
+ return `grep 'failures="[1-9]' $file`;
+}
+
+sub heart_beat {
+ my($args) = @_;
+
+ my $obj = fiddle_share($args,
+ sub {
+ my $obj = shift;
+ $obj->{browsers}{_browser_key($args)}{heart_beat} = time;
+ $obj->{browsers}{_browser_key($args)}{name} = _browser_name($args);
+ }
+ );
+
+ my $return;
+ my $base_dir = $args->{dirs}{output_dir};
+ while (my $component = <$base_dir/*>) {
+ $component = basename($component);
+ my @test_files = <$base_dir/$component/*.xml>;
+ my $test_results = [];
+ foreach (@test_files) {
+ if (failed_tests($_)) {
+ push @$test_results, { name => basename($_), failed => 1 }
+ } else {
+ push @$test_results, { name => basename($_), failed => 0 }
+ }
+ }
+ my $coverage = -d "$base_dir/$component/lcov-report";
+ $return->{current_results}{$component}{test_results} = $test_results;
+ $return->{current_results}{$component}{coverage} = $coverage;
+ }
+
+# warn "SHARE: -" . $args->{share}->fetch() . "-\n";
+ $return->{current_status} = from_json($args->{share}->fetch());
+
+ my $response = $args->{response};
+ $response->code( 200 );
+ $response->content(to_json($return, {utf8 => 1, pretty => 1}));
+ $response->content_type('text/plain');
+}
+
+sub pop {
+ my($args) = @_;
+
+ my $obj = fiddle_share($args,
+ sub {
+ shift @{shift->{tests_to_run}};
+ }
+ );
+
+ _dump_obj($args);
+}
+
+sub run_multiple {
+ my($args) = @_;
+
+ my @tests = split /;/, $args->{params}{test};
+ foreach my $test (@tests) {
+ $test =~ s/[[:^print:]]//g;
+ $args->{params}{test} = $test;
+ run_test($args);
+ }
+
+ $args->{response}->code(302);
+ $args->{response}->content("/jute_docs/run_tests.html");
+}
+
+sub _dump_file {
+ my($vars, $data_key, $def_file, $component, $args) = @_;
+
+ my $output_dir = $args->{dirs}{output_dir};
+ my $dir = join '/', $output_dir, (make_sane_names($component))[0];
+ my $file = $def_file;
+ my $data = $vars->{$data_key};
+
+ warn "Dumping $file to $dir";
+
+ system("mkdir -p $dir");
+ open(C, ">$dir/$file") || die "Error making $dir/$file: $!";
+ print C $data;
+ close(C);
+
+
+ system("chmod -R 777 $output_dir");
+ return ("$dir/$file", $dir);
+}
+
+sub _get_share {
+ my($args) = @_;
+
+ my $share = $args->{share};
+ $share->lock(LOCK_EX);
+ return from_json($share->fetch());
+}
+
+sub _release_share {
+ my($args, $val) = @_;
+
+ my $share = $args->{share};
+ eval { $share->store(to_json($val)) if ($val) };
+ warn "error to_json: $@\n" if ($@);
+ $share->unlock(LOCK_UN);
+}
+
+sub fiddle_share {
+ my($args, $handler) = @_;
+
+ my $obj = _get_share($args);
+ my $ret = eval { $handler->($obj) } if ($handler);
+ _release_share($args, $obj);
+
+ return ($obj, $ret);
+}
+
+# A pretty dump of the shared object
+sub _dump_obj {
+ my($args) = @_;
+
+ my $response = $args->{response};
+ $response->code( 200 );
+ $response->content(to_json(from_json($args->{share}->fetch()), {utf8 => 1, pretty => 1}));
+ $response->content_type('text/plain');
+}
+
+sub _lock_file {
+ my ($fh) = @_;
+ flock($fh, LOCK_EX) or warn "Cannot lock - $!\n";
+
+ # and, in case someone appended while we were waiting...
+ seek($fh, 0, SEEK_END) or warn "Cannot seek - $!\n";
+}
+
+sub _unlock_file {
+ my ($fh) = @_;
+ flock($fh, LOCK_UN) or warn "Cannot unlock - $!\n";
+}
+
+################################################################################
+# FROM Proc::Daemon....
+# Fork(): Retries to fork over 30 seconds if possible to fork at all and
+# if necessary.
+# Returns the child PID to the parent process and 0 to the child process.
+# If the fork is unsuccessful it C<warn>s and returns C<undef>.
+################################################################################
+sub Fork {
+ my $pid;
+ my $loop = 0;
+
+ FORK: {
+ if ( defined( $pid = fork ) ) {
+ return $pid;
+ }
+
+ # EAGAIN - fork cannot allocate sufficient memory to copy the parent's
+ # page tables and allocate a task structure for the child.
+ # ENOMEM - fork failed to allocate the necessary kernel structures
+ # because memory is tight.
+ # Last the loop after 30 seconds
+ if ( $loop < 6 && ( $! == EAGAIN() || $! == ENOMEM() ) ) {
+ $loop++; sleep 5; redo FORK;
+ }
+ }
+
+ warn "Can't fork: $!";
+
+ return undef;
+}
+
+1;
View
85 backend/Yahoo/JUTE/Settings.pm
@@ -0,0 +1,85 @@
+package Yahoo::JUTE::Settings;
+
+my $VERSION = '1.0';
+
+use vars qw($as_user $html_root_dir $doc_root $test_dir $output_dir $fcgi_processes $heartbeat_interval $test_wait_threshold $test_files $full_find $port);
+
+####
+## Name of user to run the JUTE FCGI processes as
+## Bascially this username needs to have write permissions into $output_dir
+## DEFAULT 'nobody'
+####
+$as_user = 'nobody';
+
+####
+## Your document root directory
+## DEFAULT '/var/www'
+####
+$doc_root = '/var/www';
+
+####
+## Name of directory under $doc_root where yer code is
+## This is typically a symlink into your local code repo in your home dir
+## DEFAULT 'jutebase'
+####
+$html_root_dir = 'jutebase';
+
+####
+## Name of directory under $html_root_dir where your test files are
+## DEFAULT 'test'
+####
+$test_dir = 'test';
+
+####
+## Name of directory under $html_root_dir where test results and code coverage
+## output goes
+## DEFAULT 'output'
+####
+$output_dir = 'output';
+
+####
+## Number of FastCGI processes to start
+## DEFAULT 5
+####
+$fcgi_processes = 5;
+
+####
+## Number of seconds to wait before timing out a captured browser
+## DEFAULT 20
+####
+$heartbeat_interval = 20;
+
+####
+## How long we're willing to wait for a Selenium browser to connect
+## to us
+## DEFAULT 60
+###
+$test_wait_threshold = 60;
+
+####
+## Basic find expression to find your HTML test files
+## looks for these under $test_dir
+## DEFAULT: *.html
+####
+$test_files = '';
+
+###
+# Arguments to 'find' for finer-grained control to find your test files
+# looks for these under $test_dir
+# DEFAULT: -not \\( -path '*/.svn/*' \\) -name '$test_files'
+###
+$full_find = '';
+
+###
+# IF you're using the JUTE standalone webserver, port to listen on
+# DEFAULT: 8080
+###
+$port = 8080;
+
+##
+# Directory where yuitest-coverage.jar and yuitest-coverage-report.jar live
+# DEFAULT: /usr/local/lib/
+###
+$jar_dir = '/usr/local/lib/';
+
+1;
View
142 backend/capture.html
@@ -0,0 +1,142 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html lang="en">
+
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
+ <meta http-equiv="pragma" content="no-cache">
+ <title>JUTE - Javascript Unit Test Environment</title>
+ <style type="text/css">
+ body {
+ font-family: Verdana, Arial, Helvetica;
+ }
+ #results {
+ position: absolute;
+ left: 950px;
+ top: 14px;
+ width: 300px;
+ height:500px;
+ overflow: auto;
+ padding: 0px;
+ border: 2px solid #511061;
+ border-top: 10px solid #511061;
+ }
+ #results h4 {
+ color: #FFF;
+ background-color: #511061;
+ padding-bottom: 5px;
+ margin: 0px;
+ width: 100%;
+ }
+ #results li {
+ font-size: 70%;
+ padding: 5px 0px 0px 0px;
+ }
+ #results p {
+ font-size: 70%;
+ padding: 5px 0px 0px 24px;
+ }
+ #list {
+ position: absolute;
+ left: 8px;
+ top: 14px;
+ width: 300px;
+ height: 500px;
+ border: 2px solid #511061;
+ padding: 0px;
+ overflow: auto;
+ border-top: 10px solid #511061;
+ }
+ #list li {
+ font-size: 80%;
+ }
+ #list h4 {
+ color: #FFF;
+ background-color: #511061;
+ margin: 0px;
+ padding-bottom: 5px;
+ width: 100%;
+ }
+ #run_tests {
+ width:600px;
+ height: 472px;
+ border: 2px solid #511061;
+ }
+ #grabber {
+ border: 0px;
+ padding: 0px;
+ height: 0px;
+ width: 0px;
+ }
+ #buttons {
+ position: absolute;
+ top: 17px;
+ left: 420px;
+ }
+ #tests {
+ position: absolute;
+ top: 50px;
+ left: 330px;
+ width: 600px;
+ height: 500px;
+ }
+ #pagetitle {
+ position: absolute;
+ top: 20px;
+ left: 320px;
+ }
+ #pagetitle h1 {
+ margin: 0px 5px 10px 10px;
+ color: #511061;
+ font-weight: bold;
+ font-size: 100%;
+ line-break: none;
+ }
+ .divider {
+ width: 1246px;
+ border-top: 6px solid #511061;
+ }
+ label, button, input {
+ font-size: 70%;
+ }
+
+ </style>
+</head>
+
+<body id="body">
+ <div class="divider"></div>
+
+ <div id="pagetitle"><h1>JUTE</h1></div>
+ <div id="buttons">
+ <label>Load Page:</label><input type="text" id="load_file" size="15" name="load" /><button id="load_button">Load It</button>
+ <!-- <button id="get_coverage_button">Get Coverage!</button> -->
+ <button id="kick_frame">Kick Lower Frame</button>
+ <button id="clear_tests">Clear Tests</button>
+ <button id="clear_results">Clear Results</button>
+ </div>
+
+ <div id="results"></div>
+ <div id="list"></div>
+
+ <div id="tests">
+
+ <iframe src="" name="run_tests" id="run_tests" frameborder="0">Erg, you needs iframes</iframe>
+
+ <iframe id="grabber" src="" width="0" height="0"></iframe>
+
+ <script src="http://yui.yahooapis.com/3.3.0/build/yui/yui-min.js"></script>
+ <script>
+ var qs = window.location.search.substring(1);
+ document.getElementById('run_tests').src = '/jute_docs/run_tests.html?' + qs;
+
+ YUI({
+ gallery: 'gallery-2011.06.22-20-13'
+ }).use('gallery-jute', function(Y) {
+ console.log('Y UNIT TEST GOT GALlERY JUTE');
+ console.log(Y.UnitTest);
+ Y.UnitTest.heartBeat();
+ });
+ </script>
+ </div>
+</body>
+</html>
+
View
366 backend/jute.fcgi
@@ -0,0 +1,366 @@
+#!/usr/bin/perl
+
+my $VERSION = '1.0';
+$FASTCGI = $0 =~ /\.fcgi/ ? 1 : 0;
+my $FASTCGI = 0;
+
+use 5.008;
+use strict;
+use IO::File;
+use Cwd;
+use CGI;
+use File::Basename;
+use JSON;
+use URI;
+use IPC::ShareLite;
+use HTTP::Response;
+use Fcntl qw(:flock);
+use Yahoo::JUTE::Actions;
+use Yahoo::JUTE::Settings;
+use URI::Escape;
+use POSIX;
+use Data::UUID;
+use HTTP::Status;
+use Socket;
+use LWP::UserAgent;
+
+my $ug = new Data::UUID;
+my $me = $JUTE::Settings::as_user || $ENV{USER};
+
+# set group and user
+my ($uid, $gid) = (getpwnam($me))[2,3];
+POSIX::setgid($gid);
+POSIX::setuid($uid);
+
+warn "Running as " . getpwuid($uid) . '/' . getgrgid($gid) . "\n";
+
+# Run only 1 of me
+open SELF, $0 or die "This is weird\n";
+flock SELF, LOCK_EX | LOCK_NB or exit; # already running
+
+$| = 1;
+
+my $html_root = $Yahoo::JUTE::Settings::html_root_dir || 'jutebase';
+my $root = $Yahoo::JUTE::Settings::doc_root . "/$html_root";
+$root .= '/' unless $root =~ m/\/$/;
+
+my $dirs = {};
+$dirs->{root} = $root;
+$dirs->{html_root} = $html_root;
+$dirs->{html_test_root} = $Yahoo::JUTE::Settings::test_dir || 'test';
+$dirs->{test_dir} = $root . $dirs->{html_test_root};
+$dirs->{jar_dir} = $Yahoo::JUTE::Settings::jar_dir;
+$dirs->{html_output_root} = $Yahoo::JUTE::Settings::output_dir || 'output';
+$dirs->{output_dir} = $root . $dirs->{html_output_root};
+
+system("mkdir -p " . $dirs->{output_dir}) unless (-d $dirs->{output_dir});
+
+my $java;
+if (-d $ENV{JAVA_HOME}) {
+ $java = join '/', $ENV{JAVA_HOME}, 'bin', 'java';
+}
+if (!$java || !-x $java) {
+ $java = `which java`;
+ chomp($java);
+}
+
+die "Cannot find 'java' executable!! Either set \$JAVA_HOME or \$PATH or install yjava_jdk (linux) or vespa_jdk (bsd)!!!\n\n" unless (-f $java && -x $java);
+
+$dirs->{java} = $java;
+
+my $share = IPC::ShareLite->new(
+ -key => 1971,
+ -create => 'yes',
+ -destroy => 'no'
+) or die $!;
+
+# prime pump
+$share->store(to_json({ tests_to_run => [] }));
+
+my($socket, $request, $daemon, $proc_manager);
+if ($FASTCGI) {
+ require FCGI;
+ require FCGI::ProcManager;
+
+ # Start FCGI party
+ umask(0);
+ $socket = FCGI::OpenSocket("/tmp/jute.socket", 5);
+ $request = FCGI::Request(\*STDIN, \*STDOUT, \*STDERR, \%ENV, $socket, &FCGI::FAIL_ACCEPT_ON_INTR);
+
+ daemon_fork();
+ $proc_manager = FCGI::ProcManager->new(
+ {
+ n_processes => $Yahoo::JUTE::Settings::fcgi_processes || 5,
+ pid_fname => '/tmp/jute.pid',
+ }
+ );
+
+ # detach *before* the ProcManager inits
+ daemon_detach();
+
+ $proc_manager->pm_manage();
+
+ # Give each child its own RNG state.
+ srand;
+ do_fcgi_request_loop();
+} else {
+ require HTTP::Daemon;
+ $daemon = HTTP::Daemon->new(
+ LocalPort => $Yahoo::JUTE::Settings::port || 8080,
+ ReuseAddr => 1,
+ );
+ do_server_request_loop();
+}
+
+sub do_server_request_loop() {
+ while (my($c, $remote_addr) = $daemon->accept) {
+
+ warn "GOT ACCEPT\n";
+ while (my $r = $c->get_request) {
+ warn "GOT REQUEST\n";
+ my $path = $r->uri->path;
+ $path =~ s#^/jute/##;
+ $path ||= '_capture.html';
+ $r->uri->path($path);
+
+ warn "GOT REQUEST FOR $path\n";
+
+ $ENV{CONTENT_TYPE} = join('; ', $r->content_type) || '';
+
+ my ($port,$iaddr) = unpack_sockaddr_in($remote_addr);
+ $ENV{REMOTE_ADDR} = inet_ntoa($iaddr);
+
+ $ENV{USER_AGENT} = $r->header('User-Agent');
+
+ $ENV{SESSION_COOKIE} = (split /=/, $r->header('Cookie'))[1];
+
+ local *main::STDIN = $c;
+ my $q = CGI->new();
+
+ local *main::STDOUT = $c;
+ my $response = handleConnection($path, $q, \%ENV);
+ $c->send_response($response);
+ warn "SENT RESPONSE\n";
+# my $output_buffer = handle_response($response);
+# warn "SENDING: \n$output_buffer\n";
+# $c->syswrite($output_buffer);
+ }
+ $c->close;
+ undef($c);
+ }
+}
+
+sub do_fcgi_request_loop() {
+ while( $request->Accept() >= 0 ) {
+ $proc_manager->pm_pre_dispatch();
+
+ my $q = CGI->new;
+
+ # use Data::Dumper;
+ # warn Dumper(\%ENV);
+
+ $ENV{REQUEST_URI} =~ s#^/jute/##;
+ my $url = $ENV{REQUEST_URI} || '_capture.html';
+
+ my $response = handleConnection($url, $q, \%ENV);
+ my $output_buffer = handle_response($response);
+
+ *STDOUT->syswrite($output_buffer);
+ CGI->initialize_globals();
+
+ $proc_manager->pm_post_dispatch();
+ }
+}
+
+sub handle_response {
+ my($response) = @_;
+ my $output_buffer;
+ if ($response->is_redirect) {
+ warn "Redirect to " . $response->content . "\n";
+ $output_buffer = 'Location: ' . $response->content() . "\n\n";
+ } else {
+ unless ($response->{already_sent_header}) {
+# warn "b4 content type: " . $response->content_type . "\n";
+ $response->content_type ? 1 : $response->content_type("text/html");
+# warn "content type: " . $response->content_type . "\n";
+
+ $output_buffer = "Status: " . $response->code() . "\n";
+ $output_buffer .= $response->{_headers}->as_string() . "\n";
+ }
+
+ if (ref $response->content() eq 'CODE') {
+ while (my $buf = $response->content()->()) {
+ $output_buffer .= $buf;
+ }
+ } else {
+ $output_buffer .= $response->content();
+ }
+ }
+
+ return $output_buffer;
+}
+
+sub handleConnection {
+ my($url, $q, $env) = @_;
+
+ my $response = new HTTP::Response(404, undef, undef, "404 - Not found.");
+
+ my $session = $q->cookie('session') || $env->{SESSION_COOKIE};
+ if (!$session) {
+# warn "CREATE NEW SESSION\n";
+ $session = $ug->create_str(); # Guaranteed unique until 3400CE - Woo!!
+ } else {
+# warn "reusing old session!\n";
+ }
+
+ $response->header(Set_Cookie => $q->cookie(-name=>'session', -value=>$session));
+
+ $url =~ s/\?(.*)$//;
+ my $get_p = CGI->new($1)->Vars || '';
+
+ # Internal action
+ $url =~ s#^_## if ($url !~ /\./);
+
+ my $args = {
+ params => $q->Vars || {},
+ get_p => $get_p,
+ response => $response,
+ share => $share,
+ root => $root,
+ action => $url,
+ env => $env,
+ dirs => $dirs,
+ fcgi => $request,
+ session => $session,
+ cgi => $q,
+ };
+
+ # clear out any cruft
+ if (Yahoo::JUTE::Actions::prune($args)) {
+ warn "redirect!\n";
+ $response->code( 200 );
+ $response->content_type('text/plain');
+ $response->content(to_json({ redirect_run_tests => '/jute_docs/run_tests.html' }));
+ } elsif (Yahoo::JUTE::Actions->can($url)) {
+ eval {
+ no strict 'refs';
+ "Yahoo::JUTE::Actions::$url"->($args);
+ };
+ if ($@) {
+ $response->code(500);
+ $response->content("<h1>Error: $@</h1>");
+ }
+ } else {
+ return fetch($url, $q, $response, $env);
+ }
+
+ return $response;
+}
+
+sub fetch {
+ my($url, $q, $response, $env) = @_;
+
+# warn "FETCH: $url\n";
+
+ # for JS stuff I may coverage
+ $url =~ s#^/$html_root/##;
+
+ my $obj = from_json($share->fetch());
+
+ # do coverage IF this is a coverage-able file AND Referer wants us to do coverage
+ my $full_path_file;
+ my $qp = $q->Vars;
+ my $ref_qp = CGI->new(URI->new($env->{HTTP_REFERER})->query)->Vars;
+ if ($qp->{coverage} && $url =~ /\.js$/ && $ref_qp->{do_coverage}) {
+ # coverage-zie this file first
+ my $cover_file = "${root}$url";
+ $full_path_file = '/tmp/' . basename($url);
+ chmod 0777, $full_path_file;
+ warn "Generating coverage file $full_path_file for $cover_file...";
+ system($dirs->{java} . ' -jar ' . $dirs->{jar_dir} . "/yuitest-coverage.jar -o $full_path_file $cover_file");
+ }
+
+ # a local meta-file
+ if ($url =~ s/^_//) {
+ $full_path_file = $Yahoo::JUTE::Settings::doc_root . "/jute_docs/$url";
+ }
+
+ if ($url =~ m#/jute_docs/#) {
+ $full_path_file = $Yahoo::JUTE::Settings::doc_root . $url;
+ }
+
+ if ($url =~ s#/jute_loader/##) {
+ warn "MOCK: $url\n";
+ # is it a standard YUI3 module??
+ my $yui3_base = 'http://yui.yahooapis.com/3.3.0/build/';
+ my $ua = LWP::UserAgent->new;
+ warn "Getting $yui3_base$url\n";
+ my $yui3_response = $ua->get($yui3_base . $url);
+
+ if ($yui3_response->is_success) {
+ # A core yui3 object!
+ $response->content($yui3_response->content);
+ $response->header(Content_Type => $yui3_response->content_type);
+ $response->header(Content_Length => $yui3_response->content_length);
+ $response->code(200);
+ return $response;
+ } else {
+ die $yui3_response->status_line . "\n";
+ }
+ }
+
+
+ warn "URL: $url\n";
+
+ my $local_file = $full_path_file || "${root}$url";
+ warn "Local file: $local_file";
+ my $file = IO::File->new("< $local_file");
+ if (defined $file) {
+ $response->code(200);
+ binmode $file;
+ my $buf = sysread($file, my ($buf), 16*1024); # No error checking ???
+ $response->content($buf);
+ $response->header(Content_Length => -s $file);
+ if ($local_file =~ /\.js$/) {
+ $response->content_type('application/javascript');
+ }
+
+# my $size = -s $file;
+#
+# my ($startrange, $endrange) = (0,$size-1);
+# if (defined $env->{HTTP_RANGE}
+# and $env->{HTTP_RANGE} =~ /bytes\s*=\s*(\d+)-(\d+)?/) {
+# $response->code(206);
+# ($startrange,$endrange) = ($1,$2 || $endrange);
+# };
+# $file->seek($startrange,0);
+#
+# $response->header(Content_Length => $endrange-$startrange);
+# $response->header(Content_Range => "bytes $startrange-$endrange/$size");
+# $response->content(
+# sub {
+# sysread($file, my ($buf), 16*1024); # No error checking ???
+# return $buf;
+# }
+# );
+ }
+
+ return $response;
+}
+
+if ($FASTCGI) {
+ FCGI::CloseSocket( $socket );
+}
+
+sub daemon_fork {
+ fork && exit;
+}
+
+sub daemon_detach {
+ print "FastCGI daemon started (pid $$)\n";
+ open STDIN, "+</dev/null" or die $!;
+ open STDOUT, ">&STDIN" or die $!;
+ open STDERR, ">&STDIN" or die $!;
+ POSIX::setsid();
+}
+
View
34 backend/jute.yapache.conf
@@ -0,0 +1,34 @@
+# ########
+# Apache 1.x JUTE FastCGI Setup
+# #######
+#
+
+# Setup FCGI
+AddType application/x-httpd-fcgi .fcgi
+AddHandler fastcgi-script fcgi
+
+# External so can run as user
+FastCgiExternalServer /tmp/jute.fcgi -flush -socket /tmp/jute.socket -idle-timeout 1000 -pass-header HTTP_REFERER,HTTP_USER_AGENT
+
+# Find anything that needs coverage & pass it on to FCGI
+# Anything w/coverage=1 in query string
+# AND doesn't already have 'jute' in URI
+# AND is a JS file
+RewriteEngine On
+RewriteCond %{QUERY_STRING} coverage=1
+RewriteCond %{REQUEST_URI} !^/jute/
+RewriteCond %{REQUEST_URI} .js$
+RewriteRule ^(.*)$ /tmp/jute.fcgi$1?coverage=1 [L]
+
+# Get /jute URLs that don't end in '/'
+RewriteCond %{REQUEST_URI} /jute$
+RewriteRule ^(.*)$ /jute/ [L,R=302]
+
+# anything under 'jute' goes to FCGI too
+ScriptAliasMatch ^(/jute/.*)$ /tmp/jute.fcgi$1
+
+# Anyone can join the fun
+<Directory /tmp>
+ Order allow,deny
+ allow from all
+</Directory>
View
52 backend/run_tests.html
@@ -0,0 +1,52 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html lang="en">
+
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
+ <style type="text/css">
+body {
+ font-family: Verdana, Arial, Helvetica;
+ margin: 0px;
+}
+h2 {
+ color: #FFF;
+ background-color: #511061;
+ margin: 0px 0px 5px 0px;
+ font-size: 85%;
+ width: 100%;
+ padding: 10px 0px 10px 0px;
+ }
+table {
+ background-color: #e4e4e4;
+ color: #000;
+}
+th {
+ text-align:left;
+ font-weight: bold;
+ font-size: 80%;
+ background-color: #FFF;
+}
+td {
+ text-align:left;
+ font-weight: normal;
+ font-size: 70%;
+ background-color: #FFF;
+}
+
+ </style>
+
+</head>
+
+<body>
+ <div id="content">Don't mind me - waiting around to run tests...</div>
+ <script src="http://yui.yahooapis.com/3.3.0/build/yui/yui-min.js"></script>
+ <script>
+ YUI({
+ gallery: 'gallery-2011.06.22-20-13'
+ }).use('gallery-jute', function(Y) {
+ Y.UnitTest.waitLoop();
+ });
+ </script>
+</body>
+</html>
+
View
108 backend/submit_test.pl
@@ -0,0 +1,108 @@
+#!/usr/local/bin/perl
+
+use Getopt::Long;
+use Data::Dumper;
+use HTTP::Request::Common;
+use LWP::UserAgent;
+my $ua = LWP::UserAgent->new(timeout => 30*60);
+
+my(@tests, $sel_host, $sel_browser, $send_output, $v8, $browsers);
+GetOptions(
+ "test=s" => \@tests,
+ "sel_host=s" => \$sel_host,
+ "sel_browser=s" => \$sel_browser,
+ "send_output" => \$send_output,
+ "v8" => \$v8,
+ "browsers=i" => \$browsers,
+);
+
+my $args = {};
+my $test = shift;
+
+if (@tests) {
+ my $run_tests = [];
+ foreach $test (@tests) {
+ if ($test eq '-') {
+ my @stdin_tests = <STDIN>;
+ foreach $test (@stdin_tests) {
+ chomp $test;
+ $test = fix_test($test);
+ push @$run_tests, $test;
+ }
+ } else {
+ $test = fix_test($test);
+ push @$run_tests, $test;
+ }
+ }
+ $args = { tests => "@$run_tests" };
+} elsif ($test) {
+ $sel_host = shift;
+ $sel_browser = shift;
+
+ $test = fix_test($test);
+ $args = { test => $test };
+} else {
+ die "ERROR: Must specify a test!!\n";
+}
+
+if ($sel_host) {
+ $sel_browser ||= '*firefox';
+ $args->{sel_host} = $sel_host;
+ $args->{browser} = $sel_browser;
+ $args->{browsers} = $browsers;
+}
+
+if ($v8) {
+ foreach(@$run_tests, $test) {
+ my($coverage) = $_ =~ s/\?do_coverage=1$//;
+ if (-x '/home/y/bin/jute_v8.js') {
+ print STDERR "ERROR: You must install the 'jute_v8' package to run V8 tests!!\n";
+ exit(1);
+ }
+ system("/home/y/bin/jute_v8.js $_ $coverage");
+ }
+ exit;
+}
+
+$args->{send_output} = $send_output;
+
+my $jute_server = '$(jute_server)' || 'localhost';
+
+print "Submitting: " . Dumper($args) . "\n";
+#my $response = $ua->post("http://$jute_server/jute/_run_test", $args);
+my $bytes_received = 0;
+my $response = $ua->request(POST("http://$jute_server/jute/_run_test", $args),
+ sub {
+ my($chunk, $res) = @_;
+ $bytes_received += length($chunk);
+ unless (defined $expected_length) {
+ $expected_length = $res->content_length || 0;
+ }
+ if ($expected_length) {
+# printf STDERR "%d%% - ", 100 * $bytes_received / $expected_length;
+ }
+# print STDERR "$bytes_received bytes received\n";
+
+ # XXX Should really do something with the chunk itself
+ # print $chunk;
+ print "The chunk is:\n" . $chunk;
+ }
+);
+print "The response code is:\n" . $response->code . "\n";
+print "The response decoded content is:\n" . $response->decoded_content . "\n";
+exit if ($response->is_success);
+die "ERROR: Failed to submit test\n";
+
+sub fix_test {
+ my($test) = @_;
+
+ return $test if ($v8);
+
+ $test =~ s#^\.\.#/$(html_root)#;
+ if ($test !~ m#^/#) {
+ $test = '/$(html_root)/$(test_dir)/' . $test;
+ }
+
+ return $test;
+}
+
View
37 backend/wait_for_tests.pl
@@ -0,0 +1,37 @@
+#!/usr/local/bin/perl
+
+use LWP::UserAgent;
+use JSON;
+use Sys::Hostname;
+
+my $ua = LWP::UserAgent->new;
+my $total_time;
+my $MAX_WAIT = '$(total_wait_threshold)' * 60; # give up after 10 mins
+
+my $host = '$(jute_server)' || hostname;
+
+print "Connect to http://$host/jute/ to run tests...\n";
+
+my $verbose = shift;
+while(1) {
+
+ my $response = $ua->get("http://$host/jute/_status");
+
+ if ($response->is_success) {
+ my $obj = from_json($response->decoded_content);
+ die "No browsers: " . $response->decoded_content . "\n" if (!$obj->{browsers} || !%{$obj->{browsers}});
+ my $tests = $obj->{tests_to_run};
+ local($") = ", ";
+ if (@$tests) {
+ print "Waiting for " . $tests->[0]{url} . "...\n" if ($verbose);
+ } else {
+ exit;
+ }
+ }
+ else {
+ die $response->decoded_content;
+ }
+} continue {
+ $total_time += sleep(10);
+ die "Timed out waiting for tests to finish\n" if ($total_time > $MAX_WAIT);
+}
View
617 js/README
@@ -0,0 +1,617 @@
+Table of Contents
+JUTE...................................................................................................................................................................1
+ Abstract...............................................................................................................................................................2
+ 3.1.1+ of YUI3........................................................................................................................................2
+ yapache 1.x (maybe 2.x works - have not tested) for Selenium and Capture modes.............................2
+NodeJS .2 for v8 mode............................................................................................................................2
+ Super Quick Start..............................................................................................................................................3
+ Variable Setup.........................................................................................................................................3
+ Install JUTE............................................................................................................................................3
+ Include JUTE in your HTML.................................................................................................................3
+ Require JUTE module in your TEST Javascript.....................................................................................3
+ Connect to JUTE.....................................................................................................................................3
+ Click on a test & run it!..........................................................................................................................4
+ In-Depth Start....................................................................................................................................................5
+ The JUTE User.......................................................................................................................................5
+ The jutebase symlink..............................................................................................................................5
+JUTE output directory.............................................................................................................................5
+JUTE test directory.................................................................................................................................5
+ Starting and Stopping JUTE...................................................................................................................6
+ yapache...................................................................................................................................................6
+JUTE Backends..................................................................................................................................................7
+ Capture....................................................................................................................................................7
+ Selenium.................................................................................................................................................7
+ V8/NodeJS..............................................................................................................................................7
+ Using JUTE.........................................................................................................................................................8
+WebUI.....................................................................................................................................................8
+JUTE UI Status.................................................................................................................................8
+ Browsers....................................................................................................................................8
+ Tests...........................................................................................................................................8
+ Test Files..........................................................................................................................................8
+ Running a Single Test................................................................................................................8
+ Running Multiple Tests.............................................................................................................9
+ Kick Lower Frame.....................................................................................................................9
+ Clear Tests.................................................................................................................................9
+ Clear Results..............................................................................................................................9
+ Results..............................................................................................................................................9
+ Command Line.......................................................................................................................................9
+ /home/y/bin/submit_test.pl...............................................................................................................9
+ Submitting Tests........................................................................................................................9
+ One Test............................................................................................................................10
+ Multiple Tests....................................................................................................................10
+ Running tests through Selenium..............................................................................................10
+ Parallelization....................................................................................................................10
+ Running tests through V8........................................................................................................10
+ Specifying code coverage........................................................................................................10
+ wait_for_tests.pl....................................................................................................................................11
+JUTE Output....................................................................................................................................................12
+ Test Results...........................................................................................................................................12
+ Coverage Output...................................................................................................................................12
+iTable of Contents
+ Writing Unit Tests for JUTE..........................................................................................................................13
+ YUI3 'test' module................................................................................................................................13
+ Code Requirements...............................................................................................................................13
+ HTML requirements.......................................................................................................................13
+ Javascript Requirements.................................................................................................................14
+JUTE V8...........................................................................................................................................................16
+ Installation............................................................................................................................................16
+ Configuration........................................................................................................................................16
+ HTML_ROOT................................................................................................................................16
+ TEST_ROOT..................................................................................................................................16
+ OUTPUT_ROOT...........................................................................................................................16
+ Running.................................................................................................................................................16
+ Results...................................................................................................................................................17
+ V8 Caveats............................................................................................................................................17
+ Debugging.............................................................................................................................................17
+ Best Practices....................................................................................................................................................18
+ Directory Setup.....................................................................................................................................18
+ Test File Setup......................................................................................................................................18
+ Developer Environment........................................................................................................................18
+ Developer Build Environment..............................................................................................................18
+ A Captured Browser.......................................................................................................................18
+ V8...................................................................................................................................................18
+ Hudson Build Environment..................................................................................................................19
+Yahoo Continuous Integration Standards.....................................................................................................20
+Yahoo CI Unit Test Standards........................................................................................................................21
+ Build/Hudson Integration...............................................................................................................................22
+ Running Tests.......................................................................................................................................22
+ Local...............................................................................................................................................22
+ Captured Browser(s)................................................................................................................22
+ Selenium..................................................................................................................................22
+ V8.............................................................................................................................................22
+ Hudson............................................................................................................................................22
+ Captured Browsers and Selenium............................................................................................22
+ V8.............................................................................................................................................22
+ Viewing Test Results............................................................................................................................23
+ Output Directory.............................................................................................................................23
+ Browser..........................................................................................................................................23
+ Hudson............................................................................................................................................23
+ Test Results..............................................................................................................................23
+ Code Coverage.........................................................................................................................23
+iiJUTE
+Javascript Unit Testing Environment (JUTE)
+ JUTE 1Abstract
+JUTE allows unobtrusive JavaScript YUI3 unit testing, code coverage, and yslow. Command line and
+web-based interfaces make JUTE easy to integrate with Hudson, developers, and even (gasp!) managers.
+There are 3 backends available to test your code: Selenium, Capture Mode, and V8.
+Requirements
+3.1.1+ of YUI3
+yapache 1.x (maybe 2.x works - have not tested) for
+Selenium and Capture modes
+NodeJS .2 for v8 mode
+ Abstract 2Super Quick Start
+Variable Setup
+JUTE assumes a symlink named '/home/y/share/htdocs/jutebase' to your real DOCUMENT_ROOT. For
+Mail/Neo that symlink points back into our home directories but that doesn't have to be the case. However
+whichever user JUTE runs as (which you specify, I'll show you how in a moment) needs to be able to write
+files in a sub-directory under your DOCUMENT_ROOT - by default in directory named 'output'. Note all of
+this can be changed via yinst variables but the simplest case is:
+* symlink /home/y/share/htdocs/jutebase to your DOCUMENT_ROOT (can/should? be somewhere in your
+home directory where you've checked out your subversion tree)
+* All of your test files live in DOCUMENT_ROOT/test
+* All of JUTE's output will go into DOCUMENT_ROOT/output
+Install JUTE
+% yinst install -b test jute
+Include JUTE in your HTML
+In your HTML files that you load into your browser to run your unit tests add another script tag BEFORE you
+load in your JavaScript file that contains your tests:
+Require JUTE module in your TEST Javascript
+Along with YUI3's 'test' module you must include the 'jute' module like:
+YUI({
+ logInclude: { TestRunner: true }
+}).use('test', 'jute', ..., function(Y) {
+....
+;({
+Connect to JUTE
+First, if you haven't done so, start JUTE:
+% yinst start jute
+Then point your browser to:
+http://<host where jute is running>/jute/
+Note: In case jute has problems starting, try
+sudo rm -rf /tmp/jute.socket
+ Super Quick Start 3Click on a test & run it!
+ Click on a test & run it! 4In-Depth Start
+The JUTE User
+JUTE by default run as the user who yinst installs the JUTE package. JUTE needs to run as a 'real' user to
+write files (unit test results and optionally coverage information). For developers this real user is YOU. For
+Hudson this user should be the same user that Hudson runs as when doing your build. This will all be handled
+automatically when that user installs the 'jute' package.
+If that doesn't work for you, you can force the user JUTE runs as using a yinst variable:
+% yinst set jute.as_user=<user you want JUTE to run as>
+% yinst restart jute
+The jutebase symlink
+JUTE expects your PROJECT ROOT to be available to it at /home/y/share/htdocs/jutebase. This path is
+typically a symlink to your real PROJECT ROOT. For developers this is typically somewhere in their home
+directory. For Hudson build this typically is either in the Hudson user's home directory or somewhere in
+/home/y/share/htdocs/<SOMEWHERE>
+Regardless create a symlink to that spot:
+% ln -s PROJECT_ROOT /home/y/share/htdocs/jutebase
+If for whatever zany reason you do not like the directory name 'jutebase' or your PROJECT ROOT is already
+in /home/y/share/htdocs and you don't want to symlink you can change it:
+% yinst set jute.html_root=<SOMETHING_ELSE>
+Now JUTE will use /home/y/share/htdocs/<SOMETHING_ELSE> as your PROJECT ROOT. Note this
+PROJECT ROOT directory is used as the BASE directory for the OUTPUT and TEST directories explained
+below.
+You must restart JUTE after changing this setting.
+JUTE output directory
+JUTE writes unit test results and optionally coverage information into an output directory. By default this
+output directory is BASE/output. Ensure the user JUTE runs as can write to this directory! You can change
+the directory name by:
+% yinst set jute.output_dir=&lt;SOMETHING_ELSE&gt;
+Now it will put output files into BASE/<SOMETHING_ELSE>
+You must restart JUTE after changing this setting.
+JUTE test directory
+By default JUTE looks in BASE/test for test HTML files. This can be changed:
+% yinst set jute.test_dir=<SOMETHING_ELSE>
+ In-Depth Start 5Now JUTE will look for tests in BASE/<SOMETHING_ELSE>
+You must restart JUTE after changing this setting.
+Starting and Stopping JUTE
+JUTE is a standalone ('external' in FCGI parlance) fastcgi server that listens on a UNIX socket. By default
+JUTE will automatically start itself when installed. It can be started, stopped, and restarted:
+% yinst start jute
+% yinst stop jute
+% yinst restart jute
+The UNIX socket is located at:
+srwxrwxrwx 1 trostler users 0 Mar 10 13:05 /tmp/jute.socket
+Ensure this socket is owned by who you expect JUTE to be running as. If it's owned by root or someone else
+unexpected and you're having trouble with JUTE you can manually delete this file and try to restart JUTE:
+% sudo rm /tmp/jute.socket
+% yinst restart jute
+yapache
+JUTE requires yapache 1.x - it is expected to be installed and running (note if you ONLY want to run unit
+tests on V8 you do not need yapache - just install the 'jute_v8' package and NOT the 'jute' package).
+JUTE test directory 6JUTE Backends
+Capture
+This is the default mode for the JUTE UI. Any captured browsers (see 'Browsers' below) will run any
+submitted unit tests in parallel. To capture a browser point it to:
+http://<JUTE_HOST>/jute/
+The browser is now 'captured'. Any submitted unit tests will be run in parallel through all currently captured
+browsers.
+Each captured browser will create a test results file whose name contains the modified user agent string of that
+browser (so you can tell them apart).
+However only ONE code coverage output file will be created regardless of how many browsers are connected.
+When you click to run (a) unit test(s) in the JUTE UI the test(s) will be run in 'capture' mode - meaning any
+captured browser will run the selected test(s).
+Selenium
+This mode can currently only be accessed via the command line. Tests can be run serially or in parallel.
+V8/NodeJS
+This is essentially a standalone backend as it does NOT require yapache running. Tests are expected to be in
+the standard test location and output will go into the standard output location as detailed above. Note not all of
+your unit tests are guaranteed to run in the V8 backend!! Tests which require browser-y features like event
+simulation will not run as mouseclicks and keyevents and the like do not exist in NodeJS.
+Note to use V8 as a backend you MUST ALSO INSTALL THE jute_v8 PACKAGE.
+If you ONLY want to run unit tests on V8 you do not need the 'jute' package - just install the 'jute_v8' package
+and NOT the 'jute' package.
+ JUTE Backends 7Using JUTE
+WebUI
+Now that JUTE is up and running you can contact it here:
+http://<JUTE_HOST>/jute/
+THE SLASH ON THE END IS REQUIRED!!!!
+You should see the JUTE UI:
+This browser is now 'captured' and is ready to run unit tests.
+JUTE UI Status
+Browsers
+This lists all the currently captured browsers. Any submitted unit tests (that are not meant for Selenium (more
+below)) will be run in parallel in each of the captured browsers.
+Tests
+These are list of currently running/queued tests. A highlighted row means that test is currently running. You
+can watch tests running and being popped off the stack and subsequent tests run. These are the tests for this
+captured browser specifically. Browsers will run tests at different speeds.
+Test Files
+The middle panel is the list of all the test files JUTE knows about. If you do not see any files here JUTE is
+misconfigured. Look above at the 'html_dir' and 'test_dir' directory setting and ensure they're set properly.
+Running a Single Test
+Click on the test file link to run that test immediately without coverage. When running a single test the output
+will remain in the center panel (so you can see it) until you click 'Kick Lower Frame' to go back to the test
+listing.
+ Using JUTE 8Running Multiple Tests
+Clicking the checkboxes allows you to run multiple tests, either with or without coverage.
+Clicking the top checkboxes will select or unselect all tests in that column.
+Click 'Run Tests' to run selected tests.
+After running multiple tests the middle panel will return to the list of unit tests.
+Kick Lower Frame
+This button reloads the list of unit tests in the center panel
+Clear Tests
+This button deletes any pending/queued unit tests
+Clear Results
+This will clear all results - both unit test results and coverage information
+Results
+This column allows you to see the unit test results and code coverage information. For each browser there will
+be unit test output. Note this output is XML and the file contents may NOT be visible in your browser. 'View
+Source' if this is the case. A green link means all tests passed successfully and red link means at least one test
+failed.
+Command Line
+The script
+/home/y/bin/submit_test.pl
+Is the main command line interface to JUTE. This script allows you to submit unit tests to JUTE to run either
+in capture or Selenium mode. After all of your tests are submitted you can wait around for them using:
+/home/y/bin/wait_for_tests.pl
+This script will exit when all unit tests have finished.
+NOTE submit_test.pl will exit immediately after submitted the unit test(s)! If you want to wait until all
+submitted tests are complete, after running submit_test.pl (which you can do multiple times) use
+wait_for_tests.pl.
+/home/y/bin/submit_test.pl
+Submitting Tests
+There are several ways to submit unit tests. You must specify ONLY relative path from the TEST directory
+you defined above.
+ Running Multiple Tests 9One Test
+/home/y/bin/submit_test.pl path/to/my/test/index.html
+Note the PROJECT_ROOT/TEST_DIR will be prepended to the specified file.
+Multiple Tests
+On command line:
+/home/y/bin/submit_test.pl -test path/to/my/test/index.html -test path/to/other/test/index.html -test path/to/other/other/test.html
+OR
+If you have a filename with one test file per line:
+cat LIST_OF_TEST_FILES | /home/y/bin/submit_test.pl -
+Note the PROJECT_ROOT/TEST_DIR will be prepended to the specified test files.
+Running tests through Selenium
+Specify -sel_host to run the submitted tests through Selenium. You can optionally also supply -sel_browser to
+give a Selenium browser specification. -sel_browser defaults to '*firefox'.
+/home/y/bin/submit_test.pl -sel_host 10.3.4.45 [-sel_browser '*ieexplore'] path/to/test/index.html
+Of course -sel_host can either point to an individual Selenium host or a Selenium grid host.
+Note the PROJECT_ROOT/TEST_DIR will be prepended to the specified test files.
+Parallelization
+Specifying the '-browsers' option. This is '1' by default. Specifying a larger number will spawn off that many
+Selenium browsers (as specified by '-sel_host' and '-sel_browser') in parallel to run all submitted tests in
+parallel.
+Running tests through V8
+IF you also have the 'jute_v8' package installed you can use /home/y/bin/submit_test.pl to also run tests thru
+the NodeJS/V8 backend. Simply specify '-v8' on the command line and all of the specified tests given will be
+run through V8.
+/home/y/bin/submit_test.pl -v8 path/to/test/index.html
+OR any other permutation of test specification as outlined above.
+Specifying code coverage
+If you'd like your test(s) to run with code coverage enabled add the querystring '?do_coverage=1' to each test
+you want code coverage enabled for:
+/home/y/bin/submit_test.pl path/to/my/test/index.html?do_coverage=1
+OR
+ One Test 10/home/y/bin/submit_test.pl -sel_host 10.3.4.45 path/to/test/index.html?do_coverage=1
+OR
+/home/y/bin/submit_test.pl -test path/to/my/test/index.html?do_coverage=1 -test path/to/other/test/index.html -test path/to/other/other/test.html
+OR IF you have the 'jute_v8' package also installed:
+/home/y/bin/submit_test.pl -v8 path/to/my/test/index.html?do_coverage=1
+&c
+Note that last example code coverage will ONLY be generated for that first test since it's the only one with the
+'?do_coverage=1' querystring.
+Note the PROJECT_ROOT/TEST_DIR will be prepended to the specified test files.
+wait_for_tests.pl
+This script will exit once all currently submitted unit tests have finished running
+Specifying code coverage 11JUTE Output
+JUTE output all goes into the OUTPUT DIRECTORY as specified above. Within that directory will be a
+directory for each unit test. The name of the directory is the NAME OF THE TEST SUITE as specified in
+your javascript test file. This will be explained in more detail below.
+Note both test results and coverage information are available for easy viewing via the JUTE WebUI.
+Test Results
+For each browser an XML file will be created. The name of the XML file is a modified version of the USER
+AGENT string of that browser. An example:
+Mozilla5_0__Macintosh_U_Intel_Mac_OS_X_10_5_8_en-US__AppleWebKit534_16__KHTML__like_Gecko__Chrome10_0_648_127_Safari534_16-test.xml
+This unit test was run in the Chrome browser an Intel Mac OS version 10.5.8.
+The format of this file is JUnit XML style test output recognizable by most tools including Hudson.
+This looks like:
+ <?xml version="1.0" encoding="UTF-8"?>
+ <testsuites>
+ <testsuite name="Mozilla5.0.Macintosh.U.Intel.Mac.OS.X.10.5.8.en-US.AppleWebKit534.16.KHTML.like.Gecko.Chrome10.0.648.127.Safari534.16.initialization" tests="3" failures="0" time="0.021">
+ <testcase name="testLoggerNotInitialized" time="0.001"></testcase>
+ <testcase name="testInitRocketStats" time="0.001"></testcase>
+ <testcase name="testLoggerInititialized" time="0"> </testcase>
+ </testsuite>
+ <testsuite name="Mozilla5.0.Macintosh.U.Intel.Mac.OS.X.10.5.8.en-US.AppleWebKit534.16.KHTML.like.Gecko.Chrome10.0.648.127.Safari534.16.logger" tests="4" failures="0" time="0.025">
+ <testcase name="testGetTheInitializedLoggerObject" time="0"> </testcase>
+ <testcase name="testLoggerHasCorrectLogLevels" time="0.001"> </testcase>
+ <testcase name="testLoggerHasRightNumberOfAppenders" time="0.001"></testcase>
+ <testcase name="testAddAppenderToLogger" time="0.001"></testcase>
+ </testsuite>
+ </testsuites>
+Coverage Output
+The the same directory as the test result output will be a directory named 'lcov-report'. In that directory will be
+an index.html file you can load into your browser to see coverage results. JUTE uses yuitest_coverage to
+instrument and exact coverage information form the requested Javascript files. See below for how to tell
+JUTE to instrument selected Javascript files for code coverage.
+ JUTE Output 12Writing Unit Tests for JUTE
+JUTE requires VERY LITTLE from the developer to have their unit tests incorporated into the JUTE
+framework. Any new or already-written Unit Tests can easily be added to the JUTE framework.
+YUI3 'test' module
+The main requirement is using the YUI3 TestRunner and Assertion Framework for running and creating your
+Unit Tests. Look here for detailed information about getting started with YUI3 test module, including
+running, asserting, and potentially mocking objects for your unit tests.
+Note you must use YUI3 version 3.1.1+.
+Code Requirements
+To utilize JUTE you need only to make small additions to your current HTML file and 1 small addition to
+your test Javascript file.
+HTML requirements
+You must include '/jute_docs/jute.js' in a script tag BEFORE you load your test Javascript file.
+Here is a standard test HTML file - we will examine all the important bits below.
+ 1 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+ 2 <html lang="en">
+ 3
+ 4 <head>
+ 5 <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
+ 6 </head>
+ 7
+ 8 <body class="yui3-skin-sam">
+ 9 <div id="log"></div>
+ 10
+ 11 <div id="pagetoolbar">
+ 12 <span class="btn left right" >
+ 13 <a id="deleteLink" href="http://dont.go.here.com" title="{{str|replace_me}}" data-action="delete">Delete</a>
+ 14 </span>
+ 15 <span class="btn menu" id="btn-move" data-action="menu">
+ 16 <a id="moveLink" href="http://dont.go.here.com" title="{{str|replace_me}}">Move<b></b></a>
+ 17 </span>
+ 18 </div>
+ 19
+ 20 <script src="http://yui.yahooapis.com/3.3.0/build/yui/yui.js"></script>
+ 21 <script src="/jute_docs/jute.js"></script>
+ 22 <script src="../../../../src/common/utils/utils.js"></script>
+ 23 <script src="../../../../src/common/ui/toolbar/toolbar.js?coverage=1"></script>
+ 24 <script src="../../../../src/mods/neo.js"></script>
+ 25 <script src="../../../../src/templates/js/minty/module/toolbar/inbox.js"></script>
+ 26 <script src="testToolbar.js"></script>
+ 27 </body>
+ 28 </html>
+Line 8 - the class on the body tag tell YUI3 which skin we want to use.
+Line 9 - we make a <div> for the console logger.
+ Writing Unit Tests for JUTE 13Lines 11-18 are the markup for out tests - you could also create this dynamically in your test Javascript file.
+Lines 20-26 - Javascript files necessary to run the test suite(s)
+Line 20 - we include the YUI3 seed
+Line 21 - we include the required jute.js script. JUTE will also put this file there so you can copy this line
+exactly.
+Line 23 - the querystring appended to toolbar.js - '?coverage=1' tells JUTE we want code coverage
+information for this file when we run with code coverage enabled. You can append this to more than one
+Javascript file you load if you'd like coverage information generated for more than file during this unit test
+run. Note you still MUST run the tests with coverage enabled for coverage information to be generated. JUTE
+will automatically instrument files with '?coverage=1' with coverage data.
+Line 26 - the test Javascript file is loaded. We'll look at that next.
+Javascript Requirements
+Your Javascript test file must 'use' the 'jute' module.
+Below is the corresponding test file to the above HTML file - I'll point out the interesting lines below:
+ 1 YUI({
+ 2 logInclude: { TestRunner: true }
+ 3 }).use('test', 'io-base', 'node-event-simulate', 'common-ui-toolbar-base', 'jute', 'minty_module_toolbar_inbox', function(Y) {
+ 4
+ 5 // Setup dummy strings object
+ 6 window.strings = {}
+ 7
+ 8 var suite = new Y.Test.Suite('toolbar');
+ 9
+ 10 suite.add(new Y.Test.Case({
+ 11
+ 12 name:'tool zot bar',
+ 13
+ 14 setUp: function() {
+ 15 Y.Node.prototype.simulate = function(type, options) {
+ 16 Y.Event.simulate(Y.Node.getDOMNode(this), type, options);
+ 17 };
+ 18 },
+ 19
+ 20 testNewToolbar_noParams : function() {
+ 21 var tb = new Y.mail.ui.Toolbar();
+ 22 },
+ 23 testNewToolbar_template : function() {
+ 24 var tb = new Y.mail.ui.Toolbar({ template: Y.ui.Templates.minty_module_toolbar_inbox.base });
+ 25 },
+ 26 testNewToolbar_root : function() {
+ 27 var tb = new Y.mail.ui.Toolbar({ root: Y.one("#pagetoolbar") });
+ 28 },
+ 29 testGetContent : function () {
+ 30 var tb = new Y.mail.ui.Toolbar({ template: Y.ui.Templates.minty_module_toolbar_inbox.base });
+ 31 var node = tb.get('content');
+ 32 Y.Assert.isObject(node);
+ 33 },
+ 34
+ 35 testSelectButton : function() {
+ 36 var tb = new Y.mail.ui.Toolbar({ root: Y.one("#pagetoolbar") });
+ 37
+ 38 tb.on('itemSelected', function(e) {
+HTML requirements 14 39 Y.Assert.areEqual(e.dataAction, 'delete', "Click on toolbar link");
+ 40 tb.detach('itemSelected');
+ 41 });
+ 42
+ 43 // Click the link!
+ 44 Y.one("#deleteLink").simulate("click");
+ 45 },
+ 46
+ 47 testSelectMenu : function() {
+ 48 var tb = new Y.mail.ui.Toolbar({ root: Y.one("#pagetoolbar") });
+ 49
+ 50 tb.on('itemSelected', function(e) {
+ 51 Y.Assert.areEqual(e.dataAction, 'menu', "Click on menu item");
+ 52 tb.detach('itemSelected');
+ 53 });
+ 54
+ 55 // Click the link!
+ 56 Y.one("#moveLink").simulate("click");
+ 57 }
+ 58 }));
+ 59
+ 60 Y.Test.Runner.add(suite);
+ 61 Y.UnitTest.go();
+ 62
+ 63 });
+Line 2 - Tell the logger to output TestRunner output
+Line 3 - Ensure we use BOTH the 'test' and 'jute' modules. And of course the actual modules you're testing
+and anything else you need.
+Line 8 - We give a name to our Suite - this name is later used as the directory name under the OUTPUT
+DIRECTORY where test results and code coverage will live.
+Lines 10-58 - Our test functions and a setUp function - standard test stuff
+Line 60 - We add our suite to the YUI3 TestRunner
+Line 61 - We start our tests! This is a convenience function provided by JUTE to initialize the console and
+start our tests. If you want to handle that stuff yourself you can just call the standard 'Y.Test.Runner.run()'
+yourself.
+That's IT!! The only changes you needed to make from a standard YUI3 test Javascript file was including the
+'jute' module (which you loaded in your HTML file) and optionally calling UnitTest.go() instead of
+Y.Test.Runner.run().
+With this setup you can run your test thru any JUTE backend.
+Javascript Requirements 15JUTE V8
+V8 support in JUTE is available in a separate yinst package:
+% yinst i -b test jute_v8
+This package mainly provides the /home/y/bin/jute_v8.js file used to run your unit test(s) thru V8.
+You do NOT need to install the 'jute' package to install and use 'jute_v8'.
+Installation
+Even if you already have the 'jute' package installed you still need to:
+% yinst install -b test jute_v8
+Note you do NOT have to have the 'jute' package installed if you only want to run V8 unit tests. If you want
+the WebUI and the ability to use Selenium and Captured browsers then you must also install the 'jute' package'
+See above for gory details.
+Configuration
+There are 3 yinst variables that must be set correctly:
+HTML_ROOT
+This is the HTML_ROOT of your app:
+% yinst set jute_v8.HTML_ROOT=/home/y/share/htdocs/jutebase/src/
+TEST_ROOT
+This is the full path to the base of your test directory:
+% yinst set jute_v8.TEST_ROOT=/home/y/share/htdocs/jutebase/test/
+OUTPUT_ROOT
+This is the full path to the base of where your output files will live:
+% yinst set jute_v8.OUTPUT_ROOT=/home/y/share/htdocs/jutebase/output/
+Running
+IF you also have the 'jute' package installed you can use /home/y/bin/submit_test.pl to run V8 tests as outline
+above.
+IF you do NOT have the 'jute' package installed or want to do something else you can use the NodeJS script in
+/home/y/bin/jute_v8.js
+/home/y/bin/jute_v8.js path/to/test/index.html [1]
+ JUTE V8 16The script takes 2 arguments - the path to the test HTML file relative to TEST_ROOT and an optional
+boolean second argument for code coverage. SO without code coverage:
+/home/y/bin/jute_v8.js path/to/test/index.html
+With code coverage:
+/home/y/bin/jute_v8.js path/to/test/index.html 1
+Results
+Results in JUnit XML format are stored in the OUTPUT_ROOT just like in the regular jute case. Code
+coverage will be put there too as jute does. If you have the WebUI installed (in the 'jute' package) you can
+now see the V8 results.
+Test run information, total test statistics and code coverage statistics are also available via STDOUT.
+V8 Caveats
+Not all unit tests will run in the V8 backend!! Event simulation is not supported. Crazy iframe stuff can be
+problematic. Anything expecting a real browser will be disappointed. All the basic DOM manipulation is
+available, UI events are not. No mouse events, No key events, No focus events. So be safe out there!
+Debugging
+If you set the JUTE_DEBUG environment variable:
+% export JUTE_DEBUG=1
+You'll see even more gory debug output.
+Running 17Best Practices
+By default JUTE assumes a 'best practices' setup. Using the JUTE yinst set variable outlined above this
+behavior can be changed.
+Directory Setup
+Your PROJECT ROOT should be the parent directory of a 'src', 'test', and 'output' directory. The 'test'
+directory should be a mirror of the 'src' directory hierarchy. This eases the setup and keeps the directory
+structure simple and clean.
+Test File Setup
+You should use relative paths in your HTML test files pointing to project files over in the 'src' tree that you're
+testing.
+Developer Environment
+Your local svn directory should be symlinked into /home/y/share/htdocs somewhere for local testing. Your
+test files can be served from yapache or thru a file URL. You can also run individual tests using the JUTE
+command line.
+Developer Build Environment
+A Captured Browser
+a local dev build can run all available unit tests - if they're all in a 'test' directory the command to run the tests
+can be as easy as:
+LOCAL_TEST_DIR = ../test
+run_unit_tests:
+ cd $(LOCAL_TEST_DIR) && find . -not \( -path "*/.svn/*" \) -name '*.html' -exec /home/y/bin/submit_test.pl {} do_coverage=$(DO_COVERAGE) \; -print
+ /home/y/bin/wait_for_tests.pl 1
+DO_COVERAGE can be set to '1' to generate coverage reports:
+% make run_unit_tests DO_COVERAGE=1
+Note this assumes you have at least 1 captured browser. This will run all of your unit tests with code coverage.
+You can of course also select all checkboxes in the webui and the result is the same.
+V8
+run_v8_unit_tests:
+ cd $(LOCAL_TEST_DIR) && find . -not \( -path "*/.svn/*" \) -name '*.html' -exec /home/y/bin/jute_v8.js {} $(DO_COVERAGE) \;
+All output will go to STDOUT.
+ Best Practices 18Hudson Build Environment
+All of your tests can be run thru Selenium - they all need to submitted at once to run as one job:
+submit_selenium_tests:
+ cd $(LOCAL_TEST_DIR) && find . -not \( -path "*/.svn/*" \) -name '*.html' -printf '%p?do_coverage=$(DO_COVERAGE)\n' | /home/y/bin/submit_test.pl -test - -sel_host $(SEL_HOST) -sel_browser $(SEL_BROWSER) -send_output
+This will return once all tests have run. Ensure Hudson is configured correctly to look in the output directory
+for test results and cover coverage. If a unit test fails Hudson will label the build 'Unstable'. Clicking thru the
+"Test Results" will reveal the failed test(s).
+Note you are not allowed to run a web server on your Hudson build machine. You can use Stagecoach to run
+the unit tests remotely and return the results back to your Hudson build machine. Alternatively you can use
+the V8 backend IF all of your unit tests can run under V8.
+ Hudson Build Environment 19Yahoo Continuous Integration Standards
+ Yahoo Continuous Integration Standards 20Yahoo CI Unit Test Standards
+ Yahoo CI Unit Test Standards 21Build/Hudson Integration
+Running tests and result and code coverage information are easily integrated into your builds and Hudson.
+Running Tests
+Where:
+LOCAL_TEST_DIR is the root of where your test files live on the build or local host
+SEL_HOST is the hostname/IP of your Selenium box or grid
+SEL_BROWSER is '*firefox' or '*iexplore' or whatever browser specification you want to use
+DO_COVERAGE is 1 or 0 depending on if you want coverage (you probably do)
+Local
+Running JUTE/browser tests locally is a simple Makefile rule.
+Captured Browser(s)
+submit_tests:
+ cd $(LOCAL_TEST_DIR) && find . -not \( -path "*/.svn/*" \) -name '*.html' -exec /home/y/bin/submit_test.pl {} do_coverage=$(DO_COVERAGE} \; -print
+ /home/y/bin/wait_for_tests.pl 1
+Selenium
+submit_selenium_tests:
+ cd $(LOCAL_TEST_DIR) && find . -not \( -path "*/.svn/*" \) -name '*.html' -printf '%p?do_coverage=$(DO_COVERAGE)\n' | /home/y/bin/submit.pl -test - -sel_host $(SEL_HOST) -sel_browser $(SEL_BROWSER) -send_output
+V8
+submit_v8_tests:
+ cd $(LOCAL_TEST_DIR) && find . -not \( -path "*/.svn/*" \) -name '*.html' -exec /home/y/bin/jute_v8.js {} $(DO_COVERAGE} \;
+Hudson
+Captured Browsers and Selenium
+Running browser-based tests in Hudson is a little goofy due mainly to the restriction of not running a
+webserver on your Hudson slave. One solution is to use Stagecoach to tar up your source tree (including tests)
+and scp it all over to another machine that will host your tests - which of course is running yapache.
+On that remote box you install all necessary package for running your tests (jute & possibly others) and using
+Stagecoach run one of the captured or Selenium test Makefile rules.
+After all tests are finished you need to tar up the results and scp them back to the Hudson host. Just drop the
+results where ever output_dir is pointing to.
+V8
+Just call the 'submit_v8_tests' Makefile target! There's no webserver or remote captured or Selenium host to
+run the tests so you're good to go!!
+ Build/Hudson Integration 22Viewing Test Results
+Output Directory
+All results are stored in output_dir - you can look at the *.xml files to view the raw JUnit XML output.
+Browser
+If the 'jute' package is installed you can visit:
+http://<jute host>/jute/
+& click on links in the 'Results' column - this will show results for captured, Selenium, and V8 tests.
+Hudson
+Test Results
+You need to configure your Hudson project to find the *.xml files the tests generated.
+Check the 'Publish Test Results in Labeled Groups (recommended by Yahoo!)' and use
+'trunk/output/**/*-test.xml' as your Result File Mask. Note you may have to change this path to point to
+where under your svn root your output_dir lies.
+Be sure to set 'Report Format' to 'JUnit Parser'
+Code Coverage
+Running individual tests will generate a directory hierarchy rooted at your output_dir. The first thing you need
+to do is to aggregate all the individual coverage output into one mongo output files containing all the the
+individual output files.
+This can be accomplished by a simple Makefile rule:
+LCOV_GENHTML = /home/y/bin/genhtml # from the 'lcov' yinst package
+TOTAL_LCOV_FILE = $(OUTPUT_DIR)/lcov.info
+OUTPUT_DIR = <jute.output_dir or jute_v8.OUTPUT_DIR>
+make_total_lcov:
+ /bin/rm -f $(TOTAL_LCOV_FILE)
+ @echo "OUTPUT DIR: ${OUTPUT_DIR}"
+ @echo "TOTAL LCOV FILE: ${TOTAL_LCOV_FILE}"
+ find $(OUTPUT_DIR) -name lcov.info -exec cat {} >> $(TOTAL_LCOV_FILE) \;
+ @ls ${OUTPUT_DIR}
+ /bin/rm -rf $(OUTPUT_DIR)/lcov-report
+ $(LCOV_GENHTML) -o $(OUTPUT_DIR)/lcov-report $(TOTAL_LCOV_FILE)
+Now simply point Hudson to this aggregated 'lcov.info' file - check 'Publish Lcov Coverage Report' and set
+'lcov.info file mask' to something similar to 'trunk/output/lcov.info' depending on where your output_dir is
+relative to your SVN root.