diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml new file mode 100644 index 000000000000..aec0366a60b6 --- /dev/null +++ b/.github/actions/setup/action.yml @@ -0,0 +1,268 @@ +name: 'Setup Environment' +description: 'Sets up the environment for Frappe development' +inputs: + python-version: + description: 'Python version to use' + required: false + default: '3.12.6' + node-version: + description: 'Node.js version to use' + required: false + default: '20' + build-assets: + required: false + description: 'Wether to build assets' + default: true + enable-coverage: + required: false + default: false + enable-watch: + required: false + default: false + enable-schedule: + required: false + default: false + disable-web: + required: false + default: false + disable-socketio: + required: false + default: false + disable-redis-socketio: + required: false + default: false + db: + required: false + default: mariadb + db-root-password: + required: true + +runs: + using: "composite" + steps: + - name: Clone + uses: actions/checkout@v4 + with: + path: ${{ github.event.repository.name }} + + - name: Checkout Frappe + uses: actions/checkout@v4 + with: + repository: ${{ env.GH_ORG || github.repository_owner }}/frappe + ref: ${{ env.FRAPPE_BRANCH || github.base_ref || github.ref_name }} + path: frappe + if: github.event.repository.name != 'frappe' + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: ${{ inputs.python-version }} + + - shell: bash -e {0} + run: | + # Check for valid Python & Merge Conflicts + python -m compileall -q -f "${GITHUB_WORKSPACE}/${{ github.event.repository.name }}" + if grep -lr --exclude-dir=node_modules "^<<<<<<< " "${GITHUB_WORKSPACE}/${{ github.event.repository.name }}" + then echo "Found merge conflicts" + exit 1 + fi + + - uses: actions/setup-node@v4 + with: + node-version: ${{ inputs.node-version }} + check-latest: true + + - shell: bash -e {0} + run: | + # Add 'test_site' to /etc/hosts + echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts + git config --global init.defaultBranch main + git config --global advice.detachedHead false + + - name: Cache pip + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/setup.py') }} + restore-keys: | + ${{ runner.os }}-pip- + ${{ runner.os }}- + + - id: yarn-cache-dir-path + shell: bash -e {0} + run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT + - 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- + + - shell: bash -e {0} + run: | + # Install System Dependencies + start_time=$(date +%s) + + sudo apt -qq update + sudo apt -qq remove mysql-server mysql-client + sudo apt -qq install libcups2-dev redis-server mariadb-client-10.6 + + if [ "$(lsb_release -rs)" = "22.04" ]; then + wget -q -O /tmp/wkhtmltox.deb https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-2/wkhtmltox_0.12.6.1-2.jammy_amd64.deb + sudo apt install /tmp/wkhtmltox.deb + else + echo "Please update frappe/.github/lib/tests.yml to support wkhtmltopdf for $(lsb_release -ds)" + exit 1 + fi + + end_time=$(date +%s) + echo -e "\033[33mInstall System Dependencies: $((end_time - start_time)) seconds\033[0m" + + - shell: bash -e {0} + run: | + # Install Bench + start_time=$(date +%s) + + cd ~ || exit + pip install frappe-bench + + end_time=$(date +%s) + echo -e "\033[33mInstall Bench: $((end_time - start_time)) seconds\033[0m" + + - shell: bash -e {0} + run: | + # Init Bench + start_time=$(date +%s) + + cd ~ || exit + verbosity="${BENCH_INIT_VERBOSITY_FLAG:-}" + bench $verbosity init frappe-bench --skip-assets --python "$(which python)" --frappe-path "${GITHUB_WORKSPACE}/frappe" + + end_time=$(date +%s) + echo -e "\033[33mInit Bench: $((end_time - start_time)) seconds\033[0m" + + - shell: bash -e {0} + run: | + # Install App(s) + start_time=$(date +%s) + + cd ~/frappe-bench || exit + verbosity="${BENCH_INIT_VERBOSITY_FLAG:-}" + + for app in ${GITHUB_WORKSPACE}/*/; do + if [ -f "${app}setup.py" ] || [ -f "${app}pyproject.toml" ]; then + start_time=$(date +%s) + echo "Installing app in ${app}" + pip install --upgrade -e "${app}[dev,test]" + end_time=$(date +%s) + echo "Time taken to Install ${app} requirements: $((end_time - start_time)) seconds" + fi + done + # collect old style tools.bench.dev-dependencies + bench $verbosity setup requirements --dev; + if [ "$TYPE" == "ui" ] + then + bench $verbosity setup requirements --node; + fi + + end_time=$(date +%s) + echo -e "\033[33mInstall App(s): $((end_time - start_time)) seconds\033[0m" + env: + TYPE: server + + - shell: bash -e {0} + run: | + # Setup Test Site + start_time=$(date +%s) + + cd ~/frappe-bench || exit + + mkdir ~/frappe-bench/sites/test_site + + # Attempt to copy the configuration file + if cp "${GITHUB_WORKSPACE}/${{ github.event.repository.name }}/.github/helper/db/$DB.json" ~/frappe-bench/sites/test_site/site_config.json; then + echo "Successfully copied ${DB}.json to site_config.json." + else + echo "Error: The configuration file ${GITHUB_WORKSPACE}/${{ github.event.repository.name }}/.github/helper/db/$DB.json does not exist." + echo "Please ensure that the database JSON file is correctly named and located in the helper/db directory." + exit 1 # Exit with a non-zero status to indicate failure + fi + + if [ "$DB" == "mariadb" ]; then + mariadb --host 127.0.0.1 --port 3306 -u root -p${{ inputs.db-root-password }} -e "SET GLOBAL character_set_server = 'utf8mb4'"; + mariadb --host 127.0.0.1 --port 3306 -u root -p${{ inputs.db-root-password }} -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'"; + + mariadb --host 127.0.0.1 --port 3306 -u root -p${{ inputs.db-root-password }} -e "CREATE DATABASE test_frappe"; + mariadb --host 127.0.0.1 --port 3306 -u root -p${{ inputs.db-root-password }} -e "CREATE USER 'test_frappe'@'localhost' IDENTIFIED BY 'test_frappe'"; + mariadb --host 127.0.0.1 --port 3306 -u root -p${{ inputs.db-root-password }} -e "GRANT ALL PRIVILEGES ON \`test_frappe\`.* TO 'test_frappe'@'localhost'"; + + mariadb --host 127.0.0.1 --port 3306 -u root -p${{ inputs.db-root-password }} -e "FLUSH PRIVILEGES"; + elif [ "$DB" == "postgres" ]; then + echo "${{ inputs.db-root-password }}" | psql -h 127.0.0.1 -p 5432 -c "CREATE DATABASE test_frappe" -U postgres; + echo "${{ inputs.db-root-password }}" | psql -h 127.0.0.1 -p 5432 -c "CREATE USER test_frappe WITH PASSWORD 'test_frappe'" -U postgres; + fi + + end_time=$(date +%s) + echo -e "\033[33mSetup Test Site: $((end_time - start_time)) seconds\033[0m" + env: + DB: ${{ inputs.db }} + + - shell: bash -e {0} + run: | + # Modify Procfile + cd ~/frappe-bench || exit + if ${{ inputs.enable-watch != 'true' }}; then + sed -i 's/^watch:/# watch:/g' Procfile + fi + if ${{ inputs.enable-schedule != 'true'}}; then + sed -i 's/^schedule:/# schedule:/g' Procfile + fi + if ${{ inputs.disable-socketio }}; then + sed -i 's/^socketio:/# socketio:/g' Procfile + fi + if ${{ inputs.disable-redis-socketio }}; then + sed -i 's/^redis_socketio:/# redis_socketio:/g' Procfile + fi + if ${{ inputs.enable-coverage }}; then + sed -i 's/^web: bench serve/web: bench serve --with-coverage/g' Procfile + fi + if ${{ inputs.disable-web }}; then + sed -i 's/^web:/# web:/g' Procfile + fi + + - shell: bash -e {0} + run: | + # Display modified Procfile + cd ~/frappe-bench || exit + cat Procfile | awk '{print "\033[0;34m" $0 "\033[0m"}' + + - shell: bash -e {0} + run: | + # Start Bench + cd ~/frappe-bench || exit + bench start &> ~/frappe-bench/bench_start.log & + + - shell: bash -e {0} + if: ${{ inputs.build-assets == 'true' }} + run: | + # Build Assets + start_time=$(date +%s) + + cd ~/frappe-bench || exit + CI=Yes bench build --app frappe + + end_time=$(date +%s) + echo -e "\033[33mBuild Assets: $((end_time - start_time)) seconds\033[0m" + + - shell: bash -e {0} + run: | + # Reinstall Test Site + start_time=$(date +%s) + + cd ~/frappe-bench || exit + bench --site test_site reinstall --yes + + end_time=$(date +%s) + echo -e "\033[33mReinstall Test Site: $((end_time - start_time)) seconds\033[0m" + diff --git a/.github/helper/ci.py b/.github/helper/ci.py deleted file mode 100644 index df18466cd504..000000000000 --- a/.github/helper/ci.py +++ /dev/null @@ -1,160 +0,0 @@ -""" -Code Coverage and Parallel Test Runner Script - -This script is designed to run parallel tests for Frappe applications with optional code coverage. -It sets up the test environment, handles code coverage configuration, and executes tests using -either a local parallel test runner or an orchestrator-based runner. - -Key features: -- Configurable code coverage for specific apps -- Support for local parallel testing and orchestrator-based testing -- Customizable inclusion and exclusion patterns for coverage -- Environment variable based configuration - -Usage: -This script is typically run as part of a CI/CD pipeline or for local development testing. -It can be configured using environment variables such as SITE, ORCHESTRATOR_URL, WITH_COVERAGE, etc. -""" - -# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors -# MIT License. See LICENSE - -import json -import os -from pathlib import Path - -# Define standard patterns for file inclusions and exclusions in coverage -STANDARD_INCLUSIONS = ["*.py"] - -STANDARD_EXCLUSIONS = [ - "*.js", - "*.xml", - "*.pyc", - "*.css", - "*.less", - "*.scss", - "*.vue", - "*.html", - "*/test_*", - "*/node_modules/*", - "*/doctype/*/*_dashboard.py", - "*/patches/*", - ".github/*", -] - -# Files that are tested via command line interface -TESTED_VIA_CLI = [ - "*/frappe/installer.py", - "*/frappe/utils/install.py", - "*/frappe/utils/scheduler.py", - "*/frappe/utils/doctor.py", - "*/frappe/build.py", - "*/frappe/database/__init__.py", - "*/frappe/database/db_manager.py", - "*/frappe/database/**/setup_db.py", -] - -# Additional exclusions specific to the Frappe app -FRAPPE_EXCLUSIONS = [ - "*/tests/*", - "*/commands/*", - "*/frappe/change_log/*", - "*/frappe/exceptions*", - "*/frappe/desk/page/setup_wizard/setup_wizard.py", - "*/frappe/coverage.py", - "*frappe/setup.py", - "*/frappe/hooks.py", - "*/doctype/*/*_dashboard.py", - "*/patches/*", - "*/.github/helper/ci.py", - *TESTED_VIA_CLI, -] - - -def get_bench_path(): - """Get the path to the bench directory.""" - return Path(__file__).resolve().parents[4] - - -class CodeCoverage: - """ - Context manager for handling code coverage. - - This class sets up code coverage measurement for a specific app, - applying the appropriate inclusion and exclusion patterns. - """ - - def __init__(self, with_coverage, app): - self.with_coverage = with_coverage - self.app = app or "frappe" - - def __enter__(self): - if self.with_coverage: - import os - from coverage import Coverage - - # Set up coverage for the specific app - source_path = os.path.join(get_bench_path(), "apps", self.app) - print(f"Source path: {source_path}") - omit = STANDARD_EXCLUSIONS[:] - - if self.app == "frappe": - omit.extend(FRAPPE_EXCLUSIONS) - - self.coverage = Coverage(source=[source_path], omit=omit, include=STANDARD_INCLUSIONS) - self.coverage.start() - - def __exit__(self, exc_type, exc_value, traceback): - if self.with_coverage: - self.coverage.stop() - self.coverage.save() - self.coverage.xml_report() - - -if __name__ == "__main__": - # Configuration - app = "frappe" - site = os.environ.get("SITE") or "test_site" - use_orchestrator = bool(os.environ.get("ORCHESTRATOR_URL")) - with_coverage = json.loads(os.environ.get("WITH_COVERAGE", "true").lower()) - build_number = 1 - total_builds = 1 - - # Parse build information from environment variables - try: - build_number = int(os.environ.get("BUILD_NUMBER")) - except Exception: - pass - - try: - total_builds = int(os.environ.get("TOTAL_BUILDS")) - except Exception: - pass - - # Run tests with code coverage - with CodeCoverage(with_coverage=with_coverage, app=app): - # Add ASCII banner at the end - if use_orchestrator: - from frappe.parallel_test_runner import ParallelTestWithOrchestrator - - runner = ParallelTestWithOrchestrator(app, site=site) - else: - from frappe.parallel_test_runner import ParallelTestRunner - - runner = ParallelTestRunner(app, site=site, build_number=build_number, total_builds=total_builds) - - mode = "Orchestrator" if use_orchestrator else "Parallel" - banner = f""" - ╔════════════════════════════════════════════╗ - ║ CI Helper Script Execution Summary ║ - ╠════════════════════════════════════════════╣ - ║ Mode: {mode:<26} ║ - ║ App: {app:<26} ║ - ║ Site: {site:<26} ║ - ║ Build Number: {build_number:<26} ║ - ║ Total Builds: {total_builds:<26} ║ - ║ Tests in Build: ~{runner.total_tests:<25} ║ - ╚════════════════════════════════════════════╝ - """ - print(banner) - runner.setup_and_run() diff --git a/.github/helper/db/mariadb.json b/.github/helper/db/mariadb.json index 0a6c9890c42d..4b1f98e78ff4 100644 --- a/.github/helper/db/mariadb.json +++ b/.github/helper/db/mariadb.json @@ -1,19 +1,19 @@ { - "db_host": "127.0.0.1", - "db_port": 3306, - "db_name": "test_frappe", - "db_password": "test_frappe", - "allow_tests": true, - "db_type": "mariadb", - "auto_email_id": "test@example.com", - "mail_server": "localhost", - "mail_port": 2525, - "mail_login": "test@example.com", - "mail_password": "test", - "admin_password": "admin", - "root_login": "root", - "root_password": "travis", - "host_name": "http://test_site:8000", - "monitor": 1, - "server_script_enabled": true -} + "db_host": "127.0.0.1", + "db_port": 3306, + "db_name": "test_frappe", + "db_password": "test_frappe", + "allow_tests": true, + "db_type": "mariadb", + "auto_email_id": "test@example.com", + "mail_server": "localhost", + "mail_port": 2525, + "mail_login": "test@example.com", + "mail_password": "test", + "admin_password": "admin", + "root_login": "root", + "root_password": "db_root", + "host_name": "http://test_site:8000", + "monitor": 1, + "server_script_enabled": true +} \ No newline at end of file diff --git a/.github/helper/db/postgres.json b/.github/helper/db/postgres.json index f830e717ed5f..ea15dc82ee64 100644 --- a/.github/helper/db/postgres.json +++ b/.github/helper/db/postgres.json @@ -1,18 +1,18 @@ { - "db_host": "127.0.0.1", - "db_port": 5432, - "db_name": "test_frappe", - "db_password": "test_frappe", - "db_type": "postgres", - "allow_tests": true, - "auto_email_id": "test@example.com", - "mail_server": "localhost", - "mail_port": 2525, - "mail_login": "test@example.com", - "mail_password": "test", - "admin_password": "admin", - "root_login": "postgres", - "root_password": "travis", - "host_name": "http://test_site:8000", - "server_script_enabled": true -} + "db_host": "127.0.0.1", + "db_port": 5432, + "db_name": "test_frappe", + "db_password": "test_frappe", + "db_type": "postgres", + "allow_tests": true, + "auto_email_id": "test@example.com", + "mail_server": "localhost", + "mail_port": 2525, + "mail_login": "test@example.com", + "mail_password": "test", + "admin_password": "admin", + "root_login": "postgres", + "root_password": "db_root", + "host_name": "http://test_site:8000", + "server_script_enabled": true +} \ No newline at end of file diff --git a/.github/helper/install_site.sh b/.github/helper/install_site.sh index bdee99908f55..c87b905ae4ea 100644 --- a/.github/helper/install_site.sh +++ b/.github/helper/install_site.sh @@ -8,19 +8,19 @@ cp "${GITHUB_WORKSPACE}/.github/helper/db/$DB.json" ~/frappe-bench/sites/test_si if [ "$DB" == "mariadb" ] then - mariadb --host 127.0.0.1 --port 3306 -u root -ptravis -e "SET GLOBAL character_set_server = 'utf8mb4'"; - mariadb --host 127.0.0.1 --port 3306 -u root -ptravis -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'"; + mariadb --host 127.0.0.1 --port 3306 -u root -pdb_root -e "SET GLOBAL character_set_server = 'utf8mb4'"; + mariadb --host 127.0.0.1 --port 3306 -u root -pdb_root -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'"; - mariadb --host 127.0.0.1 --port 3306 -u root -ptravis -e "CREATE DATABASE test_frappe"; - mariadb --host 127.0.0.1 --port 3306 -u root -ptravis -e "CREATE USER 'test_frappe'@'localhost' IDENTIFIED BY 'test_frappe'"; - mariadb --host 127.0.0.1 --port 3306 -u root -ptravis -e "GRANT ALL PRIVILEGES ON \`test_frappe\`.* TO 'test_frappe'@'localhost'"; + mariadb --host 127.0.0.1 --port 3306 -u root -pdb_root -e "CREATE DATABASE test_frappe"; + mariadb --host 127.0.0.1 --port 3306 -u root -pdb_root -e "CREATE USER 'test_frappe'@'localhost' IDENTIFIED BY 'test_frappe'"; + mariadb --host 127.0.0.1 --port 3306 -u root -pdb_root -e "GRANT ALL PRIVILEGES ON \`test_frappe\`.* TO 'test_frappe'@'localhost'"; - mariadb --host 127.0.0.1 --port 3306 -u root -ptravis -e "FLUSH PRIVILEGES"; + mariadb --host 127.0.0.1 --port 3306 -u root -pdb_root -e "FLUSH PRIVILEGES"; fi if [ "$DB" == "postgres" ] then - echo "travis" | psql -h 127.0.0.1 -p 5432 -c "CREATE DATABASE test_frappe" -U postgres; - echo "travis" | psql -h 127.0.0.1 -p 5432 -c "CREATE USER test_frappe WITH PASSWORD 'test_frappe'" -U postgres; + echo "db_root" | psql -h 127.0.0.1 -p 5432 -c "CREATE DATABASE test_frappe" -U postgres; + echo "db_root" | psql -h 127.0.0.1 -p 5432 -c "CREATE USER test_frappe WITH PASSWORD 'test_frappe'" -U postgres; fi echo "::endgroup::" diff --git a/.github/workflows/patch-base.yml b/.github/workflows/patch-base.yml new file mode 100644 index 000000000000..7c27a31e3357 --- /dev/null +++ b/.github/workflows/patch-base.yml @@ -0,0 +1,131 @@ +name: Patch Base +on: + workflow_call: + inputs: + fake-success: + required: false + type: boolean + default: false + python-version: + required: false + type: string + default: '3.12' + node-version: + required: false + type: number + default: 20 +jobs: + # This satisfies the required checks on pull requests if tests are intentionally skipped + migration-test-fake: + name: Migrate + runs-on: ubuntu-latest + if: ${{ inputs.fake-success != false }} + steps: + - name: Pass skipped tests unconditionally + run: "echo Skipped" + + migration-test: + name: Migrate + runs-on: ubuntu-latest + if: ${{ inputs.fake-success == false }} + timeout-minutes: 60 + strategy: + fail-fast: false + env: + PYTHONWARNINGS: "ignore" + DB_ROOT_PASSWORD: db_root + services: + mariadb: + image: mariadb:11.3 + ports: + - 3306:3306 + options: --health-cmd="healthcheck.sh --connect --innodb_initialized" --health-interval=5s --health-timeout=2s --health-retries=3 + env: + MARIADB_ROOT_PASSWORD: ${{ env.DB_ROOT_PASSWORD }} + steps: + - uses: frappe/frappe/.github/actions/setup@develop + name: Environment Setup + with: + python-version: '3.10' + node-version: 20 + build-assets: false + disable-socketio: true + disable-web: true + db-root-password: ${{ env.DB_ROOT_PASSWORD }} + + - name: Recover v13 database artifact + run: | + cd ~/frappe-bench/ + wget https://frappeframework.com/files/v13-frappe.sql.gz + bench --site test_site --force restore ~/frappe-bench/v13-frappe.sql.gz + + source env/bin/activate + cd apps/frappe/ + git remote set-url upstream https://github.com/frappe/frappe.git + + - name: Update to v14 + run: | + cd ~/frappe-bench/apps/frappe/ + function update_to_version() { + version=$1 + + branch_name="version-$version-hotfix" + echo "Updating to v$version" + git fetch --depth 1 upstream $branch_name:$branch_name + git checkout -q -f $branch_name + + pgrep honcho | xargs kill + sleep 3 + rm -rf ~/frappe-bench/env + bench -v setup env + bench start &>> ~/frappe-bench/bench_start.log & + + bench --site test_site migrate + } + + update_to_version 14 + + - name: Update to v15 + run: | + cd ~/frappe-bench/apps/frappe/ + function update_to_version() { + version=$1 + + branch_name="version-$version-hotfix" + echo "Updating to v$version" + git fetch --depth 1 upstream $branch_name:$branch_name + git checkout -q -f $branch_name + + pgrep honcho | xargs kill + sleep 3 + rm -rf ~/frappe-bench/env + bench -v setup env + bench start &>> ~/frappe-bench/bench_start.log & + + bench --site test_site migrate + } + update_to_version 15 + + - name: Update to last commit + run: | + cd ~/frappe-bench/apps/frappe/ + echo "Updating to last commit" + pgrep honcho | xargs kill + sleep 3 + rm -rf ~/frappe-bench/env + git checkout -q -f "$GITHUB_SHA" + bench -v setup env + bench start &>> ~/frappe-bench/bench_start.log & + bench --site test_site migrate + bench --site test_site execute frappe.tests.utils.check_orpahned_doctypes + + - name: Show bench output + if: ${{ always() }} + run: | + cd ~/frappe-bench + cat bench_start.log || true + cd logs + for f in ./*.log*; do + echo "Printing log: $f"; + cat $f + done diff --git a/.github/workflows/patch-mariadb-tests.yml b/.github/workflows/patch-mariadb-tests.yml index f90153128bbc..1822e61be934 100644 --- a/.github/workflows/patch-mariadb-tests.yml +++ b/.github/workflows/patch-mariadb-tests.yml @@ -45,7 +45,7 @@ jobs: mariadb: image: mariadb:11.3 env: - MARIADB_ROOT_PASSWORD: travis + MARIADB_ROOT_PASSWORD: db_root ports: - 3306:3306 options: --health-cmd="healthcheck.sh --connect --innodb_initialized" --health-interval=5s --health-timeout=2s --health-retries=3 diff --git a/.github/workflows/run-indinvidual-tests.yml b/.github/workflows/run-indinvidual-tests.yml index 2ab1ad42a197..b6f8ddf60ba5 100644 --- a/.github/workflows/run-indinvidual-tests.yml +++ b/.github/workflows/run-indinvidual-tests.yml @@ -62,7 +62,7 @@ jobs: mysql: image: mariadb:11.3 env: - MARIADB_ROOT_PASSWORD: travis + MARIADB_ROOT_PASSWORD: db_root ports: - 3306:3306 options: --health-cmd="healthcheck.sh --connect --innodb_initialized" --health-interval=5s --health-timeout=2s --health-retries=3 diff --git a/.github/workflows/server-tests.yml b/.github/workflows/server-tests.yml index 2b500fe46ed1..463bdbdd6cdd 100644 --- a/.github/workflows/server-tests.yml +++ b/.github/workflows/server-tests.yml @@ -133,7 +133,7 @@ jobs: mariadb: image: mariadb:11.3 env: - MARIADB_ROOT_PASSWORD: travis + MARIADB_ROOT_PASSWORD: db_root ports: - 3306:3306 options: --health-cmd="healthcheck.sh --connect --innodb_initialized" --health-interval=5s --health-timeout=2s --health-retries=3 @@ -141,7 +141,7 @@ jobs: postgres: image: postgres:12.4 env: - POSTGRES_PASSWORD: travis + POSTGRES_PASSWORD: db_root options: >- --health-cmd pg_isready --health-interval 10s @@ -231,7 +231,10 @@ jobs: DB: ${{ matrix.db }} - name: Run Tests - run: ../env/bin/python3 ../apps/frappe/.github/helper/ci.py + run: | + bench --site test_site \ + run-parallel-tests --app frappe \ + --total-builds 2 --build-number ${{ matrix.container }} working-directory: /home/runner/frappe-bench/sites env: SITE: test_site diff --git a/.github/workflows/tests-base.yml b/.github/workflows/tests-base.yml new file mode 100644 index 000000000000..d4faa8fe9e73 --- /dev/null +++ b/.github/workflows/tests-base.yml @@ -0,0 +1,192 @@ +name: Tests Base +on: + workflow_call: + inputs: + fake-success: + required: false + type: boolean + default: false + python-version: + required: false + type: string + default: '3.12' + node-version: + required: false + type: number + default: 20 + parallel-runs: + required: false + type: number + default: 2 + build-assets: + required: false + type: boolean + default: true + enable-postgres: + required: false + type: boolean + default: false + enable-coverage: + required: false + type: boolean + default: false + enable-watch: + required: false + type: boolean + default: false + enable-schedule: + required: false + type: boolean + default: false + disable-socketio: + required: false + type: boolean + default: false + disable-redis-socketio: + required: false + type: boolean + default: false + + +jobs: + unit-test: + name: Unit + runs-on: ubuntu-latest + steps: + - id: placeholder + run: | + echo "Evolution towards a set of (fast) unit tests which run without a DB connection is being planned" + gen-idx-integration: + needs: unit-test + name: Gen Integration Test Matrix + runs-on: ubuntu-latest + outputs: + indices: ${{ steps.set-indices.outputs.indices }} + steps: + - id: set-indices + run: | + indices=$(seq -s ',' 1 ${{ inputs.parallel-runs }}); echo "indices=[${indices}]" >> $GITHUB_OUTPUT + + # This satisfies the required checks on pull requests if tests are intentionally skipped + integration-test-fake: + needs: gen-idx-integration + name: Integration + runs-on: ubuntu-latest + if: ${{ inputs.fake-success != false }} + strategy: + matrix: + db: ${{ fromJson(inputs.enable-postgres && '["mariadb", "postgres"]' || '["mariadb"]') }} + index: ${{ fromJson(needs.gen-idx-integration.outputs.indices) }} + steps: + - name: Pass skipped tests unconditionally + run: "echo Skipped" + + integration-test: + needs: gen-idx-integration + name: Integration + runs-on: ubuntu-latest + if: ${{ inputs.fake-success == false }} + timeout-minutes: 30 + env: + NODE_ENV: "production" + PYTHONOPTIMIZE: 2 + # noisy 3rd party library warnings + PYTHONWARNINGS: "module,ignore:::babel.messages.extract" + DB_ROOT_PASSWORD: db_root + COVERAGE_RCFILE: ~/frappe-bench/apps/frappe/.coveragerc + + strategy: + fail-fast: false + matrix: + db: ${{ fromJson(inputs.enable-postgres && '["mariadb", "postgres"]' || '["mariadb"]') }} + index: ${{ fromJson(needs.gen-idx-integration.outputs.indices) }} + services: + mariadb: + image: mariadb:11.3 + ports: + - 3306:3306 + options: --health-cmd="healthcheck.sh --connect --innodb_initialized" --health-interval=5s --health-timeout=2s --health-retries=3 + env: + MARIADB_ROOT_PASSWORD: ${{ env.DB_ROOT_PASSWORD }} + postgres: + image: postgres:12.4 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + env: + POSTGRES_PASSWORD: ${{ env.DB_ROOT_PASSWORD }} + smtp_server: + image: rnwood/smtp4dev + ports: + - 2525:25 + - 3000:80 + steps: + - uses: frappe/frappe/.github/actions/setup@develop + name: Environment Setup + with: + python-version: '3.12' + node-version: 20 + disable-socketio: true + db-root-password: ${{ env.DB_ROOT_PASSWORD }} + db: ${{ matrix.db }} + env: + PYTHONWARNINGS: "ignore:Unimplemented abstract methods {'locate_file'}:DeprecationWarning" + + - name: Run Tests + run: | + cd ~/frappe-bench || exit + bench --site test_site \ + run-parallel-tests \ + --app "${{ github.event.repository.name }}" \ + --total-builds ${{ inputs.parallel-runs }} \ + --build-number ${{ matrix.index }} 2> >(tee -a stderr.log >&2) + + # Process warnings and create annotations + if [ -s stderr.log ] && [ "$DB" == "mariadb" ]; then + echo "Processing deprecation warnings..." + grep -E "DeprecationWarning" stderr.log | sort -u | while read -r warning; do + # Extract file path, line number, and warning type + file_info=$(echo "$warning" | grep -oP '^.*?:\d+:') + file_path=$(echo "$file_info" | cut -d':' -f1) + line_number=$(echo "$file_info" | cut -d':' -f2) + warning_type=$(echo "$warning" | grep -oP '\w+Warning') + + # Extract the actual warning message + message=$(echo "$warning" | sed -E "s/^.*$warning_type: //") + + # Create the annotation + echo "::warning file=${file_path},line=${line_number}::${warning_type}: ${message}" + done + else + echo "No deprecation warnings found." + fi + env: + DB: ${{ matrix.db }} + # consumed by bench run-parallel-tests + CAPTURE_COVERAGE: ${{ inputs.enable-coverage }} + + - name: Upload coverage data + uses: actions/upload-artifact@v3 + if: inputs.enable-coverage + with: + name: coverage-${{ matrix.db }}-${{ matrix.index }} + path: ~/frappe-bench/sites/*-coverage*.xml + + - name: Setup tmate session + uses: mxschmitt/action-tmate@v3 + if: ${{ failure() && contains( github.event.pull_request.labels.*.name, 'debug-gha') }} + + - name: Show bench output + if: ${{ always() }} + run: | + cd ~/frappe-bench || exit + cat bench_start.log || true + cd logs + for f in ./*.log*; do + echo "Printing log: $f"; + cat $f + done diff --git a/.github/workflows/tests-ui-base.yml b/.github/workflows/tests-ui-base.yml new file mode 100644 index 000000000000..384f289dba9c --- /dev/null +++ b/.github/workflows/tests-ui-base.yml @@ -0,0 +1,162 @@ +name: Tests Base +on: + workflow_call: + inputs: + fake-success: + required: false + type: boolean + default: false + python-version: + required: false + type: string + default: '3.12' + node-version: + required: false + type: number + default: 20 + parallel-runs: + required: false + type: number + default: 2 + enable-coverage: + required: false + type: boolean + default: false + + +jobs: + gen-idx-integration: + name: Gen Integration Test Matrix + runs-on: ubuntu-latest + outputs: + indices: ${{ steps.set-indices.outputs.indices }} + steps: + - id: set-indices + run: | + indices=$(seq -s ',' 1 ${{ inputs.parallel-runs }}); echo "indices=[${indices}]" >> $GITHUB_OUTPUT + + # This satisfies the required checks on pull requests if tests are intentionally skipped + ui-test-fake: + needs: gen-idx-integration + name: Integration + runs-on: ubuntu-latest + if: ${{ inputs.fake-success != false }} + strategy: + matrix: + db: ["mariadb"] + index: ${{ fromJson(needs.gen-idx-integration.outputs.indices) }} + steps: + - name: Pass skipped tests unconditionally + run: "echo Skipped" + + ui-test: + needs: gen-idx-integration + name: Integration + runs-on: ubuntu-latest + if: ${{ inputs.fake-success == false }} + timeout-minutes: 30 + env: + NODE_ENV: "production" + PYTHONOPTIMIZE: 2 + # noisy 3rd party library warnings + PYTHONWARNINGS: "ignore" + DB_ROOT_PASSWORD: db_root + COVERAGE_RCFILE: ~/frappe-bench/apps/frappe/.coveragerc + + strategy: + fail-fast: false + matrix: + db: ["mariadb"] + index: ${{ fromJson(needs.gen-idx-integration.outputs.indices) }} + services: + mariadb: + image: mariadb:11.3 + ports: + - 3306:3306 + options: --health-cmd="healthcheck.sh --connect --innodb_initialized" --health-interval=5s --health-timeout=2s --health-retries=3 + env: + MARIADB_ROOT_PASSWORD: ${{ env.DB_ROOT_PASSWORD }} + steps: + - uses: frappe/frappe/.github/actions/setup@develop + name: Environment Setup + with: + python-version: '3.12' + node-version: 20 + build-assets: false + enable-coverage: ${{ inputs.enable-coverage }} + db-root-password: ${{ env.DB_ROOT_PASSWORD }} + db: ${{ matrix.db }} + + - name: Verify yarn.lock + run: | + cd ~/frappe-bench/apps/${{ github.event.repository.name }} + git diff --exit-code yarn.lock + + - name: Cache cypress binary + uses: actions/cache@v4 + with: + path: ~/.cache/Cypress + key: ${{ runner.os }}-cypress + + - name: Instrument Source Code + run: | + cd ~/frappe-bench/apps/${{ github.event.repository.name }} + npx nyc instrument \ + -x '${{ github.event.repository.name }}/public/dist/**' \ + -x '${{ github.event.repository.name }}/public/js/lib/**' \ + -x '**/*.bundle.js' --compact=false --in-place ${{ github.event.repository.name }} + + - name: Build + run: | + cd ~/frappe-bench/ + bench build --apps ${{ github.event.repository.name }} + + - name: Site Setup + run: | + cd ~/frappe-bench/ + bench --site test_site execute frappe.utils.install.complete_setup_wizard + bench --site test_site execute frappe.tests.ui_test_helpers.create_test_user + + - name: Run Tests + run: | + cd ~/frappe-bench/ + bench --site test_site \ + run-ui-tests ${{ github.event.repository.name }} \ + --with-coverage \ + --headless \ + --parallel \ + --ci-build-id $GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT + env: + CYPRESS_RECORD_KEY: 4a48f41c-11b3-425b-aa88-c58048fa69eb + + - name: Stop server and wait for coverage file + if: inputs.enable-coverage + run: | + ps -ef | grep "[f]rappe serve" | awk '{print $2}' | xargs kill -s SIGINT + sleep 5 + ( tail -f ~/frappe-bench/sites/*-coverage*.xml & ) | grep -q "\/coverage" + + - name: Upload JS coverage data + uses: actions/upload-artifact@v3 + if: inputs.enable-coverage + with: + name: coverage-js-${{ matrix.index }} + path: ~/frappe-bench/apps/${{ github.event.repository.name }}/.cypress-coverage/clover.xml + + - name: Upload coverage data + uses: actions/upload-artifact@v3 + if: inputs.enable-coverage + with: + name: coverage-py-${{ matrix.index }} + path: ~/frappe-bench/sites/*-coverage*.xml + + - name: Show bench output + if: ${{ always() }} + run: | + cd ~/frappe-bench || exit + cat bench_start.log || true + cd logs + for f in ./*.log*; do + echo "Printing log: $f"; + cat $f + done diff --git a/.github/workflows/type-check-base.yml b/.github/workflows/type-check-base.yml new file mode 100644 index 000000000000..fd1690b7d2a7 --- /dev/null +++ b/.github/workflows/type-check-base.yml @@ -0,0 +1,81 @@ +name: Type Check Base +on: + workflow_call: + inputs: + python-version: + required: false + type: string + default: '3.12.6' + +jobs: + typecheck: + name: Check + runs-on: ubuntu-latest + steps: + - run: npm install toml + - name: Get pyproject.toml + uses: actions/github-script@v7 + id: get-pyproject + with: + github-token: ${{secrets.GITHUB_TOKEN}} + script: | + const { data: pyprojectContent } = await github.rest.repos.getContent({ + owner: context.repo.owner, + repo: context.repo.repo, + path: 'pyproject.toml', + ref: context.payload.pull_request.head.sha + }); + const content = Buffer.from(pyprojectContent.content, 'base64').toString(); + const toml = require('toml'); + const parsed = toml.parse(content); + const mypyFiles = parsed.tool.mypy.files; + return { mypyFiles, content }; + + - name: Check for changes in mypy files + uses: actions/github-script@v7 + id: check-changes + with: + github-token: ${{secrets.GITHUB_TOKEN}} + script: | + const { mypyFiles } = ${{ steps.get-pyproject.outputs.result }}; + const { data: changedFiles } = await github.rest.pulls.listFiles({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.payload.pull_request.number + }); + const changedMypyFiles = changedFiles + .filter(file => mypyFiles.includes(file.filename)) + .map(file => file.filename); + return changedMypyFiles.length > 0; + + - name: Set up Python + if: steps.check-changes.outputs.result == 'true' + uses: actions/setup-python@v5 + with: + python-version: ${{ inputs.python-version }} + + - uses: actions/checkout@v4 + if: steps.check-changes.outputs.result == 'true' + + - name: Cache pip + uses: actions/cache@v4 + if: steps.check-changes.outputs.result == 'true' + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-typecheck-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/setup.py') }} + restore-keys: | + ${{ runner.os }}-pip-typecheck- + ${{ runner.os }}-pip- + ${{ runner.os }}- + + - name: Install dependencies + if: steps.check-changes.outputs.result == 'true' + run: | + python -m pip install --upgrade pip + pip install -e .[dev,test] + + - name: Run mypy + if: steps.check-changes.outputs.result == 'true' + run: | + mypy --version + mypy diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml index ccc5a3d171e8..328f3d3b8e1b 100644 --- a/.github/workflows/ui-tests.yml +++ b/.github/workflows/ui-tests.yml @@ -57,7 +57,7 @@ jobs: mariadb: image: mariadb:11.3 env: - MARIADB_ROOT_PASSWORD: travis + MARIADB_ROOT_PASSWORD: db_root ports: - 3306:3306 options: --health-cmd="healthcheck.sh --connect --innodb_initialized" --health-interval=5s --health-timeout=2s --health-retries=3 diff --git a/frappe/commands/testing.py b/frappe/commands/testing.py index d487c012421f..3dd381bc8674 100644 --- a/frappe/commands/testing.py +++ b/frappe/commands/testing.py @@ -361,12 +361,12 @@ def run_parallel_tests( from frappe.coverage import CodeCoverage - with CodeCoverage(with_coverage, app): + with CodeCoverage(with_coverage, app) as cc: site = get_site(context) if use_orchestrator: from frappe.parallel_test_runner import ParallelTestWithOrchestrator - ParallelTestWithOrchestrator(app, site=site) + runner = ParallelTestWithOrchestrator(app, site=site) else: from frappe.parallel_test_runner import ParallelTestRunner @@ -377,7 +377,25 @@ def run_parallel_tests( total_builds=total_builds, dry_run=dry_run, ) - runner.setup_and_run() + mode = "Orchestrator" if use_orchestrator else "Parallel" + banner = f""" + ╔════════════════════════════════════════════╗ + ║ Parallel Test Runner Execution Summary ║ + ╠════════════════════════════════════════════╣ + ║ Mode: {mode:<26} ║ + ║ App: {app:<26} ║ + ║ Site: {site:<26} ║ + ║ Build Number: {build_number:<26} ║ + ║ Total Builds: {total_builds:<26} ║ + ║ Tests in Build: ~{runner.total_tests:<25} ║""" + if cc.with_coverage: + banner += """ + ║ Coverage Rep.: {cc.outfile:<26} ║""" + banner += """ + ╚════════════════════════════════════════════╝ + """ + print(banner) + runner.setup_and_run() @click.command( diff --git a/frappe/coverage.py b/frappe/coverage.py index b1e5a96249a7..41253eed1845 100644 --- a/frappe/coverage.py +++ b/frappe/coverage.py @@ -1,10 +1,10 @@ # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors # MIT License. See LICENSE """ - frappe.coverage - ~~~~~~~~~~~~~~~~ +frappe.coverage +~~~~~~~~~~~~~~~~ - Coverage settings for frappe +Coverage settings for frappe """ STANDARD_INCLUSIONS = ["*.py"] @@ -27,6 +27,9 @@ # tested via commands' test suite TESTED_VIA_CLI = [ "*/frappe/installer.py", + "*/frappe/utils/install.py", + "*/frappe/utils/scheduler.py", + "*/frappe/utils/doctor.py", "*/frappe/build.py", "*/frappe/database/__init__.py", "*/frappe/database/db_manager.py", @@ -38,6 +41,7 @@ "*/commands/*", "*/frappe/change_log/*", "*/frappe/exceptions*", + "*/frappe/desk/page/setup_wizard/setup_wizard.py", "*/frappe/coverage.py", "*frappe/setup.py", "*/doctype/*/*_dashboard.py", @@ -47,9 +51,17 @@ class CodeCoverage: - def __init__(self, with_coverage, app): + """ + Context manager for handling code coverage. + + This class sets up code coverage measurement for a specific app, + applying the appropriate inclusion and exclusion patterns. + """ + + def __init__(self, with_coverage, app, outfile="coverage.xml"): self.with_coverage = with_coverage self.app = app or "frappe" + self.outfile = outfile def __enter__(self): if self.with_coverage: @@ -68,10 +80,11 @@ def __enter__(self): self.coverage = Coverage(source=[source_path], omit=omit, include=STANDARD_INCLUSIONS) self.coverage.start() + return self def __exit__(self, exc_type, exc_value, traceback): if self.with_coverage: self.coverage.stop() self.coverage.save() - self.coverage.xml_report() + self.coverage.xml_report(outfile=self.outfile) print("Saved Coverage")