sapling/tests/testutil/argspans.py
Jun Wu 57e6e896ad testutil/dott: simplify error messages
Summary:
The current error message is a bit noisy. Let's just get to the point about the
filename and line number that is interesting without tracebacks. This only
affects functions using the `autofix.eq` API, other kinds of exceptions will
have tracebacks as usual.

Before, run-tests.py (19 lines):

  --- test-empty-t.py.out
  +++ test-empty-t.py.err
  @@ -0,0 +1,14 @@
  +Traceback (most recent call last):
  +  File "hg/tests/test-empty-t.py", line 71, in <module>
  +    """
  +  File "hg/tests/testutil/dott/shobj.py", line 89, in __eq__
  +    autofix.eq(out, rhs, nested=1, eqfunc=eqglob)
  +  File "hg/tests/testutil/autofix.py", line 93, in eq
  +    raise AssertionError("actual != expected\n%s" % diff)
  +AssertionError: actual != expected
  +--- expected
  ++++ actual
  +@@ -1 +1 @@
  +-someheads
  ++allheads
  +

  ERROR: test-empty-t.py output changed

Before, run directly via python (13 lines):

  Traceback (most recent call last):
    File "test-empty-t.py", line 71, in <module>
      """
    File "hg/tests/testutil/dott/shobj.py", line 89, in __eq__
      autofix.eq(out, rhs, nested=1, eqfunc=eqglob)
    File "hg/tests/testutil/autofix.py", line 93, in eq
      raise AssertionError("actual != expected\n%s" % diff)
  AssertionError: actual != expected
  --- expected
  +++ actual
  @@ -1 +1 @@
  -someheads
  +allheads

After, run-tests.py (8 lines):

  --- test-empty-t.py:71 (expected)
  +++ test-empty-t.py:71 (actual)
  @@ -1 +1 @@
  -someheads
  +allheads

  ERROR: test-empty-t.py output changed

After, run directly (5 lines):

  % python test-empty-t.py
  --- test-empty-t.py:71 (expected)
  +++ test-empty-t.py:71 (actual)
  @@ -1 +1 @@
  -someheads
  +allheads

Reviewed By: xavierd

Differential Revision: D17277286

fbshipit-source-id: a48d4d1e817f67e221a901977e0c0f8bdc1a62ab
2019-09-10 13:01:33 -07:00

118 lines
3.5 KiB
Python

# Copyright 2019 Facebook, Inc.
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2 or any later version.
from __future__ import absolute_import
import inspect
import os
class memoize(dict):
def __init__(self, func):
self.func = func
def __call__(self, *args):
return self[args]
def __missing__(self, key):
result = self[key] = self.func(*key)
return result
@memoize
def parse(path):
# Import parso lazily. So importing this module won't error out if parso is
# not available.
import parso
return parso.parse(open(path).read())
def argspans(nested=0):
"""Return argument positions of the function being called.
The return value is in this form:
filepath, lineno, indent, spans
filepath is the absolute file path.
lineno is the line number as seen by Python (not necessarily the start or
the end line).
indent is the number of spaces of the indentation of the function call.
spans is in this form:
[((start line, start col), (end line, end col))]
Line numbers start from 1. Column numbers start from 0.
`spans, indent` can be `None, 0` if the parsing library (parso) is not
available, or the callsite location cannot be found.
If nested is 0, check the function calling `argspans`. If nested is 1, check
function calling the function calling `argspans`, and so on.
"""
path, lineno, funcname = sourcelocation(nested + 1)
def locate(node):
"""Find the node that is the callsite invocation"""
children = getattr(node, "children", ())
if len(children) == 2:
name = children[0]
if (
name.type == "name"
and name.value == funcname
and node.end_pos[0] >= lineno
and node.start_pos[0] <= lineno
):
yield node, "args"
return
if len(children) == 3 and funcname == "__eq__":
# Special case: Treat the RHS as "__eq__" args.
op = children[1]
if (
op.type == "operator"
and op.value == "=="
and node.end_pos[0] >= lineno
and node.start_pos[0] <= lineno
):
yield node, "=="
return
for c in children:
for subnode in locate(c):
yield subnode
try:
node, nodetype = next(locate(parse(path)))
except StopIteration:
spans = None
indent = 0
else:
if nodetype == "args":
arglist = node.children[1].children[1]
assert arglist.type == "arglist"
# "::2" removes argument separators like ",".
spans = [(a.start_pos, a.end_pos) for a in arglist.children[::2]]
elif nodetype == "==":
rhs = node.children[2]
spans = [(rhs.start_pos, rhs.end_pos)]
indent = node.start_pos[1]
return path, lineno, indent, spans
def sourcelocation(nested=0, _cwd=os.getcwd()):
"""Return (path, lineno, funcname) from Python frames"""
frame = inspect.currentframe().f_back # the function calling argspans()
for _i in range(nested):
frame = frame.f_back
funcname = frame.f_code.co_name
frame = frame.f_back # the callsite calling "the function" (funcname)
lineno = frame.f_lineno
path = os.path.realpath(os.path.join(_cwd, frame.f_code.co_filename))
return path, lineno, funcname