Class: PRSpec

Inherits:
Object
  • Object
show all
Defined in:
lib/prspec.rb

Constant Summary collapse

SPEC_FILE_FILTER =
'_spec.rb'
INFO_FILE =
".prspec"

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(args) ⇒ PRSpec

Returns a new instance of PRSpec.



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
# File 'lib/prspec.rb', line 31

def initialize(args)
  @output = ''
  # create tracking file
  yml = { :running_threads => 0 }
  File.open(INFO_FILE, 'w') { |f| f.write yml.to_yaml }
  if (!args.nil? && args.length > 0 && !args[0].nil?)
    opts = parse_args(args)
    if (!opts[:help])
      @num_threads = opts[:thread_count]

      @tests = get_spec_tests(opts)
      if (tests.length > 0)
        process_tests = divide_spec_tests(tests)
        $log.debug "#{tests.length} Spec tests divided among #{@num_threads} arrays."
      else
        $log.warn "No spec tests found.  Exiting."
        exit 1
      end

      $log.info "Creating array of Child Processes..."
      @processes = build_process_array(process_tests, opts)
      
      begin_run(@processes, opts)
    end
  end
ensure
  FileUtils.remove_file(INFO_FILE, :force => true) if File.exists?(INFO_FILE)
end

Instance Attribute Details

#num_threadsObject

Returns the value of attribute num_threads.



27
28
29
# File 'lib/prspec.rb', line 27

def num_threads
  @num_threads
end

#outputObject

Returns the value of attribute output.



27
28
29
# File 'lib/prspec.rb', line 27

def output
  @output
end

#processesObject

Returns the value of attribute processes.



27
28
29
# File 'lib/prspec.rb', line 27

def processes
  @processes
end

#testsObject

Returns the value of attribute tests.



27
28
29
# File 'lib/prspec.rb', line 27

def tests
  @tests
end

Class Method Details

.get_number_of_running_threadsObject



149
150
151
152
# File 'lib/prspec.rb', line 149

def self.get_number_of_running_threads
  prspec_info = YAML.load_file(INFO_FILE)
  return prspec_info[:running_threads].to_i
end

.is_windows?Boolean

Returns:

  • (Boolean)


319
320
321
# File 'lib/prspec.rb', line 319

def self.is_windows?
  return (RUBY_PLATFORM.match(/mingw/i)) ? true : false
end

Instance Method Details

#begin_run(processes, options) ⇒ Object



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
# File 'lib/prspec.rb', line 257

def begin_run(processes, options)
  if (!processes.nil? && processes.length > 0)
    $log.info "Starting all Child Processes..."
    update_running_thread_count(processes.length)
    processes.each do |proc|
      if (proc.is_a?(PRSpecThread) && options.is_a?(Hash))
        proc.start unless options[:test_mode]
      else
        raise "Invalid datatype where PRSpecThread or Hash exepcted.  Found: #{proc.class.to_s}, #{options.class.to_s}"
      end
    end
    $log.info "All processes started..."
    while processes.length > 0
      processes.each do |proc|
        if (!proc.done?) # confirm threads are running
          $log.debug "Thread#{proc.id}: alive..."
        else
          $log.debug "Thread#{proc.id}: done."
          if (options[:serialize])
            puts proc.output unless options[:quiet_mode]
          end
          # collect thread output if in quiet mode
          if (options[:quiet_mode])
            @output << proc.output
          end
          processes.delete(proc) # remove from the array of processes so we don't count it again
          update_running_thread_count(processes.length)
        end
      end
      sleep 0.5 # wait half a second for processes to run and then re-check their status
    end
    $log.info "All processes complete."
  else
    raise "Invalid input passed to method: 'processes' must be a valid Array of PRSpecThread objects"
  end
end

#build_process_array(process_tests, opts) ⇒ Object



249
250
251
252
253
254
255
# File 'lib/prspec.rb', line 249

def build_process_array(process_tests, opts)
  processes = []
  for i in 0..@num_threads-1
    processes[i] = PRSpecThread.new(i, process_tests[i], {'TEST_ENV_NUMBER'=>i, 'HOME'=>nil}, opts)
  end
  return processes
end

#closeObject



313
314
315
316
317
# File 'lib/prspec.rb', line 313

def close
  @processes.each do |proc|
    proc.close
  end
end

#divide_spec_tests(tests) ⇒ Object



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
# File 'lib/prspec.rb', line 223

def divide_spec_tests(tests)
  if (tests.length < @num_threads)
    @num_threads = tests.length
    $log.info "reducing number of threads due to low number of spec tests found: Threads = #{@num_threads}"
  end
  spec_arrays = Array.new(@num_threads)
  num_per_thread = tests.length.fdiv(@num_threads).ceil
  $log.debug "Approximate number of tests per thread: #{num_per_thread}"
  # ensure an even distribution
  i = 0
  tests.each do |tname|
    if (i >= @num_threads)
      i = 0
    end
    if (spec_arrays[i].nil?)
      spec_arrays[i] = []
    end
    
    spec_arrays[i].push(tname)

    i+=1
  end

  return spec_arrays
end

#get_number_of_processorsObject



144
145
146
147
# File 'lib/prspec.rb', line 144

def get_number_of_processors
  count = Parallel.processor_count
  return count
end

#get_spec_files(options) ⇒ Object



161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
# File 'lib/prspec.rb', line 161

def get_spec_files(options)
  base_dir = options[:dir]
  path = options[:path]
  if (path.nil? || path == '')
    path = '.'
  end
  full_path = ""
  if (path.end_with?('.rb'))
    full_path = File.join(base_dir, path.to_s)
  else
    full_path = File.join(base_dir, path.to_s, '**', "*#{SPEC_FILE_FILTER}")
  end
  $log.debug "full_path: #{full_path}"

  files = []
  if (options[:excludes].nil?)
    files = Dir.glob(full_path)
  else
    files = Dir.glob(full_path).reject { |f| f[options[:excludes]] }
  end

  return files
end

#get_spec_tests(options) ⇒ Object



154
155
156
157
158
159
# File 'lib/prspec.rb', line 154

def get_spec_tests(options)
  files = get_spec_files(options)
  tests = get_tests_from_files(files, options)
  $log.debug "Found #{tests.length} tests in #{files.length} files"
  return tests
end

#get_test_description(description_line) ⇒ Object



216
217
218
219
220
221
# File 'lib/prspec.rb', line 216

def get_test_description(description_line)
  # get index of first "'"
  trim_front = description_line.sub(/[\s]*(it)[\s]*(')/,'')
  description = trim_front.sub(/(')[\s]*(,[\s\S]*)*(do|{)[\s\S]*/,'')
  return description.gsub(/["]/,'\"').gsub(/\\'/,"'")
end

#get_tests_from_files(files, options) ⇒ Object



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
# File 'lib/prspec.rb', line 185

def get_tests_from_files(files, options)
  tests = []
  match_test_name_format = /^[\s]*(it)[\s]*(')[\s\S]*(')[\s\S]*(do|{)/
  files.each do |file|
    lines = File.readlines(file)
    for i in 0..lines.length-1
      if lines[i] =~ match_test_name_format 
        m = lines[i]
        match = true
        if (options[:tag_name] != '')
          if (m.rindex(options[:tag_name]).nil? || (m.rindex(options[:tag_value]) <= m.rindex(options[:tag_name]))) 
            match = false
          end
        end
        # if ignore_pending specified then skip tests containing 'pending' on next line
        if (options[:ignore_pending])
          if (i+1 < lines.length-1 && lines[i+1].include?('pending'))
            match = false
          end
        end
        if (match)
          description = get_test_description(m)
          tests.push("\"#{description}\"")
        end
      end
    end
  end

  return tests
end

#parse_args(args) ⇒ Object



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
# File 'lib/prspec.rb', line 60

def parse_args(args)
  $log.debug("Parsing arguments of: #{args}")
  options = {
    :dir=>'.',
    :path=>'spec',
    :thread_count=>get_number_of_processors,
    :test_mode=>false,
    :help=>false,
    :excludes=>nil,
    :rspec_args=>[],
    :tag_name=>'',
    :tag_value=>'',
    :ignore_pending=>false,
    :serialize=>false
  }
  o = OptionParser.new do |opts|
    opts.banner = "Usage: prspec [options]"
    opts.on("-p", "--path PATH", "Relative path from the base directory to search for spec files") do |v|
      $log.debug "path specified... value: #{v}"
      options[:path] = v
    end
    opts.on("-e", "--exclude REGEX", "Regex string used to exclude files") do |v|
      $log.debug "excludes specified... value: #{v}"
      options[:excludes] = v
    end
    opts.on("-d", "--dir DIRECTORY", "The base directory to run from") do |v|
      $log.debug "directory specified... value: #{v}"
      options[:dir] = v
    end
    opts.on("-n", "--num-threads THREADS", "The number of threads to use") do |v|
      $log.debug "number of threads specified... value: #{v}"
      options[:thread_count] = v.to_i
    end
    opts.on("-t", "--tag TAG", "A rspec tag value to filter by") do |v|
      $log.debug "tag filter specified... value: #{v}"
      tag = v
      value = 'true'
      if (v.include?(':')) # split to tag and value
        tag_value = v.split(':')
        tag = ":#{tag_value[0]}"
        value = "#{tag_value[1]}"
      end
      options[:tag_name] = tag
      options[:tag_value] = value
    end
    opts.on("-r", "--rspec-args \"RSPEC_ARGS\"", "Additional arguments to be passed to rspec (must be surrounded with double quotes)") do |v|
      $log.debug "rspec arguments specified... value: #{v}"
      options[:rspec_args] = v.gsub(/"/,'').split(' ') # create an array of each argument
    end
    opts.on("--test-mode", "Do everything except actually starting the test threads") do
      $log.debug "test mode specified... threads will NOT be started."
      options[:test_mode] = true
    end
    opts.on("-q", "--quiet", "Quiet mode. Do not display parallel thread output") do
      $log.debug "quiet mode specified... thread output will not be displayed"
      options[:quiet_mode] = true
    end
    opts.on("-h", "--help", "Display a help message") do 
      $log.debug "help message requested..."
      options[:help] = true
      puts opts
    end
    opts.on("--ignore-pending", "Ignore all pending tests") do
      $log.debug "ignore pending specified... all pending tests will be excluded"
      options[:ignore_pending] = true
    end
    opts.on("-s", "--serialize-output", "Wait for each thread to complete and then output to STDOUT serially") do
      $log.debug "serialize output specified... all threads will output to STDOUT as they complete"
      options[:serialize] = true
    end
  end

  # handle invalid options
  begin 
    o.parse! args
  rescue OptionParser::InvalidOption => e
    $log.error e
    puts o
    exit 1
  end

  return options
end

#running?Boolean

Returns:

  • (Boolean)


303
304
305
306
307
308
309
310
311
# File 'lib/prspec.rb', line 303

def running?
  @processes.each do |proc|
    if (!proc.done?)
      $log.debug "Found running process..."
      return true
    end
  end
  return false
end

#update_running_thread_count(count) ⇒ Object



294
295
296
297
298
299
300
301
# File 'lib/prspec.rb', line 294

def update_running_thread_count(count)
  (file = File.new(INFO_FILE,'w')).flock(File::LOCK_EX)
  yml = { :running_threads => count }
  file.write yml.to_yaml
ensure
  file.flock(File::LOCK_UN)
  file.close
end