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  }