fileset: detect unintentional existing() invocation at runtime
A fileset predicate can invoke 'matchctx.existing()' successfully,
even if it isn't marked as "existing caller". It is aborted only in
some corner cases: e.g. there were one deleted file in the working
directory (see 8a0513bf030a for detail).
This patch makes 'matchctx.existing()' invocation abort if not
'_existingenabled', which is true only while "existing caller"
running.
After this changes, non-"existing caller" predicate function is
aborted immediately, whenever it invokes 'matchctx.existing()'. This
prevent developer from forgetting to mark a predicate as "existing
caller".
BTW, unintentional 'matchctx.status()' invocation can be detected
easily without any additional trick like this patch, because it
returns 'None' if a predicate isn't marked as "status caller", and
referring field (e.g. '.modified') of it is always aborted.
--- a/mercurial/fileset.py Mon Dec 21 22:31:16 2015 +0900
+++ b/mercurial/fileset.py Mon Dec 21 22:31:16 2015 +0900
@@ -248,7 +248,13 @@
def func(mctx, a, b):
if a[0] == 'symbol' and a[1] in symbols:
- return symbols[a[1]](mctx, b)
+ funcname = a[1]
+ enabled = mctx._existingenabled
+ mctx._existingenabled = funcname in _existingcallers
+ try:
+ return symbols[funcname](mctx, b)
+ finally:
+ mctx._existingenabled = enabled
keep = lambda fn: getattr(fn, '__doc__', None) is not None
@@ -497,6 +503,7 @@
self.ctx = ctx
self.subset = subset
self._status = status
+ self._existingenabled = False
def status(self):
return self._status
def matcher(self, patterns):
@@ -504,6 +511,7 @@
def filter(self, files):
return [f for f in files if f in self.subset]
def existing(self):
+ assert self._existingenabled, 'unexpected existing() invocation'
if self._status is not None:
removed = set(self._status[3])
unknown = set(self._status[4] + self._status[5])
--- a/tests/test-fileset.t Mon Dec 21 22:31:16 2015 +0900
+++ b/tests/test-fileset.t Mon Dec 21 22:31:16 2015 +0900
@@ -328,3 +328,22 @@
b2link
bin
c1
+
+Test detection of unintentional 'matchctx.existing()' invocation
+
+ $ cat > $TESTTMP/existingcaller.py <<EOF
+ > from mercurial import fileset
+ >
+ > @fileset.predicate('existingcaller()', callexisting=False)
+ > def existingcaller(mctx, x):
+ > # this 'mctx.existing()' invocation is unintentional
+ > return [f for f in mctx.existing()]
+ > EOF
+
+ $ cat >> .hg/hgrc <<EOF
+ > [extensions]
+ > existingcaller = $TESTTMP/existingcaller.py
+ > EOF
+
+ $ fileset 'existingcaller()' 2>&1 | tail -1
+ AssertionError: unexpected existing() invocation