Skip to content

Conversation

@shaggysa
Copy link
Contributor

@shaggysa shaggysa commented Oct 31, 2025

Most errors are caught by the robot after the program has been sent, but a few will abort the entire program.
While this used to be fine, it can be annoying when the --stay-connected flag is active.

Currently, this only works after the program has been successfully sent once, but I'm open to applying it more broadly. However, getting that to work would probably require a small refactor.

image

@shaggysa
Copy link
Contributor Author

shaggysa commented Nov 1, 2025

I just moved the menu into a function so that I can call it from an except block. Please let me know if there is a cleaner way to do this that I'm not thinking of.

@dlech
Copy link
Member

dlech commented Nov 28, 2025

Hi @shaggysa. This got buried in the backlog. For future reference, I personally don't mind a gentle ping if it has been more than two weeks and it seems like I forgot about something.

@shaggysa
Copy link
Contributor Author

I moved it to a separate function, but I am unable to test it with a robot at the moment. The linter is happy, but I can't guarantee that it will work the way I intended.

@dlech
Copy link
Member

dlech commented Nov 29, 2025

Apparently I've done something to mpy-cross that makes it segfault instead of reporting a SyntaxError.

I made some other changes that I just merged to fix a different issue, but it means we no longer get the SyntaxError raised from ModuleFinder, so we will need to reconsider this a bit. But it looks like I will need to figure out what is going on with mpy-cross first and fix that.

@shaggysa
Copy link
Contributor Author

Apparently I've done something to mpy-cross that makes it segfault instead of reporting a SyntaxError.

Ouch... That seems like a pretty big vulnerability...

I guess let me know if I need to adjust anything to make the cli catch/report it properly once you have that figured out.

@dlech
Copy link
Member

dlech commented Nov 29, 2025

OK, the crash is fixed now (you will need to merge the master branch and then poetry install to get the updates).

Because of the other change I made, it will now raise subprocess.CalledProcessError instead of SyntaxError. We can catch this new exception and print the stderr instead of catching the SyntaxError.

It would also be nice if we could get some unit tests for this.

@shaggysa
Copy link
Contributor Author

Ok, I'll see what I can do about fixing it to work with the new mpy-cross. I haven't really messed with unit testing before, but I can give that a shot as well.

@shaggysa
Copy link
Contributor Author

shaggysa commented Nov 30, 2025

Ok, I think I'm catching the error properly now (I was actually able to test with a robot this time). The error seems to always show stdin as the file, which should probably be changed to make it a bit more helpful on multi-file projects.

@shaggysa shaggysa marked this pull request as draft December 1, 2025 03:41
@shaggysa
Copy link
Contributor Author

shaggysa commented Dec 1, 2025

I added a few tests related to the menu, but it'll probably need some major reworking (any possibly some additions) since I don't have any experience with writing unit tests.

The final test I wrote (test_stay_connected_menu_interruptions) appears to be leaking coroutines, but I can't figure out how to fix it.

@shaggysa
Copy link
Contributor Author

shaggysa commented Dec 1, 2025

It also appears that a couple of the tests failed on the gh runner. I'll have to look into that.

It looks like the mock menu had to have the side effect of an actual async function for it to return a proper coroutine. When its side effect was hardcoded values, the AsyncMock function was unable to be cancelled properly, causing coroutines to leak.
@shaggysa shaggysa marked this pull request as ready for review December 1, 2025 15:06
@shaggysa
Copy link
Contributor Author

shaggysa commented Dec 2, 2025

I decided to go ahead and add program-cancelling from the terminal. I decided on a non-signal keypress because I didn't want to intercept a KBI in the case that the "racing" function was not active and I didn't want to worry about double presses cancelling everything. I briefly tried EOF, but it looked like I had to manually re-open stdin every time that EOF was received.

I landed on an invisible PromptToolKit app that listens for the letter 'q'. The letter it listens for can be easily modified if desired.

This should at least partially address #67.

One quirk that I can't seem to figure out is that

{'pybricks.tools'}
{'pybricks.tools'}

is printed on every call to hub.download. I didn't touch that function, and I know that the race_keypress function isn't causing it because it doesn't happen on hub.start_user_program (which uses race_keypress) and it does happen on hub.download standalone (which doesn't use race_keypress).

@dlech
Copy link
Member

dlech commented Dec 2, 2025

I decided to go ahead and add program-cancelling from the terminal.

Sounds great. Can we save that for a separate pull request? One feature is enough for one pull request. 😄

@shaggysa shaggysa force-pushed the feature/catch-syntax-error branch from 293faba to 45fed26 Compare December 2, 2025 18:03
@shaggysa
Copy link
Contributor Author

shaggysa commented Dec 2, 2025

Alright, fair enough. I moved that to a separate branch for the time being.

It looks like the random printing in the hub.download function is still an issue in the reverted timeline.

shortdemo double-prints "{'pybricks.tools'}", longdemo double-prints "set()", sockfw dobule-prints "{'utime'}", module1 double-prints "{pybricks.parameters}", etc.

The printing occurs prior to the loading bar starting.

@shaggysa
Copy link
Contributor Author

shaggysa commented Dec 2, 2025

Ok, the master branch seems to have the exact same issue, so I think that the mpy-cross update has something to do with it.

The function that this option calls is not available on hub firmware versions prior to 3.2.0-beta.3, so this test confirms that it is not called when the hub's firmware version is too old.
@shaggysa shaggysa requested a review from dlech December 4, 2025 15:30
Comment on lines 417 to 425
if args.stay_connected:
await self.stay_connected_menu(hub, args)

case ResponseOptions.RUN_STORED:
if hub.fw_version < Version("3.2.0-beta.4"):
print(
"Running a stored program remotely is only supported in the hub firmware version >= v3.2.0."
)
else:
await hub.start_user_program()
await hub._wait_for_user_program_stop()

case ResponseOptions.CHANGE_TARGET_FILE:
args.file.close()
while True:
try:
args.file = open(
await hub.race_disconnect(
hub.race_power_button_press(
questionary.path(
"What file would you like to use?"
).ask_async()
)
)
)
break
except FileNotFoundError:
print("The file was not found. Please try again.")
# send the new target file to the hub
with _get_script_path(args.file) as script_path:
await hub.download(script_path)

case _:
return

except HubPowerButtonPressedError:
# This means the user pressed the button on the hub to re-start the
# current program, so the menu was canceled and we are now printing
# the hub stdout until the user program ends on the hub.
try:
await hub._wait_for_power_button_release()
await hub._wait_for_user_program_stop()

except HubDisconnectError:
hub = await reconnect_hub()

except HubDisconnectError:
# let terminal cool off before making a new prompt
await asyncio.sleep(0.3)
hub = await reconnect_hub()
except subprocess.CalledProcessError as e:
print()
print("A syntax error occurred while parsing your program:")
print(e.stderr.decode())
if args.stay_connected:
await self.stay_connected_menu(hub, args)
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
if args.stay_connected:
await self.stay_connected_menu(hub, args)
case ResponseOptions.RUN_STORED:
if hub.fw_version < Version("3.2.0-beta.4"):
print(
"Running a stored program remotely is only supported in the hub firmware version >= v3.2.0."
)
else:
await hub.start_user_program()
await hub._wait_for_user_program_stop()
case ResponseOptions.CHANGE_TARGET_FILE:
args.file.close()
while True:
try:
args.file = open(
await hub.race_disconnect(
hub.race_power_button_press(
questionary.path(
"What file would you like to use?"
).ask_async()
)
)
)
break
except FileNotFoundError:
print("The file was not found. Please try again.")
# send the new target file to the hub
with _get_script_path(args.file) as script_path:
await hub.download(script_path)
case _:
return
except HubPowerButtonPressedError:
# This means the user pressed the button on the hub to re-start the
# current program, so the menu was canceled and we are now printing
# the hub stdout until the user program ends on the hub.
try:
await hub._wait_for_power_button_release()
await hub._wait_for_user_program_stop()
except HubDisconnectError:
hub = await reconnect_hub()
except HubDisconnectError:
# let terminal cool off before making a new prompt
await asyncio.sleep(0.3)
hub = await reconnect_hub()
except subprocess.CalledProcessError as e:
print()
print("A syntax error occurred while parsing your program:")
print(e.stderr.decode())
if args.stay_connected:
await self.stay_connected_menu(hub, args)
except subprocess.CalledProcessError as e:
print()
print("A syntax error occurred while parsing your program:")
print(e.stderr.decode())
pass
if args.stay_connected:
await self.stay_connected_menu(hub, args)

This looks a bit odd with calling stay_connected_menu() twice. We should be able to rearrange it to something like this.

We just need to be careful about the scope of with _get_script_path().

"""Test that the stay connected menu is called upon a syntax error when the appropriate flag is active."""

# Create a mock hub
mock_hub = AsyncMock()
Copy link
Member

@dlech dlech Dec 13, 2025

Choose a reason for hiding this comment

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

I would rather avoid mocks when we can. It should be trivial to create a file with a syntax error to raise this error.

The only time we really need mocks is for hardware that can't be tested on CI, like Bluetooth and USB. Otherwise, a lot of the code isn't getting tested.

@shaggysa
Copy link
Contributor Author

I think I got most of what you suggested integrated in. One thing to note is that the menu gets called even on a KBI, which we may or may not want.

Please let me know if there is anything else you want adjusted.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants