diff --git a/README.md b/README.md index 28152c0c..8571784c 100644 --- a/README.md +++ b/README.md @@ -4,20 +4,41 @@ Project with test for NativeScript tooling. -## Install Requirements +## Requirements + +**Posix:** +- Python 2.7 or Python 3.2+ + +**Windows** +- Python 3.2+ -Install Python 2.*: -``` -brew install python -``` + +## Before Running Tests + +**Install Required Packages** Update `pip` and install project requirements: ``` python -m pip install --upgrade pip -pip install -r requirements.txt --user ``` -## Before Running Tests +Install packages on macOS: +```bash +pip install --upgrade -r requirements_darwin.txt --user +``` +Install packages on Windows on Linux: +```bash +pip install --upgrade -r requirements.txt --user +``` + +Set `PYTHONUNBUFFERED` and `PYTHONIOENCODING` environment variables: +```bash +export PYTHONUNBUFFERED=1 +export PYTHONIOENCODING=utf-8 +``` +Notes: +- `PYTHONUNBUFFERED` is required to get logging on Jenkins CI working properly. +- `PYTHONIOENCODING` helps to get command execution more stable. **Setup Machine** diff --git a/SETUP.md b/SETUP.md index a04a1ed2..1631ce79 100644 --- a/SETUP.md +++ b/SETUP.md @@ -2,7 +2,7 @@ ## Install Tesseract -In order to get OCR features workign you need to install `tesseract`. +In order to get OCR features working you need to install `tesseract`. **macOS** ```bash @@ -20,6 +20,14 @@ Download [installer](https://github.com/UB-Mannheim/tesseract/wiki) and install Notes: Installation of python wrapper around `tesseract` is handled in `requirements.txt`. +## OpenCV + +OpenCV has some known installation issues on Windows when Python 3.7 is used. + +Please read those articles: +- [import-cv2-doesnt-give-error-on-command-prompt-but-error-on-idle-on-windows-10รณ](https://stackoverflow.com/questions/49516989/import-cv2-doesnt-give-error-on-command-prompt-but-error-on-idle-on-windows-10) +- [opencv-for-python-3-x-under-windows](https://stackoverflow.com/questions/26489867/opencv-for-python-3-x-under-windows) +- [pythonlibs](https://www.lfd.uci.edu/~gohlke/pythonlibs/#opencv) ## (macOS Only) Allow apps to control your computer diff --git a/core/base_test/tns_test.py b/core/base_test/tns_test.py index 7bb6b129..12595dc0 100644 --- a/core/base_test/tns_test.py +++ b/core/base_test/tns_test.py @@ -27,7 +27,6 @@ def setUpClass(cls): Tns.kill() Gradle.kill() TnsTest.kill_emulators() - cls.kill_processes() # Ensure log folders are create Folder.create(Settings.TEST_OUT_HOME) @@ -50,10 +49,16 @@ def setUp(self): def tearDown(self): # Kill processes Tns.kill() - self.kill_processes() + Process.kill_all_in_context() # Analise test result - result = self._resultForDoCleanups + if Settings.PYTHON_VERSION < 3: + # noinspection PyUnresolvedReferences + result = self._resultForDoCleanups + else: + # noinspection PyUnresolvedReferences + result = self._outcome.result + outcome = 'FAILED' if result.errors == [] and result.failures == []: outcome = 'PASSED' @@ -69,18 +74,9 @@ def tearDownClass(cls): """ Tns.kill() TnsTest.kill_emulators() - for process in TestContext.STARTED_PROCESSES: - Log.info("Kill Process: " + os.linesep + process.commandline) - Process.kill_pid(process.pid) + Process.kill_all_in_context() Log.test_class_end(class_name=cls.__name__) - @staticmethod - def kill_processes(): - for process in TestContext.STARTED_PROCESSES: - if Process.is_running(process.pid): - Log.info("Kill Process: " + os.linesep + process.commandline) - Process.kill_pid(process.pid) - @staticmethod def kill_emulators(): DeviceManager.Emulator.stop() diff --git a/core/log/log.py b/core/log/log.py index 7d833cf6..c55615e9 100644 --- a/core/log/log.py +++ b/core/log/log.py @@ -8,10 +8,10 @@ class Log(object): @staticmethod - def log(level, message): + def log(level, msg): if level != logging.DEBUG: date = datetime.datetime.now().strftime('%H:%M:%S') - print '{0} {1}'.format(date, message) + print('{0} {1}'.format(date, msg)) @staticmethod def debug(message): @@ -51,12 +51,14 @@ def test_end(test_name, outcome): Log.info('TEST COMPLETE: {0}'.format(test_name)) Log.info('OUTCOME: {0}'.format(outcome)) Log.info('=============================================================') + Log.info('') @staticmethod def test_class_end(class_name): Log.info('') Log.info('END CLASS: {0}'.format(class_name)) Log.info('=============================================================') + Log.info('') @staticmethod def test_step(message): diff --git a/core/settings/Settings.py b/core/settings/Settings.py index 7b82cbd1..a8cae734 100644 --- a/core/settings/Settings.py +++ b/core/settings/Settings.py @@ -1,6 +1,7 @@ import logging import os import platform +import sys from core.enums.env import EnvironmentType from core.enums.os_type import OSType @@ -17,6 +18,10 @@ def get_os(): return OSType.LINUX +def get_python_version(): + return sys.version_info[0] + + def get_env(): env = os.environ.get('TEST_ENV', 'next') if 'next' in env: @@ -34,14 +39,14 @@ def get_project_home(): return home +HOST_OS = get_os() +PYTHON_VERSION = get_python_version() +ENV = get_env() + LOG_LEVEL = logging.DEBUG NS_GIT_ORG = 'NativeScript' -HOST_OS = get_os() - -ENV = get_env() - TEST_RUN_HOME = get_project_home() TEST_SUT_HOME = os.path.join(TEST_RUN_HOME, 'sut') diff --git a/core/utils/device/adb.py b/core/utils/device/adb.py index 7e31979f..d0a20133 100644 --- a/core/utils/device/adb.py +++ b/core/utils/device/adb.py @@ -5,7 +5,8 @@ from core.enums.os_type import OSType from core.settings import Settings from core.utils.file_utils import File -from core.utils.process import Run, Process +from core.utils.process import Process +from core.utils.run import run ANDROID_HOME = os.environ.get('ANDROID_HOME') ADB_PATH = os.path.join(ANDROID_HOME, 'platform-tools', 'adb') @@ -19,7 +20,7 @@ def __run_adb_command(command, id=None, wait=True, timeout=60, fail_safe=False, command = '{0} {1}'.format(ADB_PATH, command) else: command = '{0} -s {1} {2}'.format(ADB_PATH, id, command) - return Run.command(cmd=command, wait=wait, timeout=timeout, fail_safe=fail_safe, log_level=log_level) + return run(cmd=command, wait=wait, timeout=timeout, fail_safe=fail_safe, log_level=log_level) @staticmethod def __get_ids(include_emulator=False): @@ -145,12 +146,10 @@ def is_text_visible(id, text, case_sensitive=False): @staticmethod def get_screen(id, file_path): File.clean(path=file_path) - if Settings.OSType == OSType.WINDOWS: - Adb.__run_adb_command(command='exec-out screencap -p > ' + file_path, id=id, log_level=logging.INFO) + if Settings.HOST_OS == OSType.WINDOWS: + Adb.__run_adb_command(command='exec-out screencap -p > ' + file_path, id=id, log_level=logging.DEBUG) else: - Adb.__run_adb_command(command='shell rm /sdcard/screen.png', id=id) - Adb.__run_adb_command(command='shell screencap -p /sdcard/screen.png', id=id) - Adb.pull(id=id, source='/sdcard/screen.png', target=file_path) + Adb.__run_adb_command(command="shell screencap -p | perl -pe 's/\\x0D\\x0A/\\x0A/g' > " + file_path, id=id) if File.exists(file_path): return else: diff --git a/core/utils/device/device.py b/core/utils/device/device.py index 2bd8b20d..9d6a6063 100644 --- a/core/utils/device/device.py +++ b/core/utils/device/device.py @@ -11,7 +11,7 @@ from core.utils.device.simctl import Simctl from core.utils.file_utils import File, Folder from core.utils.image_utils import ImageUtils -from core.utils.process import Run +from core.utils.run import run from core.utils.wait import Wait if Settings.HOST_OS is OSType.OSX: @@ -26,7 +26,7 @@ def __init__(self, id, name, type, version): self.version = version if type is DeviceType.IOS: - type = Run.command(cmd="ideviceinfo | grep ProductType") + type = run(cmd="ideviceinfo | grep ProductType") type = type.replace(',', '') type = type.replace('ProductType:', '').strip(' ') self.name = type @@ -57,7 +57,7 @@ def is_text_visible(self, text): # Retry find with ORC if macOS automation fails if not is_visible: - actual_text = self.get_text().encode('utf-8').strip() + actual_text = self.get_text() if text in actual_text: is_visible = True else: @@ -70,7 +70,11 @@ def get_text(self): actual_image_path = os.path.join(Settings.TEST_OUT_IMAGES, img_name) File.clean(actual_image_path) self.get_screen(path=actual_image_path, log_level=logging.DEBUG) - return ImageUtils.get_text(image_path=actual_image_path) + text = ImageUtils.get_text(image_path=actual_image_path) + if Settings.PYTHON_VERSION < 3: + return text.encode('utf-8').strip() + else: + return text.encode('utf-8').strip().decode('utf-8') def wait_for_text(self, text, timeout=30, retry_delay=1): t_end = time.time() + timeout @@ -114,7 +118,7 @@ def get_screen(self, path, log_level=logging.INFO): image_saved = True if image_saved: message = "Image of {0} saved at {1}".format(self.id, path) - Log.log(level=log_level, message=message) + Log.log(level=log_level, msg=message) else: message = "Failed to save image of {0} saved at {1}".format(self.id, path) Log.error(message) diff --git a/core/utils/device/device_manager.py b/core/utils/device/device_manager.py index 0db16ca1..c1437bab 100644 --- a/core/utils/device/device_manager.py +++ b/core/utils/device/device_manager.py @@ -7,7 +7,8 @@ from core.utils.device.device import Device from core.utils.device.idevice import IDevice from core.utils.device.simctl import Simctl -from core.utils.process import Run, Process +from core.utils.process import Process +from core.utils.run import run class DeviceManager(object): @@ -59,7 +60,7 @@ def start(emulator, wipe_data=True): command = '{0} @{1} {2}'.format(emulator_path, emulator.avd, options) Log.info('Booting {0} with cmd:'.format(emulator.avd)) Log.info(command) - Run.command(cmd=command, wait=False, register_for_cleanup=False) + run(cmd=command, wait=False, register=False) booted = Adb.wait_until_boot(id=emulator.id) if booted: Log.info('{0} is up and running!'.format(emulator.avd)) @@ -102,7 +103,7 @@ class Simulator(object): def create(simulator_info): cmd = 'xcrun simctl create {0} "{1}" com.apple.CoreSimulator.SimRuntime.iOS-{2}' \ .format(simulator_info.name, simulator_info.device_type, str(simulator_info.sdk).replace('.', '-')) - result = Run.command(cmd=cmd, timeout=60) + result = run(cmd=cmd, timeout=60) assert result.exit_code == 0, 'Failed to create iOS Simulator with name {0}'.format(simulator_info.name) assert '-' in result.output, 'Failed to create iOS Simulator with name {0}'.format(simulator_info.name) simulator_info.id = result.output.splitlines()[0] @@ -121,8 +122,8 @@ def stop(id='booted'): Process.kill('launchd_sim') Process.kill_by_commandline('CoreSimulator') else: - print 'Stop simulator with id ' + id - Run.command(cmd='xcrun simctl shutdown {0}'.format(id), timeout=60) + Log.info('Stop simulator with id ' + id) + run(cmd='xcrun simctl shutdown {0}'.format(id), timeout=60) @staticmethod def start(simulator_info): @@ -135,7 +136,7 @@ def start(simulator_info): Log.debug('Simulator GUI is already running.') else: Log.info('Start simulator GUI.') - Run.command(cmd='open -a Simulator') + run(cmd='open -a Simulator') # Return result device = Device(id=simulator_info.id, name=simulator_info.name, type=DeviceType.SIM, diff --git a/core/utils/device/simctl.py b/core/utils/device/simctl.py index 7bca22f0..91526869 100644 --- a/core/utils/device/simctl.py +++ b/core/utils/device/simctl.py @@ -4,35 +4,26 @@ from core.log.log import Log from core.utils.file_utils import File -from core.utils.process import Run, Process -from core.utils.wait import Wait +from core.utils.run import run # noinspection PyShadowingBuiltins class Simctl(object): @staticmethod - def __run_simctl_command(command, wait=True, timeout=30): + def __run_simctl_command(command, wait=True, timeout=60): command = '{0} {1}'.format('xcrun simctl', command) - return Run.command(cmd=command, wait=wait, timeout=timeout) + return run(cmd=command, wait=wait, timeout=timeout) # noinspection PyBroadException @staticmethod def __get_simulators(): - result = Simctl.__run_simctl_command(command='list --json devices', wait=False) - logs = result.log_file - found = Wait.until(lambda: 'iPhone' in File.read(logs), timeout=30) - Process.kill_pid(result.pid) - if found: - json_content = '{' + File.read(logs).split('{', 1)[-1] - try: - return json.loads(json_content) - except ValueError: - Log.error('Failed to parse json ' + os.linesep + json_content) - return json.loads('{}') - else: - Log.error(File.read(logs)) - raise Exception('Failed to list iOS Devices!') + result = Simctl.__run_simctl_command(command='list --json devices') + try: + return json.loads(result.output) + except ValueError: + Log.error('Failed to parse json ' + os.linesep + result.output) + return json.loads('{}') @staticmethod def start(simulator_info): diff --git a/core/utils/git.py b/core/utils/git.py index be632cdc..7a22acf5 100644 --- a/core/utils/git.py +++ b/core/utils/git.py @@ -3,7 +3,7 @@ """ from core.settings import Settings from core.utils.file_utils import Folder -from core.utils.process import Run +from core.utils.run import run def get_repo_url(repo_url, ssh_clone=False): @@ -29,6 +29,6 @@ def clone(repo_url, local_folder, branch=None): command = 'git clone {0} "{1}"'.format(repo_url, str(local_folder)) if branch is not None: command = command + ' -b ' + branch - result = Run.command(cmd=command) + result = run(cmd=command) assert "fatal" not in result.output, "Failed to clone: " + repo_url assert result.exit_code is 0, "Failed to clone: " + repo_url diff --git a/core/utils/gradle.py b/core/utils/gradle.py index 0ec3de88..d1a34e5d 100644 --- a/core/utils/gradle.py +++ b/core/utils/gradle.py @@ -7,7 +7,8 @@ from core.enums.os_type import OSType from core.log.log import Log from core.settings import Settings -from core.utils.process import Run, Process +from core.utils.process import Process +from core.utils.run import run class Gradle(object): @@ -18,12 +19,12 @@ def kill(): Process.kill(proc_name='java.exe', proc_cmdline='gradle') else: command = "ps -ef | grep '.gradle/wrapper' | grep -v grep | awk '{ print $2 }' | xargs kill -9" - Run.command(cmd=command) + run(cmd=command) @staticmethod def cache_clean(): Log.info("Clean gradle cache.") if Settings.HOST_OS is OSType.WINDOWS: - Run.command(cmd="rmdir /s /q {USERPROFILE}\\.gradle".format(**os.environ)) + run(cmd="rmdir /s /q {USERPROFILE}\\.gradle".format(**os.environ)) else: - Run.command(cmd="rm -rf ~/.gradle") + run(cmd="rm -rf ~/.gradle") diff --git a/core/utils/npm.py b/core/utils/npm.py index 9e5c5978..558d100a 100644 --- a/core/utils/npm.py +++ b/core/utils/npm.py @@ -6,7 +6,7 @@ from core.log.log import Log from core.settings import Settings from core.utils.file_utils import File -from core.utils.process import Run +from core.utils.run import run from core.utils.version import Version @@ -15,7 +15,7 @@ class Npm(object): def __run_npm_command(cmd, folder=Settings.TEST_RUN_HOME, verify=True): command = 'npm {0}'.format(cmd) Log.info(command) - result = Run.command(cmd=command, cwd=folder) + result = run(cmd=command, cwd=folder, wait=True, timeout=300) if verify: assert result.exit_code is 0, "`npm " + command + "` exited with non zero exit code!: \n" + result.output Log.debug(result.output) diff --git a/core/utils/process.py b/core/utils/process.py index 9a350f20..66df3d71 100644 --- a/core/utils/process.py +++ b/core/utils/process.py @@ -1,9 +1,6 @@ import logging import os -import shlex import time -from datetime import datetime -from subprocess import Popen, PIPE import psutil @@ -11,72 +8,6 @@ from core.enums.os_type import OSType from core.log.log import Log from core.settings import Settings -from core.utils.file_utils import File -from core.utils.process_info import ProcessInfo - - -class Run(object): - @staticmethod - def command(cmd, cwd=Settings.TEST_RUN_HOME, wait=True, fail_safe=False, register_for_cleanup=True, timeout=600, - log_level=logging.DEBUG): - # Init result values - log_file = None - complete = False - duration = None - output = '' - - # Command settings - if not wait: - time_string = datetime.now().strftime('%Y_%m_%d_%H_%M_%S') - log_file = os.path.join(Settings.TEST_OUT_LOGS, 'command_{0}.txt'.format(time_string)) - File.write(path=log_file, text=cmd + os.linesep + '====>' + os.linesep) - cmd = cmd + ' >> ' + log_file + ' 2>&1 &' - - # Execute command: - Log.log(level=log_level, message='Execute command: ' + cmd) - Log.log(level=logging.DEBUG, message='CWD: ' + cwd) - if wait: - start = time.time() - if Settings.HOST_OS is OSType.WINDOWS: - process = Popen(cmd, stdout=PIPE, bufsize=1, cwd=cwd, shell=True) - else: - args = shlex.split(cmd) - process = Popen(args, stdout=PIPE, stderr=PIPE, cwd=cwd, shell=False) - p = psutil.Process(process.pid) - - # TODO: On Windows we hang if command do not complete for specified time - # See: https://stackoverflow.com/questions/2408650/why-does-python-subprocess-hang-after-proc-communicate - if Settings.HOST_OS is not OSType.WINDOWS: - try: - p.wait(timeout=timeout) - except psutil.TimeoutExpired: - p.kill() - if not fail_safe: - raise - else: - Log.error('Command reached timeout limit: {0}'.format(cmd)) - out, err = process.communicate() - output = (str(out) + os.linesep + str(err)).rstrip() - complete = True - end = time.time() - duration = end - start - else: - process = Popen(cmd, shell=True, stdin=None, stdout=None, stderr=None, close_fds=True, cwd=cwd) - - # Get result - pid = process.pid - exit_code = process.returncode - - if wait: - Log.log(level=log_level, message='OUTPUT: ' + os.linesep + output) - else: - Log.log(level=log_level, message='OUTPUT REDIRECTED: ' + log_file) - - result = ProcessInfo(cmd=cmd, pid=pid, exit_code=exit_code, output=output, log_file=log_file, complete=complete, - duration=duration) - if psutil.pid_exists(result.pid) and register_for_cleanup: - TestContext.STARTED_PROCESSES.append(result) - return result # noinspection PyBroadException,PyUnusedLocal @@ -165,10 +96,12 @@ def kill(proc_name, proc_cmdline=None): except Exception: continue if proc_name == name: + if Settings.HOST_OS == OSType.WINDOWS: + cmdline = cmdline.replace('\\\\', '\\') if (proc_cmdline is None) or (proc_cmdline is not None and proc_cmdline in cmdline): try: proc.kill() - Log.log(level=logging.DEBUG, message="Process {0} has been killed.".format(proc_name)) + Log.log(level=logging.DEBUG, msg="Process {0} has been killed.".format(proc_name)) result = True except psutil.NoSuchProcess: continue @@ -186,7 +119,7 @@ def kill_by_commandline(cmdline): if cmdline in cmd: try: proc.kill() - Log.log(level=logging.DEBUG, message="Process {0} has been killed.".format(cmdline)) + Log.log(level=logging.DEBUG, msg="Process {0} has been killed.".format(cmdline)) result = True except psutil.NoSuchProcess: continue @@ -197,7 +130,7 @@ def kill_pid(pid): try: p = psutil.Process(pid) p.terminate() - Log.log(level=logging.DEBUG, message="Process has been killed: {0}{1}".format(os.linesep, p.cmdline())) + Log.log(level=logging.DEBUG, msg="Process has been killed: {0}{1}".format(os.linesep, p.cmdline())) except Exception: pass @@ -207,8 +140,14 @@ def kill_by_handle(file_path): try: for item in proc.open_files(): if file_path in item.path: - print "{0} is locked by {1}".format(file_path, proc.name()) - print "Proc cmd: {0}".format(proc.cmdline()) + Log.debug("{0} is locked by {1}".format(file_path, proc.name())) + Log.debug("Proc cmd: {0}".format(proc.cmdline())) proc.kill() except Exception: continue + + @staticmethod + def kill_all_in_context(): + for process in TestContext.STARTED_PROCESSES: + name = process.commandline.split(' ')[0] + Process.kill(proc_name=name, proc_cmdline=Settings.TEST_RUN_HOME) diff --git a/core/utils/run.py b/core/utils/run.py new file mode 100644 index 00000000..588b8d43 --- /dev/null +++ b/core/utils/run.py @@ -0,0 +1,92 @@ +import logging +import os +import time +from datetime import datetime + +import psutil + +from core.base_test.test_context import TestContext +from core.log.log import Log +from core.settings import Settings +from core.utils.file_utils import File +from core.utils.process_info import ProcessInfo + +if os.name == 'posix' and Settings.PYTHON_VERSION < 3: + # Import subprocess32 on Posix when Python2 is detected + # noinspection PyPackageRequirements + import subprocess32 as subprocess +else: + import subprocess + + +def run(cmd, cwd=Settings.TEST_RUN_HOME, wait=True, timeout=600, fail_safe=False, register=True, + log_level=logging.DEBUG): + # Init result values + time_string = datetime.now().strftime('%Y_%m_%d_%H_%M_%S') + log_file = os.path.join(Settings.TEST_OUT_LOGS, 'command_{0}.txt'.format(time_string)) + complete = False + duration = None + output = '' + + # Command settings + if not wait: + File.write(path=log_file, text=cmd + os.linesep + '====>' + os.linesep) + cmd = cmd + ' >> ' + log_file + ' 2>&1 &' + + # Log command that will be executed: + Log.log(level=log_level, msg='Execute command: ' + cmd) + Log.log(level=logging.DEBUG, msg='CWD: ' + cwd) + + # Execute command: + if wait: + start = time.time() + with open(log_file, "w") as log: + process = subprocess.Popen(cmd, cwd=cwd, shell=True, stdout=subprocess.PIPE, stderr=log) + + # Wait until command complete + try: + process.wait(timeout=timeout) + complete = True + out, err = process.communicate() + if out is not None: + output = str(out.decode('utf-8')).strip() + if err is not None: + output = os.linesep + str(err.decode('utf-8')).strip() + except subprocess.TimeoutExpired: + process.kill() + if fail_safe: + Log.error('Command "{0}" timeout after {1} seconds.'.format(cmd, timeout)) + else: + raise + output = output + File.read(path=log_file) + # noinspection PyBroadException + try: + File.clean(path=log_file) + except Exception: + Log.debug('Failed to clean log file: {0}'.format(log_file)) + log_file = None + end = time.time() + duration = end - start + else: + process = psutil.Popen(cmd, cwd=cwd, shell=True, stdin=None, stdout=None, stderr=None, close_fds=True) + + # Get result + pid = process.pid + exit_code = process.returncode + + # Log output of the process + if wait: + Log.log(level=log_level, msg='OUTPUT: ' + os.linesep + output) + else: + Log.log(level=log_level, msg='OUTPUT REDIRECTED: ' + log_file) + + # Construct result + result = ProcessInfo(cmd=cmd, pid=pid, exit_code=exit_code, output=output, log_file=log_file, complete=complete, + duration=duration) + + # Register in TestContext + if psutil.pid_exists(result.pid) and register: + TestContext.STARTED_PROCESSES.append(result) + + # Return the result + return result diff --git a/core/utils/screen.py b/core/utils/screen.py index 87f440a9..1e73bbb3 100644 --- a/core/utils/screen.py +++ b/core/utils/screen.py @@ -1,25 +1,25 @@ +import logging import os - import time -import pytesseract -from PIL import Image - from core.enums.os_type import OSType from core.settings import Settings -from core.utils.file_utils import File -from core.utils.process import Run +from log.log import Log +from utils.file_utils import File +from utils.image_utils import ImageUtils +from utils.run import run class Screen(object): @staticmethod - def save_screen(path): + def save_screen(path, log_level=logging.DEBUG): """ Save screen of host machine. :param path: Path where screen will be saved. + :param log_level: Log level of the command. """ - print 'Save current host screen at {0}'.format(path) + Log.log(level=log_level, msg='Save current host screen at {0}'.format(path)) if Settings.HOST_OS is OSType.LINUX: import os os.system("import -window root {0}".format(path)) @@ -29,11 +29,10 @@ def save_screen(path): im = ImageGrab.grab() im.save(path) except IOError: - print 'Failed to take screen of host OS' + Log.error('Failed to take screen of host OS') if Settings.HOST_OS is OSType.OSX: - print 'Retry...' - # noinspection SpellCheckingInspection - Run.command(cmd='screencapture ' + path) + Log.info('Retry...') + run(cmd='screencapture ' + path) @staticmethod def get_screen_text(): @@ -45,8 +44,8 @@ def get_screen_text(): if File.exists(actual_image_path): File.clean(actual_image_path) Screen.save_screen(path=actual_image_path) - image = Image.open(actual_image_path) - text = pytesseract.image_to_string(image.convert('L')) + text = ImageUtils.get_text(image_path=actual_image_path) + File.clean(actual_image_path) return text @staticmethod @@ -59,17 +58,16 @@ def wait_for_text(text, timeout=60): """ t_end = time.time() + timeout found = False - actual_text = "" + actual_text = '' while time.time() < t_end: actual_text = Screen.get_screen_text() if text in actual_text: - print text + " found the screen!" + Log.info('"{0}" found on screen.'.format(text)) found = True break else: - print text + " NOT found the screen!" + Log.debug('"{0}" NOT found on screen.'.format(text)) time.sleep(5) if not found: - print "ACTUAL TEXT:" - print actual_text + Log.info('Actual text: {0}{1}'.format(os.linesep, actual_text)) return found diff --git a/core/utils/xcode.py b/core/utils/xcode.py index 81b4f97d..18338781 100644 --- a/core/utils/xcode.py +++ b/core/utils/xcode.py @@ -1,7 +1,7 @@ """ A wrapper around Xcode. """ -from core.utils.process import Run +from core.utils.run import run from core.utils.version import Version @@ -11,7 +11,7 @@ def cache_clean(): """ Cleanup Xcode cache and derived data """ - Run.command(cmd="rm -rf ~/Library/Developer/Xcode/DerivedData/*") + run(cmd="rm -rf ~/Library/Developer/Xcode/DerivedData/*") @staticmethod def get_version(): @@ -19,5 +19,5 @@ def get_version(): Get Xcode version :return: Version as int. """ - result = Run.command(cmd='xcodebuild -version').output.splitlines()[0].replace(' ', '').replace('Xcode', '') + result = run(cmd='xcodebuild -version').output.splitlines()[0].replace(' ', '').replace('Xcode', '') return Version.get(result) diff --git a/core_tests/core/adb_tests.py b/core_tests/device/adb_tests.py similarity index 94% rename from core_tests/core/adb_tests.py rename to core_tests/device/adb_tests.py index 113f01f6..bd96a401 100644 --- a/core_tests/core/adb_tests.py +++ b/core_tests/device/adb_tests.py @@ -1,14 +1,13 @@ import os from core.base_test.tns_test import TnsTest -from core.log.log import Log from core.settings import Settings from core.utils.device.adb import Adb from core.utils.device.device_manager import DeviceManager -# noinspection PyMethodMayBeStatic from core.utils.file_utils import File +# noinspection PyMethodMayBeStatic class AdbTests(TnsTest): emu = None @@ -16,7 +15,7 @@ class AdbTests(TnsTest): def setUpClass(cls): TnsTest.setUpClass() DeviceManager.Emulator.stop() - cls.emu = DeviceManager.Emulator.start(Settings.Emulators.DEFAULT, wipe_data=False) + cls.emu = DeviceManager.Emulator.start(Settings.Emulators.DEFAULT, wipe_data=True) def setUp(self): TnsTest.setUp(self) diff --git a/core_tests/core/device_tests.py b/core_tests/device/device_tests.py similarity index 99% rename from core_tests/core/device_tests.py rename to core_tests/device/device_tests.py index cc4126ad..0fe6641c 100644 --- a/core_tests/core/device_tests.py +++ b/core_tests/device/device_tests.py @@ -1,5 +1,4 @@ import os -import unittest from core.base_test.tns_test import TnsTest from core.enums.device_type import DeviceType diff --git a/core_tests/core/image_tests.py b/core_tests/utils/image_tests.py similarity index 100% rename from core_tests/core/image_tests.py rename to core_tests/utils/image_tests.py diff --git a/core_tests/core/process_tests.py b/core_tests/utils/process_tests.py similarity index 84% rename from core_tests/core/process_tests.py rename to core_tests/utils/process_tests.py index e0e239e6..fe8591af 100644 --- a/core_tests/core/process_tests.py +++ b/core_tests/utils/process_tests.py @@ -12,8 +12,9 @@ from core.settings import Settings from core.utils.file_utils import File from core.utils.perf_utils import PerfUtils -from core.utils.process import Run, Process +from core.utils.process import Process from core.utils.wait import Wait +from utils.run import run # noinspection PyMethodMayBeStatic @@ -21,12 +22,13 @@ class ProcessTests(unittest.TestCase): def tearDown(self): for process in TestContext.STARTED_PROCESSES: - Log.info("Kill Process: " + os.linesep + process.commandline) - Process.kill_pid(process.pid) + if Process.is_running(process.pid): + Log.info("Kill Process: " + os.linesep + process.commandline) + Process.kill_pid(process.pid) def test_01_run_simple_command(self): home = expanduser("~") - result = Run.command(cmd='ls ' + home) + result = run(cmd='ls ' + home) assert result.exit_code == 0, 'Wrong exit code of successful command.' assert result.log_file is None, 'No log file should be generated if wait=True.' assert result.complete is True, 'Complete should be true when process execution is complete.' @@ -36,19 +38,19 @@ def test_01_run_simple_command(self): @timed(5) def test_02_run_command_without_wait_for_completion(self): if Settings.HOST_OS == OSType.WINDOWS: - result = Run.command(cmd='pause', wait=False) + result = run(cmd='pause', wait=False) time.sleep(1) assert result.exit_code is None, 'exit code should be None when command is not complete.' - assert result.complete is False, 'tail command should not exit.' + assert result.complete is False, 'pause command should not exit.' assert result.duration is None, 'duration should be None in case process is not complete' assert result.output is '', 'output should be empty string.' - assert result.log_file is not None, 'stdout and stderr of tail command should be redirected to file.' + assert result.log_file is not None, 'stdout and stderr of pause command should be redirected to file.' assert 'pause' in File.read(result.log_file), 'Log file should contains cmd of the command.' assert 'Press any key' in File.read(result.log_file), 'Log file should contains output of the command.' else: file_path = os.path.join(Settings.TEST_OUT_HOME, 'temp.txt') File.write(path=file_path, text='test') - result = Run.command(cmd='tail -f ' + file_path, wait=False) + result = run(cmd='tail -f ' + file_path, wait=False) time.sleep(1) assert result.exit_code is None, 'exit code should be None when command is not complete.' assert result.complete is False, 'tail command should not exit.' @@ -66,7 +68,7 @@ def test_10_wait(self): @timed(5) def test_20_get_average_time(self): - ls_time = PerfUtils.get_average_time(lambda: Run.command(cmd='ifconfig'), retry_count=5) + ls_time = PerfUtils.get_average_time(lambda: run(cmd='ifconfig'), retry_count=5) assert 0.005 <= ls_time <= 0.025, "Command not executed in acceptable time. Actual value: " + str(ls_time) @staticmethod diff --git a/core_tests/utils/run_tests.py b/core_tests/utils/run_tests.py new file mode 100644 index 00000000..6982ae9d --- /dev/null +++ b/core_tests/utils/run_tests.py @@ -0,0 +1,87 @@ +import os +import time +import unittest +from os.path import expanduser + +from nose.tools import timed + +from core.settings import Settings +from core.utils.file_utils import File +from core.utils.process import Process +from core.utils.run import run + + +# noinspection PyMethodMayBeStatic +class RunTests(unittest.TestCase): + + def tearDown(self): + Process.kill_all_in_context() + + def test_01_run_simple_command(self): + home = expanduser("~") + result = run(cmd='ls ' + home, wait=True, timeout=1) + assert result.exit_code == 0, 'Wrong exit code of successful command.' + # assert result.log_file is None, 'No log file should be generated if wait=True.' + assert result.complete is True, 'Complete should be true when process execution is complete.' + assert result.duration < 1, 'Process duration took too much time.' + assert 'Desktop' in result.output, 'Listing home do not include Desktop folder.' + + def test_02_run_command_with_redirect(self): + home = expanduser("~") + out_file = os.path.join(Settings.TEST_OUT_HOME, 'log.txt') + result = run(cmd='ls ' + home + ' > ' + out_file, wait=True, timeout=1) + assert result.exit_code == 0, 'Wrong exit code of successful command.' + # assert result.log_file is None, 'No log file should be generated if wait=True.' + assert result.complete is True, 'Complete should be true when process execution is complete.' + assert result.duration < 1, 'Process duration took too much time.' + assert result.output == '', 'Output should be empty.' + assert 'Desktop' in File.read(path=out_file) + + def test_03_run_command_with_pipe(self): + result = run(cmd='echo "test case" | wc -w ', wait=True, timeout=1) + assert result.exit_code == 0, 'Wrong exit code of successful command.' + # assert result.log_file is None, 'No log file should be generated if wait=True.' + assert result.complete is True, 'Complete should be true when process execution is complete.' + assert result.duration < 1, 'Process duration took too much time.' + assert result.output == '2', 'Output should be 2.' + + def test_10_run_command_with_wait_true_that_exceed_timeout(self): + # noinspection PyBroadException + try: + run(cmd='sleep 3', wait=True, timeout=1, fail_safe=False) + assert False, 'This line should not be executed, because the line above should raise an exception.' + except Exception: + pass + + def test_11_run_command_with_wait_true_and_fail_safe_that_exceed_timeout(self): + result = run(cmd='sleep 3', wait=True, timeout=1, fail_safe=True) + assert result.exit_code is None, 'Exit code on non completed programs should be None.' + # assert result.log_file is None, 'No log file should be generated if wait=True.' + assert result.complete is False, 'Complete should be true when process execution is complete.' + assert result.duration < 2, 'Process duration should be same as timeout.' + assert result.output == '', 'No output for not completed programs.' + + @timed(5) + def test_20_run_long_living_process(self): + file_path = os.path.join(Settings.TEST_OUT_HOME, 'temp.txt') + File.write(path=file_path, text='test') + result = run(cmd='tail -f ' + file_path, wait=False) + time.sleep(1) + Process.kill_pid(pid=result.pid) + assert result.exit_code is None, 'exit code should be None when command is not complete.' + assert result.complete is False, 'tail command should not exit.' + assert result.duration is None, 'duration should be None in case process is not complete' + assert result.output is '', 'output should be empty string.' + assert result.log_file is not None, 'stdout and stderr of tail command should be redirected to file.' + assert 'tail' in File.read(result.log_file), 'Log file should contains cmd of the command.' + assert 'test' in File.read(result.log_file), 'Log file should contains output of the command.' + + @timed(30) + def test_40_run_npm_pack(self): + path = os.path.join(Settings.TEST_SUT_HOME, 'tns-android-5.0.0.tgz') + File.clean(path) + result = run(cmd='npm pack https://registry.npmjs.org/tns-android/-/tns-android-5.0.0.tgz', + cwd=Settings.TEST_SUT_HOME, wait=True) + assert File.exists(path) + assert 'tns-android-5.0.0.tgz' in result.output + assert '=== Tarball Contents ===' in result.output diff --git a/products/angular/ng.py b/products/angular/ng.py index f793ee58..26680c7f 100644 --- a/products/angular/ng.py +++ b/products/angular/ng.py @@ -4,7 +4,8 @@ from core.base_test.test_context import TestContext from core.settings import Settings from core.utils.file_utils import File, Folder -from core.utils.process import Run, Process +from core.utils.process import Process +from core.utils.run import run from core.utils.wait import Wait NS_SCHEMATICS = "@nativescript/schematics" @@ -23,7 +24,7 @@ def exec_command(command, cwd=Settings.TEST_RUN_HOME, wait=True): :rtype: core.utils.process_info.ProcessInfo """ cmd = '{0} {1}'.format(Settings.Executables.NG, command) - return Run.command(cmd=cmd, cwd=cwd, wait=wait, log_level=logging.INFO) + return run(cmd=cmd, cwd=cwd, wait=wait, log_level=logging.INFO) @staticmethod def new(collection=NS_SCHEMATICS, project=Settings.AppName.DEFAULT, shared=True, sample=False, prefix=None, diff --git a/products/nativescript/app.py b/products/nativescript/app.py index 1b3e237c..fd1011ae 100644 --- a/products/nativescript/app.py +++ b/products/nativescript/app.py @@ -4,7 +4,7 @@ from core.settings import Settings from core.utils.json_utils import JsonUtils from core.utils.npm import Npm -from core.utils.process import Run +from core.utils.run import run class App(object): @@ -46,24 +46,28 @@ def install_dev_dependency(app_name, dependency, version='latest'): @staticmethod def update(app_name, modules=True, angular=True, typescript=True, web_pack=True, ns_plugins=False): app_path = os.path.join(Settings.TEST_RUN_HOME, app_name) + modules_path = os.path.join(app_path, 'node_modules') if modules and App.is_dependency(app_name=app_name, dependency='tns-core-modules'): Npm.uninstall(package='tns-core-modules', option='--save', folder=app_path) Npm.install(package=Settings.Packages.MODULES, option='--save', folder=app_path) if angular and App.is_dependency(app_name=app_name, dependency='nativescript-angular'): Npm.uninstall(package='nativescript-angular', option='--save', folder=app_path) Npm.install(package=Settings.Packages.ANGULAR, option='--save', folder=app_path) - update_script = os.path.join(app_path, 'node_modules', '.bin', 'update-app-ng-deps') - result = Run.command(cmd=update_script, log_level=logging.INFO) + update_script = os.path.join(modules_path, '.bin', 'update-app-ng-deps') + result = run(cmd=update_script, log_level=logging.INFO) assert 'Angular dependencies updated' in result.output, 'Angular dependencies not updated.' Npm.install(folder=app_path) if typescript and App.is_dev_dependency(app_name=app_name, dependency='nativescript-dev-typescript'): Npm.uninstall(package='nativescript-dev-typescript', option='--save-dev', folder=app_path) Npm.install(package=Settings.Packages.TYPESCRIPT, option='--save-dev', folder=app_path) + update_script = os.path.join(modules_path, 'nativescript-dev-typescript', 'bin', 'ns-upgrade-tsconfig') + result = run(cmd=update_script, log_level=logging.INFO) + assert 'Adding tns-core-modules path mappings lib' in result.output if web_pack and App.is_dev_dependency(app_name=app_name, dependency='nativescript-dev-webpack'): Npm.uninstall(package='nativescript-dev-webpack', option='--save-dev', folder=app_path) Npm.install(package=Settings.Packages.WEBPACK, option='--save-dev', folder=app_path) - update_script = os.path.join(app_path, 'node_modules', '.bin', 'update-ns-webpack') + ' --deps --configs' - result = Run.command(cmd=update_script, log_level=logging.INFO) + update_script = os.path.join(modules_path, '.bin', 'update-ns-webpack') + ' --deps --configs' + result = run(cmd=update_script, log_level=logging.INFO) assert 'Updating dev dependencies...' in result.output, 'Webpack dependencies not updated.' assert 'Updating configuration files...' in result.output, 'Webpack configs not updated.' Npm.install(folder=app_path) diff --git a/products/nativescript/tns.py b/products/nativescript/tns.py index f18e7cad..843e91f9 100644 --- a/products/nativescript/tns.py +++ b/products/nativescript/tns.py @@ -7,7 +7,9 @@ from core.enums.platform_type import Platform from core.settings import Settings from core.utils.file_utils import Folder, File -from core.utils.process import Run, Process +from core.utils.process import Process +from core.utils.run import run +from log.log import Log from products.nativescript.app import App from products.nativescript.tns_assert import TnsAssert @@ -73,7 +75,15 @@ def exec_command(command, cwd=Settings.TEST_RUN_HOME, platform=Platform.NONE, em cmd += ' --justlaunch' if log_trace: cmd += ' --log trace' - return Run.command(cmd=cmd, cwd=cwd, wait=wait, log_level=logging.INFO, timeout=timeout) + + result = run(cmd=cmd, cwd=cwd, wait=wait, log_level=logging.INFO, timeout=timeout) + + # Retry in case of connectivity issues + if result.output is not None and 'Bad Gateway' in result.output: + Log.info('"Bad Gateway" issue detected! Will retry the command ...') + result = run(cmd=cmd, cwd=cwd, wait=wait, log_level=logging.INFO, timeout=timeout) + + return result @staticmethod def create(app_name=Settings.AppName.DEFAULT, template=None, path=None, app_id=None, diff --git a/requirements_darwin.txt b/requirements_darwin.txt index e8c5dd8f..5f8fb141 100644 --- a/requirements_darwin.txt +++ b/requirements_darwin.txt @@ -11,3 +11,4 @@ pytesseract>=0.2.5 flake8>=3.6.0 atomac>=1.1.0 PyObjC>=5.1.1 +subprocess32>=3.5.3 diff --git a/run_common.py b/run_common.py index bd5abbdf..bd98c132 100644 --- a/run_common.py +++ b/run_common.py @@ -17,6 +17,11 @@ def __cleanup(): """ Wipe TEST_OUT_HOME. """ + Folder.clean(os.path.join(Settings.TEST_RUN_HOME, 'node_modules')) + Folder.clean(Settings.TEST_OUT_HOME) + Folder.create(Settings.TEST_OUT_LOGS) + Folder.create(Settings.TEST_OUT_IMAGES) + DeviceManager.Emulator.stop() if Settings.HOST_OS == OSType.OSX: DeviceManager.Simulator.stop() @@ -25,10 +30,6 @@ def __cleanup(): Tns.kill() Gradle.kill() Gradle.cache_clean() - Folder.clean(os.path.join(Settings.TEST_RUN_HOME, 'node_modules')) - Folder.clean(Settings.TEST_OUT_HOME) - Folder.create(Settings.TEST_OUT_LOGS) - Folder.create(Settings.TEST_OUT_IMAGES) def __get_templates(): diff --git a/run_ns.py b/run_ns.py index c86c23e1..3c748ef3 100644 --- a/run_ns.py +++ b/run_ns.py @@ -8,7 +8,7 @@ if __name__ == '__main__': run_common.prepare() Log.info("Running tests...") - arguments = ['nosetests', '-v', '-s', '--nologcapture', '--logging-filter=nose', '--with-xunit', '--with-flaky'] + arguments = ['nosetests', '-v', '-s', '--nologcapture', '--with-doctest', '--with-xunit'] for i in sys.argv: arguments.append(str(i)) nose.run(argv=arguments)