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  }