From 028e4584c2364440f8d352fb4961a3394e32e983 Mon Sep 17 00:00:00 2001 From: Vladimir Lagunov Date: Tue, 19 Jan 2016 14:35:02 +0600 Subject: [PATCH 1/4] fix: avoid errors when filename is too long --- pytest-profiling/pytest_profiling.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/pytest-profiling/pytest_profiling.py b/pytest-profiling/pytest_profiling.py index a5fa4c1b2f..098351f653 100644 --- a/pytest-profiling/pytest_profiling.py +++ b/pytest-profiling/pytest_profiling.py @@ -5,6 +5,8 @@ import cProfile import pstats import pipes +import errno +from hashlib import md5 class Profiling(object): @@ -50,9 +52,22 @@ def pytest_pyfunc_call(self, __multicall__, pyfuncitem): """Hook into pytest_pyfunc_call; marked as a tryfirst hook so that we can call everyone else inside `cProfile.runctx`. """ - prof = os.path.join("prof", pyfuncitem.name + ".prof") - cProfile.runctx("fn()", globals(), dict(fn=__multicall__.execute), filename=prof) - self.profs.append(prof) + prof = cProfile.Profile() + prof.runctx("fn()", globals(), dict(fn=__multicall__.execute)) + prof_filename = os.path.join("prof", pyfuncitem.name + ".prof") + try: + prof.dump_stats(prof_filename) + except EnvironmentError as err: + if err.errno != errno.ENAMETOOLONG: + raise + + hash_len = 8 + if len(pyfuncitem.name) < hash_len: + raise + + hash_str = md5(pyfuncitem.name).hexdigest()[:hash_len] + prof_filename = os.path.join("prof", hash_str + ".prof") + prof.dump_stats(prof_filename) def pytest_addoption(parser): From f9e7275a8ab924543bc812838b57d3305d86d276 Mon Sep 17 00:00:00 2001 From: Vladimir Lagunov Date: Thu, 21 Jan 2016 13:41:58 +0600 Subject: [PATCH 2/4] LARGE_FILENAME_HASH_LEN constant --- pytest-profiling/pytest_profiling.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pytest-profiling/pytest_profiling.py b/pytest-profiling/pytest_profiling.py index 098351f653..517b0430ee 100644 --- a/pytest-profiling/pytest_profiling.py +++ b/pytest-profiling/pytest_profiling.py @@ -9,6 +9,9 @@ from hashlib import md5 +LARGE_FILENAME_HASH_LEN = 8 + + class Profiling(object): """Profiling plugin for pytest.""" svg = False @@ -61,11 +64,10 @@ def pytest_pyfunc_call(self, __multicall__, pyfuncitem): if err.errno != errno.ENAMETOOLONG: raise - hash_len = 8 - if len(pyfuncitem.name) < hash_len: + if len(pyfuncitem.name) < LARGE_FILENAME_HASH_LEN: raise - hash_str = md5(pyfuncitem.name).hexdigest()[:hash_len] + hash_str = md5(pyfuncitem.name).hexdigest()[:LARGE_FILENAME_HASH_LEN] prof_filename = os.path.join("prof", hash_str + ".prof") prof.dump_stats(prof_filename) From 1a760205d742f68a96429953354f973332ea7354 Mon Sep 17 00:00:00 2001 From: Vladimir Lagunov Date: Thu, 21 Jan 2016 13:51:48 +0600 Subject: [PATCH 3/4] Fixed tests --- pytest-profiling/pytest_profiling.py | 1 + pytest-profiling/tests/unit/test_profile.py | 13 ++++++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/pytest-profiling/pytest_profiling.py b/pytest-profiling/pytest_profiling.py index 517b0430ee..4ba61f3d05 100644 --- a/pytest-profiling/pytest_profiling.py +++ b/pytest-profiling/pytest_profiling.py @@ -70,6 +70,7 @@ def pytest_pyfunc_call(self, __multicall__, pyfuncitem): hash_str = md5(pyfuncitem.name).hexdigest()[:LARGE_FILENAME_HASH_LEN] prof_filename = os.path.join("prof", hash_str + ".prof") prof.dump_stats(prof_filename) + self.profs.append(prof_filename) def pytest_addoption(parser): diff --git a/pytest-profiling/tests/unit/test_profile.py b/pytest-profiling/tests/unit/test_profile.py index 2dfe7127fe..4567ab8f49 100644 --- a/pytest-profiling/tests/unit/test_profile.py +++ b/pytest-profiling/tests/unit/test_profile.py @@ -22,13 +22,16 @@ def test_hooks_pyfunc_call(): multicall, pyfuncitem, plugin = Mock(), Mock(), Profiling(False) pyfuncitem.name.__add__ = Mock() with patch('os.path.join', return_value=sentinel.join): - with patch('pytest_profiling.cProfile') as cProfile: + with patch('pytest_profiling.cProfile.Profile') as Profile: plugin.pytest_pyfunc_call(multicall, pyfuncitem) - assert cProfile.runctx.called - args, kwargs = cProfile.runctx.call_args - assert kwargs['filename'] == sentinel.join + prof = Profile() # mock instances are singletons + assert prof.runctx.called + assert prof.dump_stats.called + runctx_args, _ = prof.runctx.call_args + (dump_stats_filename, ), _ = prof.dump_stats.call_args + assert dump_stats_filename == sentinel.join assert not multicall.execute.called - eval(*args) + eval(*runctx_args) assert multicall.execute.called assert plugin.profs == [sentinel.join] From 87fc4839a8aa18372d3bf6c50ae6de354a5ba03f Mon Sep 17 00:00:00 2001 From: Vladimir Lagunov Date: Thu, 21 Jan 2016 14:05:20 +0600 Subject: [PATCH 4/4] Notes in README.md about file name hashing. --- pytest-profiling/README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pytest-profiling/README.md b/pytest-profiling/README.md index 8d787efc49..f39a9eb9ed 100644 --- a/pytest-profiling/README.md +++ b/pytest-profiling/README.md @@ -89,6 +89,15 @@ pstats files (one per test item) are retained for later analysis in `prof` direc test_import.prof ``` +Profiling plugin by default names pstats filename as corresponding test name. If full path is longer that operation system allows then plugin renames it to first 4 bytes of md5 hash of test name: + +```bash + $ ls -1 prof/ + combined.prof + test_not_longer_than_max_allowed.prof + 68b329da.prof +``` + If the ``--profile-svg`` option is given, along with the prof files and tabular output a svg file will be generated: ```bash