Skip to content

Commit

Permalink
Merge pull request #118 from robinmatz/117-create-get-string-position…
Browse files Browse the repository at this point in the history
…s-only-before-and-after

#117 Create Get String Positions Only Before / After keywords
  • Loading branch information
samuelpcabral authored Dec 12, 2023
2 parents 522f2ff + 1b2f425 commit d8c6e0b
Show file tree
Hide file tree
Showing 9 changed files with 261 additions and 49 deletions.
15 changes: 4 additions & 11 deletions Mainframe3270/keywords/commands.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import time
from typing import Optional, Union

from robot.api import logger
from robot.api.deco import keyword

from Mainframe3270.librarycomponent import LibraryComponent
from Mainframe3270.utils import coordinates_to_dict
from Mainframe3270.utils import ResultMode, prepare_position_as


class CommandKeywords(LibraryComponent):
Expand Down Expand Up @@ -86,7 +85,7 @@ def send_pf(self, pf: str) -> None:
time.sleep(self.wait_time)

@keyword("Get Current Position")
def get_current_position(self, mode: str = "As Tuple") -> Union[tuple, dict]:
def get_current_position(self, mode: ResultMode = ResultMode.As_Tuple) -> Union[tuple, dict]:
"""Returns the current cursor position. The coordinates are 1 based.
By default, this keyword returns a tuple of integers. However, if you specify the `mode` with the value
Expand All @@ -97,14 +96,8 @@ def get_current_position(self, mode: str = "As Tuple") -> Union[tuple, dict]:
| Get Cursor Position | As Dict | # Returns a position like {"xpos": 1, "ypos": 1} |
"""
ypos, xpos = self.mf.get_current_position()
if mode.lower() == "as dict":
return coordinates_to_dict(ypos, xpos)
elif mode.lower() == "as tuple":
return ypos, xpos
else:
logger.warn('"mode" should be either "as dict" or "as tuple". Returning the result as tuple')
return ypos, xpos
position = self.mf.get_current_position()
return prepare_position_as(position, mode)

@keyword("Move Cursor To")
def move_cursor_to(self, ypos: int, xpos: int):
Expand Down
73 changes: 59 additions & 14 deletions Mainframe3270/keywords/read_write.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import time
from typing import Any, Optional

from robot.api import logger
from robot.api.deco import keyword

from Mainframe3270.librarycomponent import LibraryComponent
from Mainframe3270.utils import coordinates_to_dict
from Mainframe3270.utils import ResultMode, prepare_positions_as


class ReadWriteKeywords(LibraryComponent):
Expand All @@ -24,7 +23,7 @@ def read(self, ypos: int, xpos: int, length: int) -> str:
def read_from_current_position(self, length: int):
"""Similar to `Read`, however this keyword only takes `length` as an argument
to get a string of length from the current cursor position."""
ypos, xpos = self.library.get_current_position()
ypos, xpos = self.mf.get_current_position()
return self.mf.string_get(ypos, xpos, length)

@keyword("Read All Screen")
Expand All @@ -45,7 +44,7 @@ def read_all_screen(self) -> str:
return self.mf.read_all_screen()

@keyword("Get String Positions")
def get_string_positions(self, string: str, mode: str = "As Tuple", ignore_case: bool = False):
def get_string_positions(self, string: str, mode: ResultMode = ResultMode.As_Tuple, ignore_case: bool = False):
"""Returns a list of tuples of ypos and xpos for the position where the `string` was found,
or an empty list if it was not found.
Expand All @@ -55,17 +54,63 @@ def get_string_positions(self, string: str, mode: str = "As Tuple", ignore_case:
If `ignore_case` is set to `True`, then the search is done case-insensitively.
Example:
| ${positions} | Get String Positions | Abc | # Returns something like [(1, 8)]
| ${positions} | Get String Positions | Abc | As Dict | # Returns something like [{"ypos": 1, "xpos": 8}]
| ${positions} | Get String Positions | Abc | | # Returns a list like [(1, 8)] |
| ${positions} | Get String Positions | Abc | As Dict | # Returns a list like [{"ypos": 1, "xpos": 8}] |
"""
results = self.mf.get_string_positions(string, ignore_case)
if mode.lower() == "as dict":
return [coordinates_to_dict(ypos, xpos) for ypos, xpos in results]
elif mode.lower() == "as tuple":
return results
else:
logger.warn('"mode" should be either "as dict" or "as tuple". Returning the result as tuple')
return results
positions = self.mf.get_string_positions(string, ignore_case)
return prepare_positions_as(positions, mode)

@keyword("Get String Positions Only After")
def get_string_positions_only_after(
self,
ypos: int,
xpos: int,
string: str,
mode: ResultMode = ResultMode.As_Tuple,
ignore_case: bool = False,
):
"""Returns a list of tuples of ypos and xpos for the position where the `string` was found,
but only after the specified ypos/xpos coordinates. If it is not found an empty list is returned.
If you specify the `mode` with the value `"As Dict"` (case-insensitive),
a list of dictionaries in the form of ``[{"xpos": int, "ypos": int}]`` is returned.
If `ignore_case` is set to `True`, then the search is done case-insensitively.
Example:
| ${positions} | Get String Positions Only After | 5 | 4 | Abc | | # Returns a list like [(5, 5)] |
| ${positions} | Get String Positions Only After | 5 | 4 | Abc | As Dict | # Returns a list like [{"ypos": 5, "xpos": 5}] |
"""
self.mf.check_limits(ypos, xpos)
positions = self.mf.get_string_positions(string, ignore_case)
filtered_positions = [position for position in positions if position > (ypos, xpos)]
return prepare_positions_as(filtered_positions, mode)

@keyword("Get String Positions Only Before")
def get_string_positions_only_before(
self,
ypos: int,
xpos: int,
string: str,
mode: ResultMode = ResultMode.As_Tuple,
ignore_case: bool = False,
):
"""Returns a list of tuples of ypos and xpos for the position where the `string` was found,
but only before the specified ypos/xpos coordinates. If it is not found an empty list is returned.
If you specify the `mode` with the value `"As Dict"` (case-insensitive),
a list of dictionaries in the form of ``[{"xpos": int, "ypos": int}]`` is returned.
If `ignore_case` is set to `True`, then the search is done case-insensitively.
Example:
| ${positions} | Get String Positions Only Before | 11 | 20 | Abc | | # Returns a list like [(11, 19)] |
| ${positions} | Get String Positions Only Before | 11 | 20 | Abc | As Dict | # Returns a list like [{"ypos": 11, "xpos": 19}] |
"""
self.mf.check_limits(ypos, xpos)
positions = self.mf.get_string_positions(string, ignore_case)
filtered_positions = [position for position in positions if position < (ypos, xpos)]
return prepare_positions_as(filtered_positions, mode)

@keyword("Write")
def write(self, txt: str) -> None:
Expand Down
6 changes: 3 additions & 3 deletions Mainframe3270/py3270.py
Original file line number Diff line number Diff line change
Expand Up @@ -431,7 +431,7 @@ def move_to(self, ypos, xpos):
move the cursor to the given coordinates. Coordinates are 1
based, as listed in the status area of the terminal.
"""
self._check_limits(ypos, xpos)
self.check_limits(ypos, xpos)
# the screen's coordinates are 1 based, but the command is 0 based
xpos -= 1
ypos -= 1
Expand Down Expand Up @@ -461,7 +461,7 @@ def string_get(self, ypos, xpos, length):
Coordinates are 1 based, as listed in the status area of the
terminal.
"""
self._check_limits(ypos, xpos)
self.check_limits(ypos, xpos)
if (xpos + length) > (self.model_dimensions["columns"] + 1):
raise Exception("You have exceeded the x-axis limit of the mainframe screen")
# the screen's coordinates are 1 based, but the command is 0 based
Expand Down Expand Up @@ -542,7 +542,7 @@ def get_current_position(self):
list_of_strings = command.data[0].decode("utf-8").split(" ")
return tuple([int(i) + 1 for i in list_of_strings])

def _check_limits(self, ypos, xpos):
def check_limits(self, ypos, xpos):
if ypos > self.model_dimensions["rows"]:
raise Exception("You have exceeded the y-axis limit of the mainframe screen")
if xpos > self.model_dimensions["columns"]:
Expand Down
25 changes: 25 additions & 0 deletions Mainframe3270/utils.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,33 @@
from datetime import timedelta
from enum import Enum, auto
from typing import List, Tuple

from robot.api import logger
from robot.utils import timestr_to_secs


class ResultMode(Enum):
As_Tuple = auto()
As_Dict = auto()


def prepare_position_as(position: Tuple[int, int], mode: ResultMode):
return prepare_positions_as([position], mode)[0]


def prepare_positions_as(positions: List[Tuple[int, int]], mode: ResultMode):
if mode == ResultMode.As_Dict:
return [coordinates_to_dict(ypos, xpos) for ypos, xpos in positions]
elif mode == ResultMode.As_Tuple:
return positions
else:
logger.warn(
f'"mode" should be either "{ResultMode.As_Dict}" or "{ResultMode.As_Tuple}". '
"Returning the result as tuple"
)
return positions


def convert_timeout(time):
if isinstance(time, timedelta):
return time.total_seconds()
Expand Down
64 changes: 54 additions & 10 deletions atest/mainframe.robot
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,18 @@ Exception Test Move Cursor To
Run Keyword And Expect Error ${X_AXIS_EXCEEDED_EXPECTED_ERROR}
... Move Cursor To 1 81

Exception Test Get String Positions Only After
Run Keyword And Expect Error ${Y_AXIS_EXCEEDED_EXPECTED_ERROR} Get String Positions Only After
... 25 1 my search string
Run Keyword And Expect Error ${X_AXIS_EXCEEDED_EXPECTED_ERROR} Get String Positions Only After
... 1 81 my search string

Exception Test Get String Positions Only Before
Run Keyword And Expect Error ${Y_AXIS_EXCEEDED_EXPECTED_ERROR} Get String Positions Only Before
... 25 1 my search string
Run Keyword And Expect Error ${X_AXIS_EXCEEDED_EXPECTED_ERROR} Get String Positions Only Before
... 1 81 my search string

Test Wait Until String
Wait Until String ${WELCOME_TITLE} timeout=4

Expand Down Expand Up @@ -259,23 +271,55 @@ Test Get Current Position
Should Be Equal ${{ {"xpos": 27, "ypos": 6} }} ${position_as_dict}

Test Get String Positions
${position} Get String Positions Welcome
Should Be Equal ${{ [(1, 10)] }} ${position}
${positions} Get String Positions Welcome
Should Be Equal ${{ [(1, 10)] }} ${positions}

Test Get String Positions Case-Insensitive
${position} Get String Positions Welcome ignore_case=True
Should Be Equal ${{ [(1, 10), (9, 5)] }} ${position}
${positions} Get String Positions Welcome ignore_case=True
Should Be Equal ${{ [(1, 10), (9, 5)] }} ${positions}

Test Get String Positions As Dict
${position} Get String Positions Welcome As Dict
${positions} Get String Positions Welcome As Dict
Should Be Equal ${{ [{"ypos": 1, "xpos": 10}] }}
... ${position}
... ${positions}

Test Get String Positions Without Result
${position} Get String Positions ${STRING_NON_EXISTENT}
Should Be Equal ${{ [] }} ${position}
${position} Get String Positions ${STRING_NON_EXISTENT} As Dict
Should Be Equal ${{ [] }} ${position}
${positions} Get String Positions ${STRING_NON_EXISTENT}
Should Be Equal ${{ [] }} ${positions}
${positions} Get String Positions ${STRING_NON_EXISTENT} As Dict
Should Be Equal ${{ [] }} ${positions}

Test Get String Positions Only After
${positions} Get String Positions Only After 5 10 name
Should Be Equal ${{ [(5, 11), (21, 38)] }} ${positions}

Test Get String Positions Only After As Dict
${positions} Get String Positions Only After 5 10 name As Dict
Should Be Equal ${{ [{'ypos': 5, 'xpos': 11}, {'ypos': 21, 'xpos': 38}] }} ${positions}

Test Get String Positions Only After Case-Insensitive
${positions} Get String Positions Only After 9 4 Welcome ignore_case=True
Should Be Equal ${{ [(9, 5)] }} ${positions}

Test Get String Positions Only After Without Results
${positions} Get String Positions Only After 9 5 Welcome ignore_case=True
Should Be Empty ${positions}

Test Get String Positions Only Before
${positions} Get String Positions Only Before 5 11 name
Should Be Equal ${{ [(2, 55), (4, 56)] }} ${positions}

Test Get String Positions Only Before As Dict
${positions} Get String Positions Only Before 5 11 name As Dict
Should Be Equal ${{ [{'ypos': 2, 'xpos': 55}, {'ypos': 4, 'xpos': 56}] }} ${positions}

Test Get String Positions Only Before Case-Insensitive
${positions} Get String Positions Only Before 1 11 Welcome ignore_case=True
Should Be Equal ${{ [(1, 10)] }} ${positions}

Test Get String Positions Only Before Without Results
${positions} Get String Positions Only Before 1 10 Welcome ignore_case=True
Should Be Empty ${positions}


*** Keywords ***
Expand Down
7 changes: 5 additions & 2 deletions utest/Mainframe3270/keywords/test_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from Mainframe3270.keywords import CommandKeywords
from Mainframe3270.py3270 import Emulator
from Mainframe3270.utils import ResultMode

from .utils import create_test_object_for

Expand Down Expand Up @@ -107,7 +108,7 @@ def test_get_current_position(mocker: MockerFixture, under_test: CommandKeywords
def test_get_current_position_as_dict(mocker: MockerFixture, under_test: CommandKeywords):
mocker.patch("Mainframe3270.py3270.Emulator.get_current_position", return_value=(6, 6))

assert under_test.get_current_position("as DiCt") == {"xpos": 6, "ypos": 6}
assert under_test.get_current_position(ResultMode.As_Dict) == {"xpos": 6, "ypos": 6}


def test_get_current_position_invalid_mode(mocker: MockerFixture, under_test: CommandKeywords):
Expand All @@ -116,7 +117,9 @@ def test_get_current_position_invalid_mode(mocker: MockerFixture, under_test: Co

assert under_test.get_current_position("this is wrong") == (6, 6)

logger.warn.assert_called_with('"mode" should be either "as dict" or "as tuple". Returning the result as tuple')
logger.warn.assert_called_with(
'"mode" should be either "ResultMode.As_Dict" or "ResultMode.As_Tuple". ' "Returning the result as tuple"
)


def test_move_cursor_to(mocker: MockerFixture, under_test: CommandKeywords):
Expand Down
Loading

0 comments on commit d8c6e0b

Please sign in to comment.