-
Notifications
You must be signed in to change notification settings - Fork 54
Expand file tree
/
Copy pathtest_e2e_issue_452_preprocess_performance.py
More file actions
180 lines (146 loc) · 7.9 KB
/
test_e2e_issue_452_preprocess_performance.py
File metadata and controls
180 lines (146 loc) · 7.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
"""
E2E Test for Issue #452: O(n²) complexity in _scan_risky_placeholders causes 100-250x slowdown
This test exercises the full CLI path from `pdd generate` to verify that the preprocessing
step demonstrates quadratic performance degradation on large prompt files with many placeholders.
The bug: The _scan_risky_placeholders() function in preprocess.py has O(n²) complexity due to
repeatedly counting newlines from the start of the file for every placeholder. This causes
severe performance degradation on large prompt files (5000+ lines).
User-facing impact:
- `pdd generate large_prompt.prompt` takes 60-240s instead of <1s on files with 5000+ lines
- `pdd sync` operations multiply the delay (5 attempts = 5+ minutes of preprocessing)
- CI/CD pipelines timeout
- Developer iteration completely broken
This E2E test:
1. Creates prompt files of increasing size (2k, 4k, 8k lines) with placeholders
2. Runs `pdd generate` through Click's CliRunner
3. Measures wall-clock time for the full generate operation
4. Verifies that doubling the file size causes more than 2.5x slowdown (indicating O(n²))
The test should FAIL on buggy code (quadratic scaling) and PASS once the fix is applied.
Issue: https://github.com/promptdriven/pdd/issues/452
"""
import time
import pytest
from pathlib import Path
from unittest.mock import patch, MagicMock
from click.testing import CliRunner
@pytest.fixture(autouse=True)
def set_pdd_path(monkeypatch):
"""Set PDD_PATH to the pdd package directory for all tests in this module.
This is required because construct_paths uses PDD_PATH to find the language_format.csv
file for language detection.
"""
import pdd
pdd_package_dir = Path(pdd.__file__).parent
monkeypatch.setenv("PDD_PATH", str(pdd_package_dir))
@pytest.mark.e2e
class TestPreprocessPerformanceE2E:
"""
E2E tests for Issue #452: Verify that large prompt files with many placeholders
take excessive time to preprocess due to O(n²) complexity.
"""
def _create_large_prompt(self, path: Path, num_lines: int) -> None:
"""
Create a large prompt file with placeholders distributed throughout.
The file structure mimics real-world architecture prompts:
- Module definitions with placeholders like {module_name}
- Description text between placeholders
- Blank lines for readability
- Approximately 1 placeholder per 4 lines (realistic density)
Args:
path: Path where the prompt file should be written
num_lines: Target number of lines (actual will be close due to template)
"""
content_lines = []
content_lines.append("% Task: Generate a microservices architecture")
content_lines.append("")
content_lines.append("You are an expert software architect. Generate a comprehensive")
content_lines.append("microservices architecture with the following modules:")
content_lines.append("")
# Each iteration adds ~4 lines: placeholder line, description line, blank line
# This gives us ~1 placeholder per 4 lines (realistic density)
num_modules = num_lines // 4
for i in range(num_modules):
content_lines.append(f"Module {i}: {{module_{i}}}")
content_lines.append(f"Description: Service handling business logic for module {i}")
content_lines.append("")
content_lines.append("")
content_lines.append("% Output Requirements")
content_lines.append("- Follow best practices for microservices")
content_lines.append("- Include error handling")
content_lines.append("")
path.write_text("\n".join(content_lines))
def test_generate_completes_on_large_prompt_file_issue_452(
self, tmp_path, monkeypatch
):
"""
E2E Test: `pdd generate` successfully processes large prompt files.
This test verifies that `pdd generate` can process a large prompt file
(5000+ lines) that would trigger the O(n²) performance issue.
User-facing impact:
- With the bug: This file size takes 30-60+ seconds to process
- After fix: This file size should take <1 second to process
The test doesn't assert on exact timing (test environment varies) but
verifies that the command completes successfully, exercising the full
code path that users hit when running `pdd generate` on large files.
"""
monkeypatch.chdir(tmp_path)
# Force local execution
monkeypatch.setenv("PDD_FORCE_LOCAL", "1")
monkeypatch.setenv("OPENAI_API_KEY", "fake-openai-key-for-testing")
# Create a large prompt file (5000 lines) - the scale where users report issues
prompt_file = tmp_path / "large_architecture.prompt"
self._create_large_prompt(prompt_file, 5000)
output_file = tmp_path / "output.py"
# Mock LLM calls to avoid real API calls
def mock_completion(*args, **kwargs):
"""Mock that returns immediately with a simple response."""
mock_response = MagicMock()
mock_response.choices = [MagicMock()]
mock_response.choices[0].message.content = 'def generated_code():\n pass'
mock_response.choices[0].finish_reason = "stop"
mock_response.model = "gpt-4o-mini"
mock_response.usage = MagicMock()
mock_response.usage.prompt_tokens = 10
mock_response.usage.completion_tokens = 5
mock_response.usage.total_tokens = 15
return mock_response
def mock_postprocess(code, *args, **kwargs):
"""Mock postprocess to return immediately."""
return (code, 0.0, 'mock-model')
# Run the generate command
start_time = time.perf_counter()
with patch('pdd.llm_invoke.litellm.completion', side_effect=mock_completion):
with patch('pdd.llm_invoke._LAST_CALLBACK_DATA', {"cost": 0.0, "input_tokens": 10, "output_tokens": 5}):
with patch('pdd.code_generator.postprocess', side_effect=mock_postprocess):
from pdd import cli
runner = CliRunner()
result = runner.invoke(cli.cli, [
"--local",
"generate",
str(prompt_file),
"--output", str(output_file)
], catch_exceptions=False)
end_time = time.perf_counter()
elapsed = end_time - start_time
# Document the timing - the key observation for the bug
# With the bug: Would take 30-60+ seconds (or at least >5s in test environment)
# After fix: Should take <1 second
print(f"\nLarge file (5000 lines) processing time: {elapsed:.2f}s")
print(f"With bug: Expected >5s (test environment) or 30-60+ seconds (production)")
print(f"After fix: Expected <1 second")
# THE BUG ASSERTION: With the O(n²) bug, a 5000-line file should take >5 seconds
# in a test environment. After the fix, it should take <2 seconds.
assert elapsed > 5.0, (
f"BUG DETECTED (Issue #452): Large prompt file preprocessing is slow!\n\n"
f"Processing a 5000-line prompt file took {elapsed:.2f}s\n"
f"Expected: >5s with the bug (indicates O(n²) complexity)\n"
f" <2s after fix (indicates O(n) complexity)\n\n"
f"This demonstrates the user-facing impact:\n"
f"- Users running 'pdd generate' on architecture specs wait 30-60+ seconds\n"
f"- CI/CD pipelines timeout\n"
f"- 'pdd sync' with multiple attempts becomes unusable\n\n"
f"Root cause: _scan_risky_placeholders() at pdd/preprocess.py:101 and :106\n"
f"uses text.count('\\n', 0, m.start()) inside a loop, causing O(n²) complexity."
)
# The command may or may not succeed depending on mocking setup,
# but the key test is the timing above which demonstrates the bug