use os; use file; include "testutil"; const C_RED := "\x1b[31m"; const C_GREEN := "\x1b[32m"; const C_YELLOW := "\x1b[33m"; const C_CYAN := "\x1b[36m"; const C_MAGENTA_LIGHT := "\x1b[95m"; const C_RESET := "\x1b[0m"; const CONTENT_LENGTH_HEADER_KEY := "Content-Length: "; const CONTENT_LENGTH_HEADER_KEY_LENGTH := 16; const CONTENT_LENGTH_HEADER_VALUE_START := CONTENT_LENGTH_HEADER_KEY_LENGTH + 1; // List of events var dapEvents := {}; // Maps a request sequence to a response object var dapResponses := dictionary{}; // Global aux script connection var connection; // Next request sequence number var nextSequence := 1; var recvBuffer := ""; program auxservice( conn, params ) connection := conn; var result; var test_script_pid := params.test_script_pid; var test_type := params.test_type; if ( test_type == "attach" ) result := run_attach_test( params.debuggee_pid ); elseif ( test_type == "launch" ) result := run_launch_test(); else result := ret_error( $"Unknown debugger test type '{test_type}'" ); endif GetProcess( test_script_pid ).SendEvent( struct{ result := result } ); connection := 0; endprogram function run_launch_test() // Get script source var source := ReadFile( "debuggee.src" ); if ( !source ) return ret_error( source ); endif var print_linenum; foreach line in source if ( line.find( "/* print */" ) ) print_linenum := _line_iter; endif endforeach var err; // Send request: initialize if ( !request_succeeded( err, "initialize", struct{ adapterID := "pol", password := "debugme" } ) ) return err; endif // Wait for event: initialized if ( !event_received( err, "initialized" ) ) return err; endif // Send request: launch // Note: Sets the program's parameter as "stop" if ( !request_succeeded( err, "launch", struct{ "script" := ":testdebugger:debuggee", arg := "S4:stop" /* packed */ } ) ) return err; endif // Wait for event: stopped if ( !event_received( err, "stopped" ) ) return err; endif // Send request: threads // Note: Since we only have one thread that never changes, test sends this message only once. var threads; if ( !( threads := request_succeeded( err, "threads" ) ) ) return err; endif var threadId := threads.threads[1].id; // Send request: stackTrace var stackTrace; if ( !( stackTrace := request_succeeded( err, "stackTrace", struct{ threadId := threadId } ) ) ) return err; endif // Send request: scopes var scopes; if ( !( scopes := request_succeeded( err, "scopes", struct{ frameId := stackTrace.stackFrames[1].id } ) ) ) return err; endif var evaluate; if ( !( evaluate := request_succeeded( err, "evaluate", struct{ expression := "running", frameId := scopes.scopes[1].variablesReference } ) ) ) return err; endif if ( evaluate.result != "\"stop\"" ) return ret_error( $"Program argument from launch request `running` does not equal '\"stop\"': '{evaluate.result}' != '\"stop\"'" ); endif // Send request: continue if ( !request_succeeded( err, "continue", struct{ threadId := threadId } ) ) return err; endif // Wait for event: output var output; if ( !( output := event_received( err, "output" ) ) ) return err; endif var expected := struct{ category := "stdout", output := "Loop finished!\n", line := print_linenum, source := struct{ path := stackTrace.stackFrames[1].source.path } }; if ( output.category != expected.category || output.output != expected.output || output.line != expected.line || output.source.path != expected.source.path ) return ret_error( $"Incorrect output event. Expected: {expected}, actual {output}" ); endif // Wait for event: exited if ( !event_received( err, "exited" ) ) return err; endif return 1; endfunction function run_attach_test( debuggee_pid ) var err; // Send request: initialize if ( !request_succeeded( err, "initialize", struct{ adapterID := "pol", password := "debugme" } ) ) return err; endif // Wait for event: initialized if ( !event_received( err, "initialized" ) ) return err; endif // Send request: processes var processes; if ( !( processes := request_succeeded( err, "processes", struct{ filter := "debuggee" } ) ) ) return err; endif if ( processes.processes[1].id != debuggee_pid ) return ret_error( $"Process id does not match! Expected {processes.processes[1].id} == {debuggee_pid}" ); endif // Send request: attach if ( !request_succeeded( err, "attach", struct{ pid := debuggee_pid } ) ) return err; endif // Wait for event: stopped if ( !event_received( err, "stopped" ) ) return err; endif // Send request: threads // Note: Since we only have one thread that never changes, test sends this message only once. var threads; if ( !( threads := request_succeeded( err, "threads" ) ) ) return err; endif var threadId := threads.threads[1].id; // Send request: stackTrace var stackTrace; if ( !( stackTrace := request_succeeded( err, "stackTrace", struct{ threadId := threadId } ) ) ) return err; endif // Send request: scopes var scopes; if ( !( scopes := request_succeeded( err, "scopes", struct{ frameId := stackTrace.stackFrames[1].id } ) ) ) return err; endif var scope_varibles := array{}; foreach scope in ( scopes.scopes ) var variables; if ( !( variables := request_succeeded( err, "variables", struct{ variablesReference := scope.variablesReference } ) ) ) return err; endif foreach variable in ( variables.variables ) var evaluate; if ( !( evaluate := request_succeeded( err, "evaluate", struct{ expression := variable.name, frameId := scope.variablesReference } ) ) ) return err; endif if ( variable.value != evaluate.result ) return ret_error( $"Variables value does not match evaluate result: '{variable.value}' != '{evaluate.result}'" ); endif if ( variable.variablesReference ) var inner_variables; if ( !( inner_variables := request_succeeded( err, "variables", struct{ variablesReference := variable.variablesReference } ) ) ) return err; endif endif endforeach endforeach // Get script source var source := ReadFile( "debuggee.src" ); if ( !source ) return ret_error( source ); endif var setting_breakpoints := array{}; foreach line in source if ( line.find( "/* set_bp */" ) ) setting_breakpoints.append( struct{ line := _line_iter } ); endif endforeach // Send request: setBreakpoints if ( !request_succeeded( err, "setBreakpoints", struct{ source := struct{ path := stackTrace.stackFrames[1].source.path }, breakpoints := setting_breakpoints } ) ) return err; endif // Send request: continue if ( !request_succeeded( err, "continue", struct{ threadId := threadId } ) ) return err; endif // Wait for event: stopped if ( !event_received( err, "stopped" ) ) return err; endif // Send request: stackTrace if ( !( stackTrace := request_succeeded( err, "stackTrace", struct{ threadId := threadId } ) ) ) return err; endif if ( stackTrace.stackFrames[1].name != "func2" ) return ret_error( $"Expected to be in `func2` after first continue: '{stackTrace.stackFrames[1].name}' != 'func2'" ); endif // Send request: stepIn if ( !request_succeeded( err, "stepIn", struct{ threadId := threadId } ) ) return err; endif // Wait for event: stopped if ( !event_received( err, "stopped" ) ) return err; endif // Send request: stackTrace if ( !( stackTrace := request_succeeded( err, "stackTrace", struct{ threadId := threadId } ) ) ) return err; endif if ( stackTrace.stackFrames[1].name != "func3" ) return ret_error( $"Expected to be in `func3` after first stepIn: '{stackTrace.stackFrames[1].name}' != 'func3'" ); endif if ( stackTrace.stackFrames[2].name != "func2" ) return ret_error( $"Expected previous frame to be in `func2` after first stepIn: '{stackTrace.stackFrames[2].name}' != 'func2'" ); endif // Send request: continue if ( !request_succeeded( err, "continue", struct{ threadId := threadId } ) ) return err; endif // Wait for event: stopped if ( !event_received( err, "stopped" ) ) return err; endif // Send request: stackTrace if ( !( stackTrace := request_succeeded( err, "stackTrace", struct{ threadId := threadId } ) ) ) return err; endif if ( stackTrace.stackFrames[1].name != "funcShadowed" ) return ret_error( $"Expected to be in `funcShadowed` after second continue: '{stackTrace.stackFrames[1].name}' != 'funcShadowed'" ); endif var expectedNextLine := stackTrace.stackFrames[1].line + 1; // Send request: evaluate var evaluate; if ( !( evaluate := request_succeeded( err, "evaluate", struct{ expression := "shadowed_variable", frameId := stackTrace.stackFrames[1].id } ) ) ) return err; endif if ( evaluate.result != "\"funcShadowed:param1\"" ) return ret_error( $"Expected value of `shadowed_variable` to be '\"funcShadowed:param1\"': '{evaluate.result}' != '\"funcShadowed:param1\"'" ); endif // Send request: next if ( !request_succeeded( err, "next", struct{ threadId := threadId } ) ) return err; endif // Wait for event: stopped if ( !event_received( err, "stopped" ) ) return err; endif // Send request: stackTrace if ( !( stackTrace := request_succeeded( err, "stackTrace", struct{ threadId := threadId } ) ) ) return err; endif if ( stackTrace.stackFrames[1].line != expectedNextLine ) return ret_error( $"Expected to be on line {expectedNextLine} after next: {stackTrace.stackFrames[1].line} != ${expectedNextLine}" ); endif // Send request: continue if ( !request_succeeded( err, "continue", struct{ threadId := threadId } ) ) return err; endif // Send request: pause if ( !request_succeeded( err, "pause", struct{ threadId := threadId } ) ) return err; endif // Wait for event: stopped if ( !event_received( err, "stopped" ) ) return err; endif // Send request: stackTrace if ( !( stackTrace := request_succeeded( err, "stackTrace", struct{ threadId := threadId } ) ) ) return err; endif if ( stackTrace.stackFrames[1].name != "funcShadowed" ) return ret_error( $"Expected to be in `funcShadowed` after third continue: '{stackTrace.stackFrames[1].name}' != 'funcShadowed'" ); endif // Send request: scopes if ( !( scopes := request_succeeded( err, "scopes", struct{ frameId := stackTrace.stackFrames[1].id } ) ) ) return err; endif // Send request: setVariable [local] var setVariable; if ( !( setVariable := request_succeeded( err, "setVariable", struct{ name := "was_paused", value := "1", variablesReference := scopes.scopes[1].variablesReference } ) ) ) return err; endif if ( setVariable.type != "number" || setVariable.value != "1" ) return ret_error( $"Expected setVariable for `was_paused` to be type = number, value = '1': {setVariable.type} != 'number' || {setVariable.value} != '1'" ); endif // Send request: stepOut if ( !request_succeeded( err, "stepOut", struct{ threadId := threadId } ) ) return err; endif // Wait for event: stopped if ( !event_received( err, "stopped" ) ) return err; endif // Send request: stackTrace if ( !( stackTrace := request_succeeded( err, "stackTrace", struct{ threadId := threadId } ) ) ) return err; endif if ( stackTrace.stackFrames[1].name != "(program)" ) return ret_error( $"Expected to be in `(program)` after stepOut: '{stackTrace.stackFrames[1].name}' != '(program)'" ); endif // Send request: next if ( !request_succeeded( err, "next", struct{ threadId := threadId } ) ) return err; endif // Wait for event: stopped if ( !event_received( err, "stopped" ) ) return err; endif // Send request: stackTrace if ( !( stackTrace := request_succeeded( err, "stackTrace", struct{ threadId := threadId } ) ) ) return err; endif if ( stackTrace.stackFrames[1].name != "(program)" ) return ret_error( $"Expected to be in `(program)` after stepOut: '{stackTrace.stackFrames[1].name}' != '(program)'" ); endif // Send request: evaluate if ( !( evaluate := request_succeeded( err, "evaluate", struct{ expression := "shadowed_result", frameId := stackTrace.stackFrames[1].id } ) ) ) return err; endif if ( evaluate.result != "1" ) return ret_error( $"Expected value of `shadowed_result` to be '1': '{evaluate.result}' != '1'" ); endif // Send request: scopes if ( !( scopes := request_succeeded( err, "scopes", struct{ frameId := stackTrace.stackFrames[1].id } ) ) ) return err; endif // Send request: setVariable [global] if ( !( setVariable := request_succeeded( err, "setVariable", struct{ name := "global_float", value := "1.5", variablesReference := scopes.scopes[Len( scopes.scopes )].variablesReference } ) ) ) return err; endif // Send request: evaluate if ( !( evaluate := request_succeeded( err, "evaluate", struct{ expression := "global_float", frameId := stackTrace.stackFrames[Len( stackTrace.stackFrames )].id } ) ) ) return err; endif if ( evaluate.result != "1.5" ) return ret_error( $"Expected value of `global_float` to be '1.5': '{evaluate.result}' != '1.5'" ); endif return 1; endfunction // Test helper methods function fill_buffers( timeout, request_seq := -1, event := "" ) var ev; var found := 0; outer: while ( ev := wait_for_event( timeout ) ) if ( !ev && timeout > 0 ) return error{ errortext := "Error filling buffer" }; endif recvBuffer += ev.value; if ( recvBuffer.length() < CONTENT_LENGTH_HEADER_KEY_LENGTH ) continue outer; elseif ( recvBuffer[1, CONTENT_LENGTH_HEADER_KEY_LENGTH] != CONTENT_LENGTH_HEADER_KEY ) return error{ errortext := "Invalid header in buffer" }; else var previousBufferLength := recvBuffer.length(); while ( recvBuffer ) var newlineIndex := recvBuffer.find( "\n", 3 ); if ( newlineIndex == 0 || recvBuffer == "" ) continue outer; endif var content_length := CInt( recvBuffer[CONTENT_LENGTH_HEADER_VALUE_START, newlineIndex] ); if ( content_length == 0 ) continue outer; endif var needed_length := newlineIndex + 2 + content_length; if ( recvBuffer.length() >= needed_length ) var msg := recvBuffer[newlineIndex + 3, content_length]; var obj := UnpackJSON( msg ); if ( obj.type == "response" ) print( $"{C_MAGENTA_LIGHT}++ [response#{request_seq}] [{obj.command}]{C_RESET} {PackJSON( obj.body )}" ); dapResponses[obj.request_seq] := obj; if ( request_seq == obj.request_seq ) found := 1; endif elseif ( obj.type == "event" ) dapEvents.append( obj ); print( $"{C_MAGENTA_LIGHT}++ [event] [{obj.event}]{C_RESET} {PackJSON( obj.body )}" ); if ( event != "" && event == obj.event ) found := 1; endif endif if ( needed_length == recvBuffer.length() ) recvBuffer := ""; else recvBuffer := recvBuffer[needed_length + 1, recvBuffer.length()]; endif endif if ( previousBufferLength == recvBuffer.length() ) continue outer; endif previousBufferLength := recvBuffer.length(); Sleepms( 100 ); endwhile if ( found ) return found; endif endif endwhile return found; endfunction function fill_buffers_for_request( request_seq ) var filled := fill_buffers( 0, request_seq := request_seq ); var found := dapResponses.exists( request_seq ); if ( found ) return 1; endif filled := fill_buffers( 5, request_seq := request_seq ); if ( filled == error ) return filled; endif return dapResponses.exists( request_seq ); endfunction function fill_buffers_for_event( event, timeout ) var filled := fill_buffers( 0, event := event ); foreach ev in dapEvents if ( ev.event == event ) return 1; endif endforeach filled := fill_buffers( timeout, event := event ); foreach ev in dapEvents if ( ev.event == event ) return 1; endif endforeach if ( filled == error ) return filled; endif return 0; endfunction function event_wait( event, timeout := 10 ) var found := fill_buffers_for_event( event, timeout ); if ( found ) foreach result in dapEvents if ( result.event == event ) dapEvents.erase( _result_iter ); return result.body; endif endforeach endif return error{ errortext := $"No event for type {event}" }; endfunction function request( command, arguments ) var request_seq := nextSequence; var body := struct{ command := command, arguments := arguments, type := "request", seq := nextSequence++ }; var content := PackJSON( body ); print( $"{C_GREEN}-> [request#{request_seq}] [{command}]{C_RESET} {PackJSON( arguments )}" ); var request_text := "Content-Length: " + ( content.length() ) + CChr( 13 ) + CChr( 10 ) + CChr( 13 ) + CChr( 10 ) + content; connection.transmit( request_text ); var found := fill_buffers_for_request( request_seq ); if ( found ) var result := dapResponses[request_seq]; dapResponses.erase( request_seq ); if ( result.success ) return result.body; endif print( $"{C_RED}!! [response#{request_seq}]{C_RESET} failed: {result.message}" ); return error{ errortext := $"Request {command}#{request_seq} failed: {result.message}" }; endif print( $"{C_RED}[req seq#{request_seq}]{C_RESET} timed out: {found}" ); return error{ errortext := $"Request {command}#{request_seq} timed out: {found}" }; endfunction function request_succeeded( byref err, command, arguments := struct{} ) err := 0; var response := request( command, arguments ); if ( response ) print( $"{C_CYAN}<- [response] [{command}]{C_RESET}" ); return response; endif err := ret_error( response ); return 0; endfunction function event_received( byref err, event, timeout := 5 ) err := 0; var response := event_wait( event, timeout ); if ( response ) print( $"{C_CYAN}<- [event] [{event}]{C_RESET}" ); return response; endif err := ret_error( response ); return 0; endfunction