[ Web Proxy ]
URL:
Viewing: https://raw.githubusercontent.com/sbf-developer/pythonnative/main/scripts/check-e2e-coverage.py [Back]  [Original]

#!/usr/bin/env python3
"""Verify every public ``pythonnative`` symbol is exercised by an E2E demo.

Run as part of ``./scripts/check.sh`` or directly:

    python scripts/check-e2e-coverage.py

The script:

1. Reads ``pythonnative.__all__`` to get the list of public symbols.
2. Reads ``examples/e2e-suite/app/registry.py`` to discover every
   demo's ``feature`` field. Each ``feature`` either matches a name
   in ``__all__`` or uses a ``"category::name"`` form for sub-features
   that aren't directly listed.
3. Reports any public symbol not covered by at least one demo.
4. Reports any demo flow file missing for a registered demo.

Exit code is ``0`` when every public symbol is covered and every demo
has a matching flow file; ``1`` otherwise.

This script is *static analysis only*: it doesn't run the app or the
flows. It exists so that adding a new public export to ``pythonnative``
without adding an E2E demo is a CI failure rather than a silent
regression.
"""

from __future__ import annotations

import ast
import json
import re
import sys
from pathlib import Path
from typing import Iterable, Set

ROOT = Path(__file__).resolve().parents[1]
SRC = ROOT / "src" / "pythonnative" / "__init__.py"
REGISTRY = ROOT / "examples" / "e2e-suite" / "app" / "registry.py"
FLOWS_DIR = ROOT / "tests" / "e2e" / "flows"

# Symbols intentionally NOT covered by a directly-mapped E2E flow.
# Each entry has a comment explaining why the symbol is exempt: type
# alias, ambient infrastructure exercised by every flow, or platform
# capability that requires real device hardware. New entries should
# justify themselves in the same way.
INTENTIONAL_EXEMPTIONS: Set[str] = {
    # --------------------------------------------------------------
    # Type-only re-exports: statically checkable, no UI surface.
    # --------------------------------------------------------------
    "Element",
    "AlignItems",
    "AlignSelf",
    "AutoCapitalize",
    "Color",
    "Dimension",
    "EdgeInsets",
    "FlexDirection",
    "FontWeight",
    "JustifyContent",
    "KeyboardType",
    "Overflow",
    "Position",
    "ReturnKeyType",
    "ScaleType",
    "ShadowOffset",
    "Style",
    "StyleProp",
    "TextAlign",
    "TextDecoration",
    "ThemeContext",
    "TransformSpec",
    "AnimatedValue",  # observed via use_animated_value usage in animations
    "QueryResult",  # observed via use_query demo
    "MutationCall",  # observed via use_mutation demo
    "MutationState",  # observed via use_mutation demo
    # --------------------------------------------------------------
    # Built-in Props dataclasses: exercised indirectly via their
    # corresponding component demos.
    # --------------------------------------------------------------
    "ActivityIndicatorProps",
    "ButtonProps",
    "CheckboxProps",
    "DatePickerProps",
    "ImageBackgroundProps",
    "ImageProps",
    "KeyboardAvoidingViewProps",
    "ModalProps",
    "PickerProps",
    "PressableProps",
    "ProgressBarProps",
    "SafeAreaViewProps",
    "ScrollViewProps",
    "SegmentedControlProps",
    "SliderProps",
    "SpacerProps",
    "StatusBarProps",
    "SwitchProps",
    "TextInputProps",
    "TextProps",
    "TouchableOpacityProps",
    "ViewProps",
    "WebViewProps",
    # --------------------------------------------------------------
    # Ambient infrastructure exercised by every flow (importing the
    # app + rendering any screen invokes them, so a dedicated demo
    # would be redundant).
    # --------------------------------------------------------------
    "component",  # @pn.component decorator, every screen uses it
    "Column",  # used everywhere; co-tested with View in ViewColumnRowDemo
    "Row",  # used everywhere; co-tested with View in ViewColumnRowDemo
    "NavigationContainer",  # wraps the root navigator; used by main.py
    "create_stack_navigator",  # used by main.py; every flow pushes screens
    "create_screen",  # platform bridge invoked by the native templates
    "use_navigation",  # used by every screen for go_back
    "use_animated_value",  # used in every animation demo
    "Provider",  # covered by use_context demo
    "create_context",  # covered by use_context demo
    "style",  # the style helper itself is exercised by every styled demo
    "resolve_style",  # internal helper exposed for SDK authors
    # --------------------------------------------------------------
    # Hooks whose values depend on physical device state that an
    # emulator/simulator can't reliably reproduce.
    # --------------------------------------------------------------
    "use_safe_area_insets",  # no reliable insets in emulator viewport
    "use_keyboard_height",  # requires a real keyboard transition
    "use_app_state",  # requires backgrounding the app (device lifecycle)
    "use_net_info",  # requires toggling real connectivity
    # --------------------------------------------------------------
    # Networking + native-module surfaces. These need network or
    # platform hardware (camera, location, notifications, clipboard,
    # haptics, biometrics, ) that CI emulators don't reliably
    # provide; tested via unit tests in tests/test_net.py and
    # tests/test_native_modules.py against their desktop fallbacks.
    # --------------------------------------------------------------
    "fetch",
    "HTTPError",
    "Response",
    "AppState",
    "Battery",
    "Biometrics",
    "Camera",
    "Clipboard",
    "FileSystem",
    "Haptics",
    "Linking",
    "Location",
    "NetInfo",
    "Notifications",
    "Permissions",
    "SecureStore",
    "Share",
    "Vibration",
    # --------------------------------------------------------------
    # SDK re-exports: module-level names mirroring submodule content.
    # --------------------------------------------------------------
    "runtime",  # module re-export; run_async demo covers it
    "sdk",  # module re-export; custom_component demo covers it
    "ViewHandler",  # ABC; subclassed by built-in handlers
    "element_factory",  # tested via unit tests; needs registered handlers
    "register_component",  # tested via unit tests; needs handler implementations
    "Props",  # tested in custom_component demo and SDK unit tests
}


def _public_symbols() -> Set[str]:
    """Return the values of ``__all__`` in the pythonnative package.

    Parsed via ``ast`` rather than imported, so the check can run
    without instantiating the package (which would pull in iOS/Android
    code paths that fail off-device).
    """
    tree = ast.parse(SRC.read_text(encoding="utf-8"))
    for node in ast.walk(tree):
        if isinstance(node, ast.Assign):
            for target in node.targets:
                if isinstance(target, ast.Name) and target.id == "__all__":
                    if isinstance(node.value, ast.List):
                        names: Set[str] = set()
                        for el in node.value.elts:
                            if isinstance(el, ast.Constant) and isinstance(el.value, str):
                                names.add(el.value)
                        return names
    raise RuntimeError(f"No __all__ list found in {SRC}")


def _read_registry_features() -> Set[str]:
    text = REGISTRY.read_text(encoding="utf-8")
    # Match: DemoEntry("id", "Category", "Title", "feature", ComponentName),
    pattern = re.compile(
        r'DemoEntry\(\s*"(?P[^"]+)"\s*,\s*"(?P[^"]+)"\s*,'
        r'\s*"(?P[^"]+)"\s*,\s*"(?P[^"]+)"\s*,',
        re.MULTILINE,
    )
    return {m.group("feature") for m in pattern.finditer(text)}


def _read_registry_ids() -> Set[str]:
    text = REGISTRY.read_text(encoding="utf-8")
    pattern = re.compile(r'DemoEntry\(\s*"(?P[^"]+)"', re.MULTILINE)
    return {m.group("id") for m in pattern.finditer(text)}


def _flow_files() -> Set[str]:
    """Return the set of demo ids that have a flow file."""
    ids: Set[str] = set()
    for path in FLOWS_DIR.rglob("*.yaml"):
        # Flow filename is the demo id (e.g. ``use_state.yaml``).
        ids.add(path.stem)
    return ids


def _format_report(
    public: Iterable[str],
    features: Set[str],
    demo_ids: Set[str],
    flow_ids: Set[str],
) -> dict:
    public_set = set(public)
    direct_covered = features & public_set
    missing_public = public_set - features - INTENTIONAL_EXEMPTIONS
    missing_flows = demo_ids - flow_ids
    extra_flows = flow_ids - demo_ids
    return {
        "public_symbols": len(public_set),
        "demos": len(demo_ids),
        "flows": len(flow_ids),
        "directly_covered_public_symbols": sorted(direct_covered),
        "missing_public_symbols": sorted(missing_public),
        "demos_without_flow": sorted(missing_flows),
        "flow_files_without_demo": sorted(extra_flows),
    }


def main(argv: list[str]) -> int:
    """Run the coverage check and print a human or JSON report."""
    public = _public_symbols()
    features = _read_registry_features()
    demo_ids = _read_registry_ids()
    flow_ids = _flow_files()

    report = _format_report(public, features, demo_ids, flow_ids)

    json_mode = "--json" in argv
    if json_mode:
        print(json.dumps(report, indent=2))
    else:
        print("E2E coverage report")
        print("===================")
        print(f"  Public symbols in pythonnative.__all__: {report['public_symbols']}")
        print(f"  Demo entries in registry:               {report['demos']}")
        print(f"  Flow files under tests/e2e/flows:       {report['flows']}")
        print(f"  Public symbols directly covered:        {len(report['directly_covered_public_symbols'])}")
        print(f"  Intentional exemptions:                 {len(INTENTIONAL_EXEMPTIONS)}")
        if report["missing_public_symbols"]:
            print()
            print("  ERROR: the following public symbols have no E2E demo:")
            for name in report["missing_public_symbols"]:
                print(f"    - {name}")
            print()
            print(
                "  Fix: add a DemoEntry to examples/e2e-suite/app/registry.py "
                'with feature="", create the screen, and add a '
                "tests/e2e/flows//.yaml file.\n"
                "  Alternatively, if the symbol genuinely can't be tested "
                "via a UI flow, add it to INTENTIONAL_EXEMPTIONS in this "
                "script with a justification comment."
            )
        if report["demos_without_flow"]:
            print()
            print("  ERROR: the following registered demos have no flow file:")
            for name in report["demos_without_flow"]:
                print(f"    - {name}")
        if report["flow_files_without_demo"]:
            print()
            print("  WARNING: the following flow files don't match any registered demo:")
            for name in report["flow_files_without_demo"]:
                print(f"    - {name}")

    failed = bool(report["missing_public_symbols"]) or bool(report["demos_without_flow"])
    return 1 if failed else 0


if __name__ == "__main__":
    sys.exit(main(sys.argv[1:]))

Web Proxy Viewer  |  New URL  |  Original Page