diff --git a/lib/junit.rb b/lib/junit.rb index 9cc5113..49757c6 100644 --- a/lib/junit.rb +++ b/lib/junit.rb @@ -1,246 +1,254 @@ # frozen_string_literal: true # # Copyright (C) 2017-2018 Harald Sitter # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) version 3, or any # later version accepted by the membership of KDE e.V. (or its # successor approved by the membership of KDE e.V.), which shall # act as a proxy defined in Section 6 of version 3 of the license. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library. If not, see . require 'fileutils' require 'json' require 'jenkins_junit_builder' require_relative 'result' # JUnit converter. class JUnit BUILD_URL = ENV.fetch('BUILD_URL', nil) REV = Dir.chdir(File.realpath("#{__dir__}/../")) do `git rev-parse HEAD`.strip end class Case < JenkinsJunitBuilder::Case RESULT_MAP = { ok: JenkinsJunitBuilder::Case::RESULT_PASSED, fail: JenkinsJunitBuilder::Case::RESULT_FAILURE, unknown: JenkinsJunitBuilder::Case::RESULT_PASSED, + softfail: JenkinsJunitBuilder::Case::RESULT_PASSED, # It's actually unclear why canceled appears. Code suggests it's set when # the test runner gets TERM. Question is why it would I guess. In any # event we'll consider this a failure. canceled: JenkinsJunitBuilder::Case::RESULT_FAILURE # => JenkinsJunitBuilder::Case::RESULT_SKIPPED }.freeze REPO = 'apachelogger/kde-os-autoinst'.freeze EXPECTATION_URL = format('https://raw.githubusercontent.com/%s/%s', REPO, REV && !REV.empty? ? REV : 'master').freeze attr_reader :detail def initialize(detail) super() @detail = detail self.result = translate_result(detail.result) if result == JenkinsJunitBuilder::Case::RESULT_PASSED && detail.dent # iff this detail has a dent mark it skipped in junit; best # representation we have. self.result = JenkinsJunitBuilder::Case::RESULT_SKIPPED end system_err.message = JSON.pretty_generate(detail.data) end def translate_result(r) RESULT_MAP.fetch(detail.result) end def artifact_url(artifact) "#{BUILD_URL}/artifact/testresults/#{artifact}" end end class TextDetailCase < Case def initialize(detail) super self.name = detail.title system_out << artifact_url(detail.text) end end class SoftFailureDetailCase < Case def initialize(detail) # Soft failures are difficult in that their result field is in fact # a screenshot blob. We always mark them skipped and do our best # to give useful data. super self.name = detail.title + screenshot = '' + # In newer os-autoinst softfails may also simply have result:softfail + # instead of nesting another detail with screenshot. In that case + # we have nothing to add and can simply leave the empty string + unless detail.result.is_a?(Symbol) + screenshot = artifact_url(detail.result.screenshot) + end system_out << <<-STDOUT We recorded a soft failure, this isn't a failed assertion but rather indicates that something is (temporarily) wrong with the expecations. This event was programtically created, check the code of the test case. #{artifact_url(detail.text)} -#{artifact_url(detail.result.screenshot)} +#{screenshot} STDOUT end # always mark skipped def translate_result(_r) JenkinsJunitBuilder::Case::RESULT_SKIPPED end end class ScreenshotDetailCase < Case def initialize(detail) super self.name = 'screenshot_without_match' system_out << artifact_url(detail.screenshot) end end class NeedleDetailCase < Case def initialize(detail) super self.name = 'unmatched_needle' if detail.result == :unknown system_out << "This was a check but not an assertion!!!\n" end case detail.result when :ok then system_out << ok_needles_info when :fail, :unknown then system_out << error_needles_info end end def ok_needles_info <<-STDOUT #{artifact_url(detail.screenshot)} matched: #{EXPECTATION_URL}/#{detail.json.sub('.json', '.png')} STDOUT end def error_needles_info return no_needles_info if detail.needles.empty? expected_urls = detail.needles.collect do |needle| "#{EXPECTATION_URL}/#{needle.json.sub('.json', '.png')}" end <<-STDOUT To satisfy a test for the tags '#{detail.tags}' we checked the screen and found #{artifact_url(detail.screenshot)} but expected any of: #{expected_urls.join("\n")} STDOUT end def no_needles_info <<-STDOUT We wanted to test for tags '#{detail.tags}' but found no needles to back these tags. Chances are there is no needle, or the tags are misspelled. (Other options apply but are less likely obviously.) #{artifact_url(detail.screenshot)} STDOUT end end class NeedleMatchDetailCase < NeedleDetailCase def initialize(detail) super self.name = detail.needle end end # Suite wrapper class Suite < JenkinsJunitBuilder::Suite BAD_RESULTS = [JenkinsJunitBuilder::Case::RESULT_FAILURE, JenkinsJunitBuilder::Case::RESULT_ERROR].freeze def initialize(test_file, name:) super() @failed = false result = OSAutoInst::ResultSuite.new(test_file) self.name = name self.package = name self.report_path = "junit/#{name}.xml" casify(result) end def failed? @failed end def add_case(c) @failed ||= BAD_RESULTS.include?(c.result) super end def casify(result) result.details.each do |detail| case_klass = detail.class.to_s.split('::')[-1] + 'Case' c = JUnit.const_get(case_klass).new(detail) c.name = format('%03d_%s', @cases.size, c.name) add_case(c) end add_case(meta_case(result)) end def meta_case(result) c = JenkinsJunitBuilder::Case.new c.name = 'all' c.result = Case::RESULT_MAP.fetch(result.result) c end end attr_reader :testresults_dir def initialize(testresults_dir) @testresults_dir = testresults_dir @failed = false FileUtils.rm_rf('junit') if Dir.exist?('junit') Dir.mkdir('junit') end def failed? @failed end def write_all order = OSAutoInst::TestOrder.new(testresults_dir: testresults_dir) if order.tests.empty? raise "No tests run; order array is empty in #{order.file}" end order.tests.each_with_index do |test, i| name = test.fetch(:name) test_file = test.fetch(:file) assert_test_file(name, test_file) suite = Suite.new(test_file, name: format('%03d_%s', i, name)) @failed ||= suite.failed? suite.write_report_file end end def assert_test_file(name, file) return if File.exist?(file) raise "Test '#{name}' has a missing json file; it probably failed entirely" end def self.from_openqa(testresults_dir) unit = new(testresults_dir) unit.write_all raise 'It seems some tests have not quite passed' if unit.failed? end end diff --git a/lib/result.rb b/lib/result.rb index cf588f6..e173b58 100644 --- a/lib/result.rb +++ b/lib/result.rb @@ -1,486 +1,489 @@ # frozen_string_literal: true # # Copyright (C) 2017-2018 Harald Sitter # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) version 3, or any # later version accepted by the membership of KDE e.V. (or its # successor approved by the membership of KDE e.V.), which shall # act as a proxy defined in Section 6 of version 3 of the license. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library. If not, see . require 'fileutils' require 'json' require 'jenkins_junit_builder' module OSAutoInst class GenericDetailError < StandardError; end module DetailAttributes def detail_attributes @detail_attributes ||= [] end def detail_attributes_optional @detail_attributes_optional ||= [] end def attributes @attrs ||= begin attrs = detail_attributes.dup attrs += ancestors.collect do |klass| next if klass == self || !klass.respond_to?(:attributes) klass.attributes end.flatten.uniq.compact attrs.flatten.uniq.compact end end def optional_attributes @optional_attrs ||= begin attrs = detail_attributes_optional.dup attrs += ancestors.collect do |klass| next if klass == self || !klass.respond_to?(:detail_attributes_optional) klass.detail_attributes_optional end.flatten.uniq.compact attrs.flatten.uniq.compact end end def all_attributes (attributes + optional_attributes).uniq.compact end def detail_attr(sym) detail_attributes << sym attr_reader_real sym end # Optionals are different. They may appear or not. Main example is a 'dent'. # Pretty much all details may be marked as having a dent, i.e. they passed # but not very nicely. def optional_detail_attr(sym) detail_attributes_optional << sym attr_reader_real sym end def self.extended(other) class << other alias_method :attr_reader_real, :attr_reader def attr_reader(*) raise 'When you want a public detail attribute use `detail attr`. When you want an actual reader use attr_reader_real' end end end end class DetailRepresentation extend DetailAttributes attr_reader_real :data class << self def can_represent_exactly?(data_blob) data_blob.keys.sort == attributes.sort || data_blob.keys.sort == all_attributes.sort end def can_represent_approximately?(data_blob) (data_blob.keys.sort - attributes.sort).empty? || (data_blob.keys.sort - all_attributes.sort).empty? end def need?(_data) false end def want?(_data) true end end def initialize(data) @data = data data.each do |key, value| var = "@#{key}" instance_variable_set(var, value) end end end class Detail < DetailRepresentation # :ok or :fail or :unknown or :skip detail_attr :result # Dents are optional markers to mark a result as not quite as good # as should be. (e.g. it matched but only using a workaround needle). # If I am not mistaken the primary cause for a dent is a workaround property optional_detail_attr :dent def initialize(*) # os-autoinst may create result blobs which contain no worthwhile # information. These blobs will match the Detail class directly. Detail # however is not meant to be used directly as it cannot be represented # in junit with any worthwhile information. Simply put a generic detail # means nothing so it shouldn't be used. This error needs to be handled # when factorizing details. raise GenericDetailError if self.class == Detail @dent ||= false super init_result end def init_result - results = { 'unk' => :unknown, 'ok' => :ok, 'fail' => :fail } + results = { 'unk' => :unknown, 'ok' => :ok, 'fail' => :fail, + 'softfail' => :softfail } @result = results.fetch(result) do result.is_a?(Hash) begin @result = DetailFactory.new(result).factorize rescue raise "Couldn't map result #{result}" end end end # Returns the ultimate result. Result may be another detail (e.g. # a screenshot). This method loops into results until it reaches a type # that no longer has a result method (i.e. hopefully one of the well-known # symbols). def deep_result ret = result loop do break unless ret.respond_to?(:result) ret = ret.result end ret end def coerce_result(r) # FIXME: probably should make sure only unknown gets coerced @result = r end # Whether or not this detail is equal to another detail in terms of its # functional properties. e.g. details with a tag can be the same if the # tags are the same. # This does not assert equallity (different results can still be the same # detail). # Sameness is used to determine chains of details with unknown results # and finalize their result to whatever was the final result. Namely # Needle matches can have multiple "unknown" result details which # essentially means a screenshot was taken and compared, but didn't match # and there is still time for another screenshot to match. def same?(_other) false end end # A screenshot which was taken but didn't match a needle. class ScreenshotDetail < Detail # File name of associated screenshot. detail_attr :screenshot # The Range during which the frame was taken. As Frames are only fetched # at a certain interval, each frame appears within a range of time rather # than fixed points. detail_attr :frametime end # A text assertion class TextDetail < Detail # a custom title for the test detail_attr :title # received textual output detail_attr :text end # Soft failures are text details with rubbish result... class SoftFailureDetail < TextDetail # Result gets automatically represented correctly as it is a Hash. # This class is only here so we can easily render this type of failure # differently when converting to junit. def self.need?(data) - data[:result] && data[:result].is_a?(Hash) + (data[:result] && data[:result].is_a?(Hash)) || + data[:result] == 'softfail' end def self.want?(data) - data[:result] && data[:result].is_a?(Hash) + (data[:result] && data[:result].is_a?(Hash)) || + data[:result] == 'softfail' end end class NeedleDetail < Detail detail_attr :frametime # Array of ErrorNeedles detail_attr :needles detail_attr :screenshot # screenshot of the assertion detail_attr :tags # tags searched for detail_attr :error # for the live of me I don't know what this shit is def initialize(*) # Init to avoid problems with representing approximate blobs. @needles = [] @tags = [] super return unless @needles @needles = @needles.collect { |x| DetailFactory.new(x).factorize } end # Needle types can appear in chains that have sameness. Make sure we are # same enough to previous potentially unknown needles. def same?(other) return false unless other.is_a?(self.class) # If the tags are the same and the needles are the same. Or so I think. tags.sort == other.tags.sort end end module Needle extend DetailAttributes detail_attr :area # Array of matching areas detail_attr :json # json file of the needle detail_attr :needle # string, name of needle def needle @needle || @name end # In a needles:{} the detail is refering to the name as name, in a match # it refers to it as needle. Inconsistent shitfest. Compatibility the # two incarnations. # A Detail that includes Needle never exactly matches because of this. Not # a problem right now, but if it becomes one in the future this needs # splitting into NeedleMatch and NeedleInfo respectively using the correct # attr name. detail_attr :name # string, name of NeedleError def name @name || @needle end end # this is not actually a high level detail, it appears within needle details. class NeedleError < DetailRepresentation prepend Needle detail_attr :error end class NeedleMatchDetail < NeedleDetail prepend Needle detail_attr :properties # of the matched needle end # # This is not a detail! FML. # class SoftFailure < Detail # def self.can_represent?(data_blob) # super && data_blob.fetch(:title) == 'Soft Failure' # end # # # NB: Result is ScreenshotDetail object!!!@#!!!!!! # # Title (always says it is a soft failure, details are in text) # detail_attr :title # # the software failure description # detail_attr :text # end class DetailFactory attr_reader :data def initialize(data) @data = data @representations = [] @approximations = [] end def find_klasses @representations = OSAutoInst.constants.collect do |const| klass = OSAutoInst.const_get(const) next unless klass.is_a?(Class) && klass.ancestors.include?(DetailRepresentation) unless klass.can_represent_exactly?(data) @approximations << klass if klass.can_represent_approximately?(data) next end klass end.compact end # If multiple classes can represent the same data set we essentially # X-OR them. We ask all of them if they want the data. If >1 wants it # we ask if they need the data. # This allows a class to override all other classes by needing data it # absolutely knows how to handle while also being able to not want data # which it knows other classes can handle better. # Notably both TextDetail and SoftFailureDetail can handle textish blobs # BUT only SoftFailureDetail knows how to differentiate a soft failure blob # from a regular text blob. As such both can technically represent a # textis blob but SoftFailureDetail only wants soft failure blobs. def who_needs_the_data @representations = @representations.select { |x| x.need?(data) } case @representations.size when 0 then return when 1 then return @representations[0] else raise "to many klasses need the data #{@representations} #{data}" end end # Check who wants the data. # If there are multiple, check who needs the data. # If no one needs the data use the least shitty approximation to who wants # the data. # # If no one needs the data we'll assume they can all handle the data # (as they wanted it) but are indifferent as to which gets it, so we'll # simply use the tighest approximation. i.e. the one with less attributes. # (an approximate match has a superset of attributes in the blob, so # the smallest super set becomes the best match). # e.g. {tags:,foo:} approximates to classes {tags:,foo:,bar:} and # {tags:,foo:,bar:,foobar:}. the former class is the match with less # presumed functional overhead though. def who_wants_the_data @representations = @representations.select { |x| x.want?(data) } weighted_by_attributes = @representations.sort do |x, y| x.attributes.size <=> y.attributes.size end case @representations.size when 0 then raise "no classes wanted our data #{data}" when 1 then return @representations[0] end need = who_needs_the_data return need if need approximation = weighted_by_attributes.fetch(0) warn "No class needed so we'll approximat with #{approximation} - #{data}" approximation end class NoPerfectMatchError < RuntimeError; end def best_klass case @representations.size when 0 raise NoPerfectMatchError unless @representations == @approximations raise "no representation for #{data}" when 1 then @representations[0] else who_wants_the_data end rescue NoPerfectMatchError @representations = @approximations retry end def factorize find_klasses best_klass.new(data) rescue GenericDetailError warn "Encountered generic detail, skipping: #{data}}" nil rescue NoMethodError => e warn "Failed to find class for #{data}" raise e end end class ResultSuite # Result :ok or :fail or :canceled attr_reader :result # TODO: unknown attr_reader :dents # The actual assertions attr_reader :details # Edit all details to sort out chain failures. # openqa records results not necessarily assertions. To meet a screen # assertion for 'grub' it may take multiple screenshots which may get # recorded as unknown. The last of them may be fail if no match was found # and the time ran out. In these cases we do however want to make the # entire chain of unknown preceding details fail as well. Otherwise its # hard to find out where things started to fail. # This requires that details that can appear in a fail chain implement # the {same?} method to check if they qualify as the same check as # another detail. This way we can build chains of sameish checks and let # them all fail or succeed as needed. def chain_fail running_array = [] @details.each do |detail| # If the detail is not compatible with the unknown details in the chain # the chain was broken for unknown reasons and we cannot finalize the # results of the unknown details. running_array = [] unless detail.same?(running_array[0]) # Get the deep result. We'll make assertions on its typyness. result = detail.deep_result if %i[ok fail].include?(result) # Finalize. running_array.each { |x| x.coerce_result(detail.result) } running_array = [] next end # Otherwise the detail is one more in the chain of unknown. raise unless result == :unknown # assert running_array << detail end end def initialize(path) data = JSON.parse(File.read(path), symbolize_names: true) @dents = data.delete(:dents) @result = data.delete(:result) @result = case @result when 'ok' then :ok when 'fail' then :fail when 'canceled' then :canceled else raise "Unknown result #{@result}" end @details = data.delete(:details) # also compact drop possible nil results (e.g. generic details) @details = @details.collect { |x| DetailFactory.new(x).factorize }.compact chain_fail raise unless data.keys.empty? end end class TestOrder attr_reader :file attr_reader :testresults_dir attr_reader :tests def initialize(testresults_dir:) @testresults_dir = testresults_dir @file = "#{testresults_dir}/test_order.json" raise "No tests run; can't find #{@file}" unless File.exist?(@file) @tests = JSON.parse(File.read(@file), symbolize_names: true) extend_data! end def test_files tests.collect { |t| t.fetch(:file) } end def result_suites # missing files are ignored. Could also make this opt-in fatal. No use # for this as junit (which wants to fail) does so in its own code test_files.collect do |test_file| next nil unless File.exist?(test_file) ResultSuite.new(test_file) end.compact end private def extend_data! tests.each do |test| test[:file] = "#{testresults_dir}/result-#{test.fetch(:name)}.json" end end end end diff --git a/test/data/result-install_calamares.json b/test/data/result-install_calamares.json index 5ad451f..40f2018 100644 --- a/test/data/result-install_calamares.json +++ b/test/data/result-install_calamares.json @@ -1,684 +1,689 @@ { "details" : [ { "frametime" : [ "0.62", "0.67" ], "needle" : "bootloader", "screenshot" : "install_calamares-1.png", "properties" : [], "area" : [ { "similarity" : 100, "x" : 0, "result" : "ok", "w" : "548", "h" : "264", "y" : 0 } ], "json" : "neon/needles/bootloader.json", "result" : "ok", "tags" : [ "bootloader" ] }, { "tags" : [ "live-desktop" ], "json" : "neon/needles/live-desktop.json", "result" : "ok", "area" : [ { "similarity" : 97, "x" : 0, "result" : "ok", "h" : "768", "y" : 0, "w" : "888" } ], "properties" : [], "screenshot" : "install_calamares-2.png", "frametime" : [ "4.88", "4.92" ], "needle" : "live-desktop" }, { "tags" : [ "calamares-installer-icon" ], "json" : "neon/needles/install_calamares/calamares-installer-icon.json", "result" : "ok", "area" : [ { "y" : 0, "h" : "128", "w" : "128", "similarity" : 100, "x" : 0, "result" : "ok" } ], "properties" : [], "screenshot" : "install_calamares-3.png", "frametime" : [ "5.17", "5.21" ], "needle" : "calamares-installer-icon" }, { "screenshot" : "install_calamares-4.png", "frametime" : [ "5.50", "5.54" ], "needle" : "calamares-installer-welcome", "area" : [ { "y" : 137, "h" : "518", "w" : "841", "similarity" : 100, "x" : 98, "result" : "ok" } ], "properties" : [], "tags" : [ "calamares-installer-welcome" ], "json" : "neon/needles/install_calamares/calamares-installer-welcome.json", "result" : "ok" }, { "needles" : [ { "area" : [ { "y" : 621, "h" : "28", "w" : "84", "x" : 747, "result" : "fail", "similarity" : 49 } ], "name" : "calamares-installer-next-highlight", "error" : 0.258666748965401, "json" : "neon/needles/install_calamares/calamares-installer-next-highlight.json" } ], "screenshot" : "install_calamares-5.png", "needle" : "calamares-installer-next-nohighlight", "frametime" : [ "5.58", "5.62" ], "area" : [ { "w" : "84", "y" : 621, "h" : "28", "similarity" : 100, "x" : 747, "result" : "ok" } ], "properties" : [], "tags" : [ "calamares-installer-next" ], "result" : "ok", "json" : "neon/needles/install_calamares/calamares-installer-next-nohighlight.json" }, { "tags" : [ "calamares-installer-timezone" ], "result" : "ok", "json" : "neon/needles/install_calamares/calamares-installer-timezone.json", "area" : [ { "h" : "476", "y" : 141, "w" : "640", "result" : "ok", "x" : 295, "similarity" : 100 } ], "properties" : [], "screenshot" : "install_calamares-6.png", "frametime" : [ "5.71", "5.75" ], "needle" : "calamares-installer-timezone" }, { "needles" : [ { "json" : "neon/needles/install_calamares/calamares-installer-next-highlight.json", "error" : 0.258666748965401, "name" : "calamares-installer-next-highlight", "area" : [ { "x" : 747, "result" : "fail", "similarity" : 49, "y" : 621, "h" : "28", "w" : "84" } ] } ], "needle" : "calamares-installer-next-nohighlight", "frametime" : [ "5.79", "5.83" ], "screenshot" : "install_calamares-7.png", "properties" : [], "area" : [ { "h" : "28", "y" : 621, "w" : "84", "result" : "ok", "x" : 747, "similarity" : 100 } ], "result" : "ok", "json" : "neon/needles/install_calamares/calamares-installer-next-nohighlight.json", "tags" : [ "calamares-installer-next" ] }, { "screenshot" : "install_calamares-8.png", "frametime" : [ "5.92", "5.96" ], "needle" : "calamares-installer-keyboard", "tags" : [ "calamares-installer-keyboard" ], "json" : "neon/needles/install_calamares/calamares-installer-keyboard.json", "result" : "ok", "area" : [ { "x" : 293, "result" : "ok", "similarity" : 100, "y" : 149, "h" : "471", "w" : "641" } ], "properties" : [] }, { "tags" : [ "calamares-installer-next" ], "json" : "neon/needles/install_calamares/calamares-installer-next-nohighlight.json", "result" : "ok", "area" : [ { "result" : "ok", "x" : 747, "similarity" : 100, "w" : "84", "h" : "28", "y" : 621 } ], "properties" : [], "screenshot" : "install_calamares-9.png", "needle" : "calamares-installer-next-nohighlight", "frametime" : [ "6.00", "6.04" ], "needles" : [ { "error" : 0.258666748965401, "json" : "neon/needles/install_calamares/calamares-installer-next-highlight.json", "name" : "calamares-installer-next-highlight", "area" : [ { "similarity" : 49, "x" : 747, "result" : "fail", "y" : 621, "h" : "28", "w" : "84" } ] } ] }, { "frametime" : [ "6.08", "6.12" ], "needle" : "calamares-installer-disk", "screenshot" : "install_calamares-10.png", "properties" : [], "area" : [ { "w" : "640", "y" : 140, "h" : "470", "similarity" : 100, "result" : "ok", "x" : 290 } ], "result" : "ok", "json" : "neon/needles/install_calamares/calamares-installer-disk.json", "tags" : [ "calamares-installer-disk" ] }, { "area" : [ { "w" : "533", "h" : "44", "y" : 186, "similarity" : 100, "result" : "ok", "x" : 300 } ], "properties" : [], "tags" : [ "calamares-installer-disk-erase" ], "result" : "ok", "json" : "neon/needles/install_calamares/calamares-installer-disk-erase.json", "screenshot" : "install_calamares-11.png", "needle" : "calamares-installer-disk-erase", "frametime" : [ "6.17", "6.21" ] }, { "needle" : "calamares-installer-disk-erase-selected", "frametime" : [ "6.29", "6.33" ], "screenshot" : "install_calamares-12.png", "properties" : [], "area" : [ { "w" : "640", "h" : "470", "y" : 140, "x" : 290, "result" : "ok", "similarity" : 97 } ], "json" : "neon/needles/install_calamares/calamares-installer-disk-erase-selected.json", "result" : "ok", "tags" : [ "calamares-installer-disk-erase-selected" ] }, { "needles" : [ { "name" : "calamares-installer-next-highlight", "error" : 0.258666748965401, "json" : "neon/needles/install_calamares/calamares-installer-next-highlight.json", "area" : [ { "y" : 621, "h" : "28", "w" : "84", "result" : "fail", "x" : 747, "similarity" : 49 } ] } ], "screenshot" : "install_calamares-13.png", "frametime" : [ "6.38", "6.42" ], "needle" : "calamares-installer-next-nohighlight", "area" : [ { "result" : "ok", "x" : 747, "similarity" : 100, "w" : "84", "y" : 621, "h" : "28" } ], "properties" : [], "tags" : [ "calamares-installer-next" ], "result" : "ok", "json" : "neon/needles/install_calamares/calamares-installer-next-nohighlight.json" }, { "screenshot" : "install_calamares-14.png", "frametime" : [ "6.46", "6.50" ], "needle" : "calamares-installer-user", "tags" : [ "calamares-installer-user" ], "result" : "ok", "json" : "neon/needles/install_calamares/calamares-installer-user.json", "area" : [ { "h" : "470", "y" : 140, "w" : "640", "x" : 290, "result" : "ok", "similarity" : 100 } ], "properties" : [] }, { "screenshot" : "install_calamares-15.png", "frametime" : [ "6.54", "6.58" ], "needle" : "calamares-installer-user-user", "area" : [ { "similarity" : 100, "x" : 292, "result" : "ok", "h" : "261", "y" : 142, "w" : "640" } ], "properties" : [], "tags" : [ "calamares-installer-user-user" ], "json" : "neon/needles/install_calamares/calamares-installer-user-user.json", "result" : "ok" }, { "result" : "ok", "json" : "neon/needles/install_calamares/calamares-installer-user-complete.json", "tags" : [ "calamares-installer-user-complete" ], "properties" : [], "area" : [ { "w" : "640", "h" : "261", "y" : 142, "result" : "ok", "x" : 292, "similarity" : 100 } ], "needle" : "calamares-installer-user-complete", "frametime" : [ "7.75", "7.79" ], "screenshot" : "install_calamares-16.png" }, { "tags" : [ "calamares-installer-next" ], "json" : "neon/needles/install_calamares/calamares-installer-next-nohighlight.json", "result" : "ok", "area" : [ { "h" : "28", "y" : 621, "w" : "84", "similarity" : 100, "x" : 747, "result" : "ok" } ], "properties" : [], "screenshot" : "install_calamares-17.png", "needle" : "calamares-installer-next-nohighlight", "frametime" : [ "7.83", "7.88" ], "needles" : [ { "name" : "calamares-installer-next-highlight", "error" : 0.258666748965401, "json" : "neon/needles/install_calamares/calamares-installer-next-highlight.json", "area" : [ { "w" : "84", "h" : "28", "y" : 621, "x" : 747, "result" : "fail", "similarity" : 49 } ] } ] }, { "frametime" : [ "7.96", "8.00" ], "needle" : "calamares-installer-summary", "screenshot" : "install_calamares-18.png", "json" : "neon/needles/install_calamares/calamares-installer-summary.json", "result" : "ok", "tags" : [ "calamares-installer-summary" ], "properties" : [], "area" : [ { "similarity" : 100, "result" : "ok", "x" : 290, "h" : "470", "y" : 140, "w" : "640" } ] }, { "tags" : [ "calamares-installer-next" ], "json" : "neon/needles/install_calamares/calamares-installer-next-highlight.json", "result" : "ok", "area" : [ { "h" : "28", "y" : 621, "w" : "84", "result" : "ok", "x" : 747, "similarity" : 100 } ], "properties" : [], "screenshot" : "install_calamares-19.png", "needle" : "calamares-installer-next-highlight", "frametime" : [ "8.04", "8.08" ], "needles" : [ { "area" : [ { "h" : "28", "y" : 621, "w" : "84", "similarity" : 49, "x" : 747, "result" : "fail" } ], "json" : "neon/needles/install_calamares/calamares-installer-next-nohighlight.json", "error" : 0.258666748965401, "name" : "calamares-installer-next-nohighlight" } ] }, { "area" : [ { "y" : 140, "h" : "470", "w" : "640", "similarity" : 100, "x" : 290, "result" : "ok" } ], "properties" : [], "tags" : [ "calamares-installer-show" ], "result" : "ok", "json" : "neon/needles/install_calamares/calamares-installer-show.json", "screenshot" : "install_calamares-20.png", "needle" : "calamares-installer-show", "frametime" : [ "8.29", "8.33" ] }, { "screenshot" : "install_calamares-21.png", "needle" : "calamares-installer-restart", "frametime" : [ "16.21", "16.25" ], "tags" : [ "calamares-installer-restart" ], "json" : "neon/needles/install_calamares/calamares-installer-restart.json", "result" : "ok", "area" : [ { "result" : "ok", "x" : 290, "similarity" : 100, "h" : "470", "y" : 140, "w" : "640" } ], "properties" : [] }, { "result" : "ok", "json" : "neon/needles/install_calamares/calamares-installer-restart-now.json", "tags" : [ "calamares-installer-restart-now" ], "properties" : [], "area" : [ { "similarity" : 100, "x" : 844, "result" : "ok", "y" : 622, "h" : "27", "w" : "86" } ], "frametime" : [ "16.29", "16.33" ], "needle" : "calamares-installer-restart-now", "screenshot" : "install_calamares-22.png" }, { "screenshot" : "install_calamares-23.png", "frametime" : [ "16.75", "16.79" ], "needle" : "live-remove-medium", "tags" : [ "live-remove-medium" ], "json" : "neon/needles/live-remove-medium.json", "result" : "ok", "area" : [ { "w" : "598", "y" : 561, "h" : "116", "result" : "ok", "x" : 194, "similarity" : 100 } ], "properties" : [] }, { "needle" : "sddm", "frametime" : [ "18.75", "18.79" ], "screenshot" : "install_calamares-24.png", "json" : "neon/needles/sddm.json", "result" : "ok", "tags" : [ "sddm" ], "properties" : [], "area" : [ { "w" : "862", "h" : "477", "y" : 211, "result" : "ok", "x" : 91, "similarity" : 100 } ] - } + }, + { +"result": "softfail", +"text": "first_start-119.txt", +"title": "Soft Failed" +} ], "result" : "ok", "dents" : 0 } diff --git a/test/data/results/soft_failure2.json b/test/data/results/soft_failure2.json new file mode 100644 index 0000000..f4b51b3 --- /dev/null +++ b/test/data/results/soft_failure2.json @@ -0,0 +1,5 @@ +{ +"result": "softfail", +"text": "first_start-119.txt", +"title": "Soft Failed" +}