1
1
import copy
2
+ import itertools
2
3
import json
3
4
import os
4
5
import re
5
- from typing import Dict , List , Optional , Tuple , Any
6
+ from typing import Any , Dict , List , Optional , Tuple
6
7
7
8
import jsonschema
8
9
import yaml
9
- from ray_release .test import (
10
- Test ,
11
- TestDefinition ,
12
- )
13
10
from ray_release .anyscale_util import find_cloud_by_name
14
11
from ray_release .bazel import bazel_runfile
15
12
from ray_release .exception import ReleaseTestCLIError , ReleaseTestConfigError
16
13
from ray_release .logger import logger
14
+ from ray_release .test import (
15
+ Test ,
16
+ TestDefinition ,
17
+ )
17
18
from ray_release .util import DeferredEnvVar , deep_update
18
19
19
-
20
20
DEFAULT_WHEEL_WAIT_TIMEOUT = 7200 # Two hours
21
21
DEFAULT_COMMAND_TIMEOUT = 1800
22
22
DEFAULT_BUILD_TIMEOUT = 3600
@@ -84,6 +84,11 @@ def parse_test_definition(test_definitions: List[TestDefinition]) -> List[Test]:
84
84
default_definition = {}
85
85
tests = []
86
86
for test_definition in test_definitions :
87
+ if "matrix" in test_definition and "variations" in test_definition :
88
+ raise ReleaseTestConfigError (
89
+ "You can't specify both 'matrix' and 'variations' in a test definition"
90
+ )
91
+
87
92
if test_definition ["name" ] == "DEFAULTS" :
88
93
default_definition = copy .deepcopy (test_definition )
89
94
continue
@@ -93,29 +98,102 @@ def parse_test_definition(test_definitions: List[TestDefinition]) -> List[Test]:
93
98
copy .deepcopy (default_definition ), test_definition
94
99
)
95
100
96
- if "variations" not in test_definition :
101
+ if "variations" in test_definition :
102
+ tests .extend (_parse_test_definition_with_variations (test_definition ))
103
+ elif "matrix" in test_definition :
104
+ tests .extend (_parse_test_definition_with_matrix (test_definition ))
105
+ else :
97
106
tests .append (Test (test_definition ))
98
- continue
99
107
100
- variations = test_definition .pop ("variations" )
108
+ return tests
109
+
110
+
111
+ def _parse_test_definition_with_variations (
112
+ test_definition : TestDefinition ,
113
+ ) -> List [Test ]:
114
+ tests = []
115
+
116
+ variations = test_definition .pop ("variations" )
117
+ _test_definition_invariant (
118
+ test_definition ,
119
+ variations ,
120
+ "variations field cannot be empty in a test definition" ,
121
+ )
122
+ for variation in variations :
101
123
_test_definition_invariant (
102
124
test_definition ,
103
- variations ,
104
- "variations field cannot be empty in a test definition " ,
125
+ "__suffix__" in variation ,
126
+ "missing __suffix__ field in a variation " ,
105
127
)
106
- for variation in variations :
107
- _test_definition_invariant (
108
- test_definition ,
109
- "__suffix__" in variation ,
110
- "missing __suffix__ field in a variation" ,
128
+ test = copy .deepcopy (test_definition )
129
+ test ["name" ] = f'{ test ["name" ]} .{ variation .pop ("__suffix__" )} '
130
+ test = deep_update (test , variation )
131
+ tests .append (Test (test ))
132
+
133
+ return tests
134
+
135
+
136
+ def _parse_test_definition_with_matrix (test_definition : TestDefinition ) -> List [Test ]:
137
+ tests = []
138
+
139
+ matrix = test_definition .pop ("matrix" )
140
+ variables = tuple (matrix ["setup" ].keys ())
141
+ combinations = itertools .product (* matrix ["setup" ].values ())
142
+ for combination in combinations :
143
+ test = test_definition
144
+ for variable , value in zip (variables , combination ):
145
+ test = _substitute_variable (test , variable , str (value ))
146
+ tests .append (Test (test ))
147
+
148
+ adjustments = matrix .pop ("adjustments" , [])
149
+ for adjustment in adjustments :
150
+ if not set (adjustment ["with" ]) == set (variables ):
151
+ raise ReleaseTestConfigError (
152
+ "You need to specify all matrix variables in the adjustment."
111
153
)
112
- test = copy .deepcopy (test_definition )
113
- test ["name" ] = f'{ test ["name" ]} .{ variation .pop ("__suffix__" )} '
114
- test = deep_update (test , variation )
115
- tests .append (Test (test ))
154
+
155
+ test = test_definition
156
+ for variable , value in adjustment ["with" ].items ():
157
+ test = _substitute_variable (test , variable , str (value ))
158
+ tests .append (Test (test ))
159
+
116
160
return tests
117
161
118
162
163
+ def _substitute_variable (data : Dict , variable : str , replacement : str ) -> Dict :
164
+ """Substitute a variable in the provided dictionary with a replacement value.
165
+
166
+ This function traverses dict and list values, and substitutes the variable in
167
+ string values. The syntax for variables is `{{variable}}`.
168
+
169
+ Args:
170
+ data: The dictionary to substitute the variable in.
171
+ variable: The variable to substitute.
172
+ replacement: The replacement value.
173
+
174
+ Returns:
175
+ A new dictionary with the variable substituted.
176
+
177
+ Examples:
178
+ >>> test_definition = {"name": "test-{{arg}}"}
179
+ >>> _substitute_variable(test_definition, "arg", "1")
180
+ {'name': 'test-1'}
181
+ """
182
+ # Create a copy to avoid mutating the original.
183
+ data = copy .deepcopy (data )
184
+
185
+ pattern = r"\{\{\s*" + re .escape (variable ) + r"\s*\}\}"
186
+ for key , value in data .items ():
187
+ if isinstance (value , dict ):
188
+ data [key ] = _substitute_variable (value , variable , replacement )
189
+ elif isinstance (value , list ):
190
+ data [key ] = [re .sub (pattern , replacement , string ) for string in value ]
191
+ elif isinstance (value , str ):
192
+ data [key ] = re .sub (pattern , replacement , value )
193
+
194
+ return data
195
+
196
+
119
197
def load_schema_file (path : Optional [str ] = None ) -> Dict :
120
198
path = path or RELEASE_TEST_SCHEMA_FILE
121
199
with open (path , "rt" ) as fp :
0 commit comments