1use crate::value::PerlValue;
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
14#[serde(rename_all = "camelCase")]
15pub struct RenderedVariable {
16 pub name: String,
18
19 pub value: String,
21
22 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
24 pub type_name: Option<String>,
25
26 pub variables_reference: i64,
28
29 #[serde(skip_serializing_if = "Option::is_none")]
31 pub named_variables: Option<i64>,
32
33 #[serde(skip_serializing_if = "Option::is_none")]
35 pub indexed_variables: Option<i64>,
36
37 #[serde(skip_serializing_if = "Option::is_none")]
39 pub presentation_hint: Option<VariablePresentationHint>,
40
41 #[serde(skip_serializing_if = "Option::is_none")]
43 pub memory_reference: Option<String>,
44}
45
46#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
48#[serde(rename_all = "camelCase")]
49pub struct VariablePresentationHint {
50 #[serde(skip_serializing_if = "Option::is_none")]
52 pub kind: Option<String>,
53
54 #[serde(skip_serializing_if = "Option::is_none")]
56 pub attributes: Option<Vec<String>>,
57
58 #[serde(skip_serializing_if = "Option::is_none")]
60 pub visibility: Option<String>,
61}
62
63impl RenderedVariable {
64 #[must_use]
66 pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
67 Self {
68 name: name.into(),
69 value: value.into(),
70 type_name: None,
71 variables_reference: 0,
72 named_variables: None,
73 indexed_variables: None,
74 presentation_hint: None,
75 memory_reference: None,
76 }
77 }
78
79 #[must_use]
81 pub fn with_type(mut self, type_name: impl Into<String>) -> Self {
82 self.type_name = Some(type_name.into());
83 self
84 }
85
86 #[must_use]
88 pub fn with_reference(mut self, reference: i64) -> Self {
89 self.variables_reference = reference;
90 self
91 }
92
93 #[must_use]
95 pub fn with_indexed_variables(mut self, count: i64) -> Self {
96 self.indexed_variables = Some(count);
97 self
98 }
99
100 #[must_use]
102 pub fn with_named_variables(mut self, count: i64) -> Self {
103 self.named_variables = Some(count);
104 self
105 }
106
107 #[must_use]
109 pub fn is_expandable(&self) -> bool {
110 self.variables_reference != 0
111 }
112}
113
114pub trait VariableRenderer {
119 fn render(&self, name: &str, value: &PerlValue) -> RenderedVariable;
130
131 fn render_with_reference(
142 &self,
143 name: &str,
144 value: &PerlValue,
145 reference_id: i64,
146 ) -> RenderedVariable;
147
148 fn render_children(
160 &self,
161 value: &PerlValue,
162 start: usize,
163 count: usize,
164 ) -> Vec<RenderedVariable>;
165}
166
167#[derive(Debug, Default)]
176pub struct PerlVariableRenderer {
177 max_string_length: usize,
179 max_array_preview: usize,
181 max_hash_preview: usize,
183}
184
185impl PerlVariableRenderer {
186 #[must_use]
188 pub fn new() -> Self {
189 Self { max_string_length: 100, max_array_preview: 3, max_hash_preview: 3 }
190 }
191
192 #[must_use]
194 pub fn with_max_string_length(mut self, length: usize) -> Self {
195 self.max_string_length = length;
196 self
197 }
198
199 #[must_use]
201 pub fn with_max_array_preview(mut self, count: usize) -> Self {
202 self.max_array_preview = count;
203 self
204 }
205
206 #[must_use]
208 pub fn with_max_hash_preview(mut self, count: usize) -> Self {
209 self.max_hash_preview = count;
210 self
211 }
212
213 fn format_string(&self, s: &str) -> String {
215 let truncated = if s.len() > self.max_string_length {
216 let mut end = self.max_string_length;
219 while end > 0 && !s.is_char_boundary(end) {
220 end -= 1;
221 }
222 format!("{}...", &s[..end])
223 } else {
224 s.to_string()
225 };
226
227 let escaped = truncated
229 .replace('\\', "\\\\")
230 .replace('"', "\\\"")
231 .replace('\n', "\\n")
232 .replace('\r', "\\r")
233 .replace('\t', "\\t");
234
235 format!("\"{}\"", escaped)
236 }
237
238 fn format_array_preview(&self, elements: &[PerlValue]) -> String {
240 if elements.is_empty() {
241 return "[]".to_string();
242 }
243
244 let preview: Vec<String> = elements
245 .iter()
246 .take(self.max_array_preview)
247 .map(|v| self.format_value_brief(v))
248 .collect();
249
250 let suffix = if elements.len() > self.max_array_preview {
251 format!(", ... ({} total)", elements.len())
252 } else {
253 String::new()
254 };
255
256 format!("[{}{}]", preview.join(", "), suffix)
257 }
258
259 fn format_hash_preview(&self, pairs: &[(String, PerlValue)]) -> String {
261 if pairs.is_empty() {
262 return "{}".to_string();
263 }
264
265 let preview: Vec<String> = pairs
266 .iter()
267 .take(self.max_hash_preview)
268 .map(|(k, v)| format!("{} => {}", k, self.format_value_brief(v)))
269 .collect();
270
271 let suffix = if pairs.len() > self.max_hash_preview {
272 format!(", ... ({} keys)", pairs.len())
273 } else {
274 String::new()
275 };
276
277 format!("{{{}{}}}", preview.join(", "), suffix)
278 }
279
280 fn ref_prefix_chain(&self, value: &PerlValue) -> String {
285 let mut depth = 0u32;
286 let mut current = value;
287 while let PerlValue::Reference(inner) = current {
288 depth += 1;
289 current = inner;
290 if depth >= 10 {
291 break;
292 }
293 }
294 "\\".repeat(depth as usize)
295 }
296
297 fn format_deref_target_brief(&self, value: &PerlValue) -> String {
301 let mut current = value;
302 let mut safety = 0u32;
303 while let PerlValue::Reference(inner) = current {
304 current = inner;
305 safety += 1;
306 if safety >= 10 {
307 break;
308 }
309 }
310 match current {
311 PerlValue::Reference(_) => "REF(...)".to_string(),
312 other => self.format_non_ref_brief(other),
313 }
314 }
315
316 fn format_deref_target(&self, value: &PerlValue) -> String {
318 let mut current = value;
319 let mut safety = 0u32;
320 while let PerlValue::Reference(inner) = current {
321 current = inner;
322 safety += 1;
323 if safety >= 10 {
324 break;
325 }
326 }
327 match current {
328 PerlValue::Reference(_) => "REF(...)".to_string(),
329 other => self.format_value(other),
330 }
331 }
332
333 fn format_non_ref_brief(&self, value: &PerlValue) -> String {
335 match value {
336 PerlValue::Reference(_) => "REF".to_string(),
337 other => self.format_value_brief(other),
338 }
339 }
340
341 fn format_value_brief(&self, value: &PerlValue) -> String {
343 match value {
344 PerlValue::Undef => "undef".to_string(),
345 PerlValue::Scalar(s) => self.format_string(s),
346 PerlValue::Number(n) => n.to_string(),
347 PerlValue::Integer(i) => i.to_string(),
348 PerlValue::Array(elements) => format!("ARRAY({})", elements.len()),
349 PerlValue::Hash(pairs) => format!("HASH({})", pairs.len()),
350 PerlValue::Reference(inner) => {
351 let prefix = self.ref_prefix_chain(value);
352 format!("{}{}", prefix, self.format_deref_target_brief(inner))
353 }
354 PerlValue::Object { class, value } => {
355 let backing = match value.as_ref() {
356 PerlValue::Hash(_) => "HASH",
357 PerlValue::Array(_) => "ARRAY",
358 PerlValue::Scalar(_) | PerlValue::Number(_) | PerlValue::Integer(_) => "SCALAR",
359 _ => "REF",
360 };
361 format!("{} = {}(...)", class, backing)
362 }
363 PerlValue::Code { name } => {
364 name.as_ref().map_or_else(|| "CODE(...)".to_string(), |n| format!("\\&{}", n))
365 }
366 PerlValue::Glob(name) => format!("*{}", name),
367 PerlValue::Regex(pattern) => format!("qr/{}/", pattern),
368 PerlValue::Tied { class, .. } => format!("TIED({})", class),
369 PerlValue::Truncated { summary, .. } => summary.clone(),
370 PerlValue::Error(msg) => format!("<error: {}>", msg),
371 }
372 }
373
374 fn format_value(&self, value: &PerlValue) -> String {
376 match value {
377 PerlValue::Undef => "undef".to_string(),
378 PerlValue::Scalar(s) => self.format_string(s),
379 PerlValue::Number(n) => n.to_string(),
380 PerlValue::Integer(i) => i.to_string(),
381 PerlValue::Array(elements) => self.format_array_preview(elements),
382 PerlValue::Hash(pairs) => self.format_hash_preview(pairs),
383 PerlValue::Reference(inner) => {
384 let prefix = self.ref_prefix_chain(value);
385 format!("{}{}", prefix, self.format_deref_target(inner))
386 }
387 PerlValue::Object { class, value } => {
388 let backing = match value.as_ref() {
389 PerlValue::Hash(_) => "HASH",
390 PerlValue::Array(_) => "ARRAY",
391 PerlValue::Scalar(_) | PerlValue::Number(_) | PerlValue::Integer(_) => "SCALAR",
392 _ => "REF",
393 };
394 format!("{} = {}(...)", class, backing)
395 }
396 PerlValue::Code { name } => {
397 name.as_ref().map_or_else(|| "sub { ... }".to_string(), |n| format!("\\&{}", n))
398 }
399 PerlValue::Glob(name) => format!("*{}", name),
400 PerlValue::Regex(pattern) => format!("qr/{}/", pattern),
401 PerlValue::Tied { class, value } => {
402 if let Some(v) = value {
403 format!("TIED({}) = {}", class, self.format_value_brief(v))
404 } else {
405 format!("TIED({})", class)
406 }
407 }
408 PerlValue::Truncated { summary, total_count } => {
409 if let Some(count) = total_count {
410 format!("{} ({} total)", summary, count)
411 } else {
412 summary.clone()
413 }
414 }
415 PerlValue::Error(msg) => format!("<error: {}>", msg),
416 }
417 }
418}
419
420impl VariableRenderer for PerlVariableRenderer {
421 fn render(&self, name: &str, value: &PerlValue) -> RenderedVariable {
422 let formatted_value = self.format_value(value);
423 let type_name = value.type_name().to_string();
424
425 let mut rendered = RenderedVariable::new(name, formatted_value).with_type(type_name);
426
427 match value {
429 PerlValue::Array(elements) => {
430 rendered.indexed_variables = Some(elements.len() as i64);
431 }
432 PerlValue::Hash(pairs) => {
433 rendered.named_variables = Some(pairs.len() as i64);
434 }
435 PerlValue::Object { class, value: inner } => {
436 rendered.type_name = Some(class.clone());
437 rendered.presentation_hint = Some(VariablePresentationHint {
438 kind: Some("class".to_string()),
439 attributes: None,
440 visibility: None,
441 });
442 match inner.as_ref() {
443 PerlValue::Hash(pairs) => {
444 rendered.named_variables = Some(pairs.len() as i64);
445 }
446 PerlValue::Array(elements) => {
447 rendered.indexed_variables = Some(elements.len() as i64);
448 }
449 _ => {}
450 }
451 }
452 _ => {}
453 }
454
455 rendered
456 }
457
458 fn render_with_reference(
459 &self,
460 name: &str,
461 value: &PerlValue,
462 reference_id: i64,
463 ) -> RenderedVariable {
464 let mut rendered = self.render(name, value);
465
466 if value.is_expandable() {
467 rendered.variables_reference = reference_id;
468 }
469
470 rendered
471 }
472
473 fn render_children(
474 &self,
475 value: &PerlValue,
476 start: usize,
477 count: usize,
478 ) -> Vec<RenderedVariable> {
479 match value {
480 PerlValue::Array(elements) => elements
481 .iter()
482 .enumerate()
483 .skip(start)
484 .take(count)
485 .map(|(i, v)| self.render(&format!("[{}]", i), v))
486 .collect(),
487 PerlValue::Hash(pairs) => {
488 pairs.iter().skip(start).take(count).map(|(k, v)| self.render(k, v)).collect()
489 }
490 PerlValue::Reference(inner) => {
491 vec![self.render("$_", inner)]
492 }
493 PerlValue::Object { value: inner, .. } => self.render_children(inner, start, count),
494 PerlValue::Tied { value: Some(inner), .. } => self.render_children(inner, start, count),
495 _ => vec![],
496 }
497 }
498}
499
500#[cfg(test)]
501mod tests {
502 use super::*;
503
504 #[test]
505 fn test_render_scalar() {
506 let renderer = PerlVariableRenderer::new();
507 let value = PerlValue::Scalar("hello".to_string());
508 let rendered = renderer.render("$x", &value);
509
510 assert_eq!(rendered.name, "$x");
511 assert_eq!(rendered.value, "\"hello\"");
512 assert_eq!(rendered.type_name, Some("SCALAR".to_string()));
513 assert_eq!(rendered.variables_reference, 0);
514 }
515
516 #[test]
517 fn test_render_integer() {
518 let renderer = PerlVariableRenderer::new();
519 let value = PerlValue::Integer(42);
520 let rendered = renderer.render("$n", &value);
521
522 assert_eq!(rendered.name, "$n");
523 assert_eq!(rendered.value, "42");
524 assert_eq!(rendered.type_name, Some("SCALAR".to_string()));
525 }
526
527 #[test]
528 fn test_render_array() {
529 let renderer = PerlVariableRenderer::new();
530 let value = PerlValue::Array(vec![
531 PerlValue::Integer(1),
532 PerlValue::Integer(2),
533 PerlValue::Integer(3),
534 ]);
535 let rendered = renderer.render("@arr", &value);
536
537 assert_eq!(rendered.name, "@arr");
538 assert!(rendered.value.starts_with('['));
539 assert_eq!(rendered.type_name, Some("ARRAY".to_string()));
540 assert_eq!(rendered.indexed_variables, Some(3));
541 }
542
543 #[test]
544 fn test_render_hash() {
545 let renderer = PerlVariableRenderer::new();
546 let value = PerlValue::Hash(vec![
547 ("key1".to_string(), PerlValue::Scalar("value1".to_string())),
548 ("key2".to_string(), PerlValue::Integer(42)),
549 ]);
550 let rendered = renderer.render("%hash", &value);
551
552 assert_eq!(rendered.name, "%hash");
553 assert!(rendered.value.starts_with('{'));
554 assert_eq!(rendered.type_name, Some("HASH".to_string()));
555 assert_eq!(rendered.named_variables, Some(2));
556 }
557
558 #[test]
559 fn test_render_with_reference() {
560 let renderer = PerlVariableRenderer::new();
561 let value = PerlValue::Array(vec![PerlValue::Integer(1)]);
562 let rendered = renderer.render_with_reference("@arr", &value, 42);
563
564 assert_eq!(rendered.variables_reference, 42);
565 assert!(rendered.is_expandable());
566 }
567
568 #[test]
569 fn test_render_children_array() {
570 let renderer = PerlVariableRenderer::new();
571 let value = PerlValue::Array(vec![
572 PerlValue::Integer(10),
573 PerlValue::Integer(20),
574 PerlValue::Integer(30),
575 ]);
576 let children = renderer.render_children(&value, 0, 10);
577
578 assert_eq!(children.len(), 3);
579 assert_eq!(children[0].name, "[0]");
580 assert_eq!(children[0].value, "10");
581 assert_eq!(children[1].name, "[1]");
582 assert_eq!(children[2].name, "[2]");
583 }
584
585 #[test]
586 fn test_render_children_hash() {
587 let renderer = PerlVariableRenderer::new();
588 let value = PerlValue::Hash(vec![
589 ("foo".to_string(), PerlValue::Integer(1)),
590 ("bar".to_string(), PerlValue::Integer(2)),
591 ]);
592 let children = renderer.render_children(&value, 0, 10);
593
594 assert_eq!(children.len(), 2);
595 assert_eq!(children[0].name, "foo");
596 assert_eq!(children[1].name, "bar");
597 }
598
599 #[test]
600 fn test_render_object() {
601 let renderer = PerlVariableRenderer::new();
602 let value = PerlValue::Object {
603 class: "My::Class".to_string(),
604 value: Box::new(PerlValue::Hash(vec![(
605 "attr".to_string(),
606 PerlValue::Scalar("value".to_string()),
607 )])),
608 };
609 let rendered = renderer.render("$obj", &value);
610
611 assert_eq!(rendered.name, "$obj");
612 assert!(rendered.value.contains("My::Class"));
613 assert_eq!(rendered.type_name, Some("My::Class".to_string()));
614 assert_eq!(rendered.named_variables, Some(1));
615 }
616
617 #[test]
618 fn test_string_truncation() {
619 let renderer = PerlVariableRenderer::new().with_max_string_length(10);
620 let value = PerlValue::Scalar("this is a very long string".to_string());
621 let rendered = renderer.render("$s", &value);
622
623 assert!(rendered.value.contains("..."));
624 assert!(rendered.value.len() < 30);
625 }
626
627 #[test]
628 fn test_string_truncation_zero_max_length() {
629 let renderer = PerlVariableRenderer::new().with_max_string_length(0);
630 let value = PerlValue::Scalar("non-empty".to_string());
631 let rendered = renderer.render("$s", &value);
632
633 assert_eq!(rendered.value, "\"...\"");
634 }
635
636 #[test]
637 fn test_string_truncation_utf8_boundary_safety() {
638 let renderer = PerlVariableRenderer::new().with_max_string_length(1);
639 let value = PerlValue::Scalar("éclair".to_string());
640 let rendered = renderer.render("$s", &value);
641
642 assert_eq!(rendered.value, "\"...\"");
643 }
644
645 #[test]
646 fn test_string_escaping() {
647 let renderer = PerlVariableRenderer::new();
648 let value = PerlValue::Scalar("line1\nline2\ttab".to_string());
649 let rendered = renderer.render("$s", &value);
650
651 assert!(rendered.value.contains("\\n"));
652 assert!(rendered.value.contains("\\t"));
653 }
654
655 #[test]
656 fn test_render_undef() {
657 let renderer = PerlVariableRenderer::new();
658 let value = PerlValue::Undef;
659 let rendered = renderer.render("$x", &value);
660
661 assert_eq!(rendered.value, "undef");
662 assert_eq!(rendered.type_name, Some("undef".to_string()));
663 }
664
665 #[test]
666 fn test_render_reference() {
667 let renderer = PerlVariableRenderer::new();
668 let value = PerlValue::Reference(Box::new(PerlValue::Integer(42)));
669 let rendered = renderer.render("$ref", &value);
670
671 assert_eq!(rendered.name, "$ref");
672 assert!(rendered.value.contains("42"));
673 assert_eq!(rendered.type_name, Some("REF".to_string()));
674 }
675
676 #[test]
677 fn test_render_code() {
678 let renderer = PerlVariableRenderer::new();
679 let value = PerlValue::Code { name: Some("my_sub".to_string()) };
680 let rendered = renderer.render("$code", &value);
681
682 assert!(rendered.value.contains("my_sub"));
683 assert_eq!(rendered.type_name, Some("CODE".to_string()));
684 }
685
686 #[test]
697 fn test_render_self_referential_hash() {
698 let renderer = PerlVariableRenderer::new();
699
700 let circular_marker =
703 PerlValue::Truncated { summary: "HASH(0x...circular)".to_string(), total_count: None };
704 let value = PerlValue::Hash(vec![(
705 "a".to_string(),
706 PerlValue::Reference(Box::new(circular_marker)),
707 )]);
708
709 let rendered = renderer.render("$self", &value);
710
711 assert_eq!(rendered.name, "$self");
712 assert_eq!(rendered.type_name, Some("HASH".to_string()));
713 assert_eq!(rendered.named_variables, Some(1));
714 assert!(rendered.value.contains("circular"));
716
717 let children = renderer.render_children(&value, 0, 10);
719 assert_eq!(children.len(), 1);
720 assert_eq!(children[0].name, "a");
721 assert!(children[0].value.contains("circular"));
722 }
723
724 #[test]
729 fn test_render_deep_nesting_over_100_levels_bounded() {
730 let renderer = PerlVariableRenderer::new();
731
732 let mut value = PerlValue::Integer(42);
734 for _ in 0..150 {
735 value = PerlValue::Reference(Box::new(value));
736 }
737
738 let rendered = renderer.render("$deep", &value);
739
740 assert_eq!(rendered.name, "$deep");
741 assert_eq!(rendered.type_name, Some("REF".to_string()));
742
743 let backslash_prefix_count = rendered.value.chars().take_while(|&c| c == '\\').count();
747 assert!(
748 backslash_prefix_count <= 10,
749 "backslash prefix count {} should be <= 10",
750 backslash_prefix_count,
751 );
752 assert!(
754 rendered.value.contains("REF(...)"),
755 "deeply nested ref should contain REF(...), got: {}",
756 rendered.value,
757 );
758
759 assert!(
761 rendered.value.len() < 200,
762 "rendered value length {} should be < 200",
763 rendered.value.len(),
764 );
765 }
766
767 #[test]
769 fn test_render_reference_chain_at_depth_limit() {
770 let renderer = PerlVariableRenderer::new();
771
772 let mut value = PerlValue::Scalar("leaf".to_string());
773 for _ in 0..10 {
774 value = PerlValue::Reference(Box::new(value));
775 }
776
777 let rendered = renderer.render("$ref10", &value);
778 assert!(rendered.value.contains("leaf") || rendered.value.contains("REF(...)"));
781 assert!(rendered.value.len() < 200);
782 }
783
784 #[test]
786 fn test_render_large_array_over_10k_elements_truncates() {
787 let renderer = PerlVariableRenderer::new();
788
789 let elements: Vec<PerlValue> = (0..10_001).map(PerlValue::Integer).collect();
790 let value = PerlValue::Array(elements);
791
792 let rendered = renderer.render("@big", &value);
793
794 assert_eq!(rendered.name, "@big");
795 assert_eq!(rendered.type_name, Some("ARRAY".to_string()));
796 assert_eq!(rendered.indexed_variables, Some(10_001));
797
798 assert!(
801 rendered.value.contains("10001 total"),
802 "should show total count, got: {}",
803 rendered.value,
804 );
805 assert!(rendered.value.starts_with('['));
806 assert!(rendered.value.ends_with(']'));
807
808 assert!(
810 rendered.value.len() < 500,
811 "preview length {} should be < 500",
812 rendered.value.len(),
813 );
814 }
815
816 #[test]
818 fn test_render_children_large_array_pagination() {
819 let renderer = PerlVariableRenderer::new();
820
821 let elements: Vec<PerlValue> = (0..10_001).map(PerlValue::Integer).collect();
822 let value = PerlValue::Array(elements);
823
824 let children = renderer.render_children(&value, 5000, 100);
826 assert_eq!(children.len(), 100);
827 assert_eq!(children[0].name, "[5000]");
828 assert_eq!(children[0].value, "5000");
829 assert_eq!(children[99].name, "[5099]");
830 assert_eq!(children[99].value, "5099");
831
832 let tail = renderer.render_children(&value, 10_000, 100);
834 assert_eq!(tail.len(), 1);
835 assert_eq!(tail[0].name, "[10000]");
836 }
837
838 #[test]
840 fn test_render_large_hash_over_5k_pairs_truncates() {
841 let renderer = PerlVariableRenderer::new();
842
843 let pairs: Vec<(String, PerlValue)> =
844 (0..5_001).map(|i| (format!("key_{}", i), PerlValue::Integer(i))).collect();
845 let value = PerlValue::Hash(pairs);
846
847 let rendered = renderer.render("%big", &value);
848
849 assert_eq!(rendered.name, "%big");
850 assert_eq!(rendered.type_name, Some("HASH".to_string()));
851 assert_eq!(rendered.named_variables, Some(5_001));
852
853 assert!(
856 rendered.value.contains("5001 keys"),
857 "should show key count, got: {}",
858 rendered.value,
859 );
860 assert!(rendered.value.starts_with('{'));
861 assert!(rendered.value.ends_with('}'));
862
863 assert!(
865 rendered.value.len() < 500,
866 "preview length {} should be < 500",
867 rendered.value.len(),
868 );
869 }
870
871 #[test]
873 fn test_render_children_large_hash_pagination() {
874 let renderer = PerlVariableRenderer::new();
875
876 let pairs: Vec<(String, PerlValue)> =
877 (0..5_001).map(|i| (format!("key_{}", i), PerlValue::Integer(i))).collect();
878 let value = PerlValue::Hash(pairs);
879
880 let children = renderer.render_children(&value, 2500, 50);
882 assert_eq!(children.len(), 50);
883 assert_eq!(children[0].name, "key_2500");
884 assert_eq!(children[0].value, "2500");
885
886 let tail = renderer.render_children(&value, 5000, 100);
888 assert_eq!(tail.len(), 1);
889 assert_eq!(tail[0].name, "key_5000");
890 }
891
892 #[test]
894 fn test_render_blessed_object_hash_based() {
895 let renderer = PerlVariableRenderer::new();
896
897 let value = PerlValue::Object {
898 class: "HTTP::Response".to_string(),
899 value: Box::new(PerlValue::Hash(vec![
900 ("_rc".to_string(), PerlValue::Integer(200)),
901 ("_content".to_string(), PerlValue::Scalar("OK".to_string())),
902 (
903 "_headers".to_string(),
904 PerlValue::Hash(vec![(
905 "Content-Type".to_string(),
906 PerlValue::Scalar("text/html".to_string()),
907 )]),
908 ),
909 ])),
910 };
911
912 let rendered = renderer.render("$resp", &value);
913
914 assert_eq!(rendered.name, "$resp");
915 assert_eq!(rendered.type_name, Some("HTTP::Response".to_string()));
916 assert_eq!(rendered.named_variables, Some(3));
917 assert!(rendered.value.contains("HTTP::Response"));
918
919 let children = renderer.render_children(&value, 0, 10);
921 assert_eq!(children.len(), 3);
922 assert_eq!(children[0].name, "_rc");
923 assert_eq!(children[0].value, "200");
924 assert_eq!(children[1].name, "_content");
925 assert!(children[1].value.contains("OK"));
926 }
927
928 #[test]
930 fn test_render_blessed_object_array_based() {
931 let renderer = PerlVariableRenderer::new();
932
933 let value = PerlValue::Object {
934 class: "My::InsideOut".to_string(),
935 value: Box::new(PerlValue::Array(vec![
936 PerlValue::Scalar("field_a".to_string()),
937 PerlValue::Integer(99),
938 ])),
939 };
940
941 let rendered = renderer.render("$io", &value);
942
943 assert_eq!(rendered.type_name, Some("My::InsideOut".to_string()));
944 assert_eq!(rendered.indexed_variables, Some(2));
945 assert!(rendered.value.contains("My::InsideOut"));
946
947 let children = renderer.render_children(&value, 0, 10);
948 assert_eq!(children.len(), 2);
949 assert_eq!(children[0].name, "[0]");
950 assert_eq!(children[1].name, "[1]");
951 }
952
953 #[test]
955 fn test_render_blessed_object_scalar_based() {
956 let renderer = PerlVariableRenderer::new();
957
958 let value = PerlValue::Object {
960 class: "URI".to_string(),
961 value: Box::new(PerlValue::Scalar("https://example.com".to_string())),
962 };
963
964 let rendered = renderer.render("$uri", &value);
965
966 assert_eq!(rendered.type_name, Some("URI".to_string()));
967 assert_eq!(rendered.named_variables, None);
969 assert_eq!(rendered.indexed_variables, None);
970 assert_eq!(rendered.value, "URI = SCALAR(...)");
971 }
972
973 #[test]
975 fn test_render_blessed_object_deep_namespace() {
976 let renderer = PerlVariableRenderer::new();
977
978 let value = PerlValue::Object {
979 class: "Very::Deep::Nested::Package::Name".to_string(),
980 value: Box::new(PerlValue::Hash(vec![])),
981 };
982
983 let rendered = renderer.render("$obj", &value);
984
985 assert_eq!(rendered.type_name, Some("Very::Deep::Nested::Package::Name".to_string()));
986 assert!(rendered.value.contains("Very::Deep::Nested::Package::Name"));
987 assert_eq!(rendered.named_variables, Some(0));
988 }
989
990 #[test]
993 fn test_render_blessed_object_with_reference() {
994 let renderer = PerlVariableRenderer::new();
995
996 let value = PerlValue::Object {
997 class: "DBI::db".to_string(),
998 value: Box::new(PerlValue::Hash(vec![(
999 "Driver".to_string(),
1000 PerlValue::Scalar("SQLite".to_string()),
1001 )])),
1002 };
1003
1004 let rendered = renderer.render_with_reference("$dbh", &value, 99);
1005
1006 assert_eq!(rendered.variables_reference, 99);
1007 assert!(rendered.is_expandable());
1008 assert_eq!(rendered.type_name, Some("DBI::db".to_string()));
1009 }
1010
1011 #[test]
1015 fn test_render_hash_with_multiple_back_references() {
1016 let renderer = PerlVariableRenderer::new();
1017
1018 let grandparent_marker = PerlValue::Truncated {
1019 summary: "HASH(0xaaa...circular)".to_string(),
1020 total_count: Some(5),
1021 };
1022 let self_marker = PerlValue::Truncated {
1023 summary: "HASH(0xbbb...circular)".to_string(),
1024 total_count: Some(3),
1025 };
1026
1027 let value = PerlValue::Hash(vec![
1028 ("parent".to_string(), PerlValue::Reference(Box::new(grandparent_marker))),
1029 ("child".to_string(), PerlValue::Reference(Box::new(self_marker))),
1030 ("name".to_string(), PerlValue::Scalar("node".to_string())),
1031 ]);
1032
1033 let rendered = renderer.render("$node", &value);
1034
1035 assert_eq!(rendered.named_variables, Some(3));
1036 assert!(rendered.value.len() < 500);
1038
1039 let children = renderer.render_children(&value, 0, 10);
1040 assert_eq!(children.len(), 3);
1041 assert!(children[0].value.contains("circular"));
1043 assert!(children[1].value.contains("circular"));
1044 assert!(children[2].value.contains("node"));
1045 }
1046
1047 #[test]
1049 fn test_render_empty_collections() {
1050 let renderer = PerlVariableRenderer::new();
1051
1052 let empty_arr = PerlValue::Array(vec![]);
1053 let rendered = renderer.render("@empty", &empty_arr);
1054 assert_eq!(rendered.value, "[]");
1055 assert_eq!(rendered.indexed_variables, Some(0));
1056
1057 let empty_hash = PerlValue::Hash(vec![]);
1058 let rendered = renderer.render("%empty", &empty_hash);
1059 assert_eq!(rendered.value, "{}");
1060 assert_eq!(rendered.named_variables, Some(0));
1061 }
1062
1063 #[test]
1066 fn test_render_large_array_with_custom_preview_limit() {
1067 let renderer =
1068 PerlVariableRenderer::new().with_max_array_preview(1).with_max_hash_preview(1);
1069
1070 let elements: Vec<PerlValue> = (0..100).map(PerlValue::Integer).collect();
1071 let value = PerlValue::Array(elements);
1072
1073 let rendered = renderer.render("@arr", &value);
1074 assert!(rendered.value.contains("100 total"));
1076 assert!(rendered.value.starts_with("[0"));
1078 }
1079
1080 #[test]
1082 fn test_render_large_hash_with_custom_preview_limit() {
1083 let renderer = PerlVariableRenderer::new().with_max_hash_preview(1);
1084
1085 let pairs: Vec<(String, PerlValue)> =
1086 (0..100).map(|i| (format!("k{}", i), PerlValue::Integer(i))).collect();
1087 let value = PerlValue::Hash(pairs);
1088
1089 let rendered = renderer.render("%h", &value);
1090 assert!(rendered.value.contains("100 keys"));
1091 assert!(rendered.value.starts_with('{'));
1092 }
1093}