[Libreoffice-commits] core.git: bin/gla11y config_host.mk.in configure.ac solenv/gbuild

Samuel Thibault sthibault at hypra.fr
Tue Feb 20 21:22:10 UTC 2018


 bin/gla11y                       |  216 +++++++++++++++++++++++++++++++++++++++
 config_host.mk.in                |    1 
 configure.ac                     |   10 +
 solenv/gbuild/TargetLocations.mk |    2 
 solenv/gbuild/UIConfig.mk        |   26 ++++
 5 files changed, 254 insertions(+), 1 deletion(-)

New commits:
commit 226697ae27ef451cad404256e83eef88262f16d1
Author: Samuel Thibault <sthibault at hypra.fr>
Date:   Fri Feb 16 13:22:10 2018 +0100

    Integrate initial version of gla11y tool in the build system
    
    This is part of integrating an accessibility non-regression tool. This
    adds checks in configure.ac for the presence of python lxml which we will
    need, and adds support for calling the tool at build time, to check for
    definite UI errors. For now, that only emits errors for missing or duplicate
    accessibility relation targets, and senseless relations: a label being
    mnemonic for several widgets.
    
    Change-Id: Idda91b15b9a9e0322d16db33dfac8e03f2aa518c
    Reviewed-on: https://gerrit.libreoffice.org/49856
    Tested-by: Jenkins <ci at libreoffice.org>
    Reviewed-by: Thorsten Behrens <Thorsten.Behrens at CIB.de>

diff --git a/bin/gla11y b/bin/gla11y
new file mode 100755
index 000000000000..d0619133ad0f
--- /dev/null
+++ b/bin/gla11y
@@ -0,0 +1,216 @@
+#!/usr/bin/env python
+# -*- tab-width: 4; indent-tabs-mode: nil; py-indent-offset: 4 -*-
+#
+# This file is part of the LibreOffice project.
+#
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This file incorporates work covered by the following license notice:
+#
+#   Copyright (c) 2018 Martin Pieuchot
+#   Copyright (c) 2018 Samuel Thibault <sthibault at hypra.fr>
+#
+#   Permission to use, copy, modify, and distribute this software for any
+#   purpose with or without fee is hereby granted, provided that the above
+#   copyright notice and this permission notice appear in all copies.
+#
+#   THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+#   WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+#   MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+#   ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+#   WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+#   ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+#   OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+# vim: set shiftwidth=4 softtabstop=4 expandtab:
+
+# Take LibreOffice (glade) .ui files and check for non accessible widgets
+
+from __future__ import print_function
+
+import os
+import sys
+import getopt
+import lxml.etree as ET
+
+progname = os.path.basename(sys.argv[0])
+Werror = False
+Wnone = False
+errors = 0
+warnings = 0
+
+
+def errstr(elm):
+    """
+    Print the line number of the element
+    """
+
+    return str(elm.sourceline)
+
+def err(filename, elm, msg):
+    global errors
+
+    if elm == None:
+        prefix = "%s:" % filename
+    else:
+        prefix = "%s:%s" % (filename, errstr(elm))
+
+    errors += 1
+    msg = "%s ERROR: %s" % (prefix, msg)
+    print(msg.encode('ascii', 'ignore'))
+
+
+def warn(filename, elm, msg):
+    global Werror, Wnone, errors, warnings
+
+    if Wnone:
+        return
+
+    prefix = "%s:%s" % (filename, errstr(elm))
+
+    if Werror:
+        errors += 1
+    else:
+        warnings += 1
+
+    msg = "%s WARNING: %s" % (prefix,  msg)
+    print(msg.encode('ascii', 'ignore'))
+
+
+def check_objects(filename, elm, objects, target):
+    """
+    Check that objects contains exactly one object
+    """
+    length = len(list(objects))
+    if length == 0:
+        err(filename, elm, "use of undeclared target '%s'" % target)
+    elif length > 1:
+        err(filename, elm, "sevral targets are named '%s'" % target)
+
+def check_props(filename, root, props):
+    """
+    Check the given list of relation properties
+    """
+    for prop in props:
+        objects = root.iterfind(".//object[@id='%s']" % prop.text)
+        check_objects(filename, prop, objects, prop.text)
+
+def check_rels(filename, root, rels):
+    """
+    Check the given list of relations
+    """
+    for rel in rels:
+        target = rel.attrib['target']
+        targets = root.iterfind(".//object[@id='%s']" % target)
+        check_objects(filename, rel, targets, target)
+
+def check_a11y_relation(filename, root):
+    """
+    Emit an error message if any of the 'object' elements of the XML
+    document represented by `root' doesn't comply with Accessibility
+    rules.
+    """
+
+    for obj in root.iter('object'):
+
+        label_for = obj.findall("accessibility/relation[@type='label-for']")
+        check_rels(filename, root, label_for)
+
+        labelled_by = obj.findall("accessibility/relation[@type='labelled-by']")
+        check_rels(filename, root, labelled_by)
+
+        member_of = obj.findall("accessibility/relation[@type='member-of']")
+        check_rels(filename, root, member_of)
+
+        if obj.attrib['class'] == 'GtkLabel':
+            # Case 0: A 'GtkLabel' must contain one or more "label-for"
+            # pointing to existing elements or...
+            if len(label_for) > 0:
+                continue
+
+            # ...a single "mnemonic_widget"
+            properties = obj.findall("property[@name='mnemonic_widget']")
+            check_props(filename, root, properties)
+            if len(properties) > 1:
+                # It does not make sense for a label to be a mnemonic for
+                # several actions.
+                lines = ', '.join([str(p.sourceline) for p in properties])
+                err(filename, obj, "too many sub-elements"
+                    ", expected single <property name='mnemonic_widgets'>"
+                    ": lines %s" % lines)
+                continue
+            if len(properties) == 1:
+                continue
+            # TODO: warn that it is a label for nothing
+            continue
+
+        # Case 1: has a <child internal-child="accessible"> sub-element
+        children = obj.findall("child[@internal-child='accessible']")
+        if children:
+            if len(children) > 1:
+                lines = ', '.join([str(c.sourceline) for c in children])
+                err(filename, obj, "too many sub-elements"
+                    ", expected single <child internal-child='accessible'>"
+                    ": lines %s" % lines)
+            continue
+
+        # Case 2: has an <accessibility> sub-element with a "labelled-by"
+        # <relation> pointing to an existing element.
+        if len(labelled_by) > 0:
+            continue
+
+        # TODO: after a few more checks and false-positives filtering, warn
+        # that this does not have a label
+
+
+def usage():
+    print("%s [-W error|none] [file ...]" % progname,
+          file=sys.stderr)
+    sys.exit(2)
+
+
+def main():
+    global Werror, Wnone, errors
+
+    try:
+        opts, args = getopt.getopt(sys.argv[1:], "pW:")
+    except getopt.GetoptError:
+        usage()
+
+    for o, a in opts:
+        if o == "-W":
+            if a == "error":
+                Werror = True
+            elif a == "none":
+                Wnone = True
+
+    if not args:
+        sys.exit("%s: no input files" % progname)
+
+    for filename in args:
+        try:
+            tree = ET.parse(filename)
+        except ET.ParseError:
+            err(filename, None, "malformatted xml file")
+        except IOError:
+            err(filename, None, "unable to read file")
+
+        try:
+            check_a11y_relation(filename, tree.getroot())
+        except Exception as error:
+            import traceback
+            traceback.print_exc()
+            err(filename, None, "error parsing file")
+
+
+    if errors > 0:
+        sys.exit(1)
+
+
+if __name__ == "__main__":
+    try:
+        main()
+    except KeyboardInterrupt:
+        pass
diff --git a/config_host.mk.in b/config_host.mk.in
index 8ce71275ca5c..f4d7d67a6601 100644
--- a/config_host.mk.in
+++ b/config_host.mk.in
@@ -460,6 +460,7 @@ export PTHREAD_LIBS=@PTHREAD_LIBS@
 export PYTHON_CFLAGS=$(gb_SPACE)@PYTHON_CFLAGS@
 export PYTHON_FOR_BUILD=@PYTHON_FOR_BUILD@
 export PYTHON_LIBS=$(gb_SPACE)@PYTHON_LIBS@
+export PYTHON_LXML=@PYTHON_LXML@
 export PYTHON_VERSION=@PYTHON_VERSION@
 export PYTHON_VERSION_MAJOR=@PYTHON_VERSION_MAJOR@
 export PYTHON_VERSION_MINOR=@PYTHON_VERSION_MINOR@
diff --git a/configure.ac b/configure.ac
index e20e91e7fa42..479968be94b9 100644
--- a/configure.ac
+++ b/configure.ac
@@ -8148,10 +8148,19 @@ if test $enable_python = system; then
 fi
 
 dnl By now enable_python should be "system", "internal" or "no"
+PYTHON_LXML=
 case $enable_python in
 system)
     SYSTEM_PYTHON=TRUE
 
+    AC_MSG_CHECKING([for python lxml])
+    if $PYTHON -c "import lxml.etree as ET" ; then
+        PYTHON_LXML=TRUE
+        AC_MSG_RESULT([yes])
+    else
+        AC_MSG_RESULT([no, will not be able to check UI accessibility])
+    fi
+
     dnl Check if the headers really work
     save_CPPFLAGS="$CPPFLAGS"
     CPPFLAGS="$CPPFLAGS $PYTHON_CFLAGS"
@@ -8213,6 +8222,7 @@ AC_SUBST(DISABLE_PYTHON)
 AC_SUBST(SYSTEM_PYTHON)
 AC_SUBST(PYTHON_CFLAGS)
 AC_SUBST(PYTHON_LIBS)
+AC_SUBST(PYTHON_LXML)
 AC_SUBST(PYTHON_VERSION)
 AC_SUBST(PYTHON_VERSION_MAJOR)
 AC_SUBST(PYTHON_VERSION_MINOR)
diff --git a/solenv/gbuild/TargetLocations.mk b/solenv/gbuild/TargetLocations.mk
index a8310655039a..06ec8bea0f43 100644
--- a/solenv/gbuild/TargetLocations.mk
+++ b/solenv/gbuild/TargetLocations.mk
@@ -156,6 +156,7 @@ gb_ScpTemplateTarget_get_target = $(abspath $(WORKDIR)/ScpTemplateTarget/$(dir $
 gb_SdiTarget_get_target = $(WORKDIR)/SdiTarget/$(1)
 gb_ThesaurusIndexTarget_get_target = $(WORKDIR)/ThesaurusIndexTarget/$(basename $(1)).idx
 gb_UIConfig_get_imagelist_target = $(WORKDIR)/UIConfig/$(1).ilst
+gb_UIConfig_get_a11yerrors_target = $(WORKDIR)/UIConfig/$(1).a11yerrors
 gb_UIConfig_get_target = $(WORKDIR)/UIConfig/$(1).done
 gb_UIImageListTarget_get_target = $(WORKDIR)/UIImageListTarget/$(1).ilst
 gb_UIMenubarTarget_get_target = $(WORKDIR)/UIMenubarTarget/$(1).xml
@@ -280,6 +281,7 @@ $(eval $(call gb_Helper_make_clean_targets,\
 	CppunitTestFakeExecutable \
 	CustomTarget \
 	ExternalProject \
+	UIA11YErrorsTarget \
 	UIConfig \
 	UIImageListTarget \
 	UIMenubarTarget \
diff --git a/solenv/gbuild/UIConfig.mk b/solenv/gbuild/UIConfig.mk
index 01bfdbaea5b6..fb8762e8a09c 100644
--- a/solenv/gbuild/UIConfig.mk
+++ b/solenv/gbuild/UIConfig.mk
@@ -94,6 +94,7 @@ endef
 # * UIConfig/<name> containing all nontranslatable files
 
 gb_UIConfig_INSTDIR := $(LIBO_SHARE_FOLDER)/config/soffice.cfg
+gb_UIConfig_a11yerrors_COMMAND = $(SRCDIR)/bin/gla11y
 
 $(dir $(call gb_UIConfig_get_target,%)).dir :
 	$(if $(wildcard $(dir $@)),,mkdir -p $(dir $@))
@@ -101,7 +102,7 @@ $(dir $(call gb_UIConfig_get_target,%)).dir :
 $(dir $(call gb_UIConfig_get_target,%))%/.dir :
 	$(if $(wildcard $(dir $@)),,mkdir -p $(dir $@))
 
-$(call gb_UIConfig_get_target,%) : $(call gb_UIConfig_get_imagelist_target,%)
+$(call gb_UIConfig_get_target,%) : $(call gb_UIConfig_get_imagelist_target,%) $(call gb_UIConfig_get_a11yerrors_target,%)
 	$(call gb_Output_announce,$*,$(true),UIC,2)
 	$(call gb_Helper_abbreviate_dirs,\
 		touch $@ \
@@ -117,6 +118,25 @@ $(call gb_UIConfig_get_clean_target,%) :
 		rm -f $(call gb_UIConfig_get_target,$*) \
 	)
 
+define gb_UIConfig_a11yerrors__command
+$(call gb_Output_announce,$(2),$(true),UIA,1)
+$(call gb_Helper_abbreviate_dirs,\
+	$(gb_UIConfig_a11yerrors_COMMAND) -W none $(UIFILE) > $@
+)
+endef
+
+$(call gb_UIConfig_get_a11yerrors_target,%) : $(gb_UIConfig_a11yerrors_COMMAND)
+ifeq ($(PYTHON_LXML),TRUE)
+	$(call gb_UIConfig_a11yerrors__command,$@,$*)
+else
+	touch $@
+endif
+
+.PHONY : $(call gb_UIA11YErrorsTarget_get_clean_target,%)
+$(call gb_UIA11YErrorsTarget_get_clean_target,%) :
+	$(call gb_Output_announce,$*,$(false),UIA,2)
+	rm -f $(call gb_UIConfig_get_a11yerrors_target,$*)
+
 gb_UIConfig_get_packagename = UIConfig/$(1)
 gb_UIConfig_get_packagesetname = UIConfig/$(1)
 
@@ -138,6 +158,7 @@ $(call gb_PackageSet_add_package,$(call gb_UIConfig_get_packagesetname,$(1)),$(c
 
 $(call gb_UIConfig_get_target,$(1)) :| $(dir $(call gb_UIConfig_get_target,$(1))).dir
 $(call gb_UIConfig_get_imagelist_target,$(1)) :| $(dir $(call gb_UIConfig_get_imagelist_target,$(1))).dir
+$(call gb_UIConfig_get_a11yerrors_target,$(1)) :| $(dir $(call gb_UIConfig_get_a11yerrors_target,$(1))).dir
 $(call gb_UIConfig_get_target,$(1)) : $(call gb_PackageSet_get_target,$(call gb_UIConfig_get_packagesetname,$(1)))
 $(call gb_UIConfig_get_clean_target,$(1)) : $(call gb_PackageSet_get_clean_target,$(call gb_UIConfig_get_packagesetname,$(1)))
 
@@ -168,6 +189,9 @@ $(call gb_UIConfig_get_imagelist_target,$(1)) : UI_IMAGELISTS += $(call gb_UIIma
 $(call gb_UIConfig_get_imagelist_target,$(1)) : $(call gb_UIImageListTarget_get_target,$(2))
 $(call gb_UIConfig_get_clean_target,$(1)) : $(call gb_UIImageListTarget_get_clean_target,$(2))
 
+$(call gb_UIConfig_get_a11yerrors_target,$(1)) : UIFILE := $(SRCDIR)/$(2).ui
+$(call gb_UIConfig_get_clean_target,$(1)) : $(call gb_UIA11YErrorsTarget_get_clean_target,$(2))
+
 endef
 
 gb_UIConfig_ALLFILES:=


More information about the Libreoffice-commits mailing list