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.
- pysentry_rs-0.2.3/.github/workflows/benchmark.yml +156 -0
- {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/Cargo.lock +2 -1
- {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/Cargo.toml +2 -1
- {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/PKG-INFO +1 -1
- pysentry_rs-0.2.3/benchmarks/.gitignore +2 -0
- pysentry_rs-0.2.3/benchmarks/.python-version +1 -0
- pysentry_rs-0.2.3/benchmarks/README.md +3 -0
- pysentry_rs-0.2.3/benchmarks/main.py +111 -0
- pysentry_rs-0.2.3/benchmarks/pyproject.toml +12 -0
- pysentry_rs-0.2.3/benchmarks/src/benchmark_runner.py +364 -0
- pysentry_rs-0.2.3/benchmarks/src/performance_monitor.py +157 -0
- pysentry_rs-0.2.3/benchmarks/src/report_generator.py +222 -0
- pysentry_rs-0.2.3/benchmarks/src/tool_wrapper.py +347 -0
- pysentry_rs-0.2.3/benchmarks/test_data/large_requirements.txt +55 -0
- pysentry_rs-0.2.3/benchmarks/test_data/small_requirements.txt +10 -0
- pysentry_rs-0.2.3/benchmarks/uv.lock +1099 -0
- {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/src/cache/audit.rs +18 -35
- {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/src/cache/storage.rs +3 -4
- {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/src/dependency/resolvers/mod.rs +3 -21
- {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/src/dependency/resolvers/uv.rs +41 -10
- {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/src/types.rs +3 -10
- {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/.github/FUNDING.yml +0 -0
- {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/.github/dependabot.yml +0 -0
- {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/.github/workflows/ci.yml +0 -0
- {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/.github/workflows/release.yml +0 -0
- {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/.gitignore +0 -0
- {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/.pre-commit-config.yaml +0 -0
- {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/LICENSE +0 -0
- {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/README.md +0 -0
- {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/fixtures/requirements-tests/requirements-dev.txt +0 -0
- {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/fixtures/requirements-tests/requirements.txt +0 -0
- {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/fixtures/requirements-tests-vulnerable/requirements.txt +0 -0
- {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/pyproject.toml +0 -0
- {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/python/pysentry/__init__.py +0 -0
- {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/src/cache/mod.rs +0 -0
- {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/src/cli.rs +0 -0
- {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/src/dependency/mod.rs +0 -0
- {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/src/dependency/resolvers/pip_tools.rs +0 -0
- {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/src/dependency/scanner.rs +0 -0
- {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/src/error.rs +0 -0
- {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/src/lib.rs +0 -0
- {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/src/main.rs +0 -0
- {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/src/output/mod.rs +0 -0
- {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/src/output/report.rs +0 -0
- {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/src/output/sarif.rs +0 -0
- {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/src/parsers/lock.rs +0 -0
- {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/src/parsers/mod.rs +0 -0
- {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/src/parsers/poetry_lock.rs +0 -0
- {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/src/parsers/pyproject.rs +0 -0
- {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/src/parsers/requirements.rs +0 -0
- {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/src/providers/mod.rs +0 -0
- {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/src/providers/osv.rs +0 -0
- {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/src/providers/pypa.rs +0 -0
- {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/src/providers/pypi.rs +0 -0
- {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/src/python.rs +0 -0
- {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/src/vulnerability/database.rs +0 -0
- {pysentry_rs-0.2.2 → pysentry_rs-0.2.3}/src/vulnerability/matcher.rs +0 -0
- {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.
|
|
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.
|
|
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"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.11
|
|
@@ -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
|