diff --git a/.gitignore b/.gitignore index 5875e07..44a1b60 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,5 @@ test/version_tmp tmp .tmp node_modules/ +.tmp-sprockets/ + diff --git a/bin/flowerbox b/bin/flowerbox index 1ffc87b..aa8ecf7 100755 --- a/bin/flowerbox +++ b/bin/flowerbox @@ -4,7 +4,9 @@ require 'flowerbox' require 'thor' class Flowerbox::CLI < Thor - desc "test DIR FILES...", "Run the specs found in spec dir, loading spec_helper.rb for configuration details" + include Thor::Actions + + desc "test", "Run the specs found in spec dir, loading spec_helper.rb for configuration details" method_options :pwd => :string, :env_options => nil, :runners => :string, :runner => :string, :verbose_server => false def test(dir = "spec/javascripts", *files) Dir.chdir(pwd) do @@ -22,6 +24,26 @@ class Flowerbox::CLI < Thor end end + desc "transplant DIR", "Convert an existing Jasmine gem-style project to Flowerbox" + def transplant(dir) + Flowerbox.transplant(dir) + end + + desc "clean", "Clean the Sprockets cache" + def clean + FileUtils.rm_rf(Flowerbox::CACHE_DIR) + puts "Sprockets cache cleaned." + end + + desc "plant", "Start a new Flowerbox project" + def plant(type, dir = nil) + env = Flowerbox::TestEnvironment.for(type) + + self.class.source_root(Flowerbox.path.join(env.plant_source)) + + directory('.', dir || env.plant_target) + end + default_task :test no_tasks do diff --git a/flowerbox.gemspec b/flowerbox.gemspec index aabfdec..85fc657 100644 --- a/flowerbox.gemspec +++ b/flowerbox.gemspec @@ -4,8 +4,8 @@ require File.expand_path('../lib/flowerbox/version', __FILE__) Gem::Specification.new do |gem| gem.authors = ["John Bintz"] gem.email = ["john@coswellproductions.com"] - gem.description = %q{TODO: Write a gem description} - gem.summary = %q{TODO: Write a gem summary} + gem.description = %q{No-nonsense JavaScript testing solution.} + gem.summary = %q{No-nonsense JavaScript testing solution.} gem.homepage = "" gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } @@ -23,4 +23,5 @@ Gem::Specification.new do |gem| gem.add_dependency 'selenium-webdriver' gem.add_dependency 'sinatra' gem.add_dependency 'rainbow' + gem.add_dependency 'sprockets-vendor_gems' end diff --git a/lib/assets/javascripts/flowerbox.js.coffee b/lib/assets/javascripts/flowerbox.js.coffee index a72b7a3..cfe100c 100644 --- a/lib/assets/javascripts/flowerbox.js.coffee +++ b/lib/assets/javascripts/flowerbox.js.coffee @@ -21,7 +21,7 @@ Flowerbox = onQueueStateChange: -> - queueRunner: -> + queueRunner: (failsafe = 5) -> Flowerbox.onQueueStateChange("checking queue") if Flowerbox.contactQueue.length > 0 Flowerbox.started = true @@ -55,7 +55,10 @@ Flowerbox = xhr.abort() Flowerbox.onQueueStateChange("aborted #{url}, rerunning") Flowerbox.contactQueue.unshift(info) - Flowerbox.queueRunner() + + if failsafe > 0 + failsafe -= 1 + Flowerbox.queueRunner(failsafe) , Flowerbox.delay * 5 ) diff --git a/lib/assets/javascripts/flowerbox/cucumber.js.coffee b/lib/assets/javascripts/flowerbox/cucumber.js.coffee index 4de13eb..688b14c 100644 --- a/lib/assets/javascripts/flowerbox/cucumber.js.coffee +++ b/lib/assets/javascripts/flowerbox/cucumber.js.coffee @@ -17,16 +17,22 @@ Flowerbox.World = (code = null) -> Flowerbox.Matchers = toEqual: (expected) -> @message = "Expected #{@actual} #{@notMessage} equal #{expected}" - if typeof @actual == 'object' - for key, value of @actual - return false if expected[key] != value + result = null - for key, value of expected - return false if @actual[key] != value + if @actual? && expected? + switch (typeof @actual) + when 'object' + result = true + for key, value of @actual + result = false if expected[key] != value - true - else - @actual == expected + for key, value of expected + result = false if @actual[key] != value + + if result == null + result = (@actual == expected) + + result Flowerbox.World -> @assert = (what, message = 'failed') -> diff --git a/lib/assets/javascripts/flowerbox/cucumber/reporter.js.coffee b/lib/assets/javascripts/flowerbox/cucumber/reporter.js.coffee index 52bb46a..6966442 100644 --- a/lib/assets/javascripts/flowerbox/cucumber/reporter.js.coffee +++ b/lib/assets/javascripts/flowerbox/cucumber/reporter.js.coffee @@ -39,7 +39,7 @@ class Flowerbox.Cucumber.Reporter when 'StepResult' stepResult = event.getPayloadItem('stepResult') - file = Flowerbox.Step.matchFile(@step.getName()) || "unknown:0" + file = Flowerbox.Step.matchFile(@step.getName()) || "#{Flowerbox.UNKNOWN}:0" result = new Flowerbox.Result(step_type: this.type(), source: 'cucumber', original_name: @step.getName(), name: this.nameParts(), file: file) @@ -49,6 +49,8 @@ class Flowerbox.Cucumber.Reporter result.status = Flowerbox.Result.PENDING else if stepResult.isUndefined() result.status = Flowerbox.Result.UNDEFINED + result.hasDataTable = @step.hasDataTable() + result.hasDocString = @step.hasDocString() else if stepResult.isFailed() result.status = Flowerbox.Result.FAILURE diff --git a/lib/assets/javascripts/flowerbox/jasmine/reporter.js.coffee b/lib/assets/javascripts/flowerbox/jasmine/reporter.js.coffee index df098e8..3bb6a9d 100644 --- a/lib/assets/javascripts/flowerbox/jasmine/reporter.js.coffee +++ b/lib/assets/javascripts/flowerbox/jasmine/reporter.js.coffee @@ -4,7 +4,7 @@ class jasmine.FlowerboxReporter status: Flowerbox.Result.SUCCESS source: 'jasmine' name: spec.getSpecSplitName() - file: 'unknown:0' + file: "#{Flowerbox.UNKNOWN}:0" for key, value of overrides data[key] = value @@ -38,6 +38,7 @@ class jasmine.FlowerboxReporter for item in spec.results().items_ if !item.passed_ result.status = Flowerbox.Result.FAILURE + stack = item.trace.stack || [] if stack.constructor != Array stack = stack.split("\n") diff --git a/lib/assets/javascripts/flowerbox/jasmine/selenium.js.coffee b/lib/assets/javascripts/flowerbox/jasmine/selenium.js.coffee index 473db0f..fc6db02 100644 --- a/lib/assets/javascripts/flowerbox/jasmine/selenium.js.coffee +++ b/lib/assets/javascripts/flowerbox/jasmine/selenium.js.coffee @@ -5,11 +5,13 @@ jasmine.Spec.beforeAddMatcherResult().push -> lol.wut catch e if e.stack - file = switch Flowerbox.environment + file = switch Flowerbox.environment.toLowerCase() when 'firefox' e.stack.split("\n")[3] when 'chrome' e.stack.split("\n")[4] + else + "#{Flowerbox.UNKNOWN}:0" - @trace = { stack: [ file ] } + @trace = { stack: [ @message, file ] } diff --git a/lib/flowerbox.rb b/lib/flowerbox.rb index fdc870a..8c9caa2 100644 --- a/lib/flowerbox.rb +++ b/lib/flowerbox.rb @@ -1,44 +1,27 @@ require "flowerbox/version" -require 'flowerbox-delivery' require 'rainbow' module Flowerbox - module CoreExt - autoload :Module, 'flowerbox/core_ext/module' + require 'flowerbox/core_ext/module' + + require 'flowerbox/runner' + + require 'flowerbox/run/base' + require 'flowerbox/run/test' + require 'flowerbox/run/debug' + + require 'flowerbox/test_environment' + require 'flowerbox/result' + require 'flowerbox/result_set' + require 'flowerbox/gathered_result' + + require 'flowerbox/reporter' + + if defined?(Rails::Engine) + require 'flowerbox/rails/engine' end - autoload :Runner, 'flowerbox/runner' - autoload :Task, 'flowerbox/task' - - module Run - autoload :Base, 'flowerbox/run/base' - autoload :Test, 'flowerbox/run/test' - autoload :Debug, 'flowerbox/run/debug' - end - - module Runner - autoload :Node, 'flowerbox/runner/node' - autoload :Selenium, 'flowerbox/runner/selenium' - autoload :Firefox, 'flowerbox/runner/firefox' - autoload :Chrome, 'flowerbox/runner/chrome' - autoload :Base, 'flowerbox/runner/base' - end - - autoload :TestEnvironment, 'flowerbox/test_environment' - - module TestEnvironment - autoload :Base, 'flowerbox/test_environment/base' - autoload :Jasmine, 'flowerbox/test_environment/jasmine' - autoload :Cucumber, 'flowerbox/test_environment/cucumber' - end - - autoload :Rack, 'flowerbox/rack' - - autoload :ResultSet, 'flowerbox/result_set' - autoload :GatheredResult, 'flowerbox/gathered_result' - autoload :Result, 'flowerbox/result' - - autoload :Reporter, 'flowerbox/reporter' + CACHE_DIR = '.tmp-sprockets' class << self attr_writer :reporters @@ -84,7 +67,7 @@ module Flowerbox Pathname(File.expand_path('../..', __FILE__)) end - attr_accessor :test_environment, :runner_environment, :bare_coffeescript + attr_accessor :test_environment, :runner_environment, :bare_coffeescript, :server def configure yield self @@ -126,6 +109,12 @@ module Flowerbox @browsers = {} end + + def transplant(dir) + Flowerbox::TestEnvironment.transplantable_environments.each do |env| + break if env.transplant(dir) + end + end end end diff --git a/lib/flowerbox/delivery/tilt/feature_template.rb b/lib/flowerbox/delivery/tilt/feature_template.rb deleted file mode 100644 index 12cf32f..0000000 --- a/lib/flowerbox/delivery/tilt/feature_template.rb +++ /dev/null @@ -1,22 +0,0 @@ -require 'tilt' - -module Flowerbox::Delivery::Tilt - class FeatureTemplate < Tilt::Template - self.default_mime_type = 'application/javascript' - - def prepare; end - - def evaluate(scope, locals, &block) - <<-JS -Flowerbox.Cucumber.Features = Flowerbox.Cucumber.Features || []; - -Flowerbox.Cucumber.Features.push("#{escaped_data}"); -JS - end - - def escaped_data - data.gsub("\n", "\\n").gsub('"', '\\"') - end - end -end - diff --git a/lib/flowerbox/rack.rb b/lib/flowerbox/rack.rb index d0dcf95..060bf80 100644 --- a/lib/flowerbox/rack.rb +++ b/lib/flowerbox/rack.rb @@ -66,6 +66,8 @@ module Flowerbox get %r{^/__F__/(.*)$} do |file| asset = sprockets.asset_for(file, :bundle => false) + halt(404) if !asset + content_type asset.content_type asset.body end @@ -75,12 +77,6 @@ module Flowerbox runner.template end - - class << self - private - def setup_protetion(builder) - end - end end end diff --git a/lib/flowerbox/rails/engine.rb b/lib/flowerbox/rails/engine.rb new file mode 100644 index 0000000..37d4c11 --- /dev/null +++ b/lib/flowerbox/rails/engine.rb @@ -0,0 +1,15 @@ +require 'rails/engine' + +module Flowerbox + module Rails + class Engine < ::Rails::Engine + rake_tasks do + require 'flowerbox/task' + + Flowerbox::Task.create("flowerbox:spec", :dir => "spec/javascripts") + Flowerbox::Task.create("flowerbox:integration", :dir => "js-features") + end + end + end +end + diff --git a/lib/flowerbox/reporter.rb b/lib/flowerbox/reporter.rb index f6a00c8..3c3a49d 100644 --- a/lib/flowerbox/reporter.rb +++ b/lib/flowerbox/reporter.rb @@ -2,11 +2,12 @@ module Flowerbox module Reporter extend Flowerbox::CoreExt::Module - autoload :Base, 'flowerbox/reporter/base' - autoload :ConsoleBase, 'flowerbox/reporter/console_base' - autoload :Verbose, 'flowerbox/reporter/verbose' - autoload :Progress, 'flowerbox/reporter/progress' - autoload :FileDisplay, 'flowerbox/reporter/file_display' + require 'flowerbox/reporter/file_display' + + require 'flowerbox/reporter/base' + require 'flowerbox/reporter/console_base' + require 'flowerbox/reporter/verbose' + require 'flowerbox/reporter/progress' end end diff --git a/lib/flowerbox/result.rb b/lib/flowerbox/result.rb index cd9588d..4437c4b 100644 --- a/lib/flowerbox/result.rb +++ b/lib/flowerbox/result.rb @@ -10,12 +10,12 @@ module Flowerbox extend Flowerbox::CoreExt::Module end - autoload :Base, 'flowerbox/result/base' - autoload :Exception, 'flowerbox/result/exception' - autoload :Failure, 'flowerbox/result/failure' - autoload :Pending, 'flowerbox/result/pending' - autoload :FailureMessage, 'flowerbox/result/failure_message' - autoload :FileInfo, 'flowerbox/result/file_info' + require 'flowerbox/result/base' + require 'flowerbox/result/exception' + require 'flowerbox/result/failure' + require 'flowerbox/result/pending' + require 'flowerbox/result/failure_message' + require 'flowerbox/result/file_info' class << self def for(test_env, type) diff --git a/lib/flowerbox/result/base.rb b/lib/flowerbox/result/base.rb index 1eaf7e9..b2653f4 100644 --- a/lib/flowerbox/result/base.rb +++ b/lib/flowerbox/result/base.rb @@ -3,6 +3,7 @@ require 'forwardable' module Flowerbox module Result class Base + require 'flowerbox/result/file_info' include Flowerbox::Result::FileInfo extend Forwardable diff --git a/lib/flowerbox/result/failure_message.rb b/lib/flowerbox/result/failure_message.rb index 80b19bc..6dc5c2a 100644 --- a/lib/flowerbox/result/failure_message.rb +++ b/lib/flowerbox/result/failure_message.rb @@ -31,7 +31,7 @@ module Flowerbox::Result end def stack - @data['stack'] + @data['stack'] || [] end def first_local_stack diff --git a/lib/flowerbox/result/file_info.rb b/lib/flowerbox/result/file_info.rb index d41aafd..c988fee 100644 --- a/lib/flowerbox/result/file_info.rb +++ b/lib/flowerbox/result/file_info.rb @@ -1,28 +1,38 @@ module Flowerbox::Result::FileInfo + UNKNOWN = '__unknown__' + def translated_file - @translated_file ||= filename + return @translated_file if @translated_file + + if filename == UNKNOWN + @translated_file = UNKNOWN + else + @translated_file = Flowerbox.test_environment.actual_path_for(filename) + end + + @translated_file end def file_translated? - translated_file != filename + translated_file[%r{\.coffee$}] end def filename - file.to_s.split(":").first || 'unknown' + file.to_s.split(":").first || UNKNOWN end def line_number return @line_number if @line_number @line_number = file.to_s.split(":")[1] - @line_number = "~#{@line_number}" if file_translated? + @line_number = "~#{(@line_number.to_i * 0.67).to_i}" if file_translated? @line_number ||= "0" end alias :line :line_number def translated_file_and_line - "#{translated_file.gsub(%r{^/}, '')}:#{line_number}" + "#{translated_file.gsub(%r{^#{Dir.pwd}/}, '')}:#{line_number}" end def system_files diff --git a/lib/flowerbox/result_set.rb b/lib/flowerbox/result_set.rb index f403754..a933b1e 100644 --- a/lib/flowerbox/result_set.rb +++ b/lib/flowerbox/result_set.rb @@ -41,7 +41,13 @@ module Flowerbox end def exitstatus - results.any?(&:failure?) ? 1 : 0 + if results.any?(&:failure?) + 1 + elsif results.any?(&:pending?) + 2 + else + 0 + end end def print(data = {}) diff --git a/lib/flowerbox/run/base.rb b/lib/flowerbox/run/base.rb index 189fde5..fe2e134 100644 --- a/lib/flowerbox/run/base.rb +++ b/lib/flowerbox/run/base.rb @@ -33,7 +33,9 @@ module Flowerbox::Run end def sprockets - Flowerbox::Delivery::SprocketsHandler.new( + require 'flowerbox/sprockets_handler' + + Flowerbox::SprocketsHandler.new( :asset_paths => [ Flowerbox.path.join("lib/assets/javascripts"), Flowerbox.path.join("vendor/assets/javascripts"), diff --git a/lib/flowerbox/run/test.rb b/lib/flowerbox/run/test.rb index 3c9954b..21bdd8f 100644 --- a/lib/flowerbox/run/test.rb +++ b/lib/flowerbox/run/test.rb @@ -1,5 +1,7 @@ module Flowerbox::Run class Test < Base + require 'flowerbox/runner/base' + def execute prep! diff --git a/lib/flowerbox/runner.rb b/lib/flowerbox/runner.rb index af69eb0..5506272 100644 --- a/lib/flowerbox/runner.rb +++ b/lib/flowerbox/runner.rb @@ -1,6 +1,12 @@ module Flowerbox module Runner extend Flowerbox::CoreExt::Module + + def self.find_constant(string) + require "flowerbox/runner/#{string}" + + super + end end end diff --git a/lib/flowerbox/runner/base.rb b/lib/flowerbox/runner/base.rb index 43365c5..4ccedfc 100644 --- a/lib/flowerbox/runner/base.rb +++ b/lib/flowerbox/runner/base.rb @@ -1,3 +1,5 @@ +require 'thread' + module Flowerbox module Runner class Base @@ -9,6 +11,10 @@ module Flowerbox class RunnerDiedError < StandardError ; end + def self.mutex + @mutex ||= Mutex.new + end + def initialize @results = ResultSet.new @started = false @@ -27,6 +33,9 @@ module Flowerbox if !finished? puts "Something died hard. Here are the tests that did get run before Flowerbox died.".foreground(:red) puts tests.flatten.join("\n").foreground(:red) + server.stop + Flowerbox.server = nil + raise RunnerDiedError.new end end @@ -40,18 +49,20 @@ module Flowerbox end def run(*args) - setup(*args) + self.class.mutex.synchronize do + setup(*args) - @count = 0 - @timer_running = true + @count = 0 + @timer_running = true - puts "Flowerbox running your #{Flowerbox.test_environment.name} tests on #{console_name}..." + puts "Flowerbox running your #{Flowerbox.test_environment.name} tests on #{console_name}..." - server.start + server.start - yield + yield - server.stop + server.stop + end @results end @@ -60,8 +71,7 @@ module Flowerbox true end - def configure - end + def configure ; end def pause_timer @timer_running = false @@ -100,17 +110,20 @@ module Flowerbox end def server - return @server if @server + require 'flowerbox/rack' + + Flowerbox::Rack.runner = self + Flowerbox::Rack.sprockets = @sprockets + + return Flowerbox.server if Flowerbox.server + + require 'flowerbox/server' server_options = { :app => Flowerbox::Rack } server_options[:logging] = true if options[:verbose_server] server_options[:port] = Flowerbox.port - @server = Flowerbox::Delivery::Server.new(server_options) - Flowerbox::Rack.runner = self - Flowerbox::Rack.sprockets = @sprockets - - @server + Flowerbox.server = Flowerbox::Server.new(server_options) end def log(message) diff --git a/lib/flowerbox/runner/chrome.rb b/lib/flowerbox/runner/chrome.rb index e681f64..c9483b4 100644 --- a/lib/flowerbox/runner/chrome.rb +++ b/lib/flowerbox/runner/chrome.rb @@ -1,4 +1,7 @@ +require 'flowerbox/runner/selenium' + class Flowerbox::Runner::Chrome < Flowerbox::Runner::Selenium + def name "Chrome" end diff --git a/lib/flowerbox/runner/firefox.rb b/lib/flowerbox/runner/firefox.rb index a86cf3f..27a49d6 100644 --- a/lib/flowerbox/runner/firefox.rb +++ b/lib/flowerbox/runner/firefox.rb @@ -1,3 +1,5 @@ +require 'flowerbox/runner/selenium' + class Flowerbox::Runner::Firefox < Flowerbox::Runner::Selenium def name "Firefox" @@ -8,7 +10,7 @@ class Flowerbox::Runner::Firefox < Flowerbox::Runner::Selenium end def browser - @browser ||= ::Selenium::WebDriver.for(:firefox) + Flowerbox.browsers[:firefox] ||= ::Selenium::WebDriver.for(:firefox) end end diff --git a/lib/flowerbox/runner/node.rb b/lib/flowerbox/runner/node.rb index 99464d0..f0fc59b 100644 --- a/lib/flowerbox/runner/node.rb +++ b/lib/flowerbox/runner/node.rb @@ -1,6 +1,6 @@ require 'tempfile' -require 'flowerbox/delivery/server' require 'json' +require 'flowerbox/runner/base' module Flowerbox module Runner @@ -22,7 +22,7 @@ module Flowerbox def configured? File.directory?(File.join(Dir.pwd, 'node_modules/jsdom')) && - File.directory?(File.join(Dir.pwd, 'node_modules/XMLHttpRequest')) + File.directory?(File.join(Dir.pwd, 'node_modules/xmlhttprequest')) end def cleanup ; end @@ -95,6 +95,7 @@ jsdom.env( if (!gotFlowerbox && context.Flowerbox) { context.Flowerbox.baseUrl = "http://localhost:#{server.port}/"; context.Flowerbox.environment = 'node'; + context.Flowerbox.UNKNOWN = '#{Flowerbox::Result::FileInfo::UNKNOWN}'; gotFlowerbox = true; } diff --git a/lib/flowerbox/runner/selenium.rb b/lib/flowerbox/runner/selenium.rb index 6d67203..770389c 100644 --- a/lib/flowerbox/runner/selenium.rb +++ b/lib/flowerbox/runner/selenium.rb @@ -1,4 +1,5 @@ require 'selenium-webdriver' +require 'flowerbox/runner/base' module Flowerbox module Runner @@ -55,6 +56,7 @@ console.log = function(msg) { Flowerbox.onQueueStateChange = function(msg) { //document.getElementById('queue').innerHTML = document.getElementById('queue').innerHTML + "\\n" + msg; }; + Flowerbox.UNKNOWN = '#{Flowerbox::Result::FileInfo::UNKNOWN}'; var context = this; @@ -69,7 +71,7 @@ HTML end def template_files - sprockets.files.collect { |file| %{} } + sprockets.files.collect { |file| %{} } end end end diff --git a/lib/flowerbox/server.rb b/lib/flowerbox/server.rb new file mode 100644 index 0000000..2e64e27 --- /dev/null +++ b/lib/flowerbox/server.rb @@ -0,0 +1,123 @@ +require 'rack' +require 'net/http' +require 'socket' +require 'rack/builder' +require 'thin' + +module Flowerbox + class Server + attr_reader :options + + def initialize(options = {}) + @options = { :logging => false }.merge(options || {}) + end + + def start + @server_thread = Thread.new do + server_options = { :Port => port, :Host => interface } + + app = options[:app] + + Thin::Logging.silent = !options[:logging] + + if options[:logging] + real_app = app + app = ::Rack::Builder.new do + use ::Rack::CommonLogger, STDOUT + run real_app + end + end + + ::Rack::Handler::Thin.run(app, server_options) do |server| + Thread.current[:server] = server + + trap('QUIT') { server.stop } + end + end + + while !@server_thread[:server] && @server_thread.alive? + sleep 0.1 + end + + raise StandardError.new("Server died") if !@server_thread[:server].running? + end + + def stop + if @server_thread + @server_thread[:server].stop + + wait_for_server_to_stop + end + end + + def interface + options[:interface] || '0.0.0.0' + end + + def port + return @port if @port ||= options[:port] + + attempts = 20 + + begin + attempts -= 1 + + current_port = random_port + + begin + socket = TCPSocket.new(interface, current_port) + socket.close + rescue Errno::ECONNREFUSED => e + @port = current_port + end + end while !@port and attempts > 0 + + raise StandardError.new("can't start server") if attempts == 0 + + @port + end + + def address + "http://#{interface}:#{port}/" + end + + def alive? + @server_thread.alive? + end + + private + def wait_for_server_to_start + while true do + begin + connect_interface = '127.0.0.1' if interface == '0.0.0.0' + + TCPSocket.new(connect_interface, port) + break + rescue Errno::ECONNREFUSED => e + end + + sleep 0.1 + end + end + + def wait_for_server_to_stop + while alive? do + begin + connect_interface = '127.0.0.1' if interface == '0.0.0.0' + + socket = TCPSocket.new(connect_interface, port) + socket.close + rescue Errno::ECONNREFUSED => e + return + end + + sleep 0.1 + end + end + + def random_port + 25000 + Kernel.rand(1000) + end + end +end + diff --git a/lib/flowerbox/sprockets_handler.rb b/lib/flowerbox/sprockets_handler.rb new file mode 100644 index 0000000..a0bb219 --- /dev/null +++ b/lib/flowerbox/sprockets_handler.rb @@ -0,0 +1,71 @@ +require 'sprockets' +require 'sprockets/engines' +require 'forwardable' +require 'sprockets-vendor_gems' + +module Flowerbox + class SprocketsHandler + extend Forwardable + + attr_reader :files, :options + + def_delegators :environment, :append_path, :register_engine, :[] + + def self.gem_asset_paths + @gem_asset_paths ||= Sprockets.find_gem_vendor_paths + end + + def initialize(options) + @options = options + + require 'flowerbox/unique_asset_list' + + @files = Flowerbox::UniqueAssetList.new(self) + end + + def add(asset) + paths_for(asset).each { |path| add_paths_for_compiled_asset(path) } + end + + def paths_for(asset) + environment.find_asset(asset).to_a.collect(&:pathname) + end + + def expire_index! + @environment.send(:expire_index!) + end + + def environment + return @environment if @environment + + @environment = Sprockets::Environment.new + @environment.cache = Sprockets::Cache::FileStore.new(Flowerbox::CACHE_DIR) + + self.class.gem_asset_paths.each { |path| append_path(path) } + options[:asset_paths].each { |path| append_path(path) } + + @environment + end + + def asset_for(*args) + environment.find_asset(*args) + end + + def add_paths_for_compiled_asset(path) + asset_for(path, :bundle => false).to_a.each { |file_path| @files.add(file_path) } + end + + def logical_path_for(asset) + asset_path = asset.pathname.to_s + + environment.paths.each do |path| + if asset_path[%r{^#{path}}] + return asset_path.gsub(%r{^#{path}/}, '') + end + end + + raise StandardError.new("Could not find logical path for #{asset_path}") + end + end +end + diff --git a/lib/flowerbox/test_environment.rb b/lib/flowerbox/test_environment.rb index 1f56e88..5362178 100644 --- a/lib/flowerbox/test_environment.rb +++ b/lib/flowerbox/test_environment.rb @@ -2,10 +2,18 @@ module Flowerbox module TestEnvironment extend Flowerbox::CoreExt::Module - class << self - def for(env) - find_constant(env).new + def self.for(env) + require "flowerbox/test_environment/#{env}" + + find_constant(env).new + end + + def self.transplantable_environments + Dir[File.expand_path('../test_environment/*.rb', __FILE__)].each do |file| + require file end + + constants.collect { |k| const_get(k) }.find_all(&:transplantable?) end end end diff --git a/lib/flowerbox/test_environment/base.rb b/lib/flowerbox/test_environment/base.rb index bc8cc3a..b361d14 100644 --- a/lib/flowerbox/test_environment/base.rb +++ b/lib/flowerbox/test_environment/base.rb @@ -13,6 +13,10 @@ module Flowerbox @reporters ||= [] end + def self.transplantable? + respond_to?(:transplant) + end + def set_additional_options(opts = nil) @options = {} @@ -41,6 +45,10 @@ module Flowerbox def start runner.spec_files.each { |file| @sprockets.add(file) } end + + def actual_path_for(file) + @sprockets.asset_for(file, :bundle => false).pathname.to_s + end end end end diff --git a/lib/flowerbox/test_environment/cucumber.rb b/lib/flowerbox/test_environment/cucumber.rb index c0ba320..b614882 100644 --- a/lib/flowerbox/test_environment/cucumber.rb +++ b/lib/flowerbox/test_environment/cucumber.rb @@ -1,3 +1,5 @@ +require 'flowerbox/test_environment/base' + module Flowerbox module TestEnvironment class Cucumber < Base @@ -12,7 +14,9 @@ module Flowerbox def inject_into(sprockets) super - @sprockets.register_engine('.feature', Flowerbox::Delivery::Tilt::FeatureTemplate) + require 'flowerbox/tilt/feature_template' + + @sprockets.register_engine('.feature', Flowerbox::Tilt::FeatureTemplate) end def global_system_files @@ -56,20 +60,34 @@ JS " (\d+) " end + messages = [] + + if result['hasDataTable'] + args << "table" + messages << "table is a Cucumber AST data table" + end + + if result['hasDocString'] + args << "string" + messages << "string is a doc string" + end + args_string = args.join(', ') + output = [] + if primarily_coffeescript? - <<-COFFEE -Flowerbox.#{result.step_type} /^#{matcher}$/, #{"(#{args_string}) " if !args_string.empty?}-> - @pending() # add your code here -COFFEE + output << %{Flowerbox.#{result.step_type} /^#{matcher}$/, #{"(#{args_string}) " if !args_string.empty?}->} + output += messages.collect { |msg| " # #{msg}" } + output << %{ @pending() # add your code here} else - <<-JS -Flowerbox.#{result.step_type}(/^#{matcher}$/, function(#{args_string}) { - this.pending(); // add your code here -}); -JS + output << "Flowerbox.#{result.step_type}(/^#{matcher}$/, function(#{args_string}) {" + output += messages.collect { |msg| " // #{msg}" } + output << %{ this.pending(); // add your code here} + output << "});" end + + output.collect { |line| "#{line}\n" }.join end def primarily_coffeescript? @@ -80,11 +98,15 @@ JS coffee_count > js_count end + + def plant_source + "skel/cucumber" + end + + def plant_target + "js-features" + end end end end -module Flowerbox::Delivery::Tilt - autoload :FeatureTemplate, 'flowerbox/delivery/tilt/feature_template' -end - diff --git a/lib/flowerbox/test_environment/jasmine.rb b/lib/flowerbox/test_environment/jasmine.rb index 23b987e..e171e12 100644 --- a/lib/flowerbox/test_environment/jasmine.rb +++ b/lib/flowerbox/test_environment/jasmine.rb @@ -1,8 +1,56 @@ +require 'flowerbox/test_environment/base' require 'jasmine-core' +require 'yaml' module Flowerbox module TestEnvironment class Jasmine < Base + def self.transplant(dir) + if File.file?(jasmine_yml = File.join(dir, 'support/jasmine.yml')) + puts "Transplanting #{jasmine_yml} into Flowerbox..." + + config = [ + %{f.test_with :jasmine}, + %{f.run_with :firefox}, + %{f.report_with :verbose} + ] + + YAML.load_file(jasmine_yml).each do |key, value| + case key + when 'src_dir' + [ value ].flatten.each do |dir| + config << %{f.asset_paths << "#{dir}"} + end + when 'src_files' + [ value ].flatten.each do |file| + config << %{f.additional_files << "#{file}"} + end + when 'helpers' + [ value ].flatten.each do |pattern| + Dir[File.join(dir, pattern)].each do |file| + config << %{f.additional_files << "#{file.gsub("#{dir}/", '')}"} + end + end + when 'spec_files' + [ value ].flatten.each do |pattern| + config << %{f.spec_patterns << "#{pattern}"} + end + end + end + + File.open(target = File.join(dir, 'spec_helper.rb'), 'wb') do |fh| + fh.puts "Flowerbox.configure do |f|" + config.each do |line| + fh.puts " #{line}" + end + fh.puts "end" + end + + puts "#{target} created. Run your tests with:" + puts " flowerbox test #{dir}" + end + end + def inject_into(sprockets) sprockets.append_path(::Jasmine::Core.path) diff --git a/lib/flowerbox/tilt/feature_template.rb b/lib/flowerbox/tilt/feature_template.rb new file mode 100644 index 0000000..b6586bf --- /dev/null +++ b/lib/flowerbox/tilt/feature_template.rb @@ -0,0 +1,24 @@ +require 'tilt' + +module Flowerbox + module Tilt + class FeatureTemplate < ::Tilt::Template + self.default_mime_type = 'application/javascript' + + def prepare; end + + def evaluate(scope, locals, &block) + <<-JS +Flowerbox.Cucumber.Features = Flowerbox.Cucumber.Features || []; + +Flowerbox.Cucumber.Features.push("#{escaped_data}"); +JS + end + + def escaped_data + data.gsub("\n", "\\n").gsub('"', '\\"') + end + end + end +end + diff --git a/lib/flowerbox/unique_asset_list.rb b/lib/flowerbox/unique_asset_list.rb new file mode 100644 index 0000000..03353a7 --- /dev/null +++ b/lib/flowerbox/unique_asset_list.rb @@ -0,0 +1,25 @@ +module Flowerbox + class UniqueAssetList < ::Array + attr_reader :sprockets + + def initialize(sprockets) + super([]) + + @sprockets = sprockets + end + + def add(files) + [ files ].flatten.each { |file| self << file if !include?(file) } + end + + def to_json + collect { |file| sprockets.logical_path_for(file) } + end + + private + def include?(file) + any? { |other_file| other_file.pathname.to_s == file.pathname.to_s } + end + end +end + diff --git a/skel/cucumber/features/my_first_feature.feature b/skel/cucumber/features/my_first_feature.feature new file mode 100644 index 0000000..1dd0d81 --- /dev/null +++ b/skel/cucumber/features/my_first_feature.feature @@ -0,0 +1,7 @@ +Feature: My First Feature + Scenario: Do Something + Given I have a flowerbox + When I plant a "cucumber" seed + Then I should get the following when I pick: + | cucumber | + diff --git a/skel/cucumber/spec_helper.rb b/skel/cucumber/spec_helper.rb new file mode 100644 index 0000000..ebf8442 --- /dev/null +++ b/skel/cucumber/spec_helper.rb @@ -0,0 +1,10 @@ +Flowerbox.configure do |f| + f.test_with :cucumber + f.run_with :firefox + + f.additional_files << "support/env.js.coffee" + f.spec_patterns << "features/**/*.feature" + + f.test_environment.prefer_step_language :coffeescript +end + diff --git a/skel/cucumber/step_definitions/given/i_have_a_flowerbox.js.coffee b/skel/cucumber/step_definitions/given/i_have_a_flowerbox.js.coffee new file mode 100644 index 0000000..2221119 --- /dev/null +++ b/skel/cucumber/step_definitions/given/i_have_a_flowerbox.js.coffee @@ -0,0 +1,8 @@ +Flowerbox.Given /^I have a flowerbox$/, -> + @flowerbox = + plantSeed: (type) -> + @types ||= [] + @types.push(type) + pick: -> + @types + diff --git a/skel/cucumber/step_definitions/then/i_should_get_the_following_when_i_pick.js.coffee b/skel/cucumber/step_definitions/then/i_should_get_the_following_when_i_pick.js.coffee new file mode 100644 index 0000000..b6d7f1b --- /dev/null +++ b/skel/cucumber/step_definitions/then/i_should_get_the_following_when_i_pick.js.coffee @@ -0,0 +1,5 @@ +Flowerbox.Then /^I should get the following when I pick:$/, (table) -> + # table is a Cucumber AST data table + data = (row[0] for row in table.raw()) + @expect(@flowerbox.pick()).toEqual(data) + diff --git a/skel/cucumber/step_definitions/when/i_plant_a_seed.js.coffee b/skel/cucumber/step_definitions/when/i_plant_a_seed.js.coffee new file mode 100644 index 0000000..9d98279 --- /dev/null +++ b/skel/cucumber/step_definitions/when/i_plant_a_seed.js.coffee @@ -0,0 +1,3 @@ +Flowerbox.When /^I plant a "([^"]+)" seed$/, (type) -> + @flowerbox.plantSeed(type) + diff --git a/skel/cucumber/support/env.js.coffee b/skel/cucumber/support/env.js.coffee new file mode 100644 index 0000000..624ee96 --- /dev/null +++ b/skel/cucumber/support/env.js.coffee @@ -0,0 +1 @@ +#= require_tree ../step_definitions diff --git a/spec/flowerbox/delivery/template_renderer_spec.rb b/spec/flowerbox/delivery/template_renderer_spec.rb new file mode 100644 index 0000000..7c790d0 --- /dev/null +++ b/spec/flowerbox/delivery/template_renderer_spec.rb @@ -0,0 +1,59 @@ +require 'spec_helper' + +describe Flowerbox::Delivery::TemplateRenderer do + let(:template_renderer) { described_class.new(:template => template, :files => files) } + let(:template) { 'template' } + let(:files) { 'files' } + + let(:rendered_template) { 'rendered template' } + let(:erb_template) { "#{rendered_template} <%= resource_tags %>" } + + describe '#render' do + subject { template_renderer.render } + + let(:rendered_files) { 'with files' } + let(:result) { "#{rendered_template} #{rendered_files}" } + + before do + template_renderer.expects(:resource_tags).returns(rendered_files) + template_renderer.expects(:template).returns(erb_template) + end + + it { should == result } + end + + describe '#template' do + include FakeFS::SpecHelpers + + before do + File.open(template, 'wb') { |fh| fh.print erb_template } + end + + subject { template_renderer.template } + + it { should == erb_template } + end + + describe '#resource_tags' do + subject { template_renderer.resource_tags } + + context 'success' do + let(:files) { [ js, css ] } + let(:js) { 'file.js' } + let(:css) { 'file.css' } + + it { should == [ + %{}, + %{} + ].join } + end + + context 'failure' do + let(:files) { [ 'what.ever' ] } + + it 'should raise error' do + expect { subject }.to raise_error(Flowerbox::Delivery::TemplateRenderer::FileTypeError) + end + end + end +end diff --git a/spec/flowerbox/delivery/tilt/js_template_spec.rb b/spec/flowerbox/delivery/tilt/js_template_spec.rb new file mode 100644 index 0000000..41a680b --- /dev/null +++ b/spec/flowerbox/delivery/tilt/js_template_spec.rb @@ -0,0 +1,62 @@ +require 'spec_helper' + +describe Flowerbox::Delivery::Tilt::JSTemplate do + let(:js_template) { described_class.new { '' } } + + describe '#evaluate' do + subject { js_template.evaluate(Object.new, {}) } + + before do + js_template.stubs(:file).returns(file) + end + + context '.js' do + let(:file) { 'file.js' } + + it { should == file } + end + + context 'other extension' do + let(:file) { 'file.coffee' } + let(:temp_file) { 'temp file' } + + before do + js_template.expects(:save).returns(temp_file) + end + + it { should == temp_file } + end + end + + describe '#save' do + include FakeFS::SpecHelpers + + let(:temp_file) { 'dir/temp file' } + let(:data) { 'data' } + + before do + js_template.stubs(:temp_file).returns(temp_file) + js_template.stubs(:data).returns(data) + end + + it 'should save the file to disk and return the temp path' do + js_template.save.should == temp_file + + File.read(temp_file).should == data + end + end + + describe '#temp_file' do + subject { js_template.temp_file } + + let(:filename) { "#{root_filename}.ext" } + let(:root_filename) { "dir/file.js" } + + before do + js_template.stubs(:file).returns(filename) + end + + it { should == File.join(Dir.pwd, '.tmp/sprockets', root_filename) } + end +end + diff --git a/spec/flowerbox/server_spec.rb b/spec/flowerbox/server_spec.rb new file mode 100644 index 0000000..5a73362 --- /dev/null +++ b/spec/flowerbox/server_spec.rb @@ -0,0 +1,100 @@ +require 'spec_helper' +require 'socket' +require 'thread' + +describe Flowerbox::Delivery::Server do + let(:server) { described_class.new(options) } + let(:options) { nil } + + subject { server } + + describe '#initialize' do + let(:options) { { :port => port, :interface => interface } } + let(:port) { 'port' } + let(:interface) { 'interface' } + + its(:port) { should == port } + its(:interface) { should == interface } + end + + describe '#start' do + let(:port) { 12345 } + let(:interface) { '127.0.0.1' } + + before do + server.stubs(:port).returns(port) + server.stubs(:interface).returns(interface) + end + + it 'should start a Rack server' do + server.start + + TCPSocket.new(server.interface, server.port) + end + end + + describe '#interface' do + subject { server.interface } + + it { should == '0.0.0.0' } + end + + describe '#port' do + let(:interface) { '127.0.0.1' } + let(:base) { 25000 } + let(:initial) { base + @offset } + + before do + server.stubs(:interface).returns(interface) + + @offset = 0 + ok = true + + begin + [ 0, 1 ].each do |index| + begin + TCPSocket.new(interface, base + @offset + index) + @offset += 1 + ok = false + rescue Errno::ECONNREFUSED => e + end + end + end while !ok + end + + subject { server.port } + + context 'no running service' do + before do + Kernel.stubs(:rand).returns(@offset) + end + + it { should == initial } + end + + context 'running service' do + before do + @server = Thread.new do + TCPServer.new(interface, initial) + end + + server.stubs(:random_port).returns(initial, initial + 1) + + while true + begin + TCPSocket.new(interface, initial) + break + rescue Errno::ECONNREFUSED + end + end + end + + it { should == initial + 1 } + + after do + @server.kill + end + end + end +end + diff --git a/spec/flowerbox/sprockets_handler_spec.rb b/spec/flowerbox/sprockets_handler_spec.rb new file mode 100644 index 0000000..2635a7a --- /dev/null +++ b/spec/flowerbox/sprockets_handler_spec.rb @@ -0,0 +1,52 @@ +require 'spec_helper' + +describe Flowerbox::Delivery::SprocketsHandler do + let(:sprockets_handler) { described_class.new(options) } + let(:options) { { :asset_paths => asset_paths } } + let(:asset_paths) { [ File.expand_path('asset path') ] } + + describe '#add' do + let(:asset) { 'asset' } + let(:path) { 'path' } + let(:paths) { [ path ] } + + let(:pathname_path) { 'pathname path' } + + before do + sprockets_handler.expects(:paths_for).with(asset).returns(paths) + sprockets_handler.expects(:path_for_compiled_asset).with(path).returns(pathname_path) + end + + it 'should add the asset to the list of ones to work with' do + sprockets_handler.add(asset) + + sprockets_handler.files.should == [ pathname_path ] + end + end + + describe '#paths_for' do + subject { sprockets_handler.paths_for(asset) } + + let(:asset) { 'asset' } + let(:environment) { stub } + let(:bundled_asset) { stub(:to_a => [ processed_asset ]) } + let(:processed_asset) { stub(:pathname => path) } + + let(:path) { 'path' } + + before do + sprockets_handler.stubs(:environment).returns(environment) + environment.expects(:find_asset).with(asset).returns(bundled_asset) + end + + it { should == [ path ] } + end + + describe '#environment' do + subject { sprockets_handler.environment } + + it { should be_a_kind_of(Sprockets::Environment) } + its(:paths) { should == asset_paths } + end +end + diff --git a/spec/flowerbox/unique_asset_list_spec.rb b/spec/flowerbox/unique_asset_list_spec.rb new file mode 100644 index 0000000..1def2c1 --- /dev/null +++ b/spec/flowerbox/unique_asset_list_spec.rb @@ -0,0 +1,18 @@ +require 'spec_helper' + +describe Flowerbox::Delivery::UniqueAssetList do + let(:unique_asset_list) { described_class.new } + + describe "#add" do + let(:first) { Pathname.new('one') } + let(:second) { Pathname.new('one') } + let(:third) { Pathname.new('two') } + + it 'should not add assets already added' do + unique_asset_list.add(first) + unique_asset_list.add([ second, third ]) + + unique_asset_list.should == [ first, third ] + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..fe4f8b5 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,7 @@ +require 'flowerbox-delivery' +require 'mocha' +require 'fakefs/spec_helpers' + +RSpec.configure do |c| + c.mock_with :mocha +end