diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index 5654743..6caf8c3 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -9,13 +9,12 @@ jobs: fail-fast: false matrix: include: - - {name: Linux36, python: '3.6', os: ubuntu-latest, tox: py36} - {name: Linux37, python: '3.7', os: ubuntu-latest, tox: py37} - {name: Linux38, python: '3.8', os: ubuntu-latest, tox: py38} - {name: Linux39, python: '3.9', os: ubuntu-latest, tox: py39} - {name: Linux310, python: '3.10', os: ubuntu-latest, tox: py310} - - {name: Linux311, python: '3.11.0-beta.5', os: ubuntu-latest, tox: py311} - - {name: Style, python: '3.10', os: ubuntu-latest, tox: style} + - {name: Linux311, python: '3.11', os: ubuntu-latest, tox: py311} + - {name: Style, python: '3.11', os: ubuntu-latest, tox: style} steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 diff --git a/CHANGELOG.md b/CHANGELOG.md index fcc5098..1e67d3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] +### Changed + +- Add type hints to tests ([#50]) +- Drop explicit support for Python 3.6 ([#48]) + +## [1.4.0] - 2022-11-08 + ### Added - `inital_text` parameter which, when present, will use logger to log that timer has been started (by [Matthew Price](https://github.com/pricemg) in [#47]) @@ -62,7 +69,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), Initial version of `codetiming`. Version 1.0.0 corresponds to the code in the tutorial [Python Timer Functions: Three Ways to Monitor Your Code](https://realpython.com/python-timer/). -[Unreleased]: https://github.com/realpython/codetiming/compare/v1.3.2...HEAD +[Unreleased]: https://github.com/realpython/codetiming/compare/v1.4.0...HEAD +[1.4.0]: https://github.com/realpython/codetiming/compare/v1.3.2...v1.4.0 [1.3.2]: https://github.com/realpython/codetiming/compare/v1.3.1...v1.3.2 [1.3.1]: https://github.com/realpython/codetiming/compare/v1.3.0...v1.3.1 [1.3.0]: https://github.com/realpython/codetiming/compare/v1.2.0...v1.3.0 @@ -70,19 +78,21 @@ Initial version of `codetiming`. Version 1.0.0 corresponds to the code in the tu [1.1.0]: https://github.com/realpython/codetiming/compare/v1.0.0...v1.1.0 [1.0.0]: https://github.com/realpython/codetiming/releases/tag/v1.0.0 -[#13]: https://github.com/realpython/codetiming/pull/13 -[#17]: https://github.com/realpython/codetiming/pull/17 -[#18]: https://github.com/realpython/codetiming/pull/18 -[#23]: https://github.com/realpython/codetiming/pull/23 -[#24]: https://github.com/realpython/codetiming/issues/24 -[#25]: https://github.com/realpython/codetiming/pull/25 -[#27]: https://github.com/realpython/codetiming/pull/27 -[#29]: https://github.com/realpython/codetiming/issues/29 -[#30]: https://github.com/realpython/codetiming/pull/30 -[#32]: https://github.com/realpython/codetiming/pull/32 -[#33]: https://github.com/realpython/codetiming/pull/33 -[#34]: https://github.com/realpython/codetiming/pull/34 -[#35]: https://github.com/realpython/codetiming/pull/35 -[#38]: https://github.com/realpython/codetiming/pull/38 -[#46]: https://github.com/realpython/codetiming/pull/46 +[#50]: https://github.com/realpython/codetiming/pull/50 +[#48]: https://github.com/realpython/codetiming/pull/48 [#47]: https://github.com/realpython/codetiming/pull/47 +[#46]: https://github.com/realpython/codetiming/pull/46 +[#38]: https://github.com/realpython/codetiming/pull/38 +[#35]: https://github.com/realpython/codetiming/pull/35 +[#34]: https://github.com/realpython/codetiming/pull/34 +[#33]: https://github.com/realpython/codetiming/pull/33 +[#32]: https://github.com/realpython/codetiming/pull/32 +[#30]: https://github.com/realpython/codetiming/pull/30 +[#29]: https://github.com/realpython/codetiming/issues/29 +[#27]: https://github.com/realpython/codetiming/pull/27 +[#25]: https://github.com/realpython/codetiming/pull/25 +[#24]: https://github.com/realpython/codetiming/issues/24 +[#23]: https://github.com/realpython/codetiming/pull/23 +[#18]: https://github.com/realpython/codetiming/pull/18 +[#17]: https://github.com/realpython/codetiming/pull/17 +[#13]: https://github.com/realpython/codetiming/pull/13 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2739d8c..79654c9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -26,24 +26,20 @@ Do you want to contribute code to `codetiming`? Fantastic! We welcome contributi ## Setting Up Your Environment -`codetiming` uses [`flit`](https://flit.readthedocs.io) for package management. You should first install `flit`: +`codetiming` uses [`flit`](https://flit.readthedocs.io) for package management. You can use `flit` through [`pip`](https://realpython.com/what-is-pip/). -``` -$ python -m pip install flit -``` - -You can then install `codetiming` locally for development with `flit`: +You can then install `codetiming` locally for development with `pip`: ``` -$ python -m flit install --pth-file --deps all +$ python -m pip install --editable .[dev,test] ``` -This will install `codetiming` and all its dependencies, including development tools like [`black`](https://black.readthedocs.io) and [`mypy`](http://mypy-lang.org/). The `--pth-file` option allows you to test your changes without reinstalling. On Linux and Mac, you can use `--symlink` for the same effect. +This will install `codetiming` and all its dependencies, including development tools like [`black`](https://black.readthedocs.io) and [`mypy`](http://mypy-lang.org/), and test runners like [`pytest`](https://docs.pytest.org/). The `--editable` option allows you to test your changes without reinstalling. ## Running Tests -Run tests using [`tox`](https://tox.readthedocs.io/). `tox` helps to enforce the following principles: +Run tests using [`tox`](https://tox.readthedocs.io/). You can also run individual tests manually. `tox` helps to enforce the following principles: - Consistent code style using [`black`](https://black.readthedocs.io). You can automatically format your code as follows: @@ -54,7 +50,7 @@ Run tests using [`tox`](https://tox.readthedocs.io/). `tox` helps to enforce the - Static type hinting using [`mypy`](http://mypy-lang.org/). Test your type hints as follows: ```console - $ mypy --strict codetiming/ + $ mypy --strict codetiming/ tests/ ``` See Real Python's [Python Type Checking guide](https://realpython.com/python-type-checking/) for more information. @@ -62,10 +58,10 @@ Run tests using [`tox`](https://tox.readthedocs.io/). `tox` helps to enforce the - Unit testing using [`pytest`](https://docs.pytest.org/). You can run your tests and see a coverage report as follows: ```console - $ pytest --cov=codetiming --cov-report=term-missing + $ python -m pytest --cov=codetiming --cov-report=term-missing ``` -- Code issues are checked with the [flake8]() linter. You can run flake8 manually as follows: +- Code issues are checked with the [flake8](https://flake8.pycqa.org/) linter. You can run flake8 manually as follows: ```console $ python -m flake8 codetiming/ tests/ @@ -80,7 +76,7 @@ Run tests using [`tox`](https://tox.readthedocs.io/). `tox` helps to enforce the - All modules, functions, classes, and methods must have docstrings. This is enforced by [Interrogation](https://interrogate.readthedocs.io/). You can test compliance as follows: ```console - $ interrogate -c pyproject.toml -vv + $ python -m interrogate -c pyproject.toml -vv ``` -Feel free to ask for help in your PR if you are having challenges with any of these tests. \ No newline at end of file +Feel free to ask for help in your PR if you are having challenges with any of these tests. diff --git a/codetiming/_timer.py b/codetiming/_timer.py index 4b218af..21a2796 100644 --- a/codetiming/_timer.py +++ b/codetiming/_timer.py @@ -9,12 +9,32 @@ import time from contextlib import ContextDecorator from dataclasses import dataclass, field -from typing import Any, Callable, ClassVar, Optional, Union +from typing import TYPE_CHECKING, Any, Callable, ClassVar, Optional, Union # Codetiming imports from codetiming._timers import Timers +# Special types, Protocol only works for Python >= 3.8 +if TYPE_CHECKING: # pragma: nocover + # Standard library imports + from typing import Protocol, TypeVar + T = TypeVar("T") + + class FloatArg(Protocol): + """Protocol type that allows classes that take one float argument""" + + def __call__(self: T, __seconds: float) -> T: + """Callable signature""" + ... # pragma: nocover + +else: + + class FloatArg: + """Dummy runtime class""" + + +# Timer code class TimerError(Exception): """A custom exception used to report errors in use of Timer class.""" @@ -24,12 +44,12 @@ class Timer(ContextDecorator): """Time your code using a class, context manager, or decorator.""" timers: ClassVar[Timers] = Timers() - _start_time: Optional[float] = field(default=None, init=False, repr=False) name: Optional[str] = None - text: Union[str, Callable[[float], str]] = "Elapsed time: {:0.4f} seconds" - initial_text: Union[bool, str] = False + text: Union[str, FloatArg, Callable[[float], str]] = "Elapsed time: {:0.4f} seconds" + initial_text: Union[bool, str, FloatArg] = False logger: Optional[Callable[[str], None]] = print last: float = field(default=math.nan, init=False, repr=False) + _start_time: Optional[float] = field(default=None, init=False, repr=False) def start(self) -> None: """Start a new timer.""" @@ -69,7 +89,7 @@ def stop(self) -> float: "minutes": self.last / 60, } text = self.text.format(self.last, **attributes) - self.logger(text) + self.logger(str(text)) if self.name: self.timers.add(self.name, self.last) diff --git a/pyproject.toml b/pyproject.toml index 7ac01fc..e9ddd8f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,7 +18,6 @@ classifiers = [ "Operating System :: MacOS", "Operating System :: Microsoft", "Operating System :: POSIX :: Linux", - "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", @@ -30,8 +29,8 @@ classifiers = [ "Typing :: Typed", ] keywords = ["timer", "class", "contextmanager", "decorator"] -requires-python = ">=3.6" -dependencies = ["dataclasses; python_version < '3.7'"] +requires-python = ">=3.7" +dependencies = [] [project.urls] Homepage = "https://realpython.com/python-timer" diff --git a/tests/test_codetiming.py b/tests/test_codetiming.py index c9502c8..d74a7bb 100644 --- a/tests/test_codetiming.py +++ b/tests/test_codetiming.py @@ -27,31 +27,31 @@ ) -def waste_time(num=1000): +def waste_time(num: int = 1000) -> None: """Just waste a little bit of time.""" sum(n**2 for n in range(num)) @Timer(text=TIME_MESSAGE) -def decorated_timewaste(num=1000): +def decorated_timewaste(num: int = 1000) -> None: """Just waste a little bit of time.""" sum(n**2 for n in range(num)) @Timer(text=TIME_MESSAGE, initial_text=True) -def decorated_timewaste_initial_text_true(num=1000): +def decorated_timewaste_initial_text_true(num: int = 1000) -> None: """Just waste a little bit of time.""" sum(n**2 for n in range(num)) @Timer(text=TIME_MESSAGE, initial_text="Starting the party") -def decorated_timewaste_initial_text_custom(num=1000): +def decorated_timewaste_initial_text_custom(num: int = 1000) -> None: """Just waste a little bit of time.""" sum(n**2 for n in range(num)) @Timer(name="accumulator", text=TIME_MESSAGE) -def accumulated_timewaste(num=1000): +def accumulated_timewaste(num: int = 1000) -> None: """Just waste a little bit of time.""" sum(n**2 for n in range(num)) @@ -59,11 +59,11 @@ def accumulated_timewaste(num=1000): class CustomLogger: """Simple class used to test custom logging capabilities in Timer.""" - def __init__(self): + def __init__(self) -> None: """Store log messages in the .messages attribute.""" self.messages = "" - def __call__(self, message): + def __call__(self, message: str) -> None: """Add a log message to the .messages attribute.""" self.messages += message @@ -71,7 +71,7 @@ def __call__(self, message): # # Tests # -def test_timer_as_decorator(capsys): +def test_timer_as_decorator(capsys: pytest.CaptureFixture[str]) -> None: """Test that decorated function prints timing information.""" decorated_timewaste() @@ -81,7 +81,7 @@ def test_timer_as_decorator(capsys): assert stderr == "" -def test_timer_as_context_manager(capsys): +def test_timer_as_context_manager(capsys: pytest.CaptureFixture[str]) -> None: """Test that timed context prints timing information.""" with Timer(text=TIME_MESSAGE): waste_time() @@ -92,7 +92,7 @@ def test_timer_as_context_manager(capsys): assert stderr == "" -def test_explicit_timer(capsys): +def test_explicit_timer(capsys: pytest.CaptureFixture[str]) -> None: """Test that timed section prints timing information.""" t = Timer(text=TIME_MESSAGE) t.start() @@ -105,22 +105,23 @@ def test_explicit_timer(capsys): assert stderr == "" -def test_error_if_timer_not_running(): +def test_error_if_timer_not_running() -> None: """Test that timer raises error if it is stopped before started.""" t = Timer(text=TIME_MESSAGE) with pytest.raises(TimerError): t.stop() -def test_access_timer_object_in_context(capsys): +def test_access_timer_object_in_context(capsys: pytest.CaptureFixture[str]) -> None: """Test that we can access the timer object inside a context.""" with Timer(text=TIME_MESSAGE) as t: assert isinstance(t, Timer) + assert isinstance(t.text, str) assert t.text.startswith(TIME_PREFIX) _, _ = capsys.readouterr() # Do not print log message to standard out -def test_custom_logger(): +def test_custom_logger() -> None: """Test that we can use a custom logger.""" logger = CustomLogger() with Timer(text=TIME_MESSAGE, logger=logger): @@ -128,7 +129,7 @@ def test_custom_logger(): assert RE_TIME_MESSAGE.match(logger.messages) -def test_timer_without_text(capsys): +def test_timer_without_text(capsys: pytest.CaptureFixture[str]) -> None: """Test that timer with logger=None does not print anything.""" with Timer(logger=None): waste_time() @@ -138,7 +139,7 @@ def test_timer_without_text(capsys): assert stderr == "" -def test_accumulated_decorator(capsys): +def test_accumulated_decorator(capsys: pytest.CaptureFixture[str]) -> None: """Test that decorated timer can accumulate.""" accumulated_timewaste() accumulated_timewaste() @@ -151,7 +152,7 @@ def test_accumulated_decorator(capsys): assert stderr == "" -def test_accumulated_context_manager(capsys): +def test_accumulated_context_manager(capsys: pytest.CaptureFixture[str]) -> None: """Test that context manager timer can accumulate.""" t = Timer(name="accumulator", text=TIME_MESSAGE) with t: @@ -167,10 +168,10 @@ def test_accumulated_context_manager(capsys): assert stderr == "" -def test_accumulated_explicit_timer(capsys): +def test_accumulated_explicit_timer(capsys: pytest.CaptureFixture[str]) -> None: """Test that explicit timer can accumulate.""" t = Timer(name="accumulated_explicit_timer", text=TIME_MESSAGE) - total = 0 + total = 0.0 t.start() waste_time() total += t.stop() @@ -187,7 +188,7 @@ def test_accumulated_explicit_timer(capsys): assert total == Timer.timers["accumulated_explicit_timer"] -def test_error_if_restarting_running_timer(): +def test_error_if_restarting_running_timer() -> None: """Test that restarting a running timer raises an error.""" t = Timer(text=TIME_MESSAGE) t.start() @@ -195,21 +196,23 @@ def test_error_if_restarting_running_timer(): t.start() -def test_last_starts_as_nan(): +def test_last_starts_as_nan() -> None: """Test that .last attribute is initialized as nan.""" t = Timer() assert math.isnan(t.last) -def test_timer_sets_last(): +def test_timer_sets_last() -> None: """Test that .last attribute is properly set.""" - with Timer() as t: + with Timer(logger=None) as t: time.sleep(0.02) assert t.last >= 0.02 -def test_using_name_in_text_without_explicit_timer(capsys): +def test_using_name_in_text_without_explicit_timer( + capsys: pytest.CaptureFixture[str], +) -> None: """Test that the name of the timer can be referenced in the text.""" name = "NamedTimer" with Timer(name=name, text="{name}: {:.2f}"): @@ -220,7 +223,9 @@ def test_using_name_in_text_without_explicit_timer(capsys): assert stderr == "" -def test_using_name_in_text_with_explicit_timer(capsys): +def test_using_name_in_text_with_explicit_timer( + capsys: pytest.CaptureFixture[str], +) -> None: """Test that timer name and seconds attribute can be referenced in the text.""" name = "NamedTimer" with Timer(name=name, text="{name}: {seconds:.2f}"): @@ -231,7 +236,7 @@ def test_using_name_in_text_with_explicit_timer(capsys): assert stderr == "" -def test_using_minutes_attribute_in_text(capsys): +def test_using_minutes_attribute_in_text(capsys: pytest.CaptureFixture[str]) -> None: """Test that timer can report its duration in minutes.""" with Timer(text="{minutes:.1f} minutes"): waste_time() @@ -241,7 +246,9 @@ def test_using_minutes_attribute_in_text(capsys): assert stderr == "" -def test_using_milliseconds_attribute_in_text(capsys): +def test_using_milliseconds_attribute_in_text( + capsys: pytest.CaptureFixture[str], +) -> None: """Test that timer can report its duration in milliseconds.""" with Timer(text="{milliseconds:.0f} {seconds:.3f}"): waste_time() @@ -252,10 +259,10 @@ def test_using_milliseconds_attribute_in_text(capsys): assert stderr == "" -def test_text_formatting_function(capsys): +def test_text_formatting_function(capsys: pytest.CaptureFixture[str]) -> None: """Test that text can be formatted by a separate function.""" - def format_text(seconds): + def format_text(seconds: float) -> str: """Function that returns a formatted text""" return f"Function: {seconds + 1:.0f}" @@ -267,17 +274,17 @@ def format_text(seconds): assert not stderr.strip() -def test_text_formatting_class(capsys): +def test_text_formatting_class(capsys: pytest.CaptureFixture[str]) -> None: """Test that text can be formatted by a separate class.""" class TextFormatter: """Class that behaves like a formatted text.""" - def __init__(self, seconds): - """Initialize with number of seconds""" + def __init__(self, seconds: float) -> None: + """Store the elapsed number of seconds""" self.seconds = seconds - def __str__(self): + def __str__(self) -> str: """Represent the class as a formatted text""" return f"Class: {self.seconds + 1:.0f}" @@ -289,9 +296,9 @@ def __str__(self): assert not stderr.strip() -def test_timers_cleared(): +def test_timers_cleared() -> None: """Test that timers can be cleared.""" - with Timer(name="timer_to_be_cleared"): + with Timer(name="timer_to_be_cleared", logger=None): waste_time() assert "timer_to_be_cleared" in Timer.timers @@ -299,7 +306,7 @@ def test_timers_cleared(): assert not Timer.timers -def test_running_cleared_timers(): +def test_running_cleared_timers(capsys: pytest.CaptureFixture[str]) -> None: """Test that timers can still be run after they're cleared.""" t = Timer(name="timer_to_be_cleared") Timer.timers.clear() @@ -308,14 +315,15 @@ def test_running_cleared_timers(): with t: waste_time() + capsys.readouterr() assert "accumulator" in Timer.timers assert "timer_to_be_cleared" in Timer.timers -def test_timers_stats(): +def test_timers_stats() -> None: """Test that we can get basic statistics from timers.""" name = "timer_with_stats" - t = Timer(name=name) + t = Timer(name=name, logger=None) for num in range(5, 10): with t: waste_time(num=100 * num) @@ -328,7 +336,7 @@ def test_timers_stats(): assert stats.stdev(name) >= 0 -def test_stats_missing_timers(): +def test_stats_missing_timers() -> None: """Test that getting statistics from non-existent timers raises exception.""" with pytest.raises(KeyError): Timer.timers.count("non_existent_timer") @@ -337,13 +345,15 @@ def test_stats_missing_timers(): Timer.timers.stdev("non_existent_timer") -def test_setting_timers_exception(): +def test_setting_timers_exception() -> None: """Test that setting .timers items raises exception.""" with pytest.raises(TypeError): Timer.timers["set_timer"] = 1.23 -def test_timer_as_decorator_with_initial_text_true(capsys): +def test_timer_as_decorator_with_initial_text_true( + capsys: pytest.CaptureFixture[str], +) -> None: """Test that decorated function prints at start with default initial text.""" decorated_timewaste_initial_text_true() @@ -353,7 +363,9 @@ def test_timer_as_decorator_with_initial_text_true(capsys): assert stderr == "" -def test_timer_as_context_manager_with_initial_text_true(capsys): +def test_timer_as_context_manager_with_initial_text_true( + capsys: pytest.CaptureFixture[str], +) -> None: """Test that timed context prints at start with default initial text.""" with Timer(text=TIME_MESSAGE, initial_text=True): waste_time() @@ -364,7 +376,9 @@ def test_timer_as_context_manager_with_initial_text_true(capsys): assert stderr == "" -def test_explicit_timer_with_initial_text_true(capsys): +def test_explicit_timer_with_initial_text_true( + capsys: pytest.CaptureFixture[str], +) -> None: """Test that timed section prints at start with default initial text.""" t = Timer(text=TIME_MESSAGE, initial_text=True) t.start() @@ -377,7 +391,9 @@ def test_explicit_timer_with_initial_text_true(capsys): assert stderr == "" -def test_timer_as_decorator_with_initial_text_custom(capsys): +def test_timer_as_decorator_with_initial_text_custom( + capsys: pytest.CaptureFixture[str], +) -> None: """Test that decorated function prints at start with custom initial text.""" decorated_timewaste_initial_text_custom() @@ -387,7 +403,9 @@ def test_timer_as_decorator_with_initial_text_custom(capsys): assert stderr == "" -def test_timer_as_context_manager_with_initial_text_custom(capsys): +def test_timer_as_context_manager_with_initial_text_custom( + capsys: pytest.CaptureFixture[str], +) -> None: """Test that timed context prints at start with custom initial text.""" with Timer(text=TIME_MESSAGE, initial_text="Starting the party"): waste_time() @@ -398,7 +416,9 @@ def test_timer_as_context_manager_with_initial_text_custom(capsys): assert stderr == "" -def test_explicit_timer_with_initial_text_custom(capsys): +def test_explicit_timer_with_initial_text_custom( + capsys: pytest.CaptureFixture[str], +) -> None: """Test that timed section prints at start with custom initial text.""" t = Timer(text=TIME_MESSAGE, initial_text="Starting the party") t.start() @@ -411,7 +431,9 @@ def test_explicit_timer_with_initial_text_custom(capsys): assert stderr == "" -def test_explicit_timer_with_initial_text_true_with_name(capsys): +def test_explicit_timer_with_initial_text_true_with_name( + capsys: pytest.CaptureFixture[str], +) -> None: """Test with default initial text referencing timer name.""" t = Timer(name="named", text=TIME_MESSAGE, initial_text=True) t.start() @@ -426,7 +448,9 @@ def test_explicit_timer_with_initial_text_true_with_name(capsys): assert stderr == "" -def test_explicit_timer_with_initial_text_with_name(capsys): +def test_explicit_timer_with_initial_text_with_name( + capsys: pytest.CaptureFixture[str], +) -> None: """Test with custom initial text referencing timer name.""" t = Timer(name="the party", text=TIME_MESSAGE, initial_text="Starting {name}") t.start() diff --git a/tox.ini b/tox.ini index 55cc473..10119a0 100644 --- a/tox.ini +++ b/tox.ini @@ -19,9 +19,10 @@ deps = interrogate isort mypy + pytest commands = {envpython} -m black --check --quiet codetiming/ tests/ {envpython} -m flake8 codetiming/ tests/ {envpython} -m interrogate --quiet --config=pyproject.toml {envpython} -m isort --check codetiming/ tests/ - {envpython} -m mypy --strict codetiming/ + {envpython} -m mypy --strict codetiming/ tests/