--- a/hgkw/keyword.py Sat Sep 10 13:23:41 2011 +0100
+++ b/hgkw/keyword.py Wed Sep 14 16:19:33 2011 +0100
@@ -21,6 +21,14 @@
#
# Binary files are not touched.
#
+# Setup in hgrc:
+#
+# [extensions]
+# # enable extension
+# keyword = /full/path/to/hgkw/keyword.py
+# # or, if script in canonical hgext folder:
+# # keyword =
+#
# Files to act upon/ignore are specified in the [keyword] section.
# Customized keyword template mappings in the [keywordmaps] section.
#
@@ -35,115 +43,211 @@
change history. The mechanism can be regarded as a convenience for the
current user or for archive distribution.
-Keywords expand to the changeset data pertaining to the latest change
-relative to the working directory parent of each file.
+Configuration is done in the [keyword] and [keywordmaps] sections of
+hgrc files.
-Configuration is done in the [keyword], [keywordset] and [keywordmaps]
-sections of hgrc files.
-
-Example::
+Example:
[keyword]
# expand keywords in every python file except those matching "x*"
**.py =
x* = ignore
- [keywordset]
- # prefer svn- over cvs-like default keywordmaps
- svn = True
-
-.. note::
- The more specific you are in your filename patterns the less you
- lose speed in huge repositories.
+NOTE: the more specific you are in your filename patterns the less you
+lose speed in huge repositories.
For [keywordmaps] template mapping and expansion demonstration and
-control run :hg:`kwdemo`. See :hg:`help templates` for a list of
-available templates and filters.
+control run "hg kwdemo".
-Three additional date template filters are provided:
+An additional date template filter {date|utcdate} is provided. It
+returns a date like "2006/09/18 15:13:13".
-:``utcdate``: "2006/09/18 15:13:13"
-:``svnutcdate``: "2006-09-18 15:13:13Z"
-:``svnisodate``: "2006-09-18 08:13:13 -700 (Mon, 18 Sep 2006)"
+The default template mappings (view with "hg kwdemo -d") can be
+replaced with customized keywords and templates. Again, run "hg
+kwdemo" to control the results of your config changes.
-The default template mappings (view with :hg:`kwdemo -d`) can be
-replaced with customized keywords and templates. Again, run
-:hg:`kwdemo` to control the results of your configuration changes.
-
-Before changing/disabling active keywords, you must run :hg:`kwshrink`
-to avoid storing expanded keywords in the change history.
+Before changing/disabling active keywords, run "hg kwshrink" to avoid
+the risk of inadvertently storing expanded keywords in the change
+history.
To force expansion after enabling it, or a configuration change, run
-:hg:`kwexpand`.
+"hg kwexpand".
+
+Also, when committing with the record extension or using mq's qrecord,
+be aware that keywords cannot be updated. Again, run "hg kwexpand" on
+the files in question to update keyword expansions after all changes
+have been checked in.
Expansions spanning more than one line and incremental expansions,
like CVS' $Log$, are not supported. A keyword template map "Log =
{desc}" expands to the first line of the changeset description.
+
+Caveat: With Mercurial versions prior to 4574925db5c0 "hg import" might
+ cause rejects if the patch context contains an active keyword. In that
+ case run "hg kwshrink", and then reimport. Or, better, use
+ bundle/unbundle to share changes.
'''
-from mercurial import commands, context, cmdutil, dispatch, filelog, extensions
-from mercurial import localrepo, match, patch, templatefilters, templater, util
-from mercurial import scmutil
-from mercurial.hgweb import webcommands
-from mercurial.i18n import _
-import os, re, shutil, tempfile
+from mercurial import commands, cmdutil, fancyopts, filelog
+from mercurial import localrepo, patch, revlog, templater, util
+from mercurial.node import nullid, hex
+from mercurial.i18n import gettext as _
+import getopt, os, re, shutil, tempfile, time
commands.optionalrepo += ' kwdemo'
-cmdtable = {}
-command = cmdutil.command(cmdtable)
-
# hg commands that do not act on keywords
-nokwcommands = ('add addremove annotate bundle export grep incoming init log'
- ' outgoing push tip verify convert email glog')
+nokwcommands = ('add addremove annotate bundle copy export grep incoming init'
+ ' log outgoing push rename rollback tip verify'
+ ' convert email glog')
# hg commands that trigger expansion only when writing to working dir,
# not when reading filelog, and unexpand when reading from working dir
-restricted = 'merge kwexpand kwshrink record qrecord resolve transplant'
-
-# names of extensions using dorecord
-recordextensions = 'record'
-
-colortable = {
- 'kwfiles.enabled': 'green bold',
- 'kwfiles.deleted': 'cyan bold underline',
- 'kwfiles.enabledunknown': 'green',
- 'kwfiles.ignored': 'bold',
- 'kwfiles.ignoredunknown': 'none'
-}
+restricted = ('merge record resolve qfold qimport qnew qpush qrefresh qrecord'
+ ' transplant')
-# date like in cvs' $Date
-def utcdate(text):
- ''':utcdate: Date. Returns a UTC-date in this format: "2009/08/18 11:00:13".
- '''
- return util.datestr((text[0], 0), '%Y/%m/%d %H:%M:%S')
-# date like in svn's $Date
-def svnisodate(text):
- ''':svnisodate: Date. Returns a date in this format: "2009-08-18 13:00:13
- +0200 (Tue, 18 Aug 2009)".
- '''
- return util.datestr(text, '%Y-%m-%d %H:%M:%S %1%2 (%a, %d %b %Y)')
-# date like in svn's $Id
-def svnutcdate(text):
- ''':svnutcdate: Date. Returns a UTC-date in this format: "2009-08-18
- 11:00:13Z".
- '''
- return util.datestr((text[0], 0), '%Y-%m-%d %H:%M:%SZ')
+# provide cvs-like UTC date filter
+utcdate = lambda x: time.strftime('%Y/%m/%d %H:%M:%S',
+ time.gmtime(float(x[0])))
-templatefilters.filters.update({'utcdate': utcdate,
- 'svnisodate': svnisodate,
- 'svnutcdate': svnutcdate})
+def textsafe(s):
+ '''Safe version of util.binary with reversed logic.
+ Note: argument may not be None, which is allowed for util.binary.'''
+ return '\0' not in s
# make keyword tools accessible
kwtools = {'templater': None, 'hgcmd': ''}
-def _defaultkwmaps(ui):
- '''Returns default keywordmaps according to keywordset configuration.'''
+# monkeypatch argument parsing
+# due to backwards compatibility this can't be done in uisetup
+# uisetup introduced with extensions module in 930ed513c864
+def _kwdispatch_parse(ui, args):
+ '''Monkeypatch dispatch._parse to obtain running hg command.'''
+ cmd, func, args, options, cmdoptions = _dispatch_parse(ui, args)
+ kwtools['hgcmd'] = cmd
+ return cmd, func, args, options, cmdoptions
+
+try:
+ # cmdutil.parse moves to dispatch._parse in 18a9fbb5cd78
+ from mercurial import dispatch
+ _dispatch_parse = dispatch._parse
+ dispatch._parse = _kwdispatch_parse
+except ImportError:
+ try:
+ # commands.parse moves to cmdutil.parse in 0c61124ad877
+ _dispatch_parse = cmdutil.parse
+ cmdutil.parse = _kwdispatch_parse
+ except AttributeError:
+ _dispatch_parse = commands.parse
+ commands.parse = _kwdispatch_parse
+
+try:
+ # templatefilters module introduced in 9f1e6ab76069
+ from mercurial import templatefilters
+ template_filters = templatefilters.filters
+ template_firstline = templatefilters.firstline
+except ImportError:
+ template_filters = templater.common_filters
+ template_firstline = templater.firstline
+
+def _wwrite(repo, f, data, mf):
+ '''Makes repo.wwrite backwards compatible.'''
+ # 656e06eebda7 removed file descriptor argument
+ # 67982d3ee76c added flags argument
+ try:
+ repo.wwrite(f, data, mf.flags(f))
+ except (AttributeError, TypeError):
+ repo.wwrite(f, data)
+
+def _normal(repo, files):
+ '''Backwards compatible repo.dirstate.normal/update.'''
+ # 6fd953d5faea introduced dirstate.normal()
+ try:
+ for f in files:
+ repo.dirstate.normal(f)
+ except AttributeError:
+ repo.dirstate.update(files, 'n')
+
+def _pathto(repo, f, cwd=None):
+ '''kwfiles behaves similar to status, using pathto since 78b6add1f966.'''
+ try:
+ return repo.pathto(f, cwd)
+ except AttributeError:
+ return f
+
+'''Default match argument for commit, depending on version.'''
+if hasattr(cmdutil, 'match'):
+ _defmatch = None
+else:
+ _defmatch = util.always
+
+# commands.parse/cmdutil.parse returned nothing for
+# "hg diff --rev" before 88803a69b24a due to bug in fancyopts
+def _fancyopts(args, options, state):
+ '''Fixed fancyopts from a9b7e425674f.'''
+ namelist = []
+ shortlist = ''
+ argmap = {}
+ defmap = {}
+
+ for short, name, default, comment in options:
+ # convert opts to getopt format
+ oname = name
+ name = name.replace('-', '_')
+
+ argmap['-' + short] = argmap['--' + oname] = name
+ defmap[name] = default
+
+ # copy defaults to state
+ if isinstance(default, list):
+ state[name] = default[:]
+ elif callable(default):
+ print "whoa", name, default
+ state[name] = None
+ else:
+ state[name] = default
+
+ # does it take a parameter?
+ if not (default is None or default is True or default is False):
+ if short: short += ':'
+ if oname: oname += '='
+ if short:
+ shortlist += short
+ if name:
+ namelist.append(oname)
+
+ # parse arguments
+ opts, args = getopt.getopt(args, shortlist, namelist)
+
+ # transfer result to state
+ for opt, val in opts:
+ name = argmap[opt]
+ t = type(defmap[name])
+ if t is type(fancyopts):
+ state[name] = defmap[name](val)
+ elif t is type(1):
+ state[name] = int(val)
+ elif t is type(''):
+ state[name] = val
+ elif t is type([]):
+ state[name].append(val)
+ elif t is type(None) or t is type(False):
+ state[name] = True
+
+ # return unparsed args
+ return args
+
+fancyopts.fancyopts = _fancyopts
+
+
+class kwtemplater(object):
+ '''
+ Sets up keyword templates, corresponding keyword regex, and
+ provides keyword substitution functions.
+ '''
templates = {
'Revision': '{node|short}',
'Author': '{author|user}',
- }
- kwsets = ({
'Date': '{date|utcdate}',
'RCSfile': '{file|basename},v',
'RCSFile': '{file|basename},v', # kept for backwards compatibility
@@ -151,157 +255,137 @@
'Source': '{root}/{file},v',
'Id': '{file|basename},v {node|short} {date|utcdate} {author|user}',
'Header': '{root}/{file},v {node|short} {date|utcdate} {author|user}',
- }, {
- 'Date': '{date|svnisodate}',
- 'Id': '{file|basename},v {node|short} {date|svnutcdate} {author|user}',
- 'LastChangedRevision': '{node|short}',
- 'LastChangedBy': '{author|user}',
- 'LastChangedDate': '{date|svnisodate}',
- })
- templates.update(kwsets[ui.configbool('keywordset', 'svn')])
- return templates
-
-def _shrinktext(text, subfunc):
- '''Helper for keyword expansion removal in text.
- Depending on subfunc also returns number of substitutions.'''
- return subfunc(r'$\1$', text)
-
-def _preselect(wstatus, changed):
- '''Retrieves modfied and added files from a working directory state
- and returns the subset of each contained in given changed files
- retrieved from a change context.'''
- modified, added = wstatus[:2]
- modified = [f for f in modified if f in changed]
- added = [f for f in added if f in changed]
- return modified, added
-
-
-class kwtemplater(object):
- '''
- Sets up keyword templates, corresponding keyword regex, and
- provides keyword substitution functions.
- '''
+ }
def __init__(self, ui, repo, inc, exc):
self.ui = ui
self.repo = repo
- self.match = match.match(repo.root, '', [], inc, exc)
+ self.matcher = util.matcher(repo.root, inc=inc, exc=exc)[1]
self.restrict = kwtools['hgcmd'] in restricted.split()
- self.record = False
kwmaps = self.ui.configitems('keywordmaps')
if kwmaps: # override default templates
- self.templates = dict((k, templater.parsestring(v, False))
- for k, v in kwmaps)
- else:
- self.templates = _defaultkwmaps(self.ui)
+ kwmaps = [(k, templater.parsestring(v, False))
+ for (k, v) in kwmaps]
+ self.templates = dict(kwmaps)
+ escaped = map(re.escape, self.templates.keys())
+ kwpat = r'\$(%s)(: [^$\n\r]*? )??\$' % '|'.join(escaped)
+ self.re_kw = re.compile(kwpat)
- @util.propertycache
- def escape(self):
- '''Returns bar-separated and escaped keywords.'''
- return '|'.join(map(re.escape, self.templates.keys()))
+ template_filters['utcdate'] = utcdate
+ self.ct = self._changeset_templater()
- @util.propertycache
- def rekw(self):
- '''Returns regex for unexpanded keywords.'''
- return re.compile(r'\$(%s)\$' % self.escape)
+ def _changeset_templater(self):
+ '''Backwards compatible cmdutil.changeset_templater.'''
+ # before 1e0b94cfba0e there was an extra "brinfo" argument
+ try:
+ return cmdutil.changeset_templater(self.ui, self.repo,
+ False, '', False)
+ except TypeError:
+ return cmdutil.changeset_templater(self.ui, self.repo,
+ False, None, '', False)
- @util.propertycache
- def rekwexp(self):
- '''Returns regex for expanded keywords.'''
- return re.compile(r'\$(%s): [^$\n\r]*? \$' % self.escape)
+ def getnode(self, path, fnode):
+ '''Derives changenode from file path and filenode.'''
+ # used by kwfilelog.read and kwexpand
+ c = self.repo.filectx(path, fileid=fnode)
+ return c.node()
- def substitute(self, data, path, ctx, subfunc):
+ def substitute(self, data, path, node, subfunc):
'''Replaces keywords in data with expanded template.'''
def kwsub(mobj):
kw = mobj.group(1)
- ct = cmdutil.changeset_templater(self.ui, self.repo,
- False, None, '', False)
- ct.use_template(self.templates[kw])
+ self.ct.use_template(self.templates[kw])
self.ui.pushbuffer()
- ct.show(ctx, root=self.repo.root, file=path)
- ekw = templatefilters.firstline(self.ui.popbuffer())
- return '$%s: %s $' % (kw, ekw)
+ self.ct.show(changenode=node, root=self.repo.root, file=path)
+ return '$%s: %s $' % (kw, template_firstline(self.ui.popbuffer()))
return subfunc(kwsub, data)
- def linkctx(self, path, fileid):
- '''Similar to filelog.linkrev, but returns a changectx.'''
- return self.repo.filectx(path, fileid=fileid).changectx()
-
def expand(self, path, node, data):
'''Returns data with keywords expanded.'''
- if not self.restrict and self.match(path) and not util.binary(data):
- ctx = self.linkctx(path, node)
- return self.substitute(data, path, ctx, self.rekw.sub)
+ if not self.restrict and self.matcher(path) and textsafe(data):
+ changenode = self.getnode(path, node)
+ return self.substitute(data, path, changenode, self.re_kw.sub)
return data
- def iskwfile(self, cand, ctx):
- '''Returns subset of candidates which are configured for keyword
- expansion are not symbolic links.'''
- return [f for f in cand if self.match(f) and not 'l' in ctx.flags(f)]
+ def iskwfile(self, path, islink):
+ '''Returns true if path matches [keyword] pattern
+ and is not a symbolic link.
+ Caveat: localrepository._link fails on Windows.'''
+ return self.matcher(path) and not islink(path)
- def overwrite(self, ctx, candidates, lookup, expand, rekw=False):
+ def overwrite(self, node, expand, candidates):
'''Overwrites selected files expanding/shrinking keywords.'''
- if self.restrict or lookup or self.record: # exclude kw_copy
- candidates = self.iskwfile(candidates, ctx)
- if not candidates:
- return
- kwcmd = self.restrict and lookup # kwexpand/kwshrink
- if self.restrict or expand and lookup:
+ # repo[changeid] introduced in f6c00b17387c
+ if node is not None: # commit
+ try:
+ ctx = self.repo[node]
+ except TypeError:
+ ctx = self.repo.changectx(node)
mf = ctx.manifest()
- if self.restrict or rekw:
- re_kw = self.rekw
- else:
- re_kw = self.rekwexp
- if expand:
- msg = _('overwriting %s expanding keywords\n')
+ candidates = [f for f in ctx.files() if f in mf]
+ else: # kwexpand/kwshrink
+ try:
+ ctx = self.repo['.']
+ except TypeError:
+ ctx = self.repo.changectx()
+ mf = ctx.manifest()
+ if hasattr(ctx, 'flags'):
+ # 51b0e799352f
+ islink = lambda p: 'l' in ctx.flags(p)
else:
- msg = _('overwriting %s shrinking keywords\n')
- for f in candidates:
- if self.restrict:
- data = self.repo.file(f).read(mf[f])
- else:
- data = self.repo.wread(f)
- if util.binary(data):
- continue
+ islink = mf.linkf
+ candidates = [f for f in candidates if self.iskwfile(f, islink)]
+ if candidates:
+ self.restrict = True # do not expand when reading
if expand:
- if lookup:
- ctx = self.linkctx(f, mf[f])
- data, found = self.substitute(data, f, ctx, re_kw.subn)
- elif self.restrict:
- found = re_kw.search(data)
+ msg = _('overwriting %s expanding keywords\n')
else:
- data, found = _shrinktext(data, re_kw.subn)
- if found:
- self.ui.note(msg % f)
- fp = self.repo.wopener(f, "wb", atomictemp=True)
- fp.write(data)
- fp.close()
- if kwcmd:
- self.repo.dirstate.normal(f)
- elif self.record:
- self.repo.dirstate.normallookup(f)
+ msg = _('overwriting %s shrinking keywords\n')
+ overwritten = []
+ for f in candidates:
+ fp = self.repo.file(f)
+ data = fp.read(mf[f])
+ if not textsafe(data):
+ continue
+ if expand:
+ changenode = node or self.getnode(f, mf[f])
+ data, found = self.substitute(data, f, changenode,
+ self.re_kw.subn)
+ else:
+ found = self.re_kw.search(data)
+ if found:
+ self.ui.note(msg % f)
+ fpath = self.repo.wjoin(f)
+ mode = os.lstat(fpath).st_mode
+ self.repo.wwrite(f, data, mf.flags(f))
+ os.chmod(fpath, mode)
+ overwritten.append(f)
+ _normal(self.repo, overwritten)
+ self.restrict = False
+
+ def shrinktext(self, text):
+ '''Unconditionally removes all keyword substitutions from text.'''
+ return self.re_kw.sub(r'$\1$', text)
def shrink(self, fname, text):
'''Returns text with all keyword substitutions removed.'''
- if self.match(fname) and not util.binary(text):
- return _shrinktext(text, self.rekwexp.sub)
+ if self.matcher(fname) and textsafe(text):
+ return self.shrinktext(text)
return text
def shrinklines(self, fname, lines):
'''Returns lines with keyword substitutions removed.'''
- if self.match(fname):
+ if self.matcher(fname):
text = ''.join(lines)
- if not util.binary(text):
- return _shrinktext(text, self.rekwexp.sub).splitlines(True)
+ if textsafe(text):
+ return self.shrinktext(text).splitlines(True)
return lines
def wread(self, fname, data):
'''If in restricted mode returns data read from wdir with
keyword substitutions removed.'''
- if self.restrict:
- return self.shrink(fname, data)
- return data
+ return self.restrict and self.shrink(fname, data) or data
class kwfilelog(filelog.filelog):
'''
@@ -316,8 +400,6 @@
def read(self, node):
'''Expands keywords when reading filelog.'''
data = super(kwfilelog, self).read(node)
- if self.renamed(node):
- return data
return self.kwt.expand(self.path, node, data)
def add(self, text, meta, tr, link, p1=None, p2=None):
@@ -328,39 +410,48 @@
def cmp(self, node, text):
'''Removes keyword substitutions for comparison.'''
text = self.kwt.shrink(self.path, text)
- return super(kwfilelog, self).cmp(node, text)
+ if self.renamed(node):
+ t2 = super(kwfilelog, self).read(node)
+ return t2 != text
+ return revlog.revlog.cmp(self, node, text)
-def _status(ui, repo, wctx, kwt, *pats, **opts):
+def _status(ui, repo, kwt, unknown, *pats, **opts):
'''Bails out if [keyword] configuration is not active.
Returns status of working directory.'''
if kwt:
- return repo.status(match=scmutil.match(wctx, pats, opts), clean=True,
- unknown=opts.get('unknown') or opts.get('all'))
+ try:
+ # 0159b7a36184 ff.
+ matcher = cmdutil.match(repo, pats, opts)
+ try:
+ # 4faaa0535ea7
+ return repo.status(match=matcher, unknown=unknown, clean=True)
+ except TypeError:
+ return repo.status(match=matcher, list_clean=True)
+ except AttributeError:
+ files, match, anypats = cmdutil.matchpats(repo, pats, opts)
+ return repo.status(files=files, match=match, list_clean=True)
if ui.configitems('keyword'):
raise util.Abort(_('[keyword] patterns cannot match'))
raise util.Abort(_('no [keyword] patterns configured'))
def _kwfwrite(ui, repo, expand, *pats, **opts):
'''Selects files and passes them to kwtemplater.overwrite.'''
- wctx = repo[None]
- if len(wctx.parents()) > 1:
+ if repo.dirstate.parents()[1] != nullid:
raise util.Abort(_('outstanding uncommitted merge'))
kwt = kwtools['templater']
- wlock = repo.wlock()
+ status = _status(ui, repo, kwt, False, *pats, **opts)
+ modified, added, removed, deleted, unknown, ignored, clean = status
+ if modified or added or removed or deleted:
+ raise util.Abort(_('outstanding uncommitted changes'))
+ wlock = lock = None
try:
- status = _status(ui, repo, wctx, kwt, *pats, **opts)
- modified, added, removed, deleted, unknown, ignored, clean = status
- if modified or added or removed or deleted:
- raise util.Abort(_('outstanding uncommitted changes'))
- kwt.overwrite(wctx, clean, True, expand)
+ wlock = repo.wlock()
+ lock = repo.lock()
+ kwt.overwrite(None, expand, clean)
finally:
- wlock.release()
+ del wlock, lock
-@command('kwdemo',
- [('d', 'default', None, _('show default keyword template maps')),
- ('f', 'rcfile', '',
- _('read maps from rcfile'), _('FILE'))],
- _('hg kwdemo [-d] [-f RCFILE] [TEMPLATEMAP]...'))
+
def demo(ui, repo, *args, **opts):
'''print [keywordmaps] configuration and an expansion example
@@ -371,22 +462,19 @@
and using -f/--rcfile to source an external hgrc file.
Use -d/--default to disable current configuration.
-
- See :hg:`help templates` for information on templates and filters.
'''
def demoitems(section, items):
+ items.sort()
ui.write('[%s]\n' % section)
- for k, v in sorted(items):
+ for k, v in items:
ui.write('%s = %s\n' % (k, v))
+ kwstatus = 'current'
fn = 'demo.txt'
tmpdir = tempfile.mkdtemp('', 'kwdemo.')
ui.note(_('creating temporary repository at %s\n') % tmpdir)
repo = localrepo.localrepository(ui, tmpdir, True)
ui.setconfig('keyword', fn, '')
- svn = ui.configbool('keywordset', 'svn')
- # explicitly set keywordset for demo output
- ui.setconfig('keywordset', 'svn', svn)
uikwmaps = ui.configitems('keywordmaps')
if args or opts.get('rcfile'):
@@ -394,10 +482,7 @@
if uikwmaps:
ui.status(_('\textending current template maps\n'))
if opts.get('default') or not uikwmaps:
- if svn:
- ui.status(_('\toverriding default svn keywordset\n'))
- else:
- ui.status(_('\toverriding default cvs keywordset\n'))
+ ui.status(_('\toverriding default template maps\n'))
if opts.get('rcfile'):
ui.readconfig(opts.get('rcfile'))
if args:
@@ -409,34 +494,37 @@
ui.readconfig(repo.join('hgrc'))
kwmaps = dict(ui.configitems('keywordmaps'))
elif opts.get('default'):
- if svn:
- ui.status(_('\n\tconfiguration using default svn keywordset\n'))
- else:
- ui.status(_('\n\tconfiguration using default cvs keywordset\n'))
- kwmaps = _defaultkwmaps(ui)
+ ui.status(_('\n\tconfiguration using default keyword template maps\n'))
+ kwmaps = kwtemplater.templates
if uikwmaps:
ui.status(_('\tdisabling current template maps\n'))
for k, v in kwmaps.iteritems():
ui.setconfig('keywordmaps', k, v)
else:
ui.status(_('\n\tconfiguration using current keyword template maps\n'))
- if uikwmaps:
- kwmaps = dict(uikwmaps)
- else:
- kwmaps = _defaultkwmaps(ui)
+ kwmaps = dict(uikwmaps) or kwtemplater.templates
- uisetup(ui)
reposetup(ui, repo)
- ui.write('[extensions]\nkeyword =\n')
+ for k, v in ui.configitems('extensions'):
+ if k.endswith('keyword'):
+ extension = '%s = %s' % (k, v)
+ break
+ ui.status(_('\n\tconfig using %s keyword template maps\n') % kwstatus)
+ ui.write('[extensions]\n%s\n' % extension)
demoitems('keyword', ui.configitems('keyword'))
- demoitems('keywordset', ui.configitems('keywordset'))
- demoitems('keywordmaps', kwmaps.iteritems())
- keywords = '$' + '$\n$'.join(sorted(kwmaps.keys())) + '$\n'
- repo.wopener.write(fn, keywords)
- repo[None].add([fn])
+ demoitems('keywordmaps', kwmaps.items())
+ kwkeys = kwmaps.keys()
+ kwkeys.sort()
+ keywords = '$' + '$\n$'.join(kwkeys) + '$\n'
+ repo.wopener(fn, 'w').write(keywords)
+ repo.add([fn])
ui.note(_('\nkeywords written to %s:\n') % fn)
ui.note(keywords)
- repo.dirstate.setbranch('demobranch')
+ try:
+ repo.dirstate.setbranch('demobranch')
+ except AttributeError:
+ # before 7e1c8a565a4f
+ repo.opener('branch', 'w').write('demobranch\n')
for name, cmd in ui.configitems('hooks'):
if name.split('.', 1)[0].find('commit') > -1:
repo.ui.setconfig('hooks', name, '')
@@ -447,7 +535,6 @@
ui.write(repo.wread(fn))
shutil.rmtree(tmpdir, ignore_errors=True)
-@command('kwexpand', commands.walkopts, _('hg kwexpand [OPTION]... [FILE]...'))
def expand(ui, repo, *pats, **opts):
'''expand keywords in the working directory
@@ -458,12 +545,6 @@
# 3rd argument sets expansion to True
_kwfwrite(ui, repo, True, *pats, **opts)
-@command('kwfiles',
- [('A', 'all', None, _('show keyword status flags of all files')),
- ('i', 'ignore', None, _('show files excluded from expansion')),
- ('u', 'unknown', None, _('only show unknown (not tracked) files')),
- ] + commands.walkopts,
- _('hg kwfiles [OPTION]... [FILE]...'))
def files(ui, repo, *pats, **opts):
'''show files configured for keyword expansion
@@ -474,11 +555,11 @@
execution by including only files that are actual candidates for
expansion.
- See :hg:`help keyword` on how to construct patterns both for
+ See "hg help keyword" on how to construct patterns both for
inclusion and exclusion of files.
With -A/--all and -v/--verbose the codes used to show the status
- of files are::
+ of files are:
K = keyword expansion candidate
k = keyword expansion candidate (not tracked)
@@ -486,35 +567,48 @@
i = ignored (not tracked)
'''
kwt = kwtools['templater']
- wctx = repo[None]
- status = _status(ui, repo, wctx, kwt, *pats, **opts)
+ status = _status(ui, repo, kwt, *pats, **opts)
cwd = pats and repo.getcwd() or ''
modified, added, removed, deleted, unknown, ignored, clean = status
files = []
if not opts.get('unknown') or opts.get('all'):
- files = sorted(modified + added + clean)
- kwfiles = kwt.iskwfile(files, wctx)
- kwdeleted = kwt.iskwfile(deleted, wctx)
- kwunknown = kwt.iskwfile(unknown, wctx)
+ try:
+ # f67d1468ac50
+ files = util.sort(modified + added + clean)
+ except AttributeError:
+ files = modified + added + clean
+ files.sort()
+ try:
+ # f6c00b17387c
+ wctx = repo[None]
+ except TypeError:
+ wctx = repo.workingctx()
+ if hasattr(wctx, 'flags'):
+ islink = lambda p: 'l' in wctx.flags(p)
+ elif hasattr(wctx, 'fileflags'):
+ islink = lambda p: 'l' in wctx.fileflags(p)
+ else:
+ mf = wctx.manifest()
+ islink = mf.linkf
+ kwfiles = [f for f in files if kwt.iskwfile(f, islink)]
+ kwunknown = [f for f in unknown if kwt.iskwfile(f, islink)]
if not opts.get('ignore') or opts.get('all'):
- showfiles = kwfiles, kwdeleted, kwunknown
+ showfiles = kwfiles, kwunknown
else:
- showfiles = [], [], []
+ showfiles = [], []
if opts.get('all') or opts.get('ignore'):
showfiles += ([f for f in files if f not in kwfiles],
[f for f in unknown if f not in kwunknown])
- kwlabels = 'enabled deleted enabledunknown ignored ignoredunknown'.split()
- kwstates = zip('K!kIi', showfiles, kwlabels)
- for char, filenames, kwstate in kwstates:
+ for char, filenames in zip('KkIi', showfiles):
fmt = (opts.get('all') or ui.verbose) and '%s %%s\n' % char or '%s\n'
for f in filenames:
- ui.write(fmt % repo.pathto(f, cwd), label='kwfiles.' + kwstate)
+ ui.write(fmt % _pathto(repo, f, cwd))
-@command('kwshrink', commands.walkopts, _('hg kwshrink [OPTION]... [FILE]...'))
def shrink(ui, repo, *pats, **opts):
'''revert expanded keywords in the working directory
- Must be run before changing/disabling active keywords.
+ Run before changing/disabling active keywords or if you experience
+ problems with "hg import" or "hg merge".
kwshrink refuses to run if given files contain local changes.
'''
@@ -522,17 +616,6 @@
_kwfwrite(ui, repo, False, *pats, **opts)
-def uisetup(ui):
- ''' Monkeypatches dispatch._parse to retrieve user command.'''
-
- def kwdispatch_parse(orig, ui, args):
- '''Monkeypatch dispatch._parse to obtain running hg command.'''
- cmd, func, args, options, cmdoptions = orig(ui, args)
- kwtools['hgcmd'] = cmd
- return cmd, func, args, options, cmdoptions
-
- extensions.wrapfunction(dispatch, '_parse', kwdispatch_parse)
-
def reposetup(ui, repo):
'''Sets up repo as kwrepo for keyword substitution.
Overrides file method to return kwfilelog instead of filelog
@@ -543,7 +626,7 @@
try:
if (not repo.local() or kwtools['hgcmd'] in nokwcommands.split()
- or '.hg' in util.splitpath(repo.root)
+ or '.hg' in repo.root.split(os.sep)
or repo._url.startswith('bundle:')):
return
except AttributeError:
@@ -570,132 +653,181 @@
data = super(kwrepo, self).wread(filename)
return kwt.wread(filename, data)
- def commit(self, *args, **opts):
- # use custom commitctx for user commands
- # other extensions can still wrap repo.commitctx directly
- self.commitctx = self.kwcommitctx
+ def _commit(self, files, text, user, date, match, force, lock, wlock,
+ force_editor, p1, p2, extra, empty_ok):
+ '''Private commit wrapper for backwards compatibility.'''
try:
- return super(kwrepo, self).commit(*args, **opts)
- finally:
- del self.commitctx
+ return super(kwrepo,
+ self).commit(files=files, text=text,
+ user=user, date=date, match=match,
+ force=force, lock=lock, wlock=wlock,
+ force_editor=force_editor,
+ p1=p1, p2=p2, extra=extra)
+ except TypeError:
+ try:
+ return super(kwrepo,
+ self).commit(files=files, text=text,
+ user=user, date=date,
+ match=match, force=force,
+ force_editor=force_editor,
+ p1=p1, p2=p2, extra=extra,
+ empty_ok=empty_ok)
+ except TypeError:
+ return super(kwrepo,
+ self).commit(files=files, text=text,
+ user=user, date=date,
+ match=match, force=force,
+ force_editor=force_editor,
+ p1=p1, p2=p2, extra=extra)
- def kwcommitctx(self, ctx, error=False):
- n = super(kwrepo, self).commitctx(ctx, error)
- # no lock needed, only called from repo.commit() which already locks
- if not kwt.record:
- restrict = kwt.restrict
- kwt.restrict = True
- kwt.overwrite(self[n], sorted(ctx.added() + ctx.modified()),
- False, True)
- kwt.restrict = restrict
- return n
+ def commit(self, files=None, text='', user=None, date=None,
+ match=_defmatch, force=False, lock=None, wlock=None,
+ force_editor=False, p1=None, p2=None, extra={},
+ empty_ok=False):
+ # (w)lock arguments removed in 126f527b3ba3
+ # so they are None or what was passed to commit
+ # use private _(w)lock for deletion
+ _lock = lock
+ _wlock = wlock
+ del wlock, lock
+ _p1 = _p2 = None
+ try:
+ if not _wlock:
+ _wlock = self.wlock()
+ if not _lock:
+ _lock = self.lock()
+ # store and postpone commit hooks
+ commithooks = {}
+ for name, cmd in ui.configitems('hooks'):
+ if name.split('.', 1)[0] == 'commit':
+ commithooks[name] = cmd
+ ui.setconfig('hooks', name, '')
+ if commithooks:
+ # store parents for commit hook environment
+ if p1 is None:
+ _p1, _p2 = self.dirstate.parents()
+ else:
+ _p1, _p2 = p1, p2 or nullid
+ _p1 = hex(_p1)
+ if _p2 == nullid:
+ _p2 = ''
+ else:
+ _p2 = hex(_p2)
- def rollback(self, dryrun=False):
- wlock = self.wlock()
- try:
- if not dryrun:
- changed = self['.'].files()
- ret = super(kwrepo, self).rollback(dryrun)
- if not dryrun:
- ctx = self['.']
- modified, added = _preselect(self[None].status(), changed)
- kwt.overwrite(ctx, modified, True, True)
- kwt.overwrite(ctx, added, True, False)
- return ret
+ n = self._commit(files, text, user, date, match, force, _lock,
+ _wlock, force_editor, p1, p2, extra, empty_ok)
+
+ # restore commit hooks
+ for name, cmd in commithooks.iteritems():
+ ui.setconfig('hooks', name, cmd)
+ if n is not None:
+ kwt.overwrite(n, True, None)
+ self.hook('commit', node=n, parent1=_p1, parent2=_p2)
+ return n
finally:
- wlock.release()
+ del _wlock, _lock
+
+ repo.__class__ = kwrepo
# monkeypatches
- def kwpatchfile_init(orig, self, ui, gp, backend, store, eolmode=None):
- '''Monkeypatch/wrap patch.patchfile.__init__ to avoid
- rejects or conflicts due to expanded keywords in working dir.'''
- orig(self, ui, gp, backend, store, eolmode)
- # shrink keywords read from working dir
- self.lines = kwt.shrinklines(self.fname, self.lines)
+ try:
+ # avoid spurious rejects if patchfile is available
+ def kwpatchfile_init(self, ui, fname, missing=False):
+ '''Monkeypatch/wrap patch.patchfile.__init__ to avoid
+ rejects or conflicts due to expanded keywords in working dir.'''
+ try:
+ patchfile_init(self, ui, fname, missing)
+ except TypeError:
+ # "missing" arg added in e90e72c6b4c7
+ patchfile_init(self, ui, fname)
+ self.lines = kwt.shrinklines(self.fname, self.lines)
- def kw_diff(orig, repo, node1=None, node2=None, match=None, changes=None,
- opts=None, prefix=''):
- '''Monkeypatch patch.diff to avoid expansion.'''
- kwt.restrict = True
- return orig(repo, node1, node2, match, changes, opts, prefix)
-
- def kwweb_skip(orig, web, req, tmpl):
- '''Wraps webcommands.x turning off keyword expansion.'''
- kwt.match = util.never
- return orig(web, req, tmpl)
+ patchfile_init = patch.patchfile.__init__
+ patch.patchfile.__init__ = kwpatchfile_init
+ except AttributeError:
+ pass
- def kw_copy(orig, ui, repo, pats, opts, rename=False):
- '''Wraps cmdutil.copy so that copy/rename destinations do not
- contain expanded keywords.
- Note that the source of a regular file destination may also be a
- symlink:
- hg cp sym x -> x is symlink
- cp sym x; hg cp -A sym x -> x is file (maybe expanded keywords)
- For the latter we have to follow the symlink to find out whether its
- target is configured for expansion and we therefore must unexpand the
- keywords in the destination.'''
- orig(ui, repo, pats, opts, rename)
- if opts.get('dry_run'):
- return
- wctx = repo[None]
- cwd = repo.getcwd()
+ def kw_diff(repo, node1=None, node2=None, files=None, match=_defmatch,
+ fp=None, changes=None, opts=None):
+ # only expand if comparing against working dir
+ if node2 is not None:
+ kwt.matcher = util.never
+ elif node1 is not None and node1 != repo.dirstate.parents()[0]:
+ kwt.restrict = True
+ try:
+ patch_diff(repo, node1=node1, node2=node2, files=files,
+ match=match, fp=fp, changes=changes, opts=opts)
+ except TypeError:
+ patch_diff(repo, node1=node1, node2=node2, match=match, fp=fp,
+ changes=changes, opts=opts)
- def haskwsource(dest):
- '''Returns true if dest is a regular file and configured for
- expansion or a symlink which points to a file configured for
- expansion. '''
- source = repo.dirstate.copied(dest)
- if 'l' in wctx.flags(source):
- source = scmutil.canonpath(repo.root, cwd,
- os.path.realpath(source))
- return kwt.match(source)
+ patch_diff = patch.diff
+ if not kwt.restrict:
+ patch.diff = kw_diff
+
+ try:
+ from mercurial.hgweb import webcommands
+ def kwweb_annotate(web, req, tmpl):
+ '''Wraps webcommands.annotate turning off keyword expansion.'''
+ kwt.matcher = util.never
+ return webcommands_annotate(web, req, tmpl)
+
+ def kwweb_changeset(web, req, tmpl):
+ '''Wraps webcommands.changeset turning off keyword expansion.'''
+ kwt.matcher = util.never
+ return webcommands_changeset(web, req, tmpl)
- candidates = [f for f in repo.dirstate.copies() if
- not 'l' in wctx.flags(f) and haskwsource(f)]
- kwt.overwrite(wctx, candidates, False, False)
+ def kwweb_filediff(web, req, tmpl):
+ '''Wraps webcommands.filediff turning off keyword expansion.'''
+ kwt.matcher = util.never
+ return webcommands_filediff(web, req, tmpl)
+
+ webcommands_annotate = webcommands.annotate
+ webcommands_changeset = webcommands.changeset
+ webcommands_filediff = webcommands.filediff
+
+ webcommands.annotate = kwweb_annotate
+ webcommands.changeset = webcommands.rev = kwweb_changeset
+ webcommands.filediff = webcommands.diff = kwweb_filediff
- def kw_dorecord(orig, ui, repo, commitfunc, *pats, **opts):
- '''Wraps record.dorecord expanding keywords after recording.'''
- wlock = repo.wlock()
- try:
- # record returns 0 even when nothing has changed
- # therefore compare nodes before and after
- kwt.record = True
- ctx = repo['.']
- wstatus = repo[None].status()
- ret = orig(ui, repo, commitfunc, *pats, **opts)
- recctx = repo['.']
- if ctx != recctx:
- modified, added = _preselect(wstatus, recctx.files())
- kwt.restrict = False
- kwt.overwrite(recctx, modified, False, True)
- kwt.overwrite(recctx, added, False, True, True)
- kwt.restrict = True
- return ret
- finally:
- wlock.release()
+ except ImportError:
+ from mercurial.hgweb.hgweb_mod import hgweb
+ def kwweb_do_annotate(self, req):
+ kwt.matcher = util.never
+ hgweb_do_annotate(self, req)
+
+ def kwweb_do_changeset(self, req):
+ kwt.matcher = util.never
+ hgweb_do_changeset(self, req)
+
+ def kwweb_do_filediff(self, req):
+ kwt.matcher = util.never
+ hgweb_do_filediff(self, req)
- def kwfilectx_cmp(orig, self, fctx):
- # keyword affects data size, comparing wdir and filelog size does
- # not make sense
- if (fctx._filerev is None and
- (self._repo._encodefilterpats or
- kwt.match(fctx.path()) and not 'l' in fctx.flags()) or
- self.size() == fctx.size()):
- return self._filelog.cmp(self._filenode, fctx.data())
- return True
+ hgweb_do_annotate = hgweb.do_annotate
+ hgweb_do_changeset = hgweb.do_changeset
+ hgweb_do_filediff = hgweb.do_filediff
+
+ hgweb.do_annotate = kwweb_do_annotate
+ hgweb.do_changeset = hgweb.do_rev = kwweb_do_changeset
+ hgweb.do_filediff = hgweb.do_diff = kwweb_do_filediff
+
- extensions.wrapfunction(context.filectx, 'cmp', kwfilectx_cmp)
- extensions.wrapfunction(patch.patchfile, '__init__', kwpatchfile_init)
- extensions.wrapfunction(patch, 'diff', kw_diff)
- extensions.wrapfunction(cmdutil, 'copy', kw_copy)
- for c in 'annotate changeset rev filediff diff'.split():
- extensions.wrapfunction(webcommands, c, kwweb_skip)
- for name in recordextensions.split():
- try:
- record = extensions.find(name)
- extensions.wrapfunction(record, 'dorecord', kw_dorecord)
- except KeyError:
- pass
-
- repo.__class__ = kwrepo
+cmdtable = {
+ 'kwdemo':
+ (demo,
+ [('d', 'default', None, _('show default keyword template maps')),
+ ('f', 'rcfile', '', _('read maps from rcfile'))],
+ _('hg kwdemo [-d] [-f RCFILE] [TEMPLATEMAP]...')),
+ 'kwexpand': (expand, commands.walkopts,
+ _('hg kwexpand [OPTION]... [FILE]...')),
+ 'kwfiles':
+ (files,
+ [('A', 'all', None, _('show keyword status flags of all files')),
+ ('i', 'ignore', None, _('show files excluded from expansion')),
+ ('u', 'unknown', None, _('only show unknown (not tracked) files')),
+ ] + commands.walkopts,
+ _('hg kwfiles [OPTION]... [FILE]...')),
+ 'kwshrink': (shrink, commands.walkopts,
+ _('hg kwshrink [OPTION]... [FILE]...')),
+}
--- a/tests/run-tests.py Sat Sep 10 13:23:41 2011 +0100
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,1204 +0,0 @@
-#!/usr/bin/env python
-#
-# run-tests.py - Run a set of tests on Mercurial
-#
-# Copyright 2006 Matt Mackall <mpm@selenic.com>
-#
-# This software may be used and distributed according to the terms of the
-# GNU General Public License version 2 or any later version.
-
-# Modifying this script is tricky because it has many modes:
-# - serial (default) vs parallel (-jN, N > 1)
-# - no coverage (default) vs coverage (-c, -C, -s)
-# - temp install (default) vs specific hg script (--with-hg, --local)
-# - tests are a mix of shell scripts and Python scripts
-#
-# If you change this script, it is recommended that you ensure you
-# haven't broken it by running it in various modes with a representative
-# sample of test scripts. For example:
-#
-# 1) serial, no coverage, temp install:
-# ./run-tests.py test-s*
-# 2) serial, no coverage, local hg:
-# ./run-tests.py --local test-s*
-# 3) serial, coverage, temp install:
-# ./run-tests.py -c test-s*
-# 4) serial, coverage, local hg:
-# ./run-tests.py -c --local test-s* # unsupported
-# 5) parallel, no coverage, temp install:
-# ./run-tests.py -j2 test-s*
-# 6) parallel, no coverage, local hg:
-# ./run-tests.py -j2 --local test-s*
-# 7) parallel, coverage, temp install:
-# ./run-tests.py -j2 -c test-s* # currently broken
-# 8) parallel, coverage, local install:
-# ./run-tests.py -j2 -c --local test-s* # unsupported (and broken)
-# 9) parallel, custom tmp dir:
-# ./run-tests.py -j2 --tmpdir /tmp/myhgtests
-#
-# (You could use any subset of the tests: test-s* happens to match
-# enough that it's worth doing parallel runs, few enough that it
-# completes fairly quickly, includes both shell and Python scripts, and
-# includes some scripts that run daemon processes.)
-
-from distutils import version
-import difflib
-import errno
-import optparse
-import os
-import shutil
-import subprocess
-import signal
-import sys
-import tempfile
-import time
-import re
-import threading
-
-processlock = threading.Lock()
-
-closefds = os.name == 'posix'
-def Popen4(cmd, wd, timeout):
- processlock.acquire()
- p = subprocess.Popen(cmd, shell=True, bufsize=-1, cwd=wd,
- close_fds=closefds,
- stdin=subprocess.PIPE, stdout=subprocess.PIPE,
- stderr=subprocess.STDOUT)
- processlock.release()
-
- p.fromchild = p.stdout
- p.tochild = p.stdin
- p.childerr = p.stderr
-
- p.timeout = False
- if timeout:
- def t():
- start = time.time()
- while time.time() - start < timeout and p.returncode is None:
- time.sleep(1)
- p.timeout = True
- if p.returncode is None:
- terminate(p)
- threading.Thread(target=t).start()
-
- return p
-
-# reserved exit code to skip test (used by hghave)
-SKIPPED_STATUS = 80
-SKIPPED_PREFIX = 'skipped: '
-FAILED_PREFIX = 'hghave check failed: '
-PYTHON = sys.executable
-IMPL_PATH = 'PYTHONPATH'
-if 'java' in sys.platform:
- IMPL_PATH = 'JYTHONPATH'
-
-requiredtools = ["python", "diff", "grep", "unzip", "gunzip", "bunzip2", "sed"]
-
-defaults = {
- 'jobs': ('HGTEST_JOBS', 1),
- 'timeout': ('HGTEST_TIMEOUT', 180),
- 'port': ('HGTEST_PORT', 20059),
- 'shell': ('HGTEST_SHELL', '/bin/sh'),
-}
-
-def parselistfiles(files, listtype, warn=True):
- entries = dict()
- for filename in files:
- try:
- path = os.path.expanduser(os.path.expandvars(filename))
- f = open(path, "r")
- except IOError, err:
- if err.errno != errno.ENOENT:
- raise
- if warn:
- print "warning: no such %s file: %s" % (listtype, filename)
- continue
-
- for line in f.readlines():
- line = line.split('#', 1)[0].strip()
- if line:
- entries[line] = filename
-
- f.close()
- return entries
-
-def parseargs():
- parser = optparse.OptionParser("%prog [options] [tests]")
-
- # keep these sorted
- parser.add_option("--blacklist", action="append",
- help="skip tests listed in the specified blacklist file")
- parser.add_option("--whitelist", action="append",
- help="always run tests listed in the specified whitelist file")
- parser.add_option("-C", "--annotate", action="store_true",
- help="output files annotated with coverage")
- parser.add_option("--child", type="int",
- help="run as child process, summary to given fd")
- parser.add_option("-c", "--cover", action="store_true",
- help="print a test coverage report")
- parser.add_option("-d", "--debug", action="store_true",
- help="debug mode: write output of test scripts to console"
- " rather than capturing and diff'ing it (disables timeout)")
- parser.add_option("-f", "--first", action="store_true",
- help="exit on the first test failure")
- parser.add_option("--inotify", action="store_true",
- help="enable inotify extension when running tests")
- parser.add_option("-i", "--interactive", action="store_true",
- help="prompt to accept changed output")
- parser.add_option("-j", "--jobs", type="int",
- help="number of jobs to run in parallel"
- " (default: $%s or %d)" % defaults['jobs'])
- parser.add_option("--keep-tmpdir", action="store_true",
- help="keep temporary directory after running tests")
- parser.add_option("-k", "--keywords",
- help="run tests matching keywords")
- parser.add_option("-l", "--local", action="store_true",
- help="shortcut for --with-hg=<testdir>/../hg")
- parser.add_option("-n", "--nodiff", action="store_true",
- help="skip showing test changes")
- parser.add_option("-p", "--port", type="int",
- help="port on which servers should listen"
- " (default: $%s or %d)" % defaults['port'])
- parser.add_option("--pure", action="store_true",
- help="use pure Python code instead of C extensions")
- parser.add_option("-R", "--restart", action="store_true",
- help="restart at last error")
- parser.add_option("-r", "--retest", action="store_true",
- help="retest failed tests")
- parser.add_option("-S", "--noskips", action="store_true",
- help="don't report skip tests verbosely")
- parser.add_option("--shell", type="string",
- help="shell to use (default: $%s or %s)" % defaults['shell'])
- parser.add_option("-t", "--timeout", type="int",
- help="kill errant tests after TIMEOUT seconds"
- " (default: $%s or %d)" % defaults['timeout'])
- parser.add_option("--tmpdir", type="string",
- help="run tests in the given temporary directory"
- " (implies --keep-tmpdir)")
- parser.add_option("-v", "--verbose", action="store_true",
- help="output verbose messages")
- parser.add_option("--view", type="string",
- help="external diff viewer")
- parser.add_option("--with-hg", type="string",
- metavar="HG",
- help="test using specified hg script rather than a "
- "temporary installation")
- parser.add_option("-3", "--py3k-warnings", action="store_true",
- help="enable Py3k warnings on Python 2.6+")
- parser.add_option('--extra-config-opt', action="append",
- help='set the given config opt in the test hgrc')
-
- for option, (envvar, default) in defaults.items():
- defaults[option] = type(default)(os.environ.get(envvar, default))
- parser.set_defaults(**defaults)
- (options, args) = parser.parse_args()
-
- # jython is always pure
- if 'java' in sys.platform or '__pypy__' in sys.modules:
- options.pure = True
-
- if options.with_hg:
- if not (os.path.isfile(options.with_hg) and
- os.access(options.with_hg, os.X_OK)):
- parser.error('--with-hg must specify an executable hg script')
- if not os.path.basename(options.with_hg) == 'hg':
- sys.stderr.write('warning: --with-hg should specify an hg script\n')
- if options.local:
- testdir = os.path.dirname(os.path.realpath(sys.argv[0]))
- hgbin = os.path.join(os.path.dirname(testdir), 'hg')
- if not os.access(hgbin, os.X_OK):
- parser.error('--local specified, but %r not found or not executable'
- % hgbin)
- options.with_hg = hgbin
-
- options.anycoverage = options.cover or options.annotate
- if options.anycoverage:
- try:
- import coverage
- covver = version.StrictVersion(coverage.__version__).version
- if covver < (3, 3):
- parser.error('coverage options require coverage 3.3 or later')
- except ImportError:
- parser.error('coverage options now require the coverage package')
-
- if options.anycoverage and options.local:
- # this needs some path mangling somewhere, I guess
- parser.error("sorry, coverage options do not work when --local "
- "is specified")
-
- global vlog
- if options.verbose:
- if options.jobs > 1 or options.child is not None:
- pid = "[%d]" % os.getpid()
- else:
- pid = None
- def vlog(*msg):
- iolock.acquire()
- if pid:
- print pid,
- for m in msg:
- print m,
- print
- sys.stdout.flush()
- iolock.release()
- else:
- vlog = lambda *msg: None
-
- if options.tmpdir:
- options.tmpdir = os.path.expanduser(options.tmpdir)
-
- if options.jobs < 1:
- parser.error('--jobs must be positive')
- if options.interactive and options.jobs > 1:
- print '(--interactive overrides --jobs)'
- options.jobs = 1
- if options.interactive and options.debug:
- parser.error("-i/--interactive and -d/--debug are incompatible")
- if options.debug:
- if options.timeout != defaults['timeout']:
- sys.stderr.write(
- 'warning: --timeout option ignored with --debug\n')
- options.timeout = 0
- if options.py3k_warnings:
- if sys.version_info[:2] < (2, 6) or sys.version_info[:2] >= (3, 0):
- parser.error('--py3k-warnings can only be used on Python 2.6+')
- if options.blacklist:
- options.blacklist = parselistfiles(options.blacklist, 'blacklist')
- if options.whitelist:
- options.whitelisted = parselistfiles(options.whitelist, 'whitelist',
- warn=options.child is None)
- else:
- options.whitelisted = {}
-
- return (options, args)
-
-def rename(src, dst):
- """Like os.rename(), trade atomicity and opened files friendliness
- for existing destination support.
- """
- shutil.copy(src, dst)
- os.remove(src)
-
-def splitnewlines(text):
- '''like str.splitlines, but only split on newlines.
- keep line endings.'''
- i = 0
- lines = []
- while True:
- n = text.find('\n', i)
- if n == -1:
- last = text[i:]
- if last:
- lines.append(last)
- return lines
- lines.append(text[i:n + 1])
- i = n + 1
-
-def parsehghaveoutput(lines):
- '''Parse hghave log lines.
- Return tuple of lists (missing, failed):
- * the missing/unknown features
- * the features for which existence check failed'''
- missing = []
- failed = []
- for line in lines:
- if line.startswith(SKIPPED_PREFIX):
- line = line.splitlines()[0]
- missing.append(line[len(SKIPPED_PREFIX):])
- elif line.startswith(FAILED_PREFIX):
- line = line.splitlines()[0]
- failed.append(line[len(FAILED_PREFIX):])
-
- return missing, failed
-
-def showdiff(expected, output, ref, err):
- print
- for line in difflib.unified_diff(expected, output, ref, err):
- sys.stdout.write(line)
-
-def findprogram(program):
- """Search PATH for a executable program"""
- for p in os.environ.get('PATH', os.defpath).split(os.pathsep):
- name = os.path.join(p, program)
- if os.name == 'nt' or os.access(name, os.X_OK):
- return name
- return None
-
-def checktools():
- # Before we go any further, check for pre-requisite tools
- # stuff from coreutils (cat, rm, etc) are not tested
- for p in requiredtools:
- if os.name == 'nt':
- p += '.exe'
- found = findprogram(p)
- if found:
- vlog("# Found prerequisite", p, "at", found)
- else:
- print "WARNING: Did not find prerequisite tool: "+p
-
-def terminate(proc):
- """Terminate subprocess (with fallback for Python versions < 2.6)"""
- vlog('# Terminating process %d' % proc.pid)
- try:
- getattr(proc, 'terminate', lambda : os.kill(proc.pid, signal.SIGTERM))()
- except OSError:
- pass
-
-def killdaemons():
- # Kill off any leftover daemon processes
- try:
- fp = open(DAEMON_PIDS)
- for line in fp:
- try:
- pid = int(line)
- except ValueError:
- continue
- try:
- os.kill(pid, 0)
- vlog('# Killing daemon process %d' % pid)
- os.kill(pid, signal.SIGTERM)
- time.sleep(0.25)
- os.kill(pid, 0)
- vlog('# Daemon process %d is stuck - really killing it' % pid)
- os.kill(pid, signal.SIGKILL)
- except OSError, err:
- if err.errno != errno.ESRCH:
- raise
- fp.close()
- os.unlink(DAEMON_PIDS)
- except IOError:
- pass
-
-def cleanup(options):
- if not options.keep_tmpdir:
- vlog("# Cleaning up HGTMP", HGTMP)
- shutil.rmtree(HGTMP, True)
-
-def usecorrectpython():
- # some tests run python interpreter. they must use same
- # interpreter we use or bad things will happen.
- exedir, exename = os.path.split(sys.executable)
- if exename in ('python', 'python.exe'):
- path = findprogram(exename)
- if os.path.dirname(path) == exedir:
- return
- else:
- exename = 'python'
- vlog('# Making python executable in test path use correct Python')
- mypython = os.path.join(BINDIR, exename)
- try:
- os.symlink(sys.executable, mypython)
- except AttributeError:
- # windows fallback
- shutil.copyfile(sys.executable, mypython)
- shutil.copymode(sys.executable, mypython)
-
-def installhg(options):
- vlog("# Performing temporary installation of HG")
- installerrs = os.path.join("tests", "install.err")
- pure = options.pure and "--pure" or ""
-
- # Run installer in hg root
- script = os.path.realpath(sys.argv[0])
- hgroot = os.path.dirname(os.path.dirname(script))
- os.chdir(hgroot)
- nohome = '--home=""'
- if os.name == 'nt':
- # The --home="" trick works only on OS where os.sep == '/'
- # because of a distutils convert_path() fast-path. Avoid it at
- # least on Windows for now, deal with .pydistutils.cfg bugs
- # when they happen.
- nohome = ''
- cmd = ('%s setup.py %s clean --all'
- ' build --build-base="%s"'
- ' install --force --prefix="%s" --install-lib="%s"'
- ' --install-scripts="%s" %s >%s 2>&1'
- % (sys.executable, pure, os.path.join(HGTMP, "build"),
- INST, PYTHONDIR, BINDIR, nohome, installerrs))
- vlog("# Running", cmd)
- if os.system(cmd) == 0:
- if not options.verbose:
- os.remove(installerrs)
- else:
- f = open(installerrs)
- for line in f:
- print line,
- f.close()
- sys.exit(1)
- os.chdir(TESTDIR)
-
- usecorrectpython()
-
- vlog("# Installing dummy diffstat")
- f = open(os.path.join(BINDIR, 'diffstat'), 'w')
- f.write('#!' + sys.executable + '\n'
- 'import sys\n'
- 'files = 0\n'
- 'for line in sys.stdin:\n'
- ' if line.startswith("diff "):\n'
- ' files += 1\n'
- 'sys.stdout.write("files patched: %d\\n" % files)\n')
- f.close()
- os.chmod(os.path.join(BINDIR, 'diffstat'), 0700)
-
- if options.py3k_warnings and not options.anycoverage:
- vlog("# Updating hg command to enable Py3k Warnings switch")
- f = open(os.path.join(BINDIR, 'hg'), 'r')
- lines = [line.rstrip() for line in f]
- lines[0] += ' -3'
- f.close()
- f = open(os.path.join(BINDIR, 'hg'), 'w')
- for line in lines:
- f.write(line + '\n')
- f.close()
-
- hgbat = os.path.join(BINDIR, 'hg.bat')
- if os.path.isfile(hgbat):
- # hg.bat expects to be put in bin/scripts while run-tests.py
- # installation layout put it in bin/ directly. Fix it
- f = open(hgbat, 'rb')
- data = f.read()
- f.close()
- if '"%~dp0..\python" "%~dp0hg" %*' in data:
- data = data.replace('"%~dp0..\python" "%~dp0hg" %*',
- '"%~dp0python" "%~dp0hg" %*')
- f = open(hgbat, 'wb')
- f.write(data)
- f.close()
- else:
- print 'WARNING: cannot fix hg.bat reference to python.exe'
-
- if options.anycoverage:
- custom = os.path.join(TESTDIR, 'sitecustomize.py')
- target = os.path.join(PYTHONDIR, 'sitecustomize.py')
- vlog('# Installing coverage trigger to %s' % target)
- shutil.copyfile(custom, target)
- rc = os.path.join(TESTDIR, '.coveragerc')
- vlog('# Installing coverage rc to %s' % rc)
- os.environ['COVERAGE_PROCESS_START'] = rc
- fn = os.path.join(INST, '..', '.coverage')
- os.environ['COVERAGE_FILE'] = fn
-
-def outputcoverage(options):
-
- vlog('# Producing coverage report')
- os.chdir(PYTHONDIR)
-
- def covrun(*args):
- cmd = 'coverage %s' % ' '.join(args)
- vlog('# Running: %s' % cmd)
- os.system(cmd)
-
- if options.child:
- return
-
- covrun('-c')
- omit = ','.join([BINDIR, TESTDIR])
- covrun('-i', '-r', '"--omit=%s"' % omit) # report
- if options.annotate:
- adir = os.path.join(TESTDIR, 'annotated')
- if not os.path.isdir(adir):
- os.mkdir(adir)
- covrun('-i', '-a', '"--directory=%s"' % adir, '"--omit=%s"' % omit)
-
-def pytest(test, wd, options, replacements):
- py3kswitch = options.py3k_warnings and ' -3' or ''
- cmd = '%s%s "%s"' % (PYTHON, py3kswitch, test)
- vlog("# Running", cmd)
- return run(cmd, wd, options, replacements)
-
-def shtest(test, wd, options, replacements):
- cmd = '"%s"' % test
- vlog("# Running", cmd)
- return run(cmd, wd, options, replacements)
-
-needescape = re.compile(r'[\x00-\x08\x0b-\x1f\x7f-\xff]').search
-escapesub = re.compile(r'[\x00-\x08\x0b-\x1f\\\x7f-\xff]').sub
-escapemap = dict((chr(i), r'\x%02x' % i) for i in range(256))
-escapemap.update({'\\': '\\\\', '\r': r'\r'})
-def escapef(m):
- return escapemap[m.group(0)]
-def stringescape(s):
- return escapesub(escapef, s)
-
-def tsttest(test, wd, options, replacements):
- t = open(test)
- out = []
- script = []
- salt = "SALT" + str(time.time())
-
- pos = prepos = -1
- after = {}
- expected = {}
- for n, l in enumerate(t):
- if not l.endswith('\n'):
- l += '\n'
- if l.startswith(' $ '): # commands
- after.setdefault(pos, []).append(l)
- prepos = pos
- pos = n
- script.append('echo %s %s $?\n' % (salt, n))
- script.append(l[4:])
- elif l.startswith(' > '): # continuations
- after.setdefault(prepos, []).append(l)
- script.append(l[4:])
- elif l.startswith(' '): # results
- # queue up a list of expected results
- expected.setdefault(pos, []).append(l[2:])
- else:
- # non-command/result - queue up for merged output
- after.setdefault(pos, []).append(l)
-
- t.close()
-
- script.append('echo %s %s $?\n' % (salt, n + 1))
-
- fd, name = tempfile.mkstemp(suffix='hg-tst')
-
- try:
- for l in script:
- os.write(fd, l)
- os.close(fd)
-
- cmd = '"%s" "%s"' % (options.shell, name)
- vlog("# Running", cmd)
- exitcode, output = run(cmd, wd, options, replacements)
- # do not merge output if skipped, return hghave message instead
- # similarly, with --debug, output is None
- if exitcode == SKIPPED_STATUS or output is None:
- return exitcode, output
- finally:
- os.remove(name)
-
- def rematch(el, l):
- try:
- # ensure that the regex matches to the end of the string
- return re.match(el + r'\Z', l)
- except re.error:
- # el is an invalid regex
- return False
-
- def globmatch(el, l):
- # The only supported special characters are * and ?. Escaping is
- # supported.
- i, n = 0, len(el)
- res = ''
- while i < n:
- c = el[i]
- i += 1
- if c == '\\' and el[i] in '*?\\':
- res += el[i - 1:i + 1]
- i += 1
- elif c == '*':
- res += '.*'
- elif c == '?':
- res += '.'
- else:
- res += re.escape(c)
- return rematch(res, l)
-
- pos = -1
- postout = []
- ret = 0
- for n, l in enumerate(output):
- lout, lcmd = l, None
- if salt in l:
- lout, lcmd = l.split(salt, 1)
-
- if lout:
- if lcmd:
- lout += ' (no-eol)\n'
-
- el = None
- if pos in expected and expected[pos]:
- el = expected[pos].pop(0)
-
- if el == lout: # perfect match (fast)
- postout.append(" " + lout)
- elif (el and
- (el.endswith(" (re)\n") and rematch(el[:-6] + '\n', lout) or
- el.endswith(" (glob)\n") and globmatch(el[:-8] + '\n', lout)
- or el.endswith(" (esc)\n") and
- el.decode('string-escape') == l)):
- postout.append(" " + el) # fallback regex/glob/esc match
- else:
- if needescape(lout):
- lout = stringescape(lout.rstrip('\n')) + " (esc)\n"
- postout.append(" " + lout) # let diff deal with it
-
- if lcmd:
- # add on last return code
- ret = int(lcmd.split()[1])
- if ret != 0:
- postout.append(" [%s]\n" % ret)
- if pos in after:
- postout += after.pop(pos)
- pos = int(lcmd.split()[0])
-
- if pos in after:
- postout += after.pop(pos)
-
- return exitcode, postout
-
-wifexited = getattr(os, "WIFEXITED", lambda x: False)
-def run(cmd, wd, options, replacements):
- """Run command in a sub-process, capturing the output (stdout and stderr).
- Return a tuple (exitcode, output). output is None in debug mode."""
- # TODO: Use subprocess.Popen if we're running on Python 2.4
- if options.debug:
- proc = subprocess.Popen(cmd, shell=True, cwd=wd)
- ret = proc.wait()
- return (ret, None)
-
- proc = Popen4(cmd, wd, options.timeout)
- def cleanup():
- terminate(proc)
- ret = proc.wait()
- if ret == 0:
- ret = signal.SIGTERM << 8
- killdaemons()
- return ret
-
- output = ''
- proc.tochild.close()
-
- try:
- output = proc.fromchild.read()
- except KeyboardInterrupt:
- vlog('# Handling keyboard interrupt')
- cleanup()
- raise
-
- ret = proc.wait()
- if wifexited(ret):
- ret = os.WEXITSTATUS(ret)
-
- if proc.timeout:
- ret = 'timeout'
-
- if ret:
- killdaemons()
-
- for s, r in replacements:
- output = re.sub(s, r, output)
- return ret, splitnewlines(output)
-
-def runone(options, test):
- '''tristate output:
- None -> skipped
- True -> passed
- False -> failed'''
-
- global results, resultslock, iolock
-
- testpath = os.path.join(TESTDIR, test)
-
- def result(l, e):
- resultslock.acquire()
- results[l].append(e)
- resultslock.release()
-
- def skip(msg):
- if not options.verbose:
- result('s', (test, msg))
- else:
- iolock.acquire()
- print "\nSkipping %s: %s" % (testpath, msg)
- iolock.release()
- return None
-
- def fail(msg, ret):
- if not options.nodiff:
- iolock.acquire()
- print "\nERROR: %s %s" % (testpath, msg)
- iolock.release()
- if (not ret and options.interactive
- and os.path.exists(testpath + ".err")):
- iolock.acquire()
- print "Accept this change? [n] ",
- answer = sys.stdin.readline().strip()
- iolock.release()
- if answer.lower() in "y yes".split():
- if test.endswith(".t"):
- rename(testpath + ".err", testpath)
- else:
- rename(testpath + ".err", testpath + ".out")
- result('p', test)
- return
- result('f', (test, msg))
-
- def success():
- result('p', test)
-
- def ignore(msg):
- result('i', (test, msg))
-
- if (os.path.basename(test).startswith("test-") and '~' not in test and
- ('.' not in test or test.endswith('.py') or
- test.endswith('.bat') or test.endswith('.t'))):
- if not os.path.exists(test):
- skip("doesn't exist")
- return None
- else:
- vlog('# Test file', test, 'not supported, ignoring')
- return None # not a supported test, don't record
-
- if not (options.whitelisted and test in options.whitelisted):
- if options.blacklist and test in options.blacklist:
- skip("blacklisted")
- return None
-
- if options.retest and not os.path.exists(test + ".err"):
- ignore("not retesting")
- return None
-
- if options.keywords:
- fp = open(test)
- t = fp.read().lower() + test.lower()
- fp.close()
- for k in options.keywords.lower().split():
- if k in t:
- break
- else:
- ignore("doesn't match keyword")
- return None
-
- vlog("# Test", test)
-
- # create a fresh hgrc
- hgrc = open(HGRCPATH, 'w+')
- hgrc.write('[ui]\n')
- hgrc.write('slash = True\n')
- hgrc.write('[defaults]\n')
- hgrc.write('backout = -d "0 0"\n')
- hgrc.write('commit = -d "0 0"\n')
- hgrc.write('tag = -d "0 0"\n')
- if options.inotify:
- hgrc.write('[extensions]\n')
- hgrc.write('inotify=\n')
- hgrc.write('[inotify]\n')
- hgrc.write('pidfile=%s\n' % DAEMON_PIDS)
- hgrc.write('appendpid=True\n')
- if options.extra_config_opt:
- for opt in options.extra_config_opt:
- section, key = opt.split('.', 1)
- assert '=' in key, ('extra config opt %s must '
- 'have an = for assignment' % opt)
- hgrc.write('[%s]\n%s\n' % (section, key))
- hgrc.close()
-
- ref = os.path.join(TESTDIR, test+".out")
- err = os.path.join(TESTDIR, test+".err")
- if os.path.exists(err):
- os.remove(err) # Remove any previous output files
- try:
- tf = open(testpath)
- firstline = tf.readline().rstrip()
- tf.close()
- except:
- firstline = ''
- lctest = test.lower()
-
- if lctest.endswith('.py') or firstline == '#!/usr/bin/env python':
- runner = pytest
- elif lctest.endswith('.t'):
- runner = tsttest
- ref = testpath
- else:
- # do not try to run non-executable programs
- if not os.access(testpath, os.X_OK):
- return skip("not executable")
- runner = shtest
-
- # Make a tmp subdirectory to work in
- testtmp = os.environ["TESTTMP"] = os.environ["HOME"] = \
- os.path.join(HGTMP, os.path.basename(test))
-
- os.mkdir(testtmp)
- ret, out = runner(testpath, testtmp, options, [
- (re.escape(testtmp), '$TESTTMP'),
- (r':%s\b' % options.port, ':$HGPORT'),
- (r':%s\b' % (options.port + 1), ':$HGPORT1'),
- (r':%s\b' % (options.port + 2), ':$HGPORT2'),
- ])
- vlog("# Ret was:", ret)
-
- mark = '.'
-
- skipped = (ret == SKIPPED_STATUS)
-
- # If we're not in --debug mode and reference output file exists,
- # check test output against it.
- if options.debug:
- refout = None # to match "out is None"
- elif os.path.exists(ref):
- f = open(ref, "r")
- refout = splitnewlines(f.read())
- f.close()
- else:
- refout = []
-
- if (ret != 0 or out != refout) and not skipped and not options.debug:
- # Save errors to a file for diagnosis
- f = open(err, "wb")
- for line in out:
- f.write(line)
- f.close()
-
- if skipped:
- mark = 's'
- if out is None: # debug mode: nothing to parse
- missing = ['unknown']
- failed = None
- else:
- missing, failed = parsehghaveoutput(out)
- if not missing:
- missing = ['irrelevant']
- if failed:
- fail("hghave failed checking for %s" % failed[-1], ret)
- skipped = False
- else:
- skip(missing[-1])
- elif ret == 'timeout':
- mark = 't'
- fail("timed out", ret)
- elif out != refout:
- mark = '!'
- if not options.nodiff:
- iolock.acquire()
- if options.view:
- os.system("%s %s %s" % (options.view, ref, err))
- else:
- showdiff(refout, out, ref, err)
- iolock.release()
- if ret:
- fail("output changed and returned error code %d" % ret, ret)
- else:
- fail("output changed", ret)
- ret = 1
- elif ret:
- mark = '!'
- fail("returned error code %d" % ret, ret)
- else:
- success()
-
- if not options.verbose:
- iolock.acquire()
- sys.stdout.write(mark)
- sys.stdout.flush()
- iolock.release()
-
- killdaemons()
-
- if not options.keep_tmpdir:
- shutil.rmtree(testtmp, True)
- if skipped:
- return None
- return ret == 0
-
-_hgpath = None
-
-def _gethgpath():
- """Return the path to the mercurial package that is actually found by
- the current Python interpreter."""
- global _hgpath
- if _hgpath is not None:
- return _hgpath
-
- cmd = '%s -c "import mercurial; print mercurial.__path__[0]"'
- pipe = os.popen(cmd % PYTHON)
- try:
- _hgpath = pipe.read().strip()
- finally:
- pipe.close()
- return _hgpath
-
-def _checkhglib(verb):
- """Ensure that the 'mercurial' package imported by python is
- the one we expect it to be. If not, print a warning to stderr."""
- expecthg = os.path.join(PYTHONDIR, 'mercurial')
- actualhg = _gethgpath()
- if os.path.abspath(actualhg) != os.path.abspath(expecthg):
- sys.stderr.write('warning: %s with unexpected mercurial lib: %s\n'
- ' (expected %s)\n'
- % (verb, actualhg, expecthg))
-
-def runchildren(options, tests):
- if INST:
- installhg(options)
- _checkhglib("Testing")
-
- optcopy = dict(options.__dict__)
- optcopy['jobs'] = 1
-
- # Because whitelist has to override keyword matches, we have to
- # actually load the whitelist in the children as well, so we allow
- # the list of whitelist files to pass through and be parsed in the
- # children, but not the dict of whitelisted tests resulting from
- # the parse, used here to override blacklisted tests.
- whitelist = optcopy['whitelisted'] or []
- del optcopy['whitelisted']
-
- blacklist = optcopy['blacklist'] or []
- del optcopy['blacklist']
- blacklisted = []
-
- if optcopy['with_hg'] is None:
- optcopy['with_hg'] = os.path.join(BINDIR, "hg")
- optcopy.pop('anycoverage', None)
-
- opts = []
- for opt, value in optcopy.iteritems():
- name = '--' + opt.replace('_', '-')
- if value is True:
- opts.append(name)
- elif isinstance(value, list):
- for v in value:
- opts.append(name + '=' + str(v))
- elif value is not None:
- opts.append(name + '=' + str(value))
-
- tests.reverse()
- jobs = [[] for j in xrange(options.jobs)]
- while tests:
- for job in jobs:
- if not tests:
- break
- test = tests.pop()
- if test not in whitelist and test in blacklist:
- blacklisted.append(test)
- else:
- job.append(test)
- fps = {}
-
- for j, job in enumerate(jobs):
- if not job:
- continue
- rfd, wfd = os.pipe()
- childopts = ['--child=%d' % wfd, '--port=%d' % (options.port + j * 3)]
- childtmp = os.path.join(HGTMP, 'child%d' % j)
- childopts += ['--tmpdir', childtmp]
- cmdline = [PYTHON, sys.argv[0]] + opts + childopts + job
- vlog(' '.join(cmdline))
- fps[os.spawnvp(os.P_NOWAIT, cmdline[0], cmdline)] = os.fdopen(rfd, 'r')
- os.close(wfd)
- signal.signal(signal.SIGINT, signal.SIG_IGN)
- failures = 0
- tested, skipped, failed = 0, 0, 0
- skips = []
- fails = []
- while fps:
- pid, status = os.wait()
- fp = fps.pop(pid)
- l = fp.read().splitlines()
- try:
- test, skip, fail = map(int, l[:3])
- except ValueError:
- test, skip, fail = 0, 0, 0
- split = -fail or len(l)
- for s in l[3:split]:
- skips.append(s.split(" ", 1))
- for s in l[split:]:
- fails.append(s.split(" ", 1))
- tested += test
- skipped += skip
- failed += fail
- vlog('pid %d exited, status %d' % (pid, status))
- failures |= status
- print
- skipped += len(blacklisted)
- if not options.noskips:
- for s in skips:
- print "Skipped %s: %s" % (s[0], s[1])
- for s in blacklisted:
- print "Skipped %s: blacklisted" % s
- for s in fails:
- print "Failed %s: %s" % (s[0], s[1])
-
- _checkhglib("Tested")
- print "# Ran %d tests, %d skipped, %d failed." % (
- tested, skipped, failed)
-
- if options.anycoverage:
- outputcoverage(options)
- sys.exit(failures != 0)
-
-results = dict(p=[], f=[], s=[], i=[])
-resultslock = threading.Lock()
-iolock = threading.Lock()
-
-def runqueue(options, tests, results):
- for test in tests:
- ret = runone(options, test)
- if options.first and ret is not None and not ret:
- break
-
-def runtests(options, tests):
- global DAEMON_PIDS, HGRCPATH
- DAEMON_PIDS = os.environ["DAEMON_PIDS"] = os.path.join(HGTMP, 'daemon.pids')
- HGRCPATH = os.environ["HGRCPATH"] = os.path.join(HGTMP, '.hgrc')
-
- try:
- if INST:
- installhg(options)
- _checkhglib("Testing")
-
- if options.restart:
- orig = list(tests)
- while tests:
- if os.path.exists(tests[0] + ".err"):
- break
- tests.pop(0)
- if not tests:
- print "running all tests"
- tests = orig
-
- runqueue(options, tests, results)
-
- failed = len(results['f'])
- tested = len(results['p']) + failed
- skipped = len(results['s'])
- ignored = len(results['i'])
-
- if options.child:
- fp = os.fdopen(options.child, 'w')
- fp.write('%d\n%d\n%d\n' % (tested, skipped, failed))
- for s in results['s']:
- fp.write("%s %s\n" % s)
- for s in results['f']:
- fp.write("%s %s\n" % s)
- fp.close()
- else:
- print
- for s in results['s']:
- print "Skipped %s: %s" % s
- for s in results['f']:
- print "Failed %s: %s" % s
- _checkhglib("Tested")
- print "# Ran %d tests, %d skipped, %d failed." % (
- tested, skipped + ignored, failed)
-
- if options.anycoverage:
- outputcoverage(options)
- except KeyboardInterrupt:
- failed = True
- print "\ninterrupted!"
-
- if failed:
- sys.exit(1)
-
-def main():
- (options, args) = parseargs()
- if not options.child:
- os.umask(022)
-
- checktools()
-
- if len(args) == 0:
- args = os.listdir(".")
- args.sort()
-
- tests = args
-
- # Reset some environment variables to well-known values so that
- # the tests produce repeatable output.
- os.environ['LANG'] = os.environ['LC_ALL'] = os.environ['LANGUAGE'] = 'C'
- os.environ['TZ'] = 'GMT'
- os.environ["EMAIL"] = "Foo Bar <foo.bar@example.com>"
- os.environ['CDPATH'] = ''
- os.environ['COLUMNS'] = '80'
- os.environ['GREP_OPTIONS'] = ''
- os.environ['http_proxy'] = ''
-
- # unset env related to hooks
- for k in os.environ.keys():
- if k.startswith('HG_'):
- # can't remove on solaris
- os.environ[k] = ''
- del os.environ[k]
-
- global TESTDIR, HGTMP, INST, BINDIR, PYTHONDIR, COVERAGE_FILE
- TESTDIR = os.environ["TESTDIR"] = os.getcwd()
- if options.tmpdir:
- options.keep_tmpdir = True
- tmpdir = options.tmpdir
- if os.path.exists(tmpdir):
- # Meaning of tmpdir has changed since 1.3: we used to create
- # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if
- # tmpdir already exists.
- sys.exit("error: temp dir %r already exists" % tmpdir)
-
- # Automatically removing tmpdir sounds convenient, but could
- # really annoy anyone in the habit of using "--tmpdir=/tmp"
- # or "--tmpdir=$HOME".
- #vlog("# Removing temp dir", tmpdir)
- #shutil.rmtree(tmpdir)
- os.makedirs(tmpdir)
- else:
- tmpdir = tempfile.mkdtemp('', 'hgtests.')
- HGTMP = os.environ['HGTMP'] = os.path.realpath(tmpdir)
- DAEMON_PIDS = None
- HGRCPATH = None
-
- os.environ["HGEDITOR"] = sys.executable + ' -c "import sys; sys.exit(0)"'
- os.environ["HGMERGE"] = "internal:merge"
- os.environ["HGUSER"] = "test"
- os.environ["HGENCODING"] = "ascii"
- os.environ["HGENCODINGMODE"] = "strict"
- os.environ["HGPORT"] = str(options.port)
- os.environ["HGPORT1"] = str(options.port + 1)
- os.environ["HGPORT2"] = str(options.port + 2)
-
- if options.with_hg:
- INST = None
- BINDIR = os.path.dirname(os.path.realpath(options.with_hg))
-
- # This looks redundant with how Python initializes sys.path from
- # the location of the script being executed. Needed because the
- # "hg" specified by --with-hg is not the only Python script
- # executed in the test suite that needs to import 'mercurial'
- # ... which means it's not really redundant at all.
- PYTHONDIR = BINDIR
- else:
- INST = os.path.join(HGTMP, "install")
- BINDIR = os.environ["BINDIR"] = os.path.join(INST, "bin")
- PYTHONDIR = os.path.join(INST, "lib", "python")
-
- os.environ["BINDIR"] = BINDIR
- os.environ["PYTHON"] = PYTHON
-
- if not options.child:
- path = [BINDIR] + os.environ["PATH"].split(os.pathsep)
- os.environ["PATH"] = os.pathsep.join(path)
-
- # Include TESTDIR in PYTHONPATH so that out-of-tree extensions
- # can run .../tests/run-tests.py test-foo where test-foo
- # adds an extension to HGRC
- pypath = [PYTHONDIR, TESTDIR]
- # We have to augment PYTHONPATH, rather than simply replacing
- # it, in case external libraries are only available via current
- # PYTHONPATH. (In particular, the Subversion bindings on OS X
- # are in /opt/subversion.)
- oldpypath = os.environ.get(IMPL_PATH)
- if oldpypath:
- pypath.append(oldpypath)
- os.environ[IMPL_PATH] = os.pathsep.join(pypath)
-
- COVERAGE_FILE = os.path.join(TESTDIR, ".coverage")
-
- vlog("# Using TESTDIR", TESTDIR)
- vlog("# Using HGTMP", HGTMP)
- vlog("# Using PATH", os.environ["PATH"])
- vlog("# Using", IMPL_PATH, os.environ[IMPL_PATH])
-
- try:
- if len(tests) > 1 and options.jobs > 1:
- runchildren(options, tests)
- else:
- runtests(options, tests)
- finally:
- time.sleep(1)
- cleanup(options)
-
-if __name__ == '__main__':
- main()