Skip to content

Commit a41f1c8

Browse files
committed
added wipe command (rm :/ -- reset --machine)
1 parent 3f5fbdd commit a41f1c8

6 files changed

Lines changed: 169 additions & 3 deletions

File tree

README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ It is an alternative to the official [mpremote](https://docs.micropython.org/en/
1616
(CP2102, CH340)
1717
- **Multiple reset options** - soft, MCU, hardware (RTS),
1818
bootloader entry
19+
- **Wipe device** - erase the whole filesystem and machine-reset
20+
for a clean slate (handy at the start of automated tests)
1921
- **General-purpose serial terminal** - `repl` and `monitor` work
2022
with any serial device
2123
- **Mount local directory** - VFS mount (read-only or read-write)
@@ -255,6 +257,30 @@ $ mpytool sleep 2 # sleep for 2 seconds (useful between commands)
255257
SLEEP 2.0s
256258
```
257259

260+
### Wipe device (erase all files + machine reset)
261+
```
262+
$ mpytool wipe # delete everything from / then machine.reset()
263+
WIPE
264+
rm /boot.py
265+
rm /main.py
266+
rm /lib
267+
reset
268+
269+
$ mpytool wipe -- ls # wipe, reconnect, then list (now empty)
270+
WIPE
271+
rm /main.py
272+
reset, reconnected
273+
LS:
274+
```
275+
276+
`wipe` erases the whole filesystem (recursive delete of `/`) and then does a
277+
full `machine.reset()` — a soft reset would leave open sockets, WiFi and
278+
mounted VFS alive, so machine reset is used for a true clean slate. It **never
279+
prompts** (mpytool has no interactive mode); `-y`/`--yes` is accepted as a
280+
no-op. When `wipe` is the last command it just resets and exits; when more
281+
commands follow it reconnects first (use `-t SECONDS` for the reconnect
282+
timeout). See [README_API.md](README_API.md) for `Mpy.wipe()`.
283+
258284
### Serial terminal and monitor (general purpose)
259285
```
260286
$ mpytool repl # auto-detect port, 115200 baud

README_API.md

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -892,7 +892,7 @@ Soft reset in raw REPL mode (clears RAM only, doesn't run boot.py/main.py).
892892
mpy.soft_reset_raw()
893893
```
894894

895-
#### machine_reset(reconnect=True)
895+
#### machine_reset(reconnect=True, timeout=None)
896896

897897
MCU reset using `machine.reset()`.
898898

@@ -902,6 +902,7 @@ mpy.machine_reset(reconnect=True)
902902

903903
**Parameters:**
904904
- `reconnect` (bool): If True, attempt to reconnect after reset (for USB-CDC ports)
905+
- `timeout` (int): Reconnect timeout in seconds (None = default)
905906

906907
#### hard_reset()
907908

@@ -914,6 +915,34 @@ mpy.hard_reset()
914915
**Raises:**
915916
- `NotImplementedError`: If connection doesn't support hardware reset
916917

918+
#### wipe(reconnect=True, timeout=None, on_delete=None)
919+
920+
Erase all files from the filesystem root, then `machine.reset()` the device.
921+
922+
Recursively deletes every entry in `/`, then performs a full machine reset so
923+
runtime state (open sockets, WiFi, mounted VFS) is dropped too — a soft reset
924+
would leave those alive. Intended for a clean slate, e.g. at the start of
925+
integration tests.
926+
927+
```python
928+
# Clean slate at the start of a test
929+
mpy.wipe()
930+
931+
# Wipe without waiting to reconnect
932+
mpy.wipe(reconnect=False)
933+
934+
# Show what gets removed
935+
mpy.wipe(on_delete=lambda path: print(f"removing {path}"))
936+
```
937+
938+
**Parameters:**
939+
- `reconnect` (bool): If True, wait for the device to come back after reset
940+
- `timeout` (int): Reconnect timeout in seconds (None = default)
941+
- `on_delete` (callable): Optional `callback(path)` invoked before each delete
942+
943+
**Returns:**
944+
- `bool`: True if reconnected after reset, False otherwise
945+
917946
### Flash/Partition Methods
918947

919948
#### partitions()

mpytool/mpy.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1329,6 +1329,29 @@ def machine_reset(self, reconnect=True, timeout=None):
13291329
return True
13301330
return False
13311331

1332+
def wipe(self, reconnect=True, timeout=None, on_delete=None):
1333+
"""Erase all files from filesystem root, then machine-reset device
1334+
1335+
Deletes every entry in '/' recursively, then performs a full
1336+
machine.reset() so runtime state (open sockets, WiFi, mounted VFS,
1337+
etc.) is dropped too - a soft reset would leave those alive. Intended
1338+
for a clean slate, e.g. at the start of integration tests.
1339+
1340+
Arguments:
1341+
reconnect: if True, wait for the device to come back after reset
1342+
timeout: reconnect timeout in seconds (None = default)
1343+
on_delete: optional callback(path) invoked before each delete
1344+
1345+
Returns:
1346+
True if reconnected after reset, False otherwise
1347+
"""
1348+
for name, _ in self.ls('/'):
1349+
path = '/' + name
1350+
if on_delete is not None:
1351+
on_delete(path)
1352+
self.delete(path)
1353+
return self.machine_reset(reconnect=reconnect, timeout=timeout)
1354+
13321355
def machine_bootloader(self):
13331356
"""Enter bootloader using machine.bootloader()
13341357

mpytool/mpytool.py

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
# Order of commands in help and completion
3232
_CMD_ORDER = [
3333
'ls', 'tree', 'cat', 'cp', 'mv', 'mkdir', 'rm', 'pwd', 'cd', 'path',
34-
'stop', 'reset', 'monitor', 'repl', 'exec', 'run', 'edit', 'info',
34+
'stop', 'reset', 'wipe', 'monitor', 'repl', 'exec', 'run', 'edit', 'info',
3535
'rtc', 'flash', 'mount', 'ln', 'speedtest', 'sleep', 'ports',
3636
]
3737

@@ -770,6 +770,21 @@ def cmd_reset(self, mode='soft', reconnect=True, timeout=None):
770770
except (NotImplementedError, _mpytool.ConnError) as err:
771771
raise _mpytool.MpyError(f"Bootloader reset failed: {err}")
772772

773+
def cmd_wipe(self, reconnect=True, timeout=None):
774+
"""Erase all files from root, then machine-reset the device"""
775+
self.verbose("WIPE", 1)
776+
try:
777+
self.mpy.wipe(
778+
reconnect=reconnect, timeout=timeout,
779+
on_delete=lambda path: self.verbose(f" rm {path}", 1))
780+
except (_mpytool.ConnError, OSError) as err:
781+
self.verbose(f" reconnect failed: {err}", 1, color='red')
782+
raise _mpytool.ConnError(f"Reconnect failed: {err}")
783+
if reconnect:
784+
self.verbose(" reset, reconnected", 1, color='green')
785+
else:
786+
self.verbose(" reset", 1)
787+
773788
@command('ls', 'List files and directories on device.')
774789
@argument('path', nargs='?', default=':', metavar='remote',
775790
type=_normalize_path_arg, help='device path (default: CWD)')
@@ -922,6 +937,18 @@ def _dispatch_reset(self, commands, is_last_group):
922937
reconnect = has_more if mode in ('machine', 'rts') else True
923938
self.cmd_reset(mode=mode, reconnect=reconnect, timeout=args.timeout)
924939

940+
@command('wipe', 'Erase ALL files and machine-reset the device.')
941+
@option('-y', '--yes', action='store_true',
942+
help='no-op (wipe never prompts), accepted for convenience')
943+
@option('-t', '--timeout', type=int,
944+
help='reconnect timeout in seconds')
945+
def _dispatch_wipe(self, commands, is_last_group):
946+
args, commands[:] = _make_parser(
947+
self._dispatch_wipe).parse_known_args(commands)
948+
# Reconnect only if more work follows; pointless if wipe is last.
949+
has_more = bool(commands) or not is_last_group
950+
self.cmd_wipe(reconnect=has_more, timeout=args.timeout)
951+
925952
@command('monitor', 'Monitor device output until program ends.')
926953
@option('-f', '--follow', action='store_true',
927954
help='follow output continuously (Ctrl-C to stop)')
@@ -1441,7 +1468,7 @@ def _dispatch_args(self, commands, is_last_group):
14411468

14421469
_COMMANDS = frozenset({
14431470
'ls', 'tree', 'cat', 'mkdir', 'rm', 'pwd', 'cd', 'path',
1444-
'reset', 'stop', 'monitor', 'repl', 'exec', 'run', 'edit', 'info',
1471+
'reset', 'wipe', 'stop', 'monitor', 'repl', 'exec', 'run', 'edit', 'info',
14451472
'rtc', 'flash', 'sleep', 'cp', 'mv', 'mount', 'ln', 'speedtest',
14461473
'ports', '_paths', '_commands', '_options', '_args',
14471474
})

tests/test_cli.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,12 @@ def test_reset_help(self):
137137
'soft' in result.stdout.lower() or
138138
'hard' in result.stdout.lower())
139139

140+
def test_wipe_help(self):
141+
"""mpytool wipe --help shows wipe usage"""
142+
result = run_mpytool('wipe', '--help')
143+
self.assertEqual(result.returncode, 0)
144+
self.assertIn('erase', result.stdout.lower())
145+
140146
def test_mount_help(self):
141147
"""mpytool mount --help shows mount usage"""
142148
result = run_mpytool('mount', '--help')

tests/test_mpy.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,61 @@ def test_chdir_to_parent(self):
484484
self.assertIn("os.chdir('..')", call_args)
485485

486486

487+
class TestWipe(unittest.TestCase):
488+
"""Tests for Mpy.wipe method"""
489+
490+
def setUp(self):
491+
self.mock_conn = Mock()
492+
self.mpy = Mpy(self.mock_conn)
493+
self.mpy._mpy_comm = Mock()
494+
# wipe orchestrates ls/delete/machine_reset; stub them to verify flow
495+
self.mpy.ls = Mock(return_value=[])
496+
self.mpy.delete = Mock()
497+
self.mpy.machine_reset = Mock(return_value=True)
498+
499+
def test_wipe_deletes_all_root_entries(self):
500+
"""wipe deletes every root entry using an absolute path"""
501+
self.mpy.ls.return_value = [('lib', None), ('boot.py', 42)]
502+
self.mpy.wipe()
503+
self.mpy.ls.assert_called_once_with('/')
504+
self.mpy.delete.assert_any_call('/lib')
505+
self.mpy.delete.assert_any_call('/boot.py')
506+
self.assertEqual(self.mpy.delete.call_count, 2)
507+
508+
def test_wipe_machine_resets_with_args(self):
509+
"""wipe forwards reconnect/timeout to machine_reset"""
510+
result = self.mpy.wipe(reconnect=True, timeout=5)
511+
self.mpy.machine_reset.assert_called_once_with(
512+
reconnect=True, timeout=5)
513+
self.assertTrue(result)
514+
515+
def test_wipe_empty_filesystem_still_resets(self):
516+
"""wipe on empty FS deletes nothing but still resets"""
517+
self.mpy.machine_reset.return_value = False
518+
result = self.mpy.wipe(reconnect=False)
519+
self.mpy.delete.assert_not_called()
520+
self.mpy.machine_reset.assert_called_once_with(
521+
reconnect=False, timeout=None)
522+
self.assertFalse(result)
523+
524+
def test_wipe_invokes_on_delete_callback(self):
525+
"""on_delete is called with each path before deletion"""
526+
self.mpy.ls.return_value = [('a.py', 1), ('b.py', 2)]
527+
seen = []
528+
self.mpy.wipe(on_delete=seen.append)
529+
self.assertEqual(seen, ['/a.py', '/b.py'])
530+
531+
def test_wipe_deletes_before_reset(self):
532+
"""all deletes happen before the machine reset"""
533+
calls = []
534+
self.mpy.ls.return_value = [('x', 1)]
535+
self.mpy.delete.side_effect = lambda p: calls.append(('del', p))
536+
self.mpy.machine_reset.side_effect = (
537+
lambda **k: calls.append(('reset',)) or True)
538+
self.mpy.wipe()
539+
self.assertEqual(calls, [('del', '/x'), ('reset',)])
540+
541+
487542
class TestExecSubmitOnly(unittest.TestCase):
488543
"""Tests for exec() and exec_raw_paste() with timeout=0 (submit only)"""
489544

0 commit comments

Comments
 (0)