@@ -1372,5 +1372,157 @@ public async Task WithUvEnvironment_CustomBaseImages_GeneratesDockerfileWithCust
13721372 // Verify the custom runtime image is used
13731373 Assert . Contains ( "FROM python:3.13-slim AS app" , dockerfileContent ) ;
13741374 }
1375+
1376+ [ Fact ]
1377+ public async Task FallbackDockerfile_GeneratesDockerfileWithoutUv_WithRequirementsTxt ( )
1378+ {
1379+ using var sourceDir = new TempDirectory ( ) ;
1380+ using var outputDir = new TempDirectory ( ) ;
1381+ var projectDirectory = sourceDir . Path ;
1382+
1383+ // Create a Python project without UV but with requirements.txt
1384+ var requirementsContent = """
1385+ flask==3.0.0
1386+ requests==2.31.0
1387+ """ ;
1388+
1389+ var scriptContent = """
1390+ print("Hello from non-UV project!")
1391+ """ ;
1392+
1393+ File . WriteAllText ( Path . Combine ( projectDirectory , "requirements.txt" ) , requirementsContent ) ;
1394+ File . WriteAllText ( Path . Combine ( projectDirectory , "main.py" ) , scriptContent ) ;
1395+
1396+ using var builder = TestDistributedApplicationBuilder . Create ( DistributedApplicationOperation . Publish , outputDir . Path , step : "publish-manifest" ) ;
1397+
1398+ // Add Python resources without UV environment
1399+ builder . AddPythonScript ( "script-app" , projectDirectory , "main.py" ) ;
1400+
1401+ var app = builder . Build ( ) ;
1402+ app . Run ( ) ;
1403+
1404+ // Verify that Dockerfile was generated
1405+ var dockerfilePath = Path . Combine ( outputDir . Path , "script-app.Dockerfile" ) ;
1406+ Assert . True ( File . Exists ( dockerfilePath ) , "Dockerfile should be generated for non-UV Python app" ) ;
1407+
1408+ var dockerfileContent = File . ReadAllText ( dockerfilePath ) ;
1409+
1410+ // Verify it's a fallback Dockerfile (single stage, no UV)
1411+ Assert . DoesNotContain ( "uv sync" , dockerfileContent ) ;
1412+ Assert . DoesNotContain ( "ghcr.io/astral-sh/uv" , dockerfileContent ) ;
1413+
1414+ // Verify it uses pip install for requirements.txt
1415+ Assert . Contains ( "pip install --no-cache-dir -r requirements.txt" , dockerfileContent ) ;
1416+
1417+ // Verify it uses the same runtime image as UV workflow
1418+ Assert . Contains ( "FROM python:3.13-slim-bookworm" , dockerfileContent ) ;
1419+
1420+ await Verify ( dockerfileContent ) ;
1421+ }
1422+
1423+ [ Fact ]
1424+ public async Task FallbackDockerfile_GeneratesDockerfileWithoutUv_WithoutRequirementsTxt ( )
1425+ {
1426+ using var sourceDir = new TempDirectory ( ) ;
1427+ using var outputDir = new TempDirectory ( ) ;
1428+ var projectDirectory = sourceDir . Path ;
1429+
1430+ // Create a Python project without UV and without requirements.txt
1431+ var scriptContent = """
1432+ print("Hello from non-UV project with no dependencies!")
1433+ """ ;
1434+
1435+ var pyprojectContent = """
1436+ [project]
1437+ name = "test-app"
1438+ version = "0.1.0"
1439+ requires-python = ">=3.11"
1440+ """ ;
1441+
1442+ File . WriteAllText ( Path . Combine ( projectDirectory , "main.py" ) , scriptContent ) ;
1443+ File . WriteAllText ( Path . Combine ( projectDirectory , "pyproject.toml" ) , pyprojectContent ) ;
1444+
1445+ using var builder = TestDistributedApplicationBuilder . Create ( DistributedApplicationOperation . Publish , outputDir . Path , step : "publish-manifest" ) ;
1446+
1447+ // Add Python resources without UV environment
1448+ builder . AddPythonScript ( "script-app" , projectDirectory , "main.py" ) ;
1449+
1450+ var app = builder . Build ( ) ;
1451+ app . Run ( ) ;
1452+
1453+ // Verify that Dockerfile was generated
1454+ var dockerfilePath = Path . Combine ( outputDir . Path , "script-app.Dockerfile" ) ;
1455+ Assert . True ( File . Exists ( dockerfilePath ) , "Dockerfile should be generated for non-UV Python app" ) ;
1456+
1457+ var dockerfileContent = File . ReadAllText ( dockerfilePath ) ;
1458+
1459+ // Verify it's a fallback Dockerfile (single stage, no UV)
1460+ Assert . DoesNotContain ( "uv sync" , dockerfileContent ) ;
1461+ Assert . DoesNotContain ( "ghcr.io/astral-sh/uv" , dockerfileContent ) ;
1462+
1463+ // Verify it doesn't have pip install since there's no requirements.txt
1464+ Assert . DoesNotContain ( "pip install" , dockerfileContent ) ;
1465+
1466+ // Verify it uses the same runtime image as UV workflow
1467+ Assert . Contains ( "FROM python:3.11-slim-bookworm" , dockerfileContent ) ;
1468+
1469+ await Verify ( dockerfileContent ) ;
1470+ }
1471+
1472+ [ Fact ]
1473+ public async Task FallbackDockerfile_GeneratesDockerfileForAllEntrypointTypes ( )
1474+ {
1475+ using var sourceDir = new TempDirectory ( ) ;
1476+ using var outputDir = new TempDirectory ( ) ;
1477+ var projectDirectory = sourceDir . Path ;
1478+
1479+ // Create a Python project without UV
1480+ var scriptContent = """
1481+ print("Hello!")
1482+ """ ;
1483+
1484+ var pythonVersionContent = "3.12" ;
1485+
1486+ File . WriteAllText ( Path . Combine ( projectDirectory , "main.py" ) , scriptContent ) ;
1487+ File . WriteAllText ( Path . Combine ( projectDirectory , ".python-version" ) , pythonVersionContent ) ;
1488+
1489+ using var builder = TestDistributedApplicationBuilder . Create ( DistributedApplicationOperation . Publish , outputDir . Path , step : "publish-manifest" ) ;
1490+
1491+ // Add Python resources with different entrypoint types, none using UV
1492+ builder . AddPythonScript ( "script-app" , projectDirectory , "main.py" ) ;
1493+ builder . AddPythonModule ( "module-app" , projectDirectory , "mymodule" ) ;
1494+ builder . AddPythonExecutable ( "executable-app" , projectDirectory , "pytest" ) ;
1495+
1496+ var app = builder . Build ( ) ;
1497+ app . Run ( ) ;
1498+
1499+ // Verify that Dockerfiles were generated for each entrypoint type
1500+ var scriptDockerfilePath = Path . Combine ( outputDir . Path , "script-app.Dockerfile" ) ;
1501+ Assert . True ( File . Exists ( scriptDockerfilePath ) , "Dockerfile should be generated for script entrypoint" ) ;
1502+
1503+ var moduleDockerfilePath = Path . Combine ( outputDir . Path , "module-app.Dockerfile" ) ;
1504+ Assert . True ( File . Exists ( moduleDockerfilePath ) , "Dockerfile should be generated for module entrypoint" ) ;
1505+
1506+ var executableDockerfilePath = Path . Combine ( outputDir . Path , "executable-app.Dockerfile" ) ;
1507+ Assert . True ( File . Exists ( executableDockerfilePath ) , "Dockerfile should be generated for executable entrypoint" ) ;
1508+
1509+ var scriptDockerfileContent = File . ReadAllText ( scriptDockerfilePath ) ;
1510+ var moduleDockerfileContent = File . ReadAllText ( moduleDockerfilePath ) ;
1511+ var executableDockerfileContent = File . ReadAllText ( executableDockerfilePath ) ;
1512+
1513+ // Verify none use UV
1514+ Assert . DoesNotContain ( "uv sync" , scriptDockerfileContent ) ;
1515+ Assert . DoesNotContain ( "uv sync" , moduleDockerfileContent ) ;
1516+ Assert . DoesNotContain ( "uv sync" , executableDockerfileContent ) ;
1517+
1518+ // Verify correct entrypoints
1519+ Assert . Contains ( "ENTRYPOINT [\" python\" ,\" main.py\" ]" , scriptDockerfileContent ) ;
1520+ Assert . Contains ( "ENTRYPOINT [\" python\" ,\" -m\" ,\" mymodule\" ]" , moduleDockerfileContent ) ;
1521+ Assert . Contains ( "ENTRYPOINT [\" pytest\" ]" , executableDockerfileContent ) ;
1522+
1523+ await Verify ( scriptDockerfileContent )
1524+ . AppendContentAsFile ( moduleDockerfileContent )
1525+ . AppendContentAsFile ( executableDockerfileContent ) ;
1526+ }
13751527}
13761528
0 commit comments