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-04-01 19:32:35
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/openqa-mon (Old)
 and      /work/SRC/openSUSE:Factory/.openqa-mon.new.9019 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "openqa-mon"

Sat Apr  1 19:32:35 2023 rev:7 rq:1076567 version:1.0.1

Changes:
--------
--- /work/SRC/openSUSE:Factory/openqa-mon/openqa-mon.changes    2023-03-19 
16:17:03.935464221 +0100
+++ /work/SRC/openSUSE:Factory/.openqa-mon.new.9019/openqa-mon.changes  
2023-04-01 19:32:50.457536097 +0200
@@ -1,0 +2,18 @@
+Thu Mar 30 13:47:25 UTC 2023 - Felix Niederwanger <[email protected]>
+
+- Update to version 1.0.1
+* Upgrade to gopenqa 0.7.1 and align version number
+
+-------------------------------------------------------------------
+Thu Mar 30 13:04:00 UTC 2023 - Felix Niederwanger <[email protected]>
+
+- Update to version 1.0.0
+* Switch to single request for multiple jobs (openqa-mon and openqa-revtui)
+* Improve review templates
+* Consider the parallel_failed status (openqa-revtui)
+* Add progress when loading jobs (openqa-revtui)
+* Introduce colors in status field (openqa-revtui)
+* Rate limit continuous refresh on OSD and O3
+* Show job progress in Refresh line (openqa-revtui)
+
+-------------------------------------------------------------------

Old:
----
  openqa-mon-0.27.tar.gz

New:
----
  openqa-mon-1.0.1.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ openqa-mon.spec ++++++
--- /var/tmp/diff_new_pack.35bpAt/_old  2023-04-01 19:32:51.081539373 +0200
+++ /var/tmp/diff_new_pack.35bpAt/_new  2023-04-01 19:32:51.085539394 +0200
@@ -17,7 +17,7 @@
 
 
 Name:           openqa-mon
-Version:        0.27
+Version:        1.0.1
 Release:        0
 Summary:        CLI monitoring utility for openQA
 License:        GPL-3.0-or-later

++++++ _service ++++++
--- /var/tmp/diff_new_pack.35bpAt/_old  2023-04-01 19:32:51.125539605 +0200
+++ /var/tmp/diff_new_pack.35bpAt/_new  2023-04-01 19:32:51.137539668 +0200
@@ -3,7 +3,7 @@
     <param name="url">https://github.com/grisu48/openqa-mon.git</param>
     <param name="scm">git</param>
     <param name="revision">main</param>
-    <param name="version">v0.27</param>
+    <param name="version">v1.0.1</param>
        <param name="versionrewrite-pattern">v(.*)</param>
   </service>
   <service name="tar" mode="localonly"/>

++++++ openqa-mon-0.27.tar.gz -> openqa-mon-1.0.1.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/openqa-mon-0.27/_review/opensuse-microos.toml 
new/openqa-mon-1.0.1/_review/opensuse-microos.toml
--- old/openqa-mon-0.27/_review/opensuse-microos.toml   2023-03-13 
10:29:03.000000000 +0100
+++ new/openqa-mon-1.0.1/_review/opensuse-microos.toml  2023-03-30 
15:43:35.000000000 +0200
@@ -1,13 +1,13 @@
 ## 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","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
+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", "softfailed", "cancelled", "skipped", 
"running", "uploading", "parallel_failed", "reviewed" ]           # Hide 
scheduled and passed 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 job groups
 [[Groups]]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/openqa-mon-0.27/_review/opensuse-tumbleweed.toml 
new/openqa-mon-1.0.1/_review/opensuse-tumbleweed.toml
--- old/openqa-mon-0.27/_review/opensuse-tumbleweed.toml        2023-03-13 
10:29:03.000000000 +0100
+++ new/openqa-mon-1.0.1/_review/opensuse-tumbleweed.toml       2023-03-30 
15:43:35.000000000 +0200
@@ -1,13 +1,13 @@
 ## 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","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
+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", "softfailed", "cancelled", "skipped", 
"running", "uploading", "parallel_failed", "reviewed" ]           # Hide 
scheduled and passed 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 job groups
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/openqa-mon-0.27/_review/qe-core-O3.toml 
new/openqa-mon-1.0.1/_review/qe-core-O3.toml
--- old/openqa-mon-0.27/_review/qe-core-O3.toml 2023-03-13 10:29:03.000000000 
+0100
+++ new/openqa-mon-1.0.1/_review/qe-core-O3.toml        2023-03-30 
15:43:35.000000000 +0200
@@ -1,13 +1,13 @@
 ## Review template file for QE-Core job groups in O3
 # See: 
https://confluence.suse.com/display/qasle/Bugbusters+and+Review+Shifts#BugbustersandReviewShifts-Links
 
-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", "softfailed", "running", "reviewed" ]    
       # Hide scheduled and passed 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")
+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", "softfailed", "cancelled", "skipped", 
"running", "uploading", "parallel_failed", "reviewed" ]           # Hide 
scheduled and passed 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")
 
 # Tumbleweed
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/openqa-mon-0.27/_review/qe-core-OSD.toml 
new/openqa-mon-1.0.1/_review/qe-core-OSD.toml
--- old/openqa-mon-0.27/_review/qe-core-OSD.toml        2023-03-13 
10:29:03.000000000 +0100
+++ new/openqa-mon-1.0.1/_review/qe-core-OSD.toml       2023-03-30 
15:43:35.000000000 +0200
@@ -4,7 +4,7 @@
 Instance = "https://openqa.suse.de";              # openQA instance to query
 RabbitMQ = "amqps://suse:[email protected]"    # RabbitMQ instance to query
 RabbitMQTopic = "suse.openqa.job.done"           # RabbitMQ topic to query
-HideStatus = [ "scheduled", "passed", "softfailed", "running", "reviewed" ]    
       # Hide scheduled and passed jobs
+HideStatus = ["scheduled", "passed", "softfailed", "cancelled", "skipped", 
"running", "uploading", "parallel_failed", "reviewed" ]           # Hide 
scheduled and passed 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")
@@ -63,6 +63,18 @@
 Params = { groupid = "53", build = "" }
 MaxLifetime = 86400
 
+# Quarterly Refresh
+
+[[Groups]]
+Name = "Maintenance: QR SLE 15-SP4"
+Params = { groupid = "458", build = "" }
+MaxLifetime = 86400
+
+[[Groups]]
+Name = "Maintenance: QR SLE 15-SP3"
+Params = { groupid = "373", build = "" }
+MaxLifetime = 86400
+
 # Functional
 
 [[Groups]]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/openqa-mon-0.27/_review/qec-containers.toml 
new/openqa-mon-1.0.1/_review/qec-containers.toml
--- old/openqa-mon-0.27/_review/qec-containers.toml     2023-03-13 
10:29:03.000000000 +0100
+++ new/openqa-mon-1.0.1/_review/qec-containers.toml    2023-03-30 
15:43:35.000000000 +0200
@@ -3,7 +3,7 @@
 Instance = "https://openqa.suse.de";              # openQA instance to query
 RabbitMQ = "amqps://suse:[email protected]"    # RabbitMQ instance to query
 RabbitMQTopic = "suse.openqa.job.done"           # RabbitMQ topic to query
-HideStatus = [ "scheduled", "passed", "softfailed", "running", "reviewed" ]    
       # Hide scheduled and passed jobs
+HideStatus = ["scheduled", "passed", "softfailed", "cancelled", "skipped", 
"running", "uploading", "parallel_failed", "reviewed" ]           # Hide 
scheduled and passed 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")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/openqa-mon-0.27/_review/qec-jeos.toml 
new/openqa-mon-1.0.1/_review/qec-jeos.toml
--- old/openqa-mon-0.27/_review/qec-jeos.toml   2023-03-13 10:29:03.000000000 
+0100
+++ new/openqa-mon-1.0.1/_review/qec-jeos.toml  2023-03-30 15:43:35.000000000 
+0200
@@ -3,7 +3,7 @@
 Instance = "https://openqa.suse.de";              # openQA instance to query
 RabbitMQ = "amqps://suse:[email protected]"    # RabbitMQ instance to query
 RabbitMQTopic = "suse.openqa.job.done"           # RabbitMQ topic to query
-HideStatus = [ "scheduled", "passed", "softfailed", "running", "reviewed" ]    
       # Hide scheduled and passed jobs
+HideStatus = ["scheduled", "passed", "softfailed", "cancelled", "skipped", 
"running", "uploading", "parallel_failed", "reviewed" ]           # Hide 
scheduled and passed 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")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/openqa-mon-0.27/_review/qec-publiccloud.toml 
new/openqa-mon-1.0.1/_review/qec-publiccloud.toml
--- old/openqa-mon-0.27/_review/qec-publiccloud.toml    2023-03-13 
10:29:03.000000000 +0100
+++ new/openqa-mon-1.0.1/_review/qec-publiccloud.toml   2023-03-30 
15:43:35.000000000 +0200
@@ -3,7 +3,7 @@
 Instance = "https://openqa.suse.de";              # openQA instance to query
 RabbitMQ = "amqps://suse:[email protected]"    # RabbitMQ instance to query
 RabbitMQTopic = "suse.openqa.job.done"           # RabbitMQ topic to query
-HideStatus = [ "scheduled", "passed", "softfailed", "cancelled", "skipped", 
"running", "reviewed" ]           # Hide scheduled and passed jobs
+HideStatus = ["scheduled", "passed", "softfailed", "cancelled", "skipped", 
"running", "uploading", "parallel_failed", "reviewed" ]           # Hide 
scheduled and passed jobs
 Notify = false                                   # No notifications by default
 RefreshInterval = 60                             # Refresh from API once every 
minute
 MaxJobs = 20                                     # Max. job per group to 
display
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/openqa-mon-0.27/cmd/openqa-mon/openqa-mon.go 
new/openqa-mon-1.0.1/cmd/openqa-mon/openqa-mon.go
--- old/openqa-mon-0.27/cmd/openqa-mon/openqa-mon.go    2023-03-13 
10:29:03.000000000 +0100
+++ new/openqa-mon-1.0.1/cmd/openqa-mon/openqa-mon.go   2023-03-30 
15:43:35.000000000 +0200
@@ -17,7 +17,7 @@
        "github.com/grisu48/gopenqa"
 )
 
-const VERSION = "0.8.0"
+const VERSION = "1.0.1"
 
 var config Config
 var tui TUI
@@ -677,6 +677,17 @@
                singleCall(remotes)
                os.Exit(0)
        }
+
+       // Refresh rates below 30 seconds are not allowed on public instances
+       if config.Continuous < 30 {
+               for _, remote := range remotes {
+                       if strings.Contains(remote.URI, "://openqa.suse.de") || 
strings.Contains(remote.URI, "://openqa.opensuse.org") {
+                               config.Continuous = 30
+                               break
+                       }
+               }
+       }
+
        tui = CreateTUI()
        tui.EnterAltScreen()
        tui.Clear()
@@ -700,7 +711,7 @@
        for i, remote := range remotes {
                instance := gopenqa.CreateInstance(ensureHTTP(remote.URI))
                instance.SetUserAgent("openqa-mon")
-               instance.SetMaxRecursionDepth(100) // Certain jobs (e.g. 
verification runs) can have a lot of clones
+               instance.SetMaxRecursionDepth(20) // Certain jobs (e.g. 
verification runs) can have a lot of clones
                // If no jobs are defined, fetch overview
                if len(remote.Jobs) == 0 {
                        overview, err := instance.GetOverview("", 
gopenqa.EmptyParams())
@@ -713,23 +724,21 @@
                } else {
                        // Fetch individual jobs
                        jobsModified := false // If remote.Jobs has been 
modified (e.g. id changes when detecting a restarted job)
-                       for i, id := range remote.Jobs {
-                               var job gopenqa.Job
-                               var err error
-                               if config.Follow {
-                                       job, err = instance.GetJobFollow(id)
-                               } else {
-                                       job, err = instance.GetJob(id)
-                               }
-                               if err != nil {
-                                       // It's better to ignore a single 
failure than to suppress following jobs as well
-                                       continue
-                               }
-                               if job.ID != id {
+                       jobs, err := instance.GetJobs(remote.Jobs)
+                       if err != nil {
+                               return remotes, err
+                       }
+                       for i, job := range jobs {
+                               if config.Follow && (job.IsCloned()) {
+                                       job, err = instance.GetJobFollow(job.ID)
+                                       if err != nil {
+                                               // It's better to ignore a 
single failure than to suppress following jobs as well
+                                               continue
+                                       }
                                        remote.Jobs[i] = job.ID
                                        jobsModified = true
                                }
-                               callback(id, job)
+                               callback(job.ID, job)
 
                                // Fetch children
                                if config.Hierarchy {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/openqa-mon-0.27/cmd/openqa-mq/openqa-mq.go 
new/openqa-mon-1.0.1/cmd/openqa-mq/openqa-mq.go
--- old/openqa-mon-0.27/cmd/openqa-mq/openqa-mq.go      2023-03-13 
10:29:03.000000000 +0100
+++ new/openqa-mon-1.0.1/cmd/openqa-mq/openqa-mq.go     2023-03-30 
15:43:35.000000000 +0200
@@ -10,7 +10,7 @@
        "github.com/streadway/amqp"
 )
 
-const VERSION = "1.2"
+const VERSION = "1.0.1"
 
 type Config struct {
        Remote     string   // Remote address
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/openqa-mon-0.27/cmd/openqa-revtui/openqa-revtui.go 
new/openqa-mon-1.0.1/cmd/openqa-revtui/openqa-revtui.go
--- old/openqa-mon-0.27/cmd/openqa-revtui/openqa-revtui.go      2023-03-13 
10:29:03.000000000 +0100
+++ new/openqa-mon-1.0.1/cmd/openqa-revtui/openqa-revtui.go     2023-03-30 
15:43:35.000000000 +0200
@@ -11,7 +11,7 @@
        "github.com/grisu48/gopenqa"
 )
 
-const VERSION = "0.4.0"
+const VERSION = "1.0.1"
 
 /* Group is a single configurable monitoring unit. A group contains all 
parameters that will be queried from openQA */
 type Group struct {
@@ -36,6 +36,7 @@
 
 var cf Config
 var knownJobs []gopenqa.Job
+var updatedRefresh bool
 
 func (cf *Config) LoadToml(filename string) error {
        if _, err := toml.DecodeFile(filename, cf); err != nil {
@@ -126,8 +127,29 @@
        return deltaT > maxlifetime
 }
 
-func isReviewed(job gopenqa.Job, instance gopenqa.Instance) (bool, error) {
-       comments, err := instance.GetComments(job.ID)
+func isReviewed(job gopenqa.Job, instance gopenqa.Instance, checkParallel 
bool) (bool, error) {
+       reviewed, err := checkReviewed(job.ID, instance)
+       if err != nil || reviewed {
+               return reviewed, err
+       }
+
+       // If not reviewed but "parallel_failed", check parallel jobs if they 
are reviewed
+       if checkParallel {
+               for _, childID := range job.Children.Parallel {
+                       reviewed, err := checkReviewed(childID, instance)
+                       if err != nil {
+                               return reviewed, err
+                       }
+                       if reviewed {
+                               return true, nil
+                       }
+               }
+       }
+       return false, nil
+}
+
+func checkReviewed(job int64, instance gopenqa.Instance) (bool, error) {
+       comments, err := instance.GetComments(job)
        if err != nil {
                return false, nil
        }
@@ -136,11 +158,11 @@
                        return true, nil
                }
                // Manually check for poo or bsc reference
-               if strings.Contains(c.Text, "poo#") || strings.Contains(c.Text, 
"bsc#") {
+               if strings.Contains(c.Text, "poo#") || strings.Contains(c.Text, 
"bsc#") || strings.Contains(c.Text, "boo#") {
                        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=") {
+               if strings.Contains(c.Text, "://progress.opensuse.org/issues/") 
|| strings.Contains(c.Text, "://bugzilla.suse.com/show_bug.cgi?id=") || 
strings.Contains(c.Text, "://bugzilla.opensuse.org/show_bug.cgi?id=") {
                        return true, nil
                }
        }
@@ -168,7 +190,7 @@
                if err != nil {
                        return job, err
                }
-               if job.CloneID != 0 && job.CloneID != job.ID {
+               if job.IsCloned() {
                        id = job.CloneID
                        time.Sleep(100 * time.Millisecond) // Don't spam the 
instance
                        continue
@@ -178,9 +200,33 @@
        return job, fmt.Errorf("max recursion depth reached")
 }
 
-func FetchJobs(instance gopenqa.Instance) ([]gopenqa.Job, error) {
+/* Fetch the given jobs from the instance at once */
+func fetchJobs(ids []int64, instance gopenqa.Instance) ([]gopenqa.Job, error) {
+       jobs := make([]gopenqa.Job, 0)
+
+       jobs, err := instance.GetJobs(ids)
+       if err != nil {
+               return jobs, err
+       }
+
+       // Get cloned jobs, if present
+       for i, job := range jobs {
+               if job.IsCloned() {
+                       job, err = FetchJob(job.ID, instance)
+                       if err != nil {
+                               return jobs, err
+                       }
+                       jobs[i] = job
+               }
+       }
+       return jobs, nil
+}
+
+type FetchJobsCallback func(int, int, int, int)
+
+func FetchJobs(instance gopenqa.Instance, callback FetchJobsCallback) 
([]gopenqa.Job, error) {
        ret := make([]gopenqa.Job, 0)
-       for _, group := range cf.Groups {
+       for i, group := range cf.Groups {
                params := group.Params
                jobs, err := instance.GetOverview("", params)
                if err != nil {
@@ -190,15 +236,24 @@
                if len(jobs) > cf.MaxJobs {
                        jobs = jobs[:cf.MaxJobs]
                }
-               // Get detailed job instances
+
+               // Get detailed job instances. Fetch them at once
+               ids := make([]int64, 0)
                for _, job := range jobs {
-                       if job, err = FetchJob(job.ID, instance); err != nil {
-                               return ret, err
-                       } else {
-                               // Filter too old jobs
-                               if !isJobTooOld(job, group.MaxLifetime) {
-                                       ret = append(ret, job)
-                               }
+                       ids = append(ids, job.ID)
+               }
+               if callback != nil {
+                       // Add one to the counter to indicate the progress to 
humans (0/16 looks weird)
+                       callback(i+1, len(cf.Groups), 0, len(jobs))
+               }
+               jobs, err = fetchJobs(ids, instance)
+               if err != nil {
+                       return jobs, err
+               }
+               for _, job := range jobs {
+                       // Filter too old jobs
+                       if !isJobTooOld(job, group.MaxLifetime) {
+                               ret = append(ret, job)
                        }
                }
        }
@@ -214,21 +269,26 @@
        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(orig_id int64, job gopenqa.Job, instance gopenqa.Instance) 
(gopenqa.Job, bool) {
+/** Updates the known job based on the Job ID. Returns true if it has been 
updated and the updated instance and false if and the job if the job id hasn't 
been found */
+func updateJob(job gopenqa.Job) (gopenqa.Job, bool) {
        for i, j := range knownJobs {
-               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
-                       } else {
-                               return job, false
-                       }
+               if j.ID == job.ID {
+                       knownJobs[i] = job
+                       return knownJobs[i], true
                }
        }
        return job, false
 }
 
+func getKnownJob(id int64) (gopenqa.Job, bool) {
+       for _, j := range knownJobs {
+               if j.ID == id {
+                       return j, true
+               }
+       }
+       return gopenqa.Job{}, false
+}
+
 /** Try to update the job with the given status, if present. Returns the found 
job and true if the job was present */
 func updateJobStatus(status gopenqa.JobStatus) (gopenqa.Job, bool) {
        var job gopenqa.Job
@@ -422,6 +482,15 @@
        instance := gopenqa.CreateInstance(cf.Instance)
        instance.SetUserAgent("openqa-mon/revtui")
 
+       // Refresh rates below 5 minutes are not allowed on public instances 
due to the rather large load it puts on them
+       updatedRefresh = false
+       if cf.RefreshInterval < 300 {
+               if strings.Contains(cf.Instance, "://openqa.suse.de") || 
strings.Contains(cf.Instance, "://openqa.opensuse.org") {
+                       cf.RefreshInterval = 300
+                       updatedRefresh = true
+               }
+       }
+
        // Run TUI and use the return code
        tui := CreateTUI()
        switch cf.GroupBy {
@@ -445,37 +514,49 @@
 func refreshJobs(tui *TUI, instance gopenqa.Instance) error {
        // Get fresh jobs
        status := tui.Status()
-       tui.SetStatus("Refreshing jobs ... ")
+       oldJobs := tui.Model.Jobs()
+       tui.SetStatus(fmt.Sprintf("Refreshing %d jobs ... ", len(oldJobs)))
        tui.Update()
-       jobs := tui.Model.Jobs()
-       for i, job := range jobs {
-               orig_id := job.ID
-               job, err := FetchJob(job.ID, instance)
-               if err != nil {
-                       return err
+       // Refresh all jobs at once in one request
+       ids := make([]int64, 0)
+       for _, job := range oldJobs {
+               ids = append(ids, job.ID)
+       }
+       jobs, err := instance.GetJobs(ids)
+       if err != nil {
+               return err
+       }
+       for _, job := range jobs {
+               updated := false
+               if j, found := getKnownJob(job.ID); found {
+                       updated = j.ID != job.ID || j.State != job.State || 
j.Result != job.Result
+               } else {
+                       updated = true
                }
-               job, found := updateJob(orig_id, job, instance)
-               if found {
+
+               if updated {
                        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
+               tui.Update()
+               // Scan failed jobs for comments
                state := job.JobState()
-               if state == "failed" || state == "incomplete" {
-                       reviewed, err := isReviewed(job, instance)
+               if state == "failed" || state == "incomplete" || state == 
"parallel_failed" {
+                       reviewed, err := isReviewed(job, instance, state == 
"parallel_failed")
                        if err != nil {
                                return err
                        }
+
                        tui.Model.SetReviewed(job.ID, reviewed)
                        tui.Update()
                }
        }
+       knownJobs = jobs
        tui.Model.Apply(jobs)
        tui.SetStatus(status)
        tui.Update()
@@ -530,6 +611,9 @@
 
        fmt.Println(title)
        fmt.Println("")
+       if updatedRefresh {
+               fmt.Printf(ANSI_YELLOW + "WARNING: For OSD and O3 a rate limit 
of 5 minutes between polling has been applied." + ANSI_RESET + "\n\n")
+       }
        fmt.Printf("Initial querying instance %s ... \n", cf.Instance)
        fmt.Println("\tGet job groups ... ")
        jobgroups, err := FetchJobGroups(instance)
@@ -540,16 +624,27 @@
                fmt.Fprintf(os.Stderr, "Warn: No job groups\n")
        }
        tui.Model.SetJobGroups(jobgroups)
-       fmt.Printf("\tGet jobs for %d groups ... \n", len(cf.Groups))
-       jobs, err := FetchJobs(instance)
+       fmt.Print("\033[s") // Save cursor position
+       fmt.Printf("\tGet jobs for %d groups ...", len(cf.Groups))
+       jobs, err := FetchJobs(instance, func(group int, groups int, job int, 
jobs int) {
+               fmt.Print("\033[u") // Restore cursor position
+               fmt.Print("\033[K") // Erase till end of line
+               fmt.Printf("\tGet jobs for %d groups ... %d/%d", 
len(cf.Groups), group, groups)
+               if job == 0 {
+                       fmt.Printf(" (%d jobs)", jobs)
+               } else {
+                       fmt.Printf(" (%d/%d jobs)", job, jobs)
+               }
+       })
+       fmt.Println()
        if err != nil {
                return fmt.Errorf("Error fetching jobs: %s", err)
        }
        // Failed jobs will be also scanned for comments
        for _, job := range jobs {
                state := job.JobState()
-               if state == "failed" || state == "incomplete" {
-                       reviewed, err := isReviewed(job, instance)
+               if state == "failed" || state == "incomplete" || state == 
"parallel_failed" {
+                       reviewed, err := isReviewed(job, instance, state == 
"parallel_failed")
                        if err != nil {
                                return fmt.Errorf("Error fetching job comment: 
%s", err)
                        }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/openqa-mon-0.27/cmd/openqa-revtui/tui.go 
new/openqa-mon-1.0.1/cmd/openqa-revtui/tui.go
--- old/openqa-mon-0.27/cmd/openqa-revtui/tui.go        2023-03-13 
10:29:03.000000000 +0100
+++ new/openqa-mon-1.0.1/cmd/openqa-revtui/tui.go       2023-03-30 
15:43:35.000000000 +0200
@@ -273,8 +273,9 @@
                if state == s {
                        return true
                }
+
                // Special reviewed keyword
-               if s == "reviewed" && state == "failed" {
+               if s == "reviewed" && (state == "failed" || state == 
"parallel_failed" || state == "incomplete") {
                        if reviewed, found := tui.Model.reviewed[job.ID]; found 
&& reviewed {
                                return true
                        }
@@ -306,6 +307,21 @@
        return ret
 }
 
+func jobGroupHeader(group gopenqa.JobGroup, width int) string {
+       if width <= 0 {
+               return ""
+       }
+       line := fmt.Sprintf("===== %s =====", group.Name)
+       for len(line) < width {
+               line += "="
+       }
+       // Crop if necessary
+       if len(line) > width {
+               line = line[:width]
+       }
+       return line
+}
+
 func (tui *TUI) buildJobsScreenByGroup(width int) []string {
        lines := make([]string, 0)
 
@@ -337,7 +353,8 @@
                } else {
                        lines = append(lines, "")
                }
-               lines = append(lines, fmt.Sprintf("===== %s 
====================", grp.Name))
+               lines = append(lines, jobGroupHeader(grp, width))
+
                for _, job := range jobs {
                        if !tui.hideJob(job) {
                                lines = append(lines, tui.formatJobLine(job, 
width))
@@ -356,7 +373,23 @@
                stats := sortedKeys(statC)
                for _, s := range stats {
                        c := statC[s]
-                       line += fmt.Sprintf(", %s: %d", s, c)
+                       line += ", "
+                       // Add some color
+                       if s == "passed" {
+                               line += ANSI_GREEN
+                       } else if s == "cancelled" {
+                               line += ANSI_MAGENTA
+                       } else if s == "failed" || s == "parallel_failed" || s 
== "incomplete" {
+                               line += ANSI_RED
+                       } else if s == "softfailed" {
+                               line += ANSI_YELLOW
+                       } else if s == "uploading" || s == "scheduled" || s == 
"running" {
+                               line += ANSI_BLUE
+                       } else if s == "skipped" {
+                               line += ANSI_WHITE
+                       }
+                       line += fmt.Sprintf("%s: %d", s, c)
+                       line += ANSI_RESET // Clear color
                }
                if hidden > 0 {
                        line += fmt.Sprintf(" (hidden: %d)", hidden)
@@ -458,10 +491,15 @@
        tui.Model.mutex.Lock()
        defer tui.Model.mutex.Unlock()
        width, height := terminalSize()
-       if width < 0 || height < 0 {
+       if width <= 0 || height <= 0 {
                return
        }
 
+       // Check for unreasonable values
+       if width > 1000 {
+               width = 1000
+       }
+
        // Header and footer are separate. We only scroll through the "screen"
        screen := tui.buildScreen(width)
        header := tui.buildHeader(width)
@@ -487,7 +525,7 @@
        if len(footer) > remainingLines {
                footer = make([]string, 0)
        } else {
-               remainingLines -= (len(footer) + 1)
+               remainingLines -= len(footer)
        }
 
        // Print screen
@@ -506,9 +544,9 @@
 
        // Print footer
        if len(footer) > 0 {
-               fmt.Println("")
                for _, line := range footer {
-                       fmt.Println(line)
+                       fmt.Println("")
+                       fmt.Print(line)
                }
        }
 }
@@ -529,7 +567,7 @@
                return ANSI_GREEN
        } else if state == "softfail" || state == "softfailed" {
                return ANSI_YELLOW
-       } else if state == "fail" || state == "failed" || state == "incomplete" 
{
+       } else if state == "fail" || state == "failed" || state == "incomplete" 
|| state == "parallel_failed" {
                return ANSI_RED
        } else if state == "cancelled" || state == "user_cancelled" {
                return ANSI_MAGENTA
@@ -571,7 +609,7 @@
                tStr = timestamp.Format("2006-01-02-15:04:05")
        }
        // For failed jobs check if they are reviewed
-       if job.Result == "failed" || job.Result == "incomplete" {
+       if state == "failed" || state == "incomplete" || state == 
"parallel_failed" {
                if reviewed, found := tui.Model.reviewed[job.ID]; found && 
reviewed {
                        c2 = ANSI_MAGENTA
                }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/openqa-mon-0.27/go.mod new/openqa-mon-1.0.1/go.mod
--- old/openqa-mon-0.27/go.mod  2023-03-13 10:29:03.000000000 +0100
+++ new/openqa-mon-1.0.1/go.mod 2023-03-30 15:43:35.000000000 +0200
@@ -4,7 +4,7 @@
 
 require (
        github.com/BurntSushi/toml v1.2.1
-       github.com/grisu48/gopenqa v0.5.2
+       github.com/grisu48/gopenqa v0.7.1
        github.com/streadway/amqp v1.0.0
        golang.org/x/crypto v0.4.0
 )
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/openqa-mon-0.27/go.sum new/openqa-mon-1.0.1/go.sum
--- old/openqa-mon-0.27/go.sum  2023-03-13 10:29:03.000000000 +0100
+++ new/openqa-mon-1.0.1/go.sum 2023-03-30 15:43:35.000000000 +0200
@@ -2,8 +2,8 @@
 github.com/BurntSushi/toml v1.2.1/go.mod 
h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
 github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
 github.com/google/go-cmp v0.5.5/go.mod 
h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/grisu48/gopenqa v0.5.2 
h1:ofiN5Q9KIpeI+8Eq+Mh+Smk+Ia3q70nHz7EKKpJtcZU=
-github.com/grisu48/gopenqa v0.5.2/go.mod 
h1:D7EFTPhtzNvnHnDol9UoPCFmnzOiLBVa1tOOYqJDgGo=
+github.com/grisu48/gopenqa v0.7.1 
h1:XhmsWY93WjqdnXmdDjjHTFdfLjQwz7kApviDM9lX27U=
+github.com/grisu48/gopenqa v0.7.1/go.mod 
h1:D7EFTPhtzNvnHnDol9UoPCFmnzOiLBVa1tOOYqJDgGo=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod 
h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/streadway/amqp v1.0.0 
h1:kuuDrUJFZL1QYL9hUNuCxNObNzB0bV/ZG5jV3RWAQgo=

++++++ openqa-mon.obsinfo ++++++
--- /var/tmp/diff_new_pack.35bpAt/_old  2023-04-01 19:32:51.293540486 +0200
+++ /var/tmp/diff_new_pack.35bpAt/_new  2023-04-01 19:32:51.297540507 +0200
@@ -1,5 +1,5 @@
 name: openqa-mon
-version: 0.27
-mtime: 1678699743
-commit: f3c259de42cfcbff0afd4285313b48f8d9475a84
+version: 1.0.1
+mtime: 1680183815
+commit: 53cb17dd5a792df874a72a88248f3ef87556255a
 

++++++ vendor.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vendor/github.com/grisu48/gopenqa/gopenqa.go 
new/vendor/github.com/grisu48/gopenqa/gopenqa.go
--- old/vendor/github.com/grisu48/gopenqa/gopenqa.go    2023-03-16 
10:37:09.000000000 +0100
+++ new/vendor/github.com/grisu48/gopenqa/gopenqa.go    2023-03-30 
15:45:47.000000000 +0200
@@ -11,6 +11,7 @@
        "net/url"
        "os"
        "strings"
+       "sync"
        "time"
 )
 
@@ -20,8 +21,10 @@
        apikey        string
        apisecret     string
        verbose       bool
-       maxRecursions int    // Maximum number of recursions
-       userAgent     string // Useragent sent with the request
+       maxRecursions int        // Maximum number of recursions
+       userAgent     string     // Useragent sent with the request
+       allowParallel bool       // Allow parallel requests (default: No)
+       mutFetching   sync.Mutex // Mutex to ensure only one request at the 
time is performed
 }
 
 // the settings are given as dict:
@@ -132,13 +135,21 @@
        return params.Encode()
 }
 
+func ExtractJobIDS(jobs []Job) []int64 {
+       ret := make([]int64, 0)
+       for _, job := range jobs {
+               ret = append(ret, job.ID)
+       }
+       return ret
+}
+
 func EmptyParams() map[string]string {
        return make(map[string]string, 0)
 }
 
 /* Create a openQA instance module */
 func CreateInstance(url string) Instance {
-       inst := Instance{URL: url, maxRecursions: 10, verbose: false, 
userAgent: "gopenqa"}
+       inst := Instance{URL: url, maxRecursions: 10, verbose: false, 
userAgent: "gopenqa", allowParallel: false}
        return inst
 }
 
@@ -168,6 +179,11 @@
        i.userAgent = userAgent
 }
 
+// Set to allow or disallow parallel requests to the instance
+func (i *Instance) SetAllowParallel(allow bool) {
+       i.allowParallel = allow
+}
+
 func assignInstance(jobs []Job, instance *Instance) []Job {
        for i, j := range jobs {
                j.instance = instance
@@ -226,6 +242,12 @@
  * Add the APIKEY and APISECRET credentials, if given
  */
 func (i *Instance) request(method string, url string, data []byte) ([]byte, 
error) {
+       // Request mutex to ensure, only one request at the time
+       if !i.allowParallel {
+               i.mutFetching.Lock()
+               defer i.mutFetching.Unlock()
+       }
+
        contentType := ""
        if data == nil {
                data = make([]byte, 0)
@@ -337,6 +359,9 @@
                return jobs.Jobs, err
        }
        err = json.Unmarshal(resp, &jobs)
+       if err != nil {
+               return jobs.Jobs, err
+       }
 
        // Now, get only the latest job per group_id
        mapped := make(map[int]Job)
@@ -360,19 +385,41 @@
                ret = append(ret, v)
        }
        return ret, nil
+}
 
+func (job *Job) applyInstance(i *Instance) {
+       job.Link = fmt.Sprintf("%s/tests/%d", i.URL, job.ID)
+       job.instance = i
+       job.Remote = i.URL
 }
 
 // GetJob fetches detailled job information
 func (i *Instance) GetJob(id int64) (Job, error) {
        url := fmt.Sprintf("%s/api/v1/jobs/%d", i.URL, id)
        job, err := i.fetchJob(url)
-       job.Link = fmt.Sprintf("%s/tests/%d", i.URL, id)
-       job.instance = i
-       job.Remote = i.URL
        return job, err
 }
 
+// GetJob fetches detailled information about a list of jobs
+func (i *Instance) GetJobs(ids []int64) ([]Job, error) {
+       if len(ids) == 0 {
+               return make([]Job, 0), nil
+       }
+       url := fmt.Sprintf("%s/api/v1/jobs", i.URL)
+       // Add job ids to URL
+       // Note: I'm not using strings.Join because that requires me to first 
convert ids to a []string and I believe the following approach is not worse
+       first := true
+       for _, id := range ids {
+               if first {
+                       first = false
+                       url = fmt.Sprintf("%s?ids=%d", url, id)
+               } else {
+                       url = fmt.Sprintf("%s&ids=%d", url, id)
+               }
+       }
+       return i.fetchJobsArray(url)
+}
+
 func (i *Instance) DeleteJob(id int64) error {
        url := fmt.Sprintf("%s/api/v1/jobs/%d", i.URL, id)
        buf, err := i.delete(url, nil)
@@ -383,23 +430,26 @@
 }
 
 // GetJob fetches detailled job information and follows the job, if it 
contains a CloneID
-func (i *Instance) GetJobFollow(id int64) (Job, error) {
-       recursions := 0 // keep track of the number of recursions
-fetch:
-       url := fmt.Sprintf("%s/api/v1/jobs/%d", i.URL, id)
-       job, err := i.fetchJob(url)
-       if job.CloneID != 0 && job.CloneID != job.ID {
-               recursions++
-               if i.maxRecursions != 0 && recursions >= i.maxRecursions {
-                       return job, fmt.Errorf("maximum recusion depth reached")
+func (inst *Instance) GetJobFollow(id int64) (Job, error) {
+       for recursion := 0; recursion < inst.maxRecursions; recursion++ {
+               url := fmt.Sprintf("%s/api/v1/jobs/%d", inst.URL, id)
+               job, err := inst.fetchJob(url)
+               if err != nil {
+                       return job, err
+               }
+               if job.IsCloned() {
+                       id = job.CloneID
+                       continue
                }
-               id = job.CloneID
-               goto fetch
+               return job, nil
        }
-       job.Link = fmt.Sprintf("%s/tests/%d", i.URL, id)
-       job.instance = i
-       job.Remote = i.URL
-       return job, err
+       return Job{}, fmt.Errorf("maximum recusion depth reached")
+}
+
+// GetJobState uses the (currently experimental) API call to quickly fetch a 
job state
+func (i *Instance) GetJobState(id int64) (JobState, error) {
+       url := fmt.Sprintf("%s/api/v1//experimental/jobs/%d/status", i.URL, id)
+       return i.fetchJobState(url)
 }
 
 func (i *Instance) GetJobGroups() ([]JobGroup, error) {
@@ -467,21 +517,44 @@
        return i.fetchWorkers(url)
 }
 
-func (i *Instance) fetchJobs(url string) ([]Job, error) {
+// fetchJobs fetches the given url and returns all jobs returned by it (as 
direct array)
+func (inst *Instance) fetchJobs(url string) ([]Job, error) {
        jobs := make([]Job, 0)
 
-       resp, err := i.get(url, nil)
+       resp, err := inst.get(url, nil)
        if err != nil {
                return jobs, err
        }
        err = json.Unmarshal(resp, &jobs)
+       for i, job := range jobs {
+               job.applyInstance(inst)
+               jobs[i] = job
+       }
        return jobs, err
 }
 
-func (i *Instance) fetchJobGroups(url string) ([]JobGroup, error) {
+// fetchJobs fetches the given url and returns all jobs, It expects the jobs 
to be within the "jobs" dict of the result
+func (inst *Instance) fetchJobsArray(url string) ([]Job, error) {
+       type ResultJob struct { // Expected result structure
+               Jobs []Job `json:"jobs"`
+       }
+       var ret ResultJob
+       resp, err := inst.get(url, nil)
+       if err != nil {
+               return make([]Job, 0), err
+       }
+       err = json.Unmarshal(resp, &ret)
+       for i, job := range ret.Jobs {
+               job.applyInstance(inst)
+               ret.Jobs[i] = job
+       }
+       return ret.Jobs, err
+}
+
+func (inst *Instance) fetchJobGroups(url string) ([]JobGroup, error) {
        jobs := make([]JobGroup, 0)
 
-       resp, err := i.get(url, nil)
+       resp, err := inst.get(url, nil)
        if err != nil {
                return jobs, err
        }
@@ -539,20 +612,31 @@
        return make([]Machine, 0), nil
 }
 
-func (i *Instance) fetchJob(url string) (Job, error) {
+func (inst *Instance) fetchJob(url string) (Job, error) {
        type ResultJob struct { // Expected result structure
                Job Job `json:"job"`
        }
        var job ResultJob
-       resp, err := i.get(url, nil)
+       resp, err := inst.get(url, nil)
        if err != nil {
                return job.Job, err
        }
        // TODO: Sometimes SizeLimit is returned as string but it should be an 
int. Fix this.
        err = json.Unmarshal(resp, &job)
+       job.Job.applyInstance(inst)
        return job.Job, err
 }
 
+func (i *Instance) fetchJobState(url string) (JobState, error) {
+       var state JobState
+       resp, err := i.get(url, nil)
+       if err != nil {
+               return state, err
+       }
+       err = json.Unmarshal(resp, &state)
+       return state, err
+}
+
 /* merge given parameter string to URL parameters */
 func mergeParams(params map[string]string) string {
        if len(params) == 0 {
@@ -569,17 +653,24 @@
  * Fetch the given child jobs. Use with j.Children.Chained, 
j.Children.DirectlyChained and j.Children.Parallel
  * if follow is set to true, the method will return the cloned job instead of 
the original one, if present
  */
-func (j *Job) FetchChildren(children []int64, follow bool) ([]Job, error) {
-       jobs := make([]Job, 0)
-       for _, id := range children {
-               job, err := j.instance.GetJobFollow(id)
-               if err != nil {
-                       return jobs, err
+func (j *Job) FetchChildren(ids []int64, follow bool) ([]Job, error) {
+       children, err := j.instance.GetJobs(ids)
+       if err != nil {
+               return children, err
+       }
+       if follow {
+               for i, job := range children {
+                       // Fetch cloned job, if present
+                       if job.CloneID != 0 && job.CloneID != job.ID {
+                               job, err := j.instance.GetJobFollow(job.ID)
+                               if err != nil {
+                                       return children, err
+                               }
+                               children[i] = job
+                       }
                }
-               jobs = append(jobs, job)
        }
-
-       return jobs, nil
+       return children, nil
 }
 
 /* Fetch all child jobs
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vendor/github.com/grisu48/gopenqa/job.go 
new/vendor/github.com/grisu48/gopenqa/job.go
--- old/vendor/github.com/grisu48/gopenqa/job.go        2023-03-16 
10:37:09.000000000 +0100
+++ new/vendor/github.com/grisu48/gopenqa/job.go        2023-03-30 
15:45:47.000000000 +0200
@@ -42,10 +42,19 @@
        Machine string `json:"MACHINE"`
 }
 
+/* Special struct for getting quick job status */
+type JobState struct {
+       BlockedBy int64  `json:"blocked_by_id"`
+       Result    string `json:"result"`
+       State     string `json:"state"`
+}
+
 /* Format job as a string */
 func (j *Job) String() string {
        return fmt.Sprintf("%d %s (%s)", j.ID, j.Name, j.Test)
 }
+
+/* JobState returns or the job state or the result, if the job is done */
 func (j *Job) JobState() string {
        if j.State == "done" {
                return j.Result
@@ -53,6 +62,11 @@
        return j.State
 }
 
+/* IsCloned returns true, if the job has been cloned/restarted */
+func (j *Job) IsCloned() bool {
+       return j.CloneID != 0 && j.CloneID != j.ID
+}
+
 /* Compares two jobs according to their unique parameters (ID, GroupID, Test) 
*/
 func (j1 *Job) Equals(j2 Job) bool {
        // Compare only relevant parameters
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vendor/github.com/grisu48/gopenqa/rabbitmq.go 
new/vendor/github.com/grisu48/gopenqa/rabbitmq.go
--- old/vendor/github.com/grisu48/gopenqa/rabbitmq.go   2023-03-16 
10:37:09.000000000 +0100
+++ new/vendor/github.com/grisu48/gopenqa/rabbitmq.go   2023-03-30 
15:45:47.000000000 +0200
@@ -41,29 +41,80 @@
 type RabbitMQ struct {
        remote string
        con    *amqp.Connection
+       closed bool
 }
 
+// Callback when the connection was closed
+type RabbitMQCloseCallback func(error)
+
 // Close connection
 func (mq *RabbitMQ) Close() {
+       mq.closed = true
        mq.con.Close()
 }
 
+// Connected returns true if RabbitMQ is connected
+func (mq *RabbitMQ) Connected() bool {
+       return !mq.closed && !mq.con.IsClosed()
+}
+
+// Connected returns true if RabbitMQ is closing or if it is closed.
+func (mq *RabbitMQ) Closed() bool {
+       if mq.closed {
+               return true
+       }
+       if mq.con.IsClosed() {
+               mq.closed = true
+               return true
+       }
+       return false
+}
+
+// Reconnect to the RabbitMQ server. This will close any previous connections 
and channels
+func (mq *RabbitMQ) Reconnect() error {
+       var err error
+       mq.con.Close()
+       mq.closed = false
+       mq.con, err = amqp.Dial(mq.remote)
+       return err
+}
+
+// NotifyClose registeres a defined callback function for when the RabbitMQ 
connection is closed
+func (mq *RabbitMQ) NotifyClose(callback RabbitMQCloseCallback) {
+       go func() {
+               recvChannel := make(chan *amqp.Error, 1)
+               mq.con.NotifyClose(recvChannel)
+               for err := range recvChannel {
+                       callback(fmt.Errorf(err.Error()))
+               }
+       }()
+}
+
 // RabbitMQSubscription handles a single subscription
 type RabbitMQSubscription struct {
        channel *amqp.Channel
        key     string
        obs     <-chan amqp.Delivery
        mq      *RabbitMQ
+       con     *amqp.Connection // Keep a reference to the connection to check 
if it is still connected. This is necessary because mq can reconnect and 
therefore have another new mq.con instance
+}
+
+// Connected returns true if RabbitMQ is connected
+func (sub *RabbitMQSubscription) Connected() bool {
+       return !sub.con.IsClosed()
 }
 
 // Receive receives a raw non-empty RabbitMQ messages
 func (sub *RabbitMQSubscription) Receive() (amqp.Delivery, error) {
-       for {
-               msg := <-sub.obs
+       for msg, ok := <-sub.obs; ok; {
                if len(msg.Body) > 0 {
                        return msg, nil
                }
        }
+       if sub.mq == nil || sub.mq.closed || sub.con == nil || 
sub.con.IsClosed() {
+               return amqp.Delivery{}, fmt.Errorf("EOF")
+       }
+       return amqp.Delivery{}, fmt.Errorf("channel unexpectedly closed")
 }
 
 // ReceiveJob receives the next message and try to parse it as job
@@ -174,7 +225,7 @@
 // ConnectRabbitMQ connects to a RabbitMQ instance and returns the RabbitMQ 
object
 func ConnectRabbitMQ(remote string) (RabbitMQ, error) {
        var err error
-       rmq := RabbitMQ{remote: remote}
+       rmq := RabbitMQ{remote: remote, closed: false}
 
        rmq.con, err = amqp.Dial(remote)
        if err != nil {
@@ -213,5 +264,6 @@
        sub.channel = ch
        sub.key = key
        sub.obs = obs
+       sub.con = mq.con
        return sub, nil
 }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vendor/modules.txt new/vendor/modules.txt
--- old/vendor/modules.txt      2023-03-16 10:37:10.000000000 +0100
+++ new/vendor/modules.txt      2023-03-30 15:45:47.000000000 +0200
@@ -2,7 +2,7 @@
 ## explicit; go 1.16
 github.com/BurntSushi/toml
 github.com/BurntSushi/toml/internal
-# github.com/grisu48/gopenqa v0.5.2
+# github.com/grisu48/gopenqa v0.7.1
 ## explicit; go 1.14
 github.com/grisu48/gopenqa
 # github.com/streadway/amqp v1.0.0

Reply via email to