commit
0a088cc08b
2
.github/workflows/ut_check.yml
vendored
2
.github/workflows/ut_check.yml
vendored
@ -26,5 +26,5 @@ jobs:
|
|||||||
- name: Go test
|
- name: Go test
|
||||||
run: |
|
run: |
|
||||||
export DEFAULT_EXCEPT_PKGS=e2e
|
export DEFAULT_EXCEPT_PKGS=e2e
|
||||||
go test -p 1 -race -coverprofile=coverage.txt $(go list ./... | grep -v -E $DEFAULT_EXCEPT_PKGS)
|
go test -p 1 -coverprofile=coverage.txt $(go list ./... | grep -v -E $DEFAULT_EXCEPT_PKGS)
|
||||||
bash <(curl -s https://codecov.io/bash)
|
bash <(curl -s https://codecov.io/bash)
|
||||||
|
209
cmd/diff.go
Normal file
209
cmd/diff.go
Normal file
@ -0,0 +1,209 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2020 Qiniu Cloud (qiniu.com)
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/olekukonko/tablewriter"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"github.com/qiniu/goc/pkg/cover"
|
||||||
|
"github.com/qiniu/goc/pkg/github"
|
||||||
|
"github.com/qiniu/goc/pkg/prow"
|
||||||
|
"github.com/qiniu/goc/pkg/qiniu"
|
||||||
|
)
|
||||||
|
|
||||||
|
var diffCmd = &cobra.Command{
|
||||||
|
Use: "diff",
|
||||||
|
Short: "do coverage profile diff analysis, it can also work with prow and post comments to github pull request if needed",
|
||||||
|
Example: ` # Diff two local coverage profile and display
|
||||||
|
goc diff --new-profile=<xxxx> --base-profile=<xxxx>
|
||||||
|
|
||||||
|
# Diff local coverage profile with the remote one in prow job using default qiniu-credential
|
||||||
|
goc diff --prow-postsubmit-job=<xxx> --new-profile=<xxx>
|
||||||
|
|
||||||
|
# Calculate and display full diff coverage between new-profile and base-profile, not concerned github changed files
|
||||||
|
goc diff --prow-postsubmit-job=<xxx> --new-profile=<xxx> --full-diff=true
|
||||||
|
|
||||||
|
# Diff local coverage profile with the remote one in prow job
|
||||||
|
goc diff --prow-postsubmit-job=<xxx> --prow-remote-profile-name=<xxx>
|
||||||
|
--qiniu-credential=<xxx> --new-profile=<xxxx>
|
||||||
|
|
||||||
|
# Diff coverage profile with the remote one in prow job, and post comments to github PR
|
||||||
|
goc diff --prow-postsubmit-job=<xxx> --prow-profile=<xxx>
|
||||||
|
--github-token=<xxx> --github-user=<xxx> --github-comment-prefix=<xxx>
|
||||||
|
--qiniu-credential=<xxx> --coverage-threshold-percentage=<xxx> --new-profile=<xxxx>
|
||||||
|
`,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
if baseProfile != "" {
|
||||||
|
doDiffForLocalProfiles(cmd, args)
|
||||||
|
} else if prowPostSubmitJob != "" {
|
||||||
|
doDiffUnderProw(cmd, args)
|
||||||
|
} else {
|
||||||
|
logrus.Fatalf("either base-profile or prow-postsubmit-job must be provided")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
newProfile string
|
||||||
|
baseProfile string
|
||||||
|
coverageThreshold int
|
||||||
|
|
||||||
|
prowPostSubmitJob string
|
||||||
|
prowProfile string
|
||||||
|
|
||||||
|
githubToken string
|
||||||
|
githubUser string
|
||||||
|
githubCommentPrefix string
|
||||||
|
|
||||||
|
qiniuCredential string
|
||||||
|
|
||||||
|
robotName string
|
||||||
|
fullDiff bool
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
diffCmd.Flags().StringVarP(&newProfile, "new-profile", "n", "", "local profile which works as the target to analysis")
|
||||||
|
diffCmd.MarkFlagRequired("new-profile")
|
||||||
|
diffCmd.Flags().StringVarP(&baseProfile, "base-profile", "b", "", "another local profile which works as baseline to compare with the target")
|
||||||
|
diffCmd.Flags().IntVarP(&coverageThreshold, "coverage-threshold-percentage", "", 0, "coverage threshold percentage")
|
||||||
|
diffCmd.Flags().StringVarP(&prowPostSubmitJob, "prow-postsubmit-job", "", "", "prow postsubmit job which used to find the base profile")
|
||||||
|
diffCmd.Flags().StringVarP(&prowProfile, "prow-remote-profile-name", "", "filtered.cov", "the name of profile in prow postsubmit job, which used as the base profile to compare")
|
||||||
|
diffCmd.Flags().StringVarP(&githubToken, "github-token", "", "/etc/github/oauth", "path to token to access github repo")
|
||||||
|
diffCmd.Flags().StringVarP(&githubUser, "github-user", "", "", "github user name when comments in github")
|
||||||
|
diffCmd.Flags().StringVarP(&githubCommentPrefix, "github-comment-prefix", "", "", "specific comment flag you provided")
|
||||||
|
diffCmd.Flags().StringVarP(&qiniuCredential, "qiniu-credential", "", "/etc/qiniuconfig/qiniu.json", "path to credential file to access qiniu cloud")
|
||||||
|
diffCmd.Flags().StringVarP(&robotName, "robot-name", "", "qiniu-bot", "github user name for coverage robot")
|
||||||
|
diffCmd.Flags().BoolVarP(&fullDiff, "full-diff", "", false, "when set true,calculate and display full diff coverage between new-profile and base-profile")
|
||||||
|
|
||||||
|
rootCmd.AddCommand(diffCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
//goc diff --new-profile=./new.cov --base-profile=./base.cov
|
||||||
|
//+------------------------------------------------------+---------------+--------------+--------+
|
||||||
|
//| File | Base Coverage | New Coverage | Delta |
|
||||||
|
//+------------------------------------------------------+---------------+--------------+--------+
|
||||||
|
//| qiniu.com/kodo/bd/pfd/pfdstg/cursor/mgr.go | 53.5% | 50.5% | -3.0% |
|
||||||
|
//| qiniu.com/kodo/bd/pfd/pfdstg/svr/getstripe.go | 0.5% | 0.0% | -0.5% |
|
||||||
|
//| Total | 35.7% | 35.7% | -0.0% |
|
||||||
|
//+------------------------------------------------------+---------------+--------------+--------+
|
||||||
|
func doDiffForLocalProfiles(cmd *cobra.Command, args []string) {
|
||||||
|
localP, err := cover.ReadFileToCoverList(newProfile)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
baseP, err := cover.ReadFileToCoverList(baseProfile)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
//calculate diff file cov and display
|
||||||
|
rows := cover.GetDeltaCov(localP, baseP)
|
||||||
|
rows.Sort()
|
||||||
|
table := tablewriter.NewWriter(os.Stdout)
|
||||||
|
table.SetHeader([]string{"File", "Base Coverage", "New Coverage", "Delta"})
|
||||||
|
table.SetAutoFormatHeaders(false)
|
||||||
|
table.SetColumnAlignment([]int{tablewriter.ALIGN_LEFT, tablewriter.ALIGN_CENTER, tablewriter.ALIGN_CENTER, tablewriter.ALIGN_CENTER})
|
||||||
|
for _, row := range rows {
|
||||||
|
table.Append([]string{row.FileName, row.BasePer, row.NewPer, row.DeltaPer})
|
||||||
|
}
|
||||||
|
totalDelta := cover.PercentStr(cover.TotalDelta(localP, baseP))
|
||||||
|
table.Append([]string{"Total", baseP.TotalPercentage(), localP.TotalPercentage(), totalDelta})
|
||||||
|
table.Render()
|
||||||
|
}
|
||||||
|
|
||||||
|
func doDiffUnderProw(cmd *cobra.Command, args []string) {
|
||||||
|
var (
|
||||||
|
prNumStr = os.Getenv("PULL_NUMBER")
|
||||||
|
pullSha = os.Getenv("PULL_PULL_SHA")
|
||||||
|
baseSha = os.Getenv("PULL_BASE_SHA")
|
||||||
|
repoOwner = os.Getenv("REPO_OWNER")
|
||||||
|
repoName = os.Getenv("REPO_NAME")
|
||||||
|
jobType = os.Getenv("JOB_TYPE")
|
||||||
|
jobName = os.Getenv("JOB_NAME")
|
||||||
|
buildStr = os.Getenv("BUILD_NUMBER")
|
||||||
|
artifacts = os.Getenv("ARTIFACTS")
|
||||||
|
)
|
||||||
|
logrus.Printf("Running coverage for PR = %s; PR commit SHA = %s;base SHA = %s", prNumStr, pullSha, baseSha)
|
||||||
|
|
||||||
|
switch jobType {
|
||||||
|
case "periodic":
|
||||||
|
logrus.Printf("job type %s, do nothing", jobType)
|
||||||
|
case "postsubmit":
|
||||||
|
logrus.Printf("job type %s, do nothing", jobType)
|
||||||
|
case "presubmit":
|
||||||
|
if githubToken == "" {
|
||||||
|
logrus.Fatalf("github token not provided")
|
||||||
|
}
|
||||||
|
prClient := github.NewPrClient(githubToken, repoOwner, repoName, prNumStr, robotName, githubCommentPrefix)
|
||||||
|
|
||||||
|
if qiniuCredential == "" {
|
||||||
|
logrus.Fatalf("qiniu credential not provided")
|
||||||
|
}
|
||||||
|
var qc *qiniu.Client
|
||||||
|
var conf qiniu.Config
|
||||||
|
files, err := ioutil.ReadFile(*&qiniuCredential)
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Fatal("Error reading qiniu config file")
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(files, &conf); err != nil {
|
||||||
|
logrus.Fatal("Error unmarshal qiniu config file")
|
||||||
|
}
|
||||||
|
if conf.Bucket == "" {
|
||||||
|
logrus.Fatal("no qiniu bucket provided")
|
||||||
|
}
|
||||||
|
if conf.AccessKey == "" || conf.SecretKey == "" {
|
||||||
|
logrus.Fatal("either qiniu access key or secret key was not provided")
|
||||||
|
}
|
||||||
|
if conf.Domain == "" {
|
||||||
|
logrus.Fatal("no qiniu bucket domain was provided")
|
||||||
|
}
|
||||||
|
qc = qiniu.NewClient(&conf)
|
||||||
|
|
||||||
|
localArtifacts := qiniu.Artifacts{
|
||||||
|
Directory: artifacts,
|
||||||
|
ProfileName: newProfile,
|
||||||
|
ChangedProfileName: qiniu.ChangedProfileName,
|
||||||
|
}
|
||||||
|
|
||||||
|
job := prow.Job{
|
||||||
|
JobName: jobName,
|
||||||
|
BuildId: buildStr,
|
||||||
|
Org: repoOwner,
|
||||||
|
RepoName: repoName,
|
||||||
|
PRNumStr: prNumStr,
|
||||||
|
PostSubmitJob: prowPostSubmitJob,
|
||||||
|
LocalProfilePath: newProfile,
|
||||||
|
PostSubmitCoverProfile: prowProfile,
|
||||||
|
QiniuClient: qc,
|
||||||
|
LocalArtifacts: &localArtifacts,
|
||||||
|
GithubComment: prClient,
|
||||||
|
FullDiff: fullDiff,
|
||||||
|
}
|
||||||
|
if err := job.RunPresubmit(); err != nil {
|
||||||
|
logrus.Fatalf("run presubmit job failed, err: %v", err)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
logrus.Printf("Unknown job type: %s, do nothing.", jobType)
|
||||||
|
}
|
||||||
|
}
|
110
cmd/diff_test.go
Normal file
110
cmd/diff_test.go
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2020 Qiniu Cloud (qiniu.com)
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
type diffFunc func(cmd *cobra.Command, args []string)
|
||||||
|
|
||||||
|
func captureStdout(f diffFunc, cmd *cobra.Command, args []string) string {
|
||||||
|
r, w, err := os.Pipe()
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Fatal("os pipe fail")
|
||||||
|
}
|
||||||
|
stdout := os.Stdout
|
||||||
|
os.Stdout = w
|
||||||
|
defer func() {
|
||||||
|
os.Stdout = stdout
|
||||||
|
}()
|
||||||
|
|
||||||
|
f(cmd, args)
|
||||||
|
w.Close()
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
io.Copy(&buf, r)
|
||||||
|
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDoDiffForLocalProfiles(t *testing.T) {
|
||||||
|
items := []struct {
|
||||||
|
newCovFile string
|
||||||
|
newProfile string
|
||||||
|
baseCovFile string
|
||||||
|
baseProfile string
|
||||||
|
expectOutput string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
newCovFile: "new.cov",
|
||||||
|
baseCovFile: "base.cov",
|
||||||
|
newProfile: "mode: atomic\n" +
|
||||||
|
"qiniu.com/kodo/apiserver/server/main.go:32.49,33.13 1 30\n" +
|
||||||
|
"qiniu.com/kodo/apiserver/server/main.go:42.49,43.13 1 1\n",
|
||||||
|
baseProfile: "mode: atomic\n" +
|
||||||
|
"qiniu.com/kodo/apiserver/server/main.go:32.49,33.13 1 30\n" +
|
||||||
|
"qiniu.com/kodo/apiserver/server/main.go:42.49,43.13 1 0\n",
|
||||||
|
expectOutput: `+-----------------------------------------+---------------+--------------+-------+
|
||||||
|
| File | Base Coverage | New Coverage | Delta |
|
||||||
|
+-----------------------------------------+---------------+--------------+-------+
|
||||||
|
| qiniu.com/kodo/apiserver/server/main.go | 50.0% | 100.0% | 50.0% |
|
||||||
|
| Total | 50.0% | 100.0% | 50.0% |
|
||||||
|
+-----------------------------------------+---------------+--------------+-------+
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range items {
|
||||||
|
err := ioutil.WriteFile(tc.newCovFile, []byte(tc.newProfile), 0644)
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Fatalf("write file %s failed", tc.newCovFile)
|
||||||
|
}
|
||||||
|
err = ioutil.WriteFile(tc.baseCovFile, []byte(tc.baseProfile), 0644)
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Fatalf("write file %s failed", tc.baseCovFile)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
os.Remove(tc.newCovFile)
|
||||||
|
os.Remove(tc.baseCovFile)
|
||||||
|
}()
|
||||||
|
|
||||||
|
pwd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Fatalf("get pwd failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
diffCmd.Flags().Set("new-profile", fmt.Sprintf("%s/%s", pwd, tc.newCovFile))
|
||||||
|
diffCmd.Flags().Set("base-profile", fmt.Sprintf("%s/%s", pwd, tc.baseCovFile))
|
||||||
|
out := captureStdout(doDiffForLocalProfiles, diffCmd, nil)
|
||||||
|
assert.Equal(t, out, tc.expectOutput)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDoDiffUnderProw(t *testing.T) {
|
||||||
|
|
||||||
|
}
|
12
go.mod
12
go.mod
@ -4,11 +4,21 @@ go 1.13
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/gin-gonic/gin v1.6.3
|
github.com/gin-gonic/gin v1.6.3
|
||||||
|
github.com/google/go-github v17.0.0+incompatible
|
||||||
|
github.com/julienschmidt/httprouter v1.2.0
|
||||||
|
github.com/magiconair/properties v1.8.1
|
||||||
|
github.com/mattn/go-runewidth v0.0.9 // indirect
|
||||||
|
github.com/olekukonko/tablewriter v0.0.4
|
||||||
github.com/onsi/ginkgo v1.11.0
|
github.com/onsi/ginkgo v1.11.0
|
||||||
github.com/onsi/gomega v1.8.1
|
github.com/onsi/gomega v1.8.1
|
||||||
github.com/otiai10/copy v1.0.2
|
github.com/otiai10/copy v1.0.2
|
||||||
|
github.com/qiniu/api.v7/v7 v7.5.0
|
||||||
|
github.com/sirupsen/logrus v1.4.2
|
||||||
github.com/spf13/cobra v1.0.0
|
github.com/spf13/cobra v1.0.0
|
||||||
github.com/stretchr/testify v1.5.1
|
github.com/stretchr/testify v1.5.1
|
||||||
golang.org/x/tools v0.0.0-20200303214625-2b0b585e22fe
|
golang.org/x/net v0.0.0-20200301022130-244492dfa37a
|
||||||
|
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
|
||||||
|
golang.org/x/tools v0.0.0-20200329025819-fd4102a86c65
|
||||||
|
k8s.io/kubernetes v1.13.0
|
||||||
k8s.io/test-infra v0.0.0-20200511080351-8ac9dbfab055
|
k8s.io/test-infra v0.0.0-20200511080351-8ac9dbfab055
|
||||||
)
|
)
|
||||||
|
30
go.sum
30
go.sum
@ -221,6 +221,7 @@ github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI
|
|||||||
github.com/fortytw2/leaktest v1.2.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
|
github.com/fortytw2/leaktest v1.2.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
|
||||||
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
|
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
|
||||||
github.com/frankban/quicktest v1.8.1/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y=
|
github.com/frankban/quicktest v1.8.1/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y=
|
||||||
|
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
github.com/fsouza/fake-gcs-server v0.0.0-20180612165233-e85be23bdaa8/go.mod h1:1/HufuJ+eaDf4KTnYdS6HJMGvMRU8d4cYTuu/1QaBbI=
|
github.com/fsouza/fake-gcs-server v0.0.0-20180612165233-e85be23bdaa8/go.mod h1:1/HufuJ+eaDf4KTnYdS6HJMGvMRU8d4cYTuu/1QaBbI=
|
||||||
github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
|
github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
|
||||||
@ -290,6 +291,7 @@ github.com/go-openapi/swag v0.19.7/go.mod h1:ao+8BpOPyKdpQz3AOJfbeEVpLmWAvlT1IfT
|
|||||||
github.com/go-openapi/validate v0.18.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4=
|
github.com/go-openapi/validate v0.18.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4=
|
||||||
github.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2KDnRCRMUi7GTA=
|
github.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2KDnRCRMUi7GTA=
|
||||||
github.com/go-openapi/validate v0.19.5/go.mod h1:8DJv2CVJQ6kGNpFW6eV9N3JviE1C85nY1c2z52x1Gk4=
|
github.com/go-openapi/validate v0.19.5/go.mod h1:8DJv2CVJQ6kGNpFW6eV9N3JviE1C85nY1c2z52x1Gk4=
|
||||||
|
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
|
||||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
|
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
|
||||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||||
@ -343,8 +345,10 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
|
|||||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-containerregistry v0.0.0-20200115214256-379933c9c22b/go.mod h1:Wtl/v6YdQxv397EREtzwgd9+Ud7Q5D8XMbi3Zazgkrs=
|
github.com/google/go-containerregistry v0.0.0-20200115214256-379933c9c22b/go.mod h1:Wtl/v6YdQxv397EREtzwgd9+Ud7Q5D8XMbi3Zazgkrs=
|
||||||
|
github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY=
|
||||||
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
|
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
|
||||||
github.com/google/go-licenses v0.0.0-20191112164736-212ea350c932/go.mod h1:16wa6pRqNDUIhOtwF0GcROVqMeXHZJ7H6eGDFUh5Pfk=
|
github.com/google/go-licenses v0.0.0-20191112164736-212ea350c932/go.mod h1:16wa6pRqNDUIhOtwF0GcROVqMeXHZJ7H6eGDFUh5Pfk=
|
||||||
|
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
|
||||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||||
github.com/google/go-replayers/grpcreplay v0.1.0/go.mod h1:8Ig2Idjpr6gifRd6pNVggX6TC1Zw6Jx74AKp7QNH2QE=
|
github.com/google/go-replayers/grpcreplay v0.1.0/go.mod h1:8Ig2Idjpr6gifRd6pNVggX6TC1Zw6Jx74AKp7QNH2QE=
|
||||||
github.com/google/go-replayers/httpreplay v0.1.0/go.mod h1:YKZViNhiGgqdBlUbI2MwGpq4pXxNmhJLPHQ7cv2b5no=
|
github.com/google/go-replayers/httpreplay v0.1.0/go.mod h1:YKZViNhiGgqdBlUbI2MwGpq4pXxNmhJLPHQ7cv2b5no=
|
||||||
@ -435,6 +439,7 @@ github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGn
|
|||||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||||
|
github.com/julienschmidt/httprouter v1.2.0 h1:TDTW5Yz1mjftljbcKqRcrYhd4XeOoI98t+9HbQbYf7g=
|
||||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||||
github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
|
github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
|
||||||
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
||||||
@ -447,13 +452,16 @@ github.com/klauspost/compress v1.10.2/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYs
|
|||||||
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||||
github.com/klauspost/pgzip v1.2.1/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
github.com/klauspost/pgzip v1.2.1/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
|
||||||
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA=
|
github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA=
|
||||||
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
|
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
|
||||||
|
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
|
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
|
||||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||||
@ -462,6 +470,7 @@ github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9
|
|||||||
github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc=
|
github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc=
|
||||||
github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
|
github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
|
||||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||||
|
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
|
||||||
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||||
github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
@ -482,7 +491,11 @@ github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOA
|
|||||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||||
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||||
|
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||||
|
github.com/mattn/go-runewidth v0.0.8 h1:3tS41NlGYSmhhe/8fhGRzc+z3AYCw1Fe1WAyLuujKs0=
|
||||||
github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||||
|
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
|
||||||
|
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||||
github.com/mattn/go-shellwords v1.0.9/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
|
github.com/mattn/go-shellwords v1.0.9/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
|
||||||
github.com/mattn/go-sqlite3 v2.0.1+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
github.com/mattn/go-sqlite3 v2.0.1+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||||
github.com/mattn/go-zglob v0.0.1/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo=
|
github.com/mattn/go-zglob v0.0.1/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo=
|
||||||
@ -520,7 +533,10 @@ github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uY
|
|||||||
github.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM=
|
github.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM=
|
||||||
github.com/nwaples/rardecode v1.0.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
|
github.com/nwaples/rardecode v1.0.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
|
||||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||||
|
github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5 h1:58+kh9C6jJVXYjt8IE48G2eWl6BjwU5Gj0gqY84fy78=
|
||||||
github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
|
github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
|
||||||
|
github.com/olekukonko/tablewriter v0.0.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8=
|
||||||
|
github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA=
|
||||||
github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
@ -547,6 +563,7 @@ github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJ
|
|||||||
github.com/openzipkin/zipkin-go v0.2.0/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
|
github.com/openzipkin/zipkin-go v0.2.0/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
|
||||||
github.com/otiai10/copy v1.0.2 h1:DDNipYy6RkIkjMwy+AWzgKiNTyj2RUI9yEMeETEpVyc=
|
github.com/otiai10/copy v1.0.2 h1:DDNipYy6RkIkjMwy+AWzgKiNTyj2RUI9yEMeETEpVyc=
|
||||||
github.com/otiai10/copy v1.0.2/go.mod h1:c7RpqBkwMom4bYTSkLSym4VSJz/XtncWRAj/J4PEIMY=
|
github.com/otiai10/copy v1.0.2/go.mod h1:c7RpqBkwMom4bYTSkLSym4VSJz/XtncWRAj/J4PEIMY=
|
||||||
|
github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95 h1:+OLn68pqasWca0z5ryit9KGfp3sUsW4Lqg32iRMJyzs=
|
||||||
github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE=
|
github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE=
|
||||||
github.com/otiai10/mint v1.3.0 h1:Ady6MKVezQwHBkGzLFbrsywyp09Ah7rkmfjV3Bcr5uc=
|
github.com/otiai10/mint v1.3.0 h1:Ady6MKVezQwHBkGzLFbrsywyp09Ah7rkmfjV3Bcr5uc=
|
||||||
github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo=
|
github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo=
|
||||||
@ -603,6 +620,8 @@ github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDa
|
|||||||
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
|
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
|
||||||
github.com/prometheus/procfs v0.0.10/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
|
github.com/prometheus/procfs v0.0.10/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
|
||||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||||
|
github.com/qiniu/api.v7/v7 v7.5.0 h1:DY6NrIp6FZ1GP4Roc9hRnO2m+OLzASYNnvz5Mbgw1rk=
|
||||||
|
github.com/qiniu/api.v7/v7 v7.5.0/go.mod h1:VE5oC5rkE1xul0u1S2N0b2Uxq9/6hZzhyqjgK25XDcM=
|
||||||
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||||
github.com/rcrowley/go-metrics v0.0.0-20190706150252-9beb055b7962/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
github.com/rcrowley/go-metrics v0.0.0-20190706150252-9beb055b7962/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M=
|
github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M=
|
||||||
@ -622,6 +641,7 @@ github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeV
|
|||||||
github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
|
github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
|
||||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||||
|
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
|
||||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||||
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||||
@ -695,6 +715,7 @@ github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMx
|
|||||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||||
github.com/xlab/handysort v0.0.0-20150421192137-fb3537ed64a1/go.mod h1:QcJo0QPSfTONNIgpN5RA8prR7fF8nkF6cTWTcNerRO8=
|
github.com/xlab/handysort v0.0.0-20150421192137-fb3537ed64a1/go.mod h1:QcJo0QPSfTONNIgpN5RA8prR7fF8nkF6cTWTcNerRO8=
|
||||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||||
|
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs=
|
github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs=
|
||||||
github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA=
|
github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA=
|
||||||
github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg=
|
github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg=
|
||||||
@ -746,6 +767,7 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
|
|||||||
golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 h1:xMPOj6Pz6UipU1wXLkrtqpHbR0AVFnyPEQq/wRWz9lM=
|
||||||
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
@ -809,6 +831,7 @@ golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAG
|
|||||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw=
|
||||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
@ -905,6 +928,8 @@ golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapK
|
|||||||
golang.org/x/tools v0.0.0-20200214144324-88be01311a71/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
golang.org/x/tools v0.0.0-20200214144324-88be01311a71/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
golang.org/x/tools v0.0.0-20200303214625-2b0b585e22fe h1:Kh3iY7o/2bMfQXZdwLdL9jDMU1k9HoVn0P1mGCfoFLc=
|
golang.org/x/tools v0.0.0-20200303214625-2b0b585e22fe h1:Kh3iY7o/2bMfQXZdwLdL9jDMU1k9HoVn0P1mGCfoFLc=
|
||||||
golang.org/x/tools v0.0.0-20200303214625-2b0b585e22fe/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
golang.org/x/tools v0.0.0-20200303214625-2b0b585e22fe/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||||
|
golang.org/x/tools v0.0.0-20200329025819-fd4102a86c65 h1:1KSbntBked74wYsKq0jzXYy7ZwcjAUtrl7EmPE97Iiw=
|
||||||
|
golang.org/x/tools v0.0.0-20200329025819-fd4102a86c65/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||||
@ -930,6 +955,7 @@ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7
|
|||||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||||
google.golang.org/appengine v1.6.2/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
google.golang.org/appengine v1.6.2/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||||
|
google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM=
|
||||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||||
google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8/go.mod h1:0H1ncTHf11KCFhTc/+EFRbzSCOZx+VUbRMk55Yv5MYk=
|
google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8/go.mod h1:0H1ncTHf11KCFhTc/+EFRbzSCOZx+VUbRMk55Yv5MYk=
|
||||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||||
@ -965,6 +991,7 @@ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLks
|
|||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20141024133853-64131543e789/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20141024133853-64131543e789/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
|
gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
|
||||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||||
@ -1017,6 +1044,7 @@ k8s.io/apiextensions-apiserver v0.17.2/go.mod h1:4KdMpjkEjjDI2pPfBA15OscyNldHWdB
|
|||||||
k8s.io/apimachinery v0.0.0-20190703205208-4cfb76a8bf76/go.mod h1:M2fZgZL9DbLfeJaPBCDqSqNsdsmLN+V29knYJnIXlMA=
|
k8s.io/apimachinery v0.0.0-20190703205208-4cfb76a8bf76/go.mod h1:M2fZgZL9DbLfeJaPBCDqSqNsdsmLN+V29knYJnIXlMA=
|
||||||
k8s.io/apimachinery v0.17.0/go.mod h1:b9qmWdKlLuU9EBh+06BtLcSf/Mu89rWL33naRxs1uZg=
|
k8s.io/apimachinery v0.17.0/go.mod h1:b9qmWdKlLuU9EBh+06BtLcSf/Mu89rWL33naRxs1uZg=
|
||||||
k8s.io/apimachinery v0.17.2/go.mod h1:b9qmWdKlLuU9EBh+06BtLcSf/Mu89rWL33naRxs1uZg=
|
k8s.io/apimachinery v0.17.2/go.mod h1:b9qmWdKlLuU9EBh+06BtLcSf/Mu89rWL33naRxs1uZg=
|
||||||
|
k8s.io/apimachinery v0.17.3 h1:f+uZV6rm4/tHE7xXgLyToprg6xWairaClGVkm2t8omg=
|
||||||
k8s.io/apimachinery v0.17.3/go.mod h1:gxLnyZcGNdZTCLnq3fgzyg2A5BVCHTNDFrw8AmuJ+0g=
|
k8s.io/apimachinery v0.17.3/go.mod h1:gxLnyZcGNdZTCLnq3fgzyg2A5BVCHTNDFrw8AmuJ+0g=
|
||||||
k8s.io/apiserver v0.17.0/go.mod h1:ABM+9x/prjINN6iiffRVNCBR2Wk7uY4z+EtEGZD48cg=
|
k8s.io/apiserver v0.17.0/go.mod h1:ABM+9x/prjINN6iiffRVNCBR2Wk7uY4z+EtEGZD48cg=
|
||||||
k8s.io/apiserver v0.17.2/go.mod h1:lBmw/TtQdtxvrTk0e2cgtOxHizXI+d0mmGQURIHQZlo=
|
k8s.io/apiserver v0.17.2/go.mod h1:lBmw/TtQdtxvrTk0e2cgtOxHizXI+d0mmGQURIHQZlo=
|
||||||
@ -1039,10 +1067,12 @@ k8s.io/gengo v0.0.0-20191108084044-e500ee069b5c/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8
|
|||||||
k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
|
k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
|
||||||
k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
|
k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
|
||||||
k8s.io/klog v0.3.1/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
|
k8s.io/klog v0.3.1/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
|
||||||
|
k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8=
|
||||||
k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I=
|
k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I=
|
||||||
k8s.io/kube-openapi v0.0.0-20190228160746-b3a7cee44a30/go.mod h1:BXM9ceUBTj2QnfH2MK1odQs778ajze1RxcmP6S8RVVc=
|
k8s.io/kube-openapi v0.0.0-20190228160746-b3a7cee44a30/go.mod h1:BXM9ceUBTj2QnfH2MK1odQs778ajze1RxcmP6S8RVVc=
|
||||||
k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E=
|
k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E=
|
||||||
k8s.io/kubectl v0.17.2/go.mod h1:y4rfLV0n6aPmvbRCqZQjvOp3ezxsFgpqL+zF5jH/lxk=
|
k8s.io/kubectl v0.17.2/go.mod h1:y4rfLV0n6aPmvbRCqZQjvOp3ezxsFgpqL+zF5jH/lxk=
|
||||||
|
k8s.io/kubernetes v1.13.0 h1:qTfB+u5M92k2fCCCVP2iuhgwwSOv1EkAkvQY1tQODD8=
|
||||||
k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk=
|
k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk=
|
||||||
k8s.io/legacy-cloud-providers v0.17.0/go.mod h1:DdzaepJ3RtRy+e5YhNtrCYwlgyK87j/5+Yfp0L9Syp8=
|
k8s.io/legacy-cloud-providers v0.17.0/go.mod h1:DdzaepJ3RtRy+e5YhNtrCYwlgyK87j/5+Yfp0L9Syp8=
|
||||||
k8s.io/metrics v0.17.2/go.mod h1:3TkNHET4ROd+NfzNxkjoVfQ0Ob4iZnaHmSEA4vYpwLw=
|
k8s.io/metrics v0.17.2/go.mod h1:3TkNHET4ROd+NfzNxkjoVfQ0Ob4iZnaHmSEA4vYpwLw=
|
||||||
|
@ -23,13 +23,17 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path"
|
"path"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestCover is a collection of all counters
|
// TestCover is a collection of all counters
|
||||||
@ -251,12 +255,7 @@ func AddCacheCover(pkg *Package, in *PackageCover) *PackageCover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CoverageList is a collection and summary over multiple file Coverage objects
|
// CoverageList is a collection and summary over multiple file Coverage objects
|
||||||
type CoverageList struct {
|
type CoverageList []Coverage
|
||||||
*Coverage
|
|
||||||
Groups []Coverage
|
|
||||||
ConcernedFiles map[string]bool
|
|
||||||
CovThresholdInt int
|
|
||||||
}
|
|
||||||
|
|
||||||
// Coverage stores test coverage summary data for one file
|
// Coverage stores test coverage summary data for one file
|
||||||
type Coverage struct {
|
type Coverage struct {
|
||||||
@ -272,10 +271,11 @@ type codeBlock struct {
|
|||||||
coverageCount int // number of times the block is covered
|
coverageCount int // number of times the block is covered
|
||||||
}
|
}
|
||||||
|
|
||||||
func CovList(f io.Reader) (g *CoverageList, err error) {
|
//convert profile to CoverageList struct
|
||||||
|
func CovList(f io.Reader) (g CoverageList, err error) {
|
||||||
scanner := bufio.NewScanner(f)
|
scanner := bufio.NewScanner(f)
|
||||||
scanner.Scan() // discard first line
|
scanner.Scan() // discard first line
|
||||||
g = NewCoverageList("", map[string]bool{}, 0)
|
g = NewCoverageList()
|
||||||
|
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
row := scanner.Text()
|
row := scanner.Text()
|
||||||
@ -283,19 +283,26 @@ func CovList(f io.Reader) (g *CoverageList, err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
blk.addToGroupCov(g)
|
blk.addToGroupCov(&g)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCoverageList constructs new (file) group Coverage
|
// covert profile file to CoverageList struct
|
||||||
func NewCoverageList(name string, concernedFiles map[string]bool, covThresholdInt int) *CoverageList {
|
func ReadFileToCoverList(path string) (g CoverageList, err error) {
|
||||||
return &CoverageList{
|
f, err := ioutil.ReadFile(path)
|
||||||
Coverage: newCoverage(name),
|
if err != nil {
|
||||||
Groups: []Coverage{},
|
logrus.Errorf("Open file %s failed!", path)
|
||||||
ConcernedFiles: concernedFiles,
|
return nil, err
|
||||||
CovThresholdInt: covThresholdInt,
|
|
||||||
}
|
}
|
||||||
|
g, err = CovList(bytes.NewReader(f))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCoverageList return empty CoverageList
|
||||||
|
func NewCoverageList() CoverageList {
|
||||||
|
return CoverageList{}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func newCoverage(name string) *Coverage {
|
func newCoverage(name string) *Coverage {
|
||||||
@ -332,28 +339,47 @@ func (blk *codeBlock) addToGroupCov(g *CoverageList) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *CoverageList) size() int {
|
func (g CoverageList) size() int {
|
||||||
return len(g.Groups)
|
return len(g)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *CoverageList) lastElement() *Coverage {
|
func (g CoverageList) lastElement() *Coverage {
|
||||||
return &g.Groups[g.size()-1]
|
return &g[g.size()-1]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *CoverageList) append(c *Coverage) {
|
func (g *CoverageList) append(c *Coverage) {
|
||||||
g.Groups = append(g.Groups, *c)
|
*g = append(*g, *c)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Group returns the collection of file Coverage objects
|
// sort CoverageList with filenames
|
||||||
func (g *CoverageList) Group() *[]Coverage {
|
func (g CoverageList) Sort() {
|
||||||
return &g.Groups
|
sort.SliceStable(g, func(i, j int) bool {
|
||||||
|
return g[i].Name() < g[j].Name()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g CoverageList) TotalPercentage() string {
|
||||||
|
ratio, err := g.TotalRatio()
|
||||||
|
if err == nil {
|
||||||
|
return PercentStr(ratio)
|
||||||
|
}
|
||||||
|
return "N/A"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g CoverageList) TotalRatio() (ratio float32, err error) {
|
||||||
|
var total Coverage
|
||||||
|
for _, c := range g {
|
||||||
|
total.NCoveredStmts += c.NCoveredStmts
|
||||||
|
total.NAllStmts += c.NAllStmts
|
||||||
|
}
|
||||||
|
return total.Ratio()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map returns maps the file name to its coverage for faster retrieval
|
// Map returns maps the file name to its coverage for faster retrieval
|
||||||
// & membership check
|
// & membership check
|
||||||
func (g *CoverageList) Map() map[string]Coverage {
|
func (g CoverageList) Map() map[string]Coverage {
|
||||||
m := make(map[string]Coverage)
|
m := make(map[string]Coverage)
|
||||||
for _, c := range g.Groups {
|
for _, c := range g {
|
||||||
m[c.Name()] = c
|
m[c.Name()] = c
|
||||||
}
|
}
|
||||||
return m
|
return m
|
||||||
@ -370,7 +396,6 @@ func (c *Coverage) Percentage() string {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
return PercentStr(ratio)
|
return PercentStr(ratio)
|
||||||
}
|
}
|
||||||
|
|
||||||
return "N/A"
|
return "N/A"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,7 +42,7 @@ func TestCoverageRatio(t *testing.T) {
|
|||||||
func TestRatioErr(t *testing.T) {
|
func TestRatioErr(t *testing.T) {
|
||||||
c := &Coverage{FileName: "fake-coverage", NCoveredStmts: 200, NAllStmts: 0}
|
c := &Coverage{FileName: "fake-coverage", NCoveredStmts: 200, NAllStmts: 0}
|
||||||
_, err := c.Ratio()
|
_, err := c.Ratio()
|
||||||
assert.NotNil(t, err)
|
assert.NotEqual(t, err, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPercentageNA(t *testing.T) {
|
func TestPercentageNA(t *testing.T) {
|
||||||
@ -50,56 +50,69 @@ func TestPercentageNA(t *testing.T) {
|
|||||||
assert.Equal(t, "N/A", c.Percentage())
|
assert.Equal(t, "N/A", c.Percentage())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGenLocalCoverDiffReport(t *testing.T) {
|
|
||||||
//coverage increase
|
|
||||||
newList := &CoverageList{Groups: []Coverage{{FileName: "fake-coverage", NCoveredStmts: 15, NAllStmts: 20}}}
|
|
||||||
baseList := &CoverageList{Groups: []Coverage{{FileName: "fake-coverage", NCoveredStmts: 10, NAllStmts: 20}}}
|
|
||||||
rows := GenLocalCoverDiffReport(newList, baseList)
|
|
||||||
assert.Equal(t, 1, len(rows))
|
|
||||||
assert.Equal(t, []string{"fake-coverage", "50.0%", "75.0%", "25.0%"}, rows[0])
|
|
||||||
|
|
||||||
//coverage decrease
|
|
||||||
baseList = &CoverageList{Groups: []Coverage{{FileName: "fake-coverage", NCoveredStmts: 20, NAllStmts: 20}}}
|
|
||||||
rows = GenLocalCoverDiffReport(newList, baseList)
|
|
||||||
assert.Equal(t, []string{"fake-coverage", "100.0%", "75.0%", "-25.0%"}, rows[0])
|
|
||||||
|
|
||||||
//diff file
|
|
||||||
baseList = &CoverageList{Groups: []Coverage{{FileName: "fake-coverage-v1", NCoveredStmts: 10, NAllStmts: 20}}}
|
|
||||||
rows = GenLocalCoverDiffReport(newList, baseList)
|
|
||||||
assert.Equal(t, []string{"fake-coverage", "None", "75.0%", "75.0%"}, rows[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCovList(t *testing.T) {
|
func TestCovList(t *testing.T) {
|
||||||
fileName := "qiniu.com/kodo/apiserver/server/main.go"
|
fileName := "qiniu.com/kodo/apiserver/server/main.go"
|
||||||
|
|
||||||
// percentage is 100%
|
|
||||||
p := strings.NewReader("mode: atomic\n" +
|
|
||||||
fileName + ":32.49,33.13 1 30\n")
|
|
||||||
covL, err := CovList(p)
|
|
||||||
covF := covL.Map()[fileName]
|
|
||||||
assert.Nil(t, err)
|
|
||||||
assert.Equal(t, "100.0%", covF.Percentage())
|
|
||||||
|
|
||||||
// percentage is 50%
|
|
||||||
p = strings.NewReader("mode: atomic\n" +
|
|
||||||
fileName + ":32.49,33.13 1 30\n" +
|
|
||||||
fileName + ":42.49,43.13 1 0\n")
|
|
||||||
covL, err = CovList(p)
|
|
||||||
covF = covL.Map()[fileName]
|
|
||||||
assert.Nil(t, err)
|
|
||||||
assert.Equal(t, "50.0%", covF.Percentage())
|
|
||||||
|
|
||||||
// two files
|
|
||||||
fileName1 := "qiniu.com/kodo/apiserver/server/svr.go"
|
fileName1 := "qiniu.com/kodo/apiserver/server/svr.go"
|
||||||
p = strings.NewReader("mode: atomic\n" +
|
|
||||||
fileName + ":32.49,33.13 1 30\n" +
|
items := []struct {
|
||||||
fileName1 + ":42.49,43.13 1 0\n")
|
profile string
|
||||||
covL, err = CovList(p)
|
expectPer []string
|
||||||
covF = covL.Map()[fileName]
|
}{
|
||||||
covF1 := covL.Map()[fileName1]
|
// percentage is 100%
|
||||||
assert.Nil(t, err)
|
{
|
||||||
assert.Equal(t, "100.0%", covF.Percentage())
|
profile: "mode: atomic\n" +
|
||||||
assert.Equal(t, "0.0%", covF1.Percentage())
|
fileName + ":32.49,33.13 1 30\n",
|
||||||
|
expectPer: []string{"100.0%"},
|
||||||
|
},
|
||||||
|
// percentage is 50%
|
||||||
|
{profile: "mode: atomic\n" +
|
||||||
|
fileName + ":32.49,33.13 1 30\n" +
|
||||||
|
fileName + ":42.49,43.13 1 0\n",
|
||||||
|
expectPer: []string{"50.0%"},
|
||||||
|
},
|
||||||
|
// two files
|
||||||
|
{
|
||||||
|
profile: "mode: atomic\n" +
|
||||||
|
fileName + ":32.49,33.13 1 30\n" +
|
||||||
|
fileName1 + ":42.49,43.13 1 0\n",
|
||||||
|
expectPer: []string{"100.0%", "0.0%"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range items {
|
||||||
|
r := strings.NewReader(tc.profile)
|
||||||
|
c, err := CovList(r)
|
||||||
|
c.Sort()
|
||||||
|
assert.Equal(t, err, nil)
|
||||||
|
for k, v := range c {
|
||||||
|
assert.Equal(t, tc.expectPer[k], v.Percentage())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTotalPercentage(t *testing.T) {
|
||||||
|
items := []struct {
|
||||||
|
list CoverageList
|
||||||
|
expectPer string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
list: CoverageList{Coverage{FileName: "fake-coverage", NCoveredStmts: 15, NAllStmts: 0}},
|
||||||
|
expectPer: "N/A",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
list: CoverageList{Coverage{FileName: "fake-coverage", NCoveredStmts: 15, NAllStmts: 20}},
|
||||||
|
expectPer: "75.0%",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
list: CoverageList{Coverage{FileName: "fake-coverage", NCoveredStmts: 15, NAllStmts: 20},
|
||||||
|
Coverage{FileName: "fake-coverage-1", NCoveredStmts: 10, NAllStmts: 30}},
|
||||||
|
expectPer: "50.0%",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range items {
|
||||||
|
assert.Equal(t, tc.expectPer, tc.list.TotalPercentage())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBuildCoverCmd(t *testing.T) {
|
func TestBuildCoverCmd(t *testing.T) {
|
||||||
|
@ -16,41 +16,117 @@
|
|||||||
|
|
||||||
package cover
|
package cover
|
||||||
|
|
||||||
type GroupChanges struct {
|
import "sort"
|
||||||
Added []Coverage
|
|
||||||
Deleted []Coverage
|
type DeltaCov struct {
|
||||||
Unchanged []Coverage
|
FileName string
|
||||||
Changed []Incremental
|
BasePer string
|
||||||
BaseGroup *CoverageList
|
NewPer string
|
||||||
NewGroup *CoverageList
|
DeltaPer string
|
||||||
|
LineCovLink string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Incremental struct {
|
type DeltaCovList []DeltaCov
|
||||||
base Coverage
|
|
||||||
new Coverage
|
|
||||||
}
|
|
||||||
|
|
||||||
func GenLocalCoverDiffReport(newList *CoverageList, baseList *CoverageList) [][]string {
|
// get full delta coverage between new and base profile
|
||||||
var rows [][]string
|
func GetFullDeltaCov(newList CoverageList, baseList CoverageList) (delta DeltaCovList) {
|
||||||
basePMap := baseList.Map()
|
newMap := newList.Map()
|
||||||
|
baseMap := baseList.Map()
|
||||||
|
|
||||||
for _, l := range newList.Groups {
|
for file, n := range newMap {
|
||||||
baseCov, ok := basePMap[l.Name()]
|
b, ok := baseMap[file]
|
||||||
|
//if the file not in base profile, set None
|
||||||
if !ok {
|
if !ok {
|
||||||
rows = append(rows, []string{l.FileName, "None", l.Percentage(), PercentStr(Delta(l, baseCov))})
|
delta = append(delta, DeltaCov{
|
||||||
|
FileName: file,
|
||||||
|
BasePer: "None",
|
||||||
|
NewPer: n.Percentage(),
|
||||||
|
DeltaPer: PercentStr(Delta(n, b))})
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if l.Percentage() == baseCov.Percentage() {
|
delta = append(delta, DeltaCov{
|
||||||
continue
|
FileName: file,
|
||||||
}
|
BasePer: b.Percentage(),
|
||||||
rows = append(rows, []string{l.FileName, baseCov.Percentage(), l.Percentage(), PercentStr(Delta(l, baseCov))})
|
NewPer: n.Percentage(),
|
||||||
|
DeltaPer: PercentStr(Delta(n, b))})
|
||||||
}
|
}
|
||||||
|
|
||||||
return rows
|
for file, b := range baseMap {
|
||||||
|
//if the file not in new profile, set None
|
||||||
|
if n, ok := newMap[file]; !ok {
|
||||||
|
delta = append(delta, DeltaCov{
|
||||||
|
FileName: file,
|
||||||
|
BasePer: b.Percentage(),
|
||||||
|
NewPer: "None",
|
||||||
|
DeltaPer: PercentStr(Delta(n, b))})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//get two profile diff cov
|
||||||
|
func GetDeltaCov(newList CoverageList, baseList CoverageList) (delta DeltaCovList) {
|
||||||
|
d := GetFullDeltaCov(newList, baseList)
|
||||||
|
for _, v := range d {
|
||||||
|
if v.DeltaPer == "0.0%" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
delta = append(delta, v)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//get two profile diff cov of changed files
|
||||||
|
func GetChFileDeltaCov(newList CoverageList, baseList CoverageList, changedFiles []string) (list DeltaCovList) {
|
||||||
|
d := GetFullDeltaCov(newList, baseList)
|
||||||
|
dMap := d.Map()
|
||||||
|
for _, file := range changedFiles {
|
||||||
|
if _, ok := dMap[file]; ok {
|
||||||
|
list = append(list, dMap[file])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//calculate two coverage delta
|
||||||
func Delta(new Coverage, base Coverage) float32 {
|
func Delta(new Coverage, base Coverage) float32 {
|
||||||
baseRatio, _ := base.Ratio()
|
baseRatio, _ := base.Ratio()
|
||||||
newRatio, _ := new.Ratio()
|
newRatio, _ := new.Ratio()
|
||||||
return newRatio - baseRatio
|
return newRatio - baseRatio
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//calculate two coverage delta
|
||||||
|
func TotalDelta(new CoverageList, base CoverageList) float32 {
|
||||||
|
baseRatio, _ := base.TotalRatio()
|
||||||
|
newRatio, _ := new.TotalRatio()
|
||||||
|
return newRatio - baseRatio
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map returns maps the file name to its DeltaCov for faster retrieval & membership check
|
||||||
|
func (d DeltaCovList) Map() map[string]DeltaCov {
|
||||||
|
m := make(map[string]DeltaCov)
|
||||||
|
for _, c := range d {
|
||||||
|
m[c.FileName] = c
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// sort DeltaCovList with filenames
|
||||||
|
func (d DeltaCovList) Sort() {
|
||||||
|
sort.SliceStable(d, func(i, j int) bool {
|
||||||
|
return d[i].Name() < d[j].Name()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name returns the file name
|
||||||
|
func (c *DeltaCov) Name() string {
|
||||||
|
return c.FileName
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *DeltaCov) GetLineCovLink() string {
|
||||||
|
return c.LineCovLink
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *DeltaCov) SetLineCovLink(link string) {
|
||||||
|
c.LineCovLink = link
|
||||||
|
}
|
||||||
|
133
pkg/cover/delta_test.go
Normal file
133
pkg/cover/delta_test.go
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2020 Qiniu Cloud (qiniu.com)
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package cover
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetDeltaCov(t *testing.T) {
|
||||||
|
items := []struct {
|
||||||
|
newList CoverageList
|
||||||
|
baseList CoverageList
|
||||||
|
expectDelta DeltaCovList
|
||||||
|
rows int
|
||||||
|
}{
|
||||||
|
//coverage increase
|
||||||
|
{
|
||||||
|
newList: CoverageList{Coverage{FileName: "fake-coverage", NCoveredStmts: 15, NAllStmts: 20}},
|
||||||
|
baseList: CoverageList{Coverage{FileName: "fake-coverage", NCoveredStmts: 10, NAllStmts: 20}},
|
||||||
|
expectDelta: DeltaCovList{{FileName: "fake-coverage", BasePer: "50.0%", NewPer: "75.0%", DeltaPer: "25.0%"}},
|
||||||
|
rows: 1,
|
||||||
|
},
|
||||||
|
//coverage decrease
|
||||||
|
{
|
||||||
|
newList: CoverageList{Coverage{FileName: "fake-coverage", NCoveredStmts: 15, NAllStmts: 20}},
|
||||||
|
baseList: CoverageList{Coverage{FileName: "fake-coverage", NCoveredStmts: 20, NAllStmts: 20}},
|
||||||
|
expectDelta: DeltaCovList{{FileName: "fake-coverage", BasePer: "100.0%", NewPer: "75.0%", DeltaPer: "-25.0%"}},
|
||||||
|
rows: 1,
|
||||||
|
},
|
||||||
|
//diff file
|
||||||
|
{
|
||||||
|
newList: CoverageList{Coverage{FileName: "fake-coverage", NCoveredStmts: 15, NAllStmts: 20}},
|
||||||
|
baseList: CoverageList{Coverage{FileName: "fake-coverage-v1", NCoveredStmts: 10, NAllStmts: 20}},
|
||||||
|
expectDelta: DeltaCovList{{FileName: "fake-coverage", BasePer: "None", NewPer: "75.0%", DeltaPer: "75.0%"},
|
||||||
|
{FileName: "fake-coverage-v1", BasePer: "50.0%", NewPer: "None", DeltaPer: "-50.0%"}},
|
||||||
|
rows: 2,
|
||||||
|
},
|
||||||
|
//one file has same coverage rate
|
||||||
|
{
|
||||||
|
newList: CoverageList{Coverage{FileName: "fake-coverage", NCoveredStmts: 15, NAllStmts: 20}},
|
||||||
|
baseList: CoverageList{Coverage{FileName: "fake-coverage", NCoveredStmts: 15, NAllStmts: 20},
|
||||||
|
Coverage{FileName: "fake-coverage-v1", NCoveredStmts: 10, NAllStmts: 20}},
|
||||||
|
expectDelta: DeltaCovList{{FileName: "fake-coverage-v1", BasePer: "50.0%", NewPer: "None", DeltaPer: "-50.0%"}},
|
||||||
|
rows: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range items {
|
||||||
|
d := GetDeltaCov(tc.newList, tc.baseList)
|
||||||
|
assert.Equal(t, tc.rows, len(d))
|
||||||
|
assert.Equal(t, tc.expectDelta, d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetChFileDeltaCov(t *testing.T) {
|
||||||
|
items := []struct {
|
||||||
|
newList CoverageList
|
||||||
|
baseList CoverageList
|
||||||
|
changedFiles []string
|
||||||
|
expectDelta DeltaCovList
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
newList: CoverageList{Coverage{FileName: "fake-coverage", NCoveredStmts: 15, NAllStmts: 20}},
|
||||||
|
baseList: CoverageList{Coverage{FileName: "fake-coverage-v1", NCoveredStmts: 10, NAllStmts: 20}},
|
||||||
|
changedFiles: []string{"fake-coverage"},
|
||||||
|
expectDelta: DeltaCovList{{FileName: "fake-coverage", BasePer: "None", NewPer: "75.0%", DeltaPer: "75.0%"}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range items {
|
||||||
|
d := GetChFileDeltaCov(tc.newList, tc.baseList, tc.changedFiles)
|
||||||
|
assert.Equal(t, tc.expectDelta, d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMapAndSort(t *testing.T) {
|
||||||
|
items := []struct {
|
||||||
|
dList DeltaCovList
|
||||||
|
expectMap map[string]DeltaCov
|
||||||
|
expectSort DeltaCovList
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
dList: DeltaCovList{DeltaCov{FileName: "b", BasePer: "10.0%", NewPer: "20.0%", DeltaPer: "10.0%"},
|
||||||
|
DeltaCov{FileName: "a", BasePer: "10.0%", NewPer: "30.0%", DeltaPer: "20.0%"},
|
||||||
|
},
|
||||||
|
expectMap: map[string]DeltaCov{
|
||||||
|
"a": {FileName: "a", BasePer: "10.0%", NewPer: "30.0%", DeltaPer: "20.0%"},
|
||||||
|
"b": {FileName: "b", BasePer: "10.0%", NewPer: "20.0%", DeltaPer: "10.0%"},
|
||||||
|
},
|
||||||
|
expectSort: DeltaCovList{DeltaCov{FileName: "a", BasePer: "10.0%", NewPer: "30.0%", DeltaPer: "20.0%"},
|
||||||
|
DeltaCov{FileName: "b", BasePer: "10.0%", NewPer: "20.0%", DeltaPer: "10.0%"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dList: DeltaCovList{DeltaCov{FileName: "b", BasePer: "10.0%", NewPer: "20.0%", DeltaPer: "10.0%"},
|
||||||
|
DeltaCov{FileName: "b-1", BasePer: "10.0%", NewPer: "30.0%", DeltaPer: "20.0%"},
|
||||||
|
DeltaCov{FileName: "1-b", BasePer: "10.0%", NewPer: "40.0%", DeltaPer: "30.0%"},
|
||||||
|
},
|
||||||
|
expectMap: map[string]DeltaCov{
|
||||||
|
"1-b": {FileName: "1-b", BasePer: "10.0%", NewPer: "40.0%", DeltaPer: "30.0%"},
|
||||||
|
"b": {FileName: "b", BasePer: "10.0%", NewPer: "20.0%", DeltaPer: "10.0%"},
|
||||||
|
"b-1": {FileName: "b-1", BasePer: "10.0%", NewPer: "30.0%", DeltaPer: "20.0%"},
|
||||||
|
},
|
||||||
|
expectSort: DeltaCovList{DeltaCov{FileName: "1-b", BasePer: "10.0%", NewPer: "40.0%", DeltaPer: "30.0%"},
|
||||||
|
DeltaCov{FileName: "b", BasePer: "10.0%", NewPer: "20.0%", DeltaPer: "10.0%"},
|
||||||
|
DeltaCov{FileName: "b-1", BasePer: "10.0%", NewPer: "30.0%", DeltaPer: "20.0%"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range items {
|
||||||
|
assert.Equal(t, tc.expectMap, tc.dList.Map())
|
||||||
|
tc.dList.Sort()
|
||||||
|
assert.Equal(t, tc.expectSort, tc.dList)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
180
pkg/github/github.go
Normal file
180
pkg/github/github.go
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2020 Qiniu Cloud (qiniu.com)
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package github
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/google/go-github/github"
|
||||||
|
"github.com/olekukonko/tablewriter"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
|
||||||
|
"github.com/qiniu/goc/pkg/cover"
|
||||||
|
)
|
||||||
|
|
||||||
|
const CommentsPrefix = "The following is the coverage report on the affected files."
|
||||||
|
|
||||||
|
type PrComment struct {
|
||||||
|
RobotUserName string
|
||||||
|
RepoOwner string
|
||||||
|
RepoName string
|
||||||
|
CommentFlag string
|
||||||
|
PrNumber int
|
||||||
|
Ctx context.Context
|
||||||
|
opt *github.ListOptions
|
||||||
|
GithubClient *github.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPrClient(githubTokenPath, repoOwner, repoName, prNumStr, botUserName, commentFlag string) *PrComment {
|
||||||
|
var client *github.Client
|
||||||
|
var ctx = context.Background()
|
||||||
|
|
||||||
|
prNum, err := strconv.Atoi(prNumStr)
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Fatalf("Failed to convert prNumStr(=%v) to int.\n", prNumStr)
|
||||||
|
}
|
||||||
|
token, err := ioutil.ReadFile(githubTokenPath)
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Fatalf("Failed to get github token.\n")
|
||||||
|
}
|
||||||
|
ts := oauth2.StaticTokenSource(
|
||||||
|
&oauth2.Token{AccessToken: strings.TrimSpace(string(token))},
|
||||||
|
)
|
||||||
|
tc := oauth2.NewClient(ctx, ts)
|
||||||
|
client = github.NewClient(tc)
|
||||||
|
|
||||||
|
return &PrComment{
|
||||||
|
RobotUserName: botUserName,
|
||||||
|
RepoOwner: repoOwner,
|
||||||
|
RepoName: repoName,
|
||||||
|
PrNumber: prNum,
|
||||||
|
CommentFlag: commentFlag,
|
||||||
|
Ctx: ctx,
|
||||||
|
opt: &github.ListOptions{Page: 1},
|
||||||
|
GithubClient: client,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//post github comment of diff coverage
|
||||||
|
func (c *PrComment) CreateGithubComment(commentPrefix string, diffCovList cover.DeltaCovList) (err error) {
|
||||||
|
if len(diffCovList) == 0 {
|
||||||
|
logrus.Printf("Detect 0 files coverage diff, will not comment to github.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
content := GenCommentContent(commentPrefix, diffCovList)
|
||||||
|
|
||||||
|
err = c.PostComment(content, commentPrefix)
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Fatalf("Post comment to github failed.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *PrComment) PostComment(content, commentPrefix string) error {
|
||||||
|
//step1: erase history similar comment to avoid too many comment for same job
|
||||||
|
err := c.EraseHistoryComment(commentPrefix)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
//step2: post comment with new result
|
||||||
|
comment := &github.IssueComment{
|
||||||
|
Body: &content,
|
||||||
|
}
|
||||||
|
_, _, err = c.GithubClient.Issues.CreateComment(c.Ctx, c.RepoOwner, c.RepoName, c.PrNumber, comment)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// erase history similar comment before post again
|
||||||
|
func (c *PrComment) EraseHistoryComment(commentPrefix string) error {
|
||||||
|
comments, _, err := c.GithubClient.Issues.ListComments(c.Ctx, c.RepoOwner, c.RepoName, c.PrNumber, nil)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Errorf("list PR comments failed.")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
logrus.Infof("the count of history comments by %s is: %v", c.RobotUserName, len(comments))
|
||||||
|
|
||||||
|
for _, cm := range comments {
|
||||||
|
if *cm.GetUser().Login == c.RobotUserName && strings.HasPrefix(cm.GetBody(), commentPrefix) {
|
||||||
|
_, err = c.GithubClient.Issues.DeleteComment(c.Ctx, c.RepoOwner, c.RepoName, *cm.ID)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Errorf("delete PR comments %d failed.", *cm.ID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//get github pull request changes file list
|
||||||
|
func (c *PrComment) GetPrChangedFiles() (files []string, err error) {
|
||||||
|
var commitFiles []*github.CommitFile
|
||||||
|
for {
|
||||||
|
f, resp, err := c.GithubClient.PullRequests.ListFiles(c.Ctx, c.RepoOwner, c.RepoName, c.PrNumber, c.opt)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Errorf("Get PR changed file failed. repoOwner is: %s, repoName is: %s, prNum is: %d", c.RepoOwner, c.RepoName, c.PrNumber)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
commitFiles = append(commitFiles, f...)
|
||||||
|
if resp.NextPage == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
c.opt.Page = resp.NextPage
|
||||||
|
}
|
||||||
|
logrus.Infof("get %d PR changed files:", len(commitFiles))
|
||||||
|
for _, file := range commitFiles {
|
||||||
|
files = append(files, *file.Filename)
|
||||||
|
logrus.Infof("%s", *file.Filename)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//generate github comment content based on diff coverage and commentFlag
|
||||||
|
func GenCommentContent(commentPrefix string, delta cover.DeltaCovList) string {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
table := tablewriter.NewWriter(&buf)
|
||||||
|
table.SetHeader([]string{"File", "BASE Coverage", "New Coverage", "Delta"})
|
||||||
|
table.SetAutoFormatHeaders(false)
|
||||||
|
table.SetBorders(tablewriter.Border{Left: true, Top: false, Right: true, Bottom: false})
|
||||||
|
table.SetCenterSeparator("|")
|
||||||
|
table.SetColumnAlignment([]int{tablewriter.ALIGN_LEFT, tablewriter.ALIGN_CENTER, tablewriter.ALIGN_CENTER, tablewriter.ALIGN_CENTER})
|
||||||
|
for _, d := range delta {
|
||||||
|
table.Append([]string{fmt.Sprintf("[%s](%s)", d.FileName, d.LineCovLink), d.BasePer, d.NewPer, d.DeltaPer})
|
||||||
|
}
|
||||||
|
table.Render()
|
||||||
|
|
||||||
|
content := []string{
|
||||||
|
commentPrefix,
|
||||||
|
fmt.Sprintf("Say `/test %s` to re-run this coverage report", os.Getenv("JOB_NAME")),
|
||||||
|
buf.String(),
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(content, "\n")
|
||||||
|
}
|
162
pkg/github/github_test.go
Normal file
162
pkg/github/github_test.go
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2020 Qiniu Cloud (qiniu.com)
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package github
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/go-github/github"
|
||||||
|
"github.com/julienschmidt/httprouter"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
|
"github.com/qiniu/goc/pkg/cover"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// baseURLPath is a non-empty Client.BaseURL path to use during tests,
|
||||||
|
// to ensure relative URLs are used for all endpoints. See issue #752.
|
||||||
|
baseURLPath = "/api-v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// setup sets up a test HTTP server along with a github.Client that is
|
||||||
|
// configured to talk to that test server. Tests should register handlers on
|
||||||
|
// mux which provide mock responses for the API method being tested.
|
||||||
|
func setup() (client *github.Client, router *httprouter.Router, serverURL string, teardown func()) {
|
||||||
|
// router is the HTTP request multiplexer used with the test server.
|
||||||
|
router = httprouter.New()
|
||||||
|
|
||||||
|
// We want to ensure that tests catch mistakes where the endpoint URL is
|
||||||
|
// specified as absolute rather than relative. It only makes a difference
|
||||||
|
// when there's a non-empty base URL path. So, use that. See issue #752.
|
||||||
|
apiHandler := http.NewServeMux()
|
||||||
|
apiHandler.Handle(baseURLPath+"/", http.StripPrefix(baseURLPath, router))
|
||||||
|
apiHandler.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
fmt.Fprintln(os.Stderr, "FAIL: Client.BaseURL path prefix is not preserved in the request URL:")
|
||||||
|
fmt.Fprintln(os.Stderr)
|
||||||
|
fmt.Fprintln(os.Stderr, "\t"+req.URL.String())
|
||||||
|
fmt.Fprintln(os.Stderr)
|
||||||
|
fmt.Fprintln(os.Stderr, "\tDid you accidentally use an absolute endpoint URL rather than relative?")
|
||||||
|
fmt.Fprintln(os.Stderr, "\tSee https://github.com/google/go-github/issues/752 for information.")
|
||||||
|
http.Error(w, "Client.BaseURL path prefix is not preserved in the request URL.", http.StatusInternalServerError)
|
||||||
|
})
|
||||||
|
|
||||||
|
// server is a test HTTP server used to provide mock API responses.
|
||||||
|
server := httptest.NewServer(apiHandler)
|
||||||
|
|
||||||
|
// client is the GitHub client being tested and is
|
||||||
|
// configured to use test server.
|
||||||
|
client = github.NewClient(nil)
|
||||||
|
url, _ := url.Parse(server.URL + baseURLPath + "/")
|
||||||
|
client.BaseURL = url
|
||||||
|
client.UploadURL = url
|
||||||
|
|
||||||
|
return client, router, server.URL, server.Close
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewPrClient(t *testing.T) {
|
||||||
|
items := []struct {
|
||||||
|
token string
|
||||||
|
repoOwner string
|
||||||
|
repoName string
|
||||||
|
prNumStr string
|
||||||
|
botUserName string
|
||||||
|
commentFlag string
|
||||||
|
expectPrNum int
|
||||||
|
}{
|
||||||
|
{token: "github_test.go", repoOwner: "qiniu", repoName: "goc", prNumStr: "1", botUserName: "qiniu-bot", commentFlag: "test", expectPrNum: 1},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range items {
|
||||||
|
prClient := NewPrClient(tc.token, tc.repoOwner, tc.repoName, tc.prNumStr, tc.botUserName, tc.commentFlag)
|
||||||
|
assert.Equal(t, tc.expectPrNum, prClient.PrNumber)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateGithubComment(t *testing.T) {
|
||||||
|
client, router, _, teardown := setup()
|
||||||
|
defer teardown()
|
||||||
|
|
||||||
|
var coverList = cover.DeltaCovList{{FileName: "fake-coverage", BasePer: "50.0%", NewPer: "75.0%", DeltaPer: "25.0%"}}
|
||||||
|
expectContent := GenCommentContent("", coverList)
|
||||||
|
comment := &github.IssueComment{
|
||||||
|
Body: &expectContent,
|
||||||
|
}
|
||||||
|
|
||||||
|
// create comment: https://developer.github.com/v3/issues/comments/#create-a-comment
|
||||||
|
router.HandlerFunc("POST", "/repos/qiniu/goc/issues/1/comments", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
v := new(github.IssueComment)
|
||||||
|
json.NewDecoder(r.Body).Decode(v)
|
||||||
|
assert.Equal(t, v, comment)
|
||||||
|
|
||||||
|
fmt.Fprint(w, `{"id":1}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
// list comment: https://developer.github.com/v3/issues/comments/#list-comments-on-an-issue
|
||||||
|
router.HandlerFunc("GET", "/repos/qiniu/goc/issues/1/comments", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
fmt.Fprint(w, `[{"id":1,"user": {"login": "qiniu-bot"}}]`)
|
||||||
|
})
|
||||||
|
|
||||||
|
// delete comment: https://developer.github.com/v3/issues/comments/#edit-a-comment
|
||||||
|
router.HandlerFunc("DELETE", "/repos/qiniu/goc/issues/comments/1", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
})
|
||||||
|
|
||||||
|
p := PrComment{
|
||||||
|
RobotUserName: "qiniu-bot",
|
||||||
|
RepoOwner: "qiniu",
|
||||||
|
RepoName: "goc",
|
||||||
|
CommentFlag: "",
|
||||||
|
PrNumber: 1,
|
||||||
|
Ctx: context.Background(),
|
||||||
|
opt: nil,
|
||||||
|
GithubClient: client,
|
||||||
|
}
|
||||||
|
|
||||||
|
p.CreateGithubComment("", coverList)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetPrChangedFiles(t *testing.T) {
|
||||||
|
client, router, _, teardown := setup()
|
||||||
|
defer teardown()
|
||||||
|
|
||||||
|
var expectFiles = []string{"src/qiniu.com/kodo/s3apiv2/bucket/bucket.go"}
|
||||||
|
|
||||||
|
// list files API: https://developer.github.com/v3/pulls/#list-pull-requests-files
|
||||||
|
router.HandlerFunc("GET", "/repos/qiniu/goc/pulls/1/files", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
fmt.Fprint(w, `[{"filename":"src/qiniu.com/kodo/s3apiv2/bucket/bucket.go"}]`)
|
||||||
|
})
|
||||||
|
|
||||||
|
p := PrComment{
|
||||||
|
RobotUserName: "qiniu-bot",
|
||||||
|
RepoOwner: "qiniu",
|
||||||
|
RepoName: "goc",
|
||||||
|
CommentFlag: "",
|
||||||
|
PrNumber: 1,
|
||||||
|
Ctx: context.Background(),
|
||||||
|
opt: nil,
|
||||||
|
GithubClient: client,
|
||||||
|
}
|
||||||
|
changedFiles, err := p.GetPrChangedFiles()
|
||||||
|
assert.Equal(t, err, nil)
|
||||||
|
assert.Equal(t, changedFiles, expectFiles)
|
||||||
|
}
|
232
pkg/prow/job.go
Normal file
232
pkg/prow/job.go
Normal file
@ -0,0 +1,232 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2020 Qiniu Cloud (qiniu.com)
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
package prow
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/qiniu/goc/pkg/cover"
|
||||||
|
"github.com/qiniu/goc/pkg/github"
|
||||||
|
"github.com/qiniu/goc/pkg/qiniu"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IProwAction defines the normal action in prow system
|
||||||
|
type IProwAction interface {
|
||||||
|
Fetch(BuildID, name string) []byte
|
||||||
|
RunPresubmit() error
|
||||||
|
RunPostsubmit() error
|
||||||
|
RunPeriodic() error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Job is a prowjob in prow
|
||||||
|
type Job struct {
|
||||||
|
JobName string
|
||||||
|
Org string
|
||||||
|
RepoName string
|
||||||
|
PRNumStr string
|
||||||
|
BuildId string //prow job build number
|
||||||
|
PostSubmitJob string
|
||||||
|
PostSubmitCoverProfile string
|
||||||
|
CovThreshold int
|
||||||
|
LocalProfilePath string
|
||||||
|
QiniuClient *qiniu.Client
|
||||||
|
LocalArtifacts *qiniu.Artifacts
|
||||||
|
GithubComment *github.PrComment
|
||||||
|
FullDiff bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch the file from cloud
|
||||||
|
func (j *Job) Fetch(BuildID, name string) []byte {
|
||||||
|
return []byte{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunPresubmit run a presubmit job
|
||||||
|
func (j *Job) RunPresubmit() error {
|
||||||
|
var changedFiles []string
|
||||||
|
var deltaCovList cover.DeltaCovList
|
||||||
|
|
||||||
|
// step1: get github pull request changed files' name
|
||||||
|
if !j.FullDiff {
|
||||||
|
var ghChangedFiles, err = j.GithubComment.GetPrChangedFiles()
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Fatalf("Get pull request changed file failed.")
|
||||||
|
}
|
||||||
|
if len(ghChangedFiles) == 0 {
|
||||||
|
logrus.Printf("0 files changed in github pull request, don't need to run coverage profile in presubmit.\n")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
changedFiles = trimGhFileToProfile(ghChangedFiles)
|
||||||
|
}
|
||||||
|
|
||||||
|
// step2: get local profile cov
|
||||||
|
localP, err := cover.ReadFileToCoverList(j.LocalProfilePath)
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Fatalf("failed to get remote cover profile")
|
||||||
|
}
|
||||||
|
|
||||||
|
//step3: find the remote healthy cover profile from qiniu bucket
|
||||||
|
remoteProfile, err := qiniu.FindBaseProfileFromQiniu(j.QiniuClient, j.PostSubmitJob, j.PostSubmitCoverProfile)
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Fatalf("failed to get remote cover profile")
|
||||||
|
}
|
||||||
|
if remoteProfile == nil {
|
||||||
|
logrus.Infof("get non healthy remoteProfile, do nothing")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
baseP, err := cover.CovList(bytes.NewReader(remoteProfile))
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Fatalf("failed to get remote cover profile")
|
||||||
|
}
|
||||||
|
|
||||||
|
// step4: calculate diff cov between local and remote profile
|
||||||
|
if !j.FullDiff {
|
||||||
|
deltaCovList = cover.GetChFileDeltaCov(localP, baseP, changedFiles)
|
||||||
|
} else {
|
||||||
|
deltaCovList = cover.GetDeltaCov(localP, baseP)
|
||||||
|
logrus.Infof("get delta file name is:")
|
||||||
|
for _, d := range deltaCovList {
|
||||||
|
logrus.Infof("%s", d.FileName)
|
||||||
|
changedFiles = append(changedFiles, d.FileName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// step5: generate changed file html coverage
|
||||||
|
err = j.WriteChangedCov(changedFiles)
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Fatalf("filter local profile to %s with changed files failed", j.LocalArtifacts.ChangedProfileName)
|
||||||
|
}
|
||||||
|
err = j.CreateChangedCovHtml()
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Fatalf("create changed file related coverage html failed")
|
||||||
|
}
|
||||||
|
j.SetDeltaCovLinks(deltaCovList)
|
||||||
|
|
||||||
|
// step6: post comment to github
|
||||||
|
commentPrefix := github.CommentsPrefix
|
||||||
|
if j.GithubComment.CommentFlag != "" {
|
||||||
|
commentPrefix = fmt.Sprintf("**%s** ", j.GithubComment.CommentFlag) + commentPrefix
|
||||||
|
}
|
||||||
|
if len(deltaCovList) > 0 {
|
||||||
|
totalDelta := cover.PercentStr(cover.TotalDelta(localP, baseP))
|
||||||
|
deltaCovList = append(deltaCovList, cover.DeltaCov{FileName: "Total", BasePer: baseP.TotalPercentage(), NewPer: localP.TotalPercentage(), DeltaPer: totalDelta})
|
||||||
|
}
|
||||||
|
err = j.GithubComment.CreateGithubComment(commentPrefix, deltaCovList)
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Fatalf("Post comment to github failed.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunPostsubmit run a postsubmit job
|
||||||
|
func (j *Job) RunPostsubmit() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunPeriodic run a periodic job
|
||||||
|
func (j *Job) RunPeriodic() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//trim github filename to profile format:
|
||||||
|
// src/qiniu.com/kodo/io/io/io_svr.go -> qiniu.com/kodo/io/io/io_svr.go
|
||||||
|
func trimGhFileToProfile(ghFiles []string) (pFiles []string) {
|
||||||
|
//TODO: need compatible other situation
|
||||||
|
logrus.Infof("trim PR changed file name to:")
|
||||||
|
for _, f := range ghFiles {
|
||||||
|
file := strings.TrimPrefix(f, "src/")
|
||||||
|
logrus.Infof("%s", file)
|
||||||
|
pFiles = append(pFiles, file)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// filter local profile with changed files and save to j.LocalArtifacts.ChangedProfileName
|
||||||
|
func (j *Job) WriteChangedCov(changedFiles []string) error {
|
||||||
|
p, err := ioutil.ReadFile(j.LocalProfilePath)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Printf("Open file %s failed", j.LocalProfilePath)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cp := j.LocalArtifacts.CreateChangedProfile()
|
||||||
|
defer cp.Close()
|
||||||
|
s := bufio.NewScanner(bytes.NewReader(p))
|
||||||
|
s.Scan()
|
||||||
|
writeLine(cp, s.Text())
|
||||||
|
|
||||||
|
for s.Scan() {
|
||||||
|
for _, file := range changedFiles {
|
||||||
|
if strings.HasPrefix(s.Text(), file) {
|
||||||
|
writeLine(cp, s.Text())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeLine writes a line in the given file, if the file pointer is not nil
|
||||||
|
func writeLine(file *os.File, content string) {
|
||||||
|
if file != nil {
|
||||||
|
fmt.Fprintln(file, content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *Job) JobPrefixOnQiniu() string {
|
||||||
|
return path.Join("pr-logs", "pull", j.Org+"_"+j.RepoName, j.PRNumStr, j.JobName, j.BuildId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *Job) HtmlProfile() string {
|
||||||
|
return fmt.Sprintf("%s-%s-pr%s-coverage.html", j.Org, j.RepoName, j.PRNumStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *Job) SetDeltaCovLinks(c cover.DeltaCovList) {
|
||||||
|
c.Sort()
|
||||||
|
for i := 0; i < len(c); i++ {
|
||||||
|
qnKey := path.Join(j.JobPrefixOnQiniu(), "artifacts", j.HtmlProfile())
|
||||||
|
authQnKey := j.QiniuClient.GetAccessURL(qnKey, time.Hour*24*7)
|
||||||
|
c[i].SetLineCovLink(authQnKey + "#file" + strconv.Itoa(i))
|
||||||
|
logrus.Printf("file %s html coverage link is: %s\n", c[i].FileName, c[i].GetLineCovLink())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateChangedCovHtml create changed file related coverage html base on the local artifact
|
||||||
|
func (j *Job) CreateChangedCovHtml() error {
|
||||||
|
if j.LocalArtifacts.ChangedProfileName == "" {
|
||||||
|
logrus.Errorf("param LocalArtifacts.ChangedProfileName is empty")
|
||||||
|
}
|
||||||
|
pathProfileCov := j.LocalArtifacts.ChangedProfileName
|
||||||
|
pathHtmlCov := path.Join(os.Getenv("ARTIFACTS"), j.HtmlProfile())
|
||||||
|
cmdTxt := fmt.Sprintf("go tool cover -html=%s -o %s", pathProfileCov, pathHtmlCov)
|
||||||
|
logrus.Printf("Running command '%s'\n", cmdTxt)
|
||||||
|
cmd := exec.Command("go", "tool", "cover", "-html="+pathProfileCov, "-o", pathHtmlCov)
|
||||||
|
stdOut, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
logrus.Printf("Error executing cmd: %v; combinedOutput=%s", err, stdOut)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
141
pkg/prow/job_test.go
Normal file
141
pkg/prow/job_test.go
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2020 Qiniu Cloud (qiniu.com)
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package prow
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/qiniu/goc/pkg/github"
|
||||||
|
"github.com/qiniu/goc/pkg/qiniu"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTrimGhFileToProfile(t *testing.T) {
|
||||||
|
items := []struct {
|
||||||
|
inputFiles []string
|
||||||
|
expectFiles []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
inputFiles: []string{"src/qiniu.com/kodo/io/io/io_svr.go", "README.md"},
|
||||||
|
expectFiles: []string{"qiniu.com/kodo/io/io/io_svr.go", "README.md"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range items {
|
||||||
|
f := trimGhFileToProfile(tc.inputFiles)
|
||||||
|
assert.Equal(t, f, tc.expectFiles)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setup(path, content string) {
|
||||||
|
err := ioutil.WriteFile(path, []byte(content), 0644)
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Fatalf("write file %s failed", path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWriteChangedCov(t *testing.T) {
|
||||||
|
path := "local.cov"
|
||||||
|
savePath := qiniu.ChangedProfileName
|
||||||
|
content := `mode: atomic
|
||||||
|
qiniu.com/kodo/bd/bdgetter/source.go:19.118,22.2 2 0
|
||||||
|
qiniu.com/kodo/bd/bdgetter/source.go:37.34,39.2 1 0
|
||||||
|
qiniu.com/kodo/bd/pfd/locker/app/qboxbdlocker/main.go:50.2,53.52 4 1
|
||||||
|
qiniu.com/kodo/bd/pfd/locker/bdlocker/locker.go:33.51,35.2 1 0`
|
||||||
|
changedFiles := []string{"qiniu.com/kodo/bd/pfd/locker/bdlocker/locker.go"}
|
||||||
|
expectContent := `mode: atomic
|
||||||
|
qiniu.com/kodo/bd/pfd/locker/bdlocker/locker.go:33.51,35.2 1 0
|
||||||
|
`
|
||||||
|
|
||||||
|
setup(path, content)
|
||||||
|
defer os.Remove(path)
|
||||||
|
defer os.Remove(savePath)
|
||||||
|
j := &Job{
|
||||||
|
LocalProfilePath: path,
|
||||||
|
LocalArtifacts: &qiniu.Artifacts{ChangedProfileName: savePath},
|
||||||
|
}
|
||||||
|
j.WriteChangedCov(changedFiles)
|
||||||
|
|
||||||
|
r, err := ioutil.ReadFile(savePath)
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Fatalf("read file %s failed", path)
|
||||||
|
}
|
||||||
|
assert.Equal(t, string(r), expectContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunPresubmitFulldiff(t *testing.T) {
|
||||||
|
//param
|
||||||
|
org := "qbox"
|
||||||
|
repo := "kodo"
|
||||||
|
prNum := "1"
|
||||||
|
buildId := "1266322425771986946"
|
||||||
|
jobName := "kodo-pull-integration-test"
|
||||||
|
robotName := "qiniu-bot"
|
||||||
|
githubCommentPrefix := ""
|
||||||
|
githubTokenPath := "token"
|
||||||
|
|
||||||
|
//mock local profile
|
||||||
|
pwd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Fatalf("get pwd failed")
|
||||||
|
}
|
||||||
|
localPath := "local.cov"
|
||||||
|
localProfileContent := `mode: atomic
|
||||||
|
"qiniu.com/kodo/apiserver/server/main.go:32.49,33.13 1 30
|
||||||
|
"qiniu.com/kodo/apiserver/server/main.go:42.49,43.13 1 0`
|
||||||
|
setup(localPath, localProfileContent)
|
||||||
|
defer os.Remove(path.Join(pwd, localPath))
|
||||||
|
|
||||||
|
// mock qiniu
|
||||||
|
conf := qiniu.Config{
|
||||||
|
Bucket: "artifacts",
|
||||||
|
}
|
||||||
|
qc, router, _, teardown := qiniu.MockQiniuServer(&conf)
|
||||||
|
defer teardown()
|
||||||
|
qiniu.MockRouterAPI(router, localProfileContent)
|
||||||
|
|
||||||
|
ChangedProfilePath := "changed.cov"
|
||||||
|
defer os.Remove(path.Join(pwd, ChangedProfilePath))
|
||||||
|
|
||||||
|
//mock github client
|
||||||
|
setup(githubTokenPath, "")
|
||||||
|
defer os.Remove(path.Join(pwd, githubTokenPath))
|
||||||
|
prClient := github.NewPrClient(githubTokenPath, org, repo, prNum, robotName, githubCommentPrefix)
|
||||||
|
|
||||||
|
j := &Job{
|
||||||
|
JobName: jobName,
|
||||||
|
Org: org,
|
||||||
|
RepoName: repo,
|
||||||
|
PRNumStr: prNum,
|
||||||
|
BuildId: buildId,
|
||||||
|
PostSubmitJob: "kodo-postsubmits-go-st-coverage",
|
||||||
|
PostSubmitCoverProfile: "filterd.cov",
|
||||||
|
LocalProfilePath: localPath,
|
||||||
|
LocalArtifacts: &qiniu.Artifacts{ChangedProfileName: ChangedProfilePath},
|
||||||
|
QiniuClient: qc,
|
||||||
|
GithubComment: prClient,
|
||||||
|
FullDiff: true,
|
||||||
|
}
|
||||||
|
defer os.Remove(path.Join(os.Getenv("ARTIFACTS"), j.HtmlProfile()))
|
||||||
|
|
||||||
|
j.RunPresubmit()
|
||||||
|
}
|
241
pkg/qiniu/client.go
Normal file
241
pkg/qiniu/client.go
Normal file
@ -0,0 +1,241 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2020 Qiniu Cloud (qiniu.com)
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package qiniu
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"math/rand"
|
||||||
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/qiniu/api.v7/v7/auth/qbox"
|
||||||
|
"github.com/qiniu/api.v7/v7/client"
|
||||||
|
"github.com/qiniu/api.v7/v7/storage"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config store the credentials to connect with qiniu cloud
|
||||||
|
type Config struct {
|
||||||
|
Bucket string `json:"bucket"`
|
||||||
|
AccessKey string `json:"accessKey"`
|
||||||
|
SecretKey string `json:"secretKey"`
|
||||||
|
|
||||||
|
// domain used to download files from qiniu cloud
|
||||||
|
Domain string `json:"domain"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client for the operation with qiniu cloud
|
||||||
|
type Client struct {
|
||||||
|
cfg *Config
|
||||||
|
BucketManager *storage.BucketManager
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClient creates a new client to work with qiniu cloud
|
||||||
|
func NewClient(cfg *Config) *Client {
|
||||||
|
return &Client{
|
||||||
|
cfg: cfg,
|
||||||
|
BucketManager: storage.NewBucketManager(qbox.NewMac(cfg.AccessKey, cfg.SecretKey), nil),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// QiniuObjectHandle construct a object hanle to access file in qiniu
|
||||||
|
func (q *Client) QiniuObjectHandle(key string) *ObjectHandle {
|
||||||
|
return &ObjectHandle{
|
||||||
|
key: key,
|
||||||
|
cfg: q.cfg,
|
||||||
|
bm: q.BucketManager,
|
||||||
|
mac: qbox.NewMac(q.cfg.AccessKey, q.cfg.SecretKey),
|
||||||
|
client: &client.Client{Client: http.DefaultClient},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadObject to read all the content of key
|
||||||
|
func (q *Client) ReadObject(key string) ([]byte, error) {
|
||||||
|
objectHandle := q.QiniuObjectHandle(key)
|
||||||
|
reader, err := objectHandle.NewReader(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error getting qiniu artifact reader: %v", err)
|
||||||
|
}
|
||||||
|
defer reader.Close()
|
||||||
|
return ioutil.ReadAll(reader)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListAll to list all the files with contains the expected prefix
|
||||||
|
func (q *Client) ListAll(ctx context.Context, prefix string, delimiter string) ([]string, error) {
|
||||||
|
var files []string
|
||||||
|
artifacts, err := q.listEntries(prefix, delimiter)
|
||||||
|
if err != nil {
|
||||||
|
return files, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, item := range artifacts {
|
||||||
|
files = append(files, item.Key)
|
||||||
|
}
|
||||||
|
|
||||||
|
return files, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListAll to list all the entries with contains the expected prefix
|
||||||
|
func (q *Client) listEntries(prefix string, delimiter string) ([]storage.ListItem, error) {
|
||||||
|
var marker string
|
||||||
|
var artifacts []storage.ListItem
|
||||||
|
|
||||||
|
wait := []time.Duration{16, 32, 64, 128, 256, 256, 512, 512}
|
||||||
|
for i := 0; ; {
|
||||||
|
entries, _, nextMarker, hashNext, err := q.BucketManager.ListFiles(q.cfg.Bucket, prefix, delimiter, marker, 500)
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithField("prefix", prefix).WithError(err).Error("Error accessing QINIU artifact.")
|
||||||
|
if i >= len(wait) {
|
||||||
|
return artifacts, fmt.Errorf("timed out: error accessing QINIU artifact: %v", err)
|
||||||
|
}
|
||||||
|
time.Sleep((wait[i] + time.Duration(rand.Intn(10))) * time.Millisecond)
|
||||||
|
i++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
artifacts = append(artifacts, entries...)
|
||||||
|
|
||||||
|
if hashNext {
|
||||||
|
marker = nextMarker
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return artifacts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAccessURL return a url which can access artifact directly in qiniu
|
||||||
|
func (q *Client) GetAccessURL(key string, timeout time.Duration) string {
|
||||||
|
deadline := time.Now().Add(timeout).Unix()
|
||||||
|
return storage.MakePrivateURL(qbox.NewMac(q.cfg.AccessKey, q.cfg.SecretKey), q.cfg.Domain, key, deadline)
|
||||||
|
}
|
||||||
|
|
||||||
|
type LogHistoryTemplate struct {
|
||||||
|
BucketName string
|
||||||
|
KeyPath string
|
||||||
|
Items []logHistoryItem
|
||||||
|
}
|
||||||
|
|
||||||
|
type logHistoryItem struct {
|
||||||
|
Name string
|
||||||
|
Size string
|
||||||
|
Time string
|
||||||
|
Url string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Artifacts lists all artifacts available for the given job source
|
||||||
|
func (q *Client) GetArtifactDetails(key string) (*LogHistoryTemplate, error) {
|
||||||
|
tmpl := new(LogHistoryTemplate)
|
||||||
|
item := logHistoryItem{}
|
||||||
|
listStart := time.Now()
|
||||||
|
artifacts, err := q.listEntries(key, "")
|
||||||
|
if err != nil {
|
||||||
|
return tmpl, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, entry := range artifacts {
|
||||||
|
item.Name = splitKey(entry.Key, key)
|
||||||
|
item.Size = size(entry.Fsize)
|
||||||
|
item.Time = timeConv(entry.PutTime)
|
||||||
|
item.Url = q.GetAccessURL(entry.Key, time.Duration(time.Second*60*60))
|
||||||
|
tmpl.Items = append(tmpl.Items, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.WithField("duration", time.Since(listStart).String()).Infof("Listed %d artifacts.", len(tmpl.Items))
|
||||||
|
return tmpl, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func splitKey(item, key string) string {
|
||||||
|
return strings.TrimPrefix(item, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func size(fsize int64) string {
|
||||||
|
return strings.Join([]string{strconv.FormatInt(fsize, 10), "bytes"}, " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func timeConv(ptime int64) string {
|
||||||
|
s := strconv.FormatInt(ptime, 10)[0:10]
|
||||||
|
t, err := strconv.ParseInt(s, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Errorf("time string parse int error : %v", err)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
tm := time.Unix(t, 0)
|
||||||
|
return tm.Format("2006-01-02 03:04:05 PM")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Client) ListSubDirs(prefix string) ([]string, error) {
|
||||||
|
var dirs []string
|
||||||
|
var marker string
|
||||||
|
|
||||||
|
wait := []time.Duration{16, 32, 64, 128, 256, 256, 512, 512}
|
||||||
|
for i := 0; ; {
|
||||||
|
// use rsf list v2 interface to get the sub folder based on the delimiter
|
||||||
|
entries, err := q.BucketManager.ListBucketContext(context.Background(), q.cfg.Bucket, prefix, "/", marker)
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithField("prefix", prefix).WithError(err).Error("Error accessing QINIU artifact.")
|
||||||
|
if i >= len(wait) {
|
||||||
|
return dirs, fmt.Errorf("timed out: error accessing QINIU artifact: %v", err)
|
||||||
|
}
|
||||||
|
time.Sleep((wait[i] + time.Duration(rand.Intn(10))) * time.Millisecond)
|
||||||
|
i++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for entry := range entries {
|
||||||
|
if entry.Dir != "" {
|
||||||
|
// entry.Dir should be like "logs/kodo-periodics-integration-test/1181915661132107776/"
|
||||||
|
// the sub folder is 1181915661132107776, also known as prowjob buildid.
|
||||||
|
buildId := getBuildId(entry.Dir)
|
||||||
|
if buildId != "" {
|
||||||
|
dirs = append(dirs, buildId)
|
||||||
|
} else {
|
||||||
|
logrus.Warnf("invalid dir format: %v", entry.Dir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
marker = entry.Marker
|
||||||
|
}
|
||||||
|
|
||||||
|
if marker != "" {
|
||||||
|
i = 0
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dirs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var nonPRLogsBuildIdSubffixRe = regexp.MustCompile("([0-9]+)/$")
|
||||||
|
|
||||||
|
// extract the build number from dir path
|
||||||
|
// expect the dir as the following formats:
|
||||||
|
// 1. logs/kodo-periodics-integration-test/1181915661132107776/
|
||||||
|
func getBuildId(dir string) string {
|
||||||
|
matches := nonPRLogsBuildIdSubffixRe.FindStringSubmatch(dir)
|
||||||
|
if len(matches) == 2 {
|
||||||
|
return matches[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
40
pkg/qiniu/client_test.go
Normal file
40
pkg/qiniu/client_test.go
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2020 Qiniu Cloud (qiniu.com)
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package qiniu
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestGetBuildId(t *testing.T) {
|
||||||
|
type tc struct {
|
||||||
|
dir string
|
||||||
|
expected string
|
||||||
|
}
|
||||||
|
|
||||||
|
tcs := []tc{
|
||||||
|
{dir: "logs/kodo-periodics-integration-test/1181915661132107776/", expected: "1181915661132107776"},
|
||||||
|
{dir: "logs/kodo-periodics-integration-test/1181915661132107776", expected: ""},
|
||||||
|
{dir: "pr-logs/directory/WIP-qtest-pull-request-kodo-test/1181915661132107776/", expected: "1181915661132107776"},
|
||||||
|
{dir: "pr-logs/directory/WIP-qtest-pull-request-kodo-test/1181915661132107776.txt", expected: ""},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tcs {
|
||||||
|
got := getBuildId(tc.dir)
|
||||||
|
if tc.expected != got {
|
||||||
|
t.Errorf("getBuildId error, dir: %s, expect: %s, but got: %s", tc.dir, tc.expected, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
77
pkg/qiniu/mock.go
Normal file
77
pkg/qiniu/mock.go
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2020 Qiniu Cloud (qiniu.com)
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package qiniu
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
|
||||||
|
"github.com/julienschmidt/httprouter"
|
||||||
|
"github.com/qiniu/api.v7/v7/storage"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
func MockQiniuServer(config *Config) (client *Client, router *httprouter.Router, serverURL string, teardown func()) {
|
||||||
|
// router is the HTTP request multiplexer used with the test server.
|
||||||
|
router = httprouter.New()
|
||||||
|
|
||||||
|
// server is a test HTTP server used to provide mock API responses.
|
||||||
|
server := httptest.NewServer(router)
|
||||||
|
|
||||||
|
config.Domain = server.URL
|
||||||
|
client = NewClient(config)
|
||||||
|
client.BucketManager.Cfg = &storage.Config{
|
||||||
|
RsfHost: server.URL,
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.Infof("server url is: %s", server.URL)
|
||||||
|
return client, router, server.URL, server.Close
|
||||||
|
}
|
||||||
|
|
||||||
|
func MockRouterAPI(router *httprouter.Router, profile string) {
|
||||||
|
// mock rsf /v2/list
|
||||||
|
router.HandlerFunc("POST", "/v2/list", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
logrus.Infof("request url is: %s", r.URL.String())
|
||||||
|
|
||||||
|
fmt.Fprint(w, `{
|
||||||
|
"item": {
|
||||||
|
"key": "logs/kodo-postsubmits-go-st-coverage/1181915661132107776/finished.json",
|
||||||
|
"hash": "FkBhdo9odL2Xjvu-YdwtDIw79fIL",
|
||||||
|
"fsize": 51523,
|
||||||
|
"mimeType": "application/octet-stream",
|
||||||
|
"putTime": 15909068578047958,
|
||||||
|
"type": 0,
|
||||||
|
"status": 0,
|
||||||
|
"md5": "e0bd20e97ea1c6a5e2480192ee3ae884"
|
||||||
|
},
|
||||||
|
"marker": "",
|
||||||
|
"dir": "logs/kodo-postsubmits-go-st-coverage/1181915661132107776/"
|
||||||
|
}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
// mock io get statusJSON file
|
||||||
|
router.HandlerFunc("GET", "/logs/kodo-postsubmits-go-st-coverage/1181915661132107776/finished.json", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
fmt.Fprint(w, `{"timestamp":1590750306,"passed":true,"result":"SUCCESS","repo-version":"76433418ea48aae57af028f9cb2fa3735ce08c7d"}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
// mock io get remote coverage profile
|
||||||
|
router.HandlerFunc("GET", "/logs/kodo-postsubmits-go-st-coverage/1181915661132107776/artifacts/filterd.cov", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
fmt.Fprint(w, profile)
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
124
pkg/qiniu/object.go
Normal file
124
pkg/qiniu/object.go
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2020 Qiniu Cloud (qiniu.com)
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package qiniu
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/qiniu/api.v7/v7/auth/qbox"
|
||||||
|
"github.com/qiniu/api.v7/v7/client"
|
||||||
|
"github.com/qiniu/api.v7/v7/storage"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ObjectHandle provides operations on an object in a qiniu cloud bucket
|
||||||
|
type ObjectHandle struct {
|
||||||
|
key string
|
||||||
|
cfg *Config
|
||||||
|
bm *storage.BucketManager
|
||||||
|
mac *qbox.Mac
|
||||||
|
client *client.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *ObjectHandle) Attrs(ctx context.Context) (storage.FileInfo, error) {
|
||||||
|
//TODO(CarlJi): need retry when errors
|
||||||
|
return o.bm.Stat(o.cfg.Bucket, o.key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewReader creates a reader to read the contents of the object.
|
||||||
|
// ErrObjectNotExist will be returned if the object is not found.
|
||||||
|
// The caller must call Close on the returned Reader when done reading.
|
||||||
|
func (o *ObjectHandle) NewReader(ctx context.Context) (io.ReadCloser, error) {
|
||||||
|
return o.NewRangeReader(ctx, 0, -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRangeReader reads parts of an object, reading at most length bytes starting
|
||||||
|
// from the given offset. If length is negative, the object is read until the end.
|
||||||
|
func (o *ObjectHandle) NewRangeReader(ctx context.Context, offset, length int64) (io.ReadCloser, error) {
|
||||||
|
verb := "GET"
|
||||||
|
if length == 0 {
|
||||||
|
verb = "HEAD"
|
||||||
|
}
|
||||||
|
|
||||||
|
var res *http.Response
|
||||||
|
var err error
|
||||||
|
|
||||||
|
err = runWithRetry(3, func() (bool, error) {
|
||||||
|
headers := http.Header{}
|
||||||
|
start := offset
|
||||||
|
if length < 0 && start >= 0 {
|
||||||
|
headers.Set("Range", fmt.Sprintf("bytes=%d-", start))
|
||||||
|
} else if length > 0 {
|
||||||
|
// The end character isn't affected by how many bytes we have seen.
|
||||||
|
headers.Set("Range", fmt.Sprintf("bytes=%d-%d", start, offset+length-1))
|
||||||
|
}
|
||||||
|
|
||||||
|
deadline := time.Now().Add(time.Second * 60 * 10).Unix()
|
||||||
|
accessURL := storage.MakePrivateURL(o.mac, o.cfg.Domain, o.key, deadline)
|
||||||
|
res, err = o.client.DoRequest(ctx, verb, accessURL, headers)
|
||||||
|
if err != nil {
|
||||||
|
time.Sleep(time.Second) //TODO enhance
|
||||||
|
return true, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.StatusCode == http.StatusNotFound {
|
||||||
|
res.Body.Close()
|
||||||
|
return true, fmt.Errorf("qiniu storage: object not exists")
|
||||||
|
}
|
||||||
|
|
||||||
|
return shouldRetry(res), nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.Body, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func runWithRetry(maxTry int, f func() (bool, error)) error {
|
||||||
|
var err error
|
||||||
|
for maxTry > 0 {
|
||||||
|
needRetry, err := f()
|
||||||
|
if err != nil {
|
||||||
|
logrus.Warnf("err occurred: %v. try again", err)
|
||||||
|
} else if needRetry {
|
||||||
|
logrus.Warn("results do not meet the expectation. try again")
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
time.Sleep(time.Millisecond * 100)
|
||||||
|
maxTry = maxTry - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func shouldRetry(res *http.Response) bool {
|
||||||
|
|
||||||
|
// 571 and 573 mean the request was limited by cloud storage because of concurrency count exceed
|
||||||
|
// so it's better to retry after a while
|
||||||
|
if res.StatusCode == 571 || res.StatusCode == 573 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
130
pkg/qiniu/qnPresubmit.go
Normal file
130
pkg/qiniu/qnPresubmit.go
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2020 Qiniu Cloud (qiniu.com)
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package qiniu
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
//statusJSON is the JSON file that stores build success info
|
||||||
|
statusJSON = "finished.json"
|
||||||
|
|
||||||
|
// ArtifactsDirName is the name of directory defined in prow to store test artifacts
|
||||||
|
ArtifactsDirName = "artifacts"
|
||||||
|
|
||||||
|
//default prow coverage file
|
||||||
|
PostSubmitCoverProfile = "filtered.cov"
|
||||||
|
|
||||||
|
//default to save changed file related coverage profile
|
||||||
|
ChangedProfileName = "changed-file-profile.cov"
|
||||||
|
)
|
||||||
|
|
||||||
|
// sortBuilds converts all build from str to int and sorts all builds in descending order and
|
||||||
|
// returns the sorted slice
|
||||||
|
func sortBuilds(strBuilds []string) []int {
|
||||||
|
var res []int
|
||||||
|
for _, buildStr := range strBuilds {
|
||||||
|
num, err := strconv.Atoi(buildStr)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Non-int build number found: '%s'", buildStr)
|
||||||
|
} else {
|
||||||
|
res = append(res, num)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Sort(sort.Reverse(sort.IntSlice(res)))
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
type finishedStatus struct {
|
||||||
|
Timestamp int
|
||||||
|
Passed bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func isBuildSucceeded(jsonText []byte) bool {
|
||||||
|
var status finishedStatus
|
||||||
|
err := json.Unmarshal(jsonText, &status)
|
||||||
|
return err == nil && status.Passed
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindBaseProfileFromQiniu finds the coverage profile file from the latest healthy build
|
||||||
|
// stored in given gcs directory
|
||||||
|
func FindBaseProfileFromQiniu(qc *Client, prowJobName, covProfileName string) ([]byte, error) {
|
||||||
|
dirOfJob := path.Join("logs", prowJobName)
|
||||||
|
prefix := dirOfJob + "/"
|
||||||
|
strBuilds, err := qc.ListSubDirs(prefix)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error listing qiniu objects, prowjob:%v, err:%v", prowJobName, err)
|
||||||
|
}
|
||||||
|
if len(strBuilds) == 0 {
|
||||||
|
log.Printf("no cover profiles found from remote, do nothing")
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
log.Printf("total sub dirs: %d", len(strBuilds))
|
||||||
|
|
||||||
|
builds := sortBuilds(strBuilds)
|
||||||
|
profilePath := ""
|
||||||
|
for _, build := range builds {
|
||||||
|
buildDirPath := path.Join(dirOfJob, strconv.Itoa(build))
|
||||||
|
dirOfStatusJSON := path.Join(buildDirPath, statusJSON)
|
||||||
|
|
||||||
|
statusText, err := qc.ReadObject(dirOfStatusJSON)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Cannot read finished.json (%s) ", dirOfStatusJSON)
|
||||||
|
} else if isBuildSucceeded(statusText) {
|
||||||
|
artifactsDirPath := path.Join(buildDirPath, ArtifactsDirName)
|
||||||
|
profilePath = path.Join(artifactsDirPath, covProfileName)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if profilePath == "" {
|
||||||
|
log.Printf("no cover profiles found from remote job %s, do nothing", prowJobName)
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("base cover profile path: %s", profilePath)
|
||||||
|
return qc.ReadObject(profilePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Artifacts struct {
|
||||||
|
Directory string
|
||||||
|
ProfileName string
|
||||||
|
ChangedProfileName string // create temporary to save changed file related coverage profile
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Artifacts) ProfilePath() string {
|
||||||
|
return path.Join(a.Directory, a.ProfileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Artifacts) CreateChangedProfile() *os.File {
|
||||||
|
if a.ChangedProfileName == "" {
|
||||||
|
log.Fatalf("param Artifacts.ChangedProfileName should not be empty")
|
||||||
|
}
|
||||||
|
p, err := os.Create(a.ChangedProfileName)
|
||||||
|
log.Printf("os create: %s", a.ChangedProfileName)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("file(%s) create failed: %v", a.ChangedProfileName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return p
|
||||||
|
}
|
41
pkg/qiniu/qnPresubmit_test.go
Normal file
41
pkg/qiniu/qnPresubmit_test.go
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2020 Qiniu Cloud (qiniu.com)
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package qiniu
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFindBaseProfileFromQiniu(t *testing.T) {
|
||||||
|
conf := Config{
|
||||||
|
Bucket: "artifacts",
|
||||||
|
}
|
||||||
|
qc, router, _, teardown := MockQiniuServer(&conf)
|
||||||
|
defer teardown()
|
||||||
|
prowJobName := "kodo-postsubmits-go-st-coverage"
|
||||||
|
covProfileName := "filterd.cov"
|
||||||
|
mockProfileContent := `mode: atomic
|
||||||
|
"qiniu.com/kodo/apiserver/server/main.go:32.49,33.13 1 30
|
||||||
|
"qiniu.com/kodo/apiserver/server/main.go:42.49,43.13 1 0`
|
||||||
|
|
||||||
|
MockRouterAPI(router, mockProfileContent)
|
||||||
|
getProfile, err := FindBaseProfileFromQiniu(qc, prowJobName, covProfileName)
|
||||||
|
assert.Equal(t, err, nil)
|
||||||
|
assert.Equal(t, string(getProfile), mockProfileContent)
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user