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,8 +1,15 @@
1
+ /*
2
+ * This contains code that may be placed, by a QUnited driver, on the same page or context where
3
+ * QUnit tests are run. QUnit events are listened for and test results are collected for easier
4
+ * retrieval by other driver code.
5
+ */
6
+
1
7
  var QUnited = QUnited || {};
2
8
 
3
9
  (function() {
4
10
 
5
11
  QUnited.util = {};
12
+
6
13
  QUnited.util.dateToString = function(date) {
7
14
  if (Object.prototype.toString.call(date) === '[object String]') { return date; }
8
15
  function pad(n) { return n < 10 ? '0' + n : n; }
@@ -10,6 +17,60 @@ QUnited.util.dateToString = function(date) {
10
17
  pad(date.getUTCHours()) + ':' + pad(date.getUTCMinutes()) + ':' + pad(date.getUTCSeconds()) + 'Z';
11
18
  };
12
19
 
20
+ /*
21
+ * Converts the given serializable JavaScript object to a JSON string. Uses the native JSON.stringify.
22
+ */
23
+ QUnited.util.jsonStringify = function(object) {
24
+ var jsonString,
25
+ stringifiableTypes,
26
+ toJSONFunctions,
27
+ i;
28
+
29
+ // Hack to work around toJSON functions breaking JSON serialization. This most notably can happen
30
+ // when the Prototype library is used. As much as we'd like to just delete these we have to
31
+ // save them to restore them later since tests may be run after this and user code may depend on
32
+ // this stuff.
33
+ stringifiableTypes = [Object, Array, String];
34
+ if (typeof Hash !== 'undefined' && Hash.prototype) { stringifiableTypes.push(Hash); }
35
+ toJSONFunctions = [];
36
+
37
+ for (i = 0; i < stringifiableTypes.length; i++) {
38
+ if (stringifiableTypes[i].prototype.toJSON) {
39
+ toJSONFunctions[i] = stringifiableTypes[i].prototype.toJSON;
40
+ delete stringifiableTypes[i].prototype.toJSON;
41
+ } else {
42
+ toJSONFunctions[i] = undefined;
43
+ }
44
+ }
45
+
46
+ // The 3rd argument here adds spaces to the generated JSON string. This is necessary
47
+ // for YAML parsers that are not compliant with YAML 1.2 (the version where YAML became
48
+ // a true superset of JSON).
49
+ jsonString = JSON.stringify(object, null, 1);
50
+
51
+ // Restore any toJSON functions we removed earlier
52
+ for (i = 0; i < toJSONFunctions.length; i++) {
53
+ if (toJSONFunctions[i]) {
54
+ stringifiableTypes[i].prototype.toJSON = toJSONFunctions[i];
55
+ }
56
+ }
57
+
58
+ return jsonString;
59
+ };
60
+
61
+
62
+ /* Test results that should be output to be picked up by the formatter, for live result updates
63
+ * while tests are still running. Not all runners may be able to do live updates but keeping this
64
+ * up to date enables that in case they are able to.
65
+ */
66
+ QUnited.testResultsPendingOutput = [];
67
+
68
+ /*
69
+ * Flag to indicate whether all of the QUnit tests have completed. This is set to true with the
70
+ * QUnit.done callback.
71
+ */
72
+ QUnited.testsHaveCompleted = false;
73
+
13
74
  QUnited.startCollectingTestResults = function() {
14
75
  // Various state we'll need while running the tests
15
76
  QUnited.modulesMap = {};
@@ -19,16 +80,18 @@ QUnited.startCollectingTestResults = function() {
19
80
  ///// Listen for QUnit events during tests
20
81
 
21
82
  QUnit.testStart(function(data) {
83
+ var moduleName = data.module || "(no module)",
84
+ module = QUnited.modulesMap[moduleName];
85
+
22
86
  currentTest = {
23
87
  name: data.name,
24
- assertion_data: [], // Ruby-style, since we'll be reading it with Ruby
88
+ module_name: moduleName,
89
+ assertion_data: [],
25
90
  start: new Date(),
26
91
  assertions: 0,
27
92
  file: QUnited.currentTestFile
28
93
  };
29
94
 
30
- var moduleName = data.module || "(no module)",
31
- module = QUnited.modulesMap[moduleName];
32
95
  if (!module) {
33
96
  module = {name: moduleName, tests: []};
34
97
  QUnited.modulesMap[moduleName] = module;
@@ -40,6 +103,8 @@ QUnited.startCollectingTestResults = function() {
40
103
  currentTest.duration = ((new Date()).getTime() - currentTest.start.getTime()) / 1000;
41
104
  currentTest.failed = data.failed;
42
105
  currentTest.total = data.total;
106
+
107
+ QUnited.testResultsPendingOutput.push(currentTest);
43
108
  });
44
109
 
45
110
  /*
@@ -51,6 +116,10 @@ QUnited.startCollectingTestResults = function() {
51
116
  currentTest.assertions++;
52
117
  currentTest.assertion_data.push(data);
53
118
  });
119
+
120
+ QUnit.done(function() {
121
+ QUnited.testsHaveCompleted = true;
122
+ });
54
123
  };
55
124
 
56
125
  /* Results as an Array of modules */
@@ -62,20 +131,7 @@ QUnited.collectedTestResults = function() {
62
131
 
63
132
  /* Module results as a JSON string */
64
133
  QUnited.collectedTestResultsAsJson = function() {
65
- // Prototype can mess up JSON.stringify and break results serialization. Get
66
- // rid of the bad bits if they are present.
67
- // http://stackoverflow.com/questions/710586/json-stringify-bizarreness
68
- if (window.Prototype) {
69
- delete Object.prototype.toJSON;
70
- delete Array.prototype.toJSON;
71
- delete Hash.prototype.toJSON;
72
- delete String.prototype.toJSON;
73
- }
74
-
75
- // The 3rd argument here adds spaces to the generated JSON string. This is necessary
76
- // for YAML parsers that are not compliant with YAML 1.2 (the version where YAML became
77
- // a true superset of JSON).
78
- return JSON.stringify(QUnited.collectedTestResults(), null, 1);
134
+ return QUnited.util.jsonStringify(QUnited.collectedTestResults());
79
135
  };
80
136
 
81
137
  })();
@@ -1,3 +1,4 @@
1
+ require 'qunited/driver/results_collector'
1
2
  require 'qunited/driver/base'
2
3
  require 'qunited/driver/rhino/rhino'
3
4
  require 'qunited/driver/phantomjs/phantomjs'
@@ -0,0 +1,42 @@
1
+ module QUnited
2
+ module Formatter
3
+ class Base
4
+ attr_reader :driver_name, :output, :test_results
5
+
6
+ def initialize(options={})
7
+ @driver_name = options[:driver_name]
8
+ @output = options[:output] || $stdout
9
+ @test_results = []
10
+ end
11
+
12
+ # Called before we start running tests
13
+ def start
14
+ end
15
+
16
+ def test_passed(result)
17
+ @test_results << result
18
+ end
19
+
20
+ def test_failed(result)
21
+ @test_results << result
22
+ end
23
+
24
+ # Send arbitrary messages to the output stream
25
+ def message
26
+ output.puts message
27
+ end
28
+
29
+ # Called after all tests have run, before we summarize results
30
+ def stop
31
+ end
32
+
33
+ # Called after we have stopped running tests
34
+ def summarize
35
+ end
36
+
37
+ def close
38
+ output.close if IO === output && output != $stdout
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,90 @@
1
+ module QUnited
2
+ module Formatter
3
+ class Dots < Base
4
+ def start
5
+ super
6
+ output.print "\n# Running JavaScript tests with #{driver_name}:\n\n"
7
+ end
8
+
9
+ def test_passed(result)
10
+ super result
11
+ output.print '.'
12
+ end
13
+
14
+ def test_failed(result)
15
+ super result
16
+ output.print(result.error? ? 'E' : 'F')
17
+ end
18
+
19
+ def summarize
20
+ output.print "\n\n#{times_line}\n"
21
+ output.print failure_output
22
+ output.print "\n#{bottom_line}\n"
23
+ end
24
+
25
+ private
26
+
27
+ def failure_output
28
+ return '' unless total_failures > 0
29
+
30
+ all_failure_output = ''
31
+ count = 1
32
+ failures.each do |test|
33
+ test.assertions.reject { |a| a.passed? }.each do |assertion|
34
+ file_name_output = (test.file && !test.file.strip.empty?) ? " [#{test.file}]" : ''
35
+ msg = "\n " + (count ? "#{count.to_s}) " : "")
36
+ msg << "#{assertion.error? ? 'Error' : 'Failure'}:\n"
37
+ msg << "#{test.name} (#{test.module_name})#{file_name_output}\n"
38
+ msg << "#{assertion.message || 'Failed assertion, no message given.'}\n"
39
+
40
+ if assertion.data.key? :expected
41
+ msg << "Expected: #{assertion.expected.nil? ? 'null' : assertion.expected.inspect}\n"
42
+ msg << " Actual: #{assertion.actual.nil? ? 'null' : assertion.actual.inspect}\n"
43
+ end
44
+ all_failure_output << msg
45
+ count += 1
46
+ end
47
+ end
48
+
49
+ all_failure_output
50
+ end
51
+
52
+ def times_line
53
+ total_time = test_results.inject(0) { |total, result| total += result.duration }
54
+
55
+ tests_per = (total_time > 0) ? (total_tests / total_time) : total_tests
56
+ assertions_per = (total_time > 0) ? (total_assertions / total_time) : total_assertions
57
+
58
+ "Finished in #{"%.6g" % total_time} seconds, #{"%.6g" % tests_per} tests/s, " +
59
+ "#{"%.6g" % assertions_per} assertions/s."
60
+ end
61
+
62
+ def bottom_line
63
+ "#{total_tests} tests, #{total_assertions} assertions, " +
64
+ "#{total_failures} failures, #{total_errors} errors"
65
+ end
66
+
67
+ def failures
68
+ test_results.select { |tr| tr.failed? }
69
+ end
70
+
71
+ def total_tests
72
+ test_results.size
73
+ end
74
+
75
+ # Test failures, not assertion failures
76
+ def total_failures
77
+ failures.size
78
+ end
79
+
80
+ # Test errors, not assertion errors
81
+ def total_errors
82
+ test_results.select { |tr| tr.error? }.size
83
+ end
84
+
85
+ def total_assertions
86
+ test_results.inject(0) { |total, result| total += result.assertions.size}
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,2 @@
1
+ require 'qunited/formatter/base'
2
+ require 'qunited/formatter/dots'
@@ -0,0 +1,85 @@
1
+ module QUnited
2
+
3
+ # Contains results data from a QUnit JavaScript test. Useful for passing data
4
+ # to formatters.
5
+ class QUnitTestResult
6
+ class AssertionResult
7
+ attr_accessor :data
8
+
9
+ def initialize(assertion_data)
10
+ @data = assertion_data
11
+ end
12
+
13
+ def message
14
+ data[:message] || 'Failed assertion, no message given.'
15
+ end
16
+
17
+ def result
18
+ if data[:result]
19
+ :passed
20
+ else
21
+ data[:message] =~ /^Died on test/ ? :error : :failed
22
+ end
23
+ end
24
+
25
+ def passed?; result == :passed end
26
+ def failed?; result == :failed end
27
+ def error?; result == :error end
28
+
29
+ [:expected, :actual].each do |prop|
30
+ define_method(prop) do
31
+ data[prop]
32
+ end
33
+ end
34
+ end
35
+
36
+ def self.from_json(json)
37
+ self.new clean_up_result(YAML.load(json))
38
+ end
39
+
40
+ attr_accessor :data
41
+
42
+ def initialize(test_data)
43
+ @data = test_data
44
+ end
45
+
46
+ def passed?; result == :passed end
47
+ def failed?; result == :failed end
48
+ def error?; result == :error end
49
+
50
+ def result
51
+ @result ||= if assertions.find { |a| a.error? }
52
+ :error
53
+ else
54
+ assertions.find { |a| a.failed? } ? :failed : :passed
55
+ end
56
+ end
57
+
58
+ def assertions
59
+ @assertions ||= data[:assertion_data].map { |assertion_data| AssertionResult.new assertion_data }
60
+ end
61
+
62
+ [:name, :module_name, :duration, :file].each do |prop|
63
+ define_method(prop) do
64
+ data[prop]
65
+ end
66
+ end
67
+
68
+ private
69
+
70
+ # Turn String keys into Symbols and convert Strings representing dates
71
+ # and numbers into their appropriate objects.
72
+ def self.clean_up_result(test_result)
73
+ test_result = symbolize_keys(test_result)
74
+ test_result[:start] = DateTime.parse(test_result[:start])
75
+ test_result[:assertion_data].map! { |data| symbolize_keys data }
76
+ test_result
77
+ end
78
+
79
+ def self.symbolize_keys(hash)
80
+ new_hash = {}
81
+ hash.keys.each { |key| new_hash[key.to_sym] = hash[key] }
82
+ new_hash
83
+ end
84
+ end
85
+ end
@@ -8,21 +8,23 @@ module QUnited
8
8
  # :qunited
9
9
  attr_accessor :name
10
10
 
11
- # Glob pattern to match JavaScript source files (and any dependencies). Note that the
12
- # order will be indeterminate so if your JavaScript files must be included in a particular
13
- # order you will have to use source_files=(files_array).
14
- #
15
- # If an array of files is set with source_files=(files_array) then this will be ignored.
16
- attr_accessor :source_files_pattern
11
+ # <b>DEPRECATED:</b> Please use <tt>source_files=</tt>, which now takes either an array of files
12
+ # or a glob pattern string.
13
+ def source_files_pattern=(pattern)
14
+ warn 'source_files_pattern= is deprecated in QUnited rake task config, use source_files= with a pattern'
15
+ @source_files = pattern
16
+ end
17
17
 
18
18
  # Array of JavaScript source files (and any dependencies). These will be loaded in order
19
19
  # before loading the QUnit tests.
20
20
  attr_accessor :source_files
21
21
 
22
- # Glob pattern to match QUnit test files.
23
- #
24
- # If an array of test files is set with test_files=(files) then this will be ignored.
25
- attr_accessor :test_files_pattern
22
+ # <b>DEPRECATED:</b> Please use <tt>test_files=</tt>, which now takes either an array of files
23
+ # or a glob pattern string.
24
+ def test_files_pattern=(pattern)
25
+ warn 'test_files_pattern= is deprecated in QUnited rake task config, use test_files= with a pattern'
26
+ @test_files = pattern
27
+ end
26
28
 
27
29
  # Array of QUnit test files.
28
30
  attr_accessor :test_files
@@ -36,6 +38,12 @@ module QUnited
36
38
  # true
37
39
  attr_accessor :verbose
38
40
 
41
+ # Fail rake task when tests fail.
42
+ #
43
+ # default:
44
+ # true
45
+ attr_accessor :fail_on_test_failure
46
+
39
47
  # The port to use if running the server.
40
48
  #
41
49
  # default:
@@ -45,6 +53,7 @@ module QUnited
45
53
  def initialize(*args)
46
54
  @name = args.shift || :qunited
47
55
  @verbose = true
56
+ @fail_on_test_failure = true
48
57
  @server_port = nil
49
58
 
50
59
  yield self if block_given?
@@ -60,12 +69,19 @@ module QUnited
60
69
  elsif test_files_to_run.empty?
61
70
  puts "No QUnit test files matching #{test_files_pattern} could be found"
62
71
  else
63
- begin
64
- puts command if verbose
65
- success = system(command)
66
- rescue
72
+ command = test_command
73
+ puts command if verbose
74
+ success = system(command)
75
+
76
+ unless success
77
+ if $?.exitstatus == 10
78
+ # 10 is our test failure status code
79
+ fail 'QUnit tests failed' if @fail_on_test_failure
80
+ else
81
+ # Other status codes should mean unexpected crashes
82
+ fail 'Something went wrong when running tests with QUnited'
83
+ end
67
84
  end
68
- raise "#{command} failed" unless success
69
85
  end
70
86
  end
71
87
  end
@@ -85,18 +101,23 @@ module QUnited
85
101
 
86
102
  private
87
103
 
88
- def command
104
+ def test_command
89
105
  cmd = 'qunited'
90
106
  cmd << " --driver #{driver}" if driver
91
107
  cmd << " #{source_files_to_include.join(' ')} -- #{test_files_to_run.join(' ')}"
92
108
  end
93
109
 
94
110
  def source_files_to_include
95
- source_files || pattern_to_filelist(source_files_pattern)
111
+ files_array source_files
96
112
  end
97
113
 
98
114
  def test_files_to_run
99
- test_files || pattern_to_filelist(test_files_pattern)
115
+ files_array test_files
116
+ end
117
+
118
+ # Force convert to array of files if glob pattern
119
+ def files_array(files)
120
+ files.is_a?(Array) ? files : pattern_to_filelist(files.to_s)
100
121
  end
101
122
 
102
123
  def pattern_to_filelist(pattern)
@@ -11,14 +11,13 @@ module QUnited
11
11
  end
12
12
 
13
13
  def run
14
- driver_class = resolve_driver_class
14
+ driver_class, formatter_class = resolve_driver_class, resolve_formatter_class
15
15
  driver = driver_class.new(js_source_files, js_test_files)
16
-
17
- puts "\n# Running JavaScript tests with #{driver.name}:\n\n"
16
+ driver.formatter = formatter_class.new({:driver_name => driver.name})
18
17
 
19
18
  results = driver.run
20
- puts results
21
- results.to_i
19
+
20
+ results.all? { |r| r.passed? }
22
21
  end
23
22
 
24
23
  def resolve_driver_class
@@ -34,7 +33,6 @@ module QUnited
34
33
  elsif !driver_class.available?
35
34
  raise UsageError, "#{driver_class} driver specified, but not available"
36
35
  end
37
- driver_class
38
36
  end
39
37
 
40
38
  driver_class ||= best_available_driver
@@ -42,13 +40,32 @@ module QUnited
42
40
  driver_class
43
41
  end
44
42
 
43
+ def resolve_formatter_class
44
+ if options[:formatter]
45
+ begin
46
+ formatter_class = get_formatter(options[:formatter])
47
+ rescue NameError
48
+ raise UsageError, "#{options[:formatter].to_s} does not exist"
49
+ end
50
+
51
+ raise UsageError, "#{formatter_class} formatter not found" unless formatter_class
52
+ end
53
+
54
+ formatter_class || ::QUnited::Formatter::Dots
55
+ end
56
+
45
57
  def get_driver(klass)
46
58
  if ::QUnited::Driver.constants.reject { |d| d == :Base }.include?(klass.to_s)
47
59
  ::QUnited::Driver.const_get(klass.to_s)
48
60
  end
49
61
  end
50
62
 
51
- # Get the runner that we will be using to run the JavaScript tests.
63
+ def get_formatter(klass)
64
+ if ::QUnited::Formatter.constants.reject { |d| d == :Base }.include?(klass.to_s)
65
+ ::QUnited::Formatter.const_get(klass.to_s)
66
+ end
67
+ end
68
+
52
69
  def best_available_driver
53
70
  DRIVERS_PRIORITY.map { |driver| get_driver(driver) }.find { |driver| driver.available? }
54
71
  end
@@ -1,3 +1,3 @@
1
1
  module QUnited
2
- VERSION = '0.3.1'
2
+ VERSION = '0.4.0'
3
3
  end
data/lib/qunited.rb CHANGED
@@ -1,7 +1,8 @@
1
1
  require 'qunited/version'
2
- require 'qunited/results'
2
+ require 'qunited/qunit_test_result'
3
3
  require 'qunited/driver'
4
4
  require 'qunited/runner'
5
+ require 'qunited/formatter'
5
6
  require 'qunited/server'
6
7
  require 'qunited/application'
7
8
 
@@ -3,7 +3,7 @@ module("Math");
3
3
  test("Addition is hard", function() {
4
4
  expect(2);
5
5
  equal(1 + 1, 3, "This math is wrong");
6
- equal(2 + 2, 5, "This math is also wrong");
6
+ equal(2 + 2, null, "This expected null");
7
7
  });
8
8
 
9
9
  test("This expects the wrong number of assertions", function() {
data/test/test_helper.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  require 'rubygems'
2
2
  require 'minitest/autorun'
3
+ require 'mocha/setup'
3
4
 
4
5
  require File.join(File.dirname(__FILE__), *%w[.. lib qunited])
5
6