Kea2-python 0.3.5__py3-none-any.whl → 1.0.0__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 +1 -1
- kea2/assets/config_version.json +16 -0
- kea2/assets/fastbot_configs/teardown.py +18 -0
- kea2/assets/monkeyq.jar +0 -0
- kea2/assets/quicktest.py +21 -2
- kea2/bug_report_generator.py +115 -35
- kea2/cli.py +19 -9
- kea2/fastbotManager.py +27 -7
- kea2/keaUtils.py +362 -110
- kea2/kea_launcher.py +65 -20
- kea2/mixin.py +22 -0
- kea2/report_merger.py +107 -42
- kea2/resultSyncer.py +1 -1
- kea2/templates/bug_report_template.html +139 -13
- kea2/templates/merged_bug_report_template.html +3293 -3213
- kea2/u2Driver.py +18 -8
- kea2/utils.py +60 -14
- kea2/version_manager.py +101 -0
- {kea2_python-0.3.5.dist-info → kea2_python-1.0.0.dist-info}/METADATA +64 -15
- {kea2_python-0.3.5.dist-info → kea2_python-1.0.0.dist-info}/RECORD +24 -20
- {kea2_python-0.3.5.dist-info → kea2_python-1.0.0.dist-info}/WHEEL +0 -0
- {kea2_python-0.3.5.dist-info → kea2_python-1.0.0.dist-info}/entry_points.txt +0 -0
- {kea2_python-0.3.5.dist-info → kea2_python-1.0.0.dist-info}/licenses/LICENSE +0 -0
- {kea2_python-0.3.5.dist-info → kea2_python-1.0.0.dist-info}/top_level.txt +0 -0
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.
|
|
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")
|
kea2/bug_report_generator.py
CHANGED
|
@@ -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
|
-
#
|
|
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
|
|
382
|
+
if test_result.get("fail", 0) > 0 or test_result.get("error", 0) > 0:
|
|
382
383
|
data["bugs_found"] += 1
|
|
383
384
|
|
|
384
|
-
|
|
385
|
-
|
|
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(
|
|
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.
|
|
479
|
+
logger.debug(f"Screenshot file {screenshot_path} not exists.")
|
|
464
480
|
return False
|
|
465
481
|
|
|
466
|
-
|
|
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
|
|
|
@@ -538,7 +558,7 @@ class BugReportGenerator:
|
|
|
538
558
|
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
539
559
|
|
|
540
560
|
# Ensure coverage_trend has data
|
|
541
|
-
if not data
|
|
561
|
+
if not data.get("coverage_trend"):
|
|
542
562
|
logger.warning("No coverage trend data")
|
|
543
563
|
# Use the same field names as in coverage.log file
|
|
544
564
|
data["coverage_trend"] = [{"stepsCount": 0, "coverage": 0, "testedActivitiesCount": 0}]
|
|
@@ -612,6 +632,13 @@ class BugReportGenerator:
|
|
|
612
632
|
caption = f"{prop_name}: {state}" if prop_name else f"{state}"
|
|
613
633
|
|
|
614
634
|
screenshot_name = step_data["Screenshot"]
|
|
635
|
+
|
|
636
|
+
# Check if the screenshot file actually exists
|
|
637
|
+
screenshot_file_path = self.data_path.screenshots_dir / screenshot_name
|
|
638
|
+
if not screenshot_file_path.exists():
|
|
639
|
+
# Skip adding this screenshot if the file doesn't exist
|
|
640
|
+
return
|
|
641
|
+
|
|
615
642
|
# Use relative path string instead of Path object
|
|
616
643
|
relative_screenshot_path = f"output_{self.log_timestamp}/screenshots/{screenshot_name}"
|
|
617
644
|
|
|
@@ -841,20 +868,28 @@ class BugReportGenerator:
|
|
|
841
868
|
"total_properties": 0,
|
|
842
869
|
"total_precond_satisfied": 0,
|
|
843
870
|
"total_executed": 0,
|
|
871
|
+
"total_passes": 0,
|
|
844
872
|
"total_fails": 0,
|
|
845
873
|
"total_errors": 0,
|
|
846
874
|
"properties_with_errors": 0
|
|
847
875
|
}
|
|
848
876
|
|
|
849
877
|
for property_name, result in test_result.items():
|
|
878
|
+
executed_count = result.get("executed", result.get("executed_total", 0))
|
|
879
|
+
fail_count = result.get("fail", 0)
|
|
880
|
+
error_count = result.get("error", 0)
|
|
881
|
+
pass_count = result.get("pass_count",
|
|
882
|
+
max(executed_count - fail_count - error_count, 0))
|
|
883
|
+
|
|
850
884
|
stats_summary["total_properties"] += 1
|
|
851
885
|
stats_summary["total_precond_satisfied"] += result.get("precond_satisfied", 0)
|
|
852
|
-
stats_summary["total_executed"] +=
|
|
853
|
-
stats_summary["
|
|
854
|
-
stats_summary["
|
|
886
|
+
stats_summary["total_executed"] += executed_count
|
|
887
|
+
stats_summary["total_passes"] += pass_count
|
|
888
|
+
stats_summary["total_fails"] += fail_count
|
|
889
|
+
stats_summary["total_errors"] += error_count
|
|
855
890
|
|
|
856
891
|
# Count properties that have errors or fails
|
|
857
|
-
if
|
|
892
|
+
if fail_count > 0 or error_count > 0:
|
|
858
893
|
stats_summary["properties_with_errors"] += 1
|
|
859
894
|
|
|
860
895
|
return stats_summary
|
|
@@ -877,11 +912,11 @@ class BugReportGenerator:
|
|
|
877
912
|
with open(self.data_path.crash_dump_log, "r", encoding="utf-8") as f:
|
|
878
913
|
content = f.read()
|
|
879
914
|
|
|
880
|
-
# Parse crash events
|
|
881
|
-
crash_events = self.
|
|
915
|
+
# Parse crash events with screenshot mapping
|
|
916
|
+
crash_events = self._parse_crash_events_with_screenshots(content)
|
|
882
917
|
|
|
883
|
-
# Parse ANR events
|
|
884
|
-
anr_events = self.
|
|
918
|
+
# Parse ANR events with screenshot mapping
|
|
919
|
+
anr_events = self._parse_anr_events_with_screenshots(content)
|
|
885
920
|
|
|
886
921
|
logger.debug(f"Found {len(crash_events)} crash events and {len(anr_events)} ANR events")
|
|
887
922
|
|
|
@@ -889,26 +924,29 @@ class BugReportGenerator:
|
|
|
889
924
|
|
|
890
925
|
except Exception as e:
|
|
891
926
|
logger.error(f"Error reading crash dump file: {e}")
|
|
927
|
+
return crash_events, anr_events
|
|
892
928
|
|
|
893
|
-
|
|
894
|
-
def _parse_crash_events(self, content: str) -> List[Dict]:
|
|
929
|
+
def _parse_crash_events_with_screenshots(self, content: str) -> List[Dict]:
|
|
895
930
|
"""
|
|
896
|
-
Parse crash events from crash-dump.log content
|
|
931
|
+
Parse crash events from crash-dump.log content with screenshot mapping
|
|
897
932
|
|
|
898
933
|
Args:
|
|
899
934
|
content: Content of crash-dump.log file
|
|
900
935
|
|
|
901
936
|
Returns:
|
|
902
|
-
List[Dict]: List of crash event dictionaries
|
|
937
|
+
List[Dict]: List of crash event dictionaries with screenshot information
|
|
903
938
|
"""
|
|
904
939
|
crash_events = []
|
|
905
940
|
|
|
906
|
-
# Pattern to match crash blocks
|
|
907
|
-
|
|
941
|
+
# Pattern to match crash blocks with optional screenshot information
|
|
942
|
+
# Look for StepsCount and CrashScreen before the timestamp
|
|
943
|
+
crash_pattern = r'(?:StepsCount:\s*(\d+)\s*\nCrashScreen:\s*([^\n]+)\s*\n)?(\d{14})\ncrash:\n(.*?)\n// crash end'
|
|
908
944
|
|
|
909
945
|
for match in re.finditer(crash_pattern, content, re.DOTALL):
|
|
910
|
-
|
|
911
|
-
|
|
946
|
+
steps_count = match.group(1)
|
|
947
|
+
crash_screen = match.group(2)
|
|
948
|
+
timestamp_str = match.group(3)
|
|
949
|
+
crash_content = match.group(4)
|
|
912
950
|
|
|
913
951
|
# Parse timestamp (format: YYYYMMDDHHMMSS)
|
|
914
952
|
try:
|
|
@@ -924,31 +962,35 @@ class BugReportGenerator:
|
|
|
924
962
|
"time": formatted_time,
|
|
925
963
|
"exception_type": crash_info.get("exception_type", "Unknown"),
|
|
926
964
|
"process": crash_info.get("process", "Unknown"),
|
|
927
|
-
"stack_trace": crash_info.get("stack_trace", "")
|
|
965
|
+
"stack_trace": crash_info.get("stack_trace", ""),
|
|
966
|
+
"steps_count": steps_count,
|
|
967
|
+
"crash_screen": crash_screen.strip() if crash_screen else None
|
|
928
968
|
}
|
|
929
969
|
|
|
930
970
|
crash_events.append(crash_event)
|
|
931
971
|
|
|
932
972
|
return crash_events
|
|
933
973
|
|
|
934
|
-
def
|
|
974
|
+
def _parse_anr_events_with_screenshots(self, content: str) -> List[Dict]:
|
|
935
975
|
"""
|
|
936
|
-
Parse ANR events from crash-dump.log content
|
|
976
|
+
Parse ANR events from crash-dump.log content with screenshot mapping
|
|
937
977
|
|
|
938
978
|
Args:
|
|
939
979
|
content: Content of crash-dump.log file
|
|
940
980
|
|
|
941
981
|
Returns:
|
|
942
|
-
List[Dict]: List of ANR event dictionaries
|
|
982
|
+
List[Dict]: List of ANR event dictionaries with screenshot information
|
|
943
983
|
"""
|
|
944
984
|
anr_events = []
|
|
945
985
|
|
|
946
|
-
# Pattern to match ANR blocks
|
|
947
|
-
anr_pattern = r'(\d{14})\nanr:\n(.*?)\nanr end'
|
|
986
|
+
# Pattern to match ANR blocks with optional screenshot information
|
|
987
|
+
anr_pattern = r'(?:StepsCount:\s*(\d+)\s*\nCrashScreen:\s*([^\n]+)\s*\n)?(\d{14})\nanr:\n(.*?)\nanr end'
|
|
948
988
|
|
|
949
989
|
for match in re.finditer(anr_pattern, content, re.DOTALL):
|
|
950
|
-
|
|
951
|
-
|
|
990
|
+
steps_count = match.group(1)
|
|
991
|
+
crash_screen = match.group(2)
|
|
992
|
+
timestamp_str = match.group(3)
|
|
993
|
+
anr_content = match.group(4)
|
|
952
994
|
|
|
953
995
|
# Parse timestamp (format: YYYYMMDDHHMMSS)
|
|
954
996
|
try:
|
|
@@ -964,13 +1006,51 @@ class BugReportGenerator:
|
|
|
964
1006
|
"time": formatted_time,
|
|
965
1007
|
"reason": anr_info.get("reason", "Unknown"),
|
|
966
1008
|
"process": anr_info.get("process", "Unknown"),
|
|
967
|
-
"trace": anr_info.get("trace", "")
|
|
1009
|
+
"trace": anr_info.get("trace", ""),
|
|
1010
|
+
"steps_count": steps_count,
|
|
1011
|
+
"crash_screen": crash_screen.strip() if crash_screen else None
|
|
968
1012
|
}
|
|
969
1013
|
|
|
970
1014
|
anr_events.append(anr_event)
|
|
971
1015
|
|
|
972
1016
|
return anr_events
|
|
973
1017
|
|
|
1018
|
+
def _find_screenshot_id_by_filename(self, screenshot_filename: str) -> str:
|
|
1019
|
+
"""
|
|
1020
|
+
Find screenshot ID by filename in the screenshots list
|
|
1021
|
+
|
|
1022
|
+
Args:
|
|
1023
|
+
screenshot_filename: Name of the screenshot file
|
|
1024
|
+
|
|
1025
|
+
Returns:
|
|
1026
|
+
str: Screenshot ID if found, empty string otherwise
|
|
1027
|
+
"""
|
|
1028
|
+
if not screenshot_filename:
|
|
1029
|
+
return ""
|
|
1030
|
+
|
|
1031
|
+
for screenshot in self.screenshots:
|
|
1032
|
+
# Extract filename from path
|
|
1033
|
+
screenshot_path = screenshot.get('path', '')
|
|
1034
|
+
if screenshot_path.endswith(screenshot_filename):
|
|
1035
|
+
return str(screenshot.get('id', ''))
|
|
1036
|
+
|
|
1037
|
+
return ""
|
|
1038
|
+
|
|
1039
|
+
def _add_screenshot_ids_to_events(self, events: List[Dict]):
|
|
1040
|
+
"""
|
|
1041
|
+
Add screenshot ID information to crash/ANR events
|
|
1042
|
+
|
|
1043
|
+
Args:
|
|
1044
|
+
events: List of crash or ANR event dictionaries
|
|
1045
|
+
"""
|
|
1046
|
+
for event in events:
|
|
1047
|
+
crash_screen = event.get('crash_screen')
|
|
1048
|
+
if crash_screen:
|
|
1049
|
+
screenshot_id = self._find_screenshot_id_by_filename(crash_screen)
|
|
1050
|
+
event['screenshot_id'] = screenshot_id
|
|
1051
|
+
else:
|
|
1052
|
+
event['screenshot_id'] = ""
|
|
1053
|
+
|
|
974
1054
|
def _extract_crash_info(self, crash_content: str) -> Dict:
|
|
975
1055
|
"""
|
|
976
1056
|
Extract crash information from crash content
|
|
@@ -1109,8 +1189,8 @@ class BugReportGenerator:
|
|
|
1109
1189
|
if __name__ == "__main__":
|
|
1110
1190
|
print("Generating bug report")
|
|
1111
1191
|
# OUTPUT_PATH = "<Your output path>"
|
|
1112
|
-
OUTPUT_PATH = "
|
|
1192
|
+
OUTPUT_PATH = "/Users/drifter327/Code/Kea2/output/res_2025090122_1216279438"
|
|
1113
1193
|
|
|
1114
1194
|
report_generator = BugReportGenerator()
|
|
1115
1195
|
report_path = report_generator.generate_report(OUTPUT_PATH)
|
|
1116
|
-
print(f"bug report generated: {report_path}")
|
|
1196
|
+
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
|
-
|
|
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
|
-
|
|
98
|
-
return
|
|
105
|
+
raise FileNotFoundError(f"{path_obj}")
|
|
99
106
|
if not path_obj.is_dir():
|
|
100
|
-
|
|
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
|
-
|
|
222
|
+
from .utils import LoggingLevel
|
|
223
|
+
LoggingLevel.set_level(logging.INFO)
|
|
214
224
|
if args.debug:
|
|
215
|
-
|
|
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,13 +1,15 @@
|
|
|
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
|
|
9
11
|
from pathlib import Path
|
|
10
|
-
from kea2.utils import getLogger
|
|
12
|
+
from kea2.utils import getLogger, getProjectRoot
|
|
11
13
|
|
|
12
14
|
|
|
13
15
|
from typing import IO, TYPE_CHECKING, Dict
|
|
@@ -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
|
"""
|
|
@@ -70,14 +73,15 @@ class FastbotManager:
|
|
|
70
73
|
"/data/local/tmp/x86_64/libfastbot_native.so",
|
|
71
74
|
)
|
|
72
75
|
|
|
76
|
+
cwd = getProjectRoot()
|
|
73
77
|
whitelist = self.options.act_whitelist_file
|
|
74
78
|
blacklist = self.options.act_blacklist_file
|
|
75
79
|
if bool(whitelist) ^ bool(blacklist):
|
|
76
80
|
if whitelist:
|
|
77
|
-
file_to_push =
|
|
81
|
+
file_to_push = cwd / 'configs' / 'awl.strings'
|
|
78
82
|
remote_path = whitelist
|
|
79
83
|
else:
|
|
80
|
-
file_to_push =
|
|
84
|
+
file_to_push = cwd / 'configs' / 'abl.strings'
|
|
81
85
|
remote_path = blacklist
|
|
82
86
|
|
|
83
87
|
self.dev.sync.push(
|
|
@@ -99,7 +103,9 @@ class FastbotManager:
|
|
|
99
103
|
_http_request(dev=self.dev, device_port=8090, method="GET", path="/ping")
|
|
100
104
|
|
|
101
105
|
try:
|
|
102
|
-
|
|
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.")
|
|
103
109
|
except requests.ConnectionError:
|
|
104
110
|
raise RuntimeError("Failed to connect fastbot")
|
|
105
111
|
|
|
@@ -110,7 +116,8 @@ class FastbotManager:
|
|
|
110
116
|
def init(self, options: "Options", stamp):
|
|
111
117
|
post_data = {
|
|
112
118
|
"takeScreenshots": options.take_screenshots,
|
|
113
|
-
"
|
|
119
|
+
"preFailureScreenshots": options.pre_failure_screenshots,
|
|
120
|
+
"logStamp": stamp,
|
|
114
121
|
"deviceOutputRoot": options.device_output_root,
|
|
115
122
|
}
|
|
116
123
|
r = _http_request(
|
|
@@ -160,6 +167,15 @@ class FastbotManager:
|
|
|
160
167
|
res = r.text
|
|
161
168
|
if res != "OK":
|
|
162
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']
|
|
163
179
|
|
|
164
180
|
@property
|
|
165
181
|
def device_output_dir(self):
|
|
@@ -172,10 +188,8 @@ class FastbotManager:
|
|
|
172
188
|
"/sdcard/framework.jar:"
|
|
173
189
|
"/sdcard/fastbot-thirdpart.jar:"
|
|
174
190
|
"/sdcard/kea2-thirdpart.jar",
|
|
175
|
-
|
|
176
191
|
"exec", "app_process",
|
|
177
192
|
"/system/bin", "com.android.commands.monkey.Monkey",
|
|
178
|
-
"-p", *self.options.packageNames,
|
|
179
193
|
"--agent-u2" if self.options.agent == "u2" else "--agent",
|
|
180
194
|
"reuseq",
|
|
181
195
|
"--running-minutes", f"{self.options.running_mins}",
|
|
@@ -184,6 +198,9 @@ class FastbotManager:
|
|
|
184
198
|
"--output-directory", f"{self.options.device_output_root}/output_{self.options.log_stamp}",
|
|
185
199
|
]
|
|
186
200
|
|
|
201
|
+
pkgs = itertools.chain.from_iterable(["-p", pkg] for pkg in self.options.packageNames)
|
|
202
|
+
shell_command.extend(pkgs)
|
|
203
|
+
|
|
187
204
|
if self.options.profile_period:
|
|
188
205
|
shell_command += ["--profile-period", f"{self.options.profile_period}"]
|
|
189
206
|
|
|
@@ -197,6 +214,9 @@ class FastbotManager:
|
|
|
197
214
|
|
|
198
215
|
shell_command += ["-v", "-v", "-v"]
|
|
199
216
|
|
|
217
|
+
if self.options.extra_args:
|
|
218
|
+
shell_command += self.options.extra_args
|
|
219
|
+
|
|
200
220
|
full_cmd = ["adb"] + (["-s", self.options.serial] if self.options.serial else []) + ["shell"] + shell_command
|
|
201
221
|
|
|
202
222
|
|