From 0c7784b6c0e21cb22bd57b719123bf64f503d178 Mon Sep 17 00:00:00 2001 From: epszaw Date: Fri, 3 Apr 2026 18:19:37 +0200 Subject: [PATCH 1/3] Introduce runtime API methods to add global attachments and global errors --- .rubocop.yml | 2 +- .../lib/allure-ruby-commons.rb | 22 ++++++ .../allure_ruby_commons/allure_lifecycle.rb | 31 ++++++++- .../lib/allure_ruby_commons/file_writer.rb | 9 +++ .../model/global_attachment.rb | 19 +++++ .../allure_ruby_commons/model/global_error.rb | 23 +++++++ .../lib/allure_ruby_commons/model/globals.rb | 17 +++++ .../lib/allure_ruby_commons/result_utils.rb | 29 ++++++++ allure-ruby-commons/spec/spec_helper.rb | 1 + allure-ruby-commons/spec/unit/allure_spec.rb | 23 +++++++ .../spec/unit/attachment_spec.rb | 20 ++++++ .../spec/unit/file_writer_spec.rb | 69 +++++++++++++++++++ .../spec/unit/lifecycle_spec.rb | 47 ++++++++++++- 13 files changed, 309 insertions(+), 3 deletions(-) create mode 100644 allure-ruby-commons/lib/allure_ruby_commons/model/global_attachment.rb create mode 100644 allure-ruby-commons/lib/allure_ruby_commons/model/global_error.rb create mode 100644 allure-ruby-commons/lib/allure_ruby_commons/model/globals.rb diff --git a/.rubocop.yml b/.rubocop.yml index 766d3f83..308b92bd 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,4 +1,4 @@ -require: +plugins: - rubocop-performance AllCops: diff --git a/allure-ruby-commons/lib/allure-ruby-commons.rb b/allure-ruby-commons/lib/allure-ruby-commons.rb index a36a5ffe..1eb2cdfa 100644 --- a/allure-ruby-commons/lib/allure-ruby-commons.rb +++ b/allure-ruby-commons/lib/allure-ruby-commons.rb @@ -2,6 +2,7 @@ # frozen_string_literal: true require "require_all" +require "digest" require "securerandom" require_rel "allure_ruby_commons/**/*rb" @@ -161,6 +162,27 @@ def add_attachment(name:, source:, type:, test_case: false) lifecycle.add_attachment(name: name, source: source, type: type, test_case: test_case) end + # Add run-level attachment not bound to a test or fixture + # @param [String] name Attachment name + # @param [File, String] source File or string to save as attachment + # @param [String] type attachment type defined in {ContentType} or any other valid mime type + # @return [void] + def add_global_attachment(name:, source:, type:) + lifecycle.add_global_attachment(name: name, source: source, type: type) + end + + # Add run-level error not bound to a test or fixture + # @param [Hash] details + # @option details [Boolean] :known + # @option details [Boolean] :muted + # @option details [Boolean] :flaky + # @option details [String] :message + # @option details [String] :trace + # @return [void] + def add_global_error(**details) + lifecycle.add_global_error(**details) + end + # Manually create environment.properties file # if this method is called before test run started and # option clean_results_directory is enabled, the file will be deleted diff --git a/allure-ruby-commons/lib/allure_ruby_commons/allure_lifecycle.rb b/allure-ruby-commons/lib/allure_ruby_commons/allure_lifecycle.rb index 75060c26..df093a4f 100644 --- a/allure-ruby-commons/lib/allure_ruby_commons/allure_lifecycle.rb +++ b/allure-ruby-commons/lib/allure_ruby_commons/allure_lifecycle.rb @@ -20,7 +20,7 @@ def initialize(config = Config.instance) attr_reader :config - def_delegators :file_writer, :write_attachment + def_delegators :file_writer, :write_attachment, :write_globals # Start test result container # @param [Allure::TestResultContainer] test_result_container @@ -220,6 +220,35 @@ def add_attachment(name:, source:, type:, test_case: false) write_attachment(source, attachment) end + # Add run-level attachment + # @param [String] name Attachment name + # @param [File, String] source File or string to save as attachment + # @param [String] type attachment type defined in {Allure::ContentType} or any other valid mime type + # @return [void] + def add_global_attachment(name:, source:, type:) + attachment = ResultUtils.prepare_global_attachment(name, type, timestamp: ResultUtils.timestamp) + return logger.error { "Can't add global attachment, unrecognized mime type: #{type}" } unless attachment + + logger.debug { "Adding global attachment '#{name}'" } + write_attachment(source, attachment) + write_globals(Globals.new(attachments: [attachment], errors: [])) + end + + # Add run-level error + # @param [Hash] details + # @option details [Boolean] :known + # @option details [Boolean] :muted + # @option details [Boolean] :flaky + # @option details [String] :message + # @option details [String] :trace + # @return [void] + def add_global_error(**details) + error = ResultUtils.prepare_global_error(timestamp: ResultUtils.timestamp, **details) + + logger.debug { "Adding global error '#{error.message}'" } + write_globals(Globals.new(attachments: [], errors: [error])) + end + # Add environment.properties file # # @param [Hash, Proc] env diff --git a/allure-ruby-commons/lib/allure_ruby_commons/file_writer.rb b/allure-ruby-commons/lib/allure_ruby_commons/file_writer.rb index e04cb5db..6316db98 100644 --- a/allure-ruby-commons/lib/allure_ruby_commons/file_writer.rb +++ b/allure-ruby-commons/lib/allure_ruby_commons/file_writer.rb @@ -11,6 +11,8 @@ class FileWriter TEST_RESULT_CONTAINER_SUFFIX = "-container.json" # @return [String] attachment file suffix ATTACHMENT_FILE_SUFFIX = "-attachment" + # @return [String] globals chunk suffix + GLOBALS_SUFFIX = "-globals.json" # @return [String] environment info file ENVIRONMENT_FILE = "environment.properties" # @return [String] categories definition json @@ -45,6 +47,13 @@ def write_attachment(source, attachment) source.is_a?(File) ? copy(source.path, attachment.source) : write(attachment.source, source) end + # Write allure globals chunk + # @param [Allure::Globals] globals + # @return [void] + def write_globals(globals) + write("#{SecureRandom.uuid}#{GLOBALS_SUFFIX}", dump_json(globals)) + end + # Write allure report environment info # @param [Hash] environment # @return [void] diff --git a/allure-ruby-commons/lib/allure_ruby_commons/model/global_attachment.rb b/allure-ruby-commons/lib/allure_ruby_commons/model/global_attachment.rb new file mode 100644 index 00000000..b7712c36 --- /dev/null +++ b/allure-ruby-commons/lib/allure_ruby_commons/model/global_attachment.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Allure + # Allure model global attachment object + class GlobalAttachment < Attachment + # @param [Number] timestamp + # @param [Hash] options + # @option options [String] :name attachment name + # @option options [String] :type attachment type, {Allure::ContentType} + # @option options [String] :source attachment file name + def initialize(timestamp:, **options) + super(**options) + + @timestamp = timestamp + end + + attr_accessor :timestamp + end +end diff --git a/allure-ruby-commons/lib/allure_ruby_commons/model/global_error.rb b/allure-ruby-commons/lib/allure_ruby_commons/model/global_error.rb new file mode 100644 index 00000000..ae4377f0 --- /dev/null +++ b/allure-ruby-commons/lib/allure_ruby_commons/model/global_error.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require_relative "status_details" + +module Allure + # Allure model global error object + class GlobalError < StatusDetails + # @param [Number] timestamp + # @param [Hash] options + # @option options [Boolean] :known + # @option options [Boolean] :muted + # @option options [Boolean] :flaky + # @option options [String] :message + # @option options [String] :trace + def initialize(timestamp:, **options) + super(**options) + + @timestamp = timestamp + end + + attr_accessor :timestamp + end +end diff --git a/allure-ruby-commons/lib/allure_ruby_commons/model/globals.rb b/allure-ruby-commons/lib/allure_ruby_commons/model/globals.rb new file mode 100644 index 00000000..eb115699 --- /dev/null +++ b/allure-ruby-commons/lib/allure_ruby_commons/model/globals.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Allure + # Allure model run-level globals chunk + class Globals < JSONable + # @param [Array] attachments + # @param [Array] errors + def initialize(attachments: [], errors: []) + super() + + @attachments = attachments + @errors = errors + end + + attr_accessor :attachments, :errors + end +end diff --git a/allure-ruby-commons/lib/allure_ruby_commons/result_utils.rb b/allure-ruby-commons/lib/allure_ruby_commons/result_utils.rb index 07275d28..7f59b4b1 100644 --- a/allure-ruby-commons/lib/allure_ruby_commons/result_utils.rb +++ b/allure-ruby-commons/lib/allure_ruby_commons/result_utils.rb @@ -172,6 +172,35 @@ def prepare_attachment(name, type) Attachment.new(name: name, source: file_name, type: type) end + # Allure global attachment object + # @param [String] name + # @param [String] type + # @param [Number] timestamp + # @return [Allure::GlobalAttachment] + def prepare_global_attachment(name, type, timestamp: self.timestamp) + attachment = prepare_attachment(name, type) || return + + GlobalAttachment.new( + name: attachment.name, + source: attachment.source, + type: attachment.type, + timestamp: timestamp + ) + end + + # Allure global error object + # @param [Number] timestamp + # @param [Hash] options + # @option options [Boolean] :known + # @option options [Boolean] :muted + # @option options [Boolean] :flaky + # @option options [String] :message + # @option options [String] :trace + # @return [Allure::GlobalError] + def prepare_global_error(timestamp: self.timestamp, **options) + GlobalError.new(timestamp: timestamp, **options) + end + private # Check if value is full url diff --git a/allure-ruby-commons/spec/spec_helper.rb b/allure-ruby-commons/spec/spec_helper.rb index 4840ce8a..d687bce6 100644 --- a/allure-ruby-commons/spec/spec_helper.rb +++ b/allure-ruby-commons/spec/spec_helper.rb @@ -35,6 +35,7 @@ instance_double( "FileWriter", write_attachment: nil, + write_globals: nil, write_categories: nil, write_environment: nil, write_test_result: nil, diff --git a/allure-ruby-commons/spec/unit/allure_spec.rb b/allure-ruby-commons/spec/unit/allure_spec.rb index 1440696f..1ec6b586 100644 --- a/allure-ruby-commons/spec/unit/allure_spec.rb +++ b/allure-ruby-commons/spec/unit/allure_spec.rb @@ -119,6 +119,29 @@ def lifecycle expect(file_writer).to have_received(:write_attachment).with(args[:source], kind_of(Allure::Attachment)) end + it "adds global attachment" do + args = { name: "Global attach", source: "Some string", type: Allure::ContentType::TXT } + allure.add_global_attachment(**args) + + expect(file_writer).to have_received(:write_attachment).with(args[:source], kind_of(Allure::GlobalAttachment)) + expect(file_writer).to have_received(:write_globals).with(kind_of(Allure::Globals)) + end + + it "adds global error" do + allure.add_global_error(message: "Global failure", trace: "trace line") + + expect(file_writer).to have_received(:write_globals).with(kind_of(Allure::Globals)) do |globals| + error = globals.errors.first + + aggregate_failures do + expect(globals.attachments).to eq([]) + expect(error.message).to eq("Global failure") + expect(error.trace).to eq("trace line") + expect(error.timestamp).to be_a(Integer) + end + end + end + it "adds environment" do env = { PROP1: "test", PROP2: "test" } allure.add_environment(env) diff --git a/allure-ruby-commons/spec/unit/attachment_spec.rb b/allure-ruby-commons/spec/unit/attachment_spec.rb index 79d2c0cd..644ba00b 100644 --- a/allure-ruby-commons/spec/unit/attachment_spec.rb +++ b/allure-ruby-commons/spec/unit/attachment_spec.rb @@ -91,4 +91,24 @@ ) end end + + it "adds global attachment as a globals chunk" do + lifecycle.add_global_attachment(**attach_opts) + + aggregate_failures "Global attachment should be written" do + expect(@test_case.attachments).to be_empty + expect(file_writer).to have_received(:write_attachment).with( + "string attachment", + kind_of(Allure::GlobalAttachment) + ) + expect(file_writer).to have_received(:write_globals).with(kind_of(Allure::Globals)) do |globals| + attachment = globals.attachments.first + + expect(globals.errors).to eq([]) + expect(attachment.name).to eq("Test Attachment") + expect(attachment.type).to eq(Allure::ContentType::TXT) + expect(attachment.timestamp).to be_a(Integer) + end + end + end end diff --git a/allure-ruby-commons/spec/unit/file_writer_spec.rb b/allure-ruby-commons/spec/unit/file_writer_spec.rb index 1e5728d5..a24b2da2 100644 --- a/allure-ruby-commons/spec/unit/file_writer_spec.rb +++ b/allure-ruby-commons/spec/unit/file_writer_spec.rb @@ -50,6 +50,75 @@ expect(File.exist?(attachment_file)).to be_truthy, "Expected #{attachment_file} to exist" end + it "writes globals chunk" do + globals = Allure::Globals.new( + attachments: [ + Allure::GlobalAttachment.new( + name: "Global attachment", + type: Allure::ContentType::TXT, + source: "#{SecureRandom.uuid}-attachment.txt", + timestamp: 123 + ) + ], + errors: [] + ) + existing_files = Dir.glob(File.join(results_dir, "*-globals.json")) + file_writer.write_globals(globals) + + globals_file = (Dir.glob(File.join(results_dir, "*-globals.json")) - existing_files).first + + aggregate_failures "Expected globals chunk to exist with correct content" do + expect(globals_file).to be_a(String) + expect(File.exist?(globals_file)).to be_truthy, "Expected #{globals_file} to exist" + expect(file_writer.load_json(globals_file)).to eq( + attachments: [ + { + name: "Global attachment", + type: Allure::ContentType::TXT, + source: globals.attachments.first.source, + timestamp: 123 + } + ], + errors: [] + ) + end + end + + it "writes globals chunk with error" do + globals = Allure::Globals.new( + attachments: [], + errors: [ + Allure::GlobalError.new( + message: "Global failure", + trace: "trace line", + timestamp: 456 + ) + ] + ) + existing_files = Dir.glob(File.join(results_dir, "*-globals.json")) + file_writer.write_globals(globals) + + globals_file = (Dir.glob(File.join(results_dir, "*-globals.json")) - existing_files).first + + aggregate_failures "Expected globals error chunk to exist with correct content" do + expect(globals_file).to be_a(String) + expect(File.exist?(globals_file)).to be_truthy, "Expected #{globals_file} to exist" + expect(file_writer.load_json(globals_file)).to eq( + attachments: [], + errors: [ + { + known: false, + muted: false, + flaky: false, + message: "Global failure", + trace: "trace line", + timestamp: 456 + } + ] + ) + end + end + it "writes environment properties" do environment_file = File.join(results_dir, "environment.properties") file_writer.write_environment(PROP1: "test", PROP2: "test_2") diff --git a/allure-ruby-commons/spec/unit/lifecycle_spec.rb b/allure-ruby-commons/spec/unit/lifecycle_spec.rb index 8b9c5214..ba443b85 100644 --- a/allure-ruby-commons/spec/unit/lifecycle_spec.rb +++ b/allure-ruby-commons/spec/unit/lifecycle_spec.rb @@ -3,7 +3,15 @@ describe Allure::AllureLifecycle do subject(:lifecycle) { described_class.new(config) } - let(:file_writer) { instance_double("Allure::FileWriter", write_environment: nil, write_categories: nil) } + let(:file_writer) do + instance_double( + "Allure::FileWriter", + write_attachment: nil, + write_globals: nil, + write_environment: nil, + write_categories: nil + ) + end let(:results_dir) { "spec/allure-results" } let(:report_files) { ["result.json", "container.json"] } @@ -83,4 +91,41 @@ end end end + + describe "#add_global_attachment" do + it "writes a run-level attachment without an active test" do + lifecycle.add_global_attachment(name: "Global attachment", source: "payload", type: Allure::ContentType::TXT) + + expect(file_writer).to have_received(:write_attachment).with("payload", kind_of(Allure::GlobalAttachment)) + expect(file_writer).to have_received(:write_globals).with(kind_of(Allure::Globals)) do |globals| + attachment = globals.attachments.first + + aggregate_failures do + expect(globals.errors).to eq([]) + expect(globals.attachments.length).to eq(1) + expect(attachment.name).to eq("Global attachment") + expect(attachment.type).to eq(Allure::ContentType::TXT) + expect(attachment.timestamp).to be_a(Integer) + end + end + end + end + + describe "#add_global_error" do + it "writes a run-level error without an active test" do + lifecycle.add_global_error(message: "Global failure", trace: "trace line") + + expect(file_writer).to have_received(:write_globals).with(kind_of(Allure::Globals)) do |globals| + error = globals.errors.first + + aggregate_failures do + expect(globals.attachments).to eq([]) + expect(globals.errors.length).to eq(1) + expect(error.message).to eq("Global failure") + expect(error.trace).to eq("trace line") + expect(error.timestamp).to be_a(Integer) + end + end + end + end end From dbe9d9ee18074f9b1f495193f2f95f5c80bd30c7 Mon Sep 17 00:00:00 2001 From: epszaw Date: Tue, 7 Apr 2026 10:36:34 +0200 Subject: [PATCH 2/3] Update allure-ruby-commons/lib/allure_ruby_commons/allure_lifecycle.rb Co-authored-by: andrejs --- allure-ruby-commons/lib/allure_ruby_commons/allure_lifecycle.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/allure-ruby-commons/lib/allure_ruby_commons/allure_lifecycle.rb b/allure-ruby-commons/lib/allure_ruby_commons/allure_lifecycle.rb index df093a4f..cdae3bcd 100644 --- a/allure-ruby-commons/lib/allure_ruby_commons/allure_lifecycle.rb +++ b/allure-ruby-commons/lib/allure_ruby_commons/allure_lifecycle.rb @@ -246,7 +246,7 @@ def add_global_error(**details) error = ResultUtils.prepare_global_error(timestamp: ResultUtils.timestamp, **details) logger.debug { "Adding global error '#{error.message}'" } - write_globals(Globals.new(attachments: [], errors: [error])) + write_globals(Globals.new(errors: [error])) end # Add environment.properties file From 3f993fff63432715dd46d80f3af51af4f136448e Mon Sep 17 00:00:00 2001 From: epszaw Date: Tue, 7 Apr 2026 10:36:41 +0200 Subject: [PATCH 3/3] Update allure-ruby-commons/lib/allure_ruby_commons/allure_lifecycle.rb Co-authored-by: andrejs --- allure-ruby-commons/lib/allure_ruby_commons/allure_lifecycle.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/allure-ruby-commons/lib/allure_ruby_commons/allure_lifecycle.rb b/allure-ruby-commons/lib/allure_ruby_commons/allure_lifecycle.rb index cdae3bcd..69b2bcec 100644 --- a/allure-ruby-commons/lib/allure_ruby_commons/allure_lifecycle.rb +++ b/allure-ruby-commons/lib/allure_ruby_commons/allure_lifecycle.rb @@ -231,7 +231,7 @@ def add_global_attachment(name:, source:, type:) logger.debug { "Adding global attachment '#{name}'" } write_attachment(source, attachment) - write_globals(Globals.new(attachments: [attachment], errors: [])) + write_globals(Globals.new(attachments: [attachment])) end # Add run-level error