qunited 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
+ });