Skip to content

Commit 919f141

Browse files
committed
Add support for "json" output format
1 parent 255e0e2 commit 919f141

File tree

6 files changed

+282
-28
lines changed

6 files changed

+282
-28
lines changed

check/check.go

Lines changed: 17 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,15 @@
22
package check
33

44
import (
5-
"bytes"
65
"fmt"
76
"os"
8-
"text/template"
97

108
"github.com/arduino/arduino-check/check/checkconfigurations"
119
"github.com/arduino/arduino-check/check/checkdata"
12-
"github.com/arduino/arduino-check/check/checklevel"
13-
"github.com/arduino/arduino-check/check/checkresult"
1410
"github.com/arduino/arduino-check/configuration"
1511
"github.com/arduino/arduino-check/configuration/checkmode"
1612
"github.com/arduino/arduino-check/project"
13+
"github.com/arduino/arduino-check/result"
1714
"github.com/arduino/arduino-cli/cli/errorcodes"
1815
"github.com/arduino/arduino-cli/cli/feedback"
1916
"github.com/sirupsen/logrus"
@@ -37,20 +34,23 @@ func RunChecks(project project.Type) {
3734
continue
3835
}
3936

40-
fmt.Printf("Running check %s: ", checkConfiguration.ID)
41-
result, output := checkConfiguration.CheckFunction()
42-
fmt.Printf("%s\n", result.String())
43-
if result == checkresult.NotRun {
44-
// TODO: make the check functions output an explanation for why they didn't run
45-
fmt.Printf("%s: %s\n", checklevel.Notice, output)
46-
} else if result != checkresult.Pass {
47-
checkLevel, err := checklevel.CheckLevel(checkConfiguration)
48-
if err != nil {
49-
feedback.Errorf("Error while determining check level: %v", err)
50-
os.Exit(errorcodes.ErrGeneric)
51-
}
52-
fmt.Printf("%s: %s\n", checkLevel.String(), message(checkConfiguration.MessageTemplate, output))
37+
// Output will be printed after all checks are finished when configured for "json" output format
38+
if configuration.OutputFormat() == "text" {
39+
fmt.Printf("Running check %s: ", checkConfiguration.ID)
5340
}
41+
checkResult, checkOutput := checkConfiguration.CheckFunction()
42+
reportText := result.Record(project, checkConfiguration, checkResult, checkOutput)
43+
if configuration.OutputFormat() == "text" {
44+
fmt.Print(reportText)
45+
}
46+
}
47+
48+
// Checks are finished for this project, so summarize its check results in the report.
49+
result.AddProjectSummaryReport(project)
50+
51+
if configuration.OutputFormat() == "text" {
52+
// Print the project check results summary.
53+
fmt.Print(result.ProjectSummaryText(project))
5454
}
5555
}
5656

@@ -89,14 +89,3 @@ func shouldRun(checkConfiguration checkconfigurations.Type, currentProject proje
8989

9090
return false, fmt.Errorf("Check %s is incorrectly configured", checkConfiguration.ID)
9191
}
92-
93-
// message fills the message template provided by the check configuration with the check output.
94-
// TODO: make checkOutput a struct to allow for more advanced message templating
95-
func message(templateText string, checkOutput string) string {
96-
messageTemplate := template.Must(template.New("messageTemplate").Parse(templateText))
97-
98-
messageBuffer := new(bytes.Buffer)
99-
messageTemplate.Execute(messageBuffer, checkOutput)
100-
101-
return messageBuffer.String()
102-
}

configuration/configuration.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ func Initialize() {
1616
// TODO configuration according to command line input
1717
// TODO validate target path value, exit if not found
1818
// TODO support multiple paths
19+
// TODO validate output format input
20+
1921
targetPath = paths.New("e:/electronics/arduino/libraries/arduino-check-test-library")
2022

2123
// customCheckModes[checkmode.Permissive] = false
@@ -24,6 +26,8 @@ func Initialize() {
2426
// customCheckModes[checkmode.Official] = false
2527
// superprojectType = projecttype.All
2628

29+
outputFormat = "text"
30+
2731
feedback.SetFormat(feedback.JSON)
2832
logrus.SetLevel(logrus.PanicLevel)
2933

@@ -55,6 +59,13 @@ func Recursive() bool {
5559
return recursive
5660
}
5761

62+
var outputFormat string
63+
64+
// OutputFormat returns the tool output format configuration value.
65+
func OutputFormat() string {
66+
return outputFormat
67+
}
68+
5869
var targetPath *paths.Path
5970

6071
// TargetPath returns the projects search path.

configuration/defaults.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
func setDefaults() {
1111
superprojectTypeFilter = projecttype.All
1212
recursive = true
13+
outputFormat = "text"
1314
// TODO: targetPath defaults to current path
1415
}
1516

go.sum

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V
9898
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
9999
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
100100
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
101+
github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
101102
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
102103
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
103104
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=

main.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,44 @@
11
package main
22

33
import (
4+
"fmt"
45
"os"
56

67
"github.com/arduino/arduino-check/check"
78
"github.com/arduino/arduino-check/configuration"
89
"github.com/arduino/arduino-check/project"
10+
"github.com/arduino/arduino-check/result"
911
"github.com/arduino/arduino-cli/cli/errorcodes"
1012
"github.com/arduino/arduino-cli/cli/feedback"
1113
)
1214

1315
func main() {
1416
configuration.Initialize()
17+
// Must be called after configuration.Initialize()
18+
result.Initialize()
19+
1520
projects, err := project.FindProjects()
1621
if err != nil {
1722
feedback.Errorf("Error while finding projects: %v", err)
1823
os.Exit(errorcodes.ErrGeneric)
1924
}
25+
2026
for _, project := range projects {
2127
check.RunChecks(project)
2228
}
29+
30+
// All projects have been checked, so summarize their check results in the report.
31+
result.AddSummaryReport()
32+
33+
if configuration.OutputFormat() == "text" {
34+
if len(projects) > 1 {
35+
// There are multiple projects, print the summary of check results for all projects.
36+
fmt.Print(result.SummaryText())
37+
}
38+
} else {
39+
// Print the complete JSON formatted report.
40+
fmt.Println(result.JSONReport())
41+
}
42+
2343
// TODO: set exit status according to check results
2444
}

result/result.go

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
// Package result records check results and provides reports and summary text on those results.
2+
package result
3+
4+
import (
5+
"bytes"
6+
"encoding/json"
7+
"fmt"
8+
"html/template"
9+
"os"
10+
11+
"github.com/arduino/arduino-check/check/checkconfigurations"
12+
"github.com/arduino/arduino-check/check/checklevel"
13+
"github.com/arduino/arduino-check/check/checkresult"
14+
"github.com/arduino/arduino-check/configuration"
15+
"github.com/arduino/arduino-check/configuration/checkmode"
16+
"github.com/arduino/arduino-check/project"
17+
"github.com/arduino/arduino-cli/cli/errorcodes"
18+
"github.com/arduino/arduino-cli/cli/feedback"
19+
"github.com/arduino/go-paths-helper"
20+
)
21+
22+
type reportType struct {
23+
Configuration toolConfigurationReportType `json:"configuration"`
24+
Projects []projectReportType `json:"projects"`
25+
Summary summaryReportType `json:"summary"`
26+
}
27+
28+
type toolConfigurationReportType struct {
29+
Paths []*paths.Path `json:"paths"`
30+
ProjectType string `json:"projectType"`
31+
Recursive bool `json:"recursive"`
32+
}
33+
34+
type projectReportType struct {
35+
Path *paths.Path `json:"path"`
36+
ProjectType string `json:"projectType"`
37+
Configuration projectConfigurationReportType `json:"configuration"`
38+
Checks []checkReportType `json:"checks"`
39+
Summary summaryReportType `json:"summary"`
40+
}
41+
42+
type projectConfigurationReportType struct {
43+
Permissive bool `json:"permissive"`
44+
LibraryManagerSubmit bool `json:"libraryManagerSubmit"`
45+
LibraryManagerUpdate bool `json:"libraryManagerUpdate"`
46+
Official bool `json:"official"`
47+
}
48+
49+
type checkReportType struct {
50+
Category string `json:"category"`
51+
Subcategory string `json:"subcategory"`
52+
ID string `json:"ID"`
53+
Brief string `json:"brief"`
54+
Description string `json:"description"`
55+
Result string `json:"result"`
56+
Level string `json:"level"`
57+
Message string `json:"message"`
58+
}
59+
60+
type summaryReportType struct {
61+
Pass bool `json:"pass"`
62+
WarningCount int `json:"warningCount"`
63+
ErrorCount int `json:"errorCount"`
64+
}
65+
66+
var report reportType
67+
68+
// Initialize adds the tool configuration data to the report.
69+
func Initialize() {
70+
report.Configuration = toolConfigurationReportType{
71+
Paths: []*paths.Path{configuration.TargetPath()},
72+
ProjectType: configuration.SuperprojectTypeFilter().String(),
73+
Recursive: configuration.Recursive(),
74+
}
75+
}
76+
77+
// Record records the result of a check and returns a text summary for it.
78+
func Record(checkedProject project.Type, checkConfiguration checkconfigurations.Type, checkResult checkresult.Type, checkOutput string) string {
79+
checkMessage := message(checkConfiguration.MessageTemplate, checkOutput)
80+
81+
checkLevel, err := checklevel.CheckLevel(checkConfiguration)
82+
if err != nil {
83+
feedback.Errorf("Error while determining check level: %v", err)
84+
os.Exit(errorcodes.ErrGeneric)
85+
}
86+
87+
summaryText := fmt.Sprintf("%v\n", checkResult.String())
88+
89+
if checkResult == checkresult.NotRun {
90+
// TODO: make the check functions output an explanation for why they didn't run
91+
summaryText += fmt.Sprintf("%s: %s\n", checklevel.Notice.String(), checkOutput)
92+
} else if checkResult != checkresult.Pass {
93+
summaryText += fmt.Sprintf("%s: %s\n", checkLevel.String(), checkMessage)
94+
}
95+
96+
checkReport := checkReportType{
97+
Category: checkConfiguration.Category,
98+
Subcategory: checkConfiguration.Subcategory,
99+
ID: checkConfiguration.ID,
100+
Brief: checkConfiguration.Brief,
101+
Description: checkConfiguration.Description,
102+
Result: checkResult.String(),
103+
Level: checkLevel.String(),
104+
Message: checkMessage,
105+
}
106+
107+
reportExists, projectReportIndex := getProjectReportIndex(checkedProject.Path)
108+
if !reportExists {
109+
// There is no existing report for this project.
110+
report.Projects = append(
111+
report.Projects,
112+
projectReportType{
113+
Path: checkedProject.Path,
114+
ProjectType: checkedProject.ProjectType.String(),
115+
Configuration: projectConfigurationReportType{
116+
Permissive: configuration.CheckModes(checkedProject.ProjectType)[checkmode.Permissive],
117+
LibraryManagerSubmit: configuration.CheckModes(checkedProject.ProjectType)[checkmode.Permissive],
118+
LibraryManagerUpdate: configuration.CheckModes(checkedProject.ProjectType)[checkmode.LibraryManagerIndexed],
119+
Official: configuration.CheckModes(checkedProject.ProjectType)[checkmode.Official],
120+
},
121+
Checks: []checkReportType{checkReport},
122+
},
123+
)
124+
} else {
125+
// There's already a report for this project, just add the checks report to it
126+
report.Projects[projectReportIndex].Checks = append(report.Projects[projectReportIndex].Checks, checkReport)
127+
}
128+
129+
return summaryText
130+
}
131+
132+
// AddProjectSummaryReport summarizes the results of all checks on the given project and adds it to the report.
133+
func AddProjectSummaryReport(checkedProject project.Type) {
134+
reportExists, projectReportIndex := getProjectReportIndex(checkedProject.Path)
135+
if !reportExists {
136+
panic(fmt.Sprintf("Unable to find report for %v when generating report summary", checkedProject.Path))
137+
}
138+
139+
pass := true
140+
warningCount := 0
141+
errorCount := 0
142+
for _, checkReport := range report.Projects[projectReportIndex].Checks {
143+
if checkReport.Result == checkresult.Fail.String() {
144+
if checkReport.Level == checklevel.Warning.String() {
145+
warningCount += 1
146+
} else if checkReport.Level == checklevel.Error.String() {
147+
errorCount += 1
148+
pass = false
149+
}
150+
}
151+
}
152+
153+
report.Projects[projectReportIndex].Summary = summaryReportType{
154+
Pass: pass,
155+
WarningCount: warningCount,
156+
ErrorCount: errorCount,
157+
}
158+
}
159+
160+
// ProjectSummaryText returns a text summary of the check results for the given project.
161+
func ProjectSummaryText(checkedProject project.Type) string {
162+
reportExists, projectReportIndex := getProjectReportIndex(checkedProject.Path)
163+
if !reportExists {
164+
panic(fmt.Sprintf("Unable to find report for %v when generating report summary text", checkedProject.Path))
165+
}
166+
167+
projectSummaryReport := report.Projects[projectReportIndex].Summary
168+
return fmt.Sprintf("\nFinished checking project. Results:\nWarning count: %v\nError count: %v\nChecks passed: %v\n\n", projectSummaryReport.WarningCount, projectSummaryReport.ErrorCount, projectSummaryReport.Pass)
169+
}
170+
171+
// AddSummaryReport summarizes the check results for all projects and adds it to the report.
172+
func AddSummaryReport() {
173+
pass := true
174+
warningCount := 0
175+
errorCount := 0
176+
for _, projectReport := range report.Projects {
177+
if !projectReport.Summary.Pass {
178+
pass = false
179+
}
180+
warningCount += projectReport.Summary.WarningCount
181+
errorCount += projectReport.Summary.ErrorCount
182+
}
183+
184+
report.Summary = summaryReportType{
185+
Pass: pass,
186+
WarningCount: warningCount,
187+
ErrorCount: errorCount,
188+
}
189+
}
190+
191+
// SummaryText returns a text summary of the cumulative check results.
192+
func SummaryText() string {
193+
return fmt.Sprintf("Finished checking projects. Results:\nWarning count: %v\nError count: %v\nChecks passed: %v\n", report.Summary.WarningCount, report.Summary.ErrorCount, report.Summary.Pass)
194+
}
195+
196+
// Report returns a JSON formatted report of checks on all projects.
197+
func JSONReport() string {
198+
return string(jsonReportRaw())
199+
}
200+
201+
func jsonReportRaw() []byte {
202+
reportJSON, err := json.MarshalIndent(report, "", " ")
203+
if err != nil {
204+
panic(fmt.Sprintf("Error while formatting checks report: %v", err))
205+
}
206+
207+
return reportJSON
208+
}
209+
210+
func getProjectReportIndex(projectPath *paths.Path) (bool, int) {
211+
var index int
212+
var projectReport projectReportType
213+
for index, projectReport = range report.Projects {
214+
if projectReport.Path == projectPath {
215+
return true, index
216+
}
217+
}
218+
219+
// There is no element in the report for this project.
220+
return false, index + 1
221+
}
222+
223+
// message fills the message template provided by the check configuration with the check output.
224+
// TODO: make checkOutput a struct to allow for more advanced message templating
225+
func message(templateText string, checkOutput string) string {
226+
messageTemplate := template.Must(template.New("messageTemplate").Parse(templateText))
227+
228+
messageBuffer := new(bytes.Buffer)
229+
messageTemplate.Execute(messageBuffer, checkOutput)
230+
231+
return messageBuffer.String()
232+
}

0 commit comments

Comments
 (0)