genspec 0.2.0.prerails3.2 → 0.2.0

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.
@@ -16,10 +16,21 @@ module GenSpec
16
16
  # pass :object => true at the end;
17
17
  #
18
18
  # Ex:
19
- #
19
+ #
20
+ # with_args '--orm', 'active_record' do
21
+ # it "should use activerecord" do
22
+ # # . . .
23
+ # end
24
+ # end
25
+ #
26
+ # with_args '--size', 5, :object => true do
27
+ # # . . .
28
+ # end
29
+ #
20
30
  def with_args(*args, &block)
21
31
  options = args.extract_options!
22
32
  args = args.flatten.collect { |c| c.to_s } unless options[:object]
33
+
23
34
  if block_given?
24
35
  context "with arguments #{args.inspect}" do
25
36
  with_args(args, options)
@@ -30,6 +41,68 @@ module GenSpec
30
41
  end
31
42
  end
32
43
 
44
+ # Sets the input stream for this generator.
45
+ #
46
+ # Ex:
47
+ #
48
+ # with_input <<-end_input do
49
+ # y
50
+ # n
51
+ # a
52
+ # end_input
53
+ # it "should overwrite, then skip, then overwrite all" do
54
+ # # . . .
55
+ # end
56
+ # end
57
+ #
58
+ def with_input(string, &block)
59
+ if block_given?
60
+ context "with input string #{string.inspect}" do
61
+ with_input string
62
+ instance_eval &block
63
+ end
64
+ else
65
+ metadata[:generator_input] = string
66
+ end
67
+ end
68
+
69
+ # Executes some code within the generator's source root
70
+ # prior to the generator actually running. Useful for
71
+ # setting up fixtures.
72
+ #
73
+ # Ex:
74
+ #
75
+ # within_source_root do
76
+ # touch "Gemfile"
77
+ # end
78
+ #
79
+ # Optionally, the block may receive a single argument,
80
+ # which is the full path to the temporary directory
81
+ # representing the source root:
82
+ #
83
+ # within_source_root do |tempdir|
84
+ # # . . .
85
+ # end
86
+ #
87
+ def within_source_root(&block)
88
+ metadata[:generator_init_block] = block
89
+ end
90
+
91
+ # Returns an array of all init blocks from the topmost context down to this
92
+ # one, in that order. These blocks will be executed sequentially prior to
93
+ # each run of the generator.
94
+ def generator_init_blocks
95
+ result = []
96
+ result.concat superclass.generator_init_blocks if genspec_subclass?
97
+ result << metadata[:generator_init_block] if metadata[:generator_init_block]
98
+ result
99
+ end
100
+
101
+ # Returns the generator arguments to be used for this context. If this context doesn't
102
+ # have any generator arguments, its superclass is checked, and so on until either the
103
+ # parent isn't a GenSpec or a set of arguments is found. Only the closest argument
104
+ # set is used; any sets specified above the discovered argument set are
105
+ # ignored.
33
106
  def generator_args
34
107
  return metadata[:generator_args] if metadata[:generator_args]
35
108
 
@@ -40,16 +113,45 @@ module GenSpec
40
113
  end
41
114
  end
42
115
 
116
+ # Returns the input stream to be used for this context. If this context doesn't
117
+ # have an input stream, its superclass is checked, and so on until either the
118
+ # parent isn't a GenSpec or an input stream is found. Only the closest input
119
+ # stream is used; any streams specified above the discovered input stream are
120
+ # ignored.
121
+ def generator_input
122
+ return metadata[:generator_input] if metadata[:generator_input]
123
+
124
+ metadata[:generator_input] = if genspec_subclass?
125
+ superclass.generator_input
126
+ else
127
+ nil
128
+ end
129
+ end
130
+
43
131
  alias with_arguments with_args
44
132
  alias generator_arguments generator_args
45
133
 
134
+ # A hash containing the following:
135
+ #
136
+ # :described - the generator to be tested, or the string/symbol representing it
137
+ # :args - any arguments to be used when invoking the generator
138
+ # :input - a string to be used as an input stream, or nil
139
+ # :init_blocks - an array of blocks to be invoked prior to running the generator
140
+ #
141
+ # This hash represents the +subject+ of the spec and this is the object that will
142
+ # ultimately be passed into the GenSpec matchers.
143
+ #
46
144
  def generator_descriptor
47
145
  {
48
146
  :described => target_generator,
49
- :args => generator_args
147
+ :args => generator_args,
148
+ :input => generator_input,
149
+ :init_blocks => generator_init_blocks
50
150
  }
51
151
  end
52
152
 
153
+ # Traverses up the context tree to find the topmost description, which represents
154
+ # the controller to be tested or the string/symbol representing it.
53
155
  def target_generator
54
156
  if genspec_subclass?
55
157
  superclass.target_generator
@@ -58,6 +160,11 @@ module GenSpec
58
160
  end
59
161
  end
60
162
 
163
+ # Returns true if this object's superclass is also a GenSpec.
164
+ #
165
+ # When a context is created, rspec creates a class inheriting from the context's
166
+ # parent. Therefore, this method can be used to recurse up to the highest-level
167
+ # spec that still tests a generator.
61
168
  def genspec_subclass?
62
169
  superclass.include?(GenSpec::GeneratorExampleGroup)
63
170
  end
@@ -1,11 +1,14 @@
1
1
  module GenSpec
2
2
  module Matchers
3
3
  class Base
4
- attr_reader :block, :generator, :args, :described
5
- delegate :source_root, :to => :generator
4
+ attr_reader :block, :generator, :args, :init_blocks
6
5
  attr_reader :destination_root
7
6
  attr_accessor :error
8
7
 
8
+ def source_root
9
+ generator.source_root
10
+ end
11
+
9
12
  def initialize(&block)
10
13
  @block = block if block_given?
11
14
  @matched = false
@@ -18,20 +21,27 @@ module GenSpec
18
21
  def matches?(generator)
19
22
  @described = generator[:described]
20
23
  @args = generator[:args]
24
+ @shell = GenSpec::Shell.new("", generator[:input] || "")
25
+ @init_blocks = generator[:init_blocks]
21
26
 
22
- if @described.kind_of?(Array)
23
- @generator = Rails::Generators.find_by_namespace(*@described)
27
+ if @described.kind_of?(Class)
28
+ @generator = @described
24
29
  else
25
- @generator = Rails::Generators.find_by_namespace(@described)
30
+ if defined?(Rails)
31
+ @generator = Rails::Generators.find_by_namespace(@described)
32
+ else
33
+ @generator = Thor::Util.find_by_namespace(@described)
34
+ end
26
35
  end
27
36
 
28
37
  raise "Could not find generator: #{@described.inspect}" unless @generator
29
38
 
30
- localize_generator!
31
39
  inject_error_handlers!
32
40
  invoking
33
41
  invoke
34
42
  matched?
43
+ ensure
44
+ complete
35
45
  end
36
46
 
37
47
  def matched?
@@ -47,7 +57,13 @@ module GenSpec
47
57
  end
48
58
 
49
59
  protected
60
+ # callback fired after matching process is complete, regardless of success, failure
61
+ # or error
62
+ def complete
63
+ end
64
+
50
65
  # callback which fires just before a generator has been invoked.
66
+ # Allows matchers to inject whatever hooks they need into the generator.
51
67
  def invoking
52
68
  end
53
69
 
@@ -56,16 +72,6 @@ module GenSpec
56
72
  def generated
57
73
  end
58
74
 
59
- def temporary_root
60
- Dir.mktmpdir do |dir|
61
- # need to copy a few files for some methods, ie route_resources
62
- FileUtils.touch(File.join(dir, "Gemfile"))
63
-
64
- # all set.
65
- yield dir
66
- end
67
- end
68
-
69
75
  def spec_file_contents(filename)
70
76
  if @block
71
77
  content = File.read(filename)
@@ -73,28 +79,16 @@ module GenSpec
73
79
  end
74
80
  end
75
81
 
76
- protected
82
+ def shell
83
+ @shell
84
+ end
85
+
77
86
  # Causes errors not to be raised if a generator fails. Useful for testing output,
78
87
  # rather than results.
79
88
  def silence_errors!
80
89
  @errors_silenced = true
81
90
  end
82
91
 
83
- def silence_thor!
84
- @generator.instance_eval do
85
- alias thor_method_added method_added
86
-
87
- # to silence callbacks and errors about reserved keywords
88
- def method_added(meth)
89
- end
90
- end
91
-
92
- yield
93
-
94
- # un-silence errors and callbacks
95
- @generator.instance_eval { alias thor_method_added method_added }
96
- end
97
-
98
92
  private
99
93
  def check_for_errors
100
94
  # generation is complete - check for errors and re-raise it if it's there
@@ -102,47 +96,48 @@ module GenSpec
102
96
  end
103
97
 
104
98
  def invoke
105
- temporary_root do |tempdir|
106
- @destination_root = tempdir
107
- @generator.start(@args || [], {:destination_root => destination_root})
108
- check_for_errors
109
- generated
99
+ Dir.mktmpdir do |tempdir|
100
+ FileUtils.chdir tempdir do
101
+ init_blocks.each do |block|
102
+ block.call(tempdir)
103
+ end
104
+
105
+ @destination_root = tempdir
106
+ stdout, stderr, stdin = $stdout, $stderr, $stdin
107
+ $stdout, $stderr, $stdin = shell.output, shell.output, shell.input
108
+
109
+ begin
110
+ @generator.start(@args || [], {
111
+ :shell => @shell,
112
+ :destination_root => destination_root
113
+ })
114
+ ensure
115
+ $stdout, $stderr, $stdin = stdout, stderr, stdin
116
+ end
117
+
118
+ check_for_errors
119
+ generated
120
+ end
110
121
  end
111
122
  end
112
123
 
113
124
  def inject_error_handlers!
114
- silence_thor! do
115
- @generator.class_eval do
125
+ interceptor = self
126
+ @generator.class_eval do
127
+ no_tasks do
116
128
  def invoke_with_genspec_error_handler(*names, &block)
117
129
  invoke_without_genspec_error_handler(*names, &block)
118
130
  rescue Thor::Error => err
119
- self.class.interceptor.error = err
131
+ # self.class.interceptor.error = err
132
+ interceptor.error = err
120
133
  raise err
121
134
  end
122
-
135
+
123
136
  alias invoke_without_genspec_error_handler invoke
124
137
  alias invoke invoke_with_genspec_error_handler
125
138
  end
126
139
  end
127
140
  end
128
-
129
- def localize_generator!
130
- # subclass the generator in question so that aliasing its methods doesn't
131
- # impact the root generator (which would be a Bad Thing for other specs)
132
- gen = @generator
133
- generator = Class.new(gen)
134
- # we have to force the name in order to avoid nil errors within Thor.
135
- generator.instance_eval "def self.name; #{gen.name.inspect}; end"
136
- @generator = generator
137
-
138
-
139
- # add self as the "interceptor" so that our generator's wrapper methods can
140
- # gain access to this object.
141
- def @generator.interceptor; @interceptor; end
142
- def @generator.interceptor=(a); @interceptor = a; end
143
-
144
- @generator.interceptor = self
145
- end
146
141
  end
147
142
  end
148
143
  end
@@ -1,4 +1,23 @@
1
1
  class GenSpec::Matchers::GenerationMethodMatcher < GenSpec::Matchers::Base
2
+ # The modules whose public instance methods will be converted into GenSpec matchers.
3
+ # See #generation_methods for details.
4
+ #
5
+ # By default, this includes all of the following:
6
+ #
7
+ # * +Thor::Actions+
8
+ # * +Rails::Generators::Actions+
9
+ # * +Rails::Generators::Migration+
10
+ #
11
+ # If Rails has not been loaded, (e.g. you are testing Thor generators, not Rails generators),
12
+ # the Rails modules are silently ignored.
13
+ #
14
+ # You can add any additional modules to this list. Note that you should list them
15
+ # in the form of a String representing the module name, rather than adding the modules
16
+ # themselves. This allows you to add them prior to actually load them.
17
+ #
18
+ # This will only take effect _before_ the specs have been executed; it is best done from
19
+ # within the +spec_helper.rb+ file during the load process.
20
+ #
2
21
  GENERATION_CLASSES = [ 'Thor::Actions', 'Rails::Generators::Actions', 'Rails::Generators::Migration' ]
3
22
 
4
23
  attr_reader :method_name, :method_args
@@ -46,42 +65,80 @@ class GenSpec::Matchers::GenerationMethodMatcher < GenSpec::Matchers::Base
46
65
 
47
66
  protected
48
67
  def invoking
49
- silence_thor! do
50
- generator.class_eval <<-end_code
51
- def #{method_name}_with_intercept(*argus, &block)
52
- expected_args = self.class.interceptor.method_args
68
+ method_name = self.method_name
69
+ interceptor = self
70
+ generator.class_eval do
71
+ no_tasks do
72
+ define_method :"#{method_name}_with_intercept" do |*argus, &block|
73
+ expected_args = interceptor.method_args
53
74
  if expected_args.length > 0
54
75
  actual_args = argus[0...expected_args.length]
55
76
  if actual_args == expected_args
56
- self.class.interceptor.match!
77
+ interceptor.match!
57
78
  else
58
- self.class.interceptor.report_actual_args(actual_args)
79
+ # we've already matched the method, and there are no expected args.
80
+ interceptor.report_actual_args actual_args
59
81
  end
60
82
  else
61
- # we've already matched the method, and there are no expected args.
62
- self.class.interceptor.match!
83
+ interceptor.match!
63
84
  end
64
-
65
- #{method_name}_without_intercept(*argus, &block)
85
+
86
+ send(:"#{method_name}_without_intercept", *argus, &block)
66
87
  end
67
- end_code
68
- generator.send(:alias_method_chain, method_name, :intercept)
69
- end
88
+
89
+ alias_method_chain :"#{method_name}", :intercept
90
+ end
91
+ end
92
+ end
93
+
94
+ def complete
95
+ # we couldn't subclass the generator anonymously because this dirties the
96
+ # rails generator search process. Instead we'll rely on manually de-aliasing
97
+ # the method being monitored.
98
+ method_name = self.method_name
99
+ generator.class_eval do
100
+ no_tasks do
101
+ alias_method :"#{method_name}", :"#{method_name}_without_intercept"
102
+ end
103
+ end
70
104
  end
71
105
 
72
106
  public
73
107
  class << self
108
+ # Returns all public instance methods found in the modules listed in
109
+ # GENERATION_CLASSES. This is the list of methods that will be converted
110
+ # into matchers, which can be used like so:
111
+ #
112
+ # subject.should create_file(. . .)
113
+ #
114
+ # See also GENERATION_CLASSES
115
+ #
74
116
  def generation_methods
75
117
  GENERATION_CLASSES.inject([]) do |arr, mod|
76
- mod = mod.constantize if mod.kind_of?(String)
118
+ if mod.kind_of?(String)
119
+ next arr if !defined?(Rails) && mod =~ /^Rails/
120
+ mod = mod.constantize
121
+ end
77
122
  arr.concat mod.public_instance_methods.collect { |i| i.to_s }.reject { |i| i =~ /=/ }
78
123
  arr
79
- end
124
+ end.uniq.sort
80
125
  end
81
126
 
127
+ # called from GenSpec::Matchers#call_action
128
+ #
129
+ # example:
130
+ # subject.should call_action(:create_file, ...)
131
+ #
132
+ # equivalent to:
133
+ # subject.should GenSpec::Matchers::GenerationMethodMatcher.for_method(:create_file, ...)
134
+ #
82
135
  def for_method(which, *args, &block)
83
- if generation_methods.include?(which.to_s) then self.new(which, *args, &block)
84
- else nil
136
+ if generation_methods.include?(which.to_s)
137
+ new(which, *args, &block)
138
+ else
139
+ raise "Could not find a matcher for '#{which.inspect}'!\n\n" \
140
+ "If this is a custom action, try adding the Thor Action module to GenSpec:\n\n" \
141
+ " GenSpec::Matchers::GenerationMethodMatcher::GENERATION_CLASSES << 'My::Actions'"
85
142
  end
86
143
  end
87
144
  end
@@ -2,7 +2,7 @@ module GenSpec
2
2
  module Matchers
3
3
  class OutputMatcher < GenSpec::Matchers::Base
4
4
  def output
5
- Thor::Base.shell.output.string
5
+ shell.output.string
6
6
  end
7
7
 
8
8
  def initialize(text_or_regexp)
@@ -9,10 +9,20 @@ module GenSpec
9
9
  end
10
10
 
11
11
  def generated
12
- path = File.join(destination_root, filename)
13
- if File.exist?(path)
12
+ if filename
13
+ path = File.join(destination_root, filename)
14
+ if File.exist?(path)
15
+ match!
16
+ spec_file_contents(path)
17
+ end
18
+ else
19
+ # there was no error, so in the context of
20
+ # "should generate", it most certainly
21
+ # generated.
14
22
  match!
15
- spec_file_contents(path)
23
+ if block
24
+ block.call
25
+ end
16
26
  end
17
27
  end
18
28
 
@@ -7,21 +7,26 @@ module GenSpec
7
7
  module Matchers
8
8
  # Valid types: :dependency, :class_collisions, :file, :template, :complex_template, :directory, :readme,
9
9
  # :migration_template, :route_resources
10
- def generate(kind, *args, &block)
10
+ #
11
+ # Examples:
12
+ # subject.should generate(:file, ...)
13
+ # subject.should generate("vendor/plugins/will_paginate/init.rb")
14
+ #
15
+ def generate(kind = nil, *args, &block)
11
16
  if kind.kind_of?(Symbol)
17
+ # subject.should generate(:file, ...)
12
18
  call_action(kind, *args, &block)
13
19
  else
20
+ # subject.should generate("vendor/plugins/will_paginate/init.rb")
14
21
  GenSpec::Matchers::ResultMatcher.new(kind, &block)
15
22
  end
16
23
  end
17
24
 
25
+ # ex:
26
+ # subject.should call_action(:create_file, ...)
27
+ #
18
28
  def call_action(kind, *args, &block)
19
- unless matcher = GenSpec::Matchers::GenerationMethodMatcher.for_method(kind, *args, &block)
20
- raise "Could not find a matcher for '#{kind.inspect}'!\n\n" \
21
- "If this is a custom action, try adding the Thor Action module to GenSpec:\n\n" \
22
- "GenSpec::Matchers::GenerationMethodMatcher::GENERATION_CLASSES << 'My::Actions'"
23
- end
24
- matcher
29
+ GenSpec::Matchers::GenerationMethodMatcher.for_method(kind, *args, &block)
25
30
  end
26
31
 
27
32
  # This tests the content sent to the command line, instead of the generated product.
@@ -33,10 +38,16 @@ module GenSpec
33
38
  class << self
34
39
  def add_shorthand_methods(base)
35
40
  instance_methods = base.instance_methods.collect { |m| m.to_s }
41
+
42
+ # ex:
43
+ # subject.should create_file(...)
44
+ # equivalent to:
45
+ # subject.should call_action(:create_file, ...)
46
+
36
47
  GenSpec::Matchers::GenerationMethodMatcher.generation_methods.each do |method_name|
37
48
  # don't overwrite existing methods. since the user expects this to fire FIRST,
38
- # it's as if this method's been "overridden".
39
- next if instance_methods.include?(method_name)
49
+ # it's as if this method's been "overridden". See #included and #extended.
50
+ next if instance_methods.include?(method_name.to_s)
40
51
  base.class_eval <<-end_code
41
52
  def #{method_name}(*args, &block) # def create_file(*args, &block)
42
53
  call_action(#{method_name.inspect}, *args, &block) # call_action('create_file', *args, &block)
@@ -44,16 +55,6 @@ module GenSpec
44
55
  end_code
45
56
  end
46
57
  end
47
-
48
- # this is to delay definition of the generation method matchers (like #create_file) until
49
- # after initialization, in order to facilitate custom Thor actions.
50
- def included(base)
51
- add_shorthand_methods(base)
52
- end
53
-
54
- def extended(base)
55
- add_shorthand_methods(class << base; self; end)
56
- end
57
58
  end
58
59
  end
59
60
  end
data/lib/genspec/shell.rb CHANGED
@@ -1,10 +1,26 @@
1
+ require 'thor/shell/basic'
2
+
1
3
  module GenSpec
2
4
  # Just like a Thor::Shell::Basic except that input and output are both redirected to
3
5
  # the specified streams. By default, these are initialized to instances of StringIO.
4
6
  class Shell < Thor::Shell::Basic
5
- attr_accessor :input, :output
7
+ attr_accessor :stdin, :stdout, :stderr
8
+ alias_method :input, :stdin
9
+ alias_method :input=, :stdin=
10
+ alias_method :output, :stdout
11
+ alias_method :output=, :stdout=
12
+
13
+ def ask(statement, color = nil)
14
+ say "#{statement} ", color
15
+ response = stdin.gets
16
+ if response
17
+ response.strip
18
+ else
19
+ raise "Asked '#{statement}', but input.gets returned nil!"
20
+ end
21
+ end
6
22
 
7
- def initialize(output="", input="")
23
+ def initialize(output = "", input = "")
8
24
  super()
9
25
  new(output, input)
10
26
  end
@@ -13,81 +29,10 @@ module GenSpec
13
29
  def new(output="", input="")
14
30
  init_stream(:output, output)
15
31
  init_stream(:input, input)
32
+ @stderr = @stdout
16
33
  self
17
34
  end
18
-
19
- # Ask something to the user and receives a response.
20
- #
21
- # ==== Example
22
- # ask("What is your name?")
23
- #
24
- def ask(statement, color=nil)
25
- say("#{statement} ", color)
26
- input.gets.strip
27
- end
28
-
29
- # Say (print) something to the user. If the sentence ends with a whitespace
30
- # or tab character, a new line is not appended (print + flush). Otherwise
31
- # are passed straight to puts (behavior got from Highline).
32
- #
33
- # ==== Example
34
- # say("I know you knew that.")
35
- #
36
- def say(message="", color=nil, force_new_line=(message.to_s !~ /( |\t)$/))
37
- message = message.to_s
38
- message = set_color(message, color) if color
39
-
40
- if force_new_line
41
- output.puts(message)
42
- else
43
- output.print(message)
44
- output.flush
45
- end
46
- end
47
35
 
48
- # Prints a table.
49
- #
50
- # ==== Parameters
51
- # Array[Array[String, String, ...]]
52
- #
53
- # ==== Options
54
- # ident<Integer>:: Ident the first column by ident value.
55
- #
56
- def print_table(table, options={})
57
- return if table.empty?
58
-
59
- formats, ident = [], options[:ident].to_i
60
- options[:truncate] = terminal_width if options[:truncate] == true
61
-
62
- 0.upto(table.first.length - 2) do |i|
63
- maxima = table.max{ |a,b| a[i].size <=> b[i].size }[i].size
64
- formats << "%-#{maxima + 2}s"
65
- end
66
-
67
- formats[0] = formats[0].insert(0, " " * ident)
68
- formats << "%s"
69
-
70
- table.each do |row|
71
- sentence = ""
72
-
73
- row.each_with_index do |column, i|
74
- sentence << formats[i] % column.to_s
75
- end
76
-
77
- sentence = truncate(sentence, options[:truncate]) if options[:truncate]
78
- output.puts sentence
79
- end
80
- end
81
-
82
- # Called if something goes wrong during the execution. This is used by Thor
83
- # internally and should not be used inside your scripts. If someone went
84
- # wrong, you can always raise an exception. If you raise a Thor::Error, it
85
- # will be rescued and wrapped in the method below.
86
- #
87
- def error(statement)
88
- output.puts statement
89
- end
90
-
91
36
  private
92
37
  def init_stream(which, value)
93
38
  if value.kind_of?(String)
@@ -0,0 +1,12 @@
1
+ module GenSpec
2
+ class Version
3
+ MAJOR = 0
4
+ MINOR = 2
5
+ PATCH = 0
6
+ RELEASE = nil
7
+
8
+ STRING = (RELEASE ? [MAJOR, MINOR, PATCH, RELEASE] : [MAJOR, MINOR, PATCH]).join('.')
9
+ end
10
+
11
+ VERSION = Version::STRING
12
+ end