diff --git a/cmd/openqa-revtui/openqa-revtui.go b/cmd/openqa-revtui/openqa-revtui.go index 12a2719..a1ae908 100644 --- a/cmd/openqa-revtui/openqa-revtui.go +++ b/cmd/openqa-revtui/openqa-revtui.go @@ -11,6 +11,8 @@ import ( "github.com/grisu48/gopenqa" ) +const VERSION = "0.2b" + /* Group is a single configurable monitoring unit. A group contains all parameters that will be queried from openQA */ type Group struct { Name string @@ -107,6 +109,27 @@ func hideJob(job gopenqa.Job) bool { return false } +func isReviewed(job gopenqa.Job, instance gopenqa.Instance) (bool, error) { + comments, err := instance.GetComments(job.ID) + if err != nil { + return false, nil + } + for _, c := range comments { + if len(c.BugRefs) > 0 { + return true, nil + } + // Manually check for poo or bsc reference + if strings.Contains(c.Text, "poo#") || strings.Contains(c.Text, "bsc#") { + return true, nil + } + // Or for link to progress/bugzilla ticket + if strings.Contains(c.Text, "://progress.opensuse.org/issues/") || strings.Contains(c.Text, "://bugzilla.suse.com/show_bug.cgi?id=") { + return true, nil + } + } + return false, nil +} + func FetchJobGroups(instance gopenqa.Instance) (map[int]gopenqa.JobGroup, error) { jobGroups := make(map[int]gopenqa.JobGroup) groups, err := instance.GetJobGroups() @@ -121,17 +144,21 @@ func FetchJobGroups(instance gopenqa.Instance) (map[int]gopenqa.JobGroup, error) /* Get job or restarted current job of the given job ID */ func FetchJob(id int, instance gopenqa.Instance) (gopenqa.Job, error) { - for { - job, err := instance.GetJob(id) + var job gopenqa.Job + for i := 0; i < 10; i++ { // Max recursion depth is 10 + var err error + job, err = instance.GetJob(id) if err != nil { return job, err } - if job.CloneID == 0 || job.CloneID == job.ID { - return job, nil - } else { + if job.CloneID != 0 && job.CloneID != job.ID { id = job.CloneID + continue + } else { + return job, nil } } + return job, fmt.Errorf("max recursion depth reached") } func FetchJobs(instance gopenqa.Instance) ([]gopenqa.Job, error) { @@ -142,7 +169,7 @@ func FetchJobs(instance gopenqa.Instance) ([]gopenqa.Job, error) { if err != nil { return ret, err } - // Limit jobs to at most 100, otherwise it's too much + // Limit jobs to at most MaxJobs if len(jobs) > cf.MaxJobs { jobs = jobs[:cf.MaxJobs] } @@ -167,25 +194,19 @@ func rabbitRemote(remote string) string { return remote } -/** Try to update the given job, if it exists and if not the same. Returns the found job and true, if an update was successful*/ -func updateJob(job gopenqa.Job, instance gopenqa.Instance) (gopenqa.Job, bool, error) { +/** Try to update the given job, if it exists and if not the same. Returns the found job and true, if an update was successful */ +func updateJob(orig_id int, job gopenqa.Job, instance gopenqa.Instance) (gopenqa.Job, bool) { for i, j := range knownJobs { - if j.ID == job.ID { - // Follow jobs - if job.CloneID != 0 && job.CloneID != job.ID { - job, err := instance.GetJob(job.CloneID) - knownJobs[i] = job - return knownJobs[i], true, err - } - if j.State != job.State || j.Result != job.Result { + if j.ID == orig_id { + if j.ID != job.ID || j.State != job.State || j.Result != job.Result { knownJobs[i] = job - return knownJobs[i], true, nil + return knownJobs[i], true } else { - return job, false, nil + return job, false } } } - return job, false, nil + return job, false } /** Try to update the job with the given status, if present. Returns the found job and true if the job was present */ @@ -400,24 +421,35 @@ func refreshJobs(tui *TUI, instance gopenqa.Instance) error { status := tui.Status() tui.SetStatus("Refreshing jobs ... ") tui.Update() - if jobs, err := FetchJobs(instance); err != nil { - return err - } else { - for _, j := range jobs { - job, found, err := updateJob(j, instance) + jobs := tui.Model.Jobs() + for i, job := range jobs { + orig_id := job.ID + job, err := FetchJob(job.ID, instance) + if err != nil { + return err + } + job, found := updateJob(orig_id, job, instance) + if found { + status = fmt.Sprintf("Last update: [%s] Job %d-%s %s", time.Now().Format("15:04:05"), job.ID, job.Name, job.JobState()) + tui.SetStatus(status) + jobs[i] = job + tui.Model.Apply(jobs) + tui.Update() + if cf.Notify && !hideJob(job) { + NotifySend(fmt.Sprintf("%s: %s %s", job.JobState(), job.Name, job.Test)) + } + } + // Failed jobs will be also scanned for comments + if job.JobState() == "failed" { + reviewed, err := isReviewed(job, instance) if err != nil { return err } - if found { - status = fmt.Sprintf("Last update: [%s] Job %d-%s %s", time.Now().Format("15:04:05"), job.ID, job.Name, job.JobState()) - tui.SetStatus(status) - tui.Update() - if cf.Notify && !hideJob(job) { - NotifySend(fmt.Sprintf("%s: %s %s", job.JobState(), job.Name, job.Test)) - } - } + tui.Model.SetReviewed(job.ID, reviewed) + tui.Update() } } + tui.Model.Apply(jobs) tui.SetStatus(status) tui.Update() return nil @@ -425,7 +457,7 @@ func refreshJobs(tui *TUI, instance gopenqa.Instance) error { // main routine for the TUI instance func tui_main(tui *TUI, instance gopenqa.Instance) int { - title := "openqa Review TUI Dashboard" + title := "openqa Review TUI Dashboard v" + VERSION var rabbitmq gopenqa.RabbitMQ var err error @@ -488,9 +520,20 @@ func tui_main(tui *TUI, instance gopenqa.Instance) int { fmt.Fprintf(os.Stderr, "Error fetching jobs: %s\n", err) os.Exit(1) } + // Failed jobs will be also scanned for comments + for _, job := range jobs { + if job.JobState() == "failed" { + reviewed, err := isReviewed(job, instance) + if err != nil { + fmt.Fprintf(os.Stderr, "Error fetching job comment: %s\n", err) + os.Exit(1) + } + tui.Model.SetReviewed(job.ID, reviewed) + } + } knownJobs = jobs - tui.Start() tui.Model.Apply(knownJobs) + tui.Start() tui.Update() // Register RabbitMQ diff --git a/cmd/openqa-revtui/tui.go b/cmd/openqa-revtui/tui.go index c89c419..83b4d2c 100644 --- a/cmd/openqa-revtui/tui.go +++ b/cmd/openqa-revtui/tui.go @@ -65,6 +65,7 @@ func CreateTUI() TUI { tui.showStatus = true tui.Model.jobs = make([]gopenqa.Job, 0) tui.Model.jobGroups = make(map[int]gopenqa.JobGroup, 0) + tui.Model.reviewed = make(map[int]bool, 0) return tui } @@ -75,6 +76,7 @@ type TUIModel struct { mutex sync.Mutex // Access mutex to the model offset int // Line offset for printing printLines int // Lines that would need to be printed, needed for offset handling + reviewed map[int]bool // Indicating if failed jobs are reviewed } func (tui *TUI) visibleJobCount() int { @@ -87,6 +89,15 @@ func (tui *TUI) visibleJobCount() int { return counter } +func (model *TUIModel) SetReviewed(job int, reviewed bool) { + model.reviewed[job] = reviewed +} + +func (model *TUIModel) isReviewed(job int) bool { + reviewed, found := model.reviewed[job] + return found && reviewed +} + func (tui *TUIModel) MoveHome() { tui.mutex.Lock() defer tui.mutex.Unlock() @@ -99,6 +110,10 @@ func (tui *TUIModel) Apply(jobs []gopenqa.Job) { tui.jobs = jobs } +func (model *TUIModel) Jobs() []gopenqa.Job { + return model.jobs +} + func (tui *TUIModel) SetJobGroups(grps map[int]gopenqa.JobGroup) { tui.jobGroups = grps } @@ -271,7 +286,7 @@ func (tui *TUI) printJobs(width, height int) { for _, job := range tui.Model.jobs { if !tui.hideJob(job) { if line++; line > tui.Model.offset { - fmt.Println(formatJobLine(job, width)) + fmt.Println(tui.formatJobLine(job, width)) } } } @@ -316,7 +331,7 @@ func (tui *TUI) printJobsByGroup(width, height int) int { lines = append(lines, fmt.Sprintf("===== %s ====================\n", grp.Name)) for _, job := range jobs { if !tui.hideJob(job) { - lines = append(lines, formatJobLine(job, width)) + lines = append(lines, tui.formatJobLine(job, width)) } else { hidden++ } @@ -468,7 +483,7 @@ func getDateColorcode(t time.Time) string { return ANSI_WHITE } -func formatJobLine(job gopenqa.Job, width int) string { +func (tui *TUI) formatJobLine(job gopenqa.Job, width int) string { c1 := ANSI_WHITE // date color tStr := "" // Timestamp string @@ -488,6 +503,15 @@ func formatJobLine(job gopenqa.Job, width int) string { if state != "scheduled" && timestamp.Unix() > 0 { tStr = timestamp.Format("2006-01-02-15:04:05") } + // For failed jobs check if they are reviewed + if state == "failed" { + if reviewed, found := tui.Model.reviewed[job.ID]; found { + if reviewed { + state = "reviewed" + c2 = ANSI_MAGENTA + } + } + } // Full status line requires 89 characters (20+4+8+1+12+1+40+3) plus name if width > 90 { diff --git a/go.mod b/go.mod index 36d1c1c..2c9ab8b 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,6 @@ go 1.11 require ( github.com/BurntSushi/toml v0.3.1 - github.com/grisu48/gopenqa v0.0.0-20210301131952-e781e21a6e9c + github.com/grisu48/gopenqa v0.3.3 github.com/streadway/amqp v1.0.0 ) diff --git a/go.sum b/go.sum index d015066..fe88ba1 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,21 @@ github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/grisu48/gopenqa v0.0.0-20210224104953-f86136cfe826/go.mod h1:ZVDyBqnwiOAv+vm7FFB8/S94Rt+0da4lQD5TaZWl8oE= github.com/grisu48/gopenqa v0.0.0-20210301125648-f72f716c6bfb h1:Xvj1gHwROF2xIPETjhUW7TFXBeq29qoqYRLxu7E4J5w= github.com/grisu48/gopenqa v0.0.0-20210301125648-f72f716c6bfb/go.mod h1:ZVDyBqnwiOAv+vm7FFB8/S94Rt+0da4lQD5TaZWl8oE= github.com/grisu48/gopenqa v0.0.0-20210301131952-e781e21a6e9c h1:g8/Zih9pi182bkyGoa3Y/AFXS/qLhv5nTpoQjilwgHo= github.com/grisu48/gopenqa v0.0.0-20210301131952-e781e21a6e9c/go.mod h1:ZVDyBqnwiOAv+vm7FFB8/S94Rt+0da4lQD5TaZWl8oE= +github.com/grisu48/gopenqa v0.0.0-20210420081537-0d0d61f743e6 h1:b+OqOOGaLU9eWIEZVEeh6KCb4DYaMLDM36gSutC+hTg= +github.com/grisu48/gopenqa v0.0.0-20210420081537-0d0d61f743e6/go.mod h1:D7EFTPhtzNvnHnDol9UoPCFmnzOiLBVa1tOOYqJDgGo= +github.com/grisu48/gopenqa v0.3.0 h1:LSstlioho4vFqxSf3jNVJSU9BeCYqLW6mlSwy0Qzw9k= +github.com/grisu48/gopenqa v0.3.0/go.mod h1:D7EFTPhtzNvnHnDol9UoPCFmnzOiLBVa1tOOYqJDgGo= +github.com/grisu48/gopenqa v0.3.2 h1:v8h5iYqZqGHph69OlPvrd3+E4AqnOAph51+NbAihzUk= +github.com/grisu48/gopenqa v0.3.2/go.mod h1:D7EFTPhtzNvnHnDol9UoPCFmnzOiLBVa1tOOYqJDgGo= +github.com/grisu48/gopenqa v0.3.3 h1:WTwTBcIc06uFW4NgMkbhq6bkH+3TGF1H3p5u2/9wYoI= +github.com/grisu48/gopenqa v0.3.3/go.mod h1:D7EFTPhtzNvnHnDol9UoPCFmnzOiLBVa1tOOYqJDgGo= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/streadway/amqp v1.0.0 h1:kuuDrUJFZL1QYL9hUNuCxNObNzB0bV/ZG5jV3RWAQgo= github.com/streadway/amqp v1.0.0/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=