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
15 changes: 13 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -198,8 +198,19 @@ jobs:
Write-Host "ERROR: No wheel found for $pyVer"
exit 1
}
Write-Host "Installing wheel: $($wheel.FullName)"
uv pip install "$($wheel.FullName)"
# Install with the export extra, so the pandas/pyarrow exporters are
# actually exercised rather than skipped. Taking the extra from the
# wheel keeps its contents defined in pyproject.toml alone.
Write-Host "Installing wheel: $($wheel.FullName)[export]"
uv pip install "$($wheel.FullName)[export]"
shell: pwsh

- name: Confirm the export extra is present
# The exporters are a documented feature, and their tests skip
# themselves when pandas or pyarrow is missing. Fail here rather than
# let that turn into silent non-coverage.
run: |
& .\.venv\Scripts\python.exe -c "import pandas, pyarrow; print('pandas', pandas.__version__, '/ pyarrow', pyarrow.__version__)"
shell: pwsh

- name: Run tests
Expand Down
242 changes: 150 additions & 92 deletions tests/test_export.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
"""Tests for data export functionality (v0.3.0 - #14, #33)."""

from __future__ import annotations

import importlib.util
import json
import os
import tempfile
from pathlib import Path

import pytest

Expand All @@ -23,6 +28,39 @@ def check_extension_available() -> bool:
)


def _installed(module: str) -> bool:
return importlib.util.find_spec(module) is not None


# The exporters keep pandas and pyarrow optional, importing them inside the
# functions that need them, so `pyetwkit.export` itself always imports. Deciding
# up front which optional dependency is present is what lets everything else
# assert rather than skip: wrapping a call in `except ImportError: skip` would
# also swallow a genuine ImportError raised inside the module and report a
# broken exporter as a missing extra.
requires_pandas = pytest.mark.skipif(
not _installed("pandas"), reason="pandas is not installed (pip install pyetwkit[export])"
)
requires_pyarrow = pytest.mark.skipif(
not _installed("pyarrow"), reason="pyarrow is not installed (pip install pyetwkit[export])"
)

SAMPLE_EVENTS = [
{
"provider_name": "Microsoft-Windows-DNS-Client",
"event_id": 3020,
"process_id": 4104,
"properties": {"QueryName": "example.com", "QueryType": 1},
},
{
"provider_name": "Microsoft-Windows-DNS-Client",
"event_id": 3006,
"process_id": 4104,
"properties": {"QueryName": "github.com", "QueryType": 28},
},
]


class TestEventToDict:
"""Tests for event to dict conversion."""

Expand All @@ -42,133 +80,153 @@ def test_event_to_dict_returns_dict(self) -> None:
assert hasattr(event_class, "to_dict")


class TestExportModule:
"""Tests for export module structure."""

def test_export_module_exists(self) -> None:
"""The module must import with no optional dependency present."""
from pyetwkit import export # noqa: F401

def test_export_all_functions(self) -> None:
"""Test that all export functions are available."""
from pyetwkit.export import (
to_arrow,
to_csv,
to_dataframe,
to_json,
to_jsonl,
to_parquet,
)

assert all(
callable(f) for f in [to_dataframe, to_csv, to_json, to_jsonl, to_parquet, to_arrow]
)


class TestToJSON:
"""JSON export needs no optional dependency."""

def test_to_json_function_exists(self) -> None:
from pyetwkit.export import to_json

assert callable(to_json)

def test_to_jsonl_function_exists(self) -> None:
from pyetwkit.export import to_jsonl

assert callable(to_jsonl)

def test_to_json_round_trips(self) -> None:
from pyetwkit.export import to_json

text = to_json(SAMPLE_EVENTS)
assert text is not None
assert [e["event_id"] for e in json.loads(text)] == [3020, 3006]

def test_to_jsonl_writes_one_object_per_line(self, tmp_path: Path) -> None:
from pyetwkit.export import to_jsonl

path = tmp_path / "events.jsonl"
to_jsonl(SAMPLE_EVENTS, path)
lines = path.read_text(encoding="utf-8").splitlines()
assert len(lines) == 2
assert json.loads(lines[0])["event_id"] == 3020


class TestToDataFrame:
"""Tests for DataFrame conversion."""

def test_to_dataframe_function_exists(self) -> None:
"""Test that to_dataframe function exists in export module."""
try:
from pyetwkit.export import to_dataframe
"""Available whether or not pandas is: the import is done lazily."""
from pyetwkit.export import to_dataframe

assert callable(to_dataframe)
except ImportError:
pytest.skip("pyetwkit.export module not available")
assert callable(to_dataframe)

@requires_pandas
def test_to_dataframe_empty_list(self) -> None:
"""Test converting empty list to dataframe."""
try:
from pyetwkit.export import to_dataframe
from pyetwkit.export import to_dataframe

df = to_dataframe([])
assert len(df) == 0
except ImportError:
pytest.skip("pyetwkit.export module not available")
df = to_dataframe([])
assert len(df) == 0

@requires_pandas
def test_to_dataframe_carries_the_events(self) -> None:
from pyetwkit.export import to_dataframe

df = to_dataframe(SAMPLE_EVENTS)
assert len(df) == 2
assert list(df["event_id"]) == [3020, 3006]


class TestToCSV:
"""Tests for CSV export."""
"""CSV export goes through pandas."""

def test_to_csv_function_exists(self) -> None:
"""Test that to_csv function exists."""
try:
from pyetwkit.export import to_csv
from pyetwkit.export import to_csv

assert callable(to_csv)
except ImportError:
pytest.skip("pyetwkit.export module not available")
assert callable(to_csv)

@requires_pandas
def test_to_csv_creates_file(self) -> None:
"""Test that to_csv creates a file."""
try:
from pyetwkit.export import to_csv
from pyetwkit.export import to_csv

with tempfile.NamedTemporaryFile(suffix=".csv", delete=False) as f:
temp_path = f.name
with tempfile.NamedTemporaryFile(suffix=".csv", delete=False) as f:
temp_path = f.name

try:
to_csv([], temp_path)
assert os.path.exists(temp_path)
finally:
if os.path.exists(temp_path):
os.unlink(temp_path)
except ImportError:
pytest.skip("pyetwkit.export module not available")


class TestToJSON:
"""Tests for JSON export."""

def test_to_json_function_exists(self) -> None:
"""Test that to_json function exists."""
try:
from pyetwkit.export import to_json
to_csv([], temp_path)
assert os.path.exists(temp_path)
finally:
if os.path.exists(temp_path):
os.unlink(temp_path)

assert callable(to_json)
except ImportError:
pytest.skip("pyetwkit.export module not available")
@requires_pandas
def test_to_csv_writes_the_events(self, tmp_path: Path) -> None:
from pyetwkit.export import to_csv

def test_to_jsonl_function_exists(self) -> None:
"""Test that to_jsonl function exists."""
try:
from pyetwkit.export import to_jsonl

assert callable(to_jsonl)
except ImportError:
pytest.skip("pyetwkit.export module not available")
path = tmp_path / "events.csv"
to_csv(SAMPLE_EVENTS, path)
text = path.read_text(encoding="utf-8")
assert "event_id" in text
assert "3020" in text


class TestToParquet:
"""Tests for Parquet export."""
"""Parquet export needs pyarrow."""

def test_to_parquet_function_exists(self) -> None:
"""Test that to_parquet function exists."""
try:
from pyetwkit.export import to_parquet
from pyetwkit.export import to_parquet

assert callable(to_parquet)
except ImportError:
pytest.skip("pyetwkit.export module not available")
assert callable(to_parquet)

@requires_pandas
@requires_pyarrow
def test_to_parquet_round_trips(self, tmp_path: Path) -> None:
import pandas as pd

class TestToArrow:
"""Tests for Arrow export."""
from pyetwkit.export import to_parquet

def test_to_arrow_function_exists(self) -> None:
"""Test that to_arrow function exists."""
try:
from pyetwkit.export import to_arrow
path = tmp_path / "events.parquet"
to_parquet(SAMPLE_EVENTS, path)
assert path.exists()
assert list(pd.read_parquet(path)["event_id"]) == [3020, 3006]

assert callable(to_arrow)
except ImportError:
pytest.skip("pyetwkit.export module not available")

class TestToArrow:
"""Arrow export needs pyarrow."""

class TestExportModule:
"""Tests for export module structure."""
def test_to_arrow_function_exists(self) -> None:
from pyetwkit.export import to_arrow

def test_export_module_exists(self) -> None:
"""Test that pyetwkit.export module exists."""
try:
from pyetwkit import export # noqa: F401
assert callable(to_arrow)

assert True
except ImportError:
pytest.skip("pyetwkit.export module not available")
@requires_pandas
@requires_pyarrow
def test_to_arrow_returns_a_table_of_the_events(self) -> None:
from pyetwkit.export import to_arrow

def test_export_all_functions(self) -> None:
"""Test that all export functions are available."""
try:
from pyetwkit.export import (
to_arrow,
to_csv,
to_dataframe,
to_json,
to_jsonl,
to_parquet,
)

assert all(
callable(f) for f in [to_dataframe, to_csv, to_json, to_jsonl, to_parquet, to_arrow]
)
except ImportError:
pytest.skip("pyetwkit.export module not available")
table = to_arrow(SAMPLE_EVENTS)
assert table.num_rows == 2
assert table.column("event_id").to_pylist() == [3020, 3006]
Loading