Add testable CLI with dependency injection and new scanner/checker packages
Major changes: - Refactor CLI to accept injected I/O streams and filesystem (afero.Fs) for testing without touching the real filesystem - Add RunOptions struct and RunWithOptions() for configurable CLI execution - Add internal/scanner package with two-phase manifest generation: - Phase 1 (Enumeration): walk directories, collect metadata - Phase 2 (Scan): read contents, compute hashes, write manifest - Add internal/checker package for manifest verification with progress reporting and channel-based result streaming - Add mfer/builder.go for incremental manifest construction - Add --no-extra-files flag to check command to detect files not in manifest - Add timing summaries showing file count, size, elapsed time, and throughput - Add comprehensive tests using afero.MemMapFs (no real filesystem access) - Add contrib/usage.sh integration test script - Fix banner ASCII art alignment (consistent spacing) - Fix verbosity levels so summaries display at default log level - Update internal/log to support configurable output writers
This commit is contained in:
@@ -1,13 +1,110 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/apex/log"
|
||||
"github.com/urfave/cli/v2"
|
||||
"sneak.berlin/go/mfer/internal/checker"
|
||||
"sneak.berlin/go/mfer/internal/log"
|
||||
)
|
||||
|
||||
func (mfa *CLIApp) checkManifestOperation(c *cli.Context) error {
|
||||
log.WithError(errors.New("unimplemented"))
|
||||
func (mfa *CLIApp) checkManifestOperation(ctx *cli.Context) error {
|
||||
log.Debug("checkManifestOperation()")
|
||||
|
||||
// Get manifest path from args, default to index.mf
|
||||
manifestPath := "index.mf"
|
||||
if ctx.Args().Len() > 0 {
|
||||
manifestPath = ctx.Args().Get(0)
|
||||
}
|
||||
|
||||
basePath := ctx.String("base")
|
||||
showProgress := ctx.Bool("progress")
|
||||
|
||||
log.Debugf("checking manifest %s with base %s", manifestPath, basePath)
|
||||
|
||||
// Create checker
|
||||
chk, err := checker.NewChecker(manifestPath, basePath, mfa.Fs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load manifest: %w", err)
|
||||
}
|
||||
|
||||
log.Debugf("manifest contains %d files, %d bytes", chk.FileCount(), chk.TotalBytes())
|
||||
|
||||
// Set up results channel
|
||||
results := make(chan checker.Result, 1)
|
||||
|
||||
// Set up progress channel
|
||||
var progress chan checker.CheckStatus
|
||||
if showProgress {
|
||||
progress = make(chan checker.CheckStatus, 1)
|
||||
go func() {
|
||||
for status := range progress {
|
||||
log.Progressf("Checking: %d/%d files, %d failures",
|
||||
status.CheckedFiles,
|
||||
status.TotalFiles,
|
||||
status.Failures)
|
||||
}
|
||||
log.ProgressDone()
|
||||
}()
|
||||
}
|
||||
|
||||
// Process results in a goroutine
|
||||
var failures int64
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
for result := range results {
|
||||
if result.Status != checker.StatusOK {
|
||||
failures++
|
||||
log.Infof("%s: %s (%s)", result.Status, result.Path, result.Message)
|
||||
} else {
|
||||
log.Debugf("%s: %s", result.Status, result.Path)
|
||||
}
|
||||
}
|
||||
close(done)
|
||||
}()
|
||||
|
||||
// Run check
|
||||
err = chk.Check(ctx.Context, results, progress)
|
||||
if err != nil {
|
||||
return fmt.Errorf("check failed: %w", err)
|
||||
}
|
||||
|
||||
// Wait for results processing to complete
|
||||
<-done
|
||||
|
||||
// Check for extra files if requested
|
||||
if ctx.Bool("no-extra-files") {
|
||||
extraResults := make(chan checker.Result, 1)
|
||||
extraDone := make(chan struct{})
|
||||
go func() {
|
||||
for result := range extraResults {
|
||||
failures++
|
||||
log.Infof("%s: %s (%s)", result.Status, result.Path, result.Message)
|
||||
}
|
||||
close(extraDone)
|
||||
}()
|
||||
|
||||
err = chk.FindExtraFiles(ctx.Context, extraResults)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check for extra files: %w", err)
|
||||
}
|
||||
<-extraDone
|
||||
}
|
||||
|
||||
if !ctx.Bool("quiet") {
|
||||
elapsed := time.Since(mfa.startupTime).Seconds()
|
||||
rate := float64(chk.TotalBytes()) / elapsed / 1e6
|
||||
if failures == 0 {
|
||||
log.Infof("checked %d files (%.1f MB) in %.1fs (%.1f MB/s): all OK", chk.FileCount(), float64(chk.TotalBytes())/1e6, elapsed, rate)
|
||||
} else {
|
||||
log.Infof("checked %d files (%.1f MB) in %.1fs (%.1f MB/s): %d failed", chk.FileCount(), float64(chk.TotalBytes())/1e6, elapsed, rate, failures)
|
||||
}
|
||||
}
|
||||
|
||||
if failures > 0 {
|
||||
mfa.exitCode = 1
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user