change init command & add integration test

- now need to specify device name via cmd arg
- can use private repository with ssh key or ssh-agent
- adding integration test utility crates
This commit is contained in:
qwjyh 2024-03-07 14:59:58 +09:00
parent 9935f79920
commit 4283e1e98a
8 changed files with 387 additions and 59 deletions

207
Cargo.lock generated
View file

@ -65,6 +65,36 @@ version = "1.0.75"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
[[package]]
name = "assert_cmd"
version = "2.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed72493ac66d5804837f480ab3766c72bdfab91a65e565fc54fa9e42db0073a8"
dependencies = [
"anstyle",
"bstr",
"doc-comment",
"predicates",
"predicates-core",
"predicates-tree",
"wait-timeout",
]
[[package]]
name = "assert_fs"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2cd762e110c8ed629b11b6cde59458cc1c71de78ebbcc30099fc8e0403a2a2ec"
dependencies = [
"anstyle",
"doc-comment",
"globwalk",
"predicates",
"predicates-core",
"predicates-tree",
"tempfile",
]
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.1.0" version = "1.1.0"
@ -83,6 +113,17 @@ version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07"
[[package]]
name = "bstr"
version = "1.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05efc5cfd9110c8416e471df0e96702d58690178e206e61b7173706673c93706"
dependencies = [
"memchr",
"regex-automata",
"serde",
]
[[package]] [[package]]
name = "byte-unit" name = "byte-unit"
version = "4.0.19" version = "4.0.19"
@ -229,6 +270,12 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "difflib"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
[[package]] [[package]]
name = "dirs" name = "dirs"
version = "5.0.1" version = "5.0.1"
@ -250,6 +297,12 @@ dependencies = [
"windows-sys 0.48.0", "windows-sys 0.48.0",
] ]
[[package]]
name = "doc-comment"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
[[package]] [[package]]
name = "dyn-clone" name = "dyn-clone"
version = "1.0.16" version = "1.0.16"
@ -291,6 +344,21 @@ dependencies = [
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
[[package]]
name = "fastrand"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5"
[[package]]
name = "float-cmp"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4"
dependencies = [
"num-traits",
]
[[package]] [[package]]
name = "form_urlencoded" name = "form_urlencoded"
version = "1.2.1" version = "1.2.1"
@ -326,6 +394,30 @@ dependencies = [
"url", "url",
] ]
[[package]]
name = "globset"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1"
dependencies = [
"aho-corasick",
"bstr",
"log",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "globwalk"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757"
dependencies = [
"bitflags 2.4.1",
"ignore",
"walkdir",
]
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.14.3" version = "0.14.3"
@ -360,6 +452,22 @@ dependencies = [
"unicode-normalization", "unicode-normalization",
] ]
[[package]]
name = "ignore"
version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b46810df39e66e925525d6e38ce1e7f6e1d208f72dc39757880fcb66e2c58af1"
dependencies = [
"crossbeam-deque",
"globset",
"log",
"memchr",
"regex-automata",
"same-file",
"walkdir",
"winapi-util",
]
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.1.0" version = "2.1.0"
@ -499,9 +607,9 @@ checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.6.4" version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149"
[[package]] [[package]]
name = "memoffset" name = "memoffset"
@ -533,6 +641,12 @@ dependencies = [
"unicode-segmentation", "unicode-segmentation",
] ]
[[package]]
name = "normalize-line-endings"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be"
[[package]] [[package]]
name = "ntapi" name = "ntapi"
version = "0.4.1" version = "0.4.1"
@ -542,6 +656,15 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "num-traits"
version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a"
dependencies = [
"autocfg",
]
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.18.0" version = "1.18.0"
@ -613,6 +736,36 @@ version = "0.3.27"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964"
[[package]]
name = "predicates"
version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68b87bfd4605926cdfefc1c3b5f8fe560e3feca9d5552cf68c466d3d8236c7e8"
dependencies = [
"anstyle",
"difflib",
"float-cmp",
"normalize-line-endings",
"predicates-core",
"regex",
]
[[package]]
name = "predicates-core"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b794032607612e7abeb4db69adb4e33590fa6cf1149e95fd7cb00e634b92f174"
[[package]]
name = "predicates-tree"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "368ba315fb8c5052ab692e68a0eefec6ec57b23a36959c14496f0b0df2c0cecf"
dependencies = [
"predicates-core",
"termtree",
]
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.70" version = "1.0.70"
@ -719,6 +872,15 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]] [[package]]
name = "scopeguard" name = "scopeguard"
version = "1.2.0" version = "1.2.0"
@ -827,6 +989,19 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "tempfile"
version = "3.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa"
dependencies = [
"cfg-if",
"fastrand",
"redox_syscall",
"rustix",
"windows-sys 0.52.0",
]
[[package]] [[package]]
name = "termcolor" name = "termcolor"
version = "1.4.0" version = "1.4.0"
@ -836,6 +1011,12 @@ dependencies = [
"winapi-util", "winapi-util",
] ]
[[package]]
name = "termtree"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76"
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.50" version = "1.0.50"
@ -939,6 +1120,25 @@ version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "wait-timeout"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6"
dependencies = [
"libc",
]
[[package]]
name = "walkdir"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
dependencies = [
"same-file",
"winapi-util",
]
[[package]] [[package]]
name = "wasi" name = "wasi"
version = "0.11.0+wasi-snapshot-preview1" version = "0.11.0+wasi-snapshot-preview1"
@ -1113,6 +1313,8 @@ name = "xdbm"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"assert_cmd",
"assert_fs",
"byte-unit", "byte-unit",
"clap", "clap",
"clap-verbosity-flag", "clap-verbosity-flag",
@ -1122,6 +1324,7 @@ dependencies = [
"inquire", "inquire",
"log", "log",
"pathdiff", "pathdiff",
"predicates",
"serde", "serde",
"serde_yaml", "serde_yaml",
"sysinfo", "sysinfo",

View file

@ -20,3 +20,8 @@ byte-unit = "4.0.19"
anyhow = "1.0" anyhow = "1.0"
pathdiff = "0.2.1" pathdiff = "0.2.1"
unicode-width = "0.1.11" unicode-width = "0.1.11"
[dev-dependencies]
assert_cmd = "2.0.14"
assert_fs = "1.1.1"
predicates = "3.1.0"

View file

@ -1,4 +1,13 @@
# TODO: # TODO:
- [ ] split subcommands to functions - [x] split subcommands to functions
- [ ] write test for init subcommand
- [ ] write test with existing repo
- [ ] with ssh credential
- [ ] ssh-agent
- [ ] specify key
- [ ] add sync subcommand
- [ ] add check subcommand
- [ ] reorganize cmd option for storage - [ ] reorganize cmd option for storage
- [ ] use subcommand - [ ] use subcommand
<!-- vim: set sw=2 ts=2: -->

View file

@ -6,7 +6,7 @@ use crate::PathBuf;
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use clap_verbosity_flag::Verbosity; use clap_verbosity_flag::Verbosity;
#[derive(Parser)] #[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)] #[command(author, version, about, long_about = None)]
pub(crate) struct Cli { pub(crate) struct Cli {
#[command(subcommand)] #[command(subcommand)]
@ -20,13 +20,21 @@ pub(crate) struct Cli {
pub(crate) verbose: Verbosity, pub(crate) verbose: Verbosity,
} }
#[derive(Subcommand)] #[derive(Subcommand, Debug)]
pub(crate) enum Commands { pub(crate) enum Commands {
/// Initialize for this device. /// Initialize for this device.
/// Provide `repo_url` to use existing repository, otherwise this device will be configured as the
/// first device.
Init { Init {
/// Name for this device
device_name: String,
/// Url for existing repository. Empty if init for the first time.
#[arg(short, long)]
repo_url: Option<String>, // url? repo_url: Option<String>, // url?
/// Whether to use ssh-agent
#[arg(long)]
use_sshagent: bool,
/// Manually specify ssh key
#[arg(long)]
ssh_key: Option<PathBuf>,
}, },
/// Manage storages. /// Manage storages.
@ -42,14 +50,14 @@ pub(crate) enum Commands {
Check {}, Check {},
} }
#[derive(clap::Args)] #[derive(clap::Args, Debug)]
#[command(args_conflicts_with_subcommands = true)] #[command(args_conflicts_with_subcommands = true)]
pub(crate) struct StorageArgs { pub(crate) struct StorageArgs {
#[command(subcommand)] #[command(subcommand)]
pub(crate) command: StorageCommands, pub(crate) command: StorageCommands,
} }
#[derive(Subcommand)] #[derive(Subcommand, Debug)]
pub(crate) enum StorageCommands { pub(crate) enum StorageCommands {
/// Add new storage. /// Add new storage.
Add { Add {

View file

@ -1,24 +1,91 @@
//! Init subcommand. //! Init subcommand.
//! Initialize xdbm for the device. //! Initialize xdbm for the device.
use crate::{ use crate::{add_and_commit, full_status, get_devices, write_devices, Device, DEVICESFILE};
add_and_commit, full_status, get_devices, set_device_name, write_devices, Device, DEVICESFILE, use anyhow::{anyhow, Context, Ok, Result};
}; use core::panic;
use anyhow::{anyhow, Context, Result}; use git2::{Cred, RemoteCallbacks, Repository};
use git2::Repository; use inquire::Password;
use std::env;
use std::fs::File; use std::fs::File;
use std::io::{BufWriter, Write}; use std::io::{BufWriter, Write};
use std::path::{self, Path}; use std::path::{self, Path, PathBuf};
pub(crate) fn cmd_init(repo_url: Option<String>, config_dir: &path::PathBuf) -> Result<()> { fn clone_repo(
let is_first_device: bool; repo_url: &String,
use_sshagent: bool,
ssh_key: Option<PathBuf>,
config_dir: &path::PathBuf,
) -> Result<Repository> {
// dont use credentials
if ssh_key.is_none() && !use_sshagent {
info!("No authentication will be used.");
info!("Use either ssh_key or ssh-agent to access private repository");
return Ok(Repository::clone(&repo_url, &config_dir)?);
}
// using credentials
let mut callbacks = RemoteCallbacks::new();
callbacks.credentials(|_url, username_from_url, _allowed_types| {
if let Some(key) = &ssh_key {
info!("Using provided ssh key to access the repository");
let passwd = match Password::new("SSH passphrase").prompt() {
std::result::Result::Ok(s) => Some(s),
Err(err) => {
error!("Failed to get ssh passphrase: {:?}", err);
None
}
};
Cred::ssh_key(
username_from_url
.context("No username found from the url")
.unwrap(),
None,
&key as &Path,
passwd.as_deref(),
)
} else if use_sshagent {
// use ssh agent
info!("Using ssh agent to access the repository");
Cred::ssh_key_from_agent(
username_from_url
.context("No username found from the url")
.unwrap(),
)
} else {
error!("no ssh_key and use_sshagent");
panic!("This option must be unreachable.")
}
});
// fetch options
let mut fo = git2::FetchOptions::new();
fo.remote_callbacks(callbacks);
let mut builder = git2::build::RepoBuilder::new();
builder.fetch_options(fo);
Ok(builder.clone(&repo_url, config_dir)?)
}
pub(crate) fn cmd_init(
device_name: String,
repo_url: Option<String>,
use_sshagent: bool,
ssh_key: Option<PathBuf>,
config_dir: &path::PathBuf,
) -> Result<()> {
// validate device name
if device_name.chars().count() == 0 {
log::error!("Device name cannnot by empty");
return Err(anyhow!("Device name is empty"));
}
// get repo or initialize it // get repo or initialize it
let repo = match repo_url { let (is_first_device, repo) = match repo_url {
Some(repo_url) => { Some(repo_url) => {
trace!("repo: {}", repo_url); trace!("repo: {}", repo_url);
let repo = Repository::clone(&repo_url, &config_dir)?; let repo = clone_repo(&repo_url, use_sshagent, ssh_key, config_dir)?;
is_first_device = false; (false, repo)
repo
} }
None => { None => {
trace!("No repo provided"); trace!("No repo provided");
@ -37,14 +104,15 @@ pub(crate) fn cmd_init(repo_url: Option<String>, config_dir: &path::PathBuf) ->
add_and_commit(&repo, Path::new(".gitignore"), "Add devname to gitignore.")?; add_and_commit(&repo, Path::new(".gitignore"), "Add devname to gitignore.")?;
full_status(&repo)?; full_status(&repo)?;
} }
is_first_device = true; (true, repo)
repo
} }
}; };
full_status(&repo)?; full_status(&repo)?;
// set device name // set device name
let device = set_device_name()?; // let device = set_device_name()?;
let device = Device::new(device_name);
trace!("Device information: {:?}", device);
// save devname // save devname
let devname_path = &config_dir.join("devname"); let devname_path = &config_dir.join("devname");

View file

@ -87,7 +87,8 @@ pub fn get_device(config_dir: &Path) -> Result<Device> {
/// Get `Vec<Device>` from yaml file in `config_dir`. /// Get `Vec<Device>` from yaml file in `config_dir`.
pub fn get_devices(config_dir: &Path) -> Result<Vec<Device>> { pub fn get_devices(config_dir: &Path) -> Result<Vec<Device>> {
trace!("get_devices"); trace!("get_devices");
let f = File::open(config_dir.join(DEVICESFILE))?; let f =
File::open(config_dir.join(DEVICESFILE)).context(format!("{} not found", DEVICESFILE))?;
let reader = BufReader::new(f); let reader = BufReader::new(f);
let yaml: Vec<Device> = let yaml: Vec<Device> =
serde_yaml::from_reader(reader).context("Failed to parse devices.yml")?; serde_yaml::from_reader(reader).context("Failed to parse devices.yml")?;

View file

@ -45,11 +45,13 @@ fn main() -> Result<()> {
.filter_level(cli.verbose.log_level_filter()) .filter_level(cli.verbose.log_level_filter())
.init(); .init();
trace!("Start logging..."); trace!("Start logging...");
trace!("args: {:?}", cli);
let config_dir: std::path::PathBuf = match cli.config_dir { let config_dir: std::path::PathBuf = match cli.config_dir {
Some(path) => path, Some(path) => path,
None => { None => {
let mut config_dir = dirs::config_local_dir().context("Failed to get default config dir.")?; let mut config_dir =
dirs::config_local_dir().context("Failed to get default config dir.")?;
config_dir.push("xdbm"); config_dir.push("xdbm");
config_dir config_dir
} }
@ -57,7 +59,12 @@ fn main() -> Result<()> {
trace!("Config dir: {:?}", config_dir); trace!("Config dir: {:?}", config_dir);
match cli.command { match cli.command {
Commands::Init { repo_url } => cmd_init::cmd_init(repo_url, &config_dir)?, Commands::Init {
device_name,
repo_url,
use_sshagent,
ssh_key,
} => cmd_init::cmd_init(device_name, repo_url, use_sshagent, ssh_key, &config_dir)?,
Commands::Storage(storage) => { Commands::Storage(storage) => {
let repo = Repository::open(&config_dir).context( let repo = Repository::open(&config_dir).context(
"Repository doesn't exist on the config path. Please run init to initialize the repository.", "Repository doesn't exist on the config path. Please run init to initialize the repository.",
@ -98,38 +105,6 @@ fn main() -> Result<()> {
Ok(()) Ok(())
} }
/// Set device name interactively.
fn set_device_name() -> Result<Device> {
let validator = |input: &str| {
if input.chars().count() == 0 {
Ok(Validation::Invalid("Need at least 1 character.".into()))
} else {
Ok(Validation::Valid)
}
};
let device_name = Text::new("Provide name for this device:")
.with_validator(validator)
.prompt();
let device_name = match device_name {
Ok(device_name) => {
println!("device name: {}", device_name);
device_name
}
Err(err) => {
println!("Error {}", err);
return Err(anyhow!(err));
}
};
let device = Device::new(device_name);
trace!("Device information: {:?}", device);
trace!("Serialized: \n{}", serde_yaml::to_string(&device).unwrap());
return Ok(device);
}
fn ask_unique_name(storages: &HashMap<String, Storage>, target: String) -> Result<String> { fn ask_unique_name(storages: &HashMap<String, Storage>, target: String) -> Result<String> {
let mut disk_name = String::new(); let mut disk_name = String::new();
loop { loop {

59
tests/cli.rs Normal file
View file

@ -0,0 +1,59 @@
use assert_cmd::prelude::*;
use assert_fs::prelude::*;
mod cmd_init {
use anyhow::{Ok, Result};
use assert_cmd::{cargo::CommandCargoExt, Command};
use predicates::prelude::predicate;
#[test]
fn init_with_tmpdir() -> Result<()> {
let config_dir = assert_fs::TempDir::new()?;
let mut cmd = Command::cargo_bin("xdbm")?;
cmd.arg("-c")
.arg(config_dir.path())
.arg("init")
.arg("testdev");
cmd.assert().success().stdout(predicate::str::contains(""));
assert_eq!(
std::fs::read_to_string(config_dir.path().join("devname"))?,
"testdev\n"
);
Ok(())
}
#[test]
fn init_with_existing_repo() -> Result<()> {
// 1st device
let config_dir_1 = assert_fs::TempDir::new()?;
let mut cmd1 = Command::cargo_bin("xdbm")?;
cmd1.arg("-c")
.arg(config_dir_1.path())
.arg("init")
.arg("first");
cmd1.assert().success().stdout(predicate::str::contains(""));
// 2nd device
let config_dir_2 = assert_fs::TempDir::new()?;
let mut cmd2 = Command::cargo_bin("xdbm")?;
cmd2.arg("-c")
.arg(config_dir_2.path())
.arg("init")
.arg("second")
.arg("-r")
.arg(config_dir_1.path());
cmd2.assert().success().stdout(predicate::str::contains(""));
assert_eq!(
std::fs::read_to_string(config_dir_2.path().join("devname"))?,
"second\n"
);
assert!(
std::fs::read_to_string(config_dir_2.path().join("devices.yml"))?.contains("first")
);
assert!(
std::fs::read_to_string(config_dir_2.path().join("devices.yml"))?.contains("second")
);
Ok(())
}
}