-
Notifications
You must be signed in to change notification settings - Fork 451
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix print issue on core manager repeated test #7511
Conversation
The test, after being modified, appears to be logically similar to its previous version. Could you elaborate on why employing a context manager is deemed to be more effective compared to the decorator? The correct version with the decorator could be: @patch('builtins.print', new_callable=MagicMock, side_effect=OSError())
def test_on_core_read_ready_os_error_suppressed(mocked_print: MagicMock, core_manager):
# OSError exceptions when writing to stdout and stderr are suppressed
core_manager.app_manager.quitting_app = False
core_manager.on_core_stdout_read_ready()
core_manager.on_core_stderr_read_ready()
assert mocked_print.call_count == 2
# if app is quitting, core_manager does not write to stdout/stderr at all, and so the call counter does not grow
core_manager.app_manager.quitting_app = True
core_manager.on_core_stdout_read_ready()
core_manager.on_core_stderr_read_ready()
assert mocked_print.call_count == 2 |
Related to #7495 |
The real fix is to use the mocked print. The mocked object provided by a context manager patch or patch decorator should be logically the same.
The use of context manager is opinionated to make it a bit more explicit. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This PR presents a resolution to the issue by utilizing mocked_print as opposed to the real one. Great discovery!
However, the choice between using a context manager and a decorator strikes me as more of a personal preference (I personally would vote for the decorator). As such, I propose we respect the original author's (@kozlovsky) choice of using the decorator.
As demonstrated below, it's possible to rectify the test while maintaining the original choice.
@drew2a @xoriole The version with the So, I prefer the context manager version now |
Would the next logical step then be to rewrite all tests, transitioning from the use of decorators to context managers? |
IMO, it is just a single case and I would not think rewriting all tests is necessary. |
I mean, if we wrap a test with @patch("some.function", MagicMock(...))
def test_function(mock):
... We are creating a singleton mock object that we re-use if we re-run the same test multiple times. The fact that this mock object is a singleton that is re-used multiple times is not entirely obvious. When we use When the
If we really want to re-run each test multiple times, then yes, I think it is logical to replace all mocks that can break if they are singletons to mocks in But the bigger question is, should we really re-run the tests multiple times using this approach? We re-run tests only because we want to determine flaky tests. But flaky tests when the entire test suite is started for a single time are flaky for a different reason than when we use I think it may be better for us to find another way of re-running the same test multiple times when the global variables and singleton mocks do not affect the test execution. If we want to find flaky tests, it is better to re-run the entire test suite multiple times and not the individual tests inside a single test suite run. |
The issue did not lie in how the mock was defined, but in the way the print calls count was verified. As @xoriole pointed out,
The decorator functions properly with multiple calls, as confirmed by the fact that all other tests operate correctly. The construct used in the following example: @patch('builtins.print', new_callable=MagicMock, side_effect=OSError())
def test_on_core_read_ready_os_error_suppressed(mocked_print: MagicMock, core_manager):
... was chosen because it allows the use of a mocked object (in this case, The example you've given is not accurate. A mocked object cannot be used in the following way: @patch("some.function", MagicMock(...))
def test_function(mock):
... If this kind of definition is employed, it should be written as: @patch("some.function", MagicMock(...))
def test_function():
... |
By transitioning from a decorator to a context manager, the consistency of the tests is diminished. Currently, most tests in this file use decorators, with only one employing a context manager. This could lead to confusion for future readers of this file, prompting them to question why one test differs from the others. Is there a valid reason for this disparity? By maintaining a consistent style for our tests, we can preemptively address these types of questions. |
@drew2a First, sorry for the mistake; I misremember the API of @patch when the
Here, I have an impression our understanding of the reason for the problem is different. My point is that the mocked print was used all the time, even in the original version of the test. In the original version of the test: @patch('builtins.print', MagicMock(side_effect=OSError()))
def test_on_core_read_ready_os_error_suppressed(core_manager):
...
assert print.call_count == 2 In the expression When
@patch('tribler.core.sentry_reporter.sentry_reporter.sentry_sdk.init')
def test_init(mocked_init: Mock, sentry_reporter: SentryReporter):
...
@patch('binascii.unhexlify', new=Mock(side_effect=binascii.Error))
def test_parse_magnetlink_binascii_error_40(caplog):
...
@patch.object(AsyncGroup, 'cancel', new_callable=AsyncMock)
async def test_multiple_shutdown_calls(async_group_cancel: AsyncMock):
...
async_group_cancel.assert_called_once() In all three forms, the mock object is created and used instead of the original object/function. In the second form, when the @patch('binascii.unhexlify', new=Mock(side_effect=binascii.Error))
def test_parse_magnetlink_binascii_error_40(caplog):
... can be rewritten as SINGLETON_MOCK=Mock(side_effect=binascii.Error)
@patch('binascii.unhexlify', new=SINGLETON_MOCK)
def test_parse_magnetlink_binascii_error_40(caplog):
...
assert binascii.unhexlify is SINGLETON_MOCK
SINGLETON_MOCK.assert_called() Inside the test, there is a mock object, and we can access it using Consider a similar test with the same problem: @patch_import(['faulthandler'], strict=True, enable=MagicMock())
@patch('tribler.core.check_os.open', new=MagicMock())
def test_enable_fault_handler():
import faulthandler
enable_fault_handler(log_dir=MagicMock())
faulthandler.enable.assert_called_once() Here, we test SINGLETON_MOCK=MagicMock()
@patch_import(['faulthandler'], strict=True, enable=SINGLETON_MOCK)
@patch('tribler.core.check_os.open', new=MagicMock())
def test_enable_fault_handler():
import faulthandler
enable_fault_handler(log_dir=MagicMock())
SINGLETON_MOCK.assert_called_once() And this function has the same problem with repeated test calls; it works correctly on the first call only. But it uses the mock, so the problem is not that the mock is not used; the problem is the mock is not re-created each time the function is called. So we need to have a fresh mock in order to have the correct result for
In this specific case, as the @patch_import(['faulthandler'], strict=True, enable=MagicMock())
@patch('tribler.core.check_os.open', new=MagicMock())
def test_enable_fault_handler():
import faulthandler
faulthandler.enable.reset_mock() # allows to re-run the same test multiple times
enable_fault_handler(log_dir=MagicMock())
faulthandler.enable.assert_called_once() And I actually like the I actually do not have objections to the So, if we believe that |
b9b038b
to
e9d9bcf
Compare
Fixes #7510
Instead of patch decorator, context manager is used and mocked print object is used to check the call count instead of builtin print.