# ---------------------------------------------------------------- # Defines some helper types and functions for batchSVN and batchSVNData # # by Curt Clifton, September 2009 # ---------------------------------------------------------------- import os from string import Template from subprocess import Popen, PIPE, STDOUT class RepositorySet: """Represents a collection of SVN repositories. Examples include all the individual student repositories for the course, or the set of team repositories for a particular project.""" def __init__(self, name, reposPrefix, reposSuffixes): self.name = name self.prefix = reposPrefix self.reposSuffixes = reposSuffixes def repos(self): for s in self.reposSuffixes: yield Repository(self.prefix + s, s) def __repr__(self): return "RepositorySet(%s,%s,%s)" % (self.name, self.prefix, self.reposSuffixes) class Repository: """Represents a single repository.""" def __init__(self, longName, shortName): self.longName = longName self.shortName = shortName def __repr__(self): return "Repository(%s,%s)" % (self.longName, self.shortName) class Project: """Represents a project and associated JUnit test case classes.""" def __init__(self, name, *testClasses): """Constructs a Project instance. Optional arguments are strings giving fully qualified names of JUnit test classes.""" self.name = name self.testClasses = testClasses def __repr__(self): args = list(self.testClasses) args.insert(0, self.name) args = ','.join(args) return "Project(" + args + ")" class MenuItem: """Represents a menu item to be added to the main menu of the script.""" def __init__(self, itemText, itemFunction): """Constructs a new menu item. itemFunction is a one-argument function that will be executed when a user selects the menu item. It will be passed a DataBlob object whose fields are the relevant "global" variables from the main application. See batchSVN.py for details on the included values. The function should return True if the program should exit after executing the function, or False otherwise. """ self.text = itemText self.function = itemFunction def display(self, nextItemNumber): '''Displays this menu item with the given item number. Returns the appropriate number for the item after this one.''' print "%2d - %s" % (nextItemNumber, self.text) return nextItemNumber + 1 class MenuSeparator: """Represents a separator between items in a menu.""" def __init__(self, separatorText = '----'): self.text = separatorText def display(self, nextItemNumber): '''Displays this menu separator without an item number. Returns the appropriate number for the item after this one, that is, nextItemNumber''' print self.text return nextItemNumber class DataBlob: """Contains fields representing the relevant "global" variables from the main application. Used to pass data into menu item functions. """ pass # ---------------------------------------------------------------- def truncateMiddle(d, length): """Returns a truncated version of d by omitting characters in the middle if d is longer then length.""" splitLen = length / 2 if len(d) < length: return d else: return d[:splitLen] + '...' + d[-(length-3-splitLen):] # ---------------------------------------------------------------- def displayMenu(prompt, items, allowOthers = False): """ Displays a numbered menu consisting of the given list menu items. Returns a tuple consisting of the chosen item number (or None if other text is entered) and the text of the choice. items may be a list of strings or a list of MenuItem and MenuSeparator objects. Presents the given prompt. Continues presenting the prompt until the user picks an item from the list. If allowOthers is True, then also lets user enter a non-numeric string which is the text returned to the caller. Otherwise the element of 'items' corresponding to the entered number is returned. """ if isinstance(items[0], str): for i,t in enumerate(items): print "%2d - %s" % (i, t) else: num = 0 for mi in items: num = mi.display(num) items = filter(lambda x: isinstance(x, MenuItem), items) print choice = '' while choice == '': inputValue = raw_input(prompt).strip() try: inputValue = int(inputValue) choice = items[inputValue] except ValueError: if allowOthers: choice = inputValue inputValue = None else: print '\a*** you must choose a number from the list above ***' choice = '' except IndexError: print '\a*** only numbers in the list above are valid ***' return inputValue, choice # ---------------------------------------------------------------- def forEachRepo(command, data, msg='', create=False, withCapture=False, suppressOutput=False): """Iterates over each repository executing the given command. The command is executed with the current working directory set to the local working directory. The command is a template string. Within the template string: ${rootURL} is replaced with the root URL of the repository ${repo} is replaced with the full repository name ${proj} is replaced with the project name ${shortName} is replaced with the short repository name, that is the full repository name with the course/term prefix removed ${msg} is replaced with the msg argument ${scriptDir} is replaced with the script directory ${cwd} is replaced with the working directory ${testClasses} is replaced with a space-delimited list of the fully qualified names of the JUnit test classes msg is the replacement for the ${msg} placeholder in the command data is a DataBlob containing the "global" variables from the main script create specifies whether the local project subdirectory should be created withCapture specifies whether the output (stdout and stderr) of the command should be captured to batchLogFileName, lines of captured data are prefixed with the short repository name, a colon, and a space suppressOutput specified whether the output of the command should be suppressed on the console """ replacements = {} replacements['rootURL'] = data.rootURL replacements['proj'] = data.activeProject.name replacements['msg'] = "'" + msg + "'" replacements['scriptDir'] = data.scriptDir replacements['cwd'] = data.workingDir replacements['testClasses'] = ' '.join(data.activeProject.testClasses) commandTemplate = Template(command) if command.find('${testClasses}') >= 0 and len(data.activeProject.testClasses) == 0: print '\a\n*** No test classes listed for project "' + data.activeProject.name + '" ***\n' return # Creates project specific subdirectory if requested if create: if os.access(data.activeProject.name,os.F_OK): print '\a\n*** Local project subdirectory already exists ***\n' return else: os.mkdir(data.activeProject.name) if not os.access(data.activeProject.name,os.F_OK): print '\a\n*** Unable to create local project subdirectory ***\n' return # Verifies project subdirectory exists if not os.access(data.activeProject.name,os.F_OK): print '\a\n*** Unable to access local project subdirectory ***\n' return # Functional goodness for printing if suppressOutput: def printHelper(x): pass else: def printHelper(x): print x # Iterates over the repositories if withCapture: captureFile = open(data.batchLogFileName, 'w') for repo in data.activeRepoSet.repos(): replacements['repo'] = repo.longName replacements['shortName'] = repo.shortName cmd = commandTemplate.substitute(replacements) printHelper('-*' * (data.menuWidth / 2)) shortNameLength = len(replacements['shortName']) padRepeat = (data.menuWidth - shortNameLength) / 2 / 4 printHelper('--> ' * padRepeat + replacements['shortName'].upper() + ' <--' * padRepeat) printHelper(truncateMiddle(cmd, data.menuWidth)) os.chdir(data.activeProject.name) p = Popen(cmd, shell=True, stdin=PIPE, stdout=PIPE, stderr=STDOUT, close_fds=True) (cmdInput, cmdOutput) = (p.stdin, p.stdout) outputText = cmdOutput.readlines() cmdInput.close() cmdOutput.close() for line in outputText: printHelper(line.strip()) if withCapture: captureFile.write(replacements['shortName']) captureFile.write(': ') captureFile.write(line) os.chdir('..') if withCapture: captureFile.close()