├── Gemfile ├── spec ├── spec_helper.rb └── extend_steps_spec.rb ├── .gitignore ├── lib ├── allure-ruby-adaptor-api │ ├── version.rb │ └── builder.rb └── allure-ruby-adaptor-api.rb ├── Gemfile.lock ├── LICENSE ├── allure-ruby-adaptor-api.gemspec ├── README.md └── allure-model-1.4.0.xsd /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rspec' 2 | require 'allure-ruby-adaptor-api' 3 | 4 | AllureRubyAdaptorApi.configure do |c| 5 | c.output_dir = "allure" 6 | end 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | 3 | # Package Files # 4 | *.jar 5 | *.war 6 | *.ear 7 | *.gem 8 | 9 | target 10 | allure 11 | .idea 12 | *.iml 13 | *.ipr 14 | *.iws 15 | -------------------------------------------------------------------------------- /lib/allure-ruby-adaptor-api/version.rb: -------------------------------------------------------------------------------- 1 | module AllureRubyAdaptorApi # :nodoc: 2 | module Version # :nodoc: 3 | STRING = '0.6.4' 4 | ALLURE = '1.4.0' 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /lib/allure-ruby-adaptor-api.rb: -------------------------------------------------------------------------------- 1 | require 'allure-ruby-adaptor-api/version' 2 | require 'allure-ruby-adaptor-api/builder' 3 | 4 | module AllureRubyAdaptorApi 5 | module Config 6 | class << self 7 | attr_accessor :output_dir 8 | 9 | DEFAULT_OUTPUT_DIR = 'gen/allure-results' 10 | 11 | def output_dir 12 | @output_dir || DEFAULT_OUTPUT_DIR 13 | end 14 | end 15 | end 16 | 17 | class << self 18 | def configure(&block) 19 | yield Config 20 | end 21 | end 22 | 23 | end 24 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | allure-ruby-adaptor-api (0.6.4) 5 | mimemagic 6 | nokogiri (~> 1.6.0) 7 | uuid 8 | 9 | GEM 10 | remote: https://rubygems.org/ 11 | specs: 12 | macaddr (1.7.1) 13 | systemu (~> 2.6.2) 14 | mimemagic (0.2.1) 15 | mini_portile (0.6.0) 16 | nokogiri (1.6.3.1) 17 | mini_portile (= 0.6.0) 18 | rake (10.3.2) 19 | systemu (2.6.4) 20 | uuid (2.3.7) 21 | macaddr (~> 1.0) 22 | 23 | PLATFORMS 24 | ruby 25 | 26 | DEPENDENCIES 27 | allure-ruby-adaptor-api! 28 | bundler 29 | rake 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2014 YANDEX 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /allure-ruby-adaptor-api.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $LOAD_PATH.unshift File.expand_path("../lib", __FILE__) 3 | require "allure-ruby-adaptor-api/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = 'allure-ruby-adaptor-api' 7 | s.version = AllureRubyAdaptorApi::Version::STRING 8 | s.platform = Gem::Platform::RUBY 9 | s.authors = ['Ilya Sadykov'] 10 | s.email = ['smecsia@yandex-team.ru'] 11 | s.description = %q{This is a helper library containing the basics for any ruby-based Allure adaptor.} 12 | s.summary = "allure-ruby-adaptor-api-#{AllureRubyAdaptorApi::Version::STRING}" 13 | s.homepage = 'http://allure.qatools.ru' 14 | s.license = 'Apache2' 15 | 16 | s.files = `git ls-files`.split("\n") 17 | s.test_files = `git ls-files -- {spec,features}/*`.split("\n") 18 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 19 | s.require_paths = ['lib'] 20 | 21 | s.add_dependency 'nokogiri', '~> 1.6.0' 22 | s.add_dependency 'uuid' 23 | s.add_dependency 'mimemagic' 24 | 25 | s.add_development_dependency 'bundler' 26 | s.add_development_dependency 'rake' 27 | end 28 | -------------------------------------------------------------------------------- /spec/extend_steps_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'tempfile' 3 | 4 | describe AllureRubyAdaptorApi do 5 | let(:builder) { AllureRubyAdaptorApi::Builder } 6 | 7 | it "should build xml report" do 8 | 9 | builder.start_suite "some_suite", :severity => :normal 10 | builder.start_test "some_suite", "some_test", :feature => "Some feature" 11 | builder.start_step "some_suite", "some_test", "first step" 12 | builder.stop_step "some_suite", "some_test", "first step" 13 | builder.start_step "some_suite", "some_test", "second step" 14 | builder.stop_step "some_suite", "some_test", "second step" 15 | builder.start_step "some_suite", "some_test", "third step" 16 | builder.stop_step "some_suite", "some_test", "third step", :failed 17 | builder.stop_test "some_suite", "some_test", :status => :broken, :exception => Exception.new("some error") 18 | builder.stop_suite "some_suite" 19 | 20 | builder.start_suite "some_empty_suite" 21 | builder.stop_suite "some_empty_suite" 22 | 23 | builder.build! {|suite, xml| 24 | xml.should_not be_empty 25 | xml.should include("some_suite") 27 | xml 28 | } 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Allure Ruby Adaptor API 2 | 3 | This is a helper library containing the basics for any ruby-based Allure adaptor. 4 | Using it you can easily implement the adaptor for your favourite ruby testing library or 5 | you can just create the report of any other kind using the basic Allure terms. 6 | 7 | ## Setup 8 | 9 | Add the dependency to your Gemfile 10 | 11 | ```ruby 12 | gem 'allure-ruby-adaptor-api' 13 | ``` 14 | 15 | ## Advanced options 16 | 17 | You can specify the directory where the Allure test results will appear. By default it would be 'gen/allure-results' within 18 | your current directory. 19 | 20 | ```ruby 21 | AllureRubyAdaptorApi.configure do |c| 22 | c.output_dir = "/whatever/you/like" 23 | end 24 | ``` 25 | 26 | ## Usage examples 27 | 28 | ```ruby 29 | builder = AllureRubyAdaptorApi::Builder 30 | builder.start_suite "some_suite", :severity => :normal 31 | builder.start_test "some_suite", "some_test", :feature => "Some feature", :severity => :critical 32 | builder.start_step "some_suite", "some_test", "first step" 33 | builder.add_attachment "some_suite", "some_test", :file => Tempfile.new("somefile") 34 | builder.stop_step "some_suite", "some_test", "first step" 35 | builder.start_step "some_suite", "some_test", "second step" 36 | builder.add_attachment "some_suite", "some_test", :step => "second step", :file => Tempfile.new("somefile") 37 | builder.stop_step "some_suite", "some_test", "second step" 38 | builder.start_step "some_suite", "some_test", "third step" 39 | builder.stop_step "some_suite", "some_test", "third step", :failed 40 | builder.stop_test "some_suite", "some_test", :status => :broken, :exception => Exception.new("some error") 41 | builder.stop_suite "some_suite" 42 | 43 | # This will generate the results within your output directory 44 | builder.build! 45 | ``` 46 | -------------------------------------------------------------------------------- /allure-model-1.4.0.xsd: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | -------------------------------------------------------------------------------- /lib/allure-ruby-adaptor-api/builder.rb: -------------------------------------------------------------------------------- 1 | require 'digest' 2 | require 'mimemagic' 3 | require 'nokogiri' 4 | require 'uuid' 5 | 6 | module AllureRubyAdaptorApi 7 | 8 | class Builder 9 | class << self 10 | attr_accessor :suites 11 | MUTEX = Mutex.new 12 | 13 | def start_suite(suite, labels = {:severity => :normal}) 14 | init_suites 15 | MUTEX.synchronize do 16 | puts "Starting case_or_suite #{suite} with labels #{labels}" 17 | self.suites[suite] = { 18 | :title => suite, 19 | :start => timestamp, 20 | :tests => {}, 21 | :labels => labels 22 | } 23 | end 24 | end 25 | 26 | def start_test(suite, test, labels = {:severity => :normal}) 27 | MUTEX.synchronize do 28 | puts "Starting test #{suite}.#{test} with labels #{labels}" 29 | self.suites[suite][:tests][test] = { 30 | :title => test, 31 | :start => timestamp, 32 | :failure => nil, 33 | :steps => {}, 34 | :attachments => [], 35 | :labels => labels, 36 | } 37 | end 38 | end 39 | 40 | def stop_test(suite, test, result = {}) 41 | self.suites[suite][:tests][test][:steps].each do |step_title, step| 42 | if step[:stop].nil? || step[:stop] == 0 43 | stop_step(suite, test, step_title, result[:status]) 44 | end 45 | end 46 | MUTEX.synchronize do 47 | puts "Stopping test #{suite}.#{test}" 48 | self.suites[suite][:tests][test][:stop] = timestamp(result[:finished_at]) 49 | self.suites[suite][:tests][test][:start] = timestamp(result[:started_at]) if result[:started_at] 50 | self.suites[suite][:tests][test][:status] = result[:status] 51 | if (result[:status].to_sym != :passed) 52 | self.suites[suite][:tests][test][:failure] = { 53 | :stacktrace => ((result[:exception] && result[:exception].backtrace) || []).map { |s| s.to_s }.join("\r\n"), 54 | :message => result[:exception].to_s, 55 | } 56 | end 57 | 58 | end 59 | end 60 | 61 | def start_step(suite, test, step) 62 | MUTEX.synchronize do 63 | puts "Starting step #{suite}.#{test}.#{step}" 64 | self.suites[suite][:tests][test][:steps][step] = { 65 | :title => step, 66 | :start => timestamp, 67 | :attachments => [] 68 | } 69 | end 70 | end 71 | 72 | def add_attachment(suite, test, opts = {:step => nil, :file => nil, :mime_type => nil}) 73 | raise "File cannot be nil!" if opts[:file].nil? 74 | step = opts[:step] 75 | file = opts[:file] 76 | title = opts[:title] || file.basename 77 | puts "Adding attachment #{opts[:title]} to #{suite}.#{test}#{step.nil? ? "" : ".#{step}"}" 78 | dir = Pathname.new(Dir.pwd).join(config.output_dir) 79 | FileUtils.mkdir_p(dir) 80 | file_extname = File.extname(file.path.downcase) 81 | mime_type = opts[:mime_type] || MimeMagic.by_path(file.path) || "text/plain" 82 | attachment = dir.join("#{Digest::SHA256.file(file.path).hexdigest}-attachment#{(file_extname.empty?) ? '' : file_extname}") 83 | puts "Copying attachment to '#{attachment}'..." 84 | FileUtils.cp(file.path, attachment) 85 | attach = { 86 | :type => mime_type, 87 | :title => title, 88 | :source => attachment.basename, 89 | :file => attachment.basename, 90 | :target => attachment.basename, 91 | :size => File.stat(attachment).size 92 | } 93 | if step.nil? 94 | self.suites[suite][:tests][test][:attachments] << attach 95 | else 96 | self.suites[suite][:tests][test][:steps][step][:attachments] << attach 97 | end 98 | end 99 | 100 | def stop_step(suite, test, step, status = :passed) 101 | MUTEX.synchronize do 102 | puts "Stopping step #{suite}.#{test}.#{step}" 103 | self.suites[suite][:tests][test][:steps][step][:stop] = timestamp 104 | self.suites[suite][:tests][test][:steps][step][:status] = status 105 | end 106 | end 107 | 108 | def stop_suite(title) 109 | init_suites 110 | MUTEX.synchronize do 111 | puts "Stopping case_or_suite #{title}" 112 | self.suites[title][:stop] = timestamp 113 | end 114 | end 115 | 116 | def build!(opts = {}, &block) 117 | suites_xml = [] 118 | self.suites.each do |suite_title, suite| 119 | builder = Nokogiri::XML::Builder.new do |xml| 120 | xml.send "ns2:test-suite", :start => suite[:start] || 0, :stop => suite[:stop] || 0, 'xmlns' => '', "xmlns:ns2" => "urn:model.allure.qatools.yandex.ru" do 121 | xml.send :name, suite_title 122 | xml.send :title, suite_title 123 | xml.send "test-cases" do 124 | suite[:tests].each do |test_title, test| 125 | xml.send "test-case", :start => test[:start] || 0, :stop => test[:stop] || 0, :status => test[:status] do 126 | xml.send :name, test_title 127 | xml.send :title, test_title 128 | unless test[:failure].nil? 129 | xml.failure do 130 | xml.message test[:failure][:message] 131 | xml.send "stack-trace", test[:failure][:stacktrace] 132 | end 133 | end 134 | xml.steps do 135 | test[:steps].each do |step_title, step_obj| 136 | xml.step(:start => step_obj[:start] || 0, :stop => step_obj[:stop] || 0, :status => step_obj[:status]) do 137 | xml.send :name, step_title 138 | xml.send :title, step_title 139 | xml_attachments(xml, step_obj[:attachments]) 140 | end 141 | end 142 | end 143 | xml_attachments(xml, test[:attachments]) 144 | xml_labels(xml, suite[:labels].merge(test[:labels])) 145 | xml.parameters 146 | end 147 | end 148 | end 149 | xml_labels(xml, suite[:labels]) 150 | end 151 | end 152 | unless suite[:tests].empty? 153 | xml = builder.to_xml 154 | xml = yield suite, xml if block_given? 155 | dir = Pathname.new(config.output_dir) 156 | FileUtils.mkdir_p(dir) 157 | out_file = dir.join("#{UUID.new.generate}-testsuite.xml") 158 | puts "Writing file '#{out_file}'..." 159 | File.open(out_file, 'w+') do |file| 160 | file.write(validate_xml(xml)) 161 | end 162 | suites_xml << xml 163 | end 164 | end 165 | suites_xml 166 | end 167 | 168 | private 169 | 170 | def config 171 | AllureRubyAdaptorApi::Config 172 | end 173 | 174 | def init_suites 175 | MUTEX.synchronize { 176 | self.suites ||= {} 177 | } 178 | end 179 | 180 | def timestamp(time = nil) 181 | ((time || Time.now).to_f * 1000).to_i 182 | end 183 | 184 | def validate_xml(xml) 185 | xsd = Nokogiri::XML::Schema(File.read(Pathname.new(File.dirname(__FILE__)).join("../../allure-model-#{AllureRubyAdaptorApi::Version::ALLURE}.xsd"))) 186 | doc = Nokogiri::XML(xml) 187 | 188 | xsd.validate(doc).each do |error| 189 | $stderr.puts error.message 190 | end 191 | xml 192 | end 193 | 194 | 195 | def xml_attachments(xml, attachments) 196 | xml.attachments do 197 | attachments.each do |attach| 198 | xml.attachment :source => attach[:source], :title => attach[:title], :size => attach[:size], :type => attach[:type] 199 | end 200 | end 201 | end 202 | 203 | def xml_labels(xml, labels) 204 | xml.labels do 205 | labels.each do |name, value| 206 | if value.is_a?(Array) 207 | value.each do |v| 208 | xml.label :name => name, :value => v 209 | end 210 | else 211 | xml.label :name => name, :value => value 212 | end 213 | end 214 | end 215 | end 216 | end 217 | end 218 | end --------------------------------------------------------------------------------