genspec 0.1.1 → 0.2.0.prerails3.1

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