Kea2-python 0.1.3__py3-none-any.whl → 0.2.1__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,15 +1,33 @@
1
- import os
2
1
  import json
3
2
  import datetime
4
- import re
3
+ from dataclasses import dataclass
5
4
  from pathlib import Path
6
- import shutil
5
+ from typing import Dict, TypedDict, Literal, List
6
+ from collections import deque
7
+
8
+ from PIL import Image, ImageDraw
7
9
  from jinja2 import Environment, FileSystemLoader, select_autoescape, PackageLoader
8
10
  from .utils import getLogger
9
11
 
12
+
10
13
  logger = getLogger(__name__)
11
14
 
12
15
 
16
+ class StepData(TypedDict):
17
+ Type: str
18
+ MonkeyStepsCount: int
19
+ Time: str
20
+ Info: Dict
21
+ Screenshot: str
22
+
23
+ @dataclass
24
+ class DataPath:
25
+ steps_log: Path
26
+ result_json: Path
27
+ coverage_log: Path
28
+ screenshots_dir: Path
29
+
30
+
13
31
  class BugReportGenerator:
14
32
  """
15
33
  Generate HTML format bug reports
@@ -24,9 +42,18 @@ class BugReportGenerator:
24
42
  """
25
43
  self.result_dir = Path(result_dir)
26
44
  self.log_timestamp = self.result_dir.name.split("_", 1)[1]
27
- self.screenshots_dir = self.result_dir / f"output_{self.log_timestamp}" / "screenshots"
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
+ )
52
+
53
+ self.screenshots = deque()
54
+
28
55
  self.take_screenshots = self._detect_screenshots_setting()
29
-
56
+
30
57
  # Set up Jinja2 environment
31
58
  # First try to load templates from the package
32
59
  try:
@@ -38,17 +65,16 @@ class BugReportGenerator:
38
65
  # If unable to load from package, load from current directory's templates folder
39
66
  current_dir = Path(__file__).parent
40
67
  templates_dir = current_dir / "templates"
41
-
68
+
42
69
  # Ensure template directory exists
43
70
  if not templates_dir.exists():
44
71
  templates_dir.mkdir(parents=True, exist_ok=True)
45
-
72
+
46
73
  self.jinja_env = Environment(
47
74
  loader=FileSystemLoader(templates_dir),
48
75
  autoescape=select_autoescape(['html', 'xml'])
49
76
  )
50
-
51
- # If template file doesn't exist, it will be created on first report generation
77
+
52
78
 
53
79
  def generate_report(self):
54
80
  """
@@ -68,7 +94,7 @@ class BugReportGenerator:
68
94
  with open(report_path, "w", encoding="utf-8") as f:
69
95
  f.write(html_content)
70
96
 
71
- logger.info(f"Bug report saved to: {report_path}")
97
+ logger.debug(f"Bug report saved to: {report_path}")
72
98
 
73
99
  except Exception as e:
74
100
  logger.error(f"Error generating bug report: {e}")
@@ -80,7 +106,6 @@ class BugReportGenerator:
80
106
  data = {
81
107
  "timestamp": self.log_timestamp,
82
108
  "bugs_found": 0,
83
- "preconditions_satisfied": 0,
84
109
  "executed_events": 0,
85
110
  "total_testing_time": 0,
86
111
  "first_bug_time": 0,
@@ -90,358 +115,216 @@ class BugReportGenerator:
90
115
  "tested_activities": [],
91
116
  "property_violations": [],
92
117
  "property_stats": [],
93
- "screenshots_count": 0,
94
118
  "screenshot_info": {}, # Store detailed information for each screenshot
95
119
  "coverage_trend": [] # Store coverage trend data
96
120
  }
97
121
 
98
- # Get screenshot count
99
- if self.screenshots_dir.exists():
100
- screenshots = sorted(self.screenshots_dir.glob("screenshot-*.png"),
101
- key=lambda x: int(x.name.split("-")[1].split(".")[0]))
102
- data["screenshots_count"] = len(screenshots)
103
-
104
122
  # Parse steps.log file to get test step numbers and screenshot mappings
105
- steps_log_path = self.result_dir / f"output_{self.log_timestamp}" / "steps.log"
106
- property_violations = {} # Store multiple violation records for each property: {property_name: [{start, end, screenshot}, ...]}
107
- start_screenshot = None # Screenshot name at the start of testing
108
- fail_screenshot = None # Screenshot name at test failure
109
-
110
- # For storing time data
111
- first_precond_time = None # Time of the first ScriptInfo entry with state=start
112
- first_fail_time = None # Time of the first ScriptInfo entry with state=fail
123
+ steps_log_path = self.data_path.steps_log
124
+ property_violations = {} # Store multiple violation records for each property
125
+ relative_path = f"output_{self.log_timestamp}/screenshots"
113
126
 
114
127
  if steps_log_path.exists():
115
128
  with open(steps_log_path, "r", encoding="utf-8") as f:
116
- # First read all steps
117
- steps = []
118
-
129
+ # Track current test state
130
+ current_property = None
131
+ current_test = {}
132
+ monkey_events_count = 0
133
+ step_index = 0
134
+
119
135
  for line in f:
120
- try:
121
- step_data = json.loads(line)
122
- steps.append(step_data)
123
-
124
- # Extract time from ScriptInfo entries
125
- if step_data.get("Type") == "ScriptInfo":
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":
126
157
  try:
127
- info = json.loads(step_data.get("Info", "{}")) if isinstance(step_data.get("Info"), str) else step_data.get("Info", {})
158
+ property_name = info.get("propName", "")
128
159
  state = info.get("state", "")
129
-
130
- # Record the first ScriptInfo with state=start as precondition time
131
- if state == "start" and first_precond_time is None:
132
- first_precond_time = step_data.get("Time")
133
-
134
- # Record the first ScriptInfo with state=fail as fail time
135
- elif state == "fail" and first_fail_time is None:
136
- first_fail_time = step_data.get("Time")
160
+ current_property, current_test = self._process_script_info(
161
+ property_name, state, step_index, screenshot,
162
+ current_property, current_test, property_violations
163
+ )
137
164
  except Exception as e:
138
- logger.error(f"Error parsing ScriptInfo: {e}")
139
- except:
140
- pass
165
+ logger.error(f"Error processing ScriptInfo step {step_index}: {e}")
141
166
 
142
- # Calculate number of Monkey events
143
- monkey_events_count = sum(1 for step in steps if step.get("Type") == "Monkey")
144
- data["executed_events"] = monkey_events_count
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"]
145
171
 
146
- # Track current test state
147
- current_property = None
148
- current_test = {}
172
+ # Set the monkey events count
173
+ data["executed_events"] = monkey_events_count
149
174
 
150
- # Collect detailed information for each screenshot
151
- for step in steps:
152
- step_type = step.get("Type", "")
153
- screenshot = step.get("Screenshot", "")
154
- info = step.get("Info", "{}")
155
-
156
- if screenshot and screenshot not in data["screenshot_info"]:
157
- try:
158
- info_obj = json.loads(info) if isinstance(info, str) else info
159
- caption = ""
160
-
161
- if step_type == "Monkey":
162
- # Extract 'act' attribute for Monkey type and convert to lowercase
163
- caption = f"{info_obj.get('act', 'N/A').lower()}"
164
- elif step_type == "Script":
165
- # Extract 'method' attribute for Script type
166
- caption = f"{info_obj.get('method', 'N/A')}"
167
- elif step_type == "ScriptInfo":
168
- # Extract 'propName' and 'state' attributes for ScriptInfo type
169
- prop_name = info_obj.get('propName', '')
170
- state = info_obj.get('state', 'N/A')
171
- caption = f"{prop_name} {state}" if prop_name else f"{state}"
172
-
173
- data["screenshot_info"][screenshot] = {
174
- "type": step_type,
175
- "caption": caption
176
- }
177
- except Exception as e:
178
- logger.error(f"Error parsing screenshot info: {e}")
179
- data["screenshot_info"][screenshot] = {
180
- "type": step_type,
181
- "caption": step_type
182
- }
183
-
184
- # Find start and end step numbers and corresponding screenshots for all tests
185
- for i, step in enumerate(steps, 1): # Start counting from 1 to match screenshot numbering
186
- if step.get("Type") == "ScriptInfo":
187
- try:
188
- info = json.loads(step.get("Info", "{}"))
189
- property_name = info.get("propName", "")
190
- state = info.get("state", "")
191
- screenshot = step.get("Screenshot", "")
192
-
193
- if property_name and state:
194
- if state == "start":
195
- # Record new test start
196
- current_property = property_name
197
- current_test = {
198
- "start": i,
199
- "end": None,
200
- "screenshot_start": screenshot
201
- }
202
- # Record screenshot at test start
203
- if not start_screenshot and screenshot:
204
- start_screenshot = screenshot
205
-
206
- elif state == "fail" or state == "pass":
207
- if current_property == property_name:
208
- # Update test end information
209
- current_test["end"] = i
210
- current_test["screenshot_end"] = screenshot
211
-
212
- if state == "fail":
213
- # Record failed test
214
- if property_name not in property_violations:
215
- property_violations[property_name] = []
216
-
217
- property_violations[property_name].append({
218
- "start": current_test["start"],
219
- "end": current_test["end"],
220
- "screenshot_start": current_test["screenshot_start"],
221
- "screenshot_end": screenshot
222
- })
223
-
224
- # Record screenshot at test failure
225
- if not fail_screenshot and screenshot:
226
- fail_screenshot = screenshot
227
-
228
- # Reset current test
229
- current_property = None
230
- current_test = {}
231
- except:
232
- pass
233
-
234
- # Calculate test time
235
- start_time = None
236
-
237
- # Parse fastbot log file to get start time
238
- fastbot_log_path = list(self.result_dir.glob("fastbot_*.log"))
239
- if fastbot_log_path:
240
- try:
241
- with open(fastbot_log_path[0], "r", encoding="utf-8") as f:
242
- log_content = f.read()
243
-
244
- # Extract test start time
245
- start_match = re.search(r'\[Fastbot\]\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3})\] @Version',
246
- log_content)
247
- if start_match:
248
- start_time_str = start_match.group(1)
249
- start_time = datetime.datetime.strptime(start_time_str, "%Y-%m-%d %H:%M:%S.%f")
250
-
251
- # Extract test end time (last timestamp)
252
- end_matches = re.findall(r'\[Fastbot\]\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3})\]',
253
- log_content)
254
- end_time = None
255
- if end_matches:
256
- end_time_str = end_matches[-1]
257
- end_time = datetime.datetime.strptime(end_time_str, "%Y-%m-%d %H:%M:%S.%f")
258
-
259
- # Calculate total test time (in seconds)
260
- if start_time and end_time:
261
- data["total_testing_time"] = int((end_time - start_time).total_seconds())
262
- except Exception as e:
263
- logger.error(f"Error parsing fastbot log file: {e}")
264
- logger.error(f"Error details: {str(e)}")
265
-
266
- # Calculate first_bug_time and first_precondition_time from steps.log data
267
- if start_time:
268
- # If first_precond_time exists, calculate first_precondition_time
269
- if first_precond_time:
270
- try:
271
- precond_time = datetime.datetime.strptime(first_precond_time, "%Y-%m-%d %H:%M:%S.%f")
272
- data["first_precondition_time"] = int((precond_time - start_time).total_seconds())
273
- except Exception as e:
274
- logger.error(f"Error parsing precond_time: {e}")
275
-
276
- # If first_fail_time exists, calculate first_bug_time
277
- if first_fail_time:
278
- try:
279
- fail_time = datetime.datetime.strptime(first_fail_time, "%Y-%m-%d %H:%M:%S.%f")
280
- data["first_bug_time"] = int((fail_time - start_time).total_seconds())
281
- except Exception as e:
282
- logger.error(f"Error parsing fail_time: {e}")
175
+ # Calculate test time
176
+ if step_index > 0:
177
+ 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())
181
+ except Exception as e:
182
+ logger.error(f"Error calculating test time: {e}")
283
183
 
284
184
  # Parse result file
285
- result_json_path = list(self.result_dir.glob("result_*.json"))
286
- property_stats = {} # Store property names and corresponding statistics
287
-
288
- if result_json_path:
289
- with open(result_json_path[0], "r", encoding="utf-8") as f:
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:
290
189
  result_data = json.load(f)
291
190
 
292
- # Calculate bug count and get property names
191
+ # Calculate bug count directly from result data
293
192
  for property_name, test_result in result_data.items():
294
- # Extract property name (last part of test name)
295
-
296
- # Initialize property statistics
297
- if property_name not in property_stats:
298
- property_stats[property_name] = {
299
- "precond_satisfied": 0,
300
- "precond_checked": 0,
301
- "postcond_violated": 0,
302
- "error": 0
303
- }
304
-
305
- # Extract statistics directly from result_*.json file
306
- property_stats[property_name]["precond_satisfied"] += test_result.get("precond_satisfied", 0)
307
- property_stats[property_name]["precond_checked"] += test_result.get("executed", 0)
308
- property_stats[property_name]["postcond_violated"] += test_result.get("fail", 0)
309
- property_stats[property_name]["error"] += test_result.get("error", 0)
310
-
311
193
  # Check if failed or error
312
194
  if test_result.get("fail", 0) > 0 or test_result.get("error", 0) > 0:
313
195
  data["bugs_found"] += 1
314
196
 
315
- data["preconditions_satisfied"] += test_result.get("precond_satisfied", 0)
316
- # data["executed_events"] += test_result.get("executed", 0)
197
+ # Store the raw result data for direct use in HTML template
198
+ data["property_stats"] = result_data
317
199
 
318
- # Parse coverage data
319
- coverage_log_path = self.result_dir / f"output_{self.log_timestamp}" / "coverage.log"
320
- if coverage_log_path.exists():
321
- with open(coverage_log_path, "r", encoding="utf-8") as f:
322
- lines = f.readlines()
323
- if lines:
324
- # Collect coverage trend data
325
- for line in lines:
326
- if not line.strip():
327
- continue
328
- try:
329
- coverage_data = json.loads(line)
330
- data["coverage_trend"].append({
331
- "steps": coverage_data.get("stepsCount", 0),
332
- "coverage": coverage_data.get("coverage", 0),
333
- "tested_activities_count": len(coverage_data.get("testedActivities", []))
334
- })
335
- except Exception as e:
336
- logger.error(f"Error parsing coverage data: {e}")
337
- continue
338
-
339
- # Ensure sorted by steps
340
- data["coverage_trend"].sort(key=lambda x: x["steps"])
341
-
342
- try:
343
- # Read last line to get final coverage data
344
- coverage_data = json.loads(lines[-1])
345
- data["coverage"] = coverage_data.get("coverage", 0)
346
- data["total_activities"] = coverage_data.get("totalActivities", [])
347
- data["tested_activities"] = coverage_data.get("testedActivities", [])
348
- except Exception as e:
349
- logger.error(f"Error parsing final coverage data: {e}")
200
+ # Process coverage data
201
+ cov_trend, last_line = self._get_cov_trend()
202
+ if cov_trend:
203
+ data["coverage_trend"] = cov_trend
350
204
 
351
- # Generate Property Violations list
352
- if property_violations:
353
- index = 1
354
- for property_name, violations in property_violations.items():
355
- for violation in violations:
356
- start_step = violation["start"]
357
- end_step = violation["end"]
358
- data["property_violations"].append({
359
- "index": index,
360
- "property_name": property_name,
361
- "precondition_page": start_step,
362
- "interaction_pages": [start_step, end_step],
363
- "postcondition_page": end_step
364
- })
365
- index += 1
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}")
366
214
 
367
- # Generate Property Stats list
368
- if property_stats:
369
- index = 1
370
- for property_name, stats in property_stats.items():
371
- data["property_stats"].append({
372
- "index": index,
373
- "property_name": property_name,
374
- "precond_satisfied": stats["precond_satisfied"],
375
- "precond_checked": stats["precond_checked"],
376
- "postcond_violated": stats["postcond_violated"],
377
- "error": stats["error"]
378
- })
379
- index += 1
215
+ # Generate Property Violations list
216
+ self._generate_property_violations_list(property_violations, data)
380
217
 
381
218
  return data
382
219
 
383
- def _detect_screenshots_setting(self):
220
+ 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"))
223
+ return step_data
224
+
225
+ def _mark_screenshot(self, step_data: StepData):
226
+ if step_data["Type"] == "Monkey":
227
+ try:
228
+ act = step_data["Info"].get("act")
229
+ pos = step_data["Info"].get("pos")
230
+ screenshot_name = step_data["Screenshot"]
231
+ 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}")
237
+
238
+
239
+ def _mark_screenshot_interaction(self, screenshot_path, action_type, position):
384
240
  """
385
- Detect if screenshots were enabled during test run.
386
- Returns True if screenshots were taken, False otherwise.
241
+ Mark interaction on screenshot with colored rectangle
242
+
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]
247
+
248
+ Returns:
249
+ bool: True if marking was successful, False otherwise
387
250
  """
388
- # Method 1: Check if screenshots directory exists and has content
389
- if self.screenshots_dir.exists() and any(self.screenshots_dir.glob("screenshot-*.png")):
251
+ try:
252
+ img = Image.open(screenshot_path).convert("RGB")
253
+ draw = ImageDraw.Draw(img)
254
+
255
+ if not isinstance(position, (list, tuple)) or len(position) != 4:
256
+ logger.warning(f"Invalid position format: {position}")
257
+ return False
258
+
259
+ x1, y1, x2, y2 = map(int, position)
260
+
261
+ line_width = 5
262
+
263
+ if action_type == "CLICK":
264
+ for i in range(line_width):
265
+ draw.rectangle([x1 - i, y1 - i, x2 + i, y2 + i], outline=(255, 0, 0))
266
+ elif action_type == "LONG_CLICK":
267
+ for i in range(line_width):
268
+ draw.rectangle([x1 - i, y1 - i, x2 + i, y2 + i], outline=(0, 0, 255))
269
+ elif action_type.startswith("SCROLL"):
270
+ for i in range(line_width):
271
+ draw.rectangle([x1 - i, y1 - i, x2 + i, y2 + i], outline=(0, 255, 0))
272
+
273
+ img.save(screenshot_path)
390
274
  return True
391
-
392
- # Method 2: Try to read init config from logs
393
- fastbot_log_path = list(self.result_dir.glob("fastbot_*.log"))
394
- if fastbot_log_path:
395
- try:
396
- with open(fastbot_log_path[0], "r", encoding="utf-8") as f:
397
- log_content = f.read()
398
- if '"takeScreenshots": true' in log_content:
399
- return True
400
- except Exception:
401
- pass
402
-
403
- return False
275
+
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()
287
+
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
404
310
 
405
311
  def _generate_html_report(self, data):
406
312
  """
407
313
  Generate HTML format bug report
408
314
  """
409
315
  try:
410
- # Prepare screenshot data
411
- screenshots = []
412
- relative_path = f"output_{self.log_timestamp}/screenshots"
413
-
414
- if self.screenshots_dir.exists():
415
- screenshot_files = sorted(self.screenshots_dir.glob("screenshot-*.png"),
416
- key=lambda x: int(x.name.split("-")[1].split(".")[0]))
417
-
418
- for i, screenshot in enumerate(screenshot_files, 1):
419
- screenshot_name = screenshot.name
420
-
421
- # Get information for this screenshot
422
- caption = f"{i}"
423
- if screenshot_name in data["screenshot_info"]:
424
- info = data["screenshot_info"][screenshot_name]
425
- caption = f"{i}. {info.get('caption', '')}"
426
-
427
- screenshots.append({
428
- 'id': i,
429
- 'path': f"{relative_path}/{screenshot_name}",
430
- 'caption': caption
431
- })
432
-
433
316
  # Format timestamp for display
434
317
  timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
435
-
318
+
436
319
  # Ensure coverage_trend has data
437
320
  if not data["coverage_trend"]:
438
321
  logger.warning("No coverage trend data")
439
322
  data["coverage_trend"] = [{"steps": 0, "coverage": 0, "tested_activities_count": 0}]
440
-
323
+
441
324
  # Convert coverage_trend to JSON string, ensuring all data points are included
442
325
  coverage_trend_json = json.dumps(data["coverage_trend"])
443
326
  logger.debug(f"Number of coverage trend data points: {len(data['coverage_trend'])}")
444
-
327
+
445
328
  # Prepare template data
446
329
  template_data = {
447
330
  'timestamp': timestamp,
@@ -454,26 +337,155 @@ class BugReportGenerator:
454
337
  'total_activities_count': len(data["total_activities"]),
455
338
  'tested_activities_count': len(data["tested_activities"]),
456
339
  'tested_activities': data["tested_activities"], # Pass list of tested Activities
457
- 'total_activities': data["total_activities"], # Pass list of all Activities
340
+ 'total_activities': data["total_activities"], # Pass list of all Activities
458
341
  'items_per_page': 10, # Items to display per page
459
- 'screenshots': screenshots,
342
+ 'screenshots': self.screenshots,
460
343
  'property_violations': data["property_violations"],
461
344
  'property_stats': data["property_stats"],
462
345
  'coverage_data': coverage_trend_json,
463
346
  'take_screenshots': self.take_screenshots # Pass screenshot setting to template
464
347
  }
465
-
348
+
466
349
  # Check if template exists, if not create it
467
350
  template_path = Path(__file__).parent / "templates" / "bug_report_template.html"
468
351
  if not template_path.exists():
469
352
  logger.warning("Template file does not exist, creating default template...")
470
-
353
+
471
354
  # Use Jinja2 to render template
472
355
  template = self.jinja_env.get_template("bug_report_template.html")
473
356
  html_content = template.render(**template_data)
474
-
357
+
475
358
  return html_content
476
-
359
+
477
360
  except Exception as e:
478
361
  logger.error(f"Error rendering template: {e}")
479
- raise
362
+ raise
363
+
364
+ def _add_screenshot_info(self, screenshot: str, step_type: str, info: Dict, step_index: int, relative_path: str, data: Dict):
365
+ """
366
+ Add screenshot information to data structure
367
+
368
+ Args:
369
+ screenshot: Screenshot filename
370
+ step_type: Type of step (Monkey, Script, ScriptInfo)
371
+ info: Step information dictionary
372
+ step_index: Current step index
373
+ relative_path: Relative path to screenshots directory
374
+ data: Data dictionary to update
375
+ """
376
+ try:
377
+ caption = ""
378
+
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
+ })
418
+
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:
421
+ """
422
+ Process ScriptInfo step for property violations tracking
423
+
424
+ Args:
425
+ property_name: Property name from ScriptInfo
426
+ state: State from ScriptInfo (start, pass, fail, error)
427
+ step_index: Current step index
428
+ screenshot: Screenshot filename
429
+ current_property: Currently tracked property
430
+ current_test: Current test data
431
+ property_violations: Dictionary to store violations
432
+
433
+ Returns:
434
+ tuple: (updated_current_property, updated_current_test)
435
+ """
436
+ if property_name and state:
437
+ if state == "start":
438
+ # Record new test start
439
+ current_property = property_name
440
+ current_test = {
441
+ "start": step_index,
442
+ "end": None,
443
+ "screenshot_start": screenshot
444
+ }
445
+
446
+ elif state in ["pass", "fail", "error"]:
447
+ if current_property == property_name:
448
+ # Update test end information
449
+ current_test["end"] = step_index
450
+ current_test["screenshot_end"] = screenshot
451
+
452
+ if state == "fail" or state == "error":
453
+ # Record failed/error test
454
+ if property_name not in property_violations:
455
+ property_violations[property_name] = []
456
+
457
+ property_violations[property_name].append({
458
+ "start": current_test["start"],
459
+ "end": current_test["end"],
460
+ "screenshot_start": current_test["screenshot_start"],
461
+ "screenshot_end": screenshot
462
+ })
463
+
464
+ # Reset current test
465
+ current_property = None
466
+ current_test = {}
467
+
468
+ return current_property, current_test
469
+
470
+ def _generate_property_violations_list(self, property_violations: Dict, data: Dict):
471
+ """
472
+ Generate property violations list from collected violation data
473
+
474
+ Args:
475
+ property_violations: Dictionary containing property violations
476
+ data: Data dictionary to update with property violations list
477
+ """
478
+ if property_violations:
479
+ index = 1
480
+ for property_name, violations in property_violations.items():
481
+ for violation in violations:
482
+ start_step = violation["start"]
483
+ end_step = violation["end"]
484
+ data["property_violations"].append({
485
+ "index": index,
486
+ "property_name": property_name,
487
+ "precondition_page": start_step,
488
+ "interaction_pages": [start_step, end_step],
489
+ "postcondition_page": end_step
490
+ })
491
+ index += 1