@@ -39,6 +39,8 @@ static size_t status_win = 0;
3939
4040static volatile long g_start = 0 ;
4141static volatile long g_abort = 0 ;
42+ static volatile long g_run_one = -1 ; // Index of a single stage to run
43+ static volatile long g_busy = 0 ; // A stage is running right now
4244static volatile long g_finished = 0 ; // 1 = all passed, 2 = a stage failed
4345static volatile long g_ready = 0 ;
4446static volatile long g_report = -1 ;
@@ -161,9 +163,19 @@ static void mark_passed(size_t w, const char* label) {
161163
162164static void cb_start (webui_event_t * e ) {
163165 (void )e ;
166+ if (g_busy )
167+ return ;
164168 g_start = 1 ;
165169}
166170
171+ static void cb_run_stage (webui_event_t * e ) {
172+ if (g_busy )
173+ return ;
174+ long idx = (long )webui_get_int (e );
175+ if (idx >= 0 && idx < STAGES )
176+ g_run_one = idx ;
177+ }
178+
167179static void cb_exit (webui_event_t * e ) {
168180 (void )e ;
169181 g_abort = 1 ;
@@ -513,6 +525,11 @@ static bool stage_port_reload(int i) {
513525 snprintf (detail , sizeof (detail ), "First show failed" );
514526 ok = false;
515527 }
528+ char old_url [256 ] = {0 };
529+ if (ok ) {
530+ const char * u = webui_get_url (w );
531+ snprintf (old_url , sizeof (old_url ), "%s" , (u != NULL ? u : "" ));
532+ }
516533 size_t new_port = 0 ;
517534 if (ok ) {
518535 for (size_t p = 33000 ; p < 33100 ; p ++ ) {
@@ -531,21 +548,25 @@ static bool stage_port_reload(int i) {
531548 ok = false;
532549 }
533550 if (ok ) {
534- sleep_ms (1500 );
535- g_ready = 0 ;
536- if (!webui_show (w , make_page ("Port reload" , "ready(function(){webui.call('stage_ready',1);});" )) ||
537- !wait_long (& g_ready , 1 , 15000 )) {
538- snprintf (detail , sizeof (detail ), "Re-show on the new port failed" );
539- ok = false;
540- }
541- }
542- if (ok ) {
551+ // This blocks until the pending reload is done,
552+ // so the returned URL is the one of the new server
543553 const char * url = webui_get_url (w );
544554 char expect [32 ];
545555 snprintf (expect , sizeof (expect ), ":%zu" , new_port );
546556 if (url == NULL || strstr (url , expect ) == NULL ) {
547557 snprintf (detail , sizeof (detail ), "URL [%s] is not on port %zu" , (url != NULL ? url : "?" ), new_port );
548558 ok = false;
559+ } else if (strcmp (old_url , url ) == 0 ) {
560+ snprintf (detail , sizeof (detail ), "URL did not change after the reload" );
561+ ok = false;
562+ }
563+ }
564+ if (ok ) {
565+ g_ready = 0 ;
566+ if (!webui_show (w , make_page ("Port reload" , "ready(function(){webui.call('stage_ready',1);});" )) ||
567+ !wait_long (& g_ready , 1 , 15000 )) {
568+ snprintf (detail , sizeof (detail ), "Re-show on the new port failed" );
569+ ok = false;
549570 }
550571 }
551572 if (ok )
@@ -702,12 +723,27 @@ static bool stage_destroy_from_callback(int i) {
702723 bool ok = true;
703724 char detail [256 ] = "webui_destroy() called inside its own callback" ;
704725 size_t w = webui_new_window ();
726+ webui_bind (w , "stage_ready" , cb_ready );
705727 webui_bind (w , "kamikaze" , cb_kamikaze );
728+ g_ready = 0 ;
706729 g_kamikaze = 0 ;
707- if (!webui_show (w , make_page ("Destroy from callback" , "ready(function(){webui.call('kamikaze');});" ))) {
730+ // The page only reports that it is connected here. Destroying the
731+ // window from the connect callback itself would race `webui_show()`,
732+ // which returns the connection status: the window would already be
733+ // gone by the time it returns, and the show would look failed.
734+ if (!webui_show (w , make_page ("Destroy from callback" , "ready(function(){webui.call('stage_ready',1);});" ))) {
708735 snprintf (detail , sizeof (detail ), "Show failed" );
709736 ok = false;
710737 }
738+ if (ok && !wait_long (& g_ready , 1 , 15000 )) {
739+ snprintf (detail , sizeof (detail ), "No page signal" );
740+ ok = false;
741+ }
742+ if (ok ) {
743+ // Now let the page call the backend function
744+ // that destroys this very window
745+ webui_run (w , "webui.call('kamikaze');" );
746+ }
711747 if (ok && !wait_long (& g_kamikaze , 1 , 15000 )) {
712748 snprintf (detail , sizeof (detail ), "Callback never ran" );
713749 ok = false;
@@ -784,6 +820,60 @@ static const stage_fn stage_fns[STAGES] = {
784820 stage_rapid_open_close
785821};
786822
823+ // Run every stage in order, and stop at the first failure. Only a full
824+ // run writes the result files, they tell if the whole suite passed.
825+ static void run_all_stages (void ) {
826+
827+ remove ("test_pass.txt" );
828+ remove ("test_error.txt" );
829+ status_run ("resetStages();" );
830+ status_run ("setBanner('Running all stages...','');" );
831+
832+ int i ;
833+ bool all = true;
834+ for (i = 0 ; i < STAGES && all && !g_abort ; i ++ )
835+ all = stage_fns [i ](i );
836+
837+ if (g_abort )
838+ return ;
839+
840+ if (all ) {
841+ g_finished = 1 ;
842+ FILE * f = fopen ("test_pass.txt" , "w" );
843+ if (f != NULL )
844+ fclose (f );
845+ printf ("ALL %d STAGES PASSED\n" , STAGES );
846+ status_run ("setBanner('ALL %d STAGES PASSED - every green window is still connected','pass');" , STAGES );
847+ } else {
848+ g_finished = 2 ;
849+ for (int k = i ; k < STAGES ; k ++ )
850+ status_run ("setStage(%d,'SKIP','Stopped after a failure');" , k );
851+ FILE * f = fopen ("test_error.txt" , "w" );
852+ if (f != NULL )
853+ fclose (f );
854+ printf ("STOPPED: stage %d failed\n" , i );
855+ status_run ("setBanner('STOPPED - stage %d failed','fail');" , i );
856+ }
857+ fflush (stdout );
858+ }
859+
860+ // Run one stage alone, for a user who wants to test a single case.
861+ // This does not touch the result files.
862+ static void run_single_stage (int idx ) {
863+
864+ status_run ("setBanner('Running stage %d alone...','');" , idx + 1 );
865+
866+ bool ok = stage_fns [idx ](idx );
867+
868+ if (g_abort )
869+ return ;
870+
871+ printf ("SINGLE STAGE %d -> %s\n" , idx + 1 , ok ? "PASS" : "FAIL" );
872+ fflush (stdout );
873+ status_run ("setBanner('Stage %d %s (single run, result files unchanged)','%s');" ,
874+ idx + 1 , ok ? "passed" : "failed" , ok ? "pass" : "fail" );
875+ }
876+
787877#ifdef _WIN32
788878static DWORD WINAPI driver (LPVOID arg )
789879#else
@@ -792,38 +882,27 @@ static void* driver(void* arg)
792882{
793883 (void )arg ;
794884
795- // Wait for the Start click, or start right away in auto mode
796- while (!g_start && !g_abort )
797- sleep_ms (50 );
798- if (!g_abort ) {
799-
800- status_run ("document.getElementById('bstart').disabled=true;" );
801- status_run ("setBanner('Running...','');" );
802-
803- int i ;
804- bool all = true;
805- for (i = 0 ; i < STAGES && all && !g_abort ; i ++ )
806- all = stage_fns [i ](i );
807-
808- if (!g_abort ) {
809- if (all ) {
810- g_finished = 1 ;
811- FILE * f = fopen ("test_pass.txt" , "w" );
812- if (f != NULL )
813- fclose (f );
814- printf ("ALL %d STAGES PASSED\n" , STAGES );
815- status_run ("setBanner('ALL %d STAGES PASSED - every green window is still connected','pass');" , STAGES );
816- } else {
817- g_finished = 2 ;
818- for (int k = i ; k < STAGES ; k ++ )
819- status_run ("setStage(%d,'SKIP','Stopped after a failure');" , k );
820- FILE * f = fopen ("test_error.txt" , "w" );
821- if (f != NULL )
822- fclose (f );
823- printf ("STOPPED: stage %d failed\n" , i );
824- status_run ("setBanner('STOPPED - stage %d failed','fail');" , i );
825- }
826- fflush (stdout );
885+ // Serve run requests until the app exits. A request comes from
886+ // the Start button, from a stage Run button, or from auto mode.
887+ while (!g_abort ) {
888+
889+ if (g_start ) {
890+ g_start = 0 ;
891+ g_busy = 1 ;
892+ run_all_stages ();
893+ g_busy = 0 ;
894+ status_run ("setBusy(false);" );
895+ }
896+ else if (g_run_one >= 0 ) {
897+ int idx = (int )g_run_one ;
898+ g_run_one = -1 ;
899+ g_busy = 1 ;
900+ run_single_stage (idx );
901+ g_busy = 0 ;
902+ status_run ("setBusy(false);" );
903+ }
904+ else {
905+ sleep_ms (50 );
827906 }
828907 }
829908
@@ -842,6 +921,8 @@ static const char* status_page =
842921 "border-radius:6px;cursor:pointer;background:#3d63dd;color:#fff}"
843922 "button:disabled{background:#2b3050;color:#8a92b2;cursor:default}"
844923 "#bexit{background:#5a2530}"
924+ "button.run{font-size:12px;font-weight:600;padding:4px 14px;margin:0;background:#2f3c6b}"
925+ "button.run:hover:enabled{background:#3d63dd}"
845926 "#banner{padding:10px 14px;border-radius:8px;background:#2b3050;margin:14px 0;font-weight:600}"
846927 "#banner.pass{background:#12391f;color:#7ce38b}"
847928 "#banner.fail{background:#46151a;color:#ff8089}"
@@ -853,11 +934,11 @@ static const char* status_page =
853934 "</style></head><body>"
854935 "<h1>WebUI Stress Test</h1>"
855936 "<div>"
856- "<button id=\"bstart\" onclick=\"this.disabled= true;webui.call('start_test');\">Start</button>"
937+ "<button id=\"bstart\" onclick=\"setBusy( true) ;webui.call('start_test');\">Start</button>"
857938 "<button id=\"bexit\" onclick=\"webui.call('app_exit');\">Exit</button>"
858939 "</div>"
859- "<div id=\"banner\">Ready. Click Start to run the test</div>"
860- "<table id=\"tbl\"><tr><th>#</th><th>Stage</th><th class=\"st\">Status</th><th>Details</th></tr></table>"
940+ "<div id=\"banner\">Ready. Click Start to run every stage, or Run to test a single stage </div>"
941+ "<table id=\"tbl\"><tr><th>#</th><th>Stage</th><th class=\"st\">Status</th><th>Details</th><th></th>< /tr></table>"
861942 "<script>"
862943 "const names=['Dashboard connection','Open / close / reopen window',"
863944 "'C to JS calls (webui_run / webui_script)','JS to C calls and arguments',"
@@ -867,12 +948,18 @@ static const char* status_page =
867948 "const tbl=document.getElementById('tbl');"
868949 "names.forEach((n,i)=>{const r=tbl.insertRow(-1);r.insertCell(0).textContent=i+1;"
869950 "r.insertCell(1).textContent=n;const s=r.insertCell(2);s.className='st';s.id='st'+i;"
870- "s.textContent='PENDING';const d=r.insertCell(3);d.className='dt';d.id='dt'+i;});"
951+ "s.textContent='PENDING';const d=r.insertCell(3);d.className='dt';d.id='dt'+i;"
952+ "const a=r.insertCell(4);const b=document.createElement('button');b.className='run';"
953+ "b.textContent='Run';b.onclick=function(){setBusy(true);webui.call('run_stage',i);};"
954+ "a.appendChild(b);});"
871955 "function setStage(i,st,dt){const s=document.getElementById('st'+i);s.textContent=st;"
872- "s.className='st '+(st=='PASS'?'pass':st=='FAIL'?'fail':st=='SKIP'?'skip':'run');"
873- "if(dt)document.getElementById('dt'+i).textContent=dt;}"
956+ "s.className='st '+(st=='PASS'?'pass':st=='FAIL'?'fail':st=='SKIP'?'skip':st=='PENDING'?'': 'run');"
957+ "if(dt!==undefined )document.getElementById('dt'+i).textContent=dt;}"
874958 "function setBanner(t,cls){const b=document.getElementById('banner');"
875959 "b.textContent=t;b.className=cls||'';}"
960+ "function setBusy(b){document.getElementById('bstart').disabled=b;"
961+ "document.querySelectorAll('button.run').forEach(x=>{x.disabled=b;});}"
962+ "function resetStages(){for(let i=0;i<names.length;i++)setStage(i,'PENDING','');}"
876963 "</script></body></html>" ;
877964
878965int main (int argc , char * argv []) {
@@ -887,6 +974,7 @@ int main(int argc, char* argv[]) {
887974
888975 status_win = webui_new_window ();
889976 webui_bind (status_win , "start_test" , cb_start );
977+ webui_bind (status_win , "run_stage" , cb_run_stage );
890978 webui_bind (status_win , "app_exit" , cb_exit );
891979 if (!webui_show (status_win , status_page )) {
892980 printf ("Could not open the status window\n" );
0 commit comments