regresso 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. checksums.yaml +7 -0
  2. data/.yardopts +3 -0
  3. data/CHANGELOG.md +3 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +258 -0
  6. data/Rakefile +12 -0
  7. data/assets/logo-header.svg +28 -0
  8. data/lib/generators/regresso/install_generator.rb +29 -0
  9. data/lib/generators/regresso/templates/regresso.rb +18 -0
  10. data/lib/regresso/adapters/.gitkeep +0 -0
  11. data/lib/regresso/adapters/base.rb +30 -0
  12. data/lib/regresso/adapters/csv.rb +43 -0
  13. data/lib/regresso/adapters/database.rb +53 -0
  14. data/lib/regresso/adapters/database_snapshot.rb +49 -0
  15. data/lib/regresso/adapters/graphql.rb +75 -0
  16. data/lib/regresso/adapters/graphql_batch.rb +42 -0
  17. data/lib/regresso/adapters/http.rb +70 -0
  18. data/lib/regresso/adapters/json_file.rb +30 -0
  19. data/lib/regresso/adapters/proc.rb +30 -0
  20. data/lib/regresso/ci/github_annotation_formatter.rb +46 -0
  21. data/lib/regresso/ci/github_pr_commenter.rb +55 -0
  22. data/lib/regresso/ci/junit_xml_formatter.rb +42 -0
  23. data/lib/regresso/ci/reporter.rb +49 -0
  24. data/lib/regresso/ci.rb +6 -0
  25. data/lib/regresso/comparator.rb +45 -0
  26. data/lib/regresso/configuration.rb +92 -0
  27. data/lib/regresso/differ.rb +129 -0
  28. data/lib/regresso/difference.rb +45 -0
  29. data/lib/regresso/history/entry.rb +70 -0
  30. data/lib/regresso/history/file_backend.rb +51 -0
  31. data/lib/regresso/history/statistics.rb +65 -0
  32. data/lib/regresso/history/store.rb +122 -0
  33. data/lib/regresso/history/trend_reporter.rb +68 -0
  34. data/lib/regresso/history.rb +7 -0
  35. data/lib/regresso/json_path.rb +55 -0
  36. data/lib/regresso/minitest.rb +107 -0
  37. data/lib/regresso/notifiers/base.rb +33 -0
  38. data/lib/regresso/notifiers/microsoft_teams.rb +60 -0
  39. data/lib/regresso/notifiers/slack.rb +72 -0
  40. data/lib/regresso/notifiers.rb +5 -0
  41. data/lib/regresso/parallel/comparison_result.rb +51 -0
  42. data/lib/regresso/parallel/parallel_result.rb +68 -0
  43. data/lib/regresso/parallel/result_aggregator.rb +25 -0
  44. data/lib/regresso/parallel/runner.rb +71 -0
  45. data/lib/regresso/parallel.rb +6 -0
  46. data/lib/regresso/reporter.rb +68 -0
  47. data/lib/regresso/result.rb +70 -0
  48. data/lib/regresso/rspec/.gitkeep +0 -0
  49. data/lib/regresso/rspec/shared_examples.rb +42 -0
  50. data/lib/regresso/rspec.rb +142 -0
  51. data/lib/regresso/snapshot_manager.rb +57 -0
  52. data/lib/regresso/tasks/ci.rake +51 -0
  53. data/lib/regresso/tasks/history.rake +36 -0
  54. data/lib/regresso/templates/.gitkeep +0 -0
  55. data/lib/regresso/templates/report.html.erb +44 -0
  56. data/lib/regresso/version.rb +6 -0
  57. data/lib/regresso/web_ui/diff_formatter.rb +46 -0
  58. data/lib/regresso/web_ui/public/css/app.css +239 -0
  59. data/lib/regresso/web_ui/public/js/app.js +117 -0
  60. data/lib/regresso/web_ui/result_store.rb +60 -0
  61. data/lib/regresso/web_ui/server.rb +70 -0
  62. data/lib/regresso/web_ui/views/index.erb +58 -0
  63. data/lib/regresso/web_ui/views/layout.erb +13 -0
  64. data/lib/regresso/web_ui.rb +5 -0
  65. data/lib/regresso.rb +46 -0
  66. data/sig/regresso.rbs +4 -0
  67. data/site/Gemfile +7 -0
  68. data/site/assets/logo-header.png +0 -0
  69. data/site/assets/site.css +458 -0
  70. data/site/content/index.md +6 -0
  71. data/site/craze.yml +24 -0
  72. data/site/dist/assets/logo-header.svg +28 -0
  73. data/site/dist/assets/site.css +458 -0
  74. data/site/dist/index.html +232 -0
  75. data/site/dist/search.json +9 -0
  76. data/site/dist/sitemap.xml +7 -0
  77. data/site/templates/index.erb +232 -0
  78. data/site/templates/layout.erb +17 -0
  79. metadata +190 -0
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Regresso
4
+ # Immutable JSON-path-like identifier used in diffs.
5
+ class JsonPath
6
+ # @return [Array<String,Integer>]
7
+ attr_reader :segments
8
+
9
+ # Returns the root path.
10
+ #
11
+ # @return [Regresso::JsonPath]
12
+ def self.root
13
+ new([])
14
+ end
15
+
16
+ # @param segments [Array<String,Integer>]
17
+ def initialize(segments)
18
+ @segments = segments.dup.freeze
19
+ freeze
20
+ end
21
+
22
+ # Returns a new path with the segment appended.
23
+ #
24
+ # @param segment [String,Integer]
25
+ # @return [Regresso::JsonPath]
26
+ def /(segment)
27
+ self.class.new(segments + [segment])
28
+ end
29
+
30
+ # Returns the string representation of the path.
31
+ #
32
+ # @return [String]
33
+ def to_s
34
+ return "$" if segments.empty?
35
+
36
+ segments.reduce("$") do |path, segment|
37
+ if segment.is_a?(Integer)
38
+ "#{path}[#{segment}]"
39
+ else
40
+ "#{path}.#{segment}"
41
+ end
42
+ end
43
+ end
44
+
45
+ # Compares paths by string representation.
46
+ #
47
+ # @param other [Object]
48
+ # @return [Boolean]
49
+ def ==(other)
50
+ return false unless other.respond_to?(:to_s)
51
+
52
+ to_s == other.to_s
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "regresso"
4
+ require "regresso/snapshot_manager"
5
+
6
+ # Minitest helpers for Regresso comparisons.
7
+ # @!parse
8
+ # # Minitest namespace from the minitest gem.
9
+ # module ::Minitest
10
+ # end
11
+ #
12
+ # # Base test class from minitest.
13
+ # class ::Minitest::Test
14
+ # end
15
+
16
+ module Regresso
17
+ # Minitest integration helpers.
18
+ module Minitest
19
+ # Assertion helpers for regression testing.
20
+ module Assertions
21
+ # Asserts that two sources have no regression.
22
+ #
23
+ # @param source_a [Object]
24
+ # @param source_b [Object]
25
+ # @param tolerance [Float]
26
+ # @param ignore [Array<String,Regexp>]
27
+ # @param message [String,nil]
28
+ # @return [void]
29
+ def assert_no_regression(source_a, source_b, tolerance: 0.0, ignore: [], message: nil)
30
+ config = Regresso::Configuration.new
31
+ config.default_tolerance = tolerance
32
+ config.ignore_paths = ignore
33
+
34
+ comparator = Regresso::Comparator.new(
35
+ source_a: normalize_source(source_a),
36
+ source_b: normalize_source(source_b),
37
+ config: config
38
+ )
39
+
40
+ result = comparator.compare
41
+
42
+ msg = message || build_failure_message(result)
43
+ assert result.passed?, msg
44
+ end
45
+
46
+ # Asserts that an object matches a stored snapshot.
47
+ #
48
+ # @param actual [Object]
49
+ # @param snapshot_name [String]
50
+ # @param tolerance [Float]
51
+ # @param ignore [Array<String,Regexp>]
52
+ # @return [void]
53
+ def assert_matches_snapshot(actual, snapshot_name, tolerance: 0.0, ignore: [])
54
+ manager = Regresso::SnapshotManager.new
55
+ path = manager.path_for(snapshot_name)
56
+
57
+ if ENV["UPDATE_SNAPSHOTS"] || !File.exist?(path)
58
+ manager.save(snapshot_name, actual)
59
+ pass
60
+ else
61
+ expected = manager.load(snapshot_name)
62
+ assert_no_regression(expected, actual, tolerance: tolerance, ignore: ignore)
63
+ end
64
+ end
65
+
66
+ private
67
+
68
+ def normalize_source(source)
69
+ case source
70
+ when Regresso::Adapters::Base
71
+ source
72
+ when Hash
73
+ if source[:url] || source[:base_url]
74
+ base_url = source[:base_url] || source[:url]
75
+ Regresso::Adapters::Http.new(**source.merge(base_url: base_url).reject { |k, _| k == :url })
76
+ elsif source[:path] && source[:path].to_s.end_with?(".csv")
77
+ Regresso::Adapters::Csv.new(**source)
78
+ elsif source[:path]
79
+ Regresso::Adapters::JsonFile.new(**source)
80
+ else
81
+ Regresso::Adapters::Proc.new { source }
82
+ end
83
+ when String
84
+ if source.end_with?(".csv")
85
+ Regresso::Adapters::Csv.new(path: source)
86
+ else
87
+ Regresso::Adapters::JsonFile.new(path: source)
88
+ end
89
+ when ::Proc
90
+ Regresso::Adapters::Proc.new(source)
91
+ else
92
+ Regresso::Adapters::Proc.new { source }
93
+ end
94
+ end
95
+
96
+ def build_failure_message(result)
97
+ diffs = result.meaningful_diffs
98
+ "Expected no regression, found #{diffs.size} difference(s):\n" +
99
+ diffs.first(5).map { |diff| " #{diff}" }.join("\n")
100
+ end
101
+ end
102
+ end
103
+ end
104
+
105
+ if defined?(::Minitest::Test)
106
+ ::Minitest::Test.include Regresso::Minitest::Assertions
107
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Regresso
4
+ # Notification backends for reporting results.
5
+ module Notifiers
6
+ # Base class for notification integrations.
7
+ class Base
8
+ # Sends a notification if the notifier deems it necessary.
9
+ #
10
+ # @param result [Regresso::Parallel::ParallelResult]
11
+ # @return [void]
12
+ def notify(result)
13
+ return unless should_notify?(result)
14
+
15
+ send_notification(build_message(result))
16
+ end
17
+
18
+ private
19
+
20
+ def should_notify?(_result)
21
+ raise NotImplementedError, "Notifiers must implement #should_notify?"
22
+ end
23
+
24
+ def build_message(_result)
25
+ raise NotImplementedError, "Notifiers must implement #build_message"
26
+ end
27
+
28
+ def send_notification(_message)
29
+ raise NotImplementedError, "Notifiers must implement #send_notification"
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "faraday"
5
+
6
+ module Regresso
7
+ module Notifiers
8
+ # Microsoft Teams webhook notifier.
9
+ class MicrosoftTeams < Base
10
+ # @param webhook_url [String]
11
+ # @param notify_on [Symbol] :always, :failure, or :success
12
+ def initialize(webhook_url:, notify_on: :failure)
13
+ @webhook_url = webhook_url
14
+ @notify_on = notify_on
15
+ end
16
+
17
+ private
18
+
19
+ def should_notify?(result)
20
+ case @notify_on
21
+ when :always
22
+ true
23
+ when :failure
24
+ !result.success?
25
+ when :success
26
+ result.success?
27
+ else
28
+ false
29
+ end
30
+ end
31
+
32
+ def build_message(result)
33
+ {
34
+ "@type" => "MessageCard",
35
+ "@context" => "http://schema.org/extensions",
36
+ "summary" => "Regresso Report",
37
+ "themeColor" => result.success? ? "2ECC71" : "E74C3C",
38
+ "title" => "Regresso Report",
39
+ "sections" => [
40
+ {
41
+ "facts" => [
42
+ { "name" => "Total", "value" => result.total.to_s },
43
+ { "name" => "Passed", "value" => result.passed.to_s },
44
+ { "name" => "Failed", "value" => result.failed.to_s },
45
+ { "name" => "Errors", "value" => result.errors.to_s }
46
+ ]
47
+ }
48
+ ]
49
+ }
50
+ end
51
+
52
+ def send_notification(message)
53
+ Faraday.post(@webhook_url) do |req|
54
+ req.headers["Content-Type"] = "application/json"
55
+ req.body = JSON.generate(message)
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "faraday"
5
+
6
+ module Regresso
7
+ module Notifiers
8
+ # Slack webhook notifier.
9
+ class Slack < Base
10
+ # @param webhook_url [String]
11
+ # @param channel [String,nil]
12
+ # @param notify_on [Symbol] :always, :failure, or :success
13
+ # @param mention [String,nil]
14
+ # @param username [String,nil]
15
+ # @param icon_emoji [String,nil]
16
+ def initialize(webhook_url:, channel: nil, notify_on: :failure, mention: nil, username: nil, icon_emoji: nil)
17
+ @webhook_url = webhook_url
18
+ @channel = channel
19
+ @notify_on = notify_on
20
+ @mention = mention
21
+ @username = username
22
+ @icon_emoji = icon_emoji
23
+ end
24
+
25
+ private
26
+
27
+ def should_notify?(result)
28
+ case @notify_on
29
+ when :always
30
+ true
31
+ when :failure
32
+ !result.success?
33
+ when :success
34
+ result.success?
35
+ else
36
+ false
37
+ end
38
+ end
39
+
40
+ def build_message(result)
41
+ status_text = result.success? ? "Tests passed" : "Regression detected"
42
+ text = [@mention, status_text].compact.join(" ")
43
+
44
+ message = {
45
+ channel: @channel,
46
+ username: @username,
47
+ icon_emoji: @icon_emoji,
48
+ text: text,
49
+ attachments: [
50
+ {
51
+ color: result.success? ? "good" : "danger",
52
+ fields: [
53
+ { title: "Total", value: result.total.to_s, short: true },
54
+ { title: "Failed", value: result.failed.to_s, short: true },
55
+ { title: "Errors", value: result.errors.to_s, short: true }
56
+ ]
57
+ }
58
+ ]
59
+ }
60
+
61
+ message.delete_if { |_key, value| value.nil? }
62
+ end
63
+
64
+ def send_notification(message)
65
+ Faraday.post(@webhook_url) do |req|
66
+ req.headers["Content-Type"] = "application/json"
67
+ req.body = JSON.generate(message)
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "notifiers/base"
4
+ require_relative "notifiers/slack"
5
+ require_relative "notifiers/microsoft_teams"
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Regresso
4
+ module Parallel
5
+ # Result wrapper for a single comparison execution.
6
+ class ComparisonResult
7
+ # @return [String]
8
+ attr_reader :name
9
+
10
+ # @return [Regresso::Result,nil]
11
+ attr_reader :result
12
+
13
+ # @return [StandardError,nil]
14
+ attr_reader :error
15
+
16
+ # @return [Float]
17
+ attr_reader :duration
18
+
19
+ # @param name [String]
20
+ # @param result [Regresso::Result,nil]
21
+ # @param error [StandardError,nil]
22
+ # @param duration [Float]
23
+ def initialize(name:, result: nil, error: nil, duration:)
24
+ @name = name
25
+ @result = result
26
+ @error = error
27
+ @duration = duration
28
+ end
29
+
30
+ def passed?
31
+ error.nil? && result&.passed?
32
+ end
33
+
34
+ def failed?
35
+ error.nil? && result&.failed?
36
+ end
37
+
38
+ def error?
39
+ !error.nil?
40
+ end
41
+
42
+ # @return [Symbol] :passed, :failed, or :error
43
+ def status
44
+ return :error if error?
45
+ return :passed if passed?
46
+
47
+ :failed
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Regresso
4
+ module Parallel
5
+ # Summary of multiple comparison results.
6
+ class ParallelResult
7
+ # @return [Integer]
8
+ attr_reader :total
9
+
10
+ # @return [Integer]
11
+ attr_reader :passed
12
+
13
+ # @return [Integer]
14
+ attr_reader :failed
15
+
16
+ # @return [Integer]
17
+ attr_reader :errors
18
+
19
+ # @return [Array<Regresso::Parallel::ComparisonResult>]
20
+ attr_reader :results
21
+
22
+ # @return [Float]
23
+ attr_reader :total_duration
24
+
25
+ # @param total [Integer]
26
+ # @param passed [Integer]
27
+ # @param failed [Integer]
28
+ # @param errors [Integer]
29
+ # @param results [Array<Regresso::Parallel::ComparisonResult>]
30
+ # @param total_duration [Float]
31
+ def initialize(total:, passed:, failed:, errors:, results:, total_duration:)
32
+ @total = total
33
+ @passed = passed
34
+ @failed = failed
35
+ @errors = errors
36
+ @results = results
37
+ @total_duration = total_duration
38
+ end
39
+
40
+ # @return [Boolean]
41
+ def success?
42
+ failed.zero? && errors.zero?
43
+ end
44
+
45
+ # @return [Hash]
46
+ def summary
47
+ {
48
+ total: total,
49
+ passed: passed,
50
+ failed: failed,
51
+ errors: errors,
52
+ success_rate: (passed.to_f / total * 100).round(2),
53
+ total_duration: total_duration.round(2)
54
+ }
55
+ end
56
+
57
+ # @return [Array<Regresso::Parallel::ComparisonResult>]
58
+ def failed_results
59
+ results.select(&:failed?)
60
+ end
61
+
62
+ # @return [Array<Regresso::Parallel::ComparisonResult>]
63
+ def error_results
64
+ results.select(&:error?)
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Regresso
4
+ module Parallel
5
+ # Aggregates comparison results into a summary.
6
+ class ResultAggregator
7
+ # @param results [Array<Regresso::Parallel::ComparisonResult>]
8
+ def initialize(results)
9
+ @results = results
10
+ end
11
+
12
+ # @return [Regresso::Parallel::ParallelResult]
13
+ def aggregate
14
+ ParallelResult.new(
15
+ total: @results.size,
16
+ passed: @results.count(&:passed?),
17
+ failed: @results.count(&:failed?),
18
+ errors: @results.count(&:error?),
19
+ results: @results,
20
+ total_duration: @results.sum(&:duration)
21
+ )
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Regresso
4
+ # Parallel execution utilities for multiple comparisons.
5
+ module Parallel
6
+ # Executes multiple comparisons concurrently.
7
+ class Runner
8
+ # Default number of worker threads.
9
+ DEFAULT_WORKERS = 4
10
+
11
+ # @param comparisons [Array<Hash>]
12
+ # @param workers [Integer]
13
+ # @param config [Regresso::Configuration]
14
+ def initialize(comparisons:, workers: DEFAULT_WORKERS, config: Configuration.new)
15
+ @comparisons = comparisons
16
+ @workers = workers
17
+ @config = config
18
+ end
19
+
20
+ # @return [Regresso::Parallel::ParallelResult]
21
+ def run
22
+ results = execute_parallel
23
+ ResultAggregator.new(results).aggregate
24
+ end
25
+
26
+ private
27
+
28
+ def execute_parallel
29
+ queue = Queue.new
30
+ @comparisons.each { |comparison| queue << comparison }
31
+
32
+ threads = @workers.times.map do
33
+ Thread.new do
34
+ thread_results = []
35
+ loop do
36
+ comparison = queue.pop(true) rescue nil
37
+ break unless comparison
38
+
39
+ thread_results << execute_comparison(comparison)
40
+ end
41
+ thread_results
42
+ end
43
+ end
44
+
45
+ threads.flat_map(&:value)
46
+ end
47
+
48
+ def execute_comparison(comparison)
49
+ start_time = Time.now
50
+ comparator = Comparator.new(
51
+ source_a: comparison[:source_a],
52
+ source_b: comparison[:source_b],
53
+ config: @config
54
+ )
55
+ result = comparator.compare
56
+
57
+ ComparisonResult.new(
58
+ name: comparison[:name],
59
+ result: result,
60
+ duration: Time.now - start_time
61
+ )
62
+ rescue StandardError => e
63
+ ComparisonResult.new(
64
+ name: comparison[:name],
65
+ error: e,
66
+ duration: Time.now - start_time
67
+ )
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "parallel/comparison_result"
4
+ require_relative "parallel/parallel_result"
5
+ require_relative "parallel/result_aggregator"
6
+ require_relative "parallel/runner"
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "erb"
5
+
6
+ module Regresso
7
+ # Formats a {Regresso::Result} into text, JSON, or HTML.
8
+ class Reporter
9
+ # @param result [Regresso::Result]
10
+ def initialize(result)
11
+ @result = result
12
+ end
13
+
14
+ # Generates a report string.
15
+ #
16
+ # @param format [Symbol]
17
+ # @return [String]
18
+ def generate(format)
19
+ case format
20
+ when :text
21
+ to_text
22
+ when :json
23
+ to_json
24
+ when :html
25
+ to_html
26
+ else
27
+ raise ArgumentError, "Unknown format: #{format}"
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def to_text
34
+ lines = []
35
+ lines << "Regresso Report"
36
+ lines << "Passed: #{@result.passed?}"
37
+ lines << "Total diffs: #{@result.diffs.size}"
38
+ lines << "Meaningful diffs: #{@result.meaningful_diffs.size}"
39
+ lines << ""
40
+
41
+ @result.meaningful_diffs.each do |diff|
42
+ lines << "- #{diff}"
43
+ end
44
+
45
+ lines.join("\n")
46
+ end
47
+
48
+ def to_json
49
+ {
50
+ summary: @result.summary,
51
+ diffs: @result.diffs.map do |diff|
52
+ {
53
+ path: diff.path.to_s,
54
+ old_value: diff.old_value,
55
+ new_value: diff.new_value,
56
+ type: diff.type
57
+ }
58
+ end
59
+ }.to_json
60
+ end
61
+
62
+ def to_html
63
+ template_path = File.expand_path("templates/report.html.erb", __dir__)
64
+ template = File.read(template_path)
65
+ ERB.new(template).result(binding)
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Regresso
4
+ # Holds the outcome of comparing two sources.
5
+ class Result
6
+ # @return [Object]
7
+ attr_reader :source_a
8
+
9
+ # @return [Object]
10
+ attr_reader :source_b
11
+
12
+ # @return [Array<Regresso::Difference>]
13
+ attr_reader :diffs
14
+
15
+ # @return [Regresso::Configuration]
16
+ attr_reader :config
17
+
18
+ # @param source_a [Object]
19
+ # @param source_b [Object]
20
+ # @param diffs [Array<Regresso::Difference>]
21
+ # @param config [Regresso::Configuration]
22
+ def initialize(source_a:, source_b:, diffs:, config:)
23
+ @source_a = source_a
24
+ @source_b = source_b
25
+ @diffs = diffs
26
+ @config = config
27
+ end
28
+
29
+ # @return [Boolean]
30
+ def passed?
31
+ meaningful_diffs.empty?
32
+ end
33
+
34
+ # @return [Boolean]
35
+ def failed?
36
+ !passed?
37
+ end
38
+
39
+ # Returns diffs not ignored by configuration.
40
+ #
41
+ # @return [Array<Regresso::Difference>]
42
+ def meaningful_diffs
43
+ @meaningful_diffs ||= diffs.reject { |diff| config.ignored?(diff.path) }
44
+ end
45
+
46
+ # Returns a summary hash.
47
+ #
48
+ # @return [Hash]
49
+ def summary
50
+ {
51
+ total_diffs: diffs.size,
52
+ meaningful_diffs: meaningful_diffs.size,
53
+ ignored_diffs: diffs.size - meaningful_diffs.size,
54
+ passed: passed?
55
+ }
56
+ end
57
+
58
+ # Generates a report string for the given format.
59
+ #
60
+ # @param format [Symbol]
61
+ # @return [String]
62
+ def to_report(format: :text)
63
+ unless defined?(Regresso::Reporter)
64
+ raise NotImplementedError, "Reporter is not available"
65
+ end
66
+
67
+ Reporter.new(self).generate(format)
68
+ end
69
+ end
70
+ end
File without changes