99use Testo \Core \Context \TestResult ;
1010use Testo \Core \Filter \DataPointer ;
1111use Testo \Core \Value \Status ;
12+ use Testo \Data \DataCross ;
1213use Testo \Data \DataProvider ;
1314use Testo \Data \DataSet ;
15+ use Testo \Data \DataZip ;
1416use Testo \Data \MultipleResult ;
1517use Testo \Event \Test \TestBatchFinished ;
1618use Testo \Event \Test \TestBatchStarting ;
2628#[InterceptorOptions(order: InterceptorOptions::ORDER_DATA_PROVIDER , onConflict: ConflictPolicy::First)]
2729final class DataProviderInterceptor implements TestRunInterceptor
2830{
31+ /** Key separator for DataZip (parallel combination). */
32+ private const KEY_SEPARATOR_ZIP = '| ' ;
33+
34+ /** Key separator for DataCross (cartesian product). */
35+ private const KEY_SEPARATOR_CROSS = '× ' ;
36+
2937 public function __construct (
3038 private readonly EventDispatcherInterface $ eventDispatcher ,
3139 ) {}
@@ -55,11 +63,7 @@ public function runTest(TestInfo $info, callable $next): TestResult
5563
5664 $ attr = $ attribute ->newInstance ();
5765
58- $ datasets = match (true ) {
59- $ attr instanceof DataProvider => self ::fromDataProvider ($ info , $ attr ),
60- $ attr instanceof DataSet => [($ attr ->name ?? 0 ) => $ attr ->arguments ],
61- default => throw new \RuntimeException ('Unknown Data Provider Attribute type. ' ),
62- };
66+ $ datasets = self ::extractDataSets ($ info , $ attr );
6367
6468 # Handle each data set
6569 $ results = [];
@@ -160,6 +164,17 @@ public function run(
160164 return $ result ;
161165 }
162166
167+ private static function extractDataSets (TestInfo $ info , object $ attr ): iterable
168+ {
169+ return match (true ) {
170+ $ attr instanceof DataProvider => self ::fromDataProvider ($ info , $ attr ),
171+ $ attr instanceof DataSet => [($ attr ->name ?? 0 ) => $ attr ->arguments ],
172+ $ attr instanceof DataCross => self ::fromDataCross ($ info , $ attr ),
173+ $ attr instanceof DataZip => self ::fromDataZip ($ info , $ attr ),
174+ default => throw new \RuntimeException ('Unknown Data Provider Attribute type. ' ),
175+ };
176+ }
177+
163178 /**
164179 * Extract data sets from a DataProvider attribute.
165180 */
@@ -177,7 +192,7 @@ private static function fromDataProvider(TestInfo $info, DataProvider $attribute
177192 $ m = $ class ->getMethod ($ provider );
178193 $ provider = match (true ) {
179194 $ m ->isStatic () => $ m ->getClosure (null ),
180- default => static fn () => $ m ->getClosure ($ info ->caseInfo ->instance ->getInstance ()),
195+ default => static fn () => $ m ->getClosure ($ info ->caseInfo ->instance ->getInstance ()),
181196 };
182197 }
183198
@@ -194,4 +209,96 @@ private static function fromDataProvider(TestInfo $info, DataProvider $attribute
194209
195210 return $ datasets ;
196211 }
212+
213+ /**
214+ * Extract data sets from a DataZip attribute.
215+ */
216+ private static function fromDataZip (TestInfo $ info , DataZip $ attr ): iterable
217+ {
218+ $ generators = \array_map (
219+ static fn ($ providerAttr ): DeferredGenerator => DeferredGenerator::fromHandler (
220+ static function () use ($ info , $ providerAttr ) {
221+ yield from self ::fromDataProvider ($ info , $ providerAttr );
222+ },
223+ ),
224+ $ attr ->providers ,
225+ );
226+
227+ dataset:
228+ $ allFinished = true ;
229+ $ dataSet = [];
230+ $ key = [];
231+ foreach ($ generators as $ gen ) {
232+ if ($ gen ->valid ()) {
233+ $ allFinished = false ;
234+ $ dataSet [] = $ gen ->current ();
235+ $ key [] = $ gen ->key ();
236+ $ gen ->next ();
237+ } else {
238+ $ key [] = '' ;
239+ $ dataSet [] = null ;
240+ }
241+ }
242+
243+ if ($ allFinished ) {
244+ return ;
245+ }
246+
247+ yield \implode (self ::KEY_SEPARATOR_ZIP , $ key ) => \array_merge (...$ dataSet );
248+ goto dataset;
249+ }
250+
251+ /**
252+ * Extract data sets from a DataCross attribute (cartesian product).
253+ */
254+ private static function fromDataCross (TestInfo $ info , DataCross $ attr ): iterable
255+ {
256+ // Collect all datasets from each provider into arrays
257+ $ allDatasets = [];
258+ foreach ($ attr ->providers as $ providerAttr ) {
259+ $ datasets = [];
260+ foreach (self ::extractDataSets ($ info , $ providerAttr ) as $ key => $ dataset ) {
261+ $ datasets [$ key ] = $ dataset ;
262+ }
263+ if ($ datasets === []) {
264+ return ; // If any provider is empty, no combinations possible
265+ }
266+ $ allDatasets [] = $ datasets ;
267+ }
268+
269+ if ($ allDatasets === []) {
270+ return ;
271+ }
272+
273+ yield from self ::cartesianProduct ($ allDatasets );
274+ }
275+
276+ /**
277+ * Generate the cartesian product of multiple arrays of datasets.
278+ *
279+ * @param list<array<array-key, array>> $arrays Arrays of datasets from each provider.
280+ * @param int<0, max> $index Current provider index.
281+ * @param list<array> $currentData Accumulated dataset arguments.
282+ * @param list<string> $currentKeys Accumulated dataset keys.
283+ */
284+ private static function cartesianProduct (
285+ array $ arrays ,
286+ int $ index = 0 ,
287+ array $ currentData = [],
288+ array $ currentKeys = [],
289+ ): iterable {
290+ if ($ index === \count ($ arrays )) {
291+ yield \implode (self ::KEY_SEPARATOR_CROSS , $ currentKeys ) => \array_merge (...$ currentData );
292+ return ;
293+ }
294+
295+ foreach ($ arrays [$ index ] as $ key => $ dataset ) {
296+ yield from self ::cartesianProduct (
297+ $ arrays ,
298+ $ index + 1 ,
299+ [...$ currentData , $ dataset ],
300+ [...$ currentKeys , (string ) $ key ],
301+ );
302+ }
303+ }
197304}
0 commit comments