@@ -1385,6 +1385,143 @@ def test_start_new_removes_tags_from_processing_job(self, mock_session):
13851385 assert "tags" not in call_kwargs
13861386
13871387
1388+ class TestProcessingS3OutputOptionalS3Uri :
1389+ """Tests for ProcessingS3Output with optional s3_uri (issue #5559)."""
1390+
1391+ def test_processing_s3_output_with_none_s3_uri_is_valid (self ):
1392+ """Verify ProcessingS3Output can be instantiated with s3_uri=None."""
1393+ s3_output = ProcessingS3Output (
1394+ s3_uri = None ,
1395+ local_path = "/opt/ml/processing/output" ,
1396+ s3_upload_mode = "EndOfJob" ,
1397+ )
1398+ assert s3_output .s3_uri is None
1399+ assert s3_output .local_path == "/opt/ml/processing/output"
1400+ assert s3_output .s3_upload_mode == "EndOfJob"
1401+
1402+ def test_processing_s3_output_without_s3_uri_kwarg_is_valid (self ):
1403+ """Verify ProcessingS3Output can be instantiated without passing s3_uri at all."""
1404+ s3_output = ProcessingS3Output (
1405+ local_path = "/opt/ml/processing/output" ,
1406+ s3_upload_mode = "EndOfJob" ,
1407+ )
1408+ assert s3_output .s3_uri is None
1409+
1410+ def test_normalize_outputs_with_none_s3_uri_generates_s3_path (self , mock_session ):
1411+ """When s3_uri is None, _normalize_outputs should auto-generate an S3 path."""
1412+ processor = Processor (
1413+ role = "arn:aws:iam::123456789012:role/SageMakerRole" ,
1414+ image_uri = "test-image:latest" ,
1415+ instance_count = 1 ,
1416+ instance_type = "ml.m5.xlarge" ,
1417+ sagemaker_session = mock_session ,
1418+ )
1419+ processor ._current_job_name = "test-job"
1420+
1421+ s3_output = ProcessingS3Output (
1422+ s3_uri = None ,
1423+ local_path = "/opt/ml/processing/output" ,
1424+ s3_upload_mode = "EndOfJob" ,
1425+ )
1426+ outputs = [ProcessingOutput (output_name = "my-output" , s3_output = s3_output )]
1427+
1428+ with patch ("sagemaker.core.workflow.utilities._pipeline_config" , None ):
1429+ result = processor ._normalize_outputs (outputs )
1430+
1431+ assert len (result ) == 1
1432+ generated_uri = result [0 ].s3_output .s3_uri
1433+ assert generated_uri .startswith ("s3://" )
1434+ assert "test-job" in generated_uri
1435+ assert "my-output" in generated_uri
1436+
1437+ def test_normalize_outputs_with_none_s3_uri_and_pipeline_config (self , mock_session ):
1438+ """When s3_uri is None and pipeline_config is set, use pipeline-based path."""
1439+ processor = Processor (
1440+ role = "arn:aws:iam::123456789012:role/SageMakerRole" ,
1441+ image_uri = "test-image:latest" ,
1442+ instance_count = 1 ,
1443+ instance_type = "ml.m5.xlarge" ,
1444+ sagemaker_session = mock_session ,
1445+ )
1446+ processor ._current_job_name = "test-job"
1447+
1448+ s3_output = ProcessingS3Output (
1449+ s3_uri = None ,
1450+ local_path = "/opt/ml/processing/output" ,
1451+ s3_upload_mode = "EndOfJob" ,
1452+ )
1453+ outputs = [ProcessingOutput (output_name = "my-output" , s3_output = s3_output )]
1454+
1455+ with patch ("sagemaker.core.workflow.utilities._pipeline_config" ) as mock_config :
1456+ mock_config .pipeline_name = "test-pipeline"
1457+ mock_config .step_name = "test-step"
1458+ result = processor ._normalize_outputs (outputs )
1459+
1460+ assert len (result ) == 1
1461+ # The result should be a Join object (pipeline variable) when pipeline_config is set
1462+ assert result [0 ].s3_output .s3_uri is not None
1463+
1464+ def test_normalize_outputs_with_none_s3_uri_auto_generates_name (self , mock_session ):
1465+ """When output_name is None and s3_uri is None, both should be auto-generated."""
1466+ processor = Processor (
1467+ role = "arn:aws:iam::123456789012:role/SageMakerRole" ,
1468+ image_uri = "test-image:latest" ,
1469+ instance_count = 1 ,
1470+ instance_type = "ml.m5.xlarge" ,
1471+ sagemaker_session = mock_session ,
1472+ )
1473+ processor ._current_job_name = "test-job"
1474+
1475+ s3_output = ProcessingS3Output (
1476+ s3_uri = None ,
1477+ local_path = "/opt/ml/processing/output" ,
1478+ s3_upload_mode = "EndOfJob" ,
1479+ )
1480+ outputs = [ProcessingOutput (s3_output = s3_output )]
1481+
1482+ with patch ("sagemaker.core.workflow.utilities._pipeline_config" , None ):
1483+ result = processor ._normalize_outputs (outputs )
1484+
1485+ assert len (result ) == 1
1486+ assert result [0 ].output_name == "output-1"
1487+ generated_uri = result [0 ].s3_output .s3_uri
1488+ assert generated_uri .startswith ("s3://" )
1489+ assert "output-1" in generated_uri
1490+
1491+ def test_processing_output_to_request_dict_omits_s3_uri_when_none (self ):
1492+ """Verify _processing_output_to_request_dict omits S3Uri when s3_uri is None."""
1493+ s3_output = ProcessingS3Output (
1494+ s3_uri = None ,
1495+ local_path = "/opt/ml/processing/output" ,
1496+ s3_upload_mode = "EndOfJob" ,
1497+ )
1498+ processing_output = ProcessingOutput (output_name = "results" , s3_output = s3_output )
1499+
1500+ result = _processing_output_to_request_dict (processing_output )
1501+
1502+ assert result ["OutputName" ] == "results"
1503+ assert "S3Output" in result
1504+ assert "S3Uri" not in result ["S3Output" ]
1505+ assert result ["S3Output" ]["LocalPath" ] == "/opt/ml/processing/output"
1506+ assert result ["S3Output" ]["S3UploadMode" ] == "EndOfJob"
1507+
1508+ def test_processing_output_to_request_dict_includes_s3_uri_when_set (self ):
1509+ """Regression test: S3Uri is included when s3_uri is provided."""
1510+ s3_output = ProcessingS3Output (
1511+ s3_uri = "s3://bucket/output" ,
1512+ local_path = "/opt/ml/processing/output" ,
1513+ s3_upload_mode = "EndOfJob" ,
1514+ )
1515+ processing_output = ProcessingOutput (output_name = "results" , s3_output = s3_output )
1516+
1517+ result = _processing_output_to_request_dict (processing_output )
1518+
1519+ assert result ["OutputName" ] == "results"
1520+ assert result ["S3Output" ]["S3Uri" ] == "s3://bucket/output"
1521+ assert result ["S3Output" ]["LocalPath" ] == "/opt/ml/processing/output"
1522+ assert result ["S3Output" ]["S3UploadMode" ] == "EndOfJob"
1523+
1524+
13881525# Additional tests from test_processing_extended.py
13891526class TestProcessorBasics :
13901527 """Test cases for basic Processor functionality"""
0 commit comments