genspec 0.2.0.prerails3.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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