Search code examples
pythonsubprocesspytestpopenrpa

Automating CLI application which opens another GUI window


I have a program JLinkExe which I can start in Windows/Powershell:

& "C:\Program Files\SEGGER\JLink\JLink.exe" -nogui 1

Or in Linux/WSL:

JLinkExe -nogui 1

In both cases program starts, prints some data and presents a J-Link> prompt.

SEGGER J-Link Commander V7.88k (Compiled Jul  5 2023 15:02:18)
DLL version V7.88k, compiled Jul  5 2023 15:00:41

Connecting to J-Link via USB...O.K.
Firmware: J-Link STLink V21 compiled Aug 12 2019 10:29:20
Hardware version: V1.00
J-Link uptime (since boot): N/A (Not supported by this model)
S/N: 775087052
VTref=3.300V


Type "connect" to establish a target connection, '?' for help
J-Link>

At this point user needs to start inputting commands (each ending with enter/newline) eventually leading to flashing of some firmware to the target embedded system. The commands that I have to send are:

connect
STM32F429ZI
SWD
4000
erase
loadbin program.elf , 0x0
q

The problem here is that after command 4000 application JLinkExe spawns a GUI window where user needs to use a mouse to click Accept button.


I want to write a python3 & pytest script that will automate this so I managed to do this so far:

import pytest
import subprocess
import pyautogui
import sys
import shutil
import os

JLINK_SERIAL = "115081052"
TARGET = "STM32F429ZI"


@pytest.fixture
def canScriptRun() -> None:

    # Newline for nicer pytest formatting.
    print("\n")

    printSeparator()

    # Check whether script is ran by superuser / admin.
    if (os.name == "nt"):
        print("Operating system: NT compliant")
        if (os.access("Program Files", os.W_OK) is False):
            print("User: not admin (continue)")
        else:
            print("User: admin")
    elif (os.name == "posix"):
        print("Operating system: Posix compliant")
        if (os.access("/etc/", os.W_OK) is False):
            print("User: not superuser (exit)")
            sys.exit(0)
        else:
            print("User: superuser")

    printSeparator()


@pytest.fixture
def flash(canScriptRun) -> None:

    # JLinkExe child process creation.
    if (os.name == "nt"):
        p = subprocess.Popen(
            [r"C:\Program Files\SEGGER\JLink\JLink.exe", "-nogui",  "1"],
            stdin=subprocess.PIPE,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            universal_newlines=True,
            text=True,
            shell=True
        )
    elif (os.name == "posix"):
        p = subprocess.Popen(
            ["JLinkExe", "-nogui", "1"],
            stdin=subprocess.PIPE,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            universal_newlines=True,
            text=True
        )

    # Send newline-separated commands to the process & leave the running
    # process as it is.
    stdout, stderr = p.communicate(
        input="connect\nSTM32F429ZI\nS\n4000\n",
        timeout=None
    )
    print(stdout)
    print(stderr)

    # Locate & click the GUI accept button based on it's screenshot saved in project folder.
    buttonCenterLocation = pyautogui.locateCenterOnScreen("accept.png")
    pyautogui.move(buttonCenterLocation, 1)
    pyautogui.click()


def printSeparator() -> None:

    terminalWidth = shutil.get_terminal_size().columns
    for i in range(0, terminalWidth):
        print('-', end='')


def test_tests(flash) -> None:

    # Newline for nicer pytest formatting.
    print("\n")

    pass

Once command 4000 executes a GUI window appears prompting me to click Accept button (screenshoot), but mouse will not move to that position and will not make a click. This is because when GUI window is displayed JLinkExe is blocked and script also waits...

enter image description here

Any idea on how to solve this?


Solution

  • This is because subprocess.Popen.communicate(timeout=None) blocks the python script until the external program terminates, so your pyautogui block is never reached unless the window is closed.

    Just call that with a finite timeout, suppress the exception and move on.

    Note that this means you won't be able to read stdout and stderr until the process is terminated!

    @pytest.fixture
    def flash(canScriptRun) -> None:
    
        # ...
    
        try:
            p.communicate(
                input="connect\nSTM32F429ZI\nS\n4000\n",
                timeout=10
            )
        except subprocess.TimeoutError:
            pass
    
    
        buttonCenterLocation = pyautogui.locateCenterOnScreen("accept.png")
        pyautogui.move(buttonCenterLocation, 1)
        pyautogui.click()
    
        (sdtout, stderr) = p.communicate()
    
        print(stdout)
        print(stderr)
    

    Additional thoughts

    Pytest is really not the adequate resource for what you want to do, which is Robotic Process Automation (RPA)

    There are a bunch of RPA libraries for Python, research those instead: https://pypi.org/search/?c=Framework+%3A%3A+Robot+Framework