[master 3/8] Change the way glade tests are run.

dashea installerbot-noreply at redhat.com
Thu Nov 5 20:12:31 UTC 2015


From: David Shea <dshea at redhat.com>

The amount of work done in each glade test had gotten out of hand. Each
test had to parse command-line arguments (even though only two tests
used them), parse the glade files, and implement its own, basically
identical test framework for tracking and reporting errors.

Instead of copying all that code for each test, re-implement the glade
tests as specialized nose tests. Each test only needs to define a class
with a checkGlade method, and optionally set a translatable attribute if
it wants to check translations. These tests are run by a nose plugin
loaded in the new run_glade_tests.py.  Reporting errors can be done
through the unittest assert methods or by raising an exception, and nose
now handles the problems of gathering and reporting the results.

This changes the way individual tests can be run, since the tests
themselves are no longer executable files. Instead, specify a list of
tests to run as arguments to run_glade_tests.py. Individual glade files
can be selected using the --glade-file argument, which can be specified
multiple times.
---
 tests/Makefile.am                   |   3 +-
 tests/glade/check_format_string.py  |  71 ++++----------
 tests/glade/check_glade_validity.py |  76 ++++-----------
 tests/glade/check_icons.py          |  61 +++---------
 tests/glade/check_invisible_char.py |  65 +++----------
 tests/glade/check_markup.py         | 126 +++++++------------------
 tests/glade/check_mnemonics.py      |  53 ++---------
 tests/glade/check_pw_visibility.py  |  84 +++++------------
 tests/glade/check_viewport.py       |  56 ++---------
 tests/glade/run_glade_tests.py      |  24 +++++
 tests/glade/run_glade_tests.sh      |  48 ----------
 tests/lib/gladecheck.py             | 181 ++++++++++++++++++++++++++++++++++++
 12 files changed, 334 insertions(+), 514 deletions(-)
 mode change 100755 => 100644 tests/glade/check_format_string.py
 mode change 100755 => 100644 tests/glade/check_glade_validity.py
 mode change 100755 => 100644 tests/glade/check_icons.py
 mode change 100755 => 100644 tests/glade/check_invisible_char.py
 mode change 100755 => 100644 tests/glade/check_markup.py
 mode change 100755 => 100644 tests/glade/check_mnemonics.py
 mode change 100755 => 100644 tests/glade/check_pw_visibility.py
 mode change 100755 => 100644 tests/glade/check_viewport.py
 create mode 100755 tests/glade/run_glade_tests.py
 delete mode 100755 tests/glade/run_glade_tests.sh
 create mode 100644 tests/lib/gladecheck.py

diff --git a/tests/Makefile.am b/tests/Makefile.am
index aafee8f..f48cd4f 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -38,7 +38,6 @@ EXTRA_DIST = README.rst usercustomize.py
 
 # Test scripts need to be listed both here and in TESTS
 dist_check_SCRIPTS = $(srcdir)/glade/*.py \
-		     glade/run_glade_tests.sh \
 		     $(srcdir)/lib/*.py \
 		     $(srcdir)/lib/*.sh \
 		     nosetests.sh \
@@ -69,7 +68,7 @@ TESTS = nosetests.sh \
 	gettext_tests/style_guide.py \
 	gettext_tests/contexts.py \
 	storage/run_storage_tests.py \
-	glade/run_glade_tests.sh \
+	glade/run_glade_tests.py \
 	kickstart_tests/scripts/run_kickstart_tests.sh
 
 clean-local:
diff --git a/tests/glade/check_format_string.py b/tests/glade/check_format_string.py
old mode 100755
new mode 100644
index b730a94..f0f588c
--- a/tests/glade/check_format_string.py
+++ b/tests/glade/check_format_string.py
@@ -1,4 +1,3 @@
-#!/usr/bin/python3
 #
 # Copyright (C) 2014  Red Hat, Inc.
 #
@@ -18,64 +17,26 @@
 # Author: David Shea <dshea at redhat.com>
 #
 
-"""
-Python script to ensure that translatable format strings are not present
-in Glade files.
+from gladecheck import GladeTest
 
-Since format substitution is language-dependent, gettext is unable to check
-the validity of format string translations for strings within glade. Instead,
-the format string constant, the translation substitution, and the format
-substitution should all happen outside of glade. Untranslated placeholder
-strings are allowable within glade.
-"""
-
-# Ignore any interruptible calls
-# pylint: disable=interruptible-system-call
-
-import sys
-import argparse
-import re
-
-try:
-    from lxml import etree
-except ImportError:
-    print("You need to install the python-lxml package to use check_format_string.py")
-    sys.exit(1)
-
-def check_glade_file(glade_file_path):
-    global success
-
-    with open(glade_file_path) as glade_file:
-        # Parse the XML
-        glade_tree = etree.parse(glade_file)
+class CheckFormatString(GladeTest):
+    def checkGlade(self, tree):
+        """Reject translatable format string in glade.
 
+           Since format substitution is language-dependent, gettext is unable
+           to check the validity of format string translations for strings
+           within glade. Instead, the format string constant, the translation
+           substitution, and the format substitution should all happen outside
+           of glade. Untranslated placeholder strings are allowable within
+           glade.
+        """
         # Check any property with translatable="yes"
-        for translatable in glade_tree.xpath(".//*[@translatable='yes']"):
+        for translatable in tree.xpath(".//*[@translatable='yes']"):
             # Look for % followed by an open parenthesis (indicating %(name)
             # style substitution), one of the python format conversion flags
             # (#0- +hlL), or one of the python conversion types 
             # (diouxXeEfFgGcrs)
-            if re.search(r'%[-(#0 +hlLdiouxXeEfFgGcrs]', translatable.text):
-                print("Translatable format string found in glade at %s:%d" % \
-                        (glade_file_path, translatable.sourceline))
-                success = False
-
-if __name__ == "__main__":
-    success = True
-    parser = argparse.ArgumentParser("Check that password entries have visibility set to False")
-
-    # Ignore translation arguments
-    parser.add_argument("-t", "--translate", action='store_true',
-            help=argparse.SUPPRESS)
-    parser.add_argument("-p", "--podir", action='store', type=str,
-            metavar='PODIR', help=argparse.SUPPRESS, default='./po')
-
-    parser.add_argument("glade_files", nargs="+", metavar="GLADE-FILE",
-            help='The glade file to check')
-    args = parser.parse_args(args=sys.argv[1:])
-
-    success = True
-    for file_path in args.glade_files:
-        check_glade_file(file_path)
-
-    sys.exit(0 if success else 1)
+            self.assertNotRegex(translatable.text,
+                    r'%[-(#0 +hlLdiouxXeEfFgGcrs]',
+                    msg="Translatable format string found at %s:%d" %
+                        (translatable.base, translatable.sourceline))
diff --git a/tests/glade/check_glade_validity.py b/tests/glade/check_glade_validity.py
old mode 100755
new mode 100644
index 1101472..6eb9687
--- a/tests/glade/check_glade_validity.py
+++ b/tests/glade/check_glade_validity.py
@@ -1,4 +1,3 @@
-#!/usr/bin/python3
 #
 # Copyright (C) 2014  Red Hat, Inc.
 #
@@ -17,72 +16,31 @@
 #
 # Author: David Shea <dshea at redhat.com>
 
-import sys
-import argparse
-
 from collections import Counter
+from gladecheck import GladeTest
 
-try:
-    from lxml import etree
-except ImportError:
-    print("You need to install the python-lxml package to run the glade checks")
-    sys.exit(99)
-
-success = True
-
-def main(argv):
-    global success
-
-    for glade_file in argv:
-        # Parse the glade file to ensure it's well-formed
-        try:
-            glade_tree = etree.parse(glade_file)
-        except etree.XMLSyntaxError:
-            print("%s is not a valid XML file" % glade_file)
-            success = False
-            continue
-
+class CheckValidity(GladeTest):
+    def checkGlade(self, tree):
+        """Check for common glade validity errors"""
         # Check for duplicate IDs
-        # Build a Counter from a list of all ids, extracts the ones with count > 1
+        # Build a Counter from a list of all ids and extract the ones with count > 1
         # Fun fact: glade uses <col id="<number>"> in GtkListStore data, so ids
         # aren't actually unique and getting an object with a particular ID
         # isn't as simple as document.getElementById. Only check the IDs on objects.
-        for glade_id in [c for c in Counter(glade_tree.xpath(".//object/@id")).most_common() \
+        for glade_id in [c for c in Counter(tree.xpath(".//object/@id")).most_common() \
                 if c[1] > 1]:
-            print("%s: ID %s appears %d times" % (glade_file, glade_id[0], glade_id[1]))
-            success = False
+            raise AssertionError("%s: ID %s appears %d times" %
+                    (tree.getroot().base, glade_id[0], glade_id[1]))
 
         # Check for ID references
         # mnemonic_widget properties and action-widget elements need to refer to
         # valid object ids.
-        for mnemonic_widget in glade_tree.xpath(".//property[@name='mnemonic_widget']"):
-            if not glade_tree.xpath(".//object[@id='%s']" % mnemonic_widget.text):
-                print("mnemonic_widget reference to invalid ID %s at line %d of %s" % \
-                        (mnemonic_widget.text, mnemonic_widget.sourceline, glade_file))
-                success = False
-
-        for action_widget in glade_tree.xpath(".//action-widget"):
-            if not glade_tree.xpath(".//object[@id='%s']" % action_widget.text):
-                print("action-widget reference to invalid ID %s at line %d of %s" % \
-                        (action_widget.text, action_widget.sourceline, glade_file))
-                success = False
-
-if __name__ == "__main__":
-    parser = argparse.ArgumentParser("Check glade file validity")
-
-    # Ignore translation arguments
-    parser.add_argument("-t", "--translate", action='store_true',
-            help=argparse.SUPPRESS)
-    parser.add_argument("-p", "--podir", action='store', type=str,
-            metavar='PODIR', help=argparse.SUPPRESS, default='./po')
-
-    parser.add_argument("glade_files", nargs="+", metavar="GLADE-FILE",
-            help='The glade file to check')
-    args = parser.parse_args(args=sys.argv[1:])
-
-    main(args.glade_files)
-
-    if success:
-        sys.exit(0)
-    else:
-        sys.exit(1)
+        for mnemonic_widget in tree.xpath(".//property[@name='mnemonic_widget']"):
+            self.assertTrue(tree.xpath(".//object[@id='%s']" % mnemonic_widget.text),
+                    msg="mnemonic_widget reference to invalid ID %s at line %d of %s" %
+                        (mnemonic_widget.text, mnemonic_widget.sourceline, mnemonic_widget.base))
+
+        for action_widget in tree.xpath(".//action-widget"):
+            self.assertTrue(tree.xpath(".//object[@id='%s']" % action_widget.text),
+                msg="action-widget reference to invalid ID %s at line %d of %s" %
+                        (action_widget.text, action_widget.sourceline, action_widget.base))
diff --git a/tests/glade/check_icons.py b/tests/glade/check_icons.py
old mode 100755
new mode 100644
index bfeac6d..ae3678a
--- a/tests/glade/check_icons.py
+++ b/tests/glade/check_icons.py
@@ -1,4 +1,3 @@
-#!/usr/bin/python3
 #
 # Copyright (C) 2014  Red Hat, Inc.
 #
@@ -17,59 +16,21 @@
 #
 # Author: David Shea <dshea at redhat.com>
 #
-"""
-Check that all icons referenced from glade files are valid in the gnome icon theme.
-"""
-
-# Ignore any interruptible calls
-# pylint: disable=interruptible-system-call
-
-import argparse
-import sys
-
-try:
-    from lxml import etree
-except ImportError:
-    print("You need to install the python-lxml package to use check_pw_visibility.py")
-    sys.exit(1)
 
+from gladecheck import GladeTest
 from iconcheck import icon_exists
 
-def check_glade_file(glade_file_path):
-    glade_success = True
-    with open(glade_file_path) as glade_file:
-        # Parse the XML
-        glade_tree = etree.parse(glade_file)
-
+class CheckIcon(GladeTest):
+    def checkGlade(self, glade_tree):
+        """Check that all icons referenced from glade files are valid in the gnome icon theme."""
         # Stock image names are deprecated
-        for element in glade_tree.xpath("//property[@name='stock' or @name='stock_id']"):
-            glade_success = False
-            print("Deprecated stock icon found at %s:%d" % (glade_file_path, element.sourceline))
+        stock_elements = glade_tree.xpath("//property[@name='stock' or @name='stock_id']")
+        if stock_elements:
+            raise AssertionError("Deprecated stock icon found at %s:%d" %
+                    (stock_elements[0].base, stock_elements[0].sourceline))
 
         # Check whether named icons exist
         for element in glade_tree.xpath("//property[@name='icon_name']"):
-            if not icon_exists(element.text):
-                glade_success = False
-                print("Invalid icon name %s found at %s:%d" % (element.text, glade_file_path, element.sourceline))
-
-    return glade_success
-
-if __name__ == "__main__":
-    parser = argparse.ArgumentParser("Check that password entries have visibility set to False")
-
-    # Ignore translation arguments
-    parser.add_argument("-t", "--translate", action='store_true',
-            help=argparse.SUPPRESS)
-    parser.add_argument("-p", "--podir", action='store', type=str,
-            metavar='PODIR', help=argparse.SUPPRESS, default='./po')
-
-    parser.add_argument("glade_files", nargs="+", metavar="GLADE-FILE",
-            help='The glade file to check')
-    args = parser.parse_args(args=sys.argv[1:])
-
-    success = True
-    for file_path in args.glade_files:
-        if not check_glade_file(file_path):
-            success = False
-
-    sys.exit(0 if success else 1)
+            self.assertTrue(icon_exists(element.text),
+                    msg="Invalid icon name %s found at %s:%d" %
+                    (element.text, element.base, element.sourceline))
diff --git a/tests/glade/check_invisible_char.py b/tests/glade/check_invisible_char.py
old mode 100755
new mode 100644
index 6d43ce1..d912011
--- a/tests/glade/check_invisible_char.py
+++ b/tests/glade/check_invisible_char.py
@@ -1,4 +1,3 @@
-#!/usr/bin/python3
 #
 # Copyright (C) 2015  Red Hat, Inc.
 #
@@ -18,65 +17,27 @@
 # Author: David Shea <dshea at redhat.com>
 #
 
-"""
-Check that the invisible_char in glade files is actually a char.
+from gladecheck import GladeTest
 
-The invisible char is often non-ASCII and sometimes that gets clobbered.
-"""
+class CheckInvisibleChar(GladeTest):
+    def checkGlade(self, tree):
+        """
+        Check that the invisible_char in glade files is actually a char.
 
-# Ignore any interruptible calls
-# pylint: disable=interruptible-system-call
+        The invisible char is often non-ASCII and sometimes that gets clobbered.
+        """
 
-import argparse
-import sys
-
-try:
-    from lxml import etree
-except ImportError:
-    print("You need to install the python-lxml package to use check_pw_visibility.py")
-    sys.exit(1)
-
-def check_glade_file(glade_file_path):
-    succ = True
-
-    with open(glade_file_path, "r") as glade_file:
-        tree = etree.parse(glade_file)
         # Only look for entries with an invisible_char property
         for entry in tree.xpath("//object[@class='GtkEntry' and ./property[@name='invisible_char']]"):
             # Check the contents of the invisible_char property
             invis = entry.xpath("./property[@name='invisible_char']")[0]
-            if len(invis.text) != 1:
-                print("invisible_char at %s:%s not a character" % (glade_file_path, invis.sourceline))
-                succ = False
+            self.assertEqual(len(invis.text), 1,
+                    msg="invisible_char at %s:%s not a character" % (invis.base, invis.sourceline))
 
             # If the char is '?' that's probably also bad
-            if invis.text == '?':
-                print("invisible_char at %s:%s is not what you want" % (glade_file_path, invis.sourceline))
+            self.assertNotEqual(invis.text, "?",
+                    msg="invisible_char at %s:%s is not what you want" % (invis.base, invis.sourceline))
 
             # Check that invisible_char even does anything: visibility should be False
-            if not entry.xpath("./property[@name='visibility' and ./text() = 'False']"):
-                print("Pointless invisible_char found at %s:%s" % (glade_file_path, invis.sourceline))
-                succ = False
-
-    return succ
-
-
-if __name__ == "__main__":
-    parser = argparse.ArgumentParser("Check that invisible character properties are set correctly")
-
-    # Ignore translation arguments
-    parser.add_argument("-t", "--translate", action='store_true',
-            help=argparse.SUPPRESS)
-    parser.add_argument("-p", "--podir", action='store', type=str,
-            metavar='PODIR', help=argparse.SUPPRESS, default='./po')
-
-    parser.add_argument("glade_files", nargs="+", metavar="GLADE-FILE",
-            help='The glade file to check')
-    args = parser.parse_args(args=sys.argv[1:])
-
-    success = True
-    for file_path in args.glade_files:
-        if not check_glade_file(file_path):
-            success = False
-
-    sys.exit(0 if success else 1)
+            self.assertTrue(entry.xpath("./property[@name='visibility' and ./text() = 'False']"),
+                    msg="Pointless invisible_char found at %s:%s" % (invis.base, invis.sourceline))
diff --git a/tests/glade/check_markup.py b/tests/glade/check_markup.py
old mode 100755
new mode 100644
index 3d747d2..e1bd7d7
--- a/tests/glade/check_markup.py
+++ b/tests/glade/check_markup.py
@@ -18,32 +18,10 @@
 # Author: David Shea <dshea at redhat.com>
 #
 
-"""
-Python script to check that properties in glade using Pango markup contain
-valid markup.
-"""
-
-# Ignore any interruptible calls
-# pylint: disable=interruptible-system-call
-
-import sys
-import argparse
-
-# Import translation methods if needed
-if ('-t' in sys.argv) or ('--translate' in sys.argv):
-    try:
-        from pocketlint.translatepo import translate_all
-    except ImportError:
-        print("Unable to load po translation module")
-        sys.exit(99)
-
+from gladecheck import GladeTest
 from pocketlint.pangocheck import markup_nodes, markup_match, markup_necessary
 
-try:
-    from lxml import etree
-except ImportError:
-    print("You need to install the python-lxml package to use check_markup.py")
-    sys.exit(99)
+from lxml import etree
 
 class PangoElementException(Exception):
     def __init__(self, element):
@@ -65,74 +43,40 @@ def _validate_pango_markup(root):
     for child in root:
         _validate_pango_markup(child)
 
-def check_glade_file(glade_file_path, po_map=None):
-    glade_success = True
-    with open(glade_file_path) as glade_file:
-        # Parse the XML
-        glade_tree = etree.parse(glade_file)
+class CheckMarkup(GladeTest):
+    translatable = True
+
+    def checkGlade(self, glade_tree):
+        """Check the validity of Pango markup."""
+        lang = glade_tree.getroot().get("lang")
+        if lang:
+            lang_str = " for language %s" % lang
+        else:
+            lang_str = ""
 
         # Search for label properties on objects that have use_markup set to True
         for label in glade_tree.xpath(".//property[@name='label' and ../property[@name='use_markup']/text() = 'True']"):
-            if po_map:
-                try:
-                    label_texts = po_map.get(label.text, label.get("context"))
-                except KeyError:
-                    continue
-                lang_str = " for language %s" % po_map.metadata['Language']
-            else:
-                label_texts = (label.text,)
-                lang_str = ""
-
             # Wrap the label text in <markup> tags and parse the tree
-            for label_text in label_texts:
-                try:
-                    # pylint: disable=unescaped-markup
-                    pango_tree = etree.fromstring("<markup>%s</markup>" % label_text)
-                    _validate_pango_markup(pango_tree)
-
-                    # Check if the markup is necessary
-                    if not markup_necessary(pango_tree):
-                        print("Markup could be expressed as attributes at %s%s:%d" % \
-                                (glade_file_path, lang_str, label.sourceline))
-                        glade_success = False
-                except etree.XMLSyntaxError:
-                    print("Unable to parse pango markup at %s%s:%d" % \
-                            (glade_file_path, lang_str, label.sourceline))
-                    glade_success = False
-                except PangoElementException as px:
-                    print("Invalid pango element %s at %s%s:%d" % \
-                            (px.element, glade_file_path, lang_str, label.sourceline))
-                    glade_success = False
-                else:
-                    if po_map:
-                        # Check that translated markup has the same elements and attributes
-                        if not markup_match(label.text, label_text):
-                            print("Translated markup does not contain the same elements and attributes at %s%s:%d" % \
-                                    (glade_file_path, lang_str, label.sourceline))
-                            glade_success = False
-    return glade_success
-
-if __name__ == "__main__":
-    parser = argparse.ArgumentParser("Check Pango markup validity")
-    parser.add_argument("-t", "--translate", action='store_true',
-            help="Check translated strings")
-    parser.add_argument("-p", "--podir", action='store', type=str,
-            metavar='PODIR', help='Directory containing po files', default='./po')
-    parser.add_argument("glade_files", nargs="+", metavar="GLADE-FILE",
-            help='The glade file to check')
-    args = parser.parse_args(args=sys.argv[1:])
-
-    success = True
-    for file_path in args.glade_files:
-        if not check_glade_file(file_path):
-            success = False
-
-    # Now loop over all of the translations
-    if args.translate:
-        podicts = translate_all(args.podir)
-        for po_dict in podicts.values():
-            for file_path in args.glade_files:
-                if not check_glade_file(file_path, po_dict):
-                    success = False
-
-    sys.exit(0 if success else 1)
+            try:
+                # pylint: disable=unescaped-markup
+                pango_tree = etree.fromstring("<markup>%s</markup>" % label.text)
+                _validate_pango_markup(pango_tree)
+
+                # Check if the markup is necessary
+                self.assertTrue(markup_necessary(pango_tree),
+                        msg="Markup could be expressed as attributes at %s%s:%d" %
+                            (label.base, lang_str, label.sourceline))
+            except etree.XMLSyntaxError:
+                raise AssertionError("Unable to parse pango markup at %s%s:%d" %
+                        (label.base, lang_str, label.sourceline))
+            except PangoElementException as px:
+                raise AssertionError("Invalid pango element %s at %s%s:%d" %
+                        (px.element, label.base, lang_str, label.sourceline))
+
+            # If this is a translated node, check that the translated markup
+            # has the same elements and attributes as the original.
+            orig_markup = label.get("original_text")
+            if orig_markup:
+                self.assertTrue(markup_match(label.text, orig_markup),
+                        msg="Translated markup does not contain the same elements and attributes at %s%s:%d" %
+                                (label.base, lang_str, label.sourceline))
diff --git a/tests/glade/check_mnemonics.py b/tests/glade/check_mnemonics.py
old mode 100755
new mode 100644
index 28ac2eb..4cc78a9
--- a/tests/glade/check_mnemonics.py
+++ b/tests/glade/check_mnemonics.py
@@ -1,4 +1,3 @@
-#!/usr/bin/python3
 #
 # Copyright (C) 2015  Red Hat, Inc.
 #
@@ -16,26 +15,12 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 #
 # Author: David Shea <dshea at redhat.com>
-# pylint: disable=interruptible-system-call
 
-# Look for widgets with keyboard accelerators but no mnemonic
+from gladecheck import GladeTest
 
-
-import sys
-import argparse
-
-try:
-    from lxml import etree
-except ImportError:
-    print("You need to install the python-lxml package to run the glade checks")
-    sys.exit(99)
-
-def check_glade_file(glade_file_path):
-    glade_success = True
-
-    with open(glade_file_path) as glade_file:
-        # Parse the XML
-        glade_tree = etree.parse(glade_file)
+class CheckMnemonics(GladeTest):
+    def checkGlade(self, glade_tree):
+        """Check for widgets with keyboard accelerators but no mnemonic"""
 
         # Look for labels with use-underline=True and no mnemonic-widget
         for label in glade_tree.xpath(".//object[@class='GtkLabel' and ./property[@name='use_underline' and ./text() = 'True'] and not(./property[@name='mnemonic_widget'])]"):
@@ -45,7 +30,7 @@ def check_glade_file(glade_file_path):
             parent = label.getparent()
 
             # Is the label the child of a GtkButton? The button might be pretty far up there.
-            # Assume widgets names that end in "Button" are subclasses of GtkButton
+            # Assume widget names that end in "Button" are subclasses of GtkButton
             if parent.tag == 'child' and \
                     label.xpath("ancestor::object[substring(@class, string-length(@class) - string-length('Button') + 1) = 'Button']"):
                 continue
@@ -55,30 +40,4 @@ def check_glade_file(glade_file_path):
                     parent.getparent().get('class') == 'GtkNotebook':
                 continue
 
-            print("Label with accelerator and no mnemonic at %s:%d" % (glade_file_path, label.sourceline))
-            glade_success = False
-
-    return glade_success
-
-if __name__ == "__main__":
-    parser = argparse.ArgumentParser("Check glade file validity")
-
-    # Ignore translation arguments
-    parser.add_argument("-t", "--translate", action='store_true',
-            help=argparse.SUPPRESS)
-    parser.add_argument("-p", "--podir", action='store', type=str,
-            metavar='PODIR', help=argparse.SUPPRESS, default='./po')
-
-    parser.add_argument("glade_files", nargs="+", metavar="GLADE-FILE",
-            help='The glade file to check')
-    args = parser.parse_args(args=sys.argv[1:])
-
-    success = True
-    for file_path in args.glade_files:
-        if not check_glade_file(file_path):
-            success = False
-
-    if success:
-        sys.exit(0)
-    else:
-        sys.exit(1)
+            raise AssertionError("Label with accelerator and no mnemonic at %s:%d" % (label.base, label.sourceline))
diff --git a/tests/glade/check_pw_visibility.py b/tests/glade/check_pw_visibility.py
old mode 100755
new mode 100644
index f48f875..d9dc92c
--- a/tests/glade/check_pw_visibility.py
+++ b/tests/glade/check_pw_visibility.py
@@ -1,4 +1,3 @@
-#!/usr/bin/python3
 #
 # Copyright (C) 2013  Red Hat, Inc.
 #
@@ -24,70 +23,29 @@
 
 """
 
-# Ignore any interruptible calls
-# pylint: disable=interruptible-system-call
-
-import argparse
-import sys
-
-try:
-    from lxml import etree
-except ImportError:
-    print("You need to install the python-lxml package to use check_pw_visibility.py")
-    sys.exit(1)
+from gladecheck import GladeTest
 
 PW_ID_INDICATORS = ("pw", "password", "passwd", "passphrase")
 
-def check_glade_file(glade_file_path):
-    def check_entry(entry, fpath):
-        succ = True
-
-        entry_id = entry.attrib.get("id", "UNKNOWN ID")
-        visibility_props = entry.xpath("./property[@name='visibility']")
-
-        # no entry should have visibility specified multiple times
-        if len(visibility_props) > 1:
-            print("Visibility specified multiple times for the entry %s (%s)" % (entry_id, fpath))
-            succ = False
-
-        # password entry should have visibility set to False
-        if any(ind in entry_id.lower() for ind in PW_ID_INDICATORS):
-            if not visibility_props:
-                print("Visibility not specified for the password entry %s (%s)" % (entry_id, fpath))
-                succ = False
-            elif visibility_props[0].text.strip() != "False":
-                print("Visibility not set properly for the password entry %s (%s)" % (entry_id, fpath))
-                succ = False
-        # only password entries should have the visibility set to False
-        elif visibility_props and visibility_props[0].text.strip() == "False":
-            print("Non-password entry %s (%s) has the visibility set to False (bad id?)" % (entry_id, fpath))
-            succ = False
-
-        return succ
+class CheckPwVisibility(GladeTest):
+    def checkGlade(self, tree):
+        """Check that password GtkEntries have the visibility set to False"""
 
-    succ = True
-    with open(glade_file_path, "r") as glade_file:
-        tree = etree.parse(glade_file)
         for entry in tree.xpath("//object[@class='GtkEntry']"):
-            succ = succ and check_entry(entry, glade_file_path)
-
-        return succ
-
-if __name__ == "__main__":
-    parser = argparse.ArgumentParser("Check that password entries have visibility set to False")
-
-    # Ignore translation arguments
-    parser.add_argument("-t", "--translate", action='store_true',
-            help=argparse.SUPPRESS)
-    parser.add_argument("-p", "--podir", action='store', type=str,
-            metavar='PODIR', help=argparse.SUPPRESS, default='./po')
-
-    parser.add_argument("glade_files", nargs="+", metavar="GLADE-FILE",
-            help='The glade file to check')
-    args = parser.parse_args(args=sys.argv[1:])
-
-    success = True
-    for file_path in args.glade_files:
-        success = success and check_glade_file(file_path)
-
-    sys.exit(0 if success else 1)
+            entry_id = entry.attrib.get("id", "UNKNOWN ID")
+            visibility_props = entry.xpath("./property[@name='visibility']")
+
+            # no entry should have visibility specified multiple times
+            self.assertLessEqual(len(visibility_props), 1,
+                    msg="Visibility specified multiple times for the entry %s (%s)" % (entry_id, entry.base))
+
+            # password entry should have visibility set to False
+            if any(ind in entry_id.lower() for ind in PW_ID_INDICATORS):
+                self.assertTrue(visibility_props,
+                        msg="Visibility not specified for the password entry %s (%s)" % (entry_id, entry.base))
+                self.assertEqual(visibility_props[0].text.strip(), "False",
+                        msg="Visibility not set properly for the password entry %s (%s)" % (entry_id, entry.base))
+            # only password entries should have the visibility set to False
+            elif visibility_props and visibility_props[0].text.strip() == "False":
+                raise AssertionError("Non-password entry %s (%s) has the visibility set to False (bad id?)" %
+                        (entry_id, entry.base))
diff --git a/tests/glade/check_viewport.py b/tests/glade/check_viewport.py
old mode 100755
new mode 100644
index bd9d771..31a8a75
--- a/tests/glade/check_viewport.py
+++ b/tests/glade/check_viewport.py
@@ -1,4 +1,3 @@
-#!/usr/bin/python3
 #
 # Copyright (C) 2014  Red Hat, Inc.
 #
@@ -18,34 +17,19 @@
 # Author: David Shea <dshea at redhat.com>
 #
 
-"""
-Check that widgets that implement GtkScrollable are not placed within a
-GtkViewport. If a widget knows how to scroll itself we probably don't want
-to add an extra layer.
-"""
-
-# Ignore any interruptible calls
-# pylint: disable=interruptible-system-call
-
-import argparse
-import sys
-
-try:
-    from lxml import etree
-except ImportError:
-    print("You need to install the python-lxml package to use check_pw_visibility.py")
-    sys.exit(1)
+from gladecheck import GladeTest
 
 # I guess we could look at the introspected classes and see if they implement the Scrollable
 # interface but that sounds like kind of a pain
 SCROLLABLES = ["GtkIconView", "GtkLayout", "GtkTextView", "GtkToolPalette",
                "GtkTreeView", "GtkViewport"]
 
-def check_glade_file(glade_file_path):
-    glade_success = True
-    with open(glade_file_path) as glade_file:
-        # Parse the XML
-        glade_tree = etree.parse(glade_file)
+class CheckViewport(GladeTest):
+    def checkGlade(self, glade_tree):
+        """Check that widgets that implement GtkScrollable are not in a viewport.
+
+           If a widgets knows how to scroll itself we do not want to add an extra layer.
+        """
 
         # Look for something like:
         # <object class="GtkViewport">
@@ -53,27 +37,5 @@ def check_glade_file(glade_file_path):
         #      <object class="GtkTreeView">
         for scrollable in SCROLLABLES:
             for element in glade_tree.xpath(".//object[@class='GtkViewport']/child/object[@class='%s']" % scrollable):
-                glade_success = False
-                print("%s contained in GtkViewport at %s:%d" % (scrollable, glade_file_path,
-                                                                element.sourceline))
-    return glade_success
-
-if __name__ == "__main__":
-    parser = argparse.ArgumentParser("Check that password entries have visibility set to False")
-
-    # Ignore translation arguments
-    parser.add_argument("-t", "--translate", action='store_true',
-            help=argparse.SUPPRESS)
-    parser.add_argument("-p", "--podir", action='store', type=str,
-            metavar='PODIR', help=argparse.SUPPRESS, default='./po')
-
-    parser.add_argument("glade_files", nargs="+", metavar="GLADE-FILE",
-            help='The glade file to check')
-    args = parser.parse_args(args=sys.argv[1:])
-
-    success = True
-    for file_path in args.glade_files:
-        if not check_glade_file(file_path):
-            success = False
-
-    sys.exit(0 if success else 1)
+                raise AssertionError("%s contained in GtkViewport at %s:%d" %
+                        (scrollable, element.base, element.sourceline))
diff --git a/tests/glade/run_glade_tests.py b/tests/glade/run_glade_tests.py
new file mode 100755
index 0000000..54ff531
--- /dev/null
+++ b/tests/glade/run_glade_tests.py
@@ -0,0 +1,24 @@
+#!/usr/bin/python3
+
+import nose
+import os
+import sys
+import glob
+
+from gladecheck import GladePlugin
+
+# Check for prerequisites
+# used in check_icons.py via tests/lib/iconcheck.py
+if os.system("rpm -q gnome-icon-theme gnome-icon-theme-symbolic >/dev/null 2>&1") != 0:
+    print("gnome-icon-theme and gnome-icon-theme-symbolic must be installed")
+    sys.exit(99)
+
+# If no test scripts were specified on the command line, select check_*.py
+if len(sys.argv) <= 1 or not sys.argv[-1].endswith('.py'):
+    sys.argv.extend(glob.glob(os.path.dirname(sys.argv[0]) + "/check_*.py"))
+
+# Run in verbose mode
+sys.argv.append('-v')
+
+# Run nose with the glade plugin
+nose.main(addplugins=[GladePlugin()])
diff --git a/tests/glade/run_glade_tests.sh b/tests/glade/run_glade_tests.sh
deleted file mode 100755
index 0a4f9f9..0000000
--- a/tests/glade/run_glade_tests.sh
+++ /dev/null
@@ -1,48 +0,0 @@
-#!/bin/sh
-
-if ! type parallel 2>&1 > /dev/null; then
-    echo "parallel must be installed"
-    exit 99
-fi
-
-if ! rpm -q gnome-icon-theme &> /dev/null; then
-    # used in check_icons.py;tests/lib/iconcheck.py
-    echo "gnome-icon-theme must be installed"
-    exit 99
-fi
-
-if ! rpm -q gnome-icon-theme-symbolic &> /dev/null; then
-    # used in check_icons.py;tests/lib/iconcheck.py
-    echo "gnome-icon-theme-symbolic must be installed"
-    exit 99
-fi
-
-: "${top_srcdir:=$(dirname "$0")/../..}"
-. "${top_srcdir}/tests/testenv.sh"
-srcdir="${top_srcdir}/tests/glade"
-. "${top_srcdir}/tests/lib/testlib.sh"
-
-# If --translated was specified but not --podir, add --podir
-translate_set=0
-podir_set=0
-for arg in "$@" ; do
-    if [ "$arg" = "--translate" -o "$arg" = "-t" ]; then
-        translate_set=1
-    elif echo "$arg" | grep -q '^--podir\(=.*\)\?$' || [ "$arg" = "-p" ]; then
-        podir_set=1
-    fi
-done
-
-if [ "$translate_set" -eq 1 -a "$podir_set" -eq 0 ]; then
-    set -- "$@" --podir "${top_srcdir}/po"
-fi
-
-status=0
-for check in ${srcdir}/check_*.py ; do
-    findtestfiles -name '*.glade' | parallel --no-notice --gnu -j0 "${check}" "$@" {}
-    if [ "$?" -ne 0 ]; then
-        status=1
-    fi
-done
-
-exit $status
diff --git a/tests/lib/gladecheck.py b/tests/lib/gladecheck.py
new file mode 100644
index 0000000..0137ca6
--- /dev/null
+++ b/tests/lib/gladecheck.py
@@ -0,0 +1,181 @@
+#
+# Copyright (C) 2015  Red Hat, Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.  #
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+# Author: David Shea <dshea at redhat.com>
+#
+
+"""
+Common classes and functions for checking glade files.
+
+Glade file tests should provide a python file containing a subclass of
+GladeTest, below. When nose is run with GladePlugin, each GladeTest
+implementation will be run against each glade file.
+"""
+
+# Re-raise import errors with more helpful messages
+try:
+    from lxml import etree
+except ImportError:
+    raise ImportError("No module named lxml, you need to install the python3-lxml package")
+
+try:
+    from pocketlint.translatepo import translate_all
+except ImportError:
+    raise ImportError("Unable to load po translation module. You may need to install python3-polib")
+
+from abc import ABCMeta, abstractmethod
+import os
+import unittest
+import copy
+import nose
+
+from filelist import testfilelist
+
+import logging
+log = logging.getLogger('nose.plugins.glade')
+
+class GladeTest(unittest.TestCase, metaclass=ABCMeta):
+    """A framework for checking glade files.
+
+       Subclasses must implement the checkGlade method, which will be run for
+       each glade file that is part of the test. The unittest assert* methods
+       are available. If checkGlade returns without raising an exception it
+       is considered to pass.
+
+       If the translatable property is True and --translate was provided on the
+       command line, checkGlade will also be called with translated versions of
+       each glade file.
+    """
+
+    translatable = False
+
+    def __init__(self, *args, **kwargs):
+        super(GladeTest, self).__init__(*args, **kwargs)
+
+        # Set by the plugin in prepareTestCase, since that's easier than
+        # trying to override how this object is created.
+        self.glade_trees = []
+        self.translated_trees = {}
+
+    @abstractmethod
+    def checkGlade(self, tree):
+        """Check a parsed glade file.
+
+           :param etree.ElementTree tree: The parsed glade file
+        """
+        pass
+
+    def test_glade_file(self):
+        """Run checkGlade for each glade file."""
+        for tree in self.glade_trees:
+            with self.subTest(glade_file=tree.getroot().base):
+                self.checkGlade(tree)
+
+        if self.translatable:
+            for lang, trees in self.translated_trees.items():
+                for tree in trees:
+                    with self.subTest(glade_file=tree.getroot().base, lang=lang):
+                        self.checkGlade(tree)
+
+class GladePlugin(nose.plugins.Plugin):
+    name = "glade"
+    enabled = True
+
+    def __init__(self, *args, **kwargs):
+        super(GladePlugin, self).__init__(*args, **kwargs)
+
+        # These are filled during configure(), after we've decided what files
+        # to check and whether to translate them.
+        # Translations are a dict of {'lang': [list of trees]}
+        self.glade_trees = []
+        self.translated_trees = {}
+
+    def options(self, parser, env):
+        # Do not call the superclass options() to skip setting up the
+        # enable/disable options.
+
+        parser.add_option("--glade-file", action="append",
+                help="Glade file(s) to test. If none specified, all files will be tested")
+        parser.add_option("--translate", action="store_true", default=False,
+                help="Test glade files with translations")
+        parser.add_option("--podir", action="store", type=str,
+                default=os.environ.get('top_srcdir', '.') + "/po",
+                metavar="PODIR", help="Directory containing .po files")
+
+    def configure(self, options, conf):
+        super(GladePlugin, self).configure(options, conf)
+
+        # If no glade files were specified, find all of them
+        if options.glade_file:
+            glade_files = options.glade_file
+        else:
+            glade_files = testfilelist(lambda x: x.endswith('.glade'))
+
+        # Parse all of the glade files
+        log.info("Parsing glade files...")
+        for glade_file in glade_files:
+            self.glade_trees.append(etree.parse(glade_file))
+
+        if options.translate:
+            log.info("Loading translations...")
+            podicts = translate_all(options.podir)
+
+            # Loop over each available language
+            for lang, langmap in podicts.items():
+                self.translated_trees[lang] = []
+
+                # For each language, loop over the parsed glade files
+                for tree in self.glade_trees:
+                    # Make a copy of the tree to translate and save it to
+                    # the list for this language
+                    tree = copy.deepcopy(tree)
+                    self.translated_trees[lang].append(tree)
+
+                    # Save the language as an attribute of the root of the tree
+                    tree.getroot().set("lang", lang)
+
+                    # Look for all properties with translatable=yes and translate them
+                    for translatable in tree.xpath('//property[@translatable="yes"]'):
+                        try:
+                            xlated_text = langmap.get(translatable.text, context=translatable.get('context'))[0]
+
+                            # Add the untranslated text as an attribute to this node
+                            translatable.set("original_text", translatable.text)
+
+                            # Replace the actual text
+                            translatable.text = xlated_text
+                        except KeyError:
+                            # No translation available for this string in this language
+                            pass
+
+    def prepareTestCase(self, testcase):
+        # Add the glade files to the GladeTest object
+        testcase.test.glade_trees = self.glade_trees
+        testcase.test.translated_trees = self.translated_trees
+
+    def describeTest(self, testcase):
+        # Return the first line of the doc string on checkGlade instead
+        # of the string for test_glade_file. If there is no doc string,
+        # return the name of the class.
+        doc = testcase.test.checkGlade.__doc__
+        if doc:
+            return doc.strip().split("\n")[0].strip()
+        else:
+            return testcase.test.__class__.__name__
+
+    def wantClass(self, cls):
+        # Make sure we grab all the GladeTest subclasses, and only GladeTest
+        # subclasses, regardless of name.
+        return issubclass(cls, GladeTest) and cls != GladeTest


-- 
To view this commit on github, visit https://github.com/rhinstaller/anaconda/commit/b27ce239ea7268c7606be1624f549b7b9cea817c


More information about the anaconda-patches mailing list