forked from extern/nushell
add signature information when get help on one command (#7079)
* add signature information when help on one command * tell user that one command support operated on cell paths Also, make type output to be more friendly, like `record<>` should just be `record` And the same to `table<>`, which should be `table` * simplify code * don't show signatures for parser keyword * update comment * output arg syntax shape as type, so it's the same as describe command * fix string when no positional args * update signature body * update * add help signature test * fix arg output format for composed data type like list or record * fix clippy * add comment
This commit is contained in:
parent
a896892ac9
commit
d01ccd5a54
@ -17,7 +17,7 @@ impl NuHelpCompleter {
|
||||
//Vec<(Signature, Vec<Example>, bool, bool)> {
|
||||
let mut commands = full_commands
|
||||
.iter()
|
||||
.filter(|(sig, _, _, _)| {
|
||||
.filter(|(sig, _, _, _, _)| {
|
||||
sig.name.to_lowercase().contains(&line.to_lowercase())
|
||||
|| sig.usage.to_lowercase().contains(&line.to_lowercase())
|
||||
|| sig
|
||||
@ -31,7 +31,7 @@ impl NuHelpCompleter {
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
commands.sort_by(|(a, _, _, _), (b, _, _, _)| {
|
||||
commands.sort_by(|(a, _, _, _, _), (b, _, _, _, _)| {
|
||||
let a_distance = levenshtein_distance(line, &a.name);
|
||||
let b_distance = levenshtein_distance(line, &b.name);
|
||||
a_distance.cmp(&b_distance)
|
||||
@ -39,7 +39,7 @@ impl NuHelpCompleter {
|
||||
|
||||
commands
|
||||
.into_iter()
|
||||
.map(|(sig, examples, _, _)| {
|
||||
.map(|(sig, examples, _, _, _)| {
|
||||
let mut long_desc = String::new();
|
||||
|
||||
let usage = &sig.usage;
|
||||
|
@ -29,7 +29,13 @@ impl Command for Bits {
|
||||
_input: PipelineData,
|
||||
) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> {
|
||||
Ok(Value::String {
|
||||
val: get_full_help(&Bits.signature(), &Bits.examples(), engine_state, stack),
|
||||
val: get_full_help(
|
||||
&Bits.signature(),
|
||||
&Bits.examples(),
|
||||
engine_state,
|
||||
stack,
|
||||
self.is_parser_keyword(),
|
||||
),
|
||||
span: call.head,
|
||||
}
|
||||
.into_pipeline_data())
|
||||
|
@ -29,7 +29,13 @@ impl Command for Bytes {
|
||||
_input: PipelineData,
|
||||
) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> {
|
||||
Ok(Value::String {
|
||||
val: get_full_help(&Bytes.signature(), &Bytes.examples(), engine_state, stack),
|
||||
val: get_full_help(
|
||||
&Bytes.signature(),
|
||||
&Bytes.examples(),
|
||||
engine_state,
|
||||
stack,
|
||||
self.is_parser_keyword(),
|
||||
),
|
||||
span: call.head,
|
||||
}
|
||||
.into_pipeline_data())
|
||||
|
@ -29,7 +29,13 @@ impl Command for Into {
|
||||
_input: PipelineData,
|
||||
) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> {
|
||||
Ok(Value::String {
|
||||
val: get_full_help(&Into.signature(), &[], engine_state, stack),
|
||||
val: get_full_help(
|
||||
&Into.signature(),
|
||||
&[],
|
||||
engine_state,
|
||||
stack,
|
||||
self.is_parser_keyword(),
|
||||
),
|
||||
span: call.head,
|
||||
}
|
||||
.into_pipeline_data())
|
||||
|
@ -45,6 +45,7 @@ impl Command for ExportCommand {
|
||||
&ExportCommand.examples(),
|
||||
engine_state,
|
||||
stack,
|
||||
self.is_parser_keyword(),
|
||||
),
|
||||
span: call.head,
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
use fancy_regex::Regex;
|
||||
use itertools::Itertools;
|
||||
use nu_ansi_term::{
|
||||
Color::{Default, Red, White},
|
||||
Style,
|
||||
@ -105,6 +104,7 @@ fn help(
|
||||
let mut vals = vec![];
|
||||
let decl = engine_state.get_decl(decl_id);
|
||||
let sig = decl.signature().update_from_command(decl.borrow());
|
||||
let signatures = sig.to_string();
|
||||
let key = sig.name;
|
||||
let usage = sig.usage;
|
||||
let search_terms = sig.search_terms;
|
||||
@ -154,11 +154,11 @@ fn help(
|
||||
|
||||
cols.push("signatures".into());
|
||||
vals.push(Value::String {
|
||||
val: sig
|
||||
.input_output_types
|
||||
.iter()
|
||||
.map(|(i, o)| format!("{:?} => {:?}", i.to_shape(), o.to_shape()))
|
||||
.join("\n"),
|
||||
val: if decl.is_parser_keyword() {
|
||||
"".to_string()
|
||||
} else {
|
||||
signatures
|
||||
},
|
||||
span: head,
|
||||
});
|
||||
|
||||
@ -219,6 +219,7 @@ fn help(
|
||||
let decl = engine_state.get_decl(decl_id);
|
||||
let sig = decl.signature().update_from_command(decl.borrow());
|
||||
|
||||
let signatures = sig.to_string();
|
||||
let key = sig.name;
|
||||
let usage = sig.usage;
|
||||
let search_terms = sig.search_terms;
|
||||
@ -249,11 +250,11 @@ fn help(
|
||||
|
||||
cols.push("signatures".into());
|
||||
vals.push(Value::String {
|
||||
val: sig
|
||||
.input_output_types
|
||||
.iter()
|
||||
.map(|(i, o)| format!("{:?} => {:?}", i.to_shape(), o.to_shape()))
|
||||
.join("\n"),
|
||||
val: if decl.is_parser_keyword() {
|
||||
"".to_string()
|
||||
} else {
|
||||
signatures
|
||||
},
|
||||
span: head,
|
||||
});
|
||||
|
||||
@ -290,9 +291,9 @@ fn help(
|
||||
let output = engine_state
|
||||
.get_signatures_with_examples(false)
|
||||
.iter()
|
||||
.filter(|(signature, _, _, _)| signature.name == name)
|
||||
.map(|(signature, examples, _, _)| {
|
||||
get_full_help(signature, examples, engine_state, stack)
|
||||
.filter(|(signature, _, _, _, _)| signature.name == name)
|
||||
.map(|(signature, examples, _, _, is_parser_keyword)| {
|
||||
get_full_help(signature, examples, engine_state, stack, *is_parser_keyword)
|
||||
})
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
|
@ -38,7 +38,13 @@ impl Command for Overlay {
|
||||
_input: PipelineData,
|
||||
) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> {
|
||||
Ok(Value::String {
|
||||
val: get_full_help(&Overlay.signature(), &[], engine_state, stack),
|
||||
val: get_full_help(
|
||||
&Overlay.signature(),
|
||||
&[],
|
||||
engine_state,
|
||||
stack,
|
||||
self.is_parser_keyword(),
|
||||
),
|
||||
span: call.head,
|
||||
}
|
||||
.into_pipeline_data())
|
||||
|
@ -53,7 +53,13 @@ fn date(
|
||||
let head = call.head;
|
||||
|
||||
Ok(Value::String {
|
||||
val: get_full_help(&Date.signature(), &Date.examples(), engine_state, stack),
|
||||
val: get_full_help(
|
||||
&Date.signature(),
|
||||
&Date.examples(),
|
||||
engine_state,
|
||||
stack,
|
||||
false,
|
||||
),
|
||||
span: head,
|
||||
}
|
||||
.into_pipeline_data())
|
||||
|
1
crates/nu-command/src/env/config/config_.rs
vendored
1
crates/nu-command/src/env/config/config_.rs
vendored
@ -34,6 +34,7 @@ impl Command for ConfigMeta {
|
||||
&ConfigMeta.examples(),
|
||||
engine_state,
|
||||
stack,
|
||||
self.is_parser_keyword(),
|
||||
),
|
||||
span: call.head,
|
||||
}
|
||||
|
@ -31,7 +31,13 @@ impl Command for Roll {
|
||||
_input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
Ok(Value::String {
|
||||
val: get_full_help(&Roll.signature(), &Roll.examples(), engine_state, stack),
|
||||
val: get_full_help(
|
||||
&Roll.signature(),
|
||||
&Roll.examples(),
|
||||
engine_state,
|
||||
stack,
|
||||
self.is_parser_keyword(),
|
||||
),
|
||||
span: call.head,
|
||||
}
|
||||
.into_pipeline_data())
|
||||
|
@ -27,7 +27,13 @@ impl Command for From {
|
||||
_input: PipelineData,
|
||||
) -> Result<nu_protocol::PipelineData, ShellError> {
|
||||
Ok(Value::String {
|
||||
val: get_full_help(&From.signature(), &From.examples(), engine_state, stack),
|
||||
val: get_full_help(
|
||||
&From.signature(),
|
||||
&From.examples(),
|
||||
engine_state,
|
||||
stack,
|
||||
self.is_parser_keyword(),
|
||||
),
|
||||
span: call.head,
|
||||
}
|
||||
.into_pipeline_data())
|
||||
|
@ -27,7 +27,13 @@ impl Command for To {
|
||||
_input: PipelineData,
|
||||
) -> Result<nu_protocol::PipelineData, ShellError> {
|
||||
Ok(Value::String {
|
||||
val: get_full_help(&To.signature(), &To.examples(), engine_state, stack),
|
||||
val: get_full_help(
|
||||
&To.signature(),
|
||||
&To.examples(),
|
||||
engine_state,
|
||||
stack,
|
||||
self.is_parser_keyword(),
|
||||
),
|
||||
span: call.head,
|
||||
}
|
||||
.into_pipeline_data())
|
||||
|
@ -27,7 +27,13 @@ impl Command for Hash {
|
||||
_input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
Ok(Value::String {
|
||||
val: get_full_help(&Self.signature(), &Self.examples(), engine_state, stack),
|
||||
val: get_full_help(
|
||||
&Self.signature(),
|
||||
&Self.examples(),
|
||||
engine_state,
|
||||
stack,
|
||||
self.is_parser_keyword(),
|
||||
),
|
||||
span: call.head,
|
||||
}
|
||||
.into_pipeline_data())
|
||||
|
@ -34,6 +34,7 @@ impl Command for MathCommand {
|
||||
&MathCommand.examples(),
|
||||
engine_state,
|
||||
stack,
|
||||
self.is_parser_keyword(),
|
||||
),
|
||||
span: call.head,
|
||||
}
|
||||
|
@ -35,7 +35,13 @@ impl Command for Url {
|
||||
_input: PipelineData,
|
||||
) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> {
|
||||
Ok(Value::String {
|
||||
val: get_full_help(&Url.signature(), &Url.examples(), engine_state, stack),
|
||||
val: get_full_help(
|
||||
&Url.signature(),
|
||||
&Url.examples(),
|
||||
engine_state,
|
||||
stack,
|
||||
self.is_parser_keyword(),
|
||||
),
|
||||
span: call.head,
|
||||
}
|
||||
.into_pipeline_data())
|
||||
|
@ -49,6 +49,7 @@ the path literal."#
|
||||
&PathCommand.examples(),
|
||||
engine_state,
|
||||
stack,
|
||||
self.is_parser_keyword(),
|
||||
),
|
||||
span: call.head,
|
||||
}
|
||||
|
@ -38,6 +38,7 @@ impl Command for Keybindings {
|
||||
&Keybindings.examples(),
|
||||
engine_state,
|
||||
stack,
|
||||
self.is_parser_keyword(),
|
||||
),
|
||||
span: call.head,
|
||||
}
|
||||
|
@ -38,6 +38,7 @@ impl Command for RandomCommand {
|
||||
&RandomCommand.examples(),
|
||||
engine_state,
|
||||
stack,
|
||||
self.is_parser_keyword(),
|
||||
),
|
||||
span: call.head,
|
||||
}
|
||||
|
@ -34,6 +34,7 @@ impl Command for SplitCommand {
|
||||
&SplitCommand.examples(),
|
||||
engine_state,
|
||||
stack,
|
||||
self.is_parser_keyword(),
|
||||
),
|
||||
span: call.head,
|
||||
}
|
||||
|
@ -29,7 +29,13 @@ impl Command for Str {
|
||||
_input: PipelineData,
|
||||
) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> {
|
||||
Ok(Value::String {
|
||||
val: get_full_help(&Str.signature(), &Str.examples(), engine_state, stack),
|
||||
val: get_full_help(
|
||||
&Str.signature(),
|
||||
&Str.examples(),
|
||||
engine_state,
|
||||
stack,
|
||||
self.is_parser_keyword(),
|
||||
),
|
||||
span: call.head,
|
||||
}
|
||||
.into_pipeline_data())
|
||||
|
@ -14,3 +14,15 @@ fn help_commands_length() {
|
||||
let is_positive = output_int.is_positive();
|
||||
assert!(is_positive);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn help_shows_signature() {
|
||||
let actual = nu!(cwd: ".", pipeline("help str distance"));
|
||||
assert!(actual
|
||||
.out
|
||||
.contains("<string> | str distance <string> -> <int>"));
|
||||
|
||||
// don't show signature for parser keyword
|
||||
let actual = nu!(cwd: ".", pipeline("help alias"));
|
||||
assert!(!actual.out.contains("Signatures"));
|
||||
}
|
||||
|
@ -92,7 +92,7 @@ fn reject_record_from_raw_eval() {
|
||||
)
|
||||
);
|
||||
|
||||
assert!(actual.out.contains("record<>"));
|
||||
assert!(actual.out.contains("record"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -10,6 +10,7 @@ pub fn get_full_help(
|
||||
examples: &[Example],
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
is_parser_keyword: bool,
|
||||
) -> String {
|
||||
let config = engine_state.get_config();
|
||||
let doc_config = DocumentationConfig {
|
||||
@ -17,7 +18,14 @@ pub fn get_full_help(
|
||||
no_color: !config.use_ansi_coloring,
|
||||
brief: false,
|
||||
};
|
||||
get_documentation(sig, examples, engine_state, stack, &doc_config)
|
||||
get_documentation(
|
||||
sig,
|
||||
examples,
|
||||
engine_state,
|
||||
stack,
|
||||
&doc_config,
|
||||
is_parser_keyword,
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
@ -34,6 +42,7 @@ fn get_documentation(
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
config: &DocumentationConfig,
|
||||
is_parser_keyword: bool,
|
||||
) -> String {
|
||||
// Create ansi colors
|
||||
const G: &str = "\x1b[32m"; // green
|
||||
@ -89,6 +98,18 @@ fn get_documentation(
|
||||
long_desc.push_str(&get_flags_section(sig))
|
||||
}
|
||||
|
||||
if !is_parser_keyword && !sig.input_output_types.is_empty() {
|
||||
if sig.operates_on_cell_paths() {
|
||||
let _ = writeln!(
|
||||
long_desc,
|
||||
"\n{}Signatures(Cell paths are supported){}:\n{}",
|
||||
G, RESET, sig
|
||||
);
|
||||
} else {
|
||||
let _ = writeln!(long_desc, "\n{}Signatures{}:\n{}", G, RESET, sig);
|
||||
}
|
||||
}
|
||||
|
||||
if !sig.required_positional.is_empty()
|
||||
|| !sig.optional_positional.is_empty()
|
||||
|| sig.rest_positional.is_some()
|
||||
|
@ -43,7 +43,13 @@ pub fn eval_call(
|
||||
signature.usage = decl.usage().to_string();
|
||||
signature.extra_usage = decl.extra_usage().to_string();
|
||||
|
||||
let full_help = get_full_help(&signature, &decl.examples(), engine_state, caller_stack);
|
||||
let full_help = get_full_help(
|
||||
&signature,
|
||||
&decl.examples(),
|
||||
engine_state,
|
||||
caller_stack,
|
||||
decl.is_parser_keyword(),
|
||||
);
|
||||
Ok(Value::String {
|
||||
val: full_help,
|
||||
span: call.head,
|
||||
|
@ -700,7 +700,7 @@ impl EngineState {
|
||||
pub fn get_signatures_with_examples(
|
||||
&self,
|
||||
include_hidden: bool,
|
||||
) -> Vec<(Signature, Vec<Example>, bool, bool)> {
|
||||
) -> Vec<(Signature, Vec<Example>, bool, bool, bool)> {
|
||||
self.get_decl_ids_sorted(include_hidden)
|
||||
.map(|id| {
|
||||
let decl = self.get_decl(id);
|
||||
@ -712,6 +712,7 @@ impl EngineState {
|
||||
decl.examples(),
|
||||
decl.is_plugin().is_some(),
|
||||
decl.get_block_id().is_some(),
|
||||
decl.is_parser_keyword(),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
|
@ -122,6 +122,72 @@ pub struct Signature {
|
||||
pub category: Category,
|
||||
}
|
||||
|
||||
/// Fromat argumet type for user readable output.
|
||||
///
|
||||
/// In general:
|
||||
/// if argument type is a simple type(like string), we'll wrapped with `<>`, the result will be `<string>`
|
||||
/// if argument type is already contains `<>`, like `list<any>`, the result will be `list<any>`.
|
||||
fn fmt_type(arg_type: &Type, optional: bool) -> String {
|
||||
let arg_type = arg_type.to_string();
|
||||
if arg_type.contains('<') && arg_type.contains('>') {
|
||||
if optional {
|
||||
format!("{arg_type}?")
|
||||
} else {
|
||||
arg_type
|
||||
}
|
||||
} else if optional {
|
||||
format!("<{arg_type}?>")
|
||||
} else {
|
||||
format!("<{arg_type}>")
|
||||
}
|
||||
}
|
||||
|
||||
// in general, a commands signature should looks like this:
|
||||
//
|
||||
// <string> | <string>, <int?> => string
|
||||
//
|
||||
// More detail explaination:
|
||||
// the first one is the input from previous command, aka, pipeline input
|
||||
// then followed by `|`, then positional arguments type
|
||||
// then optional arguments type, which ends with `?`
|
||||
// Then followed by `->`
|
||||
// Finally output type.
|
||||
//
|
||||
// If a command contains multiple input/output types, separate them in different lines.
|
||||
impl std::fmt::Display for Signature {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let mut args = self
|
||||
.required_positional
|
||||
.iter()
|
||||
.map(|p| fmt_type(&p.shape.to_type(), false))
|
||||
.collect::<Vec<String>>();
|
||||
args.append(
|
||||
&mut self
|
||||
.optional_positional
|
||||
.iter()
|
||||
.map(|p| fmt_type(&p.shape.to_type(), true))
|
||||
.collect::<Vec<String>>(),
|
||||
);
|
||||
let args = args.join(", ");
|
||||
|
||||
let mut signatures = vec![];
|
||||
for (input_type, output_type) in self.input_output_types.iter() {
|
||||
// ident with two spaces for user friendly output.
|
||||
let input_type = fmt_type(input_type, false);
|
||||
let output_type = fmt_type(output_type, false);
|
||||
if args.is_empty() {
|
||||
signatures.push(format!(" {input_type} | {} -> {output_type}", self.name))
|
||||
} else {
|
||||
signatures.push(format!(
|
||||
" {input_type} | {} {args} -> {output_type}",
|
||||
self.name
|
||||
))
|
||||
}
|
||||
}
|
||||
write!(f, "{}", signatures.join("\n"))
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Signature {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.name == other.name
|
||||
|
@ -107,24 +107,36 @@ impl Display for Type {
|
||||
Type::Float => write!(f, "float"),
|
||||
Type::Int => write!(f, "int"),
|
||||
Type::Range => write!(f, "range"),
|
||||
Type::Record(fields) => write!(
|
||||
f,
|
||||
"record<{}>",
|
||||
fields
|
||||
.iter()
|
||||
.map(|(x, y)| format!("{}: {}", x, y))
|
||||
.collect::<Vec<String>>()
|
||||
.join(", "),
|
||||
),
|
||||
Type::Table(columns) => write!(
|
||||
f,
|
||||
"table<{}>",
|
||||
columns
|
||||
.iter()
|
||||
.map(|(x, y)| format!("{}: {}", x, y))
|
||||
.collect::<Vec<String>>()
|
||||
.join(", ")
|
||||
),
|
||||
Type::Record(fields) => {
|
||||
if fields.is_empty() {
|
||||
write!(f, "record")
|
||||
} else {
|
||||
write!(
|
||||
f,
|
||||
"record<{}>",
|
||||
fields
|
||||
.iter()
|
||||
.map(|(x, y)| format!("{}: {}", x, y))
|
||||
.collect::<Vec<String>>()
|
||||
.join(", "),
|
||||
)
|
||||
}
|
||||
}
|
||||
Type::Table(columns) => {
|
||||
if columns.is_empty() {
|
||||
write!(f, "table")
|
||||
} else {
|
||||
write!(
|
||||
f,
|
||||
"table<{}>",
|
||||
columns
|
||||
.iter()
|
||||
.map(|(x, y)| format!("{}: {}", x, y))
|
||||
.collect::<Vec<String>>()
|
||||
.join(", ")
|
||||
)
|
||||
}
|
||||
}
|
||||
Type::List(l) => write!(f, "list<{}>", l),
|
||||
Type::Nothing => write!(f, "nothing"),
|
||||
Type::Number => write!(f, "number"),
|
||||
|
19
src/main.rs
19
src/main.rs
@ -563,8 +563,13 @@ fn parse_commandline_args(
|
||||
let help = call.has_flag("help");
|
||||
|
||||
if help {
|
||||
let full_help =
|
||||
get_full_help(&Nu.signature(), &Nu.examples(), engine_state, &mut stack);
|
||||
let full_help = get_full_help(
|
||||
&Nu.signature(),
|
||||
&Nu.examples(),
|
||||
engine_state,
|
||||
&mut stack,
|
||||
true,
|
||||
);
|
||||
|
||||
let _ = std::panic::catch_unwind(move || stdout_write_all_and_flush(full_help));
|
||||
|
||||
@ -600,7 +605,13 @@ fn parse_commandline_args(
|
||||
}
|
||||
|
||||
// Just give the help and exit if the above fails
|
||||
let full_help = get_full_help(&Nu.signature(), &Nu.examples(), engine_state, &mut stack);
|
||||
let full_help = get_full_help(
|
||||
&Nu.signature(),
|
||||
&Nu.examples(),
|
||||
engine_state,
|
||||
&mut stack,
|
||||
true,
|
||||
);
|
||||
print!("{}", full_help);
|
||||
std::process::exit(1);
|
||||
}
|
||||
@ -731,7 +742,7 @@ impl Command for Nu {
|
||||
_input: PipelineData,
|
||||
) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> {
|
||||
Ok(Value::String {
|
||||
val: get_full_help(&Nu.signature(), &Nu.examples(), engine_state, stack),
|
||||
val: get_full_help(&Nu.signature(), &Nu.examples(), engine_state, stack, true),
|
||||
span: call.head,
|
||||
}
|
||||
.into_pipeline_data())
|
||||
|
@ -53,7 +53,7 @@ fn in_and_if_else() -> TestResult {
|
||||
|
||||
#[test]
|
||||
fn help_works_with_missing_requirements() -> TestResult {
|
||||
run_test(r#"each --help | lines | length"#, "37")
|
||||
run_test(r#"each --help | lines | length"#, "40")
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
Loading…
Reference in New Issue
Block a user