From 99b3afd783e9bc6f22e34ccfdd93da9ed4901816 Mon Sep 17 00:00:00 2001 From: blackout Date: Fri, 27 Feb 2026 14:16:56 -0500 Subject: [PATCH 01/20] wip --- pyproject.toml | 3 + synapse/utils/stormcov.py | 371 +++++++++++++++++++++++++++++ synapse/utils/stormcov/__init__.py | 6 - synapse/utils/stormcov/plugin.py | 273 --------------------- 4 files changed, 374 insertions(+), 279 deletions(-) create mode 100644 synapse/utils/stormcov.py delete mode 100644 synapse/utils/stormcov/__init__.py delete mode 100644 synapse/utils/stormcov/plugin.py diff --git a/pyproject.toml b/pyproject.toml index 94ca953d60b..629dade786e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -80,6 +80,9 @@ Documentation = 'https://synapse.docs.vertex.link' Repository = 'https://github.com/vertexproject/synapse' Changelog = 'https://synapse.docs.vertex.link/en/latest/synapse/changelog.html' +[project.entry-points.pytest11] +synapse = 'synapse.utils.stormcov' + [tool.setuptools] include-package-data = true diff --git a/synapse/utils/stormcov.py b/synapse/utils/stormcov.py new file mode 100644 index 00000000000..cb20cce6e42 --- /dev/null +++ b/synapse/utils/stormcov.py @@ -0,0 +1,371 @@ +import io +import os +import sys +import inspect +import logging +import collections + +import lark +import regex +import pytest +import coverage + +from coverage.exceptions import NoSource + +import synapse.data as s_data +import synapse.common as s_common + +logger = logging.getLogger(__name__) + +def pytest_addoption(parser): + """Add options to control storm coverage.""" + + group = parser.getgroup('stormcov', 'storm coverage reporting') + group.addoption( + '--storm-exts', + action='store', + default='storm', + dest='stormexts', + help='Comma separated list of file extensions containing Storm. Default: storm', + ) + + # This option allows us to have coverage but not storm coverage (i.e. only + # Python coverage) + group.addoption( + '--no-stormcov', + action='store_true', + default=False, + dest='no_stormcov', + help='Disable stormcov. Default: False', + ) + +DISABLE = sys.monitoring.DISABLE + +def pytest_configure(config): + if config.option.no_stormcov or not config.pluginmanager.has_plugin('_cov') or config.option.no_cov: + return + + isworker = config.option.numprocesses is None or os.environ.get("PYTEST_XDIST_WORKER") is not None + config.pluginmanager.register(StormcovPlugin(config, isworker), 'stormcov') + +class StormcovPlugin: + + PARSE_METHODS = {'compute', 'once', 'lift', 'getPivsOut', 'getPivsIn'} + + def __init__(self, config, isworker): + self.toolid = 2 + self.isworker = isworker + self.config = config + self.handlers = { + 'ast.py': self.handle_ast, + 'view.py': self.handle_view, + 'stormctrl.py': self.handle_stormctrl, + } + + self.node_map = {} + self.text_map = {} + self.guid_map = {} + self.subq_map = {} + + self.freg = regex.compile(r'.*synapse/lib/(ast.py|view.py|stormctrl.py)$') + self.lines_hit = collections.defaultdict(set) + + grammar = s_data.getLark('storm') + + self.parser = lark.Lark(grammar, start='query', regex=True, parser='lalr', keep_all_tokens=True, + maybe_placeholders=False, propagate_positions=True) + + opts = config.option + + self.extensions = [e.strip() for e in opts.stormexts.split(',')] + + # --cov: Load all storm files in current directory. Only show coverage + # information for storm files that have greater than 0% coverage (had at + # least one line executed) + + # --cov=path/to/dir: Load all storm files in specified directory. Show + # coverage for all discovered storm files even if they have 0% coverage. + + self.covpaths = False + if (cov_source := self.config.option.cov_source) == [True]: + self.find_storm_files('.') + + else: + self.covpaths = True + for dirn in cov_source: + self.find_storm_files(dirn) + + def find_storm_files(self, dirn): + for path in self.find_executable_files(dirn): + with open(path, 'r') as f: + apth = os.path.abspath(path) + + try: + tree = self.parser.parse(f.read()) + except lark.exceptions.UnexpectedToken: + logger.warning('Skipping invalid storm file: %s', apth) + continue + + self.find_subqueries(tree, apth) + + guid = s_common.guid(str(tree)) + self.guid_map[guid] = apth + + def find_executable_files(self, src_dir): + rx = r"^[^#~!$@%^&*()+=,]+\.(" + "|".join(self.extensions) + r")$" + for (dirpath, dirnames, filenames) in os.walk(src_dir): + for filename in filenames: + if regex.search(rx, filename): + path = os.path.join(dirpath, filename) + yield path + + def find_subqueries(self, tree, path): + for rule in ('argvquery', 'embedquery'): + for node in tree.find_data(rule): + + subq = node.children[1] + if subq.meta.empty: + continue + + subg = s_common.guid(str(subq)) + line = (node.meta.line - 1) + + if subg in self.subq_map: + (pname, pline) = self.subq_map[subg] + logger.warning(f'Duplicate {rule} in {path} at line {line + 1}, coverage will ' + f'be reported on first instance in {pname} at line {pline + 1}') + continue + + self.subq_map[subg] = (path, line) + + @pytest.hookimpl(hookwrapper=True, tryfirst=True) + def pytest_runtestloop(self, session): + sys.monitoring.use_tool_id(self.toolid, 'pytest-stormcov') + sys.monitoring.set_events(self.toolid, sys.monitoring.events.PY_START) + sys.monitoring.register_callback(self.toolid, sys.monitoring.events.PY_START, self.sysmon_py_start) + + yield + + sys.monitoring.free_tool_id(self.toolid) + + # Get a reference to the pytest-cov plugin + covplug = session.config.pluginmanager.getplugin('_cov') + cov = covplug.cov_controller.cov + + # Load the existing data + cov.load() + data = cov.get_data() + + # Add our stormcov data + lines_hit = dict(self.lines_hit) + data.add_lines(lines_hit) + + if self.covpaths: + # Paths were specified so show all files that were discovered in the + # path even if they have 0% coverage + data.touch_files(self.guid_map.values(), 'synapse.utils.stormcov.StormReporterPlugin') + else: + # Paths were not specified so only show files that were hit + data.touch_files(lines_hit.keys(), 'synapse.utils.stormcov.StormReporterPlugin') + + # Save the coverage data + data.write() + + # Reset the pytest-cov internal cov report and then re-run it's + # pytest_runtestloop to get it to re-generate the test report. This + # should work with any test report format specified by it's command line + # options. + covplug.cov_report = io.StringIO() + for _ in covplug.pytest_runtestloop(session): + pass + + def sysmon_py_start(self, code, instruction_offset): + if (fname := self.freg.match(code.co_filename)): + return self.handlers[fname.group(1)](code) + return DISABLE + + def handle_ast(self, code, frame=None): + if frame is None: + if code.co_name == 'pullgenr': + frame = inspect.currentframe().f_back.f_back + + if frame.f_back.f_code.co_name != 'execStormCmd': + return + + frame = frame.f_back.f_back + + elif code.co_name not in self.PARSE_METHODS: + return DISABLE + + else: + frame = inspect.currentframe().f_back.f_back + + realnode = frame.f_locals.get('self') + if hasattr(realnode, '_coverage_hit'): + return + + realnode._coverage_hit = True + + node = realnode + while hasattr(node, 'parent'): + node = node.parent + + nodeid = id(node) + + info = self.node_map.get(nodeid, s_common.novalu) + if info is None: + return + + if info is not s_common.novalu: + self.mark_lines(frame, info) + return + + if not node.__class__.__name__ == 'Query': + self.node_map[nodeid] = None + return + + info = self.text_map.get(node.text, s_common.novalu) + if info is not s_common.novalu: + self.mark_lines(frame, info) + return + + tree = self.parser.parse(node.text) + guid = s_common.guid(str(tree)) + + subq = self.subq_map.get(guid) + if subq is not None: + (filename, offs) = subq + else: + filename = self.guid_map.get(guid) + offs = 0 + + if filename is None: + self.node_map[nodeid] = None + return + + self.node_map[nodeid] = (filename, offs) + self.text_map[node.text] = (filename, offs) + self.mark_lines(frame, (filename, offs)) + + def mark_lines(self, frame, info): + astn = frame.f_locals.get('self') + fname, offs = info + strt = astn.astinfo.sline + if astn.astinfo.isterm: + fini = astn.astinfo.eline + else: + fini = strt + + self.lines_hit[fname].update(range(strt + offs, fini + offs + 1)) + + PIVOT_METHODS = {'nodesByPropValu', 'nodesByPropArray', 'nodesByTag', 'getNodeByNdef'} + def handle_view(self, code): + if code.co_name not in self.PIVOT_METHODS: + return DISABLE + + frame = inspect.currentframe().f_back.f_back + if frame.f_code.co_name != 'run': + return + return self.handle_ast(code, frame=frame) + + def handle_stormctrl(self, code): + if code.co_name != '__init__': + return DISABLE + return self.handle_ast(code, frame=inspect.currentframe().f_back.f_back.f_back) + +TOKENS = [ + 'ABSPROP', + 'ABSPROPNOUNIV', + 'PROPS', + 'UNIVNAME', + 'EXPRUNIVNAME', + 'RELNAME', + 'EXPRRELNAME', + 'ALLTAGS', + 'BREAK', + 'CONTINUE', + 'CMDNAME', + 'TAGMATCH', + 'NONQUOTEWORD', + 'VARTOKN', + 'EXPRVARTOKN', + 'NUMBER', + 'HEXNUMBER', + 'OCTNUMBER', + 'BOOL', + 'EXPRTIMES', + '_EMIT', + '_STOP', + '_RETURN', +] + +class StormReporter(coverage.FileReporter): + def __init__(self, filename, parser): + super().__init__(filename) + + self._parser = parser + self._source = None + + def source(self): + if self._source is None: + try: + with open(self.filename, 'r') as f: + self._source = f.read() + + except (OSError, UnicodeError) as exc: + raise NoSource(f"Couldn't read {self.filename}: {exc}") + return self._source + + def lines(self): + source_lines = set() + + tree = self._parser.parse(self.source()) + + for token in tree.scan_values(lambda v: isinstance(v, lark.lexer.Token)): + if token.type in TOKENS: + source_lines.add(token.line) + + return source_lines - self.excluded_lines() + + def excluded_lines(self): + excluded_lines = set() + + pragma = 'pragma: no cover' + start = 'pragma: no cover start' + stop = 'pragma: no cover stop' + + lines = self.source().splitlines() + nocov = [(lineno + 1, text) for (lineno, text) in enumerate(lines) if pragma in text] + + block = None + for (lineno, text) in nocov: + if stop in text: + if block is not None: + # End a multi-line block + excluded_lines |= set(range(block, lineno + 1)) + block = None + continue + + if start in text: + if block is None: + # Start a multi-line block + block = lineno + continue + + if pragma in text: + excluded_lines.add(lineno) + + return excluded_lines + +class StormReporterPlugin(coverage.CoveragePlugin): + def __init__(self): + grammar = s_data.getLark('storm') + self.parser = lark.Lark(grammar, start='query', regex=True, parser='lalr', keep_all_tokens=True, + maybe_placeholders=False, propagate_positions=True) + + def file_reporter(self, filename): + return StormReporter(filename, self.parser) + +def coverage_init(reg, options): + plugin = StormReporterPlugin() + reg.add_file_tracer(plugin) diff --git a/synapse/utils/stormcov/__init__.py b/synapse/utils/stormcov/__init__.py deleted file mode 100644 index 6c27fd9f448..00000000000 --- a/synapse/utils/stormcov/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from .plugin import StormPlugin - -def coverage_init(reg, options): - plugin = StormPlugin(options) - reg.add_configurer(plugin) - reg.add_file_tracer(plugin) diff --git a/synapse/utils/stormcov/plugin.py b/synapse/utils/stormcov/plugin.py deleted file mode 100644 index 63826160681..00000000000 --- a/synapse/utils/stormcov/plugin.py +++ /dev/null @@ -1,273 +0,0 @@ -import os -import lark -import regex -import logging -import coverage - -from coverage.exceptions import NoSource - -import synapse.data as s_data -import synapse.common as s_common - -logger = logging.getLogger(__name__) - -class StormPlugin(coverage.CoveragePlugin, coverage.FileTracer): - - def __init__(self, options): - extensions = options.get('storm_extensions', 'storm') - self.extensions = [e.strip() for e in extensions.split(',')] - - grammar = s_data.getLark('storm') - self.parser = lark.Lark(grammar, start='query', debug=True, regex=True, - parser='lalr', keep_all_tokens=True, maybe_placeholders=False, - propagate_positions=True) - - self.node_map = {} - self.text_map = {} - self.guid_map = {} - self.subq_map = {} - - dirs = options.get('storm_dirs', '.') - if dirs: - self.stormdirs = [d.strip() for d in dirs.split(',')] - for dirn in self.stormdirs: - self.find_storm_files(dirn) - - def find_storm_files(self, dirn): - for path in self.find_executable_files(dirn): - with open(path, 'r') as f: - apth = os.path.abspath(path) - - tree = self.parser.parse(f.read()) - self.find_subqueries(tree, apth) - - guid = s_common.guid(str(tree)) - self.guid_map[guid] = apth - - def find_subqueries(self, tree, path): - for rule in ('argvquery', 'embedquery'): - for node in tree.find_data(rule): - - subq = node.children[1] - if subq.meta.empty: - continue - - subg = s_common.guid(str(subq)) - line = (subq.meta.line - 1) - - if subg in self.subq_map: - (pname, pline) = self.subq_map[subg] - logger.warning(f'Duplicate {rule} in {path} at line {line + 1}, coverage will ' - f'be reported on first instance in {pname} at line {pline + 1}') - continue - - self.subq_map[subg] = (path, subq.meta.line - 1) - - def file_tracer(self, filename): - if filename.endswith('synapse/lib/ast.py'): - return self - - if filename.endswith('synapse/lib/stormctrl.py'): - return StormCtrlTracer(self) - - if filename.endswith('synapse/lib/view.py'): - return PivotTracer(self) - - return None - - def file_reporter(self, filename): - return StormReporter(filename, self.parser) - - def find_executable_files(self, src_dir): - rx = r"^[^#~!$@%^&*()+=,]+\.(" + "|".join(self.extensions) + r")$" - for (dirpath, dirnames, filenames) in os.walk(src_dir): - for filename in filenames: - if regex.search(rx, filename): - path = os.path.join(dirpath, filename) - yield path - - def has_dynamic_source_filename(self): - return True - - PARSE_METHODS = {'compute', 'once', 'lift', 'getPivsOut', 'getPivsIn'} - - def dynamic_source_filename(self, filename, frame, force=False): - - if frame.f_code.co_name == 'pullgenr': - if frame.f_back.f_code.co_name != 'execStormCmd': - return None - frame = frame.f_back.f_back - - elif frame.f_code.co_name not in self.PARSE_METHODS and not force: - return None - - realnode = frame.f_locals.get('self') - node = realnode - while hasattr(node, 'parent'): - node = node.parent - - nodeid = id(node) - - info = self.node_map.get(nodeid, s_common.novalu) - if info is None: - return - - if info is not s_common.novalu: - realnode._coverage_offs = info[1] - return info[0] - - if not node.__class__.__name__ == 'Query': - self.node_map[nodeid] = None - return - - info = self.text_map.get(node.text, s_common.novalu) - if info is not s_common.novalu: - realnode._coverage_offs = info[1] - return info[0] - - tree = self.parser.parse(node.text) - guid = s_common.guid(str(tree)) - - subq = self.subq_map.get(guid) - if subq is not None: - (filename, offs) = subq - else: - filename = self.guid_map.get(guid) - offs = 0 - - realnode._coverage_offs = offs - - self.node_map[nodeid] = (filename, offs) - self.text_map[node.text] = (filename, offs) - return filename - - def line_number_range(self, frame): - if frame.f_code.co_name == 'pullgenr': - frame = frame.f_back.f_back - - astn = frame.f_locals.get('self') - - offs = astn._coverage_offs - strt = astn.astinfo.sline - if astn.astinfo.isterm: - fini = astn.astinfo.eline - else: - fini = strt - - return (strt + offs, fini + offs) - -class StormCtrlTracer(coverage.FileTracer): - def __init__(self, parent): - self.parent = parent - - def has_dynamic_source_filename(self): - return True - - def dynamic_source_filename(self, filename, frame): - if frame.f_code.co_name != '__init__': - return None - return self.parent.dynamic_source_filename(None, frame.f_back, force=True) - - def line_number_range(self, frame): - return self.parent.line_number_range(frame.f_back) - -class PivotTracer(coverage.FileTracer): - def __init__(self, parent): - self.parent = parent - - def has_dynamic_source_filename(self): - return True - - PARSE_METHODS = {'nodesByPropValu', 'nodesByPropArray', 'nodesByTag', 'getNodeByNdef'} - - def dynamic_source_filename(self, filename, frame): - if frame.f_code.co_name not in self.PARSE_METHODS or frame.f_back.f_code.co_name != 'run': - return None - return self.parent.dynamic_source_filename(None, frame.f_back, force=True) - - def line_number_range(self, frame): - return self.parent.line_number_range(frame.f_back) - -TOKENS = [ - 'ABSPROP', - 'ABSPROPNOUNIV', - 'PROPS', - 'UNIVNAME', - 'EXPRUNIVNAME', - 'RELNAME', - 'EXPRRELNAME', - 'ALLTAGS', - 'BREAK', - 'CONTINUE', - 'CMDNAME', - 'TAGMATCH', - 'NONQUOTEWORD', - 'VARTOKN', - 'EXPRVARTOKN', - 'NUMBER', - 'HEXNUMBER', - 'OCTNUMBER', - 'BOOL', - 'EXPRTIMES', - '_EMIT', - '_STOP', - '_RETURN', -] - -class StormReporter(coverage.FileReporter): - def __init__(self, filename, parser): - super().__init__(filename) - - self._parser = parser - self._source = None - - def source(self): - if self._source is None: - try: - with open(self.filename, 'r') as f: - self._source = f.read() - - except (OSError, UnicodeError) as exc: - raise NoSource(f"Couldn't read {self.filename}: {exc}") - return self._source - - def lines(self): - source_lines = set() - - tree = self._parser.parse(self.source()) - - for token in tree.scan_values(lambda v: isinstance(v, lark.lexer.Token)): - if token.type in TOKENS: - source_lines.add(token.line) - - return source_lines - self.excluded_lines() - - def excluded_lines(self): - excluded_lines = set() - - pragma = 'pragma: no cover' - start = 'pragma: no cover start' - stop = 'pragma: no cover stop' - - lines = self.source().splitlines() - nocov = [(lineno + 1, text) for (lineno, text) in enumerate(lines) if pragma in text] - - block = None - for (lineno, text) in nocov: - if stop in text: - if block is not None: - # End a multi-line block - excluded_lines |= set(range(block, lineno + 1)) - block = None - continue - - if start in text: - if block is None: - # Start a multi-line block - block = lineno - continue - - if pragma in text: - excluded_lines.add(lineno) - - return excluded_lines From fa9dfdbd7cd000360cbaf148b7d3482e991d4f72 Mon Sep 17 00:00:00 2001 From: blackout Date: Fri, 27 Feb 2026 14:18:41 -0500 Subject: [PATCH 02/20] wip --- synapse/utils/stormcov.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/synapse/utils/stormcov.py b/synapse/utils/stormcov.py index cb20cce6e42..43d3af521bd 100644 --- a/synapse/utils/stormcov.py +++ b/synapse/utils/stormcov.py @@ -357,6 +357,9 @@ def excluded_lines(self): return excluded_lines +# StormReporterPlugin and coverage_init below are both to support the command +# line coverage tools being able to interpret coverage results generated by +# stormcov class StormReporterPlugin(coverage.CoveragePlugin): def __init__(self): grammar = s_data.getLark('storm') From 180db188e88857c4842190e2f2f68a36761526b5 Mon Sep 17 00:00:00 2001 From: blackout Date: Sat, 28 Feb 2026 14:26:09 -0500 Subject: [PATCH 03/20] wip --- .coveragerc | 10 --- synapse/utils/stormcov.py | 136 +++++++++++++++++++++++--------------- 2 files changed, 84 insertions(+), 62 deletions(-) diff --git a/.coveragerc b/.coveragerc index 8af9056b0c6..501ba333762 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,13 +1,3 @@ [report] omit = */synapse/tests/test_* - -; Uncomment this section to enable code coverage of storm files in the -; storm_dirs directory listed below. This is disabled by default right now -; because it's pretty intensive and imposes a large perf hit on the already slow -; tests. -;[run] -;plugins = synapse.utils.stormcov - -[synapse.utils.stormcov] -storm_dirs = synapse/assets/storm diff --git a/synapse/utils/stormcov.py b/synapse/utils/stormcov.py index 43d3af521bd..438cc7dfd28 100644 --- a/synapse/utils/stormcov.py +++ b/synapse/utils/stormcov.py @@ -1,8 +1,8 @@ -import io import os import sys import inspect import logging +import pathlib import collections import lark @@ -17,10 +17,28 @@ logger = logging.getLogger(__name__) +PACKAGE_DIR = pathlib.Path('packages').absolute() + def pytest_addoption(parser): """Add options to control storm coverage.""" group = parser.getgroup('stormcov', 'storm coverage reporting') + + group.addoption( + '--stormcov', + action='store_true', + default=False, + dest='stormcov', + help='Enable stormcov. Default: False', + ) + + group.addoption( + '--storm-dirs', + action='store', + dest='stormdirs', + help='Comma separated list of paths to search for Storm files. Default: autodiscover stormdirs based on executed tests.', + ) + group.addoption( '--storm-exts', action='store', @@ -29,32 +47,35 @@ def pytest_addoption(parser): help='Comma separated list of file extensions containing Storm. Default: storm', ) - # This option allows us to have coverage but not storm coverage (i.e. only - # Python coverage) group.addoption( - '--no-stormcov', + '--stormcov-append', action='store_true', default=False, - dest='no_stormcov', - help='Disable stormcov. Default: False', + dest='stormcov_append', + help='Append to existing coverage reports instead of erasing.', + ) + + group.addoption( + '--stormcov-basedir', + default=PACKAGE_DIR, + type=pathlib.Path, + help='The base package directory. Useful for monorepo environments.', ) DISABLE = sys.monitoring.DISABLE def pytest_configure(config): - if config.option.no_stormcov or not config.pluginmanager.has_plugin('_cov') or config.option.no_cov: + if not config.option.stormcov and not (config.option.stormdirs or config.option.stormcov_append): return - isworker = config.option.numprocesses is None or os.environ.get("PYTEST_XDIST_WORKER") is not None - config.pluginmanager.register(StormcovPlugin(config, isworker), 'stormcov') + config.pluginmanager.register(StormcovPlugin(config), 'stormcov') class StormcovPlugin: PARSE_METHODS = {'compute', 'once', 'lift', 'getPivsOut', 'getPivsIn'} - def __init__(self, config, isworker): + def __init__(self, config): self.toolid = 2 - self.isworker = isworker self.config = config self.handlers = { 'ast.py': self.handle_ast, @@ -77,24 +98,11 @@ def __init__(self, config, isworker): opts = config.option + self.append = config.options.stormcov_append + self.stormdirs = config.option.stormdirs + self.basedir = config.option.stormcov_basedir self.extensions = [e.strip() for e in opts.stormexts.split(',')] - # --cov: Load all storm files in current directory. Only show coverage - # information for storm files that have greater than 0% coverage (had at - # least one line executed) - - # --cov=path/to/dir: Load all storm files in specified directory. Show - # coverage for all discovered storm files even if they have 0% coverage. - - self.covpaths = False - if (cov_source := self.config.option.cov_source) == [True]: - self.find_storm_files('.') - - else: - self.covpaths = True - for dirn in cov_source: - self.find_storm_files(dirn) - def find_storm_files(self, dirn): for path in self.find_executable_files(dirn): with open(path, 'r') as f: @@ -138,46 +146,70 @@ def find_subqueries(self, tree, path): self.subq_map[subg] = (path, line) - @pytest.hookimpl(hookwrapper=True, tryfirst=True) + @pytest.hookimpl(wrapper=True) def pytest_runtestloop(self, session): sys.monitoring.use_tool_id(self.toolid, 'pytest-stormcov') sys.monitoring.set_events(self.toolid, sys.monitoring.events.PY_START) sys.monitoring.register_callback(self.toolid, sys.monitoring.events.PY_START, self.sysmon_py_start) + self.cov = coverage.Coverage.current() + + if self.cov is None: + self.cov = coverage.Coverage() + + if not self.append: + self.cov.erase() + yield - sys.monitoring.free_tool_id(self.toolid) + self.cov.load() - # Get a reference to the pytest-cov plugin - covplug = session.config.pluginmanager.getplugin('_cov') - cov = covplug.cov_controller.cov + sys.monitoring.free_tool_id(self.toolid) - # Load the existing data - cov.load() - data = cov.get_data() + data = self.cov.get_data() # Add our stormcov data - lines_hit = dict(self.lines_hit) - data.add_lines(lines_hit) - - if self.covpaths: - # Paths were specified so show all files that were discovered in the - # path even if they have 0% coverage - data.touch_files(self.guid_map.values(), 'synapse.utils.stormcov.StormReporterPlugin') - else: - # Paths were not specified so only show files that were hit - data.touch_files(lines_hit.keys(), 'synapse.utils.stormcov.StormReporterPlugin') + data.add_lines(dict(self.lines_hit)) + data.touch_files(self.guid_map.values(), 'synapse.utils.stormcov.StormReporterPlugin') # Save the coverage data data.write() - # Reset the pytest-cov internal cov report and then re-run it's - # pytest_runtestloop to get it to re-generate the test report. This - # should work with any test report format specified by it's command line - # options. - covplug.cov_report = io.StringIO() - for _ in covplug.pytest_runtestloop(session): - pass + def discover_stormdirs(self, testpaths: list[pathlib.Path]): + # If a specific set of directories were specified, use that + if self.stormdirs: + for dirn in self.stormdirs.split(','): + self.find_storm_files(dirn) + return + + stormdirs = set() + + # Iterate through the tests, get their path, and add the containing storm package directory + for testpath in testpaths: + if testpath.is_relative_to(self.basedir): + stormdir = str(self.basedir / testpath.relative_to(self.basedir).parts[0]) + stormdirs.add(stormdir) + + for dirn in stormdirs: + self.find_storm_files(dirn) + + @pytest.hookimpl(wrapper=True) + def pytest_collection_modifyitems(self, config, items): + # Note: If using xdist, this function executes on each worker node + testpaths = [item.path for item in items] + self.discover_stormdirs(testpaths) + yield + + @pytest.hookimpl(wrapper=True) + def pytest_xdist_node_collection_finished(self, node, ids): + # Note: This hook allows the controller to get a list of test ids so we can build a list of storm dirs to present stormterm coverage + testpaths = [pathlib.Path(testid.split('::')[0]).absolute() for testid in ids] + self.discover_stormdirs(testpaths) + yield + + @pytest.hookimpl + def pytest_terminal_summary(self, terminalreporter, exitstatus, config): + self.cov.report(skip_covered=False, skip_empty=False) def sysmon_py_start(self, code, instruction_offset): if (fname := self.freg.match(code.co_filename)): From d75a0bcb4819665543a6368071855dc20f387450 Mon Sep 17 00:00:00 2001 From: blackout Date: Sat, 28 Feb 2026 15:43:28 -0500 Subject: [PATCH 04/20] wip --- synapse/utils/stormcov.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/utils/stormcov.py b/synapse/utils/stormcov.py index 438cc7dfd28..56cd225a5cf 100644 --- a/synapse/utils/stormcov.py +++ b/synapse/utils/stormcov.py @@ -98,7 +98,7 @@ def __init__(self, config): opts = config.option - self.append = config.options.stormcov_append + self.append = config.option.stormcov_append self.stormdirs = config.option.stormdirs self.basedir = config.option.stormcov_basedir self.extensions = [e.strip() for e in opts.stormexts.split(',')] From e3682c56e5b1c7997b00d9f91c7cfe655db75ee2 Mon Sep 17 00:00:00 2001 From: blackout Date: Mon, 2 Mar 2026 13:19:59 -0500 Subject: [PATCH 05/20] wip --- synapse/lib/parser.py | 2 +- synapse/tests/files/stormcov/dupesubs.storm | 6 +- synapse/tests/test_utils_stormcov.py | 94 ++++++++++++++++++++- synapse/utils/stormcov.py | 89 +++++++++++++------ 4 files changed, 159 insertions(+), 32 deletions(-) diff --git a/synapse/lib/parser.py b/synapse/lib/parser.py index 4bb0a1f88d9..023ad1c5f35 100644 --- a/synapse/lib/parser.py +++ b/synapse/lib/parser.py @@ -261,7 +261,7 @@ def embedquery(self, meta, kids): kids[0].astinfo = astinfo - return s_ast.EmbedQuery(astinfo, kids[0].getAstText(), kids=kids) + return s_ast.EmbedQuery(astinfo, kids[0].text, kids=kids) @lark.v_args(meta=True) def funccall(self, meta, kids): diff --git a/synapse/tests/files/stormcov/dupesubs.storm b/synapse/tests/files/stormcov/dupesubs.storm index 9b082696240..2eb91e8ac7d 100644 --- a/synapse/tests/files/stormcov/dupesubs.storm +++ b/synapse/tests/files/stormcov/dupesubs.storm @@ -1,10 +1,10 @@ -$eq = ${ [inet:fqdn=bar.com] } -$eq = ${ [inet:fqdn=bar.com] } +$eq = ${ [inet:fqdn=bar.com] } +$eq = ${ [inet:fqdn=bar.com] } tee { [inet:fqdn=foo.com] } { [inet:fqdn=baz.com ] } $eq - +| tee { [inet:fqdn=argv.com] } { diff --git a/synapse/tests/test_utils_stormcov.py b/synapse/tests/test_utils_stormcov.py index 046ca7c6c3c..6a7daa87708 100644 --- a/synapse/tests/test_utils_stormcov.py +++ b/synapse/tests/test_utils_stormcov.py @@ -1,5 +1,8 @@ +import os import logging import inspect +import pathlib +import argparse import unittest.mock as mock from coverage.exceptions import NoSource @@ -15,12 +18,25 @@ logger = logging.getLogger(__name__) +class StormcovConfig: + ''' + Helper class to simulate pytest config + ''' + def __init__(self, stormcov=False, stormdirs=[], stormexts='storm', stormcov_append=False, stormcov_basedir=s_stormcov.PACKAGE_DIR): + self.option = argparse.Namespace( + stormcov=stormcov, + stormdirs=stormdirs, + stormexts=stormexts, + stormcov_append=stormcov_append, + stormcov_basedir=stormcov_basedir, + ) + class TestUtilsStormcov(s_utils.SynTest): - async def test_basics(self): + async def _test_basics(self): opts = {"storm_dirs": "synapse/tests/files/stormcov"} s_stormcov.coverage_init(mock.MagicMock(), opts) - plugin = s_stormcov.StormPlugin(opts) + plugin = s_stormcov.StormcovPlugin(opts) reporter = plugin.file_reporter(s_files.getAssetPath('stormcov/stormctrl.storm')) self.eq(s_files.getAssetStr('stormcov/stormctrl.storm'), reporter.source()) @@ -146,3 +162,77 @@ async def compute(self, runt, valu): with mock.patch('synapse.lib.ast.Const.compute', compute): await core.nodes(s_files.getAssetStr('stormcov/pivot.storm')) + + async def test_stormcov_basics(self): + basedir = s_files.ASSETS + stormdir = os.path.join(basedir, 'stormcov') + opts = StormcovConfig(stormdirs=stormdir, stormcov_basedir=basedir) + + stormcov = s_stormcov.StormcovPlugin(opts) + stormcov.find_storm_files(stormdir) + self.eq( + list(stormcov.guid_map.values()), + [ + s_files.getAssetPath('stormcov/spin.storm'), + s_files.getAssetPath('stormcov/stormctrl.storm'), + s_files.getAssetPath('stormcov/lookup.storm'), + s_files.getAssetPath('stormcov/pragma-nocov.storm'), + s_files.getAssetPath('stormcov/dupesubs.storm'), + s_files.getAssetPath('stormcov/pivot.storm'), + s_files.getAssetPath('stormcov/argvquery.storm'), + ] + ) + + async with self.getTestCore() as core: + async def check_cov(filename, expected, stormopts=None): + stormcov._start_sysmon() + await core.stormlist(s_files.getAssetStr(filename), opts=stormopts) + stormcov._stop_sysmon() + + self.eq(dict(stormcov.lines_hit), {s_files.getAssetPath(filename): expected}) + + stormcov.reset() + + await check_cov('stormcov/dupesubs.storm', {1, 2, 3, 4, 5, 6, 8, 9, 11}) + await check_cov('stormcov/argvquery.storm', {1, 2, 3, 4, 8}) + await check_cov('stormcov/stormctrl.storm', {1, 2, 3, 6}) + await check_cov('stormcov/pivot.storm', {1, 2}) + await check_cov('stormcov/pragma-nocov.storm', {1, 2, 3, 18}) + await check_cov('stormcov/spin.storm', {2, 3}) + + async def test_stormcov_lookup(self): + basedir = s_files.ASSETS + stormdir = os.path.join(basedir, 'stormcov') + opts = StormcovConfig(stormdirs=stormdir, stormcov_basedir=basedir) + + stormcov = s_stormcov.StormcovPlugin(opts) + stormcov.find_storm_files(stormdir) + + async with self.getTestCore() as core: + await core.nodes('[ inet:fqdn=vertex.link ]') + stormcov._start_sysmon() + await core.stormlist(s_files.getAssetStr('stormcov/lookup.storm'), opts={'mode': 'lookup'}) + stormcov._stop_sysmon() + + # No coverage for lookup mode + self.len(0, dict(stormcov.lines_hit)) + + async def test_stormcov_stormreporter(self): + parser = s_stormcov.get_parser() + + async def check_lines(filename, expected): + reporter = s_stormcov.StormReporter(s_files.getAssetPath(filename), parser) + self.eq(reporter.lines(), expected) + + await check_lines('stormcov/pivot.storm', {1, 2}) + await check_lines('stormcov/stormctrl.storm', {1, 2, 3, 6}) + await check_lines('stormcov/pragma-nocov.storm', {1, 2, 3, 12, 18}) + await check_lines('stormcov/spin.storm', {2, 3}) + await check_lines('stormcov/argvquery.storm', {1, 2, 3, 4, 8}) + await check_lines('stormcov/lookup.storm', {1, 2, 3, 5, 6}) + await check_lines('stormcov/dupesubs.storm', {1, 2, 3, 4, 5, 6, 8, 9, 11}) + + async def test_stormcov_stormreporter_plugin(self): + plugin = s_stormcov.StormReporterPlugin() + reporter = plugin.file_reporter(s_files.getAssetPath('stormcov/pivot.storm')) + self.eq(reporter.lines(), {1, 2}) diff --git a/synapse/utils/stormcov.py b/synapse/utils/stormcov.py index 56cd225a5cf..fbbbadd9124 100644 --- a/synapse/utils/stormcov.py +++ b/synapse/utils/stormcov.py @@ -19,7 +19,7 @@ PACKAGE_DIR = pathlib.Path('packages').absolute() -def pytest_addoption(parser): +def pytest_addoption(parser): # pragma: no cover """Add options to control storm coverage.""" group = parser.getgroup('stormcov', 'storm coverage reporting') @@ -64,18 +64,26 @@ def pytest_addoption(parser): DISABLE = sys.monitoring.DISABLE -def pytest_configure(config): +def pytest_configure(config): # pragma: no cover if not config.option.stormcov and not (config.option.stormdirs or config.option.stormcov_append): return config.pluginmanager.register(StormcovPlugin(config), 'stormcov') +def get_parser(): + grammar = s_data.getLark('storm') + + return lark.Lark(grammar, start='query', regex=True, parser='lalr', keep_all_tokens=True, + maybe_placeholders=False, propagate_positions=True) + class StormcovPlugin: PARSE_METHODS = {'compute', 'once', 'lift', 'getPivsOut', 'getPivsIn'} def __init__(self, config): - self.toolid = 2 + self.toolid = sys.monitoring.COVERAGE_ID + self._freetool = False + self.config = config self.handlers = { 'ast.py': self.handle_ast, @@ -89,12 +97,9 @@ def __init__(self, config): self.subq_map = {} self.freg = regex.compile(r'.*synapse/lib/(ast.py|view.py|stormctrl.py)$') - self.lines_hit = collections.defaultdict(set) - - grammar = s_data.getLark('storm') + self.reset() - self.parser = lark.Lark(grammar, start='query', regex=True, parser='lalr', keep_all_tokens=True, - maybe_placeholders=False, propagate_positions=True) + self.parser = get_parser() opts = config.option @@ -103,6 +108,11 @@ def __init__(self, config): self.basedir = config.option.stormcov_basedir self.extensions = [e.strip() for e in opts.stormexts.split(',')] + self.debug = [] + + def reset(self): + self.lines_hit = collections.defaultdict(set) + def find_storm_files(self, dirn): for path in self.find_executable_files(dirn): with open(path, 'r') as f: @@ -135,9 +145,18 @@ def find_subqueries(self, tree, path): if subq.meta.empty: continue - subg = s_common.guid(str(subq)) + soff = subq.meta.start_pos + eoff = subq.meta.end_pos + + if rule == 'embedquery': + soff = node.meta.start_pos + 2 + eoff = node.meta.end_pos - 1 + + subg = (s_common.guid(str(tree)), soff, eoff) line = (node.meta.line - 1) + logger.info(f'guid={subg[0]} {path=} {subq=} {soff=} {eoff=} {line=}') + if subg in self.subq_map: (pname, pline) = self.subq_map[subg] logger.warning(f'Duplicate {rule} in {path} at line {line + 1}, coverage will ' @@ -146,12 +165,22 @@ def find_subqueries(self, tree, path): self.subq_map[subg] = (path, line) - @pytest.hookimpl(wrapper=True) - def pytest_runtestloop(self, session): - sys.monitoring.use_tool_id(self.toolid, 'pytest-stormcov') + def _start_sysmon(self): + if sys.monitoring.get_tool(self.toolid) is None: + sys.monitoring.use_tool_id(self.toolid, 'pytest-stormcov') + self._freetool = True + sys.monitoring.set_events(self.toolid, sys.monitoring.events.PY_START) sys.monitoring.register_callback(self.toolid, sys.monitoring.events.PY_START, self.sysmon_py_start) + def _stop_sysmon(self): + if self._freetool: + sys.monitoring.free_tool_id(self.toolid) + self._freetool = False + + @pytest.hookimpl(wrapper=True) + def pytest_runtestloop(self, session): # pragma: no cover + self.cov = coverage.Coverage.current() if self.cov is None: @@ -160,12 +189,12 @@ def pytest_runtestloop(self, session): if not self.append: self.cov.erase() + self._start_sysmon() yield + self._stop_sysmon() self.cov.load() - sys.monitoring.free_tool_id(self.toolid) - data = self.cov.get_data() # Add our stormcov data @@ -194,21 +223,20 @@ def discover_stormdirs(self, testpaths: list[pathlib.Path]): self.find_storm_files(dirn) @pytest.hookimpl(wrapper=True) - def pytest_collection_modifyitems(self, config, items): + def pytest_collection_modifyitems(self, config, items): # pragma: no cover # Note: If using xdist, this function executes on each worker node testpaths = [item.path for item in items] self.discover_stormdirs(testpaths) yield @pytest.hookimpl(wrapper=True) - def pytest_xdist_node_collection_finished(self, node, ids): - # Note: This hook allows the controller to get a list of test ids so we can build a list of storm dirs to present stormterm coverage + def pytest_xdist_node_collection_finished(self, node, ids): # pragma: no cover + # Note: This hook allows the xdist controller to get a list of test ids so we can build a list of storm dirs to present stormterm coverage testpaths = [pathlib.Path(testid.split('::')[0]).absolute() for testid in ids] self.discover_stormdirs(testpaths) yield - @pytest.hookimpl - def pytest_terminal_summary(self, terminalreporter, exitstatus, config): + def pytest_terminal_summary(self, terminalreporter, exitstatus, config): # pragma: no cover self.cov.report(skip_covered=False, skip_empty=False) def sysmon_py_start(self, code, instruction_offset): @@ -233,6 +261,7 @@ def handle_ast(self, code, frame=None): frame = inspect.currentframe().f_back.f_back realnode = frame.f_locals.get('self') + realcls = realnode.__class__.__name__ if hasattr(realnode, '_coverage_hit'): return @@ -242,6 +271,11 @@ def handle_ast(self, code, frame=None): while hasattr(node, 'parent'): node = node.parent + topnode = node + + if realcls in ('ArgvQuery', 'EmbedQuery'): + node = realnode.kids[0] + nodeid = id(node) info = self.node_map.get(nodeid, s_common.novalu) @@ -261,12 +295,15 @@ def handle_ast(self, code, frame=None): self.mark_lines(frame, info) return - tree = self.parser.parse(node.text) + tree = self.parser.parse(topnode.text) guid = s_common.guid(str(tree)) - subq = self.subq_map.get(guid) - if subq is not None: - (filename, offs) = subq + if realcls in ('ArgvQuery', 'EmbedQuery'): + posinfo = node.getPosInfo() + if realcls == 'EmbedQuery': + posinfo = realnode.getPosInfo() + soff, eoff = posinfo['offsets'] + filename, offs = self.subq_map.get((guid, soff, eoff), (None, 0)) else: filename = self.guid_map.get(guid) offs = 0 @@ -280,6 +317,7 @@ def handle_ast(self, code, frame=None): self.mark_lines(frame, (filename, offs)) def mark_lines(self, frame, info): + self.debug.append((frame, info)) astn = frame.f_locals.get('self') fname, offs = info strt = astn.astinfo.sline @@ -288,6 +326,7 @@ def mark_lines(self, frame, info): else: fini = strt + logger.info(f'{fname} {offs} {strt + offs=} {fini + offs + 1=}') self.lines_hit[fname].update(range(strt + offs, fini + offs + 1)) PIVOT_METHODS = {'nodesByPropValu', 'nodesByPropArray', 'nodesByTag', 'getNodeByNdef'} @@ -394,9 +433,7 @@ def excluded_lines(self): # stormcov class StormReporterPlugin(coverage.CoveragePlugin): def __init__(self): - grammar = s_data.getLark('storm') - self.parser = lark.Lark(grammar, start='query', regex=True, parser='lalr', keep_all_tokens=True, - maybe_placeholders=False, propagate_positions=True) + self.parser = get_parser() def file_reporter(self, filename): return StormReporter(filename, self.parser) From 963d76fce5e328053859c7675102dd1464510184 Mon Sep 17 00:00:00 2001 From: blackout Date: Tue, 3 Mar 2026 09:52:29 -0500 Subject: [PATCH 06/20] wip --- synapse/tests/files/stormcov/dupesubs.storm | 12 +++++-- synapse/tests/test_utils_stormcov.py | 2 +- synapse/utils/stormcov.py | 36 +++++++-------------- 3 files changed, 21 insertions(+), 29 deletions(-) diff --git a/synapse/tests/files/stormcov/dupesubs.storm b/synapse/tests/files/stormcov/dupesubs.storm index 2eb91e8ac7d..634b3858377 100644 --- a/synapse/tests/files/stormcov/dupesubs.storm +++ b/synapse/tests/files/stormcov/dupesubs.storm @@ -1,12 +1,18 @@ -$eq = ${ [inet:fqdn=bar.com] } -$eq = ${ [inet:fqdn=bar.com] } +$eq = ${ + [inet:fqdn=bar.com] +} +$eq = ${ + [inet:fqdn=bar.com] +} tee { [inet:fqdn=foo.com] } { [inet:fqdn=baz.com ] } $eq | tee { - [inet:fqdn=argv.com] + if (false) { + [inet:fqdn=argv.com] + } } { [inet:fqdn=argv.com] } diff --git a/synapse/tests/test_utils_stormcov.py b/synapse/tests/test_utils_stormcov.py index 6a7daa87708..bc805621a4a 100644 --- a/synapse/tests/test_utils_stormcov.py +++ b/synapse/tests/test_utils_stormcov.py @@ -230,7 +230,7 @@ async def check_lines(filename, expected): await check_lines('stormcov/spin.storm', {2, 3}) await check_lines('stormcov/argvquery.storm', {1, 2, 3, 4, 8}) await check_lines('stormcov/lookup.storm', {1, 2, 3, 5, 6}) - await check_lines('stormcov/dupesubs.storm', {1, 2, 3, 4, 5, 6, 8, 9, 11}) + await check_lines('stormcov/dupesubs.storm', {1, 2, 4, 5, 7, 8, 9, 10, 12, 13, 14, 17}) async def test_stormcov_stormreporter_plugin(self): plugin = s_stormcov.StormReporterPlugin() diff --git a/synapse/utils/stormcov.py b/synapse/utils/stormcov.py index fbbbadd9124..3e79a099e3f 100644 --- a/synapse/utils/stormcov.py +++ b/synapse/utils/stormcov.py @@ -82,6 +82,7 @@ class StormcovPlugin: def __init__(self, config): self.toolid = sys.monitoring.COVERAGE_ID + self._prevcb = None self._freetool = False self.config = config @@ -145,18 +146,9 @@ def find_subqueries(self, tree, path): if subq.meta.empty: continue - soff = subq.meta.start_pos - eoff = subq.meta.end_pos - - if rule == 'embedquery': - soff = node.meta.start_pos + 2 - eoff = node.meta.end_pos - 1 - - subg = (s_common.guid(str(tree)), soff, eoff) + subg = s_common.guid(str(subq)) line = (node.meta.line - 1) - logger.info(f'guid={subg[0]} {path=} {subq=} {soff=} {eoff=} {line=}') - if subg in self.subq_map: (pname, pline) = self.subq_map[subg] logger.warning(f'Duplicate {rule} in {path} at line {line + 1}, coverage will ' @@ -171,9 +163,13 @@ def _start_sysmon(self): self._freetool = True sys.monitoring.set_events(self.toolid, sys.monitoring.events.PY_START) - sys.monitoring.register_callback(self.toolid, sys.monitoring.events.PY_START, self.sysmon_py_start) + self._prevcb = sys.monitoring.register_callback(self.toolid, sys.monitoring.events.PY_START, self.sysmon_py_start) def _stop_sysmon(self): + if self._prevcb: + sys.monitoring.register_callback(self.toolid, sys.monitoring.events.PY_START, self._prevcb) + return + if self._freetool: sys.monitoring.free_tool_id(self.toolid) self._freetool = False @@ -261,7 +257,6 @@ def handle_ast(self, code, frame=None): frame = inspect.currentframe().f_back.f_back realnode = frame.f_locals.get('self') - realcls = realnode.__class__.__name__ if hasattr(realnode, '_coverage_hit'): return @@ -271,11 +266,6 @@ def handle_ast(self, code, frame=None): while hasattr(node, 'parent'): node = node.parent - topnode = node - - if realcls in ('ArgvQuery', 'EmbedQuery'): - node = realnode.kids[0] - nodeid = id(node) info = self.node_map.get(nodeid, s_common.novalu) @@ -295,15 +285,12 @@ def handle_ast(self, code, frame=None): self.mark_lines(frame, info) return - tree = self.parser.parse(topnode.text) + tree = self.parser.parse(node.text) guid = s_common.guid(str(tree)) - if realcls in ('ArgvQuery', 'EmbedQuery'): - posinfo = node.getPosInfo() - if realcls == 'EmbedQuery': - posinfo = realnode.getPosInfo() - soff, eoff = posinfo['offsets'] - filename, offs = self.subq_map.get((guid, soff, eoff), (None, 0)) + subq = self.subq_map.get(guid) + if subq is not None: + (filename, offs) = subq else: filename = self.guid_map.get(guid) offs = 0 @@ -326,7 +313,6 @@ def mark_lines(self, frame, info): else: fini = strt - logger.info(f'{fname} {offs} {strt + offs=} {fini + offs + 1=}') self.lines_hit[fname].update(range(strt + offs, fini + offs + 1)) PIVOT_METHODS = {'nodesByPropValu', 'nodesByPropArray', 'nodesByTag', 'getNodeByNdef'} From 56552b5efe2d1285501d774ef47b66977c69970c Mon Sep 17 00:00:00 2001 From: blackout Date: Tue, 3 Mar 2026 10:18:54 -0500 Subject: [PATCH 07/20] checkpoint --- synapse/tests/test_utils_stormcov.py | 144 +-------------------------- synapse/utils/stormcov.py | 10 +- 2 files changed, 6 insertions(+), 148 deletions(-) diff --git a/synapse/tests/test_utils_stormcov.py b/synapse/tests/test_utils_stormcov.py index bc805621a4a..f0278f909f9 100644 --- a/synapse/tests/test_utils_stormcov.py +++ b/synapse/tests/test_utils_stormcov.py @@ -1,15 +1,6 @@ import os import logging -import inspect -import pathlib import argparse -import unittest.mock as mock - -from coverage.exceptions import NoSource - -import synapse.lib.ast as s_ast -import synapse.lib.view as s_view -import synapse.lib.stormctrl as s_stormctrl import synapse.tests.files as s_files import synapse.tests.utils as s_utils @@ -32,137 +23,6 @@ def __init__(self, stormcov=False, stormdirs=[], stormexts='storm', stormcov_app ) class TestUtilsStormcov(s_utils.SynTest): - async def _test_basics(self): - - opts = {"storm_dirs": "synapse/tests/files/stormcov"} - s_stormcov.coverage_init(mock.MagicMock(), opts) - plugin = s_stormcov.StormcovPlugin(opts) - - reporter = plugin.file_reporter(s_files.getAssetPath('stormcov/stormctrl.storm')) - self.eq(s_files.getAssetStr('stormcov/stormctrl.storm'), reporter.source()) - self.eq(reporter.lines(), {1, 2, 3, 6}) - self.eq(reporter.translate_lines({1, 2}), {1, 2}) - - # no cover, no cover start, and no cover stop - reporter = plugin.file_reporter(s_files.getAssetPath('stormcov/pragma-nocov.storm')) - self.eq(reporter.lines(), {1, 2, 3, 12, 18}) - self.eq(reporter.excluded_lines(), {6, 8, 9, 10, 14, 15, 16}) - - # We no longer do whitespace transformations of lines. - reporter = plugin.file_reporter(s_files.getAssetPath('stormcov/spin.storm')) - self.eq(reporter.translate_lines({1, 2}), {1, 2}) - - with self.raises(NoSource): - reporter = plugin.file_reporter('newp') - reporter.source() - - stormtracer = plugin.file_tracer('synapse/lib/ast.py') - self.true(stormtracer.has_dynamic_source_filename()) - self.none(stormtracer.dynamic_source_filename(None, inspect.currentframe())) - - ctrltracer = plugin.file_tracer('synapse/lib/stormctrl.py') - self.true(ctrltracer.has_dynamic_source_filename()) - self.none(ctrltracer.dynamic_source_filename(None, inspect.currentframe())) - - pivotracer = plugin.file_tracer('synapse/lib/view.py') - self.true(pivotracer.has_dynamic_source_filename()) - self.none(pivotracer.dynamic_source_filename(None, inspect.currentframe())) - - self.none(plugin.file_tracer('newp')) - - async with self.getTestCore() as core: - orig = s_stormctrl.StormCtrlFlow.__init__ - def __init__(self, item=None): - frame = inspect.currentframe() - assert 'stormctrl.storm' in ctrltracer.dynamic_source_filename(None, frame) - assert (3, 3) == ctrltracer.line_number_range(frame) - orig(self, item=item) - - with mock.patch('synapse.lib.stormctrl.StormCtrlFlow.__init__', __init__): - await core.nodes(s_files.getAssetStr('stormcov/stormctrl.storm')) - - def __init__(self, item=None): - frame = inspect.currentframe() - assert 'argvquery.storm' in ctrltracer.dynamic_source_filename(None, frame) - assert (4, 4) == ctrltracer.line_number_range(frame) - orig(self, item=item) - - with mock.patch('synapse.lib.stormctrl.StormCtrlFlow.__init__', __init__): - await core.stormlist(s_files.getAssetStr('stormcov/argvquery.storm')) - - def __init__(self, item=None): - frame = inspect.currentframe() - assert ctrltracer.dynamic_source_filename(None, frame) is None - orig(self, item=item) - - with mock.patch('synapse.lib.stormctrl.StormCtrlFlow.__init__', __init__): - await core.stormlist(s_files.getAssetStr('stormcov/lookup.storm'), opts={'mode': 'lookup'}) - - orig = s_view.View.nodesByPropValu - async def nodesByPropValu(self, full, cmpr, valu, norm=True): - frame = inspect.currentframe() - if pivotracer.dynamic_source_filename(None, frame) is not None: - assert (2, 2) == pivotracer.line_number_range(frame) - - async for item in orig(self, full, cmpr, valu): - yield item - - with mock.patch('synapse.lib.view.View.nodesByPropValu', nodesByPropValu): - await core.nodes(s_files.getAssetStr('stormcov/pivot.storm')) - - async def pullone(genr): - gotone = None - async for gotone in genr: - break - - async def pullgenr(): - frame = inspect.currentframe() - assert 'spin.storm' in stormtracer.dynamic_source_filename(None, frame) - assert (3, 3) == stormtracer.line_number_range(frame) - - if gotone is None: - return - - yield gotone - async for item in genr: - yield item - - return pullgenr(), gotone is None - - with mock.patch('synapse.lib.ast.pullone', pullone): - await core.nodes(s_files.getAssetStr('stormcov/spin.storm')) - - async def pullone(genr): - gotone = None - async for gotone in genr: - break - - async def pullgenr(): - frame = inspect.currentframe() - assert stormtracer.dynamic_source_filename(None, frame) is None - - if gotone is None: - return - - yield gotone - async for item in genr: - yield item - - return pullgenr(), gotone is None - - with mock.patch('synapse.lib.ast.pullone', pullone): - await core.nodes(s_files.getAssetStr('stormcov/pivot.storm')) - - orig = s_ast.Const.compute - async def compute(self, runt, valu): - frame = inspect.currentframe() - assert 'pivot.storm' in stormtracer.dynamic_source_filename(None, frame) - assert stormtracer.line_number_range(frame) in ((1, 1), (2, 2)) - return await orig(self, runt, valu) - - with mock.patch('synapse.lib.ast.Const.compute', compute): - await core.nodes(s_files.getAssetStr('stormcov/pivot.storm')) - async def test_stormcov_basics(self): basedir = s_files.ASSETS stormdir = os.path.join(basedir, 'stormcov') @@ -193,8 +53,8 @@ async def check_cov(filename, expected, stormopts=None): stormcov.reset() - await check_cov('stormcov/dupesubs.storm', {1, 2, 3, 4, 5, 6, 8, 9, 11}) - await check_cov('stormcov/argvquery.storm', {1, 2, 3, 4, 8}) + # await check_cov('stormcov/dupesubs.storm', {1, 2, 3, 4, 5, 6, 8, 9, 11}) + # await check_cov('stormcov/argvquery.storm', {1, 2, 3, 4, 8}) await check_cov('stormcov/stormctrl.storm', {1, 2, 3, 6}) await check_cov('stormcov/pivot.storm', {1, 2}) await check_cov('stormcov/pragma-nocov.storm', {1, 2, 3, 18}) diff --git a/synapse/utils/stormcov.py b/synapse/utils/stormcov.py index 3e79a099e3f..ac479222e14 100644 --- a/synapse/utils/stormcov.py +++ b/synapse/utils/stormcov.py @@ -72,7 +72,6 @@ def pytest_configure(config): # pragma: no cover def get_parser(): grammar = s_data.getLark('storm') - return lark.Lark(grammar, start='query', regex=True, parser='lalr', keep_all_tokens=True, maybe_placeholders=False, propagate_positions=True) @@ -81,7 +80,9 @@ class StormcovPlugin: PARSE_METHODS = {'compute', 'once', 'lift', 'getPivsOut', 'getPivsIn'} def __init__(self, config): - self.toolid = sys.monitoring.COVERAGE_ID + # toolid=4 is not defined in sys.monitoring so there's less of a chance + # that we interfere with another tool, namely coveragepy. + self.toolid = 4 self._prevcb = None self._freetool = False @@ -109,8 +110,6 @@ def __init__(self, config): self.basedir = config.option.stormcov_basedir self.extensions = [e.strip() for e in opts.stormexts.split(',')] - self.debug = [] - def reset(self): self.lines_hit = collections.defaultdict(set) @@ -276,7 +275,7 @@ def handle_ast(self, code, frame=None): self.mark_lines(frame, info) return - if not node.__class__.__name__ == 'Query': + if node.__class__.__name__ != 'Query': self.node_map[nodeid] = None return @@ -304,7 +303,6 @@ def handle_ast(self, code, frame=None): self.mark_lines(frame, (filename, offs)) def mark_lines(self, frame, info): - self.debug.append((frame, info)) astn = frame.f_locals.get('self') fname, offs = info strt = astn.astinfo.sline From a6228d2f6325292eae9cfa4bacf2cc66e8e84fa5 Mon Sep 17 00:00:00 2001 From: blackout Date: Tue, 3 Mar 2026 11:03:37 -0500 Subject: [PATCH 08/20] wip --- synapse/tests/test_utils_stormcov.py | 4 ++-- synapse/utils/stormcov.py | 12 +++++++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/synapse/tests/test_utils_stormcov.py b/synapse/tests/test_utils_stormcov.py index f0278f909f9..19690c266be 100644 --- a/synapse/tests/test_utils_stormcov.py +++ b/synapse/tests/test_utils_stormcov.py @@ -53,8 +53,8 @@ async def check_cov(filename, expected, stormopts=None): stormcov.reset() - # await check_cov('stormcov/dupesubs.storm', {1, 2, 3, 4, 5, 6, 8, 9, 11}) - # await check_cov('stormcov/argvquery.storm', {1, 2, 3, 4, 8}) + await check_cov('stormcov/dupesubs.storm', {1, 2, 4, 7, 8, 9, 10, 12, 13, 16, 17}) + await check_cov('stormcov/argvquery.storm', {1, 2, 3, 4, 6, 8}) await check_cov('stormcov/stormctrl.storm', {1, 2, 3, 6}) await check_cov('stormcov/pivot.storm', {1, 2}) await check_cov('stormcov/pragma-nocov.storm', {1, 2, 3, 18}) diff --git a/synapse/utils/stormcov.py b/synapse/utils/stormcov.py index ac479222e14..b21ffdb2efb 100644 --- a/synapse/utils/stormcov.py +++ b/synapse/utils/stormcov.py @@ -86,6 +86,8 @@ def __init__(self, config): self._prevcb = None self._freetool = False + self.isworker = os.environ.get("PYTEST_XDIST_WORKER") is not None + self.config = config self.handlers = { 'ast.py': self.handle_ast, @@ -146,7 +148,7 @@ def find_subqueries(self, tree, path): continue subg = s_common.guid(str(subq)) - line = (node.meta.line - 1) + line = node.meta.line if subg in self.subq_map: (pname, pline) = self.subq_map[subg] @@ -192,6 +194,11 @@ def pytest_runtestloop(self, session): # pragma: no cover data = self.cov.get_data() + # Bail if there's no coverage. This could be because of a xdist worker + # that didn't have any work + if not self.lines_hit: + return + # Add our stormcov data data.add_lines(dict(self.lines_hit)) data.touch_files(self.guid_map.values(), 'synapse.utils.stormcov.StormReporterPlugin') @@ -232,6 +239,9 @@ def pytest_xdist_node_collection_finished(self, node, ids): # pragma: no cover yield def pytest_terminal_summary(self, terminalreporter, exitstatus, config): # pragma: no cover + if self.isworker: + return + self.cov.report(skip_covered=False, skip_empty=False) def sysmon_py_start(self, code, instruction_offset): From fc88b4179172b3ca6639e3ec9289ee27eb2baec1 Mon Sep 17 00:00:00 2001 From: blackout Date: Tue, 3 Mar 2026 14:22:31 -0500 Subject: [PATCH 09/20] wip --- pyproject.toml | 4 +- synapse/tests/files/stormcov/argvquery.storm | 2 + synapse/tests/files/stormcov/dupesubs.storm | 2 + synapse/tests/files/stormcov/embedquery.storm | 17 ++++++ synapse/tests/files/stormcov/lookup.storm | 1 + synapse/tests/files/stormcov/pivot.storm | 2 + .../tests/files/stormcov/pragma-nocov.storm | 2 + synapse/tests/files/stormcov/spin.storm | 2 + synapse/tests/files/stormcov/stormctrl.storm | 2 + synapse/tests/test_lib_grammar.py | 6 +- synapse/tests/test_lib_stormlib_macro.py | 6 +- synapse/tests/test_lib_stormtypes.py | 5 +- synapse/tests/test_utils_stormcov.py | 61 +++++++++++++------ 13 files changed, 85 insertions(+), 27 deletions(-) create mode 100644 synapse/tests/files/stormcov/embedquery.storm diff --git a/pyproject.toml b/pyproject.toml index 629dade786e..29412210618 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -80,8 +80,8 @@ Documentation = 'https://synapse.docs.vertex.link' Repository = 'https://github.com/vertexproject/synapse' Changelog = 'https://synapse.docs.vertex.link/en/latest/synapse/changelog.html' -[project.entry-points.pytest11] -synapse = 'synapse.utils.stormcov' +#[project.entry-points.pytest11] +#synapse = 'synapse.utils.stormcov' [tool.setuptools] include-package-data = true diff --git a/synapse/tests/files/stormcov/argvquery.storm b/synapse/tests/files/stormcov/argvquery.storm index 3d2314179a3..d2020c4cd96 100644 --- a/synapse/tests/files/stormcov/argvquery.storm +++ b/synapse/tests/files/stormcov/argvquery.storm @@ -6,3 +6,5 @@ function foo() { } {} } yield $foo() +// coverage: 1, 2, 3, 4, 6, 8 +// lines: 1, 2, 3, 4, 8 diff --git a/synapse/tests/files/stormcov/dupesubs.storm b/synapse/tests/files/stormcov/dupesubs.storm index 634b3858377..890443800a2 100644 --- a/synapse/tests/files/stormcov/dupesubs.storm +++ b/synapse/tests/files/stormcov/dupesubs.storm @@ -16,3 +16,5 @@ tee { } { [inet:fqdn=argv.com] } +// coverage: 1, 2, 4, 7, 8, 9, 10, 12, 13, 16, 17 +// lines: 1, 2, 4, 5, 7, 8, 9, 10, 12, 13, 14, 17 diff --git a/synapse/tests/files/stormcov/embedquery.storm b/synapse/tests/files/stormcov/embedquery.storm new file mode 100644 index 00000000000..6bf32a43e7c --- /dev/null +++ b/synapse/tests/files/stormcov/embedquery.storm @@ -0,0 +1,17 @@ +$eq1 = ${ + [ inet:fqdn=vertex.link ] +} +$eq2 = ${ + if (true) { + [ inet:fqdn=vertex.link ] + } +} +$eq3 = ${ + if (false) { + [ inet:fqdn=vertex.link ] + } +} + +tee $eq1 $eq2 $eq3 +// coverage: 1, 2, 4, 5, 6, 9, 10, 15 +// lines: 1, 2, 4, 5, 6, 9, 10, 11, 15 diff --git a/synapse/tests/files/stormcov/lookup.storm b/synapse/tests/files/stormcov/lookup.storm index 23172dc849b..28a733d1eb9 100644 --- a/synapse/tests/files/stormcov/lookup.storm +++ b/synapse/tests/files/stormcov/lookup.storm @@ -4,3 +4,4 @@ function foo() { } yield $foo() yield $foo() +// lines: 1, 2, 3, 5, 6 diff --git a/synapse/tests/files/stormcov/pivot.storm b/synapse/tests/files/stormcov/pivot.storm index ced65f58b6e..d264208afcb 100644 --- a/synapse/tests/files/stormcov/pivot.storm +++ b/synapse/tests/files/stormcov/pivot.storm @@ -1,2 +1,4 @@ [inet:fqdn=vertex.link] -> inet:fqdn +// coverage: 1, 2 +// lines: 1, 2 diff --git a/synapse/tests/files/stormcov/pragma-nocov.storm b/synapse/tests/files/stormcov/pragma-nocov.storm index ce8cd72153f..171930d09f5 100644 --- a/synapse/tests/files/stormcov/pragma-nocov.storm +++ b/synapse/tests/files/stormcov/pragma-nocov.storm @@ -16,3 +16,5 @@ function foo() { } // pragma: no cover stop } yield $foo() +// coverage: 1, 2, 3, 18 +// lines: 1, 2, 3, 12, 18 diff --git a/synapse/tests/files/stormcov/spin.storm b/synapse/tests/files/stormcov/spin.storm index fade24bdfb2..54c3f07e697 100644 --- a/synapse/tests/files/stormcov/spin.storm +++ b/synapse/tests/files/stormcov/spin.storm @@ -1,3 +1,5 @@ [inet:fqdn=vertex.link] spin +// coverage: 2, 3 +// lines: 2, 3 diff --git a/synapse/tests/files/stormcov/stormctrl.storm b/synapse/tests/files/stormcov/stormctrl.storm index e6570e51858..b0323a501ad 100644 --- a/synapse/tests/files/stormcov/stormctrl.storm +++ b/synapse/tests/files/stormcov/stormctrl.storm @@ -4,3 +4,5 @@ function foo() { } } yield $foo() +// coverage: 1, 2, 3, 6 +// lines: 1, 2, 3, 6 diff --git a/synapse/tests/test_lib_grammar.py b/synapse/tests/test_lib_grammar.py index 49714f4fb05..a7453e88081 100644 --- a/synapse/tests/test_lib_grammar.py +++ b/synapse/tests/test_lib_grammar.py @@ -51,6 +51,7 @@ 'try { inet:ip=asdf } catch FooBar as err { } catch * as err { }', 'test:array*[=1.2.3.4]', 'macro.set hehe ${ inet:ip }', + 'macro.set hehe ${\n inet:ip\ninet:fqdn }', '$q=${#foo.bar}', 'metrics.edits.byprop inet:fqdn:domain --newv $lib.null', 'tee // comment', @@ -840,7 +841,8 @@ 'Query: [TryCatch: [Query: [LiftPropBy: [Const: inet:ip, Const: =, Const: asdf]], CatchBlock: [Const: TypeError, Const: err, Query: []]]]', 'Query: [TryCatch: [Query: [LiftPropBy: [Const: inet:ip, Const: =, Const: asdf]], CatchBlock: [Const: FooBar, Const: err, Query: []], CatchBlock: [Const: *, Const: err, Query: []]]]', 'Query: [LiftByArray: [Const: test:array, Const: =, Const: 1.2.3.4]]', - 'Query: [CmdOper: [Const: macro.set, List: [Const: hehe, EmbedQuery: inet:ip ]]]', + 'Query: [CmdOper: [Const: macro.set, List: [Const: hehe, EmbedQuery: inet:ip]]]', + 'Query: [CmdOper: [Const: macro.set, List: [Const: hehe, EmbedQuery: inet:ip\ninet:fqdn]]]', 'Query: [SetVarOper: [Const: q, EmbedQuery: #foo.bar]]', 'Query: [CmdOper: [Const: metrics.edits.byprop, List: [Const: inet:fqdn:domain, Const: --newv, VarDeref: [VarValue: [Const: lib], Const: null]]]]', 'Query: [CmdOper: [Const: tee, Const: ()]]', @@ -1419,7 +1421,7 @@ 'Query: [SetVarOper: [Const: p, Const: names], LiftPropBy: [Const: entity:contact:name, Const: =, Const: foo], EditPropSet: [RelProp: [VarValue: [Const: p]], Const: ?-=, Const: bar]]', 'Query: [SetVarOper: [Const: pvar, Const: stuff], LiftProp: [Const: test:arrayprop], FiltOper: [Const: +, ArrayCond: [RelProp: [VarValue: [Const: pvar]], Const: =, Const: neato]]]', 'Query: [SetVarOper: [Const: pvar, Const: ints], LiftProp: [Const: test:arrayprop], FiltOper: [Const: +, ArrayCond: [RelProp: [VarValue: [Const: pvar]], Const: =, VarValue: [Const: othervar]]]]', - 'Query: [SetVarOper: [Const: foo, DollarExpr: [ExprDict: [Const: foo, EmbedQuery: inet:fqdn ]]]]', + 'Query: [SetVarOper: [Const: foo, DollarExpr: [ExprDict: [Const: foo, EmbedQuery: inet:fqdn]]]]', 'Query: [EditPropSet: [RelProp: [Const: seen], Const: ?=, DollarExpr: [ExprNode: [VarDeref: [VarValue: [Const: foo], Const: bar], Const: *, Const: 1000]]]]', 'Query: [EditPropSet: [RelProp: [Const: seen], Const: ?=, DollarExpr: [ExprNode: [RelPropValue: [RelProp: [Const: foo], VirtProps: [Const: virt]], Const: *, Const: 1000]]]]', 'Query: [EditNodeAdd: [FormName: [Const: test:str], Const: =, Const: foo], EditCondPropSet: [RelProp: [Const: hehe], CondSetOper: [Const: unset], Const: heval]]', diff --git a/synapse/tests/test_lib_stormlib_macro.py b/synapse/tests/test_lib_stormlib_macro.py index 123bd97aa29..5d6993f86c1 100644 --- a/synapse/tests/test_lib_stormlib_macro.py +++ b/synapse/tests/test_lib_stormlib_macro.py @@ -89,7 +89,7 @@ async def test_stormlib_macro(self): name = 'v' * 491 q = '$lib.macro.set($name, ${ help }) return ( $lib.macro.get($name) )' mdef = await core.callStorm(q, opts={'vars': {'name': name}}) - self.eq(mdef.get('storm'), ' help ') + self.eq(mdef.get('storm'), 'help') badname = 'v' * 492 with self.raises(s_exc.BadArg): @@ -381,7 +381,7 @@ async def test_stormlib_behold_macro(self): self.eq('storm:macro:add', addmesg['data']['event']) macro = addmesg['data']['info']['macro'] self.eq(macro['name'], 'foobar') - self.eq(macro['storm'], ' file:bytes | [+#neato] ') + self.eq(macro['storm'], 'file:bytes | [+#neato]') self.ne(visi.iden, macro['creator']) self.nn(macro['iden']) @@ -389,7 +389,7 @@ async def test_stormlib_behold_macro(self): self.eq('storm:macro:mod', setmesg['data']['event']) event = setmesg['data']['info'] self.nn(event['macro']) - self.eq(event['info']['storm'], ' inet:ip | [+#burrito] ') + self.eq(event['info']['storm'], 'inet:ip | [+#burrito]') self.nn(event['info']['updated']) modmesg = await sock.receive_json() diff --git a/synapse/tests/test_lib_stormtypes.py b/synapse/tests/test_lib_stormtypes.py index 1f5fabec0b9..e24d92c7b6a 100644 --- a/synapse/tests/test_lib_stormtypes.py +++ b/synapse/tests/test_lib_stormtypes.py @@ -687,7 +687,7 @@ async def test_storm_lib_base(self): self.stormIsInPrint("['1', 2, '3']", mesgs) mesgs = await core.stormlist('$lib.print(${ $foo=bar })') - self.stormIsInPrint('storm:query: " $foo=bar "', mesgs) + self.stormIsInPrint('storm:query: "$foo=bar"', mesgs) mesgs = await core.stormlist('$lib.print($lib.set(1,2,3))') self.stormIsInPrint("'1'", mesgs) @@ -1055,8 +1055,7 @@ async def test_storm_lib_query(self): msgs = await core.stormlist(q) fires = [m for m in msgs if m[0] == 'storm:fire'] self.len(1, fires) - self.eq(fires[0][1].get('data').get('q'), - " $lib.print('fire in the hole') ") + self.eq(fires[0][1].get('data').get('q'), "$lib.print('fire in the hole')") q = ''' $q=${ [test:int=1 test:int=2] } diff --git a/synapse/tests/test_utils_stormcov.py b/synapse/tests/test_utils_stormcov.py index 19690c266be..3bd6d70b113 100644 --- a/synapse/tests/test_utils_stormcov.py +++ b/synapse/tests/test_utils_stormcov.py @@ -2,6 +2,8 @@ import logging import argparse +import regex + import synapse.tests.files as s_files import synapse.tests.utils as s_utils @@ -30,7 +32,7 @@ async def test_stormcov_basics(self): stormcov = s_stormcov.StormcovPlugin(opts) stormcov.find_storm_files(stormdir) - self.eq( + self.sorteq( list(stormcov.guid_map.values()), [ s_files.getAssetPath('stormcov/spin.storm'), @@ -40,25 +42,42 @@ async def test_stormcov_basics(self): s_files.getAssetPath('stormcov/dupesubs.storm'), s_files.getAssetPath('stormcov/pivot.storm'), s_files.getAssetPath('stormcov/argvquery.storm'), + s_files.getAssetPath('stormcov/embedquery.storm'), ] ) + async def test_stormcov_coverage(self): + basedir = s_files.ASSETS + stormdir = os.path.join(basedir, 'stormcov') + opts = StormcovConfig(stormdirs=stormdir, stormcov_basedir=basedir) + + stormcov = s_stormcov.StormcovPlugin(opts) + stormcov.find_storm_files(stormdir) + async with self.getTestCore() as core: - async def check_cov(filename, expected, stormopts=None): + async def check_cov(filename): + storm = s_files.getAssetStr(filename) stormcov._start_sysmon() - await core.stormlist(s_files.getAssetStr(filename), opts=stormopts) + await core.stormlist(storm) stormcov._stop_sysmon() + coverage = regex.search(r'\/\/ coverage:.*?$', storm, flags=regex.M) + self.nn(coverage, msg='Stormcov sample files require a "// coverage: #, #, #" line') + + linenums = regex.findall(r'\d+', coverage.group()) + expected = set(map(int, linenums)) + self.eq(dict(stormcov.lines_hit), {s_files.getAssetPath(filename): expected}) stormcov.reset() - await check_cov('stormcov/dupesubs.storm', {1, 2, 4, 7, 8, 9, 10, 12, 13, 16, 17}) - await check_cov('stormcov/argvquery.storm', {1, 2, 3, 4, 6, 8}) - await check_cov('stormcov/stormctrl.storm', {1, 2, 3, 6}) - await check_cov('stormcov/pivot.storm', {1, 2}) - await check_cov('stormcov/pragma-nocov.storm', {1, 2, 3, 18}) - await check_cov('stormcov/spin.storm', {2, 3}) + await check_cov('stormcov/dupesubs.storm') + await check_cov('stormcov/embedquery.storm') + await check_cov('stormcov/argvquery.storm') + await check_cov('stormcov/stormctrl.storm') + await check_cov('stormcov/pivot.storm') + await check_cov('stormcov/pragma-nocov.storm') + await check_cov('stormcov/spin.storm') async def test_stormcov_lookup(self): basedir = s_files.ASSETS @@ -80,17 +99,25 @@ async def test_stormcov_lookup(self): async def test_stormcov_stormreporter(self): parser = s_stormcov.get_parser() - async def check_lines(filename, expected): + async def check_lines(filename): + storm = s_files.getAssetStr(filename) reporter = s_stormcov.StormReporter(s_files.getAssetPath(filename), parser) + + lines = regex.search(r'\/\/ lines:.*?$', storm, flags=regex.M) + self.nn(lines, msg='Stormcov sample files require a "// lines: #, #, #" line') + + linenums = regex.findall(r'\d+', lines.group()) + expected = set(map(int, linenums)) + self.eq(reporter.lines(), expected) - await check_lines('stormcov/pivot.storm', {1, 2}) - await check_lines('stormcov/stormctrl.storm', {1, 2, 3, 6}) - await check_lines('stormcov/pragma-nocov.storm', {1, 2, 3, 12, 18}) - await check_lines('stormcov/spin.storm', {2, 3}) - await check_lines('stormcov/argvquery.storm', {1, 2, 3, 4, 8}) - await check_lines('stormcov/lookup.storm', {1, 2, 3, 5, 6}) - await check_lines('stormcov/dupesubs.storm', {1, 2, 4, 5, 7, 8, 9, 10, 12, 13, 14, 17}) + await check_lines('stormcov/pivot.storm') + await check_lines('stormcov/stormctrl.storm') + await check_lines('stormcov/pragma-nocov.storm') + await check_lines('stormcov/spin.storm') + await check_lines('stormcov/argvquery.storm') + await check_lines('stormcov/lookup.storm') + await check_lines('stormcov/dupesubs.storm') async def test_stormcov_stormreporter_plugin(self): plugin = s_stormcov.StormReporterPlugin() From e4a38e41aa6a51b2882830f67bc83fe5aba62d64 Mon Sep 17 00:00:00 2001 From: blackout Date: Tue, 3 Mar 2026 14:34:24 -0500 Subject: [PATCH 10/20] wip --- pyproject.toml | 3 --- 1 file changed, 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 29412210618..94ca953d60b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -80,9 +80,6 @@ Documentation = 'https://synapse.docs.vertex.link' Repository = 'https://github.com/vertexproject/synapse' Changelog = 'https://synapse.docs.vertex.link/en/latest/synapse/changelog.html' -#[project.entry-points.pytest11] -#synapse = 'synapse.utils.stormcov' - [tool.setuptools] include-package-data = true From fb9ec245fc5484ecd338b6f9dea1bd1bf7a6293c Mon Sep 17 00:00:00 2001 From: blackout Date: Wed, 4 Mar 2026 09:09:56 -0500 Subject: [PATCH 11/20] stormcov coverage --- synapse/tests/test_utils_stormcov.py | 62 +++++++++++++++++++--------- synapse/utils/stormcov.py | 28 +++++++++---- 2 files changed, 62 insertions(+), 28 deletions(-) diff --git a/synapse/tests/test_utils_stormcov.py b/synapse/tests/test_utils_stormcov.py index 3bd6d70b113..7390a674637 100644 --- a/synapse/tests/test_utils_stormcov.py +++ b/synapse/tests/test_utils_stormcov.py @@ -1,8 +1,10 @@ import os import logging +import pathlib import argparse import regex +import coverage.exceptions import synapse.tests.files as s_files import synapse.tests.utils as s_utils @@ -15,40 +17,54 @@ class StormcovConfig: ''' Helper class to simulate pytest config ''' - def __init__(self, stormcov=False, stormdirs=[], stormexts='storm', stormcov_append=False, stormcov_basedir=s_stormcov.PACKAGE_DIR): + def __init__(self, stormcov=False, stormdirs='', stormexts='storm', stormcov_append=False, stormcov_basedir=s_stormcov.PACKAGE_DIR): self.option = argparse.Namespace( stormcov=stormcov, stormdirs=stormdirs, stormexts=stormexts, stormcov_append=stormcov_append, - stormcov_basedir=stormcov_basedir, + stormcov_basedir=pathlib.Path(stormcov_basedir) ) class TestUtilsStormcov(s_utils.SynTest): async def test_stormcov_basics(self): - basedir = s_files.ASSETS - stormdir = os.path.join(basedir, 'stormcov') + basedir = pathlib.Path(s_files.ASSETS) + stormdir = str(basedir / 'stormcov') opts = StormcovConfig(stormdirs=stormdir, stormcov_basedir=basedir) + stormfiles = [ + s_files.getAssetPath('stormcov/spin.storm'), + s_files.getAssetPath('stormcov/stormctrl.storm'), + s_files.getAssetPath('stormcov/lookup.storm'), + s_files.getAssetPath('stormcov/pragma-nocov.storm'), + s_files.getAssetPath('stormcov/dupesubs.storm'), + s_files.getAssetPath('stormcov/pivot.storm'), + s_files.getAssetPath('stormcov/argvquery.storm'), + s_files.getAssetPath('stormcov/embedquery.storm'), + ] + stormcov = s_stormcov.StormcovPlugin(opts) - stormcov.find_storm_files(stormdir) - self.sorteq( - list(stormcov.guid_map.values()), - [ - s_files.getAssetPath('stormcov/spin.storm'), - s_files.getAssetPath('stormcov/stormctrl.storm'), - s_files.getAssetPath('stormcov/lookup.storm'), - s_files.getAssetPath('stormcov/pragma-nocov.storm'), - s_files.getAssetPath('stormcov/dupesubs.storm'), - s_files.getAssetPath('stormcov/pivot.storm'), - s_files.getAssetPath('stormcov/argvquery.storm'), - s_files.getAssetPath('stormcov/embedquery.storm'), - ] - ) + with self.getLoggerStream('synapse.utils.stormcov') as stream: + stormcov.find_storm_files(stormdir) + + badstorm = s_files.getAssetPath('stormcov/badstorm.storm') + stream.expect(f'Skipping invalid storm file: {badstorm}') + + self.sorteq(list(stormcov.guid_map.values()), stormfiles) + stormcov.reset() + + stormcov.discover_stormdirs('') + self.sorteq(list(stormcov.guid_map.values()), stormfiles) + stormcov.reset() + + stormcov.stormdirs = [] + stormcov.discover_stormdirs([pathlib.Path(stormdir)]) + self.sorteq(list(stormcov.guid_map.values()), stormfiles) + stormcov.reset() async def test_stormcov_coverage(self): - basedir = s_files.ASSETS - stormdir = os.path.join(basedir, 'stormcov') + basedir = pathlib.Path(s_files.ASSETS) + stormdir = str(basedir / 'stormcov') opts = StormcovConfig(stormdirs=stormdir, stormcov_basedir=basedir) stormcov = s_stormcov.StormcovPlugin(opts) @@ -119,6 +135,12 @@ async def check_lines(filename): await check_lines('stormcov/lookup.storm') await check_lines('stormcov/dupesubs.storm') + # Non-existent file + reporter = s_stormcov.StormReporter('newp', parser) + with self.raises(coverage.exceptions.NoSource) as exc: + reporter.source() + self.eq(str(exc.exception), "Couldn't read newp: [Errno 2] No such file or directory: 'newp'") + async def test_stormcov_stormreporter_plugin(self): plugin = s_stormcov.StormReporterPlugin() reporter = plugin.file_reporter(s_files.getAssetPath('stormcov/pivot.storm')) diff --git a/synapse/utils/stormcov.py b/synapse/utils/stormcov.py index b21ffdb2efb..52447b265ea 100644 --- a/synapse/utils/stormcov.py +++ b/synapse/utils/stormcov.py @@ -20,6 +20,7 @@ PACKAGE_DIR = pathlib.Path('packages').absolute() def pytest_addoption(parser): # pragma: no cover + # NB: no coverage since this is a pytest hook """Add options to control storm coverage.""" group = parser.getgroup('stormcov', 'storm coverage reporting') @@ -65,6 +66,7 @@ def pytest_addoption(parser): # pragma: no cover DISABLE = sys.monitoring.DISABLE def pytest_configure(config): # pragma: no cover + # NB: no coverage since this is a pytest hook if not config.option.stormcov and not (config.option.stormdirs or config.option.stormcov_append): return @@ -99,9 +101,9 @@ def __init__(self, config): self.text_map = {} self.guid_map = {} self.subq_map = {} + self.lines_hit = collections.defaultdict(set) self.freg = regex.compile(r'.*synapse/lib/(ast.py|view.py|stormctrl.py)$') - self.reset() self.parser = get_parser() @@ -167,7 +169,7 @@ def _start_sysmon(self): self._prevcb = sys.monitoring.register_callback(self.toolid, sys.monitoring.events.PY_START, self.sysmon_py_start) def _stop_sysmon(self): - if self._prevcb: + if self._prevcb: # pragma: no cover sys.monitoring.register_callback(self.toolid, sys.monitoring.events.PY_START, self._prevcb) return @@ -177,6 +179,7 @@ def _stop_sysmon(self): @pytest.hookimpl(wrapper=True) def pytest_runtestloop(self, session): # pragma: no cover + # NB: no coverage since this is a pytest hook self.cov = coverage.Coverage.current() @@ -226,6 +229,7 @@ def discover_stormdirs(self, testpaths: list[pathlib.Path]): @pytest.hookimpl(wrapper=True) def pytest_collection_modifyitems(self, config, items): # pragma: no cover + # NB: no coverage since this is a pytest hook # Note: If using xdist, this function executes on each worker node testpaths = [item.path for item in items] self.discover_stormdirs(testpaths) @@ -233,23 +237,27 @@ def pytest_collection_modifyitems(self, config, items): # pragma: no cover @pytest.hookimpl(wrapper=True) def pytest_xdist_node_collection_finished(self, node, ids): # pragma: no cover + # NB: no coverage since this is a pytest hook # Note: This hook allows the xdist controller to get a list of test ids so we can build a list of storm dirs to present stormterm coverage testpaths = [pathlib.Path(testid.split('::')[0]).absolute() for testid in ids] self.discover_stormdirs(testpaths) yield def pytest_terminal_summary(self, terminalreporter, exitstatus, config): # pragma: no cover + # NB: no coverage since this is a pytest hook if self.isworker: return self.cov.report(skip_covered=False, skip_empty=False) - def sysmon_py_start(self, code, instruction_offset): + def sysmon_py_start(self, code, instruction_offset): # pragma: no cover + # NB: no coverage since this runs inside of the sys.monitoring callback if (fname := self.freg.match(code.co_filename)): return self.handlers[fname.group(1)](code) return DISABLE - def handle_ast(self, code, frame=None): + def handle_ast(self, code, frame=None): # pragma: no cover + # NB: no coverage since this runs inside of the sys.monitoring callback if frame is None: if code.co_name == 'pullgenr': frame = inspect.currentframe().f_back.f_back @@ -312,7 +320,8 @@ def handle_ast(self, code, frame=None): self.text_map[node.text] = (filename, offs) self.mark_lines(frame, (filename, offs)) - def mark_lines(self, frame, info): + def mark_lines(self, frame, info): # pragma: no cover + # NB: no coverage since this runs inside of the sys.monitoring callback astn = frame.f_locals.get('self') fname, offs = info strt = astn.astinfo.sline @@ -324,7 +333,8 @@ def mark_lines(self, frame, info): self.lines_hit[fname].update(range(strt + offs, fini + offs + 1)) PIVOT_METHODS = {'nodesByPropValu', 'nodesByPropArray', 'nodesByTag', 'getNodeByNdef'} - def handle_view(self, code): + def handle_view(self, code): # pragma: no cover + # NB: no coverage since this runs inside of the sys.monitoring callback if code.co_name not in self.PIVOT_METHODS: return DISABLE @@ -333,7 +343,8 @@ def handle_view(self, code): return return self.handle_ast(code, frame=frame) - def handle_stormctrl(self, code): + def handle_stormctrl(self, code): # pragma: no cover + # NB: no coverage since this runs inside of the sys.monitoring callback if code.co_name != '__init__': return DISABLE return self.handle_ast(code, frame=inspect.currentframe().f_back.f_back.f_back) @@ -432,6 +443,7 @@ def __init__(self): def file_reporter(self, filename): return StormReporter(filename, self.parser) -def coverage_init(reg, options): +def coverage_init(reg, options): # pragma: no cover + # NB: no coverage since this is the coverage plugin entrypoint plugin = StormReporterPlugin() reg.add_file_tracer(plugin) From 89167e97ec105fd11132e018c86ccad798c85c18 Mon Sep 17 00:00:00 2001 From: blackout Date: Wed, 4 Mar 2026 09:21:56 -0500 Subject: [PATCH 12/20] wip --- synapse/tests/files/stormcov/badstorm.storm | 1 + 1 file changed, 1 insertion(+) create mode 100644 synapse/tests/files/stormcov/badstorm.storm diff --git a/synapse/tests/files/stormcov/badstorm.storm b/synapse/tests/files/stormcov/badstorm.storm new file mode 100644 index 00000000000..93d51406d63 --- /dev/null +++ b/synapse/tests/files/stormcov/badstorm.storm @@ -0,0 +1 @@ +[{}] From 029065f3ab982339332f681ffbff5c49ad603f96 Mon Sep 17 00:00:00 2001 From: blackout Date: Wed, 4 Mar 2026 09:47:45 -0500 Subject: [PATCH 13/20] better xdist handling --- synapse/utils/stormcov.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/synapse/utils/stormcov.py b/synapse/utils/stormcov.py index 52447b265ea..563a0903cae 100644 --- a/synapse/utils/stormcov.py +++ b/synapse/utils/stormcov.py @@ -90,6 +90,14 @@ def __init__(self, config): self.isworker = os.environ.get("PYTEST_XDIST_WORKER") is not None + # xdist detection logic from here: + # https://github.com/pytest-dev/pytest-cov/blob/be3366838e41a9cbefa714636a39c5c2f6d5f588/src/pytest_cov/plugin.py#L235C19-L235C139 + self.iscontroller = ( + getattr(config.option, 'numprocesses', False) or + getattr(config.option, 'distload', False) or + getattr(config.option, 'dist', 'no') != 'no' + ) and not self.isworker + self.config = config self.handlers = { 'ast.py': self.handle_ast, @@ -248,6 +256,9 @@ def pytest_terminal_summary(self, terminalreporter, exitstatus, config): # pragm if self.isworker: return + if not self.iscontroller and not self.lines_hit: + return + self.cov.report(skip_covered=False, skip_empty=False) def sysmon_py_start(self, code, instruction_offset): # pragma: no cover From f830e8eca376f0387cbffe7ea09e6179cd8a0ad6 Mon Sep 17 00:00:00 2001 From: blackout Date: Wed, 4 Mar 2026 11:03:36 -0500 Subject: [PATCH 14/20] wip --- synapse/lib/parser.py | 4 +--- synapse/tests/files/stormcov/dupesubs.storm | 12 ++++++++++-- synapse/tests/files/stormcov/embedquery.storm | 10 +++++++--- synapse/tests/test_lib_grammar.py | 6 ++---- synapse/tests/test_lib_stormlib_macro.py | 6 +++--- synapse/tests/test_lib_stormtypes.py | 5 +++-- synapse/tests/test_utils_stormcov.py | 6 +++++- synapse/utils/stormcov.py | 8 ++++++-- 8 files changed, 37 insertions(+), 20 deletions(-) diff --git a/synapse/lib/parser.py b/synapse/lib/parser.py index 023ad1c5f35..eede9b54349 100644 --- a/synapse/lib/parser.py +++ b/synapse/lib/parser.py @@ -261,7 +261,7 @@ def embedquery(self, meta, kids): kids[0].astinfo = astinfo - return s_ast.EmbedQuery(astinfo, kids[0].text, kids=kids) + return s_ast.EmbedQuery(astinfo, kids[0].getAstText(), kids=kids) @lark.v_args(meta=True) def funccall(self, meta, kids): @@ -271,8 +271,6 @@ def funccall(self, meta, kids): argkids = [] kwargkids = [] kwnames = set() - indx = 1 - kcnt = len(kids) todo = collections.deque(kids) diff --git a/synapse/tests/files/stormcov/dupesubs.storm b/synapse/tests/files/stormcov/dupesubs.storm index 890443800a2..872374a6963 100644 --- a/synapse/tests/files/stormcov/dupesubs.storm +++ b/synapse/tests/files/stormcov/dupesubs.storm @@ -16,5 +16,13 @@ tee { } { [inet:fqdn=argv.com] } -// coverage: 1, 2, 4, 7, 8, 9, 10, 12, 13, 16, 17 -// lines: 1, 2, 4, 5, 7, 8, 9, 10, 12, 13, 14, 17 +| +tee +{ + [ inet:fqdn=dup00.com ] +} +{ + [ inet:fqdn=dup00.com ] +} +// coverage: 1, 2, 4, 7, 8, 9, 10, 12, 13, 16, 17, 20, 21, 22, 24 +// lines: 1, 2, 4, 5, 7, 8, 9, 10, 12, 13, 14, 17, 20, 22, 25 diff --git a/synapse/tests/files/stormcov/embedquery.storm b/synapse/tests/files/stormcov/embedquery.storm index 6bf32a43e7c..d3a515267d8 100644 --- a/synapse/tests/files/stormcov/embedquery.storm +++ b/synapse/tests/files/stormcov/embedquery.storm @@ -11,7 +11,11 @@ $eq3 = ${ [ inet:fqdn=vertex.link ] } } +$eq4 = ${ [ inet:fqdn=vtx.lk ] } +$eq5 = ${ -tee $eq1 $eq2 $eq3 -// coverage: 1, 2, 4, 5, 6, 9, 10, 15 -// lines: 1, 2, 4, 5, 6, 9, 10, 11, 15 + [ inet:fqdn=v.vtx.lk ] +} +tee $eq1 $eq2 $eq3 $eq4 $eq5 +// coverage: 1, 2, 4, 5, 6, 9, 10, 14, 15, 17, 19 +// lines: 1, 2, 4, 5, 6, 9, 10, 11, 14, 15, 17, 19 diff --git a/synapse/tests/test_lib_grammar.py b/synapse/tests/test_lib_grammar.py index a7453e88081..49714f4fb05 100644 --- a/synapse/tests/test_lib_grammar.py +++ b/synapse/tests/test_lib_grammar.py @@ -51,7 +51,6 @@ 'try { inet:ip=asdf } catch FooBar as err { } catch * as err { }', 'test:array*[=1.2.3.4]', 'macro.set hehe ${ inet:ip }', - 'macro.set hehe ${\n inet:ip\ninet:fqdn }', '$q=${#foo.bar}', 'metrics.edits.byprop inet:fqdn:domain --newv $lib.null', 'tee // comment', @@ -841,8 +840,7 @@ 'Query: [TryCatch: [Query: [LiftPropBy: [Const: inet:ip, Const: =, Const: asdf]], CatchBlock: [Const: TypeError, Const: err, Query: []]]]', 'Query: [TryCatch: [Query: [LiftPropBy: [Const: inet:ip, Const: =, Const: asdf]], CatchBlock: [Const: FooBar, Const: err, Query: []], CatchBlock: [Const: *, Const: err, Query: []]]]', 'Query: [LiftByArray: [Const: test:array, Const: =, Const: 1.2.3.4]]', - 'Query: [CmdOper: [Const: macro.set, List: [Const: hehe, EmbedQuery: inet:ip]]]', - 'Query: [CmdOper: [Const: macro.set, List: [Const: hehe, EmbedQuery: inet:ip\ninet:fqdn]]]', + 'Query: [CmdOper: [Const: macro.set, List: [Const: hehe, EmbedQuery: inet:ip ]]]', 'Query: [SetVarOper: [Const: q, EmbedQuery: #foo.bar]]', 'Query: [CmdOper: [Const: metrics.edits.byprop, List: [Const: inet:fqdn:domain, Const: --newv, VarDeref: [VarValue: [Const: lib], Const: null]]]]', 'Query: [CmdOper: [Const: tee, Const: ()]]', @@ -1421,7 +1419,7 @@ 'Query: [SetVarOper: [Const: p, Const: names], LiftPropBy: [Const: entity:contact:name, Const: =, Const: foo], EditPropSet: [RelProp: [VarValue: [Const: p]], Const: ?-=, Const: bar]]', 'Query: [SetVarOper: [Const: pvar, Const: stuff], LiftProp: [Const: test:arrayprop], FiltOper: [Const: +, ArrayCond: [RelProp: [VarValue: [Const: pvar]], Const: =, Const: neato]]]', 'Query: [SetVarOper: [Const: pvar, Const: ints], LiftProp: [Const: test:arrayprop], FiltOper: [Const: +, ArrayCond: [RelProp: [VarValue: [Const: pvar]], Const: =, VarValue: [Const: othervar]]]]', - 'Query: [SetVarOper: [Const: foo, DollarExpr: [ExprDict: [Const: foo, EmbedQuery: inet:fqdn]]]]', + 'Query: [SetVarOper: [Const: foo, DollarExpr: [ExprDict: [Const: foo, EmbedQuery: inet:fqdn ]]]]', 'Query: [EditPropSet: [RelProp: [Const: seen], Const: ?=, DollarExpr: [ExprNode: [VarDeref: [VarValue: [Const: foo], Const: bar], Const: *, Const: 1000]]]]', 'Query: [EditPropSet: [RelProp: [Const: seen], Const: ?=, DollarExpr: [ExprNode: [RelPropValue: [RelProp: [Const: foo], VirtProps: [Const: virt]], Const: *, Const: 1000]]]]', 'Query: [EditNodeAdd: [FormName: [Const: test:str], Const: =, Const: foo], EditCondPropSet: [RelProp: [Const: hehe], CondSetOper: [Const: unset], Const: heval]]', diff --git a/synapse/tests/test_lib_stormlib_macro.py b/synapse/tests/test_lib_stormlib_macro.py index 5d6993f86c1..123bd97aa29 100644 --- a/synapse/tests/test_lib_stormlib_macro.py +++ b/synapse/tests/test_lib_stormlib_macro.py @@ -89,7 +89,7 @@ async def test_stormlib_macro(self): name = 'v' * 491 q = '$lib.macro.set($name, ${ help }) return ( $lib.macro.get($name) )' mdef = await core.callStorm(q, opts={'vars': {'name': name}}) - self.eq(mdef.get('storm'), 'help') + self.eq(mdef.get('storm'), ' help ') badname = 'v' * 492 with self.raises(s_exc.BadArg): @@ -381,7 +381,7 @@ async def test_stormlib_behold_macro(self): self.eq('storm:macro:add', addmesg['data']['event']) macro = addmesg['data']['info']['macro'] self.eq(macro['name'], 'foobar') - self.eq(macro['storm'], 'file:bytes | [+#neato]') + self.eq(macro['storm'], ' file:bytes | [+#neato] ') self.ne(visi.iden, macro['creator']) self.nn(macro['iden']) @@ -389,7 +389,7 @@ async def test_stormlib_behold_macro(self): self.eq('storm:macro:mod', setmesg['data']['event']) event = setmesg['data']['info'] self.nn(event['macro']) - self.eq(event['info']['storm'], 'inet:ip | [+#burrito]') + self.eq(event['info']['storm'], ' inet:ip | [+#burrito] ') self.nn(event['info']['updated']) modmesg = await sock.receive_json() diff --git a/synapse/tests/test_lib_stormtypes.py b/synapse/tests/test_lib_stormtypes.py index e24d92c7b6a..1f5fabec0b9 100644 --- a/synapse/tests/test_lib_stormtypes.py +++ b/synapse/tests/test_lib_stormtypes.py @@ -687,7 +687,7 @@ async def test_storm_lib_base(self): self.stormIsInPrint("['1', 2, '3']", mesgs) mesgs = await core.stormlist('$lib.print(${ $foo=bar })') - self.stormIsInPrint('storm:query: "$foo=bar"', mesgs) + self.stormIsInPrint('storm:query: " $foo=bar "', mesgs) mesgs = await core.stormlist('$lib.print($lib.set(1,2,3))') self.stormIsInPrint("'1'", mesgs) @@ -1055,7 +1055,8 @@ async def test_storm_lib_query(self): msgs = await core.stormlist(q) fires = [m for m in msgs if m[0] == 'storm:fire'] self.len(1, fires) - self.eq(fires[0][1].get('data').get('q'), "$lib.print('fire in the hole')") + self.eq(fires[0][1].get('data').get('q'), + " $lib.print('fire in the hole') ") q = ''' $q=${ [test:int=1 test:int=2] } diff --git a/synapse/tests/test_utils_stormcov.py b/synapse/tests/test_utils_stormcov.py index 7390a674637..4d622358dab 100644 --- a/synapse/tests/test_utils_stormcov.py +++ b/synapse/tests/test_utils_stormcov.py @@ -50,6 +50,10 @@ async def test_stormcov_basics(self): badstorm = s_files.getAssetPath('stormcov/badstorm.storm') stream.expect(f'Skipping invalid storm file: {badstorm}') + dupesubs = s_files.getAssetPath('stormcov/dupesubs.storm') + stream.expect(f'Duplicate argvquery in {dupesubs} at line 25, coverage will be reported on first instance in {dupesubs} at line 22') + stream.expect(f'Duplicate embedquery in {dupesubs} at line 5, coverage will be reported on first instance in {dupesubs} at line 2') + self.sorteq(list(stormcov.guid_map.values()), stormfiles) stormcov.reset() @@ -87,8 +91,8 @@ async def check_cov(filename): stormcov.reset() - await check_cov('stormcov/dupesubs.storm') await check_cov('stormcov/embedquery.storm') + await check_cov('stormcov/dupesubs.storm') await check_cov('stormcov/argvquery.storm') await check_cov('stormcov/stormctrl.storm') await check_cov('stormcov/pivot.storm') diff --git a/synapse/utils/stormcov.py b/synapse/utils/stormcov.py index 563a0903cae..6d35b4a43bf 100644 --- a/synapse/utils/stormcov.py +++ b/synapse/utils/stormcov.py @@ -159,11 +159,15 @@ def find_subqueries(self, tree, path): subg = s_common.guid(str(subq)) line = node.meta.line + offs = 0 + if rule == 'embedquery': + line -= 1 + offs += 1 if subg in self.subq_map: (pname, pline) = self.subq_map[subg] - logger.warning(f'Duplicate {rule} in {path} at line {line + 1}, coverage will ' - f'be reported on first instance in {pname} at line {pline + 1}') + logger.warning(f'Duplicate {rule} in {path} at line {line + 1 + offs}, coverage will ' + f'be reported on first instance in {pname} at line {pline + 1 + offs}') continue self.subq_map[subg] = (path, line) From 74a84252c2be7dc87f64a989ba6bd11b5a6406eb Mon Sep 17 00:00:00 2001 From: blackout Date: Wed, 4 Mar 2026 11:07:37 -0500 Subject: [PATCH 15/20] wip --- synapse/utils/stormcov.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/synapse/utils/stormcov.py b/synapse/utils/stormcov.py index 6d35b4a43bf..8b9f3138d48 100644 --- a/synapse/utils/stormcov.py +++ b/synapse/utils/stormcov.py @@ -158,16 +158,16 @@ def find_subqueries(self, tree, path): continue subg = s_common.guid(str(subq)) + offs = 1 line = node.meta.line - offs = 0 if rule == 'embedquery': - line -= 1 offs += 1 + line -= 1 if subg in self.subq_map: (pname, pline) = self.subq_map[subg] - logger.warning(f'Duplicate {rule} in {path} at line {line + 1 + offs}, coverage will ' - f'be reported on first instance in {pname} at line {pline + 1 + offs}') + logger.warning(f'Duplicate {rule} in {path} at line {line + offs}, coverage will ' + f'be reported on first instance in {pname} at line {pline + offs}') continue self.subq_map[subg] = (path, line) From ba1679d5d267c0c33de19313ee360413f09a8dc8 Mon Sep 17 00:00:00 2001 From: blackout Date: Wed, 4 Mar 2026 14:08:08 -0500 Subject: [PATCH 16/20] wip --- synapse/tests/test_utils_stormcov.py | 37 +++++++++++++++++----------- synapse/utils/stormcov.py | 8 +++--- 2 files changed, 26 insertions(+), 19 deletions(-) diff --git a/synapse/tests/test_utils_stormcov.py b/synapse/tests/test_utils_stormcov.py index 4d622358dab..1657e3b3bdc 100644 --- a/synapse/tests/test_utils_stormcov.py +++ b/synapse/tests/test_utils_stormcov.py @@ -33,14 +33,16 @@ async def test_stormcov_basics(self): opts = StormcovConfig(stormdirs=stormdir, stormcov_basedir=basedir) stormfiles = [ - s_files.getAssetPath('stormcov/spin.storm'), - s_files.getAssetPath('stormcov/stormctrl.storm'), - s_files.getAssetPath('stormcov/lookup.storm'), - s_files.getAssetPath('stormcov/pragma-nocov.storm'), - s_files.getAssetPath('stormcov/dupesubs.storm'), - s_files.getAssetPath('stormcov/pivot.storm'), s_files.getAssetPath('stormcov/argvquery.storm'), + s_files.getAssetPath('stormcov/argvquery2.storm'), + s_files.getAssetPath('stormcov/dupesubs.storm'), s_files.getAssetPath('stormcov/embedquery.storm'), + s_files.getAssetPath('stormcov/embedquery2.storm'), + s_files.getAssetPath('stormcov/lookup.storm'), + s_files.getAssetPath('stormcov/pivot.storm'), + s_files.getAssetPath('stormcov/pragma-nocov.storm'), + s_files.getAssetPath('stormcov/spin.storm'), + s_files.getAssetPath('stormcov/stormctrl.storm'), ] stormcov = s_stormcov.StormcovPlugin(opts) @@ -51,8 +53,8 @@ async def test_stormcov_basics(self): stream.expect(f'Skipping invalid storm file: {badstorm}') dupesubs = s_files.getAssetPath('stormcov/dupesubs.storm') - stream.expect(f'Duplicate argvquery in {dupesubs} at line 25, coverage will be reported on first instance in {dupesubs} at line 22') - stream.expect(f'Duplicate embedquery in {dupesubs} at line 5, coverage will be reported on first instance in {dupesubs} at line 2') + stream.expect(f'Duplicate argvquery in {dupesubs} at line 24, coverage will be reported on first instance in {dupesubs} at line 21') + stream.expect(f'Duplicate embedquery in {dupesubs} at line 4, coverage will be reported on first instance in {dupesubs} at line 1') self.sorteq(list(stormcov.guid_map.values()), stormfiles) stormcov.reset() @@ -91,13 +93,15 @@ async def check_cov(filename): stormcov.reset() - await check_cov('stormcov/embedquery.storm') - await check_cov('stormcov/dupesubs.storm') await check_cov('stormcov/argvquery.storm') - await check_cov('stormcov/stormctrl.storm') + await check_cov('stormcov/argvquery2.storm') + await check_cov('stormcov/dupesubs.storm') + await check_cov('stormcov/embedquery.storm') + await check_cov('stormcov/embedquery2.storm'), await check_cov('stormcov/pivot.storm') await check_cov('stormcov/pragma-nocov.storm') await check_cov('stormcov/spin.storm') + await check_cov('stormcov/stormctrl.storm') async def test_stormcov_lookup(self): basedir = s_files.ASSETS @@ -131,13 +135,16 @@ async def check_lines(filename): self.eq(reporter.lines(), expected) + await check_lines('stormcov/argvquery.storm') + await check_lines('stormcov/argvquery2.storm') + await check_lines('stormcov/dupesubs.storm') + await check_lines('stormcov/embedquery.storm'), + await check_lines('stormcov/embedquery2.storm'), + await check_lines('stormcov/lookup.storm') await check_lines('stormcov/pivot.storm') - await check_lines('stormcov/stormctrl.storm') await check_lines('stormcov/pragma-nocov.storm') await check_lines('stormcov/spin.storm') - await check_lines('stormcov/argvquery.storm') - await check_lines('stormcov/lookup.storm') - await check_lines('stormcov/dupesubs.storm') + await check_lines('stormcov/stormctrl.storm') # Non-existent file reporter = s_stormcov.StormReporter('newp', parser) diff --git a/synapse/utils/stormcov.py b/synapse/utils/stormcov.py index 8b9f3138d48..e1472d04369 100644 --- a/synapse/utils/stormcov.py +++ b/synapse/utils/stormcov.py @@ -159,10 +159,10 @@ def find_subqueries(self, tree, path): subg = s_common.guid(str(subq)) offs = 1 - line = node.meta.line - if rule == 'embedquery': - offs += 1 - line -= 1 + line = node.meta.line - 1 + if rule == 'argvquery': + offs = 0 + line = subq.meta.line - 1 if subg in self.subq_map: (pname, pline) = self.subq_map[subg] From 295a5d6cdd887125ad9df3c8914077b83d5154c9 Mon Sep 17 00:00:00 2001 From: blackout Date: Wed, 4 Mar 2026 14:14:32 -0500 Subject: [PATCH 17/20] wip --- synapse/tests/files/stormcov/argvquery2.storm | 4 ++++ synapse/tests/files/stormcov/argvquery3.storm | 6 ++++++ synapse/tests/files/stormcov/embedquery2.storm | 4 ++++ synapse/tests/files/stormcov/embedquery3.storm | 6 ++++++ synapse/tests/test_utils_stormcov.py | 6 ++++++ 5 files changed, 26 insertions(+) create mode 100644 synapse/tests/files/stormcov/argvquery2.storm create mode 100644 synapse/tests/files/stormcov/argvquery3.storm create mode 100644 synapse/tests/files/stormcov/embedquery2.storm create mode 100644 synapse/tests/files/stormcov/embedquery3.storm diff --git a/synapse/tests/files/stormcov/argvquery2.storm b/synapse/tests/files/stormcov/argvquery2.storm new file mode 100644 index 00000000000..4d18aecf835 --- /dev/null +++ b/synapse/tests/files/stormcov/argvquery2.storm @@ -0,0 +1,4 @@ + +tee { inet:fqdn=argvquery2.storm } +// coverage: 2 +// lines: 2 diff --git a/synapse/tests/files/stormcov/argvquery3.storm b/synapse/tests/files/stormcov/argvquery3.storm new file mode 100644 index 00000000000..ce5893acd4a --- /dev/null +++ b/synapse/tests/files/stormcov/argvquery3.storm @@ -0,0 +1,6 @@ + +tee { + inet:fqdn=argvquery3.storm +} +// coverage: 2, 3 +// lines: 2, 3 diff --git a/synapse/tests/files/stormcov/embedquery2.storm b/synapse/tests/files/stormcov/embedquery2.storm new file mode 100644 index 00000000000..e838cec22ee --- /dev/null +++ b/synapse/tests/files/stormcov/embedquery2.storm @@ -0,0 +1,4 @@ + +tee ${ inet:fqdn=embedquery2.storm } +// coverage: 2 +// lines: 2 diff --git a/synapse/tests/files/stormcov/embedquery3.storm b/synapse/tests/files/stormcov/embedquery3.storm new file mode 100644 index 00000000000..31cf550fed3 --- /dev/null +++ b/synapse/tests/files/stormcov/embedquery3.storm @@ -0,0 +1,6 @@ + +tee ${ + inet:fqdn=embedquery3.storm +} +// coverage: 2, 3 +// lines: 2, 3 diff --git a/synapse/tests/test_utils_stormcov.py b/synapse/tests/test_utils_stormcov.py index 1657e3b3bdc..40806e2a0e2 100644 --- a/synapse/tests/test_utils_stormcov.py +++ b/synapse/tests/test_utils_stormcov.py @@ -35,9 +35,11 @@ async def test_stormcov_basics(self): stormfiles = [ s_files.getAssetPath('stormcov/argvquery.storm'), s_files.getAssetPath('stormcov/argvquery2.storm'), + s_files.getAssetPath('stormcov/argvquery3.storm'), s_files.getAssetPath('stormcov/dupesubs.storm'), s_files.getAssetPath('stormcov/embedquery.storm'), s_files.getAssetPath('stormcov/embedquery2.storm'), + s_files.getAssetPath('stormcov/embedquery3.storm'), s_files.getAssetPath('stormcov/lookup.storm'), s_files.getAssetPath('stormcov/pivot.storm'), s_files.getAssetPath('stormcov/pragma-nocov.storm'), @@ -95,9 +97,11 @@ async def check_cov(filename): await check_cov('stormcov/argvquery.storm') await check_cov('stormcov/argvquery2.storm') + await check_cov('stormcov/argvquery3.storm') await check_cov('stormcov/dupesubs.storm') await check_cov('stormcov/embedquery.storm') await check_cov('stormcov/embedquery2.storm'), + await check_cov('stormcov/embedquery3.storm'), await check_cov('stormcov/pivot.storm') await check_cov('stormcov/pragma-nocov.storm') await check_cov('stormcov/spin.storm') @@ -137,9 +141,11 @@ async def check_lines(filename): await check_lines('stormcov/argvquery.storm') await check_lines('stormcov/argvquery2.storm') + await check_lines('stormcov/argvquery3.storm') await check_lines('stormcov/dupesubs.storm') await check_lines('stormcov/embedquery.storm'), await check_lines('stormcov/embedquery2.storm'), + await check_lines('stormcov/embedquery3.storm'), await check_lines('stormcov/lookup.storm') await check_lines('stormcov/pivot.storm') await check_lines('stormcov/pragma-nocov.storm') From d2902acaf03b452505027b21e13c0d47b4d27665 Mon Sep 17 00:00:00 2001 From: blackout Date: Wed, 4 Mar 2026 14:18:07 -0500 Subject: [PATCH 18/20] wip --- synapse/tests/files/stormcov/argvquery4.storm | 7 +++++++ synapse/tests/files/stormcov/embedquery4.storm | 7 +++++++ synapse/tests/test_utils_stormcov.py | 6 ++++++ 3 files changed, 20 insertions(+) create mode 100644 synapse/tests/files/stormcov/argvquery4.storm create mode 100644 synapse/tests/files/stormcov/embedquery4.storm diff --git a/synapse/tests/files/stormcov/argvquery4.storm b/synapse/tests/files/stormcov/argvquery4.storm new file mode 100644 index 00000000000..650a2a93a1c --- /dev/null +++ b/synapse/tests/files/stormcov/argvquery4.storm @@ -0,0 +1,7 @@ + +tee +{ + inet:fqdn=argvquery4.storm +} +// coverage: 2, 3, 4 +// lines: 2, 4 diff --git a/synapse/tests/files/stormcov/embedquery4.storm b/synapse/tests/files/stormcov/embedquery4.storm new file mode 100644 index 00000000000..1bb1402d543 --- /dev/null +++ b/synapse/tests/files/stormcov/embedquery4.storm @@ -0,0 +1,7 @@ + +tee +${ + inet:fqdn=embedquery4.storm +} +// coverage: 2, 3, 4 +// lines: 2, 4 diff --git a/synapse/tests/test_utils_stormcov.py b/synapse/tests/test_utils_stormcov.py index 40806e2a0e2..7f2f9c468c4 100644 --- a/synapse/tests/test_utils_stormcov.py +++ b/synapse/tests/test_utils_stormcov.py @@ -36,10 +36,12 @@ async def test_stormcov_basics(self): s_files.getAssetPath('stormcov/argvquery.storm'), s_files.getAssetPath('stormcov/argvquery2.storm'), s_files.getAssetPath('stormcov/argvquery3.storm'), + s_files.getAssetPath('stormcov/argvquery4.storm'), s_files.getAssetPath('stormcov/dupesubs.storm'), s_files.getAssetPath('stormcov/embedquery.storm'), s_files.getAssetPath('stormcov/embedquery2.storm'), s_files.getAssetPath('stormcov/embedquery3.storm'), + s_files.getAssetPath('stormcov/embedquery4.storm'), s_files.getAssetPath('stormcov/lookup.storm'), s_files.getAssetPath('stormcov/pivot.storm'), s_files.getAssetPath('stormcov/pragma-nocov.storm'), @@ -98,10 +100,12 @@ async def check_cov(filename): await check_cov('stormcov/argvquery.storm') await check_cov('stormcov/argvquery2.storm') await check_cov('stormcov/argvquery3.storm') + await check_cov('stormcov/argvquery4.storm') await check_cov('stormcov/dupesubs.storm') await check_cov('stormcov/embedquery.storm') await check_cov('stormcov/embedquery2.storm'), await check_cov('stormcov/embedquery3.storm'), + await check_cov('stormcov/embedquery4.storm'), await check_cov('stormcov/pivot.storm') await check_cov('stormcov/pragma-nocov.storm') await check_cov('stormcov/spin.storm') @@ -142,10 +146,12 @@ async def check_lines(filename): await check_lines('stormcov/argvquery.storm') await check_lines('stormcov/argvquery2.storm') await check_lines('stormcov/argvquery3.storm') + await check_lines('stormcov/argvquery4.storm') await check_lines('stormcov/dupesubs.storm') await check_lines('stormcov/embedquery.storm'), await check_lines('stormcov/embedquery2.storm'), await check_lines('stormcov/embedquery3.storm'), + await check_lines('stormcov/embedquery4.storm'), await check_lines('stormcov/lookup.storm') await check_lines('stormcov/pivot.storm') await check_lines('stormcov/pragma-nocov.storm') From be3ca8310c3c199e73cf18f97c5871f16aa5f8f2 Mon Sep 17 00:00:00 2001 From: blackout Date: Wed, 4 Mar 2026 15:52:25 -0500 Subject: [PATCH 19/20] dup line numbers --- synapse/tests/files/stormcov/argvquery5.storm | 11 +++++ synapse/tests/files/stormcov/dupewarn.storm | 39 +++++++++++++++ .../tests/files/stormcov/embedquery5.storm | 11 +++++ synapse/tests/test_utils_stormcov.py | 47 +++++++++++++++++-- synapse/utils/stormcov.py | 15 +++--- 5 files changed, 112 insertions(+), 11 deletions(-) create mode 100644 synapse/tests/files/stormcov/argvquery5.storm create mode 100644 synapse/tests/files/stormcov/dupewarn.storm create mode 100644 synapse/tests/files/stormcov/embedquery5.storm diff --git a/synapse/tests/files/stormcov/argvquery5.storm b/synapse/tests/files/stormcov/argvquery5.storm new file mode 100644 index 00000000000..de715cc6085 --- /dev/null +++ b/synapse/tests/files/stormcov/argvquery5.storm @@ -0,0 +1,11 @@ + +tee +{ + + + inet:fqdn=argvquery5.storm + + +} +// coverage: 2, 3, 6 +// lines: 2, 6 diff --git a/synapse/tests/files/stormcov/dupewarn.storm b/synapse/tests/files/stormcov/dupewarn.storm new file mode 100644 index 00000000000..f8f17b77936 --- /dev/null +++ b/synapse/tests/files/stormcov/dupewarn.storm @@ -0,0 +1,39 @@ +tee ${ inet:fqdn=01.embed.dupewarn.com } ${ inet:fqdn=01.embed.dupewarn.com } +| +tee ${ + [ inet:fqdn=00.embed.dupewarn.com ] +} +${ + + + [ inet:fqdn=00.embed.dupewarn.com ] + + +} +| +tee +${ inet:fqdn=02.embed.dupewarn.com } +${ + inet:fqdn=02.embed.dupewarn.com +} +// embddups: 1:1, 6:3, 16:15 +| +tee { + [ inet:fqdn=00.argv.dupewarn.com ] +} +{ + + + [ inet:fqdn=00.argv.dupewarn.com ] + + +} +| +tee { inet:fqdn=01.argv.dupewarn.com } { inet:fqdn=01.argv.dupewarn.com } +| +tee +{ inet:fqdn=02.argv.dupewarn.com } +{ + inet:fqdn=02.argv.dupewarn.com +} +// argvdups: 24:21, 32:32, 36:35 diff --git a/synapse/tests/files/stormcov/embedquery5.storm b/synapse/tests/files/stormcov/embedquery5.storm new file mode 100644 index 00000000000..e97963568d6 --- /dev/null +++ b/synapse/tests/files/stormcov/embedquery5.storm @@ -0,0 +1,11 @@ + +tee +${ + + + inet:fqdn=embedquery5.storm + + +} +// coverage: 2, 3, 6 +// lines: 2, 6 diff --git a/synapse/tests/test_utils_stormcov.py b/synapse/tests/test_utils_stormcov.py index 7f2f9c468c4..255cd58b2e6 100644 --- a/synapse/tests/test_utils_stormcov.py +++ b/synapse/tests/test_utils_stormcov.py @@ -37,11 +37,14 @@ async def test_stormcov_basics(self): s_files.getAssetPath('stormcov/argvquery2.storm'), s_files.getAssetPath('stormcov/argvquery3.storm'), s_files.getAssetPath('stormcov/argvquery4.storm'), + s_files.getAssetPath('stormcov/argvquery5.storm'), s_files.getAssetPath('stormcov/dupesubs.storm'), + s_files.getAssetPath('stormcov/dupewarn.storm'), s_files.getAssetPath('stormcov/embedquery.storm'), s_files.getAssetPath('stormcov/embedquery2.storm'), s_files.getAssetPath('stormcov/embedquery3.storm'), s_files.getAssetPath('stormcov/embedquery4.storm'), + s_files.getAssetPath('stormcov/embedquery5.storm'), s_files.getAssetPath('stormcov/lookup.storm'), s_files.getAssetPath('stormcov/pivot.storm'), s_files.getAssetPath('stormcov/pragma-nocov.storm'), @@ -56,10 +59,6 @@ async def test_stormcov_basics(self): badstorm = s_files.getAssetPath('stormcov/badstorm.storm') stream.expect(f'Skipping invalid storm file: {badstorm}') - dupesubs = s_files.getAssetPath('stormcov/dupesubs.storm') - stream.expect(f'Duplicate argvquery in {dupesubs} at line 24, coverage will be reported on first instance in {dupesubs} at line 21') - stream.expect(f'Duplicate embedquery in {dupesubs} at line 4, coverage will be reported on first instance in {dupesubs} at line 1') - self.sorteq(list(stormcov.guid_map.values()), stormfiles) stormcov.reset() @@ -72,6 +71,42 @@ async def test_stormcov_basics(self): self.sorteq(list(stormcov.guid_map.values()), stormfiles) stormcov.reset() + async def test_stormcov_dups(self): + basedir = pathlib.Path(s_files.ASSETS) + stormdir = str(basedir / 'stormcov') + opts = StormcovConfig(stormdirs=stormdir, stormcov_basedir=basedir) + + stormcov = s_stormcov.StormcovPlugin(opts) + with self.getLoggerStream('synapse.utils.stormcov') as stream: + stormcov.find_storm_files(stormdir) + + dupewarn = s_files.getAssetPath('stormcov/dupewarn.storm') + self.isin(dupewarn, list(stormcov.guid_map.values())) + + storm = s_files.getAssetStr('stormcov/dupewarn.storm') + + # Read argvquery duplicate pairs + argvdups = regex.search(r'\/\/ argvdups:.*?$', storm, flags=regex.M) + self.nn(argvdups, msg='dupewarn.storm requires a "// argvdups: #:#, #:#, #:#" line') + argvpairs = regex.findall(r'\d+:\d+', argvdups.group()) + + # Read embedquery duplicate pairs + embddups = regex.search(r'\/\/ embddups:.*?$', storm, flags=regex.M) + self.nn(embddups, msg='dupewarn.storm requires a "// embddups: #:#, #:#, #:#" line') + embdpairs = regex.findall(r'\d+:\d+', embddups.group()) + + def splitpair(x): + return list(map(int, x.split(':'))) + + argvexp = map(splitpair, argvpairs) + embdexp = map(splitpair, embdpairs) + + for last, first in embdexp: + stream.expect(f'Duplicate embedquery in {dupewarn} at line {last}, coverage will be reported on first instance in {dupewarn} at line {first}') + + for last, first in argvexp: + stream.expect(f'Duplicate argvquery in {dupewarn} at line {last}, coverage will be reported on first instance in {dupewarn} at line {first}') + async def test_stormcov_coverage(self): basedir = pathlib.Path(s_files.ASSETS) stormdir = str(basedir / 'stormcov') @@ -101,11 +136,13 @@ async def check_cov(filename): await check_cov('stormcov/argvquery2.storm') await check_cov('stormcov/argvquery3.storm') await check_cov('stormcov/argvquery4.storm') + await check_cov('stormcov/argvquery5.storm') await check_cov('stormcov/dupesubs.storm') await check_cov('stormcov/embedquery.storm') await check_cov('stormcov/embedquery2.storm'), await check_cov('stormcov/embedquery3.storm'), await check_cov('stormcov/embedquery4.storm'), + await check_cov('stormcov/embedquery5.storm'), await check_cov('stormcov/pivot.storm') await check_cov('stormcov/pragma-nocov.storm') await check_cov('stormcov/spin.storm') @@ -147,11 +184,13 @@ async def check_lines(filename): await check_lines('stormcov/argvquery2.storm') await check_lines('stormcov/argvquery3.storm') await check_lines('stormcov/argvquery4.storm') + await check_lines('stormcov/argvquery5.storm') await check_lines('stormcov/dupesubs.storm') await check_lines('stormcov/embedquery.storm'), await check_lines('stormcov/embedquery2.storm'), await check_lines('stormcov/embedquery3.storm'), await check_lines('stormcov/embedquery4.storm'), + await check_lines('stormcov/embedquery5.storm'), await check_lines('stormcov/lookup.storm') await check_lines('stormcov/pivot.storm') await check_lines('stormcov/pragma-nocov.storm') diff --git a/synapse/utils/stormcov.py b/synapse/utils/stormcov.py index e1472d04369..1a0b0350b7d 100644 --- a/synapse/utils/stormcov.py +++ b/synapse/utils/stormcov.py @@ -67,6 +67,7 @@ def pytest_addoption(parser): # pragma: no cover def pytest_configure(config): # pragma: no cover # NB: no coverage since this is a pytest hook + breakpoint() if not config.option.stormcov and not (config.option.stormdirs or config.option.stormcov_append): return @@ -158,19 +159,19 @@ def find_subqueries(self, tree, path): continue subg = s_common.guid(str(subq)) - offs = 1 line = node.meta.line - 1 + rline = line + 1 if rule == 'argvquery': - offs = 0 line = subq.meta.line - 1 + rline = node.meta.line if subg in self.subq_map: - (pname, pline) = self.subq_map[subg] - logger.warning(f'Duplicate {rule} in {path} at line {line + offs}, coverage will ' - f'be reported on first instance in {pname} at line {pline + offs}') + (pname, _, pline) = self.subq_map[subg] + logger.warning(f'Duplicate {rule} in {path} at line {rline}, coverage will ' + f'be reported on first instance in {pname} at line {pline}') continue - self.subq_map[subg] = (path, line) + self.subq_map[subg] = (path, line, rline) def _start_sysmon(self): if sys.monitoring.get_tool(self.toolid) is None: @@ -322,7 +323,7 @@ def handle_ast(self, code, frame=None): # pragma: no cover subq = self.subq_map.get(guid) if subq is not None: - (filename, offs) = subq + (filename, offs, _) = subq else: filename = self.guid_map.get(guid) offs = 0 From 9d972646c5616999d341f67b6669e79af7a60655 Mon Sep 17 00:00:00 2001 From: blackout Date: Wed, 4 Mar 2026 16:01:31 -0500 Subject: [PATCH 20/20] feedback --- synapse/utils/stormcov.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/synapse/utils/stormcov.py b/synapse/utils/stormcov.py index 1a0b0350b7d..869920a680b 100644 --- a/synapse/utils/stormcov.py +++ b/synapse/utils/stormcov.py @@ -67,7 +67,6 @@ def pytest_addoption(parser): # pragma: no cover def pytest_configure(config): # pragma: no cover # NB: no coverage since this is a pytest hook - breakpoint() if not config.option.stormcov and not (config.option.stormdirs or config.option.stormcov_append): return @@ -159,11 +158,11 @@ def find_subqueries(self, tree, path): continue subg = s_common.guid(str(subq)) + line = node.meta.line - 1 - rline = line + 1 + rline = node.meta.line if rule == 'argvquery': line = subq.meta.line - 1 - rline = node.meta.line if subg in self.subq_map: (pname, _, pline) = self.subq_map[subg]