feat: add list command with global filters
This commit is contained in:
123
tests/contract/list_command_test.go
Normal file
123
tests/contract/list_command_test.go
Normal file
@@ -0,0 +1,123 @@
|
||||
package contract
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/rogeecn/renamer/internal/listing"
|
||||
"github.com/rogeecn/renamer/internal/output"
|
||||
)
|
||||
|
||||
type captureFormatter struct {
|
||||
entries []output.Entry
|
||||
summary output.Summary
|
||||
}
|
||||
|
||||
func (f *captureFormatter) Begin(io.Writer) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *captureFormatter) WriteEntry(_ io.Writer, entry output.Entry) error {
|
||||
f.entries = append(f.entries, entry)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *captureFormatter) WriteSummary(_ io.Writer, summary output.Summary) error {
|
||||
f.summary = summary
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestListServiceFiltersByExtension(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
|
||||
mustWriteFile(t, filepath.Join(tmp, "keep.jpg"))
|
||||
mustWriteFile(t, filepath.Join(tmp, "skip.txt"))
|
||||
|
||||
formatter := &captureFormatter{}
|
||||
|
||||
svc := listing.NewService()
|
||||
req := &listing.ListingRequest{
|
||||
WorkingDir: tmp,
|
||||
Extensions: []string{".jpg"},
|
||||
Format: listing.FormatPlain,
|
||||
}
|
||||
|
||||
summary, err := svc.List(context.Background(), req, formatter, io.Discard)
|
||||
if err != nil {
|
||||
t.Fatalf("List returned error: %v", err)
|
||||
}
|
||||
|
||||
if len(formatter.entries) != 1 {
|
||||
t.Fatalf("expected 1 entry, got %d", len(formatter.entries))
|
||||
}
|
||||
|
||||
entry := formatter.entries[0]
|
||||
if entry.Path != "keep.jpg" {
|
||||
t.Fatalf("expected path keep.jpg, got %q", entry.Path)
|
||||
}
|
||||
if entry.MatchedExtension != ".jpg" {
|
||||
t.Fatalf("expected matched extension .jpg, got %q", entry.MatchedExtension)
|
||||
}
|
||||
|
||||
if summary.Files != 1 || summary.Total() != 1 {
|
||||
t.Fatalf("unexpected summary: %+v", summary)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListServiceFormatParity(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
|
||||
mustWriteFile(t, filepath.Join(tmp, "a.txt"))
|
||||
|
||||
svc := listing.NewService()
|
||||
|
||||
plainReq := &listing.ListingRequest{
|
||||
WorkingDir: tmp,
|
||||
Format: listing.FormatPlain,
|
||||
}
|
||||
|
||||
plainSummary, err := svc.List(context.Background(), plainReq, output.NewPlainFormatter(), io.Discard)
|
||||
if err != nil {
|
||||
t.Fatalf("plain list error: %v", err)
|
||||
}
|
||||
|
||||
tableReq := &listing.ListingRequest{
|
||||
WorkingDir: tmp,
|
||||
Format: listing.FormatTable,
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
tableSummary, err := svc.List(context.Background(), tableReq, output.NewTableFormatter(), &buf)
|
||||
if err != nil {
|
||||
t.Fatalf("table list error: %v", err)
|
||||
}
|
||||
|
||||
if plainSummary.Total() != tableSummary.Total() {
|
||||
t.Fatalf("summary total mismatch: plain %d vs table %d", plainSummary.Total(), tableSummary.Total())
|
||||
}
|
||||
|
||||
header := buf.String()
|
||||
if !strings.Contains(header, "PATH") || !strings.Contains(header, "TYPE") {
|
||||
t.Fatalf("expected table header in output, got: %s", header)
|
||||
}
|
||||
}
|
||||
|
||||
func mustWriteFile(t *testing.T, path string) {
|
||||
t.Helper()
|
||||
if err := ensureParent(path); err != nil {
|
||||
t.Fatalf("ensure parent: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(path, []byte("data"), 0o644); err != nil {
|
||||
t.Fatalf("write file %s: %v", path, err)
|
||||
}
|
||||
}
|
||||
|
||||
func ensureParent(path string) error {
|
||||
dir := filepath.Dir(path)
|
||||
return os.MkdirAll(dir, 0o755)
|
||||
}
|
||||
17
tests/fixtures/README.md
vendored
Normal file
17
tests/fixtures/README.md
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
# Test Fixtures
|
||||
|
||||
This directory stores sample filesystem layouts used by CLI integration and contract
|
||||
tests. Keep fixtures small and descriptive so test output remains easy to reason
|
||||
about.
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
- Use one subdirectory per scenario (e.g., `basic-mixed-types`, `nested-hidden-files`).
|
||||
- Include a `README.md` inside complex scenarios to explain intent when necessary.
|
||||
- Avoid binary assets larger than a few kilobytes; prefer small text placeholders.
|
||||
|
||||
## Maintenance Tips
|
||||
|
||||
- Regenerate fixture trees with helper scripts instead of manual editing whenever possible.
|
||||
- Document any platform-specific quirks (case sensitivity, symlinks) alongside the fixture.
|
||||
- Update this file when adding new conventions or shared assumptions.
|
||||
69
tests/integration/global_flag_parity_test.go
Normal file
69
tests/integration/global_flag_parity_test.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/rogeecn/renamer/internal/listing"
|
||||
)
|
||||
|
||||
func TestScopeFlagsProduceConsistentRequests(t *testing.T) {
|
||||
root := &cobra.Command{Use: "renamer"}
|
||||
listing.RegisterScopeFlags(root.PersistentFlags())
|
||||
|
||||
listCmd := &cobra.Command{Use: "list"}
|
||||
previewCmd := &cobra.Command{Use: "preview"}
|
||||
|
||||
root.AddCommand(listCmd, previewCmd)
|
||||
|
||||
tmp := t.TempDir()
|
||||
|
||||
mustSet := func(name, value string) {
|
||||
if err := root.PersistentFlags().Set(name, value); err != nil {
|
||||
t.Fatalf("set %s: %v", name, err)
|
||||
}
|
||||
}
|
||||
|
||||
mustSet("path", tmp)
|
||||
mustSet("recursive", "true")
|
||||
mustSet("include-dirs", "true")
|
||||
mustSet("hidden", "true")
|
||||
mustSet("extensions", ".jpg|.png")
|
||||
|
||||
reqList, err := listing.ScopeFromCmd(listCmd)
|
||||
if err != nil {
|
||||
t.Fatalf("list request: %v", err)
|
||||
}
|
||||
|
||||
reqPreview, err := listing.ScopeFromCmd(previewCmd)
|
||||
if err != nil {
|
||||
t.Fatalf("preview request: %v", err)
|
||||
}
|
||||
|
||||
if reqList.WorkingDir != reqPreview.WorkingDir {
|
||||
t.Fatalf("working dir mismatch: %s vs %s", reqList.WorkingDir, reqPreview.WorkingDir)
|
||||
}
|
||||
if reqList.Recursive != reqPreview.Recursive {
|
||||
t.Fatalf("recursive mismatch")
|
||||
}
|
||||
if reqList.IncludeDirectories != reqPreview.IncludeDirectories {
|
||||
t.Fatalf("include-dirs mismatch")
|
||||
}
|
||||
if reqList.IncludeHidden != reqPreview.IncludeHidden {
|
||||
t.Fatalf("hidden mismatch")
|
||||
}
|
||||
if len(reqList.Extensions) != len(reqPreview.Extensions) {
|
||||
t.Fatalf("extension length mismatch: %d vs %d", len(reqList.Extensions), len(reqPreview.Extensions))
|
||||
}
|
||||
for i := range reqList.Extensions {
|
||||
if reqList.Extensions[i] != reqPreview.Extensions[i] {
|
||||
t.Fatalf("extension mismatch at %d: %s vs %s", i, reqList.Extensions[i], reqPreview.Extensions[i])
|
||||
}
|
||||
}
|
||||
|
||||
if filepath.Clean(reqList.WorkingDir) != reqList.WorkingDir {
|
||||
t.Fatalf("expected cleaned working dir, got %s", reqList.WorkingDir)
|
||||
}
|
||||
}
|
||||
103
tests/integration/list_recursive_test.go
Normal file
103
tests/integration/list_recursive_test.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/rogeecn/renamer/internal/listing"
|
||||
"github.com/rogeecn/renamer/internal/output"
|
||||
)
|
||||
|
||||
type captureFormatter struct {
|
||||
paths []string
|
||||
}
|
||||
|
||||
func (f *captureFormatter) Begin(io.Writer) error { return nil }
|
||||
|
||||
func (f *captureFormatter) WriteEntry(_ io.Writer, entry output.Entry) error {
|
||||
f.paths = append(f.paths, entry.Path)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *captureFormatter) WriteSummary(io.Writer, output.Summary) error { return nil }
|
||||
|
||||
func TestListServiceRecursiveTraversal(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
|
||||
mustWriteFile(t, filepath.Join(tmp, "root.txt"))
|
||||
mustWriteFile(t, filepath.Join(tmp, "nested", "child.txt"))
|
||||
mustWriteDir(t, filepath.Join(tmp, "nested", "inner"))
|
||||
|
||||
svc := listing.NewService()
|
||||
req := &listing.ListingRequest{
|
||||
WorkingDir: tmp,
|
||||
Recursive: true,
|
||||
Format: listing.FormatPlain,
|
||||
}
|
||||
|
||||
formatter := &captureFormatter{}
|
||||
summary, err := svc.List(context.Background(), req, formatter, io.Discard)
|
||||
if err != nil {
|
||||
t.Fatalf("List returned error: %v", err)
|
||||
}
|
||||
|
||||
sort.Strings(formatter.paths)
|
||||
expected := []string{"nested/child.txt", "root.txt"}
|
||||
if len(formatter.paths) != len(expected) {
|
||||
t.Fatalf("expected %d paths, got %d (%v)", len(expected), len(formatter.paths), formatter.paths)
|
||||
}
|
||||
for i, path := range expected {
|
||||
if formatter.paths[i] != path {
|
||||
t.Fatalf("expected path %q at index %d, got %q", path, i, formatter.paths[i])
|
||||
}
|
||||
}
|
||||
|
||||
if summary.Total() != len(expected) {
|
||||
t.Fatalf("unexpected summary total: %d", summary.Total())
|
||||
}
|
||||
}
|
||||
|
||||
func TestListServiceDirectoryOnlyMode(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
|
||||
mustWriteFile(t, filepath.Join(tmp, "file.txt"))
|
||||
mustWriteDir(t, filepath.Join(tmp, "folder"))
|
||||
|
||||
svc := listing.NewService()
|
||||
req := &listing.ListingRequest{
|
||||
WorkingDir: tmp,
|
||||
IncludeDirectories: true,
|
||||
Format: listing.FormatPlain,
|
||||
}
|
||||
|
||||
formatter := &captureFormatter{}
|
||||
_, err := svc.List(context.Background(), req, formatter, io.Discard)
|
||||
if err != nil {
|
||||
t.Fatalf("List returned error: %v", err)
|
||||
}
|
||||
|
||||
if len(formatter.paths) != 1 || formatter.paths[0] != "folder" {
|
||||
t.Fatalf("expected only directory entry, got %v", formatter.paths)
|
||||
}
|
||||
}
|
||||
|
||||
func mustWriteFile(t *testing.T, path string) {
|
||||
t.Helper()
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
t.Fatalf("mkdir %s: %v", path, err)
|
||||
}
|
||||
if err := os.WriteFile(path, []byte("data"), 0o644); err != nil {
|
||||
t.Fatalf("write file %s: %v", path, err)
|
||||
}
|
||||
}
|
||||
|
||||
func mustWriteDir(t *testing.T, path string) {
|
||||
t.Helper()
|
||||
if err := os.MkdirAll(path, 0o755); err != nil {
|
||||
t.Fatalf("mkdir dir %s: %v", path, err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user