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.

@@ -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, Literal, List
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
- def __init__(self, result_dir):
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
- self.result_dir = Path(result_dir)
44
- self.log_timestamp = self.result_dir.name.split("_", 1)[1]
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.screenshots = deque()
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
- def generate_report(self):
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
- "screenshot_info": {}, # Store detailed information for each screenshot
119
- "coverage_trend": [] # Store coverage trend data
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
- relative_path = f"output_{self.log_timestamp}/screenshots"
126
-
127
- if steps_log_path.exists():
128
- with open(steps_log_path, "r", encoding="utf-8") as f:
129
- # Track current test state
130
- current_property = None
131
- current_test = {}
132
- monkey_events_count = 0
133
- step_index = 0
134
-
135
- for line in f:
136
- step_data = self._parse_step_data(line)
137
- if step_data:
138
- step_index += 1 # Count steps starting from 1
139
- step_type = step_data.get("Type", "")
140
- screenshot = step_data.get("Screenshot", "")
141
- info = step_data.get("Info", {})
142
-
143
- # Count Monkey events
144
- if step_type == "Monkey":
145
- monkey_events_count += 1
146
-
147
- # If screenshots are enabled, mark the screenshot
148
- if self.take_screenshots and screenshot:
149
- self._mark_screenshot(step_data)
150
-
151
- # Collect detailed information for each screenshot
152
- if screenshot and screenshot not in data["screenshot_info"]:
153
- self._add_screenshot_info(screenshot, step_type, info, step_index, relative_path, data)
154
-
155
- # Process ScriptInfo for property violations
156
- if step_type == "ScriptInfo":
157
- try:
158
- property_name = info.get("propName", "")
159
- state = info.get("state", "")
160
- current_property, current_test = self._process_script_info(
161
- property_name, state, step_index, screenshot,
162
- current_property, current_test, property_violations
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
- data["total_testing_time"] = int((datetime.datetime.strptime(last_step_time,"%Y-%m-%d %H:%M:%S.%f") -
179
- datetime.datetime.strptime(first_step_time,"%Y-%m-%d %H:%M:%S.%f")
180
- ).total_seconds())
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 calculating test time: {e}")
349
+ logger.error(f"Error processing ScriptInfo step {step_index}: {e}")
183
350
 
184
- # Parse result file
185
- result_json_path = self.data_path.result_json
186
-
187
- if result_json_path.exists():
188
- with open(result_json_path, "r", encoding="utf-8") as f:
189
- result_data = json.load(f)
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
- # Calculate bug count directly from result data
192
- for property_name, test_result in result_data.items():
193
- # Check if failed or error
194
- if test_result.get("fail", 0) > 0 or test_result.get("error", 0) > 0:
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
- # Store the raw result data for direct use in HTML template
198
- data["property_stats"] = result_data
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
- cov_trend, last_line = self._get_cov_trend()
202
- if cov_trend:
203
- data["coverage_trend"] = cov_trend
204
-
205
- if last_line:
206
- try:
207
- coverage_data = json.loads(last_line)
208
- if coverage_data:
209
- data["coverage"] = coverage_data.get("coverage", 0)
210
- data["total_activities"] = coverage_data.get("totalActivities", [])
211
- data["tested_activities"] = coverage_data.get("testedActivities", [])
212
- except Exception as e:
213
- logger.error(f"Error parsing final coverage data: {e}")
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.get("Info"))
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
- if step_data["Type"] == "Monkey":
227
- try:
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
- screenshot_path = self.data_path.screenshots_dir / screenshot_name
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
- def _mark_screenshot_interaction(self, screenshot_path, action_type, position):
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
- Mark interaction on screenshot with colored rectangle
437
+ Mark interaction on screenshot with colored rectangle
242
438
 
243
- Args:
244
- screenshot_path (Path): Path to the screenshot file
245
- action_type (str): Type of action ('CLICK' or 'LONG_CLICK' or 'SCROLL')
246
- position (list): Position coordinates [x1, y1, x2, y2]
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
- Returns:
249
- bool: True if marking was successful, False otherwise
445
+ Returns:
446
+ bool: True if marking was successful, False otherwise
250
447
  """
251
- try:
252
- img = Image.open(screenshot_path).convert("RGB")
253
- draw = ImageDraw.Draw(img)
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
- if not isinstance(position, (list, tuple)) or len(position) != 4:
256
- logger.warning(f"Invalid position format: {position}")
257
- return False
453
+ img = Image.open(screenshot_path).convert("RGB")
454
+ draw = ImageDraw.Draw(img)
455
+ line_width = 5
258
456
 
259
- x1, y1, x2, y2 = map(int, position)
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
- line_width = 5
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
- img.save(screenshot_path)
274
- return True
474
+ elif step_type == "Script":
475
+ if action_type == "click":
275
476
 
276
- except Exception as e:
277
- logger.error(f"Error marking screenshot {screenshot_path}: {e}")
278
- return False
279
-
280
-
281
- def _detect_screenshots_setting(self):
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
- def _get_cov_trend(self):
289
- # Parse coverage data
290
- coverage_log_path = self.data_path.coverage_log
291
- cov_trend = []
292
- last_line = None
293
- if coverage_log_path.exists():
294
- with open(coverage_log_path, "r", encoding="utf-8") as f:
295
- for line in f:
296
- if not line.strip():
297
- continue
298
- try:
299
- coverage_data = json.loads(line)
300
- cov_trend.append({
301
- "steps": coverage_data.get("stepsCount", 0),
302
- "coverage": coverage_data.get("coverage", 0),
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
- def _generate_html_report(self, data):
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.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
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
- data["coverage_trend"] = [{"steps": 0, "coverage": 0, "tested_activities_count": 0}]
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
- 'first_bug_time': data["first_bug_time"],
336
- 'first_precondition_time': data["first_precondition_time"],
337
- 'total_activities_count': len(data["total_activities"]),
338
- 'tested_activities_count': len(data["tested_activities"]),
339
- 'tested_activities': data["tested_activities"], # Pass list of tested Activities
340
- 'total_activities': data["total_activities"], # Pass list of all Activities
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, screenshot: str, step_type: str, info: Dict, step_index: int, relative_path: str, data: Dict):
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
- screenshot: Screenshot filename
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
- try:
377
- caption = ""
586
+ caption = ""
378
587
 
379
- if step_type == "Monkey":
380
- # Extract 'act' attribute for Monkey type and convert to lowercase
381
- caption = f"{info.get('act', 'N/A').lower()}"
382
- elif step_type == "Script":
383
- # Extract 'method' attribute for Script type
384
- caption = f"{info.get('method', 'N/A')}"
385
- elif step_type == "ScriptInfo":
386
- # Extract 'propName' and 'state' attributes for ScriptInfo type
387
- prop_name = info.get('propName', '')
388
- state = info.get('state', 'N/A')
389
- caption = f"{prop_name} {state}" if prop_name else f"{state}"
390
-
391
- data["screenshot_info"][screenshot] = {
392
- "type": step_type,
393
- "caption": caption,
394
- "step_index": step_index
395
- }
396
-
397
- screenshot_caption = data["screenshot_info"][screenshot].get('caption', '')
398
- self.screenshots.append({
399
- 'id': step_index,
400
- 'path': f"{relative_path}/{screenshot}",
401
- 'caption': f"{step_index}. {screenshot_caption}"
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
- def _process_script_info(self, property_name: str, state: str, step_index: int, screenshot: str,
420
- current_property: str, current_test: Dict, property_violations: Dict) -> tuple:
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}")