diff options
-rw-r--r-- | Makefile | 2 | ||||
-rw-r--r-- | imag-link/Cargo.toml | 1 | ||||
-rw-r--r-- | imag-link/src/main.rs | 163 | ||||
-rw-r--r-- | imag-link/tests/Makefile | 13 | ||||
-rw-r--r-- | imag-link/tests/link-test.sh | 100 | ||||
-rw-r--r-- | imag-link/tests/utils.sh | 6 | ||||
-rw-r--r-- | libimagrt/src/configuration.rs | 55 | ||||
-rw-r--r-- | libimagrt/src/lib.rs | 1 | ||||
-rw-r--r-- | libimagrt/src/runtime.rs | 144 | ||||
-rw-r--r-- | libimagrt/src/spec.rs | 30 |
10 files changed, 327 insertions, 188 deletions
@@ -76,6 +76,8 @@ $(TARGETS): %: .FORCE @$(CARGO) build --manifest-path ./$@/Cargo.toml $(BIN_TARGET_TESTS): %-test: % .FORCE + @$(ECHO) "\t[CARGO ][TEST]: \t$@" + @$(CARGO) test --manifest-path ./$(subst -test,,$@)/Cargo.toml @$(ECHO) "\t[BINTEST]:\t$@" if [ -f $(subst -test,,$@)/tests/Makefile ]; then \ $(MAKE) -C $(subst -test,,$@)/tests || exit 1;\ diff --git a/imag-link/Cargo.toml b/imag-link/Cargo.toml index d346fe98..0f6d8d51 100644 --- a/imag-link/Cargo.toml +++ b/imag-link/Cargo.toml @@ -19,6 +19,7 @@ clap = ">=2.17" log = "0.3" version = "2.0.1" toml = "^0.4" +toml-query = "0.1" url = "1.2" [dependencies.libimagstore] diff --git a/imag-link/src/main.rs b/imag-link/src/main.rs index 2585ed00..f3dc6ca4 100644 --- a/imag-link/src/main.rs +++ b/imag-link/src/main.rs @@ -36,6 +36,7 @@ extern crate clap; extern crate semver; extern crate toml; +extern crate toml_query; extern crate url; #[macro_use] extern crate version; @@ -334,3 +335,165 @@ fn list_links_for_entry(store: &Store, entry: &mut FileLockEntry) { .ok(); } +#[cfg(test)] +mod tests { + use handle_internal_linking; + + use std::path::PathBuf; + use std::ffi::OsStr; + + use clap::{App, ArgMatches}; + use toml::value::Value; + use toml_query::read::TomlValueReadExt; + use toml_query::error::Result as TomlQueryResult; + + use libimagrt::spec::CliSpec; + use libimagrt::runtime::Runtime; + use libimagrt::error::RuntimeError; + use libimagrt::configuration::{Configuration, InternalConfiguration}; + + use libimagstore::storeid::StoreId; + use libimagstore::store::{Result as StoreResult, FileLockEntry}; + + static DEFAULT_ENTRY: &'static str = "\ +---\ +[imag]\ +links = []\ +version = \"0.3.0\"\ +---"; + + #[derive(Clone)] + struct MockLinkApp<'a> { + args: Vec<&'static str>, + inner: App<'a, 'a>, + } + + impl<'a> MockLinkApp<'a> { + fn new(args: Vec<&'static str>) -> Self { + MockLinkApp { + args: args, + inner: ::build_ui(Runtime::get_default_cli_builder("imag-link", + "0.3.0", + "Link entries test")), + } + } + } + + impl<'a> CliSpec<'a> for MockLinkApp<'a> { + fn name(&self) -> &str { + self.inner.get_name() + } + + fn matches(self) -> ArgMatches<'a> { + self.inner.get_matches_from(self.args) + } + } + + impl<'a> InternalConfiguration for MockLinkApp<'a> { + fn enable_logging(&self) -> bool { + false + } + + fn use_inmemory_fs(&self) -> bool { + true + } + } + + fn generate_test_config() -> Option<Configuration> { + ::toml::de::from_str("[store]\nimplicit-create=true") + .map(Configuration::with_value) + .ok() + } + + fn generate_test_runtime<'a>(mut args: Vec<&'static str>) -> Result<Runtime<'a>, RuntimeError> { + let mut cli_args = vec!["imag-link", "--rtp", "/tmp"]; + + cli_args.append(&mut args); + + let cli_app = MockLinkApp::new(cli_args); + Runtime::with_configuration(cli_app, generate_test_config()) + } + + fn create_test_entry<'a, S: AsRef<OsStr>>(rt: &'a Runtime, name: S) -> StoreResult<StoreId> { + let mut path = PathBuf::new(); + path.set_file_name(name); + + let id = StoreId::new_baseless(path)?; + let mut entry = rt.store().create(id.clone())?; + entry.get_content_mut().push_str(DEFAULT_ENTRY); + + Ok(id) + } + + fn get_entry_links<'a>(entry: &'a FileLockEntry<'a>) -> TomlQueryResult<&'a Value> { + entry.get_header().read(&"imag.links".to_owned()) + } + + fn links_toml_value<'a, I: IntoIterator<Item = &'static str>>(links: I) -> Value { + Value::Array(links + .into_iter() + .map(|s| Value::String(s.to_owned())) + .collect()) + } + + #[test] + fn test_link_modificates() { + let rt = generate_test_runtime(vec!["internal", "add", "--from", "test1", "--to", "test2"]) + .unwrap(); + + let test_id1 = create_test_entry(&rt, "test1").unwrap(); + let test_id2 = create_test_entry(&rt, "test2").unwrap(); + + handle_internal_linking(&rt); + + let test_entry1 = rt.store().get(test_id1).unwrap().unwrap(); + let test_links1 = get_entry_links(&test_entry1).unwrap(); + + let test_entry2 = rt.store().get(test_id2).unwrap().unwrap(); + let test_links2 = get_entry_links(&test_entry2).unwrap(); + + assert_ne!(*test_links1, links_toml_value(vec![])); + assert_ne!(*test_links2, links_toml_value(vec![])); + } + + #[test] + fn test_linking_links() { + let rt = generate_test_runtime(vec!["internal", "add", "--from", "test1", "--to", "test2"]) + .unwrap(); + + let test_id1 = create_test_entry(&rt, "test1").unwrap(); + let test_id2 = create_test_entry(&rt, "test2").unwrap(); + + handle_internal_linking(&rt); + + let test_entry1 = rt.store().get(test_id1).unwrap().unwrap(); + let test_links1 = get_entry_links(&test_entry1).unwrap(); + + let test_entry2 = rt.store().get(test_id2).unwrap().unwrap(); + let test_links2 = get_entry_links(&test_entry2).unwrap(); + + assert_eq!(*test_links1, links_toml_value(vec!["test2"])); + assert_eq!(*test_links2, links_toml_value(vec!["test1"])); + } + + #[test] + fn test_multilinking() { + let rt = generate_test_runtime(vec!["internal", "add", "--from", "test1", "--to", "test2"]) + .unwrap(); + + let test_id1 = create_test_entry(&rt, "test1").unwrap(); + let test_id2 = create_test_entry(&rt, "test2").unwrap(); + + handle_internal_linking(&rt); + handle_internal_linking(&rt); + + let test_entry1 = rt.store().get(test_id1).unwrap().unwrap(); + let test_links1 = get_entry_links(&test_entry1).unwrap(); + + let test_entry2 = rt.store().get(test_id2).unwrap().unwrap(); + let test_links2 = get_entry_links(&test_entry2).unwrap(); + + assert_eq!(*test_links1, links_toml_value(vec!["test2"])); + assert_eq!(*test_links2, links_toml_value(vec!["test1"])); + } +} diff --git a/imag-link/tests/Makefile b/imag-link/tests/Makefile deleted file mode 100644 index 2713587a..00000000 --- a/imag-link/tests/Makefile +++ /dev/null @@ -1,13 +0,0 @@ -ECHO=$(shell which echo) -e -TARGETS=$(shell find -name "*test.sh" -type f) -BASH=$(shell which bash) - -all: $(TARGETS) - @$(ECHO) $(TARGETS) - -$(TARGETS): %: .FORCE - @$(ECHO) "\t[BASH ]:\t$@" - @$(BASH) $@ - -.FORCE: - diff --git a/imag-link/tests/link-test.sh b/imag-link/tests/link-test.sh deleted file mode 100644 index 34b74aea..00000000 --- a/imag-link/tests/link-test.sh +++ /dev/null @@ -1,100 +0,0 @@ -#!/usr/bin/env bash - -source $(dirname ${BASH_SOURCE[0]})/../../tests/utils.sh -source $(dirname ${BASH_SOURCE[0]})/utils.sh - -default_entry() { - cat <<EOS ---- -[imag] -links = [] -version = "0.3.0" ---- - -EOS -} - -entry_linked_to() { - cat <<EOS ---- -[imag] -links = [$1] -version = "0.3.0" ---- - -EOS -} - -mktestentry() { - mkdir -p ${STORE} - default_entry > ${STORE}/$1 -} - -test_link_modificates() { - mktestentry "test" - mktestentry "test2" - - imag-link internal add --from "test" --to "test2" - - if [ "$(default_entry)" == "$(cat_entry 'test')" ] || - [ "$(default_entry)" == "$(cat_entry 'test2')" ] - then - err "Entry was unmodified after linking" - return 1; - fi -} - -test_linking_links() { - mktestentry "test" - mktestentry "test2" - - imag-link internal add --from "test" --to "test2" - - if [[ "$(entry_linked_to '"test"')" != "$(cat_entry 'test2')" ]]; - then - err "Linking to 'test' didn't succeed for 'test2'" - err "\n$(cat_entry 'test2')\n" - fi - - if [[ "$(entry_linked_to '"test2"')" != "$(cat_entry 'test')" ]]; - then - err "Linking to 'test2' didn't succeed for 'test'" - err "\n$(cat_entry 'test')\n" - fi -} - -test_multilinking() { - mktestentry "test" - mktestentry "test2" - - imag-link internal add --from "test" --to "test2" || { - err "Linking failed"; return 1 - } - - imag-link internal add --from "test" --to "test2" || { - err "Linking again failed"; return 1 - } - - local linked_to_test="$(entry_linked_to '"test"' | sha1sum)" - local linked_to_test2="$(entry_linked_to '"test2"' | sha1sum)" - local t2="$(cat_entry 'test2' | sha1sum)" - local t1="$(cat_entry 'test' | sha1sum)" - - if [ "${linked_to_test}" != "${t2}" ]; - then - err "Linking twice to 'test' didn't result in the expected output for 'test2'" - err "\n$(cat_entry 'test2')\n" - fi - - if [ "${linked_to_test2}" != "${t1}" ]; - then - err "Linking twice to 'test2' didn't result in the expected output for 'test'" - err "\n$(cat_entry 'test')\n" - fi -} - -invoke_tests \ - test_link_modificates \ - test_linking_links \ - test_multilinking - diff --git a/imag-link/tests/utils.sh b/imag-link/tests/utils.sh deleted file mode 100644 index ec6c42a9..00000000 --- a/imag-link/tests/utils.sh +++ /dev/null @@ -1,6 +0,0 @@ -source $(dirname ${BASH_SOURCE[0]})/../../tests/utils.sh - -imag-link() { - imag-call-binary "$(dirname ${BASH_SOURCE[0]})/../../target/debug/" imag-link $* -} - diff --git a/libimagrt/src/configuration.rs b/libimagrt/src/configuration.rs index fe1c1614..1ee76d59 100644 --- a/libimagrt/src/configuration.rs +++ b/libimagrt/src/configuration.rs @@ -22,9 +22,7 @@ use std::result::Result as RResult; use std::ops::Deref; use toml::Value; - -use error::RuntimeErrorKind as REK; -use libimagerror::into::IntoError; +use clap::App; generate_error_module!( generate_error_types!(ConfigError, ConfigErrorKind, @@ -92,6 +90,16 @@ impl Configuration { }) } + /// Get a new configuration object built from the given toml value. + pub fn with_value(value: Value) -> Configuration { + Configuration{ + verbosity: get_verbosity(&value), + editor: get_editor(&value), + editor_opts: get_editor_opts(&value), + config: value, + } + } + /// Get the Editor setting from the configuration pub fn editor(&self) -> Option<&String> { self.editor.as_ref() @@ -219,12 +227,14 @@ fn fetch_config(rtp: &PathBuf) -> Result<Value> { use std::env; use std::fs::File; use std::io::Read; + use std::io::Write; + use std::io::stderr; use xdg_basedir; use itertools::Itertools; use libimagutil::variants::generate_variants as gen_vars; - use self::error::MapErrInto; + use libimagerror::trace::trace_error; let variants = vec!["config", "config.toml", "imagrc", "imagrc.toml"]; let modifier = |base: &PathBuf, v: &'static str| { @@ -245,8 +255,7 @@ fn fetch_config(rtp: &PathBuf) -> Result<Value> { ].iter() .flatten() .filter(|path| path.exists() && path.is_file()) - .filter_map(|path| if path.exists() && path.is_file() { - debug!("Reading {:?}", path); + .map(|path| { let content = { let mut s = String::new(); let f = File::open(path); @@ -258,19 +267,29 @@ fn fetch_config(rtp: &PathBuf) -> Result<Value> { s }; - trace!("Contents of config file: \n---\n{}\n---", content); - - let toml = ::toml::de::from_str(&content[..]) - .map_err_into(ConfigErrorKind::TOMLParserError) - .map_err(Box::new) - .map_err(|e| REK::Instantiate.into_error_with_cause(e)); - - Some(toml) - } else { - None + match ::toml::de::from_str(&content[..]) { + Ok(res) => res, + Err(e) => { + write!(stderr(), "Config file parser error:").ok(); + trace_error(&e); + None + } + } }) - .filter(|loaded| loaded.is_ok()) - .map(|inner| Value::Table(inner.unwrap())) + .filter(|loaded| loaded.is_some()) .nth(0) + .map(|inner| Value::Table(inner.unwrap())) .ok_or(ConfigErrorKind::NoConfigFileFound.into()) } + +pub trait InternalConfiguration { + fn enable_logging(&self) -> bool { + true + } + + fn use_inmemory_fs(&self) -> bool { + false + } +} + +impl<'a> InternalConfiguration for App<'a, 'a> {} diff --git a/libimagrt/src/lib.rs b/libimagrt/src/lib.rs index 63a49c45..55398013 100644 --- a/libimagrt/src/lib.rs +++ b/libimagrt/src/lib.rs @@ -53,4 +53,5 @@ pub mod configuration; pub mod logger; pub mod runtime; pub mod setup; +pub mod spec; diff --git a/libimagrt/src/runtime.rs b/libimagrt/src/runtime.rs index e0df57fc..1b5f3f7c 100644 --- a/libimagrt/src/runtime.rs +++ b/libimagrt/src/runtime.rs @@ -29,13 +29,15 @@ use clap::{Arg, ArgMatches}; use log; use log::LogLevelFilter; -use configuration::Configuration; +use configuration::{Configuration, InternalConfiguration}; use error::RuntimeError; use error::RuntimeErrorKind; use error::MapErrInto; use logger::ImagLogger; use libimagstore::store::Store; +use libimagstore::file_abstraction::InMemoryFileAbstraction; +use spec::CliSpec; /// The Runtime object /// @@ -54,60 +56,25 @@ impl<'a> Runtime<'a> { /// in $HOME/.imag/config, $XDG_CONFIG_DIR/imag/config or from env("$IMAG_CONFIG") /// and builds the Runtime object with it. /// - /// The cli_spec object should be initially build with the ::get_default_cli_builder() function. - pub fn new(mut cli_spec: App<'a, 'a>) -> Result<Runtime<'a>, RuntimeError> { - use std::env; - use std::io::stdout; - - use clap::Shell; - + /// The cli_app object should be initially build with the ::get_default_cli_builder() function. + pub fn new<C>(cli_app: C) -> Result<Runtime<'a>, RuntimeError> + where C: Clone + CliSpec<'a> + InternalConfiguration + { use libimagerror::trace::trace_error; use libimagerror::into::IntoError; use configuration::error::ConfigErrorKind; - let matches = cli_spec.clone().get_matches(); - - let is_debugging = matches.is_present("debugging"); - let is_verbose = matches.is_present("verbosity"); - let colored = !matches.is_present("no-color-output"); - - Runtime::init_logger(is_debugging, is_verbose, colored); - - match matches.value_of(Runtime::arg_generate_compl()) { - Some(shell) => { - debug!("Generating shell completion script, writing to stdout"); - let shell = shell.parse::<Shell>().unwrap(); // clap has our back here. - let appname = String::from(cli_spec.get_name()); - cli_spec.gen_completions_to(appname, shell, &mut stdout()); - }, - _ => debug!("Not generating shell completion script"), - } + let matches = cli_app.clone().matches(); - let rtp : PathBuf = matches.value_of("runtimepath") - .map_or_else(|| { - env::var("HOME") - .map(PathBuf::from) - .map(|mut p| { p.push(".imag"); p}) - .unwrap_or_else(|_| { - panic!("You seem to be $HOME-less. Please get a $HOME before using this software. We are sorry for you and hope you have some accommodation anyways."); - }) - }, PathBuf::from); - let storepath = matches.value_of("storepath") - .map_or_else(|| { - let mut spath = rtp.clone(); - spath.push("store"); - spath - }, PathBuf::from); + let rtp = get_rtp_match(&matches); - let configpath = matches.value_of("config") + let configpath = matches.value_of(Runtime::arg_config_name()) .map_or_else(|| rtp.clone(), PathBuf::from); - debug!("RTP path = {:?}", rtp); - debug!("Store path = {:?}", storepath); debug!("Config path = {:?}", configpath); - let cfg = match Configuration::new(&configpath) { + let config = match Configuration::new(&configpath) { Err(e) => if e.err_type() != ConfigErrorKind::NoConfigFileFound { return Err(RuntimeErrorKind::Instantiate.into_error_with_cause(Box::new(e))); } else { @@ -116,32 +83,91 @@ impl<'a> Runtime<'a> { None }, - Ok(mut cfg) => { - if let Err(e) = cfg.override_config(get_override_specs(&matches)) { + Ok(mut config) => { + if let Err(e) = config.override_config(get_override_specs(&matches)) { error!("Could not apply config overrides"); trace_error(&e); // TODO: continue question (interactive) } - Some(cfg) + Some(config) } }; - let store_config = match cfg { + Runtime::_new(cli_app, matches, config) + } + + /// Builds the Runtime object using the given `config`. + pub fn with_configuration<C>(cli_app: C, config: Option<Configuration>) + -> Result<Runtime<'a>, RuntimeError> + where C: Clone + CliSpec<'a> + InternalConfiguration + { + let matches = cli_app.clone().matches(); + Runtime::_new(cli_app, matches, config) + } + + fn _new<C>(mut cli_app: C, matches: ArgMatches<'a>, config: Option<Configuration>) + -> Result<Runtime<'a>, RuntimeError> + where C: Clone + CliSpec<'a> + InternalConfiguration + { + use std::io::stdout; + + use clap::Shell; + + let is_debugging = matches.is_present(Runtime::arg_debugging_name()); + + if cli_app.enable_logging() { + let is_verbose = matches.is_present(Runtime::arg_verbosity_name()); + let colored = !matches.is_present(Runtime::arg_no_color_output_name()); + + Runtime::init_logger(is_debugging, is_verbose, colored); + } + + match matches.value_of(Runtime::arg_generate_compl()) { + Some(shell) => { + debug!("Generating shell completion script, writing to stdout"); + let shell = shell.parse::<Shell>().unwrap(); // clap has our back here. + let appname = String::from(cli_app.name()); + cli_app.completions(appname, shell, &mut stdout()); + }, + _ => debug!("Not generating shell completion script"), + } + + let rtp = get_rtp_match(&matches); + + let storepath = matches.value_of(Runtime::arg_storepath_name()) + .map_or_else(|| { + let mut spath = rtp.clone(); + spath.push("store"); + spath + }, PathBuf::from); + + debug!("RTP path = {:?}", rtp); + debug!("Store path = {:?}", storepath); + + let store_config = match config { Some(ref c) => c.store_config().cloned(), None => None, }; if is_debugging { - write!(stderr(), "Config: {:?}\n", cfg).ok(); + write!(stderr(), "Config: {:?}\n", config).ok(); write!(stderr(), "Store-config: {:?}\n", store_config).ok(); } - Store::new(storepath.clone(), store_config).map(|store| { + let store_result = if cli_app.use_inmemory_fs() { + Store::new_with_backend(storepath, + store_config, + Box::new(InMemoryFileAbstraction::new())) + } else { + Store::new(storepath, store_config) + }; + + store_result.map(|store| { Runtime { cli_matches: matches, - configuration: cfg, + configuration: config, rtp: rtp, store: store, } @@ -403,6 +429,22 @@ impl<'a> Runtime<'a> { } } +fn get_rtp_match<'a>(matches: &ArgMatches<'a>) -> PathBuf { + use std::env; + + matches.value_of(Runtime::arg_runtimepath_name()) + .map_or_else(|| { + env::var("HOME") + .map(PathBuf::from) + .map(|mut p| { p.push(".imag"); p }) + .unwrap_or_else(|_| { + panic!("You seem to be $HOME-less. Please get a $HOME before using this \ + software. We are sorry for you and hope you have some \ + accommodation anyways."); + }) + }, PathBuf::from) +} + fn get_override_specs(matches: &ArgMatches) -> Vec<String> { matches .values_of("config-override") diff --git a/libimagrt/src/spec.rs b/libimagrt/src/spec.rs new file mode 100644 index 00000000..0293f249 --- /dev/null +++ b/libimagrt/src/spec.rs @@ -0,0 +1,30 @@ +use std::io::Write; + +use clap::{App, ArgMatches, Shell}; + +/// An abstraction over `clap::App` functionality needed for initializing `Runtime`. Different +/// implementations can be used for testing `imag` binaries without running them as separate +/// processes. +pub trait CliSpec<'a> { + fn name(&self) -> &str; + fn matches(self) -> ArgMatches<'a>; + fn completions<W: Write, S: Into<String>>(&mut self, _: S, _: Shell, _: &mut W) {} +} + +impl<'a> CliSpec<'a> for App<'a, 'a> { + fn name(&self) -> &str { + self.get_name() + } + + fn matches(self) -> ArgMatches<'a> { + self.get_matches() + } + + fn completions<W: Write, S: Into<String>>(&mut self, + bin_name: S, + for_shell: Shell, + buf: &mut W) { + + self.gen_completions_to(bin_name, for_shell, buf); + } +} |