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
328
329
# File 'lib/henitai/integration.rb', line 316

def build_result(wait_result, log_paths)
  status = scenario_status(wait_result)
  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.new(
    status:,
    stdout:,
    stderr:,
    log_path: log_paths[:log_path],
    exit_status: exit_status_for(wait_result)
  )
end

#combined_log(stdout, stderr) ⇒ Object



462
463
464
465
466
467
# File 'lib/henitai/integration.rb', line 462

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

#exit_status_for(wait_result) ⇒ Object



339
340
341
342
343
344
# File 'lib/henitai/integration.rb', line 339

def exit_status_for(wait_result)
  return nil if wait_result == :timeout
  return nil unless wait_result.respond_to?(:exitstatus)

  wait_result.exitstatus
end

#expand_candidates(base_path, required_path) ⇒ Object



417
418
419
420
421
422
# File 'lib/henitai/integration.rb', line 417

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



350
351
352
353
354
355
356
357
358
359
360
# File 'lib/henitai/integration.rb', line 350

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



451
452
453
454
455
# File 'lib/henitai/integration.rb', line 451

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

  File.read(path)
end

#relative_candidates(spec_file, required_path) ⇒ Object



407
408
409
# File 'lib/henitai/integration.rb', line 407

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

#require_candidates(spec_file, required_path) ⇒ Object



411
412
413
414
415
# File 'lib/henitai/integration.rb', line 411

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



387
388
389
390
391
392
393
394
# File 'lib/henitai/integration.rb', line 387

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)


369
370
371
372
373
# File 'lib/henitai/integration.rb', line 369

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)


375
376
377
378
379
380
381
382
383
384
385
# File 'lib/henitai/integration.rb', line 375

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



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

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



438
439
440
441
442
443
444
445
446
447
448
449
# File 'lib/henitai/integration.rb', line 438

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

#scenario_status(wait_result) ⇒ Object



331
332
333
334
335
336
337
# File 'lib/henitai/integration.rb', line 331

def scenario_status(wait_result)
  return :timeout if wait_result == :timeout
  return :compile_error if exit_status_for(wait_result) == 2
  return :survived if wait_result.respond_to?(:success?) && wait_result.success?

  :killed
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



362
363
364
365
366
367
# File 'lib/henitai/integration.rb', line 362

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

#spawn_suite_process(test_files, log_paths) ⇒ Object



424
425
426
427
428
429
430
431
432
433
434
435
436
# File 'lib/henitai/integration.rb', line 424

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



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

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



457
458
459
460
# File 'lib/henitai/integration.rb', line 457

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