Home | History | Annotate | Download | only in scripts
      1 # -*- coding: utf-8 -*-
      2 
      3 #-------------------------------------------------------------------------
      4 # drawElements Quality Program utilities
      5 # --------------------------------------
      6 #
      7 # Copyright 2015 The Android Open Source Project
      8 #
      9 # Licensed under the Apache License, Version 2.0 (the "License");
     10 # you may not use this file except in compliance with the License.
     11 # You may obtain a copy of the License at
     12 #
     13 #      http://www.apache.org/licenses/LICENSE-2.0
     14 #
     15 # Unless required by applicable law or agreed to in writing, software
     16 # distributed under the License is distributed on an "AS IS" BASIS,
     17 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     18 # See the License for the specific language governing permissions and
     19 # limitations under the License.
     20 #
     21 #-------------------------------------------------------------------------
     22 
     23 from build.common import *
     24 from build.config import *
     25 from build.build import *
     26 
     27 import os
     28 import sys
     29 import string
     30 import socket
     31 import fnmatch
     32 from datetime import datetime
     33 
     34 BASE_NIGHTLY_DIR	= os.path.normpath(os.path.join(DEQP_DIR, "..", "deqp-nightly"))
     35 BASE_BUILD_DIR		= os.path.join(BASE_NIGHTLY_DIR, "build")
     36 BASE_LOGS_DIR		= os.path.join(BASE_NIGHTLY_DIR, "logs")
     37 BASE_REFS_DIR		= os.path.join(BASE_NIGHTLY_DIR, "refs")
     38 
     39 EXECUTOR_PATH		= "executor/executor"
     40 LOG_TO_CSV_PATH		= "executor/testlog-to-csv"
     41 EXECSERVER_PATH		= "execserver/execserver"
     42 
     43 CASELIST_PATH		= os.path.join(DEQP_DIR, "Candy", "Data")
     44 
     45 COMPARE_NUM_RESULTS	= 4
     46 COMPARE_REPORT_NAME	= "nightly-report.html"
     47 
     48 COMPARE_REPORT_TMPL = '''
     49 <html>
     50 <head>
     51 <title>${TITLE}</title>
     52 <style type="text/css">
     53 <!--
     54 body				{ font: serif; font-size: 1em; }
     55 table				{ border-spacing: 0; border-collapse: collapse; }
     56 td					{ border-width: 1px; border-style: solid; border-color: #808080; }
     57 .Header				{ font-weight: bold; font-size: 1em; border-style: none; }
     58 .CasePath			{ }
     59 .Pass				{ background: #80ff80; }
     60 .Fail				{ background: #ff4040; }
     61 .QualityWarning		{ background: #ffff00; }
     62 .CompabilityWarning	{ background: #ffff00; }
     63 .Pending			{ background: #808080; }
     64 .Running			{ background: #d3d3d3; }
     65 .NotSupported		{ background: #ff69b4; }
     66 .ResourceError		{ background: #ff4040; }
     67 .InternalError		{ background: #ff1493; }
     68 .Canceled			{ background: #808080; }
     69 .Crash				{ background: #ffa500; }
     70 .Timeout			{ background: #ffa500; }
     71 .Disabled			{ background: #808080; }
     72 .Missing			{ background: #808080; }
     73 .Ignored			{ opacity: 0.5; }
     74 -->
     75 </style>
     76 </head>
     77 <body>
     78 <h1>${TITLE}</h1>
     79 <table>
     80 ${RESULTS}
     81 </table>
     82 </body>
     83 </html>
     84 '''
     85 
     86 class NightlyRunConfig:
     87 	def __init__(self, name, buildConfig, generator, binaryName, testset, args = [], exclude = [], ignore = []):
     88 		self.name			= name
     89 		self.buildConfig	= buildConfig
     90 		self.generator		= generator
     91 		self.binaryName		= binaryName
     92 		self.testset		= testset
     93 		self.args			= args
     94 		self.exclude		= exclude
     95 		self.ignore			= ignore
     96 
     97 	def getBinaryPath(self, basePath):
     98 		return os.path.join(self.buildConfig.getBuildDir(), self.generator.getBinaryPath(self.buildConfig.getBuildType(), basePath))
     99 
    100 class NightlyBuildConfig(BuildConfig):
    101 	def __init__(self, name, buildType, args):
    102 		BuildConfig.__init__(self, os.path.join(BASE_BUILD_DIR, name), buildType, args)
    103 
    104 class TestCaseResult:
    105 	def __init__ (self, name, statusCode):
    106 		self.name		= name
    107 		self.statusCode	= statusCode
    108 
    109 class MultiResult:
    110 	def __init__ (self, name, statusCodes):
    111 		self.name			= name
    112 		self.statusCodes	= statusCodes
    113 
    114 class BatchResult:
    115 	def __init__ (self, name):
    116 		self.name		= name
    117 		self.results	= []
    118 
    119 def parseResultCsv (data):
    120 	lines	= data.splitlines()[1:]
    121 	results	= []
    122 
    123 	for line in lines:
    124 		items = line.split(",")
    125 		results.append(TestCaseResult(items[0], items[1]))
    126 
    127 	return results
    128 
    129 def readTestCaseResultsFromCSV (filename):
    130 	return parseResultCsv(readFile(filename))
    131 
    132 def readBatchResultFromCSV (filename, batchResultName = None):
    133 	batchResult = BatchResult(batchResultName if batchResultName != None else os.path.basename(filename))
    134 	batchResult.results = readTestCaseResultsFromCSV(filename)
    135 	return batchResult
    136 
    137 def getResultTimestamp ():
    138 	return datetime.now().strftime("%Y-%m-%d-%H-%M")
    139 
    140 def getCompareFilenames (logsDir):
    141 	files = []
    142 	for file in os.listdir(logsDir):
    143 		fullPath = os.path.join(logsDir, file)
    144 		if os.path.isfile(fullPath) and fnmatch.fnmatch(file, "*.csv"):
    145 			files.append(fullPath)
    146 	files.sort()
    147 
    148 	return files[-COMPARE_NUM_RESULTS:]
    149 
    150 def parseAsCSV (logPath, config):
    151 	args = [config.getBinaryPath(LOG_TO_CSV_PATH), "--mode=all", "--format=csv", logPath]
    152 	proc = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    153 	out, err = proc.communicate()
    154 	return out
    155 
    156 def computeUnifiedTestCaseList (batchResults):
    157 	caseList	= []
    158 	caseSet		= set()
    159 
    160 	for batchResult in batchResults:
    161 		for result in batchResult.results:
    162 			if not result.name in caseSet:
    163 				caseList.append(result.name)
    164 				caseSet.add(result.name)
    165 
    166 	return caseList
    167 
    168 def computeUnifiedResults (batchResults):
    169 
    170 	def genResultMap (batchResult):
    171 		resMap = {}
    172 		for result in batchResult.results:
    173 			resMap[result.name] = result
    174 		return resMap
    175 
    176 	resultMap	= [genResultMap(r) for r in batchResults]
    177 	caseList	= computeUnifiedTestCaseList(batchResults)
    178 	results		= []
    179 
    180 	for caseName in caseList:
    181 		statusCodes = []
    182 
    183 		for i in range(0, len(batchResults)):
    184 			result		= resultMap[i][caseName] if caseName in resultMap[i] else None
    185 			statusCode	= result.statusCode if result != None else 'Missing'
    186 			statusCodes.append(statusCode)
    187 
    188 		results.append(MultiResult(caseName, statusCodes))
    189 
    190 	return results
    191 
    192 def allStatusCodesEqual (result):
    193 	firstCode = result.statusCodes[0]
    194 	for i in range(1, len(result.statusCodes)):
    195 		if result.statusCodes[i] != firstCode:
    196 			return False
    197 	return True
    198 
    199 def computeDiffResults (unifiedResults):
    200 	diff = []
    201 	for result in unifiedResults:
    202 		if not allStatusCodesEqual(result):
    203 			diff.append(result)
    204 	return diff
    205 
    206 def genCompareReport (batchResults, title, ignoreCases):
    207 	class TableRow:
    208 		def __init__ (self, testCaseName, innerHTML):
    209 			self.testCaseName = testCaseName
    210 			self.innerHTML = innerHTML
    211 
    212 	unifiedResults	= computeUnifiedResults(batchResults)
    213 	diffResults		= computeDiffResults(unifiedResults)
    214 	rows			= []
    215 
    216 	# header
    217 	headerCol = '<td class="Header">Test case</td>\n'
    218 	for batchResult in batchResults:
    219 		headerCol += '<td class="Header">%s</td>\n' % batchResult.name
    220 	rows.append(TableRow(None, headerCol))
    221 
    222 	# results
    223 	for result in diffResults:
    224 		col = '<td class="CasePath">%s</td>\n' % result.name
    225 		for statusCode in result.statusCodes:
    226 			col += '<td class="%s">%s</td>\n' % (statusCode, statusCode)
    227 
    228 		rows.append(TableRow(result.name, col))
    229 
    230 	tableStr = ""
    231 	for row in rows:
    232 		if row.testCaseName is not None and matchesAnyPattern(row.testCaseName, ignoreCases):
    233 			tableStr += '<tr class="Ignored">\n%s</tr>\n' % row.innerHTML
    234 		else:
    235 			tableStr += '<tr>\n%s</tr>\n' % row.innerHTML
    236 
    237 	html = COMPARE_REPORT_TMPL
    238 	html = html.replace("${TITLE}", title)
    239 	html = html.replace("${RESULTS}", tableStr)
    240 
    241 	return html
    242 
    243 def matchesAnyPattern (name, patterns):
    244 	for pattern in patterns:
    245 		if fnmatch.fnmatch(name, pattern):
    246 			return True
    247 	return False
    248 
    249 def statusCodesMatch (refResult, resResult):
    250 	return refResult == 'Missing' or resResult == 'Missing' or refResult == resResult
    251 
    252 def compareBatchResults (referenceBatch, resultBatch, ignoreCases):
    253 	unifiedResults	= computeUnifiedResults([referenceBatch, resultBatch])
    254 	failedCases		= []
    255 
    256 	for result in unifiedResults:
    257 		if not matchesAnyPattern(result.name, ignoreCases):
    258 			refResult		= result.statusCodes[0]
    259 			resResult		= result.statusCodes[1]
    260 
    261 			if not statusCodesMatch(refResult, resResult):
    262 				failedCases.append(result)
    263 
    264 	return failedCases
    265 
    266 def getUnusedPort ():
    267 	# \note Not 100%-proof method as other apps may grab this port before we launch execserver
    268 	s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    269 	s.bind(('localhost', 0))
    270 	addr, port = s.getsockname()
    271 	s.close()
    272 	return port
    273 
    274 def runNightly (config):
    275 	build(config.buildConfig, config.generator)
    276 
    277 	# Run parameters
    278 	timestamp		= getResultTimestamp()
    279 	logDir			= os.path.join(BASE_LOGS_DIR, config.name)
    280 	testLogPath		= os.path.join(logDir, timestamp + ".qpa")
    281 	infoLogPath		= os.path.join(logDir, timestamp + ".txt")
    282 	csvLogPath		= os.path.join(logDir, timestamp + ".csv")
    283 	compareLogPath	= os.path.join(BASE_REFS_DIR, config.name + ".csv")
    284 	port			= getUnusedPort()
    285 
    286 	if not os.path.exists(logDir):
    287 		os.makedirs(logDir)
    288 
    289 	if os.path.exists(testLogPath) or os.path.exists(infoLogPath):
    290 		raise Exception("Result '%s' already exists", timestamp)
    291 
    292 	# Paths, etc.
    293 	binaryName		= config.generator.getBinaryPath(config.buildConfig.getBuildType(), os.path.basename(config.binaryName))
    294 	workingDir		= os.path.join(config.buildConfig.getBuildDir(), os.path.dirname(config.binaryName))
    295 
    296 	execArgs = [
    297 		config.getBinaryPath(EXECUTOR_PATH),
    298 		'--start-server=%s' % config.getBinaryPath(EXECSERVER_PATH),
    299 		'--port=%d' % port,
    300 		'--binaryname=%s' % binaryName,
    301 		'--cmdline=%s' % string.join([shellquote(arg) for arg in config.args], " "),
    302 		'--workdir=%s' % workingDir,
    303 		'--caselistdir=%s' % CASELIST_PATH,
    304 		'--testset=%s' % string.join(config.testset, ","),
    305 		'--out=%s' % testLogPath,
    306 		'--info=%s' % infoLogPath,
    307 		'--summary=no'
    308 	]
    309 
    310 	if len(config.exclude) > 0:
    311 		execArgs += ['--exclude=%s' % string.join(config.exclude, ",")]
    312 
    313 	execute(execArgs)
    314 
    315 	# Translate to CSV for comparison purposes
    316 	lastResultCsv		= parseAsCSV(testLogPath, config)
    317 	writeFile(csvLogPath, lastResultCsv)
    318 
    319 	if os.path.exists(compareLogPath):
    320 		refBatchResult = readBatchResultFromCSV(compareLogPath, "reference")
    321 	else:
    322 		refBatchResult = None
    323 
    324 	# Generate comparison report
    325 	compareFilenames	= getCompareFilenames(logDir)
    326 	batchResults		= [readBatchResultFromCSV(filename) for filename in compareFilenames]
    327 
    328 	if refBatchResult != None:
    329 		batchResults = [refBatchResult] + batchResults
    330 
    331 	writeFile(COMPARE_REPORT_NAME, genCompareReport(batchResults, config.name, config.ignore))
    332 	print "Comparison report written to %s" % COMPARE_REPORT_NAME
    333 
    334 	# Compare to reference
    335 	if refBatchResult != None:
    336 		curBatchResult		= BatchResult("current")
    337 		curBatchResult.results = parseResultCsv(lastResultCsv)
    338 		failedCases			= compareBatchResults(refBatchResult, curBatchResult, config.ignore)
    339 
    340 		print ""
    341 		for result in failedCases:
    342 			print "MISMATCH: %s: expected %s, got %s" % (result.name, result.statusCodes[0], result.statusCodes[1])
    343 
    344 		print ""
    345 		print "%d / %d cases passed, run %s" % (len(curBatchResult.results)-len(failedCases), len(curBatchResult.results), "FAILED" if len(failedCases) > 0 else "passed")
    346 
    347 		if len(failedCases) > 0:
    348 			return False
    349 
    350 	return True
    351 
    352 # Configurations
    353 
    354 DEFAULT_WIN32_GENERATOR				= ANY_VS_X32_GENERATOR
    355 DEFAULT_WIN64_GENERATOR				= ANY_VS_X64_GENERATOR
    356 
    357 WGL_X64_RELEASE_BUILD_CFG			= NightlyBuildConfig("wgl_x64_release", "Release", ['-DDEQP_TARGET=win32_wgl'])
    358 ARM_GLES3_EMU_X32_RELEASE_BUILD_CFG	= NightlyBuildConfig("arm_gles3_emu_release", "Release", ['-DDEQP_TARGET=arm_gles3_emu'])
    359 
    360 BASE_ARGS							= ['--deqp-visibility=hidden', '--deqp-watchdog=enable', '--deqp-crashhandler=enable']
    361 
    362 CONFIGS = [
    363 	NightlyRunConfig(
    364 		name			= "wgl_x64_release_gles2",
    365 		buildConfig		= WGL_X64_RELEASE_BUILD_CFG,
    366 		generator		= DEFAULT_WIN64_GENERATOR,
    367 		binaryName		= "modules/gles2/deqp-gles2",
    368 		args			= ['--deqp-gl-config-name=rgba8888d24s8ms0'] + BASE_ARGS,
    369 		testset			= ["dEQP-GLES2.info.*", "dEQP-GLES2.functional.*", "dEQP-GLES2.usecases.*"],
    370 		exclude			= [
    371 				"dEQP-GLES2.functional.shaders.loops.*while*unconditional_continue*",
    372 				"dEQP-GLES2.functional.shaders.loops.*while*only_continue*",
    373 				"dEQP-GLES2.functional.shaders.loops.*while*double_continue*",
    374 			],
    375 		ignore			= []
    376 		),
    377 	NightlyRunConfig(
    378 		name			= "wgl_x64_release_gles3",
    379 		buildConfig		= WGL_X64_RELEASE_BUILD_CFG,
    380 		generator		= DEFAULT_WIN64_GENERATOR,
    381 		binaryName		= "modules/gles3/deqp-gles3",
    382 		args			= ['--deqp-gl-config-name=rgba8888d24s8ms0'] + BASE_ARGS,
    383 		testset			= ["dEQP-GLES3.info.*", "dEQP-GLES3.functional.*", "dEQP-GLES3.usecases.*"],
    384 		exclude			= [
    385 				"dEQP-GLES3.functional.shaders.loops.*while*unconditional_continue*",
    386 				"dEQP-GLES3.functional.shaders.loops.*while*only_continue*",
    387 				"dEQP-GLES3.functional.shaders.loops.*while*double_continue*",
    388 			],
    389 		ignore			= [
    390 				"dEQP-GLES3.functional.transform_feedback.*",
    391 				"dEQP-GLES3.functional.occlusion_query.*",
    392 				"dEQP-GLES3.functional.lifetime.*",
    393 				"dEQP-GLES3.functional.fragment_ops.depth_stencil.stencil_ops",
    394 			]
    395 		),
    396 	NightlyRunConfig(
    397 		name			= "wgl_x64_release_gles31",
    398 		buildConfig		= WGL_X64_RELEASE_BUILD_CFG,
    399 		generator		= DEFAULT_WIN64_GENERATOR,
    400 		binaryName		= "modules/gles31/deqp-gles31",
    401 		args			= ['--deqp-gl-config-name=rgba8888d24s8ms0'] + BASE_ARGS,
    402 		testset			= ["dEQP-GLES31.*"],
    403 		exclude			= [],
    404 		ignore			= [
    405 				"dEQP-GLES31.functional.draw_indirect.negative.command_bad_alignment_3",
    406 				"dEQP-GLES31.functional.draw_indirect.negative.command_offset_not_in_buffer",
    407 				"dEQP-GLES31.functional.vertex_attribute_binding.negative.bind_vertex_buffer_negative_offset",
    408 				"dEQP-GLES31.functional.ssbo.layout.single_basic_type.packed.mediump_uint",
    409 				"dEQP-GLES31.functional.blend_equation_advanced.basic.*",
    410 				"dEQP-GLES31.functional.blend_equation_advanced.srgb.*",
    411 				"dEQP-GLES31.functional.blend_equation_advanced.barrier.*",
    412 				"dEQP-GLES31.functional.uniform_location.*",
    413 				"dEQP-GLES31.functional.debug.negative_coverage.log.state.get_framebuffer_attachment_parameteriv",
    414 				"dEQP-GLES31.functional.debug.negative_coverage.log.state.get_renderbuffer_parameteriv",
    415 				"dEQP-GLES31.functional.debug.error_filters.case_0",
    416 				"dEQP-GLES31.functional.debug.error_filters.case_2",
    417 			]
    418 		),
    419 	NightlyRunConfig(
    420 		name			= "wgl_x64_release_gl3",
    421 		buildConfig		= WGL_X64_RELEASE_BUILD_CFG,
    422 		generator		= DEFAULT_WIN64_GENERATOR,
    423 		binaryName		= "modules/gl3/deqp-gl3",
    424 		args			= ['--deqp-gl-config-name=rgba8888d24s8ms0'] + BASE_ARGS,
    425 		testset			= ["dEQP-GL3.info.*", "dEQP-GL3.functional.*"],
    426 		exclude			= [
    427 				"dEQP-GL3.functional.shaders.loops.*while*unconditional_continue*",
    428 				"dEQP-GL3.functional.shaders.loops.*while*only_continue*",
    429 				"dEQP-GL3.functional.shaders.loops.*while*double_continue*",
    430 			],
    431 		ignore			= [
    432 				"dEQP-GL3.functional.transform_feedback.*"
    433 			]
    434 		),
    435 	NightlyRunConfig(
    436 		name			= "arm_gles3_emu_x32_egl",
    437 		buildConfig		= ARM_GLES3_EMU_X32_RELEASE_BUILD_CFG,
    438 		generator		= DEFAULT_WIN32_GENERATOR,
    439 		binaryName		= "modules/egl/deqp-egl",
    440 		args			= BASE_ARGS,
    441 		testset			= ["dEQP-EGL.info.*", "dEQP-EGL.functional.*"],
    442 		exclude			= [
    443 				"dEQP-EGL.functional.sharing.gles2.multithread.*",
    444 				"dEQP-EGL.functional.multithread.*",
    445 			],
    446 		ignore			= []
    447 		),
    448 	NightlyRunConfig(
    449 		name			= "opencl_x64_release",
    450 		buildConfig		= NightlyBuildConfig("opencl_x64_release", "Release", ['-DDEQP_TARGET=opencl_icd']),
    451 		generator		= DEFAULT_WIN64_GENERATOR,
    452 		binaryName		= "modules/opencl/deqp-opencl",
    453 		args			= ['--deqp-cl-platform-id=2 --deqp-cl-device-ids=1'] + BASE_ARGS,
    454 		testset			= ["dEQP-CL.*"],
    455 		exclude			= ["dEQP-CL.performance.*", "dEQP-CL.robustness.*", "dEQP-CL.stress.memory.*"],
    456 		ignore			= [
    457 				"dEQP-CL.scheduler.random.*",
    458 				"dEQP-CL.language.set_kernel_arg.random_structs.*",
    459 				"dEQP-CL.language.builtin_function.work_item.invalid_get_global_offset",
    460 				"dEQP-CL.language.call_function.arguments.random_structs.*",
    461 				"dEQP-CL.language.call_kernel.random_structs.*",
    462 				"dEQP-CL.language.inf_nan.nan.frexp.float",
    463 				"dEQP-CL.language.inf_nan.nan.lgamma_r.float",
    464 				"dEQP-CL.language.inf_nan.nan.modf.float",
    465 				"dEQP-CL.language.inf_nan.nan.sqrt.float",
    466 				"dEQP-CL.api.multithread.*",
    467 				"dEQP-CL.api.callback.random.nested.*",
    468 				"dEQP-CL.api.memory_migration.out_of_order_host.image2d.single_device_kernel_migrate_validate_abb",
    469 				"dEQP-CL.api.memory_migration.out_of_order.image2d.single_device_kernel_migrate_kernel_validate_abbb",
    470 				"dEQP-CL.image.addressing_filtering12.1d_array.*",
    471 				"dEQP-CL.image.addressing_filtering12.2d_array.*"
    472 			]
    473 		)
    474 ]
    475 
    476 if __name__ == "__main__":
    477 	config = None
    478 
    479 	if len(sys.argv) == 2:
    480 		cfgName = sys.argv[1]
    481 		for curCfg in CONFIGS:
    482 			if curCfg.name == cfgName:
    483 				config = curCfg
    484 				break
    485 
    486 	if config != None:
    487 		isOk = runNightly(config)
    488 		if not isOk:
    489 			sys.exit(-1)
    490 	else:
    491 		print "%s: [config]" % sys.argv[0]
    492 		print ""
    493 		print "  Available configs:"
    494 		for config in CONFIGS:
    495 			print "    %s" % config.name
    496 		sys.exit(-1)
    497