Testing module in jython for End-To-End testing?

I’ve brought this up before on this forum before where I use a java.awt.Robot to try to test a GUI, run db queries before and after test to make sure that every executed as expected, stuff like that which we currently do completely manually to make sure all our intermediary scripts are working right.

Now, I am trying to make this a little bit more developer friendly so that other people in my company can use this for testing out other projects.

Here’s a little proof of concept I have before I ask my questions -

# Needed for moving Stuff around
import java.awt.Robot
from java.awt.event import InputEvent, KeyEvent
from java.lang import String
import java.lang.IllegalArgumentException
from java.awt import EventQueue, AWTEvent
# For handling lists of GUIActions appropriately
import Queue

DELAY = 1000
rob = java.awt.Robot()

class GUIAction():
	def __init__(self):
		self.completed = False

	def _execute(self):
		system.util.invokeLater(self.execute, DELAY)
	
	def execute(self):
		# Required to be filled in by subclasses
		pass

class MouseAction(GUIAction):
	def __init__(self, x, y):
		GUIAction.__init__(self)
		self.x = x
		self.y = y

class LeftClick(MouseAction):
	def __init__(self, x, y):
		MouseAction.__init__(self, x, y)
		
	def execute(self):
		rob.mouseMove(self.x, self.y)
		rob.mousePress(InputEvent.BUTTON1_MASK)
		rob.mouseRelease(InputEvent.BUTTON1_MASK)
		self.completed = True

class RightClick(MouseAction):
	def __init__(self, x, y):
		MouseAction.__init__(self, x, y)

	def execute(self):
		rob.mouseMove(self.x, self.y)
		rob.mousePress(InputEvent.BUTTON3_MASK)
		rob.mouseRelease(InputEvent.BUTTON3_MASK)
		self.completed = True

class GUITest(Queue.Queue):
	def __init__(self, *args, **kwargs):
		Queue.Queue.__init__(self, *args, **kwargs)
	
	def runNextBlock(self):
		nextStep = self.get()
		nextStep._execute()
		while nextStep.completed == False:
			# Please don't judge this too harshly lol
			for i in range(1, 200000):
				pass
		
	def run(self):
		while self.qsize() > 0:
			self.runNextBlock()

def runSampleTest():
	sampleTest = GUITest()
	sampleTest.put(LeftClick(100,100))
	sampleTest.put(RightClick(200,200))
	sampleTest.run()

And this does work reasonably well, I don’t need to keep track of end times of system.util.invokes as long as the execute function ends in a self.completed = True then the Queue works correctly, I see the mouse moves, waits, moves, as expected, but I can’t help but feel that this in particular

		while nextStep.completed == False:
			# Please don't judge this too harshly lol
			for i in range(1, 200000):
				pass

is very hacky.

So I have three questions

  1. If I continue to use the built-in python Queue, is there a better way to keep track of the instructions being completed and for the queue to know this?
  2. I was told in an earlier post about java.awt.EventQueue that takes in java.awt.AWTEvent’s that I am under the impression can do this sort of thing but I can’t seem to get it to work. For example It would require my LeftClick class to be a sublcass of java.awt.AWTEvent for it to be able to be entered into the EventQueue - I’m not sure how my LeftClick class would have to look like to satisfy that. Post I am referring to that I could not get to work - Have Robot type any arbitrary message via keyboard in vision? - #14 by PGriffith
  3. In terms of writing tests, right now I just put one together quickly by manually creating and adding to a queue and then calling my custom run function, but would this be the way to go going forward? Or would it be better to sublcass my GUITest class for each individual test, and I suppose in the __init__ I would add all my instructions?

Apologies for all the questions, in the winter break I’ve lost about 50% of my programming brain cells.

I like the idea, but I’d use the completable futures from my later.py module to wait for gui operations in a background thread. You can have a foreground operation return a new future when it defers something again. (The background thread would check the .get() return value and .get() again.)

Then the background thread just loops through the list of simulation operations. And the background can safely sleep, which your hacky loop is effectively doing. (Don’t do that in the foreground.)

1 Like

I’m not sure if I actually like this idea, but you could do a sort of DSL for constructing test cases; perhaps passing a list of steps like this:

[
    ("leftClick", (100, 100)),
    ("type",  ("abc")),
    ("rightClick", (200, 200))
]

The downside is the relative lack of ‘type-safety’, but you already don’t have much of that with Python and our limited IDE.

I’ll revisit your main question soon.

1 Like

@PGriffith

[
    ("leftClick", (100, 100)),
    ("type",  ("abc")),
    ("rightClick", (200, 200))
]

This is the ultimate end goal.

Well really, to avoid doing an XY Problem on myself, let me explain the actual ultimate end goal for me for now-

I have a CSV file with a list of X plants. Each plant can have any of 1 - 8 products. In my particular use case, this comes out to almost 500 tests (each product in each plant must be tested).

I want to iterate per plant, and then per product, and then run all the operations via the GUI that I want to test. Then move onto the next product in that plant, etc, and then onto the next row.

Luckily moving around the GUI of the product windows is identical from plant to plant. So I was thinking breaking things down in that way, a class for Product1GUIOperationToTest1, Product1GUIOperationToTest2, etc,.

But I was imaginging something like you have -

class Product1GUIOperationToTest1(??):
    def __init__(self):
        self.instructions = [ ("leftClick", (100, 100)),
                              ("type",  ("abc")),
                              ("rightClick", (200, 200)),
                              ...
                            ]

I hope this makes sense.

@pturmel
I’ve never used your later.py before but I am looking through it now. I’ve explained more context in my other reply if you want to take a look.

I am testing my code by calling runSampleTest() directly from a gui button. This means that sampleTest.run() is the foreground thread, so it tells the first instruction to _execute and then the foreground is running the empty foreloop until nextStep.completed is True, indicating the instruction completed. So does that mean then to avoid this and to have the empty for loop check going on in the background, I need to change this

def run(self):
	while self.qsize() > 0:
		self.runNextBlock()

into something like this?

def _run(self):
    while self.qsize() > 0:
        self.runNextBlock()

def run(self):
    future = shared.later.callLater(self._run)

Just want to make sure I am understanding correctly.

Have you actually tried this? I would expect it to lock up the client. I recommend this entire topic, especially @Claus_Nielsen's explorations:

You’re right. Running my code as is from a button locks up the GUI without fail. I had been calling the function from Script Console in the designer works perfectly there oddly enough.

Consider something like this in a script module:

# Foreground functions.  Never sleep in these.

def leftClick(*args):
	# do robot stuff for a left leftClick
	pass

def rightClick(*args):
	# do robot stuff for a left leftClick
	pass

def typetext(*args):
	# do robot stuff to type into your interface
	pass

# Dummy function to do delays after foreground operations.
def dummy():
	pass

# Background function to run through a bunch of actions.  All
# delays are side effects of f.get().  Pass a component that
# will be updated at the end.  The list of operations must be
# in this form:
#
# [
#   ('functionName', (some, arguments, as, a, tuple)),
#   ('delay', (500, )),
#   . . .
# ]
#
def backgroundTest(someComponent, operations):
	for operation, args in operations:
		if operation == 'delay':
			opfunction = dummy
			millis = args[0]
			args = []
		else:
			opfunction = globals().get(operation)
			millis = 0
		f = shared.later.callLater(opfunction, *args, ms=millis)
		while isinstance(shared.later.CompleteableFuture, f):
			f = f.get()

	shared.later.assignLater(someComponent, 'finishProp', True)
# 

Use shared.later.callAsync(project.whatever.backgroundTest, event.source, opsList) in your button’s actionPerformed event.

Consider disabling the button just before that, and using ‘enabled’ as your finishProp.

2 Likes

Oh, and go grab a fresh copy of later.py. I just added the millis handling to callLater.

3 Likes

Ok so I am trying to generalize this out a bit. I have the latest copy of later.py that you just linked. Here is my code if you want to replicate my issue -

# project.tests.actions
import java.awt.Robot
from java.lang import String
import java.lang.IllegalArgumentException

ROB = java.awt.Robot()

def leftClick(x, y):
	ROB.mouseMove(x, y)
	ROB.mousePress(InputEvent.BUTTON1_MASK)
	ROB.mouseRelease(InputEvent.BUTTON1_MASK)

def rightClick(x, y):
	ROB.mouseMove(x, y)
	ROB.mousePress(InputEvent.BUTTON3_MASK)
	ROB.mouseRelease(InputEvent.BUTTON3_MASK)

def delay():
	pass

def keyboardType():
	pass

def keyboardPaste():
	pass

Now I am trying to abstract a bit with the list of instructions, as I will have many repeated instructions. Here’s a simple example of what I am trying to do now -

import project.tests.actions
import project.shared.later

func_mapping = {
	'lclick':project.tests.actions.leftClick,
	'rclick':project.tests.actions.rightClick,
	'ktype':project.tests.actions.keyboardType,
	'kpaste':project.tests.actions.keyboardPaste,
	'delay':project.tests.actions.delay
}

class Instruction():
	def __init__(self, gui_instructions):
		self.gui_instructions = gui_instructions

	def parseInstructions(self, gui_instruction):
		try:
			return func_mapping[gui_instruction]
		except KeyError:
			return func_mapping['delay']
	
	def _run(self):
		for gui_instruction, args in self.gui_instructions:
			func_to_run = self.parseInstructions(gui_instruction)
			milliseconds = 500
			f = project.shared.later.callLater(func_to_run, *args, ms=milliseconds)
			while isinstance(shared.later.CompleteableFuture, f):
				f = f.get()
			
	def run(self):
		project.shared.later.callAsync(self._run) 

class LoginAsBrian(Instruction):
	def __init__(self):
		gui_instructions = [
			('lclick',(100, 100)),
			('lclick',(400, 400)),
			('lclick',(800,800))
		]
		Instruction.__init__(self, gui_instructions)

And then in my button I do a

import project.tests.instructions

loginInstruction = project.tests.instructions.LoginAsBrian()
loginInstruction.run()

I keep running into this -

Traceback (most recent call last):

  File "<event:actionPerformed>", line 1, in <module>

  File "<module:project.tests.instructions>", line 26

    f = project.shared.later.callLater(func_to_run, *args, ms=milliseconds)

                                                          ^

SyntaxError: mismatched input 'ms' expecting DOUBLESTAR

If I remove the ms=milliseconds argument the error I see my first lclick instruction execute and then I get

Traceback (most recent call last):

  File "<module:project.shared.later>", line 154, in callAsyncInvoked

  File "<module:project.shared.later>", line 95, in __init__

NameError: global name 'fullClassName' is not defined

I am trying to abstract a to Instruction class that will consist of multiple smaller GUI instructions so that my tests could eventually look something like Login, NavigateToPageX, FillInValue(y) etc. I’m not sure how this would affect though getting and using the futures.

My brains fried for the day so I will come back to this tomorrow but if you have any suggestions re: the errors I am getting or how I would group multiple tinier gui actions together in a way that I can link them similarly to how “atomic” click/type events are here, I would greatly appreciate it.

You could make Phil’s backgroundTest take *operations, so you can call it with a sequence of arguments, and then compose your higher-level operations as simple top-level variables:

LoginAsBrian = [
	('lclick',(100, 100)),
	('lclick',(400, 400)),
	('lclick',(800,800))
]

EnterProcessData = [
	# do the things
]

def loginInstruction():
	project.later.backgroundTest(someComponent, LoginAsBrian, EnterProcessData)

Meh. Should have been:

f = shared.later.callLater(opfunction, ms=millis, *args)

The latter problem is some scoping issue with @staticmethod. Haven’t tracking it down. Reposted later.py with that pulled out to the top level.

BTW, looks very strange as project.shared.later.whatever. I would expect project.later.whatever or shared.later.whatever, not both smooshed together. And you didn’t fix up the isinstance?

Anyways, it seems to me the class definition adds unnecessary complication. I like Paul’s idea.

1 Like

Gotcha I will try it later.

Oh I thought that later.py was meant to be inside a module called shared. I’m using 7.9.9 so everything is a project. prefix it seems like. I don’t know if this is specific to the fact that this project also has the legacy code in the modules.

You’re right I didn’t I missed the isinstance part I missed.

I think the way to go forward might be to put my semantic actions as hardcoded variables like

LoginAsBrian = [
	('lclick',(100, 100)),
	('lclick',(400, 400)),
	('lclick',(800,800))
]

NavigateToWindow = [
   ...
]

etc

and then in a function

def testProductOneOperationOne():
    fullInstructions = LoginAsBrian + NavigateToWindow + whateverOtherInstructions
    # Now run the background thread logic on the full list of robot instructions

v7.9.x has global scripts. That is the shared. namespace. That is where one puts scripts that all projects can use, ergo “shared”.