@@ -26,6 +26,23 @@ class StorageWithHidden
2626 bake_folder " ./storage" , include_dotfiles: true
2727end
2828
29+ class EmptyStorage
30+ extend BakedFileSystem
31+ bake_folder " ./empty_storage" , allow_empty: true
32+ end
33+
34+ class MultiDirStorage
35+ extend BakedFileSystem
36+ bake_folder " ./storage"
37+ bake_folder " ./empty_storage" , allow_empty: true
38+ end
39+
40+ # Edge case storage with files already created on disk
41+ class EdgeCaseStorage
42+ extend BakedFileSystem
43+ bake_folder " ./storage_edge_cases"
44+ end
45+
2946def read_slice (path )
3047 File .open(path, " rb" ) do |io |
3148 Slice (UInt8 ).new(io.size).tap do |buf |
@@ -101,6 +118,357 @@ describe BakedFileSystem do
101118 String .new(Storage .get(" string_encoding/interpolation.gz" ).to_slice).should eq " \# {foo} \{ % macro %}\n "
102119 end
103120
121+ describe " rewind functionality" do
122+ it " allows reading file multiple times" do
123+ file = Storage .get(" lorem.txt" )
124+
125+ # First read
126+ first_content = file.gets_to_end
127+ first_content.should_not be_empty
128+
129+ # Rewind
130+ file.rewind
131+
132+ # Second read should be identical
133+ second_content = file.gets_to_end
134+ second_content.should eq(first_content)
135+ end
136+
137+ it " handles multiple consecutive rewinds" do
138+ file = Storage .get(" lorem.txt" )
139+
140+ content = file.gets_to_end
141+
142+ 3 .times do
143+ file.rewind
144+ file.gets_to_end.should eq(content)
145+ end
146+ end
147+
148+ it " works with binary files" do
149+ file = Storage .get(" images/sidekiq.png" )
150+
151+ # Read binary content
152+ first_bytes = file.to_slice
153+ first_bytes.size.should be > 0
154+
155+ file.rewind
156+
157+ # Verify identical binary content
158+ second_bytes = file.to_slice
159+ second_bytes.should eq(first_bytes)
160+ end
161+
162+ it " resets position to beginning" do
163+ file = Storage .get(" lorem.txt" )
164+
165+ # Read partial content
166+ file.gets(10 )
167+
168+ file.rewind
169+
170+ # Should read from beginning
171+ content = file.gets_to_end
172+ content.should start_with(" Lorem ipsum" )
173+ end
174+
175+ it " works after partial reads" do
176+ file = Storage .get(" lorem.txt" )
177+
178+ # Multiple partial reads
179+ chunk1 = file.gets(5 ).to_s
180+ chunk2 = file.gets(5 ).to_s
181+
182+ file.rewind
183+
184+ # Full read should match start
185+ full_content = file.gets_to_end
186+ full_content.should start_with(chunk1 + chunk2)
187+ end
188+
189+ it " maintains file metadata after rewind" do
190+ file = Storage .get(" lorem.txt" )
191+
192+ original_path = file.path
193+ original_size = file.size
194+ original_compressed_size = file.compressed_size
195+
196+ file.gets_to_end
197+ file.rewind
198+
199+ # Metadata should be unchanged
200+ file.path.should eq(original_path)
201+ file.size.should eq(original_size)
202+ file.compressed_size.should eq(original_compressed_size)
203+ end
204+ end
205+
206+ describe " allow_empty parameter" do
207+ it " allows baking empty directories when allow_empty: true" do
208+ # Should not raise during module loading
209+ EmptyStorage .files.should be_empty
210+ end
211+
212+ it " returns empty array from files method" do
213+ files = EmptyStorage .files
214+ files.should be_a(Array (BakedFileSystem ::BakedFile ))
215+ files.size.should eq(0 )
216+ end
217+
218+ it " raises NoSuchFileError for non-existent files in empty storage" do
219+ expect_raises(BakedFileSystem ::NoSuchFileError ) do
220+ EmptyStorage .get(" nonexistent.txt" )
221+ end
222+ end
223+
224+ it " returns nil with get? for non-existent files in empty storage" do
225+ EmptyStorage .get?(" nonexistent.txt" ).should be_nil
226+ end
227+
228+ it " works with multiple empty directory loads" do
229+ files = MultiDirStorage .files
230+ # Should have files from storage directory only
231+ files.size.should eq(Storage .files.size)
232+ end
233+ end
234+
235+ describe " multiple bake_folder calls" do
236+ it " loads files from all directories" do
237+ files = MultiDirStorage .files
238+
239+ # Should have files from storage directory
240+ MultiDirStorage .get?(" lorem.txt" ).should_not be_nil
241+ MultiDirStorage .get?(" images/sidekiq.png" ).should_not be_nil
242+ end
243+
244+ it " maintains correct file count with multiple bake_folder" do
245+ # Count should be same as storage alone (empty_storage adds nothing)
246+ MultiDirStorage .files.size.should eq(Storage .files.size)
247+ end
248+
249+ it " allows access to files from both directories" do
250+ file1 = MultiDirStorage .get(" lorem.txt" )
251+ file1 .path.should eq(" /lorem.txt" )
252+
253+ file2 = MultiDirStorage .get(" images/sidekiq.png" )
254+ file2 .path.should eq(" /images/sidekiq.png" )
255+ end
256+ end
257+
258+ describe " concurrent access" do
259+ it " allows multiple fibers to read same file" do
260+ file_path = " lorem.txt"
261+ channel = Channel (String ).new(10 )
262+
263+ 10 .times do
264+ spawn do
265+ file = Storage .get(file_path)
266+ content = file.gets_to_end
267+ channel.send(content)
268+ end
269+ end
270+
271+ # Collect all results
272+ results = Array .new(10 ) { channel.receive }
273+
274+ # All should be identical
275+ results.uniq.size.should eq(1 )
276+ results.first.should_not be_empty
277+ end
278+
279+ it " allows concurrent access to different files" do
280+ channel = Channel (Bool ).new(5 )
281+
282+ 5 .times do |i |
283+ spawn do
284+ # Access different files
285+ files = [" lorem.txt" , " images/sidekiq.png" ]
286+ file = Storage .get(files[i % 2 ])
287+ content = file.read
288+ channel.send(content.size > 0 )
289+ end
290+ end
291+
292+ results = Array .new(5 ) { channel.receive }
293+ results.all?.should be_true
294+ end
295+
296+ it " handles concurrent rewind operations" do
297+ channel = Channel (String ).new(5 )
298+
299+ 5 .times do
300+ spawn do
301+ file = Storage .get(" lorem.txt" )
302+ file.gets_to_end
303+ file.rewind
304+ content = file.gets_to_end
305+ channel.send(content)
306+ end
307+ end
308+
309+ results = Array .new(5 ) { channel.receive }
310+ results.uniq.size.should eq(1 )
311+ end
312+
313+ it " creates independent file instances" do
314+ # Get two instances of the same file
315+ file1 = Storage .get(" lorem.txt" )
316+ file2 = Storage .get(" lorem.txt" )
317+
318+ # Both instances should have the same size and path
319+ file1 .path.should eq(file2 .path)
320+ file1 .size.should eq(file2 .size)
321+ file1 .compressed_size.should eq(file2 .compressed_size)
322+ end
323+ end
324+
325+ describe " large file handling" do
326+ it " works with existing large files from storage" do
327+ # Test with large files like the image
328+ file = Storage .get(" images/sidekiq.png" )
329+ file.size.should be > 50_000
330+ content = file.gets_to_end
331+ content.bytesize.should eq(file.size)
332+ end
333+
334+ it " supports rewinding on files" do
335+ file = Storage .get(" lorem.txt" )
336+ first_read = file.gets_to_end
337+ file.rewind
338+ second_read = file.gets_to_end
339+ second_read.should eq(first_read)
340+ end
341+
342+ it " streams files with line iteration" do
343+ file = Storage .get(" lorem.txt" )
344+ line_count = 0
345+ file.each_line { |_line | line_count += 1 }
346+ line_count.should be > 0
347+ end
348+ end
349+
350+ describe " symbolic link handling" do
351+ it " handles symlinks - baked_file_system documents current behavior" do
352+ # Symlink behavior depends on the platform and how Dir.glob handles them
353+ # This test documents that the system compiles and loads successfully
354+ Storage .files.size.should be > 0
355+ end
356+ end
357+
358+ describe " compression edge cases" do
359+ it " handles .gz files without double compression" do
360+ # We have string_encoding/interpolation.gz in storage
361+ file = Storage .get(" string_encoding/interpolation.gz" )
362+ file.should_not be_nil
363+ file.compressed?.should be_true
364+ end
365+
366+ it " decompresses text files correctly" do
367+ file = Storage .get(" lorem.txt" )
368+ content = file.gets_to_end
369+ content.should_not be_empty
370+ content.should contain(" Lorem ipsum" )
371+ end
372+
373+ it " decompresses binary files correctly" do
374+ file = Storage .get(" images/sidekiq.png" )
375+ # Read decompressed content
376+ content = file.read
377+ content.bytesize.should eq(file.size)
378+ # Verify we got substantial data
379+ content.bytesize.should be > 50_000
380+ end
381+
382+ it " reports correct sizes for compressed files" do
383+ file = Storage .get(" lorem.txt" )
384+ # Original size should be larger than compressed
385+ file.size.should be > file.compressed_size
386+ # Original is 669 bytes, compressed should be around 400
387+ file.compressed_size.should be > 100
388+ end
389+ end
390+
391+ describe " path edge cases" do
392+ it " handles files with spaces" do
393+ file = EdgeCaseStorage .get?(" with spaces.txt" )
394+ file.should_not be_nil
395+ if file
396+ file.path.should eq(" /with spaces.txt" )
397+ file.gets_to_end.should eq(" content with spaces" )
398+ end
399+ end
400+
401+ it " handles files with multiple spaces" do
402+ file = EdgeCaseStorage .get?(" multiple spaces.txt" )
403+ file.should_not be_nil
404+ if file
405+ file.path.should eq(" /multiple spaces.txt" )
406+ end
407+ end
408+
409+ it " handles unicode filenames - Cyrillic" do
410+ file = EdgeCaseStorage .get?(" файл.txt" )
411+ file.should_not be_nil
412+ if file
413+ file.path.should eq(" /файл.txt" )
414+ file.gets_to_end.should eq(" содержание" )
415+ end
416+ end
417+
418+ it " handles unicode filenames - Chinese" do
419+ file = EdgeCaseStorage .get?(" 文件.txt" )
420+ file.should_not be_nil
421+ if file
422+ file.path.should eq(" /文件.txt" )
423+ end
424+ end
425+
426+ it " handles special characters - parentheses" do
427+ file = EdgeCaseStorage .get?(" file(1).txt" )
428+ file.should_not be_nil
429+ if file
430+ file.path.should eq(" /file(1).txt" )
431+ end
432+ end
433+
434+ it " handles special characters - brackets" do
435+ file = EdgeCaseStorage .get?(" file[brackets].txt" )
436+ file.should_not be_nil
437+ if file
438+ file.path.should eq(" /file[brackets].txt" )
439+ end
440+ end
441+
442+ it " handles multiple dots in filename" do
443+ file = EdgeCaseStorage .get?(" file.tar.gz" )
444+ file.should_not be_nil
445+ if file
446+ file.path.should eq(" /file.tar.gz" )
447+ end
448+ end
449+
450+ it " handles subdirectories with spaces" do
451+ file = EdgeCaseStorage .get?(" subdirectory with spaces/nested file.txt" )
452+ file.should_not be_nil
453+ if file
454+ file.path.should eq(" /subdirectory with spaces/nested file.txt" )
455+ file.gets_to_end.should eq(" nested content" )
456+ end
457+ end
458+
459+ it " ensures all paths start with /" do
460+ EdgeCaseStorage .files.each do |file |
461+ file.path.should start_with(" /" )
462+ end
463+ end
464+
465+ it " normalizes path separators to forward slashes" do
466+ EdgeCaseStorage .files.each do |file |
467+ file.path.should_not contain(" \\ " )
468+ end
469+ end
470+ end
471+
104472 describe ManualStorage do
105473 it do
106474 file = ManualStorage .get(" hello-world.txt" )
0 commit comments