Repository: flant/shell-operator
Pull request #509 (2023-06-16 20:30:25)

[feature] Add format for `queue list` command
<!--
Thank you for sending a pull request! Here some tips for contributors:

1. Fill the description template below.
2. Include appropriate tests (if necessary). Make sure that all CI checks passed.
3. If the Pull Request is a work in progress, make use of GitHub's "Draft PR" feature and mark it as such.
-->

#### Overview

Add format(text, json, yaml) for the `queue list` command
Empty queues are hide by default, you can add flag `--show-empty` to print them

#### What this PR does / why we need it

Output formatting were broken before.
Empty queues generated a lot of useless output.

#### Special notes for your reviewer
`text` format left as is.

`json` format looks like: 
```json
{
  "active": [
    {
      "name": "main",
      "tasksCount": 5,
      "tasks": [
        {
          "index": 1,
          "description": ":::test_task_main_0004"
        },
        {
          "index": 2,
          "description": ":::test_task_main_0003"
        },
        {
          "index": 3,
          "description": ":::test_task_main_0002"
        },
        {
          "index": 4,
          "description": ":::test_task_main_0001"
        },
        {
          "index": 5,
          "description": ":::test_task_main_0000"
        }
      ]
    },
    {
      "name": "active-queue",
      "tasksCount": 4,
      "tasks": [
        {
          "index": 1,
          "description": ":::test_task_active-queue_0003"
        },
        {
          "index": 2,
          "description": ":::test_task_active-queue_0002"
        },
        {
          "index": 3,
          "description": ":::test_task_active-queue_0001"
        },
        {
          "index": 4,
          "description": ":::test_task_active-queue_0000"
        }
      ]
    }
  ],
  "empty": [
    {
      "name": "empty",
      "tasksCount": 0
    }
  ],
  "summary": {
    "mainQueueTasks": 5,
    "otherQueues": {
      "active": 1,
      "empty": 1,
      "tasks": 4
    },
    "totalTasks": 9
  }
}
```

`yaml` format looks like:
```yaml
active:
  - name: main
    tasks:
      - description: ':::test_task_main_0004'
        index: 1
      - description: ':::test_task_main_0003'
        index: 2
      - description: ':::test_task_main_0002'
        index: 3
      - description: ':::test_task_main_0001'
        index: 4
      - description: ':::test_task_main_0000'
        index: 5
    tasksCount: 5
  - name: active-queue
    tasks:
      - description: ':::test_task_active-queue_0003'
        index: 1
      - description: ':::test_task_active-queue_0002'
        index: 2
      - description: ':::test_task_active-queue_0001'
        index: 3
      - description: ':::test_task_active-queue_0000'
        index: 4
    tasksCount: 4
empty:
  - name: empty
    tasksCount: 0
summary:
  mainQueueTasks: 5
  otherQueues:
    active: 1
    empty: 1
    tasks: 4
  totalTasks: 9

```



Patch:
diff --git a/pkg/debug/debug-cmd.go b/pkg/debug/debug-cmd.go
index 438a237..07f8ddd 100644
--- a/pkg/debug/debug-cmd.go
+++ b/pkg/debug/debug-cmd.go
@@ -9,7 +9,10 @@ import (
 	"github.com/flant/shell-operator/pkg/app"
 )
 
-var OutputFormat = "text"
+var (
+	outputFormat = "text"
+	showEmpty    = false
+)
 
 func DefineDebugCommands(kpApp *kingpin.Application) {
 	// Queue dump commands.
@@ -17,19 +20,22 @@ func DefineDebugCommands(kpApp *kingpin.Application) {
 
 	queueListCmd := queueCmd.Command("list", "Dump tasks in all queues.").
 		Action(func(c *kingpin.ParseContext) error {
-			out, err := Queue(DefaultClient()).List(OutputFormat)
+			out, err := Queue(DefaultClient()).List(outputFormat, showEmpty)
 			if err != nil {
 				return err
 			}
 			fmt.Println(string(out))
 			return nil
 		})
+	queueListCmd.Flag("show-empty", "Show empty queues.").Short('e').
+		Default("false").
+		BoolVar(&showEmpty)
 	AddOutputJsonYamlTextFlag(queueListCmd)
 	app.DefineDebugUnixSocketFlag(queueListCmd)
 
 	queueMainCmd := queueCmd.Command("main", "Dump tasks in the main queue.").
 		Action(func(c *kingpin.ParseContext) error {
-			out, err := Queue(DefaultClient()).Main(OutputFormat)
+			out, err := Queue(DefaultClient()).Main(outputFormat)
 			if err != nil {
 				return err
 			}
@@ -44,7 +50,7 @@ func DefineDebugCommands(kpApp *kingpin.Application) {
 
 	configListCmd := configCmd.Command("list", "List available runtime parameters.").
 		Action(func(c *kingpin.ParseContext) error {
-			out, err := Config(DefaultClient()).List(OutputFormat)
+			out, err := Config(DefaultClient()).List(outputFormat)
 			if err != nil {
 				return err
 			}
@@ -92,7 +98,7 @@ func DefineDebugCommandsSelf(kpApp *kingpin.Application) {
 	hookCmd := app.CommandWithDefaultUsageTemplate(kpApp, "hook", "Actions for hooks")
 	hookListCmd := hookCmd.Command("list", "List all hooks.").
 		Action(func(c *kingpin.ParseContext) error {
-			outBytes, err := Hook(DefaultClient()).List(OutputFormat)
+			outBytes, err := Hook(DefaultClient()).List(outputFormat)
 			if err != nil {
 				return err
 			}
@@ -106,7 +112,7 @@ func DefineDebugCommandsSelf(kpApp *kingpin.Application) {
 	var hookName string
 	hookSnapshotCmd := hookCmd.Command("snapshot", "Dump hook snapshots.").
 		Action(func(c *kingpin.ParseContext) error {
-			outBytes, err := Hook(DefaultClient()).Name(hookName).Snapshots(OutputFormat)
+			outBytes, err := Hook(DefaultClient()).Name(hookName).Snapshots(outputFormat)
 			if err != nil {
 				return err
 			}
@@ -121,7 +127,7 @@ func DefineDebugCommandsSelf(kpApp *kingpin.Application) {
 func AddOutputJsonYamlTextFlag(cmd *kingpin.CmdClause) {
 	cmd.Flag("output", "Output format: json|yaml|text.").Short('o').
 		Default("text").
-		EnumVar(&OutputFormat, "json", "yaml", "text")
+		EnumVar(&outputFormat, "json", "yaml", "text")
 }
 
 type QueueRequest struct {
@@ -134,8 +140,8 @@ func Queue(client *Client) *QueueRequest {
 	}
 }
 
-func (qr *QueueRequest) List(format string) ([]byte, error) {
-	url := fmt.Sprintf("http://unix/queue/list.%s", format)
+func (qr *QueueRequest) List(format string, showEmpty bool) ([]byte, error) {
+	url := fmt.Sprintf("http://unix/queue/list.%s?showEmpty=%t", format, showEmpty)
 	return qr.client.Get(url)
 }
 
diff --git a/pkg/shell-operator/debug_server.go b/pkg/shell-operator/debug_server.go
index 70e4a82..d4cd847 100644
--- a/pkg/shell-operator/debug_server.go
+++ b/pkg/shell-operator/debug_server.go
@@ -3,6 +3,7 @@ package shell_operator
 import (
 	"fmt"
 	"net/http"
+	"strconv"
 	"time"
 
 	"github.com/go-chi/chi/v5"
@@ -37,8 +38,14 @@ func RegisterDebugQueueRoutes(dbgSrv *debug.Server, op *ShellOperator) {
 		return dump.TaskQueueMainToText(op.TaskQueues), nil
 	})
 
-	dbgSrv.Route("/queue/list.{format:(json|yaml|text)}", func(_ *http.Request) (interface{}, error) {
-		return dump.TaskQueueSetToText(op.TaskQueues), nil
+	dbgSrv.Route("/queue/list.{format:(json|yaml|text)}", func(req *http.Request) (interface{}, error) {
+		showEmptyStr := req.URL.Query().Get("showEmpty")
+		showEmpty, err := strconv.ParseBool(showEmptyStr)
+		if err != nil {
+			showEmpty = false
+		}
+		format := debug.FormatFromRequest(req)
+		return dump.TaskQueues(op.TaskQueues, format, showEmpty), nil
 	})
 }
 
diff --git a/pkg/task/dump/dump.go b/pkg/task/dump/dump.go
index 7ea84ea..19d9d03 100644
--- a/pkg/task/dump/dump.go
+++ b/pkg/task/dump/dump.go
@@ -1,31 +1,34 @@
 package dump
 
 import (
+	"encoding/json"
 	"fmt"
 	"sort"
 	"strings"
 
+	"sigs.k8s.io/yaml"
+
 	"github.com/flant/shell-operator/pkg/task"
 	"github.com/flant/shell-operator/pkg/task/queue"
 )
 
-// AsQueueNames implements sort.Interface for array of queue names.
+// asQueueNames implements sort.Interface for array of queue names.
 // 'main' queue has the top priority. Other names are sorted as usual.
-type AsQueueNames []string
+type asQueueNames []dumpQueue
 
-func (a AsQueueNames) Len() int      { return len(a) }
-func (a AsQueueNames) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
-func (a AsQueueNames) Less(i, j int) bool {
+func (a asQueueNames) Len() int      { return len(a) }
+func (a asQueueNames) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
+func (a asQueueNames) Less(i, j int) bool {
 	p, q := a[i], a[j]
 
 	switch {
-	case q == queue.MainQueueName:
+	case q.Name == queue.MainQueueName:
 		return false
-	case p == queue.MainQueueName:
+	case p.Name == queue.MainQueueName:
 		return true
 	}
 	// both names are main or both are not main, so compare as usual.
-	return p < q
+	return p.Name < q.Name
 }
 
 // TaskQueueMainToText dumps only the 'main' queue.
@@ -42,65 +45,236 @@ func TaskQueueMainToText(tqs *queue.TaskQueueSet) string {
 	return buf.String()
 }
 
-// TaskQueueSetToText dumps all queues.
-func TaskQueueSetToText(tqs *queue.TaskQueueSet) string {
-	var buf strings.Builder
-
-	names := map[string][]string{
-		"active": make([]string, 0),
-		"empty":  make([]string, 0),
+// TaskQueues dumps all queues.
+func TaskQueues(tqs *queue.TaskQueueSet, format string, showEmpty bool) string {
+	result := dumpTaskQueues{
+		Empty:  make([]dumpQueue, 0),
+		Active: make([]dumpQueue, 0),
 	}
+
 	otherQueuesCount := 0
 	activeQueues := 0
 	emptyQueues := 0
 	tasksCount := 0
 	mainTasksCount := 0
-	hasMain := false
-	hasOthers := false
+
 	tqs.Iterate(func(queue *queue.TaskQueue) {
+		if queue == nil {
+			return
+		}
+
 		if queue.Name == tqs.MainName {
 			mainTasksCount = queue.Length()
-			hasMain = true
 			if queue.IsEmpty() {
-				names["empty"] = append(names["empty"], queue.Name)
+				mainQueue := dumpQueue{
+					Name:       queue.Name,
+					TasksCount: queue.Length(),
+				}
+				result.Empty = append(result.Empty, mainQueue)
+				result.MainQueue = &mainQueue
 			} else {
-				names["active"] = append(names["active"], queue.Name)
+				tasks := getTasksForQueue(queue)
+				mainQueue := dumpQueue{
+					Name:       queue.Name,
+					TasksCount: queue.Length(),
+					Status:     queue.Status,
+					Tasks:      tasks,
+				}
+				result.Active = append(result.Active, mainQueue)
+				result.MainQueue = &mainQueue
 			}
 			return
 		}
 
 		otherQueuesCount++
-		hasOthers = true
 		if queue.IsEmpty() {
 			emptyQueues++
-			names["empty"] = append(names["empty"], queue.Name)
+			result.Empty = append(result.Empty, dumpQueue{
+				Name: queue.Name,
+			})
 		} else {
 			activeQueues++
 			tasksCount += queue.Length()
-			names["active"] = append(names["active"], queue.Name)
+			tasks := getTasksForQueue(queue)
+			result.Active = append(result.Active, dumpQueue{
+				Name:       queue.Name,
+				TasksCount: queue.Length(),
+				Status:     queue.Status,
+				Tasks:      tasks,
+			})
 		}
 	})
 
-	sort.Sort(AsQueueNames(names["active"]))
-	sort.Sort(AsQueueNames(names["empty"]))
+	result.SortByName()
+
+	result.Summary = dumpSummary{
+		mainQueueTasksCount:    mainTasksCount,
+		otherQueuesActiveCount: activeQueues,
+		otherQueuesEmptyCount:  emptyQueues,
+		otherQueuesTasksCount:  tasksCount,
+		totalTasksCount:        mainTasksCount + tasksCount,
+	}
+
+	if showEmpty {
+		// Empty queues. Do not report single empty main queue.
+		if len(result.Empty) > 0 && otherQueuesCount == 0 {
+			showEmpty = false
+		}
+	}
+
+	return result.Format(format, showEmpty)
+}
+
+func pluralize(n int, zero, one, many string) string {
+	if n == 0 && zero != "" {
+		return zero
+	}
+	description := many
+	if n == 1 {
+		description = one
+	}
+	return fmt.Sprintf("%d %s", n, description)
+}
+
+// TaskQueueToText dumps all tasks in queue.
+func TaskQueueToText(q *queue.TaskQueue) string {
+	var buf strings.Builder
+	buf.WriteString(fmt.Sprintf("Queue '%s': length %d, status: '%s'\n", q.Name, q.Length(), q.Status))
+	buf.WriteString("\n")
+
+	index := 1
+	q.Iterate(func(task task.Task) {
+		buf.WriteString(fmt.Sprintf("%2d. ", index))
+		buf.WriteString(task.GetDescription())
+		buf.WriteString("\n")
+		index++
+	})
+
+	return buf.String()
+}
+
+func getTasksForQueue(q *queue.TaskQueue) []dumpTask {
+	tasks := make([]dumpTask, 0, q.Length())
+
+	index := 1
+	q.Iterate(func(task task.Task) {
+		tasks = append(tasks, dumpTask{
+			Index:       index,
+			Description: task.GetDescription(),
+		})
+		index++
+	})
+
+	return tasks
+}
+
+type dumpTaskQueues struct {
+	MainQueue *dumpQueue  `json:"-"`
+	Active    []dumpQueue `json:"active" yaml:"active"`
+	Empty     []dumpQueue `json:"empty,omitempty" yaml:"empty,omitempty"`
 
-	for i, name := range names["active"] {
-		q := tqs.GetByName(name)
+	Summary dumpSummary `json:"summary" yaml:"summary"`
+}
+
+type dumpQueue struct {
+	Name       string     `json:"name"`
+	TasksCount int        `json:"tasksCount"`
+	Status     string     `json:"status,omitempty"`
+	Tasks      []dumpTask `json:"tasks,omitempty"`
+}
+
+type dumpTask struct {
+	Index       int    `json:"index"`
+	Description string `json:"description"`
+}
+
+type dumpSummary struct {
+	// - 'main' queue: [empty | 1 task | 5 tasks].
+	// - 24 other queues (3 active, 21 empty): 56 tasks.
+	// - total 68 tasks to handle.
+
+	mainQueueTasksCount    int
+	otherQueuesActiveCount int
+	otherQueuesEmptyCount  int
+	otherQueuesTasksCount  int
+	totalTasksCount        int
+}
+
+func (ds dumpSummary) MarshalJSON() ([]byte, error) {
+	s := struct {
+		Main   int `json:"mainQueueTasks"`
+		Others struct {
+			Active int `json:"active"`
+			Empty  int `json:"empty"`
+			Tasks  int `json:"tasks"`
+		} `json:"otherQueues"`
+		TotalTasks int `json:"totalTasks"`
+	}{
+		Main:       ds.mainQueueTasksCount,
+		TotalTasks: ds.totalTasksCount,
+	}
+
+	s.Others.Tasks = ds.otherQueuesTasksCount
+	s.Others.Active = ds.otherQueuesActiveCount
+	s.Others.Empty = ds.otherQueuesEmptyCount
+
+	return json.Marshal(s)
+}
+
+func (dtq dumpTaskQueues) SortByName() {
+	sort.Sort(asQueueNames(dtq.Active))
+
+	sort.Sort(asQueueNames(dtq.Empty))
+}
+
+func (dtq dumpTaskQueues) Format(format string, showEmpty bool) string {
+	switch format {
+	case "text":
+		return dtq.asText(showEmpty)
+
+	case "json":
+		if !showEmpty {
+			dtq.Empty = make([]dumpQueue, 0)
+		}
+		res, _ := json.Marshal(dtq)
+
+		return string(res)
+
+	case "yaml":
+		if !showEmpty {
+			dtq.Empty = make([]dumpQueue, 0)
+		}
+		res, _ := yaml.Marshal(dtq)
+
+		return string(res)
+	}
+
+	return ""
+}
+
+func (dtq dumpTaskQueues) asText(showEmpty bool) string {
+	var buf strings.Builder
+
+	for i, taskQueue := range dtq.Active {
 		if i > 0 {
 			buf.WriteString("\n")
 		}
-		if q == nil {
-			buf.WriteString(fmt.Sprintf("Queue '%s' is not created\n", name))
-		} else {
-			buf.WriteString(TaskQueueToText(q))
+
+		buf.WriteString(fmt.Sprintf("Queue '%s': length %d, status: '%s'\n", taskQueue.Name, taskQueue.TasksCount, taskQueue.Status))
+		buf.WriteString("\n")
+
+		for _, tk := range taskQueue.Tasks {
+			buf.WriteString(fmt.Sprintf("%2d. ", tk.Index))
+			buf.WriteString(tk.Description)
+			buf.WriteString("\n")
 		}
 	}
 
 	// Empty queues. Do not report single empty main queue.
-	if emptyQueues > 0 && hasOthers {
-		buf.WriteString(fmt.Sprintf("\nEmpty queues (%d):\n", len(names["empty"])))
-		for _, name := range names["empty"] {
-			buf.WriteString(fmt.Sprintf("- %s\n", name))
+	if showEmpty {
+		buf.WriteString(fmt.Sprintf("\nEmpty queues (%d):\n", len(dtq.Empty)))
+		for _, taskQueue := range dtq.Empty {
+			buf.WriteString(fmt.Sprintf("- %s\n", taskQueue.Name))
 		}
 	}
 
@@ -111,60 +285,35 @@ func TaskQueueSetToText(tqs *queue.TaskQueueSet) string {
 	// - 'main' queue: [empty | 1 task | 5 tasks].
 	// - 24 other queues (3 active, 21 empty): 56 tasks.
 	// - total 68 tasks to handle.
-	if !hasMain && !hasOthers {
+	if len(dtq.Active) == 0 && len(dtq.Empty) == 0 {
 		buf.WriteString("Summary:\n  No queues.\n")
 	} else {
+		otherQueuesCount := len(dtq.Active) + len(dtq.Empty)
+
 		if buf.Len() > 0 {
 			buf.WriteString("\n")
 		}
 		buf.WriteString("Summary:\n")
-		if hasMain {
+		if dtq.MainQueue != nil {
+			otherQueuesCount-- // minus main queue
 			buf.WriteString(fmt.Sprintf("- '%s' queue: %s.\n",
-				tqs.MainName,
-				pluralize(mainTasksCount, "empty", "task", "tasks")))
+				dtq.MainQueue.Name,
+				pluralize(len(dtq.MainQueue.Tasks), "empty", "task", "tasks")))
 		}
-		if hasOthers {
+
+		if otherQueuesCount > 0 {
 			buf.WriteString(fmt.Sprintf("- %s (%d active, %d empty): %s.\n",
 				pluralize(otherQueuesCount, "", "other queue", "other queues"),
-				activeQueues, emptyQueues,
-				pluralize(tasksCount, "", "task", "tasks")))
+				dtq.Summary.otherQueuesActiveCount, dtq.Summary.otherQueuesEmptyCount,
+				pluralize(dtq.Summary.otherQueuesTasksCount, "", "task", "tasks")))
 		}
-		totalTasks := mainTasksCount + tasksCount
-		if totalTasks == 0 {
+		if dtq.Summary.totalTasksCount == 0 {
 			buf.WriteString("- no tasks to handle.\n")
 		} else {
 			buf.WriteString(fmt.Sprintf("- total %s to handle.\n",
-				pluralize(totalTasks, "", "task", "tasks")))
+				pluralize(dtq.Summary.totalTasksCount, "", "task", "tasks")))
 		}
 	}
 
 	return buf.String()
 }
-
-func pluralize(n int, zero, one, many string) string {
-	if n == 0 && zero != "" {
-		return zero
-	}
-	description := many
-	if n == 1 {
-		description = one
-	}
-	return fmt.Sprintf("%d %s", n, description)
-}
-
-// TaskQueueToText dumps all tasks in queue.
-func TaskQueueToText(q *queue.TaskQueue) string {
-	var buf strings.Builder
-	buf.WriteString(fmt.Sprintf("Queue '%s': length %d, status: '%s'\n", q.Name, q.Length(), q.Status))
-	buf.WriteString("\n")
-
-	index := 1
-	q.Iterate(func(task task.Task) {
-		buf.WriteString(fmt.Sprintf("%2d. ", index))
-		buf.WriteString(task.GetDescription())
-		buf.WriteString("\n")
-		index++
-	})
-
-	return buf.String()
-}


Test patch:
diff --git a/pkg/task/dump/dump_test.go b/pkg/task/dump/dump_test.go
index 534e28a..ed68ced 100644
--- a/pkg/task/dump/dump_test.go
+++ b/pkg/task/dump/dump_test.go
@@ -16,17 +16,21 @@ import (
 func Test_Sort_ByNamespaceAndName(t *testing.T) {
 	g := NewWithT(t)
 
-	sorted := []string{
+	sortedStr := []string{
 		"main",
 		"asd",
 		"hook.kube",
 		"sched-queue",
 		"str",
 	}
+	sorted := make([]dumpQueue, 0, len(sortedStr))
+	for _, ss := range sortedStr {
+		sorted = append(sorted, dumpQueue{Name: ss})
+	}
 
-	g.Expect(sort.IsSorted(AsQueueNames(sorted))).To(BeTrue())
+	g.Expect(sort.IsSorted(asQueueNames(sorted))).To(BeTrue())
 
-	input := []string{
+	inputStr := []string{
 		"str",
 		"asd",
 		"hook.kube",
@@ -34,7 +38,12 @@ func Test_Sort_ByNamespaceAndName(t *testing.T) {
 		"sched-queue",
 	}
 
-	sort.Sort(AsQueueNames(input))
+	input := make([]dumpQueue, 0, len(inputStr))
+	for _, ss := range inputStr {
+		input = append(input, dumpQueue{Name: ss})
+	}
+
+	sort.Sort(asQueueNames(input))
 
 	for i, s := range sorted {
 		g.Expect(input[i]).Should(Equal(s), "fail on 'input' index %d", i)
@@ -63,40 +72,67 @@ func Test_Dump(t *testing.T) {
 	tqs.WithContext(context.Background())
 
 	// Empty set, no queues, dump should not fail.
-	dump := TaskQueueSetToText(tqs)
-	g.Expect(dump).To(ContainSubstring("No queues"))
+	t.Run("empty set", func(t *testing.T) {
+		dump := TaskQueues(tqs, "text", true)
+		g.Expect(dump).To(ContainSubstring("No queues"))
+	})
 
 	// Create and fill main queue.
-	tqs.NewNamedQueue("main", nil)
+	t.Run("single main queue", func(t *testing.T) {
+		tqs.NewNamedQueue("main", nil)
 
-	// Single empty main should be reported only as summary.
-	dump = TaskQueueSetToText(tqs)
-	t.Log(dump)
-	g.Expect(dump).ToNot(ContainSubstring("Empty queues"))
-	g.Expect(dump).To(ContainSubstring(": empty"))
+		// Single empty main should be reported only as summary.
+		dump := TaskQueues(tqs, "text", true)
+		t.Log(dump)
+		g.Expect(dump).ToNot(ContainSubstring("Empty queues"))
+		g.Expect(dump).To(ContainSubstring(": empty"))
 
-	fillQueue(tqs.GetMain(), mainTasks)
+		dump = TaskQueues(tqs, "json", true)
+		t.Log(dump)
+		g.Expect(dump).To(MatchJSON(`{"active":[],"summary":{"mainQueueTasks":0,"otherQueues":{"active":0,"empty":0,"tasks":0},"totalTasks":0}}`))
+	})
 
-	// Main queue is not counted as active or empty queue.
-	dump = TaskQueueSetToText(tqs)
-	g.Expect(dump).ToNot(ContainSubstring("other"))
-	g.Expect(dump).To(ContainSubstring(": %d tasks", mainTasks))
+	t.Run("main queue as active", func(t *testing.T) {
+		fillQueue(tqs.GetMain(), mainTasks)
+
+		// Main queue is not counted as active or empty queue.
+		dump := TaskQueues(tqs, "text", true)
+		g.Expect(dump).ToNot(ContainSubstring("other"))
+		g.Expect(dump).To(ContainSubstring(": %d tasks", mainTasks))
+	})
 
 	// Create and fill active queue.
-	tqs.NewNamedQueue("active-queue", nil)
-	fillQueue(tqs.GetByName("active-queue"), activeTasks)
+	t.Run("fill active queue", func(t *testing.T) {
+		tqs.NewNamedQueue("active-queue", nil)
+		fillQueue(tqs.GetByName("active-queue"), activeTasks)
 
-	dump = TaskQueueSetToText(tqs)
-	g.Expect(dump).To(ContainSubstring("1 active"))
-	g.Expect(dump).To(ContainSubstring(": %d tasks", mainTasks))
-	g.Expect(dump).To(ContainSubstring(": %d tasks", activeTasks))
+		dump := TaskQueues(tqs, "text", true)
+		g.Expect(dump).To(ContainSubstring("1 active"))
+		g.Expect(dump).To(ContainSubstring(": %d tasks", mainTasks))
+		g.Expect(dump).To(ContainSubstring(": %d tasks", activeTasks))
+	})
 
 	// Create empty queue.
-	tqs.NewNamedQueue("empty", nil)
-
-	dump = TaskQueueSetToText(tqs)
-	t.Log(dump)
-	g.Expect(dump).To(ContainSubstring("1 active"))
-	g.Expect(dump).To(ContainSubstring("1 empty"))
-	g.Expect(dump).To(ContainSubstring("total %d tasks", mainTasks+activeTasks))
+	t.Run("create empty queue", func(t *testing.T) {
+		tqs.NewNamedQueue("empty", nil)
+
+		dump := TaskQueues(tqs, "text", true)
+		t.Log(dump)
+		g.Expect(dump).To(ContainSubstring("1 active"))
+		g.Expect(dump).To(ContainSubstring("1 empty"))
+		g.Expect(dump).To(ContainSubstring("total %d tasks", mainTasks+activeTasks))
+
+		dump = TaskQueues(tqs, "json", true)
+		g.Expect(dump).To(MatchJSON(`{"active":[{"name":"main","tasksCount":5,"tasks":[{"index":1,"description":":::test_task_main_0004"},{"index":2,"description":":::test_task_main_0003"},{"index":3,"description":":::test_task_main_0002"},{"index":4,"description":":::test_task_main_0001"},{"index":5,"description":":::test_task_main_0000"}]},{"name":"active-queue","tasksCount":4,"tasks":[{"index":1,"description":":::test_task_active-queue_0003"},{"index":2,"description":":::test_task_active-queue_0002"},{"index":3,"description":":::test_task_active-queue_0001"},{"index":4,"description":":::test_task_active-queue_0000"}]}],"empty":[{"name":"empty","tasksCount":0}],"summary":{"mainQueueTasks":5,"otherQueues":{"active":1,"empty":1,"tasks":4},"totalTasks":9}}`))
+	})
+
+	t.Run("omit empty queue", func(t *testing.T) {
+		dump := TaskQueues(tqs, "text", false)
+		g.Expect(dump).To(ContainSubstring("1 active"))
+		g.Expect(dump).ToNot(ContainSubstring("Empty queues (1):"))
+		g.Expect(dump).To(ContainSubstring("total %d tasks", mainTasks+activeTasks))
+
+		dump = TaskQueues(tqs, "json", false)
+		g.Expect(dump).To(MatchJSON(`{"active":[{"name":"main","tasksCount":5,"tasks":[{"index":1,"description":":::test_task_main_0004"},{"index":2,"description":":::test_task_main_0003"},{"index":3,"description":":::test_task_main_0002"},{"index":4,"description":":::test_task_main_0001"},{"index":5,"description":":::test_task_main_0000"}]},{"name":"active-queue","tasksCount":4,"tasks":[{"index":1,"description":":::test_task_active-queue_0003"},{"index":2,"description":":::test_task_active-queue_0002"},{"index":3,"description":":::test_task_active-queue_0001"},{"index":4,"description":":::test_task_active-queue_0000"}]}],"summary":{"mainQueueTasks":5,"otherQueues":{"active":1,"empty":1,"tasks":4},"totalTasks":9}}`))
+	})
 }