Move ruby logic out of jasmine contrib (for now)
This commit is contained in:
parent
e3585b79f0
commit
2b9667c82e
@ -29,6 +29,9 @@ Gem::Specification.new do |s|
|
|||||||
"jasmine/lib/json2.js",
|
"jasmine/lib/json2.js",
|
||||||
"lib/jasmine-ruby/jasmine_helper.rb",
|
"lib/jasmine-ruby/jasmine_helper.rb",
|
||||||
"lib/jasmine-ruby/jasmine_meta_spec.rb",
|
"lib/jasmine-ruby/jasmine_meta_spec.rb",
|
||||||
|
"lib/jasmine-ruby/jasmine_runner.rb",
|
||||||
|
"lib/jasmine-ruby/jasmine_spec_builder.rb",
|
||||||
|
"lib/jasmine-ruby/run.html",
|
||||||
"lib/jasmine-ruby.rb",
|
"lib/jasmine-ruby.rb",
|
||||||
"tasks/jasmine.rake",
|
"tasks/jasmine.rake",
|
||||||
"templates/example_spec.js",
|
"templates/example_spec.js",
|
||||||
|
@ -1 +1,3 @@
|
|||||||
require 'jasmine-ruby/jasmine_helper'
|
require 'jasmine-ruby/jasmine_helper'
|
||||||
|
require 'jasmine-ruby/jasmine_runner'
|
||||||
|
require 'jasmine-ruby/jasmine_spec_builder'
|
293
lib/jasmine-ruby/jasmine_runner.rb
Normal file
293
lib/jasmine-ruby/jasmine_runner.rb
Normal file
@ -0,0 +1,293 @@
|
|||||||
|
require 'socket'
|
||||||
|
require 'erb'
|
||||||
|
require 'json'
|
||||||
|
|
||||||
|
module Jasmine
|
||||||
|
def self.root
|
||||||
|
File.expand_path(File.join(File.dirname(__FILE__), '../../jasmine'))
|
||||||
|
end
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
def self.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 self.wait_for_listener(port, name = "required process", seconds_to_wait = 10)
|
||||||
|
time_out_at = Time.now + seconds_to_wait
|
||||||
|
until server_is_listening_on "localhost", port
|
||||||
|
sleep 0.1
|
||||||
|
puts "Waiting for #{name} on #{port}..."
|
||||||
|
raise "#{name} didn't show up on port #{port} after #{seconds_to_wait} seconds." if Time.now > time_out_at
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.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 self.cachebust(files, root_dir="", replace=nil, replace_with=nil)
|
||||||
|
require 'digest/md5'
|
||||||
|
files.collect do |file_name|
|
||||||
|
real_file_name = replace && replace_with ? file_name.sub(replace, replace_with) : file_name
|
||||||
|
begin
|
||||||
|
digest = Digest::MD5.hexdigest(File.read("#{root_dir}#{real_file_name}"))
|
||||||
|
rescue
|
||||||
|
digest = "MISSING-FILE"
|
||||||
|
end
|
||||||
|
"#{file_name}?cachebust=#{digest}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class RunAdapter
|
||||||
|
def initialize(spec_files_or_proc, options = {})
|
||||||
|
@spec_files_or_proc = Jasmine.files(spec_files_or_proc) || []
|
||||||
|
@jasmine_files = Jasmine.files(options[:jasmine_files]) || [
|
||||||
|
"/__JASMINE_ROOT__/lib/" + File.basename(Dir.glob("#{Jasmine.root}/lib/jasmine*.js").first),
|
||||||
|
"/__JASMINE_ROOT__/lib/TrivialReporter.js",
|
||||||
|
"/__JASMINE_ROOT__/lib/json2.js",
|
||||||
|
"/__JASMINE_ROOT__/lib/consolex.js",
|
||||||
|
]
|
||||||
|
@stylesheets = ["/__JASMINE_ROOT__/lib/jasmine.css"] + (Jasmine.files(options[:stylesheets]) || [])
|
||||||
|
@spec_helpers = Jasmine.files(options[:spec_helpers]) || []
|
||||||
|
end
|
||||||
|
|
||||||
|
def call(env)
|
||||||
|
run
|
||||||
|
end
|
||||||
|
|
||||||
|
def run
|
||||||
|
stylesheets = @stylesheets
|
||||||
|
spec_helpers = @spec_helpers
|
||||||
|
spec_files = @spec_files_or_proc
|
||||||
|
|
||||||
|
jasmine_files = @jasmine_files
|
||||||
|
jasmine_files = jasmine_files.call if jasmine_files.respond_to?(:call)
|
||||||
|
|
||||||
|
css_files = @stylesheets
|
||||||
|
|
||||||
|
|
||||||
|
body = ERB.new(File.read(File.join(File.dirname(__FILE__), "run.html"))).result(binding)
|
||||||
|
[
|
||||||
|
200,
|
||||||
|
{ 'Content-Type' => 'text/html' },
|
||||||
|
body
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
class Redirect
|
||||||
|
def initialize(url)
|
||||||
|
@url = url
|
||||||
|
end
|
||||||
|
|
||||||
|
def call(env)
|
||||||
|
[
|
||||||
|
302,
|
||||||
|
{ 'Location' => @url },
|
||||||
|
[]
|
||||||
|
]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class JsAlert
|
||||||
|
def call(env)
|
||||||
|
[
|
||||||
|
200,
|
||||||
|
{ 'Content-Type' => 'application/javascript' },
|
||||||
|
"document.write('<p>Couldn\\'t load #{env["PATH_INFO"]}!</p>');"
|
||||||
|
]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class FocusedSuite
|
||||||
|
def initialize(spec_files_or_proc, options)
|
||||||
|
@spec_files_or_proc = Jasmine.files(spec_files_or_proc) || []
|
||||||
|
@options = options
|
||||||
|
end
|
||||||
|
|
||||||
|
def call(env)
|
||||||
|
spec_files = @spec_files_or_proc
|
||||||
|
matching_specs = spec_files.select {|spec_file| spec_file =~ /#{Regexp.escape(env["PATH_INFO"])}/ }.compact
|
||||||
|
if !matching_specs.empty?
|
||||||
|
run_adapter = Jasmine::RunAdapter.new(matching_specs, @options)
|
||||||
|
run_adapter.run
|
||||||
|
else
|
||||||
|
[
|
||||||
|
200,
|
||||||
|
{ 'Content-Type' => 'application/javascript' },
|
||||||
|
"document.write('<p>Couldn\\'t find any specs matching #{env["PATH_INFO"]}!</p>');"
|
||||||
|
]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
class SimpleServer
|
||||||
|
def self.start(port, spec_files_or_proc, mappings, options = {})
|
||||||
|
require 'thin'
|
||||||
|
config = {
|
||||||
|
'/__suite__' => Jasmine::FocusedSuite.new(spec_files_or_proc, options),
|
||||||
|
'/run.html' => Jasmine::Redirect.new('/'),
|
||||||
|
'/' => Jasmine::RunAdapter.new(spec_files_or_proc, options)
|
||||||
|
}
|
||||||
|
mappings.each do |from, to|
|
||||||
|
config[from] = Rack::File.new(to)
|
||||||
|
end
|
||||||
|
|
||||||
|
config["/__JASMINE_ROOT__"] = Rack::File.new(Jasmine.root)
|
||||||
|
|
||||||
|
app = Rack::Cascade.new([
|
||||||
|
Rack::URLMap.new(config),
|
||||||
|
JsAlert.new
|
||||||
|
])
|
||||||
|
|
||||||
|
begin
|
||||||
|
Thin::Server.start('0.0.0.0', port, app)
|
||||||
|
rescue RuntimeError => e
|
||||||
|
raise e unless e.message == 'no acceptor'
|
||||||
|
raise RuntimeError.new("A server is already running on port #{port}")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class SimpleClient
|
||||||
|
def initialize(selenium_host, selenium_port, selenium_browser_start_command, http_address)
|
||||||
|
require 'selenium/client'
|
||||||
|
@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("/")
|
||||||
|
end
|
||||||
|
|
||||||
|
def disconnect
|
||||||
|
@driver.stop
|
||||||
|
end
|
||||||
|
|
||||||
|
def run
|
||||||
|
until tests_have_finished? do
|
||||||
|
sleep 0.1
|
||||||
|
end
|
||||||
|
|
||||||
|
puts @driver.get_eval("window.results()")
|
||||||
|
failed_count = @driver.get_eval("window.jasmine.getEnv().currentRunner.results().failedCount").to_i
|
||||||
|
failed_count == 0
|
||||||
|
end
|
||||||
|
|
||||||
|
def eval_js(script)
|
||||||
|
escaped_script = "'" + script.gsub(/(['\\])/) { '\\' + $1 } + "'"
|
||||||
|
|
||||||
|
result = @driver.get_eval(" try { eval(#{escaped_script}, window); } catch(err) { window.eval(#{escaped_script}); }")
|
||||||
|
JSON.parse("[#{result}]")[0]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class Runner
|
||||||
|
def initialize(selenium_jar_path, spec_files, dir_mappings, options={})
|
||||||
|
@selenium_jar_path = selenium_jar_path
|
||||||
|
@spec_files = spec_files
|
||||||
|
@dir_mappings = dir_mappings
|
||||||
|
@options = options
|
||||||
|
|
||||||
|
@browser = options[:browser] ? options[:browser].delete(:browser) : 'firefox'
|
||||||
|
@selenium_pid = nil
|
||||||
|
@jasmine_server_pid = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def start
|
||||||
|
start_servers
|
||||||
|
@client = Jasmine::SimpleClient.new("localhost", @selenium_server_port, "*#{@browser}", "http://localhost:#{@jasmine_server_port}/")
|
||||||
|
@client.connect
|
||||||
|
end
|
||||||
|
|
||||||
|
def stop
|
||||||
|
@client.disconnect
|
||||||
|
stop_servers
|
||||||
|
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, @options)
|
||||||
|
exit! 0
|
||||||
|
end
|
||||||
|
puts "jasmine server started. pid is #{@jasmine_server_pid}"
|
||||||
|
|
||||||
|
Jasmine::wait_for_listener(@selenium_server_port, "selenium server")
|
||||||
|
Jasmine::wait_for_listener(@jasmine_server_port, "jasmine server")
|
||||||
|
end
|
||||||
|
|
||||||
|
def stop_servers
|
||||||
|
puts "shutting down the servers..."
|
||||||
|
Jasmine::kill_process_group(@selenium_pid) if @selenium_pid
|
||||||
|
Jasmine::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
|
||||||
|
|
||||||
|
def self.files(f)
|
||||||
|
result = f
|
||||||
|
result = result.call if result.respond_to?(:call)
|
||||||
|
result
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
152
lib/jasmine-ruby/jasmine_spec_builder.rb
Normal file
152
lib/jasmine-ruby/jasmine_spec_builder.rb
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
require 'enumerator'
|
||||||
|
module Jasmine
|
||||||
|
|
||||||
|
class SpecBuilder
|
||||||
|
attr_accessor :suites
|
||||||
|
|
||||||
|
def initialize(spec_files, runner)
|
||||||
|
@spec_files = spec_files
|
||||||
|
@runner = runner
|
||||||
|
@spec_ids = []
|
||||||
|
end
|
||||||
|
|
||||||
|
def start
|
||||||
|
guess_example_locations
|
||||||
|
|
||||||
|
@runner.start
|
||||||
|
load_suite_info
|
||||||
|
wait_for_suites_to_finish_running
|
||||||
|
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
|
||||||
|
started = Time.now
|
||||||
|
while !eval_js('jsApiReporter && jsApiReporter.started') do
|
||||||
|
raise "couldn't connect to Jasmine after 60 seconds" if (started + 60 < Time.now)
|
||||||
|
sleep 0.1
|
||||||
|
end
|
||||||
|
|
||||||
|
@suites = eval_js('JSON.stringify(jsApiReporter.suites())')
|
||||||
|
end
|
||||||
|
|
||||||
|
def results_for(spec_id)
|
||||||
|
@spec_results ||= load_results
|
||||||
|
@spec_results[spec_id.to_s]
|
||||||
|
end
|
||||||
|
|
||||||
|
def load_results
|
||||||
|
@spec_results = {}
|
||||||
|
@spec_ids.each_slice(50) do |slice|
|
||||||
|
@spec_results.merge!(eval_js("JSON.stringify(jsApiReporter.resultsForSpecs(#{JSON.generate(slice)}))"))
|
||||||
|
end
|
||||||
|
@spec_results
|
||||||
|
end
|
||||||
|
|
||||||
|
def wait_for_suites_to_finish_running
|
||||||
|
puts "Waiting for suite to finish in browser ..."
|
||||||
|
while !eval_js('jsApiReporter.finished') do
|
||||||
|
sleep 0.1
|
||||||
|
end
|
||||||
|
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"]
|
||||||
|
@spec_ids << spec["id"]
|
||||||
|
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
|
||||||
|
unless message["message"] =~ /^Passed.$/
|
||||||
|
STDERR << message["message"]
|
||||||
|
STDERR << "\n"
|
||||||
|
|
||||||
|
out << message["message"]
|
||||||
|
out << "\n"
|
||||||
|
end
|
||||||
|
|
||||||
|
if !message["passed"] && message["trace"]["stack"]
|
||||||
|
stack_trace = message["trace"]["stack"].gsub(/<br \/>/, "\n").gsub(/<\/?b>/, " ")
|
||||||
|
STDERR << stack_trace.gsub(/\(.*\)@http:\/\/localhost:[0-9]+\/specs\//, "/spec/")
|
||||||
|
STDERR << "\n"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
fail out unless spec_results['result'] == 'passed'
|
||||||
|
puts out unless out.empty?
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def eval_js(js)
|
||||||
|
@runner.eval_js(js)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
47
lib/jasmine-ruby/run.html
Normal file
47
lib/jasmine-ruby/run.html
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
|
||||||
|
<html xml:lang="en" xmlns="http://www.w3.org/1999/xhtml">
|
||||||
|
<head>
|
||||||
|
<meta content="text/html;charset=UTF-8" http-equiv="Content-Type"/>
|
||||||
|
<title>Jasmine suite</title>
|
||||||
|
<% css_files.each do |css_file| %>
|
||||||
|
<link rel="stylesheet" href="<%= css_file %>" type="text/css" media="screen"/>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<% jasmine_files.each do |jasmine_file| %>
|
||||||
|
<script src="<%= jasmine_file %>" type="text/javascript"></script>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<% spec_helpers.each do |spec_helper| %>
|
||||||
|
<script src="<%= spec_helper %>" type="text/javascript"></script>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
var jsApiReporter;
|
||||||
|
(function() {
|
||||||
|
var jasmineEnv = jasmine.getEnv();
|
||||||
|
|
||||||
|
jsApiReporter = new jasmine.JsApiReporter();
|
||||||
|
var trivialReporter = new jasmine.TrivialReporter();
|
||||||
|
|
||||||
|
jasmineEnv.addReporter(jsApiReporter);
|
||||||
|
jasmineEnv.addReporter(trivialReporter);
|
||||||
|
|
||||||
|
jasmineEnv.specFilter = function(spec) {
|
||||||
|
return trivialReporter.specFilter(spec);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.onload = function() {
|
||||||
|
jasmineEnv.execute();
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<% spec_files.each do |spec_file| %>
|
||||||
|
<script src="<%= spec_file %>" type="text/javascript"></script>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="jasmine_content"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -4,7 +4,6 @@ namespace :jasmine do
|
|||||||
if File.exist?(helper_overrides)
|
if File.exist?(helper_overrides)
|
||||||
require helper_overrides
|
require helper_overrides
|
||||||
end
|
end
|
||||||
require File.expand_path(File.join(JasmineHelper.root, "contrib/ruby/jasmine_spec_builder"))
|
|
||||||
|
|
||||||
desc "Run continuous integration tests"
|
desc "Run continuous integration tests"
|
||||||
require "spec"
|
require "spec"
|
||||||
|
Loading…
Reference in New Issue
Block a user