use std::fs;
use std::path::Path;
use config_disassembler::run;
fn argv(items: &[&str]) -> Vec<String> {
items.iter().map(|s| (*s).to_string()).collect()
}
async fn run_ok(args: &[&str]) {
run(argv(args))
.await
.unwrap_or_else(|e| panic!("expected Ok, got Err({e}) for args {args:?}"));
}
async fn run_err(args: &[&str]) -> String {
match run(argv(args)).await {
Ok(()) => panic!("expected Err for args {args:?}"),
Err(e) => e.to_string(),
}
}
#[tokio::test]
async fn no_args_prints_help() {
run_ok(&["config-disassembler"]).await;
}
#[tokio::test]
async fn help_aliases_all_succeed() {
for flag in ["help", "--help", "-h"] {
run_ok(&["config-disassembler", flag]).await;
}
}
#[tokio::test]
async fn unknown_subcommand_is_usage_error() {
let msg = run_err(&["config-disassembler", "wat"]).await;
assert!(msg.contains("unknown subcommand"), "got: {msg}");
}
#[tokio::test]
async fn json_without_action_is_usage_error() {
let msg = run_err(&["config-disassembler", "json"]).await;
assert!(msg.contains("disassemble"), "got: {msg}");
}
#[tokio::test]
async fn unknown_action_is_usage_error() {
let msg = run_err(&["config-disassembler", "json", "wat"]).await;
assert!(msg.contains("unknown action"), "got: {msg}");
}
#[tokio::test]
async fn missing_input_is_usage_error() {
let msg = run_err(&["config-disassembler", "json", "disassemble"]).await;
assert!(msg.contains("missing <input>"), "got: {msg}");
}
#[tokio::test]
async fn missing_input_dir_is_usage_error() {
let msg = run_err(&["config-disassembler", "json", "reassemble"]).await;
assert!(msg.contains("missing <input-dir>"), "got: {msg}");
}
#[tokio::test]
async fn unknown_disassemble_option_is_usage_error() {
let msg = run_err(&[
"config-disassembler",
"json",
"disassemble",
"--bogus",
"x.json",
])
.await;
assert!(msg.contains("unknown option"), "got: {msg}");
}
#[tokio::test]
async fn unknown_reassemble_option_is_usage_error() {
let msg = run_err(&[
"config-disassembler",
"yaml",
"reassemble",
"--bogus",
"dir",
])
.await;
assert!(msg.contains("unknown option"), "got: {msg}");
}
#[tokio::test]
async fn flag_without_value_is_usage_error() {
let msg = run_err(&[
"config-disassembler",
"json",
"disassemble",
"--output-format",
])
.await;
assert!(msg.contains("expects a value"), "got: {msg}");
}
#[tokio::test]
async fn unknown_format_value_is_usage_error() {
let msg = run_err(&[
"config-disassembler",
"json",
"disassemble",
"--output-format",
"xml",
"x.json",
])
.await;
assert!(msg.contains("unknown format"), "got: {msg}");
}
#[tokio::test]
async fn extra_positional_is_usage_error() {
let msg = run_err(&[
"config-disassembler",
"json",
"disassemble",
"first.json",
"second.json",
])
.await;
assert!(msg.contains("unexpected"), "got: {msg}");
}
#[tokio::test]
async fn yaml_subcommand_help() {
run_ok(&["config-disassembler", "yaml", "--help"]).await;
}
#[tokio::test]
async fn yml_alias_dispatches_to_yaml() {
run_ok(&["config-disassembler", "yml", "help"]).await;
}
#[tokio::test]
async fn json5_subcommand_action_help() {
run_ok(&["config-disassembler", "json5", "disassemble", "--help"]).await;
run_ok(&["config-disassembler", "json5", "reassemble", "--help"]).await;
}
#[tokio::test]
async fn jsonc_subcommand_action_help() {
run_ok(&["config-disassembler", "jsonc", "disassemble", "--help"]).await;
run_ok(&["config-disassembler", "jsonc", "reassemble", "--help"]).await;
}
#[tokio::test]
async fn toon_subcommand_action_help() {
run_ok(&["config-disassembler", "toon", "disassemble", "--help"]).await;
run_ok(&["config-disassembler", "toon", "reassemble", "--help"]).await;
}
#[tokio::test]
async fn full_disassemble_reassemble_via_cli() {
let tmp = tempfile::tempdir().unwrap();
let input = tmp.path().join("config.json");
fs::write(
&input,
r#"{"name":"demo","version":1,"settings":{"x":1,"y":2}}"#,
)
.unwrap();
let split_dir = tmp.path().join("split");
let rebuilt = tmp.path().join("rebuilt.yaml");
run_ok(&[
"config-disassembler",
"json",
"disassemble",
input.to_str().unwrap(),
"--output-dir",
split_dir.to_str().unwrap(),
"--output-format",
"yaml",
])
.await;
assert!(split_dir.join("settings.yaml").exists());
run_ok(&[
"config-disassembler",
"json",
"reassemble",
split_dir.to_str().unwrap(),
"-o",
rebuilt.to_str().unwrap(),
"--output-format",
"yaml",
])
.await;
let parsed: serde_json::Value =
serde_yaml::from_str(&fs::read_to_string(&rebuilt).unwrap()).unwrap();
let expected: serde_json::Value =
serde_json::from_str(&fs::read_to_string(&input).unwrap()).unwrap();
assert_eq!(parsed, expected);
}
#[tokio::test]
async fn full_array_unique_id_via_cli_with_short_o() {
let tmp = tempfile::tempdir().unwrap();
let input = tmp.path().join("items.yaml");
fs::write(&input, "- name: alpha\n v: 1\n- name: beta\n v: 2\n").unwrap();
let split_dir = tmp.path().join("split");
run_ok(&[
"config-disassembler",
"yaml",
"disassemble",
input.to_str().unwrap(),
"-o",
split_dir.to_str().unwrap(),
"--unique-id",
"name",
"--input-format",
"yaml",
"--output-format",
"json",
])
.await;
assert!(split_dir.join("alpha.json").exists());
assert!(split_dir.join("beta.json").exists());
}
#[tokio::test]
async fn pre_purge_and_post_purge_via_cli() {
let tmp = tempfile::tempdir().unwrap();
let input = tmp.path().join("c.json");
fs::write(&input, r#"{"k":{"v":1}}"#).unwrap();
let split_dir = tmp.path().join("split");
fs::create_dir_all(&split_dir).unwrap();
fs::write(split_dir.join("stale.txt"), "old").unwrap();
run_ok(&[
"config-disassembler",
"json",
"disassemble",
input.to_str().unwrap(),
"--output-dir",
split_dir.to_str().unwrap(),
"--pre-purge",
"--post-purge",
])
.await;
assert!(!split_dir.join("stale.txt").exists());
assert!(!input.exists(), "post-purge should remove the input file");
let rebuilt = tmp.path().join("rebuilt.json");
run_ok(&[
"config-disassembler",
"json",
"reassemble",
split_dir.to_str().unwrap(),
"--output",
rebuilt.to_str().unwrap(),
"--post-purge",
])
.await;
assert!(rebuilt.exists());
assert!(
!split_dir.exists(),
"post-purge should remove the disassembled directory"
);
}
#[tokio::test]
async fn toml_subcommand_help() {
run_ok(&["config-disassembler", "toml", "--help"]).await;
run_ok(&["config-disassembler", "toml", "help"]).await;
run_ok(&["config-disassembler", "toml", "disassemble", "--help"]).await;
run_ok(&["config-disassembler", "toml", "reassemble", "--help"]).await;
}
#[tokio::test]
async fn toml_subcommand_round_trip_via_cli() {
let tmp = tempfile::tempdir().unwrap();
let input = tmp.path().join("config.toml");
fs::write(
&input,
"name = \"demo\"\nversion = 1\nenabled = true\n\n[settings]\nx = 1\ny = 2\n",
)
.unwrap();
let split_dir = tmp.path().join("split");
let rebuilt = tmp.path().join("rebuilt.toml");
run_ok(&[
"config-disassembler",
"toml",
"disassemble",
input.to_str().unwrap(),
"--output-dir",
split_dir.to_str().unwrap(),
])
.await;
assert!(split_dir.join("settings.toml").exists());
assert!(split_dir.join("_main.toml").exists());
run_ok(&[
"config-disassembler",
"toml",
"reassemble",
split_dir.to_str().unwrap(),
"-o",
rebuilt.to_str().unwrap(),
])
.await;
let rebuilt_value: serde_json::Value =
toml::from_str(&fs::read_to_string(&rebuilt).unwrap()).unwrap();
let original_value: serde_json::Value =
toml::from_str(&fs::read_to_string(&input).unwrap()).unwrap();
assert_eq!(rebuilt_value, original_value);
}
#[tokio::test]
async fn toml_rejects_input_format_flag() {
let msg = run_err(&[
"config-disassembler",
"toml",
"disassemble",
"--input-format",
"json",
"x.toml",
])
.await;
assert!(
msg.contains("--input-format is not supported"),
"got: {msg}"
);
}
#[tokio::test]
async fn toml_rejects_output_format_flag_on_disassemble() {
let msg = run_err(&[
"config-disassembler",
"toml",
"disassemble",
"--output-format",
"json",
"x.toml",
])
.await;
assert!(
msg.contains("--output-format is not supported"),
"got: {msg}"
);
}
#[tokio::test]
async fn toml_rejects_output_format_flag_on_reassemble() {
let msg = run_err(&[
"config-disassembler",
"toml",
"reassemble",
"--output-format",
"yaml",
"dir",
])
.await;
assert!(
msg.contains("--output-format is not supported"),
"got: {msg}"
);
}
#[tokio::test]
async fn json_to_toml_cross_format_is_rejected() {
let tmp = tempfile::tempdir().unwrap();
let input = tmp.path().join("c.json");
fs::write(&input, r#"{"a":{"b":1}}"#).unwrap();
let msg = run_err(&[
"config-disassembler",
"json",
"disassemble",
input.to_str().unwrap(),
"--output-dir",
tmp.path().join("out").to_str().unwrap(),
"--output-format",
"toml",
])
.await;
assert!(msg.contains("TOML can only be converted"), "got: {msg}");
}
#[tokio::test]
async fn xml_subcommand_passes_through_to_inner_cli() {
run_ok(&["config-disassembler", "xml"]).await;
run_ok(&["config-disassembler", "xml", "this-is-not-a-real-action"]).await;
let tmp = tempfile::tempdir().unwrap();
let missing = tmp.path().join("does-not-exist.xml");
let result = run(argv(&[
"config-disassembler",
"xml",
"disassemble",
missing.to_str().unwrap(),
]))
.await;
if let Err(e) = result {
assert!(e.to_string().contains("xml-disassembler"));
}
}
#[tokio::test]
async fn disassemble_scalar_root_is_invalid() {
let tmp = tempfile::tempdir().unwrap();
let input = tmp.path().join("scalar.json");
fs::write(&input, "42").unwrap();
let msg = run_err(&[
"config-disassembler",
"json",
"disassemble",
input.to_str().unwrap(),
"--output-dir",
tmp.path().join("out").to_str().unwrap(),
])
.await;
assert!(msg.contains("object or array"), "got: {msg}");
}
#[tokio::test]
async fn disassemble_unknown_extension_is_format_error() {
let tmp = tempfile::tempdir().unwrap();
let input = tmp.path().join("noext");
fs::write(&input, "{}").unwrap();
let msg = run(argv(&[
"config-disassembler",
"json",
"disassemble",
input.to_str().unwrap(),
"--output-dir",
tmp.path().join("out").to_str().unwrap(),
]))
.await
.err()
.map(|e| e.to_string())
.unwrap_or_default();
let _ = msg;
}
#[tokio::test]
async fn reassemble_missing_metadata_is_invalid() {
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path().join("empty");
fs::create_dir_all(&dir).unwrap();
let msg = run_err(&[
"config-disassembler",
"json",
"reassemble",
dir.to_str().unwrap(),
])
.await;
assert!(msg.contains("metadata"), "got: {msg}");
}
#[tokio::test]
async fn reassemble_input_must_be_directory() {
let tmp = tempfile::tempdir().unwrap();
let file = tmp.path().join("not-a-dir.json");
fs::write(&file, "{}").unwrap();
let msg = run_err(&[
"config-disassembler",
"json",
"reassemble",
file.to_str().unwrap(),
])
.await;
assert!(msg.contains("not a directory"), "got: {msg}");
}
#[test]
fn fixtures_dir_exists_for_other_tests() {
let manifest = Path::new(env!("CARGO_MANIFEST_DIR"));
assert!(manifest.join("fixtures").is_dir());
}
#[tokio::test]
async fn json_disassemble_directory_input_walks_and_ignores() {
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path();
fs::write(
dir.join("keep.json"),
r#"{"settings": {"a": 1}, "name": "x"}"#,
)
.unwrap();
fs::write(
dir.join("skip.json"),
r#"{"settings": {"b": 2}, "name": "y"}"#,
)
.unwrap();
fs::write(dir.join("README.txt"), "not a config").unwrap();
let ignore = dir.join(".myignore");
fs::write(&ignore, "skip.json\n").unwrap();
run_ok(&[
"config-disassembler",
"json",
"disassemble",
dir.to_str().unwrap(),
"--ignore-path",
ignore.to_str().unwrap(),
])
.await;
assert!(
dir.join("keep").is_dir(),
"keep/ should be created from keep.json"
);
assert!(dir.join("keep/settings.json").exists());
assert!(
!dir.join("skip").exists(),
"skip.json was ignored, so skip/ should not exist"
);
assert!(dir.join("README.txt").exists());
}
#[tokio::test]
async fn yaml_disassemble_directory_uses_cdignore_default() {
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path();
fs::write(dir.join("a.yaml"), "x:\n y: 1\n").unwrap();
fs::write(dir.join("b.yaml"), "x:\n y: 2\n").unwrap();
fs::write(dir.join(".cdignore"), "b.yaml\n").unwrap();
run_ok(&[
"config-disassembler",
"yaml",
"disassemble",
dir.to_str().unwrap(),
])
.await;
assert!(dir.join("a").is_dir());
assert!(!dir.join("b").exists(), "b.yaml ignored via .cdignore");
}
#[tokio::test]
async fn directory_input_rejects_output_dir_flag() {
let tmp = tempfile::tempdir().unwrap();
fs::write(tmp.path().join("a.json"), "{}").unwrap();
let msg = run_err(&[
"config-disassembler",
"json",
"disassemble",
tmp.path().to_str().unwrap(),
"--output-dir",
tmp.path().join("out").to_str().unwrap(),
])
.await;
assert!(msg.contains("--output-dir"), "got: {msg}");
}
#[tokio::test]
async fn directory_input_format_filter_skips_other_formats() {
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path();
fs::write(dir.join("a.json"), r#"{"k": {"v": 1}}"#).unwrap();
fs::write(dir.join("b.yaml"), "k:\n v: 1\n").unwrap();
run_ok(&[
"config-disassembler",
"json",
"disassemble",
dir.to_str().unwrap(),
"--input-format",
"json",
])
.await;
assert!(dir.join("a").is_dir(), "a.json was disassembled");
assert!(
!dir.join("b").exists(),
"b.yaml is not json so it was skipped"
);
}
#[tokio::test]
async fn format_help_mentions_ignore_path() {
for fmt in ["json", "json5", "jsonc", "yaml", "toon", "toml"] {
run_ok(&["config-disassembler", fmt, "--help"]).await;
}
}