Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[report]
omit =
*/tests/*
*/config/*
*/insights/setup.py
*/coverage.py
38 changes: 38 additions & 0 deletions .github/helpers/legacy_v2_python_globs.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Paths are relative to apps/insights.
# Keep this list aligned with the Python files deleted by the remove-v2-code branch.

insights/api/home.py
insights/api/notebooks.py
insights/api/permissions.py
insights/api/public.py
insights/api/queries.py
insights/api/setup.py
insights/api/subscription.py

insights/insights/doctype/insights_chart/**
insights/insights/doctype/insights_dashboard/**
insights/insights/doctype/insights_dashboard_item/**
insights/insights/doctype/insights_data_source/**
insights/insights/doctype/insights_notebook/**
insights/insights/doctype/insights_notebook_page/**
insights/insights/doctype/insights_query/**
insights/insights/doctype/insights_query_chart/**
insights/insights/doctype/insights_query_column/**
insights/insights/doctype/insights_query_result/**
insights/insights/doctype/insights_query_table/**
insights/insights/doctype/insights_query_transform/**
insights/insights/doctype/insights_table/**
insights/insights/doctype/insights_table_column/**
insights/insights/doctype/insights_table_link/**
insights/insights/doctype/insights_team/insights_team_client.py
insights/insights/doctype/insights_team/test_insights_team.py

insights/insights/query_builders/__init__.py
insights/insights/query_builders/legacy_query_builder.py
insights/insights/query_builders/postgresql/builder.py
insights/insights/query_builders/sql_builder.py
insights/insights/query_builders/sqlite/sqlite_query_builder.py
insights/insights/query_builders/test_sql_builder.py
insights/insights/query_builders/utils.py

insights/www/insights_v2.py
171 changes: 171 additions & 0 deletions .github/helpers/run_backend_tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
#!/usr/bin/env python3

from __future__ import annotations

import argparse
import os
from fnmatch import fnmatch
from pathlib import Path

from coverage import Coverage
from coverage.exceptions import NoDataError

APP_ROOT = Path(__file__).resolve().parents[2]
DEFAULT_APP = APP_ROOT.name
LEGACY_GLOBS_FILE = Path(__file__).with_name("legacy_v2_python_globs.txt")
COVERAGE_CONFIG_FILE = APP_ROOT / ".coveragerc"
SKIP_DIRS = {"node_modules", "locals", "public", "__pycache__"}


def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Run Insights backend tests while excluding legacy v2 code from discovery and coverage."
)
parser.add_argument("--site", help="Site to run tests against")
parser.add_argument("--app", default=DEFAULT_APP, help="App to run tests for")
parser.add_argument(
"--coverage-file",
default="sites/coverage.xml",
help="Coverage XML output path, relative to the current working directory",
)
parser.add_argument(
"--legacy-globs-file",
default=str(LEGACY_GLOBS_FILE),
help="Path to the legacy v2 path glob list",
)
parser.add_argument(
"--list-only",
action="store_true",
help="Print the included and excluded test modules without running tests",
)
return parser.parse_args()


def dedupe(items: list[str]) -> list[str]:
return list(dict.fromkeys(items))


def load_globs(path: Path) -> list[str]:
patterns: list[str] = []
for line in path.read_text().splitlines():
stripped = line.strip()
if stripped and not stripped.startswith("#"):
patterns.append(stripped)
return patterns


def matches_any(path: str, patterns: list[str]) -> bool:
return any(fnmatch(path, pattern) for pattern in patterns)


def discover_test_modules(app_root: Path, legacy_globs: list[str]) -> tuple[list[str], list[str]]:
modules: list[str] = []
excluded: list[str] = []

for file_path in sorted(app_root.rglob("test_*.py")):
rel_path = file_path.relative_to(app_root)
rel_path_str = rel_path.as_posix()

if file_path.name == "test_runner.py":
continue

if any(part.startswith(".") for part in rel_path.parts):
continue

if any(skip_dir in rel_path.parts for skip_dir in SKIP_DIRS):
continue

if "doctype/doctype/boilerplate" in rel_path_str:
continue

module_name = ".".join(file_path.relative_to(app_root).with_suffix("").parts)
if matches_any(rel_path_str, legacy_globs):
excluded.append(module_name)
continue

modules.append(module_name)

return modules, excluded


def load_base_omit_patterns(config_file: Path) -> list[str]:
if not config_file.exists():
return []

coverage = Coverage(config_file=str(config_file))
return dedupe(list(coverage.config.run_omit) + list(coverage.config.report_omit))


def build_coverage_omit_patterns(base_patterns: list[str], legacy_globs: list[str]) -> list[str]:
from frappe.coverage import STANDARD_EXCLUSIONS

legacy_patterns = [f"*/{pattern}" for pattern in legacy_globs]
return dedupe(STANDARD_EXCLUSIONS + base_patterns + legacy_patterns)


def run_tests(site: str, app: str, modules: list[str], coverage_file: Path, omit_patterns: list[str]) -> None:
import frappe
from frappe.commands.testing import main as run_tests_main

bench_root = Path.cwd()
coverage_file = coverage_file if coverage_file.is_absolute() else bench_root / coverage_file
sites_dir = bench_root / "sites"
original_cwd = Path.cwd()
source_root = APP_ROOT / app
coverage = Coverage(
source=[str(source_root)],
omit=omit_patterns,
)

coverage_file.parent.mkdir(parents=True, exist_ok=True)
coverage_started = False

try:
os.chdir(sites_dir)
frappe.init(site)
coverage.start()
coverage_started = True
run_tests_main(site=site, app=app, module=modules)
finally:
if coverage_started:
coverage.stop()
coverage.save()
try:
coverage.xml_report(outfile=str(coverage_file))
except NoDataError:
print("No coverage data collected")
frappe.destroy()
os.chdir(original_cwd)
print(f"Saved Coverage: {coverage_file}")


def main() -> None:
args = parse_args()
legacy_globs = load_globs(Path(args.legacy_globs_file))
modules, excluded_modules = discover_test_modules(APP_ROOT, legacy_globs)

print(f"Included test modules: {len(modules)}")
print(f"Excluded legacy test modules: {len(excluded_modules)}")

if excluded_modules:
for module_name in excluded_modules:
print(f"- {module_name}")

if args.list_only:
return

if not args.site:
raise SystemExit("--site is required unless --list-only is used")

if not modules:
raise SystemExit("No backend test modules remain after applying the legacy v2 exclusions")

omit_patterns = build_coverage_omit_patterns(
load_base_omit_patterns(COVERAGE_CONFIG_FILE),
legacy_globs,
)
run_tests(args.site, args.app, modules, Path(args.coverage_file), omit_patterns)


if __name__ == "__main__":
main()
82 changes: 55 additions & 27 deletions .github/workflows/server-tests.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
name: Server Tests
name: Server

on:
pull_request:
workflow_dispatch:
schedule:
# Run everday at midnight UTC / 5:30 IST
- cron: "0 0 * * *"

concurrency:
group: develop-insights-${{ github.event.number }}
group: server-${{ github.event_name }}-${{ github.event.number }}
cancel-in-progress: true

permissions:
Expand All @@ -18,6 +20,12 @@ jobs:
strategy:
fail-fast: false
name: Server
env:
BENCH_PATH: /home/runner/frappe-bench
SITE_NAME: test_site
DB_ROOT_PASSWORD: root
ADMIN_PASSWORD: admin
FRAPPE_BRANCH: develop

services:
mariadb:
Expand All @@ -30,7 +38,7 @@ jobs:

steps:
- name: Clone
uses: actions/checkout@v2
uses: actions/checkout@v6

- name: Check for valid Python & Merge Conflicts
run: |
Expand All @@ -41,18 +49,18 @@ jobs:
fi

- name: Setup Python
uses: actions/setup-python@v2
uses: actions/setup-python@v6
with:
python-version: "3.10"
python-version: "3.14"

- name: Setup Node
uses: actions/setup-node@v2
uses: actions/setup-node@v5
with:
node-version: 18
node-version: 24
check-latest: true

- name: Cache pip
uses: actions/cache@v2
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/setup.py', '**/setup.cfg') }}
Expand All @@ -62,62 +70,82 @@ jobs:

- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: 'echo "::set-output name=dir::$(yarn cache dir)"'
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT

- uses: actions/cache@v2
- uses: actions/cache@v4
id: yarn-cache
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-

- name: Install system dependencies
run: |
sudo apt -qq update
sudo apt -qq install -y redis-server

- name: Setup
run: |
pip install frappe-bench
bench init --skip-redis-config-generation --skip-assets --python "$(which python)" ~/frappe-bench
mysql --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL character_set_server = 'utf8mb4'"
mysql --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'"
bench init \
--frappe-branch "$FRAPPE_BRANCH" \
--skip-assets \
--python "$(which python)" \
"$BENCH_PATH"
mysql --host 127.0.0.1 --port 3306 -u root -p"$DB_ROOT_PASSWORD" -e "SET GLOBAL character_set_server = 'utf8mb4'"
mysql --host 127.0.0.1 --port 3306 -u root -p"$DB_ROOT_PASSWORD" -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'"

- name: Install
working-directory: /home/runner/frappe-bench
working-directory: ${{ env.BENCH_PATH }}
run: |
bench get-app insights $GITHUB_WORKSPACE
bench get-app insights $GITHUB_WORKSPACE --skip-assets
bench setup requirements --dev
bench new-site --db-root-password root --admin-password admin test_site
bench --site test_site install-app insights
bench new-site --db-root-password "$DB_ROOT_PASSWORD" --admin-password "$ADMIN_PASSWORD" "$SITE_NAME"
bench --site "$SITE_NAME" set-config server_script_enabled 1 --parse
bench --site "$SITE_NAME" install-app insights
bench build
env:
CI: "Yes"

- name: Start Bench Services
working-directory: ${{ env.BENCH_PATH }}
run: |
sed -i '/^watch:/d' Procfile
sed -i '/^schedule:/d' Procfile
sed -i '/^socketio:/d' Procfile
bench start &> bench_start.log &

- name: Run Tests
working-directory: /home/runner/frappe-bench
working-directory: ${{ env.BENCH_PATH }}
run: |
bench --site test_site set-config allow_tests true
bench --site test_site run-tests --app insights --coverage
bench --site "$SITE_NAME" set-config allow_tests 1 --parse
env/bin/python apps/insights/.github/helpers/run_backend_tests.py --site "$SITE_NAME"
env:
TYPE: server

- name: Upload coverage data
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: coverage-${{ matrix.container }}
path: /home/runner/frappe-bench/sites/coverage.xml
name: coverage-server
path: ${{ env.BENCH_PATH }}/sites/coverage.xml

coverage:
name: Coverage Wrap Up
needs: tests
runs-on: ubuntu-latest
steps:
- name: Clone
uses: actions/checkout@v3
uses: actions/checkout@v6

- name: Download artifacts
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4

- name: Upload coverage data
uses: codecov/codecov-action@v3
uses: codecov/codecov-action@v6
with:
name: coverage-unittests
name: Server
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
verbose: true
flags: server
Loading
Loading