3 Commits

3 changed files with 236 additions and 17 deletions

View File

@@ -3,6 +3,8 @@ package main
import (
"fmt"
"os"
"sort"
"strings"
"sync"
)
@@ -17,6 +19,7 @@ type FileSystem interface {
Link(source, target string) error
RecordLinkAttempt(kind string, source, target string, err error, dryRun bool)
SummaryLines() []string
SummaryRecords() []operationRecord
IsDryRun() bool
}
@@ -37,26 +40,96 @@ type operationRecord struct {
}
func (r operationRecord) summaryLine() (string, bool) {
if r.kind != opSymlink && r.kind != opHardlink {
return "", false
detail := r.detail()
if detail != "" {
return fmt.Sprintf("%s %s → %s (%s) %s",
r.kindLabel(),
r.formattedSource(),
r.formattedTarget(),
r.resultLabel(),
detail), true
}
kindLabel := "Symlink"
if r.kind == opHardlink {
kindLabel = "Hardlink"
}
return fmt.Sprintf("%s %s → %s (%s)",
r.kindLabel(),
r.formattedSource(),
r.formattedTarget(),
r.resultLabel()), true
}
status := "OK"
func (r operationRecord) kindLabel() string {
switch r.kind {
case opSymlink:
return "Symlink"
case opHardlink:
return "Hardlink"
case opRemove:
return "Remove"
case opRemoveAll:
return "RemoveAll"
case opMkdirAll:
return "MkdirAll"
default:
return r.kind
}
}
func (r operationRecord) resultLabel() string {
if r.err != nil {
status = fmt.Sprintf("FAIL: %v", r.err)
} else if r.dryRun {
status = "DRY-RUN"
return fmt.Sprintf("%sFAIL%s", BRed, Reset)
}
if r.dryRun {
return fmt.Sprintf("%sDRY-RUN%s", BCyan, Reset)
}
return fmt.Sprintf("%sOK%s", BGreen, Reset)
}
source := FormatSourcePath(r.source)
target := FormatTargetPath(r.target)
func (r operationRecord) resultLabelPlain() string {
if r.err != nil {
return "FAIL"
}
if r.dryRun {
return "DRY-RUN"
}
return "OK"
}
return fmt.Sprintf("%s %s → %s (%s)", kindLabel, source, target, status), true
func (r operationRecord) formattedSource() string {
if r.source == "" {
return "-"
}
return FormatSourcePath(r.source)
}
func (r operationRecord) plainSource() string {
if r.source == "" {
return "-"
}
return r.source
}
func (r operationRecord) formattedTarget() string {
if r.target == "" {
return "-"
}
return FormatTargetPath(r.target)
}
func (r operationRecord) plainTarget() string {
if r.target == "" {
return "-"
}
return r.target
}
func (r operationRecord) detail() string {
if r.err != nil {
return r.err.Error()
}
if r.dryRun {
return "dry-run"
}
return ""
}
type baseFileSystem struct {
@@ -141,6 +214,10 @@ func (fs *realFileSystem) SummaryLines() []string {
return summarizeOperations(fs.snapshot())
}
func (fs *realFileSystem) SummaryRecords() []operationRecord {
return fs.snapshot()
}
func (fs *realFileSystem) IsDryRun() bool {
return false
}
@@ -187,6 +264,132 @@ func (fs *dryRunFileSystem) SummaryLines() []string {
return summarizeOperations(fs.snapshot())
}
func (fs *dryRunFileSystem) SummaryRecords() []operationRecord {
return fs.snapshot()
}
func (fs *dryRunFileSystem) IsDryRun() bool {
return true
}
func BuildSummaryLines(records []operationRecord) []string {
if len(records) == 0 {
return nil
}
sorted := make([]operationRecord, len(records))
copy(sorted, records)
opOrder := map[string]int{
opRemove: 0,
opRemoveAll: 1,
opMkdirAll: 2,
opSymlink: 3,
opHardlink: 4,
}
sort.SliceStable(sorted, func(i, j int) bool {
ti := strings.ToLower(sorted[i].plainTarget())
tj := strings.ToLower(sorted[j].plainTarget())
if ti != tj {
return ti < tj
}
si := strings.ToLower(sorted[i].plainSource())
sj := strings.ToLower(sorted[j].plainSource())
if si != sj {
return si < sj
}
oi := opOrderValue(sorted[i].kind, opOrder)
oj := opOrderValue(sorted[j].kind, opOrder)
if oi != oj {
return oi < oj
}
return sorted[i].detail() < sorted[j].detail()
})
header := []string{"RESULT", "OPERATION", "SOURCE", "TARGET", "DETAIL"}
widths := make([]int, len(header))
for i, h := range header {
widths[i] = len(h)
}
plainRows := make([][]string, len(sorted))
coloredRows := make([][]string, len(sorted))
for i, record := range sorted {
detail := record.detail()
if detail == "" {
detail = "-"
}
plain := []string{
record.resultLabelPlain(),
record.kindLabel(),
record.plainSource(),
record.plainTarget(),
detail,
}
colored := []string{
record.resultLabel(),
record.kindLabel(),
record.formattedSource(),
record.formattedTarget(),
detail,
}
plainRows[i] = plain
coloredRows[i] = colored
for j, val := range plain {
if val == "" {
val = "-"
}
if len(val) > widths[j] {
widths[j] = len(val)
}
}
}
lines := make([]string, 0, len(sorted)+1)
lines = append(lines, formatSummaryRow(header, header, widths))
for i := range coloredRows {
lines = append(lines, formatSummaryRow(coloredRows[i], plainRows[i], widths))
}
return lines
}
func formatSummaryRow(colored, plain []string, widths []int) string {
var b strings.Builder
for i := range colored {
p := plain[i]
if p == "" {
p = "-"
}
col := colored[i]
if col == "" {
col = p
}
pad := widths[i] - len(p)
if pad < 0 {
pad = 0
}
b.WriteString(col)
if pad > 0 {
b.WriteString(strings.Repeat(" ", pad))
}
if i < len(colored)-1 {
b.WriteString(" ")
}
}
return b.String()
}
func opOrderValue(kind string, order map[string]int) int {
if v, ok := order[kind]; ok {
return v
}
return len(order)
}

19
filesystem_test.go Normal file
View File

@@ -0,0 +1,19 @@
package main
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
func TestSummaryLineMarksFailuresInRed(t *testing.T) {
fs := NewRealFileSystem()
fs.RecordLinkAttempt(opSymlink, "/tmp/source", "/tmp/target", fmt.Errorf("boom"), false)
lines := fs.SummaryLines()
assert.Equal(t, 1, len(lines))
assert.Contains(t, lines[0], BRed+"FAIL"+Reset)
assert.Contains(t, lines[0], "boom")
}

View File

@@ -185,10 +185,7 @@ func processInstructions(instructions chan *LinkInstruction) int32 {
}
func printSummary(fs FileSystem) {
lines := fs.SummaryLines()
if len(lines) == 0 {
return
}
lines := BuildSummaryLines(fs.SummaryRecords())
LogInfo("Summary:")
for _, line := range lines {
LogInfo("%s", line)