Esempio n. 1
0
class PackageManager:
    """
    Main interface for Modest Package Manager
    """
    _config = Inject('Config')
    _varMgr = Inject('VarManager')
    _log = Inject('Logger')
    _sys = Inject('SystemHelper')
    _unityHelper = Inject('UnityHelper')
    _junctionHelper = Inject('JunctionHelper')
    _projectInitHandlers = InjectMany('ProjectInitHandlers')
    _schemaLoader = Inject('ProjectSchemaLoader')
    _commonSettings = Inject('CommonSettings')

    def projectExists(self, projectName):
        return self._sys.directoryExists('[UnityProjectsDir]/{0}'.format(projectName))

    def listAllProjects(self):
        projectNames = self.getAllProjectNames()

        defaultProj = self._config.tryGetString(None, 'DefaultProject')

        self._log.info("Found {0} Projects:".format(len(projectNames)))
        for proj in projectNames:
            alias = self.tryGetAliasFromFullName(proj)
            output = proj
            if alias:
                output = "{0} ({1})".format(output, alias)

            if defaultProj == proj:
                output += " (default)"

            self._log.info("  " + output)

    def listAllPackages(self, projectName):
        packagesNames = self.getAllPackageNames(projectName)
        self._log.info("Found {0} Packages:".format(len(packagesNames)))
        for packageName in packagesNames:
            self._log.info("  " + packageName)

    def _findSourceControl(self):
        for dirPath in self._sys.getParentDirectoriesWithSelf('[ConfigDir]'):
            if self._sys.directoryExists(os.path.join(dirPath, '.git')):
                return SourceControlTypes.Git

            if self._sys.directoryExists(os.path.join(dirPath, '.svn')):
                return SourceControlTypes.Subversion

        return None

    def createProject(self, projName, settingsProject = None):
        with self._log.heading('Initializing new project "{0}"', projName):
            projDirPath = self._varMgr.expand('[UnityProjectsDir]/{0}'.format(projName))
            assertThat(not self._sys.directoryExists(projDirPath), "Cannot initialize new project '{0}', found existing project at '{1}'", projName, projDirPath)

            self._sys.createDirectory(projDirPath)

            if settingsProject == None:
                settingsPath = '[ProjectRoot]/ProjectSettings'
                newProjSettingsDir = os.path.join(projDirPath, 'ProjectSettings')

                if self._varMgr.hasKey('DefaultProjectSettingsDir') and self._sys.directoryExists('[DefaultProjectSettingsDir]'):
                    self._sys.copyDirectory('[DefaultProjectSettingsDir]', newProjSettingsDir)
                else:
                    self._sys.createDirectory(newProjSettingsDir)
            else:
                settingsPath = '[ProjectRoot]/../{0}/ProjectSettings'.format(settingsProject)

            with self._sys.openOutputFile(os.path.join(projDirPath, ProjectConfigFileName)) as outFile:
                outFile.write(
"""
ProjectSettingsPath: '{0}'
#AssetsFolder:
    # Uncomment and Add package names here
""".format(settingsPath))

            self.updateProjectJunctions(projName, Platforms.Windows)

    def getProjectFromAlias(self, alias):
        result = self.tryGetProjectFromAlias(alias)
        assertThat(result, "Unrecognized project '{0}' and could not find an alias with that name either".format(alias))
        return result

    def tryGetProjectFromAlias(self, alias):
        aliasMap = self._config.tryGetDictionary({}, 'ProjectAliases')

        if alias not in aliasMap.keys():
            return None

        return aliasMap[alias]

    def tryGetAliasFromFullName(self, name):
        aliasMap = self._config.tryGetDictionary({}, 'ProjectAliases')

        for pair in aliasMap.items():
            if pair[1] == name:
                return pair[0]

        return None

    def _validateDirForFolderType(self, packageInfo, sourceDir):
        if packageInfo.folderType == FolderTypes.AndroidProject:
            assertThat(os.path.exists(os.path.join(sourceDir, "project.properties")), "Project '{0}' is marked with foldertype AndroidProject and therefore must contain a project.properties file".format(packageInfo.name))

    def updateProjectJunctions(self, projectName, platform):
        """
        Initialize all the folder links for the given project
        """

        with self._log.heading('Updating package directories for project {0}'.format(projectName)):
            self.checkProjectInitialized(projectName, platform)
            self.setPathsForProjectPlatform(projectName, platform)
            schema = self._schemaLoader.loadSchema(projectName, platform)
            self._updateDirLinksForSchema(schema)

            self._checkForVersionControlIgnore()

            self._log.good('Finished updating packages for project "{0}"'.format(schema.name))

    def _checkForVersionControlIgnore(self):
        sourceControlType = self._findSourceControl()

        if sourceControlType == SourceControlTypes.Git:
            self._log.info('Detected git repository.  Making sure generated project folders are ignored by git...')
            if not self._sys.fileExists('[ProjectRoot]/.gitignore'):
                self._sys.copyFile('[ProjectRootGitIgnoreTemplate]', '[ProjectRoot]/.gitignore')
                self._log.warn('Added new git ignore file to project root')
        elif sourceControlType == SourceControlTypes.Subversion:
            self._log.info('Detected subversion repository. Making sure generated project folders are ignored by SVN...')
            try:
                self._sys.executeAndWait('svn propset svn:ignore -F [ProjectRootSvnIgnoreTemplate] .', '[ProjectRoot]')
            except Exception as e:
                self._log.warn("Warning: Failed to add generated project directories to SVN ignore!  This may be caused by 'svn' not being available on the command line.  Details: " + str(e))
        #else:
            #self._log.warn('Warning: Could not determine source control in use!  An ignore file will not be added for your project.')

    def getAllPackageFolderInfos(self, projectName):
        folderInfos = []

        self.setPathsForProject(projectName)
        projConfig = self._schemaLoader.loadProjectConfig(projectName)

        for packageFolder in projConfig.packageFolders:
            folderInfo = PackageFolderInfo()
            folderInfo.path = packageFolder

            if self._sys.directoryExists(packageFolder):
                for packageName in self._sys.walkDir(packageFolder):
                    packageDirPath = os.path.join(packageFolder, packageName)

                    if not self._sys.IsDir(packageDirPath):
                        continue

                    installInfoFilePath = os.path.join(packageDirPath, InstallInfoFileName)

                    packageInfo = PackageInfo()
                    packageInfo.name = packageName

                    if self._sys.fileExists(installInfoFilePath):
                        installInfo = YamlSerializer.deserialize(self._sys.readFileAsText(installInfoFilePath))
                        packageInfo.installInfo = installInfo

                    folderInfo.packages.append(packageInfo)

            folderInfos.append(folderInfo)

        return folderInfos

    def deleteProject(self, projName):
        with self._log.heading("Deleting project '{0}'", projName):
            assertThat(self._varMgr.hasKey('UnityProjectsDir'), "Could not find 'UnityProjectsDir' in PathVars.  Have you set up your {0} file?", ConfigFileName)
            fullPath = '[UnityProjectsDir]/{0}'.format(projName)

            assertThat(self._sys.directoryExists(fullPath), "Could not find project with name '{0}' - delete failed", projName)

            self.clearProjectGeneratedFiles(projName)
            self._sys.deleteDirectory(fullPath)

    def getAllPackageNames(self, projectName):
        results = []
        self.setPathsForProject(projectName)
        projConfig = self._schemaLoader.loadProjectConfig(projectName)

        for packageFolder in projConfig.packageFolders:
            if not self._sys.directoryExists(packageFolder):
                continue

            for name in self._sys.walkDir(packageFolder):
                if self._sys.IsDir(os.path.join(packageFolder, name)):
                    results.append(name)

        return results

    def getAllProjectNames(self):
        assertThat(self._varMgr.hasKey('UnityProjectsDir'), "Could not find 'UnityProjectsDir' in PathVars.  Have you set up your {0} file?", ConfigFileName)

        results = []
        for name in self._sys.walkDir('[UnityProjectsDir]'):
            if self._sys.IsDir('[UnityProjectsDir]/' + name):
                results.append(name)
        return results

    # This will set up all the directory junctions for all projects for all platforms
    def updateLinksForAllProjects(self):
        for projectName in self.getAllProjectNames():
            with self._log.heading('Initializing project "{0}"'.format(projectName)):
                try:
                    #for platform in Platforms.All:
                    for platform in [Platforms.Windows]:
                        self.updateProjectJunctions(projectName, platform)

                    self._log.good('Successfully initialized project "{0}"'.format(projectName))
                except Exception as e:
                    self._log.warn('Failed to initialize project "{0}": {1}'.format(projectName, e))

    def _createSwitchProjectMenuScript(self, currentProjName, outputPath):

        foundCurrent = False
        menuFile = """
using UnityEditor;
using Projeny.Internal;

namespace Projeny
{
    public static class ProjenyChangeProjectMenu
    {"""
        projIndex = 1
        for projName in self.getAllProjectNames():
            menuFile += """
        [MenuItem("Projeny/Change Project/{0}", false, 8)]""".format(projName)

            menuFile += """
        public static void ChangeProject{0}()""".format(projIndex)

            menuFile += """
        {"""

            menuFile += """
            PrjHelper.ChangeProject("{0}");""".format(projName)

            menuFile += """
        }
"""
            if projName == currentProjName:
                assertThat(not foundCurrent)
                foundCurrent = True
                menuFile += """
        [MenuItem("Projeny/Change Project/{0}", true, 8)]""".format(currentProjName)
                menuFile += """
        public static bool ChangeProject{0}Validate()""".format(projIndex)
                menuFile += """
        {
            return false;
        }"""

            projIndex += 1

        menuFile += """
    }
}
"""
        #assertThat(foundCurrent, "Could not find project " + currentProjName)
        self._sys.writeFileAsText(outputPath, menuFile)

    def _addGeneratedProjenyFiles(self, outDir, schema):
        menuFileOutPath = outDir + '/Editor/ProjenyChangeProjectMenu.cs'
        placeholderOutPath1 = outDir + '/Placeholder.cs'
        placeholderOutPath2 = outDir + '/Editor/Placeholder.cs'

        # Need to always use the same meta files to avoid having unity do a refresh
        self._createSwitchProjectMenuScript(schema.name, menuFileOutPath)
        self._sys.copyFile('[ProjenyChangeProjectMenuMeta]', menuFileOutPath + ".meta")

        self._sys.copyFile('[PlaceholderFile1]', placeholderOutPath1)
        self._sys.copyFile('[PlaceholderFile1].meta', placeholderOutPath1 + ".meta")

        self._sys.copyFile('[PlaceholderFile2]', placeholderOutPath2)
        self._sys.copyFile('[PlaceholderFile2].meta', placeholderOutPath2 + ".meta")

    def _updateDirLinksForSchema(self, schema):
        self._removeProjectPlatformJunctions()

        self._sys.deleteDirectoryIfExists('[PluginsDir]/Projeny')

        # Define DoNotIncludeProjenyInUnityProject only if you want to include Projeny as just another prebuilt package
        # This is nice because then you can call methods on projeny from another package
        if self._config.tryGetBool(False, 'DoNotIncludeProjenyInUnityProject'):
            self._addGeneratedProjenyFiles('[PluginsDir]/ProjenyGenerated', schema)
        else:
            dllOutPath = '[PluginsDir]/Projeny/Editor/Projeny.dll'

            self._sys.copyFile('[ProjenyUnityEditorDllPath]', dllOutPath)
            self._sys.copyFile('[ProjenyUnityEditorDllMetaFilePath]', dllOutPath + '.meta')

            self._sys.copyFile('[YamlDotNetDllPath]', '[PluginsDir]/Projeny/Editor/YamlDotNet.dll')

            self._sys.copyDirectory('[ProjenyUnityEditorAssetsDirPath]', '[PluginsDir]/Projeny/Editor/Assets')

            self._addGeneratedProjenyFiles('[PluginsDir]/Projeny', schema)

        self._junctionHelper.makeJunction(schema.projectSettingsPath, '[ProjectPlatformRoot]/ProjectSettings')

        for packageInfo in schema.packages.values():

            self._log.debug('Processing package "{0}"'.format(packageInfo.name))

            self._validateDirForFolderType(packageInfo, packageInfo.dirPath)

            assertThat(os.path.exists(packageInfo.dirPath),
               "Could not find package with name '{0}' while processing schema '{1}'.  See build log for full object graph to see where it is referenced".format(packageInfo.name, schema.name))

            outputPackageDir = self._varMgr.expandPath(packageInfo.outputDirVar)

            linkDir = os.path.join(outputPackageDir, packageInfo.name)

            assertThat(not os.path.exists(linkDir), "Did not expect this path to exist: '{0}'".format(linkDir))

            self._junctionHelper.makeJunction(packageInfo.dirPath, linkDir)

    def checkProjectInitialized(self, projectName, platform):
        self.setPathsForProjectPlatform(projectName, platform)

        if self._sys.directoryExists('[ProjectPlatformRoot]'):
            return

        self._log.warn('Project "{0}" is not initialized for platform "{1}".  Initializing now.'.format(projectName, platform))
        self._initNewProjectForPlatform(projectName, platform)

    def setPathsForProject(self, projectName):
        self._varMgr.set('ShortProjectName', self._commonSettings.getShortProjectName(projectName))
        self._varMgr.set('ProjectName', projectName)
        self._varMgr.set('ProjectRoot', '[UnityProjectsDir]/[ProjectName]')

    def setPathsForProjectPlatform(self, projectName, platform):

        self.setPathsForProject(projectName)

        self._varMgr.set('ShortPlatform', PlatformUtil.toPlatformFolderName(platform))

        self._varMgr.set('Platform', platform)

        self._varMgr.set('ProjectPlatformRoot', '[ProjectRoot]/[ShortProjectName]-[ShortPlatform]')
        self._varMgr.set('ProjectAssetsDir', '[ProjectPlatformRoot]/Assets')

        self._varMgr.set('UnityGeneratedProjectEditorPath', '[ProjectPlatformRoot]/[ShortProjectName]-[ShortPlatform].CSharp.Editor.Plugins.csproj')
        self._varMgr.set('UnityGeneratedProjectPath', '[ProjectPlatformRoot]/[ShortProjectName]-[ShortPlatform].CSharp.Plugins.csproj')

        # For reasons I don't understand, the unity generated project is named with 'Assembly' on some machines and not other
        # Problem due to unity version but for now just allow either or
        self._varMgr.set('UnityGeneratedProjectEditorPath2', '[ProjectPlatformRoot]/Assembly-CSharp-Editor-firstpass.csproj')
        self._varMgr.set('UnityGeneratedProjectPath2', '[ProjectPlatformRoot]/Assembly-CSharp-firstpass.csproj')

        self._varMgr.set('PluginsDir', '[ProjectAssetsDir]/Plugins')
        self._varMgr.set('PluginsAndroidDir', '[PluginsDir]/Android')
        self._varMgr.set('PluginsAndroidLibraryDir', '[PluginsDir]/Android/libs')
        self._varMgr.set('PluginsIosLibraryDir', '[PluginsDir]/iOS')
        self._varMgr.set('PluginsWebGlLibraryDir', '[PluginsDir]/WebGL')

        self._varMgr.set('StreamingAssetsDir', '[ProjectAssetsDir]/StreamingAssets')

        self._varMgr.set('IntermediateFilesDir', '[ProjectPlatformRoot]/obj')

        self._varMgr.set('SolutionPath', '[ProjectRoot]/[ProjectName]-[Platform].sln')

    def deleteAllLinks(self):
        with self._log.heading('Deleting all junctions for all projects'):
            projectNames = []
            projectsDir = self._varMgr.expandPath('[UnityProjectsDir]')

            for itemName in os.listdir(projectsDir):
                fullPath = os.path.join(projectsDir, itemName)
                if os.path.isdir(fullPath):
                    projectNames.append(itemName)

            for projectName in projectNames:
                for platform in Platforms.All:
                    self.setPathsForProjectPlatform(projectName, platform)
                    self._removeProjectPlatformJunctions()

    def _removeProjectPlatformJunctions(self):
        self._junctionHelper.removeJunctionsInDirectory('[ProjectPlatformRoot]', True)

    def clearAllProjectGeneratedFiles(self):
        for projName in self.getAllProjectNames():
            self.clearProjectGeneratedFiles(projName)

    def clearProjectGeneratedFiles(self, projectName):
        with self._log.heading('Clearing generated files for project {0}'.format(projectName)):
            self._junctionHelper.removeJunctionsInDirectory('[UnityProjectsDir]/{0}'.format(projectName), True)
            for platform in Platforms.All:
                self.setPathsForProjectPlatform(projectName, platform)

                if os.path.exists(self._varMgr.expandPath('[ProjectPlatformRoot]')):
                    platformRootPath = self._varMgr.expand('[ProjectPlatformRoot]')

                    try:
                        shutil.rmtree(platformRootPath)
                    except:
                        self._log.warn('Unable to remove path {0}.  Trying to kill adb.exe to see if that will help...'.format(platformRootPath))
                        MiscUtil.tryKillAdbExe(self._sys)

                        try:
                            shutil.rmtree(platformRootPath)
                        except:
                            self._log.error('Still unable to remove path {0}!  A running process may have one of the files locked.  Ensure you have closed down unity / visual studio / etc.'.format(platformRootPath))
                            raise

                    self._log.debug('Removed project directory {0}'.format(platformRootPath))
                    self._log.good('Successfully deleted project {0} ({1})'.format(projectName, platform))
                else:
                    self._log.debug('Project {0} ({1}) already deleted'.format(projectName, platform))

                # Remove the solution files and the suo files etc.
                self._sys.removeByRegex('[ProjectRoot]/[ProjectName]-[Platform].*')

    def _initNewProjectForPlatform(self, projectName, platform):

        with self._log.heading('Initializing new project {0} ({1})'.format(projectName, platform)):
            schema = self._schemaLoader.loadSchema(projectName, platform)
            self.setPathsForProjectPlatform(projectName, platform)

            assertThat(self._sys.directoryExists(schema.projectSettingsPath),
               "Expected to find project settings directory at '{0}'", self._varMgr.expand(schema.projectSettingsPath))

            if self._sys.directoryExists('[ProjectPlatformRoot]'):
                raise Exception('Unable to create project "{0}". Directory already exists at path "{1}".'.format(projectName, self._varMgr.expandPath('[ProjectPlatformRoot]')))

            try:
                self._sys.createDirectory('[ProjectPlatformRoot]')

                self._log.debug('Created directory "{0}"'.format(self._varMgr.expandPath('[ProjectPlatformRoot]')))

                self._junctionHelper.makeJunction(schema.projectSettingsPath, '[ProjectPlatformRoot]/ProjectSettings')

                self._updateDirLinksForSchema(schema)

                for handler in self._projectInitHandlers:
                    handler.onProjectInit(projectName, platform)

            except:
                self._log.error("Failed to initialize project '{0}' for platform '{1}'.".format(schema.name, platform))
                raise

            self._log.good('Finished creating new project "{0}" ({1})'.format(schema.name, platform))
Esempio n. 2
0
class Logger:
    _streams = InjectMany('LogStream')
    _config = Inject('Config')

    ''' Simple log class to use with build scripts '''
    def __init__(self):
        self._totalStartTime = None
        self._headingBlocks = []

        self.goodPatterns = self._getPatterns('GoodPatterns')
        self.goodMaps = self._getPatternMaps('GoodPatternMaps')

        self.infoPatterns = self._getPatterns('InfoPatterns')
        self.infoMaps = self._getPatternMaps('InfoPatternMaps')

        self.errorPatterns = self._getPatterns('ErrorPatterns')
        self.errorMaps = self._getPatternMaps('ErrorPatternMaps')

        self.warningPatterns = self._getPatterns('WarningPatterns')
        self.warningMaps = self._getPatternMaps('WarningPatternMaps')
        self.warningPatternsIgnore = self._getPatterns('WarningPatternsIgnore')

        self.debugPatterns = self._getPatterns('DebugPatterns')
        self.debugMaps = self._getPatternMaps('DebugPatternMaps')

    @property
    def totalStartTime(self):
        return self._totalStartTime

    @property
    def hasHeading(self):
        return any(self._headingBlocks)

    def getCurrentNumHeadings(self):
        return len(self._headingBlocks)

    def heading(self, message, *args):

        if not self._totalStartTime:
            self._totalStartTime = datetime.now()

        # Need to format it now so that heading gets the args
        if len(args) > 0:
            message = message.format(*args)

        block = HeadingBlock(self, message)
        self._headingBlocks.append(block)
        return block

    def noise(self, message, *args):
        self._logInternal(message, LogType.Noise, *args)

    def debug(self, message, *args):
        self._logInternal(message, LogType.Debug, *args)

    def info(self, message, *args):
        self._logInternal(message, LogType.Info, *args)

    def error(self, message, *args):

        self._logInternal(message, LogType.Error, *args)

    def warn(self, message, *args):
        self._logInternal(message, LogType.Warn, *args)

    def good(self, message, *args):
        self._logInternal(message, LogType.Good, *args)

    def _logInternal(self, message, logType, *args):

        if len(args) > 0:
            message = message.format(*args)

        newLogType, newMessage = self.classifyMessage(logType, message)

        for stream in self._streams:
            stream.log(newLogType, newMessage)

    def _getPatternMaps(self, settingName):
        maps = self._config.tryGetDictionary({}, 'Log', settingName)

        result = []
        for key, value in maps.items():
            regex = re.compile(key)
            logMap = LogMap(regex, value)
            result.append(logMap)

        return result

    def _getPatterns(self, settingName):
        patternStrings = self._config.tryGetList([], 'Log', settingName)

        result = []
        for pattern in patternStrings:
            result.append(re.compile('.*' + pattern + '.*'))

        return result

    def tryMatchPattern(self, message, maps, patterns):
        for logMap in maps:
            if logMap.regex.match(message):
                return logMap.regex.sub(logMap.sub, message)

        for pattern in patterns:
            match = pattern.match(message)

            if match:
                groups = match.groups()

                if len(groups) > 0:
                    return groups[0]

                return message

        return None

    def classifyMessage(self, logType, message):

        if logType != LogType.Noise:
            # If it is explicitly logged as something by calling for eg. log.info, use info type
            return logType, message

        parsedMessage = self.tryMatchPattern(message, self.errorMaps, self.errorPatterns)
        if parsedMessage:
            return LogType.Error, parsedMessage

        if not any(p.match(message) for p in self.warningPatternsIgnore):
            parsedMessage = self.tryMatchPattern(message, self.warningMaps, self.warningPatterns)
            if parsedMessage:
                return LogType.Warn, parsedMessage

        parsedMessage = self.tryMatchPattern(message, self.goodMaps, self.goodPatterns)
        if parsedMessage:
            return LogType.Good, parsedMessage

        parsedMessage = self.tryMatchPattern(message, self.infoMaps, self.infoPatterns)
        if parsedMessage:
            return LogType.Info, parsedMessage

        parsedMessage = self.tryMatchPattern(message, self.debugMaps, self.debugPatterns)
        if parsedMessage:
            return LogType.Debug, parsedMessage

        return LogType.Noise, message
Esempio n. 3
0
class Logger:
    _streams = InjectMany('LogStream')
    ''' Simple log class to use with build scripts '''
    def __init__(self):
        self.currentHeading = ''
        self._startTime = None
        self._headerStartTime = None
        self._errorOccurred = False

    @property
    def hasHeading(self):
        return self._headerStartTime != None

    def heading(self, msg, *args):

        self.endHeading()

        self._errorOccurred = False
        self._headerStartTime = datetime.now()

        if not self._startTime:
            self._startTime = datetime.now()

        # Need to format it now so that heading gets the args
        if len(args) > 0:
            msg = msg.format(*args)

        self.currentHeading = msg
        self._logInternal(msg, LogType.Heading)

    def debug(self, msg, *args):
        self._logInternal(msg, LogType.Debug, *args)

    def info(self, msg, *args):
        self._logInternal(msg, LogType.Info, *args)

    def error(self, msg, *args):

        if self._headerStartTime:
            self._errorOccurred = True

        self._logInternal(msg, LogType.Error, *args)

    def warn(self, msg, *args):
        self._logInternal(msg, LogType.Warn, *args)

    def finished(self, msg, *args):
        ''' Call this when your script is completely finished '''

        self.endHeading()
        self._logInternal(msg, LogType.Heading, *args)

    def good(self, msg, *args):
        self._logInternal(msg, LogType.Good, *args)

    def endHeading(self):

        if not self._headerStartTime:
            return

        delta = datetime.now() - self._headerStartTime
        totalDelta = datetime.now() - self._startTime

        message = ''

        if self._errorOccurred:
            message = 'Failed'
        else:
            message = 'Done'

        message += ' (Took %s, time: %s, total elapsed: %s)' % (
            Util.formatTimeDelta(
                delta.total_seconds()), datetime.now().strftime('%H:%M:%S'),
            Util.formatTimeDelta(totalDelta.total_seconds()))

        self._headerStartTime = None

        if self._errorOccurred:
            self._logInternal(message, LogType.HeadingFailed)
        else:
            self._logInternal(message, LogType.HeadingSucceeded)

    def _logInternal(self, msg, logType, *args):

        if len(args) > 0:
            msg = msg.format(*args)

        if logType == LogType.Heading:
            msg += '...'

        for stream in self._streams:
            stream.log(logType, msg)