Kea2-python 0.0.1b2__py3-none-any.whl → 0.1.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/keaUtils.py CHANGED
@@ -1,10 +1,8 @@
1
1
  import json
2
2
  import os
3
3
  from pathlib import Path
4
- import subprocess
5
- import threading
6
4
  import traceback
7
- from typing import IO, Callable, Any, Dict, List, Literal, NewType, Union
5
+ from typing import Callable, Any, Dict, List, Literal, NewType, TypedDict, Union
8
6
  from unittest import TextTestRunner, registerResult, TestSuite, TestCase, TextTestResult
9
7
  import random
10
8
  import warnings
@@ -12,11 +10,12 @@ from dataclasses import dataclass, asdict
12
10
  import requests
13
11
  from .absDriver import AbstractDriver
14
12
  from functools import wraps
15
- from time import sleep
16
- from .adbUtils import push_file
13
+ from .bug_report_generator import BugReportGenerator
14
+ from .resultSyncer import ResultSyncer
17
15
  from .logWatcher import LogWatcher
18
16
  from .utils import TimeStamp, getProjectRoot, getLogger
19
- from .u2Driver import StaticU2UiObject
17
+ from .u2Driver import StaticU2UiObject, selector_to_xpath
18
+ from .fastbotManager import FastbotManager
20
19
  import uiautomator2 as u2
21
20
  import types
22
21
 
@@ -30,10 +29,14 @@ logger = getLogger(__name__)
30
29
  # Class Typing
31
30
  PropName = NewType("PropName", str)
32
31
  PropertyStore = NewType("PropertyStore", Dict[PropName, TestCase])
32
+ PropertyExecutionInfo = TypedDict(
33
+ "PropertyExecutionInfo",
34
+ {"propName": PropName, "state": Literal["start", "pass", "fail", "error"]}
35
+ )
33
36
 
34
37
  STAMP = TimeStamp().getTimeStamp()
35
- LOGFILE = f"fastbot_{STAMP}.log"
36
- RESFILE = f"result_{STAMP}.json"
38
+ LOGFILE: str
39
+ RESFILE: str
37
40
 
38
41
  def precondition(precond: Callable[[Any], bool]) -> Callable:
39
42
  """the decorator @precondition
@@ -120,7 +123,11 @@ class Options:
120
123
  # the stamp for log file and result file, default: current time stamp
121
124
  log_stamp: str = None
122
125
  # the profiling period to get the coverage result.
123
- profile_period: int = None
126
+ profile_period: int = 25
127
+ # take screenshots for every step
128
+ take_screenshots: bool = False
129
+ # the debug mode
130
+ debug: bool = False
124
131
 
125
132
  def __setattr__(self, name, value):
126
133
  if value is None:
@@ -130,10 +137,21 @@ class Options:
130
137
  def __post_init__(self):
131
138
  if self.serial and self.Driver:
132
139
  self.Driver.setDeviceSerial(self.serial)
140
+ global LOGFILE, RESFILE, STAMP
133
141
  if self.log_stamp:
134
- global LOGFILE, RESFILE
135
- LOGFILE = f"fastbot_{self.log_stamp}.log"
136
- RESFILE = f"result_{self.log_stamp}.json"
142
+ STAMP = self.log_stamp
143
+ self.output_dir = Path(self.output_dir).absolute() / f"res_{STAMP}"
144
+ LOGFILE = f"fastbot_{STAMP}.log"
145
+ RESFILE = f"result_{STAMP}.json"
146
+
147
+ self.profile_period = int(self.profile_period)
148
+ if self.profile_period < 1:
149
+ raise ValueError("--profile-period should be greater than 0")
150
+
151
+ self.throttle = int(self.throttle)
152
+ if self.throttle < 0:
153
+ raise ValueError("--throttle should be greater than or equal to 0")
154
+
137
155
  _check_package_installation(self.serial, self.packageNames)
138
156
 
139
157
 
@@ -168,6 +186,11 @@ def getFullPropName(testCase: TestCase):
168
186
 
169
187
  class JsonResult(TextTestResult):
170
188
  res: PBTTestResult
189
+
190
+ lastExecutedInfo: PropertyExecutionInfo = {
191
+ "propName": "",
192
+ "state": "",
193
+ }
171
194
 
172
195
  @classmethod
173
196
  def setProperties(cls, allProperties: Dict):
@@ -184,6 +207,10 @@ class JsonResult(TextTestResult):
184
207
 
185
208
  def addExcuted(self, test: TestCase):
186
209
  self.res[getFullPropName(test)].executed += 1
210
+ self.lastExecutedInfo = {
211
+ "propName": getFullPropName(test),
212
+ "state": "start",
213
+ }
187
214
 
188
215
  def addPrecondSatisfied(self, test: TestCase):
189
216
  self.res[getFullPropName(test)].precond_satisfied += 1
@@ -191,128 +218,34 @@ class JsonResult(TextTestResult):
191
218
  def addFailure(self, test, err):
192
219
  super().addFailure(test, err)
193
220
  self.res[getFullPropName(test)].fail += 1
221
+ self.lastExecutedInfo["state"] = "fail"
194
222
 
195
223
  def addError(self, test, err):
196
224
  super().addError(test, err)
197
225
  self.res[getFullPropName(test)].error += 1
226
+ self.lastExecutedInfo["state"] = "error"
227
+
228
+ def updateExectedInfo(self):
229
+ if self.lastExecutedInfo["state"] == "start":
230
+ self.lastExecutedInfo["state"] = "pass"
198
231
 
199
232
  def getExcuted(self, test: TestCase):
200
233
  return self.res[getFullPropName(test)].executed
201
234
 
202
235
 
203
- def activateFastbot(options: Options, port=None) -> threading.Thread:
204
- """
205
- activate fastbot.
206
- :params: options: the running setting for fastbot
207
- :params: port: the listening port for script driver
208
- :return: the fastbot daemon thread
209
- """
210
- cur_dir = Path(__file__).parent
211
- push_file(
212
- Path.joinpath(cur_dir, "assets/monkeyq.jar"),
213
- "/sdcard/monkeyq.jar",
214
- device=options.serial
215
- )
216
- push_file(
217
- Path.joinpath(cur_dir, "assets/fastbot-thirdpart.jar"),
218
- "/sdcard/fastbot-thirdpart.jar",
219
- device=options.serial,
220
- )
221
- push_file(
222
- Path.joinpath(cur_dir, "assets/framework.jar"),
223
- "/sdcard/framework.jar",
224
- device=options.serial
225
- )
226
- push_file(
227
- Path.joinpath(cur_dir, "assets/fastbot_libs/arm64-v8a"),
228
- "/data/local/tmp",
229
- device=options.serial
230
- )
231
- push_file(
232
- Path.joinpath(cur_dir, "assets/fastbot_libs/armeabi-v7a"),
233
- "/data/local/tmp",
234
- device=options.serial
235
- )
236
- push_file(
237
- Path.joinpath(cur_dir, "assets/fastbot_libs/x86"),
238
- "/data/local/tmp",
239
- device=options.serial
240
- )
241
- push_file(
242
- Path.joinpath(cur_dir, "assets/fastbot_libs/x86_64"),
243
- "/data/local/tmp",
244
- device=options.serial
245
- )
246
-
247
- t = startFastbotService(options)
248
- print("[INFO] Running Fastbot...", flush=True)
249
-
250
- return t
251
-
252
-
253
- def check_alive(port):
254
- """
255
- check if the script driver and proxy server are alive.
256
- """
257
- for _ in range(10):
258
- sleep(2)
259
- try:
260
- requests.get(f"http://localhost:{port}/ping")
261
- return
262
- except requests.ConnectionError:
263
- print("[INFO] waiting for connection.", flush=True)
264
- pass
265
- raise RuntimeError("Failed to connect fastbot")
266
-
267
-
268
- def startFastbotService(options: Options) -> threading.Thread:
269
- shell_command = [
270
- "CLASSPATH=/sdcard/monkeyq.jar:/sdcard/framework.jar:/sdcard/fastbot-thirdpart.jar",
271
- "exec", "app_process",
272
- "/system/bin", "com.android.commands.monkey.Monkey",
273
- "-p", *options.packageNames,
274
- "--agent-u2" if options.agent == "u2" else "--agent",
275
- "reuseq",
276
- "--running-minutes", f"{options.running_mins}",
277
- "--throttle", f"{options.throttle}",
278
- "--bugreport", "--output-directory", "/sdcard/fastbot_report",
279
- "-v", "-v", "-v"
280
- ]
281
-
282
- full_cmd = ["adb"] + (["-s", options.serial] if options.serial else []) + ["shell"] + shell_command
283
-
284
- outfile = open(LOGFILE, "w", encoding="utf-8", buffering=1)
285
-
286
- print("[INFO] Options info: {}".format(asdict(options)), flush=True)
287
- print("[INFO] Launching fastbot with shell command:\n{}".format(" ".join(full_cmd)), flush=True)
288
- print("[INFO] Fastbot log will be saved to {}".format(outfile.name), flush=True)
289
-
290
- # process handler
291
- proc = subprocess.Popen(full_cmd, stdout=outfile, stderr=outfile)
292
- t = threading.Thread(target=close_on_exit, args=(proc, outfile), daemon=True)
293
- t.start()
294
-
295
- return t
296
-
297
-
298
- def close_on_exit(proc: subprocess.Popen, f: IO):
299
- proc.wait()
300
- f.close()
301
-
302
-
303
236
  class KeaTestRunner(TextTestRunner):
304
237
 
305
238
  resultclass: JsonResult
306
239
  allProperties: PropertyStore
307
240
  options: Options = None
308
- _block_widgets_funcs = None
241
+ _block_funcs: Dict[Literal["widgets", "trees"], List[Callable]] = None
309
242
 
310
243
  @classmethod
311
244
  def setOptions(cls, options: Options):
312
245
  if not isinstance(options.packageNames, list) and len(options.packageNames) > 0:
313
246
  raise ValueError("packageNames should be given in a list.")
314
247
  if options.Driver is not None and options.agent == "native":
315
- print("[Warning] Can not use any Driver when runing native mode.", flush=True)
248
+ logger.warning("[Warning] Can not use any Driver when runing native mode.")
316
249
  options.Driver = None
317
250
  cls.options = options
318
251
 
@@ -331,7 +264,7 @@ class KeaTestRunner(TextTestRunner):
331
264
  self.collectAllProperties(test)
332
265
 
333
266
  if len(self.allProperties) == 0:
334
- print("[Warning] No property has been found.", flush=True)
267
+ logger.warning("[Warning] No property has been found.")
335
268
 
336
269
  self._setOuputDir()
337
270
 
@@ -360,38 +293,44 @@ class KeaTestRunner(TextTestRunner):
360
293
  message=r"Please use assert\w+ instead.",
361
294
  )
362
295
 
363
- t = activateFastbot(options=self.options)
364
296
  log_watcher = LogWatcher(LOGFILE)
365
- if self.options.agent == "native":
366
- t.join()
367
- else:
297
+ fb = FastbotManager(self.options, LOGFILE)
298
+ fb.start()
299
+
300
+ if self.options.agent == "u2":
368
301
  # initialize the result.json file
369
302
  result.flushResult(outfile=RESFILE)
370
303
  # setUp for the u2 driver
371
304
  self.scriptDriver = self.options.Driver.getScriptDriver()
372
- check_alive(port=self.scriptDriver.lport)
305
+ fb.check_alive(port=self.scriptDriver.lport)
306
+ self._init()
307
+
308
+ resultSyncer = ResultSyncer(self.device_output_dir, self.options.output_dir)
309
+ resultSyncer.run()
373
310
 
374
311
  end_by_remote = False
375
312
  self.stepsCount = 0
376
313
  while self.stepsCount < self.options.maxStep:
377
314
 
378
315
  self.stepsCount += 1
379
- print("[INFO] Sending monkeyEvent {}".format(
316
+ logger.info("Sending monkeyEvent {}".format(
380
317
  f"({self.stepsCount} / {self.options.maxStep})" if self.options.maxStep != float("inf")
381
318
  else f"({self.stepsCount})"
382
319
  )
383
- , flush=True)
320
+ )
384
321
 
385
322
  try:
386
323
  xml_raw = self.stepMonkey()
387
- stat = self._getStat()
388
324
  propsSatisfiedPrecond = self.getValidProperties(xml_raw, result)
389
325
  except requests.ConnectionError:
390
- print(
391
- "[INFO] Exploration times up (--running-minutes)."
392
- , flush=True)
393
- end_by_remote = True
394
- break
326
+ if fb.get_return_code() == 0:
327
+ logger.info("[INFO] Exploration times up (--running-minutes).")
328
+ end_by_remote = True
329
+ break
330
+ raise RuntimeError("Fastbot Aborted.")
331
+
332
+ if self.options.profile_period and self.stepsCount % self.options.profile_period == 0:
333
+ resultSyncer.sync_event.set()
395
334
 
396
335
  print(f"{len(propsSatisfiedPrecond)} precond satisfied.", flush=True)
397
336
 
@@ -420,20 +359,24 @@ class KeaTestRunner(TextTestRunner):
420
359
  print("execute property %s." % execPropName, flush=True)
421
360
 
422
361
  result.addExcuted(test)
362
+ self._logScript(result.lastExecutedInfo)
423
363
  try:
424
364
  test(result)
425
365
  finally:
426
366
  result.printErrors()
427
367
 
368
+ result.updateExectedInfo()
369
+ self._logScript(result.lastExecutedInfo)
428
370
  result.flushResult(outfile=RESFILE)
429
371
 
430
372
  if not end_by_remote:
431
373
  self.stopMonkey()
432
374
  result.flushResult(outfile=RESFILE)
433
-
375
+ resultSyncer.close()
376
+
377
+ fb.join()
434
378
  print(f"Finish sending monkey events.", flush=True)
435
379
  log_watcher.close()
436
- self.tearDown()
437
380
 
438
381
  # Source code from unittest Runner
439
382
  # process the result
@@ -475,15 +418,19 @@ class KeaTestRunner(TextTestRunner):
475
418
  """
476
419
  send a step monkey request to the server and get the xml string.
477
420
  """
478
- block_widgets: List[str] = self._getBlockedWidgets()
421
+ block_dict = self._getBlockedWidgets()
422
+ block_widgets: List[str] = block_dict['widgets']
423
+ block_trees: List[str] = block_dict['trees']
479
424
  URL = f"http://localhost:{self.scriptDriver.lport}/stepMonkey"
480
425
  logger.debug(f"Sending request: {URL}")
481
426
  logger.debug(f"Blocking widgets: {block_widgets}")
427
+ logger.debug(f"Blocking trees: {block_trees}")
482
428
  r = requests.post(
483
429
  url=URL,
484
430
  json={
485
431
  "steps_count": self.stepsCount,
486
- "block_widgets": block_widgets
432
+ "block_widgets": block_widgets,
433
+ "block_trees": block_trees
487
434
  }
488
435
  )
489
436
 
@@ -533,14 +480,31 @@ class KeaTestRunner(TextTestRunner):
533
480
  validProps[propName] = test
534
481
  return validProps
535
482
 
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)
483
+ def _logScript(self, execution_info:Dict):
484
+ URL = f"http://localhost:{self.scriptDriver.lport}/logScript"
485
+ r = requests.post(
486
+ url=URL,
487
+ json=execution_info
488
+ )
489
+ res = r.content.decode(encoding="utf-8")
490
+ if res != "OK":
491
+ print(f"[ERROR] Error when logging script: {execution_info}", flush=True)
492
+
493
+ def _init(self):
494
+ URL = f"http://localhost:{self.scriptDriver.lport}/init"
495
+ data = {
496
+ "takeScreenshots": self.options.take_screenshots,
497
+ "Stamp": STAMP
498
+ }
499
+ print(f"[INFO] Init fastbot: {data}", flush=True)
500
+ r = requests.post(
501
+ url=URL,
502
+ json=data
503
+ )
504
+ res = r.content.decode(encoding="utf-8")
505
+ import re
506
+ self.device_output_dir = re.match(r"outputDir:(.+)", res).group(1)
507
+ print(f"[INFO] Fastbot initiated. Device outputDir: {res}", flush=True)
544
508
 
545
509
  def collectAllProperties(self, test: TestSuite):
546
510
  """collect all the properties to prepare for PBT
@@ -556,8 +520,8 @@ class KeaTestRunner(TextTestRunner):
556
520
  """remove the tearDown function in PBT
557
521
  """
558
522
  def tearDown(self): ...
559
- testCase = types.MethodType(tearDown, testCase)
560
-
523
+ testCase.tearDown = types.MethodType(tearDown, testCase)
524
+
561
525
  def iter_tests(suite):
562
526
  for test in suite:
563
527
  if isinstance(test, TestSuite):
@@ -577,17 +541,24 @@ class KeaTestRunner(TextTestRunner):
577
541
  # save it into allProperties for PBT
578
542
  self.allProperties[testMethodName] = t
579
543
  print(f"[INFO] Load property: {getFullPropName(t)}", flush=True)
580
-
544
+
581
545
  @property
582
546
  def _blockWidgetFuncs(self):
583
- if self._block_widgets_funcs is None:
584
- self._block_widgets_funcs = list()
547
+ """
548
+ load and process blocking functions from widget.block.py configuration file.
549
+
550
+ Returns:
551
+ dict: A dictionary containing two lists:
552
+ - 'widgets': List of functions that block individual widgets
553
+ - 'trees': List of functions that block widget trees
554
+ """
555
+ if self._block_funcs is None:
556
+ self._block_funcs = {"widgets": list(), "trees": list()}
585
557
  root_dir = getProjectRoot()
586
558
  if root_dir is None or not os.path.exists(
587
- file_block_widgets := root_dir / "configs" / "widget.block.py"
559
+ file_block_widgets := root_dir / "configs" / "widget.block.py"
588
560
  ):
589
561
  print(f"[WARNING] widget.block.py not find", flush=True)
590
-
591
562
 
592
563
  def __get_block_widgets_module():
593
564
  import importlib.util
@@ -601,46 +572,94 @@ class KeaTestRunner(TextTestRunner):
601
572
 
602
573
  import inspect
603
574
  for func_name, func in inspect.getmembers(mod, inspect.isfunction):
604
- if func_name.startswith("block_") or func_name == "global_block_widgets":
575
+ if func_name == "global_block_widgets":
576
+ self._block_funcs["widgets"].append(func)
577
+ setattr(func, PRECONDITIONS_MARKER, (lambda d: True,))
578
+ continue
579
+ if func_name == "global_block_tree":
580
+ self._block_funcs["trees"].append(func)
581
+ setattr(func, PRECONDITIONS_MARKER, (lambda d: True,))
582
+ continue
583
+ if func_name.startswith("block_") and not func_name.startswith("block_tree_"):
605
584
  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)
585
+ logger.warning(f"No precondition in block widget function: {func_name}. Default globally active.")
586
+ setattr(func, PRECONDITIONS_MARKER, (lambda d: True,))
587
+ self._block_funcs["widgets"].append(func)
588
+ continue
589
+ if func_name.startswith("block_tree_"):
590
+ if getattr(func, PRECONDITIONS_MARKER, None) is None:
591
+ logger.warning(f"No precondition in block tree function: {func_name}. Default globally active.")
592
+ setattr(func, PRECONDITIONS_MARKER, (lambda d: True,))
593
+ self._block_funcs["trees"].append(func)
594
+
595
+ return self._block_funcs
610
596
 
611
- return self._block_widgets_funcs
612
597
 
613
598
  def _getBlockedWidgets(self):
614
- blocked_widgets = list()
615
- for func in self._blockWidgetFuncs:
599
+ """
600
+ Executes all blocking functions to get lists of widgets and trees to be blocked during testing.
601
+
602
+ Returns:
603
+ dict: A dictionary containing:
604
+ - 'widgets': List of XPath strings for individual widgets to block
605
+ - 'trees': List of XPath strings for widget trees to block
606
+ """
607
+ def _get_xpath_widgets(func):
608
+ blocked_set = set()
616
609
  try:
617
610
  script_driver = self.options.Driver.getScriptDriver()
618
- preconds = getattr(func, PRECONDITIONS_MARKER)
619
- if all([precond(script_driver) for precond in preconds]):
611
+ preconds = getattr(func, PRECONDITIONS_MARKER, [])
612
+ if all(precond(script_driver) for precond in preconds):
620
613
  _widgets = func(self.options.Driver.getStaticChecker())
621
- if not isinstance(_widgets, list):
622
- _widgets = [_widgets]
614
+ _widgets = _widgets if isinstance(_widgets, list) else [_widgets]
623
615
  for w in _widgets:
624
616
  if isinstance(w, StaticU2UiObject):
625
- blocked_widgets.append(w._getXPath(w.selector))
617
+ xpath = selector_to_xpath(w.selector, True)
618
+ blocked_set.add(xpath)
626
619
  elif isinstance(w, u2.xpath.XPathSelector):
627
- def getXPathRepr(w):
628
- return w._parent.xpath
629
- blocked_widgets.append(getXPathRepr(w))
620
+ xpath = w._parent.xpath
621
+ blocked_set.add(xpath)
630
622
  else:
631
623
  logger.warning(f"{w} Not supported")
632
- # blocked_widgets.extend([
633
- # w._getXPath(w.selector) for w in _widgets
634
- # ])
635
624
  except Exception as e:
636
- logger.error(f"error when getting blocked widgets: {e}")
637
- import traceback
625
+ logger.error(f"Error processing blocked widgets: {e}")
638
626
  traceback.print_exc()
627
+ return blocked_set
628
+
629
+ res = {
630
+ "widgets": set(),
631
+ "trees": set()
632
+ }
633
+
634
+
635
+ for func in self._blockWidgetFuncs["widgets"]:
636
+ widgets = _get_xpath_widgets(func)
637
+ res["widgets"].update(widgets)
638
+
639
+
640
+ for func in self._blockWidgetFuncs["trees"]:
641
+ trees = _get_xpath_widgets(func)
642
+ res["trees"].update(trees)
643
+
644
+
645
+ res["widgets"] = list(res["widgets"] - res["trees"])
646
+ res["trees"] = list(res["trees"])
647
+
648
+ return res
639
649
 
640
- return blocked_widgets
641
650
 
642
651
  def __del__(self):
643
652
  """tearDown method. Cleanup the env.
644
653
  """
654
+ try:
655
+ logger.debug("Generating test bug report")
656
+ report_generator = BugReportGenerator(self.options.output_dir)
657
+ report_generator.generate_report()
658
+ except Exception as e:
659
+ logger.error(f"Error generating bug report: {e}", flush=True)
660
+ try:
661
+ self.stopMonkey()
662
+ except Exception as e:
663
+ pass
645
664
  if self.options.Driver:
646
665
  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__":