Kea2-python 0.3.6__py3-none-any.whl → 1.0.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/__init__.py CHANGED
@@ -1 +1 @@
1
- from .keaUtils import KeaTestRunner, precondition, prob, max_tries, Options
1
+ from .keaUtils import KeaTestRunner, precondition, prob, max_tries, Options, interruptable,HybridTestRunner,kea2_breakpoint
@@ -0,0 +1,16 @@
1
+ {
2
+ "compatibility infos": [
3
+ {
4
+ "name": "previous version",
5
+ "description": "The default initial version, <=0.3.6",
6
+ "from": "0.0.0",
7
+ "to": "0.3.6"
8
+ },
9
+ {
10
+ "name": "Hybrid test version 1.0.0",
11
+ "description": "Hybrid test was added in version 1.0.0. hybrid_test_config.py is required.",
12
+ "from": "1.0.0",
13
+ "to": ""
14
+ }
15
+ ]
16
+ }
@@ -0,0 +1,18 @@
1
+ from uiautomator2 import Device
2
+ import time
3
+
4
+
5
+ class HybridTestCase:
6
+ d: Device
7
+
8
+ PACKAGE_NAME = "it.feio.android.omninotes.alpha"
9
+ MAIN_ACTIVITY = "it.feio.android.omninotes.MainActivity"
10
+
11
+
12
+ def setUp(self: HybridTestCase):
13
+ self.d.app_start(PACKAGE_NAME, MAIN_ACTIVITY)
14
+ time.sleep(2)
15
+
16
+
17
+ def tearDown(self: HybridTestCase):
18
+ self.d.app_stop(PACKAGE_NAME)
kea2/assets/monkeyq.jar CHANGED
Binary file
kea2/assets/quicktest.py CHANGED
@@ -9,9 +9,27 @@ from kea2.u2Driver import U2Driver
9
9
  class Omni_Notes_Sample(unittest.TestCase):
10
10
 
11
11
  def setUp(self):
12
- self.d = u2.connect()
12
+ self.d = u2.connect()
13
+
14
+ @prob(0.2)
15
+ @precondition(
16
+ lambda self: self.d(description="Navigate up").exists
17
+ )
18
+ def test_goBack(self):
19
+ print("Navigate back")
20
+ self.d(description="Navigate up").click()
21
+ sleep(0.5)
22
+
23
+ @prob(0.2)
24
+ @precondition(
25
+ lambda self: self.d(description="drawer closed").exists
26
+ )
27
+ def test_openDrawer(self):
28
+ print("Open drawer")
29
+ self.d(description="drawer closed").click()
30
+ sleep(0.5)
13
31
 
14
- @prob(0.7) # The probability of executing the function when precondition is satisfied.
32
+ @prob(0.5) # The probability of executing the function when precondition is satisfied.
15
33
  @precondition(
16
34
  lambda self: self.d(text="Omni Notes Alpha").exists
17
35
  and self.d(text="Settings").exists
@@ -45,6 +63,7 @@ class Omni_Notes_Sample(unittest.TestCase):
45
63
  assertion:
46
64
  The search input box is still being opened
47
65
  """
66
+ print("rotate the device")
48
67
  self.d.set_orientation("l")
49
68
  sleep(2)
50
69
  self.d.set_orientation("n")
@@ -375,21 +375,32 @@ class BugReportGenerator:
375
375
  minutes, seconds = divmod(remainder, 60)
376
376
  data["total_testing_time"] = f"{hours:02d}:{minutes:02d}:{seconds:02d}"
377
377
 
378
- # Calculate bug count directly from result data
378
+ # Enrich property statistics with derived metrics and calculate bug count
379
+ enriched_property_stats = {}
379
380
  for property_name, test_result in self.test_result.items():
380
381
  # Check if failed or error
381
- if test_result["fail"] > 0 or test_result["error"] > 0:
382
+ if test_result.get("fail", 0) > 0 or test_result.get("error", 0) > 0:
382
383
  data["bugs_found"] += 1
383
384
 
384
- # Store the raw result data for direct use in HTML template
385
- data["property_stats"] = self.test_result
385
+ executed_count = test_result.get("executed", 0)
386
+ fail_count = test_result.get("fail", 0)
387
+ error_count = test_result.get("error", 0)
388
+ pass_count = max(executed_count - fail_count - error_count, 0)
389
+
390
+ enriched_property_stats[property_name] = {
391
+ **test_result,
392
+ "pass_count": pass_count
393
+ }
394
+
395
+ # Store the enriched result data for direct use in HTML template
396
+ data["property_stats"] = enriched_property_stats
386
397
 
387
398
  # Calculate properties statistics
388
399
  data["all_properties_count"] = len(self.test_result)
389
400
  data["executed_properties_count"] = sum(1 for result in self.test_result.values() if result.get("executed", 0) > 0)
390
401
 
391
402
  # Calculate detailed property statistics for table headers
392
- property_stats_summary = self._calculate_property_stats_summary(self.test_result)
403
+ property_stats_summary = self._calculate_property_stats_summary(enriched_property_stats)
393
404
  data["property_stats_summary"] = property_stats_summary
394
405
 
395
406
  # Process coverage data
@@ -415,6 +426,11 @@ class BugReportGenerator:
415
426
 
416
427
  # Load crash and ANR events from crash-dump.log
417
428
  crash_events, anr_events = self._load_crash_dump_data()
429
+
430
+ # Add screenshot ID information to crash and ANR events
431
+ self._add_screenshot_ids_to_events(crash_events)
432
+ self._add_screenshot_ids_to_events(anr_events)
433
+
418
434
  data["crash_events"] = crash_events
419
435
  data["anr_events"] = anr_events
420
436
 
@@ -460,10 +476,14 @@ class BugReportGenerator:
460
476
  """
461
477
  screenshot_path: Path = self.data_path.screenshots_dir / screenshot_name
462
478
  if not screenshot_path.exists():
463
- logger.error(f"Screenshot file {screenshot_path} not exists.")
479
+ logger.debug(f"Screenshot file {screenshot_path} not exists.")
464
480
  return False
465
481
 
466
- img = Image.open(screenshot_path).convert("RGB")
482
+ try:
483
+ img = Image.open(screenshot_path).convert("RGB")
484
+ except OSError as e:
485
+ logger.debug(f"Error opening image {screenshot_path}: {e}")
486
+ return False
467
487
  draw = ImageDraw.Draw(img)
468
488
  line_width = 5
469
489
 
@@ -572,6 +592,8 @@ class BugReportGenerator:
572
592
  'activity_count_history': data["activity_count_history"],
573
593
  'crash_events': data["crash_events"],
574
594
  'anr_events': data["anr_events"],
595
+ 'triggered_crash_count': len(data["crash_events"]),
596
+ 'triggered_anr_count': len(data["anr_events"]),
575
597
  'property_stats_summary': data["property_stats_summary"]
576
598
  }
577
599
 
@@ -612,6 +634,13 @@ class BugReportGenerator:
612
634
  caption = f"{prop_name}: {state}" if prop_name else f"{state}"
613
635
 
614
636
  screenshot_name = step_data["Screenshot"]
637
+
638
+ # Check if the screenshot file actually exists
639
+ screenshot_file_path = self.data_path.screenshots_dir / screenshot_name
640
+ if not screenshot_file_path.exists():
641
+ # Skip adding this screenshot if the file doesn't exist
642
+ return
643
+
615
644
  # Use relative path string instead of Path object
616
645
  relative_screenshot_path = f"output_{self.log_timestamp}/screenshots/{screenshot_name}"
617
646
 
@@ -841,20 +870,28 @@ class BugReportGenerator:
841
870
  "total_properties": 0,
842
871
  "total_precond_satisfied": 0,
843
872
  "total_executed": 0,
873
+ "total_passes": 0,
844
874
  "total_fails": 0,
845
875
  "total_errors": 0,
846
876
  "properties_with_errors": 0
847
877
  }
848
878
 
849
879
  for property_name, result in test_result.items():
880
+ executed_count = result.get("executed", result.get("executed_total", 0))
881
+ fail_count = result.get("fail", 0)
882
+ error_count = result.get("error", 0)
883
+ pass_count = result.get("pass_count",
884
+ max(executed_count - fail_count - error_count, 0))
885
+
850
886
  stats_summary["total_properties"] += 1
851
887
  stats_summary["total_precond_satisfied"] += result.get("precond_satisfied", 0)
852
- stats_summary["total_executed"] += result.get("executed", 0)
853
- stats_summary["total_fails"] += result.get("fail", 0)
854
- stats_summary["total_errors"] += result.get("error", 0)
888
+ stats_summary["total_executed"] += executed_count
889
+ stats_summary["total_passes"] += pass_count
890
+ stats_summary["total_fails"] += fail_count
891
+ stats_summary["total_errors"] += error_count
855
892
 
856
893
  # Count properties that have errors or fails
857
- if result.get("fail", 0) > 0 or result.get("error", 0) > 0:
894
+ if fail_count > 0 or error_count > 0:
858
895
  stats_summary["properties_with_errors"] += 1
859
896
 
860
897
  return stats_summary
@@ -877,11 +914,11 @@ class BugReportGenerator:
877
914
  with open(self.data_path.crash_dump_log, "r", encoding="utf-8") as f:
878
915
  content = f.read()
879
916
 
880
- # Parse crash events
881
- crash_events = self._parse_crash_events(content)
917
+ # Parse crash events with screenshot mapping
918
+ crash_events = self._parse_crash_events_with_screenshots(content)
882
919
 
883
- # Parse ANR events
884
- anr_events = self._parse_anr_events(content)
920
+ # Parse ANR events with screenshot mapping
921
+ anr_events = self._parse_anr_events_with_screenshots(content)
885
922
 
886
923
  logger.debug(f"Found {len(crash_events)} crash events and {len(anr_events)} ANR events")
887
924
 
@@ -889,26 +926,29 @@ class BugReportGenerator:
889
926
 
890
927
  except Exception as e:
891
928
  logger.error(f"Error reading crash dump file: {e}")
929
+ return crash_events, anr_events
892
930
 
893
-
894
- def _parse_crash_events(self, content: str) -> List[Dict]:
931
+ def _parse_crash_events_with_screenshots(self, content: str) -> List[Dict]:
895
932
  """
896
- Parse crash events from crash-dump.log content
933
+ Parse crash events from crash-dump.log content with screenshot mapping
897
934
 
898
935
  Args:
899
936
  content: Content of crash-dump.log file
900
937
 
901
938
  Returns:
902
- List[Dict]: List of crash event dictionaries
939
+ List[Dict]: List of crash event dictionaries with screenshot information
903
940
  """
904
941
  crash_events = []
905
942
 
906
- # Pattern to match crash blocks
907
- crash_pattern = r'(\d{14})\ncrash:\n(.*?)\n// crash end'
943
+ # Pattern to match crash blocks with optional screenshot information
944
+ # Look for StepsCount and CrashScreen before the timestamp
945
+ crash_pattern = r'(?:StepsCount:\s*(\d+)\s*\nCrashScreen:\s*([^\n]+)\s*\n)?(\d{14})\ncrash:\n(.*?)\n// crash end'
908
946
 
909
947
  for match in re.finditer(crash_pattern, content, re.DOTALL):
910
- timestamp_str = match.group(1)
911
- crash_content = match.group(2)
948
+ steps_count = match.group(1)
949
+ crash_screen = match.group(2)
950
+ timestamp_str = match.group(3)
951
+ crash_content = match.group(4)
912
952
 
913
953
  # Parse timestamp (format: YYYYMMDDHHMMSS)
914
954
  try:
@@ -924,31 +964,35 @@ class BugReportGenerator:
924
964
  "time": formatted_time,
925
965
  "exception_type": crash_info.get("exception_type", "Unknown"),
926
966
  "process": crash_info.get("process", "Unknown"),
927
- "stack_trace": crash_info.get("stack_trace", "")
967
+ "stack_trace": crash_info.get("stack_trace", ""),
968
+ "steps_count": steps_count,
969
+ "crash_screen": crash_screen.strip() if crash_screen else None
928
970
  }
929
971
 
930
972
  crash_events.append(crash_event)
931
973
 
932
974
  return crash_events
933
975
 
934
- def _parse_anr_events(self, content: str) -> List[Dict]:
976
+ def _parse_anr_events_with_screenshots(self, content: str) -> List[Dict]:
935
977
  """
936
- Parse ANR events from crash-dump.log content
978
+ Parse ANR events from crash-dump.log content with screenshot mapping
937
979
 
938
980
  Args:
939
981
  content: Content of crash-dump.log file
940
982
 
941
983
  Returns:
942
- List[Dict]: List of ANR event dictionaries
984
+ List[Dict]: List of ANR event dictionaries with screenshot information
943
985
  """
944
986
  anr_events = []
945
987
 
946
- # Pattern to match ANR blocks
947
- anr_pattern = r'(\d{14})\nanr:\n(.*?)\nanr end'
988
+ # Pattern to match ANR blocks with optional screenshot information
989
+ anr_pattern = r'(?:StepsCount:\s*(\d+)\s*\nCrashScreen:\s*([^\n]+)\s*\n)?(\d{14})\nanr:\n(.*?)\nanr end'
948
990
 
949
991
  for match in re.finditer(anr_pattern, content, re.DOTALL):
950
- timestamp_str = match.group(1)
951
- anr_content = match.group(2)
992
+ steps_count = match.group(1)
993
+ crash_screen = match.group(2)
994
+ timestamp_str = match.group(3)
995
+ anr_content = match.group(4)
952
996
 
953
997
  # Parse timestamp (format: YYYYMMDDHHMMSS)
954
998
  try:
@@ -964,13 +1008,51 @@ class BugReportGenerator:
964
1008
  "time": formatted_time,
965
1009
  "reason": anr_info.get("reason", "Unknown"),
966
1010
  "process": anr_info.get("process", "Unknown"),
967
- "trace": anr_info.get("trace", "")
1011
+ "trace": anr_info.get("trace", ""),
1012
+ "steps_count": steps_count,
1013
+ "crash_screen": crash_screen.strip() if crash_screen else None
968
1014
  }
969
1015
 
970
1016
  anr_events.append(anr_event)
971
1017
 
972
1018
  return anr_events
973
1019
 
1020
+ def _find_screenshot_id_by_filename(self, screenshot_filename: str) -> str:
1021
+ """
1022
+ Find screenshot ID by filename in the screenshots list
1023
+
1024
+ Args:
1025
+ screenshot_filename: Name of the screenshot file
1026
+
1027
+ Returns:
1028
+ str: Screenshot ID if found, empty string otherwise
1029
+ """
1030
+ if not screenshot_filename:
1031
+ return ""
1032
+
1033
+ for screenshot in self.screenshots:
1034
+ # Extract filename from path
1035
+ screenshot_path = screenshot.get('path', '')
1036
+ if screenshot_path.endswith(screenshot_filename):
1037
+ return str(screenshot.get('id', ''))
1038
+
1039
+ return ""
1040
+
1041
+ def _add_screenshot_ids_to_events(self, events: List[Dict]):
1042
+ """
1043
+ Add screenshot ID information to crash/ANR events
1044
+
1045
+ Args:
1046
+ events: List of crash or ANR event dictionaries
1047
+ """
1048
+ for event in events:
1049
+ crash_screen = event.get('crash_screen')
1050
+ if crash_screen:
1051
+ screenshot_id = self._find_screenshot_id_by_filename(crash_screen)
1052
+ event['screenshot_id'] = screenshot_id
1053
+ else:
1054
+ event['screenshot_id'] = ""
1055
+
974
1056
  def _extract_crash_info(self, crash_content: str) -> Dict:
975
1057
  """
976
1058
  Extract crash information from crash content
@@ -1109,8 +1191,8 @@ class BugReportGenerator:
1109
1191
  if __name__ == "__main__":
1110
1192
  print("Generating bug report")
1111
1193
  # OUTPUT_PATH = "<Your output path>"
1112
- OUTPUT_PATH = "P:/Python/Kea2/output/res_2025072011_5048015228"
1194
+ OUTPUT_PATH = "/Users/drifter327/Code/Kea2/output/res_2025090122_1216279438"
1113
1195
 
1114
1196
  report_generator = BugReportGenerator()
1115
1197
  report_path = report_generator.generate_report(OUTPUT_PATH)
1116
- print(f"bug report generated: {report_path}")
1198
+ print(f"bug report generated: {report_path}")
kea2/cli.py CHANGED
@@ -2,9 +2,11 @@
2
2
  # cli.py
3
3
 
4
4
  from __future__ import absolute_import, print_function
5
+ from datetime import datetime
5
6
  import sys
6
7
  from .utils import getProjectRoot, getLogger
7
8
  from .kea_launcher import run
9
+ from .version_manager import check_config_compatibility, get_cur_version
8
10
  import argparse
9
11
 
10
12
  import os
@@ -15,8 +17,7 @@ logger = getLogger(__name__)
15
17
 
16
18
 
17
19
  def cmd_version(args):
18
- from importlib.metadata import version
19
- print(version("Kea2-python"), flush=True)
20
+ print(get_cur_version(), flush=True)
20
21
 
21
22
 
22
23
  def cmd_init(args):
@@ -36,9 +37,16 @@ def cmd_init(args):
36
37
  src = Path(__file__).parent / "assets" / "quicktest.py"
37
38
  dst = cwd / "quicktest.py"
38
39
  shutil.copyfile(src, dst)
40
+
41
+ def save_version():
42
+ import json
43
+ version_file = configs_dir / "version.json"
44
+ with open(version_file, "w") as fp:
45
+ json.dump({"version": get_cur_version(), "init date": datetime.now().strftime("%Y-%m-%d %H:%M:%S")}, fp, indent=4)
39
46
 
40
47
  copy_configs()
41
48
  copy_samples()
49
+ save_version()
42
50
  logger.info("Kea2 project initialized.")
43
51
 
44
52
 
@@ -94,11 +102,9 @@ def cmd_merge(args):
94
102
  for path in args.paths:
95
103
  path_obj = Path(path)
96
104
  if not path_obj.exists():
97
- logger.error(f"Test report path does not exist: {path}")
98
- return
105
+ raise FileNotFoundError(f"{path_obj}")
99
106
  if not path_obj.is_dir():
100
- logger.error(f"Path is not a directory: {path}")
101
- return
107
+ raise NotADirectoryError(f"{path_obj}")
102
108
 
103
109
  logger.debug(f"Merging {len(args.paths)} test report directories...")
104
110
 
@@ -118,7 +124,7 @@ def cmd_merge(args):
118
124
  print(f"📈 Merged {merge_summary.get('merged_directories', 0)} directories", flush=True)
119
125
 
120
126
  except Exception as e:
121
- logger.error(f"Error during merge operation: {e}")
127
+ logger.error(f"Error during merge operation: {e}")
122
128
 
123
129
 
124
130
  def cmd_run(args):
@@ -126,6 +132,9 @@ def cmd_run(args):
126
132
  if base_dir is None:
127
133
  logger.error("kea2 project not initialized. Use `kea2 init`.")
128
134
  return
135
+
136
+ check_config_compatibility()
137
+
129
138
  run(args)
130
139
 
131
140
 
@@ -210,9 +219,10 @@ def main():
210
219
  args = parser.parse_args()
211
220
 
212
221
  import logging
213
- logging.basicConfig(level=logging.INFO)
222
+ from .utils import LoggingLevel
223
+ LoggingLevel.set_level(logging.INFO)
214
224
  if args.debug:
215
- logging.basicConfig(level=logging.DEBUG)
225
+ LoggingLevel.set_level(logging.DEBUG)
216
226
  logger.debug("args: %s", args)
217
227
 
218
228
  if args.subparser:
kea2/fastbotManager.py CHANGED
@@ -1,8 +1,10 @@
1
+ import itertools
1
2
  from retry import retry
2
3
  from retry.api import retry_call
3
4
  from dataclasses import asdict
4
5
  import requests
5
6
  from packaging.version import parse as parse_version
7
+ from time import sleep
6
8
 
7
9
  from uiautomator2.core import HTTPResponse, _http_request
8
10
  from kea2.adbUtils import ADBDevice, ADBStreamShell_V2
@@ -28,6 +30,7 @@ class FastbotManager:
28
30
  ADBDevice.setDevice(options.serial, options.transport_id)
29
31
  self.dev = ADBDevice()
30
32
  self.android_release = parse_version(self.dev.getprop("ro.build.version.release"))
33
+ self.executed_prop = False
31
34
 
32
35
  def _activateFastbot(self) -> ADBStreamShell_V2:
33
36
  """
@@ -100,7 +103,9 @@ class FastbotManager:
100
103
  _http_request(dev=self.dev, device_port=8090, method="GET", path="/ping")
101
104
 
102
105
  try:
103
- retry_call(_check_alive_request, tries=10, delay=2)
106
+ logger.info("Connecting to fastbot server...")
107
+ retry_call(_check_alive_request, tries=10, delay=2, logger=logger)
108
+ logger.info("Connected to fastbot server.")
104
109
  except requests.ConnectionError:
105
110
  raise RuntimeError("Failed to connect fastbot")
106
111
 
@@ -111,7 +116,8 @@ class FastbotManager:
111
116
  def init(self, options: "Options", stamp):
112
117
  post_data = {
113
118
  "takeScreenshots": options.take_screenshots,
114
- "Stamp": stamp,
119
+ "preFailureScreenshots": options.pre_failure_screenshots,
120
+ "logStamp": stamp,
115
121
  "deviceOutputRoot": options.device_output_root,
116
122
  }
117
123
  r = _http_request(
@@ -161,6 +167,15 @@ class FastbotManager:
161
167
  res = r.text
162
168
  if res != "OK":
163
169
  print(f"[ERROR] Error when logging script: {execution_info}", flush=True)
170
+
171
+ @retry(Exception, tries=2, delay=2)
172
+ def dumpHierarchy(self):
173
+ sleep(self.options.throttle / 1000)
174
+ r = self.request(
175
+ method="GET",
176
+ path="/dumpHierarchy",
177
+ )
178
+ return r.json()['result']
164
179
 
165
180
  @property
166
181
  def device_output_dir(self):
@@ -173,10 +188,8 @@ class FastbotManager:
173
188
  "/sdcard/framework.jar:"
174
189
  "/sdcard/fastbot-thirdpart.jar:"
175
190
  "/sdcard/kea2-thirdpart.jar",
176
-
177
191
  "exec", "app_process",
178
192
  "/system/bin", "com.android.commands.monkey.Monkey",
179
- "-p", *self.options.packageNames,
180
193
  "--agent-u2" if self.options.agent == "u2" else "--agent",
181
194
  "reuseq",
182
195
  "--running-minutes", f"{self.options.running_mins}",
@@ -185,6 +198,9 @@ class FastbotManager:
185
198
  "--output-directory", f"{self.options.device_output_root}/output_{self.options.log_stamp}",
186
199
  ]
187
200
 
201
+ pkgs = itertools.chain.from_iterable(["-p", pkg] for pkg in self.options.packageNames)
202
+ shell_command.extend(pkgs)
203
+
188
204
  if self.options.profile_period:
189
205
  shell_command += ["--profile-period", f"{self.options.profile_period}"]
190
206