You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@qpid.apache.org by ch...@apache.org on 2019/11/25 16:35:08 UTC

[qpid-dispatch] branch master updated: DISPATCH-1186: Add qdstat option to get CSV format for tables

This is an automated email from the ASF dual-hosted git repository.

chug pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/qpid-dispatch.git


The following commit(s) were added to refs/heads/master by this push:
     new 7a10a4d  DISPATCH-1186: Add qdstat option to get CSV format for tables
7a10a4d is described below

commit 7a10a4d467d9cd1d7a1d75467266cfb01e770b86
Author: Chuck Rolke <ch...@apache.org>
AuthorDate: Mon Nov 25 09:47:17 2019 -0500

    DISPATCH-1186: Add qdstat option to get CSV format for tables
    
    Add a "--csv" switch to qdstat command line to apply to display tables.
    
    || CSV character  || value        ||
    |  separator      |  comma        |
    |  string quote   |  double quote |
    
    All non-blank values in headings and tables are output as quoted strings.
    Blank values in tables are not quoted. The output shall be consecutive
    separator commas.
    
    This option removes ambiguity for certain tables (like 'qdstat -l') where
    columns (like peer and phs) have no entry, and headers (like 'conn id') have
    spaces in their names.
---
 python/qpid_dispatch_internal/tools/__init__.py |   4 +-
 python/qpid_dispatch_internal/tools/command.py  |   1 +
 python/qpid_dispatch_internal/tools/display.py  |  48 ++++-
 tests/system_tests_qdstat.py                    | 230 ++++++++++++++++++++++++
 tools/qdstat.in                                 |  21 +--
 5 files changed, 290 insertions(+), 14 deletions(-)

diff --git a/python/qpid_dispatch_internal/tools/__init__.py b/python/qpid_dispatch_internal/tools/__init__.py
index 157f503..db810c6 100644
--- a/python/qpid_dispatch_internal/tools/__init__.py
+++ b/python/qpid_dispatch_internal/tools/__init__.py
@@ -22,6 +22,6 @@ from __future__ import division
 from __future__ import absolute_import
 from __future__ import print_function
 
-from .display import Display, Header, Sorter, YN, Commas, TimeLong, TimeShort, Sortable
+from .display import Display, Header, Sorter, YN, Commas, TimeLong, TimeShort, Sortable, BodyFormat
 
-__all__ = ["Display", "Header", "Sorter", "YN", "Commas", "TimeLong", "TimeShort", "Sortable"]
+__all__ = ["Display", "Header", "Sorter", "YN", "Commas", "TimeLong", "TimeShort", "Sortable", "BodyFormat"]
diff --git a/python/qpid_dispatch_internal/tools/command.py b/python/qpid_dispatch_internal/tools/command.py
index bb98196..434a987 100644
--- a/python/qpid_dispatch_internal/tools/command.py
+++ b/python/qpid_dispatch_internal/tools/command.py
@@ -183,6 +183,7 @@ def _qdstat_parser(BusManager):
     # like -c, -l, -a, --autolinks, --linkroutes and --log.
     # By default, the limit is not set, which means the limit is unlimited.
     parser.add_argument("--limit", help="Limit number of output rows. Unlimited if limit is zero or if limit not specified", type=int, default=None)
+    parser.add_argument("--csv", help="Render tabular output in csv format", action="store_true")
 
     add_connection_options(parser)
     return parser
diff --git a/python/qpid_dispatch_internal/tools/display.py b/python/qpid_dispatch_internal/tools/display.py
index 2acb8b7..3e2d1b4 100644
--- a/python/qpid_dispatch_internal/tools/display.py
+++ b/python/qpid_dispatch_internal/tools/display.py
@@ -138,14 +138,33 @@ class Header:
     value /= 1000
     return self.numCell(value, 'g')
 
+class BodyFormat:
+  """
+  Display body format chooses between:
+   CLASSIC - original variable-width, unquoted, text delimited by white space
+   CSV     - quoted text delimited by commas
+  """
+  CLASSIC = 1
+  CSV = 2
+
+class CSV_CONFIG:
+  """ """
+  SEPERATOR = u','
+  STRING_QUOTE = u'"'
 
 class Display:
   """ Display formatting """
   
-  def __init__(self, spacing=2, prefix="    "):
+  def __init__(self, spacing=2, prefix="    ", bodyFormat=BodyFormat.CLASSIC):
     self.tableSpacing    = spacing
     self.tablePrefix     = prefix
     self.timestampFormat = "%X"
+    if bodyFormat == BodyFormat.CLASSIC:
+      self.printTable = self.table
+    elif bodyFormat == BodyFormat.CSV:
+      self.printTable = self.tableCsv
+    else:
+      raise Exception("Table body format must be CLASSIC or CSV.")
 
   def formattedTable(self, title, heads, rows):
     fRows = []
@@ -159,7 +178,7 @@ class Display:
     headtext = []
     for head in heads:
       headtext.append(head.text)
-    self.table(title, headtext, fRows)
+    self.printTable(title, headtext, fRows)
 
   def table(self, title, heads, rows):
     """ Print a table with autosized columns """
@@ -208,6 +227,31 @@ class Display:
         col = col + 1
       print(line)
 
+  def tableCsv(self, title, heads, rows):
+    """
+    Print a table with CSV format.
+    """
+
+    def csvEscape(text):
+      """
+      Given a unicode text field, return the quoted CSV format for it
+      :param text: a header field or a table row field
+      :return:
+      """
+      if len(text) == 0:
+        return ""
+      else:
+        text = text.replace(CSV_CONFIG.STRING_QUOTE, CSV_CONFIG.STRING_QUOTE*2)
+        return CSV_CONFIG.STRING_QUOTE + text + CSV_CONFIG.STRING_QUOTE
+
+    print("%s" % title)
+    if len (rows) == 0:
+      return
+
+    print(','.join([csvEscape(UNICODE(head)) for head in heads]))
+    for row in rows:
+      print(','.join([csvEscape(UNICODE(item)) for item in row]))
+
   def do_setTimeFormat (self, fmt):
     """ Select timestamp format """
     if fmt == "long":
diff --git a/tests/system_tests_qdstat.py b/tests/system_tests_qdstat.py
index ed4ee87..7ebe2c9 100644
--- a/tests/system_tests_qdstat.py
+++ b/tests/system_tests_qdstat.py
@@ -26,6 +26,7 @@ import os
 import re
 import system_test
 import unittest
+import sys
 from subprocess import PIPE
 from proton import Url, SSLDomain, SSLUnavailable, SASL
 from system_test import main_module, SkipIfNeeded
@@ -73,31 +74,66 @@ class QdstatTest(system_test.TestCase):
         self.assertTrue("Mode                             standalone" in out)
         self.assertEqual(out.count("QDR.A"), 2)
 
+    def test_general_csv(self):
+        out = self.run_qdstat(['--general', '--csv'], r'(?s)Router Statistics.*Mode","Standalone')
+        self.assertTrue("Connections","1" in out)
+        self.assertTrue("Nodes","0" in out)
+        self.assertTrue("Auto Links","0" in out)
+        self.assertTrue("Link Routes","0" in out)
+        self.assertTrue("Router Id","QDR.A" in out)
+        self.assertTrue("Mode","standalone" in out)
+        self.assertEqual(out.count("QDR.A"), 2)
+
     def test_connections(self):
         self.run_qdstat(['--connections'], r'host.*container.*role')
         outs = self.run_qdstat(['--connections'], 'no-auth')
         outs = self.run_qdstat(['--connections'], 'QDR.A')
 
+    def test_connections_csv(self):
+        self.run_qdstat(['--connections', "--csv"], r'host.*container.*role')
+        outs = self.run_qdstat(['--connections'], 'no-auth')
+        outs = self.run_qdstat(['--connections'], 'QDR.A')
+
     def test_links(self):
         self.run_qdstat(['--links'], r'QDR.A')
         out = self.run_qdstat(['--links'], r'endpoint.*out.*local.*temp.')
         parts = out.split("\n")
         self.assertEqual(len(parts), 9)
 
+    def test_links_csv(self):
+        self.run_qdstat(['--links', "--csv"], r'QDR.A')
+        out = self.run_qdstat(['--links'], r'endpoint.*out.*local.*temp.')
+        parts = out.split("\n")
+        self.assertEqual(len(parts), 9)
+
     def test_links_with_limit(self):
         out = self.run_qdstat(['--links', '--limit=1'])
         parts = out.split("\n")
         self.assertEqual(len(parts), 8)
 
+    def test_links_with_limit_csv(self):
+        out = self.run_qdstat(['--links', '--limit=1', "--csv"])
+        parts = out.split("\n")
+        self.assertEqual(len(parts), 7)
+
     def test_nodes(self):
         self.run_qdstat(['--nodes'], r'No Router List')
 
+    def test_nodes_csv(self):
+        self.run_qdstat(['--nodes', "--csv"], r'No Router List')
+
     def test_address(self):
         out = self.run_qdstat(['--address'], r'QDR.A')
         out = self.run_qdstat(['--address'], r'\$management')
         parts = out.split("\n")
         self.assertEqual(len(parts), 11)
 
+    def test_address_csv(self):
+        out = self.run_qdstat(['--address'], r'QDR.A')
+        out = self.run_qdstat(['--address'], r'\$management')
+        parts = out.split("\n")
+        self.assertEqual(len(parts), 11)
+
     def test_qdstat_no_args(self):
         outs = self.run_qdstat(args=None)
         self.assertTrue("Presettled Count" in outs)
@@ -111,6 +147,19 @@ class QdstatTest(system_test.TestCase):
         self.assertTrue("Ingress Count" in outs)
         self.assertTrue("Uptime" in outs)
 
+    def test_qdstat_no_other_args_csv(self):
+        outs = self.run_qdstat(["--csv"])
+        self.assertTrue("Presettled Count" in outs)
+        self.assertTrue("Dropped Presettled Count" in outs)
+        self.assertTrue("Accepted Count" in outs)
+        self.assertTrue("Rejected Count" in outs)
+        self.assertTrue("Deliveries from Route Container" in outs)
+        self.assertTrue("Deliveries to Route Container" in outs)
+        self.assertTrue("Deliveries to Fallback" in outs)
+        self.assertTrue("Egress Count" in outs)
+        self.assertTrue("Ingress Count" in outs)
+        self.assertTrue("Uptime" in outs)
+
     def test_address_priority(self):
         out = self.run_qdstat(['--address'])
         lines = out.split("\n")
@@ -137,11 +186,43 @@ class QdstatTest(system_test.TestCase):
                 self.assertTrue(priority >= -1, "Priority was less than -1")
                 self.assertTrue(priority <= 9, "Priority was greater than 9")
 
+    def test_address_priority_csv(self):
+        HEADER_ROW = 4
+        PRI_COL = 4
+        out = self.run_qdstat(['--address', "--csv"])
+        lines = out.split("\n")
+
+        # make sure the output contains a header line
+        self.assertTrue(len(lines) >= 2)
+
+        # see if the header line has the word priority in it
+        header_line = lines[HEADER_ROW].split(',')
+        self.assertTrue(header_line[PRI_COL] == '"pri"')
+
+        # extract the number in the priority column of every address
+        for i in range(HEADER_ROW + 1, len(lines) - 1):
+            line = lines[i].split(',')
+            pri = line[PRI_COL][1:-1] # unquoted value
+
+            # make sure the priority found is a hyphen or a legal number
+            if pri == '-':
+                pass # naked hypnen is allowed
+            else:
+                priority = int(pri)
+                # make sure the priority is from -1 to 9
+                self.assertTrue(priority >= -1, "Priority was less than -1")
+                self.assertTrue(priority <= 9, "Priority was greater than 9")
+
     def test_address_with_limit(self):
         out = self.run_qdstat(['--address', '--limit=1'])
         parts = out.split("\n")
         self.assertEqual(len(parts), 8)
 
+    def test_address_with_limit_csv(self):
+        out = self.run_qdstat(['--address', '--limit=1', '--csv'])
+        parts = out.split("\n")
+        self.assertEqual(len(parts), 7)
+
     def test_memory(self):
         out = self.run_qdstat(['--memory'])
         if out.strip() == "No memory statistics available":
@@ -152,6 +233,16 @@ class QdstatTest(system_test.TestCase):
         regexp = r'qdr_address_t\s+[0-9]+'
         assert re.search(regexp, out, re.I), "Can't find '%s' in '%s'" % (regexp, out)
 
+    def test_memory_csv(self):
+        out = self.run_qdstat(['--memory', '--csv'])
+        if out.strip() == "No memory statistics available":
+            # router built w/o memory pools enabled]
+            return self.skipTest("Router's memory pools disabled")
+        self.assertTrue("QDR.A" in out)
+        self.assertTrue("UTC" in out)
+        regexp = r'qdr_address_t","[0-9]+'
+        assert re.search(regexp, out, re.I), "Can't find '%s' in '%s'" % (regexp, out)
+
     def test_log(self):
         self.run_qdstat(['--log',  '--limit=5'], r'AGENT \(debug\).*GET-LOG')
 
@@ -220,6 +311,18 @@ class QdstatTest(system_test.TestCase):
 
         self.assertEqual(links, 2000)
 
+        # Run qdstat with a limit less than 10,000
+        # repeat with --csv
+        outs = self.run_qdstat(['--links', '--limit=2000', '--csv'])
+        out_list = outs.split("\n")
+
+        links = 0
+        for out in out_list:
+            if "endpoint" in out and "examples" in out:
+                links += 1
+
+        self.assertEqual(links, 2000)
+
         # Run qdstat with a limit of 700 because 700
         # is the maximum number of rows we get per request
         outs = self.run_qdstat(['--links', '--limit=700'])
@@ -232,6 +335,19 @@ class QdstatTest(system_test.TestCase):
 
         self.assertEqual(links, 700)
 
+        # Run qdstat with a limit of 700 because 700
+        # is the maximum number of rows we get per request
+        # repeat with --csv
+        outs = self.run_qdstat(['--links', '--limit=700', '--csv'])
+        out_list = outs.split("\n")
+
+        links = 0
+        for out in out_list:
+            if "endpoint" in out and "examples" in out:
+                links += 1
+
+        self.assertEqual(links, 700)
+
         # Run qdstat with a limit of 500 because 700
         # is the maximum number of rows we get per request
         # and we want to try something less than 700
@@ -245,6 +361,20 @@ class QdstatTest(system_test.TestCase):
 
         self.assertEqual(links, 500)
 
+        # Run qdstat with a limit of 500 because 700
+        # is the maximum number of rows we get per request
+        # and we want to try something less than 700
+        # repeat with --csv
+        outs = self.run_qdstat(['--links', '--limit=500', '--csv'])
+        out_list = outs.split("\n")
+
+        links = 0
+        for out in out_list:
+            if "endpoint" in out and "examples" in out:
+                links += 1
+
+        self.assertEqual(links, 500)
+
         # DISPATCH-1485. Try to run qdstat with a limit=0. Without the fix for DISPATCH-1485
         # this following command will hang and the test will fail.
         outs = self.run_qdstat(['--links', '--limit=0'])
@@ -256,6 +386,18 @@ class QdstatTest(system_test.TestCase):
                 links += 1
         self.assertEqual(links, COUNT*2)
 
+        # DISPATCH-1485. Try to run qdstat with a limit=0. Without the fix for DISPATCH-1485
+        # this following command will hang and the test will fail.
+        # repeat with --csv
+        outs = self.run_qdstat(['--links', '--limit=0', '--csv'])
+        out_list = outs.split("\n")
+
+        links = 0
+        for out in out_list:
+            if "endpoint" in out and "examples" in out:
+                links += 1
+        self.assertEqual(links, COUNT*2)
+
 
         # This test would fail without the fix for DISPATCH-974
         outs = self.run_qdstat(['--address'])
@@ -357,6 +499,94 @@ class QdstatLinkPriorityTest(system_test.TestCase):
         # make sure that all priorities are present in the list (currently 0-9)
         self.assertEqual(len(priorities.keys()), 10, "Not all priorities are present")
 
+
+    def test_link_priority_csv(self):
+        HEADER_ROW = 4
+        TYPE_COL = 0
+        PRI_COL = 9
+        out = self.run_qdstat(['--links', '--csv'])
+        lines = out.split("\n")
+
+        # make sure the output contains a header line
+        self.assertTrue(len(lines) >= 2)
+
+        # see if the header line has the word priority in it
+        header_line = lines[HEADER_ROW].split(',')
+        self.assertTrue(header_line[PRI_COL] == '"pri"')
+
+        # extract the number in the priority column of every inter-router link
+        priorities = {}
+        for i in range(HEADER_ROW + 1, len(lines) - 1):
+            line = lines[i].split(',')
+            if line[TYPE_COL] == '"inter-router"':
+                pri = line[PRI_COL][1:-1]
+                # make sure the priority found is a number
+                self.assertTrue(len(pri) > 0, "Can not find numeric priority in '%s'" % lines[i])
+                self.assertTrue(pri != '-')  # naked hypen disallowed
+                priority = int(pri)
+                # make sure the priority is from 0 to 9
+                self.assertTrue(priority >= 0, "Priority was less than 0")
+                self.assertTrue(priority <= 9, "Priority was greater than 9")
+
+                # mark this priority as present
+                priorities[priority] = True
+
+        # make sure that all priorities are present in the list (currently 0-9)
+        self.assertEqual(len(priorities.keys()), 10, "Not all priorities are present")
+
+
+    def _test_links_all_routers(self, command):
+        out = self.run_qdstat(command)
+
+        self.assertTrue(out.count('UTC') == 1)
+        self.assertTrue(out.count('Router Links') == 2)
+        self.assertTrue(out.count('inter-router') == 40)
+        self.assertTrue(out.count('router-control') == 4)
+
+    def test_links_all_routers(self):
+        self._test_links_all_routers(['--links', '--all-routers'])
+
+    def test_links_all_routers_csv(self):
+        self._test_links_all_routers(['--links', '--all-routers', '--csv'])
+
+
+    def _test_all_entities(self, command):
+        out = self.run_qdstat(command)
+
+        self.assertTrue(out.count('UTC') == 1)
+        self.assertTrue(out.count('Router Links') == 1)
+        self.assertTrue(out.count('Router Addresses') == 1)
+        self.assertTrue(out.count('Connections') == 2)
+        self.assertTrue(out.count('AutoLinks') == 2)
+        self.assertTrue(out.count('Link Routes') == 3)
+        self.assertTrue(out.count('Router Statistics') == 1)
+        self.assertTrue(out.count('Types') == 1)
+
+    def test_all_entities(self):
+        self._test_all_entities(['--all-entities'])
+
+    def test_all_entities_csv(self):
+        self._test_all_entities(['--all-entities', '--csv'])
+
+    def _test_all_entities_all_routers(self, command):
+        out = self.run_qdstat(command)
+
+        self.assertTrue(out.count('UTC') == 1)
+        self.assertTrue(out.count('Router Links') == 2)
+        self.assertTrue(out.count('Router Addresses') == 2)
+        self.assertTrue(out.count('Connections') == 4)
+        self.assertTrue(out.count('AutoLinks') == 4)
+        self.assertTrue(out.count('Link Routes') == 6)
+        self.assertTrue(out.count('Router Statistics') == 2)
+        self.assertTrue(out.count('Types') == 2)
+
+    def test_all_entities_all_routers(self):
+        self._test_all_entities_all_routers(['--all-entities', '--all-routers'])
+
+    def test_all_entities_all_routers_csv(self):
+        self._test_all_entities_all_routers(['--all-entities', '--csv', '--all-routers'])
+
+
 try:
     SSLDomain(SSLDomain.MODE_CLIENT)
     class QdstatSslTest(system_test.TestCase):
diff --git a/tools/qdstat.in b/tools/qdstat.in
index d4b52b3..9b39820 100755
--- a/tools/qdstat.in
+++ b/tools/qdstat.in
@@ -34,7 +34,7 @@ from time import ctime, strftime, gmtime
 import qpid_dispatch_site
 from qpid_dispatch.management.client import Url, Node, Entity
 from qpid_dispatch_internal.management.qdrouter import QdSchema
-from qpid_dispatch_internal.tools import Display, Header, Sorter, YN, Commas, TimeLong, TimeShort
+from qpid_dispatch_internal.tools import Display, Header, Sorter, YN, Commas, TimeLong, TimeShort, BodyFormat
 from qpid_dispatch_internal.tools.command import (parse_args_qdstat, main,
                                                   opts_ssl_domain, opts_sasl,
                                                   opts_url)
@@ -55,6 +55,7 @@ class BusManager(Node):
                             ssl_domain=opts_ssl_domain(opts),
                             sasl=opts_sasl(opts)))
         self.show = getattr(self, opts.show)
+        self.bodyFormat = BodyFormat.CSV if opts.csv else BodyFormat.CLASSIC
 
     def query(self, entity_type, attribute_names=None, limit=None):
         if attribute_names:
@@ -120,7 +121,7 @@ class BusManager(Node):
 
 
     def displayEdges(self, show_date_id=True):
-        disp = Display(prefix="  ")
+        disp = Display(prefix="  ", bodyFormat=self.bodyFormat)
         heads = []
         heads.append(Header("id"))
         heads.append(Header("host"))
@@ -183,7 +184,7 @@ class BusManager(Node):
         disp.formattedTable(title, heads, dispRows)            
         
     def displayConnections(self, show_date_id=True):
-        disp = Display(prefix="  ")
+        disp = Display(prefix="  ", bodyFormat=self.bodyFormat)
         heads = []
         heads.append(Header("id"))
         heads.append(Header("host"))
@@ -305,7 +306,7 @@ class BusManager(Node):
         return outlist
 
     def displayGeneral(self, show_date_id=True):
-        disp = Display(prefix="  ")
+        disp = Display(prefix="  ", bodyFormat=self.bodyFormat)
         heads = []
         heads.append(Header("attr"))
         heads.append(Header("value"))
@@ -366,7 +367,7 @@ class BusManager(Node):
         disp.formattedTable(title, heads, dispRows)
 
     def displayRouterLinks(self, show_date_id=True):
-        disp = Display(prefix="  ")
+        disp = Display(prefix="  ", bodyFormat=self.bodyFormat)
         heads = []
         heads.append(Header("type"))
         heads.append(Header("dir"))
@@ -483,7 +484,7 @@ class BusManager(Node):
         disp.formattedTable(title, heads, dispRows)
 
     def displayRouterNodes(self, show_date_id=True):
-        disp = Display(prefix="  ")
+        disp = Display(prefix="  ", bodyFormat=self.bodyFormat)
         heads = []
         heads.append(Header("router-id"))
         heads.append(Header("next-hop"))
@@ -534,7 +535,7 @@ class BusManager(Node):
             print("Router is Standalone - No Router List")
 
     def displayAddresses(self, show_date_id=True):
-        disp = Display(prefix="  ")
+        disp = Display(prefix="  ", bodyFormat=self.bodyFormat)
         heads = []
         heads.append(Header("class"))
         heads.append(Header("addr"))
@@ -602,7 +603,7 @@ class BusManager(Node):
         disp.formattedTable(title, heads, dispRows)
 
     def displayAutolinks(self, show_date_id=True):
-        disp = Display(prefix="  ")
+        disp = Display(prefix="  ", bodyFormat=self.bodyFormat)
         heads = []
         heads.append(Header("addr"))
         heads.append(Header("dir"))
@@ -641,7 +642,7 @@ class BusManager(Node):
         disp.formattedTable(title, heads, dispRows)
 
     def displayLinkRoutes(self, show_date_id=True):
-        disp = Display(prefix="  ")
+        disp = Display(prefix="  ", bodyFormat=self.bodyFormat)
         heads = []
         heads.append(Header("address"))
         heads.append(Header("dir"))
@@ -687,7 +688,7 @@ class BusManager(Node):
         disp.formattedTable(title, heads, dispRows)
 
     def displayMemory(self, show_date_id=True):
-        disp = Display(prefix="  ")
+        disp = Display(prefix="  ", bodyFormat=self.bodyFormat)
         heads = []
         heads.append(Header("type"))
         heads.append(Header("size", Header.COMMAS))


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@qpid.apache.org
For additional commands, e-mail: commits-help@qpid.apache.org