tree_sitter_stack_graphs/
test.rs

1// -*- coding: utf-8 -*-
2// ------------------------------------------------------------------------------------------------
3// Copyright © 2022, stack-graphs authors.
4// Licensed under either of Apache License, Version 2.0, or MIT license, at your option.
5// Please see the LICENSE-APACHE or LICENSE-MIT files in this distribution for license details.
6// ------------------------------------------------------------------------------------------------
7
8//! Defines a test file format for stack graph resolution.
9//!
10//! ## Assertions
11//!
12//! Test files are source files in the language under test with assertions added in comments.
13//! Assertions indicate the position of a reference or definition with a carrot `^` in the
14//! source, and specify comma separated expected values.
15//!
16//! An example test for Python might be defined in `test.py` and look as follows:
17//!
18//! ``` skip
19//! foo = 42
20//! # ^ defines: foo
21//! print(foo, bar)
22//! #     ^ refers: foo
23//! #     ^ defined: 1
24//! #          ^ refers: bar
25//! #          ^ defined:
26//! ```
27//!
28//! Consecutive lines with assertions all apply to the last source line without an assertion.
29//! In the example, both assertions refer to positions on line 3.
30//!
31//! The following assertions are supported:
32//!
33//!  - `defined`: takes a comma-separated list of line numbers, and expects a reference at this
34//!    position to resolves to definitions on those lines.
35//!  - `defines`: takes a comma-separated list of names, and expects definitions at this position
36//!    with the given names.
37//!  - `refers`: takes a comma-separated list of names, and expects references at this position
38//!    with the given names.
39//!
40//! ## Fragments for multi-file testing
41//!
42//! Test files may also consist of multiple fragments, which are treated as separate files in the
43//! stack graph. An example test that simulates two different Python files:
44//!
45//! ``` skip
46//! # --- path: one.py ---
47//! x = 42
48//! y = -1
49//! # --- path: one.py ---
50//! print(x, y)
51//! #     ^ defined: 2
52//! #        ^ defined: 3
53//! ```
54//!
55//! Note that the line numbers still refer to lines in the complete test file, and are not relative
56//! to a fragment.
57//!
58//! Any content before the first fragment header of the file is ignored, and will not be part of the test.
59
60use itertools::Itertools;
61use lsp_positions::Position;
62use lsp_positions::PositionedSubstring;
63use lsp_positions::SpanCalculator;
64use once_cell::sync::Lazy;
65use regex::Regex;
66use stack_graphs::arena::Handle;
67use stack_graphs::assert::Assertion;
68use stack_graphs::assert::AssertionError;
69use stack_graphs::assert::AssertionSource;
70use stack_graphs::assert::AssertionTarget;
71use stack_graphs::graph::File;
72use stack_graphs::graph::Node;
73use stack_graphs::graph::SourceInfo;
74use stack_graphs::graph::StackGraph;
75use stack_graphs::partial::PartialPaths;
76use stack_graphs::stitching::Database;
77use stack_graphs::stitching::StitcherConfig;
78use std::collections::HashMap;
79use std::path::Path;
80use std::path::PathBuf;
81use thiserror::Error;
82use tree_sitter_graph::Variables;
83
84use crate::CancellationFlag;
85
86const DEFINED: &'static str = "defined";
87const DEFINES: &'static str = "defines";
88const REFERS: &'static str = "refers";
89
90static PATH_REGEX: Lazy<Regex> =
91    Lazy::new(|| Regex::new(r#"---\s*path:\s*([^\s]+)\s*---"#).unwrap());
92static GLOBAL_REGEX: Lazy<Regex> =
93    Lazy::new(|| Regex::new(r#"---\s*global:\s*([^\s]+)=([^\s]+)\s*---"#).unwrap());
94static ASSERTION_REGEX: Lazy<Regex> =
95    Lazy::new(|| Regex::new(r#"(\^)\s*(\w+):\s*([^\s,]+(?:\s*,\s*[^\s,]+)*)?"#).unwrap());
96static LINE_NUMBER_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r#"\d+"#).unwrap());
97static NAME_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r#"[^\s,]+"#).unwrap());
98
99/// An error that can occur while parsing tests
100#[derive(Debug, Error)]
101pub enum TestError {
102    AssertionRefersToNonSourceLine(usize),
103    DuplicateGlobalVariable(usize, String),
104    DuplicatePath(usize, String),
105    GlobalBeforeFirstFragment(usize),
106    InvalidAssertion(usize, String),
107    InvalidColumn(usize, usize, usize),
108}
109
110impl std::fmt::Display for TestError {
111    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
112        match self {
113            Self::AssertionRefersToNonSourceLine(line) => {
114                write!(
115                    f,
116                    "Assertion on line {} refers to non-source line",
117                    line + 1
118                )
119            }
120            Self::DuplicateGlobalVariable(line, global) => {
121                write!(
122                    f,
123                    "Duplicate global variable {} on line {}",
124                    global,
125                    line + 1
126                )
127            }
128            Self::DuplicatePath(line, path) => {
129                write!(f, "Duplicate path {} on line {}", path, line + 1)
130            }
131            Self::GlobalBeforeFirstFragment(line) => {
132                write!(f, "Global set before first fragment on line {}", line + 1)
133            }
134            Self::InvalidAssertion(line, assertion) => {
135                write!(f, "Invalid assertion {} on line {}", assertion, line + 1)
136            }
137            Self::InvalidColumn(line, column, regular_line) => write!(
138                f,
139                "Assertion on line {} refers to missing column {} on line {}",
140                line + 1,
141                column + 1,
142                regular_line + 1
143            ),
144        }
145    }
146}
147
148/// A stack graph test
149pub struct Test {
150    pub path: PathBuf,
151    pub fragments: Vec<TestFragment>,
152    pub graph: StackGraph,
153}
154
155/// A fragment from a stack graph test
156#[derive(Debug, Clone)]
157pub struct TestFragment {
158    pub file: Handle<File>,
159    pub path: PathBuf,
160    pub source: String,
161    pub assertions: Vec<Assertion>,
162    pub globals: HashMap<String, String>,
163}
164
165impl Test {
166    /// Creates a test from source. If the test contains no `path` sections,
167    /// the default fragment path is used for the test's single test fragment.
168    pub fn from_source(
169        path: &Path,
170        source: &str,
171        default_fragment_path: &Path,
172    ) -> Result<Self, TestError> {
173        let mut graph = StackGraph::new();
174        let mut fragments = Vec::new();
175        let mut have_fragments = false;
176        let mut current_path = default_fragment_path.to_path_buf();
177        let mut current_source = String::new();
178        let mut current_globals = HashMap::new();
179        let mut have_globals = false;
180        let mut prev_source = String::new();
181        let mut line_files = Vec::new();
182        let mut line_count = 0;
183        for (current_line_number, current_line) in
184            PositionedSubstring::lines_iter(source).enumerate()
185        {
186            line_count += 1;
187            if let Some(m) = PATH_REGEX.captures_iter(current_line.content).next() {
188                // in a test with fragments, any content before the first fragment is
189                // ignored, so that the file name of the test does not interfere with
190                // the file names of the fragments
191                if have_fragments {
192                    let file = graph
193                        .add_file(&current_path.to_string_lossy())
194                        .map_err(|_| {
195                            TestError::DuplicatePath(
196                                line_files.len(),
197                                format!("{}", current_path.display()),
198                            )
199                        })?;
200                    (line_files.len()..current_line_number)
201                        .for_each(|_| line_files.push(Some(file)));
202                    fragments.push(TestFragment {
203                        file,
204                        path: current_path,
205                        source: current_source,
206                        assertions: Vec::new(),
207                        globals: current_globals,
208                    });
209                } else {
210                    if have_globals {
211                        return Err(TestError::GlobalBeforeFirstFragment(current_line_number));
212                    }
213                    have_fragments = true;
214                    (line_files.len()..current_line_number).for_each(|_| line_files.push(None));
215                }
216                current_path = m.get(1).unwrap().as_str().into();
217                current_source = prev_source.clone();
218                current_globals = HashMap::new();
219
220                Self::push_whitespace_for(&current_line, &mut current_source);
221            } else if let Some(m) = GLOBAL_REGEX.captures_iter(current_line.content).next() {
222                have_globals = true;
223                let global_name = m.get(1).unwrap().as_str();
224                let global_value = m.get(2).unwrap().as_str();
225                if current_globals
226                    .insert(global_name.into(), global_value.into())
227                    .is_some()
228                {
229                    return Err(TestError::DuplicateGlobalVariable(
230                        current_line_number,
231                        global_name.to_string(),
232                    ));
233                }
234
235                Self::push_whitespace_for(&current_line, &mut current_source);
236            } else {
237                current_source.push_str(current_line.content);
238            }
239            current_source.push_str("\n");
240
241            Self::push_whitespace_for(&current_line, &mut prev_source);
242            prev_source.push_str("\n");
243        }
244        {
245            let file = graph
246                .add_file(&current_path.to_string_lossy())
247                .map_err(|_| {
248                    TestError::DuplicatePath(
249                        line_files.len(),
250                        format!("{}", current_path.display()),
251                    )
252                })?;
253            (line_files.len()..line_count).for_each(|_| line_files.push(Some(file)));
254            fragments.push(TestFragment {
255                file,
256                path: current_path,
257                source: current_source,
258                assertions: Vec::new(),
259                globals: current_globals,
260            });
261        }
262
263        for fragment in &mut fragments {
264            fragment
265                .parse_assertions(&mut graph, |line| line_files.get(line).cloned().flatten())?;
266        }
267
268        Ok(Self {
269            path: path.to_path_buf(),
270            fragments,
271            graph,
272        })
273    }
274
275    /// Pushes whitespace equivalent to the given line into the string.
276    /// This is used to "erase" preceding content in multi-file test.
277    /// It is implemented as pushing as many SPACE-s as there are code
278    /// units in the original line.
279    /// * This preserves global UTF-8 positions, i.e., a UTF-8 position in a
280    ///   test file source points to the same thing in the original source
281    ///   and vice versa. However, global UTF-16 and grapheme positions are
282    ///   not preserved.
283    /// * Line numbers are preserved, i.e., a line in the test file source
284    ///   has the same line number as in the overall source, and vice versa.
285    /// * Positions within "erased" lines are not preserved, regardless of
286    ///   whether they are UTF-8, UTF-16, or grapheme positions. Positions
287    ///   in the actual content are preserved between the test file source and
288    ///   the test source.
289    fn push_whitespace_for(line: &PositionedSubstring, into: &mut String) {
290        (0..line.utf8_bounds.end).for_each(|_| into.push_str(" "));
291    }
292}
293
294impl TestFragment {
295    /// Parse assertions in the source.
296    fn parse_assertions<F>(&mut self, graph: &mut StackGraph, line_file: F) -> Result<(), TestError>
297    where
298        F: Fn(usize) -> Option<Handle<File>>,
299    {
300        self.assertions.clear();
301
302        let mut current_line_span_calculator = SpanCalculator::new(&self.source);
303        let mut last_regular_line: Option<PositionedSubstring> = None;
304        let mut last_regular_line_number = None;
305        let mut last_regular_line_span_calculator = SpanCalculator::new(&self.source);
306        for (current_line_number, current_line) in
307            PositionedSubstring::lines_iter(&self.source).enumerate()
308        {
309            if let Some(m) = ASSERTION_REGEX.captures_iter(current_line.content).next() {
310                // assertion line
311                let last_regular_line = last_regular_line.as_ref().ok_or_else(|| {
312                    TestError::AssertionRefersToNonSourceLine(current_line_number)
313                })?;
314                let last_regular_line_number = last_regular_line_number.unwrap();
315
316                let carret_match = m.get(1).unwrap();
317                let assertion_match = m.get(2).unwrap();
318                let values_match = m.get(3);
319
320                let column_utf8_offset = carret_match.start();
321                let column_grapheme_offset = current_line_span_calculator
322                    .for_line_and_column(
323                        current_line_number,
324                        current_line.utf8_bounds.start,
325                        column_utf8_offset,
326                    )
327                    .column
328                    .grapheme_offset;
329                if column_grapheme_offset >= last_regular_line.grapheme_length {
330                    return Err(TestError::InvalidColumn(
331                        current_line_number,
332                        column_grapheme_offset,
333                        last_regular_line_number,
334                    ));
335                }
336                let position = last_regular_line_span_calculator.for_line_and_grapheme(
337                    last_regular_line_number,
338                    last_regular_line.utf8_bounds.start,
339                    column_grapheme_offset,
340                );
341                let source = AssertionSource {
342                    file: self.file,
343                    position,
344                };
345
346                match assertion_match.as_str() {
347                    DEFINED => {
348                        let mut targets = Vec::new();
349                        for line in LINE_NUMBER_REGEX
350                            .find_iter(values_match.map(|m| m.as_str()).unwrap_or(""))
351                        {
352                            let line = line.as_str().parse::<usize>().unwrap() - 1;
353                            let file = line_file(line).ok_or(
354                                TestError::AssertionRefersToNonSourceLine(current_line_number),
355                            )?;
356                            targets.push(AssertionTarget { file, line });
357                        }
358                        self.assertions.push(Assertion::Defined { source, targets });
359                    }
360                    DEFINES => {
361                        let mut symbols = Vec::new();
362                        for name in
363                            NAME_REGEX.find_iter(values_match.map(|m| m.as_str()).unwrap_or(""))
364                        {
365                            let symbol = graph.add_symbol(name.as_str());
366                            symbols.push(symbol);
367                        }
368                        self.assertions.push(Assertion::Defines { source, symbols });
369                    }
370                    REFERS => {
371                        let mut symbols = Vec::new();
372                        for name in
373                            NAME_REGEX.find_iter(values_match.map(|m| m.as_str()).unwrap_or(""))
374                        {
375                            let symbol = graph.add_symbol(name.as_str());
376                            symbols.push(symbol);
377                        }
378                        self.assertions.push(Assertion::Refers { source, symbols });
379                    }
380                    _ => {
381                        return Err(TestError::InvalidAssertion(
382                            current_line_number,
383                            assertion_match.as_str().to_string(),
384                        ));
385                    }
386                }
387            } else {
388                // regular source line
389                last_regular_line = Some(current_line);
390                last_regular_line_number = Some(current_line_number);
391            }
392        }
393
394        Ok(())
395    }
396}
397
398/// Result of running a stack graph test.
399#[derive(Debug, Clone)]
400pub struct TestResult {
401    success_count: usize,
402    failures: Vec<TestFailure>,
403}
404
405impl TestResult {
406    pub fn new() -> Self {
407        Self {
408            failures: Vec::new(),
409            success_count: 0,
410        }
411    }
412
413    fn add_success(&mut self) {
414        self.success_count += 1;
415    }
416
417    fn add_failure(&mut self, reason: TestFailure) {
418        self.failures.push(reason);
419    }
420
421    /// Number of successfull assertions.
422    pub fn success_count(&self) -> usize {
423        self.success_count
424    }
425
426    /// Number of failed assertions.
427    pub fn failure_count(&self) -> usize {
428        self.failures.len()
429    }
430
431    pub fn failures_iter(&self) -> std::slice::Iter<'_, TestFailure> {
432        self.failures.iter()
433    }
434
435    pub fn into_failures_iter(self) -> std::vec::IntoIter<TestFailure> {
436        self.failures.into_iter()
437    }
438
439    /// Total number of assertions that were run.
440    pub fn count(&self) -> usize {
441        self.success_count() + self.failure_count()
442    }
443
444    pub fn absorb(&mut self, other: TestResult) {
445        self.success_count += other.success_count;
446        let mut failures = other.failures;
447        self.failures.append(&mut failures);
448    }
449}
450
451impl std::fmt::Display for TestResult {
452    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
453        write!(
454            f,
455            "{} tests: {} passed, {} failed",
456            self.count(),
457            self.success_count(),
458            self.failure_count()
459        )
460    }
461}
462
463/// Description of test failures.
464// This mirrors AssertionError, but provides cleaner error messages. The underlying
465// assertions report errors in terms of the virtual files in the test. This type
466// ensures errors are reported in terms of locations in the original test file.
467// This makes errors clickable in e.g. the VS Code console, improving the developer
468// experience.
469#[derive(Debug, Clone)]
470pub enum TestFailure {
471    NoReferences {
472        path: PathBuf,
473        position: Position,
474    },
475    IncorrectResolutions {
476        path: PathBuf,
477        position: Position,
478        references: Vec<String>,
479        missing_lines: Vec<usize>,
480        unexpected_lines: HashMap<String, Vec<Option<usize>>>,
481    },
482    IncorrectDefinitions {
483        path: PathBuf,
484        position: Position,
485        missing_symbols: Vec<String>,
486        unexpected_symbols: Vec<String>,
487    },
488    IncorrectReferences {
489        path: PathBuf,
490        position: Position,
491        missing_symbols: Vec<String>,
492        unexpected_symbols: Vec<String>,
493    },
494    Cancelled(stack_graphs::CancellationError),
495}
496
497impl std::fmt::Display for TestFailure {
498    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
499        match self {
500            Self::NoReferences { path, position } => {
501                write!(
502                    f,
503                    "{}:{}:{}: no references found",
504                    path.display(),
505                    position.line + 1,
506                    position.column.grapheme_offset + 1
507                )
508            }
509            Self::IncorrectResolutions {
510                path,
511                position,
512                references,
513                missing_lines,
514                unexpected_lines,
515            } => {
516                write!(
517                    f,
518                    "{}:{}:{}: ",
519                    path.display(),
520                    position.line + 1,
521                    position.column.grapheme_offset + 1
522                )?;
523                write!(f, "definition(s) for reference(s)")?;
524                for reference in references {
525                    write!(f, " ‘{}’", reference)?;
526                }
527                if !missing_lines.is_empty() {
528                    write!(
529                        f,
530                        " missing expected on line(s) {}",
531                        missing_lines.iter().map(|l| l + 1).format(", ")
532                    )?;
533                }
534                if !unexpected_lines.is_empty() {
535                    write!(f, " found unexpected",)?;
536                    let mut first = true;
537                    for (definition, lines) in unexpected_lines.into_iter() {
538                        if first {
539                            first = false;
540                        } else {
541                            write!(f, ",")?;
542                        }
543                        write!(f, " ‘{}’ on lines(s) ", definition)?;
544                        write!(
545                            f,
546                            "{}",
547                            lines
548                                .into_iter()
549                                .map(|l| l.map(|l| format!("{}", l + 1)).unwrap_or("?".into()))
550                                .format(", ")
551                        )?;
552                    }
553                }
554                Ok(())
555            }
556            Self::IncorrectDefinitions {
557                path,
558                position,
559                missing_symbols,
560                unexpected_symbols,
561            } => {
562                write!(
563                    f,
564                    "{}:{}:{}: definitions",
565                    path.display(),
566                    position.line + 1,
567                    position.column.grapheme_offset + 1
568                )?;
569                if !missing_symbols.is_empty() {
570                    write!(
571                        f,
572                        " missing expected {}",
573                        missing_symbols.iter().format(", ")
574                    )?;
575                }
576                if !unexpected_symbols.is_empty() {
577                    write!(
578                        f,
579                        " found unexpected {}",
580                        unexpected_symbols.iter().format(", ")
581                    )?;
582                }
583                Ok(())
584            }
585            Self::IncorrectReferences {
586                path,
587                position,
588                missing_symbols,
589                unexpected_symbols,
590            } => {
591                write!(
592                    f,
593                    "{}:{}:{}: references",
594                    path.display(),
595                    position.line + 1,
596                    position.column.grapheme_offset + 1
597                )?;
598                if !missing_symbols.is_empty() {
599                    write!(
600                        f,
601                        " missing expected {}",
602                        missing_symbols.iter().format(", ")
603                    )?;
604                }
605                if !unexpected_symbols.is_empty() {
606                    write!(
607                        f,
608                        " found unexpected {}",
609                        unexpected_symbols.iter().format(", ")
610                    )?;
611                }
612                Ok(())
613            }
614            Self::Cancelled(err) => write!(f, "{}", err),
615        }
616    }
617}
618
619impl Test {
620    /// Run the test. It is the responsibility of the caller to ensure that
621    /// the stack graph for the test fragments has been constructed, and the
622    /// database has been filled with partial paths before running the test.
623    pub fn run(
624        &mut self,
625        partials: &mut PartialPaths,
626        db: &mut Database,
627        stitcher_config: StitcherConfig,
628        cancellation_flag: &dyn CancellationFlag,
629    ) -> Result<TestResult, stack_graphs::CancellationError> {
630        let mut result = TestResult::new();
631        for fragment in &self.fragments {
632            for assertion in &fragment.assertions {
633                match assertion
634                    .run(
635                        &self.graph,
636                        partials,
637                        db,
638                        stitcher_config,
639                        &cancellation_flag,
640                    )
641                    .map_or_else(|e| self.from_error(e), |v| Ok(v))
642                {
643                    Ok(_) => result.add_success(),
644                    Err(f) => result.add_failure(f),
645                }
646            }
647        }
648        Ok(result)
649    }
650
651    /// Construct a TestFailure from an AssertionError.
652    fn from_error(&self, err: AssertionError) -> Result<(), TestFailure> {
653        match err {
654            AssertionError::NoReferences { source } => Err(TestFailure::NoReferences {
655                path: self.path.clone(),
656                position: source.position,
657            }),
658            AssertionError::IncorrectlyDefined {
659                source,
660                references,
661                missing_targets,
662                unexpected_paths,
663            } => {
664                let references = references
665                    .into_iter()
666                    .map(|r| self.graph[self.graph[r].symbol().unwrap()].to_string())
667                    .unique()
668                    .sorted()
669                    .collect();
670                let missing_lines = missing_targets
671                    .into_iter()
672                    .map(|t| t.line)
673                    .unique()
674                    .sorted()
675                    .collect::<Vec<_>>();
676                let unexpected_lines = unexpected_paths
677                    .into_iter()
678                    .filter(|p| {
679                        // ignore results outside of this test, which may be include files or builtins
680                        self.fragments
681                            .iter()
682                            .any(|f| f.file == self.graph[p.end_node].id().file().unwrap())
683                    })
684                    .map(|p| {
685                        let symbol =
686                            self.graph[self.graph[p.end_node].symbol().unwrap()].to_string();
687                        let line = self
688                            .get_source_info(p.end_node)
689                            .map(|si| si.span.start.line);
690                        (symbol, line)
691                    })
692                    .unique()
693                    .sorted()
694                    .into_group_map();
695                if missing_lines.is_empty() && unexpected_lines.is_empty() {
696                    return Ok(());
697                }
698                Err(TestFailure::IncorrectResolutions {
699                    path: self.path.clone(),
700                    position: source.position,
701                    references,
702                    missing_lines,
703                    unexpected_lines,
704                })
705            }
706            AssertionError::IncorrectDefinitions {
707                source,
708                missing_symbols,
709                unexpected_symbols,
710            } => {
711                let missing_symbols = missing_symbols
712                    .iter()
713                    .map(|s| self.graph[*s].to_string())
714                    .collect::<Vec<_>>();
715                let unexpected_symbols = unexpected_symbols
716                    .iter()
717                    .map(|s| self.graph[*s].to_string())
718                    .collect::<Vec<_>>();
719                Err(TestFailure::IncorrectDefinitions {
720                    path: self.path.clone(),
721                    position: source.position,
722                    missing_symbols,
723                    unexpected_symbols,
724                })
725            }
726            AssertionError::IncorrectReferences {
727                source,
728                missing_symbols,
729                unexpected_symbols,
730            } => {
731                let missing_symbols = missing_symbols
732                    .iter()
733                    .map(|s| self.graph[*s].to_string())
734                    .collect::<Vec<_>>();
735                let unexpected_symbols = unexpected_symbols
736                    .iter()
737                    .map(|s| self.graph[*s].to_string())
738                    .collect::<Vec<_>>();
739                Err(TestFailure::IncorrectReferences {
740                    path: self.path.clone(),
741                    position: source.position,
742                    missing_symbols,
743                    unexpected_symbols,
744                })
745            }
746            AssertionError::Cancelled(err) => Err(TestFailure::Cancelled(err)),
747        }
748    }
749
750    /// Get source info for a node, using a heuristic to rule default null source info results.
751    fn get_source_info(&self, node: Handle<Node>) -> Option<&SourceInfo> {
752        self.graph.source_info(node).filter(|si| {
753            !(si.span.start.line == 0
754                && si.span.start.column.utf8_offset == 0
755                && si.span.end.line == 0
756                && si.span.end.column.utf8_offset == 0)
757        })
758    }
759}
760
761impl TestFragment {
762    pub fn add_globals_to(&self, variables: &mut Variables) {
763        for (name, value) in self.globals.iter() {
764            variables
765                .add(name.as_str().into(), value.as_str().into())
766                .unwrap();
767        }
768    }
769}