@@ -7,6 +7,7 @@ use Symfony\Component\Console\Input\InputArgument;
77use Symfony \Component \Console \Input \InputInterface ;
88use Symfony \Component \Console \Input \InputOption ;
99use Symfony \Component \Console \Output \OutputInterface ;
10+ use Symfony \Component \Console \Question \ChoiceQuestion ;
1011use Symfony \Component \Console \SingleCommandApplication ;
1112use Symfony \Component \Console \Style \SymfonyStyle ;
1213use Symfony \Component \Finder \Finder ;
@@ -22,6 +23,7 @@ $app = (new SingleCommandApplication('Symfony AI Example Runner'))
2223 ->setDescription ('Runs all Symfony AI examples in folder examples/ ' )
2324 ->addArgument ('subdirectories ' , InputArgument::OPTIONAL | InputArgument::IS_ARRAY , 'List of subdirectories to run examples from, e.g. "anthropic" or "huggingface". ' )
2425 ->addOption ('filter ' , 'f ' , InputOption::VALUE_REQUIRED , 'Filter examples by name, e.g. "audio" or "toolcall". ' )
26+ ->addOption ('chunk ' , 'c ' , InputOption::VALUE_REQUIRED , 'Number of examples to run in parallel per chunk. ' , 30 )
2527 ->setCode (function (InputInterface $ input , OutputInterface $ output ) {
2628 $ io = new SymfonyStyle ($ input , $ output );
2729 $ io ->title ('Symfony AI Examples ' );
@@ -54,60 +56,100 @@ $app = (new SingleCommandApplication('Symfony AI Example Runner'))
5456 ->notName (['bootstrap.php ' , '_[a-z\-]*.php ' ])
5557 ->files ();
5658
57- $ io ->comment (sprintf ('Found %d example(s) to run. ' , count ($ examples )));
59+ $ chunkSize = (int ) $ input ->getOption ('chunk ' );
60+ $ examplesArray = iterator_to_array ($ examples );
61+ $ chunks = array_chunk ($ examplesArray , $ chunkSize );
62+
63+ $ io ->comment (sprintf ('Found %d example(s) to run in %d chunk(s) of max %d examples. ' , count ($ examplesArray ), count ($ chunks ), $ chunkSize ));
5864
5965 /** @var array{example: SplFileInfo, process: Process} $exampleRuns */
6066 $ exampleRuns = [];
61- foreach ($ examples as $ example ) {
62- $ exampleRuns [] = [
63- 'example ' => $ example ,
64- 'process ' => $ process = new Process (['php ' , $ example ->getRealPath ()]),
65- ];
66- $ process ->start ();
67- }
6867
69- $ section = $ output ->section ();
70- $ renderTable = function () use ($ exampleRuns , $ section ) {
71- $ section ->clear ();
72- $ table = new Table ($ section );
73- $ table ->setHeaders (['Example ' , 'State ' , 'Output ' ]);
74- foreach ($ exampleRuns as $ run ) {
75- /** @var SplFileInfo $example */
76- /** @var Process $process */
77- ['example ' => $ example , 'process ' => $ process ] = $ run ;
78-
79- $ output = str_replace (PHP_EOL , ' ' , $ process ->getOutput ());
80- $ output = strlen ($ output ) <= 100 ? $ output : substr ($ output , 0 , 100 ).'... ' ;
81- $ emptyOutput = 0 === strlen (trim ($ output ));
82-
83- $ state = 'Running ' ;
84- if ($ process ->isTerminated ()) {
85- $ success = $ process ->isSuccessful () && !$ emptyOutput ;
86- $ state = $ success ? '<info>Finished</info> '
87- : (1 === $ run ['process ' ]->getExitCode () || $ emptyOutput ? '<error>Failed</error> ' : '<comment>Skipped</comment> ' );
68+ foreach ($ chunks as $ chunkIndex => $ chunk ) {
69+ $ io ->section (sprintf ('Running chunk %d/%d (%d examples) ' , $ chunkIndex + 1 , count ($ chunks ), count ($ chunk )));
70+
71+ $ chunkRuns = [];
72+ foreach ($ chunk as $ example ) {
73+ $ run = [
74+ 'example ' => $ example ,
75+ 'process ' => $ process = new Process (['php ' , $ example ->getRealPath ()]),
76+ ];
77+ $ chunkRuns [] = $ run ;
78+ $ exampleRuns [] = $ run ;
79+ $ process ->start ();
80+ }
81+
82+ $ section = $ output ->section ();
83+ $ renderTable = function () use ($ chunkRuns , $ section ) {
84+ $ section ->clear ();
85+ $ table = new Table ($ section );
86+ $ table ->setHeaders (['Example ' , 'State ' , 'Output ' ]);
87+ foreach ($ chunkRuns as $ run ) {
88+ /** @var SplFileInfo $example */
89+ /** @var Process $process */
90+ ['example ' => $ example , 'process ' => $ process ] = $ run ;
91+
92+ $ output = str_replace (PHP_EOL , ' ' , $ process ->getOutput ());
93+ $ output = strlen ($ output ) <= 100 ? $ output : substr ($ output , 0 , 100 ).'... ' ;
94+ $ emptyOutput = 0 === strlen (trim ($ output ));
95+
96+ $ state = 'Running ' ;
97+ if ($ process ->isTerminated ()) {
98+ $ success = $ process ->isSuccessful () && !$ emptyOutput ;
99+ $ state = $ success ? '<info>Finished</info> '
100+ : (1 === $ run ['process ' ]->getExitCode () || $ emptyOutput ? '<error>Failed</error> ' : '<comment>Skipped</comment> ' );
101+ }
102+
103+ $ table ->addRow ([$ example ->getRelativePathname (), $ state , $ output ]);
88104 }
105+ $ table ->render ();
106+ };
89107
90- $ table ->addRow ([$ example ->getRelativePathname (), $ state , $ output ]);
108+ $ chunkRunning = fn () => array_reduce ($ chunkRuns , fn ($ running , $ example ) => $ running || $ example ['process ' ]->isRunning (), false );
109+ while ($ chunkRunning ()) {
110+ $ renderTable ();
111+ sleep (1 );
91112 }
92- $ table ->render ();
93- };
94113
95- $ examplesRunning = fn () => array_reduce ($ exampleRuns , fn ($ running , $ example ) => $ running || $ example ['process ' ]->isRunning (), false );
96- while ($ examplesRunning ()) {
97114 $ renderTable ();
98- sleep ( 1 );
115+ $ io -> newLine ( );
99116 }
100117
101- $ renderTable ();
102- $ io ->newLine ();
118+ // Group results by directory
119+ $ resultsByDirectory = [];
120+ foreach ($ exampleRuns as $ run ) {
121+ $ directory = trim (str_replace (__DIR__ , '' , $ run ['example ' ]->getPath ()), '/ ' );
122+ if (!isset ($ resultsByDirectory [$ directory ])) {
123+ $ resultsByDirectory [$ directory ] = ['successful ' => 0 , 'skipped ' => 0 , 'failed ' => 0 ];
124+ }
103125
104- $ successCount = array_reduce ($ exampleRuns , function ($ count , $ example ) {
105- if ($ example ['process ' ]->isSuccessful () && strlen (trim ($ example ['process ' ]->getOutput ())) > 0 ) {
106- return $ count + 1 ;
126+ $ emptyOutput = 0 === strlen (trim ($ run ['process ' ]->getOutput ()));
127+ if ($ run ['process ' ]->isSuccessful () && !$ emptyOutput ) {
128+ $ resultsByDirectory [$ directory ]['successful ' ]++;
129+ } elseif (1 === $ run ['process ' ]->getExitCode () || $ emptyOutput ) {
130+ $ resultsByDirectory [$ directory ]['failed ' ]++;
131+ } else {
132+ $ resultsByDirectory [$ directory ]['skipped ' ]++;
107133 }
108- return $ count ;
109- }, 0 );
134+ }
135+
136+ ksort ($ resultsByDirectory );
137+
138+ $ io ->section ('Results by Directory ' );
139+ $ resultsTable = new Table ($ output );
140+ $ resultsTable ->setHeaders (['Directory ' , 'Successful ' , 'Skipped ' , 'Failed ' ]);
141+ foreach ($ resultsByDirectory as $ directory => $ stats ) {
142+ $ resultsTable ->addRow ([
143+ $ directory ?: '. ' ,
144+ sprintf ('<info>%d</info> ' , $ stats ['successful ' ]),
145+ sprintf ('<comment>%d</comment> ' , $ stats ['skipped ' ]),
146+ sprintf ('<error>%d</error> ' , $ stats ['failed ' ]),
147+ ]);
148+ }
149+ $ resultsTable ->render ();
150+ $ io ->newLine ();
110151
152+ $ successCount = array_sum (array_column ($ resultsByDirectory , 'successful ' ));
111153 $ totalCount = count ($ exampleRuns );
112154
113155 if ($ successCount < $ totalCount ) {
@@ -116,11 +158,60 @@ $app = (new SingleCommandApplication('Symfony AI Example Runner'))
116158 $ io ->success (sprintf ('All %d examples ran successfully! ' , $ totalCount ));
117159 }
118160
119- foreach ($ exampleRuns as $ run ) {
120- if (!$ run ['process ' ]->isSuccessful ()) {
121- $ io ->section ('Error in ' . $ run ['example ' ]->getRelativePathname ());
122- $ output = $ run ['process ' ]->getErrorOutput ();
123- $ io ->text ('' !== $ output ? $ output : $ run ['process ' ]->getOutput ());
161+ if ($ output ->isVerbose ()) {
162+ foreach ($ exampleRuns as $ run ) {
163+ if (!$ run ['process ' ]->isSuccessful ()) {
164+ $ io ->section ('Error in ' . $ run ['example ' ]->getRelativePathname ());
165+ $ output = $ run ['process ' ]->getErrorOutput ();
166+ $ io ->text ('' !== $ output ? $ output : $ run ['process ' ]->getOutput ());
167+ }
168+ }
169+ }
170+
171+ // Interactive retry for failed examples
172+ if ($ input ->isInteractive ()) {
173+ $ failedRuns = array_filter ($ exampleRuns , fn ($ run ) => !$ run ['process ' ]->isSuccessful ());
174+
175+ while (count ($ failedRuns ) > 0 ) {
176+ $ io ->newLine ();
177+ $ choices = [];
178+ $ choiceMap = [];
179+ foreach ($ failedRuns as $ key => $ run ) {
180+ $ choice = $ run ['example ' ]->getRelativePathname ();
181+ $ choices [] = $ choice ;
182+ $ choiceMap [$ choice ] = $ key ;
183+ }
184+ $ choices [] = 'Exit ' ;
185+
186+ $ question = new ChoiceQuestion (
187+ sprintf ('Select a failed example to re-run (%d remaining) ' , count ($ failedRuns )),
188+ $ choices ,
189+ count ($ choices ) - 1
190+ );
191+ $ question ->setErrorMessage ('Choice %s is invalid. ' );
192+
193+ $ selected = $ io ->askQuestion ($ question );
194+
195+ if ('Exit ' === $ selected ) {
196+ break ;
197+ }
198+
199+ $ runKey = $ choiceMap [$ selected ];
200+ $ run = $ failedRuns [$ runKey ];
201+
202+ $ io ->section (sprintf ('Re-running: %s ' , $ run ['example ' ]->getRelativePathname ()));
203+ $ process = new Process (['php ' , $ run ['example ' ]->getRealPath ()]);
204+ $ process ->run (function ($ type , $ buffer ) use ($ output ) {
205+ $ output ->write ($ buffer );
206+ });
207+
208+ if ($ process ->isSuccessful ()) {
209+ unset($ failedRuns [$ runKey ]);
210+ }
211+ }
212+
213+ if ($ successCount !== $ totalCount && count ($ failedRuns ) === 0 ) {
214+ $ io ->success ('All previously failed examples now pass! ' );
124215 }
125216 }
126217
0 commit comments