mirror of
https://github.com/python/cpython.git
synced 2025-08-31 14:07:50 +00:00
Merged revisions 70768,71657,71721,71729,71794,71976,72036-72037,72079,72085,72131-72134,72191,72197-72198,72219,72221,72225,72303,72434,72467,72476 via svnmerge from
svn+ssh://pythondev@svn.python.org/python/trunk ........ r70768 | andrew.kuchling | 2009-03-30 17:29:15 -0500 (Mon, 30 Mar 2009) | 1 line Typo fixes ........ r71657 | vinay.sajip | 2009-04-16 14:07:37 -0500 (Thu, 16 Apr 2009) | 1 line Issue #5768: Change to Unicode output logic and test case for same. ........ r71721 | benjamin.peterson | 2009-04-18 14:26:19 -0500 (Sat, 18 Apr 2009) | 1 line fix a few nits in unittest.py #5771 ........ r71729 | benjamin.peterson | 2009-04-18 16:03:10 -0500 (Sat, 18 Apr 2009) | 1 line move test to a more appropiate one ........ r71794 | vinay.sajip | 2009-04-22 07:10:47 -0500 (Wed, 22 Apr 2009) | 2 lines Issue #5170: Fixed regression caused when fixing #5768. ........ r71976 | mark.dickinson | 2009-04-26 14:54:55 -0500 (Sun, 26 Apr 2009) | 2 lines Fix typo in function name ........ r72036 | georg.brandl | 2009-04-27 12:04:23 -0500 (Mon, 27 Apr 2009) | 1 line #5848: small unittest doc patch. ........ r72037 | georg.brandl | 2009-04-27 12:09:53 -0500 (Mon, 27 Apr 2009) | 1 line #5840: dont claim we dont support TLS. ........ r72079 | r.david.murray | 2009-04-28 14:02:55 -0500 (Tue, 28 Apr 2009) | 2 lines Remove spurious 'u'. ........ r72085 | georg.brandl | 2009-04-28 16:48:35 -0500 (Tue, 28 Apr 2009) | 1 line Make the doctests in the docs pass, except for those in the turtle module. ........ r72131 | benjamin.peterson | 2009-04-29 17:43:35 -0500 (Wed, 29 Apr 2009) | 1 line fix test_shutil on ZFS #5676 ........ r72132 | georg.brandl | 2009-04-29 17:44:07 -0500 (Wed, 29 Apr 2009) | 1 line #5878: fix repr of re object. ........ r72133 | benjamin.peterson | 2009-04-29 17:44:15 -0500 (Wed, 29 Apr 2009) | 1 line make sure mode is removable while cleaning up test droppings ........ r72134 | benjamin.peterson | 2009-04-29 19:06:33 -0500 (Wed, 29 Apr 2009) | 1 line make sure to close file ........ r72191 | michael.foord | 2009-05-02 06:43:06 -0500 (Sat, 02 May 2009) | 9 lines Adds an exit parameter to unittest.main(). If False main no longer calls sys.exit. Closes issue 3379. Michael Foord ........ r72197 | benjamin.peterson | 2009-05-02 11:24:37 -0500 (Sat, 02 May 2009) | 1 line don't let sys.argv be used in the tests ........ r72198 | andrew.kuchling | 2009-05-02 12:12:15 -0500 (Sat, 02 May 2009) | 1 line Add items ........ r72219 | michael.foord | 2009-05-02 15:15:05 -0500 (Sat, 02 May 2009) | 8 lines Add addCleanup and doCleanups to unittest.TestCase. Closes issue 5679. Michael Foord ........ r72221 | benjamin.peterson | 2009-05-02 15:26:53 -0500 (Sat, 02 May 2009) | 1 line add myself ........ r72225 | michael.foord | 2009-05-02 17:43:34 -0500 (Sat, 02 May 2009) | 1 line ........ r72303 | benjamin.peterson | 2009-05-04 19:55:24 -0500 (Mon, 04 May 2009) | 1 line using sys._getframe(x), where x > 0 doesnt' work on IronPython ........ r72434 | r.david.murray | 2009-05-07 13:09:58 -0500 (Thu, 07 May 2009) | 2 lines Pre-opened test file needs to be opened in binary mode. ........ r72467 | georg.brandl | 2009-05-08 07:17:34 -0500 (Fri, 08 May 2009) | 1 line Fix name. ........ r72476 | thomas.heller | 2009-05-08 15:09:40 -0500 (Fri, 08 May 2009) | 4 lines Add a file that contains diffs between offical libffi files and the files in this repository. Should make it easier to merge new libffi versions. ........
This commit is contained in:
parent
8a282d175b
commit
25c95f1298
22 changed files with 910 additions and 146 deletions
|
@ -9,9 +9,10 @@ Still need testing:
|
|||
import re
|
||||
from test import support
|
||||
import unittest
|
||||
from unittest import TestCase
|
||||
from unittest import TestCase, TestProgram
|
||||
import types
|
||||
from copy import deepcopy
|
||||
import io
|
||||
|
||||
### Support code
|
||||
################################################################
|
||||
|
@ -25,10 +26,18 @@ class LoggingResult(unittest.TestResult):
|
|||
self._events.append('startTest')
|
||||
super().startTest(test)
|
||||
|
||||
def startTestRun(self):
|
||||
self._events.append('startTestRun')
|
||||
super(LoggingResult, self).startTestRun()
|
||||
|
||||
def stopTest(self, test):
|
||||
self._events.append('stopTest')
|
||||
super().stopTest(test)
|
||||
|
||||
def stopTestRun(self):
|
||||
self._events.append('stopTestRun')
|
||||
super(LoggingResult, self).stopTestRun()
|
||||
|
||||
def addFailure(self, *args):
|
||||
self._events.append('addFailure')
|
||||
super().addFailure(*args)
|
||||
|
@ -1826,6 +1835,12 @@ class Test_TestResult(TestCase):
|
|||
self.assertEqual(result.testsRun, 1)
|
||||
self.assertEqual(result.shouldStop, False)
|
||||
|
||||
# "Called before and after tests are run. The default implementation does nothing."
|
||||
def test_startTestRun_stopTestRun(self):
|
||||
result = unittest.TestResult()
|
||||
result.startTestRun()
|
||||
result.stopTestRun()
|
||||
|
||||
# "addSuccess(test)"
|
||||
# ...
|
||||
# "Called when the test case test succeeds"
|
||||
|
@ -1973,6 +1988,53 @@ class Foo(unittest.TestCase):
|
|||
class Bar(Foo):
|
||||
def test2(self): pass
|
||||
|
||||
class LoggingTestCase(unittest.TestCase):
|
||||
"""A test case which logs its calls."""
|
||||
|
||||
def __init__(self, events):
|
||||
super(LoggingTestCase, self).__init__('test')
|
||||
self.events = events
|
||||
|
||||
def setUp(self):
|
||||
self.events.append('setUp')
|
||||
|
||||
def test(self):
|
||||
self.events.append('test')
|
||||
|
||||
def tearDown(self):
|
||||
self.events.append('tearDown')
|
||||
|
||||
class ResultWithNoStartTestRunStopTestRun(object):
|
||||
"""An object honouring TestResult before startTestRun/stopTestRun."""
|
||||
|
||||
def __init__(self):
|
||||
self.failures = []
|
||||
self.errors = []
|
||||
self.testsRun = 0
|
||||
self.skipped = []
|
||||
self.expectedFailures = []
|
||||
self.unexpectedSuccesses = []
|
||||
self.shouldStop = False
|
||||
|
||||
def startTest(self, test):
|
||||
pass
|
||||
|
||||
def stopTest(self, test):
|
||||
pass
|
||||
|
||||
def addError(self, test):
|
||||
pass
|
||||
|
||||
def addFailure(self, test):
|
||||
pass
|
||||
|
||||
def addSuccess(self, test):
|
||||
pass
|
||||
|
||||
def wasSuccessful(self):
|
||||
return True
|
||||
|
||||
|
||||
################################################################
|
||||
### /Support code for Test_TestCase
|
||||
|
||||
|
@ -2067,21 +2129,32 @@ class Test_TestCase(TestCase, TestEquality, TestHashing):
|
|||
events = []
|
||||
result = LoggingResult(events)
|
||||
|
||||
class Foo(unittest.TestCase):
|
||||
class Foo(LoggingTestCase):
|
||||
def setUp(self):
|
||||
events.append('setUp')
|
||||
super(Foo, self).setUp()
|
||||
raise RuntimeError('raised by Foo.setUp')
|
||||
|
||||
def test(self):
|
||||
events.append('test')
|
||||
|
||||
def tearDown(self):
|
||||
events.append('tearDown')
|
||||
|
||||
Foo('test').run(result)
|
||||
Foo(events).run(result)
|
||||
expected = ['startTest', 'setUp', 'addError', 'stopTest']
|
||||
self.assertEqual(events, expected)
|
||||
|
||||
# "With a temporary result stopTestRun is called when setUp errors.
|
||||
def test_run_call_order__error_in_setUp_default_result(self):
|
||||
events = []
|
||||
|
||||
class Foo(LoggingTestCase):
|
||||
def defaultTestResult(self):
|
||||
return LoggingResult(self.events)
|
||||
|
||||
def setUp(self):
|
||||
super(Foo, self).setUp()
|
||||
raise RuntimeError('raised by Foo.setUp')
|
||||
|
||||
Foo(events).run()
|
||||
expected = ['startTestRun', 'startTest', 'setUp', 'addError',
|
||||
'stopTest', 'stopTestRun']
|
||||
self.assertEqual(events, expected)
|
||||
|
||||
# "When a setUp() method is defined, the test runner will run that method
|
||||
# prior to each test. Likewise, if a tearDown() method is defined, the
|
||||
# test runner will invoke that method after each test. In the example,
|
||||
|
@ -2093,20 +2166,32 @@ class Test_TestCase(TestCase, TestEquality, TestHashing):
|
|||
events = []
|
||||
result = LoggingResult(events)
|
||||
|
||||
class Foo(unittest.TestCase):
|
||||
def setUp(self):
|
||||
events.append('setUp')
|
||||
|
||||
class Foo(LoggingTestCase):
|
||||
def test(self):
|
||||
events.append('test')
|
||||
super(Foo, self).test()
|
||||
raise RuntimeError('raised by Foo.test')
|
||||
|
||||
def tearDown(self):
|
||||
events.append('tearDown')
|
||||
|
||||
expected = ['startTest', 'setUp', 'test', 'addError', 'tearDown',
|
||||
'stopTest']
|
||||
Foo('test').run(result)
|
||||
Foo(events).run(result)
|
||||
self.assertEqual(events, expected)
|
||||
|
||||
# "With a default result, an error in the test still results in stopTestRun
|
||||
# being called."
|
||||
def test_run_call_order__error_in_test_default_result(self):
|
||||
events = []
|
||||
|
||||
class Foo(LoggingTestCase):
|
||||
def defaultTestResult(self):
|
||||
return LoggingResult(self.events)
|
||||
|
||||
def test(self):
|
||||
super(Foo, self).test()
|
||||
raise RuntimeError('raised by Foo.test')
|
||||
|
||||
expected = ['startTestRun', 'startTest', 'setUp', 'test', 'addError',
|
||||
'tearDown', 'stopTest', 'stopTestRun']
|
||||
Foo(events).run()
|
||||
self.assertEqual(events, expected)
|
||||
|
||||
# "When a setUp() method is defined, the test runner will run that method
|
||||
|
@ -2120,20 +2205,30 @@ class Test_TestCase(TestCase, TestEquality, TestHashing):
|
|||
events = []
|
||||
result = LoggingResult(events)
|
||||
|
||||
class Foo(unittest.TestCase):
|
||||
def setUp(self):
|
||||
events.append('setUp')
|
||||
|
||||
class Foo(LoggingTestCase):
|
||||
def test(self):
|
||||
events.append('test')
|
||||
super(Foo, self).test()
|
||||
self.fail('raised by Foo.test')
|
||||
|
||||
def tearDown(self):
|
||||
events.append('tearDown')
|
||||
|
||||
expected = ['startTest', 'setUp', 'test', 'addFailure', 'tearDown',
|
||||
'stopTest']
|
||||
Foo('test').run(result)
|
||||
Foo(events).run(result)
|
||||
self.assertEqual(events, expected)
|
||||
|
||||
# "When a test fails with a default result stopTestRun is still called."
|
||||
def test_run_call_order__failure_in_test_default_result(self):
|
||||
|
||||
class Foo(LoggingTestCase):
|
||||
def defaultTestResult(self):
|
||||
return LoggingResult(self.events)
|
||||
def test(self):
|
||||
super(Foo, self).test()
|
||||
self.fail('raised by Foo.test')
|
||||
|
||||
expected = ['startTestRun', 'startTest', 'setUp', 'test', 'addFailure',
|
||||
'tearDown', 'stopTest', 'stopTestRun']
|
||||
events = []
|
||||
Foo(events).run()
|
||||
self.assertEqual(events, expected)
|
||||
|
||||
# "When a setUp() method is defined, the test runner will run that method
|
||||
|
@ -2147,22 +2242,44 @@ class Test_TestCase(TestCase, TestEquality, TestHashing):
|
|||
events = []
|
||||
result = LoggingResult(events)
|
||||
|
||||
class Foo(unittest.TestCase):
|
||||
def setUp(self):
|
||||
events.append('setUp')
|
||||
|
||||
def test(self):
|
||||
events.append('test')
|
||||
|
||||
class Foo(LoggingTestCase):
|
||||
def tearDown(self):
|
||||
events.append('tearDown')
|
||||
super(Foo, self).tearDown()
|
||||
raise RuntimeError('raised by Foo.tearDown')
|
||||
|
||||
Foo('test').run(result)
|
||||
Foo(events).run(result)
|
||||
expected = ['startTest', 'setUp', 'test', 'tearDown', 'addError',
|
||||
'stopTest']
|
||||
self.assertEqual(events, expected)
|
||||
|
||||
# "When tearDown errors with a default result stopTestRun is still called."
|
||||
def test_run_call_order__error_in_tearDown_default_result(self):
|
||||
|
||||
class Foo(LoggingTestCase):
|
||||
def defaultTestResult(self):
|
||||
return LoggingResult(self.events)
|
||||
def tearDown(self):
|
||||
super(Foo, self).tearDown()
|
||||
raise RuntimeError('raised by Foo.tearDown')
|
||||
|
||||
events = []
|
||||
Foo(events).run()
|
||||
expected = ['startTestRun', 'startTest', 'setUp', 'test', 'tearDown',
|
||||
'addError', 'stopTest', 'stopTestRun']
|
||||
self.assertEqual(events, expected)
|
||||
|
||||
# "TestCase.run() still works when the defaultTestResult is a TestResult
|
||||
# that does not support startTestRun and stopTestRun.
|
||||
def test_run_call_order_default_result(self):
|
||||
|
||||
class Foo(unittest.TestCase):
|
||||
def defaultTestResult(self):
|
||||
return ResultWithNoStartTestRunStopTestRun()
|
||||
def test(self):
|
||||
pass
|
||||
|
||||
Foo('test').run()
|
||||
|
||||
# "This class attribute gives the exception raised by the test() method.
|
||||
# If a test framework needs to use a specialized exception, possibly to
|
||||
# carry additional information, it must subclass this exception in
|
||||
|
@ -2253,7 +2370,9 @@ class Test_TestCase(TestCase, TestEquality, TestHashing):
|
|||
self.failUnless(isinstance(Foo().id(), str))
|
||||
|
||||
# "If result is omitted or None, a temporary result object is created
|
||||
# and used, but is not made available to the caller"
|
||||
# and used, but is not made available to the caller. As TestCase owns the
|
||||
# temporary result startTestRun and stopTestRun are called.
|
||||
|
||||
def test_run__uses_defaultTestResult(self):
|
||||
events = []
|
||||
|
||||
|
@ -2267,7 +2386,8 @@ class Test_TestCase(TestCase, TestEquality, TestHashing):
|
|||
# Make run() find a result object on its own
|
||||
Foo('test').run()
|
||||
|
||||
expected = ['startTest', 'test', 'addSuccess', 'stopTest']
|
||||
expected = ['startTestRun', 'startTest', 'test', 'addSuccess',
|
||||
'stopTest', 'stopTestRun']
|
||||
self.assertEqual(events, expected)
|
||||
|
||||
def testShortDescriptionWithoutDocstring(self):
|
||||
|
@ -3012,6 +3132,220 @@ class TestLongMessage(TestCase):
|
|||
"^unexpectedly identical: None : oops$"])
|
||||
|
||||
|
||||
class TestCleanUp(TestCase):
|
||||
|
||||
def testCleanUp(self):
|
||||
class TestableTest(TestCase):
|
||||
def testNothing(self):
|
||||
pass
|
||||
|
||||
test = TestableTest('testNothing')
|
||||
self.assertEqual(test._cleanups, [])
|
||||
|
||||
cleanups = []
|
||||
|
||||
def cleanup1(*args, **kwargs):
|
||||
cleanups.append((1, args, kwargs))
|
||||
|
||||
def cleanup2(*args, **kwargs):
|
||||
cleanups.append((2, args, kwargs))
|
||||
|
||||
test.addCleanup(cleanup1, 1, 2, 3, four='hello', five='goodbye')
|
||||
test.addCleanup(cleanup2)
|
||||
|
||||
self.assertEqual(test._cleanups,
|
||||
[(cleanup1, (1, 2, 3), dict(four='hello', five='goodbye')),
|
||||
(cleanup2, (), {})])
|
||||
|
||||
result = test.doCleanups()
|
||||
self.assertTrue(result)
|
||||
|
||||
self.assertEqual(cleanups, [(2, (), {}), (1, (1, 2, 3), dict(four='hello', five='goodbye'))])
|
||||
|
||||
def testCleanUpWithErrors(self):
|
||||
class TestableTest(TestCase):
|
||||
def testNothing(self):
|
||||
pass
|
||||
|
||||
class MockResult(object):
|
||||
errors = []
|
||||
def addError(self, test, exc_info):
|
||||
self.errors.append((test, exc_info))
|
||||
|
||||
result = MockResult()
|
||||
test = TestableTest('testNothing')
|
||||
test._result = result
|
||||
|
||||
exc1 = Exception('foo')
|
||||
exc2 = Exception('bar')
|
||||
def cleanup1():
|
||||
raise exc1
|
||||
|
||||
def cleanup2():
|
||||
raise exc2
|
||||
|
||||
test.addCleanup(cleanup1)
|
||||
test.addCleanup(cleanup2)
|
||||
|
||||
self.assertFalse(test.doCleanups())
|
||||
|
||||
(test1, (Type1, instance1, _)), (test2, (Type2, instance2, _)) = reversed(MockResult.errors)
|
||||
self.assertEqual((test1, Type1, instance1), (test, Exception, exc1))
|
||||
self.assertEqual((test2, Type2, instance2), (test, Exception, exc2))
|
||||
|
||||
def testCleanupInRun(self):
|
||||
blowUp = False
|
||||
ordering = []
|
||||
|
||||
class TestableTest(TestCase):
|
||||
def setUp(self):
|
||||
ordering.append('setUp')
|
||||
if blowUp:
|
||||
raise Exception('foo')
|
||||
|
||||
def testNothing(self):
|
||||
ordering.append('test')
|
||||
|
||||
def tearDown(self):
|
||||
ordering.append('tearDown')
|
||||
|
||||
test = TestableTest('testNothing')
|
||||
|
||||
def cleanup1():
|
||||
ordering.append('cleanup1')
|
||||
def cleanup2():
|
||||
ordering.append('cleanup2')
|
||||
test.addCleanup(cleanup1)
|
||||
test.addCleanup(cleanup2)
|
||||
|
||||
def success(some_test):
|
||||
self.assertEqual(some_test, test)
|
||||
ordering.append('success')
|
||||
|
||||
result = unittest.TestResult()
|
||||
result.addSuccess = success
|
||||
|
||||
test.run(result)
|
||||
self.assertEqual(ordering, ['setUp', 'test', 'tearDown',
|
||||
'cleanup2', 'cleanup1', 'success'])
|
||||
|
||||
blowUp = True
|
||||
ordering = []
|
||||
test = TestableTest('testNothing')
|
||||
test.addCleanup(cleanup1)
|
||||
test.run(result)
|
||||
self.assertEqual(ordering, ['setUp', 'cleanup1'])
|
||||
|
||||
|
||||
class Test_TestProgram(TestCase):
|
||||
|
||||
# Horrible white box test
|
||||
def testNoExit(self):
|
||||
result = object()
|
||||
test = object()
|
||||
|
||||
class FakeRunner(object):
|
||||
def run(self, test):
|
||||
self.test = test
|
||||
return result
|
||||
|
||||
runner = FakeRunner()
|
||||
|
||||
try:
|
||||
oldParseArgs = TestProgram.parseArgs
|
||||
TestProgram.parseArgs = lambda *args: None
|
||||
TestProgram.test = test
|
||||
|
||||
program = TestProgram(testRunner=runner, exit=False)
|
||||
|
||||
self.assertEqual(program.result, result)
|
||||
self.assertEqual(runner.test, test)
|
||||
|
||||
finally:
|
||||
TestProgram.parseArgs = oldParseArgs
|
||||
del TestProgram.test
|
||||
|
||||
|
||||
class FooBar(unittest.TestCase):
|
||||
def testPass(self):
|
||||
assert True
|
||||
def testFail(self):
|
||||
assert False
|
||||
|
||||
class FooBarLoader(unittest.TestLoader):
|
||||
"""Test loader that returns a suite containing FooBar."""
|
||||
def loadTestsFromModule(self, module):
|
||||
return self.suiteClass(
|
||||
[self.loadTestsFromTestCase(Test_TestProgram.FooBar)])
|
||||
|
||||
|
||||
def test_NonExit(self):
|
||||
program = unittest.main(exit=False,
|
||||
argv=["foobar"],
|
||||
testRunner=unittest.TextTestRunner(stream=io.StringIO()),
|
||||
testLoader=self.FooBarLoader())
|
||||
self.assertTrue(hasattr(program, 'result'))
|
||||
|
||||
|
||||
def test_Exit(self):
|
||||
self.assertRaises(
|
||||
SystemExit,
|
||||
unittest.main,
|
||||
argv=["foobar"],
|
||||
testRunner=unittest.TextTestRunner(stream=io.StringIO()),
|
||||
exit=True,
|
||||
testLoader=self.FooBarLoader())
|
||||
|
||||
|
||||
def test_ExitAsDefault(self):
|
||||
self.assertRaises(
|
||||
SystemExit,
|
||||
unittest.main,
|
||||
argv=["foobar"],
|
||||
testRunner=unittest.TextTestRunner(stream=io.StringIO()),
|
||||
testLoader=self.FooBarLoader())
|
||||
|
||||
|
||||
class Test_TextTestRunner(TestCase):
|
||||
"""Tests for TextTestRunner."""
|
||||
|
||||
def test_works_with_result_without_startTestRun_stopTestRun(self):
|
||||
class OldTextResult(ResultWithNoStartTestRunStopTestRun):
|
||||
separator2 = ''
|
||||
def printErrors(self):
|
||||
pass
|
||||
|
||||
class Runner(unittest.TextTestRunner):
|
||||
def __init__(self):
|
||||
super(Runner, self).__init__(io.StringIO())
|
||||
|
||||
def _makeResult(self):
|
||||
return OldTextResult()
|
||||
|
||||
runner = Runner()
|
||||
runner.run(unittest.TestSuite())
|
||||
|
||||
def test_startTestRun_stopTestRun_called(self):
|
||||
class LoggingTextResult(LoggingResult):
|
||||
separator2 = ''
|
||||
def printErrors(self):
|
||||
pass
|
||||
|
||||
class LoggingRunner(unittest.TextTestRunner):
|
||||
def __init__(self, events):
|
||||
super(LoggingRunner, self).__init__(io.StringIO())
|
||||
self._events = events
|
||||
|
||||
def _makeResult(self):
|
||||
return LoggingTextResult(self._events)
|
||||
|
||||
events = []
|
||||
runner = LoggingRunner(events)
|
||||
runner.run(unittest.TestSuite())
|
||||
expected = ['startTestRun', 'stopTestRun']
|
||||
self.assertEqual(events, expected)
|
||||
|
||||
|
||||
######################################################################
|
||||
## Main
|
||||
######################################################################
|
||||
|
@ -3019,7 +3353,8 @@ class TestLongMessage(TestCase):
|
|||
def test_main():
|
||||
support.run_unittest(Test_TestCase, Test_TestLoader,
|
||||
Test_TestSuite, Test_TestResult, Test_FunctionTestCase,
|
||||
Test_TestSkipping, Test_Assertions, TestLongMessage)
|
||||
Test_TestSkipping, Test_Assertions, TestLongMessage,
|
||||
Test_TestProgram, TestCleanUp)
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_main()
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue