Skip to content

Commit

Permalink
add some unit tests and a workflow to run tests in pull requests.
Browse files Browse the repository at this point in the history
Signed-off-by: bin liu <[email protected]>
  • Loading branch information
liubin committed Dec 2, 2022
1 parent b29fe5f commit 679a961
Show file tree
Hide file tree
Showing 6 changed files with 274 additions and 14 deletions.
22 changes: 22 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: CI
on:
pull_request:
types:
- opened
- edited
- reopened
- synchronize

env:
CARGO_TERM_COLOR: always
RUST_TARGET: x86_64-unknown-linux-musl

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Build and test
uses: gmiam/rust-musl-action@master
with:
args: make build && make ut
51 changes: 51 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,6 @@ serde_json = "1.0"
structopt = "0.3"
toml_edit = "0.15.0"
chrono = "0.4"

[dev-dependencies]
tempfile = "3.3.0"
24 changes: 24 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
default: build

CARGO ?= $(shell which cargo)
RUST_TARGET ?= x86_64-unknown-linux-musl

.format:
${CARGO} fmt -- --check

build: .format
${CARGO} build --target ${RUST_TARGET} --release
# Cargo will skip checking if it is already checked
${CARGO} clippy --bins --tests -- -Dwarnings

clean:
${CARGO} clean

ut:
RUST_BACKTRACE=1 ${CARGO} test --workspace -- --skip integration --nocapture

integration:
# run tests under `test` directory
RUST_BACKTRACE=1 ${CARGO} test --workspace -- integration --nocapture

test: ut integration
186 changes: 173 additions & 13 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,14 @@ enum Args {
// TODO: append/add (name TBD)
}

#[derive(StructOpt)]
#[derive(Clone, Copy, Default, StructOpt)]
struct GetOpts {
/// Print as a TOML fragment (default: print as JSON)
#[structopt(long)]
output_toml: bool,
}

#[derive(StructOpt)]
#[derive(Clone, Copy, Default, StructOpt)]
struct SetOpts {
/// Overwrite the TOML file (default: print to stdout)
#[structopt(long)]
Expand Down Expand Up @@ -137,20 +137,31 @@ fn check(path: PathBuf, query: &str) {
}

fn get(path: PathBuf, query: &str, opts: GetOpts) -> Result<(), Error> {
let value = get_value(path, query, opts)?;
if opts.output_toml {
print!("{}", value);
} else {
println!("{}", value);
}
Ok(())
}

fn get_value(path: PathBuf, query: &str, opts: GetOpts) -> Result<String, Error> {
let tpath = parse_query_cli(query)?.0;
let doc = read_parse(path)?;

if opts.output_toml {
print_toml_fragment(&doc, &tpath);
let value = if opts.output_toml {
format_toml_fragment(&doc, &tpath)
} else {
let item = walk_tpath(doc.as_item(), &tpath);
// TODO: support shell-friendly output like `jq -r`
println!("{}", serde_json::to_string(&JsonItem(item))?);
}
Ok(())
serde_json::to_string(&JsonItem(item))?
};

Ok(value)
}

fn print_toml_fragment(doc: &Document, tpath: &[TpathSegment]) {
fn format_toml_fragment(doc: &Document, tpath: &[TpathSegment]) -> String {
let mut item = doc.as_item();
let mut breadcrumbs = vec![];
for seg in tpath {
Expand Down Expand Up @@ -193,10 +204,23 @@ fn print_toml_fragment(doc: &Document, tpath: &[TpathSegment]) {

let root = item.into_table().unwrap();
let doc: Document = root.into();
print!("{}", doc);
format!("{}", doc)
}

fn set(path: PathBuf, query: &str, value_str: &str, opts: SetOpts) -> Result<(), Error> {
let result = set_value(path, query, value_str, opts)?;
if let Some(doc) = result {
print!("{}", doc);
}
Ok(())
}

fn set_value(
path: PathBuf,
query: &str,
value_str: &str,
opts: SetOpts,
) -> Result<Option<String>, Error> {
let tpath = parse_query_cli(query)?.0;
let mut doc = read_parse(path.clone())?;

Expand Down Expand Up @@ -241,7 +265,7 @@ fn set(path: PathBuf, query: &str, value_str: &str, opts: SetOpts) -> Result<(),
}
*item = detect_value(value_str);

if opts.overwrite {
let result = if opts.overwrite {
// write content to path
if opts.backup {
let now: DateTime<Utc> = Utc::now();
Expand All @@ -251,10 +275,12 @@ fn set(path: PathBuf, query: &str, value_str: &str, opts: SetOpts) -> Result<(),
}
let mut output = OpenOptions::new().write(true).truncate(true).open(path)?;
write!(output, "{}", doc)?;
None
} else {
print!("{}", doc);
}
Ok(())
Some(format!("{}", doc))
};

Ok(result)
}

fn detect_value(value_str: &str) -> Item {
Expand Down Expand Up @@ -355,3 +381,137 @@ impl Serialize for JsonValue<'_> {
}
}
}

#[cfg(test)]
mod tests {
use std::fs;

// functions to test
use super::check_exists;
use super::detect_value;
use super::{get_value, GetOpts};
use super::{set_value, SetOpts};

#[test]
fn test_detect_value() {
let i = detect_value("abc");
assert_eq!("string", i.type_name());
assert!(i.is_str());
assert_eq!(Some("abc"), i.as_str());

let i = detect_value("123");
assert_eq!("integer", i.type_name());
assert!(i.is_integer());
assert_eq!(Some(123), i.as_integer());

let i = detect_value("true");
assert_eq!("boolean", i.type_name());
assert!(i.is_bool());
assert_eq!(Some(true), i.as_bool());
}

#[test]
fn test_check_exists() {
let body = r#"[a]
b = "c"
[x]
y = "z""#;
let dir = tempfile::tempdir().expect("failed to create tempdir");
let toml_file = dir.path().join("test.toml");
fs::write(&toml_file, body).expect("failed to create tempfile");

// x.y exists
let result = check_exists(toml_file.clone(), "x.y");
assert!(result.is_ok());
assert!(result.unwrap());

// x.z does not exists
let result = check_exists(toml_file, "x.z");
assert!(result.is_ok());
assert!(!result.unwrap());
}

#[test]
fn test_get_value() {
let body = r#"[a]
b = "c"
[x]
y = "z""#;
let dir = tempfile::tempdir().expect("failed to create tempdir");
let toml_file = dir.path().join("test.toml");
fs::write(&toml_file, body).expect("failed to write tempfile");

let opts = GetOpts::default();
// x.y exists
let result = get_value(toml_file.clone(), "x.y", opts);
assert!(result.is_ok());
assert_eq!("\"z\"", result.unwrap());

// x.z does not exists
// FIXME: get_value now will panic, it's not a well-desined API.
let result = std::panic::catch_unwind(|| {
let _ = get_value(toml_file.clone(), "x.z", opts);
});
assert!(result.is_err());
}

#[test]
fn test_set_value() {
// fn set(path: PathBuf, query: &str, value_str: &str, opts: SetOpts) -> Result<(), Error> {
let body = r#"[a]
b = "c"
[x]
y = "z""#;
let dir = tempfile::tempdir().expect("failed to create tempdir");
let toml_file = dir.path().join("test.toml");
fs::write(&toml_file, body).expect("failed to write tempfile");

let mut opts = SetOpts::default();
// x.y exists
let result = set_value(toml_file.clone(), "x.y", "new", opts);
assert!(result.is_ok());
let excepted = r#"[a]
b = "c"
[x]
y = "new"
"#;
assert_eq!(excepted, result.unwrap().unwrap());

let result = set_value(toml_file.clone(), "x.z", "123", opts);
assert!(result.is_ok());
let excepted = r#"[a]
b = "c"
[x]
y = "z"
z = 123
"#;
assert_eq!(excepted, result.unwrap().unwrap());

let result = set_value(toml_file.clone(), "x.z", "false", opts);
assert!(result.is_ok());
let excepted = r#"[a]
b = "c"
[x]
y = "z"
z = false
"#;
assert_eq!(excepted, result.unwrap().unwrap());

// test overwrite the original file
opts.overwrite = true;
let result = set_value(toml_file.clone(), "x.z", "false", opts);
assert!(result.is_ok());
println!("{:?}", result);
// --overwrite will not generate any output.
assert_eq!(None, result.unwrap());

let excepted = r#"[a]
b = "c"
[x]
y = "z"
z = false
"#;
let new_body = fs::read_to_string(toml_file).expect("failed to read TOML file");
assert_eq!(excepted, new_body);
}
}
2 changes: 1 addition & 1 deletion test/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use std::process;
use std::str;

#[test]
fn help_if_no_args() {
fn integration_test_help_if_no_args() {
// Probably want to factor out much of this when adding more tests.
let proc = process::Command::new(get_exec_path()).output().unwrap();
assert!(!proc.status.success());
Expand Down

0 comments on commit 679a961

Please sign in to comment.