99
1010use Tester ;
1111use Tester \Ansi ;
12+ use Tester \Environment ;
13+ use Tester \Runner \Job ;
1214use Tester \Runner \Runner ;
1315use Tester \Runner \Test ;
14- use function sprintf , strlen ;
15- use const DIRECTORY_SEPARATOR ;
16+ use function count , fwrite , sprintf , str_repeat , strlen ;
1617
1718
1819/**
@@ -24,6 +25,8 @@ class ConsolePrinter implements Tester\Runner\OutputHandler
2425 public const ModeCider = 2 ;
2526 public const ModeLines = 3 ;
2627
28+ private const MaxDisplayedThreads = 20 ;
29+
2730 /** @var resource */
2831 private $ file ;
2932 private string $ buffer ;
@@ -33,6 +36,11 @@ class ConsolePrinter implements Tester\Runner\OutputHandler
3336 /** @var array<Test::Passed|Test::Skipped|Test::Failed, int> result type => count */
3437 private array $ results ;
3538 private ?string $ baseDir ;
39+ private int $ panelWidth = 60 ;
40+ private int $ panelHeight = 0 ;
41+
42+ /** @var \WeakMap<Job, float> */
43+ private \WeakMap $ startTimes ;
3644
3745
3846 public function __construct (
@@ -43,6 +51,7 @@ public function __construct(
4351 private int $ mode = self ::ModeDots,
4452 ) {
4553 $ this ->file = fopen ($ file ?? 'php://output ' , 'w ' ) ?: throw new \RuntimeException ("Cannot open file ' $ file' for writing. " );
54+ $ this ->startTimes = new \WeakMap ;
4655 }
4756
4857
@@ -53,6 +62,9 @@ public function begin(): void
5362 $ this ->baseDir = null ;
5463 $ this ->results = [Test::Passed => 0 , Test::Skipped => 0 , Test::Failed => 0 ];
5564 $ this ->time = -microtime (as_float: true );
65+ if ($ this ->mode === self ::ModeCider && $ this ->runner ->threadCount < 2 ) {
66+ $ this ->mode = self ::ModeLines;
67+ }
5668 fwrite ($ this ->file , $ this ->runner ->getInterpreter ()->getShortInfo ()
5769 . ' | ' . $ this ->runner ->getInterpreter ()->getCommandLine ()
5870 . " | {$ this ->runner ->threadCount } thread " . ($ this ->runner ->threadCount > 1 ? 's ' : '' ) . "\n\n" );
@@ -89,7 +101,7 @@ public function finish(Test $test): void
89101 $ this ->results [$ result ]++;
90102 fwrite ($ this ->file , match ($ this ->mode ) {
91103 self ::ModeDots => [Test::Passed => '. ' , Test::Skipped => 's ' , Test::Failed => Ansi::colorize ('F ' , 'white/red ' )][$ result ],
92- self ::ModeCider => [Test::Passed => ' 🍏 ' , Test::Skipped => ' s ' , Test::Failed => ' 🍎 ' ][ $ result ] ,
104+ self ::ModeCider => '' ,
93105 self ::ModeLines => $ this ->generateFinishLine ($ test ),
94106 });
95107
@@ -106,6 +118,12 @@ public function finish(Test $test): void
106118
107119 public function end (): void
108120 {
121+ if ($ this ->panelHeight ) {
122+ fwrite ($ this ->file , Ansi::cursorMove (y: -$ this ->panelHeight )
123+ . str_repeat (Ansi::clearLine () . "\n" , $ this ->panelHeight )
124+ . Ansi::cursorMove (y: -$ this ->panelHeight ));
125+ }
126+
109127 $ run = array_sum ($ this ->results );
110128 fwrite ($ this ->file , !$ this ->count ? "No tests found \n" :
111129 "\n\n" . $ this ->buffer . "\n"
@@ -158,4 +176,76 @@ private function generateFinishLine(Test $test): string
158176 $ message ,
159177 );
160178 }
179+
180+
181+ public function jobStarted (Job $ job ): void
182+ {
183+ $ this ->startTimes [$ job ] = microtime (true );
184+ }
185+
186+
187+ /**
188+ * @param Job[] $running
189+ */
190+ public function tick (array $ running ): void
191+ {
192+ if ($ this ->mode !== self ::ModeCider) {
193+ return ;
194+ }
195+
196+ // Move cursor up to overwrite previous output
197+ if ($ this ->panelHeight ) {
198+ fwrite ($ this ->file , Ansi::cursorMove (y: -$ this ->panelHeight ));
199+ }
200+
201+ $ lines = [];
202+
203+ // Header with progress bar
204+ $ barWidth = $ this ->panelWidth - 12 ;
205+ $ filled = (int ) round ($ barWidth * ($ this ->runner ->getFinishedCount () / $ this ->runner ->getJobCount ()));
206+ $ lines [] = '╭ ' . Ansi::pad (' ' . str_repeat ('█ ' , $ filled ) . str_repeat ('░ ' , $ barWidth - $ filled ) . ' ' , $ this ->panelWidth - 2 , '─ ' , STR_PAD_BOTH ) . '╮ ' ;
207+
208+ $ threadJobs = [];
209+ foreach ($ running as $ job ) {
210+ $ threadJobs [(int ) $ job ->getEnvironmentVariable (Environment::VariableThread)] = $ job ;
211+ }
212+
213+ // Thread lines
214+ $ numWidth = strlen ((string ) $ this ->runner ->threadCount );
215+ $ displayCount = min ($ this ->runner ->threadCount , self ::MaxDisplayedThreads);
216+
217+ for ($ t = 1 ; $ t <= $ displayCount ; $ t ++) {
218+ if (isset ($ threadJobs [$ t ])) {
219+ $ job = $ threadJobs [$ t ];
220+ $ name = basename ($ job ->getTest ()->getFile ());
221+ $ time = sprintf ('%0.1fs ' , microtime (true ) - ($ this ->startTimes [$ job ] ?? microtime (true )));
222+ $ nameWidth = $ this ->panelWidth - $ numWidth - strlen ($ time ) - 7 ;
223+ $ name = Ansi::pad (Ansi::truncate ($ name , $ nameWidth ), $ nameWidth );
224+ $ line = Ansi::colorize (sprintf ("% {$ numWidth }d: " , $ t ), 'lime ' ) . " $ name " . Ansi::colorize ($ time , 'yellow ' );
225+ } else {
226+ $ line = Ansi::pad (Ansi::colorize (sprintf ("% {$ numWidth }d: - " , $ t ), 'gray ' ), $ this ->panelWidth - 4 );
227+ }
228+ $ lines [] = '│ ' . $ line . ' │ ' ;
229+ }
230+
231+ if ($ this ->runner ->threadCount > self ::MaxDisplayedThreads) {
232+ $ more = $ this ->runner ->threadCount - self ::MaxDisplayedThreads;
233+ $ ellipsis = Ansi::colorize ("… and $ more more " , 'gray ' );
234+ $ lines [] = '│ ' . Ansi::pad ($ ellipsis , $ this ->panelWidth - 2 ) . '│ ' ;
235+ }
236+
237+ // Footer: (85 tests, 🍏×74 🍎×2, 9.0s)
238+ $ summary = "( $ this ->count tests, "
239+ . ($ this ->results [Test::Passed] ? "🍏× {$ this ->results [Test::Passed]}" : '' )
240+ . ($ this ->results [Test::Failed] ? " 🍎× {$ this ->results [Test::Failed]}" : '' )
241+ . ', ' . sprintf ('%0.1fs ' , $ this ->time + microtime (true )) . ') ' ;
242+ $ lines [] = '╰ ' . Ansi::pad ($ summary , $ this ->panelWidth - 2 , '─ ' , STR_PAD_BOTH ) . '╯ ' ;
243+
244+ foreach ($ lines as $ line ) {
245+ fwrite ($ this ->file , "\r" . $ line . Ansi::clearLine () . "\n" );
246+ }
247+ fflush ($ this ->file );
248+
249+ $ this ->panelHeight = count ($ lines );
250+ }
161251}
0 commit comments