Kea2-python 0.0.1b2__py3-none-any.whl → 0.1.0b0__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,4 +1 @@
1
- from .keaUtils import KeaTestRunner, precondition, prob, max_tries, Options
2
-
3
- import logging
4
- logging.basicConfig(level=logging.DEBUG)
1
+ from .keaUtils import KeaTestRunner, precondition, prob, max_tries, Options
kea2/adbUtils.py CHANGED
@@ -147,7 +147,6 @@ def pull_file(remote_path: str, local_path: str, device: Optional[str] = None):
147
147
  """
148
148
  return run_adb_command(["-s", device, "pull", remote_path, local_path])
149
149
 
150
-
151
150
  # Forward-related functions
152
151
 
153
152
 
@@ -4,15 +4,35 @@ from kea2.keaUtils import precondition
4
4
 
5
5
  def global_block_widgets(d: "Device"):
6
6
  """
7
- global block widgets.
8
- return the widgets you want to block globally
9
- only available in mode `u2 agent`
7
+ Specify UI widgets to be blocked globally during testing.
8
+ Returns a list of widgets that should be blocked from exploration.
9
+ This function is only available in 'u2 agent' mode.
10
10
  """
11
+ # return [d(text="widgets to block"), d.xpath(".//node[@text='widget to block']")]
11
12
  return []
12
13
 
13
14
 
14
- # conditional block list
15
- @precondition(lambda d: d(text="In the home page").exists)
15
+ # Example of conditional blocking with precondition
16
+ # @precondition(lambda d: d(text="In the home page").exists)
17
+ @precondition(lambda d: False)
16
18
  def block_sth(d: "Device"):
17
- # Important: function shold starts with "block_"
18
- return [d(text="widgets to block"), d.xpath(".//node[@text='widget to block']")]
19
+ # Note: Function name must start with "block_"
20
+ return []
21
+
22
+
23
+ def global_block_tree(d: "Device"):
24
+ """
25
+ Specify UI widget trees to be blocked globally during testing.
26
+ Returns a list of root nodes whose entire subtrees will be blocked from exploration.
27
+ This function is only available in 'u2 agent' mode.
28
+ """
29
+ # return [d(text="trees to block"), d.xpath(".//node[@text='tree to block']")]
30
+ return []
31
+
32
+
33
+ # Example of conditional tree blocking with precondition
34
+ # @precondition(lambda d: d(text="In the home page").exists)
35
+ @precondition(lambda d: False)
36
+ def block_tree_sth(d: "Device"):
37
+ # Note: Function name must start with "block_tree_"
38
+ return []
kea2/assets/monkeyq.jar CHANGED
Binary file
kea2/keaUtils.py CHANGED
@@ -14,9 +14,10 @@ from .absDriver import AbstractDriver
14
14
  from functools import wraps
15
15
  from time import sleep
16
16
  from .adbUtils import push_file
17
+ from .resultSyncer import ResultSyncer
17
18
  from .logWatcher import LogWatcher
18
19
  from .utils import TimeStamp, getProjectRoot, getLogger
19
- from .u2Driver import StaticU2UiObject
20
+ from .u2Driver import StaticU2UiObject, selector_to_xpath
20
21
  import uiautomator2 as u2
21
22
  import types
22
23
 
@@ -32,8 +33,8 @@ PropName = NewType("PropName", str)
32
33
  PropertyStore = NewType("PropertyStore", Dict[PropName, TestCase])
33
34
 
34
35
  STAMP = TimeStamp().getTimeStamp()
35
- LOGFILE = f"fastbot_{STAMP}.log"
36
- RESFILE = f"result_{STAMP}.json"
36
+ LOGFILE: str
37
+ RESFILE: str
37
38
 
38
39
  def precondition(precond: Callable[[Any], bool]) -> Callable:
39
40
  """the decorator @precondition
@@ -120,7 +121,11 @@ class Options:
120
121
  # the stamp for log file and result file, default: current time stamp
121
122
  log_stamp: str = None
122
123
  # the profiling period to get the coverage result.
123
- profile_period: int = None
124
+ profile_period: int = 25
125
+ # take screenshots for every step
126
+ take_screenshots: bool = False
127
+ # the debug mode
128
+ debug: bool = False
124
129
 
125
130
  def __setattr__(self, name, value):
126
131
  if value is None:
@@ -130,10 +135,21 @@ class Options:
130
135
  def __post_init__(self):
131
136
  if self.serial and self.Driver:
132
137
  self.Driver.setDeviceSerial(self.serial)
138
+ global LOGFILE, RESFILE, STAMP
133
139
  if self.log_stamp:
134
- global LOGFILE, RESFILE
135
- LOGFILE = f"fastbot_{self.log_stamp}.log"
136
- RESFILE = f"result_{self.log_stamp}.json"
140
+ STAMP = self.log_stamp
141
+ self.output_dir = Path(self.output_dir).absolute() / f"res_{STAMP}"
142
+ LOGFILE = f"fastbot_{STAMP}.log"
143
+ RESFILE = f"result_{STAMP}.json"
144
+
145
+ self.profile_period = int(self.profile_period)
146
+ if self.profile_period < 1:
147
+ raise ValueError("--profile-period should be greater than 0")
148
+
149
+ self.throttle = int(self.throttle)
150
+ if self.throttle < 0:
151
+ raise ValueError("--throttle should be greater than or equal to 0")
152
+
137
153
  _check_package_installation(self.serial, self.packageNames)
138
154
 
139
155
 
@@ -168,6 +184,11 @@ def getFullPropName(testCase: TestCase):
168
184
 
169
185
  class JsonResult(TextTestResult):
170
186
  res: PBTTestResult
187
+
188
+ lastExecutedInfo: Dict = {
189
+ "propName": "",
190
+ "state": "",
191
+ }
171
192
 
172
193
  @classmethod
173
194
  def setProperties(cls, allProperties: Dict):
@@ -184,6 +205,10 @@ class JsonResult(TextTestResult):
184
205
 
185
206
  def addExcuted(self, test: TestCase):
186
207
  self.res[getFullPropName(test)].executed += 1
208
+ self.lastExecutedInfo = {
209
+ "propName": getFullPropName(test),
210
+ "state": "start",
211
+ }
187
212
 
188
213
  def addPrecondSatisfied(self, test: TestCase):
189
214
  self.res[getFullPropName(test)].precond_satisfied += 1
@@ -191,10 +216,12 @@ class JsonResult(TextTestResult):
191
216
  def addFailure(self, test, err):
192
217
  super().addFailure(test, err)
193
218
  self.res[getFullPropName(test)].fail += 1
219
+ self.lastExecutedInfo["state"] = "fail"
194
220
 
195
221
  def addError(self, test, err):
196
222
  super().addError(test, err)
197
223
  self.res[getFullPropName(test)].error += 1
224
+ self.lastExecutedInfo["state"] = "error"
198
225
 
199
226
  def getExcuted(self, test: TestCase):
200
227
  return self.res[getFullPropName(test)].executed
@@ -271,14 +298,18 @@ def startFastbotService(options: Options) -> threading.Thread:
271
298
  "exec", "app_process",
272
299
  "/system/bin", "com.android.commands.monkey.Monkey",
273
300
  "-p", *options.packageNames,
274
- "--agent-u2" if options.agent == "u2" else "--agent",
301
+ "--agent-u2" if options.agent == "u2" else "--agent",
275
302
  "reuseq",
276
303
  "--running-minutes", f"{options.running_mins}",
277
304
  "--throttle", f"{options.throttle}",
278
- "--bugreport", "--output-directory", "/sdcard/fastbot_report",
279
- "-v", "-v", "-v"
305
+ "--bugreport",
280
306
  ]
281
307
 
308
+ if options.profile_period:
309
+ shell_command += ["--profile-period", f"{options.profile_period}"]
310
+
311
+ shell_command += ["-v", "-v", "-v"]
312
+
282
313
  full_cmd = ["adb"] + (["-s", options.serial] if options.serial else []) + ["shell"] + shell_command
283
314
 
284
315
  outfile = open(LOGFILE, "w", encoding="utf-8", buffering=1)
@@ -298,14 +329,15 @@ def startFastbotService(options: Options) -> threading.Thread:
298
329
  def close_on_exit(proc: subprocess.Popen, f: IO):
299
330
  proc.wait()
300
331
  f.close()
301
-
332
+
302
333
 
303
334
  class KeaTestRunner(TextTestRunner):
304
335
 
305
336
  resultclass: JsonResult
306
337
  allProperties: PropertyStore
307
338
  options: Options = None
308
- _block_widgets_funcs = None
339
+ _block_funcs: Dict[Literal["widgets", "trees"], List[Callable]] = None
340
+ # _block_trees_funcs = None
309
341
 
310
342
  @classmethod
311
343
  def setOptions(cls, options: Options):
@@ -370,6 +402,10 @@ class KeaTestRunner(TextTestRunner):
370
402
  # setUp for the u2 driver
371
403
  self.scriptDriver = self.options.Driver.getScriptDriver()
372
404
  check_alive(port=self.scriptDriver.lport)
405
+ self._init()
406
+
407
+ resultSyncer = ResultSyncer(self.device_output_dir, self.options.output_dir)
408
+ resultSyncer.run()
373
409
 
374
410
  end_by_remote = False
375
411
  self.stepsCount = 0
@@ -384,7 +420,6 @@ class KeaTestRunner(TextTestRunner):
384
420
 
385
421
  try:
386
422
  xml_raw = self.stepMonkey()
387
- stat = self._getStat()
388
423
  propsSatisfiedPrecond = self.getValidProperties(xml_raw, result)
389
424
  except requests.ConnectionError:
390
425
  print(
@@ -393,6 +428,9 @@ class KeaTestRunner(TextTestRunner):
393
428
  end_by_remote = True
394
429
  break
395
430
 
431
+ if self.options.profile_period and self.stepsCount % self.options.profile_period == 0:
432
+ resultSyncer.sync_event.set()
433
+
396
434
  print(f"{len(propsSatisfiedPrecond)} precond satisfied.", flush=True)
397
435
 
398
436
  # Go to the next round if no precond satisfied
@@ -420,20 +458,23 @@ class KeaTestRunner(TextTestRunner):
420
458
  print("execute property %s." % execPropName, flush=True)
421
459
 
422
460
  result.addExcuted(test)
461
+ self._logScript(result.lastExecutedInfo)
423
462
  try:
424
463
  test(result)
425
464
  finally:
426
465
  result.printErrors()
427
466
 
467
+ self._logScript(result.lastExecutedInfo)
428
468
  result.flushResult(outfile=RESFILE)
429
469
 
430
470
  if not end_by_remote:
431
471
  self.stopMonkey()
432
472
  result.flushResult(outfile=RESFILE)
473
+ resultSyncer.close()
433
474
 
434
475
  print(f"Finish sending monkey events.", flush=True)
435
476
  log_watcher.close()
436
- self.tearDown()
477
+
437
478
 
438
479
  # Source code from unittest Runner
439
480
  # process the result
@@ -475,15 +516,19 @@ class KeaTestRunner(TextTestRunner):
475
516
  """
476
517
  send a step monkey request to the server and get the xml string.
477
518
  """
478
- block_widgets: List[str] = self._getBlockedWidgets()
519
+ block_dict = self._getBlockedWidgets()
520
+ block_widgets: List[str] = block_dict['widgets']
521
+ block_trees: List[str] = block_dict['trees']
479
522
  URL = f"http://localhost:{self.scriptDriver.lport}/stepMonkey"
480
523
  logger.debug(f"Sending request: {URL}")
481
524
  logger.debug(f"Blocking widgets: {block_widgets}")
525
+ logger.debug(f"Blocking trees: {block_trees}")
482
526
  r = requests.post(
483
527
  url=URL,
484
528
  json={
485
529
  "steps_count": self.stepsCount,
486
- "block_widgets": block_widgets
530
+ "block_widgets": block_widgets,
531
+ "block_trees": block_trees
487
532
  }
488
533
  )
489
534
 
@@ -533,14 +578,31 @@ class KeaTestRunner(TextTestRunner):
533
578
  validProps[propName] = test
534
579
  return validProps
535
580
 
536
- def _getStat(self):
537
- # profile when reaching the profile period
538
- if (self.options.profile_period and
539
- self.stepsCount % self.options.profile_period == 0
540
- ):
541
- URL = f"http://localhost:{self.scriptDriver.lport}/getStat"
542
- r = requests.get(URL)
543
- res = json.loads(r.content)
581
+ def _logScript(self, execution_info:Dict):
582
+ URL = f"http://localhost:{self.scriptDriver.lport}/logScript"
583
+ r = requests.post(
584
+ url=URL,
585
+ json=execution_info
586
+ )
587
+ res = r.content.decode(encoding="utf-8")
588
+ if res != "OK":
589
+ print(f"[ERROR] Error when logging script: {execution_info}", flush=True)
590
+
591
+ def _init(self):
592
+ URL = f"http://localhost:{self.scriptDriver.lport}/init"
593
+ data = {
594
+ "takeScreenshots": self.options.take_screenshots,
595
+ "Stamp": STAMP
596
+ }
597
+ print(f"[INFO] Init fastbot: {data}", flush=True)
598
+ r = requests.post(
599
+ url=URL,
600
+ json=data
601
+ )
602
+ res = r.content.decode(encoding="utf-8")
603
+ import re
604
+ self.device_output_dir = re.match(r"outputDir:(.+)", res).group(1)
605
+ print(f"[INFO] Fastbot initiated. Device outputDir: {res}", flush=True)
544
606
 
545
607
  def collectAllProperties(self, test: TestSuite):
546
608
  """collect all the properties to prepare for PBT
@@ -556,7 +618,7 @@ class KeaTestRunner(TextTestRunner):
556
618
  """remove the tearDown function in PBT
557
619
  """
558
620
  def tearDown(self): ...
559
- testCase = types.MethodType(tearDown, testCase)
621
+ testCase.tearDown = types.MethodType(tearDown, testCase)
560
622
 
561
623
  def iter_tests(suite):
562
624
  for test in suite:
@@ -577,17 +639,24 @@ class KeaTestRunner(TextTestRunner):
577
639
  # save it into allProperties for PBT
578
640
  self.allProperties[testMethodName] = t
579
641
  print(f"[INFO] Load property: {getFullPropName(t)}", flush=True)
580
-
642
+
581
643
  @property
582
644
  def _blockWidgetFuncs(self):
583
- if self._block_widgets_funcs is None:
584
- self._block_widgets_funcs = list()
645
+ """
646
+ load and process blocking functions from widget.block.py configuration file.
647
+
648
+ Returns:
649
+ dict: A dictionary containing two lists:
650
+ - 'widgets': List of functions that block individual widgets
651
+ - 'trees': List of functions that block widget trees
652
+ """
653
+ if self._block_funcs is None:
654
+ self._block_funcs = {"widgets": list(), "trees": list()}
585
655
  root_dir = getProjectRoot()
586
656
  if root_dir is None or not os.path.exists(
587
- file_block_widgets := root_dir / "configs" / "widget.block.py"
657
+ file_block_widgets := root_dir / "configs" / "widget.block.py"
588
658
  ):
589
659
  print(f"[WARNING] widget.block.py not find", flush=True)
590
-
591
660
 
592
661
  def __get_block_widgets_module():
593
662
  import importlib.util
@@ -601,46 +670,88 @@ class KeaTestRunner(TextTestRunner):
601
670
 
602
671
  import inspect
603
672
  for func_name, func in inspect.getmembers(mod, inspect.isfunction):
604
- if func_name.startswith("block_") or func_name == "global_block_widgets":
673
+ if func_name == "global_block_widgets":
674
+ self._block_funcs["widgets"].append(func)
675
+ setattr(func, PRECONDITIONS_MARKER, (lambda d: True,))
676
+ continue
677
+ if func_name == "global_block_tree":
678
+ self._block_funcs["trees"].append(func)
679
+ setattr(func, PRECONDITIONS_MARKER, (lambda d: True,))
680
+ continue
681
+ if func_name.startswith("block_") and not func_name.startswith("block_tree_"):
682
+ if getattr(func, PRECONDITIONS_MARKER, None) is None:
683
+ logger.warning(f"No precondition in block widget function: {func_name}. Default globally active.")
684
+ setattr(func, PRECONDITIONS_MARKER, (lambda d: True,))
685
+ self._block_funcs["widgets"].append(func)
686
+ continue
687
+ if func_name.startswith("block_tree_"):
605
688
  if getattr(func, PRECONDITIONS_MARKER, None) is None:
606
- if func_name.startswith("block_"):
607
- logger.warning(f"No precondition in block widget function: {func_name}. Default globally active.")
608
- setattr(func, PRECONDITIONS_MARKER, (lambda d: True, ))
609
- self._block_widgets_funcs.append(func)
689
+ logger.warning(f"No precondition in block tree function: {func_name}. Default globally active.")
690
+ setattr(func, PRECONDITIONS_MARKER, (lambda d: True,))
691
+ self._block_funcs["trees"].append(func)
692
+
693
+ return self._block_funcs
610
694
 
611
- return self._block_widgets_funcs
612
695
 
613
696
  def _getBlockedWidgets(self):
614
- blocked_widgets = list()
615
- for func in self._blockWidgetFuncs:
697
+ """
698
+ Executes all blocking functions to get lists of widgets and trees to be blocked during testing.
699
+
700
+ Returns:
701
+ dict: A dictionary containing:
702
+ - 'widgets': List of XPath strings for individual widgets to block
703
+ - 'trees': List of XPath strings for widget trees to block
704
+ """
705
+ def _get_xpath_widgets(func):
706
+ blocked_set = set()
616
707
  try:
617
708
  script_driver = self.options.Driver.getScriptDriver()
618
- preconds = getattr(func, PRECONDITIONS_MARKER)
619
- if all([precond(script_driver) for precond in preconds]):
709
+ preconds = getattr(func, PRECONDITIONS_MARKER, [])
710
+ if all(precond(script_driver) for precond in preconds):
620
711
  _widgets = func(self.options.Driver.getStaticChecker())
621
- if not isinstance(_widgets, list):
622
- _widgets = [_widgets]
712
+ _widgets = _widgets if isinstance(_widgets, list) else [_widgets]
623
713
  for w in _widgets:
624
714
  if isinstance(w, StaticU2UiObject):
625
- blocked_widgets.append(w._getXPath(w.selector))
715
+ xpath = selector_to_xpath(w.selector, True)
716
+ blocked_set.add(xpath)
626
717
  elif isinstance(w, u2.xpath.XPathSelector):
627
- def getXPathRepr(w):
628
- return w._parent.xpath
629
- blocked_widgets.append(getXPathRepr(w))
718
+ xpath = w._parent.xpath
719
+ blocked_set.add(xpath)
630
720
  else:
631
721
  logger.warning(f"{w} Not supported")
632
- # blocked_widgets.extend([
633
- # w._getXPath(w.selector) for w in _widgets
634
- # ])
635
722
  except Exception as e:
636
- logger.error(f"error when getting blocked widgets: {e}")
637
- import traceback
723
+ logger.error(f"Error processing blocked widgets: {e}")
638
724
  traceback.print_exc()
725
+ return blocked_set
726
+
727
+ res = {
728
+ "widgets": set(),
729
+ "trees": set()
730
+ }
731
+
732
+
733
+ for func in self._blockWidgetFuncs["widgets"]:
734
+ widgets = _get_xpath_widgets(func)
735
+ res["widgets"].update(widgets)
736
+
737
+
738
+ for func in self._blockWidgetFuncs["trees"]:
739
+ trees = _get_xpath_widgets(func)
740
+ res["trees"].update(trees)
741
+
742
+
743
+ res["widgets"] = list(res["widgets"] - res["trees"])
744
+ res["trees"] = list(res["trees"])
745
+
746
+ return res
639
747
 
640
- return blocked_widgets
641
748
 
642
749
  def __del__(self):
643
750
  """tearDown method. Cleanup the env.
644
751
  """
752
+ try:
753
+ self.stopMonkey()
754
+ except Exception as e:
755
+ pass
645
756
  if self.options.Driver:
646
757
  self.options.Driver.tearDown()
kea2/kea_launcher.py CHANGED
@@ -1,8 +1,6 @@
1
- import os
2
1
  import sys
3
2
  import argparse
4
3
  import unittest
5
- from pathlib import Path
6
4
  from typing import List
7
5
 
8
6
  def _set_runner_parser(subparsers: "argparse._SubParsersAction[argparse.ArgumentParser]"):
@@ -49,6 +47,7 @@ def _set_runner_parser(subparsers: "argparse._SubParsersAction[argparse.Argument
49
47
  dest="running_minutes",
50
48
  type=int,
51
49
  required=False,
50
+ default=10,
52
51
  help="Time to run fastbot",
53
52
  )
54
53
 
@@ -89,8 +88,18 @@ def _set_runner_parser(subparsers: "argparse._SubParsersAction[argparse.Argument
89
88
  dest="profile_period",
90
89
  type=int,
91
90
  required=False,
91
+ default=25,
92
92
  help="Steps to profile the testing statistics.",
93
93
  )
94
+
95
+ parser.add_argument(
96
+ "--take-screenshots",
97
+ dest="take_screenshots",
98
+ required=False,
99
+ action="store_true",
100
+ default=False,
101
+ help="Take screenshots for every step.",
102
+ )
94
103
 
95
104
  parser.add_argument(
96
105
  "extra",
@@ -118,6 +127,10 @@ def driver_info_logger(args):
118
127
  print(" running_minutes:", args.running_minutes, flush=True)
119
128
  if args.throttle_ms:
120
129
  print(" throttle_ms:", args.throttle_ms, flush=True)
130
+ if args.log_stamp:
131
+ print(" log_stamp:", args.log_stamp, flush=True)
132
+ if args.take_screenshots:
133
+ print(" take_screenshots:", args.take_screenshots, flush=True)
121
134
 
122
135
 
123
136
  def parse_args(argv: List):
@@ -140,9 +153,7 @@ def _sanitize_args(args):
140
153
  def run(args=None):
141
154
  if args is None:
142
155
  args = parse_args(sys.argv[1:])
143
-
144
156
  _sanitize_args(args)
145
-
146
157
  driver_info_logger(args)
147
158
  unittest_info_logger(args)
148
159
 
@@ -159,6 +170,7 @@ def run(args=None):
159
170
  throttle=args.throttle_ms,
160
171
  log_stamp=args.log_stamp,
161
172
  profile_period=args.profile_period,
173
+ take_screenshots=args.take_screenshots,
162
174
  )
163
175
 
164
176
  KeaTestRunner.setOptions(options)
kea2/logWatcher.py CHANGED
@@ -20,10 +20,13 @@ class LogWatcher:
20
20
  self.buffer = ""
21
21
  self.last_pos = 0
22
22
 
23
- while True:
23
+ while not self.end_flag:
24
24
  self.read_log()
25
25
  time.sleep(poll_interval)
26
26
 
27
+ time.sleep(0.2)
28
+ self.read_log()
29
+
27
30
  def read_log(self):
28
31
  time.sleep(0.02)
29
32
  with open(self.log_file, 'r', encoding='utf-8') as f:
@@ -46,25 +49,26 @@ class LogWatcher:
46
49
  exception_body +
47
50
  "\nSee fastbot.log for details."
48
51
  )
49
- statistic_match = PATTERN_STATISTIC.search(buffer)
50
- if statistic_match:
51
- statistic_body = statistic_match.group(1).strip()
52
- if statistic_body:
53
- print(
54
- "[INFO] Fastbot exit:\n" +
55
- statistic_body
56
- , flush=True)
52
+ if self.end_flag:
53
+ statistic_match = PATTERN_STATISTIC.search(buffer)
54
+ if statistic_match:
55
+ statistic_body = statistic_match.group(1).strip()
56
+ if statistic_body:
57
+ print(
58
+ "[INFO] Fastbot exit:\n" +
59
+ statistic_body
60
+ , flush=True)
57
61
 
58
62
  def __init__(self, log_file):
59
63
  self.log_file = log_file
64
+ self.end_flag = False
60
65
 
61
66
  threading.excepthook = thread_excepthook
62
67
  t = threading.Thread(target=self.watcher, daemon=True)
63
68
  t.start()
64
69
 
65
70
  def close(self):
66
- time.sleep(0.2) # wait for the written logfile close
67
- self.read_log()
71
+ self.end_flag = True
68
72
 
69
73
 
70
74
  if __name__ == "__main__":
kea2/resultSyncer.py ADDED
@@ -0,0 +1,56 @@
1
+ import threading
2
+ from .adbUtils import adb_shell, pull_file
3
+ from .utils import getLogger
4
+
5
+ logger = getLogger(__name__)
6
+
7
+
8
+ class ResultSyncer:
9
+
10
+ def __init__(self, device_output_dir, output_dir):
11
+ self.device_output_dir = device_output_dir
12
+ self.output_dir = output_dir
13
+ self.running = False
14
+ self.thread = None
15
+ self.sync_event = threading.Event()
16
+
17
+ def run(self):
18
+ """Start a background thread to sync device data when triggered"""
19
+ self.running = True
20
+ self.thread = threading.Thread(target=self._sync_thread, daemon=True)
21
+ self.thread.start()
22
+
23
+ def _sync_thread(self):
24
+ """Thread function that waits for sync event and then syncs data"""
25
+ while self.running:
26
+ # Wait for sync event with a timeout to periodically check if still running
27
+ if self.sync_event.wait(timeout=3):
28
+ self._sync_device_data()
29
+ self.sync_event.clear()
30
+
31
+ def close(self):
32
+ self.running = False
33
+ self.sync_event.set()
34
+ if self.thread and self.thread.is_alive():
35
+ self.thread.join(timeout=10)
36
+ self._sync_device_data()
37
+ try:
38
+ logger.debug(f"Removing device output directory: {self.device_output_dir}")
39
+ remove_device_dir = ["rm", "-rf", self.device_output_dir]
40
+ adb_shell(remove_device_dir)
41
+ except Exception as e:
42
+ logger.error(f"Error removing device output directory: {e}", flush=True)
43
+
44
+ def _sync_device_data(self):
45
+ """
46
+ Sync the device data to the local directory.
47
+ """
48
+ try:
49
+ logger.debug("Syncing data")
50
+
51
+ pull_file(self.device_output_dir, str(self.output_dir))
52
+
53
+ remove_pulled_screenshots = ["find", self.device_output_dir, "-name", "\"*.png\"", "-delete"]
54
+ adb_shell(remove_pulled_screenshots)
55
+ except Exception as e:
56
+ logger.error(f"Error in data sync: {e}", flush=True)