Class: Henitai::Integration::Rspec

Inherits:
Base
  • Object
show all
Defined in:
lib/henitai/integration.rb

Overview

RSpec integration adapter.

Direct Known Subclasses

Minitest

Constant Summary collapse

DEFAULT_SUITE_TIMEOUT =
300.0
REQUIRE_DIRECTIVE_PATTERN =
/
  \A\s*
  (require|require_relative)
  \s*
  (?:\(\s*)?
  ["']([^"']+)["']
  \s*\)?
/x

Instance Method Summary collapse

Methods inherited from Base

#cleanup_process_group, #reap_child, #wait_with_timeout

Instance Method Details

#build_result(wait_result, log_paths) ⇒ Object



316
317
318
319
320
321
322
323
324
325
326
327
# File 'lib/henitai/integration.rb', line 316

def build_result(wait_result, log_paths)
  stdout = read_log_file(log_paths[:stdout_path])
  stderr = read_log_file(log_paths[:stderr_path])
  write_combined_log(log_paths[:log_path], stdout, stderr)

  ScenarioExecutionResult.build(
    wait_result:,
    stdout:,
    stderr:,
    log_path: log_paths[:log_path]
  )
end

#combined_log(stdout, stderr) ⇒ Object



445
446
447
448
449
450
# File 'lib/henitai/integration.rb', line 445

def combined_log(stdout, stderr)
  [
    (stdout.empty? ? nil : "stdout:\n#{stdout}"),
    (stderr.empty? ? nil : "stderr:\n#{stderr}")
  ].compact.join("\n")
end

#expand_candidates(base_path, required_path) ⇒ Object



400
401
402
403
404
405
# File 'lib/henitai/integration.rb', line 400

def expand_candidates(base_path, required_path)
  [
    File.expand_path(required_path, base_path),
    File.expand_path("#{required_path}.rb", base_path)
  ].uniq
end

#fallback_spec_files(subject) ⇒ Object



333
334
335
336
337
338
339
340
341
342
343
# File 'lib/henitai/integration.rb', line 333

def fallback_spec_files(subject)
  return [] unless subject.source_file

  matches = spec_files.select do |path|
    requires_source_file_transitively?(path, subject.source_file)
  rescue StandardError
    false
  end

  matches.empty? ? spec_files : matches
end

#per_test_coverage_supported?Boolean

Returns:

  • (Boolean)


280
281
282
# File 'lib/henitai/integration.rb', line 280

def per_test_coverage_supported?
  true
end

#read_log_file(path) ⇒ Object



434
435
436
437
438
# File 'lib/henitai/integration.rb', line 434

def read_log_file(path)
  return "" unless File.exist?(path)

  File.read(path)
end

#relative_candidates(spec_file, required_path) ⇒ Object



390
391
392
# File 'lib/henitai/integration.rb', line 390

def relative_candidates(spec_file, required_path)
  expand_candidates(File.dirname(spec_file), required_path)
end

#require_candidates(spec_file, required_path) ⇒ Object



394
395
396
397
398
# File 'lib/henitai/integration.rb', line 394

def require_candidates(spec_file, required_path)
  ([File.dirname(spec_file), Dir.pwd] + $LOAD_PATH).flat_map do |base_path|
    expand_candidates(base_path, required_path)
  end
end

#required_files(spec_file) ⇒ Object



370
371
372
373
374
375
376
377
# File 'lib/henitai/integration.rb', line 370

def required_files(spec_file)
  File.read(spec_file).lines.filter_map do |line|
    match = line.match(REQUIRE_DIRECTIVE_PATTERN)
    next unless match

    resolve_required_file(spec_file, match[1].to_s, match[2].to_s)
  end
end

#requires_source_file?(spec_file, source_file) ⇒ Boolean

Returns:

  • (Boolean)


352
353
354
355
356
# File 'lib/henitai/integration.rb', line 352

def requires_source_file?(spec_file, source_file)
  content = File.read(spec_file)
  basename = File.basename(source_file, ".rb")
  content.include?(basename) || content.include?(source_file)
end

#requires_source_file_transitively?(spec_file, source_file, visited = []) ⇒ Boolean

Returns:

  • (Boolean)


358
359
360
361
362
363
364
365
366
367
368
# File 'lib/henitai/integration.rb', line 358

def requires_source_file_transitively?(spec_file, source_file, visited = [])
  normalized_spec_file = File.expand_path(spec_file)
  return false if visited.include?(normalized_spec_file)

  visited << normalized_spec_file
  return true if requires_source_file?(spec_file, source_file)

  required_files(spec_file).any? do |required_file|
    requires_source_file_transitively?(required_file, source_file, visited)
  end
end

#resolve_required_file(spec_file, method_name, required_path) ⇒ Object



379
380
381
382
383
384
385
386
387
388
# File 'lib/henitai/integration.rb', line 379

def resolve_required_file(spec_file, method_name, required_path)
  candidates =
    if method_name == "require_relative"
      relative_candidates(spec_file, required_path)
    else
      require_candidates(spec_file, required_path)
    end

  candidates.find { |candidate| File.file?(candidate) }
end

#run_in_child(mutant:, test_files:, log_paths:) ⇒ Object



421
422
423
424
425
426
427
428
429
430
431
432
# File 'lib/henitai/integration.rb', line 421

def run_in_child(mutant:, test_files:, log_paths:)
  Thread.report_on_exception = false
  with_subprocess_env do
    scenario_log_support.with_coverage_dir(mutant.id) do
      scenario_log_support.capture_child_output(log_paths) do
        return 2 if Mutant::Activator.activate!(mutant) == :compile_error

        run_tests(test_files)
      end
    end
  end
end

#run_mutant(mutant:, test_files:, timeout:) ⇒ Object



276
277
278
# File 'lib/henitai/integration.rb', line 276

def run_mutant(mutant:, test_files:, timeout:)
  RspecProcessRunner.new.run_mutant(self, mutant:, test_files:, timeout:)
end

#run_suite(test_files, timeout: DEFAULT_SUITE_TIMEOUT) ⇒ Object



284
285
286
# File 'lib/henitai/integration.rb', line 284

def run_suite(test_files, timeout: DEFAULT_SUITE_TIMEOUT)
  RspecProcessRunner.new.run_suite(self, test_files, timeout:)
end

#run_tests(test_files) ⇒ Object



298
299
300
301
302
303
304
# File 'lib/henitai/integration.rb', line 298

def run_tests(test_files)
  require "rspec/core"
  status = RSpec::Core::Runner.run(test_files + rspec_options)
  return status if status.is_a?(Integer)

  status == true ? 0 : 1
end

#scenario_log_paths(name) ⇒ Object



306
307
308
309
310
311
312
313
314
# File 'lib/henitai/integration.rb', line 306

def scenario_log_paths(name)
  reports_dir = ENV.fetch("HENITAI_REPORTS_DIR", "reports")
  log_dir = File.join(reports_dir, "mutation-logs")
  {
    stdout_path: File.join(log_dir, "#{name}.stdout.log"),
    stderr_path: File.join(log_dir, "#{name}.stderr.log"),
    log_path: File.join(log_dir, "#{name}.log")
  }
end

#select_tests(subject) ⇒ Object



259
260
261
262
263
264
265
266
267
268
269
270
# File 'lib/henitai/integration.rb', line 259

def select_tests(subject)
  matches = spec_files.select do |path|
    content = File.read(path)
    selection_patterns(subject).any? { |pattern| content.include?(pattern) }
  rescue StandardError
    false
  end

  return matches unless matches.empty?

  fallback_spec_files(subject)
end

#selection_patterns(subject) ⇒ Object



345
346
347
348
349
350
# File 'lib/henitai/integration.rb', line 345

def selection_patterns(subject)
  [
    subject.expression,
    subject.namespace
  ].compact.uniq.sort_by(&:length).reverse
end

#spawn_suite_process(test_files, log_paths) ⇒ Object



407
408
409
410
411
412
413
414
415
416
417
418
419
# File 'lib/henitai/integration.rb', line 407

def spawn_suite_process(test_files, log_paths)
  File.open(log_paths[:stdout_path], "w") do |stdout_file|
    File.open(log_paths[:stderr_path], "w") do |stderr_file|
      Process.spawn(
        subprocess_env,
        *suite_command(test_files),
        out: stdout_file,
        err: stderr_file,
        pgroup: true
      )
    end
  end
end

#spec_filesObject



329
330
331
# File 'lib/henitai/integration.rb', line 329

def spec_files
  Dir.glob("spec/**/*_spec.rb")
end

#suite_command(test_files) ⇒ Object



288
289
290
291
292
293
294
295
296
# File 'lib/henitai/integration.rb', line 288

def suite_command(test_files)
  [
    "bundle", "exec", "ruby",
    "-r", "henitai/rspec_coverage_formatter",
    "-S", "rspec", *test_files,
    "--format", "progress",
    "--format", "Henitai::CoverageFormatter"
  ]
end

#test_filesObject



272
273
274
# File 'lib/henitai/integration.rb', line 272

def test_files
  spec_files
end

#write_combined_log(path, stdout, stderr) ⇒ Object



440
441
442
443
# File 'lib/henitai/integration.rb', line 440

def write_combined_log(path, stdout, stderr)
  FileUtils.mkdir_p(File.dirname(path))
  File.write(path, combined_log(stdout, stderr))
end