diff --git a/contrib/ruby/jasmine_runner.rb b/contrib/ruby/jasmine_runner.rb new file mode 100644 index 00000000..633c89f6 --- /dev/null +++ b/contrib/ruby/jasmine_runner.rb @@ -0,0 +1,188 @@ +require 'socket' +require 'erb' + +module Jasmine + # this seemingly-over-complex method is necessary to get an open port on at least some of our Macs + def self.open_socket_on_unused_port + infos = Socket::getaddrinfo("localhost", nil, Socket::AF_UNSPEC, Socket::SOCK_STREAM, 0, Socket::AI_PASSIVE) + families = Hash[*infos.collect { |af, *_| af }.uniq.zip([]).flatten] + + return TCPServer.open('0.0.0.0', 0) if families.has_key?('AF_INET') + return TCPServer.open('::', 0) if families.has_key?('AF_INET6') + return TCPServer.open(0) + end + + def self.find_unused_port + socket = open_socket_on_unused_port + port = socket.addr[1] + socket.close + port + end + + class RunAdapter + def initialize(spec_files) + p "spec_files: #{spec_files}" + + @spec_files = spec_files + end + + def call(env) + spec_files = @spec_files + body = ERB.new(File.read(File.join(File.dirname(__FILE__), "run.html"))).result(binding) + [ + 200, + { 'Content-Type' => 'text/html' }, + body + ] + end + end + + class SimpleServer + def self.start(port, spec_dir, mappings) + require 'thin' + + config = { + '/run.html' => Jasmine::RunAdapter.new(spec_dir) + } + mappings.each do |from, to| + config[from] = Rack::File.new(to) + end + + app = Rack::URLMap.new(config) + + server_port = Jasmine::find_unused_port + Thin::Server.start('0.0.0.0', port, app) + end + end + + class SimpleClient + def initialize(selenium_host, selenium_port, selenium_browser_start_command, http_address) + require 'selenium' + @driver = Selenium::Client::Driver.new( + selenium_host, + selenium_port, + selenium_browser_start_command, + http_address + ) + @http_address = http_address + end + + def tests_have_finished? + @driver.get_eval("window.jasmine.getEnv().currentRunner.finished") == "true" + end + + def connect + @driver.start + @driver.open("/run.html") + end + + def disconnect + @driver.stop + end + + def run + until tests_have_finished? do + sleep 0.1 + end + + puts @driver.get_eval("window.getResults()") + failed_count = @driver.get_eval("window.jasmine.getEnv().currentRunner.getResults().failedCount").to_i + failed_count == 0 + end + + def eval_js(script) + escaped_script = "'" + script.gsub(/(['\\])/) { '\\' + $1 } + "'" + + result = @driver.get_eval("window.eval(#{escaped_script})") + JSON.parse("[#{result}]")[0] + end + end + + class Runner + def initialize(selenium_jar_path, spec_files, dir_mappings) + @selenium_jar_path = selenium_jar_path + @spec_files = spec_files + @dir_mappings = dir_mappings + + @selenium_pid = nil + @jasmine_server_pid = nil + end + + def start + start_servers + @client = Jasmine::SimpleClient.new("localhost", @selenium_server_port, "*firefox", "http://localhost:#{@jasmine_server_port}/") + @client.connect + end + + def stop + @client.disconnect + stop_servers + end + + def server_is_listening_on(hostname, port) + require 'socket' + begin + socket = TCPSocket.open(hostname, port) + rescue Errno::ECONNREFUSED + return false + end + socket.close + true + end + + def wait_for_listener(port, name = "required process", seconds_to_wait = 10) + seconds_waited = 0 + until server_is_listening_on "localhost", port + sleep 1 + puts "Waiting for #{name} on #{port}..." + raise "#{name} didn't show up on port #{port} after #{seconds_to_wait} seconds." if (seconds_waited += 1) >= seconds_to_wait + end + end + + def start_servers + @jasmine_server_port = Jasmine::find_unused_port + @selenium_server_port = Jasmine::find_unused_port + + @selenium_pid = fork do + Process.setpgrp + exec "java -jar #{@selenium_jar_path} -port #{@selenium_server_port} > /dev/null 2>&1" + end + puts "selenium started. pid is #{@selenium_pid}" + + @jasmine_server_pid = fork do + Process.setpgrp + Jasmine::SimpleServer.start(@jasmine_server_port, @spec_files, @dir_mappings) + exit! 0 + end + puts "jasmine server started. pid is #{@jasmine_server_pid}" + + wait_for_listener(@selenium_server_port, "selenium server") + wait_for_listener(@jasmine_server_port, "jasmine server") + end + + def kill_process_group(process_group_id, signal="TERM") + Process.kill signal, -process_group_id # negative pid means kill process group. (see man 2 kill) + end + + def stop_servers + puts "shutting down the servers..." + kill_process_group(@selenium_pid) if @selenium_pid + kill_process_group(@jasmine_server_pid) if @jasmine_server_pid + end + + def run + begin + start + puts "servers are listening on their ports -- running the test script..." + tests_passed = @client.run + ensure + stop + end + return tests_passed + end + + def eval_js(script) + @client.eval_js(script) + end + end +end diff --git a/contrib/ruby/jasmine_spec_builder.rb b/contrib/ruby/jasmine_spec_builder.rb new file mode 100644 index 00000000..0071a49d --- /dev/null +++ b/contrib/ruby/jasmine_spec_builder.rb @@ -0,0 +1,142 @@ +require File.expand_path(File.join(File.dirname(__FILE__), "jasmine_runner.rb")) + +module Jasmine + + class SpecBuilder + attr_accessor :suites + + def initialize(spec_files, runner) + @spec_files = spec_files + @runner = runner + end + + def start + guess_example_locations + + @runner.start + load_suite_info + @spec_results = {} + end + + def stop + @runner.stop + end + + def script_path + File.expand_path(__FILE__) + end + + def guess_example_locations + @example_locations = {} + + example_name_parts = [] + previous_indent_level = 0 + @spec_files.each do |filename| + line_number = 1 + File.open(filename, "r") do |file| + file.readlines.each do |line| + match = /^(\s*)(describe|it)\s*\(\s*["'](.*)["']\s*,\s*function/.match(line) + if (match) + indent_level = match[1].length / 2 + example_name = match[3] + example_name_parts[indent_level] = example_name + + full_example_name = example_name_parts.slice(0, indent_level + 1).join(" ") + @example_locations[full_example_name] = "#{filename}:#{line_number}: in `it'" + end + line_number += 1 + end + end + end + end + + def load_suite_info + while !eval_js('jsApiReporter.started') do + sleep 0.1 + end + + @suites = eval_js('JSON.stringify(jsApiReporter.suites)') + end + + def results_for(spec_id) + spec_id = spec_id.to_s + return @spec_results[spec_id] if @spec_results[spec_id] + + @spec_results[spec_id] = eval_js("JSON.stringify(jsApiReporter.results[#{spec_id}])") + while @spec_results[spec_id].nil? do + sleep 0.1 + @spec_results[spec_id] = eval_js("JSON.stringify(jsApiReporter.results[#{spec_id}])") + end + + @spec_results[spec_id] + end + + def declare_suites + me = self + suites.each do |suite| + declare_suite(self, suite) + end + end + + def declare_suite(parent, suite) + me = self + parent.describe suite["name"] do + suite["children"].each do |suite_or_spec| + type = suite_or_spec["type"] + if type == "suite" + me.declare_suite(self, suite_or_spec) + elsif type == "spec" + me.declare_spec(self, suite_or_spec) + else + raise "unknown type #{type} for #{suite_or_spec.inspect}" + end + end + end + end + + def declare_spec(parent, spec) + me = self + example_name = spec["name"] + + backtrace = @example_locations[parent.description + " " + example_name] + parent.it example_name, {}, backtrace do + me.report_spec(spec["id"]) + end + end + + def report_spec(spec_id) + spec_results = results_for(spec_id) + + out = "" + messages = spec_results['messages'].each do |message| + case + when message["type"] == "MessageResult" + puts message["text"] + puts "\n" + else + STDERR << message["message"] + STDERR << "\n" + + out << message["message"] + out << "\n" + + unless message["passed"] + stack_trace = message["trace"]["stack"] + STDERR << stack_trace.gsub(/\(.*\)@http:\/\/localhost:[0-9]+\/specs\//, "/spec/") + STDERR << "\n" + end + end + + end + fail out unless spec_results['result'] == 'passed' + puts out + end + + private + + def eval_js(js) + @runner.eval_js(js) + end + end +end + \ No newline at end of file diff --git a/contrib/ruby/run.html b/contrib/ruby/run.html new file mode 100644 index 00000000..7ddbe809 --- /dev/null +++ b/contrib/ruby/run.html @@ -0,0 +1,29 @@ + + + + + Jasmine suite + + + + + + + + + <% spec_files.each do |spec_file| %> + + <% end %> + + +
+ + diff --git a/lib/TrivialReporter.js b/lib/TrivialReporter.js index 5d10f5d0..39b0c5e8 100644 --- a/lib/TrivialReporter.js +++ b/lib/TrivialReporter.js @@ -35,7 +35,7 @@ jasmine.TrivialReporter.prototype.reportSuiteResults = function(suite) { jasmine.TrivialReporter.prototype.reportSpecResults = function(spec) { var specDiv = this.createDom('div', { - className: spec.getResults().passed() ? 'spec passed' : 'spec failed' + className: spec.getResults().passed ? 'spec passed' : 'spec failed' }, spec.getFullName()); var resultItems = spec.getResults().getItems(); diff --git a/lib/jasmine.js b/lib/jasmine.js index 851d6068..63020c29 100644 --- a/lib/jasmine.js +++ b/lib/jasmine.js @@ -587,6 +587,7 @@ jasmine.Env = function() { }; this.nextSpecId_ = 0; + this.nextSuiteId_ = 0; this.equalityTesters_ = []; }; @@ -869,7 +870,7 @@ jasmine.Reporter.prototype.reportSpecResults = function(spec) { }; //noinspection JSUnusedLocalSymbols -jasmine.Reporter.prototype.log = function (str) { +jasmine.Reporter.prototype.log = function(str) { }; /** @@ -914,7 +915,8 @@ jasmine.Block.prototype.fail = function(e) { */ jasmine.Suite = function(env, description, specDefinitions, parentSuite) { jasmine.ActionCollection.call(this, env); - + + this.id = env.nextSuiteId_++; this.description = description; this.specs = this.actions; this.parentSuite = parentSuite; @@ -948,6 +950,9 @@ jasmine.Suite.prototype.afterEach = function(afterEachFunction) { jasmine.Suite.prototype.getResults = function() { var results = new jasmine.NestedResults(); + results.description = this.description; + results.id = this.id; + for (var i = 0; i < this.specs.length; i++) { results.rollupCounts(this.specs[i].getResults()); } @@ -984,7 +989,7 @@ jasmine.PrettyPrinter.prototype.format = function(value) { } else if (value instanceof jasmine.Matchers.Any) { this.emitScalar(value.toString()); } else if (typeof value === 'string') { - this.emitScalar("'" + value + "'"); + this.emitString(value); } else if (typeof value === 'function') { this.emitScalar('Function'); } else if (typeof value.nodeType === 'number') { @@ -1019,6 +1024,7 @@ jasmine.PrettyPrinter.prototype.iterateObject = function(obj, fn) { jasmine.PrettyPrinter.prototype.emitArray = jasmine.unimplementedMethod_; jasmine.PrettyPrinter.prototype.emitObject = jasmine.unimplementedMethod_; jasmine.PrettyPrinter.prototype.emitScalar = jasmine.unimplementedMethod_; +jasmine.PrettyPrinter.prototype.emitString = jasmine.unimplementedMethod_; jasmine.StringPrettyPrinter = function() { jasmine.PrettyPrinter.call(this); @@ -1031,6 +1037,10 @@ jasmine.StringPrettyPrinter.prototype.emitScalar = function(value) { this.append(value); }; +jasmine.StringPrettyPrinter.prototype.emitString = function(value) { + this.append("'" + value + "'"); +}; + jasmine.StringPrettyPrinter.prototype.emitArray = function(array) { this.append('[ '); for (var i = 0; i < array.length; i++) { @@ -1097,6 +1107,64 @@ jasmine.MultiReporter.prototype.addReporter = function(reporter) { })(functionName); } })(); +/** JavaScript API reporter. + * + * @constructor + */ +jasmine.JsApiReporter = function() { + this.started = false; + this.finished = false; + this.suites = []; + this.results = {}; +}; + +jasmine.JsApiReporter.prototype.reportRunnerStarting = function(runner) { + this.started = true; + + for (var i = 0; i < runner.suites.length; i++) { + var suite = runner.suites[i]; + this.suites.push(this.summarize_(suite)); + } +}; + +jasmine.JsApiReporter.prototype.summarize_ = function(suiteOrSpec) { + var summary = { + id: suiteOrSpec.id, + name: suiteOrSpec.description, + type: suiteOrSpec instanceof jasmine.Suite ? 'suite' : 'spec', + children: [] + }; + + if (suiteOrSpec.specs) { + for (var i = 0; i < suiteOrSpec.specs.length; i++) { + summary.children.push(this.summarize_(suiteOrSpec.specs[i])); + } + } + + return summary; +}; + +//noinspection JSUnusedLocalSymbols +jasmine.JsApiReporter.prototype.reportRunnerResults = function(runner) { + this.finished = true; +}; + +//noinspection JSUnusedLocalSymbols +jasmine.JsApiReporter.prototype.reportSuiteResults = function(suite) { +}; + +//noinspection JSUnusedLocalSymbols +jasmine.JsApiReporter.prototype.reportSpecResults = function(spec) { + this.results[spec.id] = { + messages: spec.results.getItems(), + result: spec.results.failedCount > 0 ? "failed" : "passed" + }; +}; + +//noinspection JSUnusedLocalSymbols +jasmine.JsApiReporter.prototype.log = function(str) { +}; + /** * Holds results for a set of Jasmine spec. Allows for the results array to hold another jasmine.NestedResults * @@ -1174,9 +1242,9 @@ jasmine.NestedResults.prototype.addResult = function(result) { /** * @returns {Boolean} True if everything below passed */ -jasmine.NestedResults.prototype.passed = function() { +jasmine.NestedResults.prototype.__defineGetter__('passed', function() { return this.passedCount === this.totalCount; -}; +}); /** * Runner * diff --git a/spec/bootstrap.html b/spec/bootstrap.html deleted file mode 100755 index 39fe6514..00000000 --- a/spec/bootstrap.html +++ /dev/null @@ -1,52 +0,0 @@ - - - - Jasmine Tests - - - - - - - - - - - - - - - - - - - - - - - - - -

- Running all Jasmine Test Suites -

-
- - -
-
- - -
- - - - - - diff --git a/spec/bootstrap.js b/spec/bootstrap.js deleted file mode 100755 index 342b10e5..00000000 --- a/spec/bootstrap.js +++ /dev/null @@ -1,146 +0,0 @@ -var createElement = function(tag, attrs) { - var element = document.createElement(tag); - for (var attr in attrs) { - element[attr] = attrs[attr]; - } - return element; -}; - -// Bootstrap Test Reporter function -var Reporter = function () { - this.total = 0; - this.passes = 0; - this.fails = 0; - this.start = new Date(); -}; - -Reporter.prototype.toJSON = function(object) { - return JSON.stringify(object); -}; - -Reporter.prototype.test = function (result, message) { - this.total++; - - if (result) { - this.passes++; - iconElement = document.getElementById('icons'); - iconElement.appendChild(createElement('img', {src: '../images/go-16.png'})); - } - else { - this.fails++; - var fails_report = document.getElementById('fails'); - fails_report.style.display = ""; - - var iconElement = document.getElementById('icons'); - iconElement.appendChild(createElement('img', {src: '../images/fail-16.png'})); - - var failMessages = document.getElementById('fail_messages'); - var newFail = createElement('p', {'class': 'fail'}); - newFail.innerHTML = message; - failMessages.appendChild(newFail); - } -}; - -Reporter.prototype.summary = function () { - var el = createElement('p', {'class': ((this.fails > 0) ? 'fail_in_summary' : '') }); - el.innerHTML = this.total + ' expectations, ' + this.passes + ' passing, ' + this.fails + ' failed in ' + (new Date().getTime() - this.start.getTime()) + "ms."; - - var summaryElement = document.getElementById('results_summary'); - summaryElement.appendChild(el); - summaryElement.style.display = ""; -}; - - -var reporter = new Reporter(); - -function runSuite(filename) { - console.log(filename); - var suite = jasmine.include(filename); - suite.execute(); - emitSuiteResults(filename, suite); -} - -function emitSpecResults(testName, spec) { - var results = spec.results.getItems(); - reporter.test(results.length > 0, testName + ": should have results, got " + results.length); - - for (var i = 0; i < results.length; i++) { - reporter.test(results[i].passed === true, testName + ':' + spec.getFullName() + ": expectation number " + i + " failed: " + results[i].message); - } -} - -function emitSuiteResults(testName, suite) { - for (var j = 0; j < suite.specs.length; j++) { - var specOrSuite = suite.specs[j]; - - if (specOrSuite instanceof jasmine.Suite) { - emitSuiteResults(testName, specOrSuite); - } else { - emitSpecResults(testName, specOrSuite); - } - } -} - -var testExplodes = function () { - var suite = describe('exploding', function () { - it('should throw an exception when this.explodes is called inside a spec', function() { - var exceptionMessage = false; - - try { - this.explodes(); - } - catch (e) { - exceptionMessage = e; - } - expect(exceptionMessage).toEqual('explodes function should not have been called'); - }); - - }); - suite.execute(); - - emitSuiteResults('testExplodes', suite); -}; - -function newJasmineEnv() { - return new jasmine.Env(); -} - -var testRunner = function() { -}; - -var testRunnerFinishCallback = function () { - var env = newJasmineEnv(); - var foo = 0; - - env.currentRunner.finish(); - - reporter.test((env.currentRunner.finished === true), - "Runner finished flag was not set."); - - env.currentRunner.finishCallback = function () { - foo++; - }; - - env.currentRunner.finish(); - - reporter.test((env.currentRunner.finished === true), - "Runner finished flag was not set."); - reporter.test((foo === 1), - "Runner finish callback was not called"); -}; - -var runTests = function () { - document.getElementById('spinner').style.display = ""; - - runSuite('suites/PrettyPrintTest.js'); - runSuite('suites/MatchersTest.js'); - runSuite('suites/SpecRunningTest.js'); - runSuite('suites/NestedResultsTest.js'); - runSuite('suites/ReporterTest.js'); - runSuite('suites/RunnerTest.js'); - runSuite('suites/SpyTest.js'); - runSuite('suites/ExceptionsTest.js'); - - reporter.summary(); - document.getElementById('spinner').style.display = "none"; -}; \ No newline at end of file diff --git a/spec/runner.html b/spec/runner.html index 9f9076e4..0fef41dd 100644 --- a/spec/runner.html +++ b/spec/runner.html @@ -3,7 +3,6 @@ Jasmine Test Runner - @@ -25,43 +24,43 @@ - + - + .stackTrace { + white-space: pre; + font-size: .8em; + margin-left: 10px; + } + + diff --git a/src/Env.js b/src/Env.js index fd4dc70d..851adce2 100644 --- a/src/Env.js +++ b/src/Env.js @@ -18,6 +18,7 @@ jasmine.Env = function() { }; this.nextSpecId_ = 0; + this.nextSuiteId_ = 0; this.equalityTesters_ = []; }; diff --git a/src/JsApiReporter.js b/src/JsApiReporter.js new file mode 100644 index 00000000..1501123a --- /dev/null +++ b/src/JsApiReporter.js @@ -0,0 +1,58 @@ +/** JavaScript API reporter. + * + * @constructor + */ +jasmine.JsApiReporter = function() { + this.started = false; + this.finished = false; + this.suites = []; + this.results = {}; +}; + +jasmine.JsApiReporter.prototype.reportRunnerStarting = function(runner) { + this.started = true; + + for (var i = 0; i < runner.suites.length; i++) { + var suite = runner.suites[i]; + this.suites.push(this.summarize_(suite)); + } +}; + +jasmine.JsApiReporter.prototype.summarize_ = function(suiteOrSpec) { + var summary = { + id: suiteOrSpec.id, + name: suiteOrSpec.description, + type: suiteOrSpec instanceof jasmine.Suite ? 'suite' : 'spec', + children: [] + }; + + if (suiteOrSpec.specs) { + for (var i = 0; i < suiteOrSpec.specs.length; i++) { + summary.children.push(this.summarize_(suiteOrSpec.specs[i])); + } + } + + return summary; +}; + +//noinspection JSUnusedLocalSymbols +jasmine.JsApiReporter.prototype.reportRunnerResults = function(runner) { + this.finished = true; +}; + +//noinspection JSUnusedLocalSymbols +jasmine.JsApiReporter.prototype.reportSuiteResults = function(suite) { +}; + +//noinspection JSUnusedLocalSymbols +jasmine.JsApiReporter.prototype.reportSpecResults = function(spec) { + this.results[spec.id] = { + messages: spec.results.getItems(), + result: spec.results.failedCount > 0 ? "failed" : "passed" + }; +}; + +//noinspection JSUnusedLocalSymbols +jasmine.JsApiReporter.prototype.log = function(str) { +}; + diff --git a/src/NestedResults.js b/src/NestedResults.js index c5fabf7f..4e4842fa 100644 --- a/src/NestedResults.js +++ b/src/NestedResults.js @@ -75,6 +75,6 @@ jasmine.NestedResults.prototype.addResult = function(result) { /** * @returns {Boolean} True if everything below passed */ -jasmine.NestedResults.prototype.passed = function() { +jasmine.NestedResults.prototype.__defineGetter__('passed', function() { return this.passedCount === this.totalCount; -}; +}); diff --git a/src/PrettyPrinter.js b/src/PrettyPrinter.js index 9d8f4f96..ff793a91 100644 --- a/src/PrettyPrinter.js +++ b/src/PrettyPrinter.js @@ -28,7 +28,7 @@ jasmine.PrettyPrinter.prototype.format = function(value) { } else if (value instanceof jasmine.Matchers.Any) { this.emitScalar(value.toString()); } else if (typeof value === 'string') { - this.emitScalar("'" + value + "'"); + this.emitString(value); } else if (typeof value === 'function') { this.emitScalar('Function'); } else if (typeof value.nodeType === 'number') { @@ -63,6 +63,7 @@ jasmine.PrettyPrinter.prototype.iterateObject = function(obj, fn) { jasmine.PrettyPrinter.prototype.emitArray = jasmine.unimplementedMethod_; jasmine.PrettyPrinter.prototype.emitObject = jasmine.unimplementedMethod_; jasmine.PrettyPrinter.prototype.emitScalar = jasmine.unimplementedMethod_; +jasmine.PrettyPrinter.prototype.emitString = jasmine.unimplementedMethod_; jasmine.StringPrettyPrinter = function() { jasmine.PrettyPrinter.call(this); @@ -75,6 +76,10 @@ jasmine.StringPrettyPrinter.prototype.emitScalar = function(value) { this.append(value); }; +jasmine.StringPrettyPrinter.prototype.emitString = function(value) { + this.append("'" + value + "'"); +}; + jasmine.StringPrettyPrinter.prototype.emitArray = function(array) { this.append('[ '); for (var i = 0; i < array.length; i++) { diff --git a/src/Reporter.js b/src/Reporter.js index 0e4350ce..06f8c344 100644 --- a/src/Reporter.js +++ b/src/Reporter.js @@ -22,6 +22,6 @@ jasmine.Reporter.prototype.reportSpecResults = function(spec) { }; //noinspection JSUnusedLocalSymbols -jasmine.Reporter.prototype.log = function (str) { +jasmine.Reporter.prototype.log = function(str) { }; diff --git a/src/Suite.js b/src/Suite.js index 60ac46c0..9113ecab 100644 --- a/src/Suite.js +++ b/src/Suite.js @@ -9,7 +9,8 @@ */ jasmine.Suite = function(env, description, specDefinitions, parentSuite) { jasmine.ActionCollection.call(this, env); - + + this.id = env.nextSuiteId_++; this.description = description; this.specs = this.actions; this.parentSuite = parentSuite; @@ -43,6 +44,9 @@ jasmine.Suite.prototype.afterEach = function(afterEachFunction) { jasmine.Suite.prototype.getResults = function() { var results = new jasmine.NestedResults(); + results.description = this.description; + results.id = this.id; + for (var i = 0; i < this.specs.length; i++) { results.rollupCounts(this.specs[i].getResults()); }