Kea2-python 1.0.6b0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of Kea2-python might be problematic. Click here for more details.

Files changed (52) hide show
  1. kea2/__init__.py +3 -0
  2. kea2/absDriver.py +56 -0
  3. kea2/adbUtils.py +554 -0
  4. kea2/assets/config_version.json +16 -0
  5. kea2/assets/fastbot-thirdpart.jar +0 -0
  6. kea2/assets/fastbot_configs/abl.strings +2 -0
  7. kea2/assets/fastbot_configs/awl.strings +3 -0
  8. kea2/assets/fastbot_configs/max.config +7 -0
  9. kea2/assets/fastbot_configs/max.fuzzing.strings +699 -0
  10. kea2/assets/fastbot_configs/max.schema.strings +1 -0
  11. kea2/assets/fastbot_configs/max.strings +3 -0
  12. kea2/assets/fastbot_configs/max.tree.pruning +27 -0
  13. kea2/assets/fastbot_configs/teardown.py +18 -0
  14. kea2/assets/fastbot_configs/widget.block.py +38 -0
  15. kea2/assets/fastbot_libs/arm64-v8a/libfastbot_native.so +0 -0
  16. kea2/assets/fastbot_libs/armeabi-v7a/libfastbot_native.so +0 -0
  17. kea2/assets/fastbot_libs/x86/libfastbot_native.so +0 -0
  18. kea2/assets/fastbot_libs/x86_64/libfastbot_native.so +0 -0
  19. kea2/assets/framework.jar +0 -0
  20. kea2/assets/kea2-thirdpart.jar +0 -0
  21. kea2/assets/monkeyq.jar +0 -0
  22. kea2/assets/quicktest.py +126 -0
  23. kea2/cli.py +320 -0
  24. kea2/fastbotManager.py +267 -0
  25. kea2/fastbotx/ActivityTimes.py +52 -0
  26. kea2/fastbotx/ReuseEntry.py +74 -0
  27. kea2/fastbotx/ReuseModel.py +63 -0
  28. kea2/fastbotx/__init__.py +7 -0
  29. kea2/fbm_parser.py +871 -0
  30. kea2/fs_lock.py +131 -0
  31. kea2/kea2_api.py +166 -0
  32. kea2/keaUtils.py +1112 -0
  33. kea2/kea_launcher.py +319 -0
  34. kea2/logWatcher.py +92 -0
  35. kea2/mixin.py +22 -0
  36. kea2/report/__init__.py +0 -0
  37. kea2/report/bug_report_generator.py +793 -0
  38. kea2/report/mixin.py +482 -0
  39. kea2/report/report_merger.py +797 -0
  40. kea2/report/templates/bug_report_template.html +3876 -0
  41. kea2/report/templates/merged_bug_report_template.html +3333 -0
  42. kea2/report/utils.py +10 -0
  43. kea2/resultSyncer.py +65 -0
  44. kea2/u2Driver.py +610 -0
  45. kea2/utils.py +184 -0
  46. kea2/version_manager.py +102 -0
  47. kea2_python-1.0.6b0.dist-info/METADATA +447 -0
  48. kea2_python-1.0.6b0.dist-info/RECORD +52 -0
  49. kea2_python-1.0.6b0.dist-info/WHEEL +5 -0
  50. kea2_python-1.0.6b0.dist-info/entry_points.txt +2 -0
  51. kea2_python-1.0.6b0.dist-info/licenses/LICENSE +16 -0
  52. kea2_python-1.0.6b0.dist-info/top_level.txt +1 -0
kea2/report/mixin.py ADDED
@@ -0,0 +1,482 @@
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
+
297
+ @property
298
+ def take_screenshots(self: "BugReportGenerator") -> bool:
299
+ """Whether the `--take-screenshots` enabled. Should we report the screenshots?
300
+
301
+ Returns:
302
+ bool: Whether the `--take-screenshots` enabled.
303
+ """
304
+ if self._take_screenshots is None:
305
+ self._take_screenshots = self.data_path.screenshots_dir.exists()
306
+ return self._take_screenshots
307
+
308
+ @catchException("Error when marking screenshot")
309
+ def _mark_screenshot(self: "BugReportGenerator", step_data: "StepData"):
310
+ step_type = step_data["Type"]
311
+ screenshot_name = step_data["Screenshot"]
312
+ if not screenshot_name:
313
+ return
314
+ info = step_data.get("Info")
315
+ if not isinstance(info, dict):
316
+ return
317
+
318
+ if step_type == "Monkey":
319
+ act = info.get("act")
320
+ pos = info.get("pos")
321
+ if act in ["CLICK", "LONG_CLICK"] or act.startswith("SCROLL"):
322
+ self._mark_screenshot_interaction(step_type, screenshot_name, act, pos)
323
+
324
+ elif step_type == "Script":
325
+ act = info.get("method")
326
+ pos = info.get("params")
327
+ if act in ["click", "setText", "swipe"]:
328
+ self._mark_screenshot_interaction(step_type, screenshot_name, act, pos)
329
+
330
+ def _mark_screenshot_interaction(
331
+ self: "BugReportGenerator",
332
+ step_type: str, screenshot_name: str, action_type: str, position: Union[List, Tuple]
333
+ ) -> bool:
334
+ """
335
+ Mark interaction on screenshot with colored rectangle
336
+
337
+ Args:
338
+ step_type (str): Type of the step (Monkey or Script)
339
+ screenshot_name (str): Name of the screenshot file
340
+ action_type (str): Type of action (CLICK/LONG_CLICK/SCROLL for Monkey, click/setText/swipe for Script)
341
+ position: Position coordinates or parameters (format varies by action type)
342
+
343
+ Returns:
344
+ bool: True if marking was successful, False otherwise
345
+ """
346
+ screenshot_path: Path = self.data_path.screenshots_dir / screenshot_name
347
+ if not screenshot_path.exists():
348
+ logger.debug(f"Screenshot file {screenshot_path} not exists.")
349
+ return False
350
+
351
+ try:
352
+ img = Image.open(screenshot_path).convert("RGB")
353
+ except OSError as e:
354
+ logger.debug(f"Error opening image {screenshot_path}: {e}")
355
+ return False
356
+ draw = ImageDraw.Draw(img)
357
+ line_width = 5
358
+
359
+ if step_type == "Monkey":
360
+ if len(position) < 4:
361
+ logger.warning(f"Monkey action requires 4 coordinates, got {len(position)}. Skip drawing.")
362
+ return False
363
+
364
+ x1, y1, x2, y2 = map(int, position[:4])
365
+
366
+ if action_type == "CLICK":
367
+ for i in range(line_width):
368
+ draw.rectangle([x1 - i, y1 - i, x2 + i, y2 + i], outline=(255, 0, 0))
369
+ elif action_type == "LONG_CLICK":
370
+ for i in range(line_width):
371
+ draw.rectangle([x1 - i, y1 - i, x2 + i, y2 + i], outline=(0, 0, 255))
372
+ elif action_type.startswith("SCROLL"):
373
+ for i in range(line_width):
374
+ draw.rectangle([x1 - i, y1 - i, x2 + i, y2 + i], outline=(0, 255, 0))
375
+
376
+ elif step_type == "Script":
377
+ if action_type == "click":
378
+
379
+ if len(position) < 2:
380
+ logger.warning(f"Script click action requires 2 coordinates, got {len(position)}. Skip drawing.")
381
+ return False
382
+
383
+ x, y = map(float, position[:2])
384
+ x1, y1, x2, y2 = x - 50, y - 50, x + 50, y + 50
385
+
386
+ for i in range(line_width):
387
+ draw.rectangle([x1 - i, y1 - i, x2 + i, y2 + i], outline=(255, 0, 0))
388
+
389
+ elif action_type == "swipe":
390
+
391
+ if len(position) < 4:
392
+ logger.warning(f"Script swipe action requires 4 coordinates, got {len(position)}. Skip drawing.")
393
+ return False
394
+
395
+ x1, y1, x2, y2 = map(float, position[:4])
396
+
397
+ # mark start and end positions with rectangles
398
+ start_x1, start_y1, start_x2, start_y2 = x1 - 50, y1 - 50, x1 + 50, y1 + 50
399
+ for i in range(line_width):
400
+ draw.rectangle([start_x1 - i, start_y1 - i, start_x2 + i, start_y2 + i], outline=(255, 0, 0))
401
+
402
+ end_x1, end_y1, end_x2, end_y2 = x2 - 50, y2 - 50, x2 + 50, y2 + 50
403
+ for i in range(line_width):
404
+ draw.rectangle([end_x1 - i, end_y1 - i, end_x2 + i, end_y2 + i], outline=(255, 0, 0))
405
+
406
+ # draw line between start and end positions
407
+ draw.line([(x1, y1), (x2, y2)], fill=(255, 0, 0), width=line_width)
408
+
409
+ # add text labels for start and end positions
410
+ font = ImageFont.truetype("arial.ttf", 80)
411
+
412
+ # draw "start" at start position
413
+ draw.text((x1 - 20, y1 - 70), "start", fill=(255, 0, 0), font=font)
414
+
415
+ # draw "end" at end position
416
+ draw.text((x2 - 15, y2 - 70), "end", fill=(255, 0, 0), font=font)
417
+
418
+ img.save(screenshot_path)
419
+ return True
420
+
421
+ def _add_screenshot_info(self:"BugReportGenerator", step_data: "StepData", step_index: int, data: Dict):
422
+ """
423
+ Add screenshot information to data structure
424
+
425
+ Args:
426
+ step_data: data for the current step
427
+ step_index: Current step index
428
+ data: Data dictionary to update
429
+ """
430
+ caption = ""
431
+ info = step_data.get("Info")
432
+
433
+ if step_data["Type"] == "Monkey":
434
+ # Extract 'act' attribute for Monkey type and add MonkeyStepsCount
435
+ monkey_steps_count = step_data.get('MonkeyStepsCount', 'N/A')
436
+ if isinstance(info, dict):
437
+ action = info.get('act', 'N/A')
438
+ else:
439
+ action = str(info) if info else 'N/A'
440
+ caption = f"Monkey Step {monkey_steps_count}: {action}"
441
+ elif step_data["Type"] == "Script":
442
+ # Extract 'method' attribute for Script type
443
+ if isinstance(info, dict):
444
+ caption = f"{info.get('method', 'N/A')}"
445
+ else:
446
+ caption = str(info) if info else "N/A"
447
+ elif step_data["Type"] == "ScriptInfo":
448
+ # Extract 'propName' and 'state' attributes for ScriptInfo type
449
+ if isinstance(info, dict):
450
+ prop_name = info.get('propName', '')
451
+ state = info.get('state', 'N/A')
452
+ else:
453
+ prop_name = ''
454
+ state = str(info) if info else 'N/A'
455
+ caption = f"{prop_name}: {state}" if prop_name else f"{state}"
456
+ elif step_data["Type"] == "Fuzz":
457
+ monkey_steps_count = step_data.get('MonkeyStepsCount', 'N/A')
458
+ caption = f"Monkey Step {monkey_steps_count}: Fuzz"
459
+
460
+ screenshot_name = step_data["Screenshot"]
461
+
462
+ # Check if the screenshot file actually exists
463
+ screenshot_file_path = self.data_path.screenshots_dir / screenshot_name
464
+ if not screenshot_file_path.exists():
465
+ # Skip adding this screenshot if the file doesn't exist
466
+ return
467
+
468
+ # Use relative path string instead of Path object
469
+ abs_screenshots_path = self.data_path.output_dir / "screenshots" / screenshot_name
470
+ relative_screenshot_path = str(abs_screenshots_path.relative_to(self.result_dir))
471
+
472
+ data["screenshot_info"][screenshot_name] = {
473
+ "type": step_data["Type"],
474
+ "caption": caption,
475
+ "step_index": step_index
476
+ }
477
+
478
+ self.screenshots.append({
479
+ 'id': step_index,
480
+ 'path': relative_screenshot_path, # Now using string path
481
+ 'caption': f"{step_index}. {caption}"
482
+ })