← Back to team overview

kicad-developers team mailing list archive

Eeschema unit testing

 

Hi,

I have here a couple of patches that enable unit tests in eeschema's
library code. Previously, when I got the eeschema tests "working", it
turned out only the APIEXPORT'd functions were testable. This was the
grand total of one function (SCH_IO_MGR::FindPlugin).

Because we build the kiface shared lib with -fvisibility=hidden,
that's how it is, unless we manually export most of the eeschema
library for the purpose of unit testing only (which kind of defeats
the point) or we build the whole thing twice, once with full
visibility and once with hidden.

The other option, which I did here, is to do what pcbnew does and make
the library code an object library. This is not really ideal either,
since it inflates link time and memory consumption. Until CMake 3.9
(i.e. the far future), there are other annoyances with these libraries
too.

Probably the ideal way in the longer term is to move to a suite of
smaller, self-contained libraries (e.g. s-expression, netlist,
connectivity, geometry, schematic, etc), and put *those* interfaces
under test. Then the kifaces link those libraries and presents it
restricted interface. Due to the immense effort of doing that, I don't
suggest we put off being able to test *any* eeschema code until then.

So, this is a proposal to merge the changes attached, at the cost of
another heavy link. However, having a working eeschema test suite will
allow such things as eeschema's netlisting functions to get some
tests, so I think it's a valuable tool.

In related news, we now have XML test reporting, so Jenkins can report
the tests with full granularity, including the Python tests. E.g. [1].
In principle, any QA process can be run here, as long as it outputs
some kind of j/xUnit test report that Jenkins understands.

Cheers,

John

[1]: https://jenkins.simonrichter.eu/job/linux-kicad-head/lastCompletedBuild/testReport/
From 8586df1dc314ed797debd943137eac04b3f7d519 Mon Sep 17 00:00:00 2001
From: John Beard <john.j.beard@xxxxxxxxx>
Date: Tue, 23 Apr 2019 08:58:17 +0100
Subject: [PATCH 1/4] Eeschema: build with object libraries

This is done to allow access to the eeschema library
internals for purposes of test and script access, as the
DLL library has highly restrictive -fvisibility settings
that otherwise prevent the tests being able to access 99.9%
of the eeschema library functions (only a single function
is APIEXPORT'ed, therefore that's the only test we can do).

Using object libraries is a bit of a hack, and makes for
a slower link when done for multiple targets, but with the currently
supported CMake versions, it's about as good as we can get.

A better solution in the longer term may be to break eeschema_kiface(_objects)
into many smaller libraries, each of which has a much more defined scope,
rather than one big interlinked amorphous lump. This has the advantage that
each module is testable in isolation, and we get better organisation of
inter-dependencies in the codebase.

Then, the kiface DLL will gather these sub-libs and present what
is needed on the visible DLL API. Thus, we get both a testable
suite of library functions, and a restricted kiface DLL interface.
---
 eeschema/CMakeLists.txt    | 22 +++++++++++++++-------
 qa/eeschema/CMakeLists.txt | 20 ++++++++++++--------
 2 files changed, 27 insertions(+), 15 deletions(-)

diff --git a/eeschema/CMakeLists.txt b/eeschema/CMakeLists.txt
index a6c238c85..43d196c7b 100644
--- a/eeschema/CMakeLists.txt
+++ b/eeschema/CMakeLists.txt
@@ -155,7 +155,6 @@ set( EESCHEMA_SRCS
     edit_bitmap.cpp
     edit_component_in_schematic.cpp
     edit_label.cpp
-    eeschema.cpp
     eeschema_config.cpp
     erc.cpp
     fields_grid_table.cpp
@@ -339,21 +338,30 @@ target_link_libraries( eeschema
     ${wxWidgets_LIBRARIES}
     )
 
-# the DSO (KIFACE) housing the main eeschema code:
-add_library( eeschema_kiface SHARED
+# the main Eeschema program, in DSO form.
+add_library( eeschema_kiface_objects OBJECT
     ${EESCHEMA_SRCS}
     ${EESCHEMA_COMMON_SRCS}
     )
+
+# CMake <3.9 can't link anything to object libraries,
+# but we only need include directories, as we will link the kiface MODULE
+target_include_directories( eeschema_kiface_objects PRIVATE
+   $<TARGET_PROPERTY:common,INCLUDE_DIRECTORIES>
+   $<TARGET_PROPERTY:legacy_gal,INCLUDE_DIRECTORIES>
+)
+
+add_library( eeschema_kiface MODULE
+    eeschema.cpp
+    $<TARGET_OBJECTS:eeschema_kiface_objects>
+    )
+
 target_link_libraries( eeschema_kiface
-    gal
     legacy_gal
     common
     ${wxWidgets_LIBRARIES}
     ${GDI_PLUS_LIBRARIES}
     )
-target_include_directories( eeschema_kiface PUBLIC
-    ${CMAKE_CURRENT_SOURCE_DIR}
-    )
 
 if( KICAD_SPICE )
     target_link_libraries( eeschema_kiface
diff --git a/qa/eeschema/CMakeLists.txt b/qa/eeschema/CMakeLists.txt
index 0e0a013aa..6880ab7b7 100644
--- a/qa/eeschema/CMakeLists.txt
+++ b/qa/eeschema/CMakeLists.txt
@@ -23,17 +23,11 @@
 
 
 include_directories( BEFORE ${INC_BEFORE} )
-include_directories( AFTER ${INC_AFTER} )
-
 
 add_executable( qa_eeschema
     # A single top to load the pcnew kiface
     # ../../common/single_top.cpp
 
-    # stuff from common due to...units?
-    ../../common/base_units.cpp
-    ../../common/eda_text.cpp
-
     # stuff from common which is needed...why?
     ../../common/colors.cpp
     ../../common/observable.cpp
@@ -44,18 +38,28 @@ add_executable( qa_eeschema
     test_module.cpp
 
     test_eagle_plugin.cpp
+
+    # Older CMakes cannot link OBJECT libraries
+    # https://cmake.org/pipermail/cmake/2013-November/056263.html
+    $<TARGET_OBJECTS:eeschema_kiface_objects>
 )
 
 target_link_libraries( qa_eeschema
-    eeschema_kiface
     common
-    gal
+    legacy_gal
     qa_utils
     unit_test_utils
     ${GDI_PLUS_LIBRARIES}
     ${Boost_LIBRARIES}
 )
 
+target_include_directories( qa_eeschema PUBLIC
+    # Paths for eeschema lib usage (should really be in eeschema/common
+    # target_include_directories and made PUBLIC)
+    ${CMAKE_SOURCE_DIR}/eeschema
+    ${INC_AFTER}
+)
+
 # Eeschema tests, so pretend to be eeschema (for units, etc)
 target_compile_definitions( qa_eeschema
     PUBLIC EESCHEMA
-- 
2.20.1

From a2961a7e4a9bf02a97af919379b087c882041320 Mon Sep 17 00:00:00 2001
From: John Beard <john.j.beard@xxxxxxxxx>
Date: Tue, 23 Apr 2019 09:18:15 +0100
Subject: [PATCH 2/4] QA eeschema: add some tests

This adds a few tests on:

* LIB_PART
* SCH_PIN
* SCH_SHEET
* SCH_SHEET_PATH

These tests exercise some of the basic code paths in these classes
and show some of the expected behaviours.

None of these tests are particularly ground-breaking, but they
provide a starting point to build out further tests, and to ensure
the already-covered behaviour is stable.

It does expose some places where SCH_SHEET could probably use const.
---
 Documentation/development/testing.md          |   9 +
 qa/eeschema/CMakeLists.txt                    |   8 +
 qa/eeschema/lib_field_test_utils.h            | 122 ++++++++++
 qa/eeschema/mocks_eeschema.cpp                | 141 +++++++++++
 qa/eeschema/test_lib_part.cpp                 | 206 ++++++++++++++++
 qa/eeschema/test_module.cpp                   |  20 +-
 qa/eeschema/test_sch_pin.cpp                  | 159 +++++++++++++
 qa/eeschema/test_sch_sheet.cpp                | 225 ++++++++++++++++++
 qa/eeschema/test_sch_sheet_path.cpp           | 137 +++++++++++
 qa/eeschema/timestamp_test_utils.cpp          |  82 +++++++
 qa/eeschema/timestamp_test_utils.h            |  82 +++++++
 qa/unit_test_utils/CMakeLists.txt             |   2 +
 .../include/unit_test_utils/unit_test_utils.h |  65 +++++
 .../include/unit_test_utils/wx_assert.h       |  67 ++++++
 qa/unit_test_utils/unit_test_utils.cpp        |  15 +-
 qa/unit_test_utils/wx_assert.cpp              |  52 ++++
 16 files changed, 1388 insertions(+), 4 deletions(-)
 create mode 100644 qa/eeschema/lib_field_test_utils.h
 create mode 100644 qa/eeschema/mocks_eeschema.cpp
 create mode 100644 qa/eeschema/test_lib_part.cpp
 create mode 100644 qa/eeschema/test_sch_pin.cpp
 create mode 100644 qa/eeschema/test_sch_sheet.cpp
 create mode 100644 qa/eeschema/test_sch_sheet_path.cpp
 create mode 100644 qa/eeschema/timestamp_test_utils.cpp
 create mode 100644 qa/eeschema/timestamp_test_utils.h
 create mode 100644 qa/unit_test_utils/include/unit_test_utils/wx_assert.h
 create mode 100644 qa/unit_test_utils/wx_assert.cpp

diff --git a/Documentation/development/testing.md b/Documentation/development/testing.md
index be51c05c5..49756b1c9 100644
--- a/Documentation/development/testing.md
+++ b/Documentation/development/testing.md
@@ -130,6 +130,15 @@ the triggering of a bug prior to fixing it. This is advantageous, not only from
 a "project history" perspective, but also to ensure that the test you write to
 catch the bug in question does, in fact, catch the bug in the first place.
 
+### Assertions {#test-assertions}
+
+It is possible to check for assertions in unit tests. When running the unit
+tests, `wxASSERT` calls are caught and re-thrown as exceptions. You can then use
+the `CHECK_WX_ASSERT` macro to check this is called in Debug builds. In Release
+builds, the check is not run, as `wxASSERT` is disabled in these builds.
+
+You can use this to ensure that code rejects invalid input correctly.
+
 ## Python modules {#python-tests}
 
 The Pcbnew Python modules have some test programs in the `qa` directory.
diff --git a/qa/eeschema/CMakeLists.txt b/qa/eeschema/CMakeLists.txt
index 6880ab7b7..ae411de6b 100644
--- a/qa/eeschema/CMakeLists.txt
+++ b/qa/eeschema/CMakeLists.txt
@@ -32,12 +32,20 @@ add_executable( qa_eeschema
     ../../common/colors.cpp
     ../../common/observable.cpp
 
+    # need the mock Pgm for many functions
+    mocks_eeschema.cpp
+
     eeschema_test_utils.cpp
+    timestamp_test_utils.cpp
 
     # The main test entry points
     test_module.cpp
 
     test_eagle_plugin.cpp
+    test_lib_part.cpp
+    test_sch_pin.cpp
+    test_sch_sheet.cpp
+    test_sch_sheet_path.cpp
 
     # Older CMakes cannot link OBJECT libraries
     # https://cmake.org/pipermail/cmake/2013-November/056263.html
diff --git a/qa/eeschema/lib_field_test_utils.h b/qa/eeschema/lib_field_test_utils.h
new file mode 100644
index 000000000..51288420f
--- /dev/null
+++ b/qa/eeschema/lib_field_test_utils.h
@@ -0,0 +1,122 @@
+/*
+ * This program source code file is part of KiCad, a free EDA CAD application.
+ *
+ * Copyright (C) 2019 KiCad Developers, see CHANGELOG.TXT for contributors.
+ *
+ * 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 2
+ * 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 this program; if not, you may find one here:
+ * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
+ * or you may search the http://www.gnu.org website for the version 2 license,
+ * or you may write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
+ */
+
+/**
+ * @file
+ * Test utils (e.g. print helpers and test predicates for LIB_FIELD objects
+ */
+
+#ifndef QA_EESCHEMA_LIB_FIELD_TEST_UTILS__H
+#define QA_EESCHEMA_LIB_FIELD_TEST_UTILS__H
+
+#include <unit_test_utils/unit_test_utils.h>
+
+#include <class_libentry.h>
+#include <template_fieldnames.h>
+
+
+BOOST_TEST_PRINT_NAMESPACE_OPEN
+{
+template <>
+struct print_log_value<LIB_FIELD>
+{
+    inline void operator()( std::ostream& os, LIB_FIELD const& f )
+    {
+        os << "LIB_FIELD[ " << f.GetName() << " ]";
+    }
+};
+
+template <>
+struct print_log_value<LIB_FIELDS>
+{
+    inline void operator()( std::ostream& os, LIB_FIELDS const& f )
+    {
+        os << "LIB_FIELDS[ " << f.size() << " ]";
+    }
+};
+}
+BOOST_TEST_PRINT_NAMESPACE_CLOSE
+
+
+namespace KI_TEST
+{
+
+/**
+ * Predicate to check a field name is as expected
+ * @param  aField    LIB_FIELD to check the name
+ * @param  aExpectedName the expected field name
+ * @param  aExpectedId the expected field id
+ * @return           true if match
+ */
+bool FieldNameIdMatches(
+        const LIB_FIELD& aField, const std::string& aExpectedName, int aExpectedId )
+{
+    bool       ok = true;
+    const auto gotName = aField.GetName( false );
+
+    if( gotName != aExpectedName )
+    {
+        BOOST_TEST_INFO(
+                "Field name mismatch: got '" << gotName << "', expected '" << aExpectedName );
+        ok = false;
+    }
+
+    const int gotId = aField.GetId();
+
+    if( gotId != aExpectedId )
+    {
+        BOOST_TEST_INFO( "Field ID mismatch: got '" << gotId << "', expected '" << aExpectedId );
+        ok = false;
+    }
+
+    return ok;
+}
+
+/**
+ * Predicate to check that the mandatory fields in a LIB_FIELDS object look sensible
+ * @param  aFields the fields to check
+ * @return         true if valid
+ */
+bool AreDefaultFieldsCorrect( const LIB_FIELDS& aFields )
+{
+    const unsigned expectedCount = NumFieldType::MANDATORY_FIELDS;
+    if( aFields.size() < expectedCount )
+    {
+        BOOST_TEST_INFO(
+                "Expected at least " << expectedCount << " fields, got " << aFields.size() );
+        return false;
+    }
+
+    bool ok = true;
+
+    ok &= FieldNameIdMatches( aFields[0], "Reference", NumFieldType::REFERENCE );
+    ok &= FieldNameIdMatches( aFields[1], "Value", NumFieldType::VALUE );
+    ok &= FieldNameIdMatches( aFields[2], "Footprint", NumFieldType::FOOTPRINT );
+    ok &= FieldNameIdMatches( aFields[3], "Datasheet", NumFieldType::DATASHEET );
+
+    return ok;
+}
+
+} // namespace KI_TEST
+
+#endif // QA_EESCHEMA_LIB_FIELD_TEST_UTILS__H
\ No newline at end of file
diff --git a/qa/eeschema/mocks_eeschema.cpp b/qa/eeschema/mocks_eeschema.cpp
new file mode 100644
index 000000000..4b21eadc9
--- /dev/null
+++ b/qa/eeschema/mocks_eeschema.cpp
@@ -0,0 +1,141 @@
+/*
+ * This program source code file is part of KiCad, a free EDA CAD application.
+ *
+ * Copyright (C) 2019 KiCad Developers, see CHANGELOG.TXT for contributors.
+ *
+ * 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 2
+ * 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 this program; if not, you may find one here:
+ * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
+ * or you may search the http://www.gnu.org website for the version 2 license,
+ * or you may write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
+ */
+
+#include <kiface_i.h>
+#include <pgm_base.h>
+
+#include <sch_edit_frame.h>
+
+// The main sheet of the project
+SCH_SHEET* g_RootSheet = nullptr;
+
+// a transform matrix, to display components in lib editor
+TRANSFORM DefaultTransform = TRANSFORM( 1, 0, 0, -1 );
+
+static struct IFACE : public KIFACE_I
+{
+    // Of course all are overloads, implementations of the KIFACE.
+
+    IFACE( const char* aName, KIWAY::FACE_T aType ) : KIFACE_I( aName, aType )
+    {
+    }
+
+    bool OnKifaceStart( PGM_BASE* aProgram, int aCtlBits ) override
+    {
+        return true;
+    }
+
+    void OnKifaceEnd() override
+    {
+    }
+
+    wxWindow* CreateWindow(
+            wxWindow* aParent, int aClassId, KIWAY* aKiway, int aCtlBits = 0 ) override
+    {
+        assert( false );
+        return nullptr;
+    }
+
+    /**
+     * Function IfaceOrAddress
+     * return a pointer to the requested object.  The safest way to use this
+     * is to retrieve a pointer to a static instance of an interface, similar to
+     * how the KIFACE interface is exported.  But if you know what you are doing
+     * use it to retrieve anything you want.
+     *
+     * @param aDataId identifies which object you want the address of.
+     *
+     * @return void* - and must be cast into the know type.
+     */
+    void* IfaceOrAddress( int aDataId ) override
+    {
+        return NULL;
+    }
+} kiface( "mock_eeschema", KIWAY::FACE_SCH );
+
+static struct PGM_MOCK_EESCHEMA_FRAME : public PGM_BASE
+{
+    bool OnPgmInit();
+
+    void OnPgmExit()
+    {
+        Kiway.OnKiwayEnd();
+
+        // Destroy everything in PGM_BASE, especially wxSingleInstanceCheckerImpl
+        // earlier than wxApp and earlier than static destruction would.
+        PGM_BASE::Destroy();
+    }
+
+    void MacOpenFile( const wxString& aFileName ) override
+    {
+        wxFileName filename( aFileName );
+
+        if( filename.FileExists() )
+        {
+#if 0
+            // this pulls in EDA_DRAW_FRAME type info, which we don't want in
+            // the single_top link image.
+            KIWAY_PLAYER* frame = dynamic_cast<KIWAY_PLAYER*>( App().GetTopWindow() );
+#else
+            KIWAY_PLAYER* frame = (KIWAY_PLAYER*) App().GetTopWindow();
+#endif
+
+            if( frame )
+                frame->OpenProjectFiles( std::vector<wxString>( 1, aFileName ) );
+        }
+    }
+} program;
+
+PGM_BASE& Pgm()
+{
+    return program;
+}
+
+
+KIFACE_I& Kiface()
+{
+    return kiface;
+}
+
+
+static COLOR4D s_layerColor[LAYER_ID_COUNT];
+
+COLOR4D GetLayerColor( SCH_LAYER_ID aLayer )
+{
+    unsigned layer = ( aLayer );
+    wxASSERT( layer < arrayDim( s_layerColor ) );
+    return s_layerColor[layer];
+}
+
+void SetLayerColor( COLOR4D aColor, SCH_LAYER_ID aLayer )
+{
+    // Do not allow non-background layers to be completely white.
+    // This ensures the BW printing recognizes that the colors should be
+    // printed black.
+    if( aColor == COLOR4D::WHITE && aLayer != LAYER_SCHEMATIC_BACKGROUND )
+        aColor.Darken( 0.01 );
+
+    unsigned layer = aLayer;
+    wxASSERT( layer < arrayDim( s_layerColor ) );
+    s_layerColor[layer] = aColor;
+}
\ No newline at end of file
diff --git a/qa/eeschema/test_lib_part.cpp b/qa/eeschema/test_lib_part.cpp
new file mode 100644
index 000000000..10ecd9532
--- /dev/null
+++ b/qa/eeschema/test_lib_part.cpp
@@ -0,0 +1,206 @@
+/*
+ * This program source code file is part of KiCad, a free EDA CAD application.
+ *
+ * Copyright (C) 2019 KiCad Developers, see CHANGELOG.TXT for contributors.
+ *
+ * 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 2
+ * 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 this program; if not, you may find one here:
+ * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
+ * or you may search the http://www.gnu.org website for the version 2 license,
+ * or you may write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
+ */
+
+/**
+ * @file
+ * Test suite for LIB_PART
+ */
+
+#include <unit_test_utils/unit_test_utils.h>
+
+// Code under test
+#include <class_libentry.h>
+
+#include "lib_field_test_utils.h"
+
+class TEST_LIB_PART_FIXTURE
+{
+public:
+    TEST_LIB_PART_FIXTURE() : m_part_no_data( "part_name", nullptr )
+    {
+    }
+
+    ///> Part with no extra data set
+    LIB_PART m_part_no_data;
+};
+
+
+/**
+ * Declare the test suite
+ */
+BOOST_FIXTURE_TEST_SUITE( LibPart, TEST_LIB_PART_FIXTURE )
+
+
+/**
+ * Check that we can get the basic properties out as expected
+ */
+BOOST_AUTO_TEST_CASE( DefaultProperties )
+{
+    BOOST_CHECK_EQUAL( m_part_no_data.GetName(), "part_name" );
+
+    // Didn't set a library, so this is empty
+    BOOST_CHECK_EQUAL( m_part_no_data.GetLibraryName(), "" );
+    BOOST_CHECK_EQUAL( m_part_no_data.GetLib(), nullptr );
+
+    // only get the root
+    BOOST_CHECK_EQUAL( m_part_no_data.GetAliasCount(), 1 );
+
+    // no sub units
+    BOOST_CHECK_EQUAL( m_part_no_data.GetUnitCount(), 1 );
+    BOOST_CHECK_EQUAL( m_part_no_data.IsMulti(), false );
+
+    // no conversion
+    BOOST_CHECK_EQUAL( m_part_no_data.HasConversion(), false );
+}
+
+
+/**
+ * Check the drawings on a "blank" LIB_PART
+ */
+BOOST_AUTO_TEST_CASE( DefaultDrawings )
+{
+    // default drawings exist
+    BOOST_CHECK_EQUAL( m_part_no_data.GetDrawItems().size(), 4 );
+}
+
+
+/**
+ * Check the default fields are present as expected
+ */
+BOOST_AUTO_TEST_CASE( DefaultFields )
+{
+    LIB_FIELDS fields;
+    m_part_no_data.GetFields( fields );
+
+    // Should get the 4 default fields
+    BOOST_CHECK_PREDICATE( KI_TEST::AreDefaultFieldsCorrect, ( fields ) );
+
+    // but no more (we didn't set them)
+    BOOST_CHECK_EQUAL( fields.size(), NumFieldType::MANDATORY_FIELDS );
+
+    // also check the default field accessors
+    BOOST_CHECK_PREDICATE( KI_TEST::FieldNameIdMatches,
+            ( m_part_no_data.GetReferenceField() )( "Reference" )( NumFieldType::REFERENCE ) );
+    BOOST_CHECK_PREDICATE( KI_TEST::FieldNameIdMatches,
+            ( m_part_no_data.GetValueField() )( "Value" )( NumFieldType::VALUE ) );
+    BOOST_CHECK_PREDICATE( KI_TEST::FieldNameIdMatches,
+            ( m_part_no_data.GetFootprintField() )( "Footprint" )( NumFieldType::FOOTPRINT ) );
+}
+
+
+/**
+ * Test adding fields to a LIB_PART
+ */
+BOOST_AUTO_TEST_CASE( AddedFields )
+{
+    LIB_FIELDS fields;
+    m_part_no_data.GetFields( fields );
+
+    // Ctor takes non-const ref (?!)
+    const std::string newFieldName = "new_field";
+    wxString          nonConstNewFieldName = newFieldName;
+    fields.push_back( LIB_FIELD( 42, nonConstNewFieldName ) );
+
+    // fairly roundabout way to add a field, but it is what it is
+    m_part_no_data.SetFields( fields );
+
+    // Should get the 4 default fields
+    BOOST_CHECK_PREDICATE( KI_TEST::AreDefaultFieldsCorrect, ( fields ) );
+
+    // and our new one
+    BOOST_REQUIRE_EQUAL( fields.size(), NumFieldType::MANDATORY_FIELDS + 1 );
+
+    BOOST_CHECK_PREDICATE( KI_TEST::FieldNameIdMatches,
+            ( fields[NumFieldType::MANDATORY_FIELDS] )( newFieldName )( 42 ) );
+
+    // Check by-id lookup
+
+    LIB_FIELD* gotNewField = m_part_no_data.GetField( 42 );
+
+    BOOST_REQUIRE_NE( gotNewField, nullptr );
+
+    BOOST_CHECK_PREDICATE( KI_TEST::FieldNameIdMatches, ( *gotNewField )( newFieldName )( 42 ) );
+
+    // Check by-name lookup
+
+    gotNewField = m_part_no_data.FindField( newFieldName );
+
+    BOOST_REQUIRE_NE( gotNewField, nullptr );
+    BOOST_CHECK_PREDICATE( KI_TEST::FieldNameIdMatches, ( *gotNewField )( newFieldName )( 42 ) );
+}
+
+
+struct TEST_LIB_PART_SUBREF_CASE
+{
+    int         m_index;
+    bool        m_addSep;
+    std::string m_expSubRef;
+};
+
+
+/**
+ * Test the subreference indexing
+ */
+BOOST_AUTO_TEST_CASE( SubReference )
+{
+    const std::vector<TEST_LIB_PART_SUBREF_CASE> cases = {
+        {
+            1,
+            false,
+            "A",
+        },
+        {
+            2,
+            false,
+            "B",
+        },
+        {
+            26,
+            false,
+            "Z",
+        },
+        {
+            27,
+            false,
+            "AA",
+        },
+        { // haven't configured a separator, so should be nothing
+            1,
+            true,
+            "A",
+        },
+    };
+
+    for( const auto& c : cases )
+    {
+        BOOST_TEST_CONTEXT(
+                "Subref: " << c.m_index << ", " << c.m_addSep << " -> '" << c.m_expSubRef << "'" )
+        {
+            const auto subref = m_part_no_data.SubReference( c.m_index, c.m_addSep );
+            BOOST_CHECK_EQUAL( subref, c.m_expSubRef );
+        }
+    }
+}
+
+
+BOOST_AUTO_TEST_SUITE_END()
\ No newline at end of file
diff --git a/qa/eeschema/test_module.cpp b/qa/eeschema/test_module.cpp
index 5dbce91ca..732f88b7e 100644
--- a/qa/eeschema/test_module.cpp
+++ b/qa/eeschema/test_module.cpp
@@ -30,11 +30,29 @@
 
 #include <wx/init.h>
 
+#include <unit_test_utils/wx_assert.h>
+
+/*
+ * Simple function to handle a WX assertion and throw a real exception.
+ *
+ * This is useful when you want to check assertions fire in unit tests.
+ */
+void wxAssertThrower( const wxString& aFile, int aLine, const wxString& aFunc,
+        const wxString& aCond, const wxString& aMsg )
+{
+    throw KI_TEST::WX_ASSERT_ERROR( aFile, aLine, aFunc, aCond, aMsg );
+}
+
 
 bool init_unit_test()
 {
     boost::unit_test::framework::master_test_suite().p_name.value = "Common Eeschema module tests";
-    return wxInitialize();
+
+    bool ok = wxInitialize();
+
+    wxSetAssertHandler( &wxAssertThrower );
+
+    return ok;
 }
 
 
diff --git a/qa/eeschema/test_sch_pin.cpp b/qa/eeschema/test_sch_pin.cpp
new file mode 100644
index 000000000..ec4d5dbf2
--- /dev/null
+++ b/qa/eeschema/test_sch_pin.cpp
@@ -0,0 +1,159 @@
+/*
+ * This program source code file is part of KiCad, a free EDA CAD application.
+ *
+ * Copyright (C) 2019 KiCad Developers, see CHANGELOG.TXT for contributors.
+ *
+ * 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 2
+ * 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 this program; if not, you may find one here:
+ * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
+ * or you may search the http://www.gnu.org website for the version 2 license,
+ * or you may write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
+ */
+
+/**
+ * @file
+ * Test suite for SCH_PIM
+ */
+
+#include <unit_test_utils/unit_test_utils.h>
+#include <unit_test_utils/wx_assert.h>
+
+// Code under test
+#include <sch_pin.h>
+
+#include <sch_component.h>
+
+
+class TEST_SCH_PIN_FIXTURE
+{
+public:
+    TEST_SCH_PIN_FIXTURE()
+            : m_parent_part( "parent_part", nullptr ),
+              m_lib_pin( &m_parent_part ),
+              m_parent_comp( wxPoint( 0, 0 ), nullptr ),
+              m_sch_pin( &m_lib_pin, &m_parent_comp )
+    {
+        // give the pin some kind of data we can use to test
+        m_lib_pin.SetNumber( "42" );
+        m_lib_pin.SetName( "pinname" );
+        m_lib_pin.SetType( ELECTRICAL_PINTYPE::PIN_INPUT );
+
+        SCH_SHEET_PATH path;
+        m_parent_comp.SetRef( &path, "U2" );
+    }
+
+    LIB_PART m_parent_part;
+    LIB_PIN  m_lib_pin;
+
+    SCH_COMPONENT m_parent_comp;
+    SCH_PIN       m_sch_pin;
+};
+
+
+/**
+ * Declare the test suite
+ */
+BOOST_FIXTURE_TEST_SUITE( SchPin, TEST_SCH_PIN_FIXTURE )
+
+/**
+ * Check basic properties of an un-modified SCH_PIN object
+ */
+BOOST_AUTO_TEST_CASE( DefaultProperties )
+{
+    BOOST_CHECK_EQUAL( m_sch_pin.GetParentComponent(), &m_parent_comp );
+    BOOST_CHECK_EQUAL( m_sch_pin.GetLibPin(), &m_lib_pin );
+
+    BOOST_CHECK_EQUAL( m_sch_pin.GetPosition(), wxPoint( 0, 0 ) );
+
+    // the bbox should assert
+    CHECK_WX_ASSERT( m_sch_pin.GetBoundingBox() );
+
+    // These just forward to LIB_PIN for now, so this isn't very interesting
+    // but later we will want to test these functions for SCH_PIN's own functionality
+    BOOST_CHECK_EQUAL( m_sch_pin.IsVisible(), m_lib_pin.IsVisible() );
+    BOOST_CHECK_EQUAL( m_sch_pin.GetName(), m_lib_pin.GetName() );
+    BOOST_CHECK_EQUAL( m_sch_pin.GetNumber(), m_lib_pin.GetNumber() );
+    BOOST_CHECK_EQUAL( m_sch_pin.GetType(), m_lib_pin.GetType() );
+    BOOST_CHECK_EQUAL( m_sch_pin.IsPowerConnection(), m_lib_pin.IsPowerConnection() );
+}
+
+/**
+ * Check the assignment operator
+ */
+BOOST_AUTO_TEST_CASE( Assign )
+{
+    SCH_PIN assigned = m_sch_pin;
+
+    BOOST_CHECK_EQUAL( assigned.GetParentComponent(), &m_parent_comp );
+}
+
+/**
+ * Check the copy ctor
+ */
+BOOST_AUTO_TEST_CASE( Copy )
+{
+    SCH_PIN copied( m_sch_pin );
+
+    BOOST_CHECK_EQUAL( copied.GetParentComponent(), &m_parent_comp );
+}
+
+/**
+ * Check the pin dangling flag
+ */
+BOOST_AUTO_TEST_CASE( PinDangling )
+{
+    // dangles by default
+    BOOST_CHECK_EQUAL( m_sch_pin.IsDangling(), true );
+
+    // all you have to do to un-dangle is say so
+    m_sch_pin.SetIsDangling( false );
+    BOOST_CHECK_EQUAL( m_sch_pin.IsDangling(), false );
+
+    // and the same to re-dangle
+    m_sch_pin.SetIsDangling( true );
+    BOOST_CHECK_EQUAL( m_sch_pin.IsDangling(), true );
+}
+
+/**
+ * Check the pin labelling
+ */
+BOOST_AUTO_TEST_CASE( PinNumbering )
+{
+    SCH_SHEET_PATH path;
+
+    const wxString name = m_sch_pin.GetDefaultNetName( path );
+    BOOST_CHECK_EQUAL( name, "Net-(U2-Pad42)" );
+
+    // do it again: this should now (transparently) go though the net name map
+    // can't really check directly, but coverage tools should see this
+    const wxString map_name = m_sch_pin.GetDefaultNetName( path );
+    BOOST_CHECK_EQUAL( map_name, name );
+}
+
+/**
+ * Check the pin labelling when it's a power pin
+ */
+BOOST_AUTO_TEST_CASE( PinNumberingPower )
+{
+    // but if we set is power...
+    m_lib_pin.SetType( ELECTRICAL_PINTYPE::PIN_POWER_IN );
+    m_parent_part.SetPower();
+
+    // the name is just the pin name
+    SCH_SHEET_PATH path;
+    const wxString pwr_name = m_sch_pin.GetDefaultNetName( path );
+    BOOST_CHECK_EQUAL( pwr_name, "pinname" );
+}
+
+BOOST_AUTO_TEST_SUITE_END()
\ No newline at end of file
diff --git a/qa/eeschema/test_sch_sheet.cpp b/qa/eeschema/test_sch_sheet.cpp
new file mode 100644
index 000000000..706f2762b
--- /dev/null
+++ b/qa/eeschema/test_sch_sheet.cpp
@@ -0,0 +1,225 @@
+/*
+ * This program source code file is part of KiCad, a free EDA CAD application.
+ *
+ * Copyright (C) 2019 KiCad Developers, see CHANGELOG.TXT for contributors.
+ *
+ * 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 2
+ * 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 this program; if not, you may find one here:
+ * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
+ * or you may search the http://www.gnu.org website for the version 2 license,
+ * or you may write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
+ */
+
+/**
+ * @file
+ * Test suite for SCH_SHEET
+ */
+
+#include <unit_test_utils/unit_test_utils.h>
+
+// Code under test
+#include <sch_sheet.h>
+
+#include "timestamp_test_utils.h"
+
+#include <unit_test_utils/wx_assert.h>
+
+class TEST_SCH_SHEET_FIXTURE
+{
+public:
+    TEST_SCH_SHEET_FIXTURE() : m_sheet(), m_csheet( m_sheet )
+    {
+    }
+
+    SCH_SHEET m_sheet;
+
+    ///> Can use when you need a const ref (lots of places need fixing here)
+    const SCH_SHEET& m_csheet;
+};
+
+
+BOOST_TEST_PRINT_NAMESPACE_OPEN
+{
+template <>
+struct print_log_value<DANGLING_END_ITEM>
+{
+    inline void operator()( std::ostream& os, DANGLING_END_ITEM const& d )
+    {
+        os << "DANGLING_END_ITEM[ type " << d.GetType() << " @(" << d.GetPosition().x << ", "
+           << d.GetPosition().y << "), item " << d.GetItem() << ", parent " << d.GetParent() << " ]";
+    }
+};
+}
+BOOST_TEST_PRINT_NAMESPACE_CLOSE
+
+
+bool operator==( const DANGLING_END_ITEM& aA, const DANGLING_END_ITEM& aB )
+{
+    return aA.GetItem() == aB.GetItem()
+        && aA.GetPosition() == aB.GetPosition()
+        && aA.GetType() == aB.GetType()
+        && aA.GetParent() == aB.GetParent();
+}
+
+bool operator!=( const DANGLING_END_ITEM& aA, const DANGLING_END_ITEM& aB )
+{
+    return !( aA == aB );
+}
+
+/**
+ * Declare the test suite
+ */
+BOOST_FIXTURE_TEST_SUITE( SchSheet, TEST_SCH_SHEET_FIXTURE )
+
+
+/**
+ * Check default properties
+ */
+BOOST_AUTO_TEST_CASE( Default )
+{
+    BOOST_CHECK_EQUAL( m_csheet.GetPosition(), wxPoint( 0, 0 ) );
+
+    BOOST_CHECK_PREDICATE( KI_TEST::EndsInTimestamp, ( m_csheet.GetName().ToStdString() ) );
+
+    // it is it's own root sheet
+    BOOST_CHECK_EQUAL( m_sheet.GetRootSheet(), &m_sheet );
+    BOOST_CHECK_EQUAL( m_sheet.CountSheets(), 1 );
+
+    BOOST_CHECK_EQUAL( m_csheet.GetScreenCount(), 0 );
+
+    BOOST_CHECK_EQUAL( m_sheet.ComponentCount(), 0 );
+}
+
+/**
+ * Test adding pins to a sheet
+ */
+BOOST_AUTO_TEST_CASE( AddPins )
+{
+    // we should catch null insertions
+    CHECK_WX_ASSERT( m_sheet.AddPin( nullptr ) );
+
+    auto newPin = std::make_unique<SCH_SHEET_PIN>( &m_sheet, wxPoint( 42, 13 ), "pinname" );
+
+    // can't be const because of RemovePin (?!)
+    SCH_SHEET_PIN& pinRef = *newPin;
+
+    m_sheet.AddPin( newPin.release() );
+
+    // now we can find it in the list
+    BOOST_CHECK_EQUAL( m_sheet.HasPins(), true );
+    BOOST_CHECK_EQUAL( m_sheet.HasPin( "pinname" ), true );
+    BOOST_CHECK_EQUAL( m_sheet.HasPin( "PINname" ), true );
+
+    BOOST_CHECK_EQUAL( m_sheet.GetPin( wxPoint( 42, 13 ) ), &pinRef );
+
+    // check the actual list can be retrieved
+    // this should be const...
+    SCH_SHEET_PINS& pins = m_sheet.GetPins();
+    BOOST_CHECK_EQUAL( &pins[0], &pinRef );
+
+    // catch the bad call
+    CHECK_WX_ASSERT( m_sheet.RemovePin( nullptr ) );
+
+    m_sheet.RemovePin( &pinRef );
+
+    // and it's gone
+    BOOST_CHECK_EQUAL( m_sheet.HasPins(), false );
+    BOOST_CHECK_EQUAL( m_sheet.HasPin( "pinname" ), false );
+    BOOST_CHECK_EQUAL( m_sheet.GetPin( wxPoint( 42, 13 ) ), nullptr );
+}
+
+/**
+ * Check that pins are added and renumbered to be unique
+ */
+BOOST_AUTO_TEST_CASE( PinRenumbering )
+{
+    for( int i = 0; i < 5; ++i )
+    {
+        auto pin = std::make_unique<SCH_SHEET_PIN>( &m_sheet, wxPoint{ i, i }, "name" );
+
+        // set the pins to have the same number going in
+        pin->SetNumber( 2 );
+
+        m_sheet.AddPin( pin.release() );
+    }
+
+    SCH_SHEET_PINS& pins = m_sheet.GetPins();
+
+    std::vector<int> numbers;
+
+    for( const auto& pin : pins )
+    {
+        numbers.push_back( pin.GetNumber() );
+    }
+
+    // and now...they are all unique
+    BOOST_CHECK_PREDICATE( KI_TEST::CollectionHasNoDuplicates<decltype( numbers )>, ( numbers ) );
+}
+
+
+/**
+ * Test the endpoint and connection point collections: we should be able to add pins, then
+ * have them appear as endpoints.
+ */
+BOOST_AUTO_TEST_CASE( EndconnectionPoints )
+{
+    // x = zero because the pin is clamped to the left side by default
+    m_sheet.AddPin(
+            std::make_unique<SCH_SHEET_PIN>( &m_sheet, wxPoint{ 0, 13 }, "1name" ).release() );
+    m_sheet.AddPin(
+            std::make_unique<SCH_SHEET_PIN>( &m_sheet, wxPoint{ 0, 130 }, "2name" ).release() );
+
+    SCH_SHEET_PINS& pins = m_sheet.GetPins();
+
+    // make sure the pins made it in
+    BOOST_CHECK_EQUAL( pins.size(), 2 );
+
+    // Check that the EndPoint getter gets the right things
+    {
+        std::vector<DANGLING_END_ITEM> expectedDangling;
+        std::vector<wxPoint>           expectedConnections;
+
+        for( auto& pin : pins )
+        {
+            expectedDangling.emplace_back(
+                    DANGLING_END_T::SHEET_LABEL_END, &pin, pin.GetPosition(), &pin );
+        }
+
+        std::vector<DANGLING_END_ITEM> dangling;
+        m_sheet.GetEndPoints( dangling );
+
+        BOOST_CHECK_EQUAL_COLLECTIONS( dangling.begin(), dangling.end(), expectedDangling.begin(),
+                expectedDangling.end() );
+    }
+
+    // And check the connection getter
+    {
+        std::vector<wxPoint> expectedConnections;
+
+        // we want to see every pin that we just added
+        for( auto& pin : pins )
+        {
+            expectedConnections.push_back( pin.GetPosition() );
+        }
+
+        std::vector<wxPoint> connections;
+        m_sheet.GetConnectionPoints( connections );
+
+        BOOST_CHECK_EQUAL_COLLECTIONS( connections.begin(), connections.end(),
+                expectedConnections.begin(), expectedConnections.end() );
+    }
+}
+
+
+BOOST_AUTO_TEST_SUITE_END()
\ No newline at end of file
diff --git a/qa/eeschema/test_sch_sheet_path.cpp b/qa/eeschema/test_sch_sheet_path.cpp
new file mode 100644
index 000000000..fa2ecc7bf
--- /dev/null
+++ b/qa/eeschema/test_sch_sheet_path.cpp
@@ -0,0 +1,137 @@
+/*
+ * This program source code file is part of KiCad, a free EDA CAD application.
+ *
+ * Copyright (C) 2019 KiCad Developers, see CHANGELOG.TXT for contributors.
+ *
+ * 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 2
+ * 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 this program; if not, you may find one here:
+ * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
+ * or you may search the http://www.gnu.org website for the version 2 license,
+ * or you may write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
+ */
+
+/**
+ * @file
+ * Test suite for SCH_SHEET_PATH
+ */
+
+#include <unit_test_utils/unit_test_utils.h>
+
+// Code under test
+#include <sch_sheet_path.h>
+
+#include "timestamp_test_utils.h"
+
+#include <sch_sheet.h>
+
+#include <sstream>
+
+class TEST_SCH_SHEET_PATH_FIXTURE
+{
+public:
+    TEST_SCH_SHEET_PATH_FIXTURE()
+    {
+        for( unsigned i = 0; i < 4; ++i )
+        {
+            m_sheets.emplace_back( wxPoint( i, i ) );
+
+            std::ostringstream ss;
+            ss << "Sheet" << i;
+            m_sheets[i].SetName( ss.str() );
+        }
+
+        // 0->1->2
+        m_linear.push_back( &m_sheets[0] );
+        m_linear.push_back( &m_sheets[1] );
+        m_linear.push_back( &m_sheets[2] );
+    }
+
+    SCH_SHEET_PATH m_empty_path;
+
+    /**
+     * We look at sheet 2 in the hierarchy:
+     * Sheets: 0 -> 1 -> 2
+     */
+    SCH_SHEET_PATH m_linear;
+
+    /// handy store of SCH_SHEET objects
+    std::vector<SCH_SHEET> m_sheets;
+};
+
+
+/**
+ * Declare the test suite
+ */
+BOOST_FIXTURE_TEST_SUITE( SchSheetPath, TEST_SCH_SHEET_PATH_FIXTURE )
+
+
+/**
+ * Check properties of an empty SCH_SHEET_PATH
+ */
+BOOST_AUTO_TEST_CASE( Empty )
+{
+    BOOST_CHECK_EQUAL( m_empty_path.size(), 0 );
+
+    BOOST_CHECK_THROW( m_empty_path.at( 0 ), std::out_of_range );
+
+    BOOST_CHECK_EQUAL( m_empty_path.GetPageNumber(), 0 );
+
+    // These accessors return nullptr when empty (i.e. they don't crash)
+    BOOST_CHECK_EQUAL( m_empty_path.Last(), nullptr );
+    BOOST_CHECK_EQUAL( m_empty_path.LastScreen(), nullptr );
+    BOOST_CHECK_EQUAL( m_empty_path.LastDrawList(), nullptr );
+    BOOST_CHECK_EQUAL( m_empty_path.FirstDrawList(), nullptr );
+
+    BOOST_CHECK_EQUAL( m_empty_path.Path(), "/" );
+    BOOST_CHECK_EQUAL( m_empty_path.PathHumanReadable(), "/" );
+}
+
+
+/**
+ * Check properties of a non-empty SCH_SHEET_PATH
+ */
+BOOST_AUTO_TEST_CASE( NonEmpty )
+{
+    BOOST_CHECK_EQUAL( m_linear.size(), 3 );
+
+    BOOST_CHECK_EQUAL( m_linear.at( 0 ), &m_sheets[0] );
+    BOOST_CHECK_EQUAL( m_linear.at( 1 ), &m_sheets[1] );
+    BOOST_CHECK_EQUAL( m_linear.at( 2 ), &m_sheets[2] );
+
+    BOOST_CHECK_EQUAL( m_linear.GetPageNumber(), 0 );
+
+    BOOST_CHECK_EQUAL( m_linear.Last(), &m_sheets[2] );
+    BOOST_CHECK_EQUAL( m_linear.LastScreen(), nullptr );
+    BOOST_CHECK_EQUAL( m_linear.LastDrawList(), nullptr );
+    BOOST_CHECK_EQUAL( m_linear.FirstDrawList(), nullptr );
+
+    // don't know what the timestamps will be, but we know the format: /<8 chars>/<8 chars>/
+    BOOST_CHECK_PREDICATE(
+            KI_TEST::IsTimestampStringWithLevels, ( m_linear.Path().ToStdString() )( 2 ) );
+
+    // Sheet0 is the root sheet and isn't in the path
+    BOOST_CHECK_EQUAL( m_linear.PathHumanReadable(), "/Sheet1/Sheet2/" );
+}
+
+
+BOOST_AUTO_TEST_CASE( Compare )
+{
+    SCH_SHEET_PATH otherEmpty;
+
+    BOOST_CHECK( m_empty_path == otherEmpty );
+
+    BOOST_CHECK( m_empty_path != m_linear );
+}
+
+BOOST_AUTO_TEST_SUITE_END()
\ No newline at end of file
diff --git a/qa/eeschema/timestamp_test_utils.cpp b/qa/eeschema/timestamp_test_utils.cpp
new file mode 100644
index 000000000..05f16d725
--- /dev/null
+++ b/qa/eeschema/timestamp_test_utils.cpp
@@ -0,0 +1,82 @@
+/*
+ * This program source code file is part of KiCad, a free EDA CAD application.
+ *
+ * Copyright (C) 2019 KiCad Developers, see CHANGELOG.TXT for contributors.
+ *
+ * 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 2
+ * 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 this program; if not, you may find one here:
+ * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
+ * or you may search the http://www.gnu.org website for the version 2 license,
+ * or you may write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
+ */
+
+#include "timestamp_test_utils.h"
+
+#include <unit_test_utils/unit_test_utils.h>
+
+namespace KI_TEST
+{
+
+bool EndsInTimestamp( const std::string& aStr )
+{
+    if( aStr.size() < 8 )
+    {
+        BOOST_TEST_INFO( "Too short to be timestamp: " << aStr.size() );
+        return false;
+    }
+
+    return IsTimeStampish( aStr.end() - 8, aStr.end() );
+}
+
+bool IsTimestampStringWithLevels( const std::string& aStr, unsigned aLevels )
+{
+    const unsigned tsLen = 8;
+    const unsigned levelLen = tsLen + 1; // add the /
+
+    if( aStr.size() != aLevels * levelLen + 1 )
+    {
+        BOOST_TEST_INFO( "String is the wrong length for " << aLevels << " levels." );
+        return false;
+    }
+
+    if( aStr[0] != '/' )
+    {
+        BOOST_TEST_INFO( "Doesn't start with '/'" );
+        return false;
+    }
+
+    auto tsBegin = aStr.begin() + 1;
+
+    for( unsigned i = 0; i < aLevels; i++ )
+    {
+        if( !IsTimeStampish( tsBegin, tsBegin + tsLen ) )
+        {
+            BOOST_TEST_INFO( "Not a timeStamp at level "
+                             << i << ": " << std::string( tsBegin, tsBegin + tsLen ) );
+            return false;
+        }
+
+        if( *( tsBegin + tsLen ) != '/' )
+        {
+            BOOST_TEST_INFO( "level doesn't end in '/'" );
+            return false;
+        }
+
+        tsBegin += levelLen;
+    }
+
+    return true;
+}
+
+} // namespace KI_TEST
diff --git a/qa/eeschema/timestamp_test_utils.h b/qa/eeschema/timestamp_test_utils.h
new file mode 100644
index 000000000..519e4fd72
--- /dev/null
+++ b/qa/eeschema/timestamp_test_utils.h
@@ -0,0 +1,82 @@
+/*
+ * This program source code file is part of KiCad, a free EDA CAD application.
+ *
+ * Copyright (C) 2019 KiCad Developers, see CHANGELOG.TXT for contributors.
+ *
+ * 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 2
+ * 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 this program; if not, you may find one here:
+ * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
+ * or you may search the http://www.gnu.org website for the version 2 license,
+ * or you may write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
+ */
+
+#ifndef QA_EESCHEMA_TIMESTAMP_TEST_UTILS__H
+#define QA_EESCHEMA_TIMESTAMP_TEST_UTILS__H
+
+#include <algorithm>
+#include <string>
+
+/**
+ * @file
+ * Test utilities for timestamps
+ */
+
+namespace KI_TEST
+{
+
+/**
+ * Predicate for checking a timestamp character
+ * @param  aChr the character
+ * @return      true if it's a valid timestamp char (0-9, A-F)
+ */
+inline bool IsTimeStampChar( char aChr )
+{
+    return ( aChr >= 'A' && aChr <= 'F' ) || ( aChr >= '0' && aChr <= '9' );
+}
+
+/**
+ * Check if the string between the iterators looks like a timestamp (i.e. 8 hex digits)
+ */
+template <typename T>
+bool IsTimeStampish( const T& aBegin, const T& aEnd )
+{
+    // Wrong length
+    if( aEnd != aBegin + 8 )
+        return false;
+
+    // Check all chars
+    return std::all_of( aBegin, aEnd, IsTimeStampChar );
+}
+
+/**
+ * Predicate to check if a string look like it ends in a timestamp
+ * @param  aStr the string to check
+ * @return      true if it does
+ */
+bool EndsInTimestamp( const std::string& aStr );
+
+/**
+ * Predicate to check a string is a timestmap path format
+ *
+ * Eg. levels=2: /1234ABCD/9878DEFC/
+ *
+ * @param  aStr   candidate string
+ * @param  levels expected levels
+ * @return        true if format matches
+ */
+bool IsTimestampStringWithLevels( const std::string& aStr, unsigned aLevels );
+
+} // namespace KI_TEST
+
+#endif // QA_EESCHEMA_TIMESTAMP_TEST_UTILS__H
\ No newline at end of file
diff --git a/qa/unit_test_utils/CMakeLists.txt b/qa/unit_test_utils/CMakeLists.txt
index 806aa59d4..80ca2f9db 100644
--- a/qa/unit_test_utils/CMakeLists.txt
+++ b/qa/unit_test_utils/CMakeLists.txt
@@ -29,6 +29,7 @@ find_package( Boost COMPONENTS unit_test_framework filesystem system REQUIRED )
 
 set( SRCS
     unit_test_utils.cpp
+    wx_assert.cpp
 )
 
 add_library( unit_test_utils STATIC ${SRCS} )
@@ -37,6 +38,7 @@ target_link_libraries( unit_test_utils PUBLIC
     ${Boost_UNIT_TEST_FRAMEWORK_LIBRARY}
     ${Boost_FILESYSTEM_LIBRARY}
     ${Boost_SYSTEM_LIBRARY}
+    ${wxWidgets_LIBRARIES}
 )
 
 target_include_directories( unit_test_utils PUBLIC
diff --git a/qa/unit_test_utils/include/unit_test_utils/unit_test_utils.h b/qa/unit_test_utils/include/unit_test_utils/unit_test_utils.h
index 87c3f9073..1335197b0 100644
--- a/qa/unit_test_utils/include/unit_test_utils/unit_test_utils.h
+++ b/qa/unit_test_utils/include/unit_test_utils/unit_test_utils.h
@@ -27,9 +27,12 @@
 #include <boost/test/test_case_template.hpp>
 #include <boost/test/unit_test.hpp>
 
+#include <unit_test_utils/wx_assert.h>
+
 #include <functional>
 #include <set>
 
+#include <wx/gdicmn.h>
 /**
  * If HAVE_EXPECTED_FAILURES is defined, this means that
  * boost::unit_test::expected_failures is available.
@@ -130,6 +133,43 @@ BOOST_TEST_PRINT_NAMESPACE_CLOSE
 
 #endif
 
+
+BOOST_TEST_PRINT_NAMESPACE_OPEN
+{
+
+/**
+ * Boost print helper for generic vectors
+ */
+template <typename T>
+struct print_log_value<std::vector<T>>
+{
+    inline void operator()( std::ostream& os, std::vector<T> const& aVec )
+    {
+        os << "std::vector size " << aVec.size() << "[";
+
+        for( const auto& i : aVec )
+        {
+            os << "\n    ";
+            print_log_value<T>()( os, i );
+        }
+
+        os << "]";
+    }
+};
+
+/**
+ * Boost print helper for wxPoint. Note operator<< for this type doesn't
+ * exist in non-DEBUG builds.
+ */
+template <>
+struct print_log_value<wxPoint>
+{
+    void operator()( std::ostream& os, wxPoint const& aVec );
+};
+}
+BOOST_TEST_PRINT_NAMESPACE_CLOSE
+
+
 namespace KI_TEST
 {
 
@@ -231,6 +271,19 @@ void CheckUnorderedMatches(
 }
 
 
+/**
+ * Predicate to check a collection has no duplicate elements
+ */
+template <typename T>
+bool CollectionHasNoDuplicates( const T& aCollection )
+{
+    T sorted = aCollection;
+    std::sort( sorted.begin(), sorted.end() );
+
+    return std::adjacent_find( sorted.begin(), sorted.end() ) == sorted.end();
+}
+
+
 /**
  * Get a simple string "aIn -> aOut".
  *
@@ -251,6 +304,18 @@ std::string InOutString( const IN& aIn, const OUT& aOut )
     return ss.str();
 }
 
+/**
+ * A test macro to check a wxASSERT is thrown.
+ *
+ * This only happens in DEBUG builds, so prevent test failures in Release builds
+ * by using this macro.
+ */
+#ifdef DEBUG
+#define CHECK_WX_ASSERT( STATEMENT ) BOOST_CHECK_THROW( STATEMENT, KI_TEST::WX_ASSERT_ERROR );
+#else
+#define CHECK_WX_ASSERT( STATEMENT )
+#endif
+
 } // namespace KI_TEST
 
 #endif // UNIT_TEST_UTILS__H
\ No newline at end of file
diff --git a/qa/unit_test_utils/include/unit_test_utils/wx_assert.h b/qa/unit_test_utils/include/unit_test_utils/wx_assert.h
new file mode 100644
index 000000000..f25f5b395
--- /dev/null
+++ b/qa/unit_test_utils/include/unit_test_utils/wx_assert.h
@@ -0,0 +1,67 @@
+/*
+ * This program source code file is part of KiCad, a free EDA CAD application.
+ *
+ * Copyright (C) 2019 KiCad Developers, see CHANGELOG.TXT for contributors.
+ *
+ * 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 2
+ * 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 this program; if not, you may find one here:
+ * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
+ * or you may search the http://www.gnu.org website for the version 2 license,
+ * or you may write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
+ */
+
+#ifndef UNIT_TEST_UTILS_WX_ASSERT__H
+#define UNIT_TEST_UTILS_WX_ASSERT__H
+
+#include <wx/string.h>
+
+#include <exception>
+#include <string>
+
+namespace KI_TEST
+{
+
+/**
+ * An exception class to represent a WX assertion.
+ *
+ * In normal programs, this is popped as a dialog, but in unit tests, it
+ * prints a fairly unhelpful stack trace and otherwise doesn't inform the
+ * test runner.
+ *
+ * We want to raise a formal exception to allow us to catch it with
+ * things like BOOST_CHECK_THROW if expected, or for the test case to fail if
+ * not expected.
+ */
+class WX_ASSERT_ERROR : public std::exception
+{
+public:
+    WX_ASSERT_ERROR( const wxString& aFile, int aLine, const wxString& aFunc, const wxString& aCond,
+            const wxString& aMsg );
+
+    const char* what() const noexcept override;
+
+    // Public, so catchers can have a look (though be careful as the function
+    // names can change over time!)
+    std::string m_file;
+    int         m_line;
+    std::string m_func;
+    std::string m_cond;
+    std::string m_msg;
+
+    std::string m_format_msg;
+};
+
+} // namespace KI_TEST
+
+#endif // UNIT_TEST_UTILS_WX_ASSERT__H
\ No newline at end of file
diff --git a/qa/unit_test_utils/unit_test_utils.cpp b/qa/unit_test_utils/unit_test_utils.cpp
index e96ba6c09..71550e564 100644
--- a/qa/unit_test_utils/unit_test_utils.cpp
+++ b/qa/unit_test_utils/unit_test_utils.cpp
@@ -21,6 +21,15 @@
  * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
  */
 
-/*
- * Nothing here yet, but CMake requires *something*.
- */
\ No newline at end of file
+#include <unit_test_utils/unit_test_utils.h>
+
+BOOST_TEST_PRINT_NAMESPACE_OPEN
+{
+
+void print_log_value<wxPoint>::operator()( std::ostream& os, wxPoint const& aPt )
+{
+    os << "WXPOINT[ x=\"" << aPt.x << "\" y=\"" << aPt.y << "\" ]";
+}
+
+}
+BOOST_TEST_PRINT_NAMESPACE_CLOSE
\ No newline at end of file
diff --git a/qa/unit_test_utils/wx_assert.cpp b/qa/unit_test_utils/wx_assert.cpp
new file mode 100644
index 000000000..a34141c8a
--- /dev/null
+++ b/qa/unit_test_utils/wx_assert.cpp
@@ -0,0 +1,52 @@
+/*
+ * This program source code file is part of KiCad, a free EDA CAD application.
+ *
+ * Copyright (C) 2019 KiCad Developers, see CHANGELOG.TXT for contributors.
+ *
+ * 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 2
+ * 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 this program; if not, you may find one here:
+ * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
+ * or you may search the http://www.gnu.org website for the version 2 license,
+ * or you may write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
+ */
+
+#include <unit_test_utils/wx_assert.h>
+
+#include <sstream>
+
+namespace KI_TEST
+{
+WX_ASSERT_ERROR::WX_ASSERT_ERROR( const wxString& aFile, int aLine, const wxString& aFunc,
+        const wxString& aCond, const wxString& aMsg )
+        : m_file( aFile ), m_line( aLine ), m_func( aFunc ), m_cond( aCond ), m_msg( aMsg )
+{
+    std::ostringstream ss;
+
+    ss << "WX assertion in " << m_file << ":" << m_line << "\n"
+       << "in function " << m_func << "\n"
+       << "failed condition: " << m_cond;
+
+    if( m_msg.size() )
+        ss << "\n"
+           << "with message: " << m_msg;
+
+    m_format_msg = ss.str();
+}
+
+const char* WX_ASSERT_ERROR::what() const noexcept
+{
+    return m_format_msg.c_str();
+}
+
+} // namespace KI_TEST
\ No newline at end of file
-- 
2.20.1


Follow ups