add qunit to autospec

This commit is contained in:
Régis Hanol 2013-11-01 23:57:50 +01:00
parent e679ba97a3
commit b56b11d96a
18 changed files with 921 additions and 571 deletions

View File

@ -62,16 +62,12 @@ class Users::OmniauthCallbacksController < ApplicationController
BUILTIN_AUTH.each do |authenticator|
if authenticator.name == name
raise Discourse::InvalidAccess.new("provider is not enabled") unless SiteSetting.send("enable_#{name}_logins?")
return authenticator
end
end
Discourse.auth_providers.each do |provider|
if provider.name == name
return provider.authenticator
end
return provider.authenticator if provider.name == name
end
raise Discourse::InvalidAccess.new("provider is not found")

View File

@ -2,10 +2,9 @@ require_dependency "auth/current_user_provider"
class Auth::DefaultCurrentUserProvider
CURRENT_USER_KEY = "_DISCOURSE_CURRENT_USER"
API_KEY = "_DISCOURSE_API"
TOKEN_COOKIE = "_t"
CURRENT_USER_KEY ||= "_DISCOURSE_CURRENT_USER"
API_KEY ||= "_DISCOURSE_API"
TOKEN_COOKIE ||= "_t"
# do all current user initialization here
def initialize(env)

View File

@ -1,22 +1,36 @@
module Autospec
class BaseRunner
def run(args, specs)
end
def abort
end
def reload
# used when starting the runner - preloading happens here
def start(opts = {})
end
# indicates whether tests are running
def running?
true
end
def start
# launch a batch of specs/tests
def run(specs)
end
# used when we need to reload the whole application
def reload
end
# used to abort the current run
def abort
end
def failed_specs
[]
end
# used to stop the runner
def stop
end
end
end

View File

@ -1,39 +1,46 @@
require "rspec/core/formatters/base_formatter"
require "rspec/core/formatters/base_text_formatter"
module Autospec; end
class Autospec::Formatter < RSpec::Core::Formatters::BaseFormatter
def dump_summary(duration, total, failures, pending)
# failed_specs = examples.delete_if{|e| e.execution_result[:status] != "failed"}.map{|s| s.metadata[:location]}
class Autospec::Formatter < RSpec::Core::Formatters::BaseTextFormatter
# # if this fails don't kill everything
# begin
# FileUtils.mkdir_p('tmp')
# File.open("./tmp/rspec_result","w") do |f|
# f.puts failed_specs.join("\n")
# end
# rescue
# # nothing really we can do, at least don't kill the test runner
# end
RSPEC_RESULT = "./tmp/rspec_result"
def initialize(output)
super
FileUtils.mkdir_p("tmp") unless Dir.exists?("tmp")
end
def start(count)
FileUtils.mkdir_p('tmp')
@fail_file = File.open("./tmp/rspec_result","w")
super(count)
def start(example_count)
super
File.delete(RSPEC_RESULT) if File.exists?(RSPEC_RESULT)
@fail_file = File.open(RSPEC_RESULT,"w")
end
def example_passed(example)
super
output.print success_color(".")
end
def example_pending(example)
super
output.print pending_color("*")
end
def example_failed(example)
super
output.print failure_color("F")
@fail_file.puts(example.metadata[:location] + " ")
@fail_file.flush
end
def start_dump
super
output.puts
end
def close
@fail_file.close
super
end
def example_failed(example)
@fail_file.puts example.metadata[:location]
@fail_file.flush
super(example)
end
end

241
lib/autospec/manager.rb Normal file
View File

@ -0,0 +1,241 @@
require "listen"
require "thread"
require "fileutils"
require "autospec/reload_css"
require "autospec/base_runner"
module Autospec; end
class Autospec::Manager
def self.run(opts={})
self.new.run(opts)
end
def initialize
@queue = []
@mutex = Mutex.new
@signal = ConditionVariable.new
end
def run(opts = {})
@runners = [ruby_runner, javascript_runner]
Signal.trap("HUP") { stop_runners; exit }
Signal.trap("INT") { stop_runners; exit }
ensure_all_specs_will_run
start_runners
start_service_queue
listen_for_changes
puts "Press [ENTER] to stop the current run"
while @runners.any?(&:running?)
STDIN.gets
process_queue
end
rescue => e
fail(e, "failed in run")
ensure
stop_runners
end
private
def ruby_runner
if ENV["SPORK"]
require "autospec/spork_runner"
Autospec::SporkRunner.new
else
require "autospec/simple_runner"
Autospec::SimpleRunner.new
end
end
def javascript_runner
require "autospec/qunit_runner"
Autospec::QunitRunner.new
end
def ensure_all_specs_will_run
@runners.each do |runner|
@queue << ['spec', 'spec', runner] unless @queue.any? { |f, s, r| s == "spec" && r == runner }
end
end
[:start, :stop, :abort].each do |verb|
define_method("#{verb}_runners") do
@runners.each(&verb)
end
end
def start_service_queue
Thread.new do
while true
thread_loop
end
end
end
# the main loop, will run the specs in the queue till one fails or the queue is empty
def thread_loop
@mutex.synchronize do
current = @queue.first
last_failed = false
last_failed = process_spec(current) if current
# stop & wait for the queue to have at least one item or when there's been a failure
@signal.wait(@mutex) if @queue.length == 0 || last_failed
end
rescue => e
fail(e, "failed in main loop")
end
# will actually run the spec and check whether the spec has failed or not
def process_spec(current)
has_failed = false
# retrieve the instance of the runner
runner = current[2]
# actually run the spec (blocking call)
result = runner.run(current[1]).to_i
if result == 0
# remove the spec from the queue
@queue.shift
else
has_failed = true
if result > 0
focus_on_failed_tests(current)
ensure_all_specs_will_run
end
end
has_failed
end
def focus_on_failed_tests(current)
runner = current[2]
# we only want 1 focus in the queue
@queue.shift if current[0] == "focus"
# focus on the first 10 failed specs
failed_specs = runner.failed_specs[0..10]
# focus on the failed specs
@queue.unshift ["focus", failed_specs.join(" "), runner] if failed_specs.length > 0
end
def listen_for_changes(opts = {})
options = {
ignore: /^public|^lib\/autospec/,
relative_paths: true,
}
if opts[:force_polling]
options[:force_polling] = true
options[:latency] = opts[:latency] || 3
end
Thread.start do
Listen.to('.', options) do |modified, added, removed|
process_change([modified, added].flatten.compact)
end
end
end
def process_change(files)
return if files.length == 0
specs = []
hit = false
files.each do |file|
@runners.each do |runner|
# reloaders
runner.reloaders.each do |k|
if k.match(file)
runner.reload
return
end
end
# watchers
runner.watchers.each do |k,v|
if m = k.match(file)
hit = true
spec = v ? (v.arity == 1 ? v.call(m) : v.call) : file
specs << [file, spec, runner] if File.exists?(spec) || Dir.exists?(spec)
end
end
end
# special watcher for styles/templates
Autospec::ReloadCss::WATCHERS.each do |k,v|
matches = []
matches << file if k.match(file)
Autospec::ReloadCss.run_on_change(matches) if matches.present?
end
end
queue_specs(specs) if hit
rescue => e
fail(e, "failed in watcher")
end
def queue_specs(specs)
if specs.length == 0
locked = @mutex.try_lock
if locked
@signal.signal
@mutex.unlock
end
return
else
abort_runners
end
@mutex.synchronize do
specs.each do |file, spec, runner|
# make sure there's no other instance of this spec in the queue
@queue.delete_if { |f, s, r| s.strip == spec.strip && r == runner }
# deal with focused specs
if @queue.first && @queue.first[0] == "focus"
focus = @queue.shift
@queue.unshift([file, spec, runner])
if focus[1].include?(spec) || file != spec
@queue.unshift(focus)
end
else
@queue.unshift([file, spec, runner])
end
end
@signal.signal
end
end
def process_queue
if @queue.length == 0
ensure_all_specs_will_run
@signal.signal
else
current = @queue.first
runner = current[2]
specs = runner.failed_specs
puts
puts
if specs.length == 0
puts "No specs have failed yet!"
puts
else
puts "The following specs have failed:"
specs.each { |s| puts s }
puts
specs = specs.map { |s| [s, s, runner] }
queue_specs(specs)
end
end
end
def fail(exception, message = nil)
puts message if message
puts exception.message
puts exception.backtrace.join("\n")
end
end

View File

@ -0,0 +1,150 @@
require "demon/rails_autospec"
module Autospec
class QunitRunner < BaseRunner
WATCHERS = {}
def self.watch(pattern, &blk); WATCHERS[pattern] = blk; end
def watchers; WATCHERS; end
# Discourse specific
watch(%r{^app/assets/javascripts/discourse/(.+)\.js$}) { |m| "test/javascripts/#{m[1]}_test.js" }
watch(%r{^app/assets/javascripts/admin/(.+)\.js$}) { |m| "test/javascripts/admin/#{m[1]}_test.js" }
watch(%r{^test/javascripts/.+\.js$})
RELOADERS = Set.new
def self.reload(pattern); RELOADERS << pattern; end
def reloaders; RELOADERS; end
# Discourse specific
reload(%r{^test/javascripts/fixtures/.+_fixtures\.js$})
reload(%r{^test/javascripts/(helpers|mixins)/.+\.js$})
reload("test/javascripts/test_helper.js")
require "socket"
class PhantomJsNotInstalled < Exception; end
def initialize
ensure_phantomjs_is_installed
end
def start
# ensure we can launch the rails server
unless port_available?(port)
puts "Port #{port} is not available"
puts "Either kill the process using that port or use the `TEST_SERVER_PORT` environment variable"
return
end
# start rails
start_rails_server
@running = true
end
def running?
@running
end
def run(specs)
puts "Running Qunit: #{specs}"
abort
qunit_url = "http://localhost:#{port}/qunit"
if specs != "spec" && specs.split.length == 1
module_name = try_to_find_module_name(specs.strip)
qunit_url << "?module=#{module_name}" if module_name
end
cmd = "phantomjs #{Rails.root}/lib/autospec/run-qunit.js \"#{qunit_url}\""
@pid = Process.spawn(cmd)
_, status = Process.wait2(@pid)
status.exitstatus
end
def reload
stop_rails_server
sleep 1
start_rails_server
end
def abort
if @pid
children_processes(@pid).each { |pid| kill_process(pid) }
kill_process(@pid)
@pid = nil
end
end
def failed_specs
specs = []
path = './tmp/qunit_result'
specs = File.readlines(path) if File.exist?(path)
specs
end
def stop
# kill phantomjs first
abort
stop_rails_server
@running = false
end
private
def ensure_phantomjs_is_installed
raise PhantomJsNotInstalled.new unless system("command -v phantomjs >/dev/null;")
end
def port_available?(port)
TCPServer.open(port).close
true
rescue Errno::EADDRINUSE
false
end
def port
@port ||= ENV["TEST_SERVER_PORT"] || 60099
end
def start_rails_server
Demon::RailsAutospec.start(1)
end
def stop_rails_server
Demon::RailsAutospec.stop
end
def children_processes(base = Process.pid)
process_tree = Hash.new { |hash, key| hash[key] = [key] }
Hash[*`ps -eo pid,ppid`.scan(/\d+/).map(&:to_i)].each do |pid, ppid|
process_tree[ppid] << process_tree[pid]
end
process_tree[base].flatten - [base]
end
def kill_process(pid)
return unless pid
Process.kill("INT", pid) rescue nil
while (Process.getpgid(pid) rescue nil)
sleep 0.001
end
end
def try_to_find_module_name(file)
return unless File.exists?(file)
File.open(file, "r").each_line do |line|
if m = /module\(['"]([^'"]+)/i.match(line)
return m[1]
end
end
end
end
end

View File

@ -1,23 +1,23 @@
module Autospec; end
class Autospec::ReloadCss
MATCHERS = {}
WATCHERS = {}
def self.watch(pattern, &blk)
MATCHERS[pattern] = blk
WATCHERS[pattern] = blk
end
watch(/tmp\/refresh_browser/)
# css, scss, sass or handlebars
watch(/\.css$/)
watch(/\.css\.erb$/)
watch(/\.sass$/)
watch(/\.scss$/)
watch(/\.sass\.erb$/)
watch(/\.ca?ss\.erb$/)
watch(/\.s[ac]ss$/)
watch(/\.handlebars$/)
def self.message_bus
MessageBus::Instance.new.tap do |bus|
bus.site_id_lookup do
# this is going to be dev the majority of the time, if you have multisite configured in dev stuff may be different
# this is going to be dev the majority of the time
# if you have multisite configured in dev stuff may be different
"default"
end
end
@ -26,13 +26,13 @@ class Autospec::ReloadCss
def self.run_on_change(paths)
paths.map! do |p|
hash = nil
fullpath = Rails.root.to_s + "/" + p
hash = Digest::MD5.hexdigest(File.read(fullpath)) if File.exists? fullpath
fullpath = "#{Rails.root}/#{p}"
hash = Digest::MD5.hexdigest(File.read(fullpath)) if File.exists?(fullpath)
p = p.sub /\.sass\.erb/, ""
p = p.sub /\.sass/, ""
p = p.sub /\.scss/, ""
p = p.sub /^app\/assets\/stylesheets/, "assets"
{name: p, hash: hash}
{ name: p, hash: hash }
end
message_bus.publish "/file-change", paths
end

View File

@ -0,0 +1,43 @@
module Autospec
class RspecRunner < BaseRunner
WATCHERS = {}
def self.watch(pattern, &blk); WATCHERS[pattern] = blk; end
def watchers; WATCHERS; end
# Discourse specific
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/components/#{m[1]}_spec.rb" }
# Rails example
watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
watch(%r{^app/(.+)(\.erb|\.haml)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" }
watch(%r{^spec/.+_spec\.rb$})
watch(%r{^spec/support/.+\.rb$}) { "spec" }
watch("app/controllers/application_controller.rb") { "spec/controllers" }
# Capybara request specs
watch(%r{^app/views/(.+)/.+\.(erb|haml)$}) { |m| "spec/requests/#{m[1]}_spec.rb" }
# Fabrication
watch(%r{^spec/fabricators/.+_fabricator\.rb$}) { "spec" }
RELOADERS = Set.new
def self.reload(pattern); RELOADERS << pattern; end
def reloaders; RELOADERS; end
# We need to reload the whole app when changing any of these files
reload('spec/spec_helper.rb')
reload('config/(.*).rb')
reload('app/helpers/(.*).rb')
def failed_specs
specs = []
path = './tmp/rspec_result'
specs = File.readlines(path) if File.exist?(path)
specs
end
end
end

175
lib/autospec/run-qunit.js Normal file
View File

@ -0,0 +1,175 @@
// THIS FILE IS CALLED BY "qunit_runner.rb" IN AUTOSPEC
if (phantom.args.length != 1) {
console.log("Usage: " + phantom.scriptName + " <URL>");
phantom.exit(1);
}
var system = require('system'),
fs = require('fs'),
page = require('webpage').create(),
QUNIT_RESULT = "./tmp/qunit_result";
if (fs.exists(QUNIT_RESULT) && fs.isFile(QUNIT_RESULT)) { fs.remove(QUNIT_RESULT); }
page.onConsoleMessage = function (message) {
// filter out Ember's debug messages
if (message.slice(0, 8) === "WARNING:") { return; }
if (message.slice(0, 6) === "DEBUG:") { return; }
console.log(message);
};
page.onCallback = function (message) {
// write to the result file
if (message.slice(0, 5) === "FILE:") { fs.write(QUNIT_RESULT, message.slice(6), "a"); }
// forward the message to the standard output
if (message.slice(0, 6) === "PRINT:") { system.stdout.write(message.slice(7)); }
};
page.start = new Date();
// -----------------------------------WARNING --------------------------------------
// calling "console.log" BELOW this line will go through the "page.onConsoleMessage"
// -----------------------------------WARNING --------------------------------------
page.open(phantom.args[0], function (status) {
if (status !== "success") {
console.log("\nNO NETWORK :(\n");
phantom.exit(1);
} else {
console.log("QUnit loaded in " + (new Date() - page.start) + " ms");
page.evaluate(colorizer);
page.evaluate(logQUnit);
// wait up to 60 seconds for QUnit to finish
var timeout = 60 * 1000,
start = Date.now();
var interval = setInterval(function() {
if (Date.now() - start > timeout) {
console.error("\nTIME OUT :(\n");
phantom.exit(1);
} else {
var qunitResult = page.evaluate(function() { return window.qunitResult; });
if (qunitResult) {
clearInterval(interval);
if (qunitResult.failed > 0) {
phantom.exit(1);
} else {
phantom.exit(0);
}
}
}
}, 250);
}
});
// https://github.com/jquery/qunit/pull/470
function colorizer() {
window.ANSI = {
colorMap: {
"red": "\u001b[31m",
"green": "\u001b[32m",
"blue": "\u001b[34m",
"end": "\u001b[0m"
},
highlightMap: {
"red": "\u001b[41m\u001b[37m", // change 37 to 30 for black text
"green": "\u001b[42m\u001b[30m",
"blue": "\u001b[44m\u001b[37m",
"end": "\u001b[0m"
},
highlight: function (text, color) {
var colorCode = this.highlightMap[color],
colorEnd = this.highlightMap.end;
return colorCode + text + colorEnd;
},
colorize: function (text, color) {
var colorCode = this.colorMap[color],
colorEnd = this.colorMap.end;
return colorCode + text + colorEnd;
}
};
};
function logQUnit() {
// keep track of error messages
var errors = {};
QUnit.begin(function () {
console.log("BEGIN");
});
QUnit.log(function (context) {
if (!context.result) {
var module = context.module,
test = context.name;
var assertion = {
message: context.message,
expected: context.expected,
actual: context.actual
};
if (!errors[module]) { errors[module] = {}; }
if (!errors[module][test]) { errors[module][test] = []; }
errors[module][test].push(assertion);
var fileName = context.source
.replace(/[^\S\n]+at[^\S\n]+/g, "")
.split("\n")[1]
.replace(/\?.+$/, "")
.replace(/^.+\/assets\//, "test/javascripts/");
window.callPhantom("FILE: " + fileName + " ");
}
});
QUnit.testDone(function (context) {
if (context.failed > 0) {
window.callPhantom("PRINT: " + ANSI.colorize("F", "red"));
} else {
window.callPhantom("PRINT: " + ANSI.colorize(".", "green"));
}
});
QUnit.done(function (context) {
console.log("\n");
// display failures
if (Object.keys(errors).length > 0) {
console.log("Failures:\n");
for (m in errors) {
var module = errors[m];
console.log("Module Failed: " + ANSI.highlight(m, "red"));
for (t in module) {
var test = module[t];
console.log(" Test Failed: " + t);
for (var a = 0; a < test.length; a++) {
var assertion = test[a];
console.log(" Assertion Failed: " + (assertion.message || ""));
if (assertion.expected) {
console.log(" Expected: " + assertion.expected);
console.log(" Actual: " + assertion.actual);
}
}
}
}
}
// display summary
console.log("\n");
console.log("Finished in " + (context.runtime / 1000) + " seconds");
var color = context.failed > 0 ? "red" : "green";
console.log(ANSI.colorize(context.total + " examples, " + context.failed + " failures", color));
// we're done
window.qunitResult = context;
});
};

View File

@ -1,308 +0,0 @@
require "drb/drb"
require "thread"
require "fileutils"
require "autospec/reload_css"
require "autospec/base_runner"
require "autospec/simple_runner"
require "autospec/spork_runner"
module Autospec; end
class Autospec::Runner
MATCHERS = {}
def self.watch(pattern, &blk)
MATCHERS[pattern] = blk
end
watch(%r{^spec/.+_spec\.rb$})
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/components/#{m[1]}_spec.rb" }
# Rails example
watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
watch(%r{^app/(.*)(\.erb|\.haml)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" }
watch(%r{^app/controllers/(.+)_(controller)\.rb$}) { |m| "spec/#{m[2]}s/#{m[1]}_#{m[2]}_spec.rb" }
watch(%r{^spec/support/(.+)\.rb$}) { "spec" }
watch("app/controllers/application_controller.rb") { "spec/controllers" }
# Capybara request specs
watch(%r{^app/views/(.+)/.*\.(erb|haml)$}) { |m| "spec/requests/#{m[1]}_spec.rb" }
# Fabrication
watch(%r{^spec/fabricators/(.+)_fabricator\.rb$}) { "spec" }
RELOAD_MATCHERS = Set.new
def self.watch_reload(pattern)
RELOAD_MATCHERS << pattern
end
watch_reload('spec/spec_helper.rb')
watch_reload('config/(.*).rb')
watch_reload(%r{app/helpers/(.*).rb})
def self.run(opts={})
self.new.run(opts)
end
def initialize
@queue = []
@mutex = Mutex.new
@signal = ConditionVariable.new
start_service_queue
end
def run(opts = {})
puts "Forced polling (slower) - inotify does not work on network filesystems, use local filesystem to avoid" if opts[:force_polling]
if ENV["SPORK"] == "0"
puts "Using Simple Runner"
@runner = Autospec::SimpleRunner.new
else
puts "Using Spork Runner"
@runner = Autospec::SporkRunner.new
end
@runner.start
Signal.trap("HUP") {@runner.stop; exit }
Signal.trap("SIGINT") {@runner.stop; exit }
options = {filter: /^app|^spec|^lib/, relative_paths: true}
if opts[:force_polling]
options[:force_polling] = true
options[:latency] = opts[:latency] || 3
end
Thread.start do
Listen.to('.', options ) do |modified, added, removed|
process_change([modified, added].flatten.compact)
end
end
@mutex.synchronize do
@queue << ['spec', 'spec']
@signal.signal
end
while @runner.running?
process_queue
end
rescue => e
puts e
puts e.backtrace
@runner.stop
end
def process_queue
STDIN.gets
if @queue.length == 0
@queue << ['spec', 'spec']
@signal.signal
else
specs = failed_specs(:delete => false)
puts
puts
if specs.length == 0
puts "No specs have failed yet!"
puts
else
puts "The following specs have failed: "
specs.each do |s|
puts s
end
puts
queue_specs(specs.zip specs)
end
end
end
def wait_for(timeout_milliseconds)
timeout = (timeout_milliseconds + 0.0) / 1000
finish = Time.now + timeout
t = Thread.new do
while Time.now < finish && !yield
sleep(0.001)
end
end
t.join rescue nil
end
def force_polling?
works = false
begin
require 'rb-inotify'
require 'fileutils'
n = INotify::Notifier.new
FileUtils.touch('./tmp/test_polling')
n.watch("./tmp", :modify, :attrib){ works = true }
quit = false
Thread.new do
while !works && !quit
if IO.select([n.to_io], [], [], 0.1)
n.process
end
end
end
sleep 0.01
FileUtils.touch('./tmp/test_polling')
wait_for(100) { works }
File.unlink('./tmp/test_polling')
n.stop
quit = true
rescue LoadError
#assume it works (mac)
works = true
end
!works
end
def process_change(files)
return unless files.length > 0
specs = []
hit = false
files.each do |file|
RELOAD_MATCHERS.each do |k|
if k.match(file)
@runner.reload
return
end
end
MATCHERS.each do |k,v|
if m = k.match(file)
hit = true
spec = v ? ( v.arity == 1 ? v.call(m) : v.call ) : file
if File.exists?(spec) || Dir.exists?(spec)
specs << [file, spec]
end
end
end
Autospec::ReloadCss::MATCHERS.each do |k,v|
matches = []
if k.match(file)
matches << file
end
Autospec::ReloadCss.run_on_change(matches) if matches.present?
end
end
queue_specs(specs) if hit
rescue => e
p "failed in watcher"
p e
p e.backtrace
end
def queue_specs(specs)
if specs.length == 0
locked = @mutex.try_lock
if locked
@signal.signal
@mutex.unlock
end
return
else
@runner.abort
end
@mutex.synchronize do
specs.each do |c,spec|
@queue.delete([c,spec])
if @queue.last && @queue.last[0] == "focus"
focus = @queue.pop
@queue << [c,spec]
if focus[1].include?(spec) || c != spec
@queue << focus
end
else
@queue << [c,spec]
end
end
@signal.signal
end
end
def thread_loop
@mutex.synchronize do
last_failed = false
current = @queue.last
if current
last_failed = process_spec(current[1])
end
wait = @queue.length == 0 || last_failed
@signal.wait(@mutex) if wait
end
rescue => e
p "DISASTA PASTA"
puts e
puts e.backtrace
end
def process_spec(spec)
last_failed = false
result = run_spec(spec)
if result == 0
@queue.pop
else
last_failed = true
if result.to_i > 0
focus_on_failed_tests
ensure_all_specs_will_run
end
end
last_failed
end
def start_service_queue
@worker ||= Thread.new do
while true
thread_loop
end
end
end
def focus_on_failed_tests
current = @queue.last
specs = failed_specs[0..10]
if current[0] == "focus"
@queue.pop
end
@queue << ["focus", specs.join(" ")]
end
def ensure_all_specs_will_run
unless @queue.any?{|s,t| t == 'spec'}
@queue.unshift(['spec','spec'])
end
end
def failed_specs(opts={:delete => true})
specs = []
path = './tmp/rspec_result'
if File.exist?(path)
specs = File.open(path) { |file| file.read.split("\n") }
File.delete(path) if opts[:delete]
end
specs
end
def run_spec(specs)
File.delete("tmp/rspec_result") if File.exists?("tmp/rspec_result")
args = ["-f", "progress", specs.split(" "),
"-r", "#{File.dirname(__FILE__)}/formatter.rb",
"-f", "Autospec::Formatter"].flatten
@runner.run(args, specs)
end
end

View File

@ -1,26 +1,36 @@
require "autospec/rspec_runner"
module Autospec
class SimpleRunner < BaseRunner
class SimpleRunner < RspecRunner
def run(specs)
puts "Running Rspec: " << specs
# kill previous rspec instance
abort
# we use our custom rspec formatter
args = ["-r", "#{File.dirname(__FILE__)}/formatter.rb",
"-f", "Autospec::Formatter", specs.split].flatten.join(" ")
# launch rspec
@pid = Process.spawn({"RAILS_ENV" => "test"}, "bundle exec rspec #{args}")
_, status = Process.wait2(@pid)
status.exitstatus
end
def abort
if @pid
Process.kill("SIGINT", @pid) rescue nil
while(Process.getpgid(@pid) rescue nil)
Process.kill("INT", @pid) rescue nil
while (Process.getpgid(@pid) rescue nil)
sleep 0.001
end
@pid = nil
end
end
def run(args, spec)
self.abort
puts "Running: " << spec
@pid = Process.spawn({"RAILS_ENV" => "test"}, "bundle exec rspec " << args.join(" "))
pid, status = Process.wait2(@pid)
status
def stop
abort
end
def stop
self.abort
end
end
end

View File

@ -1,5 +1,9 @@
require "drb/drb"
require "autospec/rspec_runner"
module Autospec
class SporkRunner < BaseRunner
class SporkRunner < RspecRunner
def start
if already_running?(pid_file)
@ -13,33 +17,38 @@ module Autospec
end
def running?
# launch a thread that will wait for spork to die
@monitor_thread ||=
Thread.new do
Process.wait(@spork_pid)
@spork_running = false
end
@spork_running
end
def stop
stop_spork
end
def run(args,specs)
def run(specs)
args = ["-r", "#{File.dirname(__FILE__)}/formatter.rb",
"-f", "Autospec::Formatter", specs.split].flatten
spork_service.run(args,$stderr,$stdout)
end
def abort
spork_service.abort
end
def reload
stop_spork
sleep 1
start_spork
end
def abort
spork_service.abort
end
def stop
stop_spork
end
private
def spork_pid_file
Rails.root + "tmp/pids/spork.pid"
end
@ -55,7 +64,7 @@ module Autospec
end
end
def write_pid_file(file,pid)
def write_pid_file(file, pid)
FileUtils.mkdir_p(Rails.root + "tmp/pids")
File.open(file,'w') do |f|
f.write(pid)
@ -67,25 +76,18 @@ module Autospec
end
def spork_service
unless @drb_listener_running
begin
DRb.start_service("druby://127.0.0.1:0")
rescue SocketError, Errno::EADDRNOTAVAIL
DRb.start_service("druby://:0")
end
@drb_listener_running = true
end
@spork_service ||= DRbObject.new_with_uri("druby://127.0.0.1:8989")
end
def stop_spork
pid = File.read(spork_pid_file).to_i
Process.kill("SIGTERM",pid)
end
def start_spork
if already_running?(spork_pid_file)
puts "Killing old orphan spork instance"
@ -101,7 +103,13 @@ module Autospec
running = spork_running?
sleep 0.01
end
end
def stop_spork
pid = File.read(spork_pid_file).to_i
Process.kill("SIGTERM", pid) rescue nil
end
end
end
end

142
lib/demon/base.rb Normal file
View File

@ -0,0 +1,142 @@
module Demon; end
# intelligent fork based demonizer
class Demon::Base
def self.start(count)
@demons ||= {}
count.times do |i|
(@demons["#{prefix}_#{i}"] ||= new(i)).start
end
end
def self.stop
return unless @demons
@demons.values.each do |demon|
demon.stop
end
end
def initialize(index)
@index = index
@pid = nil
@parent_pid = Process.pid
@monitor = nil
end
def pid_file
"#{Rails.root}/tmp/pids/#{self.class.prefix}_#{@index}.pid"
end
def stop
if @monitor
@monitor.kill
@monitor.join
@monitor = nil
end
if @pid
Process.kill("HUP",@pid)
@pid = nil
end
end
def start
if existing = already_running?
# should not happen ... so kill violently
Process.kill("TERM",existing)
end
return if @pid
if @pid = fork
write_pid_file
monitor_child
return
end
monitor_parent
establish_app
after_fork
end
def already_running?
if File.exists? pid_file
pid = File.read(pid_file).to_i
if alive?(pid)
return pid
end
end
nil
end
private
def monitor_child
@monitor ||= Thread.new do
while true
sleep 5
unless alive?(@pid)
STDERR.puts "#{@pid} died, restarting the process"
@pid = nil
start
end
end
end
end
def write_pid_file
FileUtils.mkdir_p(Rails.root + "tmp/pids")
File.open(pid_file,'w') do |f|
f.write(@pid)
end
end
def delete_pid_file
File.delete(pid_file)
end
def monitor_parent
Thread.new do
while true
unless alive?(@parent_pid)
Process.kill "QUIT", Process.pid
end
sleep 1
end
end
end
def alive?(pid)
begin
Process.getpgid(pid)
true
rescue Errno::ESRCH
false
end
end
def establish_app
ActiveRecord::Base.connection_handler.clear_active_connections!
ActiveRecord::Base.establish_connection
$redis.client.reconnect
Rails.cache.reconnect
MessageBus.after_fork
Signal.trap("HUP") do
begin
delete_pid_file
ensure
exit
end
end
# keep stuff simple for now
$stdout.reopen("/dev/null", "w")
$stderr.reopen("/dev/null", "w")
end
def after_fork
end
end

View File

@ -0,0 +1,25 @@
require "demon/base"
class Demon::RailsAutospec < Demon::Base
def self.prefix
"rails-autospec"
end
private
def after_fork
require "rack"
ENV["RAILS_ENV"] = "test"
Rack::Server.start(
:config => "config.ru",
:AccessLog => [],
:Port => ENV["TEST_SERVER_PORT"] || 60099,
)
rescue => e
STDERR.puts e.message
STDERR.puts e.backtrace.join("\n")
exit 1
end
end

View File

@ -1,148 +1,7 @@
module Demon; end
# intelligent fork based demonizer for sidekiq
class Demon::Base
def self.start(count)
@demons ||= {}
count.times do |i|
(@demons["#{prefix}_#{i}"] ||= new(i)).start
end
end
def self.stop
@demons.values.each do |demon|
demon.stop
end
end
def initialize(index)
@index = index
@pid = nil
@parent_pid = Process.pid
@monitor = nil
end
def pid_file
"#{Rails.root}/tmp/pids/#{self.class.prefix}_#{@index}.pid"
end
def stop
if @monitor
@monitor.kill
@monitor.join
@monitor = nil
end
if @pid
Process.kill("SIGHUP",@pid)
@pid = nil
end
end
def start
if existing = already_running?
# should not happen ... so kill violently
Process.kill("SIGTERM",existing)
end
return if @pid
if @pid = fork
write_pid_file
monitor_child
return
end
monitor_parent
establish_app
after_fork
end
def already_running?
if File.exists? pid_file
pid = File.read(pid_file).to_i
if alive?(pid)
return pid
end
end
nil
end
private
def monitor_child
@monitor ||= Thread.new do
while true
sleep 5
unless alive?(@pid)
STDERR.puts "#{@pid} died, restarting sidekiq"
@pid = nil
start
end
end
end
end
def write_pid_file
FileUtils.mkdir_p(Rails.root + "tmp/pids")
File.open(pid_file,'w') do |f|
f.write(@pid)
end
end
def delete_pid_file
File.delete(pid_file)
end
def monitor_parent
Thread.new do
while true
unless alive?(@parent_pid)
Process.kill "QUIT", Process.pid
end
sleep 1
end
end
end
def alive?(pid)
begin
Process.getpgid(pid)
true
rescue Errno::ESRCH
false
end
end
def establish_app
ActiveRecord::Base.connection_handler.clear_active_connections!
ActiveRecord::Base.establish_connection
$redis.client.reconnect
Rails.cache.reconnect
MessageBus.after_fork
Signal.trap("HUP") do
begin
delete_pid_file
ensure
exit
end
end
# keep stuff simple for now
$stdout.reopen("/dev/null", "w")
# $stderr.reopen("/dev/null", "w")
end
def after_fork
end
end
require "demon/base"
class Demon::Sidekiq < Demon::Base
def self.prefix
"sidekiq"
end
@ -151,9 +10,7 @@ class Demon::Sidekiq < Demon::Base
def after_fork
require 'sidekiq/cli'
begin
# Reload initializer cause it needs to run after sidekiq/cli
# was required
# Reload initializer cause it needs to run after sidekiq/cli was required
load Rails.root + "config/initializers/sidekiq.rb"
cli = Sidekiq::CLI.instance
cli.parse([])
@ -164,5 +21,4 @@ class Demon::Sidekiq < Demon::Base
exit 1
end
end
end

View File

@ -4,22 +4,17 @@
desc "Run all specs automatically as needed"
task "autospec" => :environment do
require 'autospec/manager'
if RUBY_PLATFORM.include?('linux')
require 'rb-inotify'
end
require 'listen'
puts "If file watching is not working you can force polling with: bundle exec rake autospec p l=3"
require 'autospec/runner'
force_polling = ARGV.any?{|a| a == "p" || a == "polling"}
latency = ((ARGV.find{|a| a =~ /l=|latency=/}||"").split("=")[1] || 3).to_i
force_polling = ARGV.any?{ |a| a == "p" || a == "polling" }
latency = ((ARGV.find{ |a| a =~ /l=|latency=/ } || "").split("=")[1] || 3).to_i
if force_polling
puts "polling has been forced (slower) checking every #{latency} #{"second".pluralize(latency)}"
puts "Polling has been forced (slower) - checking every #{latency} #{"second".pluralize(latency)}"
else
puts "If file watching is not working, you can force polling with: bundle exec rake autospec p l=3"
end
Autospec::Runner.run(force_polling: force_polling, latency: latency)
Autospec::Manager.run(force_polling: force_polling, latency: latency)
end

View File

@ -28,7 +28,6 @@ describe UserSerializer do
end
it "has a name" do
puts json[:name]
json[:name].should be_blank
end
end

View File

@ -19,16 +19,14 @@ page.onConsoleMessage = function(msg) {
if (msg.slice(0,8) === 'WARNING:') { return; }
if (msg.slice(0,6) === 'DEBUG:') { return; }
// Hack to access the print method
// If there's a better way to do this, please change
if (msg.slice(0,6) === 'PRINT:') {
print(msg.slice(7));
return;
}
console.log(msg);
};
page.onCallback = function (message) {
// forward the message to the standard output
system.stdout.write(message);
};
page.open(args[0], function(status) {
if (status !== 'success') {
console.error("Unable to access network");
@ -80,9 +78,9 @@ function logQUnit() {
var msg = " Test Failed: " + context.name + assertionErrors.join(" ");
testErrors.push(msg);
assertionErrors = [];
console.log('PRINT: F');
window.callPhantom('F');
} else {
console.log('PRINT: .');
window.callPhantom('.');
}
});