Skip to content
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

Speed up unit tests #1336

Merged
merged 12 commits into from
Jul 20, 2021
Merged

Speed up unit tests #1336

merged 12 commits into from
Jul 20, 2021

Conversation

mssalvatore
Copy link
Collaborator

@mssalvatore mssalvatore commented Jul 19, 2021

What does this PR do?

Resolves #1125

This PR uses a number of tricks to speed up unit test execution:

  1. Remove an unnecessary 1 second sleep from test_monkey.py.
  2. Scope expensive fixtures at the session instead of test level.
  3. Reduce calls to expensive functions
  4. Avoid expensive imports where possible
  5. Use pytest markers to filter out slow tests.

Results

Collection Time

Collection time is sped up by 34%
image

Total Run Time

Total run time for the entire suite is sped up by 30%
image

Option to skip slow tests

For some slow tests, it would cost more to speed them up than it would save. A nice workaround for this is to use pytest markers to mark them as slow. They can then be skipped by running pytest -m 'not slow'. This speeds up the total runtime by 60%, at the expense of skipping 14 tests.

image

PR Checklist

  • Have you added an explanation of what your changes do and why you'd like to include them?
  • Is the TravisCI build passing?
  • Was the CHANGELOG.md updated to reflect the changes?
  • Was the documentation framework updated to reflect the changes?

Testing Checklist

  • Added relevant unit tests?
  • Have you successfully tested your changes locally? Elaborate:

    Tested by running unit tests

  • If applicable, add screenshots or log transcripts of the feature working

Calls to encrypt_string() result in calls to pyAesCrypt.encryptStream().
These calls are very slow (about .150ms). Modifying these tests to use
static ciphertext instead of encrypting the file each time saves
approximately 300ms when running the unit test suite.
This allows you to skip slow tests by running `pytest -m 'not slow'`.
WmiExploiter relies on impacket. Importing impacket is slow, which has a
negative impact on the speed of pytest collection. SSHExploiter is much
quicker to import.
The ZerologonExploiter relies on impacket. Importing impacket is slow
(approximately .72s). By moving the import statement in zerologon tests
and marking them as slow, the import (and tests) can now be skipped by
running `pytest -m 'not slow'`.
@codecov
Copy link

codecov bot commented Jul 19, 2021

Codecov Report

Merging #1336 (adb1006) into develop (81f7de7) will decrease coverage by 0.22%.
The diff coverage is 100.00%.

Impacted file tree graph

@@             Coverage Diff             @@
##           develop    #1336      +/-   ##
===========================================
- Coverage    40.31%   40.09%   -0.23%     
===========================================
  Files          475      474       -1     
  Lines        13874    13878       +4     
===========================================
- Hits          5593     5564      -29     
- Misses        8281     8314      +33     
Impacted Files Coverage Δ
monkey/monkey_island/cc/services/config.py 53.33% <100.00%> (ø)
monkey/tests/unit_tests/conftest.py 100.00% <100.00%> (ø)
.../unit_tests/monkey_island/cc/resources/conftest.py 100.00% <100.00%> (ø)
monkey/infection_monkey/exploit/wmiexec.py 0.00% <0.00%> (-28.38%) ⬇️
monkey/infection_monkey/exploit/tools/wmi_tools.py 0.00% <0.00%> (-23.66%) ⬇️
monkey/infection_monkey/exploit/tools/smb_tools.py 0.00% <0.00%> (-15.11%) ⬇️
monkey/infection_monkey/exploit/sshexec.py 21.67% <0.00%> (+21.67%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 81f7de7...adb1006. Read the comment docs.

@@ -360,7 +360,7 @@ def _encrypt_or_decrypt_config(config, is_decrypt=False):
parent_config_arr = config_arr
config_arr = config_arr[config_key_part]

if isinstance(config_arr, collections.Sequence) and not isinstance(config_arr, str):
if isinstance(config_arr, collections.abc.Sequence) and not isinstance(config_arr, str):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the rationale behind this?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

@VakarisZ
Copy link
Contributor

Did you do any more intricate profiling than just running the pytest? With --collect-only I get 326 tests collected in 2.72s. Without it I get 3 failed, 315 passed, 8 skipped, 2 warnings in 5.73s. Looks like about half of the time is spent collecting the tests 🤔I've ran the profiler and most time seems to be spent doing os.stat, not sure if and how we can optimize it.

Copy link
Contributor

@shreyamalviya shreyamalviya left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

# Emulate timeout - ttl is manually deleted here, since we're using mongomock and not a
# real mongo instance.
sleep(1)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was this here in the first place?

Copy link
Collaborator Author

@mssalvatore mssalvatore Jul 20, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

¯\(ツ)

@@ -2,7 +2,7 @@

import pytest

from infection_monkey.exploit.wmiexec import WmiExploiter
from infection_monkey.exploit.sshexec import SSHExploiter
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we changing the exploiter ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See commit messages: 2496ed0

@mssalvatore
Copy link
Collaborator Author

@VakarisZ

Did you do any more intricate profiling than just running the pytest?

I did.

With --collect-only I get 326 tests collected in 2.72s. Without it I get 3 failed, 315 passed, 8 skipped, 2 warnings in 5.73s.

Did this PR introduce failures for you?

Looks like about half of the time is spent collecting the tests. I've ran the profiler and most time seems to be spent doing os.stat, not sure if and how we can optimize it.

Most of the collection time is because of slow imports. The biggest one is, impacket. For the most part, we're not testing the code that uses impacket, but it still gets imported. I was able to avoid importing impacket if you run pytest -m 'not slow', but actually fixing the issue would involve a significant refactor of the ZeroLogon exploiter.

Some other imports are also noticeably slow, but due to some tight coupling in the system, it's not trivial to avoid those imports.

Below are some nicely formatted cProfile results from when pytest is run with --collect-only
image

@mssalvatore mssalvatore merged commit 5f31822 into develop Jul 20, 2021
@mssalvatore mssalvatore deleted the speed-up-unit-tests branch July 20, 2021 13:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Speed up unit test suite
4 participants