1/* 2Copyright 2019 The Kubernetes Authors. 3 4Licensed under the Apache License, Version 2.0 (the "License"); 5you may not use this file except in compliance with the License. 6You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10Unless required by applicable law or agreed to in writing, software 11distributed under the License is distributed on an "AS IS" BASIS, 12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13See the License for the specific language governing permissions and 14limitations under the License. 15*/ 16 17package reporters 18 19import ( 20 "bytes" 21 "encoding/json" 22 "fmt" 23 "io/ioutil" 24 "net/http" 25 "strings" 26 "time" 27 28 "k8s.io/klog/v2" 29 30 "github.com/onsi/ginkgo/config" 31 "github.com/onsi/ginkgo/types" 32) 33 34// ProgressReporter is a ginkgo reporter which tracks the total number of tests to be run/passed/failed/skipped. 35// As new tests are completed it updates the values and prints them to stdout and optionally, sends the updates 36// to the configured URL. 37type ProgressReporter struct { 38 LastMsg string `json:"msg"` 39 40 TestsTotal int `json:"total"` 41 TestsCompleted int `json:"completed"` 42 TestsSkipped int `json:"skipped"` 43 TestsFailed int `json:"failed"` 44 45 Failures []string `json:"failures,omitempty"` 46 47 progressURL string 48 client *http.Client 49} 50 51// NewProgressReporter returns a progress reporter which posts updates to the given URL. 52func NewProgressReporter(progressReportURL string) *ProgressReporter { 53 rep := &ProgressReporter{ 54 Failures: []string{}, 55 progressURL: progressReportURL, 56 } 57 if len(progressReportURL) > 0 { 58 rep.client = &http.Client{ 59 Timeout: time.Second * 10, 60 } 61 } 62 return rep 63} 64 65// SpecSuiteWillBegin is invoked by ginkgo when the suite is about to start and is the first point in which we can 66// antipate the number of tests which will be run. 67func (reporter *ProgressReporter) SpecSuiteWillBegin(cfg config.GinkgoConfigType, summary *types.SuiteSummary) { 68 reporter.TestsTotal = summary.NumberOfSpecsThatWillBeRun 69 reporter.LastMsg = "Test Suite starting" 70 reporter.sendUpdates() 71} 72 73// SpecSuiteDidEnd is the last method invoked by Ginkgo after all the specs are run. 74func (reporter *ProgressReporter) SpecSuiteDidEnd(summary *types.SuiteSummary) { 75 reporter.LastMsg = "Test Suite completed" 76 reporter.sendUpdates() 77} 78 79// SpecDidComplete is invoked by Ginkgo each time a spec is completed (including skipped specs). 80func (reporter *ProgressReporter) SpecDidComplete(specSummary *types.SpecSummary) { 81 testname := strings.Join(specSummary.ComponentTexts[1:], " ") 82 switch specSummary.State { 83 case types.SpecStateFailed: 84 if len(specSummary.ComponentTexts) > 0 { 85 reporter.Failures = append(reporter.Failures, testname) 86 } else { 87 reporter.Failures = append(reporter.Failures, "Unknown test name") 88 } 89 reporter.TestsFailed++ 90 reporter.LastMsg = fmt.Sprintf("FAILED %v", testname) 91 case types.SpecStatePassed: 92 reporter.TestsCompleted++ 93 reporter.LastMsg = fmt.Sprintf("PASSED %v", testname) 94 case types.SpecStateSkipped: 95 reporter.TestsSkipped++ 96 return 97 default: 98 return 99 } 100 101 reporter.sendUpdates() 102} 103 104// sendUpdates serializes the current progress and prints it to stdout and also posts it to the configured endpoint if set. 105func (reporter *ProgressReporter) sendUpdates() { 106 b := reporter.serialize() 107 fmt.Println(string(b)) 108 go reporter.postProgressToURL(b) 109} 110 111func (reporter *ProgressReporter) postProgressToURL(b []byte) { 112 // If a progressURL and client is set/available then POST to it. Noop otherwise. 113 if reporter.client == nil || len(reporter.progressURL) == 0 { 114 return 115 } 116 117 resp, err := reporter.client.Post(reporter.progressURL, "application/json", bytes.NewReader(b)) 118 if err != nil { 119 klog.Errorf("Failed to post progress update to %v: %v", reporter.progressURL, err) 120 return 121 } 122 if resp.StatusCode >= 400 { 123 klog.Errorf("Unexpected response when posting progress update to %v: %v", reporter.progressURL, resp.StatusCode) 124 if resp.Body != nil { 125 defer resp.Body.Close() 126 respBody, err := ioutil.ReadAll(resp.Body) 127 if err != nil { 128 klog.Errorf("Failed to read response body from posting progress: %v", err) 129 return 130 } 131 klog.Errorf("Response body from posting progress update: %v", respBody) 132 } 133 134 return 135 } 136} 137 138func (reporter *ProgressReporter) serialize() []byte { 139 b, err := json.Marshal(reporter) 140 if err != nil { 141 return []byte(fmt.Sprintf(`{"msg":"%v", "error":"%v"}`, reporter.LastMsg, err)) 142 } 143 return b 144} 145 146// SpecWillRun is implemented as a noop to satisfy the reporter interface for ginkgo. 147func (reporter *ProgressReporter) SpecWillRun(specSummary *types.SpecSummary) {} 148 149// BeforeSuiteDidRun is implemented as a noop to satisfy the reporter interface for ginkgo. 150func (reporter *ProgressReporter) BeforeSuiteDidRun(setupSummary *types.SetupSummary) {} 151 152// AfterSuiteDidRun is implemented as a noop to satisfy the reporter interface for ginkgo. 153func (reporter *ProgressReporter) AfterSuiteDidRun(setupSummary *types.SetupSummary) {} 154