[Libreoffice-commits] core.git: pyuno/qa pyuno/source sfx2/qa solenv/gbuild sw/qa test/source unotest/source

Libreoffice Gerrit user logerrit at kemper.freedesktop.org
Fri Sep 7 05:32:12 UTC 2018


 pyuno/qa/pytests/testcollections_XCellRange.py         |   34 +++++++++++++++++
 pyuno/qa/pytests/testcollections_XEnumeration.py       |   11 +++++
 pyuno/qa/pytests/testcollections_XEnumerationAccess.py |   15 +++++++
 pyuno/qa/pytests/testcollections_XIndexAccess.py       |   24 ++++++++++++
 pyuno/qa/pytests/testcollections_XIndexContainer.py    |    2 +
 pyuno/qa/pytests/testcollections_XIndexReplace.py      |   18 +++++++++
 pyuno/qa/pytests/testcollections_XNameAccess.py        |   22 +++++++++++
 pyuno/qa/pytests/testcollections_XNameContainer.py     |   12 ++++++
 pyuno/qa/pytests/testcollections_XNameReplace.py       |    6 +++
 pyuno/qa/pytests/testcollections_misc.py               |    8 ++++
 pyuno/qa/pytests/testcollections_mixednameindex.py     |    2 +
 pyuno/source/module/pyuno_module.cxx                   |   33 ++++++++++++++++
 sfx2/qa/python/check_sidebar.py                        |    3 -
 sfx2/qa/python/check_sidebar_registry.py               |    1 
 solenv/gbuild/PythonTest.mk                            |    7 ++-
 sw/qa/python/check_bookmarks.py                        |    4 ++
 sw/qa/python/check_change_color.py                     |    1 
 sw/qa/python/check_cross_references.py                 |    4 ++
 sw/qa/python/check_fields.py                           |    8 ++--
 sw/qa/python/check_flies.py                            |    8 ++--
 sw/qa/python/check_indexed_property_values.py          |    1 
 sw/qa/python/check_named_property_values.py            |    1 
 sw/qa/python/check_table.py                            |    4 ++
 sw/qa/python/get_expression.py                         |    4 ++
 sw/qa/python/set_expression.py                         |    4 +-
 sw/qa/python/text_portion_enumeration_test.py          |    5 ++
 sw/qa/python/var_fields.py                             |    3 -
 test/source/bootstrapfixture.cxx                       |    6 +++
 unotest/source/python/org/libreoffice/unittest.py      |   29 ++++++++++++++
 unotest/source/python/org/libreoffice/unotest.py       |    7 +++
 30 files changed, 266 insertions(+), 21 deletions(-)

New commits:
commit e7a3329fd0a68c95f00e6cdfdc3e40e6afa5411c
Author:     Stephan Bergmann <sbergman at redhat.com>
AuthorDate: Thu Sep 6 17:13:54 2018 +0200
Commit:     Stephan Bergmann <sbergman at redhat.com>
CommitDate: Fri Sep 7 07:31:48 2018 +0200

    DeInitVCL in PythonTest
    
    After b9757f5cfdb62b24e79eeb4c0ef0c8b98056cecf "loplugin:useuniqueptr in
    vcl/svdata" ASan/UBSan builds started to fail (like
    <https://ci.libreoffice.org//job/lo_ubsan/1025/>) at the end of
    PythonTest_dbaccess_python (and probably other PythonTests), when during exit
    the static utl::ConfigManager instance already happens to be destroyed by the
    time the static ImplSVData's mpSettingsConfigItem is destroyed (which would
    normally be cleared during DeInitVCL, if PythonTests would call that, and which
    in the past had thus simply been leaked in PythonTests when that
    mpSettingsConfigItem was a plain pointer instead of std::unique_ptr).
    
    So ensure that PythonTests that initialize VCL also call DeInitVCL, via a new
    private_deinitTestEnvironment, complementing the existing
    private_initTestEnvironment.
    
    However, while private_initTestEnvironment is called once (typically via
    UnoInProcess.setUp, which internally makes sure to only call it once) as soon as
    the first executed test needs it, private_deinitTestEnvironment must be called
    once after the lasts test needing it has executed.  The only way that I found to
    do that is to override unittest.TextTestResult's stopTestRun method, which is
    called once after all tests have been executed.  Hence a new test runner setup
    in unotest/source/python/org/libreoffice/unittest.py that is now called from
    solenv/gbuild/PythonTest.mk.
    
    That revealed a few places in PythonTests that didn't yet close/delete documents
    that they had opened, which has now been added.
    
    One remaining problem then is that classes like SwXTextDocument and friends call
    Application::GetSolarMutex from their dtors, via sw::UnoImplPtrDeleter (a "Smart
    pointer class ensuring that the pointed object is deleted with a locked
    SolarMutex", sw/inc/unobaseclass.hxx).  That means that any PyUNO proxies to
    such C++ objects that remain alive after private_deinitTestEnvironment will
    cause issues at exit, when Python does a final garbage collection of those
    objects.  The ultimate fix will be to remove that unhelpful UnoImplPtrDeleter
    and its locking of SolarMutex from the dtors of UNO objects; until then, the
    Python code is now sprinkled with some HACKs to make sure all those PyUNO
    proxies are released in a timely fashion (see the comment in
    unotest/source/python/org/libreoffice/unittest.py for details).  (Also, it would
    probably help if UnoInProcess didn't keep a local self.xDoc around referencing
    (just) the last result of calling one of its open* methods, confusingly making
    it the responsibility of UnoInProcess to close that one document while making it
    the responsibility of the test code making the other UnoInProcess.open* calls to
    close any other documents.)
    
    Change-Id: Ief27c81e2b763e9be20cbf3234b68924315f13be
    Reviewed-on: https://gerrit.libreoffice.org/60100
    Tested-by: Jenkins
    Reviewed-by: Stephan Bergmann <sbergman at redhat.com>

diff --git a/pyuno/qa/pytests/testcollections_XCellRange.py b/pyuno/qa/pytests/testcollections_XCellRange.py
index 6754ef52814c..2e0ef8a7d3d7 100644
--- a/pyuno/qa/pytests/testcollections_XCellRange.py
+++ b/pyuno/qa/pytests/testcollections_XCellRange.py
@@ -43,6 +43,8 @@ class TestXCellRange(CollectionsTestBase):
         self.assertEqual(0, cell.CellAddress.Row)
         self.assertEqual(0, cell.CellAddress.Column)
 
+        spr.close(True)
+
     # Tests syntax:
     #    cell = cellrange[0,0]       # Access cell by indices
     # For:
@@ -63,6 +65,8 @@ class TestXCellRange(CollectionsTestBase):
         # Then
         self.assertEqual('A1', cell.CellName)
 
+        doc.close(True)
+
     # Tests syntax:
     #    cell = cellrange[0,0]       # Access cell by indices
     # For:
@@ -81,6 +85,8 @@ class TestXCellRange(CollectionsTestBase):
         self.assertEqual(3, rng.CellAddress.Row)
         self.assertEqual(7, rng.CellAddress.Column)
 
+        spr.close(True)
+
     # Tests syntax:
     #    cell = cellrange[0,0]       # Access cell by indices
     # For:
@@ -101,6 +107,8 @@ class TestXCellRange(CollectionsTestBase):
         # Then
         self.assertEqual('H4', cell.CellName)
 
+        doc.close(True)
+
     # Tests syntax:
     #    rng = cellrange[0,1:2]      # Access cell range by index,slice
     # For:
@@ -120,6 +128,8 @@ class TestXCellRange(CollectionsTestBase):
         self.assertEqual(0, rng.RangeAddress.EndRow)
         self.assertEqual(2, rng.RangeAddress.EndColumn)
 
+        spr.close(True)
+
     # Tests syntax:
     #    rng = cellrange[0,1:2]      # Access cell range by index,slice
     # For:
@@ -142,6 +152,8 @@ class TestXCellRange(CollectionsTestBase):
         # Then
         self.assertEqual((('101', '102'),), rng.DataArray)
 
+        doc.close(True)
+
     # Tests syntax:
     #    rng = cellrange[1:2,0]      # Access cell range by slice,index
     # For:
@@ -161,6 +173,8 @@ class TestXCellRange(CollectionsTestBase):
         self.assertEqual(2, rng.RangeAddress.EndRow)
         self.assertEqual(0, rng.RangeAddress.EndColumn)
 
+        spr.close(True)
+
     # Tests syntax:
     #    rng = cellrange[1:2,0]      # Access cell range by index,slice
     # For:
@@ -183,6 +197,8 @@ class TestXCellRange(CollectionsTestBase):
         # Then
         self.assertEqual((('110',), ('120',)), rng.DataArray)
 
+        doc.close(True)
+
     # Tests syntax:
     #    rng = cellrange[0:1,2:3]    # Access cell range by slices
     # For:
@@ -202,6 +218,8 @@ class TestXCellRange(CollectionsTestBase):
         self.assertEqual(2, rng.RangeAddress.EndRow)
         self.assertEqual(4, rng.RangeAddress.EndColumn)
 
+        spr.close(True)
+
     # Tests syntax:
     #    rng = cellrange[0:1,2:3]    # Access cell range by slices
     # For:
@@ -218,6 +236,8 @@ class TestXCellRange(CollectionsTestBase):
         with self.assertRaises(KeyError):
             rng = sht[1:3, 3:3]
 
+        spr.close(True)
+
     # Tests syntax:
     #    rng = cellrange[0:1,2:3]    # Access cell range by slices
     # For:
@@ -240,6 +260,8 @@ class TestXCellRange(CollectionsTestBase):
         # Then
         self.assertEqual((('113', '114'), ('123', '124')), rng.DataArray)
 
+        doc.close(True)
+
     # Tests syntax:
     #    rng = cellrange['A1:B2']    # Access cell range by descriptor
     # For:
@@ -259,6 +281,8 @@ class TestXCellRange(CollectionsTestBase):
         self.assertEqual(3, rng.RangeAddress.EndRow)
         self.assertEqual(1, rng.RangeAddress.EndColumn)
 
+        spr.close(True)
+
     # Tests syntax:
     #    rng = cellrange['A1:B2']    # Access cell range by descriptor
     # For:
@@ -281,6 +305,8 @@ class TestXCellRange(CollectionsTestBase):
         # Then
         self.assertEqual((('120', '121'), ('130', '131')), rng.DataArray)
 
+        doc.close(True)
+
     # Tests syntax:
     #    rng = cellrange['Name']     # Access cell range by name
     # For:
@@ -303,6 +329,8 @@ class TestXCellRange(CollectionsTestBase):
         self.assertEqual(9, rng.RangeAddress.EndRow)
         self.assertEqual(5, rng.RangeAddress.EndColumn)
 
+        spr.close(True)
+
     # Tests syntax:
     #    rng = cellrange[0]          # Access cell range by row index
     # For:
@@ -322,6 +350,8 @@ class TestXCellRange(CollectionsTestBase):
         self.assertEqual(0, rng.RangeAddress.EndRow)
         self.assertEqual(1023, rng.RangeAddress.EndColumn)
 
+        spr.close(True)
+
     # Tests syntax:
     #    rng = cellrange[0,:]        # Access cell range by row index
     # For:
@@ -341,6 +371,8 @@ class TestXCellRange(CollectionsTestBase):
         self.assertEqual(0, rng.RangeAddress.EndRow)
         self.assertEqual(1023, rng.RangeAddress.EndColumn)
 
+        spr.close(True)
+
     # Tests syntax:
     #    rng = cellrange[:,0]        # Access cell range by column index
     # For:
@@ -360,6 +392,8 @@ class TestXCellRange(CollectionsTestBase):
         self.assertEqual(1048575, rng.RangeAddress.EndRow)
         self.assertEqual(0, rng.RangeAddress.EndColumn)
 
+        spr.close(True)
+
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/pyuno/qa/pytests/testcollections_XEnumeration.py b/pyuno/qa/pytests/testcollections_XEnumeration.py
index 6edc77f44952..8d1f8eece046 100644
--- a/pyuno/qa/pytests/testcollections_XEnumeration.py
+++ b/pyuno/qa/pytests/testcollections_XEnumeration.py
@@ -38,6 +38,8 @@ class TestXEnumeration(CollectionsTestBase):
         # Then
         self.assertEqual(1, len(paragraphs))
 
+        doc.close(True)
+
     # Tests syntax:
     #    if val in itr: ...          # Test value presence
     # For:
@@ -54,6 +56,8 @@ class TestXEnumeration(CollectionsTestBase):
         # Then
         self.assertTrue(result)
 
+        doc.close(True)
+
     # Tests syntax:
     #    if val in itr: ...          # Test value presence
     # For:
@@ -71,6 +75,9 @@ class TestXEnumeration(CollectionsTestBase):
         # Then
         self.assertFalse(result)
 
+        doc1.close(True)
+        doc2.close(True)
+
     # Tests syntax:
     #    if val in itr: ...          # Test value presence
     # For:
@@ -86,6 +93,8 @@ class TestXEnumeration(CollectionsTestBase):
         # Then
         self.assertFalse(result)
 
+        doc.close(True)
+
     # Tests syntax:
     #    if val in itr: ...          # Test value presence
     # For:
@@ -104,6 +113,8 @@ class TestXEnumeration(CollectionsTestBase):
         # Then
         self.assertFalse(result)
 
+        doc.close(True)
+
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/pyuno/qa/pytests/testcollections_XEnumerationAccess.py b/pyuno/qa/pytests/testcollections_XEnumerationAccess.py
index dc5a52f54537..a62b05ce9c5f 100644
--- a/pyuno/qa/pytests/testcollections_XEnumerationAccess.py
+++ b/pyuno/qa/pytests/testcollections_XEnumerationAccess.py
@@ -37,6 +37,8 @@ class TestXEnumerationAccess(CollectionsTestBase):
         # Then
         self.assertEqual(1, len(paragraphs))
 
+        doc.close(True)
+
     # Tests syntax:
     #    itr = iter(obj)             # Named iterator
     # For:
@@ -53,6 +55,8 @@ class TestXEnumerationAccess(CollectionsTestBase):
         with self.assertRaises(StopIteration):
             next(itr)
 
+        doc.close(True)
+
     # Tests syntax:
     #    if val in obj: ...          # Test value presence
     # For:
@@ -68,6 +72,8 @@ class TestXEnumerationAccess(CollectionsTestBase):
         # Then
         self.assertTrue(result)
 
+        doc.close(True)
+
     # Tests syntax:
     #    if val in obj: ...          # Test value presence
     # For:
@@ -84,6 +90,9 @@ class TestXEnumerationAccess(CollectionsTestBase):
         # Then
         self.assertFalse(result)
 
+        doc1.close(True)
+        doc2.close(True)
+
     # Tests syntax:
     #    if val in obj: ...          # Test value presence
     # For:
@@ -98,6 +107,8 @@ class TestXEnumerationAccess(CollectionsTestBase):
         # Then
         self.assertFalse(result)
 
+        doc.close(True)
+
     # Tests syntax:
     #    if val in obj: ...          # Test value presence
     # For:
@@ -112,6 +123,8 @@ class TestXEnumerationAccess(CollectionsTestBase):
         # Then
         self.assertFalse(result)
 
+        doc.close(True)
+
     # Tests syntax:
     #    if val in obj: ...          # Test value presence
     # For:
@@ -124,6 +137,8 @@ class TestXEnumerationAccess(CollectionsTestBase):
         with self.assertRaises(TypeError):
             result = {} in doc.Text
 
+        doc.close(True)
+
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/pyuno/qa/pytests/testcollections_XIndexAccess.py b/pyuno/qa/pytests/testcollections_XIndexAccess.py
index 4631ca3706ed..7228ed87336b 100644
--- a/pyuno/qa/pytests/testcollections_XIndexAccess.py
+++ b/pyuno/qa/pytests/testcollections_XIndexAccess.py
@@ -77,6 +77,8 @@ class TestXIndexAccess(CollectionsTestBase):
         # Then
         self.assertEqual(0, count)
 
+        doc.close(True);
+
     # Tests syntax:
     #    num = len(obj)              # Number of elements
     # For:
@@ -94,6 +96,8 @@ class TestXIndexAccess(CollectionsTestBase):
         # Then
         self.assertEqual(1, count)
 
+        doc.close(True);
+
     # Tests syntax:
     #    val = obj[0]                # Access by index
     # For:
@@ -117,6 +121,7 @@ class TestXIndexAccess(CollectionsTestBase):
         self.readValuesTestFixture(doc, 2, 1,          1)
         self.readValuesTestFixture(doc, 2, 2,          IndexError)
         self.readValuesTestFixture(doc, 2, 3,          IndexError)
+        doc.close(True);
 
     def test_XIndexAccess_ReadIndex_Single_Invalid(self):
         doc = self.createBlankTextDocument()
@@ -126,6 +131,7 @@ class TestXIndexAccess(CollectionsTestBase):
         self.readValuesTestFixture(doc, 0, (0, 1),     TypeError)
         self.readValuesTestFixture(doc, 0, [0, 1],     TypeError)
         self.readValuesTestFixture(doc, 0, {'a': 'b'}, TypeError)
+        doc.close(True);
 
     # Tests syntax:
     #    val1,val2 = obj[2:4]        # Access by slice
@@ -139,6 +145,7 @@ class TestXIndexAccess(CollectionsTestBase):
                     key = slice(j, k)
                     expected = t[key]
                     self.readValuesTestFixture(doc, i, key, expected)
+        doc.close(True);
 
     # Tests syntax:
     #    val1,val2 = obj[0:3:2]      # Access by extended slice
@@ -153,6 +160,7 @@ class TestXIndexAccess(CollectionsTestBase):
                         key = slice(j, k, l)
                         expected = t[key]
                         self.readValuesTestFixture(doc, i, key, expected)
+        doc.close(True);
 
     # Tests syntax:
     #    if val in obj: ...          # Test value presence
@@ -173,6 +181,8 @@ class TestXIndexAccess(CollectionsTestBase):
         # Then
         self.assertTrue(present)
 
+        doc.close(True);
+
     # Tests syntax:
     #    if val in obj: ...          # Test value presence
     # For:
@@ -187,6 +197,8 @@ class TestXIndexAccess(CollectionsTestBase):
         # Then
         self.assertFalse(present)
 
+        doc.close(True);
+
     # Tests syntax:
     #    if val in obj: ...          # Test value presence
     # For:
@@ -201,6 +213,8 @@ class TestXIndexAccess(CollectionsTestBase):
         # Then
         self.assertFalse(present)
 
+        doc.close(True);
+
     # Tests syntax:
     #    if val in obj: ...          # Test value presence
     # For:
@@ -213,6 +227,8 @@ class TestXIndexAccess(CollectionsTestBase):
         with self.assertRaises(TypeError):
             present = {} in doc.Footnotes
 
+        doc.close(True);
+
     # Tests syntax:
     #    for val in obj: ...         # Implicit iterator (values)
     # For:
@@ -229,6 +245,8 @@ class TestXIndexAccess(CollectionsTestBase):
         # Then
         self.assertEqual(0, len(read_footnotes))
 
+        doc.close(True);
+
     # Tests syntax:
     #    for val in obj: ...         # Implicit iterator (values)
     # For:
@@ -250,6 +268,8 @@ class TestXIndexAccess(CollectionsTestBase):
         self.assertEqual(1, len(read_footnotes))
         self.assertEqual('foo', read_footnotes[0].Label)
 
+        doc.close(True);
+
     # Tests syntax:
     #    for val in obj: ...         # Implicit iterator (values)
     # For:
@@ -275,6 +295,8 @@ class TestXIndexAccess(CollectionsTestBase):
         self.assertEqual('foo', read_footnotes[0].Label)
         self.assertEqual('bar', read_footnotes[1].Label)
 
+        doc.close(True);
+
     # Tests syntax:
     #    itr = iter(obj)             # Named iterator (values)
     # For:
@@ -295,6 +317,8 @@ class TestXIndexAccess(CollectionsTestBase):
         with self.assertRaises(StopIteration):
             next(itr)
 
+        doc.close(True);
+
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/pyuno/qa/pytests/testcollections_XIndexContainer.py b/pyuno/qa/pytests/testcollections_XIndexContainer.py
index 7bf9e5223039..73be6b57c25d 100644
--- a/pyuno/qa/pytests/testcollections_XIndexContainer.py
+++ b/pyuno/qa/pytests/testcollections_XIndexContainer.py
@@ -145,6 +145,8 @@ class TestXIndexContainer(CollectionsTestBase):
         # Then
         self.assertEqual('foo', doc.DrawPage.Forms[0].Name)
 
+        doc.close(True)
+
     # Tests syntax:
     #    obj[0:3:2] = val1,val2      # Replace by extended slice
     # For:
diff --git a/pyuno/qa/pytests/testcollections_XIndexReplace.py b/pyuno/qa/pytests/testcollections_XIndexReplace.py
index 45d1cc075f33..bbf424f0bdfb 100644
--- a/pyuno/qa/pytests/testcollections_XIndexReplace.py
+++ b/pyuno/qa/pytests/testcollections_XIndexReplace.py
@@ -78,6 +78,8 @@ class TestXIndexReplace(CollectionsTestBase):
         # Then
         self.assertEqual(('Caption',), index.LevelParagraphStyles[0])
 
+        doc.close(True)
+
     # Tests syntax:
     #    obj[0] = val                # Replace by index
     # For:
@@ -91,6 +93,8 @@ class TestXIndexReplace(CollectionsTestBase):
         with self.assertRaises(TypeError):
             index.LevelParagraphStyles[0] = None
 
+        doc.close(True)
+
     # Tests syntax:
     #    obj[0] = val                # Replace by index
     # For:
@@ -104,6 +108,8 @@ class TestXIndexReplace(CollectionsTestBase):
         with self.assertRaises(TypeError):
             index.LevelParagraphStyles[0] = 'foo'
 
+        doc.close(True)
+
     # Tests syntax:
     #    obj[0] = val                # Replace by index
     # For:
@@ -117,6 +123,8 @@ class TestXIndexReplace(CollectionsTestBase):
         with self.assertRaises(TypeError):
             index.LevelParagraphStyles[0] = 12.34
 
+        doc.close(True)
+
     # Tests syntax:
     #    obj[0] = val                # Replace by index
     # For:
@@ -130,6 +138,8 @@ class TestXIndexReplace(CollectionsTestBase):
         with self.assertRaises(TypeError):
             index.LevelParagraphStyles[0] = [0, 1]
 
+        doc.close(True)
+
     # Tests syntax:
     #    obj[0] = val                # Replace by index
     # For:
@@ -143,6 +153,8 @@ class TestXIndexReplace(CollectionsTestBase):
         with self.assertRaises(TypeError):
             index.LevelParagraphStyles[0] = {'a': 'b'}
 
+        doc.close(True)
+
     # Tests syntax:
     #    obj[0] = val                # Replace by index
     # For:
@@ -156,6 +168,8 @@ class TestXIndexReplace(CollectionsTestBase):
         with self.assertRaises(TypeError):
             index.LevelParagraphStyles[0] = ('Caption', ())
 
+        doc.close(True)
+
     # Tests syntax:
     #    obj[2:4] = val1,val2        # Replace by slice
     # For:
@@ -177,6 +191,7 @@ class TestXIndexReplace(CollectionsTestBase):
                     if (len(expected) != 10):
                         expected = ValueError()
                     self.assignValuesTestFixture(doc, key, assign, expected)
+        doc.close(True)
 
     # Tests syntax:
     #    obj[2:4] = val1,val2        # Replace by slice
@@ -194,6 +209,8 @@ class TestXIndexReplace(CollectionsTestBase):
                 12.34
             )
 
+        doc.close(True)
+
     # Tests syntax:
     #    obj[0:3:2] = val1,val2      # Replace by extended slice
     # For:
@@ -214,6 +231,7 @@ class TestXIndexReplace(CollectionsTestBase):
                         except Exception as e:
                             expected = e
                         self.assignValuesTestFixture(doc, key, assign, expected)
+        doc.close(True)
 
 
 if __name__ == '__main__':
diff --git a/pyuno/qa/pytests/testcollections_XNameAccess.py b/pyuno/qa/pytests/testcollections_XNameAccess.py
index a93064e78bb1..7f987a370077 100644
--- a/pyuno/qa/pytests/testcollections_XNameAccess.py
+++ b/pyuno/qa/pytests/testcollections_XNameAccess.py
@@ -35,6 +35,8 @@ class TestXNameAccess(CollectionsTestBase):
         # Then
         self.assertEqual(2, length)
 
+        drw.close(True)
+
     # Tests syntax:
     #    val = obj[key]              # Access by key
     # For:
@@ -50,6 +52,8 @@ class TestXNameAccess(CollectionsTestBase):
         # Then
         self.assertEqual('foo', link.getName())
 
+        drw.close(True)
+
     # Tests syntax:
     #    val = obj[key]              # Access by key
     # For:
@@ -62,6 +66,8 @@ class TestXNameAccess(CollectionsTestBase):
         with self.assertRaises(KeyError):
             link = drw.Links['foo']
 
+        drw.close(True)
+
     # Tests syntax:
     #    val = obj[key]              # Access by key
     # For:
@@ -74,6 +80,8 @@ class TestXNameAccess(CollectionsTestBase):
         with self.assertRaises(TypeError):
             link = drw.Links[None]
 
+        drw.close(True)
+
     # Tests syntax:
     #    val = obj[key]              # Access by key
     # For:
@@ -86,6 +94,8 @@ class TestXNameAccess(CollectionsTestBase):
         with self.assertRaises(TypeError):
             link = drw.Links[12.34]
 
+        drw.close(True)
+
     # Tests syntax:
     #    val = obj[key]              # Access by key
     # For:
@@ -98,6 +108,8 @@ class TestXNameAccess(CollectionsTestBase):
         with self.assertRaises(TypeError):
             link = drw.Links[(1, 2)]
 
+        drw.close(True)
+
     # Tests syntax:
     #    val = obj[key]              # Access by key
     # For:
@@ -110,6 +122,8 @@ class TestXNameAccess(CollectionsTestBase):
         with self.assertRaises(TypeError):
             link = drw.Links[[1, 2]]
 
+        drw.close(True)
+
     # Tests syntax:
     #    val = obj[key]              # Access by key
     # For:
@@ -122,6 +136,8 @@ class TestXNameAccess(CollectionsTestBase):
         with self.assertRaises(TypeError):
             link = drw.Links[{'a': 'b'}]
 
+        drw.close(True)
+
     # Tests syntax:
     #    if key in obj: ...          # Test key presence
     # For:
@@ -137,6 +153,8 @@ class TestXNameAccess(CollectionsTestBase):
         # Then
         self.assertTrue(present)
 
+        drw.close(True)
+
     # Tests syntax:
     #    for key in obj: ...         # Implicit iterator (keys)
     # For:
@@ -157,6 +175,8 @@ class TestXNameAccess(CollectionsTestBase):
         # Then
         self.assertEqual(['foo0', 'foo1'], read_links)
 
+        drw.close(True)
+
     # Tests syntax:
     #    itr = iter(obj)             # Named iterator (keys)
     # For:
@@ -174,6 +194,8 @@ class TestXNameAccess(CollectionsTestBase):
         with self.assertRaises(StopIteration):
             next(itr)
 
+        drw.close(True)
+
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/pyuno/qa/pytests/testcollections_XNameContainer.py b/pyuno/qa/pytests/testcollections_XNameContainer.py
index 48b63a786f7e..5c8b676c0f6e 100644
--- a/pyuno/qa/pytests/testcollections_XNameContainer.py
+++ b/pyuno/qa/pytests/testcollections_XNameContainer.py
@@ -41,6 +41,8 @@ class TestXNameContainer(CollectionsTestBase):
         # Then
         self.assertEqual(1, len(ranges.ElementNames))
 
+        spr.close(True)
+
     # Tests syntax:
     #    obj[key] = val              # Insert by key
     # For:
@@ -55,6 +57,8 @@ class TestXNameContainer(CollectionsTestBase):
         with self.assertRaises(TypeError):
             ranges[12.34] = new_range
 
+        spr.close(True)
+
     # Tests syntax:
     #    obj[key] = val              # Replace by key
     def test_XNameContainer_ReplaceName(self):
@@ -73,6 +77,8 @@ class TestXNameContainer(CollectionsTestBase):
         read_range = ranges['foo']
         self.assertEqual(6, read_range.CellAddress.Column)
 
+        spr.close(True)
+
     # Tests syntax:
     #    del obj[key]                # Delete by key
     # For:
@@ -89,6 +95,8 @@ class TestXNameContainer(CollectionsTestBase):
         self.assertEqual(1, len(spr.Sheets))
         self.assertFalse('foo' in spr.Sheets)
 
+        spr.close(True)
+
     # Tests syntax:
     #    del obj[key]                # Delete by key
     # For:
@@ -101,6 +109,8 @@ class TestXNameContainer(CollectionsTestBase):
         with self.assertRaises(KeyError):
             del spr.Sheets['foo']
 
+        spr.close(True)
+
     # Tests syntax:
     #    del obj[key]                # Delete by key
     # For:
@@ -113,6 +123,8 @@ class TestXNameContainer(CollectionsTestBase):
         with self.assertRaises(TypeError):
             del spr.Sheets[12.34]
 
+        spr.close(True)
+
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/pyuno/qa/pytests/testcollections_XNameReplace.py b/pyuno/qa/pytests/testcollections_XNameReplace.py
index 2176f935d9dc..18476fd2b447 100644
--- a/pyuno/qa/pytests/testcollections_XNameReplace.py
+++ b/pyuno/qa/pytests/testcollections_XNameReplace.py
@@ -40,6 +40,8 @@ class TestXNameReplace(CollectionsTestBase):
         on_save = [p.Value for p in doc.Events['OnSave'] if p.Name == 'Script'][0]
         self.assertEqual(getScriptName(), on_save)
 
+        doc.close(True)
+
     # Tests syntax:
     #    obj[key] = val              # Replace by key
     # For:
@@ -53,6 +55,8 @@ class TestXNameReplace(CollectionsTestBase):
         with self.assertRaises(KeyError):
             doc.Events['qqqqq'] = event_properties
 
+        doc.close(True)
+
     # Tests syntax:
     #    obj[key] = val              # Replace by key
     # For:
@@ -66,6 +70,8 @@ class TestXNameReplace(CollectionsTestBase):
         with self.assertRaises(TypeError):
             doc.Events[12.34] = event_properties
 
+        doc.close(True)
+
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/pyuno/qa/pytests/testcollections_misc.py b/pyuno/qa/pytests/testcollections_misc.py
index 04dcf595931a..1dba098eeccd 100644
--- a/pyuno/qa/pytests/testcollections_misc.py
+++ b/pyuno/qa/pytests/testcollections_misc.py
@@ -32,6 +32,8 @@ class TestMisc(CollectionsTestBase):
             for val in doc.UIConfigurationManager:
                 pass
 
+        doc.close(True)
+
     # Tests syntax:
     #    if val in itr: ...          # Test value presence
     # For:
@@ -44,6 +46,8 @@ class TestMisc(CollectionsTestBase):
         with self.assertRaises(TypeError):
             foo = "bar" in doc.UIConfigurationManager
 
+        doc.close(True)
+
     # Tests syntax:
     #    num = len(obj)              # Number of elements
     # For:
@@ -56,6 +60,8 @@ class TestMisc(CollectionsTestBase):
         with self.assertRaises(TypeError):
             len(doc.UIConfigurationManager)
 
+        doc.close(True)
+
     # Tests syntax:
     #    val = obj[0]                # Access by index
     # For:
@@ -68,6 +74,8 @@ class TestMisc(CollectionsTestBase):
         with self.assertRaises(TypeError):
             doc.UIConfigurationManager[0]
 
+        doc.close(True)
+
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/pyuno/qa/pytests/testcollections_mixednameindex.py b/pyuno/qa/pytests/testcollections_mixednameindex.py
index 67e97a88dd90..b4c7958c6996 100644
--- a/pyuno/qa/pytests/testcollections_mixednameindex.py
+++ b/pyuno/qa/pytests/testcollections_mixednameindex.py
@@ -41,6 +41,8 @@ class TestMixedNameIndex(CollectionsTestBase):
         self.assertEqual('foo', table_by_index.Name)
         self.assertEqual(table_by_name, table_by_index)
 
+        doc.close(True)
+
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/pyuno/source/module/pyuno_module.cxx b/pyuno/source/module/pyuno_module.cxx
index e212e8de9f8d..0f08ebc53367 100644
--- a/pyuno/source/module/pyuno_module.cxx
+++ b/pyuno/source/module/pyuno_module.cxx
@@ -22,6 +22,7 @@
 
 #include "pyuno_impl.hxx"
 
+#include <cassert>
 #include <unordered_map>
 #include <utility>
 
@@ -318,12 +319,22 @@ static PyObject* getComponentContext(
     return ret.getAcquired();
 }
 
+// While pyuno.private_initTestEnvironment is called from individual Python tests (e.g., from
+// UnoInProcess in unotest/source/python/org/libreoffice/unotest.py, which makes sure to call it
+// only once), pyuno.private_deinitTestEnvironment is called centrally from
+// unotest/source/python/org/libreoffice/unittest.py at the end of every PythonTest (to DeInitVCL
+// exactly once near the end of the process, if InitVCL has ever been called via
+// pyuno.private_initTestEnvironment):
+
+static osl::Module * testModule = nullptr;
+
 static PyObject* initTestEnvironment(
     SAL_UNUSED_PARAMETER PyObject*, SAL_UNUSED_PARAMETER PyObject*)
 {
     // this tries to bootstrap enough of the soffice from python to run
     // unit tests, which is only possible indirectly because pyuno is URE
     // so load "test" library and invoke a function there to do the work
+    assert(testModule == nullptr);
     try
     {
         PyObject *const ctx(getComponentContext(nullptr, nullptr));
@@ -353,6 +364,7 @@ static PyObject* initTestEnvironment(
                 mod.getFunctionSymbol("test_init"));
         if (!pFunc) { abort(); }
         reinterpret_cast<void (SAL_CALL *)(XMultiServiceFactory*)>(pFunc)(xMSF.get());
+        testModule = &mod;
     }
     catch (const css::uno::Exception &)
     {
@@ -361,6 +373,26 @@ static PyObject* initTestEnvironment(
     return Py_None;
 }
 
+static PyObject* deinitTestEnvironment(
+    SAL_UNUSED_PARAMETER PyObject*, SAL_UNUSED_PARAMETER PyObject*)
+{
+    if (testModule != nullptr)
+    {
+        try
+        {
+            oslGenericFunction const pFunc(
+                    testModule->getFunctionSymbol("test_deinit"));
+            if (!pFunc) { abort(); }
+            reinterpret_cast<void (SAL_CALL *)()>(pFunc)();
+        }
+        catch (const css::uno::Exception &)
+        {
+            abort();
+        }
+    }
+    return Py_None;
+}
+
 PyObject * extractOneStringArg( PyObject *args, char const *funcName )
 {
     if( !PyTuple_Check( args ) || PyTuple_Size( args) != 1 )
@@ -843,6 +875,7 @@ static PyObject *sal_debug(
 struct PyMethodDef PyUNOModule_methods [] =
 {
     {"private_initTestEnvironment", initTestEnvironment, METH_VARARGS, nullptr},
+    {"private_deinitTestEnvironment", deinitTestEnvironment, METH_VARARGS, nullptr},
     {"getComponentContext", getComponentContext, METH_VARARGS, nullptr},
     {"_createUnoStructHelper", reinterpret_cast<PyCFunction>(createUnoStructHelper), METH_VARARGS | METH_KEYWORDS, nullptr},
     {"getTypeByName", getTypeByName, METH_VARARGS, nullptr},
diff --git a/sfx2/qa/python/check_sidebar.py b/sfx2/qa/python/check_sidebar.py
index e8fd50338b27..59cc955b8016 100644
--- a/sfx2/qa/python/check_sidebar.py
+++ b/sfx2/qa/python/check_sidebar.py
@@ -24,7 +24,6 @@ class CheckSidebar(unittest.TestCase):
     def setUpClass(cls):
         cls._uno = UnoInProcess()
         cls._uno.setUp()
-        cls._xDoc = cls._uno.openEmptyDoc( url = "private:factory/scalc", bHidden = False, bReadOnly = False)
 
     @classmethod
     def tearDownClass(cls):
@@ -32,7 +31,7 @@ class CheckSidebar(unittest.TestCase):
 
     def test_check_sidebar(self):
 
-        xDoc = self.__class__._xDoc
+        xDoc = self.__class__._uno.openEmptyDoc( url = "private:factory/scalc", bHidden = False, bReadOnly = False)
         xController = xDoc.getCurrentController()
 
         xSidebar = xController.getSidebar()
diff --git a/sfx2/qa/python/check_sidebar_registry.py b/sfx2/qa/python/check_sidebar_registry.py
index 86f22f4fd44a..47b8eecdefb9 100644
--- a/sfx2/qa/python/check_sidebar_registry.py
+++ b/sfx2/qa/python/check_sidebar_registry.py
@@ -19,7 +19,6 @@ class CheckSidebarRegistry(unittest.TestCase):
     def setUpClass(cls):
         cls._uno = UnoInProcess()
         cls._uno.setUp()
-        cls._xDoc = cls._uno.openEmptyDoc( url = "private:factory/scalc", bHidden = False, bReadOnly = False)
 
     @classmethod
     def tearDownClass(cls):
diff --git a/solenv/gbuild/PythonTest.mk b/solenv/gbuild/PythonTest.mk
index a2bac3819e02..329009c4b1d2 100644
--- a/solenv/gbuild/PythonTest.mk
+++ b/solenv/gbuild/PythonTest.mk
@@ -21,7 +21,7 @@ gb_PythonTest_EXECUTABLE_GDB := $(PYTHON_FOR_BUILD)
 gb_PythonTest_DEPS :=
 endif
 
-gb_PythonTest_COMMAND := $(gb_PythonTest_EXECUTABLE) -m unittest
+gb_PythonTest_COMMAND := $(gb_PythonTest_EXECUTABLE) -m org.libreoffice.unittest
 
 .PHONY : $(call gb_PythonTest_get_clean_target,%)
 $(call gb_PythonTest_get_clean_target,%) :
@@ -31,7 +31,10 @@ $(call gb_PythonTest_get_clean_target,%) :
 ifneq ($(DISABLE_PYTHON),TRUE)
 
 .PHONY : $(call gb_PythonTest_get_target,%)
-$(call gb_PythonTest_get_target,%) :| $(gb_PythonTest_DEPS)
+$(call gb_PythonTest_get_target,%) :\
+        $(call gb_Library_get_target,pyuno) \
+        $(if $(filter-out WNT,$(OS)),$(call gb_Library_get_target,pyuno_wrapper)) \
+        | $(gb_PythonTest_DEPS)
 ifneq ($(gb_SUPPRESS_TESTS),)
 	@true
 else
diff --git a/sw/qa/python/check_bookmarks.py b/sw/qa/python/check_bookmarks.py
index 73fd9bb6b98e..8210b4eb005a 100644
--- a/sw/qa/python/check_bookmarks.py
+++ b/sw/qa/python/check_bookmarks.py
@@ -47,6 +47,10 @@ class CheckBookmarks(unittest.TestCase):
     @classmethod
     def tearDownClass(cls):
         cls._uno.tearDown()
+        # HACK in case cls._xDoc holds a UNO proxy to an SwXTextDocument (whose dtor calls
+        # Application::GetSolarMutex via sw::UnoImplPtrDeleter), which would potentially only be
+        # garbage-collected after VCL has already been deinitialized:
+        cls._xDoc = None
 
     def test_bookmarks(self):
         self.xDoc = self.__class__._xDoc
diff --git a/sw/qa/python/check_change_color.py b/sw/qa/python/check_change_color.py
index d8562bc77b52..07b622031a61 100644
--- a/sw/qa/python/check_change_color.py
+++ b/sw/qa/python/check_change_color.py
@@ -27,7 +27,6 @@ class CheckChangeColor(unittest.TestCase):
     def setUpClass(cls):
         cls._uno = UnoInProcess()
         cls._uno.setUp()
-        cls._xEmptyDoc = cls._uno.openEmptyWriterDoc()
         cls.RED = 0xFF0000
         cls.BLUE = 0x0000FF
         cls.GREEN = 0x008000
diff --git a/sw/qa/python/check_cross_references.py b/sw/qa/python/check_cross_references.py
index 742cc0d94ad5..6aad1c47ca3a 100644
--- a/sw/qa/python/check_cross_references.py
+++ b/sw/qa/python/check_cross_references.py
@@ -46,6 +46,10 @@ class CheckCrossReferences(unittest.TestCase):
     @classmethod
     def tearDownClass(cls):
         cls._uno.tearDown()
+        # HACK in case cls.document holds a UNO proxy to an SwXTextDocument (whose dtor calls
+        # Application::GetSolarMutex via sw::UnoImplPtrDeleter), which would potentially only be
+        # garbage-collected after VCL has already been deinitialized:
+        cls.document = None
 
     def getNextField(self):
         while True:
diff --git a/sw/qa/python/check_fields.py b/sw/qa/python/check_fields.py
index 60418a93001d..eb6dd2dc1c8a 100644
--- a/sw/qa/python/check_fields.py
+++ b/sw/qa/python/check_fields.py
@@ -17,8 +17,6 @@ class CheckFields(unittest.TestCase):
     def setUpClass(cls):
         cls._uno = UnoInProcess()
         cls._uno.setUp()
-        cls._xDoc = cls._uno.openTemplateFromTDOC("fdo39694.ott")
-        cls._xEmptyDoc = cls._uno.openEmptyWriterDoc()
 
     @classmethod
     def tearDownClass(cls):
@@ -26,7 +24,7 @@ class CheckFields(unittest.TestCase):
 
     def test_fdo39694_load(self):
         placeholders = ["<Kadr1>", "<Kadr2>", "<Kadr3>", "<Kadr4>", "<Pnname>", "<Pvname>", "<Pgeboren>"]
-        xDoc = self.__class__._xDoc
+        xDoc = self.__class__._uno.openTemplateFromTDOC("fdo39694.ott")
         xEnumerationAccess = xDoc.getTextFields()
         xFieldEnum = xEnumerationAccess.createEnumeration()
         for xField in xFieldEnum:
@@ -35,9 +33,10 @@ class CheckFields(unittest.TestCase):
                 read_content = xAnchor.getString()
                 self.assertTrue(read_content in placeholders,
                                 "field %s is not contained: " % read_content)
+        xDoc.close(True)
 
     def test_fdo42073(self):
-        xDoc = self.__class__._xEmptyDoc
+        xDoc = self.__class__._uno.openEmptyWriterDoc()
         xBodyText = xDoc.getText()
         xCursor = xBodyText.createTextCursor()
         xTextField = xDoc.createInstance("com.sun.star.text.TextField.Input")
@@ -48,6 +47,7 @@ class CheckFields(unittest.TestCase):
         xTextField.setPropertyValue("Content", content)
         read_content = xTextField.getPropertyValue("Content")
         self.assertEqual(content, read_content)
+        xDoc.close(True)
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/sw/qa/python/check_flies.py b/sw/qa/python/check_flies.py
index 6353ccda150d..0e60b2195e89 100644
--- a/sw/qa/python/check_flies.py
+++ b/sw/qa/python/check_flies.py
@@ -26,18 +26,18 @@ class CheckFlies(unittest.TestCase):
     def setUpClass(cls):
         cls._uno = UnoInProcess()
         cls._uno.setUp()
-        cls.document = cls._uno.openDocFromTDOC("CheckFlies.odt")
 
     @classmethod
     def tearDownClass(cls):
         cls._uno.tearDown()
 
     def test_checkFlies(self):
-        xTFS = self.__class__.document
+        document = self.__class__._uno.openDocFromTDOC("CheckFlies.odt")
+        xTFS = document
         self.checkTextFrames(xTFS)
-        xTGOS = self.__class__.document
+        xTGOS = document
         self.checkGraphicFrames(xTGOS)
-        xTEOS = self.__class__.document
+        xTEOS = document
         self.checkEmbeddedFrames(xTEOS)
 
     def checkEmbeddedFrames(self, xTGOS):
diff --git a/sw/qa/python/check_indexed_property_values.py b/sw/qa/python/check_indexed_property_values.py
index 5609aa4225cb..ceaf82a6cac6 100644
--- a/sw/qa/python/check_indexed_property_values.py
+++ b/sw/qa/python/check_indexed_property_values.py
@@ -34,7 +34,6 @@ class CheckIndexedPropertyValues(unittest.TestCase):
         cls._uno = UnoInProcess()
         cls._uno.setUp()
         cls.xContext = cls._uno.getContext()
-        cls.xDoc = cls._uno.openEmptyWriterDoc()
 
     @classmethod
     def tearDownClass(cls):
diff --git a/sw/qa/python/check_named_property_values.py b/sw/qa/python/check_named_property_values.py
index dd06adc60313..1a81d13a6323 100644
--- a/sw/qa/python/check_named_property_values.py
+++ b/sw/qa/python/check_named_property_values.py
@@ -36,7 +36,6 @@ class CheckNamedPropertyValues(unittest.TestCase):
         cls._uno = UnoInProcess()
         cls._uno.setUp()
         cls.xContext = cls._uno.getContext()
-        cls.xDoc = cls._uno.openEmptyWriterDoc()
 
     @classmethod
     def tearDownClass(cls):
diff --git a/sw/qa/python/check_table.py b/sw/qa/python/check_table.py
index 35da08fe8195..8fd888f187bd 100644
--- a/sw/qa/python/check_table.py
+++ b/sw/qa/python/check_table.py
@@ -583,6 +583,8 @@ class CheckTable(unittest.TestCase):
         xCellRangeString = xChartDataProvider.convertRangeFromXML("Table1.$A$1:.$C$3")
         self.assertEqual("Table1.A1:C3", xCellRangeString)
 
+        xDoc.dispose()
+
     def test_splitRangeHorizontal(self):
         xDoc = CheckTable._uno.openEmptyWriterDoc()
         xTable = xDoc.createInstance("com.sun.star.text.TextTable")
@@ -600,6 +602,7 @@ class CheckTable(unittest.TestCase):
         self.assertTrue(math.isnan(xTable.Data[1][1]))
         self.assertTrue(math.isnan(xTable.Data[2][0]))
         self.assertTrue(math.isnan(xTable.Data[2][1]))
+        xDoc.dispose()
 
     def test_mergeRangeHorizontal(self):
         xDoc = CheckTable._uno.openEmptyWriterDoc()
@@ -618,6 +621,7 @@ class CheckTable(unittest.TestCase):
         self.assertEqual(xTable.Data[1][1], float(5))
         self.assertEqual(xTable.Data[1][2], float(6))
         self.assertEqual(xTable.Data[2], (float(7), float(8), float(9)))
+        xDoc.dispose()
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/sw/qa/python/get_expression.py b/sw/qa/python/get_expression.py
index 7462db68a730..98e9402bb602 100644
--- a/sw/qa/python/get_expression.py
+++ b/sw/qa/python/get_expression.py
@@ -22,6 +22,10 @@ class TestGetExpression(unittest.TestCase):
     @classmethod
     def tearDownClass(cls):
         cls._uno.tearDown()
+        # HACK in case cls._xDoc holds a UNO proxy to an SwXTextDocument (whose dtor calls
+        # Application::GetSolarMutex via sw::UnoImplPtrDeleter), which would potentially only be
+        # garbage-collected after VCL has already been deinitialized:
+        cls._xDoc = None
 
     def test_get_expression(self):
         self.__class__._uno.checkProperties(
diff --git a/sw/qa/python/set_expression.py b/sw/qa/python/set_expression.py
index 220952536fea..c5dc5e6ae2e9 100644
--- a/sw/qa/python/set_expression.py
+++ b/sw/qa/python/set_expression.py
@@ -18,15 +18,15 @@ class TestSetExpression(unittest.TestCase):
     def setUpClass(cls):
         cls._uno = UnoInProcess()
         cls._uno.setUp()
-        cls._xDoc = cls._uno.openEmptyWriterDoc()
 
     @classmethod
     def tearDownClass(cls):
         cls._uno.tearDown()
 
     def test_set_expression(self):
+        xDoc = self.__class__._uno.openEmptyWriterDoc()
         self.__class__._uno.checkProperties(
-            self.__class__._xDoc.createInstance("com.sun.star.text.textfield.SetExpression"),
+            xDoc.createInstance("com.sun.star.text.textfield.SetExpression"),
             {"NumberingType": 0,
              "Content": "foo",
              "CurrentPresentation": "bar",
diff --git a/sw/qa/python/text_portion_enumeration_test.py b/sw/qa/python/text_portion_enumeration_test.py
index 3a7e9d8586be..c379138db303 100644
--- a/sw/qa/python/text_portion_enumeration_test.py
+++ b/sw/qa/python/text_portion_enumeration_test.py
@@ -934,6 +934,11 @@ class TextPortionEnumerationTest(unittest.TestCase):
     @classmethod
     def tearDownClass(cls):
         cls.xDoc.close(True)
+        cls._uno.tearDown()
+        # HACK in case cls.xDoc holds a UNO proxy to an SwXTextDocument (whose dtor calls
+        # Application::GetSolarMutex via sw::UnoImplPtrDeleter), which would potentially only be
+        # garbage-collected after VCL has already been deinitialized:
+        cls.xDoc = None
 
     def test_text(self):
         root = TreeNode()
diff --git a/sw/qa/python/var_fields.py b/sw/qa/python/var_fields.py
index c2af7a40f408..52fe3ddd6938 100644
--- a/sw/qa/python/var_fields.py
+++ b/sw/qa/python/var_fields.py
@@ -19,7 +19,6 @@ class TestVarFields(unittest.TestCase):
     def setUpClass(cls):
         cls._uno = UnoInProcess()
         cls._uno.setUp()
-        cls._xDoc = cls._uno.openEmptyWriterDoc()
 
     @classmethod
     def tearDownClass(cls):
@@ -32,7 +31,7 @@ class TestVarFields(unittest.TestCase):
         sw/qa/complex/writer/VarFields.java
 
         """
-        xDoc = self.__class__._xDoc
+        xDoc = self.__class__._uno.openEmptyWriterDoc()
         xBodyText = xDoc.getText()
         xCursor = xBodyText.createTextCursor()
         # 0. create text field
diff --git a/test/source/bootstrapfixture.cxx b/test/source/bootstrapfixture.cxx
index a26b79a00c10..a8da11caed92 100644
--- a/test/source/bootstrapfixture.cxx
+++ b/test/source/bootstrapfixture.cxx
@@ -99,6 +99,12 @@ SAL_DLLPUBLIC_EXPORT void test_init(lang::XMultiServiceFactory *pFactory)
     catch (...) { abort(); }
 }
 
+// this is called from pyuno
+SAL_DLLPUBLIC_EXPORT void test_deinit()
+{
+    DeInitVCL();
+}
+
 } // extern "C"
 
 void test::BootstrapFixture::setUp()
diff --git a/unotest/source/python/org/libreoffice/unittest.py b/unotest/source/python/org/libreoffice/unittest.py
new file mode 100644
index 000000000000..364462ed3827
--- /dev/null
+++ b/unotest/source/python/org/libreoffice/unittest.py
@@ -0,0 +1,29 @@
+# -*- tab-width: 4; indent-tabs-mode: nil; py-indent-offset: 4 -*-
+#
+# This file is part of the LibreOffice project.
+#
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+
+import gc
+import pyuno
+import unittest
+
+class LoTestResult(unittest.TextTestResult):
+    def stopTestRun(self):
+        # HACK calling gc.collect() to get rid of as many still existing UNO proxies to
+        # SwXTextDocument and friends as possible; those C++ classes' dtors call
+        # Application::GetSolarMutex via sw::UnoImplPtrDeleter, so the dtors must be called before
+        # DeInitVCL in the call to pyuno.private_deinitTestEnvironment(); any remainging proxies
+        # that are still referenced (UnoInProcess' self.xDoc in
+        # unotest/source/python/org/libreoffice/unotest.py, or per-class variables in the various
+        # PythonTests) need to be individually released (each marked as "HACK" in the code):
+        gc.collect()
+        pyuno.private_deinitTestEnvironment()
+
+if __name__ == '__main__':
+    unittest.main(module=None, testRunner=unittest.TextTestRunner(resultclass=LoTestResult))
+
+# vim: set shiftwidth=4 softtabstop=4 expandtab:
diff --git a/unotest/source/python/org/libreoffice/unotest.py b/unotest/source/python/org/libreoffice/unotest.py
index 000a148b353f..804ddafc5518 100644
--- a/unotest/source/python/org/libreoffice/unotest.py
+++ b/unotest/source/python/org/libreoffice/unotest.py
@@ -246,7 +246,12 @@ class UnoInProcess:
     def postTest(self):
         assert(self.xContext)
     def tearDown(self):
-        self.xDoc.close(True)
+        if hasattr(self, 'xDoc'):
+            self.xDoc.close(True)
+            # HACK in case self.xDoc holds a UNO proxy to an SwXTextDocument (whose dtor calls
+            # Application::GetSolarMutex via sw::UnoImplPtrDeleter), which would potentially only be
+            # garbage-collected after VCL has already been deinitialized:
+            self.xDoc = None
 
 def simpleInvoke(connection, test):
     try:


More information about the Libreoffice-commits mailing list