Skip to content

Commit 2dce08d

Browse files
committedMar 11, 2024
Initial demystifier commit with code from:
Tiger Kaovilai and Michal Pryc Signed-off-by: Tiger Kaovilai <tkaovila@redhat.com> Signed-off-by: Michal Pryc <mpryc@redhat.com>
0 parents  commit 2dce08d

15 files changed

+7252
-0
lines changed
 

‎.editorconfig

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# https://EditorConfig.org
2+
3+
root = true
4+
5+
[*]
6+
indent_size = 4
7+
indent_style = space
8+
end_of_line = lf
9+
charset = utf-8
10+
trim_trailing_whitespace = true
11+
insert_final_newline = true
12+
13+
[{*.go,go.mod,Makefile}]
14+
indent_style = unset
15+
16+
[{*.{yaml,yml},PROJECT}]
17+
indent_size = 2
18+
19+
[{*.md,LICENSE}]
20+
indent_size = unset
21+
22+
[/tests/testdata/**]
23+
charset = unset
24+
end_of_line = unset
25+
insert_final_newline = unset
26+
trim_trailing_whitespace = unset
27+
indent_style = unset
28+
indent_size = unset

‎.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
bin
2+
.vscode
3+
demystifier

‎.golangci.yml

+129
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
# Documentation reference https://github.com/golangci/golangci-lint/blob/v1.56.2/.golangci.reference.yml
2+
run:
3+
skip-dirs-use-default: false
4+
modules-download-mode: readonly
5+
allow-parallel-runners: true
6+
skip-dirs:
7+
- test/*
8+
9+
output:
10+
format: colored-line-number
11+
print-issued-lines: true
12+
print-linter-name: true
13+
uniq-by-line: true
14+
sort-results: true
15+
16+
linters-settings:
17+
dogsled:
18+
max-blank-identifiers: 2
19+
errcheck:
20+
check-type-assertions: true
21+
check-blank: true
22+
gci:
23+
sections:
24+
- standard
25+
- default
26+
- prefix(github.com/migtools/oadp-non-admin)
27+
goconst:
28+
min-len: 3
29+
min-occurrences: 5
30+
gofmt:
31+
simplify: true
32+
goheader:
33+
# copy from ./hack/boilerplate.go.txt
34+
template: |-
35+
Copyright 2024.
36+
37+
Licensed under the Apache License, Version 2.0 (the "License");
38+
you may not use this file except in compliance with the License.
39+
You may obtain a copy of the License at
40+
41+
http://www.apache.org/licenses/LICENSE-2.0
42+
43+
Unless required by applicable law or agreed to in writing, software
44+
distributed under the License is distributed on an "AS IS" BASIS,
45+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
46+
See the License for the specific language governing permissions and
47+
limitations under the License.
48+
govet:
49+
enable-all: true
50+
misspell:
51+
locale: US
52+
nakedret:
53+
max-func-lines: 30
54+
nolintlint:
55+
allow-unused: false
56+
allow-no-explanation: []
57+
require-explanation: true
58+
require-specific: true
59+
revive:
60+
enable-all-rules: true
61+
rules:
62+
- name: line-length-limit
63+
disabled: true
64+
- name: function-length
65+
disabled: true
66+
# TODO remove
67+
- name: cyclomatic
68+
disabled: true
69+
- name: cognitive-complexity
70+
disabled: true
71+
unparam:
72+
check-exported: true
73+
74+
linters:
75+
disable-all: true
76+
enable:
77+
- asasalint
78+
- asciicheck
79+
- bidichk
80+
- bodyclose
81+
- dogsled
82+
- dupword
83+
- durationcheck
84+
- errcheck
85+
- errchkjson
86+
- exportloopref
87+
- gci
88+
- ginkgolinter
89+
- goconst
90+
- gofmt
91+
- goheader
92+
- goprintffuncname
93+
- gosec
94+
- gosimple
95+
- govet
96+
- ineffassign
97+
- misspell
98+
- nakedret
99+
- nilerr
100+
- noctx
101+
- nolintlint
102+
- nosprintfhostport
103+
- revive
104+
- staticcheck
105+
- stylecheck
106+
- unconvert
107+
- unparam
108+
- unused
109+
- usestdlibvars
110+
fast: false
111+
112+
issues:
113+
exclude-use-default: false
114+
exclude-rules:
115+
- linters:
116+
- revive
117+
text: "^struct-tag: unknown option 'inline' in JSON tag$"
118+
- linters:
119+
- revive
120+
text: "^add-constant: avoid magic numbers like '0', create a named constant for it$"
121+
- linters:
122+
- revive
123+
text: "^add-constant: avoid magic numbers like '1', create a named constant for it$"
124+
max-issues-per-linter: 0
125+
max-same-issues: 0
126+
127+
severity:
128+
default-severity: error
129+
case-sensitive: false

‎LICENSE

+191
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
2+
Apache License
3+
Version 2.0, January 2004
4+
http://www.apache.org/licenses/
5+
6+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7+
8+
1. Definitions.
9+
10+
"License" shall mean the terms and conditions for use, reproduction,
11+
and distribution as defined by Sections 1 through 9 of this document.
12+
13+
"Licensor" shall mean the copyright owner or entity authorized by
14+
the copyright owner that is granting the License.
15+
16+
"Legal Entity" shall mean the union of the acting entity and all
17+
other entities that control, are controlled by, or are under common
18+
control with that entity. For the purposes of this definition,
19+
"control" means (i) the power, direct or indirect, to cause the
20+
direction or management of such entity, whether by contract or
21+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
22+
outstanding shares, or (iii) beneficial ownership of such entity.
23+
24+
"You" (or "Your") shall mean an individual or Legal Entity
25+
exercising permissions granted by this License.
26+
27+
"Source" form shall mean the preferred form for making modifications,
28+
including but not limited to software source code, documentation
29+
source, and configuration files.
30+
31+
"Object" form shall mean any form resulting from mechanical
32+
transformation or translation of a Source form, including but
33+
not limited to compiled object code, generated documentation,
34+
and conversions to other media types.
35+
36+
"Work" shall mean the work of authorship, whether in Source or
37+
Object form, made available under the License, as indicated by a
38+
copyright notice that is included in or attached to the work
39+
(an example is provided in the Appendix below).
40+
41+
"Derivative Works" shall mean any work, whether in Source or Object
42+
form, that is based on (or derived from) the Work and for which the
43+
editorial revisions, annotations, elaborations, or other modifications
44+
represent, as a whole, an original work of authorship. For the purposes
45+
of this License, Derivative Works shall not include works that remain
46+
separable from, or merely link (or bind by name) to the interfaces of,
47+
the Work and Derivative Works thereof.
48+
49+
"Contribution" shall mean any work of authorship, including
50+
the original version of the Work and any modifications or additions
51+
to that Work or Derivative Works thereof, that is intentionally
52+
submitted to Licensor for inclusion in the Work by the copyright owner
53+
or by an individual or Legal Entity authorized to submit on behalf of
54+
the copyright owner. For the purposes of this definition, "submitted"
55+
means any form of electronic, verbal, or written communication sent
56+
to the Licensor or its representatives, including but not limited to
57+
communication on electronic mailing lists, source code control systems,
58+
and issue tracking systems that are managed by, or on behalf of, the
59+
Licensor for the purpose of discussing and improving the Work, but
60+
excluding communication that is conspicuously marked or otherwise
61+
designated in writing by the copyright owner as "Not a Contribution."
62+
63+
"Contributor" shall mean Licensor and any individual or Legal Entity
64+
on behalf of whom a Contribution has been received by Licensor and
65+
subsequently incorporated within the Work.
66+
67+
2. Grant of Copyright License. Subject to the terms and conditions of
68+
this License, each Contributor hereby grants to You a perpetual,
69+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70+
copyright license to reproduce, prepare Derivative Works of,
71+
publicly display, publicly perform, sublicense, and distribute the
72+
Work and such Derivative Works in Source or Object form.
73+
74+
3. Grant of Patent License. Subject to the terms and conditions of
75+
this License, each Contributor hereby grants to You a perpetual,
76+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77+
(except as stated in this section) patent license to make, have made,
78+
use, offer to sell, sell, import, and otherwise transfer the Work,
79+
where such license applies only to those patent claims licensable
80+
by such Contributor that are necessarily infringed by their
81+
Contribution(s) alone or by combination of their Contribution(s)
82+
with the Work to which such Contribution(s) was submitted. If You
83+
institute patent litigation against any entity (including a
84+
cross-claim or counterclaim in a lawsuit) alleging that the Work
85+
or a Contribution incorporated within the Work constitutes direct
86+
or contributory patent infringement, then any patent licenses
87+
granted to You under this License for that Work shall terminate
88+
as of the date such litigation is filed.
89+
90+
4. Redistribution. You may reproduce and distribute copies of the
91+
Work or Derivative Works thereof in any medium, with or without
92+
modifications, and in Source or Object form, provided that You
93+
meet the following conditions:
94+
95+
(a) You must give any other recipients of the Work or
96+
Derivative Works a copy of this License; and
97+
98+
(b) You must cause any modified files to carry prominent notices
99+
stating that You changed the files; and
100+
101+
(c) You must retain, in the Source form of any Derivative Works
102+
that You distribute, all copyright, patent, trademark, and
103+
attribution notices from the Source form of the Work,
104+
excluding those notices that do not pertain to any part of
105+
the Derivative Works; and
106+
107+
(d) If the Work includes a "NOTICE" text file as part of its
108+
distribution, then any Derivative Works that You distribute must
109+
include a readable copy of the attribution notices contained
110+
within such NOTICE file, excluding those notices that do not
111+
pertain to any part of the Derivative Works, in at least one
112+
of the following places: within a NOTICE text file distributed
113+
as part of the Derivative Works; within the Source form or
114+
documentation, if provided along with the Derivative Works; or,
115+
within a display generated by the Derivative Works, if and
116+
wherever such third-party notices normally appear. The contents
117+
of the NOTICE file are for informational purposes only and
118+
do not modify the License. You may add Your own attribution
119+
notices within Derivative Works that You distribute, alongside
120+
or as an addendum to the NOTICE text from the Work, provided
121+
that such additional attribution notices cannot be construed
122+
as modifying the License.
123+
124+
You may add Your own copyright statement to Your modifications and
125+
may provide additional or different license terms and conditions
126+
for use, reproduction, or distribution of Your modifications, or
127+
for any such Derivative Works as a whole, provided Your use,
128+
reproduction, and distribution of the Work otherwise complies with
129+
the conditions stated in this License.
130+
131+
5. Submission of Contributions. Unless You explicitly state otherwise,
132+
any Contribution intentionally submitted for inclusion in the Work
133+
by You to the Licensor shall be under the terms and conditions of
134+
this License, without any additional terms or conditions.
135+
Notwithstanding the above, nothing herein shall supersede or modify
136+
the terms of any separate license agreement you may have executed
137+
with Licensor regarding such Contributions.
138+
139+
6. Trademarks. This License does not grant permission to use the trade
140+
names, trademarks, service marks, or product names of the Licensor,
141+
except as required for reasonable and customary use in describing the
142+
origin of the Work and reproducing the content of the NOTICE file.
143+
144+
7. Disclaimer of Warranty. Unless required by applicable law or
145+
agreed to in writing, Licensor provides the Work (and each
146+
Contributor provides its Contributions) on an "AS IS" BASIS,
147+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148+
implied, including, without limitation, any warranties or conditions
149+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150+
PARTICULAR PURPOSE. You are solely responsible for determining the
151+
appropriateness of using or redistributing the Work and assume any
152+
risks associated with Your exercise of permissions under this License.
153+
154+
8. Limitation of Liability. In no event and under no legal theory,
155+
whether in tort (including negligence), contract, or otherwise,
156+
unless required by applicable law (such as deliberate and grossly
157+
negligent acts) or agreed to in writing, shall any Contributor be
158+
liable to You for damages, including any direct, indirect, special,
159+
incidental, or consequential damages of any character arising as a
160+
result of this License or out of the use or inability to use the
161+
Work (including but not limited to damages for loss of goodwill,
162+
work stoppage, computer failure or malfunction, or any and all
163+
other commercial damages or losses), even if such Contributor
164+
has been advised of the possibility of such damages.
165+
166+
9. Accepting Warranty or Additional Liability. While redistributing
167+
the Work or Derivative Works thereof, You may choose to offer,
168+
and charge a fee for, acceptance of support, warranty, indemnity,
169+
or other liability obligations and/or rights consistent with this
170+
License. However, in accepting such obligations, You may act only
171+
on Your own behalf and on Your sole responsibility, not on behalf
172+
of any other Contributor, and only if You agree to indemnify,
173+
defend, and hold each Contributor harmless for any liability
174+
incurred by, or claims asserted against, such Contributor by reason
175+
of your accepting any such warranty or additional liability.
176+
177+
END OF TERMS AND CONDITIONS
178+
179+
Copyright 2016 Red Hat, Inc.
180+
181+
Licensed under the Apache License, Version 2.0 (the "License");
182+
you may not use this file except in compliance with the License.
183+
You may obtain a copy of the License at
184+
185+
http://www.apache.org/licenses/LICENSE-2.0
186+
187+
Unless required by applicable law or agreed to in writing, software
188+
distributed under the License is distributed on an "AS IS" BASIS,
189+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
190+
See the License for the specific language governing permissions and
191+
limitations under the License.

‎Makefile

+104
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
#
2+
# Copyright 2024.
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+
## Location to install dependencies to
16+
17+
LOCALBIN ?= $(shell pwd)/bin
18+
$(LOCALBIN):
19+
mkdir -p $(LOCALBIN)
20+
21+
# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
22+
ifeq (,$(shell go env GOBIN))
23+
GOBIN=$(shell go env GOPATH)/bin
24+
else
25+
GOBIN=$(shell go env GOBIN)
26+
endif
27+
28+
GOBUILD = $(GOBIN)/go build
29+
GOCLEAN = $(GOBIN)/go clean
30+
GORUN = $(GOBIN)/go run
31+
GOTEST = $(GOBIN)/go test
32+
33+
BINARY_NAME = demystifier
34+
35+
GOLANGCI_LINT = $(LOCALBIN)/golangci-lint-$(GOLANGCI_LINT_VERSION)
36+
GOLANGCI_LINT_VERSION ?= v1.56.2
37+
38+
EC ?= $(LOCALBIN)/ec-$(EC_VERSION)
39+
EC_VERSION ?= 2.8.0
40+
41+
.PHONY: editorconfig
42+
editorconfig: $(LOCALBIN) ## Download editorconfig locally if necessary.
43+
@[ -f $(EC) ] || { \
44+
set -e ;\
45+
ec_binary=ec-$(shell go env GOOS)-$(shell go env GOARCH) ;\
46+
ec_tar=$(LOCALBIN)/$${ec_binary}.tar.gz ;\
47+
curl -sSLo $${ec_tar} https://github.com/editorconfig-checker/editorconfig-checker/releases/download/$(EC_VERSION)/$${ec_binary}.tar.gz ;\
48+
tar xzf $${ec_tar} ;\
49+
rm -rf $${ec_tar} ;\
50+
mv $(LOCALBIN)/$${ec_binary} $(EC) ;\
51+
}
52+
53+
.PHONY: all
54+
all: build
55+
56+
# Build target
57+
build:
58+
GOARCH=amd64 $(GOBUILD) -o $(BINARY_NAME) ./cmd/main/demystifier.go
59+
chmod +x $(BINARY_NAME)
60+
61+
# Example make run ARGS="--help"
62+
.PHONY: run
63+
run:
64+
$(GORUN) ./cmd/main/demystifier.go $(ARGS)
65+
66+
# Clean target
67+
clean:
68+
$(GOCLEAN)
69+
rm -f $(BINARY_NAME)
70+
71+
.PHONY: test
72+
test:
73+
$(GOTEST) $$(go list ./...)
74+
75+
.PHONY: golangci-lint
76+
golangci-lint: $(GOLANGCI_LINT) ## Download golangci-lint locally if necessary.
77+
$(GOLANGCI_LINT): $(LOCALBIN)
78+
$(call go-install-tool,$(GOLANGCI_LINT),github.com/golangci/golangci-lint/cmd/golangci-lint,${GOLANGCI_LINT_VERSION})
79+
80+
.PHONY: lint
81+
lint: golangci-lint ## Run golangci-lint linter & yamllint
82+
$(GOLANGCI_LINT) run
83+
84+
.PHONY: lint-fix
85+
lint-fix: golangci-lint ## Run golangci-lint linter and perform fixes
86+
$(GOLANGCI_LINT) run --fix
87+
88+
# go-install-tool will 'go install' any package with custom target and name of binary, if it doesn't exist
89+
# $1 - target path with name of binary (ideally with version)
90+
# $2 - package url which can be installed
91+
# $3 - specific version of package
92+
define go-install-tool
93+
@[ -f $(1) ] || { \
94+
set -e; \
95+
package=$(2)@$(3) ;\
96+
echo "Downloading $${package}" ;\
97+
GOBIN=$(LOCALBIN) go install $${package} ;\
98+
mv "$$(echo "$(1)" | sed "s/-$(3)$$//")" $(1) ;\
99+
}
100+
endef
101+
102+
.PHONY: ec
103+
ec: editorconfig ## Run file formatter checks against all project's files.
104+
$(EC)

‎README.md

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# Demystifier
2+
3+
A simple CLI to gather PROW logs, and parse them for further processing.
4+
5+
6+
## Getting Started
7+
8+
### Prerequisites
9+
- go version v1.19.0+
10+
- shell
11+
12+
### Building the tool
13+
```sh
14+
$ make build
15+
$ ./demystifier --help
16+
```
17+
18+
** Cleanup the build**
19+
```sh
20+
$ make clean
21+
```
22+
23+
### Running
24+
25+
#### Gather summary information about the PROW job run
26+
27+
```sh
28+
$ ./demystifier "${URL}"
29+
30+
# Example, with URL from the GitHub PR comment
31+
$ ./demystifier https://prow.ci.openshift.org/view/gs/test-platform-results/pr-logs/pull/openshift_oadp-operator/1266/pull-ci-openshift-oadp-operator-master-4.13-e2e-test-azure/1767186600720076800
32+
33+
# Example, with URL pointing directly to the log file
34+
$ ./demystifier https://gcsweb-ci.apps.ci.l2s4.p1.openshiftapps.com/gcs/test-platform-results/pr-logs/pull/openshift_oadp-operator/1266/pull-ci-openshift-oadp-operator-master-4.13-e2e-test-azure/1767186600720076800/artifacts/e2e-test-azure/e2e/build-log.txt
35+
```
36+
37+
#### Gather logs from the PROW job run and store them in a local folder
38+
39+
```sh
40+
$ ./demystifier -f OUTPUT_LOGS_DIR "${URL}"
41+
42+
# Example, with URL from the GitHub PR comment, to dump the logs into /tmp/logs_dir folder
43+
$ ./demystifier -f /tmp/logs_dir https://prow.ci.openshift.org/view/gs/test-platform-results/pr-logs/pull/openshift_oadp-operator/1266/pull-ci-openshift-oadp-operator-master-4.13-e2e-test-azure/1767186600720076800
44+
```
45+
46+
### Tests
47+
48+
To run unit tests, run
49+
```sh
50+
$ make test
51+
```
52+
53+
## License
54+
55+
Copyright 2024.
56+
57+
Licensed under the Apache License, Version 2.0 (the "License");
58+
you may not use this file except in compliance with the License.
59+
You may obtain a copy of the License at
60+
61+
http://www.apache.org/licenses/LICENSE-2.0
62+
63+
Unless required by applicable law or agreed to in writing, software
64+
distributed under the License is distributed on an "AS IS" BASIS,
65+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
66+
See the License for the specific language governing permissions and
67+
limitations under the License.

‎cmd/main/demystifier.go

+225
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
/*
2+
Copyright 2024.
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+
// Demystifier app
18+
package main
19+
20+
import (
21+
"flag"
22+
"fmt"
23+
"os"
24+
"sort"
25+
"time"
26+
27+
"github.com/migtools/demystifier/internal/utils"
28+
log "github.com/sirupsen/logrus"
29+
)
30+
31+
const saveFolderPerm = 0750
32+
33+
func parseLogFile(logFile string) (*utils.TestRunData, error) {
34+
testRunDataPtr, err := utils.GetRunDataFromLog(logFile)
35+
36+
if err != nil {
37+
log.WithFields(log.Fields{
38+
"error": err,
39+
}).Fatal("Error")
40+
return nil, err
41+
}
42+
43+
err = utils.SetIndividualTestsFromLog(testRunDataPtr, "It")
44+
45+
if err != nil {
46+
log.WithFields(log.Fields{
47+
"error": err,
48+
}).Fatal("Error")
49+
return nil, err
50+
}
51+
52+
return testRunDataPtr, nil
53+
}
54+
55+
// DumpTestsToFolder saves logs to a destination folder
56+
func DumpTestsToFolder(testData *utils.TestRunData, folder string) {
57+
mkdirErr := os.MkdirAll(folder, saveFolderPerm)
58+
if mkdirErr != nil {
59+
log.WithFields(log.Fields{
60+
"error": mkdirErr,
61+
}).Fatal("Error creating dir")
62+
}
63+
for i := range testData.TestRun {
64+
thisRun := &testData.TestRun[i]
65+
for j := range thisRun.Attempt {
66+
thisAttempt := &thisRun.Attempt[j]
67+
err := thisAttempt.DumpLogsToFileWithPrefixes(j, folder, thisAttempt.Name, ": ")
68+
if err != nil {
69+
log.WithFields(log.Fields{
70+
"error": err,
71+
}).Fatal("Error dumping logs to file")
72+
}
73+
}
74+
}
75+
}
76+
77+
// PrintTestSummary prints the summary of tests
78+
func PrintTestSummary(testData *utils.TestRunData) {
79+
// Define a struct to hold the summary data
80+
type TestSummary struct {
81+
Name string
82+
NumAttempts int
83+
NumFailed int
84+
TotalRunTime time.Duration
85+
NumOver1Second int
86+
AverageRunTime time.Duration
87+
}
88+
89+
// Initialize a slice to hold the summary data for each test run
90+
var summaries []TestSummary
91+
92+
// Loop through each test run to collect summary data
93+
for i := range testData.TestRun {
94+
var numAttempts, failedAttempts, numOver1Second int
95+
totalRunTime := time.Duration(0)
96+
thisTest := &testData.TestRun[i]
97+
for j := range thisTest.Attempt {
98+
// Increment the number of attempts
99+
numAttempts++
100+
thisAttempt := &thisTest.Attempt[j]
101+
// If the attempt failed, increment the failed attempts counter
102+
if thisAttempt.Status.Status == "FAILED" {
103+
failedAttempts++
104+
}
105+
106+
// If the duration is greater than 1 second, increment the counter
107+
if thisAttempt.Duration > time.Second {
108+
numOver1Second++
109+
}
110+
111+
// Add the duration to the total run time
112+
totalRunTime += thisAttempt.Duration
113+
}
114+
115+
// Calculate the average run time based on durations over 1 second
116+
var averageRunTime time.Duration
117+
if numOver1Second > 0 {
118+
averageRunTime = totalRunTime / time.Duration(numOver1Second)
119+
}
120+
121+
// Append the summary data to the slice
122+
summaries = append(summaries, TestSummary{
123+
Name: thisTest.ShortName,
124+
NumAttempts: numAttempts,
125+
NumFailed: failedAttempts,
126+
TotalRunTime: totalRunTime,
127+
NumOver1Second: numOver1Second,
128+
AverageRunTime: averageRunTime,
129+
})
130+
}
131+
132+
// Sort by avg time
133+
sort.Slice(summaries, func(i, j int) bool {
134+
return summaries[i].AverageRunTime < summaries[j].AverageRunTime
135+
})
136+
137+
// Print the summary table
138+
fmt.Println("Test Summary Table:")
139+
headerStr := "---------------------------------------------------------------------------------------------------"
140+
fmt.Println(headerStr)
141+
fmt.Printf("| %-40s | %-15s | %-11s | %-20s |\n", "Test Name", "Num Attempts", "Num Failed", "Average Run Time")
142+
fmt.Println(headerStr)
143+
for _, summary := range summaries {
144+
fmt.Printf("| %-40s | %-15d | %-11d | %-20s |\n", summary.Name, summary.NumAttempts, summary.NumFailed, summary.AverageRunTime)
145+
}
146+
fmt.Println(headerStr)
147+
}
148+
149+
func main() {
150+
log.SetLevel(log.InfoLevel)
151+
152+
log.WithFields(log.Fields{
153+
">>> start_demystifier_timestamp": time.Now().Unix(),
154+
}).Info("Test Demystifier starts its journey")
155+
156+
var (
157+
logLocation string
158+
showPassing bool
159+
timeStamps bool
160+
debugMode bool
161+
dumpLogsToFolder string
162+
)
163+
164+
flag.BoolVar(&timeStamps, "t", false, "whether to include timestamps in the output (shorthand)")
165+
flag.BoolVar(&showPassing, "s", false, "show all tests even those passing")
166+
flag.BoolVar(&debugMode, "d", false, "debug mode")
167+
flag.StringVar(&dumpLogsToFolder, "f", "", "dump logs to folder")
168+
169+
flag.Parse()
170+
171+
if debugMode {
172+
log.SetLevel(log.DebugLevel)
173+
}
174+
175+
if len(flag.Args()) > 0 {
176+
logLocation = utils.GeneratesLogURL(flag.Arg(0))
177+
}
178+
179+
log.WithFields(log.Fields{
180+
">>> location": logLocation,
181+
}).Info("Using log from")
182+
183+
testData, _ := parseLogFile(logLocation)
184+
185+
for i := range testData.TestRun {
186+
failedAttempts := 0 // Initialize counter for failed attempts in this test run
187+
thisTest := &testData.TestRun[i]
188+
for j := range thisTest.Attempt {
189+
thisAttempt := &thisTest.Attempt[j]
190+
fields := log.Fields{
191+
"Name": thisTest.ShortName,
192+
"No": thisAttempt.AttemptNo,
193+
"Time": thisAttempt.Duration,
194+
}
195+
196+
// If the attempt failed or showPassing is true, log the attempt
197+
if thisAttempt.Status.Status == utils.Failed {
198+
log.WithFields(fields).Error("Failed attempt run")
199+
// Increment the counter if the attempt failed
200+
if thisAttempt.Status.Status == utils.Failed {
201+
failedAttempts++
202+
}
203+
} else if showPassing {
204+
log.WithFields(fields).Info("Pass attempt run")
205+
}
206+
}
207+
208+
// Summary for this test run
209+
if failedAttempts > 0 {
210+
log.WithFields(log.Fields{
211+
"Name": thisTest.Name,
212+
"Failed": failedAttempts,
213+
}).Info("Test Summary")
214+
}
215+
}
216+
if dumpLogsToFolder != "" {
217+
DumpTestsToFolder(testData, dumpLogsToFolder)
218+
os.Exit(0)
219+
}
220+
PrintTestSummary(testData)
221+
222+
log.WithFields(log.Fields{
223+
">>> end_demystifier_timestamp": time.Now().Unix(),
224+
}).Info("Test Demystifier finishes its journey")
225+
}

‎cmd/main/test_summary_test.go

+109
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/*
2+
Copyright 2024.
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 main
18+
19+
import (
20+
"bytes"
21+
"io"
22+
"os"
23+
"testing"
24+
)
25+
26+
const logFile = "../../tests/testdata/build-log.txt"
27+
28+
func TestPrintTestSummary(t *testing.T) {
29+
type args struct {
30+
logFile string
31+
}
32+
tests := []struct {
33+
name string
34+
args args
35+
want string
36+
}{
37+
{
38+
name: "Test with build-log.txt",
39+
args: args{
40+
logFile: logFile,
41+
},
42+
want: `Test Summary Table:
43+
---------------------------------------------------------------------------------------------------
44+
| Test Name | Num Attempts | Num Failed | Average Run Time |
45+
---------------------------------------------------------------------------------------------------
46+
| Should succeed | 1 | 0 | 5.044s |
47+
| AWS Without Region And S3ForcePathStyle true should fail | 1 | 0 | 20.036s |
48+
| Should succeed | 1 | 0 | 20.071s |
49+
| HTTP_PROXY set | 1 | 0 | 35.243s |
50+
| NO_PROXY set | 1 | 0 | 35.291s |
51+
| unsupportedOverrides should succeed | 1 | 0 | 1m20.133s |
52+
| Adding CSI plugin | 1 | 0 | 1m20.133s |
53+
| Provider plugin | 1 | 0 | 1m20.136s |
54+
| AWS With Region And S3ForcePathStyle should succeed | 1 | 0 | 1m20.138s |
55+
| Adding Velero custom plugin | 1 | 0 | 1m20.139s |
56+
| Set restic node selector | 1 | 0 | 1m20.14s |
57+
| AWS Without Region No S3ForcePathStyle with BackupImages false should succeed | 1 | 0 | 1m20.141s |
58+
| NoDefaultBackupLocation | 1 | 0 | 1m20.141s |
59+
| Default velero CR, test carriage return | 1 | 0 | 1m20.141s |
60+
| Default velero CR | 1 | 0 | 1m20.142s |
61+
| DPA CR with bsl and vsl | 1 | 0 | 1m20.143s |
62+
| Enable tolerations | 1 | 0 | 1m20.148s |
63+
| Adding Velero resource allocations | 1 | 0 | 1m20.153s |
64+
| Default velero CR with restic disabled | 1 | 0 | 1m20.172s |
65+
| HTTPS_PROXY set | 1 | 0 | 2m5.099s |
66+
| Mongo application KOPIA | 1 | 0 | 2m31.823s |
67+
| MySQL application KOPIA | 1 | 0 | 2m36.65s |
68+
| MySQL application RESTIC | 1 | 0 | 2m46.649s |
69+
| Mongo application RESTIC | 1 | 0 | 2m51.694s |
70+
| MySQL application CSI | 2 | 1 | 3m8.943s |
71+
| Config unset | 1 | 0 | 3m31.199s |
72+
| Mongo application CSI | 1 | 0 | 3m36.749s |
73+
| MySQL application DATAMOVER | 1 | 0 | 4m16.949s |
74+
| Mongo application DATAMOVER | 1 | 0 | 4m17.239s |
75+
| Mongo application DATAMOVER | 1 | 0 | 4m36.933s |
76+
| Mongo application BlockDevice DATAMOVER | 1 | 0 | 5m6.999s |
77+
| MySQL application two Vol CSI | 3 | 3 | 6m16.036s |
78+
---------------------------------------------------------------------------------------------------
79+
`,
80+
},
81+
}
82+
for _, tt := range tests {
83+
t.Run(tt.name, func(t *testing.T) {
84+
testData, err := parseLogFile(tt.args.logFile)
85+
if err != nil {
86+
t.Errorf("Error parsing log file: %v", err)
87+
}
88+
old := os.Stdout // keep backup of the real stdout
89+
r, w, _ := os.Pipe()
90+
os.Stdout = w
91+
92+
PrintTestSummary(testData)
93+
94+
outC := make(chan string)
95+
// copy the output in a separate goroutine so printing can't block indefinitely
96+
go func() {
97+
var buf bytes.Buffer
98+
io.Copy(&buf, r)
99+
outC <- buf.String()
100+
}()
101+
w.Close()
102+
os.Stdout = old
103+
outString := <-outC
104+
if outString != tt.want {
105+
t.Errorf("PrintTestSummary() = %v, want %v", outString, tt.want)
106+
}
107+
})
108+
}
109+
}

‎go.mod

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
module github.com/migtools/demystifier
2+
3+
go 1.21.4
4+
5+
require github.com/sirupsen/logrus v1.9.3
6+
7+
require golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect

‎go.sum

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
2+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
3+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
4+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
5+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
6+
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
7+
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
8+
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
9+
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
10+
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
11+
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
12+
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
13+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
14+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
15+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

‎internal/utils/logs.go

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
Copyright 2024.
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 utils for the demystifier CLI
18+
package utils
19+
20+
import (
21+
"fmt"
22+
"os"
23+
"strings"
24+
)
25+
26+
// DumpLogsToFileWithPrefixes saves logs as files
27+
func (a *AttemptData) DumpLogsToFileWithPrefixes(attemptNo int, folder string, prefixes ...string) error {
28+
// replace / in name
29+
fileName := strings.ReplaceAll(a.Name, "/", "_")
30+
filename := fmt.Sprintf("%s/%s_%d.log", folder, fileName, attemptNo)
31+
32+
file, err := os.Create(filename)
33+
34+
if err != nil {
35+
return fmt.Errorf("error creating log file: %v", err)
36+
}
37+
defer file.Close()
38+
for i := range a.Logs {
39+
for j := range prefixes {
40+
if _, err := file.WriteString(prefixes[j]); err != nil {
41+
return err
42+
}
43+
}
44+
if _, err := file.WriteString(a.Logs[i] + "\n"); err != nil {
45+
return err
46+
}
47+
}
48+
return nil
49+
}

‎internal/utils/test_result_helpers.go

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*
2+
Copyright 2024.
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 utils
18+
19+
import "time"
20+
21+
const (
22+
Failed = "FAILED"
23+
Passed = "PASSED"
24+
Timeout = "TIMEOUT"
25+
)
26+
27+
type EventStatus struct {
28+
Status string
29+
}
30+
31+
func (s *EventStatus) SetFailed() {
32+
s.Status = Failed
33+
}
34+
func (s *EventStatus) SetPassing() {
35+
s.Status = Passed
36+
}
37+
func (s *EventStatus) SetTimeout() {
38+
s.Status = Timeout
39+
}
40+
41+
// Event is for example Backup or Restore
42+
type EventData struct {
43+
Name string
44+
StartTime time.Time
45+
EndTime time.Time
46+
Duration time.Duration
47+
Status EventStatus
48+
Logs []string
49+
}
50+
51+
// Attempt is for a single Test run that may include
52+
// multiple Events
53+
type AttemptData struct {
54+
AttemptNo int
55+
Name string
56+
StartTime time.Time
57+
EndTime time.Time
58+
Duration time.Duration
59+
Status EventStatus // Don't yet know if it is better to be here or in the EventData
60+
Logs []string
61+
Events []EventData
62+
}
63+
64+
// IndividualTestRunData may consists of many attempts, each attempt
65+
// is run of the same test, but may lead to different
66+
// results or failures
67+
type IndividualTestRunData struct {
68+
Name string
69+
ShortName string
70+
Attempt []AttemptData
71+
}
72+
73+
// This is representation of full run, it may not have tests itself
74+
// but w want to store full log
75+
type TestRunData struct {
76+
FullLogs string
77+
TestRun []IndividualTestRunData
78+
}

‎internal/utils/test_utils.go

+259
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
/*
2+
Copyright 2024.
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 utils
18+
19+
import (
20+
"bufio"
21+
"bytes"
22+
"errors"
23+
"fmt"
24+
"io"
25+
"net/http"
26+
"os"
27+
"regexp"
28+
"strings"
29+
"time"
30+
31+
log "github.com/sirupsen/logrus"
32+
)
33+
34+
// GetRunDataFromLog
35+
// parameters:
36+
// - logFile string, the location of the log file, local or remote (prefixes: http:// or https://)
37+
// returns:
38+
// - *TestRunData, a pointer to TestRunData struct representing the test run data to be updated.
39+
func GetRunDataFromLog(logFile string) (*TestRunData, error) {
40+
var testRunData TestRunData
41+
42+
var data []byte
43+
44+
if strings.HasPrefix(logFile, "http://") || strings.HasPrefix(logFile, "https://") {
45+
log.WithFields(log.Fields{
46+
"log location": logFile,
47+
}).Debug("Using log from URL")
48+
resp, err := http.Get(logFile)
49+
if err != nil {
50+
return nil, fmt.Errorf("error opening URL: %v", err)
51+
}
52+
defer resp.Body.Close()
53+
54+
data, err = io.ReadAll(resp.Body)
55+
if err != nil {
56+
return nil, fmt.Errorf("error reading HTTP response body: %v", err)
57+
}
58+
} else {
59+
log.WithFields(log.Fields{
60+
"log location": logFile,
61+
}).Debug("Using log from file")
62+
file, err := os.Open(logFile)
63+
if err != nil {
64+
return nil, fmt.Errorf("errosr opening file: %v", err)
65+
}
66+
defer file.Close()
67+
68+
data, err = io.ReadAll(file)
69+
if err != nil {
70+
return nil, fmt.Errorf("error reading file: %v", err)
71+
}
72+
}
73+
74+
scanner := bufio.NewScanner(bytes.NewReader(data)) // Create scanner from data
75+
76+
var fullLogs strings.Builder
77+
for scanner.Scan() {
78+
fullLogs.WriteString(scanner.Text() + "\n")
79+
}
80+
81+
if err := scanner.Err(); err != nil {
82+
return nil, err
83+
}
84+
85+
testRunData.FullLogs = fullLogs.String()
86+
87+
return &testRunData, nil
88+
}
89+
90+
// GenerateLogURL generates a URL for the log file.
91+
// This function may be replaced with your actual URL generation logic.
92+
func GeneratesLogURL(originalURL string) string {
93+
// Check if the original URL already points to build-log.txt
94+
if strings.HasSuffix(originalURL, "/build-log.txt") {
95+
return originalURL
96+
}
97+
parts := strings.Split(originalURL, "https://prow.ci.openshift.org/view/gs/")
98+
99+
re := regexp.MustCompile(`e2e-test-(.*?)/`)
100+
testType := re.FindString(originalURL)
101+
logURL := fmt.Sprintf("https://gcsweb-ci.apps.ci.l2s4.p1.openshiftapps.com/gcs/%s/artifacts/%se2e/build-log.txt", parts[1], testType)
102+
return logURL
103+
}
104+
105+
// SetIndividualTestsFromLog processes the log data and updates the test run data accordingly.
106+
// It sets individual test runs and their corresponding attempts based on the provided log data.
107+
// If the provided testRunData is nil or if the logs are empty, it returns an error.
108+
// The anchorTag parameter specifies the tag used to identify the start and end of individual tests.
109+
//
110+
// Parameters:
111+
// - testRunData: A pointer to TestRunData struct representing the test run data to be updated.
112+
// - anchorTag: A string indicating the tag used to identify the start and end of individual tests.
113+
//
114+
// Returns:
115+
// - An error if any issue occurs during processing, or nil if the processing is successful.
116+
func SetIndividualTestsFromLog(testRunData *TestRunData, anchorTag string) error {
117+
if testRunData == nil {
118+
return errors.New("testRunData is nil")
119+
}
120+
121+
if testRunData.FullLogs == "" {
122+
return errors.New("logs were not provided")
123+
}
124+
125+
attempts := make(map[string]int)
126+
127+
lines := strings.Split(testRunData.FullLogs, "\n")
128+
startRegex := regexp.MustCompile(fmt.Sprintf(`> Enter \[%s\] (.+) - (.+) @ (.+)`, anchorTag))
129+
endRegex := regexp.MustCompile(fmt.Sprintf(`< Exit \[%s\] (.+?) - .+ @ (.+) \(.+\)`, anchorTag))
130+
failureRegex := regexp.MustCompile(`^[\t ]*\[FAILED\].*`)
131+
132+
var currentAttempt *AttemptData
133+
134+
for _, line := range lines {
135+
if matches := startRegex.FindStringSubmatch(line); matches != nil {
136+
currentAttempt = handleStartTag(line, matches, attempts, testRunData)
137+
} else if matches := endRegex.FindStringSubmatch(line); matches != nil {
138+
handleEndTag(line, matches, currentAttempt)
139+
} else if matches := failureRegex.FindStringSubmatch(line); matches != nil {
140+
log.WithFields(log.Fields{
141+
"Line": currentAttempt.Name,
142+
"Attempt no": currentAttempt.AttemptNo,
143+
}).Debug("Marking attempt FAILED")
144+
currentAttempt.Status = EventStatus{Status: Failed}
145+
} else if currentAttempt != nil {
146+
handleLogs(line, currentAttempt)
147+
}
148+
}
149+
150+
return nil
151+
}
152+
153+
// handleStartTag add a new attempt data to a test run and returns current attempt
154+
func handleStartTag(line string, matches []string, attempts map[string]int, testRunsPtr *TestRunData) *AttemptData {
155+
eventName := matches[2]
156+
shortEventName := matches[1]
157+
log.WithFields(log.Fields{
158+
"Line": line,
159+
"Attempt no": attempts[eventName],
160+
}).Debug("Found new Attempt")
161+
162+
currentTestRunPtr := getOrAddTestRun(testRunsPtr, eventName, shortEventName)
163+
164+
// Create a new instance of AttemptData
165+
currentTestRunPtr.Attempt = append(currentTestRunPtr.Attempt, AttemptData{
166+
AttemptNo: attempts[eventName],
167+
Name: eventName,
168+
})
169+
newAttempt := &currentTestRunPtr.Attempt[len(currentTestRunPtr.Attempt)-1]
170+
171+
attempts[eventName]++
172+
173+
// Add logs to the new attempt
174+
newAttempt.Logs = append(newAttempt.Logs, line)
175+
176+
// Parse and set the start time for the new attempt
177+
parsedTime, err := parseGingkoTime(matches[3])
178+
if err != nil {
179+
log.Error("Error parsing time:", err)
180+
return newAttempt
181+
}
182+
newAttempt.StartTime = parsedTime
183+
184+
log.WithFields(log.Fields{
185+
"Test": currentTestRunPtr.ShortName,
186+
"Attempt no": newAttempt.AttemptNo,
187+
"Attempt name": newAttempt.Name,
188+
"Start Time": newAttempt.StartTime,
189+
}).Debug("Created New Attempt")
190+
return newAttempt
191+
}
192+
193+
func getOrAddTestRun(testRunsPtr *TestRunData, eventName string, shortEventName string) *IndividualTestRunData {
194+
// Ensure that the TestRun slice is initialized
195+
if testRunsPtr.TestRun == nil {
196+
testRunsPtr.TestRun = []IndividualTestRunData{}
197+
}
198+
199+
// Iterate through existing IndividualTestRunData instances
200+
for i := range testRunsPtr.TestRun {
201+
if testRunsPtr.TestRun[i].Name == eventName {
202+
// If an IndividualTestRunData with the same eventName exists, return a pointer to it
203+
return &testRunsPtr.TestRun[i]
204+
}
205+
}
206+
207+
// If no matching IndividualTestRunData was found, create a new one
208+
// Append it to the TestRun slice
209+
testRunsPtr.TestRun = append(testRunsPtr.TestRun, IndividualTestRunData{Name: eventName, ShortName: shortEventName})
210+
// Return a pointer to the newly created IndividualTestRunData
211+
return &testRunsPtr.TestRun[len(testRunsPtr.TestRun)-1]
212+
}
213+
214+
func handleEndTag(line string, matches []string, currentAttempt *AttemptData) {
215+
if currentAttempt != nil {
216+
log.WithFields(log.Fields{
217+
"Line": line,
218+
"Attempt no": currentAttempt.AttemptNo,
219+
}).Debug("Found end Attempt")
220+
endTime, err := parseGingkoTime(matches[2])
221+
if err != nil {
222+
log.Error("Error parsing end time:", err)
223+
return
224+
}
225+
currentAttempt.EndTime = endTime
226+
currentAttempt.Duration = endTime.Sub(currentAttempt.StartTime)
227+
log.WithFields(log.Fields{
228+
"StartTime": currentAttempt.StartTime,
229+
"EndTime": currentAttempt.EndTime,
230+
"Duration": currentAttempt.Duration,
231+
}).Debug("Attempt times")
232+
currentAttempt.Logs = append(currentAttempt.Logs, line)
233+
}
234+
}
235+
236+
func handleLogs(line string, currentAttempt *AttemptData) {
237+
currentAttempt.Logs = append(currentAttempt.Logs, line)
238+
}
239+
240+
func parseGingkoTime(timeStr string) (time.Time, error) {
241+
formats := []string{
242+
"01/02/06 15:04:05.000",
243+
"01/02/06 15:04:05.00",
244+
"01/02/06 15:04:05.0",
245+
"01/02/06 15:04:05",
246+
}
247+
var parsedTime time.Time
248+
var err error
249+
for _, format := range formats {
250+
parsedTime, err = time.Parse(format, timeStr)
251+
if err == nil {
252+
break
253+
}
254+
}
255+
if err != nil {
256+
return time.Time{}, err
257+
}
258+
return parsedTime, nil
259+
}

‎internal/utils/test_utils_test.go

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
Copyright 2024.
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 utils
18+
19+
import "testing"
20+
21+
func TestGenerateLogsURL(t *testing.T) {
22+
type args struct {
23+
originalURL string
24+
}
25+
tests := []struct {
26+
name string
27+
args args
28+
want string
29+
}{
30+
{
31+
name: "Test with pull-ci-openshift-oadp-operator-master-4.12-e2e-test-azure",
32+
args: args{
33+
originalURL: "https://prow.ci.openshift.org/view/gs/test-platform-results/pr-logs/pull/openshift_oadp-operator/1330/pull-ci-openshift-oadp-operator-master-4.12-e2e-test-azure/1757841602983759872",
34+
},
35+
want: "https://gcsweb-ci.apps.ci.l2s4.p1.openshiftapps.com/gcs/test-platform-results/pr-logs/pull/openshift_oadp-operator/1330/pull-ci-openshift-oadp-operator-master-4.12-e2e-test-azure/1757841602983759872/artifacts/e2e-test-azure/e2e/build-log.txt",
36+
},
37+
{
38+
name: "Test with pull-ci-openshift-oadp-operator-master-4.14-e2e-test-aws",
39+
args: args{
40+
originalURL: "https://prow.ci.openshift.org/view/gs/test-platform-results/pr-logs/pull/openshift_oadp-operator/1330/pull-ci-openshift-oadp-operator-master-4.14-e2e-test-aws/1757841603164114944",
41+
},
42+
want: "https://gcsweb-ci.apps.ci.l2s4.p1.openshiftapps.com/gcs/test-platform-results/pr-logs/pull/openshift_oadp-operator/1330/pull-ci-openshift-oadp-operator-master-4.14-e2e-test-aws/1757841603164114944/artifacts/e2e-test-aws/e2e/build-log.txt",
43+
},
44+
}
45+
for _, tt := range tests {
46+
t.Run(tt.name, func(t *testing.T) {
47+
if got := GeneratesLogURL(tt.args.originalURL); got != tt.want {
48+
t.Errorf("GeneratesLogURL() = %v, want %v", got, tt.want)
49+
}
50+
})
51+
}
52+
}

‎tests/testdata/build-log.txt

+5,936
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)
Please sign in to comment.