Skip to content

Commit

Permalink
Unit test completion (#9)
Browse files Browse the repository at this point in the history
* Code Linting.

* Make EpiLog a typed package. Improve unit tests to include teardown fixtures. Include tox configuration file.

* Complete Test Coverage. Update Benchmark Behavior.

* Increase Minor version, due to changes to benchmark class which modify behavior. Improve docstring, and bring important classes into root namespace for accessibility.

* Update Github Worflow to use tox configuration to orchestrate unit testing.

* Upgrade build tools before begining workflows.

* Correct tox installation step.

* Add a little more typing info in unit test utilities.

* Force color output on github actions.

* Confirm tox runs all subtask tests.

* Separate Tox workflows for github actions.

* Add class annotations. Provide utilities to convert performance benchmark units into most reasonable units.
  • Loading branch information
Spill-Tea authored May 6, 2024
1 parent 18b5a9c commit a0a512e
Show file tree
Hide file tree
Showing 12 changed files with 540 additions and 151 deletions.
43 changes: 26 additions & 17 deletions .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,37 @@ on:
pull_request:
branches: [ main ]

env:
FORCE_COLOR: "1"

jobs:
build:

runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.8", "3.9"]
python-version: ["3.8", "3.9", "3.10", "3.11"]

steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install -U pip
pip install -e .[test]
- name: Lint with flake8
run: |
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Test with pytest
run: |
pytest
- uses: actions/checkout@v4

- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python }}

- name: Install tox
run: |
python -m pip install -U pip setuptools wheel
pip install tox
- name: PyTest Unit Testing
run: tox -e py

- name: Code Linting & Formatting
if: always()
run: tox -e lint

- name: MyPy Static Type Check
if: always()
run: tox -e type
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,10 @@ ENV/
env.bak/
venv.bak/

# Spyder project settings
# IDE project settings
.spyderproject
.spyproject
.idea

# Rope project settings
.ropeproject
Expand Down
31 changes: 27 additions & 4 deletions EpiLog/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,28 @@
"""
EpiLog
# MIT License

"""
__version__ = "1.0.0"
# Copyright (c) 2023 Spill-Tea

# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:

# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.

# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
"""EpiLog Logging Manager Package."""

from .benchmark import BenchMark
from .manager import EpiLog


__version__ = "1.1.0"
102 changes: 89 additions & 13 deletions EpiLog/benchmark.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,24 +19,99 @@
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
"""
EpiLog/benchmark.py
"""Simple context manager to log procedure operation duration, for benchmark."""

from __future__ import annotations

"""
# Python Dependencies
import logging
from dataclasses import dataclass
from time import perf_counter_ns


@dataclass
class UnitTime:
"""Unit Time comparing string id to a modifier of next unit."""

unit: str
base: int = 1
modifier: int | None = None


NS_UNITS = (
UnitTime(unit="ns", modifier=1000),
UnitTime(unit="us", modifier=1000),
UnitTime(unit="ms", modifier=1000),
UnitTime(unit="s", modifier=60),
UnitTime(unit="min", modifier=60),
UnitTime(unit="hr", modifier=24),
UnitTime(unit="days", modifier=7),
UnitTime(unit="weeks", modifier=None),
)


def _update():
for n, current in enumerate(NS_UNITS[1:]):
previous = NS_UNITS[n]
current.base = previous.base * previous.modifier


_update()


def convert_units(time_ns: int) -> tuple[float, str]:
"""Get More Accurate Timing."""
new_time: float = float(time_ns)
text: str = "ns"

for u in NS_UNITS:
new_time = time_ns / u.base
text = u.unit
if u.modifier is None or new_time < u.modifier:
break

return new_time, text


def breakdown_units(value: int) -> dict[str, int]:
"""Split ns value into component time bins."""
data = {}
for unit in reversed(NS_UNITS):
data[unit.unit], value = divmod(value, unit.base)
return data


class BenchMark:
"""Context Manager to Benchmark any process through a log."""
__slots__ = ("enabled", "log", "description", "t0")
"""Context Manager to Benchmark any process through a log.
Args:
----
log (logging.Logger):
description (str): Message used to describe actions performed during benchmark.
level (int): Logging Level
def __init__(self, log: logging.Logger, description: str, level: int = logging.INFO):
self.enabled = log.isEnabledFor(level)
Attributes:
----------
enabled (bool): If Benchmark level is compatible with log level to emit message.
t0 (int): Entry time to benchmark suite.
"""

__slots__ = ("level", "enabled", "log", "description", "t0")

level: int
enabled: bool
log: logging.Logger
description: str
t0: int

def __init__(
self, log: logging.Logger, description: str, level: int = logging.INFO
):
self.level = level
self.enabled = log.isEnabledFor(self.level)
self.log = log
self.description = description
self.t0 = None
self.t0 = 0

def __enter__(self):
if self.enabled:
Expand All @@ -46,11 +121,12 @@ def __enter__(self):

def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is not None:
logging.error("Traceback...:\n", exc_info=(exc_type, exc_val, exc_tb))
self.log.error("Traceback:", exc_info=(exc_type, exc_val, exc_tb))
return

if not self.enabled:
elif not self.enabled:
return

end = perf_counter_ns()
elapsed = (end - self.t0) / 1_000_000
self.log.info("%s: %.4fms", self.description, elapsed)
elapsed, unit = convert_units(end - self.t0)
self.log.log(self.level, "%s: (%.4f %s)", self.description, elapsed, unit)
Loading

0 comments on commit a0a512e

Please sign in to comment.