-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(debugger): Add support for debugging tests (#5208) #7
base: feat/5208-debug-tests-repl-dap
Are you sure you want to change the base?
Changes from 27 commits
37283ac
21f0739
1589176
1580110
e3f10d6
d9aaa1d
6d16905
aebba26
8b96a17
7b3961a
536a89f
54fd555
6b1cc78
16a76a1
873d516
92e551d
8da3eef
5a1625e
9eacfe6
c36f131
c8f8b40
ac3aa22
7914726
19bf1e9
2dc687c
b70cd79
8d76cac
2dbda88
4364c48
5ef8e84
38b58e7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,7 @@ | ||
use crate::foreign_calls::DebugForeignCallExecutor; | ||
use acvm::acir::brillig::BitSize; | ||
use acvm::acir::circuit::brillig::BrilligBytecode; | ||
use acvm::acir::circuit::{Circuit, Opcode, OpcodeLocation}; | ||
use acvm::acir::circuit::{Circuit, Opcode, OpcodeLocation, ResolvedOpcodeLocation}; | ||
use acvm::acir::native_types::{Witness, WitnessMap, WitnessStack}; | ||
use acvm::brillig_vm::MemoryValue; | ||
use acvm::pwg::{ | ||
|
@@ -12,7 +12,7 @@ | |
|
||
use codespan_reporting::files::{Files, SimpleFile}; | ||
use fm::FileId; | ||
use nargo::errors::{ExecutionError, Location}; | ||
use nargo::errors::{map_execution_error, ExecutionError, Location}; | ||
use nargo::NargoError; | ||
use noirc_artifacts::debug::{DebugArtifact, StackFrame}; | ||
use noirc_driver::DebugFile; | ||
|
@@ -183,9 +183,14 @@ | |
|
||
#[derive(Debug)] | ||
pub(super) enum DebugCommandResult { | ||
// TODO: validate comments | ||
// The debugging session is over successfully | ||
Done, | ||
// The session is active and we should continue with the execution | ||
Ok, | ||
// Execution should be paused since we reached a Breakpoint | ||
BreakpointReached(DebugLocation), | ||
// Session is over with an error | ||
Error(NargoError<FieldElement>), | ||
} | ||
|
||
|
@@ -316,6 +321,20 @@ | |
frames | ||
} | ||
|
||
fn get_resolved_call_stack(&self) -> Vec<ResolvedOpcodeLocation> { | ||
self.get_call_stack() | ||
.iter() | ||
.map(|debug_loc| { | ||
// usize should be at least u32 for supported platforms | ||
let acir_function_index = usize::try_from(debug_loc.circuit_id).unwrap(); | ||
acvm::acir::circuit::ResolvedOpcodeLocation { | ||
acir_function_index, | ||
opcode_location: debug_loc.opcode_location, | ||
} | ||
}) | ||
Comment on lines
+327
to
+334
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It may be better to implement the self.get_call_stack().iter().map(|debug_loc| debug_loc.into()).collect() |
||
.collect() | ||
} | ||
|
||
pub(super) fn is_source_location_in_debug_module(&self, location: &Location) -> bool { | ||
self.debug_artifact | ||
.file_map | ||
|
@@ -472,9 +491,13 @@ | |
self.brillig_solver = Some(solver); | ||
self.handle_foreign_call(foreign_call) | ||
} | ||
Err(err) => DebugCommandResult::Error(NargoError::ExecutionError( | ||
ExecutionError::SolvingError(err, None), | ||
)), | ||
Err(err) => { | ||
self.brillig_solver = Some(solver); | ||
DebugCommandResult::Error(NargoError::ExecutionError(map_execution_error( | ||
err, | ||
&self.get_resolved_call_stack(), | ||
))) | ||
} | ||
} | ||
} | ||
|
||
|
@@ -574,7 +597,7 @@ | |
} | ||
} | ||
ACVMStatus::Failure(error) => DebugCommandResult::Error(NargoError::ExecutionError( | ||
ExecutionError::SolvingError(error, None), | ||
map_execution_error(error, &self.get_resolved_call_stack()), | ||
)), | ||
ACVMStatus::RequiresForeignCall(foreign_call) => self.handle_foreign_call(foreign_call), | ||
ACVMStatus::RequiresAcirCall(call_info) => self.handle_acir_call(call_info), | ||
|
@@ -904,7 +927,7 @@ | |
outputs: vec![], | ||
predicate: None, | ||
}]; | ||
let brillig_funcs = &vec![brillig_bytecode]; | ||
let current_witness_index = 2; | ||
let circuit = Circuit { current_witness_index, opcodes, ..Circuit::default() }; | ||
let circuits = &vec![circuit]; | ||
|
@@ -923,7 +946,7 @@ | |
debug_artifact, | ||
initial_witness, | ||
foreign_call_executor, | ||
brillig_funcs, | ||
); | ||
|
||
assert_eq!( | ||
|
@@ -1042,14 +1065,14 @@ | |
|
||
let foreign_call_executor = | ||
Box::new(DefaultDebugForeignCallExecutor::from_artifact(true, debug_artifact)); | ||
let brillig_funcs = &vec![brillig_bytecode]; | ||
let mut context = DebugContext::new( | ||
&StubbedBlackBoxSolver, | ||
circuits, | ||
debug_artifact, | ||
initial_witness, | ||
foreign_call_executor, | ||
brillig_funcs, | ||
); | ||
|
||
// set breakpoint | ||
|
@@ -1116,7 +1139,7 @@ | |
}; | ||
let circuits = vec![circuit_one, circuit_two]; | ||
let debug_artifact = DebugArtifact { debug_symbols: vec![], file_map: BTreeMap::new() }; | ||
let brillig_funcs = &vec![brillig_one, brillig_two]; | ||
|
||
let context = DebugContext::new( | ||
&StubbedBlackBoxSolver, | ||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -1,10 +1,12 @@ | ||||||
use std::collections::BTreeMap; | ||||||
use std::io::{Read, Write}; | ||||||
|
||||||
use acvm::acir::circuit::brillig::BrilligBytecode; | ||||||
use acvm::acir::circuit::Circuit; | ||||||
use acvm::acir::native_types::WitnessMap; | ||||||
use acvm::{BlackBoxFunctionSolver, FieldElement}; | ||||||
use nargo::errors::try_to_diagnose_runtime_error; | ||||||
use nargo::ops::check_expected_failure_message; | ||||||
use noirc_abi::Abi; | ||||||
use noirc_frontend::hir::def_map::TestFunction; | ||||||
|
||||||
use crate::context::DebugContext; | ||||||
use crate::context::{DebugCommandResult, DebugLocation}; | ||||||
|
@@ -21,8 +23,8 @@ use dap::responses::{ | |||||
}; | ||||||
use dap::server::Server; | ||||||
use dap::types::{ | ||||||
Breakpoint, DisassembledInstruction, Scope, Source, StackFrame, SteppingGranularity, | ||||||
StoppedEventReason, Thread, Variable, | ||||||
Breakpoint, DisassembledInstruction, OutputEventCategory, Scope, Source, StackFrame, | ||||||
SteppingGranularity, StoppedEventReason, Thread, Variable, | ||||||
}; | ||||||
use noirc_artifacts::debug::DebugArtifact; | ||||||
|
||||||
|
@@ -39,6 +41,8 @@ pub struct DapSession<'a, R: Read, W: Write, B: BlackBoxFunctionSolver<FieldElem | |||||
next_breakpoint_id: BreakpointId, | ||||||
instruction_breakpoints: Vec<(DebugLocation, BreakpointId)>, | ||||||
source_breakpoints: BTreeMap<FileId, Vec<(DebugLocation, BreakpointId)>>, | ||||||
test_function: Option<TestFunction>, | ||||||
abi: &'a Abi, | ||||||
} | ||||||
|
||||||
enum ScopeReferences { | ||||||
|
@@ -61,18 +65,18 @@ impl<'a, R: Read, W: Write, B: BlackBoxFunctionSolver<FieldElement>> DapSession< | |||||
pub fn new( | ||||||
server: Server<R, W>, | ||||||
solver: &'a B, | ||||||
circuits: &'a [Circuit<FieldElement>], | ||||||
program: &'a CompiledProgram, | ||||||
debug_artifact: &'a DebugArtifact, | ||||||
initial_witness: WitnessMap<FieldElement>, | ||||||
unconstrained_functions: &'a [BrilligBytecode<FieldElement>], | ||||||
test_function: Option<TestFunction>, | ||||||
) -> Self { | ||||||
let context = DebugContext::new( | ||||||
solver, | ||||||
circuits, | ||||||
&program.program.functions, | ||||||
debug_artifact, | ||||||
initial_witness, | ||||||
Box::new(DefaultDebugForeignCallExecutor::from_artifact(true, debug_artifact)), | ||||||
unconstrained_functions, | ||||||
&program.program.unconstrained_functions, | ||||||
); | ||||||
Self { | ||||||
server, | ||||||
|
@@ -82,6 +86,8 @@ impl<'a, R: Read, W: Write, B: BlackBoxFunctionSolver<FieldElement>> DapSession< | |||||
next_breakpoint_id: 1, | ||||||
instruction_breakpoints: vec![], | ||||||
source_breakpoints: BTreeMap::new(), | ||||||
test_function, | ||||||
abi: &program.abi, | ||||||
} | ||||||
} | ||||||
|
||||||
|
@@ -341,6 +347,14 @@ impl<'a, R: Read, W: Write, B: BlackBoxFunctionSolver<FieldElement>> DapSession< | |||||
match result { | ||||||
DebugCommandResult::Done => { | ||||||
self.running = false; | ||||||
if let Some(test_function) = self.test_function.as_ref() { | ||||||
let test_result = if test_function.should_fail() { | ||||||
"x Test failed: Test passed when it should have failed" | ||||||
} else { | ||||||
"✓ Test passed" | ||||||
}; | ||||||
self.send_debug_output_message(String::from(test_result))?; | ||||||
} | ||||||
} | ||||||
DebugCommandResult::Ok => { | ||||||
self.server.send_event(Event::Stopped(StoppedEventBody { | ||||||
|
@@ -368,18 +382,65 @@ impl<'a, R: Read, W: Write, B: BlackBoxFunctionSolver<FieldElement>> DapSession< | |||||
DebugCommandResult::Error(err) => { | ||||||
self.server.send_event(Event::Stopped(StoppedEventBody { | ||||||
reason: StoppedEventReason::Exception, | ||||||
description: Some(format!("{err:?}")), | ||||||
description: Some(String::from("Paused on exception")), // This is shown in callstack section of the debug panel | ||||||
thread_id: Some(0), | ||||||
preserve_focus_hint: Some(false), | ||||||
text: None, | ||||||
text: Some(format!("{err}\n{err:?}")), // This is shown in the exception panel | ||||||
all_threads_stopped: Some(false), | ||||||
hit_breakpoint_ids: None, | ||||||
}))?; | ||||||
if let Some(test_function) = self.test_function.as_ref() { | ||||||
let test_should_have_passed = !test_function.should_fail(); | ||||||
if test_should_have_passed { | ||||||
// Do not finish the execution of the debugger | ||||||
// Since the test shouldn't have failed, so that user can | ||||||
// - see the error on the exception panel | ||||||
// - restart the debug session | ||||||
let message = format!("x Text failed: {}", err); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Nit and typo |
||||||
self.send_debug_output_message(message)?; | ||||||
} else { | ||||||
// Finish the execution of the debugger since the test reached | ||||||
// the expected state (failed) | ||||||
self.running = false; | ||||||
let diagnostic = try_to_diagnose_runtime_error( | ||||||
&err, | ||||||
self.abi, | ||||||
&self.debug_artifact.debug_symbols, | ||||||
); | ||||||
let message = match check_expected_failure_message( | ||||||
test_function, | ||||||
err.user_defined_failure_message(&self.abi.error_types), | ||||||
diagnostic, | ||||||
) { | ||||||
nargo::ops::TestStatus::Pass => String::from("✓ Test passed"), | ||||||
nargo::ops::TestStatus::Fail { message, .. } => { | ||||||
format!("x Test failed: {}", message) | ||||||
} | ||||||
nargo::ops::TestStatus::CompileError(..) => { | ||||||
String::from("x Test failed: Something went wrong") | ||||||
anaPerezGhiglia marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
} // TODO: this shouldn't happen. Should we panic? | ||||||
}; | ||||||
self.send_debug_output_message(message)?; | ||||||
}; | ||||||
} | ||||||
} | ||||||
} | ||||||
Ok(()) | ||||||
} | ||||||
|
||||||
fn send_debug_output_message(&mut self, message: String) -> Result<(), ServerError> { | ||||||
self.server.send_event(Event::Output(dap::events::OutputEventBody { | ||||||
category: Some(OutputEventCategory::Console), | ||||||
output: message, | ||||||
group: None, | ||||||
variables_reference: None, | ||||||
source: None, | ||||||
line: None, | ||||||
column: None, | ||||||
data: None, | ||||||
})) | ||||||
} | ||||||
|
||||||
fn get_next_breakpoint_id(&mut self) -> BreakpointId { | ||||||
let id = self.next_breakpoint_id; | ||||||
self.next_breakpoint_id += 1; | ||||||
|
@@ -607,16 +668,12 @@ pub fn run_session<R: Read, W: Write, B: BlackBoxFunctionSolver<FieldElement>>( | |||||
solver: &B, | ||||||
program: CompiledProgram, | ||||||
initial_witness: WitnessMap<FieldElement>, | ||||||
test_function: Option<TestFunction>, | ||||||
) -> Result<(), ServerError> { | ||||||
let debug_artifact = DebugArtifact { debug_symbols: program.debug, file_map: program.file_map }; | ||||||
let mut session = DapSession::new( | ||||||
server, | ||||||
solver, | ||||||
&program.program.functions, | ||||||
&debug_artifact, | ||||||
initial_witness, | ||||||
&program.program.unconstrained_functions, | ||||||
); | ||||||
let debug_artifact = | ||||||
DebugArtifact { debug_symbols: program.debug.clone(), file_map: program.file_map.clone() }; | ||||||
let mut session = | ||||||
DapSession::new(server, solver, &program, &debug_artifact, initial_witness, test_function); | ||||||
|
||||||
session.run_loop() | ||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,6 +15,8 @@ use crate::{ | |
const ARROW: &str = "▶\u{fe0e}"; | ||
const TEST_COMMAND: &str = "nargo.test"; | ||
const TEST_CODELENS_TITLE: &str = "Run Test"; | ||
const DEBUG_TEST_COMMAND: &str = "nargo.debug.test"; | ||
const DEBUG_TEST_CODELENS_TITLE: &str = "Debug test"; | ||
const COMPILE_COMMAND: &str = "nargo.compile"; | ||
const COMPILE_CODELENS_TITLE: &str = "Compile"; | ||
const INFO_COMMAND: &str = "nargo.info"; | ||
|
@@ -120,7 +122,7 @@ pub(crate) fn collect_lenses_for_package( | |
arguments: Some( | ||
[ | ||
package_selection_args(workspace, package), | ||
vec!["--exact".into(), func_name.into()], | ||
vec!["--exact".into(), serde_json::Value::from(String::from(&func_name))], | ||
Comment on lines
-123
to
+125
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this change needed? |
||
] | ||
.concat(), | ||
), | ||
|
@@ -129,6 +131,22 @@ pub(crate) fn collect_lenses_for_package( | |
let test_lens = CodeLens { range, command: Some(test_command), data: None }; | ||
|
||
lenses.push(test_lens); | ||
|
||
let debug_test_command = Command { | ||
title: DEBUG_TEST_CODELENS_TITLE.to_string(), | ||
command: DEBUG_TEST_COMMAND.into(), | ||
arguments: Some( | ||
[ | ||
package_selection_args(workspace, package), | ||
vec!["--exact".into(), serde_json::Value::from(String::from(&func_name))], | ||
] | ||
.concat(), | ||
), | ||
}; | ||
|
||
let debug_test_lens = CodeLens { range, command: Some(debug_test_command), data: None }; | ||
|
||
lenses.push(debug_test_lens); | ||
} | ||
|
||
if package.is_binary() { | ||
|
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -218,3 +218,29 @@ pub fn try_to_diagnose_runtime_error( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
.with_call_stack(source_locations), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
pub fn map_execution_error<F: AcirField>( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not fan of the name... maybe |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
error: OpcodeResolutionError<F>, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
call_stack: &[ResolvedOpcodeLocation], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
) -> ExecutionError<F> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
let call_stack = match &error { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
OpcodeResolutionError::UnsatisfiedConstrain { .. } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| OpcodeResolutionError::IndexOutOfBounds { .. } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| OpcodeResolutionError::BrilligFunctionFailed { .. } => Some(call_stack.to_vec()), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
_ => None, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
let assertion_payload: Option<ResolvedAssertionPayload<F>> = match &error { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
OpcodeResolutionError::BrilligFunctionFailed { payload, .. } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| OpcodeResolutionError::UnsatisfiedConstrain { payload, .. } => payload.clone(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
_ => None, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
match assertion_payload { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Some(payload) => ExecutionError::AssertionFailed( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
payload, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
call_stack.expect("Should have call stack for an assertion failure"), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
None => ExecutionError::SolvingError(error, call_stack), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+226
to
+245
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this can be simplified to
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In all of these, it's probably desirable to use
///
to use the comments as documentation.