github.com/go-maxhub/gremlins@v1.0.1-0.20231227222204-b03a6a1e3e09/core/report/report.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 18 19 import ( 20 "encoding/json" 21 "os" 22 "time" 23 24 "github.com/fatih/color" 25 "github.com/hako/durafmt" 26 27 "github.com/go-maxhub/gremlins/core/log" 28 "github.com/go-maxhub/gremlins/core/mutator" 29 "github.com/go-maxhub/gremlins/core/report/internal" 30 31 "github.com/go-maxhub/gremlins/core/configuration" 32 "github.com/go-maxhub/gremlins/core/execution" 33 ) 34 35 var ( 36 fgRed = color.New(color.FgRed).SprintFunc() 37 fgGreen = color.New(color.FgGreen).SprintFunc() 38 fgHiGreen = color.New(color.FgHiGreen).SprintFunc() 39 fgHiBlack = color.New(color.FgHiBlack).SprintFunc() 40 fgHiYellow = color.New(color.FgYellow).SprintFunc() 41 ) 42 43 // Results contains the list of mutator.Mutator to be reported 44 // and the time it took to discover and test them. 45 type Results struct { 46 Module string 47 Mutants []mutator.Mutator 48 Elapsed time.Duration 49 } 50 51 type reportStatus struct { 52 files map[string][]internal.Mutation 53 54 elapsed *durafmt.Durafmt 55 module string 56 57 killed int 58 lived int 59 timedOut int 60 notCovered int 61 skipped int 62 notViable int 63 runnable int 64 65 mutatorStatistics internal.MutatorType 66 67 tEfficacy float64 68 mCovered float64 69 } 70 71 func newReport(results Results) (*reportStatus, bool) { 72 if len(results.Mutants) == 0 { 73 74 return nil, false 75 } 76 rep := &reportStatus{ 77 module: results.Module, 78 elapsed: durafmt.Parse(results.Elapsed).LimitFirstN(2), 79 } 80 rep.files = make(map[string][]internal.Mutation) 81 for _, m := range results.Mutants { 82 rep.files[m.Position().Filename] = append(rep.files[m.Position().Filename], internal.Mutation{ 83 Line: m.Position().Line, 84 Column: m.Position().Column, 85 Type: m.Type().String(), 86 Status: m.Status().String(), 87 }) 88 89 reportMutationStatus(m, rep) 90 reportMutatorType(m, rep) 91 } 92 if !rep.isDryRun() { 93 if rep.killed > 0 { 94 rep.tEfficacy = float64(rep.killed) / float64(rep.killed+rep.lived) * 100 95 } 96 if rep.killed+rep.lived > 0 { 97 rep.mCovered = float64(rep.killed+rep.lived) / float64(rep.killed+rep.lived+rep.notCovered) * 100 98 } 99 } else if rep.runnable > 0 { 100 rep.mCovered = float64(rep.runnable) / float64(rep.runnable+rep.notCovered) * 100 101 } 102 103 return rep, true 104 } 105 106 func reportMutationStatus(m mutator.Mutator, rep *reportStatus) { 107 switch m.Status() { 108 case mutator.Killed: 109 rep.killed++ 110 case mutator.Lived: 111 rep.lived++ 112 case mutator.NotCovered: 113 rep.notCovered++ 114 case mutator.Skipped: 115 rep.skipped++ 116 case mutator.TimedOut: 117 rep.timedOut++ 118 case mutator.NotViable: 119 rep.notViable++ 120 case mutator.Runnable: 121 rep.runnable++ 122 } 123 } 124 125 func reportMutatorType(m mutator.Mutator, rep *reportStatus) { 126 switch m.Type() { 127 case mutator.ArithmeticBase: 128 rep.mutatorStatistics.ArithmeticBase++ 129 case mutator.ConditionalsNegation: 130 rep.mutatorStatistics.ConditionalsNegation++ 131 case mutator.ConditionalsBoundary: 132 rep.mutatorStatistics.ConditionalsBoundary++ 133 case mutator.IncrementDecrement: 134 rep.mutatorStatistics.IncrementDecrement++ 135 case mutator.InvertAssignments: 136 rep.mutatorStatistics.InvertAssignments++ 137 case mutator.InvertBitwiseAssignments: 138 rep.mutatorStatistics.InvertBitwiseAssignments++ 139 case mutator.InvertBitwise: 140 rep.mutatorStatistics.InvertBitwise++ 141 case mutator.InvertLogical: 142 rep.mutatorStatistics.InvertLogical++ 143 case mutator.InvertLoopCtrl: 144 rep.mutatorStatistics.InvertLoopCtrl++ 145 case mutator.InvertNegatives: 146 rep.mutatorStatistics.InvertNegatives++ 147 case mutator.RemoveSelfAssignments: 148 rep.mutatorStatistics.RemoveSelfAssignments++ 149 } 150 } 151 152 func (*reportStatus) isDryRun() bool { 153 return configuration.Get[bool](configuration.UnleashDryRunKey) 154 } 155 156 func (r *reportStatus) reportFindings() { 157 if r.isDryRun() { 158 r.dryRunReport() 159 } else { 160 r.fullRunReport() 161 } 162 r.fileReport() 163 } 164 165 func (r *reportStatus) fileReport() { 166 if output := configuration.Get[string](configuration.UnleashOutputKey); output != "" { 167 files := make([]internal.OutputFile, 0, len(r.files)) 168 for fName, mutations := range r.files { 169 of := internal.OutputFile{Filename: fName} 170 of.Mutations = append(of.Mutations, mutations...) 171 files = append(files, of) 172 } 173 174 result := internal.OutputResult{ 175 GoModule: r.module, 176 TestEfficacy: r.tEfficacy, 177 MutationsCoverage: r.mCovered, 178 MutantsTotal: r.lived + r.killed + r.notViable, 179 MutantsKilled: r.killed, 180 MutantsLived: r.lived, 181 MutantsNotViable: r.notViable, 182 MutantsNotCovered: r.notCovered, 183 ElapsedTime: r.elapsed.Duration().Seconds(), 184 MutatorStatistics: r.mutatorStatistics, 185 Files: files, 186 } 187 188 jsonResult, _ := json.Marshal(result) 189 f, err := os.Create(output) 190 if err != nil { 191 log.Errorf("impossible to write file: %s\n", err) 192 } 193 defer func(f *os.File) { 194 _ = f.Close() 195 }(f) 196 if _, err := f.Write(jsonResult); err != nil { 197 log.Errorf("impossible to write file: %s\n", err) 198 } 199 200 } 201 } 202 203 func (r *reportStatus) dryRunReport() { 204 notCovered := fgHiYellow(r.notCovered) 205 runnable := fgGreen(r.runnable) 206 log.Infoln("") 207 log.Infof("Dry run completed in %s\n", r.elapsed.String()) 208 log.Infof("Runnable: %s, Not covered: %s\n", runnable, notCovered) 209 log.Infof("Mutator coverage: %.2f%%\n", r.mCovered) 210 } 211 212 func (r *reportStatus) fullRunReport() { 213 killed := fgHiGreen(r.killed) 214 lived := fgRed(r.lived) 215 timedOut := fgGreen(r.timedOut) 216 notViable := fgHiBlack(r.notViable) 217 skipped := fgHiBlack(r.skipped) 218 notCovered := fgHiYellow(r.notCovered) 219 log.Infoln("") 220 log.Infof("Mutation testing completed in %s\n", r.elapsed.String()) 221 log.Infof("Killed: %s, Lived: %s, Not covered: %s\n", killed, lived, notCovered) 222 log.Infof("Timed out: %s, Not viable: %s, Skipped: %s\n", timedOut, notViable, skipped) 223 log.Infof("Test efficacy: %.2f%%\n", r.tEfficacy) 224 log.Infof("Mutator coverage: %.2f%%\n", r.mCovered) 225 } 226 227 func (r *reportStatus) assess(tEfficacy, rCoverage float64) error { 228 if r.isDryRun() { 229 return nil 230 } 231 232 et := configuration.Get[float64](configuration.UnleashThresholdEfficacyKey) 233 if et == 0 { 234 et = float64(configuration.Get[int](configuration.UnleashThresholdEfficacyKey)) 235 } 236 if et > 0 && tEfficacy <= et { 237 return execution.NewExitErr(execution.EfficacyThreshold) 238 } 239 ct := configuration.Get[float64](configuration.UnleashThresholdMCoverageKey) 240 if ct == 0 { 241 ct = float64(configuration.Get[int](configuration.UnleashThresholdMCoverageKey)) 242 } 243 if ct > 0 && rCoverage <= ct { 244 return execution.NewExitErr(execution.MutantCoverageThreshold) 245 } 246 247 return nil 248 } 249 250 // Do generates the report of the Results received. 251 // This function uses the log package in gremlins to write to the 252 // chosen io.Writer, so it is necessary to call log.Init before 253 // the report generation. 254 func Do(results Results) error { 255 rep, ok := newReport(results) 256 if !ok { 257 log.Infoln("\nNo results to report.") 258 259 return nil 260 } 261 rep.reportFindings() 262 263 return rep.assess(rep.tEfficacy, rep.mCovered) 264 } 265 266 // Mutant logs a mutator.Mutator. 267 // It reports the mutant.Status, the mutator.Type and its position. 268 // This function uses the log package in gremlins to write to the 269 // chosen io.Writer, so it is necessary to call log.Init before 270 // the report generation. 271 func Mutant(m mutator.Mutator) { 272 status := m.Status().String() 273 switch m.Status() { 274 case mutator.Killed, mutator.Runnable: 275 status = fgHiGreen(m.Status()) 276 case mutator.Lived: 277 status = fgRed(m.Status()) 278 case mutator.NotCovered: 279 status = fgHiYellow(m.Status()) 280 case mutator.TimedOut: 281 status = fgGreen(m.Status()) 282 case mutator.NotViable, mutator.Skipped: 283 status = fgHiBlack(m.Status()) 284 } 285 log.Infof("%s%s %s at %s\n", padding(m.Status()), status, m.Type(), m.Position()) 286 } 287 288 func padding(s mutator.Status) string { 289 var pad string 290 padLen := 12 - len(s.String()) 291 for i := 0; i < padLen; i++ { 292 pad += " " 293 } 294 295 return pad 296 }