From 01fc8dabd29f49914fe172a1059b53986b3da2af Mon Sep 17 00:00:00 2001
From: Pierre-Marie de Rodat <derodat@adacore.com>
Date: Wed, 26 Jul 2017 14:38:12 +0200
Subject: [PATCH 2/2] Introduce Python testcases to check DWARF output

For now, this supports only platforms that have an objdump available for
the corresponding target. There are several things that would be nico to
have in the future:

  * add support for more DWARF dumping tools, such as otool on Darwin;

  * have a DWARF location expression decoder, to be able to parse and
    pattern match expressions that objdump does not decode itself;

  * complete the set of decoders for DIE attributes.

gcc/testsuite/

	* .gitignore: Add gcc/testsuite/python/htmlcov
	* lib/gcc-dwarf.exp: New helper files.
	* python/dwarfutils/__init__.py,
	python/dwarfutils/data.py,
	python/dwarfutils/helpers.py,
	python/dwarfutils/objdump.py,
	python/dwarfutils/scan_dwarf.py,
	python/scan-dwarf.py: New Python helpers.
	* python/dwarfutils/test_data.py,
	python/dwarfutils/test_objdump.py,
	python/runtests.py,
	python/test_testutils.py: New testsuite for these Python
	helpers.
	* gcc.dg/debug/dwarf2-py/dwarf2-py.exp,
	gnat.dg/dwarf/dwarf.exp: New test drivers.
	* gcc.dg/debug/dwarf2-py/dwarf-die1.c,
	gcc.dg/debug/dwarf2-py/dwarf-float.c,
	gcc.dg/debug/dwarf2-py/sso.c,
	gcc.dg/debug/dwarf2-py/sso.py,
	gcc.dg/debug/dwarf2-py/var2.c,
	gcc.dg/debug/dwarf2-py/var2.py,
	gnat.dg/dwarf/debug9.adb,
	gnat.dg/dwarf/debug9.py,
	gnat.dg/dwarf/debug11.adb,
	gnat.dg/dwarf/debug11.py,
	gnat.dg/dwarf/debug12.adb,
	gnat.dg/dwarf/debug12.ads: New tests.
---
 .gitignore                                         |   3 +
 gcc/testsuite/gcc.dg/debug/dwarf2-py/dwarf-die1.c  |  15 +
 gcc/testsuite/gcc.dg/debug/dwarf2-py/dwarf-float.c |  26 +
 gcc/testsuite/gcc.dg/debug/dwarf2-py/dwarf2-py.exp |  50 ++
 gcc/testsuite/gcc.dg/debug/dwarf2-py/sso.c         |  19 +
 gcc/testsuite/gcc.dg/debug/dwarf2-py/sso.py        |  52 ++
 gcc/testsuite/gcc.dg/debug/dwarf2-py/var2.c        |  13 +
 gcc/testsuite/gcc.dg/debug/dwarf2-py/var2.py       |  11 +
 gcc/testsuite/gnat.dg/dg.exp                       |   1 +
 gcc/testsuite/gnat.dg/dwarf/debug11.adb            |  19 +
 gcc/testsuite/gnat.dg/dwarf/debug11.py             |  51 ++
 gcc/testsuite/gnat.dg/dwarf/debug12.adb            |  14 +
 gcc/testsuite/gnat.dg/dwarf/debug12.ads            |   8 +
 gcc/testsuite/gnat.dg/dwarf/debug9.adb             |  45 ++
 gcc/testsuite/gnat.dg/dwarf/debug9.py              |  18 +
 gcc/testsuite/gnat.dg/dwarf/dwarf.exp              |  37 ++
 gcc/testsuite/lib/gcc-dwarf.exp                    |  53 ++
 gcc/testsuite/python/dwarfutils/__init__.py        |  70 +++
 gcc/testsuite/python/dwarfutils/data.py            | 616 +++++++++++++++++++++
 gcc/testsuite/python/dwarfutils/helpers.py         |  11 +
 gcc/testsuite/python/dwarfutils/objdump.py         | 404 ++++++++++++++
 gcc/testsuite/python/dwarfutils/scan_dwarf.py      | 145 +++++
 gcc/testsuite/python/dwarfutils/test_data.py       | 486 ++++++++++++++++
 gcc/testsuite/python/dwarfutils/test_objdump.py    | 215 +++++++
 gcc/testsuite/python/dwarfutils/test_scan_dwarf.py | 167 ++++++
 gcc/testsuite/python/runtests.py                   |  57 ++
 gcc/testsuite/python/scan-dwarf.py                 |  27 +
 gcc/testsuite/python/test_testutils.py             |  37 ++
 28 files changed, 2670 insertions(+)
 create mode 100644 gcc/testsuite/gcc.dg/debug/dwarf2-py/dwarf-die1.c
 create mode 100644 gcc/testsuite/gcc.dg/debug/dwarf2-py/dwarf-float.c
 create mode 100644 gcc/testsuite/gcc.dg/debug/dwarf2-py/dwarf2-py.exp
 create mode 100644 gcc/testsuite/gcc.dg/debug/dwarf2-py/sso.c
 create mode 100644 gcc/testsuite/gcc.dg/debug/dwarf2-py/sso.py
 create mode 100644 gcc/testsuite/gcc.dg/debug/dwarf2-py/var2.c
 create mode 100644 gcc/testsuite/gcc.dg/debug/dwarf2-py/var2.py
 create mode 100644 gcc/testsuite/gnat.dg/dwarf/debug11.adb
 create mode 100644 gcc/testsuite/gnat.dg/dwarf/debug11.py
 create mode 100644 gcc/testsuite/gnat.dg/dwarf/debug12.adb
 create mode 100644 gcc/testsuite/gnat.dg/dwarf/debug12.ads
 create mode 100644 gcc/testsuite/gnat.dg/dwarf/debug9.adb
 create mode 100644 gcc/testsuite/gnat.dg/dwarf/debug9.py
 create mode 100644 gcc/testsuite/gnat.dg/dwarf/dwarf.exp
 create mode 100644 gcc/testsuite/lib/gcc-dwarf.exp
 create mode 100644 gcc/testsuite/python/dwarfutils/__init__.py
 create mode 100644 gcc/testsuite/python/dwarfutils/data.py
 create mode 100644 gcc/testsuite/python/dwarfutils/helpers.py
 create mode 100644 gcc/testsuite/python/dwarfutils/objdump.py
 create mode 100644 gcc/testsuite/python/dwarfutils/scan_dwarf.py
 create mode 100644 gcc/testsuite/python/dwarfutils/test_data.py
 create mode 100644 gcc/testsuite/python/dwarfutils/test_objdump.py
 create mode 100644 gcc/testsuite/python/dwarfutils/test_scan_dwarf.py
 create mode 100755 gcc/testsuite/python/runtests.py
 create mode 100755 gcc/testsuite/python/scan-dwarf.py
 create mode 100644 gcc/testsuite/python/test_testutils.py

diff --git a/.gitignore b/.gitignore
index b53f60db792..03f9f66d571 100644
--- a/.gitignore
+++ b/.gitignore
@@ -55,3 +55,6 @@ REVISION
 /mpc*
 /gmp*
 /isl*
+
+# ignore code coverage HTML report for the Python testsuite
+gcc/testsuite/python/htmlcov
diff --git a/gcc/testsuite/gcc.dg/debug/dwarf2-py/dwarf-die1.c b/gcc/testsuite/gcc.dg/debug/dwarf2-py/dwarf-die1.c
new file mode 100644
index 00000000000..7fef7600c3f
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/debug/dwarf2-py/dwarf-die1.c
@@ -0,0 +1,15 @@
+/* Verify that inline function never actually inlined has no abstract DIE.  */
+/* { dg-do assemble } */
+/* { dg-options "-O2 -gdwarf" } */
+/* { dg-final { scan-dwarf } }
+>>>
+Match('DW_TAG_subprogram',
+      attrs={'DW_AT_name': 't',
+	     'DW_AT_inline': None})
+>>> */
+
+inline int t()
+{
+}
+
+int (*q)()=t;
diff --git a/gcc/testsuite/gcc.dg/debug/dwarf2-py/dwarf-float.c b/gcc/testsuite/gcc.dg/debug/dwarf2-py/dwarf-float.c
new file mode 100644
index 00000000000..481b55ac544
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/debug/dwarf2-py/dwarf-float.c
@@ -0,0 +1,26 @@
+/* Verify the DWARF encoding of C99 floating point types.  */
+
+/* { dg-do assemble } */
+/* { dg-options "-O0 -gdwarf" } */
+/* { dg-final { scan-dwarf } }
+    >>>
+    Match('DW_TAG_base_type',
+	  attrs={'DW_AT_name': 'float',
+		 'DW_AT_byte_size': 4,
+		 'DW_AT_encoding': 'float'})
+    Match('DW_TAG_base_type',
+	  attrs={'DW_AT_name': 'double',
+		 'DW_AT_byte_size': 8,
+		 'DW_AT_encoding': 'float'})
+    Match('DW_TAG_base_type',
+	  attrs={'DW_AT_name': 'long double',
+		 'DW_AT_byte_size': 16,
+		 'DW_AT_encoding': 'float'})
+    >>> */
+
+void foo ()
+{
+  float f = 1.5f;
+  double d = 1.5;
+  long double l = 1.5l;
+}
diff --git a/gcc/testsuite/gcc.dg/debug/dwarf2-py/dwarf2-py.exp b/gcc/testsuite/gcc.dg/debug/dwarf2-py/dwarf2-py.exp
new file mode 100644
index 00000000000..70e5c4df25e
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/debug/dwarf2-py/dwarf2-py.exp
@@ -0,0 +1,50 @@
+# Copyright (C) 2017 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 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 General Public License for more details.
+# 
+# You should have received a copy of the GNU General Public License
+# along with GCC; see the file COPYING3.  If not see
+# <http://www.gnu.org/licenses/>.
+
+# Testsuite driver for testcases that check the DWARF output with Python
+# scripts.
+
+load_lib gcc-dg.exp
+load_lib gcc-python.exp
+load_lib gcc-dwarf.exp
+
+# This series of tests require a working Python interpreter and a supported
+# host tool to dump DWARF.
+if { ![check-python-available] || ![detect-dwarf-dump-tool] } {
+    return
+}
+
+# If a testcase doesn't have special options, use these.
+global DEFAULT_CFLAGS
+if ![info exists DEFAULT_CFLAGS] then {
+    set DEFAULT_CFLAGS " -ansi -pedantic-errors -gdwarf"
+}
+
+# Initialize `dg'.
+dg-init
+
+# Main loop.
+set comp_output [gcc_target_compile \
+    "$srcdir/$subdir/../trivial.c" "trivial.S" assembly \
+    "additional_flags=-gdwarf"]
+if { ! [string match "*: target system does not support the * debug format*" \
+    $comp_output] } {
+    remove-build-file "trivial.S"
+    dg-runtest [lsort [glob -nocomplain $srcdir/$subdir/*.\[cS\] ] ] "" $DEFAULT_CFLAGS
+}
+
+# All done.
+dg-finish
diff --git a/gcc/testsuite/gcc.dg/debug/dwarf2-py/sso.c b/gcc/testsuite/gcc.dg/debug/dwarf2-py/sso.c
new file mode 100644
index 00000000000..f7429a58179
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/debug/dwarf2-py/sso.c
@@ -0,0 +1,19 @@
+/* { dg-do assemble } */
+/* { dg-options "-gdwarf-3" } */
+/* { dg-final { python-test sso.py } } */
+
+#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
+#define REVERSE_SSO __attribute__((scalar_storage_order("big-endian")));
+#else
+#define REVERSE_SSO __attribute__((scalar_storage_order("little-endian")));
+#endif
+
+struct S0 { int i; };
+
+struct S1 { int i; struct S0 s; } REVERSE_SSO;
+
+struct S2 { int a[4]; struct S0 s; } REVERSE_SSO;
+
+struct S0 s0;
+struct S1 s1;
+struct S2 s2;
diff --git a/gcc/testsuite/gcc.dg/debug/dwarf2-py/sso.py b/gcc/testsuite/gcc.dg/debug/dwarf2-py/sso.py
new file mode 100644
index 00000000000..0c95abfe2b8
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/debug/dwarf2-py/sso.py
@@ -0,0 +1,52 @@
+import dwarfutils
+from dwarfutils.data import Capture, DIE, Matcher
+from testutils import check
+
+
+cu = dwarfutils.parse_dwarf()
+s0 = cu.find(tag='DW_TAG_structure_type', name='S0')
+s1 = cu.find(tag='DW_TAG_structure_type', name='S1')
+s2 = cu.find(tag='DW_TAG_structure_type', name='S2')
+
+# Check the DIE structure of these structure types
+m0 = s0.tree_check(Matcher(
+    'DW_TAG_structure_type', 'S0',
+    children=[Matcher('DW_TAG_member', 'i',
+                      attrs={'DW_AT_type': Capture('s0_i_type')})]
+))
+m1 = s1.tree_check(Matcher(
+    'DW_TAG_structure_type', 'S1',
+    children=[
+        Matcher('DW_TAG_member', 'i',
+                attrs={'DW_AT_type': Capture('s1_i_type')}),
+        Matcher('DW_TAG_member', 's', attrs={'DW_AT_type': s0}),
+    ]
+))
+m2 = s2.tree_check(Matcher(
+    'DW_TAG_structure_type', 'S2',
+    children=[
+        Matcher('DW_TAG_member', 'a',
+                attrs={'DW_AT_type': Capture('s2_a_type')}),
+        Matcher('DW_TAG_member', 's', attrs={'DW_AT_type': s0}),
+    ]
+))
+
+# Now check that their scalar members have expected types
+s0_i_type = m0.capture('s0_i_type').value
+s1_i_type = m1.capture('s1_i_type').value
+s2_a_type = m2.capture('s2_a_type').value
+
+# S0.i must not have a DW_AT_endianity attribute.  S1.i must have one.
+s0_i_type.tree_check(Matcher('DW_TAG_base_type',
+                             attrs={'DW_AT_endianity': None}))
+s1_i_type.tree_check(Matcher('DW_TAG_base_type',
+                             attrs={'DW_AT_endianity': True}))
+
+# So does the integer type that S2.a contains.
+ma = s2_a_type.tree_check(Matcher(
+    'DW_TAG_array_type',
+    attrs={'DW_AT_type': Capture('element_type')}
+))
+element_type = ma.capture('element_type').value
+check(element_type == s1_i_type,
+      'check element type of S2.a is type of S1.i')
diff --git a/gcc/testsuite/gcc.dg/debug/dwarf2-py/var2.c b/gcc/testsuite/gcc.dg/debug/dwarf2-py/var2.c
new file mode 100644
index 00000000000..e77adc0eaf5
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/debug/dwarf2-py/var2.c
@@ -0,0 +1,13 @@
+/* PR 23190 */
+/* { dg-do assemble } */
+/* { dg-options "-O2 -gdwarf" } */
+/* { dg-final { python-test var2.py } } */
+
+static int foo;
+int bar;
+int main(void)
+{
+   foo += 3;
+   bar *= 5;
+   return 0;
+}
diff --git a/gcc/testsuite/gcc.dg/debug/dwarf2-py/var2.py b/gcc/testsuite/gcc.dg/debug/dwarf2-py/var2.py
new file mode 100644
index 00000000000..9a9b2c4a4ca
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/debug/dwarf2-py/var2.py
@@ -0,0 +1,11 @@
+import dwarfutils
+from dwarfutils.data import Capture, DIE, Matcher
+from testutils import check
+
+
+cu = dwarfutils.parse_dwarf()
+foo = cu.find(tag='DW_TAG_variable', name='foo')
+bar = cu.find(tag='DW_TAG_variable', name='bar')
+
+foo.check_attr('DW_AT_location', [('DW_OP_addr', '0')])
+bar.check_attr('DW_AT_location', [('DW_OP_addr', '0')])
diff --git a/gcc/testsuite/gnat.dg/dg.exp b/gcc/testsuite/gnat.dg/dg.exp
index 228c71e85bb..dff86600957 100644
--- a/gcc/testsuite/gnat.dg/dg.exp
+++ b/gcc/testsuite/gnat.dg/dg.exp
@@ -18,6 +18,7 @@
 
 # Load support procs.
 load_lib gnat-dg.exp
+load_lib gcc-python.exp
 
 # If a testcase doesn't have special options, use these.
 global DEFAULT_CFLAGS
diff --git a/gcc/testsuite/gnat.dg/dwarf/debug11.adb b/gcc/testsuite/gnat.dg/dwarf/debug11.adb
new file mode 100644
index 00000000000..a87470925f1
--- /dev/null
+++ b/gcc/testsuite/gnat.dg/dwarf/debug11.adb
@@ -0,0 +1,19 @@
+--  { dg-options "-cargs -O0 -g -dA -fgnat-encodings=minimal -margs" }
+--  { dg-do assemble }
+--  { dg-final { python-test debug11.py } }
+
+with Ada.Text_IO;
+
+procedure Debug11 is
+   type Rec_Type (C : Character) is record
+      case C is
+         when 'Z' .. Character'Val (128) => I : Integer;
+         when others                     => null;
+      end case;
+   end record;
+   --  R : Rec_Type := ('Z', 2);
+   R : Rec_Type ('Z');
+begin
+   R.I := 0;
+   Ada.Text_IO.Put_Line ("" & R.C);
+end Debug11;
diff --git a/gcc/testsuite/gnat.dg/dwarf/debug11.py b/gcc/testsuite/gnat.dg/dwarf/debug11.py
new file mode 100644
index 00000000000..26c3fdfeeda
--- /dev/null
+++ b/gcc/testsuite/gnat.dg/dwarf/debug11.py
@@ -0,0 +1,51 @@
+import dwarfutils
+from dwarfutils.data import Capture, DIE, Matcher
+from testutils import check, print_pass
+
+
+cu = dwarfutils.parse_dwarf()
+rec_type = cu.find(tag='DW_TAG_structure_type', name='debug11__rec_type')
+
+check(rec_type.parent.matches(tag='DW_TAG_subprogram', name='debug11'),
+      'check that rec_type appears in the expected context')
+
+# Check that rec_type has the expected DIE structure
+m = rec_type.tree_check(Matcher(
+    'DW_TAG_structure_type', 'debug11__rec_type',
+    children=[
+        Matcher('DW_TAG_member', 'c', capture='c'),
+        Matcher(
+            'DW_TAG_variant_part',
+            attrs={'DW_AT_discr': Capture('discr')},
+            children=[
+                Matcher(
+                    'DW_TAG_variant',
+                    attrs={'DW_AT_discr_list': Capture('discr_list'),
+                           'DW_AT_discr_value': None},
+                    children=[
+                        Matcher('DW_TAG_member', 'i'),
+                    ]
+                ),
+                Matcher(
+                    'DW_TAG_variant',
+                    attrs={'DW_AT_discr_list': None,
+                           'DW_AT_discr_value': None},
+                    children=[]
+                )
+            ]
+        )
+    ]
+))
+
+# Check that DW_AT_discr refers to the expected DW_TAG_member
+c = m.capture('c')
+discr = m.capture('discr')
+check(c == discr.value, 'check that discriminant is {}'.format(discr.value))
+
+# Check that DW_AT_discr_list has the expected content: the C discriminant must
+# be properly described as unsigned, hence the 0x5a ('Z') and 0x80 0x01 (128)
+# values in the DW_AT_discr_list attribute. If it was described as signed, we
+# would have instead 90 and -128.
+discr_list = m.capture('discr_list')
+check(discr_list.value == [0x1, 0x5a, 0x80, 0x1],
+      'check discriminant list')
diff --git a/gcc/testsuite/gnat.dg/dwarf/debug12.adb b/gcc/testsuite/gnat.dg/dwarf/debug12.adb
new file mode 100644
index 00000000000..7b9839687c9
--- /dev/null
+++ b/gcc/testsuite/gnat.dg/dwarf/debug12.adb
@@ -0,0 +1,14 @@
+--  { dg-options "-cargs -gdwarf-4 -margs" }
+--  { dg-do assemble }
+--  { dg-final { scan-dwarf } }
+--  >>>
+--  Match('DW_TAG_variable', 'debug12__a2___XR_debug12__a___XEXS2',
+--        attrs={'DW_AT_location': [('DW_OP_const1s', '-1')]})
+--  >>>
+
+package body Debug12 is
+   function Get_A2 return Boolean is
+   begin
+      return A2;
+   end Get_A2;
+end Debug12;
diff --git a/gcc/testsuite/gnat.dg/dwarf/debug12.ads b/gcc/testsuite/gnat.dg/dwarf/debug12.ads
new file mode 100644
index 00000000000..dbc5896cc73
--- /dev/null
+++ b/gcc/testsuite/gnat.dg/dwarf/debug12.ads
@@ -0,0 +1,8 @@
+package Debug12 is
+   type Bit_Array is array (Positive range <>) of Boolean
+      with Pack;
+   A  : Bit_Array := (1 .. 10 => False);
+   A2 : Boolean renames A (2);
+
+   function Get_A2 return Boolean;
+end Debug12;
diff --git a/gcc/testsuite/gnat.dg/dwarf/debug9.adb b/gcc/testsuite/gnat.dg/dwarf/debug9.adb
new file mode 100644
index 00000000000..9ed66b55cdf
--- /dev/null
+++ b/gcc/testsuite/gnat.dg/dwarf/debug9.adb
@@ -0,0 +1,45 @@
+--  { dg-options "-cargs -g -fgnat-encodings=minimal -dA -margs" }
+--  { dg-do assemble }
+--  { dg-final { python-test debug9.py } }
+
+procedure Debug9 is
+   type Array_Type is array (Natural range <>) of Integer;
+   type Record_Type (L1, L2 : Natural) is record
+      I1 : Integer;
+      A1 : Array_Type (1 .. L1);
+      I2 : Integer;
+      A2 : Array_Type (1 .. L2);
+      I3 : Integer;
+   end record;
+
+   function Get (L1, L2 : Natural) return Record_Type is
+      Result : Record_Type (L1, L2);
+   begin
+      Result.I1 := 1;
+      for I in Result.A1'Range loop
+         Result.A1 (I) := I;
+      end loop;
+      Result.I2 := 2;
+      for I in Result.A2'Range loop
+         Result.A2 (I) := I;
+      end loop;
+      Result.I3 := 3;
+      return Result;
+   end Get;
+
+   R1 : Record_Type := Get (0, 0);
+   R2 : Record_Type := Get (1, 0);
+   R3 : Record_Type := Get (0, 1);
+   R4 : Record_Type := Get (2, 2);
+
+   procedure Process (R : Record_Type) is
+   begin
+      null;
+   end Process;
+
+begin
+   Process (R1);
+   Process (R2);
+   Process (R3);
+   Process (R4);
+end Debug9;
diff --git a/gcc/testsuite/gnat.dg/dwarf/debug9.py b/gcc/testsuite/gnat.dg/dwarf/debug9.py
new file mode 100644
index 00000000000..c7d8ec478fe
--- /dev/null
+++ b/gcc/testsuite/gnat.dg/dwarf/debug9.py
@@ -0,0 +1,18 @@
+import dwarfutils
+from dwarfutils.data import Capture, DIE, Matcher
+from testutils import check, print_pass, print_fail
+
+
+cu = dwarfutils.parse_dwarf()
+cu_die = cu.root
+
+# Check that array and structure types are not declared as compilation
+# unit-level types.
+types = cu.find(
+    predicate=lambda die: die.tag in ('DW_TAG_structure_type ',
+                                      'DW_TAG_array_type'),
+    single=False
+)
+
+global_types = [t for t in types if t.parent == cu_die]
+check(not global_types, 'check composite types are not global')
diff --git a/gcc/testsuite/gnat.dg/dwarf/dwarf.exp b/gcc/testsuite/gnat.dg/dwarf/dwarf.exp
new file mode 100644
index 00000000000..b8fee73229c
--- /dev/null
+++ b/gcc/testsuite/gnat.dg/dwarf/dwarf.exp
@@ -0,0 +1,37 @@
+# Copyright (C) 2017 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 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 General Public License for more details.
+# 
+# You should have received a copy of the GNU General Public License
+# along with GCC; see the file COPYING3.  If not see
+# <http://www.gnu.org/licenses/>.
+
+# Testsuite driver for testcases that check the DWARF output with Python
+# scripts.
+
+load_lib gnat-dg.exp
+load_lib gcc-python.exp
+load_lib gcc-dwarf.exp
+
+# This series of tests require a working Python interpreter and a supported
+# host tool to dump DWARF.
+if { ![check-python-available] || ![detect-dwarf-dump-tool] } {
+    return
+}
+
+# Initialize `dg'.
+dg-init
+
+# Main loop.
+dg-runtest [lsort [glob $srcdir/$subdir/*.adb]] "" ""
+
+# All done.
+dg-finish
diff --git a/gcc/testsuite/lib/gcc-dwarf.exp b/gcc/testsuite/lib/gcc-dwarf.exp
new file mode 100644
index 00000000000..61eef735f8b
--- /dev/null
+++ b/gcc/testsuite/lib/gcc-dwarf.exp
@@ -0,0 +1,53 @@
+# Copyright (C) 2017 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 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 General Public License for more details.
+# 
+# You should have received a copy of the GNU General Public License
+# along with GCC; see the file COPYING3.  If not see
+# <http://www.gnu.org/licenses/>.
+
+# Helpers to run tools to dump DWARF
+
+load_lib "remote.exp"
+
+# Look for a tool that we can use to dump DWARF. If nothing is found, return 0.
+#
+# If one is found, return 1, set the DWARF_DUMP_TOOL_KIND environment variable
+# to contain the class of tool detected (e.g. objdump) and set the
+# DWARF_DUMP_TOOL to the name of the tool program (e.g. arm-eabi-objdump).
+
+proc detect-dwarf-dump-tool { } {
+
+    # Look for an objdump corresponding to the current target
+    set objdump [transform objdump]
+    set result [local_exec "which $objdump" "" "" 300]
+    set status [lindex $result 0]
+
+    if { $status == 0 } {
+	setenv DWARF_DUMP_TOOL_KIND objdump
+	setenv DWARF_DUMP_TOOL $objdump
+	return 1
+    }
+
+    return 0
+}
+
+# Run the "scan-dwarf.py" check program on the current source file
+
+proc scan-dwarf { } {
+    # This assumes that we are three frames down from dg-test, and that
+    # it still stores the filename of the testcase in a local variable "name".
+    # A cleaner solution would require a new DejaGnu release.
+    upvar 2 prog src_file
+
+    set support_dir [python-support-path]
+    python-test-with-src "$support_dir/scan-dwarf.py" $src_file
+}
diff --git a/gcc/testsuite/python/dwarfutils/__init__.py b/gcc/testsuite/python/dwarfutils/__init__.py
new file mode 100644
index 00000000000..965be0a8c4c
--- /dev/null
+++ b/gcc/testsuite/python/dwarfutils/__init__.py
@@ -0,0 +1,70 @@
+# Copyright (C) 2017 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with GCC; see the file COPYING3.  If not see
+# <http://www.gnu.org/licenses/>.
+
+# Helpers to parse and check DWARF in object files.
+#
+# The purpose of these is to make it easy to write "smart" tests on DWARF
+# information: pattern matching on DIEs and their attributes, check links
+# between DIEs, etc. Doing these checks using abstract representations of DIEs
+# is far easier than scanning the generated assembly!
+
+import os
+import sys
+
+import dwarfutils.objdump
+import testutils
+
+
+# Fetch the DWARF parsing function that correspond to the DWARF dump tool to
+# use.
+DWARF_DUMP_TOOL_KIND = os.environ['DWARF_DUMP_TOOL_KIND']
+DWARF_DUMP_TOOL = os.environ['DWARF_DUMP_TOOL']
+
+dwarf_parsers = {
+    'objdump': dwarfutils.objdump.parse_dwarf,
+}
+try:
+    dwarf_parser = dwarf_parsers[DWARF_DUMP_TOOL_KIND]
+except KeyError: # no-coverage
+    raise RuntimeError('Unhandled DWARF dump tool: {}'.format(
+        DWARF_DUMP_TOOL_KIND
+    ))
+
+
+def parse_dwarf(object_file=None, single_cu=True): # no-coverage
+    """
+    Fetch and decode DWARF compilation units in `object_file`.
+
+    If `single_cu` is true, make sure there is exactly one compilation unit and
+    return it. Otherwise, return compilation units as a list.
+
+    :param str|None object_file: Name of the object file to process. If left to
+        None, `sys.argv[1]` is used instead.
+
+    :rtype: dwarfutils.data.CompilationUnit
+           |list[dwarfutils.data.CompilationUnit]
+    """
+    script_args = testutils.parse_args()
+    result = dwarf_parser(script_args.object_file)
+
+    if single_cu:
+        if not result:
+            return None
+        if len(result) > 1:
+            raise ValueError('Multiple compilation units found')
+        return result[0]
+    else:
+        return result
diff --git a/gcc/testsuite/python/dwarfutils/data.py b/gcc/testsuite/python/dwarfutils/data.py
new file mode 100644
index 00000000000..4a47a636cb7
--- /dev/null
+++ b/gcc/testsuite/python/dwarfutils/data.py
@@ -0,0 +1,616 @@
+# Copyright (C) 2017 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with GCC; see the file COPYING3.  If not see
+# <http://www.gnu.org/licenses/>.
+
+# Data structures to represent DWARF compilation units, DIEs and attributes,
+# and helpers to perform various checks on them.
+
+from testutils import check
+
+
+class Abbrev(object):
+    """DWARF abbreviation entry."""
+
+    def __init__(self, number, tag, has_children, attributes=[]):
+        """
+        :param int number: Abbreviation number, which is 1-based, as in the
+            DWARF standard.
+        :param str|int tag: Tag name or, if unknown, tag number.
+        :param bool has_children: Whether DIEs will have children.
+        :param list[(str|int, str)] attributes: List of attributes.
+        """
+        self.number = number
+        self.tag = tag
+        self.has_children = has_children
+        self.attributes = list(attributes)
+
+    def add_attribute(self, name, form):
+        """
+        :param str|int name: Attribute name or, if unknown, attribute number.
+        :param str form: Form for this attribute.
+        """
+        self.attributes.append((name, form))
+
+    def __repr__(self):
+        return '<Abbrev #{0} {1}>'.format(self.number, self.tag)
+
+
+class CompilationUnit(object):
+    """DWARF compilation unit."""
+
+    def __init__(self, offset, length, is_32bit, version, abbrevs,
+                 pointer_size):
+        """
+        :param int offset: Offset of this compilation unit in the .debug_info
+            section.
+        :param int length: Value of the length field for this compilation unit.
+        :param bool is_32bit: Whether this compilation unit is encoded in the
+            32-bit format. If not, it must be the 64-bit one.
+        :param int version: DWARF version used by this compilation unit.
+        :param list[Abbrev] abbrevs: List of abbreviations for this compilation
+            unit.
+        :param int pointer_size: Size of pointers for this architecture.
+        """
+        self.offset = offset
+        self.length = length
+        self.is_32bit = is_32bit
+        self.version = version
+        self.abbrevs = abbrevs
+        self.pointer_size = pointer_size
+
+        self.root = None
+        self.offset_to_die = {}
+
+    def set_root(self, die):
+        assert self.root is None, ('Trying to create the root DIE of a'
+                                   ' compilation unit that already has one')
+        self.root = die
+
+    def get(self, offset):
+        """
+        Fetch the DIE that starts at the given offset. Raise a KeyError if not
+        found.
+
+        :param int offset: DIE offset in this compilation unit.
+        :rtype: DIE
+        """
+        return self.offset_to_die[offset]
+
+    def find(self, *args, **kwargs):
+        return self.root.find(*args, **kwargs)
+
+    def __repr__(self):
+        return '<CompilationUnit at {0:#0x}>'.format(self.offset)
+
+
+class DIE(object):
+    """DWARF information entry."""
+
+    def __init__(self, cu, level, offset, abbrev_number):
+        """
+        :param CompilationUnit cu: Compilation unit this DIE belongs to.
+        :param int level: Depth for this DIE.
+        :param int offset: Offset of this DIE in the .debug_info section.
+        :param int abbrev_number: Abbreviation number for this DIE.
+        """
+        self.cu = cu
+        self.cu.offset_to_die[offset] = self
+
+        self.level = level
+        self.offset = offset
+        self.abbrev_number = abbrev_number
+
+        self.parent = None
+        self.attributes = []
+        self.children = []
+
+    @property
+    def abbrev(self):
+        """Abbreviation for this DIE.
+
+        :rtype: Abbrev
+        """
+        # The abbreviation number is 1-based, but list indexes are 0-based
+        return self.cu.abbrevs[self.abbrev_number - 1]
+
+    @property
+    def tag(self):
+        """Tag for this DIE.
+
+        :rtype: str|int
+        """
+        return self.abbrev.tag
+
+    @property
+    def has_children(self):
+        return self.abbrev.has_children
+
+    def get_attr(self, name, single=True, or_error=True):
+        """Look for an attribute in this DIE.
+
+        :param str|int name: Attribute name, or number if name is unknown.
+        :param bool single: If true, this will raise a KeyError for
+            zero/multiple matches and return an Attribute instance when found.
+            Otherwise, return a potentially empty list of attributes.
+        :param bool or_error: When true, if `single` is true and no attribute
+            is found, return None instead of raising a KeyError.
+        :rtype: Attribute|list[Attribute]
+        """
+        result = [a for a in self.attributes if a.name == name]
+
+        if single:
+            if not result:
+                if or_error:
+                    raise KeyError('No {0} attribute in {1}'.format(name,
+                                                                    self))
+                else:
+                    return None
+            if len(result) > 1:
+                raise KeyError('Multiple {0} attributes in {1}'.format(name,
+                                                                       self))
+            return result[0]
+        else:
+            return result
+
+    def check_attr(self, name, value):
+        """Check for the presence/value of an attribute.
+
+        :param str|int name: Attribute name, or number if name is unknown.
+        :param value: If None, check that the attribute is not present.
+            Otherwise, check that the attribute exists and that its value
+            matches `value`.
+        """
+        m = MatchResult()
+        Matcher._match_attr(self, name, value, m)
+        check(
+            m.succeeded,
+            m.mismatch_reason or 'check attribute {0} of {1}'.format(name,
+                                                                     self)
+        )
+
+    def get_child(self, child_index):
+        """Get a DIE child.
+
+        :param int child_index: Index of the child to fetch (zero-based index).
+        :rtype: DIE
+        """
+        return self.children[child_index]
+
+    @property
+    def name(self):
+        """Return the name (DW_AT_name) for this DIE, if any.
+
+        :rtype: str|None
+        """
+        name = self.get_attr('DW_AT_name', or_error=False)
+        return name.value if name is not None else None
+
+    def __str__(self):
+        tag = (self.tag if isinstance(self.tag, str) else
+               'DIE {0}'.format(self.tag))
+        name = self.name
+        fmt = '{tag} "{name}"' if name else '{tag}'
+        return fmt.format(tag=tag, name=name)
+
+    def __repr__(self):
+        return '<{0} at {1:#x}>'.format(self, self.offset)
+
+    def matches(self, tag=None, name=None):
+        """Return whether this DIE matches expectations.
+
+        :rtype: bool
+        """
+        return ((tag is None or self.tag == tag) and
+                (name is None or self.name == name))
+
+    def tree_matches(self, matcher):
+        """Match this DIE against the given match object.
+
+        :param Matcher matcher: Match object used to check the structure of
+            this DIE.
+        :rtype: MatchResult
+        """
+        return matcher.matches(self)
+
+    def tree_check(self, matcher):
+        """Like `tree_matches`, but also check that the DIE matches."""
+        m = self.tree_matches(matcher)
+        check(
+            m.succeeded,
+            m.mismatch_reason or 'check structure of {0}'.format(self)
+        )
+        return m
+
+    def find(self, predicate=None, tag=None, name=None, recursive=True,
+             single=True):
+        """Look for a DIE that satisfies the given expectations.
+
+        :param None|(DIE) -> bool predicate: If provided, function that filters
+            out DIEs when it returns false.
+        :param str|int|None tag: If provided, filter out DIEs whose tag does
+            not match.
+        :param str|None name: If provided, filter out DIEs whose name (see
+            the `name` property) does not match.
+        :param bool recursive: If true, perform the search recursively in
+            self's children.
+        :param bool single: If true, look for a single DIE and raise a
+            ValueError if none or several DIEs are found. Otherwise, return a
+            potentially empty list of DIEs.
+
+        :rtype: DIE|list[DIE]
+        """
+        def p(die):
+            return (die.matches(tag, name) and
+                    (predicate is None or predicate(die)))
+        result = self._find(p, recursive)
+
+        if single:
+            if not result:
+                raise ValueError('No matching DIE found')
+            if len(result) > 1:
+                raise ValueError('Multiple matching DIEs found')
+            return result[0]
+        else:
+            return result
+
+    def _find(self, predicate, recursive):
+        result = []
+
+        if predicate(self):
+            result.append(self)
+
+        for c in self.children:
+            if not recursive:
+                if predicate(c):
+                    result.append(c)
+            else:
+                result.extend(c._find(predicate, recursive))
+
+        return result
+
+    def next_attribute_form(self, name):
+        """Return the form of the next attribute this DIE requires.
+
+        Used during DIE tree construction.
+
+        :param str name: Expected name for this attribute. The abbreviation
+            will confirm it.
+        :rtype: str
+        """
+        assert len(self.attributes) < len(self.abbrev.attributes)
+        expected_name, form = self.abbrev.attributes[len(self.attributes)]
+        assert name == expected_name, (
+            'Attribute desynchronization in {0}'.format(self)
+        )
+        return form
+
+    def add_attribute(self, name, form, offset, value):
+        """Add an attribute to this DIE.
+
+        Used during DIE tree construction. See Attribute's constructor for the
+        meaning of arguments.
+        """
+        self.attributes.append(Attribute(self, name, form, offset, value))
+
+    def add_child(self, child):
+        """Add a DIE child to this DIE.
+
+        Used during DIE tree construction.
+
+        :param DIE child: DIE to append.
+        """
+        assert self.has_children
+        assert child.parent is None
+        child.parent = self
+        self.children.append(child)
+
+
+class Attribute(object):
+    """DIE attribute."""
+
+    def __init__(self, die, name, form, offset, value):
+        """
+        :param DIE die: DIE that will own this attribute.
+        :param str|int name: Attribute name, or attribute number if unknown.
+        :param str form: Attribute form.
+        :param int offset: Offset of this attribute in the .debug_info section.
+        :param value: Decoded value for this attribute. If it's a Defer
+            instance, decoding will happen the first time the "value" property
+            is evaluated.
+        """
+        self.die = die
+        self.name = name
+        self.form = form
+        self.offset = offset
+
+        if isinstance(value, Defer):
+            self._value = None
+            self._value_getter = value
+        else:
+            self._value = value
+            self._value_getter = None
+            self._refine_value()
+
+    @property
+    def value(self):
+        if self._value_getter:
+            self._value = self._value_getter.get()
+            self._value_getter = None
+            self._refine_value()
+        return self._value
+
+    def _refine_value(self):
+        # If we hold a location expression, bind it to this attribute
+        if isinstance(self._value, Exprloc):
+            self._value.attribute = self
+
+    def __repr__(self):
+        label = (self.name if isinstance(self.name, str) else
+                 'Attribute {0}'.format(self.name))
+        return '<{0} at {1:#x}>'.format(label, self.offset)
+
+
+class Exprloc(object):
+    """DWARF location expression."""
+
+    def __init__(self, byte_list, operations):
+        """
+        :param list[int] byte_list: List of bytes that encode this expression.
+        :param list[(str, ...)] operations: List of operations this expression
+            contains. Each expression is a tuple whose first element is the
+            opcode name (DW_OP_...) and whose other elements are operands.
+        """
+        self.attribute = None
+        self.byte_list = byte_list
+        self.operations = operations
+
+    @property
+    def die(self):
+        return self.attribute.die
+
+    @staticmethod
+    def format_operation(operation):
+        opcode = operation[0]
+        operands = operation[1:]
+        return ('{0}: {1}'.format(opcode, ' '.join(str(o) for o in operands))
+                if operands else opcode)
+
+    def matches(self, operations):
+        """Match this list of operations to `operations`.
+
+        :param list[(str, ...)] operations: List of operations to match.
+        :rtype: bool
+        """
+        return self.operations == operations
+
+    def __repr__(self):
+        return '{0} ({1})'.format(
+            ' '.join(hex(b) for b in self.byte_list),
+            '; '.join(self.format_operation(op) for op in self.operations)
+        )
+
+
+class Defer(object):
+    """Helper to defer a computation."""
+
+    def __init__(self, func):
+        """
+        :param () -> T func: Callback to perform the computation.
+        """
+        self.func = func
+
+    def get(self):
+        """
+        :rtype: T
+        """
+        return self.func()
+
+
+class Matcher(object):
+    """Specification for DIE tree pattern matching."""
+
+    def __init__(self, tag=None, name=None, attrs=None, children=None,
+                 capture=None):
+        """
+        :param None|str tag: If provided, name of the tag that DIEs must match.
+        :param None|str name: If provided, name that DIEs must match (see the
+            DIE.name property).
+        :param attrs: If provided, dictionary that specifies attribute
+            expectations. Keys are attribute names. Values can be:
+
+              * None, so that attribute must be undefined in the DIE;
+              * a value, so that attribute must be defined and the value must
+                match;
+              * a Capture instance, so that the attribute value (or None, if
+                undefined) is captured.
+
+        :param None | list[DIE|Capture] children: If provided, list of DIEs
+            that children must match. Capture instances match any DIE and
+            captures it.
+
+        :param str|None capture: If provided, capture the DIE to match with the
+            given name.
+        """
+        self.tag = tag
+        self.name = name
+        self.attrs = attrs
+        self.children = children
+        self.capture_name = capture
+
+    def matches(self, die):
+        """Pattern match the given DIE.
+
+        :param DIE die: DIE to match.
+        :rtype: MatchResult
+        """
+        result = MatchResult()
+        self._match_die(die, result)
+        return result
+
+    def _match_die(self, die, result):
+        """Helper for the "matches" method.
+
+        Return whether DIE could be matched. If not, a message to describe why
+        is recorded in `result`.
+
+        :param DIE die: DIE to match.
+        :param MatchResult result: Holder for the result of the match.
+        :rtype: bool
+        """
+
+        # If asked to, check the DIE tag
+        if self.tag is not None and self.tag != die.tag:
+            result.mismatch_reason = '{0} is expected to be a {1}'.format(
+                die, self.tag
+            )
+            return False
+
+        # If asked to, check the DIE name
+        if self.name is not None and self.name != die.name:
+            result.mismatch_reason = (
+                '{0} is expected to be called "{1}"'.format(die, self.name)
+            )
+            return False
+
+        # Check attribute expectations
+        if self.attrs:
+            for n, v in self.attrs.items():
+                if not self._match_attr(die, n, v, result):
+                    return False
+
+        # Check children expectations
+        if self.children is not None:
+
+            # The number of children must match
+            if len(self.children) != len(die.children):
+                result.mismatch_reason = (
+                    '{0} has {1} children, {2} expected'.format(
+                        die, len(die.children), len(self.children)
+                    )
+                )
+                return False
+
+            # Then each child must match the corresponding child matcher
+            for matcher_child, die_child in zip(self.children,
+                                                die.children):
+                # Capture instances matches anything and captures it
+                if isinstance(matcher_child, Capture):
+                    result.dict[matcher_child.name] = die_child
+
+                elif not matcher_child._match_die(die_child, result):
+                    return False
+
+        # Capture the input DIE if asked to
+        if self.capture_name:
+            result.dict[self.capture_name] = die
+
+        # If no check failed, the DIE matches the pattern
+        return True
+
+    @staticmethod
+    def _match_attr(die, attr_name, attr_value, result):
+        """Helper for the "matches" method.
+
+        Return whether the `attr_name` attribute in DIE matches the
+        `attr_value` expectation. If not, a message to describe why is recorded
+        in `result`.
+
+        :param DIE die: DIE that contain the attribute to match.
+        :param str attr_name: Attribute name.
+        :param attr_value: Attribute expectation. See attrs's description in
+            Match.__init__ docstring for possible values.
+        """
+        attr = die.get_attr(attr_name, or_error=False)
+
+        if attr_value is None:
+            # The attribute is expected not to be defined
+            if attr is None:
+                return True
+
+            result.mismatch_reason = (
+                '{0} has a {1} attribute, none expected'.format(
+                    die, attr_name
+                )
+            )
+            return False
+
+        # Capture instances matches anything and capture it
+        if isinstance(attr_value, Capture):
+            result.dict[attr_value.name] = attr
+            return True
+
+        # If we reach this point, the attribute is supposed to be defined:
+        # check it is.
+        if attr is None:
+            result.mismatch_reason = (
+                '{0} is missing a {1} attribute'.format(die, attr_name)
+            )
+            return False
+
+        # Check the value of the attribute matches
+        if isinstance(attr.value, Exprloc):
+            is_matching = attr.value.matches(attr_value)
+        else:
+            is_matching = attr.value == attr_value
+        if not is_matching:
+            result.mismatch_reason = (
+                '{0}: {1} is {2}, expected to be {3}'.format(
+                    die, attr_name, attr.value, attr_value
+                )
+            )
+            return False
+
+        # If no check failed, the attribute matches the pattern
+        return True
+
+
+class Capture(object):
+    """Placeholder in Matcher tree patterns.
+
+    This is used to capture specific elements during pattern matching.
+    """
+    def __init__(self, name):
+        """
+        :param str name: Capture name.
+        """
+        self.name = name
+
+
+class MatchResult(object):
+    """Holder for the result of a DIE tree pattern match."""
+
+    def __init__(self):
+        self.dict = {}
+
+        self.mismatch_reason = None
+        """
+        If left to None, the match succeeded. Otherwise, must be set to a
+        string that describes why the match failed.
+
+        :type: None|str
+        """
+
+    @property
+    def succeeded(self):
+        return self.mismatch_reason is None
+
+    def capture(self, name):
+        """Return what has been captured by the `name` capture.
+
+        This is valid iff the match succeeded.
+
+        :param str name: Capture name:
+        """
+        return self.dict[name]
diff --git a/gcc/testsuite/python/dwarfutils/helpers.py b/gcc/testsuite/python/dwarfutils/helpers.py
new file mode 100644
index 00000000000..06352586226
--- /dev/null
+++ b/gcc/testsuite/python/dwarfutils/helpers.py
@@ -0,0 +1,11 @@
+import sys
+
+
+def as_ascii(str_or_byte): # no-coverage
+    """
+    Python 2/3 compatibility helper.
+
+    In Python 2, just return the input. In Python 3, decode the input as ASCII.
+    """
+    major_version = sys.version_info[0]
+    return str_or_byte if major_version < 3 else str_or_byte.decode('ascii')
diff --git a/gcc/testsuite/python/dwarfutils/objdump.py b/gcc/testsuite/python/dwarfutils/objdump.py
new file mode 100644
index 00000000000..a644c640b0c
--- /dev/null
+++ b/gcc/testsuite/python/dwarfutils/objdump.py
@@ -0,0 +1,404 @@
+# Copyright (C) 2017 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with GCC; see the file COPYING3.  If not see
+# <http://www.gnu.org/licenses/>.
+
+# objdump-based DWARF parser
+
+# TODO: for now, this assumes that there is only one compilation unit per
+# object file. This should be implemented later if needed.
+
+import re
+import subprocess
+
+import dwarfutils
+from dwarfutils.data import Abbrev, CompilationUnit, Defer, DIE, Exprloc
+from dwarfutils.helpers import as_ascii
+
+
+abbrev_tag_re = re.compile(r'\s+(?P<number>\d+)'
+                           r'\s+(?P<tag>DW_TAG_[a-zA-Z0-9_]+)'
+                           r'\s+\[(?P<has_children>.*)\]')
+attr_re = re.compile(r'\s+(?P<attr>DW_AT(_[a-zA-Z0-9_]+| value: \d+))'
+                     r'\s+(?P<form>DW_FORM(_[a-zA-Z0-9_]+| value: \d+))')
+
+compilation_unit_re = re.compile(r'\s+Compilation Unit @ offset'
+                                 r' (?P<offset>0x[0-9a-f]+):')
+compilation_unit_attr_re = re.compile(r'\s+(?P<name>[A-Z][a-zA-Z ]*):'
+                                      r'\s+(?P<value>.*)')
+die_re = re.compile(r'\s+<(?P<level>\d+)>'
+                    r'<(?P<offset>[0-9a-f]+)>:'
+                    r' Abbrev Number: (?P<abbrev_number>\d+)'
+                    r'( \((?P<tag>DW_TAG_[a-zA-Z0-9_]+)\))?')
+die_attr_re = re.compile(r'\s+<(?P<offset>[0-9a-f]+)>'
+                         r'\s+(?P<attr>DW_AT_[a-zA-Z0-9_]+)'
+                         r'\s*: (?P<value>.*)')
+
+indirect_string_re = re.compile(r'\(indirect string, offset: 0x[0-9a-f]+\):'
+                                r' (?P<value>.*)')
+language_re = re.compile(r'(?P<number>\d+)\s+\((?P<name>.*)\)')
+block_re = re.compile(r'\d+ byte block: (?P<value>[0-9a-f ]+)')
+loc_expr_re = re.compile(r'\d+ byte block:'
+                         r' (?P<bytes>[0-9a-f ]+)'
+                         r'\s+\((?P<expr>.*)\)')
+
+
+def command_output(argv): # no-coverage
+    """
+    Dummy re-implementation of `subprocess.check_output`. This function was
+    added in Python 2.7 and we need to support Python 2.6 interpreters.
+
+    :param list[str] argv: Command line to run.
+    :rtype: str
+    """
+    command = argv[0]
+    p = subprocess.Popen(
+        argv, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
+        stderr=subprocess.PIPE
+    )
+    stdout, stderr = p.communicate()
+    if stderr:
+        print('Running {0} gave a non-empty stderr:'.format(command))
+        print(stderr)
+        raise RuntimeError('non-empty stderr')
+    if p.returncode != 0:
+        print('Running {0} gave a non-zero return code ({1})'.format(
+            command, p.returncode
+        ))
+        raise RuntimeError('non-zero return code')
+    return stdout
+
+
+class Objdump(object): # no-coverage
+    """
+    Runner for objdump to get dumps.
+    """
+
+    def __init__(self, object_file):
+        self.object_file = object_file
+
+    def _run(self, part):
+        return [
+            as_ascii(line).rstrip()
+            for line in command_output([dwarfutils.DWARF_DUMP_TOOL,
+                                        '--dwarf=' + part,
+                                        self.object_file]).splitlines()
+            if line.strip()
+        ]
+
+    def get_info(self):
+        """Run objdump --dwarf=info."""
+        return self._run('info')
+
+    def get_abbrev(self):
+        return self._run('abbrev')
+
+
+def parse_dwarf(object_file): # no-coverage
+    """
+    Implementation of dwarfutils.parse_dwarf for objdump.
+
+    Run objdump on `object_file` and parse the list compilation units it
+    contains.
+
+    :param str object_file: Name of the object file to process.
+    :rtype: list[CompilationUnit]
+    """
+    return _parse_dwarf(Objdump(object_file))
+
+
+def parse_abbrevs(object_file): # no-coverage
+    """
+    Run objdump on `object_file` and parse the list of abbreviations it
+    contains.
+
+    :param str object_file: Name of the object file to process.
+    :rtype: list[Abbrev]
+    """
+    return _parse_abbrevs(Objdump(object_file))
+
+
+def _parse_dwarf(objdump):
+    """
+    Implementation of dwarfutils.parse_dwarf for objdump.
+
+    Run objdump on `object_file` and parse the list compilation units it
+    contains.
+
+    :param str object_file: Name of the object file to process.
+    :rtype: list[CompilationUnit]
+    """
+    abbrevs = _parse_abbrevs(objdump)
+
+    lines = objdump.get_info()
+    i = [0]
+    def next_line():
+        if i[0] >= len(lines):
+            return None
+        i[0] += 1
+        return lines[i[0] - 1]
+
+    result = []
+    die_stack = []
+    last_die = None
+
+    while True:
+        line = next_line()
+        if line is None:
+            break
+
+        # Try to match the beginning of a compilation unit
+        m = compilation_unit_re.match(line)
+        if m:
+            offset = int(m.group('offset'), 16)
+
+            attrs = {}
+            while True:
+                m = compilation_unit_attr_re.match(next_line())
+                if not m:
+                    i[0] -= 1
+                    break
+                attrs[m.group('name')] = m.group('value')
+
+            length, is_32bit = attrs['Length'].split()
+            length = int(length, 16)
+            is_32bit = is_32bit == '(32-bit)'
+
+            version = int(attrs['Version'])
+            abbrev_offset = int(attrs['Abbrev Offset'], 16)
+            pointer_size = int(attrs['Pointer Size'])
+
+            assert abbrev_offset == 0, ('Multiple compilations unit are not'
+                                        ' handled for now')
+            abbrevs_sublist = list(abbrevs)
+
+            result.append(CompilationUnit(offset, length, is_32bit, version,
+                                          abbrevs_sublist, pointer_size))
+            continue
+
+        # Try to match the beginning of a DIE
+        m = die_re.match(line)
+        if m:
+            assert result, 'Invalid DIE: missing containing compilation unit'
+            cu = result[-1]
+
+            level = int(m.group('level'))
+            offset = int(m.group('offset'), 16)
+            abbrev_number = int(m.group('abbrev_number'))
+            tag = m.group('tag')
+
+            assert level == len(die_stack)
+
+            # The end of child list is represented as a special DIE with
+            # abbreviation number 0.
+            if tag is None:
+                assert abbrev_number == 0
+                die_stack.pop()
+                continue
+
+            die = DIE(cu, level, offset, abbrev_number)
+            last_die = die
+            assert die.tag == tag, 'Unexpected tag for {0}: got {1}'.format(
+                die, tag
+            )
+            if die_stack:
+                die_stack[-1].add_child(die)
+            else:
+                cu.set_root(die)
+            if die.has_children:
+                die_stack.append(die)
+            continue
+
+        # Try to match an attribute
+        m = die_attr_re.match(line)
+        if m:
+            assert die_stack, 'Invalid attribute: missing containing DIE'
+            die = last_die
+
+            offset = int(m.group('offset'), 16)
+            name = m.group('attr')
+            value = m.group('value')
+
+            form = die.next_attribute_form(name)
+            try:
+                value_decoder = value_decoders[form]
+            except KeyError:
+                pass
+            else:
+                try:
+                    value = value_decoder(die, name, form, offset, value)
+                except ValueError:
+                    print('Error while decoding {0} ({1}) at {2:#x}:'
+                          ' {3}'.format(name, form, offset, value))
+                    raise
+            die.add_attribute(name, form, offset, value)
+            continue
+
+        # Otherwise, we must be processing "header" text before the dump
+        # itself: just discard it.
+        assert not result, 'Unhandled output: ' + line
+
+    return result
+
+
+def _parse_abbrevs(objdump):
+    """
+    Run objdump on `object_file` and parse the list of abbreviations it
+    contains.
+
+    :param str object_file: Name of the object file to process.
+    :rtype: list[Abbrev]
+    """
+    result = []
+
+    for line in objdump.get_abbrev():
+        # Try to match a new abbrevation
+        m = abbrev_tag_re.match(line)
+        if m:
+            number = int(m.group('number'))
+            tag = m.group('tag')
+            has_children = m.group('has_children')
+            assert has_children in ('has children', 'no children')
+            has_children = has_children == 'has children'
+
+            result.append(Abbrev(number, tag, has_children))
+            continue
+
+        # Try to match an attribute
+        m = attr_re.match(line)
+        if m:
+            assert result, 'Invalid attribute: missing containing abbreviation'
+            name = m.group('attr')
+            form = m.group('form')
+
+            # When objdump finds unknown abbreviation numbers or unknown form
+            # numbers, it cannot turn them into names.
+            if name.startswith('DW_AT value'):
+                name = int(name.split()[-1])
+            if form.startswith('DW_FORM value'):
+                form = int(form.split()[-1])
+
+            # The (0, 0) couple marks the end of the attribute list
+            if name != 0 or form != 0:
+                result[-1].add_attribute(name, form)
+            continue
+
+        # Otherwise, we must be processing "header" text before the dump
+        # itself: just discard it.
+        assert not result, 'Unhandled output: ' + line
+
+    return result
+
+
+# Decoders for attribute values
+
+def _decode_flag_present(die, name, form, offset, value):
+    return True
+
+
+def _decode_flag(die, name, form, offset, value):
+    return bool(int(value))
+
+
+def _decode_data(die, name, form, offset, value):
+    if name == 'DW_AT_language':
+        m = language_re.match(value)
+        assert m, 'Unhandled language value: {0}'.format(value)
+        return m.group('name')
+
+    elif name == 'DW_AT_encoding':
+        m = language_re.match(value)
+        assert m, 'Unhandled encoding value: {0}'.format(value)
+        return m.group('name')
+
+    return int(value, 16) if value.startswith('0x') else int(value)
+
+
+def _decode_ref(die, name, form, offset, value):
+    assert value[0] == '<' and value[-1] == '>'
+    offset = int(value[1:-1], 16)
+    return Defer(lambda: die.cu.get(offset))
+
+
+def _decode_indirect_string(die, name, form, offset, value):
+    m = indirect_string_re.match(value)
+    assert m, 'Unhandled indirect string: ' + value
+    return m.group('value')
+
+
+def _decode_block(die, name, form, offset, value, no_exprloc=False):
+    if (
+        not no_exprloc and
+        name in ('DW_AT_location', 'DW_AT_data_member_location')
+    ):
+        return _decode_exprloc(die, name, form, offset, value, )
+
+    m = block_re.match(value)
+    assert m, 'Unhandled block value: {0}'.format(value)
+    return [int(b, 16) for b in m.group('value').split()]
+
+
+def _decode_exprloc(die, name, form, offset, value):
+    m = loc_expr_re.match(value)
+    if not m:
+        # Even though they have the expected DW_FORM_exploc form, objdump does
+        # not decode some location expressions such as DW_AT_byte_size. In this
+        # case, return a dummy block decoding instead.
+        # TODO: implement raw bytes parsing into expressions instead.
+        return _decode_block(die, name, form, offset, value, no_exprloc=True)
+
+    byte_list = [int(b, 16) for b in m.group('bytes').split()]
+
+    expr = m.group('expr')
+    operations = []
+    for op in expr.split('; '):
+        chunks = op.split(': ', 1)
+        assert len(chunks) <= 2, (
+            'Unhandled DWARF expression operation: {0}'.format(op)
+        )
+        opcode = chunks[0]
+        operands = chunks[1].split() if len(chunks) == 2 else []
+        operations.append((opcode, ) + tuple(operands))
+
+    return Exprloc(byte_list, operations)
+
+
+value_decoders = {
+    'DW_FORM_flag_present': _decode_flag_present,
+    'DW_FORM_flag': _decode_flag,
+
+    'DW_FORM_addr': _decode_data,
+    'DW_FORM_sec_offset': _decode_data,
+    'DW_FORM_data1': _decode_data,
+    'DW_FORM_data2': _decode_data,
+    'DW_FORM_data4': _decode_data,
+    'DW_FORM_data8': _decode_data,
+    'DW_FORM_sdata': _decode_data,
+    'DW_FORM_udata': _decode_data,
+
+    'DW_FORM_ref4': _decode_ref,
+    'DW_FORM_ref8': _decode_ref,
+
+    'DW_FORM_strp': _decode_indirect_string,
+
+    'DW_FORM_block': _decode_block,
+    'DW_FORM_block1': _decode_block,
+    'DW_FORM_block2': _decode_block,
+    'DW_FORM_block4': _decode_block,
+    'DW_FORM_block8': _decode_block,
+    'DW_FORM_block8': _decode_block,
+    'DW_FORM_exprloc': _decode_exprloc,
+
+    # TODO: handle all existing forms
+}
diff --git a/gcc/testsuite/python/dwarfutils/scan_dwarf.py b/gcc/testsuite/python/dwarfutils/scan_dwarf.py
new file mode 100644
index 00000000000..43dd7e579f3
--- /dev/null
+++ b/gcc/testsuite/python/dwarfutils/scan_dwarf.py
@@ -0,0 +1,145 @@
+# Copyright (C) 2017 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with GCC; see the file COPYING3.  If not see
+# <http://www.gnu.org/licenses/>.
+
+# Helpers to implement the "scan-dwarf" check command (see gcc-dwarf.exp)
+
+import ast
+
+from dwarfutils.data import Matcher
+from testutils import check
+
+
+def expand_tabs(line):
+    """Expand tab characters in `line` into spaces."""
+    result = ''
+    for c in line:
+        if c == '\t':
+            column_increment = 8 - len(result) % 8
+            result += ' ' * column_increment
+        else:
+            result += c
+    return ''.join(result)
+
+
+def extract_matchers_from_file(source_file): # no-coverage
+    """
+    Wrapper around `extract_matchers` to read `source_file`
+
+    :param str source_file: Path to the source file to scan.
+    :rtype: list[(int, code-object)]
+    """
+    with open(source_file, 'r') as f:
+        return extract_matchers(source_file, f.read())
+
+
+
+def extract_matchers(source_file, lines):
+    """
+    Scan `source_file` to find the Python expressions located between ">>>"
+    lines.
+
+    For instance, if the source file is a C file that contains::
+
+        /* >>>
+           Matcher('DW_TAG_subprogram')
+           Matcher('DW_TAG_structure_type')
+           >>>
+
+    This will extract both calls to Matcher and turn them into compiled code
+    that can be executed with `eval`. The result is a list of couples: source line
+    number and code objects.
+
+    The source file is expected to contain exactly two ">>>" lines and ">>>"
+    parts must appear on the same column. Then, all the lines in between are
+    required to be a valid sequence of Python expressions, starting on the same
+    column and which evaluate to Matcher instances.
+
+    :param str source_file: Path to the source file to scan.
+    :param str lines: Source file content to scan.
+    :rtype: list[(int, code-object)]
+    """
+    result = []
+
+    lines = lines.splitlines()
+
+    # Make sure there are only two ">>>" lines in this source file
+    error = ValueError('{0} must contain exactly two lines that hold'
+                       ' ">>>"'.format(source_file))
+    column_no = None
+    opening = None
+    closing = None
+    for i, line in enumerate(lines):
+        if '>>>' in line:
+            line = expand_tabs(line)
+            if opening is None:
+                opening = i
+                column_no = line.find('>>>')
+            elif closing is None:
+                closing = i
+                if line.find('>>>') != column_no:
+                    raise ValueError('">>>" lines in {0} must appear on the'
+                                     ' same column'.format(source_file))
+            else:
+                raise error
+    if opening is None or closing is None:
+        raise error
+
+    # Now we have located the >>> pair, extract the code in between, removing
+    # indentation.
+    code_lines = '\n'.join(expand_tabs(line)[column_no:]
+                           for line in lines[opening + 1:closing])
+
+    # Parse the code chunk. This is supposed to return an ast.Module instance.
+    location = '{0}[DWARF matching code]'.format(source_file)
+    mod = ast.parse(code_lines, 'Matcher code in {0}'.format(source_file))
+
+    # And now compile each individual expression to build the result
+    for expr in mod.body:
+        lineno = opening + expr.lineno + 1
+        if not isinstance(expr, ast.Expr):
+            raise ValueError('{0}:{1}: expression expected'.format(
+                source_file, lineno
+            ))
+        code = compile(ast.Expression(expr.value), location, 'eval')
+        result.append((lineno, code))
+
+    return result
+
+
+def run_matchers(matchers, compilation_unit):
+    """
+    Run the given matchers against the DIE tree in the given compilation unit.
+
+    Check that all matchers succeed using `testutils.check`.
+
+    :param list[(int, code-object)] matchers: Matchers to run. This is meant to
+        be the result of `extract_matchers`.
+    """
+    for lineno, matcher_code in matchers:
+        def predicate(die):
+            try:
+                matcher = eval(matcher_code, {'Match': Matcher})
+            except Exception as exc:
+                raise RuntimeError('line {0}: {1}: {2}'.format(
+                    lineno, type(exc).__name__, str(exc)
+                ))
+            if not isinstance(matcher, Matcher):
+                raise RuntimeError('line {0}: got {1} but a Matcher instance'
+                                   ' was expected'.format(lineno,
+                                                          repr(matcher)))
+            return die.tree_matches(matcher).mismatch_reason is None
+        dies = compilation_unit.find(predicate, single=False)
+        check(dies, 'find DIE to match pattern at line {0}'.format(lineno))
diff --git a/gcc/testsuite/python/dwarfutils/test_data.py b/gcc/testsuite/python/dwarfutils/test_data.py
new file mode 100644
index 00000000000..be91507b337
--- /dev/null
+++ b/gcc/testsuite/python/dwarfutils/test_data.py
@@ -0,0 +1,486 @@
+import unittest
+
+from dwarfutils.data import (Abbrev, Attribute, Capture, CompilationUnit,
+                             Defer, DIE, Exprloc, Matcher)
+from test_testutils import assertStdout
+
+
+class Builder(object):
+    """Helper to populate CompileUnit instances.
+
+    First, create the CompilationUnit to populate:
+    >>> cu = CompilationUnit(offset=0, length=13, is_32bit=True, version=5,
+                             abbrevs=list_of_abbrevs, pointer_size=8)
+
+    Then create the builder itself to populate it:
+    >>> db = Builder(cu)
+
+    Populating is done calling the .build method, giving it a build recipe. For
+    instance, to only create a single root DIE:
+    >>> db.build(db.die(level=0, offset=0xc, abbrev_number=1))
+
+    A little more complex: create a tree of DIEs:
+    >>> db.build(db.die(level=0, offset=0xc, abbrev_number=1, children=[
+        db.die(level=1, offset=0xd, abbrev_number=2, children=[
+            db.die(level=3, offset=0xe, abbrev_number=3),
+        ]),
+        db.die(level=2, offset=0xf, abbrev_number=3),
+    ]))
+
+    The last bit is how to create attributes:
+    >>> db.build(db.die(level=0, offset=0xc, abbrev_number=1, attributes=[
+        db.attr('DW_AT_name', 'DW_FORM_string', 0xd, 'foo'),
+        db.attr('DW_AT_low_pc', 'DW_FORM_addr', 0x10, 0xdeadbeef),
+    ]))
+    """
+
+    class DIERecipe(object):
+        def __init__(self, level, offset, abbrev_number, attrs=[],
+                     children=[]):
+            self.level = level
+            self.offset = offset
+            self.abbrev_number = abbrev_number
+            self.attrs = attrs
+            self.children = children
+
+        def apply(self, builder):
+            die = DIE(builder.cu, self.level, self.offset, self.abbrev_number)
+            for attr_recipe in self.attrs:
+                attr_recipe.apply(builder, die)
+            for die_recipe in self.children:
+                die.add_child(die_recipe.apply(builder))
+            return die
+
+    class AttributeRecipe(object):
+        def __init__(self, name, form, offset, value):
+            self.name = name
+            self.form = form
+            self.offset = offset
+            self.value = value
+
+        def apply(self, builder, die):
+            die.add_attribute(self.name, self.form, self.offset, self.value)
+
+    def __init__(self, cu):
+        self.cu = cu
+
+    def build(self, die_recipe):
+        self.cu.set_root(die_recipe.apply(self))
+
+    def die(self, *args, **kwargs):
+        return self.DIERecipe(*args, **kwargs)
+
+    def attr(self, *args, **kwargs):
+        return self.AttributeRecipe(*args, **kwargs)
+
+    def die_ref(self, offset):
+        return Defer(lambda: self.cu.get(offset))
+
+
+class CompareMixin(object):
+    """Provide helpers to test the content of compilation units."""
+
+    def _check_types(self, first, second, expected_type):
+        """
+        Internal helper to check that `first` and `second` are two instances of
+        `expected_type`. Call `self.fail` if not.
+        """
+        type_name = expected_type.__name__
+        for obj, name in [(first, 'First'), (second, 'Second')]:
+            if not isinstance(obj, expected_type):
+                self.fail(self._formatMessage(
+                    None,
+                    '{0} is not a {1}: {2}'.format(name, type_name, obj)
+                ))
+
+    def _compare_seqs(self, first, second, where, comp):
+        """
+        Internal helper to compare two sequences with a given item comparator.
+
+        Call `self.fail` if both sequences don't have the same length.
+
+        :param list[T] first: First list.
+        :param list[T] second: Second list.
+        :param str where: Short string to describe where the lists come from,
+            for debug purposes.
+        :param (T, T) -> None: Function to compare two list items. It is up to
+            this function to call the appropriate assertion functions.
+        """
+        if len(first) != len(second):
+            self.fail(self._formatMessage(
+                None,
+                '{0}, first has {1} items, second has {2} ones'.format(
+                    where, len(first), len(second)
+                )
+            ))
+
+        for f, s in zip(first, second):
+            comp(f, s)
+
+    def assertAbbrevsEqual(self, first, second):
+        """Assert that two Abbrev lists are equivalent."""
+        self._compare_seqs(first, second, 'In abbrev list',
+                           self.assertAbbrevEqual)
+
+    def assertAbbrevEqual(self, first, second):
+        """Assert that two Abbrev instances are equivalent."""
+        self._check_types(first, second, Abbrev)
+
+        def info(abbrev):
+            return [('number', abbrev.number),
+                    ('tag', abbrev.tag),
+                    ('has_children', abbrev.has_children),
+                    ('attributes', abbrev.attributes)]
+        self.assertEqual(info(first), info(second))
+
+    def assertCUsEqual(self, first, second):
+        """Assert that two CompilationUnit lists are equivalent."""
+        self._compare_seqs(first, second, 'In compilation unit list',
+                           self.assertCUEqual)
+
+    def assertCUEqual(self, first, second):
+        """Assert that two CompilationUnit instances are equivalent."""
+        self._check_types(first, second, CompilationUnit)
+
+        def basic_info(cu):
+            return [('offset', cu.offset),
+                    ('length', cu.length),
+                    ('is_32bit', cu.is_32bit),
+                    ('version', cu.version),
+                    ('pointer_size', cu.pointer_size)]
+        self.assertEqual(basic_info(first), basic_info(second),
+                         'mismatching CU info')
+
+        self.assertAbbrevsEqual(first.abbrevs, second.abbrevs)
+        self.assertDIEEqual(first.root, second.root)
+
+    def assertDIEEqual(self, first, second):
+        """Assert that two DIE instances are equivalent."""
+        self._check_types(first, second, DIE)
+
+        def basic_info(die):
+            return [('level', die.level),
+                    ('offset', die.offset),
+                    ('abbrev_number', die.abbrev_number)]
+        self.assertEqual(basic_info(first), basic_info(second),
+                         'mismatching CU info')
+
+        self._compare_seqs(first.attributes, second.attributes,
+                           'In {0} attributes'.format(repr(second)),
+                           self.assertAttrEqual)
+
+        self._compare_seqs(first.children, second.children,
+                           'In {0} children'.format(repr(second)),
+                           self.assertDIEEqual)
+
+    def assertAttrEqual(self, first, second):
+        """Assert that two Attribute instances are equivalent."""
+        def info(attr):
+            # To avoid infinite recursion, only compare DIE offsets
+            value = attr.value
+            if isinstance(value, DIE):
+                compared_value = 'DIE at {0:#0x}'.format(value.offset)
+            elif isinstance(value, Exprloc):
+                compared_value = str(value)
+            else:
+                compared_value = value
+
+            return [('name', attr.name),
+                    ('form', attr.form),
+                    ('offset', attr.offset),
+                    ('value', compared_value)]
+        self.assertEqual(info(first), info(second))
+
+
+class DefaultFixtures(object):
+    """
+    Provide a CompilationUnit with complex data on which testcases can work.
+    """
+
+    def setUp(self):
+        self.cu = CompilationUnit(0, 13, True, 5, [
+            Abbrev(1, 'DW_TAG_compile_unit', True, []),
+            Abbrev(2, 'DW_TAG_base_type', True, [
+                ('DW_AT_name', 'DW_FORM_string'),
+                ('DW_AT_byte_size', 'DW_FORM_data1'),
+            ]),
+            Abbrev(3, 'DW_TAG_structure_type', True, [
+                ('DW_AT_name', 'DW_FORM_string'),
+                ('DW_AT_byte_size', 'DW_FORM_data1'),
+                ('DW_AT_byte_size', 'DW_FORM_data1'),
+            ]),
+            Abbrev(4, 'DW_TAG_member', False, [
+                ('DW_AT_name', 'DW_FORM_string'),
+                ('DW_AT_type', 'DW_FORM_ref4'),
+                ('DW_AT_data_member_location', 'DW_FORM_data1')
+            ]),
+            Abbrev(5, 'DW_TAG_member', False, [
+                ('DW_AT_name', 'DW_FORM_string'),
+                ('DW_AT_type', 'DW_FORM_ref4'),
+                ('DW_AT_data_member_location', 'DW_FORM_exprloc')
+            ]),
+        ], 8)
+        db = Builder(self.cu)
+
+        # DW_TAG_compile_unit
+        db.build(db.die(0, 0x1, 1, children=[
+
+            # DW_TAG_base_type
+            db.die(1, 0x2, 2, attrs=[
+                db.attr('DW_AT_name', 'DW_FORM_strp', 0x3, 'type1'),
+                db.attr('DW_AT_byte_size', 'DW_FORM_data1', 0x4, 2),
+            ]),
+
+            # DW_TAG_base_type
+            db.die(1, 0x5, 2, attrs=[
+                db.attr('DW_AT_name', 'DW_FORM_strp', 0x6, 'type2'),
+                db.attr('DW_AT_byte_size', 'DW_FORM_data1', 0x7, 4),
+            ]),
+
+            # DW_TAG_structure_type
+            db.die(
+                1, 0x8, 3,
+                attrs=[
+                    db.attr('DW_AT_name', 'DW_FORM_string', 0x9, 'struct'),
+                    db.attr('DW_AT_byte_size', 'DW_FORM_data1', 0xa, 1),
+                    db.attr('DW_AT_byte_size', 'DW_FORM_data1', 0xb, 2),
+                ],
+                children=[
+
+                    # DW_TAG_member
+                    db.die(2, 0xc, 4, attrs=[
+                        db.attr('DW_AT_name', 'DW_FORM_string', 0xd, 'm1'),
+                        db.attr('DW_AT_type', 'DW_FORM_ref4', 0xe,
+                                db.die_ref(0x2)),
+                        db.attr('DW_AT_data_member_location', 'DW_FORM_data1',
+                                0xf, 0),
+                    ]),
+
+                    # DW_TAG_member
+                    db.die(2, 0x10, 5, attrs=[
+                        db.attr('DW_AT_name', 'DW_FORM_string', 0x11, 'm2'),
+                        db.attr('DW_AT_type', 'DW_FORM_ref4', 0x12,
+                                db.die_ref(0x5)),
+                        db.attr('DW_AT_data_member_location', 'DW_FORM_data1',
+                                0x13, Exprloc(
+                                    [0x97, 0x94, 0x4, 0x23, 0x2, 0x34, 0x1e,
+                                     0x22],
+                                    [('DW_OP_push_object_address', ),
+                                     ('DW_OP_deref_size', 4),
+                                     ('DW_OP_plus_uconst', 2),
+                                     ('DW_OP_lit4', ),
+                                     ('DW_OP_mul', ),
+                                     ('DW_OP_plus', )])),
+                    ]),
+                ]
+            ),
+        ]))
+
+        self.cu_die = self.cu.get(0x1)
+        self.type1 = self.cu.get(0x2)
+        self.type2 = self.cu.get(0x5)
+        self.struct = self.cu.get(0x8)
+        self.m1 = self.cu.get(0xc)
+        self.m2 = self.cu.get(0x10)
+
+
+class AbbrevTest(unittest.TestCase):
+    def test_repr(self):
+        abbrev = Abbrev(13, 'DW_AT_subprogram', True)
+        self.assertEqual(repr(abbrev), '<Abbrev #13 DW_AT_subprogram>')
+
+
+class CompilationUnitTest(DefaultFixtures, unittest.TestCase):
+
+    def test_repr(self):
+        self.assertEqual(repr(self.cu), '<CompilationUnit at 0x0>')
+
+    def test_find_default(self):
+        self.assertEqual(
+            self.cu.find(single=False),
+            [self.cu_die, self.type1, self.type2, self.struct, self.m1,
+             self.m2]
+        )
+
+    def test_find_predicate(self):
+        self.assertEqual(
+            self.cu.find(
+                tag='DW_TAG_base_type',
+                predicate=lambda die:
+                          die.get_attr('DW_AT_byte_size').value == 4
+            ),
+            self.type2
+        )
+
+    def test_find_tag(self):
+        self.assertEqual(self.cu.find(tag='DW_TAG_base_type', single=False),
+                         [self.type1, self.type2])
+
+    def test_find_name(self):
+        self.assertEqual(self.cu.find(name='type1', single=False), [self.type1])
+
+    def test_find_recursive(self):
+        self.assertEqual(
+            self.cu.find(name='m1', recursive=False, single=False),
+            []
+        )
+        self.assertEqual(
+            self.cu.find(recursive=False, single=False),
+            [self.cu_die, self.type1, self.type2, self.struct]
+        )
+
+    def test_find_single_ok(self):
+        self.assertEqual(self.cu.find(name='m1'), self.m1)
+
+    def test_find_single_too_many(self):
+        self.assertRaises(ValueError, self.cu.find)
+
+    def test_find_single_none(self):
+        self.assertRaises(ValueError, self.cu.find, name='nosuchname')
+
+
+class DIETest(DefaultFixtures, unittest.TestCase):
+
+    def test_repr(self):
+        self.assertEqual(repr(self.struct),
+                         '<DW_TAG_structure_type "struct" at 0x8>')
+
+    def test_get_child_0(self):
+        self.assertEqual(self.struct.get_child(0), self.m1)
+
+    def test_get_child_minus1(self):
+        self.assertEqual(self.struct.get_child(-1), self.m2)
+
+    def test_get_child_out_of_bounds(self):
+        self.assertRaises(IndexError, self.struct.get_child, 2)
+
+    def test_get_attr(self):
+        self.assertEqual(
+            repr(self.struct.get_attr('DW_AT_name', single=False)),
+            '[<DW_AT_name at 0x9>]'
+        )
+
+    def test_get_attr_single_ok(self):
+        self.assertEqual(self.struct.get_attr('DW_AT_name').value, 'struct')
+
+    def test_get_attr_single_too_many(self):
+        self.assertRaises(KeyError, self.struct.get_attr, 'DW_AT_byte_size')
+
+    def test_get_attr_single_none(self):
+        self.assertRaises(KeyError, self.struct.get_attr, 'DW_AT_nosuchname')
+
+    def test_check_attr(self):
+        with assertStdout(self, [
+            'PASS: check attribute DW_AT_name of DW_TAG_base_type "type1"'
+        ]):
+            self.type1.check_attr('DW_AT_name', 'type1')
+
+    def test_tree_check(self):
+        with assertStdout(self, [
+            'PASS: check structure of DW_TAG_base_type "type1"'
+        ]):
+            self.type1.tree_check(Matcher(name='type1'))
+
+
+class MatcherTest(DefaultFixtures, unittest.TestCase):
+
+    def test_tag_pass(self):
+        m = Matcher(tag='DW_TAG_base_type').matches(self.type1)
+        self.assertEqual(m.mismatch_reason, None)
+
+    def test_tag_fail(self):
+        m = Matcher(tag='DW_TAG_subprogram').matches(self.type1)
+        self.assertEqual(m.mismatch_reason,
+                         'DW_TAG_base_type "type1" is expected to be a'
+                         ' DW_TAG_subprogram')
+
+    def test_name_pass(self):
+        m = Matcher(name='type1').matches(self.type1)
+        self.assertEqual(m.mismatch_reason, None)
+
+    def test_name_fail(self):
+        m = Matcher(name='type2').matches(self.type1)
+        self.assertEqual(m.mismatch_reason,
+                         'DW_TAG_base_type "type1" is expected to be called'
+                         ' "type2"')
+
+    def test_attr_value_pass(self):
+        m = Matcher(attrs={'DW_AT_name': 'type1'}).matches(self.type1)
+        self.assertEqual(m.mismatch_reason, None)
+
+    def test_attr_value_fail(self):
+        m = Matcher(attrs={'DW_AT_name': 'type2'}).matches(self.type1)
+        self.assertEqual(m.mismatch_reason,
+                         'DW_TAG_base_type "type1": DW_AT_name is type1,'
+                         ' expected to be type2')
+
+    def test_attr_missing_pass(self):
+        m = Matcher(attrs={'DW_AT_nosuchname': None}).matches(self.type1)
+        self.assertEqual(m.mismatch_reason, None)
+
+    def test_attr_missing_fail(self):
+        m = Matcher(attrs={'DW_AT_nosuchname': 1}).matches(self.type1)
+        self.assertEqual(m.mismatch_reason,
+                         'DW_TAG_base_type "type1" is missing a'
+                         ' DW_AT_nosuchname attribute')
+
+    def test_attr_present_fail(self):
+        m = Matcher(attrs={'DW_AT_byte_size': None}).matches(self.type1)
+        self.assertEqual(m.mismatch_reason,
+                         'DW_TAG_base_type "type1" has a DW_AT_byte_size'
+                         ' attribute, none expected')
+
+    def test_attr_capture(self):
+        m = Matcher(
+            attrs={'DW_AT_byte_size': Capture('bs')}
+        ).matches(self.type1)
+        self.assertEqual(m.capture('bs').value, 2)
+
+    def test_attr_exprloc(self):
+        m = Matcher(attrs={'DW_AT_data_member_location': [
+             ('DW_OP_push_object_address', ),
+             ('DW_OP_deref_size', 4),
+             ('DW_OP_plus_uconst', 2),
+             ('DW_OP_lit4', ),
+             ('DW_OP_mul', ),
+             ('DW_OP_plus', )]
+         }).matches(self.m2)
+        self.assertEqual(m.mismatch_reason, None)
+
+    def test_children_none(self):
+        m = Matcher(children=[]).matches(self.type1)
+        self.assertEqual(m.mismatch_reason, None)
+
+    def test_children_count_fail_1(self):
+        m = Matcher(children=[]).matches(self.struct)
+        self.assertEqual(m.mismatch_reason,
+                         'DW_TAG_structure_type "struct" has 2 children, 0'
+                         ' expected')
+
+    def test_children_count_fail_2(self):
+        m = Matcher(children=[Matcher(), Matcher()]).matches(self.type1)
+        self.assertEqual(m.mismatch_reason, 'DW_TAG_base_type "type1" has 0'
+                                            ' children, 2 expected')
+
+    def test_children_capture(self):
+        m = Matcher(
+            children=[Capture('m1'), Capture('m2')]
+        ).matches(self.struct)
+        self.assertEqual(m.mismatch_reason, None)
+        self.assertEqual(m.capture('m1'), self.m1)
+        self.assertEqual(m.capture('m2'), self.m2)
+
+    def test_children_mismatch(self):
+        m = Matcher(
+            children=[Matcher(tag='DW_TAG_member'),
+                      Matcher(tag='DW_TAG_subprogram')]
+        ).matches(self.struct)
+        self.assertEqual(m.mismatch_reason,
+                         'DW_TAG_member "m2" is expected to be a'
+                         ' DW_TAG_subprogram')
+
+    def test_capture(self):
+        m = Matcher(tag='DW_TAG_base_type',
+                    capture='type1').matches(self.type1)
+        self.assertEqual(m.mismatch_reason, None)
+        self.assertEqual(m.capture('type1'), self.type1)
diff --git a/gcc/testsuite/python/dwarfutils/test_objdump.py b/gcc/testsuite/python/dwarfutils/test_objdump.py
new file mode 100644
index 00000000000..426afaf67b2
--- /dev/null
+++ b/gcc/testsuite/python/dwarfutils/test_objdump.py
@@ -0,0 +1,215 @@
+import unittest
+
+from dwarfutils.data import Abbrev, CompilationUnit, Exprloc
+import dwarfutils.objdump
+
+from dwarfutils.test_data import Builder, CompareMixin
+
+
+class ObjdumpMock(object):
+    def __init__(self, info=None, abbrev=None):
+        self.info = self._splitinput(info)
+        self.abbrev = self._splitinput(abbrev)
+
+    def get_info(self):
+        return self.info
+
+    def get_abbrev(self):
+        return self.abbrev
+
+    def _splitinput(self, string):
+        if string is None:
+            return []
+
+        result = string.rstrip().splitlines()
+
+        if not result[0].strip():
+            result.pop(0)
+
+        base_indent = len(result[0]) - len(result[0].lstrip())
+        for i in range(len(result)):
+            assert not result[i][:base_indent].strip()
+            result[i] = result[i][base_indent:]
+        return result
+
+
+def parse_abbrevs(abbrev):
+    return dwarfutils.objdump._parse_abbrevs(ObjdumpMock(info=None,
+                                                         abbrev=abbrev))
+
+
+def parse_info(info, abbrev):
+    return dwarfutils.objdump._parse_dwarf(ObjdumpMock(info=info,
+                                                       abbrev=abbrev))
+
+
+class ParseAbbrevTest(unittest.TestCase, CompareMixin):
+    def test_parse_abbrevs(self):
+        self.assertAbbrevsEqual(
+            parse_abbrevs("""
+            Contents of the .debug_abbrev section:
+
+              Number TAG (0x0)
+               1      DW_TAG_compile_unit    [has children]
+                DW_AT_producer     DW_FORM_strp
+                DW_AT_language     DW_FORM_data1
+                DW_AT value: 0     DW_FORM value: 0
+               2      DW_TAG_base_type    [no children]
+                DW_AT_byte_size    DW_FORM_data1
+                DW_AT_encoding     DW_FORM_data1
+                DW_AT value: 0     DW_FORM value: 0
+            """),
+            [Abbrev(1, 'DW_TAG_compile_unit', True, [
+                ('DW_AT_producer', 'DW_FORM_strp'),
+                ('DW_AT_language', 'DW_FORM_data1'),
+             ]),
+             Abbrev(2, 'DW_TAG_base_type', False, [
+                ('DW_AT_byte_size', 'DW_FORM_data1'),
+                ('DW_AT_encoding', 'DW_FORM_data1')])]
+        )
+
+
+class ParseInfoTest(unittest.TestCase, CompareMixin):
+    def test_parse_info(self):
+        expected_abbrevs = [
+            Abbrev(1, 'DW_TAG_compile_unit', True, [
+                ('DW_AT_producer', 'DW_FORM_strp'),
+                ('DW_AT_language', 'DW_FORM_data1'),
+                ('DW_AT_name', 'DW_FORM_strp'),
+                ('DW_AT_comp_dir', 'DW_FORM_strp'),
+                ('DW_AT_low_pc', 'DW_FORM_addr'),
+                ('DW_AT_high_pc', 'DW_FORM_data8'),
+                ('DW_AT_stmt_list', 'DW_FORM_sec_offset'),
+            ]),
+            Abbrev(2, 'DW_TAG_subprogram', False, [
+                ('DW_AT_external', 'DW_FORM_flag_present'),
+                ('DW_AT_name', 'DW_FORM_string'),
+                ('DW_AT_decl_file', 'DW_FORM_data1'),
+                ('DW_AT_decl_line', 'DW_FORM_data1'),
+                ('DW_AT_prototyped', 'DW_FORM_flag_present'),
+                ('DW_AT_type', 'DW_FORM_ref4'),
+                ('DW_AT_low_pc', 'DW_FORM_addr'),
+                ('DW_AT_high_pc', 'DW_FORM_data8'),
+                ('DW_AT_frame_base', 'DW_FORM_exprloc'),
+                ('DW_AT_GNU_all_call_sites', 'DW_FORM_flag_present'),
+            ]),
+            Abbrev(3, 'DW_TAG_base_type', False, [
+                ('DW_AT_byte_size', 'DW_FORM_data1'),
+                ('DW_AT_encoding', 'DW_FORM_data1'),
+                ('DW_AT_name', 'DW_FORM_string'),
+            ]),
+        ]
+        expected_cus = [CompilationUnit(0, 0x4e, True, 4, expected_abbrevs, 8)]
+        db = Builder(expected_cus[0])
+
+        db.build(db.die(
+            0, 0xb, 1,
+            attrs=[db.attr('DW_AT_producer', 'DW_FORM_strp', 0xc,
+                           'GNU C11 7.1.1 20170630 -mtune=generic'
+                           ' -march=x86-64 -g'),
+                   db.attr('DW_AT_language', 'DW_FORM_data1', 0x10,
+                           'ANSI C99'),
+                   db.attr('DW_AT_name', 'DW_FORM_strp', 0x11, 'foo.c'),
+                   db.attr('DW_AT_comp_dir', 'DW_FORM_strp', 0x15, '/tmp/foo'),
+                   db.attr('DW_AT_low_pc', 'DW_FORM_addr', 0x19, 0x0),
+                   db.attr('DW_AT_high_pc', 'DW_FORM_data8', 0x21, 0xb),
+                   db.attr('DW_AT_stmt_list', 'DW_FORM_sec_offset', 0x29,
+                           0x0)],
+            children=[
+                db.die(1, 0x2d, 2, attrs=[
+                    db.attr('DW_AT_external', 'DW_FORM_flag_present', 0x2e,
+                            True),
+                    db.attr('DW_AT_name', 'DW_FORM_string', 0x2e, 'foo'),
+                    db.attr('DW_AT_decl_file', 'DW_FORM_data1', 0x32, 1),
+                    db.attr('DW_AT_decl_line', 'DW_FORM_data1', 0x33, 1),
+                    db.attr('DW_AT_prototyped', 'DW_FORM_flag_present', 0x34,
+                            True),
+                    db.attr('DW_AT_type', 'DW_FORM_ref4', 0x34,
+                            db.die_ref(0x4a)),
+                    db.attr('DW_AT_low_pc', 'DW_FORM_addr', 0x38, 0x0),
+                    db.attr('DW_AT_high_pc', 'DW_FORM_data8', 0x40, 0xb),
+                    db.attr('DW_AT_frame_base', 'DW_FORM_exprloc', 0x48,
+                            Exprloc([0x9c], [('DW_OP_call_frame_cfa', )])),
+                    db.attr('DW_AT_GNU_all_call_sites', 'DW_FORM_flag_present',
+                            0x4a, True),
+                ]),
+                db.die(1, 0x4a, 3, attrs=[
+                    db.attr('DW_AT_byte_size', 'DW_FORM_data1', 0x4b, 4),
+                    db.attr('DW_AT_encoding', 'DW_FORM_data1', 0x4c,
+                            'signed'),
+                    db.attr('DW_AT_name', 'DW_FORM_string', 0x4d, 'int'),
+                ])
+            ]),
+        )
+
+        self.assertCUsEqual(
+            parse_info("""
+            foo.o:     file format elf64-x86-64
+
+            Contents of the .debug_info section:
+
+              Compilation Unit @ offset 0x0:
+               Length:        0x4e (32-bit)
+               Version:       4
+               Abbrev Offset: 0x0
+               Pointer Size:  8
+             <0><b>: Abbrev Number: 1 (DW_TAG_compile_unit)
+                <c>   DW_AT_producer    : (indirect string, offset: 0x24): GNU C11 7.1.1 20170630 -mtune=generic -march=x86-64 -g
+                <10>   DW_AT_language    : 12       (ANSI C99)
+                <11>   DW_AT_name        : (indirect string, offset: 0x1e): foo.c
+                <15>   DW_AT_comp_dir    : (indirect string, offset: 0x0): /tmp/foo
+                <19>   DW_AT_low_pc      : 0x0
+                <21>   DW_AT_high_pc     : 0xb
+                <29>   DW_AT_stmt_list   : 0x0
+             <1><2d>: Abbrev Number: 2 (DW_TAG_subprogram)
+                <2e>   DW_AT_external    : 1
+                <2e>   DW_AT_name        : foo
+                <32>   DW_AT_decl_file   : 1
+                <33>   DW_AT_decl_line   : 1
+                <34>   DW_AT_prototyped  : 1
+                <34>   DW_AT_type        : <0x4a>
+                <38>   DW_AT_low_pc      : 0x0
+                <40>   DW_AT_high_pc     : 0xb
+                <48>   DW_AT_frame_base  : 1 byte block: 9c         (DW_OP_call_frame_cfa)
+                <4a>   DW_AT_GNU_all_call_sites: 1
+             <1><4a>: Abbrev Number: 3 (DW_TAG_base_type)
+                <4b>   DW_AT_byte_size   : 4
+                <4c>   DW_AT_encoding    : 5        (signed)
+                <4d>   DW_AT_name        : int
+             <1><51>: Abbrev Number: 0
+            """,
+            """
+            foo.o:     file format elf64-x86-64
+
+            Contents of the .debug_abbrev section:
+
+              Number TAG (0x0)
+               1      DW_TAG_compile_unit    [has children]
+                DW_AT_producer     DW_FORM_strp
+                DW_AT_language     DW_FORM_data1
+                DW_AT_name         DW_FORM_strp
+                DW_AT_comp_dir     DW_FORM_strp
+                DW_AT_low_pc       DW_FORM_addr
+                DW_AT_high_pc      DW_FORM_data8
+                DW_AT_stmt_list    DW_FORM_sec_offset
+                DW_AT value: 0     DW_FORM value: 0
+               2      DW_TAG_subprogram    [no children]
+                DW_AT_external     DW_FORM_flag_present
+                DW_AT_name         DW_FORM_string
+                DW_AT_decl_file    DW_FORM_data1
+                DW_AT_decl_line    DW_FORM_data1
+                DW_AT_prototyped   DW_FORM_flag_present
+                DW_AT_type         DW_FORM_ref4
+                DW_AT_low_pc       DW_FORM_addr
+                DW_AT_high_pc      DW_FORM_data8
+                DW_AT_frame_base   DW_FORM_exprloc
+                DW_AT_GNU_all_call_sites DW_FORM_flag_present
+                DW_AT value: 0     DW_FORM value: 0
+               3      DW_TAG_base_type    [no children]
+                DW_AT_byte_size    DW_FORM_data1
+                DW_AT_encoding     DW_FORM_data1
+                DW_AT_name         DW_FORM_string
+                DW_AT value: 0     DW_FORM value: 0
+            """),
+            expected_cus
+        )
diff --git a/gcc/testsuite/python/dwarfutils/test_scan_dwarf.py b/gcc/testsuite/python/dwarfutils/test_scan_dwarf.py
new file mode 100644
index 00000000000..006269e9bb2
--- /dev/null
+++ b/gcc/testsuite/python/dwarfutils/test_scan_dwarf.py
@@ -0,0 +1,167 @@
+import unittest
+
+from dwarfutils.scan_dwarf import expand_tabs, extract_matchers, run_matchers
+from dwarfutils.test_data import DefaultFixtures
+from test_testutils import assertStdout
+
+
+class ComplexAssertFixture(object):
+
+    def assertRaisesMessage(self, expected, callable, *args, **kwargs):
+        try:
+            callable(*args, **kwargs)
+            exc = None
+        except Exception as exc:
+            exc = exc
+
+        if exc is None:
+            self.fail('No exception raised')
+        else:
+            self.assertEqual(repr(exc), repr(expected))
+
+
+class ExpandTabsTest(unittest.TestCase):
+
+    def test_empty(self):
+        self.assertEqual(expand_tabs(''), '')
+
+    def test_notab(self):
+        self.assertEqual(expand_tabs('hello world'), 'hello world')
+
+    def test_tab_first(self):
+        self.assertEqual(expand_tabs('\thello world'),
+                         '        hello world')
+
+    def test_tab_middle(self):
+        self.assertEqual(expand_tabs('\thello\tworld'),
+                         '        hello   world')
+
+
+class ExtractMatchersTest(ComplexAssertFixture, unittest.TestCase):
+
+    def test_empty(self):
+        self.assertRaisesMessage(
+            ValueError('src.c must contain exactly two lines that hold ">>>"'),
+            extract_matchers, 'src.c', ''
+        )
+
+    def test_one_bound(self):
+        self.assertRaisesMessage(
+            ValueError('src.c must contain exactly two lines that hold ">>>"'),
+            extract_matchers, 'src.c', '/* >>> */'
+        )
+
+    def test_three_bounds(self):
+        self.assertRaisesMessage(
+            ValueError('src.c must contain exactly two lines that hold ">>>"'),
+            extract_matchers, 'src.c',
+            """
+            /* >>>
+               >>>
+               >>> */
+            """
+        )
+
+    def test_bad_alignment(self):
+        self.assertRaisesMessage(
+            ValueError('">>>" lines in src.c must appear on the same column'),
+            extract_matchers, 'src.c',
+            """
+            /* >>>
+              >>> */
+            """
+        )
+
+    def test_syntax_error(self):
+        self.assertRaises(SyntaxError, extract_matchers, 'src.c',
+            """
+            /* >>>
+               <doesnotparse>
+               >>> */
+            """
+        )
+
+    def test_not_expr(self):
+        self.assertRaisesMessage(
+            ValueError('src.c:3: expression expected'),
+            extract_matchers, 'src.c',
+            """
+            /* >>>
+               import foo
+               >>> */
+            """
+        )
+
+    def test_one_matcher(self):
+        matchers = extract_matchers('src.c',
+            """
+            /* >>>
+               Matcher('DW_TAG_subprogram')
+               >>> */
+            """
+        )
+        line_numbers, code_objects = zip(*matchers)
+        self.assertEqual(list(line_numbers), [3])
+
+    def test_several_matcher(self):
+        matchers = extract_matchers('src.c',
+            """
+            /* >>>
+               Matcher('DW_TAG_subprogram')
+               Matcher('DW_TAG_array_type',
+                       attrs={})
+               Matcher('DW_TAG_structure_type')
+               >>> */
+            """
+        )
+        line_numbers, code_objects = zip(*matchers)
+        self.assertEqual(list(line_numbers), [3, 4, 6])
+
+
+class RunMatchersTest(ComplexAssertFixture, DefaultFixtures,
+                      unittest.TestCase):
+
+    def test_check_ok(self):
+        matchers = extract_matchers('src.c',
+            """
+            /* >>>
+               Matcher('DW_TAG_subprogram')
+               Matcher('DW_TAG_array_type',
+                       attrs={})
+               Matcher('DW_TAG_structure_type')
+               >>> */
+            """
+        )
+        with assertStdout(self, [
+            'FAIL: find DIE to match pattern at line 3',
+            'FAIL: find DIE to match pattern at line 4',
+            'PASS: find DIE to match pattern at line 6',
+        ]):
+            run_matchers(matchers, self.cu)
+
+    def test_eval_error(self):
+        matchers = extract_matchers('src.c',
+            """
+            /* >>>
+               Matcher(error)
+               >>> */
+            """
+        )
+        self.assertRaisesMessage(
+            RuntimeError("line 3: NameError: name 'error' is not defined"),
+            run_matchers, matchers, self.cu
+        )
+
+    def test_eval_not_matcher(self):
+        matchers = extract_matchers('src.c',
+            """
+            /* >>>
+               'hello'
+               >>> */
+            """
+        )
+        self.assertRaisesMessage(
+            RuntimeError("line 3: got 'hello' but a Matcher instance was"
+                         " expected"),
+            run_matchers, matchers, self.cu
+        )
diff --git a/gcc/testsuite/python/runtests.py b/gcc/testsuite/python/runtests.py
new file mode 100755
index 00000000000..e3f243debd3
--- /dev/null
+++ b/gcc/testsuite/python/runtests.py
@@ -0,0 +1,57 @@
+#! /usr/bin/env python
+
+# Entry point for the Python support material testsuite
+
+import os
+import os.path
+import sys
+import unittest
+
+# If we can compute code coverage, do it
+try:
+    import coverage
+except ImportError:
+    coverage = None
+    print('Could not import the "coverage" Python package: tests will run but'
+          ' no code coverage report will be generated')
+
+
+# Define environment variables so that we can import the modules to test, even
+# though their actual value don't matter for testing.
+os.environ['DWARF_DUMP_TOOL_KIND'] = 'objdump'
+os.environ['DWARF_DUMP_TOOL'] = 'objdump'
+
+
+test_modules = [
+    'dwarfutils.test_data',
+    'dwarfutils.test_objdump',
+    'dwarfutils.test_scan_dwarf',
+    'test_testutils',
+]
+
+
+if coverage:
+    cov = coverage.Coverage(
+        branch=True,
+        omit=['runtests.py',
+              '*/test_*',
+              '*DWARF matching code*'],
+    )
+    cov.exclude('# no-coverage')
+    cov.start()
+
+# In Python 2.6, unittest.main does not support the "exit=False" argument.
+# Workaround this.
+saved_exit = sys.exit
+sys.exit = lambda _: None
+try:
+    u = unittest.main(module=None, argv=[sys.argv[0]] + test_modules)
+finally:
+    sys.exit = saved_exit
+
+if coverage:
+    directory = 'htmlcov'
+    index = os.path.join(os.getcwd(), directory, 'index.html')
+    cov.stop()
+    cov.html_report()
+    print('Code coverage report generated at {0}'.format(index))
diff --git a/gcc/testsuite/python/scan-dwarf.py b/gcc/testsuite/python/scan-dwarf.py
new file mode 100755
index 00000000000..f4b533758de
--- /dev/null
+++ b/gcc/testsuite/python/scan-dwarf.py
@@ -0,0 +1,27 @@
+# Copyright (C) 2017 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with GCC; see the file COPYING3.  If not see
+# <http://www.gnu.org/licenses/>.
+
+# Python entry point for the "scan-dwarf" check command (see gcc-dwarf.exp)
+
+from dwarfutils import parse_dwarf
+from dwarfutils.scan_dwarf import extract_matchers_from_file, run_matchers
+import testutils
+
+
+args = testutils.parse_args()
+matchers = extract_matchers_from_file(args.source_file)
+cu = parse_dwarf(args.object_file)
+run_matchers(matchers, cu)
diff --git a/gcc/testsuite/python/test_testutils.py b/gcc/testsuite/python/test_testutils.py
new file mode 100644
index 00000000000..e82e6b66cef
--- /dev/null
+++ b/gcc/testsuite/python/test_testutils.py
@@ -0,0 +1,37 @@
+import contextlib
+import sys
+import unittest
+
+from testutils import check
+
+
+@contextlib.contextmanager
+def assertStdout(testcase, expected_lines):
+
+    class StubFile(object):
+        def __init__(self):
+            self.content = ''
+
+        def write(self, content):
+            self.content += content
+
+    lines = []
+    saved_stdout = sys.stdout
+    stub_stdout = sys.stdout = StubFile()
+
+    yield
+
+    sys.stdout = saved_stdout
+    lines = stub_stdout.content.splitlines()
+    testcase.assertEqual(lines, expected_lines)
+
+
+class CheckTest(unittest.TestCase):
+
+    def test_check_pass(self):
+        with assertStdout(self, ['PASS: foo']):
+            check(True, 'foo')
+
+    def test_check_fail(self):
+        with assertStdout(self, ['FAIL: foo']):
+            check(False, 'foo')
-- 
2.13.4

