summaryrefslogtreecommitdiff
path: root/spec/mspec/lib/mspec/utils/script.rb
blob: 3e507d2b4adea72b3e65865697079d164e193e9f (plain)
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
require 'mspec/guards/guard'
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

  def initialize
    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|
        file = File.expand_path name, dir
        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 config[:formatter].nil?
      config[:formatter] = STDOUT.tty? ? SpinnerFormatter : @files.size < 50 ? DottedFormatter : FileFormatter
    end

    if config[:formatter]
      formatter = config[:formatter].new(config[:output])
      formatter.register
      MSpec.store :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

  # 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|
      expanded = File.expand_path(pattern)
      if File.file?(expanded) && expanded.end_with?('.rb')
        return [expanded]
      elsif File.directory?(expanded)
        return Dir["#{expanded}/**/*_spec.rb"].sort
      end
    end

    abort "Could not find spec file #{partial}"
  end

  # Resolves each entry in +list+ to a set of files.
  #
  # If the entry 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(list)
    list.inject([]) do |files, item|
      case item[0]
      when ?^
        files -= entries(item[1..-1])
      when ?:
        key = item[1..-1].to_sym
        files += files(Array(config[key]))
      else
        files += entries(item)
      end
      files
    end
  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
      if patterns.empty?
        puts "No files specified."
        exit 1
      end
    end
    files patterns
  end

  def cores
    require 'etc'
    Etc.nprocessors
  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
    script = new
    script.load_default
    script.try_load '~/.mspecrc'
    script.options
    script.signals
    script.register
    script.setup_env
    require 'mspec'
    script.run
  end
end