Skip to content

Commit d4d2d50

Browse files
author
Aleksandr Asmakov
committed
Merge branch 'master' into CTHL-5116-adds-global-regex-anotation
2 parents e92ea95 + d5530ff commit d4d2d50

9 files changed

Lines changed: 828 additions & 28 deletions

File tree

.github/workflows/makefile.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ jobs:
1717
- name: Set up Go
1818
uses: actions/setup-go@v2
1919
with:
20-
go-version: 1.23.6
20+
go-version: 1.25.5
2121

2222
- uses: actions/checkout@v2
2323
with:

README.md

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ Mutation testing is also especially interesting for comparing automatically gene
6767
go-mutesting includes a binary which is go-getable.
6868

6969
```bash
70-
go get -t -v github.com/avito-tech/go-mutesting/...
70+
go install -v github.com/avito-tech/go-mutesting/...
7171
```
7272

7373
The binary's help can be invoked by executing the binary without arguments or with the `--help` argument.
@@ -429,12 +429,13 @@ If `--config` is presented, the library will use the given config. Otherwise, no
429429
The config contains the following parameters:
430430
431431
432-
| Name | Default value |Description |
433-
| :------------------- | :------------ |:--------------------------------------------- |
434-
| skip_without_test | true | Skip files without _test.go tests. |
435-
| skip_with_build_tags | true | If in _test.go file we have --build tag - then skip it. |
436-
| json_output | false | Make report.json file with a mutation test report. |
437-
| silent_mode | false | Do not print mutation stats. |
432+
| Name | Default value | Description |
433+
|:---------------------| :------------ |:-------------------------------------------------------------------------------------------------------------------------------------------------------------------|
434+
| skip_without_test | true | Skip files without _test.go tests. |
435+
| skip_with_build_tags | true | If in _test.go file we have --build tag - then skip it. |
436+
| json_output | false | Make report.json file with a mutation test report. |
437+
| html_output | false | Make go-mutesting-report.html file with a mutation test report. |
438+
| silent_mode | false | Do not print mutation stats. |
438439
| exclude_dirs | []string(nil) | Directories for excluding. In fact, there are not directories. These are the prefix for a path when we scan a file system. So this parameter is sensitive for args |
439440
440441
## <a name="write-mutators"></a>How do I write my own mutators?

cmd/go-mutesting/main.go

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package main
33
import (
44
"bytes"
55
"crypto/md5"
6-
"encoding/json"
76
"fmt"
87
"go/ast"
98
"go/format"
@@ -27,6 +26,7 @@ import (
2726
"github.com/avito-tech/go-mutesting/internal/importing"
2827
"github.com/avito-tech/go-mutesting/internal/models"
2928
"github.com/avito-tech/go-mutesting/internal/parser"
29+
"github.com/avito-tech/go-mutesting/internal/reportmaker"
3030
"github.com/jessevdk/go-flags"
3131
"github.com/zimmski/osutil"
3232

@@ -280,34 +280,22 @@ MUTATOR:
280280
fmt.Println("Cannot do a mutation testing summary since no exec command was executed.")
281281
}
282282

283-
jsonContent, err := json.Marshal(report)
283+
err = reportmaker.MakeJSONReport(*report)
284284
if err != nil {
285285
return exitError(err.Error())
286286
}
287287

288-
file, err := os.OpenFile(models.ReportFileName, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
289-
if err != nil {
290-
return exitError(err.Error())
291-
}
292-
293-
if file == nil {
294-
return exitError("Cannot create file for report")
295-
}
288+
console.Verbose(opts, "Save report into %q", models.ReportFileName)
296289

297-
defer func() {
298-
err = file.Close()
290+
if opts.Config.HTMLOutput || opts.General.HTMLOutput {
291+
err = reportmaker.MakeHTMLReport(*report)
299292
if err != nil {
300-
fmt.Printf("Error while report file closing: %v", err.Error())
293+
return exitError(err.Error())
301294
}
302-
}()
303295

304-
_, err = file.WriteString(string(jsonContent))
305-
if err != nil {
306-
return exitError(err.Error())
296+
console.Verbose(opts, "Save report into %q", models.ReportHTMLFileName)
307297
}
308298

309-
console.Verbose(opts, "Save report into %q", models.ReportFileName)
310-
311299
return returnOk
312300
}
313301

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module github.com/avito-tech/go-mutesting
22

3-
go 1.23.6
3+
go 1.25.5
44

55
require (
66
github.com/fatih/color v1.18.0

internal/models/options.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ type Options struct {
88
Help bool `long:"help" description:"Show this help message"`
99
Verbose bool `long:"verbose" description:"Verbose log output"`
1010
Config string `long:"config" description:"Path to config file"`
11+
HTMLOutput bool `long:"html-output" description:"Generates a go-mutesting-report.html file after testing is complete"`
1112
} `group:"General options"`
1213

1314
Files struct {
@@ -43,6 +44,7 @@ type Options struct {
4344
SkipFileWithoutTest bool `yaml:"skip_without_test"`
4445
SkipFileWithBuildTag bool `yaml:"skip_with_build_tags"`
4546
JSONOutput bool `yaml:"json_output"`
47+
HTMLOutput bool `yaml:"html_output"`
4648
SilentMode bool `yaml:"silent_mode"`
4749
ExcludeDirs []string `yaml:"exclude_dirs"`
4850
ExcludeRegexp []string `yaml:"exclude_regexp"`

internal/models/report.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ package models
33
// ReportFileName File name for json report
44
var ReportFileName string = "report.json"
55

6+
// ReportHTMLFileName File name for html report
7+
var ReportHTMLFileName string = "go-mutesting-report.html"
8+
69
// Report Structure for mutation report
710
type Report struct {
811
Stats Stats `json:"stats"`

internal/reportmaker/maker.go

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
package reportmaker
2+
3+
import (
4+
_ "embed" // for embedding report template
5+
"encoding/json"
6+
"errors"
7+
"fmt"
8+
"html/template"
9+
"math"
10+
"os"
11+
"strings"
12+
13+
"github.com/avito-tech/go-mutesting/internal/models"
14+
)
15+
16+
//go:embed templates/report.html.gotpl
17+
var reportTmpl string
18+
19+
var funcMap = template.FuncMap{
20+
"splitDiff": func(diff string) []string {
21+
return strings.Split(diff, "\n")
22+
},
23+
"hasPrefix": strings.HasPrefix,
24+
}
25+
26+
// MakeHTMLReport is a function for creating an HTML report based on a stripped-down version of the models.Report model (not all fields are used)
27+
func MakeHTMLReport(report models.Report) error {
28+
// MSI in percent
29+
report.Stats.Msi = math.Round(report.Stats.Msi*10_000) / 100
30+
groupedMutants := groupEscapedMutants(report.Escaped)
31+
32+
t, err := template.New(models.ReportHTMLFileName).Funcs(funcMap).Parse(reportTmpl)
33+
if err != nil {
34+
return fmt.Errorf("Error while parse template: %w ", err)
35+
}
36+
37+
file, err := createOrTruncateReportFile(models.ReportHTMLFileName)
38+
if err != nil {
39+
return fmt.Errorf("Error while open/create .html report file from template: %w ", err)
40+
}
41+
defer closeReportFile(file, models.ReportHTMLFileName)
42+
43+
data := struct {
44+
Stats models.Stats
45+
GroupedMutants map[string][]models.Mutant
46+
}{
47+
Stats: report.Stats,
48+
GroupedMutants: groupedMutants,
49+
}
50+
51+
err = t.Execute(file, data)
52+
if err != nil {
53+
return fmt.Errorf("Error while execute template for .html report: %w ", err)
54+
}
55+
56+
return nil
57+
}
58+
59+
// MakeJSONReport is a function for creating json report, which is based on models.Report
60+
func MakeJSONReport(report models.Report) error {
61+
jsonContent, err := json.Marshal(report)
62+
if err != nil {
63+
return err
64+
}
65+
66+
file, err := createOrTruncateReportFile(models.ReportFileName)
67+
if err != nil {
68+
return fmt.Errorf("Error while open/create .json report file from template: %w ", err)
69+
}
70+
defer closeReportFile(file, models.ReportFileName)
71+
72+
if file == nil {
73+
return errors.New("cannot create file for .json report")
74+
}
75+
76+
_, err = file.WriteString(string(jsonContent))
77+
if err != nil {
78+
return err
79+
}
80+
81+
return nil
82+
}
83+
84+
func createOrTruncateReportFile(filename string) (*os.File, error) {
85+
return os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
86+
}
87+
88+
func closeReportFile(file *os.File, filename string) {
89+
if err := file.Close(); err != nil {
90+
fmt.Printf("Error while closing %s: %v\n", filename, err)
91+
}
92+
}
93+
94+
func groupEscapedMutants(escaped []models.Mutant) map[string][]models.Mutant {
95+
if len(escaped) == 0 {
96+
return make(map[string][]models.Mutant)
97+
}
98+
99+
mutantCount := make(map[string]int)
100+
for _, mutant := range escaped {
101+
filePath := mutant.Mutator.OriginalFilePath
102+
mutantCount[filePath]++
103+
}
104+
105+
groupedMutants := make(map[string][]models.Mutant, len(mutantCount))
106+
for filePath, count := range mutantCount {
107+
groupedMutants[filePath] = make([]models.Mutant, 0, count)
108+
}
109+
110+
for _, mutant := range escaped {
111+
filePath := mutant.Mutator.OriginalFilePath
112+
groupedMutants[filePath] = append(groupedMutants[filePath], mutant)
113+
}
114+
115+
return groupedMutants
116+
}

0 commit comments

Comments
 (0)