github.com/go-maxhub/gremlins@v1.0.1-0.20231227222204-b03a6a1e3e09/core/report/report_test.go (about) 1 /* 2 * Copyright 2022 The Gremlins Authors 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package report_test 18 19 import ( 20 "bytes" 21 "encoding/json" 22 "errors" 23 "go/token" 24 "os" 25 "path/filepath" 26 "runtime" 27 "testing" 28 "time" 29 30 "github.com/google/go-cmp/cmp" 31 "github.com/google/go-cmp/cmp/cmpopts" 32 "github.com/hectane/go-acl" 33 "github.com/spf13/viper" 34 35 "github.com/go-maxhub/gremlins/core/log" 36 "github.com/go-maxhub/gremlins/core/mutator" 37 "github.com/go-maxhub/gremlins/core/report" 38 "github.com/go-maxhub/gremlins/core/report/internal" 39 40 "github.com/go-maxhub/gremlins/core/configuration" 41 "github.com/go-maxhub/gremlins/core/execution" 42 ) 43 44 var fakePosition = newPosition("aFolder/aFile.go", 3, 12) 45 46 func TestReport(t *testing.T) { 47 const ( 48 testingLine = "Mutation testing completed in 2 minutes 22 seconds\n" 49 coverageLine = "Mutator coverage: 0.00%\n" 50 ) 51 52 nrTestCases := []struct { 53 name string 54 mutants []mutator.Mutator 55 want string 56 }{ 57 { 58 name: "reports findings in normal run", 59 mutants: []mutator.Mutator{ 60 stubMutant{status: mutator.Lived, mutantType: mutator.ConditionalsNegation, position: fakePosition}, 61 stubMutant{status: mutator.Killed, mutantType: mutator.ConditionalsNegation, position: fakePosition}, 62 stubMutant{status: mutator.NotCovered, mutantType: mutator.ConditionalsNegation, position: fakePosition}, 63 stubMutant{status: mutator.NotViable, mutantType: mutator.ConditionalsBoundary, position: fakePosition}, 64 stubMutant{status: mutator.TimedOut, mutantType: mutator.ConditionalsBoundary, position: fakePosition}, 65 stubMutant{status: mutator.Skipped, mutantType: mutator.ConditionalsBoundary, position: fakePosition}, 66 }, 67 want: "\n" + 68 // Limit the time reporting to the first two units (millis are excluded) 69 testingLine + 70 "Killed: 1, Lived: 1, Not covered: 1\n" + 71 "Timed out: 1, Not viable: 1, Skipped: 1\n" + 72 "Test efficacy: 50.00%\n" + 73 "Mutator coverage: 66.67%\n", 74 }, 75 { 76 name: "reports findings with no coverage", 77 mutants: []mutator.Mutator{ 78 stubMutant{status: mutator.NotCovered, mutantType: mutator.ConditionalsNegation, position: fakePosition}, 79 }, 80 want: "\n" + 81 // Limit the time reporting to the first two units (millis are excluded) 82 testingLine + 83 "Killed: 0, Lived: 0, Not covered: 1\n" + 84 "Timed out: 0, Not viable: 0, Skipped: 0\n" + 85 "Test efficacy: 0.00%\n" + 86 coverageLine, 87 }, 88 { 89 name: "reports findings with timeouts", 90 mutants: []mutator.Mutator{ 91 stubMutant{status: mutator.TimedOut, mutantType: mutator.ConditionalsNegation, position: fakePosition}, 92 stubMutant{status: mutator.TimedOut, mutantType: mutator.ConditionalsBoundary, position: fakePosition}, 93 }, 94 want: "\n" + 95 // Limit the time reporting to the first two units (millis are excluded) 96 testingLine + 97 "Killed: 0, Lived: 0, Not covered: 0\n" + 98 "Timed out: 2, Not viable: 0, Skipped: 0\n" + 99 "Test efficacy: 0.00%\n" + 100 coverageLine, 101 }, 102 { 103 name: "reports nothing if no result", 104 mutants: []mutator.Mutator{}, 105 want: "\n" + 106 "No results to report.\n", 107 }, 108 } 109 for _, tc := range nrTestCases { 110 t.Run(tc.name, func(t *testing.T) { 111 out := &bytes.Buffer{} 112 log.Init(out, &bytes.Buffer{}) 113 defer log.Reset() 114 115 data := report.Results{ 116 Mutants: tc.mutants, 117 Elapsed: (2 * time.Minute) + (22 * time.Second) + (123 * time.Millisecond), 118 } 119 120 _ = report.Do(data) 121 122 got := out.String() 123 124 if !cmp.Equal(got, tc.want) { 125 t.Errorf(cmp.Diff(tc.want, got)) 126 } 127 }) 128 } 129 130 drTestCases := []struct { 131 name string 132 mutants []mutator.Mutator 133 want string 134 }{ 135 { 136 name: "reports findings in dry-run", 137 mutants: []mutator.Mutator{ 138 stubMutant{status: mutator.Runnable, mutantType: mutator.ConditionalsNegation, position: fakePosition}, 139 stubMutant{status: mutator.Runnable, mutantType: mutator.ConditionalsNegation, position: fakePosition}, 140 stubMutant{status: mutator.NotCovered, mutantType: mutator.ConditionalsNegation, position: fakePosition}, 141 }, 142 want: "\n" + 143 // Limit the time reporting to the first two units (millis are excluded) 144 "Dry run completed in 2 minutes 22 seconds\n" + 145 "Runnable: 2, Not covered: 1\n" + 146 "Mutator coverage: 66.67%\n", 147 }, 148 { 149 name: "reports coverage zero in dry-run with timeout", 150 mutants: []mutator.Mutator{ 151 stubMutant{status: mutator.TimedOut, mutantType: mutator.ConditionalsNegation, position: fakePosition}, 152 }, 153 want: "\n" + 154 // Limit the time reporting to the first two units (millis are excluded) 155 "Dry run completed in 2 minutes 22 seconds\n" + 156 "Runnable: 0, Not covered: 0\n" + 157 coverageLine, 158 }, 159 } 160 for _, tc := range drTestCases { 161 t.Run(tc.name, func(t *testing.T) { 162 viper.Set(configuration.UnleashDryRunKey, true) 163 defer viper.Reset() 164 165 out := &bytes.Buffer{} 166 log.Init(out, &bytes.Buffer{}) 167 defer log.Reset() 168 169 data := report.Results{ 170 Mutants: tc.mutants, 171 Elapsed: (2 * time.Minute) + (22 * time.Second) + (123 * time.Millisecond), 172 } 173 174 _ = report.Do(data) 175 176 got := out.String() 177 178 if !cmp.Equal(got, tc.want) { 179 t.Errorf(cmp.Diff(tc.want, got)) 180 } 181 }) 182 } 183 } 184 185 func newPosition(filename string, col, line int) token.Position { 186 return token.Position{ 187 Filename: filename, 188 Offset: 0, 189 Line: line, 190 Column: col, 191 } 192 } 193 194 func TestAssessment(t *testing.T) { 195 testCases := []struct { 196 value any 197 name string 198 confKey string 199 expectError bool 200 }{ 201 // Efficacy-threshold as float64 202 { 203 name: "efficacy < efficacy-threshold", 204 confKey: configuration.UnleashThresholdEfficacyKey, 205 value: float64(51), 206 expectError: true, 207 }, 208 { 209 name: "efficacy >= efficacy-threshold", 210 confKey: configuration.UnleashThresholdEfficacyKey, 211 value: float64(50), 212 expectError: false, 213 }, 214 { 215 name: "efficacy-threshold == 0", 216 confKey: configuration.UnleashThresholdEfficacyKey, 217 value: float64(0), 218 expectError: false, 219 }, 220 // Efficacy-threshold as float64 221 { 222 name: "efficacy < efficacy-threshold", 223 confKey: configuration.UnleashThresholdEfficacyKey, 224 value: 51, 225 expectError: true, 226 }, 227 // Mutator coverage-threshold as float 228 { 229 name: "coverage < coverage-threshold", 230 confKey: configuration.UnleashThresholdMCoverageKey, 231 value: float64(51), 232 expectError: true, 233 }, 234 { 235 name: "coverage >= coverage-threshold", 236 confKey: configuration.UnleashThresholdMCoverageKey, 237 value: float64(50), 238 expectError: false, 239 }, 240 { 241 name: "coverage-threshold == 0", 242 confKey: configuration.UnleashThresholdMCoverageKey, 243 value: float64(0), 244 expectError: false, 245 }, 246 // Mutator coverage-threshold as int 247 { 248 name: "coverage < coverage-threshold", 249 confKey: configuration.UnleashThresholdMCoverageKey, 250 value: 51, 251 expectError: true, 252 }, 253 } 254 255 for _, tc := range testCases { 256 tc := tc 257 t.Run(tc.name, func(t *testing.T) { 258 log.Init(&bytes.Buffer{}, &bytes.Buffer{}) 259 defer log.Reset() 260 261 viper.Set(tc.confKey, tc.value) 262 defer viper.Reset() 263 264 // Always 50% 265 mutants := []mutator.Mutator{ 266 stubMutant{status: mutator.Killed, mutantType: mutator.ConditionalsNegation, position: fakePosition}, 267 stubMutant{status: mutator.Lived, mutantType: mutator.ConditionalsNegation, position: fakePosition}, 268 stubMutant{status: mutator.NotCovered, mutantType: mutator.ConditionalsNegation, position: fakePosition}, 269 stubMutant{status: mutator.NotCovered, mutantType: mutator.ConditionalsNegation, position: fakePosition}, 270 } 271 data := report.Results{ 272 Mutants: mutants, 273 Elapsed: 1 * time.Minute, 274 } 275 276 err := report.Do(data) 277 278 if tc.expectError && err == nil { 279 t.Fatal("expected an error") 280 } 281 if !tc.expectError { 282 return 283 } 284 var exitErr *execution.ExitError 285 if errors.As(err, &exitErr) { 286 if exitErr.ExitCode() == 0 { 287 t.Errorf("expected exit code to be different from 0, got %d", exitErr.ExitCode()) 288 } 289 } else { 290 t.Errorf("expected err to be ExitError") 291 } 292 }) 293 } 294 } 295 296 func TestMutantLog(t *testing.T) { 297 out := &bytes.Buffer{} 298 defer out.Reset() 299 log.Init(out, &bytes.Buffer{}) 300 defer log.Reset() 301 302 m := stubMutant{status: mutator.Lived, mutantType: mutator.ConditionalsBoundary, position: fakePosition} 303 report.Mutant(m) 304 m = stubMutant{status: mutator.Killed, mutantType: mutator.ConditionalsBoundary, position: fakePosition} 305 report.Mutant(m) 306 m = stubMutant{status: mutator.NotCovered, mutantType: mutator.ConditionalsBoundary, position: fakePosition} 307 report.Mutant(m) 308 m = stubMutant{status: mutator.Runnable, mutantType: mutator.ConditionalsBoundary, position: fakePosition} 309 report.Mutant(m) 310 m = stubMutant{status: mutator.NotViable, mutantType: mutator.ConditionalsBoundary, position: fakePosition} 311 report.Mutant(m) 312 m = stubMutant{status: mutator.TimedOut, mutantType: mutator.ConditionalsBoundary, position: fakePosition} 313 report.Mutant(m) 314 315 got := out.String() 316 317 want := "" + 318 " LIVED CONDITIONALS_BOUNDARY at aFolder/aFile.go:12:3\n" + 319 " KILLED CONDITIONALS_BOUNDARY at aFolder/aFile.go:12:3\n" + 320 " NOT COVERED CONDITIONALS_BOUNDARY at aFolder/aFile.go:12:3\n" + 321 " RUNNABLE CONDITIONALS_BOUNDARY at aFolder/aFile.go:12:3\n" + 322 " NOT VIABLE CONDITIONALS_BOUNDARY at aFolder/aFile.go:12:3\n" + 323 " TIMED OUT CONDITIONALS_BOUNDARY at aFolder/aFile.go:12:3\n" 324 325 if !cmp.Equal(got, want) { 326 t.Errorf(cmp.Diff(got, want)) 327 } 328 } 329 330 func TestReportToFile(t *testing.T) { 331 outFile := "findings.json" 332 mutants := []mutator.Mutator{ 333 stubMutant{status: mutator.Killed, mutantType: mutator.ConditionalsNegation, position: newPosition("file1.go", 3, 10)}, 334 stubMutant{status: mutator.Lived, mutantType: mutator.ArithmeticBase, position: newPosition("file1.go", 8, 20)}, 335 stubMutant{status: mutator.NotCovered, mutantType: mutator.IncrementDecrement, position: newPosition("file1.go", 7, 40)}, 336 stubMutant{status: mutator.NotViable, mutantType: mutator.InvertAssignments, position: newPosition("file1.go", 8, 10)}, 337 stubMutant{status: mutator.NotCovered, mutantType: mutator.InvertLoopCtrl, position: newPosition("file2.go", 3, 20)}, 338 stubMutant{status: mutator.Killed, mutantType: mutator.IncrementDecrement, position: newPosition("file2.go", 17, 44)}, 339 stubMutant{status: mutator.NotCovered, mutantType: mutator.ConditionalsBoundary, position: newPosition("file2.go", 3, 500)}, 340 stubMutant{status: mutator.Lived, mutantType: mutator.InvertBitwise, position: newPosition("file2.go", 3, 100)}, 341 stubMutant{status: mutator.Killed, mutantType: mutator.InvertBitwiseAssignments, position: newPosition("file2.go", 4, 10)}, 342 stubMutant{status: mutator.Lived, mutantType: mutator.InvertLogical, position: newPosition("file2.go", 4, 11)}, 343 stubMutant{status: mutator.NotViable, mutantType: mutator.InvertNegatives, position: newPosition("file3.go", 4, 200)}, 344 stubMutant{status: mutator.Killed, mutantType: mutator.RemoveSelfAssignments, position: newPosition("file3.go", 4, 100)}, 345 } 346 data := report.Results{ 347 Module: "example.com/go/module", 348 Mutants: mutants, 349 Elapsed: (2 * time.Minute) + (22 * time.Second) + (123 * time.Millisecond), 350 } 351 f, _ := os.ReadFile("testdata/normal_output.json") 352 want := internal.OutputResult{} 353 _ = json.Unmarshal(f, &want) 354 355 t.Run("it writes on file when output is set", func(t *testing.T) { 356 outDir := t.TempDir() 357 output := filepath.Join(outDir, outFile) 358 viper.Set(configuration.UnleashOutputKey, output) 359 defer viper.Reset() 360 361 if err := report.Do(data); err != nil { 362 t.Fatal("error not expected") 363 } 364 365 file, err := os.ReadFile(output) 366 if err != nil { 367 t.Fatal("file not found") 368 } 369 370 var got internal.OutputResult 371 err = json.Unmarshal(file, &got) 372 if err != nil { 373 t.Fatal("impossible to unmarshal results") 374 } 375 376 if !cmp.Equal(got, want, cmpopts.SortSlices(sortOutputFile), cmpopts.SortSlices(sortMutation)) { 377 t.Errorf(cmp.Diff(got, want)) 378 } 379 }) 380 381 t.Run("it doesn't write on file when output isn't set", func(t *testing.T) { 382 outDir := t.TempDir() 383 output := filepath.Join(outDir, outFile) 384 385 if err := report.Do(data); err != nil { 386 t.Fatal("error not expected") 387 } 388 389 _, err := os.ReadFile(output) 390 if err == nil { 391 t.Errorf("expected file not found") 392 } 393 }) 394 395 // In this case we don't want to stop the execution with an error, but we just want to log the fact. 396 t.Run("it doesn't report error when file is not writeable, but doesn't write file", func(t *testing.T) { 397 outDir, cl := notWriteableDir(t) 398 defer cl() 399 output := filepath.Join(outDir, outFile) 400 viper.Set(configuration.UnleashOutputKey, output) 401 defer viper.Reset() 402 403 if err := report.Do(data); err != nil { 404 t.Fatal("error not expected") 405 } 406 407 _, err := os.ReadFile(output) 408 if err == nil { 409 t.Errorf("expected file not found") 410 } 411 }) 412 } 413 414 func notWriteableDir(t *testing.T) (string, func()) { 415 t.Helper() 416 tmp := t.TempDir() 417 outPath, _ := os.MkdirTemp(tmp, "test-") 418 _ = os.Chmod(outPath, 0000) 419 clean := os.Chmod 420 if runtime.GOOS == "windows" { 421 _ = acl.Chmod(outPath, 0000) 422 clean = acl.Chmod 423 } 424 425 return outPath, func() { 426 _ = clean(outPath, 0700) 427 } 428 } 429 430 func sortOutputFile(x, y internal.OutputFile) bool { 431 return x.Filename < y.Filename 432 } 433 434 func sortMutation(x, y internal.Mutation) bool { 435 if x.Line == y.Line { 436 437 return x.Column < y.Column 438 } 439 440 return x.Line < y.Line 441 } 442 443 type stubMutant struct { 444 position token.Position 445 status mutator.Status 446 mutantType mutator.Type 447 } 448 449 func (s stubMutant) Type() mutator.Type { 450 return s.mutantType 451 } 452 453 func (stubMutant) SetType(_ mutator.Type) { 454 panic("implement me") 455 } 456 457 func (s stubMutant) Status() mutator.Status { 458 return s.status 459 } 460 461 func (stubMutant) SetStatus(_ mutator.Status) { 462 panic("implement me") 463 } 464 465 func (s stubMutant) Position() token.Position { 466 return s.position 467 } 468 469 func (stubMutant) Pos() token.Pos { 470 return 123 471 } 472 473 func (stubMutant) Pkg() string { 474 panic("implement me") 475 } 476 477 func (stubMutant) SetWorkdir(_ string) { 478 panic("implement me") 479 } 480 481 func (stubMutant) Workdir() string { 482 panic("implement me") 483 } 484 485 func (stubMutant) Apply() error { 486 panic("implement me") 487 } 488 489 func (stubMutant) Rollback() error { 490 panic("implement me") 491 }