Source code for pysys.xml.project

#!/usr/bin/env python
# PySys System Test Framework, Copyright (C) 2006-2020 M.B. Grieve

# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.

# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.

# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA

"""
The `Project <pysys.xml.project.Project>` class holds the ``pysysproject.xml`` project configuration, including all 
user-defined project properties. 

"""

__all__ = ['Project'] # Project is the only member we expose/document from this module

import os.path, logging, xml.dom.minidom, collections, codecs, time

from pysys.constants import *
from pysys import __version__
from pysys.utils.loader import import_module
from pysys.utils.logutils import ColorLogFormatter, BaseLogFormatter
from pysys.utils.stringutils import compareVersions
from pysys.utils.fileutils import mkdir
from pysys.utils.pycompat import openfile
from pysys.exceptions import UserError

log = logging.getLogger('pysys.xml.project')

DTD='''
<!DOCTYPE pysysproject [
<!ELEMENT pysysproject (property*, path*, requires-python?, requires-pysys?, runner?, maker?, writers?, default-file-encodings?, formatters?, performance-reporter?), collect-test-output*, project-help >
<!ELEMENT property (#PCDATA)>
<!ELEMENT path (#PCDATA)>
<!ELEMENT requires-python (#PCDATA)>
<!ELEMENT requires-pysys (#PCDATA)>
<!ELEMENT runner (#PCDATA)>
<!ELEMENT performance-reporter (property*)>
<!ELEMENT maker (#PCDATA)>
<!ELEMENT default-file-encodings (default-file-encoding+) >
<!ELEMENT formatters (formatter+) >
<!ELEMENT formatter (property*) >
<!ELEMENT project-help (#PCDATA)>
<!ELEMENT writers (writer+) >
<!ELEMENT writer (property*) >
<!ATTLIST property root CDATA #IMPLIED>
<!ATTLIST property environment CDATA #IMPLIED>
<!ATTLIST property osfamily CDATA #IMPLIED>
<!ATTLIST property file CDATA #IMPLIED>
<!ATTLIST property name CDATA #IMPLIED>
<!ATTLIST property value CDATA #IMPLIED>
<!ATTLIST property default CDATA #IMPLIED>
<!ATTLIST path value CDATA #REQUIRED>
<!ATTLIST path relative CDATA #IMPLIED>
<!ATTLIST runner classname CDATA #REQUIRED>
<!ATTLIST runner module CDATA #REQUIRED>
<!ATTLIST performance-reporter classname CDATA #REQUIRED>
<!ATTLIST performance-reporter module CDATA #REQUIRED>
<!ATTLIST performance-reporter summaryfile CDATA #REQUIRED>
<!ATTLIST maker classname CDATA #REQUIRED>
<!ATTLIST maker module CDATA #REQUIRED>
<!ATTLIST formatter name CDATA #REQUIRED>
<!ATTLIST formatter messagefmt CDATA #IMPLIED>
<!ATTLIST formatter datefmt CDATA #IMPLIED>
<!ATTLIST formatter classname CDATA #IMPLIED>
<!ATTLIST formatter module CDATA #IMPLIED>
<!ATTLIST writer classname CDATA #REQUIRED>
<!ATTLIST writer module CDATA #REQUIRED>
<!ATTLIST writer file CDATA #IMPLIED>
<!ATTLIST default-file-encoding pattern CDATA #REQUIRED>
<!ATTLIST default-file-encoding encoding CDATA #REQUIRED>
<!ATTLIST collect-test-output pattern outputDir outputPattern #REQUIRED>
]>
'''

PROPERTY_EXPAND_ENV = "(?P<replace>\${%s.(?P<key>.*?)})"
PROPERTY_EXPAND = "(?P<replace>\${(?P<key>.*?)})"


class XMLProjectParser(object):
	"""
	:meta private: Not public API. 
	"""
	def __init__(self, dirname, file):
		self.dirname = dirname
		self.xmlfile = os.path.join(dirname, file)
		log.debug('Loading project file: %s', self.xmlfile)
		self.rootdir = 'root'
		self.environment = 'env'
		self.osfamily = 'osfamily'
		
		# project load time is a reasonable proxy for test start time, 
		# and we might want to substitute the date/time into property values
		self.startTimestamp = time.time()
		
		self.properties = {
			self.rootdir:self.dirname, 
			self.osfamily:OSFAMILY, 
			'hostname':HOSTNAME.lower().split('.')[0],
			'startDate':time.strftime('%Y-%m-%d', time.gmtime(self.startTimestamp)),
			'startTime':time.strftime('%H.%M.%S', time.gmtime(self.startTimestamp)),
		}
		
		if not os.path.exists(self.xmlfile):
			raise Exception("Unable to find supplied project file \"%s\"" % self.xmlfile)
		
		try:
			self.doc = xml.dom.minidom.parse(self.xmlfile)
		except Exception:
			raise Exception(sys.exc_info()[1])
		else:
			if self.doc.getElementsByTagName('pysysproject') == []:
				raise Exception("No <pysysproject> element supplied in project file")
			else:
				self.root = self.doc.getElementsByTagName('pysysproject')[0]


	def checkVersions(self):
		requirespython = self.root.getElementsByTagName('requires-python')
		if requirespython and requirespython[0].firstChild: 
			requirespython = requirespython[0].firstChild.nodeValue
			if requirespython:
				if list(sys.version_info) < list(map(int, requirespython.split('.'))):
					raise Exception('This test project requires Python version %s or greater, but this is version %s (from %s)'%(requirespython, '.'.join([str(x) for x in sys.version_info[:3]]), sys.executable))

		requirespysys = self.root.getElementsByTagName('requires-pysys')
		if requirespysys and requirespysys[0].firstChild: 
			requirespysys = requirespysys[0].firstChild.nodeValue
			if requirespysys:
				thisversion = __version__
				if compareVersions(requirespysys, thisversion) > 0:
					raise Exception('This test project requires PySys version %s or greater, but this is version %s'%(requirespysys, thisversion))


	def unlink(self):
		if self.doc: self.doc.unlink()	


	def getProperties(self):
		propertyNodeList = [element for element in self.root.getElementsByTagName('property') if element.parentNode == self.root]

		for propertyNode in propertyNodeList:
			if propertyNode.hasAttribute("environment"):
				self.environment = propertyNode.getAttribute("environment")

			elif propertyNode.hasAttribute("root"):
				self.properties.pop(self.rootdir, "")
				self.rootdir = propertyNode.getAttribute("root")
				self.properties[self.rootdir] = self.dirname
				log.debug('Setting project property %s="%s"', self.rootdir, self.dirname)

			elif propertyNode.hasAttribute("osfamily"):
				self.properties.pop(self.osfamily, "")
				self.osfamily = propertyNode.getAttribute("osfamily")
				self.properties[self.osfamily] = OSFAMILY
				log.debug('Setting project property %s="%s"', self.osfamily, OSFAMILY)
					
			elif propertyNode.hasAttribute("file"): 
				file = self.expandFromProperty(propertyNode.getAttribute("file"), propertyNode.getAttribute("default"))
				self.getPropertiesFromFile(os.path.normpath(os.path.join(self.dirname, file)) if file else '', 
					pathMustExist=(propertyNode.getAttribute("pathMustExist") or '').lower()=='true')
			
			elif propertyNode.hasAttribute("name"):
				name = propertyNode.getAttribute("name") 
				value = self.expandFromEnvironent(propertyNode.getAttribute("value"), propertyNode.getAttribute("default"))
				self.properties[name] = value = self.expandFromProperty(value, propertyNode.getAttribute("default"))
				log.debug('Setting project property %s="%s"', name, value)

				if (propertyNode.getAttribute("pathMustExist") or '').lower()=='true':
					if not (value and os.path.exists(os.path.join(self.dirname, value))):
						raise UserError('Cannot find path referenced in project property "%s": "%s"'%(
							name, '' if not value else os.path.normpath(os.path.join(self.dirname, value))))

		return self.properties


	def getPropertiesFromFile(self, file, pathMustExist=False):
		if not os.path.isfile(file):
			if pathMustExist:
				raise UserError('Cannot find properties file referenced in %s: "%s"'%(
					self.xmlfile, file))

			log.debug('Skipping project properties file which not exist: "%s"', file)
			return

		with open(file, 'r') as fp:
			for line in fp:
				line = line.split('=', 1)
				if len(line) == 2:
					name, value = line[0], line[1]
					value = self.expandFromProperty(value, "")	
					name = name.strip()
					value = value.strip()
					self.properties[name] = value
					log.debug('Setting project property %s="%s" (from %s)', name, self.properties[name], file)


	def expandFromEnvironent(self, value, default):
		regex = re.compile(PROPERTY_EXPAND_ENV%self.environment, re.M)
		while regex.search(value) is not None:
			matches = regex.findall(value)				
			for m in matches:
				try:
					insert = os.environ[m[1]]
				except Exception:
					# this means that if the default also contains something that can't be resolved we get a hard failure 
					# (otherwise would stack overflow)
					if default==value:
						raise Exception('Cannot expand default property value "%s": cannot resolve %s'%(default or value, m[1]))
					log.debug('Failed to expand property from environment variables; "%s" env var does not exist so using default "%s"', m[1], default)

					value = default
					break
				value = value.replace(m[0], insert)
				log.debug('Expanding project property from environment: %s->"%s"', m[0], insert)

		return value		


	def expandFromProperty(self, value, default):
		regex = re.compile(PROPERTY_EXPAND, re.M)
		while regex.search(value) is not None:
			matches = regex.findall(value)
			for m in matches:
				try:
					insert = self.properties[m[1]]
				except Exception as e:
					# this means that if the default also contains something that can't be resolved we get a hard failure 
					# (otherwise would stack overflow)
					if default==value:
						raise Exception('Cannot expand default property value "%s": cannot resolve %s'%(default or value, m[1]))
					log.debug('Failed to expand property %s in "%s" (will use default "%s") - %s: %s', m[1], value, default, e.__class__.__name__, e)
					value = default
					break
				value = value.replace(m[0], insert)
		return value


	def getRunnerDetails(self):
		try:
			runnerNodeList = self.root.getElementsByTagName('runner')[0]
			return [runnerNodeList.getAttribute('classname'), runnerNodeList.getAttribute('module')]
		except Exception:
			return DEFAULT_RUNNER


	def getCollectTestOutputDetails(self):
		r = []
		for n in self.root.getElementsByTagName('collect-test-output'):
			x = {
				'pattern':n.getAttribute('pattern'),
				'outputDir':self.expandFromProperty(n.getAttribute('outputDir'), n.getAttribute('outputDir')),
				'outputPattern':n.getAttribute('outputPattern'),
			}
			assert 'pattern' in x, x
			assert 'outputDir' in x, x
			assert 'outputPattern' in x, x
			assert '@UNIQUE@' in x['outputPattern'], 'collect-test-output outputPattern must include @UNIQUE@'
			r.append(x)
		return r


	def getPerformanceReporterDetails(self):
		nodeList = self.root.getElementsByTagName('performance-reporter')
		cls, optionsDict = self._parseClassAndConfigDict(nodeList[0] if nodeList else None, 'pysys.utils.perfreporter.CSVPerformanceReporter')
			
		summaryfile = optionsDict.pop('summaryfile', '')
		summaryfile = self.expandFromProperty(summaryfile, summaryfile)
		if optionsDict: raise Exception('Unexpected performancereporter attribute(s): '+', '.join(list(optionsDict.keys())))
		
		return cls, summaryfile

	def getProjectHelp(self):
		help = ''
		for e in self.root.getElementsByTagName('project-help'):
			for n in e.childNodes:
				if (n.nodeType in {e.TEXT_NODE,e.CDATA_SECTION_NODE}) and n.data:
					help += n.data
		return help

	def getDescriptorLoaderClass(self):
		nodeList = self.root.getElementsByTagName('descriptor-loader')
		cls, optionsDict = self._parseClassAndConfigDict(nodeList[0] if nodeList else None, 'pysys.xml.descriptor.DescriptorLoader')
		
		if optionsDict: raise Exception('Unexpected descriptor-loader attribute(s): '+', '.join(list(optionsDict.keys())))
		
		return cls

	def getMakerDetails(self):
		try:
			makerNodeList = self.root.getElementsByTagName('maker')[0]
			return [makerNodeList.getAttribute('classname'), makerNodeList.getAttribute('module')]
		except Exception:
			return DEFAULT_MAKER


	def createFormatters(self):
		stdout = runlog = None
		
		formattersNodeList = self.root.getElementsByTagName('formatters')
		if formattersNodeList:
			formattersNodeList = formattersNodeList[0].getElementsByTagName('formatter')
		if formattersNodeList:
			for formatterNode in formattersNodeList:
				fname = formatterNode.getAttribute('name')
				if fname not in ['stdout', 'runlog']:
					raise Exception('Formatter "%s" is invalid - must be stdout or runlog'%fname)

				if fname == 'stdout':
					cls, options = self._parseClassAndConfigDict(formatterNode, 'pysys.utils.logutils.ColorLogFormatter')
					stdout = cls(options)
				else:
					cls, options = self._parseClassAndConfigDict(formatterNode, 'pysys.utils.logutils.BaseLogFormatter')
					runlog = cls(options)
		return stdout, runlog

	def getDefaultFileEncodings(self):
		result = []
		for n in self.root.getElementsByTagName('default-file-encoding'):
			pattern = (n.getAttribute('pattern') or '').strip().replace('\\','/')
			encoding = (n.getAttribute('encoding') or '').strip()
			if not pattern: raise Exception('<default-file-encoding> element must include both a pattern= attribute')
			if encoding: 
				codecs.lookup(encoding) # give an exception if an invalid encoding is specified
			else:
				encoding=None
			result.append({'pattern':pattern, 'encoding':encoding})
		return result

	def getExecutionOrderHints(self):
		result = []
		secondaryModesHintDelta = None
		
		def makeregex(s):
			if not s: return None
			if s.startswith('!'): raise UserError('Exclusions such as !xxx are not permitted in execution-order configuration')
			
			# make a regex that will match either the entire expression as a literal 
			# or the entire expression as a regex
			s = s.rstrip('$')
			try:
				#return re.compile('(%s|%s)$'%(re.escape(s), s))
				return re.compile('%s$'%(s))
			except Exception as ex:
				raise UserError('Invalid regular expression in execution-order "%s": %s'%(s, ex))
		
		for parent in self.root.getElementsByTagName('execution-order'):
			if parent.getAttribute('secondaryModesHintDelta'):
				secondaryModesHintDelta = float(parent.getAttribute('secondaryModesHintDelta'))
			for n in parent.getElementsByTagName('execution-order'):
				moderegex = makeregex(n.getAttribute('forMode'))
				groupregex = makeregex(n.getAttribute('forGroup'))
				if not (moderegex or groupregex): raise UserError('Must specify either forMode, forGroup or both')
				
				hintmatcher = lambda groups, mode, moderegex=moderegex, groupregex=groupregex: (
					(moderegex is None or moderegex.match(mode or '')) and
					(groupregex is None or any(groupregex.match(group) for group in groups))
					)
				
				result.append( 
					(float(n.getAttribute('hint')), hintmatcher )
					)
		if secondaryModesHintDelta is None: 
			secondaryModesHintDelta = +100.0 # default value
		return result, secondaryModesHintDelta

	def getWriterDetails(self):
		writersNodeList = self.root.getElementsByTagName('writers')
		if writersNodeList == []: return [DEFAULT_WRITER]
		
		try:
			writers = []
			writerNodeList = writersNodeList[0].getElementsByTagName('writer')
			if writerNodeList != []:
				for writerNode in writerNodeList:
					try:
						file = writerNode.getAttribute('file') if writerNode.hasAttribute('file') else None
						writer = [writerNode.getAttribute('classname'), writerNode.getAttribute('module'), file, {}]
					except Exception:
						pass
					else:
						propertyNodeList = writerNode.getElementsByTagName('property')
						for propertyNode in propertyNodeList:
							try:
								name = propertyNode.getAttribute("name") 
								value = self.expandFromEnvironent(propertyNode.getAttribute("value"), propertyNode.getAttribute("default"))
								writer[3][name] = self.expandFromProperty(value, propertyNode.getAttribute("default"))
							except Exception:
								pass
						writers.append(writer)				
			else:
				writers.append(DEFAULT_WRITER)
			return writers
		except Exception:
			return [DEFAULT_WRITER]
		

	def addToPath(self):		
		for elementname in ['path', 'pythonpath']:
			pathNodeList = self.root.getElementsByTagName(elementname)

			for pathNode in pathNodeList:
					raw = self.expandFromEnvironent(pathNode.getAttribute("value"), "")
					value = self.expandFromProperty(raw, "")
					relative = pathNode.getAttribute("relative")
					if not value: 
						log.warn('Cannot add directory to the python <path>: "%s"', raw)
						continue

					if relative == "true": value = os.path.join(self.dirname, value)
					value = os.path.normpath(value)
					if not os.path.isdir(value): 
						log.warn('Cannot add non-existent directory to the python <path>: "%s"', value)
					else:
						log.debug('Adding value to path ')
						sys.path.append(value)


	def writeXml(self):
		f = open(self.xmlfile, 'w')
		f.write(self.doc.toxml())
		f.close()


	def _parseClassAndConfigDict(self, node, defaultClass):
		"""Parses a dictionary of arbitrary options and a python class out of the specified XML node.

		The node may optionally contain classname and module (if not specified as a separate attribute,
		module will be extracted from the first part of classname); any other attributes will be returned in
		the optionsDict, as will <option name=""></option> child elements.

		:param node: The node, may be None
		:param defaultClass: a string specifying the default fully-qualified class
		:return: a tuple of (pythonclassconstructor, propertiesdict)
		"""
		optionsDict = {}
		if node:
			for att in range(node.attributes.length):
				value = self.expandFromEnvironent(node.attributes.item(att).value, None)
				optionsDict[node.attributes.item(att).name] = self.expandFromProperty(value, None)
			for tag in node.getElementsByTagName('property'):
				assert tag.getAttribute('name')
				value = self.expandFromEnvironent(tag.getAttribute("value"), tag.getAttribute("default"))
				optionsDict[tag.getAttribute('name')] = self.expandFromProperty(value, tag.getAttribute("default"))

		classname = optionsDict.pop('classname', defaultClass)
		mod = optionsDict.pop('module', '.'.join(classname.split('.')[:-1]))
		classname = classname.split('.')[-1]

		# defer importing the module until we actually need to instantiate the 
		# class, to avoid introducing tricky module import order problems, given 
		# that the project itself needs loading very early
		def classConstructor(*args, **kwargs):
			module = import_module(mod, sys.path)
			cls = getattr(module, classname)
			return cls(*args, **kwargs) # invoke the constructor for this class
		return classConstructor, optionsDict

def getProjectConfigTemplates():
	"""Get a list of available templates that can be used for creating a new project configuration. 
	
	:return: A dict, where each value is an absolute path to an XML template file 
		and each key is the display name for that template. 
	"""
	templatedir = os.path.dirname(__file__)+'/templates/project'
	templates = { t.replace('.xml',''): templatedir+'/'+t 
		for t in os.listdir(templatedir) if t.endswith('.xml')}
	assert templates, 'No project templates found in %s'%templatedir
	return templates

def createProjectConfig(targetdir, templatepath=None):
	"""Create a new project configuration file in the specified targetdir. 
	"""
	if not templatepath: templatepath = getProjectConfigTemplates()['default']
	mkdir(targetdir)
	# using ascii ensures we don't unintentionally add weird characters to the default (utf-8) file
	with openfile(templatepath, encoding='ascii') as src:
		with openfile(os.path.abspath(targetdir+'/'+DEFAULT_PROJECTFILE[0]), 'w', encoding='ascii') as target:
			for l in src:
				l = l.replace('@PYTHON_VERSION@', '%s.%s.%s'%sys.version_info[0:3])
				l = l.replace('@PYSYS_VERSION@', '.'.join(__version__.split('.')[0:3]))
				target.write(l)

[docs]class Project(object): """Contains settings for the entire test project, as defined by the ``pysysproject.xml`` project configuration file. To get a reference to the current `Project` instance, use the `pysys.basetest.BaseTest.project` (or `pysys.process.user.ProcessUser.project`) field. This class reads and parses the PySys project file if it exists and sets an instance attribute for every:: <property name="PROP_NAME">prop value</property> element in the file. :ivar dict(str,str) ~.properties: The resolved values of all project properties defined in the configuration file. In addition, each of these is set as an attribute onto the `Project` instance itself. :ivar str ~.root: Full path to the project root directory, as specified by the first PySys project file encountered when walking up the directory tree from the start directory. If no project file was found, this is just the start directory PySys was run from. :ivar str ~.projectFile: Full path to the project file. """ __INSTANCE = None def __init__(self, root, projectFile): self.root = root self.startTimestamp = time.time() self.runnerClassname, self.runnerModule = DEFAULT_RUNNER self.makerClassname, self.makerModule = DEFAULT_MAKER self.writers = [DEFAULT_WRITER] self.perfReporterConfig = None self.defaultFileEncodings = [] # ordered list where each item is a dictionary with pattern and encoding; first matching item wins self.collectTestOutput = [] self.projectHelp = None self.properties = {} stdoutformatter, runlogformatter = None, None self.projectFile = None if projectFile is not None: if not os.path.exists(os.path.join(root, projectFile)): raise Exception("Project file not found: %s" % os.path.normpath(os.path.join(root, projectFile))) from pysys.xml.project import XMLProjectParser try: parser = XMLProjectParser(root, projectFile) except Exception as e: raise Exception("Error parsing project file \"%s\": %s" % (os.path.join(root, projectFile),sys.exc_info()[1])) else: parser.checkVersions() self.projectFile = os.path.join(root, projectFile) self.startTimestamp = parser.startTimestamp # get the properties properties = parser.getProperties() keys = list(properties.keys()) keys.sort() for key in keys: setattr(self, key, properties[key]) self.properties = dict(properties) # add to the python path parser.addToPath() # get the runner if specified self.runnerClassname, self.runnerModule = parser.getRunnerDetails() # get the maker if specified self.makerClassname, self.makerModule = parser.getMakerDetails() # get the loggers to use self.writers = parser.getWriterDetails() self.perfReporterConfig = parser.getPerformanceReporterDetails() self.descriptorLoaderClass = parser.getDescriptorLoaderClass() # get the stdout and runlog formatters stdoutformatter, runlogformatter = parser.createFormatters() self.defaultFileEncodings = parser.getDefaultFileEncodings() self.executionOrderHints, self.executionOrderSecondaryModesHintDelta = parser.getExecutionOrderHints() self.collectTestOutput = parser.getCollectTestOutputDetails() self.projectHelp = parser.getProjectHelp() def expandProperty(m): m = m.group(1) if m == '$': return '$' return properties[m] # expand ${...} property in project help self.projectHelp = re.sub(r'[$][{]([^}]+)[}]', expandProperty, self.projectHelp) # set the data attributes parser.unlink() if not stdoutformatter: stdoutformatter = ColorLogFormatter({}) if not runlogformatter: runlogformatter = BaseLogFormatter({}) PySysFormatters = collections.namedtuple('PySysFormatters', ['stdout', 'runlog']) self.formatters = PySysFormatters(stdoutformatter, runlogformatter)
[docs] @staticmethod def getInstance(): """ Provides access to the singleton instance of Project. Raises an exception if the project has not yet been loaded. Use ``self.project`` to get access to the project instance where possible, for example from a `pysys.basetest.BaseTest` or `pysys.baserunner.BaseRunner` class. This attribute is for use in internal functions and classes that do not have a ``self.project``. """ if Project.__INSTANCE: return Project.__INSTANCE if 'doctest' in sys.argv[0]: return None # special-case for doctesting raise Exception('Cannot call Project.getInstance() as the project has not been loaded yet')
[docs] @staticmethod def findAndLoadProject(startdir): """Find and load a project file, starting from the specified directory. If this fails an error is logged and the process is terminated. The method walks up the directory tree from the supplied path until the PySys project file is found. The location of the project file defines the project root location. The contents of the project file determine project specific constants as specified by property elements in the xml project file. To ensure that all loaded modules have a pre-initialised projects instance, any launching application should first import the loadproject file, and then make a call to it prior to importing all names within the constants module. :param startdir: The initial path to start from when trying to locate the project file """ projectFile = os.getenv('PYSYS_PROJECTFILE', None) search = startdir if not projectFile: projectFileSet = set(DEFAULT_PROJECTFILE) drive, path = os.path.splitdrive(search) while (not search == drive): intersection = projectFileSet & set(os.listdir(search)) if intersection : projectFile = intersection.pop() break else: search, drop = os.path.split(search) if not drop: search = drive if not (projectFile is not None and os.path.exists(os.path.join(search, projectFile))): # pragma: no cover if os.getenv('PYSYS_PERMIT_NO_PROJECTFILE','').lower()=='true': sys.stderr.write("WARNING: No project file found; using default settings and taking project root to be '%s' \n" % (search or '.')) else: sys.stderr.write('\n'.join([ # | "WARNING: No PySys test project file exists in this directory (or its parents):", " - If you wish to start a new project, begin by running 'pysys makeproject'.", " - If you are trying to use an existing project, change directory to a ", " location under the root test directory that contains your project file.", " - If you wish to use an existing project that has no configuration file, ", " set the PYSYS_PERMIT_NO_PROJECTFILE=true environment variable.", "" ])) sys.exit(1) try: project = Project(search, projectFile) stdoutHandler.setFormatter(project.formatters.stdout) Project.__INSTANCE = project # set singleton return project except UserError as e: sys.stderr.write("ERROR: Failed to load project - %s"%e) sys.exit(1) except Exception as e: sys.stderr.write("ERROR: Failed to load project due to %s - %s\n"%(e.__class__.__name__, e)) traceback.print_exc() sys.exit(1)