From b56b11d96aafa5cd9e2f004ae631348fba4574ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Fri, 1 Nov 2013 23:57:50 +0100 Subject: [PATCH] add qunit to autospec --- .../users/omniauth_callbacks_controller.rb | 6 +- lib/auth/default_current_user_provider.rb | 7 +- lib/autospec/base_runner.rb | 30 +- lib/autospec/formatter.rb | 57 ++-- lib/autospec/manager.rb | 241 ++++++++++++++ lib/autospec/qunit_runner.rb | 150 +++++++++ lib/autospec/reload_css.rb | 22 +- lib/autospec/rspec_runner.rb | 43 +++ lib/autospec/run-qunit.js | 175 ++++++++++ lib/autospec/runner.rb | 308 ------------------ lib/autospec/simple_runner.rb | 34 +- lib/autospec/spork_runner.rb | 46 +-- lib/demon/base.rb | 142 ++++++++ lib/demon/rails_autospec.rb | 25 ++ lib/demon/sidekiq.rb | 168 +--------- lib/tasks/autospec.rake | 21 +- spec/serializers/user_serializer_spec.rb | 1 - vendor/assets/javascripts/run-qunit.js | 16 +- 18 files changed, 921 insertions(+), 571 deletions(-) create mode 100644 lib/autospec/manager.rb create mode 100644 lib/autospec/qunit_runner.rb create mode 100644 lib/autospec/rspec_runner.rb create mode 100644 lib/autospec/run-qunit.js delete mode 100644 lib/autospec/runner.rb create mode 100644 lib/demon/base.rb create mode 100644 lib/demon/rails_autospec.rb diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb index a0d510aed0b..b629228188b 100644 --- a/app/controllers/users/omniauth_callbacks_controller.rb +++ b/app/controllers/users/omniauth_callbacks_controller.rb @@ -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") diff --git a/lib/auth/default_current_user_provider.rb b/lib/auth/default_current_user_provider.rb index 1eb90b22b7c..98430e32072 100644 --- a/lib/auth/default_current_user_provider.rb +++ b/lib/auth/default_current_user_provider.rb @@ -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) diff --git a/lib/autospec/base_runner.rb b/lib/autospec/base_runner.rb index b3e884c0c4d..30bd513a714 100644 --- a/lib/autospec/base_runner.rb +++ b/lib/autospec/base_runner.rb @@ -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 diff --git a/lib/autospec/formatter.rb b/lib/autospec/formatter.rb index 32bdd810595..d3ec0d892f6 100644 --- a/lib/autospec/formatter.rb +++ b/lib/autospec/formatter.rb @@ -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 diff --git a/lib/autospec/manager.rb b/lib/autospec/manager.rb new file mode 100644 index 00000000000..2d9760833d7 --- /dev/null +++ b/lib/autospec/manager.rb @@ -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 diff --git a/lib/autospec/qunit_runner.rb b/lib/autospec/qunit_runner.rb new file mode 100644 index 00000000000..3dae8a2f249 --- /dev/null +++ b/lib/autospec/qunit_runner.rb @@ -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 diff --git a/lib/autospec/reload_css.rb b/lib/autospec/reload_css.rb index ab4d9f74dc3..5c69bdf94d6 100644 --- a/lib/autospec/reload_css.rb +++ b/lib/autospec/reload_css.rb @@ -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 diff --git a/lib/autospec/rspec_runner.rb b/lib/autospec/rspec_runner.rb new file mode 100644 index 00000000000..fe5f23703e8 --- /dev/null +++ b/lib/autospec/rspec_runner.rb @@ -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 diff --git a/lib/autospec/run-qunit.js b/lib/autospec/run-qunit.js new file mode 100644 index 00000000000..15804f269de --- /dev/null +++ b/lib/autospec/run-qunit.js @@ -0,0 +1,175 @@ +// THIS FILE IS CALLED BY "qunit_runner.rb" IN AUTOSPEC + +if (phantom.args.length != 1) { + console.log("Usage: " + phantom.scriptName + " "); + 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; + }); + +}; diff --git a/lib/autospec/runner.rb b/lib/autospec/runner.rb deleted file mode 100644 index 7ba9cbb5295..00000000000 --- a/lib/autospec/runner.rb +++ /dev/null @@ -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 diff --git a/lib/autospec/simple_runner.rb b/lib/autospec/simple_runner.rb index d43cb32abb8..e94d3d93413 100644 --- a/lib/autospec/simple_runner.rb +++ b/lib/autospec/simple_runner.rb @@ -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 diff --git a/lib/autospec/spork_runner.rb b/lib/autospec/spork_runner.rb index 95b3525418f..fbd05d93ef5 100644 --- a/lib/autospec/spork_runner.rb +++ b/lib/autospec/spork_runner.rb @@ -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 diff --git a/lib/demon/base.rb b/lib/demon/base.rb new file mode 100644 index 00000000000..ad059c8878a --- /dev/null +++ b/lib/demon/base.rb @@ -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 diff --git a/lib/demon/rails_autospec.rb b/lib/demon/rails_autospec.rb new file mode 100644 index 00000000000..92d15a8fb25 --- /dev/null +++ b/lib/demon/rails_autospec.rb @@ -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 diff --git a/lib/demon/sidekiq.rb b/lib/demon/sidekiq.rb index f4820f35c88..392128e1edf 100644 --- a/lib/demon/sidekiq.rb +++ b/lib/demon/sidekiq.rb @@ -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,18 +10,15 @@ class Demon::Sidekiq < Demon::Base def after_fork require 'sidekiq/cli' - begin - # 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([]) - cli.run - rescue => e - STDERR.puts e.message - STDERR.puts e.backtrace.join("\n") - exit 1 - end - + # 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([]) + cli.run + rescue => e + STDERR.puts e.message + STDERR.puts e.backtrace.join("\n") + exit 1 end + end diff --git a/lib/tasks/autospec.rake b/lib/tasks/autospec.rake index c4ca2008c30..43455ac850c 100644 --- a/lib/tasks/autospec.rake +++ b/lib/tasks/autospec.rake @@ -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 diff --git a/spec/serializers/user_serializer_spec.rb b/spec/serializers/user_serializer_spec.rb index aac9a5228b3..26c24722bfb 100644 --- a/spec/serializers/user_serializer_spec.rb +++ b/spec/serializers/user_serializer_spec.rb @@ -28,7 +28,6 @@ describe UserSerializer do end it "has a name" do - puts json[:name] json[:name].should be_blank end end diff --git a/vendor/assets/javascripts/run-qunit.js b/vendor/assets/javascripts/run-qunit.js index 469c7705919..3f0272f39e1 100644 --- a/vendor/assets/javascripts/run-qunit.js +++ b/vendor/assets/javascripts/run-qunit.js @@ -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('.'); } });