182 lines
6.5 KiB
Python
182 lines
6.5 KiB
Python
# Licensed to the Apache Software Foundation (ASF) under one
|
|
# or more contributor license agreements. See the NOTICE file
|
|
# distributed with this work for additional information
|
|
# regarding copyright ownership. The ASF licenses this file
|
|
# to you under the Apache License, Version 2.0 (the
|
|
# "License"); you may not use this file except in compliance
|
|
# with the License. You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing,
|
|
# software distributed under the License is distributed on an
|
|
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
# KIND, either express or implied. See the License for the
|
|
# specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
import argparse
|
|
import os
|
|
import subprocess
|
|
from datetime import datetime
|
|
|
|
XVFB_PRE_CMD = "xvfb-run --auto-servernum --server-args='-screen 0, 1280x1024x24' "
|
|
REPO = os.getenv("GITHUB_REPOSITORY") or "apache/superset"
|
|
GITHUB_EVENT_NAME = os.getenv("GITHUB_EVENT_NAME") or "push"
|
|
CYPRESS_RECORD_KEY = os.getenv("CYPRESS_RECORD_KEY") or ""
|
|
|
|
|
|
def generate_build_id() -> str:
|
|
"""Generates a build ID based on the current timestamp."""
|
|
now = datetime.now()
|
|
rounded_minute = now.minute - (now.minute % 20)
|
|
rounded_time = now.replace(minute=rounded_minute, second=0, microsecond=0)
|
|
return (os.getenv("GITHUB_SHA") or "DUMMY")[:8] + rounded_time.strftime(
|
|
"%Y%m%d%H%M"
|
|
)
|
|
|
|
|
|
def run_cypress_for_test_file(
|
|
test_file: str, retries: int, use_dashboard: bool, group: str, dry_run: bool, i: int
|
|
) -> int:
|
|
"""Runs Cypress for a single test file and retries upon failure."""
|
|
cypress_cmd = "./node_modules/.bin/cypress run"
|
|
os.environ["TERM"] = "xterm"
|
|
os.environ["ELECTRON_DISABLE_GPU"] = "true"
|
|
build_id = generate_build_id()
|
|
browser = os.getenv("CYPRESS_BROWSER", "chrome")
|
|
chrome_flags = "--disable-dev-shm-usage"
|
|
|
|
for attempt in range(retries):
|
|
# Create Cypress command for a single test file
|
|
cmd: str = ""
|
|
if use_dashboard:
|
|
# If/when we want to use cypress' dashboard feature to record the run
|
|
group_id = f"matrix{group}-file{i}-{attempt}"
|
|
cmd = (
|
|
f"{XVFB_PRE_CMD} "
|
|
f'{cypress_cmd} --spec "{test_file}" '
|
|
f"--config numTestsKeptInMemory=0 "
|
|
f"--browser {browser} "
|
|
f"--record --group {group_id} --tag {REPO},{GITHUB_EVENT_NAME} "
|
|
f"--ci-build-id {build_id} "
|
|
f"--wait-for-missing-groups "
|
|
f"-- {chrome_flags}"
|
|
)
|
|
else:
|
|
os.environ.pop("CYPRESS_RECORD_KEY", None)
|
|
cmd = (
|
|
f"{XVFB_PRE_CMD} "
|
|
f"{cypress_cmd} "
|
|
f"--browser {browser} "
|
|
f"--config numTestsKeptInMemory=0 "
|
|
f'--spec "{test_file}" '
|
|
f"-- {chrome_flags}"
|
|
)
|
|
print(f"RUN: {cmd} (Attempt {attempt + 1}/{retries})")
|
|
if dry_run:
|
|
# Print the command instead of executing it
|
|
print(f"DRY RUN: {cmd}")
|
|
return 0
|
|
|
|
process = subprocess.Popen( # noqa: S602
|
|
cmd,
|
|
shell=True,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.STDOUT,
|
|
universal_newlines=True,
|
|
)
|
|
|
|
# Stream stdout in real-time
|
|
if process.stdout:
|
|
for stdout_line in iter(process.stdout.readline, ""):
|
|
print(stdout_line, end="")
|
|
|
|
process.wait()
|
|
|
|
if process.returncode == 0:
|
|
print(f"Test {test_file} succeeded on attempt {attempt + 1}")
|
|
return 0
|
|
else:
|
|
print(f"Test {test_file} failed on attempt {attempt + 1}")
|
|
|
|
print(f"Test {test_file} failed after {retries} retries.")
|
|
return process.returncode
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(
|
|
description="Run Cypress tests with retries per test file"
|
|
)
|
|
parser.add_argument(
|
|
"--use-dashboard",
|
|
action="store_true",
|
|
help="Use Cypress Dashboard for parallelization",
|
|
)
|
|
parser.add_argument(
|
|
"--parallelism", type=int, default=10, help="Number of parallel groups"
|
|
)
|
|
parser.add_argument(
|
|
"--parallelism-id", type=int, required=True, help="ID of the parallelism group"
|
|
)
|
|
parser.add_argument(
|
|
"--filter", type=str, required=False, default=None, help="Filter to test"
|
|
)
|
|
parser.add_argument("--group", type=str, default="Default", help="Group name")
|
|
parser.add_argument(
|
|
"--retries", type=int, default=3, help="Number of retries per test file"
|
|
)
|
|
parser.add_argument(
|
|
"--dry-run",
|
|
action="store_true",
|
|
help="Print the command instead of executing it",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
script_dir = os.path.dirname(os.path.abspath(__file__))
|
|
cypress_base_path = "superset-frontend/cypress-base/"
|
|
cypress_base_full_path = os.path.join(script_dir, "../", cypress_base_path)
|
|
cypress_tests_path = os.path.join(cypress_base_full_path, "cypress/e2e")
|
|
|
|
test_files = []
|
|
file_count = 0
|
|
for root, _, files in os.walk(cypress_tests_path):
|
|
for file in files:
|
|
if file.endswith("test.ts") or file.endswith("test.js"):
|
|
file_count += 1
|
|
test_files.append(
|
|
os.path.join(root, file).replace(cypress_base_full_path, "")
|
|
)
|
|
print(f"Found {file_count} test files.")
|
|
|
|
# Initialize groups for round-robin distribution
|
|
groups: dict[int, list[str]] = {i: [] for i in range(args.parallelism)}
|
|
|
|
# Sort test files to ensure deterministic distribution
|
|
sorted_test_files = sorted(test_files)
|
|
|
|
# Distribute test files in a round-robin manner
|
|
for index, test_file in enumerate(sorted_test_files):
|
|
group_index = index % args.parallelism
|
|
groups[group_index].append(test_file)
|
|
|
|
# Only run tests for the group that matches the parallelism ID
|
|
group_id = args.parallelism_id
|
|
spec_list = groups[group_id]
|
|
|
|
# Run each test file independently with retry logic or dry-run
|
|
processed_file_count: int = 0
|
|
for i, test_file in enumerate(spec_list):
|
|
result = run_cypress_for_test_file(
|
|
test_file, args.retries, args.use_dashboard, args.group, args.dry_run, i
|
|
)
|
|
if result != 0:
|
|
print(f"Exiting due to failure in {test_file}")
|
|
exit(result)
|
|
processed_file_count += 1
|
|
print(f"Ran {processed_file_count} test files successfully.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|