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.
- kea2/absDriver.py +1 -1
- kea2/adbUtils.py +281 -20
- kea2/assets/monkeyq.jar +0 -0
- kea2/bug_report_generator.py +334 -322
- kea2/fastbotManager.py +110 -59
- kea2/keaUtils.py +61 -71
- kea2/kea_launcher.py +15 -0
- kea2/logWatcher.py +30 -33
- kea2/resultSyncer.py +18 -8
- kea2/templates/bug_report_template.html +15 -52
- kea2/u2Driver.py +27 -30
- kea2/utils.py +1 -0
- {kea2_python-0.1.3.dist-info → kea2_python-0.2.1.dist-info}/METADATA +42 -32
- {kea2_python-0.1.3.dist-info → kea2_python-0.2.1.dist-info}/RECORD +18 -18
- {kea2_python-0.1.3.dist-info → kea2_python-0.2.1.dist-info}/WHEEL +0 -0
- {kea2_python-0.1.3.dist-info → kea2_python-0.2.1.dist-info}/entry_points.txt +0 -0
- {kea2_python-0.1.3.dist-info → kea2_python-0.2.1.dist-info}/licenses/LICENSE +0 -0
- {kea2_python-0.1.3.dist-info → kea2_python-0.2.1.dist-info}/top_level.txt +0 -0
kea2/bug_report_generator.py
CHANGED
|
@@ -1,15 +1,33 @@
|
|
|
1
|
-
import os
|
|
2
1
|
import json
|
|
3
2
|
import datetime
|
|
4
|
-
import
|
|
3
|
+
from dataclasses import dataclass
|
|
5
4
|
from pathlib import Path
|
|
6
|
-
import
|
|
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
|
-
|
|
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.
|
|
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.
|
|
106
|
-
property_violations = {} # Store multiple violation records for each property
|
|
107
|
-
|
|
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
|
-
#
|
|
117
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
steps
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
158
|
+
property_name = info.get("propName", "")
|
|
128
159
|
state = info.get("state", "")
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
|
139
|
-
except:
|
|
140
|
-
pass
|
|
165
|
+
logger.error(f"Error processing ScriptInfo step {step_index}: {e}")
|
|
141
166
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
#
|
|
147
|
-
|
|
148
|
-
current_test = {}
|
|
172
|
+
# Set the monkey events count
|
|
173
|
+
data["executed_events"] = monkey_events_count
|
|
149
174
|
|
|
150
|
-
#
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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 =
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
|
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
|
-
|
|
316
|
-
|
|
197
|
+
# Store the raw result data for direct use in HTML template
|
|
198
|
+
data["property_stats"] = result_data
|
|
317
199
|
|
|
318
|
-
#
|
|
319
|
-
|
|
320
|
-
if
|
|
321
|
-
|
|
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
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|
|
368
|
-
|
|
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
|
|
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
|
-
|
|
386
|
-
|
|
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
|
-
|
|
389
|
-
|
|
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
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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"],
|
|
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
|