config test harness.

This commit is contained in:
Andrés N. Robalino 2019-09-10 05:08:01 -05:00
parent 7e2d701725
commit f61144006f
5 changed files with 237 additions and 45 deletions

View file

@ -1,16 +1,17 @@
use crate::prelude::*;
use crate::commands::WholeStreamCommand; use crate::commands::WholeStreamCommand;
use crate::data::{config, Value}; use crate::data::{config, Value};
use crate::errors::ShellError; use crate::errors::ShellError;
use crate::parser::hir::SyntaxType; use crate::parser::hir::SyntaxType;
use crate::parser::registry::{self}; use crate::parser::registry::{self};
use crate::prelude::*;
use std::iter::FromIterator; use std::iter::FromIterator;
use std::path::PathBuf;
pub struct Config; pub struct Config;
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct ConfigArgs { pub struct ConfigArgs {
load: Option<Tagged<PathBuf>>,
set: Option<(Tagged<String>, Tagged<Value>)>, set: Option<(Tagged<String>, Tagged<Value>)>,
get: Option<Tagged<String>>, get: Option<Tagged<String>>,
clear: Tagged<bool>, clear: Tagged<bool>,
@ -25,6 +26,7 @@ impl WholeStreamCommand for Config {
fn signature(&self) -> Signature { fn signature(&self) -> Signature {
Signature::build("config") Signature::build("config")
.named("load", SyntaxType::Path)
.named("set", SyntaxType::Any) .named("set", SyntaxType::Any)
.named("get", SyntaxType::Any) .named("get", SyntaxType::Any)
.named("remove", SyntaxType::Any) .named("remove", SyntaxType::Any)
@ -47,6 +49,7 @@ impl WholeStreamCommand for Config {
pub fn config( pub fn config(
ConfigArgs { ConfigArgs {
load,
set, set,
get, get,
clear, clear,
@ -55,7 +58,15 @@ pub fn config(
}: ConfigArgs, }: ConfigArgs,
RunnableContext { name, .. }: RunnableContext, RunnableContext { name, .. }: RunnableContext,
) -> Result<OutputStream, ShellError> { ) -> Result<OutputStream, ShellError> {
let mut result = crate::data::config::config(name)?; let name_span = name;
let configuration = if let Some(supplied) = load {
Some(supplied.item().clone())
} else {
None
};
let mut result = crate::data::config::read(name_span, &configuration)?;
if let Some(v) = get { if let Some(v) = get {
let key = v.to_string(); let key = v.to_string();
@ -63,15 +74,27 @@ pub fn config(
.get(&key) .get(&key)
.ok_or_else(|| ShellError::string(&format!("Missing key {} in config", key)))?; .ok_or_else(|| ShellError::string(&format!("Missing key {} in config", key)))?;
return Ok( let mut results = VecDeque::new();
stream![value.clone()].into(), // futures::stream::once(futures::future::ready(ReturnSuccess::Value(value.clone()))).into(),
); match value {
Tagged {
item: Value::Table(list),
..
} => {
for l in list {
results.push_back(ReturnSuccess::value(l.clone()));
}
}
x => results.push_back(ReturnSuccess::value(x.clone())),
}
return Ok(results.to_output_stream());
} }
if let Some((key, value)) = set { if let Some((key, value)) = set {
result.insert(key.to_string(), value.clone()); result.insert(key.to_string(), value.clone());
config::write_config(&result)?; config::write(&result, &configuration)?;
return Ok(stream![Tagged::from_simple_spanned_item( return Ok(stream![Tagged::from_simple_spanned_item(
Value::Row(result.into()), Value::Row(result.into()),
@ -87,7 +110,7 @@ pub fn config(
{ {
result.clear(); result.clear();
config::write_config(&result)?; config::write(&result, &configuration)?;
return Ok(stream![Tagged::from_simple_spanned_item( return Ok(stream![Tagged::from_simple_spanned_item(
Value::Row(result.into()), Value::Row(result.into()),
@ -101,10 +124,10 @@ pub fn config(
tag: Tag { span, .. }, tag: Tag { span, .. },
} = path } = path
{ {
let path = config::config_path()?; let path = config::default_path_for(&configuration)?;
return Ok(stream![Tagged::from_simple_spanned_item( return Ok(stream![Tagged::from_simple_spanned_item(
Value::Primitive(Primitive::Path(path)), Value::string(path.to_string_lossy()),
span span
)] )]
.from_input_stream()); .from_input_stream());
@ -115,7 +138,7 @@ pub fn config(
if result.contains_key(&key) { if result.contains_key(&key) {
result.remove(&key); result.remove(&key);
config::write_config(&result)?; config::write(&result, &configuration)?;
} else { } else {
return Err(ShellError::string(&format!( return Err(ShellError::string(&format!(
"{} does not exist in config", "{} does not exist in config",

View file

@ -11,52 +11,60 @@ use std::fs::{self, OpenOptions};
use std::io; use std::io;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
const APP_INFO: AppInfo = AppInfo {
name: "nu",
author: "nu shell developers",
};
#[derive(Deserialize, Serialize)] #[derive(Deserialize, Serialize)]
struct Config { struct Config {
#[serde(flatten)] #[serde(flatten)]
extra: IndexMap<String, Tagged<Value>>, extra: IndexMap<String, Tagged<Value>>,
} }
pub(crate) fn config_path() -> Result<PathBuf, ShellError> { pub const APP_INFO: AppInfo = AppInfo {
let location = app_root(AppDataType::UserConfig, &APP_INFO) name: "nu",
.map_err(|err| ShellError::string(&format!("Couldn't open config file:\n{}", err)))?; author: "nu shell developers",
};
Ok(location.join("config.toml")) pub fn config_path() -> Result<PathBuf, ShellError> {
let path = app_root(AppDataType::UserConfig, &APP_INFO)
.map_err(|err| ShellError::string(&format!("Couldn't open config path:\n{}", err)))?;
Ok(path)
} }
pub(crate) fn write_config(config: &IndexMap<String, Tagged<Value>>) -> Result<(), ShellError> { pub fn default_path() -> Result<PathBuf, ShellError> {
let location = app_root(AppDataType::UserConfig, &APP_INFO) default_path_for(&None)
.map_err(|err| ShellError::string(&format!("Couldn't open config file:\n{}", err)))?;
let filename = location.join("config.toml");
touch(&filename)?;
let contents =
value_to_toml_value(&Value::Row(Dictionary::new(config.clone())).tagged_unknown())?;
let contents = toml::to_string(&contents)?;
fs::write(&filename, &contents)?;
Ok(())
} }
pub(crate) fn config(span: impl Into<Span>) -> Result<IndexMap<String, Tagged<Value>>, ShellError> { pub fn default_path_for(file: &Option<PathBuf>) -> Result<PathBuf, ShellError> {
let span = span.into(); let filename = &mut config_path()?;
let filename = match file {
None => {
filename.push("config.toml");
filename
}
Some(file) => {
filename.push(file);
filename
}
};
let location = app_root(AppDataType::UserConfig, &APP_INFO) Ok(filename.clone())
.map_err(|err| ShellError::string(&format!("Couldn't open config file:\n{}", err)))?; }
pub fn read(
span: impl Into<Span>,
at: &Option<PathBuf>,
) -> Result<IndexMap<String, Tagged<Value>>, ShellError> {
let filename = default_path()?;
let filename = match at {
None => filename,
Some(ref file) => file.clone(),
};
let filename = location.join("config.toml");
touch(&filename)?; touch(&filename)?;
trace!("config file = {}", filename.display()); trace!("config file = {}", filename.display());
let span = span.into();
let contents = fs::read_to_string(filename) let contents = fs::read_to_string(filename)
.map(|v| v.simple_spanned(span)) .map(|v| v.simple_spanned(span))
.map_err(|err| ShellError::string(&format!("Couldn't read config file:\n{}", err)))?; .map_err(|err| ShellError::string(&format!("Couldn't read config file:\n{}", err)))?;
@ -75,6 +83,34 @@ pub(crate) fn config(span: impl Into<Span>) -> Result<IndexMap<String, Tagged<Va
} }
} }
pub(crate) fn config(span: impl Into<Span>) -> Result<IndexMap<String, Tagged<Value>>, ShellError> {
read(span, &None)
}
pub fn write(
config: &IndexMap<String, Tagged<Value>>,
at: &Option<PathBuf>,
) -> Result<(), ShellError> {
let filename = &mut default_path()?;
let filename = match at {
None => filename,
Some(file) => {
filename.pop();
filename.push(file);
filename
}
};
let contents =
value_to_toml_value(&Value::Row(Dictionary::new(config.clone())).tagged_unknown())?;
let contents = toml::to_string(&contents)?;
fs::write(&filename, &contents)?;
Ok(())
}
// A simple implementation of `% touch path` (ignores existing files) // A simple implementation of `% touch path` (ignores existing files)
fn touch(path: &Path) -> io::Result<()> { fn touch(path: &Path) -> io::Result<()> {
match OpenOptions::new().create(true).write(true).open(path) { match OpenOptions::new().create(true).write(true).open(path) {

View file

@ -28,6 +28,7 @@ pub use crate::parser::parse::token_tree_builder::TokenTreeBuilder;
pub use crate::plugin::{serve_plugin, Plugin}; pub use crate::plugin::{serve_plugin, Plugin};
pub use crate::utils::{AbsoluteFile, AbsolutePath, RelativePath}; pub use crate::utils::{AbsoluteFile, AbsolutePath, RelativePath};
pub use cli::cli; pub use cli::cli;
pub use data::config::{APP_INFO, config_path};
pub use data::base::{Primitive, Value}; pub use data::base::{Primitive, Value};
pub use data::dict::{Dictionary, TaggedDictBuilder}; pub use data::dict::{Dictionary, TaggedDictBuilder};
pub use data::meta::{Span, Tag, Tagged, TaggedItem}; pub use data::meta::{Span, Tag, Tagged, TaggedItem};

View file

@ -0,0 +1,120 @@
mod helpers;
use helpers as h;
use helpers::{Playground, Stub::*};
use std::path::PathBuf;
#[test]
fn has_default_configuration_file() {
let expected = "config.toml";
Playground::setup("config_test_1", |dirs, _| {
nu!(cwd: dirs.root(), "config");
assert_eq!(
dirs.config_path().join(expected),
nu::config_path().unwrap().join(expected)
);
})
}
#[test]
fn shows_path_of_configuration_file() {
let expected = "config.toml";
Playground::setup("config_test_2", |dirs, _| {
let actual = nu!(
cwd: dirs.test(),
"config --path | echo $it"
);
assert_eq!(PathBuf::from(actual), dirs.config_path().join(expected));
});
}
#[test]
fn use_different_configuration() {
Playground::setup("config_test_3", |dirs, sandbox| {
sandbox
.with_files(vec![FileWithContent(
"test_3.toml",
r#"
caballero_1 = "Andrés N. Robalino"
caballero_2 = "Jonathan Turner"
caballero_3 = "Yehuda katz"
"#
)]);
let actual = nu!(
cwd: dirs.root(),
"config --get caballero_1 --load {}/test_3.toml | echo $it",
dirs.test()
);
assert_eq!(actual, "Andrés N. Robalino");
});
h::delete_file_at(nu::config_path().unwrap().join("test_3.toml"));
}
#[test]
fn sets_configuration_value() {
Playground::setup("config_test_4", |dirs, sandbox| {
sandbox
.with_files(vec![FileWithContent(
"test_4.toml",
r#"
caballero_1 = "Andrés N. Robalino"
caballero_2 = "Jonathan Turner"
caballero_3 = "Yehuda katz"
"#
)]);
nu!(
cwd: dirs.test(),
"config --load test_4.toml --set [caballero_4 jonas]"
);
let actual = nu!(
cwd: dirs.root(),
r#"open "{}/test_4.toml" | get caballero_4 | echo $it"#,
dirs.config_path()
);
assert_eq!(actual, "jonas");
});
h::delete_file_at(nu::config_path().unwrap().join("test_4.toml"));
}
#[test]
fn removes_configuration_value() {
Playground::setup("config_test_5", |dirs, sandbox| {
sandbox
.with_files(vec![FileWithContent(
"test_5.toml",
r#"
caballeros = [1, 1, 1]
podershell = [1, 1, 1]
"#
)]);
nu!(
cwd: dirs.test(),
"config --load test_5.toml --remove podershell"
);
let actual = nu_error!(
cwd: dirs.root(),
r#"open "{}/test_5.toml" | get podershell | echo $it"#,
dirs.config_path()
);
assert!(actual.contains("table missing column"));
});
h::delete_file_at(nu::config_path().unwrap().join("test_5.toml"));
}

View file

@ -4,6 +4,7 @@ use glob::glob;
pub use std::path::Path; pub use std::path::Path;
pub use std::path::PathBuf; pub use std::path::PathBuf;
use app_dirs::{get_app_root, AppDataType};
use getset::Getters; use getset::Getters;
use std::io::Read; use std::io::Read;
use tempfile::{tempdir, TempDir}; use tempfile::{tempdir, TempDir};
@ -177,6 +178,10 @@ impl Dirs {
pub fn formats(&self) -> PathBuf { pub fn formats(&self) -> PathBuf {
PathBuf::from(self.fixtures.join("formats")) PathBuf::from(self.fixtures.join("formats"))
} }
pub fn config_path(&self) -> PathBuf {
get_app_root(AppDataType::UserConfig, &nu::APP_INFO).unwrap()
}
} }
impl Playground { impl Playground {
@ -227,8 +232,7 @@ impl Playground {
playground_root.join(topic).display() playground_root.join(topic).display()
)); ));
let root = let root = dunce::canonicalize(playground_root).expect(&format!(
dunce::canonicalize(playground_root).expect(&format!(
"Couldn't canonicalize tests root path {}", "Couldn't canonicalize tests root path {}",
playground_root.display() playground_root.display()
)); ));
@ -332,6 +336,14 @@ pub fn line_ending() -> String {
} }
} }
pub fn delete_file_at(full_path: impl AsRef<Path>) {
let full_path = full_path.as_ref();
if full_path.exists() {
std::fs::remove_file(full_path).expect("can not delete file");
}
}
pub fn create_file_at(full_path: impl AsRef<Path>) -> Result<(), std::io::Error> { pub fn create_file_at(full_path: impl AsRef<Path>) -> Result<(), std::io::Error> {
let full_path = full_path.as_ref(); let full_path = full_path.as_ref();