-
-
-
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml
deleted file mode 100644
index 105ce2d..0000000
--- a/.idea/inspectionProfiles/profiles_settings.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
deleted file mode 100644
index 02c2b01..0000000
--- a/.idea/misc.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
deleted file mode 100644
index e58beaa..0000000
--- a/.idea/modules.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/pytest-slow-first.iml b/.idea/pytest-slow-first.iml
deleted file mode 100644
index 8b8c395..0000000
--- a/.idea/pytest-slow-first.iml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/README.md b/README.md
index e275f87..3bd0f2a 100644
--- a/README.md
+++ b/README.md
@@ -39,44 +39,6 @@ In the next time you run it, tests will be sorted by time spend in the last run,
-Usage
------
-
-You just need to define two functions inside your conftest.py file: `slow_first_save_durations` and `slow_first_load_durations`.
-
-The first one is to save results of current run and the second one is to load the same results in the folowing run. Allowing this plugin to sort execuntion of tests based in these results.
-
-Example of `conftest.py` file:
-```python
-import os, json
-
-
-def slow_first_load_durations():
- if os.path.exists('/tmp/tests_duration'):
- with open('/tmp/tests_duration', 'r') as f:
- return f.read()
- else:
- # Durations not found. Run with default order
- return None
-
-def slow_first_save_durations(durations_data: str):
- with open('/tmp/tests_duration', 'w') as f:
- f.write(durations_data)
-```
-
-#### Explanation
-
-1. First, `slow_first_load_durations` will be called before your tests starts running, it will load the durantion of the tests
-of the previous run.
-
- * **obs**: if its the first time using this plugin or if you can't load the results, this function must return None.
-
-2. If `slow_first_load_durations` finds data, it returns the content and slow-first plugin will sort your tests, otherwise
-the test suite will run at default order.
-
-3. If the suit runs with success, `slow_first_save_durations` is going to be called with durations as argument. This function must save the results
-in a way that `slow_first_load_durations` can load in the next run.
-
### Running with pytest-slow-first plugin
Finally, activate the plugin by passing `--slow-first` as paramter of pytest command:
@@ -87,6 +49,10 @@ pytest tests --slow-first -n3 # using along side xdist
+THis plugin will save the duration of each of your tests in a file named `pytest-slow-first.json` in the current directory.
+You can change the location by setting the enviroment variable `SLOW_FIRST_PATH` to the path you want.
+Ex: `export SLOW_FIRST_PATH=/tmp/pytest-slow-first.json pytest --slow-first`
+
Installation
------------
diff --git a/pytest_slow_first.py b/pytest_slow_first.py
index 5b64070..22828e5 100644
--- a/pytest_slow_first.py
+++ b/pytest_slow_first.py
@@ -2,6 +2,18 @@
import importlib
import json
import logging
+import os
+from typing import Union, Dict
+
+import pytest
+from _pytest.config import ExitCode
+
+FORMAT_VERSION = '1.0.0'
+SLOW_FIRST_PATH = os.environ.get('SLOW_FIRST_PATH', 'pytest-slow-first.json')
+
+
+def log_slow_first(message: str):
+ logging.getLogger().info(f"[pytest-slow-first] {message}")
class SlowFirstRequiredFunctionNotImplemented(Exception):
@@ -17,60 +29,77 @@ def _get_slow_first_from_config(config):
class Test:
- def __init__(self, name, setup_duration: float = None, call_duration: float = None, teardown_duration: float = None):
- self.name = name
+ nodeid: str
+ setup_duration: Union[float, None]
+ call_duration: Union[float, None]
+ teardown_duration: Union[float, None]
+
+ def __init__(self, nodeid: str, setup_duration: float = None, call_duration: float = None, teardown_duration: float = None):
+ self.nodeid = nodeid
self.setup_duration = setup_duration
self.call_duration = call_duration
self.teardown_duration = teardown_duration
@property
- def total_duration(self):
+ def total_duration(self) -> float:
return self.setup_duration + self.call_duration + self.teardown_duration
def set_duration(self, kind: str, duration: float):
setattr(self, f"{kind}_duration", duration)
- def serialize(self):
- return {'name': self.name, 'setup_duration': self.setup_duration,
+ def serialize(self) -> Dict[str, Union[str, float]]:
+ return {'nodeid': self.nodeid, 'setup_duration': self.setup_duration,
'call_duration': self.call_duration, 'teardown_duration': self.teardown_duration}
@staticmethod
- def deserialize(data: dict):
- test = Test(data['name'])
- test.setup_duration = data['setup_duration']
- test.call_duration = data['call_duration']
- test.teardown_duration = data['teardown_duration']
- return test
+ def deserialize(data: dict) -> "Test":
+ return Test(**data)
class SlowFirst:
- def __init__(self, tests_by_name: dict = None, enabled: bool = False):
+ _tests_by_name: Dict[str, Test]
+ enabled: bool
+
+ def __init__(self, tests_by_name: Dict[str, Test] = None, enabled: bool = False):
self._tests_by_name = tests_by_name or {}
self.enabled = enabled
- if enabled:
- self._validate_conftest_functions_are_defined()
-
def save(self):
- logging.getLogger().info("Saving testes durations")
- self._get_conftest_module().slow_first_save_durations(self.serialize())
+ log_slow_first("Saving testes durations")
+ with open(SLOW_FIRST_PATH, 'w') as f:
+ f.write(self.serialize())
@staticmethod
def load():
- logging.getLogger().info("Loading testes durations of previous run")
- data = SlowFirst._get_conftest_module().slow_first_load_durations()
-
- if not data:
+ if os.path.exists(SLOW_FIRST_PATH):
+ with open(SLOW_FIRST_PATH, 'r') as f:
+ data = f.read()
+ log_slow_first("Loaded testes durations from previous run. Applying order")
+ else:
+ log_slow_first("No previous run found. Skipping order")
return None
return SlowFirst.deserialize(data)
def serialize(self) -> str:
- return json.dumps(list(map(Test.serialize, self._tests_by_name.values())))
+ return json.dumps({
+ 'format_version': FORMAT_VERSION,
+ 'tests': [test.serialize() for test in self._tests_by_name.values()]
+ })
@staticmethod
- def deserialize(data: str):
- return SlowFirst({test.name: test for test in map(Test.deserialize, json.loads(data))})
+ def deserialize(data: str) -> "SlowFirst":
+ data = json.loads(data)
+
+ if data['format_version'] != FORMAT_VERSION:
+ pytest.exit(
+ reason=f"[pytest-slow-first] The format version of {SLOW_FIRST_PATH} "
+ f"is not compatible with this version of pytest-slow-first. "
+ f"Please delete {SLOW_FIRST_PATH} and run tests again.",
+ returncode=ExitCode.USAGE_ERROR
+ )
+
+ return SlowFirst({test.nodeid: test for test in map(Test.deserialize, data['tests'])})
def set_duration(self, name: str, kind: str, duration: float):
test = self._tests_by_name.get(name)
@@ -79,37 +108,16 @@ def set_duration(self, name: str, kind: str, duration: float):
test.set_duration(kind, duration)
else:
test = Test(name)
- self._tests_by_name[test.name] = test
+ self._tests_by_name[test.nodeid] = test
test.set_duration(kind, duration)
- def get_order(self, test_name: str):
+ def get_order(self, test_name: str) -> float:
test = self._tests_by_name.get(test_name)
if test:
return test.total_duration
else:
return 0
- @staticmethod
- def _get_conftest_module():
- return importlib.import_module('conftest')
-
- @staticmethod
- def _validate_conftest_functions_are_defined():
- try:
- conftest = SlowFirst._get_conftest_module()
- except ModuleNotFoundError as e:
- raise ConftestModuleNotFound('slow_first plugin was not able to load conftest module') from e
-
- if not hasattr(conftest, 'slow_first_save_durations'):
- raise SlowFirstRequiredFunctionNotImplemented(
- 'slow_first_save_durations function is not defined in conftest.py'
- )
-
- if not hasattr(conftest, 'slow_first_load_durations'):
- raise SlowFirstRequiredFunctionNotImplemented(
- 'slow_first_load_durations function is not defined in conftest.py'
- )
-
def pytest_addoption(parser):
group = parser.getgroup('slow-first')
@@ -118,7 +126,7 @@ def pytest_addoption(parser):
action='store_true',
dest='slow_first',
default=False,
- help='If given, will enable tests sorting by durations from last run.'
+ help='Sort tests from slowest to fastest.'
)
diff --git a/setup.py b/setup.py
index 4f900f0..cb98420 100644
--- a/setup.py
+++ b/setup.py
@@ -13,7 +13,7 @@ def read(fname):
setup(
name='pytest-slow-first',
- version='0.1.0',
+ version='1.0.0',
author='João Vitor Silvestre',
author_email='joao_vitor_silvestre@outlook.com',
maintainer='João Vitor Silvestre',
diff --git a/tests/test_slow_first.py b/tests/test_slow_first.py
index 0a8045b..fe3d2f2 100644
--- a/tests/test_slow_first.py
+++ b/tests/test_slow_first.py
@@ -1,4 +1,6 @@
# -*- coding: utf-8 -*-
+import json
+
from _pytest.config import ExitCode
@@ -18,22 +20,9 @@ def test3():
sleep(0.2)
""")
- file_to_save = testdir.tmpdir.join('slow_first.json')
+ file_to_save = testdir.tmpdir.join('pytest-slow-first.json')
- testdir.makeconftest(f"""
- import os
- import json
-
- def slow_first_save_durations(durations_data: str):
- assert json.loads(durations_data)
- with open('{file_to_save}', 'w') as f:
- f.write(durations_data)
-
- def slow_first_load_durations():
- if os.path.exists('{file_to_save}'):
- with open('{file_to_save}', 'r') as f:
- return f.read()
- """)
+ testdir.makeconftest("")
# 1º theres no previous durention saved, the first run will run
# tests in the order they are defined
@@ -76,16 +65,21 @@ def slow_first_load_durations():
assert result.ret == 0
-def test_validate_user_implemented_functions_in_conftest(testdir):
- testdir.makepyfile("""
- from time import sleep
+def test_different_format(testdir):
+ from pytest_slow_first import SlowFirst
+ testdir.makepyfile("""
def test1():
- pass
+ sleep(0.1)
""")
+ file_oprevious_run = testdir.tmpdir.join('pytest-slow-first.json')
+ with open(f'{file_oprevious_run}', 'w') as f:
+ json.dump({"format_version": "0.0.1", "tests": []}, f)
+
testdir.makeconftest("")
+ # 1º Must exit with error because the format version is not compatible
result = testdir.runpytest('--slow-first', '-v')
- assert result.ret == ExitCode.INTERNAL_ERROR
+ assert result.ret == ExitCode.USAGE_ERROR