use System.Diagnostics use System.Reflection use System.Text.RegularExpressions use System.Threading class TestifyRunner """ Implements the -testify option of the command line. """ var _startTime as DateTime var _cl as CommandLine var _pathList as List var _extraTestifyArgs as String? var _testifyCount as int var _failureCount as int # comes in handy when profiling: var _maxCount = 0 var _willRunExes = true var _firstAttempt as bool var _cachedTestifyModules as IList? # two writers. status goes to console and results goes to a file. var _statusWriter as IndentedWriter? var _statusCount as int var _resultsWriter as IndentedWriter? const bar = '----------------------------------------------------------------------------------------------------' ## Init cue init(startTime as DateTime, cl as CommandLine, paths as List) base.init _startTime = startTime _cl = cl _pathList = paths ## Properties get pathList from var get options as OptionValues return _cl.options get verbosity as int return _cl.verbosity ## Run def run paths = _pathList if paths.count == 0 paths = .cobraTestPaths + [Path.getFullPath(Path.combine('..', 'HowTo'))] numThreads = .options.getDefault('testify-threads', 1) to int if numThreads > 1 .runThreaded(numThreads, paths) else .runNonThreaded(paths) # these vars support running sub-processes, managed by multiple threads in the original cobra executable var _subDirQueue = Queue() var _subCobraExe = '' var _subCommandLineArgs = List() var _subResultsFileNames = List() def runThreaded(numThreads as int, paths as List) require numThreads > 1 _statusWriter = IndentedWriter(AutoFlushWriter(Console.out), indentString=' ') args = CobraCore.commandLineArgs _subCobraExe = args[0] _subCommandLineArgs = for arg in args[1:] where arg.startsWith('-') and not '-testify-threads:' in arg _statusWriter.writeLine('Queueing for threads:') for path in paths _statusWriter.writeLine(' [path]') _subDirQueue.enqueue(path) _subResultsFileNames.add('') # empty placeholder. these values are set in order assert _subDirQueue.count > 0 threads = List() for i in numThreads, threads.add(Thread(ref .runTestifyProcessInThread)) # CC: threads = for i in numThreads get Thread(ref .runTestifyProcessInThread) for t in threads, t.start for t in threads, t.join _concatSubResults _printTotals def _concatSubResults sep, sepNl = '', CobraCore.newLine + CobraCore.newLine + CobraCore.newLine concat = StringBuilder() for fileName in _subResultsFileNames.reversed # they are set in a reversed order concat.append(sep) concat.append(File.readAllText(fileName)) sep = sepNl File.writeAllText(.options['testify-results'] to String, concat.toString) for fileName in _subResultsFileNames try File.delete(fileName) catch exc as Exception _statusWriter.writeLine('warning: Cannot delete "[fileName]" due to: [exc]') def _printTotals resultsFileName = .options['testify-results'] to String using resultsWriter = File.appendText(resultsFileName) __printTotals(resultsWriter to !) __printTotals(_statusWriter to !) def __printTotals(writer as TextWriter) writer.writeLine writer.writeLine('Final multithreaded results:') writer.writeLine('[_testifyCount] Tests') if _failureCount > 0 writer.writeLine('[_failureCount] Failures') else writer.writeLine('Success.') def runTestifyProcessInThread tid = Thread.currentThread.getHashCode while true # keep getting a work queue item lock _subDirQueue if _subDirQueue.count == 0, break # ...until they run out path = _subDirQueue.dequeue pathIndex = _subDirQueue.count resultsFileName = 'r-testify-[pathIndex]' lock _subResultsFileNames, _subResultsFileNames[pathIndex] = resultsFileName args = _subCommandLineArgs + ['-testify-results:[resultsFileName]', '-testify-threads:1', '"[path]"'] lock _statusWriter, _statusWriter.writeLine('Thread [tid] start: [args.join(" ")]') p = Process() p.startInfo.useShellExecute = false p.startInfo.redirectStandardOutput = true p.startInfo.redirectStandardError = true p.startInfo.fileName = _subCobraExe p.startInfo.arguments = args.join(' ') p.start # TODO: get the process output line by line and display as we go output = p.standardOutput.readToEnd output += p.standardError.readToEnd p.waitForExit lock _statusWriter _statusWriter.writeLine _statusWriter.writeLine('Thread [tid] output:') for line in output.splitLines if .verbosity >= 2 _statusWriter.writeLine(' t[tid]|[line]') else if line.startsWith('Finished at') or line.startsWith('timeit =') continue _statusWriter.writeLine(' ' + line) m = Regex.match(line, r'(\d+) Tests? in ') if m.success nTests = _parseIntGroup(m) _testifyCount += nTests m = Regex.match(line, r'(\d+) Failure') if m.success nTests = _parseIntGroup(m) _failureCount += nTests _statusWriter.writeLine def _parseIntGroup(m as Match) as int """ Return first capture of first group of a Regexp.Match parsed to an integer. """ cap = m.groups[1].captures[0].value n as int int.tryParse(cap, out n) return n def runNonThreaded(paths as List) _statusWriter = IndentedWriter(AutoFlushWriter(Console.out)) _statusWriter.indentString = ' ' try resultsFileName = .options['testify-results'] to String using resultsWriter = File.createText(resultsFileName) _resultsWriter = IndentedWriter(AutoFlushWriter(resultsWriter)) print to _resultsWriter to ! print 'Cobra: Testify' print 'Started at', DateTime.now print _innerRun(paths) finally if .verbosity >= 2 _statusWriter.writeLine('Results in [resultsFileName]') _statusWriter = nil def _innerRun(paths as List) # TODO Console.error = Console.out _testifyCount = 0 for pathName in paths if Directory.exists(pathName) .testifyDir(pathName) else if File.exists(pathName) _testifyCount += .testifyFile(pathName) else .error('No such directory or file named "[pathName]".') if _maxCount > 0 and _testifyCount >= _maxCount break .testifyFinish(if(_failureCount, 'Failure.', 'Success.')) ## The Rest def error(msg as String) _cl.error(msg) def parseArgs(args as String, options as out OptionValues?, paths as out List?) """ This overload is primarily for -testify. Arguments that come in from the system are already divided up into a list. """ ensure options paths body args = args.trim argsList = if('|' in args, args.split(c'|'), args.split) _cl.parseArgs(argsList to passthrough, out options, out paths) get cobraTestPaths as List """ Only used when -testify is passed with no path. --testify is an "internal" feature of the cobra command line front end. """ # -testify is often invoked out of the next-door directory "Source" # so check next door, first: slash = Path.directorySeparatorChar path = "..[slash]Tests" if Directory.exists(path) paths = List() for subdir in Directory.getDirectories(path) fileName = Path.getFileName(subdir) if not fileName.startsWith('.') and not fileName.startsWith('_') paths.add(Path.getFullPath(subdir)) return paths throw Exception('Cannot find Tests directory next door.') def _testifyFlush _statusWriter.flush _resultsWriter.flush def testifyOptions as OptionValues options as OptionValues? paths as List? .parseArgs(_extraTestifyArgs ? '', out options, out paths) # to get the default options # remember that you cannot use synonyms below. you must use the canonical name of the option options['debug'] = '+' options['debugging-tips'] = false options['embed-run-time'] = false options['verbosity'] = 2 options['back-end'] = .options['back-end'] #trace options return options to ! def testifyFinish(message as String) _testifyFinish(message, _statusWriter to !) _testifyFinish(message, _resultsWriter to !) def _testifyFinish(message as String, writer as TextWriter) print to writer duration = DateTime.now.subtract(_startTime) print print 'Finished at', DateTime.now print '[_testifyCount] Tests in [duration].' if _failureCount print '[_failureCount] Failures.' print print message def testifyDir(dirName as String) """ Returns the number tests that passed. """ baseName = Path.getFileName(dirName) to ! # gets rid of "." and ".." as prefix for relative dirs if baseName.startsWith('.') or baseName.startsWith('_') # examples: .svn, _svn. Also, _ is a nice way to temporarily exclude a directory if possible return configPath = Path.combine(dirName, 'testify.kv') if File.exists(configPath) dirOptions = Utils.readKeyValues(configPath) if dirOptions.containsKey('args') argSets = dirOptions['args'].split(@[c'|']) for argSet in argSets _testifyDir(dirName, argSet.trim) return _testifyDir(dirName, nil) def _testifyDir(dirName as String, args as String?) willCopyLibs = true # not CobraCore.isRunningMono # On Mono, we use MONO_PATH instead of coping the Cobra.Lang.dll around libNames = ['Cobra.Lang.dll'] _extraTestifyArgs = args _statusWriter.writeLine(if(args, '[dirName] with args: [args]', '[dirName]')) _statusWriter.indent try print 'Running tests in [dirName]' saveDir = Environment.currentDirectory if willCopyLibs # _cleanDir will remove these later for libName in libNames # Ideally, the file operations below would not be wrapped with try...catch and a # 'warning' because if the current library files cannot be copied into the test # directory, then the test is not valid. However, Windows has problems with # deleting and/or copying over Cobra.Lang.dll right after it has been used. This # comes up on Tests\340-contracts whose testify.kv config file specifies two # runs of the directory. targetPath = Path.combine(dirName, libName) if File.exists(targetPath) if Utils.isRunningOnUnix File.delete(targetPath) else # Windows is just too damn picky... try File.delete(targetPath) catch UnauthorizedAccessException print 'testify warning: cannot delete:', targetPath if libName == 'Cobra.Lang.dll' or File.exists(libName) if Utils.isRunningOnUnix File.copy(Path.combine(saveDir, libName), targetPath) else try File.copy(Path.combine(saveDir, libName), targetPath, true) catch IOException print 'testify warning: cannot copy or copy over:', targetPath Directory.setCurrentDirectory(dirName) try setUpScript = if(Utils.isRunningOnUnix, 'testify-set-up', 'testify-set-up.bat') if File.exists(setUpScript) print '* * * * Running [setUpScript]' process = Process() process.startInfo.fileName = setUpScript setUpOutput = CobraCore.runAndCaptureAllOutput(process) if process.exitCode <> 0 print 'TESTIFY WARNING: Script [setUpScript] exited with error code [process.exitCode]' print 'begin output' print setUpOutput.trim print 'end output' paths = List(Directory.getFiles('.')) paths.addRange(Directory.getDirectories('.') to passthrough) paths.sort for baseName in paths baseName = Utils.normalizePath(baseName) if baseName.startsWith('_'), continue if baseName.endsWith('.cobra') or baseName.endsWith('.COBRA') _testifyCount += .testifyFile(baseName) else if Directory.exists(baseName) .testifyDir(baseName) _testifyFlush if _maxCount > 0 and _testifyCount >= _maxCount break _cleanDir(dirName) finally Directory.setCurrentDirectory(saveDir) finally _statusWriter.dedent _testifyFlush def testifyFile(baseName as String) as int save = _failureCount _firstAttempt = true try result1 = _testifyFile(baseName) if _failureCount > save print print print 'DUE TO FAILURE, RERUNNING WITH MORE OUTPUT:' _firstAttempt = false result2 = _testifyFile(baseName) if result1 <> result2 print 'ERROR: Result mismatch. result1=[result1], result2=[result2]' finally _testifyFlush return result1 def _testifyFile(baseName as String) as int Node.setCompiler(nil) verbose = not _firstAttempt compilerVerbosity = if(.verbosity, .verbosity, if(verbose, 1, 0)) if Path.pathSeparator in baseName return .testifyFilePath(baseName) _statusWriter.writeLine('([_statusCount]) [baseName]') _statusCount += 1 assert File.exists(baseName) source = File.readAllText(baseName) print print print print 'RUN [baseName]' print ' [Utils.combinePaths(Environment.currentDirectory, baseName)]' print ' Test #[_testifyCount+1]' print .bar print .bar if verbose Utils.printSource(source) print .bar lines = List(source.split(c'\n')) # CC: axe list wrapper fileNames = [baseName] options = .testifyOptions options['willRunExe'] = _willRunExes # internal, specific to testify firstLine = '' count0 = lines.count rc = _processFirstlineDirectives(baseName, fileNames, inout lines, inout options, out firstLine) if rc == 0, return 0 firstLineInsensitive = firstLine.trim.replace(' ', '') nRemovedLines = count0 - lines.count # Check for inline warning and error messages that are expected. # (Starting in 2007-12 this is now the preferred way to specify these messages-- # with the actual line of code that generates them. # The old method of specifying at the top will still be needed for errors # and warnings that have no associated line number.) expectingError = false inLineMessages = _getInlineMessages(lines, nRemovedLines, out expectingError) if inLineMessages.count > 0 # hasInlineMessages return _testifyInlineMessages(inLineMessages, expectingError, compilerVerbosity, [baseName], options, verbose ) if firstLineInsensitive.startsWith('#.error.') # deprecated: put errors on the lines where they occur. the "hasInlineMessages" code above will detect them. # Note that errors that are only detected by the backend C# compiler are not detectable by testify # CC: support split with a String extension method # error = firstLine.split('.error.',1)[1].trim.toLower index = firstLine.indexOf('.error.') error = firstLine.substring(index+7).trim.toLower return _testifyHeadError(error, compilerVerbosity, fileNames, options, verbose) if firstLineInsensitive.startsWith('#.warning.') # deprecated: put warnings on the lines where they occur. the "hasInlineMessages" code above will detect them. index = firstLine.indexOf('.warning.') warning = firstLine.substring(index+9).trim.toLower return _testifyHeadWarning(warning, false, compilerVerbosity, fileNames, options, verbose) if firstLineInsensitive.startsWith('#.warning-lax.') # deprecated: put warnings on the lines where they occur. the "hasInlineMessages" code above will detect them. index = firstLine.indexOf('.warning-lax.') warning = firstLine.substring(index+13).trim.toLower return _testifyHeadWarning(warning, true, compilerVerbosity, fileNames, options, verbose) return _testifyStd(compilerVerbosity, fileNames, options, verbose) def _processFirstlineDirectives(baseName as String, fileNames as List, lines as inout List, options as inout OptionValues, firstLine as out String) as int """ Check first few lines for Testify directives (start with '#.') and process or setup for later handling. """ firstLine = lines[0] firstLineInsensitive = firstLine.trim.replace(' ', '') while firstLineInsensitive.startsWith('#.') if firstLineInsensitive.startsWith('#.multi.') print 'Running multiple files.' for fileName in firstLine.substring(firstLine.indexOf('.multi.')+8).split if fileName.length fileNames.add(Utils.combinePaths(Path.getDirectoryName(baseName) to !, fileName)) print 'Multiple filenames:', fileNames.join(', ') # enable having another directive on the next line, such as .error. lines = lines[1:] firstLine = lines[0] firstLineInsensitive = firstLine.trim.replace(' ', '') continue if firstLineInsensitive.startsWith('#.require.') rtPlatform = if(options['back-end']=='none', CobraCore.runtimePlatform, options['back-end']) what = firstLineInsensitive[10:] branch what on 'mono' if rtPlatform <> 'clr' or not CobraCore.isRunningOnMono print 'Skipping test because requirement for "mono" is not met.' return 0 on 'dotnet' if rtPlatform <> 'clr' or CobraCore.isRunningOnMono print 'Skipping test because requirement for "dotnet" is not met.' return 0 on 'clr' # mono or dotNet if rtPlatform <> 'clr' print 'Skipping test because requirement for "clr" is not met.' return 0 on 'jvm' if rtPlatform <> 'jvm' print 'Skipping test because requirement for "jvm" is not met.' return 0 else if what.endsWith('.dll') try loadAssemblyResult = Utils.loadWithPartialName(what) catch assExc as Exception print 'Skipping test because DLL requirement "[what]" failing with exception: [assExc]' return 0 if not loadAssemblyResult print 'Skipping test because DLL requirement "[what]" not found.' return 0 else .error('Unrecognized requirement: "[what]"') lines = lines[1:] firstLine = lines[0] firstLineInsensitive = firstLine.trim.replace(' ', '') continue if firstLineInsensitive.startsWith('#.compile-only.') # also meaning don't run the .exe options['willRunExe'] = false lines = lines[1:] firstLine = lines[0] firstLineInsensitive = firstLine.trim.replace(' ', '') continue if firstLineInsensitive.startsWith('#.args.') i = firstLine.indexOf('.args.') args as OptionValues? .parseArgs(firstLine[i+'.args.'.length:], out args, out _pathList) options = .testifyOptions options.combine(args to !) # enable having another directive on the next line, such as .error. lines = lines[1:] firstLine = lines[0] firstLineInsensitive = firstLine.trim.replace(' ', '') continue if firstLineInsensitive.startsWith('#.skip.') comment = firstLine[firstLine.indexOf('.skip.')+6:].trim if comment.length, comment = '"' + comment + '"' msg = 'Skipping test because of directive. [comment]'.trim _statusWriter.indent _statusWriter.writeLine(msg) _statusWriter.dedent print msg return 0 if firstLineInsensitive.startsWith('#.multipart.') # .multi. is the one that gets run along with its associated files # the associated files then specify .multipart. and get skipped when encountered print 'Skipping test because multipart.' return 0 if firstLineInsensitive.startsWith('#.error.') or firstLineInsensitive.startsWith('#.warning.') or firstLineInsensitive.startsWith('#.warning-lax.') # these are handled below break throw Exception('Bad first line: [lines[0]]') return 1 # continue processing in caller def _getInlineMessages(lines as List, offset as int, expectingError as out bool) as Dictionary """ Walk lines and accumulate inline warnings and error messages. """ inLineMessages = Dictionary() firstLine = 1 + offset lineNum = firstLine expectingError = false for line in lines if lineNum > firstLine and ('.warning.' in line or '.error.' in line) if '.warning.' in line message = line[line.indexOf('.warning.') + '.warning.'.length:] messageType = 'w' else if '.error.' in line message = line[line.indexOf('.error.') + '.error.'.length:] messageType = 'e' expectingError = true else throw FallThroughException(line) inLineMessages[lineNum] = messageType + message lineNum += 1 return inLineMessages def _testifyInlineMessages(inLineMessages as Dictionary, expectingError as bool, compilerVerbosity as int, fileNames as IList, options as OptionValues, verbose as bool) as int """Testify on files that have inline checks for compiler errors and warnings""" try c = Compiler(compilerVerbosity, _cachedTestifyModules, commandLineArgParser=_cl.argParser) c.testifyFilesNamed(fileNames, options, _resultsWriter to !, verbose) catch StopCompilation pass catch exc as Exception print 'Internal exception: [exc]' .failed return 0 for msg in c.messages if not msg.hasSourceSite or msg.lineNum == 0 print 'Not expecting messages without any line number information:' print msg bad = true continue if not inLineMessages.containsKey(msg.lineNum) print 'Encountered unexpected message:' print msg bad = true continue expected = inLineMessages[msg.lineNum] branch expected[0] on c'w' if msg.isError print 'Expecting warning on line [msg.lineNum], but got error instead.' bad = true on c'e' if not msg.isError print 'Expecting error on line [msg.lineNum], but got warning instead.' bad = true else throw FallThroughException(expected) if bad, continue expected = expected[1:].trim if msg.message.trim.toLower.indexOf(expected.toLower) == -1 print 'Expecting message :', expected print 'But got :', msg.message.trim print 'At line :', msg.lineNum bad = true continue # we made it! same type of message and text print 'Message for line [msg.lineNum] was expected.' inLineMessages.remove(msg.lineNum) # check for expected messages that never occurred for key in inLineMessages.keys bad = true print 'Expecting message on line [key]:', inLineMessages[key][1:].trim if bad .failed return 0 else if expectingError return 1 else if options.boolValue('willRunExe') # a test case with nothing but warnings is still executed return _testifyRun(c) return 1 def _testifyHeadError(error as String, compilerVerbosity as int, fileNames as IList, options as OptionValues, verbose as bool) as int try c = Compiler(compilerVerbosity, _cachedTestifyModules, commandLineArgParser=_cl.argParser) c.testifyFilesNamed(fileNames, options, _resultsWriter to !, verbose) print 'Expecting error(s): [error]' print 'No error at all.' if c.errors.count > 0 print 'warning: error count > 0 but StopCompilation was not thrown' .failed return 0 catch StopCompilation assert c.errors.count expectedErrors = error.split(c'&') for i in 0 : expectedErrors.length expectedError = expectedErrors[i].trim print 'Expecting error substring [i+1] of [expectedErrors.length]: **[expectedError]**' if i >= c.errors.count print 'Ran out of real errors.' .failed return 0 actualError = c.errors[i] if actualError.message.toLower.indexOf(expectedError) == -1 print 'Actual error is: **[actualError.message]**' .failed return 0 print 'Matches: "[actualError.message]"' if c.errors.count > expectedErrors.length print 'There are more actual errors than expected errors:' for i in expectedErrors.length : c.errors.count print 'Another actual error: [c.errors[i].message]' .failed return 0 catch exc as Exception print 'Internal exception: [exc]' .failed return 0 return 1 def _testifyHeadWarning(warning as String, lax as bool, compilerVerbosity as int, fileNames as IList, options as OptionValues, verbose as bool) as int # TODO: the following code both checks for warnings to be thrown as well as going through a list of warnings. Seems like it should just need to do one or the other. try c = Compiler(compilerVerbosity, _cachedTestifyModules, commandLineArgParser=_cl.argParser) c.testifyFilesNamed(fileNames, options, _resultsWriter to !, verbose) catch StopCompilation if not lax print 'Expecting warning substring: "[warning]"' print 'But got errors.' .failed return 0 catch exc as Exception print 'Internal exception: [exc]' .failed return 0 expectedWarnings = warning.split(c'&') for i in 0 : expectedWarnings.length expectedWarning = expectedWarnings[i].trim print 'Expecting warning substring [i+1] of [expectedWarnings.length]: **[expectedWarning]**' if i >= c.warnings.count print 'Ran out of real warnings.' .failed return 0 actualWarning = c.warnings[i] if actualWarning.message.toLower.indexOf(expectedWarning)==-1 print 'Actual warning is: **[actualWarning.message]**' .failed return 0 else print 'Matches: "[actualWarning.message]"' if c.warnings.count > expectedWarnings.length print 'There are more actual warnings than expected warnings:' for i in expectedWarnings.length : c.warnings.count print 'Another actual warning: [c.warnings[i].message]' .failed return 0 return 1 def _testifyStd(compilerVerbosity as int, fileNames as IList, options as OptionValues, verbose as bool) as int c = Compiler(compilerVerbosity, _cachedTestifyModules, commandLineArgParser=_cl.argParser) try c.testifyFilesNamed(fileNames, options, _resultsWriter to !, verbose) catch StopCompilation .failed return 0 catch exc as Exception print 'Internal exception: [exc]' .failed return 0 if options.boolValue('willRunExe') and options.boolValue('compile') # maybe changed by compiler directive options['willRunExe'] = false if c.messages.count # can't be errors or StopCompilation would have been caught above print 'Unexpected warnings in test.' .failed return 0 if options.boolValue('willRunExe') return _testifyRun(c) return 1 def _testifyRun(c as Compiler) as int if not c.backEnd.isRunnableFile(c.fullExeFileName) # below assumes created file placed in cwd if File.exists(c.fullExeFileName) print 'Produced file "[c.fullExeFileName]" as expected.' return 1 else print 'Did not produce file "[c.fullExeFileName]".' .failed return 0 else print 'Run:' if .verbosity >= 1, print 'c.fullExeFileName = "[c.fullExeFileName]"' p = c.runProcess if .verbosity >= 2, print '[p.startInfo.fileName] [p.startInfo.arguments]' output = CobraCore.runAndCaptureAllOutput(p).trim print 'Output:' if output.length print output if p.exitCode <> 0 print 'Exit code = [p.exitCode]' .failed return 0 if output.toLower.indexOf('unhandled exception') <> -1 .failed return 0 print .bar _cachedTestifyModules = for mod in c.modules where mod inherits AssemblyModule get mod return 1 def failed """ Produces output and increments the failure count, but does not throw an exception or exit. """ if _firstAttempt _statusWriter.writeLine('FAILURE ----------------------------------------------------------------------') _failureCount += 1 print print print 'TEST FAILURE. SEE BELOW FOR VERBOSE RERUN.' def testifyFilePath(pathName as String) as int dirName = Path.getDirectoryName(pathName) baseName = Path.getFileName(pathName) to ! assert dirName and dirName.length assert baseName.length saveDir = Environment.currentDirectory Directory.setCurrentDirectory(dirName) try return .testifyFile(baseName) finally Directory.setCurrentDirectory(saveDir) def _cleanDir(dirName as String) curDir = Environment.currentDirectory if dirName.endsWith(Path.directorySeparatorChar.toString), dirName = dirName[:-1] assert Path.getFileName(dirName) == Path.getFileName(curDir) di = DirectoryInfo(curDir) for fileInfo in di.getFiles try if fileInfo.extension in ['.exe', '.dll', '.mdb', '.pdb', '.class', '.tmp'] _ and fileInfo.name not in ['Nested.dll', 'Foo.iSeries.dll'] # TODO: perhaps the extensions for generated files should come from the back-end # TODO: find a general way to determine if a binary file is part of the workspace/repository fileInfo.delete catch ex as Exception if not fileInfo.name.startsWith('Cobra.') # this happens regularly and I don't feel like seeing it print 'warning: cannot delete [fileInfo] due to: [ex.message]'