Skip to main content

gdtest_analysis/
gut_migrate.rs

1use std::path::Path;
2
3use anyhow::{Context, Result};
4use regex::Regex;
5use serde::Serialize;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
8#[serde(rename_all = "snake_case")]
9pub enum GutMigrationStatus {
10    CandidateForExecution,
11    RewriteIncomplete,
12}
13
14#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
15pub struct GutMigrationAction {
16    pub line: usize,
17    pub pattern: String,
18    pub replacement: String,
19}
20
21#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
22pub struct GutMigrationIssue {
23    pub line: usize,
24    pub pattern: String,
25    pub message: String,
26}
27
28#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
29pub struct GutMigrationOutput {
30    pub source: Vec<u8>,
31    pub changed: bool,
32    pub status: GutMigrationStatus,
33    pub rewritten: Vec<GutMigrationAction>,
34    pub blocked: Vec<GutMigrationIssue>,
35    pub manual_followups: Vec<GutMigrationIssue>,
36}
37
38pub fn is_gut_test_file(path: &Path) -> Result<bool> {
39    let source = std::fs::read(path).with_context(|| format!("reading {}", path.display()))?;
40    Ok(is_gut_test_source(&source))
41}
42
43pub fn migrate_gut_file(path: &Path, res_path: &str) -> Result<GutMigrationOutput> {
44    let source = std::fs::read(path).with_context(|| format!("reading {}", path.display()))?;
45    Ok(migrate_gut_source(&source, res_path))
46}
47
48pub fn is_gut_test_source(source: &[u8]) -> bool {
49    let text = String::from_utf8_lossy(source);
50    text.lines().any(|line| {
51        let trimmed = line.trim_start();
52        trimmed.starts_with("extends GutTest")
53            || trimmed.starts_with("extends \"res://addons/gut/test.gd\"")
54            || trimmed.starts_with("extends 'res://addons/gut/test.gd'")
55    })
56}
57
58pub fn migrate_gut_source(source: &[u8], _res_path: &str) -> GutMigrationOutput {
59    let text = String::from_utf8_lossy(source);
60    let mut rewritten = Vec::new();
61    let mut blocked = Vec::new();
62    let mut manual_followups = Vec::new();
63    let mut lines: Vec<String> = text.lines().map(str::to_string).collect();
64    let had_trailing_newline = text.ends_with('\n');
65
66    for (idx, line) in lines.iter_mut().enumerate() {
67        let line_no = idx + 1;
68        let original_line = line.clone();
69        let trimmed = original_line.trim_start();
70        if trimmed.starts_with('#') {
71            continue;
72        }
73
74        if replace_extends(line) && original_line != *line {
75            rewritten.push(GutMigrationAction {
76                line: line_no,
77                pattern: "extends GutTest".to_string(),
78                replacement: "extends GdTest".to_string(),
79            });
80        }
81
82        rewrite_constructor_chain(
83            line,
84            line_no,
85            "partial_double",
86            "partial_mock",
87            &mut rewritten,
88        );
89        rewrite_constructor_chain(line, line_no, "double", "mock", &mut rewritten);
90        rewrite_stub_to_return(line, line_no, &mut rewritten);
91        rewrite_call_name(
92            line,
93            line_no,
94            "yield_for",
95            "wait_physics_seconds",
96            &mut rewritten,
97        );
98        rewrite_call_name(
99            line,
100            line_no,
101            "yield_frames",
102            "wait_physics_frames",
103            &mut rewritten,
104        );
105        rewrite_call_name(
106            line,
107            line_no,
108            "wait_frames",
109            "wait_physics_frames",
110            &mut rewritten,
111        );
112        rewrite_wait_process_frames(line, line_no, &mut rewritten, &mut manual_followups);
113        rewrite_wait_idle_frames(line, line_no, &mut rewritten, &mut manual_followups);
114        rewrite_signal_assertions(line, line_no, &mut rewritten, &mut manual_followups);
115        rewrite_yield_to(line, line_no, &mut rewritten, &mut manual_followups);
116
117        if original_line.contains("watch_signals(") {
118            manual_followups.push(GutMigrationIssue {
119                line: line_no,
120                pattern: "watch_signals(...)".to_string(),
121                message: "watch_signals() is not auto-migrated; expand to explicit gdtest per-signal assertions by hand.".to_string(),
122            });
123        }
124        if original_line.contains("use_parameters(") {
125            blocked.push(GutMigrationIssue {
126                line: line_no,
127                pattern: "use_parameters(...)".to_string(),
128                message:
129                    "Parameterized GUT tests need manual restructuring; v1 does not rewrite them."
130                        .to_string(),
131            });
132        }
133        if original_line.contains("assert_engine_error(")
134            || original_line.contains("assert_engine_error_count(")
135        {
136            blocked.push(GutMigrationIssue {
137                line: line_no,
138                pattern: "assert_engine_error*".to_string(),
139                message: "Engine error assertions have no safe canonical gdtest rewrite in v1."
140                    .to_string(),
141            });
142        }
143        if trimmed.starts_with("func should_skip_script(") {
144            blocked.push(GutMigrationIssue {
145                line: line_no,
146                pattern: "should_skip_script()".to_string(),
147                message: "Script-level skip behavior is not auto-migrated; convert it manually."
148                    .to_string(),
149            });
150        }
151        if original_line.contains("partial_double_singleton(")
152            || original_line.contains("double_singleton(")
153        {
154            blocked.push(GutMigrationIssue {
155                line: line_no,
156                pattern: "double_singleton()/partial_double_singleton()".to_string(),
157                message:
158                    "Singleton doubles need manual migration; no safe gdtest rewrite is applied."
159                        .to_string(),
160            });
161        }
162        if original_line.contains("double(")
163            && !original_line.contains(".new()")
164            && line == &original_line
165        {
166            blocked.push(GutMigrationIssue {
167                line: line_no,
168                pattern: "double(...)".to_string(),
169                message: "Only `double(...).new()` is auto-rewritten in v1. Other double() forms need manual review.".to_string(),
170            });
171        }
172        if original_line.contains("partial_double(")
173            && !original_line.contains(".new()")
174            && line == &original_line
175        {
176            blocked.push(GutMigrationIssue {
177                line: line_no,
178                pattern: "partial_double(...)".to_string(),
179                message: "Only `partial_double(...).new()` is auto-rewritten in v1. Other partial_double() forms need manual review.".to_string(),
180            });
181        }
182        if original_line.contains("stub(")
183            && original_line.contains(".to_")
184            && line == &original_line
185        {
186            manual_followups.push(GutMigrationIssue {
187                line: line_no,
188                pattern: "stub(...).to_*".to_string(),
189                message: "This stub chain was not recognized as a safe direct-value rewrite; convert it manually.".to_string(),
190            });
191        }
192    }
193
194    dedup_issues(&mut blocked);
195    dedup_issues(&mut manual_followups);
196
197    if !blocked.is_empty() || !manual_followups.is_empty() {
198        let mut header: Vec<String> =
199            vec!["# gdtest migrate gut: manual follow-up required".to_string()];
200        for issue in blocked.iter().chain(manual_followups.iter()) {
201            header.push(format!("# line {}: {}", issue.line, issue.message));
202        }
203        header.push(String::new());
204        let mut merged = header;
205        merged.extend(lines);
206        lines = merged;
207    }
208
209    let mut rendered = lines.join("\n");
210    if had_trailing_newline {
211        rendered.push('\n');
212    }
213
214    let changed = rendered.as_bytes() != source;
215    let status = if blocked.is_empty() && manual_followups.is_empty() {
216        GutMigrationStatus::CandidateForExecution
217    } else {
218        GutMigrationStatus::RewriteIncomplete
219    };
220
221    GutMigrationOutput {
222        source: rendered.into_bytes(),
223        changed,
224        status,
225        rewritten,
226        blocked,
227        manual_followups,
228    }
229}
230
231fn replace_extends(line: &mut String) -> bool {
232    let original = line.clone();
233    *line = line.replace("extends GutTest", "extends GdTest");
234    *line = line.replace("extends \"res://addons/gut/test.gd\"", "extends GdTest");
235    *line = line.replace("extends 'res://addons/gut/test.gd'", "extends GdTest");
236    *line != original
237}
238
239fn rewrite_call_name(
240    line: &mut String,
241    line_no: usize,
242    old_name: &str,
243    new_name: &str,
244    rewritten: &mut Vec<GutMigrationAction>,
245) {
246    let mut cursor = 0usize;
247    while let Some((start, end, args)) = find_named_call(line, old_name, cursor) {
248        line.replace_range(start..end, &format!("{new_name}({args})"));
249        rewritten.push(GutMigrationAction {
250            line: line_no,
251            pattern: format!("{old_name}(...)"),
252            replacement: format!("{new_name}(...)"),
253        });
254        cursor = start + new_name.len();
255    }
256}
257
258fn rewrite_wait_process_frames(
259    line: &mut String,
260    line_no: usize,
261    rewritten: &mut Vec<GutMigrationAction>,
262    manual_followups: &mut Vec<GutMigrationIssue>,
263) {
264    rewrite_wait_single_frame(
265        line,
266        line_no,
267        "wait_process_frames",
268        "wait_process_frame",
269        rewritten,
270        manual_followups,
271    );
272}
273
274fn rewrite_wait_idle_frames(
275    line: &mut String,
276    line_no: usize,
277    rewritten: &mut Vec<GutMigrationAction>,
278    manual_followups: &mut Vec<GutMigrationIssue>,
279) {
280    rewrite_wait_single_frame(
281        line,
282        line_no,
283        "wait_idle_frames",
284        "wait_process_frame",
285        rewritten,
286        manual_followups,
287    );
288}
289
290fn rewrite_wait_single_frame(
291    line: &mut String,
292    line_no: usize,
293    old_name: &str,
294    new_name: &str,
295    rewritten: &mut Vec<GutMigrationAction>,
296    manual_followups: &mut Vec<GutMigrationIssue>,
297) {
298    let mut cursor = 0usize;
299    while let Some((start, end, args)) = find_named_call(line, old_name, cursor) {
300        let parsed = split_top_level_args(&args);
301        if parsed.len() == 1 && is_literal_one(parsed[0].as_str()) {
302            line.replace_range(start..end, &format!("{new_name}()"));
303            rewritten.push(GutMigrationAction {
304                line: line_no,
305                pattern: format!("{old_name}(1)"),
306                replacement: format!("{new_name}()"),
307            });
308            cursor = start + new_name.len();
309        } else {
310            manual_followups.push(GutMigrationIssue {
311                line: line_no,
312                pattern: format!("{old_name}(...)"),
313                message: format!("{old_name} only auto-rewrites when waiting for exactly one frame; larger counts need manual expansion."),
314            });
315            cursor = end;
316        }
317    }
318}
319
320fn rewrite_constructor_chain(
321    line: &mut String,
322    line_no: usize,
323    old_name: &str,
324    new_name: &str,
325    rewritten: &mut Vec<GutMigrationAction>,
326) {
327    let mut cursor = 0usize;
328    while let Some((call_start, call_end, args)) = find_named_call(line, old_name, cursor) {
329        let suffix = &line[call_end..];
330        let Some(new_end) = parse_dot_new_suffix(suffix).map(|offset| call_end + offset) else {
331            cursor = call_end;
332            continue;
333        };
334        line.replace_range(call_start..new_end, &format!("{new_name}({args})"));
335        rewritten.push(GutMigrationAction {
336            line: line_no,
337            pattern: format!("{old_name}(...).new()"),
338            replacement: format!("{new_name}(...)"),
339        });
340        cursor = call_start + new_name.len();
341    }
342}
343
344fn rewrite_stub_to_return(
345    line: &mut String,
346    line_no: usize,
347    rewritten: &mut Vec<GutMigrationAction>,
348) {
349    let mut cursor = 0usize;
350    while let Some((call_start, call_end, args)) = find_named_call(line, "stub", cursor) {
351        let suffix = &line[call_end..];
352        let Some((suffix_end, value)) = parse_stub_return_suffix(suffix) else {
353            cursor = call_end;
354            continue;
355        };
356        let stub_args = split_top_level_args(&args);
357        if stub_args.len() != 2 {
358            cursor = call_end;
359            continue;
360        }
361        line.replace_range(
362            call_start..(call_end + suffix_end),
363            &format!("stub({}, {}, {})", stub_args[0], stub_args[1], value.trim()),
364        );
365        rewritten.push(GutMigrationAction {
366            line: line_no,
367            pattern: "stub(...).to_return(...)".to_string(),
368            replacement: "stub(..., value)".to_string(),
369        });
370        cursor = call_start + 4;
371    }
372}
373
374fn rewrite_signal_assertions(
375    line: &mut String,
376    line_no: usize,
377    rewritten: &mut Vec<GutMigrationAction>,
378    manual_followups: &mut Vec<GutMigrationIssue>,
379) {
380    let mappings = [
381        ("assert_signal_emitted", "assert_signal", 2usize, false),
382        (
383            "assert_signal_not_emitted",
384            "assert_no_signal",
385            2usize,
386            false,
387        ),
388        (
389            "assert_signal_emit_count",
390            "assert_signal_count",
391            3usize,
392            false,
393        ),
394        (
395            "assert_signal_emitted_with_parameters",
396            "assert_signal_with_args",
397            3usize,
398            true,
399        ),
400    ];
401    for (old_name, new_name, required_args, block_extra) in mappings {
402        let mut cursor = 0usize;
403        while let Some((start, end, args)) = find_named_call(line, old_name, cursor) {
404            let parsed = split_top_level_args(&args);
405            let uses_signal_ref = parsed
406                .first()
407                .map(|arg| !looks_like_signal_object_form(arg))
408                .unwrap_or(false);
409            if uses_signal_ref {
410                manual_followups.push(GutMigrationIssue {
411                    line: line_no,
412                    pattern: format!("{old_name}(signal_ref, ...)"),
413                    message: format!("{old_name} only auto-rewrites for `(object, \"signal\")` calls; signal-reference overloads need manual conversion."),
414                });
415                cursor = end;
416                continue;
417            }
418            if parsed.len() < required_args
419                || (block_extra && parsed.len() != required_args)
420                || (!block_extra && parsed.len() > required_args)
421            {
422                manual_followups.push(GutMigrationIssue {
423                    line: line_no,
424                    pattern: format!("{old_name}(...)"),
425                    message: format!("{old_name} had optional arguments or an unsupported shape; convert it manually."),
426                });
427                cursor = end;
428                continue;
429            }
430            line.replace_range(start..end, &format!("{new_name}({})", parsed.join(", ")));
431            rewritten.push(GutMigrationAction {
432                line: line_no,
433                pattern: format!("{old_name}(...)"),
434                replacement: format!("{new_name}(...)"),
435            });
436            cursor = start + new_name.len();
437        }
438    }
439}
440
441fn rewrite_yield_to(
442    line: &mut String,
443    line_no: usize,
444    rewritten: &mut Vec<GutMigrationAction>,
445    manual_followups: &mut Vec<GutMigrationIssue>,
446) {
447    let mut cursor = 0usize;
448    while let Some((start, end, args)) = find_named_call(line, "yield_to", cursor) {
449        let parsed = split_top_level_args(&args);
450        if parsed.len() != 3
451            || !looks_like_signal_object_form(&parsed[0])
452            || !looks_like_string_literal(&parsed[1])
453        {
454            manual_followups.push(GutMigrationIssue {
455                line: line_no,
456                pattern: "yield_to(...)".to_string(),
457                message: "yield_to() only auto-rewrites for `(object, \"signal\", timeout_seconds)` calls without a message.".to_string(),
458            });
459            cursor = end;
460            continue;
461        }
462        let timeout_expr = format!("int(ceil(({}) * 1000.0))", parsed[2].trim());
463        line.replace_range(
464            start..end,
465            &format!(
466                "assert_signal({}, {}, {})",
467                parsed[0], parsed[1], timeout_expr
468            ),
469        );
470        rewritten.push(GutMigrationAction {
471            line: line_no,
472            pattern: "yield_to(obj, \"signal\", timeout_seconds)".to_string(),
473            replacement: "assert_signal(obj, \"signal\", timeout_ms)".to_string(),
474        });
475        cursor = start + "assert_signal".len();
476    }
477}
478
479fn find_named_call(line: &str, name: &str, from: usize) -> Option<(usize, usize, String)> {
480    let needle = format!("{name}(");
481    let mut search = from;
482    while let Some(found) = line[search..].find(&needle) {
483        let start = search + found;
484        if start > 0 {
485            let prev = line[..start].chars().next_back().unwrap_or(' ');
486            if prev.is_ascii_alphanumeric() || prev == '_' || prev == '.' {
487                search = start + needle.len();
488                continue;
489            }
490        }
491        let open = start + name.len();
492        let end = find_matching_paren(line, open)?;
493        return Some((start, end + 1, line[(open + 1)..end].to_string()));
494    }
495    None
496}
497
498fn find_matching_paren(text: &str, open_idx: usize) -> Option<usize> {
499    let bytes = text.as_bytes();
500    if bytes.get(open_idx) != Some(&b'(') {
501        return None;
502    }
503    let mut depth = 0usize;
504    let mut in_string: Option<u8> = None;
505    let mut escaped = false;
506    for (idx, byte) in bytes.iter().enumerate().skip(open_idx) {
507        if let Some(quote) = in_string {
508            if escaped {
509                escaped = false;
510                continue;
511            }
512            if *byte == b'\\' {
513                escaped = true;
514                continue;
515            }
516            if *byte == quote {
517                in_string = None;
518            }
519            continue;
520        }
521        match *byte {
522            b'"' | b'\'' => in_string = Some(*byte),
523            b'(' | b'[' | b'{' => depth += 1,
524            b')' => {
525                depth = depth.saturating_sub(1);
526                if depth == 0 {
527                    return Some(idx);
528                }
529            }
530            b']' | b'}' => depth = depth.saturating_sub(1),
531            _ => {}
532        }
533    }
534    None
535}
536
537fn split_top_level_args(args: &str) -> Vec<String> {
538    let mut parts = Vec::new();
539    let mut current = String::new();
540    let mut paren = 0usize;
541    let mut bracket = 0usize;
542    let mut brace = 0usize;
543    let mut in_string: Option<char> = None;
544    let mut escaped = false;
545    for ch in args.chars() {
546        if let Some(quote) = in_string {
547            current.push(ch);
548            if escaped {
549                escaped = false;
550            } else if ch == '\\' {
551                escaped = true;
552            } else if ch == quote {
553                in_string = None;
554            }
555            continue;
556        }
557        match ch {
558            '"' | '\'' => {
559                in_string = Some(ch);
560                current.push(ch);
561            }
562            '(' => {
563                paren += 1;
564                current.push(ch);
565            }
566            ')' => {
567                paren = paren.saturating_sub(1);
568                current.push(ch);
569            }
570            '[' => {
571                bracket += 1;
572                current.push(ch);
573            }
574            ']' => {
575                bracket = bracket.saturating_sub(1);
576                current.push(ch);
577            }
578            '{' => {
579                brace += 1;
580                current.push(ch);
581            }
582            '}' => {
583                brace = brace.saturating_sub(1);
584                current.push(ch);
585            }
586            ',' if paren == 0 && bracket == 0 && brace == 0 => {
587                let part = current.trim();
588                if !part.is_empty() {
589                    parts.push(part.to_string());
590                }
591                current.clear();
592            }
593            _ => current.push(ch),
594        }
595    }
596    let part = current.trim();
597    if !part.is_empty() {
598        parts.push(part.to_string());
599    }
600    parts
601}
602
603fn parse_dot_new_suffix(suffix: &str) -> Option<usize> {
604    let re = Regex::new(r"^\s*\.new\(\s*\)").ok()?;
605    re.find(suffix).map(|m| m.end())
606}
607
608fn parse_stub_return_suffix(suffix: &str) -> Option<(usize, String)> {
609    let re = Regex::new(r"^\s*\.to_return(?:_const)?\(").ok()?;
610    let m = re.find(suffix)?;
611    let open_idx = m.end() - 1;
612    let close_idx = find_matching_paren(suffix, open_idx)?;
613    Some((close_idx + 1, suffix[(open_idx + 1)..close_idx].to_string()))
614}
615
616fn looks_like_signal_object_form(arg: &str) -> bool {
617    let trimmed = arg.trim();
618    !trimmed.is_empty() && !trimmed.contains('.') && !trimmed.starts_with("Signal(")
619}
620
621fn looks_like_string_literal(arg: &str) -> bool {
622    let trimmed = arg.trim();
623    (trimmed.starts_with('"') && trimmed.ends_with('"'))
624        || (trimmed.starts_with('\'') && trimmed.ends_with('\''))
625}
626
627fn is_literal_one(arg: &str) -> bool {
628    matches!(arg.trim(), "1" | "1.0")
629}
630
631fn dedup_issues(issues: &mut Vec<GutMigrationIssue>) {
632    issues.sort_by(|a, b| a.line.cmp(&b.line).then_with(|| a.pattern.cmp(&b.pattern)));
633    issues.dedup_by(|a, b| a.line == b.line && a.pattern == b.pattern && a.message == b.message);
634}
635
636#[cfg(test)]
637mod tests {
638    use super::*;
639
640    #[test]
641    fn rewrites_basic_gut_extends_and_waits() {
642        let src = br#"extends GutTest
643
644func test_waits():
645    await wait_frames(2)
646    await yield_for(0.5)
647"#;
648        let out = migrate_gut_source(src, "res://test/test_waits.gd");
649        let text = String::from_utf8(out.source).unwrap();
650        assert!(text.contains("extends GdTest"));
651        assert!(text.contains("wait_physics_frames(2)"));
652        assert!(text.contains("wait_physics_seconds(0.5)"));
653        assert_eq!(out.status, GutMigrationStatus::CandidateForExecution);
654    }
655
656    #[test]
657    fn rewrites_signal_assertions_and_doubles() {
658        let src = br#"extends GutTest
659
660func test_signal_and_mock():
661    var m = double(HealthComponent).new()
662    stub(m, "get_hp").to_return(42)
663    assert_signal_emitted(self, "done")
664    assert_signal_emit_count(self, "done", 2)
665    assert_signal_emitted_with_parameters(self, "done", [42])
666"#;
667        let out = migrate_gut_source(src, "res://test/test_signal.gd");
668        let text = String::from_utf8(out.source).unwrap();
669        assert!(text.contains("var m = mock(HealthComponent)"));
670        assert!(text.contains("stub(m, \"get_hp\", 42)"));
671        assert!(text.contains("assert_signal(self, \"done\")"));
672        assert!(text.contains("assert_signal_count(self, \"done\", 2)"));
673        assert!(text.contains("assert_signal_with_args(self, \"done\", [42])"));
674        assert_eq!(out.status, GutMigrationStatus::CandidateForExecution);
675    }
676
677    #[test]
678    fn flags_unsupported_patterns() {
679        let src = br#"extends GutTest
680
681func should_skip_script():
682    return true
683
684func test_unsupported():
685    watch_signals(self)
686    use_parameters([1, 2, 3])
687    assert_engine_error("boom")
688    assert_signal_emitted(self.done)
689    await wait_process_frames(2)
690"#;
691        let out = migrate_gut_source(src, "res://test/test_blocked.gd");
692        let text = String::from_utf8(out.source).unwrap();
693        assert!(text.starts_with("# gdtest migrate gut: manual follow-up required"));
694        assert_eq!(out.status, GutMigrationStatus::RewriteIncomplete);
695        assert!(out
696            .blocked
697            .iter()
698            .any(|i| i.pattern == "should_skip_script()"));
699        assert!(out
700            .blocked
701            .iter()
702            .any(|i| i.pattern == "use_parameters(...)"));
703        assert!(out
704            .blocked
705            .iter()
706            .any(|i| i.pattern == "assert_engine_error*"));
707        assert!(out
708            .manual_followups
709            .iter()
710            .any(|i| i.pattern == "watch_signals(...)"));
711        assert!(out
712            .manual_followups
713            .iter()
714            .any(|i| i.pattern == "assert_signal_emitted(signal_ref, ...)"));
715        assert!(out
716            .manual_followups
717            .iter()
718            .any(|i| i.pattern == "wait_process_frames(...)"));
719    }
720
721    #[test]
722    fn rewrites_single_frame_wait_process_variants() {
723        let src = br#"extends GutTest
724
725func test_one_frame():
726    await wait_process_frames(1)
727    await wait_idle_frames(1)
728"#;
729        let out = migrate_gut_source(src, "res://test/test_process_frame.gd");
730        let text = String::from_utf8(out.source).unwrap();
731        assert!(text.contains("await wait_process_frame()"));
732        assert_eq!(text.matches("wait_process_frame()").count(), 2);
733    }
734}