pysentry-rs 0.2.2__tar.gz → 0.2.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of pysentry-rs might be problematic. Click here for more details.

Files changed (58) hide show
  1. pysentry_rs-0.2.3/.github/workflows/benchmark.yml +156 -0
  2. {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/Cargo.lock +2 -1
  3. {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/Cargo.toml +2 -1
  4. {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/PKG-INFO +1 -1
  5. pysentry_rs-0.2.3/benchmarks/.gitignore +2 -0
  6. pysentry_rs-0.2.3/benchmarks/.python-version +1 -0
  7. pysentry_rs-0.2.3/benchmarks/README.md +3 -0
  8. pysentry_rs-0.2.3/benchmarks/main.py +111 -0
  9. pysentry_rs-0.2.3/benchmarks/pyproject.toml +12 -0
  10. pysentry_rs-0.2.3/benchmarks/src/benchmark_runner.py +364 -0
  11. pysentry_rs-0.2.3/benchmarks/src/performance_monitor.py +157 -0
  12. pysentry_rs-0.2.3/benchmarks/src/report_generator.py +222 -0
  13. pysentry_rs-0.2.3/benchmarks/src/tool_wrapper.py +347 -0
  14. pysentry_rs-0.2.3/benchmarks/test_data/large_requirements.txt +55 -0
  15. pysentry_rs-0.2.3/benchmarks/test_data/small_requirements.txt +10 -0
  16. pysentry_rs-0.2.3/benchmarks/uv.lock +1099 -0
  17. {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/src/cache/audit.rs +18 -35
  18. {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/src/cache/storage.rs +3 -4
  19. {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/src/dependency/resolvers/mod.rs +3 -21
  20. {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/src/dependency/resolvers/uv.rs +41 -10
  21. {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/src/types.rs +3 -10
  22. {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/.github/FUNDING.yml +0 -0
  23. {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/.github/dependabot.yml +0 -0
  24. {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/.github/workflows/ci.yml +0 -0
  25. {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/.github/workflows/release.yml +0 -0
  26. {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/.gitignore +0 -0
  27. {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/.pre-commit-config.yaml +0 -0
  28. {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/LICENSE +0 -0
  29. {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/README.md +0 -0
  30. {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/fixtures/requirements-tests/requirements-dev.txt +0 -0
  31. {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/fixtures/requirements-tests/requirements.txt +0 -0
  32. {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/fixtures/requirements-tests-vulnerable/requirements.txt +0 -0
  33. {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/pyproject.toml +0 -0
  34. {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/python/pysentry/__init__.py +0 -0
  35. {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/src/cache/mod.rs +0 -0
  36. {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/src/cli.rs +0 -0
  37. {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/src/dependency/mod.rs +0 -0
  38. {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/src/dependency/resolvers/pip_tools.rs +0 -0
  39. {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/src/dependency/scanner.rs +0 -0
  40. {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/src/error.rs +0 -0
  41. {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/src/lib.rs +0 -0
  42. {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/src/main.rs +0 -0
  43. {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/src/output/mod.rs +0 -0
  44. {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/src/output/report.rs +0 -0
  45. {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/src/output/sarif.rs +0 -0
  46. {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/src/parsers/lock.rs +0 -0
  47. {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/src/parsers/mod.rs +0 -0
  48. {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/src/parsers/poetry_lock.rs +0 -0
  49. {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/src/parsers/pyproject.rs +0 -0
  50. {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/src/parsers/requirements.rs +0 -0
  51. {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/src/providers/mod.rs +0 -0
  52. {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/src/providers/osv.rs +0 -0
  53. {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/src/providers/pypa.rs +0 -0
  54. {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/src/providers/pypi.rs +0 -0
  55. {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/src/python.rs +0 -0
  56. {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/src/vulnerability/database.rs +0 -0
  57. {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/src/vulnerability/matcher.rs +0 -0
  58. {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/src/vulnerability/mod.rs +0 -0
@@ -0,0 +1,156 @@
1
+ name: Benchmark Release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+ workflow_dispatch:
8
+ inputs:
9
+ version:
10
+ description: "Version to benchmark (e.g., v0.2.3)"
11
+ required: true
12
+ default: "v0.2.3"
13
+
14
+ env:
15
+ CARGO_TERM_COLOR: always
16
+ RUST_BACKTRACE: 1
17
+
18
+ jobs:
19
+ benchmark:
20
+ name: Run Benchmarks
21
+ runs-on: ubuntu-latest
22
+ permissions:
23
+ contents: write
24
+ pull-requests: write
25
+
26
+ steps:
27
+ - name: Checkout code
28
+ uses: actions/checkout@v4
29
+ with:
30
+ fetch-depth: 0
31
+
32
+ - name: Extract version from tag
33
+ id: version
34
+ run: |
35
+ if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
36
+ VERSION="${{ github.event.inputs.version }}"
37
+ else
38
+ VERSION="${{ github.ref_name }}"
39
+ fi
40
+ VERSION_CLEAN=${VERSION#v}
41
+ echo "version=${VERSION_CLEAN}" >> $GITHUB_OUTPUT
42
+ echo "version_with_v=${VERSION}" >> $GITHUB_OUTPUT
43
+ echo "branch_name=benchmark-${VERSION_CLEAN}" >> $GITHUB_OUTPUT
44
+
45
+ - name: Install system dependencies
46
+ run: |
47
+ sudo apt-get update
48
+ sudo apt-get install -y libssl-dev pkg-config
49
+
50
+ - name: Install Rust
51
+ uses: dtolnay/rust-toolchain@stable
52
+
53
+ - name: Cache cargo
54
+ uses: actions/cache@v4
55
+ with:
56
+ path: |
57
+ ~/.cargo/registry/index/
58
+ ~/.cargo/registry/cache/
59
+ ~/.cargo/git/db/
60
+ target
61
+ key: ${{ runner.os }}-cargo-benchmark-${{ hashFiles('**/Cargo.lock') }}
62
+ restore-keys: |
63
+ ${{ runner.os }}-cargo-benchmark-
64
+ ${{ runner.os }}-cargo-build-
65
+
66
+ - name: Build PySentry
67
+ run: cargo build --release
68
+
69
+ - name: Set up Python
70
+ uses: actions/setup-python@v5
71
+ with:
72
+ python-version: "3.11"
73
+
74
+ - name: Install uv
75
+ run: |
76
+ curl -LsSf https://astral.sh/uv/install.sh | sh
77
+ echo "$HOME/.local/bin" >> $GITHUB_PATH
78
+
79
+ - name: Install pip-audit for benchmark comparison
80
+ run: pip install pip-audit
81
+
82
+ - name: Install benchmark dependencies
83
+ run: |
84
+ cd benchmarks
85
+ uv sync
86
+
87
+ - name: Run benchmark suite
88
+ run: |
89
+ cd benchmarks
90
+ uv run python main.py --skip-build
91
+
92
+ ls -la results/
93
+
94
+ LATEST_FILE=$(ls results/*.md 2>/dev/null | sort -r | head -n 1)
95
+ if [ -f "$LATEST_FILE" ]; then
96
+ cp "$LATEST_FILE" results/latest.md
97
+ echo "Created latest.md from: $LATEST_FILE"
98
+ else
99
+ echo "Warning: No benchmark files found to create latest.md"
100
+ fi
101
+
102
+ - name: Configure Git
103
+ run: |
104
+ git config --global user.name "github-actions[bot]"
105
+ git config --global user.email "github-actions[bot]@users.noreply.github.com"
106
+
107
+ - name: Create and switch to benchmark branch
108
+ run: |
109
+ BRANCH_NAME="${{ steps.version.outputs.branch_name }}"
110
+ git checkout -b $BRANCH_NAME
111
+
112
+ - name: Commit benchmark results
113
+ run: |
114
+ VERSION="${{ steps.version.outputs.version }}"
115
+
116
+ git add benchmarks/results/
117
+
118
+ if git diff --staged --quiet; then
119
+ echo "No changes to commit"
120
+ exit 0
121
+ fi
122
+
123
+ git commit -m "Add benchmark results for version ${VERSION}"
124
+
125
+ - name: Push benchmark branch
126
+ run: |
127
+ BRANCH_NAME="${{ steps.version.outputs.branch_name }}"
128
+ git push origin $BRANCH_NAME
129
+
130
+ - name: Create Pull Request
131
+ env:
132
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
133
+ run: |
134
+ VERSION="${{ steps.version.outputs.version }}"
135
+ BRANCH_NAME="${{ steps.version.outputs.branch_name }}"
136
+
137
+ PR_BODY="This PR contains automated benchmark results comparing PySentry v${VERSION} against pip-audit."
138
+
139
+ gh pr create \
140
+ --title "Benchmark results for v${VERSION}" \
141
+ --body "$PR_BODY" \
142
+ --base main \
143
+ --head $BRANCH_NAME \
144
+ --label "benchmark,automated"
145
+
146
+ - name: Summary
147
+ run: |
148
+ VERSION="${{ steps.version.outputs.version }}"
149
+ BRANCH_NAME="${{ steps.version.outputs.branch_name }}"
150
+
151
+ echo "Benchmark workflow completed successfully!"
152
+ echo ""
153
+ echo "Benchmarked version: v${VERSION}"
154
+ echo "Created branch: ${BRANCH_NAME}"
155
+ echo "Results location: benchmarks/results/"
156
+ echo "Pull request created automatically"
@@ -1115,7 +1115,7 @@ dependencies = [
1115
1115
 
1116
1116
  [[package]]
1117
1117
  name = "pysentry"
1118
- version = "0.2.2"
1118
+ version = "0.2.3"
1119
1119
  dependencies = [
1120
1120
  "anyhow",
1121
1121
  "async-trait",
@@ -1128,6 +1128,7 @@ dependencies = [
1128
1128
  "pyo3",
1129
1129
  "regex",
1130
1130
  "reqwest",
1131
+ "rustc-hash",
1131
1132
  "serde",
1132
1133
  "serde_json",
1133
1134
  "serde_yaml",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "pysentry"
3
- version = "0.2.2"
3
+ version = "0.2.3"
4
4
  edition = "2021"
5
5
  rust-version = "1.79"
6
6
  description = "Security vulnerability auditing for Python packages"
@@ -33,6 +33,7 @@ pep440_rs = "0.7.3"
33
33
  pyo3 = { version = "0.25.1", features = ["extension-module"], optional = true }
34
34
  regex = "1.11.1"
35
35
  reqwest = { version = "0.12.22", features = ["json", "stream", "rustls-tls"], default-features = false }
36
+ rustc-hash = "2.1.1"
36
37
  serde = { version = "1.0.219", features = ["derive"] }
37
38
  serde_json = "1.0.142"
38
39
  serde_yaml = "0.9.34"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pysentry-rs
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
@@ -0,0 +1,2 @@
1
+ workdirs
2
+ cache
@@ -0,0 +1 @@
1
+ 3.11
@@ -0,0 +1,3 @@
1
+ # PySentry - pip-audit Benchmark Suite
2
+
3
+ Look at the latest results at [results/latest.md](results/latest.md)
@@ -0,0 +1,111 @@
1
+ import sys
2
+ import argparse
3
+ from pathlib import Path
4
+
5
+ sys.path.insert(0, str(Path(__file__).parent / "src"))
6
+
7
+ from src.benchmark_runner import BenchmarkRunner
8
+
9
+
10
+ def main():
11
+ parser = argparse.ArgumentParser(
12
+ description="PySentry vs pip-audit benchmark suite",
13
+ formatter_class=argparse.RawDescriptionHelpFormatter,
14
+ epilog="""
15
+ Examples:
16
+ python main.py # Run full benchmark suite
17
+ python main.py --quick # Run only small dataset for quick testing
18
+ python main.py --output-dir ./custom-results # Custom output directory
19
+ """,
20
+ )
21
+
22
+ parser.add_argument(
23
+ "--quick", action="store_true", help="Run only small dataset for quick testing"
24
+ )
25
+
26
+ parser.add_argument(
27
+ "--output-dir",
28
+ type=Path,
29
+ help="Custom output directory for results (default: ./results/)",
30
+ )
31
+
32
+ parser.add_argument(
33
+ "--verbose", "-v", action="store_true", help="Enable verbose output"
34
+ )
35
+
36
+ parser.add_argument(
37
+ "--skip-build",
38
+ action="store_true",
39
+ help="Skip PySentry build check (assume it's already built)",
40
+ )
41
+
42
+ args = parser.parse_args()
43
+
44
+ try:
45
+ benchmark_dir = Path(__file__).parent
46
+ if args.output_dir:
47
+ runner = BenchmarkRunner(benchmark_dir)
48
+ runner.results_dir = args.output_dir
49
+ runner.results_dir.mkdir(parents=True, exist_ok=True)
50
+ else:
51
+ runner = BenchmarkRunner(benchmark_dir)
52
+
53
+ if args.verbose:
54
+ print(f"Benchmark directory: {benchmark_dir}")
55
+ print(f"Results directory: {runner.results_dir}")
56
+
57
+ if args.quick:
58
+ print("Quick mode: Running only small dataset...")
59
+ large_dataset = runner.test_data_dir / "large_requirements.txt"
60
+ backup_path = None
61
+ if large_dataset.exists():
62
+ backup_path = runner.test_data_dir / "large_requirements.txt.backup"
63
+ large_dataset.rename(backup_path)
64
+
65
+ try:
66
+ print("Starting benchmark suite...")
67
+ suite = runner.run_full_benchmark_suite()
68
+
69
+ report_path = runner.save_and_generate_report(suite)
70
+
71
+ successful_runs = len(
72
+ [r for r in suite.results if r.metrics.exit_code <= 1]
73
+ )
74
+ total_runs = len(suite.results)
75
+
76
+ print("\n" + "=" * 60)
77
+ print("BENCHMARK SUITE COMPLETED")
78
+ print("=" * 60)
79
+ print(f"Total runs: {total_runs}")
80
+ print(f"Successful: {successful_runs}")
81
+ print(f"Failed: {total_runs - successful_runs}")
82
+ print(f"Duration: {suite.total_duration:.2f} seconds")
83
+ print(f"Report saved to: {report_path}")
84
+ print("=" * 60)
85
+
86
+ exit_code = 0 if successful_runs == total_runs else 1
87
+
88
+ if exit_code != 0:
89
+ print(f"WARNING: {total_runs - successful_runs} benchmark runs failed!")
90
+
91
+ return exit_code
92
+
93
+ finally:
94
+ if args.quick and backup_path and backup_path.exists():
95
+ backup_path.rename(large_dataset)
96
+
97
+ except KeyboardInterrupt:
98
+ print("\nBenchmark interrupted by user.")
99
+ return 1
100
+
101
+ except Exception as e:
102
+ print(f"Error running benchmark suite: {e}")
103
+ if args.verbose:
104
+ import traceback
105
+
106
+ traceback.print_exc()
107
+ return 1
108
+
109
+
110
+ if __name__ == "__main__":
111
+ sys.exit(main())
@@ -0,0 +1,12 @@
1
+ [project]
2
+ name = "benchmarks"
3
+ version = "0.1.0"
4
+ description = "Performance benchmark suite for PySentry vs pip-audit"
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ dependencies = [
8
+ "matplotlib>=3.10.5",
9
+ "pip-audit>=2.9.0",
10
+ "psutil>=7.0.0",
11
+ "tabulate>=0.9.0",
12
+ ]
@@ -0,0 +1,364 @@
1
+ import shutil
2
+ from pathlib import Path
3
+ from typing import List, Dict, Any, Optional
4
+ from datetime import datetime
5
+ from dataclasses import dataclass, asdict
6
+
7
+ from .tool_wrapper import ToolRegistry, BenchmarkConfig
8
+ from .performance_monitor import PerformanceMetrics, SystemInfo
9
+ from .report_generator import ReportGenerator
10
+
11
+
12
+ @dataclass
13
+ class BenchmarkResult:
14
+ config_name: str
15
+ tool_name: str
16
+ dataset_name: str
17
+ cache_type: str
18
+ metrics: PerformanceMetrics
19
+ timestamp: str
20
+
21
+ def to_dict(self) -> Dict[str, Any]:
22
+ data = asdict(self)
23
+ data["metrics"] = asdict(self.metrics)
24
+ return data
25
+
26
+
27
+ @dataclass
28
+ class BenchmarkSuite:
29
+ system_info: SystemInfo
30
+ results: List[BenchmarkResult]
31
+ start_time: str
32
+ end_time: str
33
+ total_duration: float
34
+
35
+ def to_dict(self) -> Dict[str, Any]:
36
+ return {
37
+ "system_info": asdict(self.system_info),
38
+ "results": [result.to_dict() for result in self.results],
39
+ "start_time": self.start_time,
40
+ "end_time": self.end_time,
41
+ "total_duration": self.total_duration,
42
+ }
43
+
44
+
45
+ class BenchmarkRunner:
46
+ def __init__(self, benchmark_dir: Optional[Path] = None):
47
+ if benchmark_dir is None:
48
+ benchmark_dir = Path(__file__).parent.parent
49
+
50
+ self.benchmark_dir = benchmark_dir
51
+ self.test_data_dir = benchmark_dir / "test_data"
52
+ self.results_dir = benchmark_dir / "results"
53
+ self.cache_dir = benchmark_dir / "cache"
54
+ self.workdirs = benchmark_dir / "workdirs"
55
+
56
+ self.results_dir.mkdir(exist_ok=True)
57
+ self.cache_dir.mkdir(exist_ok=True)
58
+ self.workdirs.mkdir(exist_ok=True)
59
+
60
+ self.tool_registry = ToolRegistry(cache_dir=self.cache_dir)
61
+ self.report_generator = ReportGenerator()
62
+
63
+ def initialize_cache_directory(self):
64
+ print(f"Using cache directory: {self.cache_dir}")
65
+
66
+ self.cache_dir.mkdir(exist_ok=True)
67
+
68
+ if self.cache_dir.exists():
69
+ for cache_file in self.cache_dir.glob("*"):
70
+ if cache_file.is_file():
71
+ cache_file.unlink()
72
+ elif cache_file.is_dir():
73
+ shutil.rmtree(cache_file)
74
+
75
+ print("Initialized clean cache directory")
76
+
77
+ def clear_cache_directory(self):
78
+ print(f"Clearing cache directory: {self.cache_dir}")
79
+
80
+ if self.cache_dir.exists():
81
+ for cache_file in self.cache_dir.glob("*"):
82
+ if cache_file.is_file():
83
+ cache_file.unlink()
84
+ elif cache_file.is_dir():
85
+ shutil.rmtree(cache_file)
86
+
87
+ print("Cache directory cleared")
88
+
89
+ def clean_work_directories(self):
90
+ print(f"Cleaning work directories in: {self.workdirs}")
91
+
92
+ if self.workdirs.exists():
93
+ for work_dir in self.workdirs.glob("*"):
94
+ if work_dir.is_dir():
95
+ try:
96
+ shutil.rmtree(work_dir)
97
+ except Exception as e:
98
+ print(f"Warning: Could not remove {work_dir}: {e}")
99
+
100
+ print("Work directories cleaned")
101
+
102
+ def run_single_benchmark(
103
+ self, config: BenchmarkConfig, dataset_path: Path, cache_type: str
104
+ ) -> BenchmarkResult:
105
+ dataset_name = dataset_path.stem
106
+
107
+ print(f"Running {config.config_name} on {dataset_name} ({cache_type} cache)...")
108
+
109
+ tool = self.tool_registry.get_tool(config.tool_name)
110
+ if not tool:
111
+ raise ValueError(f"Tool {config.tool_name} not available")
112
+
113
+ work_dir_name = f"{dataset_name}_{config.config_name}_{cache_type}"
114
+ work_path = self.workdirs / work_dir_name
115
+
116
+ if work_path.exists():
117
+ shutil.rmtree(work_path)
118
+ work_path.mkdir()
119
+
120
+ try:
121
+ temp_requirements = work_path / "requirements.txt"
122
+ shutil.copy2(dataset_path, temp_requirements)
123
+
124
+ (work_path / "setup.py").write_text("# Minimal setup.py for benchmarking")
125
+
126
+ print(f" Working in: {work_path}")
127
+
128
+ use_cache = cache_type == "hot"
129
+ metrics = tool.execute(
130
+ config,
131
+ temp_requirements,
132
+ use_cache=use_cache,
133
+ working_dir=work_path,
134
+ dataset_name=dataset_name,
135
+ cache_type=cache_type,
136
+ )
137
+ except Exception as e:
138
+ print(f" ✗ Exception during execution: {e}")
139
+ raise
140
+
141
+ if metrics.exit_code <= 1:
142
+ try:
143
+ shutil.rmtree(work_path)
144
+ except:
145
+ pass
146
+ else:
147
+ print(f" ! Work directory preserved for debugging: {work_path}")
148
+
149
+ return BenchmarkResult(
150
+ config_name=config.config_name,
151
+ tool_name=config.tool_name,
152
+ dataset_name=dataset_name,
153
+ cache_type=cache_type,
154
+ metrics=metrics,
155
+ timestamp=datetime.now().isoformat(),
156
+ )
157
+
158
+ def run_dataset_benchmarks(self, dataset_path: Path) -> List[BenchmarkResult]:
159
+ results = []
160
+ configs = self.tool_registry.get_all_benchmark_configs(dataset_path)
161
+
162
+ if not configs:
163
+ print("No benchmark configurations available!")
164
+ return results
165
+
166
+ print(
167
+ f"Running benchmarks on {dataset_path.name} ({len(configs)} configurations)"
168
+ )
169
+ print("Testing strategy:")
170
+ print(" - Cold phase: Clear cache → Run cold → Record")
171
+ print(" - Hot phase: Clear cache → Run cold (warmup) → Run hot → Record")
172
+
173
+ print(f"\n🧊 COLD TESTING PHASE - {dataset_path.name}")
174
+ print("=" * 60)
175
+ for i, config in enumerate(configs):
176
+ print(f"\nCold test {i + 1}/{len(configs)}: {config.config_name}")
177
+ print("-" * 40)
178
+
179
+ print("🧽 Clearing all caches...")
180
+ self.tool_registry.clear_all_caches()
181
+
182
+ try:
183
+ cold_result = self.run_single_benchmark(config, dataset_path, "cold")
184
+ results.append(cold_result)
185
+ self._show_result_feedback(cold_result, "cold")
186
+
187
+ except Exception as e:
188
+ print(f" ✗ Error running {config.config_name} (cold): {e}")
189
+ error_result = self._create_error_result(
190
+ config, dataset_path, "cold", str(e)
191
+ )
192
+ results.append(error_result)
193
+
194
+ print(f"\n🔥 HOT TESTING PHASE - {dataset_path.name}")
195
+ print("=" * 60)
196
+ for i, config in enumerate(configs):
197
+ print(f"\nHot test {i + 1}/{len(configs)}: {config.config_name}")
198
+ print("-" * 40)
199
+
200
+ print("🧽 Clearing all caches...")
201
+ self.tool_registry.clear_all_caches()
202
+
203
+ print("🌡️ Running warmup (cold test to populate cache)...")
204
+ try:
205
+ warmup_result = self.run_single_benchmark(config, dataset_path, "cold")
206
+ if warmup_result.metrics.exit_code <= 1:
207
+ print(
208
+ f" ✓ Warmup completed ({warmup_result.metrics.execution_time:.2f}s)"
209
+ )
210
+ else:
211
+ print(
212
+ f" ✗ Warmup failed (exit code {warmup_result.metrics.exit_code})"
213
+ )
214
+ except Exception as e:
215
+ print(f" ⚠️ Warmup failed: {e}")
216
+
217
+ print("🔥 Running hot test (with cache from warmup)...")
218
+ try:
219
+ hot_result = self.run_single_benchmark(config, dataset_path, "hot")
220
+ results.append(hot_result)
221
+ self._show_result_feedback(hot_result, "hot")
222
+
223
+ except Exception as e:
224
+ print(f" ✗ Error running {config.config_name} (hot): {e}")
225
+ error_result = self._create_error_result(
226
+ config, dataset_path, "hot", str(e)
227
+ )
228
+ results.append(error_result)
229
+
230
+ print(f"\n✅ Completed all configurations for {dataset_path.name}")
231
+ return results
232
+
233
+ def _show_result_feedback(self, result: BenchmarkResult, cache_type: str):
234
+ if result.metrics.exit_code == 0:
235
+ print(
236
+ f" ✓ {result.config_name} ({cache_type}): "
237
+ f"{result.metrics.execution_time:.2f}s, "
238
+ f"{result.metrics.peak_memory_mb:.1f}MB (no vulnerabilities)"
239
+ )
240
+ elif result.metrics.exit_code == 1:
241
+ print(
242
+ f" ✓ {result.config_name} ({cache_type}): "
243
+ f"{result.metrics.execution_time:.2f}s, "
244
+ f"{result.metrics.peak_memory_mb:.1f}MB (vulnerabilities found)"
245
+ )
246
+ else:
247
+ print(
248
+ f" ✗ {result.config_name} ({cache_type}): FAILED (exit code {result.metrics.exit_code})"
249
+ )
250
+
251
+ def _create_error_result(
252
+ self,
253
+ config: BenchmarkConfig,
254
+ dataset_path: Path,
255
+ cache_type: str,
256
+ error_msg: str,
257
+ ) -> BenchmarkResult:
258
+ return BenchmarkResult(
259
+ config_name=config.config_name,
260
+ tool_name=config.tool_name,
261
+ dataset_name=dataset_path.stem,
262
+ cache_type=cache_type,
263
+ metrics=PerformanceMetrics(
264
+ execution_time=0.0,
265
+ peak_memory_mb=0.0,
266
+ avg_memory_mb=0.0,
267
+ cpu_percent=0.0,
268
+ exit_code=-1,
269
+ stdout="",
270
+ stderr=f"Benchmark error: {error_msg}",
271
+ ),
272
+ timestamp=datetime.now().isoformat(),
273
+ )
274
+
275
+ def run_full_benchmark_suite(self) -> BenchmarkSuite:
276
+ start_time = datetime.now()
277
+ print(f"Starting full benchmark suite at {start_time.isoformat()}")
278
+
279
+ self.clean_work_directories()
280
+
281
+ if not self.tool_registry.ensure_pysentry_built():
282
+ raise RuntimeError("Could not build or find PySentry binary")
283
+
284
+ available_tools = self.tool_registry.get_available_tools()
285
+ print(f"Available tools: {', '.join(available_tools)}")
286
+
287
+ if not available_tools:
288
+ raise RuntimeError("No tools available for benchmarking")
289
+
290
+ datasets = []
291
+ for pattern in ["small_requirements.txt", "large_requirements.txt"]:
292
+ dataset_path = self.test_data_dir / pattern
293
+ if dataset_path.exists():
294
+ datasets.append(dataset_path)
295
+ else:
296
+ print(f"Warning: Dataset {pattern} not found")
297
+
298
+ if not datasets:
299
+ raise RuntimeError("No benchmark datasets found")
300
+
301
+ print(f"Found {len(datasets)} datasets: {[d.name for d in datasets]}")
302
+
303
+ all_results = []
304
+ for i, dataset in enumerate(datasets):
305
+ print(f"\n{'=' * 80}")
306
+ print(f"TESTING DATASET {i + 1}/{len(datasets)}: {dataset.name}")
307
+ print(f"{'=' * 80}")
308
+
309
+ results = self.run_dataset_benchmarks(dataset)
310
+ all_results.extend(results)
311
+ print(f"Completed {dataset.name}: {len(results)} results")
312
+
313
+ end_time = datetime.now()
314
+ duration = (end_time - start_time).total_seconds()
315
+
316
+ suite = BenchmarkSuite(
317
+ system_info=SystemInfo.get_current(),
318
+ results=all_results,
319
+ start_time=start_time.isoformat(),
320
+ end_time=end_time.isoformat(),
321
+ total_duration=duration,
322
+ )
323
+
324
+ print(f"Benchmark suite completed in {duration:.2f} seconds")
325
+ print(f"Total results: {len(all_results)}")
326
+
327
+ return suite
328
+
329
+ def get_pysentry_version(self) -> str:
330
+ try:
331
+ pysentry_tool = self.tool_registry.get_tool("pysentry")
332
+ if pysentry_tool and pysentry_tool.binary_path:
333
+ import subprocess
334
+
335
+ result = subprocess.run(
336
+ [str(pysentry_tool.binary_path), "--version"],
337
+ capture_output=True,
338
+ text=True,
339
+ timeout=10,
340
+ )
341
+ if result.returncode == 0:
342
+ version_line = result.stdout.strip()
343
+ if " " in version_line:
344
+ return version_line.split()[-1]
345
+ return version_line
346
+ except Exception:
347
+ pass
348
+ return "unknown"
349
+
350
+ def save_and_generate_report(self, suite: BenchmarkSuite) -> Path:
351
+ version = self.get_pysentry_version()
352
+ report_filename = f"{version}.md"
353
+ report_path = self.results_dir / report_filename
354
+
355
+ print(f"Generating report: {report_path}")
356
+
357
+ markdown_content = self.report_generator.generate_report(suite)
358
+
359
+ with open(report_path, "w", encoding="utf-8") as f:
360
+ f.write(markdown_content)
361
+
362
+ print(f"Report saved to: {report_path}")
363
+
364
+ return report_path