use super::*;
const CRATE_PATH_ARG: &str = "--crate-path";
const DEV_FLAG: &str = "--dev";
const DOUBLE_DASH: &str = "--";
const OUT_DIR_ARG: &str = "--out-dir";
const OUT_NAME_ARG: &str = "--out-name";
const PORT_ARG: &str = "--port";
const PROFILING_FLAG: &str = "--profiling";
const RELEASE_FLAG: &str = "--release";
const WWW_DIR_ARG: &str = "--www-dir";
fn args_from(extra: &[&str]) -> ModeArgs {
let mut argv: Vec<&str> = vec!["euv"];
argv.extend_from_slice(extra);
ModeArgs::parse_from(argv)
}
fn owned(items: &[&str]) -> Vec<String> {
items.iter().map(|s: &&str| s.to_string()).collect()
}
#[test]
fn a_build_mode_flag_is_detected_by_any_of_its_three_spellings() {
assert!(has_build_mode_flag(&owned(&[DEV_FLAG])));
assert!(has_build_mode_flag(&owned(&[RELEASE_FLAG])));
assert!(has_build_mode_flag(&owned(&[PROFILING_FLAG])));
}
#[test]
fn a_list_without_a_build_mode_flag_reports_none() {
assert!(!has_build_mode_flag(&owned(&[])));
assert!(!has_build_mode_flag(&owned(&[
"--out-dir",
"pkg",
"--target",
"web"
])));
assert!(
!has_build_mode_flag(&owned(&["--developer"])),
"a flag that merely starts with --dev is not --dev"
);
}
#[test]
fn filtering_drops_each_euv_flag_together_with_the_token_after_it() {
let filtered: Vec<String> = filter_euv_args(&owned(&[
"--target",
"web",
PORT_ARG,
"8080",
CRATE_PATH_ARG,
"/tmp/crate",
]));
assert_eq!(
filtered,
vec!["--target".to_string(), "web".to_string()],
"a detached euv flag takes the next token with it"
);
}
#[test]
fn filtering_lets_the_wasm_pack_flags_that_look_similar_through() {
let filtered: Vec<String> =
filter_euv_args(&owned(&[OUT_DIR_ARG, "pkg", OUT_NAME_ARG, "game"]));
assert_eq!(
filtered,
vec![
OUT_DIR_ARG.to_string(),
"pkg".to_string(),
OUT_NAME_ARG.to_string(),
"game".to_string()
],
"--out-dir and --out-name are wasm-pack's own, not euv's, so they must pass through"
);
}
#[test]
fn filtering_skips_the_token_after_an_euv_flag_even_when_it_looks_like_another_flag() {
let filtered: Vec<String> = filter_euv_args(&owned(&["--features", PORT_ARG, RELEASE_FLAG]));
assert_eq!(
filtered,
vec!["--features".to_string()],
"the skip is unconditional: a flag following an euv flag is eaten as its value"
);
}
#[test]
fn filtering_drops_an_inline_assignment_because_the_flag_name_matches() {
let filtered: Vec<String> =
filter_euv_args(&owned(&[&format!("{PORT_ARG}=8080"), RELEASE_FLAG]));
assert_eq!(
filtered,
vec![RELEASE_FLAG.to_string()],
"the whitelist compares the flag NAME, so `--port=8080` matches EUV_ARGS and the consumed value never leaks to wasm-pack"
);
}
#[test]
fn filtering_only_considers_the_arguments_after_the_last_separator() {
let filtered: Vec<String> = filter_euv_args(&owned(&[
"--target",
"web",
DOUBLE_DASH,
"--no-opt",
"--features",
"a,b",
]));
assert_eq!(
filtered,
vec![
"--no-opt".to_string(),
"--features".to_string(),
"a,b".to_string()
],
"everything before the last -- is not part of the forwarded set"
);
}
#[test]
fn filtering_with_no_separator_and_no_euv_args_returns_the_input_unchanged() {
let filtered: Vec<String> = filter_euv_args(&owned(&["--target", "web", "--release"]));
assert_eq!(
filtered,
vec![
"--target".to_string(),
"web".to_string(),
"--release".to_string()
]
);
}
#[test]
fn a_build_mode_flag_maps_to_its_own_wasm_pack_spelling() {
assert_eq!(build_mode_to_flag(BuildMode::Dev), DEV_FLAG);
assert_eq!(build_mode_to_flag(BuildMode::Release), RELEASE_FLAG);
assert_eq!(build_mode_to_flag(BuildMode::Profiling), PROFILING_FLAG);
}
#[test]
fn profiling_outranks_release_and_release_outranks_dev() {
let both: ModeArgs = args_from(&[DEV_FLAG, RELEASE_FLAG, PROFILING_FLAG]);
assert_eq!(resolve_build_mode(&both), BuildMode::Profiling);
let dev_and_release: ModeArgs = args_from(&[DEV_FLAG, RELEASE_FLAG]);
assert_eq!(resolve_build_mode(&dev_and_release), BuildMode::Release);
let dev_only: ModeArgs = args_from(&[DEV_FLAG]);
assert_eq!(resolve_build_mode(&dev_only), BuildMode::Dev);
}
#[test]
fn a_build_mode_forwarded_inside_wasm_pack_args_still_resolves() {
let forwarded: ModeArgs = args_from(&[RELEASE_FLAG]);
assert_eq!(
resolve_build_mode(&forwarded),
BuildMode::Release,
"trailing_var_arg swallows the flag, but the mode must still be found"
);
let forwarded_profiling: ModeArgs = args_from(&[PROFILING_FLAG]);
assert_eq!(
resolve_build_mode(&forwarded_profiling),
BuildMode::Profiling
);
}
#[test]
fn with_no_mode_flag_anywhere_the_build_defaults_to_dev() {
let plain: ModeArgs = args_from(&["--target", "web"]);
assert_eq!(resolve_build_mode(&plain), BuildMode::Dev);
let bare: ModeArgs = args_from(&[]);
assert_eq!(resolve_build_mode(&bare), BuildMode::Dev);
}
#[test]
fn the_output_dir_defaults_to_a_pkg_folder_inside_the_www_dir() {
let args: ModeArgs = args_from(&[]);
let out_dir: PathBuf = resolve_out_dir(&args);
assert!(out_dir.ends_with("www/pkg"), "got {out_dir:?}");
}
#[test]
fn an_explicit_out_dir_is_honoured_in_both_spellings() {
let spaced: ModeArgs = args_from(&[OUT_DIR_ARG, "dist"]);
let out_dir: PathBuf = resolve_out_dir(&spaced);
assert!(out_dir.ends_with("dist"), "got {out_dir:?}");
let inline: ModeArgs = args_from(&[&format!("{OUT_DIR_ARG}=dist2")]);
assert!(resolve_out_dir(&inline).ends_with("dist2"));
}
#[test]
fn an_absolute_out_dir_is_not_joined_onto_the_crate_path() {
let args: ModeArgs = args_from(&[OUT_DIR_ARG, "/tmp/euv-pkg"]);
let out_dir: PathBuf = resolve_out_dir(&args);
assert!(out_dir.is_absolute(), "got {out_dir:?}");
assert_eq!(out_dir, PathBuf::from("/tmp/euv-pkg"));
}
#[test]
fn an_out_name_becomes_a_js_filename_with_hyphens_swapped_for_underscores() {
let args: ModeArgs = args_from(&[OUT_NAME_ARG, "my-game"]);
assert_eq!(resolve_out_name(&args), "my_game.js");
let inline: ModeArgs = args_from(&[&format!("{OUT_NAME_ARG}=other-game")]);
assert_eq!(resolve_out_name(&inline), "other_game.js");
}
#[test]
fn without_an_out_name_the_crate_directory_name_becomes_the_filename() {
let args: ModeArgs = args_from(&["--crate-path", "/tmp/some-crate"]);
assert_eq!(
resolve_out_name(&args),
"some_crate.js",
"with no Cargo.toml to read, the directory name is the fallback"
);
}
#[test]
fn the_import_path_points_at_the_js_file_relative_to_the_serving_root() {
let args: ModeArgs = args_from(&[]);
let import: String = resolve_import_path(&args);
assert!(
import.starts_with("./") || import.starts_with("../"),
"an import path is always relative, got {import}"
);
assert!(
import.ends_with(".js"),
"and it must land on the generated script, got {import}"
);
assert!(
!import.contains('\\'),
"the separator is always forward slashes, got {import}"
);
}
#[test]
fn the_serving_route_prefix_is_the_served_directory_relative_to_the_crate() {
let args: ModeArgs = args_from(&[]);
let prefix: String = resolve_serving_route_prefix(&args);
assert_eq!(
prefix, "www",
"with the default layout the served root is the www directory itself"
);
}
fn io_error() -> io::Error {
io::Error::new(io::ErrorKind::NotFound, "no such file")
}
fn utf8_error() -> FromUtf8Error {
match String::from_utf8(vec![0xff, 0xfe]) {
Ok(_) => panic!("the invalid bytes must not decode"),
Err(error) => error,
}
}
#[test]
fn an_io_error_display_joins_the_message_and_the_cause() {
let error: EuvError = EuvError::Io {
message: String::from("reading the manifest"),
error: io_error(),
};
assert_eq!(
error.to_string(),
"reading the manifest: no such file",
"the message alone is not actionable without the cause"
);
}
#[test]
fn an_io_path_error_display_quotes_the_path() {
let error: EuvError = EuvError::IoPath {
message: String::from("writing the output"),
path: PathBuf::from("/tmp/pkg/artifact.wasm"),
error: io_error(),
};
assert_eq!(
error.to_string(),
"writing the output '/tmp/pkg/artifact.wasm': no such file",
"the path is the part the user has to act on, so it is quoted"
);
}
#[test]
fn a_utf8_error_display_joins_the_message_and_the_cause() {
let error: EuvError = EuvError::Utf8 {
message: String::from("decoding the class manifest"),
error: utf8_error(),
};
let rendered: String = error.to_string();
assert!(
rendered.starts_with("decoding the class manifest: "),
"got {rendered}"
);
}
#[test]
fn the_string_only_variants_display_verbatim() {
assert_eq!(
EuvError::Server(String::from("the server refused the port")).to_string(),
"the server refused the port"
);
assert_eq!(
EuvError::Message(String::from("nothing to do")).to_string(),
"nothing to do"
);
}
#[test]
fn the_source_is_the_cause_when_there_is_one_and_none_otherwise() {
let io_kind: EuvError = EuvError::Io {
message: String::from("ctx"),
error: io_error(),
};
let path_kind: EuvError = EuvError::IoPath {
message: String::from("ctx"),
path: PathBuf::from("/tmp"),
error: io_error(),
};
let utf8_kind: EuvError = EuvError::Utf8 {
message: String::from("ctx"),
error: utf8_error(),
};
for error in [io_kind, path_kind, utf8_kind] {
assert!(
error.source().is_some(),
"{error} must hand the caller the underlying cause"
);
}
assert!(
EuvError::Server(String::from("ctx")).source().is_none(),
"a bare message has no cause to expose"
);
assert!(EuvError::Message(String::from("ctx")).source().is_none());
}
#[test]
fn reconciling_moves_a_post_separator_www_dir_into_the_typed_field() {
let mut args: ModeArgs = args_from(&[DOUBLE_DASH, WWW_DIR_ARG, "custom"]);
let before: PathBuf = resolve_out_dir(&args);
assert!(
before.ends_with("www/pkg"),
"after a `--` clap stops parsing, so the typed field still holds its default, got {before:?}"
);
reconcile_args(&mut args);
let after: PathBuf = resolve_out_dir(&args);
assert!(
after.ends_with("custom/pkg"),
"the post-separator --www-dir must reach the field resolve_out_dir reads, got {after:?}"
);
}
#[test]
fn reconciling_is_a_no_op_for_a_www_dir_clap_already_parsed() {
let mut args: ModeArgs = args_from(&[WWW_DIR_ARG, "custom"]);
let before: PathBuf = resolve_out_dir(&args);
assert!(before.ends_with("custom/pkg"));
reconcile_args(&mut args);
assert_eq!(
resolve_out_dir(&args),
before,
"an already-parsed flag must survive reconciliation untouched"
);
}
#[test]
fn reconciling_leaves_an_empty_queue_untouched() {
let mut args: ModeArgs = args_from(&[]);
let before: PathBuf = resolve_out_dir(&args);
reconcile_args(&mut args);
assert_eq!(resolve_out_dir(&args), before);
}
#[test]
fn the_pkg_directory_is_the_resolved_out_directory() {
let args: ModeArgs = args_from(&[]);
assert_eq!(
resolve_pkg_dir(&args),
resolve_out_dir(&args),
"the server serves the same directory the build writes to"
);
}
#[test]
fn the_banner_prints_for_every_action() {
print_banner(Action::Run);
print_banner(Action::Build);
}
#[test]
fn a_banner_action_carries_no_payload() {
let run: Action = Action::Run;
let build: Action = Action::Build;
assert_ne!(run, build, "the two actions must stay distinguishable");
}