Skip to content

Commit f6fe09b

Browse files
mohamedzeidan2021Mohamed Zeidan
andauthored
test(train): patch mocks at the lookup site so unit tests stop calling AWS (#6491)
Five tests in sagemaker-train's unit suite issued real AWS requests. They pass in CI only because the build role carries credentials, so the mocks were silently ineffective rather than visibly broken. Root cause in three shapes: 1. Patch targeting the defining module instead of the lookup site. base_trainer does `from ...finetune_utils import get_recipe_s3_uri` at module level, so patching finetune_utils.get_recipe_s3_uri never rebinds the reference base_trainer already holds and the real function ran, calling DescribeHubContent. Same for get_training_image, and for TrainDefaults, which handed back a real Session so download_file issued a live HeadObject. Only two of the four finetune_utils patches in that block were affected: _render_recipe_placeholders and _get_smtj_override_spec are imported inside _train_serverful_smtj, so that import re-executes under patch and finetune_utils is the correct target for them. Those are left as they are. 2. Missing mock. base_evaluator imports verify_evaluation_caller_permissions in the same statement as resolve_and_validate_role, but only the latter was ever patched, so _get_aws_execution_context reached STS GetCallerIdentity and IAM. 3. Incomplete mock. test_model_trainer_no_from_recipe_raises patched TrainDefaults.get_role for exactly this reason, but STS was also reached by a second route: model_post_init builds a default OutputDataConfig, which calls session.default_bucket(). The session is stubbed rather than get_output_data_config so that the defaulting logic stays under test. Verified with deliberately invalid credentials plus a transport-level recorder that counts swallowed calls: - the three files go from 5 failed / 97 passed to 102 passed - the full sagemaker-train unit suite goes from 19 to 14 failures; the remaining 14 are a local botocore shape mismatch, not these tests - zero outbound AWS requests from the three files Coverage moves by one line, 27402 to 27401, with no real loss. The 12 lines no longer covered are all sagemaker-core session_helper AWS plumbing (default_bucket, generate_default_sagemaker_bucket_name, botocore_resolver, sts_regional_endpoint) that these tests only covered by genuinely calling STS; all 12 are covered by sagemaker-core's own unit suite. Offsetting that, 11 lines become newly covered in base_trainer, base_evaluator, benchmark_evaluator, custom_scorer_evaluator and model_trainer, because the tests no longer abort partway through on an AWS error. No production code changes. Co-authored-by: Mohamed Zeidan <zeidmo@amazon.com>
1 parent b04efc3 commit f6fe09b

3 files changed

Lines changed: 50 additions & 7 deletions

File tree

‎sagemaker-train/tests/unit/train/evaluate/test_mtrl_evaluator_handshake.py‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,11 @@ def test_custom_scorer_evaluator_rejects_mtrl_trainer_without_job(self, mock_mlf
292292
class TestEvaluateSubmissionWithMTRLTrainer:
293293
"""Test that evaluate() can be called successfully when model is an MTRLTrainer."""
294294

295+
# _get_aws_execution_context calls verify_evaluation_caller_permissions,
296+
# which reaches STS GetCallerIdentity and IAM. base_evaluator imports it in
297+
# the same statement as resolve_and_validate_role, so it needs its own patch
298+
# in that namespace; without it this test resolves a real Session.
299+
@patch("sagemaker.train.evaluate.base_evaluator.verify_evaluation_caller_permissions")
295300
@patch(
296301
"sagemaker.train.evaluate.base_evaluator.resolve_and_validate_role",
297302
side_effect=lambda provided_role, **kwargs: provided_role,
@@ -307,7 +312,14 @@ class TestEvaluateSubmissionWithMTRLTrainer:
307312
new_callable=PropertyMock,
308313
)
309314
def test_benchmark_evaluate_submission_with_mtrl_trainer(
310-
self, mock_hyperparams, mock_start, mock_artifact, mock_resolve_mp, mock_mlflow, mock_role
315+
self,
316+
mock_hyperparams,
317+
mock_start,
318+
mock_artifact,
319+
mock_resolve_mp,
320+
mock_mlflow,
321+
mock_role,
322+
mock_verify_permissions,
311323
):
312324
"""BenchMarkEvaluator.evaluate() should successfully submit when model is MTRLTrainer."""
313325
from sagemaker.train.evaluate import BenchMarkEvaluator, get_benchmarks
@@ -349,6 +361,9 @@ def test_benchmark_evaluate_submission_with_mtrl_trainer(
349361
assert execution.arn is not None
350362
mock_start.assert_called_once()
351363

364+
# See the note on the benchmark test above: verify_evaluation_caller_permissions
365+
# needs its own patch in base_evaluator's namespace or this reaches STS.
366+
@patch("sagemaker.train.evaluate.base_evaluator.verify_evaluation_caller_permissions")
352367
@patch("sagemaker.train.evaluate.custom_scorer_evaluator.validate_data_path_exists")
353368
@patch(
354369
"sagemaker.train.evaluate.base_evaluator.resolve_and_validate_role",
@@ -373,6 +388,7 @@ def test_custom_scorer_evaluate_submission_with_mtrl_trainer(
373388
mock_mlflow,
374389
mock_role,
375390
mock_validate_data,
391+
mock_verify_permissions,
376392
):
377393
"""CustomScorerEvaluator.evaluate() should successfully submit when model is MTRLTrainer."""
378394
from sagemaker.train.evaluate import CustomScorerEvaluator

‎sagemaker-train/tests/unit/train/test_sft_trainer.py‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1565,13 +1565,22 @@ def _run_smtj_and_capture_input_config(self, model_name):
15651565
mock_session.sagemaker_config = {}
15661566

15671567
with (
1568-
patch("sagemaker.train.defaults.TrainDefaults") as mock_defaults,
1568+
# These three are imported into base_trainer at module level, so
1569+
# they must be patched in base_trainer's namespace -- patching them
1570+
# where they are defined does not rebind the reference base_trainer
1571+
# already holds. Left unpatched there, TrainDefaults hands back a
1572+
# real Session (so download_file issues a live HeadObject) and
1573+
# get_recipe_s3_uri calls DescribeHubContent for real. The two
1574+
# finetune_utils patches below are correct as written, because
1575+
# _train_serverful_smtj imports those names in its own body, so the
1576+
# import re-executes while the patch is active.
1577+
patch("sagemaker.train.base_trainer.TrainDefaults") as mock_defaults,
15691578
patch(
1570-
"sagemaker.train.common_utils.finetune_utils.get_recipe_s3_uri",
1579+
"sagemaker.train.base_trainer.get_recipe_s3_uri",
15711580
return_value="s3://bucket/recipe.yaml",
15721581
),
15731582
patch(
1574-
"sagemaker.train.common_utils.finetune_utils.get_training_image",
1583+
"sagemaker.train.base_trainer.get_training_image",
15751584
return_value="img:latest",
15761585
),
15771586
patch(

‎sagemaker-train/tests/unit/train/test_trainer_recipe_integration.py‎

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -552,14 +552,32 @@ def test_model_trainer_from_recipe_get_resolved(self, tmp_path):
552552

553553
def test_model_trainer_no_from_recipe_raises(self):
554554
"""ModelTrainer not created via from_recipe raises AttributeError."""
555+
from sagemaker.core.helper.session_helper import Session
555556
from sagemaker.train.model_trainer import ModelTrainer
556557
from sagemaker.train.configs import Compute
557558

558559
# Construction resolves an execution role via STS; mock it so this unit
559560
# test (which only exercises get_resolved_recipe) runs without AWS creds.
560-
with patch(
561-
"sagemaker.train.defaults.TrainDefaults.get_role",
562-
return_value="arn:aws:iam::123456789012:role/test-role",
561+
# It also builds a default OutputDataConfig, which calls
562+
# session.default_bucket() and so reaches STS by a second route, so the
563+
# session is stubbed too. Stubbing the session rather than
564+
# get_output_data_config keeps that defaulting logic under test.
565+
# spec=Session is required because ModelTrainer validates the field as a
566+
# Session instance; default_bucket_prefix is set on instances, not the
567+
# class, so it has to be assigned explicitly.
568+
mock_session = MagicMock(spec=Session)
569+
mock_session.default_bucket.return_value = "test-bucket"
570+
mock_session.default_bucket_prefix = None
571+
572+
with (
573+
patch(
574+
"sagemaker.train.defaults.TrainDefaults.get_role",
575+
return_value="arn:aws:iam::123456789012:role/test-role",
576+
),
577+
patch(
578+
"sagemaker.train.defaults.TrainDefaults.get_sagemaker_session",
579+
return_value=mock_session,
580+
),
563581
):
564582
trainer = ModelTrainer(
565583
training_image="123456789012.dkr.ecr.us-east-1.amazonaws.com/test:latest",

0 commit comments

Comments
 (0)