Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 10 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,21 @@

[![Downloads](https://static.pepy.tech/badge/locklib/month)](https://pepy.tech/project/locklib)
[![Downloads](https://static.pepy.tech/badge/locklib)](https://pepy.tech/project/locklib)
[![Coverage Status](https://coveralls.io/repos/github/pomponchik/locklib/badge.svg?branch=main)](https://coveralls.io/github/pomponchik/locklib?branch=main)
[![Lines of code](https://sloc.xyz/github/pomponchik/locklib/?category=code?)](https://github.com/boyter/scc/)
[![Hits-of-Code](https://hitsofcode.com/github/pomponchik/locklib?branch=main)](https://hitsofcode.com/github/pomponchik/locklib/view?branch=main)
[![Test-Package](https://github.com/pomponchik/locklib/actions/workflows/tests_and_coverage.yml/badge.svg)](https://github.com/pomponchik/locklib/actions/workflows/tests_and_coverage.yml)
[![Coverage Status](https://coveralls.io/repos/github/mutating/locklib/badge.svg?branch=main)](https://coveralls.io/github/mutating/locklib?branch=main)
[![Lines of code](https://sloc.xyz/github/mutating/locklib/?category=code?)](https://github.com/boyter/scc/)
[![Hits-of-Code](https://hitsofcode.com/github/mutating/locklib?branch=main)](https://hitsofcode.com/github/mutating/locklib/view?branch=main)
[![Test-Package](https://github.com/mutating/locklib/actions/workflows/tests_and_coverage.yml/badge.svg)](https://github.com/mutating/locklib/actions/workflows/tests_and_coverage.yml)
[![Python versions](https://img.shields.io/pypi/pyversions/locklib.svg)](https://pypi.python.org/pypi/locklib)
[![PyPI version](https://badge.fury.io/py/locklib.svg)](https://badge.fury.io/py/locklib)
[![Checked with mypy](http://www.mypy-lang.org/static/mypy_badge.svg)](http://mypy-lang.org/)
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
[![DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/pomponchik/locklib)
[![DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/mutating/locklib)

</details>

<p align="center">

![logo](https://raw.githubusercontent.com/pomponchik/locklib/develop/docs/assets/logo_7.svg)
![logo](https://raw.githubusercontent.com/mutating/locklib/develop/docs/assets/logo_7.svg)

</p>

Expand All @@ -43,7 +43,7 @@ pip install locklib
... or directly from git:

```bash
pip install git+https://github.com/pomponchik/locklib.git
pip install git+https://github.com/mutating/locklib.git
```

You can also quickly try out this and other packages without having to install using [instld](https://github.com/pomponchik/instld).
Expand Down Expand Up @@ -219,4 +219,6 @@ If the `notify` method was called with the same parameter only when the lock act

How does it work? A modified [algorithm for determining the correct parenthesis sequence](https://ru.wikipedia.org/wiki/%D0%9F%D1%80%D0%B0%D0%B2%D0%B8%D0%BB%D1%8C%D0%BD%D0%B0%D1%8F_%D1%81%D0%BA%D0%BE%D0%B1%D0%BE%D1%87%D0%BD%D0%B0%D1%8F_%D0%BF%D0%BE%D1%81%D0%BB%D0%B5%D0%B4%D0%BE%D0%B2%D0%B0%D1%82%D0%B5%D0%BB%D1%8C%D0%BD%D0%BE%D1%81%D1%82%D1%8C) is used here. For each thread for which any events were registered (taking the mutex, releasing the mutex, and also calling the `notify` method), the check takes place separately, that is, we determine that it was the same thread that held the mutex when `notify` was called, and not some other one.

> ⚠️ The thread id is used to identify the streams. This id may be reused if the current thread ends, which in some cases may lead to incorrect identification of lock coverage for operations that were not actually covered by the lock. Make sure that this cannot happen during your test.
> ⚠️ The thread id is used to identify the threads. This id may be reused if the current thread ends, which in some cases may lead to incorrect identification of lock coverage for operations that were not actually covered by the lock. Make sure that this cannot happen during your test.

If no event with the specified identifier was recorded in any of the threads, the `ThereWasNoSuchEventError` exception will be raised by default. If you want to disable this so that the method simply returns `False` in such situations, pass the additional argument `raise_exception=False` to `was_event_locked`.
3 changes: 3 additions & 0 deletions locklib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
from locklib.errors import (
StrangeEventOrderError as StrangeEventOrderError,
)
from locklib.errors import (
ThereWasNoSuchEventError as ThereWasNoSuchEventError,
)
from locklib.locks.smart_lock.lock import SmartLock as SmartLock
from locklib.locks.tracer.tracer import (
LockTraceWrapper as LockTraceWrapper,
Expand Down
7 changes: 5 additions & 2 deletions locklib/errors.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
class DeadLockError(Exception):
pass
...

class StrangeEventOrderError(Exception):
pass
...

class ThereWasNoSuchEventError(Exception):
...
21 changes: 15 additions & 6 deletions locklib/locks/tracer/tracer.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from types import TracebackType
from typing import Dict, List, Optional, Type

from locklib.errors import StrangeEventOrderError
from locklib.errors import StrangeEventOrderError, ThereWasNoSuchEventError
from locklib.locks.tracer.events import TracerEvent, TracerEventType
from locklib.protocols.lock import LockProtocol

Expand Down Expand Up @@ -46,9 +46,11 @@ def notify(self, identifier: str) -> None:
),
)

def was_event_locked(self, identifier: str) -> bool:
def was_event_locked(self, identifier: str, raise_exception: bool = True) -> bool:
stacks: Dict[int, List[TracerEvent]] = defaultdict(list)

there_was_action_with_this_identifier = False

for event in self.trace:
stack = stacks[event.thread_id]

Expand All @@ -57,11 +59,18 @@ def was_event_locked(self, identifier: str) -> bool:

elif event.type == TracerEventType.RELEASE:
if not stack:
raise StrangeEventOrderError('Release event without corresponding acquire event.')
if raise_exception:
raise StrangeEventOrderError('Release event without corresponding acquire event.')
return False
stack.pop()

elif event.type == TracerEventType.ACTION:
if event.identifier == identifier and not stack:
return False
if event.identifier == identifier:
there_was_action_with_this_identifier = True
if not stack:
return False

if (not there_was_action_with_this_identifier) and raise_exception:
raise ThereWasNoSuchEventError(f'No events with identifier "{identifier}" occurred in any of the threads, so the question "was it thread-safe" is meaningless.')

return True
return there_was_action_with_this_identifier
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = 'setuptools.build_meta'

[project]
name = 'locklib'
version = '0.0.19'
version = '0.0.20'
authors = [
{ name='Evgeniy Blinov', email='zheni-b@yandex.ru' },
]
Expand Down
32 changes: 28 additions & 4 deletions tests/units/locks/tracer/test_tracer.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
from typing import List, Union

import pytest
from full_match import match

from locklib import LockTraceWrapper, StrangeEventOrderError
from locklib import LockTraceWrapper, StrangeEventOrderError, ThereWasNoSuchEventError
from locklib.locks.tracer.events import TracerEvent, TracerEventType


Expand Down Expand Up @@ -72,11 +73,22 @@ def release(self):
with pytest.raises(StrangeEventOrderError):
wrapper.was_event_locked('kek')

with pytest.raises(StrangeEventOrderError):
wrapper.was_event_locked('kek', raise_exception=True)

assert not wrapper.was_event_locked('kek', raise_exception=False)


def test_event_is_locked_if_there_was_no_events():
wrapper = LockTraceWrapper(Lock())

assert wrapper.was_event_locked('kek')
assert not wrapper.was_event_locked('kek', raise_exception=False)

with pytest.raises(ThereWasNoSuchEventError, match=match('No events with identifier "kek" occurred in any of the threads, so the question "was it thread-safe" is meaningless.')):
wrapper.was_event_locked('kek')

with pytest.raises(ThereWasNoSuchEventError, match=match('No events with identifier "kek" occurred in any of the threads, so the question "was it thread-safe" is meaningless.')):
wrapper.was_event_locked('kek', raise_exception=True)


def test_event_is_locked_if_there_are_only_opening_and_slosing_events():
Expand All @@ -85,7 +97,13 @@ def test_event_is_locked_if_there_are_only_opening_and_slosing_events():
with wrapper:
pass

assert wrapper.was_event_locked('kek')
assert not wrapper.was_event_locked('kek', raise_exception=False)

with pytest.raises(ThereWasNoSuchEventError, match=match('No events with identifier "kek" occurred in any of the threads, so the question "was it thread-safe" is meaningless.')):
wrapper.was_event_locked('kek')

with pytest.raises(ThereWasNoSuchEventError, match=match('No events with identifier "kek" occurred in any of the threads, so the question "was it thread-safe" is meaningless.')):
wrapper.was_event_locked('kek', raise_exception=True)


def test_simple_case_of_locked_event():
Expand Down Expand Up @@ -208,4 +226,10 @@ def test_unknown_event_type():

wrapper.trace.append(TracerEvent('unknown', 1))

assert wrapper.was_event_locked('lol')
assert not wrapper.was_event_locked('lol', raise_exception=False)

with pytest.raises(ThereWasNoSuchEventError, match=match('No events with identifier "lol" occurred in any of the threads, so the question "was it thread-safe" is meaningless.')):
wrapper.was_event_locked('lol')

with pytest.raises(ThereWasNoSuchEventError, match=match('No events with identifier "lol" occurred in any of the threads, so the question "was it thread-safe" is meaningless.')):
wrapper.was_event_locked('lol', raise_exception=True)
Loading