spinach 0.0.2 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
data/.travis.yml ADDED
@@ -0,0 +1,8 @@
1
+ env:
2
+ - RBXOPT=-X19 JRUBY_OPTS="--1.9"
3
+ rvm:
4
+ - 1.9.2
5
+ - 1.9.3
6
+ - ruby-head
7
+ - rbx-2.0
8
+ - jruby
data/Rakefile CHANGED
@@ -5,8 +5,12 @@ Bundler::GemHelper.install_tasks
5
5
  require 'rake/testtask'
6
6
  Rake::TestTask.new do |t|
7
7
  t.libs << "test"
8
- t.test_files = FileList['test/**/*_test.rb']
9
- t.verbose = true
8
+ t.test_files = FileList['./test/**/*_test.rb']
9
+ # t.loader = :direct
10
10
  end
11
11
 
12
- task :default => [:test]
12
+ task :spinach do
13
+ exec "bundle exec spinach"
14
+ end
15
+
16
+ task :default => [:test, :spinach]
data/bin/spinach CHANGED
@@ -6,20 +6,10 @@ rescue LoadError
6
6
  require_relative '../lib/spinach'
7
7
  end
8
8
 
9
- Dir.glob(
10
- File.expand_path File.join(Spinach.config[:step_definitions_path], '**', '*.rb')
11
- ).each do |file|
12
- require file
9
+ features = if ARGV.any?
10
+ ARGV
11
+ else
12
+ Dir.glob(File.join 'features', '**', '*.feature')
13
13
  end
14
14
 
15
- Dir.glob(
16
- File.expand_path File.join(Spinach.config[:support_path], '**', '*.rb')
17
- ).each do |file|
18
- require file
19
- end
20
-
21
- ARGV.each do |file|
22
- Spinach::Runner.new(
23
- Spinach::Parser.new(file).parse
24
- ).run
25
- end
15
+ Spinach::Runner.new(features).run
@@ -0,0 +1,10 @@
1
+ Feature: Feature name guessing
2
+ In order to be faster writing steps
3
+ As a test writer
4
+ I want the names of the features to be guessed from the feature class name
5
+
6
+ Scenario: Basic guess
7
+ Given I am writing a feature called "My cool feature"
8
+ And I write a class named "MyCoolFeature"
9
+ When I run spinach
10
+ Then I want "MyCoolFeature" class to be used to run it
@@ -0,0 +1,39 @@
1
+ require 'aruba/api'
2
+ class FeatureNameGuessing < Spinach::Feature
3
+ include Aruba::Api
4
+
5
+ Given 'I am writing a feature called "My cool feature"' do
6
+ write_file('features/my_cool_feature.feature',
7
+ 'Feature: My cool feature
8
+
9
+ Scenario: This is scenario is cool
10
+ When this is so meta
11
+ Then the world is crazy
12
+ ')
13
+ end
14
+
15
+ And 'I write a class named "MyCoolFeature"' do
16
+ write_file('features/steps/my_cool_feature.rb',
17
+ 'class MyCoolFeature < Spinach::Feature
18
+ When "this is so meta" do
19
+ end
20
+
21
+ Then "the world is crazy" do
22
+ end
23
+ end')
24
+ end
25
+
26
+ When 'I run spinach'do
27
+ run_feature 'features/my_cool_feature.feature'
28
+ end
29
+
30
+ Then 'I want "MyCoolFeature" class to be used to run it' do
31
+ all_stderr.must_be_empty
32
+ end
33
+
34
+ private
35
+
36
+ def run_feature(command)
37
+ run "../../bin/spinach #{command}"
38
+ end
39
+ end
@@ -21,8 +21,6 @@ module Spinach
21
21
  include ::Capybara::DSL
22
22
  include InstanceMethods
23
23
 
24
- def before
25
- end
26
24
  def after
27
25
  ::Capybara.current_session.reset! if ::Capybara.app
28
26
  end
@@ -41,7 +41,7 @@ module Spinach
41
41
  # output to the standard output
42
42
  #
43
43
  def default_reporter
44
- @default_reporter || Spinach::Reporter::Stdout
44
+ @default_reporter || Spinach::Reporter::Stdout.new
45
45
  end
46
46
 
47
47
  # Allows you to read the config object using a hash-like syntax.
data/lib/spinach/dsl.rb CHANGED
@@ -4,39 +4,74 @@ module Spinach
4
4
  #
5
5
  module DSL
6
6
 
7
- # Defines an action to perform given a particular step literal.
8
- #
9
- # @param [String] step name
10
- # The step literal
11
- # @param [Proc] block
12
- # action to perform in that step
13
- #
14
- # @example
15
- # class MyFeature << Spinach::Feature
16
- # When "I go to the toilet" do
17
- # @sittin_on_the_toilet.must_equal true
18
- # end
19
- # end
20
- #
21
- %w{When Given Then And But}.each do |connector|
22
- define_method connector do |string, &block|
23
- define_method("#{connector} #{string}", &block)
7
+ def self.included(base)
8
+ base.class_eval do
9
+ include InstanceMethods
10
+ extend ClassMethods
24
11
  end
25
12
  end
26
13
 
27
- # Defines this feature's name
28
- #
29
- # @example
30
- # class MyFeature < Spinach::Feature
31
- # feature "Satisfy needs"
32
- # end
33
- #
34
- def feature(name)
35
- @feature_name = name
14
+ module ClassMethods
15
+
16
+ # Defines an action to perform given a particular step literal.
17
+ #
18
+ # @param [String] step name
19
+ # The step literal
20
+ # @param [Proc] block
21
+ # action to perform in that step
22
+ #
23
+ # @example
24
+ # class MyFeature << Spinach::Feature
25
+ # When "I go to the toilet" do
26
+ # @sittin_on_the_toilet.must_equal true
27
+ # end
28
+ # end
29
+ #
30
+ def Given(string, &block)
31
+ define_method(string, &block)
32
+ end
33
+
34
+ alias_method :When, :Given
35
+ alias_method :Then, :Given
36
+ alias_method :And, :Given
37
+ alias_method :But, :Given
38
+
39
+ # Defines this feature's name
40
+ #
41
+ # @example
42
+ # class MyFeature < Spinach::Feature
43
+ # feature "Satisfy needs"
44
+ # end
45
+ #
46
+ def feature(name)
47
+ @feature_name = name
48
+ end
49
+
50
+ # @return [String] this feature's name
51
+ #
52
+ attr_reader :feature_name
36
53
  end
37
54
 
38
- # @return [String] this feature's name
39
- #
40
- attr_reader :feature_name
55
+ module InstanceMethods
56
+
57
+ # Execute a given step.
58
+ #
59
+ # @param [String] step
60
+ # the step to execute
61
+ #
62
+ def execute_step(step)
63
+ if self.respond_to?(step)
64
+ self.send(step)
65
+ else
66
+ raise Spinach::StepNotDefinedException.new(
67
+ self, step
68
+ )
69
+ end
70
+ end
71
+
72
+ def name
73
+ self.class.feature_name
74
+ end
75
+ end
41
76
  end
42
77
  end
@@ -0,0 +1,28 @@
1
+ module Spinach
2
+ # This class represents the feature raised when Spniach can't find a class
3
+ # for a feature.
4
+ class FeatureNotFoundException < StandardError
5
+ def initialize(options)
6
+ @not_found_class = options.first
7
+ @feature = options.last
8
+ end
9
+
10
+ def message
11
+ "Could not find class for `#{@feature}` feature. Please create a #{@not_found_class}.rb file at #{Spinach.config[:step_definitions_path]}"
12
+ end
13
+ end
14
+
15
+ # This class represents the step raised when Spinach can't find a step for a
16
+ # Scenario.
17
+ #
18
+ class StepNotDefinedException < StandardError
19
+
20
+ attr_reader :feature, :step
21
+
22
+ def initialize(feature, step)
23
+ @feature = feature
24
+ @step = step
25
+ end
26
+ end
27
+
28
+ end
@@ -5,7 +5,7 @@ module Spinach
5
5
  # The feature class is the class where all the features must inherit from.
6
6
  #
7
7
  class Feature
8
- extend DSL
8
+ include DSL
9
9
  include MiniTest::Assertions
10
10
 
11
11
  def before; end;
@@ -6,6 +6,10 @@ module Spinach
6
6
  # needed from the plain feature definition.
7
7
  #
8
8
  class Parser
9
+
10
+ # @param [String] filename
11
+ # the file to parse
12
+ #
9
13
  def initialize(filename)
10
14
  @filename = filename
11
15
  @formatter = Gherkin::Formatter::JSONFormatter.new(nil)
@@ -0,0 +1,75 @@
1
+ # encoding: utf-8
2
+
3
+ module Spinach
4
+ class Reporter
5
+ # The Stdout reporter outputs the runner results to the standard output
6
+ #
7
+ class Stdout < Reporter
8
+
9
+ # Prints the feature name to the standard output
10
+ #
11
+ def feature(name)
12
+ puts "\nFeature: #{name}".white.underline
13
+ end
14
+
15
+ # Prints the scenario name to the standard ouput
16
+ #
17
+ def scenario(name)
18
+ puts "\n Scenario: #{name}".white
19
+ end
20
+
21
+ # Prints the step name to the standard output. If failed, it puts an
22
+ # F! before
23
+ #
24
+ def step(keyword, name, result)
25
+ case result
26
+ when :success
27
+ puts " ✔ #{keyword} #{name}".green
28
+ when :undefined_step
29
+ puts " ? #{keyword} #{name}".yellow
30
+ when :failure
31
+ puts " ✘ #{keyword} #{name}".red
32
+ when :error
33
+ puts " ! #{keyword} #{name}".red
34
+ when :skip
35
+ puts " ~ #{keyword} #{name}".cyan
36
+ end
37
+ end
38
+
39
+ # Prints a blank line at the end
40
+ #
41
+ def end
42
+ puts ""
43
+ end
44
+
45
+ def error_summary(errors)
46
+ puts
47
+ puts " #{"Error summary".underline}"
48
+ puts
49
+ errors.each do |error, step, line, scenario|
50
+ step_file = error.backtrace.detect do |f|
51
+ f =~ /<class:#{scenario.feature.class}>/
52
+ end
53
+ step_file = step_file.split(':')[0..1].join(':') if step_file
54
+
55
+ puts
56
+ puts " #{scenario.feature_name.light_white} :: #{scenario.name.light_blue} :: #{step.light_green} (line #{line})"
57
+ puts " #{step_file}" if step_file
58
+ puts
59
+ puts " * #{error.message}".red
60
+ puts error.backtrace.map {|e| " #{e}"}
61
+ puts
62
+ end
63
+ end
64
+
65
+ def report_exception(exception)
66
+ @errors << exception
67
+ output = exception.message.split("\n").map{ |line|
68
+ " #{line}"
69
+ }.join("\n")
70
+ puts "#{output}\n\n"
71
+ end
72
+
73
+ end
74
+ end
75
+ end
@@ -6,6 +6,10 @@ module Spinach
6
6
  # results
7
7
  #
8
8
  class Reporter
9
+ # Initialize a reporter with an empty error container.
10
+ def initialize
11
+ @errors = []
12
+ end
9
13
 
10
14
  # Receives this hook when a feature is invoked
11
15
  # @param [String] name
@@ -29,7 +33,7 @@ module Spinach
29
33
  # @param [Symbol] result
30
34
  # the step name and its finishing state. May be :success or :failure
31
35
  #
32
- def step(name, result)
36
+ def step(keyword, name, result)
33
37
  raise "Abstract method!"
34
38
  end
35
39
 
@@ -39,43 +43,7 @@ module Spinach
39
43
  raise "Abstract method!"
40
44
  end
41
45
 
42
- # The Stdout reporter outputs the runner results to the standard output
43
- #
44
- class Stdout < Reporter
45
-
46
- # Prints the feature name to the standard output
47
- #
48
- def feature(name)
49
- puts "\nFeature: #{name}".white.underline
50
- end
51
-
52
- # Prints the scenario name to the standard ouput
53
- #
54
- def scenario(name)
55
- puts " Scenario: #{name}".white
56
- end
57
-
58
- # Prints the step name to the standard output. If failed, it puts an
59
- # F! before
60
- #
61
- def step(name, result)
62
- words = name.split(" ")
63
- connector = words.shift
64
- phrase = words.join(" ")
65
- if result == :success
66
- puts " ✔ #{connector} #{phrase}".green
67
- elsif result == :failure
68
- puts " ✘ #{connector} #{phrase}".red
69
- elsif result == :skip
70
- puts " ~ #{connector} #{phrase}".yellow
71
- end
72
- end
73
-
74
- # Prints a blank line at the end
75
- #
76
- def end
77
- puts ""
78
- end
79
- end
80
46
  end
81
47
  end
48
+
49
+ require_relative 'reporter/stdout'
@@ -0,0 +1,72 @@
1
+ module Spinach
2
+ class Runner
3
+ # A feature runner handles a particular Spinach::Feature run.
4
+ #
5
+ class Feature
6
+
7
+ # @param [String] filename
8
+ # path to the feature file. Scenario line could be passed to run just
9
+ # that scenario.
10
+ # @example feature/a_cool_feature.feature:12
11
+ #
12
+ # @param [Spinach::Reporter] reporter
13
+ # the reporter that will log this run
14
+ #
15
+ def initialize(filename, reporter)
16
+ @filename, @scenario_line = filename.split(':')
17
+ @reporter = reporter
18
+ end
19
+
20
+ attr_reader :reporter
21
+ attr_reader :filename
22
+
23
+ # @return [Spinach::Feature]
24
+ # the feature towards which this scenario will be run
25
+ #
26
+ def feature
27
+ @feature ||= Spinach.find_feature(feature_name).new
28
+ end
29
+
30
+ # @return [Hash]
31
+ # the parsed data for this feature
32
+ #
33
+ def data
34
+ @data ||= Spinach::Parser.new(filename).parse
35
+ end
36
+
37
+ # @return [String]
38
+ # this feature's name
39
+ #
40
+ def feature_name
41
+ @feature_name ||= data['name']
42
+ end
43
+
44
+ # @return [Hash]
45
+ # the parsed scenarios for this runner's feature
46
+ #
47
+ def scenarios
48
+ @scenarios ||= data['elements']
49
+ end
50
+
51
+ # Runs this feature
52
+ #
53
+ def run
54
+ reporter.feature(feature_name)
55
+ failures = []
56
+ scenarios.each do |scenario|
57
+ if !@scenario_line || scenario['line'].to_s == @scenario_line
58
+ feature.send(:before)
59
+ failure = Scenario.new(feature_name, feature, scenario, reporter).run
60
+ failures << failure if failure
61
+ feature.send(:after)
62
+ end
63
+ end
64
+
65
+ unless failures.length.zero?
66
+ reporter.error_summary(failures)
67
+ end
68
+ end
69
+
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,55 @@
1
+ module Spinach
2
+ class Runner
3
+ # A Scenario Runner handles a particular scenario run.
4
+ #
5
+ class Scenario
6
+ attr_reader :name, :feature, :feature_name, :steps, :reporter
7
+
8
+ # @param [Spinach::Feature] feature
9
+ # the feature that contains the steps
10
+ #
11
+ # @param [Hash] data
12
+ # the parsed feature data
13
+ #
14
+ # @param [Spinach::Reporter]
15
+ # the reporter
16
+ #
17
+ def initialize(feature_name, feature, data, reporter)
18
+ @feature_name = feature_name
19
+ @name = data['name']
20
+ @steps = data['steps']
21
+ @reporter = reporter
22
+ @feature = feature
23
+ end
24
+
25
+ # Runs this scenario and signals the reporter
26
+ #
27
+ def run
28
+ reporter.scenario(name)
29
+ steps.each do |step|
30
+ keyword = step['keyword'].strip
31
+ name = step['name'].strip
32
+ line = step['line']
33
+ unless @failure
34
+ begin
35
+ feature.execute_step(name)
36
+ reporter.step(keyword, name, :success)
37
+ rescue MiniTest::Assertion => e
38
+ @failure = [e, name, line, self]
39
+ reporter.step(keyword, name, :failure)
40
+ rescue Spinach::StepNotDefinedException => e
41
+ @failure = [e, name, line, self]
42
+ reporter.step(keyword, name, :undefined_step)
43
+ rescue StandardError => e
44
+ @failure = [e, name, line, self]
45
+ reporter.step(keyword, name, :error)
46
+ end
47
+ else
48
+ reporter.step(keyword, name, :skip)
49
+ end
50
+ end
51
+ @failure
52
+ end
53
+ end
54
+ end
55
+ end
@@ -3,84 +3,85 @@ module Spinach
3
3
  # actual calls to the feature classes.
4
4
  #
5
5
  class Runner
6
+
6
7
  # Initializes the runner with a parsed feature
7
- # @param [Hash] data
8
- # the parsed feature data
9
8
  #
10
- def initialize(data)
11
- @feature_name = data['name']
12
- @scenarios = data['elements']
13
- @reporter = Spinach::config.default_reporter.new
14
- end
9
+ # @param [Array<String>] filenames
10
+ # an array of feature filenames to run
11
+ #
12
+ # @param [Hash] options
13
+ #
14
+ # @option options [String] :step_definitions_path
15
+ # The path in which step definitions are found
16
+ #
17
+ # @option options [String] :support_path
18
+ # The path with the support ruby files
19
+ #
20
+ def initialize(filenames, options = {})
21
+ @filenames = filenames
15
22
 
16
- # The default reporter associated to this run
17
- attr_reader :reporter
23
+ @step_definitions_path = options.delete(:step_definitions_path ) ||
24
+ Spinach.config.step_definitions_path
25
+
26
+ @support_path = options.delete(:support_path ) ||
27
+ Spinach.config.support_path
18
28
 
19
- # Returns the feature class for the provided feature data
20
- # @return [Spinach::Feature] feature
21
- # this runner's feature
22
- #
23
- def feature
24
- @feature ||= Spinach.find_feature(@feature_name)
25
29
  end
26
30
 
27
- # @return [Hash]
28
- # the parsed scenarios for this runner's feature
29
- #
30
- def scenarios
31
- @scenarios
31
+ def reporter
32
+ @reporter ||= Spinach::config.default_reporter
32
33
  end
33
34
 
35
+ attr_reader :filenames
36
+
37
+ # The default path where the steps are located
38
+ attr_reader :step_definitions_path
39
+
40
+ # The default path where the support files are located
41
+ attr_reader :support_path
42
+
34
43
  # Runs this runner and outputs the results in a colorful manner.
35
44
  #
36
45
  def run
37
- step_count = 0
38
- reports = []
39
-
40
- reporter.feature(@feature_name)
46
+ require_dependencies
41
47
 
42
- scenarios.each do |scenario|
43
- Scenario.new(self, scenario).run
48
+ filenames.each do |filename|
49
+ Feature.new(filename, reporter).run
44
50
  end
45
51
  reporter.end
46
52
 
47
53
  end
48
54
 
49
- class Scenario
50
- attr_reader :name, :steps
51
-
52
- def initialize(runner, data)
53
- @name = data['name']
54
- @steps = data['steps']
55
- @runner = runner
55
+ # Requires step definitions and support files
56
+ #
57
+ def require_dependencies
58
+ (support_files + step_definition_files).each do |file|
59
+ require file
56
60
  end
61
+ end
57
62
 
58
- def reporter; @runner.reporter; end;
59
-
60
- def feature
61
- @feature ||= @runner.feature.new
62
- end
63
+ # List of step definition files
64
+ #
65
+ # @return [Array<String>] files
66
+ #
67
+ def step_definition_files
68
+ Dir.glob(
69
+ File.expand_path File.join(step_definitions_path, '**', '*.rb')
70
+ )
71
+ end
63
72
 
64
- def run
65
- reporter.scenario(name)
66
- feature.send(:before)
67
- steps.each do |step|
68
- step_name = "#{step['keyword'].strip} #{step['name']}"
69
- unless @failed
70
- begin
71
- feature.send(step_name)
72
- reporter.step(step_name, :success)
73
- rescue MiniTest::Assertion=>e
74
- reporter.step(step_name, :failure)
75
- @failed = true
76
- end
77
- else
78
- reporter.step(step_name, :skip)
79
- end
80
- end
81
- feature.send(:after)
82
- end
73
+ # List of support files
74
+ #
75
+ # @return [Array<String>] files
76
+ #
77
+ def support_files
78
+ Dir.glob(
79
+ File.expand_path File.join(support_path, '**', '*.rb')
80
+ )
83
81
  end
84
82
 
85
83
  end
86
84
  end
85
+
86
+ require_relative 'runner/feature'
87
+ require_relative 'runner/scenario'