9194ef5abd
Docs/Test Workflow / Test docs build (push) Failing after 0s
Check links & references / links-check (push) Failing after 1s
Pytest/Test Workflow / Import Test and Pytest Run (ubuntu-latest, 3.10) (push) Failing after 0s
Pytest/Test Workflow / Import Test and Pytest Run (ubuntu-latest, 3.11) (push) Failing after 0s
PR Conflict Labeler / main (push) Failing after 2s
Pytest/Test Workflow / Import Test and Pytest Run (ubuntu-latest, 3.12) (push) Failing after 2s
Pytest/Test Workflow / Import Test and Pytest Run (ubuntu-latest, 3.13) (push) Failing after 0s
Pytest/Test Workflow / Build this Package (push) Failing after 5s
Pytest/Test Workflow / Import Test and Pytest Run (macos-latest, 3.10) (push) Has been cancelled
Pytest/Test Workflow / Import Test and Pytest Run (macos-latest, 3.11) (push) Has been cancelled
Pytest/Test Workflow / Import Test and Pytest Run (macos-latest, 3.12) (push) Has been cancelled
Pytest/Test Workflow / Import Test and Pytest Run (macos-latest, 3.13) (push) Has been cancelled
Pytest/Test Workflow / Import Test and Pytest Run (windows-latest, 3.10) (push) Has been cancelled
Pytest/Test Workflow / Import Test and Pytest Run (windows-latest, 3.11) (push) Has been cancelled
Pytest/Test Workflow / Import Test and Pytest Run (windows-latest, 3.12) (push) Has been cancelled
Pytest/Test Workflow / Import Test and Pytest Run (windows-latest, 3.13) (push) Has been cancelled
Pytest/Test Workflow / testing-guardian (push) Has been cancelled
324 lines
11 KiB
Python
324 lines
11 KiB
Python
import random
|
|
from contextlib import ExitStack as DoesNotRaise
|
|
from pathlib import Path
|
|
from typing import TypeVar
|
|
|
|
import numpy as np
|
|
import pytest
|
|
|
|
import supervision.dataset.utils as dataset_utils
|
|
from supervision import Detections
|
|
from supervision.dataset.utils import (
|
|
approximate_mask_with_polygons,
|
|
build_class_index_mapping,
|
|
check_no_basename_collisions,
|
|
map_detections_class_id,
|
|
merge_class_lists,
|
|
train_test_split,
|
|
)
|
|
from tests.helpers import _create_detections
|
|
|
|
T = TypeVar("T")
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("data", "train_ratio", "random_state", "shuffle", "expected_result", "exception"),
|
|
[
|
|
([], 0.5, None, False, ([], []), DoesNotRaise()), # empty data
|
|
(
|
|
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
|
|
0.5,
|
|
None,
|
|
False,
|
|
([0, 1, 2, 3, 4], [5, 6, 7, 8, 9]),
|
|
DoesNotRaise(),
|
|
), # data with 10 numbers and 50% train split
|
|
(
|
|
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
|
|
1.0,
|
|
None,
|
|
False,
|
|
([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], []),
|
|
DoesNotRaise(),
|
|
), # data with 10 numbers and 100% train split
|
|
(
|
|
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
|
|
0.0,
|
|
None,
|
|
False,
|
|
([], [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]),
|
|
DoesNotRaise(),
|
|
), # data with 10 numbers and 0% train split
|
|
(
|
|
["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"],
|
|
0.5,
|
|
None,
|
|
False,
|
|
(["a", "b", "c", "d", "e"], ["f", "g", "h", "i", "j"]),
|
|
DoesNotRaise(),
|
|
), # data with 10 chars and 50% train split
|
|
(
|
|
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
|
|
0.5,
|
|
23,
|
|
True,
|
|
([7, 8, 5, 6, 3], [2, 9, 0, 1, 4]),
|
|
DoesNotRaise(),
|
|
), # data with 10 numbers and 50% train split with 23 random seed
|
|
(
|
|
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
|
|
0.5,
|
|
32,
|
|
True,
|
|
([4, 6, 0, 8, 9], [5, 7, 2, 3, 1]),
|
|
DoesNotRaise(),
|
|
), # data with 10 numbers and 50% train split with 23 random seed
|
|
],
|
|
)
|
|
def test_train_test_split(
|
|
data: list[T],
|
|
train_ratio: float,
|
|
random_state: int,
|
|
shuffle: bool,
|
|
expected_result: tuple[list[T], list[T]] | None,
|
|
exception: Exception,
|
|
) -> None:
|
|
with exception:
|
|
result = train_test_split(
|
|
data=data,
|
|
train_ratio=train_ratio,
|
|
random_state=random_state,
|
|
shuffle=shuffle,
|
|
)
|
|
assert result == expected_result
|
|
|
|
|
|
def test_approximate_mask_with_polygons_default_preserves_polygon(
|
|
monkeypatch,
|
|
) -> None:
|
|
"""Default mask polygon conversion forwards zero simplification."""
|
|
percentages: list[float] = []
|
|
|
|
def fake_approximate_polygon(polygon: np.ndarray, percentage: float) -> np.ndarray:
|
|
"""Capture simplification percentage while preserving the polygon."""
|
|
percentages.append(percentage)
|
|
return polygon
|
|
|
|
monkeypatch.setattr(dataset_utils, "approximate_polygon", fake_approximate_polygon)
|
|
|
|
approximate_mask_with_polygons(np.ones((3, 3), dtype=bool))
|
|
|
|
assert percentages == [0.0]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("class_lists", "expected_result", "exception"),
|
|
[
|
|
([], [], DoesNotRaise()), # empty class lists
|
|
(
|
|
[["dog", "person"]],
|
|
["dog", "person"],
|
|
DoesNotRaise(),
|
|
), # single class list; already alphabetically sorted
|
|
(
|
|
[["person", "dog"]],
|
|
["dog", "person"],
|
|
DoesNotRaise(),
|
|
), # single class list; not alphabetically sorted
|
|
(
|
|
[["dog", "person"], ["dog", "person"]],
|
|
["dog", "person"],
|
|
DoesNotRaise(),
|
|
), # two class lists; the same classes; already alphabetically sorted
|
|
(
|
|
[["dog", "person"], ["cat"]],
|
|
["cat", "dog", "person"],
|
|
DoesNotRaise(),
|
|
), # two class lists; different classes; already alphabetically sorted
|
|
],
|
|
)
|
|
def test_merge_class_maps(
|
|
class_lists: list[list[str]], expected_result: list[str], exception: Exception
|
|
) -> None:
|
|
with exception:
|
|
result = merge_class_lists(class_lists=class_lists)
|
|
assert result == expected_result
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("source_classes", "target_classes", "expected_result", "exception"),
|
|
[
|
|
([], [], {}, DoesNotRaise()), # empty class lists
|
|
([], ["dog", "person"], {}, DoesNotRaise()), # empty source class list
|
|
(
|
|
["dog", "person"],
|
|
[],
|
|
None,
|
|
pytest.raises(ValueError, match="Class dog not found"),
|
|
), # empty target class list
|
|
(
|
|
["dog", "person"],
|
|
["dog", "person"],
|
|
{0: 0, 1: 1},
|
|
DoesNotRaise(),
|
|
), # same class lists
|
|
(
|
|
["dog", "person"],
|
|
["person", "dog"],
|
|
{0: 1, 1: 0},
|
|
DoesNotRaise(),
|
|
), # same class lists but not alphabetically sorted
|
|
(
|
|
["dog", "person"],
|
|
["cat", "dog", "person"],
|
|
{0: 1, 1: 2},
|
|
DoesNotRaise(),
|
|
), # source class list is a subset of target class list
|
|
(
|
|
["dog", "person"],
|
|
["cat", "dog"],
|
|
None,
|
|
pytest.raises(ValueError, match="Class person not found"),
|
|
), # source class list is not a subset of target class list
|
|
],
|
|
)
|
|
def test_build_class_index_mapping(
|
|
source_classes: list[str],
|
|
target_classes: list[str],
|
|
expected_result: dict[int, int] | None,
|
|
exception: Exception,
|
|
) -> None:
|
|
with exception:
|
|
result = build_class_index_mapping(
|
|
source_classes=source_classes, target_classes=target_classes
|
|
)
|
|
assert result == expected_result
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("source_to_target_mapping", "detections", "expected_result", "exception"),
|
|
[
|
|
(
|
|
{},
|
|
_create_detections(xyxy=[[0, 0, 10, 10]], class_id=[0]),
|
|
None,
|
|
pytest.raises(ValueError, match="subset of source_to_target_mapping"),
|
|
), # empty mapping
|
|
(
|
|
{0: 1},
|
|
_create_detections(xyxy=[[0, 0, 10, 10]], class_id=[0]),
|
|
_create_detections(xyxy=[[0, 0, 10, 10]], class_id=[1]),
|
|
DoesNotRaise(),
|
|
), # single mapping
|
|
(
|
|
{0: 1, 1: 2},
|
|
Detections.empty(),
|
|
Detections.empty(),
|
|
DoesNotRaise(),
|
|
), # empty detections
|
|
(
|
|
{0: 1, 1: 2},
|
|
_create_detections(xyxy=[[0, 0, 10, 10]], class_id=[0]),
|
|
_create_detections(xyxy=[[0, 0, 10, 10]], class_id=[1]),
|
|
DoesNotRaise(),
|
|
), # multiple mappings
|
|
(
|
|
{0: 1, 1: 2},
|
|
_create_detections(xyxy=[[0, 0, 10, 10], [0, 0, 10, 10]], class_id=[0, 1]),
|
|
_create_detections(xyxy=[[0, 0, 10, 10], [0, 0, 10, 10]], class_id=[1, 2]),
|
|
DoesNotRaise(),
|
|
), # multiple mappings
|
|
(
|
|
{0: 1, 1: 2},
|
|
_create_detections(xyxy=[[0, 0, 10, 10]], class_id=[2]),
|
|
None,
|
|
pytest.raises(ValueError, match="source_to_target_mapping keys"),
|
|
), # class_id not in mapping
|
|
(
|
|
{0: 1, 1: 2},
|
|
_create_detections(xyxy=[[0, 0, 10, 10]], class_id=[0], confidence=[0.5]),
|
|
_create_detections(xyxy=[[0, 0, 10, 10]], class_id=[1], confidence=[0.5]),
|
|
DoesNotRaise(),
|
|
), # confidence is not None
|
|
],
|
|
)
|
|
def test_map_detections_class_id(
|
|
source_to_target_mapping: dict[int, int],
|
|
detections: Detections,
|
|
expected_result: Detections | None,
|
|
exception: Exception,
|
|
) -> None:
|
|
with exception:
|
|
result = map_detections_class_id(
|
|
source_to_target_mapping=source_to_target_mapping, detections=detections
|
|
)
|
|
assert result == expected_result
|
|
|
|
|
|
class TestTrainTestSplitRngIsolation:
|
|
"""Regression tests for train_test_split RNG isolation (DAT-02)."""
|
|
|
|
def test_does_not_mutate_input_list(self) -> None:
|
|
"""split() must not reorder the caller's list in place."""
|
|
data = list(range(10))
|
|
original = data.copy()
|
|
train_test_split(data=data, train_ratio=0.5, random_state=42, shuffle=True)
|
|
assert data == original
|
|
|
|
def test_does_not_pollute_global_rng(self) -> None:
|
|
"""split() must not disturb the process-global random state."""
|
|
state_before = random.getstate()
|
|
train_test_split(
|
|
data=list(range(10)), train_ratio=0.5, random_state=42, shuffle=True
|
|
)
|
|
assert random.getstate() == state_before
|
|
|
|
def test_result_independent_of_global_rng(self) -> None:
|
|
"""A fixed random_state yields the same split regardless of global RNG."""
|
|
first = train_test_split(
|
|
data=list(range(10)), train_ratio=0.5, random_state=42, shuffle=True
|
|
)
|
|
for _ in range(5):
|
|
random.random() # noqa: S311 — perturb global RNG; split must ignore it
|
|
second = train_test_split(
|
|
data=list(range(10)), train_ratio=0.5, random_state=42, shuffle=True
|
|
)
|
|
assert first == second
|
|
|
|
|
|
class TestCheckNoBasenameCollisions:
|
|
"""Regression tests for export basename collision detection (DAT-04)."""
|
|
|
|
def test_raises_on_colliding_output_names(self) -> None:
|
|
"""Two source paths mapping to one output name must raise ValueError."""
|
|
with pytest.raises(ValueError, match="both map to image file"):
|
|
check_no_basename_collisions(
|
|
image_paths=["a/img.jpg", "b/img.jpg"],
|
|
key=lambda image_path: Path(image_path).name,
|
|
output_kind="image",
|
|
)
|
|
|
|
def test_passes_on_unique_output_names(self) -> None:
|
|
"""Distinct output names must not raise."""
|
|
check_no_basename_collisions(
|
|
image_paths=["a/img1.jpg", "b/img2.jpg"],
|
|
key=lambda image_path: Path(image_path).name,
|
|
output_kind="image",
|
|
)
|
|
|
|
def test_passes_on_empty_image_paths(self) -> None:
|
|
"""Empty list must not raise (vacuously no collision)."""
|
|
check_no_basename_collisions(
|
|
image_paths=[],
|
|
key=lambda image_path: Path(image_path).name,
|
|
output_kind="image",
|
|
)
|
|
|
|
def test_passes_on_single_image_path(self) -> None:
|
|
"""Single element list cannot collide with itself."""
|
|
check_no_basename_collisions(
|
|
image_paths=["dir/only.jpg"],
|
|
key=lambda image_path: Path(image_path).name,
|
|
output_kind="image",
|
|
)
|