├── distbot ├── __init__.py ├── AssistantLibrary.py ├── __main__.py └── tasksdb.py ├── scripts ├── distbot └── distbot.bat ├── examples ├── logindemo2.robot ├── demo │ └── logindemo.robot └── base │ ├── getElementByExtJSQuery.js │ ├── Config.robot │ ├── getCaseInsensitiveExtJSQuery.js │ ├── github.robot │ └── ExtJS.robot ├── run.bat ├── docker-compose.yml ├── Assistant └── assistant.robot ├── setup.py ├── Dockerfile ├── .gitignore ├── README.md └── LICENSE /distbot/__init__.py: -------------------------------------------------------------------------------- 1 | # -------------------------------------------------------------------------------- /scripts/distbot: -------------------------------------------------------------------------------- 1 | python -m distbot %* 2 | -------------------------------------------------------------------------------- /scripts/distbot.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | python -m distbot %* -------------------------------------------------------------------------------- /examples/logindemo2.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Suite Setup Setup All 3 | Suite Teardown Close All 4 | Resource ${EXECDIR}/examples/base/github.robot 5 | 6 | *** Test Cases *** 7 | Test github login 8 | Login to github 9 | capture screenshot 10 | 11 | -------------------------------------------------------------------------------- /run.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | setlocal 3 | 4 | rem set Path=%Path%;D:/testing/robot-framework/tools/Python27;D:/testing/robot-framework/tools/Python27/Scripts;D:/testing/robot-framework/tools/drivers 5 | python distbot/distbot.py %* -d report 6 | 7 | :END 8 | endlocal 9 | -------------------------------------------------------------------------------- /examples/demo/logindemo.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Suite Setup Setup All 3 | Suite Teardown Close All 4 | Resource ${EXECDIR}/examples/base/github.robot 5 | 6 | *** Test Cases *** 7 | Test github login 8 | Login to github 9 | capture screenshot 10 | 11 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | distbot-dev-ff: 4 | volumes: 5 | - .:/tests 6 | network_mode: "host" 7 | image: rajeevnaikte/distbot 8 | build: . 9 | command: -e dev -b headlessfirefox --mode distributed --max-cpu-percent 80 --max-memory 1024 examples -v USERNAME:lll -v PASSWORD:xxx 10 | 11 | -------------------------------------------------------------------------------- /Assistant/assistant.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Library AssistantLibrary 3 | Library Process 4 | Suite Setup Setup All 5 | Suite Teardown Close All 6 | Resource ${EXECDIR}/examples/base/github.robot 7 | 8 | *** Tasks *** 9 | interactive 10 | start assistant 11 | 12 | *** Keywords *** 13 | open notpad 14 | Start Process notepad.exe 15 | -------------------------------------------------------------------------------- /examples/base/getElementByExtJSQuery.js: -------------------------------------------------------------------------------- 1 | try { 2 | var compQuery = arguments[0]; 3 | var idFn = arguments[1]; 4 | 5 | var comp = Ext.ComponentQuery.query(compQuery)[0]; 6 | var elId; 7 | if (typeof comp[idFn] === 'function') { 8 | elId = comp[idFn](); 9 | } 10 | else { 11 | elId = comp[idFn]; 12 | } 13 | return window.document.getElementById(elId); 14 | } catch (e) {} -------------------------------------------------------------------------------- /examples/base/Config.robot: -------------------------------------------------------------------------------- 1 | *** Keywords **** 2 | Set up variables 3 | ${URL}= Set Variable If 4 | ... '${ENV}'=='dev' https://github.com/login 5 | ... '${ENV}'=='stage' https://github.com/login 6 | ... '${ENV}'=='preprod' https://github.com/login 7 | ... '${ENV}'=='local' https://localhost/login 8 | Set Suite Variable \${URL} ${URL} 9 | 10 | Connect to database 11 | ${host}= Set Variable If 12 | ... '${ENV}'=='dev' dev.mysql.com 13 | ... '${ENV}'=='local' local.mysql.com 14 | ... '${ENV}'=='stage' stage.mysql.com 15 | #Connect to Database pymysql tempdb mysql mysql ${host} 1433 16 | -------------------------------------------------------------------------------- /examples/base/getCaseInsensitiveExtJSQuery.js: -------------------------------------------------------------------------------- 1 | try { 2 | var compQuery = arguments[0]; 3 | compQuery = arguments[0].replace(/\[([^\[\]~^]*)=([^\[\]"]*)\]/g, function (a, attr, value) { 4 | var valRegex = ''; 5 | for (var i = 0; i < value.length; i++) { 6 | var code = value.charCodeAt(i); 7 | var char = value.charAt(i); 8 | if (code > 64 && code < 91) { 9 | valRegex += '[' + char + char.toLowerCase() + ']'; 10 | } 11 | else if (code > 96 && code < 123) { 12 | valRegex += '[' + char.toUpperCase() + char + ']'; 13 | } 14 | else { 15 | valRegex += '[' + char + ']'; 16 | } 17 | } 18 | return '[' + attr + '/="' + valRegex + '"]'; 19 | }); 20 | 21 | return compQuery; 22 | } catch (e) {} -------------------------------------------------------------------------------- /distbot/AssistantLibrary.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import robot.running 3 | from robot.output import librarylogger as log 4 | from robot.libraries.BuiltIn import BuiltIn 5 | 6 | class AssistantLibrary: 7 | def start_assistant(self): 8 | log.console('\nHello, how may i help you today?\n') 9 | while True: 10 | keyword = raw_input().strip() 11 | if keyword == '': 12 | continue 13 | if keyword == 'bye': 14 | break 15 | if keyword.lower() == 'start assistant': 16 | log.console('Already running') 17 | continue 18 | try: 19 | ret = BuiltIn().run_keyword(*keyword.split(' ')) 20 | log.console(ret) 21 | except Exception as e: 22 | log.console(e) 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /examples/base/github.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Library SeleniumLibrary 15 3 | Library DatabaseLibrary 4 | Library Collections 5 | Library OperatingSystem 6 | Resource Config.robot 7 | 8 | *** Keywords **** 9 | Setup All 10 | Connect to database 11 | Set up variables 12 | 13 | Close All 14 | #Disconnect From Database 15 | Close All Browsers 16 | 17 | Get data from sql script file 18 | [Arguments] ${scriptFile} 19 | ${getScript}= Get Binary File ${scriptFile} 20 | ${results}= Query ${getScript} 21 | [Return] ${results} 22 | 23 | Update data with sql script file 24 | [Arguments] ${scriptFile} 25 | ${getScript}= Get Binary File ${scriptFile} 26 | Execute SQL string ${getScript} 27 | 28 | Login to github 29 | Login to github with user ${USERNAME} and password ${PASSWORD} 30 | 31 | Login to github with user ${username} and password ${password} 32 | Open Browser ${URL} ${BROWSER} ${SUITE NAME} 33 | Maximize Browser Window 34 | Wait Until Page Contains Element login_field 35 | Input Text login_field ${username} 36 | Input Password password ${password} 37 | Click Element commit 38 | Wait Until Page Contains Element //img[@alt='@${username}'] 39 | 40 | capture screenshot 41 | Capture Page Screenshot ${TEST NAME}-{index}.png 42 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from distutils.core import setup 4 | import os 5 | from setuptools import find_packages 6 | 7 | name = 'Rajeev Naik' 8 | address = name.lower().replace(' ', '')+'te'+chr(64)+'gmail.com' 9 | 10 | setup(name='robotframework-distbot', 11 | version='1.0', 12 | description='Distributed test executor for Robot Framework', 13 | long_description='With Distbot you can distribute tests into multiple machines/docker and save test execution time.', 14 | author=name, 15 | author_email=address, 16 | url='https://github.com/rajeevnaikte/distbot', 17 | download_url='https://pypi.python.org/pypi/robotframework-distbot', 18 | packages=find_packages(), 19 | classifiers=[ 20 | 'Intended Audience :: Developers', 21 | 'Natural Language :: English', 22 | 'Programming Language :: Python :: 2.7', 23 | 'Programming Language :: Python :: 3', 24 | 'Topic :: Software Development :: Testing', 25 | 'License :: OSI Approved :: Apache Software License', 26 | 'Development Status :: 5 - Production/Stable', 27 | 'Framework :: Robot Framework' 28 | ], 29 | license='Apache License, Version 2.0', 30 | scripts=[os.path.join('scripts', 'distbot'), 31 | os.path.join('scripts', 'distbot.bat')], 32 | install_requires=['robotframework', 'psutil']) 33 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu 2 | 3 | RUN apt-get update -y 4 | RUN apt-get install -y --no-install-recommends firefox 5 | RUN apt-get install -y --no-install-recommends python3 6 | RUN apt-get install -y --no-install-recommends build-essential python3-dev 7 | RUN apt-get install -y --no-install-recommends python3-setuptools 8 | RUN apt-get install -y --no-install-recommends python3-pip 9 | RUN apt-get install -y --no-install-recommends wget 10 | RUN rm -rf /var/lib/apt/lists/* /var/cache/apt/* 11 | 12 | RUN python3 --version 13 | 14 | ARG GECKODRIVER_VERSION=v0.30.0 15 | RUN apt-get install ca-certificates \ 16 | && wget --no-verbose --no-check-certificate -O /tmp/geckodriver.tar.gz https://github.com/mozilla/geckodriver/releases/download/$GECKODRIVER_VERSION/geckodriver-$GECKODRIVER_VERSION-linux64.tar.gz \ 17 | && rm -rf /opt/geckodriver \ 18 | && tar -C /opt -zxf /tmp/geckodriver.tar.gz \ 19 | && rm /tmp/geckodriver.tar.gz \ 20 | && mv /opt/geckodriver /opt/geckodriver-$GECKODRIVER_VERSION \ 21 | && chmod 755 /opt/geckodriver-$GECKODRIVER_VERSION \ 22 | && ln -fs /opt/geckodriver-$GECKODRIVER_VERSION /usr/bin/geckodriver 23 | 24 | RUN python3 -m pip install psutil 25 | RUN python3 -m pip install robotframework 26 | RUN python3 -m pip install robotframework-distbot 27 | RUN python3 -m pip install robotframework-seleniumlibrary 28 | RUN python3 -m pip install robotframework-databaselibrary 29 | RUN python3 -m pip install PyMySQL 30 | RUN python3 -m pip install pymssql 31 | RUN python3 -m pip install postgres 32 | 33 | RUN apt-get remove -y build-essential python3-dev python3-setuptools python3-pip wget 34 | RUN rm -rf /var/lib/apt/lists/* /var/cache/apt/* 35 | 36 | WORKDIR /tests 37 | ENTRYPOINT ["python3", "-m", "distbot"] 38 | 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | report/ 107 | drivers/ 108 | .idea/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # distbot 2 | Distributed/parallel execution of Robot Framework (RF) test suites using multiple docker containers 3 | # Demo 4 | https://www.youtube.com/watch?v=KL4V0QkMMVc 5 | # How it works 6 | Start multiple docker containers of rajeevnaikte/distbot (e.g. ```docker-compose up --scale=4```), with all pointing to a shared volume folder (please refer docker-compose volumes for configurations), where the robot framework test suites are stored, and set argument ```--mode distributed``` (e.g. in docker-compose.yml file). Each robot suite/file will be run in a separate process/container. This program will automatically distribute the suites among the docker containers and run in parallel (without need of a master node). Also, in each container multiple suites can be run in parallel as per the --max-* configurations mentioned below. Once all suites are completed, one of the container will execute ```rebot``` of robot framework to combine all the reports. The report will be stored in same shared volume folder (You can also specify different location using --outputdir argument of robot frameowrk). 7 | # Usage 8 | ```pip install robotframework-distbot```
9 | Run the docker container or ```python -m distbot``` with -h argument to see all options of the progam.
10 | ``` 11 | usage: -e ENV [options] main_suite [robot arguments] 12 | 13 | Explanation: first give options from below list as per your need (these are specific to this library), 14 | next give main suite name (folder name), next give RF arguments as per your needs (these are RF specific as mentioned in thier guide). 15 | If there is a conflicting options between this library and RF, then you can use full name for that option in RF. 16 | E.g. -e is used by this library for ENV, but RF as well has it for exclude option. So you can use --exclude for RF. 17 | i.e. -e ENV my_suite --exclude something 18 | 19 | positional arguments: 20 | main_suite Folder name containing all the robot framework scripts. 21 | 22 | optional arguments: 23 | -h, --help show this help message and exit 24 | -e E dev, stage, prod etc. This value will be available as variable ENV. 25 | -b B This value will be available as variable BROWSER. 26 | --mode {sequential,distributed} 27 | --max-cpu-percent MAX_CPU_PERCENT 28 | Program will stop spawning new process when cpu usage reaches this value. 29 | --max-memory MAX_MEMORY 30 | Program will stop spawning new process when memory usage reaches this value. 31 | --max-processes MAX_PROCESSES 32 | Program will stop spawning new process when running processes count is this value. 33 | -d OUTPUTDIR, --outputdir OUTPUTDIR 34 | Directory to save report files. Default is workingdir/report 35 | -s SUITE, --suite SUITE 36 | Only run suites matching this value 37 | ``` 38 | # Assistant mode 39 | There is an assistant library here to use during development. This program will keep the robot framework running, and allow you to type in the keywords from terminal/cmd prompt. It is a good tool for web-developers while coding to see the effect while trying out differnet logics.
40 | Starting assistant is simply using keywork 'start assistant'. e.g. in this repo there is file Assistant/assistant.robot. Just run that -
41 | ```python -m distbot -e local -b ff Assistant``` 42 | -------------------------------------------------------------------------------- /distbot/__main__.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import os 3 | import time 4 | import psutil 5 | import robot 6 | from multiprocessing import Process, Value, Lock 7 | from robot.running.builder import TestSuiteBuilder 8 | from .tasksdb import TasksDB 9 | 10 | class DistRoboRunner(object): 11 | def __init__(self, args, robotArgs): 12 | self.args = args 13 | self.robotArgs = robotArgs 14 | if self.args.outputdir==None: 15 | self.args.outputdir='report' 16 | self.robotArgs += ['-d', self.args.outputdir] 17 | 18 | def sequential(self): 19 | if self.args.suite: 20 | self.robotArgs += ['-s', self.args.suite] 21 | self.robotArgs += [self.args.main_suite] 22 | print(self.robotArgs) 23 | robot.run_cli(self.robotArgs) 24 | 25 | def distributed(self): 26 | tasks = TasksDB(self.args.outputdir) 27 | if not tasks.isTasksLoaded(): 28 | newTasks = { 29 | 'sequential': [ 30 | { 31 | 'parallel': self.getAllSuites() 32 | }, 33 | 'rebot' 34 | ] 35 | } 36 | tasks.loadTasks(newTasks) 37 | tasks.printTableData() 38 | procCount = Value('i', 0) 39 | lock = Lock() 40 | while True: 41 | if self.canStartProcess(procCount.value): 42 | task = tasks.getTaskToWorkOn() 43 | if task: 44 | p = Process(target=self.runTask, args=(task,procCount,lock)) 45 | p.start() 46 | time.sleep(2) 47 | else: 48 | break 49 | else: 50 | time.sleep(5) 51 | 52 | def canStartProcess(self, procsCount): 53 | canStart = True 54 | allNone = True 55 | if self.args.max_processes: 56 | print('Running processes count:' + str(procsCount)) 57 | canStart = canStart and (self.args.max_processes > procsCount) 58 | allNone = False 59 | if self.args.max_cpu_percent: 60 | cpuPerc = psutil.cpu_percent() 61 | print('Used CPU (%):' + str(cpuPerc)) 62 | canStart = canStart and (self.args.max_cpu_percent > cpuPerc) 63 | allNone = False 64 | if self.args.max_memory: 65 | memMB = psutil.virtual_memory().used/1024/1024 66 | print('Used memory (MB):' + str(memMB)) 67 | canStart = canStart and (self.args.max_memory > memMB) 68 | allNone = False 69 | if allNone: 70 | canStart = canStart and (5 > procsCount) 71 | return canStart 72 | 73 | def runTask(self, rowid, procsCount, lock): 74 | tasks = TasksDB(self.args.outputdir) 75 | suite = tasks.getTask(rowid) 76 | print(str(os.getpid()) + '|Running suite ' + suite + ' .....') 77 | with lock: 78 | procsCount.value += 1 79 | if suite == 'rebot': 80 | runArgs = ['-R','-d',self.args.outputdir,self.args.outputdir+'/*.xml'] 81 | robot.rebot_cli(runArgs, exit=False) 82 | else: 83 | runArgs = self.robotArgs + ['-l','NONE','-r','NONE','-s',suite,'-o',suite+'.xml',self.args.main_suite] 84 | robot.run_cli(runArgs, exit=False) 85 | tasks.finishTask(rowid) 86 | tasks.printTableData() 87 | with lock: 88 | procsCount.value -= 1 89 | 90 | def getAllSuites(self): 91 | root = TestSuiteBuilder(included_suites=([] if self.args.suite==None else self.args.suite.split(','))).build(self.args.main_suite) 92 | return self.traverseTestDataDir(root, []) 93 | 94 | def traverseTestDataDir(self, suite, suites): 95 | if suite.suites: 96 | for childSuite in suite.suites._items: 97 | self.traverseTestDataDir(childSuite, suites) 98 | else: 99 | suites += [suite.longname] 100 | return suites 101 | 102 | def main(): 103 | parser = argparse.ArgumentParser(usage='-e ENV [options] main_suite [robot arguments]', add_help=True) 104 | parser.add_argument('-e', required=True, help='dev, stage, prod etc. This value will be available as variable ENV.') 105 | parser.add_argument('-b', required=False, help='This value will be available as variable BROWSER.') 106 | parser.add_argument('--mode', required=False, choices=['sequential', 'distributed'], default='sequential') 107 | parser.add_argument('--max-cpu-percent', required=False, type=float, help='Program will stop spawning new process when cpu usage reaches this value.') 108 | parser.add_argument('--max-memory', required=False, type=int, help='Program will stop spawning new process when memory usage reaches this value.') 109 | parser.add_argument('--max-processes', required=False, type=int, help='Program will stop spawning new process when running processes count is this value.') 110 | parser.add_argument('main_suite', help='Folder name containing all the robot framework scripts.') 111 | parser.add_argument('-d', '--outputdir', required=False, help='Directory to save report files. Default is /report') 112 | parser.add_argument('-s', '--suite', required=False, help='Only run suites matching this value.') 113 | args, robotArgs = parser.parse_known_args() 114 | 115 | robotArgs += ['-v', 'ENV:'+args.e] 116 | if args.b: 117 | robotArgs += ['-v', 'BROWSER:'+args.b] 118 | 119 | getattr(DistRoboRunner(args, robotArgs), args.mode, lambda: "Unkown run mode!")() 120 | 121 | if __name__ == '__main__': 122 | main() 123 | -------------------------------------------------------------------------------- /distbot/tasksdb.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sqlite3 3 | import json 4 | import time 5 | import uuid 6 | import logging 7 | 8 | class TasksDB(object): 9 | def __init__(self, directory): 10 | if not os.path.exists(directory): 11 | os.makedirs(directory) 12 | self.dbPath = directory + '/tasks.db' 13 | self.conn = sqlite3.connect(self.dbPath) 14 | with self.conn: 15 | self.conn.execute(''' 16 | Create table if not exists Tasks ( 17 | task text not null, 18 | status integer default 0, 19 | worker text, 20 | groupid integer not null 21 | ); 22 | ''') 23 | self.conn.execute(''' 24 | Create table if not exists TaskGroups ( 25 | groupid integer not null, 26 | parentGroupId integer not null, 27 | status integer default 0 28 | ); 29 | ''') 30 | self.conn.execute(''' 31 | Create table if not exists TasksLoadHistory ( 32 | commands text not null, 33 | worker text not null, 34 | status integer default 0, 35 | loadDateTime timestamp default current_timestamp, 36 | unique (commands, status) on conflict replace 37 | ); 38 | ''') 39 | 40 | def __del__(self): 41 | self.conn.close() 42 | 43 | def isTasksLoaded(self): 44 | rows = self.conn.execute('select task from Tasks where status is not 2 limit 1;').fetchall() 45 | if len(rows) == 0: 46 | rows = self.conn.execute("select * from TasksLoadHistory where loadDateTime > datetime('now', '-30 seconds')").fetchall() 47 | if len(rows) == 0: 48 | return False 49 | return True 50 | 51 | def loadTasks(self, tasks): 52 | uId = uuid.uuid1().hex 53 | with self.conn: 54 | try: 55 | self.conn.execute('insert into TasksLoadHistory(commands,worker) values (?,?)', (json.dumps(tasks),uId)) 56 | except: 57 | time.sleep(1) 58 | return 59 | time.sleep(0.5) 60 | if len(self.conn.execute('select * from TasksLoadHistory where worker=?', (uId,)).fetchall())==0: 61 | attempts = 0 62 | while len(self.conn.execute('select 1 from Tasks t where t.status=0 limit 1;').fetchall())==0: 63 | time.sleep(0.5) 64 | if attempts > 5: 65 | break 66 | attempts = attempts + 1 67 | return 68 | with self.conn: 69 | self.conn.execute('delete from Tasks') 70 | self.conn.execute('delete from TaskGroups') 71 | self.parseTasks(tasks, 0, 0) 72 | 73 | def parseTasks(self, tasks, taskGroup, parentGroup): 74 | groupTasks = None 75 | nextGroup = taskGroup + 1 76 | getTaskGroup = None 77 | if 'sequential' in tasks: 78 | groupTasks = tasks['sequential'] 79 | nextGroup = taskGroup + len(groupTasks) + 1 80 | getTaskGroup = lambda group: group+1 81 | elif 'parallel' in tasks: 82 | groupTasks = tasks['parallel'] 83 | getTaskGroup = lambda group: group 84 | for task in groupTasks: 85 | taskGroup = getTaskGroup(taskGroup) 86 | self.conn.execute('''insert into TaskGroups(groupId,parentGroupId) 87 | select ?,? 88 | where not exists(select 1 from TaskGroups 89 | where groupid=? and parentGroupId=?);''', (taskGroup,parentGroup,taskGroup,parentGroup)) 90 | if isinstance(task, str): 91 | self.conn.execute('insert into Tasks(task,groupid) values (?,?);', (task,taskGroup)) 92 | else: 93 | nextGroup = self.parseTasks(task, nextGroup, taskGroup) 94 | 95 | return nextGroup 96 | 97 | def getTaskToWorkOn(self): 98 | uId = uuid.uuid1().hex 99 | rowid = None 100 | while len(self.conn.execute('select 1 from Tasks t where t.status=0 limit 1;').fetchall()) > 0: 101 | rows = self.conn.execute('''select t.rowid from Tasks t 102 | where t.status=0 and t.groupid in ( 103 | select tg.groupid from TaskGroups tg 104 | where not exists ( 105 | select 1 from TaskGroups tg2 106 | where tg2.groupid < tg.groupid and tg2.parentgroupid=tg.parentgroupid 107 | and tg2.status is not 2 108 | ) 109 | ) limit 1;''').fetchall() 110 | if len(rows) == 0: 111 | time.sleep(5) 112 | else: 113 | with self.conn: 114 | self.conn.execute('''update Tasks 115 | set status=1,worker=? 116 | where rowid=?;''', (uId, rows[0][0],)) 117 | time.sleep(0.5) 118 | rows = self.conn.execute('select rowid from Tasks where status=1 and worker=?;', (uId,)).fetchall() 119 | if len(rows) > 0: 120 | rowid = rows[0][0] 121 | break 122 | return rowid 123 | 124 | def getTask(self, rowid): 125 | return self.conn.execute('select task from Tasks where rowid=?', (rowid,)).fetchall()[0][0] 126 | 127 | def finishTask(self, task): 128 | with self.conn: 129 | self.conn.execute('update Tasks set status=2 where rowid=?;', (task,)) 130 | groupId = self.conn.execute('select groupid from Tasks where rowid=?;', (task,)).fetchall()[0][0] 131 | rows = self.conn.execute('select 1 from Tasks where groupid=? and status is not 2', (groupId,)).fetchall() 132 | if len(rows) == 0: 133 | with self.conn: 134 | self.conn.execute('''update TaskGroups set status=2 135 | where groupid=?;''', (groupId,)) 136 | with self.conn: 137 | self.conn.execute('''update TaskGroups set status=2 138 | where not exists( 139 | select 1 from TaskGroups tg2 140 | where tg2.parentGroupId=groupId and tg2.status is not 2 141 | );''') 142 | with self.conn: 143 | self.conn.execute('''update TasksLoadHistory set status=2 144 | where not exists( 145 | select 1 from Tasks t where t.status is not 2 146 | ) 147 | ''') 148 | 149 | def printTableData(self): 150 | print(str(os.getpid())+'|'+str(self.conn.execute('select * from TaskGroups').fetchall())) 151 | print(str(os.getpid())+'|'+str(self.conn.execute('select * from Tasks').fetchall())) 152 | return 153 | -------------------------------------------------------------------------------- /examples/base/ExtJS.robot: -------------------------------------------------------------------------------- 1 | *** Keywords **** 2 | select ${item} in ${label} dropdown 3 | select ${item} in combobox[title=${label}] in current window 4 | 5 | Click ${label} button 6 | Click element button[title=${label}],button[text=${label}] in current window 7 | 8 | Input ${text} into ${label} textbox 9 | Input ${text} into element textfield[title=${label}] in current window 10 | 11 | Input ${text} into textbox in fieldset ${label} 12 | Input ${text} into element fieldset[title=${label}] textfield in current window 13 | 14 | Get ${label} value 15 | ${value}= Get value of displayfield[fieldLabel=${label}] 16 | [Return] ${value} 17 | 18 | select ${item} in ${label} dropdown in ${panel name} panel 19 | Wait Until Page Does Not Contain Element extJs=window[title=${window name}] panel[itemId=${panel name}][loadMask],panel[title=${panel name}][loadMask] 20 | select ${item} in panel[itemId=${panel name}],panel[title=${panel name}] combobox[title=${label}] in current window 21 | 22 | Click ${label} button in ${panel name} panel 23 | Wait Until Page Does Not Contain Element extJs=window[title=${window name}] panel[itemId=${panel name}][loadMask],panel[title=${panel name}][loadMask] 24 | Click element panel[itemId=${panel name}],panel[title=${panel name}] button[title=${label}],button[text=${label}] in current window 25 | Wait Until Page Does Not Contain Element extJs=window[title=${window name}][isLoading=true] 26 | 27 | Input ${text} into ${label} textbox in ${panel name} panel 28 | Wait Until Page Does Not Contain Element extJs=window[title=${window name}] panel[itemId=${panel name}][loadMask],panel[title=${panel name}][loadMask] 29 | Input ${text} into element panel[itemId=${panel name}],panel[title=${panel name}] textfield[title=${label}] in current window 30 | 31 | Get ${label} value in ${panel name} panel 32 | Wait Until Page Does Not Contain Element extJs=window[title=${window name}] panel[itemId=${panel name}][loadMask],panel[title=${panel name}][loadMask] 33 | ${value}= Get value of panel[itemId=${panel name}],panel[title=${panel name}] displayfield[fieldLabel=${label}] 34 | [Return] ${value} 35 | 36 | window must have message with text ${text} 37 | Element label should contain text ${text} in current window 38 | 39 | panel ${panel name} must have message with text ${text} 40 | Wait Until Page Does Not Contain Element extJs=window[title=${window name}] panel[itemId=${panel name}][loadMask],panel[title=${panel name}][loadMask] 41 | Element panel[itemId=${panel name}],panel[title=${panel name}] label should contain text ${text} in current window 42 | 43 | Element ${ext query} should contain text ${text} in current window 44 | wait until page contains Element extJs=window[title=${window name}] ${ext query} 45 | Capture Page Screenshot ${TEST NAME}-{index}.png 46 | ${message}= get text extJs=window[title=${window name}] ${ext query} 47 | Should Contain ${message} ${text} 48 | 49 | Set up extjs 50 | Add Location Strategy extJs get web element from extJs query 51 | Add Location Strategy extJsInput get input web element from extJs query 52 | ${jsscript}= Get Binary File ${EXECDIR}/examples/base/getElementByExtJSQuery.js 53 | Set Suite Variable ${getElementByExtJSQuery} ${jsscript} 54 | ${jsscript}= Get Binary File ${EXECDIR}/examples/base/getCaseInsensitiveExtJSQuery.js 55 | Set Suite Variable ${getCaseInsensitiveExtJSQuery} ${jsscript} 56 | 57 | get web element from extjs query 58 | [Arguments] ${browser} ${locator} ${tag} ${constraints} 59 | ${web element}= get web element using extJs component query ${locator} getId 60 | [Return] ${web element} 61 | 62 | get input web element from extjs query 63 | [Arguments] ${browser} ${locator} ${tag} ${constraints} 64 | ${web element}= get web element using extJs component query ${locator} inputId 65 | [Return] ${web element} 66 | 67 | get web element using extJs component query 68 | [Arguments] ${comp query} ${id function} 69 | ${comp query}= Execute Javascript ${getCaseInsensitiveExtJSQuery} ARGUMENTS ${comp query} 70 | log to console Searching ${comp query} 71 | ${web element}= Execute Javascript ${getElementByExtJSQuery} ARGUMENTS ${comp query} ${id function} 72 | [Return] ${web element} 73 | 74 | Check ${window name} window is open 75 | Wait Until Element Is Visible extJs=window[title=${window name}] 76 | Wait Until Page Does Not Contain Element extJs=window[title=${window name}][isLoading=true] 77 | Set Suite Variable \${window name} ${window name} 78 | Double Click Element extJs=window[title=${window name}] title 79 | 80 | Close current window 81 | Click Element extJs=window[title=${window name}] tool[tooltip='Close dialog'] 82 | 83 | Switch to ${new window name} window 84 | Sleep 500 millisecond waiting for default focus 85 | ${extjs query}= Execute Javascript ${getCaseInsensitiveExtJSQuery} ARGUMENTS window[title=${new window name}] 86 | Execute Javascript Ext.ComponentQuery.query('${extjs query}')[0].show() 87 | Set Suite Variable ${window name} ${new window name} 88 | 89 | Click element ${extjs query} in current window 90 | ${extjs query}= wait for component ${extjs query} in current window 91 | Click Element extJs=${extjs query} 92 | Sleep 500 millisecond waiting for loading start 93 | Wait Until Page Does Not Contain Element extJs=window[title=${window name}][isLoading=true] 94 | 95 | Input ${text} into element ${extjs query} in current window 96 | Wait Until Page Contains Element extJsInput=window[title=${window name}] ${extjs query} 97 | Wait Until Element Is Visible extJsInput=window[title=${window name}] ${extjs query} 98 | wait Until Page Does Not Contain Element extJs=window[title=${window name}] ${extjs query + '[loadMask]'} 99 | Input Text extJsInput=window[title=${window name}] ${extjs query} ${text} 100 | 101 | Search ${text} in combobox ${extjs query} 102 | Wait Until Page Contains Element extJsInput=${extjs query} 103 | Wait Until Element Is Visible extJsInput=${extjs query} 104 | Sleep 500 millisecond waiting for default focus 105 | ${elementId}= Execute Javascript return Ext.ComponentQuery.query('${extjs query}')[0].id; 106 | Input Text extJsInput=${extjs query} ${text} 107 | Wait Until Element Is Visible extJs=boundlist[id=${elementId}-picker] 108 | Wait Until Element Is Not Visible extJs=loadmask 109 | Execute Javascript var combo=Ext.getCmp('${elementId}-picker');combo.select(combo.getStore().getAt(0)); 110 | 111 | select ${text} in ${extjs query} in current window 112 | ${extjs query}= wait for component ${extjs query} in current window 113 | ${extjs query}= Execute Javascript ${getCaseInsensitiveExtJSQuery} ARGUMENTS ${extjs query} 114 | Execute Javascript var combo=Ext.ComponentQuery.query('${extjs query}')[0];combo.select(combo.getStore().byText.get('${text}')); 115 | 116 | Get value of ${extjs query} 117 | ${extjs query}= wait for component ${extjs query} in current window 118 | ${extjs query}= Execute Javascript ${getCaseInsensitiveExtJSQuery} ARGUMENTS ${extjs query} 119 | ${value}= Execute Javascript return Ext.ComponentQuery.query('${extjs query}')[0].value; 120 | [Return] ${value} 121 | 122 | wait for component ${extjs query} in current window 123 | ${extjs query}= Catenate window[title=${window name}] ${extjs query} 124 | Wait Until Page Contains Element extJs=${extjs query} 125 | Wait Until Element Is Visible extJs=${extjs query} 126 | wait Until Page Does Not Contain Element extJs=${extjs query + '[loadMask]'} 127 | [Return] ${extjs query} 128 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------