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