| # Copyright (C) 2021 Igalia S.L. |
| # |
| # Redistribution and use in source and binary forms, with or without |
| # modification, are permitted provided that the following conditions |
| # are met: |
| # |
| # 1. Redistributions of source code must retain the above copyright |
| # notice, this list of conditions and the following disclaimer. |
| # 2. Redistributions in binary form must reproduce the above copyright |
| # notice, this list of conditions and the following disclaimer in the |
| # documentation and/or other materials provided with the distribution. |
| # |
| # THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY |
| # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
| # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
| # DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY |
| # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES |
| # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
| # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND |
| # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF |
| # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| |
| # Visits the tests in the runlist and invokes a number of |
| # callbacks. Used via subclassing. |
| class TestResultEvaluator |
| def initialize(runlist, statusMap) |
| @runlist = runlist |
| @statusMap = statusMap |
| end |
| def noResult(plan) |
| # XXX: maybe focus on incomplete in reporting? |
| end |
| def passed(plan) |
| end |
| def failed(plan) |
| end |
| def wasFlaky(plan, successes, tries, successful) |
| end |
| def visiting(plan) |
| end |
| def validate |
| end |
| def visit!(opts={}) |
| @runlist.each { |
| |plan| |
| visiting(plan) |
| if not @statusMap.has_key?(plan.index) |
| noResult(plan) |
| next |
| end |
| statuses = @statusMap[plan.index] |
| testWasSuccessful = nil |
| if plan.retryParameters.nil? |
| # If any of the results we got was a pass, consider the |
| # test successful. The assumption here is that, unless a |
| # test is marked as flaky, failures are because of |
| # infrastructure issues (which is why the test was run |
| # multiple times in the first place). |
| testWasSuccessful = statuses.include?(STATUS_FILE_PASS) |
| else |
| testWasSuccessful = plan.retryParameters.result(statuses) |
| if testWasSuccessful.nil? |
| if opts[:treatIncompleteAsFailed] |
| # Maximum number of iterations reached without a |
| # conclusive result; draw the developer's attention. |
| testWasSuccessful = false |
| else |
| # Not completed yet |
| next |
| end |
| end |
| end |
| if testWasSuccessful |
| passed(plan) |
| else |
| failed(plan) |
| end |
| nresults = statuses.uniq.size |
| if nresults < 1 |
| $stderr.puts("Unexpected number of results: #{nresults} for plan #{plan}") |
| raise "Unexpected number of results" |
| elsif nresults != 1 and not plan.retryParameters.nil? |
| # The flakiness stats are about diagnosing code issues, so only |
| # record flakiness when we've intentionally retried the test. |
| # Otherwise, multiple results might have come in because of |
| # transient infrastructure failures causing the test to be |
| # rescheduled on another host too. But then any failures might |
| # be because the machine was under load (causing the test to hit |
| # a timeout) or OOM'd. |
| wasFlaky(plan, statuses.count(STATUS_FILE_PASS), statuses.size, testWasSuccessful) |
| end |
| } |
| end |
| end |
| |
| # Simple bean counting class with no side-effects. |
| class TestResultEvaluatorSimple < TestResultEvaluator |
| attr_reader :testsPassed, :testsFailed |
| def initialize(runlist, statusMap) |
| super(runlist, statusMap) |
| @testsPassed = Set.new |
| @testsFailed = Set.new |
| @noResult = Set.new |
| @all = Set.new |
| end |
| def visiting(plan) |
| @all.add(plan) |
| end |
| def passed(plan) |
| @testsPassed.add(plan) |
| end |
| def failed(plan) |
| @testsFailed.add(plan) |
| end |
| def noResult(plan) |
| @noResult.add(plan) |
| end |
| def completed |
| @testsPassed | @testsFailed |
| end |
| def missing |
| @noResult |
| end |
| def validate |
| counted = @testsPassed.size + @testsFailed.size + @noResult.size |
| if counted != @all.size |
| $stderr.puts("Accounting mismatch: expected #{@all.size} test results " + |
| "but got #{counted} (#{@testsPassed.size}, #{@testsFailed.size}, #{@noResult.size})") |
| raise "Internal error in #{File.basename($0)}" |
| end |
| end |
| end |
| |
| class ResultsWriter |
| def initialize |
| @files = {} |
| end |
| def append(filename, s) |
| if not @files.has_key?(filename) |
| @files[filename] = File.open($outputDir + filename, "a") |
| end |
| @files[filename].puts(s) |
| end |
| def appendFailure(plan) |
| append("failed", plan.name) |
| end |
| def appendPass(plan) |
| append("passed", plan.name) |
| end |
| def appendNoResult(plan) |
| append("noresult", plan.name) |
| end |
| def appendFlaky(plan, successes, tries, successful) |
| successful = successful ? 1 : 0 |
| append("flaky", "#{plan.name} #{successes} #{tries} #{successful}") |
| end |
| def appendResult(plan, didPass) |
| append("results", "#{plan.name}: #{didPass ? 'PASS' : 'FAIL'}") |
| end |
| def close |
| @files.each_value { |f| |
| f.close |
| } |
| @files.clear |
| end |
| end |
| |
| # Final accounting of results. |
| class TestResultEvaluatorFinal < TestResultEvaluatorSimple |
| attr_reader :noresult, :familyMap |
| def initialize(runlist, statusMap) |
| super(runlist, statusMap) |
| @noresult = 0 |
| @familyMap = {} |
| @writer = ResultsWriter.new |
| end |
| def noResult(plan) |
| super(plan) |
| @writer.appendNoResult(plan) |
| @noresult += 1 |
| end |
| def passed(plan) |
| super(plan) |
| @writer.appendPass(plan) |
| appendResult(plan, true) |
| addToFamilyMap(plan, "PASS") |
| end |
| def failed(plan) |
| super(plan) |
| @writer.appendFailure(plan) |
| appendResult(plan, false) |
| addToFamilyMap(plan, "FAIL") |
| end |
| def wasFlaky(plan, successes, tries, successful) |
| @writer.appendFlaky(plan, successes, tries, successful) |
| end |
| private |
| def appendResult(plan, successful) |
| @writer.appendResult(plan, successful) |
| end |
| def addToFamilyMap(plan, result) |
| unless familyMap[plan.family] |
| familyMap[plan.family] = [] |
| end |
| familyMap[plan.family] << { :result => result, :plan => plan} |
| end |
| end |
| |
| def verifyCompletedTestsMatchExpected(statusMap, completed, expected) |
| if completed != expected |
| unexpectedlyCompleted = (completed - expected).collect { |plan| |
| "##{plan.index}: #{statusMap[plan.index]}" |
| }.join(" | ") |
| $stderr.puts("Unexpectedly completed: #{unexpectedlyCompleted}") |
| expectedButNotCompleted = (expected - completed).collect { |plan| |
| "##{plan.index}: #{statusMap[plan.index]}" |
| }.join(" | ") |
| $stderr.puts("Expected but not completed: #{expectedButNotCompleted}") |
| raise "mismatch" |
| end |
| end |
| |
| module TestResultEvaluatorSelfTests |
| def TestResultEvaluatorSelfTests.selfTestTestResultEvaluator |
| runlist = [] |
| statusMap = {} |
| expectedPass = Set.new |
| expectedFail = Set.new |
| tc = Struct.new(:statuses, :expectedPass) |
| [ |
| tc.new([STATUS_FILE_PASS], true), |
| tc.new([STATUS_FILE_PASS], true), |
| tc.new([STATUS_FILE_PASS, STATUS_FILE_PASS], true), |
| tc.new([STATUS_FILE_PASS, STATUS_FILE_FAIL], true), |
| tc.new([STATUS_FILE_FAIL], false), |
| tc.new([STATUS_FILE_FAIL, STATUS_FILE_FAIL], false), |
| ].each_with_index { |
| |test, testIndex| |
| plan = BasePlan.mock("f", "t#{testIndex}") |
| runlist << plan |
| statusMap[plan.index] = test.statuses |
| if test.expectedPass |
| expectedPass.add(plan) |
| else |
| expectedFail.add(plan) |
| end |
| } |
| evaluator = TestResultEvaluatorSimple.new(runlist, statusMap) |
| evaluator.visit! |
| |
| if expectedPass != evaluator.testsPassed |
| $stderr.puts("expectedPass: #{expectedPass}") |
| $stderr.puts("Passed: #{evaluator.testsPassed}") |
| raise "expectedPass mismatch" |
| end |
| if expectedFail != evaluator.testsFailed |
| $stderr.puts("expectedFail: #{expectedFail}") |
| $stderr.puts("Failed: #{evaluator.testsFailed}") |
| raise "expectedFail mismatch" |
| end |
| end |
| |
| def getCompletedTestsFromStatusMap(runlist, statusMap) |
| evaluator = TestResultEvaluatorSimple.new(runlist, statusMap) |
| evaluator.visit! |
| evaluator.completed |
| end |
| |
| def TestResultEvaluatorSelfTests.selfTestGetCompletedTestsFromStatusMap |
| runlist = [] |
| expectCompleted = Set.new |
| statusMap = {} |
| tc = Struct.new(:statuses, :retryParameters, :shouldCountAsCompleted) |
| [ |
| # No results => not completed |
| tc.new(nil, nil, false), |
| tc.new(nil, RetryParameters.new(0.6, 2), false), |
| |
| # Passed/failed and not flaky -> completed |
| tc.new([STATUS_FILE_PASS], nil, true), |
| tc.new([STATUS_FILE_FAIL], nil, true), |
| |
| # If it has multiple results and is not a flaky, that means it |
| # was re-run because of infrastructure issues. Counts as |
| # completed regardless of the actual results. |
| tc.new([STATUS_FILE_PASS, STATUS_FILE_PASS], nil, true), |
| tc.new([STATUS_FILE_PASS, STATUS_FILE_FAIL], nil, true), |
| tc.new([STATUS_FILE_FAIL, STATUS_FILE_FAIL], nil, true), |
| |
| # Not completed: we have one run remaining and this could flip |
| # the percentage. |
| tc.new([STATUS_FILE_PASS], RetryParameters.new(0.6, 2), false), |
| tc.new([STATUS_FILE_FAIL], RetryParameters.new(0.4, 2), false), |
| |
| # Even if the last one fails we're still gonna be above |
| # (respectively below) the pass percentage. |
| tc.new([STATUS_FILE_PASS, STATUS_FILE_PASS], RetryParameters.new(0.5, 3), true), |
| tc.new([STATUS_FILE_FAIL, STATUS_FILE_FAIL], RetryParameters.new(0.5, 3), true), |
| ].each_with_index { |
| |test, testIndex| |
| plan = BasePlan.mock("f", "t#{testIndex}", test.retryParameters) |
| runlist << plan |
| if not test.statuses.nil? |
| statusMap[plan.index] = test.statuses |
| end |
| if test.shouldCountAsCompleted |
| expectCompleted.add(plan) |
| end |
| } |
| evaluator = TestResultEvaluatorSimple.new(runlist, statusMap) |
| evaluator.visit! |
| |
| verifyCompletedTestsMatchExpected(statusMap, evaluator.completed, expectCompleted) |
| end |
| def TestResultEvaluatorSelfTests.run |
| TestResultEvaluatorSelfTests.selfTestTestResultEvaluator |
| TestResultEvaluatorSelfTests.selfTestGetCompletedTestsFromStatusMap |
| end |
| end |