Kea2-python 0.2.2__py3-none-any.whl → 0.2.4__py3-none-any.whl
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 Kea2-python might be problematic. Click here for more details.
- kea2/adbUtils.py +3 -1
- kea2/assets/monkeyq.jar +0 -0
- kea2/bug_report_generator.py +570 -233
- kea2/cli.py +42 -0
- kea2/fastbotManager.py +8 -3
- kea2/keaUtils.py +58 -53
- kea2/kea_launcher.py +2 -0
- kea2/logWatcher.py +16 -2
- kea2/templates/bug_report_template.html +925 -171
- kea2/u2Driver.py +2 -1
- kea2/utils.py +14 -0
- {kea2_python-0.2.2.dist-info → kea2_python-0.2.4.dist-info}/METADATA +4 -5
- {kea2_python-0.2.2.dist-info → kea2_python-0.2.4.dist-info}/RECORD +17 -17
- {kea2_python-0.2.2.dist-info → kea2_python-0.2.4.dist-info}/WHEEL +0 -0
- {kea2_python-0.2.2.dist-info → kea2_python-0.2.4.dist-info}/entry_points.txt +0 -0
- {kea2_python-0.2.2.dist-info → kea2_python-0.2.4.dist-info}/licenses/LICENSE +0 -0
- {kea2_python-0.2.2.dist-info → kea2_python-0.2.4.dist-info}/top_level.txt +0 -0
kea2/bug_report_generator.py
CHANGED
|
@@ -1,31 +1,124 @@
|
|
|
1
1
|
import json
|
|
2
|
-
import datetime
|
|
2
|
+
from datetime import datetime
|
|
3
3
|
from dataclasses import dataclass
|
|
4
4
|
from pathlib import Path
|
|
5
|
-
from typing import Dict, TypedDict,
|
|
5
|
+
from typing import Dict, TypedDict, List, Deque, NewType, Union, Optional
|
|
6
6
|
from collections import deque
|
|
7
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
7
8
|
|
|
8
|
-
from PIL import Image, ImageDraw
|
|
9
|
+
from PIL import Image, ImageDraw, ImageFont
|
|
9
10
|
from jinja2 import Environment, FileSystemLoader, select_autoescape, PackageLoader
|
|
10
|
-
from .utils import getLogger
|
|
11
|
-
|
|
11
|
+
from kea2.utils import getLogger
|
|
12
12
|
|
|
13
13
|
logger = getLogger(__name__)
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
class StepData(TypedDict):
|
|
17
|
+
# The type of the action (Monkey / Script / Script Info)
|
|
17
18
|
Type: str
|
|
19
|
+
# The steps of monkey event when the action happened
|
|
20
|
+
# ps: since we insert script actions into monkey actions. Total actions count >= Monkey actions count
|
|
18
21
|
MonkeyStepsCount: int
|
|
22
|
+
# The time stamp of the action
|
|
19
23
|
Time: str
|
|
24
|
+
# The execution info of the action
|
|
20
25
|
Info: Dict
|
|
26
|
+
# The screenshot of the action
|
|
21
27
|
Screenshot: str
|
|
22
28
|
|
|
29
|
+
|
|
30
|
+
class CovData(TypedDict):
|
|
31
|
+
stepsCount: int
|
|
32
|
+
coverage: float
|
|
33
|
+
totalActivitiesCount: int
|
|
34
|
+
testedActivitiesCount: int
|
|
35
|
+
totalActivities: List[str]
|
|
36
|
+
testedActivities: List[str]
|
|
37
|
+
activityCountHistory: Dict[str, int]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ReportData(TypedDict):
|
|
41
|
+
timestamp: str
|
|
42
|
+
bugs_found: int
|
|
43
|
+
executed_events: int
|
|
44
|
+
total_testing_time: float
|
|
45
|
+
coverage: float
|
|
46
|
+
total_activities_count: int
|
|
47
|
+
tested_activities_count: int
|
|
48
|
+
total_activities: List
|
|
49
|
+
tested_activities: List
|
|
50
|
+
all_properties_count: int
|
|
51
|
+
executed_properties_count: int
|
|
52
|
+
property_violations: List[Dict]
|
|
53
|
+
property_stats: List
|
|
54
|
+
property_error_details: Dict[str, List[Dict]] # Support multiple errors per property
|
|
55
|
+
screenshot_info: Dict
|
|
56
|
+
coverage_trend: List
|
|
57
|
+
property_execution_trend: List # Track executed properties count over steps
|
|
58
|
+
activity_count_history: Dict[str, int] # Activity traversal count from final coverage data
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class PropertyExecResult(TypedDict):
|
|
62
|
+
precond_satisfied: int
|
|
63
|
+
executed: int
|
|
64
|
+
fail: int
|
|
65
|
+
error: int
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass
|
|
69
|
+
class PropertyExecInfo:
|
|
70
|
+
"""Class representing property execution information from property_exec_info file"""
|
|
71
|
+
prop_name: str
|
|
72
|
+
state: str # start, pass, fail, error
|
|
73
|
+
traceback: str
|
|
74
|
+
start_steps_count: int
|
|
75
|
+
occurrence_count: int = 1
|
|
76
|
+
short_description: str = ""
|
|
77
|
+
start_steps_count_list: List[int] = None
|
|
78
|
+
|
|
79
|
+
def __post_init__(self):
|
|
80
|
+
if self.start_steps_count_list is None:
|
|
81
|
+
self.start_steps_count_list = [self.start_steps_count]
|
|
82
|
+
if not self.short_description and self.traceback:
|
|
83
|
+
self.short_description = self._extract_error_summary(self.traceback)
|
|
84
|
+
|
|
85
|
+
def _extract_error_summary(self, traceback: str) -> str:
|
|
86
|
+
"""Extract a short error summary from the full traceback"""
|
|
87
|
+
try:
|
|
88
|
+
lines = traceback.strip().split('\n')
|
|
89
|
+
for line in reversed(lines):
|
|
90
|
+
line = line.strip()
|
|
91
|
+
if line and not line.startswith(' '):
|
|
92
|
+
return line
|
|
93
|
+
return "Unknown error"
|
|
94
|
+
except Exception:
|
|
95
|
+
return "Error parsing traceback"
|
|
96
|
+
|
|
97
|
+
def get_error_hash(self) -> int:
|
|
98
|
+
"""Generate hash key for error deduplication"""
|
|
99
|
+
return hash((self.state, self.traceback))
|
|
100
|
+
|
|
101
|
+
def is_error_state(self) -> bool:
|
|
102
|
+
"""Check if this is an error or fail state"""
|
|
103
|
+
return self.state in ["fail", "error"]
|
|
104
|
+
|
|
105
|
+
def add_occurrence(self, start_steps_count: int):
|
|
106
|
+
"""Add another occurrence of the same error"""
|
|
107
|
+
self.occurrence_count += 1
|
|
108
|
+
self.start_steps_count_list.append(start_steps_count)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
PropertyName = NewType("PropertyName", str)
|
|
112
|
+
TestResult = NewType("TestResult", Dict[PropertyName, PropertyExecResult])
|
|
113
|
+
|
|
114
|
+
|
|
23
115
|
@dataclass
|
|
24
116
|
class DataPath:
|
|
25
117
|
steps_log: Path
|
|
26
118
|
result_json: Path
|
|
27
119
|
coverage_log: Path
|
|
28
120
|
screenshots_dir: Path
|
|
121
|
+
property_exec_info: Path
|
|
29
122
|
|
|
30
123
|
|
|
31
124
|
class BugReportGenerator:
|
|
@@ -33,26 +126,66 @@ class BugReportGenerator:
|
|
|
33
126
|
Generate HTML format bug reports
|
|
34
127
|
"""
|
|
35
128
|
|
|
36
|
-
|
|
129
|
+
_cov_trend: Deque[CovData] = None
|
|
130
|
+
_test_result: TestResult = None
|
|
131
|
+
_take_screenshots: bool = None
|
|
132
|
+
_data_path: DataPath = None
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def cov_trend(self):
|
|
136
|
+
if self._cov_trend is not None:
|
|
137
|
+
return self._cov_trend
|
|
138
|
+
|
|
139
|
+
# Parse coverage data
|
|
140
|
+
if not self.data_path.coverage_log.exists():
|
|
141
|
+
logger.error(f"{self.data_path.coverage_log} not exists")
|
|
142
|
+
|
|
143
|
+
cov_trend = list()
|
|
144
|
+
|
|
145
|
+
with open(self.data_path.coverage_log, "r", encoding="utf-8") as f:
|
|
146
|
+
for line in f:
|
|
147
|
+
if not line.strip():
|
|
148
|
+
continue
|
|
149
|
+
|
|
150
|
+
coverage_data = json.loads(line)
|
|
151
|
+
cov_trend.append(coverage_data)
|
|
152
|
+
self._cov_trend = cov_trend
|
|
153
|
+
return self._cov_trend
|
|
154
|
+
|
|
155
|
+
@property
|
|
156
|
+
def take_screenshots(self) -> bool:
|
|
157
|
+
"""Whether the `--take-screenshots` enabled. Should we report the screenshots?
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
bool: Whether the `--take-screenshots` enabled.
|
|
161
|
+
"""
|
|
162
|
+
if self._take_screenshots is None:
|
|
163
|
+
self._take_screenshots = self.data_path.screenshots_dir.exists()
|
|
164
|
+
return self._take_screenshots
|
|
165
|
+
|
|
166
|
+
@property
|
|
167
|
+
def test_result(self) -> TestResult:
|
|
168
|
+
if self._test_result is not None:
|
|
169
|
+
return self._test_result
|
|
170
|
+
|
|
171
|
+
if not self.data_path.result_json.exists():
|
|
172
|
+
logger.error(f"{self.data_path.result_json} not found")
|
|
173
|
+
with open(self.data_path.result_json, "r", encoding="utf-8") as f:
|
|
174
|
+
self._test_result: TestResult = json.load(f)
|
|
175
|
+
|
|
176
|
+
return self._test_result
|
|
177
|
+
|
|
178
|
+
def __init__(self, result_dir=None):
|
|
37
179
|
"""
|
|
38
180
|
Initialize the bug report generator
|
|
39
181
|
|
|
40
182
|
Args:
|
|
41
183
|
result_dir: Directory path containing test results
|
|
42
184
|
"""
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
self.data_path: DataPath = DataPath(
|
|
47
|
-
steps_log=self.result_dir / f"output_{self.log_timestamp}" / "steps.log",
|
|
48
|
-
result_json=self.result_dir / f"result_{self.log_timestamp}.json",
|
|
49
|
-
coverage_log=self.result_dir / f"output_{self.log_timestamp}" / "coverage.log",
|
|
50
|
-
screenshots_dir=self.result_dir / f"output_{self.log_timestamp}" / "screenshots"
|
|
51
|
-
)
|
|
185
|
+
if result_dir is not None:
|
|
186
|
+
self._setup_paths(result_dir)
|
|
52
187
|
|
|
53
|
-
self.
|
|
54
|
-
|
|
55
|
-
self.take_screenshots = self._detect_screenshots_setting()
|
|
188
|
+
self.executor = ThreadPoolExecutor(max_workers=128)
|
|
56
189
|
|
|
57
190
|
# Set up Jinja2 environment
|
|
58
191
|
# First try to load templates from the package
|
|
@@ -75,16 +208,48 @@ class BugReportGenerator:
|
|
|
75
208
|
autoescape=select_autoescape(['html', 'xml'])
|
|
76
209
|
)
|
|
77
210
|
|
|
211
|
+
def _setup_paths(self, result_dir):
|
|
212
|
+
"""
|
|
213
|
+
Setup paths for a given result directory
|
|
78
214
|
|
|
79
|
-
|
|
215
|
+
Args:
|
|
216
|
+
result_dir: Directory path containing test results
|
|
217
|
+
"""
|
|
218
|
+
self.result_dir = Path(result_dir)
|
|
219
|
+
self.log_timestamp = self.result_dir.name.split("_", 1)[1]
|
|
220
|
+
|
|
221
|
+
self.data_path: DataPath = DataPath(
|
|
222
|
+
steps_log=self.result_dir / f"output_{self.log_timestamp}" / "steps.log",
|
|
223
|
+
result_json=self.result_dir / f"result_{self.log_timestamp}.json",
|
|
224
|
+
coverage_log=self.result_dir / f"output_{self.log_timestamp}" / "coverage.log",
|
|
225
|
+
screenshots_dir=self.result_dir / f"output_{self.log_timestamp}" / "screenshots",
|
|
226
|
+
property_exec_info=self.result_dir / f"property_exec_info_{self.log_timestamp}.json"
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
self.screenshots = deque()
|
|
230
|
+
|
|
231
|
+
def generate_report(self, result_dir_path=None):
|
|
80
232
|
"""
|
|
81
233
|
Generate bug report and save to result directory
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
result_dir_path: Directory path containing test results (optional)
|
|
237
|
+
If not provided, uses the path from initialization
|
|
82
238
|
"""
|
|
83
239
|
try:
|
|
240
|
+
# Setup paths if result_dir_path is provided
|
|
241
|
+
if result_dir_path is not None:
|
|
242
|
+
self._setup_paths(result_dir_path)
|
|
243
|
+
|
|
244
|
+
# Check if paths are properly set up
|
|
245
|
+
if not hasattr(self, 'result_dir') or self.result_dir is None:
|
|
246
|
+
raise ValueError(
|
|
247
|
+
"No result directory specified. Please provide result_dir_path or initialize with a directory.")
|
|
248
|
+
|
|
84
249
|
logger.debug("Starting bug report generation")
|
|
85
250
|
|
|
86
251
|
# Collect test data
|
|
87
|
-
test_data = self._collect_test_data()
|
|
252
|
+
test_data: ReportData = self._collect_test_data()
|
|
88
253
|
|
|
89
254
|
# Generate HTML report
|
|
90
255
|
html_content = self._generate_html_report(test_data)
|
|
@@ -95,170 +260,206 @@ class BugReportGenerator:
|
|
|
95
260
|
f.write(html_content)
|
|
96
261
|
|
|
97
262
|
logger.debug(f"Bug report saved to: {report_path}")
|
|
263
|
+
return str(report_path)
|
|
98
264
|
|
|
99
265
|
except Exception as e:
|
|
100
266
|
logger.error(f"Error generating bug report: {e}")
|
|
267
|
+
finally:
|
|
268
|
+
self.executor.shutdown()
|
|
101
269
|
|
|
102
|
-
def _collect_test_data(self):
|
|
270
|
+
def _collect_test_data(self) -> ReportData:
|
|
103
271
|
"""
|
|
104
272
|
Collect test data, including results, coverage, etc.
|
|
105
273
|
"""
|
|
106
|
-
data = {
|
|
274
|
+
data: ReportData = {
|
|
107
275
|
"timestamp": self.log_timestamp,
|
|
108
276
|
"bugs_found": 0,
|
|
109
277
|
"executed_events": 0,
|
|
110
278
|
"total_testing_time": 0,
|
|
111
|
-
"first_bug_time": 0,
|
|
112
|
-
"first_precondition_time": 0,
|
|
113
279
|
"coverage": 0,
|
|
114
280
|
"total_activities": [],
|
|
115
281
|
"tested_activities": [],
|
|
282
|
+
"all_properties_count": 0,
|
|
283
|
+
"executed_properties_count": 0,
|
|
116
284
|
"property_violations": [],
|
|
117
285
|
"property_stats": [],
|
|
118
|
-
"
|
|
119
|
-
"
|
|
286
|
+
"property_error_details": {},
|
|
287
|
+
"screenshot_info": {},
|
|
288
|
+
"coverage_trend": [],
|
|
289
|
+
"property_execution_trend": [],
|
|
290
|
+
"activity_count_history": {}
|
|
120
291
|
}
|
|
121
292
|
|
|
122
293
|
# Parse steps.log file to get test step numbers and screenshot mappings
|
|
123
|
-
steps_log_path = self.data_path.steps_log
|
|
124
294
|
property_violations = {} # Store multiple violation records for each property
|
|
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
|
-
except Exception as e:
|
|
165
|
-
logger.error(f"Error processing ScriptInfo step {step_index}: {e}")
|
|
166
|
-
|
|
167
|
-
# Store first and last step for time calculation
|
|
168
|
-
if step_index == 1:
|
|
169
|
-
first_step_time = step_data["Time"]
|
|
170
|
-
last_step_time = step_data["Time"]
|
|
171
|
-
|
|
172
|
-
# Set the monkey events count
|
|
173
|
-
data["executed_events"] = monkey_events_count
|
|
174
|
-
|
|
175
|
-
# Calculate test time
|
|
176
|
-
if step_index > 0:
|
|
295
|
+
executed_properties_by_step = {} # Track executed properties at each step: {step_count: set()}
|
|
296
|
+
executed_properties = set() # Track unique executed properties
|
|
297
|
+
|
|
298
|
+
if not self.data_path.steps_log.exists():
|
|
299
|
+
logger.error(f"{self.data_path.steps_log} not exists")
|
|
300
|
+
return
|
|
301
|
+
|
|
302
|
+
current_property = None
|
|
303
|
+
current_test = {}
|
|
304
|
+
step_index = 0
|
|
305
|
+
monkey_events_count = 0 # Track monkey events separately
|
|
306
|
+
|
|
307
|
+
with open(self.data_path.steps_log, "r", encoding="utf-8") as f:
|
|
308
|
+
# Track current test state
|
|
309
|
+
|
|
310
|
+
for step_index, line in enumerate(f, start=1):
|
|
311
|
+
step_data = self._parse_step_data(line)
|
|
312
|
+
|
|
313
|
+
if not step_data:
|
|
314
|
+
continue
|
|
315
|
+
|
|
316
|
+
step_type = step_data.get("Type", "")
|
|
317
|
+
screenshot = step_data.get("Screenshot", "")
|
|
318
|
+
info = step_data.get("Info", {})
|
|
319
|
+
|
|
320
|
+
# Count Monkey events separately
|
|
321
|
+
if step_type == "Monkey":
|
|
322
|
+
monkey_events_count += 1
|
|
323
|
+
|
|
324
|
+
# If screenshots are enabled, mark the screenshot
|
|
325
|
+
if self.take_screenshots and step_data["Screenshot"]:
|
|
326
|
+
self.executor.submit(self._mark_screenshot, step_data)
|
|
327
|
+
|
|
328
|
+
# Collect detailed information for each screenshot
|
|
329
|
+
if screenshot and screenshot not in data["screenshot_info"]:
|
|
330
|
+
self._add_screenshot_info(step_data, step_index, data)
|
|
331
|
+
|
|
332
|
+
# Process ScriptInfo for property violations and execution tracking
|
|
333
|
+
if step_type == "ScriptInfo":
|
|
177
334
|
try:
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
335
|
+
property_name = info.get("propName", "")
|
|
336
|
+
state = info.get("state", "")
|
|
337
|
+
|
|
338
|
+
# Track executed properties (properties that have been started)
|
|
339
|
+
if property_name and state == "start":
|
|
340
|
+
executed_properties.add(property_name)
|
|
341
|
+
# Record the monkey steps count for this property execution
|
|
342
|
+
executed_properties_by_step[monkey_events_count] = executed_properties.copy()
|
|
343
|
+
|
|
344
|
+
current_property, current_test = self._process_script_info(
|
|
345
|
+
property_name, state, step_index, screenshot,
|
|
346
|
+
current_property, current_test, property_violations
|
|
347
|
+
)
|
|
181
348
|
except Exception as e:
|
|
182
|
-
logger.error(f"Error
|
|
349
|
+
logger.error(f"Error processing ScriptInfo step {step_index}: {e}")
|
|
183
350
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
351
|
+
# Store first and last step for time calculation
|
|
352
|
+
if step_index == 1:
|
|
353
|
+
first_step_time = step_data["Time"]
|
|
354
|
+
last_step_time = step_data["Time"]
|
|
355
|
+
|
|
356
|
+
# Set the monkey events count correctly
|
|
357
|
+
data["executed_events"] = monkey_events_count
|
|
358
|
+
|
|
359
|
+
# Calculate test time
|
|
360
|
+
if first_step_time and last_step_time:
|
|
361
|
+
def _get_datetime(raw_datetime) -> datetime:
|
|
362
|
+
return datetime.strptime(raw_datetime, r"%Y-%m-%d %H:%M:%S.%f")
|
|
363
|
+
|
|
364
|
+
test_time = _get_datetime(last_step_time) - _get_datetime(first_step_time)
|
|
190
365
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
data["bugs_found"] += 1
|
|
366
|
+
total_seconds = int(test_time.total_seconds())
|
|
367
|
+
hours, remainder = divmod(total_seconds, 3600)
|
|
368
|
+
minutes, seconds = divmod(remainder, 60)
|
|
369
|
+
data["total_testing_time"] = f"{hours:02d}:{minutes:02d}:{seconds:02d}"
|
|
196
370
|
|
|
197
|
-
|
|
198
|
-
|
|
371
|
+
# Calculate bug count directly from result data
|
|
372
|
+
for property_name, test_result in self.test_result.items():
|
|
373
|
+
# Check if failed or error
|
|
374
|
+
if test_result["fail"] > 0 or test_result["error"] > 0:
|
|
375
|
+
data["bugs_found"] += 1
|
|
376
|
+
|
|
377
|
+
# Store the raw result data for direct use in HTML template
|
|
378
|
+
data["property_stats"] = self.test_result
|
|
379
|
+
|
|
380
|
+
# Calculate properties statistics
|
|
381
|
+
data["all_properties_count"] = len(self.test_result)
|
|
382
|
+
data["executed_properties_count"] = sum(1 for result in self.test_result.values() if result.get("executed", 0) > 0)
|
|
199
383
|
|
|
200
384
|
# Process coverage data
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
385
|
+
data["coverage_trend"] = self.cov_trend
|
|
386
|
+
|
|
387
|
+
if self.cov_trend:
|
|
388
|
+
final_trend = self.cov_trend[-1]
|
|
389
|
+
data["coverage"] = final_trend["coverage"]
|
|
390
|
+
data["total_activities"] = final_trend["totalActivities"]
|
|
391
|
+
data["tested_activities"] = final_trend["testedActivities"]
|
|
392
|
+
data["total_activities_count"] = final_trend["totalActivitiesCount"]
|
|
393
|
+
data["tested_activities_count"] = final_trend["testedActivitiesCount"]
|
|
394
|
+
data["activity_count_history"] = final_trend["activityCountHistory"]
|
|
395
|
+
|
|
396
|
+
# Generate property execution trend aligned with coverage trend
|
|
397
|
+
data["property_execution_trend"] = self._generate_property_execution_trend(executed_properties_by_step)
|
|
214
398
|
|
|
215
399
|
# Generate Property Violations list
|
|
216
400
|
self._generate_property_violations_list(property_violations, data)
|
|
217
401
|
|
|
402
|
+
# Load error details for properties with fail/error state
|
|
403
|
+
data["property_error_details"] = self._load_property_error_details()
|
|
404
|
+
|
|
218
405
|
return data
|
|
219
406
|
|
|
220
407
|
def _parse_step_data(self, raw_step_info: str) -> StepData:
|
|
221
|
-
step_data = json.loads(raw_step_info)
|
|
222
|
-
step_data["Info"] = json.loads(step_data
|
|
408
|
+
step_data: StepData = json.loads(raw_step_info)
|
|
409
|
+
step_data["Info"] = json.loads(step_data["Info"])
|
|
223
410
|
return step_data
|
|
224
411
|
|
|
225
412
|
def _mark_screenshot(self, step_data: StepData):
|
|
226
|
-
|
|
227
|
-
|
|
413
|
+
try:
|
|
414
|
+
step_type = step_data["Type"]
|
|
415
|
+
screenshot_name = step_data["Screenshot"]
|
|
416
|
+
if not screenshot_name:
|
|
417
|
+
return
|
|
418
|
+
|
|
419
|
+
if step_type == "Monkey":
|
|
228
420
|
act = step_data["Info"].get("act")
|
|
229
421
|
pos = step_data["Info"].get("pos")
|
|
230
|
-
screenshot_name = step_data["Screenshot"]
|
|
231
422
|
if act in ["CLICK", "LONG_CLICK"] or act.startswith("SCROLL"):
|
|
232
|
-
|
|
233
|
-
if screenshot_path.exists():
|
|
234
|
-
self._mark_screenshot_interaction(screenshot_path, act, pos)
|
|
235
|
-
except Exception as e:
|
|
236
|
-
logger.error(f"Error processing Monkey step: {e}")
|
|
423
|
+
self._mark_screenshot_interaction(step_type, screenshot_name, act, pos)
|
|
237
424
|
|
|
425
|
+
elif step_type == "Script":
|
|
426
|
+
act = step_data["Info"].get("method")
|
|
427
|
+
pos = step_data["Info"].get("params")
|
|
428
|
+
if act in ["click", "setText", "swipe"]:
|
|
429
|
+
self._mark_screenshot_interaction(step_type, screenshot_name, act, pos)
|
|
238
430
|
|
|
239
|
-
|
|
431
|
+
except Exception as e:
|
|
432
|
+
logger.error(f"Error when marking screenshots: {e}")
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def _mark_screenshot_interaction(self, step_type: str, screenshot_name: str, action_type: str, position: Union[List, tuple]) -> bool:
|
|
240
436
|
"""
|
|
241
|
-
|
|
437
|
+
Mark interaction on screenshot with colored rectangle
|
|
242
438
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
439
|
+
Args:
|
|
440
|
+
step_type (str): Type of the step (Monkey or Script)
|
|
441
|
+
screenshot_name (str): Name of the screenshot file
|
|
442
|
+
action_type (str): Type of action (CLICK/LONG_CLICK/SCROLL for Monkey, click/setText/swipe for Script)
|
|
443
|
+
position: Position coordinates or parameters (format varies by action type)
|
|
247
444
|
|
|
248
|
-
|
|
249
|
-
|
|
445
|
+
Returns:
|
|
446
|
+
bool: True if marking was successful, False otherwise
|
|
250
447
|
"""
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
448
|
+
screenshot_path: Path = self.data_path.screenshots_dir / screenshot_name
|
|
449
|
+
if not screenshot_path.exists():
|
|
450
|
+
logger.error(f"Screenshot file {screenshot_path} not exists.")
|
|
451
|
+
return False
|
|
254
452
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
453
|
+
img = Image.open(screenshot_path).convert("RGB")
|
|
454
|
+
draw = ImageDraw.Draw(img)
|
|
455
|
+
line_width = 5
|
|
258
456
|
|
|
259
|
-
|
|
457
|
+
if step_type == "Monkey":
|
|
458
|
+
if len(position) < 4:
|
|
459
|
+
logger.warning(f"Monkey action requires 4 coordinates, got {len(position)}. Skip drawing.")
|
|
460
|
+
return False
|
|
260
461
|
|
|
261
|
-
|
|
462
|
+
x1, y1, x2, y2 = map(int, position[:4])
|
|
262
463
|
|
|
263
464
|
if action_type == "CLICK":
|
|
264
465
|
for i in range(line_width):
|
|
@@ -270,56 +471,64 @@ class BugReportGenerator:
|
|
|
270
471
|
for i in range(line_width):
|
|
271
472
|
draw.rectangle([x1 - i, y1 - i, x2 + i, y2 + i], outline=(0, 255, 0))
|
|
272
473
|
|
|
273
|
-
|
|
274
|
-
|
|
474
|
+
elif step_type == "Script":
|
|
475
|
+
if action_type == "click":
|
|
275
476
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
"""
|
|
283
|
-
Detect if screenshots were enabled during test run.
|
|
284
|
-
Returns True if screenshots were taken, False otherwise.
|
|
285
|
-
"""
|
|
286
|
-
return self.data_path.screenshots_dir.exists()
|
|
477
|
+
if len(position) < 2:
|
|
478
|
+
logger.warning(f"Script click action requires 2 coordinates, got {len(position)}. Skip drawing.")
|
|
479
|
+
return False
|
|
480
|
+
|
|
481
|
+
x, y = map(float, position[:2])
|
|
482
|
+
x1, y1, x2, y2 = x - 50, y - 50, x + 50, y + 50
|
|
287
483
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
"tested_activities_count": coverage_data.get("testedActivitiesCount", 0)
|
|
304
|
-
})
|
|
305
|
-
last_line = line
|
|
306
|
-
except Exception as e:
|
|
307
|
-
logger.error(f"Error parsing coverage data: {e}")
|
|
308
|
-
continue
|
|
309
|
-
return cov_trend, last_line
|
|
484
|
+
for i in range(line_width):
|
|
485
|
+
draw.rectangle([x1 - i, y1 - i, x2 + i, y2 + i], outline=(255, 0, 0))
|
|
486
|
+
|
|
487
|
+
elif action_type == "swipe":
|
|
488
|
+
|
|
489
|
+
if len(position) < 4:
|
|
490
|
+
logger.warning(f"Script swipe action requires 4 coordinates, got {len(position)}. Skip drawing.")
|
|
491
|
+
return False
|
|
492
|
+
|
|
493
|
+
x1, y1, x2, y2 = map(float, position[:4])
|
|
494
|
+
|
|
495
|
+
# mark start and end positions with rectangles
|
|
496
|
+
start_x1, start_y1, start_x2, start_y2 = x1 - 50, y1 - 50, x1 + 50, y1 + 50
|
|
497
|
+
for i in range(line_width):
|
|
498
|
+
draw.rectangle([start_x1 - i, start_y1 - i, start_x2 + i, start_y2 + i], outline=(255, 0, 0))
|
|
310
499
|
|
|
311
|
-
|
|
500
|
+
end_x1, end_y1, end_x2, end_y2 = x2 - 50, y2 - 50, x2 + 50, y2 + 50
|
|
501
|
+
for i in range(line_width):
|
|
502
|
+
draw.rectangle([end_x1 - i, end_y1 - i, end_x2 + i, end_y2 + i], outline=(255, 0, 0))
|
|
503
|
+
|
|
504
|
+
# draw line between start and end positions
|
|
505
|
+
draw.line([(x1, y1), (x2, y2)], fill=(255, 0, 0), width=line_width)
|
|
506
|
+
|
|
507
|
+
# add text labels for start and end positions
|
|
508
|
+
font = ImageFont.truetype("arial.ttf", 80)
|
|
509
|
+
|
|
510
|
+
# draw "start" at start position
|
|
511
|
+
draw.text((x1 - 20, y1 - 70), "start", fill=(255, 0, 0), font=font)
|
|
512
|
+
|
|
513
|
+
# draw "end" at end position
|
|
514
|
+
draw.text((x2 - 15, y2 - 70), "end", fill=(255, 0, 0), font=font)
|
|
515
|
+
|
|
516
|
+
img.save(screenshot_path)
|
|
517
|
+
return True
|
|
518
|
+
|
|
519
|
+
def _generate_html_report(self, data: ReportData):
|
|
312
520
|
"""
|
|
313
521
|
Generate HTML format bug report
|
|
314
522
|
"""
|
|
315
523
|
try:
|
|
316
524
|
# Format timestamp for display
|
|
317
|
-
timestamp = datetime.
|
|
525
|
+
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
318
526
|
|
|
319
527
|
# Ensure coverage_trend has data
|
|
320
528
|
if not data["coverage_trend"]:
|
|
321
529
|
logger.warning("No coverage trend data")
|
|
322
|
-
|
|
530
|
+
# Use the same field names as in coverage.log file
|
|
531
|
+
data["coverage_trend"] = [{"stepsCount": 0, "coverage": 0, "testedActivitiesCount": 0}]
|
|
323
532
|
|
|
324
533
|
# Convert coverage_trend to JSON string, ensuring all data points are included
|
|
325
534
|
coverage_trend_json = json.dumps(data["coverage_trend"])
|
|
@@ -332,18 +541,22 @@ class BugReportGenerator:
|
|
|
332
541
|
'total_testing_time': data["total_testing_time"],
|
|
333
542
|
'executed_events': data["executed_events"],
|
|
334
543
|
'coverage_percent': round(data["coverage"], 2),
|
|
335
|
-
'
|
|
336
|
-
'
|
|
337
|
-
'
|
|
338
|
-
'
|
|
339
|
-
'
|
|
340
|
-
'
|
|
544
|
+
'total_activities_count': data["total_activities_count"],
|
|
545
|
+
'tested_activities_count': data["tested_activities_count"],
|
|
546
|
+
'tested_activities': data["tested_activities"],
|
|
547
|
+
'total_activities': data["total_activities"],
|
|
548
|
+
'all_properties_count': data["all_properties_count"],
|
|
549
|
+
'executed_properties_count': data["executed_properties_count"],
|
|
341
550
|
'items_per_page': 10, # Items to display per page
|
|
342
551
|
'screenshots': self.screenshots,
|
|
343
552
|
'property_violations': data["property_violations"],
|
|
344
553
|
'property_stats': data["property_stats"],
|
|
554
|
+
'property_error_details': data["property_error_details"],
|
|
345
555
|
'coverage_data': coverage_trend_json,
|
|
346
|
-
'take_screenshots': self.take_screenshots # Pass screenshot setting to template
|
|
556
|
+
'take_screenshots': self.take_screenshots, # Pass screenshot setting to template
|
|
557
|
+
'property_execution_trend': data["property_execution_trend"],
|
|
558
|
+
'property_execution_data': json.dumps(data["property_execution_trend"]),
|
|
559
|
+
'activity_count_history': data["activity_count_history"]
|
|
347
560
|
}
|
|
348
561
|
|
|
349
562
|
# Check if template exists, if not create it
|
|
@@ -361,66 +574,52 @@ class BugReportGenerator:
|
|
|
361
574
|
logger.error(f"Error rendering template: {e}")
|
|
362
575
|
raise
|
|
363
576
|
|
|
364
|
-
def _add_screenshot_info(self,
|
|
577
|
+
def _add_screenshot_info(self, step_data: StepData, step_index: int, data: Dict):
|
|
365
578
|
"""
|
|
366
579
|
Add screenshot information to data structure
|
|
367
|
-
|
|
580
|
+
|
|
368
581
|
Args:
|
|
369
|
-
|
|
370
|
-
step_type: Type of step (Monkey, Script, ScriptInfo)
|
|
371
|
-
info: Step information dictionary
|
|
582
|
+
step_data: data for the current step
|
|
372
583
|
step_index: Current step index
|
|
373
|
-
relative_path: Relative path to screenshots directory
|
|
374
584
|
data: Data dictionary to update
|
|
375
585
|
"""
|
|
376
|
-
|
|
377
|
-
caption = ""
|
|
586
|
+
caption = ""
|
|
378
587
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
})
|
|
403
|
-
|
|
404
|
-
except Exception as e:
|
|
405
|
-
logger.error(f"Error parsing screenshot info: {e}")
|
|
406
|
-
data["screenshot_info"][screenshot] = {
|
|
407
|
-
"type": step_type,
|
|
408
|
-
"caption": step_type,
|
|
409
|
-
"step_index": step_index
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
screenshot_caption = data["screenshot_info"][screenshot].get('caption', '')
|
|
413
|
-
self.screenshots.append({
|
|
414
|
-
'id': step_index,
|
|
415
|
-
'path': f"{relative_path}/{screenshot}",
|
|
416
|
-
'caption': f"{step_index}. {screenshot_caption}"
|
|
417
|
-
})
|
|
588
|
+
if step_data["Type"] == "Monkey":
|
|
589
|
+
# Extract 'act' attribute for Monkey type and add MonkeyStepsCount
|
|
590
|
+
monkey_steps_count = step_data.get('MonkeyStepsCount', 'N/A')
|
|
591
|
+
action = step_data['Info'].get('act', 'N/A')
|
|
592
|
+
caption = f"Monkey Step {monkey_steps_count}: {action}"
|
|
593
|
+
elif step_data["Type"] == "Script":
|
|
594
|
+
# Extract 'method' attribute for Script type
|
|
595
|
+
caption = f"{step_data['Info'].get('method', 'N/A')}"
|
|
596
|
+
elif step_data["Type"] == "ScriptInfo":
|
|
597
|
+
# Extract 'propName' and 'state' attributes for ScriptInfo type
|
|
598
|
+
prop_name = step_data["Info"].get('propName', '')
|
|
599
|
+
state = step_data["Info"].get('state', 'N/A')
|
|
600
|
+
caption = f"{prop_name}: {state}" if prop_name else f"{state}"
|
|
601
|
+
|
|
602
|
+
screenshot_name = step_data["Screenshot"]
|
|
603
|
+
# Use relative path string instead of Path object
|
|
604
|
+
relative_screenshot_path = f"output_{self.log_timestamp}/screenshots/{screenshot_name}"
|
|
605
|
+
|
|
606
|
+
data["screenshot_info"][screenshot_name] = {
|
|
607
|
+
"type": step_data["Type"],
|
|
608
|
+
"caption": caption,
|
|
609
|
+
"step_index": step_index
|
|
610
|
+
}
|
|
418
611
|
|
|
419
|
-
|
|
420
|
-
|
|
612
|
+
self.screenshots.append({
|
|
613
|
+
'id': step_index,
|
|
614
|
+
'path': relative_screenshot_path, # Now using string path
|
|
615
|
+
'caption': f"{step_index}. {caption}"
|
|
616
|
+
})
|
|
617
|
+
|
|
618
|
+
def _process_script_info(self, property_name: str, state: str, step_index: int, screenshot: str,
|
|
619
|
+
current_property: str, current_test: Dict, property_violations: Dict) -> tuple:
|
|
421
620
|
"""
|
|
422
621
|
Process ScriptInfo step for property violations tracking
|
|
423
|
-
|
|
622
|
+
|
|
424
623
|
Args:
|
|
425
624
|
property_name: Property name from ScriptInfo
|
|
426
625
|
state: State from ScriptInfo (start, pass, fail, error)
|
|
@@ -429,7 +628,7 @@ class BugReportGenerator:
|
|
|
429
628
|
current_property: Currently tracked property
|
|
430
629
|
current_test: Current test data
|
|
431
630
|
property_violations: Dictionary to store violations
|
|
432
|
-
|
|
631
|
+
|
|
433
632
|
Returns:
|
|
434
633
|
tuple: (updated_current_property, updated_current_test)
|
|
435
634
|
"""
|
|
@@ -464,13 +663,13 @@ class BugReportGenerator:
|
|
|
464
663
|
# Reset current test
|
|
465
664
|
current_property = None
|
|
466
665
|
current_test = {}
|
|
467
|
-
|
|
666
|
+
|
|
468
667
|
return current_property, current_test
|
|
469
668
|
|
|
470
669
|
def _generate_property_violations_list(self, property_violations: Dict, data: Dict):
|
|
471
670
|
"""
|
|
472
671
|
Generate property violations list from collected violation data
|
|
473
|
-
|
|
672
|
+
|
|
474
673
|
Args:
|
|
475
674
|
property_violations: Dictionary containing property violations
|
|
476
675
|
data: Data dictionary to update with property violations list
|
|
@@ -489,3 +688,141 @@ class BugReportGenerator:
|
|
|
489
688
|
"postcondition_page": end_step
|
|
490
689
|
})
|
|
491
690
|
index += 1
|
|
691
|
+
|
|
692
|
+
def _load_property_error_details(self) -> Dict[str, List[Dict]]:
|
|
693
|
+
"""
|
|
694
|
+
Load property execution error details from property_exec_info file
|
|
695
|
+
|
|
696
|
+
Returns:
|
|
697
|
+
Dict[str, List[Dict]]: Mapping of property names to their error tracebacks with context
|
|
698
|
+
"""
|
|
699
|
+
if not self.data_path.property_exec_info.exists():
|
|
700
|
+
logger.warning(f"Property exec info file {self.data_path.property_exec_info} not found")
|
|
701
|
+
return {}
|
|
702
|
+
|
|
703
|
+
try:
|
|
704
|
+
property_exec_infos = self._parse_property_exec_infos()
|
|
705
|
+
return self._group_errors_by_property(property_exec_infos)
|
|
706
|
+
|
|
707
|
+
except Exception as e:
|
|
708
|
+
logger.error(f"Error reading property exec info file: {e}")
|
|
709
|
+
return {}
|
|
710
|
+
|
|
711
|
+
def _parse_property_exec_infos(self) -> List[PropertyExecInfo]:
|
|
712
|
+
"""Parse property execution info from file"""
|
|
713
|
+
exec_infos = []
|
|
714
|
+
|
|
715
|
+
with open(self.data_path.property_exec_info, "r", encoding="utf-8") as f:
|
|
716
|
+
for line_number, line in enumerate(f, 1):
|
|
717
|
+
line = line.strip()
|
|
718
|
+
if not line:
|
|
719
|
+
continue
|
|
720
|
+
|
|
721
|
+
try:
|
|
722
|
+
exec_info_data = json.loads(line)
|
|
723
|
+
prop_name = exec_info_data.get("propName", "")
|
|
724
|
+
state = exec_info_data.get("state", "")
|
|
725
|
+
tb = exec_info_data.get("tb", "")
|
|
726
|
+
start_steps_count = exec_info_data.get("startStepsCount", 0)
|
|
727
|
+
|
|
728
|
+
exec_info = PropertyExecInfo(
|
|
729
|
+
prop_name=prop_name,
|
|
730
|
+
state=state,
|
|
731
|
+
traceback=tb,
|
|
732
|
+
start_steps_count=start_steps_count
|
|
733
|
+
)
|
|
734
|
+
|
|
735
|
+
if exec_info.is_error_state() and prop_name and tb:
|
|
736
|
+
exec_infos.append(exec_info)
|
|
737
|
+
|
|
738
|
+
except json.JSONDecodeError as e:
|
|
739
|
+
logger.warning(f"Failed to parse property exec info line {line_number}: {line[:100]}... Error: {e}")
|
|
740
|
+
continue
|
|
741
|
+
|
|
742
|
+
return exec_infos
|
|
743
|
+
|
|
744
|
+
def _group_errors_by_property(self, exec_infos: List[PropertyExecInfo]) -> Dict[str, List[Dict]]:
|
|
745
|
+
"""Group errors by property name and deduplicate"""
|
|
746
|
+
error_details = {}
|
|
747
|
+
|
|
748
|
+
for exec_info in exec_infos:
|
|
749
|
+
prop_name = exec_info.prop_name
|
|
750
|
+
|
|
751
|
+
if prop_name not in error_details:
|
|
752
|
+
error_details[prop_name] = {}
|
|
753
|
+
|
|
754
|
+
error_hash = exec_info.get_error_hash()
|
|
755
|
+
|
|
756
|
+
if error_hash in error_details[prop_name]:
|
|
757
|
+
# Error already exists, add occurrence
|
|
758
|
+
error_details[prop_name][error_hash].add_occurrence(exec_info.start_steps_count)
|
|
759
|
+
else:
|
|
760
|
+
# New error, create entry
|
|
761
|
+
error_details[prop_name][error_hash] = exec_info
|
|
762
|
+
|
|
763
|
+
# Convert to template-compatible format
|
|
764
|
+
result = {}
|
|
765
|
+
for prop_name, hash_dict in error_details.items():
|
|
766
|
+
result[prop_name] = []
|
|
767
|
+
for exec_info in hash_dict.values():
|
|
768
|
+
result[prop_name].append({
|
|
769
|
+
"state": exec_info.state,
|
|
770
|
+
"traceback": exec_info.traceback,
|
|
771
|
+
"occurrence_count": exec_info.occurrence_count,
|
|
772
|
+
"short_description": exec_info.short_description,
|
|
773
|
+
"startStepsCountList": exec_info.start_steps_count_list
|
|
774
|
+
})
|
|
775
|
+
|
|
776
|
+
# Sort by earliest startStepsCount, then by occurrence count (descending)
|
|
777
|
+
result[prop_name].sort(key=lambda x: (min(x["startStepsCountList"]), -x["occurrence_count"]))
|
|
778
|
+
|
|
779
|
+
return result
|
|
780
|
+
|
|
781
|
+
def _generate_property_execution_trend(self, executed_properties_by_step: Dict[int, set]) -> List[Dict]:
|
|
782
|
+
"""
|
|
783
|
+
Generate property execution trend aligned with coverage trend
|
|
784
|
+
|
|
785
|
+
Args:
|
|
786
|
+
executed_properties_by_step: Dictionary containing executed properties at each step
|
|
787
|
+
|
|
788
|
+
Returns:
|
|
789
|
+
List[Dict]: Property execution trend data aligned with coverage trend
|
|
790
|
+
"""
|
|
791
|
+
property_execution_trend = []
|
|
792
|
+
|
|
793
|
+
# Get step points from coverage trend to ensure alignment
|
|
794
|
+
coverage_step_points = []
|
|
795
|
+
if self.cov_trend:
|
|
796
|
+
coverage_step_points = [cov_data["stepsCount"] for cov_data in self.cov_trend]
|
|
797
|
+
|
|
798
|
+
# If no coverage data, use property execution data points
|
|
799
|
+
if not coverage_step_points and executed_properties_by_step:
|
|
800
|
+
coverage_step_points = sorted(executed_properties_by_step.keys())
|
|
801
|
+
|
|
802
|
+
# Generate property execution data for each coverage step point
|
|
803
|
+
for step_count in coverage_step_points:
|
|
804
|
+
# Find the latest executed properties count up to this step
|
|
805
|
+
executed_count = 0
|
|
806
|
+
latest_step = 0
|
|
807
|
+
|
|
808
|
+
for exec_step in executed_properties_by_step.keys():
|
|
809
|
+
if exec_step <= step_count and exec_step >= latest_step:
|
|
810
|
+
latest_step = exec_step
|
|
811
|
+
executed_count = len(executed_properties_by_step[exec_step])
|
|
812
|
+
|
|
813
|
+
property_execution_trend.append({
|
|
814
|
+
"stepsCount": step_count,
|
|
815
|
+
"executedPropertiesCount": executed_count
|
|
816
|
+
})
|
|
817
|
+
|
|
818
|
+
return property_execution_trend
|
|
819
|
+
|
|
820
|
+
|
|
821
|
+
if __name__ == "__main__":
|
|
822
|
+
print("Generating bug report")
|
|
823
|
+
# OUTPUT_PATH = "<Your output path>"
|
|
824
|
+
OUTPUT_PATH = "P:/Python/Kea2/output/res_2025070814_4842540549"
|
|
825
|
+
|
|
826
|
+
report_generator = BugReportGenerator()
|
|
827
|
+
report_path = report_generator.generate_report(OUTPUT_PATH)
|
|
828
|
+
print(f"bug report generated: {report_path}")
|