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.
- data/README.rdoc +79 -31
- data/Rakefile +69 -22
- data/VERSION +1 -1
- data/genspec.gemspec +31 -40
- data/lib/gen_spec.rb +1 -0
- data/lib/genspec.rb +23 -8
- data/lib/genspec/generator_example_group.rb +47 -52
- data/lib/genspec/matchers.rb +59 -0
- data/lib/genspec/matchers/base.rb +148 -0
- data/lib/genspec/matchers/generation_method_matcher.rb +88 -0
- data/lib/genspec/matchers/output_matcher.rb +34 -0
- data/lib/genspec/matchers/result_matcher.rb +28 -0
- data/lib/genspec/shell.rb +99 -0
- data/pkg/genspec-0.1.1.gem +0 -0
- data/pkg/genspec-0.2.0.pre1.gem +0 -0
- data/pkg/genspec-0.2.0.prerails3.1.gem +0 -0
- data/spec/generators/test_rails3_spec.rb +74 -0
- data/spec/rcov.opts +2 -0
- data/spec/rspec.opts +2 -0
- data/spec/spec_helper.rb +10 -3
- data/spec/support/generators/test_rails3/USAGE +8 -0
- data/spec/support/generators/{test → test_rails3}/templates/file +0 -0
- data/spec/support/generators/test_rails3/test_rails3_generator.rb +23 -0
- metadata +48 -40
- data/lib/genspec/generation_matchers.rb +0 -27
- data/lib/genspec/generation_matchers/generation_matcher.rb +0 -147
- data/lib/genspec/generation_matchers/result_matcher.rb +0 -42
- data/pkg/genspec-0.0.0.gem +0 -0
- data/pkg/genspec-0.1.0.gem +0 -0
- data/rdoc/classes/GenSpec.html +0 -124
- data/rdoc/classes/GenSpec/GenerationMatchers.html +0 -197
- data/rdoc/classes/GenSpec/GenerationMatchers/GenerationMatcher.html +0 -363
- data/rdoc/classes/GenSpec/GenerationMatchers/ResultMatcher.html +0 -241
- data/rdoc/classes/GenSpec/GeneratorExampleGroup.html +0 -285
- data/rdoc/created.rid +0 -1
- data/rdoc/files/README_rdoc.html +0 -261
- data/rdoc/files/lib/genspec/generation_matchers/generation_matcher_rb.html +0 -101
- data/rdoc/files/lib/genspec/generation_matchers/result_matcher_rb.html +0 -101
- data/rdoc/files/lib/genspec/generation_matchers_rb.html +0 -109
- data/rdoc/files/lib/genspec/generator_example_group_rb.html +0 -101
- data/rdoc/files/lib/genspec_rb.html +0 -114
- data/rdoc/fr_class_index.html +0 -31
- data/rdoc/fr_file_index.html +0 -32
- data/rdoc/fr_method_index.html +0 -46
- data/rdoc/index.html +0 -26
- data/rdoc/rdoc-style.css +0 -208
- data/spec/generators/test_spec.rb +0 -96
- 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
|