1111
1212use Tester ;
1313use Tester \Console ;
14+ use Tester \Environment ;
15+ use Tester \Runner \Job ;
1416use Tester \Runner \Runner ;
1517use Tester \Runner \Test ;
16- use function sprintf , strlen ;
17- use const DIRECTORY_SEPARATOR ;
18+ use function count , fwrite , sprintf , str_repeat , strlen ;
19+ use const DIRECTORY_SEPARATOR , STR_PAD_BOTH ;
1820
1921
2022/**
@@ -26,13 +28,18 @@ class ConsolePrinter implements Tester\Runner\OutputHandler
2628 public const ModeCider = 2 ;
2729 public const ModeLine = 3 ;
2830
31+ private const MaxDisplayedThreads = 20 ;
32+
2933 /** @var resource */
3034 private $ file ;
3135 private string $ buffer ;
3236 private float $ time ;
3337 private int $ count ;
3438 private array $ results ;
3539 private ?string $ baseDir ;
40+ private int $ panelWidth = 60 ;
41+ private int $ panelHeight = 0 ;
42+ private \WeakMap $ startTimes ;
3643
3744
3845 public function __construct (
@@ -42,6 +49,7 @@ public function __construct(
4249 private int $ mode = self ::ModeDots,
4350 ) {
4451 $ this ->file = fopen ($ file ?? 'php://output ' , 'w ' );
52+ $ this ->startTimes = new \WeakMap ;
4553 }
4654
4755
@@ -52,6 +60,9 @@ public function begin(): void
5260 $ this ->baseDir = null ;
5361 $ this ->results = [Test::Passed => 0 , Test::Skipped => 0 , Test::Failed => 0 ];
5462 $ this ->time = -microtime (as_float: true );
63+ if ($ this ->mode === self ::ModeCider && $ this ->runner ->threadCount < 2 ) {
64+ $ this ->mode = self ::ModeLine;
65+ }
5566 fwrite ($ this ->file , Console::showCursor (false )
5667 . $ this ->runner ->getInterpreter ()->getShortInfo ()
5768 . ' | ' . $ this ->runner ->getInterpreter ()->getCommandLine ()
@@ -89,7 +100,7 @@ public function finish(Test $test): void
89100 $ this ->results [$ result ]++;
90101 fwrite ($ this ->file , match ($ this ->mode ) {
91102 self ::ModeDots => [Test::Passed => '. ' , Test::Skipped => 's ' , Test::Failed => Console::colorize ('F ' , 'white/red ' )][$ result ],
92- self ::ModeCider => [Test::Passed => ' 🍏 ' , Test::Skipped => ' s ' , Test::Failed => ' 🍎 ' ][ $ result ] ,
103+ self ::ModeCider => '' ,
93104 self ::ModeLine => $ this ->generateFinishLine ($ test ),
94105 });
95106
@@ -106,6 +117,12 @@ public function finish(Test $test): void
106117
107118 public function end (): void
108119 {
120+ if ($ this ->panelHeight ) {
121+ fwrite ($ this ->file , Console::cursorUp ($ this ->panelHeight )
122+ . str_repeat (Console::ClearLine . "\n" , $ this ->panelHeight )
123+ . Console::cursorUp ($ this ->panelHeight ));
124+ }
125+
109126 $ run = array_sum ($ this ->results );
110127 fwrite ($ this ->file , Console::showCursor (true ));
111128 fwrite ($ this ->file , !$ this ->count ? "No tests found \n" :
@@ -159,4 +176,76 @@ private function generateFinishLine(Test $test): string
159176 $ message ,
160177 );
161178 }
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 , Console::cursorUp ($ 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 [] = '╭ ' . Console::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 = Console::pad (Console::truncate ($ name , $ nameWidth ), $ nameWidth );
224+ $ line = Console::colorize (sprintf ("% {$ numWidth }d: " , $ t ), 'lime ' ) . " $ name " . Console::colorize ($ time , 'yellow ' );
225+ } else {
226+ $ line = Console::pad (Console::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 = Console::colorize ("… and $ more more " , 'gray ' );
234+ $ lines [] = '│ ' . Console::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 [] = '╰ ' . Console::pad ($ summary , $ this ->panelWidth - 2 , '─ ' , STR_PAD_BOTH ) . '╯ ' ;
243+
244+ foreach ($ lines as $ line ) {
245+ fwrite ($ this ->file , "\r" . $ line . Console::ClearLine . "\n" );
246+ }
247+ fflush ($ this ->file );
248+
249+ $ this ->panelHeight = count ($ lines );
250+ }
162251}
0 commit comments