inspektor-gadget
237 строк · 7.2 Кб
1// Copyright 2023 The Inspektor Gadget authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package main
16
17import (
18"bytes"
19"encoding/json"
20"flag"
21"fmt"
22"log"
23"os"
24"sort"
25"time"
26
27"github.com/medyagh/gopogh/pkg/models"
28"github.com/medyagh/gopogh/pkg/parser"
29"github.com/medyagh/gopogh/pkg/report"
30)
31
32const (
33// summaryLimitInBytes is the maximum size of the summary in bytes.
34// https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#step-isolation-and-limits
35summaryLimitInBytes = 1000000
36// conclusion is the conclusion (success, failure, skipped and cancelled) indicating GitHub Action test step status.
37// https://docs.github.com/en/actions/learn-github-actions/contexts#steps-context
38conclusionSuccess = "success"
39conclusionFailure = "failure"
40conclusionSkipped = "skipped"
41conclusionCancelled = "cancelled"
42)
43
44var ErrInvalidContent = fmt.Errorf("invalid content")
45
46var (
47inPath = flag.String("in", "", "path to JSON file produced by go tool test2json")
48outPath = flag.String("out", "", "path to output file")
49outSummary = flag.String("out_summary", "", "path to summary file")
50conclusion = flag.String("conclusion", "", "conclusion (success, failure, skipped and cancelled) indicating GitHub Action test step status")
51)
52
53func main() {
54flag.Parse()
55
56if *inPath == "" {
57log.Fatal("must provide path to JSON input file")
58}
59
60if *outPath == "" {
61log.Fatal("must provide path to output file")
62}
63
64events, err := parser.ParseJSON(*inPath)
65if err != nil {
66log.Fatal(err)
67}
68groups := parser.ProcessEvents(events)
69content, err := report.Generate(models.ReportDetail{}, groups)
70if err != nil {
71log.Fatal(err)
72}
73
74if *outSummary != "" {
75r, err := summaryForContent(content)
76if err != nil {
77log.Fatal(err)
78}
79if err = os.WriteFile(*outSummary, r, 0644); err != nil {
80log.Fatal(err)
81}
82}
83
84markdown, err := markdownForContent(content)
85if err != nil {
86log.Fatal(err)
87}
88
89if err = os.WriteFile(*outPath, markdown, 0644); err != nil {
90log.Fatal(err)
91}
92}
93
94func markdownForContent(content report.DisplayContent) ([]byte, error) {
95// validation
96if _, ok := content.Results["pass"]; !ok {
97return nil, fmt.Errorf("checking passed tests: %w", ErrInvalidContent)
98}
99if _, ok := content.Results["skip"]; !ok {
100return nil, fmt.Errorf("checking skip tests: %w", ErrInvalidContent)
101}
102if _, ok := content.Results["fail"]; !ok {
103return nil, fmt.Errorf("checking failed tests: %w", ErrInvalidContent)
104}
105
106// set report status icon
107var statusIcon string
108switch *conclusion {
109case conclusionFailure:
110statusIcon = ":red_circle:"
111case conclusionSkipped:
112fallthrough
113case conclusionCancelled:
114statusIcon = ":white_circle:"
115case conclusionSuccess:
116fallthrough
117default:
118statusIcon = ":green_circle:"
119}
120
121// summary
122var buf bytes.Buffer
123fmt.Fprintf(&buf, "### Test Report %s\n", statusIcon)
124fmt.Fprintf(&buf, "#### Summary\n")
125fmt.Fprintf(&buf, "| Total Tests | Passed :heavy_check_mark: | Failed :x: | Skipped :arrow_right_hook: |\n")
126fmt.Fprintf(&buf, "| ----- | ---- | ---- | ---- |\n")
127fmt.Fprintf(&buf, "| %d | %d | %d | %d |\n", content.TotalTests,
128len(content.Results["pass"]), len(content.Results["fail"]), len(content.Results["skip"]))
129
130// test durations
131fmt.Fprintf(&buf, "#### Test Durations :stopwatch:\n")
132appendDuration(content, &buf, "Passed", "pass")
133appendDuration(content, &buf, "Failed", "fail")
134appendDuration(content, &buf, "Skipped", "skip")
135
136// failed tests
137if len(content.Results["fail"]) > 0 {
138fmt.Fprintf(&buf, "\n#### Failed Tests\n")
139for _, test := range content.Results["fail"] {
140s, d := testEventToDetailsBlock(test.TestName, test.Events)
141// check if we are over the limit
142if buf.Len()+s > summaryLimitInBytes {
143fmt.Fprintf(&buf, "<details><summary>%s</summary>\n\n", test.TestName)
144fmt.Fprintf(&buf, "Logs skipped due to size limitations. Please check workflow [logs](%s) for details.\n", ghaJobUrl())
145fmt.Fprintf(&buf, "</details>\n")
146continue
147}
148
149fmt.Fprintf(&buf, "%s", d)
150}
151fmt.Fprintf(&buf, "\n")
152}
153
154return buf.Bytes(), nil
155}
156
157func summaryForContent(content report.DisplayContent) ([]byte, error) {
158var s struct {
159Id string `json:"id"`
160RunId string `json:"run_id"`
161RefName string `json:"ref_name"`
162PullRequest struct {
163Id string `json:"id"`
164Author string `json:"author"`
165Title string `json:"title"`
166} `json:"pull_request"`
167Summary struct {
168Conclusion string `json:"conclusion"`
169Pass []string `json:"pass"`
170Fail []string `json:"fail"`
171Skip []string `json:"skip"`
172} `json:"summary"`
173}
174
175if os.Getenv("GITHUB_ACTIONS") != "true" {
176return nil, fmt.Errorf("summary is only available in GitHub Actions")
177}
178
179s.Id = fmt.Sprintf("%s_%s", os.Getenv("GITHUB_RUN_NUMBER"), os.Getenv("GITHUB_RUN_ATTEMPT"))
180s.RunId = os.Getenv("GITHUB_RUN_ID")
181s.RefName = os.Getenv("GITHUB_REF_NAME")
182s.PullRequest.Id = os.Getenv("PULL_REQUEST_ID")
183s.PullRequest.Author = os.Getenv("PULL_REQUEST_AUTHOR")
184s.PullRequest.Title = os.Getenv("PULL_REQUEST_TITLE")
185
186for _, test := range content.Results["pass"] {
187s.Summary.Pass = append(s.Summary.Pass, test.TestName)
188}
189for _, test := range content.Results["fail"] {
190s.Summary.Fail = append(s.Summary.Fail, test.TestName)
191}
192for _, test := range content.Results["skip"] {
193s.Summary.Skip = append(s.Summary.Skip, test.TestName)
194}
195s.Summary.Conclusion = *conclusion
196
197return json.Marshal(s)
198}
199
200func appendDuration(content report.DisplayContent, buf *bytes.Buffer, title, status string) {
201if len(content.Results[status]) == 0 {
202return
203}
204fmt.Fprintf(buf, "<details><summary>%s</summary>\n\n", title)
205fmt.Fprintf(buf, "| Duration | Test | Run Order |\n")
206fmt.Fprintf(buf, "| -------- | ---- | --------- |\n")
207for _, test := range sortTestGroups(content.Results[status]) {
208fmt.Fprintf(buf, "| %s | %s | %d |\n", time.Duration(test.Duration*float64(time.Second)), test.TestName, test.TestOrder)
209}
210fmt.Fprintf(buf, "</details>\n")
211}
212
213func sortTestGroups(groups []models.TestGroup) []models.TestGroup {
214sort.Slice(groups, func(i, j int) bool {
215return groups[i].Duration > groups[j].Duration
216})
217return groups
218}
219
220func testEventToDetailsBlock(name string, events []models.TestEvent) (int, []byte) {
221var buf bytes.Buffer
222fmt.Fprintf(&buf, "<details><summary>%s</summary>\n\n", name)
223fmt.Fprintf(&buf, "```code\n")
224for _, event := range events {
225fmt.Fprintf(&buf, "%s", event.Output)
226}
227fmt.Fprintf(&buf, "```\n")
228fmt.Fprintf(&buf, "</details>\n")
229return len(buf.Bytes()), buf.Bytes()
230}
231
232func ghaJobUrl() string {
233if os.Getenv("GITHUB_ACTIONS") != "true" {
234return ""
235}
236return os.Getenv("GITHUB_SERVER_URL") + "/" + os.Getenv("GITHUB_REPOSITORY") + "/actions/runs/" + os.Getenv("GITHUB_RUN_ID") + "/attempts/" + os.Getenv("GITHUB_RUN_ATTEMPT")
237}
238