├── tests ├── __init__.py ├── step.robot └── test_debuglibrary.py ├── _config.yml ├── DebugLibrary ├── version.py ├── robotvar.py ├── __init__.py ├── globals.py ├── robotapp.py ├── robotselenium.py ├── styles.py ├── webdriver.py ├── shell.py ├── robotlib.py ├── keywords.py ├── steplistener.py ├── cmdcompleter.py ├── sourcelines.py ├── robotkeyword.py ├── prompttoolkitcmd.py └── debugcmd.py ├── MANIFEST.in ├── setup.cfg ├── .gitignore ├── .coveragerc ├── .github └── workflows │ ├── pythonpublish.yml │ ├── codeql.yml │ └── test.yml ├── LICENSE ├── setup.py ├── ChangeLog └── README.rst /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /DebugLibrary/version.py: -------------------------------------------------------------------------------- 1 | VERSION = '2.5.0' 2 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include ChangeLog 2 | include LICENSE 3 | include README.rst 4 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | 3 | [metadata] 4 | license_file = LICENSE 5 | 6 | [aliases] 7 | release = egg_info -b "" register sdist bdist_wheel upload 8 | 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info 2 | build 3 | dist 4 | *.pyc 5 | *.txt 6 | *.robot 7 | log.html 8 | output.xml 9 | report.html 10 | .venv 11 | TODO 12 | .eggs/ 13 | .vscode 14 | .DS_Store 15 | *.log 16 | .coverage 17 | .coverage.* 18 | htmlcov 19 | coverage.xml 20 | coverage.json 21 | -------------------------------------------------------------------------------- /tests/step.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Library DebugLibrary 3 | 4 | ** test cases ** 5 | test1 6 | debug 7 | log to console working 8 | @{list} = Create List hello world 9 | 10 | test2 11 | log to console another test case 12 | log to console end 13 | -------------------------------------------------------------------------------- /DebugLibrary/robotvar.py: -------------------------------------------------------------------------------- 1 | def assign_variable(robot_instance, variable_name, args): 2 | """Assign a robotframework variable.""" 3 | variable_value = robot_instance.run_keyword(*args) 4 | robot_instance._variables.__setitem__(variable_name, variable_value) 5 | return variable_value 6 | -------------------------------------------------------------------------------- /DebugLibrary/__init__.py: -------------------------------------------------------------------------------- 1 | from .keywords import DebugKeywords 2 | from .version import VERSION 3 | 4 | """A debug library and REPL for RobotFramework.""" 5 | 6 | 7 | class DebugLibrary(DebugKeywords): 8 | """Debug Library for RobotFramework.""" 9 | 10 | ROBOT_LIBRARY_SCOPE = 'GLOBAL' 11 | ROBOT_LIBRARY_VERSION = VERSION 12 | -------------------------------------------------------------------------------- /DebugLibrary/globals.py: -------------------------------------------------------------------------------- 1 | class SingletonContext: 2 | in_step_mode = False 3 | current_runner = None 4 | current_runner_step = None 5 | current_source_path = '' 6 | current_source_lineno = 0 7 | last_command = '' 8 | 9 | def __new__(cls): 10 | if not hasattr(cls, 'instance'): 11 | cls.instance = super(SingletonContext, cls).__new__(cls) 12 | return cls.instance 13 | 14 | 15 | context = SingletonContext() 16 | -------------------------------------------------------------------------------- /DebugLibrary/robotapp.py: -------------------------------------------------------------------------------- 1 | 2 | from robot.api import logger 3 | from robot.libraries.BuiltIn import BuiltIn 4 | from robot.running.signalhandler import STOP_SIGNAL_MONITOR 5 | 6 | 7 | def get_robot_instance(): 8 | """Get robotframework builtin instance as context.""" 9 | return BuiltIn() 10 | 11 | 12 | def reset_robotframework_exception(): 13 | """Resume RF after press ctrl+c during keyword running.""" 14 | if STOP_SIGNAL_MONITOR._signal_count: 15 | STOP_SIGNAL_MONITOR._signal_count = 0 16 | STOP_SIGNAL_MONITOR._running_keyword = True 17 | logger.info('Reset last exception of DebugLibrary') 18 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | source = DebugLibrary/ 4 | 5 | [report] 6 | # Regexes for lines to exclude from consideration 7 | exclude_lines = 8 | # Have to re-enable the standard pragma 9 | pragma: no cover 10 | 11 | # Don't complain about missing debug-only code: 12 | def __repr__ 13 | if self\.debug 14 | 15 | # Don't complain if tests don't hit defensive assertion code: 16 | raise AssertionError 17 | raise NotImplementedError 18 | 19 | # Don't complain if non-runnable code isn't run: 20 | if 0: 21 | if __name__ == .__main__.: 22 | 23 | #ignore_errors = True 24 | 25 | #[html] 26 | #directory = cov_html/ 27 | -------------------------------------------------------------------------------- /DebugLibrary/robotselenium.py: -------------------------------------------------------------------------------- 1 | 2 | from .robotkeyword import parse_keyword 3 | 4 | SELENIUM_WEBDRIVERS = ['firefox', 'chrome', 'ie', 5 | 'opera', 'safari', 'phantomjs', 'remote'] 6 | 7 | 8 | def start_selenium_commands(arg): 9 | """Start a selenium webdriver and open url in browser you expect. 10 | 11 | arg: [ or google] [ or firefox] 12 | """ 13 | yield 'import library SeleniumLibrary' 14 | 15 | # Set defaults, overriden if args set 16 | url = 'http://www.google.com/' 17 | browser = 'firefox' 18 | if arg: 19 | args = parse_keyword(arg) 20 | if len(args) == 2: 21 | url, browser = args 22 | else: 23 | url = arg 24 | if '://' not in url: 25 | url = 'http://' + url 26 | 27 | yield 'open browser %s %s' % (url, browser) 28 | -------------------------------------------------------------------------------- /.github/workflows/pythonpublish.yml: -------------------------------------------------------------------------------- 1 | # This workflows will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | name: Upload Python Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | deploy: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: Set up Python 18 | uses: actions/setup-python@v5 19 | with: 20 | python-version: '3.x' 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install setuptools wheel twine 25 | - name: Build and publish 26 | env: 27 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 28 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 29 | run: | 30 | python setup.py sdist bdist_wheel 31 | twine upload dist/* 32 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | schedule: 9 | - cron: "35 13 * * 2" 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | language: [ python ] 24 | 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v4 28 | 29 | - name: Initialize CodeQL 30 | uses: github/codeql-action/init@v2 31 | with: 32 | languages: ${{ matrix.language }} 33 | queries: +security-and-quality 34 | 35 | - name: Autobuild 36 | uses: github/codeql-action/autobuild@v2 37 | 38 | - name: Perform CodeQL Analysis 39 | uses: github/codeql-action/analyze@v2 40 | with: 41 | category: "/language:${{ matrix.language }}" 42 | -------------------------------------------------------------------------------- /DebugLibrary/styles.py: -------------------------------------------------------------------------------- 1 | from prompt_toolkit import print_formatted_text 2 | from prompt_toolkit.formatted_text import FormattedText 3 | from prompt_toolkit.styles import Style 4 | 5 | NORMAL_STYLE = Style.from_dict({ 6 | 'head': 'fg:green', 7 | 'message': 'fg:silver', 8 | }) 9 | 10 | ERROR_STYLE = Style.from_dict({ 11 | 'head': 'fg:red', 12 | 'message': 'fg:white', 13 | }) 14 | 15 | DEBUG_PROMPT_STYLE = Style.from_dict({ 16 | 'prompt': 'blue', 17 | }) 18 | 19 | 20 | def print_output(head, message, style=NORMAL_STYLE): 21 | """Print prompt-toolkit tokens to output.""" 22 | tokens = FormattedText([ 23 | ('class:head', '{0} '.format(head)), 24 | ('class:message', message), 25 | ('', ''), 26 | ]) 27 | print_formatted_text(tokens, style=style) 28 | 29 | 30 | def print_error(head, message, style=ERROR_STYLE): 31 | """Print to output with error style.""" 32 | print_output(head, message, style=style) 33 | 34 | 35 | def get_debug_prompt_tokens(prompt_text): 36 | """Print prompt-toolkit prompt.""" 37 | return [ 38 | ('class:prompt', prompt_text), 39 | ] 40 | -------------------------------------------------------------------------------- /DebugLibrary/webdriver.py: -------------------------------------------------------------------------------- 1 | from robot.api import logger 2 | 3 | from .robotapp import get_robot_instance 4 | 5 | 6 | def get_remote_url(): 7 | """Get selenium URL for connecting to remote WebDriver.""" 8 | se = get_robot_instance().get_library_instance('Selenium2Library') 9 | url = se._current_browser().command_executor._url 10 | 11 | return url 12 | 13 | 14 | def get_session_id(): 15 | """Get selenium browser session id.""" 16 | se = get_robot_instance().get_library_instance('Selenium2Library') 17 | job_id = se._current_browser().session_id 18 | 19 | return job_id 20 | 21 | 22 | def get_webdriver_remote(): 23 | """Print the way connecting to remote selenium server.""" 24 | remote_url = get_remote_url() 25 | session_id = get_session_id() 26 | 27 | code = 'from selenium import webdriver;' \ 28 | 'd=webdriver.Remote(command_executor="%s",' \ 29 | 'desired_capabilities={});' \ 30 | 'd.session_id="%s"' % ( 31 | remote_url, 32 | session_id, 33 | ) 34 | 35 | logger.console(''' 36 | DEBUG FROM CONSOLE 37 | # geckodriver user please check https://stackoverflow.com/a/37968826/150841 38 | %s 39 | ''' % (code)) 40 | logger.info(code) 41 | 42 | return code 43 | -------------------------------------------------------------------------------- /DebugLibrary/shell.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import tempfile 4 | 5 | from robot import run_cli 6 | 7 | TEST_SUITE = b'''*** Settings *** 8 | Library DebugLibrary 9 | 10 | ** test cases ** 11 | RFDEBUG REPL 12 | debug 13 | ''' 14 | 15 | 16 | def shell(): 17 | """A standalone robotframework shell.""" 18 | 19 | default_no_logs = '-l None -x None -o None -L None -r None' 20 | 21 | with tempfile.NamedTemporaryFile(prefix='robot-debug-', 22 | suffix='.robot', 23 | delete=False) as test_file: 24 | test_file.write(TEST_SUITE) 25 | test_file.flush() 26 | 27 | if len(sys.argv) > 1: 28 | args = sys.argv[1:] + [test_file.name] 29 | else: 30 | args = default_no_logs.split() + [test_file.name] 31 | 32 | try: 33 | sys.exit(run_cli(args)) 34 | finally: 35 | test_file.close() 36 | # pybot will raise PermissionError on Windows NT or later 37 | # if NamedTemporaryFile called with `delete=True`, 38 | # deleting test file seperated will be OK. 39 | if os.path.exists(test_file.name): 40 | os.unlink(test_file.name) 41 | 42 | 43 | if __name__ == "__main__": 44 | # Usage: python -m DebugLibrary.shell 45 | shell() 46 | -------------------------------------------------------------------------------- /DebugLibrary/robotlib.py: -------------------------------------------------------------------------------- 1 | from robot.libdocpkg.model import LibraryDoc 2 | from robot.libdocpkg.robotbuilder import KeywordDocBuilder, LibraryDocBuilder 3 | from robot.libraries import STDLIBS 4 | from robot.running.namespace import IMPORTER 5 | 6 | 7 | def get_builtin_libs(): 8 | """Get robotframework builtin library names.""" 9 | return list(STDLIBS) 10 | 11 | 12 | def get_libs(): 13 | """Get imported robotframework library names.""" 14 | return sorted(IMPORTER._library_cache._items, key=lambda _: _.name) 15 | 16 | 17 | def get_libs_dict(): 18 | """Get imported robotframework libraries as a name -> lib dict""" 19 | return {lib.name: lib for lib in IMPORTER._library_cache._items} 20 | 21 | 22 | def match_libs(name=''): 23 | """Find libraries by prefix of library name, default all""" 24 | libs = [_.name for _ in get_libs()] 25 | matched = [_ for _ in libs if _.lower().startswith(name.lower())] 26 | return matched 27 | 28 | 29 | class ImportedLibraryDocBuilder(LibraryDocBuilder): 30 | 31 | def build(self, lib): 32 | libdoc = LibraryDoc( 33 | name=lib.name, 34 | doc=self._get_doc(lib), 35 | doc_format=lib.doc_format, 36 | ) 37 | libdoc.inits = self._get_initializers(lib) 38 | libdoc.keywords = KeywordDocBuilder().build_keywords(lib) 39 | return libdoc 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010, Xie Yanbo 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | * Neither the name of the RobotDriver nor the 15 | names of its contributors may be used to endorse or promote products 16 | derived from this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL Xie Yanbo BE LIABLE FOR ANY 22 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 25 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /DebugLibrary/keywords.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from robot.libraries.BuiltIn import run_keyword_variant 4 | 5 | from .debugcmd import DebugCmd 6 | from .robotkeyword import run_debug_if 7 | from .steplistener import RobotLibraryStepListenerMixin, is_step_mode 8 | from .styles import print_output 9 | from .webdriver import get_remote_url, get_session_id, get_webdriver_remote 10 | 11 | 12 | class DebugKeywords(RobotLibraryStepListenerMixin): 13 | """Debug Keywords for RobotFramework.""" 14 | 15 | def debug(self): 16 | """Open a interactive shell, run any RobotFramework keywords. 17 | 18 | Keywords separated by two space or one tab, and Ctrl-D to exit. 19 | """ 20 | # re-wire stdout so that we can use the cmd module and have readline 21 | # support 22 | old_stdout = sys.stdout 23 | sys.stdout = sys.__stdout__ 24 | 25 | show_intro = not is_step_mode() 26 | if show_intro: 27 | print_output('\n>>>>>', 'Enter interactive shell') 28 | 29 | self.debug_cmd = DebugCmd() 30 | if show_intro: 31 | self.debug_cmd.cmdloop() 32 | else: 33 | self.debug_cmd.cmdloop(intro='') 34 | 35 | show_intro = not is_step_mode() 36 | if show_intro: 37 | print_output('\n>>>>>', 'Exit shell.') 38 | 39 | # put stdout back where it was 40 | sys.stdout = old_stdout 41 | 42 | @run_keyword_variant(resolve=1) 43 | def debug_if(self, condition, *args): 44 | """Runs the Debug keyword if condition is true.""" 45 | return run_debug_if(condition, *args) 46 | 47 | def get_remote_url(self): 48 | """Get selenium URL for connecting to remote WebDriver.""" 49 | return get_remote_url() 50 | 51 | def get_session_id(self): 52 | """Get selenium browser session id.""" 53 | return get_session_id() 54 | 55 | def get_webdriver_remote(self): 56 | """Print the way connecting to remote selenium server.""" 57 | return get_webdriver_remote() 58 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: test 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | name: py-${{ matrix.python-version }} rf-${{ matrix.robotframework-version }} ${{ matrix.platform }} 15 | runs-on: ${{ matrix.platform }} 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | python-version: [3.8, 3.9, "3.10", 3.11, 3.12] 20 | robotframework-version: [4.1, 5.0, 6.1, 7.0] 21 | platform: [ubuntu-latest, macOS-latest] 22 | steps: 23 | - uses: actions/checkout@v4 24 | - name: Set up Python ${{ matrix.python-version }} 25 | uses: actions/setup-python@v5 26 | with: 27 | python-version: ${{ matrix.python-version }} 28 | - name: Install dependencies 29 | run: | 30 | python -m pip install --upgrade pip 31 | python -m pip install setuptools 32 | pip install flake8 33 | pip install coverage 34 | pip install robotframework==${{ matrix.robotframework-version }} 35 | - name: Lint with flake8 36 | run: | 37 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 38 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=80 --statistics 39 | - name: Test 40 | run: | 41 | python setup.py develop 42 | python setup.py test 43 | - name: Generate coverage report 44 | run: | 45 | coverage xml 46 | - name: Send coverage report to codecov 47 | uses: codecov/codecov-action@v3 48 | with: 49 | file: ./coverage.xml 50 | - name: Send coverage report to codeclimate 51 | uses: paambaati/codeclimate-action@v5 52 | with: 53 | coverageCommand: echo "Ignore rerun" 54 | coverageLocations: ${{github.workspace}}/coverage.xml:coverage.py 55 | env: 56 | CC_TEST_REPORTER_ID: ${{secrets.CC_TEST_REPORTER_ID}} 57 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import io 4 | import os 5 | import re 6 | 7 | from setuptools import setup 8 | 9 | ROOT = os.path.abspath(os.path.dirname(__file__)) 10 | 11 | 12 | def read(*names, **kwargs): 13 | with io.open( 14 | os.path.join(ROOT, *names), 15 | encoding=kwargs.get("encoding", "utf8") 16 | ) as fp: 17 | return fp.read() 18 | 19 | 20 | def find_version(*file_paths): 21 | version_file = read(*file_paths) 22 | version_match = re.search(r"^VERSION = ['\"]([^'\"]*)['\"]", 23 | version_file, re.M) 24 | if version_match: 25 | return version_match.group(1) 26 | raise RuntimeError("Unable to find version string.") 27 | 28 | 29 | setup( 30 | name='robotframework-debuglibrary', 31 | version=find_version('DebugLibrary/version.py'), 32 | description='RobotFramework debug library and an interactive shell', 33 | long_description=read('README.rst'), 34 | author='Xie Yanbo', 35 | author_email='xieyanbo@gmail.com', 36 | license='New BSD', 37 | packages=['DebugLibrary'], 38 | entry_points={ 39 | 'console_scripts': [ 40 | 'rfshell = DebugLibrary.shell:shell', 41 | 'rfdebug = DebugLibrary.shell:shell', 42 | ], 43 | }, 44 | zip_safe=False, 45 | url='https://github.com/xyb/robotframework-debuglibrary/', 46 | keywords='robotframework,debug,shell,repl', 47 | install_requires=[ 48 | 'prompt-toolkit >= 3', 49 | 'robotframework >= 4', 50 | ], 51 | tests_require=['pexpect', 'coverage'], 52 | test_suite='tests.test_debuglibrary.suite', 53 | platforms=['Linux', 'Unix', 'Windows', 'MacOS X'], 54 | classifiers=[ 55 | 'Environment :: Console', 56 | 'Development Status :: 5 - Production/Stable', 57 | 'License :: OSI Approved :: BSD License', 58 | 'Natural Language :: English', 59 | 'Operating System :: OS Independent', 60 | 'Programming Language :: Python', 61 | 'Programming Language :: Python :: 3 :: Only', 62 | 'Programming Language :: Python :: 3.8', 63 | 'Programming Language :: Python :: 3.9', 64 | 'Programming Language :: Python :: 3.10', 65 | 'Programming Language :: Python :: 3.11', 66 | 'Programming Language :: Python :: 3.12', 67 | 'Topic :: Utilities', 68 | ], 69 | ) 70 | -------------------------------------------------------------------------------- /DebugLibrary/steplistener.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | 3 | from .globals import context 4 | 5 | 6 | class RobotLibraryStepListenerMixin: 7 | ROBOT_LISTENER_API_VERSION = 2 8 | 9 | def __init__(self): 10 | super(RobotLibraryStepListenerMixin, self).__init__() 11 | self.ROBOT_LIBRARY_LISTENER = [self] 12 | 13 | def _start_keyword(self, name, attrs): 14 | context.current_source_path = '' 15 | context.current_source_lineno = 0 16 | 17 | if not is_step_mode(): 18 | return 19 | 20 | find_runner_step() 21 | step = context.current_runner_step 22 | 23 | path = '' 24 | if hasattr(step, 'lineno'): 25 | path = step.source 26 | lineno = step.lineno 27 | elif 'lineno' in attrs: 28 | path = attrs['source'] 29 | lineno = attrs['lineno'] 30 | 31 | if path: 32 | lineno_0_based = lineno - 1 33 | context.current_source_path = path 34 | context.current_source_lineno = lineno 35 | print('> {}({})'.format(path, lineno)) 36 | line = (open(path).readlines()[lineno_0_based].strip()) 37 | print('-> {}'.format(line)) 38 | 39 | if attrs['assign']: 40 | assign = '%s = ' % ', '.join(attrs['assign']) 41 | else: 42 | assign = '' 43 | name = '{}.{}'.format(attrs['libname'], attrs['kwname']) 44 | 45 | translated = '{}{} {}'.format(assign, name, ' '.join(attrs['args'])) 46 | print('=> {}'.format(translated)) 47 | 48 | # callback debug interface 49 | self.debug() 50 | 51 | start_keyword = _start_keyword 52 | 53 | 54 | # Hack to find the current runner Step to get the source path and line number. 55 | # This method relies on the internal implementation logic of RF and may need 56 | # to be modified when there are major changes to RF. 57 | def find_runner_step(): 58 | stack = inspect.stack() 59 | for frame in stack: 60 | if (frame.function == 'run_steps' # RobotFramework < 4.0 61 | or frame.function == 'run'): # RobotFramework >= 4.0 62 | arginfo = inspect.getargvalues(frame.frame) 63 | context.current_runner = arginfo.locals.get('runner') 64 | context.current_runner_step = arginfo.locals.get('step') 65 | if context.current_runner_step: 66 | break 67 | 68 | 69 | def set_step_mode(on=True): 70 | context.in_step_mode = on 71 | 72 | 73 | def is_step_mode(): 74 | return context.in_step_mode 75 | -------------------------------------------------------------------------------- /DebugLibrary/cmdcompleter.py: -------------------------------------------------------------------------------- 1 | from prompt_toolkit.completion import Completer, Completion 2 | 3 | from .robotkeyword import parse_keyword 4 | 5 | 6 | class CmdCompleter(Completer): 7 | """Completer for debug shell.""" 8 | 9 | def __init__(self, commands, cmd_repl=None): 10 | self.names = [] 11 | self.displays = {} 12 | self.display_metas = {} 13 | for name, display, display_meta in commands: 14 | self.names.append(name) 15 | self.displays[name] = display 16 | self.display_metas[name] = display_meta 17 | self.cmd_repl = cmd_repl 18 | 19 | def _get_argument_completions(self, completer, document): 20 | """Using Cmd.py's completer to complete arguments.""" 21 | end_idx = document.cursor_position_col 22 | line = document.current_line 23 | if line[:end_idx].rfind(' ') >= 0: 24 | begin_idx = line[:end_idx].rfind(' ') + 1 25 | else: 26 | begin_idx = 0 27 | prefix = line[begin_idx:end_idx] 28 | 29 | completions = completer(prefix, line, begin_idx, end_idx) 30 | for comp in completions: 31 | yield Completion(comp, begin_idx - end_idx, display=comp) 32 | 33 | def _get_custom_completions(self, cmd_name, document): 34 | completer = getattr( 35 | self.cmd_repl, 36 | 'complete_{0}'.format(cmd_name), 37 | None, 38 | ) 39 | if completer: 40 | yield from self._get_argument_completions(completer, document) 41 | 42 | def _get_command_completions(self, text): 43 | return (Completion(name, 44 | -len(text), 45 | display=self.displays.get(name, ''), 46 | display_meta=self.display_metas.get(name, ''), 47 | ) 48 | for name in self.names 49 | if ((('.' not in name and '.' not in text) # root level 50 | or ('.' in name and '.' in text)) # library level 51 | and name.lower().strip().startswith(text.strip())) 52 | ) 53 | 54 | def get_completions(self, document, complete_event): 55 | """Compute suggestions.""" 56 | text = document.text_before_cursor.lower() 57 | parts = parse_keyword(text) 58 | 59 | if len(parts) >= 2: 60 | cmd_name = parts[0].strip() 61 | yield from self._get_custom_completions(cmd_name, document) 62 | else: 63 | yield from self._get_command_completions(text) 64 | -------------------------------------------------------------------------------- /DebugLibrary/sourcelines.py: -------------------------------------------------------------------------------- 1 | from robot.version import get_version 2 | 3 | ROBOT_VERION_RUNNER_GET_STEP_LINENO = '3.2' 4 | 5 | 6 | class RobotNeedUpgrade(Exception): 7 | """Need upgrade robotframework.""" 8 | 9 | 10 | def check_version(): 11 | if get_version() < ROBOT_VERION_RUNNER_GET_STEP_LINENO: 12 | raise RobotNeedUpgrade 13 | 14 | 15 | def print_source_lines(source_file, lineno, before_and_after=5): 16 | check_version() 17 | 18 | if not source_file or not lineno: 19 | return 20 | 21 | with open(source_file) as f: 22 | lines = f.readlines() 23 | 24 | start_index = max(1, lineno - before_and_after - 1) 25 | end_index = min(len(lines) + 1, lineno + before_and_after) 26 | _print_lines(lines, start_index, end_index, lineno) 27 | 28 | 29 | def print_test_case_lines(source_file, current_lineno): 30 | check_version() 31 | 32 | if not source_file or not current_lineno: 33 | return 34 | 35 | with open(source_file) as f: 36 | lines = f.readlines() 37 | 38 | # find the first line of current test case 39 | start_index = _find_first_lineno(lines, current_lineno) 40 | # find the last line of current test case 41 | end_index = _find_last_lineno(lines, current_lineno) 42 | 43 | _print_lines(lines, start_index, end_index, current_lineno) 44 | 45 | 46 | def _find_last_lineno(lines, begin_lineno): 47 | line_index = begin_lineno - 1 48 | while line_index < len(lines): 49 | line = lines[line_index] 50 | if not _inside_test_case_block(line): 51 | break 52 | line_index += 1 53 | return line_index 54 | 55 | 56 | def _find_first_lineno(lines, begin_lineno): 57 | line_index = begin_lineno - 1 58 | while line_index >= 0: 59 | line_index -= 1 60 | line = lines[line_index] 61 | if not _inside_test_case_block(line): 62 | break 63 | return line_index 64 | 65 | 66 | def _inside_test_case_block(line): 67 | if line.startswith(' '): 68 | return True 69 | if line.startswith('\t'): 70 | return True 71 | if line.startswith('#'): 72 | return True 73 | return False 74 | 75 | 76 | def _print_lines(lines, start_index, end_index, current_lineno): 77 | display_lines = lines[start_index:end_index] 78 | for lineno, line in enumerate(display_lines, start_index + 1): 79 | current_line_sign = '' 80 | if lineno == current_lineno: 81 | current_line_sign = '->' 82 | print('{:>3} {:2}\t{}'.format(lineno, 83 | current_line_sign, 84 | line.rstrip())) 85 | -------------------------------------------------------------------------------- /DebugLibrary/robotkeyword.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from robot.libraries.BuiltIn import BuiltIn 4 | 5 | from .robotlib import ImportedLibraryDocBuilder, get_libs 6 | from .robotvar import assign_variable 7 | 8 | try: 9 | from robot.variables.search import is_variable 10 | except ImportError: 11 | from robot.variables import is_var as is_variable # robotframework < 3.2 12 | 13 | KEYWORD_SEP = re.compile(' +|\t') 14 | 15 | _lib_keywords_cache = {} 16 | 17 | 18 | def parse_keyword(command): 19 | """Split a robotframework keyword string.""" 20 | # TODO use robotframework functions 21 | return KEYWORD_SEP.split(command) 22 | 23 | 24 | def get_lib_keywords(library): 25 | """Get keywords of imported library.""" 26 | if library.name in _lib_keywords_cache: 27 | return _lib_keywords_cache[library.name] 28 | 29 | lib = ImportedLibraryDocBuilder().build(library) 30 | keywords = [] 31 | for keyword in lib.keywords: 32 | keywords.append({ 33 | 'name': keyword.name, 34 | 'lib': library.name, 35 | 'doc': keyword.doc, 36 | 'summary': keyword.doc.split('\n')[0], 37 | }) 38 | 39 | _lib_keywords_cache[library.name] = keywords 40 | return keywords 41 | 42 | 43 | def get_keywords(): 44 | """Get all keywords of libraries.""" 45 | for lib in get_libs(): 46 | yield from get_lib_keywords(lib) 47 | 48 | 49 | def find_keyword(keyword_name): 50 | keyword_name = keyword_name.lower() 51 | return [keyword 52 | for lib in get_libs() 53 | for keyword in get_lib_keywords(lib) 54 | if keyword['name'].lower() == keyword_name] 55 | 56 | 57 | def _execute_variable(robot_instance, variable_name, keyword, args): 58 | variable_only = not args 59 | if variable_only: 60 | display_value = ['Log to console', keyword] 61 | robot_instance.run_keyword(*display_value) 62 | return None 63 | else: 64 | variable_value = assign_variable( 65 | robot_instance, 66 | variable_name, 67 | args, 68 | ) 69 | echo = '{0} = {1!r}'.format(variable_name, variable_value) 70 | return ('#', echo) 71 | 72 | 73 | def run_keyword(robot_instance, keyword): 74 | """Run a keyword in robotframewrk environment.""" 75 | if not keyword: 76 | return None 77 | 78 | keyword_args = parse_keyword(keyword) 79 | keyword = keyword_args[0] 80 | args = keyword_args[1:] 81 | 82 | is_comment = keyword.strip().startswith('#') 83 | if is_comment: 84 | return None 85 | 86 | variable_name = keyword.rstrip('= ') 87 | if is_variable(variable_name): 88 | return _execute_variable(robot_instance, variable_name, keyword, args) 89 | else: 90 | output = robot_instance.run_keyword(keyword, *args) 91 | if output: 92 | return ('<', repr(output)) 93 | else: 94 | return ('', '') 95 | 96 | 97 | def run_debug_if(condition, *args): 98 | """Runs DEBUG if condition is true.""" 99 | 100 | return BuiltIn().run_keyword_if(condition, 101 | 'DebugLibrary.DEBUG', 102 | *args) 103 | -------------------------------------------------------------------------------- /DebugLibrary/prompttoolkitcmd.py: -------------------------------------------------------------------------------- 1 | import cmd 2 | import os 3 | 4 | from prompt_toolkit.auto_suggest import AutoSuggestFromHistory 5 | from prompt_toolkit.history import FileHistory 6 | from prompt_toolkit.shortcuts import CompleteStyle, prompt 7 | 8 | 9 | class BaseCmd(cmd.Cmd): 10 | """Basic REPL tool.""" 11 | prompt = '> ' 12 | repeat_last_nonempty_command = False 13 | 14 | def emptyline(self): 15 | """Do not repeat the last command if input empty unless forced to.""" 16 | if self.repeat_last_nonempty_command: 17 | return super(BaseCmd, self).emptyline() 18 | return None 19 | 20 | def do_exit(self, arg): 21 | """Exit the interpreter. You can also use the Ctrl-D shortcut.""" 22 | 23 | return True 24 | 25 | do_EOF = do_exit 26 | 27 | def help_help(self): 28 | """Help of Help command""" 29 | 30 | print('Show help message.') 31 | 32 | def do_pdb(self, arg): 33 | """Enter the python debuger pdb. For development only.""" 34 | print('break into python debugger: pdb') 35 | import pdb 36 | pdb.set_trace() 37 | 38 | def get_cmd_names(self): 39 | """Get all command names of CMD shell.""" 40 | pre = 'do_' 41 | cut = len(pre) 42 | return [_[cut:] for _ in self.get_names() if _.startswith(pre)] 43 | 44 | def get_help_string(self, command_name): 45 | """Get help document of command.""" 46 | func = getattr(self, 'do_{0}'.format(command_name), None) 47 | if not func: 48 | return '' 49 | return func.__doc__ 50 | 51 | def get_helps(self): 52 | """Get all help documents of commands.""" 53 | return [(name, self.get_help_string(name) or name) 54 | for name in self.get_cmd_names()] 55 | 56 | def get_completer(self): 57 | """Get completer instance.""" 58 | 59 | def pre_loop_iter(self): 60 | """Excute before every loop iteration.""" 61 | 62 | def _get_input(self): 63 | if self.cmdqueue: 64 | return self.cmdqueue.pop(0) 65 | else: 66 | try: 67 | return self.get_input() 68 | except KeyboardInterrupt: 69 | return None 70 | 71 | def loop_once(self): 72 | self.pre_loop_iter() 73 | line = self._get_input() 74 | if line is None: 75 | return None 76 | 77 | if line == 'exit': 78 | line = 'EOF' 79 | 80 | line = self.precmd(line) 81 | if line == 'EOF': 82 | # do not run 'EOF' command to avoid override 'lastcmd' 83 | stop = True 84 | else: 85 | stop = self.onecmd(line) 86 | stop = self.postcmd(stop, line) 87 | return stop 88 | 89 | def cmdloop(self, intro=None): 90 | """Better command loop. 91 | 92 | override default cmdloop method 93 | """ 94 | if intro is not None: 95 | self.intro = intro 96 | if self.intro: 97 | self.stdout.write(self.intro) 98 | self.stdout.write('\n') 99 | 100 | self.preloop() 101 | 102 | stop = None 103 | while not stop: 104 | stop = self.loop_once() 105 | 106 | self.postloop() 107 | 108 | def get_input(self): 109 | return input(prompt=self.prompt) 110 | 111 | 112 | class PromptToolkitCmd(BaseCmd): 113 | """CMD shell using prompt-toolkit.""" 114 | 115 | get_prompt_tokens = None 116 | prompt_style = None 117 | intro = '''\ 118 | Only accepted plain text format keyword separated with two or more spaces. 119 | Type "help" for more information.\ 120 | ''' 121 | 122 | def __init__(self, completekey='tab', stdin=None, stdout=None, 123 | history_path=''): 124 | BaseCmd.__init__(self, completekey, stdin, stdout) 125 | self.history = FileHistory(os.path.expanduser(history_path)) 126 | 127 | def get_input(self): 128 | kwargs = dict( 129 | history=self.history, 130 | auto_suggest=AutoSuggestFromHistory(), 131 | enable_history_search=True, 132 | completer=self.get_completer(), 133 | complete_style=CompleteStyle.MULTI_COLUMN, 134 | ) 135 | if self.get_prompt_tokens: 136 | kwargs['style'] = self.prompt_style 137 | prompt_str = self.get_prompt_tokens(self.prompt) 138 | else: 139 | prompt_str = self.prompt 140 | try: 141 | line = prompt(message=prompt_str, **kwargs) 142 | except EOFError: 143 | line = 'EOF' 144 | return line 145 | -------------------------------------------------------------------------------- /tests/test_debuglibrary.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import unittest 4 | 5 | import pexpect 6 | from robot.version import get_version 7 | 8 | TIMEOUT_SECONDS = 2 9 | 10 | child = None 11 | 12 | 13 | def check_result(pattern): 14 | index = child.expect([pattern, pexpect.EOF, pexpect.TIMEOUT], 15 | timeout=TIMEOUT_SECONDS) 16 | try: 17 | assert index == 0 18 | except AssertionError: 19 | print('\n==== Screen buffer raw ====\n', 20 | child._buffer.getvalue(), 21 | '\n^^^^ Screen buffer raw ^^^^') 22 | print('==== Screen buffer ====\n', 23 | child._buffer.getvalue().decode('utf8'), 24 | '\n^^^^ Screen buffer ^^^^') 25 | raise 26 | 27 | 28 | def check_prompt(keys, pattern): 29 | child.write(keys) 30 | check_result(pattern) 31 | child.write('\003') # ctrl-c: reset inputs 32 | 33 | 34 | def check_command(command, pattern): 35 | child.sendline(command) 36 | check_result(pattern) 37 | 38 | 39 | def base_functional_testing(): 40 | global child 41 | child = pexpect.spawn('coverage', 42 | ['run', '--append', 'DebugLibrary/shell.py']) 43 | child.expect('Enter interactive shell', timeout=TIMEOUT_SECONDS * 3) 44 | 45 | # auto complete 46 | check_prompt('key\t', 'keywords') 47 | check_prompt('key\t', 'Keyword Should Exist') 48 | check_prompt('k \t', 'keywords.*Keyword Should Exist') 49 | check_prompt('keywords \t', 'BuiltIn.*DebugLibrary') 50 | check_prompt('keywords debug\t', 'DebugLibrary') 51 | #check_prompt('debu\t', 'DebugLibrary') 52 | #check_prompt('DebugLibrary.\t', 'Debug If') 53 | check_prompt('get\t', 'Get Count') 54 | check_prompt('get\t', 'Get Time') 55 | check_prompt('selenium http://google.com \t', 'firefox.*chrome') 56 | #check_prompt('selenium http://google.com fire\t', 'firefox') 57 | 58 | # keyword 59 | check_command('log to console hello', 'hello') 60 | check_command('get time', '.*-.*-.* .*:.*:.*') 61 | # auto suggest 62 | check_prompt('g', 'et time') 63 | 64 | # help 65 | check_command('libs', 66 | 'Imported libraries:.*DebugLibrary.*Builtin libraries:') 67 | check_command('help libs', 'Print imported and builtin libraries,') 68 | check_command('libs \t', '-s') 69 | check_command('libs -s', 'ibraries/BuiltIn.py.*Builtin libraries:') 70 | check_command('?keywords', 'Print keywords of libraries,') 71 | check_command('k debuglibrary', 'Debug') 72 | check_command('k nothing', 'not found library') 73 | check_command('d Debug', 'Open a interactive shell,') 74 | 75 | # var 76 | check_command('@{{list}} = Create List hello world', 77 | "@{{list}} = ['hello', 'world']") 78 | check_command('${list}', "['hello', 'world']") 79 | check_command('&{dict} = Create Dictionary name=admin', 80 | "&{dict} = {'name': 'admin'}") 81 | check_command('${dict.name}', 'admin') 82 | 83 | # fail-safe 84 | check_command('fail', 'AssertionError') 85 | check_command('nothing', "No keyword with name 'nothing' found.") 86 | check_command('get', 87 | "execution failed:.*No keyword with name 'get' found.") 88 | 89 | # debug if 90 | check_command('${secs} = Get Time epoch', 'secs.* = ') 91 | check_command('Debug If ${secs} > 1', 'Enter interactive shell') 92 | check_command('exit', 'Exit shell.') 93 | check_command('Debug If ${secs} < 1', '> ') 94 | 95 | # exit 96 | check_command('exit', 'Exit shell.') 97 | child.wait() 98 | 99 | return 'OK' 100 | 101 | 102 | def step_functional_testing(): 103 | global child 104 | # Command "coverage run robot tests/step.robot" does not work, 105 | # so start the program using DebugLibrary's shell instead of "robot". 106 | child = pexpect.spawn('coverage', 107 | ['run', '--append', 'DebugLibrary/shell.py', 108 | 'tests/step.robot']) 109 | child.expect('Type "help" for more information.*>', 110 | timeout=TIMEOUT_SECONDS * 3) 111 | 112 | check_command('list', 'Please run `step` or `next` command first.') 113 | 114 | support_source_lineno = get_version() >= '3.2' 115 | 116 | if support_source_lineno: 117 | check_command('s', # step 118 | '/tests/step.robot.7..*' 119 | '-> log to console working.*' 120 | '=> BuiltIn.Log To Console working') 121 | check_command('l', # list 122 | ' 7 -> log to console working') 123 | check_command('n', # next 124 | '/tests/step.robot.8..*' 125 | '@.* = Create List hello world.*' 126 | '@.* = BuiltIn.Create List hello world') 127 | check_command('', # just repeat last command 128 | '/tests/step.robot.11..*' 129 | '-> log to console another test case.*' 130 | '=> BuiltIn.Log To Console another test case') 131 | check_command('l', # list 132 | ' 6 debug.*' 133 | ' 7 log to console working.*' 134 | ' 8 @.* = Create List hello world.*' 135 | ' 9.*' 136 | ' 10 test2.*' 137 | ' 11 -> log to console another test case.*' 138 | ' 12 log to console end') 139 | check_command('ll', # longlist 140 | ' 10 test2.*' 141 | ' 11 -> log to console another test case.*' 142 | ' 12 log to console end') 143 | else: 144 | check_command('s', # step 145 | '=> BuiltIn.Log To Console working') 146 | check_command('l', # list 147 | 'Please upgrade robotframework') 148 | check_command('n', # next 149 | '@.* = BuiltIn.Create List hello world') 150 | check_command('', # repeat last command 151 | '=> BuiltIn.Log To Console another test case') 152 | 153 | # Exit the debug mode started by Debug keyword. 154 | check_command('c', # continue 155 | 'Exit shell.*' 156 | 'another test case.*' 157 | 'end') 158 | # Exit the interactive shell started by "DebugLibrary/shell.py". 159 | check_command('c', 'Report: ') 160 | child.wait() 161 | 162 | return 'OK' 163 | 164 | 165 | class FunctionalTestCase(unittest.TestCase): 166 | def test_base_functional(self): 167 | assert base_functional_testing() == 'OK' 168 | 169 | def test_step_functional(self): 170 | assert step_functional_testing() == 'OK' 171 | 172 | 173 | def suite(): 174 | suite = unittest.TestSuite() 175 | suite.addTest(FunctionalTestCase('test_base_functional')) 176 | suite.addTest(FunctionalTestCase('test_step_functional')) 177 | return suite 178 | 179 | 180 | if __name__ == '__main__': 181 | print(base_functional_testing()) 182 | print(step_functional_testing()) 183 | -------------------------------------------------------------------------------- /ChangeLog: -------------------------------------------------------------------------------- 1 | 2024-01-12 Xie Yanbo 2 | 3 | * drop support for python 3.7 4 | * support robotframework 7.0 5 | * release version 2.5.0 6 | 7 | 2024-01-11 Xie Yanbo 8 | 9 | * support python 3.9, 3.10, 3.11 and 3.12 10 | * drop support for python 3.6 11 | * upgrade robotframework to >= 4.0 12 | * release version 2.4.0 13 | 14 | 2022-09-07 Oli I'Anson 15 | 16 | * upgrade prompt-toolkit to 3.x 17 | 18 | 2022-06-07 Xie Yanbo 19 | 20 | * drop support for python 3.5 21 | * get source file and lineno in RF 4.0 22 | * release version 2.3.0 23 | 24 | 2022-04-13 Oli I'Anson 25 | 26 | * use exc.message instead of full_message which was removed in RF 5.0 27 | 28 | 2021-03-29 Xie Yanbo 29 | 30 | * release version 2.2.2 31 | 32 | 2021-03-25 lobinho 33 | 34 | * Adding default empty string tuple to ensure proper formatting of prompt 35 | 36 | 2020-07-17 Xie Yanbo 37 | 38 | * release version 2.2.1 39 | 40 | 2020-07-03 Stavros Ntentos 41 | 42 | * explicitly declare the dependent package prompt-toolkit must be >=2 43 | 44 | 2020-05-18 Xie Yanbo 45 | 46 | * remove selenium's short name s to avoid conflict with command step 47 | 48 | 2020-05-17 Xie Yanbo 49 | 50 | * release version 2.2.0 51 | * add Debug If keyword 52 | * rename libs' short name from l to ls to avoid conflict with command list 53 | 54 | 2020-05-09 Xie Yanbo 55 | 56 | * release version 2.1.0 57 | * add step, next, continue, list and longlist commands 58 | 59 | 2020-05-03 Xie Yanbo 60 | 61 | * release version 2.0.0 62 | * drop python 2 and 3.4 support 63 | * upgrade prompt-toolkit to 2.x 64 | * prompt-toolkit 3 is not compatible with python3.5 65 | * split the huge source file into small pieces 66 | * refactory 67 | 68 | 2020-04-30 Xie Yanbo 69 | 70 | * release version 1.3.1 71 | 72 | 2020-04-27 Jones Sabino 73 | 74 | * fix indentation bug 75 | 76 | 2020-04-26 Xie Yanbo 77 | 78 | * release version 1.3.0 79 | 80 | 2020-04-26 Jones Sabino 81 | 82 | * Compatibility with RF 3.2 83 | 84 | 2020-02-05 Xie Yanbo 85 | 86 | * release version 1.2.1 87 | * fix docs command 88 | 89 | 2020-02-05 Xie Yanbo 90 | 91 | * release version 1.2 92 | 93 | 2020-01-30 tkoukkari 94 | 95 | * added support for command d(ocs) 96 | 97 | 2019-03-27 Francesco Spegni 98 | 99 | * fix mispelling 100 | 101 | 2018-07-13 Xie Yanbo 102 | 103 | * release version 1.1.4 104 | 105 | 2018-06-29 Jonathan Gayvallet 106 | 107 | * Fix keyword discovery command 108 | 109 | 2018-06-13 Xie Yanbo 110 | 111 | * Prompt_toolkit 2.0 is not compatible with 1.0 112 | * release version 1.1.3 113 | 114 | 2018-03-08 Xie Yanbo 115 | 116 | * fix PermissionError when running rfdebug on Windows 117 | * release version 1.1.2 118 | 119 | 2018-03-04 Xie Yanbo 120 | 121 | * selenium2library has been renamed to seleniumlibrary 122 | * release version 1.1.1 123 | 124 | 2018-01-24 Xie Yanbo 125 | 126 | * support passing pybot's arguments, disabled all logs by default 127 | * support press ctrl+c to interrupt running keywords 128 | * support comment format of robotframework 129 | * print value if input variable name only 130 | * release version 1.1 131 | 132 | 2018-01-23 Xie Yanbo 133 | 134 | * support import library with arguments 135 | * release version 1.0.3 136 | 137 | 2017-09-29 Xie Yanbo 138 | 139 | * support keyword document including non-ASCII characters 140 | * release version 1.0.2 141 | 142 | 2017-08-23 Xie Yanbo 143 | 144 | * change the minimum requirement of robotframework to version 2.9 145 | * release version 1.0.1 146 | 147 | 2017-08-11 Xie Yanbo 148 | 149 | * use python-prompt-toolkit to handle prompt with color support 150 | * auto completion for robotframework keywords and debug shell commands 151 | * save history to file 152 | * rename shell script rfshell to rfdebug 153 | * release version 1.0.0 154 | 155 | 2017-08-03 Xie Yanbo 156 | 157 | * fix sys.stdout.encoding error 158 | * release version 0.9.1 159 | 160 | 2017-08-02 Xie Yanbo 161 | 162 | * autocompletion keywords and selenium commands 163 | * release version 0.9 164 | 165 | 2017-07-31 Xie Yanbo 166 | 167 | * add libs and keywords command for introspection 168 | * update document and help 169 | 170 | 2017-07-27 Xie Yanbo 171 | 172 | * release version 0.8.2 173 | 174 | 2017-07-26 asiekkinen 175 | 176 | * support non-UTF8 encoding terminals 177 | 178 | 2017-07-11 Xie Yanbo 179 | 180 | * robotframework 3.0 officially supported Python 3 181 | * release version 0.8.1 182 | 183 | 2017-07-10 indrakumarm 184 | 185 | * UTF-8 support for Python 3 186 | 187 | 2016-09-25 Xie Yanbo 188 | 189 | * release version 0.8 190 | 191 | 2016-09-23 Juha Jokimäki 192 | 193 | * Support assigning variables 194 | 195 | 2016-08-25 Xie Yanbo 196 | 197 | * release version 0.7.1 198 | 199 | 2016-08-24 Steve Turner 200 | 201 | * Fix missing browser variable 202 | 203 | 2016-07-21 Xie Yanbo 204 | 205 | * select which browser you what to launch 206 | * update documents 207 | * build wheel format package 208 | * release version 0.7 209 | 210 | 2016-07-20 Louie Lu 211 | 212 | * change seleniumlibrary to selenium2library 213 | * Add `get_webdriver_remote` for selenium debug 214 | 215 | 2015-10-07 serhatbolsu 216 | 217 | * Allow unicode characters in commands 218 | 219 | 2015-05-20 Vincent Fretin 220 | 221 | * support Python 3 222 | 223 | 2013-08-06 Xie Yanbo 224 | 225 | * support Robot Framework 2.8 226 | * release version 0.3 227 | 228 | 2012-08-05 Silvio Tomatis 229 | * support for readline 230 | 231 | 2012-03-14 Xie Yanbo 232 | 233 | * support Robot Framework 2.7 234 | * release version 0.2.4 235 | 236 | 2012-03-13 Xie Yanbo 237 | 238 | * fix temporary file deleted too soon 239 | * release version 0.2.3 240 | 241 | 2011-11-17 Xie Yanbo 242 | 243 | * fix PYPI name 244 | * fix documents 245 | * release version 0.2.2 246 | 247 | 2011-11-16 Xie Yanbo 248 | 249 | * first release, version 0.2 250 | 251 | 2011-10-13 Xie Yanbo 252 | 253 | * start project, version 0.1 254 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Debug Library for Robot Framework 2 | ================================= 3 | 4 | .. contents:: 5 | :local: 6 | 7 | Introduction 8 | ------------ 9 | 10 | Robotframework-DebugLibrary is a debug library for `RobotFramework`_, 11 | which can be used as an interactive shell(REPL) also. 12 | 13 | .. _`RobotFramework`: http://robotframework.org/ 14 | 15 | .. image:: https://api.codeclimate.com/v1/badges/5201026ff11b63530cf5/maintainability 16 | :target: https://codeclimate.com/github/xyb/robotframework-debuglibrary/maintainability 17 | :alt: Maintainability 18 | 19 | .. image:: https://api.codeclimate.com/v1/badges/5201026ff11b63530cf5/test_coverage 20 | :target: https://codeclimate.com/github/xyb/robotframework-debuglibrary/test_coverage 21 | :alt: Test Coverage 22 | 23 | .. image:: https://github.com/xyb/robotframework-debuglibrary/actions/workflows/test.yml/badge.svg 24 | :target: https://github.com/xyb/robotframework-debuglibrary/actions/workflows/test.yml 25 | :alt: test 26 | 27 | .. image:: https://img.shields.io/pypi/v/robotframework-debuglibrary.svg 28 | :target: https://pypi.org/project/robotframework-debuglibrary/ 29 | :alt: Latest version 30 | 31 | .. image:: https://img.shields.io/badge/robotframework-4%20%7C%205%20%7C%206%20%7C%207-blue 32 | :target: https://github.com/xyb/robotframework-debuglibrary 33 | :alt: Support robotframework versions 34 | 35 | .. image:: https://img.shields.io/pypi/pyversions/robotframework-debuglibrary 36 | :target: https://github.com/xyb/robotframework-debuglibrary 37 | :alt: Support python versions 38 | 39 | .. image:: https://img.shields.io/pypi/dm/robotframework-debuglibrary 40 | :target: https://pypi.org/project/robotframework-debuglibrary/ 41 | :alt: PyPI Downloads 42 | 43 | .. image:: https://img.shields.io/pypi/l/robotframework-debuglibrary.svg 44 | :target: https://github.com/xyb/robotframework-debuglibrary/blob/master/LICENSE 45 | :alt: License 46 | 47 | 48 | Installation 49 | ------------ 50 | 51 | To install using ``pip``:: 52 | 53 | pip install robotframework-debuglibrary 54 | 55 | NOTICE: 2.0 is not compatible with python 2 56 | ******************************************* 57 | 58 | ``DebugLibrary`` >= 2.0.0 supports Python versions 3.x only. 59 | If you still using python 2.7, please use ``DebugLibrary`` < 2.0.0 :: 60 | 61 | pip install 'robotframework-debuglibrary<2' 62 | 63 | Usage 64 | ----- 65 | 66 | You can use this as a library, import ``DebugLibrary`` and call ``Debug`` 67 | or ``Debug If`` keywords in your test files like this:: 68 | 69 | *** Settings *** 70 | Library DebugLibrary 71 | 72 | ** test case ** 73 | SOME TEST 74 | # some keywords... 75 | Debug 76 | # some else... 77 | ${count} = Get Element Count name:div_name 78 | Debug If ${count} < 1 79 | 80 | Or you can run it standalone as a ``RobotFramework`` shell:: 81 | 82 | $ rfdebug 83 | [...snap...] 84 | >>>>> Enter interactive shell 85 | > help 86 | Input Robotframework keywords, or commands listed below. 87 | Use "libs" or "l" to see available libraries, 88 | use "keywords" or "k" to see the list of library keywords, 89 | use the TAB keyboard key to autocomplete keywords. 90 | 91 | Documented commands (type help ): 92 | ======================================== 93 | EOF continue docs help keywords libs ll n pdb selenium 94 | c d exit k l list longlist next s step 95 | > log hello 96 | > get time 97 | < '2011-10-13 18:50:31' 98 | > # use TAB to auto complete commands 99 | > BuiltIn.Get Time 100 | < '2011-10-13 18:50:39' 101 | > import library String 102 | > get substring helloworld 5 8 103 | < 'wor' 104 | > # define variables as you wish 105 | > ${secs} = Get Time epoch 106 | # ${secs} = 1474814470 107 | > Log to console ${secs} 108 | 1474814470 109 | > @{list} = Create List hello world 110 | # @{list} = ['hello', 'world'] 111 | > Log to console ${list} 112 | ['hello', 'world'] 113 | > &{dict} = Create Dictionary name=admin email=admin@test.local 114 | # &{dict} = {'name': 'admin', 'email': 'admin@test.local'} 115 | > Log ${dict.name} 116 | > # print value if you input variable name only 117 | > ${list} 118 | [u'hello', u'world'] 119 | > ${dict.name} 120 | admin 121 | > # start a selenium server quickly 122 | > help selenium 123 | Start a selenium webdriver and open url in browser you expect. 124 | 125 | s(elenium) [] [] 126 | 127 | default url is google.com, default browser is firefox. 128 | > selenium google.com chrome 129 | # import library SeleniumLibrary 130 | # open browser http://google.com chrome 131 | < 1 132 | > close all browsers 133 | > Ctrl-D 134 | >>>>> Exit shell. 135 | 136 | The interactive shell support auto-completion for robotframework keywords and 137 | commands. Try input ``BuiltIn.`` then type ```` key to feeling it. 138 | The history will save at ``~/.rfdebug_history`` default or any file 139 | defined in environment variable ``RFDEBUG_HISTORY``. 140 | 141 | In case you don't remember the name of keyword during using ``rfdebug``, 142 | there are commands ``libs`` or ``ls`` to list the imported libraries and 143 | built-in libraries, and ``keywords `` or ``k`` to list 144 | keywords of a library. 145 | 146 | ``rfdebug`` accept any ``pybot`` arguments, but by default, ``rfdebug`` 147 | disabled all logs with ``-l None -x None -o None -L None -r None``. 148 | 149 | Step debugging 150 | ************** 151 | 152 | ``DebugLibrary`` support step debugging since version ``2.1.0``. 153 | You can use ``step``/``s``, ``next``/``n``, ``continue``/``c``, 154 | ``list``/``l`` and ``longlist``/``ll`` to trace and view the code 155 | step by step like in ``pdb``:: 156 | 157 | $ robot some.robot 158 | [...snap...] 159 | >>>>> Enter interactive shell 160 | > l 161 | Please run `step` or `next` command first. 162 | > s 163 | .> /Users/xyb/some.robot(7) 164 | -> log to console hello 165 | => BuiltIn.Log To Console hello 166 | > l 167 | 2 Library DebugLibrary 168 | 3 169 | 4 ** test case ** 170 | 5 test 171 | 6 debug 172 | 7 -> log to console hello 173 | 8 log to console world 174 | > n 175 | hello 176 | .> /Users/xyb/some.robot(8) 177 | -> log to console world 178 | => BuiltIn.Log To Console world 179 | > c 180 | >>>>> Exit shell. 181 | world 182 | 183 | Note: Single-step debugging does not support ``FOR`` loops currently. 184 | 185 | Submitting issues 186 | ----------------- 187 | 188 | Bugs and enhancements are tracked in the `issue tracker 189 | `_. 190 | 191 | Before submitting a new issue, it is always a good idea to check is the 192 | same bug or enhancement already reported. If it is, please add your comments 193 | to the existing issue instead of creating a new one. 194 | 195 | Development 196 | ----------- 197 | 198 | If you want to develop and run DebugLibrary locally, you can use :: 199 | 200 | $ python DebugLibrary/shell.py tests/step.robot 201 | 202 | `shell.py` is calling `robot` through a child process, so it will interrupt 203 | python debugging capabilities. If you want to debug in tools like vscode, 204 | pdb, you should run :: 205 | 206 | $ python -m robot tests/step.robot 207 | 208 | If you want to run the test, please install the dependency packages first 209 | and then execute the test :: 210 | 211 | $ python -m pip install setuptools 212 | $ python setup.py develop 213 | $ python setup.py test 214 | 215 | Since RF takes over stdout, debugging information can be output with :: 216 | 217 | import sys 218 | print('some information', file=sys.stdout) 219 | 220 | License 221 | ------- 222 | 223 | This software is licensed under the ``New BSD License``. See the ``LICENSE`` 224 | file in the top distribution directory for the full license text. 225 | 226 | .. # vim: syntax=rst expandtab tabstop=4 shiftwidth=4 shiftround 227 | -------------------------------------------------------------------------------- /DebugLibrary/debugcmd.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from robot.api import logger 4 | from robot.errors import ExecutionFailed, HandlerExecutionFailed 5 | 6 | from .cmdcompleter import CmdCompleter 7 | from .globals import context 8 | from .prompttoolkitcmd import PromptToolkitCmd 9 | from .robotapp import get_robot_instance, reset_robotframework_exception 10 | from .robotkeyword import (get_keywords, get_lib_keywords, find_keyword, 11 | run_keyword) 12 | from .robotlib import get_builtin_libs, get_libs, get_libs_dict, match_libs 13 | from .robotselenium import SELENIUM_WEBDRIVERS, start_selenium_commands 14 | from .sourcelines import (RobotNeedUpgrade, print_source_lines, 15 | print_test_case_lines) 16 | from .steplistener import is_step_mode, set_step_mode 17 | from .styles import (DEBUG_PROMPT_STYLE, get_debug_prompt_tokens, print_error, 18 | print_output) 19 | 20 | HISTORY_PATH = os.environ.get('RFDEBUG_HISTORY', '~/.rfdebug_history') 21 | 22 | 23 | def run_robot_command(robot_instance, command): 24 | """Run command in robotframewrk environment.""" 25 | if not command: 26 | return 27 | 28 | result = '' 29 | try: 30 | result = run_keyword(robot_instance, command) 31 | except HandlerExecutionFailed as exc: 32 | print_error('! keyword:', command) 33 | print_error('! handler execution failed:', exc.message) 34 | except ExecutionFailed as exc: 35 | print_error('! keyword:', command) 36 | print_error('! execution failed:', str(exc)) 37 | except Exception as exc: 38 | print_error('! keyword:', command) 39 | print_error('! FAILED:', repr(exc)) 40 | 41 | if result: 42 | head, message = result 43 | print_output(head, message) 44 | 45 | 46 | class DebugCmd(PromptToolkitCmd): 47 | """Interactive debug shell for robotframework.""" 48 | 49 | prompt_style = DEBUG_PROMPT_STYLE 50 | 51 | def __init__(self, completekey='tab', stdin=None, stdout=None): 52 | PromptToolkitCmd.__init__(self, completekey, stdin, stdout, 53 | history_path=HISTORY_PATH) 54 | self.robot = get_robot_instance() 55 | 56 | def get_prompt_tokens(self, prompt_text): 57 | return get_debug_prompt_tokens(prompt_text) 58 | 59 | def postcmd(self, stop, line): 60 | """Run after a command.""" 61 | return stop 62 | 63 | def pre_loop_iter(self): 64 | """Reset robotframework before every loop iteration.""" 65 | reset_robotframework_exception() 66 | 67 | def do_help(self, arg): 68 | """Show help message.""" 69 | if not arg.strip(): 70 | print('''\ 71 | Input Robotframework keywords, or commands listed below. 72 | Use "libs" or "l" to see available libraries, 73 | use "keywords" or "k" see the list of library keywords, 74 | use the TAB keyboard key to autocomplete keywords. 75 | Access https://github.com/xyb/robotframework-debuglibrary for more details.\ 76 | ''') 77 | 78 | PromptToolkitCmd.do_help(self, arg) 79 | 80 | def get_completer(self): 81 | """Get completer instance specified for robotframework.""" 82 | # commands 83 | commands = [(cmd_name, cmd_name, 'DEBUG command: {0}'.format(doc)) 84 | for cmd_name, doc in self.get_helps()] 85 | 86 | # libraries 87 | for lib in get_libs(): 88 | commands.append(( 89 | lib.name, 90 | lib.name, 91 | 'Library: {0} {1}'.format(lib.name, lib.version), 92 | )) 93 | 94 | # keywords 95 | for keyword in get_keywords(): 96 | # name with library 97 | name = '{0}.{1}'.format(keyword['lib'], keyword['name']) 98 | commands.append(( 99 | name, 100 | keyword['name'], 101 | 'Keyword: {0}'.format(keyword['summary']), 102 | )) 103 | # name without library 104 | commands.append(( 105 | keyword['name'], 106 | keyword['name'], 107 | 'Keyword[{0}.]: {1}'.format(keyword['lib'], 108 | keyword['summary']), 109 | )) 110 | 111 | return CmdCompleter(commands, self) 112 | 113 | def do_selenium(self, arg): 114 | """Start a selenium webdriver and open url in browser you expect. 115 | 116 | selenium [] [] 117 | 118 | default url is google.com, default browser is firefox. 119 | """ 120 | 121 | for command in start_selenium_commands(arg): 122 | print_output('#', command) 123 | run_robot_command(self.robot, command) 124 | 125 | def complete_selenium(self, text, line, begin_idx, end_idx): 126 | """Complete selenium command.""" 127 | if len(line.split()) == 3: 128 | command, url, driver_name = line.lower().split() 129 | return [driver for driver in SELENIUM_WEBDRIVERS 130 | if driver.startswith(driver_name)] 131 | elif len(line.split()) == 2 and line.endswith(' '): 132 | return SELENIUM_WEBDRIVERS 133 | return [] 134 | 135 | complete_s = complete_selenium 136 | 137 | def default(self, line): 138 | """Run RobotFramework keywords.""" 139 | command = line.strip() 140 | 141 | run_robot_command(self.robot, command) 142 | 143 | def _print_lib_info(self, lib, with_source_path=False): 144 | print_output(' {}'.format(lib.name), lib.version) 145 | if lib.doc: 146 | logger.console(' {}'.format(lib.doc.split('\n')[0])) 147 | if with_source_path: 148 | logger.console(' {}'.format(lib.source)) 149 | 150 | def do_libs(self, args): 151 | """Print imported and builtin libraries, with source if `-s` specified. 152 | 153 | ls( libs ) [-s] 154 | """ 155 | print_output('<', 'Imported libraries:') 156 | for lib in get_libs(): 157 | self._print_lib_info(lib, with_source_path='-s' in args) 158 | print_output('<', 'Builtin libraries:') 159 | for name in sorted(get_builtin_libs()): 160 | print_output(' ' + name, '') 161 | 162 | do_ls = do_libs 163 | 164 | def complete_libs(self, text, line, begin_idx, end_idx): 165 | """Complete libs command.""" 166 | if len(line.split()) == 1 and line.endswith(' '): 167 | return ['-s'] 168 | return [] 169 | 170 | complete_l = complete_libs 171 | 172 | def do_keywords(self, args): 173 | """Print keywords of libraries, all or starts with . 174 | 175 | k(eywords) [] 176 | """ 177 | lib_name = args 178 | matched = match_libs(lib_name) 179 | if not matched: 180 | print_error('< not found library', lib_name) 181 | return 182 | libs = get_libs_dict() 183 | for name in matched: 184 | lib = libs[name] 185 | print_output('< Keywords of library', name) 186 | for keyword in get_lib_keywords(lib): 187 | print_output(' {}\t'.format(keyword['name']), 188 | keyword['summary']) 189 | 190 | do_k = do_keywords 191 | 192 | def complete_keywords(self, text, line, begin_idx, end_idx): 193 | """Complete keywords command.""" 194 | if len(line.split()) == 2: 195 | command, lib_name = line.split() 196 | return match_libs(lib_name) 197 | elif len(line.split()) == 1 and line.endswith(' '): 198 | return [_.name for _ in get_libs()] 199 | return [] 200 | 201 | complete_k = complete_keywords 202 | 203 | def do_docs(self, keyword_name): 204 | """Get keyword documentation for individual keywords. 205 | 206 | d(ocs) [] 207 | """ 208 | 209 | keywords = find_keyword(keyword_name) 210 | if not keywords: 211 | print_error('< not find keyword', keyword_name) 212 | elif len(keywords) == 1: 213 | logger.console(keywords[0]['doc']) 214 | else: 215 | print_error('< found {} keywords'.format(len(keywords)), 216 | ', '.join(keywords)) 217 | 218 | do_d = do_docs 219 | 220 | def emptyline(self): 221 | """Repeat last nonempty command if in step mode.""" 222 | self.repeat_last_nonempty_command = is_step_mode() 223 | return super(DebugCmd, self).emptyline() 224 | 225 | def append_command(self, command): 226 | """Append a command to queue.""" 227 | self.cmdqueue.append(command) 228 | 229 | def append_exit(self): 230 | """Append exit command to queue.""" 231 | self.append_command('exit') 232 | 233 | def do_step(self, args): 234 | """Execute the current line, stop at the first possible occasion.""" 235 | set_step_mode(on=True) 236 | self.append_exit() # pass control back to robot runner 237 | 238 | do_s = do_step 239 | 240 | def do_next(self, args): 241 | """Continue execution until the next line is reached or it returns.""" 242 | self.do_step(args) 243 | 244 | do_n = do_next 245 | 246 | def do_continue(self, args): 247 | """Continue execution.""" 248 | self.do_exit(args) 249 | 250 | do_c = do_continue 251 | 252 | def do_list(self, args): 253 | """List source code for the current file.""" 254 | 255 | self.list_source(longlist=False) 256 | 257 | do_l = do_list 258 | 259 | def do_longlist(self, args): 260 | """List the whole source code for the current test case.""" 261 | 262 | self.list_source(longlist=True) 263 | 264 | do_ll = do_longlist 265 | 266 | def list_source(self, longlist=False): 267 | """List source code.""" 268 | if not is_step_mode(): 269 | print('Please run `step` or `next` command first.') 270 | return 271 | 272 | if longlist: 273 | print_function = print_test_case_lines 274 | else: 275 | print_function = print_source_lines 276 | 277 | try: 278 | print_function(context.current_source_path, 279 | context.current_source_lineno) 280 | except RobotNeedUpgrade: 281 | print('Please upgrade robotframework to support list source code:') 282 | print(' pip install "robotframework>=3.2" -U') 283 | 284 | def do_exit(self, args): 285 | """Exit debug shell.""" 286 | set_step_mode(on=False) # explicitly exit REPL will disable step mode 287 | self.append_exit() 288 | return super(DebugCmd, self).do_exit(args) 289 | 290 | def onecmd(self, line): 291 | # restore last command acrossing different Cmd instances 292 | self.lastcmd = context.last_command 293 | stop = super(DebugCmd, self).onecmd(line) 294 | context.last_command = self.lastcmd 295 | return stop 296 | --------------------------------------------------------------------------------