nushell/crates/nu-protocol/src/engine/stack.rs
Devyn Cairns f65bc97a54
Update config directly at assignment (#13332)
# Description

Allows `Stack` to have a modified local `Config`, which is updated
immediately when `$env.config` is assigned to. This means that even
within a script, commands that come after `$env.config` changes will
always see those changes in `Stack::get_config()`.

Also fixed a lot of cases where `engine_state.get_config()` was used
even when `Stack` was available.

Closes #13324.

# User-Facing Changes
- Config changes apply immediately after the assignment is executed,
rather than whenever config is read by a command that needs it.
- Potentially slower performance when executing a lot of lines that
change `$env.config` one after another. Recommended to get `$env.config`
into a `mut` variable first and do modifications, then assign it back.
- Much faster performance when executing a script that made
modifications to `$env.config`, as the changes are only parsed once.

# Tests + Formatting
All passing.

# After Submitting
- [ ] release notes
2024-07-11 06:09:33 -07:00

824 lines
30 KiB
Rust

use crate::{
engine::{
ArgumentStack, EngineState, ErrorHandlerStack, Redirection, StackCallArgGuard,
StackCaptureGuard, StackIoGuard, StackOutDest, DEFAULT_OVERLAY_NAME,
},
Config, OutDest, ShellError, Span, Value, VarId, ENV_VARIABLE_ID, NU_VARIABLE_ID,
};
use std::{
collections::{HashMap, HashSet},
fs::File,
sync::Arc,
};
/// Environment variables per overlay
pub type EnvVars = HashMap<String, HashMap<String, Value>>;
/// A runtime value stack used during evaluation
///
/// A note on implementation:
///
/// We previously set up the stack in a traditional way, where stack frames had parents which would
/// represent other frames that you might return to when exiting a function.
///
/// While experimenting with blocks, we found that we needed to have closure captures of variables
/// seen outside of the blocks, so that they blocks could be run in a way that was both thread-safe
/// and followed the restrictions for closures applied to iterators. The end result left us with
/// closure-captured single stack frames that blocks could see.
///
/// Blocks make up the only scope and stack definition abstraction in Nushell. As a result, we were
/// creating closure captures at any point we wanted to have a Block value we could safely evaluate
/// in any context. This meant that the parents were going largely unused, with captured variables
/// taking their place. The end result is this, where we no longer have separate frames, but instead
/// use the Stack as a way of representing the local and closure-captured state.
#[derive(Debug, Clone)]
pub struct Stack {
/// Variables
pub vars: Vec<(VarId, Value)>,
/// Environment variables arranged as a stack to be able to recover values from parent scopes
pub env_vars: Vec<Arc<EnvVars>>,
/// Tells which environment variables from engine state are hidden, per overlay.
pub env_hidden: Arc<HashMap<String, HashSet<String>>>,
/// List of active overlays
pub active_overlays: Vec<String>,
/// Argument stack for IR evaluation
pub arguments: ArgumentStack,
/// Error handler stack for IR evaluation
pub error_handlers: ErrorHandlerStack,
/// Set true to always use IR mode
pub use_ir: bool,
pub recursion_count: u64,
pub parent_stack: Option<Arc<Stack>>,
/// Variables that have been deleted (this is used to hide values from parent stack lookups)
pub parent_deletions: Vec<VarId>,
/// Locally updated config. Use [`.get_config()`] to access correctly.
pub config: Option<Arc<Config>>,
pub(crate) out_dest: StackOutDest,
}
impl Default for Stack {
fn default() -> Self {
Self::new()
}
}
impl Stack {
/// Create a new stack.
///
/// stdout and stderr will be set to [`OutDest::Inherit`]. So, if the last command is an external command,
/// then its output will be forwarded to the terminal/stdio streams.
///
/// Use [`Stack::capture`] afterwards if you need to evaluate an expression to a [`Value`](crate::Value)
/// (as opposed to a [`PipelineData`](crate::PipelineData)).
pub fn new() -> Self {
Self {
vars: Vec::new(),
env_vars: Vec::new(),
env_hidden: Arc::new(HashMap::new()),
active_overlays: vec![DEFAULT_OVERLAY_NAME.to_string()],
arguments: ArgumentStack::new(),
error_handlers: ErrorHandlerStack::new(),
use_ir: false,
recursion_count: 0,
parent_stack: None,
parent_deletions: vec![],
config: None,
out_dest: StackOutDest::new(),
}
}
/// Create a new child stack from a parent.
///
/// Changes from this child can be merged back into the parent with
/// [`Stack::with_changes_from_child`]
pub fn with_parent(parent: Arc<Stack>) -> Stack {
Stack {
// here we are still cloning environment variable-related information
env_vars: parent.env_vars.clone(),
env_hidden: parent.env_hidden.clone(),
active_overlays: parent.active_overlays.clone(),
arguments: ArgumentStack::new(),
error_handlers: ErrorHandlerStack::new(),
use_ir: parent.use_ir,
recursion_count: parent.recursion_count,
vars: vec![],
parent_deletions: vec![],
config: parent.config.clone(),
out_dest: parent.out_dest.clone(),
parent_stack: Some(parent),
}
}
/// Take an [`Arc`] parent, and a child, and apply all the changes from a child back to the parent.
///
/// Here it is assumed that `child` was created by a call to [`Stack::with_parent`] with `parent`.
///
/// For this to be performant and not clone `parent`, `child` should be the only other
/// referencer of `parent`.
pub fn with_changes_from_child(parent: Arc<Stack>, child: Stack) -> Stack {
// we're going to drop the link to the parent stack on our new stack
// so that we can unwrap the Arc as a unique reference
drop(child.parent_stack);
let mut unique_stack = Arc::unwrap_or_clone(parent);
unique_stack
.vars
.retain(|(var, _)| !child.parent_deletions.contains(var));
for (var, value) in child.vars {
unique_stack.add_var(var, value);
}
unique_stack.env_vars = child.env_vars;
unique_stack.env_hidden = child.env_hidden;
unique_stack.active_overlays = child.active_overlays;
unique_stack.config = child.config;
unique_stack
}
pub fn with_env(
&mut self,
env_vars: &[Arc<EnvVars>],
env_hidden: &Arc<HashMap<String, HashSet<String>>>,
) {
// Do not clone the environment if it hasn't changed
if self.env_vars.iter().any(|scope| !scope.is_empty()) {
env_vars.clone_into(&mut self.env_vars);
}
if !self.env_hidden.is_empty() {
self.env_hidden.clone_from(env_hidden);
}
}
/// Lookup a variable, returning None if it is not present
fn lookup_var(&self, var_id: VarId) -> Option<Value> {
for (id, val) in &self.vars {
if var_id == *id {
return Some(val.clone());
}
}
if let Some(stack) = &self.parent_stack {
if !self.parent_deletions.contains(&var_id) {
return stack.lookup_var(var_id);
}
}
None
}
/// Lookup a variable, erroring if it is not found
///
/// The passed-in span will be used to tag the value
pub fn get_var(&self, var_id: VarId, span: Span) -> Result<Value, ShellError> {
match self.lookup_var(var_id) {
Some(v) => Ok(v.with_span(span)),
None => Err(ShellError::VariableNotFoundAtRuntime { span }),
}
}
/// Lookup a variable, erroring if it is not found
///
/// While the passed-in span will be used for errors, the returned value
/// has the span from where it was originally defined
pub fn get_var_with_origin(&self, var_id: VarId, span: Span) -> Result<Value, ShellError> {
match self.lookup_var(var_id) {
Some(v) => Ok(v),
None => {
if var_id == NU_VARIABLE_ID || var_id == ENV_VARIABLE_ID {
return Err(ShellError::GenericError {
error: "Built-in variables `$env` and `$nu` have no metadata".into(),
msg: "no metadata available".into(),
span: Some(span),
help: None,
inner: vec![],
});
}
Err(ShellError::VariableNotFoundAtRuntime { span })
}
}
}
/// Get the local config if set, otherwise the config from the engine state.
///
/// This is the canonical way to get [`Config`] when [`Stack`] is available.
pub fn get_config(&self, engine_state: &EngineState) -> Arc<Config> {
self.config
.clone()
.unwrap_or_else(|| engine_state.config.clone())
}
/// Update the local config with the config stored in the `config` environment variable. Run
/// this after assigning to `$env.config`.
///
/// The config will be updated with successfully parsed values even if an error occurs.
pub fn update_config(&mut self, engine_state: &EngineState) -> Result<(), ShellError> {
if let Some(mut config) = self.get_env_var(engine_state, "config") {
let existing_config = self.get_config(engine_state);
let (new_config, error) = config.parse_as_config(&existing_config);
self.config = Some(new_config.into());
// The config value is modified by the update, so we should add it again
self.add_env_var("config".into(), config);
match error {
None => Ok(()),
Some(err) => Err(err),
}
} else {
self.config = None;
Ok(())
}
}
pub fn add_var(&mut self, var_id: VarId, value: Value) {
//self.vars.insert(var_id, value);
for (id, val) in &mut self.vars {
if *id == var_id {
*val = value;
return;
}
}
self.vars.push((var_id, value));
}
pub fn remove_var(&mut self, var_id: VarId) {
for (idx, (id, _)) in self.vars.iter().enumerate() {
if *id == var_id {
self.vars.remove(idx);
break;
}
}
// even if we did have it in the original layer, we need to make sure to remove it here
// as well (since the previous update might have simply hid the parent value)
if self.parent_stack.is_some() {
self.parent_deletions.push(var_id);
}
}
pub fn add_env_var(&mut self, var: String, value: Value) {
if let Some(last_overlay) = self.active_overlays.last() {
if let Some(env_hidden) = Arc::make_mut(&mut self.env_hidden).get_mut(last_overlay) {
// if the env var was hidden, let's activate it again
env_hidden.remove(&var);
}
if let Some(scope) = self.env_vars.last_mut() {
let scope = Arc::make_mut(scope);
if let Some(env_vars) = scope.get_mut(last_overlay) {
env_vars.insert(var, value);
} else {
scope.insert(last_overlay.into(), [(var, value)].into_iter().collect());
}
} else {
self.env_vars.push(Arc::new(
[(last_overlay.into(), [(var, value)].into_iter().collect())]
.into_iter()
.collect(),
));
}
} else {
// TODO: Remove panic
panic!("internal error: no active overlay");
}
}
pub fn last_overlay_name(&self) -> Result<String, ShellError> {
self.active_overlays
.last()
.cloned()
.ok_or_else(|| ShellError::NushellFailed {
msg: "No active overlay".into(),
})
}
pub fn captures_to_stack(&self, captures: Vec<(VarId, Value)>) -> Stack {
self.captures_to_stack_preserve_out_dest(captures).capture()
}
pub fn captures_to_stack_preserve_out_dest(&self, captures: Vec<(VarId, Value)>) -> Stack {
let mut env_vars = self.env_vars.clone();
env_vars.push(Arc::new(HashMap::new()));
Stack {
vars: captures,
env_vars,
env_hidden: self.env_hidden.clone(),
active_overlays: self.active_overlays.clone(),
arguments: ArgumentStack::new(),
error_handlers: ErrorHandlerStack::new(),
use_ir: self.use_ir,
recursion_count: self.recursion_count,
parent_stack: None,
parent_deletions: vec![],
config: self.config.clone(),
out_dest: self.out_dest.clone(),
}
}
pub fn gather_captures(&self, engine_state: &EngineState, captures: &[VarId]) -> Stack {
let mut vars = Vec::with_capacity(captures.len());
let fake_span = Span::new(0, 0);
for capture in captures {
// Note: this assumes we have calculated captures correctly and that commands
// that take in a var decl will manually set this into scope when running the blocks
if let Ok(value) = self.get_var(*capture, fake_span) {
vars.push((*capture, value));
} else if let Some(const_val) = &engine_state.get_var(*capture).const_val {
vars.push((*capture, const_val.clone()));
}
}
let mut env_vars = self.env_vars.clone();
env_vars.push(Arc::new(HashMap::new()));
Stack {
vars,
env_vars,
env_hidden: self.env_hidden.clone(),
active_overlays: self.active_overlays.clone(),
arguments: ArgumentStack::new(),
error_handlers: ErrorHandlerStack::new(),
use_ir: self.use_ir,
recursion_count: self.recursion_count,
parent_stack: None,
parent_deletions: vec![],
config: self.config.clone(),
out_dest: self.out_dest.clone(),
}
}
/// Flatten the env var scope frames into one frame
pub fn get_env_vars(&self, engine_state: &EngineState) -> HashMap<String, Value> {
let mut result = HashMap::new();
for active_overlay in self.active_overlays.iter() {
if let Some(env_vars) = engine_state.env_vars.get(active_overlay) {
result.extend(
env_vars
.iter()
.filter(|(k, _)| {
if let Some(env_hidden) = self.env_hidden.get(active_overlay) {
!env_hidden.contains(*k)
} else {
// nothing has been hidden in this overlay
true
}
})
.map(|(k, v)| (k.clone(), v.clone()))
.collect::<HashMap<String, Value>>(),
);
}
}
result.extend(self.get_stack_env_vars());
result
}
/// Get flattened environment variables only from the stack
pub fn get_stack_env_vars(&self) -> HashMap<String, Value> {
let mut result = HashMap::new();
for scope in &self.env_vars {
for active_overlay in self.active_overlays.iter() {
if let Some(env_vars) = scope.get(active_overlay) {
result.extend(env_vars.clone());
}
}
}
result
}
/// Get flattened environment variables only from the stack and one overlay
pub fn get_stack_overlay_env_vars(&self, overlay_name: &str) -> HashMap<String, Value> {
let mut result = HashMap::new();
for scope in &self.env_vars {
if let Some(active_overlay) = self.active_overlays.iter().find(|n| n == &overlay_name) {
if let Some(env_vars) = scope.get(active_overlay) {
result.extend(env_vars.clone());
}
}
}
result
}
/// Same as get_env_vars, but returns only the names as a HashSet
pub fn get_env_var_names(&self, engine_state: &EngineState) -> HashSet<String> {
let mut result = HashSet::new();
for active_overlay in self.active_overlays.iter() {
if let Some(env_vars) = engine_state.env_vars.get(active_overlay) {
result.extend(
env_vars
.keys()
.filter(|k| {
if let Some(env_hidden) = self.env_hidden.get(active_overlay) {
!env_hidden.contains(*k)
} else {
// nothing has been hidden in this overlay
true
}
})
.cloned()
.collect::<HashSet<String>>(),
);
}
}
for scope in &self.env_vars {
for active_overlay in self.active_overlays.iter() {
if let Some(env_vars) = scope.get(active_overlay) {
result.extend(env_vars.keys().cloned().collect::<HashSet<String>>());
}
}
}
result
}
pub fn get_env_var(&self, engine_state: &EngineState, name: &str) -> Option<Value> {
for scope in self.env_vars.iter().rev() {
for active_overlay in self.active_overlays.iter().rev() {
if let Some(env_vars) = scope.get(active_overlay) {
if let Some(v) = env_vars.get(name) {
return Some(v.clone());
}
}
}
}
for active_overlay in self.active_overlays.iter().rev() {
let is_hidden = if let Some(env_hidden) = self.env_hidden.get(active_overlay) {
env_hidden.contains(name)
} else {
false
};
if !is_hidden {
if let Some(env_vars) = engine_state.env_vars.get(active_overlay) {
if let Some(v) = env_vars.get(name) {
return Some(v.clone());
}
}
}
}
None
}
pub fn has_env_var(&self, engine_state: &EngineState, name: &str) -> bool {
for scope in self.env_vars.iter().rev() {
for active_overlay in self.active_overlays.iter().rev() {
if let Some(env_vars) = scope.get(active_overlay) {
if env_vars.contains_key(name) {
return true;
}
}
}
}
for active_overlay in self.active_overlays.iter().rev() {
let is_hidden = if let Some(env_hidden) = self.env_hidden.get(active_overlay) {
env_hidden.contains(name)
} else {
false
};
if !is_hidden {
if let Some(env_vars) = engine_state.env_vars.get(active_overlay) {
if env_vars.contains_key(name) {
return true;
}
}
}
}
false
}
pub fn remove_env_var(&mut self, engine_state: &EngineState, name: &str) -> bool {
for scope in self.env_vars.iter_mut().rev() {
let scope = Arc::make_mut(scope);
for active_overlay in self.active_overlays.iter().rev() {
if let Some(env_vars) = scope.get_mut(active_overlay) {
if env_vars.remove(name).is_some() {
return true;
}
}
}
}
for active_overlay in self.active_overlays.iter().rev() {
if let Some(env_vars) = engine_state.env_vars.get(active_overlay) {
if env_vars.get(name).is_some() {
let env_hidden = Arc::make_mut(&mut self.env_hidden);
if let Some(env_hidden_in_overlay) = env_hidden.get_mut(active_overlay) {
env_hidden_in_overlay.insert(name.into());
} else {
env_hidden
.insert(active_overlay.into(), [name.into()].into_iter().collect());
}
return true;
}
}
}
false
}
pub fn has_env_overlay(&self, name: &str, engine_state: &EngineState) -> bool {
for scope in self.env_vars.iter().rev() {
if scope.contains_key(name) {
return true;
}
}
engine_state.env_vars.contains_key(name)
}
pub fn is_overlay_active(&self, name: &str) -> bool {
self.active_overlays.iter().any(|n| n == name)
}
pub fn add_overlay(&mut self, name: String) {
self.active_overlays.retain(|o| o != &name);
self.active_overlays.push(name);
}
pub fn remove_overlay(&mut self, name: &str) {
self.active_overlays.retain(|o| o != name);
}
/// Returns the [`OutDest`] to use for the current command's stdout.
///
/// This will be the pipe redirection if one is set,
/// otherwise it will be the current file redirection,
/// otherwise it will be the process's stdout indicated by [`OutDest::Inherit`].
pub fn stdout(&self) -> &OutDest {
self.out_dest.stdout()
}
/// Returns the [`OutDest`] to use for the current command's stderr.
///
/// This will be the pipe redirection if one is set,
/// otherwise it will be the current file redirection,
/// otherwise it will be the process's stderr indicated by [`OutDest::Inherit`].
pub fn stderr(&self) -> &OutDest {
self.out_dest.stderr()
}
/// Returns the [`OutDest`] of the pipe redirection applied to the current command's stdout.
pub fn pipe_stdout(&self) -> Option<&OutDest> {
self.out_dest.pipe_stdout.as_ref()
}
/// Returns the [`OutDest`] of the pipe redirection applied to the current command's stderr.
pub fn pipe_stderr(&self) -> Option<&OutDest> {
self.out_dest.pipe_stderr.as_ref()
}
/// Temporarily set the pipe stdout redirection to [`OutDest::Capture`].
///
/// This is used before evaluating an expression into a `Value`.
pub fn start_capture(&mut self) -> StackCaptureGuard {
StackCaptureGuard::new(self)
}
/// Temporarily use the output redirections in the parent scope.
///
/// This is used before evaluating an argument to a call.
pub fn use_call_arg_out_dest(&mut self) -> StackCallArgGuard {
StackCallArgGuard::new(self)
}
/// Temporarily apply redirections to stdout and/or stderr.
pub fn push_redirection(
&mut self,
stdout: Option<Redirection>,
stderr: Option<Redirection>,
) -> StackIoGuard {
StackIoGuard::new(self, stdout, stderr)
}
/// Mark stdout for the last command as [`OutDest::Capture`].
///
/// This will irreversibly alter the output redirections, and so it only makes sense to use this on an owned `Stack`
/// (which is why this function does not take `&mut self`).
///
/// See [`Stack::start_capture`] which can temporarily set stdout as [`OutDest::Capture`] for a mutable `Stack` reference.
pub fn capture(mut self) -> Self {
self.out_dest.pipe_stdout = Some(OutDest::Capture);
self.out_dest.pipe_stderr = None;
self
}
/// Clears any pipe and file redirections and resets stdout and stderr to [`OutDest::Inherit`].
///
/// This will irreversibly reset the output redirections, and so it only makes sense to use this on an owned `Stack`
/// (which is why this function does not take `&mut self`).
pub fn reset_out_dest(mut self) -> Self {
self.out_dest = StackOutDest::new();
self
}
/// Clears any pipe redirections, keeping the current stdout and stderr.
///
/// This will irreversibly reset some of the output redirections, and so it only makes sense to use this on an owned `Stack`
/// (which is why this function does not take `&mut self`).
pub fn reset_pipes(mut self) -> Self {
self.out_dest.pipe_stdout = None;
self.out_dest.pipe_stderr = None;
self
}
/// Replaces the default stdout of the stack with a given file.
///
/// This method configures the default stdout to redirect to a specified file.
/// It is primarily useful for applications using `nu` as a language, where the stdout of
/// external commands that are not explicitly piped can be redirected to a file.
///
/// # Using Pipes
///
/// For use in third-party applications pipes might be very useful as they allow using the
/// stdout of external commands for different uses.
/// For example the [`os_pipe`](https://docs.rs/os_pipe) crate provides a elegant way to to
/// access the stdout.
///
/// ```
/// # use std::{fs::File, io::{self, Read}, thread, error};
/// # use nu_protocol::engine::Stack;
/// #
/// let (mut reader, writer) = os_pipe::pipe().unwrap();
/// // Use a thread to avoid blocking the execution of the called command.
/// let reader = thread::spawn(move || {
/// let mut buf: Vec<u8> = Vec::new();
/// reader.read_to_end(&mut buf)?;
/// Ok::<_, io::Error>(buf)
/// });
///
/// #[cfg(windows)]
/// let file = std::os::windows::io::OwnedHandle::from(writer).into();
/// #[cfg(unix)]
/// let file = std::os::unix::io::OwnedFd::from(writer).into();
///
/// let stack = Stack::new().stdout_file(file);
///
/// // Execute some nu code.
///
/// drop(stack); // drop the stack so that the writer will be dropped too
/// let buf = reader.join().unwrap().unwrap();
/// // Do with your buffer whatever you want.
/// ```
pub fn stdout_file(mut self, file: File) -> Self {
self.out_dest.stdout = OutDest::File(Arc::new(file));
self
}
/// Replaces the default stderr of the stack with a given file.
///
/// For more info, see [`stdout_file`](Self::stdout_file).
pub fn stderr_file(mut self, file: File) -> Self {
self.out_dest.stderr = OutDest::File(Arc::new(file));
self
}
/// Set the PWD environment variable to `path`.
///
/// This method accepts `path` with trailing slashes, but they're removed
/// before writing the value into PWD.
pub fn set_cwd(&mut self, path: impl AsRef<std::path::Path>) -> Result<(), ShellError> {
// Helper function to create a simple generic error.
// Its messages are not especially helpful, but these errors don't occur often, so it's probably fine.
fn error(msg: &str) -> Result<(), ShellError> {
Err(ShellError::GenericError {
error: msg.into(),
msg: "".into(),
span: None,
help: None,
inner: vec![],
})
}
let path = path.as_ref();
if !path.is_absolute() {
error("Cannot set $env.PWD to a non-absolute path")
} else if !path.exists() {
error("Cannot set $env.PWD to a non-existent directory")
} else if !path.is_dir() {
error("Cannot set $env.PWD to a non-directory")
} else {
// Strip trailing slashes, if any.
let path = nu_path::strip_trailing_slash(path);
let value = Value::string(path.to_string_lossy(), Span::unknown());
self.add_env_var("PWD".into(), value);
Ok(())
}
}
}
#[cfg(test)]
mod test {
use std::sync::Arc;
use crate::{engine::EngineState, Span, Value};
use super::Stack;
const ZERO_SPAN: Span = Span { start: 0, end: 0 };
fn string_value(s: &str) -> Value {
Value::String {
val: s.to_string(),
internal_span: ZERO_SPAN,
}
}
#[test]
fn test_children_see_inner_values() {
let mut original = Stack::new();
original.add_var(0, string_value("hello"));
let cloned = Stack::with_parent(Arc::new(original));
assert_eq!(cloned.get_var(0, ZERO_SPAN), Ok(string_value("hello")));
}
#[test]
fn test_children_dont_see_deleted_values() {
let mut original = Stack::new();
original.add_var(0, string_value("hello"));
let mut cloned = Stack::with_parent(Arc::new(original));
cloned.remove_var(0);
assert_eq!(
cloned.get_var(0, ZERO_SPAN),
Err(crate::ShellError::VariableNotFoundAtRuntime { span: ZERO_SPAN })
);
}
#[test]
fn test_children_changes_override_parent() {
let mut original = Stack::new();
original.add_var(0, string_value("hello"));
let mut cloned = Stack::with_parent(Arc::new(original));
cloned.add_var(0, string_value("there"));
assert_eq!(cloned.get_var(0, ZERO_SPAN), Ok(string_value("there")));
cloned.remove_var(0);
// the underlying value shouldn't magically re-appear
assert_eq!(
cloned.get_var(0, ZERO_SPAN),
Err(crate::ShellError::VariableNotFoundAtRuntime { span: ZERO_SPAN })
);
}
#[test]
fn test_children_changes_persist_in_offspring() {
let mut original = Stack::new();
original.add_var(0, string_value("hello"));
let mut cloned = Stack::with_parent(Arc::new(original));
cloned.add_var(1, string_value("there"));
cloned.remove_var(0);
let cloned = Stack::with_parent(Arc::new(cloned));
assert_eq!(
cloned.get_var(0, ZERO_SPAN),
Err(crate::ShellError::VariableNotFoundAtRuntime { span: ZERO_SPAN })
);
assert_eq!(cloned.get_var(1, ZERO_SPAN), Ok(string_value("there")));
}
#[test]
fn test_merging_children_back_to_parent() {
let mut original = Stack::new();
let engine_state = EngineState::new();
original.add_var(0, string_value("hello"));
let original_arc = Arc::new(original);
let mut cloned = Stack::with_parent(original_arc.clone());
cloned.add_var(1, string_value("there"));
cloned.remove_var(0);
cloned.add_env_var("ADDED_IN_CHILD".to_string(), string_value("New Env Var"));
let original = Stack::with_changes_from_child(original_arc, cloned);
assert_eq!(
original.get_var(0, ZERO_SPAN),
Err(crate::ShellError::VariableNotFoundAtRuntime { span: ZERO_SPAN })
);
assert_eq!(original.get_var(1, ZERO_SPAN), Ok(string_value("there")));
assert_eq!(
original.get_env_var(&engine_state, "ADDED_IN_CHILD"),
Some(string_value("New Env Var")),
);
}
}