Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support all Robot Framework time formats (#65) #66

Merged
merged 5 commits into from
Jul 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ jobs:
matrix:
python-version: [3.7, 3.10.0]
os: [ubuntu-latest, windows-latest]
fail-fast: false

steps:
- name: Checkout repository
Expand Down
14 changes: 8 additions & 6 deletions Mainframe3270/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from datetime import timedelta
from typing import Any

from robot.api import logger
Expand Down Expand Up @@ -28,8 +29,8 @@ class Mainframe3270(DynamicCore):
| *** Test Cases ***
| Example
| Open Connection Hostname LUname
| Change Wait Time 0.4
| Change Wait Time After Write 0.4
| Change Wait Time 0.4 seconds
| Change Wait Time After Write 0.4 seconds
| Set Screenshot Folder C:\\Temp\\IMG
| ${value} Read 3 10 17
| Page Should Contain String ENTER APPLICATION
Expand All @@ -46,9 +47,9 @@ class Mainframe3270(DynamicCore):
def __init__(
self,
visible: bool = True,
timeout: int = 30,
wait_time: float = 0.5,
wait_time_after_write: float = 0.0,
timeout: timedelta = timedelta(seconds=30),
wait_time: timedelta = timedelta(milliseconds=500),
wait_time_after_write: timedelta = timedelta(seconds=0),
img_folder: str = ".",
run_on_failure_keyword: str = "Take Screenshot",
) -> None:
Expand All @@ -62,7 +63,8 @@ def __init__(
Timeout, waits and screenshot folder are set on library import as shown above.
However, they can be changed during runtime. To modify the ``wait_time``, see `Change Wait Time`,
to modify the ``img_folder``, see `Set Screenshot Folder`,
and to modify the ``timeout``, see the `Change Timeout` keyword.
and to modify the ``timeout``, see the `Change Timeout` keyword. Timeouts support all available
Robot Framework [https://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#time-format|time formats].

By default, Mainframe3270 will take a screenshot on failure.
You can overwrite this to run any other keyword by setting the ``run_on_failure_keyword`` option.
Expand Down
91 changes: 50 additions & 41 deletions Mainframe3270/x3270.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@
import re
import socket
import time
from datetime import timedelta
from typing import Any, List, Optional, Union

from robot.api import logger
from robot.api.deco import keyword
from robot.libraries.BuiltIn import BuiltIn, RobotNotRunningError
from robot.utils import Matcher
from robot.utils import Matcher, secs_to_timestr, timestr_to_secs

from .py3270 import Emulator

Expand All @@ -16,15 +17,15 @@ class x3270(object):
def __init__(
self,
visible: bool,
timeout: int,
wait_time: float,
wait_time_after_write: float,
timeout: timedelta,
wait_time: timedelta,
wait_time_after_write: timedelta,
img_folder: str,
) -> None:
self.visible = visible
self.timeout = timeout
self.wait = wait_time
self.wait_write = wait_time_after_write
self.timeout = self._convert_timeout(timeout)
self.wait = self._convert_timeout(wait_time)
self.wait_write = self._convert_timeout(wait_time_after_write)
self.imgfolder = img_folder
self.mf: Emulator = None # type: ignore
# Try Catch to run in Pycharm, and make a documentation in libdoc with no error
Expand All @@ -33,15 +34,20 @@ def __init__(
except RobotNotRunningError:
self.output_folder = os.getcwd()

def _convert_timeout(self, time):
if isinstance(time, timedelta):
return time.total_seconds()
return timestr_to_secs(time, round_to=None)

@keyword("Change Timeout")
def change_timeout(self, seconds: int) -> None:
def change_timeout(self, seconds: timedelta) -> None:
"""
Change the timeout for connection in seconds.

Example:
| Change Timeout | 3 |
| Change Timeout | 3 seconds |
"""
self.timeout = seconds
self.timeout = self._convert_timeout(seconds)

@keyword("Open Connection")
def open_connection(
Expand Down Expand Up @@ -97,7 +103,7 @@ def close_connection(self) -> None:
self.mf = None # type: ignore

@keyword("Change Wait Time")
def change_wait_time(self, wait_time: float) -> None:
def change_wait_time(self, wait_time: timedelta) -> None:
"""To give time for the mainframe screen to be "drawn" and receive the next commands, a "wait time" has been
created, which by default is set to 0.5 seconds. This is a sleep applied AFTER the following keywords:

Expand All @@ -110,13 +116,14 @@ def change_wait_time(self, wait_time: float) -> None:
If you want to change this value, just use this keyword passing the time in seconds.

Example:
| Change Wait Time | 0.1 |
| Change Wait Time | 2 |
| Change Wait Time | 0.5 |
| Change Wait Time | 200 milliseconds |
| Change Wait Time | 0:00:01.500 |
"""
self.wait = wait_time
self.wait = self._convert_timeout(wait_time)

@keyword("Change Wait Time After Write")
def change_wait_time_after_write(self, wait_time_after_write: float) -> None:
def change_wait_time_after_write(self, wait_time_after_write: timedelta) -> None:
"""To give the user time to see what is happening inside the mainframe, a "wait time after write" has
been created, which by default is set to 0 seconds. This is a sleep applied AFTER sending a string in these
keywords:
Expand All @@ -131,10 +138,11 @@ def change_wait_time_after_write(self, wait_time_after_write: float) -> None:
Note: This keyword is useful for debug purpose

Example:
| Change Wait Time After Write | 0.5 |
| Change Wait Time After Write | 2 |
| Change Wait Time After Write | 1 |
| Change Wait Time After Write | 0.5 seconds |
| Change Wait Time After Write | 0:00:02 |
"""
self.wait_write = wait_time_after_write
self.wait_write = self._convert_timeout(wait_time_after_write)

@keyword("Read")
def read(self, ypos: int, xpos: int, length: int) -> str:
Expand Down Expand Up @@ -301,7 +309,7 @@ def send_PF(self, PF: str) -> None:
Example:
| Send PF | 3 |
"""
self.mf.exec_command(("PF(" + PF + ")").encode("utf-8"))
self.mf.exec_command(("PF({0})").format(PF).encode("utf-8"))
time.sleep(self.wait)

@keyword("Write")
Expand Down Expand Up @@ -365,22 +373,25 @@ def _write(
time.sleep(self.wait)

@keyword("Wait Until String")
def wait_until_string(self, txt: str, timeout: int = 5) -> str:
def wait_until_string(
self, txt: str, timeout: timedelta = timedelta(seconds=5)
) -> str:
"""Wait until a string exists on the mainframe screen to perform the next step. If the string does not appear in
5 seconds, the keyword will raise an exception. You can define a different timeout.

Example:
| Wait Until String | something |
| Wait Until String | something | timeout=10 |
| Wait Until String | something | 10 |
| Wait Until String | something | 15 s |
| Wait Until String | something | 0:00:15 |
"""
max_time = time.ctime(int(time.time()) + timeout)
while time.ctime(int(time.time())) < max_time:
timeout = self._convert_timeout(timeout)
max_time = time.time() + timeout # type: ignore
while time.time() < max_time:
result = self._search_string(str(txt))
if result:
return txt
raise Exception(
'String "' + txt + '" not found in ' + str(timeout) + " seconds"
)
raise Exception(f'String "{txt}" not found in {secs_to_timestr(timeout)}')

def _search_string(self, string: str, ignore_case: bool = False) -> bool:
"""Search if a string exists on the mainframe screen and return True or False."""
Expand Down Expand Up @@ -409,18 +420,18 @@ def page_should_contain_string(

Example:
| Page Should Contain String | something |
| Page Should Contain String | someTHING | ignore_case=True |
| Page Should Contain String | someTHING | ignore_case=True |
| Page Should Contain String | something | error_message=New error message |
"""
message = 'The string "' + txt + '" was not found'
message = f'The string "{txt}" was not found'
if error_message:
message = error_message
if ignore_case:
txt = txt.lower()
result = self._search_string(txt, ignore_case)
if not result:
raise Exception(message)
logger.info('The string "' + txt + '" was found')
logger.info(f'The string "{txt}" was found')

@keyword("Page Should Not Contain String")
def page_should_not_contain_string(
Expand All @@ -437,7 +448,7 @@ def page_should_not_contain_string(
| Page Should Not Contain String | someTHING | ignore_case=True |
| Page Should Not Contain String | something | error_message=New error message |
"""
message = 'The string "' + txt + '" was found'
message = f'The string "{txt}" was found'
if error_message:
message = error_message
if ignore_case:
Expand All @@ -464,7 +475,7 @@ def page_should_contain_any_string(
| Page Should Contain Any String | ${list_of_string} | ignore_case=True |
| Page Should Contain Any String | ${list_of_string} | error_message=New error message |
"""
message = 'The strings "' + str(list_string) + '" were not found'
message = f'The strings "{list_string}" were not found'
if error_message:
message = error_message
if ignore_case:
Expand Down Expand Up @@ -547,7 +558,7 @@ def page_should_not_contain_all_strings(
result = self._search_string(string, ignore_case)
if result:
if message is None:
message = 'The string "' + string + '" was found'
message = f'The string "{string}" was found'
raise Exception(message)

@keyword("Page Should Contain String X Times")
Expand Down Expand Up @@ -580,7 +591,7 @@ def page_should_contain_string_x_times(
if message is None:
message = f'The string "{txt}" was not found "{number}" times, it appears "{number_of_times}" times'
raise Exception(message)
logger.info('The string "' + txt + '" was found "' + str(number) + '" times')
logger.info(f'The string "{txt}" was found "{number}" times')

@keyword("Page Should Match Regex")
def page_should_match_regex(self, regex_pattern: str) -> None:
Expand All @@ -594,7 +605,7 @@ def page_should_match_regex(self, regex_pattern: str) -> None:
"""
page_text = self._read_all_screen()
if not re.findall(regex_pattern, page_text, re.MULTILINE):
raise Exception('No matches found for "' + regex_pattern + '" pattern')
raise Exception(f'No matches found for "{regex_pattern}" pattern')

@keyword("Page Should Not Match Regex")
def page_should_not_match_regex(self, regex_pattern: str) -> None:
Expand All @@ -608,9 +619,7 @@ def page_should_not_match_regex(self, regex_pattern: str) -> None:
"""
page_text = self._read_all_screen()
if re.findall(regex_pattern, page_text, re.MULTILINE):
raise Exception(
'There are matches found for "' + regex_pattern + '" pattern'
)
raise Exception(f'There are matches found for "{regex_pattern}" pattern')

@keyword("Page Should Contain Match")
def page_should_contain_match(
Expand Down Expand Up @@ -644,7 +653,7 @@ def page_should_contain_match(
result = matcher.match(all_screen)
if not result:
if message is None:
message = 'No matches found for "' + txt + '" pattern'
message = f'No matches found for "{txt}" pattern'
raise Exception(message)

@keyword("Page Should Not Contain Match")
Expand Down Expand Up @@ -679,7 +688,7 @@ def page_should_not_contain_match(
result = matcher.match(all_screen)
if result:
if message is None:
message = 'There are matches found for "' + txt + '" pattern'
message = f'There are matches found for "{txt}" pattern'
raise Exception(message)

def _read_all_screen(self) -> str:
Expand All @@ -702,11 +711,11 @@ def _compare_all_list_with_screen_text(
result = self._search_string(string, ignore_case)
if not should_match and result:
if message is None:
message = 'The string "' + string + '" was found'
message = f'The string "{string}" was found'
raise Exception(message)
elif should_match and not result:
if message is None:
message = 'The string "' + string + '" was not found'
message = f'The string "{string}" was not found'
raise Exception(message)

@staticmethod
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ Library Mainframe3270
*** Test Cases ***
Example
Open Connection Hostname LUname
Change Wait Time 0.4
Change Wait Time After Write 0.4
Change Wait Time 0.4 seconds
Change Wait Time After Write 0.4 seconds
Set Screenshot Folder C:\\Temp\\IMG
${value} Read 3 10 17
Page Should Contain String ENTER APPLICATION
Expand Down Expand Up @@ -71,6 +71,7 @@ This is useful when test cases are run in a CI/CD-pipeline and there is no need
Timeout, waits and screenshot folder are set on library import as shown above. However, they can be changed during runtime. To modify the ``wait_time``, see [Change Wait Time](https://raw.githack.com/Altran-PT-GDC/Robot-Framework-Mainframe-3270-Library/master/doc/Mainframe3270.html#Change%20Wait%20Time),
to modify the ``img_folder``, see [Set Screenshot Folder](https://raw.githack.com/Altran-PT-GDC/Robot-Framework-Mainframe-3270-Library/master/doc/Mainframe3270.html#Set%20Screenshot%20Folder),
and to modify the ``timeout``, see the [Change Timeout](https://raw.githack.com/Altran-PT-GDC/Robot-Framework-Mainframe-3270-Library/master/doc/Mainframe3270.html#Change%20Timeout) keyword.
Timeouts support all available Robot Framework [time formats](https://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#time-format).

By default, Mainframe3270 will take a screenshot on failure. You can overwrite this to run any other keyword by setting the ``run_on_failure_keyword`` option. If you pass ``None`` to this argument, no keyword will be run. To change the ``run_on_failure_keyword`` during runtime, see [Register Run On Failure Keyword](https://raw.githack.com/Altran-PT-GDC/Robot-Framework-Mainframe-3270-Library/master/doc/Mainframe3270.html#Register%20Run%20On%20Failure%20Keyword).

Expand Down
7 changes: 5 additions & 2 deletions atest/connection.robot
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,25 @@ Resource pub400_variables.robot

Test Teardown Test Teardown


*** Variables ***
${ARGFILE} ${CURDIR}/resources/argfile.txt
${TRACE_FILE} ${CURDIR}/x3270.trace


*** Test Cases ***
Test Connection With Extra Args List
${extra_args} Create List -trace -tracefile ${TRACE_FILE}
Open Connection ${host} extra_args=${extra_args}
Open Connection ${HOST} extra_args=${extra_args}
Sleep 0.5 s
File Should Exist ${TRACE_FILE}

Test Connection With Argfile
Open Connection ${host} extra_args=${ARGFILE}
Open Connection ${HOST} extra_args=${ARGFILE}
Sleep 0.5 s
File Should Exist ${TRACE_FILE}


*** Keywords ***
Test Teardown
Close Connection
Expand Down
Loading