Module: Verity

Defined in:
lib/verity.rb,
lib/verity/runner.rb,
lib/verity/version.rb,
lib/verity/manifest.rb,
lib/verity/reporter.rb,
lib/verity/assertions.rb,
lib/verity/fingerprint.rb,
lib/verity/configuration.rb,
lib/verity/reporters/colored_dots.rb,
lib/verity/reporters/dots_reporter.rb,
lib/verity/reporters/null_reporter.rb,
lib/verity/reporters/test_reporter.rb,
lib/verity/reporters/composite_reporter.rb,
lib/verity/reporters/documentation_reporter.rb,
lib/verity/reporters/parallel_summary_reporter.rb

Defined Under Namespace

Modules: Assertions, DSL, Fingerprint, Registry, Reporter, Reporters Classes: AssertionError, Configuration, GroupScope, Manifest, Runner, Test, TestTimeoutError

Constant Summary collapse

VERSION =
"0.1.0"

Class Method Summary collapse

Class Method Details

.after_test(&block) ⇒ Object

Public: Register a callback invoked after each individual test.

block - Proc to execute.

Returns the updated callback Array.



395
# File 'lib/verity.rb', line 395

def self.after_test(&block) = hooks[:after_test] << block

.before_test(&block) ⇒ Object

Public: Register a callback invoked before each individual test.

block - Proc to execute.

Returns the updated callback Array.



388
# File 'lib/verity.rb', line 388

def self.before_test(&block) = hooks[:before_test] << block

.before_worker_start(&block) ⇒ Object

Public: Register a callback invoked once per worker process before any tests run (useful for DB setup, connection pooling, etc.).

block - Proc to execute.

Returns the updated callback Array.



381
# File 'lib/verity.rb', line 381

def self.before_worker_start(&block) = hooks[:before_worker_start] << block

.build_reporter(spec) ⇒ Object

Public: Resolve a reporter instance from a CLI or config string.

Built-in names (case-insensitive): “documentation” (“doc”), “colored” (“colored_dots”), “dots”, “null” (“none”, “silent”). Custom reporters use the form “path/to/reporter.rb:ClassName”.

spec - String reporter name or “path:ClassName” pair.

Examples

Verity.build_reporter("dots")
# => #<Verity::Reporters::DotsReporter ...>

Verity.build_reporter("./my_reporter.rb:MyReporter")
# => #<MyReporter ...>

Returns an Object that includes Verity::Reporter. Raises ArgumentError if the spec is blank or unrecognised.

Raises:

  • (ArgumentError)


226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
# File 'lib/verity.rb', line 226

def self.build_reporter(spec)
  raise ArgumentError, "reporter name cannot be blank" if spec.nil? || spec.strip.empty?

  case spec.strip.downcase
  when "documentation", "doc"
    Reporters::DocumentationReporter.new($stdout)
  when "colored", "colored_dots"
    Reporters::ColoredDotsReporter.new($stdout)
  when "dots"
    Reporters::DotsReporter.new($stdout)
  when "null", "none", "silent"
    Reporters::NullReporter.new
  else
    reporter_from_path_and_class(spec.strip)
  end
end

.clear_group_stack!Object

Internal: Reset the current thread’s group stack to empty. Called before loading each test file to prevent cross-file leakage.

Returns an empty Array.



204
205
206
# File 'lib/verity.rb', line 204

def self.clear_group_stack!
  Thread.current[:verity_group_stack] = []
end

.configurationObject



117
118
119
# File 'lib/verity/configuration.rb', line 117

def configuration
  @configuration ||= Configuration.new
end

.configure {|configuration| ... } ⇒ Object

Yields:



121
122
123
# File 'lib/verity/configuration.rb', line 121

def configure
  yield configuration
end

.conflict_exclusion_list(running_resources, tests: Registry.all) ⇒ Object

Public: Build the set of test fingerprints that must not be claimed because they conflict with at least one currently-running test’s resources.

running_resources - Array of resource Hashes (string keys/values) from

Manifest#running_resources.

tests - Array of Verity::Test to check (default: Registry.all).

Returns an Array of fingerprint Strings. Returns [] when no resolvers are registered or running_resources is empty (bypass fast-path).



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

def self.conflict_exclusion_list(running_resources, tests: Registry.all)
  return [] if resource_resolvers.empty? || running_resources.empty?

  tests.select { |t| test_conflicts_with_running?(t.resources, running_resources) }
       .map(&:fingerprint)
end

.effective_tags(test) ⇒ Object

Public: Compute all tags that apply to a test, combining enclosing group tags with the test’s own tags (outer groups first).

test - A Verity::Test instance.

Returns an Array of Symbols.



47
48
49
# File 'lib/verity.rb', line 47

def self.effective_tags(test)
  Array(test.inherited_group_tags).map(&:to_sym) + Array(test.tags).map(&:to_sym)
end

.filter_by_location_filters(tests) ⇒ Object

Internal: Keep tests that match any configured location filter (test line or an enclosing group line). Paths compared via File.expand_path.

tests - Array of Verity::Test (typically after skip/focus narrowing).

Returns a filtered Array.



122
123
124
125
126
127
128
129
130
131
132
# File 'lib/verity.rb', line 122

def self.filter_by_location_filters(tests)
  filters = configuration.location_filters
  return tests if filters.nil? || filters.empty?

  matched = tests.select { |t| filters.any? { |pair| location_filter_match?(t, pair[0], pair[1]) } }
  if matched.empty? && !tests.empty?
    hint = filters.map { |p, l| "#{File.expand_path(p)}:#{l}" }.join(", ")
    warn "verity: no tests matched location filter (#{hint})"
  end
  matched
end

.focus_filter_active?(candidates) ⇒ Boolean

Public: Detect whether focus filtering narrowed the suite — at least one candidate has :focus and at least one does not.

candidates - Array of Verity::Test (already excluding skipped tests).

Returns true when the suite is a strict focus-filtered subset.

Returns:

  • (Boolean)


110
111
112
113
114
# File 'lib/verity.rb', line 110

def self.focus_filter_active?(candidates)
  return false if candidates.empty?

  candidates.any? { focus_tag?(_1) } && candidates.any? { !focus_tag?(_1) }
end

.focus_tag?(test) ⇒ Boolean

Public: Check whether a test is tagged with :focus.

test - A Verity::Test instance.

Returns true if the test has the focus tag.

Returns:

  • (Boolean)


88
# File 'lib/verity.rb', line 88

def self.focus_tag?(test) = effective_tags(test).include?(:focus)

.group_path_for_registrationObject

Internal: Snapshot of nested group titles for the current thread, used at registration time to capture a test’s group ancestry.

Returns a frozen Array of Strings.



172
173
174
175
176
177
# File 'lib/verity.rb', line 172

def self.group_path_for_registration
  stack = Thread.current[:verity_group_stack]
  return [].freeze if stack.nil? || stack.empty?

  stack.map { _1[:title] }.freeze
end

.group_scopes_for_registrationObject

Internal: Enclosing group source scopes for the current thread (outer first).

Returns a frozen Array of GroupScope.



182
183
184
185
186
187
# File 'lib/verity.rb', line 182

def self.group_scopes_for_registration
  stack = Thread.current[:verity_group_stack]
  return [].freeze if stack.nil? || stack.empty?

  stack.map { |g| GroupScope.new(g[:title], g[:file], g[:line]) }.freeze
end

.hooksObject

Internal: Lazily-initialised Hash of lifecycle hook Arrays keyed by :before_worker_start, :before_test, and :after_test.

Returns a Hash.



454
455
456
# File 'lib/verity.rb', line 454

def self.hooks
  @hooks ||= { before_worker_start: [], before_test: [], after_test: [] }
end

.inherited_group_tags_for_registrationObject

Internal: Collect all tags from enclosing groups for the current thread, flattened in nesting order (outermost first).

Returns a frozen Array of Symbols.



193
194
195
196
197
198
# File 'lib/verity.rb', line 193

def self.inherited_group_tags_for_registration
  stack = Thread.current[:verity_group_stack]
  return [].freeze if stack.nil? || stack.empty?

  stack.flat_map { |g| g[:tags] }.freeze
end

.load_discovery!Object

Public: Discover and load all test files according to Configuration#test_globs. Clears the registry, installs fingerprint plans, and loads each file.

Returns nothing meaningful.



470
471
472
473
474
475
476
477
478
479
480
481
482
# File 'lib/verity.rb', line 470

def self.load_discovery!
  Registry.clear
  configuration.test_files.each do |path|
    clear_group_stack!
    abs = File.expand_path(path)
    Verity::Fingerprint.install_plan!(abs)
    begin
      load abs
    ensure
      Verity::Fingerprint.clear_plan!
    end
  end
end

.location_filter_match?(test, path, line) ⇒ Boolean

Internal: True if (path, line) is this test’s ‘test` line or a GroupScope line.

Returns:

  • (Boolean)


135
136
137
138
139
140
# File 'lib/verity.rb', line 135

def self.location_filter_match?(test, path, line)
  exp = File.expand_path(path)
  return true if File.expand_path(test.file) == exp && test.line == line

  test.group_scopes.any? { |g| File.expand_path(g.file) == exp && g.line == line }
end

.pop_groupObject

Internal: Pop the most recent group frame from the current thread’s stack.

Returns the removed Hash entry, or nil.



164
165
166
# File 'lib/verity.rb', line 164

def self.pop_group
  Thread.current[:verity_group_stack]&.pop
end

.push_group(title, tags:, file:, line:) ⇒ Object

Internal: Push a group frame onto the current thread’s group stack. Called by DSL#group during test file loading.

title - String title for the group. tags - Array of Symbols (default []). file - String absolute path of the ‘group` call site. line - Integer line of the `group` call.

Returns the updated stack Array.



151
152
153
154
155
156
157
158
159
# File 'lib/verity.rb', line 151

def self.push_group(title, tags:, file:, line:)
  entry = {
    title: title.to_s,
    tags: Array(tags).map(&:to_sym),
    file: file,
    line: line
  }
  (Thread.current[:verity_group_stack] ||= []) << entry
end

.register_resource(name, conflicts_with:) ⇒ Object

Public: Declare a named resource with conflict rules for parallel scheduling.

name - Symbol resource name. conflicts_with - Conflict specification stored for the scheduler.

Returns the updated resolvers Hash.



404
405
406
# File 'lib/verity.rb', line 404

def self.register_resource(name, conflicts_with:)
  resource_resolvers[name] = conflicts_with
end

.reset_configuration!Object



125
126
127
# File 'lib/verity/configuration.rb', line 125

def reset_configuration!
  @configuration = Configuration.new
end

.resource_resolversObject

Internal: Lazily-initialised Hash mapping resource names to their conflict specifications.

Returns a Hash.



462
463
464
# File 'lib/verity.rb', line 462

def self.resource_resolvers
  @resource_resolvers ||= {}
end

.run(worker_id: 0) ⇒ Object

Public: Main entry point — discover tests, set up the manifest, and execute. When worker_count > 1 the run forks child processes that each claim work from a shared SQLite manifest.

worker_id - Integer base worker id for single-process mode (default 0).

Returns true if every test passed, false otherwise. Raises ArgumentError if parallel mode uses a :memory: manifest. Raises NotImplementedError if fork is unavailable for parallel mode.



524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
# File 'lib/verity.rb', line 524

def self.run(worker_id: 0)
  load_discovery!

  workers = configuration.resolved_worker_count

  path = configuration.manifest_path
  if workers > 1
    if configuration.memory_manifest?
      raise ArgumentError,
            "manifest_path cannot be :memory: when worker_count > 1 (SQLite memory DBs are not shared across processes)"
    end
    unless Process.respond_to?(:fork)
      raise NotImplementedError, "Parallel workers require Kernel#fork (not available on this platform)"
    end

    sync_manifest!(path)
    pids = workers.times.map do |wid|
      fork do
        Verity.send(:run_manifest_child, path, worker_id: wid)
      end
    end
    ok = pids.all? do |pid|
      _, status = Process.wait2(pid)
      status.success?
    end

    manifest = Manifest.open(path)
    begin
      abandoned = manifest.reclaim_abandoned_running!
      ok &&= abandoned.zero?

      rep = configuration.reporter
      rep.on_run_start(total: manifest.example_count, worker_id: 0)

      manifest.each_parallel_replay_result do |result|
        rep.on_test_complete(result: result, worker_id: 0)
      end

      Registry.all.select { skipped?(_1) }.sort_by(&:fingerprint).each do |t|
        rep.on_test_complete(
          result: Runner::Result.new(test: t, status: :skip, error: nil),
          worker_id: 0
        )
      end

      skip_count = Registry.all.count { skipped?(_1) }
      counts = manifest.count_by_status.merge("skipped" => skip_count)
      problem_rows = manifest.failures_for_report
      rep.on_parallel_complete(counts: counts, problem_rows: problem_rows)
    ensure
      manifest.close
    end

    ok
  else
    manifest = Manifest.open(path)
    begin
      manifest.migrate!
      manifest.replace_tests(ordered_runnable_tests)
      Runner.new.run_manifest(manifest, worker_id:)
    ensure
      manifest.close
    end
  end
end

.runnable_testsObject

Public: Collect the tests that should actually execute. Skipped tests are excluded; when any remaining test has :focus, only focused tests are kept.

Returns an Array of Verity::Test.



94
95
96
97
98
99
100
101
102
# File 'lib/verity.rb', line 94

def self.runnable_tests
  base = Registry.all.reject { skipped?(_1) }
  base = if base.any? { focus_tag?(_1) }
    base.select { focus_tag?(_1) }
  else
    base
  end
  filter_by_location_filters(base)
end

.skipped?(test) ⇒ Boolean

Public: Check whether a test is tagged with :skip.

test - A Verity::Test instance.

Returns true if the test should be skipped.

Returns:

  • (Boolean)


81
# File 'lib/verity.rb', line 81

def self.skipped?(test) = effective_tags(test).include?(:skip)

.validate_test_timeout!(timeout) ⇒ Object

Public: Validate a value for Verity::Test#timeout.

Allows nil (no limit). Otherwise timeout must be a finite Numeric strictly greater than zero (Complex is rejected).

Raises ArgumentError when invalid.

Raises:

  • (ArgumentError)


57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/verity.rb', line 57

def self.validate_test_timeout!(timeout)
  return if timeout.nil?

  unless timeout.is_a?(Numeric) && !timeout.is_a?(Complex)
    raise ArgumentError,
          "test timeout must be nil or a positive finite Numeric (got #{timeout.class}: #{timeout.inspect})"
  end

  non_finite =
    (timeout.is_a?(Float) && !timeout.finite?) ||
    (timeout.respond_to?(:infinite?) && !timeout.infinite?.nil?)

  raise ArgumentError, "test timeout must be finite (got #{timeout.inspect})" if non_finite

  return if timeout > 0

  raise ArgumentError, "test timeout must be positive (got #{timeout.inspect})"
end