| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432 |
- """
- Workflow module.
- This module handles the main workflow logic for running coverage tests and processing results.
- """
- import os
- import re
- import sys
- import subprocess
- import traceback
- from .extraction import run_coverage, compare_coverage, extract_coverage
- from .github_api import generate_comment, post_comment, set_github_output
- from .util import log, file_exists, get_file_size, list_directory, run_command
- def is_valid_branch_name(branch_name: str) -> bool:
- """
- Validate a git branch name.
-
- Args:
- branch_name: Branch name to validate
-
- Returns:
- True if valid, False otherwise
- """
- # Check for common branch name patterns
- if not re.match(r'^[a-zA-Z0-9_\-./]+$', branch_name):
- return False
-
- # Check for path traversal
- if '..' in branch_name:
- return False
-
- # Check for shell metacharacters
- if re.search(r'[;&|`$]', branch_name):
- return False
-
- return True
- def checkout_branch(branch_name: str) -> None:
- """
- Checkout a branch for testing.
-
- Args:
- branch_name: Branch name to checkout
-
- Raises:
- RuntimeError: If branch checkout fails
- ValueError: If branch name is invalid
- """
- if not is_valid_branch_name(branch_name):
- raise ValueError(f"Invalid branch name: {branch_name}")
-
- log(f"=== Checking out branch: {branch_name} ===")
-
- # Fetch the branch
- returncode, stdout, stderr = run_command(['git', 'fetch', 'origin', branch_name])
- if returncode != 0:
- log(f"ERROR: Failed to fetch branch {branch_name}")
- log(f"Error details: {stderr}")
- raise RuntimeError(f"Git fetch failed: {stderr}")
-
- # Checkout the branch
- returncode, stdout, stderr = run_command(['git', 'checkout', branch_name])
- if returncode != 0:
- log(f"ERROR: Failed to checkout branch {branch_name}")
- log(f"Error details: {stderr}")
- raise RuntimeError(f"Git checkout failed: {stderr}")
-
- log(f"Successfully checked out branch: {branch_name}")
- def extract_extension_coverage_from_file(file_path):
- """Extract extension coverage from file when run_coverage returns 0."""
- if not file_exists(file_path):
- log(f"File {file_path} does not exist, cannot extract extension coverage")
- return 0.0
-
- file_size = get_file_size(file_path)
- if file_size == 0:
- log(f"File {file_path} is empty, cannot extract extension coverage")
- return 0.0
-
- log(f"Extension coverage is 0.0, trying to read from file directly: {file_path} (size: {file_size} bytes)")
- with open(file_path, 'r') as f:
- content = f.read()
- # Extract the percentage from the "Lines" row in the coverage summary
- # Pattern: Lines : xx.xx% ( xxxxxxx/xxxxxxx )
- lines_match = re.search(r'Lines\s*:\s*(\d+\.\d+)%', content)
- if lines_match:
- coverage = float(lines_match.group(1))
- log(f"Found extension coverage in file: {coverage}%")
- return coverage
- return 0.0
- def extract_webview_coverage_from_file(file_path):
- """Extract webview coverage from file when run_coverage returns 0."""
- if not file_exists(file_path):
- log(f"File {file_path} does not exist, cannot extract webview coverage")
- return 0.0
-
- file_size = get_file_size(file_path)
- if file_size == 0:
- log(f"File {file_path} is empty, cannot extract webview coverage")
- return 0.0
-
- log(f"Webview coverage is 0.0, trying to read from file directly: {file_path} (size: {file_size} bytes)")
- with open(file_path, 'r') as f:
- content = f.read()
- # Extract the percentage from the "% Lines" column in the "All files" row
- # Pattern: All files | xx.xx | xx.xx | xx.xx | xx.xx |
- all_files_match = re.search(r'All files\s+\|\s+\d+\.\d+\s+\|\s+\d+\.\d+\s+\|\s+\d+\.\d+\s+\|\s+(\d+\.\d+)', content)
- if all_files_match:
- coverage = float(all_files_match.group(1))
- log(f"Found webview coverage in file: {coverage}%")
- return coverage
- return 0.0
- def run_extension_coverage(branch_name=None):
- """Run extension coverage tests and extract results."""
- prefix = 'base_' if branch_name else ''
- file_path = f"{prefix}extension_coverage.txt"
-
- # Run coverage tests
- ext_cov = run_coverage(
- ["xvfb-run", "-a", "npm", "run", "test:coverage"],
- file_path,
- "extension"
- )
-
- # If coverage is 0.0, try to extract from file directly
- if ext_cov == 0.0:
- ext_cov = extract_extension_coverage_from_file(file_path)
-
- return ext_cov
- def run_webview_coverage(branch_name=None):
- """Run webview coverage tests and extract results."""
- prefix = 'base_' if branch_name else ''
- file_path = f"{prefix}webview_coverage.txt"
-
- # Save current directory
- original_dir = os.getcwd()
-
- try:
- # Change to webview-ui directory
- os.chdir('webview-ui')
-
- # Install coverage dependency
- returncode, stdout, stderr = run_command(["npm", "install", "--no-save", "@vitest/coverage-v8"])
- if returncode != 0:
- log(f"Failed to install coverage dependency: {stderr}")
- return 0.0
-
- # Run coverage tests from webview-ui directory
- web_cov = run_coverage(
- ["npm", "run", "test:coverage"],
- os.path.join('..', file_path),
- "webview"
- )
- finally:
- # Always change back to original directory
- os.chdir(original_dir)
-
- # If coverage is 0.0, try to extract from file directly
- if web_cov == 0.0:
- web_cov = extract_webview_coverage_from_file(file_path)
-
- return web_cov
- def run_branch_coverage(branch_name=None):
- """
- Run coverage tests for a branch.
-
- Args:
- branch_name: Name of the branch to checkout before running tests (optional)
-
- Returns:
- Tuple of (extension_coverage, webview_coverage)
- """
- # Checkout branch if specified
- if branch_name:
- checkout_branch(branch_name)
-
- # Run coverage tests
- log(f"=== Running coverage tests{' for ' + branch_name if branch_name else ''} ===")
-
- # Run extension and webview coverage
- ext_cov = run_extension_coverage(branch_name)
- web_cov = run_webview_coverage(branch_name)
-
- return ext_cov, web_cov
- def find_potential_coverage_files():
- """Find potential coverage files in the current directory and webview-ui."""
- log("Searching for potential coverage files...")
-
- # Find files in current directory
- current_dir_files = list_directory('.')
- for name, size in current_dir_files:
- if 'coverage' in name.lower() and size != "DIR":
- log(f"Found potential coverage file: {name} (size: {size} bytes)")
-
- # Find files in webview-ui directory
- if os.path.exists('webview-ui') and os.path.isdir('webview-ui'):
- webview_files = list_directory('webview-ui')
- for name, size in webview_files:
- if 'coverage' in name.lower() and size != "DIR":
- log(f"Found potential webview coverage file: webview-ui/{name} (size: {size} bytes)")
- else:
- log("webview-ui directory not found")
- def generate_warnings(base_ext_cov, pr_ext_cov, ext_decreased, ext_diff,
- base_web_cov, pr_web_cov, web_decreased, web_diff):
- """Generate warnings for coverage decreases."""
- if not (ext_decreased or web_decreased):
- return []
-
- warnings = [
- "Test coverage has decreased in this PR",
- f"Extension coverage: {base_ext_cov}% -> {pr_ext_cov}% (Diff: {ext_diff}%)",
- f"Webview coverage: {base_web_cov}% -> {pr_web_cov}% (Diff: {web_diff}%)"
- ]
-
- # Additional warning for significant decrease (more than 1%)
- if ext_decreased and ext_diff > 1.0:
- warnings.append(f"Extension coverage decreased by more than 1% ({ext_diff}%). Consider adding tests to cover your changes.")
-
- if web_decreased and web_diff > 1.0:
- warnings.append(f"Webview coverage decreased by more than 1% ({web_diff}%). Consider adding tests to cover your changes.")
-
- return warnings
- def output_warnings(warnings):
- """Output warnings to GitHub step summary and console."""
- if not warnings:
- return
-
- # Get the GitHub step summary file path from environment variable
- github_step_summary = os.environ.get('GITHUB_STEP_SUMMARY')
-
- # Write to GitHub step summary if available
- if github_step_summary:
- with open(github_step_summary, 'a') as f:
- f.write("## Coverage Warnings\n\n")
- for warning in warnings:
- f.write(f"⚠️ {warning}\n\n")
-
- # Also output to console with ::warning:: syntax for backward compatibility
- for warning in warnings:
- log(f"::warning::{warning}")
- def output_github_results(pr_ext_cov, pr_web_cov, base_ext_cov, base_web_cov,
- ext_decreased, ext_diff, web_decreased, web_diff):
- """Output results for GitHub Actions."""
- set_github_output("pr_extension_coverage", pr_ext_cov)
- set_github_output("pr_webview_coverage", pr_web_cov)
- set_github_output("base_extension_coverage", base_ext_cov)
- set_github_output("base_webview_coverage", base_web_cov)
- set_github_output("extension_decreased", str(ext_decreased).lower())
- set_github_output("extension_diff", ext_diff)
- set_github_output("webview_decreased", str(web_decreased).lower())
- set_github_output("webview_diff", web_diff)
- def extract_pr_coverage_from_artifacts():
- """
- Extract PR branch coverage from artifact files.
-
- Returns:
- Tuple of (extension_coverage, webview_coverage)
-
- Raises:
- SystemExit: If the coverage files don't exist
- """
- log("=== Extracting PR branch coverage from artifacts ===")
-
- # Check if the coverage files exist
- ext_file_path = "extension_coverage.txt"
- web_file_path = "webview-ui/webview_coverage.txt"
-
- # Extract extension coverage
- log(f"Extracting extension coverage from {ext_file_path}")
- if not file_exists(ext_file_path):
- error_msg = f"ERROR: PR extension coverage file {ext_file_path} not found"
- log(error_msg)
-
- # List directory contents for debugging
- log("Current directory contents:")
- try:
- dir_contents = list_directory('.')
- for name, size in dir_contents:
- log(f" {name} - {size}\n")
- except Exception as e:
- log(f"Error listing directory: {e}")
-
- sys.exit(1) # Exit with error code to fail the workflow
-
- ext_cov = extract_extension_coverage_from_file(ext_file_path)
- log(f"PR extension coverage from artifact: {ext_cov}%")
-
- # Extract webview coverage
- log(f"Extracting webview coverage from {web_file_path}")
- if not file_exists(web_file_path):
- error_msg = f"ERROR: PR webview coverage file {web_file_path} not found"
- log(error_msg)
-
- # Check if the webview-ui directory exists
- if not os.path.exists('webview-ui'):
- log("ERROR: webview-ui directory not found")
- else:
- # List webview-ui directory contents for debugging
- log("webview-ui directory contents:")
- try:
- dir_contents = list_directory('webview-ui')
- for name, size in dir_contents:
- log(f" {name} - {size}")
- except Exception as e:
- log(f"Error listing directory: {e}")
-
- sys.exit(1) # Exit with error code to fail the workflow
-
- web_cov = extract_webview_coverage_from_file(web_file_path)
- log(f"PR webview coverage from artifact: {web_cov}%")
-
- return ext_cov, web_cov
- def process_coverage_workflow(args):
- """
- Process the entire coverage workflow.
-
- Args:
- args: Command line arguments
- """
- # Initialize all variables at the start
- pr_ext_cov = 0.0
- pr_web_cov = 0.0
- base_ext_cov = 0.0
- base_web_cov = 0.0
- ext_decreased = False
- ext_diff = 0.0
- web_decreased = False
- web_diff = 0.0
-
- try:
- # Validate branch name
- if not is_valid_branch_name(args.base_branch):
- raise ValueError(f"Invalid base branch name: {args.base_branch}")
-
- # Check if we're running in GitHub Actions
- is_github_actions = 'GITHUB_ACTIONS' in os.environ
- if is_github_actions:
- log("Running in GitHub Actions environment")
-
- # Extract PR branch coverage from artifacts (from test job)
- pr_ext_cov, pr_web_cov = extract_pr_coverage_from_artifacts()
-
- # Verify PR coverage values
- if pr_ext_cov == 0.0:
- log("WARNING: PR extension coverage is 0.0, this may indicate an issue with the coverage report")
- find_potential_coverage_files()
-
- if pr_web_cov == 0.0:
- log("WARNING: PR webview coverage is 0.0, this may indicate an issue with the coverage report")
- find_potential_coverage_files()
-
- # Run base branch coverage
- log(f"=== Running base branch coverage for {args.base_branch} ===")
- base_ext_cov, base_web_cov = run_branch_coverage(args.base_branch)
-
- # Verify base coverage values
- if base_ext_cov == 0.0:
- log("WARNING: Base extension coverage is 0.0, this may indicate an issue with the coverage report")
-
- if base_web_cov == 0.0:
- log("WARNING: Base webview coverage is 0.0, this may indicate an issue with the coverage report")
-
- # Compare coverage
- log("=== Comparing extension coverage ===")
- ext_decreased, ext_diff = compare_coverage(base_ext_cov, pr_ext_cov)
-
- log("=== Comparing webview coverage ===")
- web_decreased, web_diff = compare_coverage(base_web_cov, pr_web_cov)
-
- # Print summary of coverage values
- log("\n=== Coverage Summary ===")
- log(f"PR extension coverage: {pr_ext_cov}%")
- log(f"Base extension coverage: {base_ext_cov}%")
- log(f"Extension coverage change: {'+' if not ext_decreased else '-'}{ext_diff}%")
- log(f"PR webview coverage: {pr_web_cov}%")
- log(f"Base webview coverage: {base_web_cov}%")
- log(f"Webview coverage change: {'+' if not web_decreased else '-'}{web_diff}%")
-
- # Generate and output warnings
- warnings = generate_warnings(
- base_ext_cov, pr_ext_cov, ext_decreased, ext_diff,
- base_web_cov, pr_web_cov, web_decreased, web_diff
- )
- output_warnings(warnings)
-
- # Generate comment
- log("=== Generating comment ===")
- comment = generate_comment(
- base_ext_cov, pr_ext_cov, str(ext_decreased).lower(), ext_diff,
- base_web_cov, pr_web_cov, str(web_decreased).lower(), web_diff
- )
-
- # Save comment to file
- with open("coverage_comment.md", "w") as f:
- f.write(comment)
-
- # Post comment if PR number is provided
- if args.pr_number:
- log(f"=== Posting comment to PR #{args.pr_number} ===")
- post_comment("coverage_comment.md", args.pr_number, args.repo, args.token)
-
- # Output results for GitHub Actions
- output_github_results(
- pr_ext_cov, pr_web_cov, base_ext_cov, base_web_cov,
- ext_decreased, ext_diff, web_decreased, web_diff
- )
-
- except Exception as e:
- log(f"ERROR in process_coverage_workflow: {e}")
- traceback.print_exc()
-
- # Try to output results even if there was an error
- try:
- output_github_results(
- pr_ext_cov, pr_web_cov, base_ext_cov, base_web_cov,
- ext_decreased, ext_diff, web_decreased, web_diff
- )
- except Exception as e2:
- log(f"ERROR outputting GitHub results: {e2}")
|