# -*- 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)