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.
- 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
|