• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# -*- coding: utf-8 -*-
2
3#-------------------------------------------------------------------------
4# drawElements Quality Program utilities
5# --------------------------------------
6#
7# Copyright 2016 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
23from ctsbuild.common import *
24from ctsbuild.config import ANY_GENERATOR
25from ctsbuild.build import build
26from build_caselists import Module, getModuleByName, getBuildConfig, genCaseList, getCaseListPath, DEFAULT_BUILD_DIR, DEFAULT_TARGET
27from fnmatch import fnmatch
28from copy import copy
29from collections import defaultdict
30
31import argparse
32import xml.etree.cElementTree as ElementTree
33import xml.dom.minidom as minidom
34
35APK_NAME		= "com.drawelements.deqp.apk"
36
37GENERATED_FILE_WARNING = """
38     This file has been automatically generated. Edit with caution.
39     """
40
41class Project:
42	def __init__ (self, path, copyright = None):
43		self.path		= path
44		self.copyright	= copyright
45
46class Configuration:
47	def __init__ (self, name, filters, glconfig = None, rotation = None, surfacetype = None, required = False, runtime = None, runByDefault = True, listOfGroupsToSplit = []):
48		self.name					= name
49		self.glconfig				= glconfig
50		self.rotation				= rotation
51		self.surfacetype			= surfacetype
52		self.required				= required
53		self.filters				= filters
54		self.expectedRuntime		= runtime
55		self.runByDefault			= runByDefault
56		self.listOfGroupsToSplit	= listOfGroupsToSplit
57
58class Package:
59	def __init__ (self, module, configurations):
60		self.module			= module
61		self.configurations	= configurations
62
63class Mustpass:
64	def __init__ (self, project, version, packages):
65		self.project	= project
66		self.version	= version
67		self.packages	= packages
68
69class Filter:
70	TYPE_INCLUDE = 0
71	TYPE_EXCLUDE = 1
72
73	def __init__ (self, type, filename):
74		self.type		= type
75		self.filename	= filename
76
77class TestRoot:
78	def __init__ (self):
79		self.children	= []
80
81class TestGroup:
82	def __init__ (self, name):
83		self.name		= name
84		self.children	= []
85
86class TestCase:
87	def __init__ (self, name):
88		self.name			= name
89		self.configurations	= []
90
91class GLESVersion:
92	def __init__(self, major, minor):
93		self.major = major
94		self.minor = minor
95
96	def encode (self):
97		return (self.major << 16) | (self.minor)
98
99def getModuleGLESVersion (module):
100	versions = {
101		'dEQP-EGL':		GLESVersion(2,0),
102		'dEQP-GLES2':	GLESVersion(2,0),
103		'dEQP-GLES3':	GLESVersion(3,0),
104		'dEQP-GLES31':	GLESVersion(3,1)
105	}
106	return versions[module.name] if module.name in versions else None
107
108def getSrcDir (mustpass):
109	return os.path.join(mustpass.project.path, mustpass.version, "src")
110
111def getTmpDir (mustpass):
112	return os.path.join(mustpass.project.path, mustpass.version, "tmp")
113
114def getModuleShorthand (module):
115	assert module.name[:5] == "dEQP-"
116	return module.name[5:].lower()
117
118def getCaseListFileName (package, configuration):
119	return "%s-%s.txt" % (getModuleShorthand(package.module), configuration.name)
120
121def getDstCaseListPath (mustpass):
122	return os.path.join(mustpass.project.path, mustpass.version)
123
124def getCTSPackageName (package):
125	return "com.drawelements.deqp." + getModuleShorthand(package.module)
126
127def getCommandLine (config):
128	cmdLine = ""
129
130	if config.glconfig != None:
131		cmdLine += "--deqp-gl-config-name=%s " % config.glconfig
132
133	if config.rotation != None:
134		cmdLine += "--deqp-screen-rotation=%s " % config.rotation
135
136	if config.surfacetype != None:
137		cmdLine += "--deqp-surface-type=%s " % config.surfacetype
138
139	cmdLine += "--deqp-watchdog=enable"
140
141	return cmdLine
142
143def readCaseDict (filename):
144	# read all cases and organize them in a tree; this is needed for chunked mustpass
145	# groups are stored as dictionaries and cases as list of strings with full case paths
146	groupTree = {}
147	# limit how deep constructed tree should be - this later simplifies applying filters;
148	# if in future we will need to split to separate .txt files deeper groups thet this value should be increased
149	limitGroupTreeDepth = 3
150	# create helper stack that will contain references to currently filled groups, from top to bottom
151	groupStack = None
152	# cretae variable that will hold currentlt processed line from the file
153	processedLine = None
154	with open(filename, 'rt') as f:
155		for nextLine in f:
156			# to be able to build tree structure we need to know what is the next line in the file this is
157			# why the first line read from the file will be actually processed during the second iteration
158			if processedLine is None:
159				processedLine		= nextLine
160				# to simplify code use this section to also extract root node name
161				rootName			= processedLine[7:processedLine.rfind('.')]
162				groupTree[rootName]	= {}
163				groupStack			= [groupTree[rootName]]
164				continue
165			# check if currently processed line is a test case or a group
166			processedEntryType = processedLine[:6]
167			if processedEntryType == "TEST: ":
168				# append this test case to the last group on the stack
169				groupStack[-1].append(processedLine[6:].strip())
170			elif processedEntryType == "GROUP:":
171				# count number of dots in path to determine what is the depth of current group in the tree
172				processedGroupDepth = processedLine.count('.')
173				# limit tree construction just to specified level
174				availableLimit = limitGroupTreeDepth - processedGroupDepth
175				if availableLimit > 0:
176					# check how deep is stack currently
177					groupStackDepth = len(groupStack)
178					# if stack is deeper then depth of current group then we need to pop number of items
179					if processedGroupDepth < groupStackDepth:
180						groupStack = groupStack[:groupStackDepth-(groupStackDepth-processedGroupDepth)]
181					# get group that will have new child - this is the last group on the stack
182					parentGroup = groupStack[-1]
183					# add new dict that will contain other groups or list of cases depending on the next line
184					# and available depth limit (if are about to reach limit we won't add group dictionaries
185					# but just add all cases from deeper groups to the group at this depth)
186					processedGroupName = processedLine[7:-1]
187					parentGroup[processedGroupName] = {} if (nextLine[:6] == "GROUP:") and (availableLimit > 1) else []
188					# add new group to the stack (items in groupStack can be either list or dict)
189					groupStack.append(parentGroup[processedGroupName])
190			# before going to the next line set procesedLine for the next iteration
191			processedLine = nextLine
192	# handle last test case - we need to do it after the loop as in the loop we needed to know what is the next line
193	assert(processedLine[:6] == "TEST: ")
194	groupStack[-1].append(processedLine[6:].strip())
195	return groupTree
196
197def getCaseDict (buildCfg, generator, module):
198	build(buildCfg, generator, [module.binName])
199	genCaseList(buildCfg, generator, module, "txt")
200	return readCaseDict(getCaseListPath(buildCfg, module, "txt"))
201
202def readPatternList (filename):
203	ptrns = []
204	with open(filename, 'rt') as f:
205		for line in f:
206			line = line.strip()
207			if len(line) > 0 and line[0] != '#':
208				ptrns.append(line)
209	return ptrns
210
211
212def constructNewDict(oldDict, listOfCases, op = lambda a: not a):
213	# Helper function used to construct case dictionary without specific cases
214	rootName		= list(oldDict.keys())[0]
215	newDict			= {rootName : {}}
216	newDictStack	= [newDict]
217	oldDictStack	= [oldDict]
218	while True:
219		# mak sure that both stacks have same number of items
220		assert(len(oldDictStack) == len(newDictStack))
221		# when all items from stack were processed then we can exit the loop
222		if len(oldDictStack) == 0:
223			break
224		# grab last item from both stacks
225		itemOnOldStack = oldDictStack.pop()
226		itemOnNewStack = newDictStack.pop()
227		# if item on stack is dictionary then it represents groups and
228		# we need to reconstruct them in new dictionary
229		if type(itemOnOldStack) is dict:
230			assert(type(itemOnNewStack) is dict)
231			listOfGroups = list(itemOnOldStack.keys())
232			for groupName in listOfGroups:
233				# create list or dictionary depending on contnent of child group
234				doesGroupsContainCases = type(itemOnOldStack[groupName]) is list
235				itemOnNewStack[groupName] = [] if doesGroupsContainCases else {}
236				# append groups on stacks
237				assert(type(itemOnNewStack[groupName]) == type(itemOnOldStack[groupName]))
238				newDictStack.append(itemOnNewStack[groupName])
239				oldDictStack.append(itemOnOldStack[groupName])
240		else:
241			# if item on stack is list then it represents group that contain cases we need
242			# to apply filter on each of them to make sure only proper cases are appended
243			assert(type(itemOnOldStack) is list)
244			assert(type(itemOnNewStack) is list)
245			for caseName in itemOnOldStack:
246				if op(caseName in listOfCases):
247					itemOnNewStack.append(caseName)
248	return newDict
249
250def constructSet(caseDict, perGroupOperation):
251	casesSet		= set()
252	dictStack		= [caseDict]
253	while True:
254		# when all items from stack were processed then we can exit the loop
255		if len(dictStack) == 0:
256			break
257		# grab last item from stack
258		itemOnStack = dictStack.pop()
259		# if item on stack is dictionary then it represents groups and we need to add them to stack
260		if type(itemOnStack) is dict:
261			for groupName in itemOnStack.keys():
262				dictStack.append(itemOnStack[groupName])
263		else:
264			# if item on stack is a list of cases we can add them to set containing all cases
265			assert(type(itemOnStack) is list)
266			casesSet = perGroupOperation(casesSet, itemOnStack)
267	return casesSet
268
269def applyPatterns (caseDict, patterns, filename, op):
270	matched			= set()
271	errors			= []
272	trivialPtrns	= [p for p in patterns if p.find('*') < 0]
273	regularPtrns	= [p for p in patterns if p.find('*') >= 0]
274
275	# Construct helper set that contains cases from all groups
276	unionOperation	= lambda resultCasesSet, groupCaseList: resultCasesSet.union(set(groupCaseList))
277	allCasesSet		= constructSet(caseDict, unionOperation)
278
279	# Apply trivial patterns - plain case paths without wildcard
280	for path in trivialPtrns:
281		if path in allCasesSet:
282			if path in matched:
283				errors.append((path, "Same case specified more than once"))
284			matched.add(path)
285		else:
286			errors.append((path, "Test case not found"))
287
288	# Construct new dictionary but without already matched paths
289	curDict = constructNewDict(caseDict, matched)
290
291	# Apply regular patterns - paths with wildcard
292	for pattern in regularPtrns:
293
294		# Helper function that checks if cases from case group match pattern
295		def matchOperation(resultCasesSet, groupCaseList):
296			for caseName in groupCaseList:
297				if fnmatch(caseName, pattern):
298					resultCasesSet.add(caseName)
299			return resultCasesSet
300
301		matchedThisPtrn = constructSet(curDict, matchOperation)
302
303		if len(matchedThisPtrn) == 0:
304			errors.append((pattern, "Pattern didn't match any cases"))
305
306		matched = matched | matchedThisPtrn
307
308		# To speed up search construct smaller case dictionary without already matched paths
309		curDict = constructNewDict(curDict, matched)
310
311	for pattern, reason in errors:
312		print("ERROR: %s: %s" % (reason, pattern))
313
314	if len(errors) > 0:
315		die("Found %s invalid patterns while processing file %s" % (len(errors), filename))
316
317	# Construct final dictionary using aproperiate operation
318	return constructNewDict(caseDict, matched, op)
319
320def applyInclude (caseDict, patterns, filename):
321	return applyPatterns(caseDict, patterns, filename, lambda b: b)
322
323def applyExclude (caseDict, patterns, filename):
324	return applyPatterns(caseDict, patterns, filename, lambda b: not b)
325
326def readPatternLists (mustpass):
327	lists = {}
328	for package in mustpass.packages:
329		for cfg in package.configurations:
330			for filter in cfg.filters:
331				if not filter.filename in lists:
332					lists[filter.filename] = readPatternList(os.path.join(getSrcDir(mustpass), filter.filename))
333	return lists
334
335def applyFilters (caseDict, patternLists, filters):
336	res = copy(caseDict)
337	for filter in filters:
338		ptrnList = patternLists[filter.filename]
339		if filter.type == Filter.TYPE_INCLUDE:
340			res = applyInclude(res, ptrnList, filter.filename)
341		else:
342			assert filter.type == Filter.TYPE_EXCLUDE
343			res = applyExclude(res, ptrnList, filter.filename)
344	return res
345
346def appendToHierarchy (root, casePath):
347	def findChild (node, name):
348		for child in node.children:
349			if child.name == name:
350				return child
351		return None
352
353	curNode		= root
354	components	= casePath.split('.')
355
356	for component in components[:-1]:
357		nextNode = findChild(curNode, component)
358		if not nextNode:
359			nextNode = TestGroup(component)
360			curNode.children.append(nextNode)
361		curNode = nextNode
362
363	if not findChild(curNode, components[-1]):
364		curNode.children.append(TestCase(components[-1]))
365
366def buildTestHierachy (caseList):
367	root = TestRoot()
368	for case in caseList:
369		appendToHierarchy(root, case)
370	return root
371
372def buildTestCaseMap (root):
373	caseMap = {}
374
375	def recursiveBuild (curNode, prefix):
376		curPath = prefix + curNode.name
377		if isinstance(curNode, TestCase):
378			caseMap[curPath] = curNode
379		else:
380			for child in curNode.children:
381				recursiveBuild(child, curPath + '.')
382
383	for child in root.children:
384		recursiveBuild(child, '')
385
386	return caseMap
387
388def include (filename):
389	return Filter(Filter.TYPE_INCLUDE, filename)
390
391def exclude (filename):
392	return Filter(Filter.TYPE_EXCLUDE, filename)
393
394def insertXMLHeaders (mustpass, doc):
395	if mustpass.project.copyright != None:
396		doc.insert(0, ElementTree.Comment(mustpass.project.copyright))
397	doc.insert(1, ElementTree.Comment(GENERATED_FILE_WARNING))
398
399def prettifyXML (doc):
400	uglyString	= ElementTree.tostring(doc, 'utf-8')
401	reparsed	= minidom.parseString(uglyString)
402	return reparsed.toprettyxml(indent='\t', encoding='utf-8')
403
404def genSpecXML (mustpass):
405	mustpassElem = ElementTree.Element("Mustpass", version = mustpass.version)
406	insertXMLHeaders(mustpass, mustpassElem)
407
408	for package in mustpass.packages:
409		packageElem = ElementTree.SubElement(mustpassElem, "TestPackage", name = package.module.name)
410
411		for config in package.configurations:
412			configElem = ElementTree.SubElement(packageElem, "Configuration",
413												caseListFile	= getCaseListFileName(package, config),
414												commandLine		= getCommandLine(config),
415												name			= config.name)
416
417	return mustpassElem
418
419def addOptionElement (parent, optionName, optionValue):
420	ElementTree.SubElement(parent, "option", name=optionName, value=optionValue)
421
422def genAndroidTestXml (mustpass):
423	RUNNER_CLASS = "com.drawelements.deqp.runner.DeqpTestRunner"
424	configElement = ElementTree.Element("configuration")
425
426	# have the deqp package installed on the device for us
427	preparerElement = ElementTree.SubElement(configElement, "target_preparer")
428	preparerElement.set("class", "com.android.tradefed.targetprep.suite.SuiteApkInstaller")
429	addOptionElement(preparerElement, "cleanup-apks", "true")
430	addOptionElement(preparerElement, "test-file-name", "com.drawelements.deqp.apk")
431
432	# Target preparer for incremental dEQP
433	preparerElement = ElementTree.SubElement(configElement, "target_preparer")
434	preparerElement.set("class", "com.android.compatibility.common.tradefed.targetprep.IncrementalDeqpPreparer")
435	addOptionElement(preparerElement, "disable", "true")
436
437	# add in metadata option for component name
438	ElementTree.SubElement(configElement, "option", name="test-suite-tag", value="cts")
439	ElementTree.SubElement(configElement, "option", key="component", name="config-descriptor:metadata", value="deqp")
440	ElementTree.SubElement(configElement, "option", key="parameter", name="config-descriptor:metadata", value="not_instant_app")
441	ElementTree.SubElement(configElement, "option", key="parameter", name="config-descriptor:metadata", value="multi_abi")
442	ElementTree.SubElement(configElement, "option", key="parameter", name="config-descriptor:metadata", value="secondary_user")
443	ElementTree.SubElement(configElement, "option", key="parameter", name="config-descriptor:metadata", value="no_foldable_states")
444	controllerElement = ElementTree.SubElement(configElement, "object")
445	controllerElement.set("class", "com.android.tradefed.testtype.suite.module.TestFailureModuleController")
446	controllerElement.set("type", "module_controller")
447	addOptionElement(controllerElement, "screenshot-on-failure", "false")
448
449	for package in mustpass.packages:
450		for config in package.configurations:
451			if not config.runByDefault:
452				continue
453
454			testElement = ElementTree.SubElement(configElement, "test")
455			testElement.set("class", RUNNER_CLASS)
456			addOptionElement(testElement, "deqp-package", package.module.name)
457			caseListFile = getCaseListFileName(package,config)
458			addOptionElement(testElement, "deqp-caselist-file", caseListFile)
459			if caseListFile.startswith("gles3"):
460				addOptionElement(testElement, "incremental-deqp-include-file", "gles3-incremental-deqp.txt")
461			elif caseListFile.startswith("vk"):
462				addOptionElement(testElement, "incremental-deqp-include-file", "vk-incremental-deqp.txt")
463			# \todo [2015-10-16 kalle]: Replace with just command line? - requires simplifications in the runner/tests as well.
464			if config.glconfig != None:
465				addOptionElement(testElement, "deqp-gl-config-name", config.glconfig)
466
467			if config.surfacetype != None:
468				addOptionElement(testElement, "deqp-surface-type", config.surfacetype)
469
470			if config.rotation != None:
471				addOptionElement(testElement, "deqp-screen-rotation", config.rotation)
472
473			if config.expectedRuntime != None:
474				addOptionElement(testElement, "runtime-hint", config.expectedRuntime)
475
476			if config.required:
477				addOptionElement(testElement, "deqp-config-required", "true")
478
479	insertXMLHeaders(mustpass, configElement)
480
481	return configElement
482
483def genMustpass (mustpass, moduleCaseDicts):
484	print("Generating mustpass '%s'" % mustpass.version)
485
486	patternLists = readPatternLists(mustpass)
487
488	for package in mustpass.packages:
489		allCasesInPkgDict	= moduleCaseDicts[package.module]
490
491		for config in package.configurations:
492
493			# construct dictionary with all filters applyed
494			filteredCaseDict	= applyFilters(allCasesInPkgDict, patternLists, config.filters)
495
496			# construct components of path to main destination file
497			mainDstFilePath		= getDstCaseListPath(mustpass)
498			mainDstFileName		= getCaseListFileName(package, config)
499			mainDstFile			= os.path.join(mainDstFilePath, mainDstFileName)
500			mainGruopSubDir		= mainDstFileName[:-4]
501
502			# if case paths should be split to multiple files then main
503			# destination file will contain paths to individual files containing cases
504			if len(config.listOfGroupsToSplit) > 0:
505				# make sure directory for group files exists
506				rootGroupPath = os.path.join(mainDstFilePath, mainGruopSubDir)
507				if not os.path.exists(rootGroupPath):
508					os.makedirs(rootGroupPath)
509
510				# iterate over case dictionary and split it to .txt files acording to
511				# groups that were specified in config.listOfGroupsToSplit
512				splitedGroupsDict	= {}
513				dictStack			= [filteredCaseDict]
514				helperListStack		= [ [] ]
515				while True:
516					# when all items from stack were processed then we can exit the loop
517					if len(dictStack) == 0:
518						break
519					assert(len(dictStack) == len(helperListStack))
520					# grab last item from stack
521					itemOnStack = dictStack.pop()
522					caseListFromHelperStack = helperListStack.pop()
523					# if item on stack is dictionary then it represents groups and we need to add them to stack
524					if type(itemOnStack) is dict:
525						for groupName in sorted(itemOnStack):
526
527							# check if this group should be split to multiple .txt files
528							if groupName in config.listOfGroupsToSplit:
529								# we can split only groups that contain other groups,
530								# listOfGroupsToSplit should not contain groups that contain test cases
531								assert(type(itemOnStack[groupName]) is dict)
532								# add child groups of this group to splitedGroupsDict
533								for childGroupName in itemOnStack[groupName]:
534									# make sure that child group should not be splited
535									# (if it should then this will be handle in one of the next iterations)
536									if childGroupName not in config.listOfGroupsToSplit:
537										splitedGroupsDict[childGroupName] = []
538
539							# add this group to stack used for iteration over casses tree
540							dictStack.append(itemOnStack[groupName])
541
542							# decide what list we should append to helperListStack;
543							# if this group represents one of individual .txt files then grab
544							# propper array of cases from splitedGroupsDict and add it to helper stack;
545							# if groupName is not in splitedGroupsDict then use the same list as was used
546							# by parent group (we are merging casses from those groups to single .txt file)
547							helperListStack.append(splitedGroupsDict.get(groupName, caseListFromHelperStack))
548					else:
549						# if item on stack is a list of cases we can add them to proper list
550						assert(type(itemOnStack) is list)
551						caseListFromHelperStack.extend(itemOnStack)
552
553				print("  Writing separated caselists:")
554				groupPathsList = []
555				for groupPath in splitedGroupsDict:
556					# skip groups that after filtering have no casses left
557					if len(splitedGroupsDict[groupPath]) == 0:
558						continue
559					# remove root node name from the beginning of group and replace all '_' with '-'
560					processedGroupPath = groupPath[groupPath.find('.')+1:].replace('_', '-')
561					# split group paths
562					groupList = processedGroupPath.split('.')
563					groupSubDir = '/'
564					# create subdirectories if there is more then one group name in groupList
565					path = rootGroupPath
566					if len(groupList) > 1:
567						for groupName in groupList[:-1]:
568							# make sure directory for group files exists
569							groupSubDir	= groupSubDir + groupName + '/'
570							path		= os.path.join(path, groupName)
571							if not os.path.exists(path):
572								os.makedirs(path)
573					# construct path to .txt file and save all cases
574					groupDstFileName	= groupList[-1] + ".txt"
575					groupDstFileFullDir	= os.path.join(path, groupDstFileName)
576					groupPathsList.append(mainGruopSubDir + groupSubDir + groupDstFileName)
577					print("    " + groupDstFileFullDir)
578					writeFile(groupDstFileFullDir, "\n".join(splitedGroupsDict[groupPath]) + "\n")
579
580				# write file containing names of all group files
581				print("  Writing file containing list of separated case files: " + mainDstFile)
582				groupPathsList.sort()
583				writeFile(mainDstFile, "\n".join(groupPathsList) + "\n")
584			else:
585				# merge all cases to single case list
586				filteredCaseList	= []
587				dictStack			= [filteredCaseDict]
588				while True:
589					# when all items from stack were processed then we can exit the loop
590					if len(dictStack) == 0:
591						break
592					# grab last item from stack
593					itemOnStack = dictStack.pop()
594					# if item on stack is dictionary then it represents groups and we need to add them to stack
595					if type(itemOnStack) is dict:
596						for groupName in itemOnStack.keys():
597							dictStack.append(itemOnStack[groupName])
598					else:
599						# if item on stack is a list of cases we can add them to filteredCaseList
600						assert(type(itemOnStack) is list)
601						filteredCaseList.extend(itemOnStack)
602				# write file containing all cases
603				if len(filteredCaseList) > 0:
604					print("  Writing deqp caselist: " + mainDstFile)
605					writeFile(mainDstFile, "\n".join(filteredCaseList) + "\n")
606
607	specXML = genSpecXML(mustpass)
608	specFilename = os.path.join(mustpass.project.path, mustpass.version, "mustpass.xml")
609
610	print("  Writing spec: " + specFilename)
611	writeFile(specFilename, prettifyXML(specXML).decode())
612
613	# TODO: Which is the best selector mechanism?
614	if (mustpass.version == "master"):
615		androidTestXML		= genAndroidTestXml(mustpass)
616		androidTestFilename	= os.path.join(mustpass.project.path, "AndroidTest.xml")
617
618		print("  Writing AndroidTest.xml: " + androidTestFilename)
619		writeFile(androidTestFilename, prettifyXML(androidTestXML).decode())
620
621	print("Done!")
622
623def genMustpassLists (mustpassLists, generator, buildCfg):
624	moduleCaseDicts = {}
625
626	# Getting case lists involves invoking build, so we want to cache the results
627	for mustpass in mustpassLists:
628		for package in mustpass.packages:
629			if not package.module in moduleCaseDicts:
630				moduleCaseDicts[package.module] = getCaseDict(buildCfg, generator, package.module)
631
632	for mustpass in mustpassLists:
633		genMustpass(mustpass, moduleCaseDicts)
634
635def parseCmdLineArgs ():
636	parser = argparse.ArgumentParser(description = "Build Android CTS mustpass",
637									 formatter_class=argparse.ArgumentDefaultsHelpFormatter)
638	parser.add_argument("-b",
639						"--build-dir",
640						dest="buildDir",
641						default=DEFAULT_BUILD_DIR,
642						help="Temporary build directory")
643	parser.add_argument("-t",
644						"--build-type",
645						dest="buildType",
646						default="Debug",
647						help="Build type")
648	parser.add_argument("-c",
649						"--deqp-target",
650						dest="targetName",
651						default=DEFAULT_TARGET,
652						help="dEQP build target")
653	return parser.parse_args()
654
655def parseBuildConfigFromCmdLineArgs ():
656	args = parseCmdLineArgs()
657	return getBuildConfig(args.buildDir, args.targetName, args.buildType)
658