From a1d5718ac92f5ac3e68ea5db1ad0ea11cc1d36f2 Mon Sep 17 00:00:00 2001 From: Michael Greenberg Date: Sun, 4 Jul 2021 18:19:31 -0700 Subject: [PATCH] `--new` flag for creating files from empty (#38) Running `--new FILE.EXT` will: - infer the output format form `EXT` - use `FILE.EXT` as the output - start with a single, empty, named directory (but allocate a bit more space) In implementing this, I realized that introducing metadata (fad45bed4ba3e84a7ed4dd9552b551eaac541632) meant we no longer inferred types automatically. I added a type `Typ::Auto` and some inference code. --- CHANGELOG.md | 1 + docs/ffs.1.md | 33 ++++- man/ffs.1 | 48 +++++- src/cli.rs | 9 ++ src/config.rs | 2 + src/format.rs | 82 ++++++++++- src/fs.rs | 2 +- src/main.rs | 399 +++++++++++++++++++++++++++++++------------------- tests/auto.sh | 47 ++++++ tests/new.sh | 43 ++++++ 10 files changed, 499 insertions(+), 167 deletions(-) create mode 100755 tests/auto.sh create mode 100755 tests/new.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 513382c..4d8f1bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ * `--pretty` flag for JSON and TOML. * Wrote INSTALL.md. * Improved manpage. +* `--new` flag for starting from an empty filesystem. ## 0.1.0 - 2021-06-26 diff --git a/docs/ffs.1.md b/docs/ffs.1.md index c2a37a7..562e228 100644 --- a/docs/ffs.1.md +++ b/docs/ffs.1.md @@ -8,6 +8,7 @@ ffs - the file filesystem # SYNOPSIS | ffs \[*FLAGS*\] \[*OPTIONS*\] \[*INPUT*\] +| ffs \[*FLAGS*\] \[*OPTIONS*\] --new \[*OUTPUT*\] | ffs *--completions* *SHELL* | ffs \[*-h*\|*--help*\] | ffs \[*-V*\|*--version*\] @@ -103,6 +104,14 @@ installed on your system to use *ffs*. : Sets the output file for saving changes (defaults to stdout) +--new *NEW* + +: Mounts an empty filesystem, inferring a mountpoint and output format. Running --new *FILE*.*EXT* is morally equivalent to running: +``` +echo '{}' | ffs --source json -o *FILE*.*EXT* --target *EXT* -m *FILE* +``` +where the mountpoint *FILE* will be created (and removed) by ffs. + --completions *SHELL* : Generate shell completions (and exits) [possible values: bash, fish, @@ -138,13 +147,20 @@ formats (currently, JSON, TOML, and YAML); *ffs* maps values in these formats to filesystems. Here are the different types and how they're mapped to a filesystem: +auto + +: Automatically detected. The following order is used for UTF-8 + encodable data: null, boolean, integer, float, datetime, string. If + data can't be encoded in UTF-8, it will always be bytes. + boolean : Mapped to a **file**. Either *true* or *false*. bytes -: Mapped to a **file**. When serializing back to format, it will be encoded in base64. +: Mapped to a **file**. When saving, bytes are typically encoded in + base64. datetime @@ -165,9 +181,9 @@ list named elements, starting from 0. Filenames will be padded with zeros to ensure proper sorting; use *--unpadded* to disable padding. While mounted, you are free to use whatever filenames you like in a list - directory. When list directories are serialized back to a format, - filenames are ignored and the sorted order of the files (in the - current locale) will be used to determine the list order. + directory. When list directories are saved, filenames are ignored + and the sorted order of the files (in the current locale) will be + used to determine the list order. named @@ -229,6 +245,15 @@ umount commits # changes are written back to commits.json (-i is in-place mode) ``` +If you want to create a new file wholesale, the --new flag is helpful. + +```shell +ffs --new file.json +# do edits in file directory +umount file +# corresponding json is in file.json +``` + To mount a JSON file and write back out a YAML file, you could run: ```shell diff --git a/man/ffs.1 b/man/ffs.1 index 8d7c585..9d9a042 100644 --- a/man/ffs.1 +++ b/man/ffs.1 @@ -11,6 +11,10 @@ ffs [\f[I]FLAGS\f[R]] [\f[I]OPTIONS\f[R]] [\f[I]INPUT\f[R]] .PD 0 .P .PD +ffs [\f[I]FLAGS\f[R]] [\f[I]OPTIONS\f[R]] --new [\f[I]OUTPUT\f[R]] +.PD 0 +.P +.PD ffs \f[I]--completions\f[R] \f[I]SHELL\f[R] .PD 0 .P @@ -92,8 +96,25 @@ specified when running on stdin -o, --output \f[I]OUTPUT\f[R] Sets the output file for saving changes (defaults to stdout) .TP +--new \f[I]NEW\f[R] +Mounts an empty filesystem, inferring a mountpoint and output format. +Running --new \f[I]FILE\f[R].\f[I]EXT\f[R] is morally equivalent to +running: +.RS +.IP +.nf +\f[C] +echo \[aq]{}\[aq] | ffs --source json -o *FILE*.*EXT* --target *EXT* -m *FILE* +\f[R] +.fi +.PP +where the mountpoint \f[I]FILE\f[R] will be created (and removed) by +ffs. +.RE +.TP --completions \f[I]SHELL\f[R] -Generate shell completions and exit [possible values: bash, fish, zsh] +Generate shell completions (and exits) [possible values: bash, fish, +zsh] .TP -s, --source \f[I]SOURCE_FORMAT\f[R] Specify the source format explicitly (by default, automatically inferred @@ -117,13 +138,19 @@ formats (currently, JSON, TOML, and YAML); \f[I]ffs\f[R] maps values in these formats to filesystems. Here are the different types and how they\[aq]re mapped to a filesystem: .TP +auto +Automatically detected. +The following order is used for UTF-8 encodable data: null, boolean, +integer, float, datetime, string. +If data can\[aq]t be encoded in UTF-8, it will always be bytes. +.TP boolean Mapped to a \f[B]file\f[R]. Either \f[I]true\f[R] or \f[I]false\f[R]. .TP bytes Mapped to a \f[B]file\f[R]. -When serializing back to format, it will be encoded in base64. +When saving, bytes are typically encoded in base64. .TP datetime Mapped to a \f[B]file\f[R]. @@ -145,9 +172,9 @@ Filenames will be padded with zeros to ensure proper sorting; use \f[I]--unpadded\f[R] to disable padding. While mounted, you are free to use whatever filenames you like in a list directory. -When list directories are serialized back to a format, filenames are -ignored and the sorted order of the files (in the current locale) will -be used to determine the list order. +When list directories are saved, filenames are ignored and the sorted +order of the files (in the current locale) will be used to determine the +list order. .TP named Mapped to a \f[B]directory\f[R]. @@ -214,6 +241,17 @@ umount commits \f[R] .fi .PP +If you want to create a new file wholesale, the --new flag is helpful. +.IP +.nf +\f[C] +ffs --new file.json +# do edits in file directory +umount file +# corresponding json is in file.json +\f[R] +.fi +.PP To mount a JSON file and write back out a YAML file, you could run: .IP .nf diff --git a/src/cli.rs b/src/cli.rs index 7a5f291..d2802ab 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -134,6 +134,15 @@ pub fn app() -> App<'static, 'static> { .short("m") .takes_value(true) ) + .arg( + Arg::with_name("NEW") + .help("Mounts an empty filesystem, inferring a mountpoint and output format") + .long("new") + .takes_value(true) + .conflicts_with("INPLACE") + .conflicts_with("SOURCE_FORMAT") + .conflicts_with("OUTPUT") + ) .arg( Arg::with_name("INPUT") .help("Sets the input file ('-' means STDIN)") diff --git a/src/config.rs b/src/config.rs index 3a7af8a..3bac584 100644 --- a/src/config.rs +++ b/src/config.rs @@ -36,11 +36,13 @@ pub struct Config { pub enum Input { Stdin, File(PathBuf), + Empty, } impl std::fmt::Display for Input { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { match self { + Input::Empty => write!(f, ""), Input::Stdin => write!(f, ""), Input::File(file) => write!(f, "{}", file.display()), } diff --git a/src/format.rs b/src/format.rs index 342fcbe..d1d86b5 100644 --- a/src/format.rs +++ b/src/format.rs @@ -6,7 +6,7 @@ use tracing::{debug, error, info, instrument, warn}; use fuser::FileType; -use super::config::{Config, Output}; +use super::config::{Config, Input, Output}; use super::fs::{DirEntry, DirType, Entry, Inode, FS}; use ::toml as serde_toml; @@ -24,6 +24,7 @@ pub enum Format { /// Types classifying string data. #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum Typ { + Auto, Null, Boolean, Integer, @@ -53,6 +54,7 @@ impl std::fmt::Display for Typ { f, "{}", match self { + Typ::Auto => "auto", Typ::Null => "null", Typ::Boolean => "boolean", Typ::Bytes => "bytes", @@ -95,7 +97,9 @@ impl FromStr for Typ { fn from_str(s: &str) -> Result { let s = s.trim().to_lowercase(); - if s == "null" { + if s == "auto" { + Ok(Typ::Auto) + } else if s == "null" { Ok(Typ::Null) } else if s == "boolean" || s == "bool" { Ok(Typ::Boolean) @@ -127,10 +131,37 @@ impl Format { /// particular `Config`. /// /// NB there is no check that `self == fs.config.input_format`! - #[instrument(level = "info", skip(reader, config))] - pub fn load(&self, reader: Box, config: Config) -> FS { + #[instrument(level = "info", skip(config))] + pub fn load(&self, config: Config) -> FS { let mut inodes: Vec> = Vec::new(); + let reader: Box = match &config.input { + Input::Stdin => Box::new(std::io::stdin()), + Input::File(file) => { + let fmt = config.input_format; + let file = std::fs::File::open(&file).unwrap_or_else(|e| { + error!("Unable to open {} for {} input: {}", file.display(), fmt, e); + std::process::exit(1); + }); + Box::new(file) + } + Input::Empty => { + // let's just reserve some space for later and get cracking + info!("reserving space in empty filesystem"); + inodes.resize_with(1024, || None); + + // create an empty directory + let contents = HashMap::with_capacity(16); + inodes[1] = Some(Inode::new( + fuser::FUSE_ROOT_ID, + fuser::FUSE_ROOT_ID, + Entry::Directory(DirType::Named, contents), + &config, + )); + return FS::new(inodes, config); + } + }; + match self { Format::Json => { info!("reading json value"); @@ -478,6 +509,19 @@ mod json { fn from_string(typ: Typ, contents: String, _config: &Config) -> Self { match typ { + Typ::Auto => { + if contents.is_empty() { + Value::Null + } else if contents == "true" { + Value::Bool(true) + } else if contents == "false" { + Value::Bool(false) + } else if let Ok(n) = serde_json::Number::from_str(&contents) { + Value::Number(n) + } else { + Value::String(contents) + } + } Typ::Boolean => { if contents == "true" { Value::Bool(true) @@ -613,6 +657,21 @@ mod toml { fn from_string(typ: Typ, contents: String, _config: &Config) -> Self { match typ { + Typ::Auto => { + if contents == "true" { + Value::Boolean(true) + } else if contents == "false" { + Value::Boolean(false) + } else if let Ok(n) = i64::from_str(&contents) { + Value::Integer(n) + } else if let Ok(n) = f64::from_str(&contents) { + Value::Float(n) + } else if let Ok(datetime) = str::parse(&contents) { + Value::Datetime(datetime) + } else { + Value::String(contents) + } + } Typ::Boolean => { if contents == "true" { Value::Boolean(true) @@ -808,6 +867,21 @@ mod yaml { fn from_string(typ: Typ, contents: String, _config: &Config) -> Self { match typ { + Typ::Auto => { + if contents.is_empty() { + Value(Yaml::Null) + } else if contents == "true" { + Value(Yaml::Boolean(true)) + } else if contents == "false" { + Value(Yaml::Boolean(false)) + } else if let Ok(n) = i64::from_str(&contents) { + Value(Yaml::Integer(n)) + } else if let Ok(_n) = f64::from_str(&contents) { + Value(Yaml::Real(contents)) + } else { + Value(Yaml::String(contents)) + } + } Typ::Boolean => { if contents == "true" { Value(Yaml::Boolean(true)) diff --git a/src/fs.rs b/src/fs.rs index 3857d47..4040404 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -950,7 +950,7 @@ impl Filesystem for FS { // create the inode entry let (entry, kind) = if file_type == libc::S_IFREG as u32 { - (Entry::File(Typ::String, Vec::new()), FileType::RegularFile) + (Entry::File(Typ::Auto, Vec::new()), FileType::RegularFile) } else { assert_eq!(file_type, libc::S_IFDIR as u32); ( diff --git a/src/main.rs b/src/main.rs index 831d77f..85fc359 100644 --- a/src/main.rs +++ b/src/main.rs @@ -129,75 +129,85 @@ fn main() { None => config.gid = unsafe { libc::getegid() }, } - config.input = match args.value_of("INPUT") { - Some(input_source) => { - if input_source == "-" { - Input::Stdin - } else { - let input_source = PathBuf::from(input_source); - if !input_source.exists() { - error!("Input file {} does not exist.", input_source.display()); - std::process::exit(1); - } - Input::File(input_source) - } - } - None => Input::Stdin, - }; - config.output = if let Some(output) = args.value_of("OUTPUT") { - Output::File(PathBuf::from(output)) - } else if args.is_present("INPLACE") { - match &config.input { - Input::Stdin => { - warn!( - "In-place output `-i` with STDIN input makes no sense; outputting on STDOUT." - ); - Output::Stdout - } - Input::File(input_source) => Output::File(input_source.clone()), - } - } else if args.is_present("NOOUTPUT") || args.is_present("QUIET") { - Output::Quiet - } else { - Output::Stdout - }; - - // infer and create mountpoint from filename as possible - config.mount = match args.value_of("MOUNT") { - Some(mount_point) => { - let mount_point = PathBuf::from(mount_point); - if !mount_point.exists() { - error!("Mount point {} does not exist.", mount_point.display()); + match args.value_of("NEW") { + Some(target_file) => { + if args.occurrences_of("INPUT") != 0 { + error!("It doesn't make sense to set `--new` with a specified input file."); std::process::exit(1); } - config.cleanup_mount = false; - Some(mount_point) - } - None => { - match &config.input { - Input::Stdin => { - error!("You must specify a mount point when reading from stdin."); - std::process::exit(1); + let output = PathBuf::from(target_file); + + if output.exists() { + error!("Output file {} already exists.", output.display()); + std::process::exit(1); + } + + let format = match args + .value_of("TARGET_FORMAT") + .ok_or(format::ParseFormatError::NoFormatProvided) + .and_then(|s| s.parse::()) + { + Ok(target_format) => target_format, + Err(e) => { + match e { + format::ParseFormatError::NoSuchFormat(s) => { + warn!( + "Unrecognized format '{}', inferring from {}.", + s, + output.display(), + ) + } + format::ParseFormatError::NoFormatProvided => { + debug!("Inferring output format from input.") + } + }; + + match output + .extension() + .and_then(|s| s.to_str()) + .ok_or(format::ParseFormatError::NoFormatProvided) + .and_then(|s| s.parse::()) + { + Ok(format) => format, + Err(_) => { + error!( + "Unrecognized format '{}'; use --target or a known extension to specify a format.", + output.display() + ); + std::process::exit(1); + } + } } - Input::File(file) => { - // If the input is from a file foo.EXT, then try to make a directory foo. - let mount_dir = file.with_extension(""); + }; + + let mount = match args.value_of("MOUNT") { + Some(mount_point) => { + let mount_point = PathBuf::from(mount_point); + if !mount_point.exists() { + error!("Mount point {} does not exist.", mount_point.display()); + std::process::exit(1); + } + config.cleanup_mount = false; + Some(mount_point) + } + None => { + // If the output is to a file foo.EXT, then try to make a directory foo. + let mount_dir = output.with_extension(""); // If that file already exists, give up and tell the user about --mount. if mount_dir.exists() { - error!("Inferred mountpoint '{mount}' for input file '{file}', but '{mount}' already exists. Use `--mount MOUNT` to specify a mountpoint.", - mount = mount_dir.display(), file = file.display()); + error!("Inferred mountpoint '{mount}' for output file '{file}', but '{mount}' already exists. Use `--mount MOUNT` to specify a mountpoint.", + mount = mount_dir.display(), file = output.display()); std::process::exit(1); } // If the mountpoint can't be created, give up and tell the user about --mount. if let Err(e) = std::fs::create_dir(&mount_dir) { - error!( - "Couldn't create mountpoint '{}': {}. Use `--mount MOUNT` to specify a mountpoint.", - mount_dir.display(), - e - ); + error!("Couldn't create mountpoint '{}': {}. Use `--mount MOUNT` to specify a mountpoint.", + mount_dir.display(), + e + ); std::process::exit(1); } @@ -205,98 +215,192 @@ fn main() { config.cleanup_mount = true; Some(mount_dir) } - } - } - }; - assert!(config.mount.is_some()); - - // try to autodetect the input format. - // - // first see if it's specified and parses okay. - // - // then see if we can pull it out of the extension. - // - // then give up and use json - config.input_format = match args - .value_of("SOURCE_FORMAT") - .ok_or(format::ParseFormatError::NoFormatProvided) - .and_then(|s| s.parse::()) - { - Ok(source_format) => source_format, - Err(e) => { - match e { - format::ParseFormatError::NoSuchFormat(s) => { - warn!("Unrecognized format '{}', inferring from input.", s) - } - format::ParseFormatError::NoFormatProvided => { - debug!("Inferring format from input.") - } }; - match &config.input { - Input::Stdin => Format::Json, - Input::File(input_source) => match input_source - .extension() - .and_then(|s| s.to_str()) - .ok_or(format::ParseFormatError::NoFormatProvided) - .and_then(|s| s.parse::()) - { - Ok(format) => format, - Err(_) => { - warn!( - "Unrecognized format {}, defaulting to JSON.", - input_source.display() - ); - Format::Json + config.input = Input::Empty; + config.output = Output::File(output); + config.input_format = format; + config.output_format = format; + config.mount = mount; + } + None => { + // configure input + config.input = match args.value_of("INPUT") { + Some(input_source) => { + if input_source == "-" { + Input::Stdin + } else { + let input_source = PathBuf::from(input_source); + if !input_source.exists() { + error!("Input file {} does not exist.", input_source.display()); + std::process::exit(1); + } + Input::File(input_source) } - }, - } - } - }; - - // try to autodetect the output format. - // - // first see if it's specified and parses okay. - // - // then see if we can pull it out of the extension (if specified) - // - // then give up and use the input format - config.output_format = match args - .value_of("TARGET_FORMAT") - .ok_or(format::ParseFormatError::NoFormatProvided) - .and_then(|s| s.parse::()) - { - Ok(target_format) => target_format, - Err(e) => { - match e { - format::ParseFormatError::NoSuchFormat(s) => { - warn!( - "Unrecognized format '{}', inferring from input and output.", - s - ) - } - format::ParseFormatError::NoFormatProvided => { - debug!("Inferring output format from input.") } + None => Input::Stdin, }; - match args - .value_of("OUTPUT") - .and_then(|s| Path::new(s).extension()) - .and_then(|s| s.to_str()) + // configure output + config.output = if let Some(output) = args.value_of("OUTPUT") { + Output::File(PathBuf::from(output)) + } else if args.is_present("INPLACE") { + match &config.input { + Input::Stdin => { + warn!( + "In-place output `-i` with STDIN input makes no sense; outputting on STDOUT." + ); + Output::Stdout + } + Input::Empty => { + warn!( + "In-place output `-i` with empty input makes no sense; outputting on STDOUT." + ); + Output::Stdout + } + Input::File(input_source) => Output::File(input_source.clone()), + } + } else if args.is_present("NOOUTPUT") || args.is_present("QUIET") { + Output::Quiet + } else { + Output::Stdout + }; + + // infer and create mountpoint from filename as possible + config.mount = match args.value_of("MOUNT") { + Some(mount_point) => { + let mount_point = PathBuf::from(mount_point); + if !mount_point.exists() { + error!("Mount point {} does not exist.", mount_point.display()); + std::process::exit(1); + } + config.cleanup_mount = false; + Some(mount_point) + } + None => { + match &config.input { + Input::Stdin => { + error!("You must specify a mount point when reading from stdin."); + std::process::exit(1); + } + Input::Empty => { + error!("You must specify a mount point when reading an empty file."); + std::process::exit(1); + } + Input::File(file) => { + // If the input is from a file foo.EXT, then try to make a directory foo. + let mount_dir = file.with_extension(""); + // If that file already exists, give up and tell the user about --mount. + if mount_dir.exists() { + error!("Inferred mountpoint '{mount}' for input file '{file}', but '{mount}' already exists. Use `--mount MOUNT` to specify a mountpoint.", + mount = mount_dir.display(), file = file.display()); + std::process::exit(1); + } + // If the mountpoint can't be created, give up and tell the user about --mount. + if let Err(e) = std::fs::create_dir(&mount_dir) { + error!( + "Couldn't create mountpoint '{}': {}. Use `--mount MOUNT` to specify a mountpoint.", + mount_dir.display(), + e + ); + std::process::exit(1); + } + // We did it! + config.cleanup_mount = true; + Some(mount_dir) + } + } + } + }; + assert!(config.mount.is_some()); + + // try to autodetect the input format. + // + // first see if it's specified and parses okay. + // + // then see if we can pull it out of the extension. + // + // then give up and use json + config.input_format = match args + .value_of("SOURCE_FORMAT") + .ok_or(format::ParseFormatError::NoFormatProvided) + .and_then(|s| s.parse::()) { - Some(s) => match s.parse::() { - Ok(format) => format, - Err(_) => { - warn!( - "Unrecognized format {}, defaulting to input format '{}'.", - s, config.input_format - ); - config.input_format + Ok(source_format) => source_format, + Err(e) => { + match e { + format::ParseFormatError::NoSuchFormat(s) => { + warn!("Unrecognized format '{}', inferring from input.", s) + } + format::ParseFormatError::NoFormatProvided => { + debug!("Inferring format from input.") + } + }; + match &config.input { + Input::Stdin => Format::Json, + Input::Empty => Format::Json, + Input::File(input_source) => match input_source + .extension() + .and_then(|s| s.to_str()) + .ok_or(format::ParseFormatError::NoFormatProvided) + .and_then(|s| s.parse::()) + { + Ok(format) => format, + Err(_) => { + warn!( + "Unrecognized format {}, defaulting to JSON.", + input_source.display() + ); + Format::Json + } + }, } - }, - None => config.input_format, - } + } + }; + // try to autodetect the output format. + // + // first see if it's specified and parses okay. + // + // then see if we can pull it out of the extension (if specified) + // + // then give up and use the input format + config.output_format = match args + .value_of("TARGET_FORMAT") + .ok_or(format::ParseFormatError::NoFormatProvided) + .and_then(|s| s.parse::()) + { + Ok(target_format) => target_format, + Err(e) => { + match e { + format::ParseFormatError::NoSuchFormat(s) => { + warn!( + "Unrecognized format '{}', inferring from input and output.", + s + ) + } + format::ParseFormatError::NoFormatProvided => { + debug!("Inferring output format from input.") + } + }; + match args + .value_of("OUTPUT") + .and_then(|s| Path::new(s).extension()) + .and_then(|s| s.to_str()) + { + Some(s) => match s.parse::() { + Ok(format) => format, + Err(_) => { + warn!( + "Unrecognized format {}, defaulting to input format '{}'.", + s, config.input_format + ); + config.input_format + } + }, + None => config.input_format, + } + } + }; } }; @@ -331,18 +435,7 @@ fn main() { }; let cleanup_mount = config.cleanup_mount; let input_format = config.input_format; - let reader: Box = match &config.input { - Input::Stdin => Box::new(std::io::stdin()), - Input::File(file) => { - let fmt = config.input_format; - let file = std::fs::File::open(&file).unwrap_or_else(|e| { - error!("Unable to open {} for {} input: {}", file.display(), fmt, e); - std::process::exit(1); - }); - Box::new(file) - } - }; - let fs = input_format.load(reader, config); + let fs = input_format.load(config); info!("mounting on {:?} with options {:?}", mount, options); fuser::mount2(fs, &mount, &options).unwrap(); diff --git a/tests/auto.sh b/tests/auto.sh new file mode 100755 index 0000000..2c0ec08 --- /dev/null +++ b/tests/auto.sh @@ -0,0 +1,47 @@ +#!/bin/sh + +fail() { + echo FAILED: $1 + if [ "$MNT" ] + then + umount "$MNT" + rm "$FILE" "$EXP" + rmdir "$MNT" + fi + exit 1 +} + +MNT=$(mktemp -d) +FILE=$(mktemp).json + +echo '{}' >"$FILE" + +EXP=$(mktemp) + +printf '{"favorite_number":47,"likes":{"cats":false,"dogs":true},"mistakes":null,"name":"Michael Greenberg","website":"https://mgree.github.io"}' >"$EXP" + +ffs -m "$MNT" -i "$FILE" & +PID=$! +sleep 2 + +ls "$MNT" +[ $(ls $MNT) ] && fail nonempty1 +[ $(ls $MNT | wc -l) -eq 0 ] || fail nonempty2 + +echo 47 >"$MNT"/favorite_number +mkdir "$MNT"/likes +echo true >"$MNT"/likes/dogs +echo false >"$MNT"/likes/cats +touch "$MNT"/mistakes +echo Michael Greenberg >"$MNT"/name +echo https://mgree.github.io >"$MNT"/website + +umount "$MNT" || fail unmount +sleep 1 +kill -0 $PID >/dev/null 2>&1 && fail process + +cat "$FILE" +diff "$FILE" "$EXP" || fail diff + +rm "$FILE" "$EXP" +rmdir "$MNT" diff --git a/tests/new.sh b/tests/new.sh new file mode 100755 index 0000000..1cd3390 --- /dev/null +++ b/tests/new.sh @@ -0,0 +1,43 @@ +#!/bin/sh + +fail() { + echo FAILED: $1 + if [ "$MNT" ] + then + umount "$MNT" + rmdir "$MNT" + rm "$OUT" "$EXP" + fi + exit 1 +} + +# really, just for the name +OUT=$(mktemp) +rm "$OUT" +MNT="$OUT" +OUT="$OUT".json + +EXP=$(mktemp) + +printf '{"handles":{"github":"mgree","stevens":"mgreenbe","twitter":"mgrnbrg"},"problems":99}' >"$EXP" + +ffs --new "$OUT" & +PID=$! +sleep 2 +[ "$(ls $MNT)" ] && fail nonempty + +mkdir "$MNT"/handles + +echo mgree >"$MNT"/handles/github +echo mgreenbe >"$MNT"/handles/stevens +echo mgrnbrg >"$MNT"/handles/twitter +echo 99 >"$MNT"/problems + +umount "$MNT" || fail unmount +sleep 1 +kill -0 $PID >/dev/null 2>&1 && fail process + +diff "$OUT" "$EXP" || fail diff + +[ -e "$MNT" ] && fail mount +rm "$OUT" "$EXP"