Skip to content

Commit 626f309

Browse files
authored
Merge pull request #157 from amgross/fix_repl_remove_redundant
Fix bug in REPL and aggregate duplicated code
2 parents a697539 + aebafa2 commit 626f309

5 files changed

Lines changed: 159 additions & 58 deletions

File tree

src/littlefs/__main__.py

Lines changed: 39 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from littlefs import LittleFS, __version__
99
from littlefs.errors import LittleFSError
1010
from littlefs.repl import LittleFSRepl
11-
from littlefs.context import UserContextFile
11+
from littlefs.context import UserContextFile, UserContext
1212

1313
# Dictionary mapping suffixes to their size in bytes
1414
_suffix_map = {
@@ -18,10 +18,12 @@
1818
}
1919

2020

21-
def _fs_from_args(args: argparse.Namespace, mount=True) -> LittleFS:
21+
def _fs_from_args(args: argparse.Namespace, block_count=None, mount=True, context: UserContext = None) -> LittleFS:
22+
block_count = block_count if block_count is not None else getattr(args, "block_count", 0)
2223
return LittleFS(
24+
context=context,
2325
block_size=args.block_size,
24-
block_count=getattr(args, "block_count", 0),
26+
block_count=block_count,
2527
name_max=args.name_max,
2628
mount=mount,
2729
)
@@ -105,11 +107,7 @@ def create(parser: argparse.ArgumentParser, args: argparse.Namespace) -> int:
105107
if args.compact:
106108
if args.verbose:
107109
print(f"Compacting... {fs.used_block_count} / {args.block_count}")
108-
compact_fs = LittleFS(
109-
block_size=args.block_size,
110-
block_count=fs.used_block_count,
111-
name_max=args.name_max,
112-
)
110+
compact_fs = _fs_from_args(args, block_count=fs.used_block_count)
113111
for root, dirs, files in fs.walk("/"):
114112
if not root.endswith("/"):
115113
root += "/"
@@ -131,21 +129,37 @@ def create(parser: argparse.ArgumentParser, args: argparse.Namespace) -> int:
131129
return 0
132130

133131

134-
def _list(parser: argparse.ArgumentParser, args: argparse.Namespace) -> int:
135-
"""List LittleFS image contents."""
136-
fs = _fs_from_args(args, mount=False)
137-
fs.context.buffer = bytearray(args.source.read_bytes())
132+
def _mount_from_context(parser: argparse.ArgumentParser, args: argparse.Namespace, context: UserContext) -> LittleFS:
133+
# Block count is 0 because we don't know the size of the real image yet, the source file may be compacted (with the create --compact option).
134+
fs = _fs_from_args(args, block_count=0, mount=False, context=context)
138135
fs.mount()
139136

140137
if args.verbose:
141-
fs_size = len(fs.context.buffer)
138+
input_image_size = context.in_size
139+
actual_image_size = fs.block_count * args.block_size
142140
print("LittleFS Configuration:")
143141
print(f" Block Size: {args.block_size:9d} / 0x{args.block_size:X}")
144-
print(f" Image Size: {fs_size:9d} / 0x{fs_size:X}")
142+
if input_image_size != actual_image_size:
143+
print(f" Image Size: {actual_image_size:9d} / 0x{actual_image_size:X}")
144+
print(f" Input Image Size (compacted): {input_image_size:9d} / 0x{input_image_size:X}")
145+
else:
146+
print(f" Image Size: {input_image_size:9d} / 0x{input_image_size:X}")
145147
print(f" Block Count: {fs.block_count:9d}")
146148
print(f" Name Max: {args.name_max:9d}")
147149
print(f" Image: {args.source}")
148150

151+
return fs
152+
153+
154+
def _list(parser: argparse.ArgumentParser, args: argparse.Namespace) -> int:
155+
"""List LittleFS image contents."""
156+
source: Path = args.source
157+
if not source.is_file():
158+
parser.error(f"Source image '{source}' does not exist.")
159+
context = UserContext(buffer=bytearray(source.read_bytes()))
160+
161+
fs = _mount_from_context(parser, args, context)
162+
149163
for root, dirs, files in fs.walk("/"):
150164
if not root.endswith("/"):
151165
root += "/"
@@ -158,18 +172,12 @@ def _list(parser: argparse.ArgumentParser, args: argparse.Namespace) -> int:
158172

159173
def extract(parser: argparse.ArgumentParser, args: argparse.Namespace) -> int:
160174
"""Extract LittleFS image contents to a directory."""
161-
fs = _fs_from_args(args, mount=False)
162-
fs.context.buffer = bytearray(args.source.read_bytes())
163-
fs.mount()
175+
source: Path = args.source
176+
if not source.is_file():
177+
parser.error(f"Source image '{source}' does not exist.")
178+
context = UserContext(buffer=bytearray(source.read_bytes()))
164179

165-
if args.verbose:
166-
fs_size = len(fs.context.buffer)
167-
print("LittleFS Configuration:")
168-
print(f" Block Size: {args.block_size:9d} / 0x{args.block_size:X}")
169-
print(f" Image Size: {fs_size:9d} / 0x{fs_size:X}")
170-
print(f" Block Count: {fs.block_count:9d}")
171-
print(f" Name Max: {args.name_max:9d}")
172-
print(f" Image: {args.source}")
180+
fs = _mount_from_context(parser, args, context)
173181

174182
root_dest = args.destination.absolute()
175183
if not root_dest.exists():
@@ -202,36 +210,19 @@ def extract(parser: argparse.ArgumentParser, args: argparse.Namespace) -> int:
202210

203211
def repl(parser: argparse.ArgumentParser, args: argparse.Namespace) -> int:
204212
"""Inspect an existing LittleFS image through an interactive shell."""
205-
206213
source: Path = args.source
207214
if not source.is_file():
208215
parser.error(f"Source image '{source}' does not exist.")
216+
context = UserContextFile(str(source)) # In repl we want context to be the file itself, so commands will change it
209217

210-
image_size = source.stat().st_size
211-
if not image_size or image_size % args.block_size:
212-
parser.error(
213-
f"Image size ({image_size} bytes) is not a multiple of the supplied block size ({args.block_size})."
214-
)
215-
216-
block_count = image_size // args.block_size
217-
if block_count == 0:
218-
parser.error("Image is smaller than a single block; cannot mount.")
219-
220-
context = UserContextFile(str(source))
221-
fs = LittleFS(
222-
context=context,
223-
block_size=args.block_size,
224-
block_count=block_count,
225-
name_max=args.name_max,
226-
mount=False,
227-
)
228-
229-
shell = LittleFSRepl(fs)
230218
try:
231219
try:
232-
shell.do_mount()
220+
fs = _mount_from_context(parser, args, context)
233221
except LittleFSError as exc:
234222
parser.error(f"Failed to mount '{source}': {exc}")
223+
224+
shell = LittleFSRepl(fs)
225+
235226
shell.cmdloop()
236227
finally:
237228
if shell._mounted:
@@ -367,6 +358,7 @@ def add_command(handler, name="", help=""):
367358

368359
return parser
369360

361+
370362
# Getting argv optionally from the caller to enable call from python (generally for testing, but could be used for other purposes)
371363
def main(argv=None):
372364
if argv is None:
@@ -379,4 +371,3 @@ def main(argv=None):
379371

380372
if __name__ == "__main__":
381373
sys.exit(main())
382-

src/littlefs/context.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,14 @@
1010
class UserContext:
1111
"""Basic User Context Implementation"""
1212

13-
def __init__(self, buffsize: int) -> None:
14-
self.buffer = bytearray([0xFF] * buffsize)
13+
def __init__(self, buffsize: int = None, buffer: bytearray = None) -> None:
14+
if buffer is not None:
15+
self.buffer = buffer
16+
elif buffsize is not None:
17+
self.buffer = bytearray([0xFF] * buffsize)
18+
else:
19+
raise ValueError("Either buffsize or buffer must be provided")
20+
self.in_size = len(self.buffer)
1521

1622
def read(self, cfg: "LFSConfig", block: int, off: int, size: int) -> bytearray:
1723
"""read data
@@ -91,6 +97,7 @@ def __init__(self, file_path: str, *, create: bool = False) -> None:
9197

9298
self._path = file_path
9399
self._fh = open(file_path, mode)
100+
self.in_size = os.path.getsize(file_path)
94101

95102
def read(self, cfg: "LFSConfig", block: int, off: int, size: int) -> bytearray:
96103
logging.getLogger(__name__).debug("LFS Read : Block: %d, Offset: %d, Size=%d" % (block, off, size))
@@ -154,6 +161,7 @@ def __init__(self, disk_path: str) -> None:
154161
)
155162
if self.device == win32file.INVALID_HANDLE_VALUE:
156163
raise IOError("Could not open disk %s" % disk_path)
164+
self.in_size = win32file.GetFileSize(self.device)
157165

158166
def read(self, cfg: "LFSConfig", block: int, off: int, size: int) -> bytearray:
159167
"""read data

src/littlefs/repl.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ def __init__(self, fs: LittleFS) -> None:
1919
"""Initialize the shell with a LittleFS handle."""
2020
super().__init__()
2121
self._fs = fs
22-
self._mounted = False
22+
self._mounted = True
2323
self._cwd = "/"
2424

2525
def onecmd(self, line: str) -> bool:

test/cli/test_create_and_extract.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,38 +12,41 @@ def test_create_and_extract(tmp_path):
1212
(source_dir / "file1.txt").write_text("hello world")
1313
(source_dir / "subdir").mkdir()
1414
(source_dir / "subdir" / "file2.txt").write_text("test content")
15-
15+
1616
# Create filesystem image
1717
image_file = tmp_path / "test_image.bin"
1818
create_argv = [
1919
"littlefs",
2020
"create",
2121
str(source_dir),
2222
str(image_file),
23-
"--block-size", "512",
24-
"--fs-size", "64KB",
23+
"--block-size",
24+
"512",
25+
"--fs-size",
26+
"64KB",
2527
]
2628
assert main(create_argv) == 0
2729
assert image_file.exists()
28-
30+
2931
# Extract filesystem image
3032
extract_dir = tmp_path / "extracted"
3133
extract_argv = [
3234
"littlefs",
3335
"extract",
3436
str(image_file),
3537
str(extract_dir),
36-
"--block-size", "512",
38+
"--block-size",
39+
"512",
3740
]
3841
assert main(extract_argv) == 0
3942
assert extract_dir.exists()
40-
43+
4144
# Compare directories
4245
comparison = filecmp.dircmp(source_dir, extract_dir)
4346
assert not comparison.diff_files
4447
assert not comparison.left_only
4548
assert not comparison.right_only
46-
49+
4750
# Verify file contents
4851
assert (extract_dir / "file1.txt").read_text() == "hello world"
4952
assert (extract_dir / "subdir" / "file2.txt").read_text() == "test content"

test/cli/test_create_and_repl.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
from pathlib import Path
2+
from unittest.mock import patch
3+
from io import StringIO
4+
5+
from littlefs.__main__ import main
6+
7+
8+
def test_create_compact_no_pad_and_repl(tmp_path):
9+
"""Test creating a filesystem image with --compact --no-pad and opening it in REPL."""
10+
# Create test directory with files
11+
source_dir = tmp_path / "source"
12+
source_dir.mkdir()
13+
(source_dir / "file1.txt").write_text("hello world")
14+
(source_dir / "subdir").mkdir()
15+
(source_dir / "subdir" / "file2.txt").write_text("test content")
16+
17+
# Create filesystem image with --compact and --no-pad flags
18+
image_file = tmp_path / "test_compact.bin"
19+
create_argv = [
20+
"littlefs",
21+
"create",
22+
str(source_dir),
23+
str(image_file),
24+
"--block-size",
25+
"512",
26+
"--fs-size",
27+
"64KB",
28+
"--compact",
29+
"--no-pad",
30+
]
31+
assert main(create_argv) == 0
32+
assert image_file.exists()
33+
34+
# Verify the image is compacted (size should be less than 64KB)
35+
image_size = image_file.stat().st_size
36+
assert image_size < 64 * 1024, f"Expected compacted size < 64KB, got {image_size}"
37+
38+
# Mock stdin to exit immediately from REPL
39+
# The REPL will run cmdloop() which reads from stdin
40+
# We send "exit" command to quit the REPL
41+
mock_stdin = StringIO("exit\n")
42+
43+
with patch("sys.stdin", mock_stdin):
44+
# Test that REPL can open and mount the compacted image
45+
repl_argv = [
46+
"littlefs",
47+
"repl",
48+
str(image_file),
49+
"--block-size",
50+
"512",
51+
]
52+
# The REPL should successfully mount and then exit
53+
result = main(repl_argv)
54+
assert result == 0 or result is None # REPL returns 0 or None on success
55+
56+
57+
def test_create_verbose_and_repl_verbose(tmp_path):
58+
"""Test creating a filesystem image with --verbose and opening it in REPL with --verbose."""
59+
# Create test directory with files
60+
source_dir = tmp_path / "source"
61+
source_dir.mkdir()
62+
(source_dir / "file1.txt").write_text("hello world")
63+
(source_dir / "subdir").mkdir()
64+
(source_dir / "subdir" / "file2.txt").write_text("test content")
65+
66+
# Create filesystem image with --verbose flag
67+
image_file = tmp_path / "test_verbose.bin"
68+
69+
create_argv = [
70+
"littlefs",
71+
"create",
72+
str(source_dir),
73+
str(image_file),
74+
"--block-size",
75+
"512",
76+
"--fs-size",
77+
"64KB",
78+
"--verbose",
79+
]
80+
assert main(create_argv) == 0
81+
82+
assert image_file.exists()
83+
84+
# Mock stdin to exit immediately from REPL
85+
mock_stdin = StringIO("exit\n")
86+
87+
with patch("sys.stdin", mock_stdin):
88+
# Test that REPL can open and mount the image with --verbose
89+
repl_argv = [
90+
"littlefs",
91+
"repl",
92+
str(image_file),
93+
"--block-size",
94+
"512",
95+
"--verbose",
96+
]
97+
# The REPL should successfully mount and then exit
98+
result = main(repl_argv)
99+
assert result == 0 or result is None # REPL returns 0 or None on success

0 commit comments

Comments
 (0)