diff --git a/docs/sources/changelog.rst b/docs/sources/changelog.rst index d4e9c61..d33bb45 100644 --- a/docs/sources/changelog.rst +++ b/docs/sources/changelog.rst @@ -2,6 +2,9 @@ Changelog ========= +* :release:`1.6.5 <2022-10-16>` +* :bug:`#60` Make sure that when psutil cannot fetch cpu frequency, the fallback mechanism is used. + * :release:`1.6.4 <2022-05-18>` * :bug:`#56` Force the CPU frequency to 0 and emit a warning when unable to fetch it from the system. * :bug:`#54` Fix a bug that crashes the monitor upon non ASCII characters in commit log under Perforce. Improved P4 change number extraction. diff --git a/docs/sources/configuration.rst b/docs/sources/configuration.rst index 12aef38..4f22bfb 100644 --- a/docs/sources/configuration.rst +++ b/docs/sources/configuration.rst @@ -176,3 +176,19 @@ garbage collector, you just have to set the option `--no-gc` on the command line bash $> pytest --no-gc +Forcing CPU frequency +--------------------- +Under some circumstances, you may want to set the CPU frequency instead of asking `pytest-monitor` to compute it. +To do so, you can either: + - ask `pytest-monitor` to use a preset value if it does not manage to compute the CPU frequency + - or to not try computing the CPU frequency and use your preset value. + + Two environment variables controls this behaviour: + - `PYTEST_MONITOR_CPU_FREQ` allows you to preset a value for the CPU frequency. It must be a float convertible value. + This value will be used if `pytest-monitor` cannot compute the CPU frequency. Otherwise, `0.0` will be used as a + default value. + - `PYTEST_MONITOR_FORCE_CPU_FREQ` instructs `pytest-monitor` to try computing the CPU frequency or not. It expects an + integer convertible value. If not set, or if the integer representation of the value is `0`, then `pytest-monitor` will + try to compute the cpu frequency and defaults to the usecase describe for the previous environment variable. + If it set and not equal to `0`, then we use the value that the environment variable `PYTEST_MONITOR_CPU_FREQ` holds + (`0.0` if not set). diff --git a/docs/sources/installation.rst b/docs/sources/installation.rst index a043578..6face42 100644 --- a/docs/sources/installation.rst +++ b/docs/sources/installation.rst @@ -11,13 +11,8 @@ Supported environments **You will need pytest 4.4+ to run pytest-monitor.** -The following versions of Python are supported: +We support all versions of Python >= 3.6. -- Python 3.5 -- Python 3.6 -- Python 3.7 - -Support for Python 3.8 is still experimental. From conda ---------- diff --git a/pytest_monitor/__init__.py b/pytest_monitor/__init__.py index 8941424..1d0ce78 100644 --- a/pytest_monitor/__init__.py +++ b/pytest_monitor/__init__.py @@ -1,2 +1,2 @@ -__version__ = "1.6.4" +__version__ = "1.6.5" __author__ = "Jean-Sebastien Dieu" diff --git a/pytest_monitor/sys_utils.py b/pytest_monitor/sys_utils.py index 58bfe21..432f7f3 100644 --- a/pytest_monitor/sys_utils.py +++ b/pytest_monitor/sys_utils.py @@ -68,11 +68,14 @@ class ExecutionContext: def __init__(self): self.__cpu_count = multiprocessing.cpu_count() self.__cpu_vendor = _get_cpu_string() - try: - self.__cpu_freq_base = psutil.cpu_freq().current - except AttributeError: - warnings.warn("Unable to fetch CPU frequency. Forcing it to 0.") - self.__cpu_freq_base = 0 + if int(os.environ.get('PYTEST_MONITOR_FORCE_CPU_FREQ', '0')): + self._read_cpu_freq_from_env() + else: + try: + self.__cpu_freq_base = psutil.cpu_freq().current + except (AttributeError, NotImplementedError, FileNotFoundError): + warnings.warn("Unable to fetch CPU frequency. Trying to read it from environment..") + self._read_cpu_freq_from_env() self.__proc_typ = platform.processor() self.__tot_mem = int(psutil.virtual_memory().total / 1024**2) self.__fqdn = socket.getfqdn() @@ -81,6 +84,13 @@ def __init__(self): self.__system = '{} - {}'.format(platform.system(), platform.release()) self.__py_ver = sys.version + def _read_cpu_freq_from_env(self): + try: + self.__cpu_freq_base = float(os.environ.get('PYTEST_MONITOR_CPU_FREQ', '0.')) + except (ValueError, TypeError): + warnings.warn("Wrong type/value while reading cpu frequency from environment. Forcing to 0.0.") + self.__cpu_freq_base = 0.0 + def to_dict(self): return dict(cpu_count=self.cpu_count, cpu_frequency=self.cpu_frequency, diff --git a/tests/test_monitor_context.py b/tests/test_monitor_context.py index e1bfbb8..60a3d49 100644 --- a/tests/test_monitor_context.py +++ b/tests/test_monitor_context.py @@ -1,42 +1,135 @@ import mock +import os import pathlib +import pytest import sqlite3 -@mock.patch('pytest_monitor.sys_utils.psutil.cpu_freq', return_value=None) -def test_when_cpu_freq_cannot_fetch_frequency(cpu_freq_mock, testdir): - """Make sure that pytest-monitor does the job when we have issue in collecing context resources""" +CPU_FREQ_PATH = 'pytest_monitor.sys_utils.psutil.cpu_freq' + +TEST_CONTENT = """ +import time + + +def test_ok(): + time.sleep(0.5) + x = ['a' * i for i in range(100)] + assert len(x) == 100 +""" + +def get_nb_metrics_with_cpu_freq(path): + pymon_path = pathlib.Path(str(path)) / '.pymon' + db = sqlite3.connect(path.as_posix()) + cursor = db.cursor() + cursor.execute('SELECT ITEM FROM TEST_METRICS;') + nb_metrics = len(cursor.fetchall()) + cursor = db.cursor() + cursor.execute('SELECT CPU_FREQUENCY_MHZ FROM EXECUTION_CONTEXTS;') + rows = cursor.fetchall() + assert 1 == len(rows) + cpu_freq = rows[0][0] + return nb_metrics, cpu_freq + + +def test_force_cpu_freq_set_0_use_psutil(testdir): + """Test that when force mode is set, we do not call psutil to fetch CPU's frequency""" + # create a temporary pytest test module - testdir.makepyfile(""" - import time + testdir.makepyfile(TEST_CONTENT) + with mock.patch(CPU_FREQ_PATH, return_value=1500) as cpu_freq_mock: + os.environ['PYTEST_MONITOR_FORCE_CPU_FREQ'] = '0' + os.environ['PYTEST_MONITOR_CPU_FREQ'] = '3000' + # run pytest with the following cmd args + result = testdir.runpytest('-vv') + del os.environ['PYTEST_MONITOR_FORCE_CPU_FREQ'] + del os.environ['PYTEST_MONITOR_CPU_FREQ'] + cpu_freq_mock.assert_called() - def test_ok(): - time.sleep(0.5) - x = ['a' * i for i in range(100)] - assert len(x) == 100 + # fnmatch_lines does an assertion internally + result.stdout.fnmatch_lines(['*::test_ok PASSED*']) + # make sure that that we get a '0' exit code for the test suite + result.assert_outcomes(passed=1) -""") + assert 1, 3000 == get_nb_metrics_with_cpu_freq(testdir) - # run pytest with the following cmd args - result = testdir.runpytest('-vv') + +def test_force_cpu_freq(testdir): + """Test that when force mode is set, we do not call psutil to fetch CPU's frequency""" + + # create a temporary pytest test module + testdir.makepyfile(TEST_CONTENT) + + with mock.patch(CPU_FREQ_PATH, return_value=1500) as cpu_freq_mock: + os.environ['PYTEST_MONITOR_FORCE_CPU_FREQ'] = '1' + os.environ['PYTEST_MONITOR_CPU_FREQ'] = '3000' + # run pytest with the following cmd args + result = testdir.runpytest('-vv') + del os.environ['PYTEST_MONITOR_FORCE_CPU_FREQ'] + del os.environ['PYTEST_MONITOR_CPU_FREQ'] + cpu_freq_mock.assert_not_called() # fnmatch_lines does an assertion internally result.stdout.fnmatch_lines(['*::test_ok PASSED*']) + # make sure that that we get a '0' exit code for the test suite + result.assert_outcomes(passed=1) + + assert 1, 3000 == get_nb_metrics_with_cpu_freq(testdir) + + +@pytest.mark.parametrize('effect', [AttributeError, NotImplementedError, FileNotFoundError]) +def test_when_cpu_freq_cannot_fetch_frequency_set_freq_by_using_fallback(effect, testdir): + """Make sure that pytest-monitor fallback takes value of CPU FREQ from special env var""" + # create a temporary pytest test module + testdir.makepyfile(TEST_CONTENT) - pymon_path = pathlib.Path(str(testdir)) / '.pymon' - assert pymon_path.exists() + with mock.patch(CPU_FREQ_PATH, side_effect=effect) as cpu_freq_mock: + os.environ['PYTEST_MONITOR_CPU_FREQ'] = '3000' + # run pytest with the following cmd args + result = testdir.runpytest('-vv') + del os.environ['PYTEST_MONITOR_CPU_FREQ'] + cpu_freq_mock.assert_called() + # fnmatch_lines does an assertion internally + result.stdout.fnmatch_lines(['*::test_ok PASSED*']) # make sure that that we get a '0' exit code for the test suite result.assert_outcomes(passed=1) - db = sqlite3.connect(str(pymon_path)) - cursor = db.cursor() - cursor.execute('SELECT ITEM FROM TEST_METRICS;') - assert 1 == len(cursor.fetchall()) # current test - cursor = db.cursor() - cursor.execute('SELECT CPU_FREQUENCY_MHZ FROM EXECUTION_CONTEXTS;') - rows = cursor.fetchall() - assert 1 == len(rows) - assert rows[0][0] == 0 + assert 1, 3000 == get_nb_metrics_with_cpu_freq(testdir) + + +@pytest.mark.parametrize('effect', [AttributeError, NotImplementedError, FileNotFoundError]) +def test_when_cpu_freq_cannot_fetch_frequency_set_freq_to_0(effect, testdir): + """Make sure that pytest-monitor's fallback mechanism is efficient enough. """ + # create a temporary pytest test module + testdir.makepyfile(TEST_CONTENT) + + with mock.patch(CPU_FREQ_PATH, side_effect=effect) as cpu_freq_mock: + # run pytest with the following cmd args + result = testdir.runpytest('-vv') + cpu_freq_mock.assert_called() + + # fnmatch_lines does an assertion internally + result.stdout.fnmatch_lines(['*::test_ok PASSED*']) + # make sure that that we get a '0' exit code for the test suite + result.assert_outcomes(passed=1) + + assert 1, 0 == get_nb_metrics_with_cpu_freq(testdir) + +@mock.patch('pytest_monitor.sys_utils.psutil.cpu_freq', return_value=None) +def test_when_cpu_freq_cannot_fetch_frequency(cpu_freq_mock, testdir): + """Make sure that pytest-monitor does the job when we have issue in collecing context resources""" + # create a temporary pytest test module + testdir.makepyfile(TEST_CONTENT) + + # run pytest with the following cmd args + result = testdir.runpytest('-vv') + + # fnmatch_lines does an assertion internally + result.stdout.fnmatch_lines(['*::test_ok PASSED*']) + # make sure that that we get a '0' exit code for the test suite + result.assert_outcomes(passed=1) + + assert 1, 0 == get_nb_metrics_with_cpu_freq(testdir) +