blob: 6843bd69d1434e4404850606d03020a7aa399f46 [file] [log] [blame]
# 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 wasFlakyAndPassed(plan, successes, tries)
end
def visiting(plan)
end
def visit!
@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?
# Not completed yet
next
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 testWasSuccessful
wasFlakyAndPassed(plan, statuses.count(STATUS_FILE_PASS), statuses.size)
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
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)
append("flaky", "#{plan.name} #{successes} #{tries}")
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 < TestResultEvaluator
attr_reader :noresult, :familyMap
def initialize(runlist, statusMap)
super(runlist, statusMap)
@noresult = 0
@familyMap = {}
@writer = ResultsWriter.new
end
def noResult(plan)
@writer.appendNoResult(plan)
@noresult += 1
end
def passed(plan)
@writer.appendPass(plan)
appendResult(plan, true)
addToFamilyMap(plan, "PASS")
end
def failed(plan)
@writer.appendFailure(plan)
appendResult(plan, false)
addToFamilyMap(plan, "FAIL")
end
def wasFlakyAndPassed(plan, successes, tries)
@writer.appendFlaky(plan, successes, tries)
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