Testing Infra Enhancements#3040
Merged
Merged
Conversation
npow
commented
Mar 24, 2026
Collaborator
- Refactor internal netflix tests to parameterize orchestrator/backend and moved the tests to this repo
- Setup local airflow/argo/sfn and k8s/batch to run in GHA
- Report coverage
- Split Tiltfile
corral is a FastAPI-based local emulator of the AWS Batch REST API that runs submitted jobs inside Docker containers on the developer's machine. It lets you test @batch-decorated Metaflow flows without provisioning real AWS infrastructure. Changes: - devtools/Tiltfile: add corral as a local_resource that starts the corral server, wires METAFLOW_BATCH_JOB_QUEUE and METAFLOW_BATCH_CLIENT_PARAMS into the generated config, and injects MinIO / metadata-service env vars into every container it launches. corral depends on the minio component. - devtools/pick_services.sh: add "corral" to the interactive service picker so devs can opt in/out at devstack start time. - test/unit/corral/: new integration-test suite modelled on the spin tests. TestBatchAPI (13 tests, no Docker required) validates all Batch REST endpoints through boto3. TestDockerExecution and TestMetaflowE2E are gated with @pytest.mark.docker and only run when a Docker daemon is reachable. corral itself lives at ~/code/corral (separate repo). Install with: pip install -e ~/code/corral Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
env must be passed to Runner.__init__, not Runner.run() — the latter treats unknown kwargs as flow parameters (CLI flags), not as subprocess environment variables. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Wires up all four remaining free service emulators so the devstack can test every Metaflow extension without a real cloud account. Metaflow core changes: - metaflow_config.py: add BATCH_CLIENT_PARAMS, SFN_CLIENT_PARAMS, and SFN_DYNAMO_DB_CLIENT_PARAMS so each boto3 client can be redirected to a local emulator via endpoint_url. - batch_client.py: pass BATCH_CLIENT_PARAMS to get_aws_client. - step_functions_client.py: pass SFN_CLIENT_PARAMS to get_aws_client. - dynamo_db_client.py: pass SFN_DYNAMO_DB_CLIENT_PARAMS to get_aws_client. Devstack changes (Tiltfile): - ddb-local: amazon/dynamodb-local, port-forwarded to localhost:8765. Sets METAFLOW_SFN_DYNAMO_DB_CLIENT_PARAMS and METAFLOW_SFN_DYNAMO_DB_TABLE. - sfn-local: amazon/aws-stepfunctions-local, port-forwarded to localhost:8082 (avoids conflict with metaflow-ui on 8083). Depends on ddb-local. Sets METAFLOW_SFN_CLIENT_PARAMS and METAFLOW_SFN_IAM_ROLE. The sfn-local container is given AWS_ENDPOINT_URL_DYNAMODB pointing at the ddb-local K8s service. - azurite: mcr.microsoft.com/azure-storage/azurite on ports 10000-10002. Creates an azurite-secret K8s secret with well-known dev credentials. Sets METAFLOW_AZURE_STORAGE_BLOB_SERVICE_ENDPOINT. - fake-gcs-server: fsouza/fake-gcs-server on port 4443. Writes STORAGE_EMULATOR_HOST=http://localhost:4443 to .devtools/env_local (source it alongside the Metaflow config; the GCS SDK picks it up automatically). - write_config_files() extended to emit .devtools/env_local for env vars that live outside Metaflow's JSON config. - pick_services.sh: all four new services added to the picker. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
- azurite: use in-cluster hostname in K8s secret, add azurite-init job to create metaflow-test container, add azurite-secret to KUBERNETES_SECRETS, set METAFLOW_DATASTORE_SYSROOT_AZURE, write AZURE_STORAGE_CONNECTION_STRING to env_local for local dev - fake-gcs-server: add fake-gcs-secret K8s Secret with in-cluster STORAGE_EMULATOR_HOST, add gcs-bucket-init job to create metaflow-test bucket, add fake-gcs-secret to KUBERNETES_SECRETS, set METAFLOW_DATASTORE_SYSROOT_GS - Quote env var values in env_local to handle semicolons (Azure conn str) Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Covers prerequisites, quickstart, service table with ports and dependencies, dev shell usage, credentials reference, and all Makefile targets. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Ports the pytest parametrization mechanism from the internal mf-5 repo
so the existing core tests can run against all devstack backends via a
single `pytest test/core/test_core_pytest.py` invocation.
Changes:
- test/core/conftest.py: new — pytest_generate_tests hook that reads
enabled contexts from contexts.json and generates parametrized
(context, graph, test_batch) combinations, mirroring the mf-5 pattern.
- test/core/test_core_pytest.py: new — test_core_combination() drives
run_tests.py as a pytest test function, one pytest case per combo.
- test/core/run_tests.py: add `import time` and a new 'scheduler'
executor type. The scheduler executor runs create → trigger
--run-id-file → polls Flow(name)[run_id].finished until the async
run completes. Resume tests are skipped for scheduler contexts since
Argo/SFN don't support resume.
- test/core/contexts.json: enable all devstack backends (was 1 enabled,
now 7). New/updated contexts:
python3-all-local-azure-storage (Azurite, local execution)
python3-all-local-gcs (fake-gcs-server, local execution)
python3-batch (corral, minio, metadata-service)
python3-k8s (minikube k8s, minio, metadata-service)
python3-argo-workflows (Argo on minikube, scheduler executor)
python3-sfn (sfn-local + corral, scheduler executor)
All S3/Azure/GCS sysroots use {nonce} for per-run isolation.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
sfn-local runs as a k8s pod inside minikube, but corral (the local AWS Batch emulator) runs as a local_resource on the Docker host. sfn-local calls the Batch API to submit and poll jobs when executing a state machine, so it needs a reachable Batch endpoint. - Set AWS_ENDPOINT_URL_BATCH=http://host.docker.internal:8000 in the sfn-local Deployment env so it reaches corral on the host (same pattern as AWS_ENDPOINT_URL_DYNAMODB pointing to ddb-local). - Add corral as a dependency of sfn-local so Tilt starts corral automatically when sfn-local is selected. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Ports the UX test infrastructure from the internal mf-5 repo to the OSS metaflow repo, keeping only tests that run with standard OSS Metaflow (no Netflix-specific compute, metadata, or scheduler). Structure: - test/ux/conftest.py: fixtures and parametrization (runner mode by default; deployer mode enabled via --scheduler-type flag) - test/ux/pytest.ini: marker definitions (basic, config, conda, etc.) - test/ux/core/test_basic.py: hello world, project decorator, conda - test/ux/core/test_config.py: Config, config_value, FlowMutator, StepMutator, config_expr corner cases (6 tests) - test/ux/core/test_utils.py: run_flow_with_env, verify_single_run, deploy_flow_to_scheduler, _evict_flow_module_cache - test/ux/core/flows/: OSS-cleaned flow files (removed Netflix-only Parameter kwargs: external_trigger, external_artifact) Key details: - _evict_flow_module_cache() clears click_api.loaded_modules between tests so FlowMutator sees fresh config on each run (fixes test isolation for mutable_flow tests run sequentially in one session) - Config file paths use os.path.dirname(__file__) for portability - Requires a clean OSS-only venv; see test/ux/README.md
The previous conftest only ran in local Runner mode. This rewrite
aligns with the mf-5 pattern where basic/config tests run against
both runner *and* a remote scheduler/compute backend.
Changes:
- ux_test_config.yaml: new config file (analogous to mf-5's
ux_test_config.yaml) with scheduler.type and compute.backend fields
- conftest.py: add compute_backend, compute_image, compute_env,
scheduler_type, cluster fixtures that read from ux_test_config.yaml
or CLI overrides (--compute-backend, --compute-image, --scheduler-type,
--cluster)
- decospecs fixture auto-injects the compute backend decospec when a
backend is configured but not already in --decospecs (mirrors mf-5
auto-injecting titus:image=...)
- pytest_generate_tests: basic/config tests get runner+deployer modes;
scheduler_only/data_ux tests get deployer only; deployer variants are
skipped gracefully when no scheduler is configured
Usage without a backend (local smoke tests, current default):
pytest test/ux/
Usage with devstack Batch + SFN:
pytest test/ux/ \
--compute-backend batch --compute-image <image> \
--scheduler-type step-functions
Usage matching mf-5 (internal, via ux_test_config.yaml):
Set scheduler.type and compute.backend in ux_test_config.yaml
Step Functions: - StepFunctionsDeployedFlow.from_deployment(): look up state machine, parse flow_name/owner from start state Parameters, create fake flow file, return deployer - StepFunctionsDeployedFlow.get_triggered_run(): thin wrapper over from_deployment - StepFunctionsTriggeredRun.status property: reads execution status via describe_execution() - StepFunctionsClient.describe_execution(): new method - StepFunctions.schedule(): early return if no cron (avoids errors in sfn-local which has no EventBridge) Batch: - batch_client: skip jobRoleArn field when not configured Airflow deployer (new): - AirflowDeployer, AirflowDeployedFlow, AirflowTriggeredRun - AirflowClient REST API wrapper - Config vars: AIRFLOW_REST_API_URL, credentials, DAG path, namespace - Registered in DEPLOYER_IMPL_PROVIDERS_DESC - dev extras in setup.py: pytest, omegaconf, kubernetes
Tiltfile:
- Inject METAFLOW_S3_ENDPOINT_URL into corral containers so Metaflow
can reach MinIO (AWS_ENDPOINT_URL_S3 alone is not sufficient as
Metaflow explicitly calls os.getenv('METAFLOW_S3_ENDPOINT_URL'))
Tests:
- ux_test_config.yaml: add sfn-batch and airflow backends
- conftest.py: rewrite to support multiple deployer backends,
parametrize by backend, handle sfn-batch/argo-kubernetes contexts
- test_basic.py: add deployer tests (deploy/trigger/from_deployment)
- test_config.py: add config mutation tests across backends
- test_utils.py: shared helpers for deployer lifecycle
- hello_from_deployment.py: new flow for from_deployment tests
README:
- Document sfn-batch and argo-kubernetes test invocations
- Add service dependency notes for each backend
Update all references following the PyPI package rename: - Tiltfile: service name, serve cmd, queue name, inject-env vars - README: service table, credential table, invocation examples - pick_services.sh: service option
- devtools/Tiltfile: full devstack with minio, postgresql, metaflow-service, argo-workflows, sfn-local, ddb-local, localbatch, airflow; pre-baked test image resource (build-test-image), eventbridge stub; PostgreSQL persistence + tolerations + liveness thresholds to survive minikube node instability; use `minikube image load` for cross-platform image loading - devtools/docker/metaflow-test/Dockerfile: pre-baked image with boto3/requests to eliminate ~2 min pip install per kubernetes/batch pod - devtools/eventbridge_stub.py: minimal EventBridge stub for sfn-local testing - test/ux/conftest.py: add --only-backend CLI option for CI matrix filtering - test/ux/core/conftest.py: devstack env vars (METAFLOW_HOME, AWS endpoints) - test/ux/core/flows/dag/: branch, foreach, nested-foreach flow fixtures - test/ux/pytest.ini: timeout=600 for kubernetes tests - test/ux/ux_test_config.yaml: add airflow-kubernetes backend; pre-baked image for argo-kubernetes and sfn-batch; enable all 4 backends - .github/workflows/ux-tests.yml: parallel matrix over local / argo-kubernetes / airflow-kubernetes backends; pytest-xdist -n 6 within each job; sfn-batch omitted pending localbatch PyPI publication
- pytest-cov --cov=metaflow --cov-branch in every backend job - each job uploads .coverage + XML + per-backend HTML as artifacts - new coverage-report job: downloads all .coverage files, combines them, generates HTML report + XML, posts summary to $GITHUB_STEP_SUMMARY - junit-xml per backend for GHA test tab visibility - .coveragerc: source/omit/paths config for clean cross-job combine
- Tiltfile: use explicit + for string concat in build-test-image cmd; Starlark (unlike Python) requires explicit + for multi-line adjacent strings - GHA workflow: poll `tilt get session` before `tilt wait` to avoid the 'No tilt apiserver found' race condition on startup
- Tiltfile: remove chart='airflow' kwarg from helm_remote('airflow', ...);
Tilt v0.37.0 changed helm_remote so the first positional arg IS chart,
making chart=kwarg a duplicate. Matches how all other helm_remote calls work.
- Workflow: pin Tilt to v0.33.11 (matches devtools/Makefile) so extensions
behave consistently between local dev and CI.
Test fixtures: - Replace symlinks to internal mf-5 paths with real files so GHA can resolve them: helloworld.py, helloproject.py, helloconda.py, config_simple.json, config_simple_cmd.json Airflow devstack: - Disable Airflow's internal PostgreSQL subchart; use the shared standalone PostgreSQL instance with a separate 'airflow' database. This eliminates the second competing PostgreSQL pod that caused init-container timeout failures on GHA runners. - Add airflow-db-init local_resource to create the airflow database before Airflow starts. - Add known postgres superuser password (auth.postgresPassword) so the db-init command can authenticate. GHA workflow: - Increase minikube memory from 4096 to 6144 MB. - Increase generate-configs wait timeout from 600s to 900s.
…esource_deps - Add create-airflow-db.sql to PostgreSQL initdb.scripts so the airflow database is created atomically at first boot (before any Airflow pod starts). Also add auth.postgresPassword for the fallback local_resource. - Fix airflow-db-init command quoting: use double quotes for sh -c wrapper so inner single-quoted SQL works correctly; add || true equivalent via exit 0 to tolerate "database already exists" on local reruns. - Add resource_deps=['airflow-db-init'] to airflow-run-migrations, airflow-create-user, and airflow-triggerer workloads so Tilt waits for the DB to exist before deploying any Airflow component. - Rename "airflow": [] to "airflow": ["postgresql"] so the component resolver includes postgresql as a transitive dependency.
… slow GHA runners
Airflow: - Replace startup probe timeout tuning with startupProbe.enabled=false for both scheduler and webserver. The exec-based scheduler probe was generating OCI exec errors on minikube/GHA runners, and both probes were preventing Tilt from declaring the stack healthy. Liveness probes provide sufficient ongoing health checking. Argo deployer tests: - Add _evict_flow_module_cache() to deploy_flow_to_scheduler() so that config_value / FlowMutator are applied to a freshly loaded class on each deploy. Without eviction, _configs_processed=True from a prior test causes _process_config_decorators to skip mutation, leaving added parameters (e.g. param3) absent from the WorkflowTemplate. - Restructure all 6 deployer test paths to capture and use the Run returned by wait_for_deployed_run() directly, instead of calling verify_single_run() which scans by tag. Parallel tests deploy the same flow (ConfigSimple / ConfigMutableFlow) to Argo, overwriting the shared WorkflowTemplate, causing the last-deployed tags to appear on all triggered runs; scanning by tag then finds 2+ runs for a single test. Using the deployer's returned run sidesteps this race entirely. - Bump per-test timeout 600s→900s to give Argo workflows enough time to schedule, pull images, and complete on GHA runners.
… for webserver
The apache-airflow 1.15.0 helm chart schema does not allow 'enabled' under
startupProbe, so we can't disable probes via helm values.
Instead:
- scheduler.startupProbe.command={/bin/true}: replaces the default
'airflow jobs check' exec probe (which fails with OCI exec errors and
deprecation warnings on GHA minikube) with a trivially-passing command.
- webserver.startupProbe.failureThreshold=60 / periodSeconds=15 /
timeoutSeconds=30: allows up to 15 minutes for the webserver HTTP
health endpoint to become reachable on slow GHA runners.
Deployer tests for the same backend share external scheduler state
(Argo WorkflowTemplates, SFN state machines, etc.). Running them in
parallel causes races where two tests try to CREATE the same resource
simultaneously and one fails with 'already exists'.
Add pytest.mark.xdist_group('deployer-{backend}') to all deployer-mode
test parameters so that pytest-xdist routes them to the same worker and
they run sequentially.
- Add scheduler.livenessProbe.command={/bin/true} to match the startup
probe fix: the default 'airflow jobs check' command prints deprecation
warnings to stderr, causing non-zero exit codes that kill the scheduler
container via the liveness probe (CrashLoopBackOff)
- Increase webserver memory limit from 512Mi to 1Gi and CPU from 250m to
500m to prevent gunicorn master timeout ('No response from gunicorn
master within 120 seconds') on resource-constrained GHA runners
- Extend webserver liveness probe thresholds (failureThreshold=10,
periodSeconds=30, timeoutSeconds=30) to absorb transient slowness
The Airflow deployer explicitly rejects image_pull_policy in the @kubernetes decorator (raises AirflowException). Drop it from the airflow-kubernetes backend decospec. The image tag (python3.9) is non-latest, so Kubernetes defaults to IfNotPresent, which is sufficient since metaflow-test:python3.9 is pre-loaded into minikube by the build-test-image Tilt resource.
added 8 commits
March 24, 2026 17:04
- Apply black formatting to test/core/conftest.py and test/core/test_core_pytest.py - Remove ux-local/ux-sfn/ux-argo/ux-airflow from tox default envlist so plain 'tox' (run by test.yml CI) only runs unit tests, not devstack-dependent ux tests
On Python 3.14, typing.Union[X, Y] and typing.Optional[X] normalize to native types.UnionType (X | Y syntax). The stub generator had no handler for types.UnionType, causing it to fall through to str(element) which produces pipe notation instead of typing.Union[...]. Add a types.UnionType branch in _get_element_name_with_module that converts native union types to typing.Union[...] notation, consistent with how typing._GenericAlias unions are handled.
- Add python_version >= '3.8' marker to localbatch in [dev] extras so Python 3.7 CI matrix can install metaflow[dev] without error - Fix stub generator to handle typing.Union/Optional on Python 3.14+ where Union types may no longer be instances of typing._GenericAlias; add __origin__ is typing.Union check as a fallback before the typing._GenericAlias branch
npow
marked this pull request as ready for review
March 25, 2026 19:52
saikonen
previously approved these changes
Apr 3, 2026
saikonen
left a comment
Collaborator
There was a problem hiding this comment.
No real dealbreakers so approved. Only major friction is introducing localbatch emulation.
- rename track_runs_by_tags -> wait_for_runs_by_tags and expand docstring to clarify blocking behavior and flakiness caveat on low-resource infra - change Tiltfile default from 'all' to metadata-service,argo-workflows - switch localbatch in requirements-devstack.txt from git fork to PyPI Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
…py3.7 Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
ubuntu-latest runners have ~14GB free which isn't enough for the sfn-batch job (minikube + docker images for 5 services). Reclaim ~30GB upfront by removing Android SDK, .NET, Haskell, and large packages. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
3 tasks
2 tasks
talsperre
pushed a commit
that referenced
this pull request
Apr 27, 2026
- Delete `test_runner`: unreachable since #3040 rewrote tox.ini with `envlist = unit` and per-env pytest commands. The script references python2 and was likely not executing since tox 4 became default (~2022-2023), which treats empty envlist as a no-op. - Remove `numpy` from CI pip install: not a metaflow dependency, not in devtools/requirements-devstack.txt, and the only test that imports it (test/unit/spin/) is --ignore'd in the unit env. - Remove dead `allowlist_externals` and `commands` from [testenv] base in tox.ini, since every concrete env overrides commands. Co-Authored-By: Claude Opus 4.6 <[email protected]>
talsperre
pushed a commit
that referenced
this pull request
Apr 27, 2026
## Summary - **Delete `test_runner`**: This script has been unreachable since #3040 rewrote `tox.ini` with `envlist = unit` and per-env pytest commands. The script references `python2` and was likely not executing since tox 4 became the default (~2022), which treats an empty `envlist` as "run nothing." - **Remove `numpy` from CI install**: `numpy` is not a metaflow dependency, not in `devtools/requirements-devstack.txt`, and the only test that imports it (`test/unit/spin/`) is `--ignore`d in the unit env. No running test needs it. - **Clean up `[testenv]` base in `tox.ini`**: Remove dead `allowlist_externals = ./test_runner` and `commands = ./test_runner` — every concrete env (`unit`, `ux-local`, `ux-sfn`, `ux-argo`, `ux-airflow`) overrides `commands` with its own pytest call. ## Test plan - [ ] CI passes — the `test.yml` workflow should run identically since `tox` still executes `envlist = unit` which uses the `[testenv:unit]` pytest command (unchanged) - [ ] No other workflow or config references `test_runner` (verified via grep) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: npow <[email protected]> Co-authored-by: Claude Opus 4.6 <[email protected]>
agsaru
pushed a commit
to agsaru/metaflow
that referenced
this pull request
May 1, 2026
## Summary - **Delete `test_runner`**: This script has been unreachable since Netflix#3040 rewrote `tox.ini` with `envlist = unit` and per-env pytest commands. The script references `python2` and was likely not executing since tox 4 became the default (~2022), which treats an empty `envlist` as "run nothing." - **Remove `numpy` from CI install**: `numpy` is not a metaflow dependency, not in `devtools/requirements-devstack.txt`, and the only test that imports it (`test/unit/spin/`) is `--ignore`d in the unit env. No running test needs it. - **Clean up `[testenv]` base in `tox.ini`**: Remove dead `allowlist_externals = ./test_runner` and `commands = ./test_runner` — every concrete env (`unit`, `ux-local`, `ux-sfn`, `ux-argo`, `ux-airflow`) overrides `commands` with its own pytest call. ## Test plan - [ ] CI passes — the `test.yml` workflow should run identically since `tox` still executes `envlist = unit` which uses the `[testenv:unit]` pytest command (unchanged) - [ ] No other workflow or config references `test_runner` (verified via grep) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: npow <[email protected]> Co-authored-by: Claude Opus 4.6 <[email protected]>
This was referenced May 9, 2026
talsperre
added a commit
that referenced
this pull request
May 31, 2026
) ## Summary Remove duplicate `--use-latest` pytest option registration that prevents the unit test suite from collecting. ## Problem Running `pytest test/unit/` fails during collection with: ``` ValueError: option names {'--use-latest'} already added ``` `--use-latest` is registered in both `test/unit/conftest.py` (added in #3040) and `test/unit/spin/conftest.py` (from #2506). pytest raises on the duplicate. ## Fix Remove `pytest_addoption` from `test/unit/spin/conftest.py`. The root `test/unit/conftest.py` already registers the option and it applies to all subdirectories. ## Testing | State | Result | |-------|--------| | Before | `pytest test/unit/` fails with collection error | | After | 468 passed, 1 failed, 19 skipped, 21 errors | > **Note:** The 1 failure (`complex_dag`) and 21 errors (`secrets`/`system_context` tests) are pre-existing infrastructure issues unrelated to this change. --------- Co-authored-by: Shashank Srikanth <[email protected]> Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.