1use crate::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, .. } => format!("{}=...", class),
355 PerlValue::Code { name } => {
356 name.as_ref().map_or_else(|| "CODE(...)".to_string(), |n| format!("\\&{}", n))
357 }
358 PerlValue::Glob(name) => format!("*{}", name),
359 PerlValue::Regex(pattern) => format!("qr/{}/", pattern),
360 PerlValue::Tied { class, .. } => format!("TIED({})", class),
361 PerlValue::Truncated { summary, .. } => summary.clone(),
362 PerlValue::Error(msg) => format!("<error: {}>", msg),
363 }
364 }
365
366 fn format_value(&self, value: &PerlValue) -> String {
368 match value {
369 PerlValue::Undef => "undef".to_string(),
370 PerlValue::Scalar(s) => self.format_string(s),
371 PerlValue::Number(n) => n.to_string(),
372 PerlValue::Integer(i) => i.to_string(),
373 PerlValue::Array(elements) => self.format_array_preview(elements),
374 PerlValue::Hash(pairs) => self.format_hash_preview(pairs),
375 PerlValue::Reference(inner) => {
376 let prefix = self.ref_prefix_chain(value);
377 format!("{}{}", prefix, self.format_deref_target(inner))
378 }
379 PerlValue::Object { class, value } => {
380 format!("{}={}", class, self.format_value_brief(value))
381 }
382 PerlValue::Code { name } => {
383 name.as_ref().map_or_else(|| "sub { ... }".to_string(), |n| format!("\\&{}", n))
384 }
385 PerlValue::Glob(name) => format!("*{}", name),
386 PerlValue::Regex(pattern) => format!("qr/{}/", pattern),
387 PerlValue::Tied { class, value } => {
388 if let Some(v) = value {
389 format!("TIED({}) = {}", class, self.format_value_brief(v))
390 } else {
391 format!("TIED({})", class)
392 }
393 }
394 PerlValue::Truncated { summary, total_count } => {
395 if let Some(count) = total_count {
396 format!("{} ({} total)", summary, count)
397 } else {
398 summary.clone()
399 }
400 }
401 PerlValue::Error(msg) => format!("<error: {}>", msg),
402 }
403 }
404}
405
406impl VariableRenderer for PerlVariableRenderer {
407 fn render(&self, name: &str, value: &PerlValue) -> RenderedVariable {
408 let formatted_value = self.format_value(value);
409 let type_name = value.type_name().to_string();
410
411 let mut rendered = RenderedVariable::new(name, formatted_value).with_type(type_name);
412
413 match value {
415 PerlValue::Array(elements) => {
416 rendered.indexed_variables = Some(elements.len() as i64);
417 }
418 PerlValue::Hash(pairs) => {
419 rendered.named_variables = Some(pairs.len() as i64);
420 }
421 PerlValue::Object { class, value: inner } => {
422 rendered.type_name = Some(class.clone());
423 match inner.as_ref() {
424 PerlValue::Hash(pairs) => {
425 rendered.named_variables = Some(pairs.len() as i64);
426 }
427 PerlValue::Array(elements) => {
428 rendered.indexed_variables = Some(elements.len() as i64);
429 }
430 _ => {}
431 }
432 }
433 _ => {}
434 }
435
436 rendered
437 }
438
439 fn render_with_reference(
440 &self,
441 name: &str,
442 value: &PerlValue,
443 reference_id: i64,
444 ) -> RenderedVariable {
445 let mut rendered = self.render(name, value);
446
447 if value.is_expandable() {
448 rendered.variables_reference = reference_id;
449 }
450
451 rendered
452 }
453
454 fn render_children(
455 &self,
456 value: &PerlValue,
457 start: usize,
458 count: usize,
459 ) -> Vec<RenderedVariable> {
460 match value {
461 PerlValue::Array(elements) => elements
462 .iter()
463 .enumerate()
464 .skip(start)
465 .take(count)
466 .map(|(i, v)| self.render(&format!("[{}]", i), v))
467 .collect(),
468 PerlValue::Hash(pairs) => {
469 pairs.iter().skip(start).take(count).map(|(k, v)| self.render(k, v)).collect()
470 }
471 PerlValue::Reference(inner) => {
472 vec![self.render("$_", inner)]
473 }
474 PerlValue::Object { value: inner, .. } => self.render_children(inner, start, count),
475 PerlValue::Tied { value: Some(inner), .. } => self.render_children(inner, start, count),
476 _ => vec![],
477 }
478 }
479}
480
481#[cfg(test)]
482mod tests {
483 use super::*;
484
485 #[test]
486 fn test_render_scalar() {
487 let renderer = PerlVariableRenderer::new();
488 let value = PerlValue::Scalar("hello".to_string());
489 let rendered = renderer.render("$x", &value);
490
491 assert_eq!(rendered.name, "$x");
492 assert_eq!(rendered.value, "\"hello\"");
493 assert_eq!(rendered.type_name, Some("SCALAR".to_string()));
494 assert_eq!(rendered.variables_reference, 0);
495 }
496
497 #[test]
498 fn test_render_integer() {
499 let renderer = PerlVariableRenderer::new();
500 let value = PerlValue::Integer(42);
501 let rendered = renderer.render("$n", &value);
502
503 assert_eq!(rendered.name, "$n");
504 assert_eq!(rendered.value, "42");
505 assert_eq!(rendered.type_name, Some("SCALAR".to_string()));
506 }
507
508 #[test]
509 fn test_render_array() {
510 let renderer = PerlVariableRenderer::new();
511 let value = PerlValue::Array(vec![
512 PerlValue::Integer(1),
513 PerlValue::Integer(2),
514 PerlValue::Integer(3),
515 ]);
516 let rendered = renderer.render("@arr", &value);
517
518 assert_eq!(rendered.name, "@arr");
519 assert!(rendered.value.starts_with('['));
520 assert_eq!(rendered.type_name, Some("ARRAY".to_string()));
521 assert_eq!(rendered.indexed_variables, Some(3));
522 }
523
524 #[test]
525 fn test_render_hash() {
526 let renderer = PerlVariableRenderer::new();
527 let value = PerlValue::Hash(vec![
528 ("key1".to_string(), PerlValue::Scalar("value1".to_string())),
529 ("key2".to_string(), PerlValue::Integer(42)),
530 ]);
531 let rendered = renderer.render("%hash", &value);
532
533 assert_eq!(rendered.name, "%hash");
534 assert!(rendered.value.starts_with('{'));
535 assert_eq!(rendered.type_name, Some("HASH".to_string()));
536 assert_eq!(rendered.named_variables, Some(2));
537 }
538
539 #[test]
540 fn test_render_with_reference() {
541 let renderer = PerlVariableRenderer::new();
542 let value = PerlValue::Array(vec![PerlValue::Integer(1)]);
543 let rendered = renderer.render_with_reference("@arr", &value, 42);
544
545 assert_eq!(rendered.variables_reference, 42);
546 assert!(rendered.is_expandable());
547 }
548
549 #[test]
550 fn test_render_children_array() {
551 let renderer = PerlVariableRenderer::new();
552 let value = PerlValue::Array(vec![
553 PerlValue::Integer(10),
554 PerlValue::Integer(20),
555 PerlValue::Integer(30),
556 ]);
557 let children = renderer.render_children(&value, 0, 10);
558
559 assert_eq!(children.len(), 3);
560 assert_eq!(children[0].name, "[0]");
561 assert_eq!(children[0].value, "10");
562 assert_eq!(children[1].name, "[1]");
563 assert_eq!(children[2].name, "[2]");
564 }
565
566 #[test]
567 fn test_render_children_hash() {
568 let renderer = PerlVariableRenderer::new();
569 let value = PerlValue::Hash(vec![
570 ("foo".to_string(), PerlValue::Integer(1)),
571 ("bar".to_string(), PerlValue::Integer(2)),
572 ]);
573 let children = renderer.render_children(&value, 0, 10);
574
575 assert_eq!(children.len(), 2);
576 assert_eq!(children[0].name, "foo");
577 assert_eq!(children[1].name, "bar");
578 }
579
580 #[test]
581 fn test_render_object() {
582 let renderer = PerlVariableRenderer::new();
583 let value = PerlValue::Object {
584 class: "My::Class".to_string(),
585 value: Box::new(PerlValue::Hash(vec![(
586 "attr".to_string(),
587 PerlValue::Scalar("value".to_string()),
588 )])),
589 };
590 let rendered = renderer.render("$obj", &value);
591
592 assert_eq!(rendered.name, "$obj");
593 assert!(rendered.value.contains("My::Class"));
594 assert_eq!(rendered.type_name, Some("My::Class".to_string()));
595 assert_eq!(rendered.named_variables, Some(1));
596 }
597
598 #[test]
599 fn test_string_truncation() {
600 let renderer = PerlVariableRenderer::new().with_max_string_length(10);
601 let value = PerlValue::Scalar("this is a very long string".to_string());
602 let rendered = renderer.render("$s", &value);
603
604 assert!(rendered.value.contains("..."));
605 assert!(rendered.value.len() < 30);
606 }
607
608 #[test]
609 fn test_string_escaping() {
610 let renderer = PerlVariableRenderer::new();
611 let value = PerlValue::Scalar("line1\nline2\ttab".to_string());
612 let rendered = renderer.render("$s", &value);
613
614 assert!(rendered.value.contains("\\n"));
615 assert!(rendered.value.contains("\\t"));
616 }
617
618 #[test]
619 fn test_render_undef() {
620 let renderer = PerlVariableRenderer::new();
621 let value = PerlValue::Undef;
622 let rendered = renderer.render("$x", &value);
623
624 assert_eq!(rendered.value, "undef");
625 assert_eq!(rendered.type_name, Some("undef".to_string()));
626 }
627
628 #[test]
629 fn test_render_reference() {
630 let renderer = PerlVariableRenderer::new();
631 let value = PerlValue::Reference(Box::new(PerlValue::Integer(42)));
632 let rendered = renderer.render("$ref", &value);
633
634 assert_eq!(rendered.name, "$ref");
635 assert!(rendered.value.contains("42"));
636 assert_eq!(rendered.type_name, Some("REF".to_string()));
637 }
638
639 #[test]
640 fn test_render_code() {
641 let renderer = PerlVariableRenderer::new();
642 let value = PerlValue::Code { name: Some("my_sub".to_string()) };
643 let rendered = renderer.render("$code", &value);
644
645 assert!(rendered.value.contains("my_sub"));
646 assert_eq!(rendered.type_name, Some("CODE".to_string()));
647 }
648
649 #[test]
660 fn test_render_self_referential_hash() {
661 let renderer = PerlVariableRenderer::new();
662
663 let circular_marker =
666 PerlValue::Truncated { summary: "HASH(0x...circular)".to_string(), total_count: None };
667 let value = PerlValue::Hash(vec![(
668 "a".to_string(),
669 PerlValue::Reference(Box::new(circular_marker)),
670 )]);
671
672 let rendered = renderer.render("$self", &value);
673
674 assert_eq!(rendered.name, "$self");
675 assert_eq!(rendered.type_name, Some("HASH".to_string()));
676 assert_eq!(rendered.named_variables, Some(1));
677 assert!(rendered.value.contains("circular"));
679
680 let children = renderer.render_children(&value, 0, 10);
682 assert_eq!(children.len(), 1);
683 assert_eq!(children[0].name, "a");
684 assert!(children[0].value.contains("circular"));
685 }
686
687 #[test]
692 fn test_render_deep_nesting_over_100_levels_bounded() {
693 let renderer = PerlVariableRenderer::new();
694
695 let mut value = PerlValue::Integer(42);
697 for _ in 0..150 {
698 value = PerlValue::Reference(Box::new(value));
699 }
700
701 let rendered = renderer.render("$deep", &value);
702
703 assert_eq!(rendered.name, "$deep");
704 assert_eq!(rendered.type_name, Some("REF".to_string()));
705
706 let backslash_prefix_count = rendered.value.chars().take_while(|&c| c == '\\').count();
710 assert!(
711 backslash_prefix_count <= 10,
712 "backslash prefix count {} should be <= 10",
713 backslash_prefix_count,
714 );
715 assert!(
717 rendered.value.contains("REF(...)"),
718 "deeply nested ref should contain REF(...), got: {}",
719 rendered.value,
720 );
721
722 assert!(
724 rendered.value.len() < 200,
725 "rendered value length {} should be < 200",
726 rendered.value.len(),
727 );
728 }
729
730 #[test]
732 fn test_render_reference_chain_at_depth_limit() {
733 let renderer = PerlVariableRenderer::new();
734
735 let mut value = PerlValue::Scalar("leaf".to_string());
736 for _ in 0..10 {
737 value = PerlValue::Reference(Box::new(value));
738 }
739
740 let rendered = renderer.render("$ref10", &value);
741 assert!(rendered.value.contains("leaf") || rendered.value.contains("REF(...)"));
744 assert!(rendered.value.len() < 200);
745 }
746
747 #[test]
749 fn test_render_large_array_over_10k_elements_truncates() {
750 let renderer = PerlVariableRenderer::new();
751
752 let elements: Vec<PerlValue> = (0..10_001).map(PerlValue::Integer).collect();
753 let value = PerlValue::Array(elements);
754
755 let rendered = renderer.render("@big", &value);
756
757 assert_eq!(rendered.name, "@big");
758 assert_eq!(rendered.type_name, Some("ARRAY".to_string()));
759 assert_eq!(rendered.indexed_variables, Some(10_001));
760
761 assert!(
764 rendered.value.contains("10001 total"),
765 "should show total count, got: {}",
766 rendered.value,
767 );
768 assert!(rendered.value.starts_with('['));
769 assert!(rendered.value.ends_with(']'));
770
771 assert!(
773 rendered.value.len() < 500,
774 "preview length {} should be < 500",
775 rendered.value.len(),
776 );
777 }
778
779 #[test]
781 fn test_render_children_large_array_pagination() {
782 let renderer = PerlVariableRenderer::new();
783
784 let elements: Vec<PerlValue> = (0..10_001).map(PerlValue::Integer).collect();
785 let value = PerlValue::Array(elements);
786
787 let children = renderer.render_children(&value, 5000, 100);
789 assert_eq!(children.len(), 100);
790 assert_eq!(children[0].name, "[5000]");
791 assert_eq!(children[0].value, "5000");
792 assert_eq!(children[99].name, "[5099]");
793 assert_eq!(children[99].value, "5099");
794
795 let tail = renderer.render_children(&value, 10_000, 100);
797 assert_eq!(tail.len(), 1);
798 assert_eq!(tail[0].name, "[10000]");
799 }
800
801 #[test]
803 fn test_render_large_hash_over_5k_pairs_truncates() {
804 let renderer = PerlVariableRenderer::new();
805
806 let pairs: Vec<(String, PerlValue)> =
807 (0..5_001).map(|i| (format!("key_{}", i), PerlValue::Integer(i))).collect();
808 let value = PerlValue::Hash(pairs);
809
810 let rendered = renderer.render("%big", &value);
811
812 assert_eq!(rendered.name, "%big");
813 assert_eq!(rendered.type_name, Some("HASH".to_string()));
814 assert_eq!(rendered.named_variables, Some(5_001));
815
816 assert!(
819 rendered.value.contains("5001 keys"),
820 "should show key count, got: {}",
821 rendered.value,
822 );
823 assert!(rendered.value.starts_with('{'));
824 assert!(rendered.value.ends_with('}'));
825
826 assert!(
828 rendered.value.len() < 500,
829 "preview length {} should be < 500",
830 rendered.value.len(),
831 );
832 }
833
834 #[test]
836 fn test_render_children_large_hash_pagination() {
837 let renderer = PerlVariableRenderer::new();
838
839 let pairs: Vec<(String, PerlValue)> =
840 (0..5_001).map(|i| (format!("key_{}", i), PerlValue::Integer(i))).collect();
841 let value = PerlValue::Hash(pairs);
842
843 let children = renderer.render_children(&value, 2500, 50);
845 assert_eq!(children.len(), 50);
846 assert_eq!(children[0].name, "key_2500");
847 assert_eq!(children[0].value, "2500");
848
849 let tail = renderer.render_children(&value, 5000, 100);
851 assert_eq!(tail.len(), 1);
852 assert_eq!(tail[0].name, "key_5000");
853 }
854
855 #[test]
857 fn test_render_blessed_object_hash_based() {
858 let renderer = PerlVariableRenderer::new();
859
860 let value = PerlValue::Object {
861 class: "HTTP::Response".to_string(),
862 value: Box::new(PerlValue::Hash(vec![
863 ("_rc".to_string(), PerlValue::Integer(200)),
864 ("_content".to_string(), PerlValue::Scalar("OK".to_string())),
865 (
866 "_headers".to_string(),
867 PerlValue::Hash(vec![(
868 "Content-Type".to_string(),
869 PerlValue::Scalar("text/html".to_string()),
870 )]),
871 ),
872 ])),
873 };
874
875 let rendered = renderer.render("$resp", &value);
876
877 assert_eq!(rendered.name, "$resp");
878 assert_eq!(rendered.type_name, Some("HTTP::Response".to_string()));
879 assert_eq!(rendered.named_variables, Some(3));
880 assert!(rendered.value.contains("HTTP::Response"));
881
882 let children = renderer.render_children(&value, 0, 10);
884 assert_eq!(children.len(), 3);
885 assert_eq!(children[0].name, "_rc");
886 assert_eq!(children[0].value, "200");
887 assert_eq!(children[1].name, "_content");
888 assert!(children[1].value.contains("OK"));
889 }
890
891 #[test]
893 fn test_render_blessed_object_array_based() {
894 let renderer = PerlVariableRenderer::new();
895
896 let value = PerlValue::Object {
897 class: "My::InsideOut".to_string(),
898 value: Box::new(PerlValue::Array(vec![
899 PerlValue::Scalar("field_a".to_string()),
900 PerlValue::Integer(99),
901 ])),
902 };
903
904 let rendered = renderer.render("$io", &value);
905
906 assert_eq!(rendered.type_name, Some("My::InsideOut".to_string()));
907 assert_eq!(rendered.indexed_variables, Some(2));
908 assert!(rendered.value.contains("My::InsideOut"));
909
910 let children = renderer.render_children(&value, 0, 10);
911 assert_eq!(children.len(), 2);
912 assert_eq!(children[0].name, "[0]");
913 assert_eq!(children[1].name, "[1]");
914 }
915
916 #[test]
918 fn test_render_blessed_object_scalar_based() {
919 let renderer = PerlVariableRenderer::new();
920
921 let value = PerlValue::Object {
923 class: "URI".to_string(),
924 value: Box::new(PerlValue::Scalar("https://example.com".to_string())),
925 };
926
927 let rendered = renderer.render("$uri", &value);
928
929 assert_eq!(rendered.type_name, Some("URI".to_string()));
930 assert_eq!(rendered.named_variables, None);
932 assert_eq!(rendered.indexed_variables, None);
933 assert!(rendered.value.contains("URI"));
934 assert!(rendered.value.contains("https://example.com"));
935 }
936
937 #[test]
939 fn test_render_blessed_object_deep_namespace() {
940 let renderer = PerlVariableRenderer::new();
941
942 let value = PerlValue::Object {
943 class: "Very::Deep::Nested::Package::Name".to_string(),
944 value: Box::new(PerlValue::Hash(vec![])),
945 };
946
947 let rendered = renderer.render("$obj", &value);
948
949 assert_eq!(rendered.type_name, Some("Very::Deep::Nested::Package::Name".to_string()),);
950 assert!(rendered.value.contains("Very::Deep::Nested::Package::Name"));
951 assert_eq!(rendered.named_variables, Some(0));
952 }
953
954 #[test]
957 fn test_render_blessed_object_with_reference() {
958 let renderer = PerlVariableRenderer::new();
959
960 let value = PerlValue::Object {
961 class: "DBI::db".to_string(),
962 value: Box::new(PerlValue::Hash(vec![(
963 "Driver".to_string(),
964 PerlValue::Scalar("SQLite".to_string()),
965 )])),
966 };
967
968 let rendered = renderer.render_with_reference("$dbh", &value, 99);
969
970 assert_eq!(rendered.variables_reference, 99);
971 assert!(rendered.is_expandable());
972 assert_eq!(rendered.type_name, Some("DBI::db".to_string()));
973 }
974
975 #[test]
979 fn test_render_hash_with_multiple_back_references() {
980 let renderer = PerlVariableRenderer::new();
981
982 let grandparent_marker = PerlValue::Truncated {
983 summary: "HASH(0xaaa...circular)".to_string(),
984 total_count: Some(5),
985 };
986 let self_marker = PerlValue::Truncated {
987 summary: "HASH(0xbbb...circular)".to_string(),
988 total_count: Some(3),
989 };
990
991 let value = PerlValue::Hash(vec![
992 ("parent".to_string(), PerlValue::Reference(Box::new(grandparent_marker))),
993 ("child".to_string(), PerlValue::Reference(Box::new(self_marker))),
994 ("name".to_string(), PerlValue::Scalar("node".to_string())),
995 ]);
996
997 let rendered = renderer.render("$node", &value);
998
999 assert_eq!(rendered.named_variables, Some(3));
1000 assert!(rendered.value.len() < 500);
1002
1003 let children = renderer.render_children(&value, 0, 10);
1004 assert_eq!(children.len(), 3);
1005 assert!(children[0].value.contains("circular"));
1007 assert!(children[1].value.contains("circular"));
1008 assert!(children[2].value.contains("node"));
1009 }
1010
1011 #[test]
1013 fn test_render_empty_collections() {
1014 let renderer = PerlVariableRenderer::new();
1015
1016 let empty_arr = PerlValue::Array(vec![]);
1017 let rendered = renderer.render("@empty", &empty_arr);
1018 assert_eq!(rendered.value, "[]");
1019 assert_eq!(rendered.indexed_variables, Some(0));
1020
1021 let empty_hash = PerlValue::Hash(vec![]);
1022 let rendered = renderer.render("%empty", &empty_hash);
1023 assert_eq!(rendered.value, "{}");
1024 assert_eq!(rendered.named_variables, Some(0));
1025 }
1026
1027 #[test]
1030 fn test_render_large_array_with_custom_preview_limit() {
1031 let renderer =
1032 PerlVariableRenderer::new().with_max_array_preview(1).with_max_hash_preview(1);
1033
1034 let elements: Vec<PerlValue> = (0..100).map(PerlValue::Integer).collect();
1035 let value = PerlValue::Array(elements);
1036
1037 let rendered = renderer.render("@arr", &value);
1038 assert!(rendered.value.contains("100 total"));
1040 assert!(rendered.value.starts_with("[0"));
1042 }
1043
1044 #[test]
1046 fn test_render_large_hash_with_custom_preview_limit() {
1047 let renderer = PerlVariableRenderer::new().with_max_hash_preview(1);
1048
1049 let pairs: Vec<(String, PerlValue)> =
1050 (0..100).map(|i| (format!("k{}", i), PerlValue::Integer(i))).collect();
1051 let value = PerlValue::Hash(pairs);
1052
1053 let rendered = renderer.render("%h", &value);
1054 assert!(rendered.value.contains("100 keys"));
1055 assert!(rendered.value.starts_with('{'));
1056 }
1057}