Kea2-python 1.0.6b0__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.

Files changed (52) hide show
  1. kea2/__init__.py +3 -0
  2. kea2/absDriver.py +56 -0
  3. kea2/adbUtils.py +554 -0
  4. kea2/assets/config_version.json +16 -0
  5. kea2/assets/fastbot-thirdpart.jar +0 -0
  6. kea2/assets/fastbot_configs/abl.strings +2 -0
  7. kea2/assets/fastbot_configs/awl.strings +3 -0
  8. kea2/assets/fastbot_configs/max.config +7 -0
  9. kea2/assets/fastbot_configs/max.fuzzing.strings +699 -0
  10. kea2/assets/fastbot_configs/max.schema.strings +1 -0
  11. kea2/assets/fastbot_configs/max.strings +3 -0
  12. kea2/assets/fastbot_configs/max.tree.pruning +27 -0
  13. kea2/assets/fastbot_configs/teardown.py +18 -0
  14. kea2/assets/fastbot_configs/widget.block.py +38 -0
  15. kea2/assets/fastbot_libs/arm64-v8a/libfastbot_native.so +0 -0
  16. kea2/assets/fastbot_libs/armeabi-v7a/libfastbot_native.so +0 -0
  17. kea2/assets/fastbot_libs/x86/libfastbot_native.so +0 -0
  18. kea2/assets/fastbot_libs/x86_64/libfastbot_native.so +0 -0
  19. kea2/assets/framework.jar +0 -0
  20. kea2/assets/kea2-thirdpart.jar +0 -0
  21. kea2/assets/monkeyq.jar +0 -0
  22. kea2/assets/quicktest.py +126 -0
  23. kea2/cli.py +320 -0
  24. kea2/fastbotManager.py +267 -0
  25. kea2/fastbotx/ActivityTimes.py +52 -0
  26. kea2/fastbotx/ReuseEntry.py +74 -0
  27. kea2/fastbotx/ReuseModel.py +63 -0
  28. kea2/fastbotx/__init__.py +7 -0
  29. kea2/fbm_parser.py +871 -0
  30. kea2/fs_lock.py +131 -0
  31. kea2/kea2_api.py +166 -0
  32. kea2/keaUtils.py +1112 -0
  33. kea2/kea_launcher.py +319 -0
  34. kea2/logWatcher.py +92 -0
  35. kea2/mixin.py +22 -0
  36. kea2/report/__init__.py +0 -0
  37. kea2/report/bug_report_generator.py +793 -0
  38. kea2/report/mixin.py +482 -0
  39. kea2/report/report_merger.py +797 -0
  40. kea2/report/templates/bug_report_template.html +3876 -0
  41. kea2/report/templates/merged_bug_report_template.html +3333 -0
  42. kea2/report/utils.py +10 -0
  43. kea2/resultSyncer.py +65 -0
  44. kea2/u2Driver.py +610 -0
  45. kea2/utils.py +184 -0
  46. kea2/version_manager.py +102 -0
  47. kea2_python-1.0.6b0.dist-info/METADATA +447 -0
  48. kea2_python-1.0.6b0.dist-info/RECORD +52 -0
  49. kea2_python-1.0.6b0.dist-info/WHEEL +5 -0
  50. kea2_python-1.0.6b0.dist-info/entry_points.txt +2 -0
  51. kea2_python-1.0.6b0.dist-info/licenses/LICENSE +16 -0
  52. kea2_python-1.0.6b0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,793 @@
1
+ import json
2
+ from datetime import datetime
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+ from typing import Dict, Tuple, TypedDict, List, Deque, NewType, Union, Optional
6
+ from collections import deque
7
+ from concurrent.futures import ThreadPoolExecutor
8
+
9
+ from jinja2 import Environment, FileSystemLoader, select_autoescape, PackageLoader
10
+ from ..utils import getLogger, catchException
11
+ from .mixin import CrashAnrMixin, PathParserMixin, ScreenshotsMixin
12
+ from .utils import thread_pool
13
+
14
+ logger = getLogger(__name__)
15
+
16
+
17
+ class StepData(TypedDict):
18
+ # The type of the action (Monkey / Script / Script Info)
19
+ Type: str
20
+ # The steps of monkey event when the action happened
21
+ # ps: since we insert script actions into monkey actions. Total actions count >= Monkey actions count
22
+ MonkeyStepsCount: int
23
+ # The time stamp of the action
24
+ Time: str
25
+ # The execution info of the action
26
+ Info: Dict
27
+ # The screenshot of the action
28
+ Screenshot: str
29
+
30
+
31
+ class CovData(TypedDict):
32
+ stepsCount: int
33
+ coverage: float
34
+ totalActivitiesCount: int
35
+ testedActivitiesCount: int
36
+ totalActivities: List[str]
37
+ testedActivities: List[str]
38
+ activityCountHistory: Dict[str, int]
39
+
40
+
41
+ class ReportData(TypedDict):
42
+ timestamp: str
43
+ bugs_found: int
44
+ executed_events: int
45
+ total_testing_time: float
46
+ coverage: float
47
+ total_activities_count: int
48
+ tested_activities_count: int
49
+ total_activities: List
50
+ tested_activities: List
51
+ all_properties_count: int
52
+ executed_properties_count: int
53
+ property_violations: List[Dict]
54
+ property_stats: List
55
+ property_error_details: Dict[str, List[Dict]] # Support multiple errors per property
56
+ screenshot_info: Dict
57
+ coverage_trend: List
58
+ property_execution_trend: List # Track executed properties count over steps
59
+ activity_count_history: Dict[str, int] # Activity traversal count from final coverage data
60
+ crash_events: List[Dict] # Crash events from crash-dump.log
61
+ anr_events: List[Dict] # ANR events from crash-dump.log
62
+ kill_apps_events: List[Dict] # kill_apps info events from steps.log
63
+
64
+
65
+ class PropertyExecResult(TypedDict):
66
+ precond_satisfied: int
67
+ executed: int
68
+ fail: int
69
+ error: int
70
+
71
+
72
+ @dataclass
73
+ class PropertyExecInfo:
74
+ """Class representing property execution information from property_exec_info file"""
75
+ prop_name: str
76
+ state: str # start, pass, fail, error
77
+ traceback: str
78
+ start_steps_count: int
79
+ occurrence_count: int = 1
80
+ short_description: str = ""
81
+ start_steps_count_list: List[int] = None
82
+
83
+ def __post_init__(self):
84
+ if self.start_steps_count_list is None:
85
+ self.start_steps_count_list = [self.start_steps_count]
86
+ if not self.short_description and self.traceback:
87
+ self.short_description = self._extract_error_summary(self.traceback)
88
+
89
+ def _extract_error_summary(self, traceback: str) -> str:
90
+ """Extract a short error summary from the full traceback"""
91
+ try:
92
+ lines = traceback.strip().split('\n')
93
+ for line in reversed(lines):
94
+ line = line.strip()
95
+ if line and not line.startswith(' '):
96
+ return line
97
+ return "Unknown error"
98
+ except Exception:
99
+ return "Error parsing traceback"
100
+
101
+ def get_error_hash(self) -> int:
102
+ """Generate hash key for error deduplication"""
103
+ return hash((self.state, self.traceback))
104
+
105
+ def is_error_state(self) -> bool:
106
+ """Check if this is an error or fail state"""
107
+ return self.state in ["fail", "error"]
108
+
109
+ def add_occurrence(self, start_steps_count: int):
110
+ """Add another occurrence of the same error"""
111
+ self.occurrence_count += 1
112
+ self.start_steps_count_list.append(start_steps_count)
113
+
114
+
115
+ PropertyName = NewType("PropertyName", str)
116
+ TestResult = NewType("TestResult", Dict[PropertyName, PropertyExecResult])
117
+
118
+
119
+ class BugReportGenerator(CrashAnrMixin, PathParserMixin, ScreenshotsMixin):
120
+ """
121
+ Generate HTML format bug reports
122
+ """
123
+
124
+ _cov_trend: Deque[CovData] = None
125
+ _test_result: TestResult = None
126
+
127
+ @property
128
+ def cov_trend(self):
129
+ if self._cov_trend is not None:
130
+ return self._cov_trend
131
+
132
+ # Parse coverage data
133
+ if not self.data_path.coverage_log.exists():
134
+ logger.error(f"{self.data_path.coverage_log} not exists")
135
+
136
+ cov_trend = list()
137
+
138
+ with open(self.data_path.coverage_log, "r", encoding="utf-8") as f:
139
+ for line in f:
140
+ if not line.strip():
141
+ continue
142
+
143
+ coverage_data = json.loads(line)
144
+ cov_trend.append(coverage_data)
145
+ self._cov_trend = cov_trend
146
+ return self._cov_trend
147
+
148
+ @property
149
+ def test_result(self) -> TestResult:
150
+ if self._test_result is not None:
151
+ return self._test_result
152
+
153
+ if not self.data_path.result_json.exists():
154
+ logger.error(f"{self.data_path.result_json} not found")
155
+ with open(self.data_path.result_json, "r", encoding="utf-8") as f:
156
+ self._test_result: TestResult = json.load(f)
157
+
158
+ return self._test_result
159
+
160
+ @property
161
+ def config(self) -> Dict:
162
+ if not hasattr(self, '_config'):
163
+ with open(self.result_dir / "bug_report_config.json", "r", encoding="utf-8") as fp:
164
+ self._config = json.load(fp)
165
+ return self._config
166
+
167
+ def __init__(self, result_dir=None):
168
+ """
169
+ Initialize the bug report generator
170
+
171
+ Args:
172
+ result_dir: Directory path containing test results
173
+ """
174
+ if result_dir is None:
175
+ raise RuntimeError("Result directory must be provided to generate report.")
176
+ self.result_dir = Path(result_dir)
177
+
178
+ def __set_up_jinja_env(self):
179
+ """Set up Jinja2 environment for HTML template rendering"""
180
+ try:
181
+ self.jinja_env = Environment(
182
+ loader=PackageLoader("kea2.report", "templates"),
183
+ autoescape=select_autoescape(['html', 'xml'])
184
+ )
185
+ except (ImportError, ValueError):
186
+ # If unable to load from package, load from current directory's templates folder
187
+ current_dir = Path(__file__).parent
188
+ templates_dir = current_dir / "templates"
189
+
190
+ # Ensure template directory exists
191
+ if not templates_dir.exists():
192
+ templates_dir.mkdir(parents=True, exist_ok=True)
193
+
194
+ self.jinja_env = Environment(
195
+ loader=FileSystemLoader(templates_dir),
196
+ autoescape=select_autoescape(['html', 'xml'])
197
+ )
198
+
199
+ @catchException("Error generating bug report")
200
+ def generate_report(self) -> Optional[str]:
201
+ """
202
+ Generate bug report and save to result directory
203
+
204
+ Args:
205
+ result_dir_path: Directory path containing test results (optional)
206
+ If not provided, uses the path from initialization
207
+ """
208
+ # Check if paths are properly set up
209
+ self.__set_up_jinja_env()
210
+
211
+ self.screenshots = deque()
212
+ with thread_pool(max_workers=128) as executor:
213
+ logger.debug("Starting bug report generation")
214
+
215
+ # Collect test data
216
+ test_data: ReportData = self._collect_test_data(executor)
217
+
218
+ # Generate HTML report
219
+ html_content = self._generate_html_report(test_data)
220
+
221
+ # Save report
222
+ report_path = self.result_dir / "bug_report.html"
223
+ with open(report_path, "w", encoding="utf-8") as f:
224
+ f.write(html_content)
225
+
226
+ logger.info(f"Bug report saved to: {report_path}")
227
+ return str(report_path)
228
+
229
+ @catchException("Error when collecting test data")
230
+ def _collect_test_data(self, executor: "ThreadPoolExecutor"=None) -> ReportData:
231
+ """
232
+ Collect test data, including results, coverage, etc.
233
+ """
234
+ data: ReportData = {
235
+ "timestamp": self.config.get("log_stamp", ""),
236
+ "test_time": self.config.get("test_time", ""),
237
+ "bugs_found": 0,
238
+ "executed_events": 0,
239
+ "total_testing_time": 0,
240
+ "coverage": 0,
241
+ "total_activities": [],
242
+ "tested_activities": [],
243
+ "all_properties_count": 0,
244
+ "executed_properties_count": 0,
245
+ "property_violations": [],
246
+ "property_stats": [],
247
+ "property_error_details": {},
248
+ "screenshot_info": {},
249
+ "coverage_trend": [],
250
+ "property_execution_trend": [],
251
+ "activity_count_history": {},
252
+ "crash_events": [],
253
+ "anr_events": [],
254
+ "kill_apps_events": [],
255
+ }
256
+
257
+ # Parse steps.log file to get test step numbers and screenshot mappings
258
+ property_violations = {} # Store multiple violation records for each property
259
+ executed_properties_by_step = {} # Track executed properties at each step: {step_count: set()}
260
+ executed_properties = set() # Track unique executed properties
261
+
262
+ if not self.data_path.steps_log.exists():
263
+ logger.error(f"{self.data_path.steps_log} not exists")
264
+ return
265
+
266
+ current_property = None
267
+ current_test = {}
268
+ step_index = 0
269
+ monkey_events_count = 0 # Track monkey events separately
270
+
271
+ with open(self.data_path.steps_log, "r", encoding="utf-8") as f:
272
+ # Track current test state
273
+
274
+ for step_index, line in enumerate(f, start=1):
275
+ step_data = self._parse_step_data(line)
276
+
277
+ if not step_data:
278
+ continue
279
+
280
+ step_type = step_data.get("Type", "")
281
+ screenshot = step_data.get("Screenshot", "")
282
+ info = step_data.get("Info", {})
283
+
284
+ # Count Monkey events separately
285
+ if step_type == "Monkey" or step_type == "Fuzz":
286
+ monkey_events_count += 1
287
+
288
+ # Record restart-app marker events (no screenshot expected)
289
+ if step_type == "Monkey" and info == "kill_apps":
290
+ monkey_steps_count = step_data.get("MonkeyStepsCount", "N/A")
291
+ caption = f"Monkey Step {monkey_steps_count}: restart app"
292
+
293
+ data["kill_apps_events"].append({
294
+ "step_index": step_index,
295
+ "monkey_steps_count": monkey_steps_count,
296
+ })
297
+
298
+ # Show this info event in the Test Screenshots timeline
299
+ self.screenshots.append({
300
+ "id": step_index,
301
+ "path": "",
302
+ "caption": f"{step_index}. {caption}",
303
+ "kind": "info",
304
+ "info": "kill_apps",
305
+ })
306
+
307
+ # If screenshots are enabled, mark the screenshot
308
+ if self.take_screenshots and step_data["Screenshot"]:
309
+ executor.submit(self._mark_screenshot, step_data)
310
+
311
+ # Collect detailed information for each screenshot
312
+ if screenshot and screenshot not in data["screenshot_info"]:
313
+ self._add_screenshot_info(step_data, step_index, data)
314
+
315
+ # Process ScriptInfo for property violations and execution tracking
316
+ if step_type == "ScriptInfo":
317
+ property_name = info.get("propName", "")
318
+ state = info.get("state", "")
319
+
320
+ # Track executed properties (properties that have been started)
321
+ if property_name and state == "start":
322
+ executed_properties.add(property_name)
323
+ # Record the monkey steps count for this property execution
324
+ executed_properties_by_step[monkey_events_count] = executed_properties.copy()
325
+
326
+ current_property, current_test = self._process_script_info(
327
+ property_name, state, step_index, screenshot,
328
+ current_property, current_test, property_violations
329
+ )
330
+
331
+ # Store first and last step for time calculation
332
+ if step_index == 1:
333
+ first_step_time = step_data["Time"]
334
+ last_step_time = step_data["Time"]
335
+
336
+ # Set the monkey events count correctly
337
+ data["executed_events"] = monkey_events_count
338
+
339
+ # Calculate test time
340
+ if first_step_time and last_step_time:
341
+ def _get_datetime(raw_datetime) -> datetime:
342
+ return datetime.strptime(raw_datetime, r"%Y-%m-%d %H:%M:%S.%f")
343
+
344
+ test_time = _get_datetime(last_step_time) - _get_datetime(first_step_time)
345
+
346
+ total_seconds = int(test_time.total_seconds())
347
+ hours, remainder = divmod(total_seconds, 3600)
348
+ minutes, seconds = divmod(remainder, 60)
349
+ data["total_testing_time"] = f"{hours:02d}:{minutes:02d}:{seconds:02d}"
350
+
351
+ # Enrich property statistics with derived metrics and calculate bug count
352
+ enriched_property_stats = {}
353
+ for property_name, test_result in self.test_result.items():
354
+ # Check if failed or error
355
+ if test_result.get("fail", 0) > 0 or test_result.get("error", 0) > 0:
356
+ data["bugs_found"] += 1
357
+
358
+ executed_count = test_result.get("executed", 0)
359
+ fail_count = test_result.get("fail", 0)
360
+ error_count = test_result.get("error", 0)
361
+ pass_count = max(executed_count - fail_count - error_count, 0)
362
+
363
+ enriched_property_stats[property_name] = {
364
+ **test_result,
365
+ "pass_count": pass_count
366
+ }
367
+
368
+ # Store the enriched result data for direct use in HTML template
369
+ data["property_stats"] = enriched_property_stats
370
+
371
+ # Calculate properties statistics
372
+ data["all_properties_count"] = len(self.test_result)
373
+ data["executed_properties_count"] = sum(1 for result in self.test_result.values() if result.get("executed", 0) > 0)
374
+
375
+ # Calculate detailed property statistics for table headers
376
+ property_stats_summary = self._calculate_property_stats_summary(enriched_property_stats)
377
+ data["property_stats_summary"] = property_stats_summary
378
+
379
+ # Process coverage data
380
+ data["coverage_trend"] = self.cov_trend
381
+
382
+ if self.cov_trend:
383
+ final_trend = self.cov_trend[-1]
384
+ data["coverage"] = final_trend["coverage"]
385
+ data["total_activities"] = final_trend["totalActivities"]
386
+ data["tested_activities"] = final_trend["testedActivities"]
387
+ data["total_activities_count"] = final_trend["totalActivitiesCount"]
388
+ data["tested_activities_count"] = final_trend["testedActivitiesCount"]
389
+ data["activity_count_history"] = final_trend["activityCountHistory"]
390
+
391
+ # Generate property execution trend aligned with coverage trend
392
+ data["property_execution_trend"] = self._generate_property_execution_trend(executed_properties_by_step)
393
+
394
+ # Generate Property Violations list
395
+ self._generate_property_violations_list(property_violations, data)
396
+
397
+ # Load error details for properties with fail/error state
398
+ data["property_error_details"] = self._load_property_error_details()
399
+
400
+ # Load crash and ANR events from crash-dump.log
401
+ crash_events, anr_events = self._load_crash_dump_data()
402
+
403
+ # Add screenshot ID information to crash and ANR events
404
+ self._add_screenshot_ids_to_events(crash_events)
405
+ self._add_screenshot_ids_to_events(anr_events)
406
+
407
+ data["crash_events"] = crash_events
408
+ data["anr_events"] = anr_events
409
+
410
+ return data
411
+
412
+ def _parse_step_data(self, raw_step_info: str) -> StepData:
413
+ step_data: StepData = json.loads(raw_step_info)
414
+ if step_data.get("Type") in {"Monkey", "Script", "ScriptInfo"}:
415
+ info = step_data.get("Info")
416
+ if isinstance(info, str):
417
+ stripped = info.strip()
418
+ if stripped and stripped[0] in "{[":
419
+ step_data["Info"] = json.loads(stripped)
420
+ return step_data
421
+
422
+
423
+
424
+ @catchException("Error rendering template")
425
+ def _generate_html_report(self, data: ReportData):
426
+ """
427
+ Generate HTML format bug report
428
+ """
429
+ # Format timestamp for display
430
+ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
431
+
432
+ # Ensure coverage_trend has data
433
+ if not data.get("coverage_trend"):
434
+ logger.warning("No coverage trend data")
435
+ # Use the same field names as in coverage.log file
436
+ data["coverage_trend"] = [{"stepsCount": 0, "coverage": 0, "testedActivitiesCount": 0}]
437
+
438
+ # Convert coverage_trend to JSON string, ensuring all data points are included
439
+ coverage_trend_json = json.dumps(data["coverage_trend"])
440
+ logger.debug(f"Number of coverage trend data points: {len(data['coverage_trend'])}")
441
+
442
+ # Prepare template data
443
+ template_data = {
444
+ 'timestamp': timestamp,
445
+ 'test_time': data.get("test_time", ""),
446
+ 'log_stamp': data.get("timestamp", ""),
447
+ 'bugs_found': data["bugs_found"],
448
+ 'total_testing_time': data["total_testing_time"],
449
+ 'executed_events': data["executed_events"],
450
+ 'coverage_percent': round(data["coverage"], 2),
451
+ 'total_activities_count': data["total_activities_count"],
452
+ 'tested_activities_count': data["tested_activities_count"],
453
+ 'tested_activities': data["tested_activities"],
454
+ 'total_activities': data["total_activities"],
455
+ 'all_properties_count': data["all_properties_count"],
456
+ 'executed_properties_count': data["executed_properties_count"],
457
+ 'items_per_page': 10, # Items to display per page
458
+ 'screenshots': self.screenshots,
459
+ 'property_violations': data["property_violations"],
460
+ 'property_stats': data["property_stats"],
461
+ 'property_error_details': data["property_error_details"],
462
+ 'coverage_data': coverage_trend_json,
463
+ 'take_screenshots': self.take_screenshots, # Pass screenshot setting to template
464
+ 'property_execution_trend': data["property_execution_trend"],
465
+ 'property_execution_data': json.dumps(data["property_execution_trend"]),
466
+ 'activity_count_history': data["activity_count_history"],
467
+ 'crash_events': data["crash_events"],
468
+ 'anr_events': data["anr_events"],
469
+ 'triggered_crash_count': len(data["crash_events"]),
470
+ 'triggered_anr_count': len(data["anr_events"]),
471
+ 'property_stats_summary': data["property_stats_summary"],
472
+ 'kill_apps_events': data.get("kill_apps_events", []),
473
+ }
474
+
475
+ # Check if template exists, if not create it
476
+ template_path = Path(__file__).parent / "templates" / "bug_report_template.html"
477
+ if not template_path.exists():
478
+ logger.warning("Template file does not exist, creating default template...")
479
+
480
+ # Use Jinja2 to render template
481
+ template = self.jinja_env.get_template("bug_report_template.html")
482
+ html_content = template.render(**template_data)
483
+
484
+ return html_content
485
+
486
+ def _process_script_info(self, property_name: str, state: str, step_index: int, screenshot: str,
487
+ current_property: str, current_test: Dict, property_violations: Dict) -> Tuple:
488
+ """
489
+ Process ScriptInfo step for property violations tracking
490
+
491
+ Args:
492
+ property_name: Property name from ScriptInfo
493
+ state: State from ScriptInfo (start, pass, fail, error)
494
+ step_index: Current step index
495
+ screenshot: Screenshot filename
496
+ current_property: Currently tracked property
497
+ current_test: Current test data
498
+ property_violations: Dictionary to store violations
499
+
500
+ Returns:
501
+ tuple: (updated_current_property, updated_current_test)
502
+ """
503
+ if property_name and state:
504
+ if state == "start":
505
+ # Record new test start
506
+ current_property = property_name
507
+ current_test = {
508
+ "start": step_index,
509
+ "end": None,
510
+ "screenshot_start": screenshot
511
+ }
512
+ elif state in ["pass", "fail", "error"]:
513
+ if current_property == property_name:
514
+ # Update test end information
515
+ current_test["end"] = step_index
516
+ current_test["screenshot_end"] = screenshot
517
+
518
+ if state == "fail" or state == "error":
519
+ # Record failed/error test
520
+ if property_name not in property_violations:
521
+ property_violations[property_name] = []
522
+
523
+ property_violations[property_name].append({
524
+ "start": current_test["start"],
525
+ "end": current_test["end"],
526
+ "screenshot_start": current_test["screenshot_start"],
527
+ "screenshot_end": screenshot,
528
+ "state": state
529
+ })
530
+
531
+ # Reset current test
532
+ current_property = None
533
+ current_test = {}
534
+
535
+ return current_property, current_test
536
+
537
+ def _generate_property_violations_list(self, property_violations: Dict, data: Dict):
538
+ """
539
+ Generate property violations list from collected violation data
540
+
541
+ Args:
542
+ property_violations: Dictionary containing property violations
543
+ data: Data dictionary to update with property violations list
544
+ """
545
+ if property_violations:
546
+ index = 1
547
+ for property_name, violations in property_violations.items():
548
+ for violation in violations:
549
+ start_step = violation["start"]
550
+ end_step = violation["end"]
551
+ data["property_violations"].append({
552
+ "index": index,
553
+ "property_name": property_name,
554
+ "interaction_pages": [start_step, end_step],
555
+ "state": violation.get("state", "fail")
556
+ })
557
+ index += 1
558
+
559
+ def _load_property_error_details(self) -> Dict[str, List[Dict]]:
560
+ """
561
+ Load property execution error details from property_exec_info file
562
+
563
+ Returns:
564
+ Dict[str, List[Dict]]: Mapping of property names to their error tracebacks with context
565
+ """
566
+ if not self.data_path.property_exec_info.exists():
567
+ logger.warning(f"Property exec info file {self.data_path.property_exec_info} not found")
568
+ return {}
569
+
570
+ try:
571
+ property_exec_infos = self._parse_property_exec_infos()
572
+ return self._group_errors_by_property(property_exec_infos)
573
+
574
+ except Exception as e:
575
+ logger.error(f"Error reading property exec info file: {e}")
576
+ return {}
577
+
578
+ def _parse_property_exec_infos(self) -> List[PropertyExecInfo]:
579
+ """Parse property execution info from file"""
580
+ exec_infos = []
581
+
582
+ with open(self.data_path.property_exec_info, "r", encoding="utf-8") as f:
583
+ for line_number, line in enumerate(f, 1):
584
+ line = line.strip()
585
+ if not line:
586
+ continue
587
+
588
+ try:
589
+ exec_info_data = json.loads(line)
590
+ prop_name = exec_info_data.get("propName", "")
591
+ state = exec_info_data.get("state", "")
592
+ tb = exec_info_data.get("tb", "")
593
+ start_steps_count = exec_info_data.get("startStepsCount", 0)
594
+
595
+ exec_info = PropertyExecInfo(
596
+ prop_name=prop_name,
597
+ state=state,
598
+ traceback=tb,
599
+ start_steps_count=start_steps_count
600
+ )
601
+
602
+ if exec_info.is_error_state() and prop_name and tb:
603
+ exec_infos.append(exec_info)
604
+
605
+ except json.JSONDecodeError as e:
606
+ logger.warning(f"Failed to parse property exec info line {line_number}: {line[:100]}... Error: {e}")
607
+ continue
608
+
609
+ return exec_infos
610
+
611
+ def _group_errors_by_property(self, exec_infos: List[PropertyExecInfo]) -> Dict[str, List[Dict]]:
612
+ """Group errors by property name and deduplicate"""
613
+ error_details = {}
614
+
615
+ for exec_info in exec_infos:
616
+ prop_name = exec_info.prop_name
617
+
618
+ if prop_name not in error_details:
619
+ error_details[prop_name] = {}
620
+
621
+ error_hash = exec_info.get_error_hash()
622
+
623
+ if error_hash in error_details[prop_name]:
624
+ # Error already exists, add occurrence
625
+ error_details[prop_name][error_hash].add_occurrence(exec_info.start_steps_count)
626
+ else:
627
+ # New error, create entry
628
+ error_details[prop_name][error_hash] = exec_info
629
+
630
+ # Convert to template-compatible format
631
+ result = {}
632
+ for prop_name, hash_dict in error_details.items():
633
+ result[prop_name] = []
634
+ for exec_info in hash_dict.values():
635
+ result[prop_name].append({
636
+ "state": exec_info.state,
637
+ "traceback": exec_info.traceback,
638
+ "occurrence_count": exec_info.occurrence_count,
639
+ "short_description": exec_info.short_description,
640
+ "startStepsCountList": exec_info.start_steps_count_list
641
+ })
642
+
643
+ # Sort by earliest startStepsCount, then by occurrence count (descending)
644
+ result[prop_name].sort(key=lambda x: (min(x["startStepsCountList"]), -x["occurrence_count"]))
645
+
646
+ return result
647
+
648
+ def _generate_property_execution_trend(self, executed_properties_by_step: Dict[int, set]) -> List[Dict]:
649
+ """
650
+ Generate property execution trend aligned with coverage trend
651
+
652
+ Args:
653
+ executed_properties_by_step: Dictionary containing executed properties at each step
654
+
655
+ Returns:
656
+ List[Dict]: Property execution trend data aligned with coverage trend
657
+ """
658
+ property_execution_trend = []
659
+
660
+ # Get step points from coverage trend to ensure alignment
661
+ coverage_step_points = []
662
+ if self.cov_trend:
663
+ coverage_step_points = [cov_data["stepsCount"] for cov_data in self.cov_trend]
664
+
665
+ # If no coverage data, use property execution data points
666
+ if not coverage_step_points and executed_properties_by_step:
667
+ coverage_step_points = sorted(executed_properties_by_step.keys())
668
+
669
+ # Generate property execution data for each coverage step point
670
+ for step_count in coverage_step_points:
671
+ # Find the latest executed properties count up to this step
672
+ executed_count = 0
673
+ latest_step = 0
674
+
675
+ for exec_step in executed_properties_by_step.keys():
676
+ if exec_step <= step_count and exec_step >= latest_step:
677
+ latest_step = exec_step
678
+ executed_count = len(executed_properties_by_step[exec_step])
679
+
680
+ property_execution_trend.append({
681
+ "stepsCount": step_count,
682
+ "executedPropertiesCount": executed_count
683
+ })
684
+
685
+ return property_execution_trend
686
+
687
+ def _calculate_property_stats_summary(self, test_result: TestResult) -> Dict[str, int]:
688
+ """
689
+ Calculate summary statistics for property checking table headers
690
+
691
+ Args:
692
+ test_result: Test result data containing property statistics
693
+
694
+ Returns:
695
+ Dict: Summary statistics for each column
696
+ """
697
+ stats_summary = {
698
+ "total_properties": 0,
699
+ "total_precond_satisfied": 0,
700
+ "total_executed": 0,
701
+ "total_passes": 0,
702
+ "total_fails": 0,
703
+ "total_errors": 0,
704
+ "properties_with_errors": 0
705
+ }
706
+
707
+ for property_name, result in test_result.items():
708
+ executed_count = result.get("executed", result.get("executed_total", 0))
709
+ fail_count = result.get("fail", 0)
710
+ error_count = result.get("error", 0)
711
+ pass_count = result.get("pass_count",
712
+ max(executed_count - fail_count - error_count, 0))
713
+
714
+ stats_summary["total_properties"] += 1
715
+ stats_summary["total_precond_satisfied"] += result.get("precond_satisfied", 0)
716
+ stats_summary["total_executed"] += executed_count
717
+ stats_summary["total_passes"] += pass_count
718
+ stats_summary["total_fails"] += fail_count
719
+ stats_summary["total_errors"] += error_count
720
+
721
+ # Count properties that have errors or fails
722
+ if fail_count > 0 or error_count > 0:
723
+ stats_summary["properties_with_errors"] += 1
724
+
725
+ return stats_summary
726
+
727
+ def _load_crash_dump_data(self) -> Tuple[List[Dict], List[Dict]]:
728
+ """
729
+ Load crash and ANR events from crash-dump.log file
730
+
731
+ Returns:
732
+ tuple: (crash_events, anr_events) - Lists of crash and ANR event dictionaries
733
+ """
734
+ crash_events = []
735
+ anr_events = []
736
+
737
+ if not self.data_path.crash_dump_log.exists():
738
+ logger.info(f"No crash was found in this run.")
739
+ return crash_events, anr_events
740
+
741
+ try:
742
+ with open(self.data_path.crash_dump_log, "r", encoding="utf-8") as f:
743
+ content = f.read()
744
+
745
+ # Parse crash events with screenshot mapping
746
+ crash_events = self._parse_crash_events_with_screenshots(content)
747
+
748
+ # Parse ANR events with screenshot mapping
749
+ anr_events = self._parse_anr_events_with_screenshots(content)
750
+
751
+ logger.debug(f"Found {len(crash_events)} crash events and {len(anr_events)} ANR events")
752
+
753
+ return crash_events, anr_events
754
+
755
+ except Exception as e:
756
+ logger.error(f"Error reading crash dump file: {e}")
757
+ return crash_events, anr_events
758
+
759
+ def _find_screenshot_id_by_filename(self, screenshot_filename: str) -> str:
760
+ """
761
+ Find screenshot ID by filename in the screenshots list
762
+
763
+ Args:
764
+ screenshot_filename: Name of the screenshot file
765
+
766
+ Returns:
767
+ str: Screenshot ID if found, empty string otherwise
768
+ """
769
+ if not screenshot_filename:
770
+ return ""
771
+
772
+ for screenshot in self.screenshots:
773
+ # Extract filename from path
774
+ screenshot_path = screenshot.get('path', '')
775
+ if screenshot_path.endswith(screenshot_filename):
776
+ return str(screenshot.get('id', ''))
777
+
778
+ return ""
779
+
780
+ def _add_screenshot_ids_to_events(self, events: List[Dict]):
781
+ """
782
+ Add screenshot ID information to crash/ANR events
783
+
784
+ Args:
785
+ events: List of crash or ANR event dictionaries
786
+ """
787
+ for event in events:
788
+ crash_screen = event.get('crash_screen')
789
+ if crash_screen:
790
+ screenshot_id = self._find_screenshot_id_by_filename(crash_screen)
791
+ event['screenshot_id'] = screenshot_id
792
+ else:
793
+ event['screenshot_id'] = ""