Repository: prefix-dev/rattler-build (rust)

Issue:
should `load_from_file` file path relative to recipe.yaml instead of cwd?
We currently load file with cwd as base path, maybe it's better to be relative to recipe.yaml instead of cwd?

This will make rattler-build free to be executed from any path.

similar problem on pixi: https://github.com/prefix-dev/pixi/issues/2615

Pull request:
feat: Add support for loading files relative to recipe path
closes #1540

Patch:
diff --git a/Cargo.toml b/Cargo.toml
index f7b2f4cf..e875f975 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -195,6 +195,10 @@ criterion = { version = "0.5.1", features = ["html_reports"] }
 name = "line_breaks"
 harness = false
 
+[[bench]]
+name = "parse_file"
+harness = false
+
 [patch.crates-io]
 # rattler = { git = "https://github.com/wolfv/rattler", branch = "pub-schema" }
 # rattler_sandbox = { git = "https://github.com/wolfv/rattler", branch = "pub-schema" }
diff --git a/benches/parse_file.rs b/benches/parse_file.rs
new file mode 100644
index 00000000..022c1893
--- /dev/null
+++ b/benches/parse_file.rs
@@ -0,0 +1,304 @@
+use anyhow::Result;
+use criterion::{Criterion, Throughput, black_box, criterion_group, criterion_main};
+use fs_err as fs;
+use memmap2::Mmap;
+use minijinja::Value as MiniJinjaValue;
+use serde_json::Value as JsonValue;
+use serde_yaml::Value as YamlValue;
+use std::io::BufReader;
+use std::path::Path;
+
+// Parse YAML from reader
+fn parse_yaml_from_reader(file_path: &Path) -> Result<YamlValue> {
+    let file = fs::File::open(file_path)?;
+    let reader = BufReader::new(file);
+    Ok(serde_yaml::from_reader(reader)?)
+}
+
+// Parse YAML from string
+fn parse_yaml_from_string(file_path: &Path) -> Result<YamlValue> {
+    let content = fs::read_to_string(file_path)?;
+    Ok(serde_yaml::from_str(&content)?)
+}
+
+// Parse JSON from reader
+fn parse_json_from_reader(file_path: &Path) -> Result<JsonValue> {
+    let file = fs::File::open(file_path)?;
+    let reader = BufReader::new(file);
+    Ok(serde_json::from_reader(reader)?)
+}
+
+// Parse JSON from string
+fn parse_json_from_string(file_path: &Path) -> Result<JsonValue> {
+    let content = fs::read_to_string(file_path)?;
+    Ok(serde_json::from_str(&content)?)
+}
+
+// Helper function to determine the file format based on the file extension
+fn get_file_format(file_path: &Path) -> &'static str {
+    file_path
+        .extension()
+        .and_then(|ext| ext.to_str())
+        .map(|ext| match ext.to_lowercase().as_str() {
+            "yaml" | "yml" => "yaml",
+            "json" => "json",
+            _ => "unknown",
+        })
+        .unwrap_or("unknown")
+}
+
+// Read and parse the file based on its format
+fn read_and_parse_file(file_path: &Path) -> Result<MiniJinjaValue> {
+    let file = fs::File::open(file_path)?;
+    let reader = BufReader::new(file);
+
+    match get_file_format(file_path) {
+        "yaml" => Ok(serde_yaml::from_reader(reader)?),
+        "json" => Ok(serde_json::from_reader(reader)?),
+        _ => {
+            let content = fs::read_to_string(file_path)?;
+            Ok(MiniJinjaValue::from(content))
+        }
+    }
+}
+
+// Parse YAML to MiniJinja Value from reader
+fn parse_yaml_to_minijinja_from_reader(file_path: &Path) -> Result<MiniJinjaValue> {
+    read_and_parse_file(file_path)
+}
+
+// Parse YAML to MiniJinja Value from string
+fn parse_yaml_to_minijinja_from_string(file_path: &Path) -> Result<MiniJinjaValue> {
+    read_and_parse_file(file_path)
+}
+
+// Parse JSON to MiniJinja Value from reader
+fn parse_json_to_minijinja_from_reader(file_path: &Path) -> Result<MiniJinjaValue> {
+    read_and_parse_file(file_path)
+}
+
+// Parse JSON to MiniJinja Value from string
+fn parse_json_to_minijinja_from_string(file_path: &Path) -> Result<MiniJinjaValue> {
+    read_and_parse_file(file_path)
+}
+
+// Parse YAML using memory mapping
+fn parse_yaml_from_mmap(file_path: &Path) -> Result<YamlValue> {
+    let file = fs::File::open(file_path)?;
+    let mmap = unsafe { Mmap::map(&file)? };
+    Ok(serde_yaml::from_slice(&mmap)?)
+}
+
+// Parse JSON using memory mapping
+fn parse_json_from_mmap(file_path: &Path) -> Result<JsonValue> {
+    let file = fs::File::open(file_path)?;
+    let mmap = unsafe { Mmap::map(&file)? };
+    Ok(serde_json::from_slice(&mmap)?)
+}
+
+// Parse YAML to MiniJinja Value using memory mapping
+fn parse_yaml_to_minijinja_from_mmap(file_path: &Path) -> Result<MiniJinjaValue> {
+    let file = fs::File::open(file_path)?;
+    let mmap = unsafe { Mmap::map(&file)? };
+    Ok(serde_yaml::from_slice(&mmap)?)
+}
+
+// Parse JSON to MiniJinja Value using memory mapping
+fn parse_json_to_minijinja_from_mmap(file_path: &Path) -> Result<MiniJinjaValue> {
+    let file = fs::File::open(file_path)?;
+    let mmap = unsafe { Mmap::map(&file)? };
+    Ok(serde_json::from_slice(&mmap)?)
+}
+
+fn create_test_files() -> (tempfile::TempDir, std::path::PathBuf, std::path::PathBuf) {
+    let temp_dir = tempfile::tempdir().unwrap();
+
+    // Create a test YAML file
+    let yaml_path = temp_dir.path().join("test.yaml");
+    let yaml_content = r#"
+name: test-package
+version: 1.0.0
+description: A test package for benchmarking
+dependencies:
+  - dep1 >=1.0.0
+  - dep2 >=2.0.0
+  - dep3 >=3.0.0
+build:
+  number: 0
+  script:
+    - echo "Building test package"
+    - make install
+test:
+  commands:
+    - test -f $PREFIX/bin/test-package
+about:
+  home: https://example.com
+  license: MIT
+  summary: Test package for benchmarking
+"#;
+    fs::write(&yaml_path, yaml_content).unwrap();
+
+    // Create a test JSON file
+    let json_path = temp_dir.path().join("test.json");
+    let json_content = r#"
+{
+  "name": "test-package",
+  "version": "1.0.0",
+  "description": "A test package for benchmarking",
+  "dependencies": [
+    "dep1 >=1.0.0",
+    "dep2 >=2.0.0",
+    "dep3 >=3.0.0"
+  ],
+  "build": {
+    "number": 0,
+    "script": [
+      "echo \"Building test package\"",
+      "make install"
+    ]
+  },
+  "test": {
+    "commands": [
+      "test -f $PREFIX/bin/test-package"
+    ]
+  },
+  "about": {
+    "home": "https://example.com",
+    "license": "MIT",
+    "summary": "Test package for benchmarking"
+  }
+}
+"#;
+    fs::write(&json_path, json_content).unwrap();
+
+    (temp_dir, yaml_path, json_path)
+}
+
+fn benchmark_yaml_parsing(c: &mut Criterion) {
+    let (temp_dir, yaml_path, _) = create_test_files();
+    let mut group = c.benchmark_group("yaml_parsing");
+    group.sample_size(5000);
+
+    let file_size = fs::metadata(&yaml_path).unwrap().len();
+    group.throughput(Throughput::Bytes(file_size));
+
+    group.bench_function("from_reader", |b| {
+        b.iter(|| {
+            black_box(parse_yaml_from_reader(&yaml_path).unwrap());
+        });
+    });
+
+    group.bench_function("from_string", |b| {
+        b.iter(|| {
+            black_box(parse_yaml_from_string(&yaml_path).unwrap());
+        });
+    });
+
+    group.bench_function("from_mmap", |b| {
+        b.iter(|| {
+            black_box(parse_yaml_from_mmap(&yaml_path).unwrap());
+        });
+    });
+
+    group.finish();
+    drop(temp_dir);
+}
+
+fn benchmark_json_parsing(c: &mut Criterion) {
+    let (temp_dir, _, json_path) = create_test_files();
+    let mut group = c.benchmark_group("json_parsing");
+    group.sample_size(5000);
+
+    let file_size = fs::metadata(&json_path).unwrap().len();
+    group.throughput(Throughput::Bytes(file_size));
+
+    group.bench_function("from_reader", |b| {
+        b.iter(|| {
+            black_box(parse_json_from_reader(&json_path).unwrap());
+        });
+    });
+
+    group.bench_function("from_string", |b| {
+        b.iter(|| {
+            black_box(parse_json_from_string(&json_path).unwrap());
+        });
+    });
+
+    group.bench_function("from_mmap", |b| {
+        b.iter(|| {
+            black_box(parse_json_from_mmap(&json_path).unwrap());
+        });
+    });
+
+    group.finish();
+    drop(temp_dir);
+}
+
+fn benchmark_yaml_to_minijinja_parsing(c: &mut Criterion) {
+    let (temp_dir, yaml_path, _) = create_test_files();
+    let mut group = c.benchmark_group("yaml_to_minijinja_parsing");
+    group.sample_size(5000);
+
+    let file_size = fs::metadata(&yaml_path).unwrap().len();
+    group.throughput(Throughput::Bytes(file_size));
+
+    group.bench_function("from_reader", |b| {
+        b.iter(|| {
+            black_box(parse_yaml_to_minijinja_from_reader(&yaml_path).unwrap());
+        });
+    });
+
+    group.bench_function("from_string", |b| {
+        b.iter(|| {
+            black_box(parse_yaml_to_minijinja_from_string(&yaml_path).unwrap());
+        });
+    });
+
+    group.bench_function("from_mmap", |b| {
+        b.iter(|| {
+            black_box(parse_yaml_to_minijinja_from_mmap(&yaml_path).unwrap());
+        });
+    });
+
+    group.finish();
+    drop(temp_dir);
+}
+
+fn benchmark_json_to_minijinja_parsing(c: &mut Criterion) {
+    let (temp_dir, _, json_path) = create_test_files();
+    let mut group = c.benchmark_group("json_to_minijinja_parsing");
+    group.sample_size(5000);
+
+    let file_size = fs::metadata(&json_path).unwrap().len();
+    group.throughput(Throughput::Bytes(file_size));
+
+    group.bench_function("from_reader", |b| {
+        b.iter(|| {
+            black_box(parse_json_to_minijinja_from_reader(&json_path).unwrap());
+        });
+    });
+
+    group.bench_function("from_string", |b| {
+        b.iter(|| {
+            black_box(parse_json_to_minijinja_from_string(&json_path).unwrap());
+        });
+    });
+
+    group.bench_function("from_mmap", |b| {
+        b.iter(|| {
+            black_box(parse_json_to_minijinja_from_mmap(&json_path).unwrap());
+        });
+    });
+
+    group.finish();
+    drop(temp_dir);
+}
+
+criterion_group!(
+    benches,
+    benchmark_yaml_parsing,
+    benchmark_json_parsing,
+    benchmark_yaml_to_minijinja_parsing,
+    benchmark_json_to_minijinja_parsing
+);
+criterion_main!(benches);
diff --git a/src/lib.rs b/src/lib.rs
index e8e11cbe..b08c8343 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -209,6 +209,7 @@ pub async fn get_build_output(
         experimental: build_data.common.experimental,
         // allow undefined while finding the variants
         allow_undefined: true,
+        recipe_path: Some(recipe_path.to_path_buf()),
     };
 
     let span = tracing::info_span!("Finding outputs from recipe");
diff --git a/src/metadata.rs b/src/metadata.rs
index c9445c3e..0af8c196 100644
--- a/src/metadata.rs
+++ b/src/metadata.rs
@@ -395,6 +395,7 @@ impl BuildConfiguration {
             hash: Some(self.hash.clone()),
             experimental: false,
             allow_undefined: false,
+            recipe_path: None,
         }
     }
 }
diff --git a/src/recipe/jinja.rs b/src/recipe/jinja.rs
index b1cc6bf4..6b43c652 100644
--- a/src/recipe/jinja.rs
+++ b/src/recipe/jinja.rs
@@ -3,6 +3,7 @@
 use fs_err as fs;
 use indexmap::IndexMap;
 use minijinja::syntax::SyntaxConfig;
+use std::io::Read;
 use std::process::Command;
 use std::sync::Arc;
 use std::{collections::BTreeMap, str::FromStr};
@@ -392,6 +393,7 @@ fn set_jinja(config: &SelectorConfig) -> minijinja::Environment<'static> {
         variant,
         experimental,
         allow_undefined,
+        recipe_path,
         ..
     } = config.clone();
 
@@ -521,36 +523,100 @@ fn set_jinja(config: &SelectorConfig) -> minijinja::Environment<'static> {
     env.add_function("is_unix", |platform: &str| {
         Ok(parse_platform(platform)?.is_unix())
     });
+    #[derive(Debug, PartialEq, Eq, Clone, Copy)]
+    enum FileFormat {
+        Yaml,
+        Json,
+        Toml,
+        Unknown,
+    }
 
-    env.add_function("load_from_file", move |path: String| {
+    // Helper function to determine the file format based on the file extension
+    fn get_file_format(file_path: &std::path::Path) -> FileFormat {
+        file_path
+            .extension()
+            .and_then(|ext| ext.to_str())
+            .map(|ext| match ext.to_lowercase().as_str() {
+                "yaml" | "yml" => FileFormat::Yaml,
+                "json" => FileFormat::Json,
+                "toml" => FileFormat::Toml,
+                _ => FileFormat::Unknown,
+            })
+            .unwrap_or(FileFormat::Unknown)
+    }
+
+    // Helper function to handle file errors
+    fn handle_file_error(e: std::io::Error, file_path: &std::path::Path) -> minijinja::Error {
+        minijinja::Error::new(
+            minijinja::ErrorKind::UndefinedError,
+            format!("Failed to open file '{}': {}", file_path.display(), e),
+        )
+    }
+
+    // Helper function to handle file reading errors
+    fn handle_read_error(e: std::io::Error, file_path: &std::path::Path) -> minijinja::Error {
+        minijinja::Error::new(
+            minijinja::ErrorKind::UndefinedError,
+            format!("Failed to read file '{}': {}", file_path.display(), e),
+        )
+    }
+
+    // Helper function to handle deserialization errors
+    fn handle_deserialize_error(e: impl std::fmt::Display) -> minijinja::Error {
+        minijinja::Error::new(minijinja::ErrorKind::CannotDeserialize, e.to_string())
+    }
+
+    // Read and parse the file based on its format
+    fn read_and_parse_file(
+        file_path: &std::path::Path,
+    ) -> Result<minijinja::Value, minijinja::Error> {
+        let file = fs::File::open(file_path).map_err(|e| handle_file_error(e, file_path))?;
+        let mut reader = std::io::BufReader::new(file);
+
+        match get_file_format(file_path) {
+            FileFormat::Yaml => serde_yaml::from_reader(reader).map_err(handle_deserialize_error),
+            FileFormat::Json => serde_json::from_reader(reader).map_err(handle_deserialize_error),
+            FileFormat::Toml => {
+                let mut content = String::new();
+                reader
+                    .read_to_string(&mut content)
+                    .map_err(|e| handle_read_error(e, file_path))?;
+                toml::from_str(&content).map_err(handle_deserialize_error)
+            }
+            FileFormat::Unknown => {
+                let mut content = String::new();
+                reader
+                    .read_to_string(&mut content)
+                    .map_err(|e| handle_read_error(e, file_path))?;
+                Ok(Value::from(content))
+            }
+        }
+    }
+    // Check if the experimental feature is enabled
+    fn check_experimental(experimental: bool) -> Result<(), minijinja::Error> {
         if !experimental {
             return Err(minijinja::Error::new(
                 minijinja::ErrorKind::InvalidOperation,
                 "Experimental feature: provide the `--experimental` flag to enable this feature",
             ));
         }
-        let src = fs::read_to_string(&path).map_err(|e| {
-            minijinja::Error::new(minijinja::ErrorKind::UndefinedError, e.to_string())
-        })?;
-        // tracing::info!("loading from path: {path}");
-        let filename = path
-            .split('/')
-            .next_back()
-            .expect("unreachable: split will always atleast return empty string");
-        // tracing::info!("loading filename: {filename}");
-        let value: minijinja::Value = match filename.split_once('.') {
-            Some((_, "yaml")) | Some((_, "yml")) => serde_yaml::from_str(&src).map_err(|e| {
-                minijinja::Error::new(minijinja::ErrorKind::CannotDeserialize, e.to_string())
-            })?,
-            Some((_, "json")) => serde_json::from_str(&src).map_err(|e| {
-                minijinja::Error::new(minijinja::ErrorKind::CannotDeserialize, e.to_string())
-            })?,
-            Some((_, "toml")) => toml::from_str(&src).map_err(|e| {
-                minijinja::Error::new(minijinja::ErrorKind::CannotDeserialize, e.to_string())
-            })?,
-            _ => Value::from(src),
-        };
-        Ok(value)
+        Ok(())
+    }
+
+    env.add_function("load_from_file", move |path: String| {
+        check_experimental(experimental)?;
+
+        if let Some(recipe_path) = recipe_path.as_ref() {
+            if let Some(parent) = recipe_path.parent() {
+                let relative_path = parent.join(&path);
+                if let Ok(value) = read_and_parse_file(&relative_path) {
+                    return Ok(value);
+                }
+            }
+        }
+
+        let file_path = std::path::Path::new(&path);
+        read_and_parse_file(file_path)
     });
 
     env
@@ -849,11 +915,71 @@ mod tests {
         let path_str = to_forward_slash_lossy(&path);
         fs::write(&path, "hello = 'world'").unwrap();
         assert_eq!(
-            jinja.eval(&format!("load_from_file('{}')['hello']", path_str)).expect("test 2").as_str(),
+            jinja.eval(&format!("load_from_file('{}')['hello']", path_str)).expect("test 3").as_str(),
             Some("world"),
         );
     }
 
+    #[test]
+    fn eval_load_from_file_relative_to_recipe() {
+        let temp_dir = tempfile::tempdir().unwrap();
+        let recipe_path = temp_dir.path().join("recipe.yaml");
+        fs::write(&recipe_path, "dummy: content").unwrap();
+
+        let data_dir = temp_dir.path().join("data");
+        fs::create_dir(&data_dir).unwrap();
+        let json_path = data_dir.join("test.json");
+        fs::write(&json_path, "{ \"hello\": \"world\" }").unwrap();
+
+        let options = SelectorConfig {
+            target_platform: Platform::Linux64,
+            build_platform: Platform::Linux64,
+            experimental: true,
+            recipe_path: Some(recipe_path),
+            ..Default::default()
+        };
+
+        let jinja = Jinja::new(options);
+
+        assert_eq!(
+            jinja
+                .eval("load_from_file('data/test.json')['hello']")
+                .expect("test relative path")
+                .as_str(),
+            Some("world"),
+        );
+    }
+
+    #[test]
+    fn eval_load_from_file_relative_to_recipe_without_experimental() {
+        let temp_dir = tempfile::tempdir().unwrap();
+        let recipe_path = temp_dir.path().join("recipe.yaml");
+        fs::write(&recipe_path, "dummy: content").unwrap();
+
+        let data_dir = temp_dir.path().join("data");
+        fs::create_dir(&data_dir).unwrap();
+        let json_path = data_dir.join("test.json");
+        fs::write(&json_path, "{ \"hello\": \"world\" }").unwrap();
+
+        let options_wo_experimental = SelectorConfig {
+            target_platform: Platform::Linux64,
+            build_platform: Platform::Linux64,
+            experimental: false,
+            recipe_path: Some(recipe_path),
+            ..Default::default()
+        };
+
+        let jinja_wo_experimental = Jinja::new(options_wo_experimental);
+
+        assert_eq!(
+            jinja_wo_experimental
+                .eval("load_from_file('data/test.json')")
+                .expect_err("should error without experimental flag")
+                .to_string(),
+            "invalid operation: Experimental feature: provide the `--experimental` flag to enable this feature (in <expression>:1)",
+        );
+    }
+
     #[test]
     #[rustfmt::skip]
     fn eval() {
diff --git a/src/selectors.rs b/src/selectors.rs
index 6516e6c0..162940d8 100644
--- a/src/selectors.rs
+++ b/src/selectors.rs
@@ -1,6 +1,7 @@
 //! Contains the selector config, which is used to render the recipe.
 
 use std::collections::BTreeMap;
+use std::path::PathBuf;
 
 use crate::{
     hash::HashInfo,
@@ -32,6 +33,8 @@ pub struct SelectorConfig {
     pub experimental: bool,
     /// Allow undefined variables
     pub allow_undefined: bool,
+    /// The path to the recipe file
+    pub recipe_path: Option<PathBuf>,
 }
 
 impl SelectorConfig {
@@ -118,6 +121,7 @@ impl Default for SelectorConfig {
             variant: Default::default(),
             experimental: false,
             allow_undefined: false,
+            recipe_path: None,
         }
     }
 }


Test patch:
diff --git a/test-data/recipes/relative_file_loading/data/package_data.yaml b/test-data/recipes/relative_file_loading/data/package_data.yaml
new file mode 100644
index 00000000..e7b1159c
--- /dev/null
+++ b/test-data/recipes/relative_file_loading/data/package_data.yaml
@@ -0,0 +1,9 @@
+name: test-relative-loading
+version: 1.0.0
+description: "Loaded from relative file"
+extra_metadata:
+  key1: value1
+  key2: value2
+build_data:
+  steps:
+    - "echo 'Step loaded from relative file'"
diff --git a/test-data/recipes/relative_file_loading/recipe.yaml b/test-data/recipes/relative_file_loading/recipe.yaml
new file mode 100644
index 00000000..49eb12d6
--- /dev/null
+++ b/test-data/recipes/relative_file_loading/recipe.yaml
@@ -0,0 +1,13 @@
+context:
+  loaded_data: ${{ load_from_file('data/package_data.yaml') }}
+  loaded_name: ${{ load_from_file('data/package_data.yaml').name }}
+  loaded_version: ${{ load_from_file('data/package_data.yaml').version }}
+  loaded_description: ${{ load_from_file('data/package_data.yaml').description }}
+
+package:
+  name: relative-file-loading
+  version: "1.0.0"
+
+about:
+  summary: "Test for relative file loading"
+  description: ${{ load_from_file('data/package_data.yaml').description }}
diff --git a/test/end-to-end/test_simple.py b/test/end-to-end/test_simple.py
index ce32fd58..8c348523 100644
--- a/test/end-to-end/test_simple.py
+++ b/test/end-to-end/test_simple.py
@@ -1544,3 +1544,56 @@ def test_channel_sources(
         "https://conda.anaconda.org/conda-forge/label/rust_dev",
         "https://conda.anaconda.org/conda-forge",
     ]
+
+
+def test_relative_file_loading(
+    rattler_build: RattlerBuild, recipes: Path, tmp_path: Path
+):
+    # build the package with experimental flag to enable the feature
+    rattler_build.build(
+        recipes / "relative_file_loading",
+        tmp_path,
+        extra_args=["--experimental"],
+    )
+
+    pkg = get_extracted_package(tmp_path, "relative-file-loading")
+    assert (pkg / "info/index.json").exists()
+    index_json = json.loads((pkg / "info/index.json").read_text())
+    assert index_json["name"] == "relative-file-loading"
+    assert index_json["version"] == "1.0.0"
+
+    assert (pkg / "info/about.json").exists()
+    about_json = json.loads((pkg / "info/about.json").read_text())
+    assert "Loaded from relative file" in about_json["description"]
+    assert (pkg / "info/recipe/data/package_data.yaml").exists()
+    recipe_data = yaml.safe_load(
+        (pkg / "info/recipe/data/package_data.yaml").read_text()
+    )
+    assert recipe_data["name"] == "test-relative-loading"
+    assert recipe_data["version"] == "1.0.0"
+    assert recipe_data["description"] == "Loaded from relative file"
+
+    # Check the rendered recipe
+    assert (pkg / "info/recipe/rendered_recipe.yaml").exists()
+    rendered_recipe = yaml.safe_load(
+        (pkg / "info/recipe/rendered_recipe.yaml").read_text()
+    )
+    print("\nRendered recipe structure:")
+    print(yaml.dump(rendered_recipe, default_flow_style=False))
+
+    assert "recipe" in rendered_recipe
+    assert "context" in rendered_recipe["recipe"]
+
+    context = rendered_recipe["recipe"]["context"]
+    assert "loaded_data" in context
+    assert "loaded_name" in context
+    assert "loaded_version" in context
+    assert "loaded_description" in context
+    assert context["loaded_name"] == "test-relative-loading"
+    assert context["loaded_version"] == "1.0.0"
+    assert context["loaded_description"] == "Loaded from relative file"
+    assert "about" in rendered_recipe["recipe"]
+    assert "description" in rendered_recipe["recipe"]["about"]
+    assert (
+        rendered_recipe["recipe"]["about"]["description"] == "Loaded from relative file"
+    )