starlark/debug/adapter/
tests.rs

1/*
2 * Copyright 2019 The Starlark in Rust Authors.
3 * Copyright (c) Facebook, Inc. and its affiliates.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *     https://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18#[cfg(test)]
19mod t {
20    use std::collections::HashMap;
21    use std::hint;
22    use std::sync::atomic::AtomicUsize;
23    use std::sync::atomic::Ordering;
24    use std::sync::Arc;
25    use std::thread;
26    use std::thread::ScopedJoinHandle;
27    use std::time::Duration;
28    use std::time::Instant;
29
30    use debugserver_types::*;
31    use dupe::Dupe;
32
33    use crate::assert::test_functions;
34    use crate::debug::adapter::implementation::prepare_dap_adapter;
35    use crate::debug::adapter::implementation::resolve_breakpoints;
36    use crate::debug::DapAdapter;
37    use crate::debug::DapAdapterClient;
38    use crate::debug::DapAdapterEvalHook;
39    use crate::debug::StepKind;
40    use crate::debug::VariablePath;
41    use crate::environment::GlobalsBuilder;
42    use crate::environment::Module;
43    use crate::eval::Evaluator;
44    use crate::eval::ReturnFileLoader;
45    use crate::syntax::AstModule;
46    use crate::syntax::Dialect;
47    use crate::values::OwnedFrozenValue;
48    use crate::wasm::is_wasm;
49
50    #[derive(Debug)]
51    struct Client {
52        controller: BreakpointController,
53    }
54
55    impl Client {
56        pub fn new(controller: BreakpointController) -> Self {
57            Self { controller }
58        }
59    }
60
61    impl DapAdapterClient for Client {
62        fn event_stopped(&self) -> crate::Result<()> {
63            println!("stopped!");
64            self.controller.eval_stopped()
65        }
66    }
67
68    #[derive(Debug, Clone, Dupe)]
69    struct BreakpointController {
70        /// The number of breakpoint hits or 999999 if cancelled.
71        breakpoints_hit: Arc<AtomicUsize>,
72    }
73
74    impl BreakpointController {
75        fn new() -> Self {
76            Self {
77                breakpoints_hit: Arc::new(AtomicUsize::new(0)),
78            }
79        }
80
81        fn get_client(&self) -> Box<dyn DapAdapterClient> {
82            Box::new(Client::new(self.dupe()))
83        }
84
85        fn eval_stopped(&self) -> crate::Result<()> {
86            loop {
87                let breakpoints_hit = self.breakpoints_hit.load(Ordering::SeqCst);
88                if breakpoints_hit == 999999 {
89                    eprintln!("eval_stopped: cancelled");
90                    return Err(anyhow::anyhow!("cancelled").into());
91                }
92                if self.breakpoints_hit.compare_exchange(
93                    breakpoints_hit,
94                    breakpoints_hit + 1,
95                    Ordering::SeqCst,
96                    Ordering::SeqCst,
97                ) == Ok(breakpoints_hit)
98                {
99                    return Ok(());
100                }
101            }
102        }
103
104        fn wait_for_eval_stopped(&self, breakpoint_count: usize, timeout: Duration) {
105            let now = Instant::now();
106            loop {
107                let breakpoints_hit = self.breakpoints_hit.load(Ordering::SeqCst);
108                assert_ne!(breakpoints_hit, 999999, "cancelled");
109                assert!(breakpoints_hit <= breakpoint_count);
110                if breakpoints_hit == breakpoint_count {
111                    break;
112                }
113                if now.elapsed() > timeout {
114                    panic!("didn't hit expected breakpoint");
115                }
116                hint::spin_loop();
117            }
118        }
119    }
120
121    struct BreakpointControllerDropGuard {
122        controller: BreakpointController,
123    }
124
125    impl Drop for BreakpointControllerDropGuard {
126        fn drop(&mut self) {
127            eprintln!("dropping controller");
128            self.controller
129                .breakpoints_hit
130                .store(999999, Ordering::SeqCst);
131        }
132    }
133
134    fn breakpoint(line: i64, condition: Option<&str>) -> SourceBreakpoint {
135        SourceBreakpoint {
136            column: None,
137            condition: condition.map(|v| v.to_owned()),
138            hit_condition: None,
139            line,
140            log_message: None,
141        }
142    }
143
144    fn breakpoints_args(path: &str, lines: &[(i64, Option<&str>)]) -> SetBreakpointsArguments {
145        SetBreakpointsArguments {
146            breakpoints: Some(
147                lines
148                    .iter()
149                    .map(|(line, condition)| breakpoint(*line, condition.as_deref()))
150                    .collect(),
151            ),
152            lines: None,
153            source: Source {
154                adapter_data: None,
155                checksums: None,
156                name: None,
157                origin: None,
158                path: Some(path.to_owned()),
159                presentation_hint: None,
160                source_reference: None,
161                sources: None,
162            },
163            source_modified: None,
164        }
165    }
166
167    fn eval_with_hook(
168        ast: AstModule,
169        hook: Box<dyn DapAdapterEvalHook>,
170    ) -> crate::Result<OwnedFrozenValue> {
171        let modules = HashMap::new();
172        let loader = ReturnFileLoader { modules: &modules };
173        let globals = GlobalsBuilder::extended().with(test_functions).build();
174        let env = Module::new();
175        let res = {
176            let mut eval = Evaluator::new(&env);
177            hook.add_dap_hooks(&mut eval);
178            eval.set_loader(&loader);
179            eval.eval_module(ast, &globals)?
180        };
181
182        env.set("_", res);
183        Ok(env
184            .freeze()
185            .expect("error freezing module")
186            .get("_")
187            .unwrap())
188    }
189
190    fn join_timeout<T>(waiting: ScopedJoinHandle<T>, timeout: Duration) -> T {
191        let start = Instant::now();
192        while !waiting.is_finished() {
193            if start.elapsed() > timeout {
194                panic!();
195            }
196        }
197        waiting.join().unwrap()
198    }
199
200    static TIMEOUT: Duration = Duration::from_secs(10);
201
202    fn dap_test_template<'env, F, R>(f: F) -> crate::Result<R>
203    where
204        F: for<'scope> FnOnce(
205            &'scope thread::Scope<'scope, 'env>,
206            BreakpointController,
207            Box<dyn DapAdapter>,
208            Box<dyn DapAdapterEvalHook>,
209        ) -> crate::Result<R>,
210    {
211        let controller = BreakpointController::new();
212
213        let _guard = BreakpointControllerDropGuard {
214            controller: controller.dupe(),
215        };
216
217        let (adapter, eval_hook) = prepare_dap_adapter(controller.get_client());
218        thread::scope(|s| f(s, controller, Box::new(adapter), Box::new(eval_hook)))
219    }
220
221    #[test]
222    fn test_breakpoint() -> crate::Result<()> {
223        if is_wasm() {
224            // `thread::scope` doesn't work in wasm.
225            return Ok(());
226        }
227
228        let file_contents = "
229x = [1, 2, 3]
230print(x)
231        ";
232        dap_test_template(|s, controller, adapter, eval_hook| {
233            let ast = AstModule::parse(
234                "test.bzl",
235                file_contents.to_owned(),
236                &Dialect::AllOptionsInternal,
237            )?;
238            let breakpoints =
239                resolve_breakpoints(&breakpoints_args("test.bzl", &[(3, None)]), &ast)?;
240            adapter.set_breakpoints("test.bzl", &breakpoints)?;
241            let eval_result =
242                s.spawn(move || -> crate::Result<_> { eval_with_hook(ast, eval_hook) });
243            controller.wait_for_eval_stopped(1, TIMEOUT);
244            // TODO(cjhopman): we currently hit breakpoints on top-level statements twice (once for the gc bytecode, once for the actual statement).
245            adapter.continue_()?;
246            controller.wait_for_eval_stopped(2, TIMEOUT);
247
248            adapter.continue_()?;
249
250            join_timeout(eval_result, TIMEOUT)?;
251            Ok(())
252        })
253    }
254
255    #[test]
256    fn test_breakpoint_with_failing_condition() -> crate::Result<()> {
257        if is_wasm() {
258            return Ok(());
259        }
260
261        let file_contents = "
262x = [1, 2, 3]
263print(x)
264        ";
265        dap_test_template(|s, _, adapter, eval_hook| {
266            let ast = AstModule::parse(
267                "test.bzl",
268                file_contents.to_owned(),
269                &Dialect::AllOptionsInternal,
270            )?;
271            let breakpoints =
272                resolve_breakpoints(&breakpoints_args("test.bzl", &[(3, Some("5 in x"))]), &ast)?;
273            adapter.set_breakpoints("test.bzl", &breakpoints)?;
274            let eval_result =
275                s.spawn(move || -> crate::Result<_> { eval_with_hook(ast, eval_hook) });
276            join_timeout(eval_result, TIMEOUT)?;
277            Ok(())
278        })
279    }
280
281    #[test]
282    fn test_breakpoint_with_passing_condition() -> crate::Result<()> {
283        if is_wasm() {
284            return Ok(());
285        }
286
287        let file_contents = "
288x = [1, 2, 3]
289print(x)
290        ";
291        dap_test_template(|s, controller, adapter, eval_hook| {
292            let ast = AstModule::parse(
293                "test.bzl",
294                file_contents.to_owned(),
295                &Dialect::AllOptionsInternal,
296            )?;
297            let breakpoints =
298                resolve_breakpoints(&breakpoints_args("test.bzl", &[(3, Some("2 in x"))]), &ast)?;
299            adapter.set_breakpoints("test.bzl", &breakpoints)?;
300            let eval_result =
301                s.spawn(move || -> crate::Result<_> { eval_with_hook(ast, eval_hook) });
302            controller.wait_for_eval_stopped(1, TIMEOUT);
303            adapter.continue_()?;
304            // TODO(cjhopman): we currently hit breakpoints on top-level statements twice (once for the gc bytecode, once for the actual statement).
305            controller.wait_for_eval_stopped(2, TIMEOUT);
306            adapter.continue_()?;
307
308            join_timeout(eval_result, TIMEOUT)?;
309            Ok(())
310        })
311    }
312
313    #[test]
314    fn test_step_over() -> crate::Result<()> {
315        if is_wasm() {
316            return Ok(());
317        }
318
319        let file_contents = "
320def adjust(y):
321    y[0] += 1
322    y[1] += 1 # line 4
323    y[2] += 1
324x = [1, 2, 3]
325adjust(x) # line 7
326adjust(x)
327print(x)
328        ";
329        dap_test_template(|s, controller, adapter, eval_hook| {
330            let ast = AstModule::parse(
331                "test.bzl",
332                file_contents.to_owned(),
333                &Dialect::AllOptionsInternal,
334            )?;
335            let breakpoints =
336                resolve_breakpoints(&breakpoints_args("test.bzl", &[(7, None)]), &ast)?;
337            adapter.set_breakpoints("test.bzl", &breakpoints)?;
338            let eval_result =
339                s.spawn(move || -> crate::Result<_> { eval_with_hook(ast, eval_hook) });
340            controller.wait_for_eval_stopped(1, TIMEOUT);
341            // TODO(cjhopman): we currently hit breakpoints on top-level statements twice (once for the gc bytecode, once for the actual statement).
342            adapter.continue_()?;
343            controller.wait_for_eval_stopped(2, TIMEOUT);
344
345            assert_eq!("1", adapter.evaluate("x[0]")?.result);
346            assert_eq!("2", adapter.evaluate("x[1]")?.result);
347            assert_eq!("3", adapter.evaluate("x[2]")?.result);
348            adapter.step(StepKind::Over)?;
349            controller.wait_for_eval_stopped(3, TIMEOUT);
350            assert_eq!("2", adapter.evaluate("x[0]")?.result);
351            assert_eq!("3", adapter.evaluate("x[1]")?.result);
352            assert_eq!("4", adapter.evaluate("x[2]")?.result);
353
354            // TODO(cjhopman): we currently hit breakpoints on top-level statements twice (once for the gc bytecode, once for the actual statement).
355            adapter.step(StepKind::Over)?;
356            controller.wait_for_eval_stopped(4, TIMEOUT);
357            adapter.step(StepKind::Over)?;
358            controller.wait_for_eval_stopped(5, TIMEOUT);
359            assert_eq!("3", adapter.evaluate("x[0]")?.result);
360            assert_eq!("4", adapter.evaluate("x[1]")?.result);
361            assert_eq!("5", adapter.evaluate("x[2]")?.result);
362            adapter.continue_()?;
363            join_timeout(eval_result, TIMEOUT)?;
364            Ok(())
365        })
366    }
367
368    #[test]
369    fn test_step_into() -> crate::Result<()> {
370        if is_wasm() {
371            return Ok(());
372        }
373
374        let file_contents = "
375def adjust(y):
376    y[0] += 1
377    y[1] += 1 # line 4
378    y[2] += 1
379x = [1, 2, 3]
380adjust(x) # line 7
381adjust(x)
382print(x)
383        ";
384        dap_test_template(|s, controller, adapter, eval_hook| {
385            let ast = AstModule::parse(
386                "test.bzl",
387                file_contents.to_owned(),
388                &Dialect::AllOptionsInternal,
389            )?;
390            let breakpoints =
391                resolve_breakpoints(&breakpoints_args("test.bzl", &[(7, None)]), &ast)?;
392            adapter.set_breakpoints("test.bzl", &breakpoints)?;
393            let eval_result =
394                s.spawn(move || -> crate::Result<_> { eval_with_hook(ast, eval_hook) });
395            controller.wait_for_eval_stopped(1, TIMEOUT);
396            // TODO(cjhopman): we currently hit breakpoints on top-level statements twice (once for the gc bytecode, once for the actual statement).
397            adapter.continue_()?;
398            controller.wait_for_eval_stopped(2, TIMEOUT);
399
400            assert_eq!("1", adapter.evaluate("x[0]")?.result);
401            assert_eq!("2", adapter.evaluate("x[1]")?.result);
402            assert_eq!("3", adapter.evaluate("x[2]")?.result);
403
404            // into adjust
405            adapter.step(StepKind::Into)?;
406            controller.wait_for_eval_stopped(3, TIMEOUT);
407            assert_eq!("1", adapter.evaluate("y[0]")?.result);
408            assert_eq!("2", adapter.evaluate("y[1]")?.result);
409            assert_eq!("3", adapter.evaluate("y[2]")?.result);
410
411            // into should go to next line
412            adapter.step(StepKind::Into)?;
413            controller.wait_for_eval_stopped(4, TIMEOUT);
414            assert_eq!("2", adapter.evaluate("y[0]")?.result);
415            assert_eq!("2", adapter.evaluate("y[1]")?.result);
416            assert_eq!("3", adapter.evaluate("y[2]")?.result);
417
418            // two more intos should get us out of the function call
419            adapter.step(StepKind::Into)?;
420            controller.wait_for_eval_stopped(5, TIMEOUT);
421            adapter.step(StepKind::Into)?;
422            controller.wait_for_eval_stopped(6, TIMEOUT);
423            assert_eq!("2", adapter.evaluate("x[0]")?.result);
424            assert_eq!("3", adapter.evaluate("x[1]")?.result);
425            assert_eq!("4", adapter.evaluate("x[2]")?.result);
426
427            // and once more back into the function
428            adapter.step(StepKind::Into)?;
429            controller.wait_for_eval_stopped(7, TIMEOUT);
430
431            // TODO(cjhopman): unfortunately, gc being marked as statements causes us to need to step_into again.
432            adapter.step(StepKind::Into)?;
433            controller.wait_for_eval_stopped(8, TIMEOUT);
434
435            assert_eq!("2", adapter.evaluate("y[0]")?.result);
436            assert_eq!("3", adapter.evaluate("y[1]")?.result);
437            assert_eq!("4", adapter.evaluate("y[2]")?.result);
438
439            adapter.continue_()?;
440            join_timeout(eval_result, TIMEOUT)?;
441            Ok(())
442        })
443    }
444
445    #[test]
446    fn test_step_out() -> crate::Result<()> {
447        if is_wasm() {
448            return Ok(());
449        }
450
451        let file_contents = "
452def adjust(y):
453    y[0] += 1
454    y[1] += 1 # line 4
455    y[2] += 1
456x = [1, 2, 3]
457adjust(x) # line 7
458adjust(x)
459print(x)
460        ";
461        dap_test_template(|s, controller, adapter, eval_hook| {
462            let ast = AstModule::parse(
463                "test.bzl",
464                file_contents.to_owned(),
465                &Dialect::AllOptionsInternal,
466            )?;
467            let breakpoints =
468                resolve_breakpoints(&breakpoints_args("test.bzl", &[(4, None)]), &ast)?;
469            adapter.set_breakpoints("test.bzl", &breakpoints)?;
470            let eval_result =
471                s.spawn(move || -> crate::Result<_> { eval_with_hook(ast, eval_hook) });
472            // should break on the first time hitting line 4
473            controller.wait_for_eval_stopped(1, TIMEOUT);
474            assert_eq!("2", adapter.evaluate("y[0]")?.result);
475            assert_eq!("2", adapter.evaluate("y[1]")?.result);
476            assert_eq!("3", adapter.evaluate("y[2]")?.result);
477
478            // step out should take us to line 8
479            adapter.step(StepKind::Out)?;
480            controller.wait_for_eval_stopped(2, TIMEOUT);
481            assert_eq!("2", adapter.evaluate("x[0]")?.result);
482            assert_eq!("3", adapter.evaluate("x[1]")?.result);
483            assert_eq!("4", adapter.evaluate("x[2]")?.result);
484
485            // step out should actually hit the breakpoint at 4 first (before getting out)
486            adapter.step(StepKind::Out)?;
487            controller.wait_for_eval_stopped(3, TIMEOUT);
488            assert_eq!("3", adapter.evaluate("y[0]")?.result);
489            assert_eq!("3", adapter.evaluate("y[1]")?.result);
490            assert_eq!("4", adapter.evaluate("y[2]")?.result);
491
492            // step out should get out to the print
493            adapter.step(StepKind::Out)?;
494            controller.wait_for_eval_stopped(4, TIMEOUT);
495            assert_eq!("3", adapter.evaluate("x[0]")?.result);
496            assert_eq!("4", adapter.evaluate("x[1]")?.result);
497            assert_eq!("5", adapter.evaluate("x[2]")?.result);
498
499            // one more out should be equivalent to continue
500            adapter.step(StepKind::Out)?;
501            join_timeout(eval_result, TIMEOUT)?;
502            Ok(())
503        })
504    }
505
506    #[test]
507    fn test_local_variables() -> crate::Result<()> {
508        if is_wasm() {
509            return Ok(());
510        }
511
512        let file_contents = "
513def do():
514    a = struct(
515        f1 = \"1\",
516        f2 = 123,
517    )
518    arr = [1, 2, 3, 4, 6, \"234\", 123.32]
519    t = (1, 2)
520    d = dict(a = 1, b = \"2\")
521    empty_dict = {}
522    empty_list = []
523    empty_tuple = ()
524    return d # line 13
525print(do())
526        ";
527        let result = dap_test_template(|s, controller, adapter, eval_hook| {
528            let ast = AstModule::parse(
529                "test.bzl",
530                file_contents.to_owned(),
531                &Dialect::AllOptionsInternal,
532            )?;
533            let breakpoints =
534                resolve_breakpoints(&breakpoints_args("test.bzl", &[(13, None)]), &ast)?;
535            adapter.set_breakpoints("test.bzl", &breakpoints)?;
536            let eval_result =
537                s.spawn(move || -> crate::Result<_> { eval_with_hook(ast, eval_hook) });
538            controller.wait_for_eval_stopped(1, TIMEOUT);
539            let result = adapter.variables();
540            adapter.continue_()?;
541            join_timeout(eval_result, TIMEOUT)?;
542            result.map_err(crate::Error::from)
543        })?;
544        // It's easier to handle errors outside of thread::scope block as the test is quite flaky
545        // and hangs in case error propagates
546        assert_eq!(
547            vec![
548                ("a".to_owned(), String::from("<type:struct, size=2>"), true),
549                ("arr".to_owned(), String::from("<list, size=7>"), true),
550                ("t".to_owned(), String::from("<tuple, size=2>"), true),
551                ("d".to_owned(), String::from("<dict, size=2>"), true),
552                ("empty_dict".to_owned(), String::from("{}"), false),
553                ("empty_list".to_owned(), String::from("[]"), false),
554                ("empty_tuple".to_owned(), String::from("()"), false),
555            ],
556            result
557                .locals
558                .into_iter()
559                .map(|v| (v.name.to_string(), v.value, v.has_children))
560                .collect::<Vec<_>>()
561        );
562
563        Ok(())
564    }
565
566    #[test]
567    fn test_inspect_variables() -> crate::Result<()> {
568        if is_wasm() {
569            return Ok(());
570        }
571
572        let file_contents = "
573def do():
574    a = struct(
575        f1 = \"1\",
576        f2 = 123,
577    )
578    arr = [1, 2, 3, 4, 6, \"234\", 123.32]
579    t = (1, 2)
580    d = dict(a = 1, b = \"2\")
581    empty_dict = {}
582    empty_list = []
583    empty_tuple = ()
584    return d # line 13
585print(do())
586        ";
587        let result = dap_test_template(|s, controller, adapter, eval_hook| {
588            let mut result = Vec::new();
589            let ast = AstModule::parse(
590                "test.bzl",
591                file_contents.to_owned(),
592                &Dialect::AllOptionsInternal,
593            )?;
594            let breakpoints =
595                resolve_breakpoints(&breakpoints_args("test.bzl", &[(13, None)]), &ast)?;
596            adapter.set_breakpoints("test.bzl", &breakpoints)?;
597            let eval_result =
598                s.spawn(move || -> crate::Result<_> { eval_with_hook(ast, eval_hook) });
599            controller.wait_for_eval_stopped(1, TIMEOUT);
600            result.extend([
601                adapter.inspect_variable(VariablePath::new_local("a")),
602                adapter.inspect_variable(VariablePath::new_local("arr")),
603                adapter.inspect_variable(VariablePath::new_local("t")),
604                adapter.inspect_variable(VariablePath::new_local("d")),
605            ]);
606            adapter.continue_()?;
607            join_timeout(eval_result, TIMEOUT)?;
608            crate::Result::Ok(result)
609        })?
610        .into_iter()
611        .collect::<anyhow::Result<Vec<_>>>()?;
612
613        // It's easier to handle errors outside of thread::scope block as the test is quite flaky
614        // and hangs in case error propagates
615
616        assert_variable("f1", "1", false, &result[0].sub_values[0]);
617        assert_variable("f2", "123", false, &result[0].sub_values[1]);
618        assert_variable("0", "1", false, &result[1].sub_values[0]);
619        assert_variable("5", "234", false, &result[1].sub_values[5]);
620        assert_variable("0", "1", false, &result[2].sub_values[0]);
621        assert_variable("1", "2", false, &result[2].sub_values[1]);
622        assert_variable("\"a\"", "1", false, &result[3].sub_values[0]);
623        assert_variable("\"b\"", "2", false, &result[3].sub_values[1]);
624        Ok(())
625    }
626
627    #[test]
628    fn test_evaluate_expression() -> crate::Result<()> {
629        if is_wasm() {
630            return Ok(());
631        }
632
633        let file_contents = "
634def do():
635    s = struct(
636        inner = struct(
637            inner = struct(
638                value = \"more_inner\"
639            ),
640            value = \"inner\",
641            arr = [dict(a = 1, b = \"2\"), 1337]
642        )
643    )
644    return s # line 12
645print(do())
646        ";
647        let result = dap_test_template(|s, controller, adapter, eval_hook| {
648            let mut result = Vec::new();
649            let ast = AstModule::parse(
650                "test.bzl",
651                file_contents.to_owned(),
652                &Dialect::AllOptionsInternal,
653            )?;
654            let breakpoints =
655                resolve_breakpoints(&breakpoints_args("test.bzl", &[(12, None)]), &ast)?;
656            adapter.set_breakpoints("test.bzl", &breakpoints)?;
657            let eval_result =
658                s.spawn(move || -> crate::Result<_> { eval_with_hook(ast, eval_hook) });
659            controller.wait_for_eval_stopped(1, TIMEOUT);
660            result.extend([
661                adapter.evaluate("s.inner.value"),
662                adapter.evaluate("s.inner.inner.value"),
663                adapter.evaluate("s.inner.arr[0]"),
664                adapter.evaluate("s.inner.arr[0][\"a\"]"),
665                adapter.evaluate("s.inner.arr[1]"),
666            ]);
667            adapter.continue_()?;
668            join_timeout(eval_result, TIMEOUT)?;
669            crate::Result::Ok(result)
670        })?
671        .into_iter()
672        .collect::<anyhow::Result<Vec<_>>>()?;
673
674        // It's easier to handle errors outside of thread::scope block as the test is quite flaky
675        // and hangs in case error propagates
676        assert_eq!(
677            vec![
678                ("inner", false),
679                ("more_inner", false),
680                ("<dict, size=2>", true),
681                ("1", false),
682                ("1337", false),
683            ],
684            result
685                .iter()
686                .map(|v| (v.result.as_str(), v.has_children))
687                .collect::<Vec<_>>()
688        );
689
690        Ok(())
691    }
692
693    fn assert_variable(
694        name: &str,
695        value: &str,
696        has_children: bool,
697        var: &crate::debug::adapter::Variable,
698    ) {
699        assert_eq!(
700            (name.to_owned(), value, has_children),
701            (var.name.to_string(), var.value.as_str(), var.has_children)
702        );
703    }
704
705    #[test]
706    pub fn test_truncate_string() {
707        assert_eq!(
708            "Hello",
709            crate::debug::adapter::Variable::truncate_string("Hello".to_owned(), 10)
710        );
711        assert_eq!(
712            "Hello",
713            crate::debug::adapter::Variable::truncate_string("Hello".to_owned(), 5)
714        );
715        assert_eq!(
716            "Hello, ...(truncated)",
717            // A string that should be truncated at a character boundary
718            crate::debug::adapter::Variable::truncate_string("Hello, 世界".to_owned(), 7)
719        );
720        assert_eq!(
721            "Hello, ...(truncated)",
722            // A string that would be truncated within a multi-byte character
723            crate::debug::adapter::Variable::truncate_string("Hello, 世界".to_owned(), 8)
724        );
725        assert_eq!(
726            "Hello, ...(truncated)",
727            // A string that should be truncated just before a multi-byte character
728            crate::debug::adapter::Variable::truncate_string("Hello, 世界".to_owned(), 9)
729        );
730        assert_eq!(
731            "Hello, 世...(truncated)",
732            crate::debug::adapter::Variable::truncate_string("Hello, 世界".to_owned(), 10)
733        );
734    }
735}