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

(#31) Setup the emulator model #107

Merged
merged 19 commits into from
Jun 14, 2023
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
57 changes: 51 additions & 6 deletions Mainframe3270/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,14 @@
from robot.utils import ConnectionCache
from robotlibcore import DynamicCore

from Mainframe3270.keywords import (AssertionKeywords, CommandKeywords,
ConnectionKeywords, ReadWriteKeywords,
ScreenshotKeywords, WaitAndTimeoutKeywords)
from Mainframe3270.keywords import (
AssertionKeywords,
CommandKeywords,
ConnectionKeywords,
ReadWriteKeywords,
ScreenshotKeywords,
WaitAndTimeoutKeywords,
)
from Mainframe3270.py3270 import Emulator
from Mainframe3270.utils import convert_timeout
from Mainframe3270.version import VERSION
Expand Down Expand Up @@ -68,6 +73,46 @@ class Mainframe3270(DynamicCore):
| Switch Connection second # switchting the ocnnection using the alias
| Page Should Contain String Second String
| [Teardown] Close All Connections

= Changing the emulator model (experimental) =

By default, the library uses the emulator model 2, which is 24 rows by 80 columns.
You can, however, change the model globally when `importing` the library with the `model` argument
set to the model of your choice.

The basic models are 2, 3, 4, and 5. These models differ in their screen size as illustrated in this table:

| *3270 Model* | *Rows* | *Columns* |
| 2 | 24 | 80 |
| 3 | 32 | 80 |
| 4 | 43 | 80 |
| 5 | 27 | 132 |


They can be combined with the 3278 (monochrome green-screen) or 3279 (color) prefix, e.g. 3278-2 or 3279-2.

In addition to that, there is a -E suffix that indicates support for the [https://x3270.miraheze.org/wiki/3270_data_stream_protocol#extended|x3270 extended data stream].

You can find more information on emulator models on the [https://x3270.miraheze.org/wiki/3270_models|x3270 wiki].

In addition to setting the model globally, you can also set the model on the individual emulator basis by providing the model arguments to the `Open Connection`
or `Open Connection From Session File` keywords.

Here is an example for setting the emulator in the Open Connection keyword:

| Open Connection pub400.com extra_args=["-xrm", "*model: 4"]

And this is how you would set the emulator model in the Open Connection From Session File keyword:

| Open Connection From Session File /path/to/session/file

Where the content of the session file would be

| *hostname: pub400.com
| *model: 4


Note that this is an experimental feature, so not all models might work as expected.
"""

ROBOT_LIBRARY_SCOPE = "TEST SUITE"
Expand All @@ -81,6 +126,7 @@ def __init__(
wait_time_after_write: timedelta = timedelta(seconds=0),
img_folder: str = ".",
run_on_failure_keyword: str = "Take Screenshot",
model: str = "2",
) -> None:
"""
By default the emulator visibility is set to visible=True.
Expand All @@ -107,6 +153,7 @@ def __init__(
self.img_folder = img_folder
self._running_on_failure_keyword = False
self.register_run_on_failure_keyword(run_on_failure_keyword)
self.model = model
self.cache = ConnectionCache()
# When generating the library documentation with libdoc, BuiltIn.get_variable_value throws
# a RobotNotRunningError. Therefore, we catch it here to be able to generate the documentation.
Expand Down Expand Up @@ -159,8 +206,6 @@ def run_on_failure(self) -> None:
self._running_on_failure_keyword = True
BuiltIn().run_keyword(self.run_on_failure_keyword)
except Exception as error:
logger.warn(
f"Keyword '{self.run_on_failure_keyword}' could not be run on failure: {error}"
)
logger.warn(f"Keyword '{self.run_on_failure_keyword}' could not be run on failure: {error}")
finally:
self._running_on_failure_keyword = False
3 changes: 1 addition & 2 deletions Mainframe3270/keywords/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,4 @@
from Mainframe3270.keywords.connection import ConnectionKeywords # noqa: F401
from Mainframe3270.keywords.read_write import ReadWriteKeywords # noqa: F401
from Mainframe3270.keywords.screenshot import ScreenshotKeywords # noqa: F401
from Mainframe3270.keywords.wait_and_timeout import \
WaitAndTimeoutKeywords # noqa: F401
from Mainframe3270.keywords.wait_and_timeout import WaitAndTimeoutKeywords # noqa: F401
10 changes: 4 additions & 6 deletions Mainframe3270/keywords/assertions.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import re
from typing import List, Optional

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@samuelpcabral , I am sorry, I know you removed these line breaks, but they are inserted automatically by isort and I haven't yet figured out how to prevent the tool from doing this.

I hope this is not an issue

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

from Mainframe3270.librarycomponent import LibraryComponent


Expand Down Expand Up @@ -109,9 +111,7 @@ def page_should_not_contain_any_string(
| Page Should Not Contain Any Strings | ${list_of_string} | ignore_case=True |
| Page Should Not Contain Any Strings | ${list_of_string} | error_message=New error message |
"""
self._compare_all_list_with_screen_text(
list_string, ignore_case, error_message, should_match=False
)
self._compare_all_list_with_screen_text(list_string, ignore_case, error_message, should_match=False)

@keyword("Page Should Contain All Strings")
def page_should_contain_all_strings(
Expand All @@ -132,9 +132,7 @@ def page_should_contain_all_strings(
| Page Should Contain All Strings | ${list_of_string} | ignore_case=True |
| Page Should Contain All Strings | ${list_of_string} | error_message=New error message |
"""
self._compare_all_list_with_screen_text(
list_string, ignore_case, error_message, should_match=True
)
self._compare_all_list_with_screen_text(list_string, ignore_case, error_message, should_match=True)

@keyword("Page Should Not Contain All Strings")
def page_should_not_contain_all_strings(
Expand Down
2 changes: 2 additions & 0 deletions Mainframe3270/keywords/commands.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import time
from typing import Optional

from robot.api.deco import keyword

from Mainframe3270.librarycomponent import LibraryComponent


Expand Down
43 changes: 19 additions & 24 deletions Mainframe3270/keywords/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
import shlex
from os import name as os_name
from typing import List, Optional, Union

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

from Mainframe3270.librarycomponent import LibraryComponent
from Mainframe3270.py3270 import Emulator

Expand Down Expand Up @@ -62,7 +64,8 @@ def open_connection(
| Open Connection | Hostname | alias=my_first_connection |
"""
extra_args = self._process_args(extra_args)
connection = Emulator(self.visible, self.timeout, extra_args)
model = self._get_model_from_list_or_file(extra_args)
connection = Emulator(self.visible, self.timeout, extra_args, model or self.model)
host_string = f"{LU}@{host}" if LU else host
if self._port_in_extra_args(extra_args):
if port != 23:
Expand Down Expand Up @@ -93,6 +96,17 @@ def _process_args(args) -> list:
processed_args.append(arg)
return processed_args

@staticmethod
def _get_model_from_list_or_file(list_or_file):
pattern = re.compile(r"[wcxs3270.*]+model:\s*([327892345E-]+)")
match = None
if isinstance(list_or_file, list):
match = pattern.findall(str(list_or_file))
elif isinstance(list_or_file, os.PathLike) or isinstance(list_or_file, str):
with open(list_or_file) as file:
match = pattern.findall(file.read())
return None if not match else match[-1]

@staticmethod
def _port_in_extra_args(args) -> bool:
if not args:
Expand All @@ -103,9 +117,7 @@ def _port_in_extra_args(args) -> bool:
return False

@keyword("Open Connection From Session File")
def open_connection_from_session_file(
self, session_file: os.PathLike, alias: Optional[str] = None
) -> int:
def open_connection_from_session_file(self, session_file: os.PathLike, alias: Optional[str] = None) -> int:
"""Create a connection to an IBM3270 mainframe
using a [https://x3270.miraheze.org/wiki/Session_file|session file].

Expand Down Expand Up @@ -134,12 +146,12 @@ def open_connection_from_session_file(
"""
self._check_session_file_extension(session_file)
self._check_contains_hostname(session_file)
self._check_model(session_file)
model = self._get_model_from_list_or_file(session_file)
if os_name == "nt" and self.visible:
connection = Emulator(self.visible, self.timeout)
connection = Emulator(self.visible, self.timeout, model=model or self.model)
connection.connect(str(session_file))
else:
connection = Emulator(self.visible, self.timeout, [str(session_file)])
connection = Emulator(self.visible, self.timeout, [str(session_file)], model or self.model)
return self.cache.register(connection, alias)

def _check_session_file_extension(self, session_file):
Expand Down Expand Up @@ -169,23 +181,6 @@ def _check_contains_hostname(session_file):
"wc3270.hostname: myhost.com\n"
)

@staticmethod
def _check_model(session_file):
with open(session_file) as file:
pattern = re.compile(r"[wcxs3270.*]+model:\s*([327892345E-]+)")
match = pattern.findall(file.read())
if not match:
return
elif match[-1] == "2":
return
else:
raise ValueError(
f'Robot-Framework-Mainframe-3270-Library currently only supports model "2", '
f'the model you specified in your session file was "{match[-1]}". '
f'Please change it to "2", using either the session wizard if you are on Windows, '
f'or by editing the model resource like this "*model: 2"'
)

@keyword("Switch Connection")
def switch_connection(self, alias_or_index: Union[str, int]):
"""Switch the current connection to the one identified by index or alias. Indices are returned from
Expand Down
2 changes: 2 additions & 0 deletions Mainframe3270/keywords/read_write.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import time
from typing import Any, Optional

from robot.api.deco import keyword

from Mainframe3270.librarycomponent import LibraryComponent


Expand Down
9 changes: 4 additions & 5 deletions Mainframe3270/keywords/screenshot.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import os
import time

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

from Mainframe3270.librarycomponent import LibraryComponent


Expand Down Expand Up @@ -42,13 +44,10 @@ def take_screenshot(self, height: int = 410, width: int = 670, filename_prefix:
"""
extension = "html"
filename_sufix = round(time.time() * 1000)
filepath = os.path.join(
self.img_folder, "%s_%s.%s" % (filename_prefix, filename_sufix, extension)
)
filepath = os.path.join(self.img_folder, "%s_%s.%s" % (filename_prefix, filename_sufix, extension))
self.mf.save_screen(os.path.join(self.output_folder, filepath))
logger.write(
'<iframe src="%s" height="%s" width="%s"></iframe>'
% (filepath.replace("\\", "/"), height, width),
'<iframe src="%s" height="%s" width="%s"></iframe>' % (filepath.replace("\\", "/"), height, width),
level="INFO",
html=True,
)
Expand Down
6 changes: 3 additions & 3 deletions Mainframe3270/keywords/wait_and_timeout.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import time
from datetime import timedelta

from robot.api.deco import keyword
from robot.utils import secs_to_timestr

from Mainframe3270.librarycomponent import LibraryComponent
from Mainframe3270.utils import convert_timeout

Expand Down Expand Up @@ -75,9 +77,7 @@ def wait_field_detected(self) -> None:
self.mf.wait_for_field()

@keyword("Wait Until String")
def wait_until_string(
self, txt: str, timeout: timedelta = timedelta(seconds=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.

Expand Down
5 changes: 5 additions & 0 deletions Mainframe3270/librarycomponent.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from robot.utils import ConnectionCache

from Mainframe3270.py3270 import Emulator


Expand Down Expand Up @@ -66,3 +67,7 @@ def mf(self) -> Emulator:
@property
def output_folder(self):
return self.library.output_folder

@property
def model(self):
return self.library.model
Loading