From 4508be7753ccfb7af0fe898b29b993180fcb1970 Mon Sep 17 00:00:00 2001 From: mordaroso Date: Fri, 10 Aug 2012 13:44:15 +0200 Subject: [PATCH] version 0.1.0 --- .travis.yml | 2 + Gemfile | 4 + Guardfile | 9 ++ README.md | 102 ++++++++++++++++--- guard-motion.gemspec | 5 +- lib/guard/motion.rb | 86 ++++++++++++++++ lib/guard/motion/runner.rb | 93 +++++++++++++++++ lib/guard/motion/tasks.rb | 21 ++++ lib/guard/motion/templates/Guardfile | 4 +- lib/guard/motion/version.rb | 2 +- lib/motion.rb | 50 --------- spec/fixtures/bundler/Gemfile | 3 + spec/guard/motion/runner_spec.rb | 140 ++++++++++++++++++++++++++ spec/guard/motion_spec.rb | 145 +++++++++++++++++++++++++++ spec/spec_helper.rb | 17 ++++ 15 files changed, 611 insertions(+), 72 deletions(-) create mode 100644 .travis.yml create mode 100644 Guardfile create mode 100644 lib/guard/motion.rb create mode 100644 lib/guard/motion/runner.rb create mode 100644 lib/guard/motion/tasks.rb delete mode 100644 lib/motion.rb create mode 100644 spec/fixtures/bundler/Gemfile create mode 100644 spec/guard/motion/runner_spec.rb create mode 100644 spec/guard/motion_spec.rb create mode 100644 spec/spec_helper.rb diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..dfcfa95 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,2 @@ +rvm: + - 1.9.3 \ No newline at end of file diff --git a/Gemfile b/Gemfile index b613b7c..408d401 100644 --- a/Gemfile +++ b/Gemfile @@ -2,3 +2,7 @@ source 'https://rubygems.org' # Specify your gem's dependencies in guard-motion.gemspec gemspec + +platforms :ruby do + gem 'rb-readline' +end \ No newline at end of file diff --git a/Guardfile b/Guardfile new file mode 100644 index 0000000..767287d --- /dev/null +++ b/Guardfile @@ -0,0 +1,9 @@ +# A sample Guardfile +# More info at https://github.com/guard/guard#readme + +guard 'rspec', :version => 2 do + watch(%r{^spec/.+_spec\.rb$}) + watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" } + watch('spec/spec_helper.rb') { "spec" } +end + diff --git a/README.md b/README.md index 89db4d3..d36fc8d 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,99 @@ -# Guard::Motion +# Guard::Motion [![Build Status](https://secure.travis-ci.org/mordaroso/guard-motion.png?branch=master)](http://travis-ci.org/mordaroso/guard-motion) -TODO: Write a gem description +Motion guard allows to automatically & intelligently launch [RubyMotion](http://www.rubymotion.com/) specs when files are modified. -## Installation +## Install -Add this line to your application's Gemfile: +Please be sure to have [Guard](https://github.com/guard/guard) installed before continue. - gem 'guard-motion' +Install the gem: -And then execute: +``` +$ gem install guard-motion +``` - $ bundle +Add it to your Gemfile (inside development group): -Or install it yourself as: +``` ruby +gem 'guard-motion' +``` - $ gem install guard-motion +Add guard definition to your Guardfile by running this command: + +``` +$ guard init motion +``` + +Make sure Guard::Motion is loaded in your project Rakefile: + +``` +require 'guard/motion' +``` ## Usage -TODO: Write usage instructions here +Please read [Guard usage doc](https://github.com/guard/guard#readme) + +## Guardfile + +Motion guard can be really adapted to all kind of project setup. + +### Typical RubyMotion App + +``` ruby +guard 'motion' do + watch(%r{^spec/.+_spec\.rb$}) + watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } +end +``` + +### Typical RubyMotion library + +``` ruby +guard 'motion' do + watch(%r{^spec/.+_spec\.rb$}) + watch(%r{^lib/[^/]+/(.+)\.rb$}) { |m| "./spec/#{m[1]}_spec.rb" } +end +``` + +Please read [Guard doc](https://github.com/guard/guard#readme) for more information about the Guardfile DSL. + +## Options + +By default, Guard::Motion will only look for spec files within `spec` in your project root. You can configure Guard::Motion to look in additional paths by using the `:spec_paths` option: + +``` ruby +guard 'motion', :spec_paths => ["spec", "vendor/other_project/spec"] do + # ... +end +``` +If you have only one path to look, you can configure `:spec_paths` option with a string: + +``` ruby +guard 'motion', :spec_paths => "test" do + # ... +end +``` + +### List of available options: + +``` ruby +:bundler => false # use "bundle exec" to run the rake command, default: true +:binstubs => true # use "bin/rake" to run the rake command (takes precedence over :bundle), default: false +:notification => false # display Growl (or Libnotify) notification after the specs are done running, default: true +:all_after_pass => false # run all specs after changed specs pass, default: true +:all_on_start => false # run all the specs at startup, default: true +:keep_failed => false # keep failed specs until they pass, default: true +:spec_paths => ["spec"] # specify an array of paths that contain spec files +``` + +You can also use a custom binstubs directory using `:binstubs => 'some-dir'`. + +Development +----------- -## Contributing +* Source hosted at [GitHub](https://github.com/mordaroso/guard-motion) +* Report issues/Questions/Feature requests on [GitHub Issues](https://github.com/mordaroso/guard-motion/issues) -1. Fork it -2. Create your feature branch (`git checkout -b my-new-feature`) -3. Commit your changes (`git commit -am 'Added some feature'`) -4. Push to the branch (`git push origin my-new-feature`) -5. Create new Pull Request +Pull requests are very welcome! Make sure your patches are well tested. Please create a topic branch for every separate change +you make. diff --git a/guard-motion.gemspec b/guard-motion.gemspec index b147d56..df471f4 100644 --- a/guard-motion.gemspec +++ b/guard-motion.gemspec @@ -4,11 +4,9 @@ require File.expand_path('../lib/guard/motion/version', __FILE__) Gem::Specification.new do |gem| gem.authors = ["mordaroso"] gem.email = ["mordaroso@gmail.com"] - gem.description = %q{TODO: Write a gem description} - gem.summary = %q{TODO: Write a gem summary} gem.homepage = 'http://rubygems.org/gems/guard-motion' gem.summary = 'Guard gem for RubyMotion' - gem.description = 'Guard::Motion automatically runs RubyMotion specs' + gem.description = 'Guard::Motion automatically run your specs (much like autotest).' gem.files = `git ls-files`.split($\) gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } @@ -18,6 +16,7 @@ Gem::Specification.new do |gem| gem.version = Guard::MotionVersion::VERSION gem.add_dependency 'guard', '>= 1.1.0' + gem.add_dependency 'rake', '>= 0.9' gem.add_development_dependency 'bundler', '~> 1.1.0' gem.add_development_dependency 'rspec', '~> 2.10' diff --git a/lib/guard/motion.rb b/lib/guard/motion.rb new file mode 100644 index 0000000..43b3e36 --- /dev/null +++ b/lib/guard/motion.rb @@ -0,0 +1,86 @@ +require 'guard' +require 'guard/guard' + +require 'guard/motion/tasks' + +module Guard + class Motion < Guard + autoload :Runner, 'guard/motion/runner' + + # Initialize a Guard. + # @param [Array] watchers the Guard file watchers + # @param [Hash] options the custom Guard options + def initialize(watchers = [], options = {}) + super + @options = { + :all_after_pass => true, + :all_on_start => true, + :keep_failed => true, + :spec_paths => ["spec"] + }.merge(options) + @last_failed = false + @failed_paths = [] + + @runner = Runner.new(@options) + end + + # Call once when Guard starts. Please override initialize method to init stuff. + # @raise [:task_has_failed] when start has failed + def start + UI.info "Guard::Motion is running" + run_all if @options[:all_on_start] + end + + # Called when `reload|r|z + enter` is pressed. + # This method should be mainly used for "reload" (really!) actions like reloading passenger/spork/bundler/... + # @raise [:task_has_failed] when reload has failed + def reload + @failed_paths = [] + end + + # Called when just `enter` is pressed + # This method should be principally used for long action like running all specs/tests/... + # @raise [:task_has_failed] when run_all has failed + def run_all + passed = @runner.run + + unless @last_failed = !passed + @failed_paths = [] + else + throw :task_has_failed + end + end + + # Called on file(s) modifications that the Guard watches. + # @param [Array] paths the changes files or paths + # @raise [:task_has_failed] when run_on_change has failed + def run_on_changes(paths) + paths += @failed_paths if @options[:keep_failed] + + if passed = @runner.run(paths) + remove_failed(paths) + + # run all the specs if the run before this one failed + if @last_failed && @options[:all_after_pass] + @last_failed = false + run_all + end + else + @last_failed = true + add_failed(paths) + + throw :task_has_failed + end + end + + private + def remove_failed(paths) + @failed_paths -= paths if @options[:keep_failed] + end + + def add_failed(paths) + @failed_paths += paths if @options[:keep_failed] + end + + end +end diff --git a/lib/guard/motion/runner.rb b/lib/guard/motion/runner.rb new file mode 100644 index 0000000..5baaa05 --- /dev/null +++ b/lib/guard/motion/runner.rb @@ -0,0 +1,93 @@ +module Guard + class Motion + class Runner + + def initialize(options = {}) + @options = { + :bundler => true, + :binstubs => false, + :notification => true, + }.merge(options) + end + + def run(paths = nil, options = {}) + if paths.nil? + paths = all_spec_paths + message = options[:message] || "Running all specs" + else + message = options[:message] || "Running: #{paths.join(' ')}" + end + + UI.info(message, :reset => true) + + run_via_shell rake_command(paths) + end + + def rake_executable + @rake_executable ||= begin + binstubs? ? "#{binstubs}/rake" : 'rake' + end + end + + def rake_command(paths) + cmd_parts = [] + cmd_parts << "bundle exec" if bundle_exec? + cmd_parts << rake_executable + cmd_parts << "spec:specific[\"#{paths.join(';')}\"]" + cmd_parts.compact.join(' ') + end + + def run_via_shell(command) + success = system(command) + + if @options[:notification] && !success + Notifier.notify("Failed", :title => "Motion spec results", :image => :failed, :priority => 2) + end + + success + end + + def all_spec_paths + @options[:spec_paths].map { |spec_path| + Dir.glob("#{spec_path}/**/*_spec.rb") + }.flatten + end + + def bundler_allowed? + if @bundler_allowed.nil? + @bundler_allowed = File.exist?("#{Dir.pwd}/Gemfile") + else + @bundler_allowed + end + end + + def bundler? + if @bundler.nil? + @bundler = bundler_allowed? && @options[:bundler] + else + @bundler + end + end + + def binstubs? + if @binstubs.nil? + @binstubs = !!@options[:binstubs] + else + @binstubs + end + end + + def binstubs + if @options[:binstubs] == true + "bin" + else + @options[:binstubs] + end + end + + def bundle_exec? + bundler? && !binstubs? + end + end + end +end \ No newline at end of file diff --git a/lib/guard/motion/tasks.rb b/lib/guard/motion/tasks.rb new file mode 100644 index 0000000..b887380 --- /dev/null +++ b/lib/guard/motion/tasks.rb @@ -0,0 +1,21 @@ +# include rake task spec:specific only if included in a rake file +if Kernel.const_defined?(:Rake) + desc "Run a specific list of motion specs" + namespace :spec do + task :specific, :files do |task, args| + files = args[:files] + + if files.nil? || files.empty? + puts "No spec file passed to the task." + puts "Please run the task like this: `rake spec:specific[./spec/app_delegate_spec.rb;./spec/other_spec.rb]`" + exit 1 + end + + App.config.spec_mode = true + spec_files = App.config.spec_files.select{|file_path| !(file_path =~ /_spec.rb$/)} + spec_files += files.split(';') + App.config.instance_variable_set("@spec_files", spec_files) + Rake::Task["simulator"].invoke + end + end +end \ No newline at end of file diff --git a/lib/guard/motion/templates/Guardfile b/lib/guard/motion/templates/Guardfile index 2f12e77..244ce7a 100644 --- a/lib/guard/motion/templates/Guardfile +++ b/lib/guard/motion/templates/Guardfile @@ -2,8 +2,8 @@ guard 'motion' do watch(%r{^spec/.+_spec\.rb$}) # RubyMotion App example - watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } + watch(%r{^app/(.+)\.rb$}) { |m| "./spec/#{m[1]}_spec.rb" } # RubyMotion gem example - watch(%r{^lib/[^/]+/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } + watch(%r{^lib/[^/]+/(.+)\.rb$}) { |m| "./spec/#{m[1]}_spec.rb" } end \ No newline at end of file diff --git a/lib/guard/motion/version.rb b/lib/guard/motion/version.rb index 6c3303c..6988068 100644 --- a/lib/guard/motion/version.rb +++ b/lib/guard/motion/version.rb @@ -1,5 +1,5 @@ module Guard module MotionVersion - VERSION = "0.0.1" + VERSION = "0.1.0" end end diff --git a/lib/motion.rb b/lib/motion.rb deleted file mode 100644 index 931285a..0000000 --- a/lib/motion.rb +++ /dev/null @@ -1,50 +0,0 @@ -require 'guard' -require 'guard/guard' - -module Guard - class Motion < Guard - - # Initialize a Guard. - # @param [Array] watchers the Guard file watchers - # @param [Hash] options the custom Guard options - def initialize(watchers = [], options = {}) - super - end - - # Call once when Guard starts. Please override initialize method to init stuff. - # @raise [:task_has_failed] when start has failed - def start - end - - # Called when `stop|quit|exit|s|q|e + enter` is pressed (when Guard quits). - # @raise [:task_has_failed] when stop has failed - def stop - end - - # Called when `reload|r|z + enter` is pressed. - # This method should be mainly used for "reload" (really!) actions like reloading passenger/spork/bundler/... - # @raise [:task_has_failed] when reload has failed - def reload - end - - # Called when just `enter` is pressed - # This method should be principally used for long action like running all specs/tests/... - # @raise [:task_has_failed] when run_all has failed - def run_all - end - - # Called on file(s) modifications that the Guard watches. - # @param [Array] paths the changes files or paths - # @raise [:task_has_failed] when run_on_change has failed - def run_on_changes(paths) - end - - # Called on file(s) deletions that the Guard watches. - # @param [Array] paths the deleted files or paths - # @raise [:task_has_failed] when run_on_change has failed - def run_on_removals(paths) - end - - end - end -end diff --git a/spec/fixtures/bundler/Gemfile b/spec/fixtures/bundler/Gemfile new file mode 100644 index 0000000..070200d --- /dev/null +++ b/spec/fixtures/bundler/Gemfile @@ -0,0 +1,3 @@ +source "http://rubygems.org" + +gem "rake" \ No newline at end of file diff --git a/spec/guard/motion/runner_spec.rb b/spec/guard/motion/runner_spec.rb new file mode 100644 index 0000000..b8d706d --- /dev/null +++ b/spec/guard/motion/runner_spec.rb @@ -0,0 +1,140 @@ +require 'spec_helper' + +module Guard + class Motion + + describe Runner do + subject { described_class.new } + + describe '#run' do + context 'when passed an empty paths list' do + it 'returns false' do + subject.run([]).should be_false + end + end + + context 'in a folder without Bundler' do + before do + Dir.stub(:pwd).and_return(@fixture_path.join('empty')) + end + + it 'runs without bundler' do + subject.should_receive(:system).with( + "rake spec:specific[\"something\"]" + ).and_return(true) + + subject.run(['something']) + end + end + + context 'in a folder with Bundler' do + before do + Dir.stub(:pwd).and_return(@fixture_path.join('bundler')) + end + + it 'runs with Bundler' do + subject.should_receive(:system).with( + "bundle exec rake spec:specific[\"something\"]" + ).and_return(true) + + subject.run(['something']) + end + + describe 'notification' do + it 'notifies when Motion specs fails to execute' do + subject.should_receive(:rake_command) { "`exit 1`" } + ::Guard::Notifier.should_receive(:notify).with( + 'Failed', :title => 'Motion spec results', :image => :failed, :priority => 2 + ) + + subject.run(['spec']) + end + + it 'does not notify that Motion specs failed when the specs pass' do + subject.should_receive(:rake_command) { "`exit 0`" } + ::Guard::Notifier.should_not_receive(:notify) + + subject.run(['spec']) + end + end + + describe 'options' do + + describe ':bundler' do + context ':bundler => false' do + subject { described_class.new(:bundler => false) } + + it 'runs without Bundler' do + subject.should_receive(:system).with( + "rake spec:specific[\"spec\"]" + ).and_return(true) + + subject.run(['spec']) + end + end + end + + describe ':binstubs' do + context ':bundler => false, :binstubs => true' do + subject { described_class.new(:bundler => false, :binstubs => true) } + + it 'runs without Bundler and with binstubs' do + subject.should_receive(:system).with( + "bin/rake spec:specific[\"spec\"]" + ).and_return(true) + + subject.run(['spec']) + end + end + + context ':bundler => true, :binstubs => true' do + subject { described_class.new(:bundler => true, :binstubs => true) } + + it 'runs without Bundler and binstubs' do + subject.should_receive(:system).with( + "bin/rake spec:specific[\"spec\"]" + ).and_return(true) + + subject.run(['spec']) + end + end + + context ':bundler => true, :binstubs => "dir"' do + subject { described_class.new(:bundler => true, :binstubs => 'dir') } + + it 'runs without Bundler and binstubs in custom directory' do + subject.should_receive(:system).with( + "dir/rake spec:specific[\"spec\"]" + ).and_return(true) + + subject.run(['spec']) + end + end + end + + describe ':notification' do + context ':notification => false' do + subject { described_class.new(:notification => false) } + + it 'runs without notification formatter' do + subject.should_receive(:system).with( + "bundle exec rake spec:specific[\"spec\"]" + ).and_return(true) + + subject.run(['spec']) + end + + it "doesn't notify when specs fails" do + subject.should_receive(:system) { mock('res', :success? => false, :exitstatus => 2) } + ::Guard::Notifier.should_not_receive(:notify) + + subject.run(['spec']) + end + end + end + end + end + end + end + end +end \ No newline at end of file diff --git a/spec/guard/motion_spec.rb b/spec/guard/motion_spec.rb new file mode 100644 index 0000000..ecd97d3 --- /dev/null +++ b/spec/guard/motion_spec.rb @@ -0,0 +1,145 @@ +require 'spec_helper' + +module Guard + describe Motion do + let(:default_options) do + { + :all_after_pass => true, :all_on_start => true, :keep_failed => true, + :spec_paths => ['spec'] + } + end + subject { described_class.new } + + let(:runner) { mock(described_class::Runner) } + + before do + described_class::Runner.stub(:new => runner) + end + + shared_examples_for 'clear failed paths' do + it 'should clear the previously failed paths' do + runner.should_receive(:run).with(['spec/foo']) { false } + expect { subject.run_on_changes(['spec/foo']) }.to throw_symbol :task_has_failed + + runner.should_receive(:run) { true } + expect { subject.run_all }.to_not throw_symbol # this actually clears the failed paths + + runner.should_receive(:run).with(['spec/bar']) { true } + subject.run_on_changes(['spec/bar']) + end + end + + describe '.initialize' do + it 'creates a runner' do + described_class::Runner.should_receive(:new).with(default_options.merge(:foo => :bar)) + + described_class.new([], :foo => :bar) + end + end + + describe '#start' do + it 'calls #run_all' do + subject.should_receive(:run_all) + subject.start + end + + context ':all_on_start option is false' do + let(:subject) { subject = described_class.new([], :all_on_start => false) } + + it "doesn't call #run_all" do + subject.should_not_receive(:run_all) + subject.start + end + end + end + + describe '#run_all' do + it "runs all specs specified by the default 'spec_paths' option" do + runner.should_receive(:run) { true } + + subject.run_all + end + + it "throws task_has_failed if specs don't passed" do + runner.should_receive(:run) { false } + + expect { subject.run_all }.to throw_symbol :task_has_failed + end + + it_should_behave_like 'clear failed paths' + end + + describe '#reload' do + it_should_behave_like 'clear failed paths' + end + + describe '#run_on_changes' do + it 'runs rspec with paths' do + runner.should_receive(:run).with(['spec/foo']) { true } + + subject.run_on_changes(['spec/foo']) + end + + # context 'the changed specs pass after failing' do + # it 'calls #run_all' do + # runner.should_receive(:run).with(['spec/foo']) { false } + + # expect { subject.run_on_changes(['spec/foo']) }.to throw_symbol :task_has_failed + + # runner.should_receive(:run).with(['spec/foo']) { true } + # subject.should_receive(:run_all) + + # expect { subject.run_on_changes(['spec/foo']) }.to_not throw_symbol + # end + + # context ':all_after_pass option is false' do + # subject { described_class.new([], :all_after_pass => false) } + + # it "doesn't call #run_all" do + # runner.should_receive(:run).with(['spec/foo']) { false } + + # expect { subject.run_on_changes(['spec/foo']) }.to throw_symbol :task_has_failed + + # runner.should_receive(:run).with(['spec/foo']) { true } + # subject.should_not_receive(:run_all) + + # expect { subject.run_on_changes(['spec/foo']) }.to_not throw_symbol + # end + # end + # end + + context 'the changed specs pass without failing' do + it "doesn't call #run_all" do + runner.should_receive(:run).with(['spec/foo']) { true } + + subject.should_not_receive(:run_all) + + subject.run_on_changes(['spec/foo']) + end + end + + it 'keeps failed spec and rerun them later' do + subject = described_class.new([], :all_after_pass => false) + + runner.should_receive(:run).with(['spec/bar']) { false } + + expect { subject.run_on_changes(['spec/bar']) }.to throw_symbol :task_has_failed + + runner.should_receive(:run).with(['spec/foo', 'spec/bar']) { true } + + subject.run_on_changes(['spec/foo']) + + runner.should_receive(:run).with(['spec/foo']) { true } + + subject.run_on_changes(['spec/foo']) + end + + it "throws task_has_failed if specs doesn't pass" do + runner.should_receive(:run).with(['spec/foo']) { false } + + expect { subject.run_on_changes(['spec/foo']) }.to throw_symbol :task_has_failed + end + end + + end +end \ No newline at end of file diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..d6eaa84 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,17 @@ +require 'guard/motion' +require 'rspec' + +ENV["GUARD_ENV"] = 'test' + +Dir["#{File.expand_path('..', __FILE__)}/support/**/*.rb"].each { |f| require f } + +RSpec.configure do |config| + config.color_enabled = true + config.filter_run :focus => true + config.run_all_when_everything_filtered = true + + config.before(:each) do + @fixture_path = Pathname.new(File.expand_path('../fixtures/', __FILE__)) + @lib_path = Pathname.new(File.expand_path('../../lib/', __FILE__)) + end +end