#!/usr/bin/env python
#
#
# Ravenbrook
# <https://www.ravenbrook.com/>
#
# P4-BISECT -- FIND CHANGE THAT INTRODUCED A BUG
#
# Gareth Rees, Ravenbrook Limited, 2014-04-14
#
#
# 1. INTRODUCTION
#
# This script automates (or partly automates) the process of finding,
# by binary search, the change that introduced a bug.
#
# The interface is modelled closely on git-bisect(1).
import argparse
from functools import partial
import json
from os import unlink
import p4
import subprocess
import sys
BISECT_FILE = '.p4-bisect'
def error(msg):
sys.stderr.write(msg)
sys.stderr.write('\n')
exit(1)
def sync(*filespecs):
try:
p4.do('sync', *filespecs)
except p4.Error as e:
if 'file(s) up-to-date' not in e.args[0]:
raise
class State(object):
def __init__(self, **d):
self.filespec = d['filespec']
self.changes = d['changes']
if 'current' in d:
self.current = d['current']
@classmethod
def load(cls):
try:
with open(BISECT_FILE, 'r') as f:
return cls(**json.load(f))
except FileNotFoundError:
error("p4-bisect not in progress here.")
def save(self):
with open(BISECT_FILE, 'w') as f:
json.dump(vars(self), f)
def update(self):
n = len(self.changes)
if n == 0:
print("no changes remaining.".format(**vars(self)))
elif n == 1:
print("{} change remaining: {}.".format(n, self.changes[0]))
elif n == 2:
print("{} changes remaining: [{}, {}]."
.format(n, self.changes[0], self.changes[-1]))
else:
print("{} changes remaining: [{}, ..., {}]."
.format(n, self.changes[0], self.changes[-1]))
if n > 0:
self.current = self.changes[n // 2]
print("Syncing to changelevel {current}.".format(**vars(self)))
sync(*['{}@{}'.format(f, self.current) for f in self.filespec])
self.save()
def help(parser, args):
parser.print_help()
def start(args):
args.filespec = args.filespec or ['...']
changes = sorted(int(c['change']) for c in p4.run('changes', *args.filespec))
if not changes:
error("No changes for {}".format(' '.join(args.filespec)))
if args.good is None:
args.good = changes[0]
if args.bad is None:
args.bad = changes[-1]
state = State(filespec=args.filespec,
changes=[c for c in changes if args.good <= c <= args.bad])
state.update()
def good(args):
state = State.load()
print("Change {current} good.".format(**vars(state)))
state.changes = [c for c in state.changes if c > state.current]
state.update()
def bad(args):
state = State.load()
print("Change {current} bad.".format(**vars(state)))
state.changes = [c for c in state.changes if c < state.current]
state.update()
def skip(args):
state = State.load()
print("Skipping change {current}.".format(**vars(state)))
state.changes.remove(state.current)
state.update()
def reset(args):
state = State.load()
sync(*state.filespec)
unlink(BISECT_FILE)
def run(args):
while True:
state = State.load()
if not state.changes:
break
result = subprocess.call([args.cmd] + args.args)
if result == 0:
good(None)
elif result == 125:
skip(None)
elif 0 < result < 128:
bad(None)
else:
exit(result)
def main(argv):
parser = argparse.ArgumentParser(
prog='p4-bisect', epilog='For help on CMD, use p4-bisect CMD -h')
subparsers = parser.add_subparsers()
a = subparsers.add_parser
help_parser = a('help', help='show this help message')
help_parser.set_defaults(func=partial(help, parser))
start_parser = a('start', help='start a p4-bisect session')
aa = start_parser.add_argument
start_parser.add_argument('-f', '--filespec', action='append',
help='filespec(s) to search')
start_parser.add_argument('good', nargs='?', type=int,
help='known good changelevel')
start_parser.add_argument('bad', nargs='?', type=int,
help='known bad changelevel')
start_parser.set_defaults(func=start)
good_parser = a('good', help='declare current revision good')
good_parser.set_defaults(func=good)
bad_parser = a('bad', help='declare current revision bad')
bad_parser.set_defaults(func=bad)
skip_parser = a('skip', help='skip current revision')
skip_parser.set_defaults(func=skip)
reset_parser = a('reset', help='finish p4-bisect session')
reset_parser.set_defaults(func=reset)
run_parser = a('run', help='run p4-bisect session automatically')
run_parser.add_argument('cmd',
help='command that determines if current '
'changelevel is good or bad')
run_parser.add_argument('args', nargs=argparse.REMAINDER,
help='arguments to pass to cmd')
run_parser.set_defaults(func=run)
args = parser.parse_args(argv[1:])
args.func(args)
if __name__ == '__main__':
main(sys.argv)