Files
unstructured-io--unstructured/test_unstructured/test_telemetry.py
T
wehub-resource-sync 461bf6fd40
CI / lint (3.11) (push) Blocked by required conditions
CI / lint (3.12) (push) Blocked by required conditions
CI / lint (3.13) (push) Blocked by required conditions
CI / shellcheck (push) Waiting to run
CI / shfmt (push) Waiting to run
CI / setup (3.11) (push) Waiting to run
CI / setup (3.12) (push) Waiting to run
CI / setup (3.13) (push) Waiting to run
CI / check-licenses (3.12) (push) Blocked by required conditions
CI / test_unit (3.11) (push) Blocked by required conditions
CI / test_unit (3.12) (push) Blocked by required conditions
CI / test_unit (3.13) (push) Blocked by required conditions
CI / test_unit_no_extras (3.11) (push) Blocked by required conditions
CI / test_unit_no_extras (3.12) (push) Blocked by required conditions
CI / test_json_to_html (3.12) (push) Blocked by required conditions
CI / test_unit_no_extras (3.13) (push) Blocked by required conditions
CI / test_unit_dependency_extras (csv, 3.11, --extra csv) (push) Blocked by required conditions
CI / test_unit_dependency_extras (csv, 3.12, --extra csv) (push) Blocked by required conditions
CI / test_unit_dependency_extras (csv, 3.13, --extra csv) (push) Blocked by required conditions
CI / test_unit_dependency_extras (docx, 3.11, --extra docx) (push) Blocked by required conditions
CI / test_unit_dependency_extras (docx, 3.12, --extra docx) (push) Blocked by required conditions
CI / test_unit_dependency_extras (docx, 3.13, --extra docx) (push) Blocked by required conditions
CI / test_unit_dependency_extras (markdown, 3.11, --extra md) (push) Blocked by required conditions
CI / test_unit_dependency_extras (markdown, 3.12, --extra md) (push) Blocked by required conditions
CI / test_unit_dependency_extras (markdown, 3.13, --extra md) (push) Blocked by required conditions
CI / test_unit_dependency_extras (odt, 3.11, --extra odt) (push) Blocked by required conditions
CI / test_unit_dependency_extras (odt, 3.12, --extra odt) (push) Blocked by required conditions
CI / test_unit_dependency_extras (pdf-image, 3.12, --extra pdf --extra image --extra paddleocr) (push) Blocked by required conditions
CI / test_unit_dependency_extras (pdf-image, 3.13, --extra pdf --extra image --extra paddleocr) (push) Blocked by required conditions
CI / test_unit_dependency_extras (pptx, 3.13, --extra pptx) (push) Blocked by required conditions
CI / test_unit_dependency_extras (odt, 3.13, --extra odt) (push) Blocked by required conditions
CI / test_unit_dependency_extras (pdf-image, 3.11, --extra pdf --extra image --extra paddleocr) (push) Blocked by required conditions
CI / test_unit_dependency_extras (pptx, 3.11, --extra pptx) (push) Blocked by required conditions
CI / test_unit_dependency_extras (pptx, 3.12, --extra pptx) (push) Blocked by required conditions
CI / test_unit_dependency_extras (pypandoc, 3.11, --extra epub --extra org --extra rtf --extra rst) (push) Blocked by required conditions
CI / test_unit_dependency_extras (pypandoc, 3.12, --extra epub --extra org --extra rtf --extra rst) (push) Blocked by required conditions
CI / test_unit_dependency_extras (pypandoc, 3.13, --extra epub --extra org --extra rtf --extra rst) (push) Blocked by required conditions
CI / test_unit_dependency_extras (xlsx, 3.11, --extra xlsx) (push) Blocked by required conditions
CI / test_unit_dependency_extras (xlsx, 3.12, --extra xlsx) (push) Blocked by required conditions
CI / test_unit_dependency_extras (xlsx, 3.13, --extra xlsx) (push) Blocked by required conditions
CI / test_ingest_src (3.12) (push) Blocked by required conditions
CI / test_json_to_markdown (3.12) (push) Blocked by required conditions
CI / changelog (push) Waiting to run
CI / test_dockerfile (push) Blocked by required conditions
CodeQL / Analyze (python) (push) Waiting to run
Build And Push Docker Image / set-short-sha (push) Waiting to run
Build And Push Docker Image / build-images (linux/amd64, opensource-linux-8core) (push) Blocked by required conditions
Build And Push Docker Image / build-images (linux/arm64, ubuntu-24.04-arm) (push) Blocked by required conditions
Build And Push Docker Image / publish-images (push) Blocked by required conditions
Partition Benchmark / setup (push) Waiting to run
Partition Benchmark / Measure and compare partition() runtime (push) Blocked by required conditions
chore: import upstream snapshot with attribution
2026-07-13 13:33:56 +08:00

258 lines
10 KiB
Python

"""Hermetic telemetry tests: env is set to opt-out before importing unstructured.
This module must set DO_NOT_TRACK (or equivalent) before any import of unstructured
so that init_telemetry() runs with opt-out at import time and no real network/subprocess
occurs. Tests then use monkeypatch and mocks to assert behavior.
"""
from __future__ import annotations
# Set opt-out before any unstructured import so package init does not run telemetry.
import os
os.environ["DO_NOT_TRACK"] = "1"
os.environ.pop("UNSTRUCTURED_TELEMETRY_ENABLED", None)
os.environ.pop("SCARF_NO_ANALYTICS", None)
import platform
import subprocess
import sys
from pathlib import Path
from unittest.mock import Mock
import pytest
import requests
from unstructured import utils
@pytest.fixture
def telemetry_mocks(monkeypatch):
"""Clear telemetry env and patch requests.get + subprocess.check_output.
Returns (mock_get, mock_subprocess). Use for both send and no-send tests so
we can assert network and subprocess side effects.
"""
monkeypatch.delenv("UNSTRUCTURED_TELEMETRY_ENABLED", raising=False)
monkeypatch.delenv("SCARF_NO_ANALYTICS", raising=False)
monkeypatch.delenv("DO_NOT_TRACK", raising=False)
mock_get = Mock()
mock_subprocess = Mock()
monkeypatch.setattr("unstructured.utils.requests.get", mock_get)
monkeypatch.setattr("unstructured.utils.subprocess.check_output", mock_subprocess)
return mock_get, mock_subprocess
def _apply_telemetry_env(monkeypatch, env_overrides):
"""Set env vars from dict; keys are env var names, values are str or None (delenv)."""
for key, value in env_overrides.items():
if value is None:
monkeypatch.delenv(key, raising=False)
else:
monkeypatch.setenv(key, value)
class DescribeScarfAnalytics:
"""Tests for scarf_analytics (telemetry off by default, opt-in only)."""
def it_telemetry_opt_out_any_non_empty_for_both_vars(self, monkeypatch):
"""Contract: DO_NOT_TRACK and SCARF_NO_ANALYTICS both opt out on any non-empty value."""
monkeypatch.delenv("DO_NOT_TRACK", raising=False)
monkeypatch.delenv("SCARF_NO_ANALYTICS", raising=False)
assert utils._telemetry_opt_out() is False
monkeypatch.setenv("DO_NOT_TRACK", "yes")
assert utils._telemetry_opt_out() is True
monkeypatch.delenv("DO_NOT_TRACK", raising=False)
monkeypatch.setenv("SCARF_NO_ANALYTICS", "on")
assert utils._telemetry_opt_out() is True
def it_telemetry_opt_in_only_true_or_1(self, monkeypatch):
"""Contract: only UNSTRUCTURED_TELEMETRY_ENABLED in ('true','1') opts in."""
monkeypatch.delenv("UNSTRUCTURED_TELEMETRY_ENABLED", raising=False)
assert utils._telemetry_opt_in() is False
for val in ("true", "1", "True", "TRUE"):
monkeypatch.setenv("UNSTRUCTURED_TELEMETRY_ENABLED", val)
assert utils._telemetry_opt_in() is True
for val in ("false", "0", "yes", ""):
monkeypatch.setenv("UNSTRUCTURED_TELEMETRY_ENABLED", val)
assert utils._telemetry_opt_in() is False
@pytest.mark.parametrize(
"env_overrides",
[
{},
{"DO_NOT_TRACK": "true"},
{"DO_NOT_TRACK": "1"},
{"DO_NOT_TRACK": "TRUE"},
{"DO_NOT_TRACK": "false"},
{"DO_NOT_TRACK": "0"},
{"SCARF_NO_ANALYTICS": "true"},
{"SCARF_NO_ANALYTICS": "yes"},
{"SCARF_NO_ANALYTICS": "on"},
{"SCARF_NO_ANALYTICS": "1"},
{"SCARF_NO_ANALYTICS": "TRUE"},
{"SCARF_NO_ANALYTICS": "false"},
{"SCARF_NO_ANALYTICS": "0"},
{"SCARF_NO_ANALYTICS": " true "},
{"UNSTRUCTURED_TELEMETRY_ENABLED": "false"},
{"UNSTRUCTURED_TELEMETRY_ENABLED": "0"},
{"UNSTRUCTURED_TELEMETRY_ENABLED": "yes"},
{"UNSTRUCTURED_TELEMETRY_ENABLED": "FALSE"},
{"UNSTRUCTURED_TELEMETRY_ENABLED": "true", "DO_NOT_TRACK": "true"},
{"UNSTRUCTURED_TELEMETRY_ENABLED": "true", "SCARF_NO_ANALYTICS": "on"},
],
ids=[
"default_no_opt_in",
"DO_NOT_TRACK=true",
"DO_NOT_TRACK=1",
"DO_NOT_TRACK=TRUE",
"DO_NOT_TRACK=false",
"DO_NOT_TRACK=0",
"SCARF_NO_ANALYTICS=true",
"SCARF_NO_ANALYTICS=yes",
"SCARF_NO_ANALYTICS=on",
"SCARF_NO_ANALYTICS=1",
"SCARF_NO_ANALYTICS=TRUE",
"SCARF_NO_ANALYTICS=false",
"SCARF_NO_ANALYTICS=0",
"SCARF_NO_ANALYTICS=whitespace",
"opt_in=false",
"opt_in=0",
"opt_in=yes",
"opt_in=FALSE",
"opt_in_true_but_DO_NOT_TRACK",
"opt_in_true_but_SCARF_NO_ANALYTICS",
],
)
def it_does_not_send_telemetry_when_disabled_or_opted_out(
self, monkeypatch, telemetry_mocks, env_overrides
):
"""No network or subprocess when telemetry disabled or opt-out set."""
mock_get, mock_subprocess = telemetry_mocks
_apply_telemetry_env(monkeypatch, env_overrides)
utils.scarf_analytics()
mock_get.assert_not_called()
mock_subprocess.assert_not_called()
@pytest.mark.parametrize("opt_in_value", ["true", "True", "TRUE", "1"])
def it_sends_telemetry_when_opt_in_is_set(self, monkeypatch, telemetry_mocks, opt_in_value):
mock_get, mock_subprocess = telemetry_mocks
_apply_telemetry_env(monkeypatch, {"UNSTRUCTURED_TELEMETRY_ENABLED": opt_in_value})
utils.scarf_analytics()
mock_get.assert_called_once()
mock_subprocess.assert_called_once_with(["nvidia-smi"], stderr=subprocess.DEVNULL)
call_args = mock_get.call_args
assert call_args[0][0] == "https://packages.unstructured.io/python-telemetry"
params = call_args[1]["params"]
assert set(params.keys()) == {"version", "platform", "python", "arch", "gpu", "dev"}
assert call_args[1]["timeout"] == 10
@pytest.mark.parametrize(
("version_val", "expected_dev"),
[("1.2.3.dev0", "true"), ("1.2.3", "false")],
ids=["dev_version", "release_version"],
)
def it_sends_telemetry_with_correct_dev_param(
self, monkeypatch, telemetry_mocks, version_val, expected_dev
):
mock_get, mock_subprocess = telemetry_mocks
_apply_telemetry_env(monkeypatch, {"UNSTRUCTURED_TELEMETRY_ENABLED": "true"})
monkeypatch.setattr("unstructured.utils.__version__", version_val)
utils.scarf_analytics()
mock_get.assert_called_once()
mock_subprocess.assert_called_once()
params = mock_get.call_args[1]["params"]
assert params["dev"] == expected_dev
assert params["version"] == version_val
assert params["platform"] == platform.system()
assert params["arch"] == platform.machine()
assert mock_get.call_args[1]["timeout"] == 10
def it_handles_requests_exception_gracefully(self, monkeypatch, telemetry_mocks):
mock_get, mock_subprocess = telemetry_mocks
mock_get.side_effect = requests.RequestException("network error")
_apply_telemetry_env(monkeypatch, {"UNSTRUCTURED_TELEMETRY_ENABLED": "true"})
utils.scarf_analytics() # does not raise
mock_get.assert_called_once()
mock_subprocess.assert_called_once()
assert mock_get.call_args[0][0] == "https://packages.unstructured.io/python-telemetry"
assert "version" in mock_get.call_args[1]["params"]
@pytest.mark.parametrize(
"exc",
[
OSError(),
PermissionError("nvidia-smi denied"),
subprocess.CalledProcessError(returncode=1, cmd=["nvidia-smi"]),
],
ids=["OSError", "PermissionError", "CalledProcessError"],
)
def it_handles_nvidia_smi_failure_gracefully(self, monkeypatch, telemetry_mocks, exc):
"""nvidia-smi probe failures must not propagate; telemetry still sends with gpu=False."""
mock_get, mock_subprocess = telemetry_mocks
mock_subprocess.side_effect = exc
_apply_telemetry_env(monkeypatch, {"UNSTRUCTURED_TELEMETRY_ENABLED": "true"})
utils.scarf_analytics() # does not raise
mock_get.assert_called_once()
assert mock_get.call_args[1]["params"]["gpu"] == "False"
mock_subprocess.assert_called_once_with(["nvidia-smi"], stderr=subprocess.DEVNULL)
def it_import_unstructured_succeeds_with_opt_out(self):
"""Import path with opt-out env does not crash (integration-style)."""
project_root = Path(__file__).resolve().parent.parent
env = {k: v for k, v in os.environ.items() if k != "UNSTRUCTURED_TELEMETRY_ENABLED"}
env.update(
{
"DO_NOT_TRACK": "1",
"SCARF_NO_ANALYTICS": "1",
"UNSTRUCTURED_TELEMETRY_ENABLED": "",
"PYTHONPATH": str(project_root),
}
)
result = subprocess.run(
[sys.executable, "-c", "import unstructured; print('ok')"],
env=env,
cwd=project_root,
capture_output=True,
text=True,
timeout=30,
)
assert result.returncode == 0, result.stderr or result.stdout
assert "ok" in result.stdout
def it_import_unstructured_runs_telemetry_once_when_opt_in(self):
"""Import path with opt-in runs init_telemetry exactly once (patch then import)."""
project_root = Path(__file__).resolve().parent.parent
env = {
k: v
for k, v in os.environ.items()
if k not in ("DO_NOT_TRACK", "SCARF_NO_ANALYTICS", "UNSTRUCTURED_TELEMETRY_ENABLED")
}
env.update(
{
"UNSTRUCTURED_TELEMETRY_ENABLED": "true",
"PYTHONPATH": str(project_root),
}
)
script = """
from unittest.mock import Mock, patch
m_get = Mock()
m_subprocess = Mock()
with patch('requests.get', m_get), patch('subprocess.check_output', m_subprocess):
import unstructured
exit(0 if (m_get.call_count == 1 and m_subprocess.call_count == 1) else 1)
"""
result = subprocess.run(
[sys.executable, "-c", script],
env=env,
cwd=project_root,
capture_output=True,
text=True,
timeout=30,
)
assert result.returncode == 0, (
"Import with opt-in should run telemetry exactly once (requests.get and "
"subprocess.check_output each called once). "
f"stderr={result.stderr!r} stdout={result.stdout!r}"
)