1 # -*- coding: utf-8 -*-
3 #-------------------------------------------------------------------------
4 # drawElements Quality Program utilities
5 # --------------------------------------
7 # Copyright 2015 The Android Open Source Project
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
13 # http://www.apache.org/licenses/LICENSE-2.0
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.
21 #-------------------------------------------------------------------------
23 from build.common import *
24 from build.config import *
25 from build.build import *
32 from datetime import datetime
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")
39 EXECUTOR_PATH = "executor/executor"
40 LOG_TO_CSV_PATH = "executor/testlog-to-csv"
41 EXECSERVER_PATH = "execserver/execserver"
43 CASELIST_PATH = os.path.join(DEQP_DIR, "Candy", "Data")
45 COMPARE_NUM_RESULTS = 4
46 COMPARE_REPORT_NAME = "nightly-report.html"
48 COMPARE_REPORT_TMPL = '''
51 <title>${TITLE}</title>
52 <style type="text/css">
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; }
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; }
86 class NightlyRunConfig:
87 def __init__(self, name, buildConfig, generator, binaryName, testset, args = [], exclude = [], ignore = []):
89 self.buildConfig = buildConfig
90 self.generator = generator
91 self.binaryName = binaryName
92 self.testset = testset
94 self.exclude = exclude
97 def getBinaryPath(self, basePath):
98 return os.path.join(self.buildConfig.getBuildDir(), self.generator.getBinaryPath(self.buildConfig.getBuildType(), basePath))
100 class NightlyBuildConfig(BuildConfig):
101 def __init__(self, name, buildType, args):
102 BuildConfig.__init__(self, os.path.join(BASE_BUILD_DIR, name), buildType, args)
104 class TestCaseResult:
105 def __init__ (self, name, statusCode):
107 self.statusCode = statusCode
110 def __init__ (self, name, statusCodes):
112 self.statusCodes = statusCodes
115 def __init__ (self, name):
119 def parseResultCsv (data):
120 lines = data.splitlines()[1:]
124 items = line.split(",")
125 results.append(TestCaseResult(items[0], items[1]))
129 def readTestCaseResultsFromCSV (filename):
130 return parseResultCsv(readFile(filename))
132 def readBatchResultFromCSV (filename, batchResultName = None):
133 batchResult = BatchResult(batchResultName if batchResultName != None else os.path.basename(filename))
134 batchResult.results = readTestCaseResultsFromCSV(filename)
137 def getResultTimestamp ():
138 return datetime.now().strftime("%Y-%m-%d-%H-%M")
140 def getCompareFilenames (logsDir):
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)
148 return files[-COMPARE_NUM_RESULTS:]
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()
156 def computeUnifiedTestCaseList (batchResults):
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)
168 def computeUnifiedResults (batchResults):
170 def genResultMap (batchResult):
172 for result in batchResult.results:
173 resMap[result.name] = result
176 resultMap = [genResultMap(r) for r in batchResults]
177 caseList = computeUnifiedTestCaseList(batchResults)
180 for caseName in caseList:
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)
188 results.append(MultiResult(caseName, statusCodes))
192 def allStatusCodesEqual (result):
193 firstCode = result.statusCodes[0]
194 for i in range(1, len(result.statusCodes)):
195 if result.statusCodes[i] != firstCode:
199 def computeDiffResults (unifiedResults):
201 for result in unifiedResults:
202 if not allStatusCodesEqual(result):
206 def genCompareReport (batchResults, title, ignoreCases):
208 def __init__ (self, testCaseName, innerHTML):
209 self.testCaseName = testCaseName
210 self.innerHTML = innerHTML
212 unifiedResults = computeUnifiedResults(batchResults)
213 diffResults = computeDiffResults(unifiedResults)
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))
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)
228 rows.append(TableRow(result.name, col))
232 if row.testCaseName is not None and matchesAnyPattern(row.testCaseName, ignoreCases):
233 tableStr += '<tr class="Ignored">\n%s</tr>\n' % row.innerHTML
235 tableStr += '<tr>\n%s</tr>\n' % row.innerHTML
237 html = COMPARE_REPORT_TMPL
238 html = html.replace("${TITLE}", title)
239 html = html.replace("${RESULTS}", tableStr)
243 def matchesAnyPattern (name, patterns):
244 for pattern in patterns:
245 if fnmatch.fnmatch(name, pattern):
249 def statusCodesMatch (refResult, resResult):
250 return refResult == 'Missing' or resResult == 'Missing' or refResult == resResult
252 def compareBatchResults (referenceBatch, resultBatch, ignoreCases):
253 unifiedResults = computeUnifiedResults([referenceBatch, resultBatch])
256 for result in unifiedResults:
257 if not matchesAnyPattern(result.name, ignoreCases):
258 refResult = result.statusCodes[0]
259 resResult = result.statusCodes[1]
261 if not statusCodesMatch(refResult, resResult):
262 failedCases.append(result)
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()
274 def runNightly (config):
275 build(config.buildConfig, config.generator)
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()
286 if not os.path.exists(logDir):
289 if os.path.exists(testLogPath) or os.path.exists(infoLogPath):
290 raise Exception("Result '%s' already exists", timestamp)
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))
297 config.getBinaryPath(EXECUTOR_PATH),
298 '--start-server=%s' % config.getBinaryPath(EXECSERVER_PATH),
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,
310 if len(config.exclude) > 0:
311 execArgs += ['--exclude=%s' % string.join(config.exclude, ",")]
315 # Translate to CSV for comparison purposes
316 lastResultCsv = parseAsCSV(testLogPath, config)
317 writeFile(csvLogPath, lastResultCsv)
319 if os.path.exists(compareLogPath):
320 refBatchResult = readBatchResultFromCSV(compareLogPath, "reference")
322 refBatchResult = None
324 # Generate comparison report
325 compareFilenames = getCompareFilenames(logDir)
326 batchResults = [readBatchResultFromCSV(filename) for filename in compareFilenames]
328 if refBatchResult != None:
329 batchResults = [refBatchResult] + batchResults
331 writeFile(COMPARE_REPORT_NAME, genCompareReport(batchResults, config.name, config.ignore))
332 print("Comparison report written to %s" % COMPARE_REPORT_NAME)
334 # Compare to reference
335 if refBatchResult != None:
336 curBatchResult = BatchResult("current")
337 curBatchResult.results = parseResultCsv(lastResultCsv)
338 failedCases = compareBatchResults(refBatchResult, curBatchResult, config.ignore)
341 for result in failedCases:
342 print("MISMATCH: %s: expected %s, got %s" % (result.name, result.statusCodes[0], result.statusCodes[1]))
345 print("%d / %d cases passed, run %s" % (len(curBatchResult.results)-len(failedCases), len(curBatchResult.results), "FAILED" if len(failedCases) > 0 else "passed"))
347 if len(failedCases) > 0:
354 DEFAULT_WIN32_GENERATOR = ANY_VS_X32_GENERATOR
355 DEFAULT_WIN64_GENERATOR = ANY_VS_X64_GENERATOR
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'])
360 BASE_ARGS = ['--deqp-visibility=hidden', '--deqp-watchdog=enable', '--deqp-crashhandler=enable']
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.*"],
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*",
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.*"],
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*",
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",
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.*"],
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",
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.*"],
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*",
432 "dEQP-GL3.functional.transform_feedback.*"
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",
441 testset = ["dEQP-EGL.info.*", "dEQP-EGL.functional.*"],
443 "dEQP-EGL.functional.sharing.gles2.multithread.*",
444 "dEQP-EGL.functional.multithread.*",
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.*"],
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.*"
476 if __name__ == "__main__":
479 if len(sys.argv) == 2:
480 cfgName = sys.argv[1]
481 for curCfg in CONFIGS:
482 if curCfg.name == cfgName:
487 isOk = runNightly(config)
491 print("%s: [config]" % sys.argv[0])
493 print(" Available configs:")
494 for config in CONFIGS:
495 print(" %s" % config.name)