Why does go.exists() require a different url from msg.post()?

Hello,

Sorry if this is a silly question, I am merely curious… I’m experimenting with writing a unit testing library for testing Defold game code and ran into a case that caused me a gray hair or two.

I have a main xunit.collection running the test script, which loads undertest.collection into a collectionproxy, then tries to do some assertion on it… it’s still early in development, so for now I am simply trying to assert(go.exists("/xunit_is_cool"), "Expected /xunit_is_cool to exist"). I quickly realized this doesn’t work…

According to these bits of documentation it looked like go.exists("undertest:/xunit_is_cool") should work:

It does not. Eventually I gave up and started trying random things with the debugger and discovered that I need to do go.exists("/tests#testproxy1/xunit_is_cool")

This is not necessarily a problem, the test code looks quite reasonable:

	case.GameTestCase_can_load_a_collection_under_test = function(self)
		-- Given
		local proxy = "/tests#testproxy1"
		self.util.loadCollection(proxy)
		self.gassert.receivedMessage(hash("proxy_loaded"))
		self.util.initCollection(proxy)

		-- When Then
		self.gassert.gameObjectExists(proxy .. "/xunit_is_cool")
	end

I might just need to think about how to document this difference if get far enough for this to turn into a real library, especially when I start adding sending messages into the collection under test or asserting if it received certain events.

The script running your unit test from xunit.collection (I suppose this is where you have the unit test script which calls go.exist) needs to use the full absolute path of the object to check.

What if you put a script on xunit_is_cool and do a print(msg.url()) to show the absolute URL of the game object?

I tried that, the output is:
url: [undertest:/xunit_is_cool#undertest]

Trying to assert go.exists() with that value from the xunit collection fails.

EDIT: to be clear, I just realized I forgot to strip of the #undertest for the script component, but tried that as well now, still fails.

Odd. And you are sure the collection is loaded and initialised before you call go.exists()?

Yeah, the way my library code works, each step is tried until it succeeds or we run out of time for the test case:

	case.GameTestCase_can_load_a_collection_under_test = function(self)
		-- Given
		local proxy = "/tests#testproxy1"
		self.util.loadCollection(proxy)  -- step 1: load collection
		self.gassert.receivedMessage(hash("proxy_loaded")) -- wait for proxy_loaded event
		self.util.initCollection(proxy) -- step 2: send init & enable events

		-- When Then
		self.gassert.gameObjectExists("undertest:/xunit_is_cool") -- wait for assert to become true
	end

EDIT: fix to show non-working assert above.

This runs 3 seconds and times out:

DEBUG:SCRIPT: Suite: Game Tests Suite
DEBUG:SCRIPT:   GameTestCaseTests.GameTestCase_can_load_a_collection_under_test: FAIL (3.013s; 217 frames)
DEBUG:SCRIPT:     Timed out, last error: main/xunit.script:194: Expected 'undertest:/xunit_is_cool' to exist
DEBUG:SCRIPT: 1 run, 1 failed

Which I think is enough time, because in the case with the working url it completes very quickly:

DEBUG:SCRIPT: Suite: Game Tests Suite
DEBUG:SCRIPT:   GameTestCaseTests.GameTestCase_can_load_a_collection_under_test: OK (0.066s; 5 frames)
DEBUG:SCRIPT: 1 run, 0 failed

I had an idea to try to confirm that the collection:/id pattern in fact works for sending messages into the loaded collection, seems so:

	case.GameTestCase_can_send_message_into_a_collection_under_test = function(self)
		-- Given
		local proxy = "/tests#testproxy1"
		self.util.loadCollection(proxy)
		self.gassert.receivedMessage(hash("proxy_loaded"))
		self.util.initCollection(proxy)

		-- When
		self.util.sendMessage("undertest:/xunit_is_cool", "create_bullet", { position = vmath.vector3(5, 5, 0) })
		
		-- Then
		self.gassert.gameObjectExists(proxy .. "/bullets/instance0") -- works
		-- self.gassert.gameObjectExists("undertest:" .. "/bullets/instance0") -- times out
	end
DEBUG:SCRIPT: Created bullet: [/instance0]
DEBUG:SCRIPT: Suite: Game Tests Suite
DEBUG:SCRIPT:   GameTestCaseTests.GameTestCase_can_send_message_into_a_collection_under_test: OK (0.081s; 6 frames)
DEBUG:SCRIPT: 1 run, 0 failed

Note: self.util.sendMessage(...) is just a wrapper for msg.post(...) to happen eventually after the other steps completed.

After some more playing around, it seems I was mistaken about this working in the first place:
go.exists("/tests#testproxy1/bullets/instance0")

The only part of that URL that is relevant is "/tests". Nothing that comes after it seems to be at all relevant:

go.exists("/tests#testproxy1/bullets/instance5")
true
go.exists("/tests#testproxy1/bullets")
true
go.exists("/tests#testproxy1")
true
go.exists("/tests")
true
go.exists("/test")
false

At this point I’m not sure if I can check for the existence of game objects inside a proxied collection… I’ll try to look around the engine code a bit to see if I can understand why / how to do it instead.

The go.exists() function will test if a game object exists. This means that all of these test if the game object with id /tests exist:

go.exists("/tests#testproxy1/bullets/instance5")
go.exists("/tests#testproxy1/bullets")
go.exists("/tests#testproxy1")

Remember than anything after # will refer to a component and will be ignored for the purpose of that function. You need to use the ‘socket’ of the loaded collection when checking your bullet instance. Something like this:

go.exists("foobar:/bullets/instance5")

This will check if the game object instance /bullets/instance5 exists in the loaded collection with socket/name foobar.

4 Likes

Thanks for taking the time to look into this!

It seems to me that using the socket in a URL like you suggest, only works for msg.post(), not for go.exists(). Here is a short proof: DefoldUrls.zip (627.0 KB).

-- Main cannot see into subscene with go.exists:
DEBUG:SCRIPT: Main 'subscene:/child' exists? no

-- Subscene can see inside itself with go.exists:
DEBUG:SCRIPT: Subscene '/child' exists? yes
DEBUG:SCRIPT: Subscene 'subscene:/child' exists? yes

-- Subscene cannot see into main with go.exists:
DEBUG:SCRIPT: Subscene 'main:/main' exists? no

-- They can only communicate via msg.post with socket:
DEBUG:SCRIPT: Main 'do_you_exist' event received? yes
DEBUG:SCRIPT: Subscene 'do_you_exist' event received? yes

Question is if it’s a bug or just something to try to clarify in documentation?

I was wrong and should have checked the code. All other go.* functions will generate a Lua error if you try to interact with a game object from a different collection, but go.exists() will simply return false.

This is not documented anywhere and the least we could do is to add a note in the docs. The alternative is to throw a Lua error. OR actually allow the function to check in another collection.

2 Likes

This is the most reasonable change to make at this point. The function will then behave just like any other go.* function.

2 Likes

Awesome, thanks for the speedy fix! This gives me a very clear idea of what to try next for what I was trying to do.

I tried to update the relevant documentation to clarify this: