Skip to content

mpremote: Refactor error handling to apply generically to any errno. #17090

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Apr 26, 2025

Conversation

AJMansfield
Copy link
Contributor

@AJMansfield AJMansfield commented Apr 7, 2025

Summary

This rewrites the code that previously manually emitted and caught various OSError subclasses with equivalent code that uses the errno name dictionary to do this generically, and updates the exception handler in do_filesystem to catch them in a similarly-generic fashion using os.strerror to retrieve an appropriate error message text equivalent to the current messages.

Note that in the CPython environments where mpremote runs, the call to the OSError constructor already returns an instance of the corresponding mapped exception subtype.

That is, this code:

raise PermissionError(errno.EPERM, '')

is exactly equivalent to:

raise OSError(errno.EPERM, '')

i.e. they both raise a PermissionError(1, '') error.

(See also: #17088, documenting this difference with micropython.)

Testing

This PR adds an automated test script, test_errno.sh. It includes test cases for all of the errnos that were previously special-cased and for raw TransportErrors not wrapping any OSError.

I've tested running the full mpremote test suite, including this new script against rp2040 and rp2350 targets, and have performed additional manual tests of all of the existing exception cases to make sure they were covered.

Manual testing also led to the discovery of the modules_errno_enotsup.py cpydiff submitted in #17088. I've also added a test to ensure mpremote correctly handles this corner case.

Trade-offs and Alternatives

This PR removes the conversion from errno.ENODEV into FileNotFoundError; see #17090 (comment) for a discussion of the rationale.

This PR subtly changes the spelling of some of the error messages that get reported when filesystem operations fail. This may break external tools that depend on those specific spellings may break; such usages arguably deserve to break, but this change also inadvertently makes mpremote compatible with any other pre-existing jank that expects the CPython error spellings.

Many of the errors this code scans for in the mpremote output are ones that micropython could never generate. This is a mild performance pessimization. I previously suspected that this might also broaden a correctness issue around injecting errno symbol names into file paths, however I've tested and verified that no such vulnerability exists.

@AJMansfield AJMansfield force-pushed the mpremote-err-refactor-1 branch from 7231295 to ca86679 Compare April 7, 2025 18:19
Copy link

codecov bot commented Apr 7, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 98.54%. Comparing base (9a37780) to head (dc46cf1).
Report is 3 commits behind head on master.

Additional details and impacted files
@@           Coverage Diff           @@
##           master   #17090   +/-   ##
=======================================
  Coverage   98.54%   98.54%           
=======================================
  Files         169      169           
  Lines       21890    21890           
=======================================
  Hits        21572    21572           
  Misses        318      318           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link

github-actions bot commented Apr 7, 2025

Code size report:

   bare-arm:    +0 +0.000% 
minimal x86:    +0 +0.000% 
   unix x64:    +0 +0.000% standard
      stm32:    +0 +0.000% PYBV10
     mimxrt:    +0 +0.000% TEENSY40
        rp2:    +0 +0.000% RPI_PICO_W
       samd:    +0 +0.000% ADAFRUIT_ITSYBITSY_M4_EXPRESS
  qemu rv32:    +0 +0.000% VIRT_RV32

@dpgeorge dpgeorge added the tools Relates to tools/ directory in source, or other tooling label Apr 8, 2025
@AJMansfield AJMansfield force-pushed the mpremote-err-refactor-1 branch 2 times, most recently from 2069827 to 790d153 Compare April 22, 2025 14:55
@AJMansfield
Copy link
Contributor Author

I've added the TransportError handler back in, and a test script to verify it and everything else.

@dpgeorge
Copy link
Member

This looks good now, thank you.

I have some feedback on the test, which is just a suggestion, not necessary to get this merged: it's possible, and simpler in the case at hand, to define an actual filesystem in Python, rather than a block device. And then make that filesystem raise the relevant exception. Eg something like this (untested!):

class FS:
    def setup_error(self, er):
        self._cur_error = er
    def mount(self, readonly, mkfs):
        pass
    def chdir(self, path):
        pass
    def open(self, path, mode):
        raise self._cur_error

fs = FS()
vfs.mount(fs, "/fs")
setup_error = fs.setup_error

@AJMansfield
Copy link
Contributor Author

AJMansfield commented Apr 24, 2025

It's possible, and simpler in the case at hand, to define an actual filesystem in Python, rather than a block device. And then make that filesystem raise the relevant exception.

Good call on that, that'd definitely make the test shorter and less clumsy -- makes for better documentation of what it's trying to do, even if it's not a technically necessary change.

I'll implement this and retest; also, I should probably test under Windows first too, just to be sure, now that I have an environment for that set up.

@AJMansfield AJMansfield force-pushed the mpremote-err-refactor-1 branch from 790d153 to 9ab2347 Compare April 24, 2025 13:41
@AJMansfield
Copy link
Contributor Author

AJMansfield commented Apr 24, 2025

I've updated the test with the simplification suggested -- which also let me replace the touch commands with even-more-noopish cat commands.

I've re-tested the full mpremote test suite with a WSL2 Linux mpremote host and RPi Pico2 device; I've confirned that the test cases in f54195c discriminate regressions on all the aforementioned issues in this thread.

@AJMansfield
Copy link
Contributor Author

AJMansfield commented Apr 24, 2025

Running the test from a Windows 11 mpremote host (using an MSYS2 bash shell) against the same RPi Pico2 device has revealed an interesting deviation:

--- test_errno.sh.exp   2025-04-24 10:02:53.218824300 -0400
+++ test_errno.sh.out   2025-04-24 10:02:57.736379400 -0400
@@ -20,6 +20,6 @@
 mpremote: cat: EPERM.py: Operation not permitted.
 expect error
 -----
-mpremote: cat: EOPNOTSUPP.py: Operation not supported.
+mpremote: cat: EOPNOTSUPP.py: Unknown error.
 expect error
 -----

It seems that os.strerror doesn't include a message for the EOPNOTSUPP errno under Windows (at least, not in Python 3.12.10, the version I tested with).

The fact there's a platform difference in what set of errnos are available leads me to think it might be better to rethink searching the error text for e.g. the symbol name strings EAGAIN and instead parse for the [Errno 11] part of OSError: [Errno 11] EAGAIN:. (And perhaps, extend that to the OSError: (30, '') format micropython falls back to for errnos it doesn't recognise.)

@AJMansfield
Copy link
Contributor Author

AJMansfield commented Apr 25, 2025

After a couple more hours working through other possible parsing strategies, I'm forced to conclude that trying to parse all the different ways micropython might output an OSError trace would go well outside the scope of this PR.

As proposed, this operates correctly on the subset of errors that the commands it's used for can produce; let's just let that be sufficient, even if it does technically break when you artificially inject more pathological error conditions.

@Josverl
Copy link
Contributor

Josverl commented Apr 25, 2025

I was looking at test coverage for mpremote ( which is a hassle due to bash, but possible ) and noticed that the test cases do not exercise all errors that could be thrown.

image
I assume that you now have these covered by the additional test that you added.

mpremote is currently at ~55% test coverage. Ill work on a PR for coverage test and reporting, always nice to push the dial up.

Signed-off-by: Anson Mansfield <amansfield@mantaro.com>
This rewrites the code that previously manually emitted and caught various
OSError subclasses with equivalent code that uses the errno name dictionary
to do this generically, and updates the exception handler in do_filesystem
to catch them in a similarly-generic fashion using os.strerror to retrieve
an appropriate error message text equivalent to the current messages.

Note that in the CPython environments where mpremote runs, the call to the
OSError constructor already returns an instance of the corresponding mapped
exception subtype.

Signed-off-by: Anson Mansfield <amansfield@mantaro.com>
Signed-off-by: Anson Mansfield <amansfield@mantaro.com>
@dpgeorge dpgeorge force-pushed the mpremote-err-refactor-1 branch from 9ab2347 to dc46cf1 Compare April 26, 2025 05:52
@dpgeorge
Copy link
Member

mpremote is currently at ~55% test coverage. Ill work on a PR for coverage test and reporting, always nice to push the dial up.

Sounds good!

@dpgeorge dpgeorge merged commit dc46cf1 into micropython:master Apr 26, 2025
64 checks passed
@dpgeorge
Copy link
Member

As proposed, this operates correctly on the subset of errors that the commands it's used for can produce; let's just let that be sufficient, even if it does technically break when you artificially inject more pathological error conditions.

Yes, good to be pragmatic. Now merged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
tools Relates to tools/ directory in source, or other tooling
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants