git » summer » commit 8a455f1

Show progress while processing

author Alberto Bertogli
2023-04-03 01:25:19 UTC
committer Alberto Bertogli
2023-04-03 01:39:20 UTC
parent cb0d27e09312054136fde427015cf6f1ab3529f2

Show progress while processing

summer.go +21 -27
test/access.t +6 -3
test/basic.t +24 -13
test/sqlite.t +9 -5
ui.go +105 -24

diff --git a/summer.go b/summer.go
index 2375457..c31ac59 100644
--- a/summer.go
+++ b/summer.go
@@ -118,7 +118,8 @@ func openAndInfo(path string, d fs.DirEntry) (*os.File, fs.FileInfo, error) {
 }
 
 func generate(db DB, root string) error {
-	var total int64
+	p := NewProgress()
+	defer p.Stop()
 	fn := func(path string, d fs.DirEntry, err error) error {
 		if !isFileRelevant(path, d, err) {
 			return err
@@ -146,17 +147,18 @@ func generate(db DB, root string) error {
 			return err
 		}
 
-		total++
+		p.PrintNew(path)
 		return nil
 	}
 
 	err := filepath.WalkDir(root, fn)
-	PrintWritten(total)
 	return err
 }
 
 func verify(db DB, root string) error {
-	var missing, modified, corrupted, matched int64
+	p := NewProgress()
+	defer p.Stop()
+
 	fn := func(path string, d fs.DirEntry, err error) error {
 		if !isFileRelevant(path, d, err) {
 			return err
@@ -173,8 +175,7 @@ func verify(db DB, root string) error {
 			return err
 		}
 		if !hasAttr {
-			PrintMissing(path)
-			missing++
+			p.PrintMissing(path)
 			return nil
 		}
 
@@ -195,30 +196,28 @@ func verify(db DB, root string) error {
 		}
 
 		if csumFromFile.ModTimeUsec != csumComputed.ModTimeUsec {
-			PrintModified(path)
-			modified++
+			p.PrintModified(path)
 		} else if csumFromFile.CRC32C != csumComputed.CRC32C {
-			PrintCorrupted(path, csumFromFile, csumComputed)
-			corrupted++
+			p.PrintCorrupted(path, csumFromFile, csumComputed)
 		} else {
-			PrintMatched(path)
-			matched++
+			p.PrintMatched(path)
 		}
 
 		return nil
 	}
 
 	err := filepath.WalkDir(root, fn)
-	PrintSummary(matched, modified, missing, corrupted)
 
-	if corrupted > 0 && err == nil {
-		err = fmt.Errorf("detected %d corrupted files", corrupted)
+	if p.corrupted > 0 && err == nil {
+		err = fmt.Errorf("detected %d corrupted files", p.corrupted)
 	}
 	return err
 }
 
 func update(db DB, root string) error {
-	var missing, modified, corrupted, matched int64
+	p := NewProgress()
+	defer p.Stop()
+
 	fn := func(path string, d fs.DirEntry, err error) error {
 		if !isFileRelevant(path, d, err) {
 			return err
@@ -249,8 +248,7 @@ func update(db DB, root string) error {
 		}
 		if !hasAttr {
 			// Attribute is missing. Expected for newly created files.
-			PrintMissing(path)
-			missing++
+			p.PrintMissing(path)
 			return db.Write(fd, csumComputed)
 		}
 
@@ -261,25 +259,21 @@ func update(db DB, root string) error {
 
 		if csumFromFile.ModTimeUsec != csumComputed.ModTimeUsec {
 			// File modified. Expected for updated files.
-			PrintModified(path)
-			modified++
+			p.PrintModified(path)
 			return db.Write(fd, csumComputed)
 		} else if csumFromFile.CRC32C != csumComputed.CRC32C {
-			PrintCorrupted(path, csumFromFile, csumComputed)
-			corrupted++
+			p.PrintCorrupted(path, csumFromFile, csumComputed)
 		} else {
-			PrintMatched(path)
-			matched++
+			p.PrintMatched(path)
 		}
 
 		return nil
 	}
 
 	err := filepath.WalkDir(root, fn)
-	PrintSummary(matched, modified, missing, corrupted)
 
-	if corrupted > 0 && err == nil {
-		err = fmt.Errorf("detected %d corrupted files", corrupted)
+	if p.corrupted > 0 && err == nil {
+		err = fmt.Errorf("detected %d corrupted files", p.corrupted)
 	}
 	return err
 }
diff --git a/test/access.t b/test/access.t
index c7d28ef..b0893b9 100644
--- a/test/access.t
+++ b/test/access.t
@@ -9,12 +9,15 @@ interfere.
   $ echo marola > root/hola
 
   $ summer -db=db.sqlite3 generate root/
-  2 checksums written
+  \r (no-eol) (esc)
+  0s: 0 matched, 0 modified, 2 new, 0 corrupted
 
   $ summer -db=db.sqlite3 verify root/
-  2 matched, 0 modified, 0 new, 0 corrupted
+  \r (no-eol) (esc)
+  0s: 2 matched, 0 modified, 0 new, 0 corrupted
   $ chmod 0000 root/empty
   $ summer -db=db.sqlite3 verify root/
-  0 matched, 0 modified, 0 new, 0 corrupted
+  \r (no-eol) (esc)
+  0s: 0 matched, 0 modified, 0 new, 0 corrupted
   open root/empty: permission denied
   [1]
diff --git a/test/basic.t b/test/basic.t
index 5e3d48c..124a812 100644
--- a/test/basic.t
+++ b/test/basic.t
@@ -10,32 +10,40 @@ Generate test data.
 Generate and verify.
 
   $ summer generate .
-  2 checksums written
+  \r (no-eol) (esc)
+  0s: 0 matched, 0 modified, 2 new, 0 corrupted
+
   $ summer verify .
-  2 matched, 0 modified, 0 new, 0 corrupted
+  \r (no-eol) (esc)
+  0s: 2 matched, 0 modified, 0 new, 0 corrupted
 
 Check handling of new and updated files.
 
   $ echo trova > nueva
   $ touch empty
   $ summer verify .
-  1 matched, 1 modified, 1 new, 0 corrupted
+  \r (no-eol) (esc)
+  0s: 1 matched, 1 modified, 1 new, 0 corrupted
   $ summer update .
-  1 matched, 1 modified, 1 new, 0 corrupted
+  \r (no-eol) (esc)
+  0s: 1 matched, 1 modified, 1 new, 0 corrupted
   $ summer verify .
-  3 matched, 0 modified, 0 new, 0 corrupted
+  \r (no-eol) (esc)
+  0s: 3 matched, 0 modified, 0 new, 0 corrupted
 
 Corrupt a file by changing its contents without changing the mtime.
 
   $ OLD_MTIME=`stat -c "%y" hola`
   $ echo sospechoso >> hola
   $ summer verify .
-  2 matched, 1 modified, 0 new, 0 corrupted
+  \r (no-eol) (esc)
+  0s: 2 matched, 1 modified, 0 new, 0 corrupted
   $ touch --date="$OLD_MTIME" hola
 
   $ summer verify .
   "hola": FILE CORRUPTED - expected:239059f6, got:916db13f
-  2 matched, 0 modified, 0 new, 1 corrupted
+  \r (no-eol) (esc)
+  0s: 2 matched, 0 modified, 0 new, 1 corrupted
   detected 1 corrupted files
   [1]
 
@@ -44,16 +52,19 @@ it.
 
   $ summer update .
   "hola": FILE CORRUPTED - expected:239059f6, got:916db13f
-  2 matched, 0 modified, 0 new, 1 corrupted
+  \r (no-eol) (esc)
+  0s: 2 matched, 0 modified, 0 new, 1 corrupted
   detected 1 corrupted files
   [1]
 
 But "generate" does override it.
 
   $ summer generate .
-  3 checksums written
+  \r (no-eol) (esc)
+  0s: 0 matched, 0 modified, 3 new, 0 corrupted
   $ summer verify .
-  3 matched, 0 modified, 0 new, 0 corrupted
+  \r (no-eol) (esc)
+  0s: 3 matched, 0 modified, 0 new, 0 corrupted
 
 Check verbose and quiet.
 
@@ -61,7 +72,7 @@ Check verbose and quiet.
   "empty": match
   "hola": match
   "nueva": match
-  3 matched, 0 modified, 0 new, 0 corrupted
+  0s: 3 matched, 0 modified, 0 new, 0 corrupted
   $ summer -q verify .
   $ summer -q generate .
   $ summer -q update .
@@ -74,7 +85,7 @@ Check that symlinks are ignored.
   "empty": match
   "hola": match
   "nueva": match
-  3 matched, 0 modified, 0 new, 0 corrupted
+  0s: 3 matched, 0 modified, 0 new, 0 corrupted
 
 Check that the root path doesn't confuse us.
 
@@ -82,4 +93,4 @@ Check that the root path doesn't confuse us.
   "/.*/empty": match (re)
   "/.*/hola": match (re)
   "/.*/nueva": match (re)
-  3 matched, 0 modified, 0 new, 0 corrupted
+  0s: 3 matched, 0 modified, 0 new, 0 corrupted
diff --git a/test/sqlite.t b/test/sqlite.t
index 66fae16..a011de9 100644
--- a/test/sqlite.t
+++ b/test/sqlite.t
@@ -7,11 +7,14 @@ This is enough to exercise the backend.
   $ echo marola > hola
 
   $ summer -db=db.sqlite3 generate .
-  3 checksums written
+  \r (no-eol) (esc)
+  0s: 0 matched, 0 modified, 3 new, 0 corrupted
   $ summer -db=db.sqlite3 verify .
-  2 matched, 1 modified, 0 new, 0 corrupted
+  \r (no-eol) (esc)
+  0s: 2 matched, 1 modified, 0 new, 0 corrupted
   $ summer -db=db.sqlite3 update .
-  2 matched, 1 modified, 0 new, 0 corrupted
+  \r (no-eol) (esc)
+  0s: 2 matched, 1 modified, 0 new, 0 corrupted
 
 Check that the root path doesn't confuse us.
 
@@ -19,12 +22,13 @@ Check that the root path doesn't confuse us.
   ".*/db.sqlite3": file modified \(not corrupted\), updating (re)
   ".*/empty": match (re)
   ".*/hola": match (re)
-  2 matched, 1 modified, 0 new, 0 corrupted
+  0s: 2 matched, 1 modified, 0 new, 0 corrupted
 
 Force a write error to check it is appropriately handled.
 
   $ summer "-db=file:db.sqlite3?mode=ro" generate .
-  . checksums written (re)
+  \r (no-eol) (esc)
+  0s: 0 matched, 0 modified, 0 new, 0 corrupted
   attempt to write a readonly database
   [1]
 
diff --git a/ui.go b/ui.go
index ca0d49c..b3442d0 100644
--- a/ui.go
+++ b/ui.go
@@ -5,6 +5,7 @@ import (
 	"fmt"
 	"os"
 	"runtime/debug"
+	"sync"
 	"time"
 )
 
@@ -30,43 +31,123 @@ func Fatalf(format string, args ...interface{}) {
 	os.Exit(1)
 }
 
-func PrintWritten(written int64) {
-	Printf("%d checksums written", written)
+func PrintVersion() {
+	info, _ := debug.ReadBuildInfo()
+	rev := ""
+	ts := time.Time{}
+	for _, s := range info.Settings {
+		switch s.Key {
+		case "vcs.revision":
+			rev = s.Value
+		case "vcs.time":
+			ts, _ = time.Parse(time.RFC3339, s.Value)
+		}
+	}
+	Printf("summer version %s (%s)", rev, ts)
 }
 
-func PrintSummary(matched, modified, missing, corrupted int64) {
-	Printf("%d matched, %d modified, %d new, %d corrupted",
-		matched, modified, missing, corrupted)
+type Progress struct {
+	start time.Time
+
+	wg sync.WaitGroup
+	mu sync.Mutex
+
+	matched, modified, missing, corrupted int64
+
+	done chan bool
 }
 
-func PrintCorrupted(path string, expected, got ChecksumV1) {
+func NewProgress() *Progress {
+	p := &Progress{
+		start: time.Now(),
+		done:  make(chan bool),
+	}
+	p.wg.Add(1)
+	go p.periodicPrint()
+	return p
+}
+
+func (p *Progress) Stop() {
+	p.done <- true
+	p.wg.Wait()
+}
+
+func (p *Progress) periodicPrint() {
+	defer p.wg.Done()
+	ticker := time.NewTicker(250 * time.Millisecond)
+	defer ticker.Stop()
+
+	if *quiet {
+		<-p.done
+		return
+	}
+
+	for {
+		select {
+		case <-p.done:
+			p.print()
+			if !*verbose {
+				fmt.Printf("\n")
+			}
+			return
+		case <-ticker.C:
+			p.print()
+		}
+	}
+}
+
+func (p *Progress) print() {
+	p.mu.Lock()
+	defer p.mu.Unlock()
+
+	// Usually we just overwrite the previous line.
+	// But when verbose, just print them.
+	prefix := "\r"
+	suffix := ""
+	if *verbose {
+		prefix = ""
+		suffix = "\n"
+	}
+
+	fmt.Printf(
+		prefix+"%v: %d matched, %d modified, %d new, %d corrupted"+suffix,
+		time.Since(p.start).Round(time.Second),
+		p.matched, p.modified, p.missing, p.corrupted,
+	)
+}
+
+func (p *Progress) PrintCorrupted(path string, expected, got ChecksumV1) {
+	p.mu.Lock()
+	defer p.mu.Unlock()
+	p.corrupted++
 	Printf("%q: FILE CORRUPTED - expected:%x, got:%x",
 		path, expected.CRC32C, got.CRC32C)
 }
 
-func PrintMissing(path string) {
+func (p *Progress) PrintNew(path string) {
+	p.mu.Lock()
+	defer p.mu.Unlock()
+	p.missing++
+	Verbosef("%q: adding checksum", path)
+}
+
+func (p *Progress) PrintMissing(path string) {
+	p.mu.Lock()
+	defer p.mu.Unlock()
+	p.missing++
 	Verbosef("%q: missing checksum attribute, adding it", path)
 }
 
-func PrintModified(path string) {
+func (p *Progress) PrintModified(path string) {
+	p.mu.Lock()
+	defer p.mu.Unlock()
+	p.modified++
 	Verbosef("%q: file modified (not corrupted), updating", path)
 }
 
-func PrintMatched(path string) {
+func (p *Progress) PrintMatched(path string) {
+	p.mu.Lock()
+	defer p.mu.Unlock()
+	p.matched++
 	Verbosef("%q: match", path)
 }
-
-func PrintVersion() {
-	info, _ := debug.ReadBuildInfo()
-	rev := ""
-	ts := time.Time{}
-	for _, s := range info.Settings {
-		switch s.Key {
-		case "vcs.revision":
-			rev = s.Value
-		case "vcs.time":
-			ts, _ = time.Parse(time.RFC3339, s.Value)
-		}
-	}
-	Printf("summer version %s (%s)", rev, ts)
-}