nushell/src/command_context.rs
Stefan Holderbach a58d9b0b3a
Refactor/fix tests affecting the whole command set (#15073)
# Description
Pre-cratification of `nu-command` we added tests that covered the whole
command set to ensure consistent documentation style choices and that
the search terms which are added are not uselessly redundant. These
tests are now moved into the suite of the main binary to truly cover all
commands.

- **Move parser quickcheck "fuzz" to `nu-cmd-lang`**
- **Factor out creation of full engine state for tests**
- **Move all-command tests to main context creation**
- **Fix all descriptions**
- **Fix search term duplicate**

# User-Facing Changes
As a result I had to fix a few command argument descriptions. (Doesn't
mean I fully stand behind this choice, but) positionals
(rest/required/optional) and top level descriptions should start with a
capital letter and end with a period. This is not enforced for flags.

# Tests + Formatting
Furthermore I moved our poor-peoples-fuzzer that runs in CI with
`quicktest` over the parser to `nu-cmd-lang` reducing its command set to
just the keywords (similar to
https://github.com/nushell/nushell/pull/15036). Thus this should also
run slightly faster (maybe a slight parallel build cost due to earlier
dependency on quicktest)
2025-02-11 11:36:36 +01:00

240 lines
7.5 KiB
Rust

use nu_protocol::engine::EngineState;
pub(crate) fn get_engine_state() -> EngineState {
let engine_state = nu_cmd_lang::create_default_context();
#[cfg(feature = "plugin")]
let engine_state = nu_cmd_plugin::add_plugin_command_context(engine_state);
let engine_state = nu_command::add_shell_command_context(engine_state);
let engine_state = nu_cmd_extra::add_extra_command_context(engine_state);
let engine_state = nu_cli::add_cli_context(engine_state);
nu_explore::add_explore_context(engine_state)
}
#[cfg(test)]
mod tests {
use super::*;
use nu_protocol::{Category, PositionalArg};
#[test]
fn arguments_end_period() {
fn ends_period(cmd_name: &str, ty: &str, arg: PositionalArg, failures: &mut Vec<String>) {
let arg_name = arg.name;
let desc = arg.desc;
if !desc.ends_with('.') {
failures.push(format!(
"{cmd_name} {ty} argument \"{arg_name}\": \"{desc}\""
));
}
}
let ctx = get_engine_state();
let decls = ctx.get_decls_sorted(true);
let mut failures = Vec::new();
for (name_bytes, decl_id) in decls {
let cmd = ctx.get_decl(decl_id);
let cmd_name = String::from_utf8_lossy(&name_bytes);
let signature = cmd.signature();
for arg in signature.required_positional {
ends_period(&cmd_name, "required", arg, &mut failures);
}
for arg in signature.optional_positional {
ends_period(&cmd_name, "optional", arg, &mut failures);
}
if let Some(arg) = signature.rest_positional {
ends_period(&cmd_name, "rest", arg, &mut failures);
}
}
assert!(
failures.is_empty(),
"Command argument description does not end with a period:\n{}",
failures.join("\n")
);
}
#[test]
fn arguments_start_uppercase() {
fn starts_uppercase(
cmd_name: &str,
ty: &str,
arg: PositionalArg,
failures: &mut Vec<String>,
) {
let arg_name = arg.name;
let desc = arg.desc;
// Check lowercase to allow usage to contain syntax like:
//
// "`as` keyword …"
if desc.starts_with(|u: char| u.is_lowercase()) {
failures.push(format!(
"{cmd_name} {ty} argument \"{arg_name}\": \"{desc}\""
));
}
}
let ctx = get_engine_state();
let decls = ctx.get_decls_sorted(true);
let mut failures = Vec::new();
for (name_bytes, decl_id) in decls {
let cmd = ctx.get_decl(decl_id);
let cmd_name = String::from_utf8_lossy(&name_bytes);
let signature = cmd.signature();
for arg in signature.required_positional {
starts_uppercase(&cmd_name, "required", arg, &mut failures);
}
for arg in signature.optional_positional {
starts_uppercase(&cmd_name, "optional", arg, &mut failures);
}
if let Some(arg) = signature.rest_positional {
starts_uppercase(&cmd_name, "rest", arg, &mut failures);
}
}
assert!(
failures.is_empty(),
"Command argument description does not end with a period:\n{}",
failures.join("\n")
);
}
#[test]
fn signature_name_matches_command_name() {
let ctx = get_engine_state();
let decls = ctx.get_decls_sorted(true);
let mut failures = Vec::new();
for (name_bytes, decl_id) in decls {
let cmd = ctx.get_decl(decl_id);
let cmd_name = String::from_utf8_lossy(&name_bytes);
let sig_name = cmd.signature().name;
let category = cmd.signature().category;
if cmd_name != sig_name {
failures.push(format!(
"{cmd_name} ({category:?}): Signature name \"{sig_name}\" is not equal to the command name \"{cmd_name}\""
));
}
}
assert!(
failures.is_empty(),
"Name mismatch:\n{}",
failures.join("\n")
);
}
#[test]
fn commands_declare_input_output_types() {
let ctx = get_engine_state();
let decls = ctx.get_decls_sorted(true);
let mut failures = Vec::new();
for (_, decl_id) in decls {
let cmd = ctx.get_decl(decl_id);
let sig_name = cmd.signature().name;
let category = cmd.signature().category;
if matches!(category, Category::Removed) {
// Deprecated/Removed commands don't have to conform
continue;
}
if cmd.signature().input_output_types.is_empty() {
failures.push(format!(
"{sig_name} ({category:?}): No pipeline input/output type signatures found"
));
}
}
assert!(
failures.is_empty(),
"Command missing type annotations:\n{}",
failures.join("\n")
);
}
#[test]
fn no_search_term_duplicates() {
let ctx = get_engine_state();
let decls = ctx.get_decls_sorted(true);
let mut failures = Vec::new();
for (name_bytes, decl_id) in decls {
let cmd = ctx.get_decl(decl_id);
let cmd_name = String::from_utf8_lossy(&name_bytes);
let search_terms = cmd.search_terms();
let category = cmd.signature().category;
for search_term in search_terms {
if cmd_name.contains(search_term) {
failures.push(format!("{cmd_name} ({category:?}): Search term \"{search_term}\" is substring of command name \"{cmd_name}\""));
}
}
}
assert!(
failures.is_empty(),
"Duplication in search terms:\n{}",
failures.join("\n")
);
}
#[test]
fn description_end_period() {
let ctx = get_engine_state();
let decls = ctx.get_decls_sorted(true);
let mut failures = Vec::new();
for (name_bytes, decl_id) in decls {
let cmd = ctx.get_decl(decl_id);
let cmd_name = String::from_utf8_lossy(&name_bytes);
let description = cmd.description();
if !description.ends_with('.') {
failures.push(format!("{cmd_name}: \"{description}\""));
}
}
assert!(
failures.is_empty(),
"Command description does not end with a period:\n{}",
failures.join("\n")
);
}
#[test]
fn description_start_uppercase() {
let ctx = get_engine_state();
let decls = ctx.get_decls_sorted(true);
let mut failures = Vec::new();
for (name_bytes, decl_id) in decls {
let cmd = ctx.get_decl(decl_id);
let cmd_name = String::from_utf8_lossy(&name_bytes);
let description = cmd.description();
// Check lowercase to allow description to contain syntax like:
//
// "`$env.FOO = ...`"
if description.starts_with(|u: char| u.is_lowercase()) {
failures.push(format!("{cmd_name}: \"{description}\""));
}
}
assert!(
failures.is_empty(),
"Command description does not start with an uppercase letter:\n{}",
failures.join("\n")
);
}
}