def test_just_pulled(): root_dir = normalise_dir(os.getcwd()) root_repo = 'file://' + os.path.join(root_dir, 'repo') # Set up our repositories setup_bzr_checkout_repositories() setup_new_build(root_repo, 'build_0') root_repo = 'file://' + os.path.join(root_dir, 'repo') banner('Build A') with NewDirectory('build_A'): muddle(['init', 'bzr+%s' % root_repo, 'builds/01.py']) muddle(['checkout', '_all']) banner('Build B') with NewDirectory('build_B'): muddle(['init', 'bzr+%s' % root_repo, 'builds/01.py']) muddle(['checkout', '_all']) banner('Change Build A') with Directory('build_A'): with Directory('src'): with Directory('builds'): append('01.py', '# Just a comment\n') # Then remove the .pyc file, because Python probably won't realise # that this new 01.py is later than the previous version os.remove('01.pyc') bzr('commit -m "A simple change"') muddle(['push']) with Directory('twolevel'): with Directory('checkout2'): append('Makefile.muddle', '# Just a comment\n') bzr('commit -m "A simple change"') muddle(['push']) banner('Pull into Build B') with Directory('build_B') as d: _just_pulled_file = os.path.join(d.where, '.muddle', '_just_pulled') if os.path.exists(_just_pulled_file): raise GiveUp('%s exists when it should not' % _just_pulled_file) muddle(['pull', '_all']) if not same_content( _just_pulled_file, 'checkout:builds/checked_out\n' 'checkout:checkout2/checked_out\n'): raise GiveUp( '%s does not contain expected labels:\n%s' % (_just_pulled_file, open(_just_pulled_file).readlines())) muddle(['pull', '_all']) if not same_content(_just_pulled_file, ''): raise GiveUp('%s should be empty, but is not' % _just_pulled_file)
def test_options(): """Test we can read back options from a stamp file. """ fname = 'test_options.stamp' touch(fname, OPTIONS_TEST) v = VersionStamp.from_file(fname) if len(v.problems) != 4: raise GiveUp('Expected 4 problems reading %s, got %d' % (fname, len(v.problems))) # Make the problem order deterministic v.problems.sort() check_problem( v.problems[0], "Cannot convert value to integer, for 'option~BadFred = int:ThreadNeedle'" ) check_problem( v.problems[1], "No datatype (no colon in value), for 'option~Aha~There = No colons here'" ) check_problem( v.problems[2], "Unrecognised datatype 'what' (not bool, int or str), for 'option~AhaTwo = what:pardon'" ) check_problem( v.problems[3], "Value is not True or False, for 'option~BadJim = bool:Immensity'") co_label = Label(LabelType.Checkout, 'co_name', None, LabelTag.CheckedOut) co_dir, co_leaf, repo = v.checkouts[co_label] options = v.options[co_label] expected_repo = 'file:///Users/tibs/sw/m3/tests/transient/repo/second_co' if co_dir is not None or co_leaf != 'co_name' or \ str(repo) != expected_repo: raise GiveUp('Error in reading checkout back\n' ' co_dir %s, expected None\n' ' co_leaf %s, expected co_name\n' ' repo %s\n' ' expected %s' % (co_dir, co_leaf, repo, expected_repo)) if len(options) != 3 or \ options['Jim'] != False or \ options['Bill'] != 'Some sort of string' or \ options['Fred'] != 99: raise GiveUp( 'Error in reading checkout options back\n' " expected {'Jim': False, 'Bill': 'Some sort of string', 'Fred': 99}\n" ' got %s' % options)
def pull(self, builder, co_label, upstream=None, repo=None, verbose=True): """ Retrieve changes from the remote repository, and apply them to the local working copy, but not if a merge operation would be required, in which case an exception shall be raised. If 'upstream' and 'repo' are given, then they specify the upstream repository we should pull from, instead of using the 'normal' repository from the build description. Returns True if it changes its checkout (changes the files visible to the user), False otherwise. """ if not (upstream and repo): repo = builder.db.get_checkout_repo(co_label) if not repo.pull: raise GiveUp( 'Failure pulling %s in %s:\n' ' %s does not allow "pull"' % (co_label, builder.db.get_checkout_location(co_label), repo)) specific_branch = self.branch_to_follow(builder, co_label) if specific_branch: # The build description told us to follow it, onto this branch # - so let's remember it on the Repository repo = repo.copy_with_changed_branch(specific_branch) print 'Specific branch %s in %s' % (specific_branch, co_label) options = builder.db.get_checkout_vcs_options(co_label) try: with Directory(builder.db.get_checkout_path(co_label)): return self.vcs.pull(repo, options, upstream=upstream, verbose=verbose) except MuddleBug as err: raise MuddleBug( 'Error pulling %s in %s:\n%s' % (co_label, builder.db.get_checkout_location(co_label), err)) except Unsupported as err: raise Unsupported( 'Not pulling %s in %s:\n%s' % (co_label, builder.db.get_checkout_location(co_label), err)) except GiveUp as err: raise GiveUp( 'Failure pulling %s in %s:\n%s' % (co_label, builder.db.get_checkout_location(co_label), err))
def get_vcs_docs(vcs): """Given a VCS short name, return the docs for how muddle handles it """ try: return vcs_docs[vcs] except KeyError: raise GiveUp("No VCS handler registered for VCS type %s" % vcs)
def get_current_branch(self, builder, co_label, verbose=False, show_pushd=False): """ Return the name of the current branch. Will be called in the actual checkout's directory. Return the name of the current branch (e.g., "master" or "Fred"), or None if there is no current branch. If 'show_pushd' is false, then we won't report as we "pushd" into the checkout directory. Raises a GiveUp exception if the VCS does not support this operation, or if something goes wrong. """ try: with Directory(builder.db.get_checkout_path(co_label), show_pushd=show_pushd): return self.vcs.get_current_branch() except (GiveUp, Unsupported) as err: raise GiveUp( 'Failure getting current branch for %s in %s:\n%s' % (co_label, builder.db.get_checkout_location(co_label), err))
def check_release_file_starts(filename, name, version, archive, compression, repo, desc, versions_repo): with open(filename) as fd: lines = fd.readlines() newlines = [] for line in lines: if line[0] == '#': continue while line and line[-1] in ('\n', '\r'): line = line[:-1] if line: newlines.append(line) expected = [ '[STAMP]', 'version = 2', '[RELEASE]', 'name = %s' % name, 'version = %s' % version, 'archive = %s' % archive, 'compression = %s' % compression, '[ROOT]', 'repository = %s' % repo, 'description = %s' % desc, 'versions_repo = %s' % versions_repo, ] if newlines[:11] != expected: print '--- Expected:' pprint.pprint(expected) print '--- Got:' pprint.pprint(newlines[:11]) raise GiveUp('Unexpected content in release stamp file %s' % filename)
def test_test_release(d, repo): banner('TEST TEST RELEASE') banner('Check out build tree, and stamp it as a release', 2) with NewCountedDirectory('build-test') as test_build: r = 'git+file://{repo}/main'.format(repo=repo) d = 'builds/01.py' v = '{root}/versions'.format(root=r) muddle(['init', r, d]) muddle(['checkout', '_all']) muddle(['stamp', 'release', 'simple', 'v1.0']) rfile = os.path.join(test_build.where, 'versions', 'simple_v1.0.release') check_release_file_starts(rfile, 'simple', 'v1.0', 'tar', 'gzip', r, d, v) UNSET = '(unset)' muddle_env = read_env_as_dict('first_pkg{x86}') if not (muddle_env['MUDDLE_RELEASE_NAME'] == UNSET and muddle_env['MUDDLE_RELEASE_VERSION'] == UNSET and muddle_env['MUDDLE_RELEASE_HASH'] == UNSET): print 'MUDDLE_RELEASE_NAME=%s' % muddle_env['MUDDLE_RELEASE_NAME'] print 'MUDDLE_RELEASE_VERSION=%s' % muddle_env[ 'MUDDLE_RELEASE_VERSION'] print 'MUDDLE_RELEASE_HASH=%s' % muddle_env['MUDDLE_RELEASE_HASH'] raise GiveUp( 'Expected the MUDDLE_RELEASE_ values to be all (unset)') banner( 'Try "muddle release -test" using that stamp, in the same directory', 2) muddle(['release', '-test', rfile]) check_release_directory(test_build)
def branch_exists(self, builder, co_label, branch, verbose=False, show_pushd=False): """ Returns True if a branch of that name exists. This allowed to be conservative - e.g., in git the existence of a remote branch with the given name can be counted as True. Will be called in the actual checkout's directory. If 'show_pushd' is false, then we won't report as we "pushd" into the checkout directory. """ try: with Directory(builder.db.get_checkout_path(co_label), show_pushd=show_pushd): return self.vcs.branch_exists(branch) except (GiveUp, Unsupported) as err: raise GiveUp( 'Failure checking existence of branch %s for %s in %s:\n%s' % (branch, co_label, builder.db.get_checkout_location(co_label), err))
def ssh_remote_cmd(self, remote_cmd, dirs=None, dry_run=False): """SSH to our location, and run the command over the directories. * 'remote_cmd' is the words that make up the command (as a list). * 'dirs' is the list of directories we want to pass to the command. If this is None, or an empty list, then we won't do that... """ parts = ['ssh'] if self.port: parts.append('-p %s' % self.port) parts.append(self.user_at_host) parts += remote_cmd cmd = ' '.join(parts) if dry_run: print "Would run: %s " % cmd if dirs: print "and pass it the following directories:" print "\n".join(dirs) elif dirs: print "> %s" % cmd p = subprocess.Popen(parts, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) stdoutdata, stderrdata = p.communicate("\n".join(dirs) + '\n') if p.returncode: print >> sys.stderr, "Error invoking the remote script (rc=%d):" % p.returncode print >> sys.stderr, stdoutdata raise GiveUp("Error invoking the remote script, rc=%d" % p.returncode) print "Script exited successfully, output was:\n%s\n<<<END OUTPUT>>>\n" % stdoutdata else: run0(cmd)
def __init__(self, where, stay_on_error=False, show_pushd=True, show_popd=False, set_PWD=True): self.start = normalise_dir(os.getcwd()) self.where = normalise_dir(where) self.close_on_error = not stay_on_error self.show_pushd = show_pushd self.show_popd = show_popd self.set_PWD = set_PWD try: os.chdir(self.where) except OSError as e: raise GiveUp('Cannot change to directory %s: %s\n' % (where, e)) if set_PWD: if 'PWD' in os.environ: self.got_old_PWD = True self.old_PWD = os.environ['PWD'] else: self.got_old_PWD = False self.old_PWD = None os.environ['PWD'] = self.where if show_pushd: sys.stdout.write('++ pushd to %s\n' % self.where)
def _git_describe_long(self, co_leaf, orig_revision, force=False, verbose=True): """ This returns a "pretty" name for the revision, but only if there are annotated tags in its history. """ retcode, revision = utils.run2('git describe --long', show_command=False) if retcode: if revision: text = utils.indent(revision.strip(), ' ') if force: if verbose: print "'git describe --long' had problems with checkout" \ " '%s'"%co_leaf print " %s" % text print "using original revision %s" % orig_revision return orig_revision else: text = ' (it failed with return code %d)' % retcode raise GiveUp("%s\n%s" % (utils.wrap( "%s: 'git describe --long'" " could not determine a revision id for checkout:" % co_leaf), text)) return revision.strip()
def push(self, repo, options, upstream=None, verbose=True): """ Will be called in the actual checkout's directory. XXX Should we grumble if the 'effective' branch is not the same as XXX the branch that is currently checked out? """ self._shallow_not_allowed(options) if self._is_detached_HEAD(): raise GiveUp( 'This checkout is in "detached HEAD" state, it is not\n' 'on any branch, and thus "muddle push" is not alllowed.\n' 'If you really want to push, first choose a branch,\n' 'e.g., "git checkout -b <new-branch-name>"') # Push this branch to the branch of the same name, whether it exists # yet or not effective_branch = 'HEAD' if not upstream: # If we're not given an upstream repository name, assume we're # dealing with an "ordinary" push, to our origin upstream = 'origin' # For an upstream, we won't necessarily have the remote in our # configuration (unless we already did an upstream pull from the same # repository...) self._setup_remote(upstream, repo, verbose=verbose) utils.shell(["git", "push", upstream, effective_branch], show_command=verbose)
def __init__(self, name, category, version=None): """Initialise a new License. The 'name' is the name of this license, as it is normally recognised. 'category' is meant to be a broad categorisation of the type of the license. Currently that is one of: * 'gpl' - some sort of GPL license, which propagate the need to distribute source code to other "adjacent" entities * 'open-source' - an open source license, anything that is not 'gpl'. Source code may, but need not be, distributed. * 'prop-source' - a proprietary source license, not an open source license. This might, for instance, be used for /etc files, which are distributed as "source code" (i.e., text), but are not in fact licensed under an open source license. * 'binary' - a binary license, indicating that the source code is not to be distributed, but binary (the contents of the "install" directory) may be. * 'private' - a marker that the checkout should not be distributed at all. 'version' may be a version string. If it is None, then it will not be shown in the str() or repr() for a license. """ self.name = name if category not in ALL_LICENSE_CATEGORIES: raise GiveUp("Attempt to create License '%s' with unrecognised" " category '%s'" % (name, category)) self.category = category self.version = version
def main(args): keep = False if args: if len(args) == 1 and args[0] == '-keep': keep = True else: print __doc__ raise GiveUp('Unexpected arguments %s' % ' '.join(args)) # Working in a local transient directory seems to work OK # although if it's anyone other than me they might prefer # somewhere in $TMPDIR... root_dir = normalise_dir(os.path.join(os.getcwd(), 'transient')) repo = os.path.join(root_dir, 'repo') with TransientDirectory(root_dir, keep_on_error=True, keep_anyway=keep) as root_d: banner('TESTING CHECKOUT OPTIONS') test_options() banner('MAKE REPOSITORIES') make_repos_with_subdomain(repo) first_stamp = test_stamp_unstamp(root_dir) test_stamp_is_current_working_set(first_stamp) test_unstamp_update_identity_operation(repo, first_stamp) test_unstamp_update_2(repo, first_stamp) test_stamping_branches(repo)
def apply_instructions(self, builder, label, prepare, deploy_path): for asm in self.assemblies: lbl = Label(utils.LabelType.Package, '*', asm.from_label.role, '*', domain=asm.from_label.domain) if not asm.obeyInstructions: continue instr_list = builder.load_instructions(lbl) for (lbl, fn, instrs) in instr_list: print "%s deployment: Applying instructions for role %s, label %s .. " % ( self.what, lbl.role, lbl) for instr in instrs: # Obey this instruction. iname = instr.outer_elem_name() print 'Instruction:', iname if iname in self.app_dict: if prepare: self.app_dict[iname].prepare( builder, instr, lbl.role, deploy_path) else: self.app_dict[iname].apply(builder, instr, lbl.role, deploy_path) else: raise GiveUp( "%s deployments don't know about instruction %s" % (self.what, iname) + " found in label %s (filename %s)" % (lbl, fn))
def check_problem(got, starts): """We can't be bothered to check ALL of the string... """ if not got.startswith(starts): raise GiveUp('Problem report does not start with what we expect\n' 'Got: %s\n' 'Wanted: %s ...' % (got, starts))
def check_text_lines_v_lines(actual_lines, wanted_lines, fold_whitespace=False): """Check two pieces of text are the same. 'wanted_lines' is the lines of text we want, presented as a list, without any newlines. If 'fold_whitespace' is true, first "fold" any sequences of whitespace in 'wanted_lines' to a single space each. This makes it easier to compare lines with data laid out using spacing. Prints out the differences (if any) and then raises a GiveUp if there *were* differences """ if fold_whitespace: compare_lines = [] for line in actual_lines: columns = line.split() line = ' '.join(columns) compare_lines.append(line) else: compare_lines = actual_lines diffs = unified_diff(wanted_lines, compare_lines, fromfile='Missing lines', tofile='Extra lines', lineterm='') difflines = list(diffs) if difflines: lines = ['Text did not match'] + list(difflines) raise GiveUp('\n'.join(lines))
def _calculate_revision(self, co_leaf, orig_revision, force=False, before=None, verbose=True): """ This returns a bare SHA1 object name for the current HEAD NB: if 'before' is specified, 'force' is ignored. """ if before: print "git rev-list -n 1 --before='%s' HEAD" % before retcode, revision = utils.run2( "git rev-list -n 1 --before='%s' HEAD" % before, show_command=False) print retcode, revision if retcode: if revision: text = utils.indent(revision.strip(), ' ') else: text = ' (it failed with return code %d)' % retcode raise GiveUp("%s\n%s" % (utils.wrap( "%s:" " \"git rev-list -n 1 --before='%s' HEAD\"'" " could not determine a revision id for checkout:" % (co_leaf, before)), text)) else: retcode, revision = utils.run2('git rev-parse HEAD', show_command=False) if retcode: if revision: text = utils.indent(revision.strip(), ' ') if force: if verbose: print "'git rev-parse HEAD' had problems with checkout" \ " '%s'"%co_leaf print " %s" % text print "using original revision %s" % orig_revision return orig_revision else: text = ' (it failed with return code %d)' % retcode raise GiveUp("%s\n%s" % (utils.wrap( "%s: 'git rev-parse HEAD'" " could not determine a revision id for checkout:" % co_leaf), text)) return revision.strip()
def __init__(self, name, role, pkgs_to_install, os_version=None): """Our arguments are: * 'name' - the name of this builder * 'role' - the role to which it belongs * 'pkgs_to_install' - a sequence specifying which packages are to be installed. Each item in the sequence 'pkgs_to_install' can be: * the name of an OS package to install - for instance, 'libxml2-dev' (this is backwards compatible with how this class worked in the past) * a Choice allowing a particular package to be selected according to the operating system. See "muddle doc Choice" for details on the Choice class. Note that a choice resulting in None (i.e., where the default value is None, and the default is selected) will not do anything. If 'os_version' is given, then it will be used as the version name, otherwise the result of calling utils.get_os_version_name() will be used. We also allow a single string, or a single Choice, treated as if they were wrapped in a list. """ super(AptGetBuilder, self).__init__(name, role) actual_packages = [] if os_version is None: os_verson = get_os_version_name() if isinstance(pkgs_to_install, basestring): # Just a single package actual_packages.append(pkgs_to_install) elif isinstance(pkgs_to_install, Choice): # Just a single Choice # Make a choice according to the OS info choice = pkgs_to_install.choose_to_match_os(os_version) if choice is not None: actual_packages.append(choice) else: for pkg in pkgs_to_install: if isinstance(pkg, basestring): actual_packages.append(pkg) elif isinstance(pkg, Choice): # Make a choice according to the OS info choice = pkg.choose_to_match_os(os_version) if choice is not None: actual_packages.append(choice) else: raise GiveUp('%r is not a string or a Choice' % pkg) self.pkgs_to_install = actual_packages
def checkout(self, builder, co_label, verbose=True): """ Check this checkout out of version control. The actual operation we perform is commonly called "clone" in actual version control systems. We retain the name "checkout" because it instantiates a muddle checkout. """ # We want to be in the checkout's parent directory parent_dir, co_leaf = os.path.split( builder.db.get_checkout_path(co_label)) repo = builder.db.get_checkout_repo(co_label) if not repo.pull: raise GiveUp('Failure checking out %s in %s:\n' ' %s does not allow "pull"' % (co_label, parent_dir, repo)) # Be careful - if the parent is 'src/', then it may well exist by now if not os.path.exists(parent_dir): os.makedirs(parent_dir) specific_branch = self.branch_to_follow(builder, co_label) if specific_branch: # The build description told us to follow it, onto this branch # - so let's remember it on the Repository repo = repo.copy_with_changed_branch(specific_branch) options = builder.db.get_checkout_vcs_options(co_label) try: # A complete hack - checkout is kinda meaningless for weld, but # we do want to make sure that the git repo is set up and correct. if repo.vcs == "weld": with Directory(builder.db.root_path): self.vcs.ensure_version(builder, repo, co_leaf, options, verbose) else: with Directory(parent_dir): self.vcs.checkout(repo, co_leaf, options, verbose) except MuddleBug as err: raise MuddleBug('Error checking out %s in %s:\n%s' % (co_label, parent_dir, err)) except GiveUp as err: raise GiveUp('Failure checking out %s in %s:\n%s' % (co_label, parent_dir, err))
def broken_muddle(cmd_list, error=None, endswith=None): try: text = captured_muddle(cmd_list) raise GiveUp('Command unexpectedly worked, returned %r' % text) except CalledProcessError as e: stripped = e.output.strip() if error: check_text(stripped, error) else: if not stripped.endswith(endswith): raise GiveUp( 'Wrong error for "muddle %s"\n' 'Got:\n' ' %s\n' 'Expected it to end:\n %s' % (' '.join(cmd_list), '\n '.join(stripped.splitlines()), '\n '.join(endswith.splitlines()))) print 'Successfully failed' print
def _git_rev_parse_HEAD(self): """ This returns a bare SHA1 object name for the current HEAD """ retcode, revision = utils.run2('git rev-parse HEAD', show_command=False) if retcode: raise GiveUp("'git rev-parse HEAD' failed with return code %d" % retcode) return revision.strip()
def expand_revision(revision): """Given something that names a revision, return its full SHA1. Raises GiveUp if the revision appears non-existent or ambiguous """ rv, out = utils.run2('git rev-parse %s' % revision, show_command=False) if rv: raise GiveUp('Revision "%s" is either non-existant or ambiguous' % revision) return out.strip()
def is_detached_head(): retcode, out = run2('git symbolic-ref -q HEAD') if retcode == 0: # HEAD is a symbolic reference - so not detached return False elif retcode == 1: # HEAD is not a symbolic reference, but a detached HEAD return True else: raise GiveUp( 'Error running "git symbolic-ref -q HEAD" to detect detached HEAD')
def _calculate_revision(self, co_leaf, orig_revision): """ This returns a bare SHA1 object name for orig_revision NB: if 'before' is specified, 'force' is ignored. """ retcode, revision, ignore = utils.run3('git rev-parse %s' % orig_revision) if retcode: if revision: text = utils.indent(revision.strip(), ' ') raise GiveUp("%s\n%s" % (utils.wrap( "%s: 'git rev-parse HEAD'" " could not determine a revision id for checkout:" % co_leaf), text)) else: raise GiveUp("%s\n" % (utils.wrap( "%s: 'git rev-parse HEAD'" " could not determine a revision id for checkout:" % co_leaf))) return revision.strip()
def revision_to_checkout(self, repo, co_leaf, options, force=False, before=None, verbose=True): """ Will be called in the actual checkout's directory. """ raise GiveUp("VCS '%s' cannot calculate a checkout revision" % self.long_name)
def check_nosuch_files(paths, verbose=True): """Given a list of paths, check they do not exist. """ if verbose: flushing_print('++ Checking files do not exist\n') for name in paths: if os.path.exists(name): raise GiveUp('File %s exists' % name) else: if verbose: sys.sydout.write(' -- %s\n' % name) if verbose: flushing_print('++ All named files do not exist\n')
def check_cmd(command, expected='', unsure=False): """Check we get the expected output from 'muddle -n <command>' * If 'expected' is given, then it is a string of the expected labels separated by spaces * If 'unsure' is true, then 'expected' is ignored, and we expect the result of the command to be: 'Not sure what you want to <command>' and an error code. """ retcode, result = run2('{muddle} -n {cmd}'.format(muddle=MUDDLE_BINARY, cmd=command)) result = result.strip() lines = result.split('\n ') if unsure: command_words = command.split(' ') wanted = 'Not sure what you want to {cmd}'.format(cmd=command_words[0]) line0 = lines[0].strip() if retcode: if line0 == wanted: return else: raise GiveUp('Wanted "{0}" but got "{1} and' ' retcode {2}"'.format(wanted, line0, retcode)) else: raise GiveUp('Expecting failure and "{0}",' ' got "{1}"'.format(wanted, line0)) elif retcode: raise GiveUp('Command failed with retcode {0},' ' got unexpected {1}'.format(retcode, result)) lines = lines[1:] # Drop the "explanation" #map(string.strip, lines) got = ' '.join(lines) if got != expected: raise GiveUp('Expected "{0}", got "{1}"'.format(expected, got))
def goto_branch(self, branch, verbose=False): """ Make the named branch the current branch. Will be called in the actual checkout's directory. It is an error if the branch does not exist, in which case a GiveUp exception will be raised. """ retcode, out = utils.run2(['git', 'checkout', branch], show_command=verbose) if retcode: raise GiveUp('Error going to branch "%s": %s' % (branch, out))
def check_exception(testing, fn, args, exception=GiveUp, startswith=None, endswith=None): """Check we get the right sort of exception. """ if not startswith and not endswith: raise ValueError('ERROR TESTING: need startswith or endswith') print testing ok = False try: fn(*args) except exception as e: if startswith: if str(e).startswith(startswith): ok = True else: raise GiveUp('Unexpected %s exception: %s\n' ' (does not start with %r)' % (e.__class__.__name__, e, startswith)) if endswith: if str(e).endswith(endswith): ok = True else: raise GiveUp('Unexpected %s exception: %s\n' ' (does not end with %r)' % (e.__class__.__name__, e, endswith)) if ok: print 'Fails OK' else: if startswith: raise GiveUp('Did not get an exception, so not starting %r' % startswith) else: raise GiveUp('Did not get an exception, so not ending %r' % endswith)