From ceb7dfd30a4de61259d72fa8777b578748016ebb Mon Sep 17 00:00:00 2001
From: Alex Goodman <wagoodman@gmail.com>
Date: Sat, 27 Jul 2019 14:15:32 -0400
Subject: [PATCH] added file report to CI results; refactored runtime package

---
 runtime/{ci/evaluator.go => ci_evaluator.go}  | 54 ++++++++++++++-----
 ...evaluator_test.go => ci_evaluator_test.go} |  6 +--
 runtime/{ci/rules.go => ci_rule.go}           |  8 +--
 runtime/export.go                             | 29 ++++++++--
 runtime/export_test.go                        |  2 +-
 runtime/options.go                            | 13 +++++
 runtime/reference_file.go                     |  7 +++
 runtime/run.go                                |  5 +-
 runtime/types.go                              | 38 -------------
 9 files changed, 95 insertions(+), 67 deletions(-)
 rename runtime/{ci/evaluator.go => ci_evaluator.go} (68%)
 rename runtime/{ci/evaluator_test.go => ci_evaluator_test.go} (94%)
 rename runtime/{ci/rules.go => ci_rule.go} (97%)
 create mode 100644 runtime/options.go
 create mode 100644 runtime/reference_file.go
 delete mode 100644 runtime/types.go

diff --git a/runtime/ci/evaluator.go b/runtime/ci_evaluator.go
similarity index 68%
rename from runtime/ci/evaluator.go
rename to runtime/ci_evaluator.go
index 9e1a7f9..d1b159d 100644
--- a/runtime/ci/evaluator.go
+++ b/runtime/ci_evaluator.go
@@ -1,8 +1,10 @@
-package ci
+package runtime
 
 import (
 	"fmt"
+	"github.com/dustin/go-humanize"
 	"sort"
+	"strconv"
 	"strings"
 
 	"github.com/spf13/viper"
@@ -11,12 +13,13 @@ import (
 	"github.com/wagoodman/dive/image"
 )
 
-type Evaluator struct {
-	Rules         []Rule
-	Results       map[string]RuleResult
-	Tally         ResultTally
-	Pass          bool
-	Misconfigured bool
+type CiEvaluator struct {
+	Rules            []CiRule
+	Results          map[string]RuleResult
+	Tally            ResultTally
+	Pass             bool
+	Misconfigured    bool
+	InefficientFiles []ReferenceFile
 }
 
 type ResultTally struct {
@@ -27,19 +30,19 @@ type ResultTally struct {
 	Total int
 }
 
-func NewEvaluator(config *viper.Viper) *Evaluator {
-	return &Evaluator{
+func NewCiEvaluator(config *viper.Viper) *CiEvaluator {
+	return &CiEvaluator{
 		Rules:   loadCiRules(config),
 		Results: make(map[string]RuleResult),
 		Pass:    true,
 	}
 }
 
-func (ci *Evaluator) isRuleEnabled(rule Rule) bool {
+func (ci *CiEvaluator) isRuleEnabled(rule CiRule) bool {
 	return rule.Configuration() != "disabled"
 }
 
-func (ci *Evaluator) Evaluate(analysis *image.AnalysisResult) bool {
+func (ci *CiEvaluator) Evaluate(analysis *image.AnalysisResult) bool {
 	canEvaluate := true
 	for _, rule := range ci.Rules {
 		if !ci.isRuleEnabled(rule) {
@@ -72,6 +75,18 @@ func (ci *Evaluator) Evaluate(analysis *image.AnalysisResult) bool {
 		return ci.Pass
 	}
 
+	// capture inefficient files
+	for idx := 0; idx < len(analysis.Inefficiencies); idx++ {
+		fileData := analysis.Inefficiencies[len(analysis.Inefficiencies)-1-idx]
+
+		ci.InefficientFiles = append(ci.InefficientFiles, ReferenceFile{
+			References: len(fileData.Nodes),
+			SizeBytes:  uint64(fileData.CumulativeSize),
+			Path:       fileData.Path,
+		})
+	}
+
+	// evaluate results against the configured CI rules
 	for _, rule := range ci.Rules {
 		if !ci.isRuleEnabled(rule) {
 			ci.Results[rule.Key()] = RuleResult{
@@ -117,7 +132,22 @@ func (ci *Evaluator) Evaluate(analysis *image.AnalysisResult) bool {
 	return ci.Pass
 }
 
-func (ci *Evaluator) Report() {
+func (ci *CiEvaluator) Report() {
+	fmt.Println(title("Inefficient Files:"))
+
+	template := "%5s  %12s  %-s\n"
+	fmt.Printf(template, "Count", "Wasted Space", "File Path")
+
+	if len(ci.InefficientFiles) == 0 {
+		fmt.Println("None")
+	} else {
+		for _, file := range ci.InefficientFiles {
+			fmt.Printf(template, strconv.Itoa(file.References), humanize.Bytes(uint64(file.SizeBytes)), file.Path)
+		}
+	}
+
+	fmt.Println(title("Results:"))
+
 	status := "PASS"
 
 	rules := make([]string, 0, len(ci.Results))
diff --git a/runtime/ci/evaluator_test.go b/runtime/ci_evaluator_test.go
similarity index 94%
rename from runtime/ci/evaluator_test.go
rename to runtime/ci_evaluator_test.go
index f5ffa4e..c7124ad 100644
--- a/runtime/ci/evaluator_test.go
+++ b/runtime/ci_evaluator_test.go
@@ -1,4 +1,4 @@
-package ci
+package runtime
 
 import (
 	"strings"
@@ -10,7 +10,7 @@ import (
 
 func Test_Evaluator(t *testing.T) {
 
-	result, err := image.TestLoadDockerImageTar("../../.data/test-docker-image.tar")
+	result, err := image.TestLoadDockerImageTar("../.data/test-docker-image.tar")
 	if err != nil {
 		t.Fatalf("Test_Export: unable to fetch analysis: %v", err)
 	}
@@ -35,7 +35,7 @@ func Test_Evaluator(t *testing.T) {
 		ciConfig.SetDefault("rules.highestWastedBytes", test.wastedBytes)
 		ciConfig.SetDefault("rules.highestUserWastedPercent", test.wastedPercent)
 
-		evaluator := NewEvaluator(ciConfig)
+		evaluator := NewCiEvaluator(ciConfig)
 
 		pass := evaluator.Evaluate(result)
 
diff --git a/runtime/ci/rules.go b/runtime/ci_rule.go
similarity index 97%
rename from runtime/ci/rules.go
rename to runtime/ci_rule.go
index a2e1c37..d14295a 100644
--- a/runtime/ci/rules.go
+++ b/runtime/ci_rule.go
@@ -1,4 +1,4 @@
-package ci
+package runtime
 
 import (
 	"fmt"
@@ -21,7 +21,7 @@ const (
 	RuleConfigured
 )
 
-type Rule interface {
+type CiRule interface {
 	Key() string
 	Configuration() string
 	Validate() error
@@ -86,8 +86,8 @@ func (status RuleStatus) String() string {
 	}
 }
 
-func loadCiRules(config *viper.Viper) []Rule {
-	var rules = make([]Rule, 0)
+func loadCiRules(config *viper.Viper) []CiRule {
+	var rules = make([]CiRule, 0)
 	var ruleKey = "lowestEfficiency"
 	rules = append(rules, newGenericCiRule(
 		ruleKey,
diff --git a/runtime/export.go b/runtime/export.go
index d2cdb55..e769015 100644
--- a/runtime/export.go
+++ b/runtime/export.go
@@ -7,10 +7,29 @@ import (
 	"github.com/wagoodman/dive/image"
 )
 
+type export struct {
+	Layer []exportLayer `json:"layer"`
+	Image exportImage   `json:"image"`
+}
+
+type exportLayer struct {
+	Index     int    `json:"index"`
+	DigestID  string `json:"digestId"`
+	SizeBytes uint64 `json:"sizeBytes"`
+	Command   string `json:"command"`
+}
+
+type exportImage struct {
+	SizeBytes        uint64          `json:"sizeBytes"`
+	InefficientBytes uint64          `json:"inefficientBytes"`
+	EfficiencyScore  float64         `json:"efficiencyScore"`
+	InefficientFiles []ReferenceFile `json:"ReferenceFile"`
+}
+
 func newExport(analysis *image.AnalysisResult) *export {
 	data := export{}
 	data.Layer = make([]exportLayer, len(analysis.Layers))
-	data.Image.InefficientFiles = make([]inefficientFiles, len(analysis.Inefficiencies))
+	data.Image.InefficientFiles = make([]ReferenceFile, len(analysis.Inefficiencies))
 
 	// export layers in order
 	for revIdx := len(analysis.Layers) - 1; revIdx >= 0; revIdx-- {
@@ -32,10 +51,10 @@ func newExport(analysis *image.AnalysisResult) *export {
 	for idx := 0; idx < len(analysis.Inefficiencies); idx++ {
 		fileData := analysis.Inefficiencies[len(analysis.Inefficiencies)-1-idx]
 
-		data.Image.InefficientFiles[idx] = inefficientFiles{
-			Count:     len(fileData.Nodes),
-			SizeBytes: uint64(fileData.CumulativeSize),
-			File:      fileData.Path,
+		data.Image.InefficientFiles[idx] = ReferenceFile{
+			References: len(fileData.Nodes),
+			SizeBytes:  uint64(fileData.CumulativeSize),
+			Path:       fileData.Path,
 		}
 	}
 
diff --git a/runtime/export_test.go b/runtime/export_test.go
index 2859ae4..709225c 100644
--- a/runtime/export_test.go
+++ b/runtime/export_test.go
@@ -109,7 +109,7 @@ func Test_Export(t *testing.T) {
     "sizeBytes": 1220598,
     "inefficientBytes": 32025,
     "efficiencyScore": 0.9844212134184309,
-    "inefficientFiles": [
+    "ReferenceFile": [
       {
         "count": 2,
         "sizeBytes": 12810,
diff --git a/runtime/options.go b/runtime/options.go
new file mode 100644
index 0000000..37620c9
--- /dev/null
+++ b/runtime/options.go
@@ -0,0 +1,13 @@
+package runtime
+
+import (
+	"github.com/spf13/viper"
+)
+
+type Options struct {
+	Ci         bool
+	ImageId    string
+	ExportFile string
+	CiConfig   *viper.Viper
+	BuildArgs  []string
+}
diff --git a/runtime/reference_file.go b/runtime/reference_file.go
new file mode 100644
index 0000000..bef54eb
--- /dev/null
+++ b/runtime/reference_file.go
@@ -0,0 +1,7 @@
+package runtime
+
+type ReferenceFile struct {
+	References int    `json:"count"`
+	SizeBytes  uint64 `json:"sizeBytes"`
+	Path       string `json:"file"`
+}
diff --git a/runtime/run.go b/runtime/run.go
index e6bd46e..a427bfa 100644
--- a/runtime/run.go
+++ b/runtime/run.go
@@ -10,7 +10,6 @@ import (
 	"github.com/logrusorgru/aurora"
 	"github.com/wagoodman/dive/filetree"
 	"github.com/wagoodman/dive/image"
-	"github.com/wagoodman/dive/runtime/ci"
 	"github.com/wagoodman/dive/ui"
 	"github.com/wagoodman/dive/utils"
 )
@@ -24,9 +23,7 @@ func runCi(analysis *image.AnalysisResult, options Options) {
 	fmt.Printf("  wastedBytes: %d bytes (%s)\n", analysis.WastedBytes, humanize.Bytes(analysis.WastedBytes))
 	fmt.Printf("  userWastedPercent: %2.4f %%\n", analysis.WastedUserPercent*100)
 
-	evaluator := ci.NewEvaluator(options.CiConfig)
-
-	fmt.Println(title("Run CI Validations..."))
+	evaluator := NewCiEvaluator(options.CiConfig)
 
 	pass := evaluator.Evaluate(analysis)
 	evaluator.Report()
diff --git a/runtime/types.go b/runtime/types.go
deleted file mode 100644
index 02f64ca..0000000
--- a/runtime/types.go
+++ /dev/null
@@ -1,38 +0,0 @@
-package runtime
-
-import (
-	"github.com/spf13/viper"
-)
-
-type Options struct {
-	Ci         bool
-	ImageId    string
-	ExportFile string
-	CiConfig   *viper.Viper
-	BuildArgs  []string
-}
-
-type export struct {
-	Layer []exportLayer `json:"layer"`
-	Image exportImage   `json:"image"`
-}
-
-type exportLayer struct {
-	Index     int    `json:"index"`
-	DigestID  string `json:"digestId"`
-	SizeBytes uint64 `json:"sizeBytes"`
-	Command   string `json:"command"`
-}
-
-type exportImage struct {
-	SizeBytes        uint64             `json:"sizeBytes"`
-	InefficientBytes uint64             `json:"inefficientBytes"`
-	EfficiencyScore  float64            `json:"efficiencyScore"`
-	InefficientFiles []inefficientFiles `json:"inefficientFiles"`
-}
-
-type inefficientFiles struct {
-	Count     int    `json:"count"`
-	SizeBytes uint64 `json:"sizeBytes"`
-	File      string `json:"file"`
-}