qunited 0.3.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,5 +1,4 @@
1
1
  require 'optparse'
2
- require 'ostruct'
3
2
 
4
3
  module QUnited
5
4
  class Application
@@ -12,7 +11,8 @@ module QUnited
12
11
 
13
12
  def run_tests
14
13
  js_source_files, js_test_files = ARGV.join(' ').split('--').map { |file_list| file_list.split(' ') }
15
- exit QUnited::Runner.new(js_source_files, js_test_files, options).run
14
+ passed = QUnited::Runner.new(js_source_files, js_test_files, options).run
15
+ exit (passed ? 0 : 10)
16
16
  end
17
17
 
18
18
  # Client options generally parsed from the command line
@@ -80,21 +80,19 @@ Options:
80
80
  def handle_exceptions
81
81
  begin
82
82
  yield
83
- rescue SystemExit
84
- exit
85
83
  rescue UsageError => ex
86
84
  $stderr.puts ex.message
87
85
  exit 1
88
86
  rescue OptionParser::InvalidOption => ex
89
87
  $stderr.puts ex.message
90
88
  exit 1
91
- rescue Exception => ex
92
- display_error_message ex
89
+ rescue StandardError => ex
90
+ display_crash_message ex
93
91
  exit 1
94
92
  end
95
93
  end
96
94
 
97
- def display_error_message(ex)
95
+ def display_crash_message(ex)
98
96
  msg = <<MSG
99
97
  QUnited has aborted! If this is unexpected, you may want to open an issue at
100
98
  github.com/aaronroyer/qunited to get a possible bug fixed. If you do, please
@@ -4,7 +4,12 @@ module QUnited
4
4
  # Path of the common (to all drivers) supporting files directory
5
5
  SUPPORT_DIR = File.expand_path('../support', __FILE__)
6
6
 
7
+ TEST_RESULT_START_TOKEN = 'QUNITED_TEST_RESULT_START_TOKEN'
8
+ TEST_RESULT_END_TOKEN = 'QUNITED_TEST_RESULT_END_TOKEN'
9
+ TEST_RESULT_REGEX = /#{TEST_RESULT_START_TOKEN}(.*?)#{TEST_RESULT_END_TOKEN}/m
10
+
7
11
  attr_reader :results, :source_files, :test_files
12
+ attr_accessor :formatter
8
13
 
9
14
  # Finds an executable on the PATH. Returns the absolute path of the
10
15
  # executable if found, otherwise nil.
@@ -49,6 +54,12 @@ module QUnited
49
54
  def name
50
55
  self.class.name.split('::')[-1]
51
56
  end
57
+
58
+ protected
59
+
60
+ def send_to_formatter(method, *args)
61
+ formatter.send(method, *args) if formatter
62
+ end
52
63
  end
53
64
  end
54
65
  end
@@ -16,7 +16,7 @@ module QUnited
16
16
  end
17
17
 
18
18
  def name
19
- "PhantomJS" # Slightly more accurate than our class name
19
+ 'PhantomJS'
20
20
  end
21
21
 
22
22
  def run
@@ -24,20 +24,37 @@ module QUnited
24
24
  tests_file.write(tests_page_content)
25
25
  tests_file.close
26
26
 
27
- results_file = Tempfile.new('qunited_results')
28
- results_file.close
27
+ send_to_formatter(:start)
29
28
 
30
- cmd = %{phantomjs "#{File.join(SUPPORT_DIR, 'runner.js')}" }
31
- cmd << %{#{tests_file.path} #{results_file.path}}
29
+ cmd = %|phantomjs "#{File.join(SUPPORT_DIR, 'runner.js')}" "#{tests_file.path}"|
30
+
31
+ @results = []
32
32
 
33
33
  Open3.popen3(cmd) do |stdin, stdout, stderr|
34
- # PhantomJS sometimes puts error messages to stdout - redirect them to stderr
35
- [stdout, stderr].each do |io|
36
- unless (io_str = io.read).strip.empty? then $stderr.puts(io_str) end
34
+ results_collector = ResultsCollector.new(stdout)
35
+
36
+ results_collector.on_test_result do |result|
37
+ @results << result
38
+ method = result.passed? ? :test_passed : :test_failed
39
+ send_to_formatter(method, result)
40
+ end
41
+
42
+ results_collector.on_non_test_result_line do |line|
43
+ # PhantomJS sometimes puts error messages to stdout. If we are not reading
44
+ # a test result then redirect any output to stderr
45
+ $stderr.puts(line)
37
46
  end
47
+
48
+ results_collector.collect_results
49
+
50
+ err = stderr.read
51
+ unless err.nil? || err.strip.empty? then $stderr.puts(err) end
38
52
  end
39
53
 
40
- @results = ::QUnited::Results.from_javascript_produced_json(IO.read(results_file.path))
54
+ send_to_formatter(:stop)
55
+ send_to_formatter(:summarize)
56
+
57
+ @results
41
58
  end
42
59
 
43
60
  private
@@ -1,102 +1,97 @@
1
1
  /*
2
- Portions of this file are from the PhantomJS project from Ofi Labs.
3
-
4
- Copyright (C) 2011 Ariya Hidayat <ariya.hidayat@gmail.com>
5
- Copyright (C) 2011 Ivan De Marino <ivan.de.marino@gmail.com>
6
-
7
- Redistribution and use in source and binary forms, with or without
8
- modification, are permitted provided that the following conditions are met:
2
+ * Runs QUnit tests in PhantomJS and outputs test result data, in JSON format, to stdout.
3
+ *
4
+ * Tokens are placed around each test result to allow them to be parsed individually. The tokens
5
+ * match the constants in QUnited::Driver::ResultsCollector.
6
+ *
7
+ * Usage:
8
+ * phantomjs runner.js PATH_TO_TESTS_HTML_PAGE
9
+ */
9
10
 
10
- * Redistributions of source code must retain the above copyright
11
- notice, this list of conditions and the following disclaimer.
12
- * Redistributions in binary form must reproduce the above copyright
13
- notice, this list of conditions and the following disclaimer in the
14
- documentation and/or other materials provided with the distribution.
15
- * Neither the name of the <organization> nor the
16
- names of its contributors may be used to endorse or promote products
17
- derived from this software without specific prior written permission.
11
+ var system = require('system'),
12
+ webpage = require('webpage');
18
13
 
19
- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
- AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
- IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
22
- ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
23
- DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
24
- (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25
- LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
26
- ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27
- (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
28
- THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29
- */
14
+ if (system.args.length < 2) {
15
+ console.log('No tests file specified');
16
+ phantom.exit(1);
17
+ }
30
18
 
31
- var system = require('system'), fs = require("fs");
19
+ var page = webpage.create(),
20
+ testsHtmlFile = system.args[1],
21
+ config = {
22
+ resultsCheckInterval: 200, // Check for new results at this interval
23
+ testsCompletedCheckInterval: 100, // Check for all tests completing at this interval
24
+ testsTimeout: 10001 // Time out and indicate error after this many millis
25
+ };
32
26
 
33
- /**
34
- * Wait until the test condition is true or a timeout occurs. Useful for waiting
35
- * on a server response or for a ui change (fadeIn, etc.) to occur.
27
+ /*
28
+ * Writes any collected QUnit results that are pending output to stdout (this is done with
29
+ * console.log in PhantomJS).
36
30
  *
37
- * @param testFx javascript condition that evaluates to a boolean,
38
- * it can be passed in as a string (e.g.: "1 == 1" or "$('#bar').is(':visible')" or
39
- * as a callback function.
40
- * @param onReady what to do when testFx condition is fulfilled,
41
- * it can be passed in as a string (e.g.: "1 == 1" or "$('#bar').is(':visible')" or
42
- * as a callback function.
43
- * @param timeOutMillis the max amount of time to wait. If not specified, 3 sec is used.
31
+ * Tokens are placed around each test result to allow them to be parsed out later. Note that the
32
+ * tokens must match the constants in QUnited::Driver::ResultsCollector.
33
+ *
34
+ * JSON.stringify must be called in the context of the page to properly serialize null values in
35
+ * results. If it is called here in the PhantomJS interpreter code then null values are serialized
36
+ * as empty strings. Finding out exactly why this happens would take more investigation. For now
37
+ * it seems that stringifying on the page is a decent solution, though it is slightly less robust
38
+ * since JSON serialization may be tampered with in user code.
44
39
  */
45
- function waitFor(testFx, onReady, timeOutMillis) {
46
- var maxtimeOutMillis = timeOutMillis ? timeOutMillis : 3001, //< Default Max Timeout is 3s
47
- start = new Date().getTime(),
48
- condition = false,
49
- interval = setInterval(function() {
50
- if ( (new Date().getTime() - start < maxtimeOutMillis) && !condition ) {
51
- // If not time-out yet and condition not yet fulfilled
52
- condition = (typeof(testFx) === "string" ? eval(testFx) : testFx()); //< defensive code
53
- } else {
54
- if (!condition) {
55
- // If condition still not fulfilled (timeout but condition is 'false')
56
- console.log("ERROR: Timeout waiting for tests to complete");
57
- phantom.exit(1);
58
- } else {
59
- // Condition fulfilled (timeout and/or condition is 'true')
60
- //console.log("'waitFor()' finished in " + (new Date().getTime() - start) + "ms.");
61
- typeof(onReady) === "string" ? eval(onReady) : onReady(); //< Do what it's supposed to do once the condition is fulfilled
62
- clearInterval(interval); //< Stop this interval
63
- }
64
- }
65
- }, 100);
66
- };
67
-
40
+ function writePendingTestResults() {
41
+ var serializedResults = page.evaluate(function() {
42
+ var pendingResults = [];
43
+ while (QUnited.testResultsPendingOutput.length > 0) {
44
+ pendingResults.push(QUnited.util.jsonStringify(QUnited.testResultsPendingOutput.shift()));
45
+ }
46
+ return pendingResults;
47
+ });
68
48
 
69
- if (system.args.length < 2) {
70
- console.log('No tests file specified');
71
- phantom.exit(1);
72
- } else if (system.args.length < 3) {
73
- console.log('No results output file specified');
74
- phantom.exit(1);
49
+ var i, output;
50
+ for (i = 0; i < serializedResults.length; i++) {
51
+ output = 'QUNITED_TEST_RESULT_START_TOKEN';
52
+ output += serializedResults[i];
53
+ output += 'QUNITED_TEST_RESULT_END_TOKEN';
54
+ console.log(output);
55
+ }
75
56
  }
76
57
 
77
- var page = require('webpage').create(),
78
- tests_html_file = system.args[1],
79
- results_output_file = system.args[2];
58
+ /*
59
+ * Executes the given function once all tests have completed. If a timeout occurs then exit
60
+ * with a status code of 1.
61
+ */
62
+ function whenTestsHaveCompleted(fn) {
63
+ var start = new Date().getTime(),
64
+ testsHaveCompleted = false,
65
+ interval = setInterval(function() {
66
+ if ( (new Date().getTime() - start < config.testsTimeout) && !testsHaveCompleted ) {
67
+ testsHaveCompleted = page.evaluate(function() { return QUnited.testsHaveCompleted; });
68
+ } else {
69
+ if (testsHaveCompleted) {
70
+ fn();
71
+ clearInterval(interval);
72
+ } else {
73
+ // Tests took too long
74
+ console.log("ERROR: Timeout waiting for tests to complete");
75
+ phantom.exit(1);
76
+ }
77
+ }
78
+ }, config.testsCompletedCheckInterval);
79
+ };
80
80
 
81
- page.open(tests_html_file, function(status) {
81
+ /*
82
+ * Open the HTML page that contains all of our QUnit tests. As it is running, check for collected
83
+ * test results and output them if we have any. Also check whether tests have completed. Once they
84
+ * have completed, output any remaining results and exit.
85
+ */
86
+ page.open(testsHtmlFile, function(status) {
82
87
  if (status !== "success") {
83
88
  console.log("Could not open tests file");
84
89
  phantom.exit(1);
85
90
  } else {
86
- waitFor(function(){
87
- // Done when all tests have run (the results have been rendered)
88
- return page.evaluate(function(){
89
- var el = document.getElementById('qunit-testresult');
90
- if (el && el.innerText.match('completed')) {
91
- return true;
92
- }
93
- return false;
94
- });
95
- }, function(){
96
- // Results should have been collected with code in qunited.js. Check that file
97
- // for more details. Grab the YAML it outputs and write it to the results file.
98
- var results = page.evaluate(function() { return QUnited.collectedTestResultsAsJson(); });
99
- fs.write(results_output_file, results, 'a');
91
+ setInterval(writePendingTestResults, config.resultsCheckInterval);
92
+
93
+ whenTestsHaveCompleted(function() {
94
+ writePendingTestResults();
100
95
  phantom.exit(0);
101
96
  });
102
97
  }
@@ -21,6 +21,9 @@
21
21
  <%= script_tag source_file %>
22
22
  <% end %>
23
23
  <% test_files.each do |test_file| %>
24
+ <script>
25
+ QUnited.currentTestFile = "<%= test_file %>";
26
+ </script>
24
27
  <%= script_tag test_file %>
25
28
  <% end %>
26
29
 
@@ -0,0 +1,105 @@
1
+ module QUnited
2
+ module Driver
3
+
4
+ # Collects test results from lines of JavaScript interpreter output.
5
+ #
6
+ # QUnited test running drivers may run a JavaScript interpreter in a separate process and
7
+ # observe the output (say, on stdout) for text containing test results. These results may
8
+ # be delimited by tokens that allow ResultsCollector to recognize the beginning and end of
9
+ # JSON test results. The test running driver must be properly configured to emit the correct
10
+ # tokens, matching TEST_RESULT_START_TOKEN and TEST_RESULT_END_TOKEN before and after strings
11
+ # of test results serialized as valid JSON.
12
+ #
13
+ # If everything is set up correctly the recognized results are parsed and QUnitTestResult
14
+ # objects are produced for each.
15
+ #
16
+ #
17
+ # To use, initialize with the IO object that provides the output from the test running
18
+ # process. Then call on_test_result with a block to be called when a test is collected.
19
+ # A QUnited::QUnitTestResult object will be passed to the block for each test.
20
+ #
21
+ # rc = ResultsCollector.new(stdout_from_test_runner)
22
+ # rc.on_test_result {|test_result| puts "I've got a result: #{test_result.inspect}" }
23
+ #
24
+ # If you need to capture output that is not part of any test result, you can call
25
+ # on_non_test_result_line with another block to do this. Each line of output that is not part
26
+ # of test result JSON is passed to the block.
27
+ #
28
+ # rc.on_non_test_result_line {|line| puts "This line is not part of a test result: #{line}"}
29
+ #
30
+ class ResultsCollector
31
+ TEST_RESULT_START_TOKEN = 'QUNITED_TEST_RESULT_START_TOKEN'
32
+ TEST_RESULT_END_TOKEN = 'QUNITED_TEST_RESULT_END_TOKEN'
33
+ ONE_LINE_TEST_RESULT_REGEX = /#{TEST_RESULT_START_TOKEN}(.*?)#{TEST_RESULT_END_TOKEN}/
34
+
35
+ def initialize(io)
36
+ @io = io
37
+ @results = []
38
+ @on_test_result_block = nil
39
+ @on_non_test_result_line_block = nil
40
+ @partial_test_result = ''
41
+ end
42
+
43
+ # Set a block to be called when a test result has been parsed. The block is passed a
44
+ # QUnitTestResult object.
45
+ def on_test_result(&block)
46
+ raise ArgumentError.new('must provide a block') unless block_given?
47
+ @on_test_result_block = block
48
+ end
49
+
50
+ # Set a block to be called when a line of output is read from the IO object that is not
51
+ # part of a test result. The block is passed the line of output.
52
+ def on_non_test_result_line(&block)
53
+ raise ArgumentError.new('must provide a block') unless block_given?
54
+ @on_non_test_result_line_block = block
55
+ end
56
+
57
+ # Read all available lines from the IO and parse results from it. If blocks have been set
58
+ # with on_test_result and/or on_non_test_result_line they will be called when appropriate.
59
+ def collect_results
60
+ while collect_next_line; end
61
+ end
62
+
63
+ # Read the next line from the IO and parse results from it, if applicable. If blocks have
64
+ # been set with on_test_result and/or on_non_test_result_line they will be called when
65
+ # appropriate.
66
+ #
67
+ # Usually collect_results should be used unless lines need to be read one at a time for
68
+ # some reason.
69
+ def collect_next_line
70
+ line = @io.gets
71
+ return nil unless line
72
+
73
+ if line =~ ONE_LINE_TEST_RESULT_REGEX
74
+ process_test_result $1
75
+
76
+ elsif line.include? TEST_RESULT_START_TOKEN
77
+ @partial_test_result << line.sub(TEST_RESULT_START_TOKEN, '')
78
+
79
+ elsif line.include? TEST_RESULT_END_TOKEN
80
+ @partial_test_result << line.sub(TEST_RESULT_END_TOKEN, '')
81
+ process_test_result @partial_test_result
82
+ @partial_test_result = ''
83
+
84
+ elsif !@partial_test_result.empty?
85
+ # Middle of a test result
86
+ @partial_test_result << line
87
+
88
+ else
89
+ @on_non_test_result_line_block.call(line) if @on_non_test_result_line_block
90
+
91
+ end
92
+
93
+ line
94
+ end
95
+
96
+ private
97
+
98
+ def process_test_result(test_result_json)
99
+ result = ::QUnited::QUnitTestResult.from_json(test_result_json)
100
+ @results << result
101
+ @on_test_result_block.call(result) if @on_test_result_block
102
+ end
103
+ end
104
+ end
105
+ end
@@ -28,23 +28,33 @@ module QUnited
28
28
  source_files_args = @source_files.map { |sf| %{"#{sf}"} }.join(' ')
29
29
  test_files_args = @test_files.map { |tf| %{"#{tf}"} }.join(' ')
30
30
 
31
- results_file = Tempfile.new('qunited_results')
32
- results_file.close
31
+ send_to_formatter(:start)
33
32
 
34
33
  cmd = %{java -jar "#{js_jar}" -opt -1 "#{runner}" }
35
- cmd << %{"#{QUnited::Driver::Base::SUPPORT_DIR}" "#{SUPPORT_DIR}" "#{results_file.path}"}
34
+ cmd << %{"#{QUnited::Driver::Base::SUPPORT_DIR}" "#{SUPPORT_DIR}"}
36
35
  cmd << " #{source_files_args} -- #{test_files_args}"
37
36
 
38
- # Swallow stdout but allow stderr to get blasted out to console - if there are uncaught
39
- # exceptions or anything else that goes wrong with the JavaScript interpreter the user
40
- # will probably want to know but we are not particularly interested in it.
37
+ @results = []
38
+
41
39
  Open3.popen3(cmd) do |stdin, stdout, stderr|
42
- stdout.each {||} # Ignore; this is just here to make sure we block
43
- # while waiting for tests to finish
40
+ results_collector = ResultsCollector.new(stdout)
41
+ results_collector.on_test_result do |result|
42
+ @results << result
43
+ method = result.passed? ? :test_passed : :test_failed
44
+ send_to_formatter(method, result)
45
+ end
46
+
47
+ results_collector.collect_results
48
+
49
+ # Allow stderr to get blasted out to console - if there are uncaught exceptions or
50
+ # anything else that goes wrong with Rhino the user will probably want to know.
44
51
  unless (err = stderr.read).strip.empty? then $stderr.puts(err) end
45
52
  end
46
53
 
47
- @results = ::QUnited::Results.from_javascript_produced_json(IO.read(results_file.path))
54
+ send_to_formatter(:stop)
55
+ send_to_formatter(:summarize)
56
+
57
+ @results
48
58
  end
49
59
  end
50
60
  end
@@ -1,22 +1,25 @@
1
- // Runs QUnit tests with Envjs and outputs test results as
2
- // an array of data serialized in YAML format.
3
- //
4
- // The first argument should be the lib directory containing common QUnited dependencies. The second
5
- // argument is the directory containing Rhino driver specific dependencies. The third argument
6
- // is the file to use for test results output. The next arguments are source JavaScript files to
7
- // test, until "--" is encountered. After the "--" the rest of the arguments are QUnit
8
- // test files.
9
- //
10
- // Example:
11
- // java -jar js.jar -opt -1 runner.js commonlibdir libdir outfile.json source.js -- test1.js test2.js
12
- // ^ our args start here
1
+ /*
2
+ * Runs QUnit tests on Rhino with Envjs and outputs test result data, in JSON format, to stdout.
3
+ *
4
+ * Tokens are placed around each test result to allow them to be parsed individually. The tokens
5
+ * match the constants in QUnited::Driver::ResultsCollector.
6
+ *
7
+ * When run, the first argument is the lib directory containing common QUnited dependencies. The
8
+ * second argument is the directory containing Rhino driver specific dependencies. The next
9
+ * arguments are source JavaScript files to test, until "--" is encountered. After the "--" the
10
+ * rest of the arguments are QUnit test files.
11
+ *
12
+ * Example:
13
+ * java -jar js.jar -opt -1 runner.js commonlibdir libdir source.js -- test1.js test2.js
14
+ * ^ our args start here
15
+ */
13
16
 
14
17
  var QUnited = { sourceFiles: [], testFiles: [] };
15
18
 
19
+ // Process command line arguments
16
20
  (function(args) {
17
21
  var commonLibDir = args.shift(),
18
22
  libDir = args.shift();
19
- QUnited.outputFile = args.shift();
20
23
 
21
24
  load(libDir + '/env.rhino.js');
22
25
 
@@ -90,21 +93,32 @@ QUnited.testFiles.forEach(function(file) {
90
93
  QUnited.modulesMap[defaultModuleName] = module;
91
94
  }
92
95
 
93
- // Push our failed test data into the default module
94
- module.tests.push({
95
- name: "Nonexistent test",
96
+ // Put our failed test data into the default module
97
+ var failingTest = {
98
+ name: "Nonexistent tests",
96
99
  assertion_data: [{
97
100
  result: false, message: "Test file did not contain any tests (or there was an error loading it)"
98
101
  }],
99
102
  start: new Date(), duration: 0,
100
103
  assertions: 1, failed: 1, total: 1,
101
104
  file: file
102
- });
105
+ }
106
+
107
+ module.tests.push(failingTest);
108
+ QUnited.testResultsPendingOutput.push(failingTest);
103
109
  }
104
- });
105
110
 
106
- (function() {
107
- var writer = new java.io.PrintWriter(QUnited.outputFile);
108
- writer.write(QUnited.collectedTestResultsAsJson());
109
- writer.close();
110
- })();
111
+ var results = [];
112
+ while (QUnited.testResultsPendingOutput.length > 0) {
113
+ results.push(QUnited.testResultsPendingOutput.shift());
114
+ }
115
+
116
+ var i, output;
117
+ for (i = 0; i < results.length; i++) {
118
+ // Beginning and end tokens should match the constants in QUnited::Driver::ResultsCollector
119
+ output = 'QUNITED_TEST_RESULT_START_TOKEN\n';
120
+ output += JSON.stringify(results[i], null, 1);
121
+ output += '\nQUNITED_TEST_RESULT_END_TOKEN';
122
+ java.lang.System.out.println(output);
123
+ }
124
+ });