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,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module Regresso
6
+ # Rails generators for Regresso setup.
7
+ module Generators
8
+ # Installs Regresso initializer and snapshot directories.
9
+ class InstallGenerator < Rails::Generators::Base
10
+ source_root File.expand_path("templates", __dir__)
11
+
12
+ # Creates config/initializers/regresso.rb.
13
+ def create_initializer
14
+ copy_file "regresso.rb", "config/initializers/regresso.rb"
15
+ end
16
+
17
+ # Creates the snapshot directory with a .gitkeep file.
18
+ def create_snapshot_directory
19
+ empty_directory "spec/snapshots/regresso"
20
+ create_file "spec/snapshots/regresso/.gitkeep"
21
+ end
22
+
23
+ # Adds Regresso snapshot temp files to .gitignore.
24
+ def add_to_gitignore
25
+ append_to_file ".gitignore", "\n# Regresso temporary files\nspec/snapshots/regresso/*.tmp\n"
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ Regresso.configure do |config|
4
+ # Default numeric tolerance for comparisons
5
+ config.default_tolerance = 0.0
6
+
7
+ # Paths to ignore (String or Regexp)
8
+ config.ignore_paths = []
9
+
10
+ # Per-path tolerance overrides
11
+ config.tolerance_overrides = {}
12
+
13
+ # Array order sensitivity
14
+ config.array_order_sensitive = true
15
+
16
+ # Type coercion for numeric comparison
17
+ config.type_coercion = true
18
+ end
File without changes
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Regresso
4
+ # Data source adapters for comparison inputs.
5
+ module Adapters
6
+ # Base interface for adapters.
7
+ class Base
8
+ attr_reader :options
9
+
10
+ # @param options [Hash]
11
+ def initialize(**options)
12
+ @options = options
13
+ end
14
+
15
+ # Fetches data from the source.
16
+ #
17
+ # @return [Object]
18
+ def fetch
19
+ raise NotImplementedError, "Adapters must implement #fetch"
20
+ end
21
+
22
+ # Returns a short description of the source.
23
+ #
24
+ # @return [String]
25
+ def description
26
+ raise NotImplementedError, "Adapters must implement #description"
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "csv"
4
+
5
+ module Regresso
6
+ module Adapters
7
+ # Adapter that parses CSV input into a comparable structure.
8
+ class Csv < Base
9
+ # @param path [String,nil]
10
+ # @param content [String,nil]
11
+ # @param headers [Boolean]
12
+ # @param col_sep [String]
13
+ def initialize(path: nil, content: nil, headers: true, col_sep: ",")
14
+ super()
15
+ @path = path
16
+ @content = content
17
+ @headers = headers
18
+ @col_sep = col_sep
19
+ end
20
+
21
+ # Parses CSV into an array of hashes.
22
+ #
23
+ # @return [Array<Hash>]
24
+ def fetch
25
+ csv_content = @content || File.read(@path)
26
+ rows = CSV.parse(csv_content, headers: @headers, col_sep: @col_sep)
27
+ if @headers
28
+ rows.map(&:to_h)
29
+ else
30
+ rows.map do |row|
31
+ values = row.is_a?(CSV::Row) ? row.fields : row
32
+ values.each_with_index.to_h { |value, index| [index.to_s, value] }
33
+ end
34
+ end
35
+ end
36
+
37
+ # @return [String]
38
+ def description
39
+ @path ? "CSV: #{@path}" : "CSV (inline content)"
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record"
4
+
5
+ module Regresso
6
+ module Adapters
7
+ # Adapter that runs a query against a database connection.
8
+ class Database < Base
9
+ # @param connection_config [Hash]
10
+ # @param query [String]
11
+ # @param params [Hash]
12
+ # @param row_identifier [String,Symbol,nil]
13
+ def initialize(connection_config:, query:, params: {}, row_identifier: nil)
14
+ super()
15
+ @connection_config = connection_config
16
+ @query = query
17
+ @params = params
18
+ @row_identifier = row_identifier
19
+ end
20
+
21
+ # Executes the query and returns rows as hashes.
22
+ #
23
+ # @return [Array<Hash>]
24
+ def fetch
25
+ with_connection do |conn|
26
+ result = conn.exec_query(sanitize_query)
27
+ rows = result.to_a
28
+ @row_identifier ? rows.sort_by { |row| row[@row_identifier.to_s] } : rows
29
+ end
30
+ end
31
+
32
+ # @return [String]
33
+ def description
34
+ "Database Query: #{@query.to_s.tr("\n", " ")[0, 50]}"
35
+ end
36
+
37
+ private
38
+
39
+ def with_connection
40
+ ActiveRecord::Base.establish_connection(@connection_config)
41
+ ActiveRecord::Base.connection_pool.with_connection do |conn|
42
+ yield conn
43
+ end
44
+ ensure
45
+ ActiveRecord::Base.connection_pool.disconnect! if ActiveRecord::Base.connected?
46
+ end
47
+
48
+ def sanitize_query
49
+ ActiveRecord::Base.sanitize_sql_array([@query, @params])
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record"
4
+
5
+ module Regresso
6
+ module Adapters
7
+ # Adapter that collects full table snapshots from a database.
8
+ class DatabaseSnapshot < Base
9
+ # @param connection_config [Hash]
10
+ # @param tables [Array<String,Symbol>]
11
+ # @param where_clause [String,nil]
12
+ def initialize(connection_config:, tables:, where_clause: nil)
13
+ super()
14
+ @connection_config = connection_config
15
+ @tables = tables
16
+ @where_clause = where_clause
17
+ end
18
+
19
+ # Fetches rows for each table into a hash keyed by table name.
20
+ #
21
+ # @return [Hash{String => Array<Hash>}]
22
+ def fetch
23
+ with_connection do |conn|
24
+ @tables.each_with_object({}) do |table, acc|
25
+ query = "SELECT * FROM #{table}"
26
+ query += " WHERE #{@where_clause}" if @where_clause
27
+ acc[table.to_s] = conn.exec_query(query).to_a
28
+ end
29
+ end
30
+ end
31
+
32
+ # @return [String]
33
+ def description
34
+ "Database Snapshot: #{@tables.join(", ")}".strip
35
+ end
36
+
37
+ private
38
+
39
+ def with_connection
40
+ ActiveRecord::Base.establish_connection(@connection_config)
41
+ ActiveRecord::Base.connection_pool.with_connection do |conn|
42
+ yield conn
43
+ end
44
+ ensure
45
+ ActiveRecord::Base.connection_pool.disconnect! if ActiveRecord::Base.connected?
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "faraday"
4
+ require "json"
5
+
6
+ module Regresso
7
+ module Adapters
8
+ # Adapter that executes GraphQL queries over HTTP.
9
+ class GraphQL < Base
10
+ # @param endpoint [String]
11
+ # @param query [String]
12
+ # @param variables [Hash]
13
+ # @param headers [Hash]
14
+ # @param operation_name [String,nil]
15
+ def initialize(endpoint:, query:, variables: {}, headers: {}, operation_name: nil)
16
+ super()
17
+ @endpoint = endpoint
18
+ @query = query
19
+ @variables = variables
20
+ @headers = headers
21
+ @operation_name = operation_name
22
+ end
23
+
24
+ # Executes the GraphQL query and returns the data payload.
25
+ #
26
+ # @return [Object]
27
+ def fetch
28
+ response = connection.post do |req|
29
+ req.headers = default_headers.merge(@headers)
30
+ req.body = {
31
+ query: @query,
32
+ variables: @variables,
33
+ operationName: @operation_name
34
+ }.compact.to_json
35
+ end
36
+
37
+ data = response.body
38
+ raise GraphQLError, data["errors"] if data["errors"]&.any?
39
+
40
+ data["data"]
41
+ end
42
+
43
+ # @return [String]
44
+ def description
45
+ "GraphQL: #{@operation_name || "anonymous"}"
46
+ end
47
+
48
+ private
49
+
50
+ def connection
51
+ @connection ||= Faraday.new(url: @endpoint) do |faraday|
52
+ faraday.request :json
53
+ faraday.response :json
54
+ faraday.adapter Faraday.default_adapter
55
+ end
56
+ end
57
+
58
+ def default_headers
59
+ { "Content-Type" => "application/json", "Accept" => "application/json" }
60
+ end
61
+ end
62
+
63
+ # Raised when a GraphQL response contains errors.
64
+ class GraphQLError < StandardError
65
+ # @return [Array<Hash>]
66
+ attr_reader :errors
67
+
68
+ # @param errors [Array<Hash>]
69
+ def initialize(errors)
70
+ @errors = errors
71
+ super(errors.map { |error| error["message"] }.join(", "))
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "regresso/adapters/graphql"
4
+
5
+ module Regresso
6
+ module Adapters
7
+ # Adapter that runs multiple GraphQL queries and returns a keyed hash.
8
+ class GraphQLBatch < Base
9
+ # @param endpoint [String]
10
+ # @param queries [Array<Hash>]
11
+ # @param headers [Hash]
12
+ def initialize(endpoint:, queries:, headers: {})
13
+ super()
14
+ @endpoint = endpoint
15
+ @queries = queries
16
+ @headers = headers
17
+ end
18
+
19
+ # Executes all configured queries.
20
+ #
21
+ # @return [Hash{String => Object}]
22
+ def fetch
23
+ @queries.each_with_object({}) do |query, acc|
24
+ adapter = GraphQL.new(
25
+ endpoint: @endpoint,
26
+ query: query.fetch(:query),
27
+ variables: query[:variables] || {},
28
+ headers: @headers,
29
+ operation_name: query[:operation_name]
30
+ )
31
+
32
+ acc[query.fetch(:name).to_s] = adapter.fetch
33
+ end
34
+ end
35
+
36
+ # @return [String]
37
+ def description
38
+ "GraphQL Batch: #{@queries.size} queries"
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "faraday"
4
+ require "faraday/follow_redirects"
5
+
6
+ module Regresso
7
+ module Adapters
8
+ # Adapter that fetches data from an HTTP endpoint.
9
+ class Http < Base
10
+ # @param base_url [String]
11
+ # @param endpoint [String]
12
+ # @param params [Hash]
13
+ # @param headers [Hash]
14
+ # @param method [Symbol]
15
+ def initialize(base_url:, endpoint:, params: {}, headers: {}, method: :get)
16
+ super()
17
+ @base_url = base_url
18
+ @endpoint = endpoint
19
+ @params = params
20
+ @headers = headers
21
+ @method = method
22
+ end
23
+
24
+ # Executes the HTTP request and returns parsed response.
25
+ #
26
+ # @return [Object]
27
+ def fetch
28
+ response = connection.public_send(@method, @endpoint) do |req|
29
+ req.params = @params
30
+ req.headers = @headers
31
+ end
32
+
33
+ if response.status >= 400
34
+ raise Regresso::Error, "HTTP request failed with status #{response.status}"
35
+ end
36
+
37
+ parse_response(response)
38
+ end
39
+
40
+ # @return [String]
41
+ def description
42
+ "#{@method.to_s.upcase} #{@base_url}#{@endpoint}"
43
+ end
44
+
45
+ private
46
+
47
+ def connection
48
+ @connection ||= Faraday.new(url: @base_url) do |faraday|
49
+ faraday.request :json
50
+ faraday.response :json
51
+ faraday.response :follow_redirects
52
+ faraday.adapter Faraday.default_adapter
53
+ end
54
+ end
55
+
56
+ def parse_response(response)
57
+ content_type = response.headers["content-type"].to_s
58
+ return response.body if content_type.empty?
59
+
60
+ if content_type.match?(/json/)
61
+ response.body
62
+ elsif content_type.match?(/csv/)
63
+ response.body
64
+ else
65
+ response.body
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Regresso
6
+ module Adapters
7
+ # Adapter that loads JSON content from a file.
8
+ class JsonFile < Base
9
+ # @param path [String]
10
+ def initialize(path:)
11
+ super()
12
+ @path = path
13
+ end
14
+
15
+ # Loads JSON from the file.
16
+ #
17
+ # @return [Object]
18
+ def fetch
19
+ raise Errno::ENOENT, @path unless File.exist?(@path)
20
+
21
+ JSON.parse(File.read(@path))
22
+ end
23
+
24
+ # @return [String]
25
+ def description
26
+ "JSON File: #{@path}"
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Regresso
4
+ module Adapters
5
+ # Adapter that calls a provided Proc or block for data.
6
+ class Proc < Base
7
+ # @param callable [Proc,nil]
8
+ # @param description [String]
9
+ def initialize(callable = nil, description: "Custom source", &block)
10
+ super()
11
+ @callable = callable || block
12
+ @description_text = description
13
+ end
14
+
15
+ # Executes the callable and returns its value.
16
+ #
17
+ # @return [Object]
18
+ def fetch
19
+ raise ArgumentError, "callable is required" unless @callable
20
+
21
+ @callable.call
22
+ end
23
+
24
+ # @return [String]
25
+ def description
26
+ @description_text
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Regresso
4
+ module CI
5
+ # Formats parallel results into GitHub Actions annotations.
6
+ class GitHubAnnotationFormatter
7
+ # @param result [Regresso::Parallel::ParallelResult]
8
+ def initialize(result)
9
+ @result = result
10
+ end
11
+
12
+ # @return [Array<Hash>] GitHub annotations payload
13
+ def annotations
14
+ @result.results.map do |res|
15
+ next unless res.failed? || res.error?
16
+
17
+ {
18
+ title: "Regresso: #{res.name}",
19
+ file: "regresso",
20
+ line: 1,
21
+ end_line: 1,
22
+ annotation_level: "error",
23
+ message: annotation_message(res)
24
+ }
25
+ end.compact
26
+ end
27
+
28
+ # @return [String] GitHub Actions workflow annotation output
29
+ def output_for_workflow
30
+ annotations.map do |annotation|
31
+ "::error file=#{annotation[:file]},line=#{annotation[:line]}::#{annotation[:message]}"
32
+ end.join("\n")
33
+ end
34
+
35
+ private
36
+
37
+ def annotation_message(res)
38
+ if res.failed?
39
+ "Regression detected"
40
+ else
41
+ "Error: #{res.error.class} - #{res.error.message}"
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "octokit"
4
+
5
+ module Regresso
6
+ module CI
7
+ # Posts a summary comment to a GitHub pull request.
8
+ class GitHubPRCommenter
9
+ # @param token [String]
10
+ # @param repo [String]
11
+ # @param pr_number [Integer]
12
+ def initialize(token:, repo:, pr_number:)
13
+ @client = Octokit::Client.new(access_token: token)
14
+ @repo = repo
15
+ @pr_number = pr_number
16
+ end
17
+
18
+ # Creates or updates a PR comment with the report summary.
19
+ #
20
+ # @param result [Regresso::Parallel::ParallelResult]
21
+ # @return [void]
22
+ def post_comment(result)
23
+ body = build_comment_body(result)
24
+ existing = find_existing_comment
25
+ if existing
26
+ @client.update_comment(@repo, existing.id, body)
27
+ else
28
+ @client.add_comment(@repo, @pr_number, body)
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def find_existing_comment
35
+ @client.issue_comments(@repo, @pr_number).find do |comment|
36
+ comment.body.include?("<!-- regresso-report -->")
37
+ end
38
+ end
39
+
40
+ def build_comment_body(result)
41
+ <<~MARKDOWN
42
+ <!-- regresso-report -->
43
+ ## Regresso Report
44
+
45
+ | Status | Count |
46
+ |--------|-------|
47
+ | Passed | #{result.passed} |
48
+ | Failed | #{result.failed} |
49
+ | Errors | #{result.errors} |
50
+ | Total | #{result.total} |
51
+ MARKDOWN
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "builder"
4
+
5
+ module Regresso
6
+ module CI
7
+ # Formats parallel results into JUnit XML.
8
+ class JUnitXmlFormatter
9
+ # @param result [Regresso::Parallel::ParallelResult]
10
+ def initialize(result)
11
+ @result = result
12
+ end
13
+
14
+ # @return [String] JUnit XML payload
15
+ def format
16
+ builder = Builder::XmlMarkup.new(indent: 2)
17
+ builder.instruct!
18
+
19
+ builder.testsuites(
20
+ name: "Regresso Regression Tests",
21
+ tests: @result.total,
22
+ failures: @result.failed,
23
+ errors: @result.errors
24
+ ) do |suites|
25
+ suites.testsuite(name: "Regression", tests: @result.total) do |suite|
26
+ @result.results.each do |res|
27
+ suite.testcase(name: res.name, time: res.duration) do |tc|
28
+ if res.failed?
29
+ tc.failure(message: "Regression detected", type: "RegressionFailure")
30
+ elsif res.error?
31
+ tc.error(message: res.error.message, type: res.error.class.name)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+ builder.target!
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Regresso
4
+ # CI-related reporting helpers.
5
+ module CI
6
+ # Builds CI-friendly reports from parallel results.
7
+ class Reporter
8
+ # @param result [Regresso::Parallel::ParallelResult]
9
+ def initialize(result)
10
+ @result = result
11
+ end
12
+
13
+ # @return [String] JUnit XML output
14
+ def to_junit_xml
15
+ JUnitXmlFormatter.new(@result).format
16
+ end
17
+
18
+ # @return [Array<Hash>] GitHub annotations payload
19
+ def to_github_annotations
20
+ GitHubAnnotationFormatter.new(@result).annotations
21
+ end
22
+
23
+ # @return [Hash] GitLab report payload
24
+ def to_gitlab_report
25
+ {
26
+ total: @result.total,
27
+ passed: @result.passed,
28
+ failed: @result.failed,
29
+ errors: @result.errors,
30
+ results: @result.results.map do |res|
31
+ {
32
+ name: res.name,
33
+ status: res.status,
34
+ duration: res.duration
35
+ }
36
+ end
37
+ }
38
+ end
39
+
40
+ # Writes JUnit XML output to a file.
41
+ #
42
+ # @param path [String]
43
+ # @return [void]
44
+ def write_junit_xml(path)
45
+ File.write(path, to_junit_xml)
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "ci/junit_xml_formatter"
4
+ require_relative "ci/github_annotation_formatter"
5
+ require_relative "ci/github_pr_commenter"
6
+ require_relative "ci/reporter"