Skip to content

refactor(tests): storage, serializer, and cloud based unit tests to follow test conventions#3249

Open
agsaru wants to merge 2 commits into
Netflix:masterfrom
agsaru:refactor-pytest-unit-storage
Open

refactor(tests): storage, serializer, and cloud based unit tests to follow test conventions#3249
agsaru wants to merge 2 commits into
Netflix:masterfrom
agsaru:refactor-pytest-unit-storage

Conversation

@agsaru

@agsaru agsaru commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

PR Type

  • Bug fix
  • New feature
  • Core Runtime change (higher bar -- see CONTRIBUTING.md)
  • Docs / tooling
  • Refactoring

Summary

Copilot AI review requested due to automatic review settings June 8, 2026 19:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

This PR refactors and expands the unit test suite to be more parameterized, readable, and resilient to global registry state leaks (notably around serializer registration / bootstrap flows).

Changes:

  • Converted many repetitive tests to pytest.mark.parametrize with clearer ids and docstrings.
  • Added/standardized fixtures to isolate or restore global SerializerStore state and to dynamically create serializer modules for lifecycle tests.
  • Improved coverage for serializer lifecycle behaviors (bootstrap, pending imports, retries, reset) and tightened S3 / CAS / Kubernetes / Argo CLI test structure.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
test/unit/test_to_pod.py Parameterized POD conversion tests and clarified callable/lambda expectations.
test/unit/test_serializer_public_api.py Consolidated public API “not exported” checks and improved pickle-serializer smoke test assertions.
test/unit/test_serializer_lifecycle.py Added fixtures for serializer store isolation and dynamic module creation; expanded lifecycle coverage.
test/unit/test_serializer_integration.py Refactored integration tests, added store-restoration fixture, and clarified exception-flow scenarios.
test/unit/test_s3_storage.py Introduced an S3Storage fixture and improved test readability around metadata preservation.
test/unit/test_s3_empty_input.py Reworked empty-input regression tests into function-style tests with fixtures and parametrization.
test/unit/test_pickle_serializer.py Improved isolation for ordering test and added clearer parametrization ids/docstrings.
test/unit/test_local_metadata_provider.py Parameterized LocalMetadataProvider run-id deduction cases.
test/unit/test_kubernetes.py Added ids and renamed tests for clearer intent around labels/key-value parsing.
test/unit/test_content_addressed_store.py Modernized helpers/docs and strengthened assertions around error-message correctness.
test/unit/test_compute_resource_attributes.py Consolidated resource-attribute merging tests into a single parametrized test matrix.
test/unit/test_aws_util.py Simplified exception testing using nullcontext() vs manual try/except.
test/unit/test_artifact_serializer.py Added registry restoration/cleanup fixtures and expanded serializer store + lazy import tests.
test/unit/test_argo_workflows_cli.py Reorganized Argo CLI tests with ids/parametrization and consolidated trigger-explanation coverage.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread test/unit/test_serializer_integration.py
Comment on lines +23 to +54
@pytest.fixture
def isolated_store():
"""
Fixture to isolate SerializerStore global state per test.
Provides a clean slate and restores original state afterward.
"""
# Snapshot original state
saved_all = dict(SerializerStore._all_serializers)
saved_active = set(SerializerStore._active_serializers)
saved_records = dict(SerializerStore._records)
saved_pending = dict(SerializerStore._pending_by_module)
saved_cache = SerializerStore._ordered_cache

# Clear state for test isolation
SerializerStore._all_serializers.clear()
SerializerStore._active_serializers.clear()
SerializerStore._records.clear()
SerializerStore._pending_by_module.clear()
SerializerStore._ordered_cache = None

yield

# Restore original state
SerializerStore._all_serializers.clear()
SerializerStore._all_serializers.update(saved_all)
SerializerStore._active_serializers.clear()
SerializerStore._active_serializers.update(saved_active)
SerializerStore._records.clear()
SerializerStore._records.update(saved_records)
SerializerStore._pending_by_module.clear()
SerializerStore._pending_by_module.update(saved_pending)
SerializerStore._ordered_cache = saved_cache
Comment on lines +23 to +24
@pytest.fixture
def isolated_store():
Comment on lines +57 to +58
@pytest.fixture
def make_serializer_module(monkeypatch):
@greptile-apps

greptile-apps Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR refactors 15 unit test files to follow project conventions: replacing try/finally teardown blocks with pytest fixtures, adding @pytest.mark.parametrize for data-driven tests, adopting f-strings and pathlib, and moving imports to module-level.

  • Fixture-based isolation (isolated_store, clean_store, make_serializer_module) centralises registry teardown that was previously duplicated in every test's finally block, reducing boilerplate and making the cleanup logic easier to audit.
  • Class-to-function migration: TestBatchAPI, TestDockerExecution, TestMetaflowE2E, TestPutManyFilesEmptyInput, and TestReadManyFilesEmptyInput are dissolved into module-level functions; Docker gating moves from a per-method _require_docker() helper to an autouse fixture driven by @pytest.mark.docker.
  • Bug fix: the original test_kubernetes.py defined two functions both named test_kubernetes_parse_keyvalue_list; pytest silently ran only the second one, and this PR renames them to distinct names.

Confidence Score: 5/5

This PR only touches test files; no production code is modified. The refactoring is structurally sound and behaviour-preserving across all 15 files.

All changes are confined to the test layer. The fixture-based isolation correctly saves and restores registry state, the Docker-gating autouse fixture reproduces the original class-level skip logic faithfully, and the duplicate-test-name bug in test_kubernetes.py is silently fixed. The few open items from prior review threads are pre-existing concerns carried into this revision.

test/unit/test_serializer_lifecycle.py — the inline interceptor cleanup in test_retry_fires_via_real_import_hook and the sys.modules leak for fixture_retry_dep are the only residual concerns, as noted in prior review threads.

Important Files Changed

Filename Overview
test/unit/test_serializer_lifecycle.py Largest change: try/finally blocks replaced by isolated_store + make_serializer_module fixtures. The interceptor cleanup in test_retry_fires_via_real_import_hook remains inline (not in a finally), so it won't run on assertion failure, and fixture_retry_dep is not removed from sys.modules on test exit.
test/unit/test_serializer_integration.py isolated_store here only restores state after the test (no pre-clear), appropriate for integration tests that need a live registry. Removed defensive is not None guards before last_error string checks.
test/unit/test_artifact_serializer.py Adds _DualFormatSerializer (used only by direct-call tests), clean_store fixture (saves/restores _all_serializers + _active_serializers), and parametrises lazy_import tests. The new class is correctly not added to _active_serializers.
test/unit/localbatch/test_localbatch.py Class-based tests dissolved into module-level functions; Docker gating moved to autouse _require_docker_if_marked fixture; all Docker-dependent tests correctly carry @pytest.mark.docker.
test/unit/test_kubernetes.py Renames duplicate test_kubernetes_parse_keyvalue_list functions (silent bug fix), adds parametrize ids, and renames tests for clarity.
test/unit/test_s3_empty_input.py _make_s3 helper promoted to an s3_instance fixture; unittest.mock.patch replaced with mocker (pytest-mock, already in devstack requirements); temp file managed via tmp_path instead of manual NamedTemporaryFile+os.unlink.
test/unit/test_aws_util.py Replaces manual try/except did_raise pattern with pytest.raises / contextlib.nullcontext; adds parametrize ids.
test/unit/test_compute_resource_attributes.py Splits two monolithic test functions into a single parametrised test covering all cases; style-only change.
test/unit/test_content_addressed_store.py Minor style cleanups: f-strings, Path type hint, set-literal, improved assertion messages.
test/unit/test_local_metadata_provider.py Loop-over-test-cases pattern replaced with parametrize; behaviour unchanged.
test/unit/test_pickle_serializer.py Blank-line cleanup only.
test/unit/test_s3_storage.py Minor style adjustments; functional behaviour unchanged.
test/unit/test_serializer_public_api.py Five individual attribute-absence checks merged into a single parametrised test; module-level imports added.
test/unit/test_argo_workflows_cli.py test_sanitize_for_argo moved after fixtures section; parametrize ids and docstring added; fixture docstring reformatted.
test/unit/test_to_pod.py Parametrised primitives and iterables tests; set comparison correctly sorted before asserting; style-only.

Reviews (2): Last reviewed commit: "added localbatch tests" | Re-trigger Greptile

Comment on lines +439 to +447
assert rec.state == SerializerState.ACTIVE
assert rec.import_trigger == "hook"
assert ser_mod._E2E in SerializerStore._active_serializers

# Cleanup interceptor state to prevent leaking to other tests
from metaflow.datastore.artifacts.lazy_registry import _interceptor

_interceptor._watched.discard("fixture_retry_dep")
_interceptor._processed.discard("fixture_retry_dep")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Interceptor cleanup no longer runs on assertion failure

The _interceptor._watched and _interceptor._processed cleanup at lines 443–447 only runs when all three preceding assertions (lines 439–441) pass. If any of those assertions fail mid-test, "fixture_retry_dep" stays in _interceptor._watched, which persists for the entire process lifetime and can cause subsequent hook-related tests to behave unexpectedly.

The original code guarded this in a finally block. The isolated_store fixture restores the serializer registry but does not touch the interceptor's watched/processed sets, so there's no safety net here.

Comment on lines +437 to +447
import fixture_retry_dep # noqa: F401

assert rec.state == SerializerState.ACTIVE
assert rec.import_trigger == "hook"
assert ser_mod._E2E in SerializerStore._active_serializers

# Cleanup interceptor state to prevent leaking to other tests
from metaflow.datastore.artifacts.lazy_registry import _interceptor

_interceptor._watched.discard("fixture_retry_dep")
_interceptor._processed.discard("fixture_retry_dep")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 fixture_retry_dep not removed from sys.modules after the test

monkeypatch.delitem(sys.modules, "fixture_retry_dep", raising=False) at line 428 removes a pre-existing entry before the test. But the bare import fixture_retry_dep at line 437 inserts a new entry that monkeypatch knows nothing about and will not undo. The original finally block explicitly called sys.modules.pop("fixture_retry_dep", None) to handle exactly this. Leaving this module cached may affect other tests that depend on fixture_retry_dep being absent from sys.modules.

Comment thread test/unit/test_serializer_integration.py
@agsaru agsaru marked this pull request as draft June 8, 2026 19:44
@codecov

codecov Bot commented Jun 8, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
⚠️ Please upload report for BASE (master@732ca76). Learn more about missing BASE report.

Additional details and impacted files
@@            Coverage Diff            @@
##             master    #3249   +/-   ##
=========================================
  Coverage          ?   28.80%           
=========================================
  Files             ?      381           
  Lines             ?    52467           
  Branches          ?     9260           
=========================================
  Hits              ?    15114           
  Misses            ?    36344           
  Partials          ?     1009           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@agsaru agsaru marked this pull request as ready for review June 9, 2026 04:14
@agsaru agsaru requested a review from Copilot June 9, 2026 04:14

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated 4 comments.

Comment on lines +42 to +64
@pytest.fixture
def isolated_store():
"""
Fixture to isolate SerializerStore global state per test.
Prevents tests from poisoning the registry if they fail mid-execution.
"""
saved_all = dict(SerializerStore._all_serializers)
saved_active = set(SerializerStore._active_serializers)
saved_records = dict(SerializerStore._records)
saved_pending = dict(SerializerStore._pending_by_module)
saved_cache = SerializerStore._ordered_cache

yield

SerializerStore._all_serializers.clear()
SerializerStore._all_serializers.update(saved_all)
SerializerStore._active_serializers.clear()
SerializerStore._active_serializers.update(saved_active)
SerializerStore._records.clear()
SerializerStore._records.update(saved_records)
SerializerStore._pending_by_module.clear()
SerializerStore._pending_by_module.update(saved_pending)
SerializerStore._ordered_cache = saved_cache
Comment on lines +284 to +304
port = 18766
store = Store(queue_name="inject-queue")
runner = DockerRunner(
store,
host_addr="host.docker.internal",
port=port,
inject_env={"LOCALBATCH_CANARY": "canary-value"},
)
app = create_app(store, runner)
server = uvicorn.Server(
uvicorn.Config(app, host="127.0.0.1", port=port, log_level="warning")
)
t = threading.Thread(target=server.run, daemon=True)
t.start()

for _ in range(100):
try:
requests.get(f"http://127.0.0.1:{port}/health", timeout=0.1)
break
except Exception:
time.sleep(0.05)
Comment on lines +340 to +341
server.should_exit = True
t.join(timeout=5)
Comment on lines +23 to +41
@pytest.fixture
def isolated_store():
"""
Fixture to isolate SerializerStore global state per test.
Provides a clean slate and restores original state afterward.
"""
# Snapshot original state
saved_all = dict(SerializerStore._all_serializers)
saved_active = set(SerializerStore._active_serializers)
saved_records = dict(SerializerStore._records)
saved_pending = dict(SerializerStore._pending_by_module)
saved_cache = SerializerStore._ordered_cache

# Clear state for test isolation
SerializerStore._all_serializers.clear()
SerializerStore._active_serializers.clear()
SerializerStore._records.clear()
SerializerStore._pending_by_module.clear()
SerializerStore._ordered_cache = None
@agsaru agsaru changed the title refactor(tests): storage, serializer, and cloud based unit tests to f… refactor(tests): storage, serializer, and cloud based unit tests to follow test conventions Jun 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants