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.1"
Class Method Summary collapse
-
.after_test(&block) ⇒ Object
Public: Register a callback invoked after each individual test.
-
.before_test(&block) ⇒ Object
Public: Register a callback invoked before each individual test.
-
.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.).
-
.build_reporter(spec) ⇒ Object
Public: Resolve a reporter instance from a CLI or config string.
-
.clear_group_stack! ⇒ Object
Internal: Reset the current thread’s group stack to empty.
- .configuration ⇒ Object
- .configure {|configuration| ... } ⇒ Object
-
.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.
-
.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).
-
.filter_by_location_filters(tests) ⇒ Object
Internal: Keep tests that match any configured location filter (test line or an enclosing group line).
-
.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.
-
.focus_tag?(test) ⇒ Boolean
Public: Check whether a test is tagged with :focus.
-
.group_path_for_registration ⇒ Object
Internal: Snapshot of nested group titles for the current thread, used at registration time to capture a test’s group ancestry.
-
.group_scopes_for_registration ⇒ Object
Internal: Enclosing group source scopes for the current thread (outer first).
-
.hooks ⇒ Object
Internal: Lazily-initialised Hash of lifecycle hook Arrays keyed by :before_worker_start, :before_test, and :after_test.
-
.inherited_group_tags_for_registration ⇒ Object
Internal: Collect all tags from enclosing groups for the current thread, flattened in nesting order (outermost first).
-
.load_discovery! ⇒ Object
Public: Discover and load all test files according to Configuration#test_globs.
-
.location_filter_match?(test, path, line) ⇒ Boolean
Internal: True if (path, line) is this test’s ‘test` line or a GroupScope line.
-
.pop_group ⇒ Object
Internal: Pop the most recent group frame from the current thread’s stack.
-
.push_group(title, tags:, file:, line:) ⇒ Object
Internal: Push a group frame onto the current thread’s group stack.
-
.register_resource(name, conflicts_with:) ⇒ Object
Public: Declare a named resource with conflict rules for parallel scheduling.
- .reset_configuration! ⇒ Object
-
.resource_resolvers ⇒ Object
Internal: Lazily-initialised Hash mapping resource names to their conflict specifications.
-
.run(worker_id: 0) ⇒ Object
Public: Main entry point — discover tests, set up the manifest, and execute.
-
.runnable_tests ⇒ Object
Public: Collect the tests that should actually execute.
-
.skipped?(test) ⇒ Boolean
Public: Check whether a test is tagged with :skip.
-
.validate_test_timeout!(timeout) ⇒ Object
Public: Validate a value for Verity::Test#timeout.
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.
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 |
.configuration ⇒ Object
117 118 119 |
# File 'lib/verity/configuration.rb', line 117 def configuration @configuration ||= Configuration.new end |
.configure {|configuration| ... } ⇒ Object
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.(test) Array(test.).map(&:to_sym) + Array(test.).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.(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.
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.
88 |
# File 'lib/verity.rb', line 88 def self.focus_tag?(test) = (test).include?(:focus) |
.group_path_for_registration ⇒ Object
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_registration ⇒ Object
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 |
.hooks ⇒ Object
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_registration ⇒ Object
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. 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.(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.
135 136 137 138 139 140 |
# File 'lib/verity.rb', line 135 def self.location_filter_match?(test, path, line) exp = File.(path) return true if File.(test.file) == exp && test.line == line test.group_scopes.any? { |g| File.(g.file) == exp && g.line == line } end |
.pop_group ⇒ Object
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().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_resolvers ⇒ Object
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_tests ⇒ Object
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.
81 |
# File 'lib/verity.rb', line 81 def self.skipped?(test) = (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.
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 |