You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
497 lines
16 KiB
497 lines
16 KiB
# -*- coding: utf-8 -*-
|
|
|
|
#-------------------------------------------------------------------------
|
|
# drawElements Quality Program utilities
|
|
# --------------------------------------
|
|
#
|
|
# Copyright 2015 The Android Open Source Project
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
#
|
|
#-------------------------------------------------------------------------
|
|
|
|
from build.common import *
|
|
from build.config import *
|
|
from build.build import *
|
|
|
|
import os
|
|
import sys
|
|
import string
|
|
import socket
|
|
import fnmatch
|
|
from datetime import datetime
|
|
|
|
BASE_NIGHTLY_DIR = os.path.normpath(os.path.join(DEQP_DIR, "..", "deqp-nightly"))
|
|
BASE_BUILD_DIR = os.path.join(BASE_NIGHTLY_DIR, "build")
|
|
BASE_LOGS_DIR = os.path.join(BASE_NIGHTLY_DIR, "logs")
|
|
BASE_REFS_DIR = os.path.join(BASE_NIGHTLY_DIR, "refs")
|
|
|
|
EXECUTOR_PATH = "executor/executor"
|
|
LOG_TO_CSV_PATH = "executor/testlog-to-csv"
|
|
EXECSERVER_PATH = "execserver/execserver"
|
|
|
|
CASELIST_PATH = os.path.join(DEQP_DIR, "Candy", "Data")
|
|
|
|
COMPARE_NUM_RESULTS = 4
|
|
COMPARE_REPORT_NAME = "nightly-report.html"
|
|
|
|
COMPARE_REPORT_TMPL = '''
|
|
<html>
|
|
<head>
|
|
<title>${TITLE}</title>
|
|
<style type="text/css">
|
|
<!--
|
|
body { font: serif; font-size: 1em; }
|
|
table { border-spacing: 0; border-collapse: collapse; }
|
|
td { border-width: 1px; border-style: solid; border-color: #808080; }
|
|
.Header { font-weight: bold; font-size: 1em; border-style: none; }
|
|
.CasePath { }
|
|
.Pass { background: #80ff80; }
|
|
.Fail { background: #ff4040; }
|
|
.QualityWarning { background: #ffff00; }
|
|
.CompabilityWarning { background: #ffff00; }
|
|
.Pending { background: #808080; }
|
|
.Running { background: #d3d3d3; }
|
|
.NotSupported { background: #ff69b4; }
|
|
.ResourceError { background: #ff4040; }
|
|
.InternalError { background: #ff1493; }
|
|
.Canceled { background: #808080; }
|
|
.Crash { background: #ffa500; }
|
|
.Timeout { background: #ffa500; }
|
|
.Disabled { background: #808080; }
|
|
.Missing { background: #808080; }
|
|
.Ignored { opacity: 0.5; }
|
|
-->
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>${TITLE}</h1>
|
|
<table>
|
|
${RESULTS}
|
|
</table>
|
|
</body>
|
|
</html>
|
|
'''
|
|
|
|
class NightlyRunConfig:
|
|
def __init__(self, name, buildConfig, generator, binaryName, testset, args = [], exclude = [], ignore = []):
|
|
self.name = name
|
|
self.buildConfig = buildConfig
|
|
self.generator = generator
|
|
self.binaryName = binaryName
|
|
self.testset = testset
|
|
self.args = args
|
|
self.exclude = exclude
|
|
self.ignore = ignore
|
|
|
|
def getBinaryPath(self, basePath):
|
|
return os.path.join(self.buildConfig.getBuildDir(), self.generator.getBinaryPath(self.buildConfig.getBuildType(), basePath))
|
|
|
|
class NightlyBuildConfig(BuildConfig):
|
|
def __init__(self, name, buildType, args):
|
|
BuildConfig.__init__(self, os.path.join(BASE_BUILD_DIR, name), buildType, args)
|
|
|
|
class TestCaseResult:
|
|
def __init__ (self, name, statusCode):
|
|
self.name = name
|
|
self.statusCode = statusCode
|
|
|
|
class MultiResult:
|
|
def __init__ (self, name, statusCodes):
|
|
self.name = name
|
|
self.statusCodes = statusCodes
|
|
|
|
class BatchResult:
|
|
def __init__ (self, name):
|
|
self.name = name
|
|
self.results = []
|
|
|
|
def parseResultCsv (data):
|
|
lines = data.splitlines()[1:]
|
|
results = []
|
|
|
|
for line in lines:
|
|
items = line.split(",")
|
|
results.append(TestCaseResult(items[0], items[1]))
|
|
|
|
return results
|
|
|
|
def readTestCaseResultsFromCSV (filename):
|
|
return parseResultCsv(readFile(filename))
|
|
|
|
def readBatchResultFromCSV (filename, batchResultName = None):
|
|
batchResult = BatchResult(batchResultName if batchResultName != None else os.path.basename(filename))
|
|
batchResult.results = readTestCaseResultsFromCSV(filename)
|
|
return batchResult
|
|
|
|
def getResultTimestamp ():
|
|
return datetime.now().strftime("%Y-%m-%d-%H-%M")
|
|
|
|
def getCompareFilenames (logsDir):
|
|
files = []
|
|
for file in os.listdir(logsDir):
|
|
fullPath = os.path.join(logsDir, file)
|
|
if os.path.isfile(fullPath) and fnmatch.fnmatch(file, "*.csv"):
|
|
files.append(fullPath)
|
|
files.sort()
|
|
|
|
return files[-COMPARE_NUM_RESULTS:]
|
|
|
|
def parseAsCSV (logPath, config):
|
|
args = [config.getBinaryPath(LOG_TO_CSV_PATH), "--mode=all", "--format=csv", logPath]
|
|
proc = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
out, err = proc.communicate()
|
|
return out
|
|
|
|
def computeUnifiedTestCaseList (batchResults):
|
|
caseList = []
|
|
caseSet = set()
|
|
|
|
for batchResult in batchResults:
|
|
for result in batchResult.results:
|
|
if not result.name in caseSet:
|
|
caseList.append(result.name)
|
|
caseSet.add(result.name)
|
|
|
|
return caseList
|
|
|
|
def computeUnifiedResults (batchResults):
|
|
|
|
def genResultMap (batchResult):
|
|
resMap = {}
|
|
for result in batchResult.results:
|
|
resMap[result.name] = result
|
|
return resMap
|
|
|
|
resultMap = [genResultMap(r) for r in batchResults]
|
|
caseList = computeUnifiedTestCaseList(batchResults)
|
|
results = []
|
|
|
|
for caseName in caseList:
|
|
statusCodes = []
|
|
|
|
for i in range(0, len(batchResults)):
|
|
result = resultMap[i][caseName] if caseName in resultMap[i] else None
|
|
statusCode = result.statusCode if result != None else 'Missing'
|
|
statusCodes.append(statusCode)
|
|
|
|
results.append(MultiResult(caseName, statusCodes))
|
|
|
|
return results
|
|
|
|
def allStatusCodesEqual (result):
|
|
firstCode = result.statusCodes[0]
|
|
for i in range(1, len(result.statusCodes)):
|
|
if result.statusCodes[i] != firstCode:
|
|
return False
|
|
return True
|
|
|
|
def computeDiffResults (unifiedResults):
|
|
diff = []
|
|
for result in unifiedResults:
|
|
if not allStatusCodesEqual(result):
|
|
diff.append(result)
|
|
return diff
|
|
|
|
def genCompareReport (batchResults, title, ignoreCases):
|
|
class TableRow:
|
|
def __init__ (self, testCaseName, innerHTML):
|
|
self.testCaseName = testCaseName
|
|
self.innerHTML = innerHTML
|
|
|
|
unifiedResults = computeUnifiedResults(batchResults)
|
|
diffResults = computeDiffResults(unifiedResults)
|
|
rows = []
|
|
|
|
# header
|
|
headerCol = '<td class="Header">Test case</td>\n'
|
|
for batchResult in batchResults:
|
|
headerCol += '<td class="Header">%s</td>\n' % batchResult.name
|
|
rows.append(TableRow(None, headerCol))
|
|
|
|
# results
|
|
for result in diffResults:
|
|
col = '<td class="CasePath">%s</td>\n' % result.name
|
|
for statusCode in result.statusCodes:
|
|
col += '<td class="%s">%s</td>\n' % (statusCode, statusCode)
|
|
|
|
rows.append(TableRow(result.name, col))
|
|
|
|
tableStr = ""
|
|
for row in rows:
|
|
if row.testCaseName is not None and matchesAnyPattern(row.testCaseName, ignoreCases):
|
|
tableStr += '<tr class="Ignored">\n%s</tr>\n' % row.innerHTML
|
|
else:
|
|
tableStr += '<tr>\n%s</tr>\n' % row.innerHTML
|
|
|
|
html = COMPARE_REPORT_TMPL
|
|
html = html.replace("${TITLE}", title)
|
|
html = html.replace("${RESULTS}", tableStr)
|
|
|
|
return html
|
|
|
|
def matchesAnyPattern (name, patterns):
|
|
for pattern in patterns:
|
|
if fnmatch.fnmatch(name, pattern):
|
|
return True
|
|
return False
|
|
|
|
def statusCodesMatch (refResult, resResult):
|
|
return refResult == 'Missing' or resResult == 'Missing' or refResult == resResult
|
|
|
|
def compareBatchResults (referenceBatch, resultBatch, ignoreCases):
|
|
unifiedResults = computeUnifiedResults([referenceBatch, resultBatch])
|
|
failedCases = []
|
|
|
|
for result in unifiedResults:
|
|
if not matchesAnyPattern(result.name, ignoreCases):
|
|
refResult = result.statusCodes[0]
|
|
resResult = result.statusCodes[1]
|
|
|
|
if not statusCodesMatch(refResult, resResult):
|
|
failedCases.append(result)
|
|
|
|
return failedCases
|
|
|
|
def getUnusedPort ():
|
|
# \note Not 100%-proof method as other apps may grab this port before we launch execserver
|
|
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
s.bind(('localhost', 0))
|
|
addr, port = s.getsockname()
|
|
s.close()
|
|
return port
|
|
|
|
def runNightly (config):
|
|
build(config.buildConfig, config.generator)
|
|
|
|
# Run parameters
|
|
timestamp = getResultTimestamp()
|
|
logDir = os.path.join(BASE_LOGS_DIR, config.name)
|
|
testLogPath = os.path.join(logDir, timestamp + ".qpa")
|
|
infoLogPath = os.path.join(logDir, timestamp + ".txt")
|
|
csvLogPath = os.path.join(logDir, timestamp + ".csv")
|
|
compareLogPath = os.path.join(BASE_REFS_DIR, config.name + ".csv")
|
|
port = getUnusedPort()
|
|
|
|
if not os.path.exists(logDir):
|
|
os.makedirs(logDir)
|
|
|
|
if os.path.exists(testLogPath) or os.path.exists(infoLogPath):
|
|
raise Exception("Result '%s' already exists", timestamp)
|
|
|
|
# Paths, etc.
|
|
binaryName = config.generator.getBinaryPath(config.buildConfig.getBuildType(), os.path.basename(config.binaryName))
|
|
workingDir = os.path.join(config.buildConfig.getBuildDir(), os.path.dirname(config.binaryName))
|
|
|
|
execArgs = [
|
|
config.getBinaryPath(EXECUTOR_PATH),
|
|
'--start-server=%s' % config.getBinaryPath(EXECSERVER_PATH),
|
|
'--port=%d' % port,
|
|
'--binaryname=%s' % binaryName,
|
|
'--cmdline=%s' % string.join([shellquote(arg) for arg in config.args], " "),
|
|
'--workdir=%s' % workingDir,
|
|
'--caselistdir=%s' % CASELIST_PATH,
|
|
'--testset=%s' % string.join(config.testset, ","),
|
|
'--out=%s' % testLogPath,
|
|
'--info=%s' % infoLogPath,
|
|
'--summary=no'
|
|
]
|
|
|
|
if len(config.exclude) > 0:
|
|
execArgs += ['--exclude=%s' % string.join(config.exclude, ",")]
|
|
|
|
execute(execArgs)
|
|
|
|
# Translate to CSV for comparison purposes
|
|
lastResultCsv = parseAsCSV(testLogPath, config)
|
|
writeFile(csvLogPath, lastResultCsv)
|
|
|
|
if os.path.exists(compareLogPath):
|
|
refBatchResult = readBatchResultFromCSV(compareLogPath, "reference")
|
|
else:
|
|
refBatchResult = None
|
|
|
|
# Generate comparison report
|
|
compareFilenames = getCompareFilenames(logDir)
|
|
batchResults = [readBatchResultFromCSV(filename) for filename in compareFilenames]
|
|
|
|
if refBatchResult != None:
|
|
batchResults = [refBatchResult] + batchResults
|
|
|
|
writeFile(COMPARE_REPORT_NAME, genCompareReport(batchResults, config.name, config.ignore))
|
|
print("Comparison report written to %s" % COMPARE_REPORT_NAME)
|
|
|
|
# Compare to reference
|
|
if refBatchResult != None:
|
|
curBatchResult = BatchResult("current")
|
|
curBatchResult.results = parseResultCsv(lastResultCsv)
|
|
failedCases = compareBatchResults(refBatchResult, curBatchResult, config.ignore)
|
|
|
|
print("")
|
|
for result in failedCases:
|
|
print("MISMATCH: %s: expected %s, got %s" % (result.name, result.statusCodes[0], result.statusCodes[1]))
|
|
|
|
print("")
|
|
print("%d / %d cases passed, run %s" % (len(curBatchResult.results)-len(failedCases), len(curBatchResult.results), "FAILED" if len(failedCases) > 0 else "passed"))
|
|
|
|
if len(failedCases) > 0:
|
|
return False
|
|
|
|
return True
|
|
|
|
# Configurations
|
|
|
|
DEFAULT_WIN32_GENERATOR = ANY_VS_X32_GENERATOR
|
|
DEFAULT_WIN64_GENERATOR = ANY_VS_X64_GENERATOR
|
|
|
|
WGL_X64_RELEASE_BUILD_CFG = NightlyBuildConfig("wgl_x64_release", "Release", ['-DDEQP_TARGET=win32_wgl'])
|
|
ARM_GLES3_EMU_X32_RELEASE_BUILD_CFG = NightlyBuildConfig("arm_gles3_emu_release", "Release", ['-DDEQP_TARGET=arm_gles3_emu'])
|
|
|
|
BASE_ARGS = ['--deqp-visibility=hidden', '--deqp-watchdog=enable', '--deqp-crashhandler=enable']
|
|
|
|
CONFIGS = [
|
|
NightlyRunConfig(
|
|
name = "wgl_x64_release_gles2",
|
|
buildConfig = WGL_X64_RELEASE_BUILD_CFG,
|
|
generator = DEFAULT_WIN64_GENERATOR,
|
|
binaryName = "modules/gles2/deqp-gles2",
|
|
args = ['--deqp-gl-config-name=rgba8888d24s8ms0'] + BASE_ARGS,
|
|
testset = ["dEQP-GLES2.info.*", "dEQP-GLES2.functional.*", "dEQP-GLES2.usecases.*"],
|
|
exclude = [
|
|
"dEQP-GLES2.functional.shaders.loops.*while*unconditional_continue*",
|
|
"dEQP-GLES2.functional.shaders.loops.*while*only_continue*",
|
|
"dEQP-GLES2.functional.shaders.loops.*while*double_continue*",
|
|
],
|
|
ignore = []
|
|
),
|
|
NightlyRunConfig(
|
|
name = "wgl_x64_release_gles3",
|
|
buildConfig = WGL_X64_RELEASE_BUILD_CFG,
|
|
generator = DEFAULT_WIN64_GENERATOR,
|
|
binaryName = "modules/gles3/deqp-gles3",
|
|
args = ['--deqp-gl-config-name=rgba8888d24s8ms0'] + BASE_ARGS,
|
|
testset = ["dEQP-GLES3.info.*", "dEQP-GLES3.functional.*", "dEQP-GLES3.usecases.*"],
|
|
exclude = [
|
|
"dEQP-GLES3.functional.shaders.loops.*while*unconditional_continue*",
|
|
"dEQP-GLES3.functional.shaders.loops.*while*only_continue*",
|
|
"dEQP-GLES3.functional.shaders.loops.*while*double_continue*",
|
|
],
|
|
ignore = [
|
|
"dEQP-GLES3.functional.transform_feedback.*",
|
|
"dEQP-GLES3.functional.occlusion_query.*",
|
|
"dEQP-GLES3.functional.lifetime.*",
|
|
"dEQP-GLES3.functional.fragment_ops.depth_stencil.stencil_ops",
|
|
]
|
|
),
|
|
NightlyRunConfig(
|
|
name = "wgl_x64_release_gles31",
|
|
buildConfig = WGL_X64_RELEASE_BUILD_CFG,
|
|
generator = DEFAULT_WIN64_GENERATOR,
|
|
binaryName = "modules/gles31/deqp-gles31",
|
|
args = ['--deqp-gl-config-name=rgba8888d24s8ms0'] + BASE_ARGS,
|
|
testset = ["dEQP-GLES31.*"],
|
|
exclude = [],
|
|
ignore = [
|
|
"dEQP-GLES31.functional.draw_indirect.negative.command_bad_alignment_3",
|
|
"dEQP-GLES31.functional.draw_indirect.negative.command_offset_not_in_buffer",
|
|
"dEQP-GLES31.functional.vertex_attribute_binding.negative.bind_vertex_buffer_negative_offset",
|
|
"dEQP-GLES31.functional.ssbo.layout.single_basic_type.packed.mediump_uint",
|
|
"dEQP-GLES31.functional.blend_equation_advanced.basic.*",
|
|
"dEQP-GLES31.functional.blend_equation_advanced.srgb.*",
|
|
"dEQP-GLES31.functional.blend_equation_advanced.barrier.*",
|
|
"dEQP-GLES31.functional.uniform_location.*",
|
|
"dEQP-GLES31.functional.debug.negative_coverage.log.state.get_framebuffer_attachment_parameteriv",
|
|
"dEQP-GLES31.functional.debug.negative_coverage.log.state.get_renderbuffer_parameteriv",
|
|
"dEQP-GLES31.functional.debug.error_filters.case_0",
|
|
"dEQP-GLES31.functional.debug.error_filters.case_2",
|
|
]
|
|
),
|
|
NightlyRunConfig(
|
|
name = "wgl_x64_release_gl3",
|
|
buildConfig = WGL_X64_RELEASE_BUILD_CFG,
|
|
generator = DEFAULT_WIN64_GENERATOR,
|
|
binaryName = "modules/gl3/deqp-gl3",
|
|
args = ['--deqp-gl-config-name=rgba8888d24s8ms0'] + BASE_ARGS,
|
|
testset = ["dEQP-GL3.info.*", "dEQP-GL3.functional.*"],
|
|
exclude = [
|
|
"dEQP-GL3.functional.shaders.loops.*while*unconditional_continue*",
|
|
"dEQP-GL3.functional.shaders.loops.*while*only_continue*",
|
|
"dEQP-GL3.functional.shaders.loops.*while*double_continue*",
|
|
],
|
|
ignore = [
|
|
"dEQP-GL3.functional.transform_feedback.*"
|
|
]
|
|
),
|
|
NightlyRunConfig(
|
|
name = "arm_gles3_emu_x32_egl",
|
|
buildConfig = ARM_GLES3_EMU_X32_RELEASE_BUILD_CFG,
|
|
generator = DEFAULT_WIN32_GENERATOR,
|
|
binaryName = "modules/egl/deqp-egl",
|
|
args = BASE_ARGS,
|
|
testset = ["dEQP-EGL.info.*", "dEQP-EGL.functional.*"],
|
|
exclude = [
|
|
"dEQP-EGL.functional.sharing.gles2.multithread.*",
|
|
"dEQP-EGL.functional.multithread.*",
|
|
],
|
|
ignore = []
|
|
),
|
|
NightlyRunConfig(
|
|
name = "opencl_x64_release",
|
|
buildConfig = NightlyBuildConfig("opencl_x64_release", "Release", ['-DDEQP_TARGET=opencl_icd']),
|
|
generator = DEFAULT_WIN64_GENERATOR,
|
|
binaryName = "modules/opencl/deqp-opencl",
|
|
args = ['--deqp-cl-platform-id=2 --deqp-cl-device-ids=1'] + BASE_ARGS,
|
|
testset = ["dEQP-CL.*"],
|
|
exclude = ["dEQP-CL.performance.*", "dEQP-CL.robustness.*", "dEQP-CL.stress.memory.*"],
|
|
ignore = [
|
|
"dEQP-CL.scheduler.random.*",
|
|
"dEQP-CL.language.set_kernel_arg.random_structs.*",
|
|
"dEQP-CL.language.builtin_function.work_item.invalid_get_global_offset",
|
|
"dEQP-CL.language.call_function.arguments.random_structs.*",
|
|
"dEQP-CL.language.call_kernel.random_structs.*",
|
|
"dEQP-CL.language.inf_nan.nan.frexp.float",
|
|
"dEQP-CL.language.inf_nan.nan.lgamma_r.float",
|
|
"dEQP-CL.language.inf_nan.nan.modf.float",
|
|
"dEQP-CL.language.inf_nan.nan.sqrt.float",
|
|
"dEQP-CL.api.multithread.*",
|
|
"dEQP-CL.api.callback.random.nested.*",
|
|
"dEQP-CL.api.memory_migration.out_of_order_host.image2d.single_device_kernel_migrate_validate_abb",
|
|
"dEQP-CL.api.memory_migration.out_of_order.image2d.single_device_kernel_migrate_kernel_validate_abbb",
|
|
"dEQP-CL.image.addressing_filtering12.1d_array.*",
|
|
"dEQP-CL.image.addressing_filtering12.2d_array.*"
|
|
]
|
|
)
|
|
]
|
|
|
|
if __name__ == "__main__":
|
|
config = None
|
|
|
|
if len(sys.argv) == 2:
|
|
cfgName = sys.argv[1]
|
|
for curCfg in CONFIGS:
|
|
if curCfg.name == cfgName:
|
|
config = curCfg
|
|
break
|
|
|
|
if config != None:
|
|
isOk = runNightly(config)
|
|
if not isOk:
|
|
sys.exit(-1)
|
|
else:
|
|
print("%s: [config]" % sys.argv[0])
|
|
print("")
|
|
print(" Available configs:")
|
|
for config in CONFIGS:
|
|
print(" %s" % config.name)
|
|
sys.exit(-1)
|