spinach 0.0.2 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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'