5252 * testbox run outputformats=json,antjunit,simple
5353 * testbox run outputformats=json,antjunit,simple outputFile=myresults
5454 * {code}
55+ * .
56+ * You can stream test results in real-time for immediate feedback during test execution
57+ * {code:bash}
58+ * testbox run --streaming
59+ * {code}
5560 *
5661 **/
5762component extends = " testboxCLI.models.BaseCommand" {
5863
5964 // DI
60- property name = " testingService" inject = " TestingService@testbox-cli" ;
61- property name = " CLIRenderer" inject = " CLIRenderer@testbox-cli" ;
65+ property name = " testingService" inject = " TestingService@testbox-cli" ;
66+ property name = " CLIRenderer" inject = " CLIRenderer@testbox-cli" ;
67+ property name = " SSEClient" inject = " SSEClient@testbox-cli" ;
68+ property name = " StreamingRenderer" inject = " StreamingRenderer@testbox-cli" ;
6269
6370 // Default Runner Options
6471 variables .RUNNER_OPTIONS = {
@@ -91,6 +98,7 @@ component extends="testboxCLI.models.BaseCommand" {
9198 * @outputFile We will store the results in this output file as well as presenting it to you.
9299 * @outputFormats A list of output reporter to produce using the runner's JSON results only. Available formats are: json,xml,junit,antjunit,simple,dot,doc,min,mintext,doc,text,tap,codexwiki
93100 * @verbose Display extra details including passing and skipped tests.
101+ * @streaming Stream test results in real-time via Server-Sent Events (SSE) for immediate feedback during test execution.
94102 * @testboxUseLocal When using outputformats, prefer testbox installation in current working directory over bundled version. If none found, it tries to download one
95103 **/
96104 function run (
@@ -108,6 +116,7 @@ component extends="testboxCLI.models.BaseCommand" {
108116 string outputFile ,
109117 string outputFormats = " " ,
110118 boolean verbose ,
119+ boolean streaming = false ,
111120 boolean testboxUseLocal = true
112121 ){
113122 // Remove /\ to . in bundles
@@ -124,6 +133,12 @@ component extends="testboxCLI.models.BaseCommand" {
124133 // Incorporate runner options
125134 arguments .testboxUrl = addRunnerOptions ( argumentCollection = arguments );
126135
136+ // If streaming mode, use SSE client
137+ if ( arguments .streaming ) {
138+ runStreaming ( argumentCollection = arguments );
139+ return ;
140+ }
141+
127142 // Advise we are running
128143 print .boldGreenLine ( " Executing tests #testboxUrl # please wait..." ).toConsole ();
129144
@@ -374,4 +389,112 @@ component extends="testboxCLI.models.BaseCommand" {
374389 }
375390 }
376391
392+ /**
393+ * Run tests in streaming mode using Server-Sent Events (SSE)
394+ * This provides real-time feedback as tests execute
395+ */
396+ private function runStreaming (){
397+ // Add streaming=true to the URL
398+ var streamingUrl = arguments .testboxUrl & " &streaming=true" ;
399+
400+ // Get verbose setting
401+ var boxOptions = packageService .readPackageDescriptor ( getCWD () ).testbox ;
402+ var isVerbose = arguments .verbose ?: boxOptions .verbose ?: false ;
403+
404+ // Advise we are running in streaming mode
405+ print
406+ .boldCyanLine ( " Executing tests in streaming mode..." )
407+ .line ()
408+ .toConsole ();
409+
410+ // Create event handlers for streaming output
411+ var eventHandlers = StreamingRenderer .createEventHandlers ( print , isVerbose );
412+
413+ // Track if tests failed for exit code
414+ var testsFailed = false ;
415+ var streamingError = false ;
416+
417+ // Override testRunEnd to capture failure state
418+ var originalTestRunEnd = eventHandlers .testRunEnd ;
419+ eventHandlers .testRunEnd = function ( data ){
420+ // Check for failures in the full results
421+ if (
422+ structKeyExists ( data , " results" ) && (
423+ ( data .results .totalFail ?: 0 ) > 0 ||
424+ ( data .results .totalError ?: 0 ) > 0
425+ )
426+ ) {
427+ testsFailed = true ;
428+ } else if ( ( data .totalFail ?: 0 ) > 0 || ( data .totalError ?: 0 ) > 0 ) {
429+ testsFailed = true ;
430+ }
431+ // Call original handler
432+ originalTestRunEnd ( data );
433+ };
434+
435+ // Consume the SSE stream
436+ var finalResults = {};
437+ try {
438+ finalResults = SSEClient .consumeStream (
439+ url = streamingUrl ,
440+ eventHandlers = eventHandlers ,
441+ onError = function ( error ){
442+ // Mark streaming as failed for exit code
443+ streamingError = true ;
444+ print .boldRedLine ( " Streaming error: #error .message #" ).toConsole ();
445+ if ( structKeyExists ( error , " detail" ) && len ( error .detail ) ) {
446+ print .redLine ( error .detail ).toConsole ();
447+ }
448+ }
449+ );
450+ } catch ( any e ) {
451+ logger .error (
452+ " Error during streaming: #e .message # #e .detail #" ,
453+ e
454+ );
455+ return error ( " Error executing streaming tests: #CR # #e .message ##CR ##e .detail #" );
456+ }
457+
458+ // Set exit code based on results or streaming errors
459+ if ( testsFailed || streamingError ) {
460+ setExitCode ( 1 );
461+ }
462+
463+ // Render final summary using CLIRenderer if we have full results
464+ if ( ! structIsEmpty ( finalResults ) ) {
465+ print .line ();
466+ CLIRenderer .render ( print , finalResults , isVerbose );
467+ }
468+
469+ // Handle output formats if specified
470+ if ( len ( arguments .outputFormats ) && ! structIsEmpty ( finalResults ) ) {
471+ print
472+ .line ()
473+ .blueLine ( " Output formats detected (#arguments .outputFormats #), building out reports..." )
474+ .toConsole ();
475+
476+ buildOutputFormats (
477+ arguments .outputFile ?: " test-results" ,
478+ arguments .outputFormats ,
479+ serializeJSON ( finalResults )
480+ );
481+ }
482+
483+ // Handle legacy output file
484+ if ( ! isNull ( arguments .outputFile ) && ! len ( arguments .outputFormats ) && ! structIsEmpty ( finalResults ) ) {
485+ arguments .outputFile = resolvePath ( arguments .outputFile );
486+
487+ var this Dir = getDirectoryFromPath ( arguments .outputFile );
488+ if ( ! directoryExists ( this Dir ) ) {
489+ directoryCreate ( this Dir );
490+ }
491+
492+ fileWrite (
493+ arguments .outputFile ,
494+ formatterUtil .formatJSON ( serializeJSON ( finalResults ) )
495+ );
496+ print .boldGreenLine ( " ===> JSON Report written to #arguments .outputFile #!" );
497+ }
498+ }
499+
377500}
0 commit comments