Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package openqa-mon for openSUSE:Factory checked in at 2023-03-19 16:16:48 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/openqa-mon (Old) and /work/SRC/openSUSE:Factory/.openqa-mon.new.31432 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "openqa-mon" Sun Mar 19 16:16:48 2023 rev:6 rq:1072868 version:0.27 Changes: -------- --- /work/SRC/openSUSE:Factory/openqa-mon/openqa-mon.changes 2023-01-20 17:39:15.640713864 +0100 +++ /work/SRC/openSUSE:Factory/.openqa-mon.new.31432/openqa-mon.changes 2023-03-19 16:17:03.935464221 +0100 @@ -1,0 +2,15 @@ +Sat Mar 18 22:38:42 UTC 2023 - Jeff Kowalczyk <[email protected]> + +- _service obs_scm revision change branch to main from master which + no longer exists. + +------------------------------------------------------------------- +Thu Mar 16 09:38:05 UTC 2023 - Felix Niederwanger <[email protected]> + +- Update to version 0.27 + * Fix incomplete jobs not considered with --exit + * Refactoring of the revtui screen + * Introduce the --input parameter in openqa-mon + * Many small bugfixes + +------------------------------------------------------------------- Old: ---- openqa-mon-0.26.obscpio openqa-mon-0.26.tar.gz New: ---- openqa-mon-0.27.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ openqa-mon.spec ++++++ --- /var/tmp/diff_new_pack.ZCY13p/_old 2023-03-19 16:17:04.575467333 +0100 +++ /var/tmp/diff_new_pack.ZCY13p/_new 2023-03-19 16:17:04.579467352 +0100 @@ -17,7 +17,7 @@ Name: openqa-mon -Version: 0.26 +Version: 0.27 Release: 0 Summary: CLI monitoring utility for openQA License: GPL-3.0-or-later ++++++ _service ++++++ --- /var/tmp/diff_new_pack.ZCY13p/_old 2023-03-19 16:17:04.611467508 +0100 +++ /var/tmp/diff_new_pack.ZCY13p/_new 2023-03-19 16:17:04.611467508 +0100 @@ -2,8 +2,8 @@ <service name="obs_scm" mode="localonly"> <param name="url">https://github.com/grisu48/openqa-mon.git</param> <param name="scm">git</param> - <param name="revision">master</param> - <param name="version">v0.26</param> + <param name="revision">main</param> + <param name="version">v0.27</param> <param name="versionrewrite-pattern">v(.*)</param> </service> <service name="tar" mode="localonly"/> ++++++ openqa-mon-0.26.tar.gz -> openqa-mon-0.27.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openqa-mon-0.26/_review/opensuse-microos.toml new/openqa-mon-0.27/_review/opensuse-microos.toml --- old/openqa-mon-0.26/_review/opensuse-microos.toml 2023-01-16 10:13:21.000000000 +0100 +++ new/openqa-mon-0.27/_review/opensuse-microos.toml 2023-03-13 10:29:03.000000000 +0100 @@ -1,15 +1,15 @@ -## Review template file for PublicCloud test runs on OSD +## Review template file for MicroOS test runs on O3 -Instance = "https://openqa.opensuse.org" # openQA instance to query -RabbitMQ = "amqps://opensuse:[email protected]" # RabbitMQ instance to query -RabbitMQTopic = "opensuse.openqa.job.done" # RabbitMQ topic to query -HideStatus = [ "scheduled","passed","assigned" ] # Hide scheduled, passed and assigned jobs -RefreshInterval = 60 # Refresh from API once every minute -MaxJobs = 20 # Max. job per group to display -GroupBy = "groups" # Group by defined groups ("none" or "groups") -DefaultParams = { distri="microos", version = "Tumbleweed" } # Set of default parameters +Instance = "https://openqa.opensuse.org" # openQA instance to query +RabbitMQ = "amqps://opensuse:[email protected]" # RabbitMQ instance to query +RabbitMQTopic = "opensuse.openqa.job.done" # RabbitMQ topic to query +HideStatus = [ "scheduled","passed","assigned","running","softfailed","reviewed" ] # Hide scheduled, passed, assigned and reviewed jobs +RefreshInterval = 60 # Refresh from API once every minute +MaxJobs = 20 # Max. job per group to display +GroupBy = "groups" # Group by defined groups ("none" or "groups") +DefaultParams = { distri="microos", version = "Tumbleweed" } # Set of default parameters -## Define container groups by their group ID +## Define job groups [[Groups]] Name = "MicroOS Image" Params = { flavor = "MicroOS-Image" } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openqa-mon-0.26/_review/opensuse-tumbleweed.toml new/openqa-mon-0.27/_review/opensuse-tumbleweed.toml --- old/openqa-mon-0.26/_review/opensuse-tumbleweed.toml 2023-01-16 10:13:21.000000000 +0100 +++ new/openqa-mon-0.27/_review/opensuse-tumbleweed.toml 2023-03-13 10:29:03.000000000 +0100 @@ -1,30 +1,36 @@ -## Review template file for PublicCloud test runs on OSD +## Review template file for Tumbleweed test runs on O3 Instance = "https://openqa.opensuse.org" # openQA instance to query RabbitMQ = "amqps://opensuse:[email protected]" # RabbitMQ instance to query RabbitMQTopic = "opensuse.openqa.job.done" # RabbitMQ topic to query -HideStatus = [ "scheduled","passed","assigned","running","softfailed" ] # Hide scheduled, passed and assigned jobs +HideStatus = [ "scheduled","passed","assigned","running","softfailed","reviewed" ] # Hide scheduled, passed, assigned and reviewed jobs RefreshInterval = 60 # Refresh from API once every minute MaxJobs = 20 # Max. job per group to display GroupBy = "groups" # Group by defined groups ("none" or "groups") DefaultParams = { distri="opensuse", version = "Tumbleweed" } # Set of default parameters -## Define container groups by their group ID +## Define job groups + [[Groups]] Name = "openSUSE Tumbleweed DVD" Params = { flavor = "DVD" } +MaxLifetime = 86400 [[Groups]] Name = "openSUSE Tumbleweed Network" Params = { flavor = "NET" } +MaxLifetime = 86400 [[Groups]] Name = "openSUSE Tumbleweed JeOS" Params = { flavor = "JeOS-for-kvm-and-xen" } +MaxLifetime = 86400 [[Groups]] Name = "openSUSE Tumbleweed Gnome-Live" Params = { flavor = "GNOME-Live" } +MaxLifetime = 86400 [[Groups]] Name = "openSUSE Tumbleweed KDE-Live" Params = { flavor = "KDE-Live" } +MaxLifetime = 86400 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openqa-mon-0.26/cmd/openqa-mon/openqa-mon.go new/openqa-mon-0.27/cmd/openqa-mon/openqa-mon.go --- old/openqa-mon-0.26/cmd/openqa-mon/openqa-mon.go 2023-01-16 10:13:21.000000000 +0100 +++ new/openqa-mon-0.27/cmd/openqa-mon/openqa-mon.go 2023-03-13 10:29:03.000000000 +0100 @@ -2,6 +2,7 @@ package main import ( + "bufio" "fmt" "os" "os/signal" @@ -16,7 +17,7 @@ "github.com/grisu48/gopenqa" ) -const VERSION = "0.7.0" +const VERSION = "0.8.0" var config Config var tui TUI @@ -30,9 +31,9 @@ var remotes []Remote func printHelp() { - fmt.Printf("Usage: %s [OPTIONS] REMOTE\n REMOTE is the base URL of the openQA server (e.g. https://openqa.opensuse.org)\n\n", os.Args[0]) - fmt.Println(" REMOTE can be the directlink to a test (e.g. https://openqa.opensuse.org/t123)") - fmt.Println(" or a job range (e.g. https://openqa.opensuse.org/t123..125 or https://openqa.opensuse.org/t123+2)") + fmt.Printf("Usage: %s [OPTIONS] REMOTE\n", os.Args[0]) + fmt.Println(" REMOTE can be the directlink to a test (e.g. https://openqa.opensuse.org/t123)") + fmt.Println(" or a job range (e.g. https://openqa.opensuse.org/t123..125 or https://openqa.opensuse.org/t123+2)") fmt.Println("") fmt.Println("OPTIONS") fmt.Println("") @@ -41,7 +42,7 @@ fmt.Println(" -j, --jobs JOBS Display information only for the given JOBS") fmt.Println(" JOBS can be a single job id, a comma separated list (e.g. 42,43,1337)") fmt.Println(" or a job range (1335..1339 or 1335+4)") - fmt.Println(" -c,--continous SECONDS Continously display stats, use rabbitmq if available otherwise status pulling") + fmt.Println(" -c,--continuous SECONDS Continuously display stats, use rabbitmq if available otherwise status pulling") fmt.Println(" -e,--exit Exit openqa-mon when all jobs are done (only in continuous mode)") fmt.Println(" Return code is 0 if all jobs are passed or softfailing, 1 otherwise.") fmt.Println(" -b,--bell Bell notification on job status changes") @@ -60,8 +61,9 @@ fmt.Println(" --hide-state STATES Hide jobs with that are in the given state (e.g. 'running,assigned')") fmt.Println("") fmt.Println(" --config FILE Read additional config file FILE") + fmt.Println(" -i, --input FILE Read jobs from FILE (additionally to stdin)") fmt.Println("") - fmt.Println("2022, https://github.com/grisu48/openqa-mon") + fmt.Println("2023, https://github.com/grisu48/openqa-mon") } /** Try to match the url to be a test url. On success, return the remote and the job id */ @@ -102,18 +104,19 @@ return true } -/* checks if the set of jobs contains failed jobs */ +/* checks if the set of jobs contains failed jobs. This includes incomplete jobs */ func getFailedJobs(jobs []gopenqa.Job) []gopenqa.Job { ret := make([]gopenqa.Job, 0) for _, job := range jobs { // We only consider completed jobs if job.State == "cancelled" { ret = append(ret, job) - } else if job.State != "done" { + } else if job.State == "done" { // Assume a job is failed, if it is not passed or softfailed if job.Result != "passed" && job.Result != "softfail" { ret = append(ret, job) } + // Note: incomplete jobs also have state "done". They are also considered failed } } return ret @@ -204,6 +207,8 @@ ret = append(ret, "--silent") case 'e': ret = append(ret, "--exit") + case 'i': + ret = append(ret, "--input") } } } else { @@ -402,6 +407,43 @@ return rmq, nil } +func readJobs(filename string) ([]Remote, error) { + remotes := make([]Remote, 0) + + fIn, err := os.OpenFile(filename, os.O_RDONLY, 0400) + if err != nil { + return remotes, err + } + defer fIn.Close() + scanner := bufio.NewScanner(fIn) + iLine := 0 + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + iLine++ + // Ignore empty lines or comments + if line == "" || line[0] == '#' { + continue + } + + // Only accept URLs here + if strings.HasPrefix(line, "http://") || strings.HasPrefix(line, "https://") { + // Try to parse as job run (e.g. http://phoenix-openqa.qam.suse.de/t1241) + match, url, jobIDs := matchTestURL(removeFragment(line)) + if match { + for _, jobID := range jobIDs { + remotes = appendRemote(remotes, url, jobID) + } + } else { + remotes = appendRemote(remotes, line, 0) + } + } else { + return remotes, fmt.Errorf("invalid job link (line %d)", iLine) + } + } + + return remotes, scanner.Err() +} + func SetStatus() { if config.Paused { if config.RabbitMQ { @@ -529,6 +571,22 @@ config.HideStates = append(config.HideStates, states...) case "--quit", "--exit": config.Quit = true + case "--input": + i++ + if i >= len(args) { + return fmt.Errorf("missing input file") + } + // Read input file and add jobs + if jobs, err := readJobs(args[i]); err != nil { + return fmt.Errorf("error reading jobs: %s", err) + } else { + // Append all found jobs + for _, remote := range jobs { + for _, job := range remote.Jobs { + remotes = appendRemote(remotes, remote.URI, job) + } + } + } default: return fmt.Errorf("invalid argument: %s", arg) } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openqa-mon-0.26/cmd/openqa-revtui/openqa-revtui.go new/openqa-mon-0.27/cmd/openqa-revtui/openqa-revtui.go --- old/openqa-mon-0.26/cmd/openqa-revtui/openqa-revtui.go 2023-01-16 10:13:21.000000000 +0100 +++ new/openqa-mon-0.27/cmd/openqa-revtui/openqa-revtui.go 2023-03-13 10:29:03.000000000 +0100 @@ -11,7 +11,7 @@ "github.com/grisu48/gopenqa" ) -const VERSION = "0.3.4" +const VERSION = "0.4.0" /* Group is a single configurable monitoring unit. A group contains all parameters that will be queried from openQA */ type Group struct { @@ -350,7 +350,6 @@ } func printUsage() { - // TODO: Write this fmt.Printf("Usage: %s [OPTIONS] [FLAVORS]\n", os.Args[0]) fmt.Println("") fmt.Println("OPTIONS") @@ -559,6 +558,7 @@ } knownJobs = jobs tui.Model.Apply(knownJobs) + fmt.Println("Initial fetching completed. Entering main loop ... ") tui.Start() tui.Update() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openqa-mon-0.26/cmd/openqa-revtui/tui.go new/openqa-mon-0.27/cmd/openqa-revtui/tui.go --- old/openqa-mon-0.26/cmd/openqa-revtui/tui.go 2023-01-16 10:13:21.000000000 +0100 +++ new/openqa-mon-0.27/cmd/openqa-revtui/tui.go 2023-03-13 10:29:03.000000000 +0100 @@ -54,6 +54,8 @@ showTracker bool // Show tracker showStatus bool // Show status line sorting int // Sorting method - 0: none, 1 - by job group + + screensize int // Lines per screen } func CreateTUI() TUI { @@ -175,7 +177,6 @@ } func (tui *TUI) readInput() { - // TODO: Find a way to read raw without ENTER var b []byte = make([]byte, 1) var p = make([]byte, 3) // History, needed for special keys for { @@ -197,8 +198,7 @@ tui.Update() } } else if p[1] == 91 && k == 66 { // Arrow down - _, height := terminalSize() - max := max(0, (tui.Model.printLines - height + 7)) + max := max(0, (tui.Model.printLines - tui.screensize)) if tui.Model.offset < max { tui.Model.offset++ tui.Update() @@ -206,17 +206,14 @@ } else if p[2] == 27 && p[1] == 91 && p[0] == 72 { // home tui.Model.offset = 0 } else if p[2] == 27 && p[1] == 91 && p[0] == 70 { // end - _, height := terminalSize() - tui.Model.offset = max(0, (tui.Model.printLines - height + 7)) + tui.Model.offset = max(0, (tui.Model.printLines - tui.screensize)) } else if p[2] == 27 && p[1] == 91 && p[0] == 53 { // page up - _, height := terminalSize() - scroll := max(1, height-5) - tui.Model.offset = max(0, tui.Model.offset-scroll) + // Always leave one line overlap for better orientation + tui.Model.offset = max(0, tui.Model.offset-tui.screensize+1) } else if p[2] == 27 && p[1] == 91 && p[0] == 54 { // page down - _, height := terminalSize() - scroll := max(1, height-5) - max := max(0, (tui.Model.printLines - height + 7)) - tui.Model.offset = min(max, tui.Model.offset+scroll) + max := max(0, (tui.Model.printLines - tui.screensize)) + // Always leave one line overlap for better orientation + tui.Model.offset = min(max, tui.Model.offset+tui.screensize-1) } // Forward keypress to listener @@ -287,16 +284,14 @@ } // print all jobs unsorted -func (tui *TUI) printJobs(width, height int) { - line := 0 +func (tui *TUI) buildJobsScreen(width int) []string { + lines := make([]string, 0) for _, job := range tui.Model.jobs { if !tui.hideJob(job) { - if line++; line > tui.Model.offset { - fmt.Println(tui.formatJobLine(job, width)) - } + lines = append(lines, tui.formatJobLine(job, width)) } } - tui.Model.printLines = len(tui.Model.jobs) + return lines } func sortedKeys(vals map[string]int) []string { @@ -311,7 +306,9 @@ return ret } -func (tui *TUI) printJobsByGroup(width, height int) int { +func (tui *TUI) buildJobsScreenByGroup(width int) []string { + lines := make([]string, 0) + // Determine active groups first groups := make(map[int][]gopenqa.Job, 0) for _, job := range tui.Model.jobs { @@ -327,14 +324,20 @@ grpIDs = append(grpIDs, k) } sort.Ints(grpIDs) + // Now print them sorted by group ID - lines := make([]string, 0) + first := true for _, id := range grpIDs { grp := tui.Model.jobGroups[id] jobs := groups[id] statC := make(map[string]int, 0) hidden := 0 - lines = append(lines, fmt.Sprintf("\n===== %s ====================", grp.Name)) + if first { + first = false + } else { + lines = append(lines, "") + } + lines = append(lines, fmt.Sprintf("===== %s ====================", grp.Name)) for _, job := range jobs { if !tui.hideJob(job) { lines = append(lines, tui.formatJobLine(job, width)) @@ -360,21 +363,7 @@ } lines = append(lines, line) } - - // For scrolling, remember the total number of lines - tui.Model.printLines = len(lines) - - // Print relevant lines, taking the offset into consideration - counter := 0 - for i := 0; i < height; i++ { - if (tui.Model.offset + i) >= len(lines) { - break - } else { - fmt.Println(lines[tui.Model.offset+i]) - counter++ - } - } - return counter + return lines } func cut(text string, n int) string { @@ -384,26 +373,42 @@ return text[:n] } } +func trimEmptyTail(lines []string) []string { + // Crop empty elements at the end of the array + for n := len(lines) - 1; n > 0; n-- { + if lines[n] != "" { + return lines[0 : n+1] + } + } + return lines[0:0] +} -/* Redraw screen */ -func (tui *TUI) Update() { - tui.Model.mutex.Lock() - defer tui.Model.mutex.Unlock() - width, height := terminalSize() - if width < 0 || height < 0 { - return +func trimEmptyHead(lines []string) []string { + // Crop empty elements at the end of the array + for i := 0; i < len(lines); i++ { + if lines[i] != "" { + return lines[i:] + } } + return lines[0:0] +} - remainingHeight := height // remaining rows in the current terminal - tui.Clear() +func trimEmpty(lines []string) []string { + lines = trimEmptyHead(lines) + lines = trimEmptyTail(lines) + return lines +} + +func (tui *TUI) buildHeader(width int) []string { + lines := make([]string, 0) if tui.header != "" { - fmt.Println(tui.header) - fmt.Println("q:Quit r:Refresh h:Hide/Show jobs m:Toggle RabbitMQ tracker s:Switch sorting Arrows:Move up/down") - fmt.Println() - remainingHeight -= 4 + lines = append(lines, tui.header) + lines = append(lines, "q:Quit r:Refresh h:Hide/Show jobs m:Toggle RabbitMQ tracker s:Switch sorting Arrows:Move up/down") } + return lines +} - // The footer is a bit more complex, build it here, so we know how many more rows are occupied +func (tui *TUI) buildFooter(width int) []string { footer := make([]string, 0) showStatus := tui.showStatus && tui.status != "" showTracker := tui.showTracker && tui.tracker != "" @@ -429,25 +434,81 @@ footer = append(footer, tui.tracker[:width]) } } - if len(footer) > 0 { - remainingHeight -= (len(footer) + 2) - } + return footer +} + +// Build the full screen +func (tui *TUI) buildScreen(width int) []string { + lines := make([]string, 0) - // Job listing depends on selected sorting method - remainingHeight -= 2 // printJobs and printJobsByGroup terminate with a newline switch tui.sorting { case 1: - tui.printJobsByGroup(width, remainingHeight) - break + lines = append(lines, tui.buildJobsScreenByGroup(width)...) default: - tui.printJobs(width, remainingHeight) - break + lines = append(lines, tui.buildJobsScreen(width)...) + } + lines = trimEmpty(lines) + + tui.Model.printLines = len(lines) + return lines +} + +/* Redraw screen */ +func (tui *TUI) Update() { + tui.Model.mutex.Lock() + defer tui.Model.mutex.Unlock() + width, height := terminalSize() + if width < 0 || height < 0 { + return } + // Header and footer are separate. We only scroll through the "screen" + screen := tui.buildScreen(width) + header := tui.buildHeader(width) + footer := tui.buildFooter(width) + + remainingLines := height + tui.Clear() + + // Print header + if len(header) > 0 { + header = append(header, "") // Add additional line after header + + for _, line := range header { + fmt.Println(line) + remainingLines-- + if remainingLines <= 0 { + return // crap. no need to continue + } + } + } + + // Reserve lines for footer, but spare footer if there is no space left + if len(footer) > remainingLines { + footer = make([]string, 0) + } else { + remainingLines -= (len(footer) + 1) + } + + // Print screen + screensize := 0 + for elem := tui.Model.offset; remainingLines > 0; remainingLines-- { + if elem >= len(screen) { + fmt.Println("") // Fill screen with empty lines for alignment + } else { + //fmt.Println(strings.TrimSpace(screen[elem])) // XXX + fmt.Println(screen[elem]) + elem++ + } + screensize++ + } + tui.screensize = screensize + + // Print footer if len(footer) > 0 { - fmt.Println() + fmt.Println("") for _, line := range footer { - fmt.Printf("\n%s", line) + fmt.Println(line) } } } @@ -516,6 +577,13 @@ } } + // Crop the state field, if necessary + if state == "timeout_exceeded" { + state = "timeout" + } else if len(state) > 12 { + state = state[0:12] + } + // Full status line requires 89 characters (20+4+8+1+12+1+40+3) plus name if width > 90 { // Crop the name, if necessary ++++++ openqa-mon.obsinfo ++++++ --- /var/tmp/diff_new_pack.ZCY13p/_old 2023-03-19 16:17:04.703467955 +0100 +++ /var/tmp/diff_new_pack.ZCY13p/_new 2023-03-19 16:17:04.707467974 +0100 @@ -1,5 +1,5 @@ name: openqa-mon -version: 0.26 -mtime: 1673860401 -commit: ae01eecbbd0010ba70c24c78f60efa047753b343 +version: 0.27 +mtime: 1678699743 +commit: f3c259de42cfcbff0afd4285313b48f8d9475a84 ++++++ vendor.tar.gz ++++++
