1111
1212use Tester ;
1313use Tester \Ansi ;
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 ;
1819
1920
2021/**
@@ -26,6 +27,8 @@ class ConsolePrinter implements Tester\Runner\OutputHandler
2627 public const ModeCider = 2 ;
2728 public const ModeLines = 3 ;
2829
30+ private const MaxDisplayedThreads = 20 ;
31+
2932 /** @var resource */
3033 private $ file ;
3134 private string $ buffer ;
@@ -35,6 +38,11 @@ class ConsolePrinter implements Tester\Runner\OutputHandler
3538 /** @var array<int, int> result type (Test::*) => count */
3639 private array $ results ;
3740 private ?string $ baseDir ;
41+ private int $ panelWidth = 60 ;
42+ private int $ panelHeight = 0 ;
43+
44+ /** @var \WeakMap<Job, float> */
45+ private \WeakMap $ startTimes ;
3846
3947
4048 public function __construct (
@@ -45,6 +53,7 @@ public function __construct(
4553 private int $ mode = self ::ModeDots,
4654 ) {
4755 $ this ->file = fopen ($ file ?? 'php://output ' , 'w ' ) ?: throw new \RuntimeException ("Cannot open file ' $ file' for writing. " );
56+ $ this ->startTimes = new \WeakMap ;
4857 }
4958
5059
@@ -55,6 +64,9 @@ public function begin(): void
5564 $ this ->baseDir = null ;
5665 $ this ->results = [Test::Passed => 0 , Test::Skipped => 0 , Test::Failed => 0 ];
5766 $ this ->time = -microtime (as_float: true );
67+ if ($ this ->mode === self ::ModeCider && $ this ->runner ->threadCount < 2 ) {
68+ $ this ->mode = self ::ModeLines;
69+ }
5870 fwrite ($ this ->file , $ this ->runner ->getInterpreter ()->getShortInfo ()
5971 . ' | ' . $ this ->runner ->getInterpreter ()->getCommandLine ()
6072 . " | {$ this ->runner ->threadCount } thread " . ($ this ->runner ->threadCount > 1 ? 's ' : '' ) . "\n\n" );
@@ -91,7 +103,7 @@ public function finish(Test $test): void
91103 $ this ->results [$ result ]++;
92104 fwrite ($ this ->file , match ($ this ->mode ) {
93105 self ::ModeDots => [Test::Passed => '. ' , Test::Skipped => 's ' , Test::Failed => Ansi::colorize ('F ' , 'white/red ' )][$ result ],
94- self ::ModeCider => [Test::Passed => ' 🍏 ' , Test::Skipped => ' s ' , Test::Failed => ' 🍎 ' ][ $ result ] ,
106+ self ::ModeCider => '' ,
95107 self ::ModeLines => $ this ->generateFinishLine ($ test ),
96108 });
97109
@@ -108,6 +120,12 @@ public function finish(Test $test): void
108120
109121 public function end (): void
110122 {
123+ if ($ this ->panelHeight ) {
124+ fwrite ($ this ->file , Ansi::cursorMove (y: -$ this ->panelHeight )
125+ . str_repeat (Ansi::clearLine () . "\n" , $ this ->panelHeight )
126+ . Ansi::cursorMove (y: -$ this ->panelHeight ));
127+ }
128+
111129 $ run = array_sum ($ this ->results );
112130 fwrite ($ this ->file , !$ this ->count ? "No tests found \n" :
113131 "\n\n" . $ this ->buffer . "\n"
@@ -160,4 +178,76 @@ private function generateFinishLine(Test $test): string
160178 $ message ,
161179 );
162180 }
181+
182+
183+ public function jobStarted (Job $ job ): void
184+ {
185+ $ this ->startTimes [$ job ] = microtime (true );
186+ }
187+
188+
189+ /**
190+ * @param Job[] $running
191+ */
192+ public function tick (array $ running ): void
193+ {
194+ if ($ this ->mode !== self ::ModeCider) {
195+ return ;
196+ }
197+
198+ // Move cursor up to overwrite previous output
199+ if ($ this ->panelHeight ) {
200+ fwrite ($ this ->file , Ansi::cursorMove (y: -$ this ->panelHeight ));
201+ }
202+
203+ $ lines = [];
204+
205+ // Header with progress bar
206+ $ barWidth = $ this ->panelWidth - 12 ;
207+ $ filled = (int ) round ($ barWidth * ($ this ->runner ->getFinishedCount () / $ this ->runner ->getJobCount ()));
208+ $ lines [] = '╭ ' . Ansi::pad (' ' . str_repeat ('█ ' , $ filled ) . str_repeat ('░ ' , $ barWidth - $ filled ) . ' ' , $ this ->panelWidth - 2 , '─ ' , STR_PAD_BOTH ) . '╮ ' ;
209+
210+ $ threadJobs = [];
211+ foreach ($ running as $ job ) {
212+ $ threadJobs [(int ) $ job ->getEnvironmentVariable (Environment::VariableThread)] = $ job ;
213+ }
214+
215+ // Thread lines
216+ $ numWidth = strlen ((string ) $ this ->runner ->threadCount );
217+ $ displayCount = min ($ this ->runner ->threadCount , self ::MaxDisplayedThreads);
218+
219+ for ($ t = 1 ; $ t <= $ displayCount ; $ t ++) {
220+ if (isset ($ threadJobs [$ t ])) {
221+ $ job = $ threadJobs [$ t ];
222+ $ name = basename ($ job ->getTest ()->getFile ());
223+ $ time = sprintf ('%0.1fs ' , microtime (true ) - ($ this ->startTimes [$ job ] ?? microtime (true )));
224+ $ nameWidth = $ this ->panelWidth - $ numWidth - strlen ($ time ) - 7 ;
225+ $ name = Ansi::pad (Ansi::truncate ($ name , $ nameWidth ), $ nameWidth );
226+ $ line = Ansi::colorize (sprintf ("% {$ numWidth }d: " , $ t ), 'lime ' ) . " $ name " . Ansi::colorize ($ time , 'yellow ' );
227+ } else {
228+ $ line = Ansi::pad (Ansi::colorize (sprintf ("% {$ numWidth }d: - " , $ t ), 'gray ' ), $ this ->panelWidth - 4 );
229+ }
230+ $ lines [] = '│ ' . $ line . ' │ ' ;
231+ }
232+
233+ if ($ this ->runner ->threadCount > self ::MaxDisplayedThreads) {
234+ $ more = $ this ->runner ->threadCount - self ::MaxDisplayedThreads;
235+ $ ellipsis = Ansi::colorize ("… and $ more more " , 'gray ' );
236+ $ lines [] = '│ ' . Ansi::pad ($ ellipsis , $ this ->panelWidth - 2 ) . '│ ' ;
237+ }
238+
239+ // Footer: (85 tests, 🍏×74 🍎×2, 9.0s)
240+ $ summary = "( $ this ->count tests, "
241+ . ($ this ->results [Test::Passed] ? "🍏× {$ this ->results [Test::Passed]}" : '' )
242+ . ($ this ->results [Test::Failed] ? " 🍎× {$ this ->results [Test::Failed]}" : '' )
243+ . ', ' . sprintf ('%0.1fs ' , $ this ->time + microtime (true )) . ') ' ;
244+ $ lines [] = '╰ ' . Ansi::pad ($ summary , $ this ->panelWidth - 2 , '─ ' , STR_PAD_BOTH ) . '╯ ' ;
245+
246+ foreach ($ lines as $ line ) {
247+ fwrite ($ this ->file , "\r" . $ line . Ansi::clearLine () . "\n" );
248+ }
249+ fflush ($ this ->file );
250+
251+ $ this ->panelHeight = count ($ lines );
252+ }
163253}
0 commit comments