Kea2-python 1.1.0b1__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.
- kea2/__init__.py +8 -0
- kea2/absDriver.py +56 -0
- kea2/adbUtils.py +554 -0
- kea2/assets/config_version.json +16 -0
- kea2/assets/fastbot-thirdpart.jar +0 -0
- kea2/assets/fastbot_configs/abl.strings +2 -0
- kea2/assets/fastbot_configs/awl.strings +3 -0
- kea2/assets/fastbot_configs/max.config +7 -0
- kea2/assets/fastbot_configs/max.fuzzing.strings +699 -0
- kea2/assets/fastbot_configs/max.schema.strings +1 -0
- kea2/assets/fastbot_configs/max.strings +3 -0
- kea2/assets/fastbot_configs/max.tree.pruning +27 -0
- kea2/assets/fastbot_configs/teardown.py +18 -0
- kea2/assets/fastbot_configs/widget.block.py +38 -0
- kea2/assets/fastbot_libs/arm64-v8a/libfastbot_native.so +0 -0
- kea2/assets/fastbot_libs/armeabi-v7a/libfastbot_native.so +0 -0
- kea2/assets/fastbot_libs/x86/libfastbot_native.so +0 -0
- kea2/assets/fastbot_libs/x86_64/libfastbot_native.so +0 -0
- kea2/assets/framework.jar +0 -0
- kea2/assets/kea2-thirdpart.jar +0 -0
- kea2/assets/monkeyq.jar +0 -0
- kea2/assets/quicktest.py +126 -0
- kea2/cli.py +216 -0
- kea2/fastbotManager.py +269 -0
- kea2/kea2_api.py +166 -0
- kea2/keaUtils.py +926 -0
- kea2/kea_launcher.py +299 -0
- kea2/logWatcher.py +92 -0
- kea2/mixin.py +0 -0
- kea2/report/__init__.py +0 -0
- kea2/report/bug_report_generator.py +879 -0
- kea2/report/mixin.py +496 -0
- kea2/report/report_merger.py +1066 -0
- kea2/report/templates/bug_report_template.html +4028 -0
- kea2/report/templates/merged_bug_report_template.html +3602 -0
- kea2/report/utils.py +10 -0
- kea2/result.py +257 -0
- kea2/resultSyncer.py +65 -0
- kea2/state.py +22 -0
- kea2/typedefs.py +32 -0
- kea2/u2Driver.py +612 -0
- kea2/utils.py +192 -0
- kea2/version_manager.py +102 -0
- kea2_python-1.1.0b1.dist-info/METADATA +447 -0
- kea2_python-1.1.0b1.dist-info/RECORD +49 -0
- kea2_python-1.1.0b1.dist-info/WHEEL +5 -0
- kea2_python-1.1.0b1.dist-info/entry_points.txt +2 -0
- kea2_python-1.1.0b1.dist-info/licenses/LICENSE +16 -0
- kea2_python-1.1.0b1.dist-info/top_level.txt +1 -0
kea2/report/mixin.py
ADDED
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
import re
|
|
3
|
+
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from PIL import Image, ImageDraw, ImageFont
|
|
7
|
+
|
|
8
|
+
from ..utils import catchException, getLogger
|
|
9
|
+
|
|
10
|
+
from typing import TYPE_CHECKING, Dict, List, Tuple, Union
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from .bug_report_generator import BugReportGenerator, StepData
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
CRASH_PATTERN = r'(?:StepsCount:\s*(\d+)\s*\nCrashScreen:\s*([^\n]*)\s*\n)?(\d{14})\ncrash:\n(.*?)\n// crash end'
|
|
16
|
+
ANR_PATTERN = r'(?:StepsCount:\s*(\d+)\s*\nCrashScreen:\s*([^\n]+)\s*\n)?(\d{14})\nanr:\n(.*?)\nanr end'
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class DataPath:
|
|
21
|
+
output_dir: Path
|
|
22
|
+
steps_log: Path
|
|
23
|
+
result_json: Path
|
|
24
|
+
coverage_log: Path
|
|
25
|
+
screenshots_dir: Path
|
|
26
|
+
property_exec_info: Path
|
|
27
|
+
crash_dump_log: Path
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
logger = getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class CrashAnrMixin:
|
|
34
|
+
def _iter_crash_info(self: "BugReportGenerator", content: str, pattern: str):
|
|
35
|
+
"""
|
|
36
|
+
Iterate over crash info blocks in crash-dump.log content
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
content: Content of crash-dump.log file
|
|
40
|
+
|
|
41
|
+
Yields:
|
|
42
|
+
Tuple[str, str, str, str]: steps_count, crash_screen, timestamp_str, crash_content
|
|
43
|
+
"""
|
|
44
|
+
for match in re.finditer(pattern, content, re.DOTALL):
|
|
45
|
+
steps_count = match.group(1)
|
|
46
|
+
crash_screen = match.group(2)
|
|
47
|
+
timestamp_str = match.group(3)
|
|
48
|
+
crash_content = match.group(4)
|
|
49
|
+
|
|
50
|
+
if timestamp_str:
|
|
51
|
+
timestamp = datetime.strptime(timestamp_str, "%Y%m%d%H%M%S")
|
|
52
|
+
timestamp_str = timestamp.strftime("%Y-%m-%d %H:%M:%S")
|
|
53
|
+
|
|
54
|
+
if not crash_screen and steps_count:
|
|
55
|
+
_crash_screens = list(self.data_path.screenshots_dir.glob(f'screenshot-{steps_count}-*.png'))
|
|
56
|
+
if _crash_screens:
|
|
57
|
+
crash_screen = str(_crash_screens[0].name)
|
|
58
|
+
|
|
59
|
+
yield steps_count, crash_screen, timestamp_str, crash_content
|
|
60
|
+
|
|
61
|
+
def _parse_crash_events_with_screenshots(self: "BugReportGenerator", content: str) -> List[Dict]:
|
|
62
|
+
"""
|
|
63
|
+
Parse crash events from crash-dump.log content with screenshot mapping
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
content: Content of crash-dump.log file
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
List[Dict]: List of crash event dictionaries with screenshot information
|
|
70
|
+
"""
|
|
71
|
+
crash_events = []
|
|
72
|
+
for steps_count, crash_screen, timestamp_str, crash_content in self._iter_crash_info(content, CRASH_PATTERN):
|
|
73
|
+
crash_info = self._extract_crash_info(crash_content)
|
|
74
|
+
crash_event = {
|
|
75
|
+
"time": timestamp_str,
|
|
76
|
+
"exception_type": crash_info.get("exception_type", "Unknown"),
|
|
77
|
+
"process": crash_info.get("process", "Unknown"),
|
|
78
|
+
"stack_trace": crash_info.get("stack_trace", ""),
|
|
79
|
+
"steps_count": steps_count,
|
|
80
|
+
"crash_screen": crash_screen.strip() if crash_screen else None
|
|
81
|
+
}
|
|
82
|
+
crash_events.append(crash_event)
|
|
83
|
+
return crash_events
|
|
84
|
+
|
|
85
|
+
def _parse_anr_events_with_screenshots(self: "BugReportGenerator", content: str) -> List[Dict]:
|
|
86
|
+
"""
|
|
87
|
+
Parse ANR events from crash-dump.log content with screenshot mapping
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
content: Content of crash-dump.log file
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
List[Dict]: List of ANR event dictionaries with screenshot information
|
|
94
|
+
"""
|
|
95
|
+
anr_events = []
|
|
96
|
+
|
|
97
|
+
for steps_count, crash_screen, timestamp_str, anr_content in self._iter_crash_info(content, ANR_PATTERN):
|
|
98
|
+
# Extract ANR information
|
|
99
|
+
anr_info = self._extract_anr_info(anr_content)
|
|
100
|
+
anr_event = {
|
|
101
|
+
"time": timestamp_str,
|
|
102
|
+
"reason": anr_info.get("reason", "Unknown"),
|
|
103
|
+
"process": anr_info.get("process", "Unknown"),
|
|
104
|
+
"trace": anr_info.get("trace", ""),
|
|
105
|
+
"steps_count": steps_count,
|
|
106
|
+
"crash_screen": crash_screen.strip() if crash_screen else None
|
|
107
|
+
}
|
|
108
|
+
anr_events.append(anr_event)
|
|
109
|
+
return anr_events
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _extract_crash_info(self, crash_content: str) -> Dict:
|
|
113
|
+
"""
|
|
114
|
+
Extract crash information from crash content
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
crash_content: Content of a single crash block
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
Dict: Extracted crash information
|
|
121
|
+
"""
|
|
122
|
+
crash_info = {
|
|
123
|
+
"exception_type": "Unknown",
|
|
124
|
+
"process": "Unknown",
|
|
125
|
+
"stack_trace": ""
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
lines = crash_content.strip().split('\n')
|
|
129
|
+
|
|
130
|
+
for line in lines:
|
|
131
|
+
line = line.strip()
|
|
132
|
+
|
|
133
|
+
# Extract PID from CRASH line
|
|
134
|
+
if line.startswith("// CRASH:"):
|
|
135
|
+
# Pattern: // CRASH: process_name (pid xxxx) (dump time: ...)
|
|
136
|
+
pid_match = re.search(r'\(pid\s+(\d+)\)', line)
|
|
137
|
+
if pid_match:
|
|
138
|
+
crash_info["process"] = pid_match.group(1)
|
|
139
|
+
|
|
140
|
+
# Extract exception type from Long Msg line
|
|
141
|
+
elif line.startswith("// Long Msg:"):
|
|
142
|
+
# Pattern: // Long Msg: ExceptionType: message
|
|
143
|
+
exception_match = re.search(r'// Long Msg:\s+([^:]+)', line)
|
|
144
|
+
if exception_match:
|
|
145
|
+
crash_info["exception_type"] = exception_match.group(1).strip()
|
|
146
|
+
|
|
147
|
+
# Extract full stack trace (all lines starting with //)
|
|
148
|
+
stack_lines = []
|
|
149
|
+
for line in lines:
|
|
150
|
+
if line.startswith("//"):
|
|
151
|
+
# Remove the "// " prefix for cleaner display
|
|
152
|
+
clean_line = line[3:] if line.startswith("// ") else line[2:]
|
|
153
|
+
stack_lines.append(clean_line)
|
|
154
|
+
|
|
155
|
+
crash_info["stack_trace"] = '\n'.join(stack_lines)
|
|
156
|
+
|
|
157
|
+
return crash_info
|
|
158
|
+
|
|
159
|
+
def _extract_anr_info(self, anr_content: str) -> Dict:
|
|
160
|
+
"""
|
|
161
|
+
Extract ANR information from ANR content
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
anr_content: Content of a single ANR block
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
Dict: Extracted ANR information
|
|
168
|
+
"""
|
|
169
|
+
anr_info = {
|
|
170
|
+
"reason": "Unknown",
|
|
171
|
+
"process": "Unknown",
|
|
172
|
+
"trace": ""
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
lines = anr_content.strip().split('\n')
|
|
176
|
+
|
|
177
|
+
for line in lines:
|
|
178
|
+
line = line.strip()
|
|
179
|
+
|
|
180
|
+
# Extract PID from ANR line
|
|
181
|
+
if line.startswith("// ANR:"):
|
|
182
|
+
# Pattern: // ANR: process_name (pid xxxx) (dump time: ...)
|
|
183
|
+
pid_match = re.search(r'\(pid\s+(\d+)\)', line)
|
|
184
|
+
if pid_match:
|
|
185
|
+
anr_info["process"] = pid_match.group(1)
|
|
186
|
+
|
|
187
|
+
# Extract reason from Reason line
|
|
188
|
+
elif line.startswith("Reason:"):
|
|
189
|
+
# Pattern: Reason: Input dispatching timed out (...)
|
|
190
|
+
reason_match = re.search(r'Reason:\s+(.+)', line)
|
|
191
|
+
if reason_match:
|
|
192
|
+
full_reason = reason_match.group(1).strip()
|
|
193
|
+
# Simplify the reason by extracting the main part before parentheses
|
|
194
|
+
simplified_reason = self._simplify_anr_reason(full_reason)
|
|
195
|
+
anr_info["reason"] = simplified_reason
|
|
196
|
+
|
|
197
|
+
# Store the full ANR trace content
|
|
198
|
+
anr_info["trace"] = anr_content
|
|
199
|
+
|
|
200
|
+
return anr_info
|
|
201
|
+
|
|
202
|
+
def _simplify_anr_reason(self, full_reason: str) -> str:
|
|
203
|
+
"""
|
|
204
|
+
Simplify ANR reason by extracting the main part
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
full_reason: Full ANR reason string
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
str: Simplified ANR reason
|
|
211
|
+
"""
|
|
212
|
+
# Common ANR reason patterns to simplify
|
|
213
|
+
simplification_patterns = [
|
|
214
|
+
# Input dispatching timed out (details...) -> Input dispatching timed out
|
|
215
|
+
(r'^(Input dispatching timed out)\s*\(.*\).*$', r'\1'),
|
|
216
|
+
# Broadcast of Intent (details...) -> Broadcast timeout
|
|
217
|
+
(r'^Broadcast of Intent.*$', 'Broadcast timeout'),
|
|
218
|
+
# Service timeout -> Service timeout
|
|
219
|
+
(r'^Service.*timeout.*$', 'Service timeout'),
|
|
220
|
+
# ContentProvider timeout -> ContentProvider timeout
|
|
221
|
+
(r'^ContentProvider.*timeout.*$', 'ContentProvider timeout'),
|
|
222
|
+
]
|
|
223
|
+
|
|
224
|
+
# Apply simplification patterns
|
|
225
|
+
for pattern, replacement in simplification_patterns:
|
|
226
|
+
match = re.match(pattern, full_reason, re.IGNORECASE)
|
|
227
|
+
if match:
|
|
228
|
+
if callable(replacement):
|
|
229
|
+
return replacement(match)
|
|
230
|
+
elif '\\1' in replacement:
|
|
231
|
+
return re.sub(pattern, replacement, full_reason, flags=re.IGNORECASE)
|
|
232
|
+
else:
|
|
233
|
+
return replacement
|
|
234
|
+
|
|
235
|
+
# If no pattern matches, try to extract the part before the first parenthesis
|
|
236
|
+
paren_match = re.match(r'^([^(]+)', full_reason)
|
|
237
|
+
if paren_match:
|
|
238
|
+
simplified = paren_match.group(1).strip()
|
|
239
|
+
# Remove trailing punctuation
|
|
240
|
+
simplified = re.sub(r'[.,;:]+$', '', simplified)
|
|
241
|
+
return simplified
|
|
242
|
+
|
|
243
|
+
# If all else fails, return the original but truncated
|
|
244
|
+
return full_reason[:50] + "..." if len(full_reason) > 50 else full_reason
|
|
245
|
+
|
|
246
|
+
class PathParserMixin:
|
|
247
|
+
|
|
248
|
+
_data_path: DataPath = None
|
|
249
|
+
|
|
250
|
+
@property
|
|
251
|
+
def data_path(self: "BugReportGenerator"):
|
|
252
|
+
if not self._data_path:
|
|
253
|
+
self._setup_paths()
|
|
254
|
+
return self._data_path
|
|
255
|
+
|
|
256
|
+
def _setup_paths(self: "BugReportGenerator"):
|
|
257
|
+
"""
|
|
258
|
+
Setup paths for a given result directory
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
result_dir: Directory path containing test results
|
|
262
|
+
"""
|
|
263
|
+
if self.config:
|
|
264
|
+
self.log_stamp = self.config.get("log_stamp", "")
|
|
265
|
+
|
|
266
|
+
if self.log_stamp:
|
|
267
|
+
output_dir = self.result_dir / f"output_{self.log_stamp}"
|
|
268
|
+
property_exec_info_file = self.result_dir / f"property_exec_info_{self.log_stamp}.json"
|
|
269
|
+
result_file = self.result_dir / f"result_{self.log_stamp}.json"
|
|
270
|
+
else:
|
|
271
|
+
output_dirs = [_ for _ in self.result_dir.glob("output_*") if _.is_dir()]
|
|
272
|
+
property_exec_info_files = [_ for _ in self.result_dir.glob("property_exec_info_*.json") if _.is_file()]
|
|
273
|
+
result_files = [_ for _ in self.result_dir.glob("result_*.json") if _.is_file()]
|
|
274
|
+
if all([output_dirs, property_exec_info_files, result_files]):
|
|
275
|
+
output_dir = output_dirs[0]
|
|
276
|
+
property_exec_info_file = property_exec_info_files[0]
|
|
277
|
+
result_file = result_files[0]
|
|
278
|
+
|
|
279
|
+
if not all([output_dir, property_exec_info_file, result_file]):
|
|
280
|
+
raise RuntimeError("Cannot determine output directory or execution info files from result directory.")
|
|
281
|
+
|
|
282
|
+
self._data_path: DataPath = DataPath(
|
|
283
|
+
output_dir=output_dir,
|
|
284
|
+
steps_log=output_dir / "steps.log",
|
|
285
|
+
coverage_log=output_dir / "coverage.log",
|
|
286
|
+
screenshots_dir=output_dir / "screenshots",
|
|
287
|
+
crash_dump_log=output_dir / "crash-dump.log",
|
|
288
|
+
property_exec_info=property_exec_info_file,
|
|
289
|
+
result_json=result_file,
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
class ScreenshotsMixin:
|
|
294
|
+
|
|
295
|
+
_take_screenshots: bool = None
|
|
296
|
+
_all_screenshot_names = set()
|
|
297
|
+
|
|
298
|
+
@property
|
|
299
|
+
def take_screenshots(self: "BugReportGenerator") -> bool:
|
|
300
|
+
"""Whether the `--take-screenshots` enabled. Should we report the screenshots?
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
bool: Whether the `--take-screenshots` enabled.
|
|
304
|
+
"""
|
|
305
|
+
if self._take_screenshots is None:
|
|
306
|
+
self._take_screenshots = self.data_path.screenshots_dir.exists()
|
|
307
|
+
return self._take_screenshots
|
|
308
|
+
|
|
309
|
+
@catchException("Error when marking screenshot")
|
|
310
|
+
def _mark_screenshot(self: "BugReportGenerator", step_data: "StepData"):
|
|
311
|
+
step_type = step_data["Type"]
|
|
312
|
+
screenshot_name = step_data["Screenshot"]
|
|
313
|
+
if not screenshot_name:
|
|
314
|
+
return
|
|
315
|
+
info = step_data.get("Info")
|
|
316
|
+
if not isinstance(info, dict):
|
|
317
|
+
return
|
|
318
|
+
|
|
319
|
+
if step_type == "Monkey":
|
|
320
|
+
act = info.get("act")
|
|
321
|
+
pos = info.get("pos")
|
|
322
|
+
if act in ["CLICK", "LONG_CLICK"] or act.startswith("SCROLL"):
|
|
323
|
+
self._mark_screenshot_interaction(step_type, screenshot_name, act, pos)
|
|
324
|
+
|
|
325
|
+
elif step_type == "Script":
|
|
326
|
+
act = info.get("method")
|
|
327
|
+
pos = info.get("params")
|
|
328
|
+
if act in ["click", "setText", "swipe"]:
|
|
329
|
+
self._mark_screenshot_interaction(step_type, screenshot_name, act, pos)
|
|
330
|
+
|
|
331
|
+
def _mark_screenshot_interaction(
|
|
332
|
+
self: "BugReportGenerator",
|
|
333
|
+
step_type: str, screenshot_name: str, action_type: str, position: Union[List, Tuple]
|
|
334
|
+
) -> bool:
|
|
335
|
+
"""
|
|
336
|
+
Mark interaction on screenshot with colored rectangle
|
|
337
|
+
|
|
338
|
+
Args:
|
|
339
|
+
step_type (str): Type of the step (Monkey or Script)
|
|
340
|
+
screenshot_name (str): Name of the screenshot file
|
|
341
|
+
action_type (str): Type of action (CLICK/LONG_CLICK/SCROLL for Monkey, click/setText/swipe for Script)
|
|
342
|
+
position: Position coordinates or parameters (format varies by action type)
|
|
343
|
+
|
|
344
|
+
Returns:
|
|
345
|
+
bool: True if marking was successful, False otherwise
|
|
346
|
+
"""
|
|
347
|
+
screenshot_path: Path = self.data_path.screenshots_dir / screenshot_name
|
|
348
|
+
if not screenshot_path.exists():
|
|
349
|
+
logger.debug(f"Screenshot file {screenshot_path} not exists.")
|
|
350
|
+
return False
|
|
351
|
+
|
|
352
|
+
try:
|
|
353
|
+
img = Image.open(screenshot_path).convert("RGB")
|
|
354
|
+
except OSError as e:
|
|
355
|
+
logger.debug(f"Error opening image {screenshot_path}: {e}")
|
|
356
|
+
return False
|
|
357
|
+
draw = ImageDraw.Draw(img)
|
|
358
|
+
line_width = 5
|
|
359
|
+
|
|
360
|
+
if step_type == "Monkey":
|
|
361
|
+
if len(position) < 4:
|
|
362
|
+
logger.warning(f"Monkey action requires 4 coordinates, got {len(position)}. Skip drawing.")
|
|
363
|
+
return False
|
|
364
|
+
|
|
365
|
+
x1, y1, x2, y2 = map(int, position[:4])
|
|
366
|
+
|
|
367
|
+
if action_type == "CLICK":
|
|
368
|
+
for i in range(line_width):
|
|
369
|
+
draw.rectangle([x1 - i, y1 - i, x2 + i, y2 + i], outline=(255, 0, 0))
|
|
370
|
+
elif action_type == "LONG_CLICK":
|
|
371
|
+
for i in range(line_width):
|
|
372
|
+
draw.rectangle([x1 - i, y1 - i, x2 + i, y2 + i], outline=(0, 0, 255))
|
|
373
|
+
elif action_type.startswith("SCROLL"):
|
|
374
|
+
for i in range(line_width):
|
|
375
|
+
draw.rectangle([x1 - i, y1 - i, x2 + i, y2 + i], outline=(0, 255, 0))
|
|
376
|
+
|
|
377
|
+
elif step_type == "Script":
|
|
378
|
+
if action_type == "click":
|
|
379
|
+
|
|
380
|
+
if len(position) < 2:
|
|
381
|
+
logger.warning(f"Script click action requires 2 coordinates, got {len(position)}. Skip drawing.")
|
|
382
|
+
return False
|
|
383
|
+
|
|
384
|
+
x, y = map(float, position[:2])
|
|
385
|
+
x1, y1, x2, y2 = x - 50, y - 50, x + 50, y + 50
|
|
386
|
+
|
|
387
|
+
for i in range(line_width):
|
|
388
|
+
draw.rectangle([x1 - i, y1 - i, x2 + i, y2 + i], outline=(255, 0, 0))
|
|
389
|
+
|
|
390
|
+
elif action_type == "swipe":
|
|
391
|
+
|
|
392
|
+
if len(position) < 4:
|
|
393
|
+
logger.warning(f"Script swipe action requires 4 coordinates, got {len(position)}. Skip drawing.")
|
|
394
|
+
return False
|
|
395
|
+
|
|
396
|
+
x1, y1, x2, y2 = map(float, position[:4])
|
|
397
|
+
|
|
398
|
+
# mark start and end positions with rectangles
|
|
399
|
+
start_x1, start_y1, start_x2, start_y2 = x1 - 50, y1 - 50, x1 + 50, y1 + 50
|
|
400
|
+
for i in range(line_width):
|
|
401
|
+
draw.rectangle([start_x1 - i, start_y1 - i, start_x2 + i, start_y2 + i], outline=(255, 0, 0))
|
|
402
|
+
|
|
403
|
+
end_x1, end_y1, end_x2, end_y2 = x2 - 50, y2 - 50, x2 + 50, y2 + 50
|
|
404
|
+
for i in range(line_width):
|
|
405
|
+
draw.rectangle([end_x1 - i, end_y1 - i, end_x2 + i, end_y2 + i], outline=(255, 0, 0))
|
|
406
|
+
|
|
407
|
+
# draw line between start and end positions
|
|
408
|
+
draw.line([(x1, y1), (x2, y2)], fill=(255, 0, 0), width=line_width)
|
|
409
|
+
|
|
410
|
+
# add text labels for start and end positions
|
|
411
|
+
font = ImageFont.truetype("arial.ttf", 80)
|
|
412
|
+
|
|
413
|
+
# draw "start" at start position
|
|
414
|
+
draw.text((x1 - 20, y1 - 70), "start", fill=(255, 0, 0), font=font)
|
|
415
|
+
|
|
416
|
+
# draw "end" at end position
|
|
417
|
+
draw.text((x2 - 15, y2 - 70), "end", fill=(255, 0, 0), font=font)
|
|
418
|
+
|
|
419
|
+
img.save(screenshot_path)
|
|
420
|
+
return True
|
|
421
|
+
|
|
422
|
+
def _add_screenshot_info(
|
|
423
|
+
self: "BugReportGenerator",
|
|
424
|
+
step_data: "StepData",
|
|
425
|
+
step_index: int,
|
|
426
|
+
data: Dict,
|
|
427
|
+
force_append: bool = False,
|
|
428
|
+
):
|
|
429
|
+
"""
|
|
430
|
+
Add screenshot information to data structure
|
|
431
|
+
|
|
432
|
+
Args:
|
|
433
|
+
step_data: data for the current step
|
|
434
|
+
step_index: Current step index
|
|
435
|
+
data: Data dictionary to update
|
|
436
|
+
"""
|
|
437
|
+
screenshot_name = step_data["Screenshot"]
|
|
438
|
+
if screenshot_name in self._all_screenshot_names:
|
|
439
|
+
return
|
|
440
|
+
self._all_screenshot_names.add(screenshot_name)
|
|
441
|
+
|
|
442
|
+
caption = ""
|
|
443
|
+
info = step_data.get("Info")
|
|
444
|
+
|
|
445
|
+
if step_data["Type"] == "Monkey":
|
|
446
|
+
# Extract 'act' attribute for Monkey type and add MonkeyStepsCount
|
|
447
|
+
monkey_steps_count = step_data.get('MonkeyStepsCount', 'N/A')
|
|
448
|
+
if isinstance(info, dict):
|
|
449
|
+
action = info.get('act', 'N/A')
|
|
450
|
+
else:
|
|
451
|
+
action = str(info) if info else 'N/A'
|
|
452
|
+
caption = f"Monkey Step {monkey_steps_count}: {action}"
|
|
453
|
+
elif step_data["Type"] == "Script":
|
|
454
|
+
# Extract 'method' attribute for Script type
|
|
455
|
+
if isinstance(info, dict):
|
|
456
|
+
caption = f"{info.get('method', 'N/A')}"
|
|
457
|
+
else:
|
|
458
|
+
caption = str(info) if info else "N/A"
|
|
459
|
+
elif step_data["Type"] == "ScriptInfo":
|
|
460
|
+
# Extract 'propName' and 'state' attributes for ScriptInfo type
|
|
461
|
+
if isinstance(info, dict):
|
|
462
|
+
prop_name = info.get('propName', '')
|
|
463
|
+
state = info.get('state', 'N/A')
|
|
464
|
+
else:
|
|
465
|
+
prop_name = ''
|
|
466
|
+
state = str(info) if info else 'N/A'
|
|
467
|
+
caption = f"{prop_name}: {state}" if prop_name else f"{state}"
|
|
468
|
+
elif step_data["Type"] == "Fuzz":
|
|
469
|
+
monkey_steps_count = step_data.get('MonkeyStepsCount', 'N/A')
|
|
470
|
+
caption = f"Monkey Step {monkey_steps_count}: Fuzz"
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
# Check if the screenshot file actually exists
|
|
474
|
+
screenshot_file_path = self.data_path.screenshots_dir / screenshot_name
|
|
475
|
+
if not screenshot_file_path.exists():
|
|
476
|
+
# Skip adding this screenshot if the file doesn't exist
|
|
477
|
+
return
|
|
478
|
+
|
|
479
|
+
# Use relative path string instead of Path object
|
|
480
|
+
abs_screenshots_path = self.data_path.output_dir / "screenshots" / screenshot_name
|
|
481
|
+
relative_screenshot_path = str(abs_screenshots_path.relative_to(self.result_dir))
|
|
482
|
+
|
|
483
|
+
if screenshot_name not in data["screenshot_info"]:
|
|
484
|
+
data["screenshot_info"][screenshot_name] = {
|
|
485
|
+
"type": step_data["Type"],
|
|
486
|
+
"caption": caption,
|
|
487
|
+
"step_index": step_index
|
|
488
|
+
}
|
|
489
|
+
elif not force_append:
|
|
490
|
+
return
|
|
491
|
+
|
|
492
|
+
self.screenshots.append({
|
|
493
|
+
'id': step_index,
|
|
494
|
+
'path': relative_screenshot_path, # Now using string path
|
|
495
|
+
'caption': f"{step_index}. {caption}"
|
|
496
|
+
})
|