1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
|
require 'mspec/guards/guard'
require 'mspec/guards/version'
require 'mspec/utils/warnings'
# MSpecScript provides a skeleton for all the MSpec runner scripts.
class MSpecScript
# Returns the config object. Maintained at the class
# level to easily enable simple config files. See the
# class method +set+.
def self.config
@config ||= {
:path => ['.', 'spec'],
:config_ext => '.mspec'
}
end
# Associates +value+ with +key+ in the config object. Enables
# simple config files of the form:
#
# class MSpecScript
# set :target, "ruby"
# set :files, ["one_spec.rb", "two_spec.rb"]
# end
def self.set(key, value)
config[key] = value
end
# Gets the value of +key+ from the config object. Simplifies
# getting values in a config file:
#
# class MSpecScript
# set :a, 1
# set :b, 2
# set :c, get(:a) + get(:b)
# end
def self.get(key)
config[key]
end
class << self
attr_accessor :child_process
end
# True if the current process is the one going to run the specs with `MSpec.process`.
# False for e.g. `mspec` which exec's to `mspec-run`.
# This is useful in .mspec config files.
def self.child_process?
MSpecScript.child_process
end
def initialize
check_version!
config[:formatter] = nil
config[:includes] = []
config[:excludes] = []
config[:patterns] = []
config[:xpatterns] = []
config[:tags] = []
config[:xtags] = []
config[:profiles] = []
config[:xprofiles] = []
config[:atags] = []
config[:astrings] = []
config[:ltags] = []
config[:abort] = true
@loaded = []
end
# Returns the config object maintained by the instance's class.
# See the class methods +set+ and +config+.
def config
MSpecScript.config
end
# Returns +true+ if the file was located in +config[:path]+,
# possibly appending +config[:config_ext]. Returns +false+
# otherwise.
def try_load(target)
names = [target]
unless target[-6..-1] == config[:config_ext]
names << target + config[:config_ext]
end
names.each do |name|
config[:path].each do |dir|
begin
file = File.expand_path name, dir
rescue ArgumentError
# File.expand_path can issue error e.g. if HOME is not available
next
end
if @loaded.include?(file)
return true
elsif File.exist? file
value = Kernel.load(file)
@loaded << file
return value
end
end
end
false
end
def load(target)
try_load(target) or abort "Could not load config file #{target}"
end
# Attempts to load a default config file. First tries to load
# 'default.mspec'. If that fails, attempts to load a config
# file name constructed from the value of RUBY_ENGINE and the
# first two numbers in RUBY_VERSION. For example, on MRI 1.8.6,
# the file name would be 'ruby.1.8.mspec'.
def load_default
try_load 'default.mspec'
if Object.const_defined?(:RUBY_ENGINE)
engine = RUBY_ENGINE
else
engine = 'ruby'
end
try_load "#{engine}.#{SpecGuard.ruby_version}.mspec"
try_load "#{engine}.mspec"
end
# Callback for enabling custom options. This version is a no-op.
# Provide an implementation specific version in a config file.
# Called by #options after the MSpec-provided options are added.
def custom_options(options)
options.doc " No custom options registered"
end
# Registers all filters and actions.
def register
require 'mspec/runner/formatters/dotted'
require 'mspec/runner/formatters/spinner'
require 'mspec/runner/formatters/file'
require 'mspec/runner/filters'
if formatter = config_formatter
formatter.register
MSpec.formatter = formatter
end
MatchFilter.new(:include, *config[:includes]).register unless config[:includes].empty?
MatchFilter.new(:exclude, *config[:excludes]).register unless config[:excludes].empty?
RegexpFilter.new(:include, *config[:patterns]).register unless config[:patterns].empty?
RegexpFilter.new(:exclude, *config[:xpatterns]).register unless config[:xpatterns].empty?
TagFilter.new(:include, *config[:tags]).register unless config[:tags].empty?
TagFilter.new(:exclude, *config[:xtags]).register unless config[:xtags].empty?
ProfileFilter.new(:include, *config[:profiles]).register unless config[:profiles].empty?
ProfileFilter.new(:exclude, *config[:xprofiles]).register unless config[:xprofiles].empty?
DebugAction.new(config[:atags], config[:astrings]).register if config[:debugger]
custom_register
end
# Makes a formatter specified by :formatter option.
def config_formatter
if config[:formatter].nil?
config[:formatter] = STDOUT.tty? ? SpinnerFormatter : @files.size < 50 ? DottedFormatter : FileFormatter
end
if config[:formatter]
config[:formatter].new(config[:output])
end
end
# Callback for enabling custom actions, etc. This version is a
# no-op. Provide an implementation specific version in a config
# file. Called by #register.
def custom_register
end
# Sets up signal handlers. Only a handler for SIGINT is
# registered currently.
def signals
if config[:abort]
Signal.trap "INT" do
MSpec.actions :abort
puts "\nProcess aborted!"
exit! 1
end
end
end
# Attempts to resolve +partial+ as a file or directory name in the
# following order:
#
# 1. +partial+
# 2. +partial+ + "_spec.rb"
# 3. <tt>File.join(config[:prefix], partial)</tt>
# 4. <tt>File.join(config[:prefix], partial + "_spec.rb")</tt>
#
# If it is a file name, returns the name as an entry in an array.
# If it is a directory, returns all *_spec.rb files in the
# directory and subdirectories.
#
# If unable to resolve +partial+, +Kernel.abort+ is called.
def entries(partial)
file = partial + "_spec.rb"
patterns = [partial, file]
if config[:prefix]
patterns << File.join(config[:prefix], partial)
patterns << File.join(config[:prefix], file)
end
patterns.each do |pattern|
begin
expanded = File.realpath(pattern)
rescue Errno::ENOENT, Errno::ENOTDIR
next
end
if File.file?(expanded) && expanded.end_with?('.rb')
return [expanded]
elsif File.directory?(expanded)
specs = Dir["#{expanded}/**/*_spec.rb"].sort
return specs unless specs.empty?
end
end
abort "Could not find spec file #{partial}"
end
# Resolves each entry in +patterns+ to a set of files.
#
# If the pattern has a leading '^' character, the list of files
# is subtracted from the list of files accumulated to that point.
#
# If the entry has a leading ':' character, the corresponding
# key is looked up in the config object and the entries in the
# value retrieved are processed through #entries.
def files(patterns)
list = []
patterns.each do |pattern|
case pattern[0]
when ?^
list -= entries(pattern[1..-1])
when ?:
key = pattern[1..-1].to_sym
value = config[key]
abort "Key #{pattern} not found in mspec config." unless value
list += files(Array(value))
else
list += entries(pattern)
end
end
list
end
def files_from_patterns(patterns)
unless $0.end_with?("_spec.rb")
if patterns.empty?
patterns = config[:files]
end
if patterns.empty? and File.directory? "./spec"
patterns = ["spec/"]
end
end
list = files(patterns)
abort "No files specified." if list.empty?
list
end
def cores(max)
require 'etc'
[Etc.nprocessors, max].min
end
def setup_env
ENV['MSPEC_RUNNER'] = '1'
unless ENV['RUBY_EXE']
ENV['RUBY_EXE'] = config[:target] if config[:target]
end
unless ENV['RUBY_FLAGS']
ENV['RUBY_FLAGS'] = config[:flags].join(" ") if config[:flags]
end
end
# Instantiates an instance and calls the series of methods to
# invoke the script.
def self.main(child_process = true)
MSpecScript.child_process = child_process
script = new
script.load_default
script.try_load '~/.mspecrc'
script.options
script.signals
script.register
script.setup_env
require 'mspec'
script.run
end
private def check_version!
ruby_version_is ""..."2.6" do
warn "MSpec is supported for Ruby 2.6 and above only"
end
end
end
|