diff --git a/.pylintrc b/.pylintrc index e6d1055..efc55fc 100644 --- a/.pylintrc +++ b/.pylintrc @@ -1,13 +1,7 @@ [pylint.messages_control] disable = - C0303, # Trailing whitespace - C0103, # Variable names - C0305, # Trailing newlines - C0304, # Missing final line C0301, # Line too long I1101, E1101, # C-modules members - W0621, # Redefine outer name - R0913 # Too many arguments + R0913, # Too many arguments + R0914 # Too many local variables -[MASTER] -ignore-paths = ^tests/ # Ignore the tests folder diff --git a/ITK_dev_shared_components/SAP/gridview_util.py b/ITK_dev_shared_components/SAP/gridview_util.py index f927675..305e081 100644 --- a/ITK_dev_shared_components/SAP/gridview_util.py +++ b/ITK_dev_shared_components/SAP/gridview_util.py @@ -1,4 +1,4 @@ -"""This module provides static functions to peform common tasks with SAP GuiGridView COM objects.""" +"""This module provides static functions to perform common tasks with SAP GuiGridView COM objects.""" def scroll_entire_table(grid_view, return_to_top=False) -> None: """This function scrolls through the entire table to load all cells. @@ -9,11 +9,13 @@ def scroll_entire_table(grid_view, return_to_top=False) -> None: Returns: _type_: _description_ - """ + """ + if grid_view.RowCount == 0 or grid_view.VisibleRowCount == 0: + return for i in range(0, grid_view.RowCount, grid_view.VisibleRowCount): grid_view.FirstVisibleRow = i - + if return_to_top: grid_view.FirstVisibleRow = 0 @@ -28,7 +30,7 @@ def get_all_rows(grid_view, pre_load=True) -> tuple[tuple[str]]: Returns: tuple[tuple[str]]: A 2D tuple of all cell values in the gridview. - """ + """ if pre_load: scroll_entire_table(grid_view, True) @@ -43,9 +45,9 @@ def get_all_rows(grid_view, pre_load=True) -> tuple[tuple[str]]: for c in columns: v = grid_view.GetCellValue(r, c) row_data.append(v) - + output.append(tuple(row_data)) - + return tuple(output) @@ -60,7 +62,7 @@ def get_row(grid_view, row:int, scroll_to_row=False) -> tuple[str]: Returns: tuple[str]: A tuple of the row's data. - """ + """ if scroll_to_row: grid_view.FirstVisibleRow = row @@ -83,7 +85,7 @@ def iterate_rows(grid_view) -> tuple[str]: Yields: tuple[str]: A tuple of the next row's data. - """ + """ row = 0 while row < grid_view.RowCount: @@ -104,7 +106,7 @@ def get_column_titles(grid_view) -> tuple[str]: Returns: tuple[str]: A tuple of the gridview's column titles. - """ + """ return tuple(grid_view.GetColumnTitles(c)[0] for c in grid_view.ColumnOrder) @@ -123,7 +125,7 @@ def find_row_index_by_value(grid_view, column:str, value:str) -> int: Returns: int: The index of the first row which column value matches the given value. - """ + """ if column not in grid_view.ColumnOrder: raise ValueError(f"Column '{column}' not in grid_view") @@ -132,14 +134,14 @@ def find_row_index_by_value(grid_view, column:str, value:str) -> int: # Only scroll when row isn't visible if not grid_view.FirstVisibleRow <= row <= grid_view.FirstVisibleRow + grid_view.VisibleRowCount-1: grid_view.FirstVisibleRow = row - + if grid_view.GetCellValue(row, column) == value: return row - + return -1 -def find_all_row_indecies_by_value(grid_view, column:str, value:str) -> list[int]: - """Find all indecies of the rows where the given column's value +def find_all_row_indices_by_value(grid_view, column:str, value:str) -> list[int]: + """Find all indices of the rows where the given column's value match the given value. Returns an empty list if no row is found. Args: @@ -151,8 +153,8 @@ def find_all_row_indecies_by_value(grid_view, column:str, value:str) -> list[int ValueError: If the column name doesn't exist in the grid view. Returns: - list[int]: A list of row indecies where the value matches. - """ + list[int]: A list of row indices where the value matches. + """ if column not in grid_view.ColumnOrder: raise ValueError(f"Column '{column}' not in grid_view") @@ -162,39 +164,8 @@ def find_all_row_indecies_by_value(grid_view, column:str, value:str) -> list[int # Only scroll when row isn't visible if not grid_view.FirstVisibleRow <= row <= grid_view.FirstVisibleRow + grid_view.VisibleRowCount-1: grid_view.FirstVisibleRow = row - + if grid_view.GetCellValue(row, column) == value: rows.append(row) - - return rows - - - - -if __name__=='__main__': - import win32com.client - - SAP = win32com.client.GetObject("SAPGUI") - app = SAP.GetScriptingEngine - connection = app.Connections(0) - session = connection.Sessions(0) - - table = session.findById("wnd[0]/usr/cntlGRID1/shellcont/shell") - - rows = find_all_row_indecies_by_value(table, "ZZ_PARTNER", '15879880') - print(rows) - table.setCurrentCell(rows[0], "ZZ_PARTNER") - - # print(get_row(table, 1, True)) - - # scroll_entire_table(table) - - # data = get_all_rows(table) - # print(len(data), len(data[0])) - - # for r in iterate_rows(table): - # print(r) - - # print(get_column_titles(table)) - + return rows diff --git a/ITK_dev_shared_components/SAP/multi_session.py b/ITK_dev_shared_components/SAP/multi_session.py index f1ce075..c7310ee 100644 --- a/ITK_dev_shared_components/SAP/multi_session.py +++ b/ITK_dev_shared_components/SAP/multi_session.py @@ -1,17 +1,19 @@ -"""This module provides static function to handle multiple sessions of SAP. +"""This module provides static functions to handle multiple sessions of SAP. Using this module you can spawn multiple sessions and automatically execute a function in parallel on the sessions.""" import time import threading +import math from typing import Callable + import pythoncom import win32com.client import win32gui def run_with_session(session_index:int, func:Callable, args:tuple) -> None: - """Run a function in a sepcific session based on the sessions index. - This function is meant to be run inside a seperate thread. + """Run a function in a specific session based on the sessions index. + This function is meant to be run inside a separate thread. The function must take a session object as its first argument. Note that this function will not spawn the sessions before running, use spawn_sessions to do that. @@ -19,8 +21,8 @@ def run_with_session(session_index:int, func:Callable, args:tuple) -> None: pythoncom.CoInitialize() - SAP = win32com.client.GetObject("SAPGUI") - app = SAP.GetScriptingEngine + sap = win32com.client.GetObject("SAPGUI") + app = sap.GetScriptingEngine connection = app.Connections(0) session = connection.Sessions(session_index) @@ -28,6 +30,7 @@ def run_with_session(session_index:int, func:Callable, args:tuple) -> None: pythoncom.CoUninitialize() + def run_batch(func:Callable, args:tuple[tuple], num_sessions=6) -> None: """Run a function in parallel sessions. The function will be run {num_sessions} times with args[i] as input. @@ -40,7 +43,7 @@ def run_batch(func:Callable, args:tuple[tuple], num_sessions=6) -> None: for i in range(num_sessions): t = ExThread(target=run_with_session, args=(i, func, args[i])) threads.append(t) - + for t in threads: t.start() for t in threads: @@ -49,6 +52,7 @@ def run_batch(func:Callable, args:tuple[tuple], num_sessions=6) -> None: if t.error: raise t.error + def run_batches(func:Callable, args:tuple[tuple], num_sessions=6): """Run a function in parallel batches. This function runs the input function for each set of arguments in args. @@ -62,16 +66,26 @@ def run_batches(func:Callable, args:tuple[tuple], num_sessions=6): batch = args[b:b+num_sessions] run_batch(func, args, len(batch)) -def spawn_sessions(num_sessions=6) -> list: + +def spawn_sessions(num_sessions=6) -> tuple: """A function to spawn multiple sessions of SAP. This function will attempt to spawn the desired number of sessions. - If the current number of open sessions exceeds the desired number of sessions + If the current number of already open sessions exceeds the desired number of sessions the already open sessions will not be closed to match the desired number. The number of sessions must be between 1 and 6. - Returns a list of all open sessions. + + Args: + num_sessions: The number of sessions desired. Defaults to 6. + + Raises: + ValueError: If the number of sessions is not between 1 and 6. + + Returns: + tuple: A tuple of all currently open sessions. """ - SAP = win32com.client.GetObject("SAPGUI") - app = SAP.GetScriptingEngine + + sap = win32com.client.GetObject("SAPGUI") + app = sap.GetScriptingEngine connection = app.Connections(0) session = connection.Sessions(0) @@ -82,25 +96,17 @@ def spawn_sessions(num_sessions=6) -> list: for _ in range(num_sessions - connection.Sessions.count): session.CreateSession() - + # Wait for the sessions to spawn while connection.Sessions.count < num_sessions: time.sleep(0.1) - sessions = list(connection.Sessions) + sessions = tuple(connection.Sessions) num_sessions = len(sessions) - if num_sessions == 1: - c = 1 - elif num_sessions <= 4: - c = 2 - elif num_sessions <= 6: - c = 3 - - if num_sessions < 3: - r = 1 - else: - r = 2 + # Calculate number of columns and rows + c = math.ceil(math.sqrt(num_sessions)) + r = math.ceil(num_sessions / c) w, h = 1920//c, 1040//r @@ -111,20 +117,22 @@ def spawn_sessions(num_sessions=6) -> list: x = i % c * w y = i // c * h win32gui.MoveWindow(hwnd, x, y, w, h, True) - + return sessions -def get_all_SAP_sessions() -> tuple: + +def get_all_SAP_sessions() -> tuple: # pylint: disable=invalid-name """Returns a tuple of all open SAP sessions (on connection index 0). Returns: tuple: A tuple of SAP GuiSession objects. """ - SAP = win32com.client.GetObject("SAPGUI") - app = SAP.GetScriptingEngine + sap = win32com.client.GetObject("SAPGUI") + app = sap.GetScriptingEngine connection = app.Connections(0) return tuple(connection.Sessions) + class ExThread(threading.Thread): """A thread with a handle to get an exception raised inside the thread: ExThread.error""" def __init__(self, *args, **kwargs): @@ -136,5 +144,3 @@ def run(self): self._target(*self._args, **self._kwargs) except Exception as e: # pylint: disable=broad-exception-caught self.error = e - - diff --git a/ITK_dev_shared_components/SAP/opret_kundekontakt.py b/ITK_dev_shared_components/SAP/opret_kundekontakt.py index adc8168..f84b235 100644 --- a/ITK_dev_shared_components/SAP/opret_kundekontakt.py +++ b/ITK_dev_shared_components/SAP/opret_kundekontakt.py @@ -1,12 +1,12 @@ -"""This module provides a single function to conviniently peform the action 'opret-kundekontaker' in SAP.""" +"""This module provides a single function to conveniently peform the action 'opret-kundekontaker' in SAP.""" from typing import Literal import win32clipboard from ITK_dev_shared_components.SAP import tree_util -def opret_kundekontakter(session, fp:str, aftaler:list[str] | None, - art:Literal[' ', 'Automatisk', 'Fakturagrundlag', 'Fuldmagt ifm. værge', 'Konverteret', 'Myndighedshenvend.', 'Orientering', 'Returpost', 'Ringeaktivitet', 'Skriftlig henvend.', 'Telefonisk henvend.'], +def opret_kundekontakter(session, fp:str, aftaler:list[str] | None, + art:Literal[' ', 'Automatisk', 'Fakturagrundlag', 'Fuldmagt ifm. værge', 'Konverteret', 'Myndighedshenvend.', 'Orientering', 'Returpost', 'Ringeaktivitet', 'Skriftlig henvend.', 'Telefonisk henvend.'], notat:str, lock=None) -> None: """Creates a kundekontakt on the given FP and aftaler. @@ -41,11 +41,11 @@ def opret_kundekontakter(session, fp:str, aftaler:list[str] | None, # Go to editor and paste (lock if multithreaded) session.findById("wnd[0]/usr/subNOTICE:SAPLEENO:1002/btnEENO_TEXTE-EDITOR").press() - if lock: + if lock: lock.acquire() - _setClipboard(notat) + _set_clipboard(notat) session.findById("wnd[0]/tbar[1]/btn[9]").press() - if lock: + if lock: lock.release() # Back and save @@ -53,21 +53,13 @@ def opret_kundekontakter(session, fp:str, aftaler:list[str] | None, session.findById("wnd[0]/tbar[0]/btn[11]").press() -def _setClipboard(text:str) -> None: +def _set_clipboard(text:str) -> None: + """Private function to set text to the clipboard. + + Args: + text: Text to set to clipboard. + """ win32clipboard.OpenClipboard() win32clipboard.EmptyClipboard() win32clipboard.SetClipboardText(text) win32clipboard.CloseClipboard() - - -if __name__ == '__main__': - from ITK_dev_shared_components.SAP import multi_session - from datetime import datetime - - session = multi_session.spawn_sessions(1)[0] - fp = '25564617' - aftaler = ['2291987', '2421562', '2311094'] - art = 'Orientering' - notat = 'Test '+ str(datetime.now()) - - opret_kundekontakter(session, fp, aftaler, 'Automatisk', notat) \ No newline at end of file diff --git a/ITK_dev_shared_components/SAP/sap_login.py b/ITK_dev_shared_components/SAP/sap_login.py index c2c98f5..8661787 100644 --- a/ITK_dev_shared_components/SAP/sap_login.py +++ b/ITK_dev_shared_components/SAP/sap_login.py @@ -1,4 +1,5 @@ -"""This module provides a functions to open SAP GUI.""" +"""This module provides functions to handle opening and closing SAP Gui +as well as a function to change user passwords.""" import os import pathlib @@ -7,6 +8,7 @@ from selenium import webdriver from selenium.webdriver.common.by import By import pywintypes +import win32com.client from ITK_dev_shared_components.SAP import multi_session @@ -43,8 +45,7 @@ def login_using_portal(username:str, password:str): _wait_for_download() driver.quit() - - _wait_for_sap_to_open() + _wait_for_sap_session(10) def _wait_for_download(): @@ -60,21 +61,26 @@ def _wait_for_download(): path = os.path.join(downloads_folder, file) os.startfile(path) return - + time.sleep(0.5) raise TimeoutError(f".SAP file not found in {downloads_folder}") -def login_using_cli(username: str, password: str, client='751', system='P02') -> None: +def login_using_cli(username: str, password: str, client:str='751', system:str='P02', timeout:int=10) -> None: """Open and login to SAP with commandline expressions. Args: - username (str): AZ username - password (str): password - client (str, optional): Kommune ID (Aarhus = 751). Defaults to '751'. - system (str, optional): Environment SID (e.g. P02 = 'KMD OPUS Produktion [P02]'). Defaults to 'P02'. - """ - + username: AZ username + password: password + client: Kommune ID (Aarhus = 751). Defaults to '751'. + system: Environment SID (e.g. P02 = 'KMD OPUS Produktion [P02]'). Defaults to 'P02'. + timeout: The time in seconds to wait for SAP Logon to start. Defaults to 10. + + Raises: + TimeoutError: If SAP doesn't start within timeout limit. + ValueError: If SAP is unable to log in using the given credentials. + """ + command_args = [ r"C:\Program Files (x86)\SAP\FrontEnd\SAPgui\sapshcut.exe", f"-system={system}", @@ -84,17 +90,21 @@ def login_using_cli(username: str, password: str, client='751', system='P02') -> ] subprocess.run(command_args, check=False) + _wait_for_sap_session(timeout) + if not _check_for_splash_screen(): + raise ValueError("Unable to log in. Please check username and password.") - _wait_for_sap_to_open() +def _wait_for_sap_session(timeout:int) -> None: + """Check every second if the SAP Gui scripting engine is available until timeout is reached. -def _wait_for_sap_to_open() -> None: - """Check every second for 10 seconds if the SAP Gui scripting engine is available. + Args: + timeout: The time in seconds to wait for SAP Logon to start. Defaults to 10. Raises: - TimeoutError: If SAP doesn't start within 10 seconds. - """ - for _ in range(10): + TimeoutError: If SAP doesn't start within timeout limit. + """ + for _ in range(timeout): time.sleep(1) try: sessions = multi_session.get_all_SAP_sessions() @@ -103,17 +113,82 @@ def _wait_for_sap_to_open() -> None: except pywintypes.com_error: pass - raise TimeoutError("SAP didn't respond within 10 seconds.") + raise TimeoutError(f"SAP didn't respond within timeout limit: {timeout} seconds.") -def kill_sap(): - """Kills all SAP processes currently running.""" - os.system("taskkill /F /IM saplogon.exe") +def _check_for_splash_screen() -> bool: + """Check if the splash screen image is currently present. + + Returns: + bool: True if the splash screen image is currently present. + """ + session = multi_session.get_all_SAP_sessions()[0] + image = session.findById("wnd[0]/usr/cntlIMAGE_CONTAINER/shellcont/shell/shellcont[1]/shell", False) + + return image is not None + +def change_password(username:str, old_password:str, new_password:str, + client:str='751', + system:str='...KMD OPUS Produktion [P02]', + timeout:int=10) -> None: + """Change the password of a user in SAP Gui. Closes SAP when done. + + Args: + username: The username of the user. + old_password: The current password of the user. + new_password: The new password to change to. + client: The client number. Defaults to '751'. + system: The description string of the connection as displayed in SAP Logon. Defaults to '...KMD OPUS Produktion [P02]'. + timeout: The time in seconds to wait for SAP Logon to start. Defaults to 10. + + Raises: + TimeoutError: If the connection couldn't be established within the timeout limit. + ValueError: If the current credentials are not valid or if the password can't be changed. + ValueError: If the new password is not valid. + + """ + + subprocess.Popen(r"C:\Program Files (x86)\SAP\FrontEnd\SAPgui\saplogon.exe") #pylint: disable=consider-using-with + + # Wait for SAP Logon to open + for _ in range(timeout): + time.sleep(1) + try: + sap = win32com.client.GetObject("SAPGUI") + app = sap.GetScriptingEngine + app.OpenConnection(system) + break + except pywintypes.com_error: + pass + else: + raise TimeoutError(f"SAP Logon didn't open within timeout limit: {timeout} seconds.") + + session = multi_session.get_all_SAP_sessions()[0] + + # Enter credentials + session.findById("wnd[0]/usr/txtRSYST-MANDT").text = client + session.findById("wnd[0]/usr/txtRSYST-BNAME").text = username + session.findById("wnd[0]/usr/pwdRSYST-BCODE").text = old_password + session.findById("wnd[0]/tbar[1]/btn[5]").press() + + # Check status bar + status_bar = session.findById("wnd[0]/sbar") + if status_bar.MessageType != 'S': + text = status_bar.Text + kill_sap() + raise ValueError(f"Password change was blocked: {text}") + + # Enter new password + session.findById("wnd[1]/usr/pwdRSYST-NCODE").text = new_password + session.findById("wnd[1]/usr/pwdRSYST-NCOD2").text = new_password + session.findById("wnd[1]/tbar[0]/btn[0]").press() + + if not _check_for_splash_screen(): + kill_sap() + raise ValueError("New password couldn't be set. Please check password requirements.") -if __name__=="__main__": - # user = "az12345" - # password = "Hunter2" - # login_using_portal(user, password) - # login_using_cli(user, password) kill_sap() +def kill_sap(): + """Kills all SAP processes currently running.""" + os.system("taskkill /F /IM saplogon.exe > NUL 2>&1") diff --git a/ITK_dev_shared_components/SAP/sap_util.py b/ITK_dev_shared_components/SAP/sap_util.py index 698d17a..8235c07 100644 --- a/ITK_dev_shared_components/SAP/sap_util.py +++ b/ITK_dev_shared_components/SAP/sap_util.py @@ -1,7 +1,7 @@ """This module provides miscellaneous static functions to peform common tasks in SAP.""" def print_all_descendants(container, max_depth=-1, indent=0): - """Prints the object and all of its decendants recursivly + """Prints the object and all of its descendants recursively to the console. Args: @@ -32,4 +32,4 @@ def print_all_descendants(container, max_depth=-1, indent=0): usr = session.FindById('/app/con[0]/ses[0]/wnd[0]/usr') - print_all_descendants(usr) \ No newline at end of file + print_all_descendants(usr) diff --git a/ITK_dev_shared_components/SAP/tree_util.py b/ITK_dev_shared_components/SAP/tree_util.py index 034186e..71730c9 100644 --- a/ITK_dev_shared_components/SAP/tree_util.py +++ b/ITK_dev_shared_components/SAP/tree_util.py @@ -2,70 +2,70 @@ def get_node_key_by_text(tree, text: str, fuzzy=False) -> str: """Get the node key of a node based on its text. - tree: A SAP GuiTree object. - text: The text to search for. - fuzzy: Whether to check if the node text just contains the search text. + + Args: + tree: A SAP GuiTree object. + text: The text to search for. + fuzzy: Whether to check if the node text just contains the search text. + + Raises: + ValueError: If no node is found with the given text. + + Returns: + str: The node key of the found node. """ for key in tree.GetAllNodeKeys(): t = tree.GetNodeTextByKey(key) if t == text or (fuzzy and text in t): return key - + raise ValueError(f"No node with the text '{text}' was found.") + def get_item_by_text(tree, text: str, fuzzy=False) -> tuple[str,str]: """Get the node key and item name of an item based on its text. - tree: A SAP GuiTree object. - text: The text to search for. - fuzzy: Whether to check if the item text just contains the search text. + + Args: + tree: A SAP GuiTree object. + text: The text to search for. + fuzzy: Whether to check if the item text just contains the search text. + + Raises: + ValueError: If no tem is found with the given text. + + Returns: + tuple[str,str]: The node key and item name of the found item. """ for key in tree.GetAllNodeKeys(): for name in tree.GetColumnNames(): t = tree.GetItemText(key, name) - + if t == text or (fuzzy and text in t): return (key, name) - + raise ValueError(f"No item with the text '{text}' was found.") + def check_all_check_boxes(tree) -> None: """Find and check all checkboxes in the tree. - tree: A SAP GuiTree object. + + Args: + tree: A SAP GuiTree object. """ for key in tree.GetAllNodeKeys(): for name in tree.GetColumnNames(): if tree.GetItemType(key, name) == 3: tree.ChangeCheckBox(key, name, True) + def uncheck_all_check_boxes(tree) -> None: """Find and uncheck all checkboxes in the tree. - tree: A SAP GuiTree object. + + Args: + tree: A SAP GuiTree object. """ for key in tree.GetAllNodeKeys(): for name in tree.GetColumnNames(): if tree.GetItemType(key, name) == 3: tree.ChangeCheckBox(key, name, False) - - - - -if __name__ == '__main__': - from ITK_dev_shared_components.SAP import multi_session - - session = multi_session.spawn_sessions(1)[0] - - tree = session.findById('/app/con[0]/ses[0]/wnd[1]/usr/cntlCONTAINER_PSOBKEY/shellcont/shell/shellcont[1]/shell[1]') - - # print([tree.GetNodeTextByKey(key) for key in tree.GetAllNodeKeys()]) - # print([tree.GetItemText(key, '&Hierarchy') for key in tree.GetAllNodeKeys()]) - - # print(list(tree.GetColumnNames())) - - # print(tree.GetItemText(' 2', '&Hierarchy')) - - # key, name = get_item_by_text(tree, '2291987', True) - # tree.ChangeCheckBox(key, name, True) - - check_all_check_boxes(tree) - uncheck_all_check_boxes(tree) \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/run_tests.bat b/tests/run_tests.bat new file mode 100644 index 0000000..7b7dd49 --- /dev/null +++ b/tests/run_tests.bat @@ -0,0 +1,28 @@ +@echo off + +:: Change dir to parent dir +echo Changing directory... +cd /d %~dp0.. + + +choice /C YN /M "Do you want to reset venv?" +if errorlevel 2 ( + echo Activating excisting venv... + call venv\Scripts\activate + +) else ( + echo Removing old venv... + rmdir /s /q venv + + echo Setting up new venv... + python -m venv venv + call venv\Scripts\activate + + echo Installing package... + pip install . +) + +echo Running unit tests... +python -m unittest discover + +pause \ No newline at end of file diff --git a/tests/test_gridview_util.py b/tests/test_gridview_util.py index e1bd259..ebdc1b9 100644 --- a/tests/test_gridview_util.py +++ b/tests/test_gridview_util.py @@ -1,75 +1,93 @@ +"""Tests relating to the module SAP.gridview_util.""" + import unittest import os from ITK_dev_shared_components.SAP import gridview_util, sap_login, multi_session -class test_gridview_util(unittest.TestCase): +class TestGridviewUtil(unittest.TestCase): + """Tests relating to the module SAP.gridview_util.""" @classmethod def setUpClass(cls): + """Launch SAP and navigate to fmcacov on FP 25564617 (Test person).""" + sap_login.kill_sap() + user, password = os.environ['SAP Login'].split(';') sap_login.login_using_cli(user, password) session = multi_session.get_all_SAP_sessions()[0] session.startTransaction("fmcacov") session.findById("wnd[0]/usr/ctxtGPART_DYN").text = "25564617" - session.findById("wnd[0]").sendVKey(0) + session.findById("wnd[0]").sendVKey(0) @classmethod def tearDownClass(cls): sap_login.kill_sap() - + def setUp(self) -> None: + # Find SAP gridview (table) object for testing session = multi_session.get_all_SAP_sessions()[0] self.table = session.findById("wnd[0]/usr/tabsDATA_DISP/tabpDATA_DISP_FC1/ssubDATA_DISP_SCA:RFMCA_COV:0202/cntlRFMCA_COV_0100_CONT5/shellcont/shell") def test_scroll_entire_table(self): + """Test scroll_entire_table. Assume success if no errors.""" gridview_util.scroll_entire_table(self.table) gridview_util.scroll_entire_table(self.table, True) def test_get_all_rows(self): + """Test get all rows of table. + Assume success if any rows and columns are loaded. + """ result = gridview_util.get_all_rows(self.table) self.assertGreater(len(result), 0) self.assertGreater(len(result[0]), 0) - + def test_get_row(self): + """Test getting a single row. + Assume success if any columns are loaded. + """ result = gridview_util.get_row(self.table, 0, False) self.assertGreater(len(result), 0) result = gridview_util.get_row(self.table, 0, True) self.assertGreater(len(result), 0) - + def test_iterate_rows(self): + """Test iterating through all rows. + Assume success if any columns are loaded for each row. + """ for row in gridview_util.iterate_rows(self.table): self.assertGreater(len(row), 0) - + def test_get_column_titles(self): + """Test getting column titles. + Assume success if any titles are loaded. + """ result = gridview_util.get_column_titles(self.table) self.assertGreater(len(result), 0) - + def test_find_row_index_by_value(self): - result = gridview_util.find_row_index_by_value(self.table, "TXTU2", "Test Deltrans") - self.assertNotEqual(result, -1) + """Test finding a single row by column value.""" + # Test finding an actual value. + index = gridview_util.find_row_index_by_value(self.table, "TXTU2", "Test Deltrans") + self.assertNotEqual(index, -1) - result = gridview_util.find_row_index_by_value(self.table, "TXTU2", "Foo") - self.assertEqual(result, -1) + # Test NOT finding a wrong value. + index = gridview_util.find_row_index_by_value(self.table, "TXTU2", "Foo") + self.assertEqual(index, -1) + # Test error on wrong column name. with self.assertRaises(ValueError): gridview_util.find_row_index_by_value(self.table, "Foo", "Bar") - def test_find_all_row_indecies_by_value(self): - result = gridview_util.find_all_row_indecies_by_value(self.table, "TXTU2", "Gebyr") - self.assertGreater(len(result), 0) + """Test finding all rows by column value.""" + # Test finding an actual value. + indices = gridview_util.find_all_row_indices_by_value(self.table, "TXTU2", "Gebyr") + self.assertGreater(len(indices), 0) - result = gridview_util.find_all_row_indecies_by_value(self.table, "TXTU2", "Foo") - self.assertEqual(len(result), 0) + # Test NOT finding a wrong value. + indices = gridview_util.find_all_row_indices_by_value(self.table, "TXTU2", "Foo") + self.assertEqual(len(indices), 0) + # Test error on wrong column name. with self.assertRaises(ValueError): - gridview_util.find_all_row_indecies_by_value(self.table, "Foo", "Bar") - - - - - - - -if __name__ == '__main__': - unittest.main() \ No newline at end of file + gridview_util.find_all_row_indices_by_value(self.table, "Foo", "Bar") diff --git a/tests/test_multi_session.py b/tests/test_multi_session.py new file mode 100644 index 0000000..db55199 --- /dev/null +++ b/tests/test_multi_session.py @@ -0,0 +1,67 @@ +"""Tests relating to the module SAP.multi_session.""" + +import unittest +import os +import threading +from ITK_dev_shared_components.SAP import sap_login, multi_session, opret_kundekontakt + +class TestMultiSession(unittest.TestCase): + """Tests relating to the module SAP.multi_session.""" + + def setUp(self): + sap_login.kill_sap() + user, password = os.environ['SAP Login'].split(';') + sap_login.login_using_cli(user, password) + + def tearDown(self): + sap_login.kill_sap() + + def test_spawn_sessions(self): + """Test spawning of multiple sessions. + It should only be possible to spawn between 1-6 sessions. + Also test getting already open sessions. + """ + with self.assertRaises(ValueError): + multi_session.spawn_sessions(0) + + with self.assertRaises(ValueError): + multi_session.spawn_sessions(7) + + sessions = multi_session.spawn_sessions(1) + self.assertEqual(len(sessions), 1) + + sessions = multi_session.spawn_sessions(6) + self.assertEqual(len(sessions), 6) + + sessions = multi_session.get_all_SAP_sessions() + self.assertEqual(len(sessions), 6) + + def test_run_batches(self): + """Test running a task in parallel in multiple sessions. + This also tests: + run_batch, run_with_session, ExThread + """ + + num_sessions = 6 + + multi_session.spawn_sessions(num_sessions) + + #Data + lock = threading.Lock() + + data = [ + ("25564617", None, 'Orientering', f"Hej {i}", lock) for i in range(12) + ] + + # Test with 12 cases + multi_session.run_batches(opret_kundekontakt.opret_kundekontakter, data, num_sessions) + + # Test with 5 cases + multi_session.run_batches(opret_kundekontakt.opret_kundekontakter, data[0:5], num_sessions) + + # Test with 1 case + multi_session.run_batches(opret_kundekontakt.opret_kundekontakter, data[0:1], num_sessions) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_opret_kundekontakt.py b/tests/test_opret_kundekontakt.py new file mode 100644 index 0000000..7452b30 --- /dev/null +++ b/tests/test_opret_kundekontakt.py @@ -0,0 +1,36 @@ +"""Test relating to the module SAP.opret_kundekontakt.""" + +import unittest +import os +from ITK_dev_shared_components.SAP import sap_login, multi_session, opret_kundekontakt + +class TestOpretKundekontakt(unittest.TestCase): + """Test relating to the module SAP.opret_kundekontakt.""" + def setUp(self): + sap_login.kill_sap() + user, password = os.environ['SAP Login'].split(';') + sap_login.login_using_cli(user, password) + + def tearDown(self): + sap_login.kill_sap() + + + def test_opret_kundekontakt(self): + """Test the function opret_kundekontakter.""" + fp = "25564617" + aftaler = ("2291987", "1990437", "1473781") + + session = multi_session.get_all_SAP_sessions()[0] + + # Test with 3 aftaler + opret_kundekontakt.opret_kundekontakter(session, fp, aftaler, 'Orientering', "Test 1") + + # Test with 1 aftale + opret_kundekontakt.opret_kundekontakter(session, fp, aftaler[0:1], 'Automatisk', "Test 2") + + # Test with 0 aftaler + opret_kundekontakt.opret_kundekontakter(session, fp, None, 'Returpost', "Test 3") + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_sap_login.py b/tests/test_sap_login.py new file mode 100644 index 0000000..bdad59b --- /dev/null +++ b/tests/test_sap_login.py @@ -0,0 +1,56 @@ +"""Tests relating to the module SAP.sap_login.""" + +import unittest +from tkinter import simpledialog + +from ITK_dev_shared_components.SAP import sap_login + +class TestSapLogin(unittest.TestCase): + """Tests relating to the module SAP.sap_login.""" + + @classmethod + def setUpClass(cls) -> None: + """Show popups that asks for username, password and new password + used in the following tests. + """ + cls.username = simpledialog.askstring("Enter username", "Enter the SAP username to be used in tests.") + cls.password = simpledialog.askstring("Enter password", "Enter the SAP password to be used in tests.") + cls.new_password = simpledialog.askstring("Enter password", "Enter the new password to be used in tests.\nRemember to write down the new password!\nLeave empty to skip test_change_password.") + + def setUp(self) -> None: + sap_login.kill_sap() + + def tearDown(self) -> None: + sap_login.kill_sap() + + def test_login_with_cli(self): + """Test login using the SAP cli interface. + Username and password is found in a environment variable. + """ + sap_login.login_using_cli(self.username, self.password) + + sap_login.kill_sap() + + with self.assertRaises(ValueError): + sap_login.login_using_cli("Foo", "Bar") + + def test_change_password(self): + """Test the function change password. + Due to a limit in SAP you can only run this function once per day. + If no new_password is entered in the setup + """ + if not self.new_password: + raise ValueError("Test not run because new_password was missing.") + + sap_login.change_password(self.username, self.password, self.new_password) + self.password = self.new_password + + with self.assertRaises(ValueError): + sap_login.change_password(self.username, "Foo", self.new_password) + + with self.assertRaises(ValueError): + sap_login.change_password(self.username, self.password, "") + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_tree_util.py b/tests/test_tree_util.py new file mode 100644 index 0000000..4375cd8 --- /dev/null +++ b/tests/test_tree_util.py @@ -0,0 +1,91 @@ +"""Tests relating to the module SAP.tree_util.""" + +import unittest +import os + +from ITK_dev_shared_components.SAP import tree_util, sap_login, multi_session + +class TestTreeUtil(unittest.TestCase): + """Tests relating to the module SAP.tree_util.""" + @classmethod + def setUpClass(cls): + """Launch SAP and navigate to fmcacov on FP 25564617.""" + sap_login.kill_sap() + + user, password = os.environ['SAP Login'].split(';') + sap_login.login_using_cli(user, password) + session = multi_session.get_all_SAP_sessions()[0] + session.startTransaction("fmcacov") + session.findById("wnd[0]/usr/ctxtGPART_DYN").text = "25564617" + session.findById("wnd[0]").sendVKey(0) + + + @classmethod + def tearDownClass(cls): + sap_login.kill_sap() + + + def test_get_node_key_by_text(self): + """Test get_node_key_by_test. + Test that strict search and fuzzy search works + and throws errors on nonsense input. + """ + session = multi_session.get_all_SAP_sessions()[0] + tree = session.findById("wnd[0]/shellcont/shell") + + result = tree_util.get_node_key_by_text(tree, "25564617") + self.assertEqual(result, "GP0000000001") + + result = tree_util.get_node_key_by_text(tree, "556461", True) + self.assertEqual(result, "GP0000000001") + + with self.assertRaises(ValueError): + tree_util.get_node_key_by_text(tree, "Foo Bar") + + with self.assertRaises(ValueError): + tree_util.get_node_key_by_text(tree, "Foo Bar", True) + + + def test_get_item_by_text(self): + """Test get_item_by_text. + Test that strict search and fuzzy search works + and throws errors on nonsense input. + """ + session = multi_session.get_all_SAP_sessions()[0] + tree = session.findById("wnd[0]/shellcont/shell") + + result = tree_util.get_item_by_text(tree, "25564617") + self.assertEqual(result, ("GP0000000001", "Column1")) + + result = tree_util.get_item_by_text(tree, "556461", True) + self.assertEqual(result, ("GP0000000001", "Column1")) + + with self.assertRaises(ValueError): + tree_util.get_item_by_text(tree, "Foo Bar") + + with self.assertRaises(ValueError): + tree_util.get_item_by_text(tree, "Foo Bar", True) + + + def test_check_uncheck_all_check_boxes(self): + """Test check_all_check_boxes and uncheck_all_check_boxes.""" + # Open popup with tree containing many checkboxes. + session = multi_session.get_all_SAP_sessions()[0] + session.findById("wnd[0]/shellcont/shell").nodeContextMenu("GP0000000001") + session.findById("wnd[0]/shellcont/shell").selectContextMenuItem("FLERE") + + # Test on tree with checkboxes + tree = session.findById("wnd[1]/usr/cntlCONTAINER_PSOBKEY/shellcont/shell/shellcont[1]/shell[1]") + tree_util.check_all_check_boxes(tree) + tree_util.uncheck_all_check_boxes(tree) + + session.findById("wnd[1]/usr/cntlCONTAINER_PSOBKEY/shellcont/shell/shellcont[1]/shell[0]").pressButton("CANCEL") + + # Test on tree without checkboxes + tree = session.findById("wnd[0]/shellcont/shell") + tree_util.check_all_check_boxes(tree) + tree_util.uncheck_all_check_boxes(tree) + + +if __name__ == '__main__': + unittest.main()