Skip to content

Commit

Permalink
fix: try to avoid recording tests
Browse files Browse the repository at this point in the history
When creating the default config, ignore directories with names that
match the regex .*test.*. This should avoid instrumenting the majority
of test functions. For those that do still get instrumented, have the
test framework integration disable the wrapt wrapped on it, thereby
disabling recording.
  • Loading branch information
apotterri committed Jul 23, 2024
1 parent a3bb402 commit 833b1d2
Show file tree
Hide file tree
Showing 27 changed files with 976 additions and 214 deletions.
54 changes: 37 additions & 17 deletions _appmap/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@
import inspect
import json
import os
import re
import sys
from os.path import realpath
from pathlib import Path
from textwrap import dedent

import yaml
from yaml import SafeLoader
from yaml.parser import ParserError

from _appmap.labels import LabelSet
Expand Down Expand Up @@ -50,20 +52,20 @@ def _get_sys_prefix():
return realpath(sys.prefix)


_EXCLUDE_PATTERN = re.compile(r"\..*|node_modules|.*test.*|site-packages")


def find_top_packages(rootdir):
"""
Scan a directory tree for packages that should appear in the
default config file.
Scan a directory tree for packages that should appear in the default config file.
Examine directories in rootdir, to see if they contains an
__init__.py. If it does, add it to the list of packages and don't
scan any of its subdirectories. If it doesn't, scan its
Examine each directory in rootdir, to see if it contains an __init__.py. If it does, add it to
the list of packages and don't scan any of its subdirectories. If it doesn't, scan its
subdirectories to find __init__.py.
Some directories are automatically excluded from the search:
* sys.prefix
* Hidden directories (i.e. those that start with a '.')
* node_modules
Directory traversal will stop at directories that match _EXCLUDE_PATTERN. Such a directory (and
its subdirectories) will not be added to the returned packages, but will in set of excluded
directories).
For example, in a directory like this
Expand Down Expand Up @@ -104,11 +106,11 @@ def find_top_packages(rootdir):
# build process copies its source to a subdirectory.
packages = set()

def excluded(d):
excluded = d == "node_modules" or d[0] == "."
if excluded:
def exclude(d):
is_excluded = _EXCLUDE_PATTERN.search(d) is not None
if is_excluded:
logger.trace("excluding dir %s", d)
return excluded
return is_excluded

sys_prefix = _get_sys_prefix()

Expand All @@ -123,13 +125,26 @@ def excluded(d):
packages.add(Path(d).name)
dirs.clear()
else:
dirs[:] = [d for d in dirs if not excluded(d)]
dirs[:] = [d for d in dirs if not exclude(d)]

return packages

class AppMapInvalidConfigException(Exception):
pass

# We don't have any control over the PyYAML class hierarchy, so we can't control how many ancestors
# SafeLoader has....
class _ConfigLoader(SafeLoader): # pylint: disable=too-many-ancestors
def construct_mapping(self, node, deep=False):
mapping = super().construct_mapping(node, deep=deep)
# Allow record_test_cases to be set using a string (in addition to allowing a boolean).
if "record_test_cases" in mapping:
val = mapping["record_test_cases"]
if isinstance(val, str):
mapping["record_test_cases"] = val.lower() == "true"
return mapping


class Config(metaclass=SingletonMeta):
"""Singleton Config class"""

Expand All @@ -156,11 +171,16 @@ def name(self):
def packages(self):
return self._config["packages"]

@property
def record_test_cases(self):
return self._config.get("record_test_cases", False)

@property
def default(self):
ret = {
"name": self.default_name,
"language": "python",
"record_test_cases": False,
"packages": self.default_packages,
}
env = Env.current
Expand Down Expand Up @@ -233,7 +253,7 @@ def _load_config(self, show_warnings=False):
Env.current.enabled = False
self.file_valid = False
try:
self._config = yaml.safe_load(path.read_text(encoding="utf-8"))
self._config = yaml.load(path.read_text(encoding="utf-8"), Loader=_ConfigLoader)
if not self._config:
# It parsed, but was (effectively) empty.
self._config = self.default
Expand Down Expand Up @@ -334,7 +354,6 @@ def _check_path_value(self, value):
except SyntaxError:
return False


def startswith(prefix, sequence):
"""
Check if a sequence starts with the prefix.
Expand Down Expand Up @@ -377,7 +396,8 @@ class DistMatcher(PathMatcher):
def __init__(self, dist, *args, **kwargs):
super().__init__(*args, **kwargs)
self.dist = dist
self.files = [str(pp.locate()) for pp in importlib.metadata.files(dist)]
dist_files = importlib.metadata.files(dist)
self.files = [str(pp.locate()) for pp in dist_files] if dist_files is not None else []

def matches(self, filterable):
try:
Expand Down
4 changes: 4 additions & 0 deletions _appmap/test/data/pytest/appmap-no-test-cases.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
name: Simple
record_test_cases: false
packages:
- path: simple
2 changes: 2 additions & 0 deletions _appmap/test/data/pytest/appmap.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
name: Simple
record_test_cases: true
packages:
- path: simple
- path: tests
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
{
"version": "1.9",
"metadata": {
"language": {
"name": "python"
},
"client": {
"name": "appmap",
"url": "https://github.com/applandinc/appmap-python"
},
"app": "Simple",
"recorder": {
"name": "pytest",
"type": "tests"
},
"source_location": "tests/test_simple.py:5",
"name": "hello world",
"feature": "Hello world",
"test_status": "succeeded"
},
"events": [
{
"defined_class": "simple.Simple",
"method_id": "hello_world",
"path": "simple.py",
"lineno": 8,
"static": false,
"receiver": {
"class": "simple.Simple",
"kind": "req",
"name": "self",
"value": "<simple.Simple object at 0xabcdef>"
},
"parameters": [],
"id": 1,
"event": "call",
"thread_id": 1
},
{
"defined_class": "simple.Simple",
"method_id": "hello",
"path": "simple.py",
"lineno": 2,
"static": false,
"receiver": {
"class": "simple.Simple",
"kind": "req",
"name": "self",
"value": "<simple.Simple object at 0xabcdef>"
},
"parameters": [],
"id": 2,
"event": "call",
"thread_id": 1
},
{
"return_value": {
"class": "builtins.str",
"value": "'Hello'"
},
"parent_id": 2,
"id": 3,
"event": "return",
"thread_id": 1
},
{
"defined_class": "simple.Simple",
"method_id": "world",
"path": "simple.py",
"lineno": 5,
"static": false,
"receiver": {
"class": "simple.Simple",
"kind": "req",
"name": "self",
"value": "<simple.Simple object at 0xabcdef>"
},
"parameters": [],
"id": 4,
"event": "call",
"thread_id": 1
},
{
"return_value": {
"class": "builtins.str",
"value": "'world!'"
},
"parent_id": 4,
"id": 5,
"event": "return",
"thread_id": 1
},
{
"return_value": {
"class": "builtins.str",
"value": "'Hello world!'"
},
"parent_id": 1,
"id": 6,
"event": "return",
"thread_id": 1
},
{
"static": false,
"receiver": {
"kind": "req",
"value": "<simple.Simple object at 0xabcdef>",
"name": "self",
"class": "simple.Simple"
},
"parameters": [],
"id": 7,
"event": "call",
"thread_id": 1,
"defined_class": "simple.Simple",
"method_id": "show_numpy_dict",
"path": "simple.py",
"lineno": 11
},
{
"static": false,
"receiver": {
"kind": "req",
"value": "<simple.Simple object at 0xabcdef>",
"name": "self",
"class": "simple.Simple"
},
"parameters": [
{
"kind": "req",
"value": "{0: 'zero', 1: 'one'}",
"name": "d",
"class": "builtins.dict",
"properties": [
{
"name": "0",
"class": "builtins.str"
},
{
"name": "1",
"class": "builtins.str"
}
],
"size": 2
}
],
"id": 8,
"event": "call",
"thread_id": 1,
"defined_class": "simple.Simple",
"method_id": "get_numpy_dict",
"path": "simple.py",
"lineno": 18
},
{
"return_value": {
"value": "{0: 'zero', 1: 'one'}",
"class": "builtins.dict",
"properties": [
{
"name": "0",
"class": "builtins.str"
},
{
"name": "1",
"class": "builtins.str"
}
],
"size": 2
},
"parent_id": 8,
"id": 9,
"event": "return",
"thread_id": 1
},
{
"return_value": {
"value": "{0: 'zero', 1: 'one'}",
"class": "builtins.dict",
"properties": [
{
"name": "0",
"class": "builtins.str"
},
{
"name": "1",
"class": "builtins.str"
}
],
"size": 2
},
"parent_id": 7,
"id": 10,
"event": "return",
"thread_id": 1
}
],
"classMap": [
{
"name": "simple",
"type": "package",
"children": [
{
"name": "Simple",
"type": "class",
"children": [
{
"name": "get_numpy_dict",
"type": "function",
"location": "simple.py:18",
"static": false
},
{
"name": "hello",
"type": "function",
"location": "simple.py:2",
"static": false
},
{
"name": "hello_world",
"type": "function",
"location": "simple.py:8",
"static": false
},
{
"name": "show_numpy_dict",
"type": "function",
"location": "simple.py:11",
"static": false
},
{
"name": "world",
"type": "function",
"location": "simple.py:5",
"static": false
}
]
}
]
}
]
}
Loading

0 comments on commit 833b1d2

Please sign in to comment.