From 381b503b217456384a791171d389ec4067c535d3 Mon Sep 17 00:00:00 2001 From: Zaq? Wiedmann Date: Sat, 14 Apr 2018 02:34:43 -0700 Subject: [PATCH] (ci_trace,tests) test selecting and stream job logs --- Gopkg.lock | 2 +- Makefile | 11 ++--- README.md | 1 - cmd/ciTrace.go | 50 ++++++++++++++-------- cmd/ciTrace_test.go | 90 +++++++++++++++++++++++++++++++++++++++ internal/gitlab/gitlab.go | 51 +++++++++++++--------- 6 files changed, 157 insertions(+), 48 deletions(-) create mode 100644 cmd/ciTrace_test.go diff --git a/Gopkg.lock b/Gopkg.lock index 2c21147e..546310cd 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -195,6 +195,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "f902c44fdfda24711d2fc77ec47941bc42dc0a0f1aad18922657e7ef5b1542f0" + inputs-digest = "527d516bd56c49e223b731ef212110897888e48d361c5a0b2776fa114ab3a0b8" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Makefile b/Makefile index 9faf2dd2..51fa56a8 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,6 @@ install: - go install -ldflags "-X \"main.version=$$(git rev-parse --short=10 HEAD)\"" github.com/zaquestion/lab - -dep: - go get -u -d github.com/zaquestion/lab - cd $GOPATH/src/github.com/zaquestion/lab dep ensure + go install -ldflags "-X \"main.version=$$(git rev-parse --short=10 HEAD)\"" github.com/zaquestion/lab test: bash -c "trap 'trap - SIGINT SIGTERM ERR; mv testdata/.git testdata/test.git; rm coverage-* 2>&1 > /dev/null; exit 1' SIGINT SIGTERM ERR; $(MAKE) internal-test" @@ -13,10 +9,9 @@ internal-test: dep ensure rm coverage-* 2>&1 > /dev/null || true mv testdata/test.git testdata/.git - go test -coverprofile=coverage-git.out -covermode=count github.com/zaquestion/lab/internal/git - go test -coverprofile=coverage-gitlab.out -covermode=count github.com/zaquestion/lab/internal/gitlab - go test -coverprofile=coverage-cmd.out -covermode=count -coverpkg ./... github.com/zaquestion/lab/cmd + go test -coverprofile=coverage-main.out -covermode=count -coverpkg ./... -run=$(run) github.com/zaquestion/lab/cmd mv testdata/.git testdata/test.git go get github.com/wadey/gocovmerge gocovmerge coverage-*.out > coverage.txt && rm coverage-*.out +.PHONY: install test internal-test diff --git a/README.md b/README.md index c89811b6..d873006c 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,6 @@ go install -ldflags "-X \"main.version=$(git rev-parse --short=10 HEAD)\"" git or ``` -make dep make install ``` diff --git a/cmd/ciTrace.go b/cmd/ciTrace.go index 075f7459..95532a51 100644 --- a/cmd/ciTrace.go +++ b/cmd/ciTrace.go @@ -6,8 +6,11 @@ import ( "io" "io/ioutil" "log" + "strings" + "sync" "time" + "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/zaquestion/lab/internal/git" lab "github.com/zaquestion/lab/internal/gitlab" @@ -15,10 +18,10 @@ import ( // ciLintCmd represents the lint command var ciTraceCmd = &cobra.Command{ - Use: "trace [remote [job]]", + Use: "trace [remote [[:]job]]", Aliases: []string{"logs"}, Short: "Trace the output of a ci job", - Long: ``, + Long: `If a job is not specified the latest running job or last job in the pipeline is used`, Run: func(cmd *cobra.Command, args []string) { var ( remote string @@ -31,15 +34,10 @@ var ciTraceCmd = &cobra.Command{ } remote = args[0] } - if len(args) > 1 { - jobName = args[1] - } if remote == "" { remote = forkedFromRemote } - // See if we're in a git repo or if global is set to determine - // if this should be a personal snippet rn, err := git.PathWithNameSpace(remote) if err != nil { log.Fatal(err) @@ -48,28 +46,42 @@ var ciTraceCmd = &cobra.Command{ if err != nil { log.Fatal(err) } - sha, err := git.Sha("HEAD") + var ref = "HEAD" + if len(args) > 1 { + jobName = args[1] + if strings.Contains(args[1], ":") { + ps := strings.Split(args[1], ":") + ref, jobName = ps[0], ps[1] + } + } + sha, err := git.Sha(ref) if err != nil { log.Fatal(err) } var ( + once sync.Once offset int64 - tick = time.Second * 3 ) FOR: - for range time.NewTicker(tick).C { - trace, status, err := lab.CITrace(project.ID, sha, jobName) - switch status { + for range time.NewTicker(time.Second * 3).C { + trace, job, err := lab.CITrace(project.ID, sha, jobName) + if job == nil { + log.Fatal(errors.Wrap(err, "failed to find job")) + } + switch job.Status { case "pending": - fmt.Println(err) + fmt.Printf("%s is pending...\n", job.Name) continue case "manual": - fmt.Println(err) + fmt.Printf("Manual job %s not started\n", job.Name) break FOR } - if err != nil { - log.Fatal(err) - } + once.Do(func() { + if jobName == "" { + jobName = job.Name + } + fmt.Printf("Showing logs for %s job #%d\n", job.Name, job.ID) + }) buf, err := ioutil.ReadAll(trace) if err != nil { log.Fatal(err) @@ -80,7 +92,9 @@ var ciTraceCmd = &cobra.Command{ offset += int64(len(new)) fmt.Print(string(new)) - if status == "success" || status == "failed" { + if job.Status == "success" || + job.Status == "failed" || + job.Status == "cancelled" { break } } diff --git a/cmd/ciTrace_test.go b/cmd/ciTrace_test.go new file mode 100644 index 00000000..33e273b2 --- /dev/null +++ b/cmd/ciTrace_test.go @@ -0,0 +1,90 @@ +package cmd + +import ( + "os/exec" + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_ciTrace(t *testing.T) { + t.Parallel() + repo := copyTestRepo(t) + cmd := exec.Command("../lab_bin", "fetch", "origin") + cmd.Dir = repo + if b, err := cmd.CombinedOutput(); err != nil { + t.Log(string(b)) + t.Fatal(err) + } + + cmd = exec.Command("../lab_bin", "checkout", "origin/ci_test_pipeline") + cmd.Dir = repo + if b, err := cmd.CombinedOutput(); err != nil { + t.Log(string(b)) + t.Fatal(err) + } + + tests := []struct { + desc string + args []string + assertContains func(t *testing.T, out string) + }{ + { + desc: "noargs", + args: []string{}, + assertContains: func(t *testing.T, out string) { + assert.Contains(t, out, "Showing logs for deploy10 job #62958489") + assert.Contains(t, out, "Checking out 09b519cb as ci_test_pipeline...") + assert.Contains(t, out, "For example you might run an update here or install a build dependency") + assert.Contains(t, out, "$ echo \"Or perhaps you might print out some debugging details\"") + assert.Contains(t, out, "Job succeeded") + }, + }, + { + desc: "manual", + args: []string{"origin", "deploy2"}, + assertContains: func(t *testing.T, out string) { + assert.Contains(t, out, "Manual job deploy2 not started\n") + }, + }, + { + desc: "arg job name", + args: []string{"origin", "deploy1"}, + assertContains: func(t *testing.T, out string) { + assert.Contains(t, out, "Showing logs for deploy1 job #62958479") + assert.Contains(t, out, "Checking out 09b519cb as ci_test_pipeline...") + assert.Contains(t, out, "For example you might run an update here or install a build dependency") + assert.Contains(t, out, "$ echo \"Or perhaps you might print out some debugging details\"") + assert.Contains(t, out, "Job succeeded") + }, + }, + { + desc: "explicit sha:job", + args: []string{"origin", "09b519cba018b707c98fc56e37df15806d89d866:deploy1"}, + assertContains: func(t *testing.T, out string) { + assert.Contains(t, out, "Showing logs for deploy1 job #62958479") + assert.Contains(t, out, "Checking out 09b519cb as ci_test_pipeline...") + assert.Contains(t, out, "For example you might do some cleanup here") + assert.Contains(t, out, "Job succeeded") + }, + }, + } + + for _, test := range tests { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + cmd = exec.Command("../lab_bin", append([]string{"ci", "trace"}, test.args...)...) + cmd.Dir = repo + + b, err := cmd.CombinedOutput() + if err != nil { + t.Log(string(b)) + t.Fatal(err) + } + out := string(b) + test.assertContains(t, out) + }) + } + +} diff --git a/internal/gitlab/gitlab.go b/internal/gitlab/gitlab.go index a2062632..17de533b 100644 --- a/internal/gitlab/gitlab.go +++ b/internal/gitlab/gitlab.go @@ -12,9 +12,9 @@ import ( "net/http" "os" "path/filepath" + "sort" "strings" - "github.com/davecgh/go-spew/spew" "github.com/pkg/errors" "github.com/xanzy/go-gitlab" "github.com/zaquestion/lab/internal/git" @@ -362,14 +362,13 @@ func ProjectDelete(pid interface{}) error { return nil } +// CIJobs returns a list of jobs in a pipeline for a given sha. The jobs are +// returned sorted by their CreatedAt time func CIJobs(pid interface{}, sha string) ([]gitlab.Job, error) { pipelines, _, err := lab.Pipelines.ListProjectPipelines(pid) if err != nil { return nil, err } - if os.Getenv("DEBUG") != "" { - spew.Dump(pipelines) - } var target int for _, p := range pipelines { if p.Sha != sha { @@ -382,36 +381,48 @@ func CIJobs(pid interface{}, sha string) ([]gitlab.Job, error) { if err != nil { return nil, err } - if os.Getenv("DEBUG") != "" { - spew.Dump(jobs) - } - // The jobs seem to be returned in reverse order - // NOTE: more recent testing seems to suggest jobs are returned in last modified order - //for i, j := 0, len(jobs)-1; i < j; i, j = i+1, j-1 { - // jobs[i], jobs[j] = jobs[j], jobs[i] - //} + sort.Sort(ciJobs(jobs)) return jobs, nil } -func CITrace(pid interface{}, sha, name string) (io.Reader, string, error) { +type ciJobs []gitlab.Job + +func (js ciJobs) Len() int { + return len(js) +} + +func (js ciJobs) Less(i, j int) bool { + return js[i].CreatedAt.Before(*js[j].CreatedAt) +} + +func (js ciJobs) Swap(i, j int) { + js[i], js[j] = js[j], js[i] +} + +// CITrace searches by name for a job and returns its trace file. The trace is +// static so may only be a portion of the logs if the job is till running. If +// no name is provided the most recent running job +func CITrace(pid interface{}, sha, name string) (io.Reader, *gitlab.Job, error) { jobs, err := CIJobs(pid, sha) if err != nil { - return nil, "", err + return nil, nil, err } - var job gitlab.Job + var ( + job = jobs[len(jobs)-1] + ) for _, j := range jobs { + if j.Status == "running" { + job = j + } if j.Name == name { job = j break } } - if os.Getenv("DEBUG") != "" { - log.Printf("found job: %d status: %s\n", job.ID, job.Status) - } r, _, err := lab.Jobs.GetTraceFile(pid, job.ID) if err != nil { - return nil, "", err + return nil, &job, err } - return r, job.Status, err + return r, &job, err }