def _start_sikuli_java_process(self): libFolder = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'lib') jarList = glob.glob(libFolder + os.sep + '*.jar') if len(jarList) != 1: raise Exception('Sikuli jar package should be exist in lib folder') sikuliJar = jarList[0] java = 'java' arguments = [ '-jar', sikuliJar, str(self.port), self._get_output_folder() ] self.process = Process() if os.getenv("DISABLE_SIKULI_LOG"): self.process.start_process(java, *arguments, shell=True) else: self.process.start_process(java, *arguments, shell=True, stdout=self._output_file(), stderr=self._err_file()) self.logger.info('Start sikuli java process on port %s' % str(self.port)) self._wait_process_started() self.logger.info('Sikuli java process is started')
def _run_process(self, command, *arguments, **configuration): expected_rc = int(configuration.pop('expected_rc', 0)) token = configuration.pop('token', 'process') merged_output = is_truthy(configuration.pop('merged_output', True)) input = configuration.pop('input', None) if input and not isinstance(input, bytes): input = input.encode() tty = is_truthy(configuration.pop('tty', False)) redirection = configuration.pop('redirection', None) # For compatibility with Process.run_process() timeout = configuration.pop('timeout', None) on_timeout = configuration.pop('on_timeout', 'terminate') if redirection and not tty: raise ValueError('Cannot use "redirection" without "tty"') with ExitStack() as stack: if merged_output: stdout = stack.enter_context(_Attachment(token + '-output.txt')).path stderr = 'STDOUT' else: stdout = stack.enter_context(_Attachment(token + '-stdout.txt')).path stderr = stack.enter_context(_Attachment(token + '-stderr.txt')).path if tty: joined = shlex.join((command, ) + arguments) if redirection: joined += ' ' + redirection command = 'script' arguments = list() arguments += ['--return'] arguments += ['--quiet'] arguments += ['--echo', 'never', '--log-out', '/dev/null'] arguments += ['--command', joined] process = Process() handle = process.start_process(command, *arguments, **configuration, stdout=str(stdout), stderr=str(stderr)) if input: process_object = process.get_process_object(handle) process_object.stdin.write(input) process_object.stdin.close() result = process.wait_for_process(handle, timeout, on_timeout) if result.rc != expected_rc: raise AssertionError( 'Process exited with unexpected code {}'.format(result.rc)) return result
def _start_sikuli_java_process(self): libFolder = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'lib') jarList = glob.glob(libFolder+os.sep+'*.jar') if len(jarList) != 1: raise Exception('Sikuli jar package should be exist in lib folder') sikuliJar = jarList[0] command = 'java -jar '+sikuliJar+' %s ' % str(self.port) process = Process() process.start_process(command, shell=True) self.logger.info('Start sikuli java process on port %s' % str(self.port)) self._wait_process_started() self.logger.info('Sikuli java process is started')
def __init__(self): Screenshot.__init__(self, "C:\\Keyword\\Results") XML.__init__(self, use_lxml=True) """CommonUtil.__init__(self, None, dict , False)""" Telnet.__init__(self, timeout='3 seconds', newline='CRLF', prompt=None, prompt_is_regexp=False, encoding='UTF-8', encoding_errors='ignore', default_log_level='INFO') Process.__init__(self) """Also this doc should be in shown in library doc."""
def _start_sikuli_java_process(self): libFolder = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'lib') jarList = glob.glob(libFolder + os.sep + '*.jar') if len(jarList) != 1: raise Exception('Sikuli jar package should be exist in lib folder') sikuliJar = jarList[0] command = 'java -jar ' + sikuliJar + ' %s ' % str( self.port) + self._get_output_folder() process = Process() process.start_process(command, shell=True, stdout=self._output_file(), stderr=self._err_file()) self.logger.info('Start sikuli java process on port %s' % str(self.port)) self._wait_process_started() self.logger.info('Sikuli java process is started')
def maybe_uninstall_sdk(self): variables = BuiltIn().get_variables() use_existing_sdk_installation = variables['${USE_EXISTING_SDK_INSTALLATION}'] if use_existing_sdk_installation: # Ensure we always return a valid result object return Process().run_process('true') return self._run_sdk_maintenance_tool(mode='uninstall')
def stop_smtp_stub(self): print 'Handle: %d' % self.__stub_handle if self.__stub_started: logger.info('Stopping STUB') #Process().terminate_process(self.__stub_handle, kill=True) Process().terminate_all_processes(kill=True) else: logger.warn('Stub already stopped') self.__stub_started = False
def start_smtp_stub(self): if not self.__stub_started: logger.info('Starting STUB on host: %s port: %d' % (self.__smtphost, self.__smtpport)) stubscript = os.path.join(base_dir, '..', 'server', 'smtp-stub.py') self.__stub_handle = Process().start_process(sys.executable, stubscript, cwd=base_dir, alias='stub') print 'Handle: %d' % self.__stub_handle else: logger.warn('Stub already started') self.__stub_started = True
def _run_process(self, command, *arguments, expected_rc=0, token='process', merged_output=True, **configuration): with ExitStack() as stack: if merged_output: stdout = stack.enter_context(_Attachment(token + '-output.txt')).path stderr = 'STDOUT' else: stdout = stack.enter_context(_Attachment(token + '-stdout.txt')).path stderr = stack.enter_context(_Attachment(token + '-stderr.txt')).path result = Process().run_process(command, *arguments, **configuration, stdout=str(stdout), stderr=str(stderr)) if result.rc != expected_rc: raise AssertionError('Process exited with unexpected code {}'.format(result.rc)) return result
def maybe_install_sdk(self, engine_memory_size_mb=None): variables = BuiltIn().get_variables() sdk_install_dir = variables['${SDK_INSTALL_DIR}'] use_existing_sdk_installation = variables[ '${USE_EXISTING_SDK_INSTALLATION}'] if use_existing_sdk_installation: OperatingSystem().directory_should_exist(sdk_install_dir) # Ensure we always return a valid result object return Process().run_process('true') try: OperatingSystem().directory_should_not_exist(sdk_install_dir) except: # Ensure maybe_uninstall_sdk will not destroy existing installation BuiltIn().set_global_variable('${USE_EXISTING_SDK_INSTALLATION}', True) raise command = variables['${INSTALLER}'] build_engine_type = variables['${BUILD_ENGINE_TYPE}'] args = [ '--verbose', 'non-interactive=1', 'accept-licenses=1', 'buildEngineType=' + build_engine_type ] result = self._run_process(command, *args, token='installer') if engine_memory_size_mb: args = ['engine', 'set', 'vm.memorySize=' + engine_memory_size_mb] result = self.run_sfdk(*args) if variables['${DO_SSU_REGISTER}']: credentials_file = variables['${CREDENTIALS}'] args = ['engine', 'exec', 'bash', '-c', 'creds=$(<"{}") && sdk-manage register-all --no-sdk --force \ --user "${{creds%%:*}}" --password "${{creds#*:}}"' \ .format(credentials_file)] result = self.run_sfdk(*args) return result
class SikuliLibrary(object): ROBOT_LIBRARY_SCOPE = 'GLOBAL' ROBOT_LIBRARY_VERSION = VERSION def __init__(self, port=0, timeout=3.0, mode='OLD'): """ @port: sikuli java process socket port @timeout: Timeout of waiting java process started @mode: if set as 'DOC', will stop java process automatically, if set as 'PYTHON', means library is running out of robot environment if set as 'CREATE', it is only for mvn package usage, will create keywords.py file if set as 'OLD'(default), sikuli java process will be started when library is inited if set as 'NEW', user should use 'start_sikuli_process' to start java process """ self.logger = self._init_logger() self.timeout = float(timeout) self.port = None self.remote = None self.mode = mode.upper().strip() if mode == 'OLD': self.start_sikuli_process(port) if mode.upper().strip() == 'DOC': self.start_sikuli_process() self._stop_thread(4) elif mode.upper().strip() == 'PYTHON': self.connect_sikuli_process(port) elif mode.upper().strip() == 'CREATE': self._create_keywords_file() elif mode.upper().strip() != 'NEW': self._check_robot_running() def start_sikuli_process(self, port=None): """ This keyword is used to start sikuli java process. If library is inited with mode "OLD", sikuli java process is started automatically. If library is inited with mode "NEW", this keyword should be used. :param port: port of sikuli java process, if value is None or 0, a random free port will be used :return: None """ if port is None or int(port) == 0: port = self._get_free_tcp_port() self.port = port start_retries = 0 started = False while start_retries < 5: try: self._start_sikuli_java_process() except RuntimeError as err: print('error........%s' % err) if self.process: self.process.terminate_process() self.port = self._get_free_tcp_port() start_retries += 1 continue started = True break if not started: raise RuntimeError('Start sikuli java process failed!') self.remote = self._connect_remote_library() def connect_sikuli_process(self, port): self.port = port self.remote = self._connect_remote_library() def _create_keywords_file(self): keywordDict = {} self.start_sikuli_process() try: keywordList = self.get_keyword_names() for keywordName in keywordList: keywordDict[keywordName] = {} keywordDict[keywordName]['arg'] = self.get_keyword_arguments( keywordName) keywordDict[keywordName][ 'doc'] = self.get_keyword_documentation(keywordName) with codecs.open(os.path.join( os.path.abspath(os.path.dirname(__file__)), 'keywords.py'), 'w', encoding='utf-8') as f: f.write('# -*- coding: utf-8 -*-\n') # keywords = ','.join(['"%s": %s' % (k, keywordDict[k]) for k in keywordDict.keys()]) f.write('KEYWORDS = %s' % keywordDict) finally: self._stop_thread(3) def _check_robot_running(self): try: BuiltIn().get_variable_value('${SUITE SOURCE}') except Exception as err: self.logger.warn('Robot may not running, stop java process: %s' % err) self._stop_thread(1) def _init_logger(self): robotLogLevels = { 'TRACE': int(logging.DEBUG / 2), 'DEBUG': logging.DEBUG, 'INFO': logging.INFO, 'HTML': logging.INFO, 'WARN': logging.WARN } builtIn = BuiltIn() handler = logging.StreamHandler(sys.stdout) formatter = logging.Formatter('%(message)s') handler.setFormatter(formatter) logger = logging.getLogger('SikuliLibraryLogger') logger.addHandler(handler) level = logging.DEBUG try: logLevel = builtIn.get_variable_value('${LOG_LEVEL}') level = robotLogLevels[logLevel] except Exception: pass logger.setLevel(level) return logger def _get_free_tcp_port(self): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.bind(('localhost', 0)) sock.listen(1) host, port = sock.getsockname() self.logger.debug('Free TCP port is: %d' % port) sock.close() return port def _start_sikuli_java_process(self): libFolder = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'lib') jarList = glob.glob(libFolder + os.sep + '*.jar') if len(jarList) != 1: raise Exception('Sikuli jar package should be exist in lib folder') sikuliJar = jarList[0] java = 'java' arguments = [ '-jar', sikuliJar, str(self.port), self._get_output_folder() ] self.process = Process() if os.getenv("DISABLE_SIKULI_LOG"): self.process.start_process(java, *arguments, shell=True) else: self.process.start_process(java, *arguments, shell=True, stdout=self._output_file(), stderr=self._err_file()) self.logger.info('Start sikuli java process on port %s' % str(self.port)) self._wait_process_started() self.logger.info('Sikuli java process is started') def _wait_process_started(self): url = "http://127.0.0.1:%s/" % str(self.port) currentTime = startedTime = time.time() started = False while (currentTime - startedTime) < self.timeout: try: urlopen(url).read() except Exception: currentTime = time.time() time.sleep(1.0) continue started = True break if not started: raise RuntimeError('Start sikuli java process failed!') def _output_file(self): outputDir = self._get_output_folder() outputFile = 'Sikuli_java_stdout_' + str(time.time()) + '.txt' return os.path.join(outputDir, outputFile) def _err_file(self): outputDir = self._get_output_folder() errFile = 'Sikuli_java_stderr_' + str(time.time()) + '.txt' return os.path.join(outputDir, errFile) def _get_output_folder(self): outputDir = os.path.abspath(os.curdir) try: outputDir = BuiltIn().get_variable_value('${OUTPUTDIR}') except Exception: pass return outputDir def _connect_remote_library(self): remoteUrl = 'http://127.0.0.1:%s/' % str(self.port) remote = Remote(remoteUrl) self._test_get_keyword_names(remote) return remote def _test_get_keyword_names(self, remote): currentTime = startedTime = time.time() started = False while (currentTime - startedTime) < self.timeout: try: remote.get_keyword_names() except Exception as err: self.logger.warn("Test get_keyword_names failed! %s" % err) currentTime = time.time() time.sleep(1.0) continue started = True break if not started: raise RuntimeError('Failed to get_keyword_names!') def get_keyword_names(self): if self.mode == 'CREATE': return self.remote.get_keyword_names() + ['start_sikuli_process'] return list(KEYWORDS.keys()) + ['start_sikuli_process'] # return self.remote.get_keyword_names() + ['start_sikuli_process'] def get_keyword_arguments(self, name): if name == 'start_sikuli_process': return ['port=None'] if self.mode == 'CREATE': return self.remote.get_keyword_arguments(name) return KEYWORDS[name]['arg'] def get_keyword_documentation(self, name): if name == 'start_sikuli_process': return self.start_sikuli_process.__doc__ elif name == '__intro__': return SikuliLibrary.__doc__ elif name == '__init__': return getattr(self, name).__doc__ if self.mode == 'CREATE': return self.remote.get_keyword_documentation(name) return KEYWORDS[name]['doc'] def run_keyword(self, name, arguments=[], kwargs={}): if name == 'start_sikuli_process': return self.start_sikuli_process(*arguments) return self.remote.run_keyword(name, arguments, kwargs) def _stop_thread(self, timeout): def stop(): time.sleep(float(timeout)) self.run_keyword('stop_remote_server') thread = threading.Thread(target=stop, args=()) thread.start()
def update_queue_message(self, MQPropertiesFile, MisysUtilBatfile, *params): """| Usage | Used to put or delete MQ queue messages. PreRequisite : Security feature should be disable in MQ before updating any queue messages. Follow below steps to disable it. 1.Login to system where MQ is installed. 2.Make sure the user with which you start your WebSphere profile should be in mq server and user should be added to mqm group 3.Run below commands : * runmqsc <QManager name> [Example: runmqsc MM453] * alter qmgr chlauth(disabled) * restart qmanager | Arguments | 'MQPropertiesFile' = mq.properties file path. 'MisysUtilBatfile' = 'misys-mq-util' bat file location present under misys-mq-util folder. '*params' : Can pass variable number of arguments in mq.properties file to configure Queue. Input format for * params : key=value (example: MQCTestCase=C:\\ITL) Example: create list variable: | *** Variables *** | | @{QueueDetails} QueueManager=xyz HostName=pqr QueueName=qm Channel=SYSTEM.DEF.SVRCONN Port=1416 InputfilePath=D:\\MQ_Queue_Messages\\message1.txt | |Update Queue Message | ${mqpropertiesfile} | ${MisysUtilBatfile} | @{QueueDetails} | """ # --- check if MQPropertiesFile exist if not os.path.exists(MQPropertiesFile): raise AssertionError("File not found error :" + MQPropertiesFile + " doesnot exist") # --- check if MisysUtilBatfile exist if not os.path.exists(MisysUtilBatfile): raise AssertionError("File not found error :" + MisysUtilBatfile + " doesnot exist") # ----- param will hold variable number of parameters that needs to be changed param_dict = collections.OrderedDict() for param in params: parameter = str(param).split("=") parameter_name = parameter[0] if parameter_name.lower() == 'inputfilepath': parameter_value = parameter[1].replace('\\', '\\\\') else: parameter_value = parameter[1] param_dict[parameter_name] = parameter_value # --- check if that parameter_name is present in file , if present then update it with open(MQPropertiesFile, 'r') as f: Filedata = f.read().split("\n") columns = list(param_dict.keys()) for column in columns: for i in range(0, len(Filedata) - 1): if str(column).lower() == str( Filedata[i].split("=")[0]).lower(): parametername = Filedata[i].split("=")[0] Filedata[ i] = parametername + "=" + param_dict[column] with open(MQPropertiesFile, 'w') as fr: for data in Filedata: fr.write(data) fr.write("\n") fr.close() # ------ create cwd for to run bat process filedir = MisysUtilBatfile.split("\\") batcwd = "" for i in range(0, len(filedir) - 1): if i < len(filedir) - 2: batcwd = batcwd + filedir[i] + "\\" else: batcwd = batcwd + filedir[i] status = Process().run_process(MisysUtilBatfile, cwd=str(batcwd).strip()) stdoutput = status.stdout if "failed" in stdoutput: print( " Following data passed in mq.properties file: ------------------------" ) for param in params: print(param) print("\n---------------Error Details --------------") raise AssertionError(stdoutput) else: print( " Following Data updated in mq.properties file: -----------------" ) for param in params: print(param) return "PASS"
def __init__(self, cf_cli_path="cf"): os.makedirs("./.cf_home", exist_ok=True) self._process = Process() self.cf_cli_path = cf_cli_path
class CFCliLibrary(object): def __init__(self, cf_cli_path="cf"): os.makedirs("./.cf_home", exist_ok=True) self._process = Process() self.cf_cli_path = cf_cli_path def login(self, api, user, password, skip_ssl=False): api_cmd = ["api", api] if skip_ssl: api_cmd.append("--skip-ssl-validation") self.cf(*api_cmd) auth_cmd = ["auth", user, password] return self.cf(*auth_cmd) def cf(self, *arguments, **configuration): configuration["env:CF_HOME"] = "./.cf_home" configuration["env:CF_PLUGIN_HOME"] = expanduser("~") result = self._process.run_process(self.cf_cli_path, *arguments, **configuration) result_msg = result.stdout if result_msg is None: result_msg = result.stderr elif result.stderr is not None: result_msg += result.stderr if result.rc != 0: raise AssertionError(result_msg) return result_msg def target(self, org, space=None): target_cmd = ["target", "-o", org] if space is not None: target_cmd.append("-s") target_cmd.append(space) return self.cf(*target_cmd) def create_org_with_space_and_target(self, org, space): self.cf("create-org", org) self.cf("create-space", space, "-o", org) return self.target(org, space) def create_space_and_target(self, org, space): self.cf("create-space", space, "-o", org) return self.target(org, space) def get_first_buildpack(self, name): result = self.cf(*["buildpacks"]) for line in result.splitlines(): fields = line.strip().split() if len(fields) < 2: continue if fields[0].startswith(name): return fields[0] # do the same job for cli v7 which set position first if fields[1].startswith(name): return fields[1] raise AssertionError('Buildpack with {} does not exists'.format(name)) def delete_org(self, org): return self.cf("delete-org", "-f", org) def download_and_extract_app(self, appname): tfile = tempfile.mkstemp(prefix="cfrobot-cats") (fd, filename) = tfile os.close(fd) tdir = tempfile.mkdtemp(prefix="cfrobot-cats") try: app_guid = self.cf("app", appname, "--guid") self.cf("curl", '/v2/apps/{}/download'.format(app_guid), "--output", filename, **{'stdout': 'DEVNULL', 'stderr': 'DEVNULL'}) with zipfile.ZipFile(filename, "r") as zip_ref: zip_ref.extractall(tdir) except Exception as e: shutil.rmtree(tdir, ignore_errors=True) raise e finally: os.remove(filename) return tdir def ensure_feature_flags(self, flags_config): feature_flags = {} result = self.cf(*["feature-flags"]) for line in result.splitlines(): fields = line.strip().split() if len(fields) < 2: continue feature_flags[fields[0]] = fields[1] for flag, state in flags_config.items(): if feature_flags[flag] != state: raise AssertionError('Flag {} is not in requested state {}'.format(flag, state)) def cleanup(self, kill=False): self._process.terminate_all_processes(kill)
class FormsLibrary(object): """Robot Framework library leveraging Java-agents to run [https://github.com/robotframework/SwingLibrary|SwingLibrary] keywords on Java-processes. To take the library in to use add formslibrary-[version].jar to PYTHONPATH. The library contains a simple socket server to communicate with Java agents. When taking the library into use, you can specify the port this server uses. Providing the port is optional. If you do not provide one, FormsLibrary will ask the OS for an unused port. Keywords directly offered by this library on top of SwingLibrary keywords are: - [#Application Started|Application Started] - [#Ensure Application Should Close|Ensure Application Should Close] - [#Log Java System Properties|Log Java System Properties] - [#Set Java Tool Options|Set Java Tool Options] - [#Start Application|Start Application] - [#System Exit|System Exit] - [#Switch To Application|Switch To Application] - [#Connect To Application|Connect To Application] FormsLibrary also introduces two global variables that can be used during testing: - ${REMOTESWINGLIBRARYPATH} the location of the formslibrary jar file. - ${REMOTESWINGLIBRARYPORT} port used by the agents to communicate with the library - this is needed if a java agent is started for example from another machine. [https://github.com/ombre42/jrobotremoteserver|jrobotremoteserver] that is used by FormsLibrary also offers a keyword: - [#Stop Remote Server|Stop Remote Server] Following SwingLibrary Keywords are not available through FormsLibrary: - Launch Application - SwingLibrary version of Start Application - Start Application In Separate Thread NOTE! [#Get Table Cell Property|Get Table Cell Property] will return the string representation of that property and not the actual object. Complex objects are not passed through Remote library interface. Examples: | * Settings * | | Library | FormsLibrary | | * Test Cases * | | Testing java application | | | Start Application | myjavaapp | java -jar myjava.jar | | | Select Window | My App | | | Ensure Application Should Close | 60 seconds | Push Button | Exit | """ ROBOT_LIBRARY_SCOPE = 'GLOBAL' KEYWORDS = ['system_exit', 'start_application', 'application_started', 'switch_to_application', 'ensure_application_should_close', 'log_java_system_properties', 'set_java_tool_options', 'connect_to_application'] REMOTES = {} CURRENT = None PROCESS = Process() ROBOT_NAMESPACE_BRIDGE = RobotLibraryImporter() TIMEOUT = 60 PORT = None APHOST = None APPORT = None AGENT_PATH = os.path.abspath(os.path.dirname(__file__)) _output_dir = '' def __init__(self, port=None, aphost='127.0.0.1', apport=None, debug=False, close_security_dialogs=False, __reload=False): """ *port*: optional port for the server receiving connections from remote agents *apport*: optional port for server receiving connections of remote agent. Should be used if you want to reconnect to same application between robot runs. *aphost*: opptional network address of application we want to connect into *debug*: optional flag that will start agent in mode with more logging for troubleshooting (set to TRUE to enable) *close_security_dialogs*: optional flag for automatic security dialogs closing (set to TRUE to enable) NOTE! with special value 'TEST' starts a test application for documentation generation purposes `python -m robot.libdoc FormsLibrary::TEST FormsLibrary.html` NOTE! FormsLibrary is a so called Global Scope library. This means when it is imported once it will be available until end of robot run. If Robot encounters another import of FormsLibrary with different import parameters the library will be reloaded and new options will be in use. You should be careful if you import FormsLibrary with different options in multiple test suites and resources. """ if not __reload: FormsLibrary.CURRENT = None global REMOTE_AGENTS_LIST REMOTE_AGENTS_LIST = AgentList() FormsLibrary.PORT = None self.ROBOT_NAMESPACE_BRIDGE.set_args(port, aphost, apport, debug, close_security_dialogs) if FormsLibrary.PORT is None: FormsLibrary.PORT = self._start_port_server(0 if port == 'TEST' else port or 0) FormsLibrary.APHOST = aphost FormsLibrary.APPORT = apport self._create_env(bool(debug), bool(close_security_dialogs)) if port == 'TEST': self.start_application('docgenerator', 'java -jar %s' % FormsLibrary.AGENT_PATH, timeout=4.0) @property def current(self): if not self.CURRENT: return None return self.REMOTES[self.CURRENT][0] def _start_port_server(self, port): address = ('0.0.0.0', int(port)) server = SocketServer.TCPServer(address, SimpleServer) server.allow_reuse_address = True #t = threading.Thread(name="FormsLibrary registration server thread", # target=server.serve_forever) t = threading.Thread(name="FormsLibrary registration server thread", target=server.serve_forever, args=(0.01,)) t.setDaemon(True) t.start() return server.server_address[1] def _create_env(self, debug, close_security_dialogs): agent_command = ' -agentlib:jdwp=transport=dt_socket,address=8000,server=y,suspend=n -javaagent:"%s"=127.0.0.1:%s' % (FormsLibrary.AGENT_PATH, FormsLibrary.PORT) if FormsLibrary.APPORT: agent_command += ':APPORT=%s' % FormsLibrary.APPORT if debug: agent_command += ':DEBUG' if close_security_dialogs: agent_command += ':CLOSE_SECURITY_DIALOGS' self._agent_command = agent_command try: BuiltIn().set_global_variable('\${REMOTESWINGLIBRARYPATH}', self._escape_path(FormsLibrary.AGENT_PATH)) BuiltIn().set_global_variable('\${REMOTESWINGLIBRARYPORT}', FormsLibrary.PORT) self._output_dir = BuiltIn().get_variable_value('${OUTPUTDIR}') except RobotNotRunningError: pass logger.info(agent_command) def _escape_path(self, text): return text.replace("\\","\\\\") @contextmanager def _agent_java_tool_options(self): old_tool_options = os.environ.get('JAVA_TOOL_OPTIONS', '') old_options = os.environ.get('_JAVA_OPTIONS', '') logger.debug("Picked old JAVA_TOOL_OPTIONS='%s'" % old_tool_options) logger.debug("Picked old _JAVA_OPTIONS='%s'" % old_options) self.set_java_tool_options() try: yield finally: os.environ['JAVA_TOOL_OPTIONS'] = old_tool_options os.environ['_JAVA_OPTIONS'] = old_options logger.debug("Returned old JAVA_TOOL_OPTIONS='%s'" % old_tool_options) logger.debug("Returned old _JAVA_OPTIONS='%s'" % old_options) def set_java_tool_options(self): """Sets the JAVA_TOOL_OPTIONS to include FormsLibrary Agent and the _JAVA_OPTIONS to set a temporary policy granting all permissions. FormsLibrary Agent is normally enabled by `Start Application` by setting the JAVA_TOOL_OPTIONS environment variable only during that keyword call. So java processes started by other commands wont normally use the FormsLibrary Agent. This keyword sets that same environment variable to be used always. So all java processes started after this will use the Agent. This methods also creates temporary Java policy file which grants all permissions. This file is set as policy for each java command call. """ os.environ['JAVA_TOOL_OPTIONS'] = self._agent_command logger.info("Set JAVA_TOOL_OPTIONS='%s'" % self._agent_command) with tempfile.NamedTemporaryFile(prefix='grant_all_', suffix='.policy', delete=False) as t: text = b""" grant { permission java.security.AllPermission; }; """ t.write(text) java_policy = '-Djava.security.policy="%s"' % t.name variables = BuiltIn().get_variables() output_dir = variables['${OUTPUTDIR}'] robot_output_dir = ' -Drobot.output_dir="%s"' % output_dir os.environ['_JAVA_OPTIONS'] = java_policy + robot_output_dir logger.info("Set _JAVA_OPTIONS='%s'" % os.environ['_JAVA_OPTIONS']) def start_application(self, alias, command, timeout=60, name_contains=None): """Starts the process in the `command` parameter on the host operating system. The given alias is stored to identify the started application in FormsLibrary. timeout (default 60) is timeout in seconds. name_contains is a text that must be part of the name of the java process that we are connecting to. name_contains helps in situations where multiple java-processes are started. To see the name of the connecting java agents run tests with --loglevel DEBUG. """ stdout = "remote_stdout.txt" stderr = "remote_stderr.txt" logger.info('<a href="%s">Link to stdout</a>' % stdout, html=True) logger.info('<a href="%s">Link to stderr</a>' % stderr, html=True) print("starting process ", command ) REMOTE_AGENTS_LIST.set_received_to_old() with self._agent_java_tool_options(): self.PROCESS.start_process(command, alias=alias, shell=True, stdout=self._output(stdout), stderr=self._output(stderr)) try: self._application_started(alias, timeout=timeout, name_contains=name_contains, accept_old=False) except TimeoutError: raise except Exception: logger.info("Failed to start application: %s" % traceback.format_exc()) # FIXME: this may hang, how is that possible? result = self.PROCESS.wait_for_process(timeout=0.01) if result: logger.info('STDOUT: %s' % result.stdout) logger.info('STDERR: %s' % result.stderr) else: logger.info("Process is running, but application startup failed") raise def connect_to_application(self, alias, timeout=60, name_contains=None): """Connects to application that was started in earlier RobotFramework run. *apport* import option needs to be set to use this keyword. Application that we want to connect into needs to be started by Start Application keyword when using same *apport* value. """ try: self._application_started(alias, timeout=timeout, name_contains=name_contains, accept_old=True) except TimeoutError: raise except Exception: logger.info("Failed to connect to application: %s" % traceback.format_exc()) result = self.PROCESS.wait_for_process(timeout=0.01) if result: logger.info('STDOUT: %s' % result.stdout) logger.info('STDERR: %s' % result.stderr) else: logger.info("Process is running, but application startup failed") raise def _output(self, filename): return os.path.join(self._output_dir, filename) def application_started(self, alias, timeout=60, name_contains=None): """Detects new FormsLibrary Java-agents in applications that are started without using the Start Application -keyword. The given alias is stored to identify the started application in FormsLibrary. Subsequent keywords will be passed on to this application.""" self._application_started(alias, timeout, name_contains, accept_old=True) def _application_started(self, alias, timeout=60, name_contains=None, accept_old=True): self.TIMEOUT = timestr_to_secs(timeout) if (FormsLibrary.APPORT): url = '%s:%s'%(FormsLibrary.APHOST, FormsLibrary.APPORT) else: url = self._get_agent_address(name_contains, accept_old) logger.info('connecting to started application at %s' % url) self._initialize_remote_libraries(alias, url) FormsLibrary.CURRENT = alias logger.debug('modifying robot framework namespace') self.ROBOT_NAMESPACE_BRIDGE.re_import_formslibrary() logger.info('connected to started application at %s' % url) def _initialize_remote_libraries(self, alias, url): swinglibrary = Remote(url) logger.debug('remote swinglibrary instantiated') services = Remote(url + '/services') logger.debug('remote services instantiated') self.REMOTES[alias] = [swinglibrary, services] def _get_agent_address(self, name_pattern, accept_old): while True: if not REMOTE_AGENTS_LIST.get(accept_old): REMOTE_AGENTS_LIST.agent_received.clear() REMOTE_AGENTS_LIST.agent_received.wait(timeout=self.TIMEOUT) if not REMOTE_AGENTS_LIST.agent_received.isSet(): raise FormsLibraryTimeoutError('Agent port not received before timeout') for address, name, age in reversed(REMOTE_AGENTS_LIST.get(accept_old)): if name_pattern is None or name_pattern in name: REMOTE_AGENTS_LIST.remove(address, name, age) return address time.sleep(0.1) def _ping_until_timeout(self, timeout): timeout = float(timeout) delta = min(0.1, timeout) endtime = timeout+time.time() while endtime > time.time(): self._run_from_services('ping') time.sleep(delta) def _run_from_services(self, kw, *args, **kwargs): return self.REMOTES[FormsLibrary.CURRENT][1].run_keyword(kw, args, kwargs) @run_keyword_variant(resolve=1) def ensure_application_should_close(self, timeout, kw, *args): """ Runs the given keyword and waits until timeout for the application to close . If the application doesn't close, the keyword will take a screenshot and close the application and after that it will fail. In many cases calling the keyword that will close the application under test brakes the remote connection. This exception is ignored as it is expected by this keyword. Other exceptions will fail this keyword as expected. """ with self._run_and_ignore_connection_lost(): BuiltIn().run_keyword(kw, *args) try: self._application_should_be_closed(timeout=timestr_to_secs(timeout)) except FormsLibraryTimeoutError as t: logger.warn('Application is not closed before timeout - killing application') self._take_screenshot() self.system_exit() raise def _take_screenshot(self): logdir = self._get_log_dir() filepath = os.path.join(logdir, 'formslibrary-screenshot%s.png' % int(time.time()*1000)) self._run_from_services('takeScreenshot', filepath) logger.info('<img src="%s"></img>' % get_link_path(filepath, logdir), html=True) def _get_log_dir(self): variables = BuiltIn().get_variables() logfile = variables['${LOG FILE}'] if logfile != 'NONE': return os.path.dirname(logfile) return variables['${OUTPUTDIR}'] def _application_should_be_closed(self, timeout): with self._run_and_ignore_connection_lost(): self._ping_until_timeout(timeout) raise FormsLibraryTimeoutError('Application was not closed before timeout') @contextmanager def _run_and_ignore_connection_lost(self): try: yield except RuntimeError as r: # disconnection from remotelibrary if 'Connection to remote server broken:' in r.args[0]: logger.info('Connection died as expected') return raise except HandlerExecutionFailed as e: # disconnection from xmlrpc wrapped in robot keyword if any(elem in e.args[0] for elem in ('Connection to remote server broken:', 'ProtocolError')): logger.info('Connection died as expected') return raise except ProtocolError as r: # disconnection from xmlrpc in jython on some platforms logger.info('Connection died as expected') return def system_exit(self, exit_code=1): """ Uses the FormsLibrary java agent to call system exit for the current java application. """ with self._run_and_ignore_connection_lost(): self._run_from_services('systemExit', exit_code) def switch_to_application(self, alias): """Switches between applications that are known to FormsLibrary. The application is identified using the alias. Subsequent keywords will be passed on to this application.""" FormsLibrary.CURRENT = alias def log_java_system_properties(self): """ log and return java properties and environment information from the current java application. """ env = self._run_from_services('getEnvironment') logger.info(env) return env def get_keyword_names(self): # get_keyword names takes argument `attempts` which makes it # wait 0,1,2,3,4,...,attempts-1 seconds in those attempts # we want to make total wait time to be close to original TIMEOUT # to do it we find minimal n which satisfies ((n)*(n-1))/2 >= TIMEOUT # solution is ceil(sqrt(TIMEOUT*2*4+1)/2+0.5) attempts = int(math.ceil(math.sqrt(FormsLibrary.TIMEOUT*2*4+1)/2+0.5)) overrided_keywords = ['startApplication', 'launchApplication', 'startApplicationInSeparateThread'] if self.current: return FormsLibrary.KEYWORDS + [kw for kw in self.current.get_keyword_names(attempts=attempts) if kw not in overrided_keywords] return FormsLibrary.KEYWORDS + [kw for kw in FormsLibrary_Keywords.keywords if kw not in overrided_keywords] def get_keyword_arguments(self, name): if name in FormsLibrary.KEYWORDS: return self._get_args(name) if self.current: return self.current.get_keyword_arguments(name) return FormsLibrary_Keywords.keyword_arguments[name] def _get_args(self, method_name): spec = inspect.getargspec(getattr(self, method_name)) args = spec[0][1:] if spec[3]: for i, item in enumerate(reversed(spec[3])): args[-i-1] = args[-i-1]+'='+str(item) if spec[1]: args += ['*'+spec[1]] if spec[2]: args += ['**'+spec[2]] return args def get_keyword_documentation(self, name): if name == '__intro__': return FormsLibrary.__doc__ if name in FormsLibrary.KEYWORDS or name == '__init__': return getattr(self, name).__doc__ if self.current: return self.current.get_keyword_documentation(name) return FormsLibrary_Keywords.keyword_documentation[name] def run_keyword(self, name, arguments, kwargs): if unicode(name).startswith(u"capture"): path = self.current.run_keyword(name, arguments, kwargs) defaultLogger.info(path, html=True) logger.info('<a href="%s"><img src="%s" width="800px"></a>' % (path, path), html=True) logger.info("<a href=\"%s\">Screenshot</a> saved." % (path), html=True) return path if name in FormsLibrary.KEYWORDS: return getattr(self, name)(*arguments, **kwargs) if self.current: return self.current.run_keyword(name, arguments, kwargs) if name in FormsLibrary_Keywords.keywords: raise Exception("To use this keyword, you need to connect to the application first.")
def __init__(self): Screenshot.__init__(self,"C:\\WebServer\\Results") XML.__init__(self,use_lxml=True) Process.__init__(self)
class RemoteSwingLibrary(object): """Robot Framework library leveraging Java-agents to run [https://github.com/robotframework/SwingLibrary|SwingLibrary] keywords on Java-processes. To take the library in to use add remoteswinglibrary-[version].jar to PYTHONPATH. The library contains a simple socket server to communicate with Java agents. When taking the library into use, you can specify the port this server uses. Providing the port is optional. If you do not provide one, RemoteSwingLibrary will ask the OS for an unused port. Keywords directly offered by this library on top of SwingLibrary keywords are: - [#Application Started|Application Started] - [#Ensure Application Should Close|Ensure Application Should Close] - [#Log Java System Properties|Log Java System Properties] - [#Reinitiate|Reinitiate] - [#Set Java Tool Options|Set Java Tool Options] - [#Start Application|Start Application] - [#Switch To Application|Switch To Application] - [#System Exit|System Exit] RemoteSwingLibrary also introduces two global variables that can be used during testing: - ${REMOTESWINGLIBRARYPATH} the location of the remoteswinglibrary jar file. - ${REMOTESWINGLIBRARYPORT} port used by the agents to communicate with the library - this is needed if a java agent is started for example from another machine. [https://github.com/ombre42/jrobotremoteserver|jrobotremoteserver] that is used by RemoteSwingLibrary also offers a keyword: - [#Stop Remote Server|Stop Remote Server] Following SwingLibrary Keywords are not available through RemoteSwingLibrary: - Launch Application - SwingLibrary version of Start Application - Start Application In Separate Thread NOTE! [#Get Table Cell Property|Get Table Cell Property] will return the string representation of that property and not the actual object. Complex objects are not passed through Remote library interface. Examples: | * Settings * | | Library | RemoteSwingLibrary | | * Test Cases * | | Testing java application | | | Start Application | myjavaapp | java -jar myjava.jar | | | Select Window | My App | | | Ensure Application Should Close | 15 seconds | Push Button | Exit | """ ROBOT_LIBRARY_SCOPE = 'GLOBAL' KEYWORDS = [ 'system_exit', 'start_application', 'application_started', 'switch_to_application', 'ensure_application_should_close', 'log_java_system_properties', 'set_java_tool_options', 'reinitiate' ] REMOTES = {} CURRENT = None PROCESS = Process() TIMEOUT = 60 PORT = None DEBUG = None AGENT_PATH = os.path.abspath(os.path.dirname(__file__)) _output_dir = '' def __init__(self, port=0, debug=False): """ *port*: optional port for the server receiving connections from remote agents *debug*: optional flag that will start agent in mode with more logging for troubleshooting (set to TRUE to enable) NOTE! RemoteSwingLibrary is a so called Global Scope library. This means when it is imported once it will be available until end of robot run. Parameters used in imports from others suites will be ignored. If you need to change import options between suites, please use *Reinitiate* keyword. """ if RemoteSwingLibrary.DEBUG is None: RemoteSwingLibrary.DEBUG = _tobool(debug) if RemoteSwingLibrary.PORT is None: RemoteSwingLibrary.PORT = self._start_port_server(int(port)) try: BuiltIn().set_global_variable( '\${REMOTESWINGLIBRARYPATH}', self._escape_path(RemoteSwingLibrary.AGENT_PATH)) BuiltIn().set_global_variable('\${REMOTESWINGLIBRARYPORT}', RemoteSwingLibrary.PORT) self._output_dir = BuiltIn().get_variable_value('${OUTPUTDIR}') except RobotNotRunningError: pass def reinitiate(self, port=0, debug=False): """ Restarts RemoteSwingLibrary with new import parameters. """ RemoteSwingLibrary.CURRENT = None global REMOTE_AGENTS_LIST REMOTE_AGENTS_LIST = AgentList() RemoteSwingLibrary.PORT = None RemoteSwingLibrary.DEBUG = None RemoteSwingLibrary.TIMEOUT = 60 self.__init__(port, debug) @property def current(self): if not self.CURRENT: return None return self.REMOTES[self.CURRENT][0] def _start_port_server(self, port): address = ('0.0.0.0', int(port)) server = SocketServer.TCPServer(address, SimpleServer) server.allow_reuse_address = True #t = threading.Thread(name="RemoteSwingLibrary registration server thread", # target=server.serve_forever) t = threading.Thread( name="RemoteSwingLibrary registration server thread", target=server.serve_forever, args=(0.01, )) t.setDaemon(True) t.start() return server.server_address[1] def _create_env(self, close_security_dialogs=False, remote_port=0): agent_command = '-javaagent:"%s"=127.0.0.1:%s' % ( RemoteSwingLibrary.AGENT_PATH, RemoteSwingLibrary.PORT) if int(remote_port): agent_command += ':APPORT=%s' % remote_port if RemoteSwingLibrary.DEBUG: agent_command += ':DEBUG' if _tobool(close_security_dialogs): agent_command += ':CLOSE_SECURITY_DIALOGS' self._agent_command = agent_command logger.info(agent_command) def _escape_path(self, text): return text.replace("\\", "\\\\") @contextmanager def _agent_java_tool_options(self, close_security_dialogs, remote_port): old_tool_options = os.environ.get('JAVA_TOOL_OPTIONS', '') old_options = os.environ.get('_JAVA_OPTIONS', '') logger.debug("Picked old JAVA_TOOL_OPTIONS='%s'" % old_tool_options) logger.debug("Picked old _JAVA_OPTIONS='%s'" % old_options) self.set_java_tool_options(close_security_dialogs, remote_port) try: yield finally: os.environ['JAVA_TOOL_OPTIONS'] = old_tool_options os.environ['_JAVA_OPTIONS'] = old_options logger.debug("Returned old JAVA_TOOL_OPTIONS='%s'" % old_tool_options) logger.debug("Returned old _JAVA_OPTIONS='%s'" % old_options) def set_java_tool_options(self, close_security_dialogs=True, remote_port=0): """Sets the JAVA_TOOL_OPTIONS to include RemoteSwingLibrary Agent and the _JAVA_OPTIONS to set a temporary policy granting all permissions. RemoteSwingLibrary Agent is normally enabled by `Start Application` by setting the JAVA_TOOL_OPTIONS environment variable only during that keyword call. So java processes started by other commands wont normally use the RemoteSwingLibrary Agent. This keyword sets that same environment variable to be used always. So all java processes started after this will use the Agent. This methods also creates temporary Java policy file which grants all permissions. This file is set as policy for each java command call. """ close_security_dialogs = _tobool(close_security_dialogs) self._create_env(close_security_dialogs, remote_port) os.environ['JAVA_TOOL_OPTIONS'] = self._agent_command logger.debug("Set JAVA_TOOL_OPTIONS='%s'" % self._agent_command) with tempfile.NamedTemporaryFile(prefix='grant_all_', suffix='.policy', delete=False) as t: text = b""" grant { permission java.security.AllPermission; }; """ t.write(text) java_policy = '-Djava.security.policy="%s"' % t.name os.environ['_JAVA_OPTIONS'] = java_policy logger.debug("Set _JAVA_OPTIONS='%s'" % java_policy) def start_application(self, alias, command, timeout=60, name_contains="", close_security_dialogs=False, remote_port=0): """Starts the process in the `command` parameter on the host operating system. The given alias is stored to identify the started application in RemoteSwingLibrary. *timeout* (default 60) is timeout in seconds. *name_contains* is a text that must be part of the name of the java process that we are connecting to. *name_contains* helps in situations where multiple java-processes are started. To see the name of the connecting java agents run tests with --loglevel DEBUG. *remote_port* forces RSL agent to run on specific port, this is useful if you want to connect to this application later from another robot run. """ close_security_dialogs = _tobool(close_security_dialogs) stdout = "remote_stdout_" + str(uuid.uuid4()) + '.txt' stderr = "remote_stderr_" + str(uuid.uuid4()) + '.txt' logger.info('<a href="%s">Link to stdout</a>' % stdout, html=True) logger.info('<a href="%s">Link to stderr</a>' % stderr, html=True) REMOTE_AGENTS_LIST.set_received_to_old() with self._agent_java_tool_options(close_security_dialogs, remote_port): self.PROCESS.start_process(command, alias=alias, shell=True, stdout=self._output(stdout), stderr=self._output(stderr)) try: self._application_started(alias, timeout, name_contains, remote_port, accept_old=False) except TimeoutError: raise except Exception: logger.info("Failed to start application: %s" % traceback.format_exc()) # FIXME: this may hang, how is that possible? result = self.PROCESS.wait_for_process(timeout=0.01) if result: logger.info('STDOUT: %s' % result.stdout) logger.info('STDERR: %s' % result.stderr) else: logger.info( "Process is running, but application startup failed") raise def _output(self, filename): return os.path.join(self._output_dir, filename) def application_started(self, alias, timeout=60, name_contains="", remote_port=0, remote_host="127.0.0.1"): """Detects new RemoteSwingLibrary Java-agents in applications that are started without using the Start Application -keyword. The given alias is stored to identify the started application in RemoteSwingLibrary. Subsequent keywords will be passed on to this application. Agents in application started in previous robot runs can't be detected automatically, so you have to use *remote_port* parameter. """ self._application_started(alias, timeout, name_contains, remote_port, remote_host) def _wait_for_api(self, url): logger.info('waiting for api at %s' % url) attempts = int(RemoteSwingLibrary.TIMEOUT) for i in range(attempts): try: result = self._run_from_services('ping') logger.info('api is ready') return result except Exception as err: error = err time.sleep(1) raise RuntimeError('Connecting to api at %s has failed: %s' % (url, error)) def _application_started(self, alias, timeout=60, name_contains="", remote_port=0, remote_host="127.0.0.1", accept_old=True): RemoteSwingLibrary.TIMEOUT = timestr_to_secs(timeout) if remote_port: url = '%s:%s' % (remote_host, remote_port) REMOTE_AGENTS_LIST.remove(url) else: url = self._get_agent_address(name_contains, accept_old) logger.info('connecting to started application at %s' % url) self._initialize_remote_libraries(alias, url) RemoteSwingLibrary.CURRENT = alias self._wait_for_api(url) logger.info('connected to started application at %s' % url) def _initialize_remote_libraries(self, alias, url): swinglibrary = Remote(url) logger.debug('remote swinglibrary instantiated') services = Remote(url + '/services') logger.debug('remote services instantiated') self.REMOTES[alias] = [swinglibrary, services] def _get_agent_address(self, name_pattern, accept_old): while True: if not REMOTE_AGENTS_LIST.get(accept_old): REMOTE_AGENTS_LIST.agent_received.clear() REMOTE_AGENTS_LIST.agent_received.wait( timeout=RemoteSwingLibrary.TIMEOUT) if not REMOTE_AGENTS_LIST.agent_received.isSet(): raise RemoteSwingLibraryTimeoutError( 'Agent port not received before timeout') for address, name, age in reversed( REMOTE_AGENTS_LIST.get(accept_old)): if name_pattern is None or name_pattern in name: REMOTE_AGENTS_LIST.remove(address) return address time.sleep(0.1) def _ping_until_timeout(self, timeout): timeout = float(timeout) delta = min(0.1, timeout) endtime = timeout + time.time() while endtime > time.time(): self._run_from_services('ping') time.sleep(delta) def _run_from_services(self, kw, *args, **kwargs): return self.REMOTES[RemoteSwingLibrary.CURRENT][1].run_keyword( kw, args, kwargs) @run_keyword_variant(resolve=1) def ensure_application_should_close(self, timeout, kw, *args): """ Runs the given keyword and waits until timeout for the application to close . If the application doesn't close, the keyword will take a screenshot and close the application and after that it will fail. In many cases calling the keyword that will close the application under test brakes the remote connection. This exception is ignored as it is expected by this keyword. Other exceptions will fail this keyword as expected. """ with self._run_and_ignore_connection_lost(): BuiltIn().run_keyword(kw, *args) try: self._application_should_be_closed( timeout=timestr_to_secs(timeout)) except RemoteSwingLibraryTimeoutError as t: logger.warn( 'Application is not closed before timeout - killing application' ) self._take_screenshot() self.system_exit() raise def _take_screenshot(self): logdir = self._get_log_dir() filepath = os.path.join( logdir, 'remoteswinglibrary-screenshot%s.png' % int(time.time() * 1000)) self._run_from_services('takeScreenshot', filepath) logger.info('<img src="%s"></img>' % get_link_path(filepath, logdir), html=True) def _get_log_dir(self): variables = BuiltIn().get_variables() logfile = variables['${LOG FILE}'] if logfile != 'NONE': return os.path.dirname(logfile) return variables['${OUTPUTDIR}'] def _application_should_be_closed(self, timeout): with self._run_and_ignore_connection_lost(): self._ping_until_timeout(timeout) raise RemoteSwingLibraryTimeoutError( 'Application was not closed before timeout') @contextmanager def _run_and_ignore_connection_lost(self): try: yield except RuntimeError as r: # disconnection from remotelibrary if 'Connection to remote server broken:' in r.args[0]: logger.info('Connection died as expected') return raise except HandlerExecutionFailed as e: # disconnection from xmlrpc wrapped in robot keyword if any(elem in e.args[0] for elem in ('Connection to remote server broken:', 'ProtocolError')): logger.info('Connection died as expected') return raise except ProtocolError as r: # disconnection from xmlrpc in jython on some platforms logger.info('Connection died as expected') return def system_exit(self, exit_code=1): """ Uses the RemoteSwingLibrary java agent to call system exit for the current java application. """ with self._run_and_ignore_connection_lost(): self._run_from_services('systemExit', exit_code) # try closing the attached process (if started with start_application) to close any open file handles try: self.PROCESS.wait_for_process(handle=RemoteSwingLibrary.CURRENT, timeout=0.01) except: return def switch_to_application(self, alias): """Switches between applications that are known to RemoteSwingLibrary. The application is identified using the alias. Subsequent keywords will be passed on to this application.""" RemoteSwingLibrary.CURRENT = alias def log_java_system_properties(self): """ log and return java properties and environment information from the current java application. """ env = self._run_from_services('getEnvironment') logger.info(env) return env def get_keyword_names(self): overrided_keywords = [ 'startApplication', 'launchApplication', 'startApplicationInSeparateThread' ] return RemoteSwingLibrary.KEYWORDS + [ kw for kw in swinglibrary.keywords if kw not in overrided_keywords ] def get_keyword_arguments(self, name): if name in RemoteSwingLibrary.KEYWORDS: return self._get_args(name) return swinglibrary.keyword_arguments[name] def _get_args(self, method_name): spec = inspect.getargspec(getattr(self, method_name)) args = spec[0][1:] if spec[3]: for i, item in enumerate(reversed(spec[3])): args[-i - 1] = args[-i - 1] + '=' + str(item) if spec[1]: args += ['*' + spec[1]] if spec[2]: args += ['**' + spec[2]] return args def get_keyword_documentation(self, name): if name == '__intro__': return RemoteSwingLibrary.__doc__ if name in RemoteSwingLibrary.KEYWORDS or name == '__init__': return getattr(self, name).__doc__ return swinglibrary.keyword_documentation[name] def run_keyword(self, name, arguments, kwargs): if name in RemoteSwingLibrary.KEYWORDS: return getattr(self, name)(*arguments, **kwargs) if self.current: return self.current.run_keyword(name, arguments, kwargs) if name in swinglibrary.keywords: raise Exception( "To use this keyword you need to connect to application first." )
class RemoteSwingLibrary(object): """RemoteSwingLibrary is a Robot Framework library leveraging Java-agents to run [https://github.com/robotframework/SwingLibrary|SwingLibrary] keywords on Java-processes. To take the library into use add ``remoteswinglibrary-[version].jar`` to ``PYTHONPATH``. The library contains a simple socket server to communicate with Java agents. When taking the library into use, you can specify the port this server uses. Providing the port is optional. If you do not provide one, RemoteSwingLibrary will ask the OS for an unused port. Keywords directly offered by this library on top of SwingLibrary keywords are: - `Application Started` - `Ensure Application Should Close` - `Log Java System Properties` - `Reinitiate` - `Set Java Tool Options` - `Start Application` - `Switch To Application` - `System Exit` RemoteSwingLibrary also introduces two global variables that can be used during testing: - ``${REMOTESWINGLIBRARYPATH}`` the location of the remoteswinglibrary jar file. - ``${REMOTESWINGLIBRARYPORT}`` port used by the agents to communicate with the library - this is needed if a java agent is started for example from another machine. The following SwingLibrary keywords are not available through RemoteSwingLibrary: - `Launch Application` - SwingLibrary version of `Start Application` - `Start Application In Separate Thread` *Note:* `Get Table Cell Property` will return the string representation of that property and not the actual object. Complex objects are not passed through Remote library interface. = Locating components = Most of the keywords that operate on a visible component take an argument named ``identifier``, which is used to locate the element. The first matching element is operated on, according to these rules: - If the ``identifier`` is a number, it is used as a zero-based index for the particular component type in the current context. Using indices is, however, fragile and is strongly discouraged. - If the ``identifier`` matches to internal name of a component (set using ``setName`` method in Java code), that component is chosen. - For components that have visible text (e.g. buttons), ``identifier`` is also matched against that. - Text field keywords also support accessing awt-text fields by prefixing the identifier with ``awt=``. Keyword `List Components in Context` lists all components and their names and indices in a given context. = Regular expressions = More information about Java regular expressions and patterns can be found here: http://java.sun.com/docs/books/tutorial/essential/regex/ and here: http://java.sun.com/javase/7/docs/api/java/util/regex/Pattern.html. = Example = | ***** Settings ***** | Documentation This example demonstrates starting a Java application | ... using RemoteSwingLibrary | | Library `RemoteSwingLibrary` | | ***** Test Cases ***** | Testing Java application | `Start Application` myjavaapp java -jar myjava.jar | `Select Window` My App | `Ensure Application Should Close` 15 seconds Push Button Exit """ ROBOT_LIBRARY_SCOPE = 'GLOBAL' ROBOT_LIBRARY_VERSION = __version__ KEYWORDS = [ 'system_exit', 'start_application', 'application_started', 'switch_to_application', 'ensure_application_should_close', 'log_java_system_properties', 'set_java_tool_options', 'reinitiate' ] REMOTES = {} CURRENT = None PROCESS = Process() TIMEOUT = 60 PORT = None DEBUG = None AGENT_PATH = os.path.abspath(os.path.dirname(__file__)) POLICY_FILE = None _output_dir = '' JAVA9_OR_NEWER = False def _remove_policy_file(self): if self.POLICY_FILE and os.path.isfile(self.POLICY_FILE): os.remove(self.POLICY_FILE) self.POLICY_FILE = None def _java9_or_newer(self): try: version = float(self._read_java_version()) except: logger.warn( 'Failed to auto-detect Java version. Assuming Java is older than 9. To run in newer Java' 'compatibility mode set java9_or_newer=True when importing the library.' ) return False return version >= 1.9 @staticmethod def read_python_path_env(): if 'PYTHONPATH' in os.environ: classpath = os.environ['PYTHONPATH'].split(os.pathsep) for path in classpath: if 'remoteswinglibrary' in path.lower(): return str(path) return None @staticmethod def _read_java_version(): def read_sys_path(): for item in sys.path: if 'remoteswinglibrary' in item.lower( ) and '.jar' in item.lower(): return str(item) return None def construct_classpath(location): classpath = location if 'CLASSPATH' in os.environ: classpath += os.pathsep + os.environ['CLASSPATH'] return classpath location = RemoteSwingLibrary.read_python_path_env() or read_sys_path() if location: os.environ['CLASSPATH'] = construct_classpath(location) p = subprocess.Popen( ['java', 'org.robotframework.remoteswinglibrary.ReadJavaVersion'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=os.environ) version, err = p.communicate() return version def __init__(self, port=0, debug=False, java9_or_newer='auto-detect'): """ ``port``: optional port for the server receiving connections from remote agents ``debug``: optional flag that will start agent in mode with more logging for troubleshooting (set to ``True`` to enable) ``java9_or_newer``: optional flag that by default will try to automatically detect if Java version is greater than 8. If it fails to detect, manually set it to ``True`` or ``False`` accordingly. If the value is different from ``true``, ``1`` or ``yes`` (case insensitive) it will default to ``False``. *Note:* RemoteSwingLibrary is a so called Global Scope library. This means when it is imported once it will be available until end of robot run. Parameters used in imports from others suites will be ignored. If you need to change import options between suites, please use `Reinitiate` keyword. """ self._initiate(port, debug, java9_or_newer) def _initiate(self, port=0, debug=False, java9_or_newer='auto-detect'): if RemoteSwingLibrary.DEBUG is None: RemoteSwingLibrary.DEBUG = _tobool(debug) if RemoteSwingLibrary.PORT is None: RemoteSwingLibrary.PORT = self._start_port_server(int(port)) if java9_or_newer == 'auto-detect': RemoteSwingLibrary.JAVA9_OR_NEWER = _tobool(self._java9_or_newer()) elif _tobool(java9_or_newer): RemoteSwingLibrary.JAVA9_OR_NEWER = True try: if '__pyclasspath__' in RemoteSwingLibrary.AGENT_PATH: RemoteSwingLibrary.AGENT_PATH = RemoteSwingLibrary.read_python_path_env( ) BuiltIn().set_global_variable( '\${REMOTESWINGLIBRARYPATH}', self._escape_path(RemoteSwingLibrary.AGENT_PATH)) BuiltIn().set_global_variable('\${REMOTESWINGLIBRARYPORT}', RemoteSwingLibrary.PORT) self._output_dir = BuiltIn().get_variable_value('${OUTPUTDIR}') except RobotNotRunningError: pass def reinitiate(self, port=0, debug=False, java9_or_newer='auto-detect'): """ Restarts RemoteSwingLibrary with new import parameters. """ RemoteSwingLibrary.CURRENT = None global REMOTE_AGENTS_LIST REMOTE_AGENTS_LIST = AgentList() RemoteSwingLibrary.PORT = None RemoteSwingLibrary.DEBUG = None RemoteSwingLibrary.TIMEOUT = 60 self._remove_policy_file() self._initiate(port, debug, java9_or_newer) @property def current(self): if not self.CURRENT: return None return self.REMOTES[self.CURRENT][0] def _start_port_server(self, port): address = ('0.0.0.0', int(port)) server = SocketServer.TCPServer(address, SimpleServer) server.allow_reuse_address = True # t = threading.Thread(name="RemoteSwingLibrary registration server thread", # target=server.serve_forever) t = threading.Thread( name="RemoteSwingLibrary registration server thread", target=server.serve_forever, args=(0.01, )) t.setDaemon(True) t.start() return server.server_address[1] def _create_env(self, close_security_dialogs=False, remote_port=0, dir_path=None, custom=None): agent_command = '-javaagent:"%s"=127.0.0.1:%s' % ( RemoteSwingLibrary.AGENT_PATH, RemoteSwingLibrary.PORT) if int(remote_port): agent_command += ':APPORT=%s' % remote_port if custom: agent_command += ':CUSTOM=%s' % custom if RemoteSwingLibrary.DEBUG: agent_command += ':DEBUG' if _tobool(close_security_dialogs): agent_command += ':CLOSE_SECURITY_DIALOGS' if is_truthy(dir_path): dir_path = os.path.abspath(dir_path) agent_command += ':DIR_PATH:%s' % dir_path self._agent_command = agent_command logger.info(agent_command) def _escape_path(self, text): return text.replace("\\", "\\\\") @contextmanager def _agent_java_tool_options(self, close_security_dialogs, remote_port, dir_path, custom): old_tool_options = os.environ.get('JAVA_TOOL_OPTIONS', '') old_options = os.environ.get('_JAVA_OPTIONS', '') logger.debug("Picked old JAVA_TOOL_OPTIONS='%s'" % old_tool_options) logger.debug("Picked old _JAVA_OPTIONS='%s'" % old_options) self.set_java_tool_options(close_security_dialogs, remote_port, dir_path, custom) try: yield finally: os.environ['JAVA_TOOL_OPTIONS'] = old_tool_options os.environ['_JAVA_OPTIONS'] = old_options logger.debug("Returned old JAVA_TOOL_OPTIONS='%s'" % old_tool_options) logger.debug("Returned old _JAVA_OPTIONS='%s'" % old_options) def set_java_tool_options(self, close_security_dialogs=True, remote_port=0, dir_path=None, custom=None): """Sets the ``JAVA_TOOL_OPTIONS`` to include RemoteSwingLibrary Agent and the ``_JAVA_OPTIONS`` to set a temporary policy granting all permissions. RemoteSwingLibrary Agent is normally enabled by `Start Application` by setting the ``JAVA_TOOL_OPTIONS`` environment variable only during that keyword call. Java processes started by other commands won't normally use the RemoteSwingLibrary agent. This keyword sets that same environment variable to be always used. So all java processes started after this will use the agent. This method also creates temporary Java policy file which grants all permissions and is set as policy for each java command call. """ close_security_dialogs = _tobool(close_security_dialogs) en_us_locale = "-Duser.language=en -Duser.country=US " self._create_env(close_security_dialogs, remote_port, dir_path, custom) logger.info("Java version > 9: " + str(RemoteSwingLibrary.JAVA9_OR_NEWER)) if RemoteSwingLibrary.JAVA9_OR_NEWER is True: self._agent_command += ' --add-exports=java.desktop/sun.awt=ALL-UNNAMED' os.environ['JAVA_TOOL_OPTIONS'] = en_us_locale + self._agent_command logger.debug("Set JAVA_TOOL_OPTIONS='%s%s'" % (en_us_locale, self._agent_command)) with tempfile.NamedTemporaryFile(prefix='grant_all_', suffix='.policy', delete=False) as t: self.POLICY_FILE = t.name text = b""" grant { permission java.security.AllPermission; }; """ t.write(text) java_policy = '-Djava.security.policy="%s"' % t.name os.environ['_JAVA_OPTIONS'] = java_policy logger.debug("Set _JAVA_OPTIONS='%s'" % java_policy) def start_application(self, alias, command, timeout=60, name_contains="", close_security_dialogs=False, remote_port=0, dir_path=None, stdout=None, stderr=None, custom=None): """Starts the process in the ``command`` parameter on the host operating system. The given ``alias`` is stored to identify the started application in RemoteSwingLibrary. ``timeout`` (default 60) is timeout in seconds. ``name_contains`` is a text that must be part of the name of the java process that we are connecting to. It helps in situations where multiple java-processes are started. To see the name of the connecting java agents run tests with ``--loglevel DEBUG``. ``remote_port`` forces RSL agent to run on specific port, this is useful if you want to connect to this application later from another robot run. ``dir_path`` is the path where security dialogs screenshots are saved. It is working both with relative and absolute path. If ``dir_path`` is not specified the screenshots will not be taken. ``stdout`` is the path where to write stdout to. ``stderr`` is the path where to write stderr to. ``custom`` is a customizable field that can be set when starting the Java agent. This keyword returns the remote port of the connection. """ close_security_dialogs = _tobool(close_security_dialogs) now = datetime.datetime.now() current_date = now.strftime("%Y%m%d%H%M%S") if stdout is None: stdout_file_name = current_date + ".remoteswinglibrary" + ".out" stdout = self._output(stdout_file_name) (stdout_dir, stdout_file_name) = os.path.split(stdout) if not os.path.exists(stdout_dir): os.makedirs(stdout_dir) if stderr is None: stderr_file_name = current_date + ".remoteswinglibrary" + ".err" stderr = self._output(stderr_file_name) if stderr != "STDOUT": (stderr_dir, stderr_file_name) = os.path.split(stderr) if not os.path.exists(stderr_dir): os.makedirs(stderr_dir) logger.info('<a href="%s">Link to stdout</a>' % stdout, html=True) logger.info('<a href="%s">Link to stderr</a>' % stderr, html=True) REMOTE_AGENTS_LIST.set_received_to_old() with self._agent_java_tool_options(close_security_dialogs, remote_port, dir_path, custom): self.PROCESS.start_process(command, alias=alias, shell=True, stdout=stdout, stderr=stderr) try: self._application_started(alias, timeout, name_contains, remote_port, accept_old=False) except TimeoutError: raise except Exception: logger.info("Failed to start application: %s" % traceback.format_exc()) # FIXME: this may hang, how is that possible? result = self.PROCESS.wait_for_process(timeout=0.01) if result: logger.info('STDOUT: %s' % result.stdout) logger.info('STDERR: %s' % result.stderr) else: logger.info( "Process is running, but application startup failed") raise finally: self._remove_policy_file() return RemoteSwingLibrary.PORT def _output(self, filename): return os.path.join(self._output_dir, filename) def application_started(self, alias, timeout=60, name_contains="", remote_port=0, remote_host="127.0.0.1"): """Detects new RemoteSwingLibrary Java-agents in applications that are started without using the `Start Application` keyword. The given ``alias`` is stored to identify the started application in RemoteSwingLibrary. Subsequent keywords will be passed on to this application. Agents in application started in previous robot runs can't be detected automatically, so you have to use ``remote_port`` parameter. """ self._application_started(alias, timeout, name_contains, remote_port, remote_host) def _wait_for_api(self, url): logger.info('waiting for api at %s' % url) attempts = int(RemoteSwingLibrary.TIMEOUT) for i in range(attempts): try: result = self._run_from_services('ping') logger.info('api is ready') return result except Exception as err: error = err time.sleep(1) raise RuntimeError('Connecting to api at %s has failed: %s' % (url, error)) def _application_started(self, alias, timeout=60, name_contains="", remote_port=0, remote_host="127.0.0.1", accept_old=True): RemoteSwingLibrary.TIMEOUT = timestr_to_secs(timeout) if remote_port: url = '%s:%s' % (remote_host, remote_port) REMOTE_AGENTS_LIST.remove(url) else: url = self._get_agent_address(name_contains, accept_old) logger.info('connecting to started application at %s' % url) self._initialize_remote_libraries(alias, url) RemoteSwingLibrary.CURRENT = alias self._wait_for_api(url) logger.info('connected to started application at %s' % url) self._remove_policy_file() def _initialize_remote_libraries(self, alias, url): swinglibrary = Remote(url) logger.debug('remote swinglibrary instantiated') services = Remote(url + '/services') logger.debug('remote services instantiated') self.REMOTES[alias] = [swinglibrary, services] def _get_agent_address(self, name_pattern, accept_old): while True: if not REMOTE_AGENTS_LIST.get(accept_old): REMOTE_AGENTS_LIST.agent_received.clear() REMOTE_AGENTS_LIST.agent_received.wait( timeout=RemoteSwingLibrary.TIMEOUT) if not REMOTE_AGENTS_LIST.agent_received.isSet(): raise RemoteSwingLibraryTimeoutError( 'Agent port not received before timeout') for address, name, age in reversed( REMOTE_AGENTS_LIST.get(accept_old)): if name_pattern is None or name_pattern in name: REMOTE_AGENTS_LIST.remove(address) return address time.sleep(0.1) def _ping_until_timeout(self, timeout): timeout = float(timeout) delta = min(0.5, timeout) endtime = timeout + time.time() while endtime > time.time(): self._run_from_services('ping') time.sleep(delta) def _run_from_services(self, kw, *args, **kwargs): return self.REMOTES[RemoteSwingLibrary.CURRENT][1].run_keyword( kw, args, kwargs) @run_keyword_variant(resolve=1) def ensure_application_should_close(self, timeout, kw, *args): """ Runs the given keyword and waits until timeout for the application to close. If the application doesn't close, the keyword will take a screenshot and close the application and after that it will fail. In many cases calling the keyword that will close the application under test brakes the remote connection. This exception is ignored as it is expected by this keyword. Other exceptions will fail this keyword as expected. """ with self._run_and_ignore_connection_lost(): BuiltIn().run_keyword(kw, *args) try: self._application_should_be_closed( timeout=timestr_to_secs(timeout)) except RemoteSwingLibraryTimeoutError as t: logger.warn( 'Application is not closed before timeout - killing application' ) self._take_screenshot() self.system_exit() raise def _take_screenshot(self): logdir = self.get_log_dir() screenshotdir = logdir + "/" + "remote-screenshots" if not os.path.exists(screenshotdir): os.makedirs(screenshotdir) filepath = os.path.join( screenshotdir, 'remote-screenshot%s.png' % re.sub('[:. ]', '-', str(datetime.datetime.now()))) self._run_from_services('takeScreenshot', filepath) logger.info('<img src="%s"></img>' % get_link_path(filepath, logdir), html=True) @staticmethod def get_log_dir(): variables = BuiltIn().get_variables() logfile = variables['${LOG FILE}'] if logfile != 'NONE': return os.path.dirname(logfile) return variables['${OUTPUTDIR}'] def _application_should_be_closed(self, timeout): with self._run_and_ignore_connection_lost(): self._ping_until_timeout(timeout) raise RemoteSwingLibraryTimeoutError( 'Application was not closed before timeout') @contextmanager def _run_and_ignore_connection_lost(self): try: yield except RuntimeError as r: # disconnection from remotelibrary if 'Connection to remote server broken:' in r.args[0]: logger.info('Connection died as expected') return raise except HandlerExecutionFailed as e: # disconnection from xmlrpc wrapped in robot keyword if any(elem in e.args[0] for elem in ('Connection to remote server broken:', 'ProtocolError')): logger.info('Connection died as expected') return raise except ProtocolError as r: # disconnection from xmlrpc in jython on some platforms logger.info('Connection died as expected') return def system_exit(self, exit_code=1): """ Uses the RemoteSwingLibrary java agent to call system exit for the current java application. """ with self._run_and_ignore_connection_lost(): self._run_from_services('systemExit', exit_code) # try closing the attached process (if started with start_application) to close any open file handles try: self.PROCESS.wait_for_process(handle=RemoteSwingLibrary.CURRENT, timeout=0.01) except: return def switch_to_application(self, alias): """Switches between applications that are known to RemoteSwingLibrary. The application is identified using the ``alias``. Subsequent keywords will be passed on to this application.""" RemoteSwingLibrary.CURRENT = alias def log_java_system_properties(self): """Log and return java properties and environment information from the current java application. """ env = self._run_from_services('getEnvironment') logger.info(env) if IS_PYTHON3 and not is_string(env): return env.decode('utf-8') return env def get_keyword_names(self): overrided_keywords = [ 'startApplication', 'launchApplication', 'startApplicationInSeparateThread' ] return RemoteSwingLibrary.KEYWORDS + [ kw for kw in swinglibrary.keywords if kw not in overrided_keywords ] def get_keyword_arguments(self, name): if name in RemoteSwingLibrary.KEYWORDS: return self._get_args(name) return swinglibrary.keyword_arguments[name] def _get_args(self, method_name): spec = inspect.getargspec(getattr(self, method_name)) args = spec[0][1:] if spec[3]: for i, item in enumerate(reversed(spec[3])): args[-i - 1] = args[-i - 1] + '=' + str(item) if spec[1]: args += ['*' + spec[1]] if spec[2]: args += ['**' + spec[2]] return args def get_keyword_documentation(self, name): if name == '__intro__': return RemoteSwingLibrary.__doc__ if name in RemoteSwingLibrary.KEYWORDS or name == '__init__': return getattr(self, name).__doc__ return swinglibrary.keyword_documentation[name] def run_keyword(self, name, args, kwargs=None): if name in RemoteSwingLibrary.KEYWORDS: return getattr(self, name)(*args, **(kwargs or {})) if self.current: return self.current.run_keyword(name, args, kwargs) if name in swinglibrary.keywords: raise Exception( "To use this keyword you need to connect to application first." )