github.com/tilt-dev/wat@v0.0.2-0.20180626175338-9349b638e250/cli/wat/decide_test.go (about) 1 package wat 2 3 import ( 4 "math" 5 "reflect" 6 "testing" 7 "time" 8 ) 9 10 var fileA = fileInfo{name: "a.txt", modTime: time.Now()} 11 var fileB = fileInfo{name: "b.txt", modTime: time.Now().Add(-time.Minute)} 12 var fileC = fileInfo{name: "c.txt", modTime: time.Now().Add(-time.Minute * 2)} 13 var fileD = fileInfo{name: "d.txt", modTime: time.Now().Add(-time.Minute * 3)} 14 15 var cmdA = WatCommand{Command: "cat a.txt", FilePattern: "a.txt"} 16 var cmdB = WatCommand{Command: "cat b.txt", FilePattern: "b.txt"} 17 var cmdC = WatCommand{Command: "cat c.txt", FilePattern: "c.txt"} 18 var cmdD = WatCommand{Command: "cat d.txt", FilePattern: "d.txt"} 19 20 var cmdLogA = newTestLog(cmdA, time.Minute, true) 21 var cmdLogB = newTestLog(cmdB, 30*time.Second, true) 22 var cmdLogC = newTestLog(cmdC, 20*time.Second, true) 23 var cmdLogD = newTestLog(cmdD, 10*time.Second, true) 24 25 var cmdLogSecSuccessA = newTestLog(cmdA, time.Second, true) 26 var cmdLogSecSuccessB = newTestLog(cmdB, time.Second, true) 27 var cmdLogSecSuccessC = newTestLog(cmdC, time.Second, true) 28 var cmdLogSecSuccessD = newTestLog(cmdD, time.Second, true) 29 var cmdLogSecFailA = newTestLog(cmdA, time.Second, false) 30 var cmdLogSecFailB = newTestLog(cmdB, time.Second, false) 31 var cmdLogSecFailC = newTestLog(cmdC, time.Second, false) 32 var cmdLogSecFailD = newTestLog(cmdD, time.Second, false) 33 var cmdLogMinuteFailA = newTestLog(cmdA, time.Minute, false) 34 35 func TestNoCommands(t *testing.T) { 36 results := decideWith(nil, nil, []fileInfo{fileA, fileB, fileC, fileD}, 10) 37 if len(results) != 0 { 38 t.Fatalf("Expected 0 results. Actual: %d", len(results)) 39 } 40 } 41 42 func TestThreeCommands(t *testing.T) { 43 results := decideWith([]WatCommand{cmdA, cmdB, cmdC}, nil, []fileInfo{fileA, fileB, fileC, fileD}, 3) 44 expected := []WatCommand{cmdA, cmdB, cmdC} 45 if !reflect.DeepEqual(results, expected) { 46 t.Fatalf("Expected %+v. Actual: %+v", expected, results) 47 } 48 } 49 50 func TestThreeCommandsToOne(t *testing.T) { 51 results := decideWith([]WatCommand{cmdD, cmdB, cmdC}, nil, []fileInfo{fileA, fileB, fileC, fileD}, 1) 52 expected := []WatCommand{cmdB} 53 if !reflect.DeepEqual(results, expected) { 54 t.Fatalf("Expected %+v. Actual: %+v", expected, results) 55 } 56 } 57 58 func TestThreeCommandsOutOfOrder(t *testing.T) { 59 results := decideWith([]WatCommand{cmdC, cmdA, cmdB}, nil, []fileInfo{fileA, fileB, fileC, fileD}, 3) 60 expected := []WatCommand{cmdA, cmdB, cmdC} 61 if !reflect.DeepEqual(results, expected) { 62 t.Fatalf("Expected %+v. Actual: %+v", expected, results) 63 } 64 } 65 66 func TestMissingFiles(t *testing.T) { 67 results := decideWith([]WatCommand{cmdA, cmdB, cmdC}, nil, []fileInfo{fileB}, 3) 68 expected := []WatCommand{cmdB, cmdA, cmdC} 69 if !reflect.DeepEqual(results, expected) { 70 t.Fatalf("Expected %+v. Actual: %+v", expected, results) 71 } 72 } 73 74 func TestCost(t *testing.T) { 75 ds := newDecisionStore() 76 if ds.HasCost(cmdA) { 77 t.Fatalf("Expected HasCost: false") 78 } 79 80 ds.addCommandCost(CommandLog{Command: cmdA.Command, Duration: time.Minute}, LogContext{}) 81 if !ds.HasCost(cmdA) { 82 t.Fatalf("Expected HasCost: true") 83 } 84 85 if ds.Cost(cmdA) != time.Minute { 86 t.Errorf("Expected cost %s. Actual %s", time.Minute, ds.Cost(cmdA)) 87 } 88 89 ds.addCommandCost(CommandLog{Command: cmdA.Command, Duration: time.Second}, LogContext{}) 90 expected := time.Duration(0.3*float64(time.Minute.Nanoseconds()) + 0.7*float64(time.Second.Nanoseconds())) 91 if ds.Cost(cmdA) != expected { 92 t.Errorf("Expected cost %s. Actual %s", expected, ds.Cost(cmdA)) 93 } 94 } 95 96 func TestFailureDecide(t *testing.T) { 97 group := CommandLogGroup{ 98 Logs: []CommandLog{ 99 cmdLogSecFailA, 100 cmdLogSecFailA, 101 cmdLogSecSuccessB, 102 cmdLogSecFailB, 103 cmdLogSecFailC, 104 cmdLogSecFailC, 105 cmdLogSecSuccessD, 106 cmdLogSecFailD, 107 }, 108 Context: LogContext{StartTime: time.Now(), Source: LogSourceUser}, 109 } 110 results := decideWith([]WatCommand{cmdA, cmdB, cmdC, cmdD}, []CommandLogGroup{group}, nil, 3) 111 expected := []WatCommand{cmdA, cmdC, cmdB} 112 if !reflect.DeepEqual(results, expected) { 113 t.Fatalf("Expected %+v. Actual: %+v", expected, results) 114 } 115 } 116 117 func TestCostSensitiveGainDecide(t *testing.T) { 118 group := CommandLogGroup{ 119 Logs: []CommandLog{ 120 // Because cmdA takes a minute, even though it fails all the time, 121 // it becomes the worst choice. 122 cmdLogMinuteFailA, 123 cmdLogMinuteFailA, 124 cmdLogSecSuccessB, 125 cmdLogSecFailB, 126 cmdLogSecFailC, 127 cmdLogSecFailC, 128 cmdLogSecSuccessD, 129 cmdLogSecFailD, 130 }, 131 Context: LogContext{StartTime: time.Now(), Source: LogSourceUser}, 132 } 133 results := decideWith([]WatCommand{cmdA, cmdB, cmdC, cmdD}, []CommandLogGroup{group}, nil, 3) 134 expected := []WatCommand{cmdC, cmdB, cmdD} 135 if !reflect.DeepEqual(results, expected) { 136 t.Fatalf("Expected %+v. Actual: %+v", expected, results) 137 } 138 } 139 140 func TestCorrelationSensitiveGainDecide(t *testing.T) { 141 // Create groups where A + B are highly correlated, 142 // so we will not choose B if A is the first task. 143 group1 := CommandLogGroup{ 144 Logs: []CommandLog{ 145 cmdLogSecSuccessA, 146 cmdLogSecSuccessB, 147 cmdLogSecSuccessC, 148 }, 149 Context: LogContext{StartTime: time.Now(), Source: LogSourceUser}, 150 } 151 group2 := CommandLogGroup{ 152 Logs: []CommandLog{ 153 cmdLogSecSuccessA, 154 cmdLogSecSuccessB, 155 cmdLogSecFailC, 156 }, 157 Context: LogContext{StartTime: time.Now(), Source: LogSourceUser}, 158 } 159 group3 := CommandLogGroup{ 160 Logs: []CommandLog{ 161 cmdLogSecFailA, 162 cmdLogSecFailB, 163 }, 164 Context: LogContext{StartTime: time.Now(), Source: LogSourceUser}, 165 } 166 167 results := decideWith([]WatCommand{cmdA, cmdC, cmdB}, 168 []CommandLogGroup{group1, group2, group3, group3, group3}, nil, 3) 169 expected := []WatCommand{cmdA, cmdC, cmdB} 170 if !reflect.DeepEqual(results, expected) { 171 t.Fatalf("Expected %+v. Actual: %+v", expected, results) 172 } 173 } 174 175 func TestFailureProbabilityDifferentPackage(t *testing.T) { 176 ds := newDecisionStore() 177 178 condB := Condition{EditedFile: "b.txt"} 179 prob := ds.FailureProbability(cmdA, condB) 180 expected := 0.5 181 if !roughlyEqual(prob, expected) { 182 t.Fatalf("Expected %v, actual: %v", expected, prob) 183 } 184 185 ctx := LogContext{} 186 ds.addCommandHistory(cmdLogA, ctx, Condition{}) 187 prob = ds.FailureProbability(cmdA, condB) 188 expected = failProbabilityZeroCase / (1 + failProbabilityZeroCase) 189 if !roughlyEqual(prob, expected) { 190 t.Fatalf("Expected %v, actual: %v", expected, prob) 191 } 192 193 ds.addCommandHistory(cmdLogSecFailA, ctx, Condition{}) 194 prob = ds.FailureProbability(cmdA, condB) 195 expected = 0.5 196 if !roughlyEqual(prob, expected) { 197 t.Fatalf("Expected %v, actual: %v", expected, prob) 198 } 199 200 ds.addCommandHistory(cmdLogSecFailA, ctx, Condition{}) 201 prob = ds.FailureProbability(cmdA, condB) 202 expected = 0.666 203 if !roughlyEqual(prob, expected) { 204 t.Fatalf("Expected %v, actual: %v", expected, prob) 205 } 206 207 // Logs that are sensitive to the most recently edited file 208 // make the narrower probability kick in. 209 ds.addCommandHistory(cmdLogA, LogContext{RecentEdits: []string{"b.txt"}}, Condition{}) 210 prob = ds.FailureProbability(cmdA, condB) 211 expected = failProbabilityZeroCase / (1 + failProbabilityZeroCase) 212 if !roughlyEqual(prob, expected) { 213 t.Fatalf("Expected %v, actual: %v", expected, prob) 214 } 215 } 216 217 func TestFailureProbabilitySamePackage(t *testing.T) { 218 ds := newDecisionStore() 219 220 condA := Condition{EditedFile: "a.txt"} 221 prob := ds.FailureProbability(cmdA, condA) 222 expected := 0.5 223 if !roughlyEqual(prob, expected) { 224 t.Fatalf("Expected %v, actual: %v", expected, prob) 225 } 226 227 ctx := LogContext{} 228 ds.addCommandHistory(cmdLogA, ctx, Condition{}) 229 prob = ds.FailureProbability(cmdA, condA) 230 expected = 0.5 231 if !roughlyEqual(prob, expected) { 232 t.Fatalf("Expected %v, actual: %v", expected, prob) 233 } 234 235 ds.addCommandHistory(cmdLogSecFailA, ctx, Condition{}) 236 prob = ds.FailureProbability(cmdA, condA) 237 expected = 0.5 238 if !roughlyEqual(prob, expected) { 239 t.Fatalf("Expected %v, actual: %v", expected, prob) 240 } 241 242 ds.addCommandHistory(cmdLogSecFailA, ctx, Condition{}) 243 prob = ds.FailureProbability(cmdA, condA) 244 expected = 0.666 245 if !roughlyEqual(prob, expected) { 246 t.Fatalf("Expected %v, actual: %v", expected, prob) 247 } 248 } 249 250 func TestCostDecide(t *testing.T) { 251 // Create log data that says D is faster than B, and only has data for B+D 252 ds := newDecisionStore() 253 ds.addCommandCost(cmdLogB, LogContext{}) 254 ds.addCommandCost(cmdLogD, LogContext{}) 255 results := secondTierDecideWith([]WatCommand{cmdA, cmdB, cmdC, cmdD}, ds, nil, 3) 256 expected := []WatCommand{cmdD, cmdB, cmdA} 257 if !reflect.DeepEqual(results, expected) { 258 t.Fatalf("Expected %+v. Actual: %+v", expected, results) 259 } 260 } 261 262 func roughlyEqual(a, b float64) bool { 263 diff := math.Abs(a - b) 264 return diff < 0.001 265 } 266 267 func newTestLog(cmd WatCommand, dur time.Duration, success bool) CommandLog { 268 return CommandLog{ 269 Command: cmd.Command, 270 Success: success, 271 Duration: dur, 272 } 273 }