diff --git a/test/renderdiff/README.md b/test/renderdiff/README.md index e4b84762f5..7bcd5ae3cf 100644 --- a/test/renderdiff/README.md +++ b/test/renderdiff/README.md @@ -138,6 +138,17 @@ python3 test/renderdiff/src/viewer.py --pr_number=[PR #] --github_token=[github where `[PR #]` is the numeric ID of your pull request, and the `[github token]` is an acess token that you (as a github user) needs to generate ([reference][github_token_ref]). +To see the results of a specific run, you would do the following + +``` +python3 test/renderdiff/src/viewer.py --run_number=[RUN #] --github_token=[github token] +``` + +where `[RUN #]` is the numeric ID of the run. You can find the run number in the URL of the +GitHub Actions page. For example, in the URL +`https://github.com/google/filament/actions/runs/18023632663/job/51286323708?pr=9264`, +the run number is `18023632663`. + [github_token_ref]: https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens [Mesa]: https://docs.mesa3d.org [SwiftShader]: https://github.com/google/swiftshader diff --git a/test/renderdiff/src/viewer.py b/test/renderdiff/src/viewer.py index 2e22d8fe3f..d3b8a5d04f 100644 --- a/test/renderdiff/src/viewer.py +++ b/test/renderdiff/src/viewer.py @@ -28,27 +28,67 @@ from flask import Flask, request, make_response, send_from_directory DIR = pathlib.Path(__file__).parent.absolute() HTML_DIR = os.path.join(DIR, "viewer_html") +def _download_and_extract_artifacts(run_id, headers, output_dir): + OWNER_REPO = 'google/filament' + artifacts_url = f"https://api.github.com/repos/{OWNER_REPO}/actions/runs/{run_id}/artifacts" + downloaded_any_artifact = False + try: + response = requests.get(artifacts_url, headers=headers) + response.raise_for_status() + artifacts_data = response.json() + artifacts = artifacts_data.get("artifacts", []) + + if not artifacts: + print(f" No artifacts found for workflow run ID {run_id}.") + return downloaded_any_artifact + + for artifact in artifacts: + artifact_id = artifact["id"] + artifact_name = artifact["name"] + archive_download_url = artifact["archive_download_url"] + + print(f" Found artifact: '{artifact_name}' (ID: {artifact_id})") + + # Perform the download request + print(f" Downloading '{artifact_name}'...") + # Use a copy of headers and specific Accept for ZIP download + download_headers = headers.copy() + download_headers["Accept"] = "application/vnd.github.v3+zip" + download_response = requests.get(archive_download_url, headers=download_headers, stream=True) + download_response.raise_for_status() # Check for errors in download + + # --- Step 5: Extract the contents --- + # Use BytesIO to handle the zip file content in memory without saving to a temporary file + with io.BytesIO(download_response.content) as zip_buffer: + try: + with zipfile.ZipFile(zip_buffer, 'r') as zip_ref: + # Create a unique subdirectory for each artifact to avoid file name conflicts + extract_path = os.path.join(output_dir, f"{artifact_name}_{artifact_id}") + os.makedirs(extract_path, exist_ok=True) + zip_ref.extractall(extract_path) + print(f" Successfully extracted '{artifact_name}' to '{extract_path}/'") + downloaded_any_artifact = True + except zipfile.BadZipFile: + print(f" Error: Downloaded file for '{artifact_name}' is not a valid zip file. Skipping extraction.") + except Exception as e: + print(f" An error occurred during extraction of '{artifact_name}': {e}") + return downloaded_any_artifact + except requests.exceptions.HTTPError as e: + print(f" An HTTP error occurred while fetching artifacts for run {run_id}: {e}") + if e.response.status_code == 403: + print(" This often means you need a GitHub Personal Access Token with 'repo' scope (even for public repos for artifact downloads).") + except requests.exceptions.RequestException as e: + print(f" A network error occurred while fetching artifacts for run {run_id}: {e}") + +def _download_github_artifacts_by_run(run_id, github_token, output_dir= ".") -> None: + headers = {"Accept": "application/vnd.github.v3+json"} + if github_token: + headers["Authorization"] = f"token {github_token}" + os.makedirs(output_dir, exist_ok=True) + return _download_and_extract_artifacts(run_id, headers, output_dir) + # Generated by gemini def _download_github_artifacts(pr_number, github_token, output_dir= ".") -> None: - """ - Downloads artifacts associated with a specific GitHub Pull Request. - - This function performs the following steps: - 1. Fetches the details of the Pull Request to get its head commit SHA. - 2. Searches for GitHub Actions workflow runs triggered by that specific commit. - 3. Iterates through successful workflow runs to find and list all associated artifacts. - 4. Downloads each artifact (which comes as a ZIP file). - 5. Extracts the contents of each downloaded ZIP file into a unique subdirectory - within the specified output directory. - - Args: - owner (str): The GitHub repository owner (e.g., "octocat"). - repo (str): The GitHub repository name (e.g., "Spoon-Knife"). - pr_number (int): The Pull Request number. - output_dir (str): The local directory where downloaded artifacts will be saved. - Defaults to the current directory. - """ - # Prepare HTTP headers for GitHub API requests headers = {"Accept": "application/vnd.github.v3+json"} if github_token: @@ -119,60 +159,9 @@ def _download_github_artifacts(pr_number, github_token, output_dir= ".") -> None run_id = run["id"] run_name = run["name"] print(f"\nProcessing workflow run '{run_name}' (ID: {run_id})...") - - artifacts_url = f"https://api.github.com/repos/{OWNER_REPO}/actions/runs/{run_id}/artifacts" - try: - response = requests.get(artifacts_url, headers=headers) - response.raise_for_status() - artifacts_data = response.json() - artifacts = artifacts_data.get("artifacts", []) - - if not artifacts: - print(f" No artifacts found for workflow run ID {run_id}.") - continue # Move to the next workflow run - - for artifact in artifacts: - artifact_id = artifact["id"] - artifact_name = artifact["name"] - archive_download_url = artifact["archive_download_url"] - - print(f" Found artifact: '{artifact_name}' (ID: {artifact_id})") - - # Perform the download request - print(f" Downloading '{artifact_name}'...") - # Use a copy of headers and specific Accept for ZIP download - download_headers = headers.copy() - download_headers["Accept"] = "application/vnd.github.v3+zip" - download_response = requests.get(archive_download_url, headers=download_headers, stream=True) - download_response.raise_for_status() # Check for errors in download - - # --- Step 5: Extract the contents --- - # Use BytesIO to handle the zip file content in memory without saving to a temporary file - with io.BytesIO(download_response.content) as zip_buffer: - try: - with zipfile.ZipFile(zip_buffer, 'r') as zip_ref: - # Create a unique subdirectory for each artifact to avoid file name conflicts - extract_path = os.path.join(output_dir, f"{artifact_name}_{artifact_id}") - os.makedirs(extract_path, exist_ok=True) - zip_ref.extractall(extract_path) - print(f" Successfully extracted '{artifact_name}' to '{extract_path}/'") - downloaded_any_artifact = True - except zipfile.BadZipFile: - print(f" Error: Downloaded file for '{artifact_name}' is not a valid zip file. Skipping extraction.") - except Exception as e: - print(f" An error occurred during extraction of '{artifact_name}': {e}") - - # Once we find the lastest run with artifacts, we just quit - if len(artifacts) > 0: + downloaded_any_artifact = _download_and_extract_artifacts(run_id, headers, output_dir) + if downloaded_any_artifact: break - except requests.exceptions.HTTPError as e: - print(f" An HTTP error occurred while fetching artifacts for run {run_id}: {e}") - if e.response.status_code == 403: - print(" This often means you need a GitHub Personal Access Token with 'repo' scope (even for public repos for artifact downloads).") - continue # Continue processing the next workflow run - except requests.exceptions.RequestException as e: - print(f" A network error occurred while fetching artifacts for run {run_id}: {e}") - continue # Continue processing the next workflow run if not downloaded_any_artifact: print("\nNo artifacts were downloaded for the specified PR.") @@ -225,15 +214,20 @@ if __name__ == '__main__': parser = ArgParseImpl() parser.add_argument('--diff', type=str, help='Diff result directory') parser.add_argument('--pr_number', type=str, help='Pull request artifacts to examine') + parser.add_argument('--run_number', type=str, help='Run number to examine') parser.add_argument('--github_token', type=str, help='Necessary for pull PR artifacts') args, _ = parser.parse_known_args(sys.argv[1:]) - if not args.diff and not args.pr_number: - print('Need to specify either a diff result directory or a Pull Request number') + if not args.diff and not args.pr_number and not args.run_number: + print('Need to specify either a diff result directory, a Pull Request number, or a run number') exit(1) - if args.diff and args.pr_number: - print('Cannot specify both a diff result directory and a Pull Request number') + if args.diff and (args.pr_number or args.run_number): + print('Cannot specify both a diff result directory and a Pull Request/run number') + exit(1) + + if args.pr_number and args.run_number: + print('Cannot specify both a PR number and a run number') exit(1) fdir = args.diff @@ -251,6 +245,20 @@ if __name__ == '__main__': directory_name = list(os.listdir(output_dir))[0] fdir = os.path.join(os.path.join(output_dir, directory_name), 'diffs/presubmit') + if args.run_number: + if not args.github_token: + print('Must provide --github_token to be able to download artifacts') + exit(1) + output_dir = f'/tmp/filament-run{args.run_number}-rdiff-result' + res = _download_github_artifacts_by_run(args.run_number, args.github_token, output_dir) + if not res: + print('Failed to retrieve run artifacts') + exit(1) + + # TODO: Clean up the following so that we're not so specific on the paths diffs/presubmit + directory_name = list(os.listdir(output_dir))[0] + fdir = os.path.join(os.path.join(output_dir, directory_name), 'diffs/presubmit') + with open(os.path.join(fdir, 'compare_results.json'), 'r') as f: config = json.loads(f.read()) config['diff_dir'] = os.path.abspath(fdir)