genspec 0.1.1 → 0.2.0.prerails3.1

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.
Files changed (48) hide show
  1. data/README.rdoc +79 -31
  2. data/Rakefile +69 -22
  3. data/VERSION +1 -1
  4. data/genspec.gemspec +31 -40
  5. data/lib/gen_spec.rb +1 -0
  6. data/lib/genspec.rb +23 -8
  7. data/lib/genspec/generator_example_group.rb +47 -52
  8. data/lib/genspec/matchers.rb +59 -0
  9. data/lib/genspec/matchers/base.rb +148 -0
  10. data/lib/genspec/matchers/generation_method_matcher.rb +88 -0
  11. data/lib/genspec/matchers/output_matcher.rb +34 -0
  12. data/lib/genspec/matchers/result_matcher.rb +28 -0
  13. data/lib/genspec/shell.rb +99 -0
  14. data/pkg/genspec-0.1.1.gem +0 -0
  15. data/pkg/genspec-0.2.0.pre1.gem +0 -0
  16. data/pkg/genspec-0.2.0.prerails3.1.gem +0 -0
  17. data/spec/generators/test_rails3_spec.rb +74 -0
  18. data/spec/rcov.opts +2 -0
  19. data/spec/rspec.opts +2 -0
  20. data/spec/spec_helper.rb +10 -3
  21. data/spec/support/generators/test_rails3/USAGE +8 -0
  22. data/spec/support/generators/{test → test_rails3}/templates/file +0 -0
  23. data/spec/support/generators/test_rails3/test_rails3_generator.rb +23 -0
  24. metadata +48 -40
  25. data/lib/genspec/generation_matchers.rb +0 -27
  26. data/lib/genspec/generation_matchers/generation_matcher.rb +0 -147
  27. data/lib/genspec/generation_matchers/result_matcher.rb +0 -42
  28. data/pkg/genspec-0.0.0.gem +0 -0
  29. data/pkg/genspec-0.1.0.gem +0 -0
  30. data/rdoc/classes/GenSpec.html +0 -124
  31. data/rdoc/classes/GenSpec/GenerationMatchers.html +0 -197
  32. data/rdoc/classes/GenSpec/GenerationMatchers/GenerationMatcher.html +0 -363
  33. data/rdoc/classes/GenSpec/GenerationMatchers/ResultMatcher.html +0 -241
  34. data/rdoc/classes/GenSpec/GeneratorExampleGroup.html +0 -285
  35. data/rdoc/created.rid +0 -1
  36. data/rdoc/files/README_rdoc.html +0 -261
  37. data/rdoc/files/lib/genspec/generation_matchers/generation_matcher_rb.html +0 -101
  38. data/rdoc/files/lib/genspec/generation_matchers/result_matcher_rb.html +0 -101
  39. data/rdoc/files/lib/genspec/generation_matchers_rb.html +0 -109
  40. data/rdoc/files/lib/genspec/generator_example_group_rb.html +0 -101
  41. data/rdoc/files/lib/genspec_rb.html +0 -114
  42. data/rdoc/fr_class_index.html +0 -31
  43. data/rdoc/fr_file_index.html +0 -32
  44. data/rdoc/fr_method_index.html +0 -46
  45. data/rdoc/index.html +0 -26
  46. data/rdoc/rdoc-style.css +0 -208
  47. data/spec/generators/test_spec.rb +0 -96
  48. data/spec/support/generators/test/test_generator.rb +0 -29
@@ -0,0 +1,59 @@
1
+ require 'genspec/matchers/base'
2
+ require 'genspec/matchers/result_matcher'
3
+ require 'genspec/matchers/generation_method_matcher'
4
+ require 'genspec/matchers/output_matcher'
5
+
6
+ module GenSpec
7
+ module Matchers
8
+ # Valid types: :dependency, :class_collisions, :file, :template, :complex_template, :directory, :readme,
9
+ # :migration_template, :route_resources
10
+ def generate(kind, *args, &block)
11
+ if kind.kind_of?(Symbol)
12
+ call_action(kind, *args, &block)
13
+ else
14
+ GenSpec::Matchers::ResultMatcher.new(kind, &block)
15
+ end
16
+ end
17
+
18
+ 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
25
+ end
26
+
27
+ # This tests the content sent to the command line, instead of the generated product.
28
+ # Useful for testing help messages, etc.
29
+ def output(text_or_regexp)
30
+ GenSpec::Matchers::OutputMatcher.new(text_or_regexp)
31
+ end
32
+
33
+ class << self
34
+ def add_shorthand_methods(base)
35
+ instance_methods = base.instance_methods.collect { |m| m.to_s }
36
+ GenSpec::Matchers::GenerationMethodMatcher.generation_methods.each do |method_name|
37
+ # 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)
40
+ base.class_eval <<-end_code
41
+ def #{method_name}(*args, &block) # def create_file(*args, &block)
42
+ call_action(#{method_name.inspect}, *args, &block) # call_action('create_file', *args, &block)
43
+ end # end
44
+ end_code
45
+ end
46
+ 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
+ end
58
+ end
59
+ end
@@ -0,0 +1,148 @@
1
+ module GenSpec
2
+ module Matchers
3
+ class Base
4
+ attr_reader :block, :generator, :args, :described
5
+ delegate :source_root, :to => :generator
6
+ attr_reader :destination_root
7
+ attr_accessor :error
8
+
9
+ def initialize(&block)
10
+ @block = block if block_given?
11
+ @matched = false
12
+ end
13
+
14
+ def match!
15
+ @matched = true
16
+ end
17
+
18
+ def matches?(generator)
19
+ @described = generator[:described]
20
+ @args = generator[:args]
21
+
22
+ if @described.kind_of?(Array)
23
+ @generator = Rails::Generators.find_by_namespace(*@described)
24
+ else
25
+ @generator = Rails::Generators.find_by_namespace(@described)
26
+ end
27
+
28
+ raise "Could not find generator: #{@described.inspect}" unless @generator
29
+
30
+ localize_generator!
31
+ inject_error_handlers!
32
+ invoking
33
+ invoke
34
+ matched?
35
+ end
36
+
37
+ def matched?
38
+ @matched
39
+ end
40
+
41
+ def failure_message
42
+ "was supposed to match and didn't"
43
+ end
44
+
45
+ def negative_failure_message
46
+ "was supposed to not match and did"
47
+ end
48
+
49
+ protected
50
+ # callback which fires just before a generator has been invoked.
51
+ def invoking
52
+ end
53
+
54
+ # callback which fires just after a generator has run and after error checking has
55
+ # been performed, if applicable.
56
+ def generated
57
+ end
58
+
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
+ def spec_file_contents(filename)
70
+ if @block
71
+ content = File.read(filename)
72
+ @block.call(content)
73
+ end
74
+ end
75
+
76
+ protected
77
+ # Causes errors not to be raised if a generator fails. Useful for testing output,
78
+ # rather than results.
79
+ def silence_errors!
80
+ @errors_silenced = true
81
+ end
82
+
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
+ private
99
+ def check_for_errors
100
+ # generation is complete - check for errors and re-raise it if it's there
101
+ raise error if error && !@errors_silenced
102
+ end
103
+
104
+ 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
110
+ end
111
+ end
112
+
113
+ def inject_error_handlers!
114
+ silence_thor! do
115
+ @generator.class_eval do
116
+ def invoke_with_genspec_error_handler(*names, &block)
117
+ invoke_without_genspec_error_handler(*names, &block)
118
+ rescue Thor::Error => err
119
+ interceptor.error = err
120
+ raise err
121
+ end
122
+
123
+ alias invoke_without_genspec_error_handler invoke
124
+ alias invoke invoke_with_genspec_error_handler
125
+ end
126
+ end
127
+ 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
+ end
147
+ end
148
+ end
@@ -0,0 +1,88 @@
1
+ class GenSpec::Matchers::GenerationMethodMatcher < GenSpec::Matchers::Base
2
+ GENERATION_CLASSES = [ 'Thor::Actions', 'Rails::Generators::Actions' ]
3
+
4
+ attr_reader :method_name, :method_args
5
+
6
+ def initialize(method_name, *args, &block)
7
+ @method_name = method_name
8
+ @method_args = args
9
+ @actual_args = nil
10
+ super(&block)
11
+ end
12
+
13
+ def report_actual_args(args)
14
+ # save a reference to the set of args that most *closely* matched the expectation.
15
+ return(@actual_args = args) if @actual_args.nil?
16
+ matches = (method_args % args).length
17
+ if matches > (method_args % @actual_args).length
18
+ @actual_args = args
19
+ end
20
+ end
21
+
22
+ def failure_message
23
+ "expected to generate a call to #{method_name.inspect}#{with_args} but #{what}"
24
+ end
25
+
26
+ def negative_failure_message
27
+ "expected not to generate a call to #{method_name.inspect}#{with_args} but it happened anyway"
28
+ end
29
+
30
+ private
31
+ def with_args
32
+ if @method_args.empty?
33
+ ''
34
+ else
35
+ " "+@method_args.inspect
36
+ end
37
+ end
38
+
39
+ def what
40
+ if @actual_args.nil?
41
+ "that did not happen"
42
+ else
43
+ "received #{@actual_args.inspect} instead"
44
+ end
45
+ end
46
+
47
+ protected
48
+ 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
53
+ if expected_args.length > 0
54
+ actual_args = argus[0...expected_args.length]
55
+ if actual_args == expected_args
56
+ self.class.interceptor.match!
57
+ else
58
+ self.class.interceptor.report_actual_args(actual_args)
59
+ end
60
+ else
61
+ # we've already matched the method, and there are no expected args.
62
+ self.class.interceptor.match!
63
+ end
64
+
65
+ #{method_name}_without_intercept(*argus, &block)
66
+ end
67
+ end_code
68
+ generator.send(:alias_method_chain, method_name, :intercept)
69
+ end
70
+ end
71
+
72
+ public
73
+ class << self
74
+ def generation_methods
75
+ GENERATION_CLASSES.inject([]) do |arr, mod|
76
+ mod = mod.constantize if mod.kind_of?(String)
77
+ arr.concat mod.public_instance_methods.collect { |i| i.to_s }.reject { |i| i =~ /=/ }
78
+ arr
79
+ end
80
+ end
81
+
82
+ def for_method(which, *args, &block)
83
+ if generation_methods.include?(which.to_s) then self.new(which, *args, &block)
84
+ else nil
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,34 @@
1
+ module GenSpec
2
+ module Matchers
3
+ class OutputMatcher < GenSpec::Matchers::Base
4
+ def output
5
+ Thor::Base.shell.output.string
6
+ end
7
+
8
+ def initialize(text_or_regexp)
9
+ regexp = if text_or_regexp.kind_of?(Regexp)
10
+ text_or_regexp
11
+ else
12
+ Regexp.compile(Regexp.escape(text_or_regexp), Regexp::MULTILINE)
13
+ end
14
+ @regexp = regexp
15
+ super()
16
+ silence_errors!
17
+ end
18
+
19
+ def generated
20
+ match! if output =~ @regexp
21
+ end
22
+
23
+ def failure_message
24
+ output + "\n" \
25
+ "expected to match #{@regexp.inspect}, but did not"
26
+ end
27
+
28
+ def negative_failure_message
29
+ output + "\n" \
30
+ "expected not to match #{@regexp.inspect}, but did"
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,28 @@
1
+ module GenSpec
2
+ module Matchers
3
+ class ResultMatcher < GenSpec::Matchers::Base
4
+ attr_reader :filename
5
+
6
+ def initialize(filename, &block)
7
+ @filename = filename
8
+ super(&block)
9
+ end
10
+
11
+ def generated
12
+ path = File.join(destination_root, filename)
13
+ if File.exist?(path)
14
+ match!
15
+ spec_file_contents(path)
16
+ end
17
+ end
18
+
19
+ def failure_message
20
+ "Expected to generate #{filename}"
21
+ end
22
+
23
+ def negative_failure_message
24
+ "Expected to not generate #{filename}"
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,99 @@
1
+ module GenSpec
2
+ # Just like a Thor::Shell::Basic except that input and output are both redirected to
3
+ # the specified streams. By default, these are initialized to instances of StringIO.
4
+ class Shell < Thor::Shell::Basic
5
+ attr_accessor :input, :output
6
+
7
+ def initialize(output="", input="")
8
+ super()
9
+ new(output, input)
10
+ end
11
+
12
+ # Reinitializes this Shell with the given input and output streams.
13
+ def new(output="", input="")
14
+ init_stream(:output, output)
15
+ init_stream(:input, input)
16
+ self
17
+ 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
+
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
+ private
92
+ def init_stream(which, value)
93
+ if value.kind_of?(String)
94
+ value = StringIO.new(value)
95
+ end
96
+ send("#{which}=", value)
97
+ end
98
+ end
99
+ end