@@ -997,6 +997,80 @@ def test_params_without_params_field(self, tmp_dir, dvc):
997997 ):
998998 resolver .resolve ()
999999
1000+ def test_foreach_with_dynamic_params_and_output (self , tmp_dir , dvc ):
1001+ """Test foreach with dynamic param file loading and file output.
1002+
1003+ This tests the exact production use case where:
1004+ - dvc.yaml is inside data/ directory
1005+ - foreach generates stages with different model/data types
1006+ - params files are loaded dynamically based on ${item.*}
1007+ - param values are used in cmd and output path
1008+ - no params.yaml file exists
1009+ """
1010+ # Remove params.yaml if it exists
1011+ if (tmp_dir / DEFAULT_PARAMS_FILE ).exists ():
1012+ (tmp_dir / DEFAULT_PARAMS_FILE ).unlink ()
1013+
1014+ # Define test data
1015+ items = [
1016+ {"model_type" : "region" , "data_type" : "full" },
1017+ {"model_type" : "country" , "data_type" : "full" },
1018+ ]
1019+
1020+ # Create directory structure and param files dynamically
1021+ (tmp_dir / "data" ).mkdir ()
1022+ for item in items :
1023+ # wdir is ../, so files are relative to repo root
1024+ level_dir = tmp_dir / f"{ item ['model_type' ]} _level"
1025+ level_dir .mkdir ()
1026+
1027+ # Create data-version.toml with DATA_DIR
1028+ (level_dir / "data-version.toml" ).dump (
1029+ {"DATA_DIR" : f"/datasets/{ item ['model_type' ]} _data" }
1030+ )
1031+
1032+ dvc_yaml = {
1033+ "stages" : {
1034+ "test" : {
1035+ "foreach" : items ,
1036+ "do" : {
1037+ "params" : [
1038+ {"${item.model_type}_level/data-version.toml" : ["DATA_DIR" ]}
1039+ ],
1040+ "wdir" : "../" ,
1041+ "cmd" : (
1042+ f"echo ${{{ PARAMS_NAMESPACE } .DATA_DIR}} > "
1043+ f"data/${{item.model_type}}_level/${{item.data_type}}/output.txt"
1044+ ),
1045+ },
1046+ }
1047+ }
1048+ }
1049+
1050+ # DataResolver is run from data/ directory
1051+ resolver = DataResolver (dvc , (tmp_dir / "data" ).fs_path , dvc_yaml )
1052+ result = resolver .resolve ()
1053+
1054+ # Build expected result with comprehension
1055+ # Note: foreach with list of dicts generates numeric indices (@0, @1, ...)
1056+ expected = {
1057+ "stages" : {
1058+ f"test@{ idx } " : {
1059+ "params" : [
1060+ {f"{ item ['model_type' ]} _level/data-version.toml" : ["DATA_DIR" ]}
1061+ ],
1062+ "wdir" : "../" ,
1063+ "cmd" : (
1064+ f"echo /datasets/{ item ['model_type' ]} _data > "
1065+ f"data/{ item ['model_type' ]} _level/{ item ['data_type' ]} /output.txt"
1066+ ),
1067+ }
1068+ for idx , item in enumerate (items )
1069+ }
1070+ }
1071+
1072+ assert_stage_equal (result , expected )
1073+
10001074 def test_missing_params_file (self , tmp_dir , dvc ):
10011075 """Test error when param file doesn't exist."""
10021076 dvc_yaml = {
0 commit comments