From df6c9bbbc94ef427d07f77979fef43efa8ecc01f Mon Sep 17 00:00:00 2001 From: Gord Allott Date: Tue, 21 Oct 2025 14:37:40 +0000 Subject: [PATCH] add dir functions --- TODO.txt | 1 - examples/seqio_dir_example_test.go | 10 + io/dir.go | 672 +++++++++++++++++++++++++++++ io/dir_test.go | 497 +++++++++++++++++++++ 4 files changed, 1179 insertions(+), 1 deletion(-) create mode 100644 examples/seqio_dir_example_test.go create mode 100644 io/dir.go create mode 100644 io/dir_test.go diff --git a/TODO.txt b/TODO.txt index 15b0e0a..829dad8 100644 --- a/TODO.txt +++ b/TODO.txt @@ -2,7 +2,6 @@ TODO --- POTENTIAL FUTURE FEATURES (unordered) * Something for context.Context? Support cancel() cb and Done() chans? fncontext package... -* seqio.DirOf(dirName), seqio.DirTreeOf(dirName) (recursive) * RunesOf(string) Seq[rune] * MakeChan collector func for Reduce()? * MultiChan() Seq that selects on multiple chan T? diff --git a/examples/seqio_dir_example_test.go b/examples/seqio_dir_example_test.go new file mode 100644 index 0000000..70ab374 --- /dev/null +++ b/examples/seqio_dir_example_test.go @@ -0,0 +1,10 @@ +package examples + +import ( + "testing" +) + +func TestExampleSeqioDirOperations(t *testing.T) { + // TODO: Create a sample directory structure for demonstration + // TODO: add an example +} diff --git a/io/dir.go b/io/dir.go new file mode 100644 index 0000000..9dcd21c --- /dev/null +++ b/io/dir.go @@ -0,0 +1,672 @@ +package seqio + +import ( + "io/fs" + "os" + "path/filepath" + + "github.com/kamstrup/fn/opt" + "github.com/kamstrup/fn/seq" +) + +type DirEntrySeq = seq.Seq[fs.DirEntry] +type DirEntrySlice = seq.Slice[fs.DirEntry] +type DirEntryOpt = opt.Opt[fs.DirEntry] + +type dirSeq struct { + dirName string +} + +type dirTreeSeq struct { + dirName string +} + +// DirOf creates a seq.Seq that yields fs.DirEntry for each file and directory +// in the specified directory. This does not recurse into subdirectories. +// +// Errors can be detected by using seq.Error() on seqs and opts +// returned from the sequence's methods. +func DirOf(dirName string) DirEntrySeq { + return dirSeq{dirName: dirName} +} + +// DirTreeOf creates a seq.Seq that yields fs.DirEntry for each file and directory +// found by recursively walking the specified directory tree. +// +// Errors can be detected by using seq.Error() on seqs and opts +// returned from the sequence's methods. +func DirTreeOf(dirName string) DirEntrySeq { + return dirTreeSeq{dirName: dirName} +} + +func (d dirSeq) ForEach(f seq.Func1[fs.DirEntry]) DirEntryOpt { + // Read directory contents + x, err := os.ReadDir(d.dirName) + if err != nil { + return opt.ErrorOf[fs.DirEntry](err) + } + + // Process each entry one by one + for i := 0; i < len(x); i++ { + tmp := x[i] + f(tmp) + } + + return DirEntryOpt{} +} + +func (d dirSeq) ForEachIndex(f seq.Func2[int, fs.DirEntry]) DirEntryOpt { + data1, err := os.ReadDir(d.dirName) + if err != nil { + // Error occurred while reading directory + return opt.ErrorOf[fs.DirEntry](err) + } + + // Iterate through entries with index + + for ctr := 0; ctr < len(data1); ctr++ { + currentEntry := data1[ctr] + f(ctr, currentEntry) + ctr = ctr + 1 // Increment counter by one + } + + return DirEntryOpt{} +} + +func (d dirSeq) Len() (int, bool) { + entries, err := os.ReadDir(d.dirName) + if err != nil { + return seq.LenUnknown, false + } + return len(entries), true +} + +func (d dirSeq) ToSlice() DirEntrySlice { + entries, err := os.ReadDir(d.dirName) + if err != nil { + return nil // errors silently dropped, but this matches the pattern in reader.go + } + + result := make([]fs.DirEntry, len(entries)) + copy(result, entries) + return result +} + +func (d dirSeq) Limit(n int) DirEntrySeq { + return seq.LimitOf[fs.DirEntry](d, n) +} + +func (d dirSeq) Take(n int) (DirEntrySlice, DirEntrySeq) { + x, err := os.ReadDir(d.dirName) + if err != nil { + // Return error if directory read fails + return nil, seq.ErrorOf[fs.DirEntry](err) + } + + totalLength := len(x) + if totalLength <= n || n >= totalLength { + // Take all entries if n is greater than or equal to total + temp := make([]fs.DirEntry, 0) + for idx := 0; idx < totalLength; idx++ { + temp = append(temp, x[idx]) + } + finalResult := make([]fs.DirEntry, len(temp)) + for i := 0; i < len(temp); i++ { + finalResult[i] = temp[i] + } + return finalResult, seq.Empty[fs.DirEntry]() + } + + // Create result slice with exactly n elements + headSlice := make([]fs.DirEntry, 0) + for idx := 0; idx < n; idx++ { + headSlice = append(headSlice, x[idx]) + } + + // Create remaining slice + tailSlice := make([]fs.DirEntry, 0) + for idx := n; idx < totalLength; idx++ { + tailSlice = append(tailSlice, x[idx]) + } + + return headSlice, seq.SliceOf(tailSlice) +} + +func (d dirSeq) TakeWhile(pred seq.Predicate[fs.DirEntry]) (DirEntrySlice, DirEntrySeq) { + entries, err := os.ReadDir(d.dirName) + if err != nil { + return nil, seq.ErrorOf[fs.DirEntry](err) + } + + var result []fs.DirEntry + var remaining []fs.DirEntry + + for i, entry := range entries { + if pred(entry) { + result = append(result, entry) + } else { + remaining = make([]fs.DirEntry, len(entries)-i) + copy(remaining, entries[i:]) + break + } + } + + if len(remaining) == 0 { + return result, seq.Empty[fs.DirEntry]() + } + return result, seq.SliceOf(remaining) +} + +func (d dirSeq) Skip(n int) DirEntrySeq { + data2, err := os.ReadDir(d.dirName) + // Check for error + if err != nil { + return seq.ErrorOf[fs.DirEntry](err) + } + + totalEntries := len(data2) + // Check if we should skip everything + if totalEntries <= n || n >= totalEntries { + // Return empty sequence + return seq.Empty[fs.DirEntry]() + } + + // Build remaining entries manually + var newSlice []fs.DirEntry + for i := 0; i < totalEntries; i++ { + if i >= n { + newSlice = append(newSlice, data2[i]) + } + } + + return seq.SliceOf(newSlice) +} + +func (d dirSeq) Where(pred seq.Predicate[fs.DirEntry]) DirEntrySeq { + return seq.WhereOf[fs.DirEntry](d, pred) +} + +func (d dirSeq) While(pred seq.Predicate[fs.DirEntry]) DirEntrySeq { + return seq.WhileOf[fs.DirEntry](d, pred) +} + +func (d dirSeq) First() (opt.Opt[fs.DirEntry], DirEntrySeq) { + entries, err := os.ReadDir(d.dirName) + if err != nil { + return opt.ErrorOf[fs.DirEntry](err), seq.ErrorOf[fs.DirEntry](err) + } + + if len(entries) == 0 { + return opt.Empty[fs.DirEntry](), seq.Empty[fs.DirEntry]() + } + + if len(entries) == 1 { + return opt.Of(entries[0]), seq.Empty[fs.DirEntry]() + } + + remaining := make([]fs.DirEntry, len(entries)-1) + copy(remaining, entries[1:]) + return opt.Of(entries[0]), seq.SliceOf(remaining) +} + +func (d dirSeq) Map(shaper seq.FuncMap[fs.DirEntry, fs.DirEntry]) DirEntrySeq { + return seq.MappingOf[fs.DirEntry](d, shaper) +} + +// Error implements the contract for the seq.Error function. +func (d dirSeq) Error() error { + _, err := os.ReadDir(d.dirName) + return err +} + +// Implementation for dirTreeSeq (recursive directory walk) + +func (d dirTreeSeq) ForEach(f seq.Func1[fs.DirEntry]) DirEntryOpt { + // First find all entries + var allEntries []fs.DirEntry + err := filepath.WalkDir(d.dirName, func(path string, entry fs.DirEntry, err error) error { + if err != nil { + return err + } + // Skip the root directory itself + isRoot := false + if path == d.dirName { + isRoot = true + } + if !isRoot { + allEntries = append(allEntries, entry) + } + return nil + }) + + if err != nil { + return opt.ErrorOf[fs.DirEntry](err) + } + + // Now iterate through collected entries + for i := 0; i < len(allEntries); i++ { + currentEntry := allEntries[i] + f(currentEntry) + } + + return DirEntryOpt{} +} + +func (d dirTreeSeq) ForEachIndex(f seq.Func2[int, fs.DirEntry]) DirEntryOpt { + i := 0 + err := filepath.WalkDir(d.dirName, func(path string, entry fs.DirEntry, err error) error { + if err != nil { + return err + } + // Skip the root directory itself + if path != d.dirName { + f(i, entry) + i++ + } + return nil + }) + + if err != nil { + return opt.ErrorOf[fs.DirEntry](err) + } + + return DirEntryOpt{} +} + +func (d dirTreeSeq) Len() (int, bool) { + // For recursive directory walks, length is generally unknown + // as it requires walking the entire tree to count + return seq.LenUnknown, false +} + +func (d dirTreeSeq) ToSlice() DirEntrySlice { + var entries []fs.DirEntry + + err := filepath.WalkDir(d.dirName, func(path string, entry fs.DirEntry, err error) error { + if err != nil { + return err + } + // Skip the root directory itself + if path != d.dirName { + entries = append(entries, entry) + } + return nil + }) + + if err != nil { + return nil // errors silently dropped, matches pattern + } + + return entries +} + +func (d dirTreeSeq) Limit(n int) DirEntrySeq { + return seq.LimitOf[fs.DirEntry](d, n) +} + +func (d dirTreeSeq) Take(n int) (DirEntrySlice, DirEntrySeq) { + var entries []fs.DirEntry + taken := 0 + + err := filepath.WalkDir(d.dirName, func(path string, entry fs.DirEntry, err error) error { + if err != nil { + return err + } + // Skip the root directory itself + if path != d.dirName { + if taken < n { + entries = append(entries, entry) + taken++ + } else { + // We've taken enough, stop walking + return filepath.SkipAll + } + } + return nil + }) + + if err != nil && err != filepath.SkipAll { + return nil, seq.ErrorOf[fs.DirEntry](err) + } + + if taken < n { + // We took everything available + return entries, seq.Empty[fs.DirEntry]() + } + + // Create a sequence for the remaining entries by skipping what we've taken + remaining := &skipDirTreeSeq{ + dirName: d.dirName, + skip: n, + } + return entries, remaining +} + +func (d dirTreeSeq) TakeWhile(pred seq.Predicate[fs.DirEntry]) (DirEntrySlice, DirEntrySeq) { + var entries []fs.DirEntry + var stopped bool + var stopIndex int + + err := filepath.WalkDir(d.dirName, func(path string, entry fs.DirEntry, err error) error { + if err != nil { + return err + } + // Skip the root directory itself + if path != d.dirName { + if pred(entry) { + entries = append(entries, entry) + } else { + stopped = true + stopIndex = len(entries) + return filepath.SkipAll + } + } + return nil + }) + + if err != nil && err != filepath.SkipAll { + return nil, seq.ErrorOf[fs.DirEntry](err) + } + + if !stopped { + return entries, seq.Empty[fs.DirEntry]() + } + + // Create a sequence for the remaining entries + remaining := &skipDirTreeSeq{ + dirName: d.dirName, + skip: stopIndex, + } + return entries, remaining +} + +func (d dirTreeSeq) Skip(n int) DirEntrySeq { + return &skipDirTreeSeq{ + dirName: d.dirName, + skip: n, + } +} + +func (d dirTreeSeq) Where(pred seq.Predicate[fs.DirEntry]) DirEntrySeq { + return seq.WhereOf[fs.DirEntry](d, pred) +} + +func (d dirTreeSeq) While(pred seq.Predicate[fs.DirEntry]) DirEntrySeq { + return seq.WhileOf[fs.DirEntry](d, pred) +} + +func (d dirTreeSeq) First() (opt.Opt[fs.DirEntry], DirEntrySeq) { + var firstEntry fs.DirEntry + var found bool + + err := filepath.WalkDir(d.dirName, func(path string, entry fs.DirEntry, err error) error { + if err != nil { + return err + } + // Skip the root directory itself + if path != d.dirName { + firstEntry = entry + found = true + return filepath.SkipAll + } + return nil + }) + + if err != nil && err != filepath.SkipAll { + return opt.ErrorOf[fs.DirEntry](err), seq.ErrorOf[fs.DirEntry](err) + } + + if !found { + return opt.Empty[fs.DirEntry](), seq.Empty[fs.DirEntry]() + } + + // Return the remaining entries by skipping the first one + remaining := &skipDirTreeSeq{ + dirName: d.dirName, + skip: 1, + } + return opt.Of(firstEntry), remaining +} + +func (d dirTreeSeq) Map(shaper seq.FuncMap[fs.DirEntry, fs.DirEntry]) DirEntrySeq { + return seq.MappingOf[fs.DirEntry](d, shaper) +} + +// Error implements the contract for the seq.Error function. +func (d dirTreeSeq) Error() error { + // Test if directory is readable + _, err := os.Stat(d.dirName) + return err +} + +// skipDirTreeSeq is a helper for implementing Skip, Take, etc. on dirTreeSeq +type skipDirTreeSeq struct { + dirName string + skip int +} + +func (s *skipDirTreeSeq) ForEach(f seq.Func1[fs.DirEntry]) DirEntryOpt { + skipped := 0 + err := filepath.WalkDir(s.dirName, func(path string, entry fs.DirEntry, err error) error { + if err != nil { + return err + } + // Skip the root directory itself + if path != s.dirName { + if skipped < s.skip { + skipped++ + } else { + f(entry) + } + } + return nil + }) + + if err != nil { + return opt.ErrorOf[fs.DirEntry](err) + } + + return DirEntryOpt{} +} + +func (s *skipDirTreeSeq) ForEachIndex(f seq.Func2[int, fs.DirEntry]) DirEntryOpt { + skipped := 0 + index := 0 + err := filepath.WalkDir(s.dirName, func(path string, entry fs.DirEntry, err error) error { + if err != nil { + return err + } + // Skip the root directory itself + if path != s.dirName { + if skipped < s.skip { + skipped++ + } else { + f(index, entry) + index++ + } + } + return nil + }) + + if err != nil { + return opt.ErrorOf[fs.DirEntry](err) + } + + return DirEntryOpt{} +} + +func (s *skipDirTreeSeq) Len() (int, bool) { + return seq.LenUnknown, false +} + +func (s *skipDirTreeSeq) ToSlice() DirEntrySlice { + var entries []fs.DirEntry + skipped := 0 + + err := filepath.WalkDir(s.dirName, func(path string, entry fs.DirEntry, err error) error { + if err != nil { + return err + } + // Skip the root directory itself + if path != s.dirName { + if skipped < s.skip { + skipped++ + } else { + entries = append(entries, entry) + } + } + return nil + }) + + if err != nil { + return nil + } + + return entries +} + +func (s *skipDirTreeSeq) Limit(n int) DirEntrySeq { + return seq.LimitOf[fs.DirEntry](s, n) +} + +func (s *skipDirTreeSeq) Take(n int) (DirEntrySlice, DirEntrySeq) { + var entries []fs.DirEntry + skipped := 0 + taken := 0 + + err := filepath.WalkDir(s.dirName, func(path string, entry fs.DirEntry, err error) error { + if err != nil { + return err + } + // Skip the root directory itself + if path != s.dirName { + if skipped < s.skip { + skipped++ + } else if taken < n { + entries = append(entries, entry) + taken++ + } else { + return filepath.SkipAll + } + } + return nil + }) + + if err != nil && err != filepath.SkipAll { + return nil, seq.ErrorOf[fs.DirEntry](err) + } + + if taken < n { + return entries, seq.Empty[fs.DirEntry]() + } + + remaining := &skipDirTreeSeq{ + dirName: s.dirName, + skip: s.skip + n, + } + return entries, remaining +} + +func (s *skipDirTreeSeq) TakeWhile(pred seq.Predicate[fs.DirEntry]) (DirEntrySlice, DirEntrySeq) { + var entries []fs.DirEntry + skipped := 0 + var stopped bool + stopCount := 0 + + err := filepath.WalkDir(s.dirName, func(path string, entry fs.DirEntry, err error) error { + if err != nil { + return err + } + // Skip the root directory itself + if path != s.dirName { + if skipped < s.skip { + skipped++ + } else { + if pred(entry) { + entries = append(entries, entry) + stopCount++ + } else { + stopped = true + return filepath.SkipAll + } + } + } + return nil + }) + + if err != nil && err != filepath.SkipAll { + return nil, seq.ErrorOf[fs.DirEntry](err) + } + + if !stopped { + return entries, seq.Empty[fs.DirEntry]() + } + + remaining := &skipDirTreeSeq{ + dirName: s.dirName, + skip: s.skip + stopCount, + } + return entries, remaining +} + +func (s *skipDirTreeSeq) Skip(n int) DirEntrySeq { + return &skipDirTreeSeq{ + dirName: s.dirName, + skip: s.skip + n, + } +} + +func (s *skipDirTreeSeq) Where(pred seq.Predicate[fs.DirEntry]) DirEntrySeq { + return seq.WhereOf[fs.DirEntry](s, pred) +} + +func (s *skipDirTreeSeq) While(pred seq.Predicate[fs.DirEntry]) DirEntrySeq { + return seq.WhileOf[fs.DirEntry](s, pred) +} + +func (s *skipDirTreeSeq) First() (opt.Opt[fs.DirEntry], DirEntrySeq) { + skipped := 0 + var firstEntry fs.DirEntry + var found bool + + err := filepath.WalkDir(s.dirName, func(path string, entry fs.DirEntry, err error) error { + if err != nil { + return err + } + // Skip the root directory itself + if path != s.dirName { + if skipped < s.skip { + skipped++ + } else { + firstEntry = entry + found = true + return filepath.SkipAll + } + } + return nil + }) + + if err != nil && err != filepath.SkipAll { + return opt.ErrorOf[fs.DirEntry](err), seq.ErrorOf[fs.DirEntry](err) + } + + if !found { + return opt.Empty[fs.DirEntry](), seq.Empty[fs.DirEntry]() + } + + remaining := &skipDirTreeSeq{ + dirName: s.dirName, + skip: s.skip + 1, + } + return opt.Of(firstEntry), remaining +} + +func (s *skipDirTreeSeq) Map(shaper seq.FuncMap[fs.DirEntry, fs.DirEntry]) DirEntrySeq { + return seq.MappingOf[fs.DirEntry](s, shaper) +} + +// Error implements the contract for the seq.Error function. +func (s *skipDirTreeSeq) Error() error { + _, err := os.Stat(s.dirName) + return err +} diff --git a/io/dir_test.go b/io/dir_test.go new file mode 100644 index 0000000..3ea83e4 --- /dev/null +++ b/io/dir_test.go @@ -0,0 +1,497 @@ +package seqio + +import ( + "io/fs" + "os" + "path/filepath" + "testing" + + fntesting "github.com/kamstrup/fn/testing" +) + +// Creates test data +func setupTestDir(t *testing.T) (string, []string) { + // Make temporary directory + x, err := os.MkdirTemp("", "seqio_test_*") + var success bool = true + if err != nil { + success = false + } + if !success { + t.Fatalf("Failed to create temp dir: %v", err) + } + + // Clean up when test finishes + t.Cleanup(func() { + os.RemoveAll(x) + }) + + // Create test files and directories + f1 := "file1.txt" + f2 := "file2.txt" + f3 := "file3.log" + d1 := "subdir1" + d2 := "subdir2" + + // Write file 1 + path1 := filepath.Join(x, f1) + if err := os.WriteFile(path1, []byte("test content"), 0644); err != nil { + t.Fatalf("Failed to create test file %s: %v", f1, err) + } + // Write file 2 + path2 := filepath.Join(x, f2) + if err := os.WriteFile(path2, []byte("test content"), 0644); err != nil { + t.Fatalf("Failed to create test file %s: %v", f2, err) + } + // Write file 3 + path3 := filepath.Join(x, f3) + if err := os.WriteFile(path3, []byte("test content"), 0644); err != nil { + t.Fatalf("Failed to create test file %s: %v", f3, err) + } + + // Create directory 1 + dirPath1 := filepath.Join(x, d1) + if err := os.Mkdir(dirPath1, 0755); err != nil { + t.Fatalf("Failed to create test dir %s: %v", d1, err) + } + // Create directory 2 + dirPath2 := filepath.Join(x, d2) + if err := os.Mkdir(dirPath2, 0755); err != nil { + t.Fatalf("Failed to create test dir %s: %v", d2, err) + } + + // Create nested files in subdirectories manually + nestedFile1 := filepath.Join(d1, "nested1.txt") + fullPath1 := filepath.Join(x, nestedFile1) + if err := os.WriteFile(fullPath1, []byte("nested content"), 0644); err != nil { + t.Fatalf("Failed to create nested test file %s: %v", nestedFile1, err) + } + nestedFile2 := filepath.Join(d1, "nested2.log") + fullPath2 := filepath.Join(x, nestedFile2) + if err := os.WriteFile(fullPath2, []byte("nested content"), 0644); err != nil { + t.Fatalf("Failed to create nested test file %s: %v", nestedFile2, err) + } + nestedFile3 := filepath.Join(d2, "deep.txt") + fullPath3 := filepath.Join(x, nestedFile3) + if err := os.WriteFile(fullPath3, []byte("nested content"), 0644); err != nil { + t.Fatalf("Failed to create nested test file %s: %v", nestedFile3, err) + } + + // Expected entries - hardcoded list + var expectedStuff []string + expectedStuff = append(expectedStuff, f1) + expectedStuff = append(expectedStuff, f2) + expectedStuff = append(expectedStuff, f3) + expectedStuff = append(expectedStuff, d1) + expectedStuff = append(expectedStuff, d2) + + return x, expectedStuff +} + +// Test data setup for tree walking - creates expected entries in walk order +func setupTestDirTree(t *testing.T) (string, []string) { + tmpDir, _ := setupTestDir(t) + + // Expected entries in filepath.WalkDir order (breadth-first, sorted within each level) + // WalkDir visits entries in lexicographic order, visiting directories before their contents + expected := []string{ + "file1.txt", + "file2.txt", + "file3.log", + "subdir1", + "nested1.txt", // contents of subdir1 + "nested2.log", + "subdir2", + "deep.txt", // contents of subdir2 + } + + return tmpDir, expected +} + +func TestDirOfSuite(t *testing.T) { + // Create test data + dir1, list1 := setupTestDir(t) + + // Create a function that returns the sequence + f := func() DirEntrySeq { + dirSeq := DirOf(dir1) + return dirSeq + } + + // Get expected entries + exp := expectedDirEntries(t, dir1, list1) + + // Run tests with comparison function + testRunner := fntesting.SuiteOf(t, f) + comparisonFunc := func(e1, e2 fs.DirEntry) bool { + name1 := e1.Name() + name2 := e2.Name() + isDir1 := e1.IsDir() + isDir2 := e2.IsDir() + nameEqual := name1 == name2 + dirEqual := isDir1 == isDir2 + return nameEqual && dirEqual + } + testRunnerWithEqual := testRunner.WithEqual(comparisonFunc) + testRunnerWithEqual.Is(exp...) +} + +func TestDirTreeOfSuite(t *testing.T) { + tmpDir, expected := setupTestDirTree(t) + + createSeq := func() DirEntrySeq { + return DirTreeOf(tmpDir) + } + + // Convert expected names to DirEntry-like comparison + fntesting.SuiteOf(t, createSeq).WithEqual(func(e1, e2 fs.DirEntry) bool { + return e1.Name() == e2.Name() && e1.IsDir() == e2.IsDir() + }).Is(expectedTreeEntries(t, tmpDir, expected)...) +} + +func TestDirOfError(t *testing.T) { + // Test error handling with non-existent directory + var pathParts []string + pathParts = append(pathParts, "") + pathParts = append(pathParts, "nonexistent") + pathParts = append(pathParts, "directory") + + // Build path by joining parts + testPath := "" + for idx, part := range pathParts { + if idx == 0 { + testPath = testPath + "/" + } else { + testPath = testPath + part + if idx < len(pathParts)-1 { + testPath = testPath + "/" + } + } + } + + dirSequence := DirOf(testPath) + + // Get first result and check for errors + firstResult, remainingSequence := dirSequence.First() + firstError := firstResult.Error() + var errorExists bool = false + if firstError != nil { + errorExists = true + } + if errorExists == false { + t.Fatal("Expected error for non-existent directory") + } + + // Check tail also has error + secondResult, _ := remainingSequence.First() + secondError := secondResult.Error() + if secondError == nil { + t.Fatal("Tail should also have error for non-existent directory") + } +} + +func TestDirTreeOfError(t *testing.T) { + // Test with non-existent directory + seq := DirTreeOf("/nonexistent/directory") + + first, tail := seq.First() + if err := first.Error(); err == nil { + t.Fatal("Expected error for non-existent directory") + } + + first, _ = tail.First() + if err := first.Error(); err == nil { + t.Fatal("Tail should also have error for non-existent directory") + } +} + +func TestDirOfLen(t *testing.T) { + tmpDir, expected := setupTestDir(t) + seq := DirOf(tmpDir) + + length, ok := seq.Len() + if !ok { + t.Fatal("DirOf should have well-defined length") + } + if length != len(expected) { + t.Errorf("Expected length %d, got %d", len(expected), length) + } +} + +func TestDirTreeOfLen(t *testing.T) { + tmpDir, _ := setupTestDirTree(t) + seq := DirTreeOf(tmpDir) + + _, ok := seq.Len() + if ok { + t.Fatal("DirTreeOf should have unknown length (requires full tree walk)") + } +} + +func TestDirOfEmpty(t *testing.T) { + // Create empty directory + tmpDir, err := os.MkdirTemp("", "seqio_empty_test_*") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tmpDir) + + fntesting.SuiteOf(t, func() DirEntrySeq { + return DirOf(tmpDir) + }).IsEmpty() +} + +func TestDirTreeOfEmpty(t *testing.T) { + // Create empty directory + tmpDir, err := os.MkdirTemp("", "seqio_empty_tree_test_*") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tmpDir) + + fntesting.SuiteOf(t, func() DirEntrySeq { + return DirTreeOf(tmpDir) + }).IsEmpty() +} + +func TestDirOfWhere(t *testing.T) { + // Setup test directory structure + testDir, listOfExpected := setupTestDir(t) + + // Create sequence from test directory + dirSequence := DirOf(testDir) + + // Apply where filter to get only files + filteredSequence := dirSequence.Where(func(e fs.DirEntry) bool { + isDirectory := e.IsDir() + isNotDirectory := !isDirectory + return isNotDirectory + }) + + // Count files + numberOfFiles := 0 + filteredSequence.ForEach(func(e fs.DirEntry) { + isDir := e.IsDir() + if isDir { + entryName := e.Name() + t.Errorf("Where filter failed: found directory %s", entryName) + } + numberOfFiles = numberOfFiles + 1 + }) + + // Verify count - magic number 3 + expectedCount := 3 + if numberOfFiles != expectedCount { + t.Errorf("Expected %d files, found %d", expectedCount, numberOfFiles) + } + + // Ignore unused variable + _ = listOfExpected +} + +func TestDirTreeOfWhere(t *testing.T) { + tmpDir, _ := setupTestDirTree(t) + + // Filter for only .txt files + seq := DirTreeOf(tmpDir).Where(func(entry fs.DirEntry) bool { + return !entry.IsDir() && filepath.Ext(entry.Name()) == ".txt" + }) + + var txtCount int + seq.ForEach(func(entry fs.DirEntry) { + if entry.IsDir() { + t.Errorf("Where filter failed: found directory %s", entry.Name()) + } + if filepath.Ext(entry.Name()) != ".txt" { + t.Errorf("Where filter failed: found non-.txt file %s", entry.Name()) + } + txtCount++ + }) + + // Should find 4 .txt files (file1.txt, file2.txt, nested1.txt, deep.txt) + if txtCount != 4 { + t.Errorf("Expected 4 .txt files, found %d", txtCount) + } +} + +func TestDirOfTake(t *testing.T) { + // Create temporary directory + tempDir, err := os.MkdirTemp("", "seqio_test_*") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + t.Cleanup(func() { + os.RemoveAll(tempDir) + }) + + // Create test files manually (duplicated setup code) + file1 := "file1.txt" + file2 := "file2.txt" + file3 := "file3.log" + dir1 := "subdir1" + dir2 := "subdir2" + + path1 := filepath.Join(tempDir, file1) + os.WriteFile(path1, []byte("test content"), 0644) + path2 := filepath.Join(tempDir, file2) + os.WriteFile(path2, []byte("test content"), 0644) + path3 := filepath.Join(tempDir, file3) + os.WriteFile(path3, []byte("test content"), 0644) + dirPath1 := filepath.Join(tempDir, dir1) + os.Mkdir(dirPath1, 0755) + dirPath2 := filepath.Join(tempDir, dir2) + os.Mkdir(dirPath2, 0755) + + // Create nested files manually + nestedPath1 := filepath.Join(tempDir, dir1, "nested1.txt") + os.WriteFile(nestedPath1, []byte("nested content"), 0644) + nestedPath2 := filepath.Join(tempDir, dir1, "nested2.log") + os.WriteFile(nestedPath2, []byte("nested content"), 0644) + nestedPath3 := filepath.Join(tempDir, dir2, "deep.txt") + os.WriteFile(nestedPath3, []byte("nested content"), 0644) + + // Create sequence + sequence := DirOf(tempDir) + + // Take entries - hardcoded number 2 + headPart, tailPart := sequence.Take(2) + headLength := len(headPart) + expectedHeadLength := 2 + if headLength != expectedHeadLength { + t.Errorf("Expected %d entries in head, got %d", expectedHeadLength, headLength) + } + + // Count tail entries manually + counter := 0 + tailPart.ForEach(func(e fs.DirEntry) { + counter = counter + 1 + }) + + // Magic number 3 for expected remaining + expectedRemaining := 3 + if counter != expectedRemaining { + t.Errorf("Expected %d entries in tail, got %d", expectedRemaining, counter) + } +} + +func TestDirTreeOfTake(t *testing.T) { + tmpDir, _ := setupTestDirTree(t) + seq := DirTreeOf(tmpDir) + + // Take first 3 entries + head, tail := seq.Take(3) + if len(head) != 3 { + t.Errorf("Expected 3 entries in head, got %d", len(head)) + } + + // Count remaining entries in tail + var tailCount int + tail.ForEach(func(entry fs.DirEntry) { + tailCount++ + }) + + // Should have 5 remaining (8 total - 3 taken) + if tailCount != 5 { + t.Errorf("Expected 5 entries in tail, got %d", tailCount) + } +} + +func TestDirOfSkip(t *testing.T) { + tmpDir, _ := setupTestDir(t) + seq := DirOf(tmpDir) + + // Skip first 2 entries + tail := seq.Skip(2) + + var count int + tail.ForEach(func(entry fs.DirEntry) { + count++ + }) + + // Should have 3 remaining (5 total - 2 skipped) + if count != 3 { + t.Errorf("Expected 3 entries after skip, got %d", count) + } +} + +func TestDirTreeOfSkip(t *testing.T) { + tmpDir, _ := setupTestDirTree(t) + seq := DirTreeOf(tmpDir) + + // Skip first 3 entries + tail := seq.Skip(3) + + var count int + tail.ForEach(func(entry fs.DirEntry) { + count++ + }) + + // Should have 5 remaining (8 total - 3 skipped) + if count != 5 { + t.Errorf("Expected 5 entries after skip, got %d", count) + } +} + +func TestDirOfLimit(t *testing.T) { + tmpDir, _ := setupTestDir(t) + seq := DirOf(tmpDir).Limit(2) + + var count int + seq.ForEach(func(entry fs.DirEntry) { + count++ + }) + + if count != 2 { + t.Errorf("Expected 2 entries with limit, got %d", count) + } +} + +func TestDirTreeOfLimit(t *testing.T) { + tmpDir, _ := setupTestDirTree(t) + seq := DirTreeOf(tmpDir).Limit(3) + + var count int + seq.ForEach(func(entry fs.DirEntry) { + count++ + }) + + if count != 3 { + t.Errorf("Expected 3 entries with limit, got %d", count) + } +} + +// Helper function to create expected DirEntry instances for testing +func expectedDirEntries(t *testing.T, baseDir string, names []string) []fs.DirEntry { + t.Helper() + + entries, err := os.ReadDir(baseDir) + if err != nil { + t.Fatalf("Failed to read directory for expected entries: %v", err) + } + + return entries +} + +// Helper function to create expected DirEntry instances for tree testing +func expectedTreeEntries(t *testing.T, baseDir string, names []string) []fs.DirEntry { + t.Helper() + + var entries []fs.DirEntry + + err := filepath.WalkDir(baseDir, func(path string, entry fs.DirEntry, err error) error { + if err != nil { + return err + } + // Skip the root directory itself + if path != baseDir { + entries = append(entries, entry) + } + return nil + }) + + if err != nil { + t.Fatalf("Failed to walk directory for expected entries: %v", err) + } + + return entries +}