Skip to content
Closed
Changes from 6 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
152 changes: 152 additions & 0 deletions tests/integration/run_benchmark.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import logging

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Expose the benchmark runner from the package

This runner is added only under tests/integration, while the package configuration includes only osa_tool and a repo-wide search shows no osa_tool.run_benchmark wrapper or console entry point. In environments that invoke benchmark runners as package modules, as with the existing osa_tool.run_multi_process, this module will not be importable from an installed package and the benchmark will fail before processing any repositories.

Useful? React with 👍 / 👎.

import os
import shutil
import sys
import time
from concurrent.futures import ProcessPoolExecutor, as_completed

import pandas as pd
from pandas import DataFrame

from osa_tool.config.settings import ConfigManager
from osa_tool.core.git.git_agent import GitHubAgent, GitLabAgent, GitverseAgent
from osa_tool.core.git.metadata import RepositoryMetadata
from osa_tool.operations.docs.readme_generation.readme_agent import ReadmeAgent
from osa_tool.tools.repository_analysis.sourcerank import SourceRank
from osa_tool.utils.arguments_parser import build_parser_from_yaml
from osa_tool.utils.logger import logger
from osa_tool.utils.utils import delete_repository, format_time, parse_git_url, rich_section


def generate_readme(config_manager: ConfigManager, metadata: RepositoryMetadata, args) -> None:
readmes_dir = os.path.join(os.path.dirname(args.table_path), "readmes")
os.makedirs(readmes_dir, exist_ok=True)

readme_agent = ReadmeAgent(
config_manager=config_manager,
metadata=metadata,
)

dest_path = os.path.join(readmes_dir, f"{metadata.name}_README.md")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use collision-free README output paths

When the input table contains two repositories with the same project basename, for example different owners both named tool, both workers write to the same readmes/<name>_README.md path and the later copy overwrites the earlier result. That corrupts benchmark outputs while both rows can still be marked successful; include the owner/full repository name or another per-row identifier in this filename.

Useful? React with 👍 / 👎.

readme_agent.file_to_save = dest_path

readme_agent.generate_readme()

src = os.path.join(readme_agent.repo_path, "README.md")
if os.path.isfile(src):
shutil.copy2(src, dest_path)
else:
logger.warning(f"README not found at clone path after generation: {src}")


def process_repository(repo_url: str, args) -> dict:
stage_start = time.time()

repos_dir = os.path.join(os.path.dirname(args.table_path), "repositories")
logs_dir = os.path.join(os.path.dirname(args.table_path), "logs")
os.makedirs(logs_dir, exist_ok=True)
os.makedirs(repos_dir, exist_ok=True)

original_cwd = os.getcwd()
os.chdir(repos_dir)

_, _, repo_name, _ = parse_git_url(repo_url)
log_file = os.path.join(logs_dir, f"{repo_name}.log")

logger.setLevel(logging.DEBUG)
file_handler = logging.FileHandler(log_file, encoding="utf-8")
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s"))
logger.addHandler(file_handler)

result = {"repository": repo_url, "name": repo_name, "status": "Failed"}

try:
args.repository = repo_url
config_manager = ConfigManager(args)

if not hasattr(config_manager.config, 'git'):
config_manager.config.git = type('obj', (object,), {'repository': repo_url})
else:
config_manager.config.git.repository = repo_url

if "github.com" in repo_url:
git_agent = GitHubAgent(repo_url)
elif "gitlab" in repo_url:
git_agent = GitLabAgent(repo_url)
elif "gitverse.ru" in repo_url:
git_agent = GitverseAgent(repo_url)
else:
logger.error(f"Unsupported GIT platform: {repo_url}")
return result

git_agent.clone_repository()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Isolate clones for repositories with the same name

Because every worker changes into the shared repositories directory and GitAgent derives its clone directory from only the repository basename, two distinct rows such as org1/tool and org2/tool can clone into and later delete the same local path when processed in parallel. In that case one README may be generated from the wrong checkout or a worker may fail while the other is using the directory; use an owner-qualified or otherwise unique clone path for each input repository.

Useful? React with 👍 / 👎.

SourceRank(config_manager)
generate_readme(config_manager, git_agent.metadata, args)

result.update({"name": git_agent.metadata.name, "status": "Success"})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Check README generation result before succeeding

ReadmeAgent.generate_readme() catches pipeline exceptions and returns a failed result instead of raising, so an LLM, graph, or write failure still reaches this line and records Success. In that scenario the main loop skips the row on reruns and the benchmark may evaluate a missing or stale README; only mark success after verifying the generation result and destination file.

Useful? React with 👍 / 👎.

logger.info(f"Successfully generated README in {format_time(time.time() - stage_start)}")

except Exception as e:
logger.error(f"Error processing {repo_url}: {e}")

finally:
file_handler.flush()
file_handler.close()
logger.removeHandler(file_handler)
delete_repository(repo_url)
os.chdir(original_cwd)

return result


def load_table(table_path: str) -> DataFrame:
if not table_path or not os.path.isfile(table_path):
logger.error(f"Table file missing or invalid: {table_path}")
sys.exit(1)

df = pd.read_csv(table_path) if table_path.endswith(".csv") else pd.read_excel(table_path)

if "repository" not in df.columns:
if "repo_url" in df.columns:
df["repository"] = df["repo_url"]
else:
logger.error("Table must contain a 'repository' or 'repo_url' column.")
sys.exit(1)

if "status" not in df.columns:
df["status"] = "Pending"
return df


def main():
parser = build_parser_from_yaml(extra_sections=["settings", "arguments", "multi-run"])
args, _ = parser.parse_known_args()

args.table_path = os.path.abspath(args.table_path)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate table_path before normalizing it

When the runner is invoked without --table-path (the parser gives this option a None default), this abspath call raises a TypeError before load_table() can report the missing argument cleanly. That makes the benchmark fail with a traceback instead of the intended validation path; check for a missing table path before calling os.path.abspath.

Useful? React with 👍 / 👎.


df = load_table(args.table_path)
repos = df["repository"].dropna().tolist()

unprocessed = [r for r in repos if df.loc[df["repository"] == r, "status"].values[0] != "Success"]

if unprocessed:
rich_section(f"Starting lightweight README Generation for {len(unprocessed)} repos")
with ProcessPoolExecutor(max_workers=max(1, os.cpu_count() // 2)) as executor:
futures = {executor.submit(process_repository, repo, args): repo for repo in unprocessed}
for future in as_completed(futures):
repo = futures[future]
try:
df.loc[df["repository"] == repo, "status"] = future.result()["status"]
if args.table_path.endswith(".csv"):
df.to_csv(args.table_path, index=False)
else:
df.to_excel(args.table_path, index=False)
except Exception as e:
logger.error(f"Failed to process {repo} — {e}")
else:
rich_section("All repositories processed successfully.")


if __name__ == "__main__":
main()
Loading