Skip to content

Commit

Permalink
Merge pull request #115 from robinmatz/111
Browse files Browse the repository at this point in the history
#111 Read From Current Position keyword
  • Loading branch information
samuelpcabral authored Nov 6, 2023
2 parents ea4b133 + e20ba7c commit 522f2ff
Show file tree
Hide file tree
Showing 8 changed files with 104 additions and 56 deletions.
24 changes: 17 additions & 7 deletions Mainframe3270/keywords/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from robot.api.deco import keyword

from Mainframe3270.librarycomponent import LibraryComponent
from Mainframe3270.utils import coordinate_tuple_to_dict
from Mainframe3270.utils import coordinates_to_dict


class CommandKeywords(LibraryComponent):
Expand Down Expand Up @@ -85,8 +85,8 @@ def send_pf(self, pf: str) -> None:
self.mf.exec_command("PF({0})".format(pf).encode("utf-8"))
time.sleep(self.wait_time)

@keyword("Get Cursor Position")
def get_cursor_position(self, mode: str = "As Tuple") -> Union[tuple, dict]:
@keyword("Get Current Position")
def get_current_position(self, mode: str = "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,11 +97,21 @@ def get_cursor_position(self, mode: str = "As Tuple") -> Union[tuple, dict]:
| Get Cursor Position | As Dict | # Returns a position like {"xpos": 1, "ypos": 1} |
"""
result = self.mf.get_cursor_position()
ypos, xpos = self.mf.get_current_position()
if mode.lower() == "as dict":
return coordinate_tuple_to_dict(result)
return coordinates_to_dict(ypos, xpos)
elif mode.lower() == "as tuple":
return result
return ypos, xpos
else:
logger.warn('"mode" should be either "as dict" or "as tuple". Returning the result as tuple')
return result
return ypos, xpos

@keyword("Move Cursor To")
def move_cursor_to(self, ypos: int, xpos: int):
"""Moves the cursor to the specified ypos/xpos position. The coordinates are 1 based.
This keyword raises an error if the specified values exceed the Mainframe screen dimensions.
Example:
| Move Cursor To | 1 | 5 |
"""
self.mf.move_to(ypos, xpos)
14 changes: 11 additions & 3 deletions Mainframe3270/keywords/read_write.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from robot.api.deco import keyword

from Mainframe3270.librarycomponent import LibraryComponent
from Mainframe3270.utils import coordinate_tuple_to_dict
from Mainframe3270.utils import coordinates_to_dict


class ReadWriteKeywords(LibraryComponent):
Expand All @@ -20,6 +20,13 @@ def read(self, ypos: int, xpos: int, length: int) -> str:
"""
return self.mf.string_get(ypos, xpos, length)

@keyword("Read From Current Position")
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()
return self.mf.string_get(ypos, xpos, length)

@keyword("Read All Screen")
def read_all_screen(self) -> str:
"""Read the current screen and returns all content in one string.
Expand Down Expand Up @@ -48,11 +55,12 @@ 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:
| ${indices} | Find String | Abc | # Returns something like [(1, 8)]
| ${positions} | Get String Positions | Abc | # Returns something like [(1, 8)]
| ${positions} | Get String Positions | Abc | As Dict | # Returns something like [{"ypos": 1, "xpos": 8}]
"""
results = self.mf.get_string_positions(string, ignore_case)
if mode.lower() == "as dict":
return [coordinate_tuple_to_dict(r) for r in results]
return [coordinates_to_dict(ypos, xpos) for ypos, xpos in results]
elif mode.lower() == "as tuple":
return results
else:
Expand Down
11 changes: 5 additions & 6 deletions Mainframe3270/py3270.py
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +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)
# the screen's coordinates are 1 based, but the command is 0 based
xpos -= 1
ypos -= 1
Expand All @@ -445,7 +446,6 @@ def send_string(self, tosend, ypos=None, xpos=None):
terminal.
"""
if xpos and ypos:
self._check_limits(ypos, xpos)
self.move_to(ypos, xpos)
# escape double quotes in the data to send
tosend = tosend.decode("utf-8").replace('"', '"')
Expand Down Expand Up @@ -488,9 +488,8 @@ def search_string(self, string, ignore_case=False):
def get_string_positions(self, string, ignore_case=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."""
screen_content = self.read_all_screen().lower() if ignore_case else self.read_all_screen()
string = string.lower() if ignore_case else string
indices_object = re.finditer(re.escape(string), screen_content)
screen_content = self.read_all_screen()
indices_object = re.finditer(re.escape(string), screen_content, flags=0 if not ignore_case else re.IGNORECASE)
indices = [index.start() for index in indices_object]
# ypos and xpos should be returned 1-based
return [self._get_ypos_and_xpos_from_index(index + 1) for index in indices]
Expand Down Expand Up @@ -535,9 +534,9 @@ def fill_field(self, ypos, xpos, tosend, length):
def save_screen(self, file_path):
self.exec_command("PrintText(html,file,{0})".format(file_path).encode("utf-8"))

def get_cursor_position(self):
def get_current_position(self):
"""Returns the current cursor position as a tuple of 1 indexed integers."""
command = self.exec_command("Query(Cursor)")
command = self.exec_command(b"Query(Cursor)")
if len(command.data) != 1:
raise Exception(f'Cursor position returned an unexpected value: "{command.data}"')
list_of_strings = command.data[0].decode("utf-8").split(" ")
Expand Down
8 changes: 2 additions & 6 deletions Mainframe3270/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,5 @@ def convert_timeout(time):
return timestr_to_secs(time, round_to=None)


def coordinate_tuple_to_dict(coordinate_tuple):
if not isinstance(coordinate_tuple, tuple):
raise TypeError(f"Input must be instance of {tuple}")
if len(coordinate_tuple) != 2:
raise ValueError(f"Length of input must be 2, but was {len(coordinate_tuple)}")
return {"ypos": coordinate_tuple[0], "xpos": coordinate_tuple[1]}
def coordinates_to_dict(ypos: int, xpos: int):
return {"ypos": ypos, "xpos": xpos}
31 changes: 26 additions & 5 deletions atest/mainframe.robot
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ Exception Test Read
Run Keyword And Expect Error ${X_AXIS_EXCEEDED_EXPECTED_ERROR} Read 4 81 1
Run Keyword And Expect Error ${Y_AXIS_EXCEEDED_EXPECTED_ERROR} Read 25 48 34

Exception Test Read From Current Position
Run Keyword And Expect Error ${X_AXIS_EXCEEDED_EXPECTED_ERROR} Read From Current Position
... 81

Exception Test Write In Position
Run Keyword And Expect Error ${X_AXIS_EXCEEDED_EXPECTED_ERROR} Write In Position ${WRITE_TEXT} 10 81
Run Keyword And Expect Error ${Y_AXIS_EXCEEDED_EXPECTED_ERROR} Write In Position ${WRITE_TEXT} 25 10
Expand Down Expand Up @@ -112,6 +116,12 @@ Exception Test Wait Until String
Exception Test Wait Until String With Timeout
Verify Wait Until String With Timeout Wait Until String ${STRING_NON_EXISTENT} timeout=2

Exception Test Move Cursor To
Run Keyword And Expect Error ${Y_AXIS_EXCEEDED_EXPECTED_ERROR}
... Move Cursor To 25 1
Run Keyword And Expect Error ${X_AXIS_EXCEEDED_EXPECTED_ERROR}
... Move Cursor To 1 81

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

Expand Down Expand Up @@ -159,17 +169,28 @@ Test Page Should Not Contain String
Page Should Not Contain String ${WELCOME_WRONG_CASE}
Page Should Not Contain String ${STRING_NON_EXISTENT} ignore_case=${True}

Test Move Cursor To
Move Cursor To 5 5
${position} Get Current Position
Should Be Equal ${{ (5, 5) }} ${position}

Test Read
${read_text} Read 1 10 48
Should Be Equal As Strings ${WELCOME_TITLE} ${read_text}

Test Read From Current Position
Move Cursor To 4 48
${read_text} Read From Current Position 12
Should Be Equal As Strings Display name ${read_text}

Test Read All Screen
${screen_content} Read All Screen
Should Contain ${screen_content} i
Should Contain ${screen_content} c I
Should Not Contain ${screen_content} xyz

Test Write Bare
Move Cursor To 5 25
Write Bare ${WRITE_TEXT}
${read_text} Read 5 25 4
Take Screenshot
Expand Down Expand Up @@ -225,16 +246,16 @@ Test Send PF
Send PF 1
Page Should Contain String Function key not allowed.

Test Get Cursor Position
Test Get Current Position
Move Next Field
${position} Get Cursor Position
${position} Get Current Position
Should Be Equal ${{ (6, 25) }} ${position}
${position_as_dict} Get Cursor Position As Dict
${position_as_dict} Get Current Position As Dict
Should Be Equal ${{ {"xpos": 25, "ypos": 6} }} ${position_as_dict}
Write Bare AB
${position} Get Cursor Position
${position} Get Current Position
Should Be Equal ${{ (6, 27) }} ${position}
${position_as_dict} Get Cursor Position As Dict
${position_as_dict} Get Current Position As Dict
Should Be Equal ${{ {"xpos": 27, "ypos": 6} }} ${position_as_dict}

Test Get String Positions
Expand Down
44 changes: 35 additions & 9 deletions utest/Mainframe3270/keywords/test_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,22 +98,48 @@ def test_send_pf(mocker: MockerFixture, under_test: CommandKeywords):
Emulator.exec_command.assert_called_with("PF(5)".encode("utf-8"))


def test_get_current_cursor_position(mocker: MockerFixture, under_test: CommandKeywords):
mocker.patch("Mainframe3270.py3270.Emulator.get_cursor_position", return_value=(6, 6))
def test_get_current_position(mocker: MockerFixture, under_test: CommandKeywords):
mocker.patch("Mainframe3270.py3270.Emulator.get_current_position", return_value=(6, 6))

assert under_test.get_cursor_position() == (6, 6)
assert under_test.get_current_position() == (6, 6)


def test_get_current_cursor_position_as_dict(mocker: MockerFixture, under_test: CommandKeywords):
mocker.patch("Mainframe3270.py3270.Emulator.get_cursor_position", return_value=(6, 6))
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_cursor_position("as DiCt") == {"xpos": 6, "ypos": 6}
assert under_test.get_current_position("as DiCt") == {"xpos": 6, "ypos": 6}


def test_get_current_cursor_position_invalid_mode(mocker: MockerFixture, under_test: CommandKeywords):
mocker.patch("Mainframe3270.py3270.Emulator.get_cursor_position", return_value=(6, 6))
def test_get_current_position_invalid_mode(mocker: MockerFixture, under_test: CommandKeywords):
mocker.patch("Mainframe3270.py3270.Emulator.get_current_position", return_value=(6, 6))
mocker.patch("robot.api.logger.warn")

assert under_test.get_cursor_position("this is wrong") == (6, 6)
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')


def test_move_cursor_to(mocker: MockerFixture, under_test: CommandKeywords):
mocker.patch("Mainframe3270.py3270.Emulator.move_to")

under_test.move_cursor_to(5, 5)

Emulator.move_to.assert_called_with(5, 5)


@pytest.mark.parametrize(
("ypos", "xpos", "expected_error"),
[
(25, 1, "You have exceeded the y-axis limit of the mainframe screen"),
(1, 81, "You have exceeded the x-axis limit of the mainframe screen"),
],
)
def test_move_cursor_to_exceeds_ypos(
mocker: MockerFixture, under_test: CommandKeywords, ypos: int, xpos: int, expected_error: str
):
mocker.patch("Mainframe3270.py3270.Emulator.exec_command")

with pytest.raises(Exception, match=expected_error):
under_test.move_cursor_to(ypos, xpos)

Emulator.exec_command.assert_not_called()
18 changes: 3 additions & 15 deletions utest/Mainframe3270/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
from datetime import timedelta

import pytest

from Mainframe3270.utils import convert_timeout, coordinate_tuple_to_dict
from Mainframe3270.utils import convert_timeout, coordinates_to_dict


def test_convert_timeout_with_timedelta():
Expand All @@ -16,15 +14,5 @@ def test_convert_timeout_with_timestring():
assert timeout == 60.0


def test_coordinate_tuple_to_dict():
assert coordinate_tuple_to_dict((1, 5)) == {"ypos": 1, "xpos": 5}


def test_coordinate_tuple_to_dict_with_wrong_input():
with pytest.raises(TypeError, match="Input must be instance of <class 'tuple'>"):
coordinate_tuple_to_dict(1)


def test_coordinate_tuple_to_dict_with_invalid_length():
with pytest.raises(ValueError, match="Length of input must be 2, but was 3"):
coordinate_tuple_to_dict((2, 4, 6))
def test_coordinates_to_dict():
assert coordinates_to_dict(1, 5) == {"ypos": 1, "xpos": 5}
10 changes: 5 additions & 5 deletions utest/py3270/test_emulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,7 @@ def test_get_current_cursor_position(mocker: MockerFixture):
under_test = Emulator()

# result is 1 based, that is why we expect (6, 6)
assert under_test.get_cursor_position() == (6, 6)
assert under_test.get_current_position() == (6, 6)


def test_get_current_cursor_position_returns_unexpected_value(mocker: MockerFixture):
Expand All @@ -352,7 +352,7 @@ def test_get_current_cursor_position_returns_unexpected_value(mocker: MockerFixt
under_test = Emulator()

with pytest.raises(Exception, match="Cursor position returned an unexpected value"):
under_test.get_cursor_position()
under_test.get_current_position()


@pytest.mark.usefixtures("mock_windows")
Expand All @@ -366,7 +366,7 @@ def test_get_current_cursor_position_returns_unexpected_value(mocker: MockerFixt
("5", 131, [(1, 132)]),
],
)
def test_find_string(mocker: MockerFixture, model, index, expected):
def test_get_string_positions(mocker: MockerFixture, model, index, expected):
under_test = Emulator(model=model)
mocker.patch(
"Mainframe3270.py3270.Emulator.read_all_screen", return_value=_mock_return_all_screen(under_test, "abc", index)
Expand All @@ -375,7 +375,7 @@ def test_find_string(mocker: MockerFixture, model, index, expected):
assert under_test.get_string_positions("abc") == expected


def test_find_string_ignore_case(mocker: MockerFixture):
def test_get_string_positions_ignore_case(mocker: MockerFixture):
under_test = Emulator()
mocker.patch(
"Mainframe3270.py3270.Emulator.read_all_screen", return_value=_mock_return_all_screen(under_test, "ABC", 5)
Expand All @@ -384,7 +384,7 @@ def test_find_string_ignore_case(mocker: MockerFixture):
assert under_test.get_string_positions("aBc", True) == [(1, 6)]


def test_find_string_without_result(mocker: MockerFixture):
def test_get_string_positions_without_result(mocker: MockerFixture):
under_test = Emulator()
mocker.patch(
"Mainframe3270.py3270.Emulator.read_all_screen", return_value=_mock_return_all_screen(under_test, "abc", 1)
Expand Down

0 comments on commit 522f2ff

Please sign in to comment.