Completion.cpp (20272B)
1 // Copyright (c) Microsoft Corporation. 2 // Licensed under the MIT License. 3 #include "pch.h" 4 #include "TestCommon.h" 5 #include <AppInstallerErrors.h> 6 #include <Command.h> 7 #include <Commands/CompleteCommand.h> 8 #include <Commands/RootCommand.h> 9 #include <Commands/SourceCommand.h> 10 #include <CompletionData.h> 11 12 using namespace std::string_literals; 13 using namespace std::string_view_literals; 14 using namespace TestCommon; 15 using namespace AppInstaller; 16 using namespace AppInstaller::CLI; 17 using namespace AppInstaller::CLI::Execution; 18 19 20 TEST_CASE("CompletionData_EmptyWord_PositionAtEnd", "[complete]") 21 { 22 CompletionData cd{ "", "winget ", "7" }; 23 REQUIRE(cd.Word() == ""); 24 REQUIRE(cd.BeforeWord().size() == 0); 25 REQUIRE(cd.AfterWord().size() == 0); 26 } 27 28 TEST_CASE("CompletionData_EmptyWord_PositionPastEnd", "[complete]") 29 { 30 CompletionData cd{ "", "winget ", "8" }; 31 REQUIRE(cd.Word() == ""); 32 REQUIRE(cd.BeforeWord().size() == 0); 33 REQUIRE(cd.AfterWord().size() == 0); 34 } 35 36 TEST_CASE("CompletionData_EmptyWord_PositionCorrect", "[complete]") 37 { 38 CompletionData cd{ "", "winget install", "7" }; 39 REQUIRE(cd.Word() == ""); 40 REQUIRE(cd.BeforeWord().size() == 0); 41 REQUIRE(cd.AfterWord().size() == 1); 42 } 43 44 45 TEST_CASE("CompletionData_EmptyWord_PositionOffset", "[complete]") 46 { 47 CompletionData cd{ "", "winget install PowerToys --version", "17" }; 48 REQUIRE(cd.Word() == ""); 49 REQUIRE(cd.BeforeWord().size() == 1); 50 REQUIRE(cd.AfterWord().size() == 2); 51 } 52 53 TEST_CASE("CompletionData_Word_NoMatch", "[complete]") 54 { 55 auto lambda = []() { CompletionData cd{ "foo", "winget install PowerToys --version", "17" }; }; 56 REQUIRE_THROWS_HR(lambda(), APPINSTALLER_CLI_ERROR_COMPLETE_INPUT_BAD); 57 } 58 59 TEST_CASE("CompletionData_Word_SingleMatch", "[complete]") 60 { 61 CompletionData cd{ "power", "winget install power --version", "17" }; 62 REQUIRE(cd.Word() == "power"); 63 REQUIRE(cd.BeforeWord().size() == 1); 64 REQUIRE(cd.AfterWord().size() == 1); 65 } 66 67 TEST_CASE("CompletionData_Word_MultiMatch_PositionCorrect", "[complete]") 68 { 69 CompletionData cd{ "power", "winget install power --id power", "27" }; 70 REQUIRE(cd.Word() == "power"); 71 REQUIRE(cd.BeforeWord().size() == 3); 72 REQUIRE(cd.AfterWord().size() == 0); 73 } 74 75 TEST_CASE("CompletionData_Word_MultiMatch_PositionOffset", "[complete]") 76 { 77 CompletionData cd{ "power", "winget install power --id power", "21" }; 78 REQUIRE(cd.Word() == "power"); 79 REQUIRE(cd.BeforeWord().size() == 1); 80 REQUIRE(cd.AfterWord().size() == 2); 81 } 82 83 TEST_CASE("CompletionData_UTF8_EmptyWord_End", "[complete]") 84 { 85 CompletionData cd{ "", u8"winget install \x175\x12b\x14b\x1e5\x229\x288 --version ", "32" }; 86 REQUIRE(cd.Word() == ""); 87 REQUIRE(cd.BeforeWord().size() == 3); 88 REQUIRE(cd.AfterWord().size() == 0); 89 } 90 91 TEST_CASE("CompletionData_UTF8_EmptyWord_Middle", "[complete]") 92 { 93 CompletionData cd{ "", u8"winget install \x175\x12b\x14b\x1e5\x229\x288 --version ", "22" }; 94 REQUIRE(cd.Word() == ""); 95 REQUIRE(cd.BeforeWord().size() == 2); 96 REQUIRE(cd.AfterWord().size() == 1); 97 } 98 99 TEST_CASE("CompletionData_UTF8_UTF8Word", "[complete]") 100 { 101 CompletionData cd{ u8"\x175\x12b\x14b\x1e5\x229\x288", u8"winget install \x175\x12b\x14b\x1e5\x229\x288 --version ", "18" }; 102 REQUIRE(cd.Word() == u8"\x175\x12b\x14b\x1e5\x229\x288"); 103 REQUIRE(cd.BeforeWord().size() == 1); 104 REQUIRE(cd.AfterWord().size() == 1); 105 } 106 107 void OutputAllSubCommands(Command& command, std::ostream& out, std::string_view filter = {}) 108 { 109 for (const auto& c : command.GetCommands()) 110 { 111 if (Utility::CaseInsensitiveStartsWith(c->Name(), filter)) 112 { 113 out << c->Name() << std::endl; 114 } 115 } 116 } 117 118 void OutputAllArgumentNames(Command& command, std::ostream& out, std::string_view filter = {}, bool includeCommon = true) 119 { 120 auto args = command.GetArguments(); 121 if (includeCommon) 122 { 123 Argument::GetCommon(args); 124 } 125 126 for (const auto& a : args) 127 { 128 if (Utility::CaseInsensitiveStartsWith(a.Name(), filter)) 129 { 130 out << APPINSTALLER_CLI_ARGUMENT_IDENTIFIER_CHAR << APPINSTALLER_CLI_ARGUMENT_IDENTIFIER_CHAR << a.Name() << std::endl; 131 } 132 } 133 } 134 135 void OutputAllArgumentAliases(Command& command, std::ostream& out, bool includeCommon = true) 136 { 137 auto args = command.GetArguments(); 138 if (includeCommon) 139 { 140 Argument::GetCommon(args); 141 } 142 143 for (const auto& a : args) 144 { 145 if (a.Alias() != ArgumentCommon::NoAlias) 146 { 147 out << APPINSTALLER_CLI_ARGUMENT_IDENTIFIER_CHAR << a.Alias() << std::endl; 148 } 149 } 150 } 151 152 TEST_CASE("CompleteCommand_FindRoot", "[complete]") 153 { 154 std::stringstream out, in; 155 Context context{ out, in }; 156 157 CompleteCommand command{ "test" }; 158 context.Args.AddArg(Args::Type::Word, ""sv); 159 context.Args.AddArg(Args::Type::CommandLine, "winget "sv); 160 context.Args.AddArg(Args::Type::Position, "7"sv); 161 command.Execute(context); 162 163 // Create expected values 164 RootCommand expectedCommand; 165 std::stringstream expected; 166 OutputAllSubCommands(expectedCommand, expected); 167 OutputAllArgumentNames(expectedCommand, expected); 168 169 REQUIRE(out.str() == expected.str()); 170 } 171 172 TEST_CASE("CompleteCommand_FindSource", "[complete]") 173 { 174 std::stringstream out, in; 175 Context context{ out, in }; 176 177 CompleteCommand command{ "test" }; 178 context.Args.AddArg(Args::Type::Word, ""sv); 179 context.Args.AddArg(Args::Type::CommandLine, "winget source "sv); 180 context.Args.AddArg(Args::Type::Position, "14"sv); 181 command.Execute(context); 182 183 // Create expected values 184 SourceCommand expectedCommand{ "test" }; 185 std::stringstream expected; 186 OutputAllSubCommands(expectedCommand, expected); 187 OutputAllArgumentNames(expectedCommand, expected); 188 189 REQUIRE(out.str() == expected.str()); 190 } 191 192 TEST_CASE("CompleteCommand_FindSourceAdd", "[complete]") 193 { 194 std::stringstream out, in; 195 Context context{ out, in }; 196 197 CompleteCommand command{ "test" }; 198 context.Args.AddArg(Args::Type::Word, ""sv); 199 context.Args.AddArg(Args::Type::CommandLine, "winget source add "sv); 200 context.Args.AddArg(Args::Type::Position, "18"sv); 201 command.Execute(context); 202 203 // Create expected values 204 SourceAddCommand expectedCommand{ "test" }; 205 std::stringstream expected; 206 OutputAllSubCommands(expectedCommand, expected); 207 OutputAllArgumentNames(expectedCommand, expected); 208 209 REQUIRE(out.str() == expected.str()); 210 } 211 212 struct CompletionTestCommand : public Command 213 { 214 CompletionTestCommand() : Command("test", "") {} 215 CompletionTestCommand(std::string_view name) : Command(name, "") {} 216 217 std::vector<std::unique_ptr<Command>> GetCommands() const override 218 { 219 std::vector<std::unique_ptr<Command>> result; 220 221 for (const auto& sc : SubCommandNames) 222 { 223 result.emplace_back(std::make_unique<CompletionTestCommand>(sc)); 224 } 225 226 return result; 227 } 228 229 std::vector<Argument> GetArguments() const override 230 { 231 return Arguments; 232 } 233 234 CLI::Resource::LocString ShortDescription() const override { return {}; } 235 CLI::Resource::LocString LongDescription() const override { return {}; } 236 237 using Command::Complete; 238 239 void Complete(Execution::Context& context, Execution::Args::Type valueType) const override 240 { 241 if (ArgumentValueCallback) 242 { 243 ArgumentValueCallback(context, valueType); 244 } 245 } 246 247 std::vector<std::string> SubCommandNames; 248 std::vector<Argument> Arguments; 249 std::function<void(Context&, Execution::Args::Type)> ArgumentValueCallback; 250 }; 251 252 struct CompletionTestContext 253 { 254 CompletionTestContext(std::string_view word, std::string_view commandLine, std::string_view position) : 255 context(out, in) 256 { 257 context.Reporter.SetChannel(Execution::Reporter::Channel::Completion); 258 context.Add<Data::CompletionData>(CompletionData{ word, commandLine, position }); 259 } 260 261 std::stringstream out; 262 std::stringstream in; 263 Context context; 264 }; 265 266 TEST_CASE("CommandComplete_Simple", "[complete]") 267 { 268 CompletionTestContext ctc{ "", "winget ", "7" }; 269 270 CompletionTestCommand command; 271 command.SubCommandNames = { "test1", "test2" }; 272 command.Arguments = { Argument{ "arg1", 'a', Args::Type::Query, CLI::Resource::String::Done, ArgumentType::Standard } }; 273 command.ArgumentValueCallback = [&](Context&, Execution::Args::Type) { FAIL("No argument value should be requested"); }; 274 command.Complete(ctc.context); 275 276 // Create expected values 277 std::stringstream expected; 278 OutputAllSubCommands(command, expected); 279 OutputAllArgumentNames(command, expected); 280 281 REQUIRE(ctc.out.str() == expected.str()); 282 } 283 284 TEST_CASE("CommandComplete_PartialCommandMatch", "[complete]") 285 { 286 CompletionTestContext ctc{ "cart", "winget cart", "11" }; 287 288 CompletionTestCommand command; 289 command.SubCommandNames = { "car", "cart", "cartesian", "carpet" }; 290 command.ArgumentValueCallback = [&](Context&, Execution::Args::Type) { FAIL("No argument value should be requested"); }; 291 command.Complete(ctc.context); 292 293 // Create expected values 294 std::stringstream expected; 295 OutputAllSubCommands(command, expected, "cart"); 296 OutputAllArgumentNames(command, expected, "cart"); 297 298 REQUIRE(ctc.out.str() == expected.str()); 299 } 300 301 TEST_CASE("CommandComplete_CommandsNotAllowed", "[complete]") 302 { 303 CompletionTestContext ctc{ "", "winget foobar ", "14" }; 304 305 CompletionTestCommand command; 306 command.SubCommandNames = { "car", "cart", "cartesian", "carpet" }; 307 command.ArgumentValueCallback = [&](Context&, Execution::Args::Type) { FAIL("No argument value should be requested"); }; 308 command.Complete(ctc.context); 309 310 // Create expected values 311 std::stringstream expected; 312 OutputAllArgumentNames(command, expected); 313 314 REQUIRE(ctc.out.str() == expected.str()); 315 } 316 317 TEST_CASE("CommandComplete_Routing1", "[complete]") 318 { 319 CompletionTestContext ctc{ "", "winget --arg1 ", "14" }; 320 321 CompletionTestCommand command; 322 command.SubCommandNames = { "car", "cart", "cartesian", "carpet" }; 323 command.Arguments = { 324 Argument{ "arg1", '1', Args::Type::Query, CLI::Resource::String::Done, ArgumentType::Standard }, 325 Argument{ "arg2", '2', Args::Type::Channel, CLI::Resource::String::Done, ArgumentType::Standard }, 326 }; 327 Args::Type argType = static_cast<Args::Type>(-1); 328 command.ArgumentValueCallback = [&](Context&, Execution::Args::Type type) { argType = type; }; 329 command.Complete(ctc.context); 330 331 // Create expected values 332 std::stringstream expected; 333 334 REQUIRE(ctc.out.str() == expected.str()); 335 REQUIRE(argType == command.Arguments[0].ExecArgType()); 336 } 337 338 TEST_CASE("CommandComplete_Routing2", "[complete]") 339 { 340 CompletionTestContext ctc{ "", "winget --arg2 ", "14" }; 341 342 CompletionTestCommand command; 343 command.SubCommandNames = { "car", "cart", "cartesian", "carpet" }; 344 command.Arguments = { 345 Argument{ "arg1", '1', Args::Type::Query, CLI::Resource::String::Done, ArgumentType::Standard }, 346 Argument{ "arg2", '2', Args::Type::Channel, CLI::Resource::String::Done, ArgumentType::Standard }, 347 }; 348 Args::Type argType = static_cast<Args::Type>(-1); 349 command.ArgumentValueCallback = [&](Context&, Execution::Args::Type type) { argType = type; }; 350 command.Complete(ctc.context); 351 352 // Create expected values 353 std::stringstream expected; 354 355 REQUIRE(ctc.out.str() == expected.str()); 356 REQUIRE(argType == command.Arguments[1].ExecArgType()); 357 } 358 359 TEST_CASE("CommandComplete_PositionalRouting", "[complete]") 360 { 361 CompletionTestContext ctc{ "", "winget ", "7" }; 362 363 CompletionTestCommand command; 364 command.SubCommandNames = { "car", "cart", "cartesian", "carpet" }; 365 command.Arguments = { 366 Argument{ "arg1", '1', Args::Type::Query, CLI::Resource::String::Done, ArgumentType::Standard }, 367 Argument{ "arg2", '2', Args::Type::Channel, CLI::Resource::String::Done, ArgumentType::Positional }, 368 }; 369 Args::Type argType = static_cast<Args::Type>(-1); 370 command.ArgumentValueCallback = [&](Context&, Execution::Args::Type type) { argType = type; }; 371 command.Complete(ctc.context); 372 373 // Create expected values 374 std::stringstream expected; 375 OutputAllSubCommands(command, expected); 376 OutputAllArgumentNames(command, expected); 377 378 REQUIRE(ctc.out.str() == expected.str()); 379 REQUIRE(argType == command.Arguments[1].ExecArgType()); 380 } 381 382 TEST_CASE("CommandComplete_PositionalRoutingAfterArgs", "[complete]") 383 { 384 CompletionTestContext ctc{ "", "winget --arg1 value ", "20" }; 385 386 CompletionTestCommand command; 387 command.SubCommandNames = { "car", "cart", "cartesian", "carpet" }; 388 command.Arguments = { 389 Argument{ "arg1", '1', Args::Type::Query, CLI::Resource::String::Done, ArgumentType::Standard }, 390 Argument{ "arg2", '2', Args::Type::Channel, CLI::Resource::String::Done, ArgumentType::Positional }, 391 }; 392 Args::Type argType = static_cast<Args::Type>(-1); 393 command.ArgumentValueCallback = [&](Context&, Execution::Args::Type type) { argType = type; }; 394 command.Complete(ctc.context); 395 396 // Create expected values 397 std::stringstream expected; 398 OutputAllArgumentNames(command, expected); 399 400 REQUIRE(ctc.out.str() == expected.str()); 401 REQUIRE(argType == command.Arguments[1].ExecArgType()); 402 } 403 404 TEST_CASE("CommandComplete_PositionalRoutingAfterDoubleDash", "[complete]") 405 { 406 CompletionTestContext ctc{ "", "winget -- ", "10" }; 407 408 CompletionTestCommand command; 409 command.SubCommandNames = { "car", "cart", "cartesian", "carpet" }; 410 command.Arguments = { 411 Argument{ "arg1", '1', Args::Type::Query, CLI::Resource::String::Done, ArgumentType::Standard }, 412 Argument{ "arg2", '2', Args::Type::Channel, CLI::Resource::String::Done, ArgumentType::Positional }, 413 }; 414 Args::Type argType = static_cast<Args::Type>(-1); 415 command.ArgumentValueCallback = [&](Context&, Execution::Args::Type type) { argType = type; }; 416 command.Complete(ctc.context); 417 418 // Create expected values 419 std::stringstream expected; 420 421 REQUIRE(ctc.out.str() == expected.str()); 422 REQUIRE(argType == command.Arguments[1].ExecArgType()); 423 } 424 425 TEST_CASE("CommandComplete_ArgNamesAfterDash", "[complete]") 426 { 427 CompletionTestContext ctc{ "-", "winget -", "8" }; 428 429 CompletionTestCommand command; 430 command.SubCommandNames = { "car", "cart", "cartesian", "carpet" }; 431 command.Arguments = { 432 Argument{ "arg1", '1', Args::Type::Query, CLI::Resource::String::Done, ArgumentType::Standard }, 433 Argument{ "arg2", '2', Args::Type::Channel, CLI::Resource::String::Done, ArgumentType::Positional }, 434 }; 435 command.ArgumentValueCallback = [&](Context&, Execution::Args::Type) { FAIL("No argument value should be requested"); }; 436 command.Complete(ctc.context); 437 438 // Create expected values 439 std::stringstream expected; 440 OutputAllArgumentNames(command, expected); 441 442 REQUIRE(ctc.out.str() == expected.str()); 443 } 444 445 TEST_CASE("CommandComplete_AliasNames", "[complete]") 446 { 447 CompletionTestContext ctc{ "-a", "winget -a", "9" }; 448 449 CompletionTestCommand command; 450 command.SubCommandNames = { "car", "cart", "cartesian", "carpet" }; 451 command.Arguments = { 452 Argument{ "arg1", '1', Args::Type::Query, CLI::Resource::String::Done, ArgumentType::Standard }, 453 Argument{ "arg2", '2', Args::Type::Channel, CLI::Resource::String::Done, ArgumentType::Positional }, 454 }; 455 command.ArgumentValueCallback = [&](Context&, Execution::Args::Type) { FAIL("No argument value should be requested"); }; 456 command.Complete(ctc.context); 457 458 // Create expected values 459 std::stringstream expected; 460 OutputAllArgumentAliases(command, expected); 461 462 REQUIRE(ctc.out.str() == expected.str()); 463 } 464 465 TEST_CASE("CommandComplete_ArgNamesFilter", "[complete]") 466 { 467 CompletionTestContext ctc{ "--a", "winget --a", "10" }; 468 469 CompletionTestCommand command; 470 command.SubCommandNames = { "car", "cart", "cartesian", "carpet" }; 471 command.Arguments = { 472 Argument{ "arg1", '1', Args::Type::Query, CLI::Resource::String::Done, ArgumentType::Standard }, 473 Argument{ "arg2", '2', Args::Type::Channel, CLI::Resource::String::Done, ArgumentType::Positional }, 474 Argument{ "foo1", '2', Args::Type::Channel, CLI::Resource::String::Done, ArgumentType::Positional }, 475 }; 476 command.ArgumentValueCallback = [&](Context&, Execution::Args::Type) { FAIL("No argument value should be requested"); }; 477 command.Complete(ctc.context); 478 479 // Create expected values 480 std::stringstream expected; 481 OutputAllArgumentNames(command, expected, "a"); 482 483 REQUIRE(ctc.out.str() == expected.str()); 484 } 485 486 TEST_CASE("CommandComplete_IgnoreBadArgs", "[complete]") 487 { 488 CompletionTestContext ctc{ "", "winget foo bar --arg1 ", "22" }; 489 490 CompletionTestCommand command; 491 command.SubCommandNames = { "car", "cart", "cartesian", "carpet" }; 492 command.Arguments = { 493 Argument{ "arg1", '1', Args::Type::Query, CLI::Resource::String::Done, ArgumentType::Standard }, 494 Argument{ "arg2", '2', Args::Type::Channel, CLI::Resource::String::Done, ArgumentType::Standard }, 495 }; 496 Args::Type argType = static_cast<Args::Type>(-1); 497 command.ArgumentValueCallback = [&](Context&, Execution::Args::Type type) { argType = type; }; 498 command.Complete(ctc.context); 499 500 // Create expected values 501 std::stringstream expected; 502 503 REQUIRE(ctc.out.str() == expected.str()); 504 REQUIRE(argType == command.Arguments[0].ExecArgType()); 505 } 506 507 TEST_CASE("CommandComplete_OtherArgsParsed", "[complete]") 508 { 509 CompletionTestContext ctc{ "", "winget --arg1 value1 --arg2 value2", "21" }; 510 511 CompletionTestCommand command; 512 command.SubCommandNames = { "car", "cart", "cartesian", "carpet" }; 513 command.Arguments = { 514 Argument{ "arg1", '1', Args::Type::Query, CLI::Resource::String::Done, ArgumentType::Standard }, 515 Argument{ "arg2", '2', Args::Type::Channel, CLI::Resource::String::Done, ArgumentType::Standard }, 516 }; 517 command.ArgumentValueCallback = [&](Context&, Execution::Args::Type) { FAIL("No argument value should be requested"); }; 518 command.Complete(ctc.context); 519 520 // Create expected values 521 std::stringstream expected; 522 OutputAllArgumentNames(command, expected); 523 524 REQUIRE(ctc.out.str() == expected.str()); 525 REQUIRE(ctc.context.Args.Contains(command.Arguments[0].ExecArgType())); 526 REQUIRE(ctc.context.Args.GetArg(command.Arguments[0].ExecArgType()) == "value1"); 527 REQUIRE(ctc.context.Args.Contains(command.Arguments[1].ExecArgType())); 528 REQUIRE(ctc.context.Args.GetArg(command.Arguments[1].ExecArgType()) == "value2"); 529 } 530 531 TEST_CASE("CommandComplete_Complex", "[complete]") 532 { 533 CompletionTestContext ctc{ "", "winget foo --arg1 value1 bar junk --arg2 ", "41" }; 534 535 CompletionTestCommand command; 536 command.SubCommandNames = { "car", "cart", "cartesian", "carpet" }; 537 command.Arguments = { 538 Argument{ "arg1", '1', Args::Type::Query, CLI::Resource::String::Done, ArgumentType::Standard }, 539 Argument{ "arg2", '2', Args::Type::Channel, CLI::Resource::String::Done, ArgumentType::Standard }, 540 }; 541 Args::Type argType = static_cast<Args::Type>(-1); 542 command.ArgumentValueCallback = [&](Context&, Execution::Args::Type type) { argType = type; }; 543 command.Complete(ctc.context); 544 545 // Create expected values 546 std::stringstream expected; 547 548 REQUIRE(ctc.out.str() == expected.str()); 549 REQUIRE(argType == command.Arguments[1].ExecArgType()); 550 REQUIRE(ctc.context.Args.Contains(command.Arguments[0].ExecArgType())); 551 REQUIRE(ctc.context.Args.GetArg(command.Arguments[0].ExecArgType()) == "value1"); 552 }